From b6951a8e791c5b9d18f33c60e134c25c4642744a Mon Sep 17 00:00:00 2001 From: Angela Zhang Date: Wed, 11 Jan 2023 11:42:51 -0600 Subject: [PATCH] Initial NodeBB Commit --- .codeclimate.yml | 30 + .editorconfig | 9 + .eslintignore | 30 + .eslintrc.cjs | 70 + .gitattributes | 13 + .github/workflows/hw1.yaml | 122 + .github/workflows/lint.yaml | 83 + .github/workflows/test.yaml | 201 + .github/workflows/volunteers.yaml | 11 + .gitignore | 84 + .husky/.gitignore | 1 + .husky/commit-msg | 4 + .husky/pre-commit | 4 + .mocharc.yml | 4 + .tx/config | 3746 +++++ CHANGELOG.md | 7546 ++++++++++ Dockerfile | 25 + Gruntfile.js | 206 + LICENSE | 674 + README.md | 87 + app.js | 82 + build/.gitignore | 4 + build/export/.gitignore | 3 + build/export/README | 5 + commitlint.config.js | 26 + docker-compose.yml | 24 + install/data/categories.json | 38 + install/data/defaults.json | 185 + install/data/footer.json | 10 + install/data/navigation.json | 77 + install/data/welcome.md | 10 + install/databases.js | 87 + install/package.json | 200 + install/web.js | 286 + loader.js | 249 + nodebb | 5 + nodebb.bat | 1 + public/.eslintrc | 3 + public/503.html | 177 + public/favicon.ico | Bin 0 -> 1150 bytes public/images/cover-default.png | Bin 0 -> 33702 bytes public/images/logo.png | Bin 0 -> 13618 bytes public/images/logo.svg | 16 + public/images/logo@3x.png | Bin 0 -> 61551 bytes public/images/sm-card.png | Bin 0 -> 220595 bytes public/images/themes/default.png | Bin 0 -> 3559 bytes public/images/touch/144.png | Bin 0 -> 6615 bytes public/images/touch/192.png | Bin 0 -> 10163 bytes public/images/touch/36.png | Bin 0 -> 1521 bytes public/images/touch/48.png | Bin 0 -> 2013 bytes public/images/touch/512.png | Bin 0 -> 78334 bytes public/images/touch/72.png | Bin 0 -> 3037 bytes public/images/touch/96.png | Bin 0 -> 4110 bytes public/images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 86 bytes public/images/ui-icons_444444_256x240.png | Bin 0 -> 3756 bytes public/images/ui-icons_555555_256x240.png | Bin 0 -> 3756 bytes public/images/ui-icons_777620_256x240.png | Bin 0 -> 3756 bytes public/images/ui-icons_777777_256x240.png | Bin 0 -> 3756 bytes public/images/ui-icons_cc0000_256x240.png | Bin 0 -> 3756 bytes public/images/ui-icons_ffffff_256x240.png | Bin 0 -> 3756 bytes public/language/README.md | 14 + public/language/ar/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/ar/admin/admin.json | 11 + public/language/ar/admin/advanced/cache.json | 9 + .../language/ar/admin/advanced/database.json | 52 + public/language/ar/admin/advanced/errors.json | 14 + public/language/ar/admin/advanced/events.json | 13 + public/language/ar/admin/advanced/logs.json | 7 + .../ar/admin/appearance/customise.json | 16 + .../language/ar/admin/appearance/skins.json | 9 + .../language/ar/admin/appearance/themes.json | 11 + public/language/ar/admin/dashboard.json | 90 + .../language/ar/admin/development/info.json | 25 + .../language/ar/admin/development/logger.json | 12 + public/language/ar/admin/extend/plugins.json | 57 + public/language/ar/admin/extend/rewards.json | 15 + public/language/ar/admin/extend/widgets.json | 30 + .../language/ar/admin/manage/admins-mods.json | 12 + .../language/ar/admin/manage/categories.json | 92 + public/language/ar/admin/manage/digest.json | 22 + public/language/ar/admin/manage/groups.json | 44 + .../language/ar/admin/manage/privileges.json | 64 + .../ar/admin/manage/registration.json | 20 + public/language/ar/admin/manage/tags.json | 18 + public/language/ar/admin/manage/uploads.json | 11 + public/language/ar/admin/manage/users.json | 112 + public/language/ar/admin/menu.json | 89 + .../language/ar/admin/settings/advanced.json | 50 + public/language/ar/admin/settings/api.json | 16 + public/language/ar/admin/settings/chat.json | 12 + .../language/ar/admin/settings/cookies.json | 13 + public/language/ar/admin/settings/email.json | 52 + .../language/ar/admin/settings/general.json | 50 + public/language/ar/admin/settings/group.json | 13 + public/language/ar/admin/settings/guest.json | 7 + .../language/ar/admin/settings/homepage.json | 8 + .../language/ar/admin/settings/languages.json | 6 + .../ar/admin/settings/navigation.json | 25 + .../ar/admin/settings/notifications.json | 7 + .../ar/admin/settings/pagination.json | 12 + public/language/ar/admin/settings/post.json | 67 + .../ar/admin/settings/reputation.json | 31 + public/language/ar/admin/settings/social.json | 5 + .../language/ar/admin/settings/sockets.json | 6 + public/language/ar/admin/settings/sounds.json | 9 + public/language/ar/admin/settings/tags.json | 12 + .../language/ar/admin/settings/uploads.json | 45 + public/language/ar/admin/settings/user.json | 83 + .../ar/admin/settings/web-crawler.json | 10 + public/language/ar/category.json | 23 + public/language/ar/email.json | 58 + public/language/ar/error.json | 224 + public/language/ar/flags.json | 89 + public/language/ar/global.json | 126 + public/language/ar/groups.json | 64 + public/language/ar/ip-blacklist.json | 19 + public/language/ar/language.json | 5 + public/language/ar/login.json | 12 + public/language/ar/modules.json | 82 + public/language/ar/notifications.json | 76 + public/language/ar/pages.json | 65 + public/language/ar/post-queue.json | 31 + public/language/ar/recent.json | 19 + public/language/ar/register.json | 32 + public/language/ar/reset_password.json | 18 + public/language/ar/search.json | 49 + public/language/ar/success.json | 7 + public/language/ar/tags.json | 8 + public/language/ar/top.json | 4 + public/language/ar/topic.json | 188 + public/language/ar/unread.json | 15 + public/language/ar/uploads.json | 9 + public/language/ar/user.json | 199 + public/language/ar/users.json | 24 + public/language/bg/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/bg/admin/admin.json | 11 + public/language/bg/admin/advanced/cache.json | 9 + .../language/bg/admin/advanced/database.json | 52 + public/language/bg/admin/advanced/errors.json | 14 + public/language/bg/admin/advanced/events.json | 13 + public/language/bg/admin/advanced/logs.json | 7 + .../bg/admin/appearance/customise.json | 16 + .../language/bg/admin/appearance/skins.json | 9 + .../language/bg/admin/appearance/themes.json | 11 + public/language/bg/admin/dashboard.json | 90 + .../language/bg/admin/development/info.json | 25 + .../language/bg/admin/development/logger.json | 12 + public/language/bg/admin/extend/plugins.json | 57 + public/language/bg/admin/extend/rewards.json | 15 + public/language/bg/admin/extend/widgets.json | 30 + .../language/bg/admin/manage/admins-mods.json | 12 + .../language/bg/admin/manage/categories.json | 92 + public/language/bg/admin/manage/digest.json | 22 + public/language/bg/admin/manage/groups.json | 44 + .../language/bg/admin/manage/privileges.json | 64 + .../bg/admin/manage/registration.json | 20 + public/language/bg/admin/manage/tags.json | 18 + public/language/bg/admin/manage/uploads.json | 11 + public/language/bg/admin/manage/users.json | 112 + public/language/bg/admin/menu.json | 89 + .../language/bg/admin/settings/advanced.json | 50 + public/language/bg/admin/settings/api.json | 16 + public/language/bg/admin/settings/chat.json | 12 + .../language/bg/admin/settings/cookies.json | 13 + public/language/bg/admin/settings/email.json | 52 + .../language/bg/admin/settings/general.json | 50 + public/language/bg/admin/settings/group.json | 13 + public/language/bg/admin/settings/guest.json | 7 + .../language/bg/admin/settings/homepage.json | 8 + .../language/bg/admin/settings/languages.json | 6 + .../bg/admin/settings/navigation.json | 25 + .../bg/admin/settings/notifications.json | 7 + .../bg/admin/settings/pagination.json | 12 + public/language/bg/admin/settings/post.json | 67 + .../bg/admin/settings/reputation.json | 31 + public/language/bg/admin/settings/social.json | 5 + .../language/bg/admin/settings/sockets.json | 6 + public/language/bg/admin/settings/sounds.json | 9 + public/language/bg/admin/settings/tags.json | 12 + .../language/bg/admin/settings/uploads.json | 45 + public/language/bg/admin/settings/user.json | 83 + .../bg/admin/settings/web-crawler.json | 10 + public/language/bg/category.json | 23 + public/language/bg/email.json | 58 + public/language/bg/error.json | 224 + public/language/bg/flags.json | 89 + public/language/bg/global.json | 126 + public/language/bg/groups.json | 64 + public/language/bg/ip-blacklist.json | 19 + public/language/bg/language.json | 5 + public/language/bg/login.json | 12 + public/language/bg/modules.json | 82 + public/language/bg/notifications.json | 76 + public/language/bg/pages.json | 65 + public/language/bg/post-queue.json | 31 + public/language/bg/recent.json | 19 + public/language/bg/register.json | 32 + public/language/bg/reset_password.json | 18 + public/language/bg/search.json | 49 + public/language/bg/success.json | 7 + public/language/bg/tags.json | 8 + public/language/bg/top.json | 4 + public/language/bg/topic.json | 188 + public/language/bg/unread.json | 15 + public/language/bg/uploads.json | 9 + public/language/bg/user.json | 199 + public/language/bg/users.json | 24 + public/language/bn/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/bn/admin/admin.json | 11 + public/language/bn/admin/advanced/cache.json | 9 + .../language/bn/admin/advanced/database.json | 52 + public/language/bn/admin/advanced/errors.json | 14 + public/language/bn/admin/advanced/events.json | 13 + public/language/bn/admin/advanced/logs.json | 7 + .../bn/admin/appearance/customise.json | 16 + .../language/bn/admin/appearance/skins.json | 9 + .../language/bn/admin/appearance/themes.json | 11 + public/language/bn/admin/dashboard.json | 90 + .../language/bn/admin/development/info.json | 25 + .../language/bn/admin/development/logger.json | 12 + public/language/bn/admin/extend/plugins.json | 57 + public/language/bn/admin/extend/rewards.json | 15 + public/language/bn/admin/extend/widgets.json | 30 + .../language/bn/admin/manage/admins-mods.json | 12 + .../language/bn/admin/manage/categories.json | 92 + public/language/bn/admin/manage/digest.json | 22 + public/language/bn/admin/manage/groups.json | 44 + .../language/bn/admin/manage/privileges.json | 64 + .../bn/admin/manage/registration.json | 20 + public/language/bn/admin/manage/tags.json | 18 + public/language/bn/admin/manage/uploads.json | 11 + public/language/bn/admin/manage/users.json | 112 + public/language/bn/admin/menu.json | 89 + .../language/bn/admin/settings/advanced.json | 50 + public/language/bn/admin/settings/api.json | 16 + public/language/bn/admin/settings/chat.json | 12 + .../language/bn/admin/settings/cookies.json | 13 + public/language/bn/admin/settings/email.json | 52 + .../language/bn/admin/settings/general.json | 50 + public/language/bn/admin/settings/group.json | 13 + public/language/bn/admin/settings/guest.json | 7 + .../language/bn/admin/settings/homepage.json | 8 + .../language/bn/admin/settings/languages.json | 6 + .../bn/admin/settings/navigation.json | 25 + .../bn/admin/settings/notifications.json | 7 + .../bn/admin/settings/pagination.json | 12 + public/language/bn/admin/settings/post.json | 67 + .../bn/admin/settings/reputation.json | 31 + public/language/bn/admin/settings/social.json | 5 + .../language/bn/admin/settings/sockets.json | 6 + public/language/bn/admin/settings/sounds.json | 9 + public/language/bn/admin/settings/tags.json | 12 + .../language/bn/admin/settings/uploads.json | 45 + public/language/bn/admin/settings/user.json | 83 + .../bn/admin/settings/web-crawler.json | 10 + public/language/bn/category.json | 23 + public/language/bn/email.json | 58 + public/language/bn/error.json | 224 + public/language/bn/flags.json | 89 + public/language/bn/global.json | 126 + public/language/bn/groups.json | 64 + public/language/bn/ip-blacklist.json | 19 + public/language/bn/language.json | 5 + public/language/bn/login.json | 12 + public/language/bn/modules.json | 82 + public/language/bn/notifications.json | 76 + public/language/bn/pages.json | 65 + public/language/bn/post-queue.json | 31 + public/language/bn/recent.json | 19 + public/language/bn/register.json | 32 + public/language/bn/reset_password.json | 18 + public/language/bn/search.json | 49 + public/language/bn/success.json | 7 + public/language/bn/tags.json | 8 + public/language/bn/top.json | 4 + public/language/bn/topic.json | 188 + public/language/bn/unread.json | 15 + public/language/bn/uploads.json | 9 + public/language/bn/user.json | 199 + public/language/bn/users.json | 24 + public/language/cs/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/cs/admin/admin.json | 11 + public/language/cs/admin/advanced/cache.json | 9 + .../language/cs/admin/advanced/database.json | 52 + public/language/cs/admin/advanced/errors.json | 14 + public/language/cs/admin/advanced/events.json | 13 + public/language/cs/admin/advanced/logs.json | 7 + .../cs/admin/appearance/customise.json | 16 + .../language/cs/admin/appearance/skins.json | 9 + .../language/cs/admin/appearance/themes.json | 11 + public/language/cs/admin/dashboard.json | 90 + .../language/cs/admin/development/info.json | 25 + .../language/cs/admin/development/logger.json | 12 + public/language/cs/admin/extend/plugins.json | 57 + public/language/cs/admin/extend/rewards.json | 15 + public/language/cs/admin/extend/widgets.json | 30 + .../language/cs/admin/manage/admins-mods.json | 12 + .../language/cs/admin/manage/categories.json | 92 + public/language/cs/admin/manage/digest.json | 22 + public/language/cs/admin/manage/groups.json | 44 + .../language/cs/admin/manage/privileges.json | 64 + .../cs/admin/manage/registration.json | 20 + public/language/cs/admin/manage/tags.json | 18 + public/language/cs/admin/manage/uploads.json | 11 + public/language/cs/admin/manage/users.json | 112 + public/language/cs/admin/menu.json | 89 + .../language/cs/admin/settings/advanced.json | 50 + public/language/cs/admin/settings/api.json | 16 + public/language/cs/admin/settings/chat.json | 12 + .../language/cs/admin/settings/cookies.json | 13 + public/language/cs/admin/settings/email.json | 52 + .../language/cs/admin/settings/general.json | 50 + public/language/cs/admin/settings/group.json | 13 + public/language/cs/admin/settings/guest.json | 7 + .../language/cs/admin/settings/homepage.json | 8 + .../language/cs/admin/settings/languages.json | 6 + .../cs/admin/settings/navigation.json | 25 + .../cs/admin/settings/notifications.json | 7 + .../cs/admin/settings/pagination.json | 12 + public/language/cs/admin/settings/post.json | 67 + .../cs/admin/settings/reputation.json | 31 + public/language/cs/admin/settings/social.json | 5 + .../language/cs/admin/settings/sockets.json | 6 + public/language/cs/admin/settings/sounds.json | 9 + public/language/cs/admin/settings/tags.json | 12 + .../language/cs/admin/settings/uploads.json | 45 + public/language/cs/admin/settings/user.json | 83 + .../cs/admin/settings/web-crawler.json | 10 + public/language/cs/category.json | 23 + public/language/cs/email.json | 58 + public/language/cs/error.json | 224 + public/language/cs/flags.json | 89 + public/language/cs/global.json | 126 + public/language/cs/groups.json | 64 + public/language/cs/ip-blacklist.json | 19 + public/language/cs/language.json | 5 + public/language/cs/login.json | 12 + public/language/cs/modules.json | 82 + public/language/cs/notifications.json | 76 + public/language/cs/pages.json | 65 + public/language/cs/post-queue.json | 31 + public/language/cs/recent.json | 19 + public/language/cs/register.json | 32 + public/language/cs/reset_password.json | 18 + public/language/cs/search.json | 49 + public/language/cs/success.json | 7 + public/language/cs/tags.json | 8 + public/language/cs/top.json | 4 + public/language/cs/topic.json | 188 + public/language/cs/unread.json | 15 + public/language/cs/uploads.json | 9 + public/language/cs/user.json | 199 + public/language/cs/users.json | 24 + public/language/da/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/da/admin/admin.json | 11 + public/language/da/admin/advanced/cache.json | 9 + .../language/da/admin/advanced/database.json | 52 + public/language/da/admin/advanced/errors.json | 14 + public/language/da/admin/advanced/events.json | 13 + public/language/da/admin/advanced/logs.json | 7 + .../da/admin/appearance/customise.json | 16 + .../language/da/admin/appearance/skins.json | 9 + .../language/da/admin/appearance/themes.json | 11 + public/language/da/admin/dashboard.json | 90 + .../language/da/admin/development/info.json | 25 + .../language/da/admin/development/logger.json | 12 + public/language/da/admin/extend/plugins.json | 57 + public/language/da/admin/extend/rewards.json | 15 + public/language/da/admin/extend/widgets.json | 30 + .../language/da/admin/manage/admins-mods.json | 12 + .../language/da/admin/manage/categories.json | 92 + public/language/da/admin/manage/digest.json | 22 + public/language/da/admin/manage/groups.json | 44 + .../language/da/admin/manage/privileges.json | 64 + .../da/admin/manage/registration.json | 20 + public/language/da/admin/manage/tags.json | 18 + public/language/da/admin/manage/uploads.json | 11 + public/language/da/admin/manage/users.json | 112 + public/language/da/admin/menu.json | 89 + .../language/da/admin/settings/advanced.json | 50 + public/language/da/admin/settings/api.json | 16 + public/language/da/admin/settings/chat.json | 12 + .../language/da/admin/settings/cookies.json | 13 + public/language/da/admin/settings/email.json | 52 + .../language/da/admin/settings/general.json | 50 + public/language/da/admin/settings/group.json | 13 + public/language/da/admin/settings/guest.json | 7 + .../language/da/admin/settings/homepage.json | 8 + .../language/da/admin/settings/languages.json | 6 + .../da/admin/settings/navigation.json | 25 + .../da/admin/settings/notifications.json | 7 + .../da/admin/settings/pagination.json | 12 + public/language/da/admin/settings/post.json | 67 + .../da/admin/settings/reputation.json | 31 + public/language/da/admin/settings/social.json | 5 + .../language/da/admin/settings/sockets.json | 6 + public/language/da/admin/settings/sounds.json | 9 + public/language/da/admin/settings/tags.json | 12 + .../language/da/admin/settings/uploads.json | 45 + public/language/da/admin/settings/user.json | 83 + .../da/admin/settings/web-crawler.json | 10 + public/language/da/category.json | 23 + public/language/da/email.json | 58 + public/language/da/error.json | 224 + public/language/da/flags.json | 89 + public/language/da/global.json | 126 + public/language/da/groups.json | 64 + public/language/da/ip-blacklist.json | 19 + public/language/da/language.json | 5 + public/language/da/login.json | 12 + public/language/da/modules.json | 82 + public/language/da/notifications.json | 76 + public/language/da/pages.json | 65 + public/language/da/post-queue.json | 31 + public/language/da/recent.json | 19 + public/language/da/register.json | 32 + public/language/da/reset_password.json | 18 + public/language/da/search.json | 49 + public/language/da/success.json | 7 + public/language/da/tags.json | 8 + public/language/da/top.json | 4 + public/language/da/topic.json | 188 + public/language/da/unread.json | 15 + public/language/da/uploads.json | 9 + public/language/da/user.json | 199 + public/language/da/users.json | 24 + public/language/de/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/de/admin/admin.json | 11 + public/language/de/admin/advanced/cache.json | 9 + .../language/de/admin/advanced/database.json | 52 + public/language/de/admin/advanced/errors.json | 14 + public/language/de/admin/advanced/events.json | 13 + public/language/de/admin/advanced/logs.json | 7 + .../de/admin/appearance/customise.json | 16 + .../language/de/admin/appearance/skins.json | 9 + .../language/de/admin/appearance/themes.json | 11 + public/language/de/admin/dashboard.json | 90 + .../language/de/admin/development/info.json | 25 + .../language/de/admin/development/logger.json | 12 + public/language/de/admin/extend/plugins.json | 57 + public/language/de/admin/extend/rewards.json | 15 + public/language/de/admin/extend/widgets.json | 30 + .../language/de/admin/manage/admins-mods.json | 12 + .../language/de/admin/manage/categories.json | 92 + public/language/de/admin/manage/digest.json | 22 + public/language/de/admin/manage/groups.json | 44 + .../language/de/admin/manage/privileges.json | 64 + .../de/admin/manage/registration.json | 20 + public/language/de/admin/manage/tags.json | 18 + public/language/de/admin/manage/uploads.json | 11 + public/language/de/admin/manage/users.json | 112 + public/language/de/admin/menu.json | 89 + .../language/de/admin/settings/advanced.json | 50 + public/language/de/admin/settings/api.json | 16 + public/language/de/admin/settings/chat.json | 12 + .../language/de/admin/settings/cookies.json | 13 + public/language/de/admin/settings/email.json | 52 + .../language/de/admin/settings/general.json | 50 + public/language/de/admin/settings/group.json | 13 + public/language/de/admin/settings/guest.json | 7 + .../language/de/admin/settings/homepage.json | 8 + .../language/de/admin/settings/languages.json | 6 + .../de/admin/settings/navigation.json | 25 + .../de/admin/settings/notifications.json | 7 + .../de/admin/settings/pagination.json | 12 + public/language/de/admin/settings/post.json | 67 + .../de/admin/settings/reputation.json | 31 + public/language/de/admin/settings/social.json | 5 + .../language/de/admin/settings/sockets.json | 6 + public/language/de/admin/settings/sounds.json | 9 + public/language/de/admin/settings/tags.json | 12 + .../language/de/admin/settings/uploads.json | 45 + public/language/de/admin/settings/user.json | 83 + .../de/admin/settings/web-crawler.json | 10 + public/language/de/category.json | 23 + public/language/de/email.json | 58 + public/language/de/error.json | 224 + public/language/de/flags.json | 89 + public/language/de/global.json | 126 + public/language/de/groups.json | 64 + public/language/de/ip-blacklist.json | 19 + public/language/de/language.json | 5 + public/language/de/login.json | 12 + public/language/de/modules.json | 82 + public/language/de/notifications.json | 76 + public/language/de/pages.json | 65 + public/language/de/post-queue.json | 31 + public/language/de/recent.json | 19 + public/language/de/register.json | 32 + public/language/de/reset_password.json | 18 + public/language/de/search.json | 49 + public/language/de/success.json | 7 + public/language/de/tags.json | 8 + public/language/de/top.json | 4 + public/language/de/topic.json | 188 + public/language/de/unread.json | 15 + public/language/de/uploads.json | 9 + public/language/de/user.json | 199 + public/language/de/users.json | 24 + public/language/el/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/el/admin/admin.json | 11 + public/language/el/admin/advanced/cache.json | 9 + .../language/el/admin/advanced/database.json | 52 + public/language/el/admin/advanced/errors.json | 14 + public/language/el/admin/advanced/events.json | 13 + public/language/el/admin/advanced/logs.json | 7 + .../el/admin/appearance/customise.json | 16 + .../language/el/admin/appearance/skins.json | 9 + .../language/el/admin/appearance/themes.json | 11 + public/language/el/admin/dashboard.json | 90 + .../language/el/admin/development/info.json | 25 + .../language/el/admin/development/logger.json | 12 + public/language/el/admin/extend/plugins.json | 57 + public/language/el/admin/extend/rewards.json | 15 + public/language/el/admin/extend/widgets.json | 30 + .../language/el/admin/manage/admins-mods.json | 12 + .../language/el/admin/manage/categories.json | 92 + public/language/el/admin/manage/digest.json | 22 + public/language/el/admin/manage/groups.json | 44 + .../language/el/admin/manage/privileges.json | 64 + .../el/admin/manage/registration.json | 20 + public/language/el/admin/manage/tags.json | 18 + public/language/el/admin/manage/uploads.json | 11 + public/language/el/admin/manage/users.json | 112 + public/language/el/admin/menu.json | 89 + .../language/el/admin/settings/advanced.json | 50 + public/language/el/admin/settings/api.json | 16 + public/language/el/admin/settings/chat.json | 12 + .../language/el/admin/settings/cookies.json | 13 + public/language/el/admin/settings/email.json | 52 + .../language/el/admin/settings/general.json | 50 + public/language/el/admin/settings/group.json | 13 + public/language/el/admin/settings/guest.json | 7 + .../language/el/admin/settings/homepage.json | 8 + .../language/el/admin/settings/languages.json | 6 + .../el/admin/settings/navigation.json | 25 + .../el/admin/settings/notifications.json | 7 + .../el/admin/settings/pagination.json | 12 + public/language/el/admin/settings/post.json | 67 + .../el/admin/settings/reputation.json | 31 + public/language/el/admin/settings/social.json | 5 + .../language/el/admin/settings/sockets.json | 6 + public/language/el/admin/settings/sounds.json | 9 + public/language/el/admin/settings/tags.json | 12 + .../language/el/admin/settings/uploads.json | 45 + public/language/el/admin/settings/user.json | 83 + .../el/admin/settings/web-crawler.json | 10 + public/language/el/category.json | 23 + public/language/el/email.json | 58 + public/language/el/error.json | 224 + public/language/el/flags.json | 89 + public/language/el/global.json | 126 + public/language/el/groups.json | 64 + public/language/el/ip-blacklist.json | 19 + public/language/el/language.json | 5 + public/language/el/login.json | 12 + public/language/el/modules.json | 82 + public/language/el/notifications.json | 76 + public/language/el/pages.json | 65 + public/language/el/post-queue.json | 31 + public/language/el/recent.json | 19 + public/language/el/register.json | 32 + public/language/el/reset_password.json | 18 + public/language/el/search.json | 49 + public/language/el/success.json | 7 + public/language/el/tags.json | 8 + public/language/el/top.json | 4 + public/language/el/topic.json | 188 + public/language/el/unread.json | 15 + public/language/el/uploads.json | 9 + public/language/el/user.json | 199 + public/language/el/users.json | 24 + .../language/en-GB/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/en-GB/admin/admin.json | 11 + .../language/en-GB/admin/advanced/cache.json | 9 + .../en-GB/admin/advanced/database.json | 52 + .../language/en-GB/admin/advanced/errors.json | 14 + .../language/en-GB/admin/advanced/events.json | 13 + .../language/en-GB/admin/advanced/logs.json | 7 + .../en-GB/admin/appearance/customise.json | 16 + .../en-GB/admin/appearance/skins.json | 9 + .../en-GB/admin/appearance/themes.json | 11 + public/language/en-GB/admin/dashboard.json | 90 + .../en-GB/admin/development/info.json | 25 + .../en-GB/admin/development/logger.json | 12 + .../language/en-GB/admin/extend/plugins.json | 57 + .../language/en-GB/admin/extend/rewards.json | 15 + .../language/en-GB/admin/extend/widgets.json | 30 + .../en-GB/admin/manage/admins-mods.json | 12 + .../en-GB/admin/manage/categories.json | 92 + .../language/en-GB/admin/manage/digest.json | 22 + .../language/en-GB/admin/manage/groups.json | 44 + .../en-GB/admin/manage/privileges.json | 64 + .../en-GB/admin/manage/registration.json | 20 + public/language/en-GB/admin/manage/tags.json | 18 + .../language/en-GB/admin/manage/uploads.json | 11 + public/language/en-GB/admin/manage/users.json | 112 + public/language/en-GB/admin/menu.json | 89 + .../en-GB/admin/settings/advanced.json | 50 + public/language/en-GB/admin/settings/api.json | 16 + .../language/en-GB/admin/settings/chat.json | 12 + .../en-GB/admin/settings/cookies.json | 13 + .../language/en-GB/admin/settings/email.json | 52 + .../en-GB/admin/settings/general.json | 50 + .../language/en-GB/admin/settings/group.json | 13 + .../language/en-GB/admin/settings/guest.json | 7 + .../en-GB/admin/settings/homepage.json | 8 + .../en-GB/admin/settings/languages.json | 6 + .../en-GB/admin/settings/navigation.json | 25 + .../en-GB/admin/settings/notifications.json | 7 + .../en-GB/admin/settings/pagination.json | 12 + .../language/en-GB/admin/settings/post.json | 67 + .../en-GB/admin/settings/reputation.json | 31 + .../language/en-GB/admin/settings/social.json | 5 + .../en-GB/admin/settings/sockets.json | 6 + .../language/en-GB/admin/settings/sounds.json | 9 + .../language/en-GB/admin/settings/tags.json | 12 + .../en-GB/admin/settings/uploads.json | 45 + .../language/en-GB/admin/settings/user.json | 83 + .../en-GB/admin/settings/web-crawler.json | 10 + public/language/en-GB/category.json | 28 + public/language/en-GB/email.json | 73 + public/language/en-GB/error.json | 263 + public/language/en-GB/flags.json | 89 + public/language/en-GB/global.json | 155 + public/language/en-GB/groups.json | 72 + public/language/en-GB/ip-blacklist.json | 19 + public/language/en-GB/language.json | 5 + public/language/en-GB/login.json | 12 + public/language/en-GB/modules.json | 88 + public/language/en-GB/notifications.json | 82 + public/language/en-GB/pages.json | 74 + public/language/en-GB/post-queue.json | 31 + public/language/en-GB/recent.json | 24 + public/language/en-GB/register.json | 33 + public/language/en-GB/reset_password.json | 18 + public/language/en-GB/search.json | 49 + public/language/en-GB/success.json | 7 + public/language/en-GB/tags.json | 8 + public/language/en-GB/top.json | 4 + public/language/en-GB/topic.json | 215 + public/language/en-GB/unread.json | 15 + public/language/en-GB/uploads.json | 9 + public/language/en-GB/user.json | 221 + public/language/en-GB/users.json | 24 + .../language/en-US/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/en-US/admin/admin.json | 11 + .../language/en-US/admin/advanced/cache.json | 9 + .../en-US/admin/advanced/database.json | 52 + .../language/en-US/admin/advanced/errors.json | 14 + .../language/en-US/admin/advanced/events.json | 13 + .../language/en-US/admin/advanced/logs.json | 7 + .../en-US/admin/appearance/customise.json | 16 + .../en-US/admin/appearance/skins.json | 9 + .../en-US/admin/appearance/themes.json | 11 + public/language/en-US/admin/dashboard.json | 90 + .../en-US/admin/development/info.json | 25 + .../en-US/admin/development/logger.json | 12 + .../language/en-US/admin/extend/plugins.json | 57 + .../language/en-US/admin/extend/rewards.json | 15 + .../language/en-US/admin/extend/widgets.json | 30 + .../en-US/admin/manage/admins-mods.json | 12 + .../en-US/admin/manage/categories.json | 92 + .../language/en-US/admin/manage/digest.json | 22 + .../language/en-US/admin/manage/groups.json | 44 + .../en-US/admin/manage/privileges.json | 64 + .../en-US/admin/manage/registration.json | 20 + public/language/en-US/admin/manage/tags.json | 18 + .../language/en-US/admin/manage/uploads.json | 11 + public/language/en-US/admin/manage/users.json | 112 + public/language/en-US/admin/menu.json | 89 + .../en-US/admin/settings/advanced.json | 50 + public/language/en-US/admin/settings/api.json | 16 + .../language/en-US/admin/settings/chat.json | 12 + .../en-US/admin/settings/cookies.json | 13 + .../language/en-US/admin/settings/email.json | 52 + .../en-US/admin/settings/general.json | 50 + .../language/en-US/admin/settings/group.json | 13 + .../language/en-US/admin/settings/guest.json | 7 + .../en-US/admin/settings/homepage.json | 8 + .../en-US/admin/settings/languages.json | 6 + .../en-US/admin/settings/navigation.json | 25 + .../en-US/admin/settings/notifications.json | 7 + .../en-US/admin/settings/pagination.json | 12 + .../language/en-US/admin/settings/post.json | 67 + .../en-US/admin/settings/reputation.json | 31 + .../language/en-US/admin/settings/social.json | 5 + .../en-US/admin/settings/sockets.json | 6 + .../language/en-US/admin/settings/sounds.json | 9 + .../language/en-US/admin/settings/tags.json | 12 + .../en-US/admin/settings/uploads.json | 45 + .../language/en-US/admin/settings/user.json | 83 + .../en-US/admin/settings/web-crawler.json | 10 + public/language/en-US/category.json | 23 + public/language/en-US/email.json | 58 + public/language/en-US/error.json | 224 + public/language/en-US/flags.json | 89 + public/language/en-US/global.json | 126 + public/language/en-US/groups.json | 64 + public/language/en-US/ip-blacklist.json | 19 + public/language/en-US/language.json | 5 + public/language/en-US/login.json | 12 + public/language/en-US/modules.json | 82 + public/language/en-US/notifications.json | 76 + public/language/en-US/pages.json | 65 + public/language/en-US/post-queue.json | 31 + public/language/en-US/recent.json | 19 + public/language/en-US/register.json | 32 + public/language/en-US/reset_password.json | 18 + public/language/en-US/search.json | 49 + public/language/en-US/success.json | 7 + public/language/en-US/tags.json | 8 + public/language/en-US/top.json | 4 + public/language/en-US/topic.json | 188 + public/language/en-US/unread.json | 15 + public/language/en-US/uploads.json | 9 + public/language/en-US/user.json | 199 + public/language/en-US/users.json | 24 + .../en-x-pirate/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/en-x-pirate/admin/admin.json | 11 + .../en-x-pirate/admin/advanced/cache.json | 9 + .../en-x-pirate/admin/advanced/database.json | 52 + .../en-x-pirate/admin/advanced/errors.json | 14 + .../en-x-pirate/admin/advanced/events.json | 13 + .../en-x-pirate/admin/advanced/logs.json | 7 + .../admin/appearance/customise.json | 16 + .../en-x-pirate/admin/appearance/skins.json | 9 + .../en-x-pirate/admin/appearance/themes.json | 11 + .../language/en-x-pirate/admin/dashboard.json | 90 + .../en-x-pirate/admin/development/info.json | 25 + .../en-x-pirate/admin/development/logger.json | 12 + .../en-x-pirate/admin/extend/plugins.json | 57 + .../en-x-pirate/admin/extend/rewards.json | 15 + .../en-x-pirate/admin/extend/widgets.json | 30 + .../en-x-pirate/admin/manage/admins-mods.json | 12 + .../en-x-pirate/admin/manage/categories.json | 92 + .../en-x-pirate/admin/manage/digest.json | 22 + .../en-x-pirate/admin/manage/groups.json | 44 + .../en-x-pirate/admin/manage/privileges.json | 64 + .../admin/manage/registration.json | 20 + .../en-x-pirate/admin/manage/tags.json | 18 + .../en-x-pirate/admin/manage/uploads.json | 11 + .../en-x-pirate/admin/manage/users.json | 112 + public/language/en-x-pirate/admin/menu.json | 89 + .../en-x-pirate/admin/settings/advanced.json | 50 + .../en-x-pirate/admin/settings/api.json | 16 + .../en-x-pirate/admin/settings/chat.json | 12 + .../en-x-pirate/admin/settings/cookies.json | 13 + .../en-x-pirate/admin/settings/email.json | 52 + .../en-x-pirate/admin/settings/general.json | 50 + .../en-x-pirate/admin/settings/group.json | 13 + .../en-x-pirate/admin/settings/guest.json | 7 + .../en-x-pirate/admin/settings/homepage.json | 8 + .../en-x-pirate/admin/settings/languages.json | 6 + .../admin/settings/navigation.json | 25 + .../admin/settings/notifications.json | 7 + .../admin/settings/pagination.json | 12 + .../en-x-pirate/admin/settings/post.json | 67 + .../admin/settings/reputation.json | 31 + .../en-x-pirate/admin/settings/social.json | 5 + .../en-x-pirate/admin/settings/sockets.json | 6 + .../en-x-pirate/admin/settings/sounds.json | 9 + .../en-x-pirate/admin/settings/tags.json | 12 + .../en-x-pirate/admin/settings/uploads.json | 45 + .../en-x-pirate/admin/settings/user.json | 83 + .../admin/settings/web-crawler.json | 10 + public/language/en-x-pirate/category.json | 23 + public/language/en-x-pirate/email.json | 58 + public/language/en-x-pirate/error.json | 224 + public/language/en-x-pirate/flags.json | 89 + public/language/en-x-pirate/global.json | 126 + public/language/en-x-pirate/groups.json | 64 + public/language/en-x-pirate/ip-blacklist.json | 19 + public/language/en-x-pirate/language.json | 5 + public/language/en-x-pirate/login.json | 12 + public/language/en-x-pirate/modules.json | 82 + .../language/en-x-pirate/notifications.json | 76 + public/language/en-x-pirate/pages.json | 65 + public/language/en-x-pirate/post-queue.json | 31 + public/language/en-x-pirate/recent.json | 19 + public/language/en-x-pirate/register.json | 32 + .../language/en-x-pirate/reset_password.json | 18 + public/language/en-x-pirate/search.json | 49 + public/language/en-x-pirate/success.json | 7 + public/language/en-x-pirate/tags.json | 8 + public/language/en-x-pirate/top.json | 4 + public/language/en-x-pirate/topic.json | 188 + public/language/en-x-pirate/unread.json | 15 + public/language/en-x-pirate/uploads.json | 9 + public/language/en-x-pirate/user.json | 199 + public/language/en-x-pirate/users.json | 24 + public/language/es/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/es/admin/admin.json | 11 + public/language/es/admin/advanced/cache.json | 9 + .../language/es/admin/advanced/database.json | 52 + public/language/es/admin/advanced/errors.json | 14 + public/language/es/admin/advanced/events.json | 13 + public/language/es/admin/advanced/logs.json | 7 + .../es/admin/appearance/customise.json | 16 + .../language/es/admin/appearance/skins.json | 9 + .../language/es/admin/appearance/themes.json | 11 + public/language/es/admin/dashboard.json | 90 + .../language/es/admin/development/info.json | 25 + .../language/es/admin/development/logger.json | 12 + public/language/es/admin/extend/plugins.json | 57 + public/language/es/admin/extend/rewards.json | 15 + public/language/es/admin/extend/widgets.json | 30 + .../language/es/admin/manage/admins-mods.json | 12 + .../language/es/admin/manage/categories.json | 92 + public/language/es/admin/manage/digest.json | 22 + public/language/es/admin/manage/groups.json | 44 + .../language/es/admin/manage/privileges.json | 64 + .../es/admin/manage/registration.json | 20 + public/language/es/admin/manage/tags.json | 18 + public/language/es/admin/manage/uploads.json | 11 + public/language/es/admin/manage/users.json | 112 + public/language/es/admin/menu.json | 89 + .../language/es/admin/settings/advanced.json | 50 + public/language/es/admin/settings/api.json | 16 + public/language/es/admin/settings/chat.json | 12 + .../language/es/admin/settings/cookies.json | 13 + public/language/es/admin/settings/email.json | 52 + .../language/es/admin/settings/general.json | 50 + public/language/es/admin/settings/group.json | 13 + public/language/es/admin/settings/guest.json | 7 + .../language/es/admin/settings/homepage.json | 8 + .../language/es/admin/settings/languages.json | 6 + .../es/admin/settings/navigation.json | 25 + .../es/admin/settings/notifications.json | 7 + .../es/admin/settings/pagination.json | 12 + public/language/es/admin/settings/post.json | 67 + .../es/admin/settings/reputation.json | 31 + public/language/es/admin/settings/social.json | 5 + .../language/es/admin/settings/sockets.json | 6 + public/language/es/admin/settings/sounds.json | 9 + public/language/es/admin/settings/tags.json | 12 + .../language/es/admin/settings/uploads.json | 45 + public/language/es/admin/settings/user.json | 83 + .../es/admin/settings/web-crawler.json | 10 + public/language/es/category.json | 23 + public/language/es/email.json | 58 + public/language/es/error.json | 224 + public/language/es/flags.json | 89 + public/language/es/global.json | 126 + public/language/es/groups.json | 64 + public/language/es/ip-blacklist.json | 19 + public/language/es/language.json | 5 + public/language/es/login.json | 12 + public/language/es/modules.json | 82 + public/language/es/notifications.json | 76 + public/language/es/pages.json | 65 + public/language/es/post-queue.json | 31 + public/language/es/recent.json | 19 + public/language/es/register.json | 32 + public/language/es/reset_password.json | 18 + public/language/es/search.json | 49 + public/language/es/success.json | 7 + public/language/es/tags.json | 8 + public/language/es/top.json | 4 + public/language/es/topic.json | 188 + public/language/es/unread.json | 15 + public/language/es/uploads.json | 9 + public/language/es/user.json | 199 + public/language/es/users.json | 24 + public/language/et/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/et/admin/admin.json | 11 + public/language/et/admin/advanced/cache.json | 9 + .../language/et/admin/advanced/database.json | 52 + public/language/et/admin/advanced/errors.json | 14 + public/language/et/admin/advanced/events.json | 13 + public/language/et/admin/advanced/logs.json | 7 + .../et/admin/appearance/customise.json | 16 + .../language/et/admin/appearance/skins.json | 9 + .../language/et/admin/appearance/themes.json | 11 + public/language/et/admin/dashboard.json | 90 + .../language/et/admin/development/info.json | 25 + .../language/et/admin/development/logger.json | 12 + public/language/et/admin/extend/plugins.json | 57 + public/language/et/admin/extend/rewards.json | 15 + public/language/et/admin/extend/widgets.json | 30 + .../language/et/admin/manage/admins-mods.json | 12 + .../language/et/admin/manage/categories.json | 92 + public/language/et/admin/manage/digest.json | 22 + public/language/et/admin/manage/groups.json | 44 + .../language/et/admin/manage/privileges.json | 64 + .../et/admin/manage/registration.json | 20 + public/language/et/admin/manage/tags.json | 18 + public/language/et/admin/manage/uploads.json | 11 + public/language/et/admin/manage/users.json | 112 + public/language/et/admin/menu.json | 89 + .../language/et/admin/settings/advanced.json | 50 + public/language/et/admin/settings/api.json | 16 + public/language/et/admin/settings/chat.json | 12 + .../language/et/admin/settings/cookies.json | 13 + public/language/et/admin/settings/email.json | 52 + .../language/et/admin/settings/general.json | 50 + public/language/et/admin/settings/group.json | 13 + public/language/et/admin/settings/guest.json | 7 + .../language/et/admin/settings/homepage.json | 8 + .../language/et/admin/settings/languages.json | 6 + .../et/admin/settings/navigation.json | 25 + .../et/admin/settings/notifications.json | 7 + .../et/admin/settings/pagination.json | 12 + public/language/et/admin/settings/post.json | 67 + .../et/admin/settings/reputation.json | 31 + public/language/et/admin/settings/social.json | 5 + .../language/et/admin/settings/sockets.json | 6 + public/language/et/admin/settings/sounds.json | 9 + public/language/et/admin/settings/tags.json | 12 + .../language/et/admin/settings/uploads.json | 45 + public/language/et/admin/settings/user.json | 83 + .../et/admin/settings/web-crawler.json | 10 + public/language/et/category.json | 23 + public/language/et/email.json | 58 + public/language/et/error.json | 224 + public/language/et/flags.json | 89 + public/language/et/global.json | 126 + public/language/et/groups.json | 64 + public/language/et/ip-blacklist.json | 19 + public/language/et/language.json | 5 + public/language/et/login.json | 12 + public/language/et/modules.json | 82 + public/language/et/notifications.json | 76 + public/language/et/pages.json | 65 + public/language/et/post-queue.json | 31 + public/language/et/recent.json | 19 + public/language/et/register.json | 32 + public/language/et/reset_password.json | 18 + public/language/et/search.json | 49 + public/language/et/success.json | 7 + public/language/et/tags.json | 8 + public/language/et/top.json | 4 + public/language/et/topic.json | 188 + public/language/et/unread.json | 15 + public/language/et/uploads.json | 9 + public/language/et/user.json | 199 + public/language/et/users.json | 24 + .../language/fa-IR/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/fa-IR/admin/admin.json | 11 + .../language/fa-IR/admin/advanced/cache.json | 9 + .../fa-IR/admin/advanced/database.json | 52 + .../language/fa-IR/admin/advanced/errors.json | 14 + .../language/fa-IR/admin/advanced/events.json | 13 + .../language/fa-IR/admin/advanced/logs.json | 7 + .../fa-IR/admin/appearance/customise.json | 16 + .../fa-IR/admin/appearance/skins.json | 9 + .../fa-IR/admin/appearance/themes.json | 11 + public/language/fa-IR/admin/dashboard.json | 90 + .../fa-IR/admin/development/info.json | 25 + .../fa-IR/admin/development/logger.json | 12 + .../language/fa-IR/admin/extend/plugins.json | 57 + .../language/fa-IR/admin/extend/rewards.json | 15 + .../language/fa-IR/admin/extend/widgets.json | 30 + .../fa-IR/admin/manage/admins-mods.json | 12 + .../fa-IR/admin/manage/categories.json | 92 + .../language/fa-IR/admin/manage/digest.json | 22 + .../language/fa-IR/admin/manage/groups.json | 44 + .../fa-IR/admin/manage/privileges.json | 64 + .../fa-IR/admin/manage/registration.json | 20 + public/language/fa-IR/admin/manage/tags.json | 18 + .../language/fa-IR/admin/manage/uploads.json | 11 + public/language/fa-IR/admin/manage/users.json | 112 + public/language/fa-IR/admin/menu.json | 89 + .../fa-IR/admin/settings/advanced.json | 50 + public/language/fa-IR/admin/settings/api.json | 16 + .../language/fa-IR/admin/settings/chat.json | 12 + .../fa-IR/admin/settings/cookies.json | 13 + .../language/fa-IR/admin/settings/email.json | 52 + .../fa-IR/admin/settings/general.json | 50 + .../language/fa-IR/admin/settings/group.json | 13 + .../language/fa-IR/admin/settings/guest.json | 7 + .../fa-IR/admin/settings/homepage.json | 8 + .../fa-IR/admin/settings/languages.json | 6 + .../fa-IR/admin/settings/navigation.json | 25 + .../fa-IR/admin/settings/notifications.json | 7 + .../fa-IR/admin/settings/pagination.json | 12 + .../language/fa-IR/admin/settings/post.json | 67 + .../fa-IR/admin/settings/reputation.json | 31 + .../language/fa-IR/admin/settings/social.json | 5 + .../fa-IR/admin/settings/sockets.json | 6 + .../language/fa-IR/admin/settings/sounds.json | 9 + .../language/fa-IR/admin/settings/tags.json | 12 + .../fa-IR/admin/settings/uploads.json | 45 + .../language/fa-IR/admin/settings/user.json | 83 + .../fa-IR/admin/settings/web-crawler.json | 10 + public/language/fa-IR/category.json | 23 + public/language/fa-IR/email.json | 58 + public/language/fa-IR/error.json | 224 + public/language/fa-IR/flags.json | 89 + public/language/fa-IR/global.json | 126 + public/language/fa-IR/groups.json | 64 + public/language/fa-IR/ip-blacklist.json | 19 + public/language/fa-IR/language.json | 5 + public/language/fa-IR/login.json | 12 + public/language/fa-IR/modules.json | 82 + public/language/fa-IR/notifications.json | 76 + public/language/fa-IR/pages.json | 65 + public/language/fa-IR/post-queue.json | 31 + public/language/fa-IR/recent.json | 19 + public/language/fa-IR/register.json | 32 + public/language/fa-IR/reset_password.json | 18 + public/language/fa-IR/search.json | 49 + public/language/fa-IR/success.json | 7 + public/language/fa-IR/tags.json | 8 + public/language/fa-IR/top.json | 4 + public/language/fa-IR/topic.json | 188 + public/language/fa-IR/unread.json | 15 + public/language/fa-IR/uploads.json | 9 + public/language/fa-IR/user.json | 199 + public/language/fa-IR/users.json | 24 + public/language/fi/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/fi/admin/admin.json | 11 + public/language/fi/admin/advanced/cache.json | 9 + .../language/fi/admin/advanced/database.json | 52 + public/language/fi/admin/advanced/errors.json | 14 + public/language/fi/admin/advanced/events.json | 13 + public/language/fi/admin/advanced/logs.json | 7 + .../fi/admin/appearance/customise.json | 16 + .../language/fi/admin/appearance/skins.json | 9 + .../language/fi/admin/appearance/themes.json | 11 + public/language/fi/admin/dashboard.json | 90 + .../language/fi/admin/development/info.json | 25 + .../language/fi/admin/development/logger.json | 12 + public/language/fi/admin/extend/plugins.json | 57 + public/language/fi/admin/extend/rewards.json | 15 + public/language/fi/admin/extend/widgets.json | 30 + .../language/fi/admin/manage/admins-mods.json | 12 + .../language/fi/admin/manage/categories.json | 92 + public/language/fi/admin/manage/digest.json | 22 + public/language/fi/admin/manage/groups.json | 44 + .../language/fi/admin/manage/privileges.json | 64 + .../fi/admin/manage/registration.json | 20 + public/language/fi/admin/manage/tags.json | 18 + public/language/fi/admin/manage/uploads.json | 11 + public/language/fi/admin/manage/users.json | 112 + public/language/fi/admin/menu.json | 89 + .../language/fi/admin/settings/advanced.json | 50 + public/language/fi/admin/settings/api.json | 16 + public/language/fi/admin/settings/chat.json | 12 + .../language/fi/admin/settings/cookies.json | 13 + public/language/fi/admin/settings/email.json | 52 + .../language/fi/admin/settings/general.json | 50 + public/language/fi/admin/settings/group.json | 13 + public/language/fi/admin/settings/guest.json | 7 + .../language/fi/admin/settings/homepage.json | 8 + .../language/fi/admin/settings/languages.json | 6 + .../fi/admin/settings/navigation.json | 25 + .../fi/admin/settings/notifications.json | 7 + .../fi/admin/settings/pagination.json | 12 + public/language/fi/admin/settings/post.json | 67 + .../fi/admin/settings/reputation.json | 31 + public/language/fi/admin/settings/social.json | 5 + .../language/fi/admin/settings/sockets.json | 6 + public/language/fi/admin/settings/sounds.json | 9 + public/language/fi/admin/settings/tags.json | 12 + .../language/fi/admin/settings/uploads.json | 45 + public/language/fi/admin/settings/user.json | 83 + .../fi/admin/settings/web-crawler.json | 10 + public/language/fi/category.json | 23 + public/language/fi/email.json | 58 + public/language/fi/error.json | 224 + public/language/fi/flags.json | 89 + public/language/fi/global.json | 126 + public/language/fi/groups.json | 64 + public/language/fi/ip-blacklist.json | 19 + public/language/fi/language.json | 5 + public/language/fi/login.json | 12 + public/language/fi/modules.json | 82 + public/language/fi/notifications.json | 76 + public/language/fi/pages.json | 65 + public/language/fi/post-queue.json | 31 + public/language/fi/recent.json | 19 + public/language/fi/register.json | 32 + public/language/fi/reset_password.json | 18 + public/language/fi/search.json | 49 + public/language/fi/success.json | 7 + public/language/fi/tags.json | 8 + public/language/fi/top.json | 4 + public/language/fi/topic.json | 188 + public/language/fi/unread.json | 15 + public/language/fi/uploads.json | 9 + public/language/fi/user.json | 199 + public/language/fi/users.json | 24 + public/language/fr/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/fr/admin/admin.json | 11 + public/language/fr/admin/advanced/cache.json | 9 + .../language/fr/admin/advanced/database.json | 52 + public/language/fr/admin/advanced/errors.json | 14 + public/language/fr/admin/advanced/events.json | 13 + public/language/fr/admin/advanced/logs.json | 7 + .../fr/admin/appearance/customise.json | 16 + .../language/fr/admin/appearance/skins.json | 9 + .../language/fr/admin/appearance/themes.json | 11 + public/language/fr/admin/dashboard.json | 90 + .../language/fr/admin/development/info.json | 25 + .../language/fr/admin/development/logger.json | 12 + public/language/fr/admin/extend/plugins.json | 57 + public/language/fr/admin/extend/rewards.json | 15 + public/language/fr/admin/extend/widgets.json | 30 + .../language/fr/admin/manage/admins-mods.json | 12 + .../language/fr/admin/manage/categories.json | 92 + public/language/fr/admin/manage/digest.json | 22 + public/language/fr/admin/manage/groups.json | 44 + .../language/fr/admin/manage/privileges.json | 64 + .../fr/admin/manage/registration.json | 20 + public/language/fr/admin/manage/tags.json | 18 + public/language/fr/admin/manage/uploads.json | 11 + public/language/fr/admin/manage/users.json | 112 + public/language/fr/admin/menu.json | 89 + .../language/fr/admin/settings/advanced.json | 50 + public/language/fr/admin/settings/api.json | 16 + public/language/fr/admin/settings/chat.json | 12 + .../language/fr/admin/settings/cookies.json | 13 + public/language/fr/admin/settings/email.json | 52 + .../language/fr/admin/settings/general.json | 50 + public/language/fr/admin/settings/group.json | 13 + public/language/fr/admin/settings/guest.json | 7 + .../language/fr/admin/settings/homepage.json | 8 + .../language/fr/admin/settings/languages.json | 6 + .../fr/admin/settings/navigation.json | 25 + .../fr/admin/settings/notifications.json | 7 + .../fr/admin/settings/pagination.json | 12 + public/language/fr/admin/settings/post.json | 67 + .../fr/admin/settings/reputation.json | 31 + public/language/fr/admin/settings/social.json | 5 + .../language/fr/admin/settings/sockets.json | 6 + public/language/fr/admin/settings/sounds.json | 9 + public/language/fr/admin/settings/tags.json | 12 + .../language/fr/admin/settings/uploads.json | 45 + public/language/fr/admin/settings/user.json | 83 + .../fr/admin/settings/web-crawler.json | 10 + public/language/fr/category.json | 23 + public/language/fr/email.json | 58 + public/language/fr/error.json | 224 + public/language/fr/flags.json | 89 + public/language/fr/global.json | 126 + public/language/fr/groups.json | 64 + public/language/fr/ip-blacklist.json | 19 + public/language/fr/language.json | 5 + public/language/fr/login.json | 12 + public/language/fr/modules.json | 82 + public/language/fr/notifications.json | 76 + public/language/fr/pages.json | 65 + public/language/fr/post-queue.json | 31 + public/language/fr/recent.json | 19 + public/language/fr/register.json | 32 + public/language/fr/reset_password.json | 18 + public/language/fr/search.json | 49 + public/language/fr/success.json | 7 + public/language/fr/tags.json | 8 + public/language/fr/top.json | 4 + public/language/fr/topic.json | 188 + public/language/fr/unread.json | 15 + public/language/fr/uploads.json | 9 + public/language/fr/user.json | 199 + public/language/fr/users.json | 24 + public/language/gl/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/gl/admin/admin.json | 11 + public/language/gl/admin/advanced/cache.json | 9 + .../language/gl/admin/advanced/database.json | 52 + public/language/gl/admin/advanced/errors.json | 14 + public/language/gl/admin/advanced/events.json | 13 + public/language/gl/admin/advanced/logs.json | 7 + .../gl/admin/appearance/customise.json | 16 + .../language/gl/admin/appearance/skins.json | 9 + .../language/gl/admin/appearance/themes.json | 11 + public/language/gl/admin/dashboard.json | 90 + .../language/gl/admin/development/info.json | 25 + .../language/gl/admin/development/logger.json | 12 + public/language/gl/admin/extend/plugins.json | 57 + public/language/gl/admin/extend/rewards.json | 15 + public/language/gl/admin/extend/widgets.json | 30 + .../language/gl/admin/manage/admins-mods.json | 12 + .../language/gl/admin/manage/categories.json | 92 + public/language/gl/admin/manage/digest.json | 22 + public/language/gl/admin/manage/groups.json | 44 + .../language/gl/admin/manage/privileges.json | 64 + .../gl/admin/manage/registration.json | 20 + public/language/gl/admin/manage/tags.json | 18 + public/language/gl/admin/manage/uploads.json | 11 + public/language/gl/admin/manage/users.json | 112 + public/language/gl/admin/menu.json | 89 + .../language/gl/admin/settings/advanced.json | 50 + public/language/gl/admin/settings/api.json | 16 + public/language/gl/admin/settings/chat.json | 12 + .../language/gl/admin/settings/cookies.json | 13 + public/language/gl/admin/settings/email.json | 52 + .../language/gl/admin/settings/general.json | 50 + public/language/gl/admin/settings/group.json | 13 + public/language/gl/admin/settings/guest.json | 7 + .../language/gl/admin/settings/homepage.json | 8 + .../language/gl/admin/settings/languages.json | 6 + .../gl/admin/settings/navigation.json | 25 + .../gl/admin/settings/notifications.json | 7 + .../gl/admin/settings/pagination.json | 12 + public/language/gl/admin/settings/post.json | 67 + .../gl/admin/settings/reputation.json | 31 + public/language/gl/admin/settings/social.json | 5 + .../language/gl/admin/settings/sockets.json | 6 + public/language/gl/admin/settings/sounds.json | 9 + public/language/gl/admin/settings/tags.json | 12 + .../language/gl/admin/settings/uploads.json | 45 + public/language/gl/admin/settings/user.json | 83 + .../gl/admin/settings/web-crawler.json | 10 + public/language/gl/category.json | 23 + public/language/gl/email.json | 58 + public/language/gl/error.json | 224 + public/language/gl/flags.json | 89 + public/language/gl/global.json | 126 + public/language/gl/groups.json | 64 + public/language/gl/ip-blacklist.json | 19 + public/language/gl/language.json | 5 + public/language/gl/login.json | 12 + public/language/gl/modules.json | 82 + public/language/gl/notifications.json | 76 + public/language/gl/pages.json | 65 + public/language/gl/post-queue.json | 31 + public/language/gl/recent.json | 19 + public/language/gl/register.json | 32 + public/language/gl/reset_password.json | 18 + public/language/gl/search.json | 49 + public/language/gl/success.json | 7 + public/language/gl/tags.json | 8 + public/language/gl/top.json | 4 + public/language/gl/topic.json | 188 + public/language/gl/unread.json | 15 + public/language/gl/uploads.json | 9 + public/language/gl/user.json | 199 + public/language/gl/users.json | 24 + public/language/he/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/he/admin/admin.json | 11 + public/language/he/admin/advanced/cache.json | 9 + .../language/he/admin/advanced/database.json | 52 + public/language/he/admin/advanced/errors.json | 14 + public/language/he/admin/advanced/events.json | 13 + public/language/he/admin/advanced/logs.json | 7 + .../he/admin/appearance/customise.json | 16 + .../language/he/admin/appearance/skins.json | 9 + .../language/he/admin/appearance/themes.json | 11 + public/language/he/admin/dashboard.json | 90 + .../language/he/admin/development/info.json | 25 + .../language/he/admin/development/logger.json | 12 + public/language/he/admin/extend/plugins.json | 57 + public/language/he/admin/extend/rewards.json | 15 + public/language/he/admin/extend/widgets.json | 30 + .../language/he/admin/manage/admins-mods.json | 12 + .../language/he/admin/manage/categories.json | 92 + public/language/he/admin/manage/digest.json | 22 + public/language/he/admin/manage/groups.json | 44 + .../language/he/admin/manage/privileges.json | 64 + .../he/admin/manage/registration.json | 20 + public/language/he/admin/manage/tags.json | 18 + public/language/he/admin/manage/uploads.json | 11 + public/language/he/admin/manage/users.json | 112 + public/language/he/admin/menu.json | 89 + .../language/he/admin/settings/advanced.json | 50 + public/language/he/admin/settings/api.json | 16 + public/language/he/admin/settings/chat.json | 12 + .../language/he/admin/settings/cookies.json | 13 + public/language/he/admin/settings/email.json | 52 + .../language/he/admin/settings/general.json | 50 + public/language/he/admin/settings/group.json | 13 + public/language/he/admin/settings/guest.json | 7 + .../language/he/admin/settings/homepage.json | 8 + .../language/he/admin/settings/languages.json | 6 + .../he/admin/settings/navigation.json | 25 + .../he/admin/settings/notifications.json | 7 + .../he/admin/settings/pagination.json | 12 + public/language/he/admin/settings/post.json | 67 + .../he/admin/settings/reputation.json | 31 + public/language/he/admin/settings/social.json | 5 + .../language/he/admin/settings/sockets.json | 6 + public/language/he/admin/settings/sounds.json | 9 + public/language/he/admin/settings/tags.json | 12 + .../language/he/admin/settings/uploads.json | 45 + public/language/he/admin/settings/user.json | 83 + .../he/admin/settings/web-crawler.json | 10 + public/language/he/category.json | 23 + public/language/he/email.json | 58 + public/language/he/error.json | 224 + public/language/he/flags.json | 89 + public/language/he/global.json | 126 + public/language/he/groups.json | 64 + public/language/he/ip-blacklist.json | 19 + public/language/he/language.json | 5 + public/language/he/login.json | 12 + public/language/he/modules.json | 82 + public/language/he/notifications.json | 76 + public/language/he/pages.json | 65 + public/language/he/post-queue.json | 31 + public/language/he/recent.json | 19 + public/language/he/register.json | 32 + public/language/he/reset_password.json | 18 + public/language/he/search.json | 49 + public/language/he/success.json | 7 + public/language/he/tags.json | 8 + public/language/he/top.json | 4 + public/language/he/topic.json | 188 + public/language/he/unread.json | 15 + public/language/he/uploads.json | 9 + public/language/he/user.json | 199 + public/language/he/users.json | 24 + public/language/hr/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/hr/admin/admin.json | 11 + public/language/hr/admin/advanced/cache.json | 9 + .../language/hr/admin/advanced/database.json | 52 + public/language/hr/admin/advanced/errors.json | 14 + public/language/hr/admin/advanced/events.json | 13 + public/language/hr/admin/advanced/logs.json | 7 + .../hr/admin/appearance/customise.json | 16 + .../language/hr/admin/appearance/skins.json | 9 + .../language/hr/admin/appearance/themes.json | 11 + public/language/hr/admin/dashboard.json | 90 + .../language/hr/admin/development/info.json | 25 + .../language/hr/admin/development/logger.json | 12 + public/language/hr/admin/extend/plugins.json | 57 + public/language/hr/admin/extend/rewards.json | 15 + public/language/hr/admin/extend/widgets.json | 30 + .../language/hr/admin/manage/admins-mods.json | 12 + .../language/hr/admin/manage/categories.json | 92 + public/language/hr/admin/manage/digest.json | 22 + public/language/hr/admin/manage/groups.json | 44 + .../language/hr/admin/manage/privileges.json | 64 + .../hr/admin/manage/registration.json | 20 + public/language/hr/admin/manage/tags.json | 18 + public/language/hr/admin/manage/uploads.json | 11 + public/language/hr/admin/manage/users.json | 112 + public/language/hr/admin/menu.json | 89 + .../language/hr/admin/settings/advanced.json | 50 + public/language/hr/admin/settings/api.json | 16 + public/language/hr/admin/settings/chat.json | 12 + .../language/hr/admin/settings/cookies.json | 13 + public/language/hr/admin/settings/email.json | 52 + .../language/hr/admin/settings/general.json | 50 + public/language/hr/admin/settings/group.json | 13 + public/language/hr/admin/settings/guest.json | 7 + .../language/hr/admin/settings/homepage.json | 8 + .../language/hr/admin/settings/languages.json | 6 + .../hr/admin/settings/navigation.json | 25 + .../hr/admin/settings/notifications.json | 7 + .../hr/admin/settings/pagination.json | 12 + public/language/hr/admin/settings/post.json | 67 + .../hr/admin/settings/reputation.json | 31 + public/language/hr/admin/settings/social.json | 5 + .../language/hr/admin/settings/sockets.json | 6 + public/language/hr/admin/settings/sounds.json | 9 + public/language/hr/admin/settings/tags.json | 12 + .../language/hr/admin/settings/uploads.json | 45 + public/language/hr/admin/settings/user.json | 83 + .../hr/admin/settings/web-crawler.json | 10 + public/language/hr/category.json | 23 + public/language/hr/email.json | 58 + public/language/hr/error.json | 224 + public/language/hr/flags.json | 89 + public/language/hr/global.json | 126 + public/language/hr/groups.json | 64 + public/language/hr/ip-blacklist.json | 19 + public/language/hr/language.json | 5 + public/language/hr/login.json | 12 + public/language/hr/modules.json | 82 + public/language/hr/notifications.json | 76 + public/language/hr/pages.json | 65 + public/language/hr/post-queue.json | 31 + public/language/hr/recent.json | 19 + public/language/hr/register.json | 32 + public/language/hr/reset_password.json | 18 + public/language/hr/search.json | 49 + public/language/hr/success.json | 7 + public/language/hr/tags.json | 8 + public/language/hr/top.json | 4 + public/language/hr/topic.json | 188 + public/language/hr/unread.json | 15 + public/language/hr/uploads.json | 9 + public/language/hr/user.json | 199 + public/language/hr/users.json | 24 + public/language/hu/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/hu/admin/admin.json | 11 + public/language/hu/admin/advanced/cache.json | 9 + .../language/hu/admin/advanced/database.json | 52 + public/language/hu/admin/advanced/errors.json | 14 + public/language/hu/admin/advanced/events.json | 13 + public/language/hu/admin/advanced/logs.json | 7 + .../hu/admin/appearance/customise.json | 16 + .../language/hu/admin/appearance/skins.json | 9 + .../language/hu/admin/appearance/themes.json | 11 + public/language/hu/admin/dashboard.json | 90 + .../language/hu/admin/development/info.json | 25 + .../language/hu/admin/development/logger.json | 12 + public/language/hu/admin/extend/plugins.json | 57 + public/language/hu/admin/extend/rewards.json | 15 + public/language/hu/admin/extend/widgets.json | 30 + .../language/hu/admin/manage/admins-mods.json | 12 + .../language/hu/admin/manage/categories.json | 92 + public/language/hu/admin/manage/digest.json | 22 + public/language/hu/admin/manage/groups.json | 44 + .../language/hu/admin/manage/privileges.json | 64 + .../hu/admin/manage/registration.json | 20 + public/language/hu/admin/manage/tags.json | 18 + public/language/hu/admin/manage/uploads.json | 11 + public/language/hu/admin/manage/users.json | 112 + public/language/hu/admin/menu.json | 89 + .../language/hu/admin/settings/advanced.json | 50 + public/language/hu/admin/settings/api.json | 16 + public/language/hu/admin/settings/chat.json | 12 + .../language/hu/admin/settings/cookies.json | 13 + public/language/hu/admin/settings/email.json | 52 + .../language/hu/admin/settings/general.json | 50 + public/language/hu/admin/settings/group.json | 13 + public/language/hu/admin/settings/guest.json | 7 + .../language/hu/admin/settings/homepage.json | 8 + .../language/hu/admin/settings/languages.json | 6 + .../hu/admin/settings/navigation.json | 25 + .../hu/admin/settings/notifications.json | 7 + .../hu/admin/settings/pagination.json | 12 + public/language/hu/admin/settings/post.json | 67 + .../hu/admin/settings/reputation.json | 31 + public/language/hu/admin/settings/social.json | 5 + .../language/hu/admin/settings/sockets.json | 6 + public/language/hu/admin/settings/sounds.json | 9 + public/language/hu/admin/settings/tags.json | 12 + .../language/hu/admin/settings/uploads.json | 45 + public/language/hu/admin/settings/user.json | 83 + .../hu/admin/settings/web-crawler.json | 10 + public/language/hu/category.json | 23 + public/language/hu/email.json | 58 + public/language/hu/error.json | 224 + public/language/hu/flags.json | 89 + public/language/hu/global.json | 126 + public/language/hu/groups.json | 64 + public/language/hu/ip-blacklist.json | 19 + public/language/hu/language.json | 5 + public/language/hu/login.json | 12 + public/language/hu/modules.json | 82 + public/language/hu/notifications.json | 76 + public/language/hu/pages.json | 65 + public/language/hu/post-queue.json | 31 + public/language/hu/recent.json | 19 + public/language/hu/register.json | 32 + public/language/hu/reset_password.json | 18 + public/language/hu/search.json | 49 + public/language/hu/success.json | 7 + public/language/hu/tags.json | 8 + public/language/hu/top.json | 4 + public/language/hu/topic.json | 188 + public/language/hu/unread.json | 15 + public/language/hu/uploads.json | 9 + public/language/hu/user.json | 199 + public/language/hu/users.json | 24 + public/language/hy/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/hy/admin/admin.json | 11 + public/language/hy/admin/advanced/cache.json | 9 + .../language/hy/admin/advanced/database.json | 52 + public/language/hy/admin/advanced/errors.json | 14 + public/language/hy/admin/advanced/events.json | 13 + public/language/hy/admin/advanced/logs.json | 7 + .../hy/admin/appearance/customise.json | 16 + .../language/hy/admin/appearance/skins.json | 9 + .../language/hy/admin/appearance/themes.json | 11 + public/language/hy/admin/dashboard.json | 90 + .../language/hy/admin/development/info.json | 25 + .../language/hy/admin/development/logger.json | 12 + public/language/hy/admin/extend/plugins.json | 57 + public/language/hy/admin/extend/rewards.json | 15 + public/language/hy/admin/extend/widgets.json | 30 + .../language/hy/admin/manage/admins-mods.json | 12 + .../language/hy/admin/manage/categories.json | 92 + public/language/hy/admin/manage/digest.json | 22 + public/language/hy/admin/manage/groups.json | 44 + .../language/hy/admin/manage/privileges.json | 64 + .../hy/admin/manage/registration.json | 20 + public/language/hy/admin/manage/tags.json | 18 + public/language/hy/admin/manage/uploads.json | 11 + public/language/hy/admin/manage/users.json | 112 + public/language/hy/admin/menu.json | 89 + .../language/hy/admin/settings/advanced.json | 50 + public/language/hy/admin/settings/api.json | 16 + public/language/hy/admin/settings/chat.json | 12 + .../language/hy/admin/settings/cookies.json | 13 + public/language/hy/admin/settings/email.json | 52 + .../language/hy/admin/settings/general.json | 50 + public/language/hy/admin/settings/group.json | 13 + public/language/hy/admin/settings/guest.json | 7 + .../language/hy/admin/settings/homepage.json | 8 + .../language/hy/admin/settings/languages.json | 6 + .../hy/admin/settings/navigation.json | 25 + .../hy/admin/settings/notifications.json | 7 + .../hy/admin/settings/pagination.json | 12 + public/language/hy/admin/settings/post.json | 67 + .../hy/admin/settings/reputation.json | 31 + public/language/hy/admin/settings/social.json | 5 + .../language/hy/admin/settings/sockets.json | 6 + public/language/hy/admin/settings/sounds.json | 9 + public/language/hy/admin/settings/tags.json | 12 + .../language/hy/admin/settings/uploads.json | 45 + public/language/hy/admin/settings/user.json | 83 + .../hy/admin/settings/web-crawler.json | 10 + public/language/hy/category.json | 23 + public/language/hy/email.json | 58 + public/language/hy/error.json | 224 + public/language/hy/flags.json | 89 + public/language/hy/global.json | 126 + public/language/hy/groups.json | 64 + public/language/hy/ip-blacklist.json | 19 + public/language/hy/language.json | 5 + public/language/hy/login.json | 12 + public/language/hy/modules.json | 82 + public/language/hy/notifications.json | 76 + public/language/hy/pages.json | 65 + public/language/hy/post-queue.json | 31 + public/language/hy/recent.json | 19 + public/language/hy/register.json | 32 + public/language/hy/reset_password.json | 18 + public/language/hy/search.json | 49 + public/language/hy/success.json | 7 + public/language/hy/tags.json | 8 + public/language/hy/top.json | 4 + public/language/hy/topic.json | 188 + public/language/hy/unread.json | 15 + public/language/hy/uploads.json | 9 + public/language/hy/user.json | 199 + public/language/hy/users.json | 24 + public/language/id/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/id/admin/admin.json | 11 + public/language/id/admin/advanced/cache.json | 9 + .../language/id/admin/advanced/database.json | 52 + public/language/id/admin/advanced/errors.json | 14 + public/language/id/admin/advanced/events.json | 13 + public/language/id/admin/advanced/logs.json | 7 + .../id/admin/appearance/customise.json | 16 + .../language/id/admin/appearance/skins.json | 9 + .../language/id/admin/appearance/themes.json | 11 + public/language/id/admin/dashboard.json | 90 + .../language/id/admin/development/info.json | 25 + .../language/id/admin/development/logger.json | 12 + public/language/id/admin/extend/plugins.json | 57 + public/language/id/admin/extend/rewards.json | 15 + public/language/id/admin/extend/widgets.json | 30 + .../language/id/admin/manage/admins-mods.json | 12 + .../language/id/admin/manage/categories.json | 92 + public/language/id/admin/manage/digest.json | 22 + public/language/id/admin/manage/groups.json | 44 + .../language/id/admin/manage/privileges.json | 64 + .../id/admin/manage/registration.json | 20 + public/language/id/admin/manage/tags.json | 18 + public/language/id/admin/manage/uploads.json | 11 + public/language/id/admin/manage/users.json | 112 + public/language/id/admin/menu.json | 89 + .../language/id/admin/settings/advanced.json | 50 + public/language/id/admin/settings/api.json | 16 + public/language/id/admin/settings/chat.json | 12 + .../language/id/admin/settings/cookies.json | 13 + public/language/id/admin/settings/email.json | 52 + .../language/id/admin/settings/general.json | 50 + public/language/id/admin/settings/group.json | 13 + public/language/id/admin/settings/guest.json | 7 + .../language/id/admin/settings/homepage.json | 8 + .../language/id/admin/settings/languages.json | 6 + .../id/admin/settings/navigation.json | 25 + .../id/admin/settings/notifications.json | 7 + .../id/admin/settings/pagination.json | 12 + public/language/id/admin/settings/post.json | 67 + .../id/admin/settings/reputation.json | 31 + public/language/id/admin/settings/social.json | 5 + .../language/id/admin/settings/sockets.json | 6 + public/language/id/admin/settings/sounds.json | 9 + public/language/id/admin/settings/tags.json | 12 + .../language/id/admin/settings/uploads.json | 45 + public/language/id/admin/settings/user.json | 83 + .../id/admin/settings/web-crawler.json | 10 + public/language/id/category.json | 23 + public/language/id/email.json | 58 + public/language/id/error.json | 224 + public/language/id/flags.json | 89 + public/language/id/global.json | 126 + public/language/id/groups.json | 64 + public/language/id/ip-blacklist.json | 19 + public/language/id/language.json | 5 + public/language/id/login.json | 12 + public/language/id/modules.json | 82 + public/language/id/notifications.json | 76 + public/language/id/pages.json | 65 + public/language/id/post-queue.json | 31 + public/language/id/recent.json | 19 + public/language/id/register.json | 32 + public/language/id/reset_password.json | 18 + public/language/id/search.json | 49 + public/language/id/success.json | 7 + public/language/id/tags.json | 8 + public/language/id/top.json | 4 + public/language/id/topic.json | 188 + public/language/id/unread.json | 15 + public/language/id/uploads.json | 9 + public/language/id/user.json | 199 + public/language/id/users.json | 24 + public/language/it/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/it/admin/admin.json | 11 + public/language/it/admin/advanced/cache.json | 9 + .../language/it/admin/advanced/database.json | 52 + public/language/it/admin/advanced/errors.json | 14 + public/language/it/admin/advanced/events.json | 13 + public/language/it/admin/advanced/logs.json | 7 + .../it/admin/appearance/customise.json | 16 + .../language/it/admin/appearance/skins.json | 9 + .../language/it/admin/appearance/themes.json | 11 + public/language/it/admin/dashboard.json | 90 + .../language/it/admin/development/info.json | 25 + .../language/it/admin/development/logger.json | 12 + public/language/it/admin/extend/plugins.json | 57 + public/language/it/admin/extend/rewards.json | 15 + public/language/it/admin/extend/widgets.json | 30 + .../language/it/admin/manage/admins-mods.json | 12 + .../language/it/admin/manage/categories.json | 92 + public/language/it/admin/manage/digest.json | 22 + public/language/it/admin/manage/groups.json | 44 + .../language/it/admin/manage/privileges.json | 64 + .../it/admin/manage/registration.json | 20 + public/language/it/admin/manage/tags.json | 18 + public/language/it/admin/manage/uploads.json | 11 + public/language/it/admin/manage/users.json | 112 + public/language/it/admin/menu.json | 89 + .../language/it/admin/settings/advanced.json | 50 + public/language/it/admin/settings/api.json | 16 + public/language/it/admin/settings/chat.json | 12 + .../language/it/admin/settings/cookies.json | 13 + public/language/it/admin/settings/email.json | 52 + .../language/it/admin/settings/general.json | 50 + public/language/it/admin/settings/group.json | 13 + public/language/it/admin/settings/guest.json | 7 + .../language/it/admin/settings/homepage.json | 8 + .../language/it/admin/settings/languages.json | 6 + .../it/admin/settings/navigation.json | 25 + .../it/admin/settings/notifications.json | 7 + .../it/admin/settings/pagination.json | 12 + public/language/it/admin/settings/post.json | 67 + .../it/admin/settings/reputation.json | 31 + public/language/it/admin/settings/social.json | 5 + .../language/it/admin/settings/sockets.json | 6 + public/language/it/admin/settings/sounds.json | 9 + public/language/it/admin/settings/tags.json | 12 + .../language/it/admin/settings/uploads.json | 45 + public/language/it/admin/settings/user.json | 83 + .../it/admin/settings/web-crawler.json | 10 + public/language/it/category.json | 23 + public/language/it/email.json | 58 + public/language/it/error.json | 224 + public/language/it/flags.json | 89 + public/language/it/global.json | 126 + public/language/it/groups.json | 64 + public/language/it/ip-blacklist.json | 19 + public/language/it/language.json | 5 + public/language/it/login.json | 12 + public/language/it/modules.json | 82 + public/language/it/notifications.json | 76 + public/language/it/pages.json | 65 + public/language/it/post-queue.json | 31 + public/language/it/recent.json | 19 + public/language/it/register.json | 32 + public/language/it/reset_password.json | 18 + public/language/it/search.json | 49 + public/language/it/success.json | 7 + public/language/it/tags.json | 8 + public/language/it/top.json | 4 + public/language/it/topic.json | 188 + public/language/it/unread.json | 15 + public/language/it/uploads.json | 9 + public/language/it/user.json | 199 + public/language/it/users.json | 24 + public/language/ja/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/ja/admin/admin.json | 11 + public/language/ja/admin/advanced/cache.json | 9 + .../language/ja/admin/advanced/database.json | 52 + public/language/ja/admin/advanced/errors.json | 14 + public/language/ja/admin/advanced/events.json | 13 + public/language/ja/admin/advanced/logs.json | 7 + .../ja/admin/appearance/customise.json | 16 + .../language/ja/admin/appearance/skins.json | 9 + .../language/ja/admin/appearance/themes.json | 11 + public/language/ja/admin/dashboard.json | 90 + .../language/ja/admin/development/info.json | 25 + .../language/ja/admin/development/logger.json | 12 + public/language/ja/admin/extend/plugins.json | 57 + public/language/ja/admin/extend/rewards.json | 15 + public/language/ja/admin/extend/widgets.json | 30 + .../language/ja/admin/manage/admins-mods.json | 12 + .../language/ja/admin/manage/categories.json | 92 + public/language/ja/admin/manage/digest.json | 22 + public/language/ja/admin/manage/groups.json | 44 + .../language/ja/admin/manage/privileges.json | 64 + .../ja/admin/manage/registration.json | 20 + public/language/ja/admin/manage/tags.json | 18 + public/language/ja/admin/manage/uploads.json | 11 + public/language/ja/admin/manage/users.json | 112 + public/language/ja/admin/menu.json | 89 + .../language/ja/admin/settings/advanced.json | 50 + public/language/ja/admin/settings/api.json | 16 + public/language/ja/admin/settings/chat.json | 12 + .../language/ja/admin/settings/cookies.json | 13 + public/language/ja/admin/settings/email.json | 52 + .../language/ja/admin/settings/general.json | 50 + public/language/ja/admin/settings/group.json | 13 + public/language/ja/admin/settings/guest.json | 7 + .../language/ja/admin/settings/homepage.json | 8 + .../language/ja/admin/settings/languages.json | 6 + .../ja/admin/settings/navigation.json | 25 + .../ja/admin/settings/notifications.json | 7 + .../ja/admin/settings/pagination.json | 12 + public/language/ja/admin/settings/post.json | 67 + .../ja/admin/settings/reputation.json | 31 + public/language/ja/admin/settings/social.json | 5 + .../language/ja/admin/settings/sockets.json | 6 + public/language/ja/admin/settings/sounds.json | 9 + public/language/ja/admin/settings/tags.json | 12 + .../language/ja/admin/settings/uploads.json | 45 + public/language/ja/admin/settings/user.json | 83 + .../ja/admin/settings/web-crawler.json | 10 + public/language/ja/category.json | 23 + public/language/ja/email.json | 58 + public/language/ja/error.json | 224 + public/language/ja/flags.json | 89 + public/language/ja/global.json | 126 + public/language/ja/groups.json | 64 + public/language/ja/ip-blacklist.json | 19 + public/language/ja/language.json | 5 + public/language/ja/login.json | 12 + public/language/ja/modules.json | 82 + public/language/ja/notifications.json | 76 + public/language/ja/pages.json | 65 + public/language/ja/post-queue.json | 31 + public/language/ja/recent.json | 19 + public/language/ja/register.json | 32 + public/language/ja/reset_password.json | 18 + public/language/ja/search.json | 49 + public/language/ja/success.json | 7 + public/language/ja/tags.json | 8 + public/language/ja/top.json | 4 + public/language/ja/topic.json | 188 + public/language/ja/unread.json | 15 + public/language/ja/uploads.json | 9 + public/language/ja/user.json | 199 + public/language/ja/users.json | 24 + public/language/ko/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/ko/admin/admin.json | 11 + public/language/ko/admin/advanced/cache.json | 9 + .../language/ko/admin/advanced/database.json | 52 + public/language/ko/admin/advanced/errors.json | 14 + public/language/ko/admin/advanced/events.json | 13 + public/language/ko/admin/advanced/logs.json | 7 + .../ko/admin/appearance/customise.json | 16 + .../language/ko/admin/appearance/skins.json | 9 + .../language/ko/admin/appearance/themes.json | 11 + public/language/ko/admin/dashboard.json | 90 + .../language/ko/admin/development/info.json | 25 + .../language/ko/admin/development/logger.json | 12 + public/language/ko/admin/extend/plugins.json | 57 + public/language/ko/admin/extend/rewards.json | 15 + public/language/ko/admin/extend/widgets.json | 30 + .../language/ko/admin/manage/admins-mods.json | 12 + .../language/ko/admin/manage/categories.json | 92 + public/language/ko/admin/manage/digest.json | 22 + public/language/ko/admin/manage/groups.json | 44 + .../language/ko/admin/manage/privileges.json | 64 + .../ko/admin/manage/registration.json | 20 + public/language/ko/admin/manage/tags.json | 18 + public/language/ko/admin/manage/uploads.json | 11 + public/language/ko/admin/manage/users.json | 112 + public/language/ko/admin/menu.json | 89 + .../language/ko/admin/settings/advanced.json | 50 + public/language/ko/admin/settings/api.json | 16 + public/language/ko/admin/settings/chat.json | 12 + .../language/ko/admin/settings/cookies.json | 13 + public/language/ko/admin/settings/email.json | 52 + .../language/ko/admin/settings/general.json | 50 + public/language/ko/admin/settings/group.json | 13 + public/language/ko/admin/settings/guest.json | 7 + .../language/ko/admin/settings/homepage.json | 8 + .../language/ko/admin/settings/languages.json | 6 + .../ko/admin/settings/navigation.json | 25 + .../ko/admin/settings/notifications.json | 7 + .../ko/admin/settings/pagination.json | 12 + public/language/ko/admin/settings/post.json | 67 + .../ko/admin/settings/reputation.json | 31 + public/language/ko/admin/settings/social.json | 5 + .../language/ko/admin/settings/sockets.json | 6 + public/language/ko/admin/settings/sounds.json | 9 + public/language/ko/admin/settings/tags.json | 12 + .../language/ko/admin/settings/uploads.json | 45 + public/language/ko/admin/settings/user.json | 83 + .../ko/admin/settings/web-crawler.json | 10 + public/language/ko/category.json | 23 + public/language/ko/email.json | 58 + public/language/ko/error.json | 224 + public/language/ko/flags.json | 89 + public/language/ko/global.json | 126 + public/language/ko/groups.json | 64 + public/language/ko/ip-blacklist.json | 19 + public/language/ko/language.json | 5 + public/language/ko/login.json | 12 + public/language/ko/modules.json | 82 + public/language/ko/notifications.json | 76 + public/language/ko/pages.json | 65 + public/language/ko/post-queue.json | 31 + public/language/ko/recent.json | 19 + public/language/ko/register.json | 32 + public/language/ko/reset_password.json | 18 + public/language/ko/search.json | 49 + public/language/ko/success.json | 7 + public/language/ko/tags.json | 8 + public/language/ko/top.json | 4 + public/language/ko/topic.json | 188 + public/language/ko/unread.json | 15 + public/language/ko/uploads.json | 9 + public/language/ko/user.json | 199 + public/language/ko/users.json | 24 + public/language/lt/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/lt/admin/admin.json | 11 + public/language/lt/admin/advanced/cache.json | 9 + .../language/lt/admin/advanced/database.json | 52 + public/language/lt/admin/advanced/errors.json | 14 + public/language/lt/admin/advanced/events.json | 13 + public/language/lt/admin/advanced/logs.json | 7 + .../lt/admin/appearance/customise.json | 16 + .../language/lt/admin/appearance/skins.json | 9 + .../language/lt/admin/appearance/themes.json | 11 + public/language/lt/admin/dashboard.json | 90 + .../language/lt/admin/development/info.json | 25 + .../language/lt/admin/development/logger.json | 12 + public/language/lt/admin/extend/plugins.json | 57 + public/language/lt/admin/extend/rewards.json | 15 + public/language/lt/admin/extend/widgets.json | 30 + .../language/lt/admin/manage/admins-mods.json | 12 + .../language/lt/admin/manage/categories.json | 92 + public/language/lt/admin/manage/digest.json | 22 + public/language/lt/admin/manage/groups.json | 44 + .../language/lt/admin/manage/privileges.json | 64 + .../lt/admin/manage/registration.json | 20 + public/language/lt/admin/manage/tags.json | 18 + public/language/lt/admin/manage/uploads.json | 11 + public/language/lt/admin/manage/users.json | 112 + public/language/lt/admin/menu.json | 89 + .../language/lt/admin/settings/advanced.json | 50 + public/language/lt/admin/settings/api.json | 16 + public/language/lt/admin/settings/chat.json | 12 + .../language/lt/admin/settings/cookies.json | 13 + public/language/lt/admin/settings/email.json | 52 + .../language/lt/admin/settings/general.json | 50 + public/language/lt/admin/settings/group.json | 13 + public/language/lt/admin/settings/guest.json | 7 + .../language/lt/admin/settings/homepage.json | 8 + .../language/lt/admin/settings/languages.json | 6 + .../lt/admin/settings/navigation.json | 25 + .../lt/admin/settings/notifications.json | 7 + .../lt/admin/settings/pagination.json | 12 + public/language/lt/admin/settings/post.json | 67 + .../lt/admin/settings/reputation.json | 31 + public/language/lt/admin/settings/social.json | 5 + .../language/lt/admin/settings/sockets.json | 6 + public/language/lt/admin/settings/sounds.json | 9 + public/language/lt/admin/settings/tags.json | 12 + .../language/lt/admin/settings/uploads.json | 45 + public/language/lt/admin/settings/user.json | 83 + .../lt/admin/settings/web-crawler.json | 10 + public/language/lt/category.json | 23 + public/language/lt/email.json | 58 + public/language/lt/error.json | 224 + public/language/lt/flags.json | 89 + public/language/lt/global.json | 126 + public/language/lt/groups.json | 64 + public/language/lt/ip-blacklist.json | 19 + public/language/lt/language.json | 5 + public/language/lt/login.json | 12 + public/language/lt/modules.json | 82 + public/language/lt/notifications.json | 76 + public/language/lt/pages.json | 65 + public/language/lt/post-queue.json | 31 + public/language/lt/recent.json | 19 + public/language/lt/register.json | 32 + public/language/lt/reset_password.json | 18 + public/language/lt/search.json | 49 + public/language/lt/success.json | 7 + public/language/lt/tags.json | 8 + public/language/lt/top.json | 4 + public/language/lt/topic.json | 188 + public/language/lt/unread.json | 15 + public/language/lt/uploads.json | 9 + public/language/lt/user.json | 199 + public/language/lt/users.json | 24 + public/language/lv/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/lv/admin/admin.json | 11 + public/language/lv/admin/advanced/cache.json | 9 + .../language/lv/admin/advanced/database.json | 52 + public/language/lv/admin/advanced/errors.json | 14 + public/language/lv/admin/advanced/events.json | 13 + public/language/lv/admin/advanced/logs.json | 7 + .../lv/admin/appearance/customise.json | 16 + .../language/lv/admin/appearance/skins.json | 9 + .../language/lv/admin/appearance/themes.json | 11 + public/language/lv/admin/dashboard.json | 90 + .../language/lv/admin/development/info.json | 25 + .../language/lv/admin/development/logger.json | 12 + public/language/lv/admin/extend/plugins.json | 57 + public/language/lv/admin/extend/rewards.json | 15 + public/language/lv/admin/extend/widgets.json | 30 + .../language/lv/admin/manage/admins-mods.json | 12 + .../language/lv/admin/manage/categories.json | 92 + public/language/lv/admin/manage/digest.json | 22 + public/language/lv/admin/manage/groups.json | 44 + .../language/lv/admin/manage/privileges.json | 64 + .../lv/admin/manage/registration.json | 20 + public/language/lv/admin/manage/tags.json | 18 + public/language/lv/admin/manage/uploads.json | 11 + public/language/lv/admin/manage/users.json | 112 + public/language/lv/admin/menu.json | 89 + .../language/lv/admin/settings/advanced.json | 50 + public/language/lv/admin/settings/api.json | 16 + public/language/lv/admin/settings/chat.json | 12 + .../language/lv/admin/settings/cookies.json | 13 + public/language/lv/admin/settings/email.json | 52 + .../language/lv/admin/settings/general.json | 50 + public/language/lv/admin/settings/group.json | 13 + public/language/lv/admin/settings/guest.json | 7 + .../language/lv/admin/settings/homepage.json | 8 + .../language/lv/admin/settings/languages.json | 6 + .../lv/admin/settings/navigation.json | 25 + .../lv/admin/settings/notifications.json | 7 + .../lv/admin/settings/pagination.json | 12 + public/language/lv/admin/settings/post.json | 67 + .../lv/admin/settings/reputation.json | 31 + public/language/lv/admin/settings/social.json | 5 + .../language/lv/admin/settings/sockets.json | 6 + public/language/lv/admin/settings/sounds.json | 9 + public/language/lv/admin/settings/tags.json | 12 + .../language/lv/admin/settings/uploads.json | 45 + public/language/lv/admin/settings/user.json | 83 + .../lv/admin/settings/web-crawler.json | 10 + public/language/lv/category.json | 23 + public/language/lv/email.json | 58 + public/language/lv/error.json | 224 + public/language/lv/flags.json | 89 + public/language/lv/global.json | 126 + public/language/lv/groups.json | 64 + public/language/lv/ip-blacklist.json | 19 + public/language/lv/language.json | 5 + public/language/lv/login.json | 12 + public/language/lv/modules.json | 82 + public/language/lv/notifications.json | 76 + public/language/lv/pages.json | 65 + public/language/lv/post-queue.json | 31 + public/language/lv/recent.json | 19 + public/language/lv/register.json | 32 + public/language/lv/reset_password.json | 18 + public/language/lv/search.json | 49 + public/language/lv/success.json | 7 + public/language/lv/tags.json | 8 + public/language/lv/top.json | 4 + public/language/lv/topic.json | 188 + public/language/lv/unread.json | 15 + public/language/lv/uploads.json | 9 + public/language/lv/user.json | 199 + public/language/lv/users.json | 24 + public/language/ms/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/ms/admin/admin.json | 11 + public/language/ms/admin/advanced/cache.json | 9 + .../language/ms/admin/advanced/database.json | 52 + public/language/ms/admin/advanced/errors.json | 14 + public/language/ms/admin/advanced/events.json | 13 + public/language/ms/admin/advanced/logs.json | 7 + .../ms/admin/appearance/customise.json | 16 + .../language/ms/admin/appearance/skins.json | 9 + .../language/ms/admin/appearance/themes.json | 11 + public/language/ms/admin/dashboard.json | 90 + .../language/ms/admin/development/info.json | 25 + .../language/ms/admin/development/logger.json | 12 + public/language/ms/admin/extend/plugins.json | 57 + public/language/ms/admin/extend/rewards.json | 15 + public/language/ms/admin/extend/widgets.json | 30 + .../language/ms/admin/manage/admins-mods.json | 12 + .../language/ms/admin/manage/categories.json | 92 + public/language/ms/admin/manage/digest.json | 22 + public/language/ms/admin/manage/groups.json | 44 + .../language/ms/admin/manage/privileges.json | 64 + .../ms/admin/manage/registration.json | 20 + public/language/ms/admin/manage/tags.json | 18 + public/language/ms/admin/manage/uploads.json | 11 + public/language/ms/admin/manage/users.json | 112 + public/language/ms/admin/menu.json | 89 + .../language/ms/admin/settings/advanced.json | 50 + public/language/ms/admin/settings/api.json | 16 + public/language/ms/admin/settings/chat.json | 12 + .../language/ms/admin/settings/cookies.json | 13 + public/language/ms/admin/settings/email.json | 52 + .../language/ms/admin/settings/general.json | 50 + public/language/ms/admin/settings/group.json | 13 + public/language/ms/admin/settings/guest.json | 7 + .../language/ms/admin/settings/homepage.json | 8 + .../language/ms/admin/settings/languages.json | 6 + .../ms/admin/settings/navigation.json | 25 + .../ms/admin/settings/notifications.json | 7 + .../ms/admin/settings/pagination.json | 12 + public/language/ms/admin/settings/post.json | 67 + .../ms/admin/settings/reputation.json | 31 + public/language/ms/admin/settings/social.json | 5 + .../language/ms/admin/settings/sockets.json | 6 + public/language/ms/admin/settings/sounds.json | 9 + public/language/ms/admin/settings/tags.json | 12 + .../language/ms/admin/settings/uploads.json | 45 + public/language/ms/admin/settings/user.json | 83 + .../ms/admin/settings/web-crawler.json | 10 + public/language/ms/category.json | 23 + public/language/ms/email.json | 58 + public/language/ms/error.json | 224 + public/language/ms/flags.json | 89 + public/language/ms/global.json | 126 + public/language/ms/groups.json | 64 + public/language/ms/ip-blacklist.json | 19 + public/language/ms/language.json | 5 + public/language/ms/login.json | 12 + public/language/ms/modules.json | 82 + public/language/ms/notifications.json | 76 + public/language/ms/pages.json | 65 + public/language/ms/post-queue.json | 31 + public/language/ms/recent.json | 19 + public/language/ms/register.json | 32 + public/language/ms/reset_password.json | 18 + public/language/ms/search.json | 49 + public/language/ms/success.json | 7 + public/language/ms/tags.json | 8 + public/language/ms/top.json | 4 + public/language/ms/topic.json | 188 + public/language/ms/unread.json | 15 + public/language/ms/uploads.json | 9 + public/language/ms/user.json | 199 + public/language/ms/users.json | 24 + public/language/nb/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/nb/admin/admin.json | 11 + public/language/nb/admin/advanced/cache.json | 9 + .../language/nb/admin/advanced/database.json | 52 + public/language/nb/admin/advanced/errors.json | 14 + public/language/nb/admin/advanced/events.json | 13 + public/language/nb/admin/advanced/logs.json | 7 + .../nb/admin/appearance/customise.json | 16 + .../language/nb/admin/appearance/skins.json | 9 + .../language/nb/admin/appearance/themes.json | 11 + public/language/nb/admin/dashboard.json | 90 + .../language/nb/admin/development/info.json | 25 + .../language/nb/admin/development/logger.json | 12 + public/language/nb/admin/extend/plugins.json | 57 + public/language/nb/admin/extend/rewards.json | 15 + public/language/nb/admin/extend/widgets.json | 30 + .../language/nb/admin/manage/admins-mods.json | 12 + .../language/nb/admin/manage/categories.json | 92 + public/language/nb/admin/manage/digest.json | 22 + public/language/nb/admin/manage/groups.json | 44 + .../language/nb/admin/manage/privileges.json | 64 + .../nb/admin/manage/registration.json | 20 + public/language/nb/admin/manage/tags.json | 18 + public/language/nb/admin/manage/uploads.json | 11 + public/language/nb/admin/manage/users.json | 112 + public/language/nb/admin/menu.json | 89 + .../language/nb/admin/settings/advanced.json | 50 + public/language/nb/admin/settings/api.json | 16 + public/language/nb/admin/settings/chat.json | 12 + .../language/nb/admin/settings/cookies.json | 13 + public/language/nb/admin/settings/email.json | 52 + .../language/nb/admin/settings/general.json | 50 + public/language/nb/admin/settings/group.json | 13 + public/language/nb/admin/settings/guest.json | 7 + .../language/nb/admin/settings/homepage.json | 8 + .../language/nb/admin/settings/languages.json | 6 + .../nb/admin/settings/navigation.json | 25 + .../nb/admin/settings/notifications.json | 7 + .../nb/admin/settings/pagination.json | 12 + public/language/nb/admin/settings/post.json | 67 + .../nb/admin/settings/reputation.json | 31 + public/language/nb/admin/settings/social.json | 5 + .../language/nb/admin/settings/sockets.json | 6 + public/language/nb/admin/settings/sounds.json | 9 + public/language/nb/admin/settings/tags.json | 12 + .../language/nb/admin/settings/uploads.json | 45 + public/language/nb/admin/settings/user.json | 83 + .../nb/admin/settings/web-crawler.json | 10 + public/language/nb/category.json | 23 + public/language/nb/email.json | 58 + public/language/nb/error.json | 224 + public/language/nb/flags.json | 89 + public/language/nb/global.json | 126 + public/language/nb/groups.json | 64 + public/language/nb/ip-blacklist.json | 19 + public/language/nb/language.json | 5 + public/language/nb/login.json | 12 + public/language/nb/modules.json | 82 + public/language/nb/notifications.json | 76 + public/language/nb/pages.json | 65 + public/language/nb/post-queue.json | 31 + public/language/nb/recent.json | 19 + public/language/nb/register.json | 32 + public/language/nb/reset_password.json | 18 + public/language/nb/search.json | 49 + public/language/nb/success.json | 7 + public/language/nb/tags.json | 8 + public/language/nb/top.json | 4 + public/language/nb/topic.json | 188 + public/language/nb/unread.json | 15 + public/language/nb/uploads.json | 9 + public/language/nb/user.json | 199 + public/language/nb/users.json | 24 + public/language/nl/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/nl/admin/admin.json | 11 + public/language/nl/admin/advanced/cache.json | 9 + .../language/nl/admin/advanced/database.json | 52 + public/language/nl/admin/advanced/errors.json | 14 + public/language/nl/admin/advanced/events.json | 13 + public/language/nl/admin/advanced/logs.json | 7 + .../nl/admin/appearance/customise.json | 16 + .../language/nl/admin/appearance/skins.json | 9 + .../language/nl/admin/appearance/themes.json | 11 + public/language/nl/admin/dashboard.json | 90 + .../language/nl/admin/development/info.json | 25 + .../language/nl/admin/development/logger.json | 12 + public/language/nl/admin/extend/plugins.json | 57 + public/language/nl/admin/extend/rewards.json | 15 + public/language/nl/admin/extend/widgets.json | 30 + .../language/nl/admin/manage/admins-mods.json | 12 + .../language/nl/admin/manage/categories.json | 92 + public/language/nl/admin/manage/digest.json | 22 + public/language/nl/admin/manage/groups.json | 44 + .../language/nl/admin/manage/privileges.json | 64 + .../nl/admin/manage/registration.json | 20 + public/language/nl/admin/manage/tags.json | 18 + public/language/nl/admin/manage/uploads.json | 11 + public/language/nl/admin/manage/users.json | 112 + public/language/nl/admin/menu.json | 89 + .../language/nl/admin/settings/advanced.json | 50 + public/language/nl/admin/settings/api.json | 16 + public/language/nl/admin/settings/chat.json | 12 + .../language/nl/admin/settings/cookies.json | 13 + public/language/nl/admin/settings/email.json | 52 + .../language/nl/admin/settings/general.json | 50 + public/language/nl/admin/settings/group.json | 13 + public/language/nl/admin/settings/guest.json | 7 + .../language/nl/admin/settings/homepage.json | 8 + .../language/nl/admin/settings/languages.json | 6 + .../nl/admin/settings/navigation.json | 25 + .../nl/admin/settings/notifications.json | 7 + .../nl/admin/settings/pagination.json | 12 + public/language/nl/admin/settings/post.json | 67 + .../nl/admin/settings/reputation.json | 31 + public/language/nl/admin/settings/social.json | 5 + .../language/nl/admin/settings/sockets.json | 6 + public/language/nl/admin/settings/sounds.json | 9 + public/language/nl/admin/settings/tags.json | 12 + .../language/nl/admin/settings/uploads.json | 45 + public/language/nl/admin/settings/user.json | 83 + .../nl/admin/settings/web-crawler.json | 10 + public/language/nl/category.json | 23 + public/language/nl/email.json | 58 + public/language/nl/error.json | 224 + public/language/nl/flags.json | 89 + public/language/nl/global.json | 126 + public/language/nl/groups.json | 64 + public/language/nl/ip-blacklist.json | 19 + public/language/nl/language.json | 5 + public/language/nl/login.json | 12 + public/language/nl/modules.json | 82 + public/language/nl/notifications.json | 76 + public/language/nl/pages.json | 65 + public/language/nl/post-queue.json | 31 + public/language/nl/recent.json | 19 + public/language/nl/register.json | 32 + public/language/nl/reset_password.json | 18 + public/language/nl/search.json | 49 + public/language/nl/success.json | 7 + public/language/nl/tags.json | 8 + public/language/nl/top.json | 4 + public/language/nl/topic.json | 188 + public/language/nl/unread.json | 15 + public/language/nl/uploads.json | 9 + public/language/nl/user.json | 199 + public/language/nl/users.json | 24 + public/language/pl/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/pl/admin/admin.json | 11 + public/language/pl/admin/advanced/cache.json | 9 + .../language/pl/admin/advanced/database.json | 52 + public/language/pl/admin/advanced/errors.json | 14 + public/language/pl/admin/advanced/events.json | 13 + public/language/pl/admin/advanced/logs.json | 7 + .../pl/admin/appearance/customise.json | 16 + .../language/pl/admin/appearance/skins.json | 9 + .../language/pl/admin/appearance/themes.json | 11 + public/language/pl/admin/dashboard.json | 90 + .../language/pl/admin/development/info.json | 25 + .../language/pl/admin/development/logger.json | 12 + public/language/pl/admin/extend/plugins.json | 57 + public/language/pl/admin/extend/rewards.json | 15 + public/language/pl/admin/extend/widgets.json | 30 + .../language/pl/admin/manage/admins-mods.json | 12 + .../language/pl/admin/manage/categories.json | 92 + public/language/pl/admin/manage/digest.json | 22 + public/language/pl/admin/manage/groups.json | 44 + .../language/pl/admin/manage/privileges.json | 64 + .../pl/admin/manage/registration.json | 20 + public/language/pl/admin/manage/tags.json | 18 + public/language/pl/admin/manage/uploads.json | 11 + public/language/pl/admin/manage/users.json | 112 + public/language/pl/admin/menu.json | 89 + .../language/pl/admin/settings/advanced.json | 50 + public/language/pl/admin/settings/api.json | 16 + public/language/pl/admin/settings/chat.json | 12 + .../language/pl/admin/settings/cookies.json | 13 + public/language/pl/admin/settings/email.json | 52 + .../language/pl/admin/settings/general.json | 50 + public/language/pl/admin/settings/group.json | 13 + public/language/pl/admin/settings/guest.json | 7 + .../language/pl/admin/settings/homepage.json | 8 + .../language/pl/admin/settings/languages.json | 6 + .../pl/admin/settings/navigation.json | 25 + .../pl/admin/settings/notifications.json | 7 + .../pl/admin/settings/pagination.json | 12 + public/language/pl/admin/settings/post.json | 67 + .../pl/admin/settings/reputation.json | 31 + public/language/pl/admin/settings/social.json | 5 + .../language/pl/admin/settings/sockets.json | 6 + public/language/pl/admin/settings/sounds.json | 9 + public/language/pl/admin/settings/tags.json | 12 + .../language/pl/admin/settings/uploads.json | 45 + public/language/pl/admin/settings/user.json | 83 + .../pl/admin/settings/web-crawler.json | 10 + public/language/pl/category.json | 23 + public/language/pl/email.json | 58 + public/language/pl/error.json | 224 + public/language/pl/flags.json | 89 + public/language/pl/global.json | 126 + public/language/pl/groups.json | 64 + public/language/pl/ip-blacklist.json | 19 + public/language/pl/language.json | 5 + public/language/pl/login.json | 12 + public/language/pl/modules.json | 82 + public/language/pl/notifications.json | 76 + public/language/pl/pages.json | 65 + public/language/pl/post-queue.json | 31 + public/language/pl/recent.json | 19 + public/language/pl/register.json | 32 + public/language/pl/reset_password.json | 18 + public/language/pl/search.json | 49 + public/language/pl/success.json | 7 + public/language/pl/tags.json | 8 + public/language/pl/top.json | 4 + public/language/pl/topic.json | 188 + public/language/pl/unread.json | 15 + public/language/pl/uploads.json | 9 + public/language/pl/user.json | 199 + public/language/pl/users.json | 24 + .../language/pt-BR/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/pt-BR/admin/admin.json | 11 + .../language/pt-BR/admin/advanced/cache.json | 9 + .../pt-BR/admin/advanced/database.json | 52 + .../language/pt-BR/admin/advanced/errors.json | 14 + .../language/pt-BR/admin/advanced/events.json | 13 + .../language/pt-BR/admin/advanced/logs.json | 7 + .../pt-BR/admin/appearance/customise.json | 16 + .../pt-BR/admin/appearance/skins.json | 9 + .../pt-BR/admin/appearance/themes.json | 11 + public/language/pt-BR/admin/dashboard.json | 90 + .../pt-BR/admin/development/info.json | 25 + .../pt-BR/admin/development/logger.json | 12 + .../language/pt-BR/admin/extend/plugins.json | 57 + .../language/pt-BR/admin/extend/rewards.json | 15 + .../language/pt-BR/admin/extend/widgets.json | 30 + .../pt-BR/admin/manage/admins-mods.json | 12 + .../pt-BR/admin/manage/categories.json | 92 + .../language/pt-BR/admin/manage/digest.json | 22 + .../language/pt-BR/admin/manage/groups.json | 44 + .../pt-BR/admin/manage/privileges.json | 64 + .../pt-BR/admin/manage/registration.json | 20 + public/language/pt-BR/admin/manage/tags.json | 18 + .../language/pt-BR/admin/manage/uploads.json | 11 + public/language/pt-BR/admin/manage/users.json | 112 + public/language/pt-BR/admin/menu.json | 89 + .../pt-BR/admin/settings/advanced.json | 50 + public/language/pt-BR/admin/settings/api.json | 16 + .../language/pt-BR/admin/settings/chat.json | 12 + .../pt-BR/admin/settings/cookies.json | 13 + .../language/pt-BR/admin/settings/email.json | 52 + .../pt-BR/admin/settings/general.json | 50 + .../language/pt-BR/admin/settings/group.json | 13 + .../language/pt-BR/admin/settings/guest.json | 7 + .../pt-BR/admin/settings/homepage.json | 8 + .../pt-BR/admin/settings/languages.json | 6 + .../pt-BR/admin/settings/navigation.json | 25 + .../pt-BR/admin/settings/notifications.json | 7 + .../pt-BR/admin/settings/pagination.json | 12 + .../language/pt-BR/admin/settings/post.json | 67 + .../pt-BR/admin/settings/reputation.json | 31 + .../language/pt-BR/admin/settings/social.json | 5 + .../pt-BR/admin/settings/sockets.json | 6 + .../language/pt-BR/admin/settings/sounds.json | 9 + .../language/pt-BR/admin/settings/tags.json | 12 + .../pt-BR/admin/settings/uploads.json | 45 + .../language/pt-BR/admin/settings/user.json | 83 + .../pt-BR/admin/settings/web-crawler.json | 10 + public/language/pt-BR/category.json | 23 + public/language/pt-BR/email.json | 58 + public/language/pt-BR/error.json | 224 + public/language/pt-BR/flags.json | 89 + public/language/pt-BR/global.json | 126 + public/language/pt-BR/groups.json | 64 + public/language/pt-BR/ip-blacklist.json | 19 + public/language/pt-BR/language.json | 5 + public/language/pt-BR/login.json | 12 + public/language/pt-BR/modules.json | 82 + public/language/pt-BR/notifications.json | 76 + public/language/pt-BR/pages.json | 65 + public/language/pt-BR/post-queue.json | 31 + public/language/pt-BR/recent.json | 19 + public/language/pt-BR/register.json | 32 + public/language/pt-BR/reset_password.json | 18 + public/language/pt-BR/search.json | 49 + public/language/pt-BR/success.json | 7 + public/language/pt-BR/tags.json | 8 + public/language/pt-BR/top.json | 4 + public/language/pt-BR/topic.json | 188 + public/language/pt-BR/unread.json | 15 + public/language/pt-BR/uploads.json | 9 + public/language/pt-BR/user.json | 199 + public/language/pt-BR/users.json | 24 + .../language/pt-PT/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/pt-PT/admin/admin.json | 11 + .../language/pt-PT/admin/advanced/cache.json | 9 + .../pt-PT/admin/advanced/database.json | 52 + .../language/pt-PT/admin/advanced/errors.json | 14 + .../language/pt-PT/admin/advanced/events.json | 13 + .../language/pt-PT/admin/advanced/logs.json | 7 + .../pt-PT/admin/appearance/customise.json | 16 + .../pt-PT/admin/appearance/skins.json | 9 + .../pt-PT/admin/appearance/themes.json | 11 + public/language/pt-PT/admin/dashboard.json | 90 + .../pt-PT/admin/development/info.json | 25 + .../pt-PT/admin/development/logger.json | 12 + .../language/pt-PT/admin/extend/plugins.json | 57 + .../language/pt-PT/admin/extend/rewards.json | 15 + .../language/pt-PT/admin/extend/widgets.json | 30 + .../pt-PT/admin/manage/admins-mods.json | 12 + .../pt-PT/admin/manage/categories.json | 92 + .../language/pt-PT/admin/manage/digest.json | 22 + .../language/pt-PT/admin/manage/groups.json | 44 + .../pt-PT/admin/manage/privileges.json | 64 + .../pt-PT/admin/manage/registration.json | 20 + public/language/pt-PT/admin/manage/tags.json | 18 + .../language/pt-PT/admin/manage/uploads.json | 11 + public/language/pt-PT/admin/manage/users.json | 112 + public/language/pt-PT/admin/menu.json | 89 + .../pt-PT/admin/settings/advanced.json | 50 + public/language/pt-PT/admin/settings/api.json | 16 + .../language/pt-PT/admin/settings/chat.json | 12 + .../pt-PT/admin/settings/cookies.json | 13 + .../language/pt-PT/admin/settings/email.json | 52 + .../pt-PT/admin/settings/general.json | 50 + .../language/pt-PT/admin/settings/group.json | 13 + .../language/pt-PT/admin/settings/guest.json | 7 + .../pt-PT/admin/settings/homepage.json | 8 + .../pt-PT/admin/settings/languages.json | 6 + .../pt-PT/admin/settings/navigation.json | 25 + .../pt-PT/admin/settings/notifications.json | 7 + .../pt-PT/admin/settings/pagination.json | 12 + .../language/pt-PT/admin/settings/post.json | 67 + .../pt-PT/admin/settings/reputation.json | 31 + .../language/pt-PT/admin/settings/social.json | 5 + .../pt-PT/admin/settings/sockets.json | 6 + .../language/pt-PT/admin/settings/sounds.json | 9 + .../language/pt-PT/admin/settings/tags.json | 12 + .../pt-PT/admin/settings/uploads.json | 45 + .../language/pt-PT/admin/settings/user.json | 83 + .../pt-PT/admin/settings/web-crawler.json | 10 + public/language/pt-PT/category.json | 23 + public/language/pt-PT/email.json | 58 + public/language/pt-PT/error.json | 224 + public/language/pt-PT/flags.json | 89 + public/language/pt-PT/global.json | 126 + public/language/pt-PT/groups.json | 64 + public/language/pt-PT/ip-blacklist.json | 19 + public/language/pt-PT/language.json | 5 + public/language/pt-PT/login.json | 12 + public/language/pt-PT/modules.json | 82 + public/language/pt-PT/notifications.json | 76 + public/language/pt-PT/pages.json | 65 + public/language/pt-PT/post-queue.json | 31 + public/language/pt-PT/recent.json | 19 + public/language/pt-PT/register.json | 32 + public/language/pt-PT/reset_password.json | 18 + public/language/pt-PT/search.json | 49 + public/language/pt-PT/success.json | 7 + public/language/pt-PT/tags.json | 8 + public/language/pt-PT/top.json | 4 + public/language/pt-PT/topic.json | 188 + public/language/pt-PT/unread.json | 15 + public/language/pt-PT/uploads.json | 9 + public/language/pt-PT/user.json | 199 + public/language/pt-PT/users.json | 24 + public/language/ro/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/ro/admin/admin.json | 11 + public/language/ro/admin/advanced/cache.json | 9 + .../language/ro/admin/advanced/database.json | 52 + public/language/ro/admin/advanced/errors.json | 14 + public/language/ro/admin/advanced/events.json | 13 + public/language/ro/admin/advanced/logs.json | 7 + .../ro/admin/appearance/customise.json | 16 + .../language/ro/admin/appearance/skins.json | 9 + .../language/ro/admin/appearance/themes.json | 11 + public/language/ro/admin/dashboard.json | 90 + .../language/ro/admin/development/info.json | 25 + .../language/ro/admin/development/logger.json | 12 + public/language/ro/admin/extend/plugins.json | 57 + public/language/ro/admin/extend/rewards.json | 15 + public/language/ro/admin/extend/widgets.json | 30 + .../language/ro/admin/manage/admins-mods.json | 12 + .../language/ro/admin/manage/categories.json | 92 + public/language/ro/admin/manage/digest.json | 22 + public/language/ro/admin/manage/groups.json | 44 + .../language/ro/admin/manage/privileges.json | 64 + .../ro/admin/manage/registration.json | 20 + public/language/ro/admin/manage/tags.json | 18 + public/language/ro/admin/manage/uploads.json | 11 + public/language/ro/admin/manage/users.json | 112 + public/language/ro/admin/menu.json | 89 + .../language/ro/admin/settings/advanced.json | 50 + public/language/ro/admin/settings/api.json | 16 + public/language/ro/admin/settings/chat.json | 12 + .../language/ro/admin/settings/cookies.json | 13 + public/language/ro/admin/settings/email.json | 52 + .../language/ro/admin/settings/general.json | 50 + public/language/ro/admin/settings/group.json | 13 + public/language/ro/admin/settings/guest.json | 7 + .../language/ro/admin/settings/homepage.json | 8 + .../language/ro/admin/settings/languages.json | 6 + .../ro/admin/settings/navigation.json | 25 + .../ro/admin/settings/notifications.json | 7 + .../ro/admin/settings/pagination.json | 12 + public/language/ro/admin/settings/post.json | 67 + .../ro/admin/settings/reputation.json | 31 + public/language/ro/admin/settings/social.json | 5 + .../language/ro/admin/settings/sockets.json | 6 + public/language/ro/admin/settings/sounds.json | 9 + public/language/ro/admin/settings/tags.json | 12 + .../language/ro/admin/settings/uploads.json | 45 + public/language/ro/admin/settings/user.json | 83 + .../ro/admin/settings/web-crawler.json | 10 + public/language/ro/category.json | 23 + public/language/ro/email.json | 58 + public/language/ro/error.json | 224 + public/language/ro/flags.json | 89 + public/language/ro/global.json | 126 + public/language/ro/groups.json | 64 + public/language/ro/ip-blacklist.json | 19 + public/language/ro/language.json | 5 + public/language/ro/login.json | 12 + public/language/ro/modules.json | 82 + public/language/ro/notifications.json | 76 + public/language/ro/pages.json | 65 + public/language/ro/post-queue.json | 31 + public/language/ro/recent.json | 19 + public/language/ro/register.json | 32 + public/language/ro/reset_password.json | 18 + public/language/ro/search.json | 49 + public/language/ro/success.json | 7 + public/language/ro/tags.json | 8 + public/language/ro/top.json | 4 + public/language/ro/topic.json | 188 + public/language/ro/unread.json | 15 + public/language/ro/uploads.json | 9 + public/language/ro/user.json | 199 + public/language/ro/users.json | 24 + public/language/ru/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/ru/admin/admin.json | 11 + public/language/ru/admin/advanced/cache.json | 9 + .../language/ru/admin/advanced/database.json | 52 + public/language/ru/admin/advanced/errors.json | 14 + public/language/ru/admin/advanced/events.json | 13 + public/language/ru/admin/advanced/logs.json | 7 + .../ru/admin/appearance/customise.json | 16 + .../language/ru/admin/appearance/skins.json | 9 + .../language/ru/admin/appearance/themes.json | 11 + public/language/ru/admin/dashboard.json | 90 + .../language/ru/admin/development/info.json | 25 + .../language/ru/admin/development/logger.json | 12 + public/language/ru/admin/extend/plugins.json | 57 + public/language/ru/admin/extend/rewards.json | 15 + public/language/ru/admin/extend/widgets.json | 30 + .../language/ru/admin/manage/admins-mods.json | 12 + .../language/ru/admin/manage/categories.json | 92 + public/language/ru/admin/manage/digest.json | 22 + public/language/ru/admin/manage/groups.json | 44 + .../language/ru/admin/manage/privileges.json | 64 + .../ru/admin/manage/registration.json | 20 + public/language/ru/admin/manage/tags.json | 18 + public/language/ru/admin/manage/uploads.json | 11 + public/language/ru/admin/manage/users.json | 112 + public/language/ru/admin/menu.json | 89 + .../language/ru/admin/settings/advanced.json | 50 + public/language/ru/admin/settings/api.json | 16 + public/language/ru/admin/settings/chat.json | 12 + .../language/ru/admin/settings/cookies.json | 13 + public/language/ru/admin/settings/email.json | 52 + .../language/ru/admin/settings/general.json | 50 + public/language/ru/admin/settings/group.json | 13 + public/language/ru/admin/settings/guest.json | 7 + .../language/ru/admin/settings/homepage.json | 8 + .../language/ru/admin/settings/languages.json | 6 + .../ru/admin/settings/navigation.json | 25 + .../ru/admin/settings/notifications.json | 7 + .../ru/admin/settings/pagination.json | 12 + public/language/ru/admin/settings/post.json | 67 + .../ru/admin/settings/reputation.json | 31 + public/language/ru/admin/settings/social.json | 5 + .../language/ru/admin/settings/sockets.json | 6 + public/language/ru/admin/settings/sounds.json | 9 + public/language/ru/admin/settings/tags.json | 12 + .../language/ru/admin/settings/uploads.json | 45 + public/language/ru/admin/settings/user.json | 83 + .../ru/admin/settings/web-crawler.json | 10 + public/language/ru/category.json | 23 + public/language/ru/email.json | 58 + public/language/ru/error.json | 224 + public/language/ru/flags.json | 89 + public/language/ru/global.json | 126 + public/language/ru/groups.json | 64 + public/language/ru/ip-blacklist.json | 19 + public/language/ru/language.json | 5 + public/language/ru/login.json | 12 + public/language/ru/modules.json | 82 + public/language/ru/notifications.json | 76 + public/language/ru/pages.json | 65 + public/language/ru/post-queue.json | 31 + public/language/ru/recent.json | 19 + public/language/ru/register.json | 32 + public/language/ru/reset_password.json | 18 + public/language/ru/search.json | 49 + public/language/ru/success.json | 7 + public/language/ru/tags.json | 8 + public/language/ru/top.json | 4 + public/language/ru/topic.json | 188 + public/language/ru/unread.json | 15 + public/language/ru/uploads.json | 9 + public/language/ru/user.json | 199 + public/language/ru/users.json | 24 + public/language/rw/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/rw/admin/admin.json | 11 + public/language/rw/admin/advanced/cache.json | 9 + .../language/rw/admin/advanced/database.json | 52 + public/language/rw/admin/advanced/errors.json | 14 + public/language/rw/admin/advanced/events.json | 13 + public/language/rw/admin/advanced/logs.json | 7 + .../rw/admin/appearance/customise.json | 16 + .../language/rw/admin/appearance/skins.json | 9 + .../language/rw/admin/appearance/themes.json | 11 + public/language/rw/admin/dashboard.json | 90 + .../language/rw/admin/development/info.json | 25 + .../language/rw/admin/development/logger.json | 12 + public/language/rw/admin/extend/plugins.json | 57 + public/language/rw/admin/extend/rewards.json | 15 + public/language/rw/admin/extend/widgets.json | 30 + .../language/rw/admin/manage/admins-mods.json | 12 + .../language/rw/admin/manage/categories.json | 92 + public/language/rw/admin/manage/digest.json | 22 + public/language/rw/admin/manage/groups.json | 44 + .../language/rw/admin/manage/privileges.json | 64 + .../rw/admin/manage/registration.json | 20 + public/language/rw/admin/manage/tags.json | 18 + public/language/rw/admin/manage/uploads.json | 11 + public/language/rw/admin/manage/users.json | 112 + public/language/rw/admin/menu.json | 89 + .../language/rw/admin/settings/advanced.json | 50 + public/language/rw/admin/settings/api.json | 16 + public/language/rw/admin/settings/chat.json | 12 + .../language/rw/admin/settings/cookies.json | 13 + public/language/rw/admin/settings/email.json | 52 + .../language/rw/admin/settings/general.json | 50 + public/language/rw/admin/settings/group.json | 13 + public/language/rw/admin/settings/guest.json | 7 + .../language/rw/admin/settings/homepage.json | 8 + .../language/rw/admin/settings/languages.json | 6 + .../rw/admin/settings/navigation.json | 25 + .../rw/admin/settings/notifications.json | 7 + .../rw/admin/settings/pagination.json | 12 + public/language/rw/admin/settings/post.json | 67 + .../rw/admin/settings/reputation.json | 31 + public/language/rw/admin/settings/social.json | 5 + .../language/rw/admin/settings/sockets.json | 6 + public/language/rw/admin/settings/sounds.json | 9 + public/language/rw/admin/settings/tags.json | 12 + .../language/rw/admin/settings/uploads.json | 45 + public/language/rw/admin/settings/user.json | 83 + .../rw/admin/settings/web-crawler.json | 10 + public/language/rw/category.json | 23 + public/language/rw/email.json | 58 + public/language/rw/error.json | 224 + public/language/rw/flags.json | 89 + public/language/rw/global.json | 126 + public/language/rw/groups.json | 64 + public/language/rw/ip-blacklist.json | 19 + public/language/rw/language.json | 5 + public/language/rw/login.json | 12 + public/language/rw/modules.json | 82 + public/language/rw/notifications.json | 76 + public/language/rw/pages.json | 65 + public/language/rw/post-queue.json | 31 + public/language/rw/recent.json | 19 + public/language/rw/register.json | 32 + public/language/rw/reset_password.json | 18 + public/language/rw/search.json | 49 + public/language/rw/success.json | 7 + public/language/rw/tags.json | 8 + public/language/rw/top.json | 4 + public/language/rw/topic.json | 188 + public/language/rw/unread.json | 15 + public/language/rw/uploads.json | 9 + public/language/rw/user.json | 199 + public/language/rw/users.json | 24 + public/language/sc/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/sc/admin/admin.json | 11 + public/language/sc/admin/advanced/cache.json | 9 + .../language/sc/admin/advanced/database.json | 52 + public/language/sc/admin/advanced/errors.json | 14 + public/language/sc/admin/advanced/events.json | 13 + public/language/sc/admin/advanced/logs.json | 7 + .../sc/admin/appearance/customise.json | 16 + .../language/sc/admin/appearance/skins.json | 9 + .../language/sc/admin/appearance/themes.json | 11 + public/language/sc/admin/dashboard.json | 90 + .../language/sc/admin/development/info.json | 25 + .../language/sc/admin/development/logger.json | 12 + public/language/sc/admin/extend/plugins.json | 57 + public/language/sc/admin/extend/rewards.json | 15 + public/language/sc/admin/extend/widgets.json | 30 + .../language/sc/admin/manage/admins-mods.json | 12 + .../language/sc/admin/manage/categories.json | 92 + public/language/sc/admin/manage/digest.json | 22 + public/language/sc/admin/manage/groups.json | 44 + .../language/sc/admin/manage/privileges.json | 64 + .../sc/admin/manage/registration.json | 20 + public/language/sc/admin/manage/tags.json | 18 + public/language/sc/admin/manage/uploads.json | 11 + public/language/sc/admin/manage/users.json | 112 + public/language/sc/admin/menu.json | 89 + .../language/sc/admin/settings/advanced.json | 50 + public/language/sc/admin/settings/api.json | 16 + public/language/sc/admin/settings/chat.json | 12 + .../language/sc/admin/settings/cookies.json | 13 + public/language/sc/admin/settings/email.json | 52 + .../language/sc/admin/settings/general.json | 50 + public/language/sc/admin/settings/group.json | 13 + public/language/sc/admin/settings/guest.json | 7 + .../language/sc/admin/settings/homepage.json | 8 + .../language/sc/admin/settings/languages.json | 6 + .../sc/admin/settings/navigation.json | 25 + .../sc/admin/settings/notifications.json | 7 + .../sc/admin/settings/pagination.json | 12 + public/language/sc/admin/settings/post.json | 67 + .../sc/admin/settings/reputation.json | 31 + public/language/sc/admin/settings/social.json | 5 + .../language/sc/admin/settings/sockets.json | 6 + public/language/sc/admin/settings/sounds.json | 9 + public/language/sc/admin/settings/tags.json | 12 + .../language/sc/admin/settings/uploads.json | 45 + public/language/sc/admin/settings/user.json | 83 + .../sc/admin/settings/web-crawler.json | 10 + public/language/sc/category.json | 23 + public/language/sc/email.json | 58 + public/language/sc/error.json | 224 + public/language/sc/flags.json | 89 + public/language/sc/global.json | 126 + public/language/sc/groups.json | 64 + public/language/sc/ip-blacklist.json | 19 + public/language/sc/language.json | 5 + public/language/sc/login.json | 12 + public/language/sc/modules.json | 82 + public/language/sc/notifications.json | 76 + public/language/sc/pages.json | 65 + public/language/sc/post-queue.json | 31 + public/language/sc/recent.json | 19 + public/language/sc/register.json | 32 + public/language/sc/reset_password.json | 18 + public/language/sc/search.json | 49 + public/language/sc/success.json | 7 + public/language/sc/tags.json | 8 + public/language/sc/top.json | 4 + public/language/sc/topic.json | 188 + public/language/sc/unread.json | 15 + public/language/sc/uploads.json | 9 + public/language/sc/user.json | 199 + public/language/sc/users.json | 24 + public/language/sk/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/sk/admin/admin.json | 11 + public/language/sk/admin/advanced/cache.json | 9 + .../language/sk/admin/advanced/database.json | 52 + public/language/sk/admin/advanced/errors.json | 14 + public/language/sk/admin/advanced/events.json | 13 + public/language/sk/admin/advanced/logs.json | 7 + .../sk/admin/appearance/customise.json | 16 + .../language/sk/admin/appearance/skins.json | 9 + .../language/sk/admin/appearance/themes.json | 11 + public/language/sk/admin/dashboard.json | 90 + .../language/sk/admin/development/info.json | 25 + .../language/sk/admin/development/logger.json | 12 + public/language/sk/admin/extend/plugins.json | 57 + public/language/sk/admin/extend/rewards.json | 15 + public/language/sk/admin/extend/widgets.json | 30 + .../language/sk/admin/manage/admins-mods.json | 12 + .../language/sk/admin/manage/categories.json | 92 + public/language/sk/admin/manage/digest.json | 22 + public/language/sk/admin/manage/groups.json | 44 + .../language/sk/admin/manage/privileges.json | 64 + .../sk/admin/manage/registration.json | 20 + public/language/sk/admin/manage/tags.json | 18 + public/language/sk/admin/manage/uploads.json | 11 + public/language/sk/admin/manage/users.json | 112 + public/language/sk/admin/menu.json | 89 + .../language/sk/admin/settings/advanced.json | 50 + public/language/sk/admin/settings/api.json | 16 + public/language/sk/admin/settings/chat.json | 12 + .../language/sk/admin/settings/cookies.json | 13 + public/language/sk/admin/settings/email.json | 52 + .../language/sk/admin/settings/general.json | 50 + public/language/sk/admin/settings/group.json | 13 + public/language/sk/admin/settings/guest.json | 7 + .../language/sk/admin/settings/homepage.json | 8 + .../language/sk/admin/settings/languages.json | 6 + .../sk/admin/settings/navigation.json | 25 + .../sk/admin/settings/notifications.json | 7 + .../sk/admin/settings/pagination.json | 12 + public/language/sk/admin/settings/post.json | 67 + .../sk/admin/settings/reputation.json | 31 + public/language/sk/admin/settings/social.json | 5 + .../language/sk/admin/settings/sockets.json | 6 + public/language/sk/admin/settings/sounds.json | 9 + public/language/sk/admin/settings/tags.json | 12 + .../language/sk/admin/settings/uploads.json | 45 + public/language/sk/admin/settings/user.json | 83 + .../sk/admin/settings/web-crawler.json | 10 + public/language/sk/category.json | 23 + public/language/sk/email.json | 58 + public/language/sk/error.json | 224 + public/language/sk/flags.json | 89 + public/language/sk/global.json | 126 + public/language/sk/groups.json | 64 + public/language/sk/ip-blacklist.json | 19 + public/language/sk/language.json | 5 + public/language/sk/login.json | 12 + public/language/sk/modules.json | 82 + public/language/sk/notifications.json | 76 + public/language/sk/pages.json | 65 + public/language/sk/post-queue.json | 31 + public/language/sk/recent.json | 19 + public/language/sk/register.json | 32 + public/language/sk/reset_password.json | 18 + public/language/sk/search.json | 49 + public/language/sk/success.json | 7 + public/language/sk/tags.json | 8 + public/language/sk/top.json | 4 + public/language/sk/topic.json | 188 + public/language/sk/unread.json | 15 + public/language/sk/uploads.json | 9 + public/language/sk/user.json | 199 + public/language/sk/users.json | 24 + public/language/sl/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/sl/admin/admin.json | 11 + public/language/sl/admin/advanced/cache.json | 9 + .../language/sl/admin/advanced/database.json | 52 + public/language/sl/admin/advanced/errors.json | 14 + public/language/sl/admin/advanced/events.json | 13 + public/language/sl/admin/advanced/logs.json | 7 + .../sl/admin/appearance/customise.json | 16 + .../language/sl/admin/appearance/skins.json | 9 + .../language/sl/admin/appearance/themes.json | 11 + public/language/sl/admin/dashboard.json | 90 + .../language/sl/admin/development/info.json | 25 + .../language/sl/admin/development/logger.json | 12 + public/language/sl/admin/extend/plugins.json | 57 + public/language/sl/admin/extend/rewards.json | 15 + public/language/sl/admin/extend/widgets.json | 30 + .../language/sl/admin/manage/admins-mods.json | 12 + .../language/sl/admin/manage/categories.json | 92 + public/language/sl/admin/manage/digest.json | 22 + public/language/sl/admin/manage/groups.json | 44 + .../language/sl/admin/manage/privileges.json | 64 + .../sl/admin/manage/registration.json | 20 + public/language/sl/admin/manage/tags.json | 18 + public/language/sl/admin/manage/uploads.json | 11 + public/language/sl/admin/manage/users.json | 112 + public/language/sl/admin/menu.json | 89 + .../language/sl/admin/settings/advanced.json | 50 + public/language/sl/admin/settings/api.json | 16 + public/language/sl/admin/settings/chat.json | 12 + .../language/sl/admin/settings/cookies.json | 13 + public/language/sl/admin/settings/email.json | 52 + .../language/sl/admin/settings/general.json | 50 + public/language/sl/admin/settings/group.json | 13 + public/language/sl/admin/settings/guest.json | 7 + .../language/sl/admin/settings/homepage.json | 8 + .../language/sl/admin/settings/languages.json | 6 + .../sl/admin/settings/navigation.json | 25 + .../sl/admin/settings/notifications.json | 7 + .../sl/admin/settings/pagination.json | 12 + public/language/sl/admin/settings/post.json | 67 + .../sl/admin/settings/reputation.json | 31 + public/language/sl/admin/settings/social.json | 5 + .../language/sl/admin/settings/sockets.json | 6 + public/language/sl/admin/settings/sounds.json | 9 + public/language/sl/admin/settings/tags.json | 12 + .../language/sl/admin/settings/uploads.json | 45 + public/language/sl/admin/settings/user.json | 83 + .../sl/admin/settings/web-crawler.json | 10 + public/language/sl/category.json | 23 + public/language/sl/email.json | 58 + public/language/sl/error.json | 224 + public/language/sl/flags.json | 89 + public/language/sl/global.json | 126 + public/language/sl/groups.json | 64 + public/language/sl/ip-blacklist.json | 19 + public/language/sl/language.json | 5 + public/language/sl/login.json | 12 + public/language/sl/modules.json | 82 + public/language/sl/notifications.json | 76 + public/language/sl/pages.json | 65 + public/language/sl/post-queue.json | 31 + public/language/sl/recent.json | 19 + public/language/sl/register.json | 32 + public/language/sl/reset_password.json | 18 + public/language/sl/search.json | 49 + public/language/sl/success.json | 7 + public/language/sl/tags.json | 8 + public/language/sl/top.json | 4 + public/language/sl/topic.json | 188 + public/language/sl/unread.json | 15 + public/language/sl/uploads.json | 9 + public/language/sl/user.json | 199 + public/language/sl/users.json | 24 + .../language/sq-AL/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/sq-AL/admin/admin.json | 11 + .../language/sq-AL/admin/advanced/cache.json | 9 + .../sq-AL/admin/advanced/database.json | 52 + .../language/sq-AL/admin/advanced/errors.json | 14 + .../language/sq-AL/admin/advanced/events.json | 13 + .../language/sq-AL/admin/advanced/logs.json | 7 + .../sq-AL/admin/appearance/customise.json | 16 + .../sq-AL/admin/appearance/skins.json | 9 + .../sq-AL/admin/appearance/themes.json | 11 + public/language/sq-AL/admin/dashboard.json | 90 + .../sq-AL/admin/development/info.json | 25 + .../sq-AL/admin/development/logger.json | 12 + .../language/sq-AL/admin/extend/plugins.json | 57 + .../language/sq-AL/admin/extend/rewards.json | 15 + .../language/sq-AL/admin/extend/widgets.json | 30 + .../sq-AL/admin/manage/admins-mods.json | 12 + .../sq-AL/admin/manage/categories.json | 92 + .../language/sq-AL/admin/manage/digest.json | 22 + .../language/sq-AL/admin/manage/groups.json | 44 + .../sq-AL/admin/manage/privileges.json | 64 + .../sq-AL/admin/manage/registration.json | 20 + public/language/sq-AL/admin/manage/tags.json | 18 + .../language/sq-AL/admin/manage/uploads.json | 11 + public/language/sq-AL/admin/manage/users.json | 112 + public/language/sq-AL/admin/menu.json | 89 + .../sq-AL/admin/settings/advanced.json | 50 + public/language/sq-AL/admin/settings/api.json | 16 + .../language/sq-AL/admin/settings/chat.json | 12 + .../sq-AL/admin/settings/cookies.json | 13 + .../language/sq-AL/admin/settings/email.json | 52 + .../sq-AL/admin/settings/general.json | 50 + .../language/sq-AL/admin/settings/group.json | 13 + .../language/sq-AL/admin/settings/guest.json | 7 + .../sq-AL/admin/settings/homepage.json | 8 + .../sq-AL/admin/settings/languages.json | 6 + .../sq-AL/admin/settings/navigation.json | 25 + .../sq-AL/admin/settings/notifications.json | 7 + .../sq-AL/admin/settings/pagination.json | 12 + .../language/sq-AL/admin/settings/post.json | 67 + .../sq-AL/admin/settings/reputation.json | 31 + .../language/sq-AL/admin/settings/social.json | 5 + .../sq-AL/admin/settings/sockets.json | 6 + .../language/sq-AL/admin/settings/sounds.json | 9 + .../language/sq-AL/admin/settings/tags.json | 12 + .../sq-AL/admin/settings/uploads.json | 45 + .../language/sq-AL/admin/settings/user.json | 83 + .../sq-AL/admin/settings/web-crawler.json | 10 + public/language/sq-AL/category.json | 23 + public/language/sq-AL/email.json | 58 + public/language/sq-AL/error.json | 224 + public/language/sq-AL/flags.json | 89 + public/language/sq-AL/global.json | 126 + public/language/sq-AL/groups.json | 64 + public/language/sq-AL/ip-blacklist.json | 19 + public/language/sq-AL/language.json | 5 + public/language/sq-AL/login.json | 12 + public/language/sq-AL/modules.json | 82 + public/language/sq-AL/notifications.json | 76 + public/language/sq-AL/pages.json | 65 + public/language/sq-AL/post-queue.json | 31 + public/language/sq-AL/recent.json | 19 + public/language/sq-AL/register.json | 32 + public/language/sq-AL/reset_password.json | 18 + public/language/sq-AL/search.json | 49 + public/language/sq-AL/success.json | 7 + public/language/sq-AL/tags.json | 8 + public/language/sq-AL/top.json | 4 + public/language/sq-AL/topic.json | 188 + public/language/sq-AL/unread.json | 15 + public/language/sq-AL/uploads.json | 9 + public/language/sq-AL/user.json | 199 + public/language/sq-AL/users.json | 24 + public/language/sr/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/sr/admin/admin.json | 11 + public/language/sr/admin/advanced/cache.json | 9 + .../language/sr/admin/advanced/database.json | 52 + public/language/sr/admin/advanced/errors.json | 14 + public/language/sr/admin/advanced/events.json | 13 + public/language/sr/admin/advanced/logs.json | 7 + .../sr/admin/appearance/customise.json | 16 + .../language/sr/admin/appearance/skins.json | 9 + .../language/sr/admin/appearance/themes.json | 11 + public/language/sr/admin/dashboard.json | 90 + .../language/sr/admin/development/info.json | 25 + .../language/sr/admin/development/logger.json | 12 + public/language/sr/admin/extend/plugins.json | 57 + public/language/sr/admin/extend/rewards.json | 15 + public/language/sr/admin/extend/widgets.json | 30 + .../language/sr/admin/manage/admins-mods.json | 12 + .../language/sr/admin/manage/categories.json | 92 + public/language/sr/admin/manage/digest.json | 22 + public/language/sr/admin/manage/groups.json | 44 + .../language/sr/admin/manage/privileges.json | 64 + .../sr/admin/manage/registration.json | 20 + public/language/sr/admin/manage/tags.json | 18 + public/language/sr/admin/manage/uploads.json | 11 + public/language/sr/admin/manage/users.json | 112 + public/language/sr/admin/menu.json | 89 + .../language/sr/admin/settings/advanced.json | 50 + public/language/sr/admin/settings/api.json | 16 + public/language/sr/admin/settings/chat.json | 12 + .../language/sr/admin/settings/cookies.json | 13 + public/language/sr/admin/settings/email.json | 52 + .../language/sr/admin/settings/general.json | 50 + public/language/sr/admin/settings/group.json | 13 + public/language/sr/admin/settings/guest.json | 7 + .../language/sr/admin/settings/homepage.json | 8 + .../language/sr/admin/settings/languages.json | 6 + .../sr/admin/settings/navigation.json | 25 + .../sr/admin/settings/notifications.json | 7 + .../sr/admin/settings/pagination.json | 12 + public/language/sr/admin/settings/post.json | 67 + .../sr/admin/settings/reputation.json | 31 + public/language/sr/admin/settings/social.json | 5 + .../language/sr/admin/settings/sockets.json | 6 + public/language/sr/admin/settings/sounds.json | 9 + public/language/sr/admin/settings/tags.json | 12 + .../language/sr/admin/settings/uploads.json | 45 + public/language/sr/admin/settings/user.json | 83 + .../sr/admin/settings/web-crawler.json | 10 + public/language/sr/category.json | 23 + public/language/sr/email.json | 58 + public/language/sr/error.json | 224 + public/language/sr/flags.json | 89 + public/language/sr/global.json | 126 + public/language/sr/groups.json | 64 + public/language/sr/ip-blacklist.json | 19 + public/language/sr/language.json | 5 + public/language/sr/login.json | 12 + public/language/sr/modules.json | 82 + public/language/sr/notifications.json | 76 + public/language/sr/pages.json | 65 + public/language/sr/post-queue.json | 31 + public/language/sr/recent.json | 19 + public/language/sr/register.json | 32 + public/language/sr/reset_password.json | 18 + public/language/sr/search.json | 49 + public/language/sr/success.json | 7 + public/language/sr/tags.json | 8 + public/language/sr/top.json | 4 + public/language/sr/topic.json | 188 + public/language/sr/unread.json | 15 + public/language/sr/uploads.json | 9 + public/language/sr/user.json | 199 + public/language/sr/users.json | 24 + public/language/sv/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/sv/admin/admin.json | 11 + public/language/sv/admin/advanced/cache.json | 9 + .../language/sv/admin/advanced/database.json | 52 + public/language/sv/admin/advanced/errors.json | 14 + public/language/sv/admin/advanced/events.json | 13 + public/language/sv/admin/advanced/logs.json | 7 + .../sv/admin/appearance/customise.json | 16 + .../language/sv/admin/appearance/skins.json | 9 + .../language/sv/admin/appearance/themes.json | 11 + public/language/sv/admin/dashboard.json | 90 + .../language/sv/admin/development/info.json | 25 + .../language/sv/admin/development/logger.json | 12 + public/language/sv/admin/extend/plugins.json | 57 + public/language/sv/admin/extend/rewards.json | 15 + public/language/sv/admin/extend/widgets.json | 30 + .../language/sv/admin/manage/admins-mods.json | 12 + .../language/sv/admin/manage/categories.json | 92 + public/language/sv/admin/manage/digest.json | 22 + public/language/sv/admin/manage/groups.json | 44 + .../language/sv/admin/manage/privileges.json | 64 + .../sv/admin/manage/registration.json | 20 + public/language/sv/admin/manage/tags.json | 18 + public/language/sv/admin/manage/uploads.json | 11 + public/language/sv/admin/manage/users.json | 112 + public/language/sv/admin/menu.json | 89 + .../language/sv/admin/settings/advanced.json | 50 + public/language/sv/admin/settings/api.json | 16 + public/language/sv/admin/settings/chat.json | 12 + .../language/sv/admin/settings/cookies.json | 13 + public/language/sv/admin/settings/email.json | 52 + .../language/sv/admin/settings/general.json | 50 + public/language/sv/admin/settings/group.json | 13 + public/language/sv/admin/settings/guest.json | 7 + .../language/sv/admin/settings/homepage.json | 8 + .../language/sv/admin/settings/languages.json | 6 + .../sv/admin/settings/navigation.json | 25 + .../sv/admin/settings/notifications.json | 7 + .../sv/admin/settings/pagination.json | 12 + public/language/sv/admin/settings/post.json | 67 + .../sv/admin/settings/reputation.json | 31 + public/language/sv/admin/settings/social.json | 5 + .../language/sv/admin/settings/sockets.json | 6 + public/language/sv/admin/settings/sounds.json | 9 + public/language/sv/admin/settings/tags.json | 12 + .../language/sv/admin/settings/uploads.json | 45 + public/language/sv/admin/settings/user.json | 83 + .../sv/admin/settings/web-crawler.json | 10 + public/language/sv/category.json | 23 + public/language/sv/email.json | 58 + public/language/sv/error.json | 224 + public/language/sv/flags.json | 89 + public/language/sv/global.json | 126 + public/language/sv/groups.json | 64 + public/language/sv/ip-blacklist.json | 19 + public/language/sv/language.json | 5 + public/language/sv/login.json | 12 + public/language/sv/modules.json | 82 + public/language/sv/notifications.json | 76 + public/language/sv/pages.json | 65 + public/language/sv/post-queue.json | 31 + public/language/sv/recent.json | 19 + public/language/sv/register.json | 32 + public/language/sv/reset_password.json | 18 + public/language/sv/search.json | 49 + public/language/sv/success.json | 7 + public/language/sv/tags.json | 8 + public/language/sv/top.json | 4 + public/language/sv/topic.json | 188 + public/language/sv/unread.json | 15 + public/language/sv/uploads.json | 9 + public/language/sv/user.json | 199 + public/language/sv/users.json | 24 + public/language/th/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/th/admin/admin.json | 11 + public/language/th/admin/advanced/cache.json | 9 + .../language/th/admin/advanced/database.json | 52 + public/language/th/admin/advanced/errors.json | 14 + public/language/th/admin/advanced/events.json | 13 + public/language/th/admin/advanced/logs.json | 7 + .../th/admin/appearance/customise.json | 16 + .../language/th/admin/appearance/skins.json | 9 + .../language/th/admin/appearance/themes.json | 11 + public/language/th/admin/dashboard.json | 90 + .../language/th/admin/development/info.json | 25 + .../language/th/admin/development/logger.json | 12 + public/language/th/admin/extend/plugins.json | 57 + public/language/th/admin/extend/rewards.json | 15 + public/language/th/admin/extend/widgets.json | 30 + .../language/th/admin/manage/admins-mods.json | 12 + .../language/th/admin/manage/categories.json | 92 + public/language/th/admin/manage/digest.json | 22 + public/language/th/admin/manage/groups.json | 44 + .../language/th/admin/manage/privileges.json | 64 + .../th/admin/manage/registration.json | 20 + public/language/th/admin/manage/tags.json | 18 + public/language/th/admin/manage/uploads.json | 11 + public/language/th/admin/manage/users.json | 112 + public/language/th/admin/menu.json | 89 + .../language/th/admin/settings/advanced.json | 50 + public/language/th/admin/settings/api.json | 16 + public/language/th/admin/settings/chat.json | 12 + .../language/th/admin/settings/cookies.json | 13 + public/language/th/admin/settings/email.json | 52 + .../language/th/admin/settings/general.json | 50 + public/language/th/admin/settings/group.json | 13 + public/language/th/admin/settings/guest.json | 7 + .../language/th/admin/settings/homepage.json | 8 + .../language/th/admin/settings/languages.json | 6 + .../th/admin/settings/navigation.json | 25 + .../th/admin/settings/notifications.json | 7 + .../th/admin/settings/pagination.json | 12 + public/language/th/admin/settings/post.json | 67 + .../th/admin/settings/reputation.json | 31 + public/language/th/admin/settings/social.json | 5 + .../language/th/admin/settings/sockets.json | 6 + public/language/th/admin/settings/sounds.json | 9 + public/language/th/admin/settings/tags.json | 12 + .../language/th/admin/settings/uploads.json | 45 + public/language/th/admin/settings/user.json | 83 + .../th/admin/settings/web-crawler.json | 10 + public/language/th/category.json | 23 + public/language/th/email.json | 58 + public/language/th/error.json | 224 + public/language/th/flags.json | 89 + public/language/th/global.json | 126 + public/language/th/groups.json | 64 + public/language/th/ip-blacklist.json | 19 + public/language/th/language.json | 5 + public/language/th/login.json | 12 + public/language/th/modules.json | 82 + public/language/th/notifications.json | 76 + public/language/th/pages.json | 65 + public/language/th/post-queue.json | 31 + public/language/th/recent.json | 19 + public/language/th/register.json | 32 + public/language/th/reset_password.json | 18 + public/language/th/search.json | 49 + public/language/th/success.json | 7 + public/language/th/tags.json | 8 + public/language/th/top.json | 4 + public/language/th/topic.json | 188 + public/language/th/unread.json | 15 + public/language/th/uploads.json | 9 + public/language/th/user.json | 199 + public/language/th/users.json | 24 + public/language/tr/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/tr/admin/admin.json | 11 + public/language/tr/admin/advanced/cache.json | 9 + .../language/tr/admin/advanced/database.json | 52 + public/language/tr/admin/advanced/errors.json | 14 + public/language/tr/admin/advanced/events.json | 13 + public/language/tr/admin/advanced/logs.json | 7 + .../tr/admin/appearance/customise.json | 16 + .../language/tr/admin/appearance/skins.json | 9 + .../language/tr/admin/appearance/themes.json | 11 + public/language/tr/admin/dashboard.json | 90 + .../language/tr/admin/development/info.json | 25 + .../language/tr/admin/development/logger.json | 12 + public/language/tr/admin/extend/plugins.json | 57 + public/language/tr/admin/extend/rewards.json | 15 + public/language/tr/admin/extend/widgets.json | 30 + .../language/tr/admin/manage/admins-mods.json | 12 + .../language/tr/admin/manage/categories.json | 92 + public/language/tr/admin/manage/digest.json | 22 + public/language/tr/admin/manage/groups.json | 44 + .../language/tr/admin/manage/privileges.json | 64 + .../tr/admin/manage/registration.json | 20 + public/language/tr/admin/manage/tags.json | 18 + public/language/tr/admin/manage/uploads.json | 11 + public/language/tr/admin/manage/users.json | 112 + public/language/tr/admin/menu.json | 89 + .../language/tr/admin/settings/advanced.json | 50 + public/language/tr/admin/settings/api.json | 16 + public/language/tr/admin/settings/chat.json | 12 + .../language/tr/admin/settings/cookies.json | 13 + public/language/tr/admin/settings/email.json | 52 + .../language/tr/admin/settings/general.json | 50 + public/language/tr/admin/settings/group.json | 13 + public/language/tr/admin/settings/guest.json | 7 + .../language/tr/admin/settings/homepage.json | 8 + .../language/tr/admin/settings/languages.json | 6 + .../tr/admin/settings/navigation.json | 25 + .../tr/admin/settings/notifications.json | 7 + .../tr/admin/settings/pagination.json | 12 + public/language/tr/admin/settings/post.json | 67 + .../tr/admin/settings/reputation.json | 31 + public/language/tr/admin/settings/social.json | 5 + .../language/tr/admin/settings/sockets.json | 6 + public/language/tr/admin/settings/sounds.json | 9 + public/language/tr/admin/settings/tags.json | 12 + .../language/tr/admin/settings/uploads.json | 45 + public/language/tr/admin/settings/user.json | 83 + .../tr/admin/settings/web-crawler.json | 10 + public/language/tr/category.json | 23 + public/language/tr/email.json | 58 + public/language/tr/error.json | 224 + public/language/tr/flags.json | 89 + public/language/tr/global.json | 126 + public/language/tr/groups.json | 64 + public/language/tr/ip-blacklist.json | 19 + public/language/tr/language.json | 5 + public/language/tr/login.json | 12 + public/language/tr/modules.json | 82 + public/language/tr/notifications.json | 76 + public/language/tr/pages.json | 65 + public/language/tr/post-queue.json | 31 + public/language/tr/recent.json | 19 + public/language/tr/register.json | 32 + public/language/tr/reset_password.json | 18 + public/language/tr/search.json | 49 + public/language/tr/success.json | 7 + public/language/tr/tags.json | 8 + public/language/tr/top.json | 4 + public/language/tr/topic.json | 188 + public/language/tr/unread.json | 15 + public/language/tr/uploads.json | 9 + public/language/tr/user.json | 199 + public/language/tr/users.json | 24 + public/language/uk/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/uk/admin/admin.json | 11 + public/language/uk/admin/advanced/cache.json | 9 + .../language/uk/admin/advanced/database.json | 52 + public/language/uk/admin/advanced/errors.json | 14 + public/language/uk/admin/advanced/events.json | 13 + public/language/uk/admin/advanced/logs.json | 7 + .../uk/admin/appearance/customise.json | 16 + .../language/uk/admin/appearance/skins.json | 9 + .../language/uk/admin/appearance/themes.json | 11 + public/language/uk/admin/dashboard.json | 90 + .../language/uk/admin/development/info.json | 25 + .../language/uk/admin/development/logger.json | 12 + public/language/uk/admin/extend/plugins.json | 57 + public/language/uk/admin/extend/rewards.json | 15 + public/language/uk/admin/extend/widgets.json | 30 + .../language/uk/admin/manage/admins-mods.json | 12 + .../language/uk/admin/manage/categories.json | 92 + public/language/uk/admin/manage/digest.json | 22 + public/language/uk/admin/manage/groups.json | 44 + .../language/uk/admin/manage/privileges.json | 64 + .../uk/admin/manage/registration.json | 20 + public/language/uk/admin/manage/tags.json | 18 + public/language/uk/admin/manage/uploads.json | 11 + public/language/uk/admin/manage/users.json | 112 + public/language/uk/admin/menu.json | 89 + .../language/uk/admin/settings/advanced.json | 50 + public/language/uk/admin/settings/api.json | 16 + public/language/uk/admin/settings/chat.json | 12 + .../language/uk/admin/settings/cookies.json | 13 + public/language/uk/admin/settings/email.json | 52 + .../language/uk/admin/settings/general.json | 50 + public/language/uk/admin/settings/group.json | 13 + public/language/uk/admin/settings/guest.json | 7 + .../language/uk/admin/settings/homepage.json | 8 + .../language/uk/admin/settings/languages.json | 6 + .../uk/admin/settings/navigation.json | 25 + .../uk/admin/settings/notifications.json | 7 + .../uk/admin/settings/pagination.json | 12 + public/language/uk/admin/settings/post.json | 67 + .../uk/admin/settings/reputation.json | 31 + public/language/uk/admin/settings/social.json | 5 + .../language/uk/admin/settings/sockets.json | 6 + public/language/uk/admin/settings/sounds.json | 9 + public/language/uk/admin/settings/tags.json | 12 + .../language/uk/admin/settings/uploads.json | 45 + public/language/uk/admin/settings/user.json | 83 + .../uk/admin/settings/web-crawler.json | 10 + public/language/uk/category.json | 23 + public/language/uk/email.json | 58 + public/language/uk/error.json | 224 + public/language/uk/flags.json | 89 + public/language/uk/global.json | 126 + public/language/uk/groups.json | 64 + public/language/uk/ip-blacklist.json | 19 + public/language/uk/language.json | 5 + public/language/uk/login.json | 12 + public/language/uk/modules.json | 82 + public/language/uk/notifications.json | 76 + public/language/uk/pages.json | 65 + public/language/uk/post-queue.json | 31 + public/language/uk/recent.json | 19 + public/language/uk/register.json | 32 + public/language/uk/reset_password.json | 18 + public/language/uk/search.json | 49 + public/language/uk/success.json | 7 + public/language/uk/tags.json | 8 + public/language/uk/top.json | 4 + public/language/uk/topic.json | 188 + public/language/uk/unread.json | 15 + public/language/uk/uploads.json | 9 + public/language/uk/user.json | 199 + public/language/uk/users.json | 24 + public/language/vi/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/vi/admin/admin.json | 11 + public/language/vi/admin/advanced/cache.json | 9 + .../language/vi/admin/advanced/database.json | 52 + public/language/vi/admin/advanced/errors.json | 14 + public/language/vi/admin/advanced/events.json | 13 + public/language/vi/admin/advanced/logs.json | 7 + .../vi/admin/appearance/customise.json | 16 + .../language/vi/admin/appearance/skins.json | 9 + .../language/vi/admin/appearance/themes.json | 11 + public/language/vi/admin/dashboard.json | 90 + .../language/vi/admin/development/info.json | 25 + .../language/vi/admin/development/logger.json | 12 + public/language/vi/admin/extend/plugins.json | 57 + public/language/vi/admin/extend/rewards.json | 15 + public/language/vi/admin/extend/widgets.json | 30 + .../language/vi/admin/manage/admins-mods.json | 12 + .../language/vi/admin/manage/categories.json | 92 + public/language/vi/admin/manage/digest.json | 22 + public/language/vi/admin/manage/groups.json | 44 + .../language/vi/admin/manage/privileges.json | 64 + .../vi/admin/manage/registration.json | 20 + public/language/vi/admin/manage/tags.json | 18 + public/language/vi/admin/manage/uploads.json | 11 + public/language/vi/admin/manage/users.json | 112 + public/language/vi/admin/menu.json | 89 + .../language/vi/admin/settings/advanced.json | 50 + public/language/vi/admin/settings/api.json | 16 + public/language/vi/admin/settings/chat.json | 12 + .../language/vi/admin/settings/cookies.json | 13 + public/language/vi/admin/settings/email.json | 52 + .../language/vi/admin/settings/general.json | 50 + public/language/vi/admin/settings/group.json | 13 + public/language/vi/admin/settings/guest.json | 7 + .../language/vi/admin/settings/homepage.json | 8 + .../language/vi/admin/settings/languages.json | 6 + .../vi/admin/settings/navigation.json | 25 + .../vi/admin/settings/notifications.json | 7 + .../vi/admin/settings/pagination.json | 12 + public/language/vi/admin/settings/post.json | 67 + .../vi/admin/settings/reputation.json | 31 + public/language/vi/admin/settings/social.json | 5 + .../language/vi/admin/settings/sockets.json | 6 + public/language/vi/admin/settings/sounds.json | 9 + public/language/vi/admin/settings/tags.json | 12 + .../language/vi/admin/settings/uploads.json | 45 + public/language/vi/admin/settings/user.json | 83 + .../vi/admin/settings/web-crawler.json | 10 + public/language/vi/category.json | 23 + public/language/vi/email.json | 58 + public/language/vi/error.json | 224 + public/language/vi/flags.json | 89 + public/language/vi/global.json | 126 + public/language/vi/groups.json | 64 + public/language/vi/ip-blacklist.json | 19 + public/language/vi/language.json | 5 + public/language/vi/login.json | 12 + public/language/vi/modules.json | 82 + public/language/vi/notifications.json | 76 + public/language/vi/pages.json | 65 + public/language/vi/post-queue.json | 31 + public/language/vi/recent.json | 19 + public/language/vi/register.json | 32 + public/language/vi/reset_password.json | 18 + public/language/vi/search.json | 49 + public/language/vi/success.json | 7 + public/language/vi/tags.json | 8 + public/language/vi/top.json | 4 + public/language/vi/topic.json | 188 + public/language/vi/unread.json | 15 + public/language/vi/uploads.json | 9 + public/language/vi/user.json | 199 + public/language/vi/users.json | 24 + .../language/zh-CN/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/zh-CN/admin/admin.json | 11 + .../language/zh-CN/admin/advanced/cache.json | 9 + .../zh-CN/admin/advanced/database.json | 52 + .../language/zh-CN/admin/advanced/errors.json | 14 + .../language/zh-CN/admin/advanced/events.json | 13 + .../language/zh-CN/admin/advanced/logs.json | 7 + .../zh-CN/admin/appearance/customise.json | 16 + .../zh-CN/admin/appearance/skins.json | 9 + .../zh-CN/admin/appearance/themes.json | 11 + public/language/zh-CN/admin/dashboard.json | 90 + .../zh-CN/admin/development/info.json | 25 + .../zh-CN/admin/development/logger.json | 12 + .../language/zh-CN/admin/extend/plugins.json | 57 + .../language/zh-CN/admin/extend/rewards.json | 15 + .../language/zh-CN/admin/extend/widgets.json | 30 + .../zh-CN/admin/manage/admins-mods.json | 12 + .../zh-CN/admin/manage/categories.json | 92 + .../language/zh-CN/admin/manage/digest.json | 22 + .../language/zh-CN/admin/manage/groups.json | 44 + .../zh-CN/admin/manage/privileges.json | 64 + .../zh-CN/admin/manage/registration.json | 20 + public/language/zh-CN/admin/manage/tags.json | 18 + .../language/zh-CN/admin/manage/uploads.json | 11 + public/language/zh-CN/admin/manage/users.json | 112 + public/language/zh-CN/admin/menu.json | 89 + .../zh-CN/admin/settings/advanced.json | 50 + public/language/zh-CN/admin/settings/api.json | 16 + .../language/zh-CN/admin/settings/chat.json | 12 + .../zh-CN/admin/settings/cookies.json | 13 + .../language/zh-CN/admin/settings/email.json | 52 + .../zh-CN/admin/settings/general.json | 50 + .../language/zh-CN/admin/settings/group.json | 13 + .../language/zh-CN/admin/settings/guest.json | 7 + .../zh-CN/admin/settings/homepage.json | 8 + .../zh-CN/admin/settings/languages.json | 6 + .../zh-CN/admin/settings/navigation.json | 25 + .../zh-CN/admin/settings/notifications.json | 7 + .../zh-CN/admin/settings/pagination.json | 12 + .../language/zh-CN/admin/settings/post.json | 67 + .../zh-CN/admin/settings/reputation.json | 31 + .../language/zh-CN/admin/settings/social.json | 5 + .../zh-CN/admin/settings/sockets.json | 6 + .../language/zh-CN/admin/settings/sounds.json | 9 + .../language/zh-CN/admin/settings/tags.json | 12 + .../zh-CN/admin/settings/uploads.json | 45 + .../language/zh-CN/admin/settings/user.json | 83 + .../zh-CN/admin/settings/web-crawler.json | 10 + public/language/zh-CN/category.json | 23 + public/language/zh-CN/email.json | 58 + public/language/zh-CN/error.json | 224 + public/language/zh-CN/flags.json | 89 + public/language/zh-CN/global.json | 126 + public/language/zh-CN/groups.json | 64 + public/language/zh-CN/ip-blacklist.json | 19 + public/language/zh-CN/language.json | 5 + public/language/zh-CN/login.json | 12 + public/language/zh-CN/modules.json | 82 + public/language/zh-CN/notifications.json | 76 + public/language/zh-CN/pages.json | 65 + public/language/zh-CN/post-queue.json | 31 + public/language/zh-CN/recent.json | 19 + public/language/zh-CN/register.json | 32 + public/language/zh-CN/reset_password.json | 18 + public/language/zh-CN/search.json | 49 + public/language/zh-CN/success.json | 7 + public/language/zh-CN/tags.json | 8 + public/language/zh-CN/top.json | 4 + public/language/zh-CN/topic.json | 188 + public/language/zh-CN/unread.json | 15 + public/language/zh-CN/uploads.json | 9 + public/language/zh-CN/user.json | 199 + public/language/zh-CN/users.json | 24 + .../language/zh-TW/_DO_NOT_EDIT_FILES_HERE.md | 3 + public/language/zh-TW/admin/admin.json | 11 + .../language/zh-TW/admin/advanced/cache.json | 9 + .../zh-TW/admin/advanced/database.json | 52 + .../language/zh-TW/admin/advanced/errors.json | 14 + .../language/zh-TW/admin/advanced/events.json | 13 + .../language/zh-TW/admin/advanced/logs.json | 7 + .../zh-TW/admin/appearance/customise.json | 16 + .../zh-TW/admin/appearance/skins.json | 9 + .../zh-TW/admin/appearance/themes.json | 11 + public/language/zh-TW/admin/dashboard.json | 90 + .../zh-TW/admin/development/info.json | 25 + .../zh-TW/admin/development/logger.json | 12 + .../language/zh-TW/admin/extend/plugins.json | 57 + .../language/zh-TW/admin/extend/rewards.json | 15 + .../language/zh-TW/admin/extend/widgets.json | 30 + .../zh-TW/admin/manage/admins-mods.json | 12 + .../zh-TW/admin/manage/categories.json | 92 + .../language/zh-TW/admin/manage/digest.json | 22 + .../language/zh-TW/admin/manage/groups.json | 44 + .../zh-TW/admin/manage/privileges.json | 64 + .../zh-TW/admin/manage/registration.json | 20 + public/language/zh-TW/admin/manage/tags.json | 18 + .../language/zh-TW/admin/manage/uploads.json | 11 + public/language/zh-TW/admin/manage/users.json | 112 + public/language/zh-TW/admin/menu.json | 89 + .../zh-TW/admin/settings/advanced.json | 50 + public/language/zh-TW/admin/settings/api.json | 16 + .../language/zh-TW/admin/settings/chat.json | 12 + .../zh-TW/admin/settings/cookies.json | 13 + .../language/zh-TW/admin/settings/email.json | 52 + .../zh-TW/admin/settings/general.json | 50 + .../language/zh-TW/admin/settings/group.json | 13 + .../language/zh-TW/admin/settings/guest.json | 7 + .../zh-TW/admin/settings/homepage.json | 8 + .../zh-TW/admin/settings/languages.json | 6 + .../zh-TW/admin/settings/navigation.json | 25 + .../zh-TW/admin/settings/notifications.json | 7 + .../zh-TW/admin/settings/pagination.json | 12 + .../language/zh-TW/admin/settings/post.json | 67 + .../zh-TW/admin/settings/reputation.json | 31 + .../language/zh-TW/admin/settings/social.json | 5 + .../zh-TW/admin/settings/sockets.json | 6 + .../language/zh-TW/admin/settings/sounds.json | 9 + .../language/zh-TW/admin/settings/tags.json | 12 + .../zh-TW/admin/settings/uploads.json | 45 + .../language/zh-TW/admin/settings/user.json | 83 + .../zh-TW/admin/settings/web-crawler.json | 10 + public/language/zh-TW/category.json | 23 + public/language/zh-TW/email.json | 58 + public/language/zh-TW/error.json | 224 + public/language/zh-TW/flags.json | 89 + public/language/zh-TW/global.json | 126 + public/language/zh-TW/groups.json | 64 + public/language/zh-TW/ip-blacklist.json | 19 + public/language/zh-TW/language.json | 5 + public/language/zh-TW/login.json | 12 + public/language/zh-TW/modules.json | 82 + public/language/zh-TW/notifications.json | 76 + public/language/zh-TW/pages.json | 65 + public/language/zh-TW/post-queue.json | 31 + public/language/zh-TW/recent.json | 19 + public/language/zh-TW/register.json | 32 + public/language/zh-TW/reset_password.json | 18 + public/language/zh-TW/search.json | 49 + public/language/zh-TW/success.json | 7 + public/language/zh-TW/tags.json | 8 + public/language/zh-TW/top.json | 4 + public/language/zh-TW/topic.json | 188 + public/language/zh-TW/unread.json | 15 + public/language/zh-TW/uploads.json | 9 + public/language/zh-TW/user.json | 199 + public/language/zh-TW/users.json | 24 + public/less/admin/admin.less | 293 + public/less/admin/advanced/database.less | 23 + public/less/admin/advanced/errors.less | 26 + public/less/admin/advanced/events.less | 8 + public/less/admin/advanced/hooks.less | 3 + public/less/admin/advanced/logs.less | 8 + public/less/admin/appearance/customise.less | 9 + public/less/admin/appearance/themes.less | 77 + public/less/admin/development/info.less | 3 + public/less/admin/extend/plugins.less | 58 + public/less/admin/extend/rewards.less | 54 + public/less/admin/extend/widgets.less | 19 + public/less/admin/general/dashboard.less | 204 + public/less/admin/general/navigation.less | 57 + public/less/admin/header.less | 163 + public/less/admin/manage/admins-mods.less | 30 + public/less/admin/manage/categories.less | 135 + public/less/admin/manage/groups.less | 52 + public/less/admin/manage/privileges.less | 36 + public/less/admin/manage/registration.less | 7 + public/less/admin/manage/tags.less | 31 + public/less/admin/manage/users.less | 19 + public/less/admin/mixins.less | 7 + public/less/admin/mobile.less | 197 + public/less/admin/modules/alerts.less | 95 + public/less/admin/modules/nprogress.less | 80 + public/less/admin/modules/search.less | 45 + public/less/admin/modules/selectable.less | 23 + public/less/admin/paper/bootswatch.less | 621 + public/less/admin/paper/variables.less | 869 ++ public/less/admin/settings.less | 35 + public/less/admin/vars.less | 20 + public/less/flags.less | 45 + public/less/generics.less | 184 + public/less/global.less | 8 + public/less/install.less | 101 + public/less/jquery-ui.less | 10 + public/less/mixins.less | 80 + public/less/modals.less | 18 + public/openapi/components/responses/400.yaml | 6 + public/openapi/components/responses/401.yaml | 6 + public/openapi/components/responses/403.yaml | 6 + public/openapi/components/responses/404.yaml | 6 + public/openapi/components/responses/426.yaml | 6 + public/openapi/components/responses/500.yaml | 6 + .../components/schemas/Breadcrumbs.yaml | 16 + .../components/schemas/CategoryObject.yaml | 88 + public/openapi/components/schemas/Chats.yaml | 191 + .../components/schemas/CommonProps.yaml | 81 + public/openapi/components/schemas/Error.yaml | 12 + .../components/schemas/FlagObject.yaml | 184 + .../components/schemas/GroupObject.yaml | 153 + .../components/schemas/Pagination.yaml | 64 + .../components/schemas/PostObject.yaml | 142 + .../components/schemas/PostsObject.yaml | 5 + .../components/schemas/SettingsObj.yaml | 152 + public/openapi/components/schemas/Status.yaml | 9 + .../openapi/components/schemas/TagObject.yaml | 19 + .../components/schemas/TopicObject.yaml | 281 + .../openapi/components/schemas/UserObj.yaml | 123 + .../components/schemas/UserObject.yaml | 677 + .../components/schemas/admin/dashboard.yaml | 47 + public/openapi/read.yaml | 334 + public/openapi/read/admin.yaml | 19 + public/openapi/read/admin/advanced/cache.yaml | 104 + .../read/admin/advanced/cache/dump.yaml | 23 + .../openapi/read/admin/advanced/database.yaml | 14 + .../openapi/read/admin/advanced/errors.yaml | 38 + .../read/admin/advanced/errors/export.yaml | 12 + .../openapi/read/admin/advanced/events.yaml | 65 + public/openapi/read/admin/advanced/hooks.yaml | 45 + public/openapi/read/admin/advanced/logs.yaml | 17 + public/openapi/read/admin/analytics.yaml | 58 + .../openapi/read/admin/appearance/term.yaml | 18 + .../read/admin/category/uploadpicture.yaml | 37 + public/openapi/read/admin/dashboard.yaml | 64 + .../openapi/read/admin/dashboard/logins.yaml | 55 + .../read/admin/dashboard/searches.yaml | 25 + .../openapi/read/admin/dashboard/topics.yaml | 34 + .../openapi/read/admin/dashboard/users.yaml | 34 + .../openapi/read/admin/development/info.yaml | 166 + .../read/admin/development/logger.yaml | 11 + public/openapi/read/admin/extend/plugins.yaml | 206 + public/openapi/read/admin/extend/rewards.yaml | 85 + public/openapi/read/admin/extend/widgets.yaml | 90 + .../read/admin/groups/groupname/csv.yaml | 25 + .../read/admin/manage/admins-mods.yaml | 107 + .../openapi/read/admin/manage/categories.yaml | 53 + .../admin/manage/categories/category_id.yaml | 42 + .../categories/category_id/analytics.yaml | 42 + public/openapi/read/admin/manage/digest.yaml | 54 + public/openapi/read/admin/manage/groups.yaml | 101 + .../read/admin/manage/groups/name.yaml | 40 + .../read/admin/manage/privileges/cid.yaml | 122 + .../read/admin/manage/registration.yaml | 90 + public/openapi/read/admin/manage/tags.yaml | 18 + public/openapi/read/admin/manage/uploads.yaml | 57 + public/openapi/read/admin/manage/users.yaml | 39 + .../openapi/read/admin/settings/advanced.yaml | 18 + public/openapi/read/admin/settings/email.yaml | 43 + .../openapi/read/admin/settings/homepage.yaml | 23 + .../read/admin/settings/languages.yaml | 35 + .../read/admin/settings/navigation.yaml | 107 + public/openapi/read/admin/settings/post.yaml | 18 + .../openapi/read/admin/settings/social.yaml | 28 + public/openapi/read/admin/settings/term.yaml | 24 + public/openapi/read/admin/settings/user.yaml | 25 + public/openapi/read/admin/upload/file.yaml | 35 + .../read/admin/uploadDefaultAvatar.yaml | 32 + .../read/admin/uploadMaskableIcon.yaml | 32 + public/openapi/read/admin/uploadOgImage.yaml | 32 + .../openapi/read/admin/uploadTouchIcon.yaml | 32 + public/openapi/read/admin/uploadfavicon.yaml | 32 + public/openapi/read/admin/uploadlogo.yaml | 32 + public/openapi/read/admin/users/csv.yaml | 19 + public/openapi/read/career.yaml | 8 + public/openapi/read/categories.yaml | 207 + .../read/categories/cid/moderators.yaml | 30 + public/openapi/read/category/category_id.yaml | 104 + public/openapi/read/chats/roomid.yaml | 20 + public/openapi/read/config.yaml | 148 + public/openapi/read/confirm/code.yaml | 21 + .../openapi/read/email/unsubscribe/token.yaml | 60 + public/openapi/read/flags.yaml | 76 + public/openapi/read/flags/flagId.yaml | 46 + public/openapi/read/groups.yaml | 109 + public/openapi/read/groups/slug.yaml | 34 + public/openapi/read/groups/slug/members.yaml | 25 + public/openapi/read/index.yaml | 208 + public/openapi/read/ip-blacklist.yaml | 7 + public/openapi/read/login.yaml | 58 + public/openapi/read/me.yaml | 10 + public/openapi/read/notifications.yaml | 112 + public/openapi/read/outgoing.yaml | 29 + public/openapi/read/popular.yaml | 111 + public/openapi/read/post-queue.yaml | 167 + public/openapi/read/post/pid.yaml | 19 + public/openapi/read/post/upload.yaml | 25 + public/openapi/read/recent.yaml | 109 + public/openapi/read/recent/posts/term.yaml | 25 + public/openapi/read/register.yaml | 57 + public/openapi/read/register/complete.yaml | 30 + public/openapi/read/registration-queue.yaml | 7 + public/openapi/read/reset.yaml | 20 + public/openapi/read/reset/code.yaml | 32 + public/openapi/read/search.yaml | 77 + public/openapi/read/self.yaml | 12 + public/openapi/read/tags.yaml | 27 + public/openapi/read/tags/tag.yaml | 261 + public/openapi/read/top.yaml | 122 + .../read/topic/pagination/topic_id.yaml | 19 + .../openapi/read/topic/teaser/topic_id.yaml | 18 + public/openapi/read/topic/thumb/upload.yaml | 35 + public/openapi/read/topic/topic_id.yaml | 392 + public/openapi/read/tos.yaml | 18 + public/openapi/read/uid/uid.yaml | 19 + public/openapi/read/unread.yaml | 243 + public/openapi/read/unread/total.yaml | 11 + public/openapi/read/user/email/email.yaml | 21 + public/openapi/read/user/uid/uid.yaml | 19 + .../read/user/uid/userslug/export/type.yaml | 19 + .../openapi/read/user/username/username.yaml | 19 + public/openapi/read/user/userslug.yaml | 86 + public/openapi/read/user/userslug/best.yaml | 45 + public/openapi/read/user/userslug/blocks.yaml | 30 + .../openapi/read/user/userslug/bookmarks.yaml | 45 + .../read/user/userslug/categories.yaml | 63 + .../read/user/userslug/chats/roomid.yaml | 314 + .../openapi/read/user/userslug/consent.yaml | 34 + .../read/user/userslug/controversial.yaml | 45 + .../openapi/read/user/userslug/downvoted.yaml | 51 + public/openapi/read/user/userslug/edit.yaml | 66 + .../read/user/userslug/edit/email.yaml | 27 + .../read/user/userslug/edit/password.yaml | 31 + .../read/user/userslug/edit/username.yaml | 27 + .../read/user/userslug/export/posts.yaml | 20 + .../read/user/userslug/export/profile.yaml | 20 + .../read/user/userslug/export/uploads.yaml | 20 + .../openapi/read/user/userslug/followers.yaml | 91 + .../openapi/read/user/userslug/following.yaml | 91 + public/openapi/read/user/userslug/groups.yaml | 32 + .../openapi/read/user/userslug/ignored.yaml | 47 + public/openapi/read/user/userslug/info.yaml | 181 + public/openapi/read/user/userslug/posts.yaml | 45 + .../read/user/userslug/session/uuid.yaml | 20 + .../openapi/read/user/userslug/sessions.yaml | 46 + .../openapi/read/user/userslug/settings.yaml | 139 + public/openapi/read/user/userslug/topics.yaml | 47 + .../openapi/read/user/userslug/uploads.yaml | 37 + .../openapi/read/user/userslug/upvoted.yaml | 51 + .../openapi/read/user/userslug/watched.yaml | 49 + public/openapi/read/users.yaml | 119 + public/openapi/write.yaml | 178 + public/openapi/write/admin/analytics.yaml | 20 + public/openapi/write/admin/analytics/set.yaml | 46 + .../openapi/write/admin/settings/setting.yaml | 37 + public/openapi/write/categories.yaml | 66 + public/openapi/write/categories/cid.yaml | 93 + .../write/categories/cid/moderator/uid.yaml | 64 + .../write/categories/cid/privileges.yaml | 94 + .../categories/cid/privileges/privilege.yaml | 222 + public/openapi/write/chats.yaml | 192 + public/openapi/write/chats/roomId.yaml | 128 + .../openapi/write/chats/roomId/messages.yaml | 54 + .../write/chats/roomId/messages/mid.yaml | 141 + public/openapi/write/chats/roomId/users.yaml | 103 + .../openapi/write/chats/roomId/users/uid.yaml | 32 + public/openapi/write/files.yaml | 31 + public/openapi/write/files/folder.yaml | 36 + public/openapi/write/flags.yaml | 38 + public/openapi/write/flags/flagId.yaml | 95 + public/openapi/write/flags/flagId/notes.yaml | 42 + .../write/flags/flagId/notes/datetime.yaml | 34 + public/openapi/write/groups.yaml | 58 + public/openapi/write/groups/slug.yaml | 80 + .../write/groups/slug/membership/uid.yaml | 66 + .../write/groups/slug/ownership/uid.yaml | 66 + public/openapi/write/login.yaml | 32 + public/openapi/write/ping.yaml | 57 + public/openapi/write/posts/pid.yaml | 141 + public/openapi/write/posts/pid/bookmark.yaml | 52 + public/openapi/write/posts/pid/diffs.yaml | 43 + .../openapi/write/posts/pid/diffs/since.yaml | 65 + .../write/posts/pid/diffs/timestamp.yaml | 25 + public/openapi/write/posts/pid/move.yaml | 36 + public/openapi/write/posts/pid/state.yaml | 52 + public/openapi/write/posts/pid/vote.yaml | 63 + public/openapi/write/topics.yaml | 46 + public/openapi/write/topics/tid.yaml | 92 + public/openapi/write/topics/tid/events.yaml | 85 + .../write/topics/tid/events/eventId.yaml | 33 + public/openapi/write/topics/tid/follow.yaml | 52 + public/openapi/write/topics/tid/ignore.yaml | 52 + public/openapi/write/topics/tid/lock.yaml | 52 + public/openapi/write/topics/tid/pin.yaml | 52 + public/openapi/write/topics/tid/state.yaml | 52 + public/openapi/write/topics/tid/tags.yaml | 65 + public/openapi/write/topics/tid/thumbs.yaml | 160 + .../write/topics/tid/thumbs/order.yaml | 41 + public/openapi/write/users.yaml | 76 + public/openapi/write/users/uid.yaml | 131 + public/openapi/write/users/uid/account.yaml | 25 + public/openapi/write/users/uid/ban.yaml | 61 + public/openapi/write/users/uid/content.yaml | 25 + public/openapi/write/users/uid/emails.yaml | 33 + .../openapi/write/users/uid/emails/email.yaml | 25 + .../write/users/uid/emails/email/confirm.yaml | 34 + .../openapi/write/users/uid/exports/type.yaml | 85 + public/openapi/write/users/uid/follow.yaml | 48 + public/openapi/write/users/uid/invites.yaml | 48 + .../write/users/uid/invites/groups.yaml | 23 + public/openapi/write/users/uid/mute.yaml | 61 + public/openapi/write/users/uid/password.yaml | 40 + public/openapi/write/users/uid/picture.yaml | 43 + .../write/users/uid/sessions/uuid.yaml | 31 + public/openapi/write/users/uid/settings.yaml | 36 + public/openapi/write/users/uid/tokens.yaml | 25 + .../openapi/write/users/uid/tokens/token.yaml | 31 + public/src/admin/.eslintrc | 5 + public/src/admin/admin.js | 244 + public/src/admin/advanced/cache.js | 32 + public/src/admin/advanced/errors.js | 113 + public/src/admin/advanced/events.js | 43 + public/src/admin/advanced/logs.js | 44 + public/src/admin/appearance/customise.js | 40 + public/src/admin/appearance/skins.js | 113 + public/src/admin/appearance/themes.js | 118 + public/src/admin/dashboard.js | 605 + public/src/admin/dashboard/logins.js | 14 + public/src/admin/dashboard/topics.js | 32 + public/src/admin/dashboard/users.js | 34 + public/src/admin/extend/plugins.js | 348 + public/src/admin/extend/rewards.js | 186 + public/src/admin/extend/widgets.js | 282 + public/src/admin/manage/admins-mods.js | 133 + public/src/admin/manage/categories.js | 304 + public/src/admin/manage/category-analytics.js | 173 + public/src/admin/manage/category.js | 310 + public/src/admin/manage/digest.js | 45 + public/src/admin/manage/group.js | 165 + public/src/admin/manage/groups.js | 122 + public/src/admin/manage/privileges.js | 501 + public/src/admin/manage/registration.js | 56 + public/src/admin/manage/tags.js | 141 + public/src/admin/manage/uploads.js | 49 + public/src/admin/manage/users.js | 549 + .../src/admin/modules/checkboxRowSelector.js | 49 + public/src/admin/modules/colorpicker.js | 31 + .../src/admin/modules/dashboard-line-graph.js | 196 + public/src/admin/modules/instance.js | 66 + public/src/admin/modules/search.js | 164 + public/src/admin/modules/selectable.js | 16 + public/src/admin/settings.js | 200 + public/src/admin/settings/api.js | 34 + public/src/admin/settings/cookies.js | 19 + public/src/admin/settings/email.js | 126 + public/src/admin/settings/general.js | 26 + public/src/admin/settings/homepage.js | 22 + public/src/admin/settings/navigation.js | 157 + public/src/admin/settings/notifications.js | 18 + public/src/admin/settings/social.js | 27 + public/src/ajaxify.js | 594 + public/src/app.js | 394 + public/src/client.js | 10 + public/src/client/account/best.js | 16 + public/src/client/account/blocks.js | 67 + public/src/client/account/bookmarks.js | 16 + public/src/client/account/categories.js | 62 + public/src/client/account/consent.js | 34 + public/src/client/account/downvoted.js | 16 + public/src/client/account/edit.js | 161 + public/src/client/account/edit/password.js | 121 + public/src/client/account/edit/username.js | 51 + public/src/client/account/followers.js | 12 + public/src/client/account/following.js | 12 + public/src/client/account/groups.js | 20 + public/src/client/account/header.js | 287 + public/src/client/account/ignored.js | 13 + public/src/client/account/info.js | 38 + public/src/client/account/posts.js | 56 + public/src/client/account/profile.js | 38 + public/src/client/account/sessions.js | 38 + public/src/client/account/settings.js | 147 + public/src/client/account/topics.js | 57 + public/src/client/account/uploads.js | 24 + public/src/client/account/upvoted.js | 16 + public/src/client/account/watched.js | 14 + public/src/client/categories.js | 71 + public/src/client/category.js | 155 + public/src/client/category/tools.js | 312 + public/src/client/chats.js | 521 + public/src/client/chats/messages.js | 210 + public/src/client/chats/recent.js | 62 + public/src/client/chats/search.js | 81 + public/src/client/compose.js | 18 + public/src/client/flags/detail.js | 178 + public/src/client/flags/list.js | 231 + public/src/client/groups/details.js | 303 + public/src/client/groups/list.js | 90 + public/src/client/groups/memberlist.js | 167 + public/src/client/header.js | 79 + public/src/client/header/chat.js | 56 + public/src/client/header/notifications.js | 46 + public/src/client/header/unread.js | 96 + public/src/client/infinitescroll.js | 124 + public/src/client/ip-blacklist.js | 134 + public/src/client/login.js | 111 + public/src/client/notifications.js | 30 + public/src/client/pagination.js | 39 + public/src/client/popular.js | 14 + public/src/client/post-queue.js | 185 + public/src/client/recent.js | 13 + public/src/client/register.js | 209 + public/src/client/reset.js | 32 + public/src/client/reset_code.js | 44 + public/src/client/search.js | 181 + public/src/client/tag.js | 13 + public/src/client/tags.js | 64 + public/src/client/top.js | 13 + public/src/client/topic.js | 364 + public/src/client/topic/change-owner.js | 91 + public/src/client/topic/delete-posts.js | 90 + public/src/client/topic/diffs.js | 117 + public/src/client/topic/events.js | 242 + public/src/client/topic/fork.js | 106 + public/src/client/topic/images.js | 34 + public/src/client/topic/merge.js | 144 + public/src/client/topic/move-post.js | 167 + public/src/client/topic/move.js | 102 + public/src/client/topic/postTools.js | 545 + public/src/client/topic/posts.js | 443 + public/src/client/topic/replies.js | 110 + public/src/client/topic/threadTools.js | 382 + public/src/client/topic/votes.js | 110 + public/src/client/unread.js | 112 + public/src/client/users.js | 122 + public/src/installer/install.js | 144 + public/src/modules/accounts/delete.js | 53 + public/src/modules/accounts/invite.js | 60 + public/src/modules/accounts/picture.js | 219 + public/src/modules/ace-editor.js | 20 + public/src/modules/alerts.js | 155 + public/src/modules/api.js | 100 + public/src/modules/autocomplete.js | 133 + public/src/modules/categoryFilter.js | 103 + public/src/modules/categorySearch.js | 101 + public/src/modules/categorySelector.js | 96 + public/src/modules/chat.js | 434 + public/src/modules/components.js | 73 + public/src/modules/coverPhoto.js | 89 + public/src/modules/flags.js | 95 + public/src/modules/groupSearch.js | 60 + public/src/modules/handleBack.js | 106 + public/src/modules/helpers.common.js | 347 + public/src/modules/helpers.js | 7 + public/src/modules/hooks.js | 173 + public/src/modules/iconSelect.js | 125 + public/src/modules/logout.js | 28 + public/src/modules/messages.js | 131 + public/src/modules/navigator.js | 646 + public/src/modules/notifications.js | 160 + public/src/modules/pictureCropper.js | 249 + public/src/modules/postSelect.js | 73 + public/src/modules/scrollStop.js | 31 + public/src/modules/search.js | 341 + public/src/modules/settings.js | 609 + public/src/modules/settings/array.js | 145 + public/src/modules/settings/checkbox.js | 39 + public/src/modules/settings/key.js | 237 + public/src/modules/settings/number.js | 17 + public/src/modules/settings/object.js | 124 + public/src/modules/settings/select.js | 46 + public/src/modules/settings/sorted-list.js | 172 + public/src/modules/settings/textarea.js | 36 + public/src/modules/share.js | 55 + public/src/modules/slugify.js | 40 + public/src/modules/sort.js | 39 + public/src/modules/storage.js | 84 + public/src/modules/taskbar.js | 213 + public/src/modules/topicList.js | 279 + public/src/modules/topicSelect.js | 88 + public/src/modules/topicThumbs.js | 130 + public/src/modules/translator.common.js | 633 + public/src/modules/translator.js | 25 + public/src/modules/uploadHelpers.js | 199 + public/src/modules/uploader.js | 118 + public/src/overrides.js | 162 + public/src/service-worker.js | 19 + public/src/sockets.js | 257 + public/src/utils.common.js | 750 + public/src/utils.js | 83 + public/src/widgets.js | 52 + public/vendor/bootbox/wrapper.js | 62 + public/vendor/fontawesome/.gitignore | 29 + public/vendor/fontawesome/LICENSE.txt | 34 + public/vendor/fontawesome/attribution.js | 3 + public/vendor/fontawesome/less/_animated.less | 19 + .../fontawesome/less/_bordered-pulled.less | 16 + public/vendor/fontawesome/less/_core.less | 12 + .../vendor/fontawesome/less/_fixed-width.less | 6 + public/vendor/fontawesome/less/_icons.less | 1462 ++ public/vendor/fontawesome/less/_larger.less | 27 + public/vendor/fontawesome/less/_list.less | 18 + public/vendor/fontawesome/less/_mixins.less | 56 + .../fontawesome/less/_rotated-flipped.less | 24 + .../fontawesome/less/_screen-reader.less | 5 + public/vendor/fontawesome/less/_shims.less | 2066 +++ public/vendor/fontawesome/less/_stacked.less | 22 + .../vendor/fontawesome/less/_variables.less | 1474 ++ public/vendor/fontawesome/less/brands.less | 23 + .../vendor/fontawesome/less/fontawesome.less | 16 + .../vendor/fontawesome/less/nodebb-shims.less | 321 + public/vendor/fontawesome/less/regular.less | 23 + public/vendor/fontawesome/less/solid.less | 24 + public/vendor/fontawesome/less/v4-shims.less | 6 + .../fontawesome/webfonts/fa-brands-400.eot | Bin 0 -> 134346 bytes .../fontawesome/webfonts/fa-brands-400.svg | 3717 +++++ .../fontawesome/webfonts/fa-brands-400.ttf | Bin 0 -> 134040 bytes .../fontawesome/webfonts/fa-brands-400.woff | Bin 0 -> 90060 bytes .../fontawesome/webfonts/fa-brands-400.woff2 | Bin 0 -> 76764 bytes .../fontawesome/webfonts/fa-regular-400.eot | Bin 0 -> 34034 bytes .../fontawesome/webfonts/fa-regular-400.svg | 801 ++ .../fontawesome/webfonts/fa-regular-400.ttf | Bin 0 -> 33736 bytes .../fontawesome/webfonts/fa-regular-400.woff | Bin 0 -> 16276 bytes .../fontawesome/webfonts/fa-regular-400.woff2 | Bin 0 -> 13276 bytes .../fontawesome/webfonts/fa-solid-900.eot | Bin 0 -> 203030 bytes .../fontawesome/webfonts/fa-solid-900.svg | 5034 +++++++ .../fontawesome/webfonts/fa-solid-900.ttf | Bin 0 -> 202744 bytes .../fontawesome/webfonts/fa-solid-900.woff | Bin 0 -> 101652 bytes .../fontawesome/webfonts/fa-solid-900.woff2 | Bin 0 -> 78196 bytes .../backgroundDraggable.js | 174 + public/vendor/mdl/material.css | 11476 ++++++++++++++++ public/vendor/redoc/index.html | 23 + renovate.json | 23 + require-main.js | 10 + src/admin/search.js | 142 + src/admin/versions.js | 52 + src/als.js | 7 + src/analytics.js | 301 + src/api/categories.js | 102 + src/api/chats.js | 120 + src/api/flags.js | 84 + src/api/groups.js | 238 + src/api/helpers.js | 142 + src/api/index.js | 11 + src/api/posts.js | 335 + src/api/topics.js | 154 + src/api/users.js | 478 + src/batch.js | 92 + src/cache.js | 9 + src/cache/lru.js | 146 + src/cache/ttl.js | 119 + src/cacheCreate.js | 3 + src/categories/activeusers.js | 17 + src/categories/create.js | 250 + src/categories/data.js | 112 + src/categories/delete.js | 91 + src/categories/index.js | 409 + src/categories/recentreplies.js | 212 + src/categories/search.js | 81 + src/categories/topics.js | 196 + src/categories/unread.js | 38 + src/categories/update.js | 145 + src/categories/watch.js | 54 + src/cli/colors.js | 160 + src/cli/index.js | 322 + src/cli/manage.js | 209 + src/cli/package-install.js | 174 + src/cli/reset.js | 157 + src/cli/running.js | 125 + src/cli/setup.js | 60 + src/cli/upgrade-plugins.js | 159 + src/cli/upgrade.js | 95 + src/cli/user.js | 311 + src/constants.js | 28 + src/controllers/404.js | 64 + src/controllers/accounts.js | 20 + src/controllers/accounts/blocks.js | 39 + src/controllers/accounts/categories.js | 44 + src/controllers/accounts/chats.js | 65 + src/controllers/accounts/consent.js | 30 + src/controllers/accounts/edit.js | 169 + src/controllers/accounts/follow.js | 41 + src/controllers/accounts/groups.js | 25 + src/controllers/accounts/helpers.js | 267 + src/controllers/accounts/info.js | 54 + src/controllers/accounts/notifications.js | 72 + src/controllers/accounts/posts.js | 254 + src/controllers/accounts/profile.js | 169 + src/controllers/accounts/sessions.js | 20 + src/controllers/accounts/settings.js | 243 + src/controllers/accounts/uploads.js | 40 + src/controllers/admin.js | 58 + src/controllers/admin/admins-mods.js | 61 + src/controllers/admin/appearance.js | 9 + src/controllers/admin/cache.js | 67 + src/controllers/admin/categories.js | 143 + src/controllers/admin/dashboard.js | 344 + src/controllers/admin/database.js | 23 + src/controllers/admin/digest.js | 23 + src/controllers/admin/errors.js | 25 + src/controllers/admin/events.js | 44 + src/controllers/admin/groups.js | 98 + src/controllers/admin/hooks.js | 32 + src/controllers/admin/info.js | 144 + src/controllers/admin/logger.js | 7 + src/controllers/admin/logs.js | 20 + src/controllers/admin/plugins.js | 69 + src/controllers/admin/privileges.js | 52 + src/controllers/admin/rewards.js | 10 + src/controllers/admin/settings.js | 110 + src/controllers/admin/tags.js | 10 + src/controllers/admin/themes.js | 31 + src/controllers/admin/uploads.js | 273 + src/controllers/admin/users.js | 280 + src/controllers/admin/widgets.js | 9 + src/controllers/api.js | 131 + src/controllers/authentication.js | 510 + src/controllers/career.js | 8 + src/controllers/categories.js | 61 + src/controllers/category.js | 206 + src/controllers/composer.js | 111 + src/controllers/composer.ts | 138 + src/controllers/errors.js | 111 + src/controllers/globalmods.js | 36 + src/controllers/groups.js | 120 + src/controllers/helpers.js | 575 + src/controllers/home.js | 64 + src/controllers/index.js | 375 + src/controllers/mods.js | 200 + src/controllers/osd.js | 57 + src/controllers/ping.js | 13 + src/controllers/popular.js | 30 + src/controllers/posts.js | 39 + src/controllers/recent.js | 99 + src/controllers/search.js | 145 + src/controllers/sitemap.js | 40 + src/controllers/tags.js | 83 + src/controllers/top.js | 28 + src/controllers/topics.js | 374 + src/controllers/unread.js | 78 + src/controllers/uploads.js | 203 + src/controllers/user.js | 118 + src/controllers/users.js | 212 + src/controllers/write/admin.js | 42 + src/controllers/write/categories.js | 82 + src/controllers/write/chats.js | 129 + src/controllers/write/files.js | 16 + src/controllers/write/flags.js | 53 + src/controllers/write/groups.js | 49 + src/controllers/write/index.js | 14 + src/controllers/write/posts.js | 116 + src/controllers/write/topics.js | 242 + src/controllers/write/users.js | 356 + src/controllers/write/utilities.js | 33 + src/coverPhoto.js | 40 + src/database/cache.js | 10 + src/database/helpers.js | 28 + src/database/index.js | 37 + src/database/mongo.js | 188 + src/database/mongo/connection.js | 62 + src/database/mongo/hash.js | 282 + src/database/mongo/helpers.js | 67 + src/database/mongo/list.js | 99 + src/database/mongo/main.js | 150 + src/database/mongo/sets.js | 199 + src/database/mongo/sorted.js | 569 + src/database/mongo/sorted/add.js | 91 + src/database/mongo/sorted/intersect.js | 219 + src/database/mongo/sorted/remove.js | 63 + src/database/mongo/sorted/union.js | 69 + src/database/mongo/transaction.js | 8 + src/database/postgres.js | 390 + src/database/postgres/connection.js | 44 + src/database/postgres/hash.js | 388 + src/database/postgres/helpers.js | 97 + src/database/postgres/list.js | 189 + src/database/postgres/main.js | 244 + src/database/postgres/sets.js | 261 + src/database/postgres/sorted.js | 682 + src/database/postgres/sorted/add.js | 133 + src/database/postgres/sorted/intersect.js | 92 + src/database/postgres/sorted/remove.js | 91 + src/database/postgres/sorted/union.js | 83 + src/database/postgres/transaction.js | 32 + src/database/redis.js | 119 + src/database/redis/connection.js | 62 + src/database/redis/hash.js | 237 + src/database/redis/helpers.js | 30 + src/database/redis/list.js | 57 + src/database/redis/main.js | 111 + src/database/redis/pubsub.js | 49 + src/database/redis/sets.js | 91 + src/database/redis/sorted.js | 325 + src/database/redis/sorted/add.js | 76 + src/database/redis/sorted/intersect.js | 66 + src/database/redis/sorted/remove.js | 46 + src/database/redis/sorted/union.js | 52 + src/database/redis/transaction.js | 8 + src/emailer.js | 368 + src/events.js | 174 + src/file.js | 158 + src/flags.js | 956 ++ src/groups/cache.js | 19 + src/groups/cover.js | 80 + src/groups/create.js | 95 + src/groups/data.js | 108 + src/groups/delete.js | 57 + src/groups/index.js | 247 + src/groups/invite.js | 117 + src/groups/join.js | 109 + src/groups/leave.js | 100 + src/groups/membership.js | 175 + src/groups/ownership.js | 39 + src/groups/posts.js | 44 + src/groups/search.js | 84 + src/groups/update.js | 291 + src/groups/user.js | 67 + src/helpers.js | 7 + src/image.js | 182 + src/install.js | 618 + src/languages.js | 87 + src/logger.js | 217 + src/messaging/create.js | 102 + src/messaging/data.js | 156 + src/messaging/delete.js | 33 + src/messaging/edit.js | 92 + src/messaging/index.js | 306 + src/messaging/notifications.js | 82 + src/messaging/rooms.js | 261 + src/messaging/unread.js | 39 + src/meta/aliases.js | 43 + src/meta/blacklist.js | 171 + src/meta/build.js | 264 + src/meta/cacheBuster.js | 41 + src/meta/configs.js | 289 + src/meta/css.js | 154 + src/meta/debugFork.js | 37 + src/meta/dependencies.js | 72 + src/meta/errors.js | 56 + src/meta/index.js | 73 + src/meta/js.js | 140 + src/meta/languages.js | 143 + src/meta/logs.js | 16 + src/meta/minifier.js | 256 + src/meta/settings.js | 127 + src/meta/tags.js | 269 + src/meta/templates.js | 139 + src/meta/themes.js | 167 + src/middleware/admin.js | 176 + src/middleware/assert.js | 141 + src/middleware/expose.js | 49 + src/middleware/header.js | 264 + src/middleware/headers.js | 116 + src/middleware/helpers.js | 68 + src/middleware/index.js | 254 + src/middleware/maintenance.js | 46 + src/middleware/ratelimit.js | 32 + src/middleware/render.js | 137 + src/middleware/uploads.js | 29 + src/middleware/user.js | 245 + src/navigation/admin.js | 104 + src/navigation/index.js | 34 + src/notifications.js | 447 + src/pagination.js | 81 + src/password.js | 81 + src/plugins/data.js | 265 + src/plugins/hooks.js | 280 + src/plugins/index.js | 320 + src/plugins/install.js | 180 + src/plugins/load.js | 171 + src/plugins/usage.js | 48 + src/posts/bookmarks.js | 68 + src/posts/cache.js | 12 + src/posts/category.js | 41 + src/posts/create.js | 83 + src/posts/data.js | 71 + src/posts/delete.js | 232 + src/posts/diffs.js | 175 + src/posts/edit.js | 217 + src/posts/index.js | 104 + src/posts/parse.js | 174 + src/posts/queue.js | 367 + src/posts/recent.js | 33 + src/posts/summary.js | 105 + src/posts/tools.js | 44 + src/posts/topics.js | 54 + src/posts/uploads.js | 231 + src/posts/user.js | 261 + src/posts/votes.js | 293 + src/prestart.js | 125 + src/privileges/admin.js | 212 + src/privileges/categories.js | 220 + src/privileges/global.js | 136 + src/privileges/helpers.js | 192 + src/privileges/index.js | 17 + src/privileges/posts.js | 234 + src/privileges/topics.js | 192 + src/privileges/users.js | 154 + src/promisify.js | 61 + src/pubsub.js | 71 + src/rewards/admin.js | 81 + src/rewards/index.js | 80 + src/routes/admin.js | 85 + src/routes/api.js | 56 + src/routes/authentication.js | 187 + src/routes/debug.js | 35 + src/routes/feeds.js | 423 + src/routes/helpers.js | 84 + src/routes/index.js | 231 + src/routes/meta.js | 18 + src/routes/user.js | 53 + src/routes/write/admin.js | 19 + src/routes/write/categories.js | 26 + src/routes/write/chats.js | 34 + src/routes/write/files.js | 33 + src/routes/write/flags.js | 23 + src/routes/write/groups.js | 23 + src/routes/write/index.js | 73 + src/routes/write/posts.js | 35 + src/routes/write/topics.js | 48 + src/routes/write/users.js | 66 + src/routes/write/utilities.js | 17 + src/search.js | 316 + src/settings.js | 240 + src/sitemap.js | 180 + src/slugify.js | 3 + src/social.js | 72 + src/social.ts | 61 + src/socket.io/admin.js | 121 + src/socket.io/admin/analytics.js | 36 + src/socket.io/admin/cache.js | 34 + src/socket.io/admin/categories.js | 44 + src/socket.io/admin/config.js | 50 + src/socket.io/admin/digest.js | 24 + src/socket.io/admin/email.js | 68 + src/socket.io/admin/errors.js | 9 + src/socket.io/admin/logs.js | 13 + src/socket.io/admin/navigation.js | 9 + src/socket.io/admin/plugins.js | 49 + src/socket.io/admin/rewards.js | 13 + src/socket.io/admin/rooms.js | 160 + src/socket.io/admin/settings.js | 24 + src/socket.io/admin/social.js | 9 + src/socket.io/admin/tags.js | 29 + src/socket.io/admin/themes.js | 24 + src/socket.io/admin/user.js | 165 + src/socket.io/admin/widgets.js | 12 + src/socket.io/blacklist.js | 36 + src/socket.io/categories.js | 167 + src/socket.io/categories/search.js | 101 + src/socket.io/groups.js | 291 + src/socket.io/helpers.js | 200 + src/socket.io/index.js | 278 + src/socket.io/meta.js | 63 + src/socket.io/modules.js | 254 + src/socket.io/notifications.js | 42 + src/socket.io/plugins.js | 17 + src/socket.io/posts.js | 184 + src/socket.io/posts/tools.js | 94 + src/socket.io/posts/votes.js | 62 + src/socket.io/topics.js | 128 + src/socket.io/topics/infinitescroll.js | 55 + src/socket.io/topics/merge.js | 29 + src/socket.io/topics/move.js | 73 + src/socket.io/topics/tags.js | 85 + src/socket.io/topics/tools.js | 40 + src/socket.io/topics/unread.js | 74 + src/socket.io/uploads.js | 53 + src/socket.io/user.js | 189 + src/socket.io/user/picture.js | 44 + src/socket.io/user/profile.js | 79 + src/socket.io/user/registration.js | 43 + src/socket.io/user/status.js | 40 + src/start.js | 145 + src/topics/bookmarks.js | 66 + src/topics/create.js | 310 + src/topics/data.js | 142 + src/topics/delete.js | 141 + src/topics/events.js | 212 + src/topics/follow.js | 177 + src/topics/fork.js | 159 + src/topics/index.js | 288 + src/topics/merge.js | 82 + src/topics/posts.js | 407 + src/topics/recent.js | 79 + src/topics/scheduled.js | 129 + src/topics/sorted.js | 220 + src/topics/suggested.js | 70 + src/topics/tags.js | 528 + src/topics/teaser.js | 176 + src/topics/thumbs.js | 162 + src/topics/tools.js | 295 + src/topics/unread.js | 389 + src/topics/user.js | 18 + src/translator.js | 12 + src/types/admin.js | 2 + src/types/admin.ts | 25 + src/types/breadcrumbs.js | 2 + src/types/breadcrumbs.ts | 9 + src/types/category.js | 2 + src/types/category.ts | 31 + src/types/chat.js | 2 + src/types/chat.ts | 41 + src/types/commonProps.js | 2 + src/types/commonProps.ts | 33 + src/types/error.js | 2 + src/types/error.ts | 5 + src/types/flag.js | 2 + src/types/flag.ts | 55 + src/types/group.js | 2 + src/types/group.ts | 44 + src/types/index.js | 30 + src/types/index.ts | 14 + src/types/pagination.js | 2 + src/types/pagination.ts | 30 + src/types/post.js | 2 + src/types/post.ts | 21 + src/types/settings.js | 2 + src/types/settings.ts | 41 + src/types/social.js | 2 + src/types/social.ts | 6 + src/types/status.js | 2 + src/types/status.ts | 4 + src/types/tag.js | 2 + src/types/tag.ts | 7 + src/types/topic.js | 2 + src/types/topic.ts | 81 + src/types/user.js | 2 + src/types/user.ts | 133 + src/upgrade.js | 204 + src/upgrades/1.0.0/chat_room_hashes.js | 39 + src/upgrades/1.0.0/chat_upgrade.js | 83 + src/upgrades/1.0.0/global_moderators.js | 22 + src/upgrades/1.0.0/social_post_sharing.js | 21 + src/upgrades/1.0.0/theme_to_active_plugins.js | 13 + src/upgrades/1.0.0/user_best_posts.js | 33 + src/upgrades/1.0.0/users_notvalidated.js | 29 + .../1.1.0/assign_topic_read_privilege.js | 35 + .../dismiss_flags_from_deleted_topics.js | 56 + src/upgrades/1.1.0/group_title_update.js | 30 + .../1.1.0/separate_upvote_downvote.js | 54 + src/upgrades/1.1.0/user_post_count_per_tid.js | 48 + .../1.1.1/remove_negative_best_posts.js | 20 + src/upgrades/1.1.1/upload_privileges.js | 38 + .../1.10.0/hash_recent_ip_addresses.js | 41 + src/upgrades/1.10.0/post_history_privilege.js | 22 + src/upgrades/1.10.0/search_privileges.js | 23 + src/upgrades/1.10.0/view_deleted_privilege.js | 22 + src/upgrades/1.10.2/event_filters.js | 37 + .../1.10.2/fix_category_post_zsets.js | 32 + .../1.10.2/fix_category_topic_zsets.js | 30 + src/upgrades/1.10.2/local_login_privileges.js | 17 + src/upgrades/1.10.2/postgres_sessions.js | 41 + src/upgrades/1.10.2/upgrade_bans_to_hashes.js | 59 + src/upgrades/1.10.2/username_email_history.js | 37 + .../1.11.0/navigation_visibility_groups.js | 58 + src/upgrades/1.11.0/resize_image_width.js | 14 + .../1.11.0/widget_visibility_groups.js | 38 + .../1.11.1/remove_ignored_cids_per_user.js | 22 + src/upgrades/1.12.0/category_watch_state.js | 35 + src/upgrades/1.12.0/global_view_privileges.js | 28 + src/upgrades/1.12.0/group_create_privilege.js | 16 + .../1.12.1/clear_username_email_history.js | 45 + .../1.12.1/moderation_notes_refactor.js | 35 + src/upgrades/1.12.1/post_upload_sizes.js | 23 + src/upgrades/1.12.3/disable_plugin_metrics.js | 11 + .../1.12.3/give_mod_info_privilege.js | 27 + src/upgrades/1.12.3/give_mod_privileges.js | 63 + .../1.12.3/update_registration_type.js | 20 + src/upgrades/1.12.3/user_pid_sets.js | 35 + src/upgrades/1.13.0/clean_flag_byCid.js | 27 + src/upgrades/1.13.0/clean_post_topic_hash.js | 95 + .../1.13.0/cleanup_old_notifications.js | 51 + src/upgrades/1.13.3/fix_users_sorted_sets.js | 62 + .../1.13.4/remove_allowFileUploads_priv.js | 22 + .../1.14.0/fix_category_image_field.js | 23 + .../1.14.0/unescape_navigation_titles.js | 32 + .../1.14.1/readd_deleted_recent_topics.js | 56 + .../1.15.0/add_target_uid_to_flags.js | 37 + src/upgrades/1.15.0/consolidate_flags.js | 46 + src/upgrades/1.15.0/disable_sounds_plugin.js | 11 + src/upgrades/1.15.0/fix_category_colors.js | 21 + src/upgrades/1.15.0/fullname_search_set.js | 26 + src/upgrades/1.15.0/remove_allow_from_uri.js | 15 + .../1.15.0/remove_flag_reporters_zset.js | 33 + src/upgrades/1.15.0/topic_poster_count.js | 30 + src/upgrades/1.15.0/track_flags_by_target.js | 15 + src/upgrades/1.15.0/verified_users_group.js | 110 + src/upgrades/1.15.4/clear_purged_replies.js | 33 + src/upgrades/1.16.0/category_tags.js | 46 + src/upgrades/1.16.0/migrate_thumbs.js | 42 + src/upgrades/1.17.0/banned_users_group.js | 63 + src/upgrades/1.17.0/category_name_zset.js | 28 + src/upgrades/1.17.0/default_favicon.js | 20 + ...edule_privilege_for_existing_categories.js | 18 + src/upgrades/1.17.0/subcategories_per_page.js | 23 + src/upgrades/1.17.0/topic_thumb_count.js | 28 + .../enable_include_unverified_emails.js | 12 + src/upgrades/1.18.0/topic_tags_refactor.js | 37 + src/upgrades/1.18.4/category_topics_views.js | 23 + .../1.19.0/navigation-enabled-hashes.js | 31 + .../1.19.0/reenable-username-login.js | 15 + ...emove_leftover_thumbs_after_topic_purge.js | 51 + .../1.19.2/store_downvoted_posts_in_zset.js | 31 + src/upgrades/1.19.3/fix_user_uploads_zset.js | 43 + .../1.19.3/rename_post_upload_hashes.js | 63 + src/upgrades/1.2.0/category_recent_tids.js | 31 + .../edit_delete_deletetopic_privileges.js | 52 + src/upgrades/1.3.0/favourites_to_bookmarks.js | 39 + .../1.3.0/sorted_sets_for_post_replies.js | 39 + .../1.4.0/global_and_user_language_keys.js | 37 + .../1.4.0/sorted_set_for_pinned_topics.js | 34 + src/upgrades/1.4.4/config_urls_update.js | 34 + src/upgrades/1.4.4/sound_settings.js | 65 + src/upgrades/1.4.6/delete_sessions.js | 41 + src/upgrades/1.5.0/allowed_file_extensions.js | 16 + src/upgrades/1.5.0/flags_refactor.js | 57 + .../1.5.0/moderation_history_refactor.js | 35 + src/upgrades/1.5.0/post_votes_zset.js | 29 + .../remove_relative_uploaded_profile_cover.js | 26 + src/upgrades/1.5.1/rename_mods_group.js | 33 + src/upgrades/1.5.2/rss_token_wipe.js | 22 + src/upgrades/1.5.2/tags_privilege.js | 22 + .../1.6.0/clear-stale-digest-template.js | 21 + src/upgrades/1.6.0/generate-email-logo.js | 53 + src/upgrades/1.6.0/ipblacklist-fix.js | 13 + src/upgrades/1.6.0/robots-config-change.js | 21 + .../1.6.2/topics_lastposttime_zset.js | 29 + src/upgrades/1.7.0/generate-custom-html.js | 43 + src/upgrades/1.7.1/notification-settings.js | 31 + src/upgrades/1.7.3/key_value_schema_change.js | 45 + src/upgrades/1.7.3/topic_votes.js | 42 + src/upgrades/1.7.4/chat_privilege.js | 12 + .../1.7.4/fix_moved_topics_byvotes.js | 31 + .../1.7.4/fix_user_topics_per_category.js | 29 + src/upgrades/1.7.4/global_upload_privilege.js | 45 + .../1.7.4/rename_min_reputation_settings.js | 25 + src/upgrades/1.7.4/vote_privilege.js | 22 + src/upgrades/1.7.6/flatten_navigation_data.js | 24 + src/upgrades/1.7.6/notification_types.js | 21 + .../1.7.6/update_min_pass_strength.js | 14 + .../1.8.0/give_signature_privileges.js | 11 + src/upgrades/1.8.0/give_spiders_privileges.js | 49 + src/upgrades/1.8.1/diffs_zset_to_listhash.js | 57 + .../1.9.0/refresh_post_upload_associations.js | 21 + src/upgrades/TEMPLATE | 14 + src/user/admin.js | 89 + src/user/approval.js | 167 + src/user/auth.js | 163 + src/user/bans.js | 143 + src/user/blocks.js | 113 + src/user/categories.js | 76 + src/user/create.js | 199 + src/user/data.js | 356 + src/user/delete.js | 217 + src/user/digest.js | 212 + src/user/email.js | 212 + src/user/follow.js | 90 + src/user/index.js | 248 + src/user/info.js | 144 + src/user/interstitials.js | 198 + src/user/invite.js | 187 + src/user/jobs.js | 66 + src/user/jobs/export-posts.js | 56 + src/user/jobs/export-profile.js | 124 + src/user/jobs/export-uploads.js | 87 + src/user/notifications.js | 233 + src/user/online.js | 43 + src/user/password.js | 47 + src/user/picture.js | 233 + src/user/posts.js | 122 + src/user/profile.js | 335 + src/user/reset.js | 165 + src/user/search.js | 159 + src/user/settings.js | 171 + src/user/topics.js | 16 + src/user/uploads.js | 90 + src/utils.js | 32 + src/views/400.tpl | 12 + src/views/403.tpl | 16 + src/views/404.tpl | 8 + src/views/500.tpl | 10 + src/views/503.tpl | 12 + src/views/admin/advanced/cache.tpl | 46 + src/views/admin/advanced/database.tpl | 146 + src/views/admin/advanced/errors.tpl | 78 + src/views/admin/advanced/events.tpl | 71 + src/views/admin/advanced/hooks.tpl | 31 + src/views/admin/advanced/logs.tpl | 23 + src/views/admin/appearance/customise.tpl | 80 + src/views/admin/appearance/skins.tpl | 11 + src/views/admin/appearance/themes.tpl | 9 + src/views/admin/dashboard.tpl | 154 + src/views/admin/dashboard/logins.tpl | 35 + src/views/admin/dashboard/searches.tpl | 25 + src/views/admin/dashboard/topics.tpl | 28 + src/views/admin/dashboard/users.tpl | 35 + src/views/admin/development/info.tpl | 71 + src/views/admin/development/logger.tpl | 38 + src/views/admin/extend/plugins.tpl | 146 + src/views/admin/extend/rewards.tpl | 82 + src/views/admin/extend/widgets.tpl | 142 + src/views/admin/footer.tpl | 25 + src/views/admin/header.tpl | 30 + src/views/admin/manage/admins-mods.tpl | 77 + src/views/admin/manage/categories.tpl | 25 + src/views/admin/manage/category-analytics.tpl | 55 + src/views/admin/manage/category.tpl | 229 + src/views/admin/manage/digest.tpl | 52 + src/views/admin/manage/group.tpl | 165 + src/views/admin/manage/groups.tpl | 114 + src/views/admin/manage/privileges.tpl | 32 + src/views/admin/manage/registration.tpl | 132 + src/views/admin/manage/tags.tpl | 80 + src/views/admin/manage/uploads.tpl | 58 + src/views/admin/manage/users.tpl | 139 + .../admin/partials/api/sorted-list/form.tpl | 15 + .../admin/partials/api/sorted-list/item.tpl | 21 + .../admin/partials/blacklist-validate.tpl | 14 + .../partials/categories/category-rows.tpl | 60 + .../partials/categories/copy-settings.tpl | 7 + .../admin/partials/categories/create.tpl | 27 + .../admin/partials/categories/groups.tpl | 20 + src/views/admin/partials/categories/purge.tpl | 8 + .../partials/categories/select-category.tpl | 25 + src/views/admin/partials/categories/users.tpl | 22 + .../admin/partials/create_user_modal.tpl | 21 + src/views/admin/partials/dashboard/graph.tpl | 35 + src/views/admin/partials/dashboard/stats.tpl | 47 + .../admin/partials/download_plugin_item.tpl | 25 + .../admin/partials/groups/add-members.tpl | 8 + .../admin/partials/groups/memberlist.tpl | 46 + .../groups/privileges-select-category.tpl | 18 + .../admin/partials/installed_plugin_item.tpl | 60 + .../admin/partials/manage_user_groups.tpl | 13 + src/views/admin/partials/menu.tpl | 303 + .../admin/partials/pageviews-range-select.tpl | 20 + src/views/admin/partials/plugins/license.tpl | 5 + .../admin/partials/plugins/no-plugins.tpl | 1 + .../admin/partials/privileges/category.tpl | 137 + .../admin/partials/privileges/global.tpl | 118 + .../admin/partials/quick_actions/alerts.tpl | 10 + .../admin/partials/quick_actions/buttons.tpl | 24 + src/views/admin/partials/settings/footer.tpl | 5 + src/views/admin/partials/settings/header.tpl | 11 + src/views/admin/partials/temporary-ban.tpl | 32 + src/views/admin/partials/temporary-mute.tpl | 27 + src/views/admin/partials/theme_list.tpl | 24 + src/views/admin/partials/widget-settings.tpl | 13 + .../partials/widgets/show_hide_groups.tpl | 18 + src/views/admin/settings/advanced.tpl | 226 + src/views/admin/settings/api.tpl | 40 + src/views/admin/settings/chat.tpl | 59 + src/views/admin/settings/cookies.tpl | 74 + src/views/admin/settings/email.tpl | 219 + src/views/admin/settings/general.tpl | 226 + src/views/admin/settings/group.tpl | 54 + src/views/admin/settings/guest.tpl | 36 + src/views/admin/settings/homepage.tpl | 37 + src/views/admin/settings/languages.tpl | 34 + src/views/admin/settings/navigation.tpl | 153 + src/views/admin/settings/notifications.tpl | 15 + src/views/admin/settings/pagination.tpl | 46 + src/views/admin/settings/post.tpl | 337 + src/views/admin/settings/reputation.tpl | 136 + src/views/admin/settings/social.tpl | 24 + src/views/admin/settings/sockets.tpl | 19 + src/views/admin/settings/tags.tpl | 52 + src/views/admin/settings/uploads.tpl | 224 + src/views/admin/settings/user.tpl | 365 + src/views/admin/settings/web-crawler.tpl | 46 + src/views/emails/banned.tpl | 48 + src/views/emails/digest.tpl | 174 + src/views/emails/invitation.tpl | 47 + src/views/emails/notification.tpl | 50 + src/views/emails/partials/footer.tpl | 26 + src/views/emails/partials/header.tpl | 187 + src/views/emails/partials/post-queue-body.tpl | 9 + src/views/emails/registration_accepted.tpl | 43 + src/views/emails/reset.tpl | 50 + src/views/emails/reset_notify.tpl | 50 + src/views/emails/test.tpl | 43 + src/views/emails/verify-email.tpl | 57 + src/views/emails/welcome.tpl | 39 + src/views/install/index.tpl | 151 + src/views/modals/crop_picture.tpl | 39 + src/views/modals/invite.tpl | 12 + src/views/modals/move-post.tpl | 23 + src/views/modals/set-pin-expiry.tpl | 5 + src/views/modals/topic-thumbs.tpl | 20 + src/views/outgoing.tpl | 12 + src/views/partials/data/category.tpl | 1 + src/views/partials/data/topic.tpl | 1 + src/views/partials/email_update.tpl | 21 + src/views/partials/fontawesome.tpl | 799 ++ src/views/partials/footer/js.tpl | 23 + src/views/partials/gdpr_consent.tpl | 23 + src/views/partials/noscript/message.tpl | 9 + src/views/partials/noscript/warning.tpl | 10 + src/views/partials/topic/post-preview.tpl | 13 + src/views/sitemap.tpl | 18 + src/webserver.js | 328 + src/widgets/admin.js | 84 + src/widgets/index.js | 231 + test/.eslintrc | 8 + test/api.js | 592 + test/authentication.js | 639 + test/batch.js | 115 + test/blacklist.js | 68 + test/build.js | 245 + test/categories.js | 914 ++ test/controllers-admin.js | 959 ++ test/controllers.js | 2605 ++++ test/coverPhoto.js | 24 + test/database.js | 66 + test/database/hash.js | 677 + test/database/keys.js | 353 + test/database/list.js | 256 + test/database/sets.js | 288 + test/database/sorted.js | 1629 +++ test/defer-logger.js | 37 + test/emailer.js | 200 + test/feeds.js | 199 + test/file.js | 122 + test/files/1.css | 1 + test/files/1.js | 5 + test/files/2.js | 3 + test/files/2.less | 1 + test/files/503.html | 177 + test/files/brokenimage.png | Bin 0 -> 7482 bytes test/files/favicon.ico | Bin 0 -> 1150 bytes test/files/normalise.jpg | Bin 0 -> 5349 bytes test/files/notanimage.png | 1 + test/files/test.png | Bin 0 -> 7189 bytes test/files/test.wav | Bin 0 -> 26124 bytes test/files/toobig.png | Bin 0 -> 317110 bytes test/flags.js | 1153 ++ test/groups.js | 1483 ++ test/helpers/index.js | 232 + test/i18n.js | 123 + test/image.js | 38 + test/locale-detect.js | 46 + test/messaging.js | 868 ++ test/meta.js | 611 + test/middleware.js | 196 + test/mocks/databasemock.js | 262 + .../@nodebb/another-thing/package.json | 1 + .../@nodebb/another-thing/plugin.json | 1 + .../@nodebb/nodebb-plugin-abc/package.json | 1 + .../@nodebb/nodebb-plugin-abc/plugin.json | 1 + .../nodebb-plugin-xyz/package.json | 1 + .../nodebb-plugin-xyz/plugin.json | 1 + .../something-else/package.json | 1 + .../plugin_modules/something-else/plugin.json | 1 + test/notifications.js | 485 + test/package-install.js | 111 + test/pagination.js | 39 + test/password.js | 52 + test/plugins-installed.js | 23 + test/plugins.js | 403 + test/posts.js | 1245 ++ test/posts/uploads.js | 417 + test/pubsub.js | 54 + test/rewards.js | 79 + test/search-admin.js | 87 + test/search.js | 293 + test/settings.js | 59 + test/socket.io.js | 807 ++ test/template-helpers.js | 238 + test/topics.js | 2847 ++++ test/topics/events.js | 105 + test/topics/thumbs.js | 437 + test/translator.js | 380 + test/upgrade.js | 35 + test/uploads.js | 583 + test/user.js | 3087 +++++ test/user/emails.js | 236 + test/user/uploads.js | 166 + test/utils.js | 449 + themes/nodebb-theme-persona/README.md | 16 + .../languages/de/persona.json | 4 + .../languages/en-GB/persona.json | 10 + .../languages/en-US/persona.json | 3 + .../languages/fa-IR/persona.json | 4 + .../languages/fr/persona.json | 3 + .../languages/hu/persona.json | 4 + .../languages/pl/persona.json | 3 + .../languages/pt-PT/persona.json | 3 + .../languages/tr/persona.json | 4 + .../languages/zh-CN/persona.json | 4 + themes/nodebb-theme-persona/less/account.less | 461 + .../less/bootstrap-flipped.css | 1550 +++ .../less/bootstrap/alerts.less | 73 + .../less/bootstrap/badges.less | 66 + .../less/bootstrap/bootstrap.less | 50 + .../less/bootstrap/breadcrumbs.less | 26 + .../less/bootstrap/button-groups.less | 243 + .../less/bootstrap/buttons.less | 160 + .../less/bootstrap/carousel.less | 269 + .../less/bootstrap/close.less | 34 + .../less/bootstrap/code.less | 69 + .../less/bootstrap/component-animations.less | 33 + .../less/bootstrap/dropdowns.less | 214 + .../less/bootstrap/forms.less | 574 + .../less/bootstrap/glyphicons.less | 305 + .../less/bootstrap/grid.less | 84 + .../less/bootstrap/input-groups.less | 166 + .../less/bootstrap/jumbotron.less | 50 + .../less/bootstrap/labels.less | 64 + .../less/bootstrap/list-group.less | 124 + .../less/bootstrap/media.less | 61 + .../less/bootstrap/mixins.less | 39 + .../less/bootstrap/mixins/alerts.less | 14 + .../bootstrap/mixins/background-variant.less | 8 + .../less/bootstrap/mixins/border-radius.less | 18 + .../less/bootstrap/mixins/buttons.less | 52 + .../less/bootstrap/mixins/center-block.less | 7 + .../less/bootstrap/mixins/clearfix.less | 22 + .../less/bootstrap/mixins/forms.less | 85 + .../less/bootstrap/mixins/gradients.less | 59 + .../less/bootstrap/mixins/grid-framework.less | 91 + .../less/bootstrap/mixins/grid.less | 122 + .../less/bootstrap/mixins/hide-text.less | 21 + .../less/bootstrap/mixins/image.less | 33 + .../less/bootstrap/mixins/labels.less | 12 + .../less/bootstrap/mixins/list-group.less | 29 + .../less/bootstrap/mixins/nav-divider.less | 10 + .../bootstrap/mixins/nav-vertical-align.less | 9 + .../less/bootstrap/mixins/opacity.less | 8 + .../less/bootstrap/mixins/pagination.less | 23 + .../less/bootstrap/mixins/panels.less | 24 + .../less/bootstrap/mixins/progress-bar.less | 10 + .../less/bootstrap/mixins/reset-filter.less | 8 + .../less/bootstrap/mixins/resize.less | 6 + .../mixins/responsive-visibility.less | 15 + .../less/bootstrap/mixins/size.less | 10 + .../less/bootstrap/mixins/tab-focus.less | 9 + .../less/bootstrap/mixins/table-row.less | 28 + .../less/bootstrap/mixins/text-emphasis.less | 8 + .../less/bootstrap/mixins/text-overflow.less | 8 + .../bootstrap/mixins/vendor-prefixes.less | 227 + .../less/bootstrap/modals.less | 150 + .../less/bootstrap/navbar.less | 660 + .../less/bootstrap/navs.less | 242 + .../less/bootstrap/normalize.less | 427 + .../less/bootstrap/pager.less | 54 + .../less/bootstrap/pagination.less | 88 + .../less/bootstrap/panels.less | 265 + .../less/bootstrap/popovers.less | 135 + .../less/bootstrap/print.less | 107 + .../less/bootstrap/progress-bars.less | 87 + .../less/bootstrap/responsive-embed.less | 35 + .../less/bootstrap/responsive-utilities.less | 194 + .../less/bootstrap/scaffolding.less | 162 + .../less/bootstrap/tables.less | 234 + .../less/bootstrap/theme.less | 273 + .../less/bootstrap/thumbnails.less | 36 + .../less/bootstrap/tooltip.less | 102 + .../less/bootstrap/type.less | 302 + .../less/bootstrap/utilities.less | 55 + .../less/bootstrap/variables.less | 861 ++ .../less/bootstrap/wells.less | 29 + themes/nodebb-theme-persona/less/career.less | 8 + .../nodebb-theme-persona/less/categories.less | 235 + .../nodebb-theme-persona/less/category.less | 210 + themes/nodebb-theme-persona/less/chats.less | 587 + themes/nodebb-theme-persona/less/flags.less | 43 + themes/nodebb-theme-persona/less/footer.less | 18 + themes/nodebb-theme-persona/less/groups.less | 202 + themes/nodebb-theme-persona/less/header.less | 445 + themes/nodebb-theme-persona/less/helpers.less | 0 .../less/ip-blacklist.less | 7 + .../nodebb-theme-persona/less/keyframes.less | 152 + themes/nodebb-theme-persona/less/mixins.less | 172 + themes/nodebb-theme-persona/less/mobile.less | 336 + .../less/modules/alerts.less | 98 + .../less/modules/bottom-sheet.less | 60 + .../less/modules/composer-default.less | 17 + .../less/modules/cookie-consent.less | 13 + .../less/modules/fab.less | 33 + .../less/modules/morph.less | 269 + .../less/modules/necro-post.less | 9 + .../less/modules/nprogress.less | 80 + .../less/modules/taskbar.less | 160 + .../less/modules/usercard.less | 65 + .../nodebb-theme-persona/less/noscript.less | 83 + .../less/notifications.less | 38 + .../nodebb-theme-persona/less/outgoing.less | 9 + themes/nodebb-theme-persona/less/persona.less | 49 + .../nodebb-theme-persona/less/post-queue.less | 19 + .../nodebb-theme-persona/less/posts_list.less | 131 + .../nodebb-theme-persona/less/register.less | 76 + themes/nodebb-theme-persona/less/rtl.less | 132 + themes/nodebb-theme-persona/less/search.less | 96 + themes/nodebb-theme-persona/less/style.less | 297 + themes/nodebb-theme-persona/less/tags.less | 38 + themes/nodebb-theme-persona/less/topic.less | 699 + .../less/topics_list.less | 16 + themes/nodebb-theme-persona/less/users.less | 73 + .../nodebb-theme-persona/less/variables.less | 7 + .../nodebb-theme-persona/lib/controllers.js | 22 + themes/nodebb-theme-persona/library.js | 109 + themes/nodebb-theme-persona/package.json | 41 + themes/nodebb-theme-persona/plugin.json | 21 + themes/nodebb-theme-persona/public/admin.js | 15 + .../public/modules/autohidingnavbar.js | 217 + .../public/modules/quickreply.js | 97 + themes/nodebb-theme-persona/public/persona.js | 486 + .../nodebb-theme-persona/public/settings.js | 53 + themes/nodebb-theme-persona/screenshot.png | Bin 0 -> 39770 bytes .../templates/account/best.tpl | 1 + .../templates/account/blocks.tpl | 35 + .../templates/account/bookmarks.tpl | 1 + .../templates/account/categories.tpl | 28 + .../templates/account/consent.tpl | 69 + .../templates/account/controversial.tpl | 1 + .../templates/account/downvoted.tpl | 1 + .../templates/account/edit.tpl | 135 + .../templates/account/edit/password.tpl | 32 + .../templates/account/edit/username.tpl | 30 + .../templates/account/followers.tpl | 17 + .../templates/account/following.tpl | 17 + .../templates/account/groups.tpl | 17 + .../templates/account/ignored.tpl | 1 + .../templates/account/info.tpl | 231 + .../templates/account/posts.tpl | 19 + .../templates/account/profile.tpl | 164 + .../templates/account/sessions.tpl | 32 + .../templates/account/settings.tpl | 217 + .../templates/account/theme.tpl | 27 + .../templates/account/topics.tpl | 30 + .../templates/account/uploads.tpl | 43 + .../templates/account/upvoted.tpl | 1 + .../templates/account/watched.tpl | 1 + .../templates/admin/plugins/persona.tpl | 29 + .../nodebb-theme-persona/templates/alert.tpl | 15 + .../nodebb-theme-persona/templates/career.tpl | 26 + .../templates/categories.tpl | 31 + .../templates/category.tpl | 62 + .../nodebb-theme-persona/templates/chat.tpl | 32 + .../nodebb-theme-persona/templates/chats.tpl | 17 + .../templates/confirm.tpl | 7 + .../templates/flags/detail.tpl | 203 + .../templates/flags/list.tpl | 71 + .../nodebb-theme-persona/templates/footer.tpl | 14 + .../templates/groups/details.tpl | 272 + .../templates/groups/list.tpl | 46 + .../templates/groups/members.tpl | 9 + .../nodebb-theme-persona/templates/header.tpl | 40 + .../templates/ip-blacklist.tpl | 56 + .../nodebb-theme-persona/templates/login.tpl | 93 + .../templates/modules/taskbar.tpl | 3 + .../templates/modules/usercard.tpl | 37 + .../templates/notifications.tpl | 64 + .../templates/partials/acceptTos.tpl | 9 + .../partials/account/category-item.tpl | 22 + .../templates/partials/account/header.tpl | 46 + .../templates/partials/account/menu.tpl | 101 + .../templates/partials/breadcrumbs.tpl | 18 + .../templates/partials/buttons/newTopic.tpl | 15 + .../templates/partials/categories/item.tpl | 46 + .../partials/categories/lastpost.tpl | 26 + .../templates/partials/categories/link.tpl | 11 + .../partials/category-filter-content.tpl | 17 + .../partials/category-filter-right.tpl | 3 + .../templates/partials/category-filter.tpl | 3 + .../partials/category-selector-content.tpl | 18 + .../partials/category-selector-right.tpl | 3 + .../templates/partials/category-selector.tpl | 3 + .../templates/partials/category/sort.tpl | 15 + .../partials/category/subcategory.tpl | 18 + .../templates/partials/category/tags.tpl | 5 + .../templates/partials/category/tools.tpl | 81 + .../templates/partials/category/watch.tpl | 23 + .../templates/partials/change_owner_modal.tpl | 22 + .../templates/partials/chats-menu.tpl | 41 + .../templates/partials/chats/dropdown.tpl | 38 + .../partials/chats/message-window.tpl | 32 + .../templates/partials/chats/message.tpl | 34 + .../templates/partials/chats/messages.tpl | 7 + .../templates/partials/chats/options.tpl | 24 + .../templates/partials/chats/recent_room.tpl | 27 + .../partials/chats/system-message.tpl | 3 + .../templates/partials/chats/user.tpl | 1 + .../templates/partials/cookie-consent.tpl | 4 + .../templates/partials/delete_posts_modal.tpl | 19 + .../templates/partials/flags/filters.tpl | 95 + .../templates/partials/fork_thread_modal.tpl | 22 + .../templates/partials/groups/list.tpl | 21 + .../templates/partials/groups/memberlist.tpl | 40 + .../templates/partials/menu.tpl | 271 + .../templates/partials/merge_topics_modal.tpl | 58 + .../partials/modals/change_picture_modal.tpl | 73 + .../templates/partials/modals/flag_modal.tpl | 45 + .../templates/partials/modals/manage_room.tpl | 11 + .../partials/modals/manage_room_users.tpl | 7 + .../partials/modals/post_history.tpl | 36 + .../templates/partials/modals/rename_room.tpl | 4 + .../partials/modals/upload_file_modal.tpl | 44 + .../modals/upload_picture_from_url_modal.tpl | 17 + .../templates/partials/modals/votes_modal.tpl | 10 + .../templates/partials/move_thread_modal.tpl | 17 + .../templates/partials/notifications_list.tpl | 25 + .../templates/partials/paginator.tpl | 45 + .../templates/partials/post_bar.tpl | 20 + .../templates/partials/posts_list.tpl | 8 + .../templates/partials/posts_list_item.tpl | 32 + .../partials/quick-search-results.tpl | 31 + .../templates/partials/search-results.tpl | 50 + .../templates/partials/slideout-menu.tpl | 4 + .../templates/partials/tags_list.tpl | 5 + .../templates/partials/thread_tools.tpl | 8 + .../templates/partials/topic/badge.tpl | 5 + .../partials/topic/browsing-users.tpl | 1 + .../partials/topic/deleted-message.tpl | 11 + .../partials/topic/navigation-post.tpl | 12 + .../templates/partials/topic/navigator.tpl | 39 + .../templates/partials/topic/necro-post.tpl | 3 + .../templates/partials/topic/post-editor.tpl | 1 + .../partials/topic/post-menu-list.tpl | 129 + .../templates/partials/topic/post-menu.tpl | 4 + .../templates/partials/topic/post.tpl | 106 + .../templates/partials/topic/quickreply.tpl | 28 + .../templates/partials/topic/reactions.tpl | 1 + .../templates/partials/topic/reply-button.tpl | 27 + .../partials/topic/selection-tooltip.tpl | 3 + .../templates/partials/topic/sort.tpl | 9 + .../templates/partials/topic/stats.tpl | 12 + .../templates/partials/topic/tags.tpl | 5 + .../partials/topic/topic-menu-list.tpl | 30 + .../templates/partials/topic/watch.tpl | 21 + .../templates/partials/topics_list.tpl | 119 + .../templates/partials/users_list.tpl | 46 + .../templates/partials/users_list_menu.tpl | 11 + .../templates/popular.tpl | 61 + .../templates/post-queue.tpl | 101 + .../nodebb-theme-persona/templates/recent.tpl | 51 + .../templates/register.tpl | 87 + .../templates/registerComplete.tpl | 45 + .../nodebb-theme-persona/templates/reset.tpl | 23 + .../templates/reset_code.tpl | 42 + .../nodebb-theme-persona/templates/search.tpl | 173 + themes/nodebb-theme-persona/templates/tag.tpl | 35 + .../nodebb-theme-persona/templates/tags.tpl | 30 + themes/nodebb-theme-persona/templates/top.tpl | 61 + .../nodebb-theme-persona/templates/topic.tpl | 112 + themes/nodebb-theme-persona/templates/tos.tpl | 4 + .../nodebb-theme-persona/templates/unread.tpl | 49 + .../templates/unsubscribe.tpl | 15 + .../nodebb-theme-persona/templates/users.tpl | 47 + themes/nodebb-theme-persona/theme.json | 7 + themes/nodebb-theme-persona/theme.less | 2 + tsconfig.json | 17 + webpack.common.js | 62 + webpack.dev.js | 9 + webpack.installer.js | 24 + webpack.prod.js | 23 + 4990 files changed, 333515 insertions(+) create mode 100644 .codeclimate.yml create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.cjs create mode 100644 .gitattributes create mode 100644 .github/workflows/hw1.yaml create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .github/workflows/volunteers.yaml create mode 100644 .gitignore create mode 100644 .husky/.gitignore create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100644 .mocharc.yml create mode 100644 .tx/config create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 Gruntfile.js create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100644 build/.gitignore create mode 100644 build/export/.gitignore create mode 100644 build/export/README create mode 100644 commitlint.config.js create mode 100644 docker-compose.yml create mode 100644 install/data/categories.json create mode 100644 install/data/defaults.json create mode 100644 install/data/footer.json create mode 100644 install/data/navigation.json create mode 100644 install/data/welcome.md create mode 100644 install/databases.js create mode 100644 install/package.json create mode 100644 install/web.js create mode 100644 loader.js create mode 100755 nodebb create mode 100644 nodebb.bat create mode 100644 public/.eslintrc create mode 100644 public/503.html create mode 100644 public/favicon.ico create mode 100644 public/images/cover-default.png create mode 100644 public/images/logo.png create mode 100644 public/images/logo.svg create mode 100644 public/images/logo@3x.png create mode 100644 public/images/sm-card.png create mode 100644 public/images/themes/default.png create mode 100644 public/images/touch/144.png create mode 100644 public/images/touch/192.png create mode 100644 public/images/touch/36.png create mode 100644 public/images/touch/48.png create mode 100644 public/images/touch/512.png create mode 100644 public/images/touch/72.png create mode 100644 public/images/touch/96.png create mode 100644 public/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 public/images/ui-icons_444444_256x240.png create mode 100644 public/images/ui-icons_555555_256x240.png create mode 100644 public/images/ui-icons_777620_256x240.png create mode 100644 public/images/ui-icons_777777_256x240.png create mode 100644 public/images/ui-icons_cc0000_256x240.png create mode 100644 public/images/ui-icons_ffffff_256x240.png create mode 100644 public/language/README.md create mode 100644 public/language/ar/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/ar/admin/admin.json create mode 100644 public/language/ar/admin/advanced/cache.json create mode 100644 public/language/ar/admin/advanced/database.json create mode 100644 public/language/ar/admin/advanced/errors.json create mode 100644 public/language/ar/admin/advanced/events.json create mode 100644 public/language/ar/admin/advanced/logs.json create mode 100644 public/language/ar/admin/appearance/customise.json create mode 100644 public/language/ar/admin/appearance/skins.json create mode 100644 public/language/ar/admin/appearance/themes.json create mode 100644 public/language/ar/admin/dashboard.json create mode 100644 public/language/ar/admin/development/info.json create mode 100644 public/language/ar/admin/development/logger.json create mode 100644 public/language/ar/admin/extend/plugins.json create mode 100644 public/language/ar/admin/extend/rewards.json create mode 100644 public/language/ar/admin/extend/widgets.json create mode 100644 public/language/ar/admin/manage/admins-mods.json create mode 100644 public/language/ar/admin/manage/categories.json create mode 100644 public/language/ar/admin/manage/digest.json create mode 100644 public/language/ar/admin/manage/groups.json create mode 100644 public/language/ar/admin/manage/privileges.json create mode 100644 public/language/ar/admin/manage/registration.json create mode 100644 public/language/ar/admin/manage/tags.json create mode 100644 public/language/ar/admin/manage/uploads.json create mode 100644 public/language/ar/admin/manage/users.json create mode 100644 public/language/ar/admin/menu.json create mode 100644 public/language/ar/admin/settings/advanced.json create mode 100644 public/language/ar/admin/settings/api.json create mode 100644 public/language/ar/admin/settings/chat.json create mode 100644 public/language/ar/admin/settings/cookies.json create mode 100644 public/language/ar/admin/settings/email.json create mode 100644 public/language/ar/admin/settings/general.json create mode 100644 public/language/ar/admin/settings/group.json create mode 100644 public/language/ar/admin/settings/guest.json create mode 100644 public/language/ar/admin/settings/homepage.json create mode 100644 public/language/ar/admin/settings/languages.json create mode 100644 public/language/ar/admin/settings/navigation.json create mode 100644 public/language/ar/admin/settings/notifications.json create mode 100644 public/language/ar/admin/settings/pagination.json create mode 100644 public/language/ar/admin/settings/post.json create mode 100644 public/language/ar/admin/settings/reputation.json create mode 100644 public/language/ar/admin/settings/social.json create mode 100644 public/language/ar/admin/settings/sockets.json create mode 100644 public/language/ar/admin/settings/sounds.json create mode 100644 public/language/ar/admin/settings/tags.json create mode 100644 public/language/ar/admin/settings/uploads.json create mode 100644 public/language/ar/admin/settings/user.json create mode 100644 public/language/ar/admin/settings/web-crawler.json create mode 100644 public/language/ar/category.json create mode 100644 public/language/ar/email.json create mode 100644 public/language/ar/error.json create mode 100644 public/language/ar/flags.json create mode 100644 public/language/ar/global.json create mode 100644 public/language/ar/groups.json create mode 100644 public/language/ar/ip-blacklist.json create mode 100644 public/language/ar/language.json create mode 100644 public/language/ar/login.json create mode 100644 public/language/ar/modules.json create mode 100644 public/language/ar/notifications.json create mode 100644 public/language/ar/pages.json create mode 100644 public/language/ar/post-queue.json create mode 100644 public/language/ar/recent.json create mode 100644 public/language/ar/register.json create mode 100644 public/language/ar/reset_password.json create mode 100644 public/language/ar/search.json create mode 100644 public/language/ar/success.json create mode 100644 public/language/ar/tags.json create mode 100644 public/language/ar/top.json create mode 100644 public/language/ar/topic.json create mode 100644 public/language/ar/unread.json create mode 100644 public/language/ar/uploads.json create mode 100644 public/language/ar/user.json create mode 100644 public/language/ar/users.json create mode 100644 public/language/bg/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/bg/admin/admin.json create mode 100644 public/language/bg/admin/advanced/cache.json create mode 100644 public/language/bg/admin/advanced/database.json create mode 100644 public/language/bg/admin/advanced/errors.json create mode 100644 public/language/bg/admin/advanced/events.json create mode 100644 public/language/bg/admin/advanced/logs.json create mode 100644 public/language/bg/admin/appearance/customise.json create mode 100644 public/language/bg/admin/appearance/skins.json create mode 100644 public/language/bg/admin/appearance/themes.json create mode 100644 public/language/bg/admin/dashboard.json create mode 100644 public/language/bg/admin/development/info.json create mode 100644 public/language/bg/admin/development/logger.json create mode 100644 public/language/bg/admin/extend/plugins.json create mode 100644 public/language/bg/admin/extend/rewards.json create mode 100644 public/language/bg/admin/extend/widgets.json create mode 100644 public/language/bg/admin/manage/admins-mods.json create mode 100644 public/language/bg/admin/manage/categories.json create mode 100644 public/language/bg/admin/manage/digest.json create mode 100644 public/language/bg/admin/manage/groups.json create mode 100644 public/language/bg/admin/manage/privileges.json create mode 100644 public/language/bg/admin/manage/registration.json create mode 100644 public/language/bg/admin/manage/tags.json create mode 100644 public/language/bg/admin/manage/uploads.json create mode 100644 public/language/bg/admin/manage/users.json create mode 100644 public/language/bg/admin/menu.json create mode 100644 public/language/bg/admin/settings/advanced.json create mode 100644 public/language/bg/admin/settings/api.json create mode 100644 public/language/bg/admin/settings/chat.json create mode 100644 public/language/bg/admin/settings/cookies.json create mode 100644 public/language/bg/admin/settings/email.json create mode 100644 public/language/bg/admin/settings/general.json create mode 100644 public/language/bg/admin/settings/group.json create mode 100644 public/language/bg/admin/settings/guest.json create mode 100644 public/language/bg/admin/settings/homepage.json create mode 100644 public/language/bg/admin/settings/languages.json create mode 100644 public/language/bg/admin/settings/navigation.json create mode 100644 public/language/bg/admin/settings/notifications.json create mode 100644 public/language/bg/admin/settings/pagination.json create mode 100644 public/language/bg/admin/settings/post.json create mode 100644 public/language/bg/admin/settings/reputation.json create mode 100644 public/language/bg/admin/settings/social.json create mode 100644 public/language/bg/admin/settings/sockets.json create mode 100644 public/language/bg/admin/settings/sounds.json create mode 100644 public/language/bg/admin/settings/tags.json create mode 100644 public/language/bg/admin/settings/uploads.json create mode 100644 public/language/bg/admin/settings/user.json create mode 100644 public/language/bg/admin/settings/web-crawler.json create mode 100644 public/language/bg/category.json create mode 100644 public/language/bg/email.json create mode 100644 public/language/bg/error.json create mode 100644 public/language/bg/flags.json create mode 100644 public/language/bg/global.json create mode 100644 public/language/bg/groups.json create mode 100644 public/language/bg/ip-blacklist.json create mode 100644 public/language/bg/language.json create mode 100644 public/language/bg/login.json create mode 100644 public/language/bg/modules.json create mode 100644 public/language/bg/notifications.json create mode 100644 public/language/bg/pages.json create mode 100644 public/language/bg/post-queue.json create mode 100644 public/language/bg/recent.json create mode 100644 public/language/bg/register.json create mode 100644 public/language/bg/reset_password.json create mode 100644 public/language/bg/search.json create mode 100644 public/language/bg/success.json create mode 100644 public/language/bg/tags.json create mode 100644 public/language/bg/top.json create mode 100644 public/language/bg/topic.json create mode 100644 public/language/bg/unread.json create mode 100644 public/language/bg/uploads.json create mode 100644 public/language/bg/user.json create mode 100644 public/language/bg/users.json create mode 100644 public/language/bn/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/bn/admin/admin.json create mode 100644 public/language/bn/admin/advanced/cache.json create mode 100644 public/language/bn/admin/advanced/database.json create mode 100644 public/language/bn/admin/advanced/errors.json create mode 100644 public/language/bn/admin/advanced/events.json create mode 100644 public/language/bn/admin/advanced/logs.json create mode 100644 public/language/bn/admin/appearance/customise.json create mode 100644 public/language/bn/admin/appearance/skins.json create mode 100644 public/language/bn/admin/appearance/themes.json create mode 100644 public/language/bn/admin/dashboard.json create mode 100644 public/language/bn/admin/development/info.json create mode 100644 public/language/bn/admin/development/logger.json create mode 100644 public/language/bn/admin/extend/plugins.json create mode 100644 public/language/bn/admin/extend/rewards.json create mode 100644 public/language/bn/admin/extend/widgets.json create mode 100644 public/language/bn/admin/manage/admins-mods.json create mode 100644 public/language/bn/admin/manage/categories.json create mode 100644 public/language/bn/admin/manage/digest.json create mode 100644 public/language/bn/admin/manage/groups.json create mode 100644 public/language/bn/admin/manage/privileges.json create mode 100644 public/language/bn/admin/manage/registration.json create mode 100644 public/language/bn/admin/manage/tags.json create mode 100644 public/language/bn/admin/manage/uploads.json create mode 100644 public/language/bn/admin/manage/users.json create mode 100644 public/language/bn/admin/menu.json create mode 100644 public/language/bn/admin/settings/advanced.json create mode 100644 public/language/bn/admin/settings/api.json create mode 100644 public/language/bn/admin/settings/chat.json create mode 100644 public/language/bn/admin/settings/cookies.json create mode 100644 public/language/bn/admin/settings/email.json create mode 100644 public/language/bn/admin/settings/general.json create mode 100644 public/language/bn/admin/settings/group.json create mode 100644 public/language/bn/admin/settings/guest.json create mode 100644 public/language/bn/admin/settings/homepage.json create mode 100644 public/language/bn/admin/settings/languages.json create mode 100644 public/language/bn/admin/settings/navigation.json create mode 100644 public/language/bn/admin/settings/notifications.json create mode 100644 public/language/bn/admin/settings/pagination.json create mode 100644 public/language/bn/admin/settings/post.json create mode 100644 public/language/bn/admin/settings/reputation.json create mode 100644 public/language/bn/admin/settings/social.json create mode 100644 public/language/bn/admin/settings/sockets.json create mode 100644 public/language/bn/admin/settings/sounds.json create mode 100644 public/language/bn/admin/settings/tags.json create mode 100644 public/language/bn/admin/settings/uploads.json create mode 100644 public/language/bn/admin/settings/user.json create mode 100644 public/language/bn/admin/settings/web-crawler.json create mode 100644 public/language/bn/category.json create mode 100644 public/language/bn/email.json create mode 100644 public/language/bn/error.json create mode 100644 public/language/bn/flags.json create mode 100644 public/language/bn/global.json create mode 100644 public/language/bn/groups.json create mode 100644 public/language/bn/ip-blacklist.json create mode 100644 public/language/bn/language.json create mode 100644 public/language/bn/login.json create mode 100644 public/language/bn/modules.json create mode 100644 public/language/bn/notifications.json create mode 100644 public/language/bn/pages.json create mode 100644 public/language/bn/post-queue.json create mode 100644 public/language/bn/recent.json create mode 100644 public/language/bn/register.json create mode 100644 public/language/bn/reset_password.json create mode 100644 public/language/bn/search.json create mode 100644 public/language/bn/success.json create mode 100644 public/language/bn/tags.json create mode 100644 public/language/bn/top.json create mode 100644 public/language/bn/topic.json create mode 100644 public/language/bn/unread.json create mode 100644 public/language/bn/uploads.json create mode 100644 public/language/bn/user.json create mode 100644 public/language/bn/users.json create mode 100644 public/language/cs/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/cs/admin/admin.json create mode 100644 public/language/cs/admin/advanced/cache.json create mode 100644 public/language/cs/admin/advanced/database.json create mode 100644 public/language/cs/admin/advanced/errors.json create mode 100644 public/language/cs/admin/advanced/events.json create mode 100644 public/language/cs/admin/advanced/logs.json create mode 100644 public/language/cs/admin/appearance/customise.json create mode 100644 public/language/cs/admin/appearance/skins.json create mode 100644 public/language/cs/admin/appearance/themes.json create mode 100644 public/language/cs/admin/dashboard.json create mode 100644 public/language/cs/admin/development/info.json create mode 100644 public/language/cs/admin/development/logger.json create mode 100644 public/language/cs/admin/extend/plugins.json create mode 100644 public/language/cs/admin/extend/rewards.json create mode 100644 public/language/cs/admin/extend/widgets.json create mode 100644 public/language/cs/admin/manage/admins-mods.json create mode 100644 public/language/cs/admin/manage/categories.json create mode 100644 public/language/cs/admin/manage/digest.json create mode 100644 public/language/cs/admin/manage/groups.json create mode 100644 public/language/cs/admin/manage/privileges.json create mode 100644 public/language/cs/admin/manage/registration.json create mode 100644 public/language/cs/admin/manage/tags.json create mode 100644 public/language/cs/admin/manage/uploads.json create mode 100644 public/language/cs/admin/manage/users.json create mode 100644 public/language/cs/admin/menu.json create mode 100644 public/language/cs/admin/settings/advanced.json create mode 100644 public/language/cs/admin/settings/api.json create mode 100644 public/language/cs/admin/settings/chat.json create mode 100644 public/language/cs/admin/settings/cookies.json create mode 100644 public/language/cs/admin/settings/email.json create mode 100644 public/language/cs/admin/settings/general.json create mode 100644 public/language/cs/admin/settings/group.json create mode 100644 public/language/cs/admin/settings/guest.json create mode 100644 public/language/cs/admin/settings/homepage.json create mode 100644 public/language/cs/admin/settings/languages.json create mode 100644 public/language/cs/admin/settings/navigation.json create mode 100644 public/language/cs/admin/settings/notifications.json create mode 100644 public/language/cs/admin/settings/pagination.json create mode 100644 public/language/cs/admin/settings/post.json create mode 100644 public/language/cs/admin/settings/reputation.json create mode 100644 public/language/cs/admin/settings/social.json create mode 100644 public/language/cs/admin/settings/sockets.json create mode 100644 public/language/cs/admin/settings/sounds.json create mode 100644 public/language/cs/admin/settings/tags.json create mode 100644 public/language/cs/admin/settings/uploads.json create mode 100644 public/language/cs/admin/settings/user.json create mode 100644 public/language/cs/admin/settings/web-crawler.json create mode 100644 public/language/cs/category.json create mode 100644 public/language/cs/email.json create mode 100644 public/language/cs/error.json create mode 100644 public/language/cs/flags.json create mode 100644 public/language/cs/global.json create mode 100644 public/language/cs/groups.json create mode 100644 public/language/cs/ip-blacklist.json create mode 100644 public/language/cs/language.json create mode 100644 public/language/cs/login.json create mode 100644 public/language/cs/modules.json create mode 100644 public/language/cs/notifications.json create mode 100644 public/language/cs/pages.json create mode 100644 public/language/cs/post-queue.json create mode 100644 public/language/cs/recent.json create mode 100644 public/language/cs/register.json create mode 100644 public/language/cs/reset_password.json create mode 100644 public/language/cs/search.json create mode 100644 public/language/cs/success.json create mode 100644 public/language/cs/tags.json create mode 100644 public/language/cs/top.json create mode 100644 public/language/cs/topic.json create mode 100644 public/language/cs/unread.json create mode 100644 public/language/cs/uploads.json create mode 100644 public/language/cs/user.json create mode 100644 public/language/cs/users.json create mode 100644 public/language/da/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/da/admin/admin.json create mode 100644 public/language/da/admin/advanced/cache.json create mode 100644 public/language/da/admin/advanced/database.json create mode 100644 public/language/da/admin/advanced/errors.json create mode 100644 public/language/da/admin/advanced/events.json create mode 100644 public/language/da/admin/advanced/logs.json create mode 100644 public/language/da/admin/appearance/customise.json create mode 100644 public/language/da/admin/appearance/skins.json create mode 100644 public/language/da/admin/appearance/themes.json create mode 100644 public/language/da/admin/dashboard.json create mode 100644 public/language/da/admin/development/info.json create mode 100644 public/language/da/admin/development/logger.json create mode 100644 public/language/da/admin/extend/plugins.json create mode 100644 public/language/da/admin/extend/rewards.json create mode 100644 public/language/da/admin/extend/widgets.json create mode 100644 public/language/da/admin/manage/admins-mods.json create mode 100644 public/language/da/admin/manage/categories.json create mode 100644 public/language/da/admin/manage/digest.json create mode 100644 public/language/da/admin/manage/groups.json create mode 100644 public/language/da/admin/manage/privileges.json create mode 100644 public/language/da/admin/manage/registration.json create mode 100644 public/language/da/admin/manage/tags.json create mode 100644 public/language/da/admin/manage/uploads.json create mode 100644 public/language/da/admin/manage/users.json create mode 100644 public/language/da/admin/menu.json create mode 100644 public/language/da/admin/settings/advanced.json create mode 100644 public/language/da/admin/settings/api.json create mode 100644 public/language/da/admin/settings/chat.json create mode 100644 public/language/da/admin/settings/cookies.json create mode 100644 public/language/da/admin/settings/email.json create mode 100644 public/language/da/admin/settings/general.json create mode 100644 public/language/da/admin/settings/group.json create mode 100644 public/language/da/admin/settings/guest.json create mode 100644 public/language/da/admin/settings/homepage.json create mode 100644 public/language/da/admin/settings/languages.json create mode 100644 public/language/da/admin/settings/navigation.json create mode 100644 public/language/da/admin/settings/notifications.json create mode 100644 public/language/da/admin/settings/pagination.json create mode 100644 public/language/da/admin/settings/post.json create mode 100644 public/language/da/admin/settings/reputation.json create mode 100644 public/language/da/admin/settings/social.json create mode 100644 public/language/da/admin/settings/sockets.json create mode 100644 public/language/da/admin/settings/sounds.json create mode 100644 public/language/da/admin/settings/tags.json create mode 100644 public/language/da/admin/settings/uploads.json create mode 100644 public/language/da/admin/settings/user.json create mode 100644 public/language/da/admin/settings/web-crawler.json create mode 100644 public/language/da/category.json create mode 100644 public/language/da/email.json create mode 100644 public/language/da/error.json create mode 100644 public/language/da/flags.json create mode 100644 public/language/da/global.json create mode 100644 public/language/da/groups.json create mode 100644 public/language/da/ip-blacklist.json create mode 100644 public/language/da/language.json create mode 100644 public/language/da/login.json create mode 100644 public/language/da/modules.json create mode 100644 public/language/da/notifications.json create mode 100644 public/language/da/pages.json create mode 100644 public/language/da/post-queue.json create mode 100644 public/language/da/recent.json create mode 100644 public/language/da/register.json create mode 100644 public/language/da/reset_password.json create mode 100644 public/language/da/search.json create mode 100644 public/language/da/success.json create mode 100644 public/language/da/tags.json create mode 100644 public/language/da/top.json create mode 100644 public/language/da/topic.json create mode 100644 public/language/da/unread.json create mode 100644 public/language/da/uploads.json create mode 100644 public/language/da/user.json create mode 100644 public/language/da/users.json create mode 100644 public/language/de/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/de/admin/admin.json create mode 100644 public/language/de/admin/advanced/cache.json create mode 100644 public/language/de/admin/advanced/database.json create mode 100644 public/language/de/admin/advanced/errors.json create mode 100644 public/language/de/admin/advanced/events.json create mode 100644 public/language/de/admin/advanced/logs.json create mode 100644 public/language/de/admin/appearance/customise.json create mode 100644 public/language/de/admin/appearance/skins.json create mode 100644 public/language/de/admin/appearance/themes.json create mode 100644 public/language/de/admin/dashboard.json create mode 100644 public/language/de/admin/development/info.json create mode 100644 public/language/de/admin/development/logger.json create mode 100644 public/language/de/admin/extend/plugins.json create mode 100644 public/language/de/admin/extend/rewards.json create mode 100644 public/language/de/admin/extend/widgets.json create mode 100644 public/language/de/admin/manage/admins-mods.json create mode 100644 public/language/de/admin/manage/categories.json create mode 100644 public/language/de/admin/manage/digest.json create mode 100644 public/language/de/admin/manage/groups.json create mode 100644 public/language/de/admin/manage/privileges.json create mode 100644 public/language/de/admin/manage/registration.json create mode 100644 public/language/de/admin/manage/tags.json create mode 100644 public/language/de/admin/manage/uploads.json create mode 100644 public/language/de/admin/manage/users.json create mode 100644 public/language/de/admin/menu.json create mode 100644 public/language/de/admin/settings/advanced.json create mode 100644 public/language/de/admin/settings/api.json create mode 100644 public/language/de/admin/settings/chat.json create mode 100644 public/language/de/admin/settings/cookies.json create mode 100644 public/language/de/admin/settings/email.json create mode 100644 public/language/de/admin/settings/general.json create mode 100644 public/language/de/admin/settings/group.json create mode 100644 public/language/de/admin/settings/guest.json create mode 100644 public/language/de/admin/settings/homepage.json create mode 100644 public/language/de/admin/settings/languages.json create mode 100644 public/language/de/admin/settings/navigation.json create mode 100644 public/language/de/admin/settings/notifications.json create mode 100644 public/language/de/admin/settings/pagination.json create mode 100644 public/language/de/admin/settings/post.json create mode 100644 public/language/de/admin/settings/reputation.json create mode 100644 public/language/de/admin/settings/social.json create mode 100644 public/language/de/admin/settings/sockets.json create mode 100644 public/language/de/admin/settings/sounds.json create mode 100644 public/language/de/admin/settings/tags.json create mode 100644 public/language/de/admin/settings/uploads.json create mode 100644 public/language/de/admin/settings/user.json create mode 100644 public/language/de/admin/settings/web-crawler.json create mode 100644 public/language/de/category.json create mode 100644 public/language/de/email.json create mode 100644 public/language/de/error.json create mode 100644 public/language/de/flags.json create mode 100644 public/language/de/global.json create mode 100644 public/language/de/groups.json create mode 100644 public/language/de/ip-blacklist.json create mode 100644 public/language/de/language.json create mode 100644 public/language/de/login.json create mode 100644 public/language/de/modules.json create mode 100644 public/language/de/notifications.json create mode 100644 public/language/de/pages.json create mode 100644 public/language/de/post-queue.json create mode 100644 public/language/de/recent.json create mode 100644 public/language/de/register.json create mode 100644 public/language/de/reset_password.json create mode 100644 public/language/de/search.json create mode 100644 public/language/de/success.json create mode 100644 public/language/de/tags.json create mode 100644 public/language/de/top.json create mode 100644 public/language/de/topic.json create mode 100644 public/language/de/unread.json create mode 100644 public/language/de/uploads.json create mode 100644 public/language/de/user.json create mode 100644 public/language/de/users.json create mode 100644 public/language/el/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/el/admin/admin.json create mode 100644 public/language/el/admin/advanced/cache.json create mode 100644 public/language/el/admin/advanced/database.json create mode 100644 public/language/el/admin/advanced/errors.json create mode 100644 public/language/el/admin/advanced/events.json create mode 100644 public/language/el/admin/advanced/logs.json create mode 100644 public/language/el/admin/appearance/customise.json create mode 100644 public/language/el/admin/appearance/skins.json create mode 100644 public/language/el/admin/appearance/themes.json create mode 100644 public/language/el/admin/dashboard.json create mode 100644 public/language/el/admin/development/info.json create mode 100644 public/language/el/admin/development/logger.json create mode 100644 public/language/el/admin/extend/plugins.json create mode 100644 public/language/el/admin/extend/rewards.json create mode 100644 public/language/el/admin/extend/widgets.json create mode 100644 public/language/el/admin/manage/admins-mods.json create mode 100644 public/language/el/admin/manage/categories.json create mode 100644 public/language/el/admin/manage/digest.json create mode 100644 public/language/el/admin/manage/groups.json create mode 100644 public/language/el/admin/manage/privileges.json create mode 100644 public/language/el/admin/manage/registration.json create mode 100644 public/language/el/admin/manage/tags.json create mode 100644 public/language/el/admin/manage/uploads.json create mode 100644 public/language/el/admin/manage/users.json create mode 100644 public/language/el/admin/menu.json create mode 100644 public/language/el/admin/settings/advanced.json create mode 100644 public/language/el/admin/settings/api.json create mode 100644 public/language/el/admin/settings/chat.json create mode 100644 public/language/el/admin/settings/cookies.json create mode 100644 public/language/el/admin/settings/email.json create mode 100644 public/language/el/admin/settings/general.json create mode 100644 public/language/el/admin/settings/group.json create mode 100644 public/language/el/admin/settings/guest.json create mode 100644 public/language/el/admin/settings/homepage.json create mode 100644 public/language/el/admin/settings/languages.json create mode 100644 public/language/el/admin/settings/navigation.json create mode 100644 public/language/el/admin/settings/notifications.json create mode 100644 public/language/el/admin/settings/pagination.json create mode 100644 public/language/el/admin/settings/post.json create mode 100644 public/language/el/admin/settings/reputation.json create mode 100644 public/language/el/admin/settings/social.json create mode 100644 public/language/el/admin/settings/sockets.json create mode 100644 public/language/el/admin/settings/sounds.json create mode 100644 public/language/el/admin/settings/tags.json create mode 100644 public/language/el/admin/settings/uploads.json create mode 100644 public/language/el/admin/settings/user.json create mode 100644 public/language/el/admin/settings/web-crawler.json create mode 100644 public/language/el/category.json create mode 100644 public/language/el/email.json create mode 100644 public/language/el/error.json create mode 100644 public/language/el/flags.json create mode 100644 public/language/el/global.json create mode 100644 public/language/el/groups.json create mode 100644 public/language/el/ip-blacklist.json create mode 100644 public/language/el/language.json create mode 100644 public/language/el/login.json create mode 100644 public/language/el/modules.json create mode 100644 public/language/el/notifications.json create mode 100644 public/language/el/pages.json create mode 100644 public/language/el/post-queue.json create mode 100644 public/language/el/recent.json create mode 100644 public/language/el/register.json create mode 100644 public/language/el/reset_password.json create mode 100644 public/language/el/search.json create mode 100644 public/language/el/success.json create mode 100644 public/language/el/tags.json create mode 100644 public/language/el/top.json create mode 100644 public/language/el/topic.json create mode 100644 public/language/el/unread.json create mode 100644 public/language/el/uploads.json create mode 100644 public/language/el/user.json create mode 100644 public/language/el/users.json create mode 100644 public/language/en-GB/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/en-GB/admin/admin.json create mode 100644 public/language/en-GB/admin/advanced/cache.json create mode 100644 public/language/en-GB/admin/advanced/database.json create mode 100644 public/language/en-GB/admin/advanced/errors.json create mode 100644 public/language/en-GB/admin/advanced/events.json create mode 100644 public/language/en-GB/admin/advanced/logs.json create mode 100644 public/language/en-GB/admin/appearance/customise.json create mode 100644 public/language/en-GB/admin/appearance/skins.json create mode 100644 public/language/en-GB/admin/appearance/themes.json create mode 100644 public/language/en-GB/admin/dashboard.json create mode 100644 public/language/en-GB/admin/development/info.json create mode 100644 public/language/en-GB/admin/development/logger.json create mode 100644 public/language/en-GB/admin/extend/plugins.json create mode 100644 public/language/en-GB/admin/extend/rewards.json create mode 100644 public/language/en-GB/admin/extend/widgets.json create mode 100644 public/language/en-GB/admin/manage/admins-mods.json create mode 100644 public/language/en-GB/admin/manage/categories.json create mode 100644 public/language/en-GB/admin/manage/digest.json create mode 100644 public/language/en-GB/admin/manage/groups.json create mode 100644 public/language/en-GB/admin/manage/privileges.json create mode 100644 public/language/en-GB/admin/manage/registration.json create mode 100644 public/language/en-GB/admin/manage/tags.json create mode 100644 public/language/en-GB/admin/manage/uploads.json create mode 100644 public/language/en-GB/admin/manage/users.json create mode 100644 public/language/en-GB/admin/menu.json create mode 100644 public/language/en-GB/admin/settings/advanced.json create mode 100644 public/language/en-GB/admin/settings/api.json create mode 100644 public/language/en-GB/admin/settings/chat.json create mode 100644 public/language/en-GB/admin/settings/cookies.json create mode 100644 public/language/en-GB/admin/settings/email.json create mode 100644 public/language/en-GB/admin/settings/general.json create mode 100644 public/language/en-GB/admin/settings/group.json create mode 100644 public/language/en-GB/admin/settings/guest.json create mode 100644 public/language/en-GB/admin/settings/homepage.json create mode 100644 public/language/en-GB/admin/settings/languages.json create mode 100644 public/language/en-GB/admin/settings/navigation.json create mode 100644 public/language/en-GB/admin/settings/notifications.json create mode 100644 public/language/en-GB/admin/settings/pagination.json create mode 100644 public/language/en-GB/admin/settings/post.json create mode 100644 public/language/en-GB/admin/settings/reputation.json create mode 100644 public/language/en-GB/admin/settings/social.json create mode 100644 public/language/en-GB/admin/settings/sockets.json create mode 100644 public/language/en-GB/admin/settings/sounds.json create mode 100644 public/language/en-GB/admin/settings/tags.json create mode 100644 public/language/en-GB/admin/settings/uploads.json create mode 100644 public/language/en-GB/admin/settings/user.json create mode 100644 public/language/en-GB/admin/settings/web-crawler.json create mode 100644 public/language/en-GB/category.json create mode 100644 public/language/en-GB/email.json create mode 100644 public/language/en-GB/error.json create mode 100644 public/language/en-GB/flags.json create mode 100644 public/language/en-GB/global.json create mode 100644 public/language/en-GB/groups.json create mode 100644 public/language/en-GB/ip-blacklist.json create mode 100644 public/language/en-GB/language.json create mode 100644 public/language/en-GB/login.json create mode 100644 public/language/en-GB/modules.json create mode 100644 public/language/en-GB/notifications.json create mode 100644 public/language/en-GB/pages.json create mode 100644 public/language/en-GB/post-queue.json create mode 100644 public/language/en-GB/recent.json create mode 100644 public/language/en-GB/register.json create mode 100644 public/language/en-GB/reset_password.json create mode 100644 public/language/en-GB/search.json create mode 100644 public/language/en-GB/success.json create mode 100644 public/language/en-GB/tags.json create mode 100644 public/language/en-GB/top.json create mode 100644 public/language/en-GB/topic.json create mode 100644 public/language/en-GB/unread.json create mode 100644 public/language/en-GB/uploads.json create mode 100644 public/language/en-GB/user.json create mode 100644 public/language/en-GB/users.json create mode 100644 public/language/en-US/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/en-US/admin/admin.json create mode 100644 public/language/en-US/admin/advanced/cache.json create mode 100644 public/language/en-US/admin/advanced/database.json create mode 100644 public/language/en-US/admin/advanced/errors.json create mode 100644 public/language/en-US/admin/advanced/events.json create mode 100644 public/language/en-US/admin/advanced/logs.json create mode 100644 public/language/en-US/admin/appearance/customise.json create mode 100644 public/language/en-US/admin/appearance/skins.json create mode 100644 public/language/en-US/admin/appearance/themes.json create mode 100644 public/language/en-US/admin/dashboard.json create mode 100644 public/language/en-US/admin/development/info.json create mode 100644 public/language/en-US/admin/development/logger.json create mode 100644 public/language/en-US/admin/extend/plugins.json create mode 100644 public/language/en-US/admin/extend/rewards.json create mode 100644 public/language/en-US/admin/extend/widgets.json create mode 100644 public/language/en-US/admin/manage/admins-mods.json create mode 100644 public/language/en-US/admin/manage/categories.json create mode 100644 public/language/en-US/admin/manage/digest.json create mode 100644 public/language/en-US/admin/manage/groups.json create mode 100644 public/language/en-US/admin/manage/privileges.json create mode 100644 public/language/en-US/admin/manage/registration.json create mode 100644 public/language/en-US/admin/manage/tags.json create mode 100644 public/language/en-US/admin/manage/uploads.json create mode 100644 public/language/en-US/admin/manage/users.json create mode 100644 public/language/en-US/admin/menu.json create mode 100644 public/language/en-US/admin/settings/advanced.json create mode 100644 public/language/en-US/admin/settings/api.json create mode 100644 public/language/en-US/admin/settings/chat.json create mode 100644 public/language/en-US/admin/settings/cookies.json create mode 100644 public/language/en-US/admin/settings/email.json create mode 100644 public/language/en-US/admin/settings/general.json create mode 100644 public/language/en-US/admin/settings/group.json create mode 100644 public/language/en-US/admin/settings/guest.json create mode 100644 public/language/en-US/admin/settings/homepage.json create mode 100644 public/language/en-US/admin/settings/languages.json create mode 100644 public/language/en-US/admin/settings/navigation.json create mode 100644 public/language/en-US/admin/settings/notifications.json create mode 100644 public/language/en-US/admin/settings/pagination.json create mode 100644 public/language/en-US/admin/settings/post.json create mode 100644 public/language/en-US/admin/settings/reputation.json create mode 100644 public/language/en-US/admin/settings/social.json create mode 100644 public/language/en-US/admin/settings/sockets.json create mode 100644 public/language/en-US/admin/settings/sounds.json create mode 100644 public/language/en-US/admin/settings/tags.json create mode 100644 public/language/en-US/admin/settings/uploads.json create mode 100644 public/language/en-US/admin/settings/user.json create mode 100644 public/language/en-US/admin/settings/web-crawler.json create mode 100644 public/language/en-US/category.json create mode 100644 public/language/en-US/email.json create mode 100644 public/language/en-US/error.json create mode 100644 public/language/en-US/flags.json create mode 100644 public/language/en-US/global.json create mode 100644 public/language/en-US/groups.json create mode 100644 public/language/en-US/ip-blacklist.json create mode 100644 public/language/en-US/language.json create mode 100644 public/language/en-US/login.json create mode 100644 public/language/en-US/modules.json create mode 100644 public/language/en-US/notifications.json create mode 100644 public/language/en-US/pages.json create mode 100644 public/language/en-US/post-queue.json create mode 100644 public/language/en-US/recent.json create mode 100644 public/language/en-US/register.json create mode 100644 public/language/en-US/reset_password.json create mode 100644 public/language/en-US/search.json create mode 100644 public/language/en-US/success.json create mode 100644 public/language/en-US/tags.json create mode 100644 public/language/en-US/top.json create mode 100644 public/language/en-US/topic.json create mode 100644 public/language/en-US/unread.json create mode 100644 public/language/en-US/uploads.json create mode 100644 public/language/en-US/user.json create mode 100644 public/language/en-US/users.json create mode 100644 public/language/en-x-pirate/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/en-x-pirate/admin/admin.json create mode 100644 public/language/en-x-pirate/admin/advanced/cache.json create mode 100644 public/language/en-x-pirate/admin/advanced/database.json create mode 100644 public/language/en-x-pirate/admin/advanced/errors.json create mode 100644 public/language/en-x-pirate/admin/advanced/events.json create mode 100644 public/language/en-x-pirate/admin/advanced/logs.json create mode 100644 public/language/en-x-pirate/admin/appearance/customise.json create mode 100644 public/language/en-x-pirate/admin/appearance/skins.json create mode 100644 public/language/en-x-pirate/admin/appearance/themes.json create mode 100644 public/language/en-x-pirate/admin/dashboard.json create mode 100644 public/language/en-x-pirate/admin/development/info.json create mode 100644 public/language/en-x-pirate/admin/development/logger.json create mode 100644 public/language/en-x-pirate/admin/extend/plugins.json create mode 100644 public/language/en-x-pirate/admin/extend/rewards.json create mode 100644 public/language/en-x-pirate/admin/extend/widgets.json create mode 100644 public/language/en-x-pirate/admin/manage/admins-mods.json create mode 100644 public/language/en-x-pirate/admin/manage/categories.json create mode 100644 public/language/en-x-pirate/admin/manage/digest.json create mode 100644 public/language/en-x-pirate/admin/manage/groups.json create mode 100644 public/language/en-x-pirate/admin/manage/privileges.json create mode 100644 public/language/en-x-pirate/admin/manage/registration.json create mode 100644 public/language/en-x-pirate/admin/manage/tags.json create mode 100644 public/language/en-x-pirate/admin/manage/uploads.json create mode 100644 public/language/en-x-pirate/admin/manage/users.json create mode 100644 public/language/en-x-pirate/admin/menu.json create mode 100644 public/language/en-x-pirate/admin/settings/advanced.json create mode 100644 public/language/en-x-pirate/admin/settings/api.json create mode 100644 public/language/en-x-pirate/admin/settings/chat.json create mode 100644 public/language/en-x-pirate/admin/settings/cookies.json create mode 100644 public/language/en-x-pirate/admin/settings/email.json create mode 100644 public/language/en-x-pirate/admin/settings/general.json create mode 100644 public/language/en-x-pirate/admin/settings/group.json create mode 100644 public/language/en-x-pirate/admin/settings/guest.json create mode 100644 public/language/en-x-pirate/admin/settings/homepage.json create mode 100644 public/language/en-x-pirate/admin/settings/languages.json create mode 100644 public/language/en-x-pirate/admin/settings/navigation.json create mode 100644 public/language/en-x-pirate/admin/settings/notifications.json create mode 100644 public/language/en-x-pirate/admin/settings/pagination.json create mode 100644 public/language/en-x-pirate/admin/settings/post.json create mode 100644 public/language/en-x-pirate/admin/settings/reputation.json create mode 100644 public/language/en-x-pirate/admin/settings/social.json create mode 100644 public/language/en-x-pirate/admin/settings/sockets.json create mode 100644 public/language/en-x-pirate/admin/settings/sounds.json create mode 100644 public/language/en-x-pirate/admin/settings/tags.json create mode 100644 public/language/en-x-pirate/admin/settings/uploads.json create mode 100644 public/language/en-x-pirate/admin/settings/user.json create mode 100644 public/language/en-x-pirate/admin/settings/web-crawler.json create mode 100644 public/language/en-x-pirate/category.json create mode 100644 public/language/en-x-pirate/email.json create mode 100644 public/language/en-x-pirate/error.json create mode 100644 public/language/en-x-pirate/flags.json create mode 100644 public/language/en-x-pirate/global.json create mode 100644 public/language/en-x-pirate/groups.json create mode 100644 public/language/en-x-pirate/ip-blacklist.json create mode 100644 public/language/en-x-pirate/language.json create mode 100644 public/language/en-x-pirate/login.json create mode 100644 public/language/en-x-pirate/modules.json create mode 100644 public/language/en-x-pirate/notifications.json create mode 100644 public/language/en-x-pirate/pages.json create mode 100644 public/language/en-x-pirate/post-queue.json create mode 100644 public/language/en-x-pirate/recent.json create mode 100644 public/language/en-x-pirate/register.json create mode 100644 public/language/en-x-pirate/reset_password.json create mode 100644 public/language/en-x-pirate/search.json create mode 100644 public/language/en-x-pirate/success.json create mode 100644 public/language/en-x-pirate/tags.json create mode 100644 public/language/en-x-pirate/top.json create mode 100644 public/language/en-x-pirate/topic.json create mode 100644 public/language/en-x-pirate/unread.json create mode 100644 public/language/en-x-pirate/uploads.json create mode 100644 public/language/en-x-pirate/user.json create mode 100644 public/language/en-x-pirate/users.json create mode 100644 public/language/es/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/es/admin/admin.json create mode 100644 public/language/es/admin/advanced/cache.json create mode 100644 public/language/es/admin/advanced/database.json create mode 100644 public/language/es/admin/advanced/errors.json create mode 100644 public/language/es/admin/advanced/events.json create mode 100644 public/language/es/admin/advanced/logs.json create mode 100644 public/language/es/admin/appearance/customise.json create mode 100644 public/language/es/admin/appearance/skins.json create mode 100644 public/language/es/admin/appearance/themes.json create mode 100644 public/language/es/admin/dashboard.json create mode 100644 public/language/es/admin/development/info.json create mode 100644 public/language/es/admin/development/logger.json create mode 100644 public/language/es/admin/extend/plugins.json create mode 100644 public/language/es/admin/extend/rewards.json create mode 100644 public/language/es/admin/extend/widgets.json create mode 100644 public/language/es/admin/manage/admins-mods.json create mode 100644 public/language/es/admin/manage/categories.json create mode 100644 public/language/es/admin/manage/digest.json create mode 100644 public/language/es/admin/manage/groups.json create mode 100644 public/language/es/admin/manage/privileges.json create mode 100644 public/language/es/admin/manage/registration.json create mode 100644 public/language/es/admin/manage/tags.json create mode 100644 public/language/es/admin/manage/uploads.json create mode 100644 public/language/es/admin/manage/users.json create mode 100644 public/language/es/admin/menu.json create mode 100644 public/language/es/admin/settings/advanced.json create mode 100644 public/language/es/admin/settings/api.json create mode 100644 public/language/es/admin/settings/chat.json create mode 100644 public/language/es/admin/settings/cookies.json create mode 100644 public/language/es/admin/settings/email.json create mode 100644 public/language/es/admin/settings/general.json create mode 100644 public/language/es/admin/settings/group.json create mode 100644 public/language/es/admin/settings/guest.json create mode 100644 public/language/es/admin/settings/homepage.json create mode 100644 public/language/es/admin/settings/languages.json create mode 100644 public/language/es/admin/settings/navigation.json create mode 100644 public/language/es/admin/settings/notifications.json create mode 100644 public/language/es/admin/settings/pagination.json create mode 100644 public/language/es/admin/settings/post.json create mode 100644 public/language/es/admin/settings/reputation.json create mode 100644 public/language/es/admin/settings/social.json create mode 100644 public/language/es/admin/settings/sockets.json create mode 100644 public/language/es/admin/settings/sounds.json create mode 100644 public/language/es/admin/settings/tags.json create mode 100644 public/language/es/admin/settings/uploads.json create mode 100644 public/language/es/admin/settings/user.json create mode 100644 public/language/es/admin/settings/web-crawler.json create mode 100644 public/language/es/category.json create mode 100644 public/language/es/email.json create mode 100644 public/language/es/error.json create mode 100644 public/language/es/flags.json create mode 100644 public/language/es/global.json create mode 100644 public/language/es/groups.json create mode 100644 public/language/es/ip-blacklist.json create mode 100644 public/language/es/language.json create mode 100644 public/language/es/login.json create mode 100644 public/language/es/modules.json create mode 100644 public/language/es/notifications.json create mode 100644 public/language/es/pages.json create mode 100644 public/language/es/post-queue.json create mode 100644 public/language/es/recent.json create mode 100644 public/language/es/register.json create mode 100644 public/language/es/reset_password.json create mode 100644 public/language/es/search.json create mode 100644 public/language/es/success.json create mode 100644 public/language/es/tags.json create mode 100644 public/language/es/top.json create mode 100644 public/language/es/topic.json create mode 100644 public/language/es/unread.json create mode 100644 public/language/es/uploads.json create mode 100644 public/language/es/user.json create mode 100644 public/language/es/users.json create mode 100644 public/language/et/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/et/admin/admin.json create mode 100644 public/language/et/admin/advanced/cache.json create mode 100644 public/language/et/admin/advanced/database.json create mode 100644 public/language/et/admin/advanced/errors.json create mode 100644 public/language/et/admin/advanced/events.json create mode 100644 public/language/et/admin/advanced/logs.json create mode 100644 public/language/et/admin/appearance/customise.json create mode 100644 public/language/et/admin/appearance/skins.json create mode 100644 public/language/et/admin/appearance/themes.json create mode 100644 public/language/et/admin/dashboard.json create mode 100644 public/language/et/admin/development/info.json create mode 100644 public/language/et/admin/development/logger.json create mode 100644 public/language/et/admin/extend/plugins.json create mode 100644 public/language/et/admin/extend/rewards.json create mode 100644 public/language/et/admin/extend/widgets.json create mode 100644 public/language/et/admin/manage/admins-mods.json create mode 100644 public/language/et/admin/manage/categories.json create mode 100644 public/language/et/admin/manage/digest.json create mode 100644 public/language/et/admin/manage/groups.json create mode 100644 public/language/et/admin/manage/privileges.json create mode 100644 public/language/et/admin/manage/registration.json create mode 100644 public/language/et/admin/manage/tags.json create mode 100644 public/language/et/admin/manage/uploads.json create mode 100644 public/language/et/admin/manage/users.json create mode 100644 public/language/et/admin/menu.json create mode 100644 public/language/et/admin/settings/advanced.json create mode 100644 public/language/et/admin/settings/api.json create mode 100644 public/language/et/admin/settings/chat.json create mode 100644 public/language/et/admin/settings/cookies.json create mode 100644 public/language/et/admin/settings/email.json create mode 100644 public/language/et/admin/settings/general.json create mode 100644 public/language/et/admin/settings/group.json create mode 100644 public/language/et/admin/settings/guest.json create mode 100644 public/language/et/admin/settings/homepage.json create mode 100644 public/language/et/admin/settings/languages.json create mode 100644 public/language/et/admin/settings/navigation.json create mode 100644 public/language/et/admin/settings/notifications.json create mode 100644 public/language/et/admin/settings/pagination.json create mode 100644 public/language/et/admin/settings/post.json create mode 100644 public/language/et/admin/settings/reputation.json create mode 100644 public/language/et/admin/settings/social.json create mode 100644 public/language/et/admin/settings/sockets.json create mode 100644 public/language/et/admin/settings/sounds.json create mode 100644 public/language/et/admin/settings/tags.json create mode 100644 public/language/et/admin/settings/uploads.json create mode 100644 public/language/et/admin/settings/user.json create mode 100644 public/language/et/admin/settings/web-crawler.json create mode 100644 public/language/et/category.json create mode 100644 public/language/et/email.json create mode 100644 public/language/et/error.json create mode 100644 public/language/et/flags.json create mode 100644 public/language/et/global.json create mode 100644 public/language/et/groups.json create mode 100644 public/language/et/ip-blacklist.json create mode 100644 public/language/et/language.json create mode 100644 public/language/et/login.json create mode 100644 public/language/et/modules.json create mode 100644 public/language/et/notifications.json create mode 100644 public/language/et/pages.json create mode 100644 public/language/et/post-queue.json create mode 100644 public/language/et/recent.json create mode 100644 public/language/et/register.json create mode 100644 public/language/et/reset_password.json create mode 100644 public/language/et/search.json create mode 100644 public/language/et/success.json create mode 100644 public/language/et/tags.json create mode 100644 public/language/et/top.json create mode 100644 public/language/et/topic.json create mode 100644 public/language/et/unread.json create mode 100644 public/language/et/uploads.json create mode 100644 public/language/et/user.json create mode 100644 public/language/et/users.json create mode 100644 public/language/fa-IR/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/fa-IR/admin/admin.json create mode 100644 public/language/fa-IR/admin/advanced/cache.json create mode 100644 public/language/fa-IR/admin/advanced/database.json create mode 100644 public/language/fa-IR/admin/advanced/errors.json create mode 100644 public/language/fa-IR/admin/advanced/events.json create mode 100644 public/language/fa-IR/admin/advanced/logs.json create mode 100644 public/language/fa-IR/admin/appearance/customise.json create mode 100644 public/language/fa-IR/admin/appearance/skins.json create mode 100644 public/language/fa-IR/admin/appearance/themes.json create mode 100644 public/language/fa-IR/admin/dashboard.json create mode 100644 public/language/fa-IR/admin/development/info.json create mode 100644 public/language/fa-IR/admin/development/logger.json create mode 100644 public/language/fa-IR/admin/extend/plugins.json create mode 100644 public/language/fa-IR/admin/extend/rewards.json create mode 100644 public/language/fa-IR/admin/extend/widgets.json create mode 100644 public/language/fa-IR/admin/manage/admins-mods.json create mode 100644 public/language/fa-IR/admin/manage/categories.json create mode 100644 public/language/fa-IR/admin/manage/digest.json create mode 100644 public/language/fa-IR/admin/manage/groups.json create mode 100644 public/language/fa-IR/admin/manage/privileges.json create mode 100644 public/language/fa-IR/admin/manage/registration.json create mode 100644 public/language/fa-IR/admin/manage/tags.json create mode 100644 public/language/fa-IR/admin/manage/uploads.json create mode 100644 public/language/fa-IR/admin/manage/users.json create mode 100644 public/language/fa-IR/admin/menu.json create mode 100644 public/language/fa-IR/admin/settings/advanced.json create mode 100644 public/language/fa-IR/admin/settings/api.json create mode 100644 public/language/fa-IR/admin/settings/chat.json create mode 100644 public/language/fa-IR/admin/settings/cookies.json create mode 100644 public/language/fa-IR/admin/settings/email.json create mode 100644 public/language/fa-IR/admin/settings/general.json create mode 100644 public/language/fa-IR/admin/settings/group.json create mode 100644 public/language/fa-IR/admin/settings/guest.json create mode 100644 public/language/fa-IR/admin/settings/homepage.json create mode 100644 public/language/fa-IR/admin/settings/languages.json create mode 100644 public/language/fa-IR/admin/settings/navigation.json create mode 100644 public/language/fa-IR/admin/settings/notifications.json create mode 100644 public/language/fa-IR/admin/settings/pagination.json create mode 100644 public/language/fa-IR/admin/settings/post.json create mode 100644 public/language/fa-IR/admin/settings/reputation.json create mode 100644 public/language/fa-IR/admin/settings/social.json create mode 100644 public/language/fa-IR/admin/settings/sockets.json create mode 100644 public/language/fa-IR/admin/settings/sounds.json create mode 100644 public/language/fa-IR/admin/settings/tags.json create mode 100644 public/language/fa-IR/admin/settings/uploads.json create mode 100644 public/language/fa-IR/admin/settings/user.json create mode 100644 public/language/fa-IR/admin/settings/web-crawler.json create mode 100644 public/language/fa-IR/category.json create mode 100644 public/language/fa-IR/email.json create mode 100644 public/language/fa-IR/error.json create mode 100644 public/language/fa-IR/flags.json create mode 100644 public/language/fa-IR/global.json create mode 100644 public/language/fa-IR/groups.json create mode 100644 public/language/fa-IR/ip-blacklist.json create mode 100644 public/language/fa-IR/language.json create mode 100644 public/language/fa-IR/login.json create mode 100644 public/language/fa-IR/modules.json create mode 100644 public/language/fa-IR/notifications.json create mode 100644 public/language/fa-IR/pages.json create mode 100644 public/language/fa-IR/post-queue.json create mode 100644 public/language/fa-IR/recent.json create mode 100644 public/language/fa-IR/register.json create mode 100644 public/language/fa-IR/reset_password.json create mode 100644 public/language/fa-IR/search.json create mode 100644 public/language/fa-IR/success.json create mode 100644 public/language/fa-IR/tags.json create mode 100644 public/language/fa-IR/top.json create mode 100644 public/language/fa-IR/topic.json create mode 100644 public/language/fa-IR/unread.json create mode 100644 public/language/fa-IR/uploads.json create mode 100644 public/language/fa-IR/user.json create mode 100644 public/language/fa-IR/users.json create mode 100644 public/language/fi/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/fi/admin/admin.json create mode 100644 public/language/fi/admin/advanced/cache.json create mode 100644 public/language/fi/admin/advanced/database.json create mode 100644 public/language/fi/admin/advanced/errors.json create mode 100644 public/language/fi/admin/advanced/events.json create mode 100644 public/language/fi/admin/advanced/logs.json create mode 100644 public/language/fi/admin/appearance/customise.json create mode 100644 public/language/fi/admin/appearance/skins.json create mode 100644 public/language/fi/admin/appearance/themes.json create mode 100644 public/language/fi/admin/dashboard.json create mode 100644 public/language/fi/admin/development/info.json create mode 100644 public/language/fi/admin/development/logger.json create mode 100644 public/language/fi/admin/extend/plugins.json create mode 100644 public/language/fi/admin/extend/rewards.json create mode 100644 public/language/fi/admin/extend/widgets.json create mode 100644 public/language/fi/admin/manage/admins-mods.json create mode 100644 public/language/fi/admin/manage/categories.json create mode 100644 public/language/fi/admin/manage/digest.json create mode 100644 public/language/fi/admin/manage/groups.json create mode 100644 public/language/fi/admin/manage/privileges.json create mode 100644 public/language/fi/admin/manage/registration.json create mode 100644 public/language/fi/admin/manage/tags.json create mode 100644 public/language/fi/admin/manage/uploads.json create mode 100644 public/language/fi/admin/manage/users.json create mode 100644 public/language/fi/admin/menu.json create mode 100644 public/language/fi/admin/settings/advanced.json create mode 100644 public/language/fi/admin/settings/api.json create mode 100644 public/language/fi/admin/settings/chat.json create mode 100644 public/language/fi/admin/settings/cookies.json create mode 100644 public/language/fi/admin/settings/email.json create mode 100644 public/language/fi/admin/settings/general.json create mode 100644 public/language/fi/admin/settings/group.json create mode 100644 public/language/fi/admin/settings/guest.json create mode 100644 public/language/fi/admin/settings/homepage.json create mode 100644 public/language/fi/admin/settings/languages.json create mode 100644 public/language/fi/admin/settings/navigation.json create mode 100644 public/language/fi/admin/settings/notifications.json create mode 100644 public/language/fi/admin/settings/pagination.json create mode 100644 public/language/fi/admin/settings/post.json create mode 100644 public/language/fi/admin/settings/reputation.json create mode 100644 public/language/fi/admin/settings/social.json create mode 100644 public/language/fi/admin/settings/sockets.json create mode 100644 public/language/fi/admin/settings/sounds.json create mode 100644 public/language/fi/admin/settings/tags.json create mode 100644 public/language/fi/admin/settings/uploads.json create mode 100644 public/language/fi/admin/settings/user.json create mode 100644 public/language/fi/admin/settings/web-crawler.json create mode 100644 public/language/fi/category.json create mode 100644 public/language/fi/email.json create mode 100644 public/language/fi/error.json create mode 100644 public/language/fi/flags.json create mode 100644 public/language/fi/global.json create mode 100644 public/language/fi/groups.json create mode 100644 public/language/fi/ip-blacklist.json create mode 100644 public/language/fi/language.json create mode 100644 public/language/fi/login.json create mode 100644 public/language/fi/modules.json create mode 100644 public/language/fi/notifications.json create mode 100644 public/language/fi/pages.json create mode 100644 public/language/fi/post-queue.json create mode 100644 public/language/fi/recent.json create mode 100644 public/language/fi/register.json create mode 100644 public/language/fi/reset_password.json create mode 100644 public/language/fi/search.json create mode 100644 public/language/fi/success.json create mode 100644 public/language/fi/tags.json create mode 100644 public/language/fi/top.json create mode 100644 public/language/fi/topic.json create mode 100644 public/language/fi/unread.json create mode 100644 public/language/fi/uploads.json create mode 100644 public/language/fi/user.json create mode 100644 public/language/fi/users.json create mode 100644 public/language/fr/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/fr/admin/admin.json create mode 100644 public/language/fr/admin/advanced/cache.json create mode 100644 public/language/fr/admin/advanced/database.json create mode 100644 public/language/fr/admin/advanced/errors.json create mode 100644 public/language/fr/admin/advanced/events.json create mode 100644 public/language/fr/admin/advanced/logs.json create mode 100644 public/language/fr/admin/appearance/customise.json create mode 100644 public/language/fr/admin/appearance/skins.json create mode 100644 public/language/fr/admin/appearance/themes.json create mode 100644 public/language/fr/admin/dashboard.json create mode 100644 public/language/fr/admin/development/info.json create mode 100644 public/language/fr/admin/development/logger.json create mode 100644 public/language/fr/admin/extend/plugins.json create mode 100644 public/language/fr/admin/extend/rewards.json create mode 100644 public/language/fr/admin/extend/widgets.json create mode 100644 public/language/fr/admin/manage/admins-mods.json create mode 100644 public/language/fr/admin/manage/categories.json create mode 100644 public/language/fr/admin/manage/digest.json create mode 100644 public/language/fr/admin/manage/groups.json create mode 100644 public/language/fr/admin/manage/privileges.json create mode 100644 public/language/fr/admin/manage/registration.json create mode 100644 public/language/fr/admin/manage/tags.json create mode 100644 public/language/fr/admin/manage/uploads.json create mode 100644 public/language/fr/admin/manage/users.json create mode 100644 public/language/fr/admin/menu.json create mode 100644 public/language/fr/admin/settings/advanced.json create mode 100644 public/language/fr/admin/settings/api.json create mode 100644 public/language/fr/admin/settings/chat.json create mode 100644 public/language/fr/admin/settings/cookies.json create mode 100644 public/language/fr/admin/settings/email.json create mode 100644 public/language/fr/admin/settings/general.json create mode 100644 public/language/fr/admin/settings/group.json create mode 100644 public/language/fr/admin/settings/guest.json create mode 100644 public/language/fr/admin/settings/homepage.json create mode 100644 public/language/fr/admin/settings/languages.json create mode 100644 public/language/fr/admin/settings/navigation.json create mode 100644 public/language/fr/admin/settings/notifications.json create mode 100644 public/language/fr/admin/settings/pagination.json create mode 100644 public/language/fr/admin/settings/post.json create mode 100644 public/language/fr/admin/settings/reputation.json create mode 100644 public/language/fr/admin/settings/social.json create mode 100644 public/language/fr/admin/settings/sockets.json create mode 100644 public/language/fr/admin/settings/sounds.json create mode 100644 public/language/fr/admin/settings/tags.json create mode 100644 public/language/fr/admin/settings/uploads.json create mode 100644 public/language/fr/admin/settings/user.json create mode 100644 public/language/fr/admin/settings/web-crawler.json create mode 100644 public/language/fr/category.json create mode 100644 public/language/fr/email.json create mode 100644 public/language/fr/error.json create mode 100644 public/language/fr/flags.json create mode 100644 public/language/fr/global.json create mode 100644 public/language/fr/groups.json create mode 100644 public/language/fr/ip-blacklist.json create mode 100644 public/language/fr/language.json create mode 100644 public/language/fr/login.json create mode 100644 public/language/fr/modules.json create mode 100644 public/language/fr/notifications.json create mode 100644 public/language/fr/pages.json create mode 100644 public/language/fr/post-queue.json create mode 100644 public/language/fr/recent.json create mode 100644 public/language/fr/register.json create mode 100644 public/language/fr/reset_password.json create mode 100644 public/language/fr/search.json create mode 100644 public/language/fr/success.json create mode 100644 public/language/fr/tags.json create mode 100644 public/language/fr/top.json create mode 100644 public/language/fr/topic.json create mode 100644 public/language/fr/unread.json create mode 100644 public/language/fr/uploads.json create mode 100644 public/language/fr/user.json create mode 100644 public/language/fr/users.json create mode 100644 public/language/gl/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/gl/admin/admin.json create mode 100644 public/language/gl/admin/advanced/cache.json create mode 100644 public/language/gl/admin/advanced/database.json create mode 100644 public/language/gl/admin/advanced/errors.json create mode 100644 public/language/gl/admin/advanced/events.json create mode 100644 public/language/gl/admin/advanced/logs.json create mode 100644 public/language/gl/admin/appearance/customise.json create mode 100644 public/language/gl/admin/appearance/skins.json create mode 100644 public/language/gl/admin/appearance/themes.json create mode 100644 public/language/gl/admin/dashboard.json create mode 100644 public/language/gl/admin/development/info.json create mode 100644 public/language/gl/admin/development/logger.json create mode 100644 public/language/gl/admin/extend/plugins.json create mode 100644 public/language/gl/admin/extend/rewards.json create mode 100644 public/language/gl/admin/extend/widgets.json create mode 100644 public/language/gl/admin/manage/admins-mods.json create mode 100644 public/language/gl/admin/manage/categories.json create mode 100644 public/language/gl/admin/manage/digest.json create mode 100644 public/language/gl/admin/manage/groups.json create mode 100644 public/language/gl/admin/manage/privileges.json create mode 100644 public/language/gl/admin/manage/registration.json create mode 100644 public/language/gl/admin/manage/tags.json create mode 100644 public/language/gl/admin/manage/uploads.json create mode 100644 public/language/gl/admin/manage/users.json create mode 100644 public/language/gl/admin/menu.json create mode 100644 public/language/gl/admin/settings/advanced.json create mode 100644 public/language/gl/admin/settings/api.json create mode 100644 public/language/gl/admin/settings/chat.json create mode 100644 public/language/gl/admin/settings/cookies.json create mode 100644 public/language/gl/admin/settings/email.json create mode 100644 public/language/gl/admin/settings/general.json create mode 100644 public/language/gl/admin/settings/group.json create mode 100644 public/language/gl/admin/settings/guest.json create mode 100644 public/language/gl/admin/settings/homepage.json create mode 100644 public/language/gl/admin/settings/languages.json create mode 100644 public/language/gl/admin/settings/navigation.json create mode 100644 public/language/gl/admin/settings/notifications.json create mode 100644 public/language/gl/admin/settings/pagination.json create mode 100644 public/language/gl/admin/settings/post.json create mode 100644 public/language/gl/admin/settings/reputation.json create mode 100644 public/language/gl/admin/settings/social.json create mode 100644 public/language/gl/admin/settings/sockets.json create mode 100644 public/language/gl/admin/settings/sounds.json create mode 100644 public/language/gl/admin/settings/tags.json create mode 100644 public/language/gl/admin/settings/uploads.json create mode 100644 public/language/gl/admin/settings/user.json create mode 100644 public/language/gl/admin/settings/web-crawler.json create mode 100644 public/language/gl/category.json create mode 100644 public/language/gl/email.json create mode 100644 public/language/gl/error.json create mode 100644 public/language/gl/flags.json create mode 100644 public/language/gl/global.json create mode 100644 public/language/gl/groups.json create mode 100644 public/language/gl/ip-blacklist.json create mode 100644 public/language/gl/language.json create mode 100644 public/language/gl/login.json create mode 100644 public/language/gl/modules.json create mode 100644 public/language/gl/notifications.json create mode 100644 public/language/gl/pages.json create mode 100644 public/language/gl/post-queue.json create mode 100644 public/language/gl/recent.json create mode 100644 public/language/gl/register.json create mode 100644 public/language/gl/reset_password.json create mode 100644 public/language/gl/search.json create mode 100644 public/language/gl/success.json create mode 100644 public/language/gl/tags.json create mode 100644 public/language/gl/top.json create mode 100644 public/language/gl/topic.json create mode 100644 public/language/gl/unread.json create mode 100644 public/language/gl/uploads.json create mode 100644 public/language/gl/user.json create mode 100644 public/language/gl/users.json create mode 100644 public/language/he/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/he/admin/admin.json create mode 100644 public/language/he/admin/advanced/cache.json create mode 100644 public/language/he/admin/advanced/database.json create mode 100644 public/language/he/admin/advanced/errors.json create mode 100644 public/language/he/admin/advanced/events.json create mode 100644 public/language/he/admin/advanced/logs.json create mode 100644 public/language/he/admin/appearance/customise.json create mode 100644 public/language/he/admin/appearance/skins.json create mode 100644 public/language/he/admin/appearance/themes.json create mode 100644 public/language/he/admin/dashboard.json create mode 100644 public/language/he/admin/development/info.json create mode 100644 public/language/he/admin/development/logger.json create mode 100644 public/language/he/admin/extend/plugins.json create mode 100644 public/language/he/admin/extend/rewards.json create mode 100644 public/language/he/admin/extend/widgets.json create mode 100644 public/language/he/admin/manage/admins-mods.json create mode 100644 public/language/he/admin/manage/categories.json create mode 100644 public/language/he/admin/manage/digest.json create mode 100644 public/language/he/admin/manage/groups.json create mode 100644 public/language/he/admin/manage/privileges.json create mode 100644 public/language/he/admin/manage/registration.json create mode 100644 public/language/he/admin/manage/tags.json create mode 100644 public/language/he/admin/manage/uploads.json create mode 100644 public/language/he/admin/manage/users.json create mode 100644 public/language/he/admin/menu.json create mode 100644 public/language/he/admin/settings/advanced.json create mode 100644 public/language/he/admin/settings/api.json create mode 100644 public/language/he/admin/settings/chat.json create mode 100644 public/language/he/admin/settings/cookies.json create mode 100644 public/language/he/admin/settings/email.json create mode 100644 public/language/he/admin/settings/general.json create mode 100644 public/language/he/admin/settings/group.json create mode 100644 public/language/he/admin/settings/guest.json create mode 100644 public/language/he/admin/settings/homepage.json create mode 100644 public/language/he/admin/settings/languages.json create mode 100644 public/language/he/admin/settings/navigation.json create mode 100644 public/language/he/admin/settings/notifications.json create mode 100644 public/language/he/admin/settings/pagination.json create mode 100644 public/language/he/admin/settings/post.json create mode 100644 public/language/he/admin/settings/reputation.json create mode 100644 public/language/he/admin/settings/social.json create mode 100644 public/language/he/admin/settings/sockets.json create mode 100644 public/language/he/admin/settings/sounds.json create mode 100644 public/language/he/admin/settings/tags.json create mode 100644 public/language/he/admin/settings/uploads.json create mode 100644 public/language/he/admin/settings/user.json create mode 100644 public/language/he/admin/settings/web-crawler.json create mode 100644 public/language/he/category.json create mode 100644 public/language/he/email.json create mode 100644 public/language/he/error.json create mode 100644 public/language/he/flags.json create mode 100644 public/language/he/global.json create mode 100644 public/language/he/groups.json create mode 100644 public/language/he/ip-blacklist.json create mode 100644 public/language/he/language.json create mode 100644 public/language/he/login.json create mode 100644 public/language/he/modules.json create mode 100644 public/language/he/notifications.json create mode 100644 public/language/he/pages.json create mode 100644 public/language/he/post-queue.json create mode 100644 public/language/he/recent.json create mode 100644 public/language/he/register.json create mode 100644 public/language/he/reset_password.json create mode 100644 public/language/he/search.json create mode 100644 public/language/he/success.json create mode 100644 public/language/he/tags.json create mode 100644 public/language/he/top.json create mode 100644 public/language/he/topic.json create mode 100644 public/language/he/unread.json create mode 100644 public/language/he/uploads.json create mode 100644 public/language/he/user.json create mode 100644 public/language/he/users.json create mode 100644 public/language/hr/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/hr/admin/admin.json create mode 100644 public/language/hr/admin/advanced/cache.json create mode 100644 public/language/hr/admin/advanced/database.json create mode 100644 public/language/hr/admin/advanced/errors.json create mode 100644 public/language/hr/admin/advanced/events.json create mode 100644 public/language/hr/admin/advanced/logs.json create mode 100644 public/language/hr/admin/appearance/customise.json create mode 100644 public/language/hr/admin/appearance/skins.json create mode 100644 public/language/hr/admin/appearance/themes.json create mode 100644 public/language/hr/admin/dashboard.json create mode 100644 public/language/hr/admin/development/info.json create mode 100644 public/language/hr/admin/development/logger.json create mode 100644 public/language/hr/admin/extend/plugins.json create mode 100644 public/language/hr/admin/extend/rewards.json create mode 100644 public/language/hr/admin/extend/widgets.json create mode 100644 public/language/hr/admin/manage/admins-mods.json create mode 100644 public/language/hr/admin/manage/categories.json create mode 100644 public/language/hr/admin/manage/digest.json create mode 100644 public/language/hr/admin/manage/groups.json create mode 100644 public/language/hr/admin/manage/privileges.json create mode 100644 public/language/hr/admin/manage/registration.json create mode 100644 public/language/hr/admin/manage/tags.json create mode 100644 public/language/hr/admin/manage/uploads.json create mode 100644 public/language/hr/admin/manage/users.json create mode 100644 public/language/hr/admin/menu.json create mode 100644 public/language/hr/admin/settings/advanced.json create mode 100644 public/language/hr/admin/settings/api.json create mode 100644 public/language/hr/admin/settings/chat.json create mode 100644 public/language/hr/admin/settings/cookies.json create mode 100644 public/language/hr/admin/settings/email.json create mode 100644 public/language/hr/admin/settings/general.json create mode 100644 public/language/hr/admin/settings/group.json create mode 100644 public/language/hr/admin/settings/guest.json create mode 100644 public/language/hr/admin/settings/homepage.json create mode 100644 public/language/hr/admin/settings/languages.json create mode 100644 public/language/hr/admin/settings/navigation.json create mode 100644 public/language/hr/admin/settings/notifications.json create mode 100644 public/language/hr/admin/settings/pagination.json create mode 100644 public/language/hr/admin/settings/post.json create mode 100644 public/language/hr/admin/settings/reputation.json create mode 100644 public/language/hr/admin/settings/social.json create mode 100644 public/language/hr/admin/settings/sockets.json create mode 100644 public/language/hr/admin/settings/sounds.json create mode 100644 public/language/hr/admin/settings/tags.json create mode 100644 public/language/hr/admin/settings/uploads.json create mode 100644 public/language/hr/admin/settings/user.json create mode 100644 public/language/hr/admin/settings/web-crawler.json create mode 100644 public/language/hr/category.json create mode 100644 public/language/hr/email.json create mode 100644 public/language/hr/error.json create mode 100644 public/language/hr/flags.json create mode 100644 public/language/hr/global.json create mode 100644 public/language/hr/groups.json create mode 100644 public/language/hr/ip-blacklist.json create mode 100644 public/language/hr/language.json create mode 100644 public/language/hr/login.json create mode 100644 public/language/hr/modules.json create mode 100644 public/language/hr/notifications.json create mode 100644 public/language/hr/pages.json create mode 100644 public/language/hr/post-queue.json create mode 100644 public/language/hr/recent.json create mode 100644 public/language/hr/register.json create mode 100644 public/language/hr/reset_password.json create mode 100644 public/language/hr/search.json create mode 100644 public/language/hr/success.json create mode 100644 public/language/hr/tags.json create mode 100644 public/language/hr/top.json create mode 100644 public/language/hr/topic.json create mode 100644 public/language/hr/unread.json create mode 100644 public/language/hr/uploads.json create mode 100644 public/language/hr/user.json create mode 100644 public/language/hr/users.json create mode 100644 public/language/hu/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/hu/admin/admin.json create mode 100644 public/language/hu/admin/advanced/cache.json create mode 100644 public/language/hu/admin/advanced/database.json create mode 100644 public/language/hu/admin/advanced/errors.json create mode 100644 public/language/hu/admin/advanced/events.json create mode 100644 public/language/hu/admin/advanced/logs.json create mode 100644 public/language/hu/admin/appearance/customise.json create mode 100644 public/language/hu/admin/appearance/skins.json create mode 100644 public/language/hu/admin/appearance/themes.json create mode 100644 public/language/hu/admin/dashboard.json create mode 100644 public/language/hu/admin/development/info.json create mode 100644 public/language/hu/admin/development/logger.json create mode 100644 public/language/hu/admin/extend/plugins.json create mode 100644 public/language/hu/admin/extend/rewards.json create mode 100644 public/language/hu/admin/extend/widgets.json create mode 100644 public/language/hu/admin/manage/admins-mods.json create mode 100644 public/language/hu/admin/manage/categories.json create mode 100644 public/language/hu/admin/manage/digest.json create mode 100644 public/language/hu/admin/manage/groups.json create mode 100644 public/language/hu/admin/manage/privileges.json create mode 100644 public/language/hu/admin/manage/registration.json create mode 100644 public/language/hu/admin/manage/tags.json create mode 100644 public/language/hu/admin/manage/uploads.json create mode 100644 public/language/hu/admin/manage/users.json create mode 100644 public/language/hu/admin/menu.json create mode 100644 public/language/hu/admin/settings/advanced.json create mode 100644 public/language/hu/admin/settings/api.json create mode 100644 public/language/hu/admin/settings/chat.json create mode 100644 public/language/hu/admin/settings/cookies.json create mode 100644 public/language/hu/admin/settings/email.json create mode 100644 public/language/hu/admin/settings/general.json create mode 100644 public/language/hu/admin/settings/group.json create mode 100644 public/language/hu/admin/settings/guest.json create mode 100644 public/language/hu/admin/settings/homepage.json create mode 100644 public/language/hu/admin/settings/languages.json create mode 100644 public/language/hu/admin/settings/navigation.json create mode 100644 public/language/hu/admin/settings/notifications.json create mode 100644 public/language/hu/admin/settings/pagination.json create mode 100644 public/language/hu/admin/settings/post.json create mode 100644 public/language/hu/admin/settings/reputation.json create mode 100644 public/language/hu/admin/settings/social.json create mode 100644 public/language/hu/admin/settings/sockets.json create mode 100644 public/language/hu/admin/settings/sounds.json create mode 100644 public/language/hu/admin/settings/tags.json create mode 100644 public/language/hu/admin/settings/uploads.json create mode 100644 public/language/hu/admin/settings/user.json create mode 100644 public/language/hu/admin/settings/web-crawler.json create mode 100644 public/language/hu/category.json create mode 100644 public/language/hu/email.json create mode 100644 public/language/hu/error.json create mode 100644 public/language/hu/flags.json create mode 100644 public/language/hu/global.json create mode 100644 public/language/hu/groups.json create mode 100644 public/language/hu/ip-blacklist.json create mode 100644 public/language/hu/language.json create mode 100644 public/language/hu/login.json create mode 100644 public/language/hu/modules.json create mode 100644 public/language/hu/notifications.json create mode 100644 public/language/hu/pages.json create mode 100644 public/language/hu/post-queue.json create mode 100644 public/language/hu/recent.json create mode 100644 public/language/hu/register.json create mode 100644 public/language/hu/reset_password.json create mode 100644 public/language/hu/search.json create mode 100644 public/language/hu/success.json create mode 100644 public/language/hu/tags.json create mode 100644 public/language/hu/top.json create mode 100644 public/language/hu/topic.json create mode 100644 public/language/hu/unread.json create mode 100644 public/language/hu/uploads.json create mode 100644 public/language/hu/user.json create mode 100644 public/language/hu/users.json create mode 100644 public/language/hy/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/hy/admin/admin.json create mode 100644 public/language/hy/admin/advanced/cache.json create mode 100644 public/language/hy/admin/advanced/database.json create mode 100644 public/language/hy/admin/advanced/errors.json create mode 100644 public/language/hy/admin/advanced/events.json create mode 100644 public/language/hy/admin/advanced/logs.json create mode 100644 public/language/hy/admin/appearance/customise.json create mode 100644 public/language/hy/admin/appearance/skins.json create mode 100644 public/language/hy/admin/appearance/themes.json create mode 100644 public/language/hy/admin/dashboard.json create mode 100644 public/language/hy/admin/development/info.json create mode 100644 public/language/hy/admin/development/logger.json create mode 100644 public/language/hy/admin/extend/plugins.json create mode 100644 public/language/hy/admin/extend/rewards.json create mode 100644 public/language/hy/admin/extend/widgets.json create mode 100644 public/language/hy/admin/manage/admins-mods.json create mode 100644 public/language/hy/admin/manage/categories.json create mode 100644 public/language/hy/admin/manage/digest.json create mode 100644 public/language/hy/admin/manage/groups.json create mode 100644 public/language/hy/admin/manage/privileges.json create mode 100644 public/language/hy/admin/manage/registration.json create mode 100644 public/language/hy/admin/manage/tags.json create mode 100644 public/language/hy/admin/manage/uploads.json create mode 100644 public/language/hy/admin/manage/users.json create mode 100644 public/language/hy/admin/menu.json create mode 100644 public/language/hy/admin/settings/advanced.json create mode 100644 public/language/hy/admin/settings/api.json create mode 100644 public/language/hy/admin/settings/chat.json create mode 100644 public/language/hy/admin/settings/cookies.json create mode 100644 public/language/hy/admin/settings/email.json create mode 100644 public/language/hy/admin/settings/general.json create mode 100644 public/language/hy/admin/settings/group.json create mode 100644 public/language/hy/admin/settings/guest.json create mode 100644 public/language/hy/admin/settings/homepage.json create mode 100644 public/language/hy/admin/settings/languages.json create mode 100644 public/language/hy/admin/settings/navigation.json create mode 100644 public/language/hy/admin/settings/notifications.json create mode 100644 public/language/hy/admin/settings/pagination.json create mode 100644 public/language/hy/admin/settings/post.json create mode 100644 public/language/hy/admin/settings/reputation.json create mode 100644 public/language/hy/admin/settings/social.json create mode 100644 public/language/hy/admin/settings/sockets.json create mode 100644 public/language/hy/admin/settings/sounds.json create mode 100644 public/language/hy/admin/settings/tags.json create mode 100644 public/language/hy/admin/settings/uploads.json create mode 100644 public/language/hy/admin/settings/user.json create mode 100644 public/language/hy/admin/settings/web-crawler.json create mode 100644 public/language/hy/category.json create mode 100644 public/language/hy/email.json create mode 100644 public/language/hy/error.json create mode 100644 public/language/hy/flags.json create mode 100644 public/language/hy/global.json create mode 100644 public/language/hy/groups.json create mode 100644 public/language/hy/ip-blacklist.json create mode 100644 public/language/hy/language.json create mode 100644 public/language/hy/login.json create mode 100644 public/language/hy/modules.json create mode 100644 public/language/hy/notifications.json create mode 100644 public/language/hy/pages.json create mode 100644 public/language/hy/post-queue.json create mode 100644 public/language/hy/recent.json create mode 100644 public/language/hy/register.json create mode 100644 public/language/hy/reset_password.json create mode 100644 public/language/hy/search.json create mode 100644 public/language/hy/success.json create mode 100644 public/language/hy/tags.json create mode 100644 public/language/hy/top.json create mode 100644 public/language/hy/topic.json create mode 100644 public/language/hy/unread.json create mode 100644 public/language/hy/uploads.json create mode 100644 public/language/hy/user.json create mode 100644 public/language/hy/users.json create mode 100644 public/language/id/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/id/admin/admin.json create mode 100644 public/language/id/admin/advanced/cache.json create mode 100644 public/language/id/admin/advanced/database.json create mode 100644 public/language/id/admin/advanced/errors.json create mode 100644 public/language/id/admin/advanced/events.json create mode 100644 public/language/id/admin/advanced/logs.json create mode 100644 public/language/id/admin/appearance/customise.json create mode 100644 public/language/id/admin/appearance/skins.json create mode 100644 public/language/id/admin/appearance/themes.json create mode 100644 public/language/id/admin/dashboard.json create mode 100644 public/language/id/admin/development/info.json create mode 100644 public/language/id/admin/development/logger.json create mode 100644 public/language/id/admin/extend/plugins.json create mode 100644 public/language/id/admin/extend/rewards.json create mode 100644 public/language/id/admin/extend/widgets.json create mode 100644 public/language/id/admin/manage/admins-mods.json create mode 100644 public/language/id/admin/manage/categories.json create mode 100644 public/language/id/admin/manage/digest.json create mode 100644 public/language/id/admin/manage/groups.json create mode 100644 public/language/id/admin/manage/privileges.json create mode 100644 public/language/id/admin/manage/registration.json create mode 100644 public/language/id/admin/manage/tags.json create mode 100644 public/language/id/admin/manage/uploads.json create mode 100644 public/language/id/admin/manage/users.json create mode 100644 public/language/id/admin/menu.json create mode 100644 public/language/id/admin/settings/advanced.json create mode 100644 public/language/id/admin/settings/api.json create mode 100644 public/language/id/admin/settings/chat.json create mode 100644 public/language/id/admin/settings/cookies.json create mode 100644 public/language/id/admin/settings/email.json create mode 100644 public/language/id/admin/settings/general.json create mode 100644 public/language/id/admin/settings/group.json create mode 100644 public/language/id/admin/settings/guest.json create mode 100644 public/language/id/admin/settings/homepage.json create mode 100644 public/language/id/admin/settings/languages.json create mode 100644 public/language/id/admin/settings/navigation.json create mode 100644 public/language/id/admin/settings/notifications.json create mode 100644 public/language/id/admin/settings/pagination.json create mode 100644 public/language/id/admin/settings/post.json create mode 100644 public/language/id/admin/settings/reputation.json create mode 100644 public/language/id/admin/settings/social.json create mode 100644 public/language/id/admin/settings/sockets.json create mode 100644 public/language/id/admin/settings/sounds.json create mode 100644 public/language/id/admin/settings/tags.json create mode 100644 public/language/id/admin/settings/uploads.json create mode 100644 public/language/id/admin/settings/user.json create mode 100644 public/language/id/admin/settings/web-crawler.json create mode 100644 public/language/id/category.json create mode 100644 public/language/id/email.json create mode 100644 public/language/id/error.json create mode 100644 public/language/id/flags.json create mode 100644 public/language/id/global.json create mode 100644 public/language/id/groups.json create mode 100644 public/language/id/ip-blacklist.json create mode 100644 public/language/id/language.json create mode 100644 public/language/id/login.json create mode 100644 public/language/id/modules.json create mode 100644 public/language/id/notifications.json create mode 100644 public/language/id/pages.json create mode 100644 public/language/id/post-queue.json create mode 100644 public/language/id/recent.json create mode 100644 public/language/id/register.json create mode 100644 public/language/id/reset_password.json create mode 100644 public/language/id/search.json create mode 100644 public/language/id/success.json create mode 100644 public/language/id/tags.json create mode 100644 public/language/id/top.json create mode 100644 public/language/id/topic.json create mode 100644 public/language/id/unread.json create mode 100644 public/language/id/uploads.json create mode 100644 public/language/id/user.json create mode 100644 public/language/id/users.json create mode 100644 public/language/it/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/it/admin/admin.json create mode 100644 public/language/it/admin/advanced/cache.json create mode 100644 public/language/it/admin/advanced/database.json create mode 100644 public/language/it/admin/advanced/errors.json create mode 100644 public/language/it/admin/advanced/events.json create mode 100644 public/language/it/admin/advanced/logs.json create mode 100644 public/language/it/admin/appearance/customise.json create mode 100644 public/language/it/admin/appearance/skins.json create mode 100644 public/language/it/admin/appearance/themes.json create mode 100644 public/language/it/admin/dashboard.json create mode 100644 public/language/it/admin/development/info.json create mode 100644 public/language/it/admin/development/logger.json create mode 100644 public/language/it/admin/extend/plugins.json create mode 100644 public/language/it/admin/extend/rewards.json create mode 100644 public/language/it/admin/extend/widgets.json create mode 100644 public/language/it/admin/manage/admins-mods.json create mode 100644 public/language/it/admin/manage/categories.json create mode 100644 public/language/it/admin/manage/digest.json create mode 100644 public/language/it/admin/manage/groups.json create mode 100644 public/language/it/admin/manage/privileges.json create mode 100644 public/language/it/admin/manage/registration.json create mode 100644 public/language/it/admin/manage/tags.json create mode 100644 public/language/it/admin/manage/uploads.json create mode 100644 public/language/it/admin/manage/users.json create mode 100644 public/language/it/admin/menu.json create mode 100644 public/language/it/admin/settings/advanced.json create mode 100644 public/language/it/admin/settings/api.json create mode 100644 public/language/it/admin/settings/chat.json create mode 100644 public/language/it/admin/settings/cookies.json create mode 100644 public/language/it/admin/settings/email.json create mode 100644 public/language/it/admin/settings/general.json create mode 100644 public/language/it/admin/settings/group.json create mode 100644 public/language/it/admin/settings/guest.json create mode 100644 public/language/it/admin/settings/homepage.json create mode 100644 public/language/it/admin/settings/languages.json create mode 100644 public/language/it/admin/settings/navigation.json create mode 100644 public/language/it/admin/settings/notifications.json create mode 100644 public/language/it/admin/settings/pagination.json create mode 100644 public/language/it/admin/settings/post.json create mode 100644 public/language/it/admin/settings/reputation.json create mode 100644 public/language/it/admin/settings/social.json create mode 100644 public/language/it/admin/settings/sockets.json create mode 100644 public/language/it/admin/settings/sounds.json create mode 100644 public/language/it/admin/settings/tags.json create mode 100644 public/language/it/admin/settings/uploads.json create mode 100644 public/language/it/admin/settings/user.json create mode 100644 public/language/it/admin/settings/web-crawler.json create mode 100644 public/language/it/category.json create mode 100644 public/language/it/email.json create mode 100644 public/language/it/error.json create mode 100644 public/language/it/flags.json create mode 100644 public/language/it/global.json create mode 100644 public/language/it/groups.json create mode 100644 public/language/it/ip-blacklist.json create mode 100644 public/language/it/language.json create mode 100644 public/language/it/login.json create mode 100644 public/language/it/modules.json create mode 100644 public/language/it/notifications.json create mode 100644 public/language/it/pages.json create mode 100644 public/language/it/post-queue.json create mode 100644 public/language/it/recent.json create mode 100644 public/language/it/register.json create mode 100644 public/language/it/reset_password.json create mode 100644 public/language/it/search.json create mode 100644 public/language/it/success.json create mode 100644 public/language/it/tags.json create mode 100644 public/language/it/top.json create mode 100644 public/language/it/topic.json create mode 100644 public/language/it/unread.json create mode 100644 public/language/it/uploads.json create mode 100644 public/language/it/user.json create mode 100644 public/language/it/users.json create mode 100644 public/language/ja/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/ja/admin/admin.json create mode 100644 public/language/ja/admin/advanced/cache.json create mode 100644 public/language/ja/admin/advanced/database.json create mode 100644 public/language/ja/admin/advanced/errors.json create mode 100644 public/language/ja/admin/advanced/events.json create mode 100644 public/language/ja/admin/advanced/logs.json create mode 100644 public/language/ja/admin/appearance/customise.json create mode 100644 public/language/ja/admin/appearance/skins.json create mode 100644 public/language/ja/admin/appearance/themes.json create mode 100644 public/language/ja/admin/dashboard.json create mode 100644 public/language/ja/admin/development/info.json create mode 100644 public/language/ja/admin/development/logger.json create mode 100644 public/language/ja/admin/extend/plugins.json create mode 100644 public/language/ja/admin/extend/rewards.json create mode 100644 public/language/ja/admin/extend/widgets.json create mode 100644 public/language/ja/admin/manage/admins-mods.json create mode 100644 public/language/ja/admin/manage/categories.json create mode 100644 public/language/ja/admin/manage/digest.json create mode 100644 public/language/ja/admin/manage/groups.json create mode 100644 public/language/ja/admin/manage/privileges.json create mode 100644 public/language/ja/admin/manage/registration.json create mode 100644 public/language/ja/admin/manage/tags.json create mode 100644 public/language/ja/admin/manage/uploads.json create mode 100644 public/language/ja/admin/manage/users.json create mode 100644 public/language/ja/admin/menu.json create mode 100644 public/language/ja/admin/settings/advanced.json create mode 100644 public/language/ja/admin/settings/api.json create mode 100644 public/language/ja/admin/settings/chat.json create mode 100644 public/language/ja/admin/settings/cookies.json create mode 100644 public/language/ja/admin/settings/email.json create mode 100644 public/language/ja/admin/settings/general.json create mode 100644 public/language/ja/admin/settings/group.json create mode 100644 public/language/ja/admin/settings/guest.json create mode 100644 public/language/ja/admin/settings/homepage.json create mode 100644 public/language/ja/admin/settings/languages.json create mode 100644 public/language/ja/admin/settings/navigation.json create mode 100644 public/language/ja/admin/settings/notifications.json create mode 100644 public/language/ja/admin/settings/pagination.json create mode 100644 public/language/ja/admin/settings/post.json create mode 100644 public/language/ja/admin/settings/reputation.json create mode 100644 public/language/ja/admin/settings/social.json create mode 100644 public/language/ja/admin/settings/sockets.json create mode 100644 public/language/ja/admin/settings/sounds.json create mode 100644 public/language/ja/admin/settings/tags.json create mode 100644 public/language/ja/admin/settings/uploads.json create mode 100644 public/language/ja/admin/settings/user.json create mode 100644 public/language/ja/admin/settings/web-crawler.json create mode 100644 public/language/ja/category.json create mode 100644 public/language/ja/email.json create mode 100644 public/language/ja/error.json create mode 100644 public/language/ja/flags.json create mode 100644 public/language/ja/global.json create mode 100644 public/language/ja/groups.json create mode 100644 public/language/ja/ip-blacklist.json create mode 100644 public/language/ja/language.json create mode 100644 public/language/ja/login.json create mode 100644 public/language/ja/modules.json create mode 100644 public/language/ja/notifications.json create mode 100644 public/language/ja/pages.json create mode 100644 public/language/ja/post-queue.json create mode 100644 public/language/ja/recent.json create mode 100644 public/language/ja/register.json create mode 100644 public/language/ja/reset_password.json create mode 100644 public/language/ja/search.json create mode 100644 public/language/ja/success.json create mode 100644 public/language/ja/tags.json create mode 100644 public/language/ja/top.json create mode 100644 public/language/ja/topic.json create mode 100644 public/language/ja/unread.json create mode 100644 public/language/ja/uploads.json create mode 100644 public/language/ja/user.json create mode 100644 public/language/ja/users.json create mode 100644 public/language/ko/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/ko/admin/admin.json create mode 100644 public/language/ko/admin/advanced/cache.json create mode 100644 public/language/ko/admin/advanced/database.json create mode 100644 public/language/ko/admin/advanced/errors.json create mode 100644 public/language/ko/admin/advanced/events.json create mode 100644 public/language/ko/admin/advanced/logs.json create mode 100644 public/language/ko/admin/appearance/customise.json create mode 100644 public/language/ko/admin/appearance/skins.json create mode 100644 public/language/ko/admin/appearance/themes.json create mode 100644 public/language/ko/admin/dashboard.json create mode 100644 public/language/ko/admin/development/info.json create mode 100644 public/language/ko/admin/development/logger.json create mode 100644 public/language/ko/admin/extend/plugins.json create mode 100644 public/language/ko/admin/extend/rewards.json create mode 100644 public/language/ko/admin/extend/widgets.json create mode 100644 public/language/ko/admin/manage/admins-mods.json create mode 100644 public/language/ko/admin/manage/categories.json create mode 100644 public/language/ko/admin/manage/digest.json create mode 100644 public/language/ko/admin/manage/groups.json create mode 100644 public/language/ko/admin/manage/privileges.json create mode 100644 public/language/ko/admin/manage/registration.json create mode 100644 public/language/ko/admin/manage/tags.json create mode 100644 public/language/ko/admin/manage/uploads.json create mode 100644 public/language/ko/admin/manage/users.json create mode 100644 public/language/ko/admin/menu.json create mode 100644 public/language/ko/admin/settings/advanced.json create mode 100644 public/language/ko/admin/settings/api.json create mode 100644 public/language/ko/admin/settings/chat.json create mode 100644 public/language/ko/admin/settings/cookies.json create mode 100644 public/language/ko/admin/settings/email.json create mode 100644 public/language/ko/admin/settings/general.json create mode 100644 public/language/ko/admin/settings/group.json create mode 100644 public/language/ko/admin/settings/guest.json create mode 100644 public/language/ko/admin/settings/homepage.json create mode 100644 public/language/ko/admin/settings/languages.json create mode 100644 public/language/ko/admin/settings/navigation.json create mode 100644 public/language/ko/admin/settings/notifications.json create mode 100644 public/language/ko/admin/settings/pagination.json create mode 100644 public/language/ko/admin/settings/post.json create mode 100644 public/language/ko/admin/settings/reputation.json create mode 100644 public/language/ko/admin/settings/social.json create mode 100644 public/language/ko/admin/settings/sockets.json create mode 100644 public/language/ko/admin/settings/sounds.json create mode 100644 public/language/ko/admin/settings/tags.json create mode 100644 public/language/ko/admin/settings/uploads.json create mode 100644 public/language/ko/admin/settings/user.json create mode 100644 public/language/ko/admin/settings/web-crawler.json create mode 100644 public/language/ko/category.json create mode 100644 public/language/ko/email.json create mode 100644 public/language/ko/error.json create mode 100644 public/language/ko/flags.json create mode 100644 public/language/ko/global.json create mode 100644 public/language/ko/groups.json create mode 100644 public/language/ko/ip-blacklist.json create mode 100644 public/language/ko/language.json create mode 100644 public/language/ko/login.json create mode 100644 public/language/ko/modules.json create mode 100644 public/language/ko/notifications.json create mode 100644 public/language/ko/pages.json create mode 100644 public/language/ko/post-queue.json create mode 100644 public/language/ko/recent.json create mode 100644 public/language/ko/register.json create mode 100644 public/language/ko/reset_password.json create mode 100644 public/language/ko/search.json create mode 100644 public/language/ko/success.json create mode 100644 public/language/ko/tags.json create mode 100644 public/language/ko/top.json create mode 100644 public/language/ko/topic.json create mode 100644 public/language/ko/unread.json create mode 100644 public/language/ko/uploads.json create mode 100644 public/language/ko/user.json create mode 100644 public/language/ko/users.json create mode 100644 public/language/lt/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/lt/admin/admin.json create mode 100644 public/language/lt/admin/advanced/cache.json create mode 100644 public/language/lt/admin/advanced/database.json create mode 100644 public/language/lt/admin/advanced/errors.json create mode 100644 public/language/lt/admin/advanced/events.json create mode 100644 public/language/lt/admin/advanced/logs.json create mode 100644 public/language/lt/admin/appearance/customise.json create mode 100644 public/language/lt/admin/appearance/skins.json create mode 100644 public/language/lt/admin/appearance/themes.json create mode 100644 public/language/lt/admin/dashboard.json create mode 100644 public/language/lt/admin/development/info.json create mode 100644 public/language/lt/admin/development/logger.json create mode 100644 public/language/lt/admin/extend/plugins.json create mode 100644 public/language/lt/admin/extend/rewards.json create mode 100644 public/language/lt/admin/extend/widgets.json create mode 100644 public/language/lt/admin/manage/admins-mods.json create mode 100644 public/language/lt/admin/manage/categories.json create mode 100644 public/language/lt/admin/manage/digest.json create mode 100644 public/language/lt/admin/manage/groups.json create mode 100644 public/language/lt/admin/manage/privileges.json create mode 100644 public/language/lt/admin/manage/registration.json create mode 100644 public/language/lt/admin/manage/tags.json create mode 100644 public/language/lt/admin/manage/uploads.json create mode 100644 public/language/lt/admin/manage/users.json create mode 100644 public/language/lt/admin/menu.json create mode 100644 public/language/lt/admin/settings/advanced.json create mode 100644 public/language/lt/admin/settings/api.json create mode 100644 public/language/lt/admin/settings/chat.json create mode 100644 public/language/lt/admin/settings/cookies.json create mode 100644 public/language/lt/admin/settings/email.json create mode 100644 public/language/lt/admin/settings/general.json create mode 100644 public/language/lt/admin/settings/group.json create mode 100644 public/language/lt/admin/settings/guest.json create mode 100644 public/language/lt/admin/settings/homepage.json create mode 100644 public/language/lt/admin/settings/languages.json create mode 100644 public/language/lt/admin/settings/navigation.json create mode 100644 public/language/lt/admin/settings/notifications.json create mode 100644 public/language/lt/admin/settings/pagination.json create mode 100644 public/language/lt/admin/settings/post.json create mode 100644 public/language/lt/admin/settings/reputation.json create mode 100644 public/language/lt/admin/settings/social.json create mode 100644 public/language/lt/admin/settings/sockets.json create mode 100644 public/language/lt/admin/settings/sounds.json create mode 100644 public/language/lt/admin/settings/tags.json create mode 100644 public/language/lt/admin/settings/uploads.json create mode 100644 public/language/lt/admin/settings/user.json create mode 100644 public/language/lt/admin/settings/web-crawler.json create mode 100644 public/language/lt/category.json create mode 100644 public/language/lt/email.json create mode 100644 public/language/lt/error.json create mode 100644 public/language/lt/flags.json create mode 100644 public/language/lt/global.json create mode 100644 public/language/lt/groups.json create mode 100644 public/language/lt/ip-blacklist.json create mode 100644 public/language/lt/language.json create mode 100644 public/language/lt/login.json create mode 100644 public/language/lt/modules.json create mode 100644 public/language/lt/notifications.json create mode 100644 public/language/lt/pages.json create mode 100644 public/language/lt/post-queue.json create mode 100644 public/language/lt/recent.json create mode 100644 public/language/lt/register.json create mode 100644 public/language/lt/reset_password.json create mode 100644 public/language/lt/search.json create mode 100644 public/language/lt/success.json create mode 100644 public/language/lt/tags.json create mode 100644 public/language/lt/top.json create mode 100644 public/language/lt/topic.json create mode 100644 public/language/lt/unread.json create mode 100644 public/language/lt/uploads.json create mode 100644 public/language/lt/user.json create mode 100644 public/language/lt/users.json create mode 100644 public/language/lv/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/lv/admin/admin.json create mode 100644 public/language/lv/admin/advanced/cache.json create mode 100644 public/language/lv/admin/advanced/database.json create mode 100644 public/language/lv/admin/advanced/errors.json create mode 100644 public/language/lv/admin/advanced/events.json create mode 100644 public/language/lv/admin/advanced/logs.json create mode 100644 public/language/lv/admin/appearance/customise.json create mode 100644 public/language/lv/admin/appearance/skins.json create mode 100644 public/language/lv/admin/appearance/themes.json create mode 100644 public/language/lv/admin/dashboard.json create mode 100644 public/language/lv/admin/development/info.json create mode 100644 public/language/lv/admin/development/logger.json create mode 100644 public/language/lv/admin/extend/plugins.json create mode 100644 public/language/lv/admin/extend/rewards.json create mode 100644 public/language/lv/admin/extend/widgets.json create mode 100644 public/language/lv/admin/manage/admins-mods.json create mode 100644 public/language/lv/admin/manage/categories.json create mode 100644 public/language/lv/admin/manage/digest.json create mode 100644 public/language/lv/admin/manage/groups.json create mode 100644 public/language/lv/admin/manage/privileges.json create mode 100644 public/language/lv/admin/manage/registration.json create mode 100644 public/language/lv/admin/manage/tags.json create mode 100644 public/language/lv/admin/manage/uploads.json create mode 100644 public/language/lv/admin/manage/users.json create mode 100644 public/language/lv/admin/menu.json create mode 100644 public/language/lv/admin/settings/advanced.json create mode 100644 public/language/lv/admin/settings/api.json create mode 100644 public/language/lv/admin/settings/chat.json create mode 100644 public/language/lv/admin/settings/cookies.json create mode 100644 public/language/lv/admin/settings/email.json create mode 100644 public/language/lv/admin/settings/general.json create mode 100644 public/language/lv/admin/settings/group.json create mode 100644 public/language/lv/admin/settings/guest.json create mode 100644 public/language/lv/admin/settings/homepage.json create mode 100644 public/language/lv/admin/settings/languages.json create mode 100644 public/language/lv/admin/settings/navigation.json create mode 100644 public/language/lv/admin/settings/notifications.json create mode 100644 public/language/lv/admin/settings/pagination.json create mode 100644 public/language/lv/admin/settings/post.json create mode 100644 public/language/lv/admin/settings/reputation.json create mode 100644 public/language/lv/admin/settings/social.json create mode 100644 public/language/lv/admin/settings/sockets.json create mode 100644 public/language/lv/admin/settings/sounds.json create mode 100644 public/language/lv/admin/settings/tags.json create mode 100644 public/language/lv/admin/settings/uploads.json create mode 100644 public/language/lv/admin/settings/user.json create mode 100644 public/language/lv/admin/settings/web-crawler.json create mode 100644 public/language/lv/category.json create mode 100644 public/language/lv/email.json create mode 100644 public/language/lv/error.json create mode 100644 public/language/lv/flags.json create mode 100644 public/language/lv/global.json create mode 100644 public/language/lv/groups.json create mode 100644 public/language/lv/ip-blacklist.json create mode 100644 public/language/lv/language.json create mode 100644 public/language/lv/login.json create mode 100644 public/language/lv/modules.json create mode 100644 public/language/lv/notifications.json create mode 100644 public/language/lv/pages.json create mode 100644 public/language/lv/post-queue.json create mode 100644 public/language/lv/recent.json create mode 100644 public/language/lv/register.json create mode 100644 public/language/lv/reset_password.json create mode 100644 public/language/lv/search.json create mode 100644 public/language/lv/success.json create mode 100644 public/language/lv/tags.json create mode 100644 public/language/lv/top.json create mode 100644 public/language/lv/topic.json create mode 100644 public/language/lv/unread.json create mode 100644 public/language/lv/uploads.json create mode 100644 public/language/lv/user.json create mode 100644 public/language/lv/users.json create mode 100644 public/language/ms/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/ms/admin/admin.json create mode 100644 public/language/ms/admin/advanced/cache.json create mode 100644 public/language/ms/admin/advanced/database.json create mode 100644 public/language/ms/admin/advanced/errors.json create mode 100644 public/language/ms/admin/advanced/events.json create mode 100644 public/language/ms/admin/advanced/logs.json create mode 100644 public/language/ms/admin/appearance/customise.json create mode 100644 public/language/ms/admin/appearance/skins.json create mode 100644 public/language/ms/admin/appearance/themes.json create mode 100644 public/language/ms/admin/dashboard.json create mode 100644 public/language/ms/admin/development/info.json create mode 100644 public/language/ms/admin/development/logger.json create mode 100644 public/language/ms/admin/extend/plugins.json create mode 100644 public/language/ms/admin/extend/rewards.json create mode 100644 public/language/ms/admin/extend/widgets.json create mode 100644 public/language/ms/admin/manage/admins-mods.json create mode 100644 public/language/ms/admin/manage/categories.json create mode 100644 public/language/ms/admin/manage/digest.json create mode 100644 public/language/ms/admin/manage/groups.json create mode 100644 public/language/ms/admin/manage/privileges.json create mode 100644 public/language/ms/admin/manage/registration.json create mode 100644 public/language/ms/admin/manage/tags.json create mode 100644 public/language/ms/admin/manage/uploads.json create mode 100644 public/language/ms/admin/manage/users.json create mode 100644 public/language/ms/admin/menu.json create mode 100644 public/language/ms/admin/settings/advanced.json create mode 100644 public/language/ms/admin/settings/api.json create mode 100644 public/language/ms/admin/settings/chat.json create mode 100644 public/language/ms/admin/settings/cookies.json create mode 100644 public/language/ms/admin/settings/email.json create mode 100644 public/language/ms/admin/settings/general.json create mode 100644 public/language/ms/admin/settings/group.json create mode 100644 public/language/ms/admin/settings/guest.json create mode 100644 public/language/ms/admin/settings/homepage.json create mode 100644 public/language/ms/admin/settings/languages.json create mode 100644 public/language/ms/admin/settings/navigation.json create mode 100644 public/language/ms/admin/settings/notifications.json create mode 100644 public/language/ms/admin/settings/pagination.json create mode 100644 public/language/ms/admin/settings/post.json create mode 100644 public/language/ms/admin/settings/reputation.json create mode 100644 public/language/ms/admin/settings/social.json create mode 100644 public/language/ms/admin/settings/sockets.json create mode 100644 public/language/ms/admin/settings/sounds.json create mode 100644 public/language/ms/admin/settings/tags.json create mode 100644 public/language/ms/admin/settings/uploads.json create mode 100644 public/language/ms/admin/settings/user.json create mode 100644 public/language/ms/admin/settings/web-crawler.json create mode 100644 public/language/ms/category.json create mode 100644 public/language/ms/email.json create mode 100644 public/language/ms/error.json create mode 100644 public/language/ms/flags.json create mode 100644 public/language/ms/global.json create mode 100644 public/language/ms/groups.json create mode 100644 public/language/ms/ip-blacklist.json create mode 100644 public/language/ms/language.json create mode 100644 public/language/ms/login.json create mode 100644 public/language/ms/modules.json create mode 100644 public/language/ms/notifications.json create mode 100644 public/language/ms/pages.json create mode 100644 public/language/ms/post-queue.json create mode 100644 public/language/ms/recent.json create mode 100644 public/language/ms/register.json create mode 100644 public/language/ms/reset_password.json create mode 100644 public/language/ms/search.json create mode 100644 public/language/ms/success.json create mode 100644 public/language/ms/tags.json create mode 100644 public/language/ms/top.json create mode 100644 public/language/ms/topic.json create mode 100644 public/language/ms/unread.json create mode 100644 public/language/ms/uploads.json create mode 100644 public/language/ms/user.json create mode 100644 public/language/ms/users.json create mode 100644 public/language/nb/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/nb/admin/admin.json create mode 100644 public/language/nb/admin/advanced/cache.json create mode 100644 public/language/nb/admin/advanced/database.json create mode 100644 public/language/nb/admin/advanced/errors.json create mode 100644 public/language/nb/admin/advanced/events.json create mode 100644 public/language/nb/admin/advanced/logs.json create mode 100644 public/language/nb/admin/appearance/customise.json create mode 100644 public/language/nb/admin/appearance/skins.json create mode 100644 public/language/nb/admin/appearance/themes.json create mode 100644 public/language/nb/admin/dashboard.json create mode 100644 public/language/nb/admin/development/info.json create mode 100644 public/language/nb/admin/development/logger.json create mode 100644 public/language/nb/admin/extend/plugins.json create mode 100644 public/language/nb/admin/extend/rewards.json create mode 100644 public/language/nb/admin/extend/widgets.json create mode 100644 public/language/nb/admin/manage/admins-mods.json create mode 100644 public/language/nb/admin/manage/categories.json create mode 100644 public/language/nb/admin/manage/digest.json create mode 100644 public/language/nb/admin/manage/groups.json create mode 100644 public/language/nb/admin/manage/privileges.json create mode 100644 public/language/nb/admin/manage/registration.json create mode 100644 public/language/nb/admin/manage/tags.json create mode 100644 public/language/nb/admin/manage/uploads.json create mode 100644 public/language/nb/admin/manage/users.json create mode 100644 public/language/nb/admin/menu.json create mode 100644 public/language/nb/admin/settings/advanced.json create mode 100644 public/language/nb/admin/settings/api.json create mode 100644 public/language/nb/admin/settings/chat.json create mode 100644 public/language/nb/admin/settings/cookies.json create mode 100644 public/language/nb/admin/settings/email.json create mode 100644 public/language/nb/admin/settings/general.json create mode 100644 public/language/nb/admin/settings/group.json create mode 100644 public/language/nb/admin/settings/guest.json create mode 100644 public/language/nb/admin/settings/homepage.json create mode 100644 public/language/nb/admin/settings/languages.json create mode 100644 public/language/nb/admin/settings/navigation.json create mode 100644 public/language/nb/admin/settings/notifications.json create mode 100644 public/language/nb/admin/settings/pagination.json create mode 100644 public/language/nb/admin/settings/post.json create mode 100644 public/language/nb/admin/settings/reputation.json create mode 100644 public/language/nb/admin/settings/social.json create mode 100644 public/language/nb/admin/settings/sockets.json create mode 100644 public/language/nb/admin/settings/sounds.json create mode 100644 public/language/nb/admin/settings/tags.json create mode 100644 public/language/nb/admin/settings/uploads.json create mode 100644 public/language/nb/admin/settings/user.json create mode 100644 public/language/nb/admin/settings/web-crawler.json create mode 100644 public/language/nb/category.json create mode 100644 public/language/nb/email.json create mode 100644 public/language/nb/error.json create mode 100644 public/language/nb/flags.json create mode 100644 public/language/nb/global.json create mode 100644 public/language/nb/groups.json create mode 100644 public/language/nb/ip-blacklist.json create mode 100644 public/language/nb/language.json create mode 100644 public/language/nb/login.json create mode 100644 public/language/nb/modules.json create mode 100644 public/language/nb/notifications.json create mode 100644 public/language/nb/pages.json create mode 100644 public/language/nb/post-queue.json create mode 100644 public/language/nb/recent.json create mode 100644 public/language/nb/register.json create mode 100644 public/language/nb/reset_password.json create mode 100644 public/language/nb/search.json create mode 100644 public/language/nb/success.json create mode 100644 public/language/nb/tags.json create mode 100644 public/language/nb/top.json create mode 100644 public/language/nb/topic.json create mode 100644 public/language/nb/unread.json create mode 100644 public/language/nb/uploads.json create mode 100644 public/language/nb/user.json create mode 100644 public/language/nb/users.json create mode 100644 public/language/nl/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/nl/admin/admin.json create mode 100644 public/language/nl/admin/advanced/cache.json create mode 100644 public/language/nl/admin/advanced/database.json create mode 100644 public/language/nl/admin/advanced/errors.json create mode 100644 public/language/nl/admin/advanced/events.json create mode 100644 public/language/nl/admin/advanced/logs.json create mode 100644 public/language/nl/admin/appearance/customise.json create mode 100644 public/language/nl/admin/appearance/skins.json create mode 100644 public/language/nl/admin/appearance/themes.json create mode 100644 public/language/nl/admin/dashboard.json create mode 100644 public/language/nl/admin/development/info.json create mode 100644 public/language/nl/admin/development/logger.json create mode 100644 public/language/nl/admin/extend/plugins.json create mode 100644 public/language/nl/admin/extend/rewards.json create mode 100644 public/language/nl/admin/extend/widgets.json create mode 100644 public/language/nl/admin/manage/admins-mods.json create mode 100644 public/language/nl/admin/manage/categories.json create mode 100644 public/language/nl/admin/manage/digest.json create mode 100644 public/language/nl/admin/manage/groups.json create mode 100644 public/language/nl/admin/manage/privileges.json create mode 100644 public/language/nl/admin/manage/registration.json create mode 100644 public/language/nl/admin/manage/tags.json create mode 100644 public/language/nl/admin/manage/uploads.json create mode 100644 public/language/nl/admin/manage/users.json create mode 100644 public/language/nl/admin/menu.json create mode 100644 public/language/nl/admin/settings/advanced.json create mode 100644 public/language/nl/admin/settings/api.json create mode 100644 public/language/nl/admin/settings/chat.json create mode 100644 public/language/nl/admin/settings/cookies.json create mode 100644 public/language/nl/admin/settings/email.json create mode 100644 public/language/nl/admin/settings/general.json create mode 100644 public/language/nl/admin/settings/group.json create mode 100644 public/language/nl/admin/settings/guest.json create mode 100644 public/language/nl/admin/settings/homepage.json create mode 100644 public/language/nl/admin/settings/languages.json create mode 100644 public/language/nl/admin/settings/navigation.json create mode 100644 public/language/nl/admin/settings/notifications.json create mode 100644 public/language/nl/admin/settings/pagination.json create mode 100644 public/language/nl/admin/settings/post.json create mode 100644 public/language/nl/admin/settings/reputation.json create mode 100644 public/language/nl/admin/settings/social.json create mode 100644 public/language/nl/admin/settings/sockets.json create mode 100644 public/language/nl/admin/settings/sounds.json create mode 100644 public/language/nl/admin/settings/tags.json create mode 100644 public/language/nl/admin/settings/uploads.json create mode 100644 public/language/nl/admin/settings/user.json create mode 100644 public/language/nl/admin/settings/web-crawler.json create mode 100644 public/language/nl/category.json create mode 100644 public/language/nl/email.json create mode 100644 public/language/nl/error.json create mode 100644 public/language/nl/flags.json create mode 100644 public/language/nl/global.json create mode 100644 public/language/nl/groups.json create mode 100644 public/language/nl/ip-blacklist.json create mode 100644 public/language/nl/language.json create mode 100644 public/language/nl/login.json create mode 100644 public/language/nl/modules.json create mode 100644 public/language/nl/notifications.json create mode 100644 public/language/nl/pages.json create mode 100644 public/language/nl/post-queue.json create mode 100644 public/language/nl/recent.json create mode 100644 public/language/nl/register.json create mode 100644 public/language/nl/reset_password.json create mode 100644 public/language/nl/search.json create mode 100644 public/language/nl/success.json create mode 100644 public/language/nl/tags.json create mode 100644 public/language/nl/top.json create mode 100644 public/language/nl/topic.json create mode 100644 public/language/nl/unread.json create mode 100644 public/language/nl/uploads.json create mode 100644 public/language/nl/user.json create mode 100644 public/language/nl/users.json create mode 100644 public/language/pl/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/pl/admin/admin.json create mode 100644 public/language/pl/admin/advanced/cache.json create mode 100644 public/language/pl/admin/advanced/database.json create mode 100644 public/language/pl/admin/advanced/errors.json create mode 100644 public/language/pl/admin/advanced/events.json create mode 100644 public/language/pl/admin/advanced/logs.json create mode 100644 public/language/pl/admin/appearance/customise.json create mode 100644 public/language/pl/admin/appearance/skins.json create mode 100644 public/language/pl/admin/appearance/themes.json create mode 100644 public/language/pl/admin/dashboard.json create mode 100644 public/language/pl/admin/development/info.json create mode 100644 public/language/pl/admin/development/logger.json create mode 100644 public/language/pl/admin/extend/plugins.json create mode 100644 public/language/pl/admin/extend/rewards.json create mode 100644 public/language/pl/admin/extend/widgets.json create mode 100644 public/language/pl/admin/manage/admins-mods.json create mode 100644 public/language/pl/admin/manage/categories.json create mode 100644 public/language/pl/admin/manage/digest.json create mode 100644 public/language/pl/admin/manage/groups.json create mode 100644 public/language/pl/admin/manage/privileges.json create mode 100644 public/language/pl/admin/manage/registration.json create mode 100644 public/language/pl/admin/manage/tags.json create mode 100644 public/language/pl/admin/manage/uploads.json create mode 100644 public/language/pl/admin/manage/users.json create mode 100644 public/language/pl/admin/menu.json create mode 100644 public/language/pl/admin/settings/advanced.json create mode 100644 public/language/pl/admin/settings/api.json create mode 100644 public/language/pl/admin/settings/chat.json create mode 100644 public/language/pl/admin/settings/cookies.json create mode 100644 public/language/pl/admin/settings/email.json create mode 100644 public/language/pl/admin/settings/general.json create mode 100644 public/language/pl/admin/settings/group.json create mode 100644 public/language/pl/admin/settings/guest.json create mode 100644 public/language/pl/admin/settings/homepage.json create mode 100644 public/language/pl/admin/settings/languages.json create mode 100644 public/language/pl/admin/settings/navigation.json create mode 100644 public/language/pl/admin/settings/notifications.json create mode 100644 public/language/pl/admin/settings/pagination.json create mode 100644 public/language/pl/admin/settings/post.json create mode 100644 public/language/pl/admin/settings/reputation.json create mode 100644 public/language/pl/admin/settings/social.json create mode 100644 public/language/pl/admin/settings/sockets.json create mode 100644 public/language/pl/admin/settings/sounds.json create mode 100644 public/language/pl/admin/settings/tags.json create mode 100644 public/language/pl/admin/settings/uploads.json create mode 100644 public/language/pl/admin/settings/user.json create mode 100644 public/language/pl/admin/settings/web-crawler.json create mode 100644 public/language/pl/category.json create mode 100644 public/language/pl/email.json create mode 100644 public/language/pl/error.json create mode 100644 public/language/pl/flags.json create mode 100644 public/language/pl/global.json create mode 100644 public/language/pl/groups.json create mode 100644 public/language/pl/ip-blacklist.json create mode 100644 public/language/pl/language.json create mode 100644 public/language/pl/login.json create mode 100644 public/language/pl/modules.json create mode 100644 public/language/pl/notifications.json create mode 100644 public/language/pl/pages.json create mode 100644 public/language/pl/post-queue.json create mode 100644 public/language/pl/recent.json create mode 100644 public/language/pl/register.json create mode 100644 public/language/pl/reset_password.json create mode 100644 public/language/pl/search.json create mode 100644 public/language/pl/success.json create mode 100644 public/language/pl/tags.json create mode 100644 public/language/pl/top.json create mode 100644 public/language/pl/topic.json create mode 100644 public/language/pl/unread.json create mode 100644 public/language/pl/uploads.json create mode 100644 public/language/pl/user.json create mode 100644 public/language/pl/users.json create mode 100644 public/language/pt-BR/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/pt-BR/admin/admin.json create mode 100644 public/language/pt-BR/admin/advanced/cache.json create mode 100644 public/language/pt-BR/admin/advanced/database.json create mode 100644 public/language/pt-BR/admin/advanced/errors.json create mode 100644 public/language/pt-BR/admin/advanced/events.json create mode 100644 public/language/pt-BR/admin/advanced/logs.json create mode 100644 public/language/pt-BR/admin/appearance/customise.json create mode 100644 public/language/pt-BR/admin/appearance/skins.json create mode 100644 public/language/pt-BR/admin/appearance/themes.json create mode 100644 public/language/pt-BR/admin/dashboard.json create mode 100644 public/language/pt-BR/admin/development/info.json create mode 100644 public/language/pt-BR/admin/development/logger.json create mode 100644 public/language/pt-BR/admin/extend/plugins.json create mode 100644 public/language/pt-BR/admin/extend/rewards.json create mode 100644 public/language/pt-BR/admin/extend/widgets.json create mode 100644 public/language/pt-BR/admin/manage/admins-mods.json create mode 100644 public/language/pt-BR/admin/manage/categories.json create mode 100644 public/language/pt-BR/admin/manage/digest.json create mode 100644 public/language/pt-BR/admin/manage/groups.json create mode 100644 public/language/pt-BR/admin/manage/privileges.json create mode 100644 public/language/pt-BR/admin/manage/registration.json create mode 100644 public/language/pt-BR/admin/manage/tags.json create mode 100644 public/language/pt-BR/admin/manage/uploads.json create mode 100644 public/language/pt-BR/admin/manage/users.json create mode 100644 public/language/pt-BR/admin/menu.json create mode 100644 public/language/pt-BR/admin/settings/advanced.json create mode 100644 public/language/pt-BR/admin/settings/api.json create mode 100644 public/language/pt-BR/admin/settings/chat.json create mode 100644 public/language/pt-BR/admin/settings/cookies.json create mode 100644 public/language/pt-BR/admin/settings/email.json create mode 100644 public/language/pt-BR/admin/settings/general.json create mode 100644 public/language/pt-BR/admin/settings/group.json create mode 100644 public/language/pt-BR/admin/settings/guest.json create mode 100644 public/language/pt-BR/admin/settings/homepage.json create mode 100644 public/language/pt-BR/admin/settings/languages.json create mode 100644 public/language/pt-BR/admin/settings/navigation.json create mode 100644 public/language/pt-BR/admin/settings/notifications.json create mode 100644 public/language/pt-BR/admin/settings/pagination.json create mode 100644 public/language/pt-BR/admin/settings/post.json create mode 100644 public/language/pt-BR/admin/settings/reputation.json create mode 100644 public/language/pt-BR/admin/settings/social.json create mode 100644 public/language/pt-BR/admin/settings/sockets.json create mode 100644 public/language/pt-BR/admin/settings/sounds.json create mode 100644 public/language/pt-BR/admin/settings/tags.json create mode 100644 public/language/pt-BR/admin/settings/uploads.json create mode 100644 public/language/pt-BR/admin/settings/user.json create mode 100644 public/language/pt-BR/admin/settings/web-crawler.json create mode 100644 public/language/pt-BR/category.json create mode 100644 public/language/pt-BR/email.json create mode 100644 public/language/pt-BR/error.json create mode 100644 public/language/pt-BR/flags.json create mode 100644 public/language/pt-BR/global.json create mode 100644 public/language/pt-BR/groups.json create mode 100644 public/language/pt-BR/ip-blacklist.json create mode 100644 public/language/pt-BR/language.json create mode 100644 public/language/pt-BR/login.json create mode 100644 public/language/pt-BR/modules.json create mode 100644 public/language/pt-BR/notifications.json create mode 100644 public/language/pt-BR/pages.json create mode 100644 public/language/pt-BR/post-queue.json create mode 100644 public/language/pt-BR/recent.json create mode 100644 public/language/pt-BR/register.json create mode 100644 public/language/pt-BR/reset_password.json create mode 100644 public/language/pt-BR/search.json create mode 100644 public/language/pt-BR/success.json create mode 100644 public/language/pt-BR/tags.json create mode 100644 public/language/pt-BR/top.json create mode 100644 public/language/pt-BR/topic.json create mode 100644 public/language/pt-BR/unread.json create mode 100644 public/language/pt-BR/uploads.json create mode 100644 public/language/pt-BR/user.json create mode 100644 public/language/pt-BR/users.json create mode 100644 public/language/pt-PT/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/pt-PT/admin/admin.json create mode 100644 public/language/pt-PT/admin/advanced/cache.json create mode 100644 public/language/pt-PT/admin/advanced/database.json create mode 100644 public/language/pt-PT/admin/advanced/errors.json create mode 100644 public/language/pt-PT/admin/advanced/events.json create mode 100644 public/language/pt-PT/admin/advanced/logs.json create mode 100644 public/language/pt-PT/admin/appearance/customise.json create mode 100644 public/language/pt-PT/admin/appearance/skins.json create mode 100644 public/language/pt-PT/admin/appearance/themes.json create mode 100644 public/language/pt-PT/admin/dashboard.json create mode 100644 public/language/pt-PT/admin/development/info.json create mode 100644 public/language/pt-PT/admin/development/logger.json create mode 100644 public/language/pt-PT/admin/extend/plugins.json create mode 100644 public/language/pt-PT/admin/extend/rewards.json create mode 100644 public/language/pt-PT/admin/extend/widgets.json create mode 100644 public/language/pt-PT/admin/manage/admins-mods.json create mode 100644 public/language/pt-PT/admin/manage/categories.json create mode 100644 public/language/pt-PT/admin/manage/digest.json create mode 100644 public/language/pt-PT/admin/manage/groups.json create mode 100644 public/language/pt-PT/admin/manage/privileges.json create mode 100644 public/language/pt-PT/admin/manage/registration.json create mode 100644 public/language/pt-PT/admin/manage/tags.json create mode 100644 public/language/pt-PT/admin/manage/uploads.json create mode 100644 public/language/pt-PT/admin/manage/users.json create mode 100644 public/language/pt-PT/admin/menu.json create mode 100644 public/language/pt-PT/admin/settings/advanced.json create mode 100644 public/language/pt-PT/admin/settings/api.json create mode 100644 public/language/pt-PT/admin/settings/chat.json create mode 100644 public/language/pt-PT/admin/settings/cookies.json create mode 100644 public/language/pt-PT/admin/settings/email.json create mode 100644 public/language/pt-PT/admin/settings/general.json create mode 100644 public/language/pt-PT/admin/settings/group.json create mode 100644 public/language/pt-PT/admin/settings/guest.json create mode 100644 public/language/pt-PT/admin/settings/homepage.json create mode 100644 public/language/pt-PT/admin/settings/languages.json create mode 100644 public/language/pt-PT/admin/settings/navigation.json create mode 100644 public/language/pt-PT/admin/settings/notifications.json create mode 100644 public/language/pt-PT/admin/settings/pagination.json create mode 100644 public/language/pt-PT/admin/settings/post.json create mode 100644 public/language/pt-PT/admin/settings/reputation.json create mode 100644 public/language/pt-PT/admin/settings/social.json create mode 100644 public/language/pt-PT/admin/settings/sockets.json create mode 100644 public/language/pt-PT/admin/settings/sounds.json create mode 100644 public/language/pt-PT/admin/settings/tags.json create mode 100644 public/language/pt-PT/admin/settings/uploads.json create mode 100644 public/language/pt-PT/admin/settings/user.json create mode 100644 public/language/pt-PT/admin/settings/web-crawler.json create mode 100644 public/language/pt-PT/category.json create mode 100644 public/language/pt-PT/email.json create mode 100644 public/language/pt-PT/error.json create mode 100644 public/language/pt-PT/flags.json create mode 100644 public/language/pt-PT/global.json create mode 100644 public/language/pt-PT/groups.json create mode 100644 public/language/pt-PT/ip-blacklist.json create mode 100644 public/language/pt-PT/language.json create mode 100644 public/language/pt-PT/login.json create mode 100644 public/language/pt-PT/modules.json create mode 100644 public/language/pt-PT/notifications.json create mode 100644 public/language/pt-PT/pages.json create mode 100644 public/language/pt-PT/post-queue.json create mode 100644 public/language/pt-PT/recent.json create mode 100644 public/language/pt-PT/register.json create mode 100644 public/language/pt-PT/reset_password.json create mode 100644 public/language/pt-PT/search.json create mode 100644 public/language/pt-PT/success.json create mode 100644 public/language/pt-PT/tags.json create mode 100644 public/language/pt-PT/top.json create mode 100644 public/language/pt-PT/topic.json create mode 100644 public/language/pt-PT/unread.json create mode 100644 public/language/pt-PT/uploads.json create mode 100644 public/language/pt-PT/user.json create mode 100644 public/language/pt-PT/users.json create mode 100644 public/language/ro/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/ro/admin/admin.json create mode 100644 public/language/ro/admin/advanced/cache.json create mode 100644 public/language/ro/admin/advanced/database.json create mode 100644 public/language/ro/admin/advanced/errors.json create mode 100644 public/language/ro/admin/advanced/events.json create mode 100644 public/language/ro/admin/advanced/logs.json create mode 100644 public/language/ro/admin/appearance/customise.json create mode 100644 public/language/ro/admin/appearance/skins.json create mode 100644 public/language/ro/admin/appearance/themes.json create mode 100644 public/language/ro/admin/dashboard.json create mode 100644 public/language/ro/admin/development/info.json create mode 100644 public/language/ro/admin/development/logger.json create mode 100644 public/language/ro/admin/extend/plugins.json create mode 100644 public/language/ro/admin/extend/rewards.json create mode 100644 public/language/ro/admin/extend/widgets.json create mode 100644 public/language/ro/admin/manage/admins-mods.json create mode 100644 public/language/ro/admin/manage/categories.json create mode 100644 public/language/ro/admin/manage/digest.json create mode 100644 public/language/ro/admin/manage/groups.json create mode 100644 public/language/ro/admin/manage/privileges.json create mode 100644 public/language/ro/admin/manage/registration.json create mode 100644 public/language/ro/admin/manage/tags.json create mode 100644 public/language/ro/admin/manage/uploads.json create mode 100644 public/language/ro/admin/manage/users.json create mode 100644 public/language/ro/admin/menu.json create mode 100644 public/language/ro/admin/settings/advanced.json create mode 100644 public/language/ro/admin/settings/api.json create mode 100644 public/language/ro/admin/settings/chat.json create mode 100644 public/language/ro/admin/settings/cookies.json create mode 100644 public/language/ro/admin/settings/email.json create mode 100644 public/language/ro/admin/settings/general.json create mode 100644 public/language/ro/admin/settings/group.json create mode 100644 public/language/ro/admin/settings/guest.json create mode 100644 public/language/ro/admin/settings/homepage.json create mode 100644 public/language/ro/admin/settings/languages.json create mode 100644 public/language/ro/admin/settings/navigation.json create mode 100644 public/language/ro/admin/settings/notifications.json create mode 100644 public/language/ro/admin/settings/pagination.json create mode 100644 public/language/ro/admin/settings/post.json create mode 100644 public/language/ro/admin/settings/reputation.json create mode 100644 public/language/ro/admin/settings/social.json create mode 100644 public/language/ro/admin/settings/sockets.json create mode 100644 public/language/ro/admin/settings/sounds.json create mode 100644 public/language/ro/admin/settings/tags.json create mode 100644 public/language/ro/admin/settings/uploads.json create mode 100644 public/language/ro/admin/settings/user.json create mode 100644 public/language/ro/admin/settings/web-crawler.json create mode 100644 public/language/ro/category.json create mode 100644 public/language/ro/email.json create mode 100644 public/language/ro/error.json create mode 100644 public/language/ro/flags.json create mode 100644 public/language/ro/global.json create mode 100644 public/language/ro/groups.json create mode 100644 public/language/ro/ip-blacklist.json create mode 100644 public/language/ro/language.json create mode 100644 public/language/ro/login.json create mode 100644 public/language/ro/modules.json create mode 100644 public/language/ro/notifications.json create mode 100644 public/language/ro/pages.json create mode 100644 public/language/ro/post-queue.json create mode 100644 public/language/ro/recent.json create mode 100644 public/language/ro/register.json create mode 100644 public/language/ro/reset_password.json create mode 100644 public/language/ro/search.json create mode 100644 public/language/ro/success.json create mode 100644 public/language/ro/tags.json create mode 100644 public/language/ro/top.json create mode 100644 public/language/ro/topic.json create mode 100644 public/language/ro/unread.json create mode 100644 public/language/ro/uploads.json create mode 100644 public/language/ro/user.json create mode 100644 public/language/ro/users.json create mode 100644 public/language/ru/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/ru/admin/admin.json create mode 100644 public/language/ru/admin/advanced/cache.json create mode 100644 public/language/ru/admin/advanced/database.json create mode 100644 public/language/ru/admin/advanced/errors.json create mode 100644 public/language/ru/admin/advanced/events.json create mode 100644 public/language/ru/admin/advanced/logs.json create mode 100644 public/language/ru/admin/appearance/customise.json create mode 100644 public/language/ru/admin/appearance/skins.json create mode 100644 public/language/ru/admin/appearance/themes.json create mode 100644 public/language/ru/admin/dashboard.json create mode 100644 public/language/ru/admin/development/info.json create mode 100644 public/language/ru/admin/development/logger.json create mode 100644 public/language/ru/admin/extend/plugins.json create mode 100644 public/language/ru/admin/extend/rewards.json create mode 100644 public/language/ru/admin/extend/widgets.json create mode 100644 public/language/ru/admin/manage/admins-mods.json create mode 100644 public/language/ru/admin/manage/categories.json create mode 100644 public/language/ru/admin/manage/digest.json create mode 100644 public/language/ru/admin/manage/groups.json create mode 100644 public/language/ru/admin/manage/privileges.json create mode 100644 public/language/ru/admin/manage/registration.json create mode 100644 public/language/ru/admin/manage/tags.json create mode 100644 public/language/ru/admin/manage/uploads.json create mode 100644 public/language/ru/admin/manage/users.json create mode 100644 public/language/ru/admin/menu.json create mode 100644 public/language/ru/admin/settings/advanced.json create mode 100644 public/language/ru/admin/settings/api.json create mode 100644 public/language/ru/admin/settings/chat.json create mode 100644 public/language/ru/admin/settings/cookies.json create mode 100644 public/language/ru/admin/settings/email.json create mode 100644 public/language/ru/admin/settings/general.json create mode 100644 public/language/ru/admin/settings/group.json create mode 100644 public/language/ru/admin/settings/guest.json create mode 100644 public/language/ru/admin/settings/homepage.json create mode 100644 public/language/ru/admin/settings/languages.json create mode 100644 public/language/ru/admin/settings/navigation.json create mode 100644 public/language/ru/admin/settings/notifications.json create mode 100644 public/language/ru/admin/settings/pagination.json create mode 100644 public/language/ru/admin/settings/post.json create mode 100644 public/language/ru/admin/settings/reputation.json create mode 100644 public/language/ru/admin/settings/social.json create mode 100644 public/language/ru/admin/settings/sockets.json create mode 100644 public/language/ru/admin/settings/sounds.json create mode 100644 public/language/ru/admin/settings/tags.json create mode 100644 public/language/ru/admin/settings/uploads.json create mode 100644 public/language/ru/admin/settings/user.json create mode 100644 public/language/ru/admin/settings/web-crawler.json create mode 100644 public/language/ru/category.json create mode 100644 public/language/ru/email.json create mode 100644 public/language/ru/error.json create mode 100644 public/language/ru/flags.json create mode 100644 public/language/ru/global.json create mode 100644 public/language/ru/groups.json create mode 100644 public/language/ru/ip-blacklist.json create mode 100644 public/language/ru/language.json create mode 100644 public/language/ru/login.json create mode 100644 public/language/ru/modules.json create mode 100644 public/language/ru/notifications.json create mode 100644 public/language/ru/pages.json create mode 100644 public/language/ru/post-queue.json create mode 100644 public/language/ru/recent.json create mode 100644 public/language/ru/register.json create mode 100644 public/language/ru/reset_password.json create mode 100644 public/language/ru/search.json create mode 100644 public/language/ru/success.json create mode 100644 public/language/ru/tags.json create mode 100644 public/language/ru/top.json create mode 100644 public/language/ru/topic.json create mode 100644 public/language/ru/unread.json create mode 100644 public/language/ru/uploads.json create mode 100644 public/language/ru/user.json create mode 100644 public/language/ru/users.json create mode 100644 public/language/rw/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/rw/admin/admin.json create mode 100644 public/language/rw/admin/advanced/cache.json create mode 100644 public/language/rw/admin/advanced/database.json create mode 100644 public/language/rw/admin/advanced/errors.json create mode 100644 public/language/rw/admin/advanced/events.json create mode 100644 public/language/rw/admin/advanced/logs.json create mode 100644 public/language/rw/admin/appearance/customise.json create mode 100644 public/language/rw/admin/appearance/skins.json create mode 100644 public/language/rw/admin/appearance/themes.json create mode 100644 public/language/rw/admin/dashboard.json create mode 100644 public/language/rw/admin/development/info.json create mode 100644 public/language/rw/admin/development/logger.json create mode 100644 public/language/rw/admin/extend/plugins.json create mode 100644 public/language/rw/admin/extend/rewards.json create mode 100644 public/language/rw/admin/extend/widgets.json create mode 100644 public/language/rw/admin/manage/admins-mods.json create mode 100644 public/language/rw/admin/manage/categories.json create mode 100644 public/language/rw/admin/manage/digest.json create mode 100644 public/language/rw/admin/manage/groups.json create mode 100644 public/language/rw/admin/manage/privileges.json create mode 100644 public/language/rw/admin/manage/registration.json create mode 100644 public/language/rw/admin/manage/tags.json create mode 100644 public/language/rw/admin/manage/uploads.json create mode 100644 public/language/rw/admin/manage/users.json create mode 100644 public/language/rw/admin/menu.json create mode 100644 public/language/rw/admin/settings/advanced.json create mode 100644 public/language/rw/admin/settings/api.json create mode 100644 public/language/rw/admin/settings/chat.json create mode 100644 public/language/rw/admin/settings/cookies.json create mode 100644 public/language/rw/admin/settings/email.json create mode 100644 public/language/rw/admin/settings/general.json create mode 100644 public/language/rw/admin/settings/group.json create mode 100644 public/language/rw/admin/settings/guest.json create mode 100644 public/language/rw/admin/settings/homepage.json create mode 100644 public/language/rw/admin/settings/languages.json create mode 100644 public/language/rw/admin/settings/navigation.json create mode 100644 public/language/rw/admin/settings/notifications.json create mode 100644 public/language/rw/admin/settings/pagination.json create mode 100644 public/language/rw/admin/settings/post.json create mode 100644 public/language/rw/admin/settings/reputation.json create mode 100644 public/language/rw/admin/settings/social.json create mode 100644 public/language/rw/admin/settings/sockets.json create mode 100644 public/language/rw/admin/settings/sounds.json create mode 100644 public/language/rw/admin/settings/tags.json create mode 100644 public/language/rw/admin/settings/uploads.json create mode 100644 public/language/rw/admin/settings/user.json create mode 100644 public/language/rw/admin/settings/web-crawler.json create mode 100644 public/language/rw/category.json create mode 100644 public/language/rw/email.json create mode 100644 public/language/rw/error.json create mode 100644 public/language/rw/flags.json create mode 100644 public/language/rw/global.json create mode 100644 public/language/rw/groups.json create mode 100644 public/language/rw/ip-blacklist.json create mode 100644 public/language/rw/language.json create mode 100644 public/language/rw/login.json create mode 100644 public/language/rw/modules.json create mode 100644 public/language/rw/notifications.json create mode 100644 public/language/rw/pages.json create mode 100644 public/language/rw/post-queue.json create mode 100644 public/language/rw/recent.json create mode 100644 public/language/rw/register.json create mode 100644 public/language/rw/reset_password.json create mode 100644 public/language/rw/search.json create mode 100644 public/language/rw/success.json create mode 100644 public/language/rw/tags.json create mode 100644 public/language/rw/top.json create mode 100644 public/language/rw/topic.json create mode 100644 public/language/rw/unread.json create mode 100644 public/language/rw/uploads.json create mode 100644 public/language/rw/user.json create mode 100644 public/language/rw/users.json create mode 100644 public/language/sc/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/sc/admin/admin.json create mode 100644 public/language/sc/admin/advanced/cache.json create mode 100644 public/language/sc/admin/advanced/database.json create mode 100644 public/language/sc/admin/advanced/errors.json create mode 100644 public/language/sc/admin/advanced/events.json create mode 100644 public/language/sc/admin/advanced/logs.json create mode 100644 public/language/sc/admin/appearance/customise.json create mode 100644 public/language/sc/admin/appearance/skins.json create mode 100644 public/language/sc/admin/appearance/themes.json create mode 100644 public/language/sc/admin/dashboard.json create mode 100644 public/language/sc/admin/development/info.json create mode 100644 public/language/sc/admin/development/logger.json create mode 100644 public/language/sc/admin/extend/plugins.json create mode 100644 public/language/sc/admin/extend/rewards.json create mode 100644 public/language/sc/admin/extend/widgets.json create mode 100644 public/language/sc/admin/manage/admins-mods.json create mode 100644 public/language/sc/admin/manage/categories.json create mode 100644 public/language/sc/admin/manage/digest.json create mode 100644 public/language/sc/admin/manage/groups.json create mode 100644 public/language/sc/admin/manage/privileges.json create mode 100644 public/language/sc/admin/manage/registration.json create mode 100644 public/language/sc/admin/manage/tags.json create mode 100644 public/language/sc/admin/manage/uploads.json create mode 100644 public/language/sc/admin/manage/users.json create mode 100644 public/language/sc/admin/menu.json create mode 100644 public/language/sc/admin/settings/advanced.json create mode 100644 public/language/sc/admin/settings/api.json create mode 100644 public/language/sc/admin/settings/chat.json create mode 100644 public/language/sc/admin/settings/cookies.json create mode 100644 public/language/sc/admin/settings/email.json create mode 100644 public/language/sc/admin/settings/general.json create mode 100644 public/language/sc/admin/settings/group.json create mode 100644 public/language/sc/admin/settings/guest.json create mode 100644 public/language/sc/admin/settings/homepage.json create mode 100644 public/language/sc/admin/settings/languages.json create mode 100644 public/language/sc/admin/settings/navigation.json create mode 100644 public/language/sc/admin/settings/notifications.json create mode 100644 public/language/sc/admin/settings/pagination.json create mode 100644 public/language/sc/admin/settings/post.json create mode 100644 public/language/sc/admin/settings/reputation.json create mode 100644 public/language/sc/admin/settings/social.json create mode 100644 public/language/sc/admin/settings/sockets.json create mode 100644 public/language/sc/admin/settings/sounds.json create mode 100644 public/language/sc/admin/settings/tags.json create mode 100644 public/language/sc/admin/settings/uploads.json create mode 100644 public/language/sc/admin/settings/user.json create mode 100644 public/language/sc/admin/settings/web-crawler.json create mode 100644 public/language/sc/category.json create mode 100644 public/language/sc/email.json create mode 100644 public/language/sc/error.json create mode 100644 public/language/sc/flags.json create mode 100644 public/language/sc/global.json create mode 100644 public/language/sc/groups.json create mode 100644 public/language/sc/ip-blacklist.json create mode 100644 public/language/sc/language.json create mode 100644 public/language/sc/login.json create mode 100644 public/language/sc/modules.json create mode 100644 public/language/sc/notifications.json create mode 100644 public/language/sc/pages.json create mode 100644 public/language/sc/post-queue.json create mode 100644 public/language/sc/recent.json create mode 100644 public/language/sc/register.json create mode 100644 public/language/sc/reset_password.json create mode 100644 public/language/sc/search.json create mode 100644 public/language/sc/success.json create mode 100644 public/language/sc/tags.json create mode 100644 public/language/sc/top.json create mode 100644 public/language/sc/topic.json create mode 100644 public/language/sc/unread.json create mode 100644 public/language/sc/uploads.json create mode 100644 public/language/sc/user.json create mode 100644 public/language/sc/users.json create mode 100644 public/language/sk/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/sk/admin/admin.json create mode 100644 public/language/sk/admin/advanced/cache.json create mode 100644 public/language/sk/admin/advanced/database.json create mode 100644 public/language/sk/admin/advanced/errors.json create mode 100644 public/language/sk/admin/advanced/events.json create mode 100644 public/language/sk/admin/advanced/logs.json create mode 100644 public/language/sk/admin/appearance/customise.json create mode 100644 public/language/sk/admin/appearance/skins.json create mode 100644 public/language/sk/admin/appearance/themes.json create mode 100644 public/language/sk/admin/dashboard.json create mode 100644 public/language/sk/admin/development/info.json create mode 100644 public/language/sk/admin/development/logger.json create mode 100644 public/language/sk/admin/extend/plugins.json create mode 100644 public/language/sk/admin/extend/rewards.json create mode 100644 public/language/sk/admin/extend/widgets.json create mode 100644 public/language/sk/admin/manage/admins-mods.json create mode 100644 public/language/sk/admin/manage/categories.json create mode 100644 public/language/sk/admin/manage/digest.json create mode 100644 public/language/sk/admin/manage/groups.json create mode 100644 public/language/sk/admin/manage/privileges.json create mode 100644 public/language/sk/admin/manage/registration.json create mode 100644 public/language/sk/admin/manage/tags.json create mode 100644 public/language/sk/admin/manage/uploads.json create mode 100644 public/language/sk/admin/manage/users.json create mode 100644 public/language/sk/admin/menu.json create mode 100644 public/language/sk/admin/settings/advanced.json create mode 100644 public/language/sk/admin/settings/api.json create mode 100644 public/language/sk/admin/settings/chat.json create mode 100644 public/language/sk/admin/settings/cookies.json create mode 100644 public/language/sk/admin/settings/email.json create mode 100644 public/language/sk/admin/settings/general.json create mode 100644 public/language/sk/admin/settings/group.json create mode 100644 public/language/sk/admin/settings/guest.json create mode 100644 public/language/sk/admin/settings/homepage.json create mode 100644 public/language/sk/admin/settings/languages.json create mode 100644 public/language/sk/admin/settings/navigation.json create mode 100644 public/language/sk/admin/settings/notifications.json create mode 100644 public/language/sk/admin/settings/pagination.json create mode 100644 public/language/sk/admin/settings/post.json create mode 100644 public/language/sk/admin/settings/reputation.json create mode 100644 public/language/sk/admin/settings/social.json create mode 100644 public/language/sk/admin/settings/sockets.json create mode 100644 public/language/sk/admin/settings/sounds.json create mode 100644 public/language/sk/admin/settings/tags.json create mode 100644 public/language/sk/admin/settings/uploads.json create mode 100644 public/language/sk/admin/settings/user.json create mode 100644 public/language/sk/admin/settings/web-crawler.json create mode 100644 public/language/sk/category.json create mode 100644 public/language/sk/email.json create mode 100644 public/language/sk/error.json create mode 100644 public/language/sk/flags.json create mode 100644 public/language/sk/global.json create mode 100644 public/language/sk/groups.json create mode 100644 public/language/sk/ip-blacklist.json create mode 100644 public/language/sk/language.json create mode 100644 public/language/sk/login.json create mode 100644 public/language/sk/modules.json create mode 100644 public/language/sk/notifications.json create mode 100644 public/language/sk/pages.json create mode 100644 public/language/sk/post-queue.json create mode 100644 public/language/sk/recent.json create mode 100644 public/language/sk/register.json create mode 100644 public/language/sk/reset_password.json create mode 100644 public/language/sk/search.json create mode 100644 public/language/sk/success.json create mode 100644 public/language/sk/tags.json create mode 100644 public/language/sk/top.json create mode 100644 public/language/sk/topic.json create mode 100644 public/language/sk/unread.json create mode 100644 public/language/sk/uploads.json create mode 100644 public/language/sk/user.json create mode 100644 public/language/sk/users.json create mode 100644 public/language/sl/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/sl/admin/admin.json create mode 100644 public/language/sl/admin/advanced/cache.json create mode 100644 public/language/sl/admin/advanced/database.json create mode 100644 public/language/sl/admin/advanced/errors.json create mode 100644 public/language/sl/admin/advanced/events.json create mode 100644 public/language/sl/admin/advanced/logs.json create mode 100644 public/language/sl/admin/appearance/customise.json create mode 100644 public/language/sl/admin/appearance/skins.json create mode 100644 public/language/sl/admin/appearance/themes.json create mode 100644 public/language/sl/admin/dashboard.json create mode 100644 public/language/sl/admin/development/info.json create mode 100644 public/language/sl/admin/development/logger.json create mode 100644 public/language/sl/admin/extend/plugins.json create mode 100644 public/language/sl/admin/extend/rewards.json create mode 100644 public/language/sl/admin/extend/widgets.json create mode 100644 public/language/sl/admin/manage/admins-mods.json create mode 100644 public/language/sl/admin/manage/categories.json create mode 100644 public/language/sl/admin/manage/digest.json create mode 100644 public/language/sl/admin/manage/groups.json create mode 100644 public/language/sl/admin/manage/privileges.json create mode 100644 public/language/sl/admin/manage/registration.json create mode 100644 public/language/sl/admin/manage/tags.json create mode 100644 public/language/sl/admin/manage/uploads.json create mode 100644 public/language/sl/admin/manage/users.json create mode 100644 public/language/sl/admin/menu.json create mode 100644 public/language/sl/admin/settings/advanced.json create mode 100644 public/language/sl/admin/settings/api.json create mode 100644 public/language/sl/admin/settings/chat.json create mode 100644 public/language/sl/admin/settings/cookies.json create mode 100644 public/language/sl/admin/settings/email.json create mode 100644 public/language/sl/admin/settings/general.json create mode 100644 public/language/sl/admin/settings/group.json create mode 100644 public/language/sl/admin/settings/guest.json create mode 100644 public/language/sl/admin/settings/homepage.json create mode 100644 public/language/sl/admin/settings/languages.json create mode 100644 public/language/sl/admin/settings/navigation.json create mode 100644 public/language/sl/admin/settings/notifications.json create mode 100644 public/language/sl/admin/settings/pagination.json create mode 100644 public/language/sl/admin/settings/post.json create mode 100644 public/language/sl/admin/settings/reputation.json create mode 100644 public/language/sl/admin/settings/social.json create mode 100644 public/language/sl/admin/settings/sockets.json create mode 100644 public/language/sl/admin/settings/sounds.json create mode 100644 public/language/sl/admin/settings/tags.json create mode 100644 public/language/sl/admin/settings/uploads.json create mode 100644 public/language/sl/admin/settings/user.json create mode 100644 public/language/sl/admin/settings/web-crawler.json create mode 100644 public/language/sl/category.json create mode 100644 public/language/sl/email.json create mode 100644 public/language/sl/error.json create mode 100644 public/language/sl/flags.json create mode 100644 public/language/sl/global.json create mode 100644 public/language/sl/groups.json create mode 100644 public/language/sl/ip-blacklist.json create mode 100644 public/language/sl/language.json create mode 100644 public/language/sl/login.json create mode 100644 public/language/sl/modules.json create mode 100644 public/language/sl/notifications.json create mode 100644 public/language/sl/pages.json create mode 100644 public/language/sl/post-queue.json create mode 100644 public/language/sl/recent.json create mode 100644 public/language/sl/register.json create mode 100644 public/language/sl/reset_password.json create mode 100644 public/language/sl/search.json create mode 100644 public/language/sl/success.json create mode 100644 public/language/sl/tags.json create mode 100644 public/language/sl/top.json create mode 100644 public/language/sl/topic.json create mode 100644 public/language/sl/unread.json create mode 100644 public/language/sl/uploads.json create mode 100644 public/language/sl/user.json create mode 100644 public/language/sl/users.json create mode 100644 public/language/sq-AL/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/sq-AL/admin/admin.json create mode 100644 public/language/sq-AL/admin/advanced/cache.json create mode 100644 public/language/sq-AL/admin/advanced/database.json create mode 100644 public/language/sq-AL/admin/advanced/errors.json create mode 100644 public/language/sq-AL/admin/advanced/events.json create mode 100644 public/language/sq-AL/admin/advanced/logs.json create mode 100644 public/language/sq-AL/admin/appearance/customise.json create mode 100644 public/language/sq-AL/admin/appearance/skins.json create mode 100644 public/language/sq-AL/admin/appearance/themes.json create mode 100644 public/language/sq-AL/admin/dashboard.json create mode 100644 public/language/sq-AL/admin/development/info.json create mode 100644 public/language/sq-AL/admin/development/logger.json create mode 100644 public/language/sq-AL/admin/extend/plugins.json create mode 100644 public/language/sq-AL/admin/extend/rewards.json create mode 100644 public/language/sq-AL/admin/extend/widgets.json create mode 100644 public/language/sq-AL/admin/manage/admins-mods.json create mode 100644 public/language/sq-AL/admin/manage/categories.json create mode 100644 public/language/sq-AL/admin/manage/digest.json create mode 100644 public/language/sq-AL/admin/manage/groups.json create mode 100644 public/language/sq-AL/admin/manage/privileges.json create mode 100644 public/language/sq-AL/admin/manage/registration.json create mode 100644 public/language/sq-AL/admin/manage/tags.json create mode 100644 public/language/sq-AL/admin/manage/uploads.json create mode 100644 public/language/sq-AL/admin/manage/users.json create mode 100644 public/language/sq-AL/admin/menu.json create mode 100644 public/language/sq-AL/admin/settings/advanced.json create mode 100644 public/language/sq-AL/admin/settings/api.json create mode 100644 public/language/sq-AL/admin/settings/chat.json create mode 100644 public/language/sq-AL/admin/settings/cookies.json create mode 100644 public/language/sq-AL/admin/settings/email.json create mode 100644 public/language/sq-AL/admin/settings/general.json create mode 100644 public/language/sq-AL/admin/settings/group.json create mode 100644 public/language/sq-AL/admin/settings/guest.json create mode 100644 public/language/sq-AL/admin/settings/homepage.json create mode 100644 public/language/sq-AL/admin/settings/languages.json create mode 100644 public/language/sq-AL/admin/settings/navigation.json create mode 100644 public/language/sq-AL/admin/settings/notifications.json create mode 100644 public/language/sq-AL/admin/settings/pagination.json create mode 100644 public/language/sq-AL/admin/settings/post.json create mode 100644 public/language/sq-AL/admin/settings/reputation.json create mode 100644 public/language/sq-AL/admin/settings/social.json create mode 100644 public/language/sq-AL/admin/settings/sockets.json create mode 100644 public/language/sq-AL/admin/settings/sounds.json create mode 100644 public/language/sq-AL/admin/settings/tags.json create mode 100644 public/language/sq-AL/admin/settings/uploads.json create mode 100644 public/language/sq-AL/admin/settings/user.json create mode 100644 public/language/sq-AL/admin/settings/web-crawler.json create mode 100644 public/language/sq-AL/category.json create mode 100644 public/language/sq-AL/email.json create mode 100644 public/language/sq-AL/error.json create mode 100644 public/language/sq-AL/flags.json create mode 100644 public/language/sq-AL/global.json create mode 100644 public/language/sq-AL/groups.json create mode 100644 public/language/sq-AL/ip-blacklist.json create mode 100644 public/language/sq-AL/language.json create mode 100644 public/language/sq-AL/login.json create mode 100644 public/language/sq-AL/modules.json create mode 100644 public/language/sq-AL/notifications.json create mode 100644 public/language/sq-AL/pages.json create mode 100644 public/language/sq-AL/post-queue.json create mode 100644 public/language/sq-AL/recent.json create mode 100644 public/language/sq-AL/register.json create mode 100644 public/language/sq-AL/reset_password.json create mode 100644 public/language/sq-AL/search.json create mode 100644 public/language/sq-AL/success.json create mode 100644 public/language/sq-AL/tags.json create mode 100644 public/language/sq-AL/top.json create mode 100644 public/language/sq-AL/topic.json create mode 100644 public/language/sq-AL/unread.json create mode 100644 public/language/sq-AL/uploads.json create mode 100644 public/language/sq-AL/user.json create mode 100644 public/language/sq-AL/users.json create mode 100644 public/language/sr/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/sr/admin/admin.json create mode 100644 public/language/sr/admin/advanced/cache.json create mode 100644 public/language/sr/admin/advanced/database.json create mode 100644 public/language/sr/admin/advanced/errors.json create mode 100644 public/language/sr/admin/advanced/events.json create mode 100644 public/language/sr/admin/advanced/logs.json create mode 100644 public/language/sr/admin/appearance/customise.json create mode 100644 public/language/sr/admin/appearance/skins.json create mode 100644 public/language/sr/admin/appearance/themes.json create mode 100644 public/language/sr/admin/dashboard.json create mode 100644 public/language/sr/admin/development/info.json create mode 100644 public/language/sr/admin/development/logger.json create mode 100644 public/language/sr/admin/extend/plugins.json create mode 100644 public/language/sr/admin/extend/rewards.json create mode 100644 public/language/sr/admin/extend/widgets.json create mode 100644 public/language/sr/admin/manage/admins-mods.json create mode 100644 public/language/sr/admin/manage/categories.json create mode 100644 public/language/sr/admin/manage/digest.json create mode 100644 public/language/sr/admin/manage/groups.json create mode 100644 public/language/sr/admin/manage/privileges.json create mode 100644 public/language/sr/admin/manage/registration.json create mode 100644 public/language/sr/admin/manage/tags.json create mode 100644 public/language/sr/admin/manage/uploads.json create mode 100644 public/language/sr/admin/manage/users.json create mode 100644 public/language/sr/admin/menu.json create mode 100644 public/language/sr/admin/settings/advanced.json create mode 100644 public/language/sr/admin/settings/api.json create mode 100644 public/language/sr/admin/settings/chat.json create mode 100644 public/language/sr/admin/settings/cookies.json create mode 100644 public/language/sr/admin/settings/email.json create mode 100644 public/language/sr/admin/settings/general.json create mode 100644 public/language/sr/admin/settings/group.json create mode 100644 public/language/sr/admin/settings/guest.json create mode 100644 public/language/sr/admin/settings/homepage.json create mode 100644 public/language/sr/admin/settings/languages.json create mode 100644 public/language/sr/admin/settings/navigation.json create mode 100644 public/language/sr/admin/settings/notifications.json create mode 100644 public/language/sr/admin/settings/pagination.json create mode 100644 public/language/sr/admin/settings/post.json create mode 100644 public/language/sr/admin/settings/reputation.json create mode 100644 public/language/sr/admin/settings/social.json create mode 100644 public/language/sr/admin/settings/sockets.json create mode 100644 public/language/sr/admin/settings/sounds.json create mode 100644 public/language/sr/admin/settings/tags.json create mode 100644 public/language/sr/admin/settings/uploads.json create mode 100644 public/language/sr/admin/settings/user.json create mode 100644 public/language/sr/admin/settings/web-crawler.json create mode 100644 public/language/sr/category.json create mode 100644 public/language/sr/email.json create mode 100644 public/language/sr/error.json create mode 100644 public/language/sr/flags.json create mode 100644 public/language/sr/global.json create mode 100644 public/language/sr/groups.json create mode 100644 public/language/sr/ip-blacklist.json create mode 100644 public/language/sr/language.json create mode 100644 public/language/sr/login.json create mode 100644 public/language/sr/modules.json create mode 100644 public/language/sr/notifications.json create mode 100644 public/language/sr/pages.json create mode 100644 public/language/sr/post-queue.json create mode 100644 public/language/sr/recent.json create mode 100644 public/language/sr/register.json create mode 100644 public/language/sr/reset_password.json create mode 100644 public/language/sr/search.json create mode 100644 public/language/sr/success.json create mode 100644 public/language/sr/tags.json create mode 100644 public/language/sr/top.json create mode 100644 public/language/sr/topic.json create mode 100644 public/language/sr/unread.json create mode 100644 public/language/sr/uploads.json create mode 100644 public/language/sr/user.json create mode 100644 public/language/sr/users.json create mode 100644 public/language/sv/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/sv/admin/admin.json create mode 100644 public/language/sv/admin/advanced/cache.json create mode 100644 public/language/sv/admin/advanced/database.json create mode 100644 public/language/sv/admin/advanced/errors.json create mode 100644 public/language/sv/admin/advanced/events.json create mode 100644 public/language/sv/admin/advanced/logs.json create mode 100644 public/language/sv/admin/appearance/customise.json create mode 100644 public/language/sv/admin/appearance/skins.json create mode 100644 public/language/sv/admin/appearance/themes.json create mode 100644 public/language/sv/admin/dashboard.json create mode 100644 public/language/sv/admin/development/info.json create mode 100644 public/language/sv/admin/development/logger.json create mode 100644 public/language/sv/admin/extend/plugins.json create mode 100644 public/language/sv/admin/extend/rewards.json create mode 100644 public/language/sv/admin/extend/widgets.json create mode 100644 public/language/sv/admin/manage/admins-mods.json create mode 100644 public/language/sv/admin/manage/categories.json create mode 100644 public/language/sv/admin/manage/digest.json create mode 100644 public/language/sv/admin/manage/groups.json create mode 100644 public/language/sv/admin/manage/privileges.json create mode 100644 public/language/sv/admin/manage/registration.json create mode 100644 public/language/sv/admin/manage/tags.json create mode 100644 public/language/sv/admin/manage/uploads.json create mode 100644 public/language/sv/admin/manage/users.json create mode 100644 public/language/sv/admin/menu.json create mode 100644 public/language/sv/admin/settings/advanced.json create mode 100644 public/language/sv/admin/settings/api.json create mode 100644 public/language/sv/admin/settings/chat.json create mode 100644 public/language/sv/admin/settings/cookies.json create mode 100644 public/language/sv/admin/settings/email.json create mode 100644 public/language/sv/admin/settings/general.json create mode 100644 public/language/sv/admin/settings/group.json create mode 100644 public/language/sv/admin/settings/guest.json create mode 100644 public/language/sv/admin/settings/homepage.json create mode 100644 public/language/sv/admin/settings/languages.json create mode 100644 public/language/sv/admin/settings/navigation.json create mode 100644 public/language/sv/admin/settings/notifications.json create mode 100644 public/language/sv/admin/settings/pagination.json create mode 100644 public/language/sv/admin/settings/post.json create mode 100644 public/language/sv/admin/settings/reputation.json create mode 100644 public/language/sv/admin/settings/social.json create mode 100644 public/language/sv/admin/settings/sockets.json create mode 100644 public/language/sv/admin/settings/sounds.json create mode 100644 public/language/sv/admin/settings/tags.json create mode 100644 public/language/sv/admin/settings/uploads.json create mode 100644 public/language/sv/admin/settings/user.json create mode 100644 public/language/sv/admin/settings/web-crawler.json create mode 100644 public/language/sv/category.json create mode 100644 public/language/sv/email.json create mode 100644 public/language/sv/error.json create mode 100644 public/language/sv/flags.json create mode 100644 public/language/sv/global.json create mode 100644 public/language/sv/groups.json create mode 100644 public/language/sv/ip-blacklist.json create mode 100644 public/language/sv/language.json create mode 100644 public/language/sv/login.json create mode 100644 public/language/sv/modules.json create mode 100644 public/language/sv/notifications.json create mode 100644 public/language/sv/pages.json create mode 100644 public/language/sv/post-queue.json create mode 100644 public/language/sv/recent.json create mode 100644 public/language/sv/register.json create mode 100644 public/language/sv/reset_password.json create mode 100644 public/language/sv/search.json create mode 100644 public/language/sv/success.json create mode 100644 public/language/sv/tags.json create mode 100644 public/language/sv/top.json create mode 100644 public/language/sv/topic.json create mode 100644 public/language/sv/unread.json create mode 100644 public/language/sv/uploads.json create mode 100644 public/language/sv/user.json create mode 100644 public/language/sv/users.json create mode 100644 public/language/th/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/th/admin/admin.json create mode 100644 public/language/th/admin/advanced/cache.json create mode 100644 public/language/th/admin/advanced/database.json create mode 100644 public/language/th/admin/advanced/errors.json create mode 100644 public/language/th/admin/advanced/events.json create mode 100644 public/language/th/admin/advanced/logs.json create mode 100644 public/language/th/admin/appearance/customise.json create mode 100644 public/language/th/admin/appearance/skins.json create mode 100644 public/language/th/admin/appearance/themes.json create mode 100644 public/language/th/admin/dashboard.json create mode 100644 public/language/th/admin/development/info.json create mode 100644 public/language/th/admin/development/logger.json create mode 100644 public/language/th/admin/extend/plugins.json create mode 100644 public/language/th/admin/extend/rewards.json create mode 100644 public/language/th/admin/extend/widgets.json create mode 100644 public/language/th/admin/manage/admins-mods.json create mode 100644 public/language/th/admin/manage/categories.json create mode 100644 public/language/th/admin/manage/digest.json create mode 100644 public/language/th/admin/manage/groups.json create mode 100644 public/language/th/admin/manage/privileges.json create mode 100644 public/language/th/admin/manage/registration.json create mode 100644 public/language/th/admin/manage/tags.json create mode 100644 public/language/th/admin/manage/uploads.json create mode 100644 public/language/th/admin/manage/users.json create mode 100644 public/language/th/admin/menu.json create mode 100644 public/language/th/admin/settings/advanced.json create mode 100644 public/language/th/admin/settings/api.json create mode 100644 public/language/th/admin/settings/chat.json create mode 100644 public/language/th/admin/settings/cookies.json create mode 100644 public/language/th/admin/settings/email.json create mode 100644 public/language/th/admin/settings/general.json create mode 100644 public/language/th/admin/settings/group.json create mode 100644 public/language/th/admin/settings/guest.json create mode 100644 public/language/th/admin/settings/homepage.json create mode 100644 public/language/th/admin/settings/languages.json create mode 100644 public/language/th/admin/settings/navigation.json create mode 100644 public/language/th/admin/settings/notifications.json create mode 100644 public/language/th/admin/settings/pagination.json create mode 100644 public/language/th/admin/settings/post.json create mode 100644 public/language/th/admin/settings/reputation.json create mode 100644 public/language/th/admin/settings/social.json create mode 100644 public/language/th/admin/settings/sockets.json create mode 100644 public/language/th/admin/settings/sounds.json create mode 100644 public/language/th/admin/settings/tags.json create mode 100644 public/language/th/admin/settings/uploads.json create mode 100644 public/language/th/admin/settings/user.json create mode 100644 public/language/th/admin/settings/web-crawler.json create mode 100644 public/language/th/category.json create mode 100644 public/language/th/email.json create mode 100644 public/language/th/error.json create mode 100644 public/language/th/flags.json create mode 100644 public/language/th/global.json create mode 100644 public/language/th/groups.json create mode 100644 public/language/th/ip-blacklist.json create mode 100644 public/language/th/language.json create mode 100644 public/language/th/login.json create mode 100644 public/language/th/modules.json create mode 100644 public/language/th/notifications.json create mode 100644 public/language/th/pages.json create mode 100644 public/language/th/post-queue.json create mode 100644 public/language/th/recent.json create mode 100644 public/language/th/register.json create mode 100644 public/language/th/reset_password.json create mode 100644 public/language/th/search.json create mode 100644 public/language/th/success.json create mode 100644 public/language/th/tags.json create mode 100644 public/language/th/top.json create mode 100644 public/language/th/topic.json create mode 100644 public/language/th/unread.json create mode 100644 public/language/th/uploads.json create mode 100644 public/language/th/user.json create mode 100644 public/language/th/users.json create mode 100644 public/language/tr/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/tr/admin/admin.json create mode 100644 public/language/tr/admin/advanced/cache.json create mode 100644 public/language/tr/admin/advanced/database.json create mode 100644 public/language/tr/admin/advanced/errors.json create mode 100644 public/language/tr/admin/advanced/events.json create mode 100644 public/language/tr/admin/advanced/logs.json create mode 100644 public/language/tr/admin/appearance/customise.json create mode 100644 public/language/tr/admin/appearance/skins.json create mode 100644 public/language/tr/admin/appearance/themes.json create mode 100644 public/language/tr/admin/dashboard.json create mode 100644 public/language/tr/admin/development/info.json create mode 100644 public/language/tr/admin/development/logger.json create mode 100644 public/language/tr/admin/extend/plugins.json create mode 100644 public/language/tr/admin/extend/rewards.json create mode 100644 public/language/tr/admin/extend/widgets.json create mode 100644 public/language/tr/admin/manage/admins-mods.json create mode 100644 public/language/tr/admin/manage/categories.json create mode 100644 public/language/tr/admin/manage/digest.json create mode 100644 public/language/tr/admin/manage/groups.json create mode 100644 public/language/tr/admin/manage/privileges.json create mode 100644 public/language/tr/admin/manage/registration.json create mode 100644 public/language/tr/admin/manage/tags.json create mode 100644 public/language/tr/admin/manage/uploads.json create mode 100644 public/language/tr/admin/manage/users.json create mode 100644 public/language/tr/admin/menu.json create mode 100644 public/language/tr/admin/settings/advanced.json create mode 100644 public/language/tr/admin/settings/api.json create mode 100644 public/language/tr/admin/settings/chat.json create mode 100644 public/language/tr/admin/settings/cookies.json create mode 100644 public/language/tr/admin/settings/email.json create mode 100644 public/language/tr/admin/settings/general.json create mode 100644 public/language/tr/admin/settings/group.json create mode 100644 public/language/tr/admin/settings/guest.json create mode 100644 public/language/tr/admin/settings/homepage.json create mode 100644 public/language/tr/admin/settings/languages.json create mode 100644 public/language/tr/admin/settings/navigation.json create mode 100644 public/language/tr/admin/settings/notifications.json create mode 100644 public/language/tr/admin/settings/pagination.json create mode 100644 public/language/tr/admin/settings/post.json create mode 100644 public/language/tr/admin/settings/reputation.json create mode 100644 public/language/tr/admin/settings/social.json create mode 100644 public/language/tr/admin/settings/sockets.json create mode 100644 public/language/tr/admin/settings/sounds.json create mode 100644 public/language/tr/admin/settings/tags.json create mode 100644 public/language/tr/admin/settings/uploads.json create mode 100644 public/language/tr/admin/settings/user.json create mode 100644 public/language/tr/admin/settings/web-crawler.json create mode 100644 public/language/tr/category.json create mode 100644 public/language/tr/email.json create mode 100644 public/language/tr/error.json create mode 100644 public/language/tr/flags.json create mode 100644 public/language/tr/global.json create mode 100644 public/language/tr/groups.json create mode 100644 public/language/tr/ip-blacklist.json create mode 100644 public/language/tr/language.json create mode 100644 public/language/tr/login.json create mode 100644 public/language/tr/modules.json create mode 100644 public/language/tr/notifications.json create mode 100644 public/language/tr/pages.json create mode 100644 public/language/tr/post-queue.json create mode 100644 public/language/tr/recent.json create mode 100644 public/language/tr/register.json create mode 100644 public/language/tr/reset_password.json create mode 100644 public/language/tr/search.json create mode 100644 public/language/tr/success.json create mode 100644 public/language/tr/tags.json create mode 100644 public/language/tr/top.json create mode 100644 public/language/tr/topic.json create mode 100644 public/language/tr/unread.json create mode 100644 public/language/tr/uploads.json create mode 100644 public/language/tr/user.json create mode 100644 public/language/tr/users.json create mode 100644 public/language/uk/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/uk/admin/admin.json create mode 100644 public/language/uk/admin/advanced/cache.json create mode 100644 public/language/uk/admin/advanced/database.json create mode 100644 public/language/uk/admin/advanced/errors.json create mode 100644 public/language/uk/admin/advanced/events.json create mode 100644 public/language/uk/admin/advanced/logs.json create mode 100644 public/language/uk/admin/appearance/customise.json create mode 100644 public/language/uk/admin/appearance/skins.json create mode 100644 public/language/uk/admin/appearance/themes.json create mode 100644 public/language/uk/admin/dashboard.json create mode 100644 public/language/uk/admin/development/info.json create mode 100644 public/language/uk/admin/development/logger.json create mode 100644 public/language/uk/admin/extend/plugins.json create mode 100644 public/language/uk/admin/extend/rewards.json create mode 100644 public/language/uk/admin/extend/widgets.json create mode 100644 public/language/uk/admin/manage/admins-mods.json create mode 100644 public/language/uk/admin/manage/categories.json create mode 100644 public/language/uk/admin/manage/digest.json create mode 100644 public/language/uk/admin/manage/groups.json create mode 100644 public/language/uk/admin/manage/privileges.json create mode 100644 public/language/uk/admin/manage/registration.json create mode 100644 public/language/uk/admin/manage/tags.json create mode 100644 public/language/uk/admin/manage/uploads.json create mode 100644 public/language/uk/admin/manage/users.json create mode 100644 public/language/uk/admin/menu.json create mode 100644 public/language/uk/admin/settings/advanced.json create mode 100644 public/language/uk/admin/settings/api.json create mode 100644 public/language/uk/admin/settings/chat.json create mode 100644 public/language/uk/admin/settings/cookies.json create mode 100644 public/language/uk/admin/settings/email.json create mode 100644 public/language/uk/admin/settings/general.json create mode 100644 public/language/uk/admin/settings/group.json create mode 100644 public/language/uk/admin/settings/guest.json create mode 100644 public/language/uk/admin/settings/homepage.json create mode 100644 public/language/uk/admin/settings/languages.json create mode 100644 public/language/uk/admin/settings/navigation.json create mode 100644 public/language/uk/admin/settings/notifications.json create mode 100644 public/language/uk/admin/settings/pagination.json create mode 100644 public/language/uk/admin/settings/post.json create mode 100644 public/language/uk/admin/settings/reputation.json create mode 100644 public/language/uk/admin/settings/social.json create mode 100644 public/language/uk/admin/settings/sockets.json create mode 100644 public/language/uk/admin/settings/sounds.json create mode 100644 public/language/uk/admin/settings/tags.json create mode 100644 public/language/uk/admin/settings/uploads.json create mode 100644 public/language/uk/admin/settings/user.json create mode 100644 public/language/uk/admin/settings/web-crawler.json create mode 100644 public/language/uk/category.json create mode 100644 public/language/uk/email.json create mode 100644 public/language/uk/error.json create mode 100644 public/language/uk/flags.json create mode 100644 public/language/uk/global.json create mode 100644 public/language/uk/groups.json create mode 100644 public/language/uk/ip-blacklist.json create mode 100644 public/language/uk/language.json create mode 100644 public/language/uk/login.json create mode 100644 public/language/uk/modules.json create mode 100644 public/language/uk/notifications.json create mode 100644 public/language/uk/pages.json create mode 100644 public/language/uk/post-queue.json create mode 100644 public/language/uk/recent.json create mode 100644 public/language/uk/register.json create mode 100644 public/language/uk/reset_password.json create mode 100644 public/language/uk/search.json create mode 100644 public/language/uk/success.json create mode 100644 public/language/uk/tags.json create mode 100644 public/language/uk/top.json create mode 100644 public/language/uk/topic.json create mode 100644 public/language/uk/unread.json create mode 100644 public/language/uk/uploads.json create mode 100644 public/language/uk/user.json create mode 100644 public/language/uk/users.json create mode 100644 public/language/vi/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/vi/admin/admin.json create mode 100644 public/language/vi/admin/advanced/cache.json create mode 100644 public/language/vi/admin/advanced/database.json create mode 100644 public/language/vi/admin/advanced/errors.json create mode 100644 public/language/vi/admin/advanced/events.json create mode 100644 public/language/vi/admin/advanced/logs.json create mode 100644 public/language/vi/admin/appearance/customise.json create mode 100644 public/language/vi/admin/appearance/skins.json create mode 100644 public/language/vi/admin/appearance/themes.json create mode 100644 public/language/vi/admin/dashboard.json create mode 100644 public/language/vi/admin/development/info.json create mode 100644 public/language/vi/admin/development/logger.json create mode 100644 public/language/vi/admin/extend/plugins.json create mode 100644 public/language/vi/admin/extend/rewards.json create mode 100644 public/language/vi/admin/extend/widgets.json create mode 100644 public/language/vi/admin/manage/admins-mods.json create mode 100644 public/language/vi/admin/manage/categories.json create mode 100644 public/language/vi/admin/manage/digest.json create mode 100644 public/language/vi/admin/manage/groups.json create mode 100644 public/language/vi/admin/manage/privileges.json create mode 100644 public/language/vi/admin/manage/registration.json create mode 100644 public/language/vi/admin/manage/tags.json create mode 100644 public/language/vi/admin/manage/uploads.json create mode 100644 public/language/vi/admin/manage/users.json create mode 100644 public/language/vi/admin/menu.json create mode 100644 public/language/vi/admin/settings/advanced.json create mode 100644 public/language/vi/admin/settings/api.json create mode 100644 public/language/vi/admin/settings/chat.json create mode 100644 public/language/vi/admin/settings/cookies.json create mode 100644 public/language/vi/admin/settings/email.json create mode 100644 public/language/vi/admin/settings/general.json create mode 100644 public/language/vi/admin/settings/group.json create mode 100644 public/language/vi/admin/settings/guest.json create mode 100644 public/language/vi/admin/settings/homepage.json create mode 100644 public/language/vi/admin/settings/languages.json create mode 100644 public/language/vi/admin/settings/navigation.json create mode 100644 public/language/vi/admin/settings/notifications.json create mode 100644 public/language/vi/admin/settings/pagination.json create mode 100644 public/language/vi/admin/settings/post.json create mode 100644 public/language/vi/admin/settings/reputation.json create mode 100644 public/language/vi/admin/settings/social.json create mode 100644 public/language/vi/admin/settings/sockets.json create mode 100644 public/language/vi/admin/settings/sounds.json create mode 100644 public/language/vi/admin/settings/tags.json create mode 100644 public/language/vi/admin/settings/uploads.json create mode 100644 public/language/vi/admin/settings/user.json create mode 100644 public/language/vi/admin/settings/web-crawler.json create mode 100644 public/language/vi/category.json create mode 100644 public/language/vi/email.json create mode 100644 public/language/vi/error.json create mode 100644 public/language/vi/flags.json create mode 100644 public/language/vi/global.json create mode 100644 public/language/vi/groups.json create mode 100644 public/language/vi/ip-blacklist.json create mode 100644 public/language/vi/language.json create mode 100644 public/language/vi/login.json create mode 100644 public/language/vi/modules.json create mode 100644 public/language/vi/notifications.json create mode 100644 public/language/vi/pages.json create mode 100644 public/language/vi/post-queue.json create mode 100644 public/language/vi/recent.json create mode 100644 public/language/vi/register.json create mode 100644 public/language/vi/reset_password.json create mode 100644 public/language/vi/search.json create mode 100644 public/language/vi/success.json create mode 100644 public/language/vi/tags.json create mode 100644 public/language/vi/top.json create mode 100644 public/language/vi/topic.json create mode 100644 public/language/vi/unread.json create mode 100644 public/language/vi/uploads.json create mode 100644 public/language/vi/user.json create mode 100644 public/language/vi/users.json create mode 100644 public/language/zh-CN/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/zh-CN/admin/admin.json create mode 100644 public/language/zh-CN/admin/advanced/cache.json create mode 100644 public/language/zh-CN/admin/advanced/database.json create mode 100644 public/language/zh-CN/admin/advanced/errors.json create mode 100644 public/language/zh-CN/admin/advanced/events.json create mode 100644 public/language/zh-CN/admin/advanced/logs.json create mode 100644 public/language/zh-CN/admin/appearance/customise.json create mode 100644 public/language/zh-CN/admin/appearance/skins.json create mode 100644 public/language/zh-CN/admin/appearance/themes.json create mode 100644 public/language/zh-CN/admin/dashboard.json create mode 100644 public/language/zh-CN/admin/development/info.json create mode 100644 public/language/zh-CN/admin/development/logger.json create mode 100644 public/language/zh-CN/admin/extend/plugins.json create mode 100644 public/language/zh-CN/admin/extend/rewards.json create mode 100644 public/language/zh-CN/admin/extend/widgets.json create mode 100644 public/language/zh-CN/admin/manage/admins-mods.json create mode 100644 public/language/zh-CN/admin/manage/categories.json create mode 100644 public/language/zh-CN/admin/manage/digest.json create mode 100644 public/language/zh-CN/admin/manage/groups.json create mode 100644 public/language/zh-CN/admin/manage/privileges.json create mode 100644 public/language/zh-CN/admin/manage/registration.json create mode 100644 public/language/zh-CN/admin/manage/tags.json create mode 100644 public/language/zh-CN/admin/manage/uploads.json create mode 100644 public/language/zh-CN/admin/manage/users.json create mode 100644 public/language/zh-CN/admin/menu.json create mode 100644 public/language/zh-CN/admin/settings/advanced.json create mode 100644 public/language/zh-CN/admin/settings/api.json create mode 100644 public/language/zh-CN/admin/settings/chat.json create mode 100644 public/language/zh-CN/admin/settings/cookies.json create mode 100644 public/language/zh-CN/admin/settings/email.json create mode 100644 public/language/zh-CN/admin/settings/general.json create mode 100644 public/language/zh-CN/admin/settings/group.json create mode 100644 public/language/zh-CN/admin/settings/guest.json create mode 100644 public/language/zh-CN/admin/settings/homepage.json create mode 100644 public/language/zh-CN/admin/settings/languages.json create mode 100644 public/language/zh-CN/admin/settings/navigation.json create mode 100644 public/language/zh-CN/admin/settings/notifications.json create mode 100644 public/language/zh-CN/admin/settings/pagination.json create mode 100644 public/language/zh-CN/admin/settings/post.json create mode 100644 public/language/zh-CN/admin/settings/reputation.json create mode 100644 public/language/zh-CN/admin/settings/social.json create mode 100644 public/language/zh-CN/admin/settings/sockets.json create mode 100644 public/language/zh-CN/admin/settings/sounds.json create mode 100644 public/language/zh-CN/admin/settings/tags.json create mode 100644 public/language/zh-CN/admin/settings/uploads.json create mode 100644 public/language/zh-CN/admin/settings/user.json create mode 100644 public/language/zh-CN/admin/settings/web-crawler.json create mode 100644 public/language/zh-CN/category.json create mode 100644 public/language/zh-CN/email.json create mode 100644 public/language/zh-CN/error.json create mode 100644 public/language/zh-CN/flags.json create mode 100644 public/language/zh-CN/global.json create mode 100644 public/language/zh-CN/groups.json create mode 100644 public/language/zh-CN/ip-blacklist.json create mode 100644 public/language/zh-CN/language.json create mode 100644 public/language/zh-CN/login.json create mode 100644 public/language/zh-CN/modules.json create mode 100644 public/language/zh-CN/notifications.json create mode 100644 public/language/zh-CN/pages.json create mode 100644 public/language/zh-CN/post-queue.json create mode 100644 public/language/zh-CN/recent.json create mode 100644 public/language/zh-CN/register.json create mode 100644 public/language/zh-CN/reset_password.json create mode 100644 public/language/zh-CN/search.json create mode 100644 public/language/zh-CN/success.json create mode 100644 public/language/zh-CN/tags.json create mode 100644 public/language/zh-CN/top.json create mode 100644 public/language/zh-CN/topic.json create mode 100644 public/language/zh-CN/unread.json create mode 100644 public/language/zh-CN/uploads.json create mode 100644 public/language/zh-CN/user.json create mode 100644 public/language/zh-CN/users.json create mode 100644 public/language/zh-TW/_DO_NOT_EDIT_FILES_HERE.md create mode 100644 public/language/zh-TW/admin/admin.json create mode 100644 public/language/zh-TW/admin/advanced/cache.json create mode 100644 public/language/zh-TW/admin/advanced/database.json create mode 100644 public/language/zh-TW/admin/advanced/errors.json create mode 100644 public/language/zh-TW/admin/advanced/events.json create mode 100644 public/language/zh-TW/admin/advanced/logs.json create mode 100644 public/language/zh-TW/admin/appearance/customise.json create mode 100644 public/language/zh-TW/admin/appearance/skins.json create mode 100644 public/language/zh-TW/admin/appearance/themes.json create mode 100644 public/language/zh-TW/admin/dashboard.json create mode 100644 public/language/zh-TW/admin/development/info.json create mode 100644 public/language/zh-TW/admin/development/logger.json create mode 100644 public/language/zh-TW/admin/extend/plugins.json create mode 100644 public/language/zh-TW/admin/extend/rewards.json create mode 100644 public/language/zh-TW/admin/extend/widgets.json create mode 100644 public/language/zh-TW/admin/manage/admins-mods.json create mode 100644 public/language/zh-TW/admin/manage/categories.json create mode 100644 public/language/zh-TW/admin/manage/digest.json create mode 100644 public/language/zh-TW/admin/manage/groups.json create mode 100644 public/language/zh-TW/admin/manage/privileges.json create mode 100644 public/language/zh-TW/admin/manage/registration.json create mode 100644 public/language/zh-TW/admin/manage/tags.json create mode 100644 public/language/zh-TW/admin/manage/uploads.json create mode 100644 public/language/zh-TW/admin/manage/users.json create mode 100644 public/language/zh-TW/admin/menu.json create mode 100644 public/language/zh-TW/admin/settings/advanced.json create mode 100644 public/language/zh-TW/admin/settings/api.json create mode 100644 public/language/zh-TW/admin/settings/chat.json create mode 100644 public/language/zh-TW/admin/settings/cookies.json create mode 100644 public/language/zh-TW/admin/settings/email.json create mode 100644 public/language/zh-TW/admin/settings/general.json create mode 100644 public/language/zh-TW/admin/settings/group.json create mode 100644 public/language/zh-TW/admin/settings/guest.json create mode 100644 public/language/zh-TW/admin/settings/homepage.json create mode 100644 public/language/zh-TW/admin/settings/languages.json create mode 100644 public/language/zh-TW/admin/settings/navigation.json create mode 100644 public/language/zh-TW/admin/settings/notifications.json create mode 100644 public/language/zh-TW/admin/settings/pagination.json create mode 100644 public/language/zh-TW/admin/settings/post.json create mode 100644 public/language/zh-TW/admin/settings/reputation.json create mode 100644 public/language/zh-TW/admin/settings/social.json create mode 100644 public/language/zh-TW/admin/settings/sockets.json create mode 100644 public/language/zh-TW/admin/settings/sounds.json create mode 100644 public/language/zh-TW/admin/settings/tags.json create mode 100644 public/language/zh-TW/admin/settings/uploads.json create mode 100644 public/language/zh-TW/admin/settings/user.json create mode 100644 public/language/zh-TW/admin/settings/web-crawler.json create mode 100644 public/language/zh-TW/category.json create mode 100644 public/language/zh-TW/email.json create mode 100644 public/language/zh-TW/error.json create mode 100644 public/language/zh-TW/flags.json create mode 100644 public/language/zh-TW/global.json create mode 100644 public/language/zh-TW/groups.json create mode 100644 public/language/zh-TW/ip-blacklist.json create mode 100644 public/language/zh-TW/language.json create mode 100644 public/language/zh-TW/login.json create mode 100644 public/language/zh-TW/modules.json create mode 100644 public/language/zh-TW/notifications.json create mode 100644 public/language/zh-TW/pages.json create mode 100644 public/language/zh-TW/post-queue.json create mode 100644 public/language/zh-TW/recent.json create mode 100644 public/language/zh-TW/register.json create mode 100644 public/language/zh-TW/reset_password.json create mode 100644 public/language/zh-TW/search.json create mode 100644 public/language/zh-TW/success.json create mode 100644 public/language/zh-TW/tags.json create mode 100644 public/language/zh-TW/top.json create mode 100644 public/language/zh-TW/topic.json create mode 100644 public/language/zh-TW/unread.json create mode 100644 public/language/zh-TW/uploads.json create mode 100644 public/language/zh-TW/user.json create mode 100644 public/language/zh-TW/users.json create mode 100644 public/less/admin/admin.less create mode 100644 public/less/admin/advanced/database.less create mode 100644 public/less/admin/advanced/errors.less create mode 100644 public/less/admin/advanced/events.less create mode 100644 public/less/admin/advanced/hooks.less create mode 100644 public/less/admin/advanced/logs.less create mode 100644 public/less/admin/appearance/customise.less create mode 100644 public/less/admin/appearance/themes.less create mode 100644 public/less/admin/development/info.less create mode 100644 public/less/admin/extend/plugins.less create mode 100644 public/less/admin/extend/rewards.less create mode 100644 public/less/admin/extend/widgets.less create mode 100644 public/less/admin/general/dashboard.less create mode 100644 public/less/admin/general/navigation.less create mode 100644 public/less/admin/header.less create mode 100644 public/less/admin/manage/admins-mods.less create mode 100644 public/less/admin/manage/categories.less create mode 100644 public/less/admin/manage/groups.less create mode 100644 public/less/admin/manage/privileges.less create mode 100644 public/less/admin/manage/registration.less create mode 100644 public/less/admin/manage/tags.less create mode 100644 public/less/admin/manage/users.less create mode 100644 public/less/admin/mixins.less create mode 100644 public/less/admin/mobile.less create mode 100644 public/less/admin/modules/alerts.less create mode 100644 public/less/admin/modules/nprogress.less create mode 100644 public/less/admin/modules/search.less create mode 100644 public/less/admin/modules/selectable.less create mode 100644 public/less/admin/paper/bootswatch.less create mode 100644 public/less/admin/paper/variables.less create mode 100644 public/less/admin/settings.less create mode 100644 public/less/admin/vars.less create mode 100644 public/less/flags.less create mode 100644 public/less/generics.less create mode 100644 public/less/global.less create mode 100644 public/less/install.less create mode 100644 public/less/jquery-ui.less create mode 100644 public/less/mixins.less create mode 100644 public/less/modals.less create mode 100644 public/openapi/components/responses/400.yaml create mode 100644 public/openapi/components/responses/401.yaml create mode 100644 public/openapi/components/responses/403.yaml create mode 100644 public/openapi/components/responses/404.yaml create mode 100644 public/openapi/components/responses/426.yaml create mode 100644 public/openapi/components/responses/500.yaml create mode 100644 public/openapi/components/schemas/Breadcrumbs.yaml create mode 100644 public/openapi/components/schemas/CategoryObject.yaml create mode 100644 public/openapi/components/schemas/Chats.yaml create mode 100644 public/openapi/components/schemas/CommonProps.yaml create mode 100644 public/openapi/components/schemas/Error.yaml create mode 100644 public/openapi/components/schemas/FlagObject.yaml create mode 100644 public/openapi/components/schemas/GroupObject.yaml create mode 100644 public/openapi/components/schemas/Pagination.yaml create mode 100644 public/openapi/components/schemas/PostObject.yaml create mode 100644 public/openapi/components/schemas/PostsObject.yaml create mode 100644 public/openapi/components/schemas/SettingsObj.yaml create mode 100644 public/openapi/components/schemas/Status.yaml create mode 100644 public/openapi/components/schemas/TagObject.yaml create mode 100644 public/openapi/components/schemas/TopicObject.yaml create mode 100644 public/openapi/components/schemas/UserObj.yaml create mode 100644 public/openapi/components/schemas/UserObject.yaml create mode 100644 public/openapi/components/schemas/admin/dashboard.yaml create mode 100644 public/openapi/read.yaml create mode 100644 public/openapi/read/admin.yaml create mode 100644 public/openapi/read/admin/advanced/cache.yaml create mode 100644 public/openapi/read/admin/advanced/cache/dump.yaml create mode 100644 public/openapi/read/admin/advanced/database.yaml create mode 100644 public/openapi/read/admin/advanced/errors.yaml create mode 100644 public/openapi/read/admin/advanced/errors/export.yaml create mode 100644 public/openapi/read/admin/advanced/events.yaml create mode 100644 public/openapi/read/admin/advanced/hooks.yaml create mode 100644 public/openapi/read/admin/advanced/logs.yaml create mode 100644 public/openapi/read/admin/analytics.yaml create mode 100644 public/openapi/read/admin/appearance/term.yaml create mode 100644 public/openapi/read/admin/category/uploadpicture.yaml create mode 100644 public/openapi/read/admin/dashboard.yaml create mode 100644 public/openapi/read/admin/dashboard/logins.yaml create mode 100644 public/openapi/read/admin/dashboard/searches.yaml create mode 100644 public/openapi/read/admin/dashboard/topics.yaml create mode 100644 public/openapi/read/admin/dashboard/users.yaml create mode 100644 public/openapi/read/admin/development/info.yaml create mode 100644 public/openapi/read/admin/development/logger.yaml create mode 100644 public/openapi/read/admin/extend/plugins.yaml create mode 100644 public/openapi/read/admin/extend/rewards.yaml create mode 100644 public/openapi/read/admin/extend/widgets.yaml create mode 100644 public/openapi/read/admin/groups/groupname/csv.yaml create mode 100644 public/openapi/read/admin/manage/admins-mods.yaml create mode 100644 public/openapi/read/admin/manage/categories.yaml create mode 100644 public/openapi/read/admin/manage/categories/category_id.yaml create mode 100644 public/openapi/read/admin/manage/categories/category_id/analytics.yaml create mode 100644 public/openapi/read/admin/manage/digest.yaml create mode 100644 public/openapi/read/admin/manage/groups.yaml create mode 100644 public/openapi/read/admin/manage/groups/name.yaml create mode 100644 public/openapi/read/admin/manage/privileges/cid.yaml create mode 100644 public/openapi/read/admin/manage/registration.yaml create mode 100644 public/openapi/read/admin/manage/tags.yaml create mode 100644 public/openapi/read/admin/manage/uploads.yaml create mode 100644 public/openapi/read/admin/manage/users.yaml create mode 100644 public/openapi/read/admin/settings/advanced.yaml create mode 100644 public/openapi/read/admin/settings/email.yaml create mode 100644 public/openapi/read/admin/settings/homepage.yaml create mode 100644 public/openapi/read/admin/settings/languages.yaml create mode 100644 public/openapi/read/admin/settings/navigation.yaml create mode 100644 public/openapi/read/admin/settings/post.yaml create mode 100644 public/openapi/read/admin/settings/social.yaml create mode 100644 public/openapi/read/admin/settings/term.yaml create mode 100644 public/openapi/read/admin/settings/user.yaml create mode 100644 public/openapi/read/admin/upload/file.yaml create mode 100644 public/openapi/read/admin/uploadDefaultAvatar.yaml create mode 100644 public/openapi/read/admin/uploadMaskableIcon.yaml create mode 100644 public/openapi/read/admin/uploadOgImage.yaml create mode 100644 public/openapi/read/admin/uploadTouchIcon.yaml create mode 100644 public/openapi/read/admin/uploadfavicon.yaml create mode 100644 public/openapi/read/admin/uploadlogo.yaml create mode 100644 public/openapi/read/admin/users/csv.yaml create mode 100644 public/openapi/read/career.yaml create mode 100644 public/openapi/read/categories.yaml create mode 100644 public/openapi/read/categories/cid/moderators.yaml create mode 100644 public/openapi/read/category/category_id.yaml create mode 100644 public/openapi/read/chats/roomid.yaml create mode 100644 public/openapi/read/config.yaml create mode 100644 public/openapi/read/confirm/code.yaml create mode 100644 public/openapi/read/email/unsubscribe/token.yaml create mode 100644 public/openapi/read/flags.yaml create mode 100644 public/openapi/read/flags/flagId.yaml create mode 100644 public/openapi/read/groups.yaml create mode 100644 public/openapi/read/groups/slug.yaml create mode 100644 public/openapi/read/groups/slug/members.yaml create mode 100644 public/openapi/read/index.yaml create mode 100644 public/openapi/read/ip-blacklist.yaml create mode 100644 public/openapi/read/login.yaml create mode 100644 public/openapi/read/me.yaml create mode 100644 public/openapi/read/notifications.yaml create mode 100644 public/openapi/read/outgoing.yaml create mode 100644 public/openapi/read/popular.yaml create mode 100644 public/openapi/read/post-queue.yaml create mode 100644 public/openapi/read/post/pid.yaml create mode 100644 public/openapi/read/post/upload.yaml create mode 100644 public/openapi/read/recent.yaml create mode 100644 public/openapi/read/recent/posts/term.yaml create mode 100644 public/openapi/read/register.yaml create mode 100644 public/openapi/read/register/complete.yaml create mode 100644 public/openapi/read/registration-queue.yaml create mode 100644 public/openapi/read/reset.yaml create mode 100644 public/openapi/read/reset/code.yaml create mode 100644 public/openapi/read/search.yaml create mode 100644 public/openapi/read/self.yaml create mode 100644 public/openapi/read/tags.yaml create mode 100644 public/openapi/read/tags/tag.yaml create mode 100644 public/openapi/read/top.yaml create mode 100644 public/openapi/read/topic/pagination/topic_id.yaml create mode 100644 public/openapi/read/topic/teaser/topic_id.yaml create mode 100644 public/openapi/read/topic/thumb/upload.yaml create mode 100644 public/openapi/read/topic/topic_id.yaml create mode 100644 public/openapi/read/tos.yaml create mode 100644 public/openapi/read/uid/uid.yaml create mode 100644 public/openapi/read/unread.yaml create mode 100644 public/openapi/read/unread/total.yaml create mode 100644 public/openapi/read/user/email/email.yaml create mode 100644 public/openapi/read/user/uid/uid.yaml create mode 100644 public/openapi/read/user/uid/userslug/export/type.yaml create mode 100644 public/openapi/read/user/username/username.yaml create mode 100644 public/openapi/read/user/userslug.yaml create mode 100644 public/openapi/read/user/userslug/best.yaml create mode 100644 public/openapi/read/user/userslug/blocks.yaml create mode 100644 public/openapi/read/user/userslug/bookmarks.yaml create mode 100644 public/openapi/read/user/userslug/categories.yaml create mode 100644 public/openapi/read/user/userslug/chats/roomid.yaml create mode 100644 public/openapi/read/user/userslug/consent.yaml create mode 100644 public/openapi/read/user/userslug/controversial.yaml create mode 100644 public/openapi/read/user/userslug/downvoted.yaml create mode 100644 public/openapi/read/user/userslug/edit.yaml create mode 100644 public/openapi/read/user/userslug/edit/email.yaml create mode 100644 public/openapi/read/user/userslug/edit/password.yaml create mode 100644 public/openapi/read/user/userslug/edit/username.yaml create mode 100644 public/openapi/read/user/userslug/export/posts.yaml create mode 100644 public/openapi/read/user/userslug/export/profile.yaml create mode 100644 public/openapi/read/user/userslug/export/uploads.yaml create mode 100644 public/openapi/read/user/userslug/followers.yaml create mode 100644 public/openapi/read/user/userslug/following.yaml create mode 100644 public/openapi/read/user/userslug/groups.yaml create mode 100644 public/openapi/read/user/userslug/ignored.yaml create mode 100644 public/openapi/read/user/userslug/info.yaml create mode 100644 public/openapi/read/user/userslug/posts.yaml create mode 100644 public/openapi/read/user/userslug/session/uuid.yaml create mode 100644 public/openapi/read/user/userslug/sessions.yaml create mode 100644 public/openapi/read/user/userslug/settings.yaml create mode 100644 public/openapi/read/user/userslug/topics.yaml create mode 100644 public/openapi/read/user/userslug/uploads.yaml create mode 100644 public/openapi/read/user/userslug/upvoted.yaml create mode 100644 public/openapi/read/user/userslug/watched.yaml create mode 100644 public/openapi/read/users.yaml create mode 100644 public/openapi/write.yaml create mode 100644 public/openapi/write/admin/analytics.yaml create mode 100644 public/openapi/write/admin/analytics/set.yaml create mode 100644 public/openapi/write/admin/settings/setting.yaml create mode 100644 public/openapi/write/categories.yaml create mode 100644 public/openapi/write/categories/cid.yaml create mode 100644 public/openapi/write/categories/cid/moderator/uid.yaml create mode 100644 public/openapi/write/categories/cid/privileges.yaml create mode 100644 public/openapi/write/categories/cid/privileges/privilege.yaml create mode 100644 public/openapi/write/chats.yaml create mode 100644 public/openapi/write/chats/roomId.yaml create mode 100644 public/openapi/write/chats/roomId/messages.yaml create mode 100644 public/openapi/write/chats/roomId/messages/mid.yaml create mode 100644 public/openapi/write/chats/roomId/users.yaml create mode 100644 public/openapi/write/chats/roomId/users/uid.yaml create mode 100644 public/openapi/write/files.yaml create mode 100644 public/openapi/write/files/folder.yaml create mode 100644 public/openapi/write/flags.yaml create mode 100644 public/openapi/write/flags/flagId.yaml create mode 100644 public/openapi/write/flags/flagId/notes.yaml create mode 100644 public/openapi/write/flags/flagId/notes/datetime.yaml create mode 100644 public/openapi/write/groups.yaml create mode 100644 public/openapi/write/groups/slug.yaml create mode 100644 public/openapi/write/groups/slug/membership/uid.yaml create mode 100644 public/openapi/write/groups/slug/ownership/uid.yaml create mode 100644 public/openapi/write/login.yaml create mode 100644 public/openapi/write/ping.yaml create mode 100644 public/openapi/write/posts/pid.yaml create mode 100644 public/openapi/write/posts/pid/bookmark.yaml create mode 100644 public/openapi/write/posts/pid/diffs.yaml create mode 100644 public/openapi/write/posts/pid/diffs/since.yaml create mode 100644 public/openapi/write/posts/pid/diffs/timestamp.yaml create mode 100644 public/openapi/write/posts/pid/move.yaml create mode 100644 public/openapi/write/posts/pid/state.yaml create mode 100644 public/openapi/write/posts/pid/vote.yaml create mode 100644 public/openapi/write/topics.yaml create mode 100644 public/openapi/write/topics/tid.yaml create mode 100644 public/openapi/write/topics/tid/events.yaml create mode 100644 public/openapi/write/topics/tid/events/eventId.yaml create mode 100644 public/openapi/write/topics/tid/follow.yaml create mode 100644 public/openapi/write/topics/tid/ignore.yaml create mode 100644 public/openapi/write/topics/tid/lock.yaml create mode 100644 public/openapi/write/topics/tid/pin.yaml create mode 100644 public/openapi/write/topics/tid/state.yaml create mode 100644 public/openapi/write/topics/tid/tags.yaml create mode 100644 public/openapi/write/topics/tid/thumbs.yaml create mode 100644 public/openapi/write/topics/tid/thumbs/order.yaml create mode 100644 public/openapi/write/users.yaml create mode 100644 public/openapi/write/users/uid.yaml create mode 100644 public/openapi/write/users/uid/account.yaml create mode 100644 public/openapi/write/users/uid/ban.yaml create mode 100644 public/openapi/write/users/uid/content.yaml create mode 100644 public/openapi/write/users/uid/emails.yaml create mode 100644 public/openapi/write/users/uid/emails/email.yaml create mode 100644 public/openapi/write/users/uid/emails/email/confirm.yaml create mode 100644 public/openapi/write/users/uid/exports/type.yaml create mode 100644 public/openapi/write/users/uid/follow.yaml create mode 100644 public/openapi/write/users/uid/invites.yaml create mode 100644 public/openapi/write/users/uid/invites/groups.yaml create mode 100644 public/openapi/write/users/uid/mute.yaml create mode 100644 public/openapi/write/users/uid/password.yaml create mode 100644 public/openapi/write/users/uid/picture.yaml create mode 100644 public/openapi/write/users/uid/sessions/uuid.yaml create mode 100644 public/openapi/write/users/uid/settings.yaml create mode 100644 public/openapi/write/users/uid/tokens.yaml create mode 100644 public/openapi/write/users/uid/tokens/token.yaml create mode 100644 public/src/admin/.eslintrc create mode 100644 public/src/admin/admin.js create mode 100644 public/src/admin/advanced/cache.js create mode 100644 public/src/admin/advanced/errors.js create mode 100644 public/src/admin/advanced/events.js create mode 100644 public/src/admin/advanced/logs.js create mode 100644 public/src/admin/appearance/customise.js create mode 100644 public/src/admin/appearance/skins.js create mode 100644 public/src/admin/appearance/themes.js create mode 100644 public/src/admin/dashboard.js create mode 100644 public/src/admin/dashboard/logins.js create mode 100644 public/src/admin/dashboard/topics.js create mode 100644 public/src/admin/dashboard/users.js create mode 100644 public/src/admin/extend/plugins.js create mode 100644 public/src/admin/extend/rewards.js create mode 100644 public/src/admin/extend/widgets.js create mode 100644 public/src/admin/manage/admins-mods.js create mode 100644 public/src/admin/manage/categories.js create mode 100644 public/src/admin/manage/category-analytics.js create mode 100644 public/src/admin/manage/category.js create mode 100644 public/src/admin/manage/digest.js create mode 100644 public/src/admin/manage/group.js create mode 100644 public/src/admin/manage/groups.js create mode 100644 public/src/admin/manage/privileges.js create mode 100644 public/src/admin/manage/registration.js create mode 100644 public/src/admin/manage/tags.js create mode 100644 public/src/admin/manage/uploads.js create mode 100644 public/src/admin/manage/users.js create mode 100644 public/src/admin/modules/checkboxRowSelector.js create mode 100644 public/src/admin/modules/colorpicker.js create mode 100644 public/src/admin/modules/dashboard-line-graph.js create mode 100644 public/src/admin/modules/instance.js create mode 100644 public/src/admin/modules/search.js create mode 100644 public/src/admin/modules/selectable.js create mode 100644 public/src/admin/settings.js create mode 100644 public/src/admin/settings/api.js create mode 100644 public/src/admin/settings/cookies.js create mode 100644 public/src/admin/settings/email.js create mode 100644 public/src/admin/settings/general.js create mode 100644 public/src/admin/settings/homepage.js create mode 100644 public/src/admin/settings/navigation.js create mode 100644 public/src/admin/settings/notifications.js create mode 100644 public/src/admin/settings/social.js create mode 100644 public/src/ajaxify.js create mode 100644 public/src/app.js create mode 100644 public/src/client.js create mode 100644 public/src/client/account/best.js create mode 100644 public/src/client/account/blocks.js create mode 100644 public/src/client/account/bookmarks.js create mode 100644 public/src/client/account/categories.js create mode 100644 public/src/client/account/consent.js create mode 100644 public/src/client/account/downvoted.js create mode 100644 public/src/client/account/edit.js create mode 100644 public/src/client/account/edit/password.js create mode 100644 public/src/client/account/edit/username.js create mode 100644 public/src/client/account/followers.js create mode 100644 public/src/client/account/following.js create mode 100644 public/src/client/account/groups.js create mode 100644 public/src/client/account/header.js create mode 100644 public/src/client/account/ignored.js create mode 100644 public/src/client/account/info.js create mode 100644 public/src/client/account/posts.js create mode 100644 public/src/client/account/profile.js create mode 100644 public/src/client/account/sessions.js create mode 100644 public/src/client/account/settings.js create mode 100644 public/src/client/account/topics.js create mode 100644 public/src/client/account/uploads.js create mode 100644 public/src/client/account/upvoted.js create mode 100644 public/src/client/account/watched.js create mode 100644 public/src/client/categories.js create mode 100644 public/src/client/category.js create mode 100644 public/src/client/category/tools.js create mode 100644 public/src/client/chats.js create mode 100644 public/src/client/chats/messages.js create mode 100644 public/src/client/chats/recent.js create mode 100644 public/src/client/chats/search.js create mode 100644 public/src/client/compose.js create mode 100644 public/src/client/flags/detail.js create mode 100644 public/src/client/flags/list.js create mode 100644 public/src/client/groups/details.js create mode 100644 public/src/client/groups/list.js create mode 100644 public/src/client/groups/memberlist.js create mode 100644 public/src/client/header.js create mode 100644 public/src/client/header/chat.js create mode 100644 public/src/client/header/notifications.js create mode 100644 public/src/client/header/unread.js create mode 100644 public/src/client/infinitescroll.js create mode 100644 public/src/client/ip-blacklist.js create mode 100644 public/src/client/login.js create mode 100644 public/src/client/notifications.js create mode 100644 public/src/client/pagination.js create mode 100644 public/src/client/popular.js create mode 100644 public/src/client/post-queue.js create mode 100644 public/src/client/recent.js create mode 100644 public/src/client/register.js create mode 100644 public/src/client/reset.js create mode 100644 public/src/client/reset_code.js create mode 100644 public/src/client/search.js create mode 100644 public/src/client/tag.js create mode 100644 public/src/client/tags.js create mode 100644 public/src/client/top.js create mode 100644 public/src/client/topic.js create mode 100644 public/src/client/topic/change-owner.js create mode 100644 public/src/client/topic/delete-posts.js create mode 100644 public/src/client/topic/diffs.js create mode 100644 public/src/client/topic/events.js create mode 100644 public/src/client/topic/fork.js create mode 100644 public/src/client/topic/images.js create mode 100644 public/src/client/topic/merge.js create mode 100644 public/src/client/topic/move-post.js create mode 100644 public/src/client/topic/move.js create mode 100644 public/src/client/topic/postTools.js create mode 100644 public/src/client/topic/posts.js create mode 100644 public/src/client/topic/replies.js create mode 100644 public/src/client/topic/threadTools.js create mode 100644 public/src/client/topic/votes.js create mode 100644 public/src/client/unread.js create mode 100644 public/src/client/users.js create mode 100644 public/src/installer/install.js create mode 100644 public/src/modules/accounts/delete.js create mode 100644 public/src/modules/accounts/invite.js create mode 100644 public/src/modules/accounts/picture.js create mode 100644 public/src/modules/ace-editor.js create mode 100644 public/src/modules/alerts.js create mode 100644 public/src/modules/api.js create mode 100644 public/src/modules/autocomplete.js create mode 100644 public/src/modules/categoryFilter.js create mode 100644 public/src/modules/categorySearch.js create mode 100644 public/src/modules/categorySelector.js create mode 100644 public/src/modules/chat.js create mode 100644 public/src/modules/components.js create mode 100644 public/src/modules/coverPhoto.js create mode 100644 public/src/modules/flags.js create mode 100644 public/src/modules/groupSearch.js create mode 100644 public/src/modules/handleBack.js create mode 100644 public/src/modules/helpers.common.js create mode 100644 public/src/modules/helpers.js create mode 100644 public/src/modules/hooks.js create mode 100644 public/src/modules/iconSelect.js create mode 100644 public/src/modules/logout.js create mode 100644 public/src/modules/messages.js create mode 100644 public/src/modules/navigator.js create mode 100644 public/src/modules/notifications.js create mode 100644 public/src/modules/pictureCropper.js create mode 100644 public/src/modules/postSelect.js create mode 100644 public/src/modules/scrollStop.js create mode 100644 public/src/modules/search.js create mode 100644 public/src/modules/settings.js create mode 100644 public/src/modules/settings/array.js create mode 100644 public/src/modules/settings/checkbox.js create mode 100644 public/src/modules/settings/key.js create mode 100644 public/src/modules/settings/number.js create mode 100644 public/src/modules/settings/object.js create mode 100644 public/src/modules/settings/select.js create mode 100644 public/src/modules/settings/sorted-list.js create mode 100644 public/src/modules/settings/textarea.js create mode 100644 public/src/modules/share.js create mode 100644 public/src/modules/slugify.js create mode 100644 public/src/modules/sort.js create mode 100644 public/src/modules/storage.js create mode 100644 public/src/modules/taskbar.js create mode 100644 public/src/modules/topicList.js create mode 100644 public/src/modules/topicSelect.js create mode 100644 public/src/modules/topicThumbs.js create mode 100644 public/src/modules/translator.common.js create mode 100644 public/src/modules/translator.js create mode 100644 public/src/modules/uploadHelpers.js create mode 100644 public/src/modules/uploader.js create mode 100644 public/src/overrides.js create mode 100644 public/src/service-worker.js create mode 100644 public/src/sockets.js create mode 100644 public/src/utils.common.js create mode 100644 public/src/utils.js create mode 100644 public/src/widgets.js create mode 100644 public/vendor/bootbox/wrapper.js create mode 100644 public/vendor/fontawesome/.gitignore create mode 100644 public/vendor/fontawesome/LICENSE.txt create mode 100644 public/vendor/fontawesome/attribution.js create mode 100644 public/vendor/fontawesome/less/_animated.less create mode 100644 public/vendor/fontawesome/less/_bordered-pulled.less create mode 100644 public/vendor/fontawesome/less/_core.less create mode 100644 public/vendor/fontawesome/less/_fixed-width.less create mode 100644 public/vendor/fontawesome/less/_icons.less create mode 100644 public/vendor/fontawesome/less/_larger.less create mode 100644 public/vendor/fontawesome/less/_list.less create mode 100644 public/vendor/fontawesome/less/_mixins.less create mode 100644 public/vendor/fontawesome/less/_rotated-flipped.less create mode 100644 public/vendor/fontawesome/less/_screen-reader.less create mode 100644 public/vendor/fontawesome/less/_shims.less create mode 100644 public/vendor/fontawesome/less/_stacked.less create mode 100644 public/vendor/fontawesome/less/_variables.less create mode 100644 public/vendor/fontawesome/less/brands.less create mode 100644 public/vendor/fontawesome/less/fontawesome.less create mode 100644 public/vendor/fontawesome/less/nodebb-shims.less create mode 100644 public/vendor/fontawesome/less/regular.less create mode 100644 public/vendor/fontawesome/less/solid.less create mode 100644 public/vendor/fontawesome/less/v4-shims.less create mode 100644 public/vendor/fontawesome/webfonts/fa-brands-400.eot create mode 100644 public/vendor/fontawesome/webfonts/fa-brands-400.svg create mode 100644 public/vendor/fontawesome/webfonts/fa-brands-400.ttf create mode 100644 public/vendor/fontawesome/webfonts/fa-brands-400.woff create mode 100644 public/vendor/fontawesome/webfonts/fa-brands-400.woff2 create mode 100644 public/vendor/fontawesome/webfonts/fa-regular-400.eot create mode 100644 public/vendor/fontawesome/webfonts/fa-regular-400.svg create mode 100644 public/vendor/fontawesome/webfonts/fa-regular-400.ttf create mode 100644 public/vendor/fontawesome/webfonts/fa-regular-400.woff create mode 100644 public/vendor/fontawesome/webfonts/fa-regular-400.woff2 create mode 100644 public/vendor/fontawesome/webfonts/fa-solid-900.eot create mode 100644 public/vendor/fontawesome/webfonts/fa-solid-900.svg create mode 100644 public/vendor/fontawesome/webfonts/fa-solid-900.ttf create mode 100644 public/vendor/fontawesome/webfonts/fa-solid-900.woff create mode 100644 public/vendor/fontawesome/webfonts/fa-solid-900.woff2 create mode 100644 public/vendor/jquery/draggable-background/backgroundDraggable.js create mode 100644 public/vendor/mdl/material.css create mode 100644 public/vendor/redoc/index.html create mode 100644 renovate.json create mode 100644 require-main.js create mode 100644 src/admin/search.js create mode 100644 src/admin/versions.js create mode 100644 src/als.js create mode 100644 src/analytics.js create mode 100644 src/api/categories.js create mode 100644 src/api/chats.js create mode 100644 src/api/flags.js create mode 100644 src/api/groups.js create mode 100644 src/api/helpers.js create mode 100644 src/api/index.js create mode 100644 src/api/posts.js create mode 100644 src/api/topics.js create mode 100644 src/api/users.js create mode 100644 src/batch.js create mode 100644 src/cache.js create mode 100644 src/cache/lru.js create mode 100644 src/cache/ttl.js create mode 100644 src/cacheCreate.js create mode 100644 src/categories/activeusers.js create mode 100644 src/categories/create.js create mode 100644 src/categories/data.js create mode 100644 src/categories/delete.js create mode 100644 src/categories/index.js create mode 100644 src/categories/recentreplies.js create mode 100644 src/categories/search.js create mode 100644 src/categories/topics.js create mode 100644 src/categories/unread.js create mode 100644 src/categories/update.js create mode 100644 src/categories/watch.js create mode 100644 src/cli/colors.js create mode 100644 src/cli/index.js create mode 100644 src/cli/manage.js create mode 100644 src/cli/package-install.js create mode 100644 src/cli/reset.js create mode 100644 src/cli/running.js create mode 100644 src/cli/setup.js create mode 100644 src/cli/upgrade-plugins.js create mode 100644 src/cli/upgrade.js create mode 100644 src/cli/user.js create mode 100644 src/constants.js create mode 100644 src/controllers/404.js create mode 100644 src/controllers/accounts.js create mode 100644 src/controllers/accounts/blocks.js create mode 100644 src/controllers/accounts/categories.js create mode 100644 src/controllers/accounts/chats.js create mode 100644 src/controllers/accounts/consent.js create mode 100644 src/controllers/accounts/edit.js create mode 100644 src/controllers/accounts/follow.js create mode 100644 src/controllers/accounts/groups.js create mode 100644 src/controllers/accounts/helpers.js create mode 100644 src/controllers/accounts/info.js create mode 100644 src/controllers/accounts/notifications.js create mode 100644 src/controllers/accounts/posts.js create mode 100644 src/controllers/accounts/profile.js create mode 100644 src/controllers/accounts/sessions.js create mode 100644 src/controllers/accounts/settings.js create mode 100644 src/controllers/accounts/uploads.js create mode 100644 src/controllers/admin.js create mode 100644 src/controllers/admin/admins-mods.js create mode 100644 src/controllers/admin/appearance.js create mode 100644 src/controllers/admin/cache.js create mode 100644 src/controllers/admin/categories.js create mode 100644 src/controllers/admin/dashboard.js create mode 100644 src/controllers/admin/database.js create mode 100644 src/controllers/admin/digest.js create mode 100644 src/controllers/admin/errors.js create mode 100644 src/controllers/admin/events.js create mode 100644 src/controllers/admin/groups.js create mode 100644 src/controllers/admin/hooks.js create mode 100644 src/controllers/admin/info.js create mode 100644 src/controllers/admin/logger.js create mode 100644 src/controllers/admin/logs.js create mode 100644 src/controllers/admin/plugins.js create mode 100644 src/controllers/admin/privileges.js create mode 100644 src/controllers/admin/rewards.js create mode 100644 src/controllers/admin/settings.js create mode 100644 src/controllers/admin/tags.js create mode 100644 src/controllers/admin/themes.js create mode 100644 src/controllers/admin/uploads.js create mode 100644 src/controllers/admin/users.js create mode 100644 src/controllers/admin/widgets.js create mode 100644 src/controllers/api.js create mode 100644 src/controllers/authentication.js create mode 100644 src/controllers/career.js create mode 100644 src/controllers/categories.js create mode 100644 src/controllers/category.js create mode 100644 src/controllers/composer.js create mode 100644 src/controllers/composer.ts create mode 100644 src/controllers/errors.js create mode 100644 src/controllers/globalmods.js create mode 100644 src/controllers/groups.js create mode 100644 src/controllers/helpers.js create mode 100644 src/controllers/home.js create mode 100644 src/controllers/index.js create mode 100644 src/controllers/mods.js create mode 100644 src/controllers/osd.js create mode 100644 src/controllers/ping.js create mode 100644 src/controllers/popular.js create mode 100644 src/controllers/posts.js create mode 100644 src/controllers/recent.js create mode 100644 src/controllers/search.js create mode 100644 src/controllers/sitemap.js create mode 100644 src/controllers/tags.js create mode 100644 src/controllers/top.js create mode 100644 src/controllers/topics.js create mode 100644 src/controllers/unread.js create mode 100644 src/controllers/uploads.js create mode 100644 src/controllers/user.js create mode 100644 src/controllers/users.js create mode 100644 src/controllers/write/admin.js create mode 100644 src/controllers/write/categories.js create mode 100644 src/controllers/write/chats.js create mode 100644 src/controllers/write/files.js create mode 100644 src/controllers/write/flags.js create mode 100644 src/controllers/write/groups.js create mode 100644 src/controllers/write/index.js create mode 100644 src/controllers/write/posts.js create mode 100644 src/controllers/write/topics.js create mode 100644 src/controllers/write/users.js create mode 100644 src/controllers/write/utilities.js create mode 100644 src/coverPhoto.js create mode 100644 src/database/cache.js create mode 100644 src/database/helpers.js create mode 100644 src/database/index.js create mode 100644 src/database/mongo.js create mode 100644 src/database/mongo/connection.js create mode 100644 src/database/mongo/hash.js create mode 100644 src/database/mongo/helpers.js create mode 100644 src/database/mongo/list.js create mode 100644 src/database/mongo/main.js create mode 100644 src/database/mongo/sets.js create mode 100644 src/database/mongo/sorted.js create mode 100644 src/database/mongo/sorted/add.js create mode 100644 src/database/mongo/sorted/intersect.js create mode 100644 src/database/mongo/sorted/remove.js create mode 100644 src/database/mongo/sorted/union.js create mode 100644 src/database/mongo/transaction.js create mode 100644 src/database/postgres.js create mode 100644 src/database/postgres/connection.js create mode 100644 src/database/postgres/hash.js create mode 100644 src/database/postgres/helpers.js create mode 100644 src/database/postgres/list.js create mode 100644 src/database/postgres/main.js create mode 100644 src/database/postgres/sets.js create mode 100644 src/database/postgres/sorted.js create mode 100644 src/database/postgres/sorted/add.js create mode 100644 src/database/postgres/sorted/intersect.js create mode 100644 src/database/postgres/sorted/remove.js create mode 100644 src/database/postgres/sorted/union.js create mode 100644 src/database/postgres/transaction.js create mode 100644 src/database/redis.js create mode 100644 src/database/redis/connection.js create mode 100644 src/database/redis/hash.js create mode 100644 src/database/redis/helpers.js create mode 100644 src/database/redis/list.js create mode 100644 src/database/redis/main.js create mode 100644 src/database/redis/pubsub.js create mode 100644 src/database/redis/sets.js create mode 100644 src/database/redis/sorted.js create mode 100644 src/database/redis/sorted/add.js create mode 100644 src/database/redis/sorted/intersect.js create mode 100644 src/database/redis/sorted/remove.js create mode 100644 src/database/redis/sorted/union.js create mode 100644 src/database/redis/transaction.js create mode 100644 src/emailer.js create mode 100644 src/events.js create mode 100644 src/file.js create mode 100644 src/flags.js create mode 100644 src/groups/cache.js create mode 100644 src/groups/cover.js create mode 100644 src/groups/create.js create mode 100644 src/groups/data.js create mode 100644 src/groups/delete.js create mode 100644 src/groups/index.js create mode 100644 src/groups/invite.js create mode 100644 src/groups/join.js create mode 100644 src/groups/leave.js create mode 100644 src/groups/membership.js create mode 100644 src/groups/ownership.js create mode 100644 src/groups/posts.js create mode 100644 src/groups/search.js create mode 100644 src/groups/update.js create mode 100644 src/groups/user.js create mode 100644 src/helpers.js create mode 100644 src/image.js create mode 100644 src/install.js create mode 100644 src/languages.js create mode 100644 src/logger.js create mode 100644 src/messaging/create.js create mode 100644 src/messaging/data.js create mode 100644 src/messaging/delete.js create mode 100644 src/messaging/edit.js create mode 100644 src/messaging/index.js create mode 100644 src/messaging/notifications.js create mode 100644 src/messaging/rooms.js create mode 100644 src/messaging/unread.js create mode 100644 src/meta/aliases.js create mode 100644 src/meta/blacklist.js create mode 100644 src/meta/build.js create mode 100644 src/meta/cacheBuster.js create mode 100644 src/meta/configs.js create mode 100644 src/meta/css.js create mode 100644 src/meta/debugFork.js create mode 100644 src/meta/dependencies.js create mode 100644 src/meta/errors.js create mode 100644 src/meta/index.js create mode 100644 src/meta/js.js create mode 100644 src/meta/languages.js create mode 100644 src/meta/logs.js create mode 100644 src/meta/minifier.js create mode 100644 src/meta/settings.js create mode 100644 src/meta/tags.js create mode 100644 src/meta/templates.js create mode 100644 src/meta/themes.js create mode 100644 src/middleware/admin.js create mode 100644 src/middleware/assert.js create mode 100644 src/middleware/expose.js create mode 100644 src/middleware/header.js create mode 100644 src/middleware/headers.js create mode 100644 src/middleware/helpers.js create mode 100644 src/middleware/index.js create mode 100644 src/middleware/maintenance.js create mode 100644 src/middleware/ratelimit.js create mode 100644 src/middleware/render.js create mode 100644 src/middleware/uploads.js create mode 100644 src/middleware/user.js create mode 100644 src/navigation/admin.js create mode 100644 src/navigation/index.js create mode 100644 src/notifications.js create mode 100644 src/pagination.js create mode 100644 src/password.js create mode 100644 src/plugins/data.js create mode 100644 src/plugins/hooks.js create mode 100644 src/plugins/index.js create mode 100644 src/plugins/install.js create mode 100644 src/plugins/load.js create mode 100644 src/plugins/usage.js create mode 100644 src/posts/bookmarks.js create mode 100644 src/posts/cache.js create mode 100644 src/posts/category.js create mode 100644 src/posts/create.js create mode 100644 src/posts/data.js create mode 100644 src/posts/delete.js create mode 100644 src/posts/diffs.js create mode 100644 src/posts/edit.js create mode 100644 src/posts/index.js create mode 100644 src/posts/parse.js create mode 100644 src/posts/queue.js create mode 100644 src/posts/recent.js create mode 100644 src/posts/summary.js create mode 100644 src/posts/tools.js create mode 100644 src/posts/topics.js create mode 100644 src/posts/uploads.js create mode 100644 src/posts/user.js create mode 100644 src/posts/votes.js create mode 100644 src/prestart.js create mode 100644 src/privileges/admin.js create mode 100644 src/privileges/categories.js create mode 100644 src/privileges/global.js create mode 100644 src/privileges/helpers.js create mode 100644 src/privileges/index.js create mode 100644 src/privileges/posts.js create mode 100644 src/privileges/topics.js create mode 100644 src/privileges/users.js create mode 100644 src/promisify.js create mode 100644 src/pubsub.js create mode 100644 src/rewards/admin.js create mode 100644 src/rewards/index.js create mode 100644 src/routes/admin.js create mode 100644 src/routes/api.js create mode 100644 src/routes/authentication.js create mode 100644 src/routes/debug.js create mode 100644 src/routes/feeds.js create mode 100644 src/routes/helpers.js create mode 100644 src/routes/index.js create mode 100644 src/routes/meta.js create mode 100644 src/routes/user.js create mode 100644 src/routes/write/admin.js create mode 100644 src/routes/write/categories.js create mode 100644 src/routes/write/chats.js create mode 100644 src/routes/write/files.js create mode 100644 src/routes/write/flags.js create mode 100644 src/routes/write/groups.js create mode 100644 src/routes/write/index.js create mode 100644 src/routes/write/posts.js create mode 100644 src/routes/write/topics.js create mode 100644 src/routes/write/users.js create mode 100644 src/routes/write/utilities.js create mode 100644 src/search.js create mode 100644 src/settings.js create mode 100644 src/sitemap.js create mode 100644 src/slugify.js create mode 100644 src/social.js create mode 100644 src/social.ts create mode 100644 src/socket.io/admin.js create mode 100644 src/socket.io/admin/analytics.js create mode 100644 src/socket.io/admin/cache.js create mode 100644 src/socket.io/admin/categories.js create mode 100644 src/socket.io/admin/config.js create mode 100644 src/socket.io/admin/digest.js create mode 100644 src/socket.io/admin/email.js create mode 100644 src/socket.io/admin/errors.js create mode 100644 src/socket.io/admin/logs.js create mode 100644 src/socket.io/admin/navigation.js create mode 100644 src/socket.io/admin/plugins.js create mode 100644 src/socket.io/admin/rewards.js create mode 100644 src/socket.io/admin/rooms.js create mode 100644 src/socket.io/admin/settings.js create mode 100644 src/socket.io/admin/social.js create mode 100644 src/socket.io/admin/tags.js create mode 100644 src/socket.io/admin/themes.js create mode 100644 src/socket.io/admin/user.js create mode 100644 src/socket.io/admin/widgets.js create mode 100644 src/socket.io/blacklist.js create mode 100644 src/socket.io/categories.js create mode 100644 src/socket.io/categories/search.js create mode 100644 src/socket.io/groups.js create mode 100644 src/socket.io/helpers.js create mode 100644 src/socket.io/index.js create mode 100644 src/socket.io/meta.js create mode 100644 src/socket.io/modules.js create mode 100644 src/socket.io/notifications.js create mode 100644 src/socket.io/plugins.js create mode 100644 src/socket.io/posts.js create mode 100644 src/socket.io/posts/tools.js create mode 100644 src/socket.io/posts/votes.js create mode 100644 src/socket.io/topics.js create mode 100644 src/socket.io/topics/infinitescroll.js create mode 100644 src/socket.io/topics/merge.js create mode 100644 src/socket.io/topics/move.js create mode 100644 src/socket.io/topics/tags.js create mode 100644 src/socket.io/topics/tools.js create mode 100644 src/socket.io/topics/unread.js create mode 100644 src/socket.io/uploads.js create mode 100644 src/socket.io/user.js create mode 100644 src/socket.io/user/picture.js create mode 100644 src/socket.io/user/profile.js create mode 100644 src/socket.io/user/registration.js create mode 100644 src/socket.io/user/status.js create mode 100644 src/start.js create mode 100644 src/topics/bookmarks.js create mode 100644 src/topics/create.js create mode 100644 src/topics/data.js create mode 100644 src/topics/delete.js create mode 100644 src/topics/events.js create mode 100644 src/topics/follow.js create mode 100644 src/topics/fork.js create mode 100644 src/topics/index.js create mode 100644 src/topics/merge.js create mode 100644 src/topics/posts.js create mode 100644 src/topics/recent.js create mode 100644 src/topics/scheduled.js create mode 100644 src/topics/sorted.js create mode 100644 src/topics/suggested.js create mode 100644 src/topics/tags.js create mode 100644 src/topics/teaser.js create mode 100644 src/topics/thumbs.js create mode 100644 src/topics/tools.js create mode 100644 src/topics/unread.js create mode 100644 src/topics/user.js create mode 100644 src/translator.js create mode 100644 src/types/admin.js create mode 100644 src/types/admin.ts create mode 100644 src/types/breadcrumbs.js create mode 100644 src/types/breadcrumbs.ts create mode 100644 src/types/category.js create mode 100644 src/types/category.ts create mode 100644 src/types/chat.js create mode 100644 src/types/chat.ts create mode 100644 src/types/commonProps.js create mode 100644 src/types/commonProps.ts create mode 100644 src/types/error.js create mode 100644 src/types/error.ts create mode 100644 src/types/flag.js create mode 100644 src/types/flag.ts create mode 100644 src/types/group.js create mode 100644 src/types/group.ts create mode 100644 src/types/index.js create mode 100644 src/types/index.ts create mode 100644 src/types/pagination.js create mode 100644 src/types/pagination.ts create mode 100644 src/types/post.js create mode 100644 src/types/post.ts create mode 100644 src/types/settings.js create mode 100644 src/types/settings.ts create mode 100644 src/types/social.js create mode 100644 src/types/social.ts create mode 100644 src/types/status.js create mode 100644 src/types/status.ts create mode 100644 src/types/tag.js create mode 100644 src/types/tag.ts create mode 100644 src/types/topic.js create mode 100644 src/types/topic.ts create mode 100644 src/types/user.js create mode 100644 src/types/user.ts create mode 100644 src/upgrade.js create mode 100644 src/upgrades/1.0.0/chat_room_hashes.js create mode 100644 src/upgrades/1.0.0/chat_upgrade.js create mode 100644 src/upgrades/1.0.0/global_moderators.js create mode 100644 src/upgrades/1.0.0/social_post_sharing.js create mode 100644 src/upgrades/1.0.0/theme_to_active_plugins.js create mode 100644 src/upgrades/1.0.0/user_best_posts.js create mode 100644 src/upgrades/1.0.0/users_notvalidated.js create mode 100644 src/upgrades/1.1.0/assign_topic_read_privilege.js create mode 100644 src/upgrades/1.1.0/dismiss_flags_from_deleted_topics.js create mode 100644 src/upgrades/1.1.0/group_title_update.js create mode 100644 src/upgrades/1.1.0/separate_upvote_downvote.js create mode 100644 src/upgrades/1.1.0/user_post_count_per_tid.js create mode 100644 src/upgrades/1.1.1/remove_negative_best_posts.js create mode 100644 src/upgrades/1.1.1/upload_privileges.js create mode 100644 src/upgrades/1.10.0/hash_recent_ip_addresses.js create mode 100644 src/upgrades/1.10.0/post_history_privilege.js create mode 100644 src/upgrades/1.10.0/search_privileges.js create mode 100644 src/upgrades/1.10.0/view_deleted_privilege.js create mode 100644 src/upgrades/1.10.2/event_filters.js create mode 100644 src/upgrades/1.10.2/fix_category_post_zsets.js create mode 100644 src/upgrades/1.10.2/fix_category_topic_zsets.js create mode 100644 src/upgrades/1.10.2/local_login_privileges.js create mode 100644 src/upgrades/1.10.2/postgres_sessions.js create mode 100644 src/upgrades/1.10.2/upgrade_bans_to_hashes.js create mode 100644 src/upgrades/1.10.2/username_email_history.js create mode 100644 src/upgrades/1.11.0/navigation_visibility_groups.js create mode 100644 src/upgrades/1.11.0/resize_image_width.js create mode 100644 src/upgrades/1.11.0/widget_visibility_groups.js create mode 100644 src/upgrades/1.11.1/remove_ignored_cids_per_user.js create mode 100644 src/upgrades/1.12.0/category_watch_state.js create mode 100644 src/upgrades/1.12.0/global_view_privileges.js create mode 100644 src/upgrades/1.12.0/group_create_privilege.js create mode 100644 src/upgrades/1.12.1/clear_username_email_history.js create mode 100644 src/upgrades/1.12.1/moderation_notes_refactor.js create mode 100644 src/upgrades/1.12.1/post_upload_sizes.js create mode 100644 src/upgrades/1.12.3/disable_plugin_metrics.js create mode 100644 src/upgrades/1.12.3/give_mod_info_privilege.js create mode 100644 src/upgrades/1.12.3/give_mod_privileges.js create mode 100644 src/upgrades/1.12.3/update_registration_type.js create mode 100644 src/upgrades/1.12.3/user_pid_sets.js create mode 100644 src/upgrades/1.13.0/clean_flag_byCid.js create mode 100644 src/upgrades/1.13.0/clean_post_topic_hash.js create mode 100644 src/upgrades/1.13.0/cleanup_old_notifications.js create mode 100644 src/upgrades/1.13.3/fix_users_sorted_sets.js create mode 100644 src/upgrades/1.13.4/remove_allowFileUploads_priv.js create mode 100644 src/upgrades/1.14.0/fix_category_image_field.js create mode 100644 src/upgrades/1.14.0/unescape_navigation_titles.js create mode 100644 src/upgrades/1.14.1/readd_deleted_recent_topics.js create mode 100644 src/upgrades/1.15.0/add_target_uid_to_flags.js create mode 100644 src/upgrades/1.15.0/consolidate_flags.js create mode 100644 src/upgrades/1.15.0/disable_sounds_plugin.js create mode 100644 src/upgrades/1.15.0/fix_category_colors.js create mode 100644 src/upgrades/1.15.0/fullname_search_set.js create mode 100644 src/upgrades/1.15.0/remove_allow_from_uri.js create mode 100644 src/upgrades/1.15.0/remove_flag_reporters_zset.js create mode 100644 src/upgrades/1.15.0/topic_poster_count.js create mode 100644 src/upgrades/1.15.0/track_flags_by_target.js create mode 100644 src/upgrades/1.15.0/verified_users_group.js create mode 100644 src/upgrades/1.15.4/clear_purged_replies.js create mode 100644 src/upgrades/1.16.0/category_tags.js create mode 100644 src/upgrades/1.16.0/migrate_thumbs.js create mode 100644 src/upgrades/1.17.0/banned_users_group.js create mode 100644 src/upgrades/1.17.0/category_name_zset.js create mode 100644 src/upgrades/1.17.0/default_favicon.js create mode 100644 src/upgrades/1.17.0/schedule_privilege_for_existing_categories.js create mode 100644 src/upgrades/1.17.0/subcategories_per_page.js create mode 100644 src/upgrades/1.17.0/topic_thumb_count.js create mode 100644 src/upgrades/1.18.0/enable_include_unverified_emails.js create mode 100644 src/upgrades/1.18.0/topic_tags_refactor.js create mode 100644 src/upgrades/1.18.4/category_topics_views.js create mode 100644 src/upgrades/1.19.0/navigation-enabled-hashes.js create mode 100644 src/upgrades/1.19.0/reenable-username-login.js create mode 100644 src/upgrades/1.19.2/remove_leftover_thumbs_after_topic_purge.js create mode 100644 src/upgrades/1.19.2/store_downvoted_posts_in_zset.js create mode 100644 src/upgrades/1.19.3/fix_user_uploads_zset.js create mode 100644 src/upgrades/1.19.3/rename_post_upload_hashes.js create mode 100644 src/upgrades/1.2.0/category_recent_tids.js create mode 100644 src/upgrades/1.2.0/edit_delete_deletetopic_privileges.js create mode 100644 src/upgrades/1.3.0/favourites_to_bookmarks.js create mode 100644 src/upgrades/1.3.0/sorted_sets_for_post_replies.js create mode 100644 src/upgrades/1.4.0/global_and_user_language_keys.js create mode 100644 src/upgrades/1.4.0/sorted_set_for_pinned_topics.js create mode 100644 src/upgrades/1.4.4/config_urls_update.js create mode 100644 src/upgrades/1.4.4/sound_settings.js create mode 100644 src/upgrades/1.4.6/delete_sessions.js create mode 100644 src/upgrades/1.5.0/allowed_file_extensions.js create mode 100644 src/upgrades/1.5.0/flags_refactor.js create mode 100644 src/upgrades/1.5.0/moderation_history_refactor.js create mode 100644 src/upgrades/1.5.0/post_votes_zset.js create mode 100644 src/upgrades/1.5.0/remove_relative_uploaded_profile_cover.js create mode 100644 src/upgrades/1.5.1/rename_mods_group.js create mode 100644 src/upgrades/1.5.2/rss_token_wipe.js create mode 100644 src/upgrades/1.5.2/tags_privilege.js create mode 100644 src/upgrades/1.6.0/clear-stale-digest-template.js create mode 100644 src/upgrades/1.6.0/generate-email-logo.js create mode 100644 src/upgrades/1.6.0/ipblacklist-fix.js create mode 100644 src/upgrades/1.6.0/robots-config-change.js create mode 100644 src/upgrades/1.6.2/topics_lastposttime_zset.js create mode 100644 src/upgrades/1.7.0/generate-custom-html.js create mode 100644 src/upgrades/1.7.1/notification-settings.js create mode 100644 src/upgrades/1.7.3/key_value_schema_change.js create mode 100644 src/upgrades/1.7.3/topic_votes.js create mode 100644 src/upgrades/1.7.4/chat_privilege.js create mode 100644 src/upgrades/1.7.4/fix_moved_topics_byvotes.js create mode 100644 src/upgrades/1.7.4/fix_user_topics_per_category.js create mode 100644 src/upgrades/1.7.4/global_upload_privilege.js create mode 100644 src/upgrades/1.7.4/rename_min_reputation_settings.js create mode 100644 src/upgrades/1.7.4/vote_privilege.js create mode 100644 src/upgrades/1.7.6/flatten_navigation_data.js create mode 100644 src/upgrades/1.7.6/notification_types.js create mode 100644 src/upgrades/1.7.6/update_min_pass_strength.js create mode 100644 src/upgrades/1.8.0/give_signature_privileges.js create mode 100644 src/upgrades/1.8.0/give_spiders_privileges.js create mode 100644 src/upgrades/1.8.1/diffs_zset_to_listhash.js create mode 100644 src/upgrades/1.9.0/refresh_post_upload_associations.js create mode 100644 src/upgrades/TEMPLATE create mode 100644 src/user/admin.js create mode 100644 src/user/approval.js create mode 100644 src/user/auth.js create mode 100644 src/user/bans.js create mode 100644 src/user/blocks.js create mode 100644 src/user/categories.js create mode 100644 src/user/create.js create mode 100644 src/user/data.js create mode 100644 src/user/delete.js create mode 100644 src/user/digest.js create mode 100644 src/user/email.js create mode 100644 src/user/follow.js create mode 100644 src/user/index.js create mode 100644 src/user/info.js create mode 100644 src/user/interstitials.js create mode 100644 src/user/invite.js create mode 100644 src/user/jobs.js create mode 100644 src/user/jobs/export-posts.js create mode 100644 src/user/jobs/export-profile.js create mode 100644 src/user/jobs/export-uploads.js create mode 100644 src/user/notifications.js create mode 100644 src/user/online.js create mode 100644 src/user/password.js create mode 100644 src/user/picture.js create mode 100644 src/user/posts.js create mode 100644 src/user/profile.js create mode 100644 src/user/reset.js create mode 100644 src/user/search.js create mode 100644 src/user/settings.js create mode 100644 src/user/topics.js create mode 100644 src/user/uploads.js create mode 100644 src/utils.js create mode 100644 src/views/400.tpl create mode 100644 src/views/403.tpl create mode 100644 src/views/404.tpl create mode 100644 src/views/500.tpl create mode 100644 src/views/503.tpl create mode 100644 src/views/admin/advanced/cache.tpl create mode 100644 src/views/admin/advanced/database.tpl create mode 100644 src/views/admin/advanced/errors.tpl create mode 100644 src/views/admin/advanced/events.tpl create mode 100644 src/views/admin/advanced/hooks.tpl create mode 100644 src/views/admin/advanced/logs.tpl create mode 100644 src/views/admin/appearance/customise.tpl create mode 100644 src/views/admin/appearance/skins.tpl create mode 100644 src/views/admin/appearance/themes.tpl create mode 100644 src/views/admin/dashboard.tpl create mode 100644 src/views/admin/dashboard/logins.tpl create mode 100644 src/views/admin/dashboard/searches.tpl create mode 100644 src/views/admin/dashboard/topics.tpl create mode 100644 src/views/admin/dashboard/users.tpl create mode 100644 src/views/admin/development/info.tpl create mode 100644 src/views/admin/development/logger.tpl create mode 100644 src/views/admin/extend/plugins.tpl create mode 100644 src/views/admin/extend/rewards.tpl create mode 100644 src/views/admin/extend/widgets.tpl create mode 100644 src/views/admin/footer.tpl create mode 100644 src/views/admin/header.tpl create mode 100644 src/views/admin/manage/admins-mods.tpl create mode 100644 src/views/admin/manage/categories.tpl create mode 100644 src/views/admin/manage/category-analytics.tpl create mode 100644 src/views/admin/manage/category.tpl create mode 100644 src/views/admin/manage/digest.tpl create mode 100644 src/views/admin/manage/group.tpl create mode 100644 src/views/admin/manage/groups.tpl create mode 100644 src/views/admin/manage/privileges.tpl create mode 100644 src/views/admin/manage/registration.tpl create mode 100644 src/views/admin/manage/tags.tpl create mode 100644 src/views/admin/manage/uploads.tpl create mode 100644 src/views/admin/manage/users.tpl create mode 100644 src/views/admin/partials/api/sorted-list/form.tpl create mode 100644 src/views/admin/partials/api/sorted-list/item.tpl create mode 100644 src/views/admin/partials/blacklist-validate.tpl create mode 100644 src/views/admin/partials/categories/category-rows.tpl create mode 100644 src/views/admin/partials/categories/copy-settings.tpl create mode 100644 src/views/admin/partials/categories/create.tpl create mode 100644 src/views/admin/partials/categories/groups.tpl create mode 100644 src/views/admin/partials/categories/purge.tpl create mode 100644 src/views/admin/partials/categories/select-category.tpl create mode 100644 src/views/admin/partials/categories/users.tpl create mode 100644 src/views/admin/partials/create_user_modal.tpl create mode 100644 src/views/admin/partials/dashboard/graph.tpl create mode 100644 src/views/admin/partials/dashboard/stats.tpl create mode 100644 src/views/admin/partials/download_plugin_item.tpl create mode 100644 src/views/admin/partials/groups/add-members.tpl create mode 100644 src/views/admin/partials/groups/memberlist.tpl create mode 100644 src/views/admin/partials/groups/privileges-select-category.tpl create mode 100644 src/views/admin/partials/installed_plugin_item.tpl create mode 100644 src/views/admin/partials/manage_user_groups.tpl create mode 100644 src/views/admin/partials/menu.tpl create mode 100644 src/views/admin/partials/pageviews-range-select.tpl create mode 100644 src/views/admin/partials/plugins/license.tpl create mode 100644 src/views/admin/partials/plugins/no-plugins.tpl create mode 100644 src/views/admin/partials/privileges/category.tpl create mode 100644 src/views/admin/partials/privileges/global.tpl create mode 100644 src/views/admin/partials/quick_actions/alerts.tpl create mode 100644 src/views/admin/partials/quick_actions/buttons.tpl create mode 100644 src/views/admin/partials/settings/footer.tpl create mode 100644 src/views/admin/partials/settings/header.tpl create mode 100644 src/views/admin/partials/temporary-ban.tpl create mode 100644 src/views/admin/partials/temporary-mute.tpl create mode 100644 src/views/admin/partials/theme_list.tpl create mode 100644 src/views/admin/partials/widget-settings.tpl create mode 100644 src/views/admin/partials/widgets/show_hide_groups.tpl create mode 100644 src/views/admin/settings/advanced.tpl create mode 100644 src/views/admin/settings/api.tpl create mode 100644 src/views/admin/settings/chat.tpl create mode 100644 src/views/admin/settings/cookies.tpl create mode 100644 src/views/admin/settings/email.tpl create mode 100644 src/views/admin/settings/general.tpl create mode 100644 src/views/admin/settings/group.tpl create mode 100644 src/views/admin/settings/guest.tpl create mode 100644 src/views/admin/settings/homepage.tpl create mode 100644 src/views/admin/settings/languages.tpl create mode 100644 src/views/admin/settings/navigation.tpl create mode 100644 src/views/admin/settings/notifications.tpl create mode 100644 src/views/admin/settings/pagination.tpl create mode 100644 src/views/admin/settings/post.tpl create mode 100644 src/views/admin/settings/reputation.tpl create mode 100644 src/views/admin/settings/social.tpl create mode 100644 src/views/admin/settings/sockets.tpl create mode 100644 src/views/admin/settings/tags.tpl create mode 100644 src/views/admin/settings/uploads.tpl create mode 100644 src/views/admin/settings/user.tpl create mode 100644 src/views/admin/settings/web-crawler.tpl create mode 100644 src/views/emails/banned.tpl create mode 100644 src/views/emails/digest.tpl create mode 100644 src/views/emails/invitation.tpl create mode 100644 src/views/emails/notification.tpl create mode 100644 src/views/emails/partials/footer.tpl create mode 100644 src/views/emails/partials/header.tpl create mode 100644 src/views/emails/partials/post-queue-body.tpl create mode 100644 src/views/emails/registration_accepted.tpl create mode 100644 src/views/emails/reset.tpl create mode 100644 src/views/emails/reset_notify.tpl create mode 100644 src/views/emails/test.tpl create mode 100644 src/views/emails/verify-email.tpl create mode 100644 src/views/emails/welcome.tpl create mode 100644 src/views/install/index.tpl create mode 100644 src/views/modals/crop_picture.tpl create mode 100644 src/views/modals/invite.tpl create mode 100644 src/views/modals/move-post.tpl create mode 100644 src/views/modals/set-pin-expiry.tpl create mode 100644 src/views/modals/topic-thumbs.tpl create mode 100644 src/views/outgoing.tpl create mode 100644 src/views/partials/data/category.tpl create mode 100644 src/views/partials/data/topic.tpl create mode 100644 src/views/partials/email_update.tpl create mode 100644 src/views/partials/fontawesome.tpl create mode 100644 src/views/partials/footer/js.tpl create mode 100644 src/views/partials/gdpr_consent.tpl create mode 100644 src/views/partials/noscript/message.tpl create mode 100644 src/views/partials/noscript/warning.tpl create mode 100644 src/views/partials/topic/post-preview.tpl create mode 100644 src/views/sitemap.tpl create mode 100644 src/webserver.js create mode 100644 src/widgets/admin.js create mode 100644 src/widgets/index.js create mode 100644 test/.eslintrc create mode 100644 test/api.js create mode 100644 test/authentication.js create mode 100644 test/batch.js create mode 100644 test/blacklist.js create mode 100644 test/build.js create mode 100644 test/categories.js create mode 100644 test/controllers-admin.js create mode 100644 test/controllers.js create mode 100644 test/coverPhoto.js create mode 100644 test/database.js create mode 100644 test/database/hash.js create mode 100644 test/database/keys.js create mode 100644 test/database/list.js create mode 100644 test/database/sets.js create mode 100644 test/database/sorted.js create mode 100644 test/defer-logger.js create mode 100644 test/emailer.js create mode 100644 test/feeds.js create mode 100644 test/file.js create mode 100644 test/files/1.css create mode 100644 test/files/1.js create mode 100644 test/files/2.js create mode 100644 test/files/2.less create mode 100644 test/files/503.html create mode 100644 test/files/brokenimage.png create mode 100644 test/files/favicon.ico create mode 100644 test/files/normalise.jpg create mode 100644 test/files/notanimage.png create mode 100644 test/files/test.png create mode 100644 test/files/test.wav create mode 100644 test/files/toobig.png create mode 100644 test/flags.js create mode 100644 test/groups.js create mode 100644 test/helpers/index.js create mode 100644 test/i18n.js create mode 100644 test/image.js create mode 100644 test/locale-detect.js create mode 100644 test/messaging.js create mode 100644 test/meta.js create mode 100644 test/middleware.js create mode 100644 test/mocks/databasemock.js create mode 100644 test/mocks/plugin_modules/@nodebb/another-thing/package.json create mode 100644 test/mocks/plugin_modules/@nodebb/another-thing/plugin.json create mode 100644 test/mocks/plugin_modules/@nodebb/nodebb-plugin-abc/package.json create mode 100644 test/mocks/plugin_modules/@nodebb/nodebb-plugin-abc/plugin.json create mode 100644 test/mocks/plugin_modules/nodebb-plugin-xyz/package.json create mode 100644 test/mocks/plugin_modules/nodebb-plugin-xyz/plugin.json create mode 100644 test/mocks/plugin_modules/something-else/package.json create mode 100644 test/mocks/plugin_modules/something-else/plugin.json create mode 100644 test/notifications.js create mode 100644 test/package-install.js create mode 100644 test/pagination.js create mode 100644 test/password.js create mode 100644 test/plugins-installed.js create mode 100644 test/plugins.js create mode 100644 test/posts.js create mode 100644 test/posts/uploads.js create mode 100644 test/pubsub.js create mode 100644 test/rewards.js create mode 100644 test/search-admin.js create mode 100644 test/search.js create mode 100644 test/settings.js create mode 100644 test/socket.io.js create mode 100644 test/template-helpers.js create mode 100644 test/topics.js create mode 100644 test/topics/events.js create mode 100644 test/topics/thumbs.js create mode 100644 test/translator.js create mode 100644 test/upgrade.js create mode 100644 test/uploads.js create mode 100644 test/user.js create mode 100644 test/user/emails.js create mode 100644 test/user/uploads.js create mode 100644 test/utils.js create mode 100644 themes/nodebb-theme-persona/README.md create mode 100644 themes/nodebb-theme-persona/languages/de/persona.json create mode 100644 themes/nodebb-theme-persona/languages/en-GB/persona.json create mode 100644 themes/nodebb-theme-persona/languages/en-US/persona.json create mode 100644 themes/nodebb-theme-persona/languages/fa-IR/persona.json create mode 100644 themes/nodebb-theme-persona/languages/fr/persona.json create mode 100644 themes/nodebb-theme-persona/languages/hu/persona.json create mode 100644 themes/nodebb-theme-persona/languages/pl/persona.json create mode 100644 themes/nodebb-theme-persona/languages/pt-PT/persona.json create mode 100644 themes/nodebb-theme-persona/languages/tr/persona.json create mode 100644 themes/nodebb-theme-persona/languages/zh-CN/persona.json create mode 100644 themes/nodebb-theme-persona/less/account.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap-flipped.css create mode 100644 themes/nodebb-theme-persona/less/bootstrap/alerts.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/badges.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/bootstrap.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/breadcrumbs.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/button-groups.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/buttons.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/carousel.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/close.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/code.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/component-animations.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/dropdowns.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/forms.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/glyphicons.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/grid.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/input-groups.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/jumbotron.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/labels.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/list-group.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/media.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/alerts.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/background-variant.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/border-radius.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/buttons.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/center-block.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/clearfix.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/forms.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/gradients.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/grid-framework.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/grid.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/hide-text.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/image.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/labels.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/list-group.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/nav-divider.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/nav-vertical-align.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/opacity.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/pagination.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/panels.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/progress-bar.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/reset-filter.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/resize.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/responsive-visibility.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/size.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/tab-focus.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/table-row.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/text-emphasis.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/text-overflow.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/mixins/vendor-prefixes.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/modals.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/navbar.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/navs.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/normalize.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/pager.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/pagination.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/panels.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/popovers.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/print.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/progress-bars.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/responsive-embed.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/responsive-utilities.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/scaffolding.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/tables.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/theme.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/thumbnails.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/tooltip.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/type.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/utilities.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/variables.less create mode 100644 themes/nodebb-theme-persona/less/bootstrap/wells.less create mode 100644 themes/nodebb-theme-persona/less/career.less create mode 100644 themes/nodebb-theme-persona/less/categories.less create mode 100644 themes/nodebb-theme-persona/less/category.less create mode 100644 themes/nodebb-theme-persona/less/chats.less create mode 100644 themes/nodebb-theme-persona/less/flags.less create mode 100644 themes/nodebb-theme-persona/less/footer.less create mode 100644 themes/nodebb-theme-persona/less/groups.less create mode 100644 themes/nodebb-theme-persona/less/header.less create mode 100644 themes/nodebb-theme-persona/less/helpers.less create mode 100644 themes/nodebb-theme-persona/less/ip-blacklist.less create mode 100644 themes/nodebb-theme-persona/less/keyframes.less create mode 100644 themes/nodebb-theme-persona/less/mixins.less create mode 100644 themes/nodebb-theme-persona/less/mobile.less create mode 100644 themes/nodebb-theme-persona/less/modules/alerts.less create mode 100644 themes/nodebb-theme-persona/less/modules/bottom-sheet.less create mode 100644 themes/nodebb-theme-persona/less/modules/composer-default.less create mode 100644 themes/nodebb-theme-persona/less/modules/cookie-consent.less create mode 100644 themes/nodebb-theme-persona/less/modules/fab.less create mode 100644 themes/nodebb-theme-persona/less/modules/morph.less create mode 100644 themes/nodebb-theme-persona/less/modules/necro-post.less create mode 100644 themes/nodebb-theme-persona/less/modules/nprogress.less create mode 100644 themes/nodebb-theme-persona/less/modules/taskbar.less create mode 100644 themes/nodebb-theme-persona/less/modules/usercard.less create mode 100644 themes/nodebb-theme-persona/less/noscript.less create mode 100644 themes/nodebb-theme-persona/less/notifications.less create mode 100644 themes/nodebb-theme-persona/less/outgoing.less create mode 100644 themes/nodebb-theme-persona/less/persona.less create mode 100644 themes/nodebb-theme-persona/less/post-queue.less create mode 100644 themes/nodebb-theme-persona/less/posts_list.less create mode 100644 themes/nodebb-theme-persona/less/register.less create mode 100644 themes/nodebb-theme-persona/less/rtl.less create mode 100644 themes/nodebb-theme-persona/less/search.less create mode 100644 themes/nodebb-theme-persona/less/style.less create mode 100644 themes/nodebb-theme-persona/less/tags.less create mode 100644 themes/nodebb-theme-persona/less/topic.less create mode 100644 themes/nodebb-theme-persona/less/topics_list.less create mode 100644 themes/nodebb-theme-persona/less/users.less create mode 100644 themes/nodebb-theme-persona/less/variables.less create mode 100644 themes/nodebb-theme-persona/lib/controllers.js create mode 100644 themes/nodebb-theme-persona/library.js create mode 100644 themes/nodebb-theme-persona/package.json create mode 100644 themes/nodebb-theme-persona/plugin.json create mode 100644 themes/nodebb-theme-persona/public/admin.js create mode 100644 themes/nodebb-theme-persona/public/modules/autohidingnavbar.js create mode 100644 themes/nodebb-theme-persona/public/modules/quickreply.js create mode 100644 themes/nodebb-theme-persona/public/persona.js create mode 100644 themes/nodebb-theme-persona/public/settings.js create mode 100644 themes/nodebb-theme-persona/screenshot.png create mode 100644 themes/nodebb-theme-persona/templates/account/best.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/blocks.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/bookmarks.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/categories.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/consent.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/controversial.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/downvoted.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/edit.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/edit/password.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/edit/username.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/followers.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/following.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/groups.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/ignored.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/info.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/posts.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/profile.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/sessions.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/settings.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/theme.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/topics.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/uploads.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/upvoted.tpl create mode 100644 themes/nodebb-theme-persona/templates/account/watched.tpl create mode 100644 themes/nodebb-theme-persona/templates/admin/plugins/persona.tpl create mode 100644 themes/nodebb-theme-persona/templates/alert.tpl create mode 100644 themes/nodebb-theme-persona/templates/career.tpl create mode 100644 themes/nodebb-theme-persona/templates/categories.tpl create mode 100644 themes/nodebb-theme-persona/templates/category.tpl create mode 100644 themes/nodebb-theme-persona/templates/chat.tpl create mode 100644 themes/nodebb-theme-persona/templates/chats.tpl create mode 100644 themes/nodebb-theme-persona/templates/confirm.tpl create mode 100644 themes/nodebb-theme-persona/templates/flags/detail.tpl create mode 100644 themes/nodebb-theme-persona/templates/flags/list.tpl create mode 100644 themes/nodebb-theme-persona/templates/footer.tpl create mode 100644 themes/nodebb-theme-persona/templates/groups/details.tpl create mode 100644 themes/nodebb-theme-persona/templates/groups/list.tpl create mode 100644 themes/nodebb-theme-persona/templates/groups/members.tpl create mode 100644 themes/nodebb-theme-persona/templates/header.tpl create mode 100644 themes/nodebb-theme-persona/templates/ip-blacklist.tpl create mode 100644 themes/nodebb-theme-persona/templates/login.tpl create mode 100644 themes/nodebb-theme-persona/templates/modules/taskbar.tpl create mode 100644 themes/nodebb-theme-persona/templates/modules/usercard.tpl create mode 100644 themes/nodebb-theme-persona/templates/notifications.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/acceptTos.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/account/category-item.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/account/header.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/account/menu.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/breadcrumbs.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/buttons/newTopic.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/categories/item.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/categories/lastpost.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/categories/link.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category-filter-content.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category-filter-right.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category-filter.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category-selector-content.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category-selector-right.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category-selector.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category/sort.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category/subcategory.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category/tags.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category/tools.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/category/watch.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/change_owner_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats-menu.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/dropdown.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/message-window.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/message.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/messages.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/options.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/recent_room.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/system-message.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/chats/user.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/cookie-consent.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/delete_posts_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/flags/filters.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/fork_thread_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/groups/list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/groups/memberlist.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/menu.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/merge_topics_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/change_picture_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/flag_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/manage_room.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/manage_room_users.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/post_history.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/rename_room.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/upload_file_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/upload_picture_from_url_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/modals/votes_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/move_thread_modal.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/notifications_list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/paginator.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/post_bar.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/posts_list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/posts_list_item.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/quick-search-results.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/search-results.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/slideout-menu.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/tags_list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/thread_tools.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/badge.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/browsing-users.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/deleted-message.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/navigation-post.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/navigator.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/necro-post.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/post-editor.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/post-menu-list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/post-menu.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/post.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/quickreply.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/reactions.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/reply-button.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/selection-tooltip.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/sort.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/stats.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/tags.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/topic-menu-list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topic/watch.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/topics_list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/users_list.tpl create mode 100644 themes/nodebb-theme-persona/templates/partials/users_list_menu.tpl create mode 100644 themes/nodebb-theme-persona/templates/popular.tpl create mode 100644 themes/nodebb-theme-persona/templates/post-queue.tpl create mode 100644 themes/nodebb-theme-persona/templates/recent.tpl create mode 100644 themes/nodebb-theme-persona/templates/register.tpl create mode 100644 themes/nodebb-theme-persona/templates/registerComplete.tpl create mode 100644 themes/nodebb-theme-persona/templates/reset.tpl create mode 100644 themes/nodebb-theme-persona/templates/reset_code.tpl create mode 100644 themes/nodebb-theme-persona/templates/search.tpl create mode 100644 themes/nodebb-theme-persona/templates/tag.tpl create mode 100644 themes/nodebb-theme-persona/templates/tags.tpl create mode 100644 themes/nodebb-theme-persona/templates/top.tpl create mode 100644 themes/nodebb-theme-persona/templates/topic.tpl create mode 100644 themes/nodebb-theme-persona/templates/tos.tpl create mode 100644 themes/nodebb-theme-persona/templates/unread.tpl create mode 100644 themes/nodebb-theme-persona/templates/unsubscribe.tpl create mode 100644 themes/nodebb-theme-persona/templates/users.tpl create mode 100644 themes/nodebb-theme-persona/theme.json create mode 100644 themes/nodebb-theme-persona/theme.less create mode 100644 tsconfig.json create mode 100644 webpack.common.js create mode 100644 webpack.dev.js create mode 100644 webpack.installer.js create mode 100644 webpack.prod.js diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000000..d40cc58e75 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,30 @@ +# Save as .codeclimate.yml (note leading .) in project root directory +version: "2" +languages: + Ruby: false + JavaScript: true + PHP: false +checks: + file-lines: + config: + threshold: 500 + method-lines: + config: + threshold: 75 + method-complexity: + config: + threshold: 10 + similar-code: + config: + threshold: 65 +plugins: + duplication: + enabled: true + config: + languages: + javascript: + mass_threshold: 110 + count_threshold: 3 +exclude_paths: +- "public/vendor/*" +- "test/*" \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..8f8a24bdf9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[{*.js, *.css, *.tpl, *.json, *.ts}] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..5afc24411a --- /dev/null +++ b/.eslintignore @@ -0,0 +1,30 @@ +node_modules/ +*.sublime-project +*.sublime-workspace +.project +.vagrant +.DS_Store +logs/ +/public/templates +/public/uploads +/public/vendor +/public/src/modules/string.js +.idea/ +.vscode/ +*.ipr +*.iws +/coverage +/build +.eslintrc +test/files +*.min.js + +/public/src/app.js +/public/src/client.js +/public/src/admin/admin.js +/public/src/modules/translator.common.js +/public/src/modules/pictureCropper.js +/public/src/modules/ace-editor.js +/public/src/client/account/header.js +/public/src/client/test.js +themes/ \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000..4ed43ffcc4 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,70 @@ + +const fs = require('fs'); +const path = require('path'); + +// Directories that contain TypeScript files of note +const tsDirs = [ + "public/src", + "src", + "test", +]; + +// Helper walk function to check all directories +function walk(dir) { + var results = []; + var list = fs.readdirSync(dir); + list.forEach(function(file) { + file = dir + '/' + file; + var stat = fs.statSync(file); + if (stat && stat.isDirectory()) { + results = results.concat(walk(file)); + } else { + results.push(file); + } + }); + return results; +} + +// Find all JS files that were compiled from TS +function find_compiled_js() { + let jsFilesList = []; + + tsDirs.forEach(tsDir => { + let filesList = walk(tsDir); + const tsFilesList = filesList.filter((file) => path.extname(file).toLowerCase() === '.ts'); + jsFilesList = jsFilesList.concat(filesList.filter( + (file) => path.extname(file).toLowerCase() === '.js' && + tsFilesList.find(tsFile => tsFile === (file.replace(/\.[^/.]+$/, "") + ".ts")) !== undefined)); + }); + + if (jsFilesList.length == 0) return ""; + return jsFilesList; + } + + module.exports = { + extends: ["nodebb"], + root: true, + ignorePatterns: find_compiled_js(), + rules: { + "indent": ["error", 4] + }, + overrides: [ + { + files: ["**/*.ts", "**/*.tsx"], + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + parserOptions: { + ecmaFeatures: { jsx: true }, + project: "./tsconfig.json" + }, + rules: { + "no-use-before-define": "off", + "@typescript-eslint/no-use-before-define": "error", + } + } + ] + }; \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..e0fa7609a5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# These files are text and should be normalized (convert crlf => lf) +*.json text +*.css text +*.less text +*.tpl text +*.html text +*.js text +*.md text + +# Images should be treated as binary +# (binary is a macro for -text -diff) +*.png binary +*.jpg binary \ No newline at end of file diff --git a/.github/workflows/hw1.yaml b/.github/workflows/hw1.yaml new file mode 100644 index 0000000000..2c334a0e0e --- /dev/null +++ b/.github/workflows/hw1.yaml @@ -0,0 +1,122 @@ +name: Homework 1 Check + +on: + pull_request: + branches: + - main + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + hw1: + permissions: + checks: write + contents: read + name: Homework 1 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [14] + runs-on: ${{ matrix.os }} + env: + TEST_ENV: ${{ matrix.test_env || 'production' }} + + services: + mongo: + image: 'mongo:3.7' + ports: + # Maps port 27017 on service container to the host + - 27017:27017 + + steps: + - uses: actions/checkout@v3 + + - run: cp install/package.json package.json + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: NPM Install + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: Setup on MongoDB + env: + SETUP: >- + { + "url": "http://127.0.0.1:4567", + "secret": "abcdef", + "admin:username": "admin", + "admin:email": "test@example.org", + "admin:password": "hAN3Eg8W", + "admin:password:confirm": "hAN3Eg8W", + + "database": "mongo", + "mongo:host": "127.0.0.1", + "mongo:port": 27017, + "mongo:username": "", + "mongo:password": "", + "mongo:database": "nodebb" + } + CI: >- + { + "host": "127.0.0.1", + "port": 27017, + "database": "ci_test" + } + run: | + node app --setup="${SETUP}" --ci="${CI}" + + - name: Get specific changed files + id: changed-files-specific + uses: tj-actions/changed-files@v24 + with: + files: | + ./**/*.js + ./**/*.ts + + - name: Check that HW1 specifications are met + run: | + IFS=' ' read -r -a addedarray <<< "${{ steps.changed-files-specific.outputs.added_files }}" + IFS=' ' read -r -a modifiedarray <<< "${{ steps.changed-files-specific.outputs.modified_files }}" + + PASS="false" + for addedfile in "${addedarray[@]}" + do + IFS='. ' read -r -a addfilesplit <<< $addedfile + ADDFILENAME="${addfilesplit[0]}" + ADDFILEEXT="${addfilesplit[1]}" + + if [ "$ADDFILEEXT" == "ts" ] + then + for modifiedfile in "${modifiedarray[@]}" + do + IFS='. ' read -r -a modfilesplit <<< $modifiedfile + MODFILENAME="${modfilesplit[0]}" + MODFILEEXT="${modfilesplit[1]}" + + if [ "$MODFILEEXT" == "js" ] && [ "$ADDFILENAME" == "$MODFILENAME" ] + then + echo "Found suitable file!" + PASS="true" + fi + done + fi + done + + if [ "$PASS" != "true" ] + then + echo "No suitable files found that match the requirements of HW1. Please check that you have translated a JS file into TypeScript and regenerated the JavaScript file using % npx tsc" + echo "Files added (should see your xx.ts file): ${{ steps.changed-files-specific.outputs.added_files }}" + echo "Files modified (should see your xx.js file): ${{ steps.changed-files-specific.outputs.modified_files }}" + exit 1 + fi diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000000..18839f5dd9 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,83 @@ +name: Lint + +on: + push: + branches: + - main + pull_request: + branches: + - main + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + lint: + permissions: + checks: write # for coverallsapp/github-action to create new checks + contents: read # for actions/checkout to fetch code + name: Lint + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [14] + runs-on: ${{ matrix.os }} + env: + TEST_ENV: ${{ matrix.test_env || 'production' }} + + services: + mongo: + image: 'mongo:3.7' + ports: + # Maps port 27017 on service container to the host + - 27017:27017 + + steps: + - uses: actions/checkout@v3 + + - run: cp install/package.json package.json + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: NPM Install + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: Setup on MongoDB + env: + SETUP: >- + { + "url": "http://127.0.0.1:4567", + "secret": "abcdef", + "admin:username": "admin", + "admin:email": "test@example.org", + "admin:password": "hAN3Eg8W", + "admin:password:confirm": "hAN3Eg8W", + + "database": "mongo", + "mongo:host": "127.0.0.1", + "mongo:port": 27017, + "mongo:username": "", + "mongo:password": "", + "mongo:database": "nodebb" + } + CI: >- + { + "host": "127.0.0.1", + "port": 27017, + "database": "ci_test" + } + run: | + node app --setup="${SETUP}" --ci="${CI}" + + - name: Run ESLint + run: npm run lint diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000000..015e096deb --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,201 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + test: + permissions: + checks: write # for coverallsapp/github-action to create new checks + contents: read # for actions/checkout to fetch code + name: Test + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + node: [14, 16, 18] + database: [mongo-dev, mongo, redis, postgres] + include: + # only run coverage once + - os: ubuntu-latest + node: 14 + coverage: true + # test under development once + - database: mongo-dev + test_env: development + runs-on: ${{ matrix.os }} + env: + TEST_ENV: ${{ matrix.test_env || 'production' }} + + services: + postgres: + image: 'postgres:14-alpine' + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps port 5432 on service container to the host + - 5432:5432 + + redis: + image: 'redis:2.8.23' + # Set health checks to wait until redis has started + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + # Maps port 6379 on service container to the host + - 6379:6379 + + mongo: + image: 'mongo:3.7' + ports: + # Maps port 27017 on service container to the host + - 27017:27017 + + steps: + - uses: actions/checkout@v3 + + - run: cp install/package.json package.json + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: NPM Install + uses: bahmutov/npm-install@v1 + with: + useLockFile: false + + - name: Setup on MongoDB + if: startsWith(matrix.database, 'mongo') + env: + SETUP: >- + { + "url": "http://127.0.0.1:4567", + "secret": "abcdef", + "admin:username": "admin", + "admin:email": "test@example.org", + "admin:password": "hAN3Eg8W", + "admin:password:confirm": "hAN3Eg8W", + + "database": "mongo", + "mongo:host": "127.0.0.1", + "mongo:port": 27017, + "mongo:username": "", + "mongo:password": "", + "mongo:database": "nodebb" + } + CI: >- + { + "host": "127.0.0.1", + "port": 27017, + "database": "ci_test" + } + run: | + node app --setup="${SETUP}" --ci="${CI}" + + - name: Setup on PostgreSQL + if: startsWith(matrix.database, 'postgres') + env: + SETUP: >- + { + "url": "http://127.0.0.1:4567", + "secret": "abcdef", + "admin:username": "admin", + "admin:email": "test@example.org", + "admin:password": "hAN3Eg8W", + "admin:password:confirm": "hAN3Eg8W", + + "database": "postgres", + "postgres:host": "127.0.0.1", + "postgres:port": 5432, + "postgres:username": "postgres", + "postgres:password": "postgres", + "postgres:database": "nodebb" + } + CI: >- + { + "host": "127.0.0.1", + "database": "ci_test", + "port": 5432, + "username": "postgres", + "password": "postgres" + } + run: | + node -e "const { Client } = require('pg'); const c = new Client({ host: '127.0.0.1', port: 5432, user: 'postgres', password: 'postgres' }); c.connect().then(() => c.query('CREATE DATABASE nodebb')).then(() => c.query('CREATE DATABASE ci_test')).then(() => c.end())" + node app --setup="${SETUP}" --ci="${CI}" + + - name: Setup on Redis + if: startsWith(matrix.database, 'redis') + env: + SETUP: >- + { + "url": "http://127.0.0.1:4567/forum", + "secret": "abcdef", + "admin:username": "admin", + "admin:email": "test@example.org", + "admin:password": "hAN3Eg8W", + "admin:password:confirm": "hAN3Eg8W", + + "database": "redis", + "redis:host": "127.0.0.1", + "redis:port": 6379, + "redis:password": "", + "redis:database": 0 + } + CI: >- + { + "host": "127.0.0.1", + "database": 1, + "port": 6379 + } + run: | + node app --setup="${SETUP}" --ci="${CI}" + + - name: Node tests + run: npm test + + - name: Extract coverage info + run: npm run coverage + + - name: Test coverage + uses: coverallsapp/github-action@1.1.3 + if: matrix.coverage + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + flag-name: ${{ matrix.os }}-node-${{ matrix.node }}-db-${{ matrix.database }} + parallel: true + + finish: + permissions: + checks: write # for coverallsapp/github-action to create new checks + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@1.1.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + parallel-finished: true diff --git a/.github/workflows/volunteers.yaml b/.github/workflows/volunteers.yaml new file mode 100644 index 0000000000..9b350dd8b4 --- /dev/null +++ b/.github/workflows/volunteers.yaml @@ -0,0 +1,11 @@ +name: "Issue volunteer assignment" + +on: [issue_comment] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: bhermann/issue-volunteer@v0.1.12 + with: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..5a176ec440 --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +dist/ +yarn.lock +npm-debug.log +node_modules/ +sftp-config.json +config.json +jsconfig.json +public/src/nodebb.min.js +!src/views/config.json +public/css/*.css +*.sublime-project +*.sublime-workspace +.project +*.swp +Vagrantfile +.vagrant +provision.sh +*.komodoproject +.DS_Store +feeds/recent.rss +.eslintcache +.svn +dump.rdb + +logs/ + +pidfile + +# templates +/public/templates + +/public/uploads +/test/uploads + +# compiled files +/public/stylesheet.css +/public/admin.css +/public/nodebb.min.js +/public/nodebb.min.js.map +/public/acp.min.js +/public/acp.min.js.map +/public/installer.css +/public/installer.min.js +/public/bootstrap.min.css +/public/logo.png + +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio +*.iml + +## Directory-based project format: +.idea/ +.vscode/ + +## File-based project format: +*.ipr +*.iws + +## Transifex +tx.exe +.transifexrc + +##Coverage output +coverage +.nyc_output + +*.log +test/files/normalise.jpg.png +test/files/normalise-resized.jpg +package-lock.json +/package.json +*.mongodb +link-plugins.sh +test.sh + +# Theme ignore +theme/*.css +!theme/less/bootstrap-flipped.css +theme/npm-debug.log +theme/sftp-config.json +theme/*.sublime-project +theme/*.sublime-workspace +theme/.idea +theme/.vscode +theme/node_modules/ diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000000..31354ec138 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000000..e8511eaeaf --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..d37daa075e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install lint-staged diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 0000000000..39a95db6d6 --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1,4 @@ +reporter: dot # For a more verbose display - spec +timeout: 25000 +exit: true +bail: true diff --git a/.tx/config b/.tx/config new file mode 100644 index 0000000000..c988effda2 --- /dev/null +++ b/.tx/config @@ -0,0 +1,3746 @@ +[main] +host = https://www.transifex.com + +[o:nodebb:p:nodebb:r:admin-admin] +file_filter = public/language//admin/admin.json +source_file = public/language/en-GB/admin/admin.json +source_lang = en_GB +type = KEYVALUEJSON +trans.pt_BR = public/language/pt-BR/admin/admin.json +trans.en@pirate = public/language/en-x-pirate/admin/admin.json +trans.hy = public/language/hy/admin/admin.json +trans.pl = public/language/pl/admin/admin.json +trans.th = public/language/th/admin/admin.json +trans.de = public/language/de/admin/admin.json +trans.el = public/language/el/admin/admin.json +trans.ar = public/language/ar/admin/admin.json +trans.bg = public/language/bg/admin/admin.json +trans.bn = public/language/bn/admin/admin.json +trans.it = public/language/it/admin/admin.json +trans.lv = public/language/lv/admin/admin.json +trans.nl = public/language/nl/admin/admin.json +trans.ru = public/language/ru/admin/admin.json +trans.sr = public/language/sr/admin/admin.json +trans.sv = public/language/sv/admin/admin.json +trans.uk = public/language/uk/admin/admin.json +trans.zh_CN = public/language/zh-CN/admin/admin.json +trans.fa_IR = public/language/fa-IR/admin/admin.json +trans.id = public/language/id/admin/admin.json +trans.ms = public/language/ms/admin/admin.json +trans.nb = public/language/nb/admin/admin.json +trans.sk = public/language/sk/admin/admin.json +trans.cs = public/language/cs/admin/admin.json +trans.fi = public/language/fi/admin/admin.json +trans.fr = public/language/fr/admin/admin.json +trans.rw = public/language/rw/admin/admin.json +trans.sq_AL = public/language/sq-AL/admin/admin.json +trans.en_US = public/language/en-US/admin/admin.json +trans.es = public/language/es/admin/admin.json +trans.et = public/language/et/admin/admin.json +trans.gl = public/language/gl/admin/admin.json +trans.he = public/language/he/admin/admin.json +trans.ja = public/language/ja/admin/admin.json +trans.lt = public/language/lt/admin/admin.json +trans.pt_PT = public/language/pt-PT/admin/admin.json +trans.sc = public/language/sc/admin/admin.json +trans.sl = public/language/sl/admin/admin.json +trans.vi = public/language/vi/admin/admin.json +trans.da = public/language/da/admin/admin.json +trans.hr = public/language/hr/admin/admin.json +trans.hu = public/language/hu/admin/admin.json +trans.ko = public/language/ko/admin/admin.json +trans.ro = public/language/ro/admin/admin.json +trans.tr = public/language/tr/admin/admin.json +trans.zh_TW = public/language/zh-TW/admin/admin.json + +[o:nodebb:p:nodebb:r:admin-advanced-cache] +file_filter = public/language//admin/advanced/cache.json +source_file = public/language/en-GB/admin/advanced/cache.json +source_lang = en_GB +type = KEYVALUEJSON +trans.fa_IR = public/language/fa-IR/admin/advanced/cache.json +trans.he = public/language/he/admin/advanced/cache.json +trans.ko = public/language/ko/admin/advanced/cache.json +trans.pt_PT = public/language/pt-PT/admin/advanced/cache.json +trans.rw = public/language/rw/admin/advanced/cache.json +trans.tr = public/language/tr/admin/advanced/cache.json +trans.en_US = public/language/en-US/admin/advanced/cache.json +trans.hu = public/language/hu/admin/advanced/cache.json +trans.id = public/language/id/admin/advanced/cache.json +trans.ms = public/language/ms/admin/advanced/cache.json +trans.sc = public/language/sc/admin/advanced/cache.json +trans.sv = public/language/sv/admin/advanced/cache.json +trans.es = public/language/es/admin/advanced/cache.json +trans.gl = public/language/gl/admin/advanced/cache.json +trans.lv = public/language/lv/admin/advanced/cache.json +trans.nl = public/language/nl/admin/advanced/cache.json +trans.ru = public/language/ru/admin/advanced/cache.json +trans.zh_TW = public/language/zh-TW/admin/advanced/cache.json +trans.bn = public/language/bn/admin/advanced/cache.json +trans.en@pirate = public/language/en-x-pirate/admin/advanced/cache.json +trans.fr = public/language/fr/admin/advanced/cache.json +trans.hr = public/language/hr/admin/advanced/cache.json +trans.pt_BR = public/language/pt-BR/admin/advanced/cache.json +trans.ar = public/language/ar/admin/advanced/cache.json +trans.bg = public/language/bg/admin/advanced/cache.json +trans.cs = public/language/cs/admin/advanced/cache.json +trans.lt = public/language/lt/admin/advanced/cache.json +trans.nb = public/language/nb/admin/advanced/cache.json +trans.uk = public/language/uk/admin/advanced/cache.json +trans.el = public/language/el/admin/advanced/cache.json +trans.pl = public/language/pl/admin/advanced/cache.json +trans.sr = public/language/sr/admin/advanced/cache.json +trans.da = public/language/da/admin/advanced/cache.json +trans.et = public/language/et/admin/advanced/cache.json +trans.fi = public/language/fi/admin/advanced/cache.json +trans.hy = public/language/hy/admin/advanced/cache.json +trans.ja = public/language/ja/admin/advanced/cache.json +trans.sl = public/language/sl/admin/advanced/cache.json +trans.sq_AL = public/language/sq-AL/admin/advanced/cache.json +trans.th = public/language/th/admin/advanced/cache.json +trans.de = public/language/de/admin/advanced/cache.json +trans.it = public/language/it/admin/advanced/cache.json +trans.ro = public/language/ro/admin/advanced/cache.json +trans.sk = public/language/sk/admin/advanced/cache.json +trans.vi = public/language/vi/admin/advanced/cache.json +trans.zh_CN = public/language/zh-CN/admin/advanced/cache.json + +[o:nodebb:p:nodebb:r:admin-advanced-database] +file_filter = public/language//admin/advanced/database.json +source_file = public/language/en-GB/admin/advanced/database.json +source_lang = en_GB +type = KEYVALUEJSON +trans.fr = public/language/fr/admin/advanced/database.json +trans.he = public/language/he/admin/advanced/database.json +trans.ja = public/language/ja/admin/advanced/database.json +trans.lv = public/language/lv/admin/advanced/database.json +trans.pt_PT = public/language/pt-PT/admin/advanced/database.json +trans.en_US = public/language/en-US/admin/advanced/database.json +trans.el = public/language/el/admin/advanced/database.json +trans.tr = public/language/tr/admin/advanced/database.json +trans.zh_CN = public/language/zh-CN/admin/advanced/database.json +trans.ar = public/language/ar/admin/advanced/database.json +trans.hr = public/language/hr/admin/advanced/database.json +trans.lt = public/language/lt/admin/advanced/database.json +trans.ro = public/language/ro/admin/advanced/database.json +trans.zh_TW = public/language/zh-TW/admin/advanced/database.json +trans.gl = public/language/gl/admin/advanced/database.json +trans.it = public/language/it/admin/advanced/database.json +trans.nb = public/language/nb/admin/advanced/database.json +trans.bn = public/language/bn/admin/advanced/database.json +trans.da = public/language/da/admin/advanced/database.json +trans.de = public/language/de/admin/advanced/database.json +trans.en@pirate = public/language/en-x-pirate/admin/advanced/database.json +trans.hu = public/language/hu/admin/advanced/database.json +trans.hy = public/language/hy/admin/advanced/database.json +trans.id = public/language/id/admin/advanced/database.json +trans.nl = public/language/nl/admin/advanced/database.json +trans.cs = public/language/cs/admin/advanced/database.json +trans.sv = public/language/sv/admin/advanced/database.json +trans.sr = public/language/sr/admin/advanced/database.json +trans.pl = public/language/pl/admin/advanced/database.json +trans.ru = public/language/ru/admin/advanced/database.json +trans.sc = public/language/sc/admin/advanced/database.json +trans.sl = public/language/sl/admin/advanced/database.json +trans.sq_AL = public/language/sq-AL/admin/advanced/database.json +trans.th = public/language/th/admin/advanced/database.json +trans.et = public/language/et/admin/advanced/database.json +trans.fa_IR = public/language/fa-IR/admin/advanced/database.json +trans.ko = public/language/ko/admin/advanced/database.json +trans.sk = public/language/sk/admin/advanced/database.json +trans.vi = public/language/vi/admin/advanced/database.json +trans.es = public/language/es/admin/advanced/database.json +trans.fi = public/language/fi/admin/advanced/database.json +trans.ms = public/language/ms/admin/advanced/database.json +trans.pt_BR = public/language/pt-BR/admin/advanced/database.json +trans.rw = public/language/rw/admin/advanced/database.json +trans.uk = public/language/uk/admin/advanced/database.json +trans.bg = public/language/bg/admin/advanced/database.json + +[o:nodebb:p:nodebb:r:admin-advanced-errors] +file_filter = public/language//admin/advanced/errors.json +source_file = public/language/en-GB/admin/advanced/errors.json +source_lang = en_GB +type = KEYVALUEJSON +trans.cs = public/language/cs/admin/advanced/errors.json +trans.en_US = public/language/en-US/admin/advanced/errors.json +trans.es = public/language/es/admin/advanced/errors.json +trans.pl = public/language/pl/admin/advanced/errors.json +trans.sk = public/language/sk/admin/advanced/errors.json +trans.uk = public/language/uk/admin/advanced/errors.json +trans.da = public/language/da/admin/advanced/errors.json +trans.gl = public/language/gl/admin/advanced/errors.json +trans.hu = public/language/hu/admin/advanced/errors.json +trans.pt_PT = public/language/pt-PT/admin/advanced/errors.json +trans.vi = public/language/vi/admin/advanced/errors.json +trans.de = public/language/de/admin/advanced/errors.json +trans.ko = public/language/ko/admin/advanced/errors.json +trans.nb = public/language/nb/admin/advanced/errors.json +trans.sc = public/language/sc/admin/advanced/errors.json +trans.sl = public/language/sl/admin/advanced/errors.json +trans.tr = public/language/tr/admin/advanced/errors.json +trans.zh_CN = public/language/zh-CN/admin/advanced/errors.json +trans.fr = public/language/fr/admin/advanced/errors.json +trans.hy = public/language/hy/admin/advanced/errors.json +trans.pt_BR = public/language/pt-BR/admin/advanced/errors.json +trans.ro = public/language/ro/admin/advanced/errors.json +trans.sr = public/language/sr/admin/advanced/errors.json +trans.sv = public/language/sv/admin/advanced/errors.json +trans.fa_IR = public/language/fa-IR/admin/advanced/errors.json +trans.it = public/language/it/admin/advanced/errors.json +trans.lt = public/language/lt/admin/advanced/errors.json +trans.ms = public/language/ms/admin/advanced/errors.json +trans.ru = public/language/ru/admin/advanced/errors.json +trans.sq_AL = public/language/sq-AL/admin/advanced/errors.json +trans.ar = public/language/ar/admin/advanced/errors.json +trans.fi = public/language/fi/admin/advanced/errors.json +trans.he = public/language/he/admin/advanced/errors.json +trans.ja = public/language/ja/admin/advanced/errors.json +trans.th = public/language/th/admin/advanced/errors.json +trans.zh_TW = public/language/zh-TW/admin/advanced/errors.json +trans.bn = public/language/bn/admin/advanced/errors.json +trans.en@pirate = public/language/en-x-pirate/admin/advanced/errors.json +trans.hr = public/language/hr/admin/advanced/errors.json +trans.rw = public/language/rw/admin/advanced/errors.json +trans.bg = public/language/bg/admin/advanced/errors.json +trans.el = public/language/el/admin/advanced/errors.json +trans.et = public/language/et/admin/advanced/errors.json +trans.id = public/language/id/admin/advanced/errors.json +trans.lv = public/language/lv/admin/advanced/errors.json +trans.nl = public/language/nl/admin/advanced/errors.json + +[o:nodebb:p:nodebb:r:admin-advanced-events] +file_filter = public/language//admin/advanced/events.json +source_file = public/language/en-GB/admin/advanced/events.json +source_lang = en_GB +type = KEYVALUEJSON +trans.bg = public/language/bg/admin/advanced/events.json +trans.el = public/language/el/admin/advanced/events.json +trans.lt = public/language/lt/admin/advanced/events.json +trans.pt_BR = public/language/pt-BR/admin/advanced/events.json +trans.zh_TW = public/language/zh-TW/admin/advanced/events.json +trans.th = public/language/th/admin/advanced/events.json +trans.gl = public/language/gl/admin/advanced/events.json +trans.ko = public/language/ko/admin/advanced/events.json +trans.nl = public/language/nl/admin/advanced/events.json +trans.ro = public/language/ro/admin/advanced/events.json +trans.sl = public/language/sl/admin/advanced/events.json +trans.ar = public/language/ar/admin/advanced/events.json +trans.hr = public/language/hr/admin/advanced/events.json +trans.vi = public/language/vi/admin/advanced/events.json +trans.fa_IR = public/language/fa-IR/admin/advanced/events.json +trans.ms = public/language/ms/admin/advanced/events.json +trans.tr = public/language/tr/admin/advanced/events.json +trans.nb = public/language/nb/admin/advanced/events.json +trans.pt_PT = public/language/pt-PT/admin/advanced/events.json +trans.sk = public/language/sk/admin/advanced/events.json +trans.en@pirate = public/language/en-x-pirate/admin/advanced/events.json +trans.es = public/language/es/admin/advanced/events.json +trans.et = public/language/et/admin/advanced/events.json +trans.he = public/language/he/admin/advanced/events.json +trans.lv = public/language/lv/admin/advanced/events.json +trans.sv = public/language/sv/admin/advanced/events.json +trans.zh_CN = public/language/zh-CN/admin/advanced/events.json +trans.pl = public/language/pl/admin/advanced/events.json +trans.rw = public/language/rw/admin/advanced/events.json +trans.cs = public/language/cs/admin/advanced/events.json +trans.de = public/language/de/admin/advanced/events.json +trans.fr = public/language/fr/admin/advanced/events.json +trans.hy = public/language/hy/admin/advanced/events.json +trans.id = public/language/id/admin/advanced/events.json +trans.ru = public/language/ru/admin/advanced/events.json +trans.sq_AL = public/language/sq-AL/admin/advanced/events.json +trans.uk = public/language/uk/admin/advanced/events.json +trans.da = public/language/da/admin/advanced/events.json +trans.en_US = public/language/en-US/admin/advanced/events.json +trans.hu = public/language/hu/admin/advanced/events.json +trans.it = public/language/it/admin/advanced/events.json +trans.ja = public/language/ja/admin/advanced/events.json +trans.bn = public/language/bn/admin/advanced/events.json +trans.fi = public/language/fi/admin/advanced/events.json +trans.sc = public/language/sc/admin/advanced/events.json +trans.sr = public/language/sr/admin/advanced/events.json + +[o:nodebb:p:nodebb:r:admin-advanced-logs] +file_filter = public/language//admin/advanced/logs.json +source_file = public/language/en-GB/admin/advanced/logs.json +source_lang = en_GB +type = KEYVALUEJSON +trans.nl = public/language/nl/admin/advanced/logs.json +trans.ru = public/language/ru/admin/advanced/logs.json +trans.vi = public/language/vi/admin/advanced/logs.json +trans.id = public/language/id/admin/advanced/logs.json +trans.fi = public/language/fi/admin/advanced/logs.json +trans.he = public/language/he/admin/advanced/logs.json +trans.hr = public/language/hr/admin/advanced/logs.json +trans.ja = public/language/ja/admin/advanced/logs.json +trans.lt = public/language/lt/admin/advanced/logs.json +trans.nb = public/language/nb/admin/advanced/logs.json +trans.rw = public/language/rw/admin/advanced/logs.json +trans.es = public/language/es/admin/advanced/logs.json +trans.sv = public/language/sv/admin/advanced/logs.json +trans.fa_IR = public/language/fa-IR/admin/advanced/logs.json +trans.fr = public/language/fr/admin/advanced/logs.json +trans.gl = public/language/gl/admin/advanced/logs.json +trans.zh_TW = public/language/zh-TW/admin/advanced/logs.json +trans.en@pirate = public/language/en-x-pirate/admin/advanced/logs.json +trans.sc = public/language/sc/admin/advanced/logs.json +trans.sq_AL = public/language/sq-AL/admin/advanced/logs.json +trans.uk = public/language/uk/admin/advanced/logs.json +trans.hu = public/language/hu/admin/advanced/logs.json +trans.bn = public/language/bn/admin/advanced/logs.json +trans.de = public/language/de/admin/advanced/logs.json +trans.it = public/language/it/admin/advanced/logs.json +trans.lv = public/language/lv/admin/advanced/logs.json +trans.ms = public/language/ms/admin/advanced/logs.json +trans.pl = public/language/pl/admin/advanced/logs.json +trans.zh_CN = public/language/zh-CN/admin/advanced/logs.json +trans.ar = public/language/ar/admin/advanced/logs.json +trans.et = public/language/et/admin/advanced/logs.json +trans.hy = public/language/hy/admin/advanced/logs.json +trans.ko = public/language/ko/admin/advanced/logs.json +trans.th = public/language/th/admin/advanced/logs.json +trans.tr = public/language/tr/admin/advanced/logs.json +trans.cs = public/language/cs/admin/advanced/logs.json +trans.ro = public/language/ro/admin/advanced/logs.json +trans.pt_BR = public/language/pt-BR/admin/advanced/logs.json +trans.da = public/language/da/admin/advanced/logs.json +trans.el = public/language/el/admin/advanced/logs.json +trans.en_US = public/language/en-US/admin/advanced/logs.json +trans.pt_PT = public/language/pt-PT/admin/advanced/logs.json +trans.sk = public/language/sk/admin/advanced/logs.json +trans.sl = public/language/sl/admin/advanced/logs.json +trans.sr = public/language/sr/admin/advanced/logs.json +trans.bg = public/language/bg/admin/advanced/logs.json + +[o:nodebb:p:nodebb:r:admin-appearance-customise] +file_filter = public/language//admin/appearance/customise.json +source_file = public/language/en-GB/admin/appearance/customise.json +source_lang = en_GB +type = KEYVALUEJSON +trans.cs = public/language/cs/admin/appearance/customise.json +trans.da = public/language/da/admin/appearance/customise.json +trans.es = public/language/es/admin/appearance/customise.json +trans.hy = public/language/hy/admin/appearance/customise.json +trans.th = public/language/th/admin/appearance/customise.json +trans.ar = public/language/ar/admin/appearance/customise.json +trans.bg = public/language/bg/admin/appearance/customise.json +trans.he = public/language/he/admin/appearance/customise.json +trans.lt = public/language/lt/admin/appearance/customise.json +trans.nb = public/language/nb/admin/appearance/customise.json +trans.pl = public/language/pl/admin/appearance/customise.json +trans.sl = public/language/sl/admin/appearance/customise.json +trans.zh_TW = public/language/zh-TW/admin/appearance/customise.json +trans.bn = public/language/bn/admin/appearance/customise.json +trans.el = public/language/el/admin/appearance/customise.json +trans.hu = public/language/hu/admin/appearance/customise.json +trans.sk = public/language/sk/admin/appearance/customise.json +trans.tr = public/language/tr/admin/appearance/customise.json +trans.et = public/language/et/admin/appearance/customise.json +trans.id = public/language/id/admin/appearance/customise.json +trans.lv = public/language/lv/admin/appearance/customise.json +trans.pt_BR = public/language/pt-BR/admin/appearance/customise.json +trans.sr = public/language/sr/admin/appearance/customise.json +trans.zh_CN = public/language/zh-CN/admin/appearance/customise.json +trans.fr = public/language/fr/admin/appearance/customise.json +trans.hr = public/language/hr/admin/appearance/customise.json +trans.it = public/language/it/admin/appearance/customise.json +trans.nl = public/language/nl/admin/appearance/customise.json +trans.sv = public/language/sv/admin/appearance/customise.json +trans.fi = public/language/fi/admin/appearance/customise.json +trans.gl = public/language/gl/admin/appearance/customise.json +trans.ko = public/language/ko/admin/appearance/customise.json +trans.sc = public/language/sc/admin/appearance/customise.json +trans.de = public/language/de/admin/appearance/customise.json +trans.en_US = public/language/en-US/admin/appearance/customise.json +trans.ja = public/language/ja/admin/appearance/customise.json +trans.ru = public/language/ru/admin/appearance/customise.json +trans.rw = public/language/rw/admin/appearance/customise.json +trans.vi = public/language/vi/admin/appearance/customise.json +trans.en@pirate = public/language/en-x-pirate/admin/appearance/customise.json +trans.fa_IR = public/language/fa-IR/admin/appearance/customise.json +trans.ms = public/language/ms/admin/appearance/customise.json +trans.pt_PT = public/language/pt-PT/admin/appearance/customise.json +trans.ro = public/language/ro/admin/appearance/customise.json +trans.sq_AL = public/language/sq-AL/admin/appearance/customise.json +trans.uk = public/language/uk/admin/appearance/customise.json + +[o:nodebb:p:nodebb:r:admin-appearance-skins] +file_filter = public/language//admin/appearance/skins.json +source_file = public/language/en-GB/admin/appearance/skins.json +source_lang = en_GB +type = KEYVALUEJSON +trans.et = public/language/et/admin/appearance/skins.json +trans.he = public/language/he/admin/appearance/skins.json +trans.hr = public/language/hr/admin/appearance/skins.json +trans.sc = public/language/sc/admin/appearance/skins.json +trans.sk = public/language/sk/admin/appearance/skins.json +trans.uk = public/language/uk/admin/appearance/skins.json +trans.vi = public/language/vi/admin/appearance/skins.json +trans.bn = public/language/bn/admin/appearance/skins.json +trans.ms = public/language/ms/admin/appearance/skins.json +trans.pl = public/language/pl/admin/appearance/skins.json +trans.sv = public/language/sv/admin/appearance/skins.json +trans.fi = public/language/fi/admin/appearance/skins.json +trans.da = public/language/da/admin/appearance/skins.json +trans.hu = public/language/hu/admin/appearance/skins.json +trans.hy = public/language/hy/admin/appearance/skins.json +trans.id = public/language/id/admin/appearance/skins.json +trans.lv = public/language/lv/admin/appearance/skins.json +trans.sq_AL = public/language/sq-AL/admin/appearance/skins.json +trans.cs = public/language/cs/admin/appearance/skins.json +trans.de = public/language/de/admin/appearance/skins.json +trans.ko = public/language/ko/admin/appearance/skins.json +trans.sl = public/language/sl/admin/appearance/skins.json +trans.zh_TW = public/language/zh-TW/admin/appearance/skins.json +trans.bg = public/language/bg/admin/appearance/skins.json +trans.en_US = public/language/en-US/admin/appearance/skins.json +trans.es = public/language/es/admin/appearance/skins.json +trans.sr = public/language/sr/admin/appearance/skins.json +trans.zh_CN = public/language/zh-CN/admin/appearance/skins.json +trans.en@pirate = public/language/en-x-pirate/admin/appearance/skins.json +trans.it = public/language/it/admin/appearance/skins.json +trans.ja = public/language/ja/admin/appearance/skins.json +trans.nb = public/language/nb/admin/appearance/skins.json +trans.fr = public/language/fr/admin/appearance/skins.json +trans.fa_IR = public/language/fa-IR/admin/appearance/skins.json +trans.gl = public/language/gl/admin/appearance/skins.json +trans.ro = public/language/ro/admin/appearance/skins.json +trans.ru = public/language/ru/admin/appearance/skins.json +trans.rw = public/language/rw/admin/appearance/skins.json +trans.el = public/language/el/admin/appearance/skins.json +trans.lt = public/language/lt/admin/appearance/skins.json +trans.nl = public/language/nl/admin/appearance/skins.json +trans.pt_BR = public/language/pt-BR/admin/appearance/skins.json +trans.pt_PT = public/language/pt-PT/admin/appearance/skins.json +trans.th = public/language/th/admin/appearance/skins.json +trans.tr = public/language/tr/admin/appearance/skins.json +trans.ar = public/language/ar/admin/appearance/skins.json + +[o:nodebb:p:nodebb:r:admin-appearance-themes] +file_filter = public/language//admin/appearance/themes.json +source_file = public/language/en-GB/admin/appearance/themes.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sq_AL = public/language/sq-AL/admin/appearance/themes.json +trans.ar = public/language/ar/admin/appearance/themes.json +trans.bg = public/language/bg/admin/appearance/themes.json +trans.bn = public/language/bn/admin/appearance/themes.json +trans.en_US = public/language/en-US/admin/appearance/themes.json +trans.fa_IR = public/language/fa-IR/admin/appearance/themes.json +trans.pt_BR = public/language/pt-BR/admin/appearance/themes.json +trans.ru = public/language/ru/admin/appearance/themes.json +trans.sv = public/language/sv/admin/appearance/themes.json +trans.cs = public/language/cs/admin/appearance/themes.json +trans.da = public/language/da/admin/appearance/themes.json +trans.sk = public/language/sk/admin/appearance/themes.json +trans.zh_CN = public/language/zh-CN/admin/appearance/themes.json +trans.et = public/language/et/admin/appearance/themes.json +trans.ja = public/language/ja/admin/appearance/themes.json +trans.sl = public/language/sl/admin/appearance/themes.json +trans.sr = public/language/sr/admin/appearance/themes.json +trans.hr = public/language/hr/admin/appearance/themes.json +trans.hu = public/language/hu/admin/appearance/themes.json +trans.ms = public/language/ms/admin/appearance/themes.json +trans.sc = public/language/sc/admin/appearance/themes.json +trans.th = public/language/th/admin/appearance/themes.json +trans.lt = public/language/lt/admin/appearance/themes.json +trans.el = public/language/el/admin/appearance/themes.json +trans.en@pirate = public/language/en-x-pirate/admin/appearance/themes.json +trans.fi = public/language/fi/admin/appearance/themes.json +trans.he = public/language/he/admin/appearance/themes.json +trans.hy = public/language/hy/admin/appearance/themes.json +trans.id = public/language/id/admin/appearance/themes.json +trans.ko = public/language/ko/admin/appearance/themes.json +trans.nb = public/language/nb/admin/appearance/themes.json +trans.pl = public/language/pl/admin/appearance/themes.json +trans.pt_PT = public/language/pt-PT/admin/appearance/themes.json +trans.tr = public/language/tr/admin/appearance/themes.json +trans.fr = public/language/fr/admin/appearance/themes.json +trans.nl = public/language/nl/admin/appearance/themes.json +trans.ro = public/language/ro/admin/appearance/themes.json +trans.uk = public/language/uk/admin/appearance/themes.json +trans.de = public/language/de/admin/appearance/themes.json +trans.es = public/language/es/admin/appearance/themes.json +trans.gl = public/language/gl/admin/appearance/themes.json +trans.it = public/language/it/admin/appearance/themes.json +trans.lv = public/language/lv/admin/appearance/themes.json +trans.vi = public/language/vi/admin/appearance/themes.json +trans.rw = public/language/rw/admin/appearance/themes.json +trans.zh_TW = public/language/zh-TW/admin/appearance/themes.json + +[o:nodebb:p:nodebb:r:admin-dashboard] +file_filter = public/language//admin/dashboard.json +source_file = public/language/en-GB/admin/dashboard.json +source_lang = en_GB +type = KEYVALUEJSON +trans.bn = public/language/bn/admin/dashboard.json +trans.en_US = public/language/en-US/admin/dashboard.json +trans.fa_IR = public/language/fa-IR/admin/dashboard.json +trans.fr = public/language/fr/admin/dashboard.json +trans.hu = public/language/hu/admin/dashboard.json +trans.nl = public/language/nl/admin/dashboard.json +trans.pl = public/language/pl/admin/dashboard.json +trans.bg = public/language/bg/admin/dashboard.json +trans.el = public/language/el/admin/dashboard.json +trans.gl = public/language/gl/admin/dashboard.json +trans.hy = public/language/hy/admin/dashboard.json +trans.id = public/language/id/admin/dashboard.json +trans.ja = public/language/ja/admin/dashboard.json +trans.pt_BR = public/language/pt-BR/admin/dashboard.json +trans.zh_CN = public/language/zh-CN/admin/dashboard.json +trans.zh_TW = public/language/zh-TW/admin/dashboard.json +trans.uk = public/language/uk/admin/dashboard.json +trans.ar = public/language/ar/admin/dashboard.json +trans.de = public/language/de/admin/dashboard.json +trans.en@pirate = public/language/en-x-pirate/admin/dashboard.json +trans.es = public/language/es/admin/dashboard.json +trans.ko = public/language/ko/admin/dashboard.json +trans.sv = public/language/sv/admin/dashboard.json +trans.lv = public/language/lv/admin/dashboard.json +trans.ms = public/language/ms/admin/dashboard.json +trans.ru = public/language/ru/admin/dashboard.json +trans.sl = public/language/sl/admin/dashboard.json +trans.sq_AL = public/language/sq-AL/admin/dashboard.json +trans.da = public/language/da/admin/dashboard.json +trans.lt = public/language/lt/admin/dashboard.json +trans.he = public/language/he/admin/dashboard.json +trans.sr = public/language/sr/admin/dashboard.json +trans.vi = public/language/vi/admin/dashboard.json +trans.pt_PT = public/language/pt-PT/admin/dashboard.json +trans.ro = public/language/ro/admin/dashboard.json +trans.rw = public/language/rw/admin/dashboard.json +trans.sc = public/language/sc/admin/dashboard.json +trans.th = public/language/th/admin/dashboard.json +trans.sk = public/language/sk/admin/dashboard.json +trans.tr = public/language/tr/admin/dashboard.json +trans.cs = public/language/cs/admin/dashboard.json +trans.et = public/language/et/admin/dashboard.json +trans.fi = public/language/fi/admin/dashboard.json +trans.hr = public/language/hr/admin/dashboard.json +trans.it = public/language/it/admin/dashboard.json +trans.nb = public/language/nb/admin/dashboard.json + +[o:nodebb:p:nodebb:r:admin-development-info] +file_filter = public/language//admin/development/info.json +source_file = public/language/en-GB/admin/development/info.json +source_lang = en_GB +type = KEYVALUEJSON +trans.de = public/language/de/admin/development/info.json +trans.et = public/language/et/admin/development/info.json +trans.hr = public/language/hr/admin/development/info.json +trans.ms = public/language/ms/admin/development/info.json +trans.pl = public/language/pl/admin/development/info.json +trans.pt_BR = public/language/pt-BR/admin/development/info.json +trans.ro = public/language/ro/admin/development/info.json +trans.rw = public/language/rw/admin/development/info.json +trans.sl = public/language/sl/admin/development/info.json +trans.sr = public/language/sr/admin/development/info.json +trans.uk = public/language/uk/admin/development/info.json +trans.da = public/language/da/admin/development/info.json +trans.en@pirate = public/language/en-x-pirate/admin/development/info.json +trans.es = public/language/es/admin/development/info.json +trans.fi = public/language/fi/admin/development/info.json +trans.it = public/language/it/admin/development/info.json +trans.lt = public/language/lt/admin/development/info.json +trans.th = public/language/th/admin/development/info.json +trans.ar = public/language/ar/admin/development/info.json +trans.bn = public/language/bn/admin/development/info.json +trans.fa_IR = public/language/fa-IR/admin/development/info.json +trans.hu = public/language/hu/admin/development/info.json +trans.id = public/language/id/admin/development/info.json +trans.lv = public/language/lv/admin/development/info.json +trans.gl = public/language/gl/admin/development/info.json +trans.hy = public/language/hy/admin/development/info.json +trans.zh_CN = public/language/zh-CN/admin/development/info.json +trans.cs = public/language/cs/admin/development/info.json +trans.ja = public/language/ja/admin/development/info.json +trans.nb = public/language/nb/admin/development/info.json +trans.sq_AL = public/language/sq-AL/admin/development/info.json +trans.sv = public/language/sv/admin/development/info.json +trans.bg = public/language/bg/admin/development/info.json +trans.he = public/language/he/admin/development/info.json +trans.sk = public/language/sk/admin/development/info.json +trans.el = public/language/el/admin/development/info.json +trans.fr = public/language/fr/admin/development/info.json +trans.ru = public/language/ru/admin/development/info.json +trans.sc = public/language/sc/admin/development/info.json +trans.tr = public/language/tr/admin/development/info.json +trans.en_US = public/language/en-US/admin/development/info.json +trans.ko = public/language/ko/admin/development/info.json +trans.nl = public/language/nl/admin/development/info.json +trans.pt_PT = public/language/pt-PT/admin/development/info.json +trans.vi = public/language/vi/admin/development/info.json +trans.zh_TW = public/language/zh-TW/admin/development/info.json + +[o:nodebb:p:nodebb:r:admin-development-logger] +file_filter = public/language//admin/development/logger.json +source_file = public/language/en-GB/admin/development/logger.json +source_lang = en_GB +type = KEYVALUEJSON +trans.de = public/language/de/admin/development/logger.json +trans.fr = public/language/fr/admin/development/logger.json +trans.gl = public/language/gl/admin/development/logger.json +trans.hr = public/language/hr/admin/development/logger.json +trans.sl = public/language/sl/admin/development/logger.json +trans.vi = public/language/vi/admin/development/logger.json +trans.zh_TW = public/language/zh-TW/admin/development/logger.json +trans.ar = public/language/ar/admin/development/logger.json +trans.he = public/language/he/admin/development/logger.json +trans.id = public/language/id/admin/development/logger.json +trans.ko = public/language/ko/admin/development/logger.json +trans.lt = public/language/lt/admin/development/logger.json +trans.nb = public/language/nb/admin/development/logger.json +trans.pt_BR = public/language/pt-BR/admin/development/logger.json +trans.ro = public/language/ro/admin/development/logger.json +trans.fi = public/language/fi/admin/development/logger.json +trans.sc = public/language/sc/admin/development/logger.json +trans.ru = public/language/ru/admin/development/logger.json +trans.sq_AL = public/language/sq-AL/admin/development/logger.json +trans.th = public/language/th/admin/development/logger.json +trans.hy = public/language/hy/admin/development/logger.json +trans.fa_IR = public/language/fa-IR/admin/development/logger.json +trans.it = public/language/it/admin/development/logger.json +trans.lv = public/language/lv/admin/development/logger.json +trans.nl = public/language/nl/admin/development/logger.json +trans.pt_PT = public/language/pt-PT/admin/development/logger.json +trans.sk = public/language/sk/admin/development/logger.json +trans.sv = public/language/sv/admin/development/logger.json +trans.el = public/language/el/admin/development/logger.json +trans.uk = public/language/uk/admin/development/logger.json +trans.ms = public/language/ms/admin/development/logger.json +trans.zh_CN = public/language/zh-CN/admin/development/logger.json +trans.bn = public/language/bn/admin/development/logger.json +trans.et = public/language/et/admin/development/logger.json +trans.ja = public/language/ja/admin/development/logger.json +trans.da = public/language/da/admin/development/logger.json +trans.en@pirate = public/language/en-x-pirate/admin/development/logger.json +trans.es = public/language/es/admin/development/logger.json +trans.cs = public/language/cs/admin/development/logger.json +trans.en_US = public/language/en-US/admin/development/logger.json +trans.hu = public/language/hu/admin/development/logger.json +trans.pl = public/language/pl/admin/development/logger.json +trans.rw = public/language/rw/admin/development/logger.json +trans.sr = public/language/sr/admin/development/logger.json +trans.tr = public/language/tr/admin/development/logger.json +trans.bg = public/language/bg/admin/development/logger.json + +[o:nodebb:p:nodebb:r:admin-extend-plugins] +file_filter = public/language//admin/extend/plugins.json +source_file = public/language/en-GB/admin/extend/plugins.json +source_lang = en_GB +type = KEYVALUEJSON +trans.en@pirate = public/language/en-x-pirate/admin/extend/plugins.json +trans.et = public/language/et/admin/extend/plugins.json +trans.hr = public/language/hr/admin/extend/plugins.json +trans.hy = public/language/hy/admin/extend/plugins.json +trans.tr = public/language/tr/admin/extend/plugins.json +trans.it = public/language/it/admin/extend/plugins.json +trans.lv = public/language/lv/admin/extend/plugins.json +trans.sl = public/language/sl/admin/extend/plugins.json +trans.th = public/language/th/admin/extend/plugins.json +trans.en_US = public/language/en-US/admin/extend/plugins.json +trans.he = public/language/he/admin/extend/plugins.json +trans.nb = public/language/nb/admin/extend/plugins.json +trans.sk = public/language/sk/admin/extend/plugins.json +trans.uk = public/language/uk/admin/extend/plugins.json +trans.es = public/language/es/admin/extend/plugins.json +trans.lt = public/language/lt/admin/extend/plugins.json +trans.sv = public/language/sv/admin/extend/plugins.json +trans.nl = public/language/nl/admin/extend/plugins.json +trans.pt_PT = public/language/pt-PT/admin/extend/plugins.json +trans.ar = public/language/ar/admin/extend/plugins.json +trans.cs = public/language/cs/admin/extend/plugins.json +trans.fi = public/language/fi/admin/extend/plugins.json +trans.id = public/language/id/admin/extend/plugins.json +trans.ja = public/language/ja/admin/extend/plugins.json +trans.ko = public/language/ko/admin/extend/plugins.json +trans.ru = public/language/ru/admin/extend/plugins.json +trans.sr = public/language/sr/admin/extend/plugins.json +trans.bn = public/language/bn/admin/extend/plugins.json +trans.de = public/language/de/admin/extend/plugins.json +trans.el = public/language/el/admin/extend/plugins.json +trans.ms = public/language/ms/admin/extend/plugins.json +trans.pt_BR = public/language/pt-BR/admin/extend/plugins.json +trans.sq_AL = public/language/sq-AL/admin/extend/plugins.json +trans.zh_TW = public/language/zh-TW/admin/extend/plugins.json +trans.da = public/language/da/admin/extend/plugins.json +trans.fa_IR = public/language/fa-IR/admin/extend/plugins.json +trans.gl = public/language/gl/admin/extend/plugins.json +trans.hu = public/language/hu/admin/extend/plugins.json +trans.sc = public/language/sc/admin/extend/plugins.json +trans.vi = public/language/vi/admin/extend/plugins.json +trans.bg = public/language/bg/admin/extend/plugins.json +trans.fr = public/language/fr/admin/extend/plugins.json +trans.pl = public/language/pl/admin/extend/plugins.json +trans.ro = public/language/ro/admin/extend/plugins.json +trans.rw = public/language/rw/admin/extend/plugins.json +trans.zh_CN = public/language/zh-CN/admin/extend/plugins.json + +[o:nodebb:p:nodebb:r:admin-extend-rewards] +file_filter = public/language//admin/extend/rewards.json +source_file = public/language/en-GB/admin/extend/rewards.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ms = public/language/ms/admin/extend/rewards.json +trans.ar = public/language/ar/admin/extend/rewards.json +trans.fi = public/language/fi/admin/extend/rewards.json +trans.fr = public/language/fr/admin/extend/rewards.json +trans.sr = public/language/sr/admin/extend/rewards.json +trans.id = public/language/id/admin/extend/rewards.json +trans.pt_BR = public/language/pt-BR/admin/extend/rewards.json +trans.sq_AL = public/language/sq-AL/admin/extend/rewards.json +trans.pt_PT = public/language/pt-PT/admin/extend/rewards.json +trans.ro = public/language/ro/admin/extend/rewards.json +trans.ru = public/language/ru/admin/extend/rewards.json +trans.sk = public/language/sk/admin/extend/rewards.json +trans.es = public/language/es/admin/extend/rewards.json +trans.lt = public/language/lt/admin/extend/rewards.json +trans.nl = public/language/nl/admin/extend/rewards.json +trans.en@pirate = public/language/en-x-pirate/admin/extend/rewards.json +trans.hr = public/language/hr/admin/extend/rewards.json +trans.ja = public/language/ja/admin/extend/rewards.json +trans.ko = public/language/ko/admin/extend/rewards.json +trans.nb = public/language/nb/admin/extend/rewards.json +trans.bg = public/language/bg/admin/extend/rewards.json +trans.bn = public/language/bn/admin/extend/rewards.json +trans.da = public/language/da/admin/extend/rewards.json +trans.hy = public/language/hy/admin/extend/rewards.json +trans.pl = public/language/pl/admin/extend/rewards.json +trans.sc = public/language/sc/admin/extend/rewards.json +trans.it = public/language/it/admin/extend/rewards.json +trans.tr = public/language/tr/admin/extend/rewards.json +trans.uk = public/language/uk/admin/extend/rewards.json +trans.vi = public/language/vi/admin/extend/rewards.json +trans.zh_CN = public/language/zh-CN/admin/extend/rewards.json +trans.en_US = public/language/en-US/admin/extend/rewards.json +trans.fa_IR = public/language/fa-IR/admin/extend/rewards.json +trans.he = public/language/he/admin/extend/rewards.json +trans.zh_TW = public/language/zh-TW/admin/extend/rewards.json +trans.lv = public/language/lv/admin/extend/rewards.json +trans.sl = public/language/sl/admin/extend/rewards.json +trans.cs = public/language/cs/admin/extend/rewards.json +trans.el = public/language/el/admin/extend/rewards.json +trans.et = public/language/et/admin/extend/rewards.json +trans.rw = public/language/rw/admin/extend/rewards.json +trans.sv = public/language/sv/admin/extend/rewards.json +trans.th = public/language/th/admin/extend/rewards.json +trans.de = public/language/de/admin/extend/rewards.json +trans.gl = public/language/gl/admin/extend/rewards.json +trans.hu = public/language/hu/admin/extend/rewards.json + +[o:nodebb:p:nodebb:r:admin-extend-widgets] +file_filter = public/language//admin/extend/widgets.json +source_file = public/language/en-GB/admin/extend/widgets.json +source_lang = en_GB +type = KEYVALUEJSON +trans.bn = public/language/bn/admin/extend/widgets.json +trans.el = public/language/el/admin/extend/widgets.json +trans.id = public/language/id/admin/extend/widgets.json +trans.nb = public/language/nb/admin/extend/widgets.json +trans.ro = public/language/ro/admin/extend/widgets.json +trans.rw = public/language/rw/admin/extend/widgets.json +trans.ar = public/language/ar/admin/extend/widgets.json +trans.hy = public/language/hy/admin/extend/widgets.json +trans.lv = public/language/lv/admin/extend/widgets.json +trans.ru = public/language/ru/admin/extend/widgets.json +trans.sc = public/language/sc/admin/extend/widgets.json +trans.sr = public/language/sr/admin/extend/widgets.json +trans.tr = public/language/tr/admin/extend/widgets.json +trans.vi = public/language/vi/admin/extend/widgets.json +trans.fr = public/language/fr/admin/extend/widgets.json +trans.fa_IR = public/language/fa-IR/admin/extend/widgets.json +trans.ko = public/language/ko/admin/extend/widgets.json +trans.lt = public/language/lt/admin/extend/widgets.json +trans.sq_AL = public/language/sq-AL/admin/extend/widgets.json +trans.uk = public/language/uk/admin/extend/widgets.json +trans.bg = public/language/bg/admin/extend/widgets.json +trans.de = public/language/de/admin/extend/widgets.json +trans.ms = public/language/ms/admin/extend/widgets.json +trans.nl = public/language/nl/admin/extend/widgets.json +trans.pt_BR = public/language/pt-BR/admin/extend/widgets.json +trans.sk = public/language/sk/admin/extend/widgets.json +trans.zh_CN = public/language/zh-CN/admin/extend/widgets.json +trans.da = public/language/da/admin/extend/widgets.json +trans.hu = public/language/hu/admin/extend/widgets.json +trans.ja = public/language/ja/admin/extend/widgets.json +trans.th = public/language/th/admin/extend/widgets.json +trans.et = public/language/et/admin/extend/widgets.json +trans.gl = public/language/gl/admin/extend/widgets.json +trans.he = public/language/he/admin/extend/widgets.json +trans.it = public/language/it/admin/extend/widgets.json +trans.pl = public/language/pl/admin/extend/widgets.json +trans.zh_TW = public/language/zh-TW/admin/extend/widgets.json +trans.en@pirate = public/language/en-x-pirate/admin/extend/widgets.json +trans.fi = public/language/fi/admin/extend/widgets.json +trans.hr = public/language/hr/admin/extend/widgets.json +trans.sl = public/language/sl/admin/extend/widgets.json +trans.sv = public/language/sv/admin/extend/widgets.json +trans.cs = public/language/cs/admin/extend/widgets.json +trans.es = public/language/es/admin/extend/widgets.json +trans.pt_PT = public/language/pt-PT/admin/extend/widgets.json +trans.en_US = public/language/en-US/admin/extend/widgets.json + +[o:nodebb:p:nodebb:r:admin-manage-admins-mods] +file_filter = public/language//admin/manage/admins-mods.json +source_file = public/language/en-GB/admin/manage/admins-mods.json +source_lang = en_GB +type = KEYVALUEJSON +trans.hu = public/language/hu/admin/manage/admins-mods.json +trans.nb = public/language/nb/admin/manage/admins-mods.json +trans.ru = public/language/ru/admin/manage/admins-mods.json +trans.fr = public/language/fr/admin/manage/admins-mods.json +trans.he = public/language/he/admin/manage/admins-mods.json +trans.lv = public/language/lv/admin/manage/admins-mods.json +trans.sc = public/language/sc/admin/manage/admins-mods.json +trans.vi = public/language/vi/admin/manage/admins-mods.json +trans.es = public/language/es/admin/manage/admins-mods.json +trans.lt = public/language/lt/admin/manage/admins-mods.json +trans.de = public/language/de/admin/manage/admins-mods.json +trans.ja = public/language/ja/admin/manage/admins-mods.json +trans.pl = public/language/pl/admin/manage/admins-mods.json +trans.sq_AL = public/language/sq-AL/admin/manage/admins-mods.json +trans.sr = public/language/sr/admin/manage/admins-mods.json +trans.zh_TW = public/language/zh-TW/admin/manage/admins-mods.json +trans.id = public/language/id/admin/manage/admins-mods.json +trans.ko = public/language/ko/admin/manage/admins-mods.json +trans.gl = public/language/gl/admin/manage/admins-mods.json +trans.hr = public/language/hr/admin/manage/admins-mods.json +trans.hy = public/language/hy/admin/manage/admins-mods.json +trans.rw = public/language/rw/admin/manage/admins-mods.json +trans.bn = public/language/bn/admin/manage/admins-mods.json +trans.fi = public/language/fi/admin/manage/admins-mods.json +trans.ms = public/language/ms/admin/manage/admins-mods.json +trans.pt_BR = public/language/pt-BR/admin/manage/admins-mods.json +trans.pt_PT = public/language/pt-PT/admin/manage/admins-mods.json +trans.sv = public/language/sv/admin/manage/admins-mods.json +trans.th = public/language/th/admin/manage/admins-mods.json +trans.uk = public/language/uk/admin/manage/admins-mods.json +trans.ar = public/language/ar/admin/manage/admins-mods.json +trans.bg = public/language/bg/admin/manage/admins-mods.json +trans.el = public/language/el/admin/manage/admins-mods.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/admins-mods.json +trans.en_US = public/language/en-US/admin/manage/admins-mods.json +trans.it = public/language/it/admin/manage/admins-mods.json +trans.nl = public/language/nl/admin/manage/admins-mods.json +trans.ro = public/language/ro/admin/manage/admins-mods.json +trans.cs = public/language/cs/admin/manage/admins-mods.json +trans.da = public/language/da/admin/manage/admins-mods.json +trans.zh_CN = public/language/zh-CN/admin/manage/admins-mods.json +trans.sk = public/language/sk/admin/manage/admins-mods.json +trans.sl = public/language/sl/admin/manage/admins-mods.json +trans.tr = public/language/tr/admin/manage/admins-mods.json +trans.et = public/language/et/admin/manage/admins-mods.json +trans.fa_IR = public/language/fa-IR/admin/manage/admins-mods.json + +[o:nodebb:p:nodebb:r:admin-manage-categories] +file_filter = public/language//admin/manage/categories.json +source_file = public/language/en-GB/admin/manage/categories.json +source_lang = en_GB +type = KEYVALUEJSON +trans.es = public/language/es/admin/manage/categories.json +trans.gl = public/language/gl/admin/manage/categories.json +trans.pt_PT = public/language/pt-PT/admin/manage/categories.json +trans.sc = public/language/sc/admin/manage/categories.json +trans.zh_TW = public/language/zh-TW/admin/manage/categories.json +trans.bg = public/language/bg/admin/manage/categories.json +trans.hu = public/language/hu/admin/manage/categories.json +trans.hy = public/language/hy/admin/manage/categories.json +trans.sk = public/language/sk/admin/manage/categories.json +trans.uk = public/language/uk/admin/manage/categories.json +trans.vi = public/language/vi/admin/manage/categories.json +trans.bn = public/language/bn/admin/manage/categories.json +trans.en_US = public/language/en-US/admin/manage/categories.json +trans.fr = public/language/fr/admin/manage/categories.json +trans.it = public/language/it/admin/manage/categories.json +trans.nb = public/language/nb/admin/manage/categories.json +trans.ru = public/language/ru/admin/manage/categories.json +trans.sr = public/language/sr/admin/manage/categories.json +trans.fa_IR = public/language/fa-IR/admin/manage/categories.json +trans.id = public/language/id/admin/manage/categories.json +trans.ms = public/language/ms/admin/manage/categories.json +trans.pl = public/language/pl/admin/manage/categories.json +trans.tr = public/language/tr/admin/manage/categories.json +trans.zh_CN = public/language/zh-CN/admin/manage/categories.json +trans.lt = public/language/lt/admin/manage/categories.json +trans.sl = public/language/sl/admin/manage/categories.json +trans.sv = public/language/sv/admin/manage/categories.json +trans.rw = public/language/rw/admin/manage/categories.json +trans.el = public/language/el/admin/manage/categories.json +trans.et = public/language/et/admin/manage/categories.json +trans.fi = public/language/fi/admin/manage/categories.json +trans.he = public/language/he/admin/manage/categories.json +trans.ja = public/language/ja/admin/manage/categories.json +trans.ko = public/language/ko/admin/manage/categories.json +trans.ro = public/language/ro/admin/manage/categories.json +trans.cs = public/language/cs/admin/manage/categories.json +trans.sq_AL = public/language/sq-AL/admin/manage/categories.json +trans.th = public/language/th/admin/manage/categories.json +trans.pt_BR = public/language/pt-BR/admin/manage/categories.json +trans.ar = public/language/ar/admin/manage/categories.json +trans.da = public/language/da/admin/manage/categories.json +trans.de = public/language/de/admin/manage/categories.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/categories.json +trans.hr = public/language/hr/admin/manage/categories.json +trans.lv = public/language/lv/admin/manage/categories.json +trans.nl = public/language/nl/admin/manage/categories.json + +[o:nodebb:p:nodebb:r:admin-manage-digest] +file_filter = public/language//admin/manage/digest.json +source_file = public/language/en-GB/admin/manage/digest.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sr = public/language/sr/admin/manage/digest.json +trans.fi = public/language/fi/admin/manage/digest.json +trans.gl = public/language/gl/admin/manage/digest.json +trans.fr = public/language/fr/admin/manage/digest.json +trans.hu = public/language/hu/admin/manage/digest.json +trans.id = public/language/id/admin/manage/digest.json +trans.lv = public/language/lv/admin/manage/digest.json +trans.pl = public/language/pl/admin/manage/digest.json +trans.sl = public/language/sl/admin/manage/digest.json +trans.da = public/language/da/admin/manage/digest.json +trans.fa_IR = public/language/fa-IR/admin/manage/digest.json +trans.sq_AL = public/language/sq-AL/admin/manage/digest.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/digest.json +trans.en_US = public/language/en-US/admin/manage/digest.json +trans.he = public/language/he/admin/manage/digest.json +trans.hr = public/language/hr/admin/manage/digest.json +trans.hy = public/language/hy/admin/manage/digest.json +trans.it = public/language/it/admin/manage/digest.json +trans.bg = public/language/bg/admin/manage/digest.json +trans.bn = public/language/bn/admin/manage/digest.json +trans.ro = public/language/ro/admin/manage/digest.json +trans.th = public/language/th/admin/manage/digest.json +trans.uk = public/language/uk/admin/manage/digest.json +trans.vi = public/language/vi/admin/manage/digest.json +trans.ms = public/language/ms/admin/manage/digest.json +trans.pt_BR = public/language/pt-BR/admin/manage/digest.json +trans.rw = public/language/rw/admin/manage/digest.json +trans.de = public/language/de/admin/manage/digest.json +trans.el = public/language/el/admin/manage/digest.json +trans.nl = public/language/nl/admin/manage/digest.json +trans.ru = public/language/ru/admin/manage/digest.json +trans.sv = public/language/sv/admin/manage/digest.json +trans.zh_TW = public/language/zh-TW/admin/manage/digest.json +trans.es = public/language/es/admin/manage/digest.json +trans.lt = public/language/lt/admin/manage/digest.json +trans.ko = public/language/ko/admin/manage/digest.json +trans.zh_CN = public/language/zh-CN/admin/manage/digest.json +trans.ar = public/language/ar/admin/manage/digest.json +trans.ja = public/language/ja/admin/manage/digest.json +trans.nb = public/language/nb/admin/manage/digest.json +trans.sk = public/language/sk/admin/manage/digest.json +trans.pt_PT = public/language/pt-PT/admin/manage/digest.json +trans.sc = public/language/sc/admin/manage/digest.json +trans.tr = public/language/tr/admin/manage/digest.json +trans.cs = public/language/cs/admin/manage/digest.json +trans.et = public/language/et/admin/manage/digest.json + +[o:nodebb:p:nodebb:r:admin-manage-groups] +file_filter = public/language//admin/manage/groups.json +source_file = public/language/en-GB/admin/manage/groups.json +source_lang = en_GB +type = KEYVALUEJSON +trans.pt_PT = public/language/pt-PT/admin/manage/groups.json +trans.tr = public/language/tr/admin/manage/groups.json +trans.uk = public/language/uk/admin/manage/groups.json +trans.zh_TW = public/language/zh-TW/admin/manage/groups.json +trans.nb = public/language/nb/admin/manage/groups.json +trans.id = public/language/id/admin/manage/groups.json +trans.lt = public/language/lt/admin/manage/groups.json +trans.pl = public/language/pl/admin/manage/groups.json +trans.sc = public/language/sc/admin/manage/groups.json +trans.hy = public/language/hy/admin/manage/groups.json +trans.he = public/language/he/admin/manage/groups.json +trans.ko = public/language/ko/admin/manage/groups.json +trans.sv = public/language/sv/admin/manage/groups.json +trans.bn = public/language/bn/admin/manage/groups.json +trans.es = public/language/es/admin/manage/groups.json +trans.ja = public/language/ja/admin/manage/groups.json +trans.th = public/language/th/admin/manage/groups.json +trans.cs = public/language/cs/admin/manage/groups.json +trans.fi = public/language/fi/admin/manage/groups.json +trans.hr = public/language/hr/admin/manage/groups.json +trans.it = public/language/it/admin/manage/groups.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/groups.json +trans.bg = public/language/bg/admin/manage/groups.json +trans.hu = public/language/hu/admin/manage/groups.json +trans.pt_BR = public/language/pt-BR/admin/manage/groups.json +trans.ro = public/language/ro/admin/manage/groups.json +trans.ru = public/language/ru/admin/manage/groups.json +trans.sq_AL = public/language/sq-AL/admin/manage/groups.json +trans.vi = public/language/vi/admin/manage/groups.json +trans.ar = public/language/ar/admin/manage/groups.json +trans.et = public/language/et/admin/manage/groups.json +trans.fa_IR = public/language/fa-IR/admin/manage/groups.json +trans.fr = public/language/fr/admin/manage/groups.json +trans.nl = public/language/nl/admin/manage/groups.json +trans.rw = public/language/rw/admin/manage/groups.json +trans.sl = public/language/sl/admin/manage/groups.json +trans.zh_CN = public/language/zh-CN/admin/manage/groups.json +trans.da = public/language/da/admin/manage/groups.json +trans.el = public/language/el/admin/manage/groups.json +trans.en_US = public/language/en-US/admin/manage/groups.json +trans.gl = public/language/gl/admin/manage/groups.json +trans.lv = public/language/lv/admin/manage/groups.json +trans.ms = public/language/ms/admin/manage/groups.json +trans.sk = public/language/sk/admin/manage/groups.json +trans.sr = public/language/sr/admin/manage/groups.json +trans.de = public/language/de/admin/manage/groups.json + +[o:nodebb:p:nodebb:r:admin-manage-privileges] +file_filter = public/language//admin/manage/privileges.json +source_file = public/language/en-GB/admin/manage/privileges.json +source_lang = en_GB +type = KEYVALUEJSON +trans.bg = public/language/bg/admin/manage/privileges.json +trans.es = public/language/es/admin/manage/privileges.json +trans.fa_IR = public/language/fa-IR/admin/manage/privileges.json +trans.ms = public/language/ms/admin/manage/privileges.json +trans.ro = public/language/ro/admin/manage/privileges.json +trans.sl = public/language/sl/admin/manage/privileges.json +trans.el = public/language/el/admin/manage/privileges.json +trans.lt = public/language/lt/admin/manage/privileges.json +trans.pt_BR = public/language/pt-BR/admin/manage/privileges.json +trans.pt_PT = public/language/pt-PT/admin/manage/privileges.json +trans.hu = public/language/hu/admin/manage/privileges.json +trans.pl = public/language/pl/admin/manage/privileges.json +trans.sc = public/language/sc/admin/manage/privileges.json +trans.uk = public/language/uk/admin/manage/privileges.json +trans.bn = public/language/bn/admin/manage/privileges.json +trans.cs = public/language/cs/admin/manage/privileges.json +trans.da = public/language/da/admin/manage/privileges.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/privileges.json +trans.vi = public/language/vi/admin/manage/privileges.json +trans.en_US = public/language/en-US/admin/manage/privileges.json +trans.hy = public/language/hy/admin/manage/privileges.json +trans.sr = public/language/sr/admin/manage/privileges.json +trans.ja = public/language/ja/admin/manage/privileges.json +trans.nb = public/language/nb/admin/manage/privileges.json +trans.nl = public/language/nl/admin/manage/privileges.json +trans.sv = public/language/sv/admin/manage/privileges.json +trans.et = public/language/et/admin/manage/privileges.json +trans.fr = public/language/fr/admin/manage/privileges.json +trans.he = public/language/he/admin/manage/privileges.json +trans.hr = public/language/hr/admin/manage/privileges.json +trans.th = public/language/th/admin/manage/privileges.json +trans.sq_AL = public/language/sq-AL/admin/manage/privileges.json +trans.gl = public/language/gl/admin/manage/privileges.json +trans.ko = public/language/ko/admin/manage/privileges.json +trans.lv = public/language/lv/admin/manage/privileges.json +trans.sk = public/language/sk/admin/manage/privileges.json +trans.tr = public/language/tr/admin/manage/privileges.json +trans.fi = public/language/fi/admin/manage/privileges.json +trans.id = public/language/id/admin/manage/privileges.json +trans.it = public/language/it/admin/manage/privileges.json +trans.rw = public/language/rw/admin/manage/privileges.json +trans.zh_TW = public/language/zh-TW/admin/manage/privileges.json +trans.ar = public/language/ar/admin/manage/privileges.json +trans.de = public/language/de/admin/manage/privileges.json +trans.ru = public/language/ru/admin/manage/privileges.json +trans.zh_CN = public/language/zh-CN/admin/manage/privileges.json + +[o:nodebb:p:nodebb:r:admin-manage-registration] +file_filter = public/language//admin/manage/registration.json +source_file = public/language/en-GB/admin/manage/registration.json +source_lang = en_GB +type = KEYVALUEJSON +trans.pl = public/language/pl/admin/manage/registration.json +trans.ro = public/language/ro/admin/manage/registration.json +trans.sk = public/language/sk/admin/manage/registration.json +trans.fi = public/language/fi/admin/manage/registration.json +trans.hu = public/language/hu/admin/manage/registration.json +trans.ms = public/language/ms/admin/manage/registration.json +trans.ru = public/language/ru/admin/manage/registration.json +trans.sc = public/language/sc/admin/manage/registration.json +trans.sr = public/language/sr/admin/manage/registration.json +trans.th = public/language/th/admin/manage/registration.json +trans.zh_TW = public/language/zh-TW/admin/manage/registration.json +trans.da = public/language/da/admin/manage/registration.json +trans.et = public/language/et/admin/manage/registration.json +trans.it = public/language/it/admin/manage/registration.json +trans.nb = public/language/nb/admin/manage/registration.json +trans.pt_BR = public/language/pt-BR/admin/manage/registration.json +trans.hy = public/language/hy/admin/manage/registration.json +trans.uk = public/language/uk/admin/manage/registration.json +trans.sq_AL = public/language/sq-AL/admin/manage/registration.json +trans.ar = public/language/ar/admin/manage/registration.json +trans.es = public/language/es/admin/manage/registration.json +trans.he = public/language/he/admin/manage/registration.json +trans.id = public/language/id/admin/manage/registration.json +trans.nl = public/language/nl/admin/manage/registration.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/registration.json +trans.en_US = public/language/en-US/admin/manage/registration.json +trans.rw = public/language/rw/admin/manage/registration.json +trans.sv = public/language/sv/admin/manage/registration.json +trans.tr = public/language/tr/admin/manage/registration.json +trans.vi = public/language/vi/admin/manage/registration.json +trans.bg = public/language/bg/admin/manage/registration.json +trans.cs = public/language/cs/admin/manage/registration.json +trans.fa_IR = public/language/fa-IR/admin/manage/registration.json +trans.fr = public/language/fr/admin/manage/registration.json +trans.lt = public/language/lt/admin/manage/registration.json +trans.ja = public/language/ja/admin/manage/registration.json +trans.ko = public/language/ko/admin/manage/registration.json +trans.lv = public/language/lv/admin/manage/registration.json +trans.bn = public/language/bn/admin/manage/registration.json +trans.de = public/language/de/admin/manage/registration.json +trans.el = public/language/el/admin/manage/registration.json +trans.gl = public/language/gl/admin/manage/registration.json +trans.hr = public/language/hr/admin/manage/registration.json +trans.pt_PT = public/language/pt-PT/admin/manage/registration.json +trans.sl = public/language/sl/admin/manage/registration.json +trans.zh_CN = public/language/zh-CN/admin/manage/registration.json + +[o:nodebb:p:nodebb:r:admin-manage-tags] +file_filter = public/language//admin/manage/tags.json +source_file = public/language/en-GB/admin/manage/tags.json +source_lang = en_GB +type = KEYVALUEJSON +trans.zh_TW = public/language/zh-TW/admin/manage/tags.json +trans.bg = public/language/bg/admin/manage/tags.json +trans.da = public/language/da/admin/manage/tags.json +trans.en_US = public/language/en-US/admin/manage/tags.json +trans.hu = public/language/hu/admin/manage/tags.json +trans.nb = public/language/nb/admin/manage/tags.json +trans.ru = public/language/ru/admin/manage/tags.json +trans.sq_AL = public/language/sq-AL/admin/manage/tags.json +trans.de = public/language/de/admin/manage/tags.json +trans.fi = public/language/fi/admin/manage/tags.json +trans.hr = public/language/hr/admin/manage/tags.json +trans.ko = public/language/ko/admin/manage/tags.json +trans.ms = public/language/ms/admin/manage/tags.json +trans.th = public/language/th/admin/manage/tags.json +trans.ar = public/language/ar/admin/manage/tags.json +trans.el = public/language/el/admin/manage/tags.json +trans.es = public/language/es/admin/manage/tags.json +trans.hy = public/language/hy/admin/manage/tags.json +trans.ja = public/language/ja/admin/manage/tags.json +trans.cs = public/language/cs/admin/manage/tags.json +trans.pl = public/language/pl/admin/manage/tags.json +trans.ro = public/language/ro/admin/manage/tags.json +trans.rw = public/language/rw/admin/manage/tags.json +trans.bn = public/language/bn/admin/manage/tags.json +trans.fa_IR = public/language/fa-IR/admin/manage/tags.json +trans.zh_CN = public/language/zh-CN/admin/manage/tags.json +trans.et = public/language/et/admin/manage/tags.json +trans.fr = public/language/fr/admin/manage/tags.json +trans.gl = public/language/gl/admin/manage/tags.json +trans.id = public/language/id/admin/manage/tags.json +trans.lt = public/language/lt/admin/manage/tags.json +trans.sv = public/language/sv/admin/manage/tags.json +trans.tr = public/language/tr/admin/manage/tags.json +trans.uk = public/language/uk/admin/manage/tags.json +trans.lv = public/language/lv/admin/manage/tags.json +trans.nl = public/language/nl/admin/manage/tags.json +trans.pt_BR = public/language/pt-BR/admin/manage/tags.json +trans.pt_PT = public/language/pt-PT/admin/manage/tags.json +trans.sc = public/language/sc/admin/manage/tags.json +trans.sk = public/language/sk/admin/manage/tags.json +trans.sl = public/language/sl/admin/manage/tags.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/tags.json +trans.he = public/language/he/admin/manage/tags.json +trans.it = public/language/it/admin/manage/tags.json +trans.sr = public/language/sr/admin/manage/tags.json +trans.vi = public/language/vi/admin/manage/tags.json + +[o:nodebb:p:nodebb:r:admin-manage-uploads] +file_filter = public/language//admin/manage/uploads.json +source_file = public/language/en-GB/admin/manage/uploads.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ms = public/language/ms/admin/manage/uploads.json +trans.nb = public/language/nb/admin/manage/uploads.json +trans.tr = public/language/tr/admin/manage/uploads.json +trans.bg = public/language/bg/admin/manage/uploads.json +trans.bn = public/language/bn/admin/manage/uploads.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/uploads.json +trans.lt = public/language/lt/admin/manage/uploads.json +trans.sq_AL = public/language/sq-AL/admin/manage/uploads.json +trans.el = public/language/el/admin/manage/uploads.json +trans.en_US = public/language/en-US/admin/manage/uploads.json +trans.fi = public/language/fi/admin/manage/uploads.json +trans.hr = public/language/hr/admin/manage/uploads.json +trans.sv = public/language/sv/admin/manage/uploads.json +trans.ar = public/language/ar/admin/manage/uploads.json +trans.de = public/language/de/admin/manage/uploads.json +trans.fr = public/language/fr/admin/manage/uploads.json +trans.he = public/language/he/admin/manage/uploads.json +trans.ro = public/language/ro/admin/manage/uploads.json +trans.rw = public/language/rw/admin/manage/uploads.json +trans.gl = public/language/gl/admin/manage/uploads.json +trans.nl = public/language/nl/admin/manage/uploads.json +trans.pl = public/language/pl/admin/manage/uploads.json +trans.pt_BR = public/language/pt-BR/admin/manage/uploads.json +trans.sc = public/language/sc/admin/manage/uploads.json +trans.th = public/language/th/admin/manage/uploads.json +trans.es = public/language/es/admin/manage/uploads.json +trans.et = public/language/et/admin/manage/uploads.json +trans.lv = public/language/lv/admin/manage/uploads.json +trans.pt_PT = public/language/pt-PT/admin/manage/uploads.json +trans.sr = public/language/sr/admin/manage/uploads.json +trans.uk = public/language/uk/admin/manage/uploads.json +trans.vi = public/language/vi/admin/manage/uploads.json +trans.cs = public/language/cs/admin/manage/uploads.json +trans.ru = public/language/ru/admin/manage/uploads.json +trans.sk = public/language/sk/admin/manage/uploads.json +trans.sl = public/language/sl/admin/manage/uploads.json +trans.fa_IR = public/language/fa-IR/admin/manage/uploads.json +trans.hu = public/language/hu/admin/manage/uploads.json +trans.hy = public/language/hy/admin/manage/uploads.json +trans.zh_CN = public/language/zh-CN/admin/manage/uploads.json +trans.ko = public/language/ko/admin/manage/uploads.json +trans.zh_TW = public/language/zh-TW/admin/manage/uploads.json +trans.da = public/language/da/admin/manage/uploads.json +trans.id = public/language/id/admin/manage/uploads.json +trans.it = public/language/it/admin/manage/uploads.json +trans.ja = public/language/ja/admin/manage/uploads.json + +[o:nodebb:p:nodebb:r:admin-manage-users] +file_filter = public/language//admin/manage/users.json +source_file = public/language/en-GB/admin/manage/users.json +source_lang = en_GB +type = KEYVALUEJSON +trans.uk = public/language/uk/admin/manage/users.json +trans.en@pirate = public/language/en-x-pirate/admin/manage/users.json +trans.en_US = public/language/en-US/admin/manage/users.json +trans.fr = public/language/fr/admin/manage/users.json +trans.ko = public/language/ko/admin/manage/users.json +trans.ms = public/language/ms/admin/manage/users.json +trans.nb = public/language/nb/admin/manage/users.json +trans.pt_BR = public/language/pt-BR/admin/manage/users.json +trans.zh_CN = public/language/zh-CN/admin/manage/users.json +trans.zh_TW = public/language/zh-TW/admin/manage/users.json +trans.cs = public/language/cs/admin/manage/users.json +trans.gl = public/language/gl/admin/manage/users.json +trans.rw = public/language/rw/admin/manage/users.json +trans.bg = public/language/bg/admin/manage/users.json +trans.de = public/language/de/admin/manage/users.json +trans.et = public/language/et/admin/manage/users.json +trans.id = public/language/id/admin/manage/users.json +trans.pt_PT = public/language/pt-PT/admin/manage/users.json +trans.ru = public/language/ru/admin/manage/users.json +trans.da = public/language/da/admin/manage/users.json +trans.he = public/language/he/admin/manage/users.json +trans.hu = public/language/hu/admin/manage/users.json +trans.ro = public/language/ro/admin/manage/users.json +trans.bn = public/language/bn/admin/manage/users.json +trans.es = public/language/es/admin/manage/users.json +trans.it = public/language/it/admin/manage/users.json +trans.nl = public/language/nl/admin/manage/users.json +trans.sq_AL = public/language/sq-AL/admin/manage/users.json +trans.tr = public/language/tr/admin/manage/users.json +trans.vi = public/language/vi/admin/manage/users.json +trans.fi = public/language/fi/admin/manage/users.json +trans.sc = public/language/sc/admin/manage/users.json +trans.hy = public/language/hy/admin/manage/users.json +trans.ja = public/language/ja/admin/manage/users.json +trans.lt = public/language/lt/admin/manage/users.json +trans.th = public/language/th/admin/manage/users.json +trans.sl = public/language/sl/admin/manage/users.json +trans.ar = public/language/ar/admin/manage/users.json +trans.el = public/language/el/admin/manage/users.json +trans.fa_IR = public/language/fa-IR/admin/manage/users.json +trans.hr = public/language/hr/admin/manage/users.json +trans.lv = public/language/lv/admin/manage/users.json +trans.pl = public/language/pl/admin/manage/users.json +trans.sk = public/language/sk/admin/manage/users.json +trans.sr = public/language/sr/admin/manage/users.json +trans.sv = public/language/sv/admin/manage/users.json + +[o:nodebb:p:nodebb:r:admin-menu] +file_filter = public/language//admin/menu.json +source_file = public/language/en-GB/admin/menu.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ru = public/language/ru/admin/menu.json +trans.th = public/language/th/admin/menu.json +trans.vi = public/language/vi/admin/menu.json +trans.zh_CN = public/language/zh-CN/admin/menu.json +trans.hr = public/language/hr/admin/menu.json +trans.ko = public/language/ko/admin/menu.json +trans.it = public/language/it/admin/menu.json +trans.lt = public/language/lt/admin/menu.json +trans.pl = public/language/pl/admin/menu.json +trans.sc = public/language/sc/admin/menu.json +trans.sv = public/language/sv/admin/menu.json +trans.en@pirate = public/language/en-x-pirate/admin/menu.json +trans.fr = public/language/fr/admin/menu.json +trans.de = public/language/de/admin/menu.json +trans.ms = public/language/ms/admin/menu.json +trans.sq_AL = public/language/sq-AL/admin/menu.json +trans.bg = public/language/bg/admin/menu.json +trans.da = public/language/da/admin/menu.json +trans.es = public/language/es/admin/menu.json +trans.hy = public/language/hy/admin/menu.json +trans.id = public/language/id/admin/menu.json +trans.ja = public/language/ja/admin/menu.json +trans.ro = public/language/ro/admin/menu.json +trans.rw = public/language/rw/admin/menu.json +trans.ar = public/language/ar/admin/menu.json +trans.en_US = public/language/en-US/admin/menu.json +trans.sl = public/language/sl/admin/menu.json +trans.uk = public/language/uk/admin/menu.json +trans.he = public/language/he/admin/menu.json +trans.hu = public/language/hu/admin/menu.json +trans.sr = public/language/sr/admin/menu.json +trans.cs = public/language/cs/admin/menu.json +trans.fa_IR = public/language/fa-IR/admin/menu.json +trans.sk = public/language/sk/admin/menu.json +trans.tr = public/language/tr/admin/menu.json +trans.fi = public/language/fi/admin/menu.json +trans.nl = public/language/nl/admin/menu.json +trans.pt_BR = public/language/pt-BR/admin/menu.json +trans.zh_TW = public/language/zh-TW/admin/menu.json +trans.el = public/language/el/admin/menu.json +trans.gl = public/language/gl/admin/menu.json +trans.lv = public/language/lv/admin/menu.json +trans.nb = public/language/nb/admin/menu.json +trans.pt_PT = public/language/pt-PT/admin/menu.json +trans.bn = public/language/bn/admin/menu.json +trans.et = public/language/et/admin/menu.json + +[o:nodebb:p:nodebb:r:admin-settings-advanced] +file_filter = public/language//admin/settings/advanced.json +source_file = public/language/en-GB/admin/settings/advanced.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ko = public/language/ko/admin/settings/advanced.json +trans.sc = public/language/sc/admin/settings/advanced.json +trans.ar = public/language/ar/admin/settings/advanced.json +trans.bn = public/language/bn/admin/settings/advanced.json +trans.el = public/language/el/admin/settings/advanced.json +trans.fa_IR = public/language/fa-IR/admin/settings/advanced.json +trans.fr = public/language/fr/admin/settings/advanced.json +trans.hy = public/language/hy/admin/settings/advanced.json +trans.bg = public/language/bg/admin/settings/advanced.json +trans.lv = public/language/lv/admin/settings/advanced.json +trans.pt_BR = public/language/pt-BR/admin/settings/advanced.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/advanced.json +trans.es = public/language/es/admin/settings/advanced.json +trans.id = public/language/id/admin/settings/advanced.json +trans.rw = public/language/rw/admin/settings/advanced.json +trans.sl = public/language/sl/admin/settings/advanced.json +trans.it = public/language/it/admin/settings/advanced.json +trans.nb = public/language/nb/admin/settings/advanced.json +trans.pt_PT = public/language/pt-PT/admin/settings/advanced.json +trans.sq_AL = public/language/sq-AL/admin/settings/advanced.json +trans.sr = public/language/sr/admin/settings/advanced.json +trans.zh_CN = public/language/zh-CN/admin/settings/advanced.json +trans.zh_TW = public/language/zh-TW/admin/settings/advanced.json +trans.cs = public/language/cs/admin/settings/advanced.json +trans.en_US = public/language/en-US/admin/settings/advanced.json +trans.hr = public/language/hr/admin/settings/advanced.json +trans.pl = public/language/pl/admin/settings/advanced.json +trans.ru = public/language/ru/admin/settings/advanced.json +trans.sv = public/language/sv/admin/settings/advanced.json +trans.vi = public/language/vi/admin/settings/advanced.json +trans.de = public/language/de/admin/settings/advanced.json +trans.et = public/language/et/admin/settings/advanced.json +trans.fi = public/language/fi/admin/settings/advanced.json +trans.ro = public/language/ro/admin/settings/advanced.json +trans.sk = public/language/sk/admin/settings/advanced.json +trans.uk = public/language/uk/admin/settings/advanced.json +trans.da = public/language/da/admin/settings/advanced.json +trans.gl = public/language/gl/admin/settings/advanced.json +trans.he = public/language/he/admin/settings/advanced.json +trans.hu = public/language/hu/admin/settings/advanced.json +trans.ja = public/language/ja/admin/settings/advanced.json +trans.tr = public/language/tr/admin/settings/advanced.json +trans.lt = public/language/lt/admin/settings/advanced.json +trans.ms = public/language/ms/admin/settings/advanced.json +trans.nl = public/language/nl/admin/settings/advanced.json +trans.th = public/language/th/admin/settings/advanced.json + +[o:nodebb:p:nodebb:r:admin-settings-api] +file_filter = public/language//admin/settings/api.json +source_file = public/language/en-GB/admin/settings/api.json +source_lang = en_GB +type = KEYVALUEJSON +trans.et = public/language/et/admin/settings/api.json +trans.lv = public/language/lv/admin/settings/api.json +trans.nl = public/language/nl/admin/settings/api.json +trans.rw = public/language/rw/admin/settings/api.json +trans.sl = public/language/sl/admin/settings/api.json +trans.bn = public/language/bn/admin/settings/api.json +trans.de = public/language/de/admin/settings/api.json +trans.el = public/language/el/admin/settings/api.json +trans.en_US = public/language/en-US/admin/settings/api.json +trans.fi = public/language/fi/admin/settings/api.json +trans.he = public/language/he/admin/settings/api.json +trans.hr = public/language/hr/admin/settings/api.json +trans.hy = public/language/hy/admin/settings/api.json +trans.ja = public/language/ja/admin/settings/api.json +trans.ko = public/language/ko/admin/settings/api.json +trans.ms = public/language/ms/admin/settings/api.json +trans.pt_BR = public/language/pt-BR/admin/settings/api.json +trans.zh_CN = public/language/zh-CN/admin/settings/api.json +trans.zh_TW = public/language/zh-TW/admin/settings/api.json +trans.cs = public/language/cs/admin/settings/api.json +trans.it = public/language/it/admin/settings/api.json +trans.nb = public/language/nb/admin/settings/api.json +trans.pt_PT = public/language/pt-PT/admin/settings/api.json +trans.ru = public/language/ru/admin/settings/api.json +trans.uk = public/language/uk/admin/settings/api.json +trans.vi = public/language/vi/admin/settings/api.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/api.json +trans.fr = public/language/fr/admin/settings/api.json +trans.lt = public/language/lt/admin/settings/api.json +trans.sr = public/language/sr/admin/settings/api.json +trans.fa_IR = public/language/fa-IR/admin/settings/api.json +trans.sq_AL = public/language/sq-AL/admin/settings/api.json +trans.th = public/language/th/admin/settings/api.json +trans.da = public/language/da/admin/settings/api.json +trans.gl = public/language/gl/admin/settings/api.json +trans.pl = public/language/pl/admin/settings/api.json +trans.ar = public/language/ar/admin/settings/api.json +trans.es = public/language/es/admin/settings/api.json +trans.hu = public/language/hu/admin/settings/api.json +trans.sc = public/language/sc/admin/settings/api.json +trans.bg = public/language/bg/admin/settings/api.json +trans.id = public/language/id/admin/settings/api.json +trans.ro = public/language/ro/admin/settings/api.json +trans.sk = public/language/sk/admin/settings/api.json +trans.sv = public/language/sv/admin/settings/api.json +trans.tr = public/language/tr/admin/settings/api.json + +[o:nodebb:p:nodebb:r:admin-settings-chat] +file_filter = public/language//admin/settings/chat.json +source_file = public/language/en-GB/admin/settings/chat.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ar = public/language/ar/admin/settings/chat.json +trans.es = public/language/es/admin/settings/chat.json +trans.et = public/language/et/admin/settings/chat.json +trans.ms = public/language/ms/admin/settings/chat.json +trans.sk = public/language/sk/admin/settings/chat.json +trans.sq_AL = public/language/sq-AL/admin/settings/chat.json +trans.da = public/language/da/admin/settings/chat.json +trans.de = public/language/de/admin/settings/chat.json +trans.en_US = public/language/en-US/admin/settings/chat.json +trans.fa_IR = public/language/fa-IR/admin/settings/chat.json +trans.fr = public/language/fr/admin/settings/chat.json +trans.id = public/language/id/admin/settings/chat.json +trans.ro = public/language/ro/admin/settings/chat.json +trans.el = public/language/el/admin/settings/chat.json +trans.hr = public/language/hr/admin/settings/chat.json +trans.ja = public/language/ja/admin/settings/chat.json +trans.ko = public/language/ko/admin/settings/chat.json +trans.nb = public/language/nb/admin/settings/chat.json +trans.nl = public/language/nl/admin/settings/chat.json +trans.uk = public/language/uk/admin/settings/chat.json +trans.he = public/language/he/admin/settings/chat.json +trans.sl = public/language/sl/admin/settings/chat.json +trans.sr = public/language/sr/admin/settings/chat.json +trans.cs = public/language/cs/admin/settings/chat.json +trans.pl = public/language/pl/admin/settings/chat.json +trans.pt_BR = public/language/pt-BR/admin/settings/chat.json +trans.sv = public/language/sv/admin/settings/chat.json +trans.zh_CN = public/language/zh-CN/admin/settings/chat.json +trans.bg = public/language/bg/admin/settings/chat.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/chat.json +trans.hu = public/language/hu/admin/settings/chat.json +trans.ru = public/language/ru/admin/settings/chat.json +trans.rw = public/language/rw/admin/settings/chat.json +trans.tr = public/language/tr/admin/settings/chat.json +trans.bn = public/language/bn/admin/settings/chat.json +trans.hy = public/language/hy/admin/settings/chat.json +trans.lt = public/language/lt/admin/settings/chat.json +trans.lv = public/language/lv/admin/settings/chat.json +trans.th = public/language/th/admin/settings/chat.json +trans.fi = public/language/fi/admin/settings/chat.json +trans.gl = public/language/gl/admin/settings/chat.json +trans.it = public/language/it/admin/settings/chat.json +trans.pt_PT = public/language/pt-PT/admin/settings/chat.json +trans.sc = public/language/sc/admin/settings/chat.json +trans.vi = public/language/vi/admin/settings/chat.json +trans.zh_TW = public/language/zh-TW/admin/settings/chat.json + +[o:nodebb:p:nodebb:r:admin-settings-cookies] +file_filter = public/language//admin/settings/cookies.json +source_file = public/language/en-GB/admin/settings/cookies.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sr = public/language/sr/admin/settings/cookies.json +trans.th = public/language/th/admin/settings/cookies.json +trans.zh_CN = public/language/zh-CN/admin/settings/cookies.json +trans.en_US = public/language/en-US/admin/settings/cookies.json +trans.lt = public/language/lt/admin/settings/cookies.json +trans.pl = public/language/pl/admin/settings/cookies.json +trans.ro = public/language/ro/admin/settings/cookies.json +trans.rw = public/language/rw/admin/settings/cookies.json +trans.sk = public/language/sk/admin/settings/cookies.json +trans.tr = public/language/tr/admin/settings/cookies.json +trans.ar = public/language/ar/admin/settings/cookies.json +trans.da = public/language/da/admin/settings/cookies.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/cookies.json +trans.gl = public/language/gl/admin/settings/cookies.json +trans.hu = public/language/hu/admin/settings/cookies.json +trans.vi = public/language/vi/admin/settings/cookies.json +trans.el = public/language/el/admin/settings/cookies.json +trans.et = public/language/et/admin/settings/cookies.json +trans.lv = public/language/lv/admin/settings/cookies.json +trans.ru = public/language/ru/admin/settings/cookies.json +trans.uk = public/language/uk/admin/settings/cookies.json +trans.ko = public/language/ko/admin/settings/cookies.json +trans.sv = public/language/sv/admin/settings/cookies.json +trans.bn = public/language/bn/admin/settings/cookies.json +trans.de = public/language/de/admin/settings/cookies.json +trans.he = public/language/he/admin/settings/cookies.json +trans.hy = public/language/hy/admin/settings/cookies.json +trans.it = public/language/it/admin/settings/cookies.json +trans.cs = public/language/cs/admin/settings/cookies.json +trans.hr = public/language/hr/admin/settings/cookies.json +trans.ja = public/language/ja/admin/settings/cookies.json +trans.pt_PT = public/language/pt-PT/admin/settings/cookies.json +trans.nb = public/language/nb/admin/settings/cookies.json +trans.sc = public/language/sc/admin/settings/cookies.json +trans.bg = public/language/bg/admin/settings/cookies.json +trans.fi = public/language/fi/admin/settings/cookies.json +trans.fr = public/language/fr/admin/settings/cookies.json +trans.id = public/language/id/admin/settings/cookies.json +trans.ms = public/language/ms/admin/settings/cookies.json +trans.zh_TW = public/language/zh-TW/admin/settings/cookies.json +trans.es = public/language/es/admin/settings/cookies.json +trans.fa_IR = public/language/fa-IR/admin/settings/cookies.json +trans.nl = public/language/nl/admin/settings/cookies.json +trans.sl = public/language/sl/admin/settings/cookies.json +trans.sq_AL = public/language/sq-AL/admin/settings/cookies.json +trans.pt_BR = public/language/pt-BR/admin/settings/cookies.json + +[o:nodebb:p:nodebb:r:admin-settings-email] +file_filter = public/language//admin/settings/email.json +source_file = public/language/en-GB/admin/settings/email.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sv = public/language/sv/admin/settings/email.json +trans.tr = public/language/tr/admin/settings/email.json +trans.uk = public/language/uk/admin/settings/email.json +trans.ar = public/language/ar/admin/settings/email.json +trans.ja = public/language/ja/admin/settings/email.json +trans.pt_PT = public/language/pt-PT/admin/settings/email.json +trans.sr = public/language/sr/admin/settings/email.json +trans.ms = public/language/ms/admin/settings/email.json +trans.nb = public/language/nb/admin/settings/email.json +trans.ru = public/language/ru/admin/settings/email.json +trans.sk = public/language/sk/admin/settings/email.json +trans.fr = public/language/fr/admin/settings/email.json +trans.he = public/language/he/admin/settings/email.json +trans.id = public/language/id/admin/settings/email.json +trans.it = public/language/it/admin/settings/email.json +trans.sl = public/language/sl/admin/settings/email.json +trans.sq_AL = public/language/sq-AL/admin/settings/email.json +trans.hr = public/language/hr/admin/settings/email.json +trans.hu = public/language/hu/admin/settings/email.json +trans.ko = public/language/ko/admin/settings/email.json +trans.zh_TW = public/language/zh-TW/admin/settings/email.json +trans.bn = public/language/bn/admin/settings/email.json +trans.fi = public/language/fi/admin/settings/email.json +trans.pt_BR = public/language/pt-BR/admin/settings/email.json +trans.es = public/language/es/admin/settings/email.json +trans.pl = public/language/pl/admin/settings/email.json +trans.ro = public/language/ro/admin/settings/email.json +trans.rw = public/language/rw/admin/settings/email.json +trans.bg = public/language/bg/admin/settings/email.json +trans.cs = public/language/cs/admin/settings/email.json +trans.el = public/language/el/admin/settings/email.json +trans.en_US = public/language/en-US/admin/settings/email.json +trans.zh_CN = public/language/zh-CN/admin/settings/email.json +trans.fa_IR = public/language/fa-IR/admin/settings/email.json +trans.gl = public/language/gl/admin/settings/email.json +trans.hy = public/language/hy/admin/settings/email.json +trans.lv = public/language/lv/admin/settings/email.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/email.json +trans.et = public/language/et/admin/settings/email.json +trans.lt = public/language/lt/admin/settings/email.json +trans.th = public/language/th/admin/settings/email.json +trans.vi = public/language/vi/admin/settings/email.json +trans.da = public/language/da/admin/settings/email.json +trans.de = public/language/de/admin/settings/email.json +trans.nl = public/language/nl/admin/settings/email.json +trans.sc = public/language/sc/admin/settings/email.json + +[o:nodebb:p:nodebb:r:admin-settings-general] +file_filter = public/language//admin/settings/general.json +source_file = public/language/en-GB/admin/settings/general.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ko = public/language/ko/admin/settings/general.json +trans.ro = public/language/ro/admin/settings/general.json +trans.sc = public/language/sc/admin/settings/general.json +trans.sk = public/language/sk/admin/settings/general.json +trans.sv = public/language/sv/admin/settings/general.json +trans.uk = public/language/uk/admin/settings/general.json +trans.fr = public/language/fr/admin/settings/general.json +trans.gl = public/language/gl/admin/settings/general.json +trans.zh_CN = public/language/zh-CN/admin/settings/general.json +trans.lt = public/language/lt/admin/settings/general.json +trans.rw = public/language/rw/admin/settings/general.json +trans.th = public/language/th/admin/settings/general.json +trans.tr = public/language/tr/admin/settings/general.json +trans.zh_TW = public/language/zh-TW/admin/settings/general.json +trans.de = public/language/de/admin/settings/general.json +trans.ja = public/language/ja/admin/settings/general.json +trans.en_US = public/language/en-US/admin/settings/general.json +trans.et = public/language/et/admin/settings/general.json +trans.fi = public/language/fi/admin/settings/general.json +trans.hy = public/language/hy/admin/settings/general.json +trans.id = public/language/id/admin/settings/general.json +trans.ru = public/language/ru/admin/settings/general.json +trans.cs = public/language/cs/admin/settings/general.json +trans.el = public/language/el/admin/settings/general.json +trans.sl = public/language/sl/admin/settings/general.json +trans.hu = public/language/hu/admin/settings/general.json +trans.it = public/language/it/admin/settings/general.json +trans.nl = public/language/nl/admin/settings/general.json +trans.pl = public/language/pl/admin/settings/general.json +trans.es = public/language/es/admin/settings/general.json +trans.fa_IR = public/language/fa-IR/admin/settings/general.json +trans.ms = public/language/ms/admin/settings/general.json +trans.nb = public/language/nb/admin/settings/general.json +trans.sr = public/language/sr/admin/settings/general.json +trans.da = public/language/da/admin/settings/general.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/general.json +trans.pt_PT = public/language/pt-PT/admin/settings/general.json +trans.he = public/language/he/admin/settings/general.json +trans.lv = public/language/lv/admin/settings/general.json +trans.pt_BR = public/language/pt-BR/admin/settings/general.json +trans.bn = public/language/bn/admin/settings/general.json +trans.hr = public/language/hr/admin/settings/general.json +trans.sq_AL = public/language/sq-AL/admin/settings/general.json +trans.vi = public/language/vi/admin/settings/general.json +trans.ar = public/language/ar/admin/settings/general.json +trans.bg = public/language/bg/admin/settings/general.json + +[o:nodebb:p:nodebb:r:admin-settings-group] +file_filter = public/language//admin/settings/group.json +source_file = public/language/en-GB/admin/settings/group.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sc = public/language/sc/admin/settings/group.json +trans.en_US = public/language/en-US/admin/settings/group.json +trans.et = public/language/et/admin/settings/group.json +trans.fr = public/language/fr/admin/settings/group.json +trans.it = public/language/it/admin/settings/group.json +trans.nb = public/language/nb/admin/settings/group.json +trans.es = public/language/es/admin/settings/group.json +trans.fa_IR = public/language/fa-IR/admin/settings/group.json +trans.gl = public/language/gl/admin/settings/group.json +trans.rw = public/language/rw/admin/settings/group.json +trans.sr = public/language/sr/admin/settings/group.json +trans.sq_AL = public/language/sq-AL/admin/settings/group.json +trans.th = public/language/th/admin/settings/group.json +trans.vi = public/language/vi/admin/settings/group.json +trans.bn = public/language/bn/admin/settings/group.json +trans.fi = public/language/fi/admin/settings/group.json +trans.hy = public/language/hy/admin/settings/group.json +trans.id = public/language/id/admin/settings/group.json +trans.ms = public/language/ms/admin/settings/group.json +trans.de = public/language/de/admin/settings/group.json +trans.he = public/language/he/admin/settings/group.json +trans.lv = public/language/lv/admin/settings/group.json +trans.pt_PT = public/language/pt-PT/admin/settings/group.json +trans.sk = public/language/sk/admin/settings/group.json +trans.tr = public/language/tr/admin/settings/group.json +trans.uk = public/language/uk/admin/settings/group.json +trans.cs = public/language/cs/admin/settings/group.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/group.json +trans.hu = public/language/hu/admin/settings/group.json +trans.nl = public/language/nl/admin/settings/group.json +trans.ru = public/language/ru/admin/settings/group.json +trans.lt = public/language/lt/admin/settings/group.json +trans.ro = public/language/ro/admin/settings/group.json +trans.sl = public/language/sl/admin/settings/group.json +trans.ar = public/language/ar/admin/settings/group.json +trans.bg = public/language/bg/admin/settings/group.json +trans.da = public/language/da/admin/settings/group.json +trans.hr = public/language/hr/admin/settings/group.json +trans.ko = public/language/ko/admin/settings/group.json +trans.el = public/language/el/admin/settings/group.json +trans.sv = public/language/sv/admin/settings/group.json +trans.ja = public/language/ja/admin/settings/group.json +trans.pl = public/language/pl/admin/settings/group.json +trans.pt_BR = public/language/pt-BR/admin/settings/group.json +trans.zh_CN = public/language/zh-CN/admin/settings/group.json +trans.zh_TW = public/language/zh-TW/admin/settings/group.json + +[o:nodebb:p:nodebb:r:admin-settings-guest] +file_filter = public/language//admin/settings/guest.json +source_file = public/language/en-GB/admin/settings/guest.json +source_lang = en_GB +type = KEYVALUEJSON +trans.cs = public/language/cs/admin/settings/guest.json +trans.da = public/language/da/admin/settings/guest.json +trans.en_US = public/language/en-US/admin/settings/guest.json +trans.lt = public/language/lt/admin/settings/guest.json +trans.ms = public/language/ms/admin/settings/guest.json +trans.nl = public/language/nl/admin/settings/guest.json +trans.ru = public/language/ru/admin/settings/guest.json +trans.sc = public/language/sc/admin/settings/guest.json +trans.sr = public/language/sr/admin/settings/guest.json +trans.th = public/language/th/admin/settings/guest.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/guest.json +trans.it = public/language/it/admin/settings/guest.json +trans.lv = public/language/lv/admin/settings/guest.json +trans.pt_BR = public/language/pt-BR/admin/settings/guest.json +trans.sk = public/language/sk/admin/settings/guest.json +trans.de = public/language/de/admin/settings/guest.json +trans.es = public/language/es/admin/settings/guest.json +trans.hr = public/language/hr/admin/settings/guest.json +trans.pt_PT = public/language/pt-PT/admin/settings/guest.json +trans.rw = public/language/rw/admin/settings/guest.json +trans.sq_AL = public/language/sq-AL/admin/settings/guest.json +trans.el = public/language/el/admin/settings/guest.json +trans.tr = public/language/tr/admin/settings/guest.json +trans.zh_CN = public/language/zh-CN/admin/settings/guest.json +trans.bg = public/language/bg/admin/settings/guest.json +trans.fi = public/language/fi/admin/settings/guest.json +trans.hu = public/language/hu/admin/settings/guest.json +trans.ja = public/language/ja/admin/settings/guest.json +trans.nb = public/language/nb/admin/settings/guest.json +trans.pl = public/language/pl/admin/settings/guest.json +trans.sl = public/language/sl/admin/settings/guest.json +trans.vi = public/language/vi/admin/settings/guest.json +trans.bn = public/language/bn/admin/settings/guest.json +trans.fa_IR = public/language/fa-IR/admin/settings/guest.json +trans.fr = public/language/fr/admin/settings/guest.json +trans.hy = public/language/hy/admin/settings/guest.json +trans.ro = public/language/ro/admin/settings/guest.json +trans.et = public/language/et/admin/settings/guest.json +trans.gl = public/language/gl/admin/settings/guest.json +trans.sv = public/language/sv/admin/settings/guest.json +trans.uk = public/language/uk/admin/settings/guest.json +trans.zh_TW = public/language/zh-TW/admin/settings/guest.json +trans.ar = public/language/ar/admin/settings/guest.json +trans.he = public/language/he/admin/settings/guest.json +trans.id = public/language/id/admin/settings/guest.json +trans.ko = public/language/ko/admin/settings/guest.json + +[o:nodebb:p:nodebb:r:admin-settings-homepage] +file_filter = public/language//admin/settings/homepage.json +source_file = public/language/en-GB/admin/settings/homepage.json +source_lang = en_GB +type = KEYVALUEJSON +trans.nb = public/language/nb/admin/settings/homepage.json +trans.tr = public/language/tr/admin/settings/homepage.json +trans.vi = public/language/vi/admin/settings/homepage.json +trans.et = public/language/et/admin/settings/homepage.json +trans.fi = public/language/fi/admin/settings/homepage.json +trans.hy = public/language/hy/admin/settings/homepage.json +trans.ru = public/language/ru/admin/settings/homepage.json +trans.sr = public/language/sr/admin/settings/homepage.json +trans.es = public/language/es/admin/settings/homepage.json +trans.id = public/language/id/admin/settings/homepage.json +trans.lt = public/language/lt/admin/settings/homepage.json +trans.sk = public/language/sk/admin/settings/homepage.json +trans.sq_AL = public/language/sq-AL/admin/settings/homepage.json +trans.th = public/language/th/admin/settings/homepage.json +trans.en_US = public/language/en-US/admin/settings/homepage.json +trans.lv = public/language/lv/admin/settings/homepage.json +trans.pt_PT = public/language/pt-PT/admin/settings/homepage.json +trans.sc = public/language/sc/admin/settings/homepage.json +trans.fa_IR = public/language/fa-IR/admin/settings/homepage.json +trans.he = public/language/he/admin/settings/homepage.json +trans.ms = public/language/ms/admin/settings/homepage.json +trans.cs = public/language/cs/admin/settings/homepage.json +trans.el = public/language/el/admin/settings/homepage.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/homepage.json +trans.gl = public/language/gl/admin/settings/homepage.json +trans.ko = public/language/ko/admin/settings/homepage.json +trans.ar = public/language/ar/admin/settings/homepage.json +trans.bg = public/language/bg/admin/settings/homepage.json +trans.bn = public/language/bn/admin/settings/homepage.json +trans.rw = public/language/rw/admin/settings/homepage.json +trans.zh_CN = public/language/zh-CN/admin/settings/homepage.json +trans.hr = public/language/hr/admin/settings/homepage.json +trans.it = public/language/it/admin/settings/homepage.json +trans.ja = public/language/ja/admin/settings/homepage.json +trans.nl = public/language/nl/admin/settings/homepage.json +trans.ro = public/language/ro/admin/settings/homepage.json +trans.da = public/language/da/admin/settings/homepage.json +trans.de = public/language/de/admin/settings/homepage.json +trans.fr = public/language/fr/admin/settings/homepage.json +trans.sv = public/language/sv/admin/settings/homepage.json +trans.hu = public/language/hu/admin/settings/homepage.json +trans.pt_BR = public/language/pt-BR/admin/settings/homepage.json +trans.sl = public/language/sl/admin/settings/homepage.json +trans.pl = public/language/pl/admin/settings/homepage.json +trans.uk = public/language/uk/admin/settings/homepage.json +trans.zh_TW = public/language/zh-TW/admin/settings/homepage.json + +[o:nodebb:p:nodebb:r:admin-settings-languages] +file_filter = public/language//admin/settings/languages.json +source_file = public/language/en-GB/admin/settings/languages.json +source_lang = en_GB +type = KEYVALUEJSON +trans.en@pirate = public/language/en-x-pirate/admin/settings/languages.json +trans.fa_IR = public/language/fa-IR/admin/settings/languages.json +trans.gl = public/language/gl/admin/settings/languages.json +trans.ro = public/language/ro/admin/settings/languages.json +trans.th = public/language/th/admin/settings/languages.json +trans.hr = public/language/hr/admin/settings/languages.json +trans.hy = public/language/hy/admin/settings/languages.json +trans.sc = public/language/sc/admin/settings/languages.json +trans.vi = public/language/vi/admin/settings/languages.json +trans.tr = public/language/tr/admin/settings/languages.json +trans.cs = public/language/cs/admin/settings/languages.json +trans.et = public/language/et/admin/settings/languages.json +trans.lv = public/language/lv/admin/settings/languages.json +trans.pl = public/language/pl/admin/settings/languages.json +trans.sr = public/language/sr/admin/settings/languages.json +trans.sv = public/language/sv/admin/settings/languages.json +trans.hu = public/language/hu/admin/settings/languages.json +trans.it = public/language/it/admin/settings/languages.json +trans.ja = public/language/ja/admin/settings/languages.json +trans.sl = public/language/sl/admin/settings/languages.json +trans.zh_TW = public/language/zh-TW/admin/settings/languages.json +trans.da = public/language/da/admin/settings/languages.json +trans.fr = public/language/fr/admin/settings/languages.json +trans.he = public/language/he/admin/settings/languages.json +trans.id = public/language/id/admin/settings/languages.json +trans.sq_AL = public/language/sq-AL/admin/settings/languages.json +trans.uk = public/language/uk/admin/settings/languages.json +trans.bn = public/language/bn/admin/settings/languages.json +trans.fi = public/language/fi/admin/settings/languages.json +trans.ko = public/language/ko/admin/settings/languages.json +trans.pt_BR = public/language/pt-BR/admin/settings/languages.json +trans.sk = public/language/sk/admin/settings/languages.json +trans.pt_PT = public/language/pt-PT/admin/settings/languages.json +trans.ru = public/language/ru/admin/settings/languages.json +trans.ar = public/language/ar/admin/settings/languages.json +trans.bg = public/language/bg/admin/settings/languages.json +trans.de = public/language/de/admin/settings/languages.json +trans.el = public/language/el/admin/settings/languages.json +trans.lt = public/language/lt/admin/settings/languages.json +trans.nl = public/language/nl/admin/settings/languages.json +trans.zh_CN = public/language/zh-CN/admin/settings/languages.json +trans.en_US = public/language/en-US/admin/settings/languages.json +trans.es = public/language/es/admin/settings/languages.json +trans.ms = public/language/ms/admin/settings/languages.json +trans.nb = public/language/nb/admin/settings/languages.json +trans.rw = public/language/rw/admin/settings/languages.json + +[o:nodebb:p:nodebb:r:admin-settings-navigation] +file_filter = public/language//admin/settings/navigation.json +source_file = public/language/en-GB/admin/settings/navigation.json +source_lang = en_GB +type = KEYVALUEJSON +trans.uk = public/language/uk/admin/settings/navigation.json +trans.bn = public/language/bn/admin/settings/navigation.json +trans.es = public/language/es/admin/settings/navigation.json +trans.fa_IR = public/language/fa-IR/admin/settings/navigation.json +trans.nl = public/language/nl/admin/settings/navigation.json +trans.pt_BR = public/language/pt-BR/admin/settings/navigation.json +trans.rw = public/language/rw/admin/settings/navigation.json +trans.zh_CN = public/language/zh-CN/admin/settings/navigation.json +trans.da = public/language/da/admin/settings/navigation.json +trans.it = public/language/it/admin/settings/navigation.json +trans.ms = public/language/ms/admin/settings/navigation.json +trans.pt_PT = public/language/pt-PT/admin/settings/navigation.json +trans.vi = public/language/vi/admin/settings/navigation.json +trans.en_US = public/language/en-US/admin/settings/navigation.json +trans.fr = public/language/fr/admin/settings/navigation.json +trans.hu = public/language/hu/admin/settings/navigation.json +trans.hy = public/language/hy/admin/settings/navigation.json +trans.ja = public/language/ja/admin/settings/navigation.json +trans.lt = public/language/lt/admin/settings/navigation.json +trans.ru = public/language/ru/admin/settings/navigation.json +trans.sk = public/language/sk/admin/settings/navigation.json +trans.sr = public/language/sr/admin/settings/navigation.json +trans.de = public/language/de/admin/settings/navigation.json +trans.et = public/language/et/admin/settings/navigation.json +trans.fi = public/language/fi/admin/settings/navigation.json +trans.gl = public/language/gl/admin/settings/navigation.json +trans.sc = public/language/sc/admin/settings/navigation.json +trans.sl = public/language/sl/admin/settings/navigation.json +trans.th = public/language/th/admin/settings/navigation.json +trans.bg = public/language/bg/admin/settings/navigation.json +trans.cs = public/language/cs/admin/settings/navigation.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/navigation.json +trans.ro = public/language/ro/admin/settings/navigation.json +trans.sq_AL = public/language/sq-AL/admin/settings/navigation.json +trans.tr = public/language/tr/admin/settings/navigation.json +trans.zh_TW = public/language/zh-TW/admin/settings/navigation.json +trans.ar = public/language/ar/admin/settings/navigation.json +trans.hr = public/language/hr/admin/settings/navigation.json +trans.id = public/language/id/admin/settings/navigation.json +trans.ko = public/language/ko/admin/settings/navigation.json +trans.nb = public/language/nb/admin/settings/navigation.json +trans.pl = public/language/pl/admin/settings/navigation.json +trans.sv = public/language/sv/admin/settings/navigation.json +trans.el = public/language/el/admin/settings/navigation.json +trans.he = public/language/he/admin/settings/navigation.json +trans.lv = public/language/lv/admin/settings/navigation.json + +[o:nodebb:p:nodebb:r:admin-settings-notifications] +file_filter = public/language//admin/settings/notifications.json +source_file = public/language/en-GB/admin/settings/notifications.json +source_lang = en_GB +type = KEYVALUEJSON +trans.zh_TW = public/language/zh-TW/admin/settings/notifications.json +trans.fi = public/language/fi/admin/settings/notifications.json +trans.id = public/language/id/admin/settings/notifications.json +trans.pt_PT = public/language/pt-PT/admin/settings/notifications.json +trans.rw = public/language/rw/admin/settings/notifications.json +trans.ar = public/language/ar/admin/settings/notifications.json +trans.bn = public/language/bn/admin/settings/notifications.json +trans.el = public/language/el/admin/settings/notifications.json +trans.en_US = public/language/en-US/admin/settings/notifications.json +trans.sk = public/language/sk/admin/settings/notifications.json +trans.sr = public/language/sr/admin/settings/notifications.json +trans.sv = public/language/sv/admin/settings/notifications.json +trans.th = public/language/th/admin/settings/notifications.json +trans.cs = public/language/cs/admin/settings/notifications.json +trans.it = public/language/it/admin/settings/notifications.json +trans.ru = public/language/ru/admin/settings/notifications.json +trans.sq_AL = public/language/sq-AL/admin/settings/notifications.json +trans.vi = public/language/vi/admin/settings/notifications.json +trans.fr = public/language/fr/admin/settings/notifications.json +trans.hy = public/language/hy/admin/settings/notifications.json +trans.sl = public/language/sl/admin/settings/notifications.json +trans.uk = public/language/uk/admin/settings/notifications.json +trans.ms = public/language/ms/admin/settings/notifications.json +trans.nl = public/language/nl/admin/settings/notifications.json +trans.ro = public/language/ro/admin/settings/notifications.json +trans.sc = public/language/sc/admin/settings/notifications.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/notifications.json +trans.gl = public/language/gl/admin/settings/notifications.json +trans.he = public/language/he/admin/settings/notifications.json +trans.hu = public/language/hu/admin/settings/notifications.json +trans.tr = public/language/tr/admin/settings/notifications.json +trans.zh_CN = public/language/zh-CN/admin/settings/notifications.json +trans.ko = public/language/ko/admin/settings/notifications.json +trans.lv = public/language/lv/admin/settings/notifications.json +trans.nb = public/language/nb/admin/settings/notifications.json +trans.pl = public/language/pl/admin/settings/notifications.json +trans.bg = public/language/bg/admin/settings/notifications.json +trans.da = public/language/da/admin/settings/notifications.json +trans.de = public/language/de/admin/settings/notifications.json +trans.hr = public/language/hr/admin/settings/notifications.json +trans.pt_BR = public/language/pt-BR/admin/settings/notifications.json +trans.et = public/language/et/admin/settings/notifications.json +trans.lt = public/language/lt/admin/settings/notifications.json +trans.es = public/language/es/admin/settings/notifications.json +trans.fa_IR = public/language/fa-IR/admin/settings/notifications.json +trans.ja = public/language/ja/admin/settings/notifications.json + +[o:nodebb:p:nodebb:r:admin-settings-pagination] +file_filter = public/language//admin/settings/pagination.json +source_file = public/language/en-GB/admin/settings/pagination.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sq_AL = public/language/sq-AL/admin/settings/pagination.json +trans.th = public/language/th/admin/settings/pagination.json +trans.de = public/language/de/admin/settings/pagination.json +trans.ru = public/language/ru/admin/settings/pagination.json +trans.rw = public/language/rw/admin/settings/pagination.json +trans.sc = public/language/sc/admin/settings/pagination.json +trans.sk = public/language/sk/admin/settings/pagination.json +trans.lv = public/language/lv/admin/settings/pagination.json +trans.ar = public/language/ar/admin/settings/pagination.json +trans.el = public/language/el/admin/settings/pagination.json +trans.en_US = public/language/en-US/admin/settings/pagination.json +trans.et = public/language/et/admin/settings/pagination.json +trans.fr = public/language/fr/admin/settings/pagination.json +trans.bg = public/language/bg/admin/settings/pagination.json +trans.fi = public/language/fi/admin/settings/pagination.json +trans.ja = public/language/ja/admin/settings/pagination.json +trans.ms = public/language/ms/admin/settings/pagination.json +trans.uk = public/language/uk/admin/settings/pagination.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/pagination.json +trans.hu = public/language/hu/admin/settings/pagination.json +trans.ko = public/language/ko/admin/settings/pagination.json +trans.lt = public/language/lt/admin/settings/pagination.json +trans.nl = public/language/nl/admin/settings/pagination.json +trans.hr = public/language/hr/admin/settings/pagination.json +trans.pl = public/language/pl/admin/settings/pagination.json +trans.pt_BR = public/language/pt-BR/admin/settings/pagination.json +trans.cs = public/language/cs/admin/settings/pagination.json +trans.da = public/language/da/admin/settings/pagination.json +trans.es = public/language/es/admin/settings/pagination.json +trans.gl = public/language/gl/admin/settings/pagination.json +trans.he = public/language/he/admin/settings/pagination.json +trans.pt_PT = public/language/pt-PT/admin/settings/pagination.json +trans.ro = public/language/ro/admin/settings/pagination.json +trans.id = public/language/id/admin/settings/pagination.json +trans.tr = public/language/tr/admin/settings/pagination.json +trans.vi = public/language/vi/admin/settings/pagination.json +trans.fa_IR = public/language/fa-IR/admin/settings/pagination.json +trans.it = public/language/it/admin/settings/pagination.json +trans.sl = public/language/sl/admin/settings/pagination.json +trans.zh_CN = public/language/zh-CN/admin/settings/pagination.json +trans.zh_TW = public/language/zh-TW/admin/settings/pagination.json +trans.bn = public/language/bn/admin/settings/pagination.json +trans.hy = public/language/hy/admin/settings/pagination.json +trans.nb = public/language/nb/admin/settings/pagination.json +trans.sr = public/language/sr/admin/settings/pagination.json +trans.sv = public/language/sv/admin/settings/pagination.json + +[o:nodebb:p:nodebb:r:admin-settings-post] +file_filter = public/language//admin/settings/post.json +source_file = public/language/en-GB/admin/settings/post.json +source_lang = en_GB +type = KEYVALUEJSON +trans.id = public/language/id/admin/settings/post.json +trans.bn = public/language/bn/admin/settings/post.json +trans.da = public/language/da/admin/settings/post.json +trans.de = public/language/de/admin/settings/post.json +trans.en_US = public/language/en-US/admin/settings/post.json +trans.ms = public/language/ms/admin/settings/post.json +trans.ro = public/language/ro/admin/settings/post.json +trans.zh_TW = public/language/zh-TW/admin/settings/post.json +trans.cs = public/language/cs/admin/settings/post.json +trans.he = public/language/he/admin/settings/post.json +trans.ja = public/language/ja/admin/settings/post.json +trans.ko = public/language/ko/admin/settings/post.json +trans.hr = public/language/hr/admin/settings/post.json +trans.sq_AL = public/language/sq-AL/admin/settings/post.json +trans.lv = public/language/lv/admin/settings/post.json +trans.nl = public/language/nl/admin/settings/post.json +trans.pt_PT = public/language/pt-PT/admin/settings/post.json +trans.sr = public/language/sr/admin/settings/post.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/post.json +trans.et = public/language/et/admin/settings/post.json +trans.hu = public/language/hu/admin/settings/post.json +trans.it = public/language/it/admin/settings/post.json +trans.th = public/language/th/admin/settings/post.json +trans.gl = public/language/gl/admin/settings/post.json +trans.nb = public/language/nb/admin/settings/post.json +trans.sc = public/language/sc/admin/settings/post.json +trans.zh_CN = public/language/zh-CN/admin/settings/post.json +trans.rw = public/language/rw/admin/settings/post.json +trans.sv = public/language/sv/admin/settings/post.json +trans.vi = public/language/vi/admin/settings/post.json +trans.ar = public/language/ar/admin/settings/post.json +trans.el = public/language/el/admin/settings/post.json +trans.fa_IR = public/language/fa-IR/admin/settings/post.json +trans.fi = public/language/fi/admin/settings/post.json +trans.ru = public/language/ru/admin/settings/post.json +trans.bg = public/language/bg/admin/settings/post.json +trans.es = public/language/es/admin/settings/post.json +trans.hy = public/language/hy/admin/settings/post.json +trans.pt_BR = public/language/pt-BR/admin/settings/post.json +trans.sl = public/language/sl/admin/settings/post.json +trans.tr = public/language/tr/admin/settings/post.json +trans.uk = public/language/uk/admin/settings/post.json +trans.fr = public/language/fr/admin/settings/post.json +trans.lt = public/language/lt/admin/settings/post.json +trans.pl = public/language/pl/admin/settings/post.json +trans.sk = public/language/sk/admin/settings/post.json + +[o:nodebb:p:nodebb:r:admin-settings-reputation] +file_filter = public/language//admin/settings/reputation.json +source_file = public/language/en-GB/admin/settings/reputation.json +source_lang = en_GB +type = KEYVALUEJSON +trans.fi = public/language/fi/admin/settings/reputation.json +trans.ja = public/language/ja/admin/settings/reputation.json +trans.lt = public/language/lt/admin/settings/reputation.json +trans.pl = public/language/pl/admin/settings/reputation.json +trans.ro = public/language/ro/admin/settings/reputation.json +trans.bg = public/language/bg/admin/settings/reputation.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/reputation.json +trans.fa_IR = public/language/fa-IR/admin/settings/reputation.json +trans.sl = public/language/sl/admin/settings/reputation.json +trans.sv = public/language/sv/admin/settings/reputation.json +trans.uk = public/language/uk/admin/settings/reputation.json +trans.sr = public/language/sr/admin/settings/reputation.json +trans.zh_CN = public/language/zh-CN/admin/settings/reputation.json +trans.ar = public/language/ar/admin/settings/reputation.json +trans.el = public/language/el/admin/settings/reputation.json +trans.es = public/language/es/admin/settings/reputation.json +trans.sc = public/language/sc/admin/settings/reputation.json +trans.hr = public/language/hr/admin/settings/reputation.json +trans.ko = public/language/ko/admin/settings/reputation.json +trans.nb = public/language/nb/admin/settings/reputation.json +trans.bn = public/language/bn/admin/settings/reputation.json +trans.de = public/language/de/admin/settings/reputation.json +trans.et = public/language/et/admin/settings/reputation.json +trans.rw = public/language/rw/admin/settings/reputation.json +trans.th = public/language/th/admin/settings/reputation.json +trans.zh_TW = public/language/zh-TW/admin/settings/reputation.json +trans.fr = public/language/fr/admin/settings/reputation.json +trans.it = public/language/it/admin/settings/reputation.json +trans.ru = public/language/ru/admin/settings/reputation.json +trans.pt_PT = public/language/pt-PT/admin/settings/reputation.json +trans.sk = public/language/sk/admin/settings/reputation.json +trans.id = public/language/id/admin/settings/reputation.json +trans.ms = public/language/ms/admin/settings/reputation.json +trans.nl = public/language/nl/admin/settings/reputation.json +trans.he = public/language/he/admin/settings/reputation.json +trans.sq_AL = public/language/sq-AL/admin/settings/reputation.json +trans.tr = public/language/tr/admin/settings/reputation.json +trans.cs = public/language/cs/admin/settings/reputation.json +trans.da = public/language/da/admin/settings/reputation.json +trans.en_US = public/language/en-US/admin/settings/reputation.json +trans.lv = public/language/lv/admin/settings/reputation.json +trans.pt_BR = public/language/pt-BR/admin/settings/reputation.json +trans.vi = public/language/vi/admin/settings/reputation.json +trans.gl = public/language/gl/admin/settings/reputation.json +trans.hu = public/language/hu/admin/settings/reputation.json +trans.hy = public/language/hy/admin/settings/reputation.json + +[o:nodebb:p:nodebb:r:admin-settings-social] +file_filter = public/language//admin/settings/social.json +source_file = public/language/en-GB/admin/settings/social.json +source_lang = en_GB +type = KEYVALUEJSON +trans.nl = public/language/nl/admin/settings/social.json +trans.sc = public/language/sc/admin/settings/social.json +trans.sv = public/language/sv/admin/settings/social.json +trans.rw = public/language/rw/admin/settings/social.json +trans.bn = public/language/bn/admin/settings/social.json +trans.hr = public/language/hr/admin/settings/social.json +trans.id = public/language/id/admin/settings/social.json +trans.pt_PT = public/language/pt-PT/admin/settings/social.json +trans.de = public/language/de/admin/settings/social.json +trans.hu = public/language/hu/admin/settings/social.json +trans.pt_BR = public/language/pt-BR/admin/settings/social.json +trans.ja = public/language/ja/admin/settings/social.json +trans.sq_AL = public/language/sq-AL/admin/settings/social.json +trans.zh_TW = public/language/zh-TW/admin/settings/social.json +trans.cs = public/language/cs/admin/settings/social.json +trans.en_US = public/language/en-US/admin/settings/social.json +trans.fi = public/language/fi/admin/settings/social.json +trans.nb = public/language/nb/admin/settings/social.json +trans.pl = public/language/pl/admin/settings/social.json +trans.th = public/language/th/admin/settings/social.json +trans.bg = public/language/bg/admin/settings/social.json +trans.lt = public/language/lt/admin/settings/social.json +trans.lv = public/language/lv/admin/settings/social.json +trans.ko = public/language/ko/admin/settings/social.json +trans.es = public/language/es/admin/settings/social.json +trans.fr = public/language/fr/admin/settings/social.json +trans.he = public/language/he/admin/settings/social.json +trans.fa_IR = public/language/fa-IR/admin/settings/social.json +trans.hy = public/language/hy/admin/settings/social.json +trans.ms = public/language/ms/admin/settings/social.json +trans.ru = public/language/ru/admin/settings/social.json +trans.sk = public/language/sk/admin/settings/social.json +trans.ar = public/language/ar/admin/settings/social.json +trans.el = public/language/el/admin/settings/social.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/social.json +trans.uk = public/language/uk/admin/settings/social.json +trans.sl = public/language/sl/admin/settings/social.json +trans.sr = public/language/sr/admin/settings/social.json +trans.tr = public/language/tr/admin/settings/social.json +trans.it = public/language/it/admin/settings/social.json +trans.ro = public/language/ro/admin/settings/social.json +trans.vi = public/language/vi/admin/settings/social.json +trans.zh_CN = public/language/zh-CN/admin/settings/social.json +trans.da = public/language/da/admin/settings/social.json +trans.et = public/language/et/admin/settings/social.json +trans.gl = public/language/gl/admin/settings/social.json + +[o:nodebb:p:nodebb:r:admin-settings-sockets] +file_filter = public/language//admin/settings/sockets.json +source_file = public/language/en-GB/admin/settings/sockets.json +source_lang = en_GB +type = KEYVALUEJSON +trans.uk = public/language/uk/admin/settings/sockets.json +trans.en_US = public/language/en-US/admin/settings/sockets.json +trans.gl = public/language/gl/admin/settings/sockets.json +trans.ja = public/language/ja/admin/settings/sockets.json +trans.pt_BR = public/language/pt-BR/admin/settings/sockets.json +trans.sc = public/language/sc/admin/settings/sockets.json +trans.tr = public/language/tr/admin/settings/sockets.json +trans.da = public/language/da/admin/settings/sockets.json +trans.lv = public/language/lv/admin/settings/sockets.json +trans.sk = public/language/sk/admin/settings/sockets.json +trans.sr = public/language/sr/admin/settings/sockets.json +trans.vi = public/language/vi/admin/settings/sockets.json +trans.ar = public/language/ar/admin/settings/sockets.json +trans.cs = public/language/cs/admin/settings/sockets.json +trans.de = public/language/de/admin/settings/sockets.json +trans.hy = public/language/hy/admin/settings/sockets.json +trans.id = public/language/id/admin/settings/sockets.json +trans.rw = public/language/rw/admin/settings/sockets.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/sockets.json +trans.fi = public/language/fi/admin/settings/sockets.json +trans.it = public/language/it/admin/settings/sockets.json +trans.ro = public/language/ro/admin/settings/sockets.json +trans.nb = public/language/nb/admin/settings/sockets.json +trans.sq_AL = public/language/sq-AL/admin/settings/sockets.json +trans.bg = public/language/bg/admin/settings/sockets.json +trans.bn = public/language/bn/admin/settings/sockets.json +trans.es = public/language/es/admin/settings/sockets.json +trans.fr = public/language/fr/admin/settings/sockets.json +trans.hr = public/language/hr/admin/settings/sockets.json +trans.ko = public/language/ko/admin/settings/sockets.json +trans.zh_TW = public/language/zh-TW/admin/settings/sockets.json +trans.el = public/language/el/admin/settings/sockets.json +trans.he = public/language/he/admin/settings/sockets.json +trans.ms = public/language/ms/admin/settings/sockets.json +trans.sv = public/language/sv/admin/settings/sockets.json +trans.et = public/language/et/admin/settings/sockets.json +trans.pl = public/language/pl/admin/settings/sockets.json +trans.ru = public/language/ru/admin/settings/sockets.json +trans.zh_CN = public/language/zh-CN/admin/settings/sockets.json +trans.th = public/language/th/admin/settings/sockets.json +trans.fa_IR = public/language/fa-IR/admin/settings/sockets.json +trans.hu = public/language/hu/admin/settings/sockets.json +trans.lt = public/language/lt/admin/settings/sockets.json +trans.nl = public/language/nl/admin/settings/sockets.json +trans.pt_PT = public/language/pt-PT/admin/settings/sockets.json +trans.sl = public/language/sl/admin/settings/sockets.json + +[o:nodebb:p:nodebb:r:admin-settings-sounds] +file_filter = public/language//admin/settings/sounds.json +source_file = public/language/en-GB/admin/settings/sounds.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sv = public/language/sv/admin/settings/sounds.json +trans.de = public/language/de/admin/settings/sounds.json +trans.el = public/language/el/admin/settings/sounds.json +trans.es = public/language/es/admin/settings/sounds.json +trans.he = public/language/he/admin/settings/sounds.json +trans.it = public/language/it/admin/settings/sounds.json +trans.pt_BR = public/language/pt-BR/admin/settings/sounds.json +trans.sq_AL = public/language/sq-AL/admin/settings/sounds.json +trans.vi = public/language/vi/admin/settings/sounds.json +trans.hr = public/language/hr/admin/settings/sounds.json +trans.hu = public/language/hu/admin/settings/sounds.json +trans.bg = public/language/bg/admin/settings/sounds.json +trans.ja = public/language/ja/admin/settings/sounds.json +trans.lt = public/language/lt/admin/settings/sounds.json +trans.nl = public/language/nl/admin/settings/sounds.json +trans.ru = public/language/ru/admin/settings/sounds.json +trans.cs = public/language/cs/admin/settings/sounds.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/sounds.json +trans.fa_IR = public/language/fa-IR/admin/settings/sounds.json +trans.fi = public/language/fi/admin/settings/sounds.json +trans.nb = public/language/nb/admin/settings/sounds.json +trans.th = public/language/th/admin/settings/sounds.json +trans.ar = public/language/ar/admin/settings/sounds.json +trans.da = public/language/da/admin/settings/sounds.json +trans.gl = public/language/gl/admin/settings/sounds.json +trans.lv = public/language/lv/admin/settings/sounds.json +trans.ms = public/language/ms/admin/settings/sounds.json +trans.sc = public/language/sc/admin/settings/sounds.json +trans.sr = public/language/sr/admin/settings/sounds.json +trans.bn = public/language/bn/admin/settings/sounds.json +trans.en_US = public/language/en-US/admin/settings/sounds.json +trans.hy = public/language/hy/admin/settings/sounds.json +trans.sk = public/language/sk/admin/settings/sounds.json +trans.sl = public/language/sl/admin/settings/sounds.json +trans.uk = public/language/uk/admin/settings/sounds.json +trans.zh_CN = public/language/zh-CN/admin/settings/sounds.json +trans.et = public/language/et/admin/settings/sounds.json +trans.fr = public/language/fr/admin/settings/sounds.json +trans.ko = public/language/ko/admin/settings/sounds.json +trans.ro = public/language/ro/admin/settings/sounds.json +trans.zh_TW = public/language/zh-TW/admin/settings/sounds.json +trans.id = public/language/id/admin/settings/sounds.json +trans.pl = public/language/pl/admin/settings/sounds.json +trans.pt_PT = public/language/pt-PT/admin/settings/sounds.json +trans.rw = public/language/rw/admin/settings/sounds.json +trans.tr = public/language/tr/admin/settings/sounds.json + +[o:nodebb:p:nodebb:r:admin-settings-tags] +file_filter = public/language//admin/settings/tags.json +source_file = public/language/en-GB/admin/settings/tags.json +source_lang = en_GB +type = KEYVALUEJSON +trans.bg = public/language/bg/admin/settings/tags.json +trans.cs = public/language/cs/admin/settings/tags.json +trans.de = public/language/de/admin/settings/tags.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/tags.json +trans.lv = public/language/lv/admin/settings/tags.json +trans.th = public/language/th/admin/settings/tags.json +trans.zh_TW = public/language/zh-TW/admin/settings/tags.json +trans.hu = public/language/hu/admin/settings/tags.json +trans.it = public/language/it/admin/settings/tags.json +trans.pt_BR = public/language/pt-BR/admin/settings/tags.json +trans.rw = public/language/rw/admin/settings/tags.json +trans.sk = public/language/sk/admin/settings/tags.json +trans.sv = public/language/sv/admin/settings/tags.json +trans.ar = public/language/ar/admin/settings/tags.json +trans.gl = public/language/gl/admin/settings/tags.json +trans.ja = public/language/ja/admin/settings/tags.json +trans.lt = public/language/lt/admin/settings/tags.json +trans.sc = public/language/sc/admin/settings/tags.json +trans.zh_CN = public/language/zh-CN/admin/settings/tags.json +trans.ro = public/language/ro/admin/settings/tags.json +trans.bn = public/language/bn/admin/settings/tags.json +trans.hr = public/language/hr/admin/settings/tags.json +trans.ko = public/language/ko/admin/settings/tags.json +trans.nb = public/language/nb/admin/settings/tags.json +trans.el = public/language/el/admin/settings/tags.json +trans.et = public/language/et/admin/settings/tags.json +trans.he = public/language/he/admin/settings/tags.json +trans.ms = public/language/ms/admin/settings/tags.json +trans.nl = public/language/nl/admin/settings/tags.json +trans.sl = public/language/sl/admin/settings/tags.json +trans.tr = public/language/tr/admin/settings/tags.json +trans.es = public/language/es/admin/settings/tags.json +trans.fr = public/language/fr/admin/settings/tags.json +trans.hy = public/language/hy/admin/settings/tags.json +trans.pl = public/language/pl/admin/settings/tags.json +trans.ru = public/language/ru/admin/settings/tags.json +trans.sq_AL = public/language/sq-AL/admin/settings/tags.json +trans.sr = public/language/sr/admin/settings/tags.json +trans.uk = public/language/uk/admin/settings/tags.json +trans.da = public/language/da/admin/settings/tags.json +trans.en_US = public/language/en-US/admin/settings/tags.json +trans.fa_IR = public/language/fa-IR/admin/settings/tags.json +trans.fi = public/language/fi/admin/settings/tags.json +trans.id = public/language/id/admin/settings/tags.json +trans.pt_PT = public/language/pt-PT/admin/settings/tags.json +trans.vi = public/language/vi/admin/settings/tags.json + +[o:nodebb:p:nodebb:r:admin-settings-uploads] +file_filter = public/language//admin/settings/uploads.json +source_file = public/language/en-GB/admin/settings/uploads.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ar = public/language/ar/admin/settings/uploads.json +trans.it = public/language/it/admin/settings/uploads.json +trans.ko = public/language/ko/admin/settings/uploads.json +trans.sk = public/language/sk/admin/settings/uploads.json +trans.sq_AL = public/language/sq-AL/admin/settings/uploads.json +trans.fa_IR = public/language/fa-IR/admin/settings/uploads.json +trans.he = public/language/he/admin/settings/uploads.json +trans.ja = public/language/ja/admin/settings/uploads.json +trans.nb = public/language/nb/admin/settings/uploads.json +trans.nl = public/language/nl/admin/settings/uploads.json +trans.zh_TW = public/language/zh-TW/admin/settings/uploads.json +trans.ro = public/language/ro/admin/settings/uploads.json +trans.sl = public/language/sl/admin/settings/uploads.json +trans.da = public/language/da/admin/settings/uploads.json +trans.es = public/language/es/admin/settings/uploads.json +trans.et = public/language/et/admin/settings/uploads.json +trans.hr = public/language/hr/admin/settings/uploads.json +trans.id = public/language/id/admin/settings/uploads.json +trans.pt_BR = public/language/pt-BR/admin/settings/uploads.json +trans.uk = public/language/uk/admin/settings/uploads.json +trans.vi = public/language/vi/admin/settings/uploads.json +trans.th = public/language/th/admin/settings/uploads.json +trans.bg = public/language/bg/admin/settings/uploads.json +trans.el = public/language/el/admin/settings/uploads.json +trans.hu = public/language/hu/admin/settings/uploads.json +trans.lv = public/language/lv/admin/settings/uploads.json +trans.ms = public/language/ms/admin/settings/uploads.json +trans.pt_PT = public/language/pt-PT/admin/settings/uploads.json +trans.lt = public/language/lt/admin/settings/uploads.json +trans.bn = public/language/bn/admin/settings/uploads.json +trans.cs = public/language/cs/admin/settings/uploads.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/uploads.json +trans.fi = public/language/fi/admin/settings/uploads.json +trans.fr = public/language/fr/admin/settings/uploads.json +trans.hy = public/language/hy/admin/settings/uploads.json +trans.gl = public/language/gl/admin/settings/uploads.json +trans.ru = public/language/ru/admin/settings/uploads.json +trans.sr = public/language/sr/admin/settings/uploads.json +trans.sv = public/language/sv/admin/settings/uploads.json +trans.zh_CN = public/language/zh-CN/admin/settings/uploads.json +trans.de = public/language/de/admin/settings/uploads.json +trans.en_US = public/language/en-US/admin/settings/uploads.json +trans.pl = public/language/pl/admin/settings/uploads.json +trans.rw = public/language/rw/admin/settings/uploads.json +trans.sc = public/language/sc/admin/settings/uploads.json +trans.tr = public/language/tr/admin/settings/uploads.json + +[o:nodebb:p:nodebb:r:admin-settings-user] +file_filter = public/language//admin/settings/user.json +source_file = public/language/en-GB/admin/settings/user.json +source_lang = en_GB +type = KEYVALUEJSON +trans.da = public/language/da/admin/settings/user.json +trans.el = public/language/el/admin/settings/user.json +trans.he = public/language/he/admin/settings/user.json +trans.ro = public/language/ro/admin/settings/user.json +trans.sq_AL = public/language/sq-AL/admin/settings/user.json +trans.gl = public/language/gl/admin/settings/user.json +trans.hu = public/language/hu/admin/settings/user.json +trans.nb = public/language/nb/admin/settings/user.json +trans.sv = public/language/sv/admin/settings/user.json +trans.uk = public/language/uk/admin/settings/user.json +trans.cs = public/language/cs/admin/settings/user.json +trans.en_US = public/language/en-US/admin/settings/user.json +trans.fa_IR = public/language/fa-IR/admin/settings/user.json +trans.fi = public/language/fi/admin/settings/user.json +trans.lv = public/language/lv/admin/settings/user.json +trans.ms = public/language/ms/admin/settings/user.json +trans.pl = public/language/pl/admin/settings/user.json +trans.ru = public/language/ru/admin/settings/user.json +trans.zh_CN = public/language/zh-CN/admin/settings/user.json +trans.hr = public/language/hr/admin/settings/user.json +trans.id = public/language/id/admin/settings/user.json +trans.it = public/language/it/admin/settings/user.json +trans.sk = public/language/sk/admin/settings/user.json +trans.sl = public/language/sl/admin/settings/user.json +trans.sr = public/language/sr/admin/settings/user.json +trans.tr = public/language/tr/admin/settings/user.json +trans.vi = public/language/vi/admin/settings/user.json +trans.de = public/language/de/admin/settings/user.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/user.json +trans.hy = public/language/hy/admin/settings/user.json +trans.pt_PT = public/language/pt-PT/admin/settings/user.json +trans.th = public/language/th/admin/settings/user.json +trans.zh_TW = public/language/zh-TW/admin/settings/user.json +trans.bn = public/language/bn/admin/settings/user.json +trans.nl = public/language/nl/admin/settings/user.json +trans.pt_BR = public/language/pt-BR/admin/settings/user.json +trans.rw = public/language/rw/admin/settings/user.json +trans.bg = public/language/bg/admin/settings/user.json +trans.fr = public/language/fr/admin/settings/user.json +trans.ko = public/language/ko/admin/settings/user.json +trans.ar = public/language/ar/admin/settings/user.json +trans.es = public/language/es/admin/settings/user.json +trans.et = public/language/et/admin/settings/user.json +trans.ja = public/language/ja/admin/settings/user.json +trans.lt = public/language/lt/admin/settings/user.json +trans.sc = public/language/sc/admin/settings/user.json + +[o:nodebb:p:nodebb:r:admin-settings-web-crawler] +file_filter = public/language//admin/settings/web-crawler.json +source_file = public/language/en-GB/admin/settings/web-crawler.json +source_lang = en_GB +type = KEYVALUEJSON +trans.cs = public/language/cs/admin/settings/web-crawler.json +trans.es = public/language/es/admin/settings/web-crawler.json +trans.fr = public/language/fr/admin/settings/web-crawler.json +trans.ko = public/language/ko/admin/settings/web-crawler.json +trans.pl = public/language/pl/admin/settings/web-crawler.json +trans.tr = public/language/tr/admin/settings/web-crawler.json +trans.uk = public/language/uk/admin/settings/web-crawler.json +trans.bg = public/language/bg/admin/settings/web-crawler.json +trans.el = public/language/el/admin/settings/web-crawler.json +trans.en@pirate = public/language/en-x-pirate/admin/settings/web-crawler.json +trans.fa_IR = public/language/fa-IR/admin/settings/web-crawler.json +trans.ms = public/language/ms/admin/settings/web-crawler.json +trans.th = public/language/th/admin/settings/web-crawler.json +trans.gl = public/language/gl/admin/settings/web-crawler.json +trans.id = public/language/id/admin/settings/web-crawler.json +trans.sk = public/language/sk/admin/settings/web-crawler.json +trans.rw = public/language/rw/admin/settings/web-crawler.json +trans.da = public/language/da/admin/settings/web-crawler.json +trans.hr = public/language/hr/admin/settings/web-crawler.json +trans.hu = public/language/hu/admin/settings/web-crawler.json +trans.ja = public/language/ja/admin/settings/web-crawler.json +trans.lv = public/language/lv/admin/settings/web-crawler.json +trans.nl = public/language/nl/admin/settings/web-crawler.json +trans.pt_BR = public/language/pt-BR/admin/settings/web-crawler.json +trans.vi = public/language/vi/admin/settings/web-crawler.json +trans.en_US = public/language/en-US/admin/settings/web-crawler.json +trans.et = public/language/et/admin/settings/web-crawler.json +trans.fi = public/language/fi/admin/settings/web-crawler.json +trans.sq_AL = public/language/sq-AL/admin/settings/web-crawler.json +trans.sv = public/language/sv/admin/settings/web-crawler.json +trans.ar = public/language/ar/admin/settings/web-crawler.json +trans.de = public/language/de/admin/settings/web-crawler.json +trans.hy = public/language/hy/admin/settings/web-crawler.json +trans.nb = public/language/nb/admin/settings/web-crawler.json +trans.ro = public/language/ro/admin/settings/web-crawler.json +trans.sr = public/language/sr/admin/settings/web-crawler.json +trans.zh_TW = public/language/zh-TW/admin/settings/web-crawler.json +trans.he = public/language/he/admin/settings/web-crawler.json +trans.it = public/language/it/admin/settings/web-crawler.json +trans.lt = public/language/lt/admin/settings/web-crawler.json +trans.ru = public/language/ru/admin/settings/web-crawler.json +trans.sl = public/language/sl/admin/settings/web-crawler.json +trans.zh_CN = public/language/zh-CN/admin/settings/web-crawler.json +trans.bn = public/language/bn/admin/settings/web-crawler.json +trans.pt_PT = public/language/pt-PT/admin/settings/web-crawler.json +trans.sc = public/language/sc/admin/settings/web-crawler.json + +[o:nodebb:p:nodebb:r:category] +file_filter = public/language//category.json +source_file = public/language/en-GB/category.json +source_lang = en_GB +type = KEYVALUEJSON +trans.th = public/language/th/category.json +trans.bg = public/language/bg/category.json +trans.hr = public/language/hr/category.json +trans.hy = public/language/hy/category.json +trans.sk = public/language/sk/category.json +trans.sl = public/language/sl/category.json +trans.sq_AL = public/language/sq-AL/category.json +trans.sv = public/language/sv/category.json +trans.vi = public/language/vi/category.json +trans.da = public/language/da/category.json +trans.en_US = public/language/en-US/category.json +trans.gl = public/language/gl/category.json +trans.ko = public/language/ko/category.json +trans.lt = public/language/lt/category.json +trans.pt_BR = public/language/pt-BR/category.json +trans.hu = public/language/hu/category.json +trans.lv = public/language/lv/category.json +trans.ro = public/language/ro/category.json +trans.ru = public/language/ru/category.json +trans.sr = public/language/sr/category.json +trans.bn = public/language/bn/category.json +trans.he = public/language/he/category.json +trans.nl = public/language/nl/category.json +trans.sc = public/language/sc/category.json +trans.zh_CN = public/language/zh-CN/category.json +trans.fi = public/language/fi/category.json +trans.ja = public/language/ja/category.json +trans.nb = public/language/nb/category.json +trans.pl = public/language/pl/category.json +trans.zh_TW = public/language/zh-TW/category.json +trans.ar = public/language/ar/category.json +trans.cs = public/language/cs/category.json +trans.fa_IR = public/language/fa-IR/category.json +trans.ms = public/language/ms/category.json +trans.pt_PT = public/language/pt-PT/category.json +trans.tr = public/language/tr/category.json +trans.en@pirate = public/language/en-x-pirate/category.json +trans.fr = public/language/fr/category.json +trans.id = public/language/id/category.json +trans.uk = public/language/uk/category.json +trans.de = public/language/de/category.json +trans.el = public/language/el/category.json +trans.es = public/language/es/category.json +trans.et = public/language/et/category.json +trans.it = public/language/it/category.json +trans.rw = public/language/rw/category.json + +[o:nodebb:p:nodebb:r:email] +file_filter = public/language//email.json +source_file = public/language/en-GB/email.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sk = public/language/sk/email.json +trans.zh_TW = public/language/zh-TW/email.json +trans.el = public/language/el/email.json +trans.fa_IR = public/language/fa-IR/email.json +trans.fi = public/language/fi/email.json +trans.id = public/language/id/email.json +trans.ko = public/language/ko/email.json +trans.en@pirate = public/language/en-x-pirate/email.json +trans.it = public/language/it/email.json +trans.lv = public/language/lv/email.json +trans.uk = public/language/uk/email.json +trans.zh_CN = public/language/zh-CN/email.json +trans.sv = public/language/sv/email.json +trans.bn = public/language/bn/email.json +trans.cs = public/language/cs/email.json +trans.ms = public/language/ms/email.json +trans.ro = public/language/ro/email.json +trans.ru = public/language/ru/email.json +trans.de = public/language/de/email.json +trans.hu = public/language/hu/email.json +trans.hy = public/language/hy/email.json +trans.sl = public/language/sl/email.json +trans.th = public/language/th/email.json +trans.ar = public/language/ar/email.json +trans.es = public/language/es/email.json +trans.gl = public/language/gl/email.json +trans.he = public/language/he/email.json +trans.ja = public/language/ja/email.json +trans.bg = public/language/bg/email.json +trans.et = public/language/et/email.json +trans.hr = public/language/hr/email.json +trans.pl = public/language/pl/email.json +trans.da = public/language/da/email.json +trans.fr = public/language/fr/email.json +trans.lt = public/language/lt/email.json +trans.pt_PT = public/language/pt-PT/email.json +trans.sc = public/language/sc/email.json +trans.sq_AL = public/language/sq-AL/email.json +trans.sr = public/language/sr/email.json +trans.tr = public/language/tr/email.json +trans.en_US = public/language/en-US/email.json +trans.nb = public/language/nb/email.json +trans.nl = public/language/nl/email.json +trans.pt_BR = public/language/pt-BR/email.json +trans.rw = public/language/rw/email.json +trans.vi = public/language/vi/email.json + +[o:nodebb:p:nodebb:r:error] +file_filter = public/language//error.json +source_file = public/language/en-GB/error.json +source_lang = en_GB +type = KEYVALUEJSON +trans.de = public/language/de/error.json +trans.ko = public/language/ko/error.json +trans.lv = public/language/lv/error.json +trans.sk = public/language/sk/error.json +trans.cs = public/language/cs/error.json +trans.it = public/language/it/error.json +trans.nl = public/language/nl/error.json +trans.sc = public/language/sc/error.json +trans.sl = public/language/sl/error.json +trans.sq_AL = public/language/sq-AL/error.json +trans.tr = public/language/tr/error.json +trans.hu = public/language/hu/error.json +trans.fr = public/language/fr/error.json +trans.hy = public/language/hy/error.json +trans.zh_CN = public/language/zh-CN/error.json +trans.ar = public/language/ar/error.json +trans.et = public/language/et/error.json +trans.fi = public/language/fi/error.json +trans.he = public/language/he/error.json +trans.hr = public/language/hr/error.json +trans.lt = public/language/lt/error.json +trans.ru = public/language/ru/error.json +trans.rw = public/language/rw/error.json +trans.bn = public/language/bn/error.json +trans.vi = public/language/vi/error.json +trans.fa_IR = public/language/fa-IR/error.json +trans.gl = public/language/gl/error.json +trans.nb = public/language/nb/error.json +trans.pl = public/language/pl/error.json +trans.ro = public/language/ro/error.json +trans.uk = public/language/uk/error.json +trans.es = public/language/es/error.json +trans.el = public/language/el/error.json +trans.en@pirate = public/language/en-x-pirate/error.json +trans.en_US = public/language/en-US/error.json +trans.ms = public/language/ms/error.json +trans.pt_PT = public/language/pt-PT/error.json +trans.bg = public/language/bg/error.json +trans.pt_BR = public/language/pt-BR/error.json +trans.sv = public/language/sv/error.json +trans.zh_TW = public/language/zh-TW/error.json +trans.da = public/language/da/error.json +trans.ja = public/language/ja/error.json +trans.sr = public/language/sr/error.json +trans.th = public/language/th/error.json +trans.id = public/language/id/error.json + +[o:nodebb:p:nodebb:r:flags] +file_filter = public/language//flags.json +source_file = public/language/en-GB/flags.json +source_lang = en_GB +type = KEYVALUEJSON +trans.nb = public/language/nb/flags.json +trans.ru = public/language/ru/flags.json +trans.sc = public/language/sc/flags.json +trans.fi = public/language/fi/flags.json +trans.hr = public/language/hr/flags.json +trans.lv = public/language/lv/flags.json +trans.ms = public/language/ms/flags.json +trans.ja = public/language/ja/flags.json +trans.nl = public/language/nl/flags.json +trans.pt_PT = public/language/pt-PT/flags.json +trans.th = public/language/th/flags.json +trans.el = public/language/el/flags.json +trans.en_US = public/language/en-US/flags.json +trans.gl = public/language/gl/flags.json +trans.hy = public/language/hy/flags.json +trans.vi = public/language/vi/flags.json +trans.rw = public/language/rw/flags.json +trans.sr = public/language/sr/flags.json +trans.zh_TW = public/language/zh-TW/flags.json +trans.sq_AL = public/language/sq-AL/flags.json +trans.sv = public/language/sv/flags.json +trans.fa_IR = public/language/fa-IR/flags.json +trans.id = public/language/id/flags.json +trans.ko = public/language/ko/flags.json +trans.pl = public/language/pl/flags.json +trans.de = public/language/de/flags.json +trans.en@pirate = public/language/en-x-pirate/flags.json +trans.he = public/language/he/flags.json +trans.zh_CN = public/language/zh-CN/flags.json +trans.lt = public/language/lt/flags.json +trans.da = public/language/da/flags.json +trans.es = public/language/es/flags.json +trans.et = public/language/et/flags.json +trans.hu = public/language/hu/flags.json +trans.tr = public/language/tr/flags.json +trans.bn = public/language/bn/flags.json +trans.it = public/language/it/flags.json +trans.pt_BR = public/language/pt-BR/flags.json +trans.sk = public/language/sk/flags.json +trans.ro = public/language/ro/flags.json +trans.sl = public/language/sl/flags.json +trans.ar = public/language/ar/flags.json +trans.bg = public/language/bg/flags.json +trans.cs = public/language/cs/flags.json +trans.fr = public/language/fr/flags.json + +[o:nodebb:p:nodebb:r:global] +file_filter = public/language//global.json +source_file = public/language/en-GB/global.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ms = public/language/ms/global.json +trans.pl = public/language/pl/global.json +trans.ru = public/language/ru/global.json +trans.bn = public/language/bn/global.json +trans.fr = public/language/fr/global.json +trans.hr = public/language/hr/global.json +trans.hy = public/language/hy/global.json +trans.lv = public/language/lv/global.json +trans.fa_IR = public/language/fa-IR/global.json +trans.sl = public/language/sl/global.json +trans.uk = public/language/uk/global.json +trans.sc = public/language/sc/global.json +trans.sv = public/language/sv/global.json +trans.th = public/language/th/global.json +trans.zh_TW = public/language/zh-TW/global.json +trans.hu = public/language/hu/global.json +trans.sk = public/language/sk/global.json +trans.sr = public/language/sr/global.json +trans.tr = public/language/tr/global.json +trans.nb = public/language/nb/global.json +trans.pt_BR = public/language/pt-BR/global.json +trans.pt_PT = public/language/pt-PT/global.json +trans.bg = public/language/bg/global.json +trans.da = public/language/da/global.json +trans.fi = public/language/fi/global.json +trans.id = public/language/id/global.json +trans.lt = public/language/lt/global.json +trans.ro = public/language/ro/global.json +trans.de = public/language/de/global.json +trans.el = public/language/el/global.json +trans.vi = public/language/vi/global.json +trans.zh_CN = public/language/zh-CN/global.json +trans.en_US = public/language/en-US/global.json +trans.et = public/language/et/global.json +trans.gl = public/language/gl/global.json +trans.he = public/language/he/global.json +trans.ko = public/language/ko/global.json +trans.ja = public/language/ja/global.json +trans.nl = public/language/nl/global.json +trans.rw = public/language/rw/global.json +trans.ar = public/language/ar/global.json +trans.cs = public/language/cs/global.json +trans.en@pirate = public/language/en-x-pirate/global.json +trans.es = public/language/es/global.json +trans.it = public/language/it/global.json +trans.sq_AL = public/language/sq-AL/global.json + +[o:nodebb:p:nodebb:r:groups] +file_filter = public/language//groups.json +source_file = public/language/en-GB/groups.json +source_lang = en_GB +type = KEYVALUEJSON +trans.zh_TW = public/language/zh-TW/groups.json +trans.de = public/language/de/groups.json +trans.es = public/language/es/groups.json +trans.sc = public/language/sc/groups.json +trans.sr = public/language/sr/groups.json +trans.da = public/language/da/groups.json +trans.en@pirate = public/language/en-x-pirate/groups.json +trans.hy = public/language/hy/groups.json +trans.pt_PT = public/language/pt-PT/groups.json +trans.ms = public/language/ms/groups.json +trans.nb = public/language/nb/groups.json +trans.ro = public/language/ro/groups.json +trans.vi = public/language/vi/groups.json +trans.fa_IR = public/language/fa-IR/groups.json +trans.he = public/language/he/groups.json +trans.hr = public/language/hr/groups.json +trans.lv = public/language/lv/groups.json +trans.bg = public/language/bg/groups.json +trans.bn = public/language/bn/groups.json +trans.ja = public/language/ja/groups.json +trans.tr = public/language/tr/groups.json +trans.zh_CN = public/language/zh-CN/groups.json +trans.ar = public/language/ar/groups.json +trans.cs = public/language/cs/groups.json +trans.it = public/language/it/groups.json +trans.sk = public/language/sk/groups.json +trans.fi = public/language/fi/groups.json +trans.fr = public/language/fr/groups.json +trans.sq_AL = public/language/sq-AL/groups.json +trans.th = public/language/th/groups.json +trans.id = public/language/id/groups.json +trans.sl = public/language/sl/groups.json +trans.sv = public/language/sv/groups.json +trans.uk = public/language/uk/groups.json +trans.el = public/language/el/groups.json +trans.et = public/language/et/groups.json +trans.gl = public/language/gl/groups.json +trans.hu = public/language/hu/groups.json +trans.pl = public/language/pl/groups.json +trans.pt_BR = public/language/pt-BR/groups.json +trans.ru = public/language/ru/groups.json +trans.rw = public/language/rw/groups.json +trans.en_US = public/language/en-US/groups.json +trans.ko = public/language/ko/groups.json +trans.lt = public/language/lt/groups.json +trans.nl = public/language/nl/groups.json + +[o:nodebb:p:nodebb:r:ip-blacklist] +file_filter = public/language//ip-blacklist.json +source_file = public/language/en-GB/ip-blacklist.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sr = public/language/sr/ip-blacklist.json +trans.et = public/language/et/ip-blacklist.json +trans.fi = public/language/fi/ip-blacklist.json +trans.hr = public/language/hr/ip-blacklist.json +trans.hu = public/language/hu/ip-blacklist.json +trans.ru = public/language/ru/ip-blacklist.json +trans.sk = public/language/sk/ip-blacklist.json +trans.sq_AL = public/language/sq-AL/ip-blacklist.json +trans.zh_CN = public/language/zh-CN/ip-blacklist.json +trans.da = public/language/da/ip-blacklist.json +trans.en@pirate = public/language/en-x-pirate/ip-blacklist.json +trans.he = public/language/he/ip-blacklist.json +trans.hy = public/language/hy/ip-blacklist.json +trans.lv = public/language/lv/ip-blacklist.json +trans.pt_BR = public/language/pt-BR/ip-blacklist.json +trans.sc = public/language/sc/ip-blacklist.json +trans.ar = public/language/ar/ip-blacklist.json +trans.fa_IR = public/language/fa-IR/ip-blacklist.json +trans.fr = public/language/fr/ip-blacklist.json +trans.gl = public/language/gl/ip-blacklist.json +trans.nb = public/language/nb/ip-blacklist.json +trans.sv = public/language/sv/ip-blacklist.json +trans.th = public/language/th/ip-blacklist.json +trans.bn = public/language/bn/ip-blacklist.json +trans.en_US = public/language/en-US/ip-blacklist.json +trans.ja = public/language/ja/ip-blacklist.json +trans.ms = public/language/ms/ip-blacklist.json +trans.nl = public/language/nl/ip-blacklist.json +trans.pt_PT = public/language/pt-PT/ip-blacklist.json +trans.ro = public/language/ro/ip-blacklist.json +trans.es = public/language/es/ip-blacklist.json +trans.id = public/language/id/ip-blacklist.json +trans.it = public/language/it/ip-blacklist.json +trans.uk = public/language/uk/ip-blacklist.json +trans.vi = public/language/vi/ip-blacklist.json +trans.zh_TW = public/language/zh-TW/ip-blacklist.json +trans.bg = public/language/bg/ip-blacklist.json +trans.de = public/language/de/ip-blacklist.json +trans.sl = public/language/sl/ip-blacklist.json +trans.pl = public/language/pl/ip-blacklist.json +trans.rw = public/language/rw/ip-blacklist.json +trans.cs = public/language/cs/ip-blacklist.json +trans.el = public/language/el/ip-blacklist.json +trans.ko = public/language/ko/ip-blacklist.json +trans.lt = public/language/lt/ip-blacklist.json +trans.tr = public/language/tr/ip-blacklist.json + +[o:nodebb:p:nodebb:r:language-1] +file_filter = public/language//language.json +source_file = public/language/en-GB/language.json +source_lang = en_GB +type = KEYVALUEJSON +trans.lt = public/language/lt/language.json +trans.vi = public/language/vi/language.json +trans.sr = public/language/sr/language.json +trans.tr = public/language/tr/language.json +trans.uk = public/language/uk/language.json +trans.bg = public/language/bg/language.json +trans.da = public/language/da/language.json +trans.en@pirate = public/language/en-x-pirate/language.json +trans.hr = public/language/hr/language.json +trans.rw = public/language/rw/language.json +trans.en_US = public/language/en-US/language.json +trans.et = public/language/et/language.json +trans.ja = public/language/ja/language.json +trans.nb = public/language/nb/language.json +trans.sk = public/language/sk/language.json +trans.el = public/language/el/language.json +trans.es = public/language/es/language.json +trans.fi = public/language/fi/language.json +trans.it = public/language/it/language.json +trans.pt_PT = public/language/pt-PT/language.json +trans.ro = public/language/ro/language.json +trans.th = public/language/th/language.json +trans.ar = public/language/ar/language.json +trans.bn = public/language/bn/language.json +trans.de = public/language/de/language.json +trans.ko = public/language/ko/language.json +trans.pl = public/language/pl/language.json +trans.gl = public/language/gl/language.json +trans.sq_AL = public/language/sq-AL/language.json +trans.zh_TW = public/language/zh-TW/language.json +trans.nl = public/language/nl/language.json +trans.ru = public/language/ru/language.json +trans.sc = public/language/sc/language.json +trans.cs = public/language/cs/language.json +trans.fr = public/language/fr/language.json +trans.he = public/language/he/language.json +trans.id = public/language/id/language.json +trans.lv = public/language/lv/language.json +trans.sl = public/language/sl/language.json +trans.sv = public/language/sv/language.json +trans.zh_CN = public/language/zh-CN/language.json +trans.fa_IR = public/language/fa-IR/language.json +trans.hu = public/language/hu/language.json +trans.hy = public/language/hy/language.json +trans.ms = public/language/ms/language.json +trans.pt_BR = public/language/pt-BR/language.json + +[o:nodebb:p:nodebb:r:login] +file_filter = public/language//login.json +source_file = public/language/en-GB/login.json +source_lang = en_GB +type = KEYVALUEJSON +trans.bn = public/language/bn/login.json +trans.cs = public/language/cs/login.json +trans.el = public/language/el/login.json +trans.ja = public/language/ja/login.json +trans.pt_BR = public/language/pt-BR/login.json +trans.ro = public/language/ro/login.json +trans.zh_TW = public/language/zh-TW/login.json +trans.bg = public/language/bg/login.json +trans.pt_PT = public/language/pt-PT/login.json +trans.sr = public/language/sr/login.json +trans.vi = public/language/vi/login.json +trans.zh_CN = public/language/zh-CN/login.json +trans.pl = public/language/pl/login.json +trans.gl = public/language/gl/login.json +trans.ko = public/language/ko/login.json +trans.lv = public/language/lv/login.json +trans.sl = public/language/sl/login.json +trans.sq_AL = public/language/sq-AL/login.json +trans.sv = public/language/sv/login.json +trans.tr = public/language/tr/login.json +trans.es = public/language/es/login.json +trans.sk = public/language/sk/login.json +trans.uk = public/language/uk/login.json +trans.it = public/language/it/login.json +trans.da = public/language/da/login.json +trans.fa_IR = public/language/fa-IR/login.json +trans.fi = public/language/fi/login.json +trans.fr = public/language/fr/login.json +trans.he = public/language/he/login.json +trans.hr = public/language/hr/login.json +trans.ar = public/language/ar/login.json +trans.en_US = public/language/en-US/login.json +trans.hu = public/language/hu/login.json +trans.ms = public/language/ms/login.json +trans.en@pirate = public/language/en-x-pirate/login.json +trans.hy = public/language/hy/login.json +trans.id = public/language/id/login.json +trans.nb = public/language/nb/login.json +trans.ru = public/language/ru/login.json +trans.rw = public/language/rw/login.json +trans.th = public/language/th/login.json +trans.et = public/language/et/login.json +trans.lt = public/language/lt/login.json +trans.nl = public/language/nl/login.json +trans.sc = public/language/sc/login.json +trans.de = public/language/de/login.json + +[o:nodebb:p:nodebb:r:modules] +file_filter = public/language//modules.json +source_file = public/language/en-GB/modules.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sl = public/language/sl/modules.json +trans.sq_AL = public/language/sq-AL/modules.json +trans.da = public/language/da/modules.json +trans.et = public/language/et/modules.json +trans.fr = public/language/fr/modules.json +trans.lt = public/language/lt/modules.json +trans.ms = public/language/ms/modules.json +trans.sk = public/language/sk/modules.json +trans.vi = public/language/vi/modules.json +trans.fa_IR = public/language/fa-IR/modules.json +trans.hr = public/language/hr/modules.json +trans.lv = public/language/lv/modules.json +trans.nb = public/language/nb/modules.json +trans.ro = public/language/ro/modules.json +trans.sv = public/language/sv/modules.json +trans.en@pirate = public/language/en-x-pirate/modules.json +trans.tr = public/language/tr/modules.json +trans.cs = public/language/cs/modules.json +trans.de = public/language/de/modules.json +trans.fi = public/language/fi/modules.json +trans.he = public/language/he/modules.json +trans.hy = public/language/hy/modules.json +trans.sr = public/language/sr/modules.json +trans.el = public/language/el/modules.json +trans.hu = public/language/hu/modules.json +trans.ko = public/language/ko/modules.json +trans.es = public/language/es/modules.json +trans.id = public/language/id/modules.json +trans.nl = public/language/nl/modules.json +trans.sc = public/language/sc/modules.json +trans.th = public/language/th/modules.json +trans.zh_TW = public/language/zh-TW/modules.json +trans.bg = public/language/bg/modules.json +trans.bn = public/language/bn/modules.json +trans.en_US = public/language/en-US/modules.json +trans.it = public/language/it/modules.json +trans.pl = public/language/pl/modules.json +trans.uk = public/language/uk/modules.json +trans.rw = public/language/rw/modules.json +trans.zh_CN = public/language/zh-CN/modules.json +trans.ar = public/language/ar/modules.json +trans.gl = public/language/gl/modules.json +trans.ja = public/language/ja/modules.json +trans.pt_BR = public/language/pt-BR/modules.json +trans.pt_PT = public/language/pt-PT/modules.json +trans.ru = public/language/ru/modules.json + +[o:nodebb:p:nodebb:r:notifications] +file_filter = public/language//notifications.json +source_file = public/language/en-GB/notifications.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ru = public/language/ru/notifications.json +trans.sq_AL = public/language/sq-AL/notifications.json +trans.el = public/language/el/notifications.json +trans.en_US = public/language/en-US/notifications.json +trans.ja = public/language/ja/notifications.json +trans.pl = public/language/pl/notifications.json +trans.sr = public/language/sr/notifications.json +trans.th = public/language/th/notifications.json +trans.tr = public/language/tr/notifications.json +trans.zh_CN = public/language/zh-CN/notifications.json +trans.cs = public/language/cs/notifications.json +trans.de = public/language/de/notifications.json +trans.fa_IR = public/language/fa-IR/notifications.json +trans.sl = public/language/sl/notifications.json +trans.sc = public/language/sc/notifications.json +trans.uk = public/language/uk/notifications.json +trans.bn = public/language/bn/notifications.json +trans.fi = public/language/fi/notifications.json +trans.lt = public/language/lt/notifications.json +trans.pt_PT = public/language/pt-PT/notifications.json +trans.es = public/language/es/notifications.json +trans.gl = public/language/gl/notifications.json +trans.sv = public/language/sv/notifications.json +trans.zh_TW = public/language/zh-TW/notifications.json +trans.pt_BR = public/language/pt-BR/notifications.json +trans.rw = public/language/rw/notifications.json +trans.lv = public/language/lv/notifications.json +trans.bg = public/language/bg/notifications.json +trans.he = public/language/he/notifications.json +trans.hu = public/language/hu/notifications.json +trans.it = public/language/it/notifications.json +trans.nl = public/language/nl/notifications.json +trans.ro = public/language/ro/notifications.json +trans.sk = public/language/sk/notifications.json +trans.vi = public/language/vi/notifications.json +trans.et = public/language/et/notifications.json +trans.hy = public/language/hy/notifications.json +trans.ko = public/language/ko/notifications.json +trans.ms = public/language/ms/notifications.json +trans.hr = public/language/hr/notifications.json +trans.id = public/language/id/notifications.json +trans.nb = public/language/nb/notifications.json +trans.ar = public/language/ar/notifications.json +trans.da = public/language/da/notifications.json +trans.en@pirate = public/language/en-x-pirate/notifications.json +trans.fr = public/language/fr/notifications.json + +[o:nodebb:p:nodebb:r:pages] +file_filter = public/language//pages.json +source_file = public/language/en-GB/pages.json +source_lang = en_GB +type = KEYVALUEJSON +trans.he = public/language/he/pages.json +trans.nb = public/language/nb/pages.json +trans.sv = public/language/sv/pages.json +trans.pt_BR = public/language/pt-BR/pages.json +trans.tr = public/language/tr/pages.json +trans.zh_TW = public/language/zh-TW/pages.json +trans.bn = public/language/bn/pages.json +trans.gl = public/language/gl/pages.json +trans.hr = public/language/hr/pages.json +trans.id = public/language/id/pages.json +trans.lt = public/language/lt/pages.json +trans.zh_CN = public/language/zh-CN/pages.json +trans.bg = public/language/bg/pages.json +trans.et = public/language/et/pages.json +trans.it = public/language/it/pages.json +trans.ro = public/language/ro/pages.json +trans.sl = public/language/sl/pages.json +trans.sk = public/language/sk/pages.json +trans.sr = public/language/sr/pages.json +trans.uk = public/language/uk/pages.json +trans.de = public/language/de/pages.json +trans.en@pirate = public/language/en-x-pirate/pages.json +trans.en_US = public/language/en-US/pages.json +trans.pt_PT = public/language/pt-PT/pages.json +trans.rw = public/language/rw/pages.json +trans.th = public/language/th/pages.json +trans.vi = public/language/vi/pages.json +trans.hu = public/language/hu/pages.json +trans.hy = public/language/hy/pages.json +trans.nl = public/language/nl/pages.json +trans.ru = public/language/ru/pages.json +trans.sc = public/language/sc/pages.json +trans.cs = public/language/cs/pages.json +trans.fi = public/language/fi/pages.json +trans.ko = public/language/ko/pages.json +trans.lv = public/language/lv/pages.json +trans.sq_AL = public/language/sq-AL/pages.json +trans.ar = public/language/ar/pages.json +trans.da = public/language/da/pages.json +trans.es = public/language/es/pages.json +trans.pl = public/language/pl/pages.json +trans.el = public/language/el/pages.json +trans.fa_IR = public/language/fa-IR/pages.json +trans.fr = public/language/fr/pages.json +trans.ja = public/language/ja/pages.json +trans.ms = public/language/ms/pages.json + +[o:nodebb:p:nodebb:r:post-queue] +file_filter = public/language//post-queue.json +source_file = public/language/en-GB/post-queue.json +source_lang = en_GB +type = KEYVALUEJSON +trans.it = public/language/it/post-queue.json +trans.ko = public/language/ko/post-queue.json +trans.lv = public/language/lv/post-queue.json +trans.nl = public/language/nl/post-queue.json +trans.rw = public/language/rw/post-queue.json +trans.da = public/language/da/post-queue.json +trans.de = public/language/de/post-queue.json +trans.el = public/language/el/post-queue.json +trans.sk = public/language/sk/post-queue.json +trans.sc = public/language/sc/post-queue.json +trans.sr = public/language/sr/post-queue.json +trans.tr = public/language/tr/post-queue.json +trans.uk = public/language/uk/post-queue.json +trans.ja = public/language/ja/post-queue.json +trans.ms = public/language/ms/post-queue.json +trans.ru = public/language/ru/post-queue.json +trans.fr = public/language/fr/post-queue.json +trans.hu = public/language/hu/post-queue.json +trans.lt = public/language/lt/post-queue.json +trans.pl = public/language/pl/post-queue.json +trans.ro = public/language/ro/post-queue.json +trans.bn = public/language/bn/post-queue.json +trans.en@pirate = public/language/en-x-pirate/post-queue.json +trans.fa_IR = public/language/fa-IR/post-queue.json +trans.sl = public/language/sl/post-queue.json +trans.vi = public/language/vi/post-queue.json +trans.zh_CN = public/language/zh-CN/post-queue.json +trans.id = public/language/id/post-queue.json +trans.en_US = public/language/en-US/post-queue.json +trans.sq_AL = public/language/sq-AL/post-queue.json +trans.sv = public/language/sv/post-queue.json +trans.zh_TW = public/language/zh-TW/post-queue.json +trans.ar = public/language/ar/post-queue.json +trans.bg = public/language/bg/post-queue.json +trans.cs = public/language/cs/post-queue.json +trans.hr = public/language/hr/post-queue.json +trans.pt_BR = public/language/pt-BR/post-queue.json +trans.th = public/language/th/post-queue.json +trans.es = public/language/es/post-queue.json +trans.et = public/language/et/post-queue.json +trans.gl = public/language/gl/post-queue.json +trans.hy = public/language/hy/post-queue.json +trans.pt_PT = public/language/pt-PT/post-queue.json +trans.fi = public/language/fi/post-queue.json +trans.he = public/language/he/post-queue.json +trans.nb = public/language/nb/post-queue.json + +[o:nodebb:p:nodebb:r:recent] +file_filter = public/language//recent.json +source_file = public/language/en-GB/recent.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sq_AL = public/language/sq-AL/recent.json +trans.sr = public/language/sr/recent.json +trans.sk = public/language/sk/recent.json +trans.uk = public/language/uk/recent.json +trans.bg = public/language/bg/recent.json +trans.cs = public/language/cs/recent.json +trans.da = public/language/da/recent.json +trans.hy = public/language/hy/recent.json +trans.id = public/language/id/recent.json +trans.lv = public/language/lv/recent.json +trans.ar = public/language/ar/recent.json +trans.de = public/language/de/recent.json +trans.es = public/language/es/recent.json +trans.fa_IR = public/language/fa-IR/recent.json +trans.gl = public/language/gl/recent.json +trans.hu = public/language/hu/recent.json +trans.lt = public/language/lt/recent.json +trans.pt_BR = public/language/pt-BR/recent.json +trans.ru = public/language/ru/recent.json +trans.vi = public/language/vi/recent.json +trans.rw = public/language/rw/recent.json +trans.sl = public/language/sl/recent.json +trans.el = public/language/el/recent.json +trans.en_US = public/language/en-US/recent.json +trans.he = public/language/he/recent.json +trans.hr = public/language/hr/recent.json +trans.ro = public/language/ro/recent.json +trans.tr = public/language/tr/recent.json +trans.zh_CN = public/language/zh-CN/recent.json +trans.bn = public/language/bn/recent.json +trans.en@pirate = public/language/en-x-pirate/recent.json +trans.ja = public/language/ja/recent.json +trans.nb = public/language/nb/recent.json +trans.nl = public/language/nl/recent.json +trans.sv = public/language/sv/recent.json +trans.th = public/language/th/recent.json +trans.fr = public/language/fr/recent.json +trans.it = public/language/it/recent.json +trans.ms = public/language/ms/recent.json +trans.pl = public/language/pl/recent.json +trans.pt_PT = public/language/pt-PT/recent.json +trans.et = public/language/et/recent.json +trans.fi = public/language/fi/recent.json +trans.ko = public/language/ko/recent.json +trans.sc = public/language/sc/recent.json +trans.zh_TW = public/language/zh-TW/recent.json + +[o:nodebb:p:nodebb:r:register] +file_filter = public/language//register.json +source_file = public/language/en-GB/register.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sq_AL = public/language/sq-AL/register.json +trans.tr = public/language/tr/register.json +trans.et = public/language/et/register.json +trans.he = public/language/he/register.json +trans.lv = public/language/lv/register.json +trans.nl = public/language/nl/register.json +trans.sc = public/language/sc/register.json +trans.en@pirate = public/language/en-x-pirate/register.json +trans.es = public/language/es/register.json +trans.pl = public/language/pl/register.json +trans.sl = public/language/sl/register.json +trans.uk = public/language/uk/register.json +trans.pt_PT = public/language/pt-PT/register.json +trans.ar = public/language/ar/register.json +trans.bg = public/language/bg/register.json +trans.de = public/language/de/register.json +trans.hr = public/language/hr/register.json +trans.nb = public/language/nb/register.json +trans.gl = public/language/gl/register.json +trans.sk = public/language/sk/register.json +trans.hy = public/language/hy/register.json +trans.ko = public/language/ko/register.json +trans.ms = public/language/ms/register.json +trans.ro = public/language/ro/register.json +trans.rw = public/language/rw/register.json +trans.zh_CN = public/language/zh-CN/register.json +trans.fi = public/language/fi/register.json +trans.it = public/language/it/register.json +trans.ru = public/language/ru/register.json +trans.sr = public/language/sr/register.json +trans.vi = public/language/vi/register.json +trans.sv = public/language/sv/register.json +trans.zh_TW = public/language/zh-TW/register.json +trans.bn = public/language/bn/register.json +trans.cs = public/language/cs/register.json +trans.fa_IR = public/language/fa-IR/register.json +trans.hu = public/language/hu/register.json +trans.lt = public/language/lt/register.json +trans.ja = public/language/ja/register.json +trans.pt_BR = public/language/pt-BR/register.json +trans.th = public/language/th/register.json +trans.da = public/language/da/register.json +trans.el = public/language/el/register.json +trans.en_US = public/language/en-US/register.json +trans.fr = public/language/fr/register.json +trans.id = public/language/id/register.json + +[o:nodebb:p:nodebb:r:reset_password] +file_filter = public/language//reset_password.json +source_file = public/language/en-GB/reset_password.json +source_lang = en_GB +type = KEYVALUEJSON +trans.bg = public/language/bg/reset_password.json +trans.fr = public/language/fr/reset_password.json +trans.hr = public/language/hr/reset_password.json +trans.hy = public/language/hy/reset_password.json +trans.ja = public/language/ja/reset_password.json +trans.pt_PT = public/language/pt-PT/reset_password.json +trans.zh_CN = public/language/zh-CN/reset_password.json +trans.sv = public/language/sv/reset_password.json +trans.de = public/language/de/reset_password.json +trans.fa_IR = public/language/fa-IR/reset_password.json +trans.gl = public/language/gl/reset_password.json +trans.hu = public/language/hu/reset_password.json +trans.id = public/language/id/reset_password.json +trans.rw = public/language/rw/reset_password.json +trans.sc = public/language/sc/reset_password.json +trans.vi = public/language/vi/reset_password.json +trans.bn = public/language/bn/reset_password.json +trans.he = public/language/he/reset_password.json +trans.ro = public/language/ro/reset_password.json +trans.sq_AL = public/language/sq-AL/reset_password.json +trans.ar = public/language/ar/reset_password.json +trans.cs = public/language/cs/reset_password.json +trans.ko = public/language/ko/reset_password.json +trans.ms = public/language/ms/reset_password.json +trans.pt_BR = public/language/pt-BR/reset_password.json +trans.sr = public/language/sr/reset_password.json +trans.tr = public/language/tr/reset_password.json +trans.ru = public/language/ru/reset_password.json +trans.zh_TW = public/language/zh-TW/reset_password.json +trans.da = public/language/da/reset_password.json +trans.en@pirate = public/language/en-x-pirate/reset_password.json +trans.en_US = public/language/en-US/reset_password.json +trans.lt = public/language/lt/reset_password.json +trans.lv = public/language/lv/reset_password.json +trans.nb = public/language/nb/reset_password.json +trans.sk = public/language/sk/reset_password.json +trans.el = public/language/el/reset_password.json +trans.es = public/language/es/reset_password.json +trans.et = public/language/et/reset_password.json +trans.uk = public/language/uk/reset_password.json +trans.fi = public/language/fi/reset_password.json +trans.it = public/language/it/reset_password.json +trans.nl = public/language/nl/reset_password.json +trans.pl = public/language/pl/reset_password.json +trans.sl = public/language/sl/reset_password.json +trans.th = public/language/th/reset_password.json + +[o:nodebb:p:nodebb:r:search] +file_filter = public/language//search.json +source_file = public/language/en-GB/search.json +source_lang = en_GB +type = KEYVALUEJSON +trans.da = public/language/da/search.json +trans.en_US = public/language/en-US/search.json +trans.ms = public/language/ms/search.json +trans.hr = public/language/hr/search.json +trans.hu = public/language/hu/search.json +trans.id = public/language/id/search.json +trans.bg = public/language/bg/search.json +trans.de = public/language/de/search.json +trans.en@pirate = public/language/en-x-pirate/search.json +trans.fa_IR = public/language/fa-IR/search.json +trans.fi = public/language/fi/search.json +trans.sc = public/language/sc/search.json +trans.sk = public/language/sk/search.json +trans.sl = public/language/sl/search.json +trans.sq_AL = public/language/sq-AL/search.json +trans.sr = public/language/sr/search.json +trans.bn = public/language/bn/search.json +trans.ja = public/language/ja/search.json +trans.ko = public/language/ko/search.json +trans.pl = public/language/pl/search.json +trans.ro = public/language/ro/search.json +trans.zh_TW = public/language/zh-TW/search.json +trans.cs = public/language/cs/search.json +trans.he = public/language/he/search.json +trans.nb = public/language/nb/search.json +trans.zh_CN = public/language/zh-CN/search.json +trans.lv = public/language/lv/search.json +trans.rw = public/language/rw/search.json +trans.sv = public/language/sv/search.json +trans.ru = public/language/ru/search.json +trans.tr = public/language/tr/search.json +trans.uk = public/language/uk/search.json +trans.el = public/language/el/search.json +trans.fr = public/language/fr/search.json +trans.gl = public/language/gl/search.json +trans.it = public/language/it/search.json +trans.pt_BR = public/language/pt-BR/search.json +trans.nl = public/language/nl/search.json +trans.pt_PT = public/language/pt-PT/search.json +trans.th = public/language/th/search.json +trans.vi = public/language/vi/search.json +trans.ar = public/language/ar/search.json +trans.es = public/language/es/search.json +trans.et = public/language/et/search.json +trans.hy = public/language/hy/search.json +trans.lt = public/language/lt/search.json + +[o:nodebb:p:nodebb:r:success] +file_filter = public/language//success.json +source_file = public/language/en-GB/success.json +source_lang = en_GB +type = KEYVALUEJSON +trans.pt_BR = public/language/pt-BR/success.json +trans.en@pirate = public/language/en-x-pirate/success.json +trans.es = public/language/es/success.json +trans.he = public/language/he/success.json +trans.hy = public/language/hy/success.json +trans.ja = public/language/ja/success.json +trans.ko = public/language/ko/success.json +trans.pl = public/language/pl/success.json +trans.de = public/language/de/success.json +trans.fa_IR = public/language/fa-IR/success.json +trans.ro = public/language/ro/success.json +trans.en_US = public/language/en-US/success.json +trans.lt = public/language/lt/success.json +trans.ru = public/language/ru/success.json +trans.sq_AL = public/language/sq-AL/success.json +trans.vi = public/language/vi/success.json +trans.bg = public/language/bg/success.json +trans.hr = public/language/hr/success.json +trans.ms = public/language/ms/success.json +trans.th = public/language/th/success.json +trans.zh_CN = public/language/zh-CN/success.json +trans.bn = public/language/bn/success.json +trans.fr = public/language/fr/success.json +trans.hu = public/language/hu/success.json +trans.id = public/language/id/success.json +trans.rw = public/language/rw/success.json +trans.sl = public/language/sl/success.json +trans.zh_TW = public/language/zh-TW/success.json +trans.ar = public/language/ar/success.json +trans.et = public/language/et/success.json +trans.it = public/language/it/success.json +trans.pt_PT = public/language/pt-PT/success.json +trans.sk = public/language/sk/success.json +trans.sv = public/language/sv/success.json +trans.tr = public/language/tr/success.json +trans.cs = public/language/cs/success.json +trans.fi = public/language/fi/success.json +trans.lv = public/language/lv/success.json +trans.nl = public/language/nl/success.json +trans.sc = public/language/sc/success.json +trans.da = public/language/da/success.json +trans.el = public/language/el/success.json +trans.gl = public/language/gl/success.json +trans.nb = public/language/nb/success.json +trans.sr = public/language/sr/success.json +trans.uk = public/language/uk/success.json + +[o:nodebb:p:nodebb:r:tags] +file_filter = public/language//tags.json +source_file = public/language/en-GB/tags.json +source_lang = en_GB +type = KEYVALUEJSON +trans.et = public/language/et/tags.json +trans.nl = public/language/nl/tags.json +trans.pt_BR = public/language/pt-BR/tags.json +trans.uk = public/language/uk/tags.json +trans.el = public/language/el/tags.json +trans.fa_IR = public/language/fa-IR/tags.json +trans.he = public/language/he/tags.json +trans.hr = public/language/hr/tags.json +trans.th = public/language/th/tags.json +trans.sl = public/language/sl/tags.json +trans.bg = public/language/bg/tags.json +trans.en_US = public/language/en-US/tags.json +trans.fi = public/language/fi/tags.json +trans.rw = public/language/rw/tags.json +trans.sc = public/language/sc/tags.json +trans.tr = public/language/tr/tags.json +trans.vi = public/language/vi/tags.json +trans.ar = public/language/ar/tags.json +trans.de = public/language/de/tags.json +trans.es = public/language/es/tags.json +trans.pt_PT = public/language/pt-PT/tags.json +trans.ro = public/language/ro/tags.json +trans.ru = public/language/ru/tags.json +trans.sk = public/language/sk/tags.json +trans.sr = public/language/sr/tags.json +trans.cs = public/language/cs/tags.json +trans.da = public/language/da/tags.json +trans.en@pirate = public/language/en-x-pirate/tags.json +trans.hu = public/language/hu/tags.json +trans.ja = public/language/ja/tags.json +trans.zh_CN = public/language/zh-CN/tags.json +trans.gl = public/language/gl/tags.json +trans.lv = public/language/lv/tags.json +trans.ms = public/language/ms/tags.json +trans.nb = public/language/nb/tags.json +trans.sv = public/language/sv/tags.json +trans.sq_AL = public/language/sq-AL/tags.json +trans.zh_TW = public/language/zh-TW/tags.json +trans.bn = public/language/bn/tags.json +trans.fr = public/language/fr/tags.json +trans.id = public/language/id/tags.json +trans.ko = public/language/ko/tags.json +trans.lt = public/language/lt/tags.json +trans.hy = public/language/hy/tags.json +trans.it = public/language/it/tags.json +trans.pl = public/language/pl/tags.json + +[o:nodebb:p:nodebb:r:top] +file_filter = public/language//top.json +source_file = public/language/en-GB/top.json +source_lang = en_GB +type = KEYVALUEJSON +trans.hy = public/language/hy/top.json +trans.el = public/language/el/top.json +trans.fa_IR = public/language/fa-IR/top.json +trans.nl = public/language/nl/top.json +trans.cs = public/language/cs/top.json +trans.en_US = public/language/en-US/top.json +trans.fi = public/language/fi/top.json +trans.he = public/language/he/top.json +trans.lt = public/language/lt/top.json +trans.ms = public/language/ms/top.json +trans.sk = public/language/sk/top.json +trans.vi = public/language/vi/top.json +trans.bg = public/language/bg/top.json +trans.de = public/language/de/top.json +trans.hu = public/language/hu/top.json +trans.pl = public/language/pl/top.json +trans.pt_PT = public/language/pt-PT/top.json +trans.sl = public/language/sl/top.json +trans.en@pirate = public/language/en-x-pirate/top.json +trans.gl = public/language/gl/top.json +trans.rw = public/language/rw/top.json +trans.sq_AL = public/language/sq-AL/top.json +trans.zh_CN = public/language/zh-CN/top.json +trans.ko = public/language/ko/top.json +trans.ro = public/language/ro/top.json +trans.pt_BR = public/language/pt-BR/top.json +trans.bn = public/language/bn/top.json +trans.fr = public/language/fr/top.json +trans.et = public/language/et/top.json +trans.id = public/language/id/top.json +trans.ja = public/language/ja/top.json +trans.lv = public/language/lv/top.json +trans.ru = public/language/ru/top.json +trans.sc = public/language/sc/top.json +trans.ar = public/language/ar/top.json +trans.da = public/language/da/top.json +trans.uk = public/language/uk/top.json +trans.sv = public/language/sv/top.json +trans.th = public/language/th/top.json +trans.it = public/language/it/top.json +trans.nb = public/language/nb/top.json +trans.sr = public/language/sr/top.json +trans.tr = public/language/tr/top.json +trans.zh_TW = public/language/zh-TW/top.json +trans.es = public/language/es/top.json +trans.hr = public/language/hr/top.json + +[o:nodebb:p:nodebb:r:topic] +file_filter = public/language//topic.json +source_file = public/language/en-GB/topic.json +source_lang = en_GB +type = KEYVALUEJSON +trans.et = public/language/et/topic.json +trans.hu = public/language/hu/topic.json +trans.nb = public/language/nb/topic.json +trans.nl = public/language/nl/topic.json +trans.sk = public/language/sk/topic.json +trans.ar = public/language/ar/topic.json +trans.es = public/language/es/topic.json +trans.fr = public/language/fr/topic.json +trans.lv = public/language/lv/topic.json +trans.ms = public/language/ms/topic.json +trans.pl = public/language/pl/topic.json +trans.pt_PT = public/language/pt-PT/topic.json +trans.uk = public/language/uk/topic.json +trans.ru = public/language/ru/topic.json +trans.bg = public/language/bg/topic.json +trans.en@pirate = public/language/en-x-pirate/topic.json +trans.fa_IR = public/language/fa-IR/topic.json +trans.fi = public/language/fi/topic.json +trans.he = public/language/he/topic.json +trans.ja = public/language/ja/topic.json +trans.pt_BR = public/language/pt-BR/topic.json +trans.tr = public/language/tr/topic.json +trans.zh_TW = public/language/zh-TW/topic.json +trans.bn = public/language/bn/topic.json +trans.da = public/language/da/topic.json +trans.en_US = public/language/en-US/topic.json +trans.hr = public/language/hr/topic.json +trans.it = public/language/it/topic.json +trans.ro = public/language/ro/topic.json +trans.cs = public/language/cs/topic.json +trans.de = public/language/de/topic.json +trans.sr = public/language/sr/topic.json +trans.sv = public/language/sv/topic.json +trans.vi = public/language/vi/topic.json +trans.ko = public/language/ko/topic.json +trans.sl = public/language/sl/topic.json +trans.el = public/language/el/topic.json +trans.gl = public/language/gl/topic.json +trans.id = public/language/id/topic.json +trans.lt = public/language/lt/topic.json +trans.sq_AL = public/language/sq-AL/topic.json +trans.th = public/language/th/topic.json +trans.hy = public/language/hy/topic.json +trans.rw = public/language/rw/topic.json +trans.sc = public/language/sc/topic.json +trans.zh_CN = public/language/zh-CN/topic.json + +[o:nodebb:p:nodebb:r:unread] +file_filter = public/language//unread.json +source_file = public/language/en-GB/unread.json +source_lang = en_GB +type = KEYVALUEJSON +trans.pt_BR = public/language/pt-BR/unread.json +trans.zh_TW = public/language/zh-TW/unread.json +trans.fa_IR = public/language/fa-IR/unread.json +trans.fr = public/language/fr/unread.json +trans.es = public/language/es/unread.json +trans.it = public/language/it/unread.json +trans.ms = public/language/ms/unread.json +trans.ro = public/language/ro/unread.json +trans.de = public/language/de/unread.json +trans.en@pirate = public/language/en-x-pirate/unread.json +trans.hr = public/language/hr/unread.json +trans.hu = public/language/hu/unread.json +trans.lt = public/language/lt/unread.json +trans.ru = public/language/ru/unread.json +trans.sl = public/language/sl/unread.json +trans.th = public/language/th/unread.json +trans.bg = public/language/bg/unread.json +trans.gl = public/language/gl/unread.json +trans.uk = public/language/uk/unread.json +trans.vi = public/language/vi/unread.json +trans.ja = public/language/ja/unread.json +trans.lv = public/language/lv/unread.json +trans.nl = public/language/nl/unread.json +trans.sr = public/language/sr/unread.json +trans.zh_CN = public/language/zh-CN/unread.json +trans.hy = public/language/hy/unread.json +trans.id = public/language/id/unread.json +trans.da = public/language/da/unread.json +trans.en_US = public/language/en-US/unread.json +trans.sk = public/language/sk/unread.json +trans.bn = public/language/bn/unread.json +trans.cs = public/language/cs/unread.json +trans.nb = public/language/nb/unread.json +trans.rw = public/language/rw/unread.json +trans.sq_AL = public/language/sq-AL/unread.json +trans.tr = public/language/tr/unread.json +trans.ar = public/language/ar/unread.json +trans.el = public/language/el/unread.json +trans.he = public/language/he/unread.json +trans.pt_PT = public/language/pt-PT/unread.json +trans.et = public/language/et/unread.json +trans.fi = public/language/fi/unread.json +trans.sc = public/language/sc/unread.json +trans.sv = public/language/sv/unread.json +trans.ko = public/language/ko/unread.json +trans.pl = public/language/pl/unread.json + +[o:nodebb:p:nodebb:r:uploads] +file_filter = public/language//uploads.json +source_file = public/language/en-GB/uploads.json +source_lang = en_GB +type = KEYVALUEJSON +trans.sv = public/language/sv/uploads.json +trans.cs = public/language/cs/uploads.json +trans.el = public/language/el/uploads.json +trans.hu = public/language/hu/uploads.json +trans.hy = public/language/hy/uploads.json +trans.it = public/language/it/uploads.json +trans.tr = public/language/tr/uploads.json +trans.uk = public/language/uk/uploads.json +trans.zh_CN = public/language/zh-CN/uploads.json +trans.en@pirate = public/language/en-x-pirate/uploads.json +trans.lt = public/language/lt/uploads.json +trans.lv = public/language/lv/uploads.json +trans.pt_BR = public/language/pt-BR/uploads.json +trans.sk = public/language/sk/uploads.json +trans.bn = public/language/bn/uploads.json +trans.hr = public/language/hr/uploads.json +trans.pl = public/language/pl/uploads.json +trans.zh_TW = public/language/zh-TW/uploads.json +trans.ru = public/language/ru/uploads.json +trans.sl = public/language/sl/uploads.json +trans.sq_AL = public/language/sq-AL/uploads.json +trans.et = public/language/et/uploads.json +trans.id = public/language/id/uploads.json +trans.ms = public/language/ms/uploads.json +trans.nb = public/language/nb/uploads.json +trans.pt_PT = public/language/pt-PT/uploads.json +trans.da = public/language/da/uploads.json +trans.es = public/language/es/uploads.json +trans.fr = public/language/fr/uploads.json +trans.th = public/language/th/uploads.json +trans.ar = public/language/ar/uploads.json +trans.en_US = public/language/en-US/uploads.json +trans.fi = public/language/fi/uploads.json +trans.gl = public/language/gl/uploads.json +trans.rw = public/language/rw/uploads.json +trans.ro = public/language/ro/uploads.json +trans.sr = public/language/sr/uploads.json +trans.vi = public/language/vi/uploads.json +trans.bg = public/language/bg/uploads.json +trans.de = public/language/de/uploads.json +trans.fa_IR = public/language/fa-IR/uploads.json +trans.ja = public/language/ja/uploads.json +trans.nl = public/language/nl/uploads.json +trans.he = public/language/he/uploads.json +trans.ko = public/language/ko/uploads.json +trans.sc = public/language/sc/uploads.json + +[o:nodebb:p:nodebb:r:user] +file_filter = public/language//user.json +source_file = public/language/en-GB/user.json +source_lang = en_GB +type = KEYVALUEJSON +trans.ms = public/language/ms/user.json +trans.sr = public/language/sr/user.json +trans.lt = public/language/lt/user.json +trans.da = public/language/da/user.json +trans.it = public/language/it/user.json +trans.ru = public/language/ru/user.json +trans.sk = public/language/sk/user.json +trans.ar = public/language/ar/user.json +trans.fr = public/language/fr/user.json +trans.he = public/language/he/user.json +trans.id = public/language/id/user.json +trans.es = public/language/es/user.json +trans.cs = public/language/cs/user.json +trans.et = public/language/et/user.json +trans.pt_BR = public/language/pt-BR/user.json +trans.zh_CN = public/language/zh-CN/user.json +trans.zh_TW = public/language/zh-TW/user.json +trans.bn = public/language/bn/user.json +trans.de = public/language/de/user.json +trans.el = public/language/el/user.json +trans.en@pirate = public/language/en-x-pirate/user.json +trans.en_US = public/language/en-US/user.json +trans.fa_IR = public/language/fa-IR/user.json +trans.gl = public/language/gl/user.json +trans.pl = public/language/pl/user.json +trans.bg = public/language/bg/user.json +trans.sv = public/language/sv/user.json +trans.th = public/language/th/user.json +trans.tr = public/language/tr/user.json +trans.uk = public/language/uk/user.json +trans.vi = public/language/vi/user.json +trans.fi = public/language/fi/user.json +trans.ko = public/language/ko/user.json +trans.lv = public/language/lv/user.json +trans.pt_PT = public/language/pt-PT/user.json +trans.rw = public/language/rw/user.json +trans.sl = public/language/sl/user.json +trans.sq_AL = public/language/sq-AL/user.json +trans.hr = public/language/hr/user.json +trans.hy = public/language/hy/user.json +trans.ja = public/language/ja/user.json +trans.nb = public/language/nb/user.json +trans.nl = public/language/nl/user.json +trans.ro = public/language/ro/user.json +trans.sc = public/language/sc/user.json +trans.hu = public/language/hu/user.json + +[o:nodebb:p:nodebb:r:users] +file_filter = public/language//users.json +source_file = public/language/en-GB/users.json +source_lang = en_GB +type = KEYVALUEJSON +trans.zh_TW = public/language/zh-TW/users.json +trans.de = public/language/de/users.json +trans.en@pirate = public/language/en-x-pirate/users.json +trans.ru = public/language/ru/users.json +trans.sc = public/language/sc/users.json +trans.en_US = public/language/en-US/users.json +trans.bn = public/language/bn/users.json +trans.hy = public/language/hy/users.json +trans.ro = public/language/ro/users.json +trans.fi = public/language/fi/users.json +trans.it = public/language/it/users.json +trans.pl = public/language/pl/users.json +trans.sk = public/language/sk/users.json +trans.ar = public/language/ar/users.json +trans.bg = public/language/bg/users.json +trans.el = public/language/el/users.json +trans.fa_IR = public/language/fa-IR/users.json +trans.sq_AL = public/language/sq-AL/users.json +trans.cs = public/language/cs/users.json +trans.he = public/language/he/users.json +trans.ja = public/language/ja/users.json +trans.ko = public/language/ko/users.json +trans.nb = public/language/nb/users.json +trans.pt_PT = public/language/pt-PT/users.json +trans.sr = public/language/sr/users.json +trans.zh_CN = public/language/zh-CN/users.json +trans.da = public/language/da/users.json +trans.id = public/language/id/users.json +trans.lt = public/language/lt/users.json +trans.ms = public/language/ms/users.json +trans.nl = public/language/nl/users.json +trans.rw = public/language/rw/users.json +trans.sl = public/language/sl/users.json +trans.th = public/language/th/users.json +trans.es = public/language/es/users.json +trans.fr = public/language/fr/users.json +trans.gl = public/language/gl/users.json +trans.hr = public/language/hr/users.json +trans.uk = public/language/uk/users.json +trans.vi = public/language/vi/users.json +trans.sv = public/language/sv/users.json +trans.tr = public/language/tr/users.json +trans.et = public/language/et/users.json +trans.hu = public/language/hu/users.json +trans.lv = public/language/lv/users.json +trans.pt_BR = public/language/pt-BR/users.json + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..89a9b5fe38 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7546 @@ +#### v2.8.1 (2022-12-30) + +##### Chores + +* fallbacks for new language string (8a69e740) +* remove extraneous lines from changelog (bbaf26ce) +* incrementing version number - v2.8.0 (8e77673d) +* update changelog for v2.8.0 (a5c2edb9) +* incrementing version number - v2.7.0 (96cc0617) +* incrementing version number - v2.6.1 (7e52a7a5) +* incrementing version number - v2.6.0 (e7fcf482) +* incrementing version number - v2.5.8 (dec0e7de) +* incrementing version number - v2.5.7 (5836bf4a) +* incrementing version number - v2.5.6 (c7bd7dbf) +* incrementing version number - v2.5.5 (3509ed94) +* incrementing version number - v2.5.4 (e83260ca) +* incrementing version number - v2.5.3 (7e922936) +* incrementing version number - v2.5.2 (babcd17e) +* incrementing version number - v2.5.1 (ce3aa950) +* incrementing version number - v2.5.0 (01d276cb) +* incrementing version number - v2.4.5 (dd3e1a28) +* incrementing version number - v2.4.4 (d5525c87) +* incrementing version number - v2.4.3 (9c647c6c) +* incrementing version number - v2.4.2 (3aa7b855) +* incrementing version number - v2.4.1 (60cbd148) +* incrementing version number - v2.4.0 (4834cde3) +* incrementing version number - v2.3.1 (d2425942) +* incrementing version number - v2.3.0 (046ea120) + +##### Bug Fixes + +* vulnerability in socket.io nested namespaces (#11117) (586eed14) +* lock post/reply similar to user.create (1ea9481a) + +#### v2.8.0 (2022-12-21) + +##### Chores + +* **deps:** + * update dependency jquery to v3.6.3 (#11107) (13a3faa0) + * update dependency eslint to v8.30.0 (#11102) (485ee130) + * update dependency mocha to v10.2.0 (#11094) (c4cc1e61) +* up jquery (3e8f5378) +* remove extraneous lines from changelog (e213dbc3) +* incrementing version number - v2.7.0 (96cc0617) +* update changelog for v2.7.0 (4701c96d) +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-email (717b3612) + * fallback strings for new resources: nodebb.admin-settings-email (4f4b4800) + +##### New Features + +* add force flag to plugin install in cli (#11089) (de31cb1a) +* integrating basic client-side form validity checking in settings v1 and v2 (33af2d9c) + +##### Bug Fixes + +* **deps:** + * update dependency sharp to v0.31.3 (#11110) (ef500af8) + * update dependency sanitize-html to v2.8.1 (#11109) (7ab46b78) + * update dependency esbuild to v0.16.10 (#11104) (eb6a9c47) + * update dependency mongodb to v4.13.0 (#11105) (05443dbe) + * update dependency esbuild to v0.16.8 (#11101) (18ff6caa) + * update dependency sanitize-html to v2.8.0 (#11098) (faaf09f7) + * update dependency ace-builds to v1.14.0 (#11095) (cde44587) + * update dependency nodebb-plugin-2factor to v5.1.2 (#11096) (5dda9a5b) + * update dependency postcss to v8.4.20 (#11097) (0a5adb41) + * update dependency compare-versions to v5.0.3 (#11092) (8b209f16) + * update dependency html-to-text to v9.0.3 (#11093) (7bcfe38e) + * update dependency @socket.io/redis-adapter to v8 (#11084) (7b9bbef5) + * update dependency nodebb-widget-essentials to v6.0.1 (#11085) (7b48156c) + * update dependency esbuild to v0.16.7 (#11086) (65ef722e) + * update dependency esbuild to v0.16.3 (#11083) (4f67fc1a) + * update dependency esbuild to v0.15.16 (#11069) (22493ffb) +* change hsts-maxage back to numeric input type, change API token uid input to numeric text type (896493db) +* replace input type number with text/pattern (2bc23a95) + +##### Refactors + +* flag states so that they are not hardcoded, allow plugins to add additional states, deprecated filter:flags.getFilters hook, closes #11065 (9f531f95) +* remove debug log closes #11090 (06f4801e) + +#### v2.7.0 (2022-12-14) + +##### Chores + +* added stub file in hy (9ee8502d) +* **deps:** + * update dependency lint-staged to v13.1.0 (#11082) (693d4783) + * update dependency eslint to v8.29.0 (#11074) (eab5b754) + * update dependency lint-staged to v13.0.4 (#11064) (f947ac6d) +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-email (0e319a58) + * fallback strings for new resources: nodebb.admin-settings-email (9676b192) + +##### New Features + +* update transifex config (for use with new cli) (f11094cb) +* integrating basic client-side form validity checking in settings v1 and v2 (dadbcd73) +* add ./nodebb install (4efc19d5) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-theme-lavender to v6.0.1 (#11081) (df3f1c5e) + * update dependency esbuild to v0.16.3 (#11083) (85d38158) + * update dependency html-to-text to v9 (#11075) (d8e9738d) + * update dependency ace-builds to v1.13.2 (#11080) (35be4594) + * update dependency fs-extra to v11 (#11072) (aafb7f6e) + * update dependency esbuild to v0.15.16 (#11069) (7bc4b836) + * update dependency mongodb to v4.12.1 (#11062) (e14d4abc) +* relax selectors for client-side form validation so that all form elements are checked (43e7c988) +* change hsts-maxage back to numeric input type, change API token uid input to numeric text type (db8d3a94) +* replace input type number with text/pattern (45ae31f8) +* categories.js not showing custom privileges (#10856) (8c4d6bbe) +* #11077, add admin uploads paths to priv mapping (07a02125) + +##### Tests + +* dont try to load admin upload routes (c2bb2b30) + +#### v2.6.1 (2022-11-28) + +##### Chores + +* incrementing version number - v2.6.0 (e7fcf482) +* update changelog for v2.6.0 (eedd84ae) +* incrementing version number - v2.5.8 (dec0e7de) +* incrementing version number - v2.5.7 (5836bf4a) +* incrementing version number - v2.5.6 (c7bd7dbf) +* incrementing version number - v2.5.5 (3509ed94) +* incrementing version number - v2.5.4 (e83260ca) +* incrementing version number - v2.5.3 (7e922936) +* incrementing version number - v2.5.2 (babcd17e) +* incrementing version number - v2.5.1 (ce3aa950) +* incrementing version number - v2.5.0 (01d276cb) +* incrementing version number - v2.4.5 (dd3e1a28) +* incrementing version number - v2.4.4 (d5525c87) +* incrementing version number - v2.4.3 (9c647c6c) +* incrementing version number - v2.4.2 (3aa7b855) +* incrementing version number - v2.4.1 (60cbd148) +* incrementing version number - v2.4.0 (4834cde3) +* incrementing version number - v2.3.1 (d2425942) +* incrementing version number - v2.3.0 (046ea120) + +##### Documentation Changes + +* remote extraneous lines from changelog (8a15e58d) + +##### Bug Fixes + +* prototype vulnerability in socket.io onMessage (48d14392) +* #11066, fix custom privilege/path in routePrefixMap (0e495f9e) + +##### Refactors + +* not deprecated on 2.x (91c2e5ac) + +#### v2.6.0 (2022-11-23) + +##### Chores + +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-advanced (1d7dbf14) + * fallback strings for new resources: nodebb.admin-settings-advanced (05b6758e) + * fallback strings for new resources: nodebb.admin-settings-user, nodebb.admin-settings-email (9ad3b214) +* **deps:** + * update commitlint monorepo to v17.3.0 (#11058) (f4784205) + * update dependency eslint to v8.28.0 (#11059) (d480f26b) + * update dependency jsdom to v20.0.3 (#11054) (5750ded1) + * update dependency jquery to v3.6.1 (#11048) (6611d44c) + * update dependency eslint-config-nodebb to v0.2.1 (#11043) (07c81d5d) + * update commitlint monorepo to v17.2.0 (#11026) (26be289e) + * update dependency husky to v8.0.2 (#11018) (4b3978be) + * update dependency eslint to v8.27.0 (#11004) (4fcef7c5) + * update dependency jsdom to v20.0.2 (#11014) (a6e4fee1) + * update dependency mocha to v10.1.0 (#10980) (ffa117ab) + * update dependency eslint to v8.25.0 (3719233a) + * update dependency jsdom to v20.0.1 (#10934) (241b7c4f) + * update dependency eslint to v8.24.0 (#10922) (8bcbff33) + * update commitlint monorepo (#10888) (3f30056a) + * update postgres docker tag to v14 (#10829) (5aa55d29) + * update dependency eslint to v8.23.1 (#10885) (0c6fb6e3) +* remove derpy extra changelog bits (445f09f0) +* incrementing version number - v2.5.8 (dec0e7de) +* update changelog for v2.5.8 (c9cd8975) + +##### New Features + +* add permissions-policy header (864fe0f9) +* allow groups to be exempt from maintenance mode (3c85b944) +* add search data to filter:search.inContent (be92be4e) +* add relevant topic events to post objects (a584dae6) +* client-side hooks for navigator scroll action (135fe55b) +* j and k hotkeys in topic to navigate through it quickly (aeb94c32) +* a couple utility methods in navigator module to get and set count and index (9f9a835f) +* paginate recentposts.rss and (ebd7c05c) +* add quiet to action:settings.set (665f36b7) + +##### Bug Fixes + +* https://github.com/NodeBB-Community/nodebb-plugin-custom-pages/issues/68 (110311b2) +* https://github.com/NodeBB-Community/nodebb-plugin-custom-pages/issues/68 (fc49665f) +* #11052, add missing await (bb82eb71) +* pin jquery version for all packages that include it as a subdependency (a87f64b4) +* #10877, define a resolution for jquery subdependency of timeago (5aaebdd3) +* #11044, allow banned users to post (abcfb631) +* check schedule privilege, closes #11032 (61090615) +* mixing of old and new bch syntax (1e484643) +* category ordering add test (61d32bde) +* use `--omit=dev` flag for npm instead of `--production` (53d47a58) +* update nav thumb on setIndex call (thx @barisusakli) (664bc5f5) +* do not call `navigator.update()` when `scrollToElement` is explicitly passed a new index value (b7287c1e) +* race condition where `navigator.update` was called when it should not be (fa643eb8) +* be tidier and explicitly clean up ctrl-f hotkey on topic page (5b9de0e5) +* listen to -d flag on cli upgrade (fe249fa5) +* automatically remove `lang` parameter if it matches the forum default (881c7c4d) +* **deps:** + * update socket.io packages to v4.5.4 (#11061) (709ca59d) + * update dependency esbuild to v0.15.15 (#11056) (d449710e) + * update dependency nodebb-theme-persona to v12.1.12 (#11057) (18eb35a1) + * update dependency cropperjs to v1.5.13 (#11055) (13f3a048) + * update dependency nodebb-plugin-markdown to v10.1.1 (#11047) (62a60cbf) + * update dependency mongodb to v4.12.0 (#11049) (59b4d95f) + * update dependency connect-pg-simple to v8 (#11033) (ca162c04) + * update dependency ace-builds to v1.13.1 (#11045) (e0b9240d) + * update dependency esbuild to v0.15.14 (a5355d78) + * update dependency nodebb-theme-persona to v12.1.11 (2af7fd5f) + * update dependency postcss to v8.4.19 (#11028) (b94bb1bf) + * update dependency ace-builds to v1.13.0 (#11031) (313d0c32) + * update dependency webpack to v5.75.0 (#11027) (40c9cc05) + * update dependency yargs to v17.6.2 (#11025) (8d3907b5) + * update dependency lru-cache to v7.14.1 (#11023) (1ea2a32b) + * update dependency sharp to v0.31.2 (#11024) (51919f7a) + * update dependency esbuild to v0.15.13 (#11021) (c4a1905b) + * update dependency ioredis to v5.2.4 (#11022) (e144debb) + * update dependency ace-builds to v1.12.5 (#11019) (de507f72) + * update dependency mongodb to v4.11.0 (#10994) (9ca2482a) + * update dependency autoprefixer to v10.4.13 (#11020) (35d67ad3) + * update dependency nodebb-plugin-composer-default to v9.2.4 (#10998) (5a5771e1) + * update dependency sanitize-html to v2.7.3 (#11008) (73a60854) + * update dependency nodebb-plugin-composer-default to v9.2.3 (#10997) (ee4fde13) + * update dependency esbuild to v0.15.12 (#10996) (29fddd65) + * update dependency nodebb-plugin-composer-default to v9.2.2 (1fc2f1e8) + * update dependency nodebb-theme-persona to v12.1.9 (6471b698) + * bump composer-default (7b9e0847) + * update dependency ace-builds to v1.12.3 (1e930f9d) + * update dependency nodebb-theme-persona to v12.1.8 (#10982) (9900e5f4) + * update dependency ace-builds to v1.12.2 (4260f0c2) + * update dependency ace-builds to v1.12.1 (6485bc25) + * update dependency ace-builds to v1.12.0 (#10978) (00ce8fd8) + * update socket.io packages to v4.5.3 (#10977) (fb41fbe7) + * update dependency nodebb-plugin-emoji to v4.0.6 (dcca3397) + * update dependency esbuild to v0.15.11 (ff69c3e1) + * update dependency @isaacs/ttlcache to v1.2.1 (647bbd57) + * update dependency nodebb-plugin-mentions to v3.0.12 (60d0145b) + * update dependency postcss to v8.4.18 (3c2a636c) + * update dependency nodebb-plugin-spam-be-gone to v1.0.2 (#10958) (b68faa09) + * update dependency body-parser to v1.20.1 (#10941) (0f63947b) + * update dependency express to v4.18.2 (#10948) (ff53064c) + * update dependency semver to v7.3.8 (#10937) (1b89b661) + * update dependency nodebb-theme-persona to v12.1.7 (1873b527) + * update dependency commander to v9.4.1 (#10928) (855a2bad) + * update dependency postcss to v8.4.17 (#10929) (d84ee308) + * update dependency nodemailer to v6.8.0 (#10925) (222ab6ae) + * update dependency yargs to v17.6.0 (#10931) (7c3c3d02) + * update dependency nodebb-plugin-spam-be-gone to v1.0.1 (#10912) (28d1844e) + * update dependency ace-builds to v1.11.2 (#10923) (87c84fe8) + * update dependency sharp to v0.31.1 (#10926) (8e23e410) + * update dependency ace-builds to v1.11.1 (#10920) (ce77605a) + * update dependency ace-builds to v1.11.0 (#10909) (d900fd17) + * update dependency autoprefixer to v10.4.12 (#10911) (54294871) + * update dependency compare-versions to v5 (#10890) (f971385b) + * bump persona #10907 (846eda0a) + * bump persona, #10907 (27c80d39) + * update dependency mongodb to v4.10.0 (#10908) (bbe7f779) + * update dependency nodebb-theme-persona to v12.1.3 (#10903) (0debc51b) + * update dependency nodebb-plugin-2factor to v5.1.1 (02fe6875) + * update dependency nodebb-theme-persona to v12.1.2 (425bf87b) + * update dependency sanitize-html to v2.7.2 (#10893) (bfeb0368) + * update dependency autoprefixer to v10.4.11 (#10892) (0758655a) + * update dependency sharp to v0.31.0 (#10887) (2836be5c) + * update socket.io packages to v4.5.2 (#10884) (22f3d0bb) + * update dependency nodebb-plugin-2factor to v5.1.0 (#10886) (335990be) + * update dependency autoprefixer to v10.4.10 (#10883) (0c2a88de) + * update dependency winston to v3.8.2 (#10882) (d911a1bf) + * update dependency pg to v8.8.0 (#10863) (01129a39) + * update dependency nodebb-plugin-emoji to v4.0.5 (#10881) (34d243e4) + * update dependency ioredis to v5.2.3 (#10861) (5e375e4a) + * update dependency pg-cursor to v2.7.4 (#10862) (86b63fab) + * update dependency mongodb to v4.9.1 (#10880) (5583ab95) + * update dependency autoprefixer to v10.4.9 (#10879) (cc6798bb) + * update dependency postcss to v8.4.16 (#10824) (8c680db5) + * update dependency jquery to v3.6.1 (#10868) (6687f49b) + * update dependency ace-builds to v1.10.1 (#10840) (c0c3ee01) + * bump composer-default to v9.2.0 (29ddeaa1) + +##### Refactors + +* don't prevent startup if staticDir is undefined (b34e859c) +* added new ajaxify method .cleanup, called before ajaxify.start. (3700174c) + +#### v2.5.8 (2022-11-09) + +##### Chores + +* really fix indents this time (c2024f34) +* fix indents (d50512e7) +* add bootstrap5 to test runner for now (be5d6d29) +* incrementing version number - v2.5.7 (5836bf4a) +* update changelog for v2.5.7 (17e948ab) + +##### New Features + +* new search hooks (b5d38bc6) +* add search data to filter:search.inContent (e3f21562) + +##### Bug Fixes + +* pass csrf_token into calls to /register/abort, #11017 (2f9d8c35) +* check for csrf token on /register/abort, + theme changes for v2.x branches of themes (55a197a7) +* upgrade script to work from 0.x to 2.x (a31ba824) +* #10519, image height in emails (673261ff) +* fallback language strings for #10987 (b9c8c02f) +* #10993, apply autoLocale middleware to guests only (6f673f80) +* check cid as well as template (9227b82e) +* revert breaking change, add back SocketUser.emailConfirm (9ee30fe7) +* in appropriately named language key `email-confirm-email2` (09f3ac65) +* correctly pass dev flag to package installer (7672194c) +* use `--omit=dev` flag for npm instead of `--production` (09cfd0bd) + +##### Refactors + +* use utils.debounce (d264c6ac) + +##### Tests + +* fix tests again (06d15391) +* fix test (c833d3cd) + +#### v2.5.7 (2022-10-14) + +##### Chores + +* incrementing version number - v2.5.6 (c7bd7dbf) +* update changelog for v2.5.6 (e92238d0) + +##### Performance Improvements + +* speed up build (dd4e9cce) + +#### v2.5.6 (2022-10-13) + +##### Chores + +* incrementing version number - v2.5.5 (3509ed94) +* update changelog for v2.5.5 (e7d0040d) + +##### Bug Fixes + +* use admin:groups priv for groups (#10960) (b879b6a0) +* https://github.com/NodeBB/NodeBB/issues/10525 (e35b0a86) + +#### v2.5.5 (2022-10-11) + +##### Chores + +* up plugins (b91ef6dd) +* incrementing version number - v2.5.4 (e83260ca) +* update changelog for v2.5.4 (aabf073c) + +#### v2.5.4 (2022-10-11) + +##### Chores + +* 🤔 (7240e8ce) +* incrementing version number - v2.5.3 (7e922936) +* update changelog for v2.5.3 (fdf240f6) + +##### Continuous Integration + +* add minimum GitHub token permissions for workflows Signed-off-by: Ashish Kurmi (fe0020fb) + +##### Bug Fixes + +* EEXISTS error on linux if plugin/theme overrides core js file (ebd5dcc6) +* category ordering add test (177d9048) +* crash in category drag, closes #10932 (989b55d0) +* broken flag history on flag update (803398e9) +* scroll to post if theme doesn't have top navbar (aad0a618) +* add lru-cache to checked packages, to fix upgrade issue with lru-cache (14515f60) + +#### v2.5.3 (2022-09-19) + +##### Chores + +* remove duplicate version increment lines in changelog (5dbcfef9) +* bring back treding plugins (8aa1596d) +* incrementing version number - v2.5.2 (babcd17e) +* update changelog for v2.5.2 (84b6a7c7) + +##### New Features + +* store topic title and tags in diffs (#10900) (b5dd89e1) + +##### Bug Fixes + +* #10906, allow `middleware.checkAccountPermissions` to be called with either uid or userslug in params (cf4f5447) +* #10896, unescape / in taskbar (8e2129f8) +* add back timeago to post history modal (d3e38df8) +* **deps:** bump composer-default to v9.1.1 (1d80a07e) + +##### Other Changes + +* fix lint (3d5a6b39) + +##### Performance Improvements + +* disable trending plugins, too slow due to nbbpm (b392450c) + +##### Tests + +* add back /admin/extend/plugins test (1c9c39a8) + +#### v2.5.2 (2022-09-04) + +##### Chores + +* incrementing version number - v2.5.1 (ce3aa950) +* update changelog for v2.5.1 (2b2fd4f3) + +##### Bug Fixes + +* registration regression, closes #10875 (f6f37dc1) + +##### Other Changes + +* fix lint error (b45e2413) + +##### Tests + +* disable nbbpm test temporarily (1dc79d76) + +#### v2.5.1 (2022-09-02) + +##### Chores + +* incrementing version number - v2.5.0 (01d276cb) +* update changelog for v2.5.0 (1076285d) + +##### Bug Fixes + +* missing escape on ACP category backgroundImage property (67cb7035) +* **deps:** temporarily add back old textcomplete dependencies so emoji plugin can import them (61d1e9e0) + +#### v2.5.0 (2022-09-01) + +##### Breaking Changes + +* reduce nodebb.min.js size by around 800kb (b7addffc) + +##### Chores + +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-reputation (e20433ec) + * fallback strings for new resources: nodebb.admin-settings-post (fcbbb4d6) + * fallback strings for new resources: nodebb.admin-advanced-cache (90fc50e1) +* move @textcomplete and its modules to composer-default (3cbb7a3d) +* remove client-side js file for tpl that no longer exists (bc2ea860) +* incrementing version number - v2.4.5 (dd3e1a28) +* update changelog for v2.4.5 (d505cc47) + +* **deps:** + * update dependency eslint to v8.22.0 (#10835) (8fce68d3) + * update mongo docker tag to v3.7 (8afaed22) + * update docker/setup-qemu-action action to v2 (4aecf399) + * update redis docker tag to v2.8.23 (#10811) (269382e0) + * update redis docker tag to v2.8.23 (#10811) (59fd0efe) + +##### Documentation Changes + +* **openapi:** v3 spec for new user export routes (70652ad4) + +##### New Features + +* remove visibilityjs (#10870) (19207325) +* introduce ACP defined option to rescind notif or do nothing on flag resolve/reject (15b1561f) +* allow v3 api to handle 202 and 204 response codes as well. (0cda5aa3) +* don't show signatures again in pagination mode in same topic (0a6900fc) +* setting to show signatures only once in topics, closes #10071 (aba420a3) +* add event type to topic event component (bcb94ede) +* allow plugins to add to admin cache list (#10833) (a9bbb586) + +##### Bug Fixes + +* **deps:** + * update dependency compare-versions to v4.1.4 (884d4075) + * update dependency mongodb to v4.9.0 (e51004e2) + * update dependency lru-cache to v7.14.0 (d8a52f81) + * bump composer-default to v9 (81f8d84f) + * remove textcomplete in favour of @textcomplete/core (same package, just refactored) (b7bcc367) + * update dependency nodebb-theme-persona to v12.1.1 (#10838) (444b4d57) + * update dependency ace-builds to v1.9.5 (4c44d125) + * update dependency lru-cache to v7.13.2 (4ddc0c7a) + * update dependency nodemailer to v6.7.8 (dd385184) + * pin dependency @isaacs/ttlcache to 1.2.0 (150a7488) + * update dependency nodebb-plugin-composer-default to v8.0.1 (#10819) (9a7dd3a3) + * update dependency nodebb-plugin-composer-default to v8.0.1 (#10819) (69c87c2c) +* empty thread tools container on open (a088eb19) +* add dropup handler to thread tools menu, updated how post tools menu adds dropup handler (c1936e87) +* empty thread tools container on open (df36d967) +* add dropup handler to thread tools menu, updated how post tools menu adds dropup handler (3dd3cd82) +* removing duplicate session rerolling code (as it is in passport@^0.6 now) (65b3996a) +* don't crash if post is undefined (e06e526e) +* return at least one in sizeCalculation (#10832) (3975fa2e) + +##### Refactors + +* remove console.log (a2d0cd16) +* move export generation logic to v3 controller, GET/HEAD routes for exports (d0570518) +* use group.slug on acp group urls closes #8277 (14c79763) + +##### Tests + +* have some build tests not pollute build folder (7c5a915d) +* User.hidePrivateData (b424ba46) +* passport0.6 (#10638) (33458701) + +#### v2.4.5 (2022-08-22) + +##### Chores + +* incrementing version number - v2.4.4 (d5525c87) +* update changelog for v2.4.4 (77e492b8) + +##### Bug Fixes + +* wrap passport.authenticate to pass in keepSessionInfo if not already set (9b96c33d) +* parseInt caller.uid closes #10849 (bc37a5c5) + +#### v2.4.4 (2022-08-18) + +##### Chores + +* incrementing version number - v2.4.3 (9c647c6c) +* update changelog for v2.4.3 (06da15a5) + +##### Bug Fixes + +* missing req, closes #10847 (489fb3a3) + +#### v2.4.3 (2022-08-18) + +##### Chores + +* incrementing version number - v2.4.2 (3aa7b855) +* update changelog for v2.4.2 (ba7a3466) + +##### Bug Fixes + +* #10845, disallow inline viewing of uploaded html files (4dc7fa05) + +#### v2.4.2 (2022-08-17) + +##### Chores + +* incrementing version number - v2.4.1 (60cbd148) +* update changelog for v2.4.1 (4b6baabb) + +##### Documentation Changes + +* explain what export routes actually do in OpenAPI documentation (#10836) (72e7b9f7) + +##### Bug Fixes + +* #10841, incorrect conditional in email interstitial partial (ec048a01) +* don't crash if post is undefined (4a3e36a7) + +##### Tests + +* passport0.6 (#10638) (6b2a6f90) + +#### v2.4.1 (2022-08-14) + +##### Chores + +* **deps:** + * update docker/build-push-action action to v3 (bfd6318c) + * update docker/login-action action to v2 (3d68accf) + * update docker/setup-buildx-action action to v2 (371ac032) +* incrementing version number - v2.4.0 (4834cde3) +* update changelog for v2.4.0 (c4714ff7) + +##### Bug Fixes + +* return at least one in sizeCalculation (#10832) (15ca460c) + +#### v2.4.0 (2022-08-10) + +##### Chores + +* **deps:** + * update dependency eslint to v8.21.0 (13a17bd1) + * bump commander from 7.2.0 to 9.4.0 in /install (993b7747) +* update to new transifex project url (659cfe85) +* re-order interstitial tests so email and gdpr tests are in sub-blocks (342cca35) +* opt-out of dependabot, due to conflicts with renovate (70d60289) +* incrementing version number - v2.3.1 (d2425942) +* update changelog for v2.3.1 (2f487175) +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-email (cdaa8f21) + * fallback strings for new resources: nodebb.admin-settings-email (3e56c547) + * fallback strings for new resources: nodebb.user (bcf7ef67) + +##### New Features + +* support packageManager property in package.json (b3a37a7f) +* automatically enable the SMTP transport option if the SMTP service is changed (4055e3bd) +* present a password challenge on email update flow (7fcee42b) +* add client side filter:chat.send, closes #10729 (cb084cbd) +* fire hook to allow plugins to filter the pids returned in a user profile (17e44ff5) +* closes #10719, don't trim children if category is marked section (be917e8d) +* closes #10719, don't trim children if category is marked section (0bec52bc) + +##### Bug Fixes + +* adapt to breaking change in commander (38bf30c8) +* move panel-offset setting code back to theme header (d0255fc6) +* #10808; tweak copy for gmail app passwords support (7082291b) +* don't require password challenge if no password is set in user account (9d27e907) +* do not throw if password passed into `isPasswordCorrect` is invalid, just return false (287f4c2c) +* don't crash if req.body.username is not string (7e8ad785) +* don't crash if target/user is undefined (55c5588a) +* race condition causing undefined ajaxify.data (4586f68e) +* #10809, test runner to only run tests for plugins included in `test_plugins` (1ca09b63) +* #10805, hide unconfirmed emails from user data retrieval methods (cba9047f) +* use different emoji on NodeBB Ready — again because procrastination (3e062a7f) +* unnecessary escape (cd438b32) +* remove socket.io cluster adapter (#10742) (456b8798) +* #10783, do not purge files without a timestamp prefix (dc3a6a29) +* **deps:** + * bump persona v12.1.0 (1465598d) + * bump 2factor to v5.0.2 (bd18004d) + * update dependency sanitize-html to v2.7.1 (#10792) (f02492bd) + * update dependency html-to-text to v8.2.1 (f22790c0) + * update dependency webpack to v5.74.0 (e748e31f) + * update dependency autoprefixer to v10.4.8 (#10799) (4ca0d571) + +##### Performance Improvements + +* make single db call (d73f0f9c) + +##### Tests + +* additional tests for password challenge on email update (65c59cc1) +* add dummy emailer hook to suppress sendmail error logging (8e1a4bb5) +* fix one last failing test (68bcd7f4) +* fix user email tests (06f089af) +* fix tests so that when user.create is called, email is set prior to confirmation (f93a0b83) + +#### v2.3.1 (2022-07-29) + +##### Chores + +* **deps:** + * bump sanitize-html from 2.7.0 to 2.7.1 in /install (7b606d2e) + * bump webpack from 5.73.0 to 5.74.0 in /install (a9900625) +* **i18n:** fallback strings for new resources: nodebb.admin-settings-advanced, nodebb.admin-menu, nodebb.error (17120e03) +* incrementing version number - v2.3.0 (046ea120) +* update changelog for v2.3.0 (a6f7fff0) + +##### New Features + +* add emoji to startup logs, because procrastination. (5176fb15) + +##### Bug Fixes + +* #10798, logic error in COEP header; helmet config (89173f17) +* #10795, early return for selection tooltip based on calling user privilege (847d2b91) +* **deps:** update persona to v12.0.14 (9f225e70) + +#### v2.3.0 (2022-07-28) + +##### Chores + +* **deps:** + * update dependency lint-staged to v13 (07ce0c39) + * bump ace-builds from 1.7.1 to 1.8.1 in /install (f397d968) + * bump ioredis from 5.2.0 to 5.2.2 in /install (067a5110) +* have renovate work off of `develop` branch instead (f334e398) +* update changelog for v2.2.5 (6c3ebf3b) + +##### New Features + +* add client side filter:chat.send, closes #10729 (b2da02d6) +* UI changes for ACP > Manage > Categories (#10782) (820bc994) +* show an informative message when no plugins are found after filtering (6840a742) +* Allow defining active plugins in config (#10767) (23cb67a1) +* allow plugins to toggle whether IPs are shown in the users CSV export (a6af47da) +* fire hook to allow plugins to filter the pids returned in a user profile (c26be43a) +* closes #10719, don't trim children if category is marked section (7e80cc10) + +##### Bug Fixes + +* **deps:** + * update dependency mongodb to v4.8.1 (8384b7cf) + * update dependency helmet to v5.1.1 (03a173bb) +* bug where fallback to forum search was not working due to client-side error (25046642) +* better looking placeholder text for ACP search (1b9c6819) +* use `user.hidePrivateData();` more consistently across user retrieval endpoints (0529f2fb) +* minor margin tweak for alert in acp header (4faf0cdf) +* cannot turn off all networks (bbc7f2af) +* cannot setting networks for sharing posts (2e088a8e) + +##### Refactors + +* invert helmet configuration (dcacd815) + +#### v2.2.5 (2022-07-21) + +##### Chores + +* **deps-dev:** + * bump eslint from 8.19.0 to 8.20.0 in /install (8d109fef) + * bump @commitlint/config-angular in /install (2a88a50f) +* **deps:** + * bump cron from 2.0.0 to 2.1.0 in /install (4c1bda32) + * bump jquery-ui from 1.13.1 to 1.13.2 in /install (abb19e98) + * bump nodebb-theme-persona in /install (9e52b8c6) + * bump ioredis from 5.1.0 to 5.2.0 in /install (8c20fdad) +* incrementing version number - v2.2.4 (d1d63e6b) +* update changelog for v2.2.4 (52f7ed64) +* incrementing version number - v2.2.3 (f80476b9) +* incrementing version number - v2.2.2 (343ffa66) +* incrementing version number - v2.2.1 (efc77b2a) +* incrementing version number - v2.2.0 (eecb836d) + +##### Bug Fixes + +* expire email validation tokens on password change (c93bd010) +* remove extraneous console.log (0d58e8a6) +* **deps:** update dependency mongodb to v4.8.0 (bff239da) + +#### v2.2.4 (2022-07-12) + +##### Chores + +* **deps:** + * update docker/metadata-action action to v4 (42a45a71) + * bump ace-builds from 1.5.3 to 1.7.1 in /install (d568d2f5) + * bump winston from 3.8.0 to 3.8.1 in /install (26b73b39) + * bump ioredis from 5.0.6 to 5.1.0 in /install (0d55c42d) + * bump nodemailer from 6.7.5 to 6.7.7 in /install (bd37b286) +* **deps-dev:** + * bump @commitlint/cli from 17.0.2 to 17.0.3 in /install (9e44cdad) + * bump eslint from 8.18.0 to 8.19.0 in /install (7139ad5f) +* incrementing version number - v2.2.3 (f80476b9) +* update changelog for v2.2.3 (8719a93f) +* incrementing version number - v2.2.2 (343ffa66) +* incrementing version number - v2.2.1 (efc77b2a) +* incrementing version number - v2.2.0 (eecb836d) + +##### Bug Fixes + +* **deps:** update dependency diff to v5.1.0 (452e5bf7) +* #10733, extraneous apostrophes in plugin upgrader (3c41ae04) +* get version from install/package.json instead (60114219) +* #10739, always re-add theme to active plugins, on theme set (8d701ec3) + +#### v2.2.3 (2022-07-05) + +##### Chores + +* incrementing version number - v2.2.2 (343ffa66) +* update changelog for v2.2.2 (f59ddc34) +* incrementing version number - v2.2.1 (efc77b2a) +* incrementing version number - v2.2.0 (eecb836d) + +##### Bug Fixes + +* move call to `ajaxify.parseData` out to root level (b778e38c) +* move `ajaxify.parseData` back out to DOMContentLoaded (2582cb53) + +##### Refactors + +* rewrite ajaxify.parseData in vanilla (165b804d) + +#### v2.2.2 (2022-06-30) + +##### Chores + +* **deps-dev:** + * bump eslint from 8.17.0 to 8.18.0 in /install (bfd626b0) + * bump jsdom from 19.0.0 to 20.0.0 in /install (d037c2c9) +* incrementing version number - v2.2.1 (efc77b2a) +* update changelog for v2.2.1 (667780cf) +* incrementing version number - v2.2.0 (eecb836d) + +##### Bug Fixes + +* improper handling of single vs. multiple inputs for hidePrivateData (f38b2a73) +* observe user-specific and ACP-specific hiding rules for fullname (ecf0e8ce) + +##### Refactors + +* allow user.hidePrivateData() to handle an array of `userData` (3f5ae893) + +#### v2.2.1 (2022-06-24) + +##### Chores + +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-uploads (da481163) + * fallback strings for new resources: nodebb.admin-settings-advanced (34d75e96) +* add in warning in language directories about not editing files directly (06074e8c) +* incrementing version number - v2.2.0 (eecb836d) +* update changelog for v2.2.0 (bb3766df) + +##### Documentation Changes + +* remove the redundant security policy template (7f5b285e) + +##### New Features + +* cross origin opener policy options (#10710) (88132358) + +##### Bug Fixes + +* **deps:** + * update dependency winston to v3.8.0 (#10725) (d4a5039e) + * update dependency sharp to v0.30.7 (#10724) (0a07c2c0) +* handle ENOENT on file deletion, closes #10645 (43f9e6c8) + +##### Tests + +* fix i18n tests (05c30677) + +#### v2.2.0 (2022-06-15) + +##### Chores + +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-uploads (f5afb5c3) + * fallback strings for new resources: nodebb.admin-manage-users (b1dc0531) + * fallback strings for new resources: nodebb.topic (d7d32a8a) +* remove unnecessary `affected` set from deprecated plugin hook (bef236f3) +* bump persona, #10699 (c7fa73b1) +* bump vanilla (d90fc18b) +* bump persona, closes #10566 (5bc972df) +* update changelog for v2.1.1 (ca038b84) +* **deps:** + * bump less from 4.1.2 to 4.1.3 in /install (#10685) (78322636) + * bump nodebb-plugin-2factor from 5.0.0 to 5.0.1 in /install (#10686) (701d8d76) + * bump spdx-license-list from 6.5.0 to 6.6.0 in /install (c82d34c8) + +##### New Features + +* new cronjob and ACP option to delete orphans after configurable number of days, closes #10659 (88aee439) +* allowed plugins to modify email verification details prior to db saving or email send/plugin fire (b9d4724e) +* pass absolute url to post into post tools response, #10566 (ece733ed) + +##### Bug Fixes + +* no need to pass in empty Set in deprecated hooks (3a015eb8) +* #10696, fix alert for password reset email (2da188fe) +* #10692 (b6f8e2fd) +* #10690, all uploads in `uploads/files` showing orphaned (a04da673) +* bug where post associations are not shown when the directory is `files` (7f870beb) + +##### Refactors + +* move orphan cleaning logic to its own method, added tests for getOrphans and cleanOrphans (22368b99) + +##### Tests + +* add more asserts to failing test (ea1ed667) + +#### v2.1.1 (2022-06-08) + +##### Chores + +* **deps:** + * bump nodebb-theme-persona from 12.0.8 to 12.0.9 in /install (bb39dfba) + * bump mongodb from 4.6.0 to 4.7.0 in /install (a8987856) + * bump async from 3.2.3 to 3.2.4 in /install (ce38f711) + * bump xregexp from 5.1.0 to 5.1.1 in /install (eb9a46f8) + * bump webpack from 5.72.1 to 5.73.0 in /install (#10677) (6239a1ff) + * bump nodebb-theme-slick from 2.0.1 to 2.0.2 in /install (557648e6) +* **deps-dev:** + * bump @commitlint/config-angular in /install (e5e788d2) + * bump eslint from 8.16.0 to 8.17.0 in /install (52c86dba) +* incrementing version number - v2.1.0 (a3114d33) +* update changelog for v2.1.0 (7121949d) + +##### Documentation Changes + +* fix changelog to more accurately reflect new changes (a9744416) + +##### Bug Fixes + +* #10675, don't ajaxify to /assets/ urls (48564cfa) + +##### Other Changes + +* //github.com/pichalite/nodebb-theme-slick/issues/60 (6a0339de) + +#### v2.1.0 (2022-06-02) + +##### Chores + +* update changelog for v2.1.0 (7121949d) +* incrementing version number - v2.1.0 (987045c3) +* bump persona (91f32487) +* update changelog for v2.0.1 (abd8e216) +* incrementing version number - v2.0.1 (768427d4) +* remove optional `middleware` parameter from setupPageRoute and setupAdminPageRoute calls (54ff768f) +* add note re: sort module (5aca106b) +* up emoji (265a0139) +* update changelog for v2.0.0 (60fc3f5d) +* incrementing version number - v2.0.0 (f23c3ff5) +* **deps:** + * bump nodebb-plugin-2factor from 4.0.1 to 5.0.0 in /install (6e95e5c5) + * update dependency lint-staged to v12.5.0 (01f607a5) + * update dependency lint-staged to v12.4.3 (8885d228) + * update dependency eslint to v8.16.0 (#10654) (f6728404) + * bump sharp from 0.30.4 to 0.30.5 in /install (#10651) (d9f2096d) + * bump postcss from 8.4.13 to 8.4.14 in /install (#10652) (0772ec07) + * bump socket.io-client from 4.5.0 to 4.5.1 in /install (#10653) (a13a523b) + * update dependency lint-staged to v12.4.2 (#10647) (18e76c21) + * bump nodebb-plugin-mentions in /install (#10648) (e894147c) + * bump helmet from 5.0.2 to 5.1.0 in /install (#10641) (5faaf6a1) + * bump socket.io from 4.5.0 to 4.5.1 in /install (#10639) (9d9b3f4e) + * bump ioredis from 5.0.4 to 5.0.5 in /install (#10637) (f9c9ac9c) + * bump ace-builds from 1.4.14 to 1.5.1 in /install (#10636) (acf188b0) + * bump yargs from 17.4.1 to 17.5.1 in /install (#10624) (c7aefe9c) + * update dependency @commitlint/cli to v16.3.0 (1dc96717) + * bump mongodb from 4.5.0 to 4.6.0 in /install (#10603) (aee74bd6) + * bump express-session from 1.17.2 to 1.17.3 in /install (#10604) (b1967681) + * bump webpack from 5.72.0 to 5.72.1 in /install (#10600) (06edb6e6) +* **deps-dev:** + * bump @commitlint/cli from 17.0.1 to 17.0.2 in /install (#10672) (5f6e9f67) + * bump @commitlint/cli from 16.3.0 to 17.0.1 in /install (#10644) (fe873182) + * bump @commitlint/config-angular in /install (#10614) (9c4ef133) + * bump husky from 8.0.0 to 8.0.1 in /install (#10595) (54876583) + * bump grunt from 1.5.2 to 1.5.3 in /install (fcb6c191) + * bump husky from 7.0.4 to 8.0.0 in /install (#10591) (03453a62) + * bump eslint from 8.14.0 to 8.15.0 in /install (#10592) (0d75c6cb) +* **i18n:** + * fallback strings for new resources: nodebb.user (2bace634) + * fallback strings for new resources: nodebb.admin-manage-admins-mods (192aa2d3) + +##### New Features + +* add clipboard to runtime modules (39d61061) +* add mute history, closes #10596 (c926358d) +* allow unban/unmute on flag details page. closes #10593 (9acdc680) +* paginaton for admins-mods, closes #10610 (b860c260) +* fix typo, show route in deprecation notice for third-param removal in `setupPageRoute` (14110596) +* add button to see category children, closes #10606 (a5831412) +* closes #10601, ability to prevent alerts on topic list (dc320c89) +* add post to hook params (f07b4484) +* add sorted-list.parse (e904f438) +* add new hook that fires when sorted-set list modal is shown (8faa6f23) +* #10585, ability to mute from flag details (7867ccd7) +* send back missing parameters as array of missing properties, in API response (0c19b1e5) +* add hook for user invite (323dbc97) + +##### Bug Fixes + +* **deps:** + * update dependency ace-builds to v1.5.3 (#10667) (e7fd1861) + * update dependency ioredis to v5.0.6 (#10668) (5bafab79) + * update dependency nodebb-theme-persona to v12.0.8 (#10669) (e47a63f0) + * update dependency nodebb-plugin-2factor to v4.0.1 (#10665) (356f9a18) + * update dependency ace-builds to v1.5.2 (#10664) (7b5f53fc) + * update dependency nodebb-theme-persona to v12.0.7 (#10666) (a9a26836) + * update dependency sharp to v0.30.6 (#10662) (23232508) + * update dependency nodebb-theme-persona to v12.0.5 (#10649) (1fd68281) + * update dependency cron to v2 (#10568) (ad370202) + * update dependency clipboard to v2.0.11 (#10574) (db67a50a) + * update dependency nodebb-theme-slick to v2.0.1 (#10578) (d98f4ea9) + * update dependency nodebb-theme-persona to v12.0.2 (#10577) (930aefcf) + * update dependency nodemailer to v6.7.5 (#10573) (6eef08f9) +* buildBreadcrumbs naively prepending relative path even if absolute paths are passed in (a3564260) +* more generic copy for notif/chat button labels (f6a7582c) +* #10642, fix order of dom ready events (2bfccac7) +* get rid of math.random in utils.generateUUID (e802fab8) +* #10528, gray out disabled nav items (7e4d2852) +* encode privilege name for API call (92abb352) +* #10631, fix user digest setting display acp (0084b563) +* translate api error messages to user lang, closes #10623, (b17a81bf) +* alert template error. closes #10620 (24a640d9) +* closes #10621, convert \r\n to \n so it isn't counted as 2 characters (3a009f96) +* remove ev, hooks don't pass event, closes #10611 (082a9e1d) +* 'unread' postIndex regression closes #10607 (0e60a704) +* clear dragging on mouseup as well (70ad4a52) +* #10588 exit code 1 on failed plugin activation (e2ff1e39) +* #10584, dont show backlinks if you dont have read privilege (5e7d366f) +* #10586, fix webinstaller folders (1928a186) +* closes #10583, replace removed socket method with api method (5e82cf23) +* add missing fs-extra, #10580 (e7077393) + +##### Other Changes + +* unnecessary escape (60eeae95) +* fix semicolon (3a77e714) + +##### Refactors + +* return module if it doesn't have default export (12b58fcf) +* deprecate middleware param (#10513) (84f27263) + +##### Tests + +* fix tests again (191fb9f4) +* fix UUID test (6677efd7) + +#### v2.0.1 (2022-05-28) + +##### Bug Fixes + +* get rid of math.random in utils.generateUUID (a4ab49c2) + +#### v2.0.0 (2022-05-04) + +##### Chores + +* incrementing version number - v2.0.0 (f23c3ff5) +* update changelog for v1.19.7 (a764df52) +* **deps:** + * update dependency mocha to v10 (a7986773) + * bump nodebb-plugin-dbsearch from 5.1.3 to 5.1.4 in /install (#10545) (a2e263a1) + * update dependency smtp-server to v3.11.0 (62f1c78a) +* **i18n:** + * fallback strings for new resources: nodebb.post-queue (d617c665) + * fallback strings for new resources: nodebb.post-queue (3492dd11) + * fallback strings for new resources: nodebb.error, nodebb.flags (72d47a0b) + +##### New Features + +* show number of selected posts in reject confirm (012860a4) +* post queue bulk actions closes #10520, fix #10555, (23175110) +* output canonical URL as last line on NodeBB boot (e4a9c078) + +##### Bug Fixes + +* **deps:** + * update dependency @socket.io/redis-adapter to v7.2.0 (#10571) (c3c77915) + * update dependency autoprefixer to v10.4.7 (#10563) (68168a7d) + * update dependency nodebb-theme-persona to v12.0.1 (#10561) (1d446e14) + * update dependency nodebb-plugin-mentions to v3.0.10 (#10560) (eb3c398e) + * update dependency nodebb-plugin-dbsearch to v5.1.5 (#10559) (c3ff28ff) + * update dependency nodebb-plugin-mentions to v3.0.9 (#10554) (1acbe4c1) + * update dependency postcss to v8.4.13 (#10553) (6217db00) + * update dependency express to v4.18.1 (#10550) (b0dc5615) + * pin dependency webpack to 5.72.0 (#10549) (672ab25a) + * update dependency cron to v1.8.3 (#10543) (d6843294) + * update dependency bootbox to v5.5.3 (#10531) (8d47f352) + * update dependency nodemailer to v6.7.4 (#10540) (afefee08) +* #10569, fix category move event text (ea01ba01) + +##### Refactors + +* not used anymore (d2e6f317) + +#### v1.19.7 (2022-04-28) + +##### Breaking Changes + +* #10443, regression where sorted-list items did not render into the DOM in the predicted order [breaking] (46fbe156) + +##### Chores + +* incrementing version number - v1.19.7 (0c4850e2) +* update changelog for v1.19.6 (acca7811) +* **deps:** + * update commitlint monorepo to v16.2.4 (0d9179f7) + * update dependency lint-staged to v12.4.1 (#10527) (804542e4) + * bump socket.io from 4.4.1 to 4.5.0 in /install (#10523) (0c2d015c) + * update dependency eslint to v8.14.0 (#10514) (40b7ff4b) + * update dependency lint-staged to v12.4.0 (fff818c6) + * update dependency lint-staged to v12.3.8 (7c1d98c7) +* **i18n:** fallback strings for new resources: nodebb.admin-settings-reputation, nodebb.error (01e65395) + +##### New Features + +* make it simpler to use redis sentinels (82389469) +* closes #10501, minimum reputation to chat (b28f9f77) + +##### Bug Fixes + +* typo in hook name (21dbd476) +* upload test for latest sharp (424db9ff) +* #10502, allow unblocking admin/mod (b9f91643) +* **deps:** + * update dependency express to v4.18.0 (#10526) (5ae690a6) + * update socket.io packages to v4.5.0 (#10522) (d86c447a) + * update dependency autoprefixer to v10.4.5 (#10521) (5ce4c874) + * update dependency nodebb-plugin-2factor to v3.0.7 (#10510) (b81a0cfe) + * update dependency nodebb-widget-essentials to v5.0.11 (#10517) (a767d623) + * update dependency sharp to v0.30.4 (#10504) (a36911fc) + * update dependency prompt to v1.3.0 (#10487) (07678fb5) + * update dependency nconf to v0.12.0 (#10496) (fe492b11) + * update dependency archiver to v5.3.1 (#10500) (6c727859) + +##### Refactors + +* closes #10509 (7f241dbb) +* show invalid uri (ac125538) +* skip content length check if submitting from post-queue (a8e642ad) + +##### Tests + +* remove node18 until nodemailer is fixed (50658a82) +* remove node 12, add 18 (976914e7) + +#### v1.19.6 (2022-04-13) + +##### Chores + +* incrementing version number - v1.19.6 (283a0072) +* update changelog for v1.19.5 (05032ca2) +* **deps:** + * bump semver from 7.3.6 to 7.3.7 in /install (#10493) (65cec8d0) + * update dependency grunt to v1.5.2 (9f496659) + * update dependency grunt to v1.5.1 (0a8bf44e) + * update dependency grunt to v1.5.0 (e82d8bb6) + * update dependency eslint to v8.13.0 (bc3aabb4) + * update dependency eslint-plugin-import to v2.26.0 (a2ebf53b) + * bump nodebb-theme-persona from 11.4.3 to 11.4.4 in /install (#10437) (5b1789c1) + * bump nodebb-plugin-composer-default in /install (#10438) (c8c42933) + * update dependency eslint to v8.12.0 (a6590e20) + * update dependency lint-staged to v12.3.7 (#10407) (5f36ad39) +* **i18n:** fallback strings for new resources: nodebb.post-queue (a06d1246) + +##### New Features + +* #10460, add cutoff to suggested topics (799e94e0) +* add response:helpers.notAllowed (e8058ca3) +* add filter:image.stripEXIF (b8765df5) +* add confirm to reject, closes #10427 (538ad9e1) +* allow client-side hook registration chaining (b88bb3cf) +* delete flagId field from post/user on flag purge (31251282) +* add flags.purge (3b529b84) +* new admin events, closes #10405 (421ba6e1) +* add Albanian localisation :tada: (309968bf) + +##### Bug Fixes + +* dont add caller to arrays (5316029f) +* #10491, don't leak deleted message in cleanedContent (c52401da) +* #10473, trim trailing slashes on config url (9f91db16) +* closes #10436, fix DST issue on acp dashboard (39877763) +* delete history as well (002a241c) +* handle purge posts as well (93b60532) +* byCid removal, targetCid not stored in flagObj (03fdb5be) +* upgrade script (55be4202) +* column counts for other privileges (2b9b2b4a) +* **deps:** + * update dependency nodebb-plugin-mentions to v3.0.8 (#10490) (7c733e9c) + * update dependency winston to v3.7.2 (#10454) (dc03a2f9) + * update dependency nconf to v0.11.4 (#10481) (f253bbdd) + * update dependency yargs to v17.4.1 (#10480) (bbf2b73e) + * update dependency ioredis to v5.0.4 (#10479) (88200ec1) + * update dependency html-to-text to v8.2.0 (#10471) (91026e5f) + * update dependency semver to v7.3.6 (#10466) (c50de911) + * update dependency html-to-text to v8.1.1 (#10470) (b3ec8059) + * update dependency nodebb-widget-essentials to v5.0.10 (#10461) (b6517cfd) + * update dependency body-parser to v1.20.0 (#10450) (26511185) + * update dependency spdx-license-list to v6.5.0 (#10452) (5e37f34e) + * update dependency graceful-fs to v4.2.10 (#10457) (947fa193) + * update dependency mongodb to v4.5.0 (#10458) (30f728ca) + * update dependency ioredis to v5.0.3 (#10446) (0d744d30) + * update dependency ioredis to v5 (#10434) (060ad1b0) + * update dependency nodebb-plugin-2factor to v3.0.6 (#10435) (0ac426e0) + * update dependency nodebb-plugin-composer-default to v7.0.21 (#10429) (898e0e89) + * update dependency nodebb-plugin-spam-be-gone to v0.8.1 (#10425) (47399bfe) + * update dependency sortablejs to v1.15.0 (#10418) (12cd1df2) + * update dependency nodemailer to v6.7.3 (#10421) (7e542495) + * update dependency yargs to v17.4.0 (#10416) (eaa05517) + * update dependency nodebb-theme-persona to v11.4.3 (#10414) (720a9dba) + * update dependency connect-redis to v6.1.3 (#10390) (06594131) +* **security:** + * explicitly declare cache-control header instead of using middleware (38ca73c4) + * cache-control on all pages using setupPageRoute or setupApiRoute, and 404 controllers. (1f6f389f) + * explicitly set cache-control 'private' on any page where a header is built (e39cdd49) + +##### Performance Improvements + +* WIP #10449, allow array of pids for posts.purge (#10465) (76797371) +* #10410, faster upgrade script (dab22d5f) + +##### Refactors + +* replace deprecated String.prototype.substr() (#10432) (200f0b2e) +* remove some verbose logging (9abe22a0) +* :trollface: (29b86b32) +* shorter generateTopicClass (f76c0e89) + +#### v1.19.5 (2022-03-16) + +##### Chores + +* incrementing version number - v1.19.5 (48d6eb4f) +* update changelog for v1.19.4 (0e6e49b2) +* **deps:** + * bump less from 3.13.1 to 4.1.2 in /install (#9856) (d33485f6) + * bump autoprefixer from 10.4.2 to 10.4.4 in /install (#10403) (90094935) + * update dependency lint-staged to v12.3.6 (0a4522a2) + * update commitlint monorepo to v16.2.3 (0a97015d) + * bump nodebb-plugin-spam-be-gone in /install (#10387) (445e3d70) + * bump connect-redis from 6.1.1 to 6.1.2 in /install (#10391) (145621f7) + * update dependency eslint to v8.11.0 (feaf3068) + * update dependency mocha to v9.2.2 (#10383) (4ffbd78d) +* **i18n:** + * fallback strings for new resources: nodebb.admin-manage-users (2f09c22c) + * fallback strings for new resources: nodebb.admin-manage-privileges, nodebb.admin-manage-users, nodebb.error, nodebb.user (15508bac) + * fallback strings for new resources: nodebb.admin-settings-reputation, nodebb.error (5274a6aa) + +##### New Features + +* collect hook logs in order to reduce console noise, flush on ajaxify loadScript completion (935704a8) +* add support for PATCH method in api module (4b79dfd2) +* on online users page override timeago cutoff to 24 hours (7c946570) +* ability to mute users (be6bbabd) +* min:rep:upvote, and other limits similar to downvotes (3414a23b) +* post-queue hooks, closes #10381 (2056ac04) + +##### Bug Fixes + +* topic events if there is a blocked user in topic (3935a86b) +* topic events disappearing if there are queued posts (2808c952) +* #10393, move 'Create User' control to overflow menu (cd687cff) +* don't append to history on refresh or ajaxify to same url (c83987bd) +* global privs (7d063d73) +* #10384 -- mixed up sizes for fallback touch icons (cb113208) +* #10377, remove logging of env vars (997ab7d4) +* **deps:** + * update dependency postcss to v8.4.12 (#10396) (bdbc168d) + * update dependency sharp to v0.30.3 (#10389) (b4213859) + +##### Refactors + +* closes #10301 (c8e986d6) + +##### Tests + +* skip i18n tests if the github event is a pull request (e578c605) +* fix middleware test (24c1f879) +* fix category tests (6344c3b6) +* fix one more test (a5511425) + +#### v1.19.4 (2022-03-09) + +##### Chores + +* incrementing version number - v1.19.4 (67282057) +* delay `filter:email.send` removal to v2.0.0 (83fd4311) +* up persona (c23b2089) +* incrementing version number - v1.19.3 (09cb11c8) +* update changelog for v1.19.3 (0b48ec54) +* **deps:** + * bump postcss from 8.4.7 to 8.4.8 in /install (#10372) (d7a4ae1f) + * update dependency lint-staged to v12.3.5 (517ae926) + * bump nodebb-plugin-spam-be-gone in /install (81e7ca20) + * update dependency eslint to v8.10.0 (e83c8be2) + * bump postcss from 8.4.6 to 8.4.7 in /install (52ee5ce8) + * bump json2csv from 5.0.6 to 5.0.7 in /install (e44cbb24) + * update dependency smtp-server to v3.10.0 (67e4df78) + * update dependency mocha to v9.2.1 (00eebf10) + * bump prompt from 1.2.1 to 1.2.2 in /install (21913b5b) + * bump express from 4.17.2 to 4.17.3 in /install (5321ba4d) +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-reputation, nodebb.flags (a5fe8350) + * fallback strings for new resources: nodebb.topic (dc0f9a73) + +##### New Features + +* add hook filter:posts.getUserInfoForPosts (df46ab48) +* add translateKeys (e841d59a) +* post auto flagging on downvotes #10029 (#10367) (62187caa) +* closes #10324, show recently online users as well (eac9cd03) +* resolve paths for staticDirs as well (e0b1c374) +* ability to go through your posts in a topic (b517b376) +* **sorted-list:** add new client-side hook `filter:settings.sorted-list.loadItem` (92d613e4) + +##### Bug Fixes + +* apply some DRY (a3b4c668) +* optional params (e9a86cb9) +* #10374, use quick search in setting (70e613f8) +* #10366, remove dupe /files (371b4658) +* always show self on /users?online (56345777) +* db call (21cd1e61) +* lastonline timestamps and display for guests (cc665fd6) +* #10357 (7ee4e4e0) +* #10358, bad uploads path (5479f364) +* #10360, only take top level posts (37ef8366) +* #10354, flag actions regression (fec907d9) +* dont overwrite asset_base_url if its set (0e12f82d) +* allow calls to api module without a defined payload (914733e4) +* #10334, use the correct env vars for web install (6b22d0e1) +* persona test fail (10a5901e) +* #10316, fix quoting regression (0b813d60) +* #10322 (5694e62e) +* #10329, select elements in sorted-list not showing proper values (1fa41342) +* allow translation keys in `label` attributes (52836f3a) +* don't load setup.json into nconf if setup.json doesn't exist (6e156daa) +* regression caused by 94b79ce4024f72a3eee2cfa06b05d8f66898149f (4164898d) +* **deps:** + * update dependency nodebb-plugin-markdown to v9.0.10 (bcb68ee9) + * update dependency mongodb to v4.4.1 (#10364) (4b730df9) + * update dependency nodebb-theme-persona to v11.4.2 (#10361) (4d590f65) + * update dependency body-parser to v1.19.2 (#10298) (c75714b7) + * update dependency sharp to v0.30.2 (#10359) (1a6c2c55) + * update dependency nodebb-plugin-mentions to v3.0.7 (#10355) (3e5a0f0d) + * update dependency nodebb-plugin-2factor to v3.0.5 (f30c65a4) + * update dependency nodebb-theme-persona to v11.4.1 (#10337) (b84e61d5) + * update dependency nodebb-theme-persona to v11.4.0 (#10325) (244d8801) + * update dependency nodebb-plugin-mentions to v3.0.6 (#10328) (31cbff19) + * update dependency nodebb-plugin-dbsearch to v5.1.3 (#10330) (1c1062e1) + * update dependency nodebb-plugin-markdown to v9.0.8 (#10327) (c17b9bd4) + * update dependency mongodb to v4.4.0 (#10319) (afd2993d) + * update dependency nodebb-plugin-emoji to v3.5.17 (#10314) (04900291) + * update dependency nodebb-plugin-dbsearch to v5.1.2 (#10313) (0e30362b) + * update dependency nodebb-widget-essentials to v5.0.9 (#10307) (5666c103) + * update dependency nodebb-widget-essentials to v5.0.8 (#10306) (636f1baf) +* **sorted-list:** + * call loadItem hook on add/edit items as well as on item retrieval, refactor edit to call parse() (1c8d1d23) + * only call `.stripHTMLTags()` on string values (fb4f89f3) + +##### Refactors + +* show a louder deprecation notice, alert once for each hook, not per plugin per hook (93b80f17) +* move header unread code to separate module (40230725) +* remove code that doesn't do anything (4a1e761a) +* wrap around if at end (3acd2ac8) +* change lang string (94961196) + +##### Tests + +* possible fix random psql test failure (50ed3a32) +* log configJSON (9db90a30) +* check contents of config.json in tests (32f69301) + +#### v1.19.3 (2022-02-16) + +##### Chores + +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-uploads (4043f179) + * fallback strings for new resources: nodebb.user (775d9077) +* **deps:** + * update dependency lint-staged to v12.3.4 (9577ef8d) + * update commitlint monorepo to v16.2.1 (2290cee5) + * update dependency eslint to v8.9.0 (763cd193) +* remove punycode dep (e9cb1452) +* incrementing version number - v1.19.2 (e49b31f0) +* update changelog for v1.19.2 (f012984d) + +##### New Features + +* delete diffs on post purge, closes #10291 (e9e48a75) +* closes #10296 (58b5781c) +* deleting a user upload dissociates from posts, and vice versa (d5ed8736) +* #10276, dont change/revert theme if its current (398d25c2) + +##### Bug Fixes + +* #10302, fix regression (503e27f7) +* one last try (9205169f) +* doggy.gif (2f64d633) +* one more fix (cfdfbf32) +* dupe key errors (770fcd9e) +* #10292, delete missing fields (dbf7a458) +* local deleteUploads() method in `src/user/delete.js` to call `User.deleteUpload()` (b9edee14) +* #10144, automatically delete uploads from disk on post purge, ACP option to keep uploads on disk if desired (84dfda59) +* four-space indents in package.json (9aa3e442) +* #10289, remove lodash dependency in src/cli/package-install.js (81fa2e22) +* non-functional upgrade script (1c7fb8fe) +* language keys (350052ec) +* #10273, properly calculate item count for best/controversial (d70ce3a9) +* **deps:** + * update dependency nodebb-plugin-emoji to v3.5.16 (#10297) (b47ca86d) + * update dependency nodebb-plugin-markdown to v9.0.7 (#10293) (5b0d4a8e) + * update dependency nodebb-plugin-emoji to v3.5.14 (#10295) (7af057fa) + * update dependency nodebb-plugin-mentions to v3.0.5 (#10294) (55a98183) + * update dependency winston to v3.6.0 (#10285) (22da7a10) + * update dependency nodebb-plugin-markdown to v9.0.6 (3225a1af) + * update dependency nodebb-plugin-spam-be-gone to v0.7.13 (#10280) (3dc108d3) + * update dependency nodebb-plugin-emoji to v3.5.12 (#10279) (2c0b6322) + * update dependency nodebb-plugin-emoji to v3.5.11 (#10274) (766ef4e5) + +##### Refactors + +* lazy load slugify (946d351f) +* .deleteUpload() to accept array of paths (7ef9c7d2) +* fix user uploads paths, and associate uid with user uploads (ea36016d) +* change the post uploads' hash seeds to have the `files/` prefix (6489e9fd) +* abstract some common code out to local utility methods (aad0c5fd) +* move post upload tests to its own file (d92da828) +* remove extra zset remove, closes #10277 (489c0d30) + +##### Code Style Changes + +* linting errors (5d7e1ebc) + +##### Tests + +* fix topic thumb tests and topic thumbs to work properly with post upload assoc. (fb78570c) +* user uploads.js tests (8c2752ba) +* testing user upload methods, already fixed one bug (11275d68) + +#### v1.19.2 (2022-02-09) + +##### Chores + +* up persona (14ecafb6) +* up markdown (8a4b7dc4) +* add missing quote (b98758d3) +* use source and current local vars, + docs (9e4147f0) +* up persona (1eaae1d0) +* up emoji (106ef7cf) +* persona (3b4cf971) +* persona (78db61cf) +* up deps (c7a56439) +* add punycode dependency (452f29c0) +* up persona (d50d4a9e) +* up persona (458606bc) +* up persona (cfe53305) +* up persona (f29bed27) +* up packages (b4a4e60e) +* up persona (3e30b6cd) +* incrementing version number - v1.19.1 (7f450268) +* update changelog for v1.19.1 (55df683a) +* **deps:** + * bump ioredis from 4.28.4 to 4.28.5 in /install (#10254) (b496ad44) + * bump nodebb-widget-essentials in /install (#10219) (b71025ce) + * update dependency lint-staged to v12.3.3 (6ba25557) + * update dependency eslint to v8.8.0 (153693e0) + * bump nodebb-theme-persona in /install (#10199) (2db54e67) + * update dependency lint-staged to v12.3.2 (814cb66b) + * update dependency mocha to v9.2.0 (05e2b354) + * bump helmet from 5.0.1 to 5.0.2 in /install (1f037bf6) + * update dependency lint-staged to v12.3.1 (ac244af3) + * update dependency lint-staged to v12.3.0 (7060837b) + * bump helmet from 4.6.0 to 5.0.1 in /install (5d3900dc) +* **i18n:** + * fallback strings for new resources: nodebb.modules (a71b8e59) + * fallback strings for new resources: nodebb.global, nodebb.pages (aa812f03) + * fallback strings for new resources: nodebb.users (70eeb204) + * fallback strings for new resources: nodebb.admin-settings-email (e9588ca7) + * fallback strings for new resources: nodebb.admin-settings-advanced (2ec4e31f) + +##### Documentation Changes + +* openapi spec for new route (9b912db7) +* some tweaks to cli help (c869d7db) + +##### New Features + +* handle array of keys in psql exists for zsets (5143ca33) +* upgrade script to clean up leftover :thumb zsets (0ac28435) +* more tests for ensuring downvoted posts are added to the :votes zset (1b8eeaf8) +* upgrade script to store downvotes posts in the user :votes sorted set (cf88483f) +* new accounts route to show most downvoted ('controversial') posts (5afd5de0) +* v3 user email tests (aa8914a1) +* allow gif profile images, sharp 0.30.0 supports gifs (7f1c4477) +* detect alternative package managers based on lockfile (8ba9e67c) +* new language key for user search in chat (766ad6b7) +* remove colors in favour of chalk (#10142) (cf8f62ae) +* add upload helper module for drag&drop, paste, closes #6388 (cf5c0968) +* no more sending emails to banned users, + feature flag (ea27eaf1) +* push the theme name into body class (e1e1d522) +* add ACP toggles for COEP and CORP headers (d91aeea3) + +##### Bug Fixes + +* **deps:** + * update dependency sharp to v0.30.1 (#10270) (8e52abe8) + * update dependency nodebb-widget-essentials to v5.0.7 (#10269) (6c0f7034) + * update dependency nodebb-theme-persona to v11.3.37 (#10265) (78d48c37) + * update dependency ioredis to v4.28.5 (#10252) (721a70c0) + * update dependency connect-redis to v6.1.1 (#10260) (a10e4940) + * update dependency nodebb-theme-persona to v11.3.36 (#10253) (0e2a4a2d) + * update dependency nodebb-theme-persona to v11.3.35 (#10251) (6465e012) + * update dependency pg-cursor to v2.7.3 (#10244) (e6185883) + * update dependency nodebb-theme-persona to v11.3.33 (#10248) (32477676) + * update dependency nodebb-theme-vanilla to v12.1.17 (#10249) (8f5b5ef1) + * update dependency nodebb-plugin-emoji to v3.5.9 (#10250) (1eb0939e) + * update dependency sanitize-html to v2.7.0 (#10246) (845717b8) + * update dependency pg to v8.7.3 (#10243) (531a3b1e) + * update dependency connect-redis to v6.1.0 (#10245) (c343b631) + * update dependency nodebb-theme-persona to v11.3.31 (#10241) (f1bed441) + * update dependency nodebb-plugin-composer-default to v7.0.20 (#10231) (a4702959) + * update dependency nodebb-theme-persona to v11.3.30 (#10232) (916a0db3) + * update dependency nodebb-plugin-emoji to v3.5.8 (#10239) (ebf4e12b) + * update dependency sharp to v0.30.0 (#10221) (2924cd3b) + * update dependency ioredis to v4.28.4 (#10224) (cda07cb7) + * update dependency clipboard to v2.0.10 (2c605d1c) + * update dependency sitemap to v7.1.1 (1bf938da) + * update dependency winston to v3.5.1 (b0dd68bb) + * pin dependency punycode to 2.1.1 (e7ba24c5) + * update dependency postcss to v8.4.6 (322f1033) + * update dependency nodebb-plugin-markdown to v9 (7d5080cd) + * update dependency ace-builds to v1.4.14 (#10200) (c50f6512) + * update dependency winston to v3.5.0 (#10202) (a7f142be) + * update dependency clipboard to v2.0.9 (#10203) (c6164e48) +* remove extraneous devDependencies on package merge (a2c7d69e) +* #10257, topic thumbs not deleting on topic deletion (0f788b8e) +* #10256, allow quote tooltip on mobile (fb3f4f9a) +* #10255, create verified/unverified groups on install (08f2a050) +* controversial posts/bests posts not showing anything (079c487d) +* regression in package.json merging logic that caused extraneous packages to not be removed (d34471f6) +* #10229, package merging should deep merge nested objects (689c125c) +* use fs.promises (a0a38706) +* bug where .reduce() exploded due to no initial value, if input value was an empty array (5cff6e3f) +* https://github.com/NodeBB/NodeBB/issues/10242 (dcb201df) +* missing early return (ad635175) +* handle case where email is explicitly passed into user.create, and thus is set in user hash, but confirmation request may have expired (936562c3) +* #10236, don't check email:uid, instead verify an email confirmation is active (0322e984) +* don't crash if requestedFields is undefined (98839108) +* a missed invocation of colors (c3d926ff) +* proactively guard against homograph characters in website values (fa7dcdb9) +* #10208, don't use leading slash in directory names (1d01741a) +* don't crash if quick search doesn't return posts (93d18383) +* properly unregister hooks in emailer tests (fc2c755c) +* email ban tests (dee9cca3) +* update usage of emailer.send to not catch (as errors are no longer thrown), email error throttler (d4e5259f) +* derp (b3f7b742) +* bug where page wouldn't complete loading if data.scripts was emptied (578145ac) +* use escaped group names in invite modal (2a89ad82) +* https://github.com/julianlam/nodebb-plugin-mentions/issues/170 (dc6e629d) +* #10197, fix relative path urls for dashboard pages (92a249c9) +* actually, CORP is ok (df8c8ad8) +* update defaults for corp and coep to be more permissive, for now, to be reverted for v1.20.0 (4467299e) +* if no group label is selected, select no group title option (94da5026) + +##### Other Changes + +* remove unused require (6be330f2) + +##### Performance Improvements + +* increase batch size (b548083b) + +##### Refactors + +* update chat plcaeholder message (fbd9ba79) +* updated package-install.js exports style, new exported method 'getPackageManager' for use in cases where nconf is unreliable, fix bug where nconf was not correctly set up in cli tools, proper installation of dev dependencies based on global env value (9a169085) +* emailer.send and emailer.sendToEmail returns Boolean based on message being successfully sent (f0e32ff1) +* sorted-list .get() to be async fn (89b559a2) + +##### Tests + +* fix occasional test failure (2dbdd181) +* add test to verify that a sorted set is automatically deleted if its last element is removed (#10261) (60680876) +* stricter isValidationPending check (d1b1f50b) +* fix derp (680e36da) +* up acp plugin page timeout (a214f9a6) + +#### v1.19.1 (2022-01-21) + +##### Chores + +* **deps:** + * bump compare-versions from 4.1.2 to 4.1.3 in /install (#10154) (4a5182e4) + * update dependency lint-staged to v12.2.2 (f090de36) + * update dependency @commitlint/cli to v16.1.0 (44d81a95) + * update dependency lint-staged to v12.2.1 (857ac480) + * update dependency @commitlint/cli to v16.0.3 (9c63d076) + * update dependency lint-staged to v12.2.0 (1a0c117d) + * update dependency eslint to v8.7.0 (8abaf3f6) + * update dependency mocha to v9.1.4 (f5ad173b) +* up persona (89871d71) +* add test.sh to gitignore (d7796f0b) +* delete test script (250274c7) +* remove unused lang key #10180 (2fe91e36) +* incrementing version number - v1.19.0 (18b0a29f) +* update changelog for v1.19.0 (c8f1bc53) +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-user (78cd6de7) + * fallback strings for new resources: nodebb.user (3ec9197c) + * fallback strings for new resources: nodebb.email (354aa1a5) + +##### New Features + +* revert label js change (1c80adf1) +* hide all categories link on flags filters (e9d0db28) +* nicer error handling for bad jwt in unsubscribe template (31ea2266) + +##### Bug Fixes + +* **deps:** + * update dependency jquery-ui to v1.13.1 (#10186) (55b3a355) + * update dependency multiparty to v4.2.3 (#10188) (7fa2ba70) + * update dependency nodebb-theme-persona to v11.3.15 (#10155) (e03d4747) + * update dependency mongodb to v4.3.1 (#10178) (693ca1f1) + * update dependency nodebb-plugin-composer-default to v7.0.18 (#10164) (2c75cce6) +* urls used when recent/unread/popular/top is used as the homepage (28359665) +* mark unread button showing the selected category (fdfafb44) +* catch exceptions from sendNotifications (c1ec2047) +* multiple cid filter on flags page (3e8cb732) +* post indices if there is a blocked user in topic (f9279b63) +* dont add duplicate link backs (3b72ff87) +* #10175, fix topic linkbacks duplicating on post edit (b06c6329) +* #10173, don't show optional message if email is required (d1eb21c5) +* #10172, fix postqueue accept/reject notification text (6a75ed50) +* #10167, fix regression prevent guest post (4799efc7) +* CSRF handler to go back to saving in session (#10159) (e9ee843b) +* #10158, fix extra padding (03f5cbcc) + +##### Refactors + +* add method to error messages (c9fabb0e) + +##### Tests + +* add failing guest csrf test (#10169) (10949184) + +#### v1.19.0 (2022-01-13) + +##### Breaking Changes + +* `GET /chats/:roomId/users` [breaking] (6eea6451) +* remove socket.emit('user.exists') (1f063058) +* remove socket.emit('user.changeUsernameEmail') (6b45dee9) +* #10077, store nav items in objects (69c96078) +* remove socket.emit('topics.follow') (f918a381) +* remove socket.emit('topics.post') (6ad04721) +* remove socket.emit('user.banUsers'); (49641a32) +* remove socket.emit('posts.reply') (4604a572) +* remove socket.emit('user.changePicture') (6d95684b) +* remove socket.emit('user.search') (0d694c78) +* remove socketHelpers.setDefaultPostData (99f865c6) +* remove deprecated groups socket calls (02ea17ea) +* remove deprecated methods (0d1e57dc) +* remove socket.emit('posts.delete') (bd1cb2d4) +* remove socket.emit('posts.upvote') (ec1d5e38) +* remove socket.emit('posts.move') (8427c5d9) +* remove socket.emit('posts.edit') (4247f624) +* remove socket.emit('posts.bookmark/unbookmark') (f7418ccd) +* remove deprecated admin.groups methods (07e2741e) +* remove socket.emit('categories.getCategory') (45d755b6) +* remove socket.emit('admin.categories.setPrivilege') and socket.emit('admin.categories.getPrivilegeSettings') (cc3f82bc) +* remove deprecated socket.emit('admin.categories.update') (0b9c01f9) +* remove deprecated socket.emit('admin.categories.purge') (ba5d2e7b) +* remove deprecated socket.emit('admin.categories.create') (b3353723) +* remove deprecated socket.emit('admin.categories.getAll') (10c19af2) +* remove getTopicPosts(tid, ...) usage (170e5dd9) +* remove deprecated post diff socket calls (8117b7f2) +* remove deprecated user middlewares (1a7fffc7) +* remove action:category.loaded, use action:topics.loading (36aa6034) +* remove setTopicSort/setCategorySort (6dcdf1d3) +* remove deprecated socket user create/delete functions (a7d1dfb6) +* remove deprecated uploads.delete (c93d7fdb) +* remove deprecated methods (79de48c5) +* remove socket.io/flags.js (c5f08fdc) + +##### Chores + +* org; merge consecutive await calls into one Promise.all (be4dbe34) +* fix #9213 (0a5420ed) +* revert engines change in install/package.json (487f25ba) +* update renovate config (f95acce3) +* update renovate range strategy... (bc0f33df) +* remove debug logs in test/api.js (82768fcf) +* use apiv3 for room rename tests (e745e212) +* trigger lang (540eeae9) +* up themes (71fa8175) +* right dropdown (7aa85882) +* right dropdown (a998cc1c) +* update readme mongodb version (af5393ec) +* incrementing version number - v1.18.6 (3a78a151) +* update changelog for v1.18.6 (3c8109e2) +* **deps:** + * update commitlint monorepo (56d134c3) + * update dependency lint-staged to v12.1.7 (40e7007f) + * update dependency lint-staged to v12.1.6 (f78108ac) + * update dependency eslint-plugin-import to v2.25.4 (a69afdb6) + * update dependency lint-staged to v12.1.5 (12038039) + * update dependency eslint to v8.6.0 (b546ff4e) + * update dependency lint-staged to v12.1.4 (87779fc8) + * update dependency jsdom to v19 (#10053) (ee05b762) + * update dependency lint-staged to v12.1.3 (c0dd8dcb) + * update dependency eslint to v8.5.0 (55b9fab1) + * update dependency eslint to v8.4.1 (ba02f015) + * update dependency eslint to v8.4.0 (4b113715) + * update dependency lint-staged to v12 (b3423389) + * update dependency eslint to v8 (e9aadde1) + * update commitlint monorepo to v15 (f6c6425d) + * update dependency jsdom to v18.1.1 (87433b79) + * bump compare-versions from 3.6.0 to 4.1.1 in /install (ea9f2c73) + * update dependency jsdom to v18.1.0 (d7c2a311) +* **i18n:** + * fallback strings for new resources: nodebb.user (d79d7e80) + * fallback strings for new resources: nodebb.admin-settings-uploads (e6a46ef6) + * fallback strings for new resources: nodebb.error (207ae8cd) + * fallback strings for new resources: nodebb.post-queue (edba10e2) + * fallback strings for new resources: nodebb.modules, nodebb.notifications (fd939f8b) + * fallback strings for new resources: nodebb.admin-admin (4a53adf6) + * fallback strings for new resources: nodebb.admin-admin (b052a8e7) + * fallback strings for new resources: nodebb.topic (23c915ba) + * fallback strings for new resources: nodebb.admin-settings-user, nodebb.login (eecd02fb) + * fallback strings for new resources: nodebb.admin-settings-email (72e1c281) + +##### Documentation Changes + +* comment hint :shipit: (ffdf26af) +* fix description for route (47ab9526) +* roomId is number (0aa25f20) +* openAPI documentation for routes (55e68e2f) +* use social media card in readme header (33c8b197) +* add social media card (f7b3f69f) +* add docs link higher up (1a85aaad) +* update readme blurb (e325aa93) + +##### New Features + +* bundling nodebb-plugin-2factor with all NodeBB installations, v1.19.0 onwards :tada: (31b4b8fd) +* revoke user sessions on successful password reset (6ca216ab) +* allow % in tags, #10135 (a75a043b) +* allow isCluster, isPrimary, and jobsDisabled to be passed in as numbers in addition to string/bool (b5b188fd) +* `GET /api/v3/chats/:roomId/messages` (2fe53cf8) +* `DELETE /api/v3/chats/:roomId/users` and `DELETE /api/v3/chats/:roomId/users/:uid` (fe17c94c) +* `POST /chats/:roomId/users` (d62ee846) +* `POST /chats/:roomId/:mid` and `DELETE /chats/:roomId/:mid` (d5fd098e) +* middleware.assert.message (90fcbe44) +* `GET /chats/:roomId/:mid` (b2929605) +* `PUT /chats/:roomId/:mid` (f48ed365) +* allow API checkRequired middleware error to be internationalized (74f1905e) +* `PUT /api/v3/chats/:roomId` (9a4fd5dc) +* `POST /api/v3/chats/:roomId` (eeffb9d9) +* `GET /api/v3/chats/:roomId` (09cf9c77) +* `POST /api/v3/chats`, chat room creation, plus openAPI docs update (40b4544e) +* `GET /api/v3/chats` (94bead71) +* stub code for v3 chats api (02e878be) +* #9506, allow seeing and editing your queued posts (c4042c70) +* pass in all query params to category search filter (599bffd8) +* add data param to filter:categories.search (c4156bdd) +* ensure that all requests handled by NodeBB fall under the relative_path as configured (a3bdb706) +* show 10 members (d9c42c00) +* closes #10018 (1e535528) +* #10018 add href (06bfec88) +* #10018 , wip (0f14f23b) +* autocomplete for activate/reset (f0d192fb) +* #10008, add history entry for note deletion (c26870d2) +* #9957, don't remove existing fields form config.json (0532c1b2) +* add feature flag to disable verification emails, closes #9996 (09e0c6d5) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-plugin-markdown to v8.14.6 (ad6f6051) + * update dependency winston to v3.4.0 (#10150) (2fee1d07) + * update dependency ioredis to v4.28.3 (#10151) (bfad04ab) + * pin dependencies (da2bb340) + * pin dependency socket.io-adapter-cluster to 1.0.1 (#10146) (15bf3db2) + * update dependency nodebb-theme-vanilla to v12.1.15 (#10149) (2fa3cd0d) + * update dependency nodebb-theme-slick to v1.4.23 (#10148) (762de2d8) + * update dependency nodebb-theme-persona to v11.3.13 (#10147) (385d4c71) + * update dependency winston to v3.3.4 (4bada01b) + * update dependency autoprefixer to v10.4.2 (a47883e8) + * update dependency mongodb to v4.3.0 (50eb4556) + * update socket.io packages to v4.4.1 (9eb00cb2) + * update dependency nodebb-rewards-essentials to v0.2.1 (0a15c99e) + * update dependency autoprefixer to v10.4.1 (f1aa5f7d) + * update dependency yargs to v17.3.1 (5c3335b7) + * update dependency nodebb-theme-vanilla to v12.1.14 (#10102) (dd8b1f75) + * update dependency nodebb-theme-slick to v1.4.22 (#10101) (dd1633ff) + * update dependency nodebb-theme-lavender to v5.3.2 (#10099) (eccdfc9d) + * update dependency nodebb-theme-persona to v11.3.12 (#10100) (4f6fc10b) + * update dependency nodebb-theme-persona to v11.3.11 (32cbf760) + * update dependency nodebb-theme-slick to v1.4.21 (9253519c) + * update dependency nodebb-theme-slick to v1.4.20 (#10084) (019804e1) + * update dependency nodebb-theme-vanilla to v12.1.13 (#10085) (66c759ed) + * update dependency nodebb-theme-persona to v11.3.10 (#10083) (d276c9cd) + * update dependency nodebb-plugin-composer-default to v7.0.17 (#10082) (0ef9c7ab) + * update dependency nodebb-theme-vanilla to v12.1.12 (dda7c075) + * update dependency nodebb-theme-slick to v1.4.19 (4577600e) + * update dependency nodebb-theme-persona to v11.3.8 (5e2281c3) + * update dependency mongodb to v4.2.2 (0551a19e) + * update dependency postcss to v8.4.5 (16398dd9) + * update dependency compare-versions to v4.1.2 (de1ed01d) + * bump persona (16c88a97) + * bump persona (c90a8926) + * update dependency nodebb-plugin-mentions to v3.0.4 (#10063) (07217762) + * update dependency nodebb-plugin-spam-be-gone to v0.7.12 (#10064) (a5840c5a) + * update dependency nodebb-theme-slick to v1.4.18 (#10065) (5477cf32) + * update dependency nodebb-plugin-markdown to v8.14.5 (#10062) (60d778f0) + * update dependency nodebb-theme-persona to v11.3.5 (#10059) (8695d370) + * update dependency nodebb-plugin-dbsearch to v5.1.1 (70068d6c) + * update dependency nodebb-plugin-composer-default to v7.0.15 (84b03a34) + * update dependency nodebb-theme-persona to v11.3.3 (8cbe0df2) + * bump emoji plugin (14e35247) + * update dependency ioredis to v4.28.2 (597d826b) + * update dependency yargs to v17.3.0 (115de4e5) + * update dependency mime to v3 (#9963) (49813cce) + * update dependency mongodb to v4.2.1 (f72af319) + * update dependency @socket.io/redis-adapter to v7.1.0 (ed4b0cf7) + * update dependency postcss to v8.4.4 (b34b8aa9) + * update dependency postcss to v8.4.3 (2dadf786) + * update dependency postcss to v8.4.1 (6a273798) + * update dependency postcss to v8.4.0 (29345275) + * update dependency ioredis to v4.28.1 (9966a00f) + * update dependency nodebb-theme-persona to v11.3.1 (fdae6991) + * update socket.io packages to v4.4.0 (f05d308a) + * update dependency nodebb-theme-vanilla to v12.1.10 (68dddbd9) + * update dependency nodebb-theme-persona to v11.2.22 (3eb91a20) + * update dependency nodebb-plugin-mentions to v3.0.3 (5ec32c31) + * update dependency mongodb to v4.2.0 (#10011) (2378fc84) + * update dependency @socket.io/redis-adapter to v7.0.1 (aae7be02) + * update dependency sharp to v0.29.3 (46162537) +* lint (c9592e17) +* icon alignment issue that was also bugging me :shipit: (37a71291) +* #10143, add back ace editor searchbox (42caef7f) +* #10095, add login info to email interstitial (5eb02f59) +* #10121, fix error messages in user creation (8ad64ec0) +* #10115, fix chat autocomplete hook (69a7634a) +* notice links in subfolder installs (cc27a324) +* move authenticateRequest before interstitial and maintenance mode middlewares, allowed plugins to disable authentication on certain routes (d89fc44c) +* retry incrObjtFieldBy (07232a8c) +* use component instead of class name (b179f0fc) +* assertion check to ensure messages are in the room when editing/deleting, etc (d95b4ee2) +* rename language key for incorrect parameter type error (aeb43b9b) +* deprecate chats.leave (16ba16cd) +* re-allow kicking of the other party in a 1-to-1 chat (6294beea) +* isFinite check for room assertion, more test migrating (140f9d24) +* #10096, don't display preview for links if hostname doesnt match (a115b771) +* only render preview on topic page (a9f81215) +* don't crash server if analytics fails to save (8fb89d76) +* upgrade script to handle strings as well (e332acf3) +* #10090, remove left over necro posts messages (fdaf8274) +* #10086, if pidfile is empty delete (59214ca2) +* utils method (91e21ecc) +* prevent crash if groups is not valid json (cc0a087a) +* upgrade script (968d4616) +* delete left over nav items after removal of some (737e1c19) +* targetBlank/dropdown not getting cleared (10e890e4) +* prettier bulk method usage (3b0c42a5) +* bug where groupChat property was not set for chat rooms (42959df0) +* #9484 show user history only to admins and gmods (bc7707aa) +* no need to pass 'img' to teaser tags stripping as images are already converted to alt text (45c9dde3) +* #10069, don't modify fields array (a8afdc60) +* #10068, update data-index values after sort (236d4e80) +* #10068, baseIndex should be read before sort update fix off by one error (9af23351) +* tests (45d8157f) +* #10067, count posts instead of incr/decr (830166d1) +* handle start=0 stop=0 for topics.getTopicPosts (906dc567) +* wrong usage of is ACP (4f423610) +* don't throw on invalid session, just return early so socket is not opened. (fa01801f) +* #10052, dont use spaces in tag class name (a2953583) +* don't crash if tid is falsy (1cdb0b1e) +* regression from https://github.com/NodeBB/NodeBB/commit/27c05448e1532ce466658513af0e2ff65576b410 (50063fe2) +* keep query params on /me redirects (941ecaf8) +* 403/400/500 page not generating csrf_token (65c55041) +* error pages dont have config.csrf_token (3dd681eb) +* quote button staying on screen on slow computers (d378bf4c) +* removed unused var (e0caa5e0) +* #10027, properly auto confirm first user (2473d5d8) +* cli password reset (71e34be5) +* #10023, bump persona (a10ea03c) +* #10020, /api/post/upload returns v3 style response (242f8e95) +* consolidate plugin reset logic (449366ca) +* search crash (9245f71a) +* #10010, handle reverse sorting for topic events (d5bfd512) +* #10006, dont allow new rooms or adding to a room if target is blocked (047f031d) + +##### Other Changes + +* remove unused (afc75ba4) +* fix (682f6089) +* missing ; (20e76699) +* remove unused (cbf198fe) +* remove unused (a20abdce) +* fix (0a6eeb93) +* remove unused utils (82b72f7e) +* fix (fa1ac04d) + +##### Performance Improvements + +* only add middleware if relativePath is set (41db9436) +* don't load all set members to get count (0414356c) +* create user tooltips on demand (23147235) +* closes #9994, bulk methods for settings (d412ba44) +* remove createUserTooltips (facc10e4) + +##### Refactors + +* remove unused language key (5278b11a) +* put message api calls behind `/messages` prefix, #10097 (aaa6f752) +* rewrite messaging tests to use API v3 calls when available, rewrote a bunch of tests to async..await (c990211c) +* only write analytics data on nbb that has runJobs=true (35fea58a) +* remove knonwOwner param (42781467) +* remove console.log (a7644466) +* remove unused validator (4e1d4217) +* only pass qs (b8896d13) +* change error message (1e60ae87) +* use hasOwnProperty (9f1b8a3b) +* remove object.create (dbba0c39) +* remove unused args (3d3ae82f) +* remove comment (633061f4) +* use .map to return promises (d4f3ee67) +* async (cfd47448) +* async (f0394c49) +* use async (8491f67d) +* remove unused (e3c0f0be) +* always returns array (9627fa90) +* use async/await (f4aa249d) +* change var name (36eb47d9) +* alerts (621c142f) +* use alerts.error/success (cba78aee) +* use lang keys, fix typo (dd02c49f) +* use alerts module (1980feed) +* dont need local function (2bb0828a) +* DRY (324262cb) +* use routePrefixMap instead of routeRegexpMap, +tests (#10035) (6c07433d) +* use hasOwnProprety (daf385cd) +* tab rules (fb363957) +* change category feed so it is not updated on every reply (697dd376) +* dont expost entire res._locals to client side (e368feef) +* shorter meta.settings.get (190532b3) +* clone settings before returning (51cbeccb) +* remove another async.series (27c05448) +* remove tabs after declaration (4359e5c9) +* update dates (6d38eab6) +* clone before returning (f729e519) +* add filter:topic.getPosts (258f368e) +* setObjectBulk to match sortedSetAddBulk (8379c11b) +* remove more async.eachSeries/mapSeries (d1964095) +* make a single call to set widgets per template (8750ee04) +* remove jshint, remove async.parallel (80f9963b) + +##### Code Style Changes + +* eslint (d960f601) + +##### Tests + +* fix util test (6c1e184c) +* update tag tests, fix linux symlink error (9b75b1ed) +* fix restore test (0d9ec9d3) +* fix user tests calling deprecated socket methods (e747998c) +* add email interstitial tests (1264dcb5) +* don't use csrf_token for get,head, options (5e08f7e6) +* fix tests (0e273ab2) +* remove old test (68fd0875) +* make it async (5c3d5f1f) +* fix (26f00ffb) +* one more getTopicPosts test (f8f0a83b) +* utils.params (7b99dc46) +* regular user shouldn't see admin:privileges (cd2040ac) +* add missing tests (58431221) +* add api token tests (7434cbf6) +* add missing tests (404a8774) +* upgrade.runParticular (2bdb4906) +* add missing acp root category test (c17ec996) +* cache dump test (217aae4c) +* add missing controllers (bc120dba) +* fix tpl name (edf7c647) +* add mising email.test tpls (6d186ff1) +* debug routes in dev (754cdab8) +* add digest route test (1280d9ae) +* digest (f11bc33a) +* fix function name (0e725125) +* submitUsage (d375dcb8) +* remove log (d7c32ccb) +* middleware/expose.js (29b3587d) +* up mongodb version (500cad78) +* mainPost removed from inf scroll (aac0792a) + +#### v1.18.6 (2021-11-10) + +##### Chores + +* make it a link (a0f0dd02) +* update badges, remove david doesnt work (dad31c8e) +* up themes (b1d6c9ba) +* up mentions (98b98a11) +* up mentions (3e4d477e) +* fix type.yaml example and summary (591424ce) +* incrementing version number - v1.18.5 (1e418f5b) +* update changelog for v1.18.5 (82eda23a) +* remove .opacity() mixin as it is supported cross-browser (28efcb59) +* **deps:** + * update dependency eslint-plugin-import to v2.25.3 (45a0895c) + * update commitlint monorepo to v14 (dc78125a) + * update dependency jsdom to v18.0.1 (7d468e72) +* **i18n:** + * fallback strings for new resources: nodebb.admin-development-info (91676c6c) + * fallback strings for new resources: nodebb.admin-settings-navigation (3727e39f) + * fallback strings for new resources: nodebb.admin-settings-post (46789910) + +##### New Features + +* #9992, hooks.one (96f13e4f) +* use auto-generated meta and link tags in ACP, closes #9991 (1719bff8) +* add node 16 (#9847) (d27c9696) +* #9967, allow dropdowns in navigation (2e623dd2) +* show number of events per type in acp (b916e42f) +* show posts previews if enabled on mouse over (8c670316) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-theme-slick to v1.4.16 (#9990) (cf30876f) + * update dependency nodebb-plugin-composer-default to v7.0.14 (#9989) (ef02bdc4) + * update dependency nodebb-plugin-composer-default to v7.0.13 (#9988) (654c8e61) + * update dependency nodebb-plugin-mentions to v3.0.2 (1a22b0ec) + * update dependency socket.io to v4.3.2 (98ebc4d9) + * update dependency html-to-text to v8.1.0 (c1f5889f) + * update dependency nodebb-plugin-dbsearch to v5.1.0 (#9983) (4f1ee1fc) + * update dependency nodebb-plugin-composer-default to v7.0.12 (7fee0e32) + * update dependency nodebb-plugin-mentions to v3.0.1 (#9979) (8224a2a9) + * update dependency nodebb-plugin-spam-be-gone to v0.7.11 (91293ecc) + * update dependency nodebb-theme-lavender to v5.3.1 (f7295aaa) + * update dependency nodebb-plugin-mentions to v3 (#9966) (0888aae6) + * update dependency mongodb to v4.1.4 (#9968) (f5993731) + * update dependency nodebb-theme-persona to v11.2.21 (#9969) (8fac8d61) + * update dependency nodebb-plugin-mentions to v2.15.1 (0f8a68c0) + * update dependency validator to v13.7.0 (81c8d70c) + * update dependency autoprefixer to v10.4.0 (755860f1) +* ability to enumerate email via updateProfile method (c1ac2912) +* accidentally not clearing email when said email is confirmed for a different uid (b912a564) +* #9976 (28dd31a8) +* #9976, handle array or object (9bfb6c72) +* dont show previews on mobile (41e02400) +* category load more btn visibility (05468526) +* #9973, ignore if assigning to same parent (66e7cdac) +* #9972 (67cb2491) +* remove tooltip on ajaxify (f728abda) +* don't highlight external nav items (8a88295d) +* don't use # for previews (5a0efd2d) +* events for just topic with main post (3d611ab7) +* #9954, get next post timestamp (89399c0e) +* topic events not rendered in infinitescroll (a7f235db) +* broken post uploads due to 6a976a9db0340e34577961ce8d5d9479c78f7856 (485b6ced) +* #9950, rename account export routes to remove `uid/` prefix (0ee85d5a) +* double invocation of authenticateRequest (60352eca) +* #9945, call authenticateRequest middleware for mount points in /api (6a976a9d) +* hooks is sometimes undefined (74aa12c9) +* typo in flags (bc4b19b4) +* remove unused code (50b2ebf8) +* handle undefined data.query (8f08d9ca) + +##### Performance Improvements + +* only load posts once (9fbb3b11) + +##### Refactors + +* shorter require (41c3eb82) +* deprecate app.alert functions user alerts module directly (0428912c) +* deprecate app.logout (8b4510cc) +* simpler rejoin (61903448) +* deprecate app.openChat/newChat (f352be63) +* move search functions from app.js to search module (1a9b1598) +* move session messages (666fe209) +* move warnings/messages out of app.js (51855254) +* remove jshint (0a7ff208) +* cleanup info, better cpu usage % (4b738c8c) +* acp only uses 3 modes and a single theme (890bf03f) +* display errors from category drag/drop (c1cc35a9) +* use utils.debounce (e8c17fee) + +##### Tests + +* add another assert for random failing test (ae64b9f4) +* socket.emit doesnt exist in tests (61d1f565) +* show body when test fails (e3f5b706) +* lint (3d2398ac) +* fix tpl test (30cce142) +* dbsearch no longer has staticDir (3386893b) +* increase timeout (4ac9270a) +* fix account export test routes (10bb8cf7) +* add test aliases.buildTargets (62ac9a8b) +* empty query params for search (bda5d144) + +#### v1.18.5 (2021-10-27) + +##### Breaking Changes + +* disable javascript in custom less tab (719cfc0d) + +##### Chores + +* remove .opacity() mixin as it is supported cross-browser (28efcb59) +* up themes (463b2076) +* up persona (1438f409) +* incrementing version number - v1.18.4 (945c2b0b) +* update changelog for v1.18.4 (7cbcb521) +* **deps:** + * update dependency lint-staged to v11.2.6 (8d4bb8bb) + * update dependency lint-staged to v11.2.5 (0728a994) + * update dependency lint-staged to v11.2.4 (f76a7882) + * update dependency husky to v7.0.4 (2a3e13f3) + * update dependency mocha to v9.1.3 (4784f016) + * update dependency eslint-plugin-import to v2.25.2 (3c3f45d9) + * update dependency jsdom to v18 (4b8dcd4c) + * update dependency eslint-plugin-import to v2.25.1 (7c4aebbd) + * update dependency lint-staged to v11.2.3 (288b5456) + * update dependency lint-staged to v11.2.2 (f96c8c4d) + * update dependency @commitlint/cli to v13.2.1 (52c38a1d) + * update dependency lint-staged to v11.2.1 (022e8df0) + * update dependency eslint-config-nodebb to v0.0.3 (4b92df82) +* **i18n:** + * fallback strings for new resources: nodebb.admin-settings-email, nodebb.error (9b68dc37) + * fallback strings for new resources: nodebb.admin-dashboard (ff962b5d) + * fallback strings for new resources: nodebb.admin-dashboard, nodebb.admin-menu (abe59131) + * fallback strings for new resources: nodebb.admin-manage-digest, nodebb.admin-settings-user, nodebb.user (2bed40be) + +##### Documentation Changes + +* update verbiage re: login API route (94c4f87b) + +##### New Features + +* new ACP option `emailPrompt` ... which allows administrators to disable the client-side prompt to encourage users to enter or confirm their email addresses (80ea12c1) +* show popular searches (f4cf482a) +* new plugin hook to allow plugins to reject email address on new registration or email change (6b4f35c2) +* utilities login API route now starts an actual login session, if requested (806a1e50) +* add method name to deprecation message (b91ae088) +* quote tooltip (66fca4e0) +* additional quality options for jpeg uploads, added quality and compression settings for png uploads (d22b076b) +* #8053, biweekly digest option (f7967bdf) +* core submit button dropdown (605a5381) +* added failing i18n tests (35af7634) +* confirm before deleting all events (#9875) (56d05b4e) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-theme-vanilla to v12.1.7 (#9944) (bf20965f) + * update dependency nodebb-theme-persona to v11.2.19 (#9943) (bcf85fcf) + * update dependency nodebb-rewards-essentials to v0.2.0 (7c2ecb12) + * update dependency nodebb-theme-vanilla to v12.1.6 (49b8b983) + * update dependency nodebb-theme-persona to v11.2.18 (ed0adf2c) + * update dependency nodebb-theme-persona to v11.2.17 (78661079) + * update dependency postcss to v8.3.11 (a5f4e206) + * update dependency nodebb-theme-vanilla to v12.1.5 (d74a6bd3) + * update dependency sharp to v0.29.2 (8b8fe393) + * update dependency postcss to v8.3.10 (b18a24e9) + * update dependency nodebb-theme-persona to v11.2.15 (f3c8d7da) + * update dependency nodebb-theme-persona to v11.2.14 (#9919) (5e08e67b) + * update dependency socket.io-client to v4.3.2 (deba3e27) + * update dependency socket.io to v4.3.1 (e1554f61) + * update socket.io packages (ce5a0a21) + * update dependency nodebb-plugin-spam-be-gone to v0.7.10 (600a8720) + * update dependency nodebb-plugin-composer-default to v7.0.10 (b0128f85) + * update dependency nodebb-plugin-markdown to v8.14.4 (f8f35d7e) + * update dependency nodebb-plugin-composer-default to v7.0.9 (ed874662) + * update dependency nodebb-theme-persona to v11.2.13 (1dba75e9) + * update dependency ioredis to v4.28.0 (4ff5452d) + * update dependency nodebb-theme-persona to v11.2.12 (fe9f82f6) + * update dependency ioredis to v4.27.11 (6d2e0aa9) + * update dependency nodebb-plugin-mentions to v2.14.1 (820f8cdf) + * update dependency jquery-ui to v1.13.0 (b0eb2aed) +* remove loading="lazy", fixes inf. scroll loaded images (01572785) +* windows tests (25ebbd65) +* undefined query showing in searches (6cfaea06) +* don't repeat search if on same page (89f5e06b) +* api session revoke test (0926ae6e) +* crash (da64810a) +* add missing translation (eb075c73) +* move record to controller (ee8e0480) +* profile edit fields showing translated values (63572c23) +* #9934, fix translator test (8d316d18) +* token verify (04dab1d5) +* guard against prototype pollution (1783f918) +* translator path traversal (c8b2fc46) +* there is no alltime digest, fixes translation in test email (e62948f7) +* clicking outside postContainer should close tooltip (47df62e7) +* minification regression (998b9e79) +* tooltip (fec7ebed) +* biweekly digest #8053 (9cb4de50) +* restore plugin upgrade checking logic (44687394) +* fallbacks for new langauge key (ed4ebd22) +* #9917, show topics as unread for guests (4333d217) +* clarify site settings urls vs config.json url (#9912) (6436aa65) +* clarify SMTP enable toggle (#9911) (09f198fc) +* don't overwrite reloadRequired with false (9e0ce027) +* delete translations in admin/general folder (since general was removed and relocated elsewhere) (b460e590) +* pushed missing key to tx and pulled fallbacks (21b61082) +* adding missing language namespace "top" (0f9b0b78) +* extra debug log (bd893cda) +* have renovate add `dependencies` label to its PRs (eddb9868) +* no global bootbox (#9879) (227456fb) +* #9872 update app badge with notification count if applicable (3e69bcdf) +* better nomenclature (c1149d04) +* html attributes (#9877) (3acaac4c) +* escape thumbs, allow robots meta tag (4f9717fb) +* missing translations (#9876) (7935bd9e) + +##### Performance Improvements + +* dont fs.open if plugin doesnt have language namespace (#9893) (1feb111a) + +##### Refactors + +* wider value field (c428ba80) +* dont save partial searches (c7e078d4) +* use search api for topic search (64192731) +* slowdown quick search (19ee7174) +* typo (a5287906) +* add callback to loadNotifications (f02fba29) +* simplified utilities API > login rout (506c34a8) +* log error as well (1d62bd6d) +* catch errors from buildHeader in error handler :fire: (73a9ca09) +* add missing helpers.tryRoute (d4da9840) +* shorter middleware (ee0282f5) +* meta/minifier use async/await (b2429ef0) +* remove unused var (90b81262) +* catch errors from digest (8e319a9b) +* less.render returns promise (14bc83a8) +* less.render already returns promise (6da32392) +* prompt.get already returns promise (c70eaa0a) +* no need for async/callbacks (057d1d58) +* no more :cow: (38756a0c) +* allow array of uids for blocks.is/list (a9bc6a09) +* show full url on error log (8e6bd7e9) +* var to const and let (#9885) (b0a24d6d) +* remove unused code (997fb2b3) +* remove unused colorpicker (543d8521) + +##### Reverts + +* lazy load (3d1cf168) + +##### Tests + +* fix broken openapi3 schema (7ef5214e) +* restore commented-out i18n test (fa1afbcf) +* moved topic event and topic thumb tests to subfolder for better organisation (154ffea0) +* remove escape (6c25b9db) +* possible fix to timeout (63109c07) +* increase timeout (8654a996) + +#### v1.18.4 (2021-10-06) + +##### Chores + +* up persona (f4e62fb1) +* incrementing version number - v1.18.3 (57358743) +* update changelog for v1.18.3 (f066ddb8) +* **deps:** + * update dependency lint-staged to v11.2.0 (840b49b9) + * update commitlint monorepo to v13.2.0 (aa370310) + * update dependency mocha to v9.1.2 (6385b88e) + +##### Documentation Changes + +* added link to unofficial IRC channel (c5a48b44) + +##### New Features + +* use unread icon in mobile (27e53b42) +* cli user management commands (#9848) (d1ff3d62) +* #9855, allow uid for post queue notifications (5aea6c6a) +* add userData to static:user.delete (f24b630e) +* closes #9845, sort by views (6399b428) +* duplicate `requireEmailAddress` settings block to Settings > User (a9645475) +* mongodb driver 4.x (#9832) (07adb49e) +* a useless hover effect because raisins (1a61ffc5) + +##### Bug Fixes + +* **deps:** + * update dependency mongodb to v4.1.3 (b4fc2773) + * update dependency postcss to v8.3.9 (9455e5b2) + * update dependency autoprefixer to v10.3.7 (78895d05) + * update dependency nodebb-plugin-composer-default to v7.0.8 (9215c7d1) + * update dependency ioredis to v4.27.10 (4694382c) + * update dependency nodebb-theme-persona to v11.2.9 (346e0890) + * update dependency autoprefixer to v10.3.6 (058fdca4) + * update dependency yargs to v17.2.1 (d50dd801) + * update dependency postcss to v8.3.8 (193c92e3) + * update dependency passport to ^0.5.0 (daea8a86) + * update dependency connect-pg-simple to v7 (#9785) (054f3da6) + * update dependency yargs to v17.2.0 (c78309b5) +* #9866, fire vote hooks after reputation changes (#9867) (8ad9a103) +* #9865, don't display register messages after login (96f5312d) +* dont show decimails on auto approva minutes (a0df3890) +* #9864 (e954ca10) +* delete old topic tags (a70c69fa) +* switch inf. scroll to xhr (#9854) (4404e819) +* #9828, max-width (40915105) +* handle undefined returnTo on registerAbort (ac1b9692) +* lint (ff850b24) +* psql tests (123354ca) +* psql test (f8d4ec6c) +* possible test fix for subfolder redirect (3605ac81) +* missing relative path in test (4eacfef0) +* #9834, missing null email check on new registrations, added tests (58e0a366) +* crossorigin not showing up on manifest link tag (0faa4937) +* #9827, fix reward duplication (89af00d1) + +##### Performance Improvements + +* convert promise.all to single query (#9851) (ea04aede) + +##### Refactors + +* use utils.debounce (a7668a7f) +* remove async.waterfall from remaining upgrade scripts (6b34065f) + +##### Tests + +* dashboard (4f8647a5) +* add tests for admin privileges (9fe9ab08) +* add missing tests (34798325) +* remove debug log (8cb47548) +* no need to create fake interstitial as NodeBB comes with some by default (cb69934a) + +#### v1.18.3 (2021-09-22) + +##### Chores + +* **deps:** update docker/build-push-action action to v2.7.0 (ee027719) +* incrementing version number - v1.18.2 (0a56158b) +* update changelog for v1.18.2 (27e9282a) + +##### New Features + +* move filter:topic.post hook to top of method (f194809f) +* add client-side static hook to fire immediately before any topic action (hint: delete `action` to stop default behaviour) (66eaae44) +* allow removing multiple items from list (397835a0) +* add uid to filter:user.saveSettings (7f48edc0) +* headers for global privs #9717 (#9762) (84ff1152) +* add ACP option to require email address on new registration (006fc700) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-plugin-composer-default to v7.0.7 (98554294) + * update dependency postcss to v8.3.7 (6ebe707c) + * update dependency autoprefixer to v10.3.5 (25687441) + * update dependency nodebb-plugin-composer-default to v7.0.6 (#9815) (c18678ce) + * update dependency nodebb-theme-persona to v11.2.8 (#9816) (39d73d0c) + * update dependency connect-mongo to v4.6.0 (8e886c85) + * update dependency nodebb-plugin-composer-default to v7.0.4 (8af54255) + * update dependency mongodb to v3.7.1 (9049dcd7) + * update dependency nodebb-theme-persona to v11.2.6 (506035b5) + * update dependency nodebb-theme-slick to v1.4.13 (787306a6) + * update dependency nodebb-plugin-composer-default to v7.0.3 (732b59c2) +* fixed element shifting in ACP menu that's been bothering me for 5-ish years (31975a62) +* #9822, use correct username/pwd (30f38771) +* remove unused translator (2add84a5) +* ban info test (07859f7e) +* #9819, show same time info for ban (9f0e55ad) +* show local time for ban (7a2f0ae1) +* crash (c437b336) +* remove caller from payload after hooks is done (15f9aaa6) +* bad uid reference (ce8ea6ea) +* update Topics.post and Topics.reply so that plugins can modify uid (or redirect a reply to a different topic) (7777812e) +* #9818, fix totalTime calculation (c4fc7bf9) +* missing microdata in category data (1ed62aa8) +* #9812, add topics:schedule (c0a52924) +* for subfolders (31a6d4b3) +* req.path doesn't have full url (0236ea86) +* escape moderation note before adding to dom (75ebe786) +* #9811, send bodyClass on 403 (40c9fca9) +* also launch docker workflow on release branches (944a7985) +* xss on flags page via ban reason (ba3582b8) +* up timeout for psql tests (896ff215) +* redis batch (0c4b875e) +* redis processing batch+1 items every iteration (3261edcc) +* #9560, don't save post diffs if content didn't change (8b576a37) +* #9790, get baseIndex on update for infinitescroll (6a55c027) +* #9790, fix sorting of more than one page of pinned topics (2657804c) +* privileges added by plugins (#9802) (3ecbb624) +* #9800, don't send all welcome test emails to test@example.org @julianlam (71ed50b9) +* docker - remove sha tag (b06e8dba) +* Return QEMU back, remove platforms definition (52eace4b) +* Docker workflow tweaks (#9792) (e7f4cde4) +* browsers autocompleting smtp fields when they should not (34afb747) + +##### Refactors + +* no regex (18252fb9) +* remove async.waterfall (58ac55c1) +* remove async.waterfall (222dccaf) +* remove async.waterfall (f35a0f43) +* allow plugins to replace og:image, or specify additional og:image (819917da) + +##### Code Style Changes + +* give me an A! :100: (0b4d7d1f) + +#### v1.18.2 (2021-09-08) + +##### Chores + +* **deps:** update commitlint monorepo to v13 (87ba768f) +* incrementing version number - v1.18.1 (f8f80e4f) +* update changelog for v1.18.1 (0713475d) + +##### New Features + +* a slightly less ugly rewards panel (bf0c02a7) + +##### Bug Fixes + +* dashboard graph controls (a7855c4c) +* #9767 ACP change group icon fix (580a016b) +* #9781 (#9782) (0ce4b87d) +* replace logic in isPrivilegedOrSelfAndPasswordMatch to use privileges.users.canEdit (856ba78a) +* handle missing uid in deprecated socket call (cdaea611) +* use privileges.users.canEdit for image upload priv check (e33e046f) +* errors from registerComplete (a48bbdbf) +* simplify logic for fullname and email blanking in user retrieval (getUserDataByUserSlug) (60de0844) +* lint (1e2bda13) +* manifest error (488f0978) +* #9772, regression from https://github.com/NodeBB/NodeBB/commit/70a04bc10577e90e28d66a647d38cafc3307a285 (72710b80) +* push back some deprecations, remove deprecated stuff scheduled for v1.18.0 (dd4e66e2) +* deprecate userData.showHidden as it is functionally equivalent to userData.canEdit (4ac701d7) +* focus on save button on plugin activation (46e5e17d) +* #9773, fire hooks properly for priv changes (#9774) (6869920e) +* **deps:** + * update dependency sharp to v0.29.1 (ac6cd02f) + * update dependency nodebb-plugin-dbsearch to v5.0.3 (338f90fc) + * update dependency nodebb-theme-vanilla to v12.1.3 (0b3ea5ad) + * update dependency nodebb-theme-persona to v11.2.5 (57e54d55) + +##### Refactors + +* deprecate picture update socket call, new API routes for picture update (0a41741b) + +##### Tests + +* added test for external image via new change picture API (8cbad61e) + +#### v1.18.1 (2021-09-03) + +##### Chores + +* found some hooks that don't play well docgen (ae793b4a) +* incrementing version number - v1.18.0 (1e436ae7) +* update changelog for v1.18.0 (2fd9c095) +* **deps:** update dependency mocha to v9.1.1 (64bac178) + +##### New Features + +* create folders in ACP uploads #9638 (#9750) (3df79683) +* column based view on wide priv. tables (#9699) (61f02f17) +* als (#9749) (e59d3575) +* add quick reply key (e9314842) +* add new lang key for no best posts (6e73d8c9) + +##### Bug Fixes + +* **deps:** + * update dependency autoprefixer to v10.3.4 (67b932f4) + * update dependency nodebb-theme-persona to v11.2.4 (fe18e100) + * update dependency mongodb to v3.7.0 (31a35d7f) + * update socket.io packages to v4.2.0 (f2028d70) + * update dependency ioredis to v4.27.9 (6052eb16) + * update dependency mongodb to v3.6.12 (#9761) (5fa982c1) + * update dependency nodebb-plugin-composer-default to v7.0.2 (33d51201) + * update dependency nodebb-theme-slick to v1.4.12 (1b416d7e) + * update dependency nodebb-theme-slick to v1.4.11 (65b32fa1) + * update dependency nodebb-theme-persona to v11.2.3 (6ce321e4) + * update dependency autoprefixer to v10.3.3 (91ba7cdf) + * update dependency nodebb-theme-slick to v1.4.9 (d80b378f) + * update dependency jquery-deserialize to v2.0.0 (#9744) (7f9451ce) +* determine indeterminate checkboxes (760ea9df) +* move app.alert back into the conditionals (ca9bae3a) +* only show email confirmation warning toast on pages that it applies (1bd1cc74) +* updated email confirm warning to be more positive (2d1380dd) +* automated tests are a good thing to have (6afeac37) + +##### Refactors + +* consistent jquery element var naming (fc0e655e) +* var to const (1272da65) + +#### v1.18.0 (2021-08-25) + +##### Breaking Changes + +* **emails:** restore ability for admins to edit a user's email address [breaking] (c4e3362b) +* #9670 return 4xx errors instead of 5xx on flag routes, when unauthenticated or not privileged [breaking] (d1959a25) +* made TopicList.onTopicsLoaded private [breaking] (07f25d8c) +* return proper API-style response if exception caught by error handler on v3 routes [breaking] (a54a3ee1) + +##### Chores + +* **deps:** + * update dependency husky to v7.0.2 (324c7d48) + * update dependency eslint-plugin-import to v2.24.2 (9a34fe18) + * update dependency eslint-config-nodebb to v0.0.2 (cd85a55d) + * update dependency mocha to v9.1.0 (c5a42273) + * update dependency eslint-plugin-import to v2.24.1 (daca09d4) + * update dependency @apidevtools/swagger-parser to v10.0.3 (ffdf61b8) + * update dependency jsdom to v17 (#9700) (41855375) + * update dependency eslint-plugin-import to v2.24.0 (23dafa20) + * update dependency lint-staged to v11.1.2 (d47bdde2) + * update dependency jsdom to v16.7.0 (9db28b4b) + * update dependency eslint to v7.32.0 (03a98f4d) + * update dependency mocha to v9.0.3 (40384fcb) + * update dependency lint-staged to v11.1.1 (7588aae1) + * update dependency lint-staged to v11.1.0 (cb5fe271) + * update dependency mocha to v9 (f43291f5) + * update dependency husky to v7 (702290c4) + * update dependency eslint to v7.31.0 (f5a53b7f) + * update dependency lint-staged to v11.0.1 (02101315) +* up markdown/composer-default, fixes: #9708 (b74eefac) +* incrementing version number - v1.17.2 (46be2046) +* update changelog for v1.17.2 (5c9c0605) + +##### Documentation Changes + +* some hook deprecation notices (6bc090f8) + +##### New Features + +* add confirmation modal to topic event deletion (e803737a) +* allow changing default search in (794bf01b) +* #9705, use radio buttons for flag reasons (382a4c27) +* Client-side hooks - replace window.trigger (#9679) (342503e0) +* closes #9684, allow event deletion (358ad740) +* replace eslint configs on server and client side to inherit rules from eslint-config-nodebb (f653a6ff) +* re-add FontAwesome font for compatibility (a370c26f) +* update to FontAwesome 5.15, resolve #6976 (41762e66) +* removed registerAndLoginUserCallback local helper, added handling if a bad interstitial doesn't go away nor throw errors (70a04bc1) +* updated email confirmation alert to more closely reflect email usage, remembering dismissal (bbbacd86) +* allow requirejs modules to be awaited (58adb762) +* show instructional modal after email change request (0e05cbe1) +* return back to profile after editing email (324a12b6) +* allow registration interstitial abort to also follow returnTo (b3c91641) +* plumb current session id into email removal/confirmation flow, so all other sessions are revoked except for the current session (96398faa) +* allow revokeAllSessions method to revoke all sessions except that which is passed in (new arg) (b0a4a1d3) +* return generic 404 on invalid confirm code (f53fc1ad) +* invites no longer require email (a917210c) +* show different registration intersitial lead text on new account vs. existing (74aaa0a9) +* removal of emailExists socket listener (12b2a979) +* add loggedin/guest class to body (04b1f702) +* convert _fireStaticHook to async function (auto-refactor by vscode) (21359eab) +* store topic tags in topic hash (#9656) (4a56388e) +* new hook `action:topics.loading` (in the same format as `action:posts.loading`) (e0db904b) +* schedule deprecation for `action:category.loaded` and `action:category.loading`. Use `action:topics.loaded/ing` instead (8ae4c300) +* internationalize API error messages (7036c375) +* #9651, change category desc to multiline (5fd190f7) +* **emails:** + * pass req in to filter:registration.interstitial (afd2d8da) + * display current email in interstitial form (f5291999) + * upgrade script for includeUnverifiedEmails (50517020) + * +includeUnverifiedEmails ACP setting (be97aa6f) + +##### Bug Fixes + +* lint (55693ec1) +* topic event deletion (1ee92c28) +* pluginPaths (0743554d) +* #9730, show warning if plugin is active but not installed (13878e9f) +* #9729, insert new posts after topic events (60bf5643) +* #9719, only apply to non https (c354cde3) +* #9727, addHandlers after hooks (77c3085a) +* allow smaller than 5mins for admin relogin duration (a288f51f) +* taskbar icon not pushed via composer/persona (3a81c8fd) +* #9698, pass along query params in redirect (9de64bf5) +* lint (8bf2896d) +* remove unnecessary quote (093ac1c0) +* parseInt tids (162ebacf) +* #9681, update posts in queue if target tid is merged (0c816429) +* email update interstitial to not error on empty email field (on new registration) (4a521ea2) +* updated ACP > Manage > Users to handle users with no email address (824a72b2) +* allowed reset and reset_notify emails to go out to unconfirmed email addresses (d5b5b7d5) +* bug where confirmation email was sent to the old email address, not the new one (414d733d) +* email validation flow, so that it actually works, fixed event logging bug, new email verification template (3bcd1f14) +* accidental early return in confirmByCode, tests, race condition (caf89687) +* test :shipit: (2c06ac9a) +* failing test from d1959a2 (f71f2951) +* #9668, add raw info to psql database page (6c47a060) +* use hooks module instead of window trigger (acb11cc7) +* tests (0960a814) +* translate language keys if passed in to formatApiResponse (415416d2) +* lint (ff78969c) +* tests (55d7e558) +* keep query string on redirects (47c8c692) +* **deps:** + * bump persona (12e7f8d5) + * update dependency nodebb-theme-persona to v11.2.1 (#9734) (2e1562b8) + * update dependency nodebb-theme-vanilla to v12.1.2 (#9735) (4bd66a7c) + * update dependency nodebb-theme-slick to v1.4.8 (#9732) (096c5a58) + * update dependency autoprefixer to v10.3.2 (d44e3a8e) + * update dependency nodebb-theme-persona to v11.2.0 (116f9cb5) + * update dependency ioredis to v4.27.8 (8461791a) + * update dependency nodebb-theme-persona to v11.1.3 (ec103ce8) + * update dependency sharp to v0.29.0 (626d5565) + * update dependency connect-mongo to v4.5.0 (ce6039f2) + * update dependency nodebb-theme-vanilla to v12.1.1 (de83f82e) + * update dependency nodebb-theme-persona to v11.1.2 (ee10ae04) + * update dependency yargs to v17.1.1 (38e38580) + * update dependency nodebb-theme-persona to v11.1.1 (47941418) + * update dependency mongodb to v3.6.11 (a0fd0268) + * update dependency yargs to v17.1.0 (181c20ba) + * update dependency ioredis to v4.27.7 (4c9d6b62) + * update theme versions for #9607 (3b34571d) + * update dependency postcss to v8.3.6 (ebdba8f1) + * update dependency html-to-text to v8 (3f24746c) + * update dependency yargs to v17 (1b6b1fe5) + * bump composer-default to v7 (51458c75) + * update dependency autoprefixer to v10.3.1 (0d3f74b7) + * update dependency nodebb-plugin-markdown to v8.14.2 (b6a84712) + * update dependency autoprefixer to v10.3.0 (72c9650f) + * update socket.io packages to v4.1.3 (f14df0d4) + * update dependency nodebb-plugin-composer-default to v6.5.34 (31dae04f) + * update dependency nodebb-theme-persona to v11.0.26 (ae14016e) + * update dependency nodebb-plugin-markdown to v8.14.1 (8b41684e) +* **emails:** + * broken test for api/user/email/:email (81611ae1) + * dont allow retrieving user data if showemail is false @julianlam (7d115c8e) + * registration tests, email no longer passed-in, API tests (confirm email for test accounts) (6694bdd5) + * don't automatically associate email during user creation if passed in at registration (e726048e) + * remove debug log (b4b65ecd) + * broken test due to sticky registration interstitial (ab9b6716) + +##### Refactors + +* remove promisify from redis, ioredis supports promises nati… (#9728) (6659e95a) +* get rid of async.waterfall/each (8fb53252) +* remove unused async (42dbd402) +* remove waterfall (6b6a7d4b) +* move interstitials into its own file in `src/user/` (e95df2f0) +* added user.email.remove method, updated email interstitial to handle email removal (ccf004f1) +* client-side to use flag notes API (ef4e74bf) +* fix wording (6ed7e937) +* **email:** validation checking methods, +tests fix (087e6020) +* **emails:** + * more work in update email interstitial, interstitial skipping, email change on confirmation, deprecation of requireEmailConfirmation (69c96dd2) + * interstitial for adding/updating email (f365bc46) + * remove email validation on client and server side (7c1d1c77) + +##### Code Style Changes + +* eslint (d2492ef4) +* lint fix (340ccb24) +* lint (52229172) + +##### Tests + +* **emails:** fixing broken tests introduced by e5ff68acd (a3a3b10f) + +#### v1.17.2 (2021-07-07) + +##### Chores + +* **deps:** + * update coverallsapp/github-action action to v1.1.3 (99c23037) + * update dependency eslint to v7.30.0 (725e70e9) + * update dependency coveralls to v3.1.1 (edefac96) + * update dependency eslint to v7.29.0 (2700f717) + * update dependency eslint to v7.28.0 (fac0bcbd) + * update dependency smtp-server to v3.9.0 (86723292) + * update dependency eslint-plugin-import to v2.23.4 (886d65f6) + * update dependency eslint-plugin-import to v2.23.3 (c3b0e2fa) +* lint (2b42e7ed) +* fix indentation (d07229aa) +* up rewards (ca9ca876) +* incrementing version number - v1.17.1 (0aad1312) +* update changelog for v1.17.1 (1e6ed0ad) + +##### Documentation Changes + +* add undoTimeout (2db77b0c) + +##### New Features + +* add merge/fork hooks (c9348efb) +* #9628, allow setting custom icon names (2fe30b6f) +* #9617 update fa link (52596902) +* add bodyClass to 500 page (46a454f1) +* clear reset tokens on successful login (f9728aff) +* add filter:categories.categorySearch (be19f27f) +* allow nested properties on category page (#9601) (cc0cf99f) +* show ip on acp manage users (8ea58432) +* add undoTimeout to moving posts as well #9599 (e588948f) +* make undoTimeout configurable, closes #9599 (05cc7ccb) +* introduce boolean res.locals flag to bypass session reroll (used by session-sharing) (816856b0) +* allow modifying default category privileges (57e46e41) +* add filter:notifications.create (ac7b093f) +* pass req.query to getUnreadData (3d5fef6e) +* added GET user route for api v3 (d2960aeb) + +##### Bug Fixes + +* **docs:** #9648 (e03782f2) +* **deps:** + * update dependency mongodb to v3.6.10 (f17625fb) + * update dependency nodebb-theme-persona to v11.0.25 (c11927c5) + * update dependency sortablejs to v1.14.0 (5ff9319f) + * update dependency nodebb-theme-persona to v11.0.24 (c7feea56) + * update dependency nodebb-plugin-composer-default to v6.5.33 (3611b04e) + * update dependency nodebb-plugin-markdown to v8.14.0 (e40f648f) + * update dependency nodebb-plugin-markdown to v8.13.1 (cf6fcc21) + * update dependency nodebb-plugin-composer-default to v6.5.32 (#9626) (90e3f5ac) + * update dependency nodebb-plugin-composer-default to v6.5.30 (#9624) (2060dc61) + * update dependency postcss to v8.3.5 (09aebbda) + * update dependency postcss to v8.3.4 (520050da) + * update dependency postcss to v8.3.3 (c7006ec6) + * update dependency ioredis to v4.27.6 (82b33dc4) + * bump persona (f4eb336a) + * update dependency postcss to v8.3.2 (88f21e91) + * update dependency postcss to v8.3.1 (71b4d634) + * update dependency ioredis to v4.27.5 (9f74caf6) + * update dependency connect-redis to v6 (#9590) (6632b2b6) + * update dependency ioredis to v4.27.4 (4ffd234f) + * update dependency nodebb-theme-persona to v11.0.21 (062928d2) + * update dependency mongodb to v3.6.9 (d8c64479) + * update dependency autoprefixer to v10.2.6 (3aeac357) +* #9634, re-jig top bar of Admin > Manage > Users (b8964843) +* hide private user data in api/v3/users/[uid] (97c8569a) +* numThumb upgrade script (d9e2190a) +* acp dashboard sometimes not loading on cold load (fee782c4) +* #9636, sanitize all attributes in meta and link tags (84904976) +* convert some hooks to use hooks module (09bac6bd) +* #9627 (acdbd049) +* #9629, translate content property of meta tags (561ce7d3) +* prevent crash in expandObjBy #9618 (ab6f062f) +* dont show system tags in whitelist dropdown to regular users (0d975bc4) +* #9622 (#9623) (84e06575) +* #9620, fix notif delay (73f40e96) +* #9619, add group chat filter to /notifications (c92fc19b) +* scope (3cd9434b) +* #9615, catch exceptions in renderOverride (1eda538d) +* purge uploaded images accordingly #9606 (#9611) (8168c6c4) +* keep query params when switching chat (7b4c0a16) +* accidental unnecessarily strict conditional that effectively rendered SSO state checking opt-in instead of opt-out (a2400f6b) +* introduce artificial delay + delay fudging on invalid email during reset token generation (f6c14d6b) +* #9605, expire all active reset tokens for a uid if that uid generates a new one (229f96f8) +* lint (8c952aa3) +* schema (39e13591) +* pagination on acp users search (6695927e) +* #9596, incorrect placeholder string in some translations (93d94564) +* extra ')' (6f732611) +* disallow editing of other users' notes (edcba61a) +* #9592, check session (286644d0) +* don't crash if session doesn't exist (3717df61) +* lint (66cae54e) +* improper use of filename extensions (16e0bca5) +* return null (d8d6c989) +* updates navigation menu user icon (f9b248b8) +* returnOriginal deprecation (3fb74445) +* error when trying to trim an object (48b8e3bb) +* **post-queue:** moderatedCids is an array of numbers (#9631) (db65360c) + +##### Performance Improvements + +* cache Topics.getTopicsTags (8e0561f2) +* bypass getMultipleUserSettings (10ddfff3) + +##### Tests + +* fixed broken tests from #9605, removed token clean on token usage as it is superceded by token clean on generation (+ associated test) (5c42b3ea) + +#### v1.17.1 (2021-05-26) + +##### Chores + +* **deps:** + * update dependency grunt to v1.4.1 (0e37bbfd) + * update dependency jsdom to v16.6.0 (60170ad6) + * update dependency eslint to v7.27.0 (298af98d) + * update dependency eslint-plugin-import to v2.23.0 (1623ba4f) + * update commitlint monorepo to v12.1.4 (4a01313d) + * update commitlint monorepo to v12.1.3 (b82774c5) + * update dependency eslint to v7.26.0 (d1418210) + * update dependency lint-staged to v11 (1bf57d40) + * update dependency mocha to v8.4.0 (461e187b) + * update dependency eslint to v7.25.0 (32c20806) + * update dependency grunt to v1.4.0 (a30deef3) +* incrementing version number - v1.17.0 (75f7972b) +* update changelog for v1.17.0 (4c441a1b) + +##### Documentation Changes + +* update API docs to better outline authentication options (6ef0c8e9) +* update API authentication verbiage (d08d0c42) + +##### New Features + +* keep query string when redirecting category (77dde41f) +* add req.query to flags.list/getCount (3d6bdeb3) +* add filter:flags.getFlagIdsWithFilters (d35c64b1) +* #9559, set order help text (f5847f4f) +* add filter:user.getWatchedCategories (4afca690) +* pass req.query to getUserDataByUserSlug (518157d9) +* #9508, add cluster support (94c12e37) +* #9551 (a3d6c56e) +* add template to hook (1f3e6601) +* add filter:account.getPostsFromUserSet (a2442ee9) +* automatically attempt reconnection on window focus (8cc61f11) +* #9533, allow redirect in build hooks (f6b583bb) +* add _hooks metadata object to all hooks that have object-type params (46899cca) +* add filter:categories.copySettingsFrom (d8e4fd4c) +* guard against multiple resolves (084c9851) +* #9511 send notifications on accept/reject (b40fc4b6) +* `hidden` class to FOUC (2bfa63ae) +* add filter:middleware.autoLocale (a478dc7e) +* remove sync hooks support (01956af4) + +##### Bug Fixes + +* #9580, proper 404 when ajaxifying (9ebfdeb7) +* lint (09f51792) +* #9567 fix tests (951e71a0) +* #9567, use regular 404 (5215c30a) +* ioredis upgrade fix, maybe (1ce59508) +* bug where interstitial errors were not properly passed to the front-end via req.flash (1d9cfe1e) +* #9553, use same fields for user search results in acp (0551642a) +* lint (e8c5c18a) +* lint (ffa80163) +* tests (fad5988e) +* tests (074ee859) +* wrong error message checked (a9bb1088) +* #9507 session reroll causes socket.io to become confused (#9534) (ec6d1e23) +* isObjectField(s) empty field (2c22b06f) +* do not register SW for Safari until upstream fixes #9193 (ce5fea2a) +* infinite scroll with small number of items #9524 (#9525) (cb1dd0a3) +* #9519 unable to properly ajaxify to home on subfolder installs if anchor did not have a trailing slash (db48b952) +* #9512, fix chat icon if no privileges (6ed8890c) +* #9503, dont error in markUnread if room doesnt exist (308252f5) +* use socket.request.headers (9e07efc1) +* buildReqObject headers for socket.io calls (ed534f34) +* **deps:** + * update dependency sharp to v0.28.3 (963a9fe6) + * update dependency ioredis to v4.27.3 (075dab27) + * update dependency nodebb-theme-vanilla to v12.0.8 (#9574) (10290f54) + * update dependency mongodb to v3.6.8 (#9573) (64935787) + * update dependency postcss to v8.3.0 (ad4afd59) + * update dependency nodebb-theme-persona to v11.0.20 (403bcfac) + * update dependency nodebb-plugin-composer-default to v6.5.29 (8d7e4420) + * update dependency mongodb to v3.6.7 (f29e4e87) + * update dependency textcomplete to ^0.18.0 (9b7653cc) + * update socket.io packages to v4.1.2 (#9563) (ca7c77bc) + * update dependency nodebb-plugin-dbsearch to v5.0.2 (#9562) (2d0564cb) + * update dependency nodebb-theme-slick to v1.4.7 (bf4aa50c) + * update dependency nodebb-plugin-composer-default to v6.5.28 (4164b322) + * update socket.io packages to v4.1.1 (155a7fb6) + * update socket.io packages to v4.1.0 (53335677) + * update dependency sharp to v0.28.2 (4bc07a08) + * update dependency postcss to v8.2.15 (7770c2a1) + * update dependency nodebb-theme-persona to v11.0.19 (3145c7d5) + * update dependency nodebb-plugin-mentions to v2.13.11 (7e2ea4df) + * update dependency nodebb-plugin-markdown to v8.12.10 (2ac5a085) + * update dependency nodebb-plugin-markdown to v8.12.9 (ee3634cc) + * update socket.io packages to v4.0.2 (ff98f854) + * update dependency postcss to v8.2.14 (22ec1ea5) + * update dependency sitemap to v7 (0389dd96) + * update dependency nodebb-widget-essentials to v5.0.4 (19f1cbfc) + * update dependency nodebb-theme-persona to v11.0.18 (33d91fde) + * update dependency connect-redis to v5.2.0 (c0d54a06) + * update dependency postcss to v8.2.13 (831e5c26) + * update dependency postcss to v8.2.12 (38454df9) +* **#9508:** switch to ioredis (#9545) (dd81dd03) + +##### Refactors + +* cli/upgrade async/await (#9558) (ac86937c) +* bubble other errors (0096cf17) + +##### Reverts + +* sync hooks (5fe97b9c) + +##### Tests + +* fix tests (1029a06a) +* fix redis tests (4e490f60) +* add test for undefined fields in getObjectsFields (92de49be) + +#### v1.17.0 (2021-04-22) + +##### Breaking Changes + +* add additional flag hooks [breaking] (00a68a95) +* remove deprecated `User.emailConfirm` [breaking] (fb84c785) +* remove deprecated plugin hook `filter:privileges:isUserAllowedTo` [breaking] (5a775e09) +* remove deprecated plugin hook methods [breaking] (d41de481) +* more removals of thumb specific backwards-compatibility [breaking] (cc0d562e) +* remove deprecated `filter:admin/header.build` hook [breaking] (5f9f241e) +* remove deprecated v2 style responses for thumbs upload route [breaking] (84dfdfe6) +* remove deprecated getObject routes [breaking] (2ad0d0d0) +* remove 'filters' and 'categories' from flag details API return [breaking] (8b72479f) +* filtering logic of flags [breaking] (1603566b) +* feature flag for auto-resolving a user's flags on ban [breaking] (6b1c97db) +* allow interstitial callbacks to be async functions [breaking] (280285cd) + +##### Chores + +* **deps:** + * update dependency jsdom to v16.5.3 (0f249aa7) + * update dependency eslint to v7.24.0 (60c0c16f) + * update dependency husky to v6 (f155f326) + * update commitlint monorepo to v12.1.1 (b4d01388) + * update dependency jsdom to v16.5.2 (5e2e7a58) + * update dependency eslint to v7.23.0 (d600cd94) + * update dependency husky to v5.2.0 (77f551a4) + * update dependency jsdom to v16.5.1 (28ed579b) + * update dependency eslint to v7.22.0 (775c3b91) + * update dependency mocha to v8.3.2 (3ce731d8) + * update dependency jsdom to v16.5.0 (fd926d61) + * update dependency mocha to v8.3.1 (651c629f) + * update dependency husky to v5.1.3 (8791b44e) + * update dependency husky to v5.1.2 (5f061b94) + * update commitlint monorepo to v12 (42f7cd52) + * update dependency eslint to v7.21.0 (59518437) + * update dependency husky to v5.1.1 (2551295c) + * update dependency husky to v5.1.0 (dc06fe22) + * update dependency eslint to v7.20.0 (9ec0b2ed) + * update dependency mocha to v8.3.0 (73f07958) + * update dependency husky to v5 (d89ccf26) + * update dependency lint-staged to v10.5.4 (030ecffa) + * update dependency eslint to v7.19.0 (3696a199) +* incrementing version number - v1.17.0-beta.5 (42c4f963) +* fix indent (6406e527) +* benchpress 2.4.1 (3403635c) +* remove log (a1ee1a2a) +* incrementing version number - v1.17.0-beta.4 (91992240) +* bump composer-default (289bfc0b) +* up themes (d14ba1f4) +* remove node 10 (8d3ec234) +* bump composer-default to 6.5.20 (33fbfdfe) +* incrementing version number - v1.17.0-beta.3 (6e8b1bb9) +* add multiparty dep (ef3ec96a) +* incrementing version number - v1.17.0-beta.2 (0c1945dc) +* incrementing version number - v1.17.0-beta.1 (31872aac) +* bump composer (30954789) +* bump deps (#9335) (b9fd2c87) +* incrementing version number - v1.17.0-beta.0 (b61257ef) +* incrementing version number - v1.16.3-beta.0 (477157cc) +* extra console.log (1ae8dda8) +* up composer (1c9acef6) +* eslint max-len (cc9d6fd0) +* eslint no-restricted-syntax (5c2f0f05) +* eslint prefer-rest-params, prefer-spread (115d19e2) +* eslint prefer-destructuring (23f212a4) +* eslint object-curly-newline (8d1462ff) +* eslint function-paren-newline (62869bae) +* eslint no-var, vars-on-top (dab3b235) +* eslint prefer-arrow-callback (b56d9e12) +* eslint prefer-template (707b55b6) +* eslint import/newline-after-import (4ee0f145) +* eslint no-script-url (9f6a682c) +* eslint no-bitwise (dad01e30) +* eslint rules matching existing styles (58528d1a) +* fallbacks (74be1a59) +* deprecation notices for plugins using plugin old hook methods (3052256d) +* add deprecation notice in comments for ajaxify.loadExtraScripts (8b09292e) +* incrementing version number - v1.16.2 (ea7f8381) +* update changelog for v1.16.2 (d3883d4b) + +##### Documentation Changes + +* fixed typo (e7550673) +* added comment re: #9305 (65c57c73) +* update deprecation-removal version for plugin hook helper methods in 1.18.0 (15ba0abb) + +##### New Features + +* lang strings (9b71b087) +* rate limit file uploads (a9978fcf) +* filter flags by username #8489 (#9451) (8faa6e45) +* roll session identifier on login, as security best practice (697ed3bf) +* allow different slugs (4494728c) +* remove sort again (fd3bc605) +* update hook (f65d2162) +* add reverse of recent to getSortedTopics (05f22361) +* allow exists methods to work with arrays and single id (285aa365) +* pass all data to filter:category.get (d16b45fd) +* add action:posts.loaded (dbb59228) +* rescheduling (editing ST) (#9445) (aa0137b1) +* upgrade sharp (#9442) (f7f46e7c) +* optional urlMethod param for loginStrategies (9e1f72a4) +* add hooks to language loading (#9426) (344575dd) +* doggy.gif (b06f0ea2) +* allow adding sorted-list items from forms outside of modal (a3e95e79) +* scheduled topics (#9399) (077330b7) +* show link if category is a link (a94d9651) +* make info page full width (dd12c83f) +* allow optional fields argument on db.getObject(s) (#9385) (4327a09d) +* closes #9380, add category filtering and topic tools to tag page (668508cc) +* allow sync function (#9379) (34b9aaed) +* allow filter functions that return promises or the data directly (e6c52cf2) +* add resolve flag to post tools (52082e12) +* hide revert button in ACP > Privileges until privileges change (4cbd13fd) +* bring back static hook timeout (46270f9f) +* upgrade connect-mongo, closes https://github.com/NodeBB/NodeBB/pull/9367 (3c60ccfd) +* pass interstital errors to individual partials as well as to registerComplete (f71cb0e4) +* add filter:plugins.firehook (5eb3132d) +* copy default favicon if it doesn't exist (754283d3) +* add missing translation keys (17184bfa) +* allow missing (or non-array) middlewares argument in route helper methods (4b545085) +* pass modified params, only affects filter hooks (e74df539) +* add back topic id input (696c4895) +* expose username validation logic to user lib, new hook `filter:username.check` (bfd512b9) +* add $.deserialize to client side (e5133a78) +* allow for settings.save/settings.load on client side (66196d2c) +* remove promise-pollyfil (902a88c2) +* category privilege API routes (c1b3079d) +* change uploadCroppedPicture to use updateProfile as well (0af9d26f) +* use updateProfile for picture change (a598abcd) +* allow payload to be passed to emailer test method (1155b0c4) +* add uid of user who created flag to action:flags.create (069ac60f) +* new client-side hook `filter:api.options` to allow plugins to modify api requests (7d391d47) +* keep notifs for one month, load 50 notifications instead of 30 (02f08111) +* also pass in uid to `filter:email.prepare` (86b0c57d) +* new hook `filter:email.prepare` (27ea3dcb) +* new hook static:email.send (bf90d158) +* show time info for upgrade scripts (14a6c349) +* add dashboard sub-pages to ACP menu (73dc64d9) +* recent logins sessions table in dashbaord subpage (2f89b0d7) +* topics dashboard details subpage (e1ed514b) +* update user list in dashboard/users on graph update (c57c7703) +* show list of recent users in dashboard/users (cc938224) +* req.query parsing and dynamically loading data instead (6fdcae73) +* new hooks for notifications get/getCount (079a13d4) +* allow hook unregistration, and temporary page-based hooks (d0136074) +* report login statistics from analytics data, instead of its own zset (16d3c457) +* track login sessions for admin dashboard reporting (9a9f366d) +* track successful logins in analytics (504fd107) +* pass user picture object into change_picture_modal (c96fd3b1) +* add logout to invalid session (beb14273) +* category search test (a592ebd1) +* pass post object to filter:post.tools (ed3d9dcb) +* allow defining a list of system tags (0e07f3c9) +* add category search test, #9307 (bbaaead0) +* add tag filter to getSortedTopics (9ce6f8ad) +* ability to re-order topic thumbnails (7223074f) +* add close button to topic thumbnail modal (db027170) +* #9304, add category/topic/username to post queue notification emails (0738dae8) +* add failing test for list append/prepend with list (#9303) (8f0386d9) +* link to post-queue from topic event (a4b4a556) +* post-queue topic event (8fd78ce5) +* add post-queue cache (3f35fd33) +* newsletter opt-in/out in UCP, closes #21 (3c7cd9a6) +* load user posts/topics via xhr on infinitescroll (35954734) +* #9294, put new categories at top (4b2bf12f) +* add invalid event name to error message (670cde78) +* new notifications load/loaded hooks on client side (7edc8f45) +* pass req.session into buildReqObject (a6fa351b) +* new hook `action:login.continue` (4f976390) +* banned-users group (53e0d4d2) +* #9109, ability to delete a post's diffs (eb642f40) +* add .delete() method to api module (501441b7) +* doc add description (cc560ca3) +* add doc for query param (ed11e171) +* #9234, add pagination to /api/recent/posts/:term? (fffdc4e0) +* allow sorted-lists on multiple pages (d5d24594) +* #9232, add profile picture into exported zip (f6cd2862) +* new hook `filter:login.override`, deprecate `action:auth.overrideLogin` (b820d234) +* guard password fields in login/register against accidental caps lock (4bb3b032) +* ability to search categories, #8813 (34c42c6f) +* restore action:script.load, allow modifying loaded module via static:script.init (05be1c66) +* async/await redis connection (fdfbc902) +* async/await psql connection (33bf1b0e) +* add group name to csv event (672959c1) +* **user:** icon background selector in change picture modal (95502124) +* **remountable-routes:** + * allow category and account routes to be remounted (9021f071) + * allow /admin and /post to be remountable (f01af62b) +* **topic-events:** + * topic events GET route in write API (dc84559d) + * server-side tests for topic events (449c379d) + * clear out topic events when a topic is purged (0d4a3775) + * client-side handling on topic event log (8e93bf73) + * handle newest_to_oldest sort in topic events, WIP (882e6a15) + * generic css for timeline-event (2293a07a) + * support for uids in topic event payloads (611d1f87) + * work in progress topic events logic and client-side implementation (ab2e1ecb) +* **hooks:** + * update action:ajaxify.end to use new hooks module (1d775721) + * client-side hooks module (01c9b184) + +##### Bug Fixes + +* regress. rescheduling shouldn't add to sets that pinning removed… (#9477) (8b79c7f1) +* logic is hard (4dd38446) +* run in series (bc0ca61c) +* wrong variable for cache (2e9efc0e) +* accidentally committed this (13fa983e) +* tests (eb240c90) +* eslint (fa0c92a7) +* use req.ip instead, since guests can upload as well (ea22cd30) +* #9492, keep query params on redirect (36f119a9) +* stripTags for editing sorted list items as well (93598982) +* #9487, session data gathered during a session is lost upon login (1fee6a70) +* failure on session reroll 🍣 test (f4c5050a) +* registration interstitials not handling promise rejections properly (e845c34b) +* stripHTMLTags for sorted list entries (75073c0e) +* restore original behavior for up/downvoting when logged out (e50408b4) +* let recent replies respect oldest/newest sort settings (60eed8d8) +* #9483, fix events count display (6907837f) +* escape flag reason (161081e9) +* copy change on plugin activate to instruct admins to rebuild as well as restart (95d5359c) +* updateCategoryTagsCount (2dc3283f) +* #9473 (#9476) (036f935f) +* #9474, load hooks on page load (1af34b43) +* spec (d09cdc04) +* #9466, don't call leaveRoom in maintenance mode (f32ea173) +* exempt ST from being del/res via last main posts (#9468) (a0dd9080) +* #9462, on install copy default favicon (784600d9) +* #9463 (c5ae8a70) +* #9465 (4041e786) +* #9450 express session saved even if saveUninitialized explicitly passed in (9c52fd2e) +* acp crash (cb53a64c) +* #9447, include query params in previousUrl (536591f8) +* thumb count not updated when uploading multiple thumbs at a time (1ad1787e) +* change email button stays disabled if user submitted an invalid email (01f63e5d) +* use app.logout() to clear session after deleting user (cfdef77b) +* ./nodebb help with commander@7 (#9434) (2a03012e) +* hide titleRaw for deleted topics as well (edf80cfb) +* #9410, fix post queue (c5dda64f) +* privilege tables (9052db93) +* #9420, paginate after loading notifications (67b09cba) +* hooks for alert animate, no more fadein/fadeout for reconnect alert (d9e20290) +* #9414, use posts:view_deleted (e42b152f) +* preserve order when changing parent (2ceda70a) +* #9411 (3c4e93a3) +* #9412 (cef58d1d) +* #9406, update flag post tools (93c595d9) +* typo in switch..case (d8ff9851) +* #9404, show signatures if the target user has signature privilege (801570e4) +* selector (ee69c1f8) +* sorting when filtering by uid (75553b24) +* allow local (and overridden) login strategies to pass Error objects back (98b72ca5) +* category search not using uid (6aa60b63) +* inf scroll with subfolder install (262e059f) +* flicker on dashboard (2041b808) +* #9398, crash on post flag (90d64fe1) +* #9395, pass all data from client to Topics.reply (#9396) (a8f7b244) +* lint (4ac38ab2) +* #9394, fix guest handles (eb360351) +* #9387, don't try to load undefined images (03e30634) +* #9389, allow admins to add themselves to private groups (5c59354c) +* #9386, add missing translation string (482641e3) +* #9383, don't show deleted topic titles in inf scroll (e789fe8d) +* #9378, crash on verifyToken if API Token settings not saved (null case error) (cc489708) +* closes #9382, fix digest topic links (35700d16) +* spec (1e1127bd) +* regression from filter hook change (53f67ff3) +* crash if unreadTopics is undefined (617f4730) +* dont crash if login el doesnt exist (f45c0aab) +* regression via c1b3079d93fb4c49ba62a4be5279b7bff8e5a54d (2a939aad) +* change notification updateCount to use client-side hooks (84725130) +* tests (39b0e0fb) +* #9370, show correct teaser index if sorting is newest to oldest (9382fc6d) +* don't copy if src doesn't exist (ebccc794) +* #9362 best not to check file exists on every page load; copying favicon to uploads/system folder instead (771a8955) +* #9362 (ad565495) +* regression where login redirect for admin routes didn't go to local=1 (678e8f0f) +* lint (f4f61b92) +* if no in passed use "titles" to match header search (e787e6ea) +* add back middleware.authenticateOrGuest (166d65a1) +* request authentication called twice in account routes (e3b2c00d) +* #9354, don't close quicksearch results if mouse is down on them (8a4c361e) +* #9339, only log email errors once per digest, notification push (3aa26c4d) +* winston.info (3f42d40c) +* #9351 bad logic when inserting rows to privilege tables, also a missing tfoot :foot: (c5e25788) +* app.parseAndTranslate to always return promise (c2650169) +* bug where fallback window trigger was not firing if there were no hook listeners attached (1e579428) +* bad assignment (c8b78654) +* #9348 incorrect redirect via connect-ensure-login (fbe9215b) +* bug where loginSeconds setting was ignored for local login (f806befd) +* remove old dep (b58bacaf) +* notif pruning (2737f653) +* notification prune test (ca817631) +* user icon text overflow in some cases (2b7d0b5a) +* use components for toggleNavbar instead (114e3a1e) +* allow interstitial callbacks to be functional (no cb required) (9bf94ad5) +* don't publish before pubClient is connected (cdf5d18f) +* remove unused async (48f1e265) +* in setupPageRoute helper, buildHeader after plugin hooks have fired (984c9dd9) +* timeago missing on table update (655e2c67) +* wrong qs param, allow string to be passed to util.getDaysArray (f8e1a74c) +* wrong call to sortedSetAdd (dbe5f702) +* session not persisting to database in some scenarios (020f0b83) +* allow hidden inputs in user settings page (beaac0a1) +* use root context if buildAvatar context is undefined (b4c0b32b) +* use bootbox module (fa91525a) +* #9307, use _.flatten (25c8f026) +* awaiting res.render in send404 controller > > A plugin wanted to use `response:rotuer.page` to 404 a specific page on some condition. res.render returns early in send404 and so must be awaited otherwise multiple responses will be sent (2fef4627) +* do not overwrite `config.port` from URL, if it's already set (34096b73) +* switch back to getSortedSetRange (8686fbfa) +* settings v3 (91734a64) +* another topic thumb test fix (782bef5e) +* thumbs.associate logic fix + tests (7ebb6d30) +* missing awaits, possible test fix (7665adf7) +* #9301, dont call sitemapstream if there are no entries in categories/pages/topics.xml (9a6cf3d9) +* properly incase its the same path (807b0d43) +* numThumbs count on associate (76bcc0c9) +* missing cache deletion calls for post-queue cache (1490b32d) +* use of removed URL to get post data (36e8d251) +* init topic events from webserver.js (b81508c4) +* check null topics (b753c69c) +* guard against null topics (58cd797e) +* tests, new categories go to top now (fc90f32e) +* #9292, messageobj.content already parsed (c953b1b3) +* clear category cache on copy parent (765db86d) +* delete category cache key on category create (ed3e9ce2) +* typo (c61cc37b) +* wait for event.log to finish before killing process (a5fa212f) +* tests, only generate csrf_token on 404 gets (b6493f89) +* #9287, generate csrf_token on 404 (94f72d60) +* do not blindly escape a notification's bodyLong (783786cf) +* pass jquery object in to action:notifications.loaded hook (16610b2d) +* #9275, (0cca6893) +* don't use global bootbox obj (cfa0d423) +* remove console.log (550cd995) +* move service worker back to relative_path/service-worker.js (fca17cb7) +* spec (ab0ef442) +* markread selector (a4878a5b) +* position when scrolling up (3090a2ae) +* cache key collision (e40af441) +* tests breakage due to 67e3fb64981fe2310b17515e1f18c32021a5e983 (5c21c33e) +* register returnTo logic to match login route (67e3fb64) +* tests (492cbc62) +* posts.uploads.sync dissociates uploaded thumbs of the main pid (f79aeef8) +* update grammar on unban text (68da1c55) +* privileges page - tweak icon position and width, group name wrapping (c729adeb) +* autofocus on search field in ACP > Manage > Plugins (4af9c2fc) +* openapi test specs (cabec378) +* include admins (7c9674de) +* include admins, limit to category mods, correct privilege name (eaf9d2e4) +* http 200 test for api routes (bd583963) +* invalid API call when unfollowing a user (58655e9a) +* example (833c73e8) +* #9127, scope service worker to relative_path for the forum (#9239) (2bc74cff) +* update docs (4c12e0aa) +* broken test after sorted-lists logic change (d6f60f45) +* clear all locks on restart (9834f72f) +* `action:admin.settingsLoaded` to use new hooks lib (5131eb6b) +* crash on firing action hook that had no listeners registered (b0f5d5a5) +* bug where `action:settings.sorted-list.loaded` fired early (1a04ec64) +* regression where `filter:settings.set` no longer received sorted-lists (a8be6fb8) +* #9231, fix redis pubsub connection (5bc1f5b4) +* don't translate message on every ajaxify (a29dd21d) +* tests (05c53394) +* improper override of req.body.username in login logic (74199220) +* full settings hash not passed through to action:settings.set (473d5f4a) +* #9223, don't overwrite stmp settings (a5bf9779) +* multiple sorted-lists do not save to the correct set (4029ec37) +* pass module name to `static:script.init`, +comments (f8bf9e99) +* handle delete and update for categories:name zset (e8429f50) +* tests remove old routes (faeb6373) +* removed object routes (d41ce873) +* removed methods (647d3ba8) +* incorrect return for Thumbs.get() if thumbs were disabled (7b090c58) +* script failure if client-side page script does not exist (7da1b43f) +* bug where `action:ajaxify.end` was never called if there were no init scripts (faf59603) +* update js concatenation logic to bundle scripts.rjs into minfile regardless of build environment (8ff07bc1) +* #7125, allow list for page route, configurable via plugin hook (f975063b) +* error on flag list if no flag filters were saved in session (942d9247) +* mod cid filter accidentally saved in session (35c92d0c) +* more tests for #9217 (ce7c74b2) +* tests for #9217 (f2a5cd0b) +* missing return for #9217 (27cae0d5) +* #9217, render 400 error page on bad access to /register (b2b1450e) +* redis check compat tests (78896fc6) +* registration completion overriding returnTo if it was already set (a186ea0f) +* add missing user delete event types (5c1b7429) +* missing option for user-deleteAccount on ACP Events page (1c420602) +* **deps:** + * update dependency html-to-text to v7.1.1 (427e4f47) + * update dependency redis to v3.1.2 (35a4d0be) + * update dependency validator to v13.6.0 (e3d5d8d7) + * update dependency nodebb-plugin-composer-default to v6.5.27 (1b846271) + * update dependency redis to v3.1.1 (286a63e3) + * update dependency nodebb-theme-persona to v11.0.17 (51d58ce6) + * update dependency nodebb-theme-vanilla to v12.0.7 (16a1ba57) + * update dependency postcss to v8.2.10 (31cec2de) + * update dependency nodebb-plugin-mentions to v2.13.9 (fe087806) + * update dependency mongodb to v3.6.6 (#9467) (4264b236) + * update dependency sharp to v0.28.1 (34cbc9e2) + * update dependency nodebb-theme-persona to v11.0.16 (a8330b6d) + * update dependency nodebb-theme-vanilla to v12.0.6 (c02310b8) + * update dependency nodebb-theme-persona to v11.0.15 (316c71d7) + * update socket.io packages to v4.0.1 (e7776f8d) + * update dependency redis to v3.1.0 (fd9ff334) + * update dependency nodebb-plugin-composer-default to v6.5.26 (#9446) (8d9afbc6) + * update dependency postcss to v8.2.9 (6f51c460) + * update dependency nodebb-theme-persona to v11.0.14 (#9443) (fecfcd81) + * update dependency nodebb-theme-persona to v11.0.13 (#9437) (e5cc6e40) + * update dependency nodebb-theme-slick to v1.4.6 (dfdb0050) + * update dependency nodebb-theme-persona to v11.0.11 (27de58f2) + * update dependency benchpressjs to v2.4.3 (382f75bc) + * update dependency nodebb-plugin-composer-default to v6.5.25 (24236718) + * update dependency nodebb-theme-vanilla to v12.0.5 (89973d80) + * update dependency nodebb-plugin-composer-default to v6.5.24 (dec34446) + * update dependency nodebb-theme-persona to v11.0.10 (f78b4ba6) + * update dependency nodebb-plugin-composer-default to v6.5.23 (#9422) (e35d0741) + * update dependency nodebb-theme-persona to v11.0.8 (124cb9d9) + * update dependency benchpressjs to v2.4.2 (1dddcb49) + * update dependency nodebb-plugin-mentions to v2.13.8 (d511216c) + * update dependency connect-mongo to v4.4.1 (29ff5bb9) + * update dependency nodebb-theme-persona to v11.0.7 (c5734063) + * update dependency nodebb-theme-vanilla to v12.0.4 (#9409) (870e6c2c) + * update dependency nodebb-theme-slick to v1.4.5 (#9408) (24be8642) + * update dependency nodebb-theme-persona to v11.0.6 (#9407) (b50739c1) + * update dependency nodebb-plugin-spam-be-gone to v0.7.9 (#9405) (9359cae9) + * update dependency nodebb-theme-persona to v11.0.5 (47b2b97f) + * update dependency nodebb-plugin-composer-default to v6.5.21 (#9401) (2f70ac5a) + * update dependency mongodb to v3.6.5 (fcd887fd) + * update dependency nodebb-plugin-composer-default to v6.5.19 (#9391) (1631f159) + * update dependency nodebb-plugin-composer-default to v6.5.17 (#9384) (8d401760) + * update dependency nodebb-theme-persona to v11.0.3 (27facadb) + * update dependency socket.io-redis to v6.1.0 (adaddde6) + * update dependency nodebb-plugin-composer-default to v6.5.16 (a98e92b4) + * update dependency nodebb-plugin-markdown to v8.12.7 (#9371) (56b0bfd5) + * update dependency nodebb-theme-vanilla to v12.0.2 (#9369) (8923d34c) + * update dependency nodebb-theme-persona to v11.0.2 (#9368) (fa71c483) + * update socket.io packages to v4 (#9363) (13f3c504) + * update dependency postcss to v8.2.8 (680cf5ef) + * update dependency nodebb-theme-persona to v10.5.17 (2645bf55) + * update dependency connect-mongo to v4.3.1 (59459074) + * update dependency connect-mongo to v4.3.0 (f388086a) + * update dependency autoprefixer to v10.2.5 (4f4cdacc) + * update dependency postcss to v8.2.7 (72db3754) + * update dependency nodebb-plugin-composer-default to v6.5.13 (017af7cb) + * update dependency jquery to v3.6.0 (dd6082a0) + * update dependency connect-mongo to v4.2.2 (ec0912cc) + * update dependency nodebb-plugin-spam-be-gone to v0.7.8 (#9337) (536bae70) + * update dependency nodebb-plugin-composer-default to v6.5.12 (2674de01) + * update socket.io packages to v3.1.2 (510eb1f9) + * update dependency nodebb-theme-persona to v10.5.16 (217d3afd) + * update dependency nodebb-plugin-emoji-android to v2.0.5 (e8209341) + * update dependency sharp to v0.27.2 (c5231f10) + * update dependency nodebb-theme-vanilla to v11.4.5 (8596dcc4) + * update dependency nodebb-theme-persona to v10.5.15 (753ab0a0) + * update dependency nodebb-theme-persona to v10.5.14 (ed503b80) + * update dependency nodebb-theme-persona to v10.5.12 (ddd8fa31) + * update dependency benchpressjs to v2.4.1 (4ee3a8e8) + * update dependency nodebb-theme-persona to v10.5.10 (7f8fd4b0) + * update dependency nodebb-theme-persona to v10.5.9 (5dd748c6) + * require xregexp 5.0.1 (86e911ba) + * update dependency xregexp to v5 (513cd1c3) + * update dependency nodebb-theme-persona to v10.5.8 (54b4dc1d) + * update dependency postcss to v8.2.6 (4d92af5a) + * update dependency nodebb-theme-persona to v10.5.7 (#9288) (c2459fd5) + * update dependency nodebb-plugin-composer-default to v6.5.10 (b312725f) + * update dependency nodebb-theme-persona to v10.5.6 (4599144f) + * update dependency nodebb-widget-essentials to v5.0.3 (#9284) (eb9f058f) + * update dependency nodebb-plugin-composer-default to v6.5.9 (6e14014b) + * update dependency nodebb-plugin-composer-default to v6.5.8 (674a31d1) + * update dependency nodebb-theme-slick to v1.4.3 (#9278) (d3923585) + * update dependency nodebb-theme-vanilla to v11.4.4 (#9279) (1f28e8c3) + * update dependency nodebb-theme-persona to v10.5.5 (#9277) (a7b46adc) + * update dependency connect-redis to v5.1.0 (#9276) (83a0b6b8) + * update dependency nodebb-theme-persona to v10.5.4 (#9270) (dc145284) + * update dependency nodebb-theme-vanilla to v11.4.3 (#9272) (2fda6774) + * update dependency nodebb-theme-slick to v1.4.2 (2b12905d) + * update dependency nodebb-theme-lavender to v5.2.1 (fb2f1143) + * update dependency nodebb-theme-slick to v1.4.1 (#9262) (2cfab367) + * update socket.io packages to v3.1.1 (#9253) (2147d386) + * update dependency postcss to v8.2.5 (1fa0d4f4) + * update dependency nodebb-plugin-emoji-android to v2.0.1 (42e365d9) + * update dependency nodebb-plugin-markdown to v8.12.6 (4fd6027b) + * update dependency nodebb-plugin-mentions to v2.13.7 (8a2fe3d9) + * update dependency nodebb-theme-vanilla to v11.4.2 (2326e9a6) + * update dependency nodebb-theme-persona to v10.5.3 (9245ffaf) + * update dependency nodebb-plugin-dbsearch to v4.2.0 (389690c3) + * update dependency nodebb-plugin-composer-default to v6.5.7 (13e12c95) + * update dependency json2csv to v5.0.6 (0aa8e03f) + * bump theme deps for #9244 (44019e28) + * update dependency mongodb to v3.6.4 (56e4e56b) + * update dependency nodebb-theme-persona to v10.5.1 (04411449) + * update dependency nodebb-theme-vanilla to v11.4.0 (#9238) (897d29ec) + * update dependency nodebb-theme-slick to v1.4.0 (#9237) (8e2deab4) + * update dependency nodebb-theme-persona to v10.5.0 (#9236) (4f842a79) + * update dependency nodebb-theme-lavender to v5.2.0 (47fd1634) + * update dependency nodebb-plugin-dbsearch to v4.1.3 (1e10ebfb) + * update dependency nodebb-plugin-composer-default to v6.5.6 (0e2b329b) + * update dependency autoprefixer to v10.2.4 (6c3b1fde) + * update dependency nodebb-plugin-markdown to v8.12.5 (05901fcd) + * update dependency nodebb-theme-persona to v10.4.1 (a9b3fb37) + * update dependency sharp to v0.27.1 (a90773a6) + * bump persona to get timeline style (ca14c0e2) + * update dependency postcss to v8.2.4 (5b2f0be0) + * update dependency autoprefixer to v10.2.3 (d99cb1cf) + * update dependency postcss-clean to v1.2.0 (4232d97b) +* **#9315:** api v3 post, put, del JSON (0d59fe3d) +* **remountable-routes:** + * more fixes to remountable routes (9d17f397) + * bug with user routes remounting to itself (bc68e990) +* **#9252:** pass site domain to nodemailer (#9254) (5e5d37c3) +* **topic-events:** + * topicEvents.init() test (aa8b84bb) + * repeated invocations of Posts.addTopicEvents caused dupes to be added to DOM (df2fdd56) +* **hooks:** + * bug where hook firing would fail if there were no listeners (efff8e2a) + * fallback handling for core invocations of hooks.fire (412d2858) + +##### Other Changes + +* schema docs for new ACP dashboard subpage routes (0804d547) + +##### Performance Improvements + +* increase batch size for notifs, run parallel (728ac5ff) +* faster category tags upgrade script (0dad568c) +* use setObjectBulk (95033ef7) +* make upgrade script faster (a07509f7) +* make upgrade script faster (0959b124) +* cache base_url (cf4002bc) +* single call to get digest topics, dont send duplicate topics (5ce28207) +* single db call to add all uids (90d5c9da) +* make digests a little bit faster (0185ea1b) +* only load thumbs for topics that actually have thumbs (7eebcbdb) + +##### Refactors + +* make debug handler async (1db8920b) +* widgets (#9471) (397baf02) +* style, no need to convert length to string (d00268c9) +* deprecate action:script.load, use filter:script.load instead (d1685600) +* remove uncessary check (f316c4d4) +* remove async.each/reduce from hooks for better stack traces (d05d7091) +* use hooks.fire (0d3979ef) +* fix variable name (1982edfd) +* account edit logic and template, closes #9364 (98bf4064) +* automatically authenticate all requests setup through route helpers (#9357) (7da061f0) +* async listen testSocket (0021c601) +* remove startsWith/endsWith (48bc23c0) +* app.parseAndTranslate to return promise if no callback passed (b5a6a314) +* privileges, export modules directly (#9325) (293b7c26) +* have Graph.init and Graph.update return promises (3fa2e3ce) +* abstract out some client side dashboard code into modules, analytics subpages for users, topics, and logins (f561799f) +* move picture change client-side logic to its own rjs module (28f6931e) +* remove dupe code (5286f208) +* thumbs.associate accepts both relative path and url in path arg (3e6640ef) +* move post queue retrival code to posts.getQueuedPosts (36f20211) +* call topic events init from within file itself (6074a0fb) +* improvements (970bd06f) +* update dom after diff deletion better (a2a7557c) +* removed 3 lines (4447a64e) +* use Map to track sorted lists in Settings.set() (65de2e76) +* **user:** all plugins to change list of icon background colours (fbccf6e2) +* **remountable-routes:** + * rename `src/routes/accounts.js` to `src/routes/user.js` to better match the route prefix (1f28713f) + * allow certain route prefixes to be mounted elsewhere (92758ec5) +* **topic-events:** + * expose addTopicEvents method in topic posts lib (9559fad8) + * break out some logic in events.get into local modifyEvent method (cec3fc93) + * fire topic event logging in topics/tools instead, pass uid into payload (425eca14) +* **hooks:** + * deprecate `action:script.load` client-side hook (8e5687a4) + * better error handling (e7bd038d) + +##### Reverts + +* revert tag sort (f9df6431) +* change toPid truthy (56523aa1) +* bring back backwards compat (a1c01446) + +##### Code Style Changes + +* eslint (b5ce8d25) +* **remountable-routes:** abstract removable routes code to a separate local fn (16c1d6e9) + +##### Tests + +* remove logs (435067aa) +* clear cache between runs, require middleware later in helpers (2ea468da) +* log (d15e2710) +* remove equals (354e0a82) +* test times (2f401d7d) +* log (80ef1082) +* added test for session id reroll on login (a3a7ab3a) +* add missing test (8ef38cb2) +* double filter test (70a653d0) +* admin/manage/users tests (0e67ab01) +* fix spec for topic thumbs (4c078084) +* added missing properties to topicObject (1d9ade4c) +* added missing test file (b31f6dd2) +* topic reordering tests (ad54b174) +* additional tests for topic thumbs (50664487) +* added more topic thumbnail tests (28b30134) +* post diff deletion tests (72b050b4) +* **user:** added additional tests for icon background colour (d3a9e76a) + +#### v1.16.2 (2021-01-21) + +##### Breaking Changes + +* unescape header navigation originalRoute [breaking] (6cb5888c) +* allow override of local fns in login controller, 400 instead of 500 for wrong login type [breaking] (1cf0032d) + +##### Chores + +* **deps:** + * update dependency husky to v4.3.8 (a6f5de86) + * update dependency eslint to v7.18.0 (afbef95f) + * update dependency husky to v4.3.7 (d3e041e2) +* incrementing version number - v1.16.2-beta.0 (43ff8e41) +* incrementing version number - v1.16.1 (e3cd7a23) +* update changelog for v1.16.1 (b6d71710) + +##### New Features + +* add filter:email.cancel to allow plugins to cancel sending emails (c2e23706) +* grant plugins the ability to specify options to the SSO handler (ab11435e) +* add unread-count badge if navigator contains /flags route (c07e1e16) +* handle HTTP 429 as a response code (8bbb3208) +* add write API route for checking login credentials (56f929ed) +* #8813, faster category search dropdown (072a0e32) +* **api:** schema definition for new credential checking route (0da28432) + +##### Bug Fixes + +* **deps:** + * update dependency bootbox to v5 (#8751) (b5cb2f8b) + * update dependency nodebb-theme-persona to v10.3.19 (f16cdc9f) + * update socket.io packages to v3.1.0 (3d1224e1) + * update dependency nodebb-theme-slick to v1.3.8 (1901ecb2) + * update dependency sortablejs to v1.13.0 (36069da2) + * update dependency autoprefixer to v10.2.1 (5b3c48fd) +* https://github.com/NodeBB/nodebb-plugin-webhooks/issues/3 (c608b0e8) +* restored sanity checks for post move socket calls (d85ad10d) +* don't chagne scrollTop if at the top of page (0fa4c11e) +* #9206, fix double escaped arguments (1590cdf1) +* regression caused by 77ab46686db62871f149419a368c35628453884e (f5fcd232) +* don't crash if fullname is not a string (4fb90787) +* #9204, bypass groupname length check for system group in upgrade script (00ba89b6) +* add missing await (9938a139) +* ssoState passed to strategies even if not called for (9b289eca) +* use max (0a471b76) +* keep 60 topics minimum on topic list (c30b40ab) +* access checks for tags and thumbs get route (77ab4668) +* #9194 global mods unable to pin or unpin topics (c0fb1cb5) +* #9192, exit after logging error (ef16cd2e) +* make sure inviter user exists (69419571) +* #9178 missing language strings (and fallbacks) for post-queue (a407a51d) +* #9185, fix string boolean values (89e6c75d) +* test for topicThumbs (e817d5be) +* #9184 proper relative_path usage in topic thumbs.get (66da6bcd) +* #9169, re-adding v2-style behaviour so as to not break the API... yet (b742229e) +* #9177, handled multiple deleted users properly (eaf62d39) +* broken test caused by errant .pop(), missing await (4ede18ce) +* missing error message (d83d40cf) +* test for https://github.com/NodeBB/NodeBB/pull/9180 (8ece64ab) +* #9176, limit description size (da546970) +* broken test due to change in response code (9534d956) +* return a user object, not an array of user objects (in v3 login check route) (97d678fd) +* bad execution flow in utilities.login (8c86f5bc) +* missing breadcrumbs in schema (87a7d85e) +* random loadFiles added by errant vscode autocompletion (53422413) +* add missing breadcrumb on /user//categories (6cbb77af) +* `--help` usage info (a51c5698) + +##### Performance Improvements + +* use only required calls (f0dd302c) + +##### Refactors + +* **api:** + * post move to write API (966c4117) + * post diffs to use write API (e118e59c) +* change var to const (1374e0ee) +* single remove call (25ab99b9) +* flags lib to have a separate getFlagIdsWithFilters method (6a1311b4) +* split out logic dedicated to calculating unread counts, to a separate local method (03a0e72f) + +##### Code Style Changes + +* update codeclimate config to be less sensitive to duplicate code blocks (fdf03472) + +#### v1.16.1 (2021-01-06) + +##### Chores + +* increase test timeout (0d7dfeeb) +* incrementing version number - v1.16.1-beta.0 (5fcf3ea6) +* add deprecation notice to topic thumb tpl value (05d8b3c3) +* minor reordering of lines (8e5a413e) +* incrementing version number - v1.16.0 (6d01fd50) +* update changelog for v1.16.0 (1437c62f) +* **deps:** + * update dependency eslint to v7.17.0 (18ae7cf7) + * update dependency eslint to v7.16.0 (2610dfcf) + * update actions/setup-node action to v2 (#9115) (55a55ea2) +* **api:** add deprecation notices re: #9123 (cdff8d28) + +##### New Features + +* #9173, show installed plugin versions in ./nodebb plugins (8c31afae) +* added note that you can now upload videos (4d6ddf6d) +* automatically attempt socket.io reconnection on ajaxify (e5edbc6f) +* #9135, don't try to reconnect forever (c1ecfd1e) +* add confirmation modal when assigning admin:admins-mods privilege (d90aa958) +* allow dashes in privilege group names (5b8558e9) +* allow multiple privileges to be defined for a given admin socket call (3aa5beb8) +* rename admin middleware header hook (fcc1e24a) +* explicitly add filter:admin/header.build hook (75b1bbd0) +* fix more tests, add more routes, update api test suite (cb32e32a) +* add registration/complete route, fix some other tests (14c51e3c) +* add missing schemas for various ACP settings routes (9de35ec5) +* add missing schema for category update and deletion (d6de9253) +* add schema for api ping routes (d85181e0) +* normalize paths before comparison (df8d62ba) +* additional test to ensure any new routes added to express have a corresponding schema doc (dbe85630) +* update html-to-text closes https://github.com/NodeBB/NodeBB/pull/8810 (a2152dd1) +* **api:** + * closes #9123 category and topic routes migrated to Write API (edb8da1e) + * #9123, migrate rest of the getObject controllers to Write API (9ecfac9b) + * #9123, migrate /api/post/pid/:pid to Write API (e267f295) + * group ownership API route, switch client-side to use API route (32e36f7b) + * add schema for groups update route (98550d61) + * added schema for email unsubscribe token (4fc13377) +* **acp:** + * admin tags privilege (223f0a55) + * admins-mods privilege (fb46a8d9) + * added new admin privilege for groups management (da191341) + +##### Bug Fixes + +* #9130, remove timestamp prefix from thumbnail names in API response (171017c3) +* #9166 missing relative path in topic thumbs modal and topic list (b9ba44ed) +* #9163, fix total connection count on ACP (1968bf50) +* genericise .necro-post, bump persona to latest (041d45c3) +* #9126, skip base64 and long values (33290850) +* #9127, use assets path (3121215e) +* inability for admins with setting privilege to save plugin settings (a555f024) +* #9149, server-side handling of disableChatMessageEditing (895e3d93) +* #9149, incorrect client-side `disableChatMessageEditing` value for admins/gmods (d27815a8) +* #9151, dont use service worker for posts requests (20c1b684) +* #9150, fix selector so it doesn't add img-responsive to profile pics (183cabe9) +* tests (28740360) +* dont show deleted posts in navigator (931105e6) +* bug in api path existence test (501a7b77) +* #9136, fix move topic/post timeout errors (2ef72a94) +* bad assignment logic in middleware.renderHeader (34ccabe3) +* #9113, wrong path separator used in thumbs.get (da4f9118) +* email testing and settings change from ACP (2be396ff) +* removing ability to specify deprecated topic 'thumb' on topic creation (713f029d) +* #9129, event is fired on socket.io (b369dc88) +* subfolder handling in tests (bbd97ccb) +* .flat() not defined in v10, added debug router to exclusion list (6062039d) +* all tests, wrap up work (f416dc17) +* two more routes (9c2de86a) +* api tests (b9a61d2d) +* don't return deleted: 0 for ephemeral groups (600807fb) +* send fewer items to client-side for ACP settings/email page (438fa5c8) +* errors in write-api schema (c079051b) +* broken tests from last round of fixes (990f1077) +* bad error message for request body api test (a9629357) +* modify backreference test to not check router.all() calls (7fc329de) +* add missing token generation route to write api spec (eef052c1) +* trigger action:posts.edited (b7b588f5) +* **deps:** + * update dependency autoprefixer to v10.2.0 (e445ae5a) + * update socket.io packages to v3.0.5 (fd045c67) + * update dependency nodebb-theme-persona to v10.3.16 (87e333b4) + * update dependency benchpressjs to v2.4.0 (4524f825) + * update dependency nodebb-theme-persona to v10.3.15 (189be9e0) + * update dependency nodebb-widget-essentials to v5.0.2 (1dd1d3b0) + * update dependency nodebb-widget-essentials to v5.0.1 (#9144) (f55dddb2) + * update dependency nodebb-plugin-composer-default to v6.5.5 (6d980d26) + * update dependency sharp to v0.27.0 (4919e596) + * update dependency nodebb-theme-persona to v10.3.12 (37b35f7d) + * update dependency nodebb-theme-persona to v10.3.11 (db4c6863) +* **tests:** handle nested allOf blocks (77a5adb6) +* **api:** + * failing test due to missing file (3959a7bd) + * tests (80ee3dfb) +* **pwa:** #9127 service-worker.js missing on subfolder installs (b8d4709e) + +##### Refactors + +* **openapi:** update TopicObject component to reference TopicObjectSlim in its schema (fb3f3f72) +* **api:** + * deprecated groups update socket in favour of API lib (1cd2689c) + * update group deletion calls to use write API (e640a41a) +* schema backreference test to use map instead of reduce, properly check write-api routes (878ee067) + +##### Tests + +* changed test a bit to see what is going on (5f038dff) + +#### v1.16.0 (2020-12-17) + +##### Breaking Changes + +* enable topic thumbnails across the board [breaking] (9342d611) +* #8808, remove utils.slugify (4a0d8833) + +##### Chores + +* **deps:** + * update dependency husky to v4.3.6 (2371b432) + * bump persona to 10.3.9 (91899329) + * bump composer to 6.5.1, re: #9067 (228cfa67) + * update dependency husky to v4.3.5 (48a31763) + * update dependency husky to v4.3.4 (cf5c482d) + * update dependency eslint to v7.15.0 (f4c4d671) + * update dependency lint-staged to v10.5.3 (3e6f7359) +* appease codeclimate (9f62df15) +* add comment for clarification (6037f5ee) +* incrementing version number - v1.15.5 (57cd1343) +* update changelog for v1.15.5 (b0299326) +* **i18n:** fallbacks for new topic thumb keys (15f1a089) + +##### New Features + +* add new client side hooks (a15ef53c) +* remove max age since cache is cleared when thumbs change (ab96f526) +* show alt text instead of images in teasers (#9107) (d28581eb) +* migration of old topic thumbs to new format (74d73313) +* allow plugins to override ACP relogin challenge (4c87f301) +* add user.email.confirmByUid for sso plugins (80de572a) +* add thumbs to category data return (24e754d1) +* broken test for bad topic thumbs logic (ce8057f3) +* clent-side modal for managing topic thumbs (a30c8ab5) +* raise maximum thumb size to 512 (37c367d6) +* associate topic thumbs with post uploads (for the mainPid) (1c5cdb51) +* helper method to get thumbs by pid (cb7e4cda) +* closes #9048, tests for topic thumbs routes, write API schema (59506833) +* tests for topic thumbs (4152aa55) +* server-side work for #9047 (ef7d6db9) +* core work for #9042, thumb deletion now accepts uuids (b5d910f5) +* more work on topic thumbs refactor (90497e3e) +* expose uploaded thumbnails to client-side via API (1257aa98) +* server-side routes for handling multiple topic thumbnails (7e9e08f7) +* allow uploadThumb controller to be called in code (98cd9e35) +* move upgrade script and make it shorter (60e7de0d) +* allow clicks on navigator, clean dupe code (74274b60) +* socket.io 3 changes (#8845) (1c45fa1b) +* **deps:** update lavender to allow category sections (6d186be0) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-plugin-composer-default to v6.5.4 (#9120) (fff0cea6) + * update dependency nodebb-theme-slick to v1.3.7 (#9112) (30688b1b) + * update dependency nodebb-theme-lavender to v5.0.17 (#9111) (877f4673) + * update dependency nodebb-theme-vanilla to v11.3.10 (ff18cdfa) + * update dependency validator to v13.5.2 (#9094) (5d718348) + * update dependency nodebb-theme-vanilla to v11.3.9 (#9091) (f37dbeed) + * update dependency nodebb-plugin-composer-default to v6.5.3 (d036408d) + * update dependency nodebb-plugin-composer-default to v6.5.2 (b07fb9ab) + * bump composer-default to 6.5.0 (0db49121) + * update dependency autoprefixer to v10.1.0 (024d1fef) + * update dependency nodebb-theme-persona to v10.3.8 (#9084) (25f697b1) + * update socket.io packages to v3.0.4 (62463430) + * update dependency nodebb-theme-persona to v10.3.7 (c22cdb51) + * update dependency nodebb-theme-persona to v10.3.6 (#9077) (5937fbaf) + * update dependency nodebb-plugin-mentions to v2.13.6 (#9071) (a535350f) + * update dependency nodebb-theme-slick to v1.3.6 (#9072) (19c438c6) + * update dependency nodebb-widget-essentials to v5 (#9070) (d7f5efd9) + * update dependency nodebb-plugin-markdown to v8.12.4 (8fb814ba) + * update dependency nodebb-theme-persona to v10.3.5 (#9060) (0d082280) + * update dependency nodebb-theme-persona to v10.3.4 (#9059) (84e4e480) + * update dependency nodebb-theme-persona to v10.3.3 (3d7e2e1e) + * update dependency nodebb-theme-persona to v10.3.2 (#9056) (f49ce4ad) + * update dependency nodebb-theme-persona to v10.3.1 (#9054) (344caf5c) + * update dependency nodebb-theme-lavender to v5.0.15 (#9053) (e7d72d8a) + * update dependency nodebb-theme-persona to v10.3.0 (#9052) (dcd6fbaf) +* api usage (feecd665) +* #9117, lower query before search (4404e32e) +* #9114, fix client side groups update for memberPostCids (3ed55799) +* test (2dee3cbe) +* don't check "select all" if there are no enabled checkboxes (3ba05755) +* #9074, fix svg uploads (8f938eba) +* #9100 topic thumbs in OG image tags (ab987408) +* update version removal comments to 1.17 for some features (378a3a69) +* postgres is slow:tm: (05dd8597) +* derp? (f8dff94a) +* attempted fix for psql test in topic thumbs (9a4ea04a) +* use getSortedSetRange instead of getSortedSetsMembers (edf67f34) +* tests (bd5c4a5c) +* bad topic thumbs logic on local thumb upload (e83baa97) +* #9092, Topic thumbnails do not work with third-party uploaders (3e54b70c) +* move topic thumb tests to root level, so they actually get run by mocha (dd448e2b) +* tests for topic thumbs (9681557f) +* iteration logic bug (2170c400) +* spec (ae943974) +* changes to thumb resizing logic (67cf5e83) +* use file lib instead of direct fs module access (08736b18) +* added back missing topic thumb tests that were removed in last commit (c043cfeb) +* tests (5ec3b3d0) +* hack uploader to handle a response from v3 write api (41379e27) +* #9055, non-standard API response from addThumbs route (340387c1) +* do not allow thumb deletion route to arbitrarily delete other files in uploads folder (c09c238e) +* missing file added (ef10b6b7) +* references to since-removed Topics.thumbs.resizeAndUpload (1f0c1cd2) +* #9041, remove Topics.thumbs.resizeAndUpload() (43dc3e3e) +* #9040 (708b1c33) +* spec (1949d20a) +* #9085, dont prevent admins from deleting other users (0f480be6) +* show errors when user delete fails (ff2aa17b) +* dont start logout timer if adminReloginDuration is disabled (dd9ed236) +* #9045, no post usage info if '/files/' path received (efa4eca0) +* reconnectin no longer fires on socket.io 3 (13d5a144) +* default values, clamp postsPerPage/topicsPerPage to max (1f32d387) +* #9081, load raw settings before merging (9da0ed40) +* #9068 (86f0f82b) +* remove old utils.slugify tests (10cfdd4c) +* dont strip tags (792e9e70) +* #9065, settings v2/v3 conflict (91c20cec) +* #9063, missing handler for passwordless accounts in admin.checkPrivileges middleware (970ccb5a) +* timeago in navigation (a389a31b) +* navigation fixes (163d1a39) +* cache some jquery objects (73d2f51d) +* add ev.cancelable (63d08395) +* #9046, pretranslate string (790f4e45) +* redirect external with absolute urls (648f6215) +* external path for subfolder installs (458bfc0f) +* **spec:** broken link to status component (d31aae16) + +##### Performance Improvements + +* don't load thumbs if disabled globally, cache thumb results (2d5a224b) +* dont build identical langs (bb6cc49c) + +##### Refactors + +* topic thumbs lib to topics.thumbs (4fc9da81) + +#### v1.15.5 (2020-12-03) + +##### Chores + +* up persona (c111bde1) +* incrementing version number - v1.15.4 (a1b658d9) +* update changelog for v1.15.4 (252dddfc) + +##### New Features + +* add socket connect/disconnect action hooks (fcb10ebd) +* allow modifiying browser title on ajaxify (698718f8) + +##### Bug Fixes + +* #9032 (64ac483d) +* sso redirect on /login & /api/login (5d00b089) +* use file lib instead of directly accessing fs (for Assert.path) (3ea66f84) +* check uid as well (ef6c3b00) +* #7597, fix progress bar of cover/profile uploads (7e867cf9) +* **deps:** update dependency nodebb-plugin-spam-be-gone to v0.7.7 (#9039) (c7f2640a) + +#### v1.15.4 (2020-12-02) + +##### Chores + +* up persona (dde3171c) +* fallbacks for nodebb.error (82ca3760) +* fallbacks for nodebb.topic (5b269bc5) +* remove test code (07fe959c) +* incrementing version number - v1.15.3 (d1ae08fa) +* update changelog for v1.15.3 (cf157c9b) + +##### New Features + +* #9005, use timestamp in profile/cover images (5f0f476b) +* #8983, update pin tooltip in topic (954dc5b7) +* option to allow auto-joining of groups (optionally skip the "request membership" step) (685f3c6a) +* user notification settings for group.leave event (c1a7968d) +* add defaults for composer help (0cba2691) +* #8900, postQueue setting for category (1eb5fabd) +* #8960, update view count after merge (14bb0a44) +* use correct code (557f0f56) +* #8989, convert widget nav to dropdown (4c650aee) +* add handler for 501 api response (007a3258) +* add translation key for pin icon label with expiry (12b3aa0d) +* add pinExpiry and pinExpiryISO to topic data (ad8e7700) +* add cancel button to pin expiration modal (e1432caf) + +##### Bug Fixes + +* #9032, fix login redirect for sso plugins (6f68f4d2) +* #8962, dont show null for purged targets (86b7f8a5) +* selector on hooks page (3488daa1) +* notification on group.leave incorrectly showing "Guest has left X group" (f7558c60) +* #9019, add missing lang strings (b46d2f93) +* #9018 (e45b5cba) +* #9015, add default value for dailyDigestFreq (0f1fc10f) +* spec (cfb7b113) +* #8997, don't send notifications if uids already in group (f7c738de) +* #9002 ban templates not user friendly (4317cdea) +* #9010, show rest of info even if clusterMonitor priv is not granted (202dcef4) +* #9007 revoke old sessions after adding (d46740f8) +* guests dont always have sid (70073653) +* allow guests to see their replies immediately (a4fe4d3c) +* privs headers (92d1b8a6) +* pwd reset test (f25000cb) +* #8991, logout on password reset, dont verify email if password expired (5080f357) +* don't show topic search if no search privilege (8adbf54a) +* #8998, allow guests to use write api to post/reply (3cd0c9a4) +* guest handles to user displayname as well (5a137a0d) +* timestamp in queue, add post queue strings (546f58bf) +* #8992, set email:confirmed for first admin user (7f5efc3e) +* typo in upgrade script, closes #8990 (80f0750b) +* #8984, post-queue ux (1269103f) +* order (9ab4fb41) +* #8982, copy color on tag rename, dont copy if target exists (d3c04afb) +* tests (b596e948) +* api test (77a6dbac) +* remove dupe (cbbda451) +* csv test (3de692cd) +* spec, remove old tests (4afdf8bc) +* #8969, export csv to file (6e6a7a8f) +* spec for /tag (88e5cda5) +* #8980, fix lang string (f4d217d8) +* #8979 (bf171adc) +* #8971, disallow flags of privileged users (mods, gmods, admins) (1e7cf1cb) +* #8974, with password login for approval queue (dadb2527) +* #8974, dont show wrong message on register queue (fdca8b16) +* #8973, fix timestamp on ban modal (5c3deb4b) +* #8968, don't show topic search if search is not enabled (c8554b78) +* flicker on tooltips if server call takes long time (4c7374ea) +* missing select/clear all checkbox added to category privileges template (#8967) (a56a6577) +* use package.name for theme.id (#8965) (ba3981e2) +* winston usages (b8cafefc) +* #8957 (414caac0) +* dont go back after delete account actions (7e6427bc) +* error message (47a19d67) +* #8954, clear purged replies and toPids (#8959) (5bb5ec46) +* #8955, popstate to purged topic should go to homepage (39dae0aa) +* 'already-deleting' error on subsequent account content deletions (21d6225c) +* #8949, faster upgrade script (93863bb3) +* **deps:** + * update dependency nodebb-theme-vanilla to v11.3.8 (#9031) (18707940) + * update dependency nodebb-theme-slick to v1.3.5 (#9030) (4085f3e6) + * update dependency nodebb-theme-persona to v10.2.98 (#9029) (f7d60c43) + * update dependency nodebb-theme-persona to v10.2.97 (42b23a3b) + * update dependency nodebb-plugin-composer-default to v6.4.10 (#9025) (43bbfb67) + * update dependency nodebb-theme-vanilla to v11.3.7 (#9024) (3f597a55) + * update dependency nodebb-plugin-composer-default to v6.4.9 (#9023) (110186b9) + * update dependency nodebb-theme-slick to v1.3.4 (#9022) (8dc1437e) + * update dependency nodebb-theme-persona to v10.2.96 (#9021) (2c9cd286) + * update dependency nodebb-plugin-composer-default to v6.4.8 (#9017) (1f5f2e1d) + * update dependency nodebb-plugin-markdown to v8.12.3 (9004319e) + * update dependency validator to v13.5.1 (7b39cf4b) + * update dependency nodebb-theme-persona to v10.2.95 (#9001) (4ddab380) + * update dependency nodebb-theme-persona to v10.2.94 (#9000) (877d8554) + * update dependency nodebb-theme-persona to v10.2.93 (#8999) (c44d9d2f) + * update dependency nodebb-theme-persona to v10.2.92 (#8995) (346b91eb) + * update dependency nodebb-theme-vanilla to v11.3.6 (#8987) (6c980db1) + * update dependency nodebb-theme-persona to v10.2.91 (#8986) (8258536a) + * update dependency autoprefixer to v10.0.4 (#8985) (fad2d342) + * update dependency nodebb-plugin-markdown to v8.12.2 (f5714452) + * update dependency nodebb-theme-persona to v10.2.90 (5664807d) + * update dependency nodebb-theme-vanilla to v11.3.5 (19fe2493) + * update dependency nodebb-theme-persona to v10.2.89 (ad60bc06) + * update dependency autoprefixer to v10.0.3 (b2f0d38f) + * update dependency benchpressjs to v2.3.0 (6c316be4) +* **openapi:** + * spec for c1a7968d23f0809e7012edfccf49b193749998ec (69864b87) + * spec for 685f3c6aa6173383d6c31b87ed51cf8ed0ca44ce (1bb75e76) +* **acp:** + * #9008 undefined link for "no users browsing" state on dashboard (54dc449f) + * #9009 no-users-browsing untranslated on dashboard (286243cd) +* **spec:** + * from 6e6a7a8f8a9a75500ba1f336cabc882234212f88 (acb57666) + * breaking tests (88a60473) + * broken test due to canFlag addition (1b1205a9) + +##### Refactors + +* remove old hack (73746bb4) +* add TopicObjectSlim common schema (22715d54) +* pin/lock threadTools to use topicCommand, rewrote topicCommand to match categoryCommand signature (15c6f32c) + +#### v1.15.3 (2020-11-26) + +##### Chores + +* bump persona (720170a9) +* remove console.log (6a819944) +* move topic route schema up two levels as slug and index are optional (ae402e21) +* move category route schema up two levels as slug and index are optional (b3b501d7) +* up persona (0ffc091b) +* up persona (2f2f0ab7) +* up persona (5c9ff18e) +* add missing plugin hook deprecation warning (98a05e4d) +* pin autoprefixer to latest (46eb7701) +* incrementing version number - v1.15.3-beta.0 (28fa03bd) +* up persona (81984285) +* up slick (5f2fe883) +* incrementing version number - v1.15.2 (5867a5b9) +* update changelog for v1.15.2 (37965d45) +* **spec:** replace ugly hack with another hack for optional properties (45a4f136) +* **deps:** + * update dependency lint-staged to v10.5.2 (db205e2e) + * update dependency eslint to v7.14.0 (80404216) + +##### New Features + +* add topicOwnerPost #8778 (c037779f) +* clear reset tokens on user delete (4f37eddc) +* select/clear all checkboxes in privilege table (#8941) (00e75de7) +* show ban reason and expiry in write api responses, if user is banned (afb26bfe) +* automatically unban users in onSuccessfulLogin (6e5ec3f8) +* #8925, #8924 (3f337b5d) +* human readable uptime (672d4da0) +* allow ACP API access to bearer tokens (3b1c03ed) +* allow pins to expire (if set) (#8908) (046d0b16) +* #8637 (903e9d82) +* add displayname into user obj #8637 (#8909) (9ca44e6f) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-theme-persona to v10.2.87 (#8946) (167ab3a4) + * update dependency nodebb-theme-persona to v10.2.86 (#8945) (5af5cb85) + * update dependency nconf to ^0.11.0 (58152606) + * update dependency postcss to v8.1.10 (5363ebbb) + * update dependency nodebb-theme-persona to v10.2.85 (#8928) (abc32d62) + * update dependency postcss to v8.1.9 (d1cb5d48) + * update dependency postcss to v8.1.8 (b47a470b) + * update dependency nodebb-theme-vanilla to v11.3.4 (#8914) (589f7a56) + * update dependency nodebb-theme-persona to v10.2.80 (#8913) (38127b04) + * update dependency nodebb-theme-persona to v10.2.79 (#8907) (8e1b2458) + * update dependency nodebb-theme-persona to v10.2.75 (b9856179) +* add topic uid to infinitescroll (6771ca15) +* #8943, session mismatch modal thrown on login (race condition) (d5845169) +* #8912 (ac734b83) +* #8918 (e32cd31e) +* basepath for r.js modules (3af4d13f) +* test (61c6a762) +* move meta.getServerTime call to admin namespace (1c0e8c16) +* add client side check for userslug #8939 (f20c12ee) +* #8939, fix username change notification getting filtered out (0ca40af8) +* #8931, fix lang string (cf903e4e) +* #8932, fix client side error when updating username (95a3f030) +* bug with Topics.resizeAndUploadThumb not checking for extension validity (eab4ca71) +* #8933 (2b73a14e) +* #8929, fix popular, top rss feed urls (77f0bff5) +* a derp (5dd3b031) +* spec (b18e7e31) +* improper handling of scheme-relative URLs in topic thumb logic (4ca62dc4) +* https://github.com/NodeBB/NodeBB/pull/8685 (5fa09832) +* on OP edit, call helper method to upload and resize thumb (f33a9185) +* https://github.com/NodeBB/NodeBB/pull/8759 (9ee1fb49) +* spec (c2bb6123) +* guest displayname (1be08b2e) +* show messages after app load (46acbfda) +* restart on js changes in vendor (814771bd) +* #8915, fix queue not being cleared after firing click events (6ef7e867) +* spec, only call modifyUser on unique user objects (dbd814c2) +* setting (ae5d4405) +* spec (8d060065) +* group userTitles translation escapes (e9585b9b) +* remove params from error log (965671a9) +* **spec:** always show thumb in topic response (493c568a) + +##### Refactors + +* remove unused require (db1c140f) +* move API banned response handler to separate internal method (906d7d73) +* move plugin hook methods to plugin.hooks.* (6e2da996) +* remove breaking change in pin expiry (ef3df47a) +* use categoryCommand local method for pin/lock in category tools (#8917) (00aee84b) + +#### v1.15.2 (2020-11-18) + +##### Chores + +* incrementing version number - v1.15.2-beta.1 (20c05e80) +* remove debug log (0a0b4661) +* incrementing version number - v1.15.2-beta.0 (996174a1) +* **deps:** update dependency smtp-server to v3.8.0 (5f5f0edb) + +##### Documentation Changes + +* openapi schema for user/group exist check, session deletion (bcccb331) + +##### New Features + +* #5274 (4e9b10ab) +* #4456 (fb567a7a) +* #8475, allow flagging self posts (a6afcfd5) +* #7550, show message if post is queued when js is disabled (120999bf) +* #8171, add oldCategory if topic is moved (35f932cd) +* #8204, separate notification type for group chats (b44ddecd) +* add test for custom translations (7928036a) +* https://github.com/NodeBB/NodeBB/issues/8147 (1d6bcbeb) +* invites regardless of registration type, invite privilege, groups to join on acceptance (#8786) (3ccebf11) +* add nodejs version to issue template (29c2ca94) +* allow groups to specify which cids to show member posts from (#8875) (8518404e) +* **api:** account deletion routes for the Write API (#8881) (a0b7a823) +* **deps:** benchpressjs@2.2.1 (#8887) (d30ea256) + +##### Bug Fixes + +* #6407, fix feeds (fa4177c3) +* navigation highlight (62b62821) +* benchpress warnings (a87ccccc) +* benchpress warnings (dfdc0c42) +* internal helper method hasGlobalPrivilege, DRY (e1d7c4d8) +* spec (2f4653a3) +* #8884, remove header/footer cache (e4d2764d) +* check tid in event handlers client side (9dac9630) +* #8883 (f14e42d8) +* #8872 missing admin/menu language key+fallback (fdab76f8) +* run every hour, dont show message if average_time is 0 (51b7eca1) +* add back test timeout for exports (b3e00489) +* pass length to messaging checkContent hook (dc9668e4) +* **deps:** + * update dependency nodebb-theme-persona to v10.2.74 (#8905) (5f6137f0) + * update dependency nodebb-theme-persona to v10.2.73 (#8904) (c2019b7b) + * update dependency nodebb-plugin-composer-default to v6.4.7 (#8902) (b7b1f203) + * update dependency nodebb-theme-persona to v10.2.72 (#8903) (bee8cfe4) + * update dependency nodebb-theme-vanilla to v11.3.3 (422aa7f0) + * update dependency nodebb-theme-persona to v10.2.71 (3b6e71d5) + * update dependency nodebb-theme-persona to v10.2.70 (d6dd1cb6) + * update dependency nodebb-theme-slick to v1.3.2 (2ce1fcd3) + * update dependency nodebb-theme-vanilla to v11.3.2 (9f2d0d42) + * update dependency nodebb-theme-persona to v10.2.69 (15810643) + * update dependency sharp to v0.26.3 (84d54577) + * update dependency nodebb-plugin-mentions to v2.13.5 (dde9f189) + * update dependency json2csv to v5.0.5 (b63b7ae5) + * update dependency benchpressjs to v2.2.2 (d1e804aa) + * update dependency nodebb-theme-vanilla to v11.3.1 (#8878) (85aaeded) + * update dependency nodebb-theme-slick to v1.3.1 (bbfb276a) + * update dependency nodebb-theme-persona to v10.2.68 (ec70329a) + * update dependency nodebb-plugin-spam-be-gone to v0.7.6 (#8877) (45922fae) + * update dependency nodebb-plugin-spam-be-gone to v0.7.5 (#8876) (48e82520) + * update dependency nodebb-plugin-spam-be-gone to v0.7.4 (#8874) (eab27f23) + +##### Refactors + +* client-side handlers for user invitations (d83eb7f8) +* async/await controllers/index.js (5598130a) +* less dupe code (8fbe8324) + +##### Tests + +* new api test to ensure each path's parameters are defined in context (97842c43) +* updated test name to be more specific (319cfeaa) + +#### v1.15.1 (2020-11-11) + +##### Chores + +* incrementing version number - v1.15.1-beta.0 (e033da8a) +* fallback l10n for admin-settings-api (8368c25b) +* **deps:** + * update dependency eslint to v7.13.0 (bcbc0854) + * update dependency eslint-config-airbnb-base to v14.2.1 (d227fe9f) + * update commitlint monorepo to v11 (90bcfa6d) + +##### New Features + +* #8864, add action:events.log (9c5c32d4) +* show db info side by side (62c0454c) +* add language keys for admin-settings-api (d32e4e02) +* #8824, cache refactor (#8851) (f1f9b225) +* move mkdirp to beforeBuild so it doesnt get called twice (6255874e) +* group exists API call in write api (d2631922) +* user exist route in write api (1446cec7) +* new shorthand route /api/v3/users/bySlug/:userslug (60e1e99b) +* allow passwords with length > 73 characters (#8818) (512f6de6) +* #8821, allow guest topic views (9e3eb5d4) + +##### Bug Fixes + +* #8869, dont escape category title,description twice (567c5f20) +* refresh flags list on bulk action success (769aba0a) +* test breakage from f300c933a50263039a57811f8cc716df39a138b0 (ee4d90f6) +* remove some unnecessary jquery wrappers (9f7902ef) +* send back jquery object to keep backwards compat (978f1ee0) +* use header/footer cache in prod (a0164b1c) +* add missing maxAge to cache (05a92885) +* clear header-cache after each suite (3f5f38dd) +* show msg on fail (255cf43e) +* spec (fe63c6ae) +* guest header/footer cache (2e446392) +* #8846, possible fix (74951f59) +* winston error message (16d03975) +* permanent redirect on user api redirect shorthand (6b196a20) +* user exist route needs no authentication (f2bb42c0) +* #8840, don't crash if /compose route is called with no query params (c61dee4b) +* XSS in event:banned messaging modal (f68bce86) +* #8838, fix chat dropdown timestamps (78ee8332) +* #8836, truncate fullname (76cd5b0f) +* #8827, do not require admin:users privilege to ban users (891a1ea2) +* **deps:** + * update dependency nodebb-plugin-mentions to v2.13.4 (1460a7a8) + * update dependency diff to v5 (72d1b3cd) + * update dependency nodebb-theme-persona to v10.2.67 (#8847) (e250c3f1) + * update dependency json2csv to v5.0.4 (#8865) (ba7b23ac) + * update dependency postcss to v8.1.7 (#8866) (2946bb16) + * update dependency nodebb-theme-slick to v1.2.40 (#8863) (20f4fe08) + * update dependency nodebb-plugin-mentions to v2.13.3 (#8862) (c18f4491) + * update dependency benchpressjs to v2.1.0 (14ba6383) + * update dependency benchpressjs to v2.0.9 (381a32ab) + * update dependency mongodb to v3.6.3 (#8841) (7e273e77) + * update dependency nodebb-theme-persona to v10.2.66 (#8839) (00f90cd9) + * update dependency nodebb-plugin-mentions to v2.13.2 (#8835) (064c99cd) + * update dependency postcss to v8.1.6 (e0cf9740) + * update dependency @nodebb/socket.io-adapter-mongo to v3.1.1 (#8831) (40eb658b) + * update dependency @nodebb/mubsub to v1.7.1 (#8830) (7b8a5567) + * update dependency postcss to v8.1.5 (9f5ef9d0) +* **#8828:** web install templates now compiled (#8832) (de5a21f1) +* **acp:** max-height for plugin menu list (eec630f1) + +##### Refactors + +* move session revocation route to write api (f300c933) +* change Benchpress.parse to .render (#8856) (e128264b) + +#### v1.15.0 (2020-11-04) + +##### Breaking Changes + +* remove toolsVisible in post tools' menu [breaking] (7e7366be) + +##### Chores + +* update changelog for v1.15.0-rc.5 (69f4d5ac) +* incrementing version number - v1.15.0-rc.5 (4bd2788b) +* add info log into tests (b6d86878) +* update renovate config to automatically merge non-major version bumps #yolo (1c8b8ce1) +* update changelog for v1.15.0-rc.4 (16882941) +* incrementing version number - v1.15.0-rc.4 (57b49320) +* update changelog for v1.15.0-rc.3 (2738041b) +* incrementing version number - v1.15.0-rc.3 (6de7760a) +* bump persona (e0b67817) +* update changelog for v1.15.0-rc.2 (c3a7ab54) +* incrementing version number - v1.15.0-rc.2 (7c084134) +* update changelog for v1.15.0-rc.1 (4f49b3da) +* incrementing version number - v1.15.0-rc.1 (943424b5) +* update changelog for v1.15.0-rc.0 (aab5f018) +* incrementing version number - v1.15.0-rc.0 (8d48455b) +* update changelog for v1.15.0-beta.30 (39648722) +* incrementing version number - v1.15.0-beta.30 (2dd3c962) +* some optimizations for codeclimate (4a63c20a) +* update changelog for v1.15.0-beta.29 (63696c40) +* incrementing version number - v1.15.0-beta.29 (fca176d2) +* refactor src/emailer.js (75459517) +* update changelog for v1.15.0-beta.28 (fd975a66) +* incrementing version number - v1.15.0-beta.28 (57f83162) +* update changelog for v1.15.0-beta.27 (70673824) +* incrementing version number - v1.15.0-beta.27 (78fad240) +* lint notifications.js (4fc84e9f) +* update changelog for v1.15.0-beta.26 (5e01d288) +* incrementing version number - v1.15.0-beta.26 (62c44095) +* up persona (ff8a833a) +* up persona (4a0f54ae) +* update changelog for v1.15.0-beta.25 (240a04b4) +* incrementing version number - v1.15.0-beta.25 (1124d4ee) +* up persona (032e6001) +* update changelog for v1.15.0-beta.24 (72ab5ba1) +* incrementing version number - v1.15.0-beta.24 (23ba071d) +* up persona (5407bf48) +* up persona (4c2eab4b) +* up persona (91133b0f) +* up persona (bcac0805) +* up persona (9dc4db1f) +* up persona (ba70a1bf) +* update changelog for v1.15.0-beta.23 (e6ea208c) +* incrementing version number - v1.15.0-beta.23 (61a04eb9) +* update changelog for v1.15.0-beta.22 (59ee74c2) +* incrementing version number - v1.15.0-beta.22 (9c9329ee) +* update changelog for v1.15.0-beta.21 (8089542e) +* incrementing version number - v1.15.0-beta.21 (966cdc13) +* update changelog for v1.15.0-beta.20 (1650f303) +* incrementing version number - v1.15.0-beta.20 (c8e57ec1) +* update changelog for v1.15.0-beta.19 (9cde5105) +* incrementing version number - v1.15.0-beta.19 (2eb7c284) +* update changelog for v1.15.0-beta.18 (b2ca9686) +* incrementing version number - v1.15.0-beta.18 (e096791e) +* update changelog for v1.15.0-beta.17 (d6178158) +* incrementing version number - v1.15.0-beta.17 (f1c28092) +* update changelog for v1.15.0-beta.16 (e08297ff) +* incrementing version number - v1.15.0-beta.16 (6043c9cb) +* update changelog for v1.15.0-beta.15 (f878b92c) +* incrementing version number - v1.15.0-beta.15 (10b94f9b) +* update changelog for v1.15.0-beta.14 (5e60d092) +* incrementing version number - v1.15.0-beta.14 (6ca4b86f) +* update commitlint config (61a8c6f0) +* update changelog for v1.15.0-beta.13 (ca8d492b) +* incrementing version number - v1.15.0-beta.13 (bdd71c51) +* update changelog for v1.15.0-beta.12 (0a242605) +* incrementing version number - v1.15.0-beta.12 (b852c9b7) +* update changelog for v1.15.0-beta.11 (f07ba866) +* incrementing version number - v1.15.0-beta.11 (c1ce7391) +* update changelog for v1.15.0-beta.10 (9c34eced) +* incrementing version number - v1.15.0-beta.10 (f252d4d7) +* update changelog for v1.15.0-beta.9 (affc7927) +* incrementing version number - v1.15.0-beta.9 (8dc513da) +* update changelog for v1.15.0-beta.8 (3b960c3b) +* incrementing version number - v1.15.0-beta.8 (34bb869c) +* update changelog for v1.15.0-beta.7 (e449ff9a) +* incrementing version number - v1.15.0-beta.7 (fd917b8e) +* update changelog for v1.15.0-beta.6 (a7f03b64) +* incrementing version number - v1.15.0-beta.6 (a9019cbe) +* update changelog for v1.15.0-beta.5 (0e90064e) +* incrementing version number - v1.15.0-beta.5 (81e4c4df) +* update changelog for v1.15.0-beta.4 (6c4aed8c) +* incrementing version number - v1.15.0-beta.4 (f2726a6d) +* update changelog for v1.15.0-beta.3 (11e2ccab) +* incrementing version number - v1.15.0-beta.3 (39178c2d) +* update changelog for v1.15.0-beta.2 (d8a9f1c3) +* incrementing version number - v1.15.0-beta.2 (59b50d0a) +* update changelog for v1.15.0-beta.1 (aeca19d5) +* incrementing version number - v1.15.0-beta.1 (4fa57fbb) +* incrementing version number - v1.15.0-beta.0 (bff80983) +* incrementing version number - v1.14.3-beta.16 (d9a05035) +* incrementing version number - v1.14.3-beta.15 (e2a253f2) +* incrementing version number - v1.14.3-beta.14 (9500871e) +* incrementing version number - v1.14.3-beta.13 (b5a2e4b6) +* incrementing version number - v1.14.3-beta.12 (f4c986a7) +* incrementing version number - v1.14.3-beta.11 (8618c32a) +* incrementing version number - v1.14.3-beta.10 (fa341714) +* incrementing version number - v1.14.3-beta.9 (9945c409) +* incrementing version number - v1.14.3-beta.8 (b5dcce9c) +* incrementing version number - v1.14.3-beta.7 (2531c44c) +* incrementing version number - v1.14.3-beta.6 (97088f68) +* incrementing version number - v1.14.3-beta.5 (a33a8a5a) +* incrementing version number - v1.14.3-beta.4 (55b3e376) +* incrementing version number - v1.14.3-beta.3 (5a5abf3c) +* incrementing version number - v1.14.3-beta.2 (bbab183f) +* incrementing version number - v1.14.3-beta.1 (2c06f6ac) +* incrementing version number - v1.14.3-beta.0 (3f87d5f9) +* update changelog for v1.14.2 (cd94c24a) +* incrementing version number - v1.14.2 (1e4d683f) +* update changelog for v1.14.2 (488e69fd) +* **deps:** + * update dependency mocha to v8.2.1 (35e725d1) + * update dependency lint-staged to v10.5.1 (2fa78e43) + * update dependency eslint to v7.12.1 (#8799) (224502d8) + * update dependency lint-staged to v10.5.0 (#8797) (1f2eca6a) + * update dependency eslint to v7.12.0 (#8791) (3108f628) + * update dependency lint-staged to v10.4.2 (#8773) (4bfd0087) + * update dependency eslint to v7.11.0 (#8747) (bb85c059) + * update dependency lint-staged to v10.4.1 (#8771) (08a240fa) + * update dependency mocha to v8.2.0 (#8772) (a471b1af) + * update dependency eslint-plugin-import to v2.22.1 (aacf8f22) + * update dependency eslint to v7.10.0 (#8687) (119ab719) + * update dependency lint-staged to v10.4.0 (2833624e) + * update dependency eslint to v7.9.0 (b1d781e8) + * update dependency eslint to v7.8.1 (#8597) (39110276) + * update dependency lint-staged to v10.3.0 (#8608) (dc1f1db9) + * update dependency mocha to v8.1.3 (#8588) (05efeff6) + * update dependency eslint to v7.8.0 (#8594) (e1b98142) + * update dependency lint-staged to v10.2.13 (efc30e97) + * update dependency mocha to v8.1.2 (#8579) (c722b0e5) + * update dependency grunt to v1.3.0 (df61d080) + * update commitlint monorepo to v9.1.2 (877cdfb9) + * update dependency eslint to v7.7.0 (#8564) (f1398da2) + * update dependency jsdom to v16.4.0 (#8554) (bb8f7c74) + * update dependency eslint-config-airbnb-base to v14.2.0 (#8396) (fde4f110) + * update dependency eslint-plugin-import to v2.22.0 (#8390) (788a8bfe) + * update dependency mocha to v8.1.1 (fe352eb1) + * update dependency eslint to v7.6.0 (#8540) (37418375) + * update dependency mocha to v8.1.0 (#8536) (72a78833) + * update dependency @apidevtools/swagger-parser to v10.0.1 (#8517) (8c498fa1) + * update dependency nyc to v15.1.0 (#8353) (c872bd8b) + * update dependency eslint to v7.5.0 (#8463) (b2be329c) + * update dependency jsdom to v16.3.0 (#8483) (82ada1bd) + * update dependency grunt to v1.2.1 (#8462) (b2dc6d60) + * update dependency @apidevtools/swagger-parser to v10 (9e2c3ce0) +* **writeapi:** cleanup (f6782471) + +##### New Features + +* add back error handling for a number of api calls (1afd2150) +* #8823, remove hardcoded write concern (a338f527) +* #8817, add login clientside hooks (715775a7) +* use github actions for ci (#8811) (eddf4a4f) +* allow mods/admins to see deleted posts on user profile (6e85920c) +* rearrange buttons on manage/users (27016d22) +* #8801, disable express compression by default (6ac73ccb) +* allow passing subset of user settings on update route (ec03af7a) +* wip, write api tests framework (b156b8b5) +* add filter:category.getFields (88a07e69) +* move postercount to topic hash (0db0231c) +* add free and total mem usage to info (a7b6d0df) +* new filter filter:teasers.configureStripTags (177a9610) +* send 'Vary' header when ACAO header set (d68ffea8) +* add filter.topics.getPostReplies (7a019494) +* topic reply to use api lib (also + missing file) (21974a77) +* send 401 for invalid-uid (ede9435f) +* async/await admin/search (c913900e) +* ignore test folder (2927509a) +* send back 403 on no-privileges error (14f9d8b0) +* refactor groups.delete (8ae1f81c) +* move groups.join to api (d69e503d) +* allow plugins to define api routes (9dd3cc04) +* require https if nodebb is configured with https url (a4ba2389) +* add nyc config (3326d80c) +* change user search to use filters array (a2edb86d) +* add filter (959314c9) +* require csrf token if not using bearer token (1e07886f) +* #8662, verified/unverified user groups (682e926c) +* more fixes (700e1e43) +* more work (40a05b70) +* wip admin/users (b038ac07) +* set unread false for guests (7beaf490) +* add stack to deprecate message (01265d08) +* add checkbox tests (d76229c0) +* add test for serialize/deserialize form (e92d4719) +* disable write api tests until fixed (ae5fb103) +* #8734, add slugify module, deprecate utils.slugify (bddfcb58) +* #8734, move bootstrap-tagsinput to package.json (f16c8268) +* #8734, move deserialize/serialize to package.json (eab7489e) +* #5964, #8734 remove colorpicker (948f2614) +* #8734 remove unused files (c721625a) +* #8734, move sortable to package.json (cc705e5e) +* #8734, move bootbox to package.json (300a8755) +* load jquery-form before using (3b231360) +* #8734, jquery-ui, jquery-form, timeago (#8748) (fda2aedf) +* remove unused textcomplete file (59311a63) +* #8734, remove semver.browser (ae3a231f) +* #8734, move slideout to package.json (2c1897b3) +* #8734, move tinycon to package.json (9c157de0) +* #8734, move visibilityjs to package.json (43589a74) +* #8734, move nprogress to package.json (a46cbb62) +* #8734 move r.js to package.json (aa08f882) +* revoke user sessions above threshold (#8731) (b3ed26ac) +* filter:settings.get plugin hook (c7d15dfa) +* topic delete/restore/purge/(un)pin/(un)lock (da25ce4d) +* add new api page to ACP menu (5fc7e7bf) +* management of API tokens via ACP (49652e6f) +* added DELETE /api/v1/categories/:cid route (3072de48) +* added PUT /api/v1/categories/:cid route (e942ad81) +* added POST /api/v1/categories route (dc666fd8) +* added PUT/DELETE /api/v1/users/:uid/ban routes (a5af2dc8) +* added POST and DELETE /api/v1/users/:uid/follow routes (b5bbcbae) +* added PUT /api/v1/users/:uid/password route (7aed174e) +* added DELETE /api/v1/users/:uid and DELETE /api/v1/users (a1ddc210) +* added openapi.yaml file for BEST documentation (91b79f17) +* added checkRequired middleware for API calls (7b6d43bc) +* migrating write-api skeleton into core (ec5c48b1) +* remove post/header selector (1542a5d7) +* category filter on post queue (#8710) (5d9a8681) +* #6594, add top/popular sections to digest email (#8709) (e60357d2) +* dedicated sorting buttons for plugin ordering in ACP (1761e13d) +* #8233, search sub categories (54737d30) +* move service-worker.js into its own file (f4d76f1e) +* analytics:maxCache setting in ACP (14ba1a6d) +* #8695, allow sort for guests (ea0f3262) +* up recommended size to 512x512 + ACP labels for PWA (9c5b6907) +* upload maskable icon for PWA (6478b32d) +* show top 5 trending plugins in new tab in Extend > Plugins (b12e8d63) +* register service worker, #8126 (aa268d5e) +* add theme-color meta tag for browser address bar (5172d731) +* basic service worker, minimum requirement for for a2hs; see #8126 (f69697b3) +* option in ACP to configure notification/email delay for chats (5b427a0c) +* allow autocomplete to pass optional params to user.search (611f3c61) +* upon plugin installation via ACP, check against nbbpm first (5ddf7022) +* up benchpress (8e88ccd6) +* add row highlighting for popular and unread (2cd5f959) +* return to previous page and/or category after marking topic unread (48a8ec4a) +* add missing lang key (bd9b6c99) +* add number of posters to topic (83d4e7ef) +* hightlight matches in quick search (f2f3ba49) +* add loading indicator (d38c8722) +* allow plugins to override tags and tag counts (81595095) +* edit test (ccddbb15) +* add x-posts/x-votes keys (23de5613) +* allow adding nested replies to a target component (ddc0ac37) +* hotkey "/" to open ACP search (43571bac) +* ctr-s for save on privileges page too (763cec31) +* ctrl-s now saves on settingsv1/v2 forms (80931423) +* single href (6669b23d) +* allow passing in container to threadTools (7148be2f) +* expose calculateTopicPostCount and getChildrenTree (43742437) +* allow custom req.query.filter on /unread /recent (de824007) +* fullname search (#8641) (4be693f2) +* sorted set lex test (9389749b) +* add topicIndex to category page (c1c617b3) +* up lavender/slick (34eb1bd2) +* switch to using topic/select component for merge (f6d56466) +* undo for posts move (762e9fe8) +* #8626, new move posts modal (5a40d26b) +* refactor app.js (5002e0f6) +* remove global RELATIVE_PATH (19c44861) +* remove app.template (3cd3b7a1) +* remove global window.templates (630bac2a) +* remove app.isConnected, use socket.connected (fddeb5c0) +* use const/let (6fc31df0) +* allow nbb to work with cloudflare rocket loader :rocket: (4d665955) +* move code (53a5f151) +* #8602, dont send emails to unconfirmed addresses (b6917b9d) +* add partials/footer/js.tpl (94da9fe5) +* add missing name to profile image upload (54b49725) +* add action:user.removeCoverPicture (b0a75922) +* add user data to action:user.removeUploadedPicture (962446a5) +* add userData to action:user.delete (9542ef12) +* force upgrade scripts for test (1b7ba2c8) +* replace relative urls to absolute before sending email notifs (1e5981c0) +* add client side action:flag.create (09de364e) +* additional sorting options for flags (0c203517) +* new filter hook prior to post queue addition (8cc36de2) +* add replies to getPostSummaryByPids (a9dfc9a7) +* add reply count to getPostSummaryByPids (de0f4aad) +* theme work for #8580, closes #8580 (6e805c1b) +* allow undo of topic move (abb5e81d) +* added alert.timeoutfn (cd8e7963) +* sort dependencies alphabetically (a4dbbc37) +* up persona (af9f328c) +* banning a user will resolve their post flags (354e6ccc) +* theme changes for #8571 (5415c01c) +* logic for bulk actions in flags list, #8571 (a3a22793) +* expose global/admin privileges to all routes (4737f937) +* additional tests for #8569 (e047b72c) +* flags list sorting, closes #8569 (346db0d8) +* up theme (bd557af2) +* allow adding multiple users to group (35a538ec) +* add filter:group.getOwnersAndMembers (f1e82b64) +* pass uid into filter:flags.list (9c70b662) +* added security policy for github (45c8de12) +* up deps (b97b51a8) +* one more test (a333cb6c) +* tests for password change (ecda4ad8) +* limit privileges column to superadmins only (0903eb4b) +* use nodebb-scoped bootswatch (12edd18b) +* use assetBaseUrl instead of hardcoding (6e918858) +* load timeago strings client-side (558a2d73) +* +assetBaseUrl, -l10BaseUrl, -requireBaseUrl (9adaccd0) +* #8550, add upgrade script test (b61a4da5) +* #8493, plugin helper for standardised link/button injection (0bbb813e) +* show event/params on error (d6baf5c2) +* introduce overridable l10nBaseUrl config value (def16f9e) +* allow multiple empty lines (58933c4c) +* remove administrator property from public routes (dfabd0a3) +* focus username when modal is shown (4216c277) +* add helmet middlewares (774e5d04) +* more discrete commit-on-save instead of commit-on-change w/ confirm modals (#8541) (a716a552) +* add ability to search groups in group details page (e7a502e0) +* use category selector in category page (fb7bb8d4) +* css fixes (560f3eb1) +* add privileges shortcut to groups list (91411cc4) +* allow passing groupName to user.search (f89ec205) +* #8531, closes #8531 (292d4904) +* allow direct link to flag from post tools, #8531 (c705e08a) +* allow changing requirejs base url (c4829fd8) +* ability to clear cache from acp (bbc7737e) +* reduce amount of data loaded on acp admin page (8d8117ff) +* remove require fallback for plugins (303bffdb) +* remove /assets/stylesheet.css (762b0be4) +* add node 14 (6250a2e2) +* make category tools in acp a dropdown (24535a62) +* series upload (37e56d94) +* resolve flag on delete/purge/ban/delete account (8bd63f61) +* 8502, don't change topics sitemaps on every reply (e045436c) +* allow building custom languages during dev (0a9ea91f) +* add link-plugins.sh (cc86f079) +* remove deprecated utils.walk (609e37a6) +* consolidation of flags to reduce flagspam, #8510 (55b0e902) +* change invalid language codes to default lang (3761f05c) +* add npx, fixes ghfw (a294e1cd) +* remove topics.async.getTopicData usage (ce6b20c4) +* remove deprecated checkGlobalPrivacySettings (0cea7136) +* remove deprecated isAdmin method (a0da2ba7) +* remove file.isFileTypeAllowed (2cdb935f) +* remove deprecated hooks (4eae927d) +* update readme (9869064e) +* up node version to 10 (b39e0140) +* highlight privs row if group is added / navigating from group page (10e4ae62) +* prevent navigation away from groups page if changes are unsaved (53f6139b) +* quick access dropdown on groups page to access privileges page (2c83278f) +* #8524, allow editing category of queued topic (844f2b4e) +* remove js-enabled (ff3c3435) +* remove js-enabled.css (ec057835) +* dont load all subscribers at once (2a5f8ab2) +* short more info (23a9a334) +* #8521, allow editing title before posting from queue (2485a550) +* show language when key isn't found (764969ab) +* allow searching categories in ACP (1e7397b1) +* #8509, don't scroll chat to bottom if user scrolled up (320008cd) +* added quick link to edit privileges on the category edit page (06143ca7) +* hide some filters behind 'more filters' button (9fb9185f) +* collapsible daily flags graph (d52d7bdb) +* **acp:** autocomplete user search for welcome notifications (d40779a4) +* **writeapi:** + * user settings API (c26f2b65) + * commented-out stub code for file upload (cfee431c) + * file deletion route (f870721f) + * admin settings update route (a55b3817) + * token generation/delete routes, ACP updates (2ec838fc) + * post bookmarking (0973feea) + * post voting (9942c248) + * post delete/restore/purge (414169fd) + * post editing (f66c2fb6) + * topic tags (1605e5e4) + * topic follow/ignore (9be56294) + * adding missing files (6096f74a) + * topic posting and replying (4c833d0b) + * added DELETE /groups/:slug/membership/:uid route (40dc1c38) + * added group joining and deletion (952dc211) + * abstracted ajax calls out to new api module (d044c322) + * added POST /api/v1/groups (ba345e53) +* **openapi:** + * schema validation for write api definitions file (87e3f26f) + * refactor into indiv. files to match API & tpl routing (ffbf2d6d) + * refactor into indiv. files to match API & tpl routing (84f5e4cf) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-plugin-composer-default to v6.4.6 (b44c7e96) + * update dependency nodebb-plugin-spam-be-gone to v0.7.3 (f06d4878) + * update dependency nodebb-theme-vanilla to v11.3.0 (0d80190f) + * update dependency textcomplete to ^0.18.0 (df5d66e5) + * update dependency nodebb-theme-persona to v10.2.65 (0c7d5860) + * update dependency jsesc to v3.0.2 (#8802) (c4431294) + * update dependency nodebb-plugin-composer-default to v6.4.5 (7d0d0005) + * update dependency nodebb-theme-persona to v10.2.63 (b3ca7de0) + * update dependency nodebb-theme-persona to v10.2.62 (#8798) (12c590ad) + * update dependency nodebb-plugin-mentions to v2.13.1 (#8790) (8353857b) + * update dependency nodebb-theme-persona to v10.2.61 (#8794) (9de8497d) + * update dependency benchpressjs to v2.0.8 (#8767) (3b88545b) + * update dependency sharp to v0.26.2 (#8765) (42aed15b) + * update dependency nodebb-rewards-essentials to v0.1.4 (#8764) (25c447df) + * update dependency nodebb-theme-persona to v10.2.60 (dcf72354) + * update dependency nodebb-plugin-mentions to v2.13.0 (#8758) (411fa8bc) + * update dependency nodebb-theme-persona to v10.2.59 (#8755) (cd34bfb1) + * update dependency nodebb-theme-persona to v10.2.58 (#8754) (a77ba71e) + * update dependency nodebb-plugin-composer-default to v6.4.4 (#8752) (0399ffaf) + * update dependency benchpressjs to v2.0.7 (#8733) (f8e2324c) + * update dependency nodebb-plugin-composer-default to v6.4.3 (#8750) (666064bf) + * update dependency nodebb-theme-persona to v10.2.57 (#8749) (1c94220b) + * update dependency nodebb-theme-persona to v10.2.56 (414fe19c) + * update dependency nodebb-theme-slick to v1.2.39 (1bc99411) + * bump vanilla/persona (944a7e0e) + * update dependency nodebb-theme-lavender to v5.0.14 (#8739) (43df4b8e) + * update dependency socket.io-client to v2.3.1 (#8700) (ba62ebb6) + * update dependency nodebb-theme-slick to v1.2.37 (#8741) (45aceb26) + * update dependency nodebb-theme-vanilla to v11.2.21 (#8740) (0c7af502) + * update dependency nodebb-theme-persona to v10.2.54 (#8736) (4207dde4) + * update dependency nodebb-theme-persona to v10.2.52 (#8735) (70c085ba) + * update dependency nodebb-theme-persona to v10.2.51 (#8729) (bae2eada) + * update dependency nodebb-theme-persona to v10.2.50 (#8727) (804729fc) + * update dependency nodebb-theme-persona to v10.2.49 (#8722) (b01bc2ae) + * update dependency nodebb-theme-persona to v10.2.48 (#8721) (01702613) + * update dependency nodebb-theme-persona to v10.2.47 (#8720) (04b1daac) + * update dependency nodebb-theme-persona to v10.2.46 (#8719) (ba906d7d) + * update dependency nodebb-plugin-composer-default to v6.4.2 (#8718) (229421ed) + * update dependency nodebb-plugin-composer-default to v6.4.0 (#8716) (b3a24f74) + * update dependency nodebb-plugin-mentions to v2.11.0 (#8714) (c95a3898) + * update dependency nodebb-theme-slick to v1.2.36 (#8713) (fc2795d3) + * update dependency nodebb-theme-vanilla to v11.2.20 (#8715) (2b0deeb7) + * update dependency nodebb-theme-persona to v10.2.45 (#8711) (f14b4945) + * update dependency nodebb-theme-persona to v10.2.43 (#8701) (1d0eaafb) + * update dependency nodebb-theme-persona to v10.2.42 (2a1da61c) + * update dependency nodebb-theme-vanilla to v11.2.18 (#8697) (9bdef707) + * update dependency nodebb-theme-slick to v1.2.34 (#8696) (3e2816ae) + * bump persona (f17d42ed) + * update dependency sanitize-html to v2 (1e5621c0) + * update dependency nodebb-theme-persona to v10.2.39 (#8684) (d0010c40) + * update dependency nodebb-theme-persona to v10.2.38 (#8683) (bb04f149) + * update dependency json2csv to v5.0.3 (30aa7e83) + * update dependency nodebb-theme-persona to v10.2.36 (#8680) (07172b6f) + * update dependency nodebb-theme-persona to v10.2.35 (#8679) (68a5e7e3) + * update dependency json2csv to v5.0.2 (5a2adb42) + * update dependency nodebb-theme-persona to v10.2.33 (#8675) (abe83773) + * update dependency nodebb-theme-persona to v10.2.31 (#8673) (80dbf97a) + * update dependency nodebb-plugin-composer-default to v6.3.57 (#8672) (7ecac97a) + * update dependency nodebb-theme-persona to v10.2.30 (#8671) (c8a75631) + * update dependency nodebb-theme-persona to v10.2.24 (#8663) (72cd5f13) + * update dependency postcss to v8.0.6 (#8657) (55921ccf) + * update dependency validator to v13.1.17 (#8659) (01232090) + * update dependency sharp to v0.26.1 (#8660) (b175d671) + * update dependency nodebb-widget-essentials to v4.1.2 (#8658) (dc0a4a49) + * postcss and autoprefixer version incompatibility (70d3ad02) + * update dependency nodebb-plugin-mentions to v2.10.0 (5ea172f9) + * update dependency autoprefixer to v10 (60679481) + * bump theme versions (685633d0) + * update dependency nodebb-plugin-composer-default to v6.3.56 (#8648) (8089387b) + * update dependency nodebb-theme-persona to v10.2.22 (c5ca2609) + * update dependency nodebb-theme-vanilla to v11.2.16 (8acdf3f6) + * update dependency nodebb-theme-persona to v10.2.21 (ed0469b5) + * update dependency nodebb-theme-vanilla to v11.2.15 (134ebba6) + * update dependency nodebb-theme-vanilla to v11.2.14 (31635e3e) + * update dependency nodebb-theme-persona to v10.2.20 (2a13e583) + * update dependency mongodb to v3.6.2 (#8634) (4969c869) + * update dependency nodebb-theme-vanilla to v11.2.13 (#8633) (8137cdc2) + * update dependency nodebb-theme-persona to v10.2.19 (#8632) (37e37c86) + * update dependency nodebb-theme-vanilla to v11.2.12 (#8628) (7ab87072) + * update dependency nodebb-theme-persona to v10.2.18 (#8627) (2740655d) + * update dependency nodebb-theme-vanilla to v11.2.11 (#8624) (fd6259c8) + * update dependency nodebb-theme-persona to v10.2.17 (#8623) (a1d310bd) + * update dependency nodebb-theme-slick to v1.2.30 (#8620) (251ea79b) + * update dependency nodebb-theme-persona to v10.2.16 (#8618) (0d85dc48) + * update dependency nodebb-theme-vanilla to v11.2.10 (#8619) (b52301b1) + * update dependency nodebb-theme-vanilla to v11.2.9 (#8616) (69373fde) + * update dependency nodebb-theme-persona to v10.2.15 (#8614) (3c0540c1) + * update dependency socket.io-redis to v5.4.0 (#8600) (5ba23f24) + * update dependency nodebb-theme-vanilla to v11.2.8 (#8599) (97e3543e) + * update dependency nodebb-theme-persona to v10.2.12 (#8598) (1bb0896e) + * update dependency nodebb-plugin-composer-default to v6.3.55 (#8606) (dfeb65bb) + * update dependency nodebb-plugin-markdown to v8.12.1 (27426c06) + * update dependency nodebb-theme-vanilla to v11.2.5 (64f4179a) + * update dependency nodebb-theme-persona to v10.2.10 (777419b2) + * update dependency sharp to v0.26.0 (#8578) (7ca967ee) + * update dependency ipaddr.js to v2 (49aeda01) + * update dependency nodebb-theme-lavender to v5.1.0 (87674d68) + * update dependency nodebb-theme-persona to v10.2.5 (8032c8bd) + * update dependency helmet to v4 (#8543) (ad68a338) + * update dependency mongodb to v3.6.0 (#8535) (4160e828) + * update dependency nodebb-theme-persona to v10.2.4 (#8544) (b30ecffb) + * update dependency nodebb-theme-persona to v10.2.1 (#8529) (7a59c2fc) + * update dependency nodebb-plugin-composer-default to v6.3.53 (d24a4bd3) + * bump themes (b714ed22) + * update dependency commander to v6 (#8518) (75fb2a47) + * update dependency archiver to v5 (#8523) (f1cc4e29) + * update dependency lru-cache to v6 (#8490) (2941b9f9) + * update dependency connect-redis to v5 (#8480) (9c17a677) + * update dependency nodebb-plugin-composer-default to v6.3.52 (#8522) (ac257a65) + * update dependency nodebb-theme-persona to v10.1.68 (#8520) (f06b1cec) + * update dependency nodebb-plugin-mentions to v2.9.3 (#8516) (b5df5766) + * update dependency nodebb-plugin-dbsearch to v4.1.2 (649c64e4) + * update dependency nodebb-plugin-mentions to v2.9.2 (91bdc12a) + * update dependency nodebb-plugin-composer-default to v6.3.51 (0e13fd0f) +* check is banned in buildHeader (4b63f993) +* undefined call (518d4fa1) +* missing await (08ff4041) +* client side crash when creating groups (5a2b14b7) +* disallow registration attempts with password length > 4096 (c0f699e6) +* missing await (4818ec37) +* broken test (87bff6cd) +* updated commitlint config to allow longer subjects, because nobody anywhere uses an email client that limits subject lines to 72 characters (a53d49a2) +* restore old behaviour of empty json w/ 401 code in admin middleware (dda5d426) +* deprecate middleware.isAdmin (15e0731d) +* post editing not taking plugin hook results into account (4439864c) +* #8805 define our own name for write API v3 (57ed6be7) +* removed superfluous assignment (a08fb8e5) +* createNewPosts to build tpl with ajaxify.data (0b6ef61e) +* handle ACP category enable/disable states after call success (bff53de0) +* remove setCategorySort and setTopicSort (a6a52430) +* sort setting not using correct field name (9082062e) +* update readme to include psql (2d29e603) +* breaking test from 0db0231cff097a6e983683e61284a72d42bd594d (cc1c7220) +* indentation (04185d94) +* add back derpy 20 second sleep (8e7914ff) +* missing method in test error output (3ebb3a34) +* topic object in post editing data return (3c98cd3d) +* bug where token generation route would fail on null case (618e0983) +* typo (2e9f27d8) +* return early for guests/spiders (203db47b) +* #8789,cache meta.settings (156e1396) +* spec (7a318352) +* tests (63e07c94) +* #8781 (db63f5e3) +* reset button loading html (1a4c0dec) +* #8779, fix move all (e6440c0d) +* allow admins adding users to global moderators (1f43e98f) +* incorrect logic for post history editable bool (a691be59) +* [breaking] send configured config URL as origin if not custom (205a1030) +* #8776 some users unable to restore old versions via history (7a8f7049) +* #8595, dont save escaped data when renaming groups (b26e9b59) +* keep sortBy while searching (3ee4936d) +* autocomplete.user on search page (85cfe49d) +* spec (cf474ab2) +* sortby (7bddec93) +* #8774 (b3619d3d) +* #8732 (c107649b) +* #8630, sort extra deps (e362c342) +* missing doTopicAction, fix wrong api params (e78c498e) +* test (8fd3c044) +* appropriate 404 handling for write API calls (b6cce75d) +* redis hget (b2ff1594) +* reimplementing isPrivilegedOrSelfAndPasswordMatch (e98285db) +* socket user bans (3f347baa) +* broken tests from api change (222b4c95) +* tests (7d86be2b) +* handler for group.join (51a60d3d) +* add missing file (d07f0081) +* #8768 (4418ff07) +* api bug where user profile editing continued even if not allowed (cc6e995e) +* module build (bae0f343) +* tests (b295d15e) +* csrf token only on non-GET routes (20bb9c7e) +* avatar selector (4b9444f1) +* test (e6ea71c9) +* timestamp (8c6a5591) +* #8763 (331d236f) +* lastonline again (a481024d) +* caret (71d82ec8) +* lastonline values (97628e2f) +* upgrade script (1289c105) +* cant join system groups (59bbede8) +* tests (a411df13) +* update server param to /api/v3 (0e0f1506) +* typo (c68653d0) +* testing openapi write tests (7aa4d104) +* enable tests (7b2301ff) +* test fix for write API (54e6003a) +* password reset to invalidate all existing reset tokens for that uid (30b3fedc) +* show more lines (ba2e1c4c) +* #8756, pass missing req to mock (30d6a2b8) +* #8757, allow all slashes in category route (1ee93848) +* timeago test for dev/prod (7db224f6) +* timeago test (cecdd291) +* undefined api require, @julianlam (931d44b5) +* watch tooltip (e2d407b7) +* module name (aedd28e0) +* pin sortablejs (8f436eb8) +* admin jquerui requires (75c96686) +* timeago locale switch (8c019a6a) +* category RSS feed was displaying deleted topics (9a5b8a79) +* #8734 make nprogress module (45e8a4d5) +* regression caused by 754595172549ba39b406bd36fc3387d95782d84f (8af30a51) +* dont allow adding duplicates to privileges page (12c8b1c2) +* #8728, dont add admins to table (f259063f) +* remove debug log (675a62da) +* tests (adcadbb2) +* bad merge w/ category ACP page (cb9369f1) +* bad logic in group assertion middleware (8e89f34d) +* errors thrown if no password sent in to profile edit route (7757f965) +* bug where middlewares seemingly ran in parallel (549ca110) +* follow route to match spec (db5c5b2c) +* missing one property in openapi doc (af2e424f) +* error handling with POST /api/v1/users (d8879d21) +* use POST call for user creation in ACP (58043e07) +* user creation POST route returns user data, updated openapi spec (bba2a463) +* selector, so it doesnt effect suggested topics (7138d433) +* use proper api url (264818e5) +* tests, get latest release tag recursively (8eb62e22) +* use app.parseAndTranslate instead of benchpress.parse (fc603a53) +* sorted list delete button (20e0cc5c) +* unable to register async method as response hook listener (dde5b6b8) +* dont allow sorting pinned topics on recent (b955fd36) +* use console.info instead of console.log for sw registration (3c7f79cb) +* spec (7cd83b9a) +* spec (6924eb6c) +* test (cb2f6f7c) +* dont let mods load postqueue for a cid they are not a mod of (7bf6d3b8) +* show disabled categories in admin&mods (2ea9768e) +* prevent mutiple highlighted rows on category/topic pages (60afb110) +* spec (fbd85680) +* do not show TOC if fewer than 2 headings (f1de084d) +* params cached in autocomplete module (caa8c00f) +* #8686, deprecate `plugin.json/library` (#8705) (017af63f) +* updating minimum node version in readme (d3951ebe) +* update "install plugins" link to go to the download tab (5441651e) +* overflow on submitPluginUsage field (c56236f0) +* #8699 tags route is case sensitive, though tags are not (38f88fc5) +* early button/anchor clicks do nothing (8437130e) +* early button/anchor clicks do nothing (966d3f76) +* error on `reset -p` if plugin is not active (7f58e3ab) +* manifest test + remove duplicate test (bf3c1c08) +* typo in getCompatiblePlugins (07af6213) +* typo (54705cc4) +* manifest - use absolute URL for start_url (31528a52) +* prefer webmanifest extension over json (46800b66) +* update notification delay ACP help text (4c1e717b) +* #8681 change owner modal's search should check if user is banned (b6f2f0e5) +* add img-responsive to post queue post items (f8032cd0) +* language key in reset password (2ad33058) +* issue where the last flag filter could not be removed (0fffe289) +* multiword match highlight (f8ef380b) +* tooltip placement on navbar search (82ab6cd3) +* rogue tooltip on navbar profile icon (683c01b0) +* #8580, @julianlam (445a840b) +* topic count on category page if filter is author (bbf6889e) +* match api when modifying ajaxify.data.deleted (int, not bool) (703ac1b6) +* lock/unlock toggle issue (9a7c3c68) +* #8665, trim email before checking validity (ac43cd8b) +* editing posts saving uncessary data (e72fe259) +* api spec (654d5830) +* #8640, add bookmarks to intFields (4f14dc7a) +* change user tooltip container to content (02a48e1c) +* auth tests (fe2dc310) +* #8656, rename /api/me to /api/self (af43f0e4) +* add user tooltips to body (047c4148) +* tests (22cd2654) +* use correct topic count for category (220297d5) +* clear old value on focus (42298fe6) +* skip elements if they dont have proper index (c9c9dd2f) +* call to reskin (99f24c59) +* move necro-post to be in-line with posts (44309ee6) +* tests, because redis is TOO FAST (4fca7938) +* remove old test (5e8c3761) +* client side crash if there are no topics (a8e18fdb) +* #8418 (38d3982b) +* typo (08912361) +* tests (65d049c6) +* check if unread_tids are followed (2d5bd153) +* openapi (e9094094) +* dont use hardcoded fontawesome icons for lock pin (632793b0) +* #8629 and change undo timeout to 10 seconds (65d94a3b) +* alert_id selector (2bebdf01) +* use text-right (a78e1df9) +* remove lang keys, remove sounds menu (a34b685c) +* lavender version (8a752364) +* tests (e3a0b4b1) +* wider widget area (4bace773) +* bypass nbbpm for now (33c1bd5f) +* #8432, add CSP frame-ancestors (46ab2711) +* allow setting maximumAboutMeLength to 0 (4588e521) +* additional commit for CF rocket loader (a38784f5) +* stack trace (4031a8ca) +* #8604 (a2638976) +* don't toggle elements if there is no inputEl (57f67e2b) +* don't crash on outdated redis instances (896fe9d6) +* update post-queue hook names (0e58d2b8) +* accept/reject in post-queue instead of accept/delete (694f4b2b) +* new openapi def for replies addition (b5871275) +* #8582 (9f9164a9) +* debug logging :dog: (fb3b4a02) +* ip-blacklist not working after #8580 (56101ae5) +* ip-blacklist not working after #8580 (c681b4b2) +* post-queue not working after #8580 (dc5bd760) +* #8411 missing language sources for ip-blacklist/post-queue (a93e1955) +* removed invalid options for the sendmail transport (#8576) (2b785628) +* missing localisation for topic move error (bacee6b4) +* select all checkbox does not re-enable bulk actions button (5d60dce3) +* dupes in unread followed topics (55533b11) +* wrong data passed to getCategories (39f4bbaa) +* openapi spec (4f7cfd53) +* api spec (c4ad14cb) +* username, email history disappering if content was deleted (91d9333a) +* up composer (9fd37753) +* package-install clobbering scoped nodebb plugins (840cb510) +* empty "manage" menu showing in ACP (4b0cb26b) +* hide nodebb version in ACP for non-superadmins (cc268605) +* change how admin middlewares are exported (f00595b3) +* one less return, to appease codeclimate (ae68a254) +* inability to access /admin if not superadmin (03bd76de) +* #8560, fix old upgrade script (29e3ab24) +* use promises in privilege save (9cbe9389) +* overzealous click handler on flag list row (7a5daff4) +* +comment, -debug log (9608b44f) +* multiple alerts in the same millisecond overwrite each other (589216e7) +* reintroduce 20s delay for api tests (56393795) +* client-side error in flags/detail (b2271eb6) +* #8570 (7daba7dd) +* sorting the flags list by newest is not considered a filter (3efe2362) +* flag list tests due to breaking change in API (360aa00d) +* no-widgets messaging in ACP + copy (334be113) +* #8568 perPage not acting like a filter (cabe62a0) +* #8562 (31c2b7d9) +* progress bar clobbering upgrade script name (2adae879) +* tweak upgrade script progress bar to only update 100 times in total (ac75c9a0) +* navigation title unescape upgrade script tweak (80a2a700) +* new language key for error handling (7456a0e3) +* upgrade scripts not run properly if schemaDate is present (de8eebbd) +* #8556, catch errors from admin check (bfaf648e) +* upgrade script for 1.15.0 (32682738) +* improper targetUid check during password change (16cee1b0) +* #8547, remove old deps during upgrade (#8557) (1d170e0c) +* #8558, only navigate if user is in same topic (79e847d4) +* upgrade script (231d34d0) +* don't allow duplicate upgrade script names (8887f0ed) +* tests (d6297b28) +* send hard 404 instead of soft 404 for missing modules (9f3b7811) +* #8549 send 308 Permanent Redirect on topic/category shortlinks (68f8d6e3) +* remove default helmet (c39c5113) +* map instead of forEach in privileges save (a7071bb8) +* remove duplicate configuration for helmet-hsts (0f10e083) +* don't process invalid topics, fallback to 0 score (335169f2) +* tests (6924a222) +* #8539, enforce content checks for post queue (bb224184) +* bug; additional reports could be added by the same user repeatedly (0f2b6f1f) +* #8538, go to first unread instead of last read (519e6659) +* #8533, validate and retry password during setup (0d698a07) +* #8534, dont show modal on search (9e80a9ef) +* language string (9f346c53) +* api test (8415d2f0) +* tests (9df871be) +* tests (69fb1527) +* reverse uid checks (7331faed) +* acp view category button if category is a link (2dfe9d49) +* remove old test (52718ce0) +* #8515, fix login redirect on subfolder (5e5815f0) +* tests (f48d1556) +* #4695, remove new notif alert (fd4c3cda) +* too small sortable handle for ACP>Manage>Categories (8473e165) +* less fixes for category dropdown (db07dd85) +* move check inside lock (57135761) +* #7351, display less errors with location and error message (d1cb405d) +* use shorter git url, #8527 (345fd72b) +* cursor for category select (4c90fac4) +* api docs for admin/manage/groups/x (8769e28b) +* edge case in test (b9cff577) +* api tests (42af4b57) +* #8500, allow regular users select topics on unread (7260646d) +* don't crash if content is undefined (4658121a) +* crash if csrfToken does not exist (a3c8d456) +* try again (6f889c9c) +* more tests (a46adb3f) +* tests (65395ae5) +* tests (72c60d19) +* tests (67ca5e32) +* #8508, dont allow moving topics if not moderator of target category (36531511) +* composer textarea placeholder l10n /cc @pitaj (1e14af45) +* **openapi:** + * final fixes to schemas (14e5c24e) + * users.yaml (16873800) + * moved write-api to public/openapi (49994f3a) + * test for new trending plugins in ACP (3fbddbe2) + * tests (d935f036) + * new api definition (8ff09630) +* **writeapi:** + * more tweaks to schema files (b2e9e3e0) + * fix components, + tag object schema (9f9e3c15) + * missing files for tests (a4f3270f) + * normalizing data (1392d064) + * tests (b092f65d) + * added examples to all parameter objects in schema (93f65f89) + * tests (ebcb664b) + * tests (229eb2c2) + * tests (b8703ba9) + * tests (41f55b7a) + * calls to profile editing routes 200 even if user DNE (8e7baac6) + * client-side group join API call (68ecf41e) + * authenticate middleware logic to work better with await (fd67355b) +* **refactor:** + * patching helpers.tryRoute for API routes, some re-org (d15d9e44) + * merging write-api auth middlewares with core middlewares (f6433ef2) +* **bug:** #8611, custom route logic corrected (#8612) (64a457a4) +* **docs:** updating changelog (6e34b9ef) + +##### Performance Improvements + +* reorder async calls (93bdfe2f) + +##### Refactors + +* simpler check in user.blocks.filter (a02ae6f5) +* remove usage of middlewares (266d7587) +* remove /users/{uid}/settings/{setting} route (aa8faf58) +* api test suite to accept methods other than GET (843aff58) +* remove unnecessary wrapper (a512d994) +* remove console.log, extra require (75024c35) +* show more lines of stack trace (ea31f505) +* posts api (d9a16855) +* post restore/delete/purge (272e73da) +* merge post.edit (9738e202) +* deprecate socket.emit('users.search') use api route (2279e372) +* api categories (083c74e0) +* remove sockets.reqFromSocket (bc880ee0) +* topic follow/ignore to use api lib (9ee3cb9b) +* topic tools' actions to use api lib (68d6818b) +* topic creation to use api lib (40598b36) +* setupApiRoute signature (bf480ee5) +* remove unused middleware (688d7a2c) +* user bans to use api lib (2d252f2f) +* move groups.leave, fix some tests (e367c540) +* change password/user follow to use api lib (960e925e) +* user deletion to use api lib (430e7f58) +* socket profile update to use api lib (77481947) +* socket profile update to use api lib (31ae8a83) +* user create and profile update to use api lib (23086dae) +* use single function for api code (5e2caf19) +* async/await src/user/approval (43afe7ff) +* async/await install/web.js (3881ac30) +* async/await (1fd2eba6) +* use app.render (d89477ca) +* api module (3673accd) +* api module returns promise, error-first cb if cb passed in (a784d10f) +* remove unused search call (dd7424e5) +* switch to using slugify module (dc29f4dc) +* require style (c954db84) +* middleware.assert.* (8ecef7b8) +* rewrite modules/api.js in AMD style (a905d7f0) +* remove posts.tools.purge (ed092bf6) +* shared constants (#8707) (1aa336d8) +* post edit (16039b11) +* expose new method for appending moderation note (29b357bc) +* socket posts delete/restore/purge so tid is not necessary (#8607) (f743f920) +* ip-blacklist and post-queue language files (4dc6e64a) +* moved ip-blacklist and post-queue styles/tpls to themes (b6b94a56) +* changed way middleware was exported (cf2f1e95) +* remove util.promisify calls (01899459) +* change pwd change logic (846b7d24) +* rewrite src/upgrade.js with async/await (33c5988c) +* change incrementViewCount and markAsRead to async/await (2c35d0ba) +* for codeclimate (2ee62483) +* rewrite src/install with async/await (776e34a8) +* removed rather redundant flag reporters zset (6c00ec84) +* flags object in post tools (fcdbdf63) +* pass only needed data (f1974fb9) +* startup (c54b6b9c) +* get rid of bind calls (af91c26a) +* setting codeclimate to A (bc26883a) +* make msg pretty (8ddc8dd1) +* flag sanity checks, +feat: flag limits (e3e55f25) +* **writeapi:** + * update route prefix to api/v3, default error option (3ea1aa47) + * post.tools.purge no longer a method (5e2a3ea6) + +##### Code Style Changes + +* **openapi:** + * break write API routes into individual files (dbb4cfe9) + * move schemas and responses out of write.yaml (ffac3c79) +* updates to openapi files (2928b9b8) +* allowing some es6 features client-side (e1c40b27) +* linting (cdb79488) + +##### Tests + +* **openapi:** missing commonprops addition (0dc55bbc) + +#### 1.14.3 (2020-07-08) + +##### Bug Fixes + +* improper targetUid check during password change (c2477d9d) + +#### 1.14.2 (2020-07-15) + +##### Chores + +* incrementing version number - v1.14.2 (1e4d683f) +* update changelog for v1.14.2 (488e69fd) +* up theme (63fb2ad7) +* incrementing version number - v1.14.2-beta.1 (9d1465d0) +* up theme (15aabfd3) +* up theme (599c5015) +* incrementing version number - v1.14.2-beta.0 (fca4ee31) +* incrementing version number - v1.14.1 (31203b16) +* update changelog for v1.14.1 (d4c16086) +* **deps:** update commitlint monorepo to v9.1.1 (0ca7e28a) + +##### New Features + +* logic for flag note editing, #8499 (14417209) +* logic for flag note deletion, #8499 (f85a45c7) +* #8460, export groups members as csv (00d39fb3) +* pass connection options to socket.io-adapter-mongo (393f19b4) +* #8023, allow wildcard search for uid/email (3dcf5387) +* up composer (6235b31c) +* #8427, daily downvote limits (c513b88d) +* warn user if whitelisted tags are less than minTags (4adbf87c) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-plugin-mentions to v2.9.1 (c54287fe) + * update dependency nodebb-plugin-mentions to v2.9.0 (7730e7da) + * update dependency nodebb-theme-persona to v10.1.62 (#8485) (4e9743ab) + * update dependency nodebb-plugin-composer-default to v6.3.49 (#8479) (ab244ca6) +* #8499 (65240a17) +* #8500 (5e984d10) +* invalid session error modal during logout (2286ee2a) +* #8488 (b3a88331) +* category search in selector (46a66863) +* groups.updateCover (73ddf1cb) +* **docs:** bad changelog (60bf488f) + +##### Other Changes + +* update changelog for v1.14.2" (e085c846) +* flag.showModal on flag modal appearance (3379d65f) +* NodeBB/NodeBB (2fba0a14) +* update changelog for v1.14.1" (26c74409) + +#### 1.14.1 (2020-07-08) + +##### Chores + +* incrementing version number - v1.14.1 (31203b16) +* update changelog for v1.14.1 (d4c16086) +* incrementing version number - v1.14.1-beta.3 (e8ecef6b) +* incrementing version number - v1.14.1-beta.2 (b8d9b6b1) +* incrementing version number - v1.14.1-beta.1 (be85123a) +* incrementing version number - v1.14.1-beta.0 (c279875a) +* incrementing version number - v1.14.0 (bb73d6a4) +* update changelog for v1.14.0 (cffae0f1) + +##### New Features + +* add tools to recent/unread (#8477) (658dd03b) +* fire new hooks on chat message editing (4f51838d) +* add back redis tests (bdc4d9e7) +* remove redis test (8461a179) +* use covered query (057b783d) +* add js-enabled.css to list of preloaded css files (da29b947) +* zscan (#8457) (723fe8e8) +* fix blocksCount not being returned on user profile (bd228d5e) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-theme-persona to v10.1.60 (#8478) (14eafcb6) + * bump nodebb-plugin-composer-default to 6.3.48 (943a344a) + * update dependency nodebb-plugin-dbsearch to v4.1.1 (#8476) (9f06f12c) + * update dependency nodebb-plugin-composer-default to v6.3.47 (#8473) (857900f1) + * update dependency nodebb-plugin-dbsearch to v4.1.0 (#8471) (eb51cfd4) + * update dependency nodebb-theme-persona to v10.1.59 (#8468) (ee38e05d) + * update dependency nodebb-widget-essentials to v4.1.1 (#8466) (519e035d) + * update dependency @nodebb/socket.io-adapter-mongo to v3.0.1 (#8464) (412ca4ae) +* #8474 (c2ca02df) +* show stack properly (7b04d897) +* editing chat messages does not go through content sanity checks (9a6b87d2) +* don't show blocked users under nested replies (d6c619cf) +* tests (87dd6c83) +* handle scan/zscan returning duplicate elements on redis (746222d6) +* #8467, fix url to merged topic in subfolder installs (9eb748b9) +* openapi (5f1865c0) +* openapi (65c0adc7) +* dont allow searching by email/ip if not privileged (ac6b571e) +* missing backgroundImage #8386 (fef04fcf) +* dont allow searching by ip/banned/flagged for regular users (02ac44cc) +* admin privileges client-side regression (f3441fce) +* only add blocksCount for self and admins (59a2ace6) +* tests (fd20e5c6) +* better changelog (f992af05) +* **tests:** + * another shot in the dark (8853cd1a) + * shot in the dark (9458d90b) +* **openapi:** tests (c468942f) + +##### Other Changes + +* update changelog for v1.14.1" (26c74409) +* //github.com/NodeBB/NodeBB (0d9461b1) +* //github.com/NodeBB/NodeBB (ace312e0) +* post.changeOwner (b60e1cbf) + +##### Reverts + +* bad changelog (a761e31f) + +#### 1.14.0 (2020-07-02) + +##### Chores + +* incrementing version number - v1.14.0 (bb73d6a4) +* update changelog for v1.14.0 (cffae0f1) +* bump persona (19f9af94) +* latest translations and fallbacks (22879633) +* incrementing version number - v1.13.3 (ee583e80) +* bump persona (d2bd746c) +* incrementing version number - v1.13.2 (beafd613) +* **deps:** + * update dependency smtp-server to v3.7.0 (e80100b5) + * update dependency eslint to v7.3.1 (#8417) (33492744) + * update commitlint monorepo to v9 (569b7664) + * update dependency lint-staged to v10.2.11 (157e7444) + * update dependency lint-staged to v10.2.10 (102a6004) + * update dependency mocha to v8 (#8393) (f4bace03) + * update dependency eslint to v7.2.0 (cd1375cb) + * update dependency eslint-plugin-import to v2.21.1 (4b577a52) + * update dependency lint-staged to v10.2.9 (#8369) (124125f7) + * update dependency lint-staged to v10.2.8 (331b1a85) + * update dependency lint-staged to v10.2.7 (d1df0826) + * update dependency mocha to v7.2.0 (ecaa9b76) + * update dependency eslint to v7.1.0 (e62d892a) + * update dependency lint-staged to v10.2.6 (78d562b3) + * update dependency lint-staged to v10.2.4 (a26011e7) + * update dependency eslint to v7 (28df9aba) + * update dependency coveralls to v3.1.0 (5ed4a108) + * pin dependency @apidevtools/swagger-parser to 9.0.1 (51eccef5) + * update dependency husky to v4.2.5 (30a25983) + * update dependency husky to v4.2.4 (0a650118) + * update dependency lint-staged to v10.1.3 (a9e68639) + * update dependency nyc to v15.0.1 (#8231) (a3789e28) + * update dependency lint-staged to v10.1.2 (#8235) (e1919c90) + * update dependency lint-staged to v10.1.1 (944a6f58) + * update dependency lint-staged to v10.1.0 (30bd233b) + * update dependency eslint-config-airbnb-base to v14.1.0 (811c3aee) + * update dependency jsdom to v16.2.2 (c5a7242d) + * update dependency eslint-plugin-import to v2.20.2 (b92c1600) + * update dependency lint-staged to v10.0.10 (0ad4b556) + * update dependency coveralls to v3.0.11 (14458087) + * update dependency smtp-server to v3.6.0 (22681945) + * update dependency mocha to v7.1.1 (#8215) (c5356541) + * update dependency grunt to v1.1.0 (#8214) (b0864e7c) + * update dependency husky to v4.2.3 (#8162) (776fe9d2) + * update dependency lint-staged to v10.0.8 (#8180) (13d8f6f1) + * update dependency eslint-plugin-import to v2.20.1 (#8081) (4cdb3131) + * update dependency jsdom to v16.2.1 (#8165) (fbd95a50) + * update dependency husky to v4.2.2 (#8160) (f4ed35c9) + * update dependency jsdom to v16 (#8114) (1037de02) + +##### Documentation Changes + +* updated changelog (146388aa) + +##### New Features + +* polish for user blocks UX (6cb31791) +* #8450, next/prev link tags on /unread /recent (eb9704f8) +* allow flagging of user acounts from post tools menu (6931f29d) +* closes #8440, allow configuring max topic count (e09ab3dc) +* add missing translation key (bffb830d) +* #3783, min/max tags per category (c718b729) +* use tags partial instead of post_bar (0482fb29) +* hide elements if search element is a direct child of dropdown (4f6b6c56) +* increase wait (6aecc177) +* move export functions into child processes (8383992d) +* display stack trace on winston.error (e80379dc) +* show more relevant snippets (f70d1648) +* #8412 breadcrumbs for ip-blacklist/post queue/flags (35a06a84) +* show editor in post diffs if available (f909ed25) +* #8408 flags' quick assignment (d5af9769) +* bump themes, closes #8406 (cb5ba76b) +* allow post diffs to be restored, #8406 (58b3d608) +* add missing translation (3a80a165) +* up composer (96cb94dc) +* more search changes (6349fa03) +* more merge/search fixes (4b38533b) +* merge changes (bb3aa540) +* more search & merge fixes (5fd05dc9) +* merge improvements wip (c4bdeae0) +* #8387 expose global and admin privs to flags detail page (4acb3fb2) +* redirect /me to user profile (3be4d5f7) +* #8384 options to delete account, content, or both (4d60eac6) +* account content deletion, closes #8381 (67aca822) +* add missing language files for #8347 (656b391f) +* privileges for Admin Control Panel (#8355) (a82e9bd7) +* add buildHeaderAsync (#8367) (842b8abb) +* #8360 flag quick actions for delete/restore/purge (8ea16348) +* #8349, remove user posts from queue if user is deleted (5a2b5154) +* up plugins (5b009e07) +* add results into results container (9ffcb6f7) +* quick search changes (f12d448e) +* move quick search into search module (bb1a56f0) +* fix session mismatch errors by clearing cookie on logout (#8338) (5781a2dc) +* add getSortedSetMembers (0009f54e) +* add privilege give/rescind hooks (#8336) (ec5582b5) +* filter followed tids by category (f3afba61) +* use getSortedSetsMembers to load followed tids (1b9e8928) +* add batch size to cursors (a015af4a) +* up cache size (9600ede5) +* tweak intersection code, add tests (4ee3543e) +* change to contains to match scoped modules (6108064e) +* improve grunt restart/rebuild speed (cb662e15) +* don't overwrite req.query.lang if it exists (4263efa0) +* convert queries so they used indices directly (12c6bc2e) +* cookie SameSite property (ae2db423) +* if only one value is passed used faster function (2587112f) +* closes #8316, add more data to export profile (f0323b6c) +* throw error if uid is missing for update (1d3fa3bc) +* ignore mongodb playground file (a219285e) +* parse quick search tpl even if no results (765b8156) +* match hook property to topic property (0e58fa33) +* add methods for adding/removing tags from topics (bfad4572) +* write-api update, WIP (9fd5c439) +* closes #8308, edit post notif for watched topic posts (a73c755b) +* add null tests for sorted sets (edf9fe3b) +* Add hooks for user blocks (#8296) (c4545381) +* allow activating additional plugins for testing via config.json (a969c5ce) +* move plugin tests to separate file (3a23ddab) +* remove node14 for now (a72e4429) +* reduce infinite scroll area (3fcbd691) +* manifest.json improvements from #8126 (#8264) (6e5ebb61) +* show error if json is invalid (15345627) +* moved component specs into separate files (cd506557) +* added UserObject, UserObjectFull, Breadcrumb, Pagination component (64d79fe5) +* added some summary and descriptions (ae3e90d6) +* add some descriptions (442c018e) +* common schema (eade13f9) +* openapi component (1af5507a) +* add page query param to docs (9987813f) +* tag route doc (bbddaadf) +* local redoc view on development mode only (1136a369) +* added auto-generated, slimmed-down openapi 3.0 file for read api (7b155dab) +* add parent cids to body class (23571224) +* add 2 hooks for modifying privileges (d080c7b0) +* add user ip to admin/dev/info (5e91a67e) +* change option name (cba5b23e) +* add no-build to ./nodebb setup (476f6717) +* add awaitable socket.emit (4083a6e3) +* settings sorted list (#8170) (3c9689a5) +* guard against accidental ommision (79737c53) +* **docs:** updated changelog (87c1687d) +* **openapi:** + * merging openapi-test branch into master (8387178b) + * move all commonprops out for commonprops component (65c78de6) + * added template to commonprops (2425f453) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-plugin-composer-default to v6.3.46 (5a713d85) + * update dependency nodebb-plugin-mentions to v2.8.3 (#8449) (bbd85049) + * update dependency nodebb-plugin-mentions to v2.8.0 (d40720f9) + * update dependency nodebb-plugin-composer-default to v6.3.44 (#8436) (c1991abe) + * update dependency winston to v3.3.3 (#8431) (6a8f54fd) + * update dependency nodebb-theme-persona to v10.1.55 (#8434) (a860a793) + * update dependency nodebb-theme-vanilla to v11.1.30 (#8435) (272b4992) + * update dependency nodebb-theme-vanilla to v11.1.29 (#8429) (b5a68a44) + * update dependency nodebb-theme-persona to v10.1.54 (#8428) (3ffb6fde) + * update dependency winston to v3.3.2 (54310d69) + * update dependency nodebb-plugin-spam-be-gone to v0.7.2 (11244348) + * update dependency winston to v3.3.1 (#8421) (fd628570) + * #8412 bump themes (365996e7) + * bump persona/vanilla, closes #8408 (225cac18) + * update dependency sharp to v0.25.4 (#8403) (3c3e1515) + * update dependency nodebb-theme-vanilla to v11.1.25 (#8405) (90446365) + * update dependency nodebb-theme-persona to v10.1.50 (#8404) (44273a64) + * update dependency mongodb to v3.5.9 (#8402) (90570660) + * update dependency nodebb-plugin-composer-default to v6.3.43 (78b7382c) + * update dependency validator to v13.1.1 (#8397) (2ae68f9b) + * update dependency validator to v13.1.0 (#8391) (ad6e3634) + * update dependency nodebb-theme-vanilla to v11.1.24 (#8395) (eec03de8) + * update dependency nodebb-theme-persona to v10.1.49 (#8394) (90846740) + * update dependency postcss to v7.0.32 (87ce31d1) + * update dependency nodebb-plugin-composer-default to v6.3.41 (dde830db) + * update dependency nodebb-plugin-composer-default to v6.3.40 (#8388) (cf0f8f64) + * bump themes, closes #8387 (e97a2b71) + * bump themes (ccac6a35) + * update dependency nodebb-theme-persona to v10.1.46 (#8382) (036e6ef5) + * update dependency nodebb-theme-vanilla to v11.1.21 (#8383) (7caeb273) + * update dependency nodebb-plugin-composer-default to v6.3.39 (aeefc60b) + * update dependency nodebb-plugin-composer-default to v6.3.37 (7f6ff0b1) + * update dependency nodebb-plugin-composer-default to v6.3.36 (6b2ea077) + * update dependency nodebb-plugin-composer-default to v6.3.35 (2d582df7) + * update dependency nodebb-theme-persona to v10.1.45 (#8372) (771ea194) + * update dependency nodebb-theme-vanilla to v11.1.20 (#8373) (f8ee981b) + * update dependency socket.io-redis to v5.3.0 (#8370) (d2463bb4) + * update dependency nodebb-theme-vanilla to v11.1.19 (f6ad9605) + * update dependency nodebb-theme-persona to v10.1.44 (cb28e6cf) + * update dependency nodebb-plugin-composer-default to v6.3.34 (#8357) (d7ab0894) + * update dependency nodebb-plugin-composer-default to v6.3.33 (c4047179) + * update dependency nodebb-plugin-spam-be-gone to v0.7.1 (27ab36ac) + * update dependency nodebb-theme-persona to v10.1.43 (#8343) (dabff972) + * update dependency mongodb to v3.5.8 (#8342) (8224127f) + * update dependency nodebb-theme-persona to v10.1.40 (#8332) (a20af6e2) + * update dependency nodebb-plugin-spam-be-gone to v0.7.0 (cc206b4d) + * bump composer-default (827d42a9) + * bump composer-default (a665881b) + * update dependency nodebb-plugin-composer-default to v6.3.29 (bf8a2c2e) + * update dependency postcss to v7.0.30 (#8288) (a532e2bb) + * update dependency sharp to v0.25.3 (0437ecc2) + * update dependency nodebb-plugin-composer-default to v6.3.28 (#8309) (f246057a) + * update dependency nodebb-plugin-composer-default to v6.3.27 (#8307) (6d57d844) + * #8298 bump persona (158d9231) + * update dependency nodebb-plugin-composer-default to v6.3.25 (89d17647) + * update dependency jquery to v3.5.1 [security] (#8281) (a69f0b29) + * update dependency nodebb-rewards-essentials to v0.1.3 (#8289) (919034a7) + * update dependency mongodb to v3.5.7 (#8279) (25d509c4) + * actually, swagger-parser is a dev dependency (d09c6ae0) + * missing @apidevtools/swagger-parser (f1720735) + * update dependency nodebb-theme-persona to v10.1.37 (#8258) (b0c30ceb) + * update dependency archiver to v4 (28777f67) + * update dependency mongodb to v3.5.6 (#8256) (49236067) + * bump dependencies (#8239) (e68156e1) + * update dependency jsesc to v3.0.1 (#8243) (92b55ef5) + * update dependency jsesc to v3 (bb70cebb) + * update dependency pg to v8 (#8227) (ac98775f) + * update dependency validator to v13 (f497ee62) + * update dependency sharp to v0.25.2 (#8220) (dd660c87) + * bump markdown (ee6cb412) + * update dependency mongodb to v3.5.5 (#8205) (5535c50c) + * update dependency sitemap to v6 (#8198) (2052f14c) + * update dependency nodebb-plugin-composer-default to v6.3.23 (6d98d5a1) + * update dependency sharp to v0.25.1 (#8199) (21e91c91) + * update dependency nodebb-plugin-composer-default to v6.3.22 (#8193) (e01f05e3) + * update dependency nodebb-theme-slick to v1.2.29 (#8177) (9daa21ff) + * update dependency nodebb-theme-vanilla to v11.1.16 (#8178) (7d6a983b) + * update dependency nodebb-theme-persona to v10.1.35 (#8176) (3acc24b0) + * update dependency sharp to v0.24.1 (#8164) (7cc63f7d) + * update dependency mongodb to v3.5.3 (#8161) (4b907137) + * update dependency nodebb-widget-essentials to v4.1.0 (#8159) (a5f3c2a2) + * update dependency request to v2.88.2 (#8158) (7fde180a) + * update dependency redis to v3 (#8152) (ef964b11) + * update dependency rimraf to v3.0.2 (#8153) (d8efc6b6) +* don't show in unreplied if score is null (fd400a00) +* lint (0d0b9513) +* upgrade script to unescape navigation titles (37b6b8fd) +* test (f0ce309d) +* show controls @julianlam (023de94e) +* #8437, #8433 (e53a18f2) +* copy settings showing empty category selection (ed4b5caf) +* don't init autocomplete if user doesn't have privs (8482a54a) +* only allow valid uids (00d8ce26) +* tests (f03ca086) +* vulnerability in cover and admin uploads (#8419) (48b41deb) +* reverse tabnabbing exploit in post images (040e6a9a) +* follower count going out of sync with real follower count (2bcf7f72) +* test lock for user create (#8415) (bef37e27) +* add mising timestamp (f0526bff) +* ban (bfd7eafe) +* remove use of 'hidden' class in navbar toggling (5a367ecb) +* bug where category privs page thought it was on admin (2515aa77) +* #8410 (b3115ea8) +* tests breaking due to #8406 (d5578c99) +* add missing translations (42466d3c) +* #8401, #8237 (7ed1a014) +* add timestamp to initial username history (18d89239) +* crash in export posts if post content is undefined (53a9517d) +* messaging unread (0041c024) +* whitespace (a024cc13) +* typo (0595e710) +* #8392, clear group member cache when group is renamed (89b01024) +* return false (8591f5d2) +* language (a255c8f6) +* #8386, use backgroundImage everywhere (8627bee5) +* prevent logout form from submitting (d92032da) +* missing space in ACP menu dropdown (daeceb45) +* #8385 (942cc4b1) +* acp language keys from #8347 not updated in tx config (9ae7fd3e) +* #8363, dont break history (50703db8) +* #8363, go to hash when entering topic (0c265a41) +* #8374, revert event delete (30cc83c0) +* new language tag for select_tags (09184f40) +* remove duplicate link to manage/tags in settings/tags (260a482c) +* tests (3a078f59) +* handle search tag permission as well (1b5d5425) +* checking correct permissions for user search (#8371) (f6b92d24) +* change event name so it doesnt trigger complete event (7786187e) +* clear error log before checking (75b3a81d) +* #8323, let admins send validation emails without timeout (e603ebc0) +* #8352, remove webfonts (#8354) (de7ec47f) +* more tests (da90fd56) +* tests due to 0633ad327 (98dffa3a) +* retry failed setObject calls (2c9e8657) +* acp menu items (0633ad32) +* re-jigged tags acp pages (a83f4259) +* move checks into timeout (47d73a2a) +* #8339, add missing translation (a9315aee) +* don't explode if server sends `checkSession` (84c20f91) +* eliminate unnecessary try..catch (f0e59c14) +* test breakages from 8d995d1eb609837e4e6e4c77cd855766830378fa (a66fe013) +* #8320, dont load moderators separately for each category (0a31e3e6) +* show stack trace on startup errors (11bb6abb) +* don't trigger quick search if val doesn't change (d6c2764f) +* prevent duplicate search triggers (6f78113e) +* user faster method if sorting by smallest set (3ec05eea) +* add txt to list of default allowed file extensions (01bff2ae) +* search post snipets incase content is plain text (ada45a34) +* derp includes (d484731d) +* another test fix (f2907908) +* tests (feb748a4) +* tests on redis (7a801aba) +* tests, handle no sessions (8bf980cb) +* #8318, clean expired sessions on login and get (a0d76ff0) +* tests (a032e12b) +* #8317 (81e33b93) +* #8142 invalid session warning if server-side session destroyed (526b3cd9) +* check privileges before exporting post/topic data (5fd81c5c) +* exporting posts (dfae664e) +* undefined uid when downloading posts (bdda0222) +* #8311, fix allowUserHomePage value (1ba6929c) +* dont crash if topic is null (e20ad5c5) +* if category in selector has url use it (1f992cf3) +* convert cids to string for comparison (c47a1c4d) +* missing await (97c086ab) +* add missing schema item (4e14cb57) +* category selector disabled categories (337be368) +* #8305, don't use null values (6a5e86dc) +* #8302, send string to writeFileSync (d09bd2cf) +* winston showing json object (7d081843) +* sortable topics even if only 1 pinned topic (6765de3d) +* #8298, use class name added by jQueryUI instead (dd2bc189) +* topic search shortcut for macs (f2c725c6) +* #8297, uids.length is different than topics.length (0431d75f) +* #8297 guest handles shown in category.tpl (fcb81cb8) +* only add to set if numRecentReplies>0 (16a98eaf) +* #8293, don't show error if there are no self messages (be305410) +* failing tests @julianlam (ecd622fd) +* #3321, run plugin tests for installed plugins (a6bb9f43) +* remove deprecated mocha.opts (3d0db963) +* spec (84383d39) +* #8290, if there are no filters go to ?reset=1 (9839346e) +* #8283, update gdpr link again (2d076344) +* add missing await (4f1128fd) +* #8287, dont readd user after deletion (9d153fd3) +* missing await (4d6b2ec3) +* #8286, rescind notif when its resolved/rejected (0391856d) +* #8284, parse ToS on register (0ca84bd9) +* #8283, point to official site (17d664e0) +* jquery xhtml violations (275e837b) +* #8274 Don't escape HTML in manage users (#8275) (4855f1de) +* crash in topic controller (0c7c70ed) +* crash when res.locals.linkTags is undefined (7cab2b0f) +* #8272 user link in digest email (e80b8101) +* tag of /api/unread/total (9ffdab02) +* response hook logic (5a1c6ee7) +* remove upload picture test (avatars) (6edf02d4) +* remove tests related to group covers, as route is gone (442fe65f) +* #8269, return array of topics from hook (4eafe0f0) +* remove dead picture upload code #8260 (ef52461f) +* path.resolve to logs file (5bcaf715) +* only trigger infinitescroll on scroll end (ba6d3fd3) +* wrong data returned in available.groups (c7ea84a2) +* no focus on find user modal (1b425ef1) +* accidental fp precision on flag and acp dash graphs (bcbf98aa) +* #8232, unresolvable session mismatch on register cancel (f2f6fbf1) +* pin jquery to 3.4.1, #8252 (e440d617) +* #8249, don't send move notifications for deleted posts/topics (d77036db) +* missing descriptions for common properties (7b31fb34) +* some definitions in read API spec (03739b6f) +* tagged all routes in read api spec (455d42bc) +* override ACAO header for read API spec file (240d9091) +* throw error if topic does not exist (59cf0e80) +* hookname (e93578b8) +* #8230, add hook getUserDataByUserSlug (0d1b5a7f) +* ignore case for group details route (15d6975e) +* lint (740e598a) +* lint (8e23dec8) +* #8221, fix parent selection (08031730) +* invite properly (071506eb) +* admins not seeing invite button (8f4b99a4) +* #8217, add missing lang key (0b5fac75) +* #8206 first message in chat has false `newSet` (93acd139) +* #8203, fix user invites refreshing page (2f9c7c62) +* #8202, filter non-existing users in search by uid (f07f4f8e) +* notification bodyShort truncated if there is a comma in topic title (266061c3) +* hsts max-age missing translation (b67af70d) +* call next (80f1bcad) +* try travis fix again (05bee629) +* try fixing psql on travis (bc9e92a1) +* dont let regular users see other users watched categories (cf6eadb9) +* also fix updating bookmark if sorting is newest_to_oldest (6e5de39b) +* #8188, fix bookmark if sorting is newest_to_oldest (32ada7c4) +* duplicate ID + label (ac241fb8) +* #8184 global mods unable to revoke other user sessions (f0db240a) +* return null if field does not exist (e72a29b3) +* #8179, limit length of location/website/fullname, check grouptitle (14e78667) +* tag key (32636755) +* #8175 (bc93b567) +* #8168 re-allowing slashes in homePageRoute (667608a0) +* tweak to session validation in addHeaders (eddbd868) +* only call clearCookie for logged in users (630f5d5b) +* #6422, update deleted/restored messages (06703408) +* #8163, prevent account deletion (4d0636f8) +* register (5a0c7c14) +* #8157, update recent tid when post is moved (e7495440) +* tests (b73aa84d) +* move start/stop every iteration (dd3893b1) +* #8154, move start/stop every iteration (300c04ce) +* #8154, respect stop (690bb69d) +* #8156 dont allow loading members from hidden groups (f23bc347) +* #8155, don't validate name on update if groupName didn't change (03a02e5d) +* return correct number of suggested topics (236e1e68) +* #8151, don't crash if taskbar doesn't have element (2e794801) +* logic for determining dailyStats hour vars (398f0120) +* fix daily analytics being one day off (9ecdb92f) +* remove debug line (0b9ad416) +* no decimal places for category analytics (14655f87) +* #8142, broken site if no server-side session (#8148) (d6e3f3f0) +* #8144 pluginHooks in maintenance mode middleware (0885ec68) +* **openapi:** + * tests for #8412 (4cae893f) + * api change for #8387 (6a969442) + * schema for new flags api (a0e243ee) + * broken tests (45dfeeb0) + * v14 test fix (23a0b8c5) + * remove account and group upload routes (d342a28c) + * more fleshing out (058a15db) + * fleshed out admin routes (bae88e08) + * added some descriptions (ab4bd7e1) + * added PostsObject component (2395d2be) + * finished moving all category objects out (23dd2727) + * changed some descriptions (c939f8c6) + * added CategoryObject component (55d0a9ff) + * removed repeated breadcrumb blocks in favour of $ref (646fac1e) + * remove all repeated pagination blocks in favour of (ac579f9d) + * removed warning for category mods route (1cf62095) + * normalising the file for programmatic updates (3a5c6e07) +* **style:** + * more switch..case (6b1d1acb) + * switch..case (922d49be) + * enforcing "better" indentation for switch..case (d135b6f7) +* **docs:** added titles to all routes (aa4ae78b) + +##### Other Changes + +* id, category fields (2355d9d5) +* //github.com/NodeBB/NodeBB (c3c8b19a) +* //github.com/NodeBB/NodeBB (0ddfb6b0) +* //github.com/NodeBB/NodeBB (d1c1cb2c) +* //github.com/NodeBB/NodeBB (79a7f892) +* user.getFields to match topic/posts (a680a95e) +* //github.com/NodeBB/NodeBB (b459592a) +* //github.com/NodeBB/NodeBB (066b442e) +* //github.com/NodeBB/NodeBB (fd6bf0c9) +* //github.com/NodeBB/NodeBB (c1d8b9bb) +* middleware.renderHeader (2727f472) +* //github.com/NodeBB/NodeBB (69a87933) +* uploadFile (7f24200c) +* #8142 invalid session warning if server-side session destroyed" (e327d124) +* ''}) (2a00b0e9) +* //github.com/NodeBB/NodeBB (8c8cdc99) +* categories.getRecentTopicReplies (aad0880f) +* #8298 (2e57d8ac) +* post.updatePostVoteCount (b25b51bd) +* //github.com/NodeBB/NodeBB (5e140454) +* categories.updateRecentTid (6c59683b) +* categories.updateRecentTid (51933c1f) +* router.page, dep. filter variant (0053e779) +* flags as well (5ebcdb18) +* crash when res.locals.linkTags is undefined" (fe03effe) +* //github.com/NodeBB/NodeBB (87a6ff0d) +* cnpm and pnpm (#8222) (e6a1741c) +* //github.com/NodeBB/NodeBB (7ae76477) +* openapi component" (683e5851) +* override ACAO header for read API spec file" (c82a2637) +* password.change (00e299e9) +* topic.tools.load (5aa76cdf) +* #8154, move start/stop every iteration" (4abe5eb7) +* **deps:** update dependency nodebb-plugin-mentions to v2.8.0" (5c7d37c0) + +##### Refactors + +* make code climate happier? (0d112b36) +* shorter code (af790e3f) +* shorter code (e8f0da6e) +* change name to privileges to match other apis (2100a03c) +* making rendering of header and footer async functions (023942da) +* remove general menu from ACP (#8347) (a51fff8b) +* use getSortedSetMembers (7d484fc0) +* src/flags.js because codeclimate (cf00cda0) +* flags detail page (8d995d1e) +* move code (3b6d57e4) +* remove console.log (40b5cbab) +* shorter function (7e429884) +* match core field name pinned (478ed6c1) +* getUsersCSV to use batch lib (1efb238a) +* reorganized socket.io admin modules (e1c6c3b2) + +##### Reverts + +* bad changelog (a761e31f) + +#### 1.13.3 (2020-05-08) + +##### Chores + +* incrementing version number - v1.13.3 (ee583e80) +* bump persona (d2bd746c) +* incrementing version number - v1.13.2 (beafd613) +* **deps:** + * update dependency coveralls to v3.1.0 (5ed4a108) + * pin dependency @apidevtools/swagger-parser to 9.0.1 (51eccef5) + * update dependency husky to v4.2.5 (30a25983) + * update dependency husky to v4.2.4 (0a650118) + * update dependency lint-staged to v10.1.3 (a9e68639) + * update dependency nyc to v15.0.1 (#8231) (a3789e28) + * update dependency lint-staged to v10.1.2 (#8235) (e1919c90) + * update dependency lint-staged to v10.1.1 (944a6f58) + * update dependency lint-staged to v10.1.0 (30bd233b) + * update dependency eslint-config-airbnb-base to v14.1.0 (811c3aee) + * update dependency jsdom to v16.2.2 (c5a7242d) + * update dependency eslint-plugin-import to v2.20.2 (b92c1600) + * update dependency lint-staged to v10.0.10 (0ad4b556) + * update dependency coveralls to v3.0.11 (14458087) + * update dependency smtp-server to v3.6.0 (22681945) + * update dependency mocha to v7.1.1 (#8215) (c5356541) + * update dependency grunt to v1.1.0 (#8214) (b0864e7c) + * update dependency husky to v4.2.3 (#8162) (776fe9d2) + * update dependency lint-staged to v10.0.8 (#8180) (13d8f6f1) + * update dependency eslint-plugin-import to v2.20.1 (#8081) (4cdb3131) + * update dependency jsdom to v16.2.1 (#8165) (fbd95a50) + * update dependency husky to v4.2.2 (#8160) (f4ed35c9) + * update dependency jsdom to v16 (#8114) (1037de02) + +##### Documentation Changes + +* updated changelog (146388aa) + +##### New Features + +* allow activating additional plugins for testing via config.json (a969c5ce) +* move plugin tests to separate file (3a23ddab) +* remove node14 for now (a72e4429) +* reduce infinite scroll area (3fcbd691) +* manifest.json improvements from #8126 (#8264) (6e5ebb61) +* show error if json is invalid (15345627) +* moved component specs into separate files (cd506557) +* added UserObject, UserObjectFull, Breadcrumb, Pagination component (64d79fe5) +* added some summary and descriptions (ae3e90d6) +* add some descriptions (442c018e) +* common schema (eade13f9) +* openapi component (1af5507a) +* add page query param to docs (9987813f) +* tag route doc (bbddaadf) +* local redoc view on development mode only (1136a369) +* added auto-generated, slimmed-down openapi 3.0 file for read api (7b155dab) +* add parent cids to body class (23571224) +* add 2 hooks for modifying privileges (d080c7b0) +* add user ip to admin/dev/info (5e91a67e) +* change option name (cba5b23e) +* add no-build to ./nodebb setup (476f6717) +* add awaitable socket.emit (4083a6e3) +* settings sorted list (#8170) (3c9689a5) +* guard against accidental ommision (79737c53) +* **openapi:** + * merging openapi-test branch into master (8387178b) + * move all commonprops out for commonprops component (65c78de6) + * added template to commonprops (2425f453) + +##### Bug Fixes + +* #8302, send string to writeFileSync (d09bd2cf) +* winston showing json object (7d081843) +* sortable topics even if only 1 pinned topic (6765de3d) +* #8298, use class name added by jQueryUI instead (dd2bc189) +* topic search shortcut for macs (f2c725c6) +* #8297, uids.length is different than topics.length (0431d75f) +* #8297 guest handles shown in category.tpl (fcb81cb8) +* only add to set if numRecentReplies>0 (16a98eaf) +* #8293, don't show error if there are no self messages (be305410) +* failing tests @julianlam (ecd622fd) +* #3321, run plugin tests for installed plugins (a6bb9f43) +* remove deprecated mocha.opts (3d0db963) +* spec (84383d39) +* #8290, if there are no filters go to ?reset=1 (9839346e) +* #8283, update gdpr link again (2d076344) +* add missing await (4f1128fd) +* #8287, dont readd user after deletion (9d153fd3) +* missing await (4d6b2ec3) +* #8286, rescind notif when its resolved/rejected (0391856d) +* #8284, parse ToS on register (0ca84bd9) +* #8283, point to official site (17d664e0) +* jquery xhtml violations (275e837b) +* #8274 Don't escape HTML in manage users (#8275) (4855f1de) +* crash in topic controller (0c7c70ed) +* crash when res.locals.linkTags is undefined (7cab2b0f) +* #8272 user link in digest email (e80b8101) +* tag of /api/unread/total (9ffdab02) +* response hook logic (5a1c6ee7) +* remove upload picture test (avatars) (6edf02d4) +* remove tests related to group covers, as route is gone (442fe65f) +* #8269, return array of topics from hook (4eafe0f0) +* remove dead picture upload code #8260 (ef52461f) +* path.resolve to logs file (5bcaf715) +* only trigger infinitescroll on scroll end (ba6d3fd3) +* wrong data returned in available.groups (c7ea84a2) +* no focus on find user modal (1b425ef1) +* accidental fp precision on flag and acp dash graphs (bcbf98aa) +* #8232, unresolvable session mismatch on register cancel (f2f6fbf1) +* pin jquery to 3.4.1, #8252 (e440d617) +* #8249, don't send move notifications for deleted posts/topics (d77036db) +* missing descriptions for common properties (7b31fb34) +* some definitions in read API spec (03739b6f) +* tagged all routes in read api spec (455d42bc) +* override ACAO header for read API spec file (240d9091) +* throw error if topic does not exist (59cf0e80) +* hookname (e93578b8) +* #8230, add hook getUserDataByUserSlug (0d1b5a7f) +* ignore case for group details route (15d6975e) +* lint (740e598a) +* lint (8e23dec8) +* #8221, fix parent selection (08031730) +* invite properly (071506eb) +* admins not seeing invite button (8f4b99a4) +* #8217, add missing lang key (0b5fac75) +* #8206 first message in chat has false `newSet` (93acd139) +* #8203, fix user invites refreshing page (2f9c7c62) +* #8202, filter non-existing users in search by uid (f07f4f8e) +* notification bodyShort truncated if there is a comma in topic title (266061c3) +* hsts max-age missing translation (b67af70d) +* call next (80f1bcad) +* try travis fix again (05bee629) +* try fixing psql on travis (bc9e92a1) +* dont let regular users see other users watched categories (cf6eadb9) +* also fix updating bookmark if sorting is newest_to_oldest (6e5de39b) +* #8188, fix bookmark if sorting is newest_to_oldest (32ada7c4) +* duplicate ID + label (ac241fb8) +* #8184 global mods unable to revoke other user sessions (f0db240a) +* return null if field does not exist (e72a29b3) +* #8179, limit length of location/website/fullname, check grouptitle (14e78667) +* tag key (32636755) +* #8175 (bc93b567) +* #8168 re-allowing slashes in homePageRoute (667608a0) +* tweak to session validation in addHeaders (eddbd868) +* only call clearCookie for logged in users (630f5d5b) +* #6422, update deleted/restored messages (06703408) +* #8163, prevent account deletion (4d0636f8) +* register (5a0c7c14) +* #8157, update recent tid when post is moved (e7495440) +* tests (b73aa84d) +* move start/stop every iteration (dd3893b1) +* #8154, move start/stop every iteration (300c04ce) +* #8154, respect stop (690bb69d) +* #8156 dont allow loading members from hidden groups (f23bc347) +* #8155, don't validate name on update if groupName didn't change (03a02e5d) +* return correct number of suggested topics (236e1e68) +* #8151, don't crash if taskbar doesn't have element (2e794801) +* logic for determining dailyStats hour vars (398f0120) +* fix daily analytics being one day off (9ecdb92f) +* remove debug line (0b9ad416) +* no decimal places for category analytics (14655f87) +* #8142, broken site if no server-side session (#8148) (d6e3f3f0) +* #8144 pluginHooks in maintenance mode middleware (0885ec68) +* **deps:** + * #8298 bump persona (158d9231) + * update dependency nodebb-plugin-composer-default to v6.3.25 (89d17647) + * update dependency jquery to v3.5.1 [security] (#8281) (a69f0b29) + * update dependency nodebb-rewards-essentials to v0.1.3 (#8289) (919034a7) + * update dependency mongodb to v3.5.7 (#8279) (25d509c4) + * actually, swagger-parser is a dev dependency (d09c6ae0) + * missing @apidevtools/swagger-parser (f1720735) + * update dependency nodebb-theme-persona to v10.1.37 (#8258) (b0c30ceb) + * update dependency archiver to v4 (28777f67) + * update dependency mongodb to v3.5.6 (#8256) (49236067) + * bump dependencies (#8239) (e68156e1) + * update dependency jsesc to v3.0.1 (#8243) (92b55ef5) + * update dependency jsesc to v3 (bb70cebb) + * update dependency pg to v8 (#8227) (ac98775f) + * update dependency validator to v13 (f497ee62) + * update dependency sharp to v0.25.2 (#8220) (dd660c87) + * bump markdown (ee6cb412) + * update dependency mongodb to v3.5.5 (#8205) (5535c50c) + * update dependency sitemap to v6 (#8198) (2052f14c) + * update dependency nodebb-plugin-composer-default to v6.3.23 (6d98d5a1) + * update dependency sharp to v0.25.1 (#8199) (21e91c91) + * update dependency nodebb-plugin-composer-default to v6.3.22 (#8193) (e01f05e3) + * update dependency nodebb-theme-slick to v1.2.29 (#8177) (9daa21ff) + * update dependency nodebb-theme-vanilla to v11.1.16 (#8178) (7d6a983b) + * update dependency nodebb-theme-persona to v10.1.35 (#8176) (3acc24b0) + * update dependency sharp to v0.24.1 (#8164) (7cc63f7d) + * update dependency mongodb to v3.5.3 (#8161) (4b907137) + * update dependency nodebb-widget-essentials to v4.1.0 (#8159) (a5f3c2a2) + * update dependency request to v2.88.2 (#8158) (7fde180a) + * update dependency redis to v3 (#8152) (ef964b11) + * update dependency rimraf to v3.0.2 (#8153) (d8efc6b6) +* **openapi:** + * v14 test fix (23a0b8c5) + * remove account and group upload routes (d342a28c) + * more fleshing out (058a15db) + * fleshed out admin routes (bae88e08) + * added some descriptions (ab4bd7e1) + * added PostsObject component (2395d2be) + * finished moving all category objects out (23dd2727) + * changed some descriptions (c939f8c6) + * added CategoryObject component (55d0a9ff) + * removed repeated breadcrumb blocks in favour of $ref (646fac1e) + * remove all repeated pagination blocks in favour of (ac579f9d) + * removed warning for category mods route (1cf62095) + * normalising the file for programmatic updates (3a5c6e07) + +##### Other Changes + +* #8298 (2e57d8ac) +* post.updatePostVoteCount (b25b51bd) +* //github.com/NodeBB/NodeBB (5e140454) +* categories.updateRecentTid (6c59683b) +* categories.updateRecentTid (51933c1f) +* router.page, dep. filter variant (0053e779) +* flags as well (5ebcdb18) +* crash when res.locals.linkTags is undefined" (fe03effe) +* //github.com/NodeBB/NodeBB (87a6ff0d) +* cnpm and pnpm (#8222) (e6a1741c) +* //github.com/NodeBB/NodeBB (7ae76477) +* openapi component" (683e5851) +* override ACAO header for read API spec file" (c82a2637) +* password.change (00e299e9) +* topic.tools.load (5aa76cdf) +* #8154, move start/stop every iteration" (4abe5eb7) + +##### Refactors + +* match core field name pinned (478ed6c1) +* getUsersCSV to use batch lib (1efb238a) +* reorganized socket.io admin modules (e1c6c3b2) + +#### 1.13.2 (2020-02-05) + +##### Chores + +* incrementing version number - v1.13.2 (71f4607d) +* bump themes (027f3f22) +* bump vanilla (236a1730) +* bump persona (82ace391) +* incrementing version number - v1.13.1 (cc6758a0) +* **deps:** + * update dependency eslint to v6.8.0 (#8062) (15783213) + * update dependency nyc to v15 (#8094) (976e26a9) + * update commitlint monorepo (#8100) (eb4a1a57) + * update dependency lint-staged to v10.0.7 (#8132) (cdfbcbb9) + * update dependency mocha to v7 (#8106) (b370333c) + * update dependency husky to v4 (dd440ce9) + * update dependency lint-staged to v10.0.1 (66992a55) + * update dependency lint-staged to v10 (d74eecfb) + +##### Documentation Changes + +* updated changelog (2edc6960) + +##### New Features + +* add test for isOnline (66febb80) +* add test for change post owner (df2c7851) +* check flag values on save (assignee and state) (#8122) (8e5a2276) + +##### Bug Fixes + +* admin relogin (a5ef6b53) +* #8135 (c35a21d7) +* handle mkdirp0.5->1.0x so it doesn't break upgrade (1e50616c) +* #8134, upgrade mkdirp to 1.0.x (87225a90) +* onSuccessfulLogin not working (111ed802) +* #8139, dont allow restore if not deleted by self (8c48f94b) +* use view_deleted when filtering, closes #8137 (9969dd63) +* escape invalid rules (d927b763) +* add missing await (3cca929a) +* missing await in SocketPosts.changeOwner (0ae1eb4f) +* #8133, check if user is in room before removing (23810cc6) +* add missing await (cd1fa27a) +* missing await (f799f017) +* dont return flag data to client (418c174d) +* check if user has read priv before flagging (51236df4) +* restrict getUsersInRoom to members (1f13ab8a) +* remove unused conditional, dont add dupe messages (3077eb94) +* tests for messaging (ecc579a2) +* #8127 user join system message duplicated (594cd7e1) +* background-size in taskbar images (106c141f) +* tests, was using hardcoded message id (1b08f376) +* typo in #8116 (8bb5e71e) +* build step defaults to series instead of parallel (3fac09b1) +* escape system message, don't allow editing system messages (6a63c1a1) +* escape register query param (c8fb7f92) +* delete upload (8c6a7954) +* check uploadName (153b1a0e) +* #8120, bubble errors from static hooks (01d1ae78) +* escape bootswatchSkin and homepageRoute (b0f3e48a) +* change owner missing await (3e525576) +* hsts always enabled (e3952674) +* escape topic.thumb (b7a57996) +* #8112, don't crash hook returns no data (4eb9652a) +* escape config.userLang/acpLang, don't allow invalid language codes (e06c1bfc) +* group create/join/update name validation (61da8c29) +* don't crash if groupData is missing (48f08627) +* #8105, fix export json on page load (5a8217de) +* #8103, fix advanced menu not displaying in ACP (52774531) +* meta description missing if url doesn't have post index (10989ccc) +* create user modal instantly closing (c1b1ee61) +* login with weak password (9d074731) +* dont check password strength on login (f6d7a24a) +* **deps:** + * update dependency connect-redis to v4.0.4 (#8143) (16ab641d) + * update dependency rimraf to v3.0.1 (#8138) (726ba71c) + * update dependency validator to v12.2.0 (#8136) (f07b4bfa) + * update dependency nodebb-theme-persona to v10.1.34 (#8140) (6d7131fb) + * update dependency nodebb-theme-persona to v10.1.31 (#8129) (c510a2c4) + * update dependency mongodb to v3.5.2 (#8092) (0e49cfb9) + * update dependency sharp to v0.24.0 (#8121) (16e8f496) + * update dependency nodebb-plugin-composer-default to v6.3.21 (#8119) (ca10f8f0) + * update dependency nodebb-widget-essentials to v4.0.18 (#8111) (df5e3a73) + +##### Other Changes + +* NodeBB/NodeBB (b959c24a) +* //github.com/NodeBB/NodeBB (ee4304b4) +* //github.com/NodeBB/NodeBB (bfaba895) +* save disableLeave (#8123) (09d55581) +* //github.com/NodeBB/NodeBB (842916ea) + +##### Refactors + +* messaging (30c50361) + +#### 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) + +##### Chores + +* incrementing version number - v1.13.0 (c38b2d23) +* incrementing version number - v1.12.2 (810e2c0b) +* **deps:** + * update dependency jsdom to v15.2.1 (9d946d1c) + * update dependency eslint to v6.6.0 (d0e428e9) + * update dependency lint-staged to v9.4.2 (#7954) (d108c7d0) + * update dependency mocha to v6.2.2 (#7984) (e31a47cd) + * update dependency coveralls to v3.0.7 (#7961) (9b308e4b) + * update dependency jsdom to v15.2.0 (#7971) (9b4e9882) + * update dependency husky to v3.0.9 (fd5095a3) + * update dependency husky to v3.0.8 (88cae415) + * update dependency lint-staged to v9.4.1 (a2a7bb3d) + * update node.js to v8.16.1 (f9ad826b) + * update node:8.16.0 docker digest to bb12612 (48cc317c) + * update dependency eslint to v6.5.1 (da12e947) + * update dependency husky to v3.0.7 (10a30e66) + * update commitlint monorepo to v8.2.0 (ddd4e039) + * update dependency mocha to v6.2.1 (b172d106) + * update dependency lint-staged to v9.2.3 (ccccba81) + * update dependency coveralls to v3.0.6 (#7820) (2b1f840e) + * update dependency husky to v3.0.3 (cbede89a) + * update dependency lint-staged to v9 (f2e4664d) + * update dependency eslint-plugin-import to v2.18.2 (#7779) (57b2a553) + * update dependency eslint to v6.1.0 (#7783) (7257e6b2) + * update dependency husky to v3.0.2 (7ad72b32) + * update node:8.16.0 docker digest to c671dc2 (e76214a2) + * update dependency mocha to v6.2.0 (d43f5dcf) + * update node:8.16.0 docker digest to 310db2a (c46a0772) + * update dependency husky to v3.0.1 (28151f86) + * update dependency eslint-plugin-import to v2.18.1 (9fda2c1f) + * update commitlint monorepo to v8.1.0 (f73468d5) + * update dependency coveralls to v3.0.5 (5b746d53) + * update dependency husky to v2.7.0 (1d0fd028) + * update dependency eslint to v6.0.1 (77347d0a) + * update dependency husky to v2.6.0 (fc69e891) + * update dependency eslint-plugin-import to v2.18.0 (744e4428) + * update dependency husky to v2.5.0 (4960b925) + * update dependency lint-staged to v8.2.1 (2fa68e3b) + * update node:8.16.0 docker digest to 06adec3 (fc224cca) + * update dependency husky to v2.4.1 (17ec8fde) + * update node:8.16.0 docker digest to d5ad3f5 (f9e99fa3) + * update node:8.16.0 docker digest to 75b524a (5995834b) + * update node:8.16.0 docker digest to b9a98ae (fca6d794) + * update dependency lint-staged to v8.2.0 (376390c4) + * update dependency husky to v2.4.0 (3d2ec0bb) + * update dependency coveralls to v3.0.4 (c360e0c2) + * update dependency eslint-plugin-import to v2.17.3 (eac4e2f1) + * update node:8.16.0 docker digest to 957cab2 (cc6fa97c) + * update commitlint monorepo to v8 (80532831) + +##### Documentation Changes + +* updated changelog (183b0ed3) + +##### New Features + +* displaying one-click unsubscribe link in email footer (#8024) (df139928) +* #7467, pass query params when redirecting to posts (480a64aa) +* use helpers.setupAdminPageRoute (b5a30006) +* wip, better digest handling (+ eventual digest resend logic) (#7995) (645d6472) +* add action:messaging.save (ac5c560c) +* #7957, allow post queue based on group (1cedc4a0) +* add filter:topics.unreadCutoff (e020b85b) +* Add filter:topic.delete and filter:topic.restore (#7946) (#7989) (989107d9) +* no more session cookie for guests (#7982) (cf7e0cfd) +* Implement WICG change-password-url (#7072) (#7990) (df1efe5f) +* log errors from mubsub (b01a47cb) +* upgrade to sitemap5 (#7980) (d6792188) +* #7964, change all categories at once (485fbd2f) +* closes #7952, translate widget-settings (990508a5) +* remove ability to delete events from acp (554e6711) +* resetting theme will reset skin (03827fa6) +* disable timeout on launch route (4bc77d06) +* add new hook to get custom category tids for unread (b1926054) +* adding filter:login.check and loginFormEntry[] for the filter:login.build hook (#7861) (94810fd6) +* #7932, redirect to group details after creation (2444ed5c) +* add new hook filteR:topics.getSortedTids (b93c826d) +* bypass cropper for gifs (9a756004) +* deprecate file.isFileTypeAllowed (ffe3670f) +* send notification to group owners when someone leaves (ed122364) +* add failing test for pagination (#7924) (22b02f14) +* refactor getSortedSetRange to allow big arrays (b602c044) +* add new test (1e5246f9) +* add rss feed url for tags (bbc2e956) +* add sm2x avatar class #7813 (35a4ca2f) +* #7090, abililty to hide widgets (71cd602d) +* #7760, body-parser config (5de6d885) +* actually cache duh (f05c1dae) +* cache tags:topic:count (63bd252f) +* increase search timeouts (1789ecb4) +* make handleSearch public (ebe5ed22) +* increase size of local cache (b81405a8) +* don't hardcode numberic fields in user search (09410ff1) +* allow only watching core (451c7fd4) +* client-side taskbar.update method, deprecates .updateTitle() (9b09ee0e) +* option to restrict group leaving, closes #7770 (1796b65d) +* preserver querystring when changing category sort (ad96b0e0) +* html sanitization on all filter:parse.* hooks, closes #7872 (2580306d) +* up socket.io-adapter-mongo (3d2cb628) +* update LESS to v3.x, #7855 (#7867) (aea04de0) +* up psql socket.io adapter (b565e568) +* add client side hook for quick search&normal search (f31d30cd) +* allow floating pinned topics to top in getSortedTopics (1d7e0c63) +* promisify recent.getData (6f7ab586) +* faster initial stat loading (89cd31ed) +* cache stats, mongo count sucks (cb50b3f4) +* dashboard stats (3ff6e1bb) +* add filter:search.getPosts (aeb44faf) +* add image and imageClass to post summaries (80bd52fc) +* #7842, make isInvited, isPending work with uids (fa268556) +* convert search controller to async/await (acf12e3d) +* notification on membership acceptance, closes #7835 (1a2a381a) +* additional events logged for various group actions (1ce33faa) +* async (e6353486) +* async/await controllers/admin/dashboard (32ea04a8) +* async/await admin/controllers (72590b34) +* async/await (7beef91c) +* async/await refactor (3cc7ec63) +* async/await controllers/accounts (a3541d88) +* rewrite getRawPost to async/await (b734defb) +* added new filter hook filter:post.getRawPost (973075cf) +* convert categories controller to async/await (e9fd49e2) +* new client-side hooks for chat minimize and close (3d3fa865) +* test psql without defineProperty (#7815) (af1f7249) +* fire updateRemainingLength hook for chats (af968c6a) +* send pids into filter:post.getFields hook (0e07ffa1) +* move cache tpl (65fc0612) +* async refactor (3a9d83a4) +* move group privs above user, add labels (47e30a67) +* widgets/index.js (dec8040c) +* use promise version (4d6b6871) +* #7743, meta/dependencies (1c2540d3) +* #7743, widgets/admin (96ebe7b5) +* #7743, meta/languages and languages (c02686bf) +* added filter:tags.getAll plugin hook (66aa443b) +* #7743, events (102d4b02) +* #7743 meta/errors (97d7a850) +* get rid of async.eachSeries (99cf47ee) +* #7743 meta/tags.js (7b908639) +* #7743 meta/templates.js (79eed9ae) +* #7743 navigation (764a2b12) +* #7743 account/posts controller (e72f3e4f) +* #7743 finish topics controller (9b3f4b98) +* #7743, webserver (0d047f4e) +* #7743 meta/themes.js (7dc0eaf0) +* #7743, meta/sounds (a15c50bf) +* #7743 , meta/cachebuster, meta/configs (7f72181e) +* convert src/messaging/* to async/await (#7778) (756a717e) +* #7743 meta/blacklist (fcf8fd51) +* #7743 meta/index (69860e58) +* #7743 plugins (c126cd85) +* #7743 plugins/data (f5f5f76b) +* dont waste whitespace (17f843f9) +* switch to promise.all (34d85b52) +* #7743, privileges (faccb191) +* #7743 , search.js (6d3a92b8) +* shorter name (8e75646a) +* cache group:members for priv groups (daed76d5) +* #7743 finish groups (72def7df) +* #7743, groups/index, invite, leave,membership (a39ca51e) +* #7743,groups/index,join (d5342a40) +* #7743, groups/delete,ownership,posts,user (fcd4445a) +* #7743 groups/cover,create,data (5e8614e1) +* #7743, finish post module (c0c6c652) +* #7743 posts/diff, posts/edit (c4bb467e) +* #7743, posts module (1b2b308a) +* #7743, finish user module (a51ec591) +* #7743 (6fea46b6) +* #7743 (cd80c263) +* #7743 (0a690c57) +* #7743 (1c5fad6d) +* #7743 (fe4c0481) +* #7743 categories (fcf3e077) +* #7743, user/digest, user/email, user/follow (c610eb14) +* #7743, user/create, user/data, user/delete (d6e36c31) +* #7743 , user/block, user/categories (4541caa4) +* #7743 notifications (6f738c2b) +* remove unused code, add 2 tests (cd2e68cb) +* #7743 user/ban, up mubsub (1970214a) +* #7743, user/approval, user/auth (b24ce976) +* #7743, posts/votes (8ef75be3) +* #7743 posts (e1ecc36d) +* #7743 user/admin.js (2c335955) +* add utils.promiseParallel (4170abfd) +* #7743, posts/user.js (0a6cfb37) +* added new admin option newbiePostEditDuration (#7737) (8a6a58ee) +* returning category data in tags page (75ff4d7d) +* ACP analytics API route (#7725) (a0c0ef1b) +* closes #7718 (ee4d78ca) +* add status to post summaries (41bc6ca2) +* use arrow functions (3100d803) +* ability to sort watched topics, closes #3735 (f24c14d7) +* use fewer system resources to draw the progress bar (d0ee312c) +* removed series from upgrade script (23fb904f) +* remove duped zadds, these are already in topics.onNewPostMade (cb51c239) +* log data on error (f8a7cf73) +* cleanup, use bulk (198d0587) +* #7707, added sortedSetAddBulk (3ecd703e) +* change widget-* to data-widget-* (#7703) (13efbc99) +* shorter setsCard (3780a58f) +* show best & latest posts on profile (b53a60c2) +* use db.sortedSetsAdd (7e54d7aa) +* use db.sortedSetsAdd (29a124c4) +* dont log action:plugins.fireHook (a7600b4b) +* upgrade to connect-mongo 3.0.0, closes #7693 (ebfc369a) +* add hook for user notifications, closes #7672 (ede060a6) +* user/notifications refactor (580f7860) +* send new post/topic event in batches (8c331088) +* allowing count to be passed to ./nodebb events (f6d3cc0e) +* awaitable websockets (#7645) (aee47b29) +* hooks can now return promise or call callbacks (a6436716) +* fallback strings for ACP events (99120676) +* design revamp of ACP events page (#7664) (c7f95cce) +* optimize group rename (48538b28) +* allow multiple scores in sortedSetsAdd (31ccc860) +* optimize copy privileges (0dca13e9) +* on category search reveal children and parents (0721bee1) +* async3 upgrade (#7639) (4d9bc30d) +* #7627, close chat on ajaxify on mobile (6cebc7f0) +* update meta tags on ajaxify (#7580), fixes #7544 (a41769e6) + +##### Bug Fixes + +* **deps:** + * update dependency mongodb to v3.3.4 (#8032) (2093418d) + * update dependency nodebb-theme-vanilla to v11.1.10 (c01699f7) + * update dependency nodebb-theme-persona to v10.1.27 (182397b1) + * update dependency nodebb-plugin-spam-be-gone to v0.6.7 (#8027) (c2565a2f) + * update dependency nodebb-theme-slick to v1.2.27 (#8020) (7122bdc7) + * update dependency nodebb-theme-persona to v10.1.26 (#8019) (73d9752e) + * update dependency connect-mongo to v3.1.2 (#8013) (bfea9d39) + * update dependency postcss to v7.0.21 (#7998) (ef5f6714) + * update dependency nodebb-plugin-composer-default to v6.3.17 (#8009) (e1e2d20c) + * update dependency sharp to v0.23.2 (#8005) (420f9fe4) + * update dependency nodebb-theme-persona to v10.1.25 (#8012) (cb91e756) + * update dependency connect-mongo to v3.1.1 (#8006) (7655265c) + * update dependency connect-mongo to v3.1.0 (#7994) (401d1eed) + * bump persona (6155c460) + * update dependency nodebb-theme-persona to v10.1.23 (#7986) (9bd6f686) + * update dependency nodebb-theme-persona to v10.1.22 (#7965) (bf6ae6d6) + * update dependency nodebb-rewards-essentials to v0.1.2 (#7962) (692d94bf) + * update dependency nodebb-theme-persona to v10.1.21 (#7956) (1a150d8f) + * update dependency nodebb-plugin-spam-be-gone to v0.6.6 (#7944) (cffbb325) + * update dependency nodebb-theme-persona to v10.1.20 (#7943) (9589fa32) + * update dependency nodebb-theme-persona to v10.1.19 (#7942) (0d629f06) + * update dependency connect-redis to v4.0.3 (#7933) (2856333d) + * update dependency spider-detector to v2 (310039e8) + * update socket.io packages to v2.3.0 (764ed7f8) + * update dependency sharp to v0.23.1 (#7928) (17437897) + * update dependency nodebb-plugin-dbsearch to v4.0.6 (#7918) (44cd7189) + * update dependency nodebb-plugin-composer-default to v6.3.16 (#7917) (d8fe6e42) + * update dependency nodebb-plugin-composer-default to v6.3.15 (#7916) (c5e5e24b) + * update dependency nodebb-plugin-composer-default to v6.3.14 (#7914) (e18392ab) + * update dependency nodebb-theme-persona to v10.1.18 (#7906) (8ed80bcb) + * update dependency nodebb-rewards-essentials to v0.1.1 (#7905) (5b8bb3e4) + * update dependency connect-redis to v4 (#7869) (8b7f6566) + * update dependency nodebb-plugin-mentions to v2.7.3 (#7899) (b2423bae) + * update dependency nodebb-plugin-mentions to v2.7.2 (#7898) (a3120a62) + * update dependency nodebb-plugin-dbsearch to v4.0.5 (#7896) (106c20e2) + * update dependency nodebb-plugin-composer-default to v6.3.13 (#7895) (fa251ece) + * update dependency nodebb-plugin-mentions to v2.7.1 (8c2fc577) + * update dependency nodebb-plugin-composer-default to v6.3.12 (#7889) (429e124f) + * update dependency nodebb-plugin-composer-default to v6.3.11 (#7888) (516b62ea) + * update dependency nodebb-plugin-composer-default to v6.3.10 (6a4a9e26) + * update dependency nodebb-plugin-dbsearch to v4.0.4 (#7883) (12ba589d) + * update dependency postcss to v7.0.18 (c75f3d27) + * update dependency nodebb-theme-persona to v10.1.17 (317c6771) + * update dependency nodebb-theme-vanilla to v11.1.8 (6e9caddc) + * update dependency nodebb-plugin-composer-default to v6.3.9 (effe3989) + * update dependency nodebb-plugin-mentions to v2.7.0 (b69769bb) + * update dependency benchpressjs to v2 (#7876) (9149db1f) + * update dependency nodebb-plugin-composer-default to v6.3.8 (793c5eaa) + * update dependency nodebb-plugin-composer-default to v6.3.7 (57de99d2) + * update dependency mongodb to v3.3.2 (#7871) (4ee2c090) + * update dependency nodebb-plugin-spam-be-gone to v0.6.5 (#7865) (8ce2a5fc) + * update dependency mongodb to v3.3.1 (#7862) (0dfce49f) + * update dependency connect-pg-simple to v6 [security] (#7864) (e4b5d0b4) + * update dependency nodebb-plugin-mentions to v2.6.1 (a1210985) + * update dependency nodebb-plugin-mentions to v2.6.0 (cdccc646) + * update dependency rimraf to v3 (#7843) (2f02edbc) + * update dependency rimraf to v2.7.1 (#7838) (cf2504f8) + * update dependency nodebb-theme-persona to v10.1.16 (#7848) (ca7ba9be) + * update dependency nodebb-theme-persona to v10.1.15 (#7845) (b1c0beb7) + * update dependency nodebb-theme-persona to v10.1.14 (#7837) (661284cf) + * update dependency nodebb-theme-persona to v10.1.13 (eaea6f63) + * update dependency sitemap to v4 (d3d677da) + * update dependency commander to v3 (7a5dbd9f) + * update dependency mongodb to v3.3.0 (360e172e) + * update dependency nodebb-plugin-markdown to v8.10.4 (61f9be99) + * update dependency nodebb-plugin-markdown to v8.10.3 (d860f8c7) + * update dependency sharp to v0.23.0 (#7806) (2258452c) + * update dependency nodebb-theme-persona to v10.1.12 (#7799) (b9aac424) + * update dependency nodebb-plugin-dbsearch to v4 (#7797) (d98313b5) + * update dependency nodebb-plugin-dbsearch to v3.0.8 (#7796) (ef734b62) + * update dependency nodebb-theme-persona to v10.1.11 (8b1fc5c8) + * update dependency nodebb-theme-vanilla to v11.1.7 (ba1bb528) + * update dependency nodebb-theme-vanilla to v11.1.6 (#7784) (7a88c4cc) + * update dependency nodebb-theme-slick to v1.2.26 (#7782) (e074b0bf) + * update dependency nodebb-theme-persona to v10.1.10 (#7781) (4f20b4d3) + * update dependency nodebb-plugin-composer-default to v6.3.6 (3fdc638b) + * update dependency connect-redis to v3.4.2 (8a11193f) + * update dependency nodebb-plugin-mentions to v2.5.4 (45223cde) + * update dependency nodebb-plugin-markdown to v8.10.2 (86546232) + * update dependency nodebb-theme-persona to v10.1.9 (#7759) (c1660a1a) + * update dependency nodebb-plugin-dbsearch to v3.0.7 (#7758) (efedd621) + * update dependency nodebb-plugin-composer-default to v6.3.5 (7f4b1043) + * update dependency nodebb-theme-vanilla to v11.1.5 (#7756) (15e01d12) + * update dependency nodebb-theme-persona to v10.1.8 (#7755) (19679608) + * update dependency nodebb-theme-persona to v10.1.7 (#7754) (3cb6cfe8) + * update dependency nodebb-plugin-composer-default to v6.3.3 (8394a0fd) + * update dependency nodebb-theme-vanilla to v11.1.4 (#7741) (43ce5f8a) + * update dependency nodebb-theme-persona to v10.1.5 (27da2325) + * update dependency validator to v11.1.0 (#7738) (bd09ba92) + * bump composer-default (851424a7) + * update dependency nodebb-theme-persona to v10.1.4 (f426e105) + * update dependency nodebb-plugin-composer-default to v6.2.16 (#7723) (fb087029) + * update dependency nodebb-theme-persona to v10.1.3 (413259a2) + * update dependency nodebb-theme-lavender to v5.0.11 (#7705) (208c821e) + * update dependency nodebb-theme-vanilla to v11.1.3 (#7714) (e0b2ae8d) + * update dependency nodebb-theme-slick to v1.2.25 (#7713) (af7ecd2f) + * update dependency nodebb-theme-persona to v10.1.2 (#7712) (e5733f40) + * update dependency nodebb-plugin-mentions to v2.5.3 (26ff02dc) + * update dependency nodebb-theme-persona to v10.1.1 (4e513cf3) + * update dependency nodebb-theme-vanilla to v11.1.2 (c2887505) + * update dependency nodebb-theme-vanilla to v11.1.1 (#7700) (b3ed89f5) + * update dependency nodebb-theme-slick to v1.2.24 (#7699) (629b5ce3) + * update to eslint@6 (289dada5) + * update dependency nodebb-plugin-emoji to v3 (15020b46) + * update dependency nodebb-plugin-emoji to v3 (6b43d26f) + * update dependency nodebb-theme-persona to v10.1.0 (fc89516e) + * update dependency nodebb-theme-vanilla to v11.1.0 (c9689f11) + * update dependency nodebb-theme-persona to v10.0.1 (#7687) (ada8f22e) + * update dependency nodebb-theme-vanilla to v11.0.1 (#7688) (6db3604e) + * update dependency nodebb-theme-vanilla to v11 (89fea9d3) + * update dependency nodebb-theme-persona to v10 (0a7778bd) + * update dependency nodebb-theme-lavender to v5.0.10 (#7682) (e1e4abeb) + * update dependency nodebb-plugin-markdown to v8.10.0 (de046297) + * update dependency postcss to v7.0.17 (ae891390) + * update dependency nodebb-rewards-essentials to v0.0.14 (#7671) (3f4f8afa) + * update dependency mongodb to v3.2.7 (#7665) (988cbb63) + * update dependency nodebb-plugin-composer-default to v6.2.15 (e21246a9) + * update dependency nodebb-theme-persona to v9.1.38 (#7652) (d202be5b) + * update dependency nodebb-theme-vanilla to v10.1.34 (#7653) (e3308659) + * update dependency nodebb-theme-vanilla to v10.1.33 (#7651) (6f70397b) + * update dependency nodebb-theme-persona to v9.1.37 (#7650) (d43bdb41) + * update dependency async to v3.0.1 (#7649) (3b8e6e15) + * update dependency mongodb to v3.2.6 (#7616) (3f21096d) + * update dependency validator to v11 (a3d42404) + * update dependency nodebb-plugin-composer-default to v6.2.14 (#7635) (b174c2d2) + * update dependency nodebb-theme-vanilla to v10.1.32 (#7631) (85ca5b19) + * update dependency nodebb-plugin-composer-default to v6.2.13 (d1f29e26) + * update dependency nodebb-plugin-spam-be-gone to v0.6.3 (#7614) (20fa751c) +* crash if post doesn't have topic data (eabc6f47) +* don't touch objects that don't have the required fields (e52ecfaf) +* add missing await to sendValidationEmail (f4381ba3) +* #8007 added tfoot to clear new group button (dee1d447) +* tests (e3c9dafa) +* #8018, allow absolute urls in notification.path (0037a038) +* #8010, fix isBanned call (59242d31) +* #8003, check children recursively (c4e58160) +* dont show delete topics on unread (b91c4790) +* #8000, tweak wording; password reset success (3952849f) +* parseInt cutoff (0e5e47f4) +* lint (4b8897d9) +* convert param to string in slugify (441dd86d) +* cli/reset.js (#7979) (f9f85fc4) +* #7977, fix undefined url (#7978) (95a372df) +* passwords always expiring upon login (ddf3812c) +* #7974 (#7976) (ca3be1f3) +* delete follower/followingCount as well (d72b0d16) +* event tests (0da4f7ee) +* #7958, give rewards one by one (3775301f) +* rewards page acp not loading rewards (88818a5b) +* #7929, use fixed bootswatch (dd202931) +* #7960, dont try to save size for non images (f272daaf) +* #7941, validate some input fields (565f9726) +* don't show deleted topics on unread (661a0f50) +* #7951, don't send notification for system messages (c01b0fbd) +* #7953, allow icon only group badges (5b458fc7) +* remove 0 fields (ac4a5004) +* remove unused data from post/topic/user hashes (75bcb0f4) +* groups not visible on widget panes (205d3f9d) +* new nav items will show group select (c5f7b66c) +* move post and change owner (f402d727) +* 404 on new groups with spaces (b19f0a68) +* #7940, run upgrade scripts on startup (7823144b) +* #7949, delete old user notifs (38322ec3) +* tests (6fb29e84) +* check threshold before setting bookmark (f983f536) +* mongo collection stats (4e28e575) +* #7947, disable unused cookie (91e8e390) +* #7945, show watched categories in ignored categories (c9bf6d0f) +* db.init calls (18bf865d) +* #7938, escape username in registration queue (d5dda26f) +* #7901, handle group names that are translation keys (3455c27a) +* fix the order of groups on user profile (41f8da2e) +* #7935, ACP digest wording (54c9f877) +* #7934, return tids (10f168b8) +* tests (3caa387a) +* tests (9c051386) +* remove jquery (95ab0712) +* picture not showing up in change modal (13c87059) +* profile page meta tags getting escaped twice (7effc892) +* missing await (2150701f) +* potential for XSS here (40f131a6) +* account/categories (7a0f6074) +* redis tests (1f054c7e) +* #7921, dont create wrong entries in db for flag (a651d154) +* #7913, dont allow urls in fullname/location, validate birthday (babb9d7c) +* upgrade scripts (bd026cb1) +* only allow png/jpg/bmp in cover/profile images (96ab8d05) +* #7919, add useUnifiedTopology (910b9b88) +* crash in feed (873ec519) +* lang key (795d4183) +* time cutoff and lang string (5b8550f0) +* returned data (87f6ac59) +* dont send all category data back to recent&unread etc (955e00fd) +* #7912 (ecf39727) +* only return necessary data for categories (307abaa8) +* dont crash if callback is undefined (e0fec866) +* #7800, dont crash on resize (781b3f1a) +* change params (de461968) +* remove dupe code (9b151b23) +* #7894, translate log in to view (2a5fe2b8) +* minor typo (b99279a8) +* #7893, check private upload setting (0843497d) +* #7892, handle string 'true'/'false' (77cb4b55) +* dont show deleted topics in digest (4652c68b) +* #7824, update timeago settings on userlanguage change (3887fc67) +* don't move theme/plugin to top if its already active (acd95764) +* #7853, dissociation on post purge (30a86ed5) +* added comment back (0c4cd840) +* inability for plugins to actually alter parser sanitization config (fe452762) +* htmlentity instead of url encoding for room rename system message (9b8ac89e) +* page count for filters (e291a609) +* #7866, show correct title based on digest interval (547bb496) +* #7780 (21e81f96) +* pinned tids showing on page > 1 (f2912e9f) +* #7860, allow running single upgrade scripts from plugins (a4dec7e3) +* reset should not automatically build assets (a67762bc) +* update morgan and lodash (2b1912a9) +* #7842, groups.invite works with an array of uids (1e0190ab) +* #7844, add uid to events, log plugin install/uninstall events (592d9c82) +* removed console.logs (c44bf48c) +* broken tests from 71b205a889da1ee8dd326b0891d122a522084f54 (f736f0b3) +* typo (c6c13725) +* category description getting translated in ACP (647713f3) +* #7791 (71620519) +* #7831, fix pagination (014e3153) +* remove empty line (292bbe34) +* remove useless catchs and empty line (5fce4558) +* #7823, fix topic move readding pids when topic is deleted (fd5f9822) +* #7826, fix order of categories on recent dropdown (3ecac97d) +* redis pubsub not being required correctly (8d4f2086) +* mongo.close using wrong client (186321e6) +* digest not sending topics (bc6f22eb) +* #7816, adding GDPR and TOU interstitials earlier on route reloading (52a2e5d6) +* #7809, dont check postDelay if submitting from queue (9780f5b9) +* newPostEditDuration (2abe244b) +* #7789 (6a289fba) +* #7798, fix nested post selection (666e0eaf) +* #7788, fix on topic move and new post (8bf40d04) +* #7788, fix another edge case (053ff510) +* topicData passed to action:topic.edit (b10ad7b6) +* derpy catch (ce912886) +* #7788 No new posts (#7793) (3c32d860) +* user urls in subfolder install (6a486e35) +* #7765 (0b498acd) +* tests (87552c55) +* revert requires (3dd806dc) +* #7763, parse about me on all profile pages (f8d34101) +* status and groupTitle, if its not set (e2e33dfb) +* don't return promise (2f0a331f) +* socket.io methods calling callbacks twice if method returns promise (44a33520) +* tests (87b1148f) +* tests (930ffd07) +* #7601 removed check for another user when sending messages (0e8ee31b) +* mongodb tests (3b24de4c) +* getTopicsFromSet (13aaf07b) +* #7762, allow array for isBanned (9eb1fcd4) +* add flag for change post owner (46639be3) +* sitemap ajaxify (9f78bd7a) +* #7751, make necro separator a partial (2b70e86f) +* #7748, if reputation system is disabled, dont check min reputation (7b95ebbb) +* remove left over code, use proper names (0ac49d63) +* compatibility conditional (fba67196) +* private upload img replacement should happen before img wrapping (16aae517) +* #7423 private uploads are linked to login page, for guests (49e3a368) +* typo (f3440ebf) +* #7722, hsts deprecation warnings (8b1dadb7) +* upgrade script #7720 (accf48f3) +* indents (a5de54f8) +* remove debug code that should not have made it in (af17c6e3) +* dont display post tools if there are no options (708df46e) +* output span in buildAvatar helper instead of div (69fae1a7) +* restore uid in teasers userObj (67b8cb89) +* dont allow edit if post is deleted by someone else (667bc67f) +* dont show delete posts on profile (e48c7cd7) +* on new post add it to cid::uid::pids (6a7bc1c5) +* #5570, create per category user post zsets (a39f0ef5) +* restrict drag drop to pin icon, closes #7702 (e1c05e59) +* remove dupe code (68e5d7ad) +* get uids directly for csv (39ee3980) +* typo (a251032f) +* id/for (b4cc8d88) +* #7659 add option to change cookie link url (25ea6347) +* middle vertical-align for .avatar elements (1894cd9b) +* dependency checker always triggering dep upgrade, hopefully (c241551d) +* #7483, show latest undeleted message as teaser (644504ff) +* #7567, allow invite and approval at the same time (4b843ba1) +* #7625, on group rename update nav/widget items (5b85ed31) +* psql error (8d319e42) +* try to fix psql :dog: (2e6b562f) +* no need for moderate bit for global mods and admins (8aef689f) +* attribute name so it works for both templates (ebb32e78) +* #7647, fix getModeratorUids (64679b37) +* #7644 (5cd9e1bf) +* bad usage of async requisition (603c5262) +* 7638, returnTo accidentally saved into user hash (f321b426) +* #7634, make strip exif configurable (157b921e) +* #7636, use reputation threshold for post queue (a8409fbd) +* #7623, wait for lastonline to update on /users (63e5d383) +* https://github.com/barisusakli/nodebb-plugin-dbsearch/issues/53 (e65ddc98) +* #7593, unable to set account password if no password set (80c0d579) +* incorrect pathname format for app.previousUrl (a97e97b9) +* #7598, use notice element instead of alert (106d52a4) +* #7620, allow adding new fields into user objects for posts (a8e65205) +* #7629, new filter hook for room rename (7ba4a864) +* #7628, log email send errors (7a4b68e8) +* {username} not working in notification.tpl (f55cc667) +* do not prevent nbb from starting (3b0459a0) +* #7624, default open state on stateless flags (91f0bce6) +* #7431, add /unread link to topic route (15391da5) +* #7619 (35d10f76) +* #7618 save pathname into app.previousUrl ins. of entire url (7e5cb72a) +* handle missing timestamps (172e6888) +* broken test for meta tags on ajaxify (b70e03a7) +* #7613 (15ce23da) +* #7600, removing nbsp from email CTAs (#7606) (f552cea5) +* #7604 shortened CTA text and added custom text for some notifs (#7605) (b32da57f) +* **style:** + * requiring parens in block bodies (29f96b19) + * updated code to follow new eslint recommendations (09212309) + +##### Refactors + +* use arrow function (6b3eb014) +* async/await (02d38caf) +* remove log, topics.exists (f8c8038a) +* async/await (dc8d721c) +* async/await mongo (1f0c6f3d) +* async/await socket.io/index (603908c1) +* async/await image.js (c5ffd8cc) +* remove var (05e753c7) +* async/await flags.js (ac6eb31c) +* async/await flags (0ced71be) +* shorter map (572bc297) +* remove logs (03971049) +* dont modify key (7e7ea7a6) +* remove logs (e32a5546) +* async/await flags (9ee1a882) +* shorter, new Date doesnt throw (625b0815) +* remove temp var (b842057f) +* async/await file (f9d6912b) +* async/await analytics (223c108c) +* change to const (5505628c) +* returned fields (86b16629) +* async/await emailer (63bd3fc5) +* async/await routes/feeds (ec3b5dd9) +* async/await rewards (b110aec6) +* async/await socket.io (a7d2b8a1) +* async/await (52b2d670) +* async/await (75d7ae92) +* async/await socket.io/topics (5c2afe5e) +* async/await controllers/user.js (3c6c40b1) +* async/await uploads (5c0266d8) +* move tos parsing to /tos (3e2ed21d) +* remove commented out code (ec98945a) +* async/await controllers/authentication (b9105ef9) +* remove async from isPasswordValid, function is sync (22f80116) +* make categories.buildForSelectCategories non async (6cda3698) +* remove dupe code (a4d84a66) +* async/await socket.io/posts (e93ef0d7) +* async/await socket.io/admin (88dfbf21) +* socket.io/admin/categories async/await (71e50bbf) +* admin groups.join socket async/await (4588a4fd) +* password async/await (dd8386d9) +* sitemap to async/await (0164e51f) +* remove hook assign (d3727207) +* async/await users (f9a804e0) +* remove empty line (19b63bfa) +* remove vars (69333f59) +* async/await controllers (2c4f0446) +* async/await controllers/tags (53624885) +* async/await (f15c7f12) +* globalmods/groups (0722cc47) +* controllers/category.js (bd8736db) +* use arrow func (27c27b49) +* async/await controllers/api (4eaa630b) +* async/await admin/controllers (6f375482) +* async/await, remove dupe code for homepage routes (c9250a01) +* async/await for src/socket.io/groups.js (71b205a8) +* rewrote SocketPosts.getRawPost in await style (08530bb3) +* replace avatar conditional code with buildAvatar helper (#7681) (25238899) + +##### Reverts + +* accidental removal of some needed translation source strings (868dedd1) + +#### 1.12.2 (2019-05-15) + +##### Chores + +* incrementing version number - v1.12.2 (22db818e) +* bump themes #7576 (d349754d) +* bump themes (59bdc970) +* bump themes (abcca134) +* bump themes (551b18cd) +* incrementing version number - v1.12.1 (dd973abe) +* **deps:** + * update dependency lint-staged to v8.1.7 (dc6b49ca) + * update commitlint monorepo (9998e86b) + * update dependency jsdom to v15.1.0 (fcd6dc88) + * update dependency mocha to v6.1.4 (3ff8154b) + * update node:8.16.0 docker digest to b5484d1 (6421f10f) + * update dependency husky to v2.3.0 (c20e3313) + * update dependency nyc to v14.1.1 (#7584) (9047210c) + * update dependency eslint to v5.16.0 (692e2ead) + * update dependency nyc to v14 (9210baf5) + * update dependency husky to v2.2.0 (8a018a5f) + * update dependency lint-staged to v8.1.6 (4e39caf8) + * update dependency husky to v2.1.0 (65ff0bbd) + * update dependency husky to v2 (e81a1dbb) + * update dependency eslint-plugin-import to v2.17.2 (#7546) (c1fb17f9) + * update dependency jsdom to v15 (#7556) (d4d8d98f) + * update dependency jsdom to v14.1.0 (#7555) (ca694fd1) + * update node.js to v8.16.0 (#7554) (f10708e7) + * update dependency eslint-plugin-import to v2.17.1 (69dd8e4d) + * update dependency mocha to v6.1.2 (b7169772) + +##### Documentation Changes + +* updated changelog (93b688d0) + +##### New Features + +* let theme know downvoting is disabled, closes https://github.com/NodeBB/NodeBB/pull/7568 (bd94fbc2) +* closes #7583 (cf5aeace) +* #7319 (9385c8e3) +* add node12 to travis (1a7036a6) +* allow file uploading on registration interstitial (ddffc904) +* #7527 (ba5e1eaa) +* #7515 (c38db4f7) + +##### Bug Fixes + +* #7599 image size measurement erroring out on missing path (0d86781c) +* #7590 updated chat and post edit and delete timeout labels in ACP (4f0dc443) +* tests (3a7e99a5) +* #7586, switchTimeagoLanguage shouldn't discriminate against languages w/o shorthands (1703233f) +* #7576 "Disable password changes" can be sidestepped (50260e13) +* if editing password is disabled in ACP, prevent direct access via route/socket (related: #7576) (e114b16d) +* #7582 (c9ca72d0) +* #7461 (96cb29aa) +* increase batch size (3d938e7b) +* #7564 (bf6fc502) +* group cover upload not working for s3 upload (#7571) (8945ebcb) +* test (b9903120) +* #7539 (c940a733) +* #7565 (07e9b67e) +* #7464 (32cf07d7) +* #7147 (fe6d64cc) +* #7424 (f86d74d8) +* #7562 (09681e6c) +* node12 tests (8775e7e6) +* add post queue to /compose POST route (c6cd6c57) +* remove redis object cache (4df925e7) +* #7545 (74038849) +* failing test from 00552d7183f0416a0caa113fe2f1e658659648f7 (9bf3517d) +* fire filter:register.complete for users approved via registration queue (00552d71) +* #7540 (8778f00b) +* more graceful error handling and output for cli/reset (d3ebda73) +* #6438 only apply whitelist when fields request empty (#7528) (808c4909) +* **deps:** + * update dependency nodebb-theme-vanilla to v10.1.31 (#7589) (a9f9d19b) + * update dependency nodebb-theme-persona to v9.1.36 (#7588) (bd86e58d) + * update dependency mongodb to v3.2.4 (#7581) (26d6d0be) + * update dependency postcss to v7.0.16 (10a47a29) + * update dependency nodebb-theme-persona to v9.1.33 (#7563) (7c4201f2) + * update dependency sharp to v0.22.1 (#7561) (288a25f0) + * update dependency nodebb-plugin-composer-default to v6.2.12 (#7538) (a54f8f00) + * update dependency mongodb to v3.2.3 (97a7f02f) + +#### 1.12.1 (2019-04-10) + +##### Chores + +* incrementing version number - v1.12.1 (dd973abe) +* comment cleanup (6bed51ef) +* bump persona (1f4db132) +* bump themes, fix: https://github.com/NodeBB/NodeBB/issues/7446 (0d19bb3d) +* incrementing version number - v1.12.0 (d87f8c52) +* **deps:** + * update dependency lint-staged to v8.1.5 (ae17481d) + * update node:8.15.1 docker digest to 918f0be (5f787d73) + * update node:8.15.1 docker digest to 3d43ad1 (ea77ad4e) + * update dependency grunt to v1.0.4 (a9130fad) + * update dependency eslint to v5.15.3 (d2932cf6) + * update dependency mocha to v6.0.2 (#7408) (1c671c32) + * update dependency eslint to v5.15.1 (d8486e88) + * update node:8.15.1 docker digest to c151597 (2cb0bdea) + * update dependency jsdom to v14 (a967253a) + +##### Documentation Changes + +* updating changelog (9aeb291e) + +##### New Features + +* #7515 (ac889db1) +* update unban logic/invocation and refactor User.bans module (3fbb6faf) +* add original sessionID to static:user.loggedOut (abe4abb6) +* #7501 (2ee2cd52) +* allow multiple cids for getActiveUsers (81306fff) +* manual password expiry. closes #7471 (24dcae21) +* #7358 (7211dde7) + +##### Bug Fixes + +* don't crash if templateData is undefined (eb2c3e56) +* clear children before building tree (397e062a) +* upgrade script date (4f3ca4d8) +* #7519 (ed91d3f2) +* #7517 (e479fad7) +* #7470 Properly handle recompilation of email templates (75ae29c1) +* resolve CORS error on uploading via URL (3871a02c) +* #7461 (dbc4d840) +* #7491 (8cc1864a) +* #7476 (4db0efe3) +* #7508 (b52dba5c) +* escape search snippet closes #7506 (e906bea4) +* #7490 once again (3ec9fc40) +* #7502 (3304ffe9) +* #7503 (1f3554ff) +* #7493, adding robots noindex meta tag to compose and reset routes (6415ba82) +* #7490, handling of image sizes if no size saved in db (86e1cdd5) +* #7494 (8f55ab13) +* #6911, email throttling options set (63e16ec0) +* throttling email output to 2 messages every second, fixes #6911 (33a1bf6c) +* #7469 missing email logos, occasionally (8d62ead3) +* #7487 (fddb783e) +* #7485 (78e0b983) +* move upload_url to prestart so it is defined in upgrade scripts (ad2d7a69) +* #7342 (cead89f0) +* #7477 (240f563a) +* #7473 (c289b904) +* #7378 (c37b2f2e) +* #7454 (dcdd41c7) +* #7472 (962b7f73) +* remove async.series, dont crash if postAtIndex is undefined (dd8d4f20) +* tests (c12e1d19) +* #7468 (b4a9bb10) +* #7444 Re-factor handling of og:image tags (#7463) (697a6597) +* tests (45d7e37a) +* callbacks (b48b6d75) +* return early if keys is empty (7addc5a0) +* return early if keys is empty (b3962c16) +* prevent infinite loops if category is already in children (b71e0eb1) +* #7448 (4732bed3) +* return early if there are not blocked uids (73d14e45) +* #7008 (756dfd8a) +* typo (f9d92db5) +* tests (e71ae80c) +* #7406 (cbfbcee5) +* #7432 (777ca032) +* change user/system cpu usage (aecdf739) +* strip exif data on image uploads (5eaa14e7) +* **deps:** + * update dependency nodebb-theme-persona to v9.1.29 (e5c12a49) + * update dependency nodebb-plugin-composer-default to v6.2.11 (30b042b8) + * update dependency nodebb-plugin-composer-default to v6.2.10 (cf180e09) + * update dependency html-to-text to v5 (b39ce25c) + * update dependency semver to v6 (8662aa95) + * update dependency spdx-license-list to v6 (447cf66b) + * update dependency nodebb-widget-essentials to v4.0.17 (#7489) (0f9cb86d) + * update dependency mongodb to v3.2.2 (#7482) (357f931c) + * update dependency nodemailer to v6 (0274c2bd) + * update dependency sharp to v0.22.0 (#7466) (c3261415) + * update dependency nodebb-theme-slick to v1.2.23 (#7460) (c20cd528) + * update dependency nodebb-theme-vanilla to v10.1.25 (fe30e06d) + * update dependency nodebb-theme-persona to v9.1.27 (5c605392) + * update dependency connect-redis to v3.4.1 (02804fe9) + * update dependency nodebb-plugin-composer-default to v6.2.9 (#7442) (80293146) + * update dependency nodebb-theme-vanilla to v10.1.23 (#7438) (48dd3c5b) + * update dependency nodebb-theme-slick to v1.2.22 (#7437) (3f4ae4b0) + * update dependency nodebb-theme-persona to v9.1.25 (#7436) (9887bb4f) + * update dependency nodebb-plugin-composer-default to v6.2.8 (#7435) (c18f2e1f) + +### 1.12.0 (2019-03-06) + +##### Chores + +* incrementing version number - v1.12.0 (d87f8c52) +* incrementing version number - v1.11.2 (757bff27) +* **deps:** + * update node:8.15.1 docker digest to 287b8a5 (0b8d1833) + * update node.js to v8.15.1 (ae89db28) + * update dependency mocha to v6 (#7387) (30bdb1a1) + * update dependency coveralls to v3.0.3 (#7397) (cf74904c) + * update dependency eslint to v5.14.1 (4df9c206) + * update dependency jsdom to v13.2.0 (cb5e3d83) + * update node:8.15.0 docker digest to a8a9d8e (edcb5314) + * update dependency nyc to v13.3.0 (a78f5da5) + * update dependency lint-staged to v8.1.4 (18c90913) + * update dependency eslint to v5.14.0 (9d8d2d0d) + * update commitlint monorepo (aed5b29d) + * update dependency eslint-plugin-import to v2.16.0 (e0fa6965) + * update dependency @commitlint/cli to v7.4.0 (362d9397) + * update dependency lint-staged to v8.1.1 (96644350) + +##### Documentation Changes + +* updated changelog (aec2f210) + +##### New Features + +* admin/groups style change (2b6f1a05) +* add process cpu usage to admin (db477538) +* pass options to digest (23da3009) +* revamp email templates to be more style agnostic (#7375) (f32a9922) +* lower search timeout (fc830c0f) +* quick search (8a0e1280) +* add vote status to getPostData API call (eafe76de) +* make topic search a function (d2b83967) +* quick search (c01d43e0) +* check overide (ba90bf31) +* check CI failure (f2d7f75e) +* logging password resets and errors into event log (0c09b740) +* add `action:alert.new`, `action:alert.update` hooks (daadcc48) +* allow themes to define custom classes for categories via filter:admin.category.get (5031bfe8) +* show more unread notifs (d75a0d77) +* pass topic creation data to action:topic.save (cd2f72fb) +* textcomplete over jquery-textcomplete, closes #7309 (02a8ed9b) +* new hook filter:privileges.posts.edit (f659ef4d) +* new hook type: `response` (a23854e3) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-theme-persona to v9.1.24 (0f2d3181) + * update dependency nodebb-plugin-composer-default to v6.2.7 (#7411) (9ebc900a) + * bump markdown (82c8ee3f) + * bump markdown (7ce5a81f) + * bump persona (a0b71f09) + * update dependency nodebb-theme-vanilla to v10.1.21 (#7404) (699eddcb) + * update dependency nodebb-theme-persona to v9.1.22 (#7403) (d87751ea) + * update dependency nodebb-theme-persona to v9.1.21 (#7401) (f721155d) + * update dependency nodebb-theme-slick to v1.2.21 (#7398) (f6e55651) + * update dependency nodebb-widget-essentials to v4.0.14 (#7399) (f812e0b7) + * update dependency nodebb-theme-persona to v9.1.19 (#7392) (deff7b34) + * update dependency nodebb-plugin-composer-default to v6.2.6 (#7389) (fab32a49) + * update dependency nodebb-theme-persona to v9.1.18 (042b81a0) + * update dependency nodebb-theme-persona to v9.1.17 (#7379) (546e04e1) + * update dependency nodebb-plugin-composer-default to v6.2.5 (#7374) (d0874f34) + * update dependency async to v2.6.2 (#7365) (264eadde) + * update dependency nodebb-plugin-composer-default to v6.2.4 (30ff4582) + * update dependency nodebb-theme-persona to v9.1.16 (65df6711) + * update dependency nodebb-plugin-markdown to v8.9.0 (07b29d59) + * bump contenteditable to v0.1.1, #7325 (fbbe2ab7) + * update dependency nodebb-plugin-composer-default to v6.2.3 (#7341) (176efb5f) + * update dependency nodebb-theme-persona to v9.1.15 (#7295) (8f69ffd4) + * update dependency nodebb-theme-slick to v1.2.20 (#7197) (a82bd3d0) + * update dependency nodebb-theme-lavender to v5.0.9 (#7322) (b350be27) + * update dependency nodebb-plugin-composer-default to v6.2.2 (99f82fb3) + * update dependency winston to v3.2.1 (#7317) (7e872d32) + * update dependency nodebb-plugin-markdown to v8.8.8 (#7314) (9cf81afe) + * update dependency nodebb-plugin-dbsearch to v3.0.6 (#7305) (d48ef6d8) + * update dependency nodebb-plugin-dbsearch to v3.0.5 (#7304) (57e3f162) + * update dependency nodebb-theme-vanilla to v10.1.19 (#7301) (443dcee4) + * update dependency winston to v3.2.0 (#7299) (6f957cb9) + * update dependency nodebb-plugin-spam-be-gone to v0.6.0 (247ac828) +* #7413 (684175f7) +* #7414 (e331f6b5) +* l2spread @baris nib (0360f6e1) +* restored email logo to all outgoing emails (6c1f9c3c) +* die hydra!!! (ad6c8dcc) +* incorrect teaserPost logic (97313508) +* normalized font sizes for paragraphs in digest (43c4eb23) +* removed notification subheader translation string (6f272e34) +* broken 50% border radius on emails, failing tests (d3a10628) +* hook names getting objectified by winston (6472a065) +* failing tests from revamped-emails branch (3a7f1c19) +* #7395 (03305db8) +* mounting of all-route middlewares to router instead of app (d722f3b8) +* incorrect returnTo set in registerComplete (f972f752) +* search.api not working on subfolder (158f68eb) +* tests for #7385 (7a534453) +* #7385 better handling for errors in Adv>Databases (57069a5c) +* no relative path needed in req.session.returnTo (949b10f1) +* don't refresh page when enabling/disabling categories (d5ece9a4) +* #7373 (c2e7ae7f) +* autocomplete not triggered if chat switched (ab0e547d) +* #7366 (6c2f48f1) +* #7357 (45c322ae) +* #7359 (2996a5dc) +* re-introducing indeterminate checkbox state to ACP privs (e8f3c256) +* #7354 (c6ad8fae) +* uid check (efd7d953) +* uid checks (c24dcf78) +* only allow numbers as scores (#7356) (5917dec2) +* #7231, missing success alert on group name change in ACP (0fffcb38) +* remove debug logs (fe63fca8) +* don't use same db as prod values (39e35275) +* database 0 was being replaced with undefined because 0 is falsy (bcd62586) +* #7352 (2e2c3ac1) +* #7261, banned users still get digests (aca05560) +* #7346, panel-header widget colours in ACP (91a7b907) +* #7350 (8c68780e) +* eslint failure from daadcc4889a91b9bbd279d49db348610cc079ccc (96b6ce1f) +* prevent crash if results.stats or results.serverStatus is undefined (fab52b84) +* move `action:alert.update` to after translator (d864da1a) +* #7098 (fc6767e1) +* #7232 (70d9c6c4) +* #7229 (e59b907d) +* #7339 (12c58990) +* #7338 (8e3bf581) +* dont crash if userData is undefined #7262 (56a493d8) +* #7240 (a2f08e7d) +* give default privs on new install to guests/spiders (cd120990) +* #6806 (c72da559) +* #7334 (ae779ea4) +* closes #7217 (9d1fcf4e) +* merge post notifs (6e69a9ab) +* don't crash if body doesn't have `skin-` (f6cfbbb5) +* #7324 (c7abf07a) +* allow regular groups to local login (0366cfd8) +* #7316 (5353960a) +* #7306 (8aebe884) +* #7312 (7a4a8ea4) +* #7311, missing dependencies (263e58df) +* #7300 (920efad0) +* #6848 (70f8b418) +* #7298 privilege header misalignment (df6f75eb) +* #7211 (cf918078) +* #7215 (7a9780f6) +* #7296, local login privilege available to registered-users only (7fb3c168) +* #7094 (d03220cd) + +#### 1.11.2 (2019-01-24) + +##### Chores + +* incrementing version number - v1.11.2 (757bff27) +* incrementing version number - v1.11.1 (2104877c) +* **deps:** + * update node:8.15.0 docker digest to cb66110 (1719cd77) + * update dependency eslint-plugin-import to v2.15.0 (f7191eb9) + * update dependency eslint to v5.12.1 (d928c54d) + * update dependency @commitlint/cli to v7.3.2 (6ae2b972) + * update node.js to v8.15.0 (ac39fe90) + * update dependency eslint to v5.12.0 (f96ef7bb) + * update commitlint monorepo to v7.3.1 (50594118) + * update dependency smtp-server to v3.5.0 (00063708) + * update dependency husky to v1.3.1 (719995a4) + * update dependency eslint to v5.11.0 (#7151) (26f3bdbf) + * update dependency husky to v1.3.0 (eb606281) + * update dependency jsdom to v13.1.0 (60e9430b) + * update dependency eslint to v5.10.0 (#7084) (dae861da) + * update dependency husky to v1.2.1 (63f4b569) + * update node:8.14.0 docker digest to dd2381f (7449ae3e) + * update node.js to v8.14.0 (8a5a031d) + * update dependency lint-staged to v8.1.0 (dd7f8a14) + * update dependency husky to v1.2.0 (aee21628) + * update node:8.12.0 docker digest to 5dae8ea (0ef451dd) + * update dependency husky to v1.1.4 (95d6ab06) + * update dependency eslint to v5.9.0 (92441794) + * pin dependencies (b0483f21) + * update dependency eslint-config-airbnb-base to v13 (#6599) (64b9dabf) + * update node.js to v8.12.0 (fa3afbd2) + * update dependency husky to v1.1.3 (6cee5b8e) + * update dependency lint-staged to v8.0.4 (9d258668) + * update dependency lint-staged to v8.0.3 (aaa6fe9e) + * update dependency lint-staged to v8 (95d7a5fa) + * update dependency jsdom to v13 (52f141c9) +* **husky:** setting up husky as recommended in docs (e8a3d929) + +##### Documentation Changes + +* updated changelog for v1.11.1 (c04e192d) + +##### New Features + +* new hook filter:user.logout (63061ffd) +* explicit handling of SSO success and failure (059a4be2) +* additional options for SSO plugins (2b9322e1) +* get rid of disk access (ed5d2d6d) +* support for one-click unsubscribe from email clients (#7203) (70a87d43) +* added new hook `static:sockets.validateSession` (#7189) (0263b4da) +* #7120 (f4ea2c43) +* #7032 (0c1ac4d6) +* small fixes (fef7e13c) +* name topic controller (b9b9d8b2) +* header (0cb9bba4) +* more naming (ae0fe5e8) +* give the rest of the middlewares names (f88db22c) +* give names to more middlewares (fdfbcc6e) +* give names to middlewares (53793e16) +* change sortedSetsScore (d2c2d56f) +* Allow getting logfile path from config (#7044) (f3e8e065) +* remove uid::ignored:cids (#7099) (263c9180) +* cache category tag whitelist (78fa7340) +* make user cards look less derpy (31bb2ae9) +* added new middleware authenticateOrGuest (4fba1492) +* closes #7070 (7ca62b83) +* added README.md in languages folder (648964fa) +* up composer (7eee8e1d) +* allow array results (54c127d1) +* #7023 (f581c052) +* close #7002, console message if mismatched origins (89c025d1) +* added changelog file to root of repo (e89b4fca) +* enabling commitlint (c58a41ed) +* allow disabling of GDPR features via ACP toggle, closes #6847 (4919e9ef) +* **deps:** update bootstrap to v3.4.0 (#7106) (d1ea5d15) +* **email:** don't escape html in notification bodies. (#7042) (d7c55bc3) + +##### Bug Fixes + +* test (bc41848a) +* #7235 (7064fd06) +* use ACP config value for checking online status (ef0e7808) +* log error to prevent headers already sent (a22a3a98) +* #7289 timeago shorthand toggle fails on non-existant language (cee47f78) +* #7276 improper request for client-noskin.css (5ee173c2) +* #7274 incorrect handling of client script 404s (831d0795) +* #7270 Flags graph label not translatable (8ceb35f5) +* #7266 body does not contain skin class (f122fc44) +* generate timeago codes from files (7524d3c3) +* removal of timeago fallback middleware (#7259) (c831ff0d) +* post queue notifs (ac655564) +* added missing translation and error state for password change (51b5fb98) +* #7236, header search stops working after header update (3859d417) +* #7226, added placeholder styling for fa-nbb-none (87c2d108) +* escape hook method (9328eeca) +* #7216, hide taskbar on chat modal invocation on mobile (a70db885) +* #7208 (428f587c) +* #7054 (a662f118) +* #7209 (b9833483) +* missing notification (1a3838e1) +* #7193, closes #7194 (7809ba28) +* #https://github.com/barisusakli/nodebb-plugin-dbsearch/issues/49 (6f1fb4eb) +* #7187 (28459d04) +* #7176, FUOC on app.reskin() (954af0f0) +* #7174 (9aa1aa68) +* #7181 (0d409610) +* #7142 (8da3b2a4) +* #7179 (03299736) +* #7169 Fixed logout being broken (b0eaa858) +* #7167, composer and chat not closing on logout (629b3554) +* shorter function (43e7cc0a) +* #7162 (2da0a657) +* uid filtering (72afc180) +* dont crash if default cover is invalid (41fb5cca) +* #7136 socket.disconnect() now called on invalid session (8e9de540) +* RTL not respected when changed in user settings, related to #7146 (4873a339) +* #7146 Better RTL handling on (de-)authentication (d81e0a5f) +* #7118, invoking autoLocale middleware on logout (900f0a0b) +* closes #6784 (#7137) (7fb29f42) +* 7100 (ab81cca7) +* #7139 (3917022a) +* #7116 (7e828404) +* #7138 (29a85aec) +* lint (b47f939b) +* #7091, #7093 (69e0dbbf) +* #7131 (d31684e8) +* remove cache (b2a74b41) +* loop (60390c01) +* #7124 (4650a760) +* unread badge (9f506268) +* move the check to get methods (99e0895e) +* #7115 (989879a6) +* #6979 (29b63ae7) +* upgrade script key (0eef3e1c) +* remove log (00afc5b3) +* #7108 (81697390) +* dont save data for non-positive uids (62f01a83) +* #7103 (f103390a) +* dont update cid::tids:votes if topic is pinned (2f57a4b9) +* #7102 (d117df77) +* #7102 (85a07e99) +* don't explode if there is no css el (74d0e88d) +* db info page (26ccd8f6) +* logAttempt conditional (a6c8e0ab) +* #7087, server-side protection against guest blocks (33d4956b) +* don't crash in flags.validate if user blocked target (81aa3a0b) +* dont send empty strings (555c092f) +* #7085 (fe0f95a2) +* #7086 (e55fb437) +* wrong variable #7085 (71163421) +* admins&mods when there are mutliple lines of users (de437e36) +* refreshing settings page on save if language changed (ed46c5e2) +* not calling authenticate middleware on resource direct access routes (eeaee8cc) +* #7038, autoLocale logic not playing nicely with no-refresh auths (#7059) (5f3d1c76) +* #7074 (2604cf63) +* #7071 buildSkinAsset won't rebuild continuously (a07d9898) +* #7063, logout code should do hard page nav to / or data.next (6df5668e) +* #7061 (eab297bd) +* skin not changing after login or logout, #7038 (28a1fa78) +* #7040 (a63ddbe2) +* #7041 (ec0c50d4) +* #7043 (8d7c3897) +* add missing render function (cb7c2d8c) +* #7033 (8808a033) +* #7037 (b86f1556) +* #6991, add timeout for version Github request (43c3bb02) +* #7030 (58d4376f) +* added admin/manage/uploads to tx config (7357926f) +* #7013, add cache buster to js-enabled.css (f6b92c1d) +* removal of scroll anchoring code in favour of browser handling (98c14e0e) +* custom navigation item not showing groups (d9452bf3) +* flags detail page crash if reporter blocks author (d027207f) +* #6922, skin assets not including plugin LESS files (a5022ce4) +* #6921, allow square brackets in usernames (da10ca08) +* interstitial redirects failing if done via ajaxify (3c8939a8) +* username trim on login, closes #6894 (157bea69) +* **deps:** + * update dependency nodebb-widget-essentials to v4.0.13 (#7293) (22cbcc3e) + * update dependency mongodb to v3.1.13 (1aadbc3c) + * update dependency postcss to v7.0.14 (4d64de76) + * #7271, updating autoprefixer to latest version (a7af0198) + * #7270 (b48f1b4d) + * update dependency sharp to v0.21.3 (#7267) (8a64667f) + * theme upgrades for #7266 (5607261c) + * update dependency mongodb to v3.1.12 (eeab7d20) + * update dependency mongodb to v3.1.11 (#7252) (b5f188b6) + * update dependency validator to v10.11.0 (77dc8fc7) + * update dependency nodebb-plugin-composer-default to v6.1.21 (2fbb2614) + * update dependency postcss to v7.0.12 (f1842295) + * update dependency postcss to v7.0.11 (57bec2fb) + * update dependency sharp to v0.21.2 (8f3c4b09) + * update dependency postcss to v7.0.10 (82475fe5) + * update dependency postcss to v7.0.9 (f171c169) + * update dependency nodebb-theme-vanilla to v10.1.15 (ea059e89) + * update dependency nodebb-theme-persona to v9.1.10 (96482569) + * update dependency nodebb-theme-persona to v9.1.9 (bbe05043) + * update dependency nodebb-theme-vanilla to v10.1.14 (6cc5dbc8) + * update dependency nodebb-theme-persona to v9.1.8 (e5443690) + * update dependency pg-cursor to v2 (29acad42) + * update dependency diff to v4 (#7198) (84e228bb) + * update dependency nodebb-plugin-mentions to v2.5.2 (#7199) (0a647316) + * update dependency nodebb-plugin-markdown to v8.8.7 (90b4d40e) + * update dependency rimraf to v2.6.3 (f4cc3122) + * update dependency spider-detector to v1.0.19 (#7177) (0faba325) + * update dependency nodemailer to v5 (4993b03c) + * update dependency json-2-csv to v3 (80cee665) + * update dependency nodebb-plugin-composer-default to v6.1.20 (07bf0b98) + * update dependency nodebb-theme-persona to v9.1.7 (#7161) (c68d4ae8) + * update dependency nodebb-plugin-composer-default to v6.1.19 (#7159) (07af46ea) + * update dependency nodebb-plugin-composer-default to v6.1.18 (#7158) (584b45fc) + * update dependency validator to v10.10.0 (#7152) (8003bed8) + * update dependency nodebb-plugin-mentions to v2.5.0 (792dce14) + * update dependency nodebb-theme-persona to v9.1.6 (#7141) (325b0293) + * update dependency nodebb-plugin-dbsearch to v3.0.4 (ddd07c1a) + * update dependency nodebb-widget-essentials to v4.0.12 (#7133) (f614a44d) + * update dependency nodebb-plugin-mentions to v2.4.0 (9ab31d7e) + * update dependency postcss to v7.0.7 (7ef8c3fd) + * update dependency sharp to v0.21.1 (#7082) (bf75f3e3) + * update dependency nodebb-theme-vanilla to v10.1.13 (#7114) (fc5598b9) + * update dependency nodebb-theme-slick to v1.2.19 (#7113) (56ad43aa) + * update dependency nodebb-theme-persona to v9.1.5 (#7112) (953f8fe5) + * update dependency nodebb-plugin-composer-default to v6.1.17 (3bcfd7fc) + * update dependency nodebb-theme-persona to v9.1.4 (b6ad5fd4) + * update dependency nodebb-plugin-markdown to v8.8.6 (#7079) (46fb365d) + * update dependency nodebb-theme-persona to v9.1.3 (#7075) (d2aea57a) + * update dependency nodebb-theme-persona to v9.1.2 (42e792ab) + * update dependency nodebb-theme-persona to v9.1.1 (#7069) (bdb33056) + * update dependency postcss to v7.0.6 (6b5428c5) + * update dependency nodebb-plugin-composer-default to v6.1.14 (#7058) (e48ed6e0) + * update dependency nodebb-plugin-composer-default to v6.1.13 (#7057) (ada1d6d0) + * update dependency nodebb-plugin-composer-default to v6.1.12 (#7056) (9f9f72da) + * update dependency nodebb-plugin-composer-default to v6.1.11 (#7055) (89acb896) + * update dependency nodebb-theme-slick to v1.2.18 (#7049) (b6cb77c1) + * update dependency nodebb-theme-slick to v1.2.17 (#7048) (7334c45b) + * update dependency nodebb-theme-slick to v1.2.16 (#7047) (1cb1af0c) + * update dependency connect-mongo to v2.0.3 (#7046) (d0d0c7f0) + * update dependency nodebb-plugin-dbsearch to v3.0.3 (#7035) (adb1b5f3) + * update dependency lru-cache to v4.1.5 (#7031) (887582eb) + * update dependency socket.io to v2.2.0 (b9d49867) + * update dependency socket.io-client to v2.2.0 (824bd541) + * update dependency nodebb-plugin-dbsearch to v3.0.2 (#7028) (11f1b409) + * update dependency nodebb-plugin-dbsearch to v3.0.1 (#7027) (e71f443c) + * update dependency nodebb-theme-vanilla to v10.1.12 (cf928f44) + * update dependency nodebb-theme-persona to v9.1.0 (179be9ed) + * update dependency nodebb-theme-persona to v9.0.63 (#7019) (68ae3eb6) + * update dependency nodebb-plugin-markdown to v8.8.5 (d3ab7d1b) + * update dependency nodebb-theme-persona to v9.0.60 (#6984) (cbd50a80) + * update dependency nodebb-theme-vanilla to v10.1.10 (#6982) (4c769487) + * update dependency nodebb-theme-slick to v1.2.15 (#6981) (acaf1a05) + * update dependency nodebb-theme-persona to v9.0.59 (#6980) (5863bb2c) + * update dependency lru-cache to v4.1.4 (#6977) (375ab769) + * update dependency connect-mongo to v2.0.2 (#6975) (e1597b83) + * update dependency nodebb-plugin-markdown to v8.8.4 (84d1013d) + * update dependency nodebb-plugin-composer-default to v6.1.8 (fee7e336) + * update dependency nodebb-plugin-markdown to v8.8.3 (b182a195) + * update dependency nodebb-plugin-composer-default to v6.1.7 (#6966) (1101f327) + * update dependency nodebb-theme-persona to v9.0.58 (#6964) (6ade156b) + * update dependency mongodb to v3.1.10 (#6962) (662215fa) + * update dependency nodebb-theme-persona to v9.0.57 (#6956) (1bf1a439) + * update dependency nodebb-theme-persona to v9.0.55 (#6955) (e06683f7) + * update dependency nodebb-plugin-composer-default to v6.1.6 (c51ceaf0) + * update dependency nodebb-theme-persona to v9.0.54 (bb940b01) + * update dependency nodebb-plugin-mentions to v2.2.12 (#6936) (e12a803b) + * update dependency nodebb-theme-vanilla to v10.1.9 (#6935) (b480c321) + * update dependency nodebb-theme-slick to v1.2.14 (#6934) (9cdd5316) + * update dependency nodebb-theme-persona to v9.0.53 (#6933) (9ee1c2f8) + * update dependency nodebb-plugin-dbsearch to v2.0.23 (#6931) (dba1db9c) + * update dependency jsesc to v2.5.2 (511b4edc) + * update dependency validator to v10.9.0 (032caafa) + * update dependency spdx-license-list to v5 (a639b6b8) + * update dependency nodebb-theme-vanilla to v10.1.8 (eb0a322d) + * update dependency nodebb-theme-persona to v9.0.52 (6566a0cb) + * update dependency nodebb-plugin-dbsearch to v2.0.22 (#6916) (7808e58c) + * update dependency mongodb to v3.1.9 (#6914) (9a9f2af9) + * update dependency nodebb-theme-persona to v9.0.51 (e2274fe0) + * update dependency nodebb-theme-slick to v1.2.13 (3005428d) + * update dependency nodebb-theme-persona to v9.0.50 (#6902) (22140a20) + * update dependency nodebb-plugin-markdown to v8.8.2 (0b4c9a80) + * update dependency nodebb-theme-vanilla to v10.1.7 (3150a2fc) + * update dependency nodebb-theme-slick to v1.2.12 (#6881) (9bcda7f7) + * update dependency nodebb-theme-persona to v9.0.49 (#6880) (e0dc00da) + * update dependency nodebb-theme-persona to v9.0.48 (2b6f5eec) +* **i18n:** pushed notifications source to tx, pulled fallbacks (8dd8370b) +* **uploads:** ugly filenames on uploaded asset downloading (f96208a0) +* **acp:** + * small UI fixes for ACP privileges category selector (#6946) (57b39d5b) + * hard-to-discover dropdown selector in ACP (b3f96d28) +* **l10n:** some translations (34cbd1fc) + +##### Other Changes + +* //github.com/NodeBB/nodebb-theme-persona/issues/363 (702be3f6) +* //github.com/NodeBB/NodeBB/issues/6433 (7e00d6b9) +* #6408 (f0f30041) +* #6425 (fbf52407) +* //github.com/NodeBB/NodeBB/issues/6073 (5da24b41) +* #5862, setting chat list height even if no message list is present (bc9a1250) +* //github.com/Schamper/nodebb-plugin-poll/issues/86 (c0f39032) + +##### Refactors + +* use loash when possible (#7230) (e1ca2d81) + +##### Code Style Changes + +* lint fix (fbe6ccd7) +* **eslint:** + * match operator-linebreak preferences (ba619c7e) + * newlines in public/src as well (f7bd398e) + * enforcing newline on chained calls (95cc27f1) + +#### 1.11.1 (2018-12-14) + +##### Chores + +* incrementing version number - v1.11.1 (2104877c) +* **deps:** + * update dependency husky to v1.2.1 (63f4b569) + * update node:8.14.0 docker digest to dd2381f (7449ae3e) + * update node.js to v8.14.0 (8a5a031d) + +##### New Features + +* Allow getting logfile path from config (#7044) (f3e8e065) +* remove uid::ignored:cids (#7099) (263c9180) +* cache category tag whitelist (78fa7340) +* make user cards look less derpy (31bb2ae9) +* added new middleware authenticateOrGuest (4fba1492) +* closes #7070 (7ca62b83) +* added README.md in languages folder (648964fa) +* up composer (7eee8e1d) +* allow array results (54c127d1) +* #7023 (f581c052) +* close #7002, console message if mismatched origins (89c025d1) +* added changelog file to root of repo (e89b4fca) +* **email:** don't escape html in notification bodies. (#7042) (d7c55bc3) + +##### Bug Fixes + +* #7108 (81697390) +* dont save data for non-positive uids (62f01a83) +* #7103 (f103390a) +* dont update cid::tids:votes if topic is pinned (2f57a4b9) +* #7102 (d117df77) +* #7102 (85a07e99) +* don't explode if there is no css el (74d0e88d) +* db info page (26ccd8f6) +* logAttempt conditional (a6c8e0ab) +* #7087, server-side protection against guest blocks (33d4956b) +* don't crash in flags.validate if user blocked target (81aa3a0b) +* dont send empty strings (555c092f) +* #7085 (fe0f95a2) +* #7086 (e55fb437) +* wrong variable #7085 (71163421) +* admins&mods when there are mutliple lines of users (de437e36) +* refreshing settings page on save if language changed (ed46c5e2) +* not calling authenticate middleware on resource direct access routes (eeaee8cc) +* #7038, autoLocale logic not playing nicely with no-refresh auths (#7059) (5f3d1c76) +* #7074 (2604cf63) +* #7071 buildSkinAsset won't rebuild continuously (a07d9898) +* #7063, logout code should do hard page nav to / or data.next (6df5668e) +* #7061 (eab297bd) +* skin not changing after login or logout, #7038 (28a1fa78) +* #7040 (a63ddbe2) +* #7041 (ec0c50d4) +* #7043 (8d7c3897) +* add missing render function (cb7c2d8c) +* #7033 (8808a033) +* #7037 (b86f1556) +* #6991, add timeout for version Github request (43c3bb02) +* #7030 (58d4376f) +* **deps:** + * update dependency nodebb-plugin-composer-default to v6.1.17 (3bcfd7fc) + * update dependency nodebb-theme-persona to v9.1.4 (b6ad5fd4) + * update dependency nodebb-plugin-markdown to v8.8.6 (#7079) (46fb365d) + * update dependency nodebb-theme-persona to v9.1.3 (#7075) (d2aea57a) + * update dependency nodebb-theme-persona to v9.1.2 (42e792ab) + * update dependency nodebb-theme-persona to v9.1.1 (#7069) (bdb33056) + * update dependency postcss to v7.0.6 (6b5428c5) + * update dependency nodebb-plugin-composer-default to v6.1.14 (#7058) (e48ed6e0) + * update dependency nodebb-plugin-composer-default to v6.1.13 (#7057) (ada1d6d0) + * update dependency nodebb-plugin-composer-default to v6.1.12 (#7056) (9f9f72da) + * update dependency nodebb-plugin-composer-default to v6.1.11 (#7055) (89acb896) + * update dependency nodebb-theme-slick to v1.2.18 (#7049) (b6cb77c1) + * update dependency nodebb-theme-slick to v1.2.17 (#7048) (7334c45b) + * update dependency nodebb-theme-slick to v1.2.16 (#7047) (1cb1af0c) + * update dependency connect-mongo to v2.0.3 (#7046) (d0d0c7f0) + * update dependency nodebb-plugin-dbsearch to v3.0.3 (#7035) (adb1b5f3) + * update dependency lru-cache to v4.1.5 (#7031) (887582eb) + * update dependency socket.io to v2.2.0 (b9d49867) + * update dependency socket.io-client to v2.2.0 (824bd541) + * update dependency nodebb-plugin-dbsearch to v3.0.2 (#7028) (11f1b409) + * update dependency nodebb-plugin-dbsearch to v3.0.1 (#7027) (e71f443c) +* **i18n:** pushed notifications source to tx, pulled fallbacks (8dd8370b) + +##### Code Style Changes + +* **eslint:** match operator-linebreak preferences (ba619c7e) + +### 1.11.0 (2018-11-28) + +##### Chores + +* **deps:** + * update dependency lint-staged to v8.1.0 (dd7f8a14) + * update dependency husky to v1.2.0 (aee21628) + * update node:8.12.0 docker digest to 5dae8ea (0ef451dd) + * update dependency husky to v1.1.4 (95d6ab06) + * update dependency eslint to v5.9.0 (92441794) + * pin dependencies (b0483f21) + * update dependency eslint-config-airbnb-base to v13 (#6599) (64b9dabf) + * update node.js to v8.12.0 (fa3afbd2) + * update dependency husky to v1.1.3 (6cee5b8e) + * update dependency lint-staged to v8.0.4 (9d258668) + * update dependency lint-staged to v8.0.3 (aaa6fe9e) + * update dependency lint-staged to v8 (95d7a5fa) + * update dependency jsdom to v13 (52f141c9) +* **husky:** setting up husky as recommended in docs (e8a3d929) + +##### New Features + +* enabling commitlint (c58a41ed) +* allow disabling of GDPR features via ACP toggle, closes #6847 (4919e9ef) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-theme-vanilla to v10.1.12 (cf928f44) + * update dependency nodebb-theme-persona to v9.1.0 (179be9ed) + * update dependency nodebb-theme-persona to v9.0.63 (#7019) (68ae3eb6) + * update dependency nodebb-plugin-markdown to v8.8.5 (d3ab7d1b) + * update dependency nodebb-theme-persona to v9.0.60 (#6984) (cbd50a80) + * update dependency nodebb-theme-vanilla to v10.1.10 (#6982) (4c769487) + * update dependency nodebb-theme-slick to v1.2.15 (#6981) (acaf1a05) + * update dependency nodebb-theme-persona to v9.0.59 (#6980) (5863bb2c) + * update dependency lru-cache to v4.1.4 (#6977) (375ab769) + * update dependency connect-mongo to v2.0.2 (#6975) (e1597b83) + * update dependency nodebb-plugin-markdown to v8.8.4 (84d1013d) + * update dependency nodebb-plugin-composer-default to v6.1.8 (fee7e336) + * update dependency nodebb-plugin-markdown to v8.8.3 (b182a195) + * update dependency nodebb-plugin-composer-default to v6.1.7 (#6966) (1101f327) + * update dependency nodebb-theme-persona to v9.0.58 (#6964) (6ade156b) + * update dependency mongodb to v3.1.10 (#6962) (662215fa) + * update dependency nodebb-theme-persona to v9.0.57 (#6956) (1bf1a439) + * update dependency nodebb-theme-persona to v9.0.55 (#6955) (e06683f7) + * update dependency nodebb-plugin-composer-default to v6.1.6 (c51ceaf0) + * update dependency nodebb-theme-persona to v9.0.54 (bb940b01) + * update dependency nodebb-plugin-mentions to v2.2.12 (#6936) (e12a803b) + * update dependency nodebb-theme-vanilla to v10.1.9 (#6935) (b480c321) + * update dependency nodebb-theme-slick to v1.2.14 (#6934) (9cdd5316) + * update dependency nodebb-theme-persona to v9.0.53 (#6933) (9ee1c2f8) + * update dependency nodebb-plugin-dbsearch to v2.0.23 (#6931) (dba1db9c) + * update dependency jsesc to v2.5.2 (511b4edc) + * update dependency validator to v10.9.0 (032caafa) + * update dependency spdx-license-list to v5 (a639b6b8) + * update dependency nodebb-theme-vanilla to v10.1.8 (eb0a322d) + * update dependency nodebb-theme-persona to v9.0.52 (6566a0cb) + * update dependency nodebb-plugin-dbsearch to v2.0.22 (#6916) (7808e58c) + * update dependency mongodb to v3.1.9 (#6914) (9a9f2af9) + * update dependency nodebb-theme-persona to v9.0.51 (e2274fe0) + * update dependency nodebb-theme-slick to v1.2.13 (3005428d) + * update dependency nodebb-theme-persona to v9.0.50 (#6902) (22140a20) + * update dependency nodebb-plugin-markdown to v8.8.2 (0b4c9a80) + * update dependency nodebb-theme-vanilla to v10.1.7 (3150a2fc) + * update dependency nodebb-theme-slick to v1.2.12 (#6881) (9bcda7f7) + * update dependency nodebb-theme-persona to v9.0.49 (#6880) (e0dc00da) + * update dependency nodebb-theme-persona to v9.0.48 (2b6f5eec) +* added admin/manage/uploads to tx config (7357926f) +* #7013, add cache buster to js-enabled.css (f6b92c1d) +* removal of scroll anchoring code in favour of browser handling (98c14e0e) +* custom navigation item not showing groups (d9452bf3) +* flags detail page crash if reporter blocks author (d027207f) +* #6922, skin assets not including plugin LESS files (a5022ce4) +* #6921, allow square brackets in usernames (da10ca08) +* interstitial redirects failing if done via ajaxify (3c8939a8) +* username trim on login, closes #6894 (157bea69) +* **uploads:** ugly filenames on uploaded asset downloading (f96208a0) +* **acp:** + * small UI fixes for ACP privileges category selector (#6946) (57b39d5b) + * hard-to-discover dropdown selector in ACP (b3f96d28) +* **l10n:** some translations (34cbd1fc) + +##### Code Style Changes + +* **eslint:** + * newlines in public/src as well (f7bd398e) + * enforcing newline on chained calls (95cc27f1) + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..8a5b7ae9bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:lts + +RUN mkdir -p /usr/src/app && \ + chown -R node:node /usr/src/app +WORKDIR /usr/src/app + +ARG NODE_ENV +ENV NODE_ENV $NODE_ENV + +COPY --chown=node:node install/package.json /usr/src/app/package.json + +USER node + +RUN npm install --only=prod && \ + npm cache clean --force + +COPY --chown=node:node . /usr/src/app + +ENV NODE_ENV=production \ + daemon=false \ + silent=false + +EXPOSE 4567 + +CMD test -n "${SETUP}" && ./nodebb setup || node ./nodebb build; node ./nodebb start diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000000..fc4e596f05 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,206 @@ +'use strict'; + +const path = require('path'); +const nconf = require('nconf'); + +nconf.argv().env({ + separator: '__', +}); +const winston = require('winston'); +const { fork } = require('child_process'); + +const { env } = process; +let worker; + +env.NODE_ENV = env.NODE_ENV || 'development'; + +const configFile = path.resolve(__dirname, nconf.any(['config', 'CONFIG']) || 'config.json'); +const prestart = require('./src/prestart'); + +prestart.loadConfig(configFile); + +const db = require('./src/database'); +const plugins = require('./src/plugins'); + +module.exports = function (grunt) { + const args = []; + + if (!grunt.option('verbose')) { + args.push('--log-level=info'); + nconf.set('log-level', 'info'); + } + prestart.setupWinston(); + + grunt.initConfig({ + watch: {}, + }); + + grunt.loadNpmTasks('grunt-contrib-watch'); + + grunt.registerTask('default', ['watch']); + + grunt.registerTask('init', async function () { + const done = this.async(); + let pluginList = []; + if (!process.argv.includes('--core')) { + await db.init(); + pluginList = await plugins.getActive(); + if (!pluginList.includes('nodebb-plugin-composer-default')) { + pluginList.push('nodebb-plugin-composer-default'); + } + } + + const styleUpdated_Client = pluginList.map(p => `node_modules/${p}/*.less`) + .concat(pluginList.map(p => `node_modules/${p}/*.css`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static|less)/**/*.less`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static)/**/*.css`)); + + const styleUpdated_Admin = pluginList.map(p => `node_modules/${p}/*.less`) + .concat(pluginList.map(p => `node_modules/${p}/*.css`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static|less)/**/*.less`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static)/**/*.css`)); + + const clientUpdated = pluginList.map(p => `node_modules/${p}/+(public|static)/**/*.js`); + const serverUpdated = pluginList.map(p => `node_modules/${p}/*.js`) + .concat(pluginList.map(p => `node_modules/${p}/+(lib|src)/**/*.js`)); + + const templatesUpdated = pluginList.map(p => `node_modules/${p}/+(public|static|templates)/**/*.tpl`); + const langUpdated = pluginList.map(p => `node_modules/${p}/+(public|static|languages)/**/*.json`); + + grunt.config(['watch'], { + styleUpdated_Client: { + files: [ + 'public/less/**/*.less', + 'themes/**/*.less', + ...styleUpdated_Client, + ], + options: { + interval: 1000, + }, + }, + styleUpdated_Admin: { + files: [ + 'public/less/**/*.less', + 'themes/**/*.less', + ...styleUpdated_Admin, + ], + options: { + interval: 1000, + }, + }, + clientUpdated: { + files: [ + 'public/src/**/*.js', + 'public/vendor/**/*.js', + ...clientUpdated, + 'node_modules/benchpressjs/build/benchpress.js', + ], + options: { + interval: 1000, + }, + }, + serverUpdated: { + files: [ + 'app.js', + 'install/*.js', + 'src/**/*.js', + 'public/src/modules/translator.common.js', + 'public/src/modules/helpers.common.js', + 'public/src/utils.common.js', + serverUpdated, + '!src/upgrades/**', + ], + options: { + interval: 1000, + }, + }, + typescriptUpdated: { + files: [ + 'install/*.ts', + 'src/**/*.ts', + 'public/src/**/*.ts', + 'public/vendor/**/*.ts', + ], + options: { + interval: 1000, + }, + }, + templatesUpdated: { + files: [ + 'src/views/**/*.tpl', + 'themes/**/*.tpl', + ...templatesUpdated, + ], + options: { + interval: 1000, + }, + }, + langUpdated: { + files: [ + 'public/language/en-GB/*.json', + 'public/language/en-GB/**/*.json', + ...langUpdated, + ], + options: { + interval: 1000, + }, + }, + }); + const build = require('./src/meta/build'); + if (!grunt.option('skip')) { + await build.build(true, { watch: true }); + } + run(); + done(); + }); + + function run() { + if (worker) { + worker.kill(); + } + + const execArgv = []; + const inspect = process.argv.find(a => a.startsWith('--inspect')); + + if (inspect) { + execArgv.push(inspect); + } + + worker = fork('app.js', args, { + env, + execArgv, + }); + } + + grunt.task.run('init'); + + grunt.event.removeAllListeners('watch'); + grunt.event.on('watch', (action, filepath, target) => { + let compiling; + if (target === 'styleUpdated_Client') { + compiling = 'clientCSS'; + } else if (target === 'styleUpdated_Admin') { + compiling = 'acpCSS'; + } else if (target === 'clientUpdated' || target === 'typescriptUpdated') { + compiling = 'js'; + } else if (target === 'templatesUpdated') { + compiling = 'tpl'; + } else if (target === 'langUpdated') { + compiling = 'lang'; + } else if (target === 'serverUpdated') { + // empty require cache + const paths = ['./src/meta/build.js', './src/meta/index.js']; + paths.forEach(p => delete require.cache[require.resolve(p)]); + return run(); + } + + require('./src/meta/build').build([compiling], { webpack: false }, (err) => { + if (err) { + winston.error(err.stack); + } + if (worker) { + worker.send({ compiling: compiling }); + } + }); + }); +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..94a9ed024d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..2c7d2a9369 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# ![NodeBB](public/images/sm-card.png) + +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + +[**NodeBB Forum Software**](https://nodebb.org) is powered by Node.js and supports either Redis, MongoDB, or a PostgreSQL database. It utilizes web sockets for instant interactions and real-time notifications. NodeBB takes the best of the modern web: real-time streaming discussions, mobile responsiveness, and rich RESTful read/write APIs, while staying true to the original bulletin board/forum format → categorical hierarchies, local user accounts, and asynchronous messaging. + +NodeBB by itself contains a "common core" of basic functionality, while additional functionality and integrations are enabled through the use of third-party plugins. + +This repository is a forked version of the base [NodeBB repository](https://github.com/NodeBB/NodeBB) with various modifications to support curriculum use. + +### [Demo](https://try.nodebb.org) | [Documentation](https://docs.nodebb.org) + +## Theming + +NodeBB's theming engine is highly flexible and does not restrict your design choices. This version of the repository has our minimalist "Persona" theme installed to get you started. + +NodeBB's base theme utilizes [Bootstrap 3](http://getbootstrap.com/) but themes can choose to use a different framework altogether. + +[![](http://i.imgur.com/HwNEXGu.png)](http://i.imgur.com/HwNEXGu.png) +[![](http://i.imgur.com/II1byYs.png)](http://i.imgur.com/II1byYs.png) + +## Installation + +[Please refer to platform-specific installation documentation](https://docs.nodebb.org/installing/os) + +For feature development, we highly recommend you install and use the suggested [grunt-cli](https://docs.nodebb.org/configuring/running/#grunt-development) to enable file-watching and live refresh. + +When running in a development environment, you can find the API specs for NodeBB at [http://localhost:4567/debug/spec/read](http://localhost:4567/debug/spec/read) and [http://localhost:4567/debug/spec/write](http://localhost:4567/debug/spec/write). + + +## TypeScript + +This codebase is in the process of being translated to[TypeScript](https://www.typescriptlang.org/)! During this intermediate stage, translated files will contain both a `.ts` and `.js` file in the repository. Translated files should be edited **only in the `.ts` file**; corresponding `.js` files will be automatically compiled and generated by the `% npx tsc` command. + +If using VSCode, you can remove duplicate files from your Explorer view by adding the following to your `.vscode/settings.json` file: +``` +{ + "files.exclude": { + "**/*.js": { "when": "$(basename).ts" }, + "**/**.js": { "when": "$(basename).tsx" } + } +} +``` + +## Development Tools +This repository comes with tools for linting (ESLint), testing (Mocha), and coverage reporting (nyc). All of these tools can be run locally: +``` +% npm run lint // Runs the linter +% npm run test // Runs test suite + generates coverage report +``` + +The first time you run the test command, it will ask you to provide a configuration for a test database. Depending on your local database setup (likely Redis), follow the instructions to add a test database configuration to `config.json`, then re-run the command. + +After running the test suite, you can find the coverage report generated in the `coverage` directory. This can be viewed in the browser by opening the `index.html` file in this directory. + +If you want to directly run the linting and testing commands with specific configurations (i.e. only running the test suite on specific files, using `--fix` with ESLint), you can find the underlying commands are in the `package.json` file. + +## License + +NodeBB is licensed under the **GNU General Public License v3 (GPL-3)** (http://www.gnu.org/copyleft/gpl.html). + +## Helpful Links + +* [NodeBB Demo](https://try.nodebb.org) +* [Documentation & Installation Instructions](http://docs.nodebb.org) +* **Git & Github:** + * [Git Documentation](https://git-scm.com/docs/gittutorial) + * [Git Flow](https://datasift.github.io/gitflow/IntroducingGitFlow.html) + * [GitHub Basics](https://guides.github.com/activities/hello-world/) + * [GitHub's Flow](https://guides.github.com/introduction/flow/) + * [GitHub Cross-Referencing](https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/autolinked-references-and-urls#issues-and-pull-requests) +* **Frontend Development:** + * [Benchpress Documentation](https://github.com/benchpressjs/benchpressjs) + * [Bootstrap 3 Documentation ](http://getbootstrap.com/) +* **Server Development:** + * [Node.js Documentation](https://nodejs.org/en/docs/) +* **Database/Backend:** + * [Redis Documentation](https://redis.io/docs/) + * [Redis CLI](https://redis.io/docs/manual/cli/) +* **Linting & Testing:** + * [Mocha Documentation](https://mochajs.org/) + * [ESLint Documentation](https://eslint.org/docs/latest/) + * [nyc Test Coverage Documentation](https://www.npmjs.com/package/nyc) +* **TypeScript:** + * [TypeScript for New Programmers](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) + * [TypeScript for JavaScript Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) + * [JavaScript to TypeScript Translation](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html#moving-to-typescript-files) \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000000..eeb84aec36 --- /dev/null +++ b/app.js @@ -0,0 +1,82 @@ +/* + NodeBB - A better forum platform for the modern web + https://github.com/NodeBB/NodeBB/ + Copyright (C) 2013-2021 NodeBB Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +'use strict'; + +require('./require-main'); + +const nconf = require('nconf'); + +nconf.argv().env({ + separator: '__', +}); + +const winston = require('winston'); +const path = require('path'); + +const file = require('./src/file'); + +process.env.NODE_ENV = process.env.NODE_ENV || 'production'; +global.env = process.env.NODE_ENV || 'production'; + +// Alternate configuration file support +const configFile = path.resolve(__dirname, nconf.any(['config', 'CONFIG']) || 'config.json'); + +const configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database')); + +const prestart = require('./src/prestart'); + +prestart.loadConfig(configFile); +prestart.setupWinston(); +prestart.versionCheck(); +winston.verbose('* using configuration stored in: %s', configFile); + +if (!process.send) { + // If run using `node app`, log GNU copyright info along with server info + winston.info(`NodeBB v${nconf.get('version')} Copyright (C) 2013-${(new Date()).getFullYear()} NodeBB Inc.`); + winston.info('This program comes with ABSOLUTELY NO WARRANTY.'); + winston.info('This is free software, and you are welcome to redistribute it under certain conditions.'); + winston.info(''); +} + +if (nconf.get('setup') || nconf.get('install')) { + require('./src/cli/setup').setup(); +} else if (!configExists) { + require('./install/web').install(nconf.get('port')); +} else if (nconf.get('upgrade')) { + require('./src/cli/upgrade').upgrade(true); +} else if (nconf.get('reset')) { + require('./src/cli/reset').reset({ + theme: nconf.get('t'), + plugin: nconf.get('p'), + widgets: nconf.get('w'), + settings: nconf.get('s'), + all: nconf.get('a'), + }); +} else if (nconf.get('activate')) { + require('./src/cli/manage').activate(nconf.get('activate')); +} else if (nconf.get('plugins') && typeof nconf.get('plugins') !== 'object') { + require('./src/cli/manage').listPlugins(); +} else if (nconf.get('build')) { + require('./src/cli/manage').build(nconf.get('build')); +} else if (nconf.get('events')) { + require('./src/cli/manage').listEvents(); +} else { + require('./src/start').start(); +} diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000000..9addbff7eb --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +* +*/ +!export +!.gitignore diff --git a/build/export/.gitignore b/build/export/.gitignore new file mode 100644 index 0000000000..f8d55e0e3f --- /dev/null +++ b/build/export/.gitignore @@ -0,0 +1,3 @@ +. +!.gitignore +!README \ No newline at end of file diff --git a/build/export/README b/build/export/README new file mode 100644 index 0000000000..a9015033f4 --- /dev/null +++ b/build/export/README @@ -0,0 +1,5 @@ +This directory contains archives of user uploads that are prepared on-demand +when a user wants to retrieve a copy of their uploaded content. + +You can delete the files in here at will. They will just be regenerated if +requested again. \ No newline at end of file diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000000..42719c621d --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + extends: ['@commitlint/config-angular'], + rules: { + 'header-max-length': [1, 'always', 72], + 'type-enum': [ + 2, + 'always', + [ + 'breaking', + 'build', + 'chore', + 'ci', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test', + ], + ], + }, +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..5e382f47f9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.5' + +services: + node: + build: . + restart: unless-stopped + depends_on: + - db + expose: + - 4567 # use a reverse proxy like Traefik + + db: + image: mongo:bionic + restart: unless-stopped + expose: + - 27017 + environment: + MONGO_INITDB_ROOT_USERNAME: root + MONGO_INITDB_ROOT_PASSWORD: root + volumes: + - mongo:/data/db + +volumes: + mongo: diff --git a/install/data/categories.json b/install/data/categories.json new file mode 100644 index 0000000000..4e29dc4fb7 --- /dev/null +++ b/install/data/categories.json @@ -0,0 +1,38 @@ +[ + { + "name": "Announcements", + "description": "Announcements regarding our community", + "descriptionParsed": "

Announcements regarding our community

\n", + "bgColor": "#fda34b", + "color": "#ffffff", + "icon" : "fa-bullhorn", + "order": 1 + }, + { + "name": "General Discussion", + "description": "A place to talk about whatever you want", + "descriptionParsed": "

A place to talk about whatever you want

\n", + "bgColor": "#59b3d0", + "color": "#ffffff", + "icon" : "fa-comments-o", + "order": 2 + }, + { + "name": "Blogs", + "description": "Blog posts from individual members", + "descriptionParsed": "

Blog posts from individual members

\n", + "bgColor": "#86ba4b", + "color": "#ffffff", + "icon" : "fa-newspaper-o", + "order": 4 + }, + { + "name": "Comments & Feedback", + "description": "Got a question? Ask away!", + "descriptionParsed": "

Got a question? Ask away!

\n", + "bgColor": "#e95c5a", + "color": "#ffffff", + "icon" : "fa-question", + "order": 3 + } +] \ No newline at end of file diff --git a/install/data/defaults.json b/install/data/defaults.json new file mode 100644 index 0000000000..e7bf3f65f4 --- /dev/null +++ b/install/data/defaults.json @@ -0,0 +1,185 @@ +{ + "title": "NodeBB", + "showSiteTitle": 1, + "defaultLang": "en-GB", + "loginDays": 14, + "loginSeconds": 0, + "loginAttempts": 5, + "lockoutDuration": 60, + "adminReloginDuration": 60, + "postDelay": 10, + "initialPostDelay": 10, + "newbiePostDelay": 120, + "postEditDuration": 0, + "newbiePostEditDuration": 3600, + "postDeleteDuration": 0, + "enablePostHistory": 1, + "topicBacklinks": 1, + "postCacheSize": 10485760, + "disableChat": 0, + "chatEditDuration": 0, + "chatDeleteDuration": 0, + "chatMessageDelay": 200, + "notificationSendDelay": 60, + "newbiePostDelayThreshold": 3, + "postQueue": 0, + "postQueueReputationThreshold": 0, + "groupsExemptFromPostQueue": ["administrators", "Global Moderators"], + "groupsExemptFromMaintenanceMode": ["administrators", "Global Moderators"], + "minimumPostLength": 8, + "maximumPostLength": 32767, + "systemTags": "", + "minimumTagsPerTopic": 0, + "maximumTagsPerTopic": 5, + "minimumTagLength": 3, + "maximumTagLength": 15, + "undoTimeout": 10000, + "allowTopicsThumbnail": 1, + "registrationType": "normal", + "registrationApprovalType": "normal", + "allowAccountDelete": 1, + "privateUploads": 0, + "allowedFileExtensions": "png,jpg,bmp,txt", + "uploadRateLimitThreshold": 10, + "uploadRateLimitCooldown": 60, + "allowUserHomePage": 1, + "allowMultipleBadges": 0, + "maximumFileSize": 2048, + "stripEXIFData": 1, + "orphanExpiryDays": 0, + "resizeImageWidthThreshold": 2000, + "resizeImageWidth": 760, + "rejectImageWidth": 5000, + "rejectImageHeight": 5000, + "resizeImageQuality": 80, + "topicThumbSize": 512, + "minimumTitleLength": 3, + "maximumTitleLength": 255, + "minimumUsernameLength": 2, + "maximumUsernameLength": 16, + "minimumPasswordLength": 6, + "minimumPasswordStrength": 1, + "maximumSignatureLength": 255, + "maximumAboutMeLength": 1000, + "maximumUsersInChatRoom": 0, + "maximumChatMessageLength": 1000, + "maximumProfileImageSize": 256, + "maximumCoverImageSize": 2048, + "profileImageDimension": 200, + "profile:convertProfileImageToPNG": 0, + "profile:keepAllUserImages": 0, + "gdpr_enabled": 1, + "allowProfileImageUploads": 1, + "teaserPost": "last-reply", + "showPostPreviewsOnHover": 1, + "allowPrivateGroups": 1, + "unreadCutoff": 2, + "bookmarkThreshold": 5, + "autoDetectLang": 1, + "reputation:disabled": 0, + "downvote:disabled": 0, + "disableSignatures": 0, + "signatures:hideDuplicates": 0, + "upvotesPerDay": 20, + "upvotesPerUserPerDay": 6, + "downvotesPerDay": 10, + "downvotesPerUserPerDay": 3, + "min:rep:chat": 0, + "min:rep:downvote": 0, + "min:rep:upvote": 0, + "min:rep:flag": 0, + "min:rep:profile-picture": 0, + "min:rep:cover-picture": 0, + "min:rep:website": 0, + "min:rep:aboutme": 0, + "min:rep:signature": 0, + "flags:limitPerTarget": 0, + "flags:autoFlagOnDownvoteThreshold": 0, + "flags:actionOnResolve": "rescind", + "flags:actionOnReject": "rescind", + "notificationType_upvote": "notification", + "notificationType_new-topic": "notification", + "notificationType_new-reply": "notification", + "notificationType_post-edit": "notification", + "notificationType_follow": "notification", + "notificationType_new-chat": "notification", + "notificationType_new-group-chat": "notification", + "notificationType_group-invite": "notification", + "notificationType_group-leave": "notification", + "notificationType_group-request-membership": "notification", + "notificationType_mention": "notification", + "notificationType_new-register": "notification", + "notificationType_post-queue": "notification", + "notificationType_new-post-flag": "notification", + "notificationType_new-user-flag": "notification", + "topicStaleDays": 60, + "maxTopicsPerPage": 20, + "maxPostsPerPage": 20, + "topicsPerPage": 20, + "postsPerPage": 20, + "categoriesPerPage": 50, + "userSearchResultsPerPage": 50, + "searchDefaultSortBy": "relevance", + "searchDefaultIn": "titlesposts", + "searchDefaultInQuick": "titles", + "maximumGroupNameLength": 255, + "maximumGroupTitleLength": 40, + "preventTopicDeleteAfterReplies": 0, + "feeds:disableSitemap": 0, + "feeds:disableRSS": 0, + "sitemapTopics": 500, + "maintenanceMode": 0, + "maintenanceModeStatus": 503, + "votesArePublic": 0, + "maximumInvites": 0, + "username:disableEdit": 0, + "email:disableEdit": 0, + "email:smtpTransport:pool": 0, + "hideFullname": 0, + "hideEmail": 0, + "showFullnameAsDisplayName": 0, + "allowGuestHandles": 0, + "guestsIncrementTopicViews": 1, + "allowGuestReplyNotifications": 1, + "incrementTopicViewsInterval": 60, + "recentMaxTopics": 200, + "disableRecentCategoryFilter": 0, + "maximumRelatedTopics": 0, + "disableEmailSubscriptions": 0, + "emailConfirmInterval": 10, + "emailConfirmExpiry": 24, + "removeEmailNotificationImages": 0, + "sendValidationEmail": 1, + "includeUnverifiedEmails": 0, + "emailPrompt": 1, + "sendEmailToBanned": 0, + "requireEmailAddress": 0, + "inviteExpiration": 7, + "dailyDigestFreq": "off", + "digestHour": 17, + "passwordExpiryDays": 0, + "cross-origin-embedder-policy": 0, + "cross-origin-opener-policy": "same-origin", + "cross-origin-resource-policy": "same-origin", + "hsts-maxage": 31536000, + "hsts-subdomains": 0, + "hsts-preload": 0, + "hsts-enabled": 0, + "eventLoopCheckEnabled": 1, + "eventLoopLagThreshold": 100, + "eventLoopInterval": 500, + "onlineCutoff": 30, + "timeagoCutoff": 30, + "necroThreshold": 7, + "categoryWatchState": "watching", + "submitPluginUsage": 1, + "showAverageApprovalTime": 1, + "autoApproveTime": 0, + "maxUserSessions": 10, + "useCompression": 0, + "updateUrlWithPostIndex": 1, + "composer:showHelpTab": 1, + "composer:allowPluginHelp": 1, + "maxReconnectionAttempts": 5, + "reconnectionDelay": 1500 +} \ No newline at end of file diff --git a/install/data/footer.json b/install/data/footer.json new file mode 100644 index 0000000000..53b2176ade --- /dev/null +++ b/install/data/footer.json @@ -0,0 +1,10 @@ +[ + { + "widget": "html", + "data" : { + "html": "", + "title":"", + "container":"" + } + } +] \ No newline at end of file diff --git a/install/data/navigation.json b/install/data/navigation.json new file mode 100644 index 0000000000..e8d4fe6e10 --- /dev/null +++ b/install/data/navigation.json @@ -0,0 +1,77 @@ +[ + { + "route": "/categories", + "title": "[[global:header.categories]]", + "enabled": true, + "iconClass": "fa-list", + "textClass": "visible-xs-inline", + "text": "[[global:header.categories]]" + }, + { + "id": "unread-count", + "route": "/unread", + "title": "[[global:header.unread]]", + "enabled": true, + "iconClass": "fa-inbox", + "textClass": "visible-xs-inline", + "text": "[[global:header.unread]]", + "groups": ["registered-users"] + }, + { + "route": "/recent", + "title": "[[global:header.recent]]", + "enabled": true, + "iconClass": "fa-clock-o", + "textClass": "visible-xs-inline", + "text": "[[global:header.recent]]" + }, + { + "route": "/tags", + "title": "[[global:header.tags]]", + "enabled": true, + "iconClass": "fa-tags", + "textClass": "visible-xs-inline", + "text": "[[global:header.tags]]" + }, + { + "route": "/popular", + "title": "[[global:header.popular]]", + "enabled": true, + "iconClass": "fa-fire", + "textClass": "visible-xs-inline", + "text": "[[global:header.popular]]" + }, + { + "route": "/users", + "title": "[[global:header.users]]", + "enabled": true, + "iconClass": "fa-user", + "textClass": "visible-xs-inline", + "text": "[[global:header.users]]" + }, + { + "route": "/groups", + "title": "[[global:header.groups]]", + "enabled": true, + "iconClass": "fa-group", + "textClass": "visible-xs-inline", + "text": "[[global:header.groups]]" + }, + { + "route": "/admin", + "title": "[[global:header.admin]]", + "enabled": true, + "iconClass": "fa-cogs", + "textClass": "visible-xs-inline", + "text": "[[global:header.admin]]", + "groups": ["administrators"] + }, + { + "route": "/career", + "title": "Career", + "enabled": true, + "iconClass": "fa-briefcase", + "textClass": "visible-xs-inline", + "text": "Career" + } +] \ No newline at end of file diff --git a/install/data/welcome.md b/install/data/welcome.md new file mode 100644 index 0000000000..86b61b63fd --- /dev/null +++ b/install/data/welcome.md @@ -0,0 +1,10 @@ +# Welcome to your brand new NodeBB forum! + +This is what a topic and post looks like. As an administrator, you can edit the post\'s title and content. +To customise your forum, go to the [Administrator Control Panel](../../admin). You can modify all aspects of your forum there, including installation of third-party plugins. + +## Additional Resources + +* [NodeBB Documentation](https://docs.nodebb.org) +* [Community Support Forum](https://community.nodebb.org) +* [Project repository](https://github.com/nodebb/nodebb) diff --git a/install/databases.js b/install/databases.js new file mode 100644 index 0000000000..33996b5776 --- /dev/null +++ b/install/databases.js @@ -0,0 +1,87 @@ +'use strict'; + +const prompt = require('prompt'); +const winston = require('winston'); + +const questions = { + redis: require('../src/database/redis').questions, + mongo: require('../src/database/mongo').questions, + postgres: require('../src/database/postgres').questions, +}; + +module.exports = async function (config) { + winston.info(`\nNow configuring ${config.database} database:`); + const databaseConfig = await getDatabaseConfig(config); + return saveDatabaseConfig(config, databaseConfig); +}; + +async function getDatabaseConfig(config) { + if (!config) { + throw new Error('invalid config, aborted'); + } + + if (config.database === 'redis') { + if (config['redis:host'] && config['redis:port']) { + return config; + } + return await prompt.get(questions.redis); + } else if (config.database === 'mongo') { + if ((config['mongo:host'] && config['mongo:port']) || config['mongo:uri']) { + return config; + } + return await prompt.get(questions.mongo); + } else if (config.database === 'postgres') { + if (config['postgres:host'] && config['postgres:port']) { + return config; + } + return await prompt.get(questions.postgres); + } + throw new Error(`unknown database : ${config.database}`); +} + +function saveDatabaseConfig(config, databaseConfig) { + if (!databaseConfig) { + throw new Error('invalid config, aborted'); + } + + // Translate redis properties into redis object + if (config.database === 'redis') { + config.redis = { + host: databaseConfig['redis:host'], + port: databaseConfig['redis:port'], + password: databaseConfig['redis:password'], + database: databaseConfig['redis:database'], + }; + + if (config.redis.host.slice(0, 1) === '/') { + delete config.redis.port; + } + } else if (config.database === 'mongo') { + config.mongo = { + host: databaseConfig['mongo:host'], + port: databaseConfig['mongo:port'], + username: databaseConfig['mongo:username'], + password: databaseConfig['mongo:password'], + database: databaseConfig['mongo:database'], + uri: databaseConfig['mongo:uri'], + }; + } else if (config.database === 'postgres') { + config.postgres = { + host: databaseConfig['postgres:host'], + port: databaseConfig['postgres:port'], + username: databaseConfig['postgres:username'], + password: databaseConfig['postgres:password'], + database: databaseConfig['postgres:database'], + ssl: databaseConfig['postgres:ssl'], + }; + } else { + throw new Error(`unknown database : ${config.database}`); + } + + const allQuestions = questions.redis.concat(questions.mongo).concat(questions.postgres); + for (let x = 0; x < allQuestions.length; x += 1) { + delete config[allQuestions[x].name]; + } + + return config; +} diff --git a/install/package.json b/install/package.json new file mode 100644 index 0000000000..25ab00d40b --- /dev/null +++ b/install/package.json @@ -0,0 +1,200 @@ +{ + "name": "nodebb", + "license": "GPL-3.0", + "description": "NodeBB Forum", + "version": "2.8.1", + "homepage": "http://www.nodebb.org", + "repository": { + "type": "git", + "url": "https://github.com/NodeBB/NodeBB/" + }, + "main": "app.js", + "scripts": { + "start": "npx tsc && node loader.js", + "lint": "npx tsc && eslint --cache ./nodebb .", + "test": "npx tsc && nyc --reporter=html --reporter=text-summary mocha", + "coverage": "nyc report --reporter=text-lcov > ./coverage/lcov.info", + "coveralls": "nyc report --reporter=text-lcov | coveralls && rm -r coverage" + }, + "nyc": { + "exclude": [ + "src/upgrades/*", + "test/*" + ] + }, + "lint-staged": { + "*.js": [ + "eslint --fix" + ] + }, + "dependencies": { + "@adactive/bootstrap-tagsinput": "0.8.2", + "@isaacs/ttlcache": "1.2.1", + "@nodebb/bootswatch": "3.4.2", + "@socket.io/redis-adapter": "8.0.0", + "@types/async": "^3.2.16", + "@types/lodash": "^4.14.191", + "@types/nconf": "^0.10.3", + "@types/semver": "^7.3.13", + "ace-builds": "1.14.0", + "archiver": "5.3.1", + "async": "3.2.4", + "autoprefixer": "10.4.13", + "bcryptjs": "2.4.3", + "benchpressjs": "2.4.3", + "body-parser": "1.20.1", + "bootbox": "5.5.3", + "bootstrap": "3.4.1", + "chalk": "4.1.2", + "chart.js": "2.9.4", + "cli-graph": "3.2.2", + "clipboard": "2.0.11", + "colors": "1.4.0", + "commander": "9.4.1", + "compare-versions": "5.0.3", + "compression": "1.7.4", + "connect-flash": "0.1.1", + "connect-mongo": "4.6.0", + "connect-multiparty": "2.2.0", + "connect-pg-simple": "8.0.0", + "connect-redis": "6.1.3", + "cookie-parser": "1.4.6", + "cron": "2.1.0", + "cropperjs": "1.5.13", + "csurf": "1.11.0", + "daemon": "1.1.0", + "diff": "5.1.0", + "esbuild": "0.16.10", + "express": "4.18.2", + "express-session": "1.17.3", + "express-useragent": "1.0.15", + "file-loader": "6.2.0", + "fs-extra": "11.1.0", + "graceful-fs": "4.2.10", + "grunt-cli": "^1.4.3", + "helmet": "5.1.1", + "html-to-text": "9.0.3", + "ioredis": "5.2.4", + "ipaddr.js": "2.0.1", + "jquery": "3.6.3", + "jquery-deserialize": "2.0.0", + "jquery-form": "4.3.0", + "jquery-serializeobject": "1.0.0", + "jquery-ui": "1.13.2", + "jsesc": "3.0.2", + "json2csv": "5.0.7", + "jsonwebtoken": "8.5.1", + "less": "4.1.3", + "lodash": "4.17.21", + "logrotate-stream": "0.2.8", + "lru-cache": "7.14.1", + "material-design-lite": "1.3.0", + "mime": "3.0.0", + "mkdirp": "1.0.4", + "mongodb": "4.13.0", + "morgan": "1.10.0", + "mousetrap": "1.6.5", + "multiparty": "4.2.3", + "nconf": "0.12.0", + "nodebb-plugin-2factor": "5.1.2", + "nodebb-plugin-composer-default": "9.2.4", + "nodebb-plugin-dbsearch": "5.1.5", + "nodebb-plugin-emoji": "4.0.6", + "nodebb-plugin-emoji-android": "3.0.0", + "nodebb-plugin-markdown": "10.1.1", + "nodebb-plugin-mentions": "3.0.12", + "nodebb-plugin-spam-be-gone": "1.0.2", + "nodebb-rewards-essentials": "0.2.1", + "nodebb-widget-essentials": "6.0.1", + "nodemailer": "6.8.0", + "nprogress": "0.2.0", + "passport": "0.6.0", + "passport-http-bearer": "1.0.1", + "passport-local": "1.0.0", + "pg": "8.8.0", + "pg-cursor": "2.7.4", + "postcss": "8.4.20", + "postcss-clean": "1.2.0", + "progress-webpack-plugin": "1.0.16", + "prompt": "1.3.0", + "request": "2.88.2", + "request-promise-native": "1.0.9", + "rimraf": "3.0.2", + "rss": "1.2.2", + "sanitize-html": "2.8.1", + "semver": "7.3.8", + "serve-favicon": "2.5.0", + "sharp": "0.31.3", + "sitemap": "7.1.1", + "slideout": "1.0.1", + "socket.io": "4.5.4", + "socket.io-client": "4.5.4", + "sortablejs": "1.15.0", + "spdx-license-list": "6.6.0", + "spider-detector": "2.0.0", + "terser-webpack-plugin": "5.3.6", + "textcomplete": "0.18.2", + "textcomplete.contenteditable": "0.1.1", + "timeago": "1.6.7", + "tinycon": "0.6.8", + "toobusy-js": "0.5.1", + "uglify-es": "3.3.9", + "validator": "13.7.0", + "webpack": "5.75.0", + "webpack-merge": "5.8.0", + "winston": "3.8.2", + "xml": "1.0.1", + "xregexp": "5.1.1", + "yargs": "17.6.2", + "zxcvbn": "4.4.2" + }, + "devDependencies": { + "@apidevtools/swagger-parser": "10.0.3", + "@commitlint/cli": "17.3.0", + "@commitlint/config-angular": "17.3.0", + "@types/express": "^4.17.15", + "@typescript-eslint/eslint-plugin": "^5.48.0", + "@typescript-eslint/parser": "^5.48.0", + "coveralls": "3.1.1", + "eslint": "^8.31.0", + "eslint-config-nodebb": "0.2.1", + "eslint-plugin-import": "2.26.0", + "grunt": "1.5.3", + "grunt-contrib-watch": "1.1.0", + "husky": "8.0.2", + "jsdom": "20.0.3", + "lint-staged": "13.1.0", + "mocha": "10.2.0", + "mocha-lcov-reporter": "1.3.0", + "mockdate": "3.0.5", + "nyc": "15.1.0", + "smtp-server": "3.11.0", + "typescript": "^4.9.4" + }, + "resolutions": { + "*/jquery": "3.6.3" + }, + "bugs": { + "url": "https://github.com/NodeBB/NodeBB/issues" + }, + "engines": { + "node": ">=12" + }, + "maintainers": [ + { + "name": "Andrew Rodrigues", + "email": "andrew@nodebb.org", + "url": "https://github.com/psychobunny" + }, + { + "name": "Julian Lam", + "email": "julian@nodebb.org", + "url": "https://github.com/julianlam" + }, + { + "name": "Barış Soner Uşaklı", + "email": "baris@nodebb.org", + "url": "https://github.com/barisusakli" + } + ] +} diff --git a/install/web.js b/install/web.js new file mode 100644 index 0000000000..233d7bb64b --- /dev/null +++ b/install/web.js @@ -0,0 +1,286 @@ +'use strict'; + +const winston = require('winston'); +const express = require('express'); +const bodyParser = require('body-parser'); +const fs = require('fs'); +const path = require('path'); +const childProcess = require('child_process'); +const less = require('less'); + +const webpack = require('webpack'); +const nconf = require('nconf'); + +const Benchpress = require('benchpressjs'); +const mkdirp = require('mkdirp'); +const { paths } = require('../src/constants'); + +const app = express(); +let server; + +const formats = [ + winston.format.colorize(), +]; + +const timestampFormat = winston.format((info) => { + const dateString = `${new Date().toISOString()} [${global.process.pid}]`; + info.level = `${dateString} - ${info.level}`; + return info; +}); +formats.push(timestampFormat()); +formats.push(winston.format.splat()); +formats.push(winston.format.simple()); + +winston.configure({ + level: 'verbose', + format: winston.format.combine.apply(null, formats), + transports: [ + new winston.transports.Console({ + handleExceptions: true, + }), + new winston.transports.File({ + filename: 'logs/webinstall.log', + handleExceptions: true, + }), + ], +}); + +const web = module.exports; +let installing = false; +let success = false; +let error = false; +let launchUrl; + +const viewsDir = path.join(paths.baseDir, 'build/public/templates'); + +web.install = async function (port) { + port = port || 4567; + winston.info(`Launching web installer on port ${port}`); + + app.use(express.static('public', {})); + app.use('/assets', express.static(path.join(__dirname, '../build/public'), {})); + + app.engine('tpl', (filepath, options, callback) => { + filepath = filepath.replace(/\.tpl$/, '.js'); + + Benchpress.__express(filepath, options, callback); + }); + app.set('view engine', 'tpl'); + app.set('views', viewsDir); + app.use(bodyParser.urlencoded({ + extended: true, + })); + try { + await Promise.all([ + compileTemplate(), + compileLess(), + runWebpack(), + copyCSS(), + loadDefaults(), + ]); + setupRoutes(); + launchExpress(port); + } catch (err) { + winston.error(err.stack); + } +}; + +async function runWebpack() { + const util = require('util'); + const webpackCfg = require('../webpack.installer'); + const compiler = webpack(webpackCfg); + const webpackRun = util.promisify(compiler.run).bind(compiler); + await webpackRun(); +} + +function launchExpress(port) { + server = app.listen(port, () => { + winston.info('Web installer listening on http://%s:%s', '0.0.0.0', port); + }); +} + +function setupRoutes() { + app.get('/', welcome); + app.post('/', install); + app.post('/launch', launch); + app.get('/ping', ping); + app.get('/sping', ping); +} + +function ping(req, res) { + res.status(200).send(req.path === '/sping' ? 'healthy' : '200'); +} + +function welcome(req, res) { + const dbs = ['mongo', 'redis', 'postgres']; + const databases = dbs.map((databaseName) => { + const questions = require(`../src/database/${databaseName}`).questions.filter(question => question && !question.hideOnWebInstall); + + return { + name: databaseName, + questions: questions, + }; + }); + + const defaults = require('./data/defaults.json'); + + res.render('install/index', { + url: nconf.get('url') || (`${req.protocol}://${req.get('host')}`), + launchUrl: launchUrl, + skipGeneralSetup: !!nconf.get('url'), + databases: databases, + skipDatabaseSetup: !!nconf.get('database'), + error: error, + success: success, + values: req.body, + minimumPasswordLength: defaults.minimumPasswordLength, + minimumPasswordStrength: defaults.minimumPasswordStrength, + installing: installing, + }); +} + +function install(req, res) { + if (installing) { + return welcome(req, res); + } + req.setTimeout(0); + installing = true; + + const database = nconf.get('database') || req.body.database || 'mongo'; + const setupEnvVars = { + ...process.env, + NODEBB_URL: nconf.get('url') || req.body.url || (`${req.protocol}://${req.get('host')}`), + NODEBB_PORT: nconf.get('port') || 4567, + NODEBB_ADMIN_USERNAME: nconf.get('admin:username') || req.body['admin:username'], + NODEBB_ADMIN_PASSWORD: nconf.get('admin:password') || req.body['admin:password'], + NODEBB_ADMIN_EMAIL: nconf.get('admin:email') || req.body['admin:email'], + NODEBB_DB: database, + NODEBB_DB_HOST: nconf.get(`${database}:host`) || req.body[`${database}:host`], + NODEBB_DB_PORT: nconf.get(`${database}:port`) || req.body[`${database}:port`], + NODEBB_DB_USER: nconf.get(`${database}:username`) || req.body[`${database}:username`], + NODEBB_DB_PASSWORD: nconf.get(`${database}:password`) || req.body[`${database}:password`], + NODEBB_DB_NAME: nconf.get(`${database}:database`) || req.body[`${database}:database`], + NODEBB_DB_SSL: nconf.get(`${database}:ssl`) || req.body[`${database}:ssl`], + defaultPlugins: JSON.stringify(nconf.get('defaultplugins') || nconf.get('defaultPlugins') || []), + }; + + winston.info('Starting setup process'); + launchUrl = setupEnvVars.NODEBB_URL; + + const child = require('child_process').fork('app', ['--setup'], { + env: setupEnvVars, + }); + + child.on('close', (data) => { + installing = false; + success = data === 0; + error = data !== 0; + + welcome(req, res); + }); +} + +async function launch(req, res) { + try { + res.json({}); + server.close(); + req.setTimeout(0); + let child; + + if (!nconf.get('launchCmd')) { + child = childProcess.spawn('node', ['loader.js'], { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'], + }); + + console.log('\nStarting NodeBB'); + console.log(' "./nodebb stop" to stop the NodeBB server'); + console.log(' "./nodebb log" to view server output'); + console.log(' "./nodebb restart" to restart NodeBB'); + } else { + // Use launchCmd instead, if specified + child = childProcess.exec(nconf.get('launchCmd'), { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'], + }); + } + + const filesToDelete = [ + path.join(__dirname, '../public', 'installer.css'), + path.join(__dirname, '../public', 'bootstrap.min.css'), + path.join(__dirname, '../build/public', 'installer.min.js'), + ]; + try { + await Promise.all( + filesToDelete.map( + filename => fs.promises.unlink(filename) + ) + ); + } catch (err) { + console.log(err.stack); + } + + child.unref(); + process.exit(0); + } catch (err) { + winston.error(err.stack); + throw err; + } +} + +// this is necessary because otherwise the compiled templates won't be available on a clean install +async function compileTemplate() { + const sourceFile = path.join(__dirname, '../src/views/install/index.tpl'); + const destTpl = path.join(viewsDir, 'install/index.tpl'); + const destJs = path.join(viewsDir, 'install/index.js'); + + const source = await fs.promises.readFile(sourceFile, 'utf8'); + + const [compiled] = await Promise.all([ + Benchpress.precompile(source, { filename: 'install/index.tpl' }), + mkdirp(path.dirname(destJs)), + ]); + + await Promise.all([ + fs.promises.writeFile(destJs, compiled), + fs.promises.writeFile(destTpl, source), + ]); +} + +async function compileLess() { + try { + const installSrc = path.join(__dirname, '../public/less/install.less'); + const style = await fs.promises.readFile(installSrc); + const css = await less.render(String(style), { filename: path.resolve(installSrc) }); + await fs.promises.writeFile(path.join(__dirname, '../public/installer.css'), css.css); + } catch (err) { + winston.error(`Unable to compile LESS: \n${err.stack}`); + throw err; + } +} + +async function copyCSS() { + const src = await fs.promises.readFile( + path.join(__dirname, '../node_modules/bootstrap/dist/css/bootstrap.min.css'), 'utf8' + ); + await fs.promises.writeFile(path.join(__dirname, '../public/bootstrap.min.css'), src); +} + +async function loadDefaults() { + const setupDefaultsPath = path.join(__dirname, '../setup.json'); + try { + // eslint-disable-next-line no-bitwise + await fs.promises.access(setupDefaultsPath, fs.constants.F_OK | fs.constants.R_OK); + } catch (err) { + // setup.json not found or inaccessible, proceed with no defaults + if (err.code !== 'ENOENT') { + throw err; + } + + return; + } + winston.info('[installer] Found setup.json, populating default values'); + nconf.file({ + file: setupDefaultsPath, + }); +} diff --git a/loader.js b/loader.js new file mode 100644 index 0000000000..964c7ac30f --- /dev/null +++ b/loader.js @@ -0,0 +1,249 @@ +'use strict'; + +const nconf = require('nconf'); +const fs = require('fs'); +const url = require('url'); +const path = require('path'); +const { fork } = require('child_process'); +const logrotate = require('logrotate-stream'); +const mkdirp = require('mkdirp'); + +const file = require('./src/file'); +const pkg = require('./package.json'); + +const pathToConfig = path.resolve(__dirname, process.env.CONFIG || 'config.json'); + +nconf.argv().env().file({ + file: pathToConfig, +}); + +const pidFilePath = path.join(__dirname, 'pidfile'); + +const outputLogFilePath = path.join(__dirname, nconf.get('logFile') || 'logs/output.log'); + +const logDir = path.dirname(outputLogFilePath); +if (!fs.existsSync(logDir)) { + mkdirp.sync(path.dirname(outputLogFilePath)); +} + +const output = logrotate({ file: outputLogFilePath, size: '1m', keep: 3, compress: true }); +const silent = nconf.get('silent') === 'false' ? false : nconf.get('silent') !== false; +let numProcs; +const workers = []; +const Loader = { + timesStarted: 0, +}; +const appPath = path.join(__dirname, 'app.js'); + +Loader.init = function () { + if (silent) { + console.log = (...args) => { + output.write(`${args.join(' ')}\n`); + }; + } + + process.on('SIGHUP', Loader.restart); + process.on('SIGTERM', Loader.stop); +}; + +Loader.displayStartupMessages = function () { + console.log(''); + console.log(`NodeBB v${pkg.version} Copyright (C) 2013-${(new Date()).getFullYear()} NodeBB Inc.`); + console.log('This program comes with ABSOLUTELY NO WARRANTY.'); + console.log('This is free software, and you are welcome to redistribute it under certain conditions.'); + console.log('For the full license, please visit: http://www.gnu.org/copyleft/gpl.html'); + console.log(''); +}; + +Loader.addWorkerEvents = function (worker) { + worker.on('exit', (code, signal) => { + if (code !== 0) { + if (Loader.timesStarted < numProcs * 3) { + Loader.timesStarted += 1; + if (Loader.crashTimer) { + clearTimeout(Loader.crashTimer); + } + Loader.crashTimer = setTimeout(() => { + Loader.timesStarted = 0; + }, 10000); + } else { + console.log(`${numProcs * 3} restarts in 10 seconds, most likely an error on startup. Halting.`); + process.exit(); + } + } + + console.log(`[cluster] Child Process (${worker.pid}) has exited (code: ${code}, signal: ${signal})`); + if (!(worker.suicide || code === 0)) { + console.log('[cluster] Spinning up another process...'); + + forkWorker(worker.index, worker.isPrimary); + } + }); + + worker.on('message', (message) => { + if (message && typeof message === 'object' && message.action) { + switch (message.action) { + case 'restart': + console.log('[cluster] Restarting...'); + Loader.restart(); + break; + case 'pubsub': + workers.forEach((w) => { + w.send(message); + }); + break; + case 'socket.io': + workers.forEach((w) => { + if (w !== worker) { + w.send(message); + } + }); + break; + } + } + }); +}; + +Loader.start = function () { + numProcs = getPorts().length; + console.log(`Clustering enabled: Spinning up ${numProcs} process(es).\n`); + + for (let x = 0; x < numProcs; x += 1) { + forkWorker(x, x === 0); + } +}; + +function forkWorker(index, isPrimary) { + const ports = getPorts(); + const args = []; + + if (!ports[index]) { + return console.log(`[cluster] invalid port for worker : ${index} ports: ${ports.length}`); + } + + process.env.isPrimary = isPrimary; + process.env.isCluster = nconf.get('isCluster') || ports.length > 1; + process.env.port = ports[index]; + + const worker = fork(appPath, args, { + silent: silent, + env: process.env, + }); + + worker.index = index; + worker.isPrimary = isPrimary; + + workers[index] = worker; + + Loader.addWorkerEvents(worker); + + if (silent) { + const output = logrotate({ file: outputLogFilePath, size: '1m', keep: 3, compress: true }); + worker.stdout.pipe(output); + worker.stderr.pipe(output); + } +} + +function getPorts() { + const _url = nconf.get('url'); + if (!_url) { + console.log('[cluster] url is undefined, please check your config.json'); + process.exit(); + } + const urlObject = url.parse(_url); + let port = nconf.get('PORT') || nconf.get('port') || urlObject.port || 4567; + if (!Array.isArray(port)) { + port = [port]; + } + return port; +} + +Loader.restart = function () { + killWorkers(); + + nconf.remove('file'); + nconf.use('file', { file: pathToConfig }); + + fs.readFile(pathToConfig, { encoding: 'utf-8' }, (err, configFile) => { + if (err) { + console.error('Error reading config'); + throw err; + } + + const conf = JSON.parse(configFile); + + nconf.stores.env.readOnly = false; + nconf.set('url', conf.url); + nconf.stores.env.readOnly = true; + + if (process.env.url !== conf.url) { + process.env.url = conf.url; + } + Loader.start(); + }); +}; + +Loader.stop = function () { + killWorkers(); + + // Clean up the pidfile + if (nconf.get('daemon') !== 'false' && nconf.get('daemon') !== false) { + fs.unlinkSync(pidFilePath); + } +}; + +function killWorkers() { + workers.forEach((worker) => { + worker.suicide = true; + worker.kill(); + }); +} + +fs.open(pathToConfig, 'r', (err) => { + if (err) { + // No config detected, kickstart web installer + fork('app'); + return; + } + + if (nconf.get('daemon') !== 'false' && nconf.get('daemon') !== false) { + if (file.existsSync(pidFilePath)) { + let pid = 0; + try { + pid = fs.readFileSync(pidFilePath, { encoding: 'utf-8' }); + if (pid) { + process.kill(pid, 0); + console.info(`Process "${pid}" from pidfile already running, exiting`); + process.exit(); + } else { + console.info(`Invalid pid "${pid}" from pidfile, deleting pidfile`); + fs.unlinkSync(pidFilePath); + } + } catch (err) { + if (err.code === 'ESRCH') { + console.info(`Process "${pid}" from pidfile not found, deleting pidfile`); + fs.unlinkSync(pidFilePath); + } else { + console.error(err.stack); + throw err; + } + } + } + + require('daemon')({ + stdout: process.stdout, + stderr: process.stderr, + cwd: process.cwd(), + }); + + fs.writeFileSync(pidFilePath, String(process.pid)); + } + try { + Loader.init(); + Loader.displayStartupMessages(); + Loader.start(); + } catch (err) { + console.error('[loader] Error during startup'); + throw err; + } +}); diff --git a/nodebb b/nodebb new file mode 100755 index 0000000000..546e608cd2 --- /dev/null +++ b/nodebb @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +'use strict'; + +require('./src/cli'); diff --git a/nodebb.bat b/nodebb.bat new file mode 100644 index 0000000000..f7f45890d9 --- /dev/null +++ b/nodebb.bat @@ -0,0 +1 @@ +@echo off && cd %~dp0 && node ./nodebb %* diff --git a/public/.eslintrc b/public/.eslintrc new file mode 100644 index 0000000000..a3ce8297a6 --- /dev/null +++ b/public/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "nodebb/public" +} diff --git a/public/503.html b/public/503.html new file mode 100644 index 0000000000..68c9386146 --- /dev/null +++ b/public/503.html @@ -0,0 +1,177 @@ + + + Excessive Load Warning + + + + + +
+
+

503

+

+ This forum is temporarily unavailable due to excessive load. +

+

+ We shouldn't be down for long. Please check back shortly. Sorry for the inconvenience! +

+

+  Alright. You can stop clicking... it's not going to make the site come back sooner! +

+
+
+ + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e73d1c154679373618773f34f549817ba20b1971 GIT binary patch literal 1150 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 literal 0 HcmV?d00001 diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..45f5be50caf7be30b62050b4cafc52579c9ccb47 GIT binary patch literal 13618 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 Ef;?;j00000 literal 0 HcmV?d00001 diff --git a/public/images/logo.svg b/public/images/logo.svg new file mode 100644 index 0000000000..cc1c56f881 --- /dev/null +++ b/public/images/logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/public/images/logo@3x.png b/public/images/logo@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..bda2ba25247cc95434d80279a77c4ba049feacdb GIT binary patch literal 61551 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%0DYM|?b$i4*qYq|f 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 B*KPm+ literal 0 HcmV?d00001 diff --git a/public/images/sm-card.png b/public/images/sm-card.png new file mode 100644 index 0000000000000000000000000000000000000000..7d61ea874f5abd246c5ffeaebce77a0bb7617de3 GIT binary patch literal 220595 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ð!0vt${ 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 literal 0 HcmV?d00001 diff --git a/public/images/themes/default.png b/public/images/themes/default.png new file mode 100644 index 0000000000000000000000000000000000000000..2b2e6a05e741ca51457181c90c5bb9c8565a4d99 GIT binary patch literal 3559 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 W*`~ed{uR)XK@4<_uN7T&dh~CMr5n)z literal 0 HcmV?d00001 diff --git a/public/images/touch/144.png b/public/images/touch/144.png new file mode 100644 index 0000000000000000000000000000000000000000..1c8c82af5fa42edac926487dd0c32c1f60632e0d GIT binary patch literal 6615 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 literal 0 HcmV?d00001 diff --git a/public/images/touch/192.png b/public/images/touch/192.png new file mode 100644 index 0000000000000000000000000000000000000000..3fb8cbec5b5ea173be18d4adb3ec9108938cf9ee GIT binary patch literal 10163 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

InWrmTjYL*;>=ce#Qi;tN$)k$1&k7D zBCJiaB_F;44}`vNduz#zr}}saASif5;<;$n#ZrqzJ8wMHq;2vT))ADGbNgtEx_Vcf ztk%_W8LtC~FatIU`kR~F*3U~=YJRJI8*L@!+kc9(rj8^ATD5 zz<`U@%ISD>61uNG+em-!v}$#q?_GAhh~=hH7+dGDqRrNn^fRezRE1XT@M!pZ)5Mlq z#v55pz@E>PVF7`~?_$O;Ybj8J;@S|qNB~8y zz|Q=8f1`q24~D9@gSve$7)=hRbyW*6RFr*o_B-v%IN|*YmPLI=+%=t&>S!56HC!J8 z1p@0ld7E3_a(%`LnJ(@c=ati0GDu+Z@P|NyKy)0M$FQ`kBMNmcjr?=UPype0pRWQ+v!lkgX4mNQ$ZCktHiX_497N}1^TX!N(|+x@(?MxQ$Oc6iQS z91&{yi=2`K1>U+>3kZH$U)M13I||&@u1SyWu{-to`aM9$-a~bFWf|Bp(mytZ+g(+W zQ=;yy_`T}1D#g0kC1!0GjLFsuXIbd+Qv;YD&HHR*(Og?2f@({9yl0>i z*!ba8L2f0?Pqzbc#p1H^DRY|GS7LNT3>PIo&1Hh9Jfbg1y8$3c@5OTRpCyh4zLxj_ zfjF!R3tx_fNQRti96j}F@XKVAOa(RK!k1_22@@EBjAIM*cbeA9wW@nZt!UVC|J;{N zKQ6$S;q-I(^;;3HBbj7N)@Lui<6a0%;bA84j`(@qEY_-~{z-h-#tCGg%e_E-3?x`e zBtCY0e_`4apF~8PB&}1|mP;w5xY_2JZ--rG5CJa395FaIE+mO8Q-qZ z;L6TZgZ#^8hwJ@kI&VzjbO+Jr<$ z8gTpJ73(aIWqLAw4OlkU>qLC*Zi8QeF`=bui|Y26e3t-n#mbJhCB%w_^lBSy_)f~{ zO3;4`t~yHq=v}{M^02xn@NR4&dl$LHEav>eTm7MDDf&mVshk&MQ4>TvpLO-ggY$Wg zAtgd9tEO_#_WqIYyEC2p)gWG8{YZGB^893Y7B$HP6~M1;aU^CszL_!QCc2C_1w{=5HoR(Q-SS#Cc37ex}?OrS7CrfAyjMiv=pV$SwIpwgP?b z?xcQW46MFCdDtlif~#HaF*RZlPY*wwb}KM|wRZFnVzK?ut5;onp3Whmsd;wp!8l4~ zuk$z!r7ItmhZIwU#~K{n4f=<3|8cyS*Rk<~Y6%59i5Qk%{V9Jwye(e0Z3h){1j4fb>IkbcyAv+x`{Tct23Mv@Bg6cXYW$7Kh$>|H7BH=h zA|MFPbV67>`ugmV_{YL>K6|9P)J@*Mha_#DV3i80+!5FwB-nhr6chw4(G)4`Rxlhf z58EmJcf>x&bn`NIpYIW)YjuZn2QWyF#0dCduqr ze6as_J(};W%cGj`UqZ?ccx2Sa@Ly&<(v*2am0tvm)eLHr|DN{9QyGe0-QM8Vt&=QmkFCg@dO!E$x7s>E4Byqs&%Cf)ChQAB0E{75XHPTigXitaoMvJx zag=^uwB0zBVD?}GC56m`)HNUD$IqNK%w&i5)!%K5GuYF~8g!{_>F-Yu zir6@=nHZyi?GYdxKm{C0Lu&G_+q)xIkC4~>$`lY2p`IFud?yFJpD%PF#3*ixN6GTW zIm5hiF8)!mtp!usIDECyA_rgp&c~c&`Eu}_hQd^gj+Yu7$N+zD}p18>q36KCx( zs%K>egEAtt<=DI%7oT6K%&6e@Losa$;H>Mf#j-$xL(lH4m|o3?nn_H(-+jOQN=>8= z>lnbLT$MRWOU15qSy)MBh|@nAPwM)u_(Nrk-3TSb?8d0VP-<@cs!O0$JMIS^Es|JV z8CnR-&eGBhb9n^eke172`UjMiiwXeWar0LpaUiJM zpMSqNcbEr2B+~&G9Td^a-$do5US}ZEstSvOzs$=GgQikgZ>7X}KE`)?1rnIHS^kv( zmKaSOKoZ4uvL)~$D1jfaXj6TMT;YE!YlAqkug-_JsG>v5OM`8Ssg1zS+e9TiB@hb%j%BVmvKUOaotiJO;WQLM^3XRRUM zo0CRwPXBJi@2iBeiY8sAo@WAG#XD4(`^Ft8u_P@DzW4yZn4Wyos!7fVnGRm(_O(T~}*iA3F(5{?uBbOs%cES39@Jo$fIL&x$y zy}&SnOC)c3cY1B02TKI*{(&j7?>*zKqQ$*Ra7Aa=c6XH0P!P*lWexqAQ5ZQy@R{R@ zQP~Irx$SN)U*sv5?7^_`<^pZIJFP+bGGDXfNxYQM_#m2vIOnqS0M&j~aVLKjeO6Y& z4(R0!vTF}Ikcteol&cfx&L|fn2R;WKJ@ARDNp@Cxkj>#YtnEFJ+^q^_!&81}{{nal zAY35)d}GH?r-|^WMI>5gE=w1`1;@sR6-{RVt{nB*RCFklkUdSk zn;E0e96G3|#DCA%(2l%GSsW?;uZj|(05FxDC51fy=lP8}`?J?-n%I$~ZFrSpb4pmsPrkJ3=@q>9Y5B18Bxaym2M?GAjUk z`fVYk;oV94DNByj4FF^667)!+cMx&Yz1+ys^yVa9cR|0q~#`OqgM z!V`!!^CE*raXM`<`&-@hChXgg=Gq0ozDC6BpEN_#qs8gNycNXmz_?=ARHDX9eSkgd9|lUTcBZza zikYM48jc&|x*)s*D$R4PT6C1yD=z6EW0|qk+nw0v%eHwwMpMw3Gr4@KB(}BSING@ z+u;0y#t7Z%a;5K6AO+?$A8l#j0K_osh&Z(prQ~vSLvZcsd8eCKj=F{%Gq^4Bt{yD# z5kH>>3z(*Vk(blmtU2eKT}I!$yam3!x-@9TbJtFzJy&4Pd6@Rn1{s;@SV&h;BAuB@ z+}3%*TXc1J*JmaLnOC5k!3h5d0XQ|Ct03LuA-+W?2Mq4sEF~U!ZidlNOh>Zwh0|h> z8CF6klT-Q(^5D#;Qn~Kun^>y#uL6@HlQv%u;|B@~{dooDbPDqSuNJ^S^-Sg9^^Pao z(}Yd_c1%?KdT>N`Q3Yl$b${b|OBU6UJHW!9L@BS19-buCJj_Z$>wKBQVOaVV3TLRt zO}GFK1XDSMbaF(*=d0Pi0c|)LaR;1D^=+CpMQ9-+&k2|3JGe?z=E4r?0Jf}tuqQt6 zdY%lS_Y)QEP<6ypg&{|nFvbE0*Qu?vJz*Y+tCWxoMk~`--24hLUO&gb4s=uu zLjw5j_PIz)=vR@Y4jph4vkZlXHH~>ArqG; zEl=p~k||OL9y7y&?gA7(bb3bHh)sk?|40HZ6~WQ@(EtTYM1181qBA&#mczrZ!fQjl zl;!K`kh?O<5>Q$98q1wy6;ZD*mX`^;y26LuWfo;0?_;;TEw5817UjqlA$f#dN7NlG zH-DNTp51rgiH}MDK51V-PUZ?eM7M6#SHRGAwkYALBY}jqtN8>9lkT+*TxJnb(r$c< za;Vs}Qc$Ge_kCYe9QtQS){zOM{#VLE_?|o)+1Q4ivFW;m_Q;DAtq03dzW6xCIHKmG z__sd=M$+*9vBSkQ8DrK(n~yC4&ZCjDF8c5L4}oL5J@WQr6u6^`YpB!gZ3b#|?Xpvj zT}w4lFoOdZV_jd!;fqvu=ty@1L2xI@mQyqn<~f8p2}Bp*>l(L}|A|rFjQ3B`IeVdx z=H!me)(N-_>>O;#j-3TNtX1*Gx&?V3f}%9INROOP)O$NC1}W{ZzHRAtZYwa@pGG96 zEBf~R3S~OLX*TF4cJwC8Eka3of>zA$`jWdCknnMBwYlSqR;yTQzYZ6)1)M2N?yV-A zbHVob=IzOD|_!3m80_d6}tfbIgxQy{ul;;fK zLBePbLU+ttibAHUs~ANu!FODK)KV4?C*zIB6v58*&p2#-WAH<}55ON5-Or>iS{}v8 zydmt0VDe}+CxEc5$sY87ufP-tB&fw)09~K5dW84C@U|IxpTatlw>eGrV(X8V_3y_b z=(Q8o7pCaR8^pA1&!g_Z13`SNJL(||TRg_FW4*U_`Q*ewItL;rxQO~!vL+7G9Ac#h zmVgQ`UYx>%I@g`N;po#}u*o#wI5VJ4KFMXiptHv}d4iNwMAo-=7_S~MNK7l^=6p*r z_0L#LcF)QL+lSGviyJ*j;Wt@c@$tUEb{&uIU23 z?=3MSvu&{IWFXrsdn@IQLj(wj6LaL-s&~dS13_ge^(}V-M#?v1T)T=!pT}6`$+eI4 zd)}@Z$5?@u{lyB#Odya({SSklZOv_Qj%x#FpS9CZ)v7BSc~TMasTS*{>CZ`g4BiA; z1_gwIzBHVnk)4I>1}!U+bACQ?&j>`6?!{HB-2(?fr`PV@2>B{^K`SGEPtkZ-+0-6; zP+%&xJ2nrA^1=fgxzy(9e^WlKzwDj@d%4X0-C3KNCDoAzRlh;>yrOx8UG#pxo+lFskuS|o~93fh6ca=tzwaP=Y@t@9czu>6ga z9>GP*BYo0^N*N+P_3^{I{+)k_`&9~*xHiN9sQprE) z;dL{wi|AuqWc#}6pTAL%BFnt+tfY>UT0~=@PPT)1QAAy&`3I8JyMWhYBBDEZOjhA< z)H5ymCJT+XRFE%Qh5SUvE}A3Xlb^lVAh8m|JnROh1x##x#*sR)Al;h_@Ncv;m+O?y zBj`Hu3fL>lc8!Nkgdf$r$y=*_5`|e>rI}S3Tv%dE_HN?T(hT7%=&vPf&Y?t0b`&np$)IMmx)B2p!sMk74ERCiCw zffYA0l%%d?;*@;0*t%Z%cTe1IFU50TA3>iED$Ss2TYQwH73rU1ymN9&53|)^ zxvp;~F{d&0Hdyg(N6N4Gbus+2EMy^-ZXeijJS(*(NY`*qD){By&)}4QYxYn<@7{MV zCv4pE$5;{Mn8A+!WdBE=3}ScvBI=Lt2%iEZ558i1`3zC;`#boO8CA$%Mgn|hem7ZuvAqfMNE0B7^J%kIJ?0>*Sb zHnIGS#{HX!-I3fmhj^(VTgLi1Aqc;1jeA3?Q;jFsvzAf&=fn^>BP6rCp^V!T6THZB zSjBjxWm@1A_o&ncd4SdLuliygghC&1ewny&J5sn{z~YXwFwM4afYq{+lCEzkBLGeT zcRdWs^-YaHdVfbJ-Y9_?j@$-m9TMX*`O(yR41oL|`$#p@v^#4eQ%+P`hDbwJ$zPfeCj{DjQ%UKNv4KOxmTuTeqr=DIo15zF7QIkZ((N!f9CLg32#(#16x5b4QDaMP)Cc|A`loD`~_2ak=be%HTjbV2| zF;tj)7D)8RfnZ!XKh5DFwOUeuq-#SA*T~f??%2%&JTX$)holfi3UFTZ#p5)# z!eC<0H)hNCLW|g!!y+r$m!ogn#-DkcOD|hTMj{zPL6!Au9xI~I);5!&NeAm!LGPuj z+JaP5mMHH8vcvK5M%-TXfDlJN58N5$T{mFDFM-P@?T^BIn+4{-Cl^U@=E`Ggh$roC zIW&@$ei|-BKKXNK<Rgx-_ zthmcSurL~Z__LicKT@Ld{Kd=Hb?A%HyO9~#-{pma+4?JElwC$2s=^mk?S|OzBn!dL zCBm}_gNq&WjJt1uvR1^&0k6^ow0d4UT$E)N=5xHraOVs~(82B)Qax-p1a7wVd7&2R ziyjcT{UI(JfzY^GUf?5J0RR` zJY^}41;tV1?+uOd?{_f=jfFW+rgiVri_RZ21JjeuWu>@4zLwUR3=~OoGy$rb z$8Ky~?K%NZQ}iFd?WZZfwn{?>elF;f@7hV?xoNw{vrEA?Zzf_i(o=o!>2Vo#ExtEK zuG0rOV4DC5=^UZ)KQd+E(_Ryta+vTfRmsFB{pSW5J;Kbht!GZT{A9!wDiey2F;ojARjRB{KBClCH&m01{k( z>@9G5wSfm*NCsu>%g8LNb8Pw zQT6C4t0;G2rjwq`p5UC6&r>x25^uEFd1!r(9~x5+94N~%r%}^CmAVOjG(0k@T6Xj1 zvRNNkgbPM3BhM+v{>3g!6FjAiwM$MO_=~hz{*a{48KMtNFdIpoG1j*$f^^!yAr~fl zKTMLggrk=V_ZaI6j+_bpnXW8%E*ox zM6lu-H#K1Cixtt(6{ZA~gos44$Y(XzJT8)Le>aF77q#$iw+b^_0;e<;d7={UGYx2E zIY3TuzmPRWYZ;qRbJ`K z;bs5L#RPNl>LU7xztL&uz;)lzb1^0Ce0~Q>v91mH)c&cFuu|DPPKve3k)iqU4Y}WW z6<(o8)!DN6cxA! zHr<_rqOd-k01+w$GHZ3=i{EfRg;Z;?GADLZ<6pch!l*$wLjT5W=*m6Q)|!5Hvw6d(loEg|3#FZwM8uGlNDorLpsC=xBgg#_u0zAb75EH4)!*^?OR|{pO>4kJ^nVDk* z0|)Ivrgbt26n#_Ius1s_$QFjQvCZN?@{S=qNeof+xlq&?qZ;U6&i9}Y_x>ybAUFAy zX?Q#SwW*RB>lz7|iz*=Bwze$|K5caMSzG)ODQOzF>?rY0VDkH3G-Kh~pT_l@ZPt20 z$1}ftd5APD#$xbb*+<57jU3=S$=>0!^ae4yl`p{CeK}S`A_`SDK`U%;07?~846|z( zt(al^cR21-k(T)5p>hp`%tvdNZ@yWv50YhW%L8tYPM(>4xjgcX)`FO7R97rjae_Hf zd1ZvORzB04J5i{KVNuS<1ekA~8{|`Oc3HZ87YuVZWxGe1X60JztKu^YP!$>=*XfN1 zsQT?kkSIr!k>%Bw0GV{IGAB`(zITx=&U+##R}GOK1K3y4wN+JfF>Qh%iu3wDz zjhqEhYdGU}cN3f53j>`qaRu~`Xx;tj>u)8SNB30fXq?ny{a-+UQQ~xcOMdd~8jgLp ztlaA+_3PZkjZiVbQ(!{MrOLS``Q*I@M#h8T#~qJq0?XtK*t4(TlbI3 z+|)cHDQ4s%v;Nlc%w2DFp10p&4%Wg-@+=ljqlwep4L`DXBB$@3UZeFj*S`KkO_(LW zgE(nnvfFWeVE8nbj^8V;CrHmo2AA(%OUSAO2dNX+c<0;!j};7Z-5zmf<($8zY7vDR zyhyDC4E*FPkuZsy9?1D!y#CqtI)=q-Pu_mTZ5+hN!X6N?aEWeFXkc*_gUZ)bq!a|c zhwPtgJt>qdvAxohb=-Zx<9tkotm4%+Cm*v391)_|vioN*Z;2wwfMvt@Ym=m5P4iqS z%D#fE*0g`bZw9m3X(?S3w=Z=MF0Tbqb&hFOT->H!sKIo007C=P*?Y7-A79aVJ--U6 zjbl1d;I`}nDtL66^oCQQmO_ug=={c6n*T}MfQ>!wyfo-aXV+16YkXCbP{Kz-X{~uo z<&@y~_RGsvHd*Y-TZ>}|F+<9w2jI)uv#e%44Qe^K{tsrD!NGRH9bk^m0f|z);}|IH zibww8!aB|jZ!zWoP9NUV!78pkB}sJ1$6I&5q`7U6fLQVIa$0JWFWZoXP{Dpuj#T?X za1j>5FMd7$a*?#{)xS@Qm6Oe?x!YYJGFW&|dYh>?mt#}LRJ5PgryDNf>w)ya6At4RNfU^axAoC^)YKV)W> zhFQ5D)Q&bI~)w{U`msXN^L+EhJ9XWg*h?C*?)*%@{3pC9K?N!;^m- zEEUY{gTC1Lz2`s6HFp1GrUSDk-Z#{A0&0{q->edIXOj;nzJ_MfLUaEuQL@VYexU`} zvQVIbPi|{!ljIstOe?N^(rJUui_bptoKG#-;lq9C>fA|3jDM)O@0fia@xs|oKL~b; zJ_~X~4|B7~nP#mnt`2s;^97Pi&0EO}2)5(#CFH$C|X z1GCc3B9x8_Z53oNeq(8>(R@Y5nE0(eO@`m3(xeY3P4_z$mOVrXM=dh2lPLans-?iy zkXISXf6QNwGJnreq6@HbIC|H`)uqe={lHovqb~Z3 zYeT(;vpHby1y@dFWqgGCgVUY1$EO9Pnl*tZzPmT;cVnrCCpg||q$_J;AN~eTwG=IR zyIy=|-nACz9&(Yikib^*87|oCMJ!i-4EtlsCe5?6cq%oov4xl8Dhf7v*|=v#pX5dj zcn+{{my&xMfO5}j<}i4&Eww#;Lc>^TXmLp#gV4Gy2_OzuN#re^5dXS#zO&nC9`t?I zGiS@EJL6Hd?`3hPNSN!F2P@&-%|=bjLYhnyt5k;P@g&cNCl(5@_e$2scylvbKjX2} z0{{hs9JQ`{(ZI<-{3gK_9(R!oE21P`x$Z-Y zvuhe{Z%gpbp|&~yQ=DZeSesbP|JVzuBByYPj2Ue+MEq!a8?&(~bIIKFT-dRF$LG-H z=Je1xn-7#blD2hM6d(BVr2^WWrDt>b??vHgX@F>v-YpC#}*DP9pH zL$hcg{pWW?abr!bA+3L2jHEs&QBLzfo)>43zIHlmk699LANqn;jD{!ylo0AwTM#dC zRMKc=-m--q^u6f#_+hi1bg%U^f9Xbof2OLhPkO6#0ksjLMFfpT?|$Q{{H*5R*P5E* zB{pnQ#nY$iuf=lGRTa22tKt6M`I7kX5W~E-EPbS}xzjPx{yYmbCXuF0fa#H1x~j+{sv^wutM|a{x&!uhVVc3&r6STi zjSCQ)yKlH$x`OoHI}>J#!rhovG#a8PVR+X#6+}LMdAvMX6tItJ4BSm?am@}pRCgyr zoJ1g70uJVfT6cfeC2{*LO-A0=uT-JUIHZZkyLNAb3Q1-M#OXckC(>-h>pzaKncV=! zT-+Ketzs(TR-GM&G~ofG!51v0Z8upgOVt~&s`|>wsk)zRz@t%qB0#E4^yZ&MX3me| z)eduZ5NcKi>+DoNZ*i~hfoOUQ;MdQA4Sgy{A<3iMz9XIHBvA;Q6<%kqf>E{U@^4Hs zLUH5B)vx0d@Qcs|#lUu60H+*%Wy|%Q`oS&fsBp&|9@c%aZ9%H{u;xYV(R)!?D<2u6 zJ{gTe_?#_AU~qa*3#CEEAL0j7a{aJeCD-JCzHfn4AVgKsfm}Dwn{}JWI)46b{vKQ& zNbQYJ>`pZ{ER4(7#!Tx0(BYwfHTT8UF%eRBce3~Z0^DH#=!NYFX1VR}&X7bY5cW&# zT;yH=UZ0cdj!l2mcKqXKZQ0BfjYDBhs|Yey`CPCTrQM-tbDTC%tZTVyK{su%$LbS57xH61A)G>|3cv6XPQv< z5tuVmPDyEw|1;|!aL>tiWCkp7`)w#giuebD6d-gyMu!I+Da2i(O;U*~{txG~N&@9~ z+4twcqcEqxzn8UdEP zE53t&6g0GnZM1yEa9*H9Zlv z{OC#wUs4SVjeSLwE74cEmzi3nd75GCz4SvH-St3| z_F$%40jMogWx@oF#>)-2NEP}!akI9Shq!q|xtv@9q7D2pz#b6@Lbe{nIln9p8pgZG zRNPwUEUY=L5+*HBWlU0fE?|Rwn9Ns4%^nV59r)2Pp76o3_lBiy|2`4JHj@e=PNW<| z0GZ8q=SZI|WKk?D;WY=|#2t2{M)CL+G)7t<=+0_{0>h~Qdl>pibAsEs?S-#DOF`AM z?28rNjW-5%4G^EZP4&1UV8w=`I%v6ZrAx5Zcd3axl%!H{O~*RhVVO7tYtjEDjlk!j zoE_J`>T5$#LK^UGbmJto&>)-xe6R9FW)udLes?G}l`pESaS;E;hqtkN-`z39*s z%oREPAT?zFJ);~NcX?0hx3jtYDzkv#N!PGgSIIGPcnR=5J#`9hIUm&Hji_8h5%iAN zUl{Nq(SZT__^fE5;xLhyY|$|E0ukT}@z~xJ?EGMYo%r$W09gD)M{@k-ZW;-pDEV{T zg6CE3R6Hn{nYR8rm9|Y((Wy8eFscl&+<6|+#g!?=^s|jiH~K}x`!-S5ATBf_XP1wd zhuY-3;BVMQ;NK-V13;=|Y=pI>Ar2gHkUwWD}V z_sd968f_UJN72ZpiAqq{ZTz|S@!&zZ?%mR!idLBSQ_>ut|7uV@DKG1r!1NXCWhR4w zsaDVrC$Z#0^wEg-3If&ozcO@dv0XQCVx$!9 zW!d>XJJlLMAdLOSm3>b}Dqbv+^_2Nu*jA;p>D2zjNBL+LZ>@2L`Ix-RV~&n9x;?YenPr=(4dwa@e!bm>)F=5A3Q}S^ zLPv4$EQ+;~rmB8X1NYn3$l&Hx2qhUgWqX3(MviHBX9o^V28#lN{y5_LZnVv&|5pp3 z*P%#yNoFYi5kUnP;%D5yIjJ)9F)=R3Ov+pk2XX9w-Qri?&QMv)TsI?Xl$%YYcqIf* zCIH`=LpsXn>R(Z#yk8^t9Mx1@Kf)BoabGv#e?$d0!@Rm7t)meW2hyflk}jF-EizY< zq^3qMRn#5m;G|DA&(EAitV2R-$gaGfEVKViImO|SJH@&9Y9{3|7wRtwd;4)@t*?U~ z1%^@p?*fnN7BcY}=XJ@zB6fQTGHF(?lFHRg*vLBbHuwD_8(H6U-vWlHC)(yKKs@VktA4YCH-4-R+Z}iUFYTs4j8kz6(#TLHcSPs zvbrgLb5=6*QZ=ehPCp+qiTfS3F4I((j__dw!3l9E=YCFRXK^7TJVR&b(c7XLK1P9b zkXweJz?KEpZ0z;r)Zh6A@-ZUmkOx&AYYQ}iXLf9T*?{b{+`8{5Mro@(MoDCYB4T6C z0qcPHjex{{G04Q^-QW$Ly)aZ^)`vaDTzf!N38DD!%BPR%``vmrTowPe%i?5t#LUG! zSVs1&s81WdoREuoR_J1BG^(Eb3a<}XFd-Q*%^1rV_dAufdNl^0pQt&SZ-zx8@yYE7 z=5HaBWaPMVmWSdj#QFkY7}yJmIVMtS zTmN(4B$*mePp(_Na=d_@^u_W`=BQ`#B>+cBB%;B#|5UA<%pvcY`t}*CNui$xouqi0 zZCz@OG*YDa;H)A8UMzI7LcWvAtU%J;MlhQmVzw26~39OQ zWbrYg+>j&xQf|`KZ+UrXPMbge)W_(nF{pY+okjX;P#tm;M*4xQ_1L;y0?Wr zS4s-z+p5X(_Vrf3fJHE3pN_YUFd1d6&^nuY;64cUS~cd3O`d z;EeGcfjDi+L|r&=^u-1lM&~|^xVo0c9MzazU0&yvcJngmEMOZ>vd!r~@q3)!i}Yd_kaO^kfSX{@Dz4ju-SGobL*W_`?yelga)DwEp$5NZafz--KY*K1S z!FjJzQHA`Q2RjUH9PRLuKxg+mu6P5Z^!n2RA6d;sf%U3X-XF77O`QY*n{v*7Kt-%f z)PIq<8R9|B@Vc_ZE6dgnU0L8?NpZI+*m5`EiO#EP6_sfItEgvyJ!d$-0#?dG0n1F& z<5)Mv6bRzuCT)9_!=t*^CGlOr=y1Ju;se%x*j*Ul7kkU_uj}c3r#qwT-ES$zdvT)z ze=J&hE9c&ZZ*CULEL;zr&#_hBi~kQzU*Q+k^Zk8yS-L?%X{1{L=~zYSQc4;`x*H^R z5fB0CZlpxILt-iE?(VLoJD&Y~pV#kCn0sf=ob#>;jrtgaRqmaGAO!HAOeuX3rV~-} z=mPBN)&uB7C8)M@$2b%+kNvV1CTmypH_RptPlgv2Bwr80XJ6eDcO${e_%1>XTQKw6 zz9Rg;Yr&y!IUq-Fzo&g$Y0CLR&Dqd>e7*0uG+$=jucrGSqY7;?T)|I5@)b|DBg=Fi z#c=&wY*5^1HiIfs-DU@@TWsYgKb+kkYKG>^HclQiO%PO=ey1f3r)^~(XEfw@nXx`in!R2$GGj^mWbi!1$oqvsfQl2 zR`DQ#=7i-1!bRR&fANU23@#989`b`)Y9>#S!H(MKp#cdj-;>j-14Zf=4MkbFxrTYs`d#&Ox=-61T8F5o z8#c&gz}9iTV)YQC+dEqgEXja^5Nwi}eb96DF|J5o+=_gi5AQWUiQ+#w`!@&=!51Ki z(sc=AcXc@U$g23`9L@dsA*FV)R(?%#B4NZ0-2aYF<>;f7PJF#-Wams!%{aC!q*?kB z8cUJ0eO|$Ka>|tpQT;0@kM*&q_ycY4Md=W_GM0npKdUbT_;QB8mml^|7&5XhC|i?I zVpH$FlTanCCD$li8RSIreQyArfeyRl5xYSnEhbC5!4#TR)$C`w<&$oqh2M0Heb?uZpY8qr|>ffe?Xec0x093hZI{}!k?Jo6cx*R@%kc& zE|rl|_3r8ORNTCR{&z-7ym6w&*;%>lK$Tk|GA=w}+EFG|qh7Jh!i2`YaTMH4vu~M{ zIbKd}`nfThA)@>MQunvgYi#fuaUs#jF9_;Y6q)r*!CTsRbo7L~-Azb2Nb=S6SZCf@&DnqW>q-3*UYLaUvQ|oX=!+-G(H4Cf>`5H&N4I7u*-4EW3j2dX?MH_jl$v=8b zuE*zo@Oh(zhgQADkxRF0Mv{q?Xn#fLZ2o}piL5L$M}?qF{Iy3g+rXm?;X#Veq%5>} zG=e4mf3x5}<91oQn&>d9U>KAAn2=>vYQ#Qfq*%O_!Kc2`G<9QpknL&o>{Jfts|MIO zE*(Wy6=l95?LoMh>4K&Cd;>DqP%pPUX>C z_-7SQfh$EFQo3dtMF^CATd-sC8sU93IfLB4w7#8iqJw(@bTN_m?;B|W^J5o(I-eo9 zM`?wwq$qLmJGzWguG=mjpaRp_qvV4Za3l?-m=x-pl6|Y&ZgiND$-%a&7^D5XS;Vbs zey4_OA*+Lc6wCnhy^4_*?eY}edThHsq8{$Rsp0an8r`=9SE)zwMn&QP%{;}50Dys~kYdNwzqgvKLdm;Tg@4mp4ONe}+b5oF9I3ko#&|V~ocN7SjTlyB1o>U{x=p zkMY)O7a_;l#_Y*lE9vO>O;6csQhhCulS4c#;eC!J!HmF`L37#84_$9Mv-y#c75$rv z)a2Q(v9G5Xd4f746d_e@%)#S5*V$>Hjq?#*aBdpYoHEAP8RvFG4L)H;Qj+I23SEPF z2xCkPju3)M0M&RE!JA=!34-kd*y-9_{Lu&Wl9D&FVwY71HzHb&kgYv@Thw*Frw`3TjXDU_MnnzkX-n!FT@M6=4t%*I27q@< z@@A8nYGtF8u%spU%i_=R$9ff&Tb$)7Oy>_3Y-r2VOMVrX4@Z{fX%9dDOYitnjj>p= zHZQET_`tn&SHt;!s^nMF_D4Hz*MqT^jy@pSr4(U`mq342z3maVn>H*7926Yau93if z!@fGW=jOHkuyy-u>xB_{`6smVjSG`HcjhT&r&OiWZeuZ|>?VCVOaC(6W!*Ug#u>2t zWO0PHr?kcjV~)`7@Ci-bvmchU@McBzjK@UPx2l;wXh)QqkEMu2ZNx*$<&wkBQmMr3 zfmW(4*x_Pt%fQiQWI+xah!gN58kQj%C9=i|aL(g9>PHY0ezc_fgYl<=!de<@Z1#ho zK{TQ;H6i>7Hk+@@rBtomeY_lBIO)Cbwlq{pP4<2|*C>Pf354Ggri#{q_Yp>0Ulb>` zLvIi&fbJH7O+?^*xN!)AU&|O01W5kAR7qGJxyI!SDJpv7Wx4z_>;XI?^tfsVw%bkn(GT9fW8T!s9=9A{S4WxpeoaEA|l+QgjKxRpM~6Q|fXq z2_S^`m)h1lq~x(KL} zS1hyk&7Jz|K7vmP*9uBH-Vbk*TIB%+>Xqa0r7CCNX~@;s?8yAXF+%vcjA&)1THKbQ zd(1D1$DP5FS*xe2M1R6Mmk93q_XzeE?$VEkb?UR3qk+_FpB2~8iB;N;EP5{``@z@~ z4p`iI&vMny-di?F#lfuVY&K>`q1&OpTMC1G14GAETO`Jjzo3{)Uwn$DUE)DM1z3j= z7+@Yn=-A>_z`gO4t4+++gTz+ztCAUz(w(CWgOM(QpakC4B`H$cYDk&o)&a;|Co%D- zmC}KdKZURN!>}TUVMYu{iEVk{Q>q1iV;mq&J+yIkpJ%-ZW8~DlofSpq-e{&vUX@t* z7z!-Me|3;Q)n|!s!~_HppG}QDe30#gG}D#m3w?eIPU$AR*exifM8h+(rJ;uJyD3U0 zQUAQ@Q`kkpC(Om&$DhezYxLL$9UL-FAtVZD$CBQzjiPj@a-l2FHhP<+CE?MxDQP(9 zSMBV|FkC~Lsh0)u0lj+mkp-;PPTe(PFl*{HLzJMPYa0e%-~hi|plWAZ!g?H| zSjPwmCWZNNxeIr~#na5LF{f_^r$Zpd7-HnUDKz`%jMM$aoi)9HdXiL#-pXF@+}=EZ@gE>z%!YC z>2qq=?$_EFi-t9@q4=r_ThDU6-Wlc?R6h*s`J697(M9}9BH9>ju6M64^gPGxDYQrE zliT;ucng~z&6ax1H#1fNREs7oEv@~_o=qE}U;-$`t^*0!b~k71N8o_q!btBk)Gtm_@bBLQW6%&iCVotIY6d{45`T4#n;Q_Zcu80%iNvqS@Wci&Nc zwn9+G6)emkow8)N>vse6Sg~kdHeZe0Pm%|}gXTpPsDcC@M_0y9gpSM2Q~v!=krjVH zr*N(B{d>ZLrZcrZiN9`bnvfG-=*)1}h9^5)ua0SbVPeAXS-x|pdOtyk6|be72(>va ze1#oo!uzl|_f7Pa3OjKSoA2PuOJHP+%sM1PPNaQg( z(BJRf_a8ayBgVYSZ>Y8h8m@5WvM^s24!Q3cH09*mjA}^|c$!xg_RRssKj(Stb$tLl zOc-koKV3kyIA*Tu+$PC}kI(xy0IZAMdlXbxCsmoV#$ESy<$YJdKg0%O47<|k!!>V< z2PskG+>cj0xu#6;$s5y3J53BnDPv}g*}JU_Ve)jbI`=&`1+YyZLAoBe%=S?A=&)!y zJ&p1)HdD#XF62AiSJ3Ej-5jL{L(Jpac5W55eCuuj{KT*R&GmoKrQg`g{WaCG|$HHNmzo0PQ_M}MTS$6h?43x2#0 zq7kbb1v(+Kw@xyfA!TkVCew!A>^fP|yuu6M*9=j&X!7sq9_`e@FZV&Lrqg<}EcdU! z44rRKmzFTMNeuYEMK7a)Z&2r?n~U0S-4%Yrg%!;390Xf(>bq>1f5S=gPc!`hZ8)OPt&=?Q@;G zyYEm_cc>~_*@rJ;;|dVSVgQFK**NL9|Dg@=z0|!VmbSm zW$YF!&gS!b*gz6j#x;}Mf zxSzM#2dLf)X(cUxX{3+8wGxKOE0rZuRS|=je5}8Ta?>y8kdXSVx;Ebalk%#4nEwPV zK6-=NiYK^!>IYQ4Alnr1(~VCTAx6zy4&sr|JQVR9h3V{Xe*^>UXg|n!Og$BFB#^Le z6dGW-QeHF02RUf~_@ArbS{*xR6TofW3gmna8jCMR!)^`me^hb@uM@}^=8;qH2tnuq zohml0vMo#}ifh~fveMq*RzzdiOD%kg1Z9DxEJ{Mx=Z@sUnV0tBfWAk`_$5jqcPXRs zv^ipOvm8%F;q#e<#S}sU=BQ7*$%&e*9e!-_ATgs?f)M8L(5{N9FCPsdu}w4bVaL&f zUWXTf3gH2}wCfyxY^Uv{UlY{@U*D{^R?n(DXz;%xS#X>(Jb`7}3WNXzR|DUn9{O*4 z*OGGs&vM@>on-K;AloIyJo+ZLc(7lJdVGOw_rddH6Z~C0XAH8?*nqdot>Ub??ijI` zxE6Dvz!MN1Mg`4KDJ|Nz5F~S4Pp0mb0=LJGuZf^qsluq9t3Z}n;i0P z_oEaT^Rc}FSGOTjKA|rMiXsgj19>Db0#5CynL%ZuMg$8U18yjKvHuMQD{nv^18s&M z^R_z|i{vc==)KhsE}j)}R(JihUb||TLw}Js0}-W19&Lz{#j$zXvfB_4TB7T*8)dR$n1;JomFp3!xaWFpc42fKTwEhZ z)_oD)(@Jfl8LjT~aN3+a+6&g?7P=^ELjp{)v|)qvE!f~zqP1XTy-AW??lE552qI!*G=%fA(H_?}x3sBBU9 zUm?X%OpKu$VbcBQdZKPr3rGIya3*p_lAz_zzMe97(etNZMf7cEm-(6>t>i0l3CcV& zA;&T*8PJdC1aU$ue7#>AN(c#bFSt3UJi=XR_CU{YYjUZoEVt&}1Bf2LV17y6nTKkT z1|c0A&UX`N25v2d|IV&4<(Td8)~6=0{4V{y2oD#WdOjZM^q8>xk2<3+*kvCPh@i?L z5IMNW`N`RyR11=x0|^?wDh+C)Qy8A@L+RG0%Vg@rhy^hC9uf!_#BTXGgr`xr910(W zuWY4s4gUnF$n~SzBBmbjqkkJH^fv)cGX>kM#0{TMnBOaYlVJfIS)ezxF@L|3KvG9m z6cIzcAjQu&Umh9e>WJY{^t!vCt%KCtT=u$J_z8oCUYNJ@xza^TP7<4&$;1VGxk=0; zFeLS{u`grDBm~>?)4a3A(*9uX=bDdQDgvPCatF`+0{6E$?|i1ZDfy*>MSdwzchTaw z|DP7%S7h+|Cgb|}F6pb^%HLq#AP3tLSrGgw07c2kD@@@^Gay&Fsmt|yLrIsu{XR{r z%!5W0L2nDM+teb&^ZI9D@@6~WPfE^A&|3lOjZyW&`zYP@*DBs&v%nko7j7z)=-GcT z1ixYfq=0Af;hxl~fn7}&yPpj~M}!I*tUjtNLcf*-lL7Fd)0-zYSZtCLHAno1rfZaT<`}RwYy?ebRg`w4j3shYs5Tr`!eq>GtQY-JJ zD8n^XBp~md+423@d2+SOZO|qjf9++Y|3cO7b4ZRjWc97&uNP5!Ywmqf8#+57<%D!a z8QQhSEdvP;YwvV=u|3or88XY1blWMdXX>;g%-N!-7;71ZN8<6{r*~bF{Z7F&?n2w@ zocgu?LR9>)+hbDvFBs|$9{(BYsFmVnd}*(_#`GtoBpvM;S^=TU;1RHiFf^{C5vs4v z$1L?ek`ah|j)cR|7~0Ax`nUgiGOX+QvD$xJk~KqE=Dmjv z1PW>i^F3$?-k?iQi?_iEFq$@lOeiMB}jf9}f z-V;TdO;d>m8-TOtVHO8%QGMDB?=K0Fo!4?_aYBUcJGW(7ESp*3%NY7P!`y*N+g#={ z*kV8ePrux2%~U9H_i6MXt?fhaeg?+P$I}c&EDS>G+1ibwC_ra}Rz(6dEdjI6FQBe+ zrLk{)fvMToyk5cWFdpD1z=QXZqFGgnb_oWg#B~Z?Lwb&g$&b~cZ_TH_!!JK?`z#E3*CmKz0X`gy@| zC5Ba@y%%HIAMnm5K*`44cqLmT?>In3S7Pnt(=HQt;;lWMhgXy$Fo*WEO@t{SRLsEs zWNnM*3N{Y?Vaxjv76)%@U}8yuu`2j z?i$|a*Yx!0J4y39e4voGq$Rk}X(BQtO{H{`Kl97y@9kmJ_k&mN+caZd^oR3Lt(Zcu zlQssEcPMjeTE)k%yD~!AUG$!|Z1{Ogr%xQ42Q2L{q0N#8z}Dq*NI>=0RWiSZruNiq0h4e2WH4K&mBNl}9Wy1zn|GK420YE}Y0@ zuT~`^XnWX-Hp4con;fX&Q)d9nxirFR~x%k)Ax zA9`7CX5h1k8u!w9VrW`5hbTjS1_)riupgNw6aZgN z78dLA2U86*2yw|LXM_EkQ!*#5hihyiF9F8jnI~}K&Hg&crsd>od(oOtK8dptXYo3Z zp=4(aNugb*!psV)-ZV_s%DVNWj}a1M*nSsp>4QVuA0h}3Q|_A`k9CUICLySBt?Rq+ z<+kG2tV+XyZZ{nR$7LRF2Ck8#y=G1iCuc6b z`YP$*{$E2jP-qGn`%2x2SEkO~mLzNN6z>0&DN0`iw$dS(F7HJLz{Lvzk8eC zW?Hdk$kHHoC2xvrrz5ZtC-+yG=-l;D{f|}Q_1cMH2<;-`VR(@X`3c=3!6o?h8({G7 z=ar<1^4*o?dA9j~%-rPG+@5kYGX#mfi52C+O_~+-;Ho@b$`T8(A^{h1zsM2Gw{Kox zhv%8izTtG1Y)~JMk1s0PX@%TTOip;rjAhMJ` z)DQSqTiD4n(aP&Zke^ib7QW%_`UeN9D@9jadTC9Sb2+s@n)&*A!y71tWHy04c)ifjRO_1Mb8n&cyJ`L2rHi4$8Fl zQMjj=`2b+&BJsD~-U$y|^Kkt-VMZFSgsW)*E!Sb$fRRl8`Ed8v)L!vM9*+_z(+3iH zJ>mmSen;n7Ry6*_j`}go3n%BT>Z9AT66n(#Ovi`UwAX+C?FM`49}U4h-|p=sU0L}E zecckj`eKvd{nSt3u`J`?Mc+LS@{L6h)*(w&k~?Z3Z!keO)~``3r3aTCRj6|L=F@y} zn(7JW-$RZi-}7HzH-L(>{zUbGI8Ls_}R{Wc<579JF^5>RZgbR3feDP6=prR0G)^2 zR$^%!ZMMqVf{+s28C3X0=i>EWHpla<_TmuyfU{^2!A)c9%T<{kG+R!7bfYuJefN$M zedD8&AueyafZYa%Xog$B<2jri^i>${ewFYAmI$j#CBv@KBhECx-pCCk!Ik7YJ@Ic4 zgw@f?cU@{(e+Tf4XC1ERn_BBb-*NEl;znAb(hNuCfyje3dpSXN!`QYdLE;Eff8T5j zqotvXu=cd|x};zVQ)-y++AnvTd;6{_MdkiNlv)XoaYM)O4;2uOJ{R%*xsXU0WO?tj zAJcrKkxd>+nyKuXN&*aK9gl5KVQP))lduX3exqRU|AtER7;wM4@9sjOK_Ny2B*hR_gyb{D_-N4_V+#uC!vB#Wo(QgUcAA%xnaVgMV zp{*;-M~lk!p|w_hQGxe2SF+O|BwSFpoOjU!M`GS=nqsK*MhfA`yh*`Zp2rf~4fyuR z`&`iA%yE-8wvlOSTV5Kj)_2tdHZI-Vy@RO7_i6XZ_og3b`$xZ*nY8`bLT94%^c~(q zqt8`BH1bd1fBdyPpF%TVdk!nYE9iqwqI^c&S5btL2f-|alG}|i0fL!V+fMQM!iKwg z!q1nbqe5|oTLf>>ENIYQMdckv+<-iIpgxbY92mRAt)tEZ>=9p)zRRe~4_0x<{Ibb> z{0W@G=RG>B>3X6f&aGT&csxWZhJ)$2Sm zTS=}rJ3t(E!~O5KO7&*nTVVF|vnGXnTH`*sbAMq~gXfT>-;!5|yt?F8hdp~+oj$eU z*iKtxNHzRNe{7@f*d3LvbfZ$2H?L26`5^nz&u>K@jhRcS`YMzKHo<1@vx#~Hz`hS} zCnU{Ii5O0gLs_%%;M{uqs2q>R>|yky<@j|>lV#)BVpoZ)rZa;SM$4B+n^;v}_;H&8 zm@+~71~eQ}?wyznxi&#V`mUoEi`08%omfGt0H$N{6nqYENqftSik`G3*@HIL44{?H z2S!{l9Qm!0+DNzgBh) zr-yk&^CB~jLrasGN@W3qe<*J)Hu#h=>d@kH^As`UJCaDjO^k4NqK@Xp`@2>jU6`@( zvEGhzu`oC?9wPB1)l1|6J@Snp-}_=eDVNj}yyh7qFy(WKa5DY!dV{yM=S?81n&4!$ z$PC=ppjNuZ>sH;#@Q$icjKfy@_9inrAeC1Wf)BLv%e1{&LB}$DnzDaK1M7d?OWQ z3>G8AT=d!5QAwGpBa440A5%No zz!6jKxbIbkT;{8u-u)R@x0dO~PphXy45qh({Sj-UR)BN=1qT+7>B(p;H*xN-~ z!#QeLYajY(GhN~y;$Th<8Vq7V^R0VY)`V*$H+Gw~N0xrGUVhZT9WbTfG$rw1Ysz42 zD~XIl8IKp%G6Z|fZr32$u?N;Zujf(1!^aIYFLRig9F!)=|36dL%EP#V!T+ZQWSURl zxZLO9@r~Y9Y4NHDnv{z&DeL03tPSr~Go!TFGO1$1%uS*%gm5gFvrlS!B$EXR26Fy> z590w7#bMpL$MAAnA4shB!P^Atb0Q*QAHE1x7Dv$aO*+uZ{PD%AU`BvTuTOOv>=3yL zDOcq+;P2l#yfdIqEqA|anHO1NF}6d10;*;6$s2SMjpaQD95d!V8vXxD>ZtjC9*Vef zc92=5W_PF2%(kEzHr+YhCX2Z8dkj-kp}tH01IkuOtqh_BX_PEt z{Xo&M_J?@#k9i*jnTeMzi~^x}$#rjn1^$jSfv`TP8+hb$k#>L_&PbrBJ%ch=hLgIR zjOB^=;w^?n2&}~4K0DhtFmrMt7cPTGi@G;^G-85tY`^+ZEkEJ0l@5>tjxURv5ZF8} zYxH-~nTimHwWmy&#TO@KwEcJEzf!opf-VhoF5V^1t}nc^d`SS79J$sqcWM|8nNU z#fx=fIn6{vqMUs~XewdC-53o>OyMUfruoN;!{qR5#A48r_r;BQ^VV8}p^FQzqKy;s zvn)^`4O-?jbe2BT?&TH7PaOv0<~8}^O)dE_z25U!F{&QwS!O?NnnKN0DCx;3DVHSG zv0SfFWk)uUBsmMeZG0PFy_3l=-}uqv#V2qB(zE|Tor$osYHmE=#)1O-*nzxjehTfF z(%SGI{6=lU{TG8M@3?it<;rzD(*I)3f>1jdaa`;2kZ^~jtK-77oBcbC+kt&IlFST9 zi4CF963h~uQh$8cQHCgL*N>AKy#SMDh^Uqt8{gr+3MG;Z_o#uT_|ogYBw0h|;X~%t_!D-(4_0|!Dm~?)B>}U2 zdBAR0`+CPh9z@$0lf2e3)fy!VYgR`$s@j{1ef{^tna;+IT1`jc0pGzDbAQgMOByLG zLWXZChz&SUJHAW9HPjDuh;v>EYppLGCv24g$rc!+_l+GxcJDWqhjxLTsF zF7ekNqe7>Ld8JY-+t)A`5x2|DGqkB#<^r2aMV{c7HduGHEQCv>dePaRK-3gK_Q>*A zCP6S)c65reQTIp909ONLv0;wj?47bSv{zFdj!pELH@)z|Mn$vA*Sgg8i$TJ5nvbYus|Pf-AA3wV5acE)AMRtRC(Lk*ZJ zvUO+%Poaip0dgSSekj%(E6aUW^jJmDmq#*E4(>uoax-@KsUCC5OMB#;>v0rY$mQyX zAV>>(Z5a1N_L~g%r`8^h6PT%ZK7~ezkASO%Sd1M)g1MeQg+c`c*Fd+$|EO2;4+p*J zQSrq#-)ak?_{-S4;is4`gPkoZdrirsE!c0<{^4JO5>HIw|9eL3pNzyKS7_+r)4{-j z!P~wQZr{e^isDvf+>6{!Ns@pL16K9sE~XFqAzSupxsRQGhL`TSC$wCG>(`=UUHv~F z57E5LBzyP}H5H%9J zrQspQ2b1+z_ag;K;@5BQLzN~I>IBU)e2SDgc(NwP>8N&Tn>h#Ne`)zP zq)rScKWw~x9x-q?7swqD>U~81oxPFu>WGAV{ts1O&2bQNTOLnI=Qo6K4u>hAkq};J zToEilGqlA+c%?I0I$&5Mn_`3U{a}y~Ly%^^awD0v6*K)MWchmT5vfMw`gl_N3y1t0 z37q;5H^;Hri<$SmRGyXZUgAsUFdGqCS$o_hpLeetWuPIpam~enF#71HO3{b>f@9K< z;YI2CtzU<<|0P8<42cSQd|7U1(0KN6K$CWScvRjiSw9%mZnvC)*DAS#>CM(Ln?x3> zarGYA&QT5?p0HudAQ0sIXTNi_mlFMDJpX>@TzUtq{LlM_@bCn7V465Q8;DI{{g@q% zuS@Y^HyvhsSvnDVdivoTUF{9`MB&&mt)Ac-%x(Hr!=iA|qx+TkgUyxwf!Bklgqd(e z*H)WAT?vI7G$XIg(l|eqMkkiO zY|6;m*XhaKSih=Pebo#hLTkR1p|rHPjSaEDw?xWB1LxL?BazSedQ;iFsVi}vvUIgk zy|&Zq{KRp?Ba8{XhX2ba6T@VKgZ(laIm_>Pi%@*9)~F+7V00Q_=If4 zPsKLo)bY?iHb0Z)ap5gZe^DNZ`n}$}>L8p!f$f)#!^pZ4O zq=(89bkYN=$S~=)jpZLnI!_qx1%|!+DbfCbP6U!|%+H_7I&A&FS%lR7DTzbRjQd!tsPS|heC z8okp@5!8BE`M{#aR`y2FkpkVj2t*{4zRB&DIK83zL0CVK%OM};-vbf>eUSPpAxX%e z%6Q}p=iM3BAlKLVITWv;&?tc)Ui|PBr^;3-n8GAWp|H%!!2xNb60q9TNdouwq972d z>v<{u>vMJTtDenHZ?ORkPk5Fnb}uu};*b;?@gc)-N-Oi02aR{Dq}@E3gKtO4*F&QFddhx6Nf>MI6oV9WezT~7hD2<=%fEGoHOFHqlzJ_o zQVrX9Ybx_XJ;xa{-Xi^%kNk;vhaXyQ9TOT4|#4PoX$LlWS

1#0;( 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

rVi<7I$d zM_~2un|kr#-L76bkj+0)@P`F>5*)P7KbWI*AJ!cxGyKQIyF-r2zpe2L*t! z9zbCP==-r2Ag%3Z2mbh-3S8G(S6VXmC&O8i1>2BS97!lER6ESZjdo$LA=DYGV{uN;69A zKS04z+Q%iEKj8;f(#d8?#nu((g77$to(%0fmqKrp~G zzH$pmEl(Sn3RQD*j@C|8uRMBa$M5zZqVW@}Apw>GAmdS5+bbt{Vf#_GRIhbyI1zd4~t$)-)Hxr@oCvJ zK-wgV&ikeSCil*|XdEE*K}C)_FPe~42HrKqQZk%E)~l@%V*s$)67Ki@%l3Zw0jJE# zuNvbvw>kW*27uJB%lvC$0VvN;I<5tb>a{qzhZPoZd01orY~kR{Gy6$tW$s=V05G6= zKKB0kZy9IKV+KnUR{30dD5}H6=&(XRDH}928Wn>ykBEA_GNio$=+$bWA-o&U3oypK z|JLhges*PsZ2Xp$kU(4opg8sB{g@7bzvK8rBP1A*tQHmnCjAjBU&4i`eo7TH_Ip^m zOR`pdQveYFU{N7hi&6n#Y$G&4YwNJ~5AACo20;xa6MHcR04p)0R`wlJd*=ScSof@E zDvGoh{wDR1^7&H#$Ek{);GP%r^PVN};=-s1jSWRQtt$Y8j;6?-nVZHkWO>|h3p4Iuw8b9(tPWtp zRzJ6R{q*}*c8ZMOwj2pi4FDMvuUx|MqgpB-QTZcja%R%%JbTGi^Uqmw z^&d&fUCt3Uf1xR*`G>7#^H&S}q+1dbSoa^oQM8IDL5|I)`k|W+PJeE7q*;FJx&VM} zmmj{ux%zrKP#vj$Ja8c{X$q}uB5~oI*=9V{&py!sm{V3jq8=mx-;dwQmmG@dzqaRX zIsfR}p7O1=9^CQw$`Xh*05GZ9gw{^(Ll6qOM$bt=z#OtzA%L{Mblm8~DmJeUDuIm~VG_ z$M~i-k-#HgcH{l-JbwIkSDPv;>}xx)OPObsw!-13zhzo40t#XOalWygxDXU*>@ozc zncx68Pnd}-j~v`_!U8Lip8klq**(zH4A*s=qsW!}O1?hn&aX><9qpHgtH>T8QmE3v->TkK71M(CJ85 zM#nA+hca-W)ZJ+e0Q+r}RRDWu1Kftv2kdk~R!GSX1qX;&!n_;$d}&BU1lQcwF%TFt z02|X%OzoNdTVu_SAgh#t<3d5M=Ik}~jXjt8Q=0$bzE~%)5~A}H%stBxNG*Wjj=m@L$#=$Q~ln-hxtPtccYlX}z?fc9S0aYp&K2%z*#SIWQD33RYxZLG?e0a*R{Hp&;K z_RhW8823xamX&OFX1`^TAA&*Y#1Y)%H3?9&37pW!u>60U{&|L@nq53N{o|v^X*thy zm^#aOz5eXB%jdu0Tz$SjBTuWQ&mDfVp1!a#P&AaV10bsfXo&>)!0{aj(Xt~A<)48A zfGE;qzxj_&RQ5TG*G>QN%1d$l#^p#LTLnOXO`iINzX!F9IwNU9k*{wYwzAAfbNmc& zf+H=Z2Q-6{0S0K_;(?vL=K7!rkhNcgPntu{qWACu=H$a%Z2gU>(U#{~bg#TK112qX z2xA6dIWuYXo}Jt?|7>gP_my-V0r4=voeh5;&n3XE09i~AGM~{#1r|k+jdF6+H;MCH|fA;UnEaJ6K>FkkAKPVqt^6b%??8wy49p$S!5`#51m{v-@Q zHLd|z*~zs+Z<~C{hknp5o%}qe%7&;Z^`Mq_-_nn^$_0*oJ5CV5uz3-`z%KvAEp zZU{SCOg~|MQ#+4b-*K9)l%YiasLKE&NC}%}tOB$W|K3?D zgjLGP%1s|IifQzzlzK4s)?06wUblUom#G`~=*Kk%pzHWY{3jwyG$d_oKq-nL$Jyw- z*g80+xp)-KSTHbTh+zirTP_38l10l|3!Jn;Mlt$F1Ki9VUSz1&6le$=WRJ{3AL_q( zg)?T%0Bqc;KDBrDsBz{Z>6eS^`2{N*Rk5&~7Ab=`f0ddb#zHCj9o4ZEU}h62A!^d! zs+xH2E&F%;(OQni+5>=HbFVbUyeZ9m?)Z6HAnml5*9-XBTdECU^yrXHRoV(T)O-(E z_fl;@W9O{<;8(8SxnnH{bo{Mu5MAE!mCmZM5+BP^%C+su`R)ZF`5|IipUBE!A5{EW&pZP zly$yqYR~LpXUx+}J{Jif0(6nm7pHBq>i|UC=5H%?7i9x88yvj`+VM96`cwx{RrXa2 z2WH;9mLgvd0PqH5%rDDMs>U&!$bl)4`B9wQk`e@~)n}6+8XOKJ;QNqs#fT6;Um5%E zqc=>yU@Zr9{H<;hP-_6hqit3Ck}@ngcg8p>(FuK02D#BVh)s4-FBvvbP9;QeItl=g zQ;GU48H}P4X>r6c@{$6;F%aNOWd~XX7%pn*02-J<{q?v8pxdNbhr2G`JNt*VGtbv> zxTY%VecIAmdRWV8h2M(Be%fSLWrGtSq@TqEW;$)`8y64Gym~Fgy!HU#^7%hBb^ZJ- zTnX07i%iWFLheMK5|~m5aqHl?YCu^ghz3EGfc||pcCZqFHYiG^0pXjKz4q4YreD04 z13LazHwhF0z_7_3u4l!80fe$>0>G#0r&?AGlzI_?f&jts{mSoywvLhl@aZj4xQ9`` zLaR29h@)HO6f31$n?B%P3EEPA1^hF1weR!Rm~jn2w>h&OcTMe`y~a6nRqOl#1sFE} zn)gU%0Ki}?8(0#6p!c;^3+U1$GeALRWz4%54((jifN?zlz>UVaCzF-PBOgB+bm&hq z7Qv}1=Q&H^lB1@4K37S)F0?2surZs3vURU094s1o5fVkTiI9>u2=X2L?XTQ0^Iw)RKI2DMNdgc1 z51+jM_wKs$_Qn~BC|8XFG;UEd8p@_3Q9Xj^Q$Yh*vchZgB~C3X1LCv_HmGk3U}~@v zAVf+Ce)A&Ol!*Kn8Tvndbd@A<@k?ees85=&4SjuQZwzmOa3zcSwZbHbrZtBqqn{EMVKjXOLzG># z_0ZkjNFyE6Fd!``jdTb|H%OO&DBa!NA)NzAcXxMpGt7MR-tXRjF!P*qcC5Ab+R-Yi z`=f9G7JBVI!tQ8dM8)s)6Q5hf|MCKUs#$mp-AQd;(=#DOz%e&v`}K@FLhEX9{1%^n zeMsr<|2hu!He3V+(L@KO)SjpBE~>lBqHKmD#fDcZhOn{%cNR`_Ho@}VK-+RIPRexu zB|UU6*tzYyMidLI?n;zr!mAN0-qgeOVp*d|39-gHvW*o4a5>_=HS4A;@AlOqTCgI7 z^fWX1pDs}R0Ke#AA3f=>Gh9toNZjO_l|=g`*~8DAtNmD1ZIaYiN0ejWQ?2!(X{^Eu z#zZRJeucFQ2|nMmGSj1a$d~x)j$`$3Sp^NRXJdd|-GD!`$oBYGRwnsTxz>Igil%#t@z zw5;iN@GCDb+GfCRm0+d4mSu^5q$6)i$KAlW(qIhN1F>v`;6kR?EhLwcf4PunR_Bv% zv8G>+urjsXmJQ2n3LGt9iXU_?_Mpzu9b44ABI?YJn^CdJ<@@y~0xpok#y6nR>vS)! zBFCknnKP0`#~sbUcm9mxl<_vFv^#x3W0fkN2SGXLBOjiDEeBU%kigqvjUoXz8zN?R zV7b8urcv#AL1JuWpwoOPI*kqvz^TOfzH=!%H|qr8Qdii~^%cXI8Mpf*sd7H}8>FWD zGgIHfxl`VEvEVzpHU=zAcfi0(^Sbxj9ar^!U^Hm{Gdxb$$$-)q_NeM^4?~Ai`fo=iR=rU{flFhKG)k#ql)m%XV(Fbsv^YVACqrg^%oZ7f}x9J2RI@E zl`*VBKcd80WS>R6mB0J~E05T{B5z%Wz~z5I78&21H|RWtVQ9At>Rq-t03&W6o9?Jr ziG)CNKJFLhnVgGP~;}$^X5c+>M7<|4zKigbPfEu1J2LU{eb(h3TNsQi*rc9dYnktz6G`Gsu(}FI9$- zWvC^?U8GUb+H@7Fh4Moy4ypw;Q7u#ck!jRtz0(n<(e)HFLjGwx|BM5Oh zzo5i4H+Wio*SI|3*bVxC0$BYq9xzGfL2O&!3)uP+1ZFXT7RqJ(GDVjq_8)ou8G$Wl zv};~yB6Ypu0y<9j+5EcEczgJN0W;<7dCM$itxV-Z4a#7qng^SZw0X#=%i;N_EGl6G zRGXzWg_;J3>u%+F`NRaD1DmyALzKC5ec}XXc44=xZ997bGcFOzt0Epiq!wvBXQ_a_ z_gb8X*qh_akfrbCih_yDw3|>egEEC01{5oc~6j! zZpaSMQC&bB+Y!%}ZgT77$iebI%k)a^o*A_;aheD&$=F>X%0#{?$j?amNx2*~-; ztv~gfO(50&DiUR4n>IXRnaGPq^9J|rlS4e&W@9pbs_*m}w%NF^dc{W_Jn|2iGM^+< z=Mk*35C|ziocX@bK8WnSsL6Vl0hc%cSa&o(fh8P*B)E=eNn+x`<4tj}rsOgpg8D4T zi^?mGD}_9w1@0{4ofR~0zrE5s+59hfIbCPBsw@nMbLwCI5ilZN*#WJ_^4fk3d#341 zlYcsfFL`|X)SOC3Y&gJ$O#J##5Vr83u+@kH;7V=p*WpWYj^J(mtsedC$0ZehH_IBt z3RS@aHPM+|RVJU#-$3n=LFRp^I!|zpD{iY*m+8+ChjNK7TMkC2>x<`{vd?cN$6^ez;c)u&=34UOLD^YQ4LBy{tBYC{cJes{50(56TFN39mmrN~t zw{K%ci@p5bb>{MXaHf7eKC}<@HL5hZdeofk*k3sYp3>)BciPeim36uryEe81tbNBJ z<%v#{Ov&hvH%qf$FMPJrAWUsJ$xrX(;`YxHA4}RAYp>UPckUbq;RJK_GC0D9qd?! zMPXdaeox^V4#(cdd-{VQLZha}I&pp+qxW1+H@66sbhSv+wZ9)|Il`OU@c`X(8_SG` z$QJC@IIaq=e}{er;q13)V7ZMRZ=#P8m1K$6tkKtduYhTm{-B9E@5kACtkr%h7w|v- zjsGA0$ZmGhj-sr2u+=X2V0ak;k+&OxBaob@f#WSu`O_^72(b&-=n={OWe>fEg%`%`L5>QcXq~riKTjT8d|!z^YT_wRE1`StqBUiH;!Oe$kalTiqYx}Iw1TC(p>0#E z$A?cue1hw_)J?w(Ku;eue*ts@;Dea(tL01p4-=famgT9&js?8^AJ9;jGF1^1;ePje zAs^j2SI=G+J@#!A@SsFlSe$WDF9UCzv^1KoBjn@%%kaPkqwNpr8^qN)+-%2FhTh+0^0)i3@_?t$E{r@(zm^I;^~tSQgwynms(q;d8gh^dN4 zEPfa}cb(`==D=YTiJ@foZ|8V1M*A*FDy%Jyt0`6hDf24=|7dZD($y2Ht^ewNJ(s<# zF_Fq--@O!Kbth&e75w{o!X-VDWbYF`E_7WEChA9+j{)Q1t2CkI3U7n%_RbLP z`iQXN0ACsBsJo?fgg}(@^jCt@vHHJ^fqHv5LlGQpj`=^ho@E}HdtFy<@=5-M>+w%PsyyT^Qws}n7YGKsoR=4_(0*;4Y< z8hX0aIhG|nT9MwLM95A7h~p{{R^hiRBW;{L=rm$>k{@)ZCoRX&Z@8EX1{%QL>V=5q zSdtx3ZB3W~Jxt>*N+abM5StjOzOz+(JLe@LS?x;A^wf0^7v);_Al+;Pe7iHfq&7Cz zM^o;!dU%^iwH(+oq+Hp~s#z>o7cto*wv7wV@AgB_Aw{)fM|WtqRpF#eU7jiF)bKg{ zLV75mG{R>RJ;vmb5zvM7w>t2xxROcoxp}=}{V>Y*)kW5JJcsja^SSbW|00a}T|KFf)=?_)w3*8(> zNeuT96*ndYnw>F|JWPt^#rVQtziPznS8BoQGEnMk6VJg~mod3^NkQB=4?gD@R7`c~ z7HlLhTWws3GVIuo5aiVL_2FC|XX42aXYmN+ZUQ(kJ~VhUW@YZZ5jE$bP&}w_#M2{G z_C;8IhcAc~J^-J>%j%_+*f^#bKIu+FY*5l^eY*>CIV$s?O}h@D_z1u1G3nPY8)z?ZuA2bGw}T-`?i$kPweR$nprK@e(M(vFn35AtyEf z!1`+17y9ITi}cWeOY_-PanxHH-K3K{pdc+UrXeQhl7IaAuH)^bHz>`X;v>&sloc$6 zMjTRq8-~Ky{FWSiIzt}$xDjTv4zGahjX&-E8yq;75rO`) z0B#CcL&e^Qkx`frBlREu!zI3Rp!@(={YCu$+Rx~kfL+ci-bGRBr9pjX3Eq`SN-{Ep zk#ACj+=;)~wZv=*t>9F+-)<$ho_d4)0sj?jJYO@a>;isnO`E=WzGlh_dshW2Gbe80 zveC|+@Fs3LHH1mz%%kggdP_g03iXIj`yJ!fpYdAocDKn8V#e{4q#>!Y!XjPZALGe()9vAF+X=F z_u&yK-QogRk~JISTJ*{IC?#Jp5PspIeNcJ-jYK+|4%2mS!DAe@w<>M$v9b%1$rzkR zzZJYUdMo8i@Yg|lrT9k*K-ityG%nzh2WXt!(# z?%26Q!%%3Y>mIaC11Ir2>UT;?_*+|i)GwDpK+RwlfM#S=d#!A8+ExzW)TAOZc}!ws zTef*xeX%|wX+^GZrz;(;{H{E9kBWcu0lYUl(<9Mks`rU@2C5uXwXFGB>|!XWaXsF9 z95xb?{qIrS%kEge1qToW$KS{EXjFcNWKns4b8)b5bHiO&qRa@BebX}*a8h*aOjRk~@vbLa z$wv!2-rDwig12YY4j!U*a_w zFz{Q*8*v>`5xQ}3e_Dayu>JA!#bj&anBQx}MnAk&V+PU`929|GlRU;dT#yFrz~L63 zOipB7YpSwVQ{q%yq+_nK5S51!evNEd@odF#Uyp_=WM-eOC*eD8q>I#ga8RFlj2-P= zEVoB~ILzl`HhX6cHOCBki5cJyf>XT71M8C4s$Pv}qx3Y&z3vziGUKYnb4RT=JDD7~ zcMh6?yTkvBhie&Gt{_6^Vi%(bhk)6=BYvn8#M_38_&2J0ZfnBLi}Ra5B0>W^KpozS z9~Co1@adVLX$*6-al+5%pdJUUVW}{*Mohb2=e=V+8!U@B#|VxrZ{2f4~Q-Z^QC+RV?AtYe}81rqHp@ z*Q1_(pTjN+VLN>4En_E=7>{zb2-C8;J}I0u9D%7+1qKB!&T-zvrKj5fmD!= zSnX}e%aE1tCG7CyxZ1D%H(Ou()Xdg8e}TlU{uG{DJH#6+HrFwh7CoQR99GiIsyTr- zq(X!@pOX71iznwH&pK6t!4eL65SXyE|I3}pu^yHVrN(R|kn zOI2BejR^8LeN%AMHhr^(iJza{Iz|m-CAn1`)p)T2!vu{xZt$rbbQfMSzOPod8oLjG z1=GuxhZS@}(>g4bVC!HH3vh}kRBJ0M1(RE$zlf}2X=`q1v`n-tx{U10nMknDgal4R z2PQdG;8VchC>&vT@Dqs=8z44(A`_Plvg`J4!j{;te)b4Gx+AxXt;l0xMU)tOY^^3{ z4_2i5jS71h-}Qbz4Rz)6Ht5Ij_;Z36+gQbE6)aO^`>zXF=XEL_(_!Z8AWtK%JM5G3 zFJNsZpYI@OBko5-yY9A;H8ZeoR8ta>++?nsX(e*UJ0R}U7E3Z2LKAf(?*oY4K=5{z zjdhaqQjY}LWOe^`vikJJ?~Fxen2Zh_BkZ##$3BBZM}@0OEra9FZJ{lHHp|e&p0pjt zrCF$fJgeS4@rhi%VC6B%X{Tg4RkAtIOyX~9m%tgFcBj*VdYzt;F!ngNq zn-|rw_=xmhQ8$AzM})_nq)&Gr^kHmeS51si$-~%Nh3kdCF3}GufOK&Uc`^xH=x+!s zL8|&M$gd>-4IQ8PUP#)T-yVOL$36phqvb`TgKEYsc#xEVTRGfgUdEYew8sXEQM%zb7*!fZf?%gotBu{#_frQ>SgeDDjkaUwPm8#JM( zaGbQ|-`5z61UHOn?z}CoUYK!eb_2bM2s3RgAG%dSp$xg@T92x3?@@qAm)B^qJhkvu z1D6Ly{rb+kAt04_qbo!{N}J#j6+^q<&cIf*mP6`dwyg{#Em3< zzvIinPLKcH11#LFM)5o@dOi&~p)zMykpe`kldgx@jrH5!NvKrArJ?FX1#U`pPwji{ zp5?GpW_mq*I$3=(gths@**M~!!&zVyGv#`-B9rOeE+>D^hVdMkUHZseEFbW0OJoW} z8~s^wamI#^(Fq`*Q$@-m8WHH~>m!&}l^|d3GcxTkqu1KLfCRuy2c09`8drbzOQR_y zywGs>4|#;W=dF;2D}6ANlX*Fqt6F$K0l45*#}T3kwz)GF=yjf}tgCPCvb9(7(-Te- z_S?Mc`|5XgRs%VGbP$}#NLzQ7`S9gs|T{^ISz z+FR~xwku?C^RDpnK5qVLV_(U{<#8)fr=Aa`-5ODJGAGvr;%|}WpsM#nA!ds*{?qM< zKRkS#_#TV2Jg27b)S8kepUrpo+3C|_nhsyqhk$==aDgz@3~0UeW%HJU00BKBI2jve z>kUUu>sfX>h-yas6=r!MNQ1t@6h-+{Kx|`?Uc(&umpr(r9=mCVoNLdVRARXYw+KA;9+(h{QnnmA#=3)r4NxJSU3gOmwPAP>L@| z8fEE8+CI1{yRkOlMMZwT?%MT@k-O33!lL>IXj6>rKtBUQau~C=3KvFE{$hb)Az>_9 z$qXqb!kj`ZCJFvapYVAV6+r^K@#j1gCS3CSQeYtKp7rVfv;Z*WliwovHaXKrP?RPO z8$NuE_KSAUZCov@)Nk7t&5G28w*+`bpYGdN*sy3(ULs56VaZL^Ppu5=b4A;+Xjly! z&;U&&1M-CbS8~18Rjf}AfI;$Z5G{@maJ=NSfbi3xVa3tg?q%~ubogatjeRQQ)zl!< zd$@&_RansT4WLPb@lLPl6;#G4YX`CY5{P5lY?xBMM5J>Eg5=nkiM=x8>Nvf5gNJCE zCSgJ1k|;mFl#o?8ljp{yv%hXARf~Z4ii*59NSj;HPSW`uASBn42nLUl8qK0K8Cbxi@4wc=l4aiCf;Y2X;gL zN5SL~aJgsSbMX=s^9lfTqT2nW9bWl~$T$YXFy}%bnE8GCsR@-gY}z{;jzW zd<*JIsDR=M`}dlkZt?0&FF9o8)#`IDEerk+nXcy({{uL?eSkP7F>trOUR}0$AJ31U zPM7Z~zA8|DAN7YJMu3tlhNsfKf3Z zn@BHQ{mUBRr)0E{rkXnsN{3JCUSR^6Bu!_q zp4phRk@pjrfcZrCYrFjLJk@ZrPf@o)S5DVH)oaV!fcu+HeFDu+!hxGN!VJU(@|W;k zJu7}E8wBGfW+5G9EXIFZdaoW{U2Uti;<1f!00EkfRV?wcMLzvo5O*%f1yuh=A{{~I z|GP+>z|F^g1d;RKd=j%`o@90x)fI0-1T@oN`+yZ&Z)aKhS!09_)foNcZw z7#{d)Xc#bz^`j|)Jx<{Z30^>~X_t*;G02rRP=a?yQ!SHFGvcezKV&oNmM6hFFw2?c zyPDZfEei)jp;}iTnh!82#%E60RV~Nz8wC$(JeTSW94c>kBm7iA(~4xSSL48^cX0Es zrE|SYqT|Bz(!~A&gNxpe0pM;`H07?le?XHjnUq4%Fg_FFNGoLog*VFw!J7JaPJx!A zdnYLKkobFyNTZb@`M{fQfl>nQFD@MSfmK_ZHFo#K{insyS=GF%8LU$bK(!E9<9ivz z^-RBviI(k&-j?2nw$%%Do}e+OD;0(M-H7GLfOClQYwd$HcKu@nqgvX(M&SB!~P#Rp{y_l52KbXP9n9rpKq zlo9~Uq#YdBuf@o*cmSWQGq2dMx1JW*9A}%Y?b6Ky&3Ccgj9i`0K7QaQyYsPNJ^`h< zz|t2S*APm75wG{5@f#dY+#3^XJQ`valvf>sS+m81&a6rjtSBq%%t>d#Yjoo)Sr?B zgbKgH12#qd6+X)}$v5zIs6l-x=uJSTRqZzm6yG6_;n6o_;J?G2t}5j!X)z%jnfcX) z;(@nLTB4m1&;$2SEo6lFy)uLmITnY1y`OB{5? z8gAnyY;a@Dh;SN-=s%T+-1(5_;`&8hnygjFa|Xqfv46ZgtXkDfW8UltI53lb<>|^x zK;+yIe{{4ps}>ySXDlOCHsztw9nYkZ z#fqezd%(0++-$NFfK|H~+%?g;wB0Zw=FMlX^cqzSlPuvji_^Y> z!F%D+*`NR8Q2c^$TT@|s8z6khK3RhCmUMVxH4h$<5+}SSz(CnH5JM=ohx{$t4ir-V zq`E1mQykk_IMi|or3s#wN^kkQNF&aF+q5@UzLDH0xp` z0H(2G+OJ`SQw$x5-{`UWexvBj9myp(y-Vt7Qf91w_-BJ;(Dl)KUWz?fUqYhJdNx2u z?5+WH?g@Poi0$q^@;#Y6gf{azh{*NX#iSL=Tep7k47wxdRYSW?pU3R``M06psUX_( zyO7^U_Rk~tD8Vsc^S^}w9EWLPWoo*&MWMl>G%NB_O_$kk@gIKKf0T+}OGp8UhrV;ZN}0Oa3S z`J;!9E))EOj1drNqrh`)(if`7153ToEu~E!#ufPfbZ5~x8@O-raD}0vKU3XfDRlY$ ze&$P(H+>nHOJ5Osov3eecxk-XWBSW$)70eXb*BXEe5M$@Xqgat1s7^asrAzCx?~>l z@WViL`b8Rt=x~bP-6wER#N^K6C4@yzda1mpC|_j1 zGMlaa>wp}D{dJq!EIsq}0DTi(q3mn`uPFTzui?-Qc`P;Q9wfNzf+oLqUGc#T6U=GI zvWFkBlf81vRlv#h2aMI!XR@*)TPT0#H{r&kk$i<&6Z}oBJuRMvmQC08*s28Riv~K< zOn&7hN7jw|zaRrcKMf~Qxk9WR5e_7y*^LBU(;Br$zg4E4REh#f@9ug@#@^ChF9e;v zk_~<)eRCNqlkvlK2QQSKNK# zod4Wr!!K&t30>o^8Kzc4M{|l(uBC@cY+9iUI~3I$JQv>v{yc9LJGj-(?8R-wDTE5l zaaY>E=TZD*ggnee_fR{AlD~G_pn;<$8ARR#?{T%G{K#8W*?H13lN%EvbpL8sdDtbw~dixd8(*0=Ng>R4c&IKSIOBQHhz) z*fAmnGN4IS&30u>j^2kaj)3NOfNn`4oRtr+aHnS~Cg_178C19o&FIL%Sy?PBQJ&6v zBR{b&*QURjI=mOJ4%=q17YXP*5^qTzM{v0!LR}>uMc|F*5IYBUJ zWES~qakwVaWQ~%EJUmD^xMr)Lf00Yhj)2&MN*goGK?Y3M{0lFA3h&bO6H`UmknjO- z$Y+i=8di%fIo8Jwue;{B2Y+r0vDb7bkkRVhp!brNc$4$)e=pWxUU3q2sBPH^aa|NU zHvYQ(Rx-|pqV$sdB@-;H(S9hrOI`DQYAU?E-E=iaHJDi;~J4X%cKzKh@K5u%_Z07T7sI(~bb536N3Lmt2 zVjhV9F�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 literal 0 HcmV?d00001 diff --git a/public/images/touch/72.png b/public/images/touch/72.png new file mode 100644 index 0000000000000000000000000000000000000000..d6668ed60d3f3a12e2fe22e902232e439252feb0 GIT binary patch literal 3037 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 literal 0 HcmV?d00001 diff --git a/public/images/ui-bg_flat_0_aaaaaa_40x100.png b/public/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e6bfc085f51b392569e58b72d454586b900f61 GIT binary patch literal 86 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F$P6UUt$JVyq?iMILR?p^S|wQb`zMgg=jq}Y hQo)$KfKh^VF#`ir1OsEt!yO=<44$rjF6*2UngAlC6uAHZ literal 0 HcmV?d00001 diff --git a/public/images/ui-icons_444444_256x240.png b/public/images/ui-icons_444444_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..92214389270e76aea5ff5e4b2f4a90c101e3602f GIT binary patch literal 3756 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 R^Y1chWnq8($uV5=e*lq0v~&Of literal 0 HcmV?d00001 diff --git a/public/images/ui-icons_555555_256x240.png b/public/images/ui-icons_555555_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..4c37296071b0a0b21d099d70fcf9c616939bb788 GIT binary patch literal 3756 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 S?mz#2My<^4Pd+(;P5BR$ddn;T literal 0 HcmV?d00001 diff --git a/public/images/ui-icons_777777_256x240.png b/public/images/ui-icons_777777_256x240.png new file mode 100644 index 0000000000000000000000000000000000000000..de6cf086bbfb5839ab4f538b5de7856d915f8404 GIT binary patch literal 3756 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
Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/digest.json b/public/language/ar/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/ar/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/ar/admin/manage/groups.json b/public/language/ar/admin/manage/groups.json new file mode 100644 index 0000000000..61f3bc8087 --- /dev/null +++ b/public/language/ar/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "اسم المجموعة", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/privileges.json b/public/language/ar/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/ar/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/registration.json b/public/language/ar/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/ar/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/tags.json b/public/language/ar/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/ar/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/uploads.json b/public/language/ar/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/ar/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/users.json b/public/language/ar/admin/manage/users.json new file mode 100644 index 0000000000..8f948a8f96 --- /dev/null +++ b/public/language/ar/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "المستخدمين", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/ar/admin/menu.json b/public/language/ar/admin/menu.json new file mode 100644 index 0000000000..028ee61cef --- /dev/null +++ b/public/language/ar/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "عام", + + "section-manage": "إدارة", + "manage/categories": "الأقسام", + "manage/privileges": "Privileges", + "manage/tags": "الكلمات المفتاحية", + "manage/users": "الأعضاء", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "قائمة انتظار التسجيل", + "manage/post-queue": "قائمة انتظار المشاركة", + "manage/groups": "المجموعات", + "manage/ip-blacklist": "قائمة حظر عناوين IP", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "إعدادات", + "settings/general": "عامة", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "البريد الإلكتروني", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "الزوار", + "settings/uploads": "الرفع", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "ترقيم الصفحات", + "settings/tags": "الكلمات المفتاحية", + "settings/notifications": "التنبيهات", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/advanced.json b/public/language/ar/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/ar/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/api.json b/public/language/ar/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/ar/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/chat.json b/public/language/ar/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/ar/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/cookies.json b/public/language/ar/admin/settings/cookies.json new file mode 100644 index 0000000000..cbcbfffe37 --- /dev/null +++ b/public/language/ar/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "تفعيل", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/email.json b/public/language/ar/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/ar/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/ar/admin/settings/general.json b/public/language/ar/admin/settings/general.json new file mode 100644 index 0000000000..4584dc9c3e --- /dev/null +++ b/public/language/ar/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "اعدادات الموقع", + "title": "عنوان الموقع", + "title.short": "عنوان قصير", + "title.short-placeholder": "ان لم تقم بكتابة عنوان مختصر, سيتم استخدام عنوان الموقع الكلي", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "اسم المنتدي", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "عنوان المتصفح", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "وصف الموقع", + "keywords": "الكلمات الدليله للموقع", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "شعار الموقع", + "logo.image": "صورة", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "رفع", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "نص بديل", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "صورة المفضله", + "favicon.upload": "رفع", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "رفع", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "لون الثيم", + "background-color": "لون الخلفية", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/ar/admin/settings/group.json b/public/language/ar/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/ar/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/guest.json b/public/language/ar/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/ar/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/homepage.json b/public/language/ar/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/ar/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/languages.json b/public/language/ar/admin/settings/languages.json new file mode 100644 index 0000000000..581e028ade --- /dev/null +++ b/public/language/ar/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "اعدادات اللغة", + "description": "تُحدد اللغة الافتراضية إعدادات اللغة لجميع المستخدمين الذين يزورون المنتدى.
يمكن للأعضاء تجاوز اللغة الافتراضية من خلال صفحة إعدادات الحساب الخاصة بهم.", + "default-language": "اللغة الافتراضية", + "auto-detect": "الكشف عن إعدادات اللغة للزوار بشكل آلي" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/navigation.json b/public/language/ar/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/ar/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/ar/admin/settings/notifications.json b/public/language/ar/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/ar/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/pagination.json b/public/language/ar/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/ar/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/post.json b/public/language/ar/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/ar/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/reputation.json b/public/language/ar/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/ar/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/social.json b/public/language/ar/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/ar/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/sockets.json b/public/language/ar/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/ar/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/sounds.json b/public/language/ar/admin/settings/sounds.json new file mode 100644 index 0000000000..6f49e01f91 --- /dev/null +++ b/public/language/ar/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "التنبيهات", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/tags.json b/public/language/ar/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/ar/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/ar/admin/settings/uploads.json b/public/language/ar/admin/settings/uploads.json new file mode 100644 index 0000000000..76afcf9660 --- /dev/null +++ b/public/language/ar/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "المشاركات", + "orphans": "Orphaned Files", + "private": "جعل الملفات التي تم رفعها خاصة", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "الحد الأقصى لحجم الملف (بالكيبيبايت)", + "max-file-size-help": "(بالكيبيبايت، الافتراضي: 2048)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "السماح للاعضاء برفع الصور المصغرة للموضوع", + "topic-thumb-size": "حجم الصورة المصغرة للموضوع", + "allowed-file-extensions": "إمتدادات الملفات المسموح بها", + "allowed-file-extensions-help": "أدخل قائمة بامتدادات الملفات مفصولة بفواصل (مثال: pdf,xls,doc). القائمة الفارغة تعني أن كل الامتدادات مسموح بها.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "الصورة الرمزية للملف الشخصي", + "allow-profile-image-uploads": "السماح للأعضاء برفع الصور الرمزية", + "convert-profile-image-png": "تحويل إمتداد الصور الرمزية المرفوعه الى PNG", + "default-avatar": "الصورة الرمزية الافتراضية", + "upload": "رفع", + "profile-image-dimension": "أبعاد الصورة الرمزية", + "profile-image-dimension-help": "(بالبكسل، الافتراضي: 128 بكسل)", + "max-profile-image-size": "الحد الأقصى لحجم الصورة الرمزية", + "max-profile-image-size-help": "(بالكيبيبايت، الافتراضي: 256)", + "max-cover-image-size": "الحد الأقصى لحجم صورة الغلاف", + "max-cover-image-size-help": "(بالكيبيبايت، الافتراضي: 2,048)", + "keep-all-user-images": "الاحتفاظ بالنسخ القديمة من الصور الرمزية وصور الغلاف في السيرفر", + "profile-covers": "غلاف الملف الشخصي", + "default-covers": "صورة الغلاف الافتراضية", + "default-covers-help": "اضف صور الغلاف الافتراضية متبوعة بفواصل لاستخدامها في الحسابات التي لا تحتوي على صور غلاف مرفوعة" +} diff --git a/public/language/ar/admin/settings/user.json b/public/language/ar/admin/settings/user.json new file mode 100644 index 0000000000..56f835492a --- /dev/null +++ b/public/language/ar/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "المصادقة", + "email-confirm-interval": "لا يمكن للمستخدم إعادة إرسال رسالة تأكيد البريد الالكتروني حتى مرور", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "السماح بتسجيل الدخول باستخدام", + "allow-login-with.username-email": "اسم المستخدم أو البريد الالكتروني", + "allow-login-with.username": "اسم المستخدم فقط", + "account-settings": "إعدادت الحساب", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "عدم السماح بتغيير اسم المستخدم", + "disable-email-changes": "عدم السماح بتغيير البريد الالكتروني", + "disable-password-changes": "عدم السماح بتغيير كلمة المرور", + "allow-account-deletion": "السماح بحذف الحساب", + "hide-fullname": "إخفاء الإسم الكامل عن المستخدمين", + "hide-email": "إخفاء البريد الإلكتروني عن المستخدمين", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "القوالب", + "disable-user-skins": "منع المستخدمين من اختيار سمة مخصص", + "account-protection": "حماية الحساب", + "admin-relogin-duration": "المدة حتى طلب إعادة تسجيل الدخول للإدارة (دقائق)", + "admin-relogin-duration-help": "بعد مرور وقت معين، يتوجب إعادة تسجيل الدخول للوصول إلى قسم الإدارة، قم بتعيين القيمة الى 0 لتعطيل الخيار", + "login-attempts": "عدد محاولات تسجيل الدخول في الساعة", + "login-attempts-help": "إذا تجاوزت محاولات تسجيل الدخول لمستخدم معين العدد المحدد، فسوف يتم تأمين الحساب ومنعه من الدخول لمدة من الوقت", + "lockout-duration": "مدة تأمين الحساب (دقائق)", + "login-days": "عدد الأيام لتذكر جلسات تسجيل دخول المستخدم", + "password-expiry-days": "فرض عملية تغيير كلمة المرور بعد مرور عدد محدد من الأيام", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "تسجيل المستخدم", + "registration-type": "نوع التسجيل", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "عادي", + "registration-type.admin-approval": "بموافقة الإدارة", + "registration-type.admin-approval-ip": "بموافقة الإدارة لعناوين IP", + "registration-type.invite-only": "بالدعوات فقط", + "registration-type.admin-invite-only": "بالدعوات من قبل الإدارة فقط", + "registration-type.disabled": "لا يوجد تسجيل", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "الحد الأقصى للدعوات لكل عضو", + "max-invites": "الحد الأقصى للدعوات لكل عضو", + "max-invites-help": "0 لعدم تحديد قيود، الإدارة تحصل على دعوات لامحدودة
هذا الخيار يعمل فقط عند تحديد خيار \"بالدعوات فقط\"", + "invite-expiration": "مدة صلاحية الدعوة", + "invite-expiration-help": "عدد الأيام حتى انتهاء صلاحية الدعوة.", + "min-username-length": "الحد الأدنى لطول اسم المستخدم", + "max-username-length": "الحد الأقصى لطول اسم المستخدم", + "min-password-length": "الحد الأدنى لطول كلمة المرور", + "min-password-strength": "الحد الأدنى لقوة كلمة المرور", + "max-about-me-length": "الحد الأعلى من الأحرف في حقل \"عني\"", + "terms-of-use": "شروط استخدام المنتدى (تترك فارغة لتعطيلها)", + "user-search": "بحث الأعضاء", + "user-search-results-per-page": "عدد النتائج المراد عرضها", + "default-user-settings": "إعدادات الأعضاء الافتراضية", + "show-email": "عرض البريد الإلكتروني", + "show-fullname": "عرض الاسم الكامل", + "restrict-chat": "السماح فقط برسائل الدردشة من المستخدمين الذين أتبعهم", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/ar/admin/settings/web-crawler.json b/public/language/ar/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/ar/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/ar/category.json b/public/language/ar/category.json new file mode 100644 index 0000000000..412a7ea191 --- /dev/null +++ b/public/language/ar/category.json @@ -0,0 +1,23 @@ +{ + "category": "قسم", + "subcategories": "قسم فرعي", + "new_topic_button": "موضوع جديد", + "guest-login-post": "سجل الدخول للمشاركة", + "no_topics": "لا توجد مواضيع في هذه القسملم لا تحاول إنشاء موضوع؟
", + "browsing": "تصفح", + "no_replies": "لم يرد أحد", + "no_new_posts": "لا توجد مشاركات جديدة.", + "watch": "تابع", + "ignore": "تجاهل", + "watching": "متابع", + "not-watching": "لست متابع", + "ignoring": "متجاهل", + "watching.description": "أظهر المواضيع في الغير مقروء و الحديث", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "الأقسام المُتابعة", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/ar/email.json b/public/language/ar/email.json new file mode 100644 index 0000000000..f807ddbfa3 --- /dev/null +++ b/public/language/ar/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "مرحبًا بك في %1", + "invite": "دعوة من %1", + "greeting_no_name": "مرحبًا", + "greeting_with_name": "مرحبًا بك يا %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "شكرًا على تسجيلك في %1!", + "welcome.text2": "لتفعيل حسابك، نحتاج إلى التأكد من صحة عنوان البريد الإلكتروني الذي سجلت به.", + "welcome.text3": "تم قبول نتسجيلك ، يمكنك الدخول باتسخدام اسم المستخدم و كلمة المرور.", + "welcome.cta": "انقر هنا لتفعيل عنوان بريدك الإلكتروني", + "invitation.text1": "%1 قام بدعوتك للانضمام لـ %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "لقد توصلنا بطلب إعادة تعيين كلمة المرور الخاصة بك، ربما لكونك قد نسيتها, إن لم يكن الأمر كذلك، المرجو تجاهل هذه الرسالة.", + "reset.text2": "لمواصلة طلب إعاة تعيين كلمة المرور، الرجاء تتبع هذا الرابط.", + "reset.cta": "انقر هنا لإعادة تعيين كلمة السر الخاصة بك.", + "reset.notify.subject": "تم تغيير كلمة المرور بنجاح", + "reset.notify.text1": "نحيطك علما أن كلمة مرورك قد تم تغييرها في %1", + "reset.notify.text2": "إن لم يكن لديك علم بهذا، المرجو إشعار مدبر النظام بأسرع مايمكن.", + "digest.latest_topics": "آخر المستجدات من %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "انقر هنا لمشاهدة %1", + "digest.unsub.info": "تم إرسال هذا الإشعار بآخر المستجدات وفقا لخيارات تسجيلكم.", + "digest.day": "يوم", + "digest.week": "أسبوع", + "digest.month": "شهر", + "digest.subject": "إستهلاك ل", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "هناك محادثة جديدة من %1", + "notif.chat.cta": "انقر هنا لمتابعة المحادثة", + "notif.chat.unsub.info": "تم إرسال هذا الإشعار بوجودة محادثة جديدة وفقا لخيارات تسجيلك.", + "notif.post.unsub.info": "تم إشعارك بهذه المشاركة بناءً على الخيارات التي سبق وأن حددتها.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "هذه رسالة تجريبية للتأكد من صحة إعدادت الرسائل الإلكترونية في منتدى NodeBB خاصتك.", + "unsub.cta": "انقر هنا لتغيير تلك الإعدادات", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "شكرًا لك!" +} \ No newline at end of file diff --git a/public/language/ar/error.json b/public/language/ar/error.json new file mode 100644 index 0000000000..ed2cb779b5 --- /dev/null +++ b/public/language/ar/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "بيانات غير صحيحة", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "لم تقم بتسجيل الدخول", + "account-locked": "تم حظر حسابك مؤقتًا.", + "search-requires-login": "البحث في المنتدى يتطلب حساب - الرجاء تسجيل الدخول أو التسجيل", + "goback": "Press back to return to the previous page", + "invalid-cid": "قائمة غير موجودة", + "invalid-tid": "موضوع غير متواجد", + "invalid-pid": "رد غير موجود", + "invalid-uid": "مستخدم غير موجود", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "اسم المستخدم غير مقبول", + "invalid-email": "البريد الاكتروني غير مقبول", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "بيانات المستخدم غير صحيحة", + "invalid-password": "كلمة السر غير مقبولة", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "المرجود تحديد اسم مستخدم و كلمة مرور", + "invalid-search-term": "كلمة البحث غير صحيحة", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "لم تتمكن من تسجيل الدخول. هنالك أحتمال ان جلستك انتهت. رجاءًا حاول مرة اخرى.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "رقم الصفحة غير صحيح ، يجب أن يكون بين %1 و %2 .", + "username-taken": "اسم المستخدم مأخوذ", + "email-taken": "البريد الالكتروني مأخوذ", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "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 may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "لم نستطع تفعيل بريدك الإلكتروني، المرجو المحاولة لاحقًا.", + "confirm-email-already-sent": "لقد تم ارسال بريد التأكيد، الرجاء اﻹنتظار 1% دقائق لإعادة اﻹرسال", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "اسم المستخدم قصير.", + "username-too-long": "اسم المستخدم طويل", + "password-too-long": "كلمة السر طويلة ", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "المستخدم محظور", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "عذرا, يجب أن تنتظر 1% ثواني قبل قيامك بأول مشاركة", + "blacklisted-ip": "نأسف، لقد تم حظرك من استخدام وتصفح المنتدى. إذا كنت تعتقد أن هذا خطأ رجاءًا اتصل بالإدارة. ", + "ban-expiry-missing": "رجاءًا ضع تاريخ نهاية الحظر. ", + "no-category": "قائمة غير موجودة", + "no-topic": "موضوع غير موجود", + "no-post": "رد غير موجود", + "no-group": "مجموعة غير موجودة", + "no-user": "اسم مستخدم غير موجود", + "no-teaser": "مقتطف غير موجود", + "no-flag": "Flag does not exist", + "no-privileges": "لاتملك الصلاحيات اللازمة للقيام بهذه العملية", + "category-disabled": "قائمة معطلة", + "topic-locked": "الموضوع مقفول", + "post-edit-duration-expired": "يسمح لك بتعديل مشاركتك حتى %1 ثانية من نشرها", + "post-edit-duration-expired-minutes": "يسمح لك بتعديل مشاركتك حتى %1 دقيقة من نشرها", + "post-edit-duration-expired-minutes-seconds": "يسمح لك بتعديل مشاركتك حتى %1 دقيقة و %2 ثوان من نشرها", + "post-edit-duration-expired-hours": "يسمح لك بتعديل مشاركتك حتى %1 ساعة من نشرها", + "post-edit-duration-expired-hours-minutes": "يسمح لك بتعديل مشاركتك حتى %1 ساعة و %2 دقيقة من نشرها", + "post-edit-duration-expired-days": "يسمح لك بتعديل مشاركتك حتى %1 يوم من نشرها", + "post-edit-duration-expired-days-hours": "يسمح لك بتعديل مشاركتك حتى %1 يوم و %2 ساعة من نشرها", + "post-delete-duration-expired": "يسمح لك بحذف مشاركتك حتى %1 ثانية من نشرها", + "post-delete-duration-expired-minutes": "يسمح لك بحذف مشاركتك حتى %1 دقيقة من نشرها", + "post-delete-duration-expired-minutes-seconds": "يسمح لك بحذف مشاركتك حتى %1 دقيقة و %2 ثوان من نشرها", + "post-delete-duration-expired-hours": "يسمح لك بحذف مشاركتك حتى %1 ساعة من نشرها", + "post-delete-duration-expired-hours-minutes": "يسمح لك بحذف مشاركتك حتى %1 ساعة و %2 دقيقة من نشرها", + "post-delete-duration-expired-days": "يسمح لك بحذف مشاركتك حتى %1 يوم من نشرها", + "post-delete-duration-expired-days-hours": "يسمح لك بحذف مشاركتك حتى %1 يوم و %2 ساعة من نشرها", + "cant-delete-topic-has-reply": "لا يمكنك حذف مشاركة تم الرد عليها", + "cant-delete-topic-has-replies": "لا يمكنك حذف مشاركة حصدت %1 ردود", + "content-too-short": "يرجى ادخال موضوع أطول. على المواضيع أن تحتوي على %1 حرف على الأقل.", + "content-too-long": "يرجى ادخال موضوع أقصر. على المواضيع أن لا تتخطى %1 حرف.", + "title-too-short": "يرجى إدخال عنوان أطول. على العناوين أن تحتوي على %1 حرف على الأقل.", + "title-too-long": "يرجى ادخال عنوان أقصر. على العناوين أن لا تتخطى %1 حرف.", + "category-not-selected": "Category not selected.", + "too-many-posts": "يسمح لك بالنشر مرة كل %1 ثانية - يرجى الإنتظار قبل النشر مجدداً", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "الرجاء الانتظار حتى يكتمل الرفع.", + "file-too-big": "الحد الأقصى لرفع الملفات %1 كيلو بت. رجاءًا ارفع ملف أصغر", + "guest-upload-disabled": "خاصية رفع الملفات غير مفعلة للزوار.", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "لايمكن حظر مدبر نظام آخر.", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "رجاءًا ، أضف مدير أخر قبل حذف صلاحيات الإدارة من حسابك.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "رجاءًا أزل صلاحيات الإدارة قبل حذف الحساب. ", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "نوع الصورة غير مدعوم. الأنواع المدعومة هي : %1", + "invalid-image-extension": "امتداد الصورة غير مدعوم.", + "invalid-file-type": "صيغة الملف غير مدعومة. الأنواع المدعومة هي: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "اسم المجموعة قصير", + "group-name-too-long": "اسم المجموعة طويل.", + "group-already-exists": "المجموعة موجودة مسبقا", + "group-name-change-not-allowed": "لايسمح بتغيير أسماء المجموعات", + "group-already-member": "أنت عضو في هذه المجموعة.", + "group-not-member": "أنت لست عضو في هذه المجموعة.", + "group-needs-owner": "هذه المجموعة تتطلب مالك واحد على اﻷقل", + "group-already-invited": "المستخدم سبق وأن تمت دعوته", + "group-already-requested": "سبق وتم تسجيل طلب العضوية", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "سبق وتم حذف هذا الرد", + "post-already-restored": "سبق وتم إلغاء حذف هذا الرد", + "topic-already-deleted": "سبق وتم حذف هذا الموضوع", + "topic-already-restored": "سبق وتم إلغاء حذف هذا الرد", + "cant-purge-main-post": "لا يمكنك محو المشاركة الأساسية، يرجى حذف الموضوع بدلاً عن ذلك", + "topic-thumbnails-are-disabled": "الصور المصغرة غير مفعلة.", + "invalid-file": "ملف غير مقبول", + "uploads-are-disabled": "رفع الملفات غير مفعل", + "signature-too-long": "عذرا، توقيعك يجب ألا يتجاوز %1 حرفًا.", + "about-me-too-long": "نأسف، ( عني ) لا يمكن أن يكون أكثر من %1 حرف. ", + "cant-chat-with-yourself": "لايمكنك فتح محادثة مع نفسك", + "chat-restricted": "هذا المستخدم عطل المحادثات الواردة عليه. يجب أن يتبعك حتى تتمكن من فتح محادثة معه.", + "chat-disabled": "نظام المحادثة معطل.", + "too-many-messages": "لقد أرسلت الكثير من الرسائل، الرجاء اﻹنتظار قليلاً", + "invalid-chat-message": "الرسالة غير صالحة.", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "غير مصرح لك بتعديل الرسالة.", + "cant-delete-chat-message": "غير مصرح لك بحذف الرسالة.", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "لقد شاركت بالتصويت ، ألا تذكر؟", + "reputation-system-disabled": "نظام السمعة معطل", + "downvoting-disabled": "التصويتات السلبية معطلة", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "المنتدى واجه مشكلة أثناء إعادة التحميل: \"%1\". سيواصل المنتدى خدمة العملاء السابقين لكن يجب عليك إلغاء أي تغيير قمت به قبل إعادة التحميل.", + "registration-error": "حدث خطأ أثناء التسجيل", + "parse-error": "حدث خطأ ما أثناء تحليل استجابة الخادم", + "wrong-login-type-email": "الرجاء استعمال بريدك اﻹلكتروني للدخول", + "wrong-login-type-username": "الرجاء استعمال اسم المستخدم الخاص بك للدخول", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "لقد قمت بدعوة الحد الأقصى من الأشخاص (%1 من %2)", + "no-session-found": "لم دخول مسجل!", + "not-in-room": "المستخدم غير موجود في الغرفة.", + "cant-kick-self": "لا يمكنك طرد نفسك من المجموعة.", + "no-users-selected": "لا يوجد مستخدم محدد.", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/ar/flags.json b/public/language/ar/flags.json new file mode 100644 index 0000000000..c7bc55119e --- /dev/null +++ b/public/language/ar/flags.json @@ -0,0 +1,89 @@ +{ + "state": "الحالة", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "المحال إليه", + "update": "تحديث", + "updated": "تم التحديث", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "ازالة الفلاتر", + "filters": "خيارات الفلتر", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "عنوان العلامة", + "filter-type-all": "كل المحتوي", + "filter-type-post": "مشاركة", + "filter-type-user": "مستخدم", + "filter-state": "الحالة", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "اجراءات سريعه", + "flagged-user": "Flagged User", + "view-profile": "مشاهدة الملف الشخصي", + "start-new-chat": "بدء محادثه جديده", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "حذف المشاركة", + "purge-post": "Purge Post", + "restore-post": "استرجاع المشاركة", + "delete": "Delete Flag", + + "user-view": "مشاهدة الملف الشخصي", + "user-edit": "تعديل الملف الشخصي", + + "notes": "Flag Notes", + "add-note": "اضافة ملاحظة", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "تم حلها", + "state-rejected": "تم رفضها", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/ar/global.json b/public/language/ar/global.json new file mode 100644 index 0000000000..7f93ee0e24 --- /dev/null +++ b/public/language/ar/global.json @@ -0,0 +1,126 @@ +{ + "home": "الصفحة الرئيسية", + "search": "بحث", + "buttons.close": "أغلق", + "403.title": "غير مسموح بالدخول", + "403.message": "يبدو أنك قد تعثرت على صفحة لا تمتلك الصلاحية للدخول إليها", + "403.login": "ربما يجب عليك تسجل دخولك.", + "404.title": "لم يتم العثور", + "404.message": "الصفحة غير موجودة. العودة لـ الرئيسية", + "500.title": "خطأ داخلي", + "500.message": "عفوا! يبدو وكأنه شيء ذهب على نحو خاطئ!", + "400.title": "طلب سيئ", + "400.message": "الرابط غير صحيح. رجاءًا تأكد من الرابط أو ارجع لـ الرئيسية", + "register": "تسجيل", + "login": "دخول", + "please_log_in": "الرجاء تسجيل الدخول", + "logout": "تسجيل الخروج", + "posting_restriction_info": "إضافة مشاركات جديد حكر على الأعضاء المسجلين، انقر هنا لتسجيل الدخول.", + "welcome_back": "مرحبًا بعودتك", + "you_have_successfully_logged_in": "تم سجيل الدخول بنجاح", + "save_changes": "حفظ التغييرات", + "save": "حفظ", + "close": "أغلق", + "pagination": "الصفحات", + "pagination.out_of": "%1 من %2", + "pagination.enter_index": "Go to post index", + "header.admin": "مدير النظام", + "header.categories": "الأقسام", + "header.recent": "حديث", + "header.unread": "غير مقروء", + "header.tags": "وسم", + "header.popular": "الأكثر شهرة", + "header.top": "Top", + "header.users": "المستخدمين", + "header.groups": "المجموعات", + "header.chats": "المحادثات", + "header.notifications": "التنبيهات", + "header.search": "بحث", + "header.profile": "ملف", + "header.navigation": "الاستكشاف", + "notifications.loading": "تحميل التنبيهات", + "chats.loading": "تحميل الدردشات", + "motd.welcome": "مرحبا بكم في NodeBB، منصة المناقشة المستقبلية.", + "previouspage": "الصفحة السابقة", + "nextpage": "الصفحة التالية", + "alert.success": "نجاح", + "alert.error": "خطأ", + "alert.banned": "محظور", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "أنت لا تتابع %1 بعد الآن!", + "alert.follow": "أنت الآن تتابع %1!", + "users": "الأعضاء", + "topics": "المواضيع", + "posts": "المشاركات", + "x-posts": "%1 posts", + "best": "الأفضل", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "الموافقين", + "upvoted": "مصوت بالموجب", + "downvoters": "مصوتين بالسالب", + "downvoted": "مصوت بالسالب", + "views": "المشاهدات", + "posters": "Posters", + "reputation": "السمعة", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "اقرأ المزيد", + "more": "المزيد", + "none": "None", + "posted_ago_by_guest": "كتب %1 بواسطة زائر", + "posted_ago_by": "كتب %1 بواسطة %2", + "posted_ago": "كتب %1", + "posted_in": "كتب في %1", + "posted_in_by": "كتب في 1% بواسطة %2", + "posted_in_ago": "كتب في %1 %2", + "posted_in_ago_by": "كتب في %1 %2 من طرف %3", + "user_posted_ago": "%1 كتب %2", + "guest_posted_ago": "كتب زائر %1", + "last_edited_by": "اخر تحرير بواسطة 1%", + "norecentposts": "لاوجود لمشاركات جديدة", + "norecenttopics": "لاوجود لمواضيع جديدة", + "recentposts": "آخر المشاركات", + "recentips": "آخر عناوين ال IP التي سجلت الدخول", + "moderator_tools": "أدوات المشرف", + "online": "المتواجدون حاليًّا", + "away": "غير متواجد", + "dnd": "عدم الإزعاج", + "invisible": "مخفي", + "offline": "غير متصل", + "email": "عنوان البريد الإلكتروني", + "language": "اللغة", + "guest": "زائر", + "guests": "الزوار", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "تم تحديث المنتدى", + "updated.message": "لقد تم تحديث المنتدى إلى آخر نسخة للتو. إضغط هنا لإعادة تحميل الصفحة.", + "privacy": "الخصوصية", + "follow": "متابعة", + "unfollow": "إلغاء المتابعة", + "delete_all": "حذف الكل", + "map": "خريطة", + "sessions": "الجلسة", + "ip_address": "عنوان الآي بي", + "enter_page_number": "ادخل رقم الصفحة", + "upload_file": "ارفع ملف", + "upload": "ارفع", + "uploads": "Uploads", + "allowed-file-types": "صيغ الملفات المدعومة هي 1%", + "unsaved-changes": "لديك تغييرات لم تحفظ. هل أنت متأكد من رغبتك بمغادرة الصفحة؟", + "reconnecting-message": "يبدو أن اتصالك لـ %1 قد فقد. رجاءًا أنتظر ثم حاول الإتصال مرة اخرى.", + "play": "تشغيل", + "cookies.message": "هذا الموقع يستخدم ملفات تعريف الارتباط لضمان حصولك على أفضل تجربة على موقعنا.", + "cookies.accept": "فهمت الأمر!", + "cookies.learn_more": "أعرف المزيد", + "edited": "حُرِر", + "disabled": "معطل", + "select": "تحديد", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/ar/groups.json b/public/language/ar/groups.json new file mode 100644 index 0000000000..73095455ce --- /dev/null +++ b/public/language/ar/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "المجموعات", + "view_group": "معاينة المجموعة", + "owner": "مالك المجموعة", + "new_group": "أنشئ مجموعة جديدة", + "no_groups_found": "لاوجدود لمجموعات يمكن معاينتها", + "pending.accept": "موافق", + "pending.reject": "رفض", + "pending.accept_all": "قبول الكل", + "pending.reject_all": "رفض الكل", + "pending.none": "لايوجد أعضاء ينتظرون التفعيل حالياً", + "invited.none": "لايوجد أعضاء مدعوون في حالياً", + "invited.uninvite": "إلغ الدعوة", + "invited.search": "ابحث عن أعضاء لدعوتهم للمجموعة", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "حفظ", + "cover-saving": "جاري الحفظ", + "details.title": "تفاصيل المجموعة", + "details.members": "لائحة الأعضاء", + "details.pending": "المستخدمون في الانتظار", + "details.invited": "اﻷعضار المدعوون", + "details.has_no_posts": "أعضاء هذه المجموعة لم يضيفوا أية مشاركة", + "details.latest_posts": "آخر المشاركات", + "details.private": "خاص", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "منح/سحب المِلكية", + "details.kick": "طرد", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "إدارة المجموعة", + "details.group_name": "اسم المجموعة", + "details.member_count": "عدد اﻷعضاء", + "details.creation_date": "تاريخ الإنشاء", + "details.description": "الوصف", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "معاينة الوسام", + "details.change_icon": "تغيير الأيقونة", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "نص الوسام", + "details.userTitleEnabled": "إظهار الوسام", + "details.private_help": "في حالة تفعيل الخيار، الانضمام إلى المجموعة يستلزم قبول مالكها", + "details.hidden": "مخفي", + "details.hidden_help": "في حالة تفعيل الخيار، لن تظهر المجموعة للعموم والإنضمام إليها سيتلزم دعوة.", + "details.delete_group": "حذف المجموعة", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "تم تحديث بيانات المجموعة", + "event.deleted": "تم حذف المجموعة %1", + "membership.accept-invitation": "اقبل الدعوة", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "الدعوة بانتظار القبول", + "membership.join-group": "انظم للمجموعة", + "membership.leave-group": "غادر المجموعة", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "رفض", + "new-group.group_name": "اسم المجموعة", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/ar/ip-blacklist.json b/public/language/ar/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/ar/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/ar/language.json b/public/language/ar/language.json new file mode 100644 index 0000000000..756c09c2b3 --- /dev/null +++ b/public/language/ar/language.json @@ -0,0 +1,5 @@ +{ + "name": "العربية", + "code": "ar", + "dir": "rtl" +} \ No newline at end of file diff --git a/public/language/ar/login.json b/public/language/ar/login.json new file mode 100644 index 0000000000..2912e45b88 --- /dev/null +++ b/public/language/ar/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "اسم المستخدم / البريد الإلكتروني", + "username": "اسم المستخدم", + "remember_me": "تذكرني؟", + "forgot_password": "نسيت كلمة المرور؟", + "alternative_logins": "تسجيلات الدخول البديلة", + "failed_login_attempt": "تسجيل الدخول غير ناجح", + "login_successful": "قمت بتسجيل الدخول بنجاح!", + "dont_have_account": "لا تملك حساب؟", + "logged-out-due-to-inactivity": "لقد تم تسجيل خروجك من لوحة تحكم بسبب عدم نشاطك", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json new file mode 100644 index 0000000000..ad1616060a --- /dev/null +++ b/public/language/ar/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "أرسل", + "chat.no_active": "لا يوجد لديك دردشات نشطة.", + "chat.user_typing": "%1 يكتب رسالة...", + "chat.user_has_messaged_you": "%1 أرسل لك رسالة.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "المرجو اختيار مرسل إليه لمعاينة تاريخ الدردشات", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "آخر الدردشات", + "chat.contacts": "الأصدقاء", + "chat.message-history": "تاريخ الرسائل", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "افتح الدردشة في نافذة خاصة", + "chat.minimize": "Minimize", + "chat.maximize": "تكبير", + "chat.seven_days": "7 أيام", + "chat.thirty_days": "30 يومًا", + "chat.three_months": "3 أشهر", + "chat.delete_message_confirm": "هل أنت متأكد من أنك تريد حذف هذه الرسالة؟", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "اكتب", + "composer.show_preview": "عرض المعاينة", + "composer.hide_preview": "إخفاء المعاينة", + "composer.user_said_in": "%1 كتب في %2", + "composer.user_said": "%1 كتب:", + "composer.discard": "هل أنت متأكد أنك تريد التخلي عن التغييرات؟", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "إلغاء", + "bootbox.confirm": "تأكيد", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/ar/notifications.json b/public/language/ar/notifications.json new file mode 100644 index 0000000000..b20e528108 --- /dev/null +++ b/public/language/ar/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "التنبيهات", + "no_notifs": "ليس لديك أية تنبيهات جديدة", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "عودة إلى %1", + "outgoing_link": "رابط خارجي", + "outgoing_link_message": "أنت تغادر اﻻن %1", + "continue_to": "استمر إلى %1", + "return_to": "عودة إى %1", + "new_notification": "لديك تنبيه جديد", + "you_have_unread_notifications": "لديك تنبيهات غير مقروءة.", + "all": "الكل", + "topics": "مواضيع", + "replies": "ردود", + "chat": "محادثات", + "group-chat": "Group Chats", + "follows": "متابعون", + "upvote": "الموافقين", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "الحظر", + "new_message_from": "رسالة جديدة من %1", + "upvoted_your_post_in": "%1 أضاف صوتًا إيجابيا إلى مشاركتك في %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 أشعَرَ بمشاركة مخلة في %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 أضاف ردا إلى: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 أنشأ موضوعًا جديدًا: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 صار يتابعك.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "تم تخصيص العلامة 1% لك", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "تم التحقق من عنوان البريد الإلكتروني", + "email-confirmed-message": "شكرًا على إثبات صحة عنوان بريدك الإلكتروني. صار حسابك مفعلًا بالكامل.", + "email-confirm-error-message": "حدث خطأ أثناء التحقق من عنوان بريدك الإلكتروني. ربما رمز التفعيل خاطئ أو انتهت صلاحيته.", + "email-confirm-sent": "تم إرسال بريد التفعيل.", + "none": "None", + "notification_only": "التنبيهات فقط", + "email_only": "البريد الالكتروني فقط", + "notification_and_email": "التنبيهات والبريد اﻻلكتروني", + "notificationType_upvote": "عندما يوافقك احدهم على منشورك", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/ar/pages.json b/public/language/ar/pages.json new file mode 100644 index 0000000000..0810992571 --- /dev/null +++ b/public/language/ar/pages.json @@ -0,0 +1,65 @@ +{ + "home": "الصفحة الرئيسية", + "unread": "المواضيع الغير مقروءة", + "popular-day": "المواضيع الشائعة اليوم", + "popular-week": "المواضيع الشائعة هذا الأسبوع", + "popular-month": "المواضيع الشائعة هذا الشهر", + "popular-alltime": "المواضيع الشائعة منذ القدم", + "recent": "المواضيع الحديثة", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "اﻷعضاء المتصلون", + "users/latest": "أحدث اﻷعضاء", + "users/sort-posts": "الأعضاء الأكثر نشاطاً", + "users/sort-reputation": "الأعضاء الأفضل سمعة", + "users/banned": "الأعضاء المحظورون", + "users/most-flags": "Most flagged users", + "users/search": "بحث عن مستخدم", + "notifications": "التنبيهات", + "tags": "الكلمات الدلالية", + "tag": "Topics tagged under "%1"", + "register": "تسجيل حساب", + "registration-complete": "Registration complete", + "login": "سجل الدخول الى حسابك", + "reset": "إعادة تعيين كلمة مرور حسابك", + "categories": "الفئات", + "groups": "المجموعات", + "group": "%1 مجموعة", + "chats": "محادثات", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "معلومات الحساب", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "خيارات المستخدم", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "تم التحقق من عنوان البريد الإلكتروني", + "maintenance.text": "جاري صيانة %1. المرجو العودة لاحقًا.", + "maintenance.messageIntro": "بالإضافة إلى ذلك، قام مدبر النظام بترك هذه الرسالة:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/ar/post-queue.json b/public/language/ar/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/ar/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/ar/recent.json b/public/language/ar/recent.json new file mode 100644 index 0000000000..fcbc6949be --- /dev/null +++ b/public/language/ar/recent.json @@ -0,0 +1,19 @@ +{ + "title": "الحديثة", + "day": "يوم", + "week": "أسبوع", + "month": "شهر", + "year": "سنة", + "alltime": "دائمًا", + "no_recent_topics": "لايوجد مواضيع جديدة", + "no_popular_topics": "لا يوجد مواضيع شائعة", + "there-is-a-new-topic": "يوجد موضوع جديد", + "there-is-a-new-topic-and-a-new-post": "يوجد موضوع جديد و رد جديد", + "there-is-a-new-topic-and-new-posts": "يوجد موضوع جديد و %1 ردود جديدة ", + "there-are-new-topics": "يوجد %1 مواضيع جديدة", + "there-are-new-topics-and-a-new-post": "يوجد %1 مواضيع جديدة و رد جديد", + "there-are-new-topics-and-new-posts": "يوجد %1 مواضيع جديدة و %2 مشاركات جديدة", + "there-is-a-new-post": "يوجد مشاركة جديدة", + "there-are-new-posts": "يوجد %1 مشاركات جديدة", + "click-here-to-reload": "إضغط هنا لإعادة التحميل" +} \ No newline at end of file diff --git a/public/language/ar/register.json b/public/language/ar/register.json new file mode 100644 index 0000000000..ce3b35e526 --- /dev/null +++ b/public/language/ar/register.json @@ -0,0 +1,32 @@ +{ + "register": "تسجيل", + "cancel_registration": "إلغاء التسجيل", + "help.email": "افتراضيا، سيتم إخفاء بريدك الإلكتروني من العامة.", + "help.username_restrictions": "اسم مستخدم فريدة من نوعها بين 1% و 2% حرفا. بإمكان الآخرين مناداتك بـ @اسم المستخدم.", + "help.minimum_password_length": "كلمة المرور يجب أن تتكون على الأقل من 1% أحرف/حروف", + "email_address": "عنوان البريد الإلكتروني", + "email_address_placeholder": "ادخل عنوان البريد الإلكتروني", + "username": "اسم المستخدم", + "username_placeholder": "أدخل اسم المستخدم", + "password": "كلمة المرور", + "password_placeholder": "أدخل كلمة المرور", + "confirm_password": "تأكيد كلمة المرور", + "confirm_password_placeholder": "تأكيد كلمة المرور", + "register_now_button": "قم بالتسجيل الآن", + "alternative_registration": "طريقة تسجيل بديلة", + "terms_of_use": "شروط الاستخدام", + "agree_to_terms_of_use": "أوافق على شروط الاستخدام", + "terms_of_use_error": "يجب عليك الموافقة على شروط الاستخدام", + "registration-added-to-queue": "تمت إضافتك في قائمة الإنتضار. ستتلقى رسالة إلكترونية عند الموافقة على تسجيلك من قبل الإدارة.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/ar/reset_password.json b/public/language/ar/reset_password.json new file mode 100644 index 0000000000..326e396955 --- /dev/null +++ b/public/language/ar/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "إعادة تعيين كلمة المرور", + "update_password": "تحديث كلمة المرور", + "password_changed.title": "تم تغير كلمة المرور", + "password_changed.message": "

تم تغير كلمة المرور بنجاح، الرجاء إعادة الدخول

", + "wrong_reset_code.title": "رمز إعادة التعيين غير صحيح", + "wrong_reset_code.message": "رمز إعادة التعين غير صحيح، يرجى المحاولة مرة أخرى أو اطلب رمزا جديدا", + "new_password": "كلمة المرور الجديدة", + "repeat_password": "تأكيد كلمة المرور", + "changing_password": "Changing Password", + "enter_email": "يرجى إدخال عنوان البريد الإلكتروني الخاص بك وسوف نرسل لك رسالة بالبريد الالكتروني مع تعليمات حول كيفية إستعادة حسابك.", + "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.", + "invalid_email": "بريد إلكتروني غير صالح أو غير موجود", + "password_too_short": "كلمة المرور التي أدخلتها قصيرة، الرجاء اختر كلمة مرور مختلفة", + "passwords_do_not_match": "كلمتا السر التي أدخلتهما غير متطابقتان", + "password_expired": "لقد انتهت صلاحية كلمة المرور الخاصة بك، الرجاء اختيار كلمة مرور جديدة" +} \ No newline at end of file diff --git a/public/language/ar/search.json b/public/language/ar/search.json new file mode 100644 index 0000000000..b2c7a32bbc --- /dev/null +++ b/public/language/ar/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 نتيجة (نتائج) موافقة لـ \"%2\", (%3 ثواني)", + "no-matches": "لم يتم العثور على نتائج.", + "advanced-search": "بحث متقدم", + "in": "في", + "titles": "العناوين", + "titles-posts": "العناوين والمشاركات", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "مشاركة من طرف", + "in-categories": "في الفئات", + "search-child-categories": "بحث في الفئات الفرعية", + "has-tags": "Has tags", + "reply-count": "عدد المشاركات", + "at-least": "على اﻷقل", + "at-most": "على اﻷكثر", + "relevance": "Relevance", + "post-time": "تاريخ المشاركة", + "votes": "Votes", + "newer-than": "أحدث من", + "older-than": "أقدم من", + "any-date": "أي وقت", + "yesterday": "أمس", + "one-week": "أسبوع", + "two-weeks": "أسبوعان", + "one-month": "شهر", + "three-months": "ثلاثة أشهر", + "six-months": "ستة أشهر", + "one-year": "عام", + "sort-by": "عرض حسب", + "last-reply-time": "تاريخ آخر رد", + "topic-title": "عنوان الموضوع", + "topic-votes": "Topic votes", + "number-of-replies": "عدد الردود", + "number-of-views": "عدد المشاهدات", + "topic-start-date": "تاريخ بدأ الموضوع", + "username": "اسم المستخدم", + "category": "فئة", + "descending": "في ترتيب تنازلي", + "ascending": "في ترتيب تصاعدي", + "save-preferences": "حفظ التفضيلات", + "clear-preferences": "ازالة التفضيلات", + "search-preferences-saved": "تم حفظ تفضيلات البحث", + "search-preferences-cleared": "تم ازالة تفضيلات البحث", + "show-results-as": "عرض النتائج كـ", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/ar/success.json b/public/language/ar/success.json new file mode 100644 index 0000000000..57b7def037 --- /dev/null +++ b/public/language/ar/success.json @@ -0,0 +1,7 @@ +{ + "success": "نجاح", + "topic-post": "لقد تمت الإضافة بنجاح.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "تم تسجيل الدخول بنجاح", + "settings-saved": "تم حفظ التغييرات!" +} \ No newline at end of file diff --git a/public/language/ar/tags.json b/public/language/ar/tags.json new file mode 100644 index 0000000000..34addbaec4 --- /dev/null +++ b/public/language/ar/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "لا يوجد مواضيع بهذه الكلمة الدلالية.", + "tags": "الكلمات الدلالية", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "أدخل الكلمات الدلالية...", + "no_tags": "لا يوجد كلمات دلالية بعد.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/ar/top.json b/public/language/ar/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/ar/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/ar/topic.json b/public/language/ar/topic.json new file mode 100644 index 0000000000..c5ec5278dc --- /dev/null +++ b/public/language/ar/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "موضوع", + "title": "Title", + "no_topics_found": "لا توجد مواضيع !", + "no_posts_found": "لا توجد مشاركات!", + "post_is_deleted": "هذه المشاركة محذوفة!", + "topic_is_deleted": "هذا الموضوع محذوف", + "profile": "الملف الشخصي", + "posted_by": "كتب من طرف %1", + "posted_by_guest": "كتب من طرف زائر", + "chat": "دردشة", + "notify_me": "تلق تنبيهات بالردود الجديدة في هذا الموضوع", + "quote": "اقتبس", + "reply": "رد", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "رد بموضوع", + "guest-login-reply": "يجب عليك تسجيل الدخول للرد", + "login-to-view": "🔒 Log in to view", + "edit": "تعديل", + "delete": "حذف", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "تطهير", + "restore": "استعادة", + "move": "نقل", + "change-owner": "Change Owner", + "fork": "فرع", + "link": "رابط", + "share": "نشر", + "tools": "أدوات", + "locked": "مقفل", + "pinned": "مثبت", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "منقول", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "اضغط هنا للعودة لأخر مشاركة مقروءة في الموضوع", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "هذه المشاركة محذوفة. فقط من لهم صلاحية الإشراف على ا لمشاركات يمكنهم معاينتها.", + "following_topic.message": "ستستلم تنبيها عند كل مشاركة جديدة في هذا الموضوع.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "المرجو إنشاء حساب أو تسجيل الدخول حتى يمكنك متابعة هذا الموضوع.", + "markAsUnreadForAll.success": "تم تحديد الموضوع على أنه غير مقروء.", + "mark_unread": "حدده كغير مقروء", + "mark_unread.success": "الموضوع حدد على أنه غير مقروء", + "watch": "مراقبة", + "unwatch": "الغاء المراقبة", + "watch.title": "استلم تنبيها بالردود الجديدة في هذا الموضوع", + "unwatch.title": "ألغ مراقبة هذا الموضوع", + "share_this_post": "انشر هذا الموضوع", + "watching": "مراقبة", + "not-watching": "غير مراقب", + "ignoring": "تجاهل", + "watching.description": "بلغني بالردود الجديدة
\nاظهر الموضوع في غير مقروء", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "أدوات الموضوع", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "علق الموضوع", + "thread_tools.unpin": "إلغاء تعليق الموضوع", + "thread_tools.lock": "أقفل الموضوع", + "thread_tools.unlock": "إلغاء إقفال الموضوع", + "thread_tools.move": "نقل الموضوع", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "نقل الكل", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "إنشاء فرع الموضوع", + "thread_tools.delete": "حذف الموضوع", + "thread_tools.delete-posts": "مشاركات محذوفة", + "thread_tools.delete_confirm": "هل أنت متأكد أنك تريد حذف هذا الموضوع؟", + "thread_tools.restore": "استعادة الموضوع", + "thread_tools.restore_confirm": "هل أنت متأكد أنك تريد استعادة هذا الموضوع؟", + "thread_tools.purge": "تطهير الموضوع", + "thread_tools.purge_confirm": "هل أنت متأكد أنك تريد تطهير هذا الموضوع؟", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "هل أنت متأكد أنك تريد حذف هذه المشاركة؟", + "post_restore_confirm": "هل أنت متأكد أنك تريد استعادة هذه المشاركة؟", + "post_purge_confirm": "هل أنت متأكد أنك تريد تطهير هذه المشاركة؟", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "تحميل الفئات", + "confirm_move": "انقل", + "confirm_fork": "فرع", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "تحميل المزيد من المشاركات", + "move_topic": "نقل الموضوع", + "move_topics": "نقل المواضيع", + "move_post": "نقل المشاركة", + "post_moved": "تم نقل المشاركة", + "fork_topic": "فرع الموضوع", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "إضغط على المشاركات التي تريد تفريعها", + "fork_no_pids": "لم تختر أي مشاركة", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "1% مشاركة محددة", + "fork_success": "تم إنشاء فرع للموضوع بنجاح! إضغط هنا لمعاينة الفرع.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "أدخل عنوان موضوعك هنا...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "نبذ التغييرات", + "composer.submit": "حفظ", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "الرد على %1", + "composer.new_topic": "موضوع جديد", + "composer.editing": "Editing", + "composer.uploading": "جاري الرفع", + "composer.thumb_url_label": "ألصق رابط الصورة المصغرة للموضوع", + "composer.thumb_title": "إضافة صورة مصغرة للموضوع", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "أو قم برفع ملف", + "composer.thumb_remove": "تفريغ الخانات", + "composer.drag_and_drop_images": "اسحب وأسقص الصور هنا", + "more_users_and_guests": "%1 مستخدم(ين) و %2 زائر(ين)", + "more_users": "%1 مستخدم(ين)", + "more_guests": "%1 زائر(ين)", + "users_and_others": "%1 و %2 آخرين", + "sort_by": "ترتيب حسب", + "oldest_to_newest": "من الأقدم إلى الأحدث", + "newest_to_oldest": "من الأحدث إلى الأقدم", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "موضوع جديد", + "stale.reply_anyway": "الرد على هذا الموضوع ", + "link_back": "رد: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/ar/unread.json b/public/language/ar/unread.json new file mode 100644 index 0000000000..54471cd600 --- /dev/null +++ b/public/language/ar/unread.json @@ -0,0 +1,15 @@ +{ + "title": "غير مقروء", + "no_unread_topics": "ليس هناك أي موضوع غير مقروء", + "load_more": "حمل المزيد", + "mark_as_read": "حدد غير مقروء", + "selected": "المحددة", + "all": "الكل", + "all_categories": "كل الفئات", + "topics_marked_as_read.success": "تم تحديد المواضيع على أنها مقروءة!", + "all-topics": "كل المواضيع", + "new-topics": "مواضيع جديدة", + "watched-topics": "المواضيع المتابعة", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/ar/uploads.json b/public/language/ar/uploads.json new file mode 100644 index 0000000000..5c0734ec55 --- /dev/null +++ b/public/language/ar/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "جاري رفع الملف...", + "select-file-to-upload": "إختر ملف لرفعه!", + "upload-success": "تم رفع الملف بنجاح!", + "maximum-file-size": "الحجم الأقصى %1 كيلوبت", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/ar/user.json b/public/language/ar/user.json new file mode 100644 index 0000000000..6edb9b3752 --- /dev/null +++ b/public/language/ar/user.json @@ -0,0 +1,199 @@ +{ + "banned": "محظور", + "muted": "Muted", + "offline": "غير متصل", + "deleted": "محذوف", + "username": "إسم المستخدم", + "joindate": "تاريخ الإنضمام", + "postcount": "عدد المشاركات", + "email": "البريد الإلكتروني", + "confirm_email": "تأكيد عنوان البريد الإلكتروني", + "account_info": "معلومات الحساب", + "admin_actions_label": "Administrative Actions", + "ban_account": "حظر الحساب", + "ban_account_confirm": "هل تريد حقاً حظر هاذا العضو؟", + "unban_account": "إزالة حظر الحساب", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "حذف الحساب", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "تم حذف الحساب", + "account-content-deleted": "Account content deleted", + "fullname": "الاسم الكامل", + "website": "الموقع الإلكتروني", + "location": "الموقع", + "age": "السن", + "joined": "تاريخ التسجيل", + "lastonline": "تاريخ آخر دخول", + "profile": "الملف الشخصي", + "profile_views": "عدد المشاهدات", + "reputation": "السمعة", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "متابع", + "ignored": "تم تجاهله", + "default-category-watch-state": "Default category watch state", + "followers": "المتابعون", + "following": "يتابع", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "معلومة عنك او السيرة الذاتية", + "signature": "توقيع", + "birthday": "عيد ميلاد", + "chat": "محادثة", + "chat_with": "متابعة الدردشة مع %1", + "new_chat_with": "بدء دردشة جديدة مع %1", + "flag-profile": "Flag Profile", + "follow": "تابع", + "unfollow": "إلغاء المتابعة", + "more": "المزيد", + "profile_update_success": "تم تحديث الملف الشخصي بنجاح", + "change_picture": "تغيير الصورة", + "change_username": "تغيير اسم المستخدم", + "change_email": "تغيير البريد اﻹلكتروني", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "تعديل", + "edit-profile": "تعديل الملف الشخصي", + "default_picture": "أيقونة افتراضية", + "uploaded_picture": "الصورة المرفوعة", + "upload_new_picture": "رفع صورة جديدة", + "upload_new_picture_from_url": "رفع صورة جديدة من رابط", + "current_password": "كلمة السر الحالية", + "change_password": "تغيير كلمة السر", + "change_password_error": "كلمة سر غير صحيحة", + "change_password_error_wrong_current": "كلمة السر الحالية ليست صحيحة", + "change_password_error_match": "كلمة السر غير مطابقة لتأكيد كلمة السر", + "change_password_error_privileges": "ليس لديك الصلاحيات الكافية لتغيير كلمة السر هذه.", + "change_password_success": "تم تحديث كلمة السر خاصتك.", + "confirm_password": "تأكيد كلمة السر", + "password": "كلمة السر", + "username_taken_workaround": "اسم المستخدم الذي اخترته مستخدم سابقا، لذلك قمنا بتغييره لك قليلا. أنت الآن مسجل بالاسم %1", + "password_same_as_username": "كلمة المرور مطابقة لاسم المستخدم الخاص بك، يرجى تحديد كلمة مرور أخرى.", + "password_same_as_email": "كلمة المرور مطابقة لبريدك الإلكتروني، يرجى تحديد كلمة مرور أخرى.", + "weak_password": "كلمة مرور ضعيفة.", + "upload_picture": "ارفع الصورة", + "upload_a_picture": "رفع صورة", + "remove_uploaded_picture": "إزالة الصورة المرفوعة", + "upload_cover_picture": "رفع صورة الغلاف", + "remove_cover_picture_confirm": "هل تريد بالتأكيد إزالة صورة الغلاف؟", + "crop_picture": "إقتصاص الصورة", + "upload_cropped_picture": "إقتصاص ورفع", + "avatar-background-colour": "Avatar background colour", + "settings": "خيارات", + "show_email": "أظهر بريدي الإلكتروني", + "show_fullname": "أظهر اسمي الكامل", + "restrict_chats": "لاتسمح بورود محادثات إلا من طرف المستخدمين الذين أتابعهم.", + "digest_label": "اشترك في النشرة الدورية", + "digest_description": "استلام اشعارات بآخر مستجدات هذا القسم (التنبيهات والمواضيع الجديدة) عبر البريد الإلكتروني وفقا لجدول زمني محدد.", + "digest_off": "غير مفعل", + "digest_daily": "يوميا", + "digest_weekly": "أسبوعيًّا", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "شهريًّا", + "has_no_follower": "هذا المستخدم ليس لديه أية متابعين :(", + "follows_no_one": "هذا المستخدم لا يتابع أحد :(", + "has_no_posts": "هذا المستخدم لم يشارك حتى الآن.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "هذا المستخدم لم يكتب أي موضوع حتى الآن.", + "has_no_watched_topics": "هذا المستخدم لم يقم بمراقبة اية مواضيع حتى الآن.", + "has_no_ignored_topics": "هذا المستخدم لم يقم بتجاهل اية مواضيع حتى الآن.", + "has_no_upvoted_posts": "هذا المستخدم لم يقم بالتصويت للأعلى لأي مشاركة حتى الآن.", + "has_no_downvoted_posts": "هذا المستخدم لم يقم بالتصويت للأسفل لأي مشاركة حتى الآن.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "البريد الإلكتروني مخفي", + "hidden": "مخفي", + "paginate_description": "عرض المواضيع والردود موزعة على صفحات عوضاً عن التمرير اللانهائي.", + "topics_per_page": "المواضيع في كل صفحة", + "posts_per_page": "الردود في كل صفحة", + "max_items_per_page": "أقصى %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "معدل تكرار تنبيهات التصويت للأعلى", + "upvote-notif-freq.all": "كل التصويتات للأعلى", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "كل عشر تصويتات للأعلى", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "معطل", + "browsing": "خيارات التصفح", + "open_links_in_new_tab": "فتح الروابط الخارجية في نافدة جديدة", + "enable_topic_searching": "تفعيل خاصية البحث داخل المواضيع", + "topic_search_help": "إذا قمت بتفعيل ميزة البحث في-الموضوع، سيتم تجاوز الخيار الافتراضي للمتصفح مما يؤدي للبحث بكامل الموضوع بدلا عن البحث في الجزء الظاهر في الشاشة.", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "بعد اضافة رد على المشاركة, قم بإظهار المشاركة", + "follow_topics_you_reply_to": "متابعة المواضيع التي تقوم بالرد عليها", + "follow_topics_you_create": "متابعة المواضيع التي تقوم بإنشائها", + "grouptitle": "عنوان المجموعة", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "لا يوجد عنوان للمجموعة", + "select-skin": "إختر سمة", + "select-homepage": "إختر الصفحة الرئيسية", + "homepage": "الصفحة الرئيسية", + "homepage_description": "حدد صفحة لاستخدامها كصفحة رئيسية للمنتدى أو \"لا شيء\" لاستخدام الصفحة الرئيسية الافتراضية.", + "custom_route": "مسار الصفحة الرئيسية المخصصة", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "خدمات تسجيل الدخول الموحد", + "sso.associated": "مرتبط مع", + "sso.not-associated": "انقر هنا لربط مع", + "sso.dissociate": "فصل", + "sso.dissociate-confirm-title": "تأكيد الفصل", + "sso.dissociate-confirm": "هل تريد بالتأكيد فصل حسابك عن %1؟", + "info.latest-flags": "أحدث العلامات", + "info.no-flags": "لم يتم العثور على مشاركات معلمة", + "info.ban-history": "سجل الحظر الأحدث", + "info.no-ban-history": "هذا المستخدم لم يتم حظره مطلقا", + "info.banned-until": "محظور حتى %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "محظور بشكل دائم", + "info.banned-reason-label": "سبب", + "info.banned-no-reason": "لم يتم إعطاء سبب.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "سجل اسم المستخدم", + "info.email-history": "سجل البريد الإلكتروني", + "info.moderation-note": "ملاحظة الإشراف", + "info.moderation-note.success": "تم حفظ ملاحظة الإشراف", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/ar/users.json b/public/language/ar/users.json new file mode 100644 index 0000000000..e7e1990e2f --- /dev/null +++ b/public/language/ar/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "أحدث الأعضاء", + "top_posters": "اﻷكثر مشاركة", + "most_reputation": "أعلى سمعة", + "most_flags": "Most Flags", + "search": "بحث", + "enter_username": "أدخل اسم مستخدم للبحث", + "search-user-for-chat": "ابحث عن مستخدم لبدء محادثة ", + "load_more": "حمل المزيد", + "users-found-search-took": "تم إيجاد %1 مستخدمـ(ين)! استغرق البحث %2 ثانية.", + "filter-by": "Filter By", + "online-only": "المتصلون فقط", + "invite": "دعوة", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "تم إرسال دعوة بالبريد الإلكتروني إلى %1", + "user_list": "قائمة اﻷعضاء", + "recent_topics": "أحدث المواضيع", + "popular_topics": "المواضيع الأكثر شهرة", + "unread_topics": "المواضيع الغير مقروءة", + "categories": "الأقسام", + "tags": "الوسوم", + "no-users-found": "لم يتم العثور على مستخدمين!" +} \ No newline at end of file diff --git a/public/language/bg/_DO_NOT_EDIT_FILES_HERE.md b/public/language/bg/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/bg/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/bg/admin/admin.json b/public/language/bg/admin/admin.json new file mode 100644 index 0000000000..80401eb919 --- /dev/null +++ b/public/language/bg/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Наистина ли искате да изградите повторно и да рестартирате NodeBB?", + "alert.confirm-restart": "Наистина ли искате да рестартирате NodeBB?", + + "acp-title": "%1 | Контролен панел за администратори на NodeBB", + "settings-header-contents": "Съдържание", + "changes-saved": "Промените са запазени", + "changes-saved-message": "Промените Ви в настройките на NodeBB бяха запазени.", + "changes-not-saved": "Промените не са запазени", + "changes-not-saved-message": "Възникна проблем при запазването на промените Ви по NodeBB. (%1)" +} \ No newline at end of file diff --git a/public/language/bg/admin/advanced/cache.json b/public/language/bg/admin/advanced/cache.json new file mode 100644 index 0000000000..d4627e3e30 --- /dev/null +++ b/public/language/bg/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Кеш за публикации", + "group-cache": "Кеш за групи", + "local-cache": "Локален кеш", + "object-cache": "Кеш за обекти", + "percent-full": "Запълненост: %1%", + "post-cache-size": "Размер на кеша за публикации", + "items-in-cache": "Елементи в кеша" +} \ No newline at end of file diff --git a/public/language/bg/admin/advanced/database.json b/public/language/bg/admin/advanced/database.json new file mode 100644 index 0000000000..12c75258d5 --- /dev/null +++ b/public/language/bg/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 Б", + "x-mb": "%1 МБ", + "x-gb": "%1 ГБ", + "uptime-seconds": "Активно време в секунди", + "uptime-days": "Активно време в дни", + + "mongo": "Mongo", + "mongo.version": "Версия на MongoDB", + "mongo.storage-engine": "Система за съхранение", + "mongo.collections": "Колекции", + "mongo.objects": "Обекти", + "mongo.avg-object-size": "Среден размер на обект", + "mongo.data-size": "Размер на данните", + "mongo.storage-size": "Размер на съхраненото", + "mongo.index-size": "Размер на индексите", + "mongo.file-size": "Размер на файловете", + "mongo.resident-memory": "Текущо активна памет", + "mongo.virtual-memory": "Виртуална памет", + "mongo.mapped-memory": "Заделена памет", + "mongo.bytes-in": "Байтове ВХ", + "mongo.bytes-out": "Байтове ИЗХ", + "mongo.num-requests": "Брой заявки", + "mongo.raw-info": "Сурови данни от MongoDB", + "mongo.unauthorized": "NodeBB не успя да получи нужните статистики от MongoDB. Моля, уверете се, че потребителят, който се използва от NodeBB, включва ролята „clusterMonitor“ за базата данни „admin“.", + + "redis": "Redis", + "redis.version": "Версия на Redis", + "redis.keys": "Ключове", + "redis.expires": "Изтичания", + "redis.avg-ttl": "Средно време на живот (TTL)", + "redis.connected-clients": "Свързани клиенти", + "redis.connected-slaves": "Свързани второстепенни сървъри", + "redis.blocked-clients": "Блокирани клиенти", + "redis.used-memory": "Използвана памет", + "redis.memory-frag-ratio": "Коефициент на фрагментиране на паметта", + "redis.total-connections-recieved": "Общо получени свързвания", + "redis.total-commands-processed": "Общо обработени команди", + "redis.iops": "Едновременни операции в секунда", + "redis.iinput": "Едновременен вход в секунда", + "redis.ioutput": "Едновременен изход в секунда", + "redis.total-input": "Общ вход", + "redis.total-output": "Общ изход", + + "redis.keyspace-hits": "Успешни търсения на ключове", + "redis.keyspace-misses": "Неуспешни търсения на ключове", + "redis.raw-info": "Сурови данни от Redis", + + "postgres": "Postgres", + "postgres.version": "Версия на PostgreSQL", + "postgres.raw-info": "Сурови данни от Postgres" +} diff --git a/public/language/bg/admin/advanced/errors.json b/public/language/bg/admin/advanced/errors.json new file mode 100644 index 0000000000..42a05713fa --- /dev/null +++ b/public/language/bg/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Фигура %1", + "error-events-per-day": "%1 събития на ден", + "error.404": "Страницата не е намерена (Грешка 404)", + "error.503": "Услугата е недостъпна (Грешка 503)", + "manage-error-log": "Управление на журнала за грешки", + "export-error-log": "Изнасяне на журнала за грешки (CSV)", + "clear-error-log": "Изчистване на журнала за грешки", + "route": "Маршрут", + "count": "Брой", + "no-routes-not-found": "Ура! Няма грешки от вида „404“!", + "clear404-confirm": "Наистина ли искате да изчистите журналите за грешки от вида 404?", + "clear404-success": "Грешките от вида „Страницата не е намерена (Грешка 404)“ бяха изчистени." +} \ No newline at end of file diff --git a/public/language/bg/admin/advanced/events.json b/public/language/bg/admin/advanced/events.json new file mode 100644 index 0000000000..30175ef9a8 --- /dev/null +++ b/public/language/bg/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Събития", + "no-events": "Няма събития", + "control-panel": "Контролен панел за събитията", + "delete-events": "Изтриване на събитията", + "confirm-delete-all-events": "Наистина ли искате да изтриете всички събития в журнала?", + "filters": "Филтри", + "filters-apply": "Прилагане на филтрите", + "filter-type": "Вид събитие", + "filter-start": "Начална дата", + "filter-end": "Крайна дата", + "filter-perPage": "На страница" +} \ No newline at end of file diff --git a/public/language/bg/admin/advanced/logs.json b/public/language/bg/admin/advanced/logs.json new file mode 100644 index 0000000000..abb27d8fae --- /dev/null +++ b/public/language/bg/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Журнали", + "control-panel": "Контролен панел за журналите", + "reload": "Презареждане на журналите", + "clear": "Изчистване на журналите", + "clear-success": "Журналите са изчистени!" +} \ No newline at end of file diff --git a/public/language/bg/admin/appearance/customise.json b/public/language/bg/admin/appearance/customise.json new file mode 100644 index 0000000000..4f3ed970ec --- /dev/null +++ b/public/language/bg/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Персонализиран CSS/LESS", + "custom-css.description": "Въведете своите собствени декларации на CSS/LESS, те ще бъдат приложени след всички останали стилове.", + "custom-css.enable": "Включване на персонализиран CSS/LESS", + + "custom-js": "Персонализиран код на Javascript", + "custom-js.description": "Въведете свой собствен код на javascript тук. Той ще бъде изпълнен след като страницата се зареди напълно.", + "custom-js.enable": "Включване на персонализирания код на Javascript", + + "custom-header": "Персонализирана заглавна част", + "custom-header.description": "Въведете своя персонализиран код HTML тук (напр. елементи „meta“ и т.н.), те ще бъдат добавени към секцията <head> в кода на Вашия форум. Ползването на елементи „script“ е позволено, но непрепоръчително, тъй като за това можете да ползвате раздела Персонализиран код на Javascript.", + "custom-header.enable": "Включване на персонализирана заглавна част", + + "custom-css.livereload": "Включване на моменталното презареждане", + "custom-css.livereload.description": "Ако включите това, всички сесии на всяко устройство, където използвате акаунта си, ще се презареждат, когато натискате „Запазване“." +} \ No newline at end of file diff --git a/public/language/bg/admin/appearance/skins.json b/public/language/bg/admin/appearance/skins.json new file mode 100644 index 0000000000..a151ad7280 --- /dev/null +++ b/public/language/bg/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Зареждане на облиците…", + "homepage": "Начална страница", + "select-skin": "Изберете облик", + "current-skin": "Текущ облик", + "skin-updated": "Обликът е променен", + "applied-success": "Обликът „%1“ беше успешно приложен", + "revert-success": "Обликът е върнат към основните цветове." +} \ No newline at end of file diff --git a/public/language/bg/admin/appearance/themes.json b/public/language/bg/admin/appearance/themes.json new file mode 100644 index 0000000000..bd3f35ebce --- /dev/null +++ b/public/language/bg/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Проверяване за инсталирани теми…", + "homepage": "Начална страница", + "select-theme": "Изберете тема", + "current-theme": "Текуща тема", + "no-themes": "Няма намерени инсталирани теми", + "revert-confirm": "Наистина ли искате да възстановите стандартната тема на NodeBB?", + "theme-changed": "Темата е променена", + "revert-success": "Вие възстановихте успешно стандартната тема на NodeBB.", + "restart-to-activate": "Моля, изградете повторно и рестартирайте NodeBB, за да може тази тема да влезе в сила напълно." +} \ No newline at end of file diff --git a/public/language/bg/admin/dashboard.json b/public/language/bg/admin/dashboard.json new file mode 100644 index 0000000000..7a9cc4416c --- /dev/null +++ b/public/language/bg/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Трафик на форума", + "page-views": "Преглеждания на страниците", + "unique-visitors": "Уникални посетители", + "logins": "Вписвания", + "new-users": "Нови потребители", + "posts": "Публикации", + "topics": "Теми", + "page-views-seven": "Последните 7 дни", + "page-views-thirty": "Последните 30 дни", + "page-views-last-day": "Последните 24 часа", + "page-views-custom": "Интервал по избор", + "page-views-custom-start": "Начална дата", + "page-views-custom-end": "Крайна дата", + "page-views-custom-help": "Въведете интервал от дати, за които искате да видите преглежданията на страниците. Ако не се появи календар за избор, можете да въведете датите във формат: ГГГГ-ММ-ДД", + "page-views-custom-error": "Моля, въведете правилен интервал от дати във формата: ГГГГ-ММ-ДД", + + "stats.yesterday": "Вчера", + "stats.today": "Днес", + "stats.last-week": "Миналата седмица", + "stats.this-week": "Тази седмица", + "stats.last-month": "Миналия месец", + "stats.this-month": "Този месец", + "stats.all": "От началото", + + "updates": "Обновления", + "running-version": "Вие използвате NodeBB версия %1.", + "keep-updated": "Стремете се винаги да използвате най-новата версия на NodeBB, за да се възползвате от последните подобрения на сигурността и поправки на проблеми.", + "up-to-date": "

Вие използвате най-новата версия

", + "upgrade-available": "

Има нова версия (версия %1). Ако имате възможност, обновете NodeBB.

", + "prerelease-upgrade-available": "

Това е остаряла предварителна версия на NodeBB. Има нова версия (версия %1). Ако имате възможност, обновете NodeBB.

", + "prerelease-warning": "

Това е версия за предварителен преглед на NodeBB. Възможно е да има неочаквани неизправности.

", + "fallback-emailer-not-found": "Не е намерен резервен изпращач на е-поща", + "running-in-development": "Форумът работи в режим за разработчици, така че може да бъде уязвим. Моля, свържете се със системния си администратор.", + "latest-lookup-failed": "

Не може да бъде извършена проверка за последната налична версия на NodeBB

", + + "notices": "Забележки", + "restart-not-required": "Не се изисква рестартиране", + "restart-required": "Изисква се рестартиране", + "search-plugin-installed": "Добавката за търсене е инсталирана", + "search-plugin-not-installed": "Добавката за търсене не е инсталирана", + "search-plugin-tooltip": "Инсталирайте добавка за търсене от страницата с добавките, за да включите функционалността за търсене", + + "control-panel": "Системен контрол", + "rebuild-and-restart": "Повторно изграждане и рестартиране", + "restart": "Рестартиране", + "restart-warning": "Повторното изграждане и рестартирането на NodeBB ще прекъснат всички връзки за няколко секунди.", + "restart-disabled": "Възможностите за повторно изграждане и рестартиране на NodeBB са изключени, тъй като изглежда, че NodeBB не се изпълнява чрез подходящия демон.", + "maintenance-mode": "Режим на профилактика", + "maintenance-mode-title": "Щракнете тук, за да зададете режим на профилактика на NodeBB", + "realtime-chart-updates": "Актуализации на таблиците в реално време", + + "active-users": "Дейни потребители", + "active-users.users": "Потребители", + "active-users.guests": "Гости", + "active-users.total": "Общо", + "active-users.connections": "Връзки", + + "guest-registered-users": "Гости към регистрирани потребители", + "guest": "Гост", + "registered": "Регистрирани", + + "user-presence": "Присъствие на потребителите ", + "on-categories": "В списъка с категории", + "reading-posts": "Четящи публикации", + "browsing-topics": "Разглеждащи теми", + "recent": "Скорошни", + "unread": "Непрочетени", + + "high-presence-topics": "Теми с най-голяма присъственост", + "popular-searches": "Популярни търсения", + + "graphs.page-views": "Преглеждания на страниците", + "graphs.page-views-registered": "Преглеждания на страниците от регистрирани потребители", + "graphs.page-views-guest": "Преглеждания на страниците от гости", + "graphs.page-views-bot": "Преглеждания на страниците от ботове", + "graphs.unique-visitors": "Уникални посетители", + "graphs.registered-users": "Регистрирани потребители", + "graphs.guest-users": "Гости", + "last-restarted-by": "Последно рестартиране от", + "no-users-browsing": "Няма разглеждащи потребители", + + "back-to-dashboard": "Назад към таблото", + "details.no-users": "В избрания период не са се регистрирали нови потребители", + "details.no-topics": "В избрания период не са публикувани нови теми", + "details.no-searches": "Все още не са правени търсения", + "details.no-logins": "В избрания период не са отчетени вписвания", + "details.logins-static": "NodeBB запазва данни за сесията в продължение на %1 дни, така че в следната таблица могат да се видят само последните активни сесии", + "details.logins-login-time": "Време на вписване" +} diff --git a/public/language/bg/admin/development/info.json b/public/language/bg/admin/development/info.json new file mode 100644 index 0000000000..08f70c0692 --- /dev/null +++ b/public/language/bg/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Вие сте на %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 възела отговориха в рамките на %2мсек!", + "host": "сървър", + "primary": "основен / изпълнение на задачите", + "pid": "ид. на процеса", + "nodejs": "nodejs", + "online": "на линия", + "git": "git", + "process-memory": "памет на процеса", + "system-memory": "системна памет", + "used-memory-process": "Използвана памет от процеса", + "used-memory-os": "Използвана системна памет", + "total-memory-os": "Обща системна памет", + "load": "натоварване на системата", + "cpu-usage": "използване на процесора", + "uptime": "активно време", + + "registered": "Регистрирани", + "sockets": "Сокети", + "guests": "Гости", + + "info": "Информация" +} \ No newline at end of file diff --git a/public/language/bg/admin/development/logger.json b/public/language/bg/admin/development/logger.json new file mode 100644 index 0000000000..26550c056a --- /dev/null +++ b/public/language/bg/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Настройки на журнала", + "description": "Ако поставите отметки тук, Вие ще виждате журнала в терминала си. Ако посочите пътечка, то вместо това журналите ще бъдат записвани във файл. Журналът чрез HTTP е удобен за получаване на статистика за това кога, кои и какви хора посещават форума Ви. В допълнение към следенето на заявките чрез HTTP, можем също да следим и събитията на socket.io. Журналът на Socket.io, в комбинация с redis-cli, може да Ви бъде много полезно, ако искате да разучите как работи NodeBB.", + "explanation": "За да включите или изключите журналите в реално време, просто поставете или премахнете отметките в настройките на журнала. Няма нужда от рестартиране.", + "enable-http": "Включване на журнала чрез HTTP", + "enable-socket": "Включване на журналите за събития на socket.io", + "file-path": "Път до файла на журнала", + "file-path-placeholder": "/път/до/файла/на/журнала.log ::: ако е празно, журналът ще се извежда в терминала", + + "control-panel": "Контролен панел за журнала", + "update-settings": "Промяна на настройките на журнала" +} \ No newline at end of file diff --git a/public/language/bg/admin/extend/plugins.json b/public/language/bg/admin/extend/plugins.json new file mode 100644 index 0000000000..2fe0b019e1 --- /dev/null +++ b/public/language/bg/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Популярни", + "installed": "Инсталирани", + "active": "Включени", + "inactive": "Изключени", + "out-of-date": "Остарели", + "none-found": "Няма намерени добавки.", + "none-active": "Няма включени добавки.", + "find-plugins": "Търсене на добавки", + + "plugin-search": "Търсене на добавки", + "plugin-search-placeholder": "Търсене на добавка…", + "submit-anonymous-usage": "Изпращане на анонимни данни за употребата на добавката", + "reorder-plugins": "Пренареждане на добавките", + "order-active": "Подреждане на включените добавки", + "dev-interested": "Искате ли да пишете добавки за NodeBB?", + "docs-info": "Пълната документация относно създаването на добавки може да бъде намерена в портала за документация на NodeBB.", + + "order.description": "Някои добавки работят най-добре, ако бъдат инсталирани преди или след други добавки.", + "order.explanation": "Добавките се зареждат в реда, посочен тук, от горе надолу.", + + "plugin-item.themes": "Теми", + "plugin-item.deactivate": "Изключване", + "plugin-item.activate": "Включване", + "plugin-item.install": "Инсталиране", + "plugin-item.uninstall": "Деинсталиране", + "plugin-item.settings": "Настройки", + "plugin-item.installed": "Инсталирани", + "plugin-item.latest": "Най-нови", + "plugin-item.upgrade": "Обновяване", + "plugin-item.more-info": "За повече информация", + "plugin-item.unknown": "Неизвестно", + "plugin-item.unknown-explanation": "Състоянието на тази добавка не може да бъде определено, може би поради грешка в конфигурацията.", + "plugin-item.compatible": "Тази добавка работи с NodeBB %1", + "plugin-item.not-compatible": "Тази добавка няма информация за съвместимост. Уверете се, че работи, преди да я инсталирате на истинския си сървър.", + + "alert.enabled": "Добавката е включена", + "alert.disabled": "Добавката е изключена", + "alert.upgraded": "Добавката е обновена", + "alert.installed": "Добавката е инсталирана", + "alert.uninstalled": "Добавката е деинсталирана", + "alert.activate-success": "Моля, изградете повторно и презаредете NodeBB, за да активирате напълно тази добавка.", + "alert.deactivate-success": "Добавката е изключена успешно.", + "alert.upgrade-success": "Моля, изградете повторно и презаредете NodeBB, за да обновите тази добавка напълно.", + "alert.install-success": "Добавката е инсталирана успешно, моля, включете я", + "alert.uninstall-success": "Добавката беше изключена и деинсталирана успешно.", + "alert.suggest-error": "

NodeBB не може да се свърже с пакетния мениджър. Искате ли да продължите с инсталацията на най-новата версия?

Сървърът върна (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB не може да се свърже с пакетния мениджър. Не се препоръчва обновяване в момента.

", + "alert.incompatible": "

Вашата версия на NodeBB (версия %1) може да използва най-много версия %2 на тази добавка. Моля, обновете NodeBB, ако искате да инсталирате по-нова версия на тази добавка.

", + "alert.possibly-incompatible": "

Няма информация за съвместимостта

Тази добавка не е посочила конкретна версия за инсталация, съвместима с Вашата версия на NodeBB. Не можем да гарантираме пълна съвместимост и има възможност Вашият NodeBB да не може да стартира правилно.

Ако NodeBB не може да стартира, използвайте следната команда:

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

Искате ли да продължите с инсталацията на най-новата версия на тази добавка?

", + "alert.reorder": "Добавките са пренаредени", + "alert.reorder-success": "Моля, изградете повторно и рестартирайте NodeBB, за да завърши този процес напълно.", + + "license.title": "Информация за лиценза на добавката", + "license.intro": "Добавката „%1“ използва лиценза „%2“. Моля, прочетете условията на лиценза и се уверете, че ги разбирате, преди да включите добавката.", + "license.cta": "Искате ли да продължите с включването на тази добавка?" +} diff --git a/public/language/bg/admin/extend/rewards.json b/public/language/bg/admin/extend/rewards.json new file mode 100644 index 0000000000..d8860e3193 --- /dev/null +++ b/public/language/bg/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Награди", + "condition-if-users": "Ако потребителският(ата/ото)", + "condition-is": "Е:", + "condition-then": "Тогава:", + "max-claims": "Колко пъти може да бъде получавана наградата", + "zero-infinite": "0 = безкраен брой пъти", + "delete": "Изтриване", + "enable": "Включване", + "disable": "Изключване", + + "alert.delete-success": "Наградата е изтрита успешно", + "alert.no-inputs-found": "Неправомерна награда — няма нищо въведено!", + "alert.save-success": "Наградите са запазени успешно" +} \ No newline at end of file diff --git a/public/language/bg/admin/extend/widgets.json b/public/language/bg/admin/extend/widgets.json new file mode 100644 index 0000000000..7582a90490 --- /dev/null +++ b/public/language/bg/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Налични джаджи", + "explanation": "Изберете джаджа от падащото меню, а след това я завлачете и пуснете в областта за джаджи в някой от шаблоните вляво.", + "none-installed": "Няма намерени джаджи! Включете добавката с основните джаджи в контролния панел за добавките.", + "clone-from": "Клониране на джаджите от", + "containers.available": "Налични контейнери", + "containers.explanation": "Завлачете и пуснете върху някоя активна джаджа", + "containers.none": "Няма", + "container.well": "Кладенец", + "container.jumbotron": "Джъмботрон", + "container.panel": "Панел", + "container.panel-header": "Заглавна част на панел", + "container.panel-body": "Основна част на панел", + "container.alert": "Предупреждение", + + "alert.confirm-delete": "Наистина ли искате да изтриете джаджата?", + "alert.updated": "Джаджите са обновени", + "alert.update-success": "Джаджите са обновени успешно", + "alert.clone-success": "Джаджите са клонирани успешно", + + "error.select-clone": "Изберете страница, от която да клонирате", + + "title": "Заглавие", + "title.placeholder": "Заглавие (показва се само в някои контейнери)", + "container": "Контейнер", + "container.placeholder": "Завлачете и пуснете контейнер или въведете HTML тук.", + "show-to-groups": "Показване на групите", + "hide-from-groups": "Скриване от групите", + "hide-on-mobile": "Скриване на мобилни устройства" +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/admins-mods.json b/public/language/bg/admin/manage/admins-mods.json new file mode 100644 index 0000000000..80fb8c944c --- /dev/null +++ b/public/language/bg/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Администратори", + "global-moderators": "Глобални модератори", + "moderators": "Модератори", + "no-global-moderators": "Няма глобални модератори", + "no-sub-categories": "Няма подкатегории", + "subcategories": "%1 подкатегории", + "no-moderators": "Няма модератори", + "add-administrator": "Добавяне на администратор", + "add-global-moderator": "Добавяне на глобален модератор", + "add-moderator": "Добавяне на модератор" +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/categories.json b/public/language/bg/admin/manage/categories.json new file mode 100644 index 0000000000..a024272dc9 --- /dev/null +++ b/public/language/bg/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Настройки на категорията", + "privileges": "Правомощия", + + "name": "Име на категорията", + "description": "Описание на категорията", + "bg-color": "Цвят на фона", + "text-color": "Цвят на текста", + "bg-image-size": "Размер на фоновото изображение", + "custom-class": "Персонализиран клас", + "num-recent-replies": "Брой на скорошните отговори", + "ext-link": "Външна връзка", + "subcategories-per-page": "Брой подкатегории на страница", + "is-section": "Използване на тази категория като раздел", + "post-queue": "Опашка за публикации", + "tag-whitelist": "Списък от разрешени етикети", + "upload-image": "Качване на изображение", + "delete-image": "Премахване", + "category-image": "Изображение на категорията", + "parent-category": "Базова категория", + "optional-parent-category": "(Незадължително) Базова категория", + "top-level": "Най-горно ниво", + "parent-category-none": "(Няма)", + "copy-parent": "Копиране на базовата", + "copy-settings": "Копиране на настройките от", + "optional-clone-settings": "(Незадължително) Копиране на настройките от категория", + "clone-children": "Клониране на дъщерните категории и настройки", + "purge": "Изтриване на категорията", + + "enable": "Включване", + "disable": "Изключване", + "edit": "Редактиране", + "analytics": "Анализи", + "view-category": "Преглед на категорията", + "set-order": "Запазване на реда", + "set-order-help": "Задаването на позиция за категорията ще я премести на желаното място и ще промени местата на другите категории, ако е необходимо. Най-малкият възможен номер е 1, което ще постави категорията най-отгоре.", + + "select-category": "Изберете категория", + "set-parent-category": "Задайте базова категория", + + "privileges.description": "В тази секция можете да настроите правомощията за достъп до различните части на уеб сайта Правомощията могат да бъдат давани на отделни потребители или на цели групи. Изберете обхвата на приложение от падащото меню по-долу.", + "privileges.category-selector": "Настройване на правомощията за ", + "privileges.warning": "Забележка: Настройките за правомощията влизат в сила моментално. Не е нужно да запазвате категорията след като промените тези настройки.", + "privileges.section-viewing": "Правомощия за преглед", + "privileges.section-posting": "Правомощия за публикуване", + "privileges.section-moderation": "Правомощия за модериране", + "privileges.section-other": "Други", + "privileges.section-user": "Потребител", + "privileges.search-user": "Добавяне на потребител", + "privileges.no-users": "В тази категория няма правомощия за отделни потребители.", + "privileges.section-group": "Група", + "privileges.group-private": "Тази група е частна", + "privileges.inheritance-exception": "Тази група не наследява правомощията от групата на регистрираните потребители", + "privileges.banned-user-inheritance": "Блокираните потребители наследяват правомощията от групата на блокираните потребители", + "privileges.search-group": "Добавяне на група", + "privileges.copy-to-children": "Копиране в наследниците", + "privileges.copy-from-category": "Копиране от категория", + "privileges.copy-privileges-to-all-categories": "Копиране във всички категории", + "privileges.copy-group-privileges-to-children": "Копиране на правомощията на тази група в поделементите на тази категория.", + "privileges.copy-group-privileges-to-all-categories": "Копиране на правомощията на тази група във всички категории.", + "privileges.copy-group-privileges-from": "Копиране на правомощията на тази група от друга категория.", + "privileges.inherit": "Ако групата на регистрираните потребители получи дадено правомощие, всички останали групи го получават като подразбиращо се правомощие, дори то да не им е специално дадено. Вие виждате това подразбиращо се правомощие, защото всички потребители са членове на групата на регистрираните потребители, така че няма нужда да се дават едни и същи правомощия на още групи.", + "privileges.copy-success": "Правомощията са копирани!", + + "analytics.back": "Назад към списъка с категориите", + "analytics.title": "Аналитични данни за категорията „%1“", + "analytics.pageviews-hourly": "Фигура 1 – Преглеждания на час за тази категория", + "analytics.pageviews-daily": "Фигура 2 – Преглеждания на ден за тази категория", + "analytics.topics-daily": "Фигура 3 – Брой теми в тази категория на ден", + "analytics.posts-daily": "Фигура 4 – Брой публикации в тази категория на ден", + + "alert.created": "Създадена", + "alert.create-success": "Категорията е създадена успешно!", + "alert.none-active": "Нямате активни категории.", + "alert.create": "Създаване на категория", + "alert.confirm-purge": "

Наистина ли искате да изтриете категорията „%1“?

Внимание! Всички теми и публикации в тази категория ще бъдат изтрити!

Изтриването на категорията ще премахне всички теми и публикации, и ще изтрие категорията от базата данни. Ако искате да премахнете категорията временно, можете просто да я „изключите“.

", + "alert.purge-success": "Категорията е изтрита!", + "alert.copy-success": "Настройките са копирани!", + "alert.set-parent-category": "Задаване на базова категория", + "alert.updated": "Обновени категории", + "alert.updated-success": "Категориите с идентификатори %1 са обновени успешно.", + "alert.upload-image": "Качване на изображение за категорията", + "alert.find-user": "Търсене на потребител", + "alert.user-search": "Потърсете потребител тук…", + "alert.find-group": "Търсене на група", + "alert.group-search": "Потърсете група тук…", + "alert.not-enough-whitelisted-tags": "Разрешените етикети са по-малко от минимума. Трябва да създадете още разрешени етикети!", + "collapse-all": "Свиване на всички", + "expand-all": "Разгъване на всички", + "disable-on-create": "Изключване при създаване", + "no-matches": "Няма съвпадения" +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/digest.json b/public/language/bg/admin/manage/digest.json new file mode 100644 index 0000000000..13da1c1476 --- /dev/null +++ b/public/language/bg/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "По-долу са показани статистики и времена за разпращането на резюмета.", + "disclaimer": "Имайте предвид, че при доставката на е-поща няма гаранции, поради същността на технологията за е-пощата. Много неща влияят на това дали едно изпратено е-писмо настина достига до получателя си, като: репутация на сървъра, блокирани IP адреси или това дали е настроено DKIM/SPF/DMARC.", + "disclaimer-continued": "Успешната доставка означава, че съобщението е изпратено успешно от NodeBB и потвърдено от сървъра на получателя. Това не означава, че писмото е достигнало до входящата кутия на получателя. За да имате по-добри резултати, препоръчвам използването на специализирана услуга за изпращане на е-писма, като SendGrid.", + + "user": "Потребител", + "subscription": "Вид на абонамента", + "last-delivery": "Последна успешна доставка", + "default": "По подразбиране за системата", + "default-help": "По подразбиране за системата означава, че потребителят не е избрал ръчно друга настройка за глобалния форум за резюметата, която в момента е;„%1“", + "resend": "Повторно изпращане на резюмето", + "resend-all-confirm": "Наистина ли искате да предизвикате ръчно изпращането на резюмето?", + "resent-single": "Ръчното повторно разпращане на резюмето е завършено", + "resent-day": "Дневното резюме беше изпратено повторно", + "resent-week": "Седмичното резюме беше изпратено повторно", + "resent-biweek": "Двуседмичното резюме беше изпратено повторно", + "resent-month": "Месечното резюме беше изпратено повторно", + "null": "Никога", + "manual-run": "Ръчно разпращане на резюмето:", + + "no-delivery-data": "Няма данни за доставката" +} diff --git a/public/language/bg/admin/manage/groups.json b/public/language/bg/admin/manage/groups.json new file mode 100644 index 0000000000..9cf6f42ff8 --- /dev/null +++ b/public/language/bg/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Име на групата", + "badge": "Емблема", + "properties": "Свойства", + "description": "Описание на групата", + "member-count": "Брой на членовете", + "system": "Системна", + "hidden": "Скрита", + "private": "Частна", + "edit": "Редактиране", + "delete": "Изтриване", + "privileges": "Правомощия", + "download-csv": "CSV", + "search-placeholder": "Търсене", + "create": "Създаване на група", + "description-placeholder": "Кратко описание на групата", + "create-button": "Създаване", + + "alerts.create-failure": "Опа!

Възникна проблем при създаването на групата. Моля, опитайте отново по-късно!

", + "alerts.confirm-delete": "Наистина ли искате да изтриете тази група?", + + "edit.name": "Име", + "edit.description": "Описание", + "edit.user-title": "Звание на членовете", + "edit.icon": "Иконка на групата", + "edit.label-color": "Цвята за етикета на групата", + "edit.text-color": "Цвята за текста на групата", + "edit.show-badge": "Показване на емблема", + "edit.private-details": "Ако е включено, присъединяването към група ще изисква одобрение от собственик на групата.", + "edit.private-override": "Внимание: Частните групи са изключени на системно ниво, това пренебрегва тази настройка.", + "edit.disable-join": "Забраняване на заявките за присъединяване", + "edit.disable-leave": "Забраняване на потребители да напускат групата", + "edit.hidden": "Скрита", + "edit.hidden-details": "Ако е включено, групата няма да е видима в списъка с групи и ще трябва потребителите да бъдат поканени специално.", + "edit.add-user": "Добавяне на потребител към групата", + "edit.add-user-search": "Търсене на потребители", + "edit.members": "Списък на членовете", + "control-panel": "Контролен панел за групите", + "revert": "Отмяна", + + "edit.no-users-found": "Няма намерени потребители", + "edit.confirm-remove-user": "Наистина ли искате да премахнете този потребител?", + "edit.save-success": "Промените са запазени!" +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/privileges.json b/public/language/bg/admin/manage/privileges.json new file mode 100644 index 0000000000..9ca0014432 --- /dev/null +++ b/public/language/bg/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Глобални", + "admin": "Администратор", + "group-privileges": "Правомощия за групите", + "user-privileges": "Правомощия за потребителите", + "edit-privileges": "Редактиране на правомощията", + "select-clear-all": "Избиране/изчистване на всичко", + "chat": "Разговор", + "upload-images": "Качване на изображения", + "upload-files": "Качване на файлове", + "signature": "Подпис", + "ban": "Блокиране", + "mute": "Заглушаване", + "invite": "Пращане на покана", + "search-content": "Търсене на съдържание", + "search-users": "Търсене на потребители", + "search-tags": "Търсене на етикети", + "view-users": "Преглед на потребителите", + "view-tags": "Преглед на етикетите", + "view-groups": "Преглед на групите", + "allow-local-login": "Локално вписване", + "allow-group-creation": "Създаване на групи", + "view-users-info": "Преглед на информацията за потребителите", + "find-category": "Търсене на категория", + "access-category": "Достъп до категория", + "access-topics": "Достъп до теми", + "create-topics": "Създаване на теми", + "reply-to-topics": "Отговаряне в теми", + "schedule-topics": "Насрочване на теми", + "tag-topics": "Поставяне на етикети на теми", + "edit-posts": "Редактиране на публикации", + "view-edit-history": "Преглед на историята на редакциите", + "delete-posts": "Изтриване на публикации", + "view_deleted": "Преглед на изтритите публикации", + "upvote-posts": "Положително гласуване за публикации", + "downvote-posts": "Отрицателно гласуване за публикации", + "delete-topics": "Изтриване на теми", + "purge": "Изчистване", + "moderate": "Модериране", + "admin-dashboard": "Табло", + "admin-categories": "Категории", + "admin-privileges": "Правомощия", + "admin-users": "Потребители", + "admin-admins-mods": "Администратори и модератори", + "admin-groups": "Групи", + "admin-tags": "Етикети", + "admin-settings": "Настройки", + + "alert.confirm-moderate": "Наистина ли искате да дадете правомощието за модериране на тази потребителска група? Тази група е публична и всеки може свободно да се присъедини към нея.", + "alert.confirm-admins-mods": "Наистина ли искате да дадете правото „Администратори и модератори“ на този потребител/група? Потребителите с това право могат да променят правомощията на други групи, включително да им дават правото на супер администратори", + "alert.confirm-save": "Моля, потвърдете желанието си да запазите тези правомощия", + "alert.saved": "Промените по правомощията са запазени и приложени", + "alert.confirm-discard": "Наистина ли искате да отхвърлите промените по правомощията?", + "alert.discarded": "Промените по правомощията са отхвърлени", + "alert.confirm-copyToAll": "Наистина ли искате да приложите този набор от %1 към всички категории?", + "alert.confirm-copyToAllGroup": "Наистина ли искате да приложите набора от %1 на тази група към всички категории?", + "alert.confirm-copyToChildren": "Наистина ли искате да приложите този набор от %1 към всички по-долни (дъщерни) категории?", + "alert.confirm-copyToChildrenGroup": "Наистина ли искате да приложите набора от %1 на тази група към всички по-долни (дъщерни) категории?", + "alert.no-undo": "Това действие е необратимо.", + "alert.admin-warning": "Администраторите имат всички правомощия по подразбиране", + "alert.copyPrivilegesFrom-title": "Изберете категория, от която да се копира", + "alert.copyPrivilegesFrom-warning": "Това ще копира %1 от избраната категория.", + "alert.copyPrivilegesFromGroup-warning": "Това ще копира набора от %1 на тези група от избраната категория." +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/registration.json b/public/language/bg/admin/manage/registration.json new file mode 100644 index 0000000000..f4b89e0cac --- /dev/null +++ b/public/language/bg/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Опашка", + "description": "Няма потребители в регистрационната опашка.
За да включите тази функционалност, отидете в Настройки → Потребител → Регистриране на потребителите и задайте Вид регистриране на „Одобрение от администратор“.", + + "list.name": "Няма", + "list.email": "Е-поща", + "list.ip": "IP адрес", + "list.time": "Време", + "list.username-spam": "Честота: %1 Появяване: %2 Увереност: %3", + "list.email-spam": "Честота: %1 Появяване: %2", + "list.ip-spam": "Честота: %1 Появяване: %2", + + "invitations": "Покани", + "invitations.description": "По-долу ще намерите пълен списък от изпратените покани. Използвайте „Ctrl-F“, за да търсите е-поща или потребителско име в списъка.

Потребителското име ще бъде показано вдясно от е-пощата за потребителите, които са приели поканата си.", + "invitations.inviter-username": "Потребителско име на канещия", + "invitations.invitee-email": "Е-поща на поканения", + "invitations.invitee-username": "Потребителско име на поканения (ако е регистриран)", + + "invitations.confirm-delete": "Наистина ли искате да изтриете тази покана?" +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/tags.json b/public/language/bg/admin/manage/tags.json new file mode 100644 index 0000000000..901707a7da --- /dev/null +++ b/public/language/bg/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Форумът все още няма теми с етикети.", + "bg-color": "Цвят на фона", + "text-color": "Цвят на текста", + "description": "Изберете етикетите чрез щракване или влачене. Използвайте CTRL, за да изберете няколко етикета.", + "create": "Създаване на етикет", + "modify": "Редактиране на етикети", + "rename": "Преименуване на етикети", + "delete": "Изтриване на избраните етикети", + "search": "Търсене на етикети…", + "settings": "Настройки за етикетите", + "name": "Име на етикета", + + "alerts.editing": "Редактиране на етикет(и)", + "alerts.confirm-delete": "Наистина ли искате да изтриете избраните етикети?", + "alerts.update-success": "Етикетът е променен!", + "reset-colors": "Възстановяване на стандартните цветовете" +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/uploads.json b/public/language/bg/admin/manage/uploads.json new file mode 100644 index 0000000000..215dc231ff --- /dev/null +++ b/public/language/bg/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Качване на файл", + "filename": "Име на файла", + "usage": "Използване в публикации", + "orphaned": "Без ползвания", + "size/filecount": "Размер / брой файлове", + "confirm-delete": "Наистина ли искате да изтриете този файл?", + "filecount": "%1 файла", + "new-folder": "Нова папка", + "name-new-folder": "Въведете име за новата папка" +} \ No newline at end of file diff --git a/public/language/bg/admin/manage/users.json b/public/language/bg/admin/manage/users.json new file mode 100644 index 0000000000..20f9525466 --- /dev/null +++ b/public/language/bg/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Потребители", + "edit": "Действия", + "make-admin": "Даване на администраторски права", + "remove-admin": "Отнемане на администраторски права", + "validate-email": "Проверка на е-пощата", + "send-validation-email": "Изпращане на е-писмо за потвърждение", + "password-reset-email": "Изпращане на е-писмо за възстановяване на паролата", + "force-password-reset": "Принудително подновяване на паролата и отписване на потребителя", + "ban": "Блокиране на потребителя/ите", + "temp-ban": "Блокиране на потребителя/ите временно", + "unban": "Деблокиране на потребителя/ите", + "reset-lockout": "Нулиране на заключването", + "reset-flags": "Анулиране на докладите", + "delete": "Изтриване на потребителя/ите", + "delete-content": "Изтриване на съдържанието на потребителя/ите", + "purge": "Изтриване на потребителя/ите и съдържанието", + "download-csv": "Сваляне във формат „CSV“", + "manage-groups": "Управление на групите", + "add-group": "Добавяне на група", + "create": "Създаване на потребител", + "invite": "Поканване по е-поща", + "new": "Нов потребител", + "filter-by": "Филтриране по", + "pills.unvalidated": "Няма потвърдена е-поща", + "pills.validated": "Потвърдена", + "pills.banned": "Блокиран", + + "50-per-page": "50 на страница", + "100-per-page": "100 на страница", + "250-per-page": "250 на страница", + "500-per-page": "500 на страница", + + "search.uid": "По потребителски идентификатор", + "search.uid-placeholder": "Въведете потребителски идентификатор, който да потърсите", + "search.username": "По име на потребител", + "search.username-placeholder": "Въведете потребителско име, което да потърсите", + "search.email": "По е-поща", + "search.email-placeholder": "Въведете е-поща, която да потърсите", + "search.ip": "По IP адрес", + "search.ip-placeholder": "Въведете IP адрес, който да потърсите", + "search.not-found": "Потребителят не е намерен!", + + "inactive.3-months": "3 месеца", + "inactive.6-months": "6 месеца", + "inactive.12-months": "12 месеца", + + "users.uid": "потр. ид.", + "users.username": "потребителско име", + "users.email": "е-поща", + "users.no-email": "(няма е-поща)", + "users.ip": "IP адрес", + "users.postcount": "брой публикации", + "users.reputation": "репутация", + "users.flags": "доклади", + "users.joined": "присъединил се", + "users.last-online": "последно на линия", + "users.banned": "блокиран", + + "create.username": "Потребителско име", + "create.email": "Е-поща", + "create.email-placeholder": "Е-поща на този потребител", + "create.password": "Парола", + "create.password-confirm": "Потвърдете паролата", + + "temp-ban.length": "Продължителност", + "temp-ban.reason": "Причина (незадължително)", + "temp-ban.hours": "Часове", + "temp-ban.days": "Дни", + "temp-ban.explanation": "Въведете продължителността на блокирането. Стойност от 0 ще направи блокирането за постоянно.", + + "alerts.confirm-ban": "Наистина ли искате да блокирате този потребител за постоянно?", + "alerts.confirm-ban-multi": "Наистина ли искате да блокирате тези потребители за постоянно?", + "alerts.ban-success": "Потребителят/ите е/са блокиран(и)!", + "alerts.button-ban-x": "Блокиране на %1 потребител(и)", + "alerts.unban-success": "Потребителят/ите е/са деблокиран(и)!", + "alerts.lockout-reset-success": "Заключването/ията е/са нулирано/и!", + "alerts.flag-reset-success": "Докладът/ите е/са анулиран(и)!", + "alerts.no-remove-yourself-admin": "Не можете да отнемете собствените си права на администратор!", + "alerts.make-admin-success": "Потребителят вече ще бъде администратор.", + "alerts.confirm-remove-admin": "Наистина ли искате да премахнете този администратор?", + "alerts.remove-admin-success": "Потребителят вече няма да бъде администратор.", + "alerts.make-global-mod-success": "Потребителят вече ще бъде глобален модератор.", + "alerts.confirm-remove-global-mod": "Наистина ли искате да премахнете този глобален модератор?", + "alerts.remove-global-mod-success": "Потребителят вече няма да бъде глобален модератор.", + "alerts.make-moderator-success": "Потребителят вече ще бъде модератор.", + "alerts.confirm-remove-moderator": "Наистина ли искате да премахнете този модератор?", + "alerts.remove-moderator-success": "Потребителят вече няма да бъде модератор.", + "alerts.confirm-validate-email": "Искате ли да проверите е-пощата/ите на този/тези потребител(и)?", + "alerts.confirm-force-password-reset": "Наистина ли искате принудително да подновите паролата и да отпишете потребителя или потребителите?", + "alerts.validate-email-success": "Е-пощите са проверени", + "alerts.validate-force-password-reset-success": "Паролата на потребителя (или паролите на потребителите) беше подновена и сесията му беше прекратена.", + "alerts.password-reset-confirm": "Искате ли да изпратите е-писмо/а за възстановяване на паролата на този/тези потребител(и)?", + "alerts.password-reset-email-sent": "Е-писмото за възстановяване на паролата е изпратено.", + "alerts.confirm-delete": "ВНИМАНИЕ!

Наистина ли искате да изтриете потребителя/ите?

Това действие е необратимо! Ще бъде изтрит само профилът на потребителя/ите, неговите/техните публикациите и теми ще останат.

", + "alerts.delete-success": "Потребителят/ите е/са изтрит(и)!", + "alerts.confirm-delete-content": "ВНИМАНИЕ!

Наистина ли искате да изтриете съдържанието на този потребител или тези потребители?

Това действие е необратимо! Профилите на потребителите ще останат, но всички техни публикации и теми ще бъдат изтрити.

", + "alerts.delete-content-success": "Съдържанието на потребителя/ите е изтрито!", + "alerts.confirm-purge": "ВНИМАНИЕ!

Наистина ли искате да изтриете потребителя/ите и неговото/тяхното съдържание?

Това действие е необратимо! Всички потребителски данни и съдържание ще бъдат заличени!

", + "alerts.create": "Създаване на потребител", + "alerts.button-create": "Създаване", + "alerts.button-cancel": "Отказ", + "alerts.error-passwords-different": "Паролите са различни!", + "alerts.error-x": "Грешка

%1

", + "alerts.create-success": "Потребителят е създаден!", + + "alerts.prompt-email": "Е-пощи: ", + "alerts.email-sent-to": "Беше изпратено е-писмо за потвърждение до %1", + "alerts.x-users-found": "Намерени потребители: %1 (%2 секунди)", + "export-users-started": "Изнасяне на потребителите във формат „csv“… Това може да отнеме известно време. Ще получите известие, когато е готово.", + "export-users-completed": "Потребителите са изнесени във формат „csv“, щракнете за сваляне." +} \ No newline at end of file diff --git a/public/language/bg/admin/menu.json b/public/language/bg/admin/menu.json new file mode 100644 index 0000000000..e5fb3489cb --- /dev/null +++ b/public/language/bg/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Табла", + "dashboard/overview": "Общ преглед", + "dashboard/logins": "Вписвания", + "dashboard/users": "Потребители", + "dashboard/topics": "Теми", + "dashboard/searches": "Търсения", + "section-general": "Общи", + + "section-manage": "Управление", + "manage/categories": "Категории", + "manage/privileges": "Правомощия", + "manage/tags": "Етикети", + "manage/users": "Потребители", + "manage/admins-mods": "Администратори и модератори", + "manage/registration": "Регистрационна опашка", + "manage/post-queue": "Опашка за публикации", + "manage/groups": "Групи", + "manage/ip-blacklist": "Черен списък за IP адреси", + "manage/uploads": "Качвания", + "manage/digest": "Резюмета", + + "section-settings": "Настройки", + "settings/general": "Общи", + "settings/homepage": "Начална страница", + "settings/navigation": "Навигация", + "settings/reputation": "Репутация и доклади", + "settings/email": "Е-поща", + "settings/user": "Потребители", + "settings/group": "Групи", + "settings/guest": "Гости", + "settings/uploads": "Качвания", + "settings/languages": "Езици", + "settings/post": "Публикации", + "settings/chat": "Разговори", + "settings/pagination": "Странициране", + "settings/tags": "Етикети", + "settings/notifications": "Известия", + "settings/api": "Достъп чрез ППИ", + "settings/sounds": "Звуци", + "settings/social": "Обществени", + "settings/cookies": "Бисквитки", + "settings/web-crawler": "Обхождач на уеб страници", + "settings/sockets": "Сокети", + "settings/advanced": "Разширени", + + "settings.page-title": "Настройки на %1", + + "section-appearance": "Външен вид", + "appearance/themes": "Теми", + "appearance/skins": "Облици", + "appearance/customise": "Персонализирано съдържание (HTML/JS/CSS)", + + "section-extend": "Разширяване", + "extend/plugins": "Добавки", + "extend/widgets": "Джаджи", + "extend/rewards": "Награди", + + "section-social-auth": "Обществено удостоверяване", + + "section-plugins": "Добавки", + "extend/plugins.install": "Инсталиране на добавки", + + "section-advanced": "Разширени", + "advanced/database": "База данни", + "advanced/events": "Събития", + "advanced/hooks": "Куки", + "advanced/logs": "Журнали", + "advanced/errors": "Грешки", + "advanced/cache": "Кеш", + "development/logger": "Система на журнала", + "development/info": "Информация", + + "rebuild-and-restart-forum": "Повторно изграждане и рестартиране на форума", + "restart-forum": "Рестартиране на форума", + "logout": "Изход", + "view-forum": "Преглед на форума", + + "search.placeholder": "Търсене на настройки", + "search.no-results": "Няма резултати…", + "search.search-forum": "Търсене във форума за ", + "search.keep-typing": "Продължете да пишете, за да видите още резултати…", + "search.start-typing": "Започнете да пишете, за да получите резултати…", + + "connection-lost": "Връзката към %1 беше прекъсната. опитваме се да Ви свържем отново…", + + "alerts.version": "Използва се NodeBB версия %1", + "alerts.upgrade": "Обновяване до v%1" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/advanced.json b/public/language/bg/admin/settings/advanced.json new file mode 100644 index 0000000000..7992505008 --- /dev/null +++ b/public/language/bg/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Режим на профилактика", + "maintenance-mode.help": "Когато форумът е в режим на профилактика, всички заявки ще бъдат пренасочвани към статична страница за изчакване, с изключение на администраторите, които ще могат да използват уеб сайта нормално.", + "maintenance-mode.status": "Код на състоянието за режима на профилактика", + "maintenance-mode.message": "Съобщение за профилактиката", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Изберете групи, които да бъдат изключени от режима на профилактика", + "headers": "Заглавни части", + "headers.allow-from": "Задайте „ALLOW-FROM“, за да поставите NodeBB в „iFrame“", + "headers.csp-frame-ancestors": "Задайте заглавката „Content-Security-Policy frame-ancestors“ за да поставите NodeBB „iFrame“", + "headers.csp-frame-ancestors-help": "„none“ (нищо), „self“ (себе си – по подразбиране) или списък от позволени адреси.", + "headers.powered-by": "Персонализиране на заглавната част „Захранван от“, която се изпраща от NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Регулярен израз за „Access-Control-Allow-Origin“", + "headers.acao-help": "За да забраните достъпа до всички уеб сайтове, оставете празно", + "headers.acao-regex-help": "Въведете регулярен израз за съвпадение с динамичните произходи. За да забраните достъпа на всички уеб сайтове, оставете това празно.", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Методи за разрешаване на управлението на достъпа", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "Когато е включено (по подразбиране), стойността на заглавката ще бъде require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Позволява задаването на стойност в заглавката „permissions-policy“ (политика за разрешенията), като например „geolocation=*, camera=()“. Вижте тук за повече информация.", + "hsts": "Стриктна транспортна сигурност", + "hsts.enabled": "Включване на HSTS (препоръчително)", + "hsts.maxAge": "Максимална възраст на HSTS", + "hsts.subdomains": "Включване на поддомейните в заглавката на HSTS", + "hsts.preload": "Позволяване на предварителното зареждане на заглавката на HSTS", + "hsts.help": "Ако това е включено, за този уеб ще бъде настроена заглавка за HSTS. Можете да изберете дали да включите поддомейните и дали за заредите предварително флаговете в заглавката си. Ако не знаете какво да направите, най-добре не избирайте нищо. Още информация", + "traffic-management": "Управление на трафика", + "traffic.help": "NodeBB използва модул, който автоматично отказва заявките в натоварените моменти. Можете да настроите поведението тук, въпреки че стойностите по подразбиране са добра отправна точка.", + "traffic.enable": "Включване на управлението на трафика", + "traffic.event-lag": "Граница на забавяне в цикъла на събитията (в милисекунди)", + "traffic.event-lag-help": "Намаляването на тази стойност ще намали времето за изчакване при зареждане на страници, но също така ще предизвика по-често показване на съобщението „прекомерно натоварване“ на повече потребители. (Нужно е рестартиране.)", + "traffic.lag-check-interval": "Интервал на проверка (в милисекунди)", + "traffic.lag-check-interval-help": "Намаляването на тази стойност ще направи NodeBB по-чувствителен към скоковете в натовареността, но може и да направи проверката твърде чувствителна. (Нужно е рестартиране.)", + + "sockets.settings": "Настройки за WebSocket", + "sockets.max-attempts": "Максимален брой опити за повторно свързване", + "sockets.default-placeholder": "По подразбиране: %1", + "sockets.delay": "Забавяне при повторно свързване", + + "analytics.settings": "Настройки за анализите", + "analytics.max-cache": "Макс. стойност на кеша за анализите", + "analytics.max-cache-help": "При инсталации с натоварен трафик, кешът може да бъде изразходен, ако има повече едновременни потребители, от колкото е максималната стойност на кеша. (Изисква рестартиране)", + "compression.settings": "Настройки за компресирането", + "compression.enable": "Включване на компресирането", + "compression.help": "Тази настройка включва компресирането чрез „gzip“. За натоварени уеб сайтове най-добрият начин за използване на компресия е тя да се случва на нивото на обратния сървър-посредник (reverse proxy). Но с цел тестване, можете да го включите и тук." +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/api.json b/public/language/bg/admin/settings/api.json new file mode 100644 index 0000000000..0e153563ba --- /dev/null +++ b/public/language/bg/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Кодове", + "settings": "Настройки", + "lead-text": "На тази страница можете да настроите достъпа до ППИ за писане в NodeBB.", + "intro": "По подразбиране ППИ за писане удостоверява потребителите чрез бисквитката им за сесията, но NodeBB поддържа и удостоверяване чрез метода „Bearer“, използвайки кодовете от тази страница.", + "docs": "Щракнете тук за достъп до пълната документация на ППИ", + + "require-https": "Ползването на ППИ да работи само чрез HTTPS", + "require-https-caveat": "Забележка: В някои случаи, когато се ползват програми за балансиране на натоварването, е възможно заявките към NodeBB да се препращат чрез HTTP – тогава тази настройка трябва да остане изключена.", + + "uid": "Потребителски ИД", + "uid-help-text": "Посочете потребителски ИД, който да бъде свързан с този код. Ако ИД е 0, това ще се счита за главен код, който може да приема идентичността на всеки от другите потребители чрез параметъра _uid", + "description": "Описание", + "no-description": "Няма описание.", + "token-on-save": "Кодът ще бъде създаден след като данните бъдат запазени" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/chat.json b/public/language/bg/admin/settings/chat.json new file mode 100644 index 0000000000..6f4e7627e9 --- /dev/null +++ b/public/language/bg/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Настройки на разговорите", + "disable": "Изключване на разговорите", + "disable-editing": "Изключване на редактирането и изтриването на съобщения в разговорите", + "disable-editing-help": "Това ограничение не засяга администраторите и глобалните модератори", + "max-length": "Максимална дължина на съобщенията в разговорите", + "max-room-size": "Максимален брой потребители в стая за разговор", + "delay": "Време между съобщенията в разговорите (в милисекунди)", + "notification-delay": "Забавяне преди известяване за съобщения в разговорите. (0 – без забавяне)", + "restrictions.seconds-edit-after": "Брой секунди, през които съобщенията в разговор могат да бъдат редактирани. (0 = изключено)", + "restrictions.seconds-delete-after": "Брой секунди, през които съобщенията в разговор могат да бъдат изтривани. (0 = изключено)" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/cookies.json b/public/language/bg/admin/settings/cookies.json new file mode 100644 index 0000000000..75d9527c19 --- /dev/null +++ b/public/language/bg/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Съглашение на ЕС", + "consent.enabled": "Включено", + "consent.message": "Съобщение за известие", + "consent.acceptance": "Съобщение за приемане", + "consent.link-text": "Връзка към текста на политиката", + "consent.link-url": "Връзка към адреса на политиката", + "consent.blank-localised-default": "Оставете това празно, за да използвате данните по подразбиране на NodeBB, които са преведени", + "settings": "Настройки", + "cookie-domain": "Домейн на бисквитката за сесията", + "max-user-sessions": "Максимален брой активни сесии за потребител", + "blank-default": "Оставете празно, за да използвате стойността по подразбиране" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/email.json b/public/language/bg/admin/settings/email.json new file mode 100644 index 0000000000..ccf78aba43 --- /dev/null +++ b/public/language/bg/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Настройки за е-пощата", + "address": "Адрес на е-пощата", + "address-help": "Следният адрес на е-поща е този, който получателят ще види в полетата „От“ и “Отговор до“.", + "from": "Име за полето „От“", + "from-help": "Името на изпращача, което да бъде показано в е-писмото.", + + "confirmation-settings": "Потвърждение", + "confirmation.expiry": "Продължителност на давността на връзката за потвърждаване, в часове", + + "smtp-transport": "Транспорт чрез SMTP", + "smtp-transport.enabled": "Включване на транспорта чрез SMTP", + "smtp-transport-help": "Можете да изберете от списък от познати услуги, или да въведете такава ръчно.", + "smtp-transport.service": "Изберете услуга", + "smtp-transport.service-custom": "Персонализирана услуга", + "smtp-transport.service-help": "Изберете името на услугата по-горе, за да използвате известните данни за нея. Или изберете „Персонализирана услуга“ и въведете данните ѝ по-долу.", + "smtp-transport.gmail-warning1": "Ако използвате GMail, ще трябва да създадете „Парола за приложение“, за да може NodeBB да използва данните за удостоверяване. Можете да създадете такава в страницата с Пароли за приложения.", + "smtp-transport.gmail-warning2": "За повече информация относно това обиколно решение, моля, прегледайте тази статия за проблема в „NodeMailer“. Друго решение би било използването на добавка за е-поща от трета страна, като например „SendGrid“, „Mailgun“ и т.н. Вижте наличните добавки тук.", + "smtp-transport.auto-enable-toast": "Изглежда настройвате функционалност, която изисква транспорт чрез SMTP. Включихме настройката „Транспорт чрез SMTP“, за да не го правите Вие.", + "smtp-transport.host": "SMTP сървър", + "smtp-transport.port": "SMTP порт", + "smtp-transport.security": "Сигурност на връзката", + "smtp-transport.security-encrypted": "Шифрована", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Няма", + "smtp-transport.username": "Потребителско име", + "smtp-transport.username-help": "За услугата на Gmail, въведете пълния адрес на е-пощата тук, особено ако използвате управляван домейн на „Google Apps“.", + "smtp-transport.password": "Парола", + "smtp-transport.pool": "Включване на групираните връзки", + "smtp-transport.pool-help": "Групирането на връзките предотвратява създаването на нова връзка за всяко е-писмо. Тази настройка има ефект, само ако е включено „Транспорт чрез SMTP“.", + + "template": "Редактирана не шаблона за е-писма", + "template.select": "Изберете шаблон за е-писма", + "template.revert": "Връщане на оригинала", + "testing": "Проба на е-писмата", + "testing.select": "Изберете шаблон за е-писма", + "testing.send": "Изпращане на пробно е-писмо", + "testing.send-help": "Пробното е-писмо ще бъде изпратено до е-пощата на текущо вписания потребител.", + "subscriptions": "Резюмета по е-поща", + "subscriptions.disable": "Изключване на резюметата по е-пощата", + "subscriptions.hour": "Време за разпращане", + "subscriptions.hour-help": "Моля, въведете число, представляващо часа, в който да се разпращат е-писма с подготвеното резюме (напр.. 0 за полунощ, 17 за 5 следобед). Имайте предвид, че този час е според часовата зона на сървъра и може да не съвпада с часовника на системата Ви.
Приблизителното време на сървъра е:
Изпращането на следващия ежедневен бюлетин е планирано за ", + "notifications.remove-images": "Премахване на изображенията от известията по е-поща", + "require-email-address": "Новите потребители задължително трябва да предоставят е-поща", + "require-email-address-warning": "По подразбиране потребителите могат да не въвеждат адрес на е-поща, като оставят полето празно. Ако включите това, те задължително ще трябва да предоставят е-поща, за да могат да се регистрират. Това не означава, че потребителят ще въведе съществуваща е-поща, нито че тя ще е негова.", + "send-validation-email": "Изпращане на е-писма за потвърждение, когато бъде добавена или променена е-поща", + "include-unverified-emails": "Изпращане на е-писма към получатели, които не са потвърдили изрично е-пощата си", + "include-unverified-warning": "За потребителите, които имат свързана е-поща с регистрацията си, тя се смята за потвърдена. Но има ситуации, в които това не е така (например при ползване на регистрация от друга система, но и в други случаи), Включете тази настройка на собствен риск – изпращането на е-писма към непотвърдени адреси може да нарушава определени местни закони против нежеланата поща.", + "prompt": "Подсещане на потребителите да въведат или потвърдят е-пощата си", + "prompt-help": "Ако потребител няма зададена е-поща, или ако тя не е потвърдена, на екрана му ще се покаже предупредително съобщение.", + "sendEmailToBanned": "Изпращане на е-писма дори до блокираните потребители" +} diff --git a/public/language/bg/admin/settings/general.json b/public/language/bg/admin/settings/general.json new file mode 100644 index 0000000000..f573592a11 --- /dev/null +++ b/public/language/bg/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Настройки на уеб сайта", + "title": "Заглавие на уеб сайта", + "title.short": "Кратко заглавие", + "title.short-placeholder": "Ако не е посочено кратко заглавие, ще бъде използвано заглавието на уеб сайта", + "title.url": "Адрес за заглавието", + "title.url-placeholder": "Адресът за заглавието на уеб сайта", + "title.url-help": "Когато потребител щракне върху заглавието, той ще бъде прехвърлен към този адрес. Ако е празно, потребителят ще бъде изпратен към началната страница на форума.
Забележка: Това не е външният адрес, който се ползва в е-писмата. Той се задава от свойството url във файла config.json", + "title.name": "Името на общността Ви", + "title.show-in-header": "Показване на заглавието на уеб сайта в заглавната част", + "browser-title": "Заглавие на браузъра", + "browser-title-help": "Ако не е посочено заглавие на браузъра, ще бъде използвано заглавието на уеб сайта", + "title-layout": "Разположение на заглавието", + "title-layout-help": "Определете как ще бъде структурирано заглавието на браузъра, например: {pageTitle} | {browserTitle}", + "description.placeholder": "Кратко описание на общността Ви", + "description": "Описание на уеб сайта", + "keywords": "Ключови думи на уеб сайта", + "keywords-placeholder": "Ключови думи, описващи общността Ви. Трябва да бъдат разделени със запетаи.", + "logo": "Лого на уеб сайта", + "logo.image": "Изображение", + "logo.image-placeholder": "Път до логото, което да бъде показано в заглавната част на форума", + "logo.upload": "Качване", + "logo.url": "Адрес за логото", + "logo.url-placeholder": "Адресът за логото на уеб сайта", + "logo.url-help": "Когато потребител щракне върху логото, той ще бъде прехвърлен към този адрес. Ако е празно, потребителят ще бъде изпратен към началната страница на форума.
Забележка: Това не е външният адрес, който се ползва в е-писмата. Той се задава от свойството url във файла config.json", + "logo.alt-text": "Алтернативен текст", + "log.alt-text-placeholder": "Алтернативен текст за достъпност", + "favicon": "Иконка на уеб сайта", + "favicon.upload": "Качване", + "pwa": "Прогресивно уеб-приложение", + "touch-icon": "Иконка за сензорен екран", + "touch-icon.upload": "Качване", + "touch-icon.help": "Препоръчителен размер и формат: 512x512, само във формат „PNG“. Ако не е посочена иконка за сензорен екран, NodeBB ще използва иконката на уеб сайта.", + "maskable-icon": "Маскируема иконка (за начален екран)", + "maskable-icon.help": "Препоръчителен размер и формат: 512x512, само във формат „PNG“. Ако не е посочена маскируема иконка, NodeBB ще използва иконката за сензорен екран.", + "outgoing-links": "Изходящи връзки", + "outgoing-links.warning-page": "Показване на предупредителна страница при щракване върху външни връзки", + "search": "Търсене", + "search-default-in": "Търсене в", + "search-default-in-quick": "Бързо търсене в", + "search-default-sort-by": "Подреждане по", + "outgoing-links.whitelist": "Домейни, за които да не се показва предупредителната страница", + "site-colors": "Мета-данни за цвета на уеб сайта", + "theme-color": "Цвят на темата", + "background-color": "Фонов цвят", + "background-color-help": "Цвят, който да се използва като фон за началния екран, когато уеб сайтът е инсталиран като приложение", + "undo-timeout": "Време за отмяна", + "undo-timeout-help": "Някои действия, като например преместването на теми, могат да бъдат отменени от модератора в рамките на определено време. Задайте 0, за да забраните изцяло отменянето.", + "topic-tools": "Инструменти за темите" +} diff --git a/public/language/bg/admin/settings/group.json b/public/language/bg/admin/settings/group.json new file mode 100644 index 0000000000..15a08b2c80 --- /dev/null +++ b/public/language/bg/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Общи", + "private-groups": "Частни групи", + "private-groups.help": "Ако е включено, присъединяването към групи ще изисква одобрение от собственик на групата. (По подразбиране: включено)", + "private-groups.warning": "Внимание! Ако това е изключено и имате частни групи, те автоматично ще станат публични.", + "allow-multiple-badges": "Позволяване на множество значки", + "allow-multiple-badges-help": "Това може да се използва, за да позволи на потребителите да избират множество значки за групите. Изисква поддържа на теми.", + "max-name-length": "Минимална дължина на името на група", + "max-title-length": "Максимална дължина на заглавието на група", + "cover-image": "Изображение на корицата за групата", + "default-cover": "Стандартни изображения на корицата", + "default-cover-help": "Добавете стандартни изображения на корицата (разделени със запетаи) за групите, които нямат качено такова." +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/guest.json b/public/language/bg/admin/settings/guest.json new file mode 100644 index 0000000000..d87b6ec60d --- /dev/null +++ b/public/language/bg/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Настройки", + "handles.enabled": "Позволяване на имената за гостите", + "handles.enabled-help": "Тази възможност предоставя ново поле, което позволява на гостите да си изберат име, което да се използва за всяка публикация, която правят. Ако е изключено, всички те просто ще имат името „Гост“.", + "topic-views.enabled": "Гостите да допринасят за броя на преглеждания на темите", + "reply-notifications.enabled": "Гостите да могат да предизвикват изпращането на известия за отговорите си" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/homepage.json b/public/language/bg/admin/settings/homepage.json new file mode 100644 index 0000000000..f0b6df5266 --- /dev/null +++ b/public/language/bg/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Начална страница", + "description": "Изберете коя страница да бъде показана, когато потребителите отидат на главния адрес на форума.", + "home-page-route": "Път на началната страница", + "custom-route": "Персонализиран път", + "allow-user-home-pages": "Разрешаване на потребителските начални страници", + "home-page-title": "Заглавие на началната страница (по подразбиране: „Начало“)" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/languages.json b/public/language/bg/admin/settings/languages.json new file mode 100644 index 0000000000..b4dbba6c3e --- /dev/null +++ b/public/language/bg/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Езикови настройки", + "description": "Езикът по подразбиране определя езиковите настройки за всички потребители, които посещават Вашия форум.
Отделните потребители могат да сменят езика си от страницата с настройки на профила си.", + "default-language": "Език по подразбиране", + "auto-detect": "Автоматично разпознаване на езика за гостите" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/navigation.json b/public/language/bg/admin/settings/navigation.json new file mode 100644 index 0000000000..34dc2112d8 --- /dev/null +++ b/public/language/bg/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Иконка:", + "change-icon": "промяна", + "route": "Маршрут:", + "tooltip": "Подсказка:", + "text": "Текст:", + "text-class": "Текстов клас: незадължително", + "class": "Клас: незадължително", + "id": "Идентификатор: незадължително", + + "properties": "Свойства:", + "groups": "Групи:", + "open-new-window": "Отваряне в нов прозорец", + "dropdown": "Падащо меню", + "dropdown-placeholder": "Въведете елементите на падащото меню по-долу. Пример:
<li><a href="https://myforum.com">Връзка 1</a></li>", + + "btn.delete": "Изтриване", + "btn.disable": "Изключване", + "btn.enable": "Включване", + + "available-menu-items": "Налични елементи за менюто", + "custom-route": "Персонализиран маршрут", + "core": "ядро", + "plugin": "добавка" +} diff --git a/public/language/bg/admin/settings/notifications.json b/public/language/bg/admin/settings/notifications.json new file mode 100644 index 0000000000..c3831f2bc2 --- /dev/null +++ b/public/language/bg/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Известия", + "welcome-notification": "Приветствено известие", + "welcome-notification-link": "Връзка за приветственото известие", + "welcome-notification-uid": "Потр. ид. за приветственото известие", + "post-queue-notification-uid": "Потр. ид. за опашката с публикации" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/pagination.json b/public/language/bg/admin/settings/pagination.json new file mode 100644 index 0000000000..c3ba5bf6ee --- /dev/null +++ b/public/language/bg/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Настройки за страницирането", + "enable": "Разделяне на темите и публикациите на страници, вместо да се превърта безкрайно.", + "posts": "Странициране в публикациите", + "topics": "Странициране в темите", + "posts-per-page": "Публикации на страница", + "max-posts-per-page": "Максимален брой публикации на страница", + "categories": "Странициране на категориите", + "topics-per-page": "Теми на страница", + "max-topics-per-page": "Максимален брой теми на страница", + "categories-per-page": "Брой категории на страница" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/post.json b/public/language/bg/admin/settings/post.json new file mode 100644 index 0000000000..d474d24ac8 --- /dev/null +++ b/public/language/bg/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Подредба на публикациите", + "sorting.post-default": "Подредба по подразбиране на публикациите", + "sorting.oldest-to-newest": "Първо най-старите", + "sorting.newest-to-oldest": "Първо най-новите", + "sorting.most-votes": "Първо тези с най-много гласове", + "sorting.most-posts": "Първо тези с най-много публикации", + "sorting.topic-default": "Подредба по подразбиране на темите", + "length": "Дължина на публикациите", + "post-queue": "Опашка за публикации", + "restrictions": "Ограничения за публикуването", + "restrictions-new": "Ограничения за новите потребители", + "restrictions.post-queue": "Включване на опашката за публикации", + "restrictions.post-queue-rep-threshold": "Нужна репутация за пропускане на опашката за публикации", + "restrictions.groups-exempt-from-post-queue": "Избиране на групи, които да пропускат опашката за публикации", + "restrictions-new.post-queue": "Включване на ограниченията за новите потребители", + "restrictions.post-queue-help": "Ако опашката за публикации е включена, публикациите на новите потребители ще бъдат добавяни в опашка за одобрение", + "restrictions-new.post-queue-help": "Ако ограниченията за новите потребители са включени, това ще зададе някои ограничения за публикациите създадени от новите потребители", + "restrictions.seconds-between": "Брой секунди между публикациите", + "restrictions.seconds-between-new": "Брой секунди между публикациите за нови потребители", + "restrictions.rep-threshold": "Необходима репутация за премахване на това ограничение", + "restrictions.seconds-before-new": "Брой секунди преди новите потребители да могат да публикуват за пръв път", + "restrictions.seconds-edit-after": "Брой секунди, през които публикациите могат да бъдат редактирани. (0 = изключено)", + "restrictions.seconds-delete-after": "Брой секунди, през които публикациите могат да бъдат изтрити. (0 = изключено)", + "restrictions.replies-no-delete": "Брой отговори, след които потребителите вече не могат да изтриват собствените си теми. (0 = изключено)", + "restrictions.min-title-length": "Минимална дължина на заглавието", + "restrictions.max-title-length": "Максимална дължина на заглавието", + "restrictions.min-post-length": "Минимална дължина на публикацията", + "restrictions.max-post-length": "Максимална дължина на публикацията", + "restrictions.days-until-stale": "Брой дни, след които темата се смята за стара", + "restrictions.stale-help": "Ако дадена тема е определена като „стара“, то потребителите, които се опитат да пишат в нея, ще получат предупредително съобщение.", + "timestamp": "Време", + "timestamp.cut-off": "Използване на дата след (в брой дни)", + "timestamp.cut-off-help": "Датите и времената ще бъдат показвани относително (напр. „преди 3 часа“ или „преди 5 дни“), и преведени на множество\n\\t\\t\\t\\t\\tезици. След определено време, този текст ще започне да показва самите дата и час, според езика на потребителя\n\\t\\t\\t\\t\\t(напр. „5 ноември 2016 15:30“).
(По подразбиране: 30, тоест един месец). Ако зададете 0, винаги ще се изписват дати, а ако оставите полето празно, времето ще бъде винаги относително.", + "timestamp.necro-threshold": "Мъртва граница (в дни)", + "timestamp.necro-threshold-help": "Между публикациите ще бъде показано съобщение, ако времето между тях е по-дълго от мъртвата граница. (По подразбиране: 7, или една седмица). Задайте 0 за изключване.", + "timestamp.topic-views-interval": "Интервал за увеличаване на броя на преглеждания на темите (в минути)", + "timestamp.topic-views-interval-help": "Броят на преглеждания на темите ще се увеличава веднъж на всеки Х минути, според тази настройка.", + "teaser": "Представителна публикация", + "teaser.last-post": "Последната – Показване на последната публикация, или първоначалната такава, ако няма отговори.", + "teaser.last-reply": "Последната – Показване на последния отговор, или „Няма отговори“, ако все още няма такива.", + "teaser.first": "Първата", + "showPostPreviewsOnHover": "Показване на кратък преглед на публикациите при посочване с мишката", + "unread": "Настройки за непрочетените", + "unread.cutoff": "Възраст на публикациите, след която те не се показват в непрочетените (в брой дни)", + "unread.min-track-last": "Минимален брой публикации в темата, след което да започва следене на последно прочетената", + "recent": "Настройки за скорошните", + "recent.max-topics": "Максимален брой теми в скорошните", + "recent.categoryFilter.disable": "Изключване на филтрирането на темите в пренебрегваните категории на страницата /recent", + "signature": "Настройки за подписите", + "signature.disable": "Забраняване на подписите", + "signature.no-links": "Забраняване на поставянето на връзки в подписите", + "signature.no-images": "Забраняване на поставянето на изображения в подписите", + "signature.hide-duplicates": "Скриване на дублираните подписи в темите", + "signature.max-length": "Максимална дължина на подписите", + "composer": "Настройки за съставянето", + "composer-help": "Следващите настройки определят функционалностите и/или вида на елемента за съставяне на\n\\t\\t\\t\\tпубликация, който се използва от потребителите, когато те създават нови теми или отговорят в съществуващи.", + "composer.show-help": "Показване на раздела „Помощ“", + "composer.enable-plugin-help": "Позволяване на добавките да добавят съдържание в раздела за помощ", + "composer.custom-help": "Персонализиран текст за помощ", + "backlinks": "Обратни връзки", + "backlinks.enabled": "Включване на обратните връзки в темите", + "backlinks.help": "Ако в публикацията има препратка към друга тема, там ще бъде поставена връзка към публикацията, с конкретното време.", + "ip-tracking": "Записване на IP адреса", + "ip-tracking.each-post": "Записване на IP адреса за всяка публикация", + "enable-post-history": "Включване на историята на публикациите" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/reputation.json b/public/language/bg/admin/settings/reputation.json new file mode 100644 index 0000000000..9601b9d0f4 --- /dev/null +++ b/public/language/bg/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Настройки за репутацията", + "disable": "Изключване на системата за репутация", + "disable-down-voting": "Забрана на отрицателното гласуване", + "votes-are-public": "Всички гласувания са публични", + "thresholds": "Ограничения на дейността", + "min-rep-upvote": "Минимална репутация, необходима за положително гласуване за публикации", + "upvotes-per-day": "Положителни гласувания за ден (задайте 0 за неограничен брой)", + "upvotes-per-user-per-day": "Положителни гласувания за потребител за ден (задайте 0 за неограничен брой)", + "min-rep-downvote": "Минимална репутация, необходима за отрицателно гласуване за публикации", + "downvotes-per-day": "Отрицателни гласувания за ден (задайте 0 за неограничен брой)", + "downvotes-per-user-per-day": "Отрицателни гласувания за потребител за ден (задайте 0 за неограничен брой)", + "min-rep-chat": "Минимална репутация, необходима за изпращане на съобщения в разговори", + "min-rep-flag": "Минимална репутация, необходима за докладване на публикации", + "min-rep-website": "Минимална репутация, необходима за добавяне на полето „Уебсайт“ към профила на потребителя", + "min-rep-aboutme": "Минимална репутация, необходима за добавяне на полето „За мен“ към профила на потребителя", + "min-rep-signature": "Минимална репутация, необходима за добавяне на полето „Подпис“ към профила на потребителя", + "min-rep-profile-picture": "Минимална репутация, необходима за добавяне на профилна снимка към профила на потребителя", + "min-rep-cover-picture": "Минимална репутация, необходима за добавяне на снимка на корицата към профила на потребителя", + + "flags": "Настройки за докладите", + "flags.limit-per-target": "Максимален брой докладвания на едно и също нещо", + "flags.limit-per-target-placeholder": "По подразбиране: 0", + "flags.limit-per-target-help": "Когато публикация или потребител бъде докладван няколко пъти, това се добавя към един общ доклад. Задайте на тази настройка стойност по-голяма от нула, за да ограничите броя на докладванията, които могат да бъдат натрупани към една публикация или потребител.", + "flags.auto-flag-on-downvote-threshold": "Брой отрицателни гласове, при които публикациите да бъдат докладвани автоматично (0 = изключено, по подразбиране: 0)", + "flags.auto-resolve-on-ban": "Автоматично премахване на всички доклади за потребител, когато той бъде блокиран", + "flags.action-on-resolve": "Когато докладване бъде разрешено, да се направи следното", + "flags.action-on-reject": "Когато докладване бъде отхвърлено, да се направи следното", + "flags.action.nothing": "Да не се прави нищо", + "flags.action.rescind": "Да се отмени известието, изпратено до модераторите/администраторите" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/social.json b/public/language/bg/admin/settings/social.json new file mode 100644 index 0000000000..e090d929dc --- /dev/null +++ b/public/language/bg/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Споделяне на публикации", + "info-plugins-additional": "Добавките могат да добавят допълнителни мрежи за споделяне на публикации.", + "save-success": "Мрежите за споделяне на публикации са запазени успешно!" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/sockets.json b/public/language/bg/admin/settings/sockets.json new file mode 100644 index 0000000000..1250c2e9fc --- /dev/null +++ b/public/language/bg/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Настройки за повторно свързване", + "max-attempts": "Максимален брой опити за повторно свързване", + "default-placeholder": "По подразбиране: %1", + "delay": "Забавяне при повторно свързване" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/sounds.json b/public/language/bg/admin/settings/sounds.json new file mode 100644 index 0000000000..563c11e917 --- /dev/null +++ b/public/language/bg/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Известия", + "chat-messages": "Съобщения в разговори", + "play-sound": "Пускане", + "incoming-message": "Входящо съобщение", + "outgoing-message": "Изходящо съобщение", + "upload-new-sound": "Качване на нов звук", + "saved": "Настройките са запазени" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/tags.json b/public/language/bg/admin/settings/tags.json new file mode 100644 index 0000000000..bcbbea49e8 --- /dev/null +++ b/public/language/bg/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Настройки за етикетите", + "link-to-manage": "Управление на етикетите", + "system-tags": "Системни етикети", + "system-tags-help": "Само потребителите с по-високи правомощия ще могат да използват тези етикети.", + "min-per-topic": "Минимален брой етикети за тема", + "max-per-topic": "Максимален брой етикети за тема", + "min-length": "Минимална дължина на етикетите", + "max-length": "Максимална дължина на етикетите", + "related-topics": "Свързани теми", + "max-related-topics": "Максимален брой свързани теми, които да бъдат показвани (ако това се поддържа от темата)" +} \ No newline at end of file diff --git a/public/language/bg/admin/settings/uploads.json b/public/language/bg/admin/settings/uploads.json new file mode 100644 index 0000000000..9b97baef8b --- /dev/null +++ b/public/language/bg/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Публикации", + "orphans": "Неизползвани файлове", + "private": "Качените файлове да бъдат частни", + "strip-exif-data": "Премахване на данните EXIF", + "preserve-orphaned-uploads": "Запазване на качените файлове на диска дори след изтриването на публикацията", + "orphanExpiryDays": "Брой дни за съхранение на неизползваните файлове", + "orphanExpiryDays-help": "След толкова на брой дни неизползваните качени файлове ще бъдат изтривани.
Задайте 0 или оставете празно, за да изключите тази функционалност.", + "private-extensions": "Файлови разширения, които да бъдат частни", + "private-uploads-extensions-help": "Въведете списък от файлови разширения, разделени със запетаи, които искате да бъдат частни (например pdf,xls,doc). Ако оставите това поле празно, всички файлове ще бъдат частни.", + "resize-image-width-threshold": "Преоразмеряване на изображенията, ако са по-широки от определената ширина", + "resize-image-width-threshold-help": "(в пиксели; по подразбиране: 1520 пиксела. 0 = изключено)", + "resize-image-width": "Намаляване на размера на изображенията до определена ширина", + "resize-image-width-help": "(в пиксели; по подразбиране: 760 пиксела. 0 = изключено)", + "resize-image-quality": "Качество при преоразмеряване на изображенията", + "resize-image-quality-help": "Използване на по-ниско качество за намаляване на размера на файловете за преоразмерените изображения.", + "max-file-size": "Максимален размер на файловете (в КиБ)", + "max-file-size-help": "(в кибибайтове; по подразбиране: 2048 КиБ)", + "reject-image-width": "Максимална ширина на изображенията (в пиксели)", + "reject-image-width-help": "Изображенията, чиято ширина е по-голяма от тази стойност, ще бъдат отхвърляни.", + "reject-image-height": "Максимална височина на изображенията (в пиксели)", + "reject-image-height-help": "Изображенията, чиято височина е по-голяма от тази стойност, ще бъдат отхвърляни.", + "allow-topic-thumbnails": "Позволяване на потребителите да качват миниатюрни изображения за темите", + "topic-thumb-size": "Размер на миниатюрите за темите", + "allowed-file-extensions": "Разрешени файлови разширения", + "allowed-file-extensions-help": "Въведете файловите разширения, разделени със запетаи (пример: pdf,xls,doc). Ако списъкът е празен, всички файлови разширения ще бъдат разрешени.", + "upload-limit-threshold": "Ограничаване на качванията на потребителите до:", + "upload-limit-threshold-per-minute": "За %1 минута", + "upload-limit-threshold-per-minutes": "За %1 минути", + "profile-avatars": "Профилни изображения", + "allow-profile-image-uploads": "Позволяване на потребителите да качват профилни изображения", + "convert-profile-image-png": "Превръщане на качените профилни изображения във формата „PNG“", + "default-avatar": "Персонализирано изображение по подразбиране", + "upload": "Качване", + "profile-image-dimension": "Размер на профилното изображение", + "profile-image-dimension-help": "(в пиксели; по подразбиране: 128 пиксела)", + "max-profile-image-size": "Максимален файлов размер на профилното изображение", + "max-profile-image-size-help": "(в кибибайтове; по подразбиране: 256 КиБ)", + "max-cover-image-size": "Максимален файлов размер на изображението на корицата", + "max-cover-image-size-help": "(в кибибайтове; по подразбиране: 2048 КиБ)", + "keep-all-user-images": "Старите версии на профилните изображения и тези на корицата да се пазят на сървъра", + "profile-covers": "Корици на профила", + "default-covers": "Стандартни изображения за корицата", + "default-covers-help": "Добавете стандартни изображения на корицата (разделени със запетаи) за акаунтите, които нямат качено такова." +} diff --git a/public/language/bg/admin/settings/user.json b/public/language/bg/admin/settings/user.json new file mode 100644 index 0000000000..618e3c7361 --- /dev/null +++ b/public/language/bg/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Удостоверяване", + "email-confirm-interval": "Потребителят не може да изпраща повторно е-писмото за потвърждение, преди да са минали", + "email-confirm-interval2": "минути са изминали", + "allow-login-with": "Позволяване на вписването чрез", + "allow-login-with.username-email": "Потребителско име или е-поща", + "allow-login-with.username": "Само потребителско име", + "account-settings": "Настройки на акаунта", + "gdpr_enabled": "Включване на искането за съгласие с ОРЗД", + "gdpr_enabled_help": "Ако това е включено, всички новорегистрирани потребители ще бъдат задължени изрично да дадат съгласието си за събирането на данни и статистики за потреблението според Общия регламент относно защитата на данните (ОРЗД). Забележка: Включването на ОРЗД не задължава съществуващите потребители да дадат съгласието си. Ако искате това, ще трябва да инсталирате добавката за ОРЗД (GDPR).", + "disable-username-changes": "Забраняване на промяната на потребителското име", + "disable-email-changes": "Забраняване на промяната на е-пощата", + "disable-password-changes": "Забраняване на промяната на паролата", + "allow-account-deletion": "Позволяване на изтриването на профила", + "hide-fullname": "Скриване на пълното име от потребителите", + "hide-email": "Скриване на е-пощата от потребителите", + "show-fullname-as-displayname": "Показване на цялото име на потребителя, ако е налично", + "themes": "Теми", + "disable-user-skins": "Потребителите да не могат да избират собствен облик", + "account-protection": "Защита на акаунта", + "admin-relogin-duration": "Повторно вписване на администратора (в минути)", + "admin-relogin-duration-help": "След определено време достъпът до административния раздел ще изисква повторно вписване. Задайте 0, за да изключите това.", + "login-attempts": "Брой опити за вписване на час", + "login-attempts-help": "Ако опитите за вписване на потребител минат тази граница, акаунтът ще бъде заключен за определено време.", + "lockout-duration": "Продължителност на заключването на акаунта (в минути)", + "login-days": "Брой дни за помнене на сесията за вписване на потребителя", + "password-expiry-days": "Изискване на промяна на паролата през определен период от дни", + "session-time": "Продължителност на сесията", + "session-time-days": "Дни", + "session-time-seconds": "Секунди", + "session-time-help": "Тези стойности се използват за определяне на дължината на периода, през който потребителите ще останат вписани в системата, ако поставят отметка в полето „Запомнете ме“ при вписването. Имайте предвид, че ще се използва само една от тези стойности. Ако няма стойност за секунди, ще се използва стойността за дни. Ако няма и стойност за дни, то ще се използва стандартната стойност от 14 дни.", + "online-cutoff": "Брой минути, след които потребителят ще бъде смятан за неактивен", + "online-cutoff-help": "Ако потребителят не извършва никакви действия през този период, ще бъде смятан за неактивен и няма да получава известия в реално време.", + "registration": "Регистриране на потребителите", + "registration-type": "Вид регистриране", + "registration-approval-type": "Вид одобрение на регистрацията", + "registration-type.normal": "Обикновено", + "registration-type.admin-approval": "Одобрена от администратор", + "registration-type.admin-approval-ip": "Одобрена от администратор по IP адрес", + "registration-type.invite-only": "Само с покана", + "registration-type.admin-invite-only": "Само с покана от администратор", + "registration-type.disabled": "Без регистриране", + "registration-type.help": "Обикновена — Потребителите могат да се регистрират от страницата /register.
\nСамо с покана — Потребителите могат да поканят други от страницата с потребителите.
\nСамо с покана от администратор — Само администратори могат да канят други от страницата с потребителите и от страниците за управление на потребителите.
\nБез регистриране — Потребителите не се регистрират.
", + "registration-approval-type.help": "Обикновена — Потребителите се регистрират на момента.
\nОдобрена от администратор — Потребителските регистрации се поставят в опашка за одобрение, която администраторите преглеждат.
\nОдобрена от администратор по IP адрес — Новите потребители се регистрират по обикновения начин, а онези, от чиито IP адрес вече са се регистрирали други акаунти, се нуждаят от одобрението на администратор.
", + "registration-queue-auto-approve-time": "Време за автоматично одобрение", + "registration-queue-auto-approve-time-help": "Брой часове преди потребител да бъде одобрен автоматично. 0 = изключено.", + "registration-queue-show-average-time": "Средното време за одобрение на нов потребител да се показва на потребителите", + "registration.max-invites": "Максимален брой покани на потребител", + "max-invites": "Максимален брой покани на потребител", + "max-invites-help": "0 = няма ограничение. Администраторите могат да разпращат неограничен брой покани.
Тази стойност се използва, само ако е избран режимът „Само с покана“.", + "invite-expiration": "Давност на поканите", + "invite-expiration-help": "Брой дни, след които поканите вече не важат.", + "min-username-length": "Минимална дължина на потребителското име", + "max-username-length": "Максимална дължина на потребителското име", + "min-password-length": "Минимална дължина на паролата", + "min-password-strength": "Минимална сложност на паролата", + "max-about-me-length": "Максимална дължина на информацията на потребителите за себе си", + "terms-of-use": "Условия за ползване на форума (Оставете празно и няма да има такива)", + "user-search": "Търсене на потребители", + "user-search-results-per-page": "Брой резултати, които да бъдат показвани", + "default-user-settings": "Настройки по подразбиране на потребителите", + "show-email": "Показване на е-пощата", + "show-fullname": "Показване на пълното име", + "restrict-chat": "Разрешаване на съобщенията само от потребители, които следвам", + "outgoing-new-tab": "Отваряне на външните връзки в нов подпрозорец", + "topic-search": "Включване на търсенето в темите", + "update-url-with-post-index": "Обновяване на адресната лента с номера на публикацията по време на разглеждане на темите", + "digest-freq": "Абониране за резюмета", + "digest-freq.off": "Изключено", + "digest-freq.daily": "Ежедневно", + "digest-freq.weekly": "Ежеседмично", + "digest-freq.biweekly": "На всеки две седмици", + "digest-freq.monthly": "Ежемесечно", + "email-chat-notifs": "Изпращане на е-писмо, ако получа ново съобщение в разговор, а не съм на линия", + "email-post-notif": "Изпращане на е-писмо, когато се появи отговор в темите, за които съм абониран(а).", + "follow-created-topics": "Следване на темите, които създавате", + "follow-replied-topics": "Следване на темите, на които отговаряте", + "default-notification-settings": "Настройки по подразбиране за известията", + "categoryWatchState": "Състояние по подразбиране за следенето на категории", + "categoryWatchState.watching": "Да се следят", + "categoryWatchState.notwatching": "Да не се следят", + "categoryWatchState.ignoring": "Да се пренебрегват" +} diff --git a/public/language/bg/admin/settings/web-crawler.json b/public/language/bg/admin/settings/web-crawler.json new file mode 100644 index 0000000000..88ca3da0f9 --- /dev/null +++ b/public/language/bg/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Настройки за възможността за обхождане", + "robots-txt": "Персонализиран файл „Robots.txt“ Оставете празно, за да се използва този по подразбиране", + "sitemap-feed-settings": "Настройки на картата на уеб сайта и емисиите", + "disable-rss-feeds": "Изключване на емисиите чрез RSS", + "disable-sitemap-xml": "Изключване на картата на уеб сайта („Sitemap.xml“)", + "sitemap-topics": "Брой теми за показване в картата на уеб сайта", + "clear-sitemap-cache": "Изчистване на кеша на картата на уеб сайта", + "view-sitemap": "Преглед на картата на уеб сайта" +} \ No newline at end of file diff --git a/public/language/bg/category.json b/public/language/bg/category.json new file mode 100644 index 0000000000..e20b4094d5 --- /dev/null +++ b/public/language/bg/category.json @@ -0,0 +1,23 @@ +{ + "category": "Категория", + "subcategories": "Подкатегории", + "new_topic_button": "Нова тема", + "guest-login-post": "Впишете се, за да можете да публикувате", + "no_topics": "Все още няма теми в тази категория.
Защо не създадете някоя?", + "browsing": "разглежда", + "no_replies": "Няма отговори", + "no_new_posts": "Няма нови публикации.", + "watch": "Следене", + "ignore": "Пренебрегване", + "watching": "Следите", + "not-watching": "Не следите", + "ignoring": "Пренебрегвате", + "watching.description": "Темите да се показват в непрочетените и скорошните", + "not-watching.description": "Темите да не се показват в непрочетените, а само в скорошните", + "ignoring.description": "Темите да не се показват нито в непрочетените, нито в скорошните", + "watching.message": "Вече следите новите неща в категорията и подкатегориите ѝ", + "notwatching.message": "Вече не следите новите неща в категорията и подкатегориите ѝ", + "ignoring.message": "Вече пренебрегвате новите неща в тази категория и всички нейни подкатегории", + "watched-categories": "Следени категории", + "x-more-categories": "Още %1 категории" +} \ No newline at end of file diff --git a/public/language/bg/email.json b/public/language/bg/email.json new file mode 100644 index 0000000000..b7d616f427 --- /dev/null +++ b/public/language/bg/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Пробно е-писмо", + "password-reset-requested": "Изпратена е заявка за подновяване на паролата!", + "welcome-to": "Добре дошли в %1", + "invite": "Покана от %1", + "greeting_no_name": "Здравейте", + "greeting_with_name": "Здравейте, %1", + "email.verify-your-email.subject": "Моля, потвърдете е-пощата си", + "email.verify.text1": "Вие поискахте да променим или потвърдим адреса на е-пощата Ви", + "email.verify.text2": "Поради причини, свързани със сигурността, можем да променим или потвърдим адреса на е-поща, само когато притежанието ѝ вече е било установено чрез е-писмо. Ако не сте поискали това, няма нужда да правите нищо.", + "email.verify.text3": "След като потвърдите адреса на тази е-поща, ще променим текущия Ви адрес с този (%1).", + "welcome.text1": "Благодарим Ви, че се регистрирахте в %1", + "welcome.text2": "За да активирате напълно акаунта си, трябва да потвърдите е-пощата, с която сте се регистрирали.", + "welcome.text3": "Вашата заявка за регистрация беше приета от администратор. Вече можете да се впишете със своето потребителско име и парола.", + "welcome.cta": "Натиснете тук, за да потвърдите своята е-поща.", + "invitation.text1": "%1 Ви покани да се присъедините към %2", + "invitation.text2": "Поканата Ви ще изтече след %1 дни.", + "invitation.cta": "Натиснете тук, за да си създадете акаунт.", + "reset.text1": "Получихме заявка за подновяване на Вашата парола, най-вероятно защото сте я забравили. Ако това не е така, моля не обръщайте внимание на това е-писмо.", + "reset.text2": "За да продължите с процедурата по подновяване на паролата, моля последвайте следната връзка:", + "reset.cta": "Натиснете тук, за да нулирате паролата си", + "reset.notify.subject": "Паролата беше променена успешно", + "reset.notify.text1": "Известяваме Ви, че на %1, Вашата парола беше променена успешно.", + "reset.notify.text2": "Ако не сте поискали това, моля, свържете се незабавно с администратор.", + "digest.latest_topics": "Последни теми от %1", + "digest.top-topics": "Най-интересните теми от %1", + "digest.popular-topics": "Популярни теми от %1", + "digest.cta": "Натиснете тук, за да посетите %1", + "digest.unsub.info": "Това резюме беше изпратено до Вас поради настройките Ви за абонаментите.", + "digest.day": "ден", + "digest.week": "месец", + "digest.month": "година", + "digest.subject": "Резюме за %1", + "digest.title.day": "Дневното Ви резюме", + "digest.title.week": "Седмичното Ви резюме", + "digest.title.month": "Месечното Ви резюме", + "notif.chat.subject": "Получено е ново съобщение от %1", + "notif.chat.cta": "Натиснете тук, за да продължите дискусията", + "notif.chat.unsub.info": "Това известие за разговор беше изпратено до Вас поради настройките Ви за абонаментите.", + "notif.post.unsub.info": "Това известие за публикация беше изпратено до Вас поради настройките Ви за абонаментите.", + "notif.post.unsub.one-click": "Или можете да се отпишете от подобни бъдещи съобщения, като натиснете", + "notif.cta": "Към форума", + "notif.cta-new-reply": "Преглед на публикацията", + "notif.cta-new-chat": "Преглед на разговора", + "notif.test.short": "Изпробване на известията", + "notif.test.long": "Това е пробно е-писмо за проверка на работата на известията.", + "test.text1": "Това е пробно е-писмо, за да потвърдим, че изпращачът на е-поща е правилно настроен за Вашия NodeBB.", + "unsub.cta": "Натиснете тук, за да промените тези настройки", + "unsubscribe": "отписване", + "unsub.success": "Повече няма да получавате е-писма от пощенския списък на %1", + "unsub.failure.title": "Отписването не може да се извърши", + "unsub.failure.message": "За съжаление не успяхме да Ви отпишем от пощенския списък, поради проблем с връзката. Можете, обаче, да промените предпочитанията си за е-писмата в потребителските си настройки.

(грешка: %1)", + "banned.subject": "Вие бяхте блокиран(а) от %1", + "banned.text1": "Потребителят %1 беше блокиран от %2.", + "banned.text2": "Това блокиране ще е в сила до %1.", + "banned.text3": "Това е причината, поради която бяхте блокиран(а):", + "closing": "Благодарим Ви!" +} \ No newline at end of file diff --git a/public/language/bg/error.json b/public/language/bg/error.json new file mode 100644 index 0000000000..ad01a8f2d4 --- /dev/null +++ b/public/language/bg/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Грешни данни", + "invalid-json": "Неправилен JSON", + "wrong-parameter-type": "За свойството `%1` се очакваше стойност от тип %3, но вместо това беше получено %2", + "required-parameters-missing": "Липсват задължителни параметри от това извикване към ППИ: %1", + "not-logged-in": "Изглежда не сте се вписали в системата.", + "account-locked": "Вашият акаунт беше заключен временно", + "search-requires-login": "Търсенето изисква регистриран акаунт! Моля, впишете се или се регистрирайте!", + "goback": "Натиснете „Назад“, за да се върнете на предишната страница", + "invalid-cid": "Грешен идентификатор на категория", + "invalid-tid": "Грешен идентификатор на тема", + "invalid-pid": "Грешен идентификатор на публикация", + "invalid-uid": "Грешен идентификатор на потребител", + "invalid-mid": "Грешен идентификатор на съобщение в разговор", + "invalid-date": "Трябва да бъде посочена правилна дата", + "invalid-username": "Грешно потребителско име", + "invalid-email": "Грешна е-поща", + "invalid-fullname": "Грешно пълно име", + "invalid-location": "Грешно местоположение", + "invalid-birthday": "Грешна рождена дата", + "invalid-title": "Грешно заглавие", + "invalid-user-data": "Грешни потребителски данни", + "invalid-password": "Грешна парола", + "invalid-login-credentials": "Неправилни данни за удостоверяване", + "invalid-username-or-password": "Моля, въведете потребителско име и парола", + "invalid-search-term": "Грешен текст за търсене", + "invalid-url": "Грешен адрес", + "invalid-event": "Грешно събитие: %1", + "local-login-disabled": "Системата за местно вписване е изключена за непривилегированите акаунти.", + "csrf-invalid": "Не успяхме да Ви впишем, най-вероятно защото сесията Ви е изтекла. Моля, опитайте отново", + "invalid-path": "Грешен път", + "folder-exists": "Вече има папка с това име", + "invalid-pagination-value": "Грешен номер на странициране, трябва да бъде между %1 и %2", + "username-taken": "Потребителското име е заето", + "email-taken": "Е-пощата е заета", + "email-nochange": "Въведената е-поща е същата като съществуващата.", + "email-invited": "На тази е-поща вече е била изпратена покана", + "email-not-confirmed": "Публикуването в някои категории и теми ще бъде възможно едва след като е-пощата Ви бъде потвърдена. Щръкнете тук, за да Ви изпратим е-писмо за потвърждение.", + "email-not-confirmed-chat": "Няма да можете да пишете в разговори, докато е-пощата Ви не бъде потвърдена. Моля, натиснете тук, за да потвърдите е-пощата си.", + "email-not-confirmed-email-sent": "Вашата е-поща все още не е потвърдена. Моля, проверете входящата си кутия за писмото за потвърждение. Възможно е да не можете да публикувате съобщения или да пишете в разговори, докато е-пощата Ви не бъде потвърдена.", + "no-email-to-confirm": "Нямате зададена е-поща. Тя е необходима за възстановяването на акаунта в случай на проблем, а може и да се изисква, за да пишете в някои категории. Натиснете тук, за да въведете е-поща.", + "user-doesnt-have-email": "Потребителят „%1“ няма зададена е-поща.", + "email-confirm-failed": "Не успяхме да потвърдим е-пощата Ви. Моля, опитайте отново по-късно.", + "confirm-email-already-sent": "Е-писмото за потвърждение вече е изпратено. Моля, почакайте още %1 минута/и, преди да изпратите ново.", + "sendmail-not-found": "Изпълнимият файл на „sendmail“ не може да бъде намерен. Моля, уверете се, че е инсталиран и изпълним за потребителя, чрез който е пуснат NodeBB.", + "digest-not-enabled": "Този потребител няма включени резюмета, или системната настройка по подразбиране е да не се изпращат резюмета", + "username-too-short": "Потребителското име е твърде кратко", + "username-too-long": "Потребителското име е твърде дълго", + "password-too-long": "Паролата е твърде дълга", + "reset-rate-limited": "Твърде много подновявания на паролата (има ограничение на честотата)", + "reset-same-password": "Моля, използвайте парола, която е различна от текущата", + "user-banned": "Потребителят е блокиран", + "user-banned-reason": "За съжаление, този акаунт е блокиран (Причина: %1)", + "user-banned-reason-until": "За съжаление, този акаунт е блокиран до %1 (Причина: %2)", + "user-too-new": "Съжаляваме, но трябва да изчакате поне %1 секунда/и, преди да направите първата си публикация", + "blacklisted-ip": "Съжаляваме, но Вашият IP адрес е забранен за ползване в тази общност. Ако смятате, че това е грешка, моля, свържете се с администратор.", + "ban-expiry-missing": "Моля, задайте крайна дата за това блокиране", + "no-category": "Категорията не съществува", + "no-topic": "Темата не съществува", + "no-post": "Публикацията не съществува", + "no-group": "Групата не съществува", + "no-user": "Потребителят не съществува", + "no-teaser": "Резюмето не съществува", + "no-flag": "Докладът не съществува", + "no-privileges": "Нямате достатъчно права за това действие.", + "category-disabled": "Категорията е изключена", + "topic-locked": "Темата е заключена", + "post-edit-duration-expired": "Можете да редактирате публикациите си до %1 секунда/и, след като ги пуснете", + "post-edit-duration-expired-minutes": "Можете да редактирате публикациите си до %1 минута/и, след като ги пуснете", + "post-edit-duration-expired-minutes-seconds": "Можете да редактирате публикациите си до %1 минута/и и %2 секунда/и, след като ги пуснете", + "post-edit-duration-expired-hours": "Можете да редактирате публикациите си до %1 час(а), след като ги пуснете", + "post-edit-duration-expired-hours-minutes": "Можете да редактирате публикациите си до %1 час(а) и %2 минута/и, след като ги пуснете", + "post-edit-duration-expired-days": "Можете да редактирате публикациите си до %1 ден(а), след като ги пуснете", + "post-edit-duration-expired-days-hours": "Можете да редактирате публикациите си до %1 ден(а) и %2 час(а), след като ги пуснете", + "post-delete-duration-expired": "Можете да изтривате публикациите си до %1 секунда/и след пускането им", + "post-delete-duration-expired-minutes": "Можете да изтривате публикациите си до %1 минута/и след пускането им", + "post-delete-duration-expired-minutes-seconds": "Можете да изтривате публикациите си до %1 минута/и и %2 секунда/и след пускането им", + "post-delete-duration-expired-hours": "Можете да изтривате публикациите си до %1 час(а) след пускането им", + "post-delete-duration-expired-hours-minutes": "Можете да изтривате публикациите си до %1 час(а) и %2 минута/и след пускането им", + "post-delete-duration-expired-days": "Можете да изтривате публикациите си до %1 ден(а) след пускането им", + "post-delete-duration-expired-days-hours": "Можете да изтривате публикациите си до %1 ден(а) и %2 час(а) след пускането им", + "cant-delete-topic-has-reply": "Не можете да изтриете темата си, след като в нея вече има един отговор", + "cant-delete-topic-has-replies": "Не можете да изтриете темата си, след като в нея вече има %1 отговора", + "content-too-short": "Моля, въведете по-дълъг текст на публикацията. Публикациите трябва да съдържат поне %1 символ(а).", + "content-too-long": "Моля, въведете по-кратък текст на публикацията. Публикациите трябва да съдържат не повече от %1 символ(а).", + "title-too-short": "Моля, въведете по-дълго заглавие. Заглавията трябва да съдържат поне %1 символ(а).", + "title-too-long": "Моля, въведете по-кратко заглавие. Заглавията трябва да съдържат не повече от %1 символ(а).", + "category-not-selected": "Не е избрана категория.", + "too-many-posts": "Можете да публикувате веднъж на %1 секунда/и – моля, изчакайте малко, преди да опитате да публикувате отново", + "too-many-posts-newbie": "Като нов потребител, Вие можете да публикувате веднъж на %1 секунда/и, докато не натрупате %2 репутация – моля, изчакайте малко, преди да опитате да публикувате отново", + "already-posting": "You are already posting", + "tag-too-short": "Моля, въведете по-дълъг етикет. Етикетите трябва да съдържат поне %1 символ(а)", + "tag-too-long": "Моля, въведете по-кратък етикет. Етикетите трябва да съдържат не повече от %1 символ(а)", + "not-enough-tags": "Недостатъчно етикети. Темите трябва да имат поне %1 етикет(а)", + "too-many-tags": "Твърде много етикети. Темите не могат да имат повече от %1 етикет(а)", + "cant-use-system-tag": "Не можете да използвате този системен етикет.", + "cant-remove-system-tag": "Не можете да премахнете този системен етикет.", + "still-uploading": "Моля, изчакайте качването да приключи.", + "file-too-big": "Максималният разрешен размер на файл е %1 КБ – моля, качете по-малък файл", + "guest-upload-disabled": "Качването не е разрешено за гости", + "cors-error": "Изображението не може да бъде качено поради неправилни настройки на CORS", + "upload-ratelimit-reached": "Качили сте твърде много файлове наведнъж. Моля, опитайте отново по-късно.", + "scheduling-to-past": "Изберете дата в бъдещето.", + "invalid-schedule-date": "Въведете правилна дата и час.", + "cant-pin-scheduled": "Насрочените теми не могат да бъдат закачени или разкачени.", + "cant-merge-scheduled": "Насрочените теми не могат да бъдат сливани.", + "cant-move-posts-to-scheduled": "Публикации не могат да бъдат премествани в насрочена тема.", + "cant-move-from-scheduled-to-existing": "Публикации от насрочена тема не могат да бъдат премествани в съществуваща тема.", + "already-bookmarked": "Вече имате отметка към тази публикация", + "already-unbookmarked": "Вече сте премахнали отметката си от тази публикация", + "cant-ban-other-admins": "Не можете да блокирате другите администратори!", + "cant-mute-other-admins": "Не можете да заглушавате другите администратори!", + "user-muted-for-hours": "Вие бяхте заглушен(а). Ще можете да пускате публикации отново след %1 час(а)", + "user-muted-for-minutes": "Вие бяхте заглушен(а). Ще можете да пускате публикации отново след %1 минута/и", + "cant-make-banned-users-admin": "Не можете да давате администраторски права на блокирани потребители.", + "cant-remove-last-admin": "Вие сте единственият администратор. Добавете друг потребител като администратор, преди да премахнете себе си като администратор", + "account-deletion-disabled": "Изтриването на акаунт е забранено", + "cant-delete-admin": "Премахнете администраторските права от този акаунт, преди да го изтриете.", + "already-deleting": "Вече е в процес на изтриване", + "invalid-image": "Грешно изображение", + "invalid-image-type": "Грешен тип на изображение. Позволените типове са: %1", + "invalid-image-extension": "Грешно разширение на изображението", + "invalid-file-type": "Грешен тип на файл. Позволените типове са: %1", + "invalid-image-dimensions": "Размерите на изображението са твърде големи", + "group-name-too-short": "Името на групата е твърде кратко", + "group-name-too-long": "Името на групата е твърде дълго", + "group-already-exists": "Вече съществува такава група", + "group-name-change-not-allowed": "Промяната на името на групата не е разрешено", + "group-already-member": "Потребителят вече членува в тази група", + "group-not-member": "Потребителят не членува в тази група", + "group-needs-owner": "Тази група се нуждае от поне един собственик", + "group-already-invited": "Този потребител вече е бил поканен", + "group-already-requested": "Вашата заявка за членство вече е била изпратена", + "group-join-disabled": "В момента не можете да се присъедините към тази група", + "group-leave-disabled": "В момента не можете да напуснете тази група", + "post-already-deleted": "Тази публикация вече е изтрита", + "post-already-restored": "Тази публикация вече е възстановена", + "topic-already-deleted": "Тази тема вече е изтрита", + "topic-already-restored": "Тази тема вече е възстановена", + "cant-purge-main-post": "Не можете да изчистите първоначалната публикация. Моля, вместо това изтрийте темата.", + "topic-thumbnails-are-disabled": "Иконките на темите са изключени.", + "invalid-file": "Грешен файл", + "uploads-are-disabled": "Качването не е разрешено", + "signature-too-long": "Съжаляваме, но подписът Ви трябва да съдържа не повече от %1 символ(а).", + "about-me-too-long": "Съжаляваме, но информацията за Вас трябва да съдържа не повече от %1 символ(а).", + "cant-chat-with-yourself": "Не можете да пишете съобщение на себе си!", + "chat-restricted": "Този потребител е ограничил съобщенията до себе си. Той трябва първо да Ви последва, преди да можете да си пишете с него.", + "chat-disabled": "Системата за разговори е изключена", + "too-many-messages": "Изпратили сте твърде много съобщения. Моля, изчакайте малко.", + "invalid-chat-message": "Неправилно съобщение", + "chat-message-too-long": "Съобщенията в разговор не може да бъдат по-дълги от %1 знака.", + "cant-edit-chat-message": "Нямате право да редактирате това съобщение", + "cant-delete-chat-message": "Нямате право да изтриете това съобщение", + "chat-edit-duration-expired": "Можете да редактирате съобщенията си в разговорите до %1 секунда/и, след като ги пуснете", + "chat-delete-duration-expired": "Можете да изтривате съобщенията си в разговорите до %1 секунда/и след пускането им", + "chat-deleted-already": "Това съобщение вече е изтрито.", + "chat-restored-already": "Това съобщение вече е възстановено.", + "chat-room-does-not-exist": "Стаята за разговори не съществува.", + "already-voting-for-this-post": "Вече сте дали глас за тази публикация.", + "reputation-system-disabled": "Системата за репутация е изключена.", + "downvoting-disabled": "Отрицателното гласуване е изключено", + "not-enough-reputation-to-chat": "Репутацията Ви трябва да бъде поне %1, за да участвате в разговори", + "not-enough-reputation-to-upvote": "Репутацията Ви трябва да бъде поне %1, за да гласувате положително", + "not-enough-reputation-to-downvote": "Репутацията Ви трябва да бъде поне %1, за да гласувате отрицателно", + "not-enough-reputation-to-flag": "Репутацията Ви трябва да бъде поне %1, за да докладвате тази публикация", + "not-enough-reputation-min-rep-website": "Репутацията Ви трябва да бъде поне %1, за да добавите уеб сайт", + "not-enough-reputation-min-rep-aboutme": "Репутацията Ви трябва да бъде поне %1, за да добавите информация за себе си", + "not-enough-reputation-min-rep-signature": "Репутацията Ви трябва да бъде поне %1, за да добавите подпис", + "not-enough-reputation-min-rep-profile-picture": "Репутацията Ви трябва да бъде поне %1, за да добавите снимка на профила си", + "not-enough-reputation-min-rep-cover-picture": "Репутацията Ви трябва да бъде поне %1, за да добавите снимка на корицата", + "post-already-flagged": "Вече сте докладвали тази публикация", + "user-already-flagged": "Вече сте докладвали този потребител", + "post-flagged-too-many-times": "Тази публикация вече е докладвана от други хора", + "user-flagged-too-many-times": "Този потребител вече е докладван от други хора", + "cant-flag-privileged": "Не можете да докладвате профилите или съдържанието от потребители с по-високи правомощия (модератори, глобални модератори, администратори)", + "self-vote": "Не можете да гласувате за собствената си публикация", + "too-many-upvotes-today": "Можете да гласувате положително не повече от %1 пъти на ден", + "too-many-upvotes-today-user": "Можете да гласувате положително за потребител не повече от %1 пъти на ден", + "too-many-downvotes-today": "Можете да гласувате отрицателно не повече от %1 пъти на ден", + "too-many-downvotes-today-user": "Можете да гласувате отрицателно за потребител не повече от %1 пъти на ден", + "reload-failed": "NodeBB срещна проблем при презареждането: „%1“. NodeBB ще продължи да поддържа съществуващите клиентски ресурси, но Вие трябва да отмените последните си действия преди презареждането.", + "registration-error": "Грешка при регистрацията", + "parse-error": "Нещо се обърка при прочитането на отговора на сървъра", + "wrong-login-type-email": "Моля, използвайте е-пощата си, за да се впишете", + "wrong-login-type-username": "Моля, използвайте потребителското си име, за да се впишете", + "sso-registration-disabled": "Регистрацията за акаунти от %1 беше забранена, моля, регистрирайте се първо с е-поща", + "sso-multiple-association": "Не можете да свържете повече от един акаунт от тази услуга с акаунта си в NodeBB. Моля, премахнете връзката със съществуващия акаунт и опитайте отново.", + "invite-maximum-met": "Вие сте поканили максимално позволения брой хора (%1 от %2).", + "no-session-found": "Няма намерена сесия на вписване!", + "not-in-room": "Потребителят не е в стаята", + "cant-kick-self": "Не можете да изритате себе си от групата", + "no-users-selected": "Няма избран(и) потребител(и)", + "invalid-home-page-route": "Грешен път към началната страница", + "invalid-session": "Изтекла сесия", + "invalid-session-text": "Изглежда сесията Ви на вписване вече е изтекла. Моля, опреснете страницата.", + "session-mismatch": "Несъответствие в сесията", + "session-mismatch-text": "Изглежда сесията Ви на вписване вече не съответства на сървъра. Моля, опреснете страницата.", + "no-topics-selected": "Няма избрани теми!", + "cant-move-to-same-topic": "Публикацията не може да бъде преместена в същата тема!", + "cant-move-topic-to-same-category": "Темата не може да бъде преместена в същата категория!", + "cannot-block-self": "Не можете да блокирате себе си!", + "cannot-block-privileged": "Не можете да блокирате администратори и глобални модератори", + "cannot-block-guest": "Гостите не могат да блокират други потребители", + "already-blocked": "Този потребител вече е блокиран", + "already-unblocked": "Този потребител вече е отблокиран", + "no-connection": "Изглежда има проблем с връзката Ви с Интернет", + "socket-reconnect-failed": "В момента сървърът е недостъпен. Натиснете тук, за да опитате отново, или опитайте пак по-късно.", + "plugin-not-whitelisted": "Добавката не може да бъде инсталирана – само добавки, одобрени от пакетния мениджър на NodeBB могат да бъдат инсталирани чрез ACP", + "plugins-set-in-configuration": "Не можете да променяте състоянието на добавката, тъй като то се определя по време на работата ѝ (чрез config.json, променливи на средата или аргументи при изпълнение). Вместо това може да промените конфигурацията.", + "theme-not-set-in-configuration": "Когато определяте активните добавки в конфигурацията, промяната на темите изисква да се добави новата тема към активните добавки, преди актуализирането ѝ в ACP", + "topic-event-unrecognized": "Събитието „%1“ на темата е неизвестно", + "cant-set-child-as-parent": "Дъщерна категория не може да се зададе като базова такава", + "cant-set-self-as-parent": "Категорията не може да се зададе като базова категория на себе си", + "api.master-token-no-uid": "Беше получен главен код без съответстващо поле `_uid` в тялото на заявката", + "api.400": "Нещо не беше наред с данните в заявката, които подадохте.", + "api.401": "Няма намерена сесия. Моля, впишете се и опитайте отново.", + "api.403": "Нямате право да изпълните тази команда", + "api.404": "Неправилна команда към ППИ", + "api.426": "Заявките към ППИ за писане изискват HTTPS. Изпратете отново заявката си чрез HTTPS", + "api.429": "Направили сте твърде много заявки. Моля, опитайте отново по-късно.", + "api.500": "При обработката на заявката Ви възникна неочаквана грешка.", + "api.501": "Пътят, който се опитвате да извикате, все още не съществува. Моля, опитайте отново утре.", + "api.503": "Пътят, който се опитвате да извикате, в момента не е достъпен, поради настройките на сървъра." +} \ No newline at end of file diff --git a/public/language/bg/flags.json b/public/language/bg/flags.json new file mode 100644 index 0000000000..14b4766cbc --- /dev/null +++ b/public/language/bg/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Състояние", + "reports": "Доклади", + "first-reported": "Първо докладване", + "no-flags": "Ура! Няма намерени доклади.", + "assignee": "Назначен", + "update": "Обновяване", + "updated": "Обновено", + "resolved": "Разрешен", + "target-purged": "Съдържанието, за което се отнася този доклад, е било изтрито и вече не е налично.", + + "graph-label": "Дневни етикети", + "quick-filters": "Бързи филтри", + "filter-active": "В този списък с доклади има един или повече филтри", + "filter-reset": "Премахване на филтрите", + "filters": "Настройки на филтрите", + "filter-reporterId": "Потр. ид. на докладвалия", + "filter-targetUid": "Потр. ид. на докладвания", + "filter-type": "Вид на доклада", + "filter-type-all": "Всичко", + "filter-type-post": "Публикация", + "filter-type-user": "Потребител", + "filter-state": "Състояние", + "filter-assignee": "Потр. ид. на назначения", + "filter-cid": "Категория", + "filter-quick-mine": "Назначени на мен", + "filter-cid-all": "Всички категории", + "apply-filters": "Прилагане на филтрите", + "more-filters": "Още филтри", + "fewer-filters": "По-малко филтри", + + "quick-actions": "Бързи действия", + "flagged-user": "Докладван потребител", + "view-profile": "Преглед на профила", + "start-new-chat": "Започване на нов разговор", + "go-to-target": "Преглед на целта на доклада", + "assign-to-me": "Назначаване на мен", + "delete-post": "Изтриване на публикацията", + "purge-post": "Изчистване на публикацията", + "restore-post": "Възстановяване на публикацията", + "delete": "Изтриване на доклада", + + "user-view": "Преглед на профила", + "user-edit": "Редактиране на профила", + + "notes": "Бележки към доклада", + "add-note": "Добавяне на бележка", + "no-notes": "Няма споделени бележки.", + "delete-note-confirm": "Наистина ли искате да изтриете тази бележка към доклада?", + "delete-flag-confirm": "Наистина ли искате да изтриете този доклад?", + "note-added": "Бележката е добавена", + "note-deleted": "Бележката е изтрита", + "flag-deleted": "Докладът е изтрит", + + "history": "Акаунт и история на докладванията", + "no-history": "Няма история на доклада.", + + "state-all": "Всички състояния", + "state-open": "Нов/отворен", + "state-wip": "В процес на работа", + "state-resolved": "Разрешен", + "state-rejected": "Отхвърлен", + "no-assignee": "Без назначение", + + "sort": "Подреждане по", + "sort-newest": "Първо най-новите", + "sort-oldest": "Първо най-старите", + "sort-reports": "Първо тези с най-много доклади", + "sort-all": "Всички видове доклади…", + "sort-posts-only": "Само публикации…", + "sort-downvotes": "Най-много отрицателни гласове", + "sort-upvotes": "Най-много положителни гласове", + "sort-replies": "Най-много отговори", + + "modal-title": "Докладване на съдържанието", + "modal-body": "Моля, посочете причината за докладването на %1 %2 за преглед. Или използвайте някой от бутоните за бързо докладване, ако са приложими.", + "modal-reason-spam": "Спам", + "modal-reason-offensive": "Обидно", + "modal-reason-other": "Друго (опишете по-долу)", + "modal-reason-custom": "Причина за докладването на това съдържание…", + "modal-submit": "Изпращане на доклада", + "modal-submit-success": "Съдържанието беше докладвано на модераторите.", + + "bulk-actions": "Групови действия", + "bulk-resolve": "Разрешаване на доклад(и)", + "bulk-success": "%1 доклада са обновени", + "flagged-timeago-readable": "Докладвано (%2)", + "auto-flagged": "[Авт. докладвано] Получени %1 отрицателни гласа." +} \ No newline at end of file diff --git a/public/language/bg/global.json b/public/language/bg/global.json new file mode 100644 index 0000000000..545e1eaa9b --- /dev/null +++ b/public/language/bg/global.json @@ -0,0 +1,126 @@ +{ + "home": "Начало", + "search": "Търсене", + "buttons.close": "Затваряне", + "403.title": "Достъпът е отказан", + "403.message": "Изглежда сте посетили страница, до която нямате достъп.", + "403.login": "Може би трябва да опитате да се впишете?", + "404.title": "Не е намерена", + "404.message": "Изглежда сте се опитали да посетите страница, която не съществува. Върнете се към началната страница.", + "500.title": "Вътрешна грешка.", + "500.message": "Опа! Изглежда нещо се обърка!", + "400.title": "Грешна заявка.", + "400.message": "Тази връзка изглежда повредена. Моля, проверете я и опитайте отново. В противен случай се върнете на началната страница.", + "register": "Регистрация", + "login": "Вписване", + "please_log_in": "Моля, впишете се", + "logout": "Изход", + "posting_restriction_info": "Публикуването в момента е позволено само за регистрираните потребители. Натиснете тук, за да се впишете.", + "welcome_back": "Добре дошли отново", + "you_have_successfully_logged_in": "Вие влязохте успешно", + "save_changes": "Запазване на промените", + "save": "Запазване", + "close": "Затваряне", + "pagination": "Странициране", + "pagination.out_of": "%1 от %2", + "pagination.enter_index": "Към публикация номер", + "header.admin": "Администратор", + "header.categories": "Категории", + "header.recent": "Скорошни", + "header.unread": "Непрочетени", + "header.tags": "Етикети", + "header.popular": "Популярни", + "header.top": "Най-харесвани", + "header.users": "Потребители", + "header.groups": "Групи", + "header.chats": "Разговори", + "header.notifications": "Известия", + "header.search": "Търсене", + "header.profile": "Профил", + "header.navigation": "Навигация", + "notifications.loading": "Зареждане на известията", + "chats.loading": "Зареждане на разговорите", + "motd.welcome": "Добре дошли в NodeBB, системата за дискусии на бъдещето.", + "previouspage": "Предишна страница", + "nextpage": "Следваща страница", + "alert.success": "Готово", + "alert.error": "Грешка", + "alert.banned": "Блокиран", + "alert.banned.message": "Вие току-що бяхте блокиран. Достъпът Ви до системата е ограничен.", + "alert.unbanned": "Деблокиран", + "alert.unbanned.message": "Блокирането Ви беше премахнато", + "alert.unfollow": "Вие вече не следвате %1!", + "alert.follow": "Вие следвате %1!", + "users": "Потребители", + "topics": "Теми", + "posts": "Публ.", + "x-posts": "%1 публикации", + "best": "Най-добри", + "controversial": "Противоречиви", + "votes": "Гласове", + "x-votes": "%1 гласа", + "voters": "Гласували", + "upvoters": "Гласували положително", + "upvoted": "С положителни гласове", + "downvoters": "Гласували отрицателно", + "downvoted": "С отрицателни гласове", + "views": "Прегл.", + "posters": "Участници", + "reputation": "Репутация", + "lastpost": "Последна публикация", + "firstpost": "Първа публикация", + "read_more": "още", + "more": "Още", + "none": "Нищо", + "posted_ago_by_guest": "публикувано %1 от гост", + "posted_ago_by": "публикувано %1 от %2", + "posted_ago": "публикувано %1", + "posted_in": "публикувано в %1", + "posted_in_by": "публикувано в %1 от %2", + "posted_in_ago": "публикувано в %1 %2", + "posted_in_ago_by": "публикувано в %1 %2 от %3", + "user_posted_ago": "%1 публикува %2", + "guest_posted_ago": "гост публикува %1", + "last_edited_by": "последно редактирано от %1", + "norecentposts": "Няма скорошни публикации", + "norecenttopics": "Няма скорошни теми", + "recentposts": "Скорошни публикации", + "recentips": "Наскоро ползвани IP адреси", + "moderator_tools": "Модераторски инструменти", + "online": "На линия", + "away": "Отсъстващ", + "dnd": "Отпочиващ", + "invisible": "Невидим", + "offline": "Извън линия", + "email": "Е-поща", + "language": "Език", + "guest": "Гост", + "guests": "Гости", + "former_user": "Бивш потребител", + "system-user": "Системен", + "unknown-user": "Непознат потребител", + "updated.title": "Форумът е актуализиран", + "updated.message": "Този форум току-що беше актуализиран до най-новата версия. Натиснете тук, за да опресните страницата.", + "privacy": "Поверителност", + "follow": "Следване", + "unfollow": "Прекратяване на следването", + "delete_all": "Изтриване на всичко", + "map": "Карта", + "sessions": "Сесии на вписване", + "ip_address": "IP адрес", + "enter_page_number": "Въведете номер на страница", + "upload_file": "Качване на файл", + "upload": "Качване", + "uploads": "Качвания", + "allowed-file-types": "Разрешените файлови типове са: %1", + "unsaved-changes": "Имате незапазени промени. Наистина ли искате да напуснете тази страница?", + "reconnecting-message": "Изглежда връзката Ви към %1 беше прекъсната. Моля, изчакайте докато се опитаме да Ви свържем отново.", + "play": "Пускане", + "cookies.message": "Този уеб сайт използва бисквитки, за да предостави услугите си по възможно най-добрия начин.", + "cookies.accept": "Разбрано!", + "cookies.learn_more": "Научете повече", + "edited": "Редактирано", + "disabled": "Изключено", + "select": "Избиране", + "user-search-prompt": "Започнете да пишете, за да потърсите потребител…" +} \ No newline at end of file diff --git a/public/language/bg/groups.json b/public/language/bg/groups.json new file mode 100644 index 0000000000..6b2c8bb74b --- /dev/null +++ b/public/language/bg/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Групи", + "view_group": "Преглед на групата", + "owner": "Собственик на групата", + "new_group": "Създаване на нова група", + "no_groups_found": "Няма групи", + "pending.accept": "Приемане", + "pending.reject": "Отхвърляне", + "pending.accept_all": "Приемане на всички", + "pending.reject_all": "Отхвърляне на всички", + "pending.none": "В момента няма чакащи членове", + "invited.none": "В момента няма поканени членове", + "invited.uninvite": "Отмяна на поканата", + "invited.search": "Потърсете потребител, когото да поканите в тази група", + "invited.notification_title": "Вие бяхте поканен/а да се присъедините към %1", + "request.notification_title": "Заявка за членство в групата от %1", + "request.notification_text": "%1 поиска да стане член на %2", + "cover-save": "Запазване", + "cover-saving": "Запазване", + "details.title": "Подробности за групата", + "details.members": "Списък на членовете", + "details.pending": "Кандидатстващи членове", + "details.invited": "Поканени членове", + "details.has_no_posts": "Членовете на тази група не са публикували нищо.", + "details.latest_posts": "Скорошни публикации", + "details.private": "Частна", + "details.disableJoinRequests": "Забраняване на заявките за присъединяване", + "details.disableLeave": "Забраняване на потребители да напускат групата", + "details.grant": "Даване/отнемане на собственост", + "details.kick": "Изгонване", + "details.kick_confirm": "Наистина ли искате да премахнете този член на групата?", + "details.add-member": "Добавяне на член", + "details.owner_options": "Администрация на групата", + "details.group_name": "Име на групата", + "details.member_count": "Брой на членовете", + "details.creation_date": "Дата на създаване", + "details.description": "Описание", + "details.member-post-cids": "Идентификатори на категории, от които да се показват публикации", + "details.badge_preview": "Преглед на емблемата", + "details.change_icon": "Промяна на иконката", + "details.change_label_colour": "Промяна на цвета на етикета", + "details.change_text_colour": "Промяна на цвета на текста", + "details.badge_text": "Текст на емблемата", + "details.userTitleEnabled": "Показване на емблемата", + "details.private_help": "Ако е включено, присъединяването към група ще изисква одобрение от собственик на групата.", + "details.hidden": "Скрита", + "details.hidden_help": "Ако е включено, групата няма да е видима в списъка с групи и ще трябва потребителите да бъдат поканени специално.", + "details.delete_group": "Изтриване на групата", + "details.private_system_help": "Частните групи са забранени на системно ниво; тази възможност не върши нищо", + "event.updated": "Подробностите за групата бяха обновени", + "event.deleted": "Групата „%1“ е изтрита", + "membership.accept-invitation": "Приемане на поканата", + "membership.accept.notification_title": "В момента сте член на %1", + "membership.invitation-pending": "Чакаща покана", + "membership.join-group": "Присъединяване към групата", + "membership.leave-group": "Напускане на групата", + "membership.leave.notification_title": "%1 напусна групата %2", + "membership.reject": "Отхвърляне", + "new-group.group_name": "Име на групата:", + "upload-group-cover": "Качване на снимка за показване на групата", + "bulk-invite-instructions": "Въведете списък от потребителски имена, разделени със запетаи", + "bulk-invite": "Масова покана", + "remove_group_cover_confirm": "Наистина ли искате да премахнете снимката на корицата?" +} \ No newline at end of file diff --git a/public/language/bg/ip-blacklist.json b/public/language/bg/ip-blacklist.json new file mode 100644 index 0000000000..ce6401534c --- /dev/null +++ b/public/language/bg/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Тук можете да настроите своя черен списък за IP адреси.", + "description": "Понякога блокирането на даден потребителски профил не е достатъчно. В такива случаи, най-добрият начин за защитаване на форума е ограничаването на достъпа до форума за конкретен IP адрес или група от адреси. В този черен списък можете да добавите проблемните IP адреси или цял блок CIDR, и тези адреси няма да могат да влизат в системата или да регистрират нови профили.", + "active-rules": "Активни правила", + "validate": "Проверка на черния списък", + "apply": "Прилагане на черния списък", + "hints": "Съвети за синтактиса", + "hint-1": "Въвеждайте по един IP адрес на ред. Можете да добавяте групи от IP адреси, ако спазват формата на CIDR (напр. 192.168.100.0/22).", + "hint-2": "Можете да добавяте коментари, като в началото на реда поставите знака #.", + + "validate.x-valid": "Правилни правила: %1 от %2.", + "validate.x-invalid": "Следните %1 правила са грешни:", + + "alerts.applied-success": "Черният списък е приложен", + + "analytics.blacklist-hourly": "Фигура 1 – Попадения в черния списък за час", + "analytics.blacklist-daily": "Фигура 2 – Попадения в черния списък за ден", + "ip-banned": "Блокиран IP адрес" +} \ No newline at end of file diff --git a/public/language/bg/language.json b/public/language/bg/language.json new file mode 100644 index 0000000000..31a3b8055b --- /dev/null +++ b/public/language/bg/language.json @@ -0,0 +1,5 @@ +{ + "name": "Български", + "code": "bg", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/bg/login.json b/public/language/bg/login.json new file mode 100644 index 0000000000..efe62d3a5c --- /dev/null +++ b/public/language/bg/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Потребителско име / е-поща", + "username": "Потребителско име", + "remember_me": "Запомнете ме?", + "forgot_password": "Забравена парола?", + "alternative_logins": "Други начини за вписване", + "failed_login_attempt": "Неуспешно вписване", + "login_successful": "Вие влязохте успешно!", + "dont_have_account": "Нямате регистрация?", + "logged-out-due-to-inactivity": "Вие излязохте автоматично от администраторския контролен панел, поради бездействие.", + "caps-lock-enabled": "Главните букви са включени" +} \ No newline at end of file diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json new file mode 100644 index 0000000000..bfad149874 --- /dev/null +++ b/public/language/bg/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Разговор с", + "chat.placeholder": "Въведете съобщение тук или пуснете снимки и натиснете Ентер за изпращане", + "chat.scroll-up-alert": "В момента разглеждате по-стари съобщения. Щракнете тук, за да се прехвърлите към най-новото съобщение.", + "chat.send": "Изпращане", + "chat.no_active": "Нямате текущи разговори.", + "chat.user_typing": "%1 пише...", + "chat.user_has_messaged_you": "%1 Ви написа съобщение.", + "chat.see_all": "Всички разговори", + "chat.mark_all_read": "Отбелязване на всички като прочетени", + "chat.no-messages": "Моля, изберете получател, за да видите историята на съобщенията", + "chat.no-users-in-room": "Няма потребители в тази стая", + "chat.recent-chats": "Скорошни разговори", + "chat.contacts": "Контакти", + "chat.message-history": "История на съобщенията", + "chat.message-deleted": "Съобщението е изтрито", + "chat.options": "Настройки на разговора", + "chat.pop-out": "Отделяне на разговора в прозорец", + "chat.minimize": "Намаляване", + "chat.maximize": "Уголемяване", + "chat.seven_days": "7 дни", + "chat.thirty_days": "30 дни", + "chat.three_months": "3 месеца", + "chat.delete_message_confirm": "Наистина ли искате да изтриете това съобщение?", + "chat.retrieving-users": "Получаване на потребителите…", + "chat.manage-room": "Управление на стаята за разговори", + "chat.add-user-help": "Тук можете да потърсите потребители. Когато някой потребител бъде избран, той ще бъде добавен в разговора. Новият потребител няма да може да вижда съобщенията, написани преди включването му в разговора. Само собствениците на стаята () могат да премахват потребители от нея.", + "chat.confirm-chat-with-dnd-user": "Този потребител е в състояние „не ме безпокойте“. Наистина ли искате да разговаряте с него?", + "chat.rename-room": "Преименуване на стаята", + "chat.rename-placeholder": "Въведете името на стаята си тук", + "chat.rename-help": "Зададеното тук име на стаята ще се вижда от всички участници в нея.", + "chat.leave": "Напускане на разговора", + "chat.leave-prompt": "Наистина ли искате да напуснете този разговор?", + "chat.leave-help": "Ако напуснете този разговор, няма да виждате следващите съобщения в него. Ако бъдете добавен(а) отново, няма да виждате историята на разговора отпреди добавянето Ви.", + "chat.in-room": "В тази стая", + "chat.kick": "Изгонване", + "chat.show-ip": "Показване на IP адреса", + "chat.owner": "Собственик на стаята", + "chat.system.user-join": "%1 се присъедини към стаята", + "chat.system.user-leave": "%1 напусна стаята", + "chat.system.room-rename": "%2 преименува тази стая: %1", + "composer.compose": "Писане", + "composer.show_preview": "Показване на прегледа", + "composer.hide_preview": "Скриване на прегледа", + "composer.user_said_in": "%1 каза в %2:", + "composer.user_said": "%1 каза:", + "composer.discard": "Наистина ли искате да отхвърлите тази публикация?", + "composer.submit_and_lock": "Публикуване и заключване", + "composer.toggle_dropdown": "Превключване на падащото меню", + "composer.uploading": "Качване на %1", + "composer.formatting.bold": "Получер", + "composer.formatting.italic": "Курсив", + "composer.formatting.list": "Списък", + "composer.formatting.strikethrough": "Зачертан", + "composer.formatting.code": "Код", + "composer.formatting.link": "Връзка", + "composer.formatting.picture": "Връзка към изображение", + "composer.upload-picture": "Качване на изображение", + "composer.upload-file": "Качване на файл", + "composer.zen_mode": "Режим Дзен", + "composer.select_category": "Изберете категория", + "composer.textarea.placeholder": "Въведете съдържанието на публикацията си тук. Можете също да влачите и пускате снимки.", + "composer.schedule-for": "Насрочване на тема за", + "composer.schedule-date": "Дата", + "composer.schedule-time": "Час", + "composer.cancel-scheduling": "Отмяна на насрочването", + "composer.set-schedule-date": "Задаване на дата", + "bootbox.ok": "Добре", + "bootbox.cancel": "Отказ", + "bootbox.confirm": "Потвърждаване", + "bootbox.submit": "Публикуване", + "bootbox.send": "Изпращане", + "cover.dragging_title": "Наместване на снимката", + "cover.dragging_message": "Преместете снимката на желаното положение и натиснете „Запазване“", + "cover.saved": "Снимката и мястото ѝ бяха запазени", + "thumbs.modal.title": "Управление на иконките на темите", + "thumbs.modal.no-thumbs": "Няма намерени иконки.", + "thumbs.modal.resize-note": "Забележка: Този форум е настроен да преоразмерява иконките на темите до максимална ширина от %1px", + "thumbs.modal.add": "Добавяне на иконка", + "thumbs.modal.remove": "Премахване на иконката", + "thumbs.modal.confirm-remove": "Наистина ли искате да премахнете тази иконка?" +} \ No newline at end of file diff --git a/public/language/bg/notifications.json b/public/language/bg/notifications.json new file mode 100644 index 0000000000..8bc7240caf --- /dev/null +++ b/public/language/bg/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Известия", + "no_notifs": "Нямате нови известия", + "see_all": "Всички известия", + "mark_all_read": "Отбелязване на всички като прочетени", + "back_to_home": "Назад към %1", + "outgoing_link": "Външна връзка", + "outgoing_link_message": "Напускате %1", + "continue_to": "Продължаване към %1", + "return_to": "Връщане към %1", + "new_notification": "Имате ново известие", + "you_have_unread_notifications": "Имате непрочетени известия", + "all": "Всички", + "topics": "Теми", + "replies": "Отговори", + "chat": "Разговори", + "group-chat": "Групови разговори", + "follows": "Следвания", + "upvote": "Положителни гласове", + "new-flags": "Нови докладвания", + "my-flags": "Докладвания, назначени на мен", + "bans": "Блокирания", + "new_message_from": "Ново съобщение от %1", + "upvoted_your_post_in": "%1 гласува положително за Ваша публикация в %2.", + "upvoted_your_post_in_dual": "%1 и %2 гласуваха положително за Ваша публикация в %3.", + "upvoted_your_post_in_multiple": "%1 и %2 други гласуваха положително за Ваша публикация в %3.", + "moved_your_post": "%1 премести публикацията Ви в %2", + "moved_your_topic": "%1 премести %2", + "user_flagged_post_in": "%1 докладва Ваша публикация в %2", + "user_flagged_post_in_dual": "%1 и %2 докладваха Ваша публикация в %3", + "user_flagged_post_in_multiple": "%1 и %2 други докладваха Ваша публикация в %3", + "user_flagged_user": "%1 докладва потребителски профил (%2)", + "user_flagged_user_dual": "%1 и %2 докладваха потребителски профил (%3)", + "user_flagged_user_multiple": "%1 и още %2 потребители докладваха потребителски профил (%3)", + "user_posted_to": "%1 публикува отговор на: %2", + "user_posted_to_dual": "%1 и %2 публикуваха отговори на: %3", + "user_posted_to_multiple": "%1 и %2 други публикуваха отговори на: %3", + "user_posted_topic": "%1 публикува нова тема: %2", + "user_edited_post": "%1 редактира публикация в %2", + "user_started_following_you": "%1 започна да Ви следва.", + "user_started_following_you_dual": "%1 и %2 започнаха да Ви следват.", + "user_started_following_you_multiple": "%1 и %2 започнаха да Ви следват.", + "new_register": "%1 изпрати заявка за регистрация.", + "new_register_multiple": "Има %1 заявки за регистрация, които очакват да бъдат прегледани.", + "flag_assigned_to_you": "Докладът %1 беше назначен на Вас", + "post_awaiting_review": "Публикацията чака да бъде прегледана", + "profile-exported": "Профилът на %1 е изнесен, щракнете за сваляне", + "posts-exported": "Публикациите на %1 са изнесени, щракнете за сваляне", + "uploads-exported": "Качванията на %1 са изнесени, щракнете за сваляне", + "users-csv-exported": "Потребителите са изнесени във формат „csv“, щракнете за сваляне", + "post-queue-accepted": "Вашата публикация, която чакаше в опашката, беше приета. Натиснете тук, за да я видите.", + "post-queue-rejected": "Вашата публикация, която чакаше в опашката, беше отхвърлена.", + "post-queue-notify": "Публикация, чакаща в опашката, получи известие:
„%1“", + "email-confirmed": "Е-пощата беше потвърдена", + "email-confirmed-message": "Благодарим Ви, че потвърдихте е-пощата си. Акаунтът Ви е вече напълно активиран.", + "email-confirm-error-message": "Възникна проблем при потвърждаването на е-пощата Ви. Може кодът да е грешен или давността му да е изтекла.", + "email-confirm-sent": "Изпратено е е-писмо за потвърждение.", + "none": "Нищо", + "notification_only": "Само известие", + "email_only": "Само е-писмо", + "notification_and_email": "Известие и е-писмо", + "notificationType_upvote": "Когато някой гласува положително за Ваша публикация", + "notificationType_new-topic": "Когато някой, когото следвате, публикува тема", + "notificationType_new-reply": "Когато бъде публикуван нов отговор в тема, която следвате", + "notificationType_post-edit": "Когато бъде редактирана публикация в тема, която следите", + "notificationType_follow": "Когато някой започне да Ви следва", + "notificationType_new-chat": "Когато получите съобщение в разговор", + "notificationType_new-group-chat": "Когато получите съобщение в групов разговор", + "notificationType_group-invite": "Когато получите покана за група", + "notificationType_group-leave": "Когато потребител напусне групата Ви", + "notificationType_group-request-membership": "Когато някой поиска да се включи в група, на която Вие сте собственик", + "notificationType_new-register": "Когато някой бъде добавен в опашката за регистрация", + "notificationType_post-queue": "Когато бъде добавена нова публикация в опашката", + "notificationType_new-post-flag": "Когато публикация бъде докладвана", + "notificationType_new-user-flag": "Когато потребител бъде докладван" +} \ No newline at end of file diff --git a/public/language/bg/pages.json b/public/language/bg/pages.json new file mode 100644 index 0000000000..cf22a19cdb --- /dev/null +++ b/public/language/bg/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Начало", + "unread": "Непрочетени теми", + "popular-day": "Популярните теми днес", + "popular-week": "Популярните теми тази седмица", + "popular-month": "Популярните теми този месец", + "popular-alltime": "Популярните теми за всички времена", + "recent": "Скорошни теми", + "top-day": "Теми с най-много гласове днес", + "top-week": "Теми с най-много гласове тази седмица", + "top-month": "Теми с най-много гласове този месец", + "top-alltime": "Теми с най-много гласове", + "moderator-tools": "Модераторски инструменти", + "flagged-content": "Докладвано съдържание", + "ip-blacklist": "Черен списък за IP адреси", + "post-queue": "Опашка за публикации", + "users/online": "Потребители на линия", + "users/latest": "Последни потребители", + "users/sort-posts": "Потребители с най-много публикации", + "users/sort-reputation": "Потребители с най-висока репутация", + "users/banned": "Блокирани потребители", + "users/most-flags": "Най-докладвани потребители", + "users/search": "Търсене на потребители", + "notifications": "Известия", + "tags": "Етикети", + "tag": "Теми отбелязани като „%1“", + "register": "Регистриране на акаунт", + "registration-complete": "Регистрацията е завършена", + "login": "Впишете се в акаунта си", + "reset": "Нулирайте паролата за акаунта си", + "categories": "Категории", + "groups": "Групи", + "group": "Група %1", + "chats": "Разговори", + "chat": "Разговаря с %1", + "flags": "Доклади", + "flag-details": "Подробности за доклад %1", + "account/edit": "Редактиране на „%1“", + "account/edit/password": "Редактиране на паролата на „%1“", + "account/edit/username": "Редактиране на потребителското име на „%1“", + "account/edit/email": "Редактиране на е-пощата на „%1“", + "account/info": "Информация за акаунта", + "account/following": "Хора, които %1 следва", + "account/followers": "Хора, които следват %1", + "account/posts": "Публикации от %1", + "account/latest-posts": "Последни публикации от %1", + "account/topics": "Теми, създадени от %1", + "account/groups": "Групите на %1", + "account/watched_categories": "Следените категории на %1", + "account/bookmarks": "Отметнатите публикации на %1", + "account/settings": "Потребителски настройки", + "account/watched": "Теми, следени от %1", + "account/ignored": "Теми, пренебрегвани от %1", + "account/upvoted": "Публикации, получили положителен глас от %1", + "account/downvoted": "Публикации, получили отрицателен глас от %1", + "account/best": "Най-добрите публикации от %1", + "account/controversial": "Противоречиви публикации от %1", + "account/blocks": "Блокирани потребители за %1", + "account/uploads": "Качвания от %1", + "account/sessions": "Сесии на вписване", + "confirm": "Е-пощата е потвърдена", + "maintenance.text": "%1 в момента е в профилактика. Моля, върнете се по-късно.", + "maintenance.messageIntro": "В допълнение, администраторът е оставил това съобщение:", + "throttled.text": "%1 в момента е недостъпен, поради прекомерно натоварване. Моля, върнете се отново по-късно." +} \ No newline at end of file diff --git a/public/language/bg/post-queue.json b/public/language/bg/post-queue.json new file mode 100644 index 0000000000..2a2114f1bc --- /dev/null +++ b/public/language/bg/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Опашка за публикации", + "description": "Няма публикации в опашката.
За да включите тази функционалност, идете в Настройки → Публикуване → Опашка за публикации и включете Опашката за публикации.", + "user": "Потребител", + "category": "Категория", + "title": "Заглавие", + "content": "Съдържание", + "posted": "Публикувано", + "reply-to": "Отговор на „%1“", + "content-editable": "Щракнете върху съдържание, за да го редактирате", + "category-editable": "Щракнете върху категория, за да я редактирате", + "title-editable": "Щракнете върху заглавие, за да го редактирате", + "reply": "Отговор", + "topic": "Тема", + "accept": "Приемане", + "reject": "Отказване", + "remove": "Премахване", + "notify": "Известяване", + "notify-user": "Известяване на потребителя", + "confirm-reject": "Искате ли да отхвърлите тази публикация?", + "bulk-actions": "Групови действия", + "accept-all": "Приемане на всички", + "accept-selected": "Приемане на избраните", + "reject-all": "Отхвърляне на всички", + "reject-all-confirm": "Наистина ли искате да отхвърлите всички публикации?", + "reject-selected": "Отхвърляне на избраните", + "reject-selected-confirm": "Наистина ли искате да отхвърлите %1 избрани публикации?", + "bulk-accept-success": "Одобрени публикации: %1", + "bulk-reject-success": "Отхвърлени публикации: %1" +} \ No newline at end of file diff --git a/public/language/bg/recent.json b/public/language/bg/recent.json new file mode 100644 index 0000000000..a3029868b0 --- /dev/null +++ b/public/language/bg/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Скорошни", + "day": "Ден", + "week": "Седмица", + "month": "Месец", + "year": "Година", + "alltime": "Цялото време", + "no_recent_topics": "Няма скорошни теми.", + "no_popular_topics": "Няма популярни теми.", + "there-is-a-new-topic": "Има нова тема.", + "there-is-a-new-topic-and-a-new-post": "Има нова тема и нова публикация.", + "there-is-a-new-topic-and-new-posts": "Има нова тема и %1 нови публикации.", + "there-are-new-topics": "Има %1 нови теми.", + "there-are-new-topics-and-a-new-post": "Има %1 нови теми и нова публикация.", + "there-are-new-topics-and-new-posts": "Има %1 нови теми и %2 нови публикации.", + "there-is-a-new-post": "Има нова публикация", + "there-are-new-posts": "Има %1 нови публикации.", + "click-here-to-reload": "Натиснете тук, за да презаредите." +} \ No newline at end of file diff --git a/public/language/bg/register.json b/public/language/bg/register.json new file mode 100644 index 0000000000..d8eb4b6004 --- /dev/null +++ b/public/language/bg/register.json @@ -0,0 +1,32 @@ +{ + "register": "Регистрация", + "cancel_registration": "Отказ от регистриране", + "help.email": "По подразбиране, Вашата е-поща ще бъде скрита за останалите.", + "help.username_restrictions": "Уникално потребителско име с дължина между %1 и %2 символа. Другите ще могат да Ви споменават чрез @потребител.", + "help.minimum_password_length": "Дължината на паролата Ви трябва да е поне %1 символа.", + "email_address": "Е-поща", + "email_address_placeholder": "Въведете адрес на е-поща", + "username": "Потребителско име", + "username_placeholder": "Въведете потребителско име", + "password": "Парола", + "password_placeholder": "Въведете парола", + "confirm_password": "Потвърдете паролата", + "confirm_password_placeholder": "Потвърдете паролата", + "register_now_button": "Регистриране", + "alternative_registration": "Друг начин за регистриране", + "terms_of_use": "Условия за ползване", + "agree_to_terms_of_use": "Съгласен съм с условията за ползване", + "terms_of_use_error": "Трябва да се съгласите с условията за ползване", + "registration-added-to-queue": "Вашата регистрация беше добавена в опашката за одобрение. Ще получите е-писмо, когато тя бъде одобрена от администратор.", + "registration-queue-average-time": "Средното време за одобрение на нови членове е %1 часа и %2 минути.", + "registration-queue-auto-approve-time": "Членството Ви в този форум ще бъде напълно активирано след около %1 часа.", + "interstitial.intro": "Нуждаем се от малко допълнителна информация, преди да можем да актуализираме акаунта Ви…", + "interstitial.intro-new": "Нуждаем се от малко допълнителна информация, преди да можем да създадем акаунта Ви…", + "interstitial.errors-found": "Моля, прегледайте въведената информация:", + "gdpr_agree_data": "Съгласявам се това личната ми информация да се съхранява и обработва от този уеб сайт.", + "gdpr_agree_email": "Съгласявам се да получавам е-писма с резюмета и известия от този уеб сайт.", + "gdpr_consent_denied": "Трябва да се съгласите с това уеб сайтът да събира/обработва информацията Ви, и да Ви изпраща е-писма.", + "invite.error-admin-only": "Директното регистриране е изключено. Моля, свържете се с администратор за повече подробности.", + "invite.error-invite-only": "Директното регистриране е изключено. Трябва да получите покана от вече регистриран потребител, за да имате достъп до този форум.", + "invite.error-invalid-data": "Получените данни за регистрация не съответстват на нашите записи. Моля, свържете се с администратор за повече подробности." +} \ No newline at end of file diff --git a/public/language/bg/reset_password.json b/public/language/bg/reset_password.json new file mode 100644 index 0000000000..86a103fd21 --- /dev/null +++ b/public/language/bg/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Подновяване на паролата", + "update_password": "Промяна на паролата", + "password_changed.title": "Паролата беше променена", + "password_changed.message": "

Паролата е нулирана успешно. Моля, впишете се отново.", + "wrong_reset_code.title": "Грешен код за подновяване", + "wrong_reset_code.message": "Полученият код за подновяване беше грешен. Моля, опитайте отново или поискайте нов код за подновяване.", + "new_password": "Нова парола", + "repeat_password": "Потвърдете паролата", + "changing_password": "Промяна на паролата…", + "enter_email": "Моля, въведете адреса на е-пощата си и ще Ви изпратим е-писмо с инструкции за това как да достъпите акаунта си.", + "enter_email_address": "Въведете адрес на е-поща", + "password_reset_sent": "Ако посоченият адрес съответства на съществуващ потребителски акаунт, то вече му е изпратено е-писмо за подновяване на паролата. Имайте предвид, че може да бъде изпращано само по едно е-писмо на минута.", + "invalid_email": "Грешна е-поща / е-пощата не съществува!", + "password_too_short": "Паролата е твърде кратка. Моля, изберете друга парола.", + "passwords_do_not_match": "Двете пароли, които въведохте, са различни.", + "password_expired": "Паролата Ви е с изтекла давност. Моля, изберете нова парола" +} \ No newline at end of file diff --git a/public/language/bg/search.json b/public/language/bg/search.json new file mode 100644 index 0000000000..c1542c0bd9 --- /dev/null +++ b/public/language/bg/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 резултат(а), отговарящи на „%2“, (%3 секунди)", + "no-matches": "Няма съвпадения", + "advanced-search": "Разширено търсене", + "in": "В", + "titles": "Заглавия", + "titles-posts": "Заглавия и публикации", + "match-words": "Съвпадащи думи", + "all": "Всички", + "any": "Която и да е", + "posted-by": "Публикувано от", + "in-categories": "В категории", + "search-child-categories": "Претърсване на подкатегориите", + "has-tags": "Има етикети", + "reply-count": "Брой на отговорите", + "at-least": "Поне", + "at-most": "Най-много", + "relevance": "Уместност", + "post-time": "Време на публикуване", + "votes": "Гласове", + "newer-than": "По-нови от", + "older-than": "По-стари от", + "any-date": "Която и да е дата", + "yesterday": "Вчера", + "one-week": "Една седмица", + "two-weeks": "Две седмици", + "one-month": "Един месец", + "three-months": "Три месеца", + "six-months": "Шест месеца", + "one-year": "Една година", + "sort-by": "Подреждане по", + "last-reply-time": "Време на последния отговор", + "topic-title": "Заглавие на темата", + "topic-votes": "Гласувания за темата", + "number-of-replies": "Брой на отговорите", + "number-of-views": "Брой на преглежданията", + "topic-start-date": "Начална дата на темата", + "username": "Потребителско име", + "category": "Категория", + "descending": "В низходящ ред", + "ascending": "Във възходящ ред", + "save-preferences": "Запазване на предпочитанията", + "clear-preferences": "Изчистване на предпочитанията", + "search-preferences-saved": "Предпочитанията за търсене бяха запазени", + "search-preferences-cleared": "Предпочитанията за търсене бяха изчистени", + "show-results-as": "Показване на резултатите като", + "see-more-results": "Показване на още резултати (%1)", + "search-in-category": "Търсене в „%1“" +} \ No newline at end of file diff --git a/public/language/bg/success.json b/public/language/bg/success.json new file mode 100644 index 0000000000..6319fc1cc0 --- /dev/null +++ b/public/language/bg/success.json @@ -0,0 +1,7 @@ +{ + "success": "Готово", + "topic-post": "Вие публикувахте успешно.", + "post-queued": "Публикацията Ви е поставена в опашка за одобрение. Ще получите известие, когато тя бъде одобрена или отхвърлена.", + "authentication-successful": "Успешно удостоверяване", + "settings-saved": "Настройките са запазени!" +} \ No newline at end of file diff --git a/public/language/bg/tags.json b/public/language/bg/tags.json new file mode 100644 index 0000000000..78fde63479 --- /dev/null +++ b/public/language/bg/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Няма теми с този етикет.", + "tags": "Етикети", + "enter_tags_here": "Тук въведете етикети, всеки между %1 и %2 знака.", + "enter_tags_here_short": "Въведете етикети...", + "no_tags": "Все още няма етикети.", + "select_tags": "Изберете етикети" +} \ No newline at end of file diff --git a/public/language/bg/top.json b/public/language/bg/top.json new file mode 100644 index 0000000000..54b8374e26 --- /dev/null +++ b/public/language/bg/top.json @@ -0,0 +1,4 @@ +{ + "title": "Най-популярни", + "no_top_topics": "Няма най-популярни теми" +} \ No newline at end of file diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json new file mode 100644 index 0000000000..7cf87f252d --- /dev/null +++ b/public/language/bg/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Тема", + "title": "Заглавие", + "no_topics_found": "Няма намерени теми!", + "no_posts_found": "Няма намерени публикации!", + "post_is_deleted": "Публикацията е изтрита!", + "topic_is_deleted": "Темата е изтрита!", + "profile": "Профил", + "posted_by": "Публикувано от %1", + "posted_by_guest": "Публикувано от гост", + "chat": "Разговор", + "notify_me": "Получавайте известия за новите отговори в тази тема", + "quote": "Цитат", + "reply": "Отговор", + "replies_to_this_post": "%1 отговора", + "one_reply_to_this_post": "1 отговор", + "last_reply_time": "Последен отговор", + "reply-as-topic": "Отговор в нова тема", + "guest-login-reply": "Впишете се, за да отговорите", + "login-to-view": "🔒 Впишете се, за да видите това", + "edit": "Редактиране", + "delete": "Изтриване", + "delete-event": "Изтриване на събитието", + "delete-event-confirm": "Наистина ли искате да изтриете това събитие?", + "purge": "Изчистване", + "restore": "Възстановяване", + "move": "Преместване", + "change-owner": "Промяна на собственика", + "fork": "Разделяне", + "link": "Връзка", + "share": "Споделяне", + "tools": "Инструменти", + "locked": "Заключена", + "pinned": "Закачена", + "pinned-with-expiry": "Закачена до %1", + "scheduled": "Насрочена", + "moved": "Преместена", + "moved-from": "Преместена от %1", + "copy-ip": "Копиране на IP адреса", + "ban-ip": "Блокиране на IP адреса", + "view-history": "История на редакциите", + "locked-by": "Заключена от", + "unlocked-by": "Отключена от", + "pinned-by": "Закачена от", + "unpinned-by": "Откачена от", + "deleted-by": "Изтрита от", + "restored-by": "Възстановена от", + "moved-from-by": "Преместена от %1 от", + "queued-by": "Публикацията е добавена в опашката за одобрение →", + "backlink": "Спомената от", + "forked-by": "Разделена от", + "bookmark_instructions": "Щракнете тук, за да се върнете към последно прочетената публикация в тази тема.", + "flag-post": "Докладване на тази публикация", + "flag-user": "Докладване на този потребител", + "already-flagged": "Вече е докладвано", + "view-flag-report": "Преглед на доклада", + "resolve-flag": "Разрешаване на доклада", + "merged_message": "Тази тема беше слята в %2", + "deleted_message": "Темата е изтрита. Само потребители с права за управление на темите могат да я видят.", + "following_topic.message": "Вече ще получавате известия когато някой публикува коментар в тази тема.", + "not_following_topic.message": "Ще виждате тази тема в списъка с непрочетени теми, но няма да получавате известия, когато хората публикуват нещо в нея.", + "ignoring_topic.message": "Вече няма да виждате тази тема в списъка с непрочетени теми. Ще получите известие, когато някой Ви спомене или гласува положително за Ваша публикация.", + "login_to_subscribe": "Моля, регистрирайте се или се впишете, за да се абонирате за тази тема.", + "markAsUnreadForAll.success": "Темата е отбелязана като непрочетена за всички.", + "mark_unread": "Отбелязване като непрочетена", + "mark_unread.success": "Темата е отбелязана като непрочетена.", + "watch": "Следене", + "unwatch": "Спиране на следенето", + "watch.title": "Получавайте известия за новите отговори в тази тема", + "unwatch.title": "Спрете да следите тази тема", + "share_this_post": "Споделете тази публикация", + "watching": "Следите", + "not-watching": "Не следите", + "ignoring": "Пренебрегвате", + "watching.description": "Искам да получавам известия за новите отговори.
Искам темата да се показва в списъка с непрочетени.", + "not-watching.description": "Не искам да получавам известия за новите отговори.
Темата да се показва в списъка с непрочетени, само ако категорията не се пренебрегва.", + "ignoring.description": "Не искам да получавам известия за новите отговори.
Не искам темата да се показва в списъка с непрочетени.", + "thread_tools.title": "Инструменти за темата", + "thread_tools.markAsUnreadForAll": "Отбелязване на всички като непрочетени", + "thread_tools.pin": "Закачане на темата", + "thread_tools.unpin": "Откачане на темата", + "thread_tools.lock": "Заключване на темата", + "thread_tools.unlock": "Отключване на темата", + "thread_tools.move": "Преместване на темата", + "thread_tools.move-posts": "Преместване на публикациите", + "thread_tools.move_all": "Преместване на всички", + "thread_tools.change_owner": "Промяна на собственика", + "thread_tools.select_category": "Избиране на категория", + "thread_tools.fork": "Разделяне на темата", + "thread_tools.delete": "Изтриване на темата", + "thread_tools.delete-posts": "Изтриване на публикациите", + "thread_tools.delete_confirm": "Наистина ли искате да изтриете тази тема?", + "thread_tools.restore": "Възстановяване на темата", + "thread_tools.restore_confirm": "Наистина ли искате да възстановите тази тема?", + "thread_tools.purge": "Изчистване на темата", + "thread_tools.purge_confirm": "Наистина ли искате да изчистите тази тема?", + "thread_tools.merge_topics": "Сливане на темите", + "thread_tools.merge": "Сливане", + "topic_move_success": "Темата ще бъде преместена в „%1“ след малко. Натиснете тук, за да отмените преместването.", + "topic_move_multiple_success": "Темите ще бъдат преместени в „%1“ след малко. Натиснете тук, за да отмените преместването.", + "topic_move_all_success": "Всички теми ще бъдат преместени в „%1“ след малко. Натиснете тук, за да отмените преместването.", + "topic_move_undone": "Преместването на темата беше отменено", + "topic_move_posts_success": "Публикациите ще бъдат преместени след малко. Натиснете тук, за да отмените преместването.", + "topic_move_posts_undone": "Преместването на публикациите беше отменено", + "post_delete_confirm": "Наистина ли искате да изтриете тази публикация?", + "post_restore_confirm": "Наистина ли искате да възстановите тази публикация?", + "post_purge_confirm": "Наистина ли искате да изчистите тази публикация?", + "pin-modal-expiry": "Дата на давност", + "pin-modal-help": "Ако желаете, тук можете да посочите дата на давност за закачените теми. Можете и да оставите полето празно, при което темата ще остане закачена, докато не бъде откачена ръчно.", + "load_categories": "Зареждане на категориите", + "confirm_move": "Преместване", + "confirm_fork": "Разделяне", + "bookmark": "Отметка", + "bookmarks": "Отметки", + "bookmarks.has_no_bookmarks": "Все още не сте си запазвали отметки към никакви публикации.", + "copy-permalink": "Копиране на постоянна връзка", + "loading_more_posts": "Зареждане на още публикации", + "move_topic": "Преместване на темата", + "move_topics": "Преместване на темите", + "move_post": "Преместване на публикацията", + "post_moved": "Публикацията беше преместена!", + "fork_topic": "Разделяне на темата", + "enter-new-topic-title": "Въведете заглавието на новата тема", + "fork_topic_instruction": "Натиснете публикациите, които искате да отделите", + "fork_no_pids": "Няма избрани публикации!", + "no-posts-selected": "Няма избрани публикации!", + "x-posts-selected": "Избрани публикации: %1", + "x-posts-will-be-moved-to-y": "%1 публикации ще бъдат преместени в „%2“", + "fork_pid_count": "Избрани публикации: %1", + "fork_success": "Темата е разделена успешно! Натиснете тук, за да преминете към отделената тема.", + "delete_posts_instruction": "Натиснете публикациите, които искате да изтриете/изчистите", + "merge_topics_instruction": "Щракнете върху темите, които искате да слеете, или ги потърсете", + "merge-topic-list-title": "Списък от темите, които ще бъдат слети", + "merge-options": "Настройки за сливането", + "merge-select-main-topic": "Изберете основната тема", + "merge-new-title-for-topic": "Ново заглавие за темата", + "topic-id": "Ид. на темата", + "move_posts_instruction": "Щракнете върху публикациите, които искате да преместите, а след това въведете ид. на тема или отидете в целевата тема", + "change_owner_instruction": "Натиснете публикациите, които искате да прехвърлите на друг потребител", + "composer.title_placeholder": "Въведете заглавието на темата си тук...", + "composer.handle_placeholder": "Въведете името тук", + "composer.discard": "Отхвърляне", + "composer.submit": "Публикуване", + "composer.additional-options": "Допълнителни настройки", + "composer.schedule": "Насрочване", + "composer.replying_to": "Отговор на %1", + "composer.new_topic": "Нова тема", + "composer.editing": "Редактиране", + "composer.uploading": "качване...", + "composer.thumb_url_label": "Поставете адреса на иконка за темата", + "composer.thumb_title": "Добавете иконка към тази тема", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Или качете файл", + "composer.thumb_remove": "Изчистване на полетата", + "composer.drag_and_drop_images": "Плъзнете снимките тук", + "more_users_and_guests": "Още %1 потребител(и) и %2 гост(и)", + "more_users": "Още %1 потребител(и)", + "more_guests": "Още %1 гост(и)", + "users_and_others": "%1 и %2 други", + "sort_by": "Подреждане по", + "oldest_to_newest": "Първо най-старите", + "newest_to_oldest": "Първо най-новите", + "most_votes": "Първо тези с най-много гласове", + "most_posts": "Първо тези с най-много публикации", + "most_views": "Първо тези с най-много преглеждания", + "stale.title": "Създаване на нова тема вместо това?", + "stale.warning": "Темата, в която отговаряте, е доста стара. Искате ли вместо това да създадете нова и да направите препратка към тази в отговора си?", + "stale.create": "Създаване на нова тема", + "stale.reply_anyway": "Отговаряне в тази тема въпреки това", + "link_back": "Отговор: [%1](%2)", + "diffs.title": "История на редакциите", + "diffs.description": "Тази публикация има %1 версии. Щракнете върху някоя от версиите по-долу, за да видите съдържанието ѝ в съответния момент.", + "diffs.no-revisions-description": "Тази публикация има %1 версии.", + "diffs.current-revision": "текуща версия", + "diffs.original-revision": "оригинална версия", + "diffs.restore": "Възстановяване на тази версия", + "diffs.restore-description": "След възстановяването към историята на редакциите на тази публикация ще бъде добавена нова версия.", + "diffs.post-restored": "Публикацията е възстановена успешно до по-ранна версия", + "diffs.delete": "Изтриване на тази версия ", + "diffs.deleted": "Версията е изтрита", + "timeago_later": "%1 по-късно", + "timeago_earlier": "%1 по-рано", + "first-post": "Първа публикация", + "last-post": "Последна публикация", + "go-to-my-next-post": "Към следващата ми публикация", + "no-more-next-post": "Нямате повече публикации в тази тема", + "post-quick-reply": "Пускане на бърза публикация" +} \ No newline at end of file diff --git a/public/language/bg/unread.json b/public/language/bg/unread.json new file mode 100644 index 0000000000..56aa07a052 --- /dev/null +++ b/public/language/bg/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Непрочетени", + "no_unread_topics": "Няма непрочетени теми.", + "load_more": "Зареждане на още", + "mark_as_read": "Отбелязване като прочетени", + "selected": "Избраните", + "all": "Всички", + "all_categories": "Всички категории", + "topics_marked_as_read.success": "Темите бяха отбелязани като прочетени!", + "all-topics": "Всички теми", + "new-topics": "Нови теми", + "watched-topics": "Следени теми", + "unreplied-topics": "Теми без отговор", + "multiple-categories-selected": "Избрани са няколко" +} \ No newline at end of file diff --git a/public/language/bg/uploads.json b/public/language/bg/uploads.json new file mode 100644 index 0000000000..f7d513e9d6 --- /dev/null +++ b/public/language/bg/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Качване на файла…", + "select-file-to-upload": "Изберете файл за качване!", + "upload-success": "Файлът е качен успешно!", + "maximum-file-size": "Най-много %1 КБ", + "no-uploads-found": "Няма качвания", + "public-uploads-info": "Качванията са публични – всички посетители могат да ги видят.", + "private-uploads-info": "Качванията са частни – само вписаните потребители могат да ги видят" +} \ No newline at end of file diff --git a/public/language/bg/user.json b/public/language/bg/user.json new file mode 100644 index 0000000000..04fdf98b62 --- /dev/null +++ b/public/language/bg/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Блокиран", + "muted": "Заглушен", + "offline": "Извън линия", + "deleted": "Изтрит", + "username": "Потребителско име", + "joindate": "Дата на присъединяване", + "postcount": "Брой публикации", + "email": "Е-поща", + "confirm_email": "Потвърдете е-пощата", + "account_info": "Информация за акаунта", + "admin_actions_label": "Административни действия", + "ban_account": "Блокиране на акаунта", + "ban_account_confirm": "Наистина ли искате да блокирате този потребител?", + "unban_account": "Деблокиране на акаунта", + "mute_account": "Заглушаване на акаунта", + "unmute_account": "Премахване на заглушаването на акаунта", + "delete_account": "Изтриване на акаунта", + "delete_account_as_admin": "Изтриване на акаунта", + "delete_content": "Изтриване на съдържанието на акаунта", + "delete_all": "Изтриване на акаунта и съдържанието", + "delete_account_confirm": "Наистина ли искате да направите публикациите си анонимни и да изтриете акаунта си?
Това действие е необратимо и няма да можете да възстановите нищо от данните си.

Въведете паролата си, за да потвърдите, че искате да унищожите този акаунт.", + "delete_this_account_confirm": "Наистина ли искате да изтриете този акаунт, но да оставите съдържанието му?
Това действие е необратимо. Публикациите ще бъдат превърнати в анонимни и вече няма да можете да възстановите връзката между публикациите и изтрития акаунт

", + "delete_account_content_confirm": "Наистина ли искате да изтриете съдържанието (публикации/теми/качвания) на този акаунт?
Това действие е необратимо и няма да можете да възстановите нищо от данните.

", + "delete_all_confirm": "Наистина ли искате да изтриете този акаунт и цялото му съдържание (публикации/теми/качвания)?
Това действие е необратимо и няма да можете да възстановите нищо от данните.

", + "account-deleted": "Акаунтът е изтрит", + "account-content-deleted": "Съдържанието на акаунта е изтрито", + "fullname": "Пълно име", + "website": "Уеб сайт", + "location": "Местоположение", + "age": "Възраст", + "joined": "Присъединил се", + "lastonline": "Последно на линия", + "profile": "Профил", + "profile_views": "Преглеждания на профила", + "reputation": "Репутация", + "bookmarks": "Отметки", + "watched_categories": "Следени категории", + "change_all": "Промяна на всички", + "watched": "Следени", + "ignored": "Пренебрегвани", + "default-category-watch-state": "Състояние по подразбиране за следенето на категории", + "followers": "Последователи", + "following": "Следва", + "blocks": "Блокира", + "block_toggle": "Превключване на блокирането", + "block_user": "Блокиране на потребителя", + "unblock_user": "Отблокиране на потребителя", + "aboutme": "За мен", + "signature": "Подпис", + "birthday": "Рождена дата", + "chat": "Разговор", + "chat_with": "Продължаване на разговора с %1", + "new_chat_with": "Започване на нов разговор с %1", + "flag-profile": "Докладване на профила", + "follow": "Следване", + "unfollow": "Спиране на следването", + "more": "Още", + "profile_update_success": "Профилът беше обновен успешно!", + "change_picture": "Промяна на снимката", + "change_username": "Промяна на потребителското име", + "change_email": "Промяна на е-пощата", + "email_same_as_password": "Моля, въведете текущата си парола, за да продължите – Вие въведохте новата си е-поща отново", + "edit": "Редактиране", + "edit-profile": "Редактиране на профила", + "default_picture": "Иконка по подразбиране", + "uploaded_picture": "Качена снимка", + "upload_new_picture": "Качване на нова снимка", + "upload_new_picture_from_url": "Качване на нова снимка от адрес", + "current_password": "Текуща парола", + "change_password": "Промяна на паролата", + "change_password_error": "Грешна парола!", + "change_password_error_wrong_current": "Текущата Ви парола е грешна!", + "change_password_error_match": "Паролите са различни!", + "change_password_error_privileges": "Нямате права да промените тази парола.", + "change_password_success": "Паролата ви е обновена!", + "confirm_password": "Потвърдете паролата", + "password": "Парола", + "username_taken_workaround": "Потребителското име, което искате, е заето и затова ние го променихме малко. Вие ще се наричате %1", + "password_same_as_username": "Паролата е същата като потребителското Ви име. Моля, изберете друга парола.", + "password_same_as_email": "Паролата е същата като е-пощата Ви. Моля, изберете друга парола.", + "weak_password": "Проста парола.", + "upload_picture": "Качване на снимка", + "upload_a_picture": "Качване на снимка", + "remove_uploaded_picture": "Премахване на качената снимка", + "upload_cover_picture": "Качване на снимка на корицата", + "remove_cover_picture_confirm": "Наистина ли искате да премахнете снимката на корицата?", + "crop_picture": "Орязване на снимката", + "upload_cropped_picture": "Орязване и качване", + "avatar-background-colour": "Фонов цвят за изображението", + "settings": "Настройки", + "show_email": "Да се показва е-пощата ми", + "show_fullname": "Да се показва цялото ми име", + "restrict_chats": "Разрешаване на съобщенията само от потребители, които следвам", + "digest_label": "Абониране за резюмета", + "digest_description": "Абониране за новини по е-пощата относно този форум (нови известия и теми) според избрания график", + "digest_off": "Изключено", + "digest_daily": "Ежедневно", + "digest_weekly": "Ежеседмично", + "digest_biweekly": "На всеки две седмици", + "digest_monthly": "Ежемесечно", + "has_no_follower": "Този потребител няма последователи :(", + "follows_no_one": "Този потребител не следва никого :(", + "has_no_posts": "Този потребител не е публикувал нищо досега.", + "has_no_best_posts": "Този потребител не е получавал положителни гласове за публикациите си досега.", + "has_no_topics": "Този потребител не е създавал теми досега.", + "has_no_watched_topics": "Този потребител не е следил нито една тема досега.", + "has_no_ignored_topics": "Този потребител не е пренебрегнал нито една тема досега.", + "has_no_upvoted_posts": "Този потребител не е гласувал положително досега.", + "has_no_downvoted_posts": "Този потребител не е гласувал отрицателно досега.", + "has_no_controversial_posts": "Този потребител няма публикации с отрицателни гласове засега.", + "has_no_blocks": "Не сте блокирали никого.", + "email_hidden": "Е-пощата е скрита", + "hidden": "скрито", + "paginate_description": "Разделяне на темите и публикациите на страници, вместо да се превърта безкрайно", + "topics_per_page": "Теми на страница", + "posts_per_page": "Публикации на страница", + "max_items_per_page": "Най-много %1", + "acp_language": "Език на администраторската страница", + "notifications": "Известия", + "upvote-notif-freq": "Честота на известията за положителни гласове", + "upvote-notif-freq.all": "Всички положителни гласове", + "upvote-notif-freq.first": "При първия за публикация", + "upvote-notif-freq.everyTen": "На всеки десет положителни гласа", + "upvote-notif-freq.threshold": "на 1, 5, 10, 25, 50, 100, 150, 200…", + "upvote-notif-freq.logarithmic": "На 10, 100, 1000…", + "upvote-notif-freq.disabled": "Изключено", + "browsing": "Настройки за страниците", + "open_links_in_new_tab": "Отваряне на външните връзки в нов подпрозорец", + "enable_topic_searching": "Включване на търсенето в темите", + "topic_search_help": "Ако е включено, търсенето в темата ще замени стандартното поведение на браузъра при търсене в страницата и ще Ви позволи да претърсвате цялата тема, а не само това, което се вижда на екрана", + "update_url_with_post_index": "Обновяване на адресната лента с номера на публикацията по време на разглеждане на темите", + "scroll_to_my_post": "След публикуване на отговор, да се показва новата публикация", + "follow_topics_you_reply_to": "Следене на темите, в които отговаряте", + "follow_topics_you_create": "Следене на темите, които създавате", + "grouptitle": "Заглавие на групата", + "group-order-help": "Изберете група и използвайте стрелките, за да пренаредите заглавията", + "no-group-title": "Няма заглавие на група", + "select-skin": "Изберете облик", + "select-homepage": "Изберете начална страница", + "homepage": "Начална страница", + "homepage_description": "Изберете страница, която да използвате като начална за форума, или „Нищо“, за да използвате тази по подразбиране.", + "custom_route": "Път до персонализираната начална страница", + "custom_route_help": "Въведете името на пътя тук, без наклонена черта пред него (пример: „recent“ или \"category/2/general-discussion\")", + "sso.title": "Услуги за еднократно вписване", + "sso.associated": "Свързан с", + "sso.not-associated": "Натиснете тук, за да свържете с", + "sso.dissociate": "Прекъсване на връзката", + "sso.dissociate-confirm-title": "Потвърждаване на прекъсването", + "sso.dissociate-confirm": "Наистина ли искате да прекъснете връзката на акаунта си от „%1“?", + "info.latest-flags": "Последни доклади", + "info.no-flags": "Не са намерени докладвани публикации", + "info.ban-history": "Скорошна история на блокиранията", + "info.no-ban-history": "Този потребител никога не е бил блокиран", + "info.banned-until": "Блокиран до %1", + "info.banned-expiry": "Давност", + "info.banned-permanently": "Блокиран за постоянно", + "info.banned-reason-label": "Причина", + "info.banned-no-reason": "Няма посочена причина.", + "info.mute-history": "Скорошна история на заглушаванията", + "info.no-mute-history": "Този потребител никога не е бил заглушаван", + "info.muted-until": "Заглушен до %1", + "info.muted-expiry": "Давност", + "info.muted-no-reason": "Няма посочена причина.", + "info.username-history": "История на потребителските имена", + "info.email-history": "Историята на е-пощите", + "info.moderation-note": "Модераторска бележка", + "info.moderation-note.success": "Модераторската бележка е запазена", + "info.moderation-note.add": "Добавяне на бележка", + "sessions.description": "На тази страница можете да видите активните си сесии на този форум и да ги анулирате, ако желаете. Можете да анулирате текущата си сесия, като се отпишете от акаунта си.", + "consent.title": "Вашите права и съгласие", + "consent.lead": "Този обществен форум събира и обработва лична информация.", + "consent.intro": "Използваме тази информация, само за да персонализираме взаимодействието Ви с форума, както и за да свържем публикациите Ви с Вашия потребителски акаунт. По време на регистрацията ще трябва да въведете потребителско име и е-поща, но ако искате, можете да предоставите и допълнителна информация, за да завършите потребителския си профил в уеб сайта.

Ние съхраняваме тази информация докато съществува потребителският Ви акаунт. Във всеки един момент можете да оттеглите съгласието си за това, като изтриете акаунта си. Във всеки един момент можете да изискате копие на въведеното от Вас в уеб сайт, чрез страницата за „Права и съгласие“.

Ако имате въпроси или притеснения, можете да се свържете с екипа от администратори на форума.", + "consent.email_intro": "Понякога може да изпращаме е-писма на регистрираната Ви е-поща, за да Ви кажем какво се случва, или да Ви уведомим, че има нещо ново, което Ви засяга. Можете да персонализирате честотата на резюметата (както и да ги изключите), както и да изберете какви известия да получавате по е-поща, чрез страницата с потребителските настройки.", + "consent.digest_frequency": "Освен ако не промените това в потребителските си настройки, тази общност ще Ви изпраща резюмета по е-поща на всеки %1.", + "consent.digest_off": "Освен ако не промените това в потребителските си настройки, тази общност няма да Ви изпраща резюмета по е-поща.", + "consent.received": "Вие сте се съгласили с това уеб сайтът да събира и обработва личната Ви информация. Не се изискват допълнителни действия.", + "consent.not_received": "Вие не сте се съгласили със събирането и обработването на Ваши данни. Администрацията на уеб сайта може по всяко време да изтрие акаунта Ви, за да спази изискванията за защита на данните.", + "consent.give": "Даване на съгласие", + "consent.right_of_access": "Имате право на достъп", + "consent.right_of_access_description": "Имате право на достъп до всички данни, събирани от този уеб сайт, при заявяване. Можете да получите копие от данните, като натиснете бутона по-долу.", + "consent.right_to_rectification": "Имате право на поправка", + "consent.right_to_rectification_description": "Имате право да промените или поправите всички неточни данни, които сте ни предоставили. Профилът Ви може да бъде променен като го редактирате, а съдържанието на публикациите може да бъде редактирано по всяко време. Ако имате по-различно изискване, моля, свържете се с администраторския екип", + "consent.right_to_erasure": "Имате право на изтриване", + "consent.right_to_erasure_description": "Във всеки един момент можете да оттеглите съгласието си за събиране и/или обработка на данни, като изтриете акаунта си. Вашият профил може да бъде изтрит, но публикуваното от Вас съдържание ще остане. Ако искате да изтриете както акаунта, така и съдържанието, публикувано от Вас, моля, свържете се с администрационния екип на уеб сайта.", + "consent.right_to_data_portability": "Имате право на пренос на данни", + "consent.right_to_data_portability_description": "Можете да изискате от нас всички събрани за Вас и акаунта Ви данни в машинен формат. Можете да направите това като натиснете съответния бутон по-долу.", + "consent.export_profile": "Изнасяне на профила (.json)", + "consent.export-profile-success": "Изнасяне на профила… Ще получите известие, когато е готово.", + "consent.export_uploads": "Изнасяне на каченото съдържание (.zip)", + "consent.export-uploads-success": "Изнасяне на каченото съдържание… Ще получите известие, когато е готово.", + "consent.export_posts": "Изнасяне на публикациите (.csv)", + "consent.export-posts-success": "Изнасяне на публикациите… Ще получите известие, когато е готово.", + "emailUpdate.intro": "Въведете е-пощата си по-долу. Този форум използва е-пощата за планирани резюмета и известия, както и за възстановяване на акаунта, в случай на забравена парола.", + "emailUpdate.optional": "Това поле не е задължително. Не сте длъжен/на да предоставяте адрес на е-поща, но без проверена е-поща, няма да можете да възстановите акаунта си в случай на проблем, нито ще можете да се вписвате с е-пощата си.", + "emailUpdate.required": "Това поле е задължително.", + "emailUpdate.change-instructions": "Ще Ви изпратим е-писмо за потвърждение на посочената е-поща, което ще съдържа уникална връзка. Щом последвате тази връзка, притежанието Ви на тази е-поща ще бъде потвърдено и тя ще бъде свързана с акаунта Ви. Ще можете да промените тази е-поща по всяко време, от страницата на акаунта си.", + "emailUpdate.password-challenge": "Въведете паролата си, за да потвърдите, че акаунтът е Ваш." +} \ No newline at end of file diff --git a/public/language/bg/users.json b/public/language/bg/users.json new file mode 100644 index 0000000000..9cd2dbb5ed --- /dev/null +++ b/public/language/bg/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Последни потребители", + "top_posters": "С най-много публикации", + "most_reputation": "С най-много репутация", + "most_flags": "С най-много доклади", + "search": "Търсене", + "enter_username": "Въведете потребителско име, което да потърсите", + "search-user-for-chat": "Потърсете потребител, с когото да започнете разговор", + "load_more": "Зареждане на още", + "users-found-search-took": "Намерени са %1 потребител(и)! Търсенето отне %2 секунди.", + "filter-by": "Филтриране", + "online-only": "Само тези на линия", + "invite": "Канене", + "prompt-email": "Е-пощи:", + "groups-to-join": "Групи, в които да се присъедини след приемане на поканата:", + "invitation-email-sent": "Беше изпратено е-писмо за потвърждение до %1", + "user_list": "Списък от потребители", + "recent_topics": "Скорошни теми", + "popular_topics": "Популярни теми", + "unread_topics": "Непрочетени теми", + "categories": "Категории", + "tags": "Етикети", + "no-users-found": "Няма намерени потребители!" +} \ No newline at end of file diff --git a/public/language/bn/_DO_NOT_EDIT_FILES_HERE.md b/public/language/bn/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/bn/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/bn/admin/admin.json b/public/language/bn/admin/admin.json new file mode 100644 index 0000000000..ec4e9b7013 --- /dev/null +++ b/public/language/bn/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "আপনি কি নিশ্চিত যে আপনি NodeBB রিবিল্ড এবং রিস্টার্ট করতে চান ? ", + "alert.confirm-restart": "আপনি কি নিশ্চিত যে আপনি NodeBB রিস্টার্ট করতে চান ?", + + "acp-title": "%1 | NodeBB এডমিন কন্ট্রোল প্যানেল", + "settings-header-contents": "কনটেন্টস", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/bn/admin/advanced/cache.json b/public/language/bn/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/bn/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/bn/admin/advanced/database.json b/public/language/bn/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/bn/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/bn/admin/advanced/errors.json b/public/language/bn/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/bn/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/bn/admin/advanced/events.json b/public/language/bn/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/bn/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/bn/admin/advanced/logs.json b/public/language/bn/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/bn/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/bn/admin/appearance/customise.json b/public/language/bn/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/bn/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/bn/admin/appearance/skins.json b/public/language/bn/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/bn/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/bn/admin/appearance/themes.json b/public/language/bn/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/bn/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/bn/admin/dashboard.json b/public/language/bn/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/bn/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/bn/admin/development/info.json b/public/language/bn/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/bn/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/bn/admin/development/logger.json b/public/language/bn/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/bn/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/bn/admin/extend/plugins.json b/public/language/bn/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/bn/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/bn/admin/extend/rewards.json b/public/language/bn/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/bn/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/bn/admin/extend/widgets.json b/public/language/bn/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/bn/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/admins-mods.json b/public/language/bn/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/bn/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/categories.json b/public/language/bn/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/bn/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/digest.json b/public/language/bn/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/bn/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/bn/admin/manage/groups.json b/public/language/bn/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/bn/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/privileges.json b/public/language/bn/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/bn/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/registration.json b/public/language/bn/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/bn/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/tags.json b/public/language/bn/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/bn/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/uploads.json b/public/language/bn/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/bn/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/bn/admin/manage/users.json b/public/language/bn/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/bn/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/bn/admin/menu.json b/public/language/bn/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/bn/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/advanced.json b/public/language/bn/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/bn/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/api.json b/public/language/bn/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/bn/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/chat.json b/public/language/bn/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/bn/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/cookies.json b/public/language/bn/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/bn/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/email.json b/public/language/bn/admin/settings/email.json new file mode 100644 index 0000000000..cacfb095b9 --- /dev/null +++ b/public/language/bn/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "ইমেইল সেটিংস", + "address": "ইমেইল অ্যাড্রেস", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "ইমেইল ডাইজেস্ট", + "subscriptions.disable": "ইমেইল ডাইজেস্ট নিষ্ক্রিয়", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/bn/admin/settings/general.json b/public/language/bn/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/bn/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/bn/admin/settings/group.json b/public/language/bn/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/bn/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/guest.json b/public/language/bn/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/bn/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/homepage.json b/public/language/bn/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/bn/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/languages.json b/public/language/bn/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/bn/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/navigation.json b/public/language/bn/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/bn/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/bn/admin/settings/notifications.json b/public/language/bn/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/bn/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/pagination.json b/public/language/bn/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/bn/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/post.json b/public/language/bn/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/bn/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/reputation.json b/public/language/bn/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/bn/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/social.json b/public/language/bn/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/bn/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/sockets.json b/public/language/bn/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/bn/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/sounds.json b/public/language/bn/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/bn/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/tags.json b/public/language/bn/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/bn/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/bn/admin/settings/uploads.json b/public/language/bn/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/bn/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/bn/admin/settings/user.json b/public/language/bn/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/bn/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/bn/admin/settings/web-crawler.json b/public/language/bn/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/bn/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/bn/category.json b/public/language/bn/category.json new file mode 100644 index 0000000000..cf9e1640f7 --- /dev/null +++ b/public/language/bn/category.json @@ -0,0 +1,23 @@ +{ + "category": "বিভাগ", + "subcategories": "উপবিভাগ", + "new_topic_button": "নতুন টপিক", + "guest-login-post": "উত্তর দিতে লগিন করুন", + "no_topics": "এই বিভাগে কোন আলোচনা নেই!
আপনি চাইলে নতুন আলোচনা শুরু করতে পারেন।", + "browsing": "ব্রাউজিং", + "no_replies": "কোন রিপ্লাই নেই", + "no_new_posts": "নতুন কোন পোস্ট নাই", + "watch": "নজর রাখুন", + "ignore": "উপেক্ষা করুন", + "watching": "দৃশ্যমান", + "not-watching": "দেখা হচ্ছে না", + "ignoring": "উপেক্ষারত", + "watching.description": "অপঠিত এবং সাম্প্রতিক বিষয়গুলো দেখাও", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "প্রেক্ষিত বিভাগসমূহ", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/bn/email.json b/public/language/bn/email.json new file mode 100644 index 0000000000..a8870a96bb --- /dev/null +++ b/public/language/bn/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "পরীক্ষামূলক ইমেইল", + "password-reset-requested": "নতুন পাসওয়ার্ডের জন্য অনুরোধ করা হয়েছে!", + "welcome-to": "%1 এ স্বাগতম", + "invite": "%1 থেকে আমন্ত্রণ", + "greeting_no_name": "স্বাগতম", + "greeting_with_name": "স্বাগতম %1", + "email.verify-your-email.subject": "দয়া করে ইমেইল যাচাই করুন", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "%1 এ নিবন্ধন করার জন্য আপনাকে ধন্যবাদ!", + "welcome.text2": "আপনার একাউন্ট এ্যাক্টিভেট করার জন্য, আপনি যে ইমেইল এড্রেস ব্যাবহার করে নিবন্ধন করেছেন তা যাচাই করতে হবে", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "আপনার ইমেইল এড্রেস নিশ্চিত করার জন্য এখানে ক্লিক করুন", + "invitation.text1": "%1 আপনাকে %2 তে যোগ দিতে আমন্ত্রণ জানিয়েছেন ", + "invitation.text2": "আপনার আমন্ত্রন পত্র %1 দিন পর বাতিল হয়ে যাবে", + "invitation.cta": "Click here to create your account.", + "reset.text1": "আমরা আপনার পাসওয়ার্ড রিসেট করার অনুরোধ পেয়েছি, সম্ভবত আপনি আপনার পাসওয়ার্ড ভুলে গিয়েছেন বলেই। তবে যদি তা না হয়ে থাকে, তাহলে এই মেইলকে উপেক্ষা করতে পারেন।", + "reset.text2": "পাসওয়ার্ড রিসেট করতে নিচের লিংকে ক্লিক করুন", + "reset.cta": "পাসওয়ার্ড রিসেট করতে এখানে ক্লিক করুন", + "reset.notify.subject": "পাসওয়ার্ড পরিবর্তন সফল হয়েছে", + "reset.notify.text1": "আপনাকে জানাচ্ছি যে %1 এ আপনার পাসওয়ার্ড পরিবর্তন হয়েছে", + "reset.notify.text2": "এটা আপনার অজান্তে হলে এখনই প্রশাসককে আবহিত করুন", + "digest.latest_topics": "%1 এর সর্বশেষ টপিকসমূহ", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "%1 ভিজিট করতে এখানে ক্লিক করুন", + "digest.unsub.info": "আপনার সাবস্ক্রীপশন সেটিংসের কারনে আপনাকে এই ডাইজেষ্টটি পাঠানো হয়েছে।", + "digest.day": "দিন", + "digest.week": "সপ্তাহ", + "digest.month": "মাস", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "%1 এর থেকে নতুন মেসেজ এসেছে।", + "notif.chat.cta": "কথপোকথন চালিয়ে যেতে এখানে ক্লিক করুন", + "notif.chat.unsub.info": "আপনার সাবস্ক্রীপশন সেটিংসের কারনে আপনার এই নোটিফিকেশন পাঠানো হয়েছে", + "notif.post.unsub.info": "আপনার সাবস্ক্রিপশন সেটিংসের কারনে আপনার এই বার্তাটি পাঠানো হয়েছে", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "আপনি সঠিকভাবে নোডবিবির জন্য মেইলার সেটাপ করেছেন কিনা নিশ্চিত করার জন্য এই টেষ্ট ইমেইল পাঠানো হয়েছে", + "unsub.cta": "সেটিংসগুলো পরিবর্তন করতে এখানে ক্লিক করুন", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "আপনি %1 এ নিষিদ্ধ হয়েছেন", + "banned.text1": "ব্যবহারকারি %1 %2 তে নিষিদ্ধ হয়েছেন", + "banned.text2": "This ban will last until %1.", + "banned.text3": "এই কারনে আপনি নিষিদ্ধ হয়েছেন :", + "closing": "ধন্যবাদ!" +} \ No newline at end of file diff --git a/public/language/bn/error.json b/public/language/bn/error.json new file mode 100644 index 0000000000..ff59e31fbe --- /dev/null +++ b/public/language/bn/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "ভুল তথ্য", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "আপনি লগিন করেননি", + "account-locked": "আপনার অ্যাকাউন্ট সাময়িকভাবে লক করা হয়েছে", + "search-requires-login": "Searching requires an account - please login or register.", + "goback": "Press back to return to the previous page", + "invalid-cid": "ভুল বিভাগ নাম্বার", + "invalid-tid": "ভুল টপিক নাম্বার", + "invalid-pid": "ভুল পোস্ট নাম্বার", + "invalid-uid": "ভুল ব্যবহারকারী নাম্বার", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "ভুল ইউজারনেম", + "invalid-email": "ভুল ইমেইল", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "ভুল ব্যবহারকারী তথ্য", + "invalid-password": "ভুল পাসওয়ার্ড", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "অনুগ্রহ পূর্বক ইউজারনেম এবং পাসওয়ার্ড উভয়ই প্রদান করুন", + "invalid-search-term": "অগ্রহনযোগ্য সার্চ টার্ম", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "ইউজারনেম আগেই ব্যবহৃত", + "email-taken": "ইমেইল আগেই ব্যবহৃত", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "খুব ছোট ইউজারনেম", + "username-too-long": "ইউজারনেম বড় হয়ে গিয়েছে", + "password-too-long": "Password too long", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "ব্যবহারকারী নিষিদ্ধ", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "বিভাগটি খুজে পাওয়া যায় নি", + "no-topic": "এই টপিক নেই", + "no-post": "এই পোষ্ট নেই", + "no-group": "এই গ্রুপ অস্তিত্বহীন", + "no-user": "এই নামে কোন সদস্য নেই", + "no-teaser": "টিজারটি খুজে পাওয়া যায় নি", + "no-flag": "Flag does not exist", + "no-privileges": "এই কাজটির জন্য আপনার পর্যাপ্ত অধিকার নেই", + "category-disabled": "বিভাগটি নিষ্ক্রিয়", + "topic-locked": "টপিক বন্ধ", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", + "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "আপলোড সম্পূর্ণ জন্য অনুগ্রহ করে অপেক্ষা করুন", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "আপনি অন্য এ্যাডমিনদের নিষিদ্ধ করতে পারেন না!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "গ্রুপের নাম খুব ছোট", + "group-name-too-long": "Group name too long", + "group-already-exists": "গ্রুপ ইতিমধ্যেই বিদ্যমান", + "group-name-change-not-allowed": "গ্রুপের নাম পরিবর্তনের অনুমতি নেই", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "এই পোস্টটি ইতিমধ্যে ডিলিট করা হয়ে গিয়েছে", + "post-already-restored": "এই পোষ্টটি ইতিমধ্যে পুনরোদ্ধার করা হয়েছে", + "topic-already-deleted": "এই টপিকটি ইতিমধ্যে ডিলিট করা হয়েছে", + "topic-already-restored": "এই টপিকটি ইতিমধ্যে পুনরোদ্ধার করা হয়েছে", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "টপিক থাম্বনেল নিষ্ক্রিয় করা। ", + "invalid-file": "ভুল ফাইল", + "uploads-are-disabled": "আপলোড নিষ্ক্রিয় করা", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "আপনি নিজের সাথে চ্যাট করতে পারবেন না!", + "chat-restricted": "এই সদস্য তার বার্তালাপ সংরক্ষিত রেখেছেন। এই সদস্য আপনাকে ফলো করার পরই কেবলমাত্র আপনি তার সাথে চ্যাট করতে পারবেন", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "সম্মাননা ব্যাবস্থা নিস্ক্রীয় রাখা হয়েছে", + "downvoting-disabled": "ঋণাত্মক ভোট নিস্ক্রীয় রাখা হয়েছে।", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "\"%1\" রিলোড করতে সমস্যা হয়েছে। রিলোডের পূর্বে যা করা হয়েছিল সেটি আনডু করা সমীচীন। ", + "registration-error": "নিবন্ধন এরর!", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/bn/flags.json b/public/language/bn/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/bn/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/bn/global.json b/public/language/bn/global.json new file mode 100644 index 0000000000..a38ab65901 --- /dev/null +++ b/public/language/bn/global.json @@ -0,0 +1,126 @@ +{ + "home": "নীড়পাতা", + "search": "অনুসন্ধান", + "buttons.close": "বন্ধ", + "403.title": "প্রবেশাধিকার প্রত্যাখ্যাত", + "403.message": "আপনি এমন জায়গাতে যেতে চাচ্ছেন যেখানে আপনার প্রবেশাধিকার নেই।", + "403.login": "সম্ভবত আপনার লগইন করা উচিত", + "404.title": "পাওয়া যায়নি", + "404.message": "আপনি এমন জায়গাতে যেতে চাচ্ছেন যার কোন অস্তিত্ব নাই। প্রথম পাতায় ফিরে যান ।", + "500.title": "Internal Error.", + "500.message": "ওহো! কিছু ভুল হয়েছে মনে হচ্ছে!", + "400.title": "ভুল ঠিকানা", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "নিবন্ধন", + "login": "প্রবেশ", + "please_log_in": "অনুগ্রহ করে প্রবেশ করুন", + "logout": "প্রস্থান", + "posting_restriction_info": "বর্তমানে নিবন্ধিত সদস্যরাই কেবল পোস্ট করতে পারেন, লগ ইন করতে এখানে ক্লিক করুন।", + "welcome_back": "আপনাকে স্বাগতম", + "you_have_successfully_logged_in": "আপনি সফলভাবে প্রবেশ করেছেন", + "save_changes": "পরিবর্তনগুলি সঞ্চয় করুন", + "save": "Save", + "close": "বন্ধ", + "pagination": "পাতা নং", + "pagination.out_of": "%2 এর মাঝে %1", + "pagination.enter_index": "Go to post index", + "header.admin": "অ্যাডমিন", + "header.categories": "বিভাগ", + "header.recent": "সাম্প্রতিক", + "header.unread": "অপঠিত", + "header.tags": "ট্যাগ", + "header.popular": "জনপ্রিয়", + "header.top": "Top", + "header.users": "ব্যবহারকারীগণ", + "header.groups": "Groups", + "header.chats": "কথোপকথন", + "header.notifications": "বিজ্ঞপ্তি", + "header.search": "অনুসন্ধান", + "header.profile": "প্রোফাইল", + "header.navigation": "Navigation", + "notifications.loading": "বিজ্ঞপ্তিগুলি লোড হচ্ছে", + "chats.loading": "কথোপকথনগুলি লোড হচ্ছে ", + "motd.welcome": "ভবিষ্যতের আলোচনার প্লাটফর্ম, NodeBB তে স্বাগতম।", + "previouspage": "আগের পাতা", + "nextpage": "পরের পাতা", + "alert.success": "সফল", + "alert.error": "ত্রুটি", + "alert.banned": "নিষিদ্ধ", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "আপনি আর %1 কে অনুসরণ করছেন না!", + "alert.follow": "আপনি এখন %1 কে অনুসরণ করছেন!", + "users": "ব্যবহারকারীগণ", + "topics": "টপিক", + "posts": "পোস্টগুলি", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "দেখেছেন", + "posters": "Posters", + "reputation": "সন্মাননা", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "আরো পড়ুন", + "more": "আরো...", + "none": "None", + "posted_ago_by_guest": "অতিথি পোস্ট করেছেন %1", + "posted_ago_by": " %1 %2 দ্বারা পোস্টকৃত", + "posted_ago": "পোস্ট করেছেন %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "%1 বিভাগে পোস্ট করা হয়েছে %2 আগে", + "posted_in_ago_by": "%3 %1 বিভাগে পোস্ট করেছেন %2", + "user_posted_ago": "%1 পোস্ট করেছেন %2", + "guest_posted_ago": "অতিথি পোস্ট করেছেন %1", + "last_edited_by": "last edited by %1", + "norecentposts": "কোনও সাম্প্রতিক পোস্ট নেই", + "norecenttopics": "কোনও সাম্প্রতিক টপিক নেই", + "recentposts": "সাম্প্রতিক পোস্ট", + "recentips": "সাম্প্রতিক প্রবেশকৃত আইপি সমুহ", + "moderator_tools": "Moderator Tools", + "online": "অনলাইন", + "away": "দূরে", + "dnd": "Do not disturb", + "invisible": "অদৃশ্য", + "offline": "অফলাইন", + "email": "ইমেইল", + "language": "ভাষা", + "guest": "অতিথি", + "guests": "অতিথি", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "ফোরাম আপডেট করা হয়েছে", + "updated.message": "এই ফোরামে এইমাত্র সর্বশেষ সংস্করণে আপডেট করা হয়েছে। পৃষ্ঠাটি রিফ্রেশ করতে এখানে ক্লিক করুন।", + "privacy": "নিরাপত্তা", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "সব মুছে ফেলুন", + "map": "ম্যাপ", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/bn/groups.json b/public/language/bn/groups.json new file mode 100644 index 0000000000..4fcb269410 --- /dev/null +++ b/public/language/bn/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "গ্রুপসমূহ", + "view_group": "গ্রুপ দেখুন", + "owner": "Group Owner", + "new_group": "Create New Group", + "no_groups_found": "There are no groups to see", + "pending.accept": "Accept", + "pending.reject": "Reject", + "pending.accept_all": "Accept All", + "pending.reject_all": "Reject All", + "pending.none": "There are no pending members at this time", + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Save", + "cover-saving": "Saving", + "details.title": "গ্রুপের বিস্তারিত", + "details.members": "সদস্য তালিকা", + "details.pending": "Pending Members", + "details.invited": "Invited Members", + "details.has_no_posts": "এই গ্রুপের সদস্যরা এখনো কোন পোষ্ট করেন নি", + "details.latest_posts": "সর্বশেষ পোষ্টসমূহ", + "details.private": "Private", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.member_count": "Member Count", + "details.creation_date": "Creation Date", + "details.description": "Description", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Badge Text", + "details.userTitleEnabled": "Show Badge", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Delete Group", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted", + "membership.accept-invitation": "Accept Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitation Pending", + "membership.join-group": "Join Group", + "membership.leave-group": "Leave Group", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Reject", + "new-group.group_name": "Group Name:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/bn/ip-blacklist.json b/public/language/bn/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/bn/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/bn/language.json b/public/language/bn/language.json new file mode 100644 index 0000000000..fedf38709f --- /dev/null +++ b/public/language/bn/language.json @@ -0,0 +1,5 @@ +{ + "name": "বাংলা", + "code": "bn", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/bn/login.json b/public/language/bn/login.json new file mode 100644 index 0000000000..b3d7c945d6 --- /dev/null +++ b/public/language/bn/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "ইউজারনেম / ইমেইল", + "username": "ইউজারনেম", + "remember_me": "মনে রাখুন", + "forgot_password": "পাসওয়ার্ড ভুলে গিয়েছেন?", + "alternative_logins": "বিকল্প প্রবেশ", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "আপনি সফলভাবে প্রবেশ করেছেন!", + "dont_have_account": "কোন একাউন্ট নেই?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/bn/modules.json b/public/language/bn/modules.json new file mode 100644 index 0000000000..199ce63048 --- /dev/null +++ b/public/language/bn/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "প্রেরন করুন", + "chat.no_active": "আপনার কোন সচল কথোপকথন নেই", + "chat.user_typing": "%1 লিখছেন", + "chat.user_has_messaged_you": "%1 আপনাকে বার্তা পাঠিয়েছেন", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "মেসেজ হিস্টোরী দেখতে প্রাপক নির্বাচন করুন", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "সাম্প্রতিক চ্যাটসমূহ", + "chat.contacts": "কন্টাক্টস", + "chat.message-history": "মেসেজ হিস্টোরী", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "চ্যাট উইন্ডো আলাদা করুন", + "chat.minimize": "Minimize", + "chat.maximize": "ম্যাক্সিমাইজ", + "chat.seven_days": "৭ দিন", + "chat.thirty_days": "৩০ দিন", + "chat.three_months": "৩ মাস", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 বলেছেন %2:", + "composer.user_said": "%1 বলেছেনঃ", + "composer.discard": "আপনি কি নিশ্চিত যে আপনি এই পোস্ট বাতিল করতে ইচ্ছুক?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/bn/notifications.json b/public/language/bn/notifications.json new file mode 100644 index 0000000000..545d7eeeea --- /dev/null +++ b/public/language/bn/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "বিজ্ঞপ্তিগুলো", + "no_notifs": "আপনার নতুন কোন বিজ্ঞপ্তি নেই", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "ফিরুন %1", + "outgoing_link": "বহির্গামী লিঙ্ক", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "%1 তে আগান", + "return_to": "%1 এ ফেরত যান", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "আপনার অপঠিত বিজ্ঞপ্তি আছে।", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "%1 থেকে নতুন বার্তা", + "upvoted_your_post_in": "%1 , %2 এ আপানার পোষ্টকে আপভোট করেছেন। ", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 একটি উত্তর দিয়েছেন: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 has posted a new topic: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 আপনাকে অনুসরন করা শুরু করেছেন।", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "ইমেইল নিশ্চিত করা হয়েছে", + "email-confirmed-message": "আপনার ইমেইল যাচাই করার জন্য আপনাকে ধন্যবাদ। আপনার অ্যাকাউন্টটি এখন সম্পূর্ণরূপে সক্রিয়।", + "email-confirm-error-message": "আপনার ইমেল ঠিকানার বৈধতা যাচাইয়ে একটি সমস্যা হয়েছে। সম্ভবত কোডটি ভুল ছিল অথবা কোডের মেয়াদ শেষ হয়ে গিয়েছে।", + "email-confirm-sent": "নিশ্চিতকরণ ইমেইল পাঠানো হয়েছে।", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/bn/pages.json b/public/language/bn/pages.json new file mode 100644 index 0000000000..bb4887abc8 --- /dev/null +++ b/public/language/bn/pages.json @@ -0,0 +1,65 @@ +{ + "home": "নীড়পাতা", + "unread": "অপঠিত টপিক", + "popular-day": "Popular topics today", + "popular-week": "Popular topics this week", + "popular-month": "Popular topics this month", + "popular-alltime": "All time popular topics", + "recent": "সাম্প্রতিক টপিক", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Online Users", + "users/latest": "Latest Users", + "users/sort-posts": "Users with the most posts", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "User Search", + "notifications": "বিজ্ঞপ্তি", + "tags": "ট্যাগসমূহ", + "tag": "Topics tagged under "%1"", + "register": "Register an account", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Reset your account password", + "categories": "বিভাগ", + "groups": "Groups", + "group": "%1 group", + "chats": "Chats", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "User Settings", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Confirmed", + "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.", + "maintenance.messageIntro": "Additionally, the administrator has left this message:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/bn/post-queue.json b/public/language/bn/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/bn/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/bn/recent.json b/public/language/bn/recent.json new file mode 100644 index 0000000000..720bab2407 --- /dev/null +++ b/public/language/bn/recent.json @@ -0,0 +1,19 @@ +{ + "title": "সাম্প্রতিক", + "day": "দিন", + "week": "সপ্তাহ", + "month": "মাস", + "year": "বছর", + "alltime": "সবসময় ", + "no_recent_topics": "কোন সাম্প্রতিক টপিক নেই। ", + "no_popular_topics": "There are no popular topics.", + "there-is-a-new-topic": "There is a new topic.", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", + "there-are-new-topics": "There are %1 new topics.", + "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", + "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", + "there-is-a-new-post": "There is a new post.", + "there-are-new-posts": "There are %1 new posts.", + "click-here-to-reload": "Click here to reload." +} \ No newline at end of file diff --git a/public/language/bn/register.json b/public/language/bn/register.json new file mode 100644 index 0000000000..a4216376a1 --- /dev/null +++ b/public/language/bn/register.json @@ -0,0 +1,32 @@ +{ + "register": "নিবন্ধন", + "cancel_registration": "নিবন্ধন বাতিল", + "help.email": "ডিফল্টভাবে, আপনার ইমেইল সর্বসাধারণ থেকে লুকানো থাকবে।", + "help.username_restrictions": "%1 এবং %2 অক্ষরের মাঝে একটি অনন্য সদস্য নাম। বাকিরা আপনাকে @নাম দিয়ে উল্লেখ করতে পারবেন।", + "help.minimum_password_length": "আপনার পাসওয়ার্ড এর দৈর্ঘ্য অন্তত %1 অক্ষরের হতে হবে।", + "email_address": "ইমেইল অ্যাড্রেস", + "email_address_placeholder": "ইমেইল অ্যাড্রেস লিখুন", + "username": "ইউজারনেম", + "username_placeholder": "ইউজারনেম লিখুন", + "password": "পাসওয়ার্ড", + "password_placeholder": "পাসওয়ার্ড লিখুন", + "confirm_password": "পাসওয়ার্ড নিশ্চিত করুন", + "confirm_password_placeholder": "পাসওয়ার্ড নিশ্চিত করুন", + "register_now_button": "নিবন্ধন করুন", + "alternative_registration": "বিকল্প নিবন্ধন", + "terms_of_use": "নিয়মাবলী", + "agree_to_terms_of_use": "আমি নিয়মাবলী মেনে চলতে সম্মতি জানালাম", + "terms_of_use_error": "আপনাকে অবশ্যই ব্যাবহার নীতিমালায় সম্মত হতে হবে।", + "registration-added-to-queue": "আপনার নিবন্ধনটি এ্যাপ্লুভাল তালিকায় যুক্ত হয়েছে। একজন এডমিনিস্ট্রেটর কর্তৃক নিবন্ধন গৃহীত হলে আপনি একটি মেইল পাবেন। ", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/bn/reset_password.json b/public/language/bn/reset_password.json new file mode 100644 index 0000000000..6d37d943a5 --- /dev/null +++ b/public/language/bn/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "পাসওয়ার্ড রিসেট", + "update_password": "পাসওয়ার্ড হালনাগাদ", + "password_changed.title": "পাসওয়ার্ড পরিবর্তন করা হয়েছে", + "password_changed.message": "

পাসওয়ার্ড সফলভাবে রিসেট করা হয়েছে, পুনরায় প্রবেশ করুন।", + "wrong_reset_code.title": "ভুল রিসেট কোড", + "wrong_reset_code.message": "প্রাপ্ত রিসেট কোডটি ভুল ছিল। আবার চেষ্টা করুন, অথবা একটি নতুন রিসেট কোড অনুরোধ করুন।", + "new_password": "নতুন পাসওয়ার্ড", + "repeat_password": "পাসওয়ার্ড নিশ্চিত করুন", + "changing_password": "Changing Password", + "enter_email": "অনুগ্রহপূর্বক আপনার ইমেইল এড্রেস প্রদান করুন, আমরা আপনাকে আপনার পাসওয়ার্ড রিসেট সম্পর্কিত তথ্যাবলী ইমেইলে পাঠিয়ে দিবো। ", + "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.", + "invalid_email": "ভুল ইমেইল / ইমেইল ডেটাবেইজে নেই", + "password_too_short": "The password entered is too short, please pick a different password.", + "passwords_do_not_match": "The two passwords you've entered do not match.", + "password_expired": "Your password has expired, please choose a new password" +} \ No newline at end of file diff --git a/public/language/bn/search.json b/public/language/bn/search.json new file mode 100644 index 0000000000..3aa5575299 --- /dev/null +++ b/public/language/bn/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "\"%2\" এর সাথে মিলিয়ে %1 ফলাফল পাওয়া গেছে, ( %3 seconds সময় লেগেছে )", + "no-matches": "কোন মিল খুঁজে পাওয়া যায় নি", + "advanced-search": "এডভান্সড সার্চ", + "in": "এর মধ্যে", + "titles": "টাইটেলস", + "titles-posts": "টাইটেল এবং পোস্ট সমূহ", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "পোষ্ট করেছেন", + "in-categories": "বিভাগের ভিতরে", + "search-child-categories": "উপবিভাগের ভিতরে", + "has-tags": "Has tags", + "reply-count": "রিপ্লাই কাউন্ট", + "at-least": "কমপক্ষে", + "at-most": "সর্বোচ্চ", + "relevance": "Relevance", + "post-time": "পোস্টের সময়", + "votes": "Votes", + "newer-than": "Newer than", + "older-than": "Older than", + "any-date": "যেকোন তারিখ", + "yesterday": "গতকাল", + "one-week": "এক সপ্তাহ", + "two-weeks": "দুই সপ্তাহ", + "one-month": "এক মাস", + "three-months": "তিন মাস", + "six-months": "ছয় মাস", + "one-year": "এক বছর", + "sort-by": "সাজানোর ভিত্তি", + "last-reply-time": "সর্বশেষ রিপ্লাইয়ের সময়", + "topic-title": "টপিকের টাইটেল", + "topic-votes": "Topic votes", + "number-of-replies": "রিপ্লাইয়ের সংখ্যা", + "number-of-views": "সর্বমোট ভিউ", + "topic-start-date": "টপিক শুরুর তারিখ", + "username": "ইউজারনেম", + "category": "বিভাগ", + "descending": "বড় থেকে ছোট অর্ডারে", + "ascending": "ছোট থেকে বড় অর্ডারে", + "save-preferences": "প্রেফারেন্স সেভ", + "clear-preferences": "Clear preferences", + "search-preferences-saved": "Search preferences saved", + "search-preferences-cleared": "Search preferences cleared", + "show-results-as": "ফলাফল দেখানো হোক : ", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/bn/success.json b/public/language/bn/success.json new file mode 100644 index 0000000000..7e8f468f6a --- /dev/null +++ b/public/language/bn/success.json @@ -0,0 +1,7 @@ +{ + "success": "সফল হয়েছে", + "topic-post": "আপনি সফলভাবে পোষ্ট করেছেন। ", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "অথেন্টিকেশন সফল হয়েছে", + "settings-saved": "সেটিংস সেভ করা হয়েছে। " +} \ No newline at end of file diff --git a/public/language/bn/tags.json b/public/language/bn/tags.json new file mode 100644 index 0000000000..0813383049 --- /dev/null +++ b/public/language/bn/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "এই ট্যাগ সম্বলিত কোন টপিক নেই", + "tags": "ট্যাগসমূহ", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "ট্যাগ বসান", + "no_tags": "এখন পর্যন্ত কোন ট্যাগ নেই", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/bn/top.json b/public/language/bn/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/bn/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/bn/topic.json b/public/language/bn/topic.json new file mode 100644 index 0000000000..504cebca23 --- /dev/null +++ b/public/language/bn/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "টপিক", + "title": "Title", + "no_topics_found": "কোন টপিক পাওয়া যায়নি!", + "no_posts_found": "কোন পোস্ট পাওয়া যায়নি", + "post_is_deleted": "এই পোস্টটি মুছে ফেলা হয়েছে!", + "topic_is_deleted": "This topic is deleted!", + "profile": "প্রোফাইল ", + "posted_by": "পোস্ট করেছেন %1", + "posted_by_guest": "অতিথি পোস্ট ", + "chat": "আলাপচারি", + "notify_me": "এই টপিকে নতুন উত্তর আসলে জানুন", + "quote": "উদ্ধৃতি", + "reply": "উত্তর", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log in to reply", + "login-to-view": "🔒 Log in to view", + "edit": "সম্পাদণা", + "delete": "মুছে ফেলুন", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "পার্জ", + "restore": "পুনরূদ্ধার", + "move": "সরানো", + "change-owner": "Change Owner", + "fork": "শাখা", + "link": "লিঙ্ক", + "share": "শেয়ার", + "tools": "টুলস", + "locked": "বন্ধ", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "এই টপিকটি মুছে ফেলা হয়েছে। শুধুমাত্র টপিক ব্যবস্থাপনার ক্ষমতাপ্রাপ্ত সদস্যগণ এটি দেখতে পারবেন।", + "following_topic.message": "এখন থেকে এই টপিকে অন্যকেউ পোস্ট করলে আপনি নোটিফিকেশন পাবেন।", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "এই টপিকে সাবস্ক্রাইব করতে চাইলে অনুগ্রহ করে নিবন্ধণ করুন অথবা প্রবেশ করুন।", + "markAsUnreadForAll.success": "টপিকটি সবার জন্য অপঠিত হিসাবে মার্ক করুন।", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "দেখা", + "unwatch": "অদেখা", + "watch.title": "এই টপিকে নতুন উত্তর এলে বিজ্ঞাপণের মাধ্যমে জানুন।", + "unwatch.title": "এই টপিক দেখা বন্ধ করুন", + "share_this_post": "এই পোষ্টটি শেয়ার করুন", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "টপিক সম্পর্কিত টুলস", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "টপিক পিন করুন", + "thread_tools.unpin": "টপিক আনপিন করুন", + "thread_tools.lock": "টপিক বন্ধ করুন", + "thread_tools.unlock": "টপিক খুলে দিন", + "thread_tools.move": "টপিক সরান", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "সমস্ত টপিক সরান", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "টপিক ফর্ক করুন", + "thread_tools.delete": "টপিক মুছে ফেলুন", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "আপনি নিশ্চিত যে আপনি এই টপিকটি মুছে ফেলতে চান?", + "thread_tools.restore": "টপিক পুনরূদ্ধার করুন", + "thread_tools.restore_confirm": "আপনি নিশ্চিত যে আপনি টপিকটি পুনরূদ্ধার করতে চান?", + "thread_tools.purge": "টপিক পার্জ করুন", + "thread_tools.purge_confirm": "আপনি নিশ্চিত যে আপনি টপিকটি পার্জ করতে চাচ্ছেন ? ", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "আপনি নিশ্চিত যে আপনি এই পোষ্টটি মুছে ফেলতে চান ?", + "post_restore_confirm": "আপনি নিশ্চিত যে আপনি এই পোষ্টটি পুনরূূদ্ধার করতে চান ? ", + "post_purge_confirm": "আপনি নিশ্চিত যে আপনি এই পোষ্টটি পার্জ করতে চান ? ", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "ক্যাটাগরী লোড করা হচ্ছে", + "confirm_move": "সরান", + "confirm_fork": "ফর্ক", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "আরো পোষ্ট লোড করা হচ্ছে", + "move_topic": "টপিক সরান", + "move_topics": "টপিক সমূহ সরান", + "move_post": "পোষ্ট সরান", + "post_moved": "পোষ্ট সরানো হয়েছে", + "fork_topic": "টপিক ফর্ক করুন", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "যে পোষ্টটি ফর্ক করতে চান সেটি ক্লিক করুন", + "fork_no_pids": "কোন পোষ্ট সিলেক্ট করা হয় নি", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "টপিক ফর্ক করা হয়েছে। ফর্ক করা টপিকে যেতে এখানে ক্লিক করুন", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "আপনার টপিকের শিরোনাম দিন", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "বাতিল", + "composer.submit": "সাবমিট", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "%1 এর উত্তরে:", + "composer.new_topic": "নতুন টপিক", + "composer.editing": "Editing", + "composer.uploading": "আপলোডিং", + "composer.thumb_url_label": "টপিকে থাম্বনেইল URL পেষ্ট করুন", + "composer.thumb_title": "এই টপিকে থাম্বনেইল যোগ করুন", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "অথবা একটি ফাইল আপলোড করুন", + "composer.thumb_remove": "ফিল্ড ক্লিয়ার করুন", + "composer.drag_and_drop_images": "ছবি এখানে ড্র্যাগ করে এনে ছেড়ে দিন", + "more_users_and_guests": "%1 more user(s) and %2 guest(s)", + "more_users": "%1 more user(s)", + "more_guests": "%1 more guest(s)", + "users_and_others": "%1 and %2 others", + "sort_by": "সাজানোর ভিত্তি:", + "oldest_to_newest": "পুরাতন থেকে নতুন", + "newest_to_oldest": "নতুন থেকে পুরাতন", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/bn/unread.json b/public/language/bn/unread.json new file mode 100644 index 0000000000..96d8b7b4c1 --- /dev/null +++ b/public/language/bn/unread.json @@ -0,0 +1,15 @@ +{ + "title": "অপঠিত", + "no_unread_topics": "কোন অপঠিত টপিক নেই", + "load_more": "আরো লোড করুন", + "mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন", + "selected": "নির্বাচিত", + "all": "সবগুলো", + "all_categories": "All categories", + "topics_marked_as_read.success": "পঠিত হিসেবে চিহ্নিত টপিকসমূহ", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/bn/uploads.json b/public/language/bn/uploads.json new file mode 100644 index 0000000000..b0906e20fc --- /dev/null +++ b/public/language/bn/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "ফাইল পাঠানো হচ্ছে...", + "select-file-to-upload": "পাঠানোর জন্য নথি নির্বাচন", + "upload-success": "সফলভাবে ফাইল দেওয়া হয়েছে!", + "maximum-file-size": "সর্বোচ্চ %1 কিবিট", + "no-uploads-found": "কোনো আপলোড নেই", + "public-uploads-info": "সব আপলোড গণ দৃশ্যমান, সব দর্শক তা দেখতে পারবে।", + "private-uploads-info": "সব আপলোড ব্যক্তিগত, কেবল প্রবেশরত ব্যবহারকারী তা দেখতে পারবে।" +} \ No newline at end of file diff --git a/public/language/bn/user.json b/public/language/bn/user.json new file mode 100644 index 0000000000..3e07785645 --- /dev/null +++ b/public/language/bn/user.json @@ -0,0 +1,199 @@ +{ + "banned": "নিষিদ্ধ", + "muted": "Muted", + "offline": "অফলাইন", + "deleted": "Deleted", + "username": "সদস্যের নাম", + "joindate": "নিবন্ধন তারিখ", + "postcount": "সর্বমোট পোষ্ট", + "email": "ইমেইল", + "confirm_email": "ইমেইল নিশ্চিত করুন", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "একাউন্ট নিষিদ্ধ করুন", + "ban_account_confirm": "আপনি কি নিশ্চিত যে এই সদস্যকে নিষিদ্ধ করতে চান ?", + "unban_account": "নিষেদ্ধাজ্ঞা তুলে নিন", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "একাউন্ট মুছে ফেলুন", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "একাউন্ট মুছে ফেলা হয়েছে", + "account-content-deleted": "Account content deleted", + "fullname": "পুর্ণ নাম", + "website": "ওয়েবসাইট", + "location": "স্থান", + "age": "বয়স", + "joined": "যোগদান করেছেন", + "lastonline": "সর্বশেষ অনলাইনে ছিলেন", + "profile": "প্রোফাইল", + "profile_views": "প্রোফাইল দেখেছেন", + "reputation": "সন্মাননা", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "দেখা হয়েছে", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "যাদের অনুসরণ করছেন", + "following": "যারা আপনাকে অনুসরণ করছে", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "আমার সম্পর্কে: ", + "signature": "স্বাক্ষর", + "birthday": "জন্মদিন", + "chat": "বার্তালাপ", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "অনুসরন করুন", + "unfollow": "অনুসরন করা থেকে বিরত থাকুন", + "more": "আরো...", + "profile_update_success": "প্রোফাইল আপডেট সফল হয়েছে", + "change_picture": "ছবি পরিবর্তন", + "change_username": "ইউজারনেম পরিবর্তন করুন", + "change_email": "ইমেইল পরিবর্তন করুন", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "সম্পাদনা", + "edit-profile": "Edit Profile", + "default_picture": "ডিফল্ট আইকন", + "uploaded_picture": "ছবি আপলোড করুন", + "upload_new_picture": "নতুন ছবি আপলোড করুন", + "upload_new_picture_from_url": "URL থেকে নতুন ছবি আপলোড করুন", + "current_password": "বর্তমান পাসওয়ার্ড", + "change_password": "পাসওয়ার্ড পরিবর্তন", + "change_password_error": "অগ্রহনযোগ্য পাসওয়ার্ড", + "change_password_error_wrong_current": "আপনার পাসওয়ার্ড সঠিক নয়", + "change_password_error_match": "পাসওয়ার্ড অবশ্যই একই হতে হবে", + "change_password_error_privileges": "আপনার পাসওয়ার্ড পরিবর্তন করার অনুমতি নেই", + "change_password_success": "আপনার পাসওয়ার্ড আপডেট করা হয়েছে", + "confirm_password": "পাসওয়ার্ড নিশ্চিত করুন", + "password": "পাসওয়ার্ড", + "username_taken_workaround": "আপনি যে ইউজারনেম চাচ্ছিলেন সেটি ইতিমধ্যে নেয়া হয়ে গেছে, কাজেই আমরা এটি কিঞ্চিং পরিবর্তন করেছি। আপনি এখন %1 হিসেবে পরিচিত", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "ছবি আপলোড করুন", + "upload_a_picture": "ছবি (একটি) আপলোড করুন", + "remove_uploaded_picture": "আপলোড করা ছবিটি সরিয়ে নাও", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "সেটিংস", + "show_email": "আমার ইমেইল দেখাও", + "show_fullname": "আমার সম্পূর্ণ নাম দেখাও", + "restrict_chats": "আমি যাদের ফলো করি কেবলমাত্র তাদের থেকে বার্তা গ্রহন করা হোক", + "digest_label": "ডাইজেষ্টে সাবস্ক্রাইব করুন", + "digest_description": "শিডিউল অনূযায়ী এই ফোরামের ইমেইল আপডেটের জন্য সাবস্ক্রাইব করুন (নতুন নোটিফিকেশন এবং টপিকসমূহ )", + "digest_off": "বন্ধ", + "digest_daily": "দৈনিক", + "digest_weekly": "সাপ্তাহিক", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "মাসিক", + "has_no_follower": "এই সদস্যের কোন ফলোয়ার নেই :(", + "follows_no_one": "এই সদস্য কাউকে ফলো করছেন না :(", + "has_no_posts": "এই সদস্য এখন পর্যন্ত কোন পোস্ট করেন নি", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "এই সদস্য এখনো কোন টপিক করেন নি", + "has_no_watched_topics": "এই সদস্য এখনো কোন টপিক দেখেন নি", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "ইমেইল গোপন রাখা হয়েছে", + "hidden": "গোপন করা হয়েছে", + "paginate_description": "ইনফাইনাইট স্ক্রলের বদলে টপিক ও পোস্টের জন্য পেজিনেশন ব্যাবহার করা হোক", + "topics_per_page": "প্রতি পেজে কতগুলো টপিক থাকবে", + "posts_per_page": "প্রতি পেইজে কতগুলো পোষ্ট থাকবে", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Browsing সেটিংস", + "open_links_in_new_tab": "আউটগোয়িং লিংকগুলো নতুন ট্যাবে খুলুন", + "enable_topic_searching": "In-Topic সার্চ সক্রীয় করো", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + "select-skin": "Select a Skin", + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/bn/users.json b/public/language/bn/users.json new file mode 100644 index 0000000000..98fe38b601 --- /dev/null +++ b/public/language/bn/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "সর্বশেষ নিবন্ধিত সদস্যরা:", + "top_posters": "সর্বোচ্চ পোষ্টকারী", + "most_reputation": "সর্বোচ্চ সম্মাননাধারী", + "most_flags": "সর্বোচ্চ অভিযোগ", + "search": "খুঁজুন", + "enter_username": "ইউজারনেম এর ভিত্তিতে সার্চ করুন", + "search-user-for-chat": "Search a user to start chat", + "load_more": "আরো লোড করুন", + "users-found-search-took": "%1 জন সদস্য(দের) খুঁজে পাওয়া গেছে। খুঁজতে সময় লেগেছে %2 সেকেন্ড ", + "filter-by": "ফিল্টার করার ধরন", + "online-only": "শুধুমাত্র অনলাইন", + "invite": "ইনভাইট", + "prompt-email": "ইমেইল", + "groups-to-join": "সদস্য অনুরোধ স্বীকৃত হলে যেসব সম্প্রদায়ে যোগ দিতে হবে", + "invitation-email-sent": "%1 কে একটি ইনভাইটেশন ইমেইল পাঠানো হয়েছে", + "user_list": "সদস্য তালিকা", + "recent_topics": "সাম্প্রতিক টপিক", + "popular_topics": "জনপ্রিয় টপিক", + "unread_topics": "অপঠিত টপিক", + "categories": "বিভাগ", + "tags": "ট্যাগসমূহ", + "no-users-found": "কোন সদস্য পাওয়া যায় নি" +} \ No newline at end of file diff --git a/public/language/cs/_DO_NOT_EDIT_FILES_HERE.md b/public/language/cs/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/cs/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/cs/admin/admin.json b/public/language/cs/admin/admin.json new file mode 100644 index 0000000000..bea7eb5dcc --- /dev/null +++ b/public/language/cs/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Jste si jist/a, že chcete znovu sestavit a restartovat NodeBB?", + "alert.confirm-restart": "Jste si jist/a, že si přejete restartovat NodeBB?", + + "acp-title": "Ovládací panel správce NodeBB | %1", + "settings-header-contents": "Obsah", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/cs/admin/advanced/cache.json b/public/language/cs/admin/advanced/cache.json new file mode 100644 index 0000000000..193dd5c2f0 --- /dev/null +++ b/public/language/cs/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Mezipaměť příspěvku", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% plný", + "post-cache-size": "Velikost mezipaměti příspěvku", + "items-in-cache": "Položek v mezipaměti" +} \ No newline at end of file diff --git a/public/language/cs/admin/advanced/database.json b/public/language/cs/admin/advanced/database.json new file mode 100644 index 0000000000..556e201dd7 --- /dev/null +++ b/public/language/cs/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Doba provozu v sekundách", + "uptime-days": "Doba provozu ve dnech", + + "mongo": "Mongo", + "mongo.version": "Verze MongoDB", + "mongo.storage-engine": "Modul úložiště", + "mongo.collections": "Fondy", + "mongo.objects": "Objekty", + "mongo.avg-object-size": "Průměrná velikost objeku", + "mongo.data-size": "Velikost dat", + "mongo.storage-size": "Velikost úložiště", + "mongo.index-size": "Velikost indexu", + "mongo.file-size": "Velikost souboru", + "mongo.resident-memory": "Residentní paměť", + "mongo.virtual-memory": "Virtuální paměť", + "mongo.mapped-memory": "Namapovaná paměť", + "mongo.bytes-in": "Bajtů ->", + "mongo.bytes-out": "Bajtů <-", + "mongo.num-requests": "Počet požadavků", + "mongo.raw-info": "Raw informace MongoDB", + "mongo.unauthorized": "NodeBB se nepodařilo odeslat dotaz na databázi MongoDB pro relevantní statistiky. Ujistěte se, že uživatel používající NodeBB obsahuje roly \"clusterMonitor\" pro „admin“ databáze.", + + "redis": "Redis", + "redis.version": "Verze Redis", + "redis.keys": "Klíče", + "redis.expires": "Platnost", + "redis.avg-ttl": "Průměrné TTL", + "redis.connected-clients": "Připojených klientů", + "redis.connected-slaves": "Druhotná připojení", + "redis.blocked-clients": "Blokovaných klientů", + "redis.used-memory": "Využitá paměť", + "redis.memory-frag-ratio": "Poměr fragmentace paměti", + "redis.total-connections-recieved": "Souhrné množství připojení", + "redis.total-commands-processed": "Souhrnně zpracováno příkazů", + "redis.iops": "Okamžité zpracování za sekundu", + "redis.iinput": "Okamžité vstupy/s", + "redis.ioutput": "Okamžité výstupy/s", + "redis.total-input": "Celkové vstupy", + "redis.total-output": "Celkové výstupy", + + "redis.keyspace-hits": "Zpracováno klíčů", + "redis.keyspace-misses": "Chyby klíče", + "redis.raw-info": "Informace Redis Raw", + + "postgres": "Postgres", + "postgres.version": "Verze PostgreSQL", + "postgres.raw-info": "Informace o Postgres" +} diff --git a/public/language/cs/admin/advanced/errors.json b/public/language/cs/admin/advanced/errors.json new file mode 100644 index 0000000000..55b40cba32 --- /dev/null +++ b/public/language/cs/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Vyřešit %1", + "error-events-per-day": "%1 událostí za den", + "error.404": "Chyba 404 – nenalezeno", + "error.503": "Chyba 503 – nedostupná služba", + "manage-error-log": "Spravovat protokol s chybami", + "export-error-log": "Exportovat protokol s chybami (CSV)", + "clear-error-log": "Smazat protokol s chybami", + "route": "Cesta", + "count": "Počet", + "no-routes-not-found": "Huráá. Žádná chyba 404.", + "clear404-confirm": "Jste si jist/a, že si přejete smazat protokol s chybami 404?", + "clear404-success": "Chyby „404 – nenalezeno” byly smazány" +} \ No newline at end of file diff --git a/public/language/cs/admin/advanced/events.json b/public/language/cs/admin/advanced/events.json new file mode 100644 index 0000000000..327b32d858 --- /dev/null +++ b/public/language/cs/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Události", + "no-events": "Žádné nové události", + "control-panel": "Ovládací panel událostí", + "delete-events": "Odstranit události", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filtry", + "filters-apply": "Použít filtry", + "filter-type": "Typ události", + "filter-start": "Datum začátku", + "filter-end": "Datum konce", + "filter-perPage": "Na stránku" +} \ No newline at end of file diff --git a/public/language/cs/admin/advanced/logs.json b/public/language/cs/admin/advanced/logs.json new file mode 100644 index 0000000000..9fde829b2b --- /dev/null +++ b/public/language/cs/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Protokoly", + "control-panel": "Ovládací panel protokolů", + "reload": "Znovu načíst protokoly", + "clear": "Smazat protokoly", + "clear-success": "Protokoly smazány." +} \ No newline at end of file diff --git a/public/language/cs/admin/appearance/customise.json b/public/language/cs/admin/appearance/customise.json new file mode 100644 index 0000000000..9f8ac1ede3 --- /dev/null +++ b/public/language/cs/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Uživatelský CSS/LESS", + "custom-css.description": "Zadejte vlastní definici CSS/LESS, která bude nadřazená ostatním stylům.", + "custom-css.enable": "Povolit uživatelský CSS/LESS", + + "custom-js": "Uživatelský Javascript", + "custom-js.description": "Zadejte zde váš javascriptový kód. Bude spuštěn, jakmile se stránka plně načte.", + "custom-js.enable": "Povolit uživatelský Javascript", + + "custom-header": "Uživatelská hlavička", + "custom-header.description": "Zde zadejte vlastní HTML (mimo meta značek, atp.), který bude přidán do části ☺<head> vašeho označení fóra. Skriptovací značky jsou povoleny, ale nedoporučujeme to, jelikož je pro to vhodná záložka Uživatelský Javascript.", + "custom-header.enable": "Povolit uživatelskou hlavičku", + + "custom-css.livereload": "Povolit aktuální znovu načtení", + "custom-css.livereload.description": "Povolením si vynutíte, aby všechny relace na každém zařízení pod vaším účtem se kdykoliv obnovili při kliknutí na tlačítko „Uložit”." +} \ No newline at end of file diff --git a/public/language/cs/admin/appearance/skins.json b/public/language/cs/admin/appearance/skins.json new file mode 100644 index 0000000000..aed0b80825 --- /dev/null +++ b/public/language/cs/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Načítání motivů…", + "homepage": "Domovská stránka", + "select-skin": "Vyberte motiv", + "current-skin": "Současný motiv", + "skin-updated": "Motiv aktualizován", + "applied-success": "Motiv %1 byl úspěšně použit", + "revert-success": "Barvy u motivu navráceny na základní" +} \ No newline at end of file diff --git a/public/language/cs/admin/appearance/themes.json b/public/language/cs/admin/appearance/themes.json new file mode 100644 index 0000000000..90bc7fc22b --- /dev/null +++ b/public/language/cs/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Vyhledávání nainstalovaných motivů…", + "homepage": "Domovská stránka", + "select-theme": "Vybrat motiv", + "current-theme": "Aktuální motiv", + "no-themes": "Žádný nainstalovaný motiv nebyl nalezen", + "revert-confirm": "Jste si jist/a, že chcete obnovit výchozí motiv NodeBB?", + "theme-changed": "Motiv byl změněn", + "revert-success": "Úspěšně jste vrátil/a NodeBB na výchozí motiv", + "restart-to-activate": "Pro úplné aktivování tohoto tématu, znovu sestavte a restartujte NodeBB." +} \ No newline at end of file diff --git a/public/language/cs/admin/dashboard.json b/public/language/cs/admin/dashboard.json new file mode 100644 index 0000000000..2ca82e2cce --- /dev/null +++ b/public/language/cs/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Provoz fóra", + "page-views": "Zobrazení stránky", + "unique-visitors": "Jedineční návštěvníci", + "logins": "Logins", + "new-users": "Nový uživatelé", + "posts": "Příspěvky", + "topics": "Témata", + "page-views-seven": "Posledních 7 dnů", + "page-views-thirty": "Posledních 30 dní", + "page-views-last-day": "Posledních 24 hodin", + "page-views-custom": "Dle rozsahu data", + "page-views-custom-start": "Začátek rozsahu", + "page-views-custom-end": "Konec rozsahu", + "page-views-custom-help": "Zadejte rozsah data zobrazení stránek, které chcete vidět. Není-li datum nastaveno, výchozí formát je YYYY-MM-DD", + "page-views-custom-error": "Zadejte správný rozsah ve formátu YYYY-MM-DD", + + "stats.yesterday": "Včera", + "stats.today": "Dnes", + "stats.last-week": "Poslední týden", + "stats.this-week": "Tento víkend", + "stats.last-month": "Poslední měsíc", + "stats.this-month": "Tento měsíc", + "stats.all": "Všechny časy", + + "updates": "Aktualizace", + "running-version": "Fungujete na NodeBB v%1.", + "keep-updated": "Vždy udržujte NodeBB aktuální kvůli bezpečnostním záplatám a opravám.", + "up-to-date": "

Máte aktuální verzi

", + "upgrade-available": "

Nová verze (v%1) byla zveřejněna. Zvažte aktualizaci vašeho NodeBB.

", + "prerelease-upgrade-available": "

Toto je zastaralá testovací verze NodeBB. Nová verze (v%1) byla zveřejněna. Zvažte aktualizaci vaší verze NodeBB.

", + "prerelease-warning": "

Toto je zkušební verze NodeBB. Mohou se vyskytnout různé chyby.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Fórum běží ve vývojářském režimu a může být potencionálně zranitelné . Kontaktujte správce systému.", + "latest-lookup-failed": "

Náhled na poslední dostupnou verzi NodeBB

", + + "notices": "Oznámení", + "restart-not-required": "Restart není potřeba", + "restart-required": "Je potřeba restartovat", + "search-plugin-installed": "Rozšíření pro hledání je nainstalováno", + "search-plugin-not-installed": "Rozšíření pro hledání není nainstalováno", + "search-plugin-tooltip": "Pro aktivování funkce vyhledávání, nainstalujte rozšíření pro hledání ze stránky rozšíření.", + + "control-panel": "Ovládání systému", + "rebuild-and-restart": "Znovu sestavit a restartovat", + "restart": "Restartovat", + "restart-warning": "Znovu sestavení nebo restartování NodeBB odpojí všechna existující připojení na několik vteřin.", + "restart-disabled": "Znovu sestavení a restartování vašeho NodeBB bylo zakázáno, protože se nezdá, že byste byl/a připojena přes příslušného „daemona”.", + "maintenance-mode": "Režim údržby", + "maintenance-mode-title": "Pro nastavení režimu údržby NodeBB, klikněte zde", + "realtime-chart-updates": "Aktualizace grafů v reálném čase", + + "active-users": "Aktivní uživatelé", + "active-users.users": "Uživatelé", + "active-users.guests": "Hosté", + "active-users.total": "Celkově", + "active-users.connections": "Připojení", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registrovaní", + + "user-presence": "Výskyt uživatele", + "on-categories": "V seznamu kategorii", + "reading-posts": "Čtení příspěvku", + "browsing-topics": "Prohlížení témat", + "recent": "Poslední", + "unread": "Nepřečtené", + + "high-presence-topics": "Témata s vysokou účastí", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Zobrazení stránky", + "graphs.page-views-registered": "Zobrazených stránek/registrovaní", + "graphs.page-views-guest": "Zobrazených stránek/hosté", + "graphs.page-views-bot": "Zobrazených stránek/bot", + "graphs.unique-visitors": "Jedineční návštěvníci", + "graphs.registered-users": "Registrovaní uživatelé", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Poslední restart od", + "no-users-browsing": "Nikdo si nic neprohlíží", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/cs/admin/development/info.json b/public/language/cs/admin/development/info.json new file mode 100644 index 0000000000..c47e061cb2 --- /dev/null +++ b/public/language/cs/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Jste u %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 vazeb odpovědělo během %2ms.", + "host": "host", + "primary": "primární / spuštěné úlohy", + "pid": "pid", + "nodejs": "nodejs", + "online": "připojen", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "zatížení systému", + "cpu-usage": "využití CPU", + "uptime": "čas spuštění", + + "registered": "Registrován", + "sockets": "Sockety", + "guests": "Hosté", + + "info": "Informace" +} \ No newline at end of file diff --git a/public/language/cs/admin/development/logger.json b/public/language/cs/admin/development/logger.json new file mode 100644 index 0000000000..688358d8d3 --- /dev/null +++ b/public/language/cs/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Nastavení protokolování", + "description": "Povolením zaškrtávacích polí, budete dostávat protokoly na váš terminál. Nastavíte-li cestu, protokoly budou místo toho uloženy do souboru. Protokolování HTTP je vhodné pro vytvoření statistiky o tom, kdo, kdy a jací lidé přistupují k vašemu fóru. Dodatečně k těmto protokolům můžeme zapisovat i události z socket.io. Protokolování socket.io v kombinaci s monitorem redis-cli je vhodné k porozumění vnitřním strukturám NodeBB.", + "explanation": "Jednoduše zaškrtněte/odškrtněte nastavení protokolu, změny se projeví okamžitě bez restartování.", + "enable-http": "Povolit protokolování HTTP", + "enable-socket": "Povolit protokolování socket.io", + "file-path": "Cesta k protokolovému souboru", + "file-path-placeholder": "/path/to/log/file.log ::: zanechte prázdné pro protokolování na vašem terminále", + + "control-panel": "Ovládací panel protokolování", + "update-settings": "Aktualizovat nastavení protokolů" +} \ No newline at end of file diff --git a/public/language/cs/admin/extend/plugins.json b/public/language/cs/admin/extend/plugins.json new file mode 100644 index 0000000000..6c9810c1a2 --- /dev/null +++ b/public/language/cs/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Nainstalováno", + "active": "Aktivní", + "inactive": "Neaktivní", + "out-of-date": "Zastaralé", + "none-found": "Nebyly nalezeny žádná rozšíření", + "none-active": "Žádné aktivní rozšíření", + "find-plugins": "Najít rozšíření", + + "plugin-search": "Hledat rozšíření", + "plugin-search-placeholder": "Hledat rozšíření…", + "submit-anonymous-usage": "Odeslat anonymní data o využití zásuvného modulu.", + "reorder-plugins": "Roztřídit rozšíření", + "order-active": "Pořadí aktivních rozšíření", + "dev-interested": "Zajímá vás psaní rozšíření pro NodeBB?", + "docs-info": "Plná dokumentace ohledně autorizace rozšíření je k nalezení na Portále dokumentů NodeBB.", + + "order.description": "Některá rozšíření fungují správně až jsou-li inicializovány před/po ostatních rozšířeních.", + "order.explanation": "Rozšíření jsou načteny dle pořadí zde určeném, odshora dolů", + + "plugin-item.themes": "Motivy", + "plugin-item.deactivate": "Deaktivovat", + "plugin-item.activate": "Aktivovat", + "plugin-item.install": "Nainstalovat", + "plugin-item.uninstall": "Odinstalovat", + "plugin-item.settings": "Nastavení", + "plugin-item.installed": "Nainstalováno", + "plugin-item.latest": "Poslední", + "plugin-item.upgrade": "Aktualizace", + "plugin-item.more-info": "Pro více informací:", + "plugin-item.unknown": "Neznámí", + "plugin-item.unknown-explanation": "Stav tohoto rozšíření nemohl být zjištěn, možná díky chybě v konfiguraci.", + "plugin-item.compatible": "Tento zásuvný modul funguje v NodeBB %1", + "plugin-item.not-compatible": "Tento zásuvný modul neobsahuje kompatibilní data. Předtím, než ho nainstalujete do vašeho prostředí, ujistěte se, že funguje správně.", + + "alert.enabled": "Rozšíření povoleno", + "alert.disabled": "Rozšíření zakázáno", + "alert.upgraded": "Rozšíření bylo aktualizováno", + "alert.installed": "Rozšíření bylo nainstalováno", + "alert.uninstalled": "Rozšíření bylo odinstalováno", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Rozšíření bylo úspěšně deaktivováno", + "alert.upgrade-success": "Pro úplnou aktualizace tohoto rozšíření, znovu sestavte a restartujte NodeBB.", + "alert.install-success": "Rozšíření bylo úspěšně nainstalováno, můžete ho aktivovat.", + "alert.uninstall-success": "Rozšíření bylo úspěšně deaktivováno a odinstalováno.", + "alert.suggest-error": "

NodeBB se nemohl připojit ke správce balíčku, pokračovat v instalaci poslední verze?

Server odpověděl (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB se nemohl připojit ke správci balíčku, aktualizace není doporučena.

", + "alert.incompatible": "

Vaše verze NodeBB (v%1) umožňuje jen aktualizovat toto rozšíření na v%2. Aktualizujte prosím NodeBB, chcete-li nainstalovat nejnovější verzi tohoto rozšíření.

", + "alert.possibly-incompatible": "

Nebyla nalezena žádná informace o kompatibilitě

Toto rozšíření nemá nastavenou požadovanou verzi NodeBB. Plná kompatibilita nemůže být garantována a může způsobit, že se vám již NodeBB nespustí.

Nespustí-li se správně NodeBB:

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

Pokračovat v instalaci této aktuální verze rozšíření?

", + "alert.reorder": "Rozšíření byly seřazeny", + "alert.reorder-success": "Pro úplné dokončení úkonu, prosím znovu sestavte a restartujte Vaše NodeBB.", + + "license.title": "Licenční informace o rozšíření", + "license.intro": "Rozšíření %1 je licencováno pod %2. Pro aktivování tohoto rozšíření si přečtěte licenční podmínky.", + "license.cta": "Přejete si pokračovat v aktivování tohoto rozšíření?" +} diff --git a/public/language/cs/admin/extend/rewards.json b/public/language/cs/admin/extend/rewards.json new file mode 100644 index 0000000000..9f0d26cfc3 --- /dev/null +++ b/public/language/cs/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Odměny", + "condition-if-users": "Pokud uživatel", + "condition-is": "je:", + "condition-then": "Pak:", + "max-claims": "Počet dosažitelnosti odměny", + "zero-infinite": "Pro neomezeně zadejte 0", + "delete": "Odstranit", + "enable": "Povolit", + "disable": "Zakázat", + + "alert.delete-success": "Odměna byla úspěšně smazána", + "alert.no-inputs-found": "Nepovolená odměna – nebyl nalezen žádný záznam.", + "alert.save-success": "Odměny byly úspěšně uloženy" +} \ No newline at end of file diff --git a/public/language/cs/admin/extend/widgets.json b/public/language/cs/admin/extend/widgets.json new file mode 100644 index 0000000000..4e3424696d --- /dev/null +++ b/public/language/cs/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Dostupné miniaplikace", + "explanation": "Vyberte si miniaplikaci z vysouvací nabídky a přetáhněte ji do oblasti šablony miniaplikace nalevo.", + "none-installed": "Nebyly nalezeny žádné miniaplikace! Aktivujte zásuvný modul miniaplikace v ovládacím panelu zásuvné moduly.", + "clone-from": "Klonovat miniaplikaci z", + "containers.available": "Dostupné moduly", + "containers.explanation": "Přetáhněte na jakoukoliv aktivní miniaplikaci", + "containers.none": "Nic", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Hlavička panelu", + "container.panel-body": "Tělo panelu", + "container.alert": "Upozornění", + + "alert.confirm-delete": "Jste si jist/a, že chcete smazat tuto miniaplikaci?", + "alert.updated": "Miniaplikace byly aktualizovány", + "alert.update-success": "Miniaplikace byly úspěšně aktualizovány", + "alert.clone-success": "Úspěšně naklonované miniaplikace", + + "error.select-clone": "Vyberte prosím stránku, ze které chcete klonovat", + + "title": "Titul", + "title.placeholder": "Titul (zobrazuje se jen v některých kontajnerech)", + "container": "Kontejner", + "container.placeholder": "Přesuňte sem kontejner nebo zadejte HTML", + "show-to-groups": "Zobrazit ve skupinách", + "hide-from-groups": "Skrýt ve skupinách", + "hide-on-mobile": "Skrýt na mobilu" +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/admins-mods.json b/public/language/cs/admin/manage/admins-mods.json new file mode 100644 index 0000000000..654ed6a2e4 --- /dev/null +++ b/public/language/cs/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Správci", + "global-moderators": "Hlavní moderátoři", + "moderators": "Moderators", + "no-global-moderators": "Žádní hlavní moderátoři", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Žádní moderátoři", + "add-administrator": "Přidat správce", + "add-global-moderator": "Přidat hlavního moderátora", + "add-moderator": "Přidat moderátora" +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/categories.json b/public/language/cs/admin/manage/categories.json new file mode 100644 index 0000000000..7d7719e51c --- /dev/null +++ b/public/language/cs/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Nastavení kategorie", + "privileges": "Oprávnění", + + "name": "Název kategorie", + "description": "Popis kategorie", + "bg-color": "Barva pozadí", + "text-color": "Barva textu", + "bg-image-size": "Velikost obrázku pozadí", + "custom-class": "Upravit třídu", + "num-recent-replies": "# posledních odpovědí", + "ext-link": "Externí odkaz", + "subcategories-per-page": "Subcategories per page", + "is-section": "Zacházet s kategorii jako se sekcí", + "post-queue": "Post queue", + "tag-whitelist": "Seznam povolených značek", + "upload-image": "Nahrát obrázek", + "delete-image": "Vyjmout", + "category-image": "Obrázek kategorie", + "parent-category": "Nadřazená kategorie", + "optional-parent-category": "Nadřazená kategorie (doporučeno)", + "top-level": "Top Level", + "parent-category-none": "(nic)", + "copy-parent": "Kopírovat nadřazenou", + "copy-settings": "Kopírovat nastavení z", + "optional-clone-settings": "Klonovat nastavení z kategorie (doporučeno)", + "clone-children": "Klonovat podřízené kategorie a nastavení", + "purge": "Vyčistit kategorii", + + "enable": "Povolit", + "disable": "Zakázat", + "edit": "Upravit", + "analytics": "Analytika", + "view-category": "Zobrazit kategorii", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Vyberte kategorii", + "set-parent-category": "Nastavit nadřazenou kategorii", + + "privileges.description": "V této části můžete konfigurovat oprávnění pro řízení přístupu pro části webu. Oprávnění lze udělit na základě uživatele nebo skupiny. Z vysouvacího seznamu níže si vyberte doménu.", + "privileges.category-selector": "Konfigurace oprávnění pro", + "privileges.warning": "Poznámka: nastavení oprávnění má okamžitý vliv. Není tedy nutné uložit kategorii pro upravení těchto nastavení", + "privileges.section-viewing": "Oprávnění prohlížení", + "privileges.section-posting": "Oprávnění příspěvků", + "privileges.section-moderation": "Oprávnění moderování", + "privileges.section-other": "Ostatní", + "privileges.section-user": "Uživatel", + "privileges.search-user": "Přidat uživatele", + "privileges.no-users": "V této kategorii není nastaveno žádné oprávnění uživatele.", + "privileges.section-group": "Skupina", + "privileges.group-private": "Tato skupina je soukromá", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Přidat skupinu", + "privileges.copy-to-children": "Kopírovat do podřazené", + "privileges.copy-from-category": "Kopírovat z kategorie", + "privileges.copy-privileges-to-all-categories": "Kopírovat do všech kategorii", + "privileges.copy-group-privileges-to-children": "Kopírovat oprávnění této skupiny do podřízené kategorie.", + "privileges.copy-group-privileges-to-all-categories": "Kopírovat oprávnění této skupiny do všech kategorii.", + "privileges.copy-group-privileges-from": "Kopírovat oprávnění této skupiny z jiné kategorie.", + "privileges.inherit": "Má-li skupina registrovaní uživatelé určitá oprávnění, ostatní skupiny budou mít totožné oprávnění i když nejsou výslovně definována/zaškrtnuta. Tyto zděděné oprávnění vám jsou zobrazena, neboť všichni uživatelé jsou součástí skupiny registrovaných uživalelů. Takže oprávnění pro další skupiny nemusí být dodatečně nastavováno.", + "privileges.copy-success": "Oprávnění bylo zkopírováno.", + + "analytics.back": "Zpět do seznamu kategorii", + "analytics.title": "Analýza pro kategorii \"%1\"", + "analytics.pageviews-hourly": "Postava 1– zobrazení stránky za hodinu pro tuto kategorii", + "analytics.pageviews-daily": "Postava 2 – zobrazení stránky za den pro tuto kategorii", + "analytics.topics-daily": "Postava 3 – vytvořených témat za den pro tuto kategorii", + "analytics.posts-daily": "Postava 4 – vytvořených příspěvků za den pro tuto kategorii", + + "alert.created": "Vytvořeno", + "alert.create-success": "Kategorie byla úspěšně vytvořena.", + "alert.none-active": "Nemáte žádné aktivní kategorie.", + "alert.create": "Vytvořit kategorii", + "alert.confirm-purge": "

Opravdu chcete vyčistit tuto kategorii \"%1\"?

UpozorněníVšechny témata a příspěvky v této kategorii budou smazána.

Smazání kategorie vyjme všechny témata a příspěvky a odstraní kategorii z databáze. Pokud chcete vyjmout kategorii dočasně, raději místo toho kategorii „zakažte”.

", + "alert.purge-success": "Kategorie byla vyčištěna.", + "alert.copy-success": "Nastavení bylo zkopírováno.", + "alert.set-parent-category": "Nastavit nadřazenou kategorii", + "alert.updated": "Kategorie byly aktualizovány", + "alert.updated-success": "ID kategorie %1 bylo aktualizováno.", + "alert.upload-image": "Nahrát obrázek kategorie", + "alert.find-user": "Najít uživatele", + "alert.user-search": "Najít uživatele…", + "alert.find-group": "Najít skupinu", + "alert.group-search": "Hledat skupinu…", + "alert.not-enough-whitelisted-tags": "Seznam povolených značek je menší než minimální počet povolených značek. Vytvořte proto další povolené značky!", + "collapse-all": "Sbalit vše", + "expand-all": "Rozbalit vše", + "disable-on-create": "Zakázat při vytvoření", + "no-matches": "Žádná shoda" +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/digest.json b/public/language/cs/admin/manage/digest.json new file mode 100644 index 0000000000..96c7c0849b --- /dev/null +++ b/public/language/cs/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Statistický seznam odeslaných přehledů spolu s časy je zobrazen níže.", + "disclaimer": "Vezměte prosím na vědomí, že kvůli charakteru e-mailové technologie není zaručeno doručení e-mailu. Mnoho proměnných ovlivňuje, zda je e-mail odeslán na server příjemce, z něj doručen do doručené pošty uživatele, včetně reputace samotného serveru, IP adres na černé listině a zda je nakonfigurováno správně DKIM / SPF / DMARC.", + "disclaimer-continued": "Úspěšné doručení zprávy znamená, že byla úspěšně odeslána NodeBB a potvrzena serverem příjemce. Ovšem to neznamená, že e-mail byl doručen do doručené pošty uživatele. Pro dosažení nejlepších výsledků doporučujeme použít službu doručování e-mailů třetích stran, například SendGrid.", + + "user": "Uživatel", + "subscription": "Typ odběru", + "last-delivery": "Poslední úspěšné doručení", + "default": "Výchozí systémové", + "default-help": "Výchozí systémové znamená, že uživatel nemůže přenastavit celkové nastavení pravidel na fóru pro odesílání přehledů, které je momentálně%1", + "resend": "Znovu odeslat přehled", + "resend-all-confirm": "Jste si jist/a, že chcete ručně spustit tento přehled.", + "resent-single": "Manuální znovu poslání přehledu bylo dokončeno", + "resent-day": "Znovu odeslat denní přehled", + "resent-week": "Znovu odeslat týdenní přehled", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Znovu odeslat měsíční přehled", + "null": "Nikdy", + "manual-run": "Spustit manuálně přehled:", + + "no-delivery-data": "Žádná data odeslání nebyla nalezena" +} diff --git a/public/language/cs/admin/manage/groups.json b/public/language/cs/admin/manage/groups.json new file mode 100644 index 0000000000..2abf7f36e9 --- /dev/null +++ b/public/language/cs/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Název skupiny", + "badge": "Symbol", + "properties": "Vlastnosti", + "description": "Popis skupiny", + "member-count": "Počet členů", + "system": "Systém", + "hidden": "Skrytý", + "private": "Soukromí", + "edit": "Upravit", + "delete": "Odstranit", + "privileges": "Oprávnění", + "download-csv": "CSV", + "search-placeholder": "Hledat", + "create": "Vytvořit skupinu", + "description-placeholder": "Krátký popis skupiny", + "create-button": "Vytvořit", + + "alerts.create-failure": "Ale, ale

Objevil se problém s vytvořením skupiny. Zkuste to později.

", + "alerts.confirm-delete": "Jste si jist, že chcete odstranit tuto skupinu?", + + "edit.name": "Jméno", + "edit.description": "Popis", + "edit.user-title": "Název členů", + "edit.icon": "Ikona skupin", + "edit.label-color": "Barva popisu skupiny", + "edit.text-color": "Barva textu skupiny", + "edit.show-badge": "Zobrazit odznak", + "edit.private-details": "Je-li povoleno, připojení ke skupině vyžaduje schválení od vlastníka skupiny.", + "edit.private-override": "Upozornění: soukromé skupiny jsou zakázány na systémové úrovni, což tuto možnost zneplatňuje.", + "edit.disable-join": "Zakázat požadavky na připojení", + "edit.disable-leave": "Nepovolit uživatelům opuštění skupiny", + "edit.hidden": "Skrýt", + "edit.hidden-details": "Je-li povoleno, tato skupina nebude zobrazena na seznamu skupin a uživatelé musí být manuálně zvány", + "edit.add-user": "Přidat uživatele do skupiny", + "edit.add-user-search": "Hledat uživatele", + "edit.members": "Seznam členů", + "control-panel": "Ovládací panel skupin", + "revert": "Zpět", + + "edit.no-users-found": "Nenalezen žádný uživatel", + "edit.confirm-remove-user": "Jste si jist/a, že chcete vyřadit tohoto uživatele?", + "edit.save-success": "Změny byly uloženy." +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/privileges.json b/public/language/cs/admin/manage/privileges.json new file mode 100644 index 0000000000..b65e9f8f3d --- /dev/null +++ b/public/language/cs/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Všeobecné", + "admin": "Správce", + "group-privileges": "Oprávnění skupiny", + "user-privileges": "Oprávnění uživatele", + "edit-privileges": "Upravit oprávnění", + "select-clear-all": "Select/Clear All", + "chat": "Konverzace", + "upload-images": "Nahrát obrázky", + "upload-files": "Náhrát soubory", + "signature": "Podpis", + "ban": "Blokovat", + "mute": "Mute", + "invite": "Invite", + "search-content": "Hledat obsah", + "search-users": "Hledat uživatele", + "search-tags": "Hledat označení", + "view-users": "Zobrazit uživatele", + "view-tags": "Zobrazit značky", + "view-groups": "Zobrazit skupiny", + "allow-local-login": "Místní přihlášení", + "allow-group-creation": "Vytvořit skupinu", + "view-users-info": "Zobrazit informace o uživateli", + "find-category": "Hledat kategorii", + "access-category": "Přístup ke kategorii", + "access-topics": "Přístup k tématům", + "create-topics": "Vytvořit téma", + "reply-to-topics": "Odpovědět na téma", + "schedule-topics": "Schedule Topics", + "tag-topics": "Označit téma", + "edit-posts": "Upravit příspěvek", + "view-edit-history": "Zobrazit historii editace", + "delete-posts": "Odstranit příspěvky", + "view_deleted": "Zobrazit odstraněné příspěvky", + "upvote-posts": "Souhlasné příspěvky", + "downvote-posts": "Nesouhlasné příspěvky", + "delete-topics": "Odstranit témata", + "purge": "Vyčistit", + "moderate": "Moderace", + "admin-dashboard": "Nástěnka", + "admin-categories": "Kategorie", + "admin-privileges": "Oprávnění", + "admin-users": "Uživatelé", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Nastavení", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/registration.json b/public/language/cs/admin/manage/registration.json new file mode 100644 index 0000000000..1958d5d957 --- /dev/null +++ b/public/language/cs/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Fronta", + "description": "V registrační frontě nejsou žádní uživatelé.
Pro povolení této funkce, přejděte do nabídky Nastavení → Uživatel → Registrace uživatele a nastavte Typ registrace na „Schválení správcem”.", + + "list.name": "Jméno", + "list.email": "E-mail", + "list.ip": "IP", + "list.time": "Čas", + "list.username-spam": "Frekvence: %1 zdá se: %2 důvěryhodnost: %3", + "list.email-spam": "Frekvence: %1 zdá se: %2", + "list.ip-spam": "Frekvence: %1 zdá se: %2", + + "invitations": "Pozvání", + "invitations.description": "Níže je kompletní seznam odeslaných pozvání. Pro hledání v seznamu pomocí e-mailu nebo jména uživatele, použijte klávesu ctrl+f.

U uživatelů, kteří využili pozvání, bude uživatelské jméno zobrazeno napravo od e-mailů.", + "invitations.inviter-username": "Uživatelské jméno pozvaného", + "invitations.invitee-email": "E-mail pozvaného", + "invitations.invitee-username": "Uživatelské jméno pozvaného (je-li registrován)", + + "invitations.confirm-delete": "Jste si jist/a, že chcete odstraniti toto pozvání?" +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/tags.json b/public/language/cs/admin/manage/tags.json new file mode 100644 index 0000000000..30cceeada0 --- /dev/null +++ b/public/language/cs/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Vaše fórum neobsahuje žádné témata se značkami.", + "bg-color": "Barva pozadí", + "text-color": "Barva textu", + "description": "Pro výběr více značek, vyberte značky kliknutím nebo přetažením, za použití klávesy CTRL.", + "create": "Vytvořit značku", + "modify": "Upravit značky", + "rename": "Přejmenovat značky", + "delete": "Odstranit vybrané značky", + "search": "Hledat značky...", + "settings": "Nastavení značek", + "name": "Název značky", + + "alerts.editing": "Upravení značky(ek)", + "alerts.confirm-delete": "Chcete odstranit vybranou značku?", + "alerts.update-success": "Značka aktualizována.", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/uploads.json b/public/language/cs/admin/manage/uploads.json new file mode 100644 index 0000000000..847bc82dc9 --- /dev/null +++ b/public/language/cs/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Nahrát soubor", + "filename": "Název souboru", + "usage": "Použito v příspěvku", + "orphaned": "Nevyužito", + "size/filecount": "Velikost / Počet souborů", + "confirm-delete": "Opravdu chcete odstranit tento soubor?", + "filecount": "%1 souborů", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/cs/admin/manage/users.json b/public/language/cs/admin/manage/users.json new file mode 100644 index 0000000000..06cb2bbffa --- /dev/null +++ b/public/language/cs/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Uživatelé", + "edit": "Actions", + "make-admin": "Učinit správcem", + "remove-admin": "Odebrat správce", + "validate-email": "Ověřit e-mail", + "send-validation-email": "Poslat ověřovací e-mail", + "password-reset-email": "Poslat e-mail pro resetování hesla", + "force-password-reset": "Vynutit reset hesla a odhlášení uživatele", + "ban": "Zakázat uživatele", + "temp-ban": "Dočasně zakázat uživatele", + "unban": "Zrušit zákaz uživatele", + "reset-lockout": "Obnovit uzamčení", + "reset-flags": "Obnovit označení", + "delete": "Odstranit Uživatele", + "delete-content": "Odstranit Obsah uživatele", + "purge": "Odstranit uživatele a obsah", + "download-csv": "Stáhnout jako CSV", + "manage-groups": "Spravovat skupiny", + "add-group": "Přidat skupinu", + "create": "Create User", + "invite": "Invite by Email", + "new": "Nový uživatel", + "filter-by": "Filter by", + "pills.unvalidated": "Neověřeno", + "pills.validated": "Validated", + "pills.banned": "Zakázán", + + "50-per-page": "50 na stránku", + "100-per-page": "100 na stránku", + "250-per-page": "250 na stránku", + "500-per-page": "500 na stránku", + + "search.uid": "Dle ID uživatele", + "search.uid-placeholder": "Pro hledání, zadejte ID uživatele", + "search.username": "Dle jména uživatele", + "search.username-placeholder": "Zadejte hledané uživatelské jméno", + "search.email": "Podle e-mailu", + "search.email-placeholder": "Zadejte hledaný e-mail", + "search.ip": "Podle IP adresy", + "search.ip-placeholder": "Zadejte hledanou IP adresu", + "search.not-found": "Uživatel nebyl nalezen.", + + "inactive.3-months": "3 měsíce", + "inactive.6-months": "6 měsíců", + "inactive.12-months": "12 měsíců", + + "users.uid": "uid", + "users.username": "jméno", + "users.email": "e-mail", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "počet příspěvků", + "users.reputation": "reputace", + "users.flags": "označení", + "users.joined": "připojil", + "users.last-online": "poslední přihlášení", + "users.banned": "zakázán", + + "create.username": "Uživatelské jméno", + "create.email": "E-mail", + "create.email-placeholder": "E-mail tohoto uživatele", + "create.password": "Heslo", + "create.password-confirm": "Potvrdit heslo", + + "temp-ban.length": "Length", + "temp-ban.reason": "Důvod (volitelné)", + "temp-ban.hours": "Hodiny", + "temp-ban.days": "Dny", + "temp-ban.explanation": "Zadejte délku trvání pro zákaz. Nezapomeňte, že 0 je považována jako trvalý zákaz.", + + "alerts.confirm-ban": "Opravdu chcete trvale zakázat tohoto uživatele?", + "alerts.confirm-ban-multi": "Opravdu chcete trvale zakázat tyto uživatele?", + "alerts.ban-success": "Uživatel byl zakázán.", + "alerts.button-ban-x": "Zakázat %1 uživatele.", + "alerts.unban-success": "Zákaz uživatele byl zrušen.", + "alerts.lockout-reset-success": "Uzamčení bylo obnoveno.", + "alerts.flag-reset-success": "Označení bylo obnoveno.", + "alerts.no-remove-yourself-admin": "Sebe jako správce nemůžete vyjmout.", + "alerts.make-admin-success": "Uživatel je nyní správcem", + "alerts.confirm-remove-admin": "Opravdu chcete vyjmout tohoto správce?", + "alerts.remove-admin-success": "Uživatel již není správcem.", + "alerts.make-global-mod-success": "Uživatel je nyní globálním moderátorem.", + "alerts.confirm-remove-global-mod": "Opravdu chcete vyjmout tohoto globálního moderátora?", + "alerts.remove-global-mod-success": "Uživatel již není globálním moderátorem.", + "alerts.make-moderator-success": "Uživatel je nyní moderátorem.", + "alerts.confirm-remove-moderator": "Opravdu chcete vyjmout tohoto moderátora?", + "alerts.remove-moderator-success": "Uživatel není již moderátorem.", + "alerts.confirm-validate-email": "Chcete schválit e-mailové adresy těchto uživatelů?", + "alerts.confirm-force-password-reset": "Jste si jist/a, že chcete resetovat heslo uživatele a rovnou je odhlásit?", + "alerts.validate-email-success": "E-maily byly ověřeny", + "alerts.validate-force-password-reset-success": "Uživatelské heslo bylo resetováno a přihlašovací relace byla ukončena.", + "alerts.password-reset-confirm": "Chcete poslat těmto uživatelům e-mail pro resetování hesla?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "Uživatel byl odstraněn.", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "Obsah uživatele(ů) odstraněn!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Vytvořit uživatele", + "alerts.button-create": "Vytvořit", + "alerts.button-cancel": "Zrušit", + "alerts.error-passwords-different": "Hesla musí souhlasit.", + "alerts.error-x": "Chyba

%1

", + "alerts.create-success": "Uživatel byl vytvořen.", + + "alerts.prompt-email": "E-maily:", + "alerts.email-sent-to": "E-mail s pozvánkou byl odeslán na %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/cs/admin/menu.json b/public/language/cs/admin/menu.json new file mode 100644 index 0000000000..d22f9a6bdd --- /dev/null +++ b/public/language/cs/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Všeobecné", + + "section-manage": "Spravovat", + "manage/categories": "Kategorie", + "manage/privileges": "Oprávnění", + "manage/tags": "Značky", + "manage/users": "Uživatelé", + "manage/admins-mods": "Správci a moderátoři", + "manage/registration": "Registrační fronta", + "manage/post-queue": "Fronta příspěvků", + "manage/groups": "Skupiny", + "manage/ip-blacklist": "Černá listina IP", + "manage/uploads": "Nahráno", + "manage/digest": "Odběry", + + "section-settings": "Nastavení", + "settings/general": "Všeobecné", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "E-mail", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Hosté", + "settings/uploads": "Nahrané", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Stránkování", + "settings/tags": "Značky", + "settings/notifications": "Oznámení", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Webový prohledávač", + "settings/sockets": "Sockety", + "settings/advanced": "Pokročilé", + + "settings.page-title": "Nastavení %1", + + "section-appearance": "Vzhled", + "appearance/themes": "Motivy", + "appearance/skins": "Vzhledy", + "appearance/customise": "Uživatelský obsah (HTML/JS/CSS)", + + "section-extend": "Rozšířit", + "extend/plugins": "Rozšíření", + "extend/widgets": "Miniaplikace", + "extend/rewards": "Odměny", + + "section-social-auth": "Sociální autentifikace", + + "section-plugins": "Rozšíření", + "extend/plugins.install": "Nainstalovat rozšíření", + + "section-advanced": "Pokročilé", + "advanced/database": "Databáze", + "advanced/events": "Události", + "advanced/hooks": "Háky", + "advanced/logs": "Protokoly", + "advanced/errors": "Chyby", + "advanced/cache": "Mezipamě", + "development/logger": "Protokolář", + "development/info": "Informace", + + "rebuild-and-restart-forum": "Znovu sestavit a restartovat fórum", + "restart-forum": "Restartovat fórum", + "logout": "Odhlásit", + "view-forum": "Zobrazit fórum", + + "search.placeholder": "Search settings", + "search.no-results": "Žádné výsledky…", + "search.search-forum": "Prohledat fórum pro ", + "search.keep-typing": "Pište dále pro zobrazení výsledků…", + "search.start-typing": "Začněte psát pro zobrazení výsledků…", + + "connection-lost": "Připojení k %1 bylo ztraceno, snaha o opětovné připojení…", + + "alerts.version": "Spušteno NodeBB v%1", + "alerts.upgrade": "Aktualizovat na v%1" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/advanced.json b/public/language/cs/admin/settings/advanced.json new file mode 100644 index 0000000000..bc10146c76 --- /dev/null +++ b/public/language/cs/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Režim údržby", + "maintenance-mode.help": "Je-li fórum v režimu údržby, všechny požadavky budou přesměrovány na statickou stránku. Správci jsou vyloučeni z tohoto přesměrování a budou mít normálně zobrazené stránky.", + "maintenance-mode.status": "Stavový kód režimu údržby", + "maintenance-mode.message": "Zpráva údržby", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Hlavičky", + "headers.allow-from": "Nastavte ALLOW-FROM pro umístění NodeBB do iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Upravte si hlavičku „Powered by” odesílanou NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "Pro zakázání přístupu na všechny stránky, zanechte prázdné", + "headers.acao-regex-help": "Zde zadejte regulární výrazy, které odpovídají dynamickým originálům. Pro zakázání všech stránek, ponechte prázdné.", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Přísné zabezpečení přenosu", + "hsts.enabled": "Povolit HSTS (doporučeno)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Zahrnout poddomény v hlavičce HSTS", + "hsts.preload": "Povolit před-načtení hlavičky HSTS", + "hsts.help": "Je-li povoleno, bude nastavena pro tyto stránky hlavička HSTS . Můžete si v hlavičce zvolit zahrnutí i poddomén a přednastavených příznaků. Nejste-li si jist/a, ponechte nezaškrtnutéVíce informací ", + "traffic-management": "Správa provozu", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Povolit správu provozu", + "traffic.event-lag": "Hranice prodlevy smyčky události (v milisekundách)", + "traffic.event-lag-help": "Snížení této hodnoty sníží čas pro načtení stránky, ale taky zobrazí více uživatelům zprávu o „přetížení stránek”. (je vyžadován restart)", + "traffic.lag-check-interval": "Kontrola intervalů (v milisekundách)", + "traffic.lag-check-interval-help": "Snížení této hodnoty způsobí, že NodeBB bude citlivější na zatížení načítání stránek a na kontrolu tohoto zatížení. (je vyžadován restart)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/api.json b/public/language/cs/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/cs/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/chat.json b/public/language/cs/admin/settings/chat.json new file mode 100644 index 0000000000..7421f4bd3a --- /dev/null +++ b/public/language/cs/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Nastavení konverzace", + "disable": "Zakázat konverzaci", + "disable-editing": "Zakázat upravení/odstranění konverzační zprávy", + "disable-editing-help": "Správci a globální moderátoři jsou vyjmuti z tohoto omezení", + "max-length": "Maximální délka konverzační zprávy", + "max-room-size": "Maximální počet uživatelů v konverzační místnosti", + "delay": "Čas mezi konverzačními zprávami v milisekundách", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Počet sekund, kdy může být ještě konverzační zpráva upravena (pro zakázání - 0)", + "restrictions.seconds-delete-after": "Počet sekund, kdy může být ještě konverzační zpráva odstraněna (pro zakázání - 0)" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/cookies.json b/public/language/cs/admin/settings/cookies.json new file mode 100644 index 0000000000..08c7f9dcc7 --- /dev/null +++ b/public/language/cs/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Souhlas EU", + "consent.enabled": "Povolit", + "consent.message": "Potvrzovací zpráva", + "consent.acceptance": "Zpráva o příjmutí", + "consent.link-text": "Text se zásadami", + "consent.link-url": "Odkaz na URL se zásadami", + "consent.blank-localised-default": "Pro použití výchozího textu NodeBB, zanechte prázdné", + "settings": "Nastavení", + "cookie-domain": "Doména relace cookies", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Pro výchozí, zanechte prázdné" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/email.json b/public/language/cs/admin/settings/email.json new file mode 100644 index 0000000000..fadb159ee1 --- /dev/null +++ b/public/language/cs/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Nastavení e-mailu", + "address": "E-mailové adresy", + "address-help": "Následující e-mailové adresy budou zobrazeny příjemci v políčkách „Od” a „Odpovědět”.", + "from": "Jméno – od", + "from-help": "Zobrazené jméno v e-mailu v – Od", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "Přenos SMTP", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Ze seznamu můžete vybrat známé služby nebo zadat vlastní.", + "smtp-transport.service": "Vyberte službu", + "smtp-transport.service-custom": "Uživatelský služba", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "Hostitel SMTP", + "smtp-transport.port": "Port SMTP", + "smtp-transport.security": "Zabezpečení připojení", + "smtp-transport.security-encrypted": "Šifrované", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Nic", + "smtp-transport.username": "Uživatelské jméno", + "smtp-transport.username-help": "Pro službu Gmail, zadejte plnou e-mailovou adresu, zvláště, používáte-li spravovanou doménu Google Apps.", + "smtp-transport.password": "Heslo", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Upravit šablonu e-mailu", + "template.select": "Vybrat šablonu e-mailu", + "template.revert": "Zpět k původnímu", + "testing": "Test e-mailu", + "testing.select": "Vyberte šablonu e-mailu", + "testing.send": "Odeslat testovací e-mail", + "testing.send-help": "Testovací e-mail bude odeslán aktuálně přihlášenému uživateli na jeho e-mailovou adresu z registrace.", + "subscriptions": "E-mailové odběry", + "subscriptions.disable": "Zakázat e-mailové odběry", + "subscriptions.hour": "Hodina přehledu", + "subscriptions.hour-help": "Zadejte číslo odpovídající hodině, kdy mají být odeslány přehledové e-maily (tj. 0 pro půlnoc, 17 pro 5:00pm). Mějte na paměti, že tato hodina závisí na hodinách samotného serveru a nemusí tak souhlasit se systémovými hodinami.
Přibližný čas serveru je: .
Další odeslání přehledů je plánováno na .", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/cs/admin/settings/general.json b/public/language/cs/admin/settings/general.json new file mode 100644 index 0000000000..6ae5d5f244 --- /dev/null +++ b/public/language/cs/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Nastavení stránky", + "title": "Název stránky", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "URL názvu stránky", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Název vaší komunity", + "title.show-in-header": "Zobrazit název stránky v hlavičce", + "browser-title": "Název prohlížeče", + "browser-title-help": "Nebude-li název prohlížeče určen, bude použit název stránky", + "title-layout": "Vzhled názvu", + "title-layout-help": "Určete jak název prohlížeče má být sestaven, tj. {pageTitle} | {browserTitle}", + "description.placeholder": "Zkrácený popis vaší komunity", + "description": "Popis stránky", + "keywords": "Klíčová slova pro stránky", + "keywords-placeholder": "Klíčová slova popisující vaši komunitu, odděleno čárkou", + "logo": "Logo stránky", + "logo.image": "Obrázek", + "logo.image-placeholder": "Cesta k logu, aby mohlo být zobrazeno v hlavičce fóra", + "logo.upload": "Nahrát", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "URL loga stránky", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Popisující text (alt)", + "log.alt-text-placeholder": "Alternativní text pro přístupnost", + "favicon": "Ikonka (favicon)", + "favicon.upload": "Nahrát", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Nahrát", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Odchozí odkazy", + "outgoing-links.warning-page": "Použít stránku s upozorněním při odchozích odkazech", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domény u kterých bude přeskočena upozorňovací stránka", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/cs/admin/settings/group.json b/public/language/cs/admin/settings/group.json new file mode 100644 index 0000000000..066361fae7 --- /dev/null +++ b/public/language/cs/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Všeobecné", + "private-groups": "Soukromé skupiny", + "private-groups.help": "Je-li povoleno, připojení ke skupině vyžaduje schválení zakladatele skupiny (výchozí: povoleno)", + "private-groups.warning": "Ale pozor, je-li tato možnost zakázána a vy máte soukromé skupiny, stanou se automaticky veřejnými.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "Toto označení může být použito, aby uživatelé mohly vybrat několik skupinových symbolů, vyžaduje podporu motivu.", + "max-name-length": "Maximální délka názvu skupiny", + "max-title-length": "Maximální délka názvu skupiny", + "cover-image": "Obrázek skupiny", + "default-cover": "Výchozí obrázek", + "default-cover-help": "Pro skupiny, které nemají nahraný obrázek, přidejte výchozí obrázky oddělené čárkami" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/guest.json b/public/language/cs/admin/settings/guest.json new file mode 100644 index 0000000000..9435fdc4df --- /dev/null +++ b/public/language/cs/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Povolit upravení zacházení s hosty", + "handles.enabled-help": "Tato možnost odkryje nové pole, které umožňuje hostům vybrat jméno, které se připojí ke každému příspěvku, který vytvoří. Bude-li zakázáno, budou jednoduše nazýváni „Host”", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/homepage.json b/public/language/cs/admin/settings/homepage.json new file mode 100644 index 0000000000..3db45d23c3 --- /dev/null +++ b/public/language/cs/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Domovská stránka", + "description": "Vyberte, kterou stránku chcete zobrazit, jakmile uživatel přejde na výchozí URL vašeho fóra.", + "home-page-route": "Cesta k domovské stránce", + "custom-route": "Upravit cestu", + "allow-user-home-pages": "Povolit uživatelům domovské stránky", + "home-page-title": "Titulka domovské stránky (výchozí „Domů”)" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/languages.json b/public/language/cs/admin/settings/languages.json new file mode 100644 index 0000000000..37124c7d04 --- /dev/null +++ b/public/language/cs/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Nastavení jazyka", + "description": "Výchozí jazyk určuje nastavení jazyka pro všechny uživatele navštěvující vaše fórum.
Každý uživatel si může pak nastavit výchozí jazyk na stránce nastavení účtu.", + "default-language": "Výchozí jazyk", + "auto-detect": "Automaticky detekovat nastavení jazyka pro hosty" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/navigation.json b/public/language/cs/admin/settings/navigation.json new file mode 100644 index 0000000000..5811c99768 --- /dev/null +++ b/public/language/cs/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ikona:", + "change-icon": "změnit", + "route": "Cesta:", + "tooltip": "Tip:", + "text": "Text:", + "text-class": "Textová třída: doporučené", + "class": "Třída: doporučené", + "id": "ID: doporučené", + + "properties": "Vlastnosti:", + "groups": "Skupiny:", + "open-new-window": "Otevřít v novém okně", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Odstranit", + "btn.disable": "Zakázat", + "btn.enable": "Povolit", + + "available-menu-items": "Dostupné položky nabídky", + "custom-route": "Upravit cestu", + "core": "jádro", + "plugin": "rozšíření" +} diff --git a/public/language/cs/admin/settings/notifications.json b/public/language/cs/admin/settings/notifications.json new file mode 100644 index 0000000000..83e73d288e --- /dev/null +++ b/public/language/cs/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Oznámení", + "welcome-notification": "Uvítání", + "welcome-notification-link": "Odkaz na uvítání", + "welcome-notification-uid": "Uvítání uživatele (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/pagination.json b/public/language/cs/admin/settings/pagination.json new file mode 100644 index 0000000000..49c2da46f0 --- /dev/null +++ b/public/language/cs/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Nastavení stránkování", + "enable": "Stránkovat témata a příspěvky namísto nekonečného posouvání", + "posts": "Post Pagination", + "topics": "Stránkování témat", + "posts-per-page": "Příspěvků na stránku", + "max-posts-per-page": "Maximální množství příspěvků na stránku", + "categories": "Stránkování kategorii", + "topics-per-page": "Témat na stránku", + "max-topics-per-page": "Maximální množství témat na stránku", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/post.json b/public/language/cs/admin/settings/post.json new file mode 100644 index 0000000000..e2c471d925 --- /dev/null +++ b/public/language/cs/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Třídění příspěvků", + "sorting.post-default": "Výchozí třídění příspěvků", + "sorting.oldest-to-newest": "Od nejstarších po nejnovější", + "sorting.newest-to-oldest": "Od nejnovějších po nejstarší", + "sorting.most-votes": "Dle počtu hlasů", + "sorting.most-posts": "Dle počtu příspěvků", + "sorting.topic-default": "Výchozí třídění tématu", + "length": "Délka příspěvku", + "post-queue": "Příspěvky ve frontě", + "restrictions": "Omezení příspěvků", + "restrictions-new": "Omezení nového uživatele", + "restrictions.post-queue": "Povolit frontu pro příspěvky", + "restrictions.post-queue-rep-threshold": "Vyžadovaná reputace pro přeskočení fronty příspěvků", + "restrictions.groups-exempt-from-post-queue": "Vyberte skupinu, která by měla být vyloučena z fronty příspěvků", + "restrictions-new.post-queue": "Povolit omezení nových uživatelů", + "restrictions.post-queue-help": "Povolení fronty příspěvků bude mít za následek vložení příspěvků nových uživatelů do fronty pro schválení.", + "restrictions-new.post-queue-help": "Povolení omezení nových uživatelů bude mít za následek omezení příspěvků vytvořených novými uživateli", + "restrictions.seconds-between": "Počet sekund mezi novými příspěvky", + "restrictions.seconds-between-new": "Sekund mezi příspěvky pro nové uživatele", + "restrictions.rep-threshold": "Ohraničení reputace než začnou platit tato omezení", + "restrictions.seconds-before-new": "Počet sekund, než může nový uživatel vytvořit první příspěvek", + "restrictions.seconds-edit-after": "Počet sekund, kdy příspěvek může být ještě upraven (pro zakázání - 0)", + "restrictions.seconds-delete-after": "Počet sekund, kdy příspěvek může být ještě odstraněn (pro zakázání - 0)", + "restrictions.replies-no-delete": "Počet odpovědí, kdy je již uživateli zakázáno odstranit založená témata (pro zakázání - 0)", + "restrictions.min-title-length": "Minimální délka názvu", + "restrictions.max-title-length": "Maximální délka názvu", + "restrictions.min-post-length": "Minimální délka příspěvku", + "restrictions.max-post-length": "Maximální délka příspěvku", + "restrictions.days-until-stale": "Počet dnů, než je téma považováno za neaktuální", + "restrictions.stale-help": "Je-li téma považováno za „staré”, uživateli se zobrazí oznámení při pokusu o přidání odpovědi.", + "timestamp": "Časový otisk", + "timestamp.cut-off": "Datum ukončení (ve dnech)", + "timestamp.cut-off-help": "Datum a čas bude zobrazen relativně (tj. „před 3 hodinami” / „před 5 dny”), a dle toho lokalizován do různých\n\t\t\t\t\tjazyků. Za určitých okolností, může tento text být přepnut na lokalizované datum\n\t\t\t\t\t(tj. 5 Led 2017 15:30)
(výchozí: 30,nebo měsíc). Nastavte na 0, pro zobrazení datumů, zanecháte-li prázdné, bude vždy zobrazen relativní čas.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Ukázka příspěvku", + "teaser.last-post": "Poslední – zobrazení posledního příspěvku, včetně hlavního příspěvku, nejsou-li odpovědi", + "teaser.last-reply": "Poslední – zobrazení poslední odpovědi, nebo nejsou-li žádné odpovědi textu „Bez odpovědi”", + "teaser.first": "První", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Nastavení nepřečtených", + "unread.cutoff": "Dny ukončení nepřečtených", + "unread.min-track-last": "Minimální počet příspěvků v tématu před posledním čtením", + "recent": "Nastavení pro poslední", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Zakázat filtrování témat v ignorovaných kategoriích na poslední stránce", + "signature": "Nastavení podpisu", + "signature.disable": "Zakázat podpisy", + "signature.no-links": "Zakázat odkazy v podpisech", + "signature.no-images": "Zakázat obrázky v podpisech", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximální délka podpisu", + "composer": "Nastavení kompozice", + "composer-help": "Následující nastavení kontroluje funkčnost a/nebo vzhled zobrazených příspěvků\n\t\t\t\tpro uživatele, kteří vytvoří nové téma nebo odpovídají na existující téma.", + "composer.show-help": "Zobrazit záložku „Nápověda”", + "composer.enable-plugin-help": "Povolit rozšíření přidat obsah do záložky nápovědy", + "composer.custom-help": "Uživatelský text nápovědy", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Sledování IP", + "ip-tracking.each-post": "Sledovat adresu IP u každého příspěvku", + "enable-post-history": "Povolit historii příspěvku" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/reputation.json b/public/language/cs/admin/settings/reputation.json new file mode 100644 index 0000000000..b59d134bcf --- /dev/null +++ b/public/language/cs/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Nastavení reputace", + "disable": "Zakázat systém reputace", + "disable-down-voting": "Zakázat hlasování", + "votes-are-public": "Všechna hlasování jsou veřejná", + "thresholds": "Omezení aktivity", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimální reputace pro vyjádření nesouhlasu s příspěvkem", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimální reputace pro označení příspěvků", + "min-rep-website": "Minimální reputace pro přidání „Webové stránky” do uživatelského profilu", + "min-rep-aboutme": "Minimální reputace pro přidání „O mně” do uživatelského profilu", + "min-rep-signature": "Minimální reputace pro přidání „Podpisu” do uživatelského profilu", + "min-rep-profile-picture": "Minimální reputace pro přidání „Profilového obrázku” do uživatelského profilu", + "min-rep-cover-picture": "Minimální reputace pro přidání „Obrázku uživatele” do uživatelského profilu", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/social.json b/public/language/cs/admin/settings/social.json new file mode 100644 index 0000000000..5645b29e42 --- /dev/null +++ b/public/language/cs/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Sdílení příspěvku", + "info-plugins-additional": "Rozšíření mohou přidat další dodatečné sítě pro sdílení příspěvků.", + "save-success": "Úspěšně uložené sítě sdílející příspěvky." +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/sockets.json b/public/language/cs/admin/settings/sockets.json new file mode 100644 index 0000000000..1250237416 --- /dev/null +++ b/public/language/cs/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Nastavení znovu připojení", + "max-attempts": "Maximální počet pokusů o znovu připojení", + "default-placeholder": "Výchozí: %1", + "delay": "Časové zpoždění pro znovu připojení" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/sounds.json b/public/language/cs/admin/settings/sounds.json new file mode 100644 index 0000000000..d9b2796971 --- /dev/null +++ b/public/language/cs/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Upozornění", + "chat-messages": "Zprávy konverzace", + "play-sound": "Přehrát", + "incoming-message": "Příchozí zpráva", + "outgoing-message": "Odchozí zpráva", + "upload-new-sound": "Nahrát nový zvuk", + "saved": "Nastavení bylo uloženo" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/tags.json b/public/language/cs/admin/settings/tags.json new file mode 100644 index 0000000000..1d16095a31 --- /dev/null +++ b/public/language/cs/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Nastavení značky", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimální počet značek/téma", + "max-per-topic": "maximální počet značek/téma", + "min-length": "Minimální délka značky", + "max-length": "Maximální délka značky", + "related-topics": "Související témata", + "max-related-topics": "Maximální počet zobrazených souvisejících témat (je-li podporováno motivem)" +} \ No newline at end of file diff --git a/public/language/cs/admin/settings/uploads.json b/public/language/cs/admin/settings/uploads.json new file mode 100644 index 0000000000..a1fbd9e71e --- /dev/null +++ b/public/language/cs/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Příspěvky", + "orphans": "Orphaned Files", + "private": "Nahrané soubory jsou soukromé", + "strip-exif-data": "Nepoužít data EXIF", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Přípona souborů je soukromá", + "private-uploads-extensions-help": "Pro nastavení soukromí, zde zadejte seznam souborů oddělený čárkou (tj. pdf, xls,doc). prázdný seznam znamená, že všechny soubory jsou soukromé.", + "resize-image-width-threshold": "Změnit velikost obrázků, jsou-li širší než určená šířka", + "resize-image-width-threshold-help": "(v pixelech, výchozí: 1520 pixelů, pro zakázání - nastavte 0)", + "resize-image-width": "Změnit velikost obrázků na určenou šířku", + "resize-image-width-help": "(v pixelech, výchozí: 760 pixelů, pro zakázání - nastavte 0)", + "resize-image-quality": "Kvalita při změně velikosti obrázků", + "resize-image-quality-help": "Pro snížení velikosti zmenšených obrázků použijte nižší nastavení kvality.", + "max-file-size": "Maximální velikost souboru (v KiB)", + "max-file-size-help": "(v kilobajtech, výchozí 2048 KiB)", + "reject-image-width": "Maximální šířka obrázku (v pixelech)", + "reject-image-width-help": "Širší obrázek než tato hodnota bude zamítnut.", + "reject-image-height": "Maximální výška obrázku (v pixelech)", + "reject-image-height-help": "Vyšší obrázek než tato hodnota bude zamítnut.", + "allow-topic-thumbnails": "Povolit uživatelům nahrát miniatury témat", + "topic-thumb-size": "Velikost miniatury tématu", + "allowed-file-extensions": "Povolené přípony souborů", + "allowed-file-extensions-help": "Zadejte seznam přípon souborů oddělených čárkou (např.: pdf, xls, doc). Prázdný seznam znamená, že všechny přípony jsou povoleny.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profilové obrázky", + "allow-profile-image-uploads": "Povolit uživatelům nahrát profilové obrázky", + "convert-profile-image-png": "Převést profilové obrázky do *.png", + "default-avatar": "Výchozí uživatelský obrázek", + "upload": "Nahrát", + "profile-image-dimension": "Rozlišení profilového obrázku", + "profile-image-dimension-help": "(v pixelech, výchozí: 128 pixelů)", + "max-profile-image-size": "Maximální velikost profilového obrázku", + "max-profile-image-size-help": "(v kilobajtech, výchozí: 256 KiB)", + "max-cover-image-size": "Maximální velikost obrázku", + "max-cover-image-size-help": "(v kilobajtech, výchozí: 2048 KiB)", + "keep-all-user-images": "Ponechat starou verzi obrázků a profilových obrázků na serveru", + "profile-covers": "Profilové obrázky", + "default-covers": "Výchozí obrázek", + "default-covers-help": "Přidat výchozí obrázky oddělené čárkou pro účty, které nemají nahraný obrázek" +} diff --git a/public/language/cs/admin/settings/user.json b/public/language/cs/admin/settings/user.json new file mode 100644 index 0000000000..da3647a63f --- /dev/null +++ b/public/language/cs/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Ověření", + "email-confirm-interval": "Uživatel nesmí požádat o znovu zaslání potvrzujícího e-mailu do", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Povolit přihlášení pomocí", + "allow-login-with.username-email": "Uživatelské jméno nebo e-mail", + "allow-login-with.username": "Pouze uživatelské jméno", + "account-settings": "Nastavení účtu", + "gdpr_enabled": "Povolit souhlas s GDPR", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Zakázat změnu uživatelského jména", + "disable-email-changes": "Zakázat změnu e-mailu", + "disable-password-changes": "Zakázat změnu hesla", + "allow-account-deletion": "Povolit smazání účtu", + "hide-fullname": "Skrýt jméno před uživateli", + "hide-email": "Skrýt e-mail před uživateli", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Motivy", + "disable-user-skins": "Zabránit uživateli ve výběru vlastního vzhledu", + "account-protection": "Ochrana účtu", + "admin-relogin-duration": "Doba pro opětovné přihlášení správce (minuty)", + "admin-relogin-duration-help": "Po nastavení počtu přístupu do správcovské části, bude vyžadováno opětovné přihlášení. Pro zakázání, nastavte na 0.", + "login-attempts": "Počet pokusů o přihlášení za hodinu", + "login-attempts-help": "Překročí-li pokusy o přihlášení uživatele/ů tuto hranici, účet bude uzamknut na určený čas", + "lockout-duration": "Délka blokování účtu (v minutách)", + "login-days": "Počet dní na zapamatování relace přihlášení uživatele", + "password-expiry-days": "Vynutit reset hesla po určitém počtu dní", + "session-time": "Čas relace", + "session-time-days": "Dny", + "session-time-seconds": "Sekundy", + "session-time-help": "Tyto hodnoty jsou využity v rozhodujícím procesu, jak dlouho zůstane uživatel přihlášen při zaškrtnutí „Zapamatovat si mě”. Nezapomeňte, že bude použita jen jedna hodnota. Jestli není nastavena hodnota v sekundách, budou brány v potaz dny. Nebudou-li nastaveny dny, hodnota bude standardně 14 dní.", + "online-cutoff": "Počet minut, kdy je uživatel považován za neaktivního", + "online-cutoff-help": "Nebude-li uživatel vykonávat žádnou akci v tomto časovém rozpětí, bude považován za neaktivního a nebude docházet k automatickým aktualizacím.", + "registration": "Registrace uživatele", + "registration-type": "Typ registrace", + "registration-approval-type": "Typ schválení registrace", + "registration-type.normal": "Normální", + "registration-type.admin-approval": "Povolení správce", + "registration-type.admin-approval-ip": "Povolení správce dle IP", + "registration-type.invite-only": "Jen na pozvání", + "registration-type.admin-invite-only": "Jen na pozvání správce", + "registration-type.disabled": "Bez registrace", + "registration-type.help": "Normální - uživatel se může registrovat ze stránky Registrace
\nJen pro pozvané - uživatel může pozvat jiné ze stránkyUživatelé.
♥\nJen pro pozvané správcem - jen správci mohou pozvat jiné ze stránky Uživateléa ze stránky admin/manage/users.
\nBez registrace - žádná registrace uživatelů.
•", + "registration-approval-type.help": "Normální - uživatel se může hned registrovat.
\nSchválení správcem - uživatelská registrace je zařazena do fronty pro schválení správcem.
\nSchválení správcem dle IP - pro nové uživatele stav Normální, Schválení správcem pro adresy IP, které již mají účet.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximální počet pozvání na uživatele", + "max-invites": "Maximální počet pozvání na uživatele", + "max-invites-help": "0 pro neomezené. Správci mají neomezeně pozvánek
Použitelné jen pro „Jen pozvané\"", + "invite-expiration": "Vypršení pozvánky", + "invite-expiration-help": "pozvání vyprší za # dní.", + "min-username-length": "Minimální délka uživatelského jména", + "max-username-length": "Maximální délka uživatelského jména", + "min-password-length": "Minimální délka hesla", + "min-password-strength": "Minimální síla hesla", + "max-about-me-length": "Maximální délka informací „O mně”", + "terms-of-use": "Podmínky užití fóra (pro zakázání zanechte prázdné)", + "user-search": "Hledat uživatele", + "user-search-results-per-page": "Počet zobrazených výsledků", + "default-user-settings": "Výchozí nastavení uživatele", + "show-email": "Zobrazit e-mail", + "show-fullname": "Zobrazit celé jméno", + "restrict-chat": "Povolit chatové zprávy jen od uživatelů, které sleduji", + "outgoing-new-tab": "Otevřít odchozí odkazy v nové záložce", + "topic-search": "Povolit hledání v tématu", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Přihlásit k přehledu", + "digest-freq.off": "Vypnuto", + "digest-freq.daily": "Denně", + "digest-freq.weekly": "Týdně", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Měsíčně", + "email-chat-notifs": "Nejsem-li online zaslat e-mail, dorazí-li nová zpráva z chatu", + "email-post-notif": "Zaslat e-mail, objeví-li se odpovědi v tématu, který sleduji", + "follow-created-topics": "Sledovat mnou vytvořená témata", + "follow-replied-topics": "Sledovat témata, na které jste odpověděl", + "default-notification-settings": "Nastavení výchozího oznámení", + "categoryWatchState": "Stav sledování výchozí kategorie", + "categoryWatchState.watching": "Sledování", + "categoryWatchState.notwatching": "Nesleduji", + "categoryWatchState.ignoring": "Ignorace" +} diff --git a/public/language/cs/admin/settings/web-crawler.json b/public/language/cs/admin/settings/web-crawler.json new file mode 100644 index 0000000000..a10912b938 --- /dev/null +++ b/public/language/cs/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Nastavit prohledávatelnost", + "robots-txt": "Upravit Robots.txt Pro výchozí zanechte prázdné", + "sitemap-feed-settings": "Nastavit zdroj a mapu stránky", + "disable-rss-feeds": "Zakázat zdroje RSS", + "disable-sitemap-xml": "Zakázat Sitemap.xml", + "sitemap-topics": "Počet témat zobrazených na mapě stránky", + "clear-sitemap-cache": "Smazat mezipaměť mapy stránky", + "view-sitemap": "Zobrazit mapu stránky" +} \ No newline at end of file diff --git a/public/language/cs/category.json b/public/language/cs/category.json new file mode 100644 index 0000000000..bb53e67664 --- /dev/null +++ b/public/language/cs/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategorie", + "subcategories": "Podkategorie", + "new_topic_button": "Nové téma", + "guest-login-post": "Přihlásit se pro přispívání", + "no_topics": "V této kategorii zatím nejsou žádné příspěvky.
Můžeš být první.", + "browsing": "prohlíží", + "no_replies": "Nikdo ještě neodpověděl", + "no_new_posts": "Žádné nové příspěvky", + "watch": "Sledovat", + "ignore": "Ignorovat", + "watching": "Sledováno", + "not-watching": "Nesledováno", + "ignoring": "Ignorováno", + "watching.description": "Zobrazit témata v nepřečtených a posledních", + "not-watching.description": "Nezobrazovat témata v nepřečtených, zobrazit poslední", + "ignoring.description": "Nezobrazovat témata v nepřečtených a posledních", + "watching.message": "Nyní sledujete aktualizace pro tuto kategorii a všech podkategorii", + "notwatching.message": "Nyní nesledujete aktualizace z této kategorie a všech podkategorií", + "ignoring.message": "Nyní ignorujete aktualizace této kategorie a všech jejich kategorii", + "watched-categories": "Sledované kategorie", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/cs/email.json b/public/language/cs/email.json new file mode 100644 index 0000000000..470ba7eca8 --- /dev/null +++ b/public/language/cs/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test e-mailu", + "password-reset-requested": "Vyžádáno resetování hesla!", + "welcome-to": "Vítejte v %1", + "invite": "Pozvánka od %1", + "greeting_no_name": "Dobrý den", + "greeting_with_name": "Dobrý den %1", + "email.verify-your-email.subject": "Ověřte prosím vaší e-mailovou adresu", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Děkujeme vám za registraci na %1!", + "welcome.text2": "Pro úplnou aktivaci vašeho účtu potřebujeme ověřit vaši e-mailovou adresu.", + "welcome.text3": "Administrátor právě potvrdil vaší registraci. Nyní se můžete přihlásit jménem a heslem.", + "welcome.cta": "Pro potvrzení vaší e-mailové adresy, klikněte zde", + "invitation.text1": "%1 vás pozval, abyste se připojil k %2", + "invitation.text2": "Vaše pozvánky vyprší za %1 dní.", + "invitation.cta": "Pro vytvoření účtu, klikněte zde.", + "reset.text1": "Obdrželi jsme požadavek na obnovu vašeho hesla, pravděpodobně z důvodu jeho zapomenutí. Pokud to není tento případ, ignorujte, prosím, tento e-mail.", + "reset.text2": "Přejete-li si pokračovat v obnově vašeho hesla, klikněte, prosím, na následující odkaz:", + "reset.cta": "Chcete-li obnovit vaše heslo, klikněte zde", + "reset.notify.subject": "Heslo úspěšně změněno", + "reset.notify.text1": "Informujeme Vás, že na %1 vaše heslo bylo úspěšně změněno.", + "reset.notify.text2": "Pokud jste to neschválil, prosíme neprodleně kontaktujte správce.", + "digest.latest_topics": "Nejnovější témata od %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Kliknutím zde navštívíte %1", + "digest.unsub.info": "Tento výtah vám byl odeslán, protože jste si to nastavili ve vašich odběrech.", + "digest.day": "den", + "digest.week": "týden", + "digest.month": "měsíc", + "digest.subject": "Výběr pro %1", + "digest.title.day": "Váš denní přehled", + "digest.title.week": "Váš týdenní přehled", + "digest.title.month": "Váš měsíční přehled", + "notif.chat.subject": "Nová zpráva z chatu od %1", + "notif.chat.cta": "Chcete-li pokračovat v konverzaci, klikněte zde.", + "notif.chat.unsub.info": "Toto upozornění na chat vám bylo odesláno na základě vašeho nastavení odběru.", + "notif.post.unsub.info": "Toto upozornění na příspěvek vám bylo odesláno na základě vašeho nastavení odběru.", + "notif.post.unsub.one-click": "Alternativně odhlášení z rozesílaných e-mailů jako je tento kliknutím", + "notif.cta": "Na fórum", + "notif.cta-new-reply": "Zobrazit příspěvky", + "notif.cta-new-chat": "Zobrazit konverzaci", + "notif.test.short": "Testování oznámení", + "notif.test.long": "Toto je test e-mailového oznámení. Pošlete pro případnou pomoc!", + "test.text1": "Tento testovací e-mail slouží k ověření, že je e-mailer správně nastaven pro práci s NodeBB.", + "unsub.cta": "Chcete-li změnit tyto nastavení, klikněte zde.", + "unsubscribe": "odhlásit", + "unsub.success": "Již nebudete nadále dostávat e-maily z %1", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Byl jste zablokován od %1", + "banned.text1": "Uživatel %1 byl zablokován od %2", + "banned.text2": "Blokace bude trvat do %1", + "banned.text3": "Toto je důvod, proč jste byl zablokován:", + "closing": "Díky." +} \ No newline at end of file diff --git a/public/language/cs/error.json b/public/language/cs/error.json new file mode 100644 index 0000000000..d2c66306cf --- /dev/null +++ b/public/language/cs/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Neplatná data", + "invalid-json": "Neplatný JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Zdá se, že nejste přihlášen/a", + "account-locked": "Váš účet byl dočasně uzamknut", + "search-requires-login": "Pro hledání je vyžadován účet – přihlaste se nebo zaregistrujte.", + "goback": "Pro návrat na předchozí stránku, stiskněte tlačítko „Zpět”", + "invalid-cid": "Neplatné ID kategorie", + "invalid-tid": "Neplatné ID tématu", + "invalid-pid": "Neplatné ID příspěvku", + "invalid-uid": "Neplatné ID uživatele", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Neplatné uživatelské jméno", + "invalid-email": "Neplatný e-mail", + "invalid-fullname": "Neplatný celý název", + "invalid-location": "Neplatné umístění", + "invalid-birthday": "Neplatné narozeniny", + "invalid-title": "Neplatný název", + "invalid-user-data": "Neplatná uživatelská data", + "invalid-password": "Neplatné heslo", + "invalid-login-credentials": "Neplatné přihlašovací údaje", + "invalid-username-or-password": "Zadejte prosím uživatelské jméno a i heslo", + "invalid-search-term": "Neplatný výraz pro vyhledávání", + "invalid-url": "Neplatné URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Systém přihlášení pro místní účty byl zakázán pro neoprávněné účty.", + "csrf-invalid": "Není možné vás přihlásit, díky vypršení relace. Zkuste to prosím znovu.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Neplatná hodnota stránkování, musí být alespoň %1 a nejvýše %2", + "username-taken": "Uživatelské jméno je již použito", + "email-taken": "Tento e-mail je již použit", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Nebude schopen konverzovat, dokud nebude váš e-mail potvrzen. Pro jeho potvrzení klikněte zde.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Nemohli jsme ověřit vaši e-mailovou adresu, zkuste to později.", + "confirm-email-already-sent": "Potvrzovací e-mail byl již odeslán. Vyčkejte %1 minut/y, chcete-li odeslat další.", + "sendmail-not-found": "Modul pro odeslání e-mailů nebyl nalezen. Zkontrolujte prosím, zda je nainstalován a spuštěn uživatelem, který spustil NodeBB.", + "digest-not-enabled": "Tento uživatel nemá povolený odběr přehledu, nebo výchozí systémové hodnoty nejsou nastaveny pro odesílání přehledů", + "username-too-short": "Uživatelské jméno je moc krátké", + "username-too-long": "Uživatelské jméno je moc dlouhé", + "password-too-long": "Heslo je moc dlouhé", + "reset-rate-limited": "Moc požadavků na reset hesla (omezený počet)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Uživatel byl zablokován", + "user-banned-reason": "Omlouváme se, ale tento účet byl zablokován (důvod: %1)", + "user-banned-reason-until": "Omlouváme se, ale tento účet je zablokován do %1 (důvod: %2)", + "user-too-new": "Omlouváme se, ale před vytvoření vašeho prvního příspěvku musíte vyčkat %1 sekund/u/y", + "blacklisted-ip": "Omlouváme se, ale vaše adresa IP byla u této komunity zablokována. Máte-li pocit, že je to chyba, kontaktujte správce.", + "ban-expiry-missing": "Zadejte prosím datum konce této blokace", + "no-category": "Kategorie neexistuje", + "no-topic": "Téma neexistuje", + "no-post": "Příspěvek neexistuje", + "no-group": "Skupina neexistuje", + "no-user": "Uživatel neexistuje", + "no-teaser": "Chyták neexistuje", + "no-flag": "Flag does not exist", + "no-privileges": "Na tuto akci nemáte dostatečné oprávnění.", + "category-disabled": "Kategorie zakázána", + "topic-locked": "Téma uzamknuto", + "post-edit-duration-expired": "Je vám umožněno upravit příspěvky jen po %1 sekund/y od jeho vytvoření", + "post-edit-duration-expired-minutes": "Je vám umožněno upravit příspěvky jen po %1 minut/y od jeho vytvoření", + "post-edit-duration-expired-minutes-seconds": "Je vám umožněno upravit příspěvky jen po %1 minut/y a %2 sekund/y od jeho vytvoření", + "post-edit-duration-expired-hours": "Je vám umožněno upravit příspěvky jen po %1 hodin/u/y od jeho vytvoření", + "post-edit-duration-expired-hours-minutes": "Je vám umožněno upravit příspěvky jen po %1 hodin/u/y %2 minut/y od jeho vytvoření", + "post-edit-duration-expired-days": "Je vám umožněno upravit příspěvky jen po %1 den/y/ů od jeho vytvoření", + "post-edit-duration-expired-days-hours": "Je vám umožněno upravit příspěvky jen po %1 den/y/ů %2 hodin/y od jeho vytvoření", + "post-delete-duration-expired": "Je vám umožněno odstranit příspěvky jen po %1 sekund/y od jeho vytvoření", + "post-delete-duration-expired-minutes": "Je vám umožněno odstranit příspěvky jen po %1 minut/y od jeho vytvoření", + "post-delete-duration-expired-minutes-seconds": "Je vám umožněno odstranit příspěvky jen po %1 minut/y %2 sekund/y od jeho vytvoření", + "post-delete-duration-expired-hours": "Je vám umožněno odstranit příspěvky jen po %1 hodin/y od jeho vytvoření", + "post-delete-duration-expired-hours-minutes": "Je vám umožněno odstranit příspěvky jen po %1 hodin/y %2 minut/y od jeho vytvoření", + "post-delete-duration-expired-days": "Je vám umožněno odstranit příspěvky jen po %1 den/y/ů od jeho vytvoření", + "post-delete-duration-expired-days-hours": "Je vám umožněno odstranit příspěvky jen po %1 den/y/ů %2 hodin/y od jeho vytvoření", + "cant-delete-topic-has-reply": "Nemůžete odstranit vaše téma, po tom co obsahuje odpověď", + "cant-delete-topic-has-replies": "Téma nelze odstranit poté, co obsahuje %1 odpovědí", + "content-too-short": "Zadejte prosím delší příspěvek. Každý příspěvek musí obsahovat alespoň %1 znaků.", + "content-too-long": "Zadejte kratší příspěvek. Příspěvky nesmí být delší než %1 znaků.", + "title-too-short": "Zadejte delší název. Titul by měl obsahovat nejméně %1 znaků.", + "title-too-long": "Zadejte kratší název. Titul by neměl být delší než %1 znaků.", + "category-not-selected": "Nebyla vybrána kategorie.", + "too-many-posts": "Můžete přispívat jednou za %1 sekund - vyčkejte tedy, než vytvoříte další příspěvek", + "too-many-posts-newbie": "Jako nový uživatel, můžete přispívat jednou za %1 sekund, dokud nezískáte pověst %2 - vyčkejte tedy, než vytvoříte další příspěvek", + "already-posting": "You are already posting", + "tag-too-short": "Zadejte delší značku. Značky by měli mít alespoň %1 znaků", + "tag-too-long": "Zadejte kratší značku. Značky nesmí být delší než %1 znaků", + "not-enough-tags": "Málo značek. Téma musí obsahovat alespoň %1 značek", + "too-many-tags": "Příliš mnoho značek. Téma nesmí mít více než %1 značek", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Vyčkejte, než se vše kompletně nahraje.", + "file-too-big": "Maximální povolená velikost je %1 kB – nahrajte menší soubor", + "guest-upload-disabled": "Nahrávání od hostů nebylo povoleno", + "cors-error": "Není možné nahrát obrázek díky špatně nakonfigurovanému „Cross-Origin Resource Sharing (CORS)”", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Již jste tento příspěvek zazáložkoval", + "already-unbookmarked": "Již jste u tohoto příspěvku odebral záložku", + "cant-ban-other-admins": "Nemůžete zablokovat jiné správce.", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Jste jediným správcem. Před vlastním odebráním oprávnění správce nejdříve přidejte jiného uživatele jako správce", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Před odstraněním účtu mu nejprve odeberte oprávnění správce.", + "already-deleting": "Already deleting", + "invalid-image": "Neplatný obrázek", + "invalid-image-type": "Neplatný typ obrázku. Povolené typy jsou: %1", + "invalid-image-extension": "Neplatná přípona obrázku", + "invalid-file-type": "Neplatný typ souboru. Povolené typy jsou: %1", + "invalid-image-dimensions": "Rozlišení obrázku je moc velké.", + "group-name-too-short": "Název skupiny je moc krátký", + "group-name-too-long": "Název skupiny je moc dlouhý", + "group-already-exists": "Skupina už existuje", + "group-name-change-not-allowed": "Změna názvu skupiny není povolena", + "group-already-member": "Již je součástí této skupiny", + "group-not-member": "Není součástí této skupiny", + "group-needs-owner": "Tato skupina vyžaduje nejméně jednoho vlastníka", + "group-already-invited": "Tento uživatel již byl pozván", + "group-already-requested": "Váš požadavek o členství již byl odeslán", + "group-join-disabled": "V tuto chvíli se nemůžete připojit k této skupině", + "group-leave-disabled": "V tuto chvíli nemůžete opustit tuto skupinu", + "post-already-deleted": "Tento příspěvek byl již odstraněn", + "post-already-restored": "Tento příspěvek byl již obnoven", + "topic-already-deleted": "Toto téma bylo již odstraněno", + "topic-already-restored": "Toto téma bylo již obnoveno", + "cant-purge-main-post": "Nemůžete vymazat hlavní příspěvek, místo toho odstraňte téma", + "topic-thumbnails-are-disabled": "Miniatury témat jsou zakázány.", + "invalid-file": "Neplatný soubor", + "uploads-are-disabled": "Nahrávání je zakázáno", + "signature-too-long": "Omlouváme se, ale podpis nesmí být delší než %1 znaků.", + "about-me-too-long": "Omlouváme se, ale \"O mně\" nesmí být delší než %1 znaků.", + "cant-chat-with-yourself": "Nemůžete konverzovat sami se sebou.", + "chat-restricted": "Tento uživatel má omezené konverzační zprávy. Nejdříve vás musí začít sledovat, než začnete spolu konverzovat", + "chat-disabled": "Konverzační systém zakázán", + "too-many-messages": "Odeslal/a jste příliš mnoho zpráv, vyčkejte chvíli.", + "invalid-chat-message": "Neplatná konverzační zpráva", + "chat-message-too-long": "Konverzační zprávy nemohou být delší než %1 znaků.", + "cant-edit-chat-message": "Tuto zprávu nemůžete upravit", + "cant-delete-chat-message": "Tuto zprávu nemůžete odstranit", + "chat-edit-duration-expired": "Je vám umožněno upravit konverzační zprávy pod dobu %1 sekund/y po jejich odeslání", + "chat-delete-duration-expired": "Je vám umožněno odstranit konverzační zprávy pod dobu %1 sekund/y po jejich odeslání", + "chat-deleted-already": "Tato konverzační zpráva již byla odstraněna.", + "chat-restored-already": "Tato konverzační zpráva již byla obnovena.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Již jste v tomto příspěvku hlasoval.", + "reputation-system-disabled": "Systém reputací je zakázán.", + "downvoting-disabled": "Systém nesouhlasu je zakázán", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "U svého vlastního příspěvku nemůžete hlasovat", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "Vyskytla se chyba v NodeBB při znovu načtení: \"%1\". NodeBB bude pokračovat v běhu na straně klienta, nicméně byste měl/a přenastavit zpět to, co jste udělal/a před opětovným načtením.", + "registration-error": "Chyba při registraci", + "parse-error": "Při analýze odpovědi serveru nastala chyba", + "wrong-login-type-email": "Pro přihlášení použijte vaši e-mailovou adresu", + "wrong-login-type-username": "Pro přihlášení použijte vaše uživatelské jméno", + "sso-registration-disabled": "Registrace byla zakázána pro účty - %1. Nejprve si zaregistrujte e-mailovou adresu", + "sso-multiple-association": "Není možné přiřadit více účtů z této služby do vašeho účtu NodeBB. Vylučte váš existující účet a zkuste to znovu.", + "invite-maximum-met": "Již jste pozval/a maximálně možný počet lidí (%1 z %2).", + "no-session-found": "Nebyla nalezena relace s přihlášením.", + "not-in-room": "Uživatel není přítomen v místnosti", + "cant-kick-self": "Nemůžete vyhodit sami sebe ze skupiny", + "no-users-selected": "Žádný uživatel/é nebyl/y vybrán/i", + "invalid-home-page-route": "Neplatná cesta k domovské stránkce", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Žádná vybraná témata.", + "cant-move-to-same-topic": "Není možné přesunout příspěvek do stejného tématu!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Nemůžete zablokovat sebe sama!", + "cannot-block-privileged": "Nemůžete zablokovat správce nebo hlavní moderátory", + "cannot-block-guest": "Hosté nemohou blokovat ostatní uživatele.", + "already-blocked": "Tento uživatel již byl zablokován.", + "already-unblocked": "Tento uživatel již byl odblokován", + "no-connection": "Zdá se, že nastal problém s připojením k internetu", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/cs/flags.json b/public/language/cs/flags.json new file mode 100644 index 0000000000..d20bbdf652 --- /dev/null +++ b/public/language/cs/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Stav", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hurá, žádné označení.", + "assignee": "Nabyvatel", + "update": "Aktualizovat", + "updated": "Aktualizováno", + "resolved": "Resolved", + "target-purged": "Obsah, na který se toto označení vztahuje, byl vymazán a již není k dispozici.", + + "graph-label": "Denní označení", + "quick-filters": "Rychlé filtry", + "filter-active": "V tomto seznamu označení je jeden nebo více aktivních filtrů", + "filter-reset": "Vyjmout filtry", + "filters": "Možnosti filtru", + "filter-reporterId": "UID ohlašovatele", + "filter-targetUid": "UID označení", + "filter-type": "Typ označení", + "filter-type-all": "Všechen obsah", + "filter-type-post": "Příspěvek", + "filter-type-user": "Uživatel", + "filter-state": "Stav", + "filter-assignee": "UID nabyvatele", + "filter-cid": "Kategorie", + "filter-quick-mine": "Přiřazeno mě", + "filter-cid-all": "Všechny kategorie", + "apply-filters": "Použít filtry", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Označený uživatel", + "view-profile": "Zobrazit profil", + "start-new-chat": "Začít novou konverzaci", + "go-to-target": "Zobrazit cílové označení", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Zobrazit profil", + "user-edit": "Upravit profil", + + "notes": "Poznámky označení", + "add-note": "Přidat poznámku", + "no-notes": "Žádné sdílené poznámky.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Poznámka přidána", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Žádná historie označení.", + + "state-all": "Všechny stavy", + "state-open": "Nové/Otevřít", + "state-wip": "Pracujeme na tom", + "state-resolved": "Vyřešeno", + "state-rejected": "Zamítnuto", + "no-assignee": "Nepřiřazeno", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Zadejte váš důvod k označení %1 %2 pro kontrolu. Nebo použijte tlačítko je-li dostupné.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Urážlivé", + "modal-reason-other": "Jiné (popište níže)", + "modal-reason-custom": "Důvod ohlášení tohoto obsahu…", + "modal-submit": "Předat hlášení", + "modal-submit-success": "Obsah byl označen pro moderaci.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/cs/global.json b/public/language/cs/global.json new file mode 100644 index 0000000000..73dfef6204 --- /dev/null +++ b/public/language/cs/global.json @@ -0,0 +1,126 @@ +{ + "home": "Domů", + "search": "Hledat", + "buttons.close": "Zavřít", + "403.title": "Přístup odepřen", + "403.message": "Zdá se, že jste narazil/a na stránky na které nemáte přístup.", + "403.login": "Možná byste měli se zkusit přihlásit?", + "404.title": "Stránka nenalezena", + "404.message": "Zdá se, že jste narazil/a na stránku která neexistuje. Vrátit se zpět na domovskou stránku.", + "500.title": "Interní chyba", + "500.message": "Jejda, vypadá to, že se něco pokazilo.", + "400.title": "Špatný požadavek.", + "400.message": "Zdá se, že tento odkaz není správny, prosím zkontrolujte ho a zkuste to znovu. Jinak, vraťte se na Domácí stránku.", + "register": "Registrovat", + "login": "Přihlásit se", + "please_log_in": "Přihlašte se, prosím", + "logout": "Odhlásit se", + "posting_restriction_info": "V současné době je zasílání příspěvků povoleno pouze registrovaným členům, klikněte zde a přihlašte se.", + "welcome_back": "Vítejte zpět", + "you_have_successfully_logged_in": "Vaše přihlášení proběhlo úspěšně", + "save_changes": "Uložit změny", + "save": "Uložit", + "close": "Zrušit", + "pagination": "Stránkování", + "pagination.out_of": "%1 z %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Administrace", + "header.categories": "Kategorie", + "header.recent": "Nejnovější", + "header.unread": "Nepřečtené", + "header.tags": "Značky", + "header.popular": "Populární", + "header.top": "Top", + "header.users": "Uživatelé", + "header.groups": "Skupiny", + "header.chats": "Chaty", + "header.notifications": "Upozornění", + "header.search": "Hledat", + "header.profile": "Profil", + "header.navigation": "Navigace", + "notifications.loading": "Načítání upozornění", + "chats.loading": "Načítání chatů", + "motd.welcome": "Vítejte na NodeBB, diskusní platforma buducnosti.", + "previouspage": "Předchozí stránka", + "nextpage": "Další stránka", + "alert.success": "Úspěšné", + "alert.error": "Chyba", + "alert.banned": "Zabanován", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Již nesledujete %1!", + "alert.follow": "Nyní sledujete %1!", + "users": "Uživatelé", + "topics": "Témata", + "posts": "Příspěvky", + "x-posts": "%1 posts", + "best": "Nejlepší", + "controversial": "Controversial", + "votes": "Počet hlasů", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Souhlasník", + "upvoted": "Souhlasů", + "downvoters": "Nesouhlasník", + "downvoted": "Nesouhlasů", + "views": "Zobrazení", + "posters": "Posters", + "reputation": "Reputace", + "lastpost": "Poslední příspěvek", + "firstpost": "První příspěvek", + "read_more": "čtěte více", + "more": "Více", + "none": "None", + "posted_ago_by_guest": "přispěl %1 host", + "posted_ago_by": "přispěl %1 od %2", + "posted_ago": "přispěl %1", + "posted_in": "přispěno v %1", + "posted_in_by": "přispěno v %1 od %2", + "posted_in_ago": "přispěno v %1 %2", + "posted_in_ago_by": "přispěl v %1 %2 od %3", + "user_posted_ago": "%1 přispěl %2", + "guest_posted_ago": "Host přispěl %1", + "last_edited_by": "naposledy upravil %1", + "norecentposts": "Žádné nedávné příspěvky", + "norecenttopics": "Žádné nedávné témata", + "recentposts": "Nedávné příspěvky", + "recentips": "Naposledy zaznamenané IP adresy", + "moderator_tools": "Nástroje moderátora", + "online": "Online", + "away": "Pryč", + "dnd": "Nevyrušovat", + "invisible": "Neviditelný", + "offline": "Offline", + "email": "E-mail", + "language": "Jazyk", + "guest": "Host", + "guests": "Hosté", + "former_user": "Bývalý uživatel", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Fórum bylo zaktualizováno", + "updated.message": "Toto fórum bylo právě aktualizováno na poslední verzi. Klikněte zde a obnovte tuto stránku.", + "privacy": "Soukromí", + "follow": "Sledovat", + "unfollow": "Prestat sledovat", + "delete_all": "Vymazat vše", + "map": "Mapa", + "sessions": "Relace přihlášení", + "ip_address": "IP adresa", + "enter_page_number": "Zadejte číslo stránky", + "upload_file": "Nahrár soubor", + "upload": "Nahrát", + "uploads": "Náhráno", + "allowed-file-types": "Povolené typy souborů jsou %1", + "unsaved-changes": "Některé změny nebyly uloženy. Jste si jist, že chcete jít jinam?", + "reconnecting-message": "Vypadá to, že vaše připojení k %1 bylo ukončeno. Vyčkejte prosím, než obnovíme připojení.", + "play": "Přehrát", + "cookies.message": "Pro využití plné funkčnosti stránek, jsou použity „cookies”.", + "cookies.accept": "Rozumím.", + "cookies.learn_more": "Zjistit více", + "edited": "Upraveno", + "disabled": "Nepovoleno", + "select": "Vyberte", + "user-search-prompt": "Pro hledání uživatelů, zde pište..." +} \ No newline at end of file diff --git a/public/language/cs/groups.json b/public/language/cs/groups.json new file mode 100644 index 0000000000..92af62ce0b --- /dev/null +++ b/public/language/cs/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Skupiny", + "view_group": "Zobrazit skupinu", + "owner": "Vlastník skupiny", + "new_group": "Vytvořit novou skupinu", + "no_groups_found": "Žádné skupiny k prohlížení", + "pending.accept": "Přijmout", + "pending.reject": "Odmítnout", + "pending.accept_all": "Přijmout vše", + "pending.reject_all": "Odmítnout vše", + "pending.none": "Žádní čekající členové v tuto chvíli", + "invited.none": "Žádní pozvaní členové v tuto chvíli", + "invited.uninvite": "Zrušit pozvánku", + "invited.search": "Hledat uživatele k pozvání do této skupiny", + "invited.notification_title": "Byl jste pozván abyste se připojil/a k %1", + "request.notification_title": "Požadavek na členství ve skupině od %1", + "request.notification_text": "%1 požádál o členství v %2", + "cover-save": "Uložit", + "cover-saving": "Ukládám", + "details.title": "Podrobnosti skupiny", + "details.members": "Seznam členů", + "details.pending": "Čekající členové", + "details.invited": "Pozvaní členové", + "details.has_no_posts": "Členové této skupiny dosud neodeslali ani jeden příspěvek.", + "details.latest_posts": "Nejnovější příspěvky", + "details.private": "Soukromé", + "details.disableJoinRequests": "Zakázat žádosti o připojení", + "details.disableLeave": "Nedovolit uživatelům upustit skupinu", + "details.grant": "Přidat/Zrušit vlastnictví", + "details.kick": "Vyhodit", + "details.kick_confirm": "Jste si jist/a, že chcete vyjmout tohoto uživatele ze skupiny?", + "details.add-member": "Přidat uživatele", + "details.owner_options": "Správa skupiny", + "details.group_name": "Název skupiny", + "details.member_count": "Počet členů", + "details.creation_date": "Datum vytvoření", + "details.description": "Popis", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Náhled symbolu", + "details.change_icon": "Změnit ikonu", + "details.change_label_colour": "Změnit barvu popisu", + "details.change_text_colour": "Změnit barvu textu", + "details.badge_text": "Text odznaku", + "details.userTitleEnabled": "Zobrazit odznak", + "details.private_help": "Je-li povoleno, připojování do skupin vyžaduje schválení od vlastníka skupiny", + "details.hidden": "Skrytý", + "details.hidden_help": "Je-li povoleno, tato skupina nebude zobrazena v seznamu skupin, uživatelé budou muset být pozváni ručně", + "details.delete_group": "Odstranit skupinu", + "details.private_system_help": "Soukromé skupiny jsou zakázány na systémové úrovni, tato možnost nebude mít žádný vliv", + "event.updated": "Podrobnosti skupiny byly aktualizovány", + "event.deleted": "Skupina \"%1\" byla odstraněna", + "membership.accept-invitation": "Přijmout pozvání", + "membership.accept.notification_title": "Nyní jste členem %1", + "membership.invitation-pending": "Čekající pozvání", + "membership.join-group": "Vstoupit do skupiny", + "membership.leave-group": "Opustit skupinu", + "membership.leave.notification_title": "%1 opustit skupinu %2", + "membership.reject": "Odmítnout", + "new-group.group_name": "Název skupiny:", + "upload-group-cover": "Nahrát titulní obrázek skupiny", + "bulk-invite-instructions": "Pro pozvání do skupiny, zadejte jména uživatelů oddělených čárkou", + "bulk-invite": "Hromadná pozvánka", + "remove_group_cover_confirm": "Jste si jist/a, že chcete smazat obrázek?" +} \ No newline at end of file diff --git a/public/language/cs/ip-blacklist.json b/public/language/cs/ip-blacklist.json new file mode 100644 index 0000000000..3b2e4e4002 --- /dev/null +++ b/public/language/cs/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Zde nastavte černou listinu IP", + "description": "Občas zablokování účtu uživatele nestačí. Někdy je nejlepším způsobem ochrany fóra omezení přístupu pro určitou IP adresu nebo celý rozsah IP. V takovém případě můžete přidat problémové IP adresy nebo celé bloky CIDR do této černé listiny a tím jim bude zabráněno přihlásit se či zaregistrovat nový účet.", + "active-rules": "Aktivní pravidla", + "validate": "Potvrdit černou listinu", + "apply": "Použít černou listinu", + "hints": "Syntaxe rad", + "hint-1": "Určete jednotlivou IP adresu na řádek. Můžete přidat IP bloky splňují-li formát CIDR (tj. 192.168.100.0/22).", + "hint-2": "Můžete přidat i komentáře, bude-li řádek začínat symbolem #.", + + "validate.x-valid": "%1 z %2 pravidel je platných.", + "validate.x-invalid": "Následujících %1 pravidel není platných:", + + "alerts.applied-success": "Černá listina byla použita", + + "analytics.blacklist-hourly": "Postava 1 – záznamů v černé listině/hodinu", + "analytics.blacklist-daily": "Postava 2 – záznamů v černé listině/den", + "ip-banned": "IP zakázáno" +} \ No newline at end of file diff --git a/public/language/cs/language.json b/public/language/cs/language.json new file mode 100644 index 0000000000..d2a404b6f7 --- /dev/null +++ b/public/language/cs/language.json @@ -0,0 +1,5 @@ +{ + "name": "Czech", + "code": "cs", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/cs/login.json b/public/language/cs/login.json new file mode 100644 index 0000000000..49463eae9d --- /dev/null +++ b/public/language/cs/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Uživatelské jméno / e-mail", + "username": "Uživatel", + "remember_me": "Zapamatovat si mě?", + "forgot_password": "Zapomněli jste heslo?", + "alternative_logins": "Další způsoby přihlášení", + "failed_login_attempt": "Přihlášení neúspěšné", + "login_successful": "Přihlášení proběhlo úspěšně!", + "dont_have_account": "Nemáte účet?", + "logged-out-due-to-inactivity": "Z důvodu nečinnosti jste byl odhlášen z ovládacího panelu administrátora", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json new file mode 100644 index 0000000000..f48d1d165a --- /dev/null +++ b/public/language/cs/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Konverzace s", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Odeslat", + "chat.no_active": "Nemáte žádné aktivní konverzace.", + "chat.user_typing": "%1 píše…", + "chat.user_has_messaged_you": "%1 Vám napsal.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Vyberte příjemce k prohlédnutí historie zpráv.", + "chat.no-users-in-room": "Žádní uživatelé v místnosti.", + "chat.recent-chats": "Aktuální konverzace", + "chat.contacts": "Kontakty", + "chat.message-history": "Historie zpráv", + "chat.message-deleted": "Message Deleted", + "chat.options": "Možnosti konverzace", + "chat.pop-out": "Skrýt konverzaci", + "chat.minimize": "Minimalizovat", + "chat.maximize": "Maximalizovat", + "chat.seven_days": "7 dní", + "chat.thirty_days": "30 dní", + "chat.three_months": "3 měsíce", + "chat.delete_message_confirm": "Jste si jist/a, že chcete odstranit tuto zprávu?", + "chat.retrieving-users": "Získávání seznamu uživatelů...", + "chat.manage-room": "Spravovat konverzační místnosti", + "chat.add-user-help": "Zde můžete vyhledávat uživatele. Jakmile si ho vyberete, uživatel bude přidán do konverzace. Nový uživatel nebude mít zobrazeny zprávy konverzace napsané dříve, než byl do konverzace přidán. Jen majitelé místnosti () mohou odebrat uživatele z konverzační místnosti.", + "chat.confirm-chat-with-dnd-user": "Tento uživatel nastavil svůj stav na NERUŠIT. Opravdu chcete začít s ním konverzaci.", + "chat.rename-room": "Přejmenovat místnost", + "chat.rename-placeholder": "Zde zadejte název místnosti", + "chat.rename-help": "Název místnosti zde nastavený bude viditelný pro všechny účastníky komunikace v místnosti", + "chat.leave": "Opustit konverzaci", + "chat.leave-prompt": "Jste si jist/a, že chcete ukončit tuto konverzaci?", + "chat.leave-help": "Ukončením této konverzace budete vyjmuti z budoucí možné komunikace v této konverzaci. Následně budete-li znovu přidán/a, neuvidíte historii komunikace od Vašeho odchodu.", + "chat.in-room": "V této místnosti", + "chat.kick": "Vykopnout", + "chat.show-ip": "Zobrazit IP", + "chat.owner": "Majitel místnosti", + "chat.system.user-join": "%1 se připojil k místnosti", + "chat.system.user-leave": "%1 opustil místnost", + "chat.system.room-rename": "%2 přejmenoval tuto místnost: %1", + "composer.compose": "Napsat", + "composer.show_preview": "Ukázat náhled", + "composer.hide_preview": "Skrýt náhled", + "composer.user_said_in": "%1 řekl v %2:", + "composer.user_said": "%1 řekl:", + "composer.discard": "Jste si jisti, že chcete zrušit tento příspěvek?", + "composer.submit_and_lock": "Potvrdit a uzamknout", + "composer.toggle_dropdown": "Rozbalovací nabídka", + "composer.uploading": "Nahrávám %1", + "composer.formatting.bold": "Tučné", + "composer.formatting.italic": "Kurzíva", + "composer.formatting.list": "Seznam", + "composer.formatting.strikethrough": "Přeškrtnutí", + "composer.formatting.code": "Kód", + "composer.formatting.link": "Odkaz", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Nahrát obrázek", + "composer.upload-file": "Nahrát soubor", + "composer.zen_mode": "Režim Zem", + "composer.select_category": "Vyberte kategorii", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Zrušit", + "bootbox.confirm": "Potvrdit", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Umístění fotografie", + "cover.dragging_message": "Přesuňte fotku na požadovanou pozici a klikněte na „Uložit”", + "cover.saved": "Fotografie a její umístění uloženo", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json new file mode 100644 index 0000000000..11af95b03f --- /dev/null +++ b/public/language/cs/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Upozornění", + "no_notifs": "Nemáte žádná nová upozornění.", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Zpět na %1", + "outgoing_link": "Odkaz mimo fórum", + "outgoing_link_message": "Opouštíte %1", + "continue_to": "Pokračovat na %1", + "return_to": "Vrátit se na %1", + "new_notification": "Máte nové upozornění", + "you_have_unread_notifications": "Máte nepřečtená upozornění.", + "all": "Vše", + "topics": "Témata", + "replies": "Odpovědi", + "chat": "Konverzace", + "group-chat": "Group Chats", + "follows": "Sledování", + "upvote": "Souhlasy", + "new-flags": "Nové označení", + "my-flags": "Označení přiřazené mě", + "bans": "Blokace", + "new_message_from": "Nová zpráva od %1", + "upvoted_your_post_in": "%1 souhlasil s vaším příspěvkem v %2.", + "upvoted_your_post_in_dual": "%1 a %2 souhlasili s vaším příspěvkem v %3.", + "upvoted_your_post_in_multiple": "%1 a %2 další/ch souhlasilo s vaším příspěvkem v %3.", + "moved_your_post": "%1 přesunul váš příspěvek do %2", + "moved_your_topic": "%1 přesunul %2", + "user_flagged_post_in": "%1 označil příspěvek v %2", + "user_flagged_post_in_dual": "%1 a %2 označil příspěvek v %3", + "user_flagged_post_in_multiple": "%1 a %2 další/ch označili příspěvěk v %3", + "user_flagged_user": "%1 označil uživatelský profil (%2)", + "user_flagged_user_dual": "%1 a %2 označili uživatelský profil (%3)", + "user_flagged_user_multiple": "%1 a %2 další/ch označili uživatelský profil (%3)", + "user_posted_to": "%1 odpověděl na: %2", + "user_posted_to_dual": "%1%2 odpověděli na: %3", + "user_posted_to_multiple": "%1 a %2 další/ch odpověděli na %3", + "user_posted_topic": "%1 založil nové téma: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 vás začal sledovat.", + "user_started_following_you_dual": "%1 a %2 vás začali sledovat.", + "user_started_following_you_multiple": "%1 a %2 další/ch vás začali sledovat.", + "new_register": "%1 odeslal registrační požadavek.", + "new_register_multiple": "Je zde %1 registračních požadavků čeká na vyřízení.", + "flag_assigned_to_you": "Označení %1 vám bylo přiřazeno", + "post_awaiting_review": "Příspěvek na schválení", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-mail potvrzen", + "email-confirmed-message": "Děkujeme za ověření vaší e-mailové adresy. Váš účet je nyní aktivní.", + "email-confirm-error-message": "Nastal problém s ověřením vaší e-mailové adresy. Kód je pravděpodobně neplatný nebo jeho platnost vypršela.", + "email-confirm-sent": "Ověřovací e-mail odeslán.", + "none": "Nic", + "notification_only": "Jen oznámení", + "email_only": "Jen e-mail", + "notification_and_email": "Oznámení a e-mail", + "notificationType_upvote": "Vyjádří-li někdo souhlas s vaším příspěvkem", + "notificationType_new-topic": "Začne-li někdo sledovat příspěvky a téma", + "notificationType_new-reply": "Bude-li přidán nový příspěvek v tématu, které sledujete", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "Začne-li vás někdo sledovat", + "notificationType_new-chat": "Obdržíte-li novou konverzační zprávu", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "Obdržíte-li pozvání ke skupině", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "Pokud někdo požaduje připojení se do vaší skupiny", + "notificationType_new-register": "Bude-li někdo přidán do registrační fronty", + "notificationType_post-queue": "Bude-li přidán nový příspěvek do fronty", + "notificationType_new-post-flag": "Bude-li příspěvek označen", + "notificationType_new-user-flag": "Bude-li uživatel označen" +} \ No newline at end of file diff --git a/public/language/cs/pages.json b/public/language/cs/pages.json new file mode 100644 index 0000000000..8b3e38baa0 --- /dev/null +++ b/public/language/cs/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Domů", + "unread": "Nepřečtená témata", + "popular-day": "Dnešní oblíbená témata", + "popular-week": "Oblíbená témata pro tento týden", + "popular-month": "Oblíbená témata pro tento měsíc", + "popular-alltime": "Oblíbená témata za celou dobu", + "recent": "Aktuální témata", + "top-day": "Dnešní témata s nejvíce souhlasy", + "top-week": "Týdenní témata s nejvíce souhlasy", + "top-month": "Měsíční témata s nejvíce souhlasy", + "top-alltime": "Témata s nejvíce souhlasy", + "moderator-tools": "Nástroje moderátora", + "flagged-content": "Nahlášený obsah", + "ip-blacklist": "Černá listina IP adres", + "post-queue": "Fronta příspěvků", + "users/online": "Připojení uživatelé", + "users/latest": "Nejnovější uživatelé", + "users/sort-posts": "Uživatelé s nejvíce příspěvky", + "users/sort-reputation": "Uživatelé s nejvyšší reputací", + "users/banned": "Zablokovaní uživatelé", + "users/most-flags": "Nejoznačovanější uživatelé", + "users/search": "Hledat uživatele", + "notifications": "Upozornění", + "tags": "Značky", + "tag": "Témata označená "%1"", + "register": "Zaregistrovat účet", + "registration-complete": "Registrace dokončena", + "login": "Přihlásit se ke svému účtu", + "reset": "Obnovit heslo k účtu", + "categories": "Kategorie", + "groups": "Skupiny", + "group": "%1 skupina", + "chats": "Konverzace", + "chat": "Konverzace s %1", + "flags": "Označení", + "flag-details": "Detaily označení %1", + "account/edit": "Úprava \"%1\"", + "account/edit/password": "Úprava hesla \"%1\"", + "account/edit/username": "Úprava jména uživatele \"%1\"", + "account/edit/email": "Úprava e-mailu \"%1\"", + "account/info": "Informace o účtu", + "account/following": "Sleduje %1 lidí", + "account/followers": "Lidé kteří sledují %1", + "account/posts": "Příspěvky od %1", + "account/latest-posts": "Poslední příspěvek od %1", + "account/topics": "Příspěvky vytvořeny uživatelem %1", + "account/groups": "%1's skupiny", + "account/watched_categories": "%1's sledovaných kategorii", + "account/bookmarks": "%1's zazáložkované příspěvky", + "account/settings": "Uživatelské nastavení", + "account/watched": "Témata sledovaná uživatelem %1", + "account/ignored": "Témata ignorovaná uživatelem %1", + "account/upvoted": "Souhlasí s příspěvkem %1", + "account/downvoted": "Nesouhlasí s příspěvkem %1", + "account/best": "Nejlepší příspěvky od %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Zablokovaní uživatelé z %1", + "account/uploads": "Nahráno od %1", + "account/sessions": "Relace s přihlášením", + "confirm": "E-mail potvrzen", + "maintenance.text": "%1 momentálně prochází údržbou. Vraťte se později.", + "maintenance.messageIntro": "Správce zanechal tuto zprávu:", + "throttled.text": "%1 je v současnou chvíli nedostupný pro velkou zátěž. Zkuste to později." +} \ No newline at end of file diff --git a/public/language/cs/post-queue.json b/public/language/cs/post-queue.json new file mode 100644 index 0000000000..32f542129d --- /dev/null +++ b/public/language/cs/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Fronta příspěvků", + "description": "Nejsou žádné příspěvky ve frontě. Pro povolení této funkčnosti, přejděte do Nastavení – Příspěvky – Fronta příspěvků a povolte Fronta příspěvků.", + "user": "Uživatel", + "category": "Kategorie", + "title": "Název", + "content": "Obsah", + "posted": "Přidáno", + "reply-to": "Odpovědět na \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/cs/recent.json b/public/language/cs/recent.json new file mode 100644 index 0000000000..34c0235f91 --- /dev/null +++ b/public/language/cs/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Nedávné", + "day": "Den", + "week": "Týden", + "month": "Měsíc", + "year": "Rok", + "alltime": "Pořád", + "no_recent_topics": "Nebyly nalezeny žádné nové téma.", + "no_popular_topics": "Žádná oblíbená téma.", + "there-is-a-new-topic": "K dispozici je nové téma.", + "there-is-a-new-topic-and-a-new-post": "K dispozici je nové téma a nový příspěvěk.", + "there-is-a-new-topic-and-new-posts": "K dispozici je nové téma a %1 nových příspěvků.", + "there-are-new-topics": "K dispozici je %1 nových témat.", + "there-are-new-topics-and-a-new-post": "K dispozici je %1 nových témat a jeden nový příspěvek.", + "there-are-new-topics-and-new-posts": "K dispozici je %1 nových témat a %2 nových příspěvků.", + "there-is-a-new-post": "K dispozici je nový příspěvek.", + "there-are-new-posts": "K dispozici je %1 nových příspěvků.", + "click-here-to-reload": "Kliknutím sem znovu načtete stránku." +} \ No newline at end of file diff --git a/public/language/cs/register.json b/public/language/cs/register.json new file mode 100644 index 0000000000..365e4c1a48 --- /dev/null +++ b/public/language/cs/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registrace", + "cancel_registration": "Zrušit registraci", + "help.email": "Ve výchozím nastavení bude váš e-mail skrytý.", + "help.username_restrictions": "Jedinečné uživatelské jméno dlouhé %1 až %2 znaků. Ostatní uživatelé Vás mohou zmínit jako @uživatelské jméno.", + "help.minimum_password_length": "Délka vašeho hesla musí být alespoň %1 znaků.", + "email_address": "E-mailová adresa", + "email_address_placeholder": "Zadejte e-mailovou adresu", + "username": "Uživatelské jméno", + "username_placeholder": "Zadejte uživatelské jméno", + "password": "Heslo", + "password_placeholder": "Zadejte heslo", + "confirm_password": "Potvrzení hesla", + "confirm_password_placeholder": "Potvrďte heslo", + "register_now_button": "Zaregistrovat se", + "alternative_registration": "Jiný způsob registrace", + "terms_of_use": "Podmínky", + "agree_to_terms_of_use": "Souhlasím s Podmínkami", + "terms_of_use_error": "Musíte souhlasit s podmínkami.", + "registration-added-to-queue": "Vaše registrace byla přidána do fronty. Obdržíte e-mail až ji správce schválí.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Dávám souhlas se sběrem a zpracováním mých osobních údajů na této webové stránce.", + "gdpr_agree_email": "Dávám souhlas k dostávání e-mailových přehledů a oznámení z týkající se této webové stránky.", + "gdpr_consent_denied": "Musíte dát souhlas této stránce sbírat/zpracovávat informace o vaší činnosti a odesílat vám e-maily.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/cs/reset_password.json b/public/language/cs/reset_password.json new file mode 100644 index 0000000000..4ac7bf3446 --- /dev/null +++ b/public/language/cs/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Obnovit heslo", + "update_password": "Upravit heslo", + "password_changed.title": "Heslo změněno", + "password_changed.message": "

Heslo bylo úspěšně změněno, přihlaste se znovu.", + "wrong_reset_code.title": "Špatný kód", + "wrong_reset_code.message": "Byl zadán špatný kód. Zadejte ho prosím znovu, nebo si nechte poslat nový.", + "new_password": "Nové heslo", + "repeat_password": "Potvrzení hesla", + "changing_password": "Changing Password", + "enter_email": "Zadejte svou e-mailovou adresu a my vám pošleme informace, jak můžete obnovit svůj účet.", + "enter_email_address": "Zadejte e-mailovou adresu", + "password_reset_sent": "Odpovídá-li zadaná adresa existujícímu uživatelskému účtu, byl odeslát e-mail s resetovaným heslem.\nMějte na paměti, že může být odeslán pouze jeden e-mail/minutu.", + "invalid_email": "Neplatný e-mail / E-mail neexistuje.", + "password_too_short": "Zadané heslo je příliš krátké, zvolte si prosím jiné.", + "passwords_do_not_match": "Vámi zadaná hesla se neshodují.", + "password_expired": "Platnost Vašeho hesla vypršela, zvolte si prosím nové." +} \ No newline at end of file diff --git a/public/language/cs/search.json b/public/language/cs/search.json new file mode 100644 index 0000000000..8c64002155 --- /dev/null +++ b/public/language/cs/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "Počet výsledků pro „%2“: %1, (%3 sekund)", + "no-matches": "Nic nebylo nalezeno", + "advanced-search": "Pokročilé hledání", + "in": "v", + "titles": "Název", + "titles-posts": "Název a příspěvky", + "match-words": "Shodná slova", + "all": "Vše", + "any": "Jakékoliv", + "posted-by": "Napsal", + "in-categories": "V kategoriích", + "search-child-categories": "Hledat podružné kategorie", + "has-tags": "Obsahuje značky", + "reply-count": "Počet odpovědí", + "at-least": "Nejméně", + "at-most": "Nejvíce", + "relevance": "Relevantnost", + "post-time": "Čas příspěvku", + "votes": "Hlasů", + "newer-than": "Novější než", + "older-than": "Starší než", + "any-date": "Jakékoliv datum", + "yesterday": "Včera", + "one-week": "Jeden týden", + "two-weeks": "Dva týdny", + "one-month": "Jeden měsíc", + "three-months": "Tři měsíce", + "six-months": "Šest měsíců", + "one-year": "Jeden rok", + "sort-by": "Řadit dle", + "last-reply-time": "Čas poslední odpovědi", + "topic-title": "Název tématu", + "topic-votes": "Hlasy tématu", + "number-of-replies": "Počet odpovědí", + "number-of-views": "Počet zobrazení", + "topic-start-date": "Počáteční datum tématu", + "username": "Uživatelské jméno", + "category": "Kategorie", + "descending": "Sestupně", + "ascending": "Vzestupně", + "save-preferences": "Uložit nastavení", + "clear-preferences": "Smazat nastavení", + "search-preferences-saved": "Hledat dle uložených nastavení", + "search-preferences-cleared": "Hledat dle smazaných nastavení", + "show-results-as": "Zobrazit výsledek jako", + "see-more-results": "Zobrazit více výsledků (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/cs/success.json b/public/language/cs/success.json new file mode 100644 index 0000000000..b1f996e509 --- /dev/null +++ b/public/language/cs/success.json @@ -0,0 +1,7 @@ +{ + "success": "Úspěšné", + "topic-post": "Příspěvek úspěšně přidán.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Úspěšné přihlášení", + "settings-saved": "Nastavení byla uložena." +} \ No newline at end of file diff --git a/public/language/cs/tags.json b/public/language/cs/tags.json new file mode 100644 index 0000000000..459d59b17c --- /dev/null +++ b/public/language/cs/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Není zde žádné téma s tímto označením.", + "tags": "Označení", + "enter_tags_here": "Zde vložte označení, každé o délce %1 až %2 znaků.", + "enter_tags_here_short": "Zadejte označení…", + "no_tags": "Zatím tu není žádné označení.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/cs/top.json b/public/language/cs/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/cs/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json new file mode 100644 index 0000000000..1631714e90 --- /dev/null +++ b/public/language/cs/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Téma", + "title": "Title", + "no_topics_found": "Nebyla nalezena žádná témata.", + "no_posts_found": "Nebyly nalezeny žádné příspěvky.", + "post_is_deleted": "Tento příspěvek je vymazán.", + "topic_is_deleted": "Toto téma je odstraněno.", + "profile": "Profil", + "posted_by": "Přidal %1", + "posted_by_guest": "Přidal Host", + "chat": "Konverzace", + "notify_me": "Dostávat upozornění na nové odpovědi", + "quote": "Citovat", + "reply": "Odpovědět", + "replies_to_this_post": "%1 odpovědí", + "one_reply_to_this_post": "1 odpověď", + "last_reply_time": "Poslední odpověď", + "reply-as-topic": "Odpovědět jako Téma", + "guest-login-reply": "Přihlásit se pro odpověď", + "login-to-view": "Přihlásit se pro zobrazení", + "edit": "Upravit", + "delete": "Odstranit", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Vypráznit", + "restore": "Obnovit", + "move": "Přesunout", + "change-owner": "Změnit vlastníka", + "fork": "Rozdělit", + "link": "Odkaz", + "share": "Sdílet", + "tools": "Nástroje", + "locked": "Uzamknuto", + "pinned": "Připnuto", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Přesunuto", + "moved-from": "Moved from %1", + "copy-ip": "Kopírovat IP", + "ban-ip": "Zakázat IP", + "view-history": "Upravit historii", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Pro návrat k poslednímu čtenému příspěvku v tématu, klikněte zde.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Toto téma bylo odstraněno. Jen uživatelé s oprávněním správy témat ho mohou vidět.", + "following_topic.message": "Nyní budete dostávat upozornění, jakmile někdo přidá příspěvek do tohoto tématu.", + "not_following_topic.message": " Toto téma uvidíte v seznamu nepřečtených témat, ale neobdržíte upozornění, přidá-li někdo nový příspěvek.", + "ignoring_topic.message": "Již nadále neuvidíte toto téma v seznamu nepřečtených témat. Budete upozorněn, jakmile se někdo o vás zmíní nebo bude vyjádřen souhlas s příspěvkem.", + "login_to_subscribe": "Pro sledování tohoto tématu se prosím přihlaste nebo zaregistrujte.", + "markAsUnreadForAll.success": "Téma označeno jako nepřečtené pro všechny.", + "mark_unread": "Označ jako nepřečtené", + "mark_unread.success": "Téma označeno jako nepřečtené", + "watch": "Sledovat", + "unwatch": "Přestat sledovat", + "watch.title": "Být upozorněn u nových odpovědí v tomto tématu", + "unwatch.title": "Přestat sledovat toto téma", + "share_this_post": "Sdílet toto téma", + "watching": "Sledováno", + "not-watching": "Nesledováno", + "ignoring": "Ignorování", + "watching.description": "Upozornit mě na nové odpovědi.
Zobrazit téma v nepřečtených.", + "not-watching.description": "Neupozorňovat na nové odpovědi.
Zobrazit téma v nepřečtených, není-li tato kategorie ignorována", + "ignoring.description": "Neupozorňovat na nové odpovědi.
Nezobrazovat téma v nepřečtených.", + "thread_tools.title": "Nástroje tématu", + "thread_tools.markAsUnreadForAll": "Označit nepřečtené pro všechny", + "thread_tools.pin": "Připnout téma", + "thread_tools.unpin": "Odepnout téma", + "thread_tools.lock": "Zamknout téma", + "thread_tools.unlock": "Odemknout téma", + "thread_tools.move": "Přesunout téma", + "thread_tools.move-posts": "Přesunout příspěvky", + "thread_tools.move_all": "Přesunout vše", + "thread_tools.change_owner": "Změnit vlastníka", + "thread_tools.select_category": "Vybrat kategorii", + "thread_tools.fork": "Větvit téma", + "thread_tools.delete": "Odstranit téma", + "thread_tools.delete-posts": "Odstranit přispěvky", + "thread_tools.delete_confirm": "Jste si jist/a, že chcete toto téma smazat.", + "thread_tools.restore": "Obnovit téma", + "thread_tools.restore_confirm": "Jste si jist/a, že chcete toto téma obnovit?", + "thread_tools.purge": "Vyčistit téma", + "thread_tools.purge_confirm": "Jste si jist/a, že chcete vyčistit toto téma?", + "thread_tools.merge_topics": "Sloučit témata", + "thread_tools.merge": "Sloučit", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Jste si jist/a, že chcete odstranit tento příspěvek?", + "post_restore_confirm": "Jste si jist/a, že chcete obnovit tento příspěvek?", + "post_purge_confirm": "Jste si jist/a, že chcete tento příspěvek vyčistit?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Načítání kategorií", + "confirm_move": "Přesunout", + "confirm_fork": "Rozdělit", + "bookmark": "Záložka", + "bookmarks": "Záložky", + "bookmarks.has_no_bookmarks": "Ještě jste nezazáložkoval žádný příspěvek.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Načítání více příspěvků", + "move_topic": "Přesunout téma", + "move_topics": "Přesunout témata", + "move_post": "Přesunout příspěvek", + "post_moved": "Příspěvek přesunut.", + "fork_topic": "Rozdělit příspěvek", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Vyberte příspěvky, které chcete oddělit", + "fork_no_pids": "Nebyly vybrány žádné příspěvky.", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "Vybráno %1 příspěvek/ů", + "fork_success": "Téma úspěšně rozděleno. Pro přejití na rozdělené téma, zde klikněte.", + "delete_posts_instruction": "Klikněte na příspěvek, který chcete odstranit/vyčistit", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Klikněte na příspěvek u kterého chcete změnit vlastníka", + "composer.title_placeholder": "Zadejte název tématu…", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Zrušit", + "composer.submit": "Odeslat", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Odpovídání na %1", + "composer.new_topic": "Nové téma", + "composer.editing": "Editing", + "composer.uploading": "nahrávání…", + "composer.thumb_url_label": "Vložit URL náhledu tématu", + "composer.thumb_title": "Přidat k tématu náhled", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Nebo nahrajte soubor", + "composer.thumb_remove": "Vymazat pole", + "composer.drag_and_drop_images": "Přetáhněte sem obrázek", + "more_users_and_guests": "%1 další/ch uživatel/é/ů a %2 host/i/ů", + "more_users": "%1 další/ch uživatel/ů", + "more_guests": "%1 další/ch host/ů", + "users_and_others": "%1 a %2 jiných", + "sort_by": "Seřadit dle", + "oldest_to_newest": "Od nejstarších po nejnovější", + "newest_to_oldest": "Od nejnovějších po nejstarší", + "most_votes": "S nejvíce hlasy", + "most_posts": "S nejvíce příspěvky", + "most_views": "Most Views", + "stale.title": "Raději vytvořit nové téma?", + "stale.warning": "Reagujete na starší téma. Nechcete raději vytvořit nové téma a na původní v něm odkázat?", + "stale.create": "Vytvořit nové téma", + "stale.reply_anyway": "Přesto reagovat na toto téma", + "link_back": "Odpověď: [%1](%2)", + "diffs.title": "Historie úpravy příspěvku", + "diffs.description": "Tento příspěvek má %1 změn. Pro zobrazení obsahu příspěvku platného v daný čas, klikněte níže na jednu ze změn.", + "diffs.no-revisions-description": "Tento příspěvek má %1 změn.", + "diffs.current-revision": "aktuální revize", + "diffs.original-revision": "originální revize", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 později", + "timeago_earlier": "%1 dříve", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/cs/unread.json b/public/language/cs/unread.json new file mode 100644 index 0000000000..35035c5cb0 --- /dev/null +++ b/public/language/cs/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Nepřečtené", + "no_unread_topics": "Nejsou zde žádné nepřečtené témata.", + "load_more": "Načíst další", + "mark_as_read": "Označit jako přečtené", + "selected": "Vybrané", + "all": "Vše", + "all_categories": "Všechny kategorie", + "topics_marked_as_read.success": "Téma bylo označeno jako přečtené.", + "all-topics": "Všechna témata", + "new-topics": "Nová témata", + "watched-topics": "Sledovaná témata", + "unreplied-topics": "Neodpovězené témata", + "multiple-categories-selected": "Vícenásobný výběr" +} \ No newline at end of file diff --git a/public/language/cs/uploads.json b/public/language/cs/uploads.json new file mode 100644 index 0000000000..373b44b70a --- /dev/null +++ b/public/language/cs/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Nahrávání souboru…", + "select-file-to-upload": "Vyberte soubor pro nahrání.", + "upload-success": "Soubor byl úspěšně nahrán.", + "maximum-file-size": "Maximálně %1 kb", + "no-uploads-found": "Nebyly nalezeny žádné nahrávání", + "public-uploads-info": "Nahrávání jsou veřejná, všichni návštěvníci je mohou vidět.", + "private-uploads-info": "Nahrávání jsou soukromá, jen přihlášení uživatelé je mohou vidět." +} \ No newline at end of file diff --git a/public/language/cs/user.json b/public/language/cs/user.json new file mode 100644 index 0000000000..8eac8be334 --- /dev/null +++ b/public/language/cs/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Zablokován", + "muted": "Muted", + "offline": "Nepřipojen", + "deleted": "Odstraněno", + "username": "Uživatelské jméno", + "joindate": "Datum registrace", + "postcount": "Počet příspěvků", + "email": "E-mail", + "confirm_email": "Potvrdit e-mail", + "account_info": "Informace o účtu", + "admin_actions_label": "Administrative Actions", + "ban_account": "Zablokovat účet", + "ban_account_confirm": "Opravdu chcete zablokovat tohoto uživatele?", + "unban_account": "Odblokovat účet", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Odstranit účet", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Účet odstraněn", + "account-content-deleted": "Account content deleted", + "fullname": "Jméno a příjmení", + "website": "Webové stránky", + "location": "Poloha", + "age": "Věk", + "joined": "Registrován", + "lastonline": "Naposledy připojen", + "profile": "Profil", + "profile_views": "Zobrazení profilu", + "reputation": "Reputace", + "bookmarks": "Záložky", + "watched_categories": "Sledované kategorie", + "change_all": "Změnit vše", + "watched": "Sledován", + "ignored": "Ignorován", + "default-category-watch-state": "Výchozí stav sledované kategorie", + "followers": "Sledují ho", + "following": "Sleduje", + "blocks": "Zablokováni", + "block_toggle": "Přepnout zablokování", + "block_user": "Zablokovat uživatele", + "unblock_user": "Odblokovat uživatele", + "aboutme": "O mně", + "signature": "Podpis", + "birthday": "Datum narození", + "chat": "Konverzace", + "chat_with": "Pokračovat v konverzaci s %1", + "new_chat_with": "Začít novou konverzaci s %1", + "flag-profile": "Označit profil", + "follow": "Sledovat", + "unfollow": "Nesledovat", + "more": "Více", + "profile_update_success": "Profil byl úspěšně aktualizován.", + "change_picture": "Změnit obrázek", + "change_username": "Změnit uživatelské jméno", + "change_email": "Změnit e-mail", + "email_same_as_password": "Chcete-li pokračovat, zadejte své aktuální heslo. – znovu jste zadal/a vaši novou e-mailovou adresu", + "edit": "Upravit", + "edit-profile": "Upravit profil", + "default_picture": "Výchozí ikonka", + "uploaded_picture": "Nahraný obrázek", + "upload_new_picture": "Nahrát nový obrázek", + "upload_new_picture_from_url": "Nahrát nový obrázek z URL", + "current_password": "Aktuální heslo", + "change_password": "Změnit heslo", + "change_password_error": "Neplatné heslo.", + "change_password_error_wrong_current": "Aktuální heslo není správně.", + "change_password_error_match": "Hesla se neshodují.", + "change_password_error_privileges": "Nemáte oprávnění změnit heslo.", + "change_password_success": "Heslo bylo aktualizováno.", + "confirm_password": "Potvrdit heslo", + "password": "Heslo", + "username_taken_workaround": "Zvolené uživatelské jméno již někdo používá, takže jsme ho trochu upravili. Nyní jste znám jako %1", + "password_same_as_username": "Vaše heslo je stejné jako vaše přihlašovací jméno. Zvolte si prosím jiné heslo.", + "password_same_as_email": "Vaše heslo je stejné jako váš e-mail. Zvolte si prosím jiné heslo.", + "weak_password": "Slabé heslo.", + "upload_picture": "Nahrát obrázek", + "upload_a_picture": "Nahrát obrázek", + "remove_uploaded_picture": "Odstranit nahraný obrázek", + "upload_cover_picture": "Náhrát titulní obrázek", + "remove_cover_picture_confirm": "Jste si jist/a, že chcete smazat obrázek?", + "crop_picture": "Oříznout obrázek", + "upload_cropped_picture": "Oříznout a nahrát", + "avatar-background-colour": "Avatar background colour", + "settings": "Nastavení", + "show_email": "Zobrazovat můj e-mail", + "show_fullname": "Zobrazovat celé jméno", + "restrict_chats": "Povolit konverzační zprávy pouze od uživatelů, které sleduji.", + "digest_label": "Odebírat přehled", + "digest_description": "Přihlásit se k odběru e-mailových aktualizací pro toto fórum (nová oznámení a témata), dle stanoveného plánu", + "digest_off": "Vypnuto", + "digest_daily": "Denně", + "digest_weekly": "Týdně", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Měsíčně", + "has_no_follower": "Tohoto uživatele nikdo nesleduje :(", + "follows_no_one": "Tento uživatel nikoho nesleduje :(", + "has_no_posts": "Tento uživatel ještě nic nenapsal.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Tento uživatel ještě nezaložil žádné téma.", + "has_no_watched_topics": "Tento uživatel zatím nesleduje žádná témata.", + "has_no_ignored_topics": "Tento uživatel ještě neignoruje žádné témata.", + "has_no_upvoted_posts": "Tento uživatel zatím nevyjádřil souhlas u žádného příspěvku.", + "has_no_downvoted_posts": "Tento uživatel zatím nevyjádřil nesouhlas u žádného příspěvku.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Nezablokoval/a jste žádné uživatele.", + "email_hidden": "E-mail je skryt", + "hidden": "skrytý", + "paginate_description": "Stránkovat témata a příspěvky místo použití nekonečného posunování", + "topics_per_page": "Témat na stránce", + "posts_per_page": "Příspěvků na stránce", + "max_items_per_page": "Maximum %1", + "acp_language": "Jazyk stránky správce", + "notifications": "Notifications", + "upvote-notif-freq": "Frekvence upozornění na souhlasy", + "upvote-notif-freq.all": "Všechny souhlasy", + "upvote-notif-freq.first": "První podle příspěvku", + "upvote-notif-freq.everyTen": "Každý desátý souhlas", + "upvote-notif-freq.threshold": "Dle 1, 5, 10, 25, 50, 100, 150, 200, ...", + "upvote-notif-freq.logarithmic": "Dle 10, 100, 1000...", + "upvote-notif-freq.disabled": "Zakázáno", + "browsing": "Nastavení prohlížení", + "open_links_in_new_tab": "Otevřít odchozí odkaz v nové záložce", + "enable_topic_searching": "Povolit vyhledávání v tématu", + "topic_search_help": "Je-li povoleno, hledání v tématu přepíše výchozí chování vyhledávání v prohlížeči a umožní vám prohledávat celé téma, namísto pouze toho, co je zobrazeno na obrazovce", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Po odeslání odpovědi, zobrazit nový příspěvek", + "follow_topics_you_reply_to": "Sledovat témata, do kterých přispějete", + "follow_topics_you_create": "Sledovat témata, která vytvoříte", + "grouptitle": "Nadpis skupiny", + "group-order-help": "Vyberte si skupiny a použijte šipky pro seřazení titulů", + "no-group-title": "Žádný nadpis skupiny", + "select-skin": "Vybrat vzhled", + "select-homepage": "Vybrat domovskou stránku", + "homepage": "Domovská stránka", + "homepage_description": "Vyberte stránku, která má být domovskou stránkou fóra nebo vyberte „Nic” a bude použita výchozí domovská stránka.", + "custom_route": "Cesta k uživatelské domovské stránce", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Služby jednotného přihlášení", + "sso.associated": "Přiřazeno k", + "sso.not-associated": "Zde klikněte pro přiřazení k", + "sso.dissociate": "Odloučit", + "sso.dissociate-confirm-title": "Potvrdit odloučení", + "sso.dissociate-confirm": "Jste si jist/a, že chcete odloučit váš účet z %1?", + "info.latest-flags": "Poslední označené", + "info.no-flags": "Nebyly nalezeny žádné označené příspěvky", + "info.ban-history": "Poslední historie blokovaných", + "info.no-ban-history": "Tento uživatel nebyl nikdy zablokován", + "info.banned-until": "Zablokován do %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Trvale zablokován", + "info.banned-reason-label": "Důvod", + "info.banned-no-reason": "Bez důvodu", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Historie uživatelského jména", + "info.email-history": "E-mailová historie", + "info.moderation-note": "Poznámka moderace", + "info.moderation-note.success": "Poznámka moderace byla uložena", + "info.moderation-note.add": "Přidat poznámku", + "sessions.description": "Tato stránka vám zobrazuje aktivní relace na tomto fóru a umožňuje vám je zrušit. Můžete tak i zrušit vlastní relaci svým odhlášením. ", + "consent.title": "Váš právní souhlas", + "consent.lead": "Toto komunitní fórum sbírá zpracovává vaše osobní údaje.", + "consent.intro": "Tyto informace používáme pouze pro peronizaci vašich zkušeností v této komunitě, stejně tak k rozpoznání příspěvků, které jste pod uživatelským účtem vytvořil. Během jednotlivých registračních kroků budete požádán/a o zadání Vašeho uživatelského jména a e-mailové adresy. Můžete také dobrovolně poskytnout některé dodatečné informace do vašeho profilu na webové stránce.Tyto informace uchováváme po dobu životnosti vašeho uživatelského účtu a Vy můžete kdykoliv zrušit tento svůj souhlas smazáním vašeho účtu. Kdykoli můžete požadovat kopii svých příspěvků na této webové stránce pomocí stránky „Práva a souhlas”

Máte-li nějaké otázky nebo obavy, obraťte se na tým správců fóra.", + "consent.email_intro": "Občas Vám zašleme zprávu na vaši registrovanou e-mailovou schránku za účelem poskytnutí přehledu novinek a/nebo Vám oznámíme o nových příspěvcích, které jsou pro vás relevantní. Časový přehled novinek si můžete kdykoliv upravit (popřípadě ho zakázat), stejně tak vybrat, které typy oznámení chcete dostávat na e-mail. Docílíte toho v uživatelském nastavení.", + "consent.digest_frequency": "Není-li ve vašem uživatelském nastavení uvedeno jinak, tato komunita rozesílá e-mailový přehled každých %1.", + "consent.digest_off": "Není-li ve vašem uživatelském nastavení uvedeno jinak, tato komunita nerozesílá e-mailové přehledy", + "consent.received": "Souhlasil/a jste, že tato stránka může shromažďovat a zpracovávat informace o Vás. Žádný dodatečný úkon není třeba.", + "consent.not_received": "Neposkytl/a jste souhlas se sběrem a zpracováním dat. V tuto chvíly tato webová stránka a její tým správců může smazat váš účet za účelem naplnění zákona „Obecné nařízení o ochraně osobních údajů (GDPR)”.", + "consent.give": "Dát souhlas", + "consent.right_of_access": "Můžete se k nám přidat", + "consent.right_of_access_description": "Máte právo ověřit si data sesbírané touto stránkou. Takovouto kopii dat získáte kliknutím na vhodné tlačítko níže.", + "consent.right_to_rectification": "Máte právo zrušit svůj souhlas", + "consent.right_to_rectification_description": "Máte právo změnit nebo aktualizovat nepřesná data, která jste nám poskytl/a. Váš profil může být aktualizován, pouhou jeho editací a obsah příspěvků může být kdykoliv upraven. Pokud Vám v tuto chvíli jde o něco jiného, kontaktujte tým správců této stránky.", + "consent.right_to_erasure": "Máte právo být smazán", + "consent.right_to_erasure_description": "Kdykoliv můžete změnit svůj souhlas se shromažďováním dat a/nebo zpracování odstraněním vašeho účtu. Váš profil bude odstraněn, ačkoliv vaše příspěvky budou zachovány. Pokud si přejete odstranění jak účtu tak i obsahu, prosím kontaktujte správce této stránky.", + "consent.right_to_data_portability": "Máte právo na přenositelnost dat", + "consent.right_to_data_portability_description": "Můžete od nás požadovat strojně čitelné data, která byla sesbírána o Vás a vašem účtu. Učiníte tak kliknutím na tlačítka zobrazená níže.", + "consent.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Exportovat nahraný obsah (*.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Exportovat příspěvky (*.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/cs/users.json b/public/language/cs/users.json new file mode 100644 index 0000000000..3ef1b712cf --- /dev/null +++ b/public/language/cs/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Nejnovější uživatelé", + "top_posters": "Nejaktivnější", + "most_reputation": "Nejváženější", + "most_flags": "Nejoznačovanější", + "search": "Hledat", + "enter_username": "Zadej uživatelské jméno k hledání", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Načíst další", + "users-found-search-took": "Nalezeno %1 uživatel(ů) za %2 vteřiny.", + "filter-by": "Filtrovat dle", + "online-only": "Pouze připojené", + "invite": "Pozvat", + "prompt-email": "E-maily:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "E-mailová pozvánka byla odeslána na adresu %1", + "user_list": "Seznam uživatelů", + "recent_topics": "Poslední témata", + "popular_topics": "Oblíbená témata", + "unread_topics": "Nepřečtená témata", + "categories": "Kategorie", + "tags": "Značky", + "no-users-found": "Nebyly nalezeny žádní uživatelé." +} \ No newline at end of file diff --git a/public/language/da/_DO_NOT_EDIT_FILES_HERE.md b/public/language/da/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/da/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/da/admin/admin.json b/public/language/da/admin/admin.json new file mode 100644 index 0000000000..45c0121aad --- /dev/null +++ b/public/language/da/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Er du sikker på at du ønsker at genstarte NodeBB?", + + "acp-title": "%1 | NodeBB Admin Kontrol Panel", + "settings-header-contents": "Indhold", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/da/admin/advanced/cache.json b/public/language/da/admin/advanced/cache.json new file mode 100644 index 0000000000..ff4a3382b0 --- /dev/null +++ b/public/language/da/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Indlægs Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Fuld", + "post-cache-size": "Indlægs Cache Størrelse", + "items-in-cache": "Ting i Cache" +} \ No newline at end of file diff --git a/public/language/da/admin/advanced/database.json b/public/language/da/admin/advanced/database.json new file mode 100644 index 0000000000..3e7c77f2f3 --- /dev/null +++ b/public/language/da/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Oppetid i Sekunder", + "uptime-days": "Oppetid i Dage", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Kollektioner", + "mongo.objects": "Objekter", + "mongo.avg-object-size": "Gennemsnitlig Objekt Størrelse", + "mongo.data-size": "Data Størrelse", + "mongo.storage-size": "Lager Størrelse", + "mongo.index-size": "Index Størrelse", + "mongo.file-size": "Fil Størrelse", + "mongo.resident-memory": "Resident Hukommelse", + "mongo.virtual-memory": "Virtuel Hukommelse", + "mongo.mapped-memory": "Kortlagt Hukommelse", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Rå Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Forbundne Klienter", + "redis.connected-slaves": "Forbundne Slaver", + "redis.blocked-clients": "Blokerede Klienter", + "redis.used-memory": "Brugt Hukommelse", + "redis.memory-frag-ratio": "Hukommelses Fragmentations Forhold", + "redis.total-connections-recieved": "Totale Forbindelser Modtaget", + "redis.total-commands-processed": "Totale Kommandoer Behandlet", + "redis.iops": "Øjeblikkelige Ops. pr. sekund", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Mellemrums Tryk", + "redis.keyspace-misses": "Mellemrums Misses", + "redis.raw-info": "Redis Rå Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/da/admin/advanced/errors.json b/public/language/da/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/da/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/da/admin/advanced/events.json b/public/language/da/admin/advanced/events.json new file mode 100644 index 0000000000..dcdb6608c1 --- /dev/null +++ b/public/language/da/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Kontrol Panel for Begivenheder", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/da/admin/advanced/logs.json b/public/language/da/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/da/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/da/admin/appearance/customise.json b/public/language/da/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/da/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/da/admin/appearance/skins.json b/public/language/da/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/da/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/da/admin/appearance/themes.json b/public/language/da/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/da/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/da/admin/dashboard.json b/public/language/da/admin/dashboard.json new file mode 100644 index 0000000000..1d7c3df85d --- /dev/null +++ b/public/language/da/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffik", + "page-views": "Side Visninger", + "unique-visitors": "Unikke Besøgere", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Opdateringer", + "running-version": "Du kører NodeBB v%1.", + "keep-updated": "Altid sikrer dig at din NodeBB er opdateret for de seneste sikkerheds og bug rettelser.", + "up-to-date": "

Du er opdateret

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

Dette er en pre-release udgave af NodeBB. Uforventede bugs kan forekomme.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Varsler", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Kontrol", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/da/admin/development/info.json b/public/language/da/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/da/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/da/admin/development/logger.json b/public/language/da/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/da/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/da/admin/extend/plugins.json b/public/language/da/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/da/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/da/admin/extend/rewards.json b/public/language/da/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/da/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/da/admin/extend/widgets.json b/public/language/da/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/da/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/da/admin/manage/admins-mods.json b/public/language/da/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/da/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/da/admin/manage/categories.json b/public/language/da/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/da/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/da/admin/manage/digest.json b/public/language/da/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/da/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/da/admin/manage/groups.json b/public/language/da/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/da/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/da/admin/manage/privileges.json b/public/language/da/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/da/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/da/admin/manage/registration.json b/public/language/da/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/da/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/da/admin/manage/tags.json b/public/language/da/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/da/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/da/admin/manage/uploads.json b/public/language/da/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/da/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/da/admin/manage/users.json b/public/language/da/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/da/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/da/admin/menu.json b/public/language/da/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/da/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/advanced.json b/public/language/da/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/da/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/da/admin/settings/api.json b/public/language/da/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/da/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/chat.json b/public/language/da/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/da/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/cookies.json b/public/language/da/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/da/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/email.json b/public/language/da/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/da/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/da/admin/settings/general.json b/public/language/da/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/da/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/da/admin/settings/group.json b/public/language/da/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/da/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/guest.json b/public/language/da/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/da/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/homepage.json b/public/language/da/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/da/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/languages.json b/public/language/da/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/da/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/navigation.json b/public/language/da/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/da/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/da/admin/settings/notifications.json b/public/language/da/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/da/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/pagination.json b/public/language/da/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/da/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/post.json b/public/language/da/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/da/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/reputation.json b/public/language/da/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/da/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/social.json b/public/language/da/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/da/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/sockets.json b/public/language/da/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/da/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/sounds.json b/public/language/da/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/da/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/tags.json b/public/language/da/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/da/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/da/admin/settings/uploads.json b/public/language/da/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/da/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/da/admin/settings/user.json b/public/language/da/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/da/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/da/admin/settings/web-crawler.json b/public/language/da/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/da/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/da/category.json b/public/language/da/category.json new file mode 100644 index 0000000000..2e8c100ec9 --- /dev/null +++ b/public/language/da/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategori", + "subcategories": "Underkategorier", + "new_topic_button": "Nyt emne", + "guest-login-post": "Log ind", + "no_topics": "Der er ikke nogen nye emner i denne kategori.
Hvorfor prøver du ikke at lave et?", + "browsing": "browse", + "no_replies": "Ingen har svaret", + "no_new_posts": "Ingen nye indlæg", + "watch": "Overvåg", + "ignore": "Ignorer", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Fulgte kategorier", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/da/email.json b/public/language/da/email.json new file mode 100644 index 0000000000..f8fe687dff --- /dev/null +++ b/public/language/da/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Velkommen til %1", + "invite": "Invitation fra %1", + "greeting_no_name": "Hej", + "greeting_with_name": "Hej %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Tak for at du registrerede dig hos %1!", + "welcome.text2": "For at færdiggøre din konto, har vi brug for at verificere at du ejer den email adresse du registerede med.", + "welcome.text3": "En administrator har accepteret din registreringsansøgning. Du kan logge ind med dit brugernavn og adgangskode nu.", + "welcome.cta": "Klik her for at bekræfte din email adresse.", + "invitation.text1": "%1 har inviteret dig til at deltage i %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Vi har modtaget en anmodning om at nulstille dit kodeord, måske fordi du har glemt det. Hvis det ikke er tilfældet, venligst ignorer denne email.", + "reset.text2": "For at fortsætte med at nulstille kodeordet, venligst klik på dette link:", + "reset.cta": "Klik her for at nulstille dit kodeord. ", + "reset.notify.subject": "Dit kodeord er nu ændret", + "reset.notify.text1": "Bemærk: %1 gang blev dit kodeord ændret.", + "reset.notify.text2": "Hvis du ikke godkendte dette, kontakt straks en administrator.", + "digest.latest_topics": "Nyeste emne fra %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Klik her for at gå til %1", + "digest.unsub.info": "Du har fået tilsendt dette sammendrag pga. indstillingerne i dit abonnement.", + "digest.day": "dag", + "digest.week": "uge", + "digest.month": "måned", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Ny chat besked modtaget fra %1", + "notif.chat.cta": "Klik her for at forsætte med samtalen", + "notif.chat.unsub.info": "Denne chat notifikation blev sendt til dig pga. indstillingerne i dit abonnement.", + "notif.post.unsub.info": "Denne indlægs notifikation var sendt pga. dine abonnering indstillinger.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Dette er en test email for at kontrollere, at den udgående email server er opsat korrekt i forhold til din NodeBB installation.", + "unsub.cta": "Klik her for at ændre disse indstillinger", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Tak!" +} \ No newline at end of file diff --git a/public/language/da/error.json b/public/language/da/error.json new file mode 100644 index 0000000000..d7612af9e1 --- /dev/null +++ b/public/language/da/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Ugyldig Data", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Det ser ikke ud til at du er logget ind.", + "account-locked": "Din konto er blevet blokeret midlertidigt.", + "search-requires-login": "Du skal have en konto for at søge - log venligst ind eller registrer dig.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Ugyldig Kategori ID", + "invalid-tid": "Ugyldig Tråd ID", + "invalid-pid": "Ugyldig Indlæg ID", + "invalid-uid": "Ugyldig Bruger ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Ugyldig Brugernavn", + "invalid-email": "Ugyldig Email", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Ugyldig Bruger Data", + "invalid-password": "Ugyldig Adgangskode", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Venligst angiv både brugernavn og adgangskode", + "invalid-search-term": "Ugyldig søgeterm", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Ugyldig side værdi, skal mindst være %1 og maks. %2", + "username-taken": "Brugernavn optaget", + "email-taken": "Emailadresse allerede i brug", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Du kan ikke chatte før din email er bekræftet, klik her for at bekræfte din email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Vi kunne ikke bekræfte din email, prøv igen senere.", + "confirm-email-already-sent": "Bekræftelses email er allerede afsendt, vent venligt %1 minut(ter) for at sende endnu en.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Brugernavn er for kort", + "username-too-long": "Brugernavn er for langt", + "password-too-long": "Kodeord er for langt", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Bruger er bortvist", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Beklager, du er nødt til at vente %1 sekund(er) før du opretter dit indlæg", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Kategorien eksisterer ikke", + "no-topic": "Tråden eksisterer ikke", + "no-post": "Indlægget eksisterer ikke", + "no-group": "Gruppen eksisterer ikke", + "no-user": "Brugeren eksisterer ikke", + "no-teaser": "Teaser eksisterer ikke", + "no-flag": "Flag does not exist", + "no-privileges": "Du har ikke nok rettigheder til at udføre denne handling", + "category-disabled": "Kategorien er deaktiveret", + "topic-locked": "Tråden er låst", + "post-edit-duration-expired": "Du kan kun redigere indlæg i %1 sekund(er) efter indlæg", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Venligst indtast et længere indlæg. Indlægget skal mindst indeholde %1 karakter(er).", + "content-too-long": "Venligt indtast et kortere indlæg. Indlæg kan ikke være længere end %1 karakter(er).", + "title-too-short": "Venligst indtast en længere titel. Titlen skal mindst indeholde %1 karakter(er).", + "title-too-long": "Venligst indtast en kortere titel. Titlen kan ikke indeholde flere end %1 karakter(er).", + "category-not-selected": "Category not selected.", + "too-many-posts": "Du kan højest skrive et indlæg hver %1 sekund(er) - venligst vent et øjeblik før næste indlæg", + "too-many-posts-newbie": "Som ny bruger kan du kun skrive et indlæg engang hvert %1. sekund() indtil du har optjent %2 omdømme point - venligst vent et øjeblik før næste indlæg.", + "already-posting": "You are already posting", + "tag-too-short": "Indtast et længere tag. Tags skal indeholde mindst %1 karakter(er).", + "tag-too-long": "Indtast et længere tag. Tags kan ikke være længere end %1 karakter(er).", + "not-enough-tags": "Ikke nok tags. Tråde skal have mindst %1 tag(s)", + "too-many-tags": "For mange tags. Tråde kan ikke have mere end %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Venligst vent til overførslen er færdig", + "file-too-big": "Maksimum filstørrelse er %1 kB - venligst overfør en mindre fil", + "guest-upload-disabled": "Gæsteupload er deaktiveret", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Du kan ikke udlukke andre administatrorer!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Du er den eneste administrator. Tilføj en anden bruger som administrator før du fjerner dig selv som administrator", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid billed type. De tilladte typer er: %1", + "invalid-image-extension": "Forkert billede filnavnsendelse", + "invalid-file-type": "Invalid fil type. Tilladte typer er: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Gruppe navn for kort", + "group-name-too-long": "Group name too long", + "group-already-exists": "Gruppen eksisterer allerede", + "group-name-change-not-allowed": "Ændring af gruppe navn er ikke tilladt", + "group-already-member": "Allerede medlem af denne gruppe", + "group-not-member": "Ikke medlem af denne gruppe", + "group-needs-owner": "Denne grupper kræver mindst én ejer", + "group-already-invited": "Denne bruger er allerede blevet inviteret", + "group-already-requested": "Din medlemskabs anmodning er allerede blevet afsendt", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Dette indlæg er allerede blevet slettet", + "post-already-restored": "Dette indlæg er allerede blevet genskabt", + "topic-already-deleted": "Denne tråd er allerede blevet slettet", + "topic-already-restored": "Denne tråd er allerede blevet genskabt", + "cant-purge-main-post": "Du kan ikke udradere hoved indlægget, fjern venligt tråden istedet", + "topic-thumbnails-are-disabled": "Tråd miniaturebilleder er slået fra.", + "invalid-file": "Ugyldig fil", + "uploads-are-disabled": "Overførsel er slået fra", + "signature-too-long": "Beklager, din signatur kan ikke være længere end %1 karakter(er).", + "about-me-too-long": "Beklager, men din om mig side kan ikke være længere end %1 karakter(er).", + "cant-chat-with-yourself": "Du kan ikke chatte med dig selv!", + "chat-restricted": "Denne bruger har spæret adgangen til chat beskeder. Brugeren må følge dig før du kan chatte med ham/hende", + "chat-disabled": "Chat system er deaktiveret", + "too-many-messages": "Du har sendt for mange beskeder, vent venligt lidt.", + "invalid-chat-message": "Ugyldig chat besked", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "Du har ikke tilladelse til at redigere denne besked", + "cant-delete-chat-message": "Du har ikke tilladelse til at slette denne besked", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Vurderingssystem er slået fra.", + "downvoting-disabled": "Nedvurdering er slået fra", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB stødte på et problem under genindlæsningen : \"%1\". NodeBB vil fortsætte med en ældre version, og det er nok god ide at genoptage fra lige før du genindlæste siden.", + "registration-error": "Registeringsfejl", + "parse-error": "Noget gik galt under fortolknings er serverens respons", + "wrong-login-type-email": "Brug venligt din email til login", + "wrong-login-type-username": "Brug venligt dit brugernavn til login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "Du har inviteret det maksimale antal personer (%1 ud af %2)", + "no-session-found": "Ingen login session kan findes!", + "not-in-room": "Bruger er ikke i rummet", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/da/flags.json b/public/language/da/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/da/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/da/global.json b/public/language/da/global.json new file mode 100644 index 0000000000..b3815b94f2 --- /dev/null +++ b/public/language/da/global.json @@ -0,0 +1,126 @@ +{ + "home": "Forside", + "search": "Søg", + "buttons.close": "Luk", + "403.title": "Adgang nægtet", + "403.message": "Det ser ud til du er stødt på en side du ikke har adgang til.", + "403.login": "Måske du skulle prøve og logge ind?", + "404.title": "Ikke fundet", + "404.message": "Det ser ud til du er stødt på en side der ikke finder. Retuner til forsiden.", + "500.title": "Internal Error.", + "500.message": "Ups! Ser ud til at noget gik galt!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Tilmeld", + "login": "Log ind", + "please_log_in": "Venligst log ind", + "logout": "Log ud", + "posting_restriction_info": "Det er i øjeblikket kun muligt at skrive indlæg som registeret medlem, klik her for at logge ind.", + "welcome_back": "Velkommen tilbage", + "you_have_successfully_logged_in": "Du er nu logget ind", + "save_changes": "Gem ændringer", + "save": "Save", + "close": "Luk", + "pagination": "Sidetal", + "pagination.out_of": "%1 ud af %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Administrator", + "header.categories": "Kategorier", + "header.recent": "Seneste", + "header.unread": "Ulæst", + "header.tags": "Etiket", + "header.popular": "Populære", + "header.top": "Top", + "header.users": "Bruger", + "header.groups": "Grupper", + "header.chats": "Chats", + "header.notifications": "Notifikationer", + "header.search": "Søg", + "header.profile": "Profil", + "header.navigation": "Navigation", + "notifications.loading": "Indlæser notifikationer", + "chats.loading": "Indlæser chats", + "motd.welcome": "Velkommen til NodeBB, fremtidens diskussion platform. ", + "previouspage": "Forrige side", + "nextpage": "Næste side", + "alert.success": "Succes", + "alert.error": "Fejl", + "alert.banned": "Forment adgang", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Du følger ikke længere %1!", + "alert.follow": "Du følger nu %1!", + "users": "Bruger", + "topics": "Emner", + "posts": "Indlæg", + "x-posts": "%1 posts", + "best": "Bedste", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Syntes godt om", + "downvoters": "Downvoters", + "downvoted": "Syntes ikke godt om", + "views": "Visninger", + "posters": "Posters", + "reputation": "Omdømme", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "læs mere", + "more": "Mere", + "none": "None", + "posted_ago_by_guest": "indsendt %1 af gæst", + "posted_ago_by": "indsendt %1 siden af %2", + "posted_ago": "Indsendt %1 siden", + "posted_in": "skrevet i %1", + "posted_in_by": "skrevet i %1 af %2", + "posted_in_ago": "skrivet i %1 %2", + "posted_in_ago_by": "skrevet i %1 %2 af %3", + "user_posted_ago": "%1 skrev for %2", + "guest_posted_ago": "Gæst skrev for %1", + "last_edited_by": "sidst redigeret af %1", + "norecentposts": "Ingen seneste indlæg", + "norecenttopics": "Ingen seneste tråde", + "recentposts": "Seneste indlæg", + "recentips": "Seneste loggede ind IPer", + "moderator_tools": "Moderator Tools", + "online": "Online", + "away": "Væk", + "dnd": "Vil ikke forstyres", + "invisible": "Usynlig", + "offline": "Offline", + "email": "Email", + "language": "Sprog", + "guest": "Gæst", + "guests": "Gæster", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum opdateret", + "updated.message": "Dette form er lige blevet opdateret til den seneste version. Klik her for at genindlæse siden.", + "privacy": "Privatliv", + "follow": "Følg", + "unfollow": "Følg ikke længere", + "delete_all": "Slet alt", + "map": "Kort", + "sessions": "Login Sessioner", + "ip_address": "IP-adresse", + "enter_page_number": "Indsæt sideantal", + "upload_file": "Upload fil", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Tilladte filtyper er %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/da/groups.json b/public/language/da/groups.json new file mode 100644 index 0000000000..b9f2c13d4c --- /dev/null +++ b/public/language/da/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "grupper", + "view_group": "se gruppe", + "owner": "Gruppe ejer", + "new_group": "Opret ny gruppe", + "no_groups_found": "Der er ingen grupper at se", + "pending.accept": "Accepter", + "pending.reject": "Afvis", + "pending.accept_all": "Acceptér Alle", + "pending.reject_all": "Afvis Alle", + "pending.none": "Der er ikke nogen afventene medlemmer i øjeblikket", + "invited.none": "Der er ingen inviterede medlemmer i øjeblikket", + "invited.uninvite": "Tilbagetræk invitation", + "invited.search": "Søg efter en bruger at invitere til denne gruppe", + "invited.notification_title": "Du er blevet inviteret til at blive medlem af %1", + "request.notification_title": "Gruppe medlemskab anmodning fra %1", + "request.notification_text": "%1 har anmodet om at blive medlem af %2", + "cover-save": "Gem", + "cover-saving": "Gemmer", + "details.title": "gruppe detaljer", + "details.members": "liste over medlemmer", + "details.pending": "Verserende medlemmer", + "details.invited": "Inviterede Medlemmer", + "details.has_no_posts": "Medlemmer af denne gruppe har ikke oprettet indlæg.", + "details.latest_posts": "seneste indlæg", + "details.private": "Privat", + "details.disableJoinRequests": "Deaktiver Anmodninger", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Giv/ophæv ejerskab", + "details.kick": "Spark", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Gruppe administration", + "details.group_name": "Gruppe navn", + "details.member_count": "Medlemsantal", + "details.creation_date": "Oprettelsesdato", + "details.description": "Beskrivelse", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Mærke forhåndsvisning", + "details.change_icon": "Skift ikon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Mærke tekst", + "details.userTitleEnabled": "Vis mærke", + "details.private_help": "Hvis aktiveret, så vil det kræve godkendelse af gruppe ejeren for at tilslutte sig en gruppe", + "details.hidden": "Skjult", + "details.hidden_help": "Hvis aktiveret, så vil denne gruppe ikke kunne ses i gruppelisten og bruhere skal inviteres manuelt", + "details.delete_group": "Slet Gruppe", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Gruppe detaljer er blevet opdateret", + "event.deleted": "Gruppen \"%1\" er blevet slettet", + "membership.accept-invitation": "Acceptér Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Afventende Invitationer", + "membership.join-group": "Bliv medlem af gruppe", + "membership.leave-group": "Forlad Gruppe", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Afvis", + "new-group.group_name": "Gruppe Navn:", + "upload-group-cover": "Upload Gruppe coverbillede", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/da/ip-blacklist.json b/public/language/da/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/da/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/da/language.json b/public/language/da/language.json new file mode 100644 index 0000000000..f831129ee9 --- /dev/null +++ b/public/language/da/language.json @@ -0,0 +1,5 @@ +{ + "name": "Danish", + "code": "da", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/da/login.json b/public/language/da/login.json new file mode 100644 index 0000000000..26d4ad2143 --- /dev/null +++ b/public/language/da/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Brugernavn / Email", + "username": "Brugernavn", + "remember_me": "Husk mig?", + "forgot_password": "Glemt kodeord?", + "alternative_logins": "alternative logins", + "failed_login_attempt": "Log Ind Mislykkedes", + "login_successful": "Du har successfuldt logged in!", + "dont_have_account": "Har du ikke en konto?", + "logged-out-due-to-inactivity": "Du er blevet logged af Admin Kontrol Panelet, på grund af din inaktiviet.", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/da/modules.json b/public/language/da/modules.json new file mode 100644 index 0000000000..970b44bbf4 --- /dev/null +++ b/public/language/da/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Send", + "chat.no_active": "Du har ingen aktive chats.", + "chat.user_typing": "%1 skriver ...", + "chat.user_has_messaged_you": "%1 har skrevet til dig.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Vælg en modtager for at se beskedhistorikken", + "chat.no-users-in-room": "Ingen brugere i rummet", + "chat.recent-chats": "Seneste chats", + "chat.contacts": "Kontakter", + "chat.message-history": "Beskedhistorik", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop ud chatten", + "chat.minimize": "Minimize", + "chat.maximize": "Maximer", + "chat.seven_days": "7 dage", + "chat.thirty_days": "30 dage", + "chat.three_months": "3 måneder", + "chat.delete_message_confirm": "Er du sikker på at du vil slette denne besked?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Skriv", + "composer.show_preview": "Vis forhåndsvisning", + "composer.hide_preview": "Fjern forhåndsvisning", + "composer.user_said_in": "%1 sagde i %2:", + "composer.user_said": "%1 sagde:", + "composer.discard": "Er du sikker på at du vil kassere dette indlæg?", + "composer.submit_and_lock": "Send og lås", + "composer.toggle_dropdown": "Skift mellem dropdown", + "composer.uploading": "Uploader %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Annuller", + "bootbox.confirm": "Bekræft", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Coverbillede positionering", + "cover.dragging_message": "Træk coverbilledet til den ønskede position og klik \"Gem\"", + "cover.saved": "Coverbillede og position gemt", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/da/notifications.json b/public/language/da/notifications.json new file mode 100644 index 0000000000..d2fd4b2638 --- /dev/null +++ b/public/language/da/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notifikationer", + "no_notifs": "Du har ingen nye notifkationer", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Tilbage til %1", + "outgoing_link": "Udgående link", + "outgoing_link_message": "Du forlader nu %1", + "continue_to": "Fortsæt til %1", + "return_to": "Returnere til %t", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Du har ulæste notifikationer.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Ny besked fra %1", + "upvoted_your_post_in": "%1 har upvotet dit indlæg i %2.", + "upvoted_your_post_in_dual": "%1 og %2 har syntes godt om dit indlæg i %3.", + "upvoted_your_post_in_multiple": "%1 og %2 andre har syntes godt om dit indlæg i%3.", + "moved_your_post": "%1 har flyttet dit indlæg til %2", + "moved_your_topic": "%1 har flyttet %2", + "user_flagged_post_in": "%1 har anmeldt et indlæg i %2", + "user_flagged_post_in_dual": "%1 og %2 har anmeldt et indlæg i %3", + "user_flagged_post_in_multiple": "%1 og %2 andre har anmeldt et indlæg i %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 har skrevet et svar til: %2", + "user_posted_to_dual": "%1 og %2 har skrevet svar til: %3", + "user_posted_to_multiple": "%1 og %2 andre har skrevet svar til: %3", + "user_posted_topic": "%1 har oprettet en ny tråd: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 har valgt at følge dig.", + "user_started_following_you_dual": "%1 og %2 har valgt at følge dig.", + "user_started_following_you_multiple": "%1 og %2 har valgt at følge dig.", + "new_register": "%1 har sendt en registrerings anmodning.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email bekræftet", + "email-confirmed-message": "Tak fordi du validerede din email. Din konto er nu fuldt ud aktiveret.", + "email-confirm-error-message": "Der var et problem med valideringen af din emailadresse. Bekræftelses koden var muligvis forkert eller udløbet.", + "email-confirm-sent": "Bekræftelses email afsendt.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/da/pages.json b/public/language/da/pages.json new file mode 100644 index 0000000000..70d679b9e7 --- /dev/null +++ b/public/language/da/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Forside", + "unread": "Ulæste tråde", + "popular-day": "Populære tråde i dag", + "popular-week": "Populære tråde denne ude", + "popular-month": "Populære tråde denne måned", + "popular-alltime": "Top populære tråde", + "recent": "Seneste tråde", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Online brugere", + "users/latest": "Seneste brugere", + "users/sort-posts": "Brugere med de fleste indlæg", + "users/sort-reputation": "Brugere med mest omdømme", + "users/banned": "Banlyste Brugere", + "users/most-flags": "Most flagged users", + "users/search": "Bruger søgning", + "notifications": "Notifikationer", + "tags": "Tags", + "tag": "Topics tagged under "%1"", + "register": "Registre en konto", + "registration-complete": "Registration complete", + "login": "Login til din konto", + "reset": "Nulstil din adgangskode", + "categories": "Kategorier", + "groups": "Grupper", + "group": "%1 gruppe", + "chats": "Chats", + "chat": "Chatter med %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Redigere \"%1\"", + "account/edit/password": "Redigerer adgangskode for \"%1\"", + "account/edit/username": "Redigerer brugernavn for \"%1\"", + "account/edit/email": "Redigerer email for \"%1\"", + "account/info": "Konto Info", + "account/following": "Personer som %1 følger", + "account/followers": "Personer som følger %1", + "account/posts": "Indlæg oprettet af %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Tråde lavet af %1", + "account/groups": "%1s grupper", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Bruger instillinger", + "account/watched": "Tråde fulgt af %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Indlæg syntes godt om af %1", + "account/downvoted": "Indlæg syntes ikke godt om af %1", + "account/best": "Bedste indlæg skrevet af %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Bekræftet", + "maintenance.text": "%1 er under vedligeholdelse. Kom venligst tilbage senere.", + "maintenance.messageIntro": "Administratoren har yderligere vedlagt denne besked:", + "throttled.text": "%1 er ikke tilgængelig på grund af overbelastning. Venligst kom tilbage senere." +} \ No newline at end of file diff --git a/public/language/da/post-queue.json b/public/language/da/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/da/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/da/recent.json b/public/language/da/recent.json new file mode 100644 index 0000000000..102e609bdc --- /dev/null +++ b/public/language/da/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Seneste", + "day": "Dag", + "week": "Uge", + "month": "Måned", + "year": "År", + "alltime": "Al tid", + "no_recent_topics": "Der er ingen seneste tråde", + "no_popular_topics": "Der er ingen populære tråde.", + "there-is-a-new-topic": "Der er en ny tråd.", + "there-is-a-new-topic-and-a-new-post": "Der er en ny tråd og et nyt indlæg.", + "there-is-a-new-topic-and-new-posts": "Der er en tråd og %1 nye indlæg", + "there-are-new-topics": "Der er %1 nye indlæg.", + "there-are-new-topics-and-a-new-post": "Der er %1 nye indlæg og et nyt indlæg.", + "there-are-new-topics-and-new-posts": "Der er %1 nye tråde og %2 nye indlæg.", + "there-is-a-new-post": "Der er et nyt indlæg.", + "there-are-new-posts": "Der er %1 nye indlæg.", + "click-here-to-reload": "Klik her for at genindlæse." +} \ No newline at end of file diff --git a/public/language/da/register.json b/public/language/da/register.json new file mode 100644 index 0000000000..49e5041f8d --- /dev/null +++ b/public/language/da/register.json @@ -0,0 +1,32 @@ +{ + "register": "Tilmeld", + "cancel_registration": "Cancel Registration", + "help.email": "Den email er skjult som standard.", + "help.username_restrictions": "Et unikt brugernavn mellem %1 og %2 karakterer. Andre kan nævne dig med @brugernavn.", + "help.minimum_password_length": "Din adgangskode skal være på mindst %1 karakterer.", + "email_address": "Emailadresse", + "email_address_placeholder": "Indtast emailadresse", + "username": "Brugernavn", + "username_placeholder": "Indtast brugernavn", + "password": "Adgangskode", + "password_placeholder": "Indtast adgangskode", + "confirm_password": "Bekræft adgangskode", + "confirm_password_placeholder": "Bekræft adgangskode", + "register_now_button": "Registrer nu", + "alternative_registration": "Alternativ registrering", + "terms_of_use": "Betingelser for brug", + "agree_to_terms_of_use": "Jeg acceptere betingelserne for brug", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Din registrering er blevet tilføjet til godkendelses køen. Du vil mostage en email når du er blevet accepteret af en administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/da/reset_password.json b/public/language/da/reset_password.json new file mode 100644 index 0000000000..1403ffb5a1 --- /dev/null +++ b/public/language/da/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Nulstil adgangskode", + "update_password": "Opdater adgangskode", + "password_changed.title": "Adgangskode ændret", + "password_changed.message": "

Adgangskode er blevet ændret, log venligt ind igen.", + "wrong_reset_code.title": "Forkert nulstillingskode", + "wrong_reset_code.message": "Nulstillingskoden var forkert. Prøv venligst igen, eller anmod om en ny nultillingskode.", + "new_password": "Ny adgangskode", + "repeat_password": "Bekræft adgangskode", + "changing_password": "Changing Password", + "enter_email": "Indtast venligst din emailadresse så vi kan sende dig instrukser til at nulstille din konto.", + "enter_email_address": "Indtast emailadresse", + "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.", + "invalid_email": "Ugyldig emailadresse / Emailadresse findes ikke", + "password_too_short": "Den indtastede adgangskode er for kort, vælg venligt en anden adgangskode.", + "passwords_do_not_match": "De to indtastede adgangskoder er ikke ens.", + "password_expired": "Din adgangskode er udløbet, vælg venligst en ny adgangskode" +} \ No newline at end of file diff --git a/public/language/da/search.json b/public/language/da/search.json new file mode 100644 index 0000000000..d2a9f8fc4e --- /dev/null +++ b/public/language/da/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 resultat(er) matcher \"%2\", (%3 sekunder)", + "no-matches": "Ingen resultatet fundet", + "advanced-search": "Advanceret søgning", + "in": "I", + "titles": "Titler", + "titles-posts": "Titler og indlæg", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Skrevet af", + "in-categories": "I katagorierne", + "search-child-categories": "Søg underkategorier", + "has-tags": "Has tags", + "reply-count": "Svar antal", + "at-least": "Mindst", + "at-most": "Højst", + "relevance": "Relevance", + "post-time": "Skrevet", + "votes": "Votes", + "newer-than": "Nyere end", + "older-than": "Ældre end", + "any-date": "Enhver dato", + "yesterday": "Igår", + "one-week": "En uge", + "two-weeks": "To uger", + "one-month": "En måned", + "three-months": "Tre måneder", + "six-months": "Seks måneder", + "one-year": "Et år", + "sort-by": "Sorter efter", + "last-reply-time": "Sidste svar tid", + "topic-title": "Tråd titel", + "topic-votes": "Topic votes", + "number-of-replies": "Antal svar", + "number-of-views": "Antal visninger", + "topic-start-date": "Tråd starts dato", + "username": "Brugernavn", + "category": "Kategori", + "descending": "I faldende rækkefølge", + "ascending": "I stigende rækkefølge", + "save-preferences": "Gem præferencer", + "clear-preferences": "Slet præferencer", + "search-preferences-saved": "Søgnings præferencer gemt", + "search-preferences-cleared": "Søgnings præferencer slettet", + "show-results-as": "Vis resultater som", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/da/success.json b/public/language/da/success.json new file mode 100644 index 0000000000..b024945606 --- /dev/null +++ b/public/language/da/success.json @@ -0,0 +1,7 @@ +{ + "success": "Udført", + "topic-post": "Du har indsendt et indlæg.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Du blev autentificeret", + "settings-saved": "Indstillinger gemt!" +} \ No newline at end of file diff --git a/public/language/da/tags.json b/public/language/da/tags.json new file mode 100644 index 0000000000..5a37670409 --- /dev/null +++ b/public/language/da/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Der er ikke indlæg med dette tag.", + "tags": "Tags", + "enter_tags_here": "Indsæt tags her, hver på mellem %1 og %2 karakterer.", + "enter_tags_here_short": "Skriv tags", + "no_tags": "Der er ingen tags endnu.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/da/top.json b/public/language/da/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/da/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/da/topic.json b/public/language/da/topic.json new file mode 100644 index 0000000000..c09722cfa2 --- /dev/null +++ b/public/language/da/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tråd", + "title": "Title", + "no_topics_found": "Ingen tråde fundet", + "no_posts_found": "Ingen indlæg fundet!", + "post_is_deleted": "Dette indlæg er slettet!", + "topic_is_deleted": "Denne tråd er blevet slettet!", + "profile": "Profil", + "posted_by": "Skrevet af %1", + "posted_by_guest": "Skrevet af Gæst", + "chat": "Chat", + "notify_me": "Bliv notificeret ved nye svar i dette emne", + "quote": "Citer", + "reply": "Svar", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Svar som emne", + "guest-login-reply": "Login for at svare", + "login-to-view": "🔒 Log in to view", + "edit": "Rediger", + "delete": "Slet", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Udrens", + "restore": "Gendan", + "move": "Flyt", + "change-owner": "Change Owner", + "fork": "Fork", + "link": "Link", + "share": "Del", + "tools": "Værktøjer", + "locked": "Låst", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Flyttet", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Klik her for at vende tilbage til den sidst læste indlæg i denne tråd.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Denne tråd er blevet slettet. Kun brugere med emne behandlings privilegier kan se den.", + "following_topic.message": "Du vil nu modtage notifikationer når nogle skriver et indlæg i dette emne.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Venligt registrer eller login for at abbonere på dette emne.", + "markAsUnreadForAll.success": "Emnet er market ulæst for alle.", + "mark_unread": "Marker ulæste", + "mark_unread.success": "Emne markeret som ulæst.", + "watch": "Overvåg", + "unwatch": "Fjern overvågning", + "watch.title": "Bliv notificeret ved nye indlæg i dette emne", + "unwatch.title": "Fjern overvågning af dette emne", + "share_this_post": "Del dette indlæg", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Emne værktøjer", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Fastgør tråd", + "thread_tools.unpin": "Frigør tråd", + "thread_tools.lock": "Lås tråd", + "thread_tools.unlock": "Lås tråd op", + "thread_tools.move": "Flyt tråd", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Flyt alt", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Fraskil tråd", + "thread_tools.delete": "Slet tråd", + "thread_tools.delete-posts": "Slet Indlæg", + "thread_tools.delete_confirm": "Er du sikker på at du vil slette dette emne?", + "thread_tools.restore": "Gendan tråd", + "thread_tools.restore_confirm": "Er du sikker på at du ønsker at genoprette denne tråd?", + "thread_tools.purge": "Udrader tråd", + "thread_tools.purge_confirm": "Er du sikker på at du vil udrense denne tråd?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Er du sikker på at du vil slette dette indlæg?", + "post_restore_confirm": "Er du sikker på at du vil gendanne dette indlæg?", + "post_purge_confirm": "Er du sikker på at du vil udradere dette indlæg?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Indlæser kategorier", + "confirm_move": "Flyt", + "confirm_fork": "Fraskil", + "bookmark": "Bogmærke", + "bookmarks": "Bogmærker", + "bookmarks.has_no_bookmarks": "Du har ikke bookmarked nogen indlæg.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Indlæser flere indlæg", + "move_topic": "Flyt tråd", + "move_topics": "Flyt tråde", + "move_post": "Flyt indlæg", + "post_moved": "Indlæg flyttet!", + "fork_topic": "Fraskil tråd", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Klik på indlæg du ønsker at fraskille", + "fork_no_pids": "Ingen indlæg valgt", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Tråden blev fraskilt! Klik her for at gå til den fraskilte tråd.", + "delete_posts_instruction": "Klik på de indlæg du vil slette/rense", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Angiv din trådtittel her ...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Fortryd", + "composer.submit": "Send", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Svare til %1", + "composer.new_topic": "Ny tråd", + "composer.editing": "Editing", + "composer.uploading": "uploader...", + "composer.thumb_url_label": "Indsæt en tråd miniature URL", + "composer.thumb_title": "Tilføj en miniature til denne tråd", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Eller upload en fil", + "composer.thumb_remove": "Slet felter", + "composer.drag_and_drop_images": "Træk og slip billeder her", + "more_users_and_guests": "%1 flere bruger(e) og %2 gæst(er)", + "more_users": "%1 flere bruger(e)", + "more_guests": "%1 flere gæst(er)", + "users_and_others": "%1 og %2 andre", + "sort_by": "Sorter efter", + "oldest_to_newest": "Ældste til nyeste", + "newest_to_oldest": "Nyeste til ældste", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Opret nyt emne istedet?", + "stale.warning": "Emnet du svarer på er ret gammelt. Vil du oprette et nyt emne istedet og referere dette indlæg i dit svar?", + "stale.create": "Opret nyt emne", + "stale.reply_anyway": "Svar dette emne alligevel", + "link_back": "Svar: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/da/unread.json b/public/language/da/unread.json new file mode 100644 index 0000000000..c06180cdff --- /dev/null +++ b/public/language/da/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Ulæst", + "no_unread_topics": "Der er ingen ulæste emner.", + "load_more": "Indlæs mere", + "mark_as_read": "Marker som læst", + "selected": "Valgte", + "all": "Alle", + "all_categories": "Alle kategorier", + "topics_marked_as_read.success": "Emner markeret som læst!", + "all-topics": "Alle Emner", + "new-topics": "Nyt Emner", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/da/uploads.json b/public/language/da/uploads.json new file mode 100644 index 0000000000..651a839876 --- /dev/null +++ b/public/language/da/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Uploading the file...", + "select-file-to-upload": "Select a file to upload!", + "upload-success": "File uploaded successfully!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/da/user.json b/public/language/da/user.json new file mode 100644 index 0000000000..6f0f367d81 --- /dev/null +++ b/public/language/da/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banlyst", + "muted": "Muted", + "offline": "Offline", + "deleted": "Deleted", + "username": "Brugernavn", + "joindate": "Oprettet", + "postcount": "Antal indlæg", + "email": "Email", + "confirm_email": "Bekræft email", + "account_info": "Konto Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Konto", + "ban_account_confirm": "Ønsker du virkelig at banne denne konto?", + "unban_account": "Afban Konto", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Slet konto", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Konto slettet", + "account-content-deleted": "Account content deleted", + "fullname": "Fulde navn", + "website": "Webside", + "location": "Lokation", + "age": "Alder", + "joined": "Oprettet", + "lastonline": "Sidst online", + "profile": "Profil", + "profile_views": "Profil visninger", + "reputation": "Omdømme", + "bookmarks": "Bogmærker", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Set", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Followers", + "following": "Følger", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "Om mig", + "signature": "Signatur", + "birthday": "Fødselsdag", + "chat": "Chat", + "chat_with": "Fortsæt chatte med %1", + "new_chat_with": "Start en ny chat med %1", + "flag-profile": "Flag Profile", + "follow": "Følg", + "unfollow": "Følg ikke", + "more": "Mere", + "profile_update_success": "Din profil blev opdateret", + "change_picture": "Skift billede", + "change_username": "Ændre brugernavn", + "change_email": "Ændre email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Rediger", + "edit-profile": "Rediger Profil", + "default_picture": "Standard ikon", + "uploaded_picture": "Upload billede", + "upload_new_picture": "Upload nyt billede", + "upload_new_picture_from_url": "Upload nyt billede fra URL", + "current_password": "Nuværende kodeord", + "change_password": "Skift kodeord", + "change_password_error": "Ukorrekt kodeord", + "change_password_error_wrong_current": "Nuværende kodeord er ikke korrekt", + "change_password_error_match": "Passwords matcher ikke!", + "change_password_error_privileges": "Du har ikke rettigheder til at ændre dette password.", + "change_password_success": "Dit password er opdateret!", + "confirm_password": "Bekræft kodeord", + "password": "Kodeord", + "username_taken_workaround": "Det valgte brugernavn er allerede taget, så vi har ændret det en smule. Du hedder nu %1", + "password_same_as_username": "Din adgangskode er det samme som dit brugernavn, vælg venligst en anden adgangskode.", + "password_same_as_email": "Dit kodeord er det samme som din email, venligst vælg et andet kodeord", + "weak_password": "Weak password.", + "upload_picture": "Upload billede", + "upload_a_picture": "Upload et billede", + "remove_uploaded_picture": "Fjern uploaded billede", + "upload_cover_picture": "Upload coverbillede", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Indstillinger", + "show_email": "Vis min emailaddresse", + "show_fullname": "Vis mit fulde navn", + "restrict_chats": "Tillad kun chat beskeder fra brugere jeg følger", + "digest_label": "Abonner på sammendrag", + "digest_description": "Abonner på email opdateringer for detta forum (nye notifikationer og indlæg) efter en bestemt køreplan", + "digest_off": "Slukket", + "digest_daily": "Daglig", + "digest_weekly": "Ugentlig", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Månedlig", + "has_no_follower": "Denne bruger har ingen følgere :(", + "follows_no_one": "Denne bruger følger ikke nogen :(", + "has_no_posts": "Denne bruger har ikke skrevet noget endnu.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Denne bruger har ikke skrævet nogle tråde endnu.", + "has_no_watched_topics": "Denne bruger har ikke fulgt nogle tråde endnu.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "Denne bruger har ikke syntes godt om nogle indlæg endnu.", + "has_no_downvoted_posts": "Denne bruger har ikke, syntes ikke godt om nogle indlæg endnu.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Email Skjult", + "hidden": "skjult", + "paginate_description": "Sideinddel emner og indlæg istedet for uendeligt rul", + "topics_per_page": "Emner per side", + "posts_per_page": "Indlæg per side", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Gennemsenings indstillinger", + "open_links_in_new_tab": "Åben udgående link i en ny tab", + "enable_topic_searching": "Slå In-Topic søgning til", + "topic_search_help": "Hvis slået til, så vil in-topic søgning overskrive browserens almindelige søge function og tillade dig at søge hele emnet, istedet for kun det der er vist på skærmen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Vis emner du har oprettet", + "grouptitle": "Gruppe Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Ingen gruppe titel", + "select-skin": "Vælg et skin", + "select-homepage": "Vælg en hjemmeside", + "homepage": "Hjemmeside", + "homepage_description": "Vælg en side som forummets hjemmeside, eller 'Ingen' for at bruge standard hjemmesiden.", + "custom_route": "Brugerdefinerede hjemme rute", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Enkeltgangs Sign-on Servicer", + "sso.associated": "Forbundet med", + "sso.not-associated": "Klik her for at forbinde med", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/da/users.json b/public/language/da/users.json new file mode 100644 index 0000000000..3f1781572f --- /dev/null +++ b/public/language/da/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Seneste brugere", + "top_posters": "Top Postere", + "most_reputation": "Mest Omdømme", + "most_flags": "Most Flags", + "search": "Søg", + "enter_username": "Indtast brugernavn for at søge", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Indlæs mere", + "users-found-search-took": "%1 bruger(e) fundet! Søgning tog %2 sekunder.", + "filter-by": "Filtre Efter", + "online-only": "Kun online", + "invite": "Invitér", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "En invitations email er blevet sendt til %1", + "user_list": "Bruger Liste", + "recent_topics": "Seneste Tråde", + "popular_topics": "Populærer Tråde", + "unread_topics": "Ulæste Tråde", + "categories": "Kategorier", + "tags": "Tags", + "no-users-found": "Ingen brugere fundet!" +} \ No newline at end of file diff --git a/public/language/de/_DO_NOT_EDIT_FILES_HERE.md b/public/language/de/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/de/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/de/admin/admin.json b/public/language/de/admin/admin.json new file mode 100644 index 0000000000..5e153b0727 --- /dev/null +++ b/public/language/de/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Bist du sicher, dass du NodeBB neu bauen und neu starten möchtest?", + "alert.confirm-restart": "Bist du sicher, dass du NodeBB neu starten möchtest?", + + "acp-title": "%1 | NodeBB Admin Systemsteuerung", + "settings-header-contents": "Inhalte", + "changes-saved": "Änderungen gespeichert", + "changes-saved-message": "Deine Änderungen an der NodeBB Konfiguration wurden gespeichert.", + "changes-not-saved": "Änderungen verworfen", + "changes-not-saved-message": "Beim Speichern der Änderungen ist ein Problem aufgetreten. (%1)" +} \ No newline at end of file diff --git a/public/language/de/admin/advanced/cache.json b/public/language/de/admin/advanced/cache.json new file mode 100644 index 0000000000..7818db96ec --- /dev/null +++ b/public/language/de/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post-Cache", + "group-cache": "Gruppen-Cache", + "local-cache": "Lokaler Cache", + "object-cache": "Objekt-Cache", + "percent-full": "%1% Voll", + "post-cache-size": "Post-Cache-Größe", + "items-in-cache": "Elemente im Cache" +} \ No newline at end of file diff --git a/public/language/de/admin/advanced/database.json b/public/language/de/admin/advanced/database.json new file mode 100644 index 0000000000..550c6c8468 --- /dev/null +++ b/public/language/de/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 B", + "x-mb": "%1 MB", + "x-gb": "%1 GB", + "uptime-seconds": "Betriebszeit in Sekunden", + "uptime-days": "Betriebszeit in Tagen", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Speicher-Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objekte", + "mongo.avg-object-size": "Durchschnittliche Objektgröße", + "mongo.data-size": "Datengröße", + "mongo.storage-size": "Speichergröße", + "mongo.index-size": "Indexgröße", + "mongo.file-size": "Dateigröße", + "mongo.resident-memory": "Permanenter Speicher", + "mongo.virtual-memory": "Virtueller Speicher", + "mongo.mapped-memory": "Zugeordneter Speicher", + "mongo.bytes-in": "Bytes eingehend", + "mongo.bytes-out": "Bytes ausgehend", + "mongo.num-requests": "Anzahl an Anfragen", + "mongo.raw-info": "MongoDB Rohinfo", + "mongo.unauthorized": "NodeBB konnte die MongoDB Datenbank für relevante Statistiken nicht abfragen. Stellen Sie bitte sicher, dass der von NodeBB verwendete Benutzer über die Rolle "clusterMonitor" für die "admin" Datenbank verfügt.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Schlüssel", + "redis.expires": "Ablauf", + "redis.avg-ttl": "Durchschnittliche TTL", + "redis.connected-clients": "Verbundene Clients", + "redis.connected-slaves": "Verbundene Slaves", + "redis.blocked-clients": "Blockierte Clients", + "redis.used-memory": "Speicherverbrauch", + "redis.memory-frag-ratio": "Speicherfragmentierungsgrad", + "redis.total-connections-recieved": "Gesamtzahl der empfangenen Verbindungen", + "redis.total-commands-processed": "Gesamtzahl der verarbeiteten Befehle", + "redis.iops": "Sofortige Operationen. Pro Sekunde", + "redis.iinput": "Sofortige Eingabe pro Sekunde", + "redis.ioutput": "Sofortige Ausgabe pro Sekunde", + "redis.total-input": "Gesamt Eingabe", + "redis.total-output": "Gesamt Ausgabe", + + "redis.keyspace-hits": "Schlüsselraum-Treffer", + "redis.keyspace-misses": "Schlüsselraum-Verfehlungen", + "redis.raw-info": "Redis Rohinfo", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Rohinformation" +} diff --git a/public/language/de/admin/advanced/errors.json b/public/language/de/admin/advanced/errors.json new file mode 100644 index 0000000000..5ec358d8fe --- /dev/null +++ b/public/language/de/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Abbildung %1", + "error-events-per-day": "%1 Ereignisse pro Tag", + "error.404": "404 Nicht gefunden", + "error.503": "503 Dienst nicht verfügbar", + "manage-error-log": "Fehlerprotokoll verwalten", + "export-error-log": "Fehlerprotokoll (CSV) exportieren", + "clear-error-log": "Fehlerprotokoll leeren", + "route": "Pfad", + "count": "Anzahl", + "no-routes-not-found": "Hurra! Keine 404 Fehler!", + "clear404-confirm": "Bist du dir sicher, dass du das 404 Fehlerprotokoll löschen möchtest?", + "clear404-success": "\"404 Not Found\" Fehler gelöscht" +} \ No newline at end of file diff --git a/public/language/de/admin/advanced/events.json b/public/language/de/admin/advanced/events.json new file mode 100644 index 0000000000..a0760a44eb --- /dev/null +++ b/public/language/de/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Ereignisse", + "no-events": "Es gibt keine Ereignisse", + "control-panel": "Ereignissteuerung", + "delete-events": "Ereignisse löschen", + "confirm-delete-all-events": "Bist du sicher, dass du alle gespeicherten Events löschen möchtest?", + "filters": "Filter", + "filters-apply": "Filter anwenden", + "filter-type": "Ereignistyp", + "filter-start": "Anfangsdatum", + "filter-end": "Enddatum", + "filter-perPage": "Pro Seite" +} \ No newline at end of file diff --git a/public/language/de/admin/advanced/logs.json b/public/language/de/admin/advanced/logs.json new file mode 100644 index 0000000000..e0f8980059 --- /dev/null +++ b/public/language/de/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Protokolle", + "control-panel": "Logs-Systemsteuerung", + "reload": "Protokolle neu laden", + "clear": "Protokolle löschen", + "clear-success": "Protokolle gelöscht" +} \ No newline at end of file diff --git a/public/language/de/admin/appearance/customise.json b/public/language/de/admin/appearance/customise.json new file mode 100644 index 0000000000..6424bc3dfb --- /dev/null +++ b/public/language/de/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Benutzerdefiniertes CSS/LESS", + "custom-css.description": "Füge deine eigenen CSS/LESS deklarationen hier ein, die nach allen anderen Styles angewandt werden.", + "custom-css.enable": "Benutzerdefiniertes CSS/LESS aktivieren", + + "custom-js": "Benutzerdefiniertes Javascript", + "custom-js.description": "Füge dein eigenes Javascipt hier ein.\nEs wird ausgeführt nachdem die Seite komplett geladen wurde.", + "custom-js.enable": "Benutzerdefiniertes Javascript aktivieren", + + "custom-header": "Benutzerdefinierter Header", + "custom-header.description": "Füge hier dein eigenes HTML ein (zum Beispiel Meta-Tags, etc.), die dann dem <head>-Element des Forums hinzugefügt werden. Script-Tags sind erlaubt, jedoch wird davon abgeraten, weil für diesen Zweck der Tab Benutzerdefiniertes Javascript existiert.", + "custom-header.enable": "Benutzerdefinierten Header aktivieren", + + "custom-css.livereload": "Live-Aktualisierung aktivieren", + "custom-css.livereload.description": "Aktiviere diese Einstellung um alle Sitzungen auf allen Geräten mit deinem Konto dazu zu zwingen Neuzuladen sobald du \"Speichern\" drückst" +} \ No newline at end of file diff --git a/public/language/de/admin/appearance/skins.json b/public/language/de/admin/appearance/skins.json new file mode 100644 index 0000000000..c56d74113f --- /dev/null +++ b/public/language/de/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Skins werden geladen...", + "homepage": "Startseite", + "select-skin": "Skin auswählen", + "current-skin": "Aktueller Skin", + "skin-updated": "Skin aktualisiert", + "applied-success": "Skin %1 wurde erfolgreich angewendet", + "revert-success": "Skin auf Basisfarben zurückgestellt." +} \ No newline at end of file diff --git a/public/language/de/admin/appearance/themes.json b/public/language/de/admin/appearance/themes.json new file mode 100644 index 0000000000..3a0efd7c8b --- /dev/null +++ b/public/language/de/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Auf installierte Themes wird geprüft...", + "homepage": "Startseite", + "select-theme": "Theme wählen", + "current-theme": "Aktuelles Theme", + "no-themes": "Keine installierten Theme gefunden.", + "revert-confirm": "Bist du sicher, dass du das standardmäßige NodeBB-Design wiederherstellen möchten?", + "theme-changed": "Theme geändert", + "revert-success": "Du hast dein NodeBB erfolgreich wieder auf das Standard-Theme zurückgesetzt.", + "restart-to-activate": "Bitte builde und starte dein NodeBB neu um das Theme zu aktivieren." +} \ No newline at end of file diff --git a/public/language/de/admin/dashboard.json b/public/language/de/admin/dashboard.json new file mode 100644 index 0000000000..aa25120bae --- /dev/null +++ b/public/language/de/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Seitenaufrufe", + "unique-visitors": "Individuelle Besucher", + "logins": "Anmeldungen", + "new-users": "Neue nutzende Person", + "posts": "Beiträge", + "topics": "Themen", + "page-views-seven": "Letzte 7 Tage", + "page-views-thirty": "Letzte 30 Tage", + "page-views-last-day": "Letzte 24 Stunden", + "page-views-custom": "Benutzerdefinierte Tagesspanne", + "page-views-custom-start": "Spannen-Anfang", + "page-views-custom-end": "Spannen-Ende", + "page-views-custom-help": "Gebe einen Datumsbereich für Seitenaufrufe ein, die du anzeigen möchtest. Wenn keine Datumsauswahl verfügbar ist, ist das akzeptierte Format YYYY-MM-DD", + "page-views-custom-error": "Bitte gib eine gültige Zeitspanne im Format YYYY-MM-DD an", + + "stats.yesterday": "Gestern", + "stats.today": "Heute", + "stats.last-week": "Letzte Woche", + "stats.this-week": "Diese Woche", + "stats.last-month": "Letzter Monat", + "stats.this-month": "Dieser Monat", + "stats.all": "Alle", + + "updates": "Updates", + "running-version": "Es läuft NodeBB v%1.", + "keep-updated": "Stelle sicher, dass dein NodeBB immer auf dem neuesten Stand für die neuesten Sicherheits-Patches und Bug-fixes ist.", + "up-to-date": "

NodeBB Version ist aktuell

", + "upgrade-available": "

Eine neuere Version (v%1) ist erschienen. Erwäge NodeBB zu upgraden.

", + "prerelease-upgrade-available": "

Das ist eine veraltete NodeBB-Vorabversion. Eine neuere Version (v%1) ist erschienen. Erwäge NodeBB zu upgraden.

", + "prerelease-warning": "

Das ist eine Vorabversion von NodeBB. Es können ungewollte Fehler auftreten.

", + "fallback-emailer-not-found": "Fallback-Emailer nicht gefunden", + "running-in-development": "Das Forum wurde im Entwicklermodus gestartet. Das Forum könnte potenziellen Gefahren ausgeliefert sein. Bitte kontaktiere den Systemadministrator.", + "latest-lookup-failed": "

Beim nachschlagen der neuesten verfügbaren NodeBB Version ist ein Fehler aufgetreten

", + + "notices": "Hinweise", + "restart-not-required": "Kein Neustart benötigt", + "restart-required": "Neustart benötigt", + "search-plugin-installed": "Such-Plugin installiert", + "search-plugin-not-installed": "Kein Such-Plugin installiert", + "search-plugin-tooltip": "Installiere ein Such-Plugin auf der Plugin-Seite um die Such-Funktionalität zu aktivieren", + + "control-panel": "Systemsteuerung", + "rebuild-and-restart": "Regenerieren & Neustarten", + "restart": "Neustarten", + "restart-warning": "NodeBB zu regenerieren oder neuzustarten wird alle existierenden Verbindungen für ein paar Sekunden trennen.", + "restart-disabled": "Das Regenerieren und Neustarten von NodeBB wurde deaktiviert, da es nicht so aussieht als ob es über einem kompatiblem daemon läuft.", + "maintenance-mode": "Wartungsmodus", + "maintenance-mode-title": "Hier klicken um NodeBB in den Wartungsmodus zu versetzen", + "realtime-chart-updates": "Echtzeit Chartaktualisierung", + + "active-users": "Aktive Benutzer", + "active-users.users": "Benutzer", + "active-users.guests": "Gäste", + "active-users.total": "Gesamt", + "active-users.connections": "Verbindungen", + + "guest-registered-users": "Gast vs. registrierte Benutzer", + "guest": "Gast", + "registered": "Registriert", + + "user-presence": "Benutzerpräsenz", + "on-categories": "Auf Kategorieübersicht", + "reading-posts": "Beiträge lesend", + "browsing-topics": "Themen durchsuchend", + "recent": "Aktuell", + "unread": "Ungelesen", + + "high-presence-topics": "Meist besuchte Themen", + "popular-searches": "Beliebte Suchanfragen", + + "graphs.page-views": "Seitenaufrufe", + "graphs.page-views-registered": "Registrierte Seitenaufrufe", + "graphs.page-views-guest": "Seitenaufrufe von Gästen", + "graphs.page-views-bot": "Seitenaufrufe von Bots", + "graphs.unique-visitors": "Verschiedene Besucher", + "graphs.registered-users": "Registrierte Benutzer", + "graphs.guest-users": "Gast-Benutzer", + "last-restarted-by": "Zuletzt Neugestartet von: ", + "no-users-browsing": "Keine aktiven Benutzer", + + "back-to-dashboard": "Zurück zur Übersicht", + "details.no-users": "Keine Benutzer sind im gewählten Zeitraum beigetreten", + "details.no-topics": "Keine Themen wurden im gewählten Zeitraum beigetreten", + "details.no-searches": "Es wurden noch keine Suchen durchgeführt", + "details.no-logins": "Keine Logins wurden im gewählten Zeitraum festgestellt", + "details.logins-static": "NodeBB speichert Sitzungsdaten nur für %1 Tage, deshalb zeigt die untere Tabelle nur die neuesten, aktiven Sitzungen", + "details.logins-login-time": "Anmelde Zeit" +} diff --git a/public/language/de/admin/development/info.json b/public/language/de/admin/development/info.json new file mode 100644 index 0000000000..082f1b212c --- /dev/null +++ b/public/language/de/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Sie verwenden %1:%2", + "ip": "IP%1", + "nodes-responded": "%1 Knoten antworteten innerhalb von %2ms", + "host": "Host", + "primary": "Primärer / Laufjob", + "pid": "PID", + "nodejs": "Node.js Version", + "online": "Online", + "git": "git", + "process-memory": "Prozess-Speicher", + "system-memory": "System-Speicher", + "used-memory-process": "Verwendeter Prozess-Speicher", + "used-memory-os": "Verwendeter System-Speicher", + "total-memory-os": "Gesamter System-Speicher", + "load": "Systemlast", + "cpu-usage": "CPU Benutzung", + "uptime": "Uptime", + + "registered": "Registriert", + "sockets": "Sockets", + "guests": "Gäste", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/de/admin/development/logger.json b/public/language/de/admin/development/logger.json new file mode 100644 index 0000000000..bf4f4a16ac --- /dev/null +++ b/public/language/de/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Protokollierungseinstellungen", + "description": "Durch das markieren der Auswahlkästchen werden sie Protokolle in ihrem Terminal finden. Wenn sie einen Dateipfad angeben, werden die Protokolle stattdessen in einer Datei gespeichert. HTTP-Logging ist nützlich um Statistiken zu sammeln darüber, wer, wann was in ihrem Forum angesehen hat. Zusätzlich kann NodeBB auch Socket.io Events Protokollieren. In Kombination mit dem redis-cli Monitor kann dies ziemlich hilfreich sein um mehr über die Interne Struktur von NodeBB zu lernen.", + "explanation": "Markiere die Protokollierungseinstellungen nebenher um die Protokollierung zu (de-)aktivieren. Ein Neustart wird nicht benötigt.", + "enable-http": "HTTP-Protokollierung aktivieren", + "enable-socket": "Socket.io-Event-Protokollierung aktivieren", + "file-path": "Dateipfad zur Protokolldatei", + "file-path-placeholder": "/pfad/zur/protokoll/datei.log ::: Feld leer lassen um im Terminal zu protokollieren", + + "control-panel": "Protokollsteuerung", + "update-settings": "Protokollierungseinstellungen aktualisieren" +} \ No newline at end of file diff --git a/public/language/de/admin/extend/plugins.json b/public/language/de/admin/extend/plugins.json new file mode 100644 index 0000000000..23670b54ae --- /dev/null +++ b/public/language/de/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Beliebt", + "installed": "Installiert", + "active": "Aktiv", + "inactive": "Inaktiv", + "out-of-date": "Veraltet", + "none-found": "Keine Plugins gefunden.", + "none-active": "Keine aktiven Plugins", + "find-plugins": "Plugins finden", + + "plugin-search": "Plugin Suche", + "plugin-search-placeholder": "Nach Plugin suchen...", + "submit-anonymous-usage": "Übermitteln Sie anonyme Plugin-Nutzungsdaten.", + "reorder-plugins": "Plugins neu sortieren", + "order-active": "Aktive Plugins sortieren", + "dev-interested": "Daran interessiert selbst Plugins für NodeBB zu schreiben?", + "docs-info": "Die komplette Dokumentation zum erstellen von Plugins kann im NodeBB Docs Portal gefunden werden.", + + "order.description": "Bestimmte Plugins funktionieren optimal, wenn diese vor/nach anderen Plugins initialisiert werden.", + "order.explanation": "Die Plugins werden in der hier spezifizierten Reihenfolge geladen, von oben nach unten", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deaktivieren", + "plugin-item.activate": "Aktivieren", + "plugin-item.install": "Installieren", + "plugin-item.uninstall": "Deinstallieren", + "plugin-item.settings": "Einstellungen", + "plugin-item.installed": "Installiert", + "plugin-item.latest": "Neueste", + "plugin-item.upgrade": "Aktualisieren", + "plugin-item.more-info": "Für weitere Informationen:", + "plugin-item.unknown": "Unbekannt", + "plugin-item.unknown-explanation": "Der Status dieses Plugins konnte nicht bestimmt werden, möglicherweise aufgrund eines Konfigurationsfehlers.", + "plugin-item.compatible": "Dieses Plugin funktioniert mit NodeBB %1", + "plugin-item.not-compatible": "Dieses Plugin hat keine Kompatibilitätsdaten. Stellen Sie sicher, dass es funktioniert, bevor Sie es in Ihrer Produktionsumgebung installieren.", + + "alert.enabled": "Plugin aktiviert", + "alert.disabled": "Plugin deaktiviert", + "alert.upgraded": "Plugin aktualisiert", + "alert.installed": "Plugin installiert", + "alert.uninstalled": "Plugin deinstalliert", + "alert.activate-success": "Bitte builde dein NodeBB neu auf und starte es neu, um dieses Plugin vollständig zu aktivieren", + "alert.deactivate-success": "Plugin erfolgreich deaktiviert", + "alert.upgrade-success": "Bitte starte dein NodeBB neu um dieses Plugin völlständig zu upgraden.", + "alert.install-success": "Plugin erfolgreich installiert. Bitte aktiviere das Plugin", + "alert.uninstall-success": "Das Plugin wurde erfolgreich deaktiviert und deinstalliert.", + "alert.suggest-error": "

NodeBB konnte den Paket-Manager nicht erreichen. Willst Du mit der Installation der neuesten Version fortfahren

Der Server meldete (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB konnte den Paket-Manager nicht erreichen, eine Aktualisierung wird momentan nicht empfohlen.

", + "alert.incompatible": "

NodeBB Version (v%1) ist nur für Aktualisierungen bis v%2 dieses Plugins bestimmt. Bitte aktualisiere NodeBB, wenn eine neuere Version dieses Plugins installiert werden soll.

", + "alert.possibly-incompatible": "

Keine Kompatibilitätsinformationen gefunden

Dieses Plugin legte keine spezifische NodeBB version fest, welche für die Installation benötigt wird. Volle Kompatibilität kann nicht gewährleistet werden, was dazu führen könnte, dass ihr NodeBB nicht mehr korrekt startet.

Für den Fall, dass NodeBB nicht mehr ordnungsgemäß startet:

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

Soll mit der installation der neuesten Version dieses Plugins fortgefahren werden?

", + "alert.reorder": "Plugins Neusortiert", + "alert.reorder-success": "Bitte starte dein NodeBB neu um diesen Prozess vollständig abzuschließen.", + + "license.title": "Plugin-Lizenzinformation", + "license.intro": "Das Plugin %1is unter der %2 lizenziert. Bitte ließ dir diese durch bevor du dieses Plugin aktivierst.", + "license.cta": "Willst du dieses Plugin wirklich aktivieren?" +} diff --git a/public/language/de/admin/extend/rewards.json b/public/language/de/admin/extend/rewards.json new file mode 100644 index 0000000000..d0ed8e051d --- /dev/null +++ b/public/language/de/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Belohnungen", + "condition-if-users": "Wenn Benutzer", + "condition-is": "Ist:", + "condition-then": "Dann:", + "max-claims": "Anzahl der Male von Belohnungen, die beansprucht werden können", + "zero-infinite": "0 für unendlich eingeben", + "delete": "Löschen", + "enable": "Aktivieren", + "disable": "Deaktivieren", + + "alert.delete-success": "Belohnung wurde erfolgreich gelöscht", + "alert.no-inputs-found": "Ungültige Belohnung - keine Eingaben gefunden!", + "alert.save-success": "Belohnungen erfolgreich gespeichert" +} \ No newline at end of file diff --git a/public/language/de/admin/extend/widgets.json b/public/language/de/admin/extend/widgets.json new file mode 100644 index 0000000000..9497575e33 --- /dev/null +++ b/public/language/de/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Verfügbare Widgets", + "explanation": "Wähle ein Widget aus dem Dropdown-Menü aus und ziehe es per Drag-and-Drop in den Widget-Bereich einer Vorlage auf der linken Seite.", + "none-installed": "Keine Widgets gefunden! Aktiviere das Widget Essentials-Plugin in der Plugin-Systemsteuerung.", + "clone-from": "Klone Widget von", + "containers.available": "Verfügbare Container", + "containers.explanation": "Per Drag-and-Drop auf ein beliebiges aktives Widget ziehen", + "containers.none": "Nichts", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel-Kopfzeile", + "container.panel-body": "Panel-Körper", + "container.alert": "Alarm", + + "alert.confirm-delete": "Möchtest Du dieses Widget wirklich löschen?", + "alert.updated": "Widgets aktualisiert", + "alert.update-success": "Widgets erfolgreich aktualisiert", + "alert.clone-success": "Widgets erfolgreich geklont", + + "error.select-clone": "Bitte wähle eine Seite aus, von der geklont werden soll", + + "title": "Titel", + "title.placeholder": "Titel (wird nur auf einigen Containern angezeigt)", + "container": "Container", + "container.placeholder": "Ziehe einen Container per Drag-and-Drop oder gebe hier HTML ein.", + "show-to-groups": "Gruppen anzeigen", + "hide-from-groups": "Vor Gruppen verstecken", + "hide-on-mobile": "Auf dem Handy verstecken" +} \ No newline at end of file diff --git a/public/language/de/admin/manage/admins-mods.json b/public/language/de/admin/manage/admins-mods.json new file mode 100644 index 0000000000..3188af49d5 --- /dev/null +++ b/public/language/de/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administratoren", + "global-moderators": "Globale Moderatoren", + "moderators": "Moderatoren", + "no-global-moderators": "Keine globalen Moderatoren", + "no-sub-categories": "Keine Unterkategorien", + "subcategories": "%1 Unterkategorien", + "no-moderators": "Keine Moderatoren", + "add-administrator": "Administrator hinzufügen", + "add-global-moderator": "Globalen Moderator hinzufügen", + "add-moderator": "Moderator hinzufügen" +} \ No newline at end of file diff --git a/public/language/de/admin/manage/categories.json b/public/language/de/admin/manage/categories.json new file mode 100644 index 0000000000..f997db33aa --- /dev/null +++ b/public/language/de/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Kategorieeinstellungen", + "privileges": "Berechtigungen", + + "name": "Kategoriename", + "description": "Kategorie-Beschreibung", + "bg-color": "Hintergrundfarbe", + "text-color": "Textfarbe", + "bg-image-size": "Hintergrundbildgröße", + "custom-class": "Benutzderdefinierte Klasse", + "num-recent-replies": "Anzahl neuer Antworten", + "ext-link": "Externer Link", + "subcategories-per-page": "Subkategorien pro Seite", + "is-section": "Behandle diese Kategorie als Abschnitt", + "post-queue": "Warteschlange", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Bild hochladen", + "delete-image": "Entfernen", + "category-image": "Kategoriebild", + "parent-category": "Übergeordnete Kategorie", + "optional-parent-category": "(Optional) Übergeordnete Kategorie", + "top-level": "Top Level", + "parent-category-none": "(Nichts)", + "copy-parent": "Übergeordnetes kopieren", + "copy-settings": "Kopiere Einstellungen von", + "optional-clone-settings": "(Optional) dubliziere Einstellungen von Kategorie", + "clone-children": "Kopiere Subkategorien und Einstellungen", + "purge": "Kategorie löschen", + + "enable": "Aktivieren", + "disable": "Deaktivieren", + "edit": "Bearbeiten", + "analytics": "Analytik", + "view-category": "Kategorie anzeigen", + "set-order": "Reihenfolge festlegen", + "set-order-help": "Wenn Sie die Reihenfolge der Kategorie festlegen, wird diese Kategorie in diese Reihenfolge verschoben und die Reihenfolge anderer Kategorien nach Bedarf aktualisiert. Die Mindestbestellmenge beträgt 1, was die Kategorie an die Spitze bringt.", + + "select-category": "Kategorie auswählen", + "set-parent-category": "Übergeordnete Kategorie festlegen", + + "privileges.description": "In diesem Abschnitt können Sie die Zugriffssteuerungsberechtigungen für Teile der Seite konfigurieren. Berechtigungen können auf Benutzerbasis oder auf Gruppenbasis gewährt werden. Wählen Sie aus der Dropdown-Liste die Effektdomäne aus.", + "privileges.category-selector": "Konfiguriere Privilegien für", + "privileges.warning": "Hinweis: Die Zugriffsberechtigungen werden sofort wirksam. Es ist nicht notwendig, die Kategorie zu speichern, nachdem du die Einstellungen angepasst hast.", + "privileges.section-viewing": "Ansichtsberechtigungen", + "privileges.section-posting": "Schreibberechtigungen", + "privileges.section-moderation": "Moderationsberechtigungen", + "privileges.section-other": "Andere", + "privileges.section-user": "Benutzer", + "privileges.search-user": "Benutzer hinzufügen", + "privileges.no-users": "Keine benutzerspezifischen Berechtigungen in dieser Kategorie.", + "privileges.section-group": "Gruppe", + "privileges.group-private": "Diese Gruppe ist privat", + "privileges.inheritance-exception": "Diese Gruppe erbt keine Berechtigungen von der Gruppe der registrierten Benutzer", + "privileges.banned-user-inheritance": "Gesperrte Benutzer erben Privilegien von der Gruppe der gesperrten Benutzer", + "privileges.search-group": "Gruppe hinzufügen", + "privileges.copy-to-children": "In Untergeordnete kopieren", + "privileges.copy-from-category": "Kopiere von Kategorie", + "privileges.copy-privileges-to-all-categories": "In alle Kategorien kopieren", + "privileges.copy-group-privileges-to-children": "Kopieren Sie die Privilegien dieser Gruppe auf die untergeordneten Elemente dieser Kategorie.", + "privileges.copy-group-privileges-to-all-categories": "Kopieren Sie die Berechtigungen dieser Gruppe in alle Kategorien.", + "privileges.copy-group-privileges-from": "Kopieren Sie die Berechtigungen dieser Gruppe aus einer anderen Kategorie.", + "privileges.inherit": "Wenn der Gruppe registered-users eine bestimmte Berechtigung erteilt wird, erhalten alle anderen Gruppen eine implizite Berechtigung, auch wenn sie nicht explizit definiert / ausgewählt werden. Diese implizite Berechtigung wird dir angezeigt, da alle Benutzer Teil der Gruppe registered-users sind und daher keine Berechtigungen für zusätzliche Gruppen explizit erteilt werden müssen.", + "privileges.copy-success": "Berechtigungen kopiert!", + + "analytics.back": "Zurück zur Kategorien Übersicht", + "analytics.title": "Analyse für \"%1\" Kategorie", + "analytics.pageviews-hourly": "Diagramm 1 – Stündliche Seitenaufrufe in dieser Kategorie", + "analytics.pageviews-daily": "Diagramm 2 – Tägliche Seitenaufrufe in dieser Kategorie", + "analytics.topics-daily": "Diagramm 3 – Täglich erstellte Themen in dieser Kategorie", + "analytics.posts-daily": "Diagramm 4 – Täglich erstellte Beiträge in dieser Kategorie", + + "alert.created": "Erstellt", + "alert.create-success": "Kategorie erfolgreich erstellt!", + "alert.none-active": "Du hast keine aktiven Kategorien.", + "alert.create": "Erstelle eine Kategorie", + "alert.confirm-purge": "

Möchtest du die Kategorie \"%1\" wirklich löschen?

Warnung! Alle Themen und Beiträge in dieser Kategorie werden gelöscht!

Löschen einer Kategorie wird alle Themen und Beiträge zu entfernen, und die Kategorie aus der Datenbank löschen. Falls du eine Kategorie temporär entfernen möchstest, dann kannst du sie stattdessen \"deaktivieren\".", + "alert.purge-success": "Kategorie gelöscht!", + "alert.copy-success": "Einstellungen kopiert!", + "alert.set-parent-category": "Übergeordnete Kategorie festlegen", + "alert.updated": "Kategorien aktualisiert", + "alert.updated-success": "Kategorie IDs %1 erfolgreich aktualisiert.", + "alert.upload-image": "Kategoriebild hochladen", + "alert.find-user": "Benutzer finden", + "alert.user-search": "Hier nach einem Benutzer suchen...", + "alert.find-group": "Gruppe finden", + "alert.group-search": "Hier nach einer Gruppe suchen...", + "alert.not-enough-whitelisted-tags": "Whitelist-Tags sind weniger als die Mindest-Tags, Sie müssen mehr Whitelist-Tags erstellen!", + "collapse-all": "Alle einklappen", + "expand-all": "Alle ausklappen", + "disable-on-create": "Deaktiviere beim erstellen", + "no-matches": "Keine Treffer" +} \ No newline at end of file diff --git a/public/language/de/admin/manage/digest.json b/public/language/de/admin/manage/digest.json new file mode 100644 index 0000000000..98b75f8e5c --- /dev/null +++ b/public/language/de/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Nachfolgend ist eine Auflistung der Zustellungsstatistiken und -zeiten zusammengefasst.", + "disclaimer": "Bitte beachten Sie, dass die Zustellung von E-Mails aufgrund der Funktionsweise von E-Mail-Technologien nicht garantiert werden kann. Ob eine E-Mail im Posteingang des Benutzers auf dem an Empfängerserver letztendlich ankommt, hängt von vielen Variablen ab, z. B. von der Reputation des Servers, von IP-Adressen, die auf der schwarzen Liste stehen, und davon, ob DKIM/SPF/DMARC konfiguriert ist.", + "disclaimer-continued": "Eine erfolgreiche Zustellung zeigt an, dass die Nachricht erfolgreich von NodeBB gesendet und vom Empfänger-Server bestätigt wurde. Es bedeutet nicht, dass die E-Mail im Posteingang gelandet ist. Um beste Ergebnisse zu erzielen, empfehlen wir, einen E-Mail-Zustelldienst eines Drittanbieters wie SendGrid zu verwenden.", + + "user": "Benutzer", + "subscription": "Abonnement Typ", + "last-delivery": "Letzte erfolgreiche Zustellung", + "default": "System Standard", + "default-help": "Systemstandard bedeutet, dass der Benutzer die globale Foreneinstellung für Tagesübersichten nicht explizit außer Kraft gesetzt hat, die derzeit wie folgt lautet: "%1"", + "resend": "Tagesübersicht erneut senden", + "resend-all-confirm": "Sind Sie sicher, dass Sie diesen Tagesübersichts-Lauf manuell ausführen möchten?", + "resent-single": "Manuelles Übersichtversenden abgeschlossen", + "resent-day": "Tägliche Übersicht erneut gesendet", + "resent-week": "Wöchentliche Übersicht erneut gesendet", + "resent-biweek": "Zweiwöchentliche Übersicht erneut gesendet", + "resent-month": "Monatliche Übersicht erneut gesendet", + "null": "Niemals", + "manual-run": "Manueller Tagesübersichts-Lauf:", + + "no-delivery-data": "Keine Zustelldaten gefunden" +} diff --git a/public/language/de/admin/manage/groups.json b/public/language/de/admin/manage/groups.json new file mode 100644 index 0000000000..c9cf421577 --- /dev/null +++ b/public/language/de/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Gruppenname", + "badge": "Abzeichen", + "properties": "Eigenschaften", + "description": "Gruppenbeschreibung", + "member-count": "Mitglieder Anzahl", + "system": "System", + "hidden": "Versteckt", + "private": "Privat", + "edit": "Ändern", + "delete": "Löschen", + "privileges": "Berechtigungen", + "download-csv": "CSV", + "search-placeholder": "Suchen", + "create": "Gruppe erstellen", + "description-placeholder": "Eine kurze Beschreibung deiner Gruppe", + "create-button": "Erstellen", + + "alerts.create-failure": "Oh Oh

Ein Problem ist beim erstellen deiner Gruppe aufgetreten. Bitte versuche es später noch mal!

", + "alerts.confirm-delete": "Diese Gruppe wirklich löschen ?", + + "edit.name": "Name", + "edit.description": "Beschreibung", + "edit.user-title": "Titel der Mitglieder", + "edit.icon": "Gruppenbild", + "edit.label-color": "Gruppenlabelfarbe", + "edit.text-color": "Gruppen-Textfarbe", + "edit.show-badge": "Abzeichen zeigen", + "edit.private-details": "Wenn aktiviert, benögt das Beitreten von Gruppen das Einverständnis eines Gruppenbesitzers", + "edit.private-override": "Warnung: Private Gruppen sind auf System-Level deaktiviert, was diese Option überschreibt.", + "edit.disable-join": "Beitrittsanfragen deaktivieren", + "edit.disable-leave": "Benutzer daran hindern, die Gruppe zu verlassen", + "edit.hidden": "Versteckt", + "edit.hidden-details": "Wenn aktiviert, wird diese Gruppe nicht im Gruppen-Listing angezeigt und Benutzer müssten manuell eingeladen werden.", + "edit.add-user": "Benutzer zur Gruppe hinzufügen", + "edit.add-user-search": "Benutzer suchen", + "edit.members": "Mitgliederliste", + "control-panel": "Gruppeneinstellungen", + "revert": "Rückgängig machen", + + "edit.no-users-found": "Keine Benutzer gefunden", + "edit.confirm-remove-user": "Sind Sie sicher, dass Sie diesen Benutzer entfernen wollen?", + "edit.save-success": "Änderungen gespeichert!" +} \ No newline at end of file diff --git a/public/language/de/admin/manage/privileges.json b/public/language/de/admin/manage/privileges.json new file mode 100644 index 0000000000..a90cc6265e --- /dev/null +++ b/public/language/de/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Administrator", + "group-privileges": "Gruppen Rechte", + "user-privileges": "Benutzer Rechte", + "edit-privileges": "Rechte bearbeiten", + "select-clear-all": "Alle Aus-/Abwählen", + "chat": "Chat", + "upload-images": "Bilder hochladen", + "upload-files": "Dateien hochladen", + "signature": "Signatur", + "ban": "Bannen", + "mute": "Stummschalten", + "invite": "Einladen", + "search-content": "Inhalt durchsuchen", + "search-users": "Benutzersuche", + "search-tags": "Tags suchen", + "view-users": "Benutzer ansehen", + "view-tags": "Tags ansehen", + "view-groups": "Gruppen ansehen", + "allow-local-login": "Lokaler Login", + "allow-group-creation": "Gruppen erstellen", + "view-users-info": "Benutzerinfo anzeigen", + "find-category": "Kategorie finden", + "access-category": "Kategoriezutritt", + "access-topics": "Themenzutritt", + "create-topics": "Themen erstellen", + "reply-to-topics": "Auf Themen antworten", + "schedule-topics": "Geplante Themen", + "tag-topics": "Themen taggen", + "edit-posts": "Beiträge editieren", + "view-edit-history": "Beitragsänderungsverlauf ansehen", + "delete-posts": "Beiträge entfernen", + "view_deleted": "Sehen gelöschter Beiträge", + "upvote-posts": "Beiträge positiv bewerten", + "downvote-posts": "Beiträge negativ bewerten", + "delete-topics": "Themen entfernen", + "purge": "Endgültig löschen", + "moderate": "Moderieren", + "admin-dashboard": "Übersicht", + "admin-categories": "Kategorien", + "admin-privileges": "Rechte", + "admin-users": "Nutzende Personen", + "admin-admins-mods": "Administratoren & Moderatoren", + "admin-groups": "Gruppen", + "admin-tags": "Schlagworte", + "admin-settings": "Einstellungen", + + "alert.confirm-moderate": "Bist Du sicher, dass du dieser Gruppe das Moderationsrecht gewähren möchtest? Diese Gruppe ist öffentlich, und alle Benutzer können nach Belieben beitreten.", + "alert.confirm-admins-mods": "Bist Du sicher, dass du \"Administrator & Moderator\" Rechte zu dieser Gruppe hinzufügen willst?Benutzer mit diesen Rechten können andere Benutzer in privilegierte Positionen heraufstufen und herabstufen, super Administrator eingeschlossen!", + "alert.confirm-save": "Bitte bestätige Deine Absicht, diese Rechte zu speichern", + "alert.saved": "Änderungen an Rechten gespeichert und angewendet", + "alert.confirm-discard": "Bist du sicher, dass du die Änderungen an den Rechten verwerfen möchtest?", + "alert.discarded": "Änderungen an Rechten verworfen", + "alert.confirm-copyToAll": "Bist Du dir sicher, dass Du die Rechte von %1 auf alle Kategorien anwenden möchtest?", + "alert.confirm-copyToAllGroup": "Bist Du dir sicher, dass Du diesen Gruppen-Satz %1 auf alle Kategorien anwenden möchtest?", + "alert.confirm-copyToChildren": "Bist Du dir sicher, dass Du diesen Satz von %1 auf alle (untergeordneten) Kategorien anwenden möchtest?", + "alert.confirm-copyToChildrenGroup": "Bist Du dir sicher, dass Du den Gruppen-Satz von %1 auf alle (untergeordneten) Kategorien anwenden möchten?", + "alert.no-undo": "Dieser Vorgang kann nicht rückgängig gemacht werden.", + "alert.admin-warning": "Administratoren erhalten implizit alle Berechtigungen", + "alert.copyPrivilegesFrom-title": "Wähle eine Kategorie aus, aus der kopiert werden soll", + "alert.copyPrivilegesFrom-warning": "Dadurch wird %1 aus der ausgewählten Kategorie kopiert.", + "alert.copyPrivilegesFromGroup-warning": "Dadurch wird der %1-Satz dieser Gruppe aus der ausgewählten Kategorie kopiert." +} \ No newline at end of file diff --git a/public/language/de/admin/manage/registration.json b/public/language/de/admin/manage/registration.json new file mode 100644 index 0000000000..e922c5f91a --- /dev/null +++ b/public/language/de/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Warteschlange", + "description": "Es befinden sich keine Benutzer in der Registrierungswarteschlange.
Um diese Funktion zu aktivieren, gehe zu Einstellungen → Benutzer → Benutzerregistrierung und stelle den Registrierungstyp auf \"Admin-Genehmigung\" ein.", + + "list.name": "Name", + "list.email": "E-Mail", + "list.ip": "IP", + "list.time": "Zeit", + "list.username-spam": "Häufigkeit: %1 Erscheint: %2 Vertrauen: %3", + "list.email-spam": "Häufigkeit: %1 Erscheint: %2", + "list.ip-spam": "Häufigkeit: %1 Erscheint: %2", + + "invitations": "Einladungen", + "invitations.description": "Nachfolgend findest du eine vollständige Liste der gesendeten Einladungen. Verwende Strg-F, um die Liste nach E-Mail oder Benutzername zu durchsuchen. Der Benutzername wird rechts neben den E-Mails für Benutzer angezeigt, die ihre Einladungen eingelöst haben.", + "invitations.inviter-username": "Nutzername des Einladenden", + "invitations.invitee-email": "E-Mail des Eingeladenen", + "invitations.invitee-username": "Benutzername des Eingeladenen (falls registriert)", + + "invitations.confirm-delete": "Möchtest du diese Einladung wirklich löschen?" +} \ No newline at end of file diff --git a/public/language/de/admin/manage/tags.json b/public/language/de/admin/manage/tags.json new file mode 100644 index 0000000000..1dca4eb4ce --- /dev/null +++ b/public/language/de/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Das Forum hat bisher noch keine Themen mit Tags.", + "bg-color": "Hintergrundfarbe", + "text-color": "Textfarbe", + "description": "Wählen Sie Tags durch Klicken oder Ziehen aus, verwenden Sie STRG, um mehrere Tags auszuwählen.", + "create": "Tag erstellen", + "modify": "Tag bearbeiten", + "rename": "Tags umbenennen", + "delete": "Ausgewählte Tags entfernen", + "search": "Nach Tags suchen", + "settings": "Tag-Einstellungen", + "name": "Tagname", + + "alerts.editing": "Tag(s) bearbeiten", + "alerts.confirm-delete": "Wollen Sie die ausgewählten Tags löschen?", + "alerts.update-success": "Tag aktualisiert!", + "reset-colors": "Farben zurücksetzen" +} \ No newline at end of file diff --git a/public/language/de/admin/manage/uploads.json b/public/language/de/admin/manage/uploads.json new file mode 100644 index 0000000000..b4008ea089 --- /dev/null +++ b/public/language/de/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Datei hochladen", + "filename": "Dateiname", + "usage": "Beitragsnutzung", + "orphaned": "Verwaist", + "size/filecount": "Größe / Dateianzahl", + "confirm-delete": "Bist du sicher, dass du diese Datei löschen willst?", + "filecount": "%1 Dateien", + "new-folder": "Neuer Ordner", + "name-new-folder": "Gib den Namen für den neuen Ordner ein" +} \ No newline at end of file diff --git a/public/language/de/admin/manage/users.json b/public/language/de/admin/manage/users.json new file mode 100644 index 0000000000..d797f28a2d --- /dev/null +++ b/public/language/de/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Benutzer", + "edit": "Aktionen", + "make-admin": "Zum Administrator befördern", + "remove-admin": "Adminstatus entfernen", + "validate-email": "E-Mail bestätigen", + "send-validation-email": "Bestätigungs E-Mail senden", + "password-reset-email": "Passwort-Reset E-Mail senden", + "force-password-reset": "Zurücksetzen des Passworts erzwingen und Benutzer abmelden", + "ban": "Benutzer verbannen", + "temp-ban": "Benutzer temporär verbannen", + "unban": "Benutzer entbannen", + "reset-lockout": "Ausschließungen zurücksetzen", + "reset-flags": "Meldungen zurücksetzen", + "delete": "Benutzer löschen", + "delete-content": "Benutzer-Inhalte löschen", + "purge": "Benutzer und Benutzer-Inhalte löschen", + "download-csv": "CSV herunterladen", + "manage-groups": "Gruppen verwalten", + "add-group": "Gruppe hinzufügen", + "create": "Benutzer erstellen", + "invite": "Einladung per E-Mail", + "new": "Neuer Benutzer", + "filter-by": "Filtern nach", + "pills.unvalidated": "Nicht bestätigt", + "pills.validated": "Bestätigt", + "pills.banned": "Gebannt", + + "50-per-page": "50 pro Seite", + "100-per-page": "100 pro Seite", + "250-per-page": "250 pro Seite", + "500-per-page": "500 pro Seite", + + "search.uid": "Nach Benutzer-ID", + "search.uid-placeholder": "Gib eine Benutzer-ID ein um danach zu suchen", + "search.username": "Nach Nutzernamen", + "search.username-placeholder": "Einen Nutzernamen eingeben, um danach zu suchen", + "search.email": "Nach E-Mail", + "search.email-placeholder": "Eine E-Mail Adresse eingeben, um danach zu suchen", + "search.ip": "Nach IP-Adresse", + "search.ip-placeholder": "IP Adresse eingeben, um danach zu suchen", + "search.not-found": "Benutzer nicht gefunden!", + + "inactive.3-months": "3 Monate", + "inactive.6-months": "6 Monate", + "inactive.12-months": "12 Monate", + + "users.uid": "UID", + "users.username": "Nutzername", + "users.email": "E-Mail", + "users.no-email": "(keine Email)", + "users.ip": "IP", + "users.postcount": "Anzahl der Beiträge", + "users.reputation": "Ansehen", + "users.flags": "Meldungen", + "users.joined": "Beigetreten am", + "users.last-online": "Letztes mal online", + "users.banned": "Gebannt", + + "create.username": "Benutzername", + "create.email": "E-Mail", + "create.email-placeholder": "E-Mail dieses Benutzers", + "create.password": "Passwort", + "create.password-confirm": "Passwort bestätigen", + + "temp-ban.length": "Länge", + "temp-ban.reason": "Grund (optional)", + "temp-ban.hours": "Stunden", + "temp-ban.days": "Tage", + "temp-ban.explanation": "Geben die dauer des Bans an. Beachte, dass eine Zeit von 0 als permanent interpretiert wird.", + + "alerts.confirm-ban": "Möchtest Du diesen Nutzer wirklich permanent bannen?", + "alerts.confirm-ban-multi": "Möchtest Du diese Nutzer wirklich permanent bannen?", + "alerts.ban-success": "Benutzer gebannt!", + "alerts.button-ban-x": "%1 Nutzer bannen", + "alerts.unban-success": "Benutzer entbannt!", + "alerts.lockout-reset-success": "Ausschlüsse zurückgesetzt", + "alerts.flag-reset-success": "Meldung(en) zurückgesetzt!", + "alerts.no-remove-yourself-admin": "Du kannst dich nicht selbst als Administrator degradieren!", + "alerts.make-admin-success": "Der Benutzer ist nun ein Administrator", + "alerts.confirm-remove-admin": "Willst du wirklich diesen Administrator entfernen?", + "alerts.remove-admin-success": "Der Benutzer ist kein Administrator mehr", + "alerts.make-global-mod-success": "Der Benutzer ist nun ein globaler Moderator", + "alerts.confirm-remove-global-mod": "Willst du wirklich diesen globalen Moderator entfernen?", + "alerts.remove-global-mod-success": "Der Benutzer ist kein globaler Moderator mehr.", + "alerts.make-moderator-success": "Der Benutzer ist nun ein Moderator", + "alerts.confirm-remove-moderator": "Willst du wirklich diesen Moderator entfernen?", + "alerts.remove-moderator-success": "Der Benutzer ist kein Moderator mehr", + "alerts.confirm-validate-email": "Möchtest Du wirklich die E-Mails dieser Benutzer/dieses Benutzers bestätigen?", + "alerts.confirm-force-password-reset": "Bist du dir sicher, dass Du die Passwörter(das Passwort) zurücksetzen und die Benutzer(den Benutzer) abmelden willst?", + "alerts.validate-email-success": "E-Mails bestätigt", + "alerts.validate-force-password-reset-success": "Die Passwörter der Benutzer wurden zurückgesetzt und ihre bestehenden Sitzungen wurden widerrufen.", + "alerts.password-reset-confirm": "Möchtest Du wirklich (eine) Passwort-Reset-Email(s) an diese(n) Benutzer schicken?", + "alerts.password-reset-email-sent": "E-Mail zum Zurücksetzen des Passworts gesendet.", + "alerts.confirm-delete": "Warnung!

Möchtest Du wirklich Benutzer löschen?

Diese Aktion ist nicht umkehrbar! Lediglich das Nutzerkonto wird gelöscht, deren Beiträge und Themen bleiben bestehen

", + "alerts.delete-success": "Benutzer gelöscht!", + "alerts.confirm-delete-content": "Warnung!

Möchtest Du diese Benutzerinhalte wirklich löschen?

Diese Aktion kann nicht umgekehrt werden! Die Konten der Benutzer bleiben bestehen, aber ihre Beiträge und Themen werden gelöscht.

", + "alerts.delete-content-success": "Beiträge des/der Nutzer(s) gelöscht!", + "alerts.confirm-purge": "Warnung!

Bist du sicher, dass Du den/die Nutzer und deren Beiträge löschen möchtest?

Diese Aktion kann nicht rückgängig gemacht werden! Alle Nutzerdaten und Beiträge werden dabei gelöscht!

", + "alerts.create": "Benutzer erstellen", + "alerts.button-create": "Erstellen", + "alerts.button-cancel": "Abbrechen", + "alerts.error-passwords-different": "Die Passwörter müssen übereinstimmen", + "alerts.error-x": "Fehler

%1

", + "alerts.create-success": "Benutzer erstellt!", + + "alerts.prompt-email": "E-Mails:", + "alerts.email-sent-to": "Eine Einladungsemail wurde an %1 gesendet", + "alerts.x-users-found": "%1 Benutzer gefunden, (%2 Sekunden)", + "export-users-started": "Der Export von Benutzern als CSV kann eine Weile dauern. Sie erhalten eine Benachrichtigung, wenn es abgeschlossen ist.", + "export-users-completed": "Benutzer wurden als CSV exportiert, klicke hier, um sie herunterzuladen." +} \ No newline at end of file diff --git a/public/language/de/admin/menu.json b/public/language/de/admin/menu.json new file mode 100644 index 0000000000..be0255aeeb --- /dev/null +++ b/public/language/de/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Übersicht", + "dashboard/logins": "Anmeldungen", + "dashboard/users": "Benutzer", + "dashboard/topics": "Themen", + "dashboard/searches": "Suchen", + "section-general": "Allgemein", + + "section-manage": "Verwalten", + "manage/categories": "Kategorien", + "manage/privileges": "Privilegien", + "manage/tags": "Tags", + "manage/users": "Benutzer", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Warteliste", + "manage/post-queue": "Beitragswarteschlange", + "manage/groups": "Gruppen", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Zusammenfassungen", + + "section-settings": "Einstellungen", + "settings/general": "Allgemein", + "settings/homepage": "Home-Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "E-Mail", + "settings/user": "Benutzer", + "settings/group": "Gruppen", + "settings/guest": "Gäste", + "settings/uploads": "Uploads", + "settings/languages": "Sprachen", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Seitennummerierung", + "settings/tags": "Tags", + "settings/notifications": "Benachrichtigungen", + "settings/api": "API-Zugriff", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Erweitert", + + "settings.page-title": "%1 Einstellungen", + + "section-appearance": "Aussehen", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Benutzerdefinierter Inhalt (HTML/JS/CSS)", + + "section-extend": "Erweitert", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Belohnungen", + + "section-social-auth": "Soziale Authentifizierung", + + "section-plugins": "Plugins", + "extend/plugins.install": "Plugins installieren", + + "section-advanced": "System", + "advanced/database": "Datenbank", + "advanced/events": "Ereignisse", + "advanced/hooks": "WebHooks", + "advanced/logs": "Protokoll", + "advanced/errors": "Fehler", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Forum regenerieren & neustarten", + "restart-forum": "Forum neu starten", + "logout": "Abmelden", + "view-forum": "Forum anzeigen", + + "search.placeholder": "Nach Einstellungen suchen", + "search.no-results": "Keine Ergebnisse...", + "search.search-forum": "Suche im Forum nach ", + "search.keep-typing": "Gib mehr ein, um die Ergebnisse zu sehen...", + "search.start-typing": "Starte die Eingabe, um die Ergebnisse zu sehen...", + + "connection-lost": "Verbindung zu %1 verloren, wird wieder hergestellt...", + + "alerts.version": "Es läuft NodeBB v%1", + "alerts.upgrade": "Upgrade auf v%1" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/advanced.json b/public/language/de/admin/settings/advanced.json new file mode 100644 index 0000000000..1f75026bc5 --- /dev/null +++ b/public/language/de/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Wartungsmodus", + "maintenance-mode.help": "Wenn sich das Forum im Wartungsmodus befindet, werden alle Anfragen auf eine statische Warteseite umgeleitet. Administratoren sind von dieser Umleitung ausgenommen und können normal auf die Site zugreifen.", + "maintenance-mode.status": "Statuscode für Wartungsmodus", + "maintenance-mode.message": "Wartungsnachricht", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "ALLOW-FROM setzen um NodeBB in einem iFrame zu platzieren", + "headers.csp-frame-ancestors": "Content-Security-Policy frame-ancestors header setzen, um NodeBB in einem iFrame zu platzieren", + "headers.csp-frame-ancestors-help": "'none', 'self' (Standard) oder Liste der zuzulassenden URIs.", + "headers.powered-by": "Anpassen des \"Powered By\" Headers von NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regulärer Ausdruck", + "headers.acao-help": "Um den Zugriff zu allen Seiten zu verbieten, leer lassen.", + "headers.acao-regex-help": "Geben Sie einen regulären Ausdruck ein, um Seiten dynamisch zu erlauben. Um den Zugriff zu allen Seiten zu verbieten, leer lassen.", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Richtlinie", + "headers.coep-help": "Wenn aktiviert (Standard), wird der Header auf require-corp gesetzt", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "HSTS Aktivieren (empfohlen)", + "hsts.maxAge": "HSTS Maximales Alter", + "hsts.subdomains": "Subdomains in HSTS Header einbinden", + "hsts.preload": "Vorabladen von HSTS Header erlauben", + "hsts.help": "Wenn aktiviert, wird ein HSTS-Header für diese Seite gesetzt. Du kannst wählen, ob du Subdomains und Preloading-Flags in deinen Header aufnehmen möchtest. Im Zweifelsfall kannst du diese unmarkiert lassen.", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB verwendet ein Modul, das in Situationen mit hohem Datenverkehr automatisch Anfragen ablehnt. Sie können diese Einstellungen hier anpassen, obwohl die Standardeinstellungen ein guter Ausgangspunkt ist.", + "traffic.enable": "Traffic Management aktivieren", + "traffic.event-lag": "Eventschleifenverzögerungsschwelle (in Millisekunden)", + "traffic.event-lag-help": "Das Heruntersetzen dieses Werts reduziert die Ladezeiten, aber wird auch dafür sorgen, dass die \"Übermäßige Belastung\" nachricht öfter angezeigt wird. (Neustart erforderlich)", + "traffic.lag-check-interval": "Prüfungsintervall (in Millisekunden)", + "traffic.lag-check-interval-help": "Das Heruntersetzen dieses Werts sorgt dafür, dass NodeBB empfindlicher auf Auslastungs-Spikes reagiert, aber könnte auch dafür sorgen, dass der Test zu empfindlich werden könnte. (Neustart erforderlich)", + + "sockets.settings": "WebSocket-Einstellungen", + "sockets.max-attempts": "Maximale Anzahl von Reconnection-Versuchen", + "sockets.default-placeholder": "Standard: %1", + "sockets.delay": "Wiederverbindungsverzögerung", + + "analytics.settings": "Analytik-Einstellungen", + "analytics.max-cache": "Analytik-Cache Max-Wert", + "analytics.max-cache-help": "Bei Installationen mit hohem Datenverkehr kann der Cache kontinuierlich erschöpft werden, wenn die Anzahl der gleichzeitig aktiven Benutzer den Wert für \"Max Cache\" überschreitet. (Neustart erforderlich)", + "compression.settings": "Komprimierungseinstellungen", + "compression.enable": "Komprimierung einschalten", + "compression.help": "Diese Einstellung aktiviert die gzip-Komprimierung. Für eine produktive Website mit hohem Datenverkehr ist es am besten, die Komprimierung auf der Ebene des Reverse-Proxys zu implementieren. Sie können sie hier zu Testzwecken aktivieren." +} \ No newline at end of file diff --git a/public/language/de/admin/settings/api.json b/public/language/de/admin/settings/api.json new file mode 100644 index 0000000000..d718623639 --- /dev/null +++ b/public/language/de/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Einstellungen", + "lead-text": "Auf dieser Seite kanst Du den Zugriff auf die Write-API in NodeBB konfigurieren.", + "intro": "Standardmäßig authentifiziert die Write-API Benutzer basierend auf ihrem Sitzungscookie, aber NodeBB unterstützt auch die Bearer-Authentifizierung über Token, die über diese Seite generiert werden.", + "docs": "Klicke hier, um auf die vollständige API-Spezifikation zuzugreifen", + + "require-https": "API-Nutzung nur über HTTPS möglich", + "require-https-caveat": "Hinweis: Einige Installationen mit Load Balancern können ihre Anfragen über HTTP an NodeBB weiterleiten, in diesem Fall sollte diese Option deaktiviert bleiben.", + + "uid": "Nutzer–ID", + "uid-help-text": "Gebe eine Benutzer-ID an, die diesem Token zugeordnet werden soll. Wenn die Benutzer-ID 0 ist, wird sie als Master-Token betrachtet, das basierend auf dem _uid-Parameter die Identität anderer Benutzer annehmen kann", + "description": "Beschreibung", + "no-description": "Keine Beschreibung angegeben.", + "token-on-save": "Token wird generiert, sobald das Formular gespeichert wird" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/chat.json b/public/language/de/admin/settings/chat.json new file mode 100644 index 0000000000..d97665c221 --- /dev/null +++ b/public/language/de/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chateinstellungen", + "disable": "Chat deaktivieren", + "disable-editing": "Chatnachrichtenbearbeitung/löschung deaktivieren", + "disable-editing-help": "Administratoren und globale Moderatoren sind von dieser Einschränkung ausgenommen", + "max-length": "Maximale Länge von Chatnachrichten", + "max-room-size": "Maximale Anzahl von Benutzern in Chatrooms", + "delay": "Zeit zwischen Chatnachrichten in Millisekunden", + "notification-delay": "Benachrichtigungsverzögerung für Chatnachrichten. (0 für keine Verzögerung)", + "restrictions.seconds-edit-after": "Anzahl der Sekunden, die eine Chat-Nachricht bearbeitet werden kann. (0 deaktiviert)", + "restrictions.seconds-delete-after": "Anzahl der Sekunden, die eine Chat-Nachricht löschbar bleibt. (0 deaktiviert)" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/cookies.json b/public/language/de/admin/settings/cookies.json new file mode 100644 index 0000000000..cf7bdde842 --- /dev/null +++ b/public/language/de/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Cookie Zustimmung", + "consent.enabled": "Aktiviert", + "consent.message": "Benachrichtigungsnachricht", + "consent.acceptance": "Annahmenachricht", + "consent.link-text": "Richtlinien-Link-Text", + "consent.link-url": "Richtlinienlink-URL", + "consent.blank-localised-default": "Feld leerlassen, um die lokalisierten NodeBB-Standardeinstellungen zu verwenden", + "settings": "Einstellungen", + "cookie-domain": "Session-Cookie-Domain", + "max-user-sessions": "Maximale aktive Sitzungen pro Benutzer", + "blank-default": "Leer lassen für Standardwert" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/email.json b/public/language/de/admin/settings/email.json new file mode 100644 index 0000000000..db1e0d1de4 --- /dev/null +++ b/public/language/de/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "E-Mail Einstellungen", + "address": "E-Mail Adresse", + "address-help": "Die folgende E-Mail Adresse ist die E-Mail Adresse, welche dem Empfänger im \"Von\" und \"Antworten\" Bereich sehen wird.", + "from": "Name des Absenders", + "from-help": "Der Name des Absenders, welcher in der E-Mail angezeigt werden soll.", + + "confirmation-settings": "Konfirmation", + "confirmation.expiry": "Stunden, um den E-Mail-Bestätigungslink gültig zu halten", + + "smtp-transport": "SMTP Konfiguration", + "smtp-transport.enabled": "SMTP-Transport aktivieren", + "smtp-transport-help": "Du kannst aus einer Liste bekannter Email-Provider auswählen, oder einen benutzerdefinierten eingeben.", + "smtp-transport.service": "Wähle einen Provider", + "smtp-transport.service-custom": "Benutzerdefiniert...", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Verbindungssicherheit", + "smtp-transport.security-encrypted": "Verschlüsselt", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Nichts", + "smtp-transport.username": "Benutzername", + "smtp-transport.username-help": "Bitte füge die komplette Email-Adresse für Gmail hier ein, insbesondere wenn du eine von Google Apps gemanagete domain verwendest.", + "smtp-transport.password": "Passwort", + "smtp-transport.pool": "Gepoolte Verbindungen erlauben", + "smtp-transport.pool-help": "Das Poolen von Verbindungen hindert NodeBB daran für jede neu erstellte E-Mail eine eigene Verbindung aufzubauen. Diese Option ist nur zutreffend, wenn SMTP aktiviert ist.", + + "template": "E-Mail Vorlage bearbeiten", + "template.select": "E-Mail Vorlage auswählen", + "template.revert": "Original wiederherstellen", + "testing": "E-Mail Test", + "testing.select": "E-Mail-Vorlage auswählen", + "testing.send": "Test-E-Mail versenden", + "testing.send-help": "Die Test-E-Mail wird an die E-Mail Adresse des momentan eingeloggten Nutzers geschickt.", + "subscriptions": "Email Zusammenfassungen", + "subscriptions.disable": "Deaktivierung der Email Zusammenfassungen", + "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. 0 für Mitternacht, 17 für 5 Uhr Nachmittags). Beachten Sie, dass die Zeit auf der Serverzeit basiert und daher nicht umbedingt mit ihrer Systemzeit übereinstimmen muss.
Die ungefähre Serverzeit ist:
Die nächste tägliche Sendung ist um geplant", + "notifications.remove-images": "Bilder aus E-Mail-Benachrichtigungen entfernen", + "require-email-address": "Neue Benutzer auffordern, eine E-Mail-Adresse anzugeben", + "require-email-address-warning": "Standardmäßig können Benutzer die Eingabe einer E-Mail-Adresse ablehnen, indem sie das Feld leer lassen. Wenn Du diese Option aktivierst, musst Du eine E-Mail-Adresse eingeben, um mit der Registrierung fortzufahren.Es stellt nicht sicher, dass der Benutzer eine echte E-Mail-Adresse eingibt, noch nicht einmal eine Adresse, die ihm gehört.", + "send-validation-email": "Validierungs-E-Mails senden, wenn eine E-Mail hinzugefügt oder geändert wird", + "include-unverified-emails": "E-Mails an Empfänger senden, die ihre E-Mails nicht explizit bestätigt haben", + "include-unverified-warning": "Standardmäßig wurden Benutzer mit E-Mail-Adressen, die mit ihrem Konto verknüpft sind, bereits verifiziert, aber es existieren Situationen, in denen dies nicht der Fall ist (z. B. SSO-Anmeldungen, Großvater-Benutzer usw.). Aktiviere diese Einstellung auf eigenes Risiko – Das Senden von E-Mails an nicht verifizierte Adressen kann einen Verstoß gegen regionale Anti-Spam-Gesetze darstellen.", + "prompt": "Benutzer auffordern, ihre E-Mail-Adressen einzugeben oder zu bestätigen", + "prompt-help": "Wenn ein Benutzer keine E-Mail-Adresse hat oder seine E-Mail-Adresse nicht bestätigt ist, wird eine Warnung auf dem Bildschirm angezeigt.", + "sendEmailToBanned": "E-Mails an Benutzer senden, selbst wenn sie gesperrt wurden" +} diff --git a/public/language/de/admin/settings/general.json b/public/language/de/admin/settings/general.json new file mode 100644 index 0000000000..2a4a76533d --- /dev/null +++ b/public/language/de/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Forum Einstellungen", + "title": "Forum Titel", + "title.short": "Kurzbezeichnung", + "title.short-placeholder": "Wenn kein Kurztitel angegeben ist, wird der Forum-Titel verwendet.", + "title.url": "Titel Link-URL", + "title.url-placeholder": "Die URL des Seitentitels", + "title.url-help": "Wenn der Titel angeklickt wird, werden die Benutzer an diese Adresse weitergeleitet. Wenn Sie nichts angeben, wird der Benutzer zum Forum-Index weitergeleitet.
Hinweis: Dies ist nicht die externe URL, die in E-Mails etc. verwendet wird. Diese wird über die Eigenschaft url in config.json festgelegt.", + "title.name": "Name Deiner Community", + "title.show-in-header": "Titel im Header anzeigen", + "browser-title": "Browser Titel", + "browser-title-help": "Wenn kein Browser Titel spezifiziert wurde, wird der Forum Titel verwendet", + "title-layout": "Titel Layout", + "title-layout-help": "Definiert wie der Browser Titel gebildet wird, z.B. {pageTitle} | {browserTitle}", + "description.placeholder": "Eine kurze Beschreibung der Community", + "description": "Forum Beschreibung", + "keywords": "Forum Schlüsselworte", + "keywords-placeholder": "Schlüsselworte, die ihre Community beschreiben, mit Komma getrennt", + "logo": "Forum Logo", + "logo.image": "Bild", + "logo.image-placeholder": "Pfad zu einem Logo, welches im Header des Forums angezeigt werden soll", + "logo.upload": "Hochladen", + "logo.url": "Logo Link-URL", + "logo.url-placeholder": "Die URL des Logos", + "logo.url-help": "Wenn das Logo angeklickt wird, werden die Benutzer an diese Adresse weitergeleitet. Wenn Sie nichts angeben, wird der Benutzer zum Forum-Index weitergeleitet.
Hinweis: Dies ist nicht die externe URL, die in E-Mails etc. verwendet wird. Diese wird über die Eigenschaft url in config.json festgelegt.", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternativer Text, falls das Bild nicht angezeigt werden kann", + "favicon": "Favicon", + "favicon.upload": "Hochladen", + "pwa": "Progressive Web-App", + "touch-icon": "Touch-Symbol", + "touch-icon.upload": "Hochladen", + "touch-icon.help": "Empfohlene Größe und Format: 512x512, nur PNG-Format. Wenn kein Touch-Symbol angegeben wird, verwendet NodeBB wieder das Favicon.", + "maskable-icon": "Maskierbares (Start-Bildschirm) Symbol", + "maskable-icon.help": "Empfohlene Größe und Format: 512x512, nur PNG-Format. Wenn kein maskierbares Icon angegeben wird, greift NodeBB auf das Touch-Symbol zurück.", + "outgoing-links": "Ausgehende Links", + "outgoing-links.warning-page": "Warnseite für ausgehende links verwenden", + "search": "Suche", + "search-default-in": "Suchen in", + "search-default-in-quick": "Schnellsuchen in", + "search-default-sort-by": "Sortieren nach", + "outgoing-links.whitelist": "Domains, für die keine Warnseite angezeigt werden soll", + "site-colors": "Website Farben-Metadaten", + "theme-color": "Theme-Farbe", + "background-color": "Hintergrundfarbe", + "background-color-help": "Farbe, die für den Hintergrund des Startbildschirms verwendet wird, wenn die Website als PWA installiert ist", + "undo-timeout": "Zeitüberschreitung rückgängig machen", + "undo-timeout-help": "Bei einigen Vorgängen, wie z. B. dem Verschieben eines Themes, kann der Moderator seine Aktion innerhalb eines bestimmten Zeitrahmens rückgängig machen. Setzen Sie den Wert auf 0, um die Rückgängigmachung vollständig zu deaktivieren.", + "topic-tools": "Themen-Tools" +} diff --git a/public/language/de/admin/settings/group.json b/public/language/de/admin/settings/group.json new file mode 100644 index 0000000000..df50632f6b --- /dev/null +++ b/public/language/de/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Allgemein", + "private-groups": "Private Gruppen", + "private-groups.help": "Wenn aktiviert, erfordert das Beitreten einer Gruppe die Bestätigung des jeweiligen Besitzers(Standard: aktiviert)", + "private-groups.warning": "Vorsicht! Wenn diese Option deaktiviert ist, und es private Gruppen gibt, werden diese automatisch öffentlich.", + "allow-multiple-badges": "Mehrere Abzeichen erlauben", + "allow-multiple-badges-help": "Diese Eintellung kann verwendet werden um Benutzern zu erlauben mehrere Gruppen abzeichen auszuwählen, benötigt Theme unterstützung.", + "max-name-length": "Maximale Länge von Gruppennamen", + "max-title-length": "Maximale Gruppentitellänge", + "cover-image": "Titelbild der Gruppe", + "default-cover": "Standard-Titelbilder", + "default-cover-help": "Füge urch Kommas getrennte Standard-Titelbilder für Gruppen hinzu, die kein hochgeladenes Titelbild haben" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/guest.json b/public/language/de/admin/settings/guest.json new file mode 100644 index 0000000000..2d5ee7a23e --- /dev/null +++ b/public/language/de/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Einstellungen", + "handles.enabled": "Gastzugänge erlauben", + "handles.enabled-help": "Diese Option zeigt ein neues Feld an, in dem Gäste einen Namen auswählen können, der jedem von ihnen erstellten Beitrag zugeordnet werden soll. Wenn sie deaktiviert sind, werden sie einfach „Gast“ genannt.", + "topic-views.enabled": "Gästen erlauben, die gezählte Anzahl der Themenaufrufe zu erhöhen", + "reply-notifications.enabled": "Erlauben Sie Gästen, Antwortbenachrichtigungen zu erstellen" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/homepage.json b/public/language/de/admin/settings/homepage.json new file mode 100644 index 0000000000..243f51bd45 --- /dev/null +++ b/public/language/de/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Startseite", + "description": "Wähle, welche Seite angezeigt wird, wenn Benutzer zur Stamm-URL deines Forums navigieren.", + "home-page-route": "Startseiten Route", + "custom-route": "Benutzerdefinierte Route", + "allow-user-home-pages": "Benutzer-Startseiten zulassen", + "home-page-title": "Titel der Startseite (Standardmäßig \"Home\")" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/languages.json b/public/language/de/admin/settings/languages.json new file mode 100644 index 0000000000..8aa3dd6590 --- /dev/null +++ b/public/language/de/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Spracheinstellungen", + "description": "Die Standardsprache bestimmt die Spracheinstellungen für alle Benutzer, die dein Forum besuchen.
Einzelne Benutzer können die Standardsprache auf ihrer Kontoeinstellungsseite überschreiben.", + "default-language": "Standardsprache", + "auto-detect": "Sprach-Einstellung bei Gästen automatisch ermitteln" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/navigation.json b/public/language/de/admin/settings/navigation.json new file mode 100644 index 0000000000..fb54380fd3 --- /dev/null +++ b/public/language/de/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "ändern", + "route": "Pfad:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Klasse: optional", + "class": "Klasse optional", + "id": "ID: optional", + + "properties": "Eigenschaften:", + "groups": "Gruppen:", + "open-new-window": "In neuem Fenster öffnen", + "dropdown": "Dropdown", + "dropdown-placeholder": "Platziere deine Dropdown-Menüpunkte unten, dh:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Löschen", + "btn.disable": "Deaktivieren", + "btn.enable": "Aktivieren", + + "available-menu-items": "Verfügbare Menüpunkte", + "custom-route": "Benutzerdefinierter Pfad", + "core": "Kern", + "plugin": "Plugin" +} diff --git a/public/language/de/admin/settings/notifications.json b/public/language/de/admin/settings/notifications.json new file mode 100644 index 0000000000..089bd86254 --- /dev/null +++ b/public/language/de/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Benachrichtigungen", + "welcome-notification": "Willkommensbenachrichtigung", + "welcome-notification-link": "Willkommens-Benachrichtigungslink", + "welcome-notification-uid": "Begrüßungsbenachrichtigungsbenutzer (UID)", + "post-queue-notification-uid": "Post-Queue-Benutzer (UID)" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/pagination.json b/public/language/de/admin/settings/pagination.json new file mode 100644 index 0000000000..3acac1a13b --- /dev/null +++ b/public/language/de/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Seitennummerierungs Einstellungen", + "enable": "Themen und Beiträge paginieren, anstatt unendlich zu scrollen.", + "posts": "Beitragsseitennummerierung", + "topics": "Themen Seitennummerierung", + "posts-per-page": "Beiträge pro Seite", + "max-posts-per-page": "Maximale Anzahl von Beiträgen pro Seite", + "categories": "Kategorie Seitennummerierung", + "topics-per-page": "Themen pro Seite", + "max-topics-per-page": "Maximale Themen pro Seite", + "categories-per-page": "Kategorien pro Seite" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/post.json b/public/language/de/admin/settings/post.json new file mode 100644 index 0000000000..74e262d7ec --- /dev/null +++ b/public/language/de/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Beitragssortierung", + "sorting.post-default": "Standardmäßige sortierung von Beiträgen", + "sorting.oldest-to-newest": "Von Alt bis Neu", + "sorting.newest-to-oldest": "Von Neu zu Alt", + "sorting.most-votes": "Meiste Bewertungen", + "sorting.most-posts": "Meiste Beiträge", + "sorting.topic-default": "Standardmäßige Themensortierung", + "length": "Beitragslänge", + "post-queue": "Beitragswarteschlange", + "restrictions": "Posting beschränkungen", + "restrictions-new": "Beschränkungen für neue Benutzer", + "restrictions.post-queue": "Beitragswarteschlange verwenden", + "restrictions.post-queue-rep-threshold": "Benötigte Reputation um Beiträge ohne Warteschlange zu verfassen", + "restrictions.groups-exempt-from-post-queue": "Gruppen auswählen, die von der Beitragswarteschlange ausgenommen sind", + "restrictions-new.post-queue": "Aktiviere Beschränkungen für neue Benutzer", + "restrictions.post-queue-help": "Aktivierte Beitragswarteschlange sorgt dafür, dass Posts von neuen Benutzern vor dem Veröffentlichen genehmigt werden müssen.", + "restrictions-new.post-queue-help": "Durch das Aktivieren von Einschränkungen für neue Benutzer werden Einschränkungen für Beiträge festgelegt, die von neuen Benutzern erstellt wurden", + "restrictions.seconds-between": "Anzahl der Sekunden zwischen Posts", + "restrictions.seconds-between-new": "Sekunden zwischen Beiträgen für neue Benutzer", + "restrictions.rep-threshold": "Mindesreputation bevor die Beschränkungen aufgehoben werden", + "restrictions.seconds-before-new": "Sekunden, bevor ein neuer Benutzer seinen ersten Beitrag schreiben kann", + "restrictions.seconds-edit-after": "Anzahl der Sekunden, die ein Beitrag bearbeitet werden kann (zum Deaktivieren auf 0 setzen)", + "restrictions.seconds-delete-after": "Anzahl der Sekunden, die ein Beitrag löschbar bleibt (zum Deaktivieren auf 0 setzen)", + "restrictions.replies-no-delete": "Anzahl der Antworten, nachdem Benutzern das Löschen ihrer eigenen Themen verweigert wurde (zum Deaktivieren auf 0 setzen)", + "restrictions.min-title-length": "Minimale Titellänge", + "restrictions.max-title-length": "Maximale Titellänge", + "restrictions.min-post-length": "Minimale Beitragslänge", + "restrictions.max-post-length": "Maximale Beitragslänge", + "restrictions.days-until-stale": "Tage bis ein Thema als alt angesehen wird", + "restrictions.stale-help": "Wenn ein Thema als \"veraltet\" angesehen wird, wird Nutzern die versuchen diesem Thema zu antworten eine Warnung gezeigt", + "timestamp": "Zeitstempel", + "timestamp.cut-off": "Tageslimit für Relative Zeitangaben (in Tagen)", + "timestamp.cut-off-help": "Tage & Zeiten werden relativ angezeigt (z.B. \"vor 3 Stunden\" / \"vor 5 Tagen\"), und in viele Sprachen übersetzt. Nach einem bestimmten Zeitpunkt, kann dieses Text durch das übersetzte Datum selbst ersetzt werden (z.B. 5 Nov 2016 15:30).
(Standard: 30, oder ein Monat). Auf 0 setzen um immer Daten anzuzeigen, leer lassung um immer relative Zeiten anzuzeigen.", + "timestamp.necro-threshold": "Nekroschwelle (in Tagen)", + "timestamp.necro-threshold-help": "Zwischen Posts wird eine Nachricht angezeigt, wenn die Zeit zwischen ihnen länger als die Necro-Schwelle ist. (Standart: 7 oder eine Woche) Zum Deaktivieren auf 0 setzen.", + "timestamp.topic-views-interval": "Intervall für Themenaufrufe erhöhen (in Minuten)", + "timestamp.topic-views-interval-help": "Themenansichten werden nur einmal alle X Minuten erhöht, wie durch diese Einstellung definiert.", + "teaser": "Teaser-Beitrag", + "teaser.last-post": "Letzter - Den neuesten Beitrag anzeigen, den originalen Beitrag innbegriffen, wenn es keine Antworten gibt", + "teaser.last-reply": "Letzter - Den neuesten Beitrag oder einen \"Keine Antworten\" Platzhalter, wenn es keine Antworten gibt anzeigen", + "teaser.first": "Erster", + "showPostPreviewsOnHover": "Eine Vorschau der Beiträge zeigen, wenn Du mit der Maus darüber fährst", + "unread": "Ungelesen-Einstellungen", + "unread.cutoff": "Ungelesen-Limit (in Tagen)", + "unread.min-track-last": "Minimale Anzahl an Beiträgen pro Thema bevor die letzte Sichtung mitgeschrieben wird", + "recent": "Kürzlich verwendete Einstellungen", + "recent.max-topics": "Maximale Themen auf /recent", + "recent.categoryFilter.disable": "Filtern von Themen in ignorierten Kategorien auf der /recent Seite deaktivieren", + "signature": "Signatureinstellungen", + "signature.disable": "Signaturen deaktivieren", + "signature.no-links": "Links in signaturen deaktivieren", + "signature.no-images": "Bilder in Signaturen deaktivieren", + "signature.hide-duplicates": "Doppelte Signaturen in Themen ausblenden", + "signature.max-length": "Maximale Signaturlänge", + "composer": "Editor Einstellungen", + "composer-help": "Die folgenden Einstellungen bestimmen die funktionalität und/oder das Aussehen des Beitragseditors, der Nutzern angezeigt wird, webb sie neue Themen erstellen, oder bereits existierenden Antworten.", + "composer.show-help": "\"Hilfe\"-Tab anzeigen", + "composer.enable-plugin-help": "Plugins erlauben Inhalte dem \"Help\"-Tab hinzuzufügen", + "composer.custom-help": "Benutzerdefinierter Hilfe-Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Themen-Backlinks aktivieren", + "backlinks.help": "Wenn ein Beitrag auf ein anderes Thema verweist, wird zu diesem Zeitpunkt ein Link zurück zu dem Beitrag in das referenzierte Thema eingefügt.", + "ip-tracking": "IP-Verfolgung", + "ip-tracking.each-post": "IP-Adresse für jeden Beitrag speichern", + "enable-post-history": "Aktiviere Beitrags-Änderungsgeschichte" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/reputation.json b/public/language/de/admin/settings/reputation.json new file mode 100644 index 0000000000..ffb9f2effc --- /dev/null +++ b/public/language/de/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Ansehenseinstellungen", + "disable": "Ansehenssystem deaktivieren", + "disable-down-voting": "Negative Bewertungen deaktivieren", + "votes-are-public": "Alle Bewertungen sind öffentlich", + "thresholds": "Aktivitätsschwelle", + "min-rep-upvote": "Mindestreputation, um Beiträge hochzustimmen", + "upvotes-per-day": "Upvotes pro Tag (für unbegrenzte Upvotes auf 0 setzen)", + "upvotes-per-user-per-day": "Upvotes pro Benutzer und Tag (für unbegrenzte Upvotes auf 0 gesetzt)", + "min-rep-downvote": "Minimales Ansehen um Beiträge negativ zu bewerten", + "downvotes-per-day": "Runtervoten pro Tag (auf 0 gesetzt für unbegrenzte Runtervotes)", + "downvotes-per-user-per-day": "Runtervoten pro Benutzer pro Tag (auf 0 gesetzt für unbegrenzte Runtervotes)", + "min-rep-chat": "Mindestreputation zum Senden von Chatnachrichten", + "min-rep-flag": "Minimales Ansehen und Beiträge zu melden", + "min-rep-website": "Erforderliche Reputation um eine \"Website\" zum Benutzerprofil hinzuzufügen", + "min-rep-aboutme": "Erforderliche Reputation um eine \"Über mich\"-Sektion zum Benutzerprofil hinzuzufügen", + "min-rep-signature": "Erforderliche Reputation um eine \"Signatur\" zum Benutzerprofil hinzuzufügen", + "min-rep-profile-picture": "Minimale Reputation um ein Profilbild hinzuzufügen", + "min-rep-cover-picture": "Minimale Reputation um ein Deckbild hinzuzufügen", + + "flags": "Merkmal-Einstellung", + "flags.limit-per-target": "Maximale Häufigkeit, mit der etwas markiert werden kann", + "flags.limit-per-target-placeholder": "Standardwert: 0", + "flags.limit-per-target-help": "Wenn ein Beitrag oder ein Benutzer mehrfach markiert wird, wird jede zusätzliche Markierung als "Nachricht" betrachtet und zur ursprünglichen Markierung hinzugezählt. Setzen Sie diese Option auf eine andere Zahl als Null, um die Anzahl der Nachricht, die ein Artikel erhalten kann, zu begrenzen.", + "flags.auto-flag-on-downvote-threshold": "Anzahl der Downvotes für Posts mit automatischer Markierung (zum Deaktivieren auf 0 setzen, Standard: 0)", + "flags.auto-resolve-on-ban": "Automatisches Beenden aller Tickets eines Benutzers, wenn dieser gesperrt wird", + "flags.action-on-resolve": "Führe Folgendes aus, wenn eine Flagge aufgelöst wird", + "flags.action-on-reject": "Gehe folgendermaßen vor, wenn eine Flagge abgelehnt wird", + "flags.action.nothing": "Nichts tun", + "flags.action.rescind": "Aufhebung der Benachrichtigung an Moderatoren/Administratoren" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/social.json b/public/language/de/admin/settings/social.json new file mode 100644 index 0000000000..081de0cb88 --- /dev/null +++ b/public/language/de/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Beitragsfreigabe", + "info-plugins-additional": "Plugins können zusätzliche Netzwerke zum Teilen von Beiträgen hinzufügen.", + "save-success": "Post-Sharing-Netzwerke erfolgreich gespeichert!" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/sockets.json b/public/language/de/admin/settings/sockets.json new file mode 100644 index 0000000000..61c2bb4fbe --- /dev/null +++ b/public/language/de/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Wiederverbindungseinstellungen", + "max-attempts": "Max. Wiederverbindungsversuche", + "default-placeholder": "Standard: %1", + "delay": "Wiederverbindungsverzögerung" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/sounds.json b/public/language/de/admin/settings/sounds.json new file mode 100644 index 0000000000..0d39be1669 --- /dev/null +++ b/public/language/de/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Benachrichtigungen", + "chat-messages": "Chat-Nachrichten", + "play-sound": "Abspielen", + "incoming-message": "Eingehende Nachricht", + "outgoing-message": "Ausgehende Nachricht", + "upload-new-sound": "Neuen Ton hochladen", + "saved": "Einstellungen gespeichert!" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/tags.json b/public/language/de/admin/settings/tags.json new file mode 100644 index 0000000000..766b30e402 --- /dev/null +++ b/public/language/de/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag-Einstellungen", + "link-to-manage": "Tags managen", + "system-tags": "System-Tags", + "system-tags-help": "Nur berechtige Benutzer können diese Tags verwenden.", + "min-per-topic": "Minimale Tags pro Thema", + "max-per-topic": "Maximale Tags pro Thema", + "min-length": "Minimale Tag Länge", + "max-length": "Maximale Tag Länge", + "related-topics": "Zusammenhängende Themen", + "max-related-topics": "Maximale Anzahl an Zusammenhängenden Themen die angezeigt werden sollen (Wenn vom Design unterstützt)" +} \ No newline at end of file diff --git a/public/language/de/admin/settings/uploads.json b/public/language/de/admin/settings/uploads.json new file mode 100644 index 0000000000..85049ff8e1 --- /dev/null +++ b/public/language/de/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Beiträge", + "orphans": "Verwaiste Dateien", + "private": "Hochgeladene Dateien privatisieren", + "strip-exif-data": "EXIF-Daten entfernen", + "preserve-orphaned-uploads": "Hochgeladene Dateien auf der Festplatte behalten, nachdem ein Beitrag gelöscht wurde", + "orphanExpiryDays": "Tage, um verwaiste Dateien aufzubewahren", + "orphanExpiryDays-help": "Nach dieser Anzahl von Tagen werden verwaiste Uploads aus dem Dateisystem gelöscht.
Auf 0 setzen oder das Feld leer lassen, um es zu deaktivieren.", + "private-extensions": "Private Dateiendungen", + "private-uploads-extensions-help": "Gib eine Komma-Separierte Liste mit Dateiendungen an, die privatisiert werden sollen (z.B. pdf,xls,doc). Eine leere Liste bedeutet, dass alle Dateien privat sind.", + "resize-image-width-threshold": "Bilder zu einer bestimmten Breite runterskalieren wenn sie breiter sind als die angegebene Breite.", + "resize-image-width-threshold-help": "(in Pixeln, standardmäßig 1520 Pixel, auf 0 setzen um zu deaktivieren)", + "resize-image-width": "Bilder zu einer bestimmten Breite runterskalieren", + "resize-image-width-help": "(in Pixeln, standardmäßig 760 Pixel, auf 0 setzen um zu deaktivieren)", + "resize-image-quality": "Zu benutzende Qualität beim verändern von Bildauflösungen", + "resize-image-quality-help": "Benutze eine niedrigere Qualitätseinstellung um die Dateigröße der gespeicherten Bilder zu minimieren.", + "max-file-size": "Maximale Dateigröße (in KiB)", + "max-file-size-help": "(In Kibibytes, Standardmäßig 2048 KiB)", + "reject-image-width": "Maximale Bildbreite (in Pixeln)", + "reject-image-width-help": "Breitere Bilder werden abgelehnt.", + "reject-image-height": "Maximale Bildhöhe (in Pixeln)", + "reject-image-height-help": "Höhere Bilder werden abgelehnt.", + "allow-topic-thumbnails": "Nutzern erlauben Themen Thumbnails hochzuladen", + "topic-thumb-size": "Thema Thumbnailgröße", + "allowed-file-extensions": "Erlaubte Dateiendungen", + "allowed-file-extensions-help": "Komma-getrennte Liste der Dateiendungen hier einfügen (z.B. pdf,xls,doc). Eine leere Liste bedeutet, dass alle Dateiendungen erlaubt sind.", + "upload-limit-threshold": "Benutzer-Uploads begrenzen auf:", + "upload-limit-threshold-per-minute": "Pro %1 Minute", + "upload-limit-threshold-per-minutes": "Pro %1 Minuten", + "profile-avatars": "Profil Avatare", + "allow-profile-image-uploads": "Nutzern erlauben Profilbilder hochzuladen", + "convert-profile-image-png": "Hochgeladene Profilbilder in PNG konvertieren", + "default-avatar": "Benutzerdefinierter Standardavatar", + "upload": "Hochladen", + "profile-image-dimension": "Profilbild-Abmessungen", + "profile-image-dimension-help": "(in pixeln, standard: 128 pixel)", + "max-profile-image-size": "Maximale Profibild-Dateigröße", + "max-profile-image-size-help": "(In Kibibytes, Standardmäßig 256 KiB)", + "max-cover-image-size": "Maximale Deckbild-Dateigröße", + "max-cover-image-size-help": "(In Kibibytes, Standardmäßig 2048 KiB)", + "keep-all-user-images": "Alte Avatar- und Deckbild-Versionen auf dem Server lassen", + "profile-covers": "Profil Deckbilder", + "default-covers": "Standard Profil-Deckbilder", + "default-covers-help": "Komma-getrennte Standard-Deckbilder für Konten, die kein Deckbild hochgeladen haben" +} diff --git a/public/language/de/admin/settings/user.json b/public/language/de/admin/settings/user.json new file mode 100644 index 0000000000..2af71b2cc6 --- /dev/null +++ b/public/language/de/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentifizierung", + "email-confirm-interval": "Der Benutzer kann für ", + "email-confirm-interval2": "Minuten sind verstrichen", + "allow-login-with": "Erlaube Login mit", + "allow-login-with.username-email": "Benutzername oder E-Mail", + "allow-login-with.username": "Nur Benutzername", + "account-settings": "Kontoeinstellungen", + "gdpr_enabled": "Aktivieren Sie die DSGVO-Zustimmungserfassung", + "gdpr_enabled_help": "Wenn diese Option aktiviert ist, müssen alle neuen Registranten ausdrücklich der Datenerhebung und -nutzung gemäß der Datenschutz-Grundverordnung (DSGVO) zustimmen. Hinweis: Durch die Aktivierung der DSGVO werden bereits vorhandene Benutzer nicht gezwungen, ihre Zustimmung zu erteilen. Dazu müssen Sie das GDPR-Plugin installieren.", + "disable-username-changes": "Deaktiviere Änderungen des Benutzernames", + "disable-email-changes": "Deaktiviere Änderungen der E-Mail Adresse", + "disable-password-changes": "Deaktiviere Änderungen des Passwortes", + "allow-account-deletion": "Erlaube löschen des Kontos", + "hide-fullname": "Den 'Kompletten Namen' von Benutzern verstecken", + "hide-email": "Die Email-Adresse von Benutzern verstecken", + "show-fullname-as-displayname": "Vollständigen Namen des Benutzers als seinen Anzeigenamen anzeigen, falls verfügbar", + "themes": "Themes", + "disable-user-skins": "Verhindere das Benutzer eigene Skins verwenden", + "account-protection": "Kontosicherheit", + "admin-relogin-duration": "Dauer bis zum erneuten Login (in Minuten)", + "admin-relogin-duration-help": "Nach einer gesetzten Zeit erfordert der Zugriff auf die Admin Sektion einen erneuten Login, 0 deaktiviert dies", + "login-attempts": "Login-Versuche pro Stunde", + "login-attempts-help": "Wenn die Loginversuche eines Kontos diese Schwelle überschreiten, wird dieser Account für eine festgelegte Zeit gesperrt", + "lockout-duration": "Konto Aussperrzeitraum (Minuten)", + "login-days": "Anzahl der tage die login sessions bestehen bleiben sollen", + "password-expiry-days": "Erzwinge ein Passwortreset nach x Tagen", + "session-time": "Sitzungszeitraum", + "session-time-days": "Tage", + "session-time-seconds": "Sekunden", + "session-time-help": "Diese Werte legen fest, wie lange ein Benutzer angemeldet bleibt, wenn er die Option "Eingeloggt bleiben" beim Login aktiviert. Beachte, dass nur einer dieser Werte verwendet wird. Wenn Sekunden nicht festgelegt wurden, greifen wir auf Tage zurück. Wenn Tage nicht festlegt wurden, werden standardmäßig 14 Tage verwendet.", + "online-cutoff": "Minuten nachdem der Benutzer als inaktiv betrachtet wird", + "online-cutoff-help": "Wenn der Benutzer für diese Dauer keine Aktionen ausführt, wird er als inaktiv betrachtet und erhält keine Echtzeit-Updates.", + "registration": "Benutzer Registrierung", + "registration-type": "Registrierungart", + "registration-approval-type": "Art der Registrierungsgenehmigung", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin-Genehmigung", + "registration-type.admin-approval-ip": "Admin Genehmigung für IPs", + "registration-type.invite-only": "Nur Einladungen", + "registration-type.admin-invite-only": "Nur Admin Einladungen", + "registration-type.disabled": "Keine Registrierung", + "registration-type.help": "Normal - Benutzer können sich auf der Seite /register registrieren
\nNur einladen – Benutzer können andere über die Benutzerseite einladen.
\nNur Administrator-Einladung – Nur Administratoren können andere von Benutzer- und Administrator-/Verwaltungs-/Benutzerseiten einladen.
\nKeine Registrierung - Keine Benutzerregistrierung.
", + "registration-approval-type.help": "Normal - Benutzer werden sofort registriert.
\nAdmin-Genehmigung – Benutzerregistrierungen werden in eine Genehmigungswarteschlange für Administratoren gestellt.
\nAdmin-Genehmigung für IPs – Normal für neue Benutzer, Admin-Genehmigung für IP-Adressen, die bereits ein Konto haben.
", + "registration-queue-auto-approve-time": "Automatische Genehmigungszeit", + "registration-queue-auto-approve-time-help": "Stunden, bevor der Benutzer automatisch genehmigt wird. 0 zum Deaktivieren.", + "registration-queue-show-average-time": "Zeigen Sie Benutzern die durchschnittliche Zeit, die es dauert, einen neuen Benutzer zu genehmigen", + "registration.max-invites": "Maximale Einladungen pro Benutzer", + "max-invites": "Maximale Einladungen pro Benutzer", + "max-invites-help": "0 für keine Beschränkung. Admins haben keine beschränkung.
Nur praktikabel für \"Nur Einladungen\".", + "invite-expiration": "Einladungsfrist", + "invite-expiration-help": "# der Tage nachdem Einladungen auslaufen.", + "min-username-length": "Minimale länge des Benutzernamens", + "max-username-length": "Maximale länge des Benutzernamens", + "min-password-length": "Minimale länge des Passwortes", + "min-password-strength": "Minimale Passwort stärke", + "max-about-me-length": "Maximale länge von Über Mich", + "terms-of-use": "Forum Nutzungsbedingungen (Leer lassen um es zu deaktivieren)", + "user-search": "Benutzersuche", + "user-search-results-per-page": "Anzahl anzuzeigener Ergebnisse", + "default-user-settings": "Standard Benutzer Einstellungen", + "show-email": "Zeige E-Mail-Adresse", + "show-fullname": "Zeige vollen Namen", + "restrict-chat": "Erlaube nur Chatnachrichten von Benutzern denen ich folge", + "outgoing-new-tab": "Öffne externe Links in einem neuen Tab", + "topic-search": "Suchen innerhalb von Themen aktivieren", + "update-url-with-post-index": " URL während Themen durchsuchen mit dem Beitragsindex aktivieren", + "digest-freq": "Zusammenfassung abonnieren", + "digest-freq.off": "Aus", + "digest-freq.daily": "Täglich", + "digest-freq.weekly": "Wöchentlich", + "digest-freq.biweekly": "Zweimal wöchentlich", + "digest-freq.monthly": "Monatlich", + "email-chat-notifs": "Sende eine E-Mail, wenn eine neue Chat-Nachricht eingeht und ich nicht online bin", + "email-post-notif": "Sende eine E-Mail wenn auf Themen die ich abonniert habe geantwortet wird", + "follow-created-topics": "Themen folgen, die du erstellst", + "follow-replied-topics": "Themen folgen, auf die du antwortest", + "default-notification-settings": "Standardbenachrichtigungseinstellungen", + "categoryWatchState": "Standardmäßige Beobachtung", + "categoryWatchState.watching": "Beobachtet", + "categoryWatchState.notwatching": "Nicht beobachtet", + "categoryWatchState.ignoring": "Ignoriert" +} diff --git a/public/language/de/admin/settings/web-crawler.json b/public/language/de/admin/settings/web-crawler.json new file mode 100644 index 0000000000..d898fa69ac --- /dev/null +++ b/public/language/de/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawling-Einstellungen", + "robots-txt": "Benutzerdefinierte robots.txt Leer lassen für Standardeinstellung", + "sitemap-feed-settings": "Sitemap- und Feed-Einstellungen", + "disable-rss-feeds": "RSS Feeds deaktivieren", + "disable-sitemap-xml": "sitemap.xml deaktivieren", + "sitemap-topics": "Anzahl der Themen, die in der Sitemap angezeigt werden sollen", + "clear-sitemap-cache": "Sitemap Cache leeren", + "view-sitemap": "Sitemap anzeigen" +} \ No newline at end of file diff --git a/public/language/de/category.json b/public/language/de/category.json new file mode 100644 index 0000000000..08465287d9 --- /dev/null +++ b/public/language/de/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategorie", + "subcategories": "Unterkategorien", + "new_topic_button": "Neues Thema", + "guest-login-post": "Melde dich an, um einen Beitrag zu erstellen", + "no_topics": "Es gibt noch keine Themen in dieser Kategorie.
Warum beginnst du nicht eins?", + "browsing": "Aktiv", + "no_replies": "Niemand hat geantwortet", + "no_new_posts": "Keine neuen Beiträge.", + "watch": "Beobachten", + "ignore": "Ignorieren", + "watching": "Beobachte", + "not-watching": "Nicht beobachtet", + "ignoring": "Ignoriert", + "watching.description": "Zeige Themen in Ungelesen und Aktuell", + "not-watching.description": "Zeige keine Themen in Ungelesen, zeige sie in Aktuell", + "ignoring.description": "Zeige keine Themen in Ungelesen und Aktuell", + "watching.message": "Du beobachtest jetzt Aktualisierungen aus dieser Kategorie und allen Unterkategorien", + "notwatching.message": "Du beobachtest jetzt keine Aktualisierungen aus dieser Kategorie und allen Unterkategorien", + "ignoring.message": "Du ignorierst jetzt Aktualisierungen aus dieser Kategorie und allen Unterkategorien", + "watched-categories": "Beobachtete Kategorien", + "x-more-categories": "%1 weitere Kategorien" +} \ No newline at end of file diff --git a/public/language/de/email.json b/public/language/de/email.json new file mode 100644 index 0000000000..212334e87f --- /dev/null +++ b/public/language/de/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test E-Mail", + "password-reset-requested": "Zurücksetzung des Passworts beantragt!", + "welcome-to": "Willkommen bei %1", + "invite": "Einladung von %1", + "greeting_no_name": "Hallo", + "greeting_with_name": "Hallo %1", + "email.verify-your-email.subject": "Bitte bestätige deine Email-Adresse", + "email.verify.text1": "Sie haben angefordert, dass wir Ihre E-Mail-Adresse ändern oder bestätigen", + "email.verify.text2": "Aus Sicherheitsgründen ändern oder bestätigen wir die hinterlegte E-Mail-Adresse erst, nachdem ihr Eigentum per E-Mail bestätigt wurde. Wenn Sie dies nicht angefordert haben, müssen Sie nichts unternehmen.", + "email.verify.text3": "Sobald Sie diese E-Mail-Adresse bestätigen, ersetzen wir Ihre aktuelle E-Mail-Adresse durch diese (%1).", + "welcome.text1": "Vielen Dank für die Registrierung bei %1!", + "welcome.text2": "Um dein Konto vollständig zu aktivieren, müssen wir überprüfen, ob du Besitzer der E-Mail-Adresse bist, mit der du dich registriert hast.", + "welcome.text3": "Ein Administrator hat deine Registrierung aktzeptiert. Du kannst dich jetzt mit deinem Benutzernamen/Passwort einloggen.", + "welcome.cta": "Klicke hier, um deine E-Mail-Adresse zu bestätigen.", + "invitation.text1": "%1 hat dich eingeladen %2 beizutreten", + "invitation.text2": "Deine Einladung wird in %1 Tagen ablaufen.", + "invitation.cta": "Klicke hier um deinen Account zu erstellen.", + "reset.text1": "Wir haben eine Anfrage auf Zurücksetzung deines Passworts erhalten, wahrscheinlich, weil du es vergessen hast. Falls dies nicht der Fall ist, ignoriere bitte diese E-Mail.", + "reset.text2": "Klicke bitte auf den folgenden Link, um mit der Zurücksetzung deines Passworts fortzufahren:", + "reset.cta": "Klicke hier, um dein Passwort zurückzusetzen", + "reset.notify.subject": "Passwort erfolgreich geändert", + "reset.notify.text1": "Wir benachrichtigen dich, dass dein Passwort am %1 erfolgreich geändert wurde.", + "reset.notify.text2": "Bitte benachrichtige umgehend einen Administrator, wenn du dies nicht autorisiert hast.", + "digest.latest_topics": "Neueste Themen auf %1", + "digest.top-topics": "Top-Themen von %1", + "digest.popular-topics": "Beliebte Themen von %1", + "digest.cta": "Klicke hier, um %1 zu besuchen", + "digest.unsub.info": "Diese Zusammenfassung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.", + "digest.day": "des letzten Tages", + "digest.week": "der letzten Woche", + "digest.month": "des letzen Monats", + "digest.subject": "Zusammenfassung für %1", + "digest.title.day": "Deine tägliche Zusammenfassung", + "digest.title.week": "Deine wöchentliche Zusammenfassung", + "digest.title.month": "Deine monatliche Zusammenfassung", + "notif.chat.subject": "Neue Chatnachricht von %1 erhalten", + "notif.chat.cta": "Klicke hier, um die Unterhaltung fortzusetzen", + "notif.chat.unsub.info": "Diese Chat-Benachrichtigung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.", + "notif.post.unsub.info": "Diese Mitteilung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.", + "notif.post.unsub.one-click": "Alternativ, melde dich von zukünftigen E-Mails wie dieser ab, durch klicken auf", + "notif.cta": "Zum Forum", + "notif.cta-new-reply": "Post anzeigen", + "notif.cta-new-chat": "Chat anzeigen", + "notif.test.short": "Benachrichtigungen testen", + "notif.test.long": "Dies ist eine Test-Mail für Benachrichtigungen. Zu Hilfe!", + "test.text1": "Dies ist eine Test-E-Mail, um zu überprüfen, ob der E-Mailer deines NodeBB korrekt eingestellt wurde.", + "unsub.cta": "Klicke hier, um diese Einstellungen zu ändern", + "unsubscribe": "abmelden", + "unsub.success": "Du wirst keine weiteren E-Mails von der %1 Mailing-Liste erhalten.", + "unsub.failure.title": "Abbestellen nicht möglich", + "unsub.failure.message": "Leider konnten wir Sie nicht von der Mailingliste abmelden, da es ein Problem mit dem Link gab. Sie können jedoch Ihre E-Mail-Einstellungen ändern, indem Sie zu Ihren Benutzereinstellungen gehen.

(Fehler: %1)", + "banned.subject": "Du wurdest von %1 gebannt.", + "banned.text1": "Der Benutzer %1 wurde von %2 gebannt.", + "banned.text2": "Dieser Bann wird bis %1 dauern.", + "banned.text3": "Diese ist der Grund weshalb du gebannt wurdest", + "closing": "Danke!" +} \ No newline at end of file diff --git a/public/language/de/error.json b/public/language/de/error.json new file mode 100644 index 0000000000..76ba50487e --- /dev/null +++ b/public/language/de/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Ungültige Daten", + "invalid-json": "Ungültiges JSON", + "wrong-parameter-type": "Für die Eigenschaft „%1“ wurde ein Wert vom Typ %3 erwartet, aber stattdessen wurde %2 empfangen", + "required-parameters-missing": "Bei diesem API-Aufruf fehlten erforderliche Parameter: %1", + "not-logged-in": "Du bist nicht angemeldet.", + "account-locked": "Dein Konto wurde vorübergehend gesperrt.", + "search-requires-login": "Die Suche erfordert ein Konto, bitte einloggen oder registrieren.", + "goback": "Drücke zurück um zur vorherigen Seite zurückzukehren", + "invalid-cid": "Ungültige Kategorie-ID", + "invalid-tid": "Ungültige Themen-ID", + "invalid-pid": "Ungültige Beitrags-ID", + "invalid-uid": "Ungültige Benutzer-ID", + "invalid-mid": "Ungültige Chatnachrichten-ID", + "invalid-date": "Es muss ein gültiges Datum angegeben werden", + "invalid-username": "Ungültiger Benutzername", + "invalid-email": "Ungültige E-Mail-Adresse", + "invalid-fullname": "Ungültiger Name", + "invalid-location": "Ungültiger Ort", + "invalid-birthday": "Ungültiger Geburtstag", + "invalid-title": "Ungültiger Titel", + "invalid-user-data": "Ungültige Benutzerdaten", + "invalid-password": "Ungültiges Passwort", + "invalid-login-credentials": "Ungültige Zugangsdaten", + "invalid-username-or-password": "Bitte gib sowohl einen Benutzernamen als auch ein Passwort an", + "invalid-search-term": "Ungültige Suchanfrage", + "invalid-url": "Ungültige URL", + "invalid-event": "Ungültiges Ereignis: %1", + "local-login-disabled": "Lokales Login System wurde für nicht-priviligierte Konten deaktiviert.", + "csrf-invalid": "Dein Login war nicht erfolgreich da wahrscheinlich deine Sitzung abgelaufen ist. Bitte versuche es noch einmal", + "invalid-path": "Ungültiger Pfad", + "folder-exists": "Ordner existiert", + "invalid-pagination-value": "Ungültige Seitennummerierung, muss mindestens %1 und maximal %2 sein", + "username-taken": "Der Benutzername ist bereits vergeben", + "email-taken": "E-Mail-Adresse vergeben", + "email-nochange": "Die eingegebene E-Mail ist die gleiche wie die bereits hinterlegte E-Mail.", + "email-invited": "E-Mail wurde bereits eingeladen", + "email-not-confirmed": "Das Schreiben von Beiträgen in einigen Kategorien oder Themen ist erst möglich, wenn Ihre E-Mail bestätigt wurde. Bitte klicken Sie hier, um eine Bestätigungs-E-Mail zu senden.", + "email-not-confirmed-chat": "Du kannst den Chat erst nutzen wenn deine E-Mail bestätigt wurde, bitte klicke hier, um deine E-Mail zu bestätigen.", + "email-not-confirmed-email-sent": "Ihre E-Mail wurde noch nicht bestätigt. Bitte überprüfen Sie Ihren Posteingang auf die Bestätigungs-E-Mail. Es kann sein, dass Sie nicht in der Lage sind, in einigen Kategorien zu schreiben oder zu chatten, bis Ihre E-Mail bestätigt ist.", + "no-email-to-confirm": "Für Ihr Konto ist keine E-Mail angegeben. Eine E-Mail ist für die Wiederherstellung des Kontos erforderlich und kann für das Chatten und in einigen Kategorien für das Schreiben von Beiträgen notwendig sein. Bitte klicken Sie hier, um eine E-Mail einzugeben.", + "user-doesnt-have-email": "Für den Benutzer \"%1\" ist keine E-Mail eingetragen.", + "email-confirm-failed": "Wir konnten deine E-Mail-Adresse nicht bestätigen, bitte versuch es später noch einmal", + "confirm-email-already-sent": "Die Bestätigungsmail wurde verschickt. Bitte warte %1 Minute(n), um eine weitere zu verschicken.", + "sendmail-not-found": "Sendmail wurde nicht gefunden. Bitte stelle sicher, dass es installiert ist und durch den Benutzer unter dem NodeBB läuft ausgeführt werden kann.", + "digest-not-enabled": "Dieser Benutzer hat Email-Zusammenfassungen deaktiviert oder das Aussenden von Email-Zusammenfassungen is in den Defaulteinstellungen des Systems nicht aktiviert.", + "username-too-short": "Benutzername ist zu kurz", + "username-too-long": "Benutzername ist zu lang", + "password-too-long": "Passwort ist zu lang", + "reset-rate-limited": "Zu viele Anfragen zum Zurücksetzen des Passworts (Rate begrenzt)", + "reset-same-password": "Bitte verwenden Sie ein anderes Passwort als Ihr derzeitiges.", + "user-banned": "Benutzer ist gesperrt", + "user-banned-reason": "Entschuldige, dieses Konto wurde gesperrt (Grund: %1)", + "user-banned-reason-until": "Entschuldigung, dieses Konto wurde bis %1 (Reason: %2) gesperrt.", + "user-too-new": "Entschuldigung, du musst %1 Sekunde(n) warten, bevor du deinen ersten Beitrag schreiben kannst.", + "blacklisted-ip": "Deine IP-Adresse ist für dieses Forum gesperrt. Sollte dies ein Irrtum sein, dann kontaktiere bitte einen Administrator.", + "ban-expiry-missing": "Bitte gib ein Enddatum für diese Sperrung an", + "no-category": "Die Kategorie existiert nicht", + "no-topic": "Das Thema existiert nicht", + "no-post": "Der Beitrag existiert nicht", + "no-group": "Die Gruppe existiert nicht", + "no-user": "Der Benutzer existiert nicht", + "no-teaser": "Zusammenfassung existiert nicht", + "no-flag": "Markierung existiert nicht", + "no-privileges": "Du verfügst nicht über ausreichende Berechtigungen, um die Aktion durchzuführen.", + "category-disabled": "Kategorie ist deaktiviert", + "topic-locked": "Thema ist gesperrt", + "post-edit-duration-expired": "Entschuldigung, du darfst Beiträge nur %1 Sekunde(n) nach dem Veröffentlichen editieren.", + "post-edit-duration-expired-minutes": "Du darfst Beiträge lediglich innerhalb von %1 Minuten/n nach dem Erstellen editieren", + "post-edit-duration-expired-minutes-seconds": "Du darfst Beiträge lediglich innerhalb von %1 Minuten/n und %2 Sekunden nach dem Erstellen editieren", + "post-edit-duration-expired-hours": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n nach dem Erstellen editieren", + "post-edit-duration-expired-hours-minutes": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n und %2 Minute/n nach dem Erstellen editieren", + "post-edit-duration-expired-days": "Du darfst Beiträge lediglich innerhalb von %1 Tag/en nach dem Erstellen editieren", + "post-edit-duration-expired-days-hours": "Du darfst Beiträge lediglich innerhalb von %1 Tag/en und %2 Stunde/n nach dem Erstellen editieren", + "post-delete-duration-expired": "Du darfst Beiträge lediglich innerhalb von %1 Sekunden nach dem Erstellen löschen", + "post-delete-duration-expired-minutes": "Du darfst Beiträge lediglich innerhalb von %1 Minute(n) nach dem Erstellen löschen", + "post-delete-duration-expired-minutes-seconds": "Du darfst Beiträge lediglich innerhalb von %1 Minute(n) und %2 Sekunde(n) nach dem Erstellen löschen", + "post-delete-duration-expired-hours": "Du darfst Beiträge lediglich innerhalb von %1 Stunde/n nach dem Erstellen löschen", + "post-delete-duration-expired-hours-minutes": "Du darfst Beiträge lediglich innerhalb von %1 Stunde(n) und %2 Minute(n) nach dem Erstellen löschen", + "post-delete-duration-expired-days": "Du darfst Beiträge lediglich innerhalb von %1 Tag(en) nach dem Erstellen löschen", + "post-delete-duration-expired-days-hours": "Du darfst Beiträge lediglich innerhalb von %1 Tag/en und %2 Stunde/n nach dem Erstellen löschen", + "cant-delete-topic-has-reply": "Du kannst ein Thema nicht löschen, wenn es bereits eine Antwort gibt", + "cant-delete-topic-has-replies": "Du kannst ein Thema nicht löschen, wenn es bereits %1 Antworten gibt", + "content-too-short": "Bitte schreibe einen längeren Beitrag. Beiträge sollten mindestens %1 Zeichen enthalten.", + "content-too-long": "Bitte schreibe einen kürzeren Beitrag. Beiträge können nicht länger als %1 Zeichen sein.", + "title-too-short": "Bitte gebe einen längeren Titel ein. Ein Titel muss mindestens %1 Zeichen enthalten.", + "title-too-long": "Bitten gebe einen kürzeren Titel ein. Ein Titel darf nicht mehr als %1 Zeichen enthalten.", + "category-not-selected": "Kategorie nicht ausgewählt", + "too-many-posts": "Du kannst nur einen Beitrag innerhalb von %1 Sekunden erstellen - Bitte warte bevor Du erneut einen Beitrag erstellst.", + "too-many-posts-newbie": "Als neuer Benutzer kannst du nur einmal alle %1 Sekunde(n) posten, bis du %2 Reputation erworben hast - bitte warte, bevor du erneut postest", + "already-posting": "You are already posting", + "tag-too-short": "Bitte gebe ein längeres Schlagwort ein. Schlagworte sollten mindestens %1 Zeichen enthalten.", + "tag-too-long": "Bitte gebe ein kürzeres Schlagwort ein. Schlagworte können nicht länger als %1 Zeichen sein.", + "not-enough-tags": "Nicht genügend Schlagworte. Themen müssen mindestens %1 Schlagwort(e) enthalten", + "too-many-tags": "Zu viele Schlagworte. Themen dürfen nicht mehr als %1 Schlagwort(e) enthalten", + "cant-use-system-tag": "Sie können dieses System-Tag nicht verwenden.", + "cant-remove-system-tag": "Sie können dieses System-Tag nicht entfernen.", + "still-uploading": "Bitte warte bis der Vorgang abgeschlossen ist.", + "file-too-big": "Die maximale Dateigröße ist %1 kB, bitte lade eine kleinere Datei hoch.", + "guest-upload-disabled": "Uploads für Gäste wurden deaktiviert.", + "cors-error": "Das Hochladen von Bildern ist aufgrund von falsch konfigurierten CORS nicht möglich.", + "upload-ratelimit-reached": "Sie haben zu viele Dateien auf einmal hochgeladen. Bitte versuchen Sie es später noch einmal.", + "scheduling-to-past": "Wählen Sie bitte ein Datum in der Zukunft.", + "invalid-schedule-date": "Geben Sie bitte ein gültiges Datum und eine Uhrzeit ein.", + "cant-pin-scheduled": "Geplante Themen können nicht (un)angeheftet werden.", + "cant-merge-scheduled": "Geplante Themen können nicht zusammengeführt werden.", + "cant-move-posts-to-scheduled": "Beiträge können nicht in ein geplantes Thema verschoben werden.", + "cant-move-from-scheduled-to-existing": "Beiträge können nicht in ein geplantes Thema verschoben werden.", + "already-bookmarked": "Du hast diesen Beitrag bereits als Lesezeichen gespeichert", + "already-unbookmarked": "Du hast diesen Beitrag bereits aus deinen Lesezeichen entfernt", + "cant-ban-other-admins": "Du kannst andere Administratoren nicht sperren!", + "cant-mute-other-admins": "Du kannst keine anderen Admins stummschalten!", + "user-muted-for-hours": "Du wurdest stumgeschlatet, du kannst wieder in %1 Stunde(n) posten", + "user-muted-for-minutes": "Du wurdest stumgeschlatet, du kannst wieder in %1 Minute(n) posten", + "cant-make-banned-users-admin": "Sie können gesperrte Benutzer nicht zum Administrator machen.", + "cant-remove-last-admin": "Du bist der einzige Administrator. Füge zuerst einen anderen Administrator hinzu, bevor du dich selbst als Administrator entfernst", + "account-deletion-disabled": "Kontolöschung ist deaktiviert", + "cant-delete-admin": "Bevor du versuchst dieses Konto zu löschen, entferne die zugehörigen Administratorrechte.", + "already-deleting": "Bereits gelöscht", + "invalid-image": "Ungültiges Bild", + "invalid-image-type": "Falsche Bildart. Erlaubte Arten sind: %1", + "invalid-image-extension": "Ungültige Dateinamenerweiterung", + "invalid-file-type": "Ungültiger Dateityp. Erlaubte Typen sind: %1", + "invalid-image-dimensions": "Die Bildabmessungen sind zu groß.", + "group-name-too-short": "Gruppenname zu kurz", + "group-name-too-long": "Gruppenname zu lang", + "group-already-exists": "Gruppe existiert bereits", + "group-name-change-not-allowed": "Du kannst den Namen der Gruppe nicht ändern", + "group-already-member": "Bereits Teil dieser Gruppe", + "group-not-member": "Du bist kein Mitglied dieser Gruppe", + "group-needs-owner": "Diese Gruppe muss mindestens einen Besitzer vorweisen", + "group-already-invited": "Dieser Benutzer wurde bereits eingeladen", + "group-already-requested": "Deine Mitgliedsanfrage wurde bereits eingereicht", + "group-join-disabled": "Du kannst dieser Gruppe zur Zeit nicht beitreten", + "group-leave-disabled": "Du kannst diese Gruppe zur Zeit nicht verlassen", + "post-already-deleted": "Dieser Beitrag ist bereits gelöscht worden", + "post-already-restored": "Dieser Beitrag ist bereits wiederhergestellt worden", + "topic-already-deleted": "Dieses Thema ist bereits gelöscht worden", + "topic-already-restored": "Dieses Thema ist bereits wiederhergestellt worden", + "cant-purge-main-post": "Du kannst den Hauptbeitrag nicht löschen, bitte lösche stattdessen das Thema", + "topic-thumbnails-are-disabled": "Vorschaubilder für Themen sind deaktiviert", + "invalid-file": "Ungültige Datei", + "uploads-are-disabled": "Uploads sind deaktiviert", + "signature-too-long": "Entschuldigung, deine Signatur kann nicht länger als %1 Zeichen sein.", + "about-me-too-long": "Entschuldigung, dein \"über mich\" kann nicht länger als %1 Zeichen sein.", + "cant-chat-with-yourself": "Du kannst nicht mit dir selber chatten!", + "chat-restricted": "Dieser Benutzer hat seine Chatfunktion eingeschränkt. Du kannst nur mit diesem Benutzer chatten, wenn er dir folgt.", + "chat-disabled": "Das Chatsystem deaktiviert", + "too-many-messages": "Du hast zu viele Nachrichten versandt, bitte warte eine Weile.", + "invalid-chat-message": "Ungültige Nachricht", + "chat-message-too-long": "Nachrichten dürfen nicht länger als %1 Zeichen sein.", + "cant-edit-chat-message": "Du darfst diese Nachricht nicht ändern", + "cant-delete-chat-message": "Du darfst diese Nachricht nicht löschen", + "chat-edit-duration-expired": "Du darfst Chat-Nachrichten nur bis zu %1 Sekunde(n) nach der erstellung verändern", + "chat-delete-duration-expired": "Du darfst Chat-Nachrichten nur bis zu %1 Sekunde(n) nach der erstellung löschen", + "chat-deleted-already": "Diese Chatnachricht wurde bereits gelöscht.", + "chat-restored-already": "Diese Chatnachricht wurde bereits wiederhergestellt.", + "chat-room-does-not-exist": "Der Chatraum existiert nicht.", + "already-voting-for-this-post": "Du hast diesen Beitrag bereits bewertet.", + "reputation-system-disabled": "Das Reputationssystem ist deaktiviert.", + "downvoting-disabled": "Downvotes sind deaktiviert.", + "not-enough-reputation-to-chat": "Du benötigst %1 Ruf zum Chatten", + "not-enough-reputation-to-upvote": "Du benötigst %1 Ruf, um upvoten zu können", + "not-enough-reputation-to-downvote": "Du benötigst %1 Ruf, um abzustimmen", + "not-enough-reputation-to-flag": "Du benötigst %1 Ruf, um diesen Beitrag zu melden", + "not-enough-reputation-min-rep-website": "Du benötigst %1 Ruf, um eine Website hinzuzufügen", + "not-enough-reputation-min-rep-aboutme": "Du benötigst %1 Ruf, um eine Über mich hinzuzufügen", + "not-enough-reputation-min-rep-signature": "Du benötigst %1 Reputation, um eine Signatur hinzuzufügen", + "not-enough-reputation-min-rep-profile-picture": "Du benötigst %1 Ruf, um ein Profilbild hinzuzufügen", + "not-enough-reputation-min-rep-cover-picture": "Du benötigst %1 Ruf, um ein Titelbild hinzuzufügen", + "post-already-flagged": "Du hast diesen Beitrag bereits gemeldet", + "user-already-flagged": "Du hast diesen Benutzer bereits gemeldet", + "post-flagged-too-many-times": "Dieser Beitrag wurde bereits von anderen Benutzern gemeldet", + "user-flagged-too-many-times": "Dieser Benutzer wurde bereits von anderen Benutzern gemeldet", + "cant-flag-privileged": "Sie dürfen die Profile oder Inhalte von privilegierten Benutzern (Moderatoren/Globalmoderatoren/Admins) nicht kennzeichnen.", + "self-vote": "Du kannst deine eigenen Beiträge nicht bewerten", + "too-many-upvotes-today": "Du kannst nur %1 Mal pro Tag upvoten", + "too-many-upvotes-today-user": "Du kannst einen Benutzer nur %1 Mal am Tag positiv bewerten", + "too-many-downvotes-today": "Du kannst nur %1 mal am Tag eine schlechte Bewertung abgeben", + "too-many-downvotes-today-user": "Du kannst einen Benutzer nur %1 mal am Tag schlecht bewerten", + "reload-failed": "Es ist ein Problem während des Reloads von NodeBB aufgetreten: \"%1\". NodeBB wird weiterhin clientseitige Assets bereitstellen, allerdings solltest du das, was du vor dem Reload gemacht hast, rückgängig machen.", + "registration-error": "Registrierungsfehler", + "parse-error": "Beim auswerten der Serverantwort ist etwas schiefgegangen", + "wrong-login-type-email": "Bitte nutze deine E-Mail-Adresse zum einloggen", + "wrong-login-type-username": "Bitte nutze deinen Benutzernamen zum einloggen", + "sso-registration-disabled": "Das Registrieren mit %1-Konten wurde deaktiviert, bitte registriere dich zuerst mit einer Email-Adresse", + "sso-multiple-association": "Du kannst mehrere Konten nicht von diesem Dienst mit deinem NodeBB Konto verknüpfen. Bitte hebe die Verknüpfung mit deinem bestehenden Konto auf und versuche es erneut.", + "invite-maximum-met": "Du hast bereits die maximale Anzahl an Personen eingeladen (%1 von %2).", + "no-session-found": "Keine Login-Sitzung gefunden!", + "not-in-room": "Benutzer nicht im Raum", + "cant-kick-self": "Du kannst dich nicht selber aus der Gruppe entfernen.", + "no-users-selected": "Kein(e) Benutzer ausgewählt", + "invalid-home-page-route": "Ungültiger Startseitenpfad", + "invalid-session": "Ungültige Session", + "invalid-session-text": "Es scheint als wäre deine Login-Sitzung nicht mehr aktiv. Bitte aktualisiere diese Seite.", + "session-mismatch": "Sitzungskonflikt", + "session-mismatch-text": "Es sieht so aus, als ob deine Login-Sitzung nicht mehr mit dem Server übereinstimmt. Bitte aktualisieren Sie diese Seite.", + "no-topics-selected": "Keine Beiträge ausgewählt!", + "cant-move-to-same-topic": "Du kannst den Beitrag nicht in das selbe Thema schieben!", + "cant-move-topic-to-same-category": "Das Thema kann nicht zur selben Kategorie verschoben werden!", + "cannot-block-self": "Du kannst dich nicht selbst blocken!", + "cannot-block-privileged": "Du kannst keine Administratoren bzw. Globale Moderatoren blocken.", + "cannot-block-guest": "Gäste können andere Nutzer nicht blockieren.", + "already-blocked": "Dieser Nutzer ist bereits gesperrt", + "already-unblocked": "Dieser Nutzer ist bereits entsperrt", + "no-connection": "Es scheint als gäbe es ein Problem mit deiner Internetverbindung", + "socket-reconnect-failed": "Der Server kann zurzeit nicht erreicht werden. Klicken Sie hier, um es erneut zu versuchen, oder versuchen Sie es später erneut", + "plugin-not-whitelisted": "Plugin kann nicht installiert werden – nur Plugins, die vom NodeBB Package Manager in die Whitelist aufgenommen wurden, können über den ACP installiert werden", + "plugins-set-in-configuration": "Du darfst den Status der Plugins nicht ändern, da sie zur Laufzeit definiert werden (config.json, Umgebungsvariablen oder Terminalargumente). Bitte ändere stattdessen die Konfiguration.", + "theme-not-set-in-configuration": "Wenn in der Konfiguration aktive Plugins definiert werden, muss bei einem Themenwechsel das neue Thema zur Liste der aktiven Plugins hinzugefügt werden, bevor es im ACP aktualisiert wird.", + "topic-event-unrecognized": "Themenereignis „%1“ nicht erkannt", + "cant-set-child-as-parent": "Untergeordnete Kategorie kann nicht als übergeordnete Kategorie festgelegt werden", + "cant-set-self-as-parent": "Die aktuelle Kategorie kann nicht als übergeordnete Kategorie festgelegt werden", + "api.master-token-no-uid": "Ein Master-Token wurde ohne eine entsprechende `_uid` im Anfrage-Body empfangen", + "api.400": "Irgendetwas stimmte nicht mit der von Ihnen übergebenen Anforderungsnutzlast.", + "api.401": "Es wurde keine gültige Anmeldesitzung gefunden. Bitte melden Sie sich an und versuchen Sie es erneut.", + "api.403": "Sie sind nicht berechtigt, diesen Anruf zu tätigen", + "api.404": "Ungültiger API-Aufruf", + "api.426": "HTTPS ist für Anfragen an die Schreib-API erforderlich, bitte senden Sie Ihre Anfrage erneut über HTTPS", + "api.429": "Sie haben zu viele Anfragen gestellt, bitte versuchen Sie es später erneut", + "api.500": "Beim Versuch, Ihre Anfrage zu bearbeiten, ist ein unerwarteter Fehler aufgetreten.", + "api.501": "Die Route, die Sie anrufen möchten, ist noch nicht implementiert. Bitte versuchen Sie es morgen erneut", + "api.503": "Die Route, die Sie anrufen möchten, ist derzeit aufgrund einer Serverkonfiguration nicht verfügbar" +} \ No newline at end of file diff --git a/public/language/de/flags.json b/public/language/de/flags.json new file mode 100644 index 0000000000..12ab0a85d4 --- /dev/null +++ b/public/language/de/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Zustand", + "reports": "Reports", + "first-reported": "Zuerst gemeldet", + "no-flags": "Hurra! Keine Meldungen gefunden.", + "assignee": "Zugeordneter Benutzer", + "update": "Aktualisieren", + "updated": "Aktualisiert", + "resolved": "Gelöst", + "target-purged": "Der Inhalt auf den diese Meldung hingewiesen hat, wurde gelöscht und ist nicht mehr verfügbar.", + + "graph-label": "Tägliche Meldungen", + "quick-filters": "Schnell-Filter", + "filter-active": "Ein oder mehrere Filter sind in dieser Meldungs-Liste aktiv", + "filter-reset": "Filter Entfernen", + "filters": "Filter Optionen", + "filter-reporterId": "Melder UID", + "filter-targetUid": "Gemeldete UID", + "filter-type": "Meldungstyp", + "filter-type-all": "Gesamter Inhalt", + "filter-type-post": "Beitrag", + "filter-type-user": "Benutzer", + "filter-state": "Status", + "filter-assignee": "UID des Zugewiesenen", + "filter-cid": "Kategorie", + "filter-quick-mine": "Mir zugewiesen", + "filter-cid-all": "Alle Kategorien", + "apply-filters": "Filter anwenden", + "more-filters": "Weitere Filter", + "fewer-filters": "weniger Filter", + + "quick-actions": "Schnellaktionen", + "flagged-user": "Gemeldeter Benutzer", + "view-profile": "Profil ansehen", + "start-new-chat": "Neuen Chat beginnen", + "go-to-target": "Meldungsziel ansehen", + "assign-to-me": "Mir zuweisen", + "delete-post": "Post löschen", + "purge-post": "Post bereiningen", + "restore-post": "Post wiederherstellen", + "delete": "Markierung löschen", + + "user-view": "Profil ansehen", + "user-edit": "Profil bearbeiten", + + "notes": "Meldungsnotizen", + "add-note": "Notiz hinzufügen", + "no-notes": "Keine geteilten Notizen", + "delete-note-confirm": "Bist du sicher, dass du diese Notiz löschen möchtest?", + "delete-flag-confirm": "Möchtest Du diese Markierung wirklich löschen?", + "note-added": "Notiz hinzugefügt", + "note-deleted": "Notiz gelöscht", + "flag-deleted": "Markierung gelöscht", + + "history": "Konto & Markierungsverlauf", + "no-history": "Kein Meldungsverlauf", + + "state-all": "Alle Status", + "state-open": "Neu/Öffnen", + "state-wip": "In Arbeit", + "state-resolved": "Gelöst", + "state-rejected": "Abgelehnt", + "no-assignee": "Nicht zugewiesen", + + "sort": "Sortieren nach", + "sort-newest": "Neuste zuerst", + "sort-oldest": "Älteste zuerst", + "sort-reports": "Meiste Meldungen", + "sort-all": "Alle Markierungstypen ...", + "sort-posts-only": "Nur Beiträge ...", + "sort-downvotes": "Meiste negativen Bewertungen", + "sort-upvotes": "Meiste positive Bewertungen", + "sort-replies": "Meiste Antworten", + + "modal-title": "Inhalt melden", + "modal-body": "Bitte geben Sie den Grund an, weshalb Sie %1 %2 melden wollen. Alternativ können Sie einen der Schnell-Meldungs-Knöpfe verwenden, wenn anwendbar.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Beleidigend", + "modal-reason-other": "Anderer (unten Angegeben)", + "modal-reason-custom": "Grund für die Meldung dieses Inhalts...", + "modal-submit": "Meldung abschicken", + "modal-submit-success": "Der Inhalt wurde gemeldet.", + + "bulk-actions": "Massenaktionen", + "bulk-resolve": "Meldungen bereiningen", + "bulk-success": "%1 Meldungen aktualisiert", + "flagged-timeago-readable": "Markiert (%2)", + "auto-flagged": "[Automatisch markiert] %1 Abwertungen erhalten." +} \ No newline at end of file diff --git a/public/language/de/global.json b/public/language/de/global.json new file mode 100644 index 0000000000..fa6d78dcdf --- /dev/null +++ b/public/language/de/global.json @@ -0,0 +1,126 @@ +{ + "home": "Übersicht", + "search": "Suche", + "buttons.close": "Schließen", + "403.title": "Zugriff verweigert", + "403.message": "Du hast keine Zugriffsberechtigung für diese Seite.", + "403.login": "Du solltest Dich anmelden.", + "404.title": " Nicht Gefunden", + "404.message": "Diese Seite existiert nicht. Zur Homepage zurückkehren.", + "500.title": "Interner Fehler.", + "500.message": "Ups! Scheint als wäre etwas schief gelaufen!", + "400.title": "Ungültige Anforderung", + "400.message": "Es scheint als wäre dieser Link fehlerhaft, bitte überprüfe ihn und versuche es erneut oder gehe zurück zur Startseite.", + "register": "Registrieren", + "login": "Anmelden", + "please_log_in": "Bitte anmelden", + "logout": "Abmelden", + "posting_restriction_info": "Nur registrierte Mitglieder dürfen Beiträge verfassen. Hier klicken zum Anmelden.", + "welcome_back": "Willkommen zurück", + "you_have_successfully_logged_in": "Du hast dich erfolgreich angemeldet", + "save_changes": "Änderungen speichern", + "save": "Speichern", + "close": "Schließen", + "pagination": "Seitennummerierung", + "pagination.out_of": "%1 von %2", + "pagination.enter_index": "Zum Beitragsindex gehen", + "header.admin": "Admin", + "header.categories": "Kategorien", + "header.recent": "Aktuell", + "header.unread": "Ungelesen", + "header.tags": "Tags", + "header.popular": "Beliebt", + "header.top": "Top", + "header.users": "Benutzer", + "header.groups": "Gruppen", + "header.chats": "Chats", + "header.notifications": "Benachrichtigungen", + "header.search": "Suche", + "header.profile": "Profil", + "header.navigation": "Navigation", + "notifications.loading": "Benachrichtigungen werden geladen", + "chats.loading": "Nachrichten werden geladen", + "motd.welcome": "Willkommen auf NodeBB, der Diskussionsplattform der Zukunft.", + "previouspage": "Vorherige Seite", + "nextpage": "Nächste Seite", + "alert.success": "Erfolg", + "alert.error": "Fehler", + "alert.banned": "Gesperrt", + "alert.banned.message": "Sie wurden gerade gesperrt, Ihr Zugang ist jetzt eingeschränkt.", + "alert.unbanned": "Nicht gesperrt", + "alert.unbanned.message": "Ihre Sperre wurde aufgehoben.", + "alert.unfollow": "Du folgst %1 nicht länger!", + "alert.follow": "Du folgst nun %1!", + "users": "Benutzer", + "topics": "Themen", + "posts": "Beiträge", + "x-posts": "%1 Beiträge", + "best": "Bestbewertet", + "controversial": "Umstritten", + "votes": "Stimmen", + "x-votes": "%1 Stimmen", + "voters": "Wähler", + "upvoters": "Upvoter", + "upvoted": "Positiv bewertet", + "downvoters": "Downvoter", + "downvoted": "Negativ bewertet", + "views": "Aufrufe", + "posters": "Kommentatoren", + "reputation": "Ansehen", + "lastpost": "Letzter Beitrag", + "firstpost": "Erster Beitrag", + "read_more": "weiterlesen", + "more": "Mehr", + "none": "Nichts", + "posted_ago_by_guest": "%1 von einem Gast geschrieben", + "posted_ago_by": "%1 von %2 geschrieben", + "posted_ago": "%1 geschrieben", + "posted_in": "Verfasst in %1", + "posted_in_by": "verfasst in %1 von %2", + "posted_in_ago": "Verfasst in %1 %2", + "posted_in_ago_by": "Verfasst in %1 %2 von %3", + "user_posted_ago": "%1 schrieb %2", + "guest_posted_ago": "Gast schrieb %1", + "last_edited_by": "zuletzt editiert von %1", + "norecentposts": "Keine aktuellen Beiträge", + "norecenttopics": "Keine aktuellen Themen", + "recentposts": "Aktuelle Beiträge", + "recentips": "Zuletzt angemeldete IPs", + "moderator_tools": "Moderatorenwerkzeuge", + "online": "Online", + "away": "Abwesend", + "dnd": "Nicht stören", + "invisible": "Unsichtbar", + "offline": "Offline", + "email": "E-Mail", + "language": "Sprache", + "guest": "Gast", + "guests": "Gäste", + "former_user": "Ein ehemaliger Benutzer", + "system-user": "System", + "unknown-user": "Unbekannter Benutzer", + "updated.title": "Forum aktualisiert", + "updated.message": "Dieses Forum wurde gerade auf die neueste Version aktualisiert. Klicke hier, um die Seite neuzuladen.", + "privacy": "Privatsphäre", + "follow": "Folgen", + "unfollow": "Entfolgen", + "delete_all": "Alles löschen", + "map": "Karte", + "sessions": "Login-Sitzungen", + "ip_address": "IP-Adresse", + "enter_page_number": "Seitennummer eingeben", + "upload_file": "Datei hochladen", + "upload": "Hochladen", + "uploads": "Uploads", + "allowed-file-types": "Erlaubte Dateitypen sind %1", + "unsaved-changes": "Es gibt ungespeicherte Änderungen. Bist du dir sicher, dass du die Seite verlassen willst?", + "reconnecting-message": "Es scheint als hättest du die Verbindung zu %1 verloren, bitte warte während wir versuchen sie wieder aufzubauen.", + "play": "Play", + "cookies.message": "Diese Website verwendet Cookies, um sicherzustellen, dass du die besten Erfahrungen auf unserer Website machst.", + "cookies.accept": "Verstanden!", + "cookies.learn_more": "Erfahre mehr", + "edited": "Bearbeitet", + "disabled": "Deaktiviert", + "select": "Auswählen", + "user-search-prompt": "Gib hier etwas ein um Benutzer zu finden..." +} \ No newline at end of file diff --git a/public/language/de/groups.json b/public/language/de/groups.json new file mode 100644 index 0000000000..96804ec235 --- /dev/null +++ b/public/language/de/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Gruppen", + "view_group": "Gruppe zeigen", + "owner": "Gruppenbesitzer", + "new_group": "Neue Gruppe erstellen", + "no_groups_found": "Es sind keine Gruppen vorhanden", + "pending.accept": "Annehmen", + "pending.reject": "Abweisen", + "pending.accept_all": "Alle annehmen", + "pending.reject_all": "Alle ablehnen", + "pending.none": "Es gibt zur Zeit keine ausstehende Mitglieder", + "invited.none": "Es sind zur Zeit keine weiteren Mitglieder eingeladen", + "invited.uninvite": "Einladung zurücknehmen", + "invited.search": "Suche nach einem Benutzer um ihn in diese Gruppe aufzunehmen", + "invited.notification_title": "Du wurdest eingeladen %1 beizutreten.", + "request.notification_title": "Mitgliedsanfrage von %1.", + "request.notification_text": "%1 möchte Mitglied von %2 werden.", + "cover-save": "Speichern", + "cover-saving": "Speicherung läuft", + "details.title": "Gruppendetails", + "details.members": "Mitgliederliste", + "details.pending": "Mitglieder in Schwebe", + "details.invited": "Eingeladene Mitglieder", + "details.has_no_posts": "Die Mitglieder dieser Gruppe haben keine Beiträge verfasst.", + "details.latest_posts": "Neueste Beiträge", + "details.private": "Privat", + "details.disableJoinRequests": "Deaktiviere Beitrittsanfragen", + "details.disableLeave": "Benutzer daran hindern die Gruppe zu verlassen", + "details.grant": "Gewähre/widerrufe Besitz", + "details.kick": "Kick", + "details.kick_confirm": "Sind Sie sicher, dass Sie dieses Mitglied aus der Gruppe entfernen möchten?", + "details.add-member": "Mitglied hinzufügen", + "details.owner_options": "Gruppenadministration", + "details.group_name": "Gruppenname", + "details.member_count": "Mitgliederanzahl", + "details.creation_date": "Erstelldatum", + "details.description": "Beschreibung", + "details.member-post-cids": "Kategorie-IDs, aus denen Beiträge angezeigt werden sollen", + "details.badge_preview": "Abzeichenvorschau", + "details.change_icon": "Symbol ändern", + "details.change_label_colour": "Label-Farbe ändern", + "details.change_text_colour": "Text-Farbe ändern", + "details.badge_text": "Text für das Abzeichen", + "details.userTitleEnabled": "Abzeichen anzeigen", + "details.private_help": "Wenn aktiviert, setzt ein Gruppenbeitritt die Zustimmung eines Gruppenbesitzers voraus", + "details.hidden": "Versteckt", + "details.hidden_help": "Wenn aktiviert, wird diese Gruppe in der Gruppenliste nicht zu finden sein, und Benutzer werden manuell eingeladen werden müssen.", + "details.delete_group": "Gruppe löschen", + "details.private_system_help": "Private Gruppen wurden systemweit deaktiviert. Diese Einstellung hat keine Funktion.", + "event.updated": "Gruppendetails wurden aktualisiert", + "event.deleted": "Die Gruppe \"%1\" wurde gelöscht.", + "membership.accept-invitation": "Einladung akzeptieren", + "membership.accept.notification_title": "Du bist nun Mitglied von %1", + "membership.invitation-pending": "Einladung ausstehend", + "membership.join-group": "Gruppe beitreten", + "membership.leave-group": "Gruppe verlassen", + "membership.leave.notification_title": "%1 hat die Gruppe %2 verlassen", + "membership.reject": "Ablehnen", + "new-group.group_name": "Gruppenname:", + "upload-group-cover": "Gruppentitelbild hochladen", + "bulk-invite-instructions": "Gib eine mit Kommata getrennte Liste von Benutzernamen ein, um sie in diese Gruppe aufzunehmen", + "bulk-invite": "Mehrere einladen", + "remove_group_cover_confirm": "Bist du sicher, dass du dein Titelbild entfernen möchtest?" +} \ No newline at end of file diff --git a/public/language/de/ip-blacklist.json b/public/language/de/ip-blacklist.json new file mode 100644 index 0000000000..ee5f7572eb --- /dev/null +++ b/public/language/de/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Konfiguriere hier deine IP-Blacklist", + "description": "Gelegentlich reicht die Sperrung eines Benutzerkontos nicht aus, um abzuschrecken. In anderen Fällen ist die Beschränkung des Zugriffs auf das Forum auf eine bestimmte IP oder eine Reihe von IPs der beste Weg, ein Forum zu schützen. In diesen Szenarien kannst Du dieser Blacklist problematische IP-Adressen oder ganze CIDR-Blöcke hinzufügen, und sie werden daran gehindert, sich bei einem neuen Konto anzumelden oder ein neues Konto zu registrieren.", + "active-rules": "Aktive Regeln", + "validate": "Blacklist validieren", + "apply": "Blacklist anwenden", + "hints": "Syntax Hinweise", + "hint-1": "Pro Zeile kann eine IP-Adresse angegeben werden. Es können auch IP-Blöcke im CIDR Format (z.B. 192.168.100.0/22) hinzugefügt werden.", + "hint-2": "Du kannst Kommentare hinzufügen, indem Du die Zeilen mit dem # Symbol beginnst.", + + "validate.x-valid": "%1 von %2 Regel(n) zulässig.", + "validate.x-invalid": "Die folgenden %1 Regeln sind unzulässig:", + + "alerts.applied-success": "Blacklist angewendet", + + "analytics.blacklist-hourly": "Figur 1 – Blacklist-Treffer pro Stunde", + "analytics.blacklist-daily": " Figur 2 – Blacklist-Treffer pro Tag", + "ip-banned": "IP-Adresse gesperrt" +} \ No newline at end of file diff --git a/public/language/de/language.json b/public/language/de/language.json new file mode 100644 index 0000000000..376cb8f2c9 --- /dev/null +++ b/public/language/de/language.json @@ -0,0 +1,5 @@ +{ + "name": "Deutsch", + "code": "de", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/de/login.json b/public/language/de/login.json new file mode 100644 index 0000000000..b3c87e732d --- /dev/null +++ b/public/language/de/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Benutzername / E-Mail-Adresse", + "username": "Benutzername", + "remember_me": "Eingeloggt bleiben?", + "forgot_password": "Passwort vergessen?", + "alternative_logins": "Alternative Logins", + "failed_login_attempt": "Login fehlgeschlagen", + "login_successful": "Du hast dich erfolgreich eingeloggt!", + "dont_have_account": "Du hast noch kein Konto?", + "logged-out-due-to-inactivity": "Du wurdest aufgrund von Inaktivität aus dem Adminbereich ausgeloggt", + "caps-lock-enabled": "Die Feststelltaste ist aktiviert" +} \ No newline at end of file diff --git a/public/language/de/modules.json b/public/language/de/modules.json new file mode 100644 index 0000000000..f5ba9f93d7 --- /dev/null +++ b/public/language/de/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chatte mit", + "chat.placeholder": "Gebe hier eine Chatnachricht ein, ziehe Bilder per Drag & Drop und drücke die Eingabetaste, um sie zu senden", + "chat.scroll-up-alert": "Diese Nachrichten sind möglicherweise veraltet, klicke hier um neuere Nachrichten anzuzeigen.", + "chat.send": "Senden", + "chat.no_active": "Du hast keine aktiven Chats.", + "chat.user_typing": "%1 tippt gerade ...", + "chat.user_has_messaged_you": "%1 hat dir geschrieben.", + "chat.see_all": "Alle Chats", + "chat.mark_all_read": "Alle als gelesen markieren", + "chat.no-messages": "Bitte wähle einen Empfänger, um den jeweiligen Nachrichtenverlauf anzuzeigen.", + "chat.no-users-in-room": "In diesem Raum befinden sich keine Benutzer.", + "chat.recent-chats": "Aktuelle Chats", + "chat.contacts": "Kontakte", + "chat.message-history": "Nachrichtenverlauf", + "chat.message-deleted": "Nachricht gelöscht", + "chat.options": "Chat-Optionen", + "chat.pop-out": "Chat als Pop-out anzeigen", + "chat.minimize": "Minimieren", + "chat.maximize": "Maximieren", + "chat.seven_days": "7 Tage", + "chat.thirty_days": "30 Tage", + "chat.three_months": "3 Monate", + "chat.delete_message_confirm": "Bist du sicher, dass du diese Nachricht löschen möchtest?", + "chat.retrieving-users": "Rufe Benutzer ab", + "chat.manage-room": "Chat-Room managen", + "chat.add-user-help": "Suche hier nach Usern. Auswählen fügt den User hinzu. Der neue User wird nicht in der Lage sein Chat Nachrichten zu lesen, die geschrieben wurden bevor er der Konversation hinzugefügt wurde. Ausschließlich Raumbesitzer () können User von Chat Rooms entfernen.", + "chat.confirm-chat-with-dnd-user": "Dieser Benutzer hat seinen Status auf DnD (Bitte nicht stören) gesetzt. Möchtest du dennoch mit ihm chatten?", + "chat.rename-room": "Raum umbenennen", + "chat.rename-placeholder": "Gib deinen Chatraumnamen hier ein", + "chat.rename-help": "Den Namen des Chatraums den du hier setzt, wird für alle Teilnehmer sichtbar sein.", + "chat.leave": "Chat verlassen", + "chat.leave-prompt": "Bist du sicher, dass du diesen Chat verlassen willst?", + "chat.leave-help": "Den Chat zu verlassen wird dich von weiterem Schriftverkehr in diesem Chat entfernen. Solltest du in der Zukunft wieder hinzugefügt werden, würdest du keinen Chatverlauf sehen können, der in der Zwischenzeit geschrieben wurde.", + "chat.in-room": "In diesem Chat-Room", + "chat.kick": "Rauswerfen", + "chat.show-ip": "IP anzeigen", + "chat.owner": "Raumbesitzer", + "chat.system.user-join": "%1 ist dem Raum beigetreten", + "chat.system.user-leave": "%1 hat den Raum verlassen", + "chat.system.room-rename": "%2 hat den Raum umbenannt: %1", + "composer.compose": "Verfassen", + "composer.show_preview": "Vorschau zeigen", + "composer.hide_preview": "Vorschau ausblenden", + "composer.user_said_in": "%1 sagte in %2:", + "composer.user_said": "%1 sagte:", + "composer.discard": "Bist du sicher, dass du diesen Beitrag verwerfen möchtest?", + "composer.submit_and_lock": "Einreichen und Sperren", + "composer.toggle_dropdown": "Menu aus-/einblenden", + "composer.uploading": "Lade %1 hoch", + "composer.formatting.bold": "Fett", + "composer.formatting.italic": "Kursiv", + "composer.formatting.list": "Liste", + "composer.formatting.strikethrough": "Durchstreichen", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Bildlink", + "composer.upload-picture": "Bild hochladen", + "composer.upload-file": "Datei hochladen", + "composer.zen_mode": "Zen Modus", + "composer.select_category": "Wähle eine Kategorie", + "composer.textarea.placeholder": "Schreibe hier deinen Beitrag, füge Bilder mit Drag and Drop hinzu", + "composer.schedule-for": "Thema einplanen für", + "composer.schedule-date": "Datum", + "composer.schedule-time": "Zeit", + "composer.cancel-scheduling": "Planung abbrechen", + "composer.set-schedule-date": "Datum einstellen", + "bootbox.ok": "OK", + "bootbox.cancel": "Abbrechen", + "bootbox.confirm": "Bestätigen", + "bootbox.submit": "Absenden", + "bootbox.send": "Senden", + "cover.dragging_title": "Titelbildpositionierung", + "cover.dragging_message": "Ziehe das Titelbild an die gewünschte Position und klicke auf \"Speichern\"", + "cover.saved": "Titelbild und -position gespeichert", + "thumbs.modal.title": "Themen-Vorschaubilder verwalten", + "thumbs.modal.no-thumbs": "Keine Vorschaubilder gefunden.", + "thumbs.modal.resize-note": "Hinweis: Dieses Forum ist so konfiguriert, dass Vorschaubilder von Themen auf eine maximale Breite von %1px verkleinert werden", + "thumbs.modal.add": "Vorschaubild hinzufügen", + "thumbs.modal.remove": "Vorschaubild entfernen", + "thumbs.modal.confirm-remove": "Bist Du dir sicher, dass du das Vorschaubild entfernen willst?" +} \ No newline at end of file diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json new file mode 100644 index 0000000000..f2114912bd --- /dev/null +++ b/public/language/de/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Benachrichtigungen", + "no_notifs": "Keine neuen Benachrichtigungen", + "see_all": "Alle Benachrichtigungen", + "mark_all_read": "Alle als gelesen markieren", + "back_to_home": "Zurück zu %1", + "outgoing_link": "Externer Link", + "outgoing_link_message": "Du verlässt nun %1", + "continue_to": "Fortfahren zu %1", + "return_to": "Kehre zurück zu %1", + "new_notification": "Du hast eine neue Benachrichtigung", + "you_have_unread_notifications": "Du hast ungelesene Benachrichtigungen.", + "all": "Alle", + "topics": "Themen", + "replies": "Antworten", + "chat": "Chats", + "group-chat": "Gruppenchats", + "follows": "Folgt", + "upvote": "Positive Bewertungen", + "new-flags": "Neue Markierungen", + "my-flags": "Mir zugewiesene Markierungen", + "bans": "Verbannungen", + "new_message_from": "Neue Nachricht von %1", + "upvoted_your_post_in": "%1 hat deinen Beitrag in %2 positiv bewertet.", + "upvoted_your_post_in_dual": "%1 und %2 haben deinen Beitrag in %3 positiv bewertet.", + "upvoted_your_post_in_multiple": "%1 und %2 andere Nutzer haben deinen Beitrag in %3 positiv bewertet.", + "moved_your_post": "%1 hat deinen Beitrag nach %2 verschoben.", + "moved_your_topic": "%1 hat %2 verschoben.", + "user_flagged_post_in": "%1 hat einen Beitrag in %2 gemeldet", + "user_flagged_post_in_dual": "%1 und %2 haben einen Beitrag in %3 gemeldet", + "user_flagged_post_in_multiple": "%1 und %2 andere Nutzer haben einen Beitrag in %3 gemeldet", + "user_flagged_user": "%1 meldete ein Nutzerprofil (%2)", + "user_flagged_user_dual": "%1 und %2 meldeten ein Nutzerprofil (%3)", + "user_flagged_user_multiple": "%1 und %2 weitere meldeten ein Nutzerprofil (%3)", + "user_posted_to": "%1 hat auf %2 geantwortet.", + "user_posted_to_dual": "%1 und %2 haben auf %3 geantwortet.", + "user_posted_to_multiple": "%1 und %2 andere Nutzer haben auf %3 geantwortet.", + "user_posted_topic": "%1 hat ein neues Thema erstellt: %2", + "user_edited_post": "%1 hat einen Post in %2 bearbeitet", + "user_started_following_you": "%1 folgt dir jetzt.", + "user_started_following_you_dual": "%1 und %2 folgen dir jetzt.", + "user_started_following_you_multiple": "%1 und %2 andere Nutzer folgen dir jetzt.", + "new_register": "%1 hat eine Registrationsanfrage geschickt.", + "new_register_multiple": "Es erwarten %1 Registrierungsanfragen eine Überprüfung.", + "flag_assigned_to_you": "Markierung %1 wurde Ihnen zugewiesen", + "post_awaiting_review": "Beitrag noch nicht Überprüft", + "profile-exported": "%1 Profil exportiert, klicke zum downloaden", + "posts-exported": "%1 Posts exportiert, klicke zum downloaden", + "uploads-exported": "%1 Uploads exportiert, klicke zum downloaden", + "users-csv-exported": "Benutzer im CSV-Format exportiert, zum Herunterladen klicken", + "post-queue-accepted": "Ihr Post in der Warteschlange wurde akzeptiert. Klicken Sie hier, um Ihren Beitrag anzuzeigen.", + "post-queue-rejected": "Ihr Post in der Warteschlange wurde abgelehnt.", + "post-queue-notify": "Post in der Warteschlange hat eine Benachrichtigung erhalten:
„%1“", + "email-confirmed": "E-Mail bestätigt", + "email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.", + "email-confirm-error-message": "Es gab ein Problem bei der Validierung Ihrer E-Mail-Adresse. Möglicherweise ist der Code ungültig oder abgelaufen.", + "email-confirm-sent": "Bestätigungs-E-Mail gesendet.", + "none": "Nichts", + "notification_only": "Nur Benachrichtigungen", + "email_only": "Nur Emails", + "notification_and_email": "Benachrichtigungen & Emails", + "notificationType_upvote": "Wenn jemand deinen beitrag positiv bewertet", + "notificationType_new-topic": "Wenn jemand, dem du folgst, einen Beitrag erstellt", + "notificationType_new-reply": "Wenn es eine neue Antwort auf ein Thema das du beobachtest gibt", + "notificationType_post-edit": "Wenn ein Post bearbeitet wurde, in einem Thema welches du beobachtest", + "notificationType_follow": "Wenn dir jemand neues folgt", + "notificationType_new-chat": "Wenn du eine Chat Nachricht erhältst", + "notificationType_new-group-chat": "Wenn Du eine Gruppen-Chat-Nachricht erhalten hast", + "notificationType_group-invite": "Wenn du eine Gruppeneinladung erhältst", + "notificationType_group-leave": "Wenn ein Benutzer Ihre Gruppe verlässt", + "notificationType_group-request-membership": "Wenn jemand einer Gruppe beitreten möchte, die dir gehört", + "notificationType_new-register": "Wenn jemand der Registrierungswarteschlange hinzugefügt wird", + "notificationType_post-queue": "Wenn ein neuer Beitrag eingereiht wird", + "notificationType_new-post-flag": "Wenn ein Beitrag gemeldet wird", + "notificationType_new-user-flag": "Wenn ein Benutzer gemeldet wird" +} \ No newline at end of file diff --git a/public/language/de/pages.json b/public/language/de/pages.json new file mode 100644 index 0000000000..2dcaed1670 --- /dev/null +++ b/public/language/de/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Übersicht", + "unread": "Ungelesene Themen", + "popular-day": "Beliebte Themen von Heute", + "popular-week": "Beliebte Themen dieser Woche", + "popular-month": "Beliebte Themen dieses Monats", + "popular-alltime": "Beliebteste Themen", + "recent": "Neueste Themen", + "top-day": "Bestbewerteste Themen von Heute", + "top-week": "Bestbewerteste Themen dieser Woche", + "top-month": "Bestbewerteste Themen dieses Monats", + "top-alltime": "Bestbewerteste Themen", + "moderator-tools": "Moderator-Werkzeuge", + "flagged-content": "Gemeldeter Inhalt", + "ip-blacklist": "IP Blacklist", + "post-queue": "Beitragswarteschlange", + "users/online": "Benutzer online", + "users/latest": "Neuste Benutzer", + "users/sort-posts": "Benutzer mit den meisten Beiträgen", + "users/sort-reputation": "Benutzer mit dem höchsten Ansehen", + "users/banned": "Gesperrte Benutzer", + "users/most-flags": "Am meisten gemeldete Benutzer", + "users/search": "Benutzer Suche", + "notifications": "Benachrichtigungen", + "tags": "Markierungen", + "tag": "Unter \"%1\" getaggte Themen", + "register": "Einen Benutzer erstellen", + "registration-complete": "Registration abgeschlossen", + "login": "Einloggen", + "reset": "Passwort zurücksetzen", + "categories": "Kategorien", + "groups": "Gruppen", + "group": "%1 Gruppe", + "chats": "Chats", + "chat": "Chatte mit %1", + "flags": "Meldungen", + "flag-details": "%1 Details melden", + "account/edit": "Bearbeite %1", + "account/edit/password": "Bearbeite Passwort von \"%1\"", + "account/edit/username": "Bearbeite Benutzernamen von \"%1\"", + "account/edit/email": "Bearbeite E-Mail von \"%1\"", + "account/info": "Kontoinformationen", + "account/following": "Nutzer, denen %1 folgt", + "account/followers": "Nutzer, die %1 folgen", + "account/posts": "Beiträge von %1", + "account/latest-posts": "Neuster Beitrag von %1", + "account/topics": "Von %1 verfasste Themen", + "account/groups": "Gruppen von %1", + "account/watched_categories": "Beobachtete Kategorien von %1", + "account/bookmarks": "Lesezeichen von %1", + "account/settings": "Benutzer-Einstellungen", + "account/watched": "Von %1 beobachtete Themen", + "account/ignored": "Ignorierte Themen von %1", + "account/upvoted": "Von %1 positiv bewertete Beiträge", + "account/downvoted": "Von %1 negativ bewertete Beiträge", + "account/best": "Bestbewertete Beiträge von %1", + "account/controversial": "Kontroverse Beiträge von %1", + "account/blocks": "Für %1 geblockte Benutzer", + "account/uploads": "Uploads von %1", + "account/sessions": "Login-Sitzungen", + "confirm": "E-Mail bestätigt", + "maintenance.text": "%1 befindet sich derzeit in der Wartung. Bitte komm später wieder.", + "maintenance.messageIntro": "Zusätzlich hat der Administrator diese Nachricht hinterlassen:", + "throttled.text": "%1 ist momentan aufgrund von Überlastung nicht verfügbar. Bitte komm später wieder." +} \ No newline at end of file diff --git a/public/language/de/post-queue.json b/public/language/de/post-queue.json new file mode 100644 index 0000000000..3c25e7fefb --- /dev/null +++ b/public/language/de/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Beitragswarteschlange", + "description": "Es gibt keine Beiträge in der Warteschlange.
Um dieses Feature zu aktivieren, gehe auf Einstellungen → Posts → Beitragswarteschlange und aktiviere Beitragswarteschlange.", + "user": "Benutzer", + "category": "Kategorie", + "title": "Titel", + "content": "Inhalt", + "posted": "Gepostet", + "reply-to": "Auf \"%1\" antworten", + "content-editable": "Inhalt zum Bearbeiten anklicken", + "category-editable": "Kategorie zum Bearbeiten anklicken", + "title-editable": "Titel zum Bearbeiten anklicken", + "reply": "Antworten", + "topic": "Thema", + "accept": "Annehmen", + "reject": "Ablehnen", + "remove": "Entfernen", + "notify": "Benachrichtigen", + "notify-user": "Benutzer benachrichtigen", + "confirm-reject": "Möchtest Du diesen Beitrag ablehnen?", + "bulk-actions": "Massenaktionen", + "accept-all": "Alle akzeptieren", + "accept-selected": "Ausgewählte akzeptieren", + "reject-all": "Alle ablehnen", + "reject-all-confirm": "Möchtest Du alle Beiträge ablehnen?", + "reject-selected": "Ausgewählte ablehnen", + "reject-selected-confirm": "Möchtest Du %1 ausgewählte Beiträge ablehnen?", + "bulk-accept-success": "%1 Beiträge akzeptiert", + "bulk-reject-success": "%1 Beiträge abgelehnt" +} \ No newline at end of file diff --git a/public/language/de/recent.json b/public/language/de/recent.json new file mode 100644 index 0000000000..d9fa50b996 --- /dev/null +++ b/public/language/de/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Aktuell", + "day": "Tag", + "week": "Woche", + "month": "Monat", + "year": "Jahr", + "alltime": "Gesamter Zeitraum", + "no_recent_topics": "Es gibt keine aktuellen Themen.", + "no_popular_topics": "Es gibt keine beliebten Themen.", + "there-is-a-new-topic": "Es gibt ein neues Thema.", + "there-is-a-new-topic-and-a-new-post": "Es gibt ein neues Thema und einen neuen Beitrag.", + "there-is-a-new-topic-and-new-posts": "Es gibt ein neues Thema und %1 neue Beiträge.", + "there-are-new-topics": "Es gibt %1 neue Themen.", + "there-are-new-topics-and-a-new-post": "Es gibt %1 neue Themen und einen neuen Beitrag.", + "there-are-new-topics-and-new-posts": "Es gibt %1 neue Themen und %2 neue Beiträge.", + "there-is-a-new-post": "Es gibt einen neuen Beitrag.", + "there-are-new-posts": "Es gibt %1 neue Beiträge.", + "click-here-to-reload": "Zum aktualisieren hier klicken." +} \ No newline at end of file diff --git a/public/language/de/register.json b/public/language/de/register.json new file mode 100644 index 0000000000..f38e2031ce --- /dev/null +++ b/public/language/de/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registrieren", + "cancel_registration": "Registrierungsvorgang abbrechen", + "help.email": "Deine E-Mail Adresse ist standardmäßig nicht öffentlich sichtbar.", + "help.username_restrictions": "Einen einmaligen Benutzernamen. %1-%2 Zeichen. Andere Benutzer können dich mit @Benutzername anschreiben.", + "help.minimum_password_length": "Dein Passwort muss mindestens %1 Zeichen lang sein.", + "email_address": "E-Mail-Adresse", + "email_address_placeholder": "E-Mail Adresse eingeben", + "username": "Benutzername", + "username_placeholder": "Benutzernamen eingeben", + "password": "Passwort", + "password_placeholder": "Passwort eingeben", + "confirm_password": "Passwort bestätigen", + "confirm_password_placeholder": "Passwort zur Bestätigung erneut eingeben", + "register_now_button": "Jetzt registrieren", + "alternative_registration": "Alternative Registrierung", + "terms_of_use": "Nutzungsbedingungen", + "agree_to_terms_of_use": "Ich stimme den Nutzungsbedingungen zu", + "terms_of_use_error": "Du musst den Nutzungsbedingungen zustimmen", + "registration-added-to-queue": "Deine Registration wurde abgeschickt. Du wirst eine E-Mail erhalten, sobald sie von einem Administrator akzeptiert wird.", + "registration-queue-average-time": "Unsere durchschnittliche Zeit für die Genehmigung von Mitgliedschaften beträgt %1 Stunden %2 Minuten.", + "registration-queue-auto-approve-time": "Ihre Mitgliedschaft in diesem Forum wird in bis zu %1 Stunden vollständig aktiviert sein.", + "interstitial.intro": "Wir benötigen einige zusätzliche Informationen, um Ihr Konto zu aktualisieren…", + "interstitial.intro-new": "Wir benötigen einige zusätzliche Informationen, bevor wir Ihr Konto erstellen können…", + "interstitial.errors-found": "Bitte überprüfen Sie die eingegebenen Informationen:", + "gdpr_agree_data": "Ich stimme der Sammlung und Verarbeitung meiner Persönlichen Daten auf dieser Website zu.", + "gdpr_agree_email": "Ich bin damit einverstanden, dass ich Informations und Benachrichtigungs-E-Mails von dieser Website erhalte.", + "gdpr_consent_denied": "Du musst zustimmen, dass diese Seite deine Daten sammeln und verarbeiten darf, und dir Emails senden darf.", + "invite.error-admin-only": "Die direkte Benutzerregistrierung wurde deaktiviert. Bitte kontaktieren Sie einen Administrator für weitere Details.", + "invite.error-invite-only": "Die direkte Benutzerregistrierung wurde deaktiviert. Sie müssen von einem bestehenden Benutzer eingeladen werden, um Zugang zu diesem Forum zu erhalten.", + "invite.error-invalid-data": "Die erhaltenen Registrierungsdaten stimmen nicht mit unseren Aufzeichnungen überein. Bitte kontaktieren Sie einen Administrator für weitere Details" +} \ No newline at end of file diff --git a/public/language/de/reset_password.json b/public/language/de/reset_password.json new file mode 100644 index 0000000000..efbe70f757 --- /dev/null +++ b/public/language/de/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Passwort zurücksetzen", + "update_password": "Password aktualisieren", + "password_changed.title": "Passwort geändert", + "password_changed.message": "

Passwort erfolgreich zurückgesetzt. Bitte logge dich erneut ein.", + "wrong_reset_code.title": "Falscher Reset-Code", + "wrong_reset_code.message": "Der empfangene Reset-Code war falsch. Bitte versuche es erneut oder fordere einen neuen Code an.", + "new_password": "Neues Passwort", + "repeat_password": "Passwort bestätigen", + "changing_password": "Passwort ändern", + "enter_email": "Bitte gebe deine E-Mail-Adresse ein und wir senden dir eine E-Mail mit Anweisungen zum Zurücksetzen deines Kontos.", + "enter_email_address": "E-Mail Adresse eingeben", + "password_reset_sent": "Wenn die angegebene Adresse einem bestehenden Benutzerkonto entspricht, wurde eine E-Mail zum Zurücksetzen des Passworts gesendet. Bitte beachte, dass nur eine E-Mail pro Minute versendet wird.", + "invalid_email": "Ungültige E-Mail / Adresse existiert nicht!", + "password_too_short": "Das eingegebene Passwort ist zu kurz, bitte wähle ein anderes Passwort.", + "passwords_do_not_match": "Die eingegebenen Passwörter stimmen nicht überein.", + "password_expired": "Dein Passwort ist abgelaufen, bitte wähle ein neues Passwort" +} \ No newline at end of file diff --git a/public/language/de/search.json b/public/language/de/search.json new file mode 100644 index 0000000000..3b9ae90b56 --- /dev/null +++ b/public/language/de/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 Ergebnis(se) stimmen mit \"%2\" überein, (%3 Sekunden)", + "no-matches": "Keine Ergebnisse gefunden", + "advanced-search": "Erweiterte Suche", + "in": "In", + "titles": "Titel", + "titles-posts": "Titel und Beiträge", + "match-words": "Übereinstimmende Worte", + "all": "Alle", + "any": "Alles", + "posted-by": "Geschrieben von", + "in-categories": "In Kategorien", + "search-child-categories": "Suche in Unterkategorien", + "has-tags": "Hat Markierungen", + "reply-count": "Anzahl Antworten", + "at-least": "Mindestens", + "at-most": "Höchstens", + "relevance": "Relevanz", + "post-time": "Verfaßt am", + "votes": "Stimmen", + "newer-than": "Neuer als", + "older-than": "Älter als", + "any-date": "Beliebiger Zeitpunkt", + "yesterday": "Gestern", + "one-week": "Eine Woche", + "two-weeks": "Zwei Wochen", + "one-month": "Ein Monat", + "three-months": "Drei Monate", + "six-months": "Sechs Monate", + "one-year": "Ein Jahr", + "sort-by": "Sortieren nach", + "last-reply-time": "Zeitpunkt der letzten Antwort", + "topic-title": "Thementitel", + "topic-votes": "Themenstimmen", + "number-of-replies": "Anzahl an Antworten", + "number-of-views": "Anzahl der Aufrufe", + "topic-start-date": "Erstelldatum des Themas", + "username": "Benutzername", + "category": "Kategorie", + "descending": "In absteigender Reihenfolge", + "ascending": "In aufsteigender Reihenfolge", + "save-preferences": "Einstellungen speichern", + "clear-preferences": "Einstellungen löschen", + "search-preferences-saved": "Sucheinstellungen gespeichert", + "search-preferences-cleared": "Sucheinstellungen gelöscht", + "show-results-as": "Ergebnisse anzeigen als", + "see-more-results": "Weitere Ergebnisse anzeigen (%1)", + "search-in-category": "Suche in \"%1\"" +} \ No newline at end of file diff --git a/public/language/de/success.json b/public/language/de/success.json new file mode 100644 index 0000000000..78445d1334 --- /dev/null +++ b/public/language/de/success.json @@ -0,0 +1,7 @@ +{ + "success": "Erfolgreich", + "topic-post": "Beitrag erfolgreich erstellt.", + "post-queued": "Dein Beitrag wird zur Genehmigung in die Warteschlange gestellt. Du erhälst eine Benachrichtigung, wenn es akzeptiert oder abgelehnt wird.", + "authentication-successful": "Authentifizierung erfolgreich!", + "settings-saved": "Einstellungen gespeichert!" +} \ No newline at end of file diff --git a/public/language/de/tags.json b/public/language/de/tags.json new file mode 100644 index 0000000000..b4f3f56d71 --- /dev/null +++ b/public/language/de/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Es gibt keine Themen mit diesem Schlagwort.", + "tags": "Schlagworte", + "enter_tags_here": "Hier Schlagworte eingeben. Jeweils %1 bis %2 Zeichen.", + "enter_tags_here_short": "Schlagworte eingeben...", + "no_tags": "Es gibt noch keine Schlagworte.", + "select_tags": "Schlagworte auswählen" +} \ No newline at end of file diff --git a/public/language/de/top.json b/public/language/de/top.json new file mode 100644 index 0000000000..5cbcfc49de --- /dev/null +++ b/public/language/de/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top-Themen", + "no_top_topics": "Keine Top-Themen" +} \ No newline at end of file diff --git a/public/language/de/topic.json b/public/language/de/topic.json new file mode 100644 index 0000000000..3702f62317 --- /dev/null +++ b/public/language/de/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Thema", + "title": "Titel", + "no_topics_found": "Keine passenden Themen gefunden!", + "no_posts_found": "Keine Beiträge gefunden!", + "post_is_deleted": "Dieser Beitrag wurde gelöscht!", + "topic_is_deleted": "Dieses Thema wurde gelöscht!", + "profile": "Profil", + "posted_by": "Verfasst von %1", + "posted_by_guest": "Verfasst von einem Gast", + "chat": "Chat", + "notify_me": "Erhalte eine Benachrichtigung bei neuen Antworten zu diesem Thema.", + "quote": "Zitieren", + "reply": "Antworten", + "replies_to_this_post": "%1 Antworten", + "one_reply_to_this_post": "1 Antwort", + "last_reply_time": "Letzte Antwort", + "reply-as-topic": "In einem neuen Thema antworten", + "guest-login-reply": "Anmelden zum Antworten", + "login-to-view": "🔒 Zum ansehen einloggen", + "edit": "Bearbeiten", + "delete": "Löschen", + "delete-event": "Ereignisse löschen", + "delete-event-confirm": "Bist du sicher, dass du dieses Ereignis löschen willst?", + "purge": "Endgültig löschen", + "restore": "Wiederherstellen", + "move": "Verschieben", + "change-owner": "Besitzer ändern", + "fork": "Aufspalten", + "link": "Link", + "share": "Teilen", + "tools": "Werkzeuge", + "locked": "Gesperrt", + "pinned": "Angeheftet", + "pinned-with-expiry": "Angepinnt bis %1", + "scheduled": "Geplant", + "moved": "Verschoben", + "moved-from": "Verschoben von %1", + "copy-ip": "IP-Adresse Kopieren", + "ban-ip": "IP-Adresse bannen", + "view-history": "Verlauf bearbeiten", + "locked-by": "Gesperrt von", + "unlocked-by": "Entsperrt von", + "pinned-by": "Angeheftet von", + "unpinned-by": "Losgelöst von", + "deleted-by": "Gelöscht von", + "restored-by": "Wiederhergestellt von", + "moved-from-by": "Von %1 verschoben durch", + "queued-by": "Beitrag zur Genehmigung in die Warteschlange eingereiht →", + "backlink": "Referenziert von", + "forked-by": "Geteilt von", + "bookmark_instructions": "Klicke hier, um zum letzten gelesenen Beitrag des Themas zurückzukehren.", + "flag-post": "Diesen Post melden", + "flag-user": "Diesen Benutzer melden", + "already-flagged": "Bereits gemeldet", + "view-flag-report": "Meldungs-Report anzeigen", + "resolve-flag": "Als Gelöst markiert", + "merged_message": "Diese Thema wurde mit %2 fusioniert", + "deleted_message": "Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.", + "following_topic.message": "Du erhältst nun eine Benachrichtigung, wenn jemand einen Beitrag zu diesem Thema verfasst.", + "not_following_topic.message": "Ungelesene Beiträge in diesem Thema werden angezeigt, aber du erhältst keine Benachrichtigung wenn jemand einen Beitrag zu diesem Thema verfasst.", + "ignoring_topic.message": "Ungelesene Beiträge in diesem Thema werden nicht mehr angezeigt. Du erhältst eine Benachrichtigung, wenn du in diesem Thema erwähnt wirst oder deine Beiträge positiv bewertet werden.", + "login_to_subscribe": "Bitte registrieren oder einloggen um dieses Thema zu abonnieren", + "markAsUnreadForAll.success": "Thema für alle als ungelesen markiert.", + "mark_unread": "Als ungelesen markieren", + "mark_unread.success": "Thema als ungelesen markiert.", + "watch": "Beobachten", + "unwatch": "Nicht mehr beobachten", + "watch.title": "Bei neuen Antworten benachrichtigen", + "unwatch.title": "Dieses Thema nicht mehr beobachten", + "share_this_post": "Diesen Beitrag teilen", + "watching": "Beobachtet", + "not-watching": "Nicht beobachtet", + "ignoring": "Ignoriert", + "watching.description": "Benachrichtigung bei neuen Beiträgen.
Ungelesen Beiträge anzeigen.", + "not-watching.description": "Keine Benachrichtigung bei neuen Beiträgen.
Ungelesen Beiträge anzeigen wenn die Kategorie nicht ignoriert wird.", + "ignoring.description": "Keine Benachrichtigung bei neuen Beiträgen.
Ungelesene Beiträge nicht anzeigen.", + "thread_tools.title": "Themen-Werkzeuge", + "thread_tools.markAsUnreadForAll": "Für alle als ungelesen markieren", + "thread_tools.pin": "Thema anheften", + "thread_tools.unpin": "Thema nicht mehr anheften", + "thread_tools.lock": "Thema schließen", + "thread_tools.unlock": "Thema öffnen", + "thread_tools.move": "Thema verschieben", + "thread_tools.move-posts": "Beiträge verschieben", + "thread_tools.move_all": "Alle verschieben", + "thread_tools.change_owner": "Besitzer ändern", + "thread_tools.select_category": "Kategorie auswählen", + "thread_tools.fork": "Thema aufspalten", + "thread_tools.delete": "Thema löschen", + "thread_tools.delete-posts": "Beiträge entfernen", + "thread_tools.delete_confirm": "Bist du sicher, dass du dieses Thema löschen möchtest?", + "thread_tools.restore": "Thema wiederherstellen", + "thread_tools.restore_confirm": "Bist du sicher, dass du dieses Thema wiederherstellen möchtest?", + "thread_tools.purge": "Thema endgültig löschen", + "thread_tools.purge_confirm": "Bist du sicher, dass du dieses Thema endgültig löschen möchtest?", + "thread_tools.merge_topics": "Themen vereinen", + "thread_tools.merge": "Vereinen", + "topic_move_success": "Dieses Thema wird in Kürze nach \"%1\" verschoben. Klicken Sie hier, um den Vorgang rückgängig zu machen.", + "topic_move_multiple_success": "Diese Themen werden in Kürze nach \"%1\" verschoben. Klicken Sie hier, um den Vorgang rückgängig zu machen.", + "topic_move_all_success": "Alle Themen werden in Kürze nach \"%1\" verschoben. Klicken Sie hier, um den Vorgang rückgängig zu machen.", + "topic_move_undone": "Thema verschieben zurückgenommen", + "topic_move_posts_success": "Beiträge werden in Kürze verschoben. Klicken Sie hier, um den Vorgang rückgängig zu machen.", + "topic_move_posts_undone": "Beitragsverschiebung rückgängig machen", + "post_delete_confirm": "Sind Sie sicher, dass Sie diesen Beitrag löschen möchten?", + "post_restore_confirm": "Sind Sie sicher, dass Sie diesen Beitrag wiederherstellen möchten?", + "post_purge_confirm": "Sind Sie sicher, dass Sie diesen Beitrag endgültig löschen möchten?", + "pin-modal-expiry": "Ablaufdatum", + "pin-modal-help": "Optional können Sie hier ein Ablaufdatum für das gepinnte Thema (die gepinnten Themen) festlegen. Alternativ können Sie dieses Feld leer lassen, damit das Thema fixiert bleibt, bis es manuell gelöst wird.", + "load_categories": "Kategorien laden", + "confirm_move": "Verschieben", + "confirm_fork": "Aufspalten", + "bookmark": "Lesezeichen", + "bookmarks": "Lesezeichen", + "bookmarks.has_no_bookmarks": "Du hast noch keine Beiträge mit Lesezeichen markiert.", + "copy-permalink": "Permalink kopieren", + "loading_more_posts": "Lade mehr Beiträge", + "move_topic": "Thema verschieben", + "move_topics": "Themen verschieben", + "move_post": "Beitrag verschieben", + "post_moved": "Beitrag wurde verschoben!", + "fork_topic": "Thema aufspalten", + "enter-new-topic-title": "Neuen Thementitel eingeben", + "fork_topic_instruction": "Klicke auf die Beiträge, die abgespalten werden sollen", + "fork_no_pids": "Keine Beiträge ausgewählt!", + "no-posts-selected": "Keine Beiträge ausgewählt!", + "x-posts-selected": "%1 Beitrag(Beiträge) ausgewählt", + "x-posts-will-be-moved-to-y": "%1 Beitrag(Beiträge) werden nach \"%2\" verschoben", + "fork_pid_count": "%1 Beiträge ausgewählt", + "fork_success": "Thema erfolgreich aufgespalten! Klicke hier, um zum abgespaltenen Thema zu gelangen.", + "delete_posts_instruction": "Wähle die zu löschenden Beiträge aus", + "merge_topics_instruction": "Klicke auf die Themen, die fusioniert werden sollen oder suche nach ihnen", + "merge-topic-list-title": "Liste der zu fusionierenden Themen", + "merge-options": "Fusionseinstellungen", + "merge-select-main-topic": "Wähle das Hauptthema aus", + "merge-new-title-for-topic": "Neuer Titel für das Thema", + "topic-id": "Themen-ID", + "move_posts_instruction": "Klicken Sie auf die Beiträge, die Sie verschieben möchten, und geben Sie dann eine Themen-ID ein oder gehen Sie zum Zielthema", + "change_owner_instruction": "Klicke auf die Beiträge, die einem anderen Benutzer zugeordnet werden sollen", + "composer.title_placeholder": "Hier den Titel des Themas eingeben...", + "composer.handle_placeholder": "Gib deinen Namen/Nick hier ein", + "composer.discard": "Verwerfen", + "composer.submit": "Absenden", + "composer.additional-options": "Zusätzliche Optionen", + "composer.schedule": "Zeitplan", + "composer.replying_to": "Antworte auf %1", + "composer.new_topic": "Neues Thema", + "composer.editing": "Bearbeitung", + "composer.uploading": "Lade hoch...", + "composer.thumb_url_label": "Vorschaubild-URL hier einfügen", + "composer.thumb_title": "Vorschaubild zu diesem Thema hinzufügen", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Oder eine Datei hochladen", + "composer.thumb_remove": "Felder leeren", + "composer.drag_and_drop_images": "Bilder hierher ziehen", + "more_users_and_guests": "%1 weitere(r) Nutzer und %2 Gäste", + "more_users": "%1 weitere(r) Nutzer", + "more_guests": "%1 weitere Gäste", + "users_and_others": "%1 und %2 andere", + "sort_by": "Sortieren nach", + "oldest_to_newest": "Älteste zuerst", + "newest_to_oldest": "Neuste zuerst", + "most_votes": "Meiste Stimmen", + "most_posts": "Meiste Beiträge", + "most_views": "Die meisten Ansichten", + "stale.title": "Stattdessen ein neues Thema erstellen?", + "stale.warning": "Das Thema auf das du antworten willst ist ziemlich alt. Möchtest du stattdessen ein neues Thema erstellen und auf dieses in deiner Antwort hinweisen?", + "stale.create": "Ein neues Thema erstellen", + "stale.reply_anyway": "Auf dieses Thema trotzdem antworten", + "link_back": "Re: [%1](%2)", + "diffs.title": "Beitragsänderungsverlauf", + "diffs.description": "Dieser Beitrag hat %1 Revisionen. Klicke unten auf eine dieser Revisionen um den Inhalt zu diesem Zeitpunkt zu sehen.", + "diffs.no-revisions-description": "Dieser Beitrag hat %1 Revisionen.", + "diffs.current-revision": "Aktuelle Revision", + "diffs.original-revision": "Ursprüngliche Revision", + "diffs.restore": "Diese Revision wiederherstellen", + "diffs.restore-description": "Eine neue Überarbeitung wird dem Beitragsänderungsverlauf hinzugefügt, nach der Wiederherstellung.", + "diffs.post-restored": "Post erfolgreich auf eine frühere Version zurückgesetzt", + "diffs.delete": "Löschen Sie diese Überarbeitung", + "diffs.deleted": "Überarbeitung gelöscht", + "timeago_later": "%1 später", + "timeago_earlier": "%1 früher", + "first-post": "Erster Beitrag", + "last-post": "Letzter Beitrag", + "go-to-my-next-post": "Zu meinem nächsten Beitrag gehen", + "no-more-next-post": "Du hast keine weiteren Beiträge zu diesem Thema", + "post-quick-reply": "Beitrag schnell Beantworten" +} \ No newline at end of file diff --git a/public/language/de/unread.json b/public/language/de/unread.json new file mode 100644 index 0000000000..1d32c7d727 --- /dev/null +++ b/public/language/de/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Ungelesen", + "no_unread_topics": "Es gibt keine ungelesenen Themen.", + "load_more": "Mehr laden", + "mark_as_read": "Als gelesen markieren", + "selected": "Ausgewählt", + "all": "Alle", + "all_categories": "Alle Kategorien", + "topics_marked_as_read.success": "Themen als gelesen markiert!", + "all-topics": "Alle Themen", + "new-topics": "Neue Themen", + "watched-topics": "Beobachtete Themen", + "unreplied-topics": "Unbeantwortete Themen", + "multiple-categories-selected": "Mehrere ausgewählt" +} \ No newline at end of file diff --git a/public/language/de/uploads.json b/public/language/de/uploads.json new file mode 100644 index 0000000000..8321cdc3d5 --- /dev/null +++ b/public/language/de/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Hochladen der Datei...", + "select-file-to-upload": "Eine Datei zum hochladen auswählen!", + "upload-success": "Datei erfolgreich hochgeladen!", + "maximum-file-size": "Maximal %1 kb", + "no-uploads-found": "Keine Uploads gefunden", + "public-uploads-info": "Uploads sind öffentlich, alle Besucher können diese sehen.", + "private-uploads-info": "Uploads sind privat, nur eingeloggte Benutzer können diese sehen." +} \ No newline at end of file diff --git a/public/language/de/user.json b/public/language/de/user.json new file mode 100644 index 0000000000..ae3f62dd99 --- /dev/null +++ b/public/language/de/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Gesperrt", + "muted": "Stummgeschaltet", + "offline": "Offline", + "deleted": "Gelöscht", + "username": "Benutzername", + "joindate": "Registriert am", + "postcount": "Beiträge", + "email": "E-Mail", + "confirm_email": "E-Mail bestätigen", + "account_info": "Kontoinformationen", + "admin_actions_label": "Administrative Aktionen", + "ban_account": "Konto sperren", + "ban_account_confirm": "Bist du sicher, dass du diesen Benutzer sperren möchtest?", + "unban_account": "Konto entsperren", + "mute_account": "Konto stummschalten", + "unmute_account": "Konto entstummen", + "delete_account": "Konto löschen", + "delete_account_as_admin": "Konto löschen", + "delete_content": "Konto-Inhalt löschen", + "delete_all": "Konto und Inhalt löschen", + "delete_account_confirm": "Bist du sicher, dass du diesen Account löschen und deine Beiträge anonymisieren mnöchtest?
Diese Aktion kann nicht rückgängig gemacht werden und die Daten können nicht wiederhergestellt werden.

Gib dein Passwort ein um das Löschen des Accounts zu bestätigen.", + "delete_this_account_confirm": "Bist du sicher, dass du diesen Account löschen und seine Inhalte beibehalten möchstes?
Diese Aktion kann nicht rückgängig gemacht werden. Beiträge werden anonymisiert und können nicht wieder mit dem gelöschten Account verknüpft werden.

", + "delete_account_content_confirm": "Bist du sicher, dass du die Inhalte dieses Accounts (Beiträge/Themen/Uploads) löschen möchtest?
Diese Aktion ist irreversibel und die Daten können nicht wiederhergestellt werden.

", + "delete_all_confirm": "Bist du sicher, dass du diesen Account und seinen gesamten Inhalt (Beiträge/Themen/Uploads) löschen möchtest?
Diese Aktion ist irreversibel und die Daten können nicht wiederhergestellt werden.

", + "account-deleted": "Konto gelöscht", + "account-content-deleted": "Inhalt des Kontos gelöscht", + "fullname": "Kompletter Name", + "website": "Homepage", + "location": "Wohnort", + "age": "Alter", + "joined": "Beigetreten", + "lastonline": "Zuletzt online", + "profile": "Profil", + "profile_views": "Profilaufrufe", + "reputation": "Ansehen", + "bookmarks": "Lesezeichen", + "watched_categories": "Beobachtete Kategorien", + "change_all": "Alle ändern", + "watched": "Beobachtet", + "ignored": "Ignoriert", + "default-category-watch-state": "Standardmäßige Beobachtung", + "followers": "Follower", + "following": "Folge ich", + "blocks": "Blockiert", + "block_toggle": "Ent-/Blocken", + "block_user": "User blockieren", + "unblock_user": "User entblocken", + "aboutme": "Über mich", + "signature": "Signatur", + "birthday": "Geburtstag", + "chat": "Chat", + "chat_with": "Führe deinen Chat mit %1 fort", + "new_chat_with": "Beginne einen neuen Chat mit %1", + "flag-profile": "Profil Melden", + "follow": "Folgen", + "unfollow": "Nicht mehr folgen", + "more": "Mehr", + "profile_update_success": "Profil erfolgreich aktualisiert!", + "change_picture": "Profilbild ändern", + "change_username": "Benutzernamen ändern", + "change_email": "E-Mail ändern", + "email_same_as_password": "Gebe bitte dein aktuelles Passwort ein um fortzufahren – Du hast deine neue E-Mail erneut eingegeben", + "edit": "Ändern", + "edit-profile": "Profil ändern", + "default_picture": "Standardsymbol", + "uploaded_picture": "Hochgeladene Bilder", + "upload_new_picture": "Neues Bild hochladen", + "upload_new_picture_from_url": "Neues Bild von URL hochladen", + "current_password": "Aktuelles Passwort", + "change_password": "Passwort ändern", + "change_password_error": "Ungültiges Passwort!", + "change_password_error_wrong_current": "Ihr derzeitiges Passwort ist ungültig!", + "change_password_error_match": "Passwörter müssen übereinstimmen!", + "change_password_error_privileges": "Deine Berechtigungen reichen nicht aus, um dieses Passwort zu ändern.", + "change_password_success": "Ihr Passwort wurde aktualisiert!", + "confirm_password": "Passwort wiederholen", + "password": "Passwort", + "username_taken_workaround": "Der gewünschte Benutzername ist bereits vergeben, deshalb haben wir ihn ein wenig verändert. Du bist jetzt unter dem Namen %1 bekannt.", + "password_same_as_username": "Dein Passwort entspricht deinem Benutzernamen, bitte wähle ein anderes Passwort.", + "password_same_as_email": "Dein Passwort entspricht deiner E-Mail-Adresse, bitte wähle ein anderes Passwort.", + "weak_password": "Schwaches Passwort.", + "upload_picture": "Bild hochladen", + "upload_a_picture": "Ein Bild hochladen", + "remove_uploaded_picture": "Hochgeladenes Bild entfernen", + "upload_cover_picture": "Titelbild hochladen", + "remove_cover_picture_confirm": "Bist du sicher, dass du dein Titelbild entfernen möchtest?", + "crop_picture": "Bild zuschneiden", + "upload_cropped_picture": "Zuschneiden und Hochladen", + "avatar-background-colour": "Hintergrundfarbe des Avatars", + "settings": "Einstellungen", + "show_email": "Meine E-Mail anzeigen", + "show_fullname": "Zeige meinen kompletten Namen an", + "restrict_chats": "Erlaube Chatnachrichten nur von Benutzern, denen ich folge.", + "digest_label": "Zusammenfassung abonnieren", + "digest_description": "Abonniere E-Mail-Benachrichtigungen für dieses Forum (neue Benachrichtigungen und Themen) nach einem festen Zeitplan.", + "digest_off": "Aus", + "digest_daily": "Täglich", + "digest_weekly": "Wöchentlich", + "digest_biweekly": "Zweiwöchentlich", + "digest_monthly": "Monatlich", + "has_no_follower": "Diesem Benutzer folgt noch niemand. :(", + "follows_no_one": "Dieser Benutzer folgt noch niemandem. :(", + "has_no_posts": "Dieser Benutzer hat noch nichts geschrieben.", + "has_no_best_posts": "Dieser Benutzer hat noch keine positiv bewerteten Beiträge.", + "has_no_topics": "Dieser Benutzer hat noch keine Themen erstellt.", + "has_no_watched_topics": "Dieser Benutzer beobachtet keine Themen.", + "has_no_ignored_topics": "Dieser Benutzer ignoriert bisher keine Themen.", + "has_no_upvoted_posts": "Dieser Benutzer hat bisher keine Beiträge positiv bewertet.", + "has_no_downvoted_posts": "Dieser Benutzer hat bisher keine Beiträge negativ bewertet.", + "has_no_controversial_posts": "Dieser Benutzer hat noch keine herabgestuften Beiträge.", + "has_no_blocks": "Du hast keine Benutzer geblockt", + "email_hidden": "E-Mail Adresse versteckt", + "hidden": "versteckt", + "paginate_description": "Themen und Beiträge in Seiten aufteilen, anstatt unendlich zu scrollen", + "topics_per_page": "Themen pro Seite", + "posts_per_page": "Beiträge pro Seite", + "max_items_per_page": "Maximal %1", + "acp_language": "Sprache der Admin Seiten", + "notifications": "Benachrichtigungen", + "upvote-notif-freq": "Benachrichtigungshäufigkeit für positive Bewertungen", + "upvote-notif-freq.all": "Alle positiven Bewertungen", + "upvote-notif-freq.first": "Erster pro Beitrag", + "upvote-notif-freq.everyTen": "Alle 10 positiven Bewertungen", + "upvote-notif-freq.threshold": "Bei 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Bei 10, 100, 1000...", + "upvote-notif-freq.disabled": "Deaktiviert", + "browsing": "Browsing", + "open_links_in_new_tab": "Ausgehende Links in neuem Tab öffnen", + "enable_topic_searching": "Suchen innerhalb von Themen aktivieren", + "topic_search_help": "Wenn aktiviert, ersetzt die im-Thema-Suche die Standardsuche des Browsers. Dadurch kannst du im ganzen Thema suchen, nicht nur im sichtbaren Abschnitt.", + "update_url_with_post_index": "Aktualisieren Sie die URL mit dem Beitragsindex, während Sie Themen durchsuchen", + "scroll_to_my_post": "Zeige eigene Antwort nach dem Erstellen im Thema an", + "follow_topics_you_reply_to": "Themen folgen, in denen du geantwortet hast", + "follow_topics_you_create": "Themen folgen, die du erstellst", + "grouptitle": "Gruppentitel", + "group-order-help": "Wähle eine Gruppe und ordne die Titel mit den Pfeiltasten", + "no-group-title": "Kein Gruppentitel", + "select-skin": "Einen Skin auswählen", + "select-homepage": "Startseite", + "homepage": "Startseite", + "homepage_description": "Wähle eine Seite, die als Forumstartseite verwendet werden soll, aus oder 'Keine' um die Standardstartseite zu verwenden.", + "custom_route": "Eigener Startseitenpfad", + "custom_route_help": "Gebe hier einen Routennamen ohne vorangestellten Schrägstrich ein (z. B. 'aktuell' oder 'Kategorie/2/allgemeine-Diskussion')", + "sso.title": "Single Sign-on Dienste", + "sso.associated": "Verbunden mit", + "sso.not-associated": "Verbinde dich mit", + "sso.dissociate": "Trennen", + "sso.dissociate-confirm-title": "Trennung bestätigen", + "sso.dissociate-confirm": "Bist du sicher, dass du dein Konto von %1 trennen willst?", + "info.latest-flags": "Neuste Meldungen", + "info.no-flags": "Keine gemeldeten Beiträge gefunden", + "info.ban-history": "Sperrungsverlauf", + "info.no-ban-history": "Dieser Benutzer wurde noch nie gesperrt", + "info.banned-until": "Gesperrt bis zum %1", + "info.banned-expiry": "Ablauf", + "info.banned-permanently": "Permanent gesperrt", + "info.banned-reason-label": "Grund", + "info.banned-no-reason": "Kein Grund angegeben.", + "info.mute-history": "Kürzlichste Stummschalthistory", + "info.no-mute-history": "Dieser Benutzer wurde noch nie stummgeschaltet", + "info.muted-until": "Bis %1 stummgeschaltet", + "info.muted-expiry": "Ablauf", + "info.muted-no-reason": "Kein Grund angegeben", + "info.username-history": "Benutzernamen Verlauf", + "info.email-history": "E-Mail Verlauf", + "info.moderation-note": "Moderationsnotiz", + "info.moderation-note.success": "Moderationsnotiz gespeichert", + "info.moderation-note.add": "Notitz hinzufügen", + "sessions.description": "Auf dieser Seite kannst du alle aktiven Sitzungen in diesem Forum einsehen und bei Bedarf widerrufen. Du kannst deine eigene Sitzung widerrufen, indem du dich von deinem Konto abmeldest.", + "consent.title": "Deine Rechte & Zustimmungen", + "consent.lead": "Dieses Community-Forum sammelt und verarbeitet deine persönlichen Daten.", + "consent.intro": "Wir verwenden diese Informationen ausschließlich, um Deine Erfahrungen in dieser Community zu personalisieren und Deine Beiträge dem Benutzerkonto zuzuordnen.

Wir bewahren diese Informationen für die Dauer Deines Benutzerkontos auf. Du kannst die Einwilligung jederzeit widerrufen, indem Du Dein Konto löschst.

Wenn Du Fragen oder Bedenken hast, empfehlen wir, dich an das Adminteam dieses Forums zu wenden.", + "consent.email_intro": "Gelegentlich senden wir E-Mails an Deine E-Mail-Adresse um Updates bereitzustellen und/oder Dich über neue Aktivitäten zu informieren.", + "consent.digest_frequency": "Sofern nicht explizit in Ihren Benutzereinstellungen geändert, werden alle %1 Zusammenfassungen per E-Mail versandt.", + "consent.digest_off": "Sofern in Ihren Benutzereinstellungen nicht explizit geändert, werden keine Zusammenfassungen per E-Mail versandt.", + "consent.received": "Du hast zugestimmt, dass diese Website deine Persönlichen Daten sammeln und verarbeiten darf. Es ist keine weitere Aktion erforderlich.", + "consent.not_received": "Du hast der Sammlung und Verarbeitung von Daten nicht zugestimmt. Diese Website-Administration behält sich vor dein Konto jederzeit zu löschen um die GDPR einzuhalten.", + "consent.give": "Zustimmen", + "consent.right_of_access": "Du hast das Recht auf Zugriff", + "consent.right_of_access_description": "Du hast das Recht deine Daten die von dieser Website gesammelt wurden auf anfrage einsehen zu können. Du kannst eine kopie bekommen, indem du unten auf den entsprechenden Knopf drückst.", + "consent.right_to_rectification": "Du hast das Recht auf Korrektur", + "consent.right_to_rectification_description": "Du hast das Recht ungenaue Daten die an uns übermittelt wurden zu ändern oder zu aktualisieren. Dein Profil kann aktualisiert werden, in dem du dein Profil bearbeitest, Beiträge können immer bearbeitet werden. Sollte dies nicht der Fall sein, melde dich bitte bei den Administratoren dieser Website.", + "consent.right_to_erasure": "Du hast das Recht auf Löschung", + "consent.right_to_erasure_description": "Du kannst deine Zustimmung zur Datensammlung und/oder Verarbeitung von Daten jederzeit widerrufen, indem du dein Konto löschst. Dein Individuelles Profil kann gelöscht werden, jedoch blieben deine Beiträge und sonstigen Inhalte. Wenn du sowohl dein Konto sowie auch deine Inhalten löschen willst, kontaktiere bitte die Administratoren dieser Seite.", + "consent.right_to_data_portability": "Du hast das Recht auf Datenportabilität", + "consent.right_to_data_portability_description": "Du kannst von uns eine Maschinen-Lesbare Datei von jeglichen gesammelten Daten von dir und deinem Konto anfordern, indem du unten auf den entsprechenden Knopf drückst. ", + "consent.export_profile": "Profil exportieren (.json)", + "consent.export-profile-success": "Profil wird exportiert, du bekommst eine Benachrichtigung sobald der Vorgang abgeschlossen ist.", + "consent.export_uploads": "Hochgeladene Dateien exportieren (.zip)", + "consent.export-uploads-success": "Uploads werden exportiert, du bekommst eine Benachrichtigung sobald der Vorgang abgeschlossen ist.", + "consent.export_posts": "Beiträge exportieren (.csv)", + "consent.export-posts-success": "Beiträge werden exportiert, du bekommst eine Benachrichtigung sobald der Vorgang abgeschlossen ist.", + "emailUpdate.intro": "Bitte gib unten deine E-Mail-Adresse ein. Dieses Forum verwendet deine E-Mail-Adresse für die geplante Zusammenfassung und diverse Benachrichtigungen sowie für die Konto-Wiederherstellung im Falle eines verlorenen Passworts.", + "emailUpdate.optional": "Dieses Feld ist optional. Du bist nicht verpflichtet, deine E-Mail-Adresse anzugeben, doch ohne eine validierte E-Mail-Adresse kannst du dein Konto nicht wiederherstellen oder dich mit deiner E-Mail-Adresse anmelden.", + "emailUpdate.required": "Dieses Feld ist erforderlich.", + "emailUpdate.change-instructions": "An die eingegebene E-Mail-Adresse wird eine Bestätigungs-E-Mail mit einem eindeutigen Link gesendet. Durch den Zugriff auf diesen Link wird dein Eigentum an der E-Mail-Adresse bestätigt und diese wird in deinem Konto aktiv. Du kannst deine E-Mail-Adresse jederzeit auf deiner Kontoseite aktualisieren.", + "emailUpdate.password-challenge": "Bitte gib dein Passwort ein, um dein Konto zu verifizieren." +} \ No newline at end of file diff --git a/public/language/de/users.json b/public/language/de/users.json new file mode 100644 index 0000000000..974094b8fc --- /dev/null +++ b/public/language/de/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Neuste Benutzer", + "top_posters": "Meiste Beiträge", + "most_reputation": "Höchstes Ansehen", + "most_flags": "Meiste Meldungen", + "search": "Suchen", + "enter_username": "Benutzer durchsuchen", + "search-user-for-chat": "Einen Benutzer suchen, um den Chat zu starten", + "load_more": "Mehr laden", + "users-found-search-took": "%1 Benutzer gefunden! Die Suche dauerte %2 s.", + "filter-by": "Filtern nach", + "online-only": "Nur Online", + "invite": "Einladen", + "prompt-email": "E-Mails:", + "groups-to-join": "Gruppen, denen beigetreten wird, wenn die Einladung angenommen wird:", + "invitation-email-sent": "Eine Einladungsemail wurde an %1 verschickt", + "user_list": "Nutzerliste", + "recent_topics": "Neueste Themen", + "popular_topics": "Beliebte Themen", + "unread_topics": "Ungelesen Themen", + "categories": "Kategorien", + "tags": "Schlagworte", + "no-users-found": "Keine Benutzer gefunden!" +} \ No newline at end of file diff --git a/public/language/el/_DO_NOT_EDIT_FILES_HERE.md b/public/language/el/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/el/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/el/admin/admin.json b/public/language/el/admin/admin.json new file mode 100644 index 0000000000..7ad3089636 --- /dev/null +++ b/public/language/el/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Είστε βέβαιοι ότι θέλετε να αναδημιουργήσετε και να επανεκκινήσετε το NodeBB;", + "alert.confirm-restart": "Είστε βέβαιοι ότι θέλετε να επανεκκινήσετε το NodeBB;", + + "acp-title": "%1 | NodeBB Πίνακας Ελέγχου", + "settings-header-contents": "Περιεχόμενα", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/el/admin/advanced/cache.json b/public/language/el/admin/advanced/cache.json new file mode 100644 index 0000000000..4a2c76fc94 --- /dev/null +++ b/public/language/el/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Προσωρινή μνήμη ανάρτησης", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Πλήρες", + "post-cache-size": "Μέγεθος προσωρινής μνήμης ανάρτησης", + "items-in-cache": "Αντικείμενα στην προσωρινή μνήμη" +} \ No newline at end of file diff --git a/public/language/el/admin/advanced/database.json b/public/language/el/admin/advanced/database.json new file mode 100644 index 0000000000..ae2f4b4e7e --- /dev/null +++ b/public/language/el/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Χρόνος λειτουργίας σε δευτερόλεπτα", + "uptime-days": "Χρόνος λειτουργίας σε ημέρες", + + "mongo": "Mongo", + "mongo.version": "Έκδοση MongoDB", + "mongo.storage-engine": "Μηχανή αποθήκευσης", + "mongo.collections": "Συλλογές", + "mongo.objects": "Αντικείμενα", + "mongo.avg-object-size": "Μέσο μέγεθος αντικειμένου", + "mongo.data-size": "Μέγεθος δεδομένων", + "mongo.storage-size": "Μέγεθος αποθήκευσης", + "mongo.index-size": "Μέγεθος ευρετηρίου", + "mongo.file-size": "Μέγεθος αρχείου", + "mongo.resident-memory": "Μόνιμη μνήμη", + "mongo.virtual-memory": "Εικονική μνήμη", + "mongo.mapped-memory": "Καταχωρισμένη μνήμη", + "mongo.bytes-in": "Bytes εντός", + "mongo.bytes-out": "Bytes εκτός", + "mongo.num-requests": "Αριθμός αιτημάτων", + "mongo.raw-info": "Πληροφορίες MongoDB", + "mongo.unauthorized": "Το NodeBB δε μπόρεσε να υποβάλει ερώτημα στη βάση δεδομένων MongoDB για σχετικά στατιστικά στοιχεία. Βεβαιωθείτε ότι ο χρήστης που χρησιμοποιείται από το NodeBB περιέχει τον ρόλο "clusterMonitor" για τη βάση δεδομένων "διαχειριστή".", + + "redis": "Redis", + "redis.version": "Έκδοση Redis", + "redis.keys": "Κλειδιά", + "redis.expires": "Λήγει", + "redis.avg-ttl": "Μέσο TTL", + "redis.connected-clients": "Συνδεδεμένοι πελάτες", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/el/admin/advanced/errors.json b/public/language/el/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/el/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/el/admin/advanced/events.json b/public/language/el/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/el/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/el/admin/advanced/logs.json b/public/language/el/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/el/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/el/admin/appearance/customise.json b/public/language/el/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/el/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/el/admin/appearance/skins.json b/public/language/el/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/el/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/el/admin/appearance/themes.json b/public/language/el/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/el/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/el/admin/dashboard.json b/public/language/el/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/el/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/el/admin/development/info.json b/public/language/el/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/el/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/el/admin/development/logger.json b/public/language/el/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/el/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/el/admin/extend/plugins.json b/public/language/el/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/el/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/el/admin/extend/rewards.json b/public/language/el/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/el/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/el/admin/extend/widgets.json b/public/language/el/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/el/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/el/admin/manage/admins-mods.json b/public/language/el/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/el/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/el/admin/manage/categories.json b/public/language/el/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/el/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/el/admin/manage/digest.json b/public/language/el/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/el/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/el/admin/manage/groups.json b/public/language/el/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/el/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/el/admin/manage/privileges.json b/public/language/el/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/el/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/el/admin/manage/registration.json b/public/language/el/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/el/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/el/admin/manage/tags.json b/public/language/el/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/el/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/el/admin/manage/uploads.json b/public/language/el/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/el/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/el/admin/manage/users.json b/public/language/el/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/el/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/el/admin/menu.json b/public/language/el/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/el/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/advanced.json b/public/language/el/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/el/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/el/admin/settings/api.json b/public/language/el/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/el/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/chat.json b/public/language/el/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/el/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/cookies.json b/public/language/el/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/el/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/email.json b/public/language/el/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/el/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/el/admin/settings/general.json b/public/language/el/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/el/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/el/admin/settings/group.json b/public/language/el/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/el/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/guest.json b/public/language/el/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/el/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/homepage.json b/public/language/el/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/el/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/languages.json b/public/language/el/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/el/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/navigation.json b/public/language/el/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/el/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/el/admin/settings/notifications.json b/public/language/el/admin/settings/notifications.json new file mode 100644 index 0000000000..b38c65a34c --- /dev/null +++ b/public/language/el/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Ειδοποιήσεις", + "welcome-notification": "Ειδοποίηση καλωσορίσματος", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/pagination.json b/public/language/el/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/el/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/post.json b/public/language/el/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/el/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/reputation.json b/public/language/el/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/el/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/social.json b/public/language/el/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/el/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/sockets.json b/public/language/el/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/el/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/sounds.json b/public/language/el/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/el/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/tags.json b/public/language/el/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/el/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/el/admin/settings/uploads.json b/public/language/el/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/el/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/el/admin/settings/user.json b/public/language/el/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/el/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/el/admin/settings/web-crawler.json b/public/language/el/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/el/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/el/category.json b/public/language/el/category.json new file mode 100644 index 0000000000..fd58099f47 --- /dev/null +++ b/public/language/el/category.json @@ -0,0 +1,23 @@ +{ + "category": "Κατηγορία", + "subcategories": "Υποκατηγορίες", + "new_topic_button": "Νέο Θέμα", + "guest-login-post": "Συνδέσου για να δημοσιεύσεις", + "no_topics": "Δεν υπάρχουν θέματα σε αυτή την κατηγορία.
Γιατί δεν δοκιμάζεις να δημοσιεύσεις ένα εσύ;", + "browsing": "περιηγούνται", + "no_replies": "Κανείς δεν έχει απαντήσει", + "no_new_posts": "Δεν υπάρχουν νέες δημοσιεύσεις", + "watch": "Παρακολουθήστε", + "ignore": "Αγνόηση", + "watching": "Παρακολουθώ", + "not-watching": "Δεν παρακολουθώ", + "ignoring": "Αγνόησε", + "watching.description": "Εμφάνιση θεμάτων σε μη αναγνωσμένα και πρόσφατα", + "not-watching.description": "Να μην εμφανίζονται θέματα σε μη αναγνωσμένα, να εμφανίζονται σε πρόσφατα", + "ignoring.description": "Να μην εμφανίζονται θέματα σε μη αναγνωσμένα και πρόσφατα", + "watching.message": "Παρακολουθείτε τώρα ενημερώσεις από αυτήν την κατηγορία και όλες τις υποκατηγορίες", + "notwatching.message": "Δεν παρακολουθείτε ενημερώσεις από αυτήν την κατηγορία και όλες τις υποκατηγορίες", + "ignoring.message": "Τώρα αγνοείτε ενημερώσεις από αυτήν την κατηγορία και όλες τις υποκατηγορίες", + "watched-categories": "Κατηγορίες υπό παρακολούθηση", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/el/email.json b/public/language/el/email.json new file mode 100644 index 0000000000..83a2f5c64e --- /dev/null +++ b/public/language/el/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Καλωσόρισες στο %1", + "invite": "Invitation from %1", + "greeting_no_name": "Γειά σου", + "greeting_with_name": "Γειά σου %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Ευχαριστούμε που γράφτηκες στο %1!", + "welcome.text2": "Για να ενεργοποιήσεις πλήρως τον λογαριασμό σου, πρέπει να επιβεβαιώσουμε πως η διεύθυνση email με την οποια γράφτηκες σου ανήκει.", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "Κάνε κλικ εδώ για να επιβεβαιώσεις την διεύθυνσή σου", + "invitation.text1": "%1 has invited you to join %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Λάβαμε ένα αίτημα για επαναφορά του κωδικού σου, πιθανότατα γιατί τον ξέχασες. Αν δεν έκανες εσύ αυτό το αίτημα, αγνόησε αυτό το email.", + "reset.text2": "Για να κάνεις την επαναφορά του κωδικού σου, παρακαλώ πάτα στο παρακάτω σύνδεσμο:", + "reset.cta": "Κάνε κλικ εδώ για να επαναφέρεις τον κωδικό σου", + "reset.notify.subject": "Password successfully changed", + "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", + "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "digest.latest_topics": "Πρόσφατα θέματα στο %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Κάνε κλικ εδώ για να επισκεφτείς το %1", + "digest.unsub.info": "Αυτή η σύνοψη σου στάλθηκε λόγω των ρυθμίσεών σου.", + "digest.day": "day", + "digest.week": "week", + "digest.month": "month", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Νέο μήνυμα συνομιλίας από τον/την %1", + "notif.chat.cta": "Κάνε κλικ εδώ για να πας στην συνομιλία", + "notif.chat.unsub.info": "Αυτή η ειδοποίηση για συνομιλία σου στάλθηκε λόγω των ρυθμίσεών σου. ", + "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Αυτό είναι ένα δοκιμαστικό email για να επιβεβαιώσουμε ότι ο emailer έχει στηθεί σωστά για το NodeBB.", + "unsub.cta": "Κάνε κλικ εδώ για να αλλάξεις αυτές τις ρυθμίσεις", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Ευχαριστούμε!" +} \ No newline at end of file diff --git a/public/language/el/error.json b/public/language/el/error.json new file mode 100644 index 0000000000..38d4c42c33 --- /dev/null +++ b/public/language/el/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Άκυρα Δεδομένα", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Φαίνεται πως δεν είσαι συνδεδεμένος/η.", + "account-locked": "Ο λογαριασμός σου έχει κλειδωθεί προσωρινά", + "search-requires-login": "Searching requires an account - please login or register.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Άκυρο ID Κατηγορίας", + "invalid-tid": "Άκυρο ID Θέματος", + "invalid-pid": "Άκυρο ID Δημοσίευσης", + "invalid-uid": "Άκυρο ID Χρήστη", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Άκυρο Όνομα Χρήστη", + "invalid-email": "Άκυρο Email", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Άκυρα Δεδομένα Χρήστη", + "invalid-password": "Άκυρος Κωδικός", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Παρακαλώ γράψε το όνομα χρήστη και τον κωδικό", + "invalid-search-term": "Άκυρος όρος αναζήτησης", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "Το όνομα χρήστη είναι πιασμένο", + "email-taken": "Το email είναι πιασμένο", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Το όνομα χρήστη είναι πολύ μικρό", + "username-too-long": "Το όνομα χρήστη είναι πολύ μεγάλο", + "password-too-long": "Password too long", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Ο Χρήστης είναι αποκλεισμένος/η", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Category does not exist", + "no-topic": "Topic does not exist", + "no-post": "Post does not exist", + "no-group": "Group does not exist", + "no-user": "User does not exist", + "no-teaser": "Teaser does not exist", + "no-flag": "Flag does not exist", + "no-privileges": "You do not have enough privileges for this action.", + "category-disabled": "Η κατηγορία έχει απενεργοποιηθεί", + "topic-locked": "Το θέμα έχει κλειδωθεί", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", + "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Παρακαλώ περίμενε να τελειώσει το ανέβασμα των αρχείων.", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Δεν μπορείς να αποκλείσεις άλλους διαχειριστές!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Το όνομα της ομάδας είναι πολύ μικρό", + "group-name-too-long": "Group name too long", + "group-already-exists": "Το όνομα της ομάδας υπάρχει ήδη", + "group-name-change-not-allowed": "Αλλαγή του ονόματος της ομάδας δεν επιτρέπεται", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "This post has already been deleted", + "post-already-restored": "This post has already been restored", + "topic-already-deleted": "This topic has already been deleted", + "topic-already-restored": "This topic has already been restored", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "Οι εικόνες θεμάτων είναι απενεργοποιημένες", + "invalid-file": "Άκυρο Αρχείο", + "uploads-are-disabled": "Το ανέβασμα αρχείων έχει απενεργοποιηθεί", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "Δεν μπορείς να συνομιλήσεις με τον εαυτό σου!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Το σύστημα φήμης έχει απενεργοποιηθεί.", + "downvoting-disabled": "Η καταψήφιση έχει απενεργοποιηθεί", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "Το NodeBB συνάντησε ένα πρόβλημα καθώς γινόταν η ανανέωση: \"%1\". Το NodeBB θα συνεχίσει να προσφέρει τα στοιχεία του χρήστη, αν και θα ήταν καλή ιδέα να επαναφέρεις ότι έκανες πριν την ανανέωση.", + "registration-error": "Registration Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/el/flags.json b/public/language/el/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/el/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/el/global.json b/public/language/el/global.json new file mode 100644 index 0000000000..5d5797c1bf --- /dev/null +++ b/public/language/el/global.json @@ -0,0 +1,126 @@ +{ + "home": "Κεντρική", + "search": "Αναζήτηση", + "buttons.close": "Κλείσιμο", + "403.title": "Δεν επιτρέπεται η πρόσβαση", + "403.message": "Φαίνεται πως βρέθηκες σε κάποια σελίδα στην οποία δεν έχεις πρόσβαση.", + "403.login": "Perhaps you should try logging in?", + "404.title": "Δεν βρέθηκε", + "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "500.title": "Εσωτερικό Σφάλμα.", + "500.message": "Ουπς! Φαίνεται πως κάτι πήγε στραβά!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Εγγραφή", + "login": "Σύνδεση", + "please_log_in": "Παρακαλώ Συνδέσου", + "logout": "Αποσύνδεση", + "posting_restriction_info": "Η δημοσίευση είναι περιορισμένη μόνο για εγγεγραμμένα μέλη, κάνε κλικ εδώ για να συνδεθείς.", + "welcome_back": "Καλωσόρισες Πάλι", + "you_have_successfully_logged_in": "Συνδέθηκες με επιτυχία", + "save_changes": "Αποθήκευση Αλλαγών", + "save": "Αποθήκευση", + "close": "Κλείσιμο", + "pagination": "Σελιδοποίηση", + "pagination.out_of": "%1 από %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Διαχειριστής", + "header.categories": "Κατηγορίες", + "header.recent": "Πρόσφατα", + "header.unread": "Μη αναγνωσμένα", + "header.tags": "Ετικέτες", + "header.popular": "Δημοφιλή", + "header.top": "Top", + "header.users": "Χρήστες", + "header.groups": "Ομάδες", + "header.chats": "Συνομιλίες", + "header.notifications": "Ειδοποιήσεις", + "header.search": "Αναζήτηση", + "header.profile": "Προφίλ", + "header.navigation": "Navigation", + "notifications.loading": "Φόρτωση Ειδοποιήσεων", + "chats.loading": "Φόρτωση Συνομιλιών", + "motd.welcome": "Καλωσόρισες στο NodeBB, την πλατφόρμα συζητήσεων του μέλλοντος.", + "previouspage": "Προηγούμενη Σελίδα", + "nextpage": "Επόμενη Σελίδα", + "alert.success": "Επιτυχία", + "alert.error": "Σφάλμα", + "alert.banned": "Αποκλεισμένος/η", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Δεν ακολουθείς πλέον τον/την %1!", + "alert.follow": "Ακολουθείς τον/την %1!", + "users": "Χρήστες", + "topics": "Θέματα", + "posts": "Δημοσιεύσεις", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Εμφανίσεις", + "posters": "Posters", + "reputation": "Φήμη", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "διάβασε περισσότερα", + "more": "Περισσότερα", + "none": "None", + "posted_ago_by_guest": "δημοσιεύτηκε πριν από %1 από Επισκέπτη", + "posted_ago_by": "δημοσιεύτηκε πριν από %1 από τον/την %2", + "posted_ago": "δημοσιεύτηκε πρίν από %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "δημοσιεύτηκε στο %1 πριν από %2", + "posted_in_ago_by": "δημοσιεύτηκε στο %1 πριν από %2 από τον/την %3", + "user_posted_ago": "Ο/Η %1 δημοσίευσε πριν από %2", + "guest_posted_ago": "Επισκέπτης δημοσίευσε πριν από %1", + "last_edited_by": "last edited by %1", + "norecentposts": "Δεν υπάρχουν πρόσφατες δημοσιεύσεις", + "norecenttopics": "Δεν υπάρχουν πρόσφατα θέματα", + "recentposts": "Πρόσφατες Δημοσιεύσεις", + "recentips": "Πρόσφατη IP Σύνδεσης", + "moderator_tools": "Moderator Tools", + "online": "Συνδεδεμένος", + "away": "Απών/ούσα", + "dnd": "Μην ενοχλείτε", + "invisible": "Αόρατος/η", + "offline": "Εκτός Σύνδεσης", + "email": "Email", + "language": "Γλώσσα", + "guest": "Επισκέπτης", + "guests": "Επισκέπτες", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Το φόρουμ αναβαθμίστηκε", + "updated.message": "Το φόρουμ μόλις αναβαθμίστηκε στην πιο πρόσφατη έκδοση. Κάνε κλικ εδώ για να ανανεώσεις την σελίδα.", + "privacy": "Privacy", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "Διαγραφή Όλων", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "Διεύθυνση IP", + "enter_page_number": "Enter page number", + "upload_file": "Ανέβασμα αρχείου", + "upload": "Ανέβασμα", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Μάθε Περισσότερα", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/el/groups.json b/public/language/el/groups.json new file mode 100644 index 0000000000..8e6431a0f9 --- /dev/null +++ b/public/language/el/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Ομάδες", + "view_group": "Προβολή Ομάδας", + "owner": "Κάτοχος Ομάδας", + "new_group": "Δημιουργία Νέας Ομάδας", + "no_groups_found": "There are no groups to see", + "pending.accept": "Αποδοχή", + "pending.reject": "Απόρριψη", + "pending.accept_all": "Αποδοχή Όλων", + "pending.reject_all": "Απόρριψη Όλων", + "pending.none": "There are no pending members at this time", + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Αποθήκευση", + "cover-saving": "Saving", + "details.title": "Λεπτομέρειες Ομάδας", + "details.members": "Λίστα Μελών", + "details.pending": "Pending Members", + "details.invited": "Invited Members", + "details.has_no_posts": "Τα μέλη αυτής της ομάδας δεν έχουν δημοσιεύσει τίποτα.", + "details.latest_posts": "Τελευταίες δημοσιεύσεις.", + "details.private": "Private", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.member_count": "Member Count", + "details.creation_date": "Ημερομηνία Δημιουργίας", + "details.description": "Περιγραφή", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Badge Text", + "details.userTitleEnabled": "Show Badge", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Διαγραφή Ομάδας", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted", + "membership.accept-invitation": "Accept Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitation Pending", + "membership.join-group": "Join Group", + "membership.leave-group": "Leave Group", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Reject", + "new-group.group_name": "Group Name:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/el/ip-blacklist.json b/public/language/el/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/el/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/el/language.json b/public/language/el/language.json new file mode 100644 index 0000000000..fe984351cc --- /dev/null +++ b/public/language/el/language.json @@ -0,0 +1,5 @@ +{ + "name": "Greek", + "code": "el", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/el/login.json b/public/language/el/login.json new file mode 100644 index 0000000000..fd1297f017 --- /dev/null +++ b/public/language/el/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Όνομα χρήστη / Email", + "username": "Όνομα Χρήστη", + "remember_me": "Απομνημόνευση;", + "forgot_password": "Ξέχασες τον κωδικό σου;", + "alternative_logins": "Εναλλακτικά Login", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "Συνδέθηκες επιτυχώς!", + "dont_have_account": "Δεν έχεις λογαριασμό;", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/el/modules.json b/public/language/el/modules.json new file mode 100644 index 0000000000..1e58a4e2ef --- /dev/null +++ b/public/language/el/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Send", + "chat.no_active": "You have no active chats.", + "chat.user_typing": "%1 is typing ...", + "chat.user_has_messaged_you": "%1 has messaged you.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Please select a recipient to view chat message history", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Recent Chats", + "chat.contacts": "Contacts", + "chat.message-history": "Message History", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop out chat", + "chat.minimize": "Minimize", + "chat.maximize": "Maximize", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 said in %2:", + "composer.user_said": "%1 said:", + "composer.discard": "Are you sure you wish to discard this post?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/el/notifications.json b/public/language/el/notifications.json new file mode 100644 index 0000000000..c930226e8f --- /dev/null +++ b/public/language/el/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notifications", + "no_notifs": "You have no new notifications", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Back to %1", + "outgoing_link": "Outgoing Link", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "Continue to %1", + "return_to": "Return to %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "You have unread notifications.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "New message from %1", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 has posted a reply to: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 has posted a new topic: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 started following you.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email Confirmed", + "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", + "email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.", + "email-confirm-sent": "Στάλθηκε email επιβεβαίωσης.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/el/pages.json b/public/language/el/pages.json new file mode 100644 index 0000000000..1f878dc456 --- /dev/null +++ b/public/language/el/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Κεντρική", + "unread": "Μη αναγνωσμένα Θέματα", + "popular-day": "Popular topics today", + "popular-week": "Popular topics this week", + "popular-month": "Popular topics this month", + "popular-alltime": "All time popular topics", + "recent": "Πρόσφατα Θέματα", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Συνδεδεμένοι Χρήστες", + "users/latest": "Πρόσφατοι Χρήστες", + "users/sort-posts": "Users with the most posts", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Αποκλεισμένοι Χρήστες", + "users/most-flags": "Most flagged users", + "users/search": "Αναζήτηση Χρήστη", + "notifications": "Ειδοποιήσεις", + "tags": "Ετικέτες", + "tag": "Topics tagged under "%1"", + "register": "Register an account", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Reset your account password", + "categories": "Categories", + "groups": "Groups", + "group": "%1 group", + "chats": "Chats", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Πληροφορίες Λογαρισμού", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Επιλογές Χρήστη", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Confirmed", + "maintenance.text": "Το %1 αυτή την στιγμή συντηρείται. Παρακαλώ έλα αργότερα.", + "maintenance.messageIntro": "Additionally, the administrator has left this message:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/el/post-queue.json b/public/language/el/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/el/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/el/recent.json b/public/language/el/recent.json new file mode 100644 index 0000000000..85f948e9c8 --- /dev/null +++ b/public/language/el/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Πρόσφατα", + "day": "Ημέρα", + "week": "Εβδομάδα", + "month": "Μήνας", + "year": "Έτος", + "alltime": "Όλο το Ιστορικό", + "no_recent_topics": "Δεν υπάρχουν πρόσφατα θέματα.", + "no_popular_topics": "Δεν υπάρχουν δημοφιλή θέματα.", + "there-is-a-new-topic": "Υπάρχει ένα νέο θέμα.", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", + "there-are-new-topics": "There are %1 new topics.", + "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", + "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", + "there-is-a-new-post": "There is a new post.", + "there-are-new-posts": "There are %1 new posts.", + "click-here-to-reload": "Click here to reload." +} \ No newline at end of file diff --git a/public/language/el/register.json b/public/language/el/register.json new file mode 100644 index 0000000000..31f5bff688 --- /dev/null +++ b/public/language/el/register.json @@ -0,0 +1,32 @@ +{ + "register": "Εγγραφή", + "cancel_registration": "Cancel Registration", + "help.email": "Από προεπιλογή, το email σου θα είναι κρυμμένο από την κοινή θέα.", + "help.username_restrictions": "Ένα μοναδικό όνομα χρήστη μεταξύ %1 και %2 χαρακτήρων. Άλλα άτομα μπορούν να σε αναφέρουν με το @username σου.", + "help.minimum_password_length": "Το μήκος του κωδικού σου πρέπει να είναι τουλάχιστον %1 χαρακτήρες.", + "email_address": "Διεύθυνση Email", + "email_address_placeholder": "Εισαγωγή Διεύθυνσης Email", + "username": "Όνομα Χρήστη", + "username_placeholder": "Εισαγωγή Ονόματος Χρήστη", + "password": "Κωδικός", + "password_placeholder": "Εισαγωγή Κωδικού", + "confirm_password": "Επιβεβαίωση Κωδικού", + "confirm_password_placeholder": "Επιβεβαίωση Κωδικού", + "register_now_button": "Εγγραφή Τώρα", + "alternative_registration": "Εναλλακτική Εγγραφή", + "terms_of_use": "Όροι Χρήσης", + "agree_to_terms_of_use": "Συμφωνώ με τους Όρους Χρήσης", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/el/reset_password.json b/public/language/el/reset_password.json new file mode 100644 index 0000000000..19b4079251 --- /dev/null +++ b/public/language/el/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Επαναφορά Κωδικού", + "update_password": "Ενημέρωση Κωδικού", + "password_changed.title": "Ο Κωδικός Άλλαξε", + "password_changed.message": "

Ο κωδικός επαναφέρθηκε με επιτυχία, παρακαλώ συνδέσου ξανά.", + "wrong_reset_code.title": "Λάθος Κώδικας Επαναφοράς", + "wrong_reset_code.message": "Ο κώδικας επαναφοράς που λήφθηκε ήταν λανθασμένος. Παρακαλώ δοκίμασε ξανά ή ζήτησε ένα νέο κώδικα επαναφοράς.", + "new_password": "Νέος Κωδικός", + "repeat_password": "Επιβεβαίωση Κωδικού", + "changing_password": "Changing Password", + "enter_email": "Παρακαλώ γράψε την διεύθυνση email σου και θα σου στείλουμε ένα email με οδηγίες για το πως να επαναφέρεις τον λογαριασμό σου.", + "enter_email_address": "Εισαγωγή Διεύθυνσης Email", + "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.", + "invalid_email": "Λάθος Email ή το Email δεν υπάρχει!", + "password_too_short": "Ο κωδικός είναι πολύ μικρός, παρακαλώ επέλεξε διαφορετικό.", + "passwords_do_not_match": "Οι κωδικοί δεν ταιριάζουν μεταξύ τους.", + "password_expired": "Ο κωδικός έληξε, παρακαλώ επίλεξε νέο κωδικό" +} \ No newline at end of file diff --git a/public/language/el/search.json b/public/language/el/search.json new file mode 100644 index 0000000000..a2be38263b --- /dev/null +++ b/public/language/el/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 αποτελεσμα(τα) για \"%2\", (%3 δευτερόλεπτα)", + "no-matches": "No matches found", + "advanced-search": "Advanced Search", + "in": "In", + "titles": "Titles", + "titles-posts": "Titles and Posts", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Posted by", + "in-categories": "In Categories", + "search-child-categories": "Search child categories", + "has-tags": "Has tags", + "reply-count": "Αριθμός Απαντήσεων", + "at-least": "Τουλάχιστον", + "at-most": "At most", + "relevance": "Relevance", + "post-time": "Post time", + "votes": "Votes", + "newer-than": "Νεότερο από", + "older-than": "Παλαιότερο από", + "any-date": "Any date", + "yesterday": "Χθες", + "one-week": "Μία εβδομάδα", + "two-weeks": "Δύο εβδομάδες", + "one-month": "Ένας μήνας", + "three-months": "Τρεις μήνες", + "six-months": "Six months", + "one-year": "Ένας χρόνος", + "sort-by": "Sort by", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "topic-votes": "Topic votes", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Username", + "category": "Κατηγορία", + "descending": "In descending order", + "ascending": "In ascending order", + "save-preferences": "Save preferences", + "clear-preferences": "Clear preferences", + "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)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/el/success.json b/public/language/el/success.json new file mode 100644 index 0000000000..89a804572f --- /dev/null +++ b/public/language/el/success.json @@ -0,0 +1,7 @@ +{ + "success": "Επιτυχία", + "topic-post": "Δημοσίευσες με επιτυχία.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Επιτυχής Ταυτοποίηση", + "settings-saved": "Οι επιλογές αποθηκεύτηκαν!" +} \ No newline at end of file diff --git a/public/language/el/tags.json b/public/language/el/tags.json new file mode 100644 index 0000000000..045d4ca62d --- /dev/null +++ b/public/language/el/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Δεν υπάρχουν θέματα με αυτή την ετικέτα.", + "tags": "Ετικέτες", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "Εισαγωγή ετικετών...", + "no_tags": "Δεν υπάρχουν ακόμα ετικέτες.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/el/top.json b/public/language/el/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/el/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/el/topic.json b/public/language/el/topic.json new file mode 100644 index 0000000000..1669d43478 --- /dev/null +++ b/public/language/el/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Θέμα", + "title": "Title", + "no_topics_found": "Δεν βρέθηκαν θέματα!", + "no_posts_found": "Δεν βρέθηκαν δημοσιεύσεις!", + "post_is_deleted": "Αυτή η δημοσίευση έχει διαγραφεί!", + "topic_is_deleted": "This topic is deleted!", + "profile": "Προφίλ", + "posted_by": "Δημοσιεύτηκε από τον/την %1", + "posted_by_guest": "Δημοσιεύτηκε από Επισκέπτη", + "chat": "Συνομιλία", + "notify_me": "Να ειδοποιούμαι για νέες απαντήσεις σε αυτό το θέμα", + "quote": "Παράθεση", + "reply": "Απάντηση", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log in to reply", + "login-to-view": "🔒 Log in to view", + "edit": "Επεξεργασία", + "delete": "Διαγραφή", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Εκκαθάριση", + "restore": "Επαναφορά", + "move": "Μετακίνηση", + "change-owner": "Change Owner", + "fork": "Διαχωρισμός", + "link": "Σύνδεσμος", + "share": "Μοιράσου το", + "tools": "Εργαλεία", + "locked": "Κλειδωμένο", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Το θέμα αυτό έχει διαγραφεί. Μόνο οι χρήστες με δικαιώματα διαχειριστή θεμάτων μπορούν να το δουν.", + "following_topic.message": "Θα λαμβάνεις ειδοποιήσεις όποτε κάποιος δημοσιεύει κάτι σε αυτό το θέμα.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Παρακαλώ εγγράψου ή συνδέσου για για γραφτείς σε αυτό το θέμα.", + "markAsUnreadForAll.success": "Το θέμα σημειώθηκε ως μη αναγνωσμένο για όλους.", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "Παρακολούθηση", + "unwatch": "Ξεπαρακολούθηση", + "watch.title": "Να ειδοποιούμαι για νέες απαντήσεις σε αυτό το θέμα", + "unwatch.title": "Να μην παρακολουθώ αυτό το θέμα", + "share_this_post": "Μοιράσου αυτή την Δημοσίευση", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Εργαλεία Θέματος", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Καρφίτσωμα Θέματος", + "thread_tools.unpin": "Ξεκαρφίτσωμα Θέματος", + "thread_tools.lock": "Κλείδωμα Θέματος", + "thread_tools.unlock": "Ξεκλείδωμα Θέματος", + "thread_tools.move": "Μετακίνηση Θέματος", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Μετακίνηση Όλων", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Διαχωρισμός Θέματος", + "thread_tools.delete": "Διαγραφή Θέματος", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Είσαι σίγουρος/η πως θέλεις να διαγράψεις αυτό το θέμα;", + "thread_tools.restore": "Επαναφορά Θέματος", + "thread_tools.restore_confirm": "Είσαι σίγουρος/η πως θέλεις να επαναφέρεις αυτό το θέμα;", + "thread_tools.purge": "Εκκαθάριση Θέματος", + "thread_tools.purge_confirm": "Είσαι σίγουρος/η πως θέλεις να εκκαθαρίσεις αυτό το θέμα;", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Είσαι σίγουρος/η πως θέλεις να διαγράψεις αυτή την δημοσίευση;", + "post_restore_confirm": "Είσαι σίγουρος/η πως θέλεις να επαναφέρεις αυτή την δημοσίευση;", + "post_purge_confirm": "Είσαι σίγουρος/η πως θέλεις να εκκαθαρίσεις αυτή την δημοσίευση;", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Οι Κατηγορίες Φορτώνουν", + "confirm_move": "Μετακίνηση", + "confirm_fork": "Διαχωρισμός", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Φόρτωση περισσότερων δημοσιεύσεων", + "move_topic": "Μετακίνηση Θέματος", + "move_topics": "Μετακίνηση Θεμάτων", + "move_post": "Μετακίνηση Δημοσίευσης", + "post_moved": "Η δημοσίευση μετακινήθηκε!", + "fork_topic": "Διαχωρισμός Θέματος", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Κάνε κλικ στις δημοσιεύσεις που θέλεις να διαχωρίσεις", + "fork_no_pids": "Δεν έχουν επιλεχθεί δημοσιεύσεις!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Successfully forked topic! Click here to go to the forked topic.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Εισαγωγή του τίτλου του θέματος εδώ...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Πέταγμα", + "composer.submit": "Υποβολή", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Απάντηση στο %1", + "composer.new_topic": "Νέο Θέμα", + "composer.editing": "Editing", + "composer.uploading": "ανέβασμα...", + "composer.thumb_url_label": "Επικόλληση του URL της εικόνας του θέματος", + "composer.thumb_title": "Προσθήκη μιας εικόνας στο θέμα", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Ή ανέβασε ένα αρχείο", + "composer.thumb_remove": "Καθαρισμός Πεδίων", + "composer.drag_and_drop_images": "Σύρε εικόνες εδώ", + "more_users_and_guests": "%1 επιπλέον χρήστης(ες) και %2 επισκέπτης(ες)", + "more_users": "%1 επιπλέον χρήστης(ες)", + "more_guests": "%1 επιπλέον επισκέπτης(ες)", + "users_and_others": "%1 και %2 άλλοι", + "sort_by": "Ταξινόμηση κατά", + "oldest_to_newest": "Παλαιότερο προς Νεότερο", + "newest_to_oldest": "Νεότερο προς Παλαιότερο", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/el/unread.json b/public/language/el/unread.json new file mode 100644 index 0000000000..3b94a4e306 --- /dev/null +++ b/public/language/el/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Μη αναγνωσμένα", + "no_unread_topics": "Δεν υπάρχουν μη αναγνωσμένα θέματα.", + "load_more": "Φόρτωση Περισσότερων", + "mark_as_read": "Σημείωση ώς Αναγνωσμένα", + "selected": "Επιλεγμένα", + "all": "Όλα", + "all_categories": "Όλες οι κατηγορίες", + "topics_marked_as_read.success": "Τα θέματα σημειώθηκαν ως αναγνωσμένα!", + "all-topics": "Όλα τα θέματα", + "new-topics": "Νέα Θέματα", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/el/uploads.json b/public/language/el/uploads.json new file mode 100644 index 0000000000..b814a06268 --- /dev/null +++ b/public/language/el/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Το αρχείο ανεβαίνει...", + "select-file-to-upload": "Επίλεξε αρχείο για ανέβασμα!", + "upload-success": "Το αρχείο ανέβηκε επιτυχώς!", + "maximum-file-size": "Μέγιστο %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/el/user.json b/public/language/el/user.json new file mode 100644 index 0000000000..1294042aac --- /dev/null +++ b/public/language/el/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Αποκλεισμένος/η", + "muted": "Muted", + "offline": "Εκτός Σύνδεσης", + "deleted": "Deleted", + "username": "Όνομα Χρήστη", + "joindate": "Join Date", + "postcount": "Post Count", + "email": "Emai", + "confirm_email": "Επιβεβαίωση Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Account", + "ban_account_confirm": "Do you really want to ban this user?", + "unban_account": "Unban Account", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Διαγραφή Λογαριασμού", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Account deleted", + "account-content-deleted": "Account content deleted", + "fullname": "Πλήρες Όνομα", + "website": "Ιστοσελίδα", + "location": "Τοποθεσία", + "age": "Ηλικία", + "joined": "Έγινε μέλος στις", + "lastonline": "Τελευταία φορά συνδέθηκε στις", + "profile": "Προφίλ", + "profile_views": "Views του προφίλ", + "reputation": "Φήμη", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Watched", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Ακόλουθοι", + "following": "Ακολουθά", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "About me", + "signature": "Υπογραφή", + "birthday": "Γενέθλια", + "chat": "Συνομιλία", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Ακολούθησε", + "unfollow": "Μην Ακολουθείς", + "more": "More", + "profile_update_success": "Το προφίλ ανανεώθηκε επιτυχώς!", + "change_picture": "Αλλαγή Φωτογραφίας", + "change_username": "Change Username", + "change_email": "Change Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Επεξεργασία", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Ανεβασμένη Φωτογραφία", + "upload_new_picture": "Ανέβασμα Νέας Φωτογραφίας", + "upload_new_picture_from_url": "Ανέβασμα Νέας Φωτογραφίας από URL", + "current_password": "Τωρινός Κωδικός", + "change_password": "Αλλαγή Κωδικού", + "change_password_error": "Άκυρος Κωδικός!", + "change_password_error_wrong_current": "Ο τωρινός σου κωδικός δεν είναι σωστός!", + "change_password_error_match": "Οι κωδικοί πρέπει να είναι οι ίδιοι!", + "change_password_error_privileges": "Δεν έχεις δικαιώματα για να αλλάξεις αυτόν τον κωδικό.", + "change_password_success": "Ο κωδικός σου ανανεώθηκε!", + "confirm_password": "Επιβεβαίωση Κωδικού", + "password": "Κωδικός", + "username_taken_workaround": "Το όνομα χρήστη που ζήτησες χρησιμοποιείται ήδη, οπότε το τροποποιήσαμε λίγο. Πλέον είσαι γνωστός/ή ώς %1", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "Ανέβασμα φωτογραφίας", + "upload_a_picture": "Ανέβασε μια φωτογραφία", + "remove_uploaded_picture": "Remove Uploaded Picture", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Επιλογές", + "show_email": "Εμφάνιση του email μου", + "show_fullname": "Show My Full Name", + "restrict_chats": "Only allow chat messages from users I follow", + "digest_label": "Εγγραφή στην Σύνοψη", + "digest_description": "Εγγράψου σε ενημερώσεις με email για αυτό το φόρουμ (νεες ειδοποιήσεις και θέματα), βάσει του επιλεγμένου προγράμματος", + "digest_off": "Off", + "digest_daily": "Ημερήσια", + "digest_weekly": "Εβδομαδιαίως", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Μηνιαία", + "has_no_follower": "Αυτός ο χρήστης δεν έχει κανέναν ακόλουθο :(", + "follows_no_one": "Αυτός ο χρήστης δεν ακολουθεί κανέναν :(", + "has_no_posts": "This user hasn't posted anything yet.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "This user hasn't posted any topics yet.", + "has_no_watched_topics": "This user hasn't watched any topics yet.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Κρυμμένο Emai", + "hidden": "κρυμμένο", + "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "topics_per_page": "Θέματα ανά σελίδα", + "posts_per_page": "Δημοσιεύσεις ανά σελίδα", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Επιλογές Περιήγησης", + "open_links_in_new_tab": "Open outgoing links in new tab", + "enable_topic_searching": "Enable In-Topic Searching", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + "select-skin": "Select a Skin", + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/el/users.json b/public/language/el/users.json new file mode 100644 index 0000000000..cef10bde03 --- /dev/null +++ b/public/language/el/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Πρόσφατοι Χρήστες", + "top_posters": "Top Δημοσιεύοντες", + "most_reputation": "Υψηλότερη Φήμη", + "most_flags": "Most Flags", + "search": "Αναζήτηση", + "enter_username": "Γράψε ένα όνομα χρήστη προς αναζήτηση", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Φόρτωση περισσότερων", + "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", + "filter-by": "Filter By", + "online-only": "Μόνο Συνδεδεμένοι", + "invite": "Πρόσκληση", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "An invitation email has been sent to %1", + "user_list": "Λίστα Χρηστών", + "recent_topics": "Πρόσφατα Θέματα", + "popular_topics": "Δημοφιλή Θέματα", + "unread_topics": "Μη αναγνωσμένα Θέματα", + "categories": "Κατηγορίες", + "tags": "Ετικέτες", + "no-users-found": "Δε βρέθηκαν χρήστες!" +} \ No newline at end of file diff --git a/public/language/en-GB/_DO_NOT_EDIT_FILES_HERE.md b/public/language/en-GB/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/en-GB/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/en-GB/admin/admin.json b/public/language/en-GB/admin/admin.json new file mode 100644 index 0000000000..39edffb66f --- /dev/null +++ b/public/language/en-GB/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/cache.json b/public/language/en-GB/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/en-GB/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/database.json b/public/language/en-GB/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/en-GB/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/en-GB/admin/advanced/errors.json b/public/language/en-GB/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/en-GB/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/events.json b/public/language/en-GB/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/en-GB/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/advanced/logs.json b/public/language/en-GB/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/en-GB/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/appearance/customise.json b/public/language/en-GB/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/en-GB/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/appearance/skins.json b/public/language/en-GB/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/en-GB/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/appearance/themes.json b/public/language/en-GB/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/en-GB/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/dashboard.json b/public/language/en-GB/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/en-GB/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/en-GB/admin/development/info.json b/public/language/en-GB/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/en-GB/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/development/logger.json b/public/language/en-GB/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/en-GB/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/extend/plugins.json b/public/language/en-GB/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/en-GB/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/en-GB/admin/extend/rewards.json b/public/language/en-GB/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/en-GB/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/extend/widgets.json b/public/language/en-GB/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/en-GB/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/admins-mods.json b/public/language/en-GB/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/en-GB/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/categories.json b/public/language/en-GB/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/en-GB/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/digest.json b/public/language/en-GB/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/en-GB/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/en-GB/admin/manage/groups.json b/public/language/en-GB/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/en-GB/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/privileges.json b/public/language/en-GB/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/en-GB/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/registration.json b/public/language/en-GB/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/en-GB/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/tags.json b/public/language/en-GB/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/en-GB/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/uploads.json b/public/language/en-GB/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/en-GB/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/en-GB/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/en-GB/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/advanced.json b/public/language/en-GB/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/en-GB/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/api.json b/public/language/en-GB/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/en-GB/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/chat.json b/public/language/en-GB/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/en-GB/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/cookies.json b/public/language/en-GB/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/en-GB/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/email.json b/public/language/en-GB/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/en-GB/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/en-GB/admin/settings/general.json b/public/language/en-GB/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/en-GB/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/en-GB/admin/settings/group.json b/public/language/en-GB/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/en-GB/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/guest.json b/public/language/en-GB/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/en-GB/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/homepage.json b/public/language/en-GB/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/en-GB/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/languages.json b/public/language/en-GB/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/en-GB/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/navigation.json b/public/language/en-GB/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/en-GB/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/en-GB/admin/settings/notifications.json b/public/language/en-GB/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/en-GB/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/pagination.json b/public/language/en-GB/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/en-GB/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/post.json b/public/language/en-GB/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/en-GB/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/reputation.json b/public/language/en-GB/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/en-GB/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/social.json b/public/language/en-GB/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/en-GB/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/sockets.json b/public/language/en-GB/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/en-GB/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/sounds.json b/public/language/en-GB/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/en-GB/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/tags.json b/public/language/en-GB/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/en-GB/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/settings/uploads.json b/public/language/en-GB/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/en-GB/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/en-GB/admin/settings/user.json b/public/language/en-GB/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/en-GB/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/en-GB/admin/settings/web-crawler.json b/public/language/en-GB/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/en-GB/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/en-GB/category.json b/public/language/en-GB/category.json new file mode 100644 index 0000000000..ef2c4beec5 --- /dev/null +++ b/public/language/en-GB/category.json @@ -0,0 +1,28 @@ +{ + "category": "Category", + "subcategories": "Subcategories", + + "new_topic_button": "New Topic", + "guest-login-post": "Log in to post", + "no_topics": "There are no topics in this category.
Why don't you try posting one?", + + "browsing": "browsing", + "no_replies": "No one has replied", + "no_new_posts": "No new posts.", + + "watch": "Watch", + "ignore": "Ignore", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + + "watched-categories": "Watched categories", + "x-more-categories": "%1 more categories" +} diff --git a/public/language/en-GB/email.json b/public/language/en-GB/email.json new file mode 100644 index 0000000000..15cbf3cf26 --- /dev/null +++ b/public/language/en-GB/email.json @@ -0,0 +1,73 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Welcome to %1", + + "invite": "Invitation from %1", + + "greeting_no_name": "Hello", + "greeting_with_name": "Hello %1", + + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + + "welcome.text1": "Thank you for registering with %1!", + "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "Click here to confirm your email address", + + "invitation.text1": "%1 has invited you to join %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + + "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", + "reset.text2": "To continue with the password reset, please click on the following link:", + "reset.cta": "Click here to reset your password", + + "reset.notify.subject": "Password successfully changed", + "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", + "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + + "digest.latest_topics": "Latest topics from %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Click here to visit %1", + "digest.unsub.info": "This digest was sent to you due to your subscription settings.", + "digest.day": "day", + "digest.week": "week", + "digest.month": "month", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + + "notif.chat.subject": "New chat message received from %1", + "notif.chat.cta": "Click here to continue the conversation", + "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + + "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + + "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", + + "unsub.cta": "Click here to alter those settings", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + + "closing": "Thanks!" +} \ No newline at end of file diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json new file mode 100644 index 0000000000..6cfc003e00 --- /dev/null +++ b/public/language/en-GB/error.json @@ -0,0 +1,263 @@ +{ + "invalid-data": "Invalid Data", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + + "not-logged-in": "You don't seem to be logged in.", + "account-locked": "Your account has been locked temporarily", + "search-requires-login": "Searching requires an account - please login or register.", + "goback": "Press back to return to the previous page", + + "invalid-cid": "Invalid Category ID", + "invalid-tid": "Invalid Topic ID", + "invalid-pid": "Invalid Post ID", + "invalid-uid": "Invalid User ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + + "invalid-username": "Invalid Username", + "invalid-email": "Invalid Email", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Invalid User Data", + "invalid-password": "Invalid Password", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Please specify both a username and password", + "invalid-search-term": "Invalid search term", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + + "username-taken": "Username taken", + "email-taken": "Email taken", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + + "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-same-password": "Please use a password that is different from your current one", + + "user-banned": "User banned", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + + "no-category": "Category does not exist", + "no-topic": "Topic does not exist", + "no-post": "Post does not exist", + "no-group": "Group does not exist", + "no-user": "User does not exist", + "no-teaser": "Teaser does not exist", + "no-flag": "Flag does not exist", + "no-privileges": "You do not have enough privileges for this action.", + + "category-disabled": "Category disabled", + + "topic-locked": "Topic Locked", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + + "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", + "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + + "still-uploading": "Please wait for uploads to complete.", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + + "cant-ban-other-admins": "You can't ban other admins!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + + "already-deleting": "Already deleting", + + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + + "group-name-too-short": "Group name too short", + "group-name-too-long": "Group name too long", + "group-already-exists": "Group already exists", + "group-name-change-not-allowed": "Group name change not allowed", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + + "post-already-deleted": "This post has already been deleted", + "post-already-restored": "This post has already been restored", + + "topic-already-deleted": "This topic has already been deleted", + "topic-already-restored": "This topic has already been restored", + + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + + "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", + "invalid-file": "Invalid File", + "uploads-are-disabled": "Uploads are disabled", + + "signature-too-long" : "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long" : "Sorry, your about me cannot be longer than %1 character(s).", + + "cant-chat-with-yourself": "You can't chat with yourself!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", + + "registration-error": "Registration Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + + "topic-event-unrecognized": "Topic event '%1' unrecognized", + + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} diff --git a/public/language/en-GB/flags.json b/public/language/en-GB/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/en-GB/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/en-GB/global.json b/public/language/en-GB/global.json new file mode 100644 index 0000000000..f1ee75200e --- /dev/null +++ b/public/language/en-GB/global.json @@ -0,0 +1,155 @@ +{ + "home": "Home", + "search": "Search", + "buttons.close": "Close", + "403.title": "Access Denied", + "403.message": "You seem to have stumbled upon a page that you do not have access to.", + "403.login": "Perhaps you should try logging in?", + "404.title": "Not Found", + "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "500.title": "Internal Error.", + "500.message": "Oops! Looks like something went wrong!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + + "register": "Register", + "login": "Login", + "please_log_in": "Please Log In", + "logout": "Logout", + + "posting_restriction_info": "Posting is currently restricted to registered members only, click here to log in.", + + "welcome_back": "Welcome Back ", + "you_have_successfully_logged_in": "You have successfully logged in", + + "save_changes": "Save Changes", + "save": "Save", + "close": "Close", + + "pagination": "Pagination", + "pagination.out_of": "%1 out of %2", + "pagination.enter_index": "Go to post index", + + "header.admin": "Admin", + "header.categories": "Categories", + "header.recent": "Recent", + "header.unread": "Unread", + "header.tags": "Tags", + "header.popular": "Popular", + "header.top": "Top", + "header.users": "Users", + "header.groups": "Groups", + "header.chats": "Chats", + "header.notifications": "Notifications", + "header.search": "Search", + "header.profile": "Profile", + "header.navigation": "Navigation", + + "notifications.loading": "Loading Notifications", + "chats.loading": "Loading Chats", + + "motd.welcome": "Welcome to NodeBB, the discussion platform of the future.", + + "previouspage": "Previous Page", + "nextpage": "Next Page", + + "alert.success": "Success", + "alert.error": "Error", + + "alert.banned": "Banned", + "alert.banned.message": "You have just been banned, your access is now restricted.", + + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + + "alert.unfollow": "You are no longer following %1!", + "alert.follow": "You are now following %1!", + + "users": "Users", + "topics": "Topics", + "posts": "Posts", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Views", + "posters": "Posters", + "reputation": "Reputation", + "lastpost": "Last post", + "firstpost": "First post", + + "read_more": "read more", + "more": "More", + "none": "None", + + "posted_ago_by_guest": "posted %1 by Guest", + "posted_ago_by": "posted %1 by %2", + "posted_ago": "posted %1", + + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "posted in %1 %2", + "posted_in_ago_by": "posted in %1 %2 by %3", + + "user_posted_ago": "%1 posted %2", + "guest_posted_ago": "Guest posted %1", + "last_edited_by": "last edited by %1", + + "norecentposts": "No Recent Posts", + "norecenttopics": "No Recent Topics", + "recentposts": "Recent Posts", + "recentips": "Recently Logged In IPs", + + "moderator_tools": "Moderator Tools", + + "online": "Online", + "away": "Away", + "dnd": "Do not disturb", + "invisible": "Invisible", + "offline": "Offline", + + "email": "Email", + "language": "Language", + + "guest": "Guest", + "guests": "Guests", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + + "updated.title": "Forum Updated", + "updated.message": "This forum has just been updated to the latest version. Click here to refresh the page.", + + "privacy": "Privacy", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "Delete All", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + + "user-search-prompt": "Type something here to find users..." +} diff --git a/public/language/en-GB/groups.json b/public/language/en-GB/groups.json new file mode 100644 index 0000000000..7e6b5ca759 --- /dev/null +++ b/public/language/en-GB/groups.json @@ -0,0 +1,72 @@ +{ + "groups": "Groups", + "view_group": "View Group", + "owner": "Group Owner", + "new_group": "Create New Group", + "no_groups_found": "There are no groups to see", + + "pending.accept": "Accept", + "pending.reject": "Reject", + "pending.accept_all": "Accept All", + "pending.reject_all": "Reject All", + "pending.none": "There are no pending members at this time", + + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + + "cover-save": "Save", + "cover-saving": "Saving", + + "details.title": "Group Details", + "details.members": "Member List", + "details.pending": "Pending Members", + "details.invited": "Invited Members", + "details.has_no_posts": "This group's members have not made any posts.", + "details.latest_posts": "Latest Posts", + "details.private": "Private", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.member_count": "Member Count", + "details.creation_date": "Creation Date", + "details.description": "Description", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Badge Text", + "details.userTitleEnabled": "Show Badge", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Delete Group", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted", + + "membership.accept-invitation": "Accept Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitation Pending", + "membership.join-group": "Join Group", + "membership.leave-group": "Leave Group", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Reject", + + "new-group.group_name": "Group Name:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/en-GB/ip-blacklist.json b/public/language/en-GB/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/en-GB/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/en-GB/language.json b/public/language/en-GB/language.json new file mode 100644 index 0000000000..dab8ef1141 --- /dev/null +++ b/public/language/en-GB/language.json @@ -0,0 +1,5 @@ +{ + "name": "English (United Kingdom/Canada)", + "code": "en-GB", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/en-GB/login.json b/public/language/en-GB/login.json new file mode 100644 index 0000000000..5421ccc307 --- /dev/null +++ b/public/language/en-GB/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Username / Email", + "username": "Username", + "remember_me": "Remember Me?", + "forgot_password": "Forgot Password?", + "alternative_logins": "Alternative Logins", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "You have successfully logged in!", + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json new file mode 100644 index 0000000000..7abfb54d39 --- /dev/null +++ b/public/language/en-GB/modules.json @@ -0,0 +1,88 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Send", + "chat.no_active": "You have no active chats.", + "chat.user_typing": "%1 is typing ...", + "chat.user_has_messaged_you": "%1 has messaged you.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Please select a recipient to view chat message history", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Recent Chats", + "chat.contacts": "Contacts", + "chat.message-history": "Message History", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop out chat", + "chat.minimize": "Minimize", + "chat.maximize": "Maximize", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 said in %2:", + "composer.user_said": "%1 said:", + "composer.discard": "Are you sure you wish to discard this post?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + + + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/en-GB/notifications.json b/public/language/en-GB/notifications.json new file mode 100644 index 0000000000..c4beb75c30 --- /dev/null +++ b/public/language/en-GB/notifications.json @@ -0,0 +1,82 @@ +{ + "title": "Notifications", + "no_notifs": "You have no new notifications", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + + "back_to_home": "Back to %1", + "outgoing_link": "Outgoing Link", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "Continue to %1", + "return_to": "Return to %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "You have unread notifications.", + + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + + + "new_message_from": "New message from %1", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to" : "%1 has posted a reply to: %2", + "user_posted_to_dual" : "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple" : "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 has posted a new topic: %2", + "user_edited_post" : "%1 has edited a post in %2", + "user_started_following_you": "%1 started following you.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + + "email-confirmed": "Email Confirmed", + "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", + "email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.", + "email-confirm-sent": "Confirmation email sent.", + + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} diff --git a/public/language/en-GB/pages.json b/public/language/en-GB/pages.json new file mode 100644 index 0000000000..6586bf499c --- /dev/null +++ b/public/language/en-GB/pages.json @@ -0,0 +1,74 @@ +{ + "home": "Home", + "unread": "Unread Topics", + "popular-day": "Popular topics today", + "popular-week": "Popular topics this week", + "popular-month": "Popular topics this month", + "popular-alltime": "All time popular topics", + "recent": "Recent Topics", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + + "users/online": "Online Users", + "users/latest": "Latest Users", + "users/sort-posts": "Users with the most posts", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "User Search", + + "notifications": "Notifications", + "tags": "Tags", + "tag": "Topics tagged under "%1"", + "register": "Register an account", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Reset your account password", + "categories": "Categories", + + "groups": "Groups", + "group": "%1 group", + + "chats": "Chats", + "chat": "Chatting with %1", + + "flags": "Flags", + "flag-details": "Flag %1 Details", + + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "User Settings", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + + "confirm": "Email Confirmed", + + "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.", + "maintenance.messageIntro": "Additionally, the administrator has left this message:", + + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/en-GB/post-queue.json b/public/language/en-GB/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/en-GB/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/en-GB/recent.json b/public/language/en-GB/recent.json new file mode 100644 index 0000000000..835dfce296 --- /dev/null +++ b/public/language/en-GB/recent.json @@ -0,0 +1,24 @@ +{ + "title": "Recent", + "day": "Day", + "week": "Week", + "month": "Month", + "year": "Year", + "alltime": "All Time", + "no_recent_topics": "There are no recent topics.", + "no_popular_topics": "There are no popular topics.", + + "there-is-a-new-topic": "There is a new topic.", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", + + "there-are-new-topics": "There are %1 new topics.", + "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", + "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", + + "there-is-a-new-post": "There is a new post.", + "there-are-new-posts": "There are %1 new posts.", + + "click-here-to-reload": "Click here to reload." + +} \ No newline at end of file diff --git a/public/language/en-GB/register.json b/public/language/en-GB/register.json new file mode 100644 index 0000000000..58cba536f0 --- /dev/null +++ b/public/language/en-GB/register.json @@ -0,0 +1,33 @@ +{ + "register": "Register", + "cancel_registration": "Cancel Registration", + "help.email": "By default, your email will be hidden from the public.", + "help.username_restrictions": "A unique username between %1 and %2 characters. Others can mention you with @username.", + "help.minimum_password_length": "Your password's length must be at least %1 characters.", + "email_address": "Email Address", + "email_address_placeholder": "Enter Email Address", + "username": "Username", + "username_placeholder": "Enter Username", + "password": "Password", + "password_placeholder": "Enter Password", + "confirm_password": "Confirm Password", + "confirm_password_placeholder": "Confirm Password", + "register_now_button": "Register Now", + "alternative_registration": "Alternative Registration", + "terms_of_use": "Terms of Use", + "agree_to_terms_of_use": "I agree to the Terms of Use", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} diff --git a/public/language/en-GB/reset_password.json b/public/language/en-GB/reset_password.json new file mode 100644 index 0000000000..d02a9bfd3f --- /dev/null +++ b/public/language/en-GB/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Reset Password", + "update_password": "Update Password", + "password_changed.title": "Password Changed", + "password_changed.message": "

Password successfully reset, please log in again.", + "wrong_reset_code.title": "Incorrect Reset Code", + "wrong_reset_code.message": "The reset code received was incorrect. Please try again, or request a new reset code.", + "new_password": "New Password", + "repeat_password": "Confirm Password", + "changing_password": "Changing Password", + "enter_email": "Please enter your email address and we will send you an email with instructions on how to reset your account.", + "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.", + "invalid_email": "Invalid Email / Email does not exist!", + "password_too_short": "The password entered is too short, please pick a different password.", + "passwords_do_not_match": "The two passwords you've entered do not match.", + "password_expired": "Your password has expired, please choose a new password" +} \ No newline at end of file diff --git a/public/language/en-GB/search.json b/public/language/en-GB/search.json new file mode 100644 index 0000000000..ab710be4d1 --- /dev/null +++ b/public/language/en-GB/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No matches found", + "advanced-search": "Advanced Search", + "in": "In", + "titles": "Titles", + "titles-posts": "Titles and Posts", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Posted by", + "in-categories": "In Categories", + "search-child-categories": "Search child categories", + "has-tags": "Has tags", + "reply-count": "Reply Count", + "at-least": "At least", + "at-most": "At most", + "relevance": "Relevance", + "post-time": "Post time", + "votes": "Votes", + "newer-than": "Newer than", + "older-than": "Older than", + "any-date": "Any date", + "yesterday": "Yesterday", + "one-week": "One week", + "two-weeks": "Two weeks", + "one-month": "One month", + "three-months": "Three months", + "six-months": "Six months", + "one-year": "One year", + "sort-by": "Sort by", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "topic-votes": "Topic votes", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Username", + "category": "Category", + "descending": "In descending order", + "ascending": "In ascending order", + "save-preferences": "Save preferences", + "clear-preferences": "Clear preferences", + "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)", + "search-in-category": "Search in \"%1\"" +} diff --git a/public/language/en-GB/success.json b/public/language/en-GB/success.json new file mode 100644 index 0000000000..6923dd3b8a --- /dev/null +++ b/public/language/en-GB/success.json @@ -0,0 +1,7 @@ +{ + "success": "Success", + "topic-post": "You have successfully posted.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Authentication Successful", + "settings-saved": "Settings saved!" +} \ No newline at end of file diff --git a/public/language/en-GB/tags.json b/public/language/en-GB/tags.json new file mode 100644 index 0000000000..48516af490 --- /dev/null +++ b/public/language/en-GB/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "There are no topics with this tag.", + "tags": "Tags", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "Enter tags...", + "no_tags": "There are no tags yet.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/en-GB/top.json b/public/language/en-GB/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/en-GB/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json new file mode 100644 index 0000000000..7d0a2aae2d --- /dev/null +++ b/public/language/en-GB/topic.json @@ -0,0 +1,215 @@ +{ + "topic": "Topic", + "title": "Title", + + "no_topics_found": "No topics found!", + "no_posts_found": "No posts found!", + + "post_is_deleted": "This post is deleted!", + "topic_is_deleted": "This topic is deleted!", + + "profile": "Profile", + "posted_by": "Posted by %1", + "posted_by_guest": "Posted by Guest", + "chat": "Chat", + "notify_me": "Be notified of new replies in this topic", + "quote": "Quote", + "reply": "Reply", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log in to reply", + "login-to-view": "🔒 Log in to view", + "edit": "Edit", + "delete": "Delete", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Purge", + "restore": "Restore", + "move": "Move", + "change-owner": "Change Owner", + "fork": "Fork", + "link": "Link", + "share": "Share", + "tools": "Tools", + "locked": "Locked", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + + "bookmark_instructions" : "Click here to return to the last read post in this thread.", + + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + + "merged_message": "This topic has been merged into %2", + "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", + + "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + + "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", + + "markAsUnreadForAll.success" : "Topic marked as unread for all.", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + + "watch": "Watch", + "unwatch": "Unwatch", + "watch.title": "Be notified of new replies in this topic", + "unwatch.title": "Stop watching this topic", + "share_this_post": "Share this Post", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + + "thread_tools.title": "Topic Tools", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Pin Topic", + "thread_tools.unpin": "Unpin Topic", + "thread_tools.lock": "Lock Topic", + "thread_tools.unlock": "Unlock Topic", + "thread_tools.move": "Move Topic", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Move All", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Fork Topic", + "thread_tools.delete": "Delete Topic", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Are you sure you want to delete this topic?", + "thread_tools.restore": "Restore Topic", + "thread_tools.restore_confirm": "Are you sure you want to restore this topic?", + "thread_tools.purge": "Purge Topic", + "thread_tools.purge_confirm" : "Are you sure you want to purge this topic?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + + "post_delete_confirm": "Are you sure you want to delete this post?", + "post_restore_confirm": "Are you sure you want to restore this post?", + "post_purge_confirm": "Are you sure you want to purge this post?", + + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + + "load_categories": "Loading Categories", + "confirm_move": "Move", + "confirm_fork": "Fork", + + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + + "copy-permalink": "Copy Permalink", + + "loading_more_posts": "Loading More Posts", + "move_topic": "Move Topic", + "move_topics": "Move Topics", + "move_post": "Move Post", + "post_moved": "Post moved!", + "fork_topic": "Fork Topic", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Click the posts you want to fork", + "fork_no_pids": "No posts selected!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Successfully forked topic! Click here to go to the forked topic.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + + "composer.title_placeholder": "Enter your topic title here...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Discard", + "composer.submit": "Submit", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Replying to %1", + "composer.new_topic": "New Topic", + "composer.editing": "Editing", + + "composer.uploading": "uploading...", + "composer.thumb_url_label": "Paste a topic thumbnail URL", + "composer.thumb_title": "Add a thumbnail to this topic", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Or upload a file", + "composer.thumb_remove": "Clear fields", + "composer.drag_and_drop_images": "Drag and Drop Images Here", + + "more_users_and_guests": "%1 more user(s) and %2 guest(s)", + "more_users": "%1 more user(s)", + "more_guests": "%1 more guest(s)", + "users_and_others": "%1 and %2 others", + + "sort_by": "Sort by", + "oldest_to_newest": "Oldest to Newest", + "newest_to_oldest": "Newest to Oldest", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + + "link_back": "Re: [%1](%2)\n\n", + + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} diff --git a/public/language/en-GB/unread.json b/public/language/en-GB/unread.json new file mode 100644 index 0000000000..625852d998 --- /dev/null +++ b/public/language/en-GB/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Unread", + "no_unread_topics": "There are no unread topics.", + "load_more": "Load More", + "mark_as_read": "Mark as Read", + "selected": "Selected", + "all": "All", + "all_categories": "All categories", + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/en-GB/uploads.json b/public/language/en-GB/uploads.json new file mode 100644 index 0000000000..4aca2bce1e --- /dev/null +++ b/public/language/en-GB/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file" : "Uploading the file...", + "select-file-to-upload": "Select a file to upload!", + "upload-success": "File uploaded successfully!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json new file mode 100644 index 0000000000..c5bebf4b37 --- /dev/null +++ b/public/language/en-GB/user.json @@ -0,0 +1,221 @@ +{ + "banned": "Banned", + "muted": "Muted", + "offline": "Offline", + "deleted": "Deleted", + "username": "User Name", + "joindate": "Join Date", + "postcount": "Post Count", + + "email": "Email", + "confirm_email": "Confirm Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Account", + "ban_account_confirm": "Do you really want to ban this user?", + "unban_account": "Unban Account", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Delete Account", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Account deleted", + "account-content-deleted": "Account content deleted", + + "fullname": "Full Name", + "website": "Website", + "location": "Location", + "age": "Age", + "joined": "Joined", + "lastonline": "Last Online", + "profile": "Profile", + "profile_views": "Profile views", + "reputation": "Reputation", + "bookmarks":"Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Watched", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Followers", + "following": "Following", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "About me", + "signature": "Signature", + "birthday": "Birthday", + "chat": "Chat", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Follow", + "unfollow": "Unfollow", + "more": "More", + + "profile_update_success": "Profile has been updated successfully!", + "change_picture": "Change Picture", + "change_username": "Change Username", + "change_email": "Change Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Edit", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Uploaded Picture", + "upload_new_picture": "Upload New Picture", + "upload_new_picture_from_url": "Upload New Picture From URL", + "current_password": "Current Password", + "change_password": "Change Password", + "change_password_error": "Invalid Password!", + "change_password_error_wrong_current": "Your current password is not correct!", + "change_password_error_match": "Passwords must match!", + "change_password_error_privileges": "You do not have the rights to change this password.", + "change_password_success": "Your password is updated!", + "confirm_password": "Confirm Password", + "password": "Password", + "username_taken_workaround": "The username you requested was already taken, so we have altered it slightly. You are now known as %1", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + + "upload_picture": "Upload picture", + "upload_a_picture": "Upload a picture", + "remove_uploaded_picture" : "Remove Uploaded Picture", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + + "avatar-background-colour": "Avatar background colour", + + "settings": "Settings", + "show_email": "Show My Email", + "show_fullname": "Show My Full Name", + "restrict_chats": "Only allow chat messages from users I follow", + "digest_label": "Subscribe to Digest", + "digest_description": "Subscribe to email updates for this forum (new notifications and topics) according to a set schedule", + "digest_off": "Off", + "digest_daily": "Daily", + "digest_weekly": "Weekly", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Monthly", + + "has_no_follower": "This user doesn't have any followers :(", + "follows_no_one": "This user isn't following anyone :(", + "has_no_posts": "This user hasn't posted anything yet.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "This user hasn't posted any topics yet.", + "has_no_watched_topics": "This user hasn't watched any topics yet.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + + "email_hidden": "Email Hidden", + "hidden": "hidden", + + "paginate_description" : "Paginate topics and posts instead of using infinite scroll", + "topics_per_page": "Topics per Page", + "posts_per_page": "Posts per Page", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + + "browsing": "Browsing Settings", + "open_links_in_new_tab": "Open outgoing links in new tab", + + "enable_topic_searching": "Enable In-Topic Searching", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + + "select-skin": "Select a Skin", + + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + + "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.", + + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} diff --git a/public/language/en-GB/users.json b/public/language/en-GB/users.json new file mode 100644 index 0000000000..0da5fb8e49 --- /dev/null +++ b/public/language/en-GB/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Latest Users", + "top_posters": "Top Posters", + "most_reputation": "Most Reputation", + "most_flags": "Most Flags", + "search": "Search", + "enter_username": "Enter a username to search", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Load More", + "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", + "filter-by": "Filter By", + "online-only": "Online only", + "invite": "Invite", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "An invitation email has been sent to %1", + "user_list": "User List", + "recent_topics": "Recent Topics", + "popular_topics": "Popular Topics", + "unread_topics": "Unread Topics", + "categories": "Categories", + "tags": "Tags", + "no-users-found": "No users found!" +} \ No newline at end of file diff --git a/public/language/en-US/_DO_NOT_EDIT_FILES_HERE.md b/public/language/en-US/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/en-US/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/en-US/admin/admin.json b/public/language/en-US/admin/admin.json new file mode 100644 index 0000000000..39edffb66f --- /dev/null +++ b/public/language/en-US/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/en-US/admin/advanced/cache.json b/public/language/en-US/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/en-US/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/en-US/admin/advanced/database.json b/public/language/en-US/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/en-US/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/en-US/admin/advanced/errors.json b/public/language/en-US/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/en-US/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/en-US/admin/advanced/events.json b/public/language/en-US/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/en-US/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/en-US/admin/advanced/logs.json b/public/language/en-US/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/en-US/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/en-US/admin/appearance/customise.json b/public/language/en-US/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/en-US/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/en-US/admin/appearance/skins.json b/public/language/en-US/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/en-US/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/en-US/admin/appearance/themes.json b/public/language/en-US/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/en-US/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/en-US/admin/dashboard.json b/public/language/en-US/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/en-US/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/en-US/admin/development/info.json b/public/language/en-US/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/en-US/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/en-US/admin/development/logger.json b/public/language/en-US/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/en-US/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/en-US/admin/extend/plugins.json b/public/language/en-US/admin/extend/plugins.json new file mode 100644 index 0000000000..57bbfa6086 --- /dev/null +++ b/public/language/en-US/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialized before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/en-US/admin/extend/rewards.json b/public/language/en-US/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/en-US/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/en-US/admin/extend/widgets.json b/public/language/en-US/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/en-US/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/admins-mods.json b/public/language/en-US/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/en-US/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/categories.json b/public/language/en-US/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/en-US/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/digest.json b/public/language/en-US/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/en-US/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/en-US/admin/manage/groups.json b/public/language/en-US/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/en-US/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/privileges.json b/public/language/en-US/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/en-US/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/registration.json b/public/language/en-US/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/en-US/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/tags.json b/public/language/en-US/admin/manage/tags.json new file mode 100644 index 0000000000..6aa2236ac4 --- /dev/null +++ b/public/language/en-US/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Color", + "text-color": "Text Color", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/uploads.json b/public/language/en-US/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/en-US/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/en-US/admin/manage/users.json b/public/language/en-US/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/en-US/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/en-US/admin/menu.json b/public/language/en-US/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/en-US/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/advanced.json b/public/language/en-US/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/en-US/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/api.json b/public/language/en-US/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/en-US/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/chat.json b/public/language/en-US/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/en-US/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/cookies.json b/public/language/en-US/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/en-US/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/email.json b/public/language/en-US/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/en-US/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/en-US/admin/settings/general.json b/public/language/en-US/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/en-US/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/en-US/admin/settings/group.json b/public/language/en-US/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/en-US/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/guest.json b/public/language/en-US/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/en-US/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/homepage.json b/public/language/en-US/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/en-US/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/languages.json b/public/language/en-US/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/en-US/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/navigation.json b/public/language/en-US/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/en-US/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/en-US/admin/settings/notifications.json b/public/language/en-US/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/en-US/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/pagination.json b/public/language/en-US/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/en-US/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/post.json b/public/language/en-US/admin/settings/post.json new file mode 100644 index 0000000000..14e7cb12a3 --- /dev/null +++ b/public/language/en-US/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/reputation.json b/public/language/en-US/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/en-US/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/social.json b/public/language/en-US/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/en-US/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/sockets.json b/public/language/en-US/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/en-US/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/sounds.json b/public/language/en-US/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/en-US/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/tags.json b/public/language/en-US/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/en-US/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/en-US/admin/settings/uploads.json b/public/language/en-US/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/en-US/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/en-US/admin/settings/user.json b/public/language/en-US/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/en-US/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/en-US/admin/settings/web-crawler.json b/public/language/en-US/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/en-US/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/en-US/category.json b/public/language/en-US/category.json new file mode 100644 index 0000000000..2f9aed9cee --- /dev/null +++ b/public/language/en-US/category.json @@ -0,0 +1,23 @@ +{ + "category": "Category", + "subcategories": "Subcategories", + "new_topic_button": "New Topic", + "guest-login-post": "Log in to post", + "no_topics": "There are no topics in this category.
Why don't you try posting one?", + "browsing": "browsing", + "no_replies": "No one has replied", + "no_new_posts": "No new posts.", + "watch": "Watch", + "ignore": "Ignore", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Watched categories", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/en-US/email.json b/public/language/en-US/email.json new file mode 100644 index 0000000000..9f748a2e61 --- /dev/null +++ b/public/language/en-US/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Welcome to %1", + "invite": "Invitation from %1", + "greeting_no_name": "Hello", + "greeting_with_name": "Hello %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Thank you for registering with %1!", + "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "Click here to confirm your email address", + "invitation.text1": "%1 has invited you to join %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", + "reset.text2": "To continue with the password reset, please click on the following link:", + "reset.cta": "Click here to reset your password", + "reset.notify.subject": "Password successfully changed", + "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", + "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "digest.latest_topics": "Latest topics from %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Click here to visit %1", + "digest.unsub.info": "This digest was sent to you due to your subscription settings.", + "digest.day": "day", + "digest.week": "week", + "digest.month": "month", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "New chat message received from %1", + "notif.chat.cta": "Click here to continue the conversation", + "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", + "unsub.cta": "Click here to alter those settings", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Thanks!" +} \ No newline at end of file diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json new file mode 100644 index 0000000000..4191fad94f --- /dev/null +++ b/public/language/en-US/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Invalid Data", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "You don't seem to be logged in.", + "account-locked": "Your account has been locked temporarily", + "search-requires-login": "Searching requires an account - please login or register.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Invalid Category ID", + "invalid-tid": "Invalid Topic ID", + "invalid-pid": "Invalid Post ID", + "invalid-uid": "Invalid User ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Invalid Username", + "invalid-email": "Invalid Email", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Invalid User Data", + "invalid-password": "Invalid Password", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Please specify both a username and password", + "invalid-search-term": "Invalid search term", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "Username taken", + "email-taken": "Email taken", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "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-same-password": "Please use a password that is different from your current one", + "user-banned": "User banned", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Category does not exist", + "no-topic": "Topic does not exist", + "no-post": "Post does not exist", + "no-group": "Group does not exist", + "no-user": "User does not exist", + "no-teaser": "Teaser does not exist", + "no-flag": "Flag does not exist", + "no-privileges": "You do not have enough privileges for this action.", + "category-disabled": "Category disabled", + "topic-locked": "Topic Locked", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", + "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Please wait for uploads to complete.", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "You can't ban other admins!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Group name too short", + "group-name-too-long": "Group name too long", + "group-already-exists": "Group already exists", + "group-name-change-not-allowed": "Group name change not allowed", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "This post has already been deleted", + "post-already-restored": "This post has already been restored", + "topic-already-deleted": "This topic has already been deleted", + "topic-already-restored": "This topic has already been restored", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", + "invalid-file": "Invalid File", + "uploads-are-disabled": "Uploads are disabled", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "You can't chat with yourself!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", + "registration-error": "Registration Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/en-US/flags.json b/public/language/en-US/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/en-US/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/en-US/global.json b/public/language/en-US/global.json new file mode 100644 index 0000000000..9cb37d6e1a --- /dev/null +++ b/public/language/en-US/global.json @@ -0,0 +1,126 @@ +{ + "home": "Home", + "search": "Search", + "buttons.close": "Close", + "403.title": "Access Denied", + "403.message": "You seem to have stumbled upon a page that you do not have access to.", + "403.login": "Perhaps you should try logging in?", + "404.title": "Not Found", + "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "500.title": "Internal Error.", + "500.message": "Oops! Looks like something went wrong!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Register", + "login": "Login", + "please_log_in": "Please Log In", + "logout": "Logout", + "posting_restriction_info": "Posting is currently restricted to registered members only, click here to log in.", + "welcome_back": "Welcome Back", + "you_have_successfully_logged_in": "You have successfully logged in", + "save_changes": "Save Changes", + "save": "Save", + "close": "Close", + "pagination": "Pagination", + "pagination.out_of": "%1 out of %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Categories", + "header.recent": "Recent", + "header.unread": "Unread", + "header.tags": "Tags", + "header.popular": "Popular", + "header.top": "Top", + "header.users": "Users", + "header.groups": "Groups", + "header.chats": "Chats", + "header.notifications": "Notifications", + "header.search": "Search", + "header.profile": "Profile", + "header.navigation": "Navigation", + "notifications.loading": "Loading Notifications", + "chats.loading": "Loading Chats", + "motd.welcome": "Welcome to NodeBB, the discussion platform of the future.", + "previouspage": "Previous Page", + "nextpage": "Next Page", + "alert.success": "Success", + "alert.error": "Error", + "alert.banned": "Banned", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "You are no longer following %1!", + "alert.follow": "You are now following %1!", + "users": "Users", + "topics": "Topics", + "posts": "Posts", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Views", + "posters": "Posters", + "reputation": "Reputation", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "read more", + "more": "More", + "none": "None", + "posted_ago_by_guest": "posted %1 by Guest", + "posted_ago_by": "posted %1 by %2", + "posted_ago": "posted %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "posted in %1 %2", + "posted_in_ago_by": "posted in %1 %2 by %3", + "user_posted_ago": "%1 posted %2", + "guest_posted_ago": "Guest posted %1", + "last_edited_by": "last edited by %1", + "norecentposts": "No Recent Posts", + "norecenttopics": "No Recent Topics", + "recentposts": "Recent Posts", + "recentips": "Recently Logged In IPs", + "moderator_tools": "Moderator Tools", + "online": "Online", + "away": "Away", + "dnd": "Do not disturb", + "invisible": "Invisible", + "offline": "Offline", + "email": "Email", + "language": "Language", + "guest": "Guest", + "guests": "Guests", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum Updated", + "updated.message": "This forum has just been updated to the latest version. Click here to refresh the page.", + "privacy": "Privacy", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "Delete All", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/en-US/groups.json b/public/language/en-US/groups.json new file mode 100644 index 0000000000..2072d52894 --- /dev/null +++ b/public/language/en-US/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Groups", + "view_group": "View Group", + "owner": "Group Owner", + "new_group": "Create New Group", + "no_groups_found": "There are no groups to see", + "pending.accept": "Accept", + "pending.reject": "Reject", + "pending.accept_all": "Accept All", + "pending.reject_all": "Reject All", + "pending.none": "There are no pending members at this time", + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Save", + "cover-saving": "Saving", + "details.title": "Group Details", + "details.members": "Member List", + "details.pending": "Pending Members", + "details.invited": "Invited Members", + "details.has_no_posts": "This group's members have not made any posts.", + "details.latest_posts": "Latest Posts", + "details.private": "Private", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.member_count": "Member Count", + "details.creation_date": "Creation Date", + "details.description": "Description", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Badge Text", + "details.userTitleEnabled": "Show Badge", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Delete Group", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted", + "membership.accept-invitation": "Accept Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitation Pending", + "membership.join-group": "Join Group", + "membership.leave-group": "Leave Group", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Reject", + "new-group.group_name": "Group Name:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/en-US/ip-blacklist.json b/public/language/en-US/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/en-US/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/en-US/language.json b/public/language/en-US/language.json new file mode 100644 index 0000000000..0967664491 --- /dev/null +++ b/public/language/en-US/language.json @@ -0,0 +1,5 @@ +{ + "name": "English (United States)", + "code": "en-US", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/en-US/login.json b/public/language/en-US/login.json new file mode 100644 index 0000000000..161982bdfa --- /dev/null +++ b/public/language/en-US/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Username / Email", + "username": "Username", + "remember_me": "Remember Me?", + "forgot_password": "Forgot Password?", + "alternative_logins": "Alternative Logins", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "You have successfully logged in!", + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/en-US/modules.json b/public/language/en-US/modules.json new file mode 100644 index 0000000000..1e58a4e2ef --- /dev/null +++ b/public/language/en-US/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Send", + "chat.no_active": "You have no active chats.", + "chat.user_typing": "%1 is typing ...", + "chat.user_has_messaged_you": "%1 has messaged you.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Please select a recipient to view chat message history", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Recent Chats", + "chat.contacts": "Contacts", + "chat.message-history": "Message History", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop out chat", + "chat.minimize": "Minimize", + "chat.maximize": "Maximize", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 said in %2:", + "composer.user_said": "%1 said:", + "composer.discard": "Are you sure you wish to discard this post?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/en-US/notifications.json b/public/language/en-US/notifications.json new file mode 100644 index 0000000000..e5a6012c13 --- /dev/null +++ b/public/language/en-US/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notifications", + "no_notifs": "You have no new notifications", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Back to %1", + "outgoing_link": "Outgoing Link", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "Continue to %1", + "return_to": "Return to %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "You have unread notifications.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "New message from %1", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 has posted a reply to: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 has posted a new topic: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 started following you.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email Confirmed", + "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", + "email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.", + "email-confirm-sent": "Confirmation email sent.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/en-US/pages.json b/public/language/en-US/pages.json new file mode 100644 index 0000000000..a885434c71 --- /dev/null +++ b/public/language/en-US/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Home", + "unread": "Unread Topics", + "popular-day": "Popular topics today", + "popular-week": "Popular topics this week", + "popular-month": "Popular topics this month", + "popular-alltime": "All time popular topics", + "recent": "Recent Topics", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Online Users", + "users/latest": "Latest Users", + "users/sort-posts": "Users with the most posts", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "User Search", + "notifications": "Notifications", + "tags": "Tags", + "tag": "Topics tagged under "%1"", + "register": "Register an account", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Reset your account password", + "categories": "Categories", + "groups": "Groups", + "group": "%1 group", + "chats": "Chats", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "User Settings", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Confirmed", + "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.", + "maintenance.messageIntro": "Additionally, the administrator has left this message:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/en-US/post-queue.json b/public/language/en-US/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/en-US/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/en-US/recent.json b/public/language/en-US/recent.json new file mode 100644 index 0000000000..f638d2f8ac --- /dev/null +++ b/public/language/en-US/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recent", + "day": "Day", + "week": "Week", + "month": "Month", + "year": "Year", + "alltime": "All Time", + "no_recent_topics": "There are no recent topics.", + "no_popular_topics": "There are no popular topics.", + "there-is-a-new-topic": "There is a new topic.", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", + "there-are-new-topics": "There are %1 new topics.", + "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", + "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", + "there-is-a-new-post": "There is a new post.", + "there-are-new-posts": "There are %1 new posts.", + "click-here-to-reload": "Click here to reload." +} \ No newline at end of file diff --git a/public/language/en-US/register.json b/public/language/en-US/register.json new file mode 100644 index 0000000000..57fd61b645 --- /dev/null +++ b/public/language/en-US/register.json @@ -0,0 +1,32 @@ +{ + "register": "Register", + "cancel_registration": "Cancel Registration", + "help.email": "By default, your email will be hidden from the public.", + "help.username_restrictions": "A unique username between %1 and %2 characters. Others can mention you with @username.", + "help.minimum_password_length": "Your password's length must be at least %1 characters.", + "email_address": "Email Address", + "email_address_placeholder": "Enter Email Address", + "username": "Username", + "username_placeholder": "Enter Username", + "password": "Password", + "password_placeholder": "Enter Password", + "confirm_password": "Confirm Password", + "confirm_password_placeholder": "Confirm Password", + "register_now_button": "Register Now", + "alternative_registration": "Alternative Registration", + "terms_of_use": "Terms of Use", + "agree_to_terms_of_use": "I agree to the Terms of Use", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/en-US/reset_password.json b/public/language/en-US/reset_password.json new file mode 100644 index 0000000000..8643f923bc --- /dev/null +++ b/public/language/en-US/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Reset Password", + "update_password": "Update Password", + "password_changed.title": "Password Changed", + "password_changed.message": "

Password successfully reset, please log in again.", + "wrong_reset_code.title": "Incorrect Reset Code", + "wrong_reset_code.message": "The reset code received was incorrect. Please try again, or request a new reset code.", + "new_password": "New Password", + "repeat_password": "Confirm Password", + "changing_password": "Changing Password", + "enter_email": "Please enter your email address and we will send you an email with instructions on how to reset your account.", + "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.", + "invalid_email": "Invalid Email / Email does not exist!", + "password_too_short": "The password entered is too short, please pick a different password.", + "passwords_do_not_match": "The two passwords you've entered do not match.", + "password_expired": "Your password has expired, please choose a new password" +} \ No newline at end of file diff --git a/public/language/en-US/search.json b/public/language/en-US/search.json new file mode 100644 index 0000000000..639cc0b653 --- /dev/null +++ b/public/language/en-US/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No matches found", + "advanced-search": "Advanced Search", + "in": "In", + "titles": "Titles", + "titles-posts": "Titles and Posts", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Posted by", + "in-categories": "In Categories", + "search-child-categories": "Search child categories", + "has-tags": "Has tags", + "reply-count": "Reply Count", + "at-least": "At least", + "at-most": "At most", + "relevance": "Relevance", + "post-time": "Post time", + "votes": "Votes", + "newer-than": "Newer than", + "older-than": "Older than", + "any-date": "Any date", + "yesterday": "Yesterday", + "one-week": "One week", + "two-weeks": "Two weeks", + "one-month": "One month", + "three-months": "Three months", + "six-months": "Six months", + "one-year": "One year", + "sort-by": "Sort by", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "topic-votes": "Topic votes", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Username", + "category": "Category", + "descending": "In descending order", + "ascending": "In ascending order", + "save-preferences": "Save preferences", + "clear-preferences": "Clear preferences", + "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)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/en-US/success.json b/public/language/en-US/success.json new file mode 100644 index 0000000000..7fa5550915 --- /dev/null +++ b/public/language/en-US/success.json @@ -0,0 +1,7 @@ +{ + "success": "Success", + "topic-post": "You have successfully posted.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Authentication Successful", + "settings-saved": "Settings saved!" +} \ No newline at end of file diff --git a/public/language/en-US/tags.json b/public/language/en-US/tags.json new file mode 100644 index 0000000000..24ca6f8a39 --- /dev/null +++ b/public/language/en-US/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "There are no topics with this tag.", + "tags": "Tags", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "Enter tags...", + "no_tags": "There are no tags yet.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/en-US/top.json b/public/language/en-US/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/en-US/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/en-US/topic.json b/public/language/en-US/topic.json new file mode 100644 index 0000000000..a275204aa2 --- /dev/null +++ b/public/language/en-US/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Topic", + "title": "Title", + "no_topics_found": "No topics found!", + "no_posts_found": "No posts found!", + "post_is_deleted": "This post is deleted!", + "topic_is_deleted": "This topic is deleted!", + "profile": "Profile", + "posted_by": "Posted by %1", + "posted_by_guest": "Posted by Guest", + "chat": "Chat", + "notify_me": "Be notified of new replies in this topic", + "quote": "Quote", + "reply": "Reply", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log in to reply", + "login-to-view": "🔒 Log in to view", + "edit": "Edit", + "delete": "Delete", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Purge", + "restore": "Restore", + "move": "Move", + "change-owner": "Change Owner", + "fork": "Fork", + "link": "Link", + "share": "Share", + "tools": "Tools", + "locked": "Locked", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", + "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", + "markAsUnreadForAll.success": "Topic marked as unread for all.", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "Watch", + "unwatch": "Unwatch", + "watch.title": "Be notified of new replies in this topic", + "unwatch.title": "Stop watching this topic", + "share_this_post": "Share this Post", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Topic Tools", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Pin Topic", + "thread_tools.unpin": "Unpin Topic", + "thread_tools.lock": "Lock Topic", + "thread_tools.unlock": "Unlock Topic", + "thread_tools.move": "Move Topic", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Move All", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Fork Topic", + "thread_tools.delete": "Delete Topic", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Are you sure you want to delete this topic?", + "thread_tools.restore": "Restore Topic", + "thread_tools.restore_confirm": "Are you sure you want to restore this topic?", + "thread_tools.purge": "Purge Topic", + "thread_tools.purge_confirm": "Are you sure you want to purge this topic?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Are you sure you want to delete this post?", + "post_restore_confirm": "Are you sure you want to restore this post?", + "post_purge_confirm": "Are you sure you want to purge this post?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Loading Categories", + "confirm_move": "Move", + "confirm_fork": "Fork", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Loading More Posts", + "move_topic": "Move Topic", + "move_topics": "Move Topics", + "move_post": "Move Post", + "post_moved": "Post moved!", + "fork_topic": "Fork Topic", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Click the posts you want to fork", + "fork_no_pids": "No posts selected!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Successfully forked topic! Click here to go to the forked topic.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Enter your topic title here...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Discard", + "composer.submit": "Submit", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Replying to %1", + "composer.new_topic": "New Topic", + "composer.editing": "Editing", + "composer.uploading": "uploading...", + "composer.thumb_url_label": "Paste a topic thumbnail URL", + "composer.thumb_title": "Add a thumbnail to this topic", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Or upload a file", + "composer.thumb_remove": "Clear fields", + "composer.drag_and_drop_images": "Drag and Drop Images Here", + "more_users_and_guests": "%1 more user(s) and %2 guest(s)", + "more_users": "%1 more user(s)", + "more_guests": "%1 more guest(s)", + "users_and_others": "%1 and %2 others", + "sort_by": "Sort by", + "oldest_to_newest": "Oldest to Newest", + "newest_to_oldest": "Newest to Oldest", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/en-US/unread.json b/public/language/en-US/unread.json new file mode 100644 index 0000000000..e7aff0b4aa --- /dev/null +++ b/public/language/en-US/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Unread", + "no_unread_topics": "There are no unread topics.", + "load_more": "Load More", + "mark_as_read": "Mark as Read", + "selected": "Selected", + "all": "All", + "all_categories": "All categories", + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/en-US/uploads.json b/public/language/en-US/uploads.json new file mode 100644 index 0000000000..651a839876 --- /dev/null +++ b/public/language/en-US/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Uploading the file...", + "select-file-to-upload": "Select a file to upload!", + "upload-success": "File uploaded successfully!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/en-US/user.json b/public/language/en-US/user.json new file mode 100644 index 0000000000..ec5f3bd60e --- /dev/null +++ b/public/language/en-US/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banned", + "muted": "Muted", + "offline": "Offline", + "deleted": "Deleted", + "username": "User Name", + "joindate": "Join Date", + "postcount": "Post Count", + "email": "Email", + "confirm_email": "Confirm Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Account", + "ban_account_confirm": "Do you really want to ban this user?", + "unban_account": "Unban Account", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Delete Account", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Account deleted", + "account-content-deleted": "Account content deleted", + "fullname": "Full Name", + "website": "Website", + "location": "Location", + "age": "Age", + "joined": "Joined", + "lastonline": "Last Online", + "profile": "Profile", + "profile_views": "Profile views", + "reputation": "Reputation", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Watched", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Followers", + "following": "Following", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "About me", + "signature": "Signature", + "birthday": "Birthday", + "chat": "Chat", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Follow", + "unfollow": "Unfollow", + "more": "More", + "profile_update_success": "Profile has been updated successfully!", + "change_picture": "Change Picture", + "change_username": "Change Username", + "change_email": "Change Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Edit", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Uploaded Picture", + "upload_new_picture": "Upload New Picture", + "upload_new_picture_from_url": "Upload New Picture From URL", + "current_password": "Current Password", + "change_password": "Change Password", + "change_password_error": "Invalid Password!", + "change_password_error_wrong_current": "Your current password is not correct!", + "change_password_error_match": "Passwords must match!", + "change_password_error_privileges": "You do not have the rights to change this password.", + "change_password_success": "Your password is updated!", + "confirm_password": "Confirm Password", + "password": "Password", + "username_taken_workaround": "The username you requested was already taken, so we have altered it slightly. You are now known as %1", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "Upload picture", + "upload_a_picture": "Upload a picture", + "remove_uploaded_picture": "Remove Uploaded Picture", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Settings", + "show_email": "Show My Email", + "show_fullname": "Show My Full Name", + "restrict_chats": "Only allow chat messages from users I follow", + "digest_label": "Subscribe to Digest", + "digest_description": "Subscribe to email updates for this forum (new notifications and topics) according to a set schedule", + "digest_off": "Off", + "digest_daily": "Daily", + "digest_weekly": "Weekly", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Monthly", + "has_no_follower": "This user doesn't have any followers :(", + "follows_no_one": "This user isn't following anyone :(", + "has_no_posts": "This user hasn't posted anything yet.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "This user hasn't posted any topics yet.", + "has_no_watched_topics": "This user hasn't watched any topics yet.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Email Hidden", + "hidden": "hidden", + "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "topics_per_page": "Topics per Page", + "posts_per_page": "Posts per Page", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Browsing Settings", + "open_links_in_new_tab": "Open outgoing links in new tab", + "enable_topic_searching": "Enable In-Topic Searching", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behavior and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + "select-skin": "Select a Skin", + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/en-US/users.json b/public/language/en-US/users.json new file mode 100644 index 0000000000..8ddd34e5d7 --- /dev/null +++ b/public/language/en-US/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Latest Users", + "top_posters": "Top Posters", + "most_reputation": "Most Reputation", + "most_flags": "Most Flags", + "search": "Search", + "enter_username": "Enter a username to search", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Load More", + "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", + "filter-by": "Filter By", + "online-only": "Online only", + "invite": "Invite", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "An invitation email has been sent to %1", + "user_list": "User List", + "recent_topics": "Recent Topics", + "popular_topics": "Popular Topics", + "unread_topics": "Unread Topics", + "categories": "Categories", + "tags": "Tags", + "no-users-found": "No users found!" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/_DO_NOT_EDIT_FILES_HERE.md b/public/language/en-x-pirate/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/en-x-pirate/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/admin.json b/public/language/en-x-pirate/admin/admin.json new file mode 100644 index 0000000000..39edffb66f --- /dev/null +++ b/public/language/en-x-pirate/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/advanced/cache.json b/public/language/en-x-pirate/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/en-x-pirate/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/advanced/database.json b/public/language/en-x-pirate/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/en-x-pirate/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/en-x-pirate/admin/advanced/errors.json b/public/language/en-x-pirate/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/en-x-pirate/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/advanced/events.json b/public/language/en-x-pirate/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/en-x-pirate/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/advanced/logs.json b/public/language/en-x-pirate/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/en-x-pirate/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/appearance/customise.json b/public/language/en-x-pirate/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/en-x-pirate/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/appearance/skins.json b/public/language/en-x-pirate/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/en-x-pirate/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/appearance/themes.json b/public/language/en-x-pirate/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/en-x-pirate/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/dashboard.json b/public/language/en-x-pirate/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/en-x-pirate/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/en-x-pirate/admin/development/info.json b/public/language/en-x-pirate/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/en-x-pirate/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/development/logger.json b/public/language/en-x-pirate/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/en-x-pirate/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/extend/plugins.json b/public/language/en-x-pirate/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/en-x-pirate/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/en-x-pirate/admin/extend/rewards.json b/public/language/en-x-pirate/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/en-x-pirate/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/extend/widgets.json b/public/language/en-x-pirate/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/en-x-pirate/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/admins-mods.json b/public/language/en-x-pirate/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/categories.json b/public/language/en-x-pirate/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/digest.json b/public/language/en-x-pirate/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/en-x-pirate/admin/manage/groups.json b/public/language/en-x-pirate/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/privileges.json b/public/language/en-x-pirate/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/registration.json b/public/language/en-x-pirate/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/tags.json b/public/language/en-x-pirate/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/uploads.json b/public/language/en-x-pirate/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/users.json b/public/language/en-x-pirate/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/en-x-pirate/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/menu.json b/public/language/en-x-pirate/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/en-x-pirate/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/advanced.json b/public/language/en-x-pirate/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/api.json b/public/language/en-x-pirate/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/chat.json b/public/language/en-x-pirate/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/cookies.json b/public/language/en-x-pirate/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/email.json b/public/language/en-x-pirate/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/en-x-pirate/admin/settings/general.json b/public/language/en-x-pirate/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/en-x-pirate/admin/settings/group.json b/public/language/en-x-pirate/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/guest.json b/public/language/en-x-pirate/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/homepage.json b/public/language/en-x-pirate/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/languages.json b/public/language/en-x-pirate/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/navigation.json b/public/language/en-x-pirate/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/en-x-pirate/admin/settings/notifications.json b/public/language/en-x-pirate/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/pagination.json b/public/language/en-x-pirate/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/post.json b/public/language/en-x-pirate/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/reputation.json b/public/language/en-x-pirate/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/social.json b/public/language/en-x-pirate/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/sockets.json b/public/language/en-x-pirate/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/sounds.json b/public/language/en-x-pirate/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/tags.json b/public/language/en-x-pirate/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/settings/uploads.json b/public/language/en-x-pirate/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/en-x-pirate/admin/settings/user.json b/public/language/en-x-pirate/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/en-x-pirate/admin/settings/web-crawler.json b/public/language/en-x-pirate/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/en-x-pirate/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/category.json b/public/language/en-x-pirate/category.json new file mode 100644 index 0000000000..6691e3e56f --- /dev/null +++ b/public/language/en-x-pirate/category.json @@ -0,0 +1,23 @@ +{ + "category": "Category", + "subcategories": "Subcategories", + "new_topic_button": "New Topic", + "guest-login-post": "Log in to post", + "no_topics": "Thar be no topics in 'tis category.
Why don't ye give a go' postin' one?", + "browsing": "browsin'", + "no_replies": "No one has replied to ye message", + "no_new_posts": "Thar be no new posts.", + "watch": "Be watchin'", + "ignore": "Be ignorin'", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Categories ye be watchin'", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/email.json b/public/language/en-x-pirate/email.json new file mode 100644 index 0000000000..fe7ea95293 --- /dev/null +++ b/public/language/en-x-pirate/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Ahoy thar %1!", + "invite": "Ye be invited by %1", + "greeting_no_name": "Hello", + "greeting_with_name": "Hello %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Thank you for registering with %1!", + "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "Click here to confirm your email address", + "invitation.text1": "%1 be invitin' ye to join %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", + "reset.text2": "To continue with the password reset, please click on the following link:", + "reset.cta": "Click here to reset your password", + "reset.notify.subject": "Password successfully changed", + "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", + "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "digest.latest_topics": "Latest topics from %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Click here to visit %1", + "digest.unsub.info": "This digest was sent to you due to your subscription settings.", + "digest.day": "day", + "digest.week": "week", + "digest.month": "month", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "New chat message received from %1", + "notif.chat.cta": "Click here to continue the conversation", + "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", + "unsub.cta": "Click here to alter those settings", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Thanks!" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json new file mode 100644 index 0000000000..4191fad94f --- /dev/null +++ b/public/language/en-x-pirate/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Invalid Data", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "You don't seem to be logged in.", + "account-locked": "Your account has been locked temporarily", + "search-requires-login": "Searching requires an account - please login or register.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Invalid Category ID", + "invalid-tid": "Invalid Topic ID", + "invalid-pid": "Invalid Post ID", + "invalid-uid": "Invalid User ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Invalid Username", + "invalid-email": "Invalid Email", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Invalid User Data", + "invalid-password": "Invalid Password", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Please specify both a username and password", + "invalid-search-term": "Invalid search term", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "Username taken", + "email-taken": "Email taken", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "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-same-password": "Please use a password that is different from your current one", + "user-banned": "User banned", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Category does not exist", + "no-topic": "Topic does not exist", + "no-post": "Post does not exist", + "no-group": "Group does not exist", + "no-user": "User does not exist", + "no-teaser": "Teaser does not exist", + "no-flag": "Flag does not exist", + "no-privileges": "You do not have enough privileges for this action.", + "category-disabled": "Category disabled", + "topic-locked": "Topic Locked", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", + "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Please wait for uploads to complete.", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "You can't ban other admins!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Group name too short", + "group-name-too-long": "Group name too long", + "group-already-exists": "Group already exists", + "group-name-change-not-allowed": "Group name change not allowed", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "This post has already been deleted", + "post-already-restored": "This post has already been restored", + "topic-already-deleted": "This topic has already been deleted", + "topic-already-restored": "This topic has already been restored", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", + "invalid-file": "Invalid File", + "uploads-are-disabled": "Uploads are disabled", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "You can't chat with yourself!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", + "registration-error": "Registration Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/flags.json b/public/language/en-x-pirate/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/en-x-pirate/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/global.json b/public/language/en-x-pirate/global.json new file mode 100644 index 0000000000..eaec53c49a --- /dev/null +++ b/public/language/en-x-pirate/global.json @@ -0,0 +1,126 @@ +{ + "home": "Home Port", + "search": "Finderer", + "buttons.close": "Shoot down", + "403.title": "Not Enough Booty Power", + "403.message": "You seem to have stumbled upon a page that you do not have access to.", + "403.login": "Perhaps you should try logging in?", + "404.title": "T'ere be nut'in 'ere", + "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "500.title": "Internal Error.", + "500.message": "Looks like we've got somethin' in th' sails.", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Register", + "login": "Login", + "please_log_in": "Please Log In", + "logout": "Logout", + "posting_restriction_info": "Postin' be currently restricted to registered members only, click here to log in.", + "welcome_back": "Welcome Back", + "you_have_successfully_logged_in": "Ye have successfully logged in", + "save_changes": "Save yer Changes", + "save": "Save", + "close": "Shoot down", + "pagination": "Pagination", + "pagination.out_of": "%1 out of %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Captains only", + "header.categories": "Categories", + "header.recent": "Recent", + "header.unread": "Undiscovered", + "header.tags": "Tags", + "header.popular": "Famous", + "header.top": "Top", + "header.users": "Mates", + "header.groups": "Groups", + "header.chats": "Yik-Yaks", + "header.notifications": "Parrot Calls", + "header.search": "Finderer", + "header.profile": "Bunk", + "header.navigation": "Navigation", + "notifications.loading": "Fetching yer Parrot Calls", + "chats.loading": "Loading Yik-Yaks", + "motd.welcome": "Welcome to NodeBB, th' discussion platform 'o th' future.", + "previouspage": "Previous Page", + "nextpage": "Next Page", + "alert.success": "Success", + "alert.error": "Somethin' broke", + "alert.banned": "Exiled", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Ye be no longer stalkin' %1!", + "alert.follow": "Ye be stalkin' %1", + "users": "Users", + "topics": "Topics", + "posts": "Messages", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Views", + "posters": "Posters", + "reputation": "Reputation", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "read more", + "more": "More", + "none": "None", + "posted_ago_by_guest": "posted %1 by Guest", + "posted_ago_by": "posted %1 by %2", + "posted_ago": "posted %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "posted in %1 %2", + "posted_in_ago_by": "posted in %1 %2 by %3", + "user_posted_ago": "%1 posted %2", + "guest_posted_ago": "Guest posted %1", + "last_edited_by": "last edited by %1", + "norecentposts": "No Recent Posts", + "norecenttopics": "No Recent Topics", + "recentposts": "Recent Messages", + "recentips": "Recently Logged In IPs", + "moderator_tools": "Moderator Tools", + "online": "Available", + "away": "Out to sea", + "dnd": "Do not disturb", + "invisible": "Magic usin'", + "offline": "Dead", + "email": "Email", + "language": "Language", + "guest": "Guest", + "guests": "Guests", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum Updated", + "updated.message": "This forum has just been updated to the latest version. Click here to refresh the page.", + "privacy": "Privacy", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "Delete All", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/groups.json b/public/language/en-x-pirate/groups.json new file mode 100644 index 0000000000..2072d52894 --- /dev/null +++ b/public/language/en-x-pirate/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Groups", + "view_group": "View Group", + "owner": "Group Owner", + "new_group": "Create New Group", + "no_groups_found": "There are no groups to see", + "pending.accept": "Accept", + "pending.reject": "Reject", + "pending.accept_all": "Accept All", + "pending.reject_all": "Reject All", + "pending.none": "There are no pending members at this time", + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Save", + "cover-saving": "Saving", + "details.title": "Group Details", + "details.members": "Member List", + "details.pending": "Pending Members", + "details.invited": "Invited Members", + "details.has_no_posts": "This group's members have not made any posts.", + "details.latest_posts": "Latest Posts", + "details.private": "Private", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.member_count": "Member Count", + "details.creation_date": "Creation Date", + "details.description": "Description", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Badge Text", + "details.userTitleEnabled": "Show Badge", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Delete Group", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted", + "membership.accept-invitation": "Accept Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitation Pending", + "membership.join-group": "Join Group", + "membership.leave-group": "Leave Group", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Reject", + "new-group.group_name": "Group Name:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/ip-blacklist.json b/public/language/en-x-pirate/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/en-x-pirate/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/language.json b/public/language/en-x-pirate/language.json new file mode 100644 index 0000000000..34b9317fb5 --- /dev/null +++ b/public/language/en-x-pirate/language.json @@ -0,0 +1,5 @@ +{ + "name": "English (Pirate)", + "code": "en-x-pirate", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/login.json b/public/language/en-x-pirate/login.json new file mode 100644 index 0000000000..6327a0b815 --- /dev/null +++ b/public/language/en-x-pirate/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Username / Email", + "username": "Username", + "remember_me": "Remember Me?", + "forgot_password": "My mind be a scatt'rbrain, help a matey out!", + "alternative_logins": "Oth'r gangplanks", + "failed_login_attempt": "Ye be refused boardin'", + "login_successful": "Welcome on board, matey!", + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/modules.json b/public/language/en-x-pirate/modules.json new file mode 100644 index 0000000000..e5ca23775a --- /dev/null +++ b/public/language/en-x-pirate/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Send Parrot", + "chat.no_active": "Ye be a lonely sailor.", + "chat.user_typing": "%1 is typing ...", + "chat.user_has_messaged_you": "%1 has messaged you.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Please select a recipient to view chat message history", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Recent Chats", + "chat.contacts": "Contacts", + "chat.message-history": "Message History", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop out chat", + "chat.minimize": "Minimize", + "chat.maximize": "Maximize", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 said in %2:", + "composer.user_said": "%1 said:", + "composer.discard": "Are you sure you wish to discard this post?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/notifications.json b/public/language/en-x-pirate/notifications.json new file mode 100644 index 0000000000..df401190e2 --- /dev/null +++ b/public/language/en-x-pirate/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notifications", + "no_notifs": "You have no new notifications", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Back to %1", + "outgoing_link": "Go offshore", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "Continue to %1", + "return_to": "Return to %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "You have unread notifications.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "New message from %1", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 has posted a reply to: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 has posted a new topic: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 started following you.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email Confirmed", + "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", + "email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.", + "email-confirm-sent": "Confirmation email sent.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/pages.json b/public/language/en-x-pirate/pages.json new file mode 100644 index 0000000000..a885434c71 --- /dev/null +++ b/public/language/en-x-pirate/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Home", + "unread": "Unread Topics", + "popular-day": "Popular topics today", + "popular-week": "Popular topics this week", + "popular-month": "Popular topics this month", + "popular-alltime": "All time popular topics", + "recent": "Recent Topics", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Online Users", + "users/latest": "Latest Users", + "users/sort-posts": "Users with the most posts", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "User Search", + "notifications": "Notifications", + "tags": "Tags", + "tag": "Topics tagged under "%1"", + "register": "Register an account", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Reset your account password", + "categories": "Categories", + "groups": "Groups", + "group": "%1 group", + "chats": "Chats", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "User Settings", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Confirmed", + "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.", + "maintenance.messageIntro": "Additionally, the administrator has left this message:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/post-queue.json b/public/language/en-x-pirate/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/en-x-pirate/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/recent.json b/public/language/en-x-pirate/recent.json new file mode 100644 index 0000000000..791760308e --- /dev/null +++ b/public/language/en-x-pirate/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recent", + "day": "Day", + "week": "Week", + "month": "Month", + "year": "Year", + "alltime": "All Time", + "no_recent_topics": "There be no recent topics.", + "no_popular_topics": "There are no popular topics.", + "there-is-a-new-topic": "There is a new topic.", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", + "there-are-new-topics": "There are %1 new topics.", + "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", + "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", + "there-is-a-new-post": "There is a new post.", + "there-are-new-posts": "There are %1 new posts.", + "click-here-to-reload": "Click here to reload." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/register.json b/public/language/en-x-pirate/register.json new file mode 100644 index 0000000000..8bf995da08 --- /dev/null +++ b/public/language/en-x-pirate/register.json @@ -0,0 +1,32 @@ +{ + "register": "Board the ship", + "cancel_registration": "Cancel Registration", + "help.email": "By default, your email will be hidden from the public.", + "help.username_restrictions": "A unique username between %1 and %2 characters. Others can mention you with @username.", + "help.minimum_password_length": "Your password's length must be at least %1 characters.", + "email_address": "Email Address", + "email_address_placeholder": "Enter Email Address", + "username": "Username", + "username_placeholder": "Enter Username", + "password": "Password", + "password_placeholder": "Enter Password", + "confirm_password": "Confirm Password", + "confirm_password_placeholder": "Confirm Password", + "register_now_button": "Register Now", + "alternative_registration": "Alternative Registration", + "terms_of_use": "Terms of Use", + "agree_to_terms_of_use": "I agree to the Terms of Use", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/reset_password.json b/public/language/en-x-pirate/reset_password.json new file mode 100644 index 0000000000..8643f923bc --- /dev/null +++ b/public/language/en-x-pirate/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Reset Password", + "update_password": "Update Password", + "password_changed.title": "Password Changed", + "password_changed.message": "

Password successfully reset, please log in again.", + "wrong_reset_code.title": "Incorrect Reset Code", + "wrong_reset_code.message": "The reset code received was incorrect. Please try again, or request a new reset code.", + "new_password": "New Password", + "repeat_password": "Confirm Password", + "changing_password": "Changing Password", + "enter_email": "Please enter your email address and we will send you an email with instructions on how to reset your account.", + "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.", + "invalid_email": "Invalid Email / Email does not exist!", + "password_too_short": "The password entered is too short, please pick a different password.", + "passwords_do_not_match": "The two passwords you've entered do not match.", + "password_expired": "Your password has expired, please choose a new password" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/search.json b/public/language/en-x-pirate/search.json new file mode 100644 index 0000000000..639cc0b653 --- /dev/null +++ b/public/language/en-x-pirate/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No matches found", + "advanced-search": "Advanced Search", + "in": "In", + "titles": "Titles", + "titles-posts": "Titles and Posts", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Posted by", + "in-categories": "In Categories", + "search-child-categories": "Search child categories", + "has-tags": "Has tags", + "reply-count": "Reply Count", + "at-least": "At least", + "at-most": "At most", + "relevance": "Relevance", + "post-time": "Post time", + "votes": "Votes", + "newer-than": "Newer than", + "older-than": "Older than", + "any-date": "Any date", + "yesterday": "Yesterday", + "one-week": "One week", + "two-weeks": "Two weeks", + "one-month": "One month", + "three-months": "Three months", + "six-months": "Six months", + "one-year": "One year", + "sort-by": "Sort by", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "topic-votes": "Topic votes", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Username", + "category": "Category", + "descending": "In descending order", + "ascending": "In ascending order", + "save-preferences": "Save preferences", + "clear-preferences": "Clear preferences", + "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)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/success.json b/public/language/en-x-pirate/success.json new file mode 100644 index 0000000000..7fa5550915 --- /dev/null +++ b/public/language/en-x-pirate/success.json @@ -0,0 +1,7 @@ +{ + "success": "Success", + "topic-post": "You have successfully posted.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Authentication Successful", + "settings-saved": "Settings saved!" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/tags.json b/public/language/en-x-pirate/tags.json new file mode 100644 index 0000000000..24ca6f8a39 --- /dev/null +++ b/public/language/en-x-pirate/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "There are no topics with this tag.", + "tags": "Tags", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "Enter tags...", + "no_tags": "There are no tags yet.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/top.json b/public/language/en-x-pirate/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/en-x-pirate/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/topic.json b/public/language/en-x-pirate/topic.json new file mode 100644 index 0000000000..a275204aa2 --- /dev/null +++ b/public/language/en-x-pirate/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Topic", + "title": "Title", + "no_topics_found": "No topics found!", + "no_posts_found": "No posts found!", + "post_is_deleted": "This post is deleted!", + "topic_is_deleted": "This topic is deleted!", + "profile": "Profile", + "posted_by": "Posted by %1", + "posted_by_guest": "Posted by Guest", + "chat": "Chat", + "notify_me": "Be notified of new replies in this topic", + "quote": "Quote", + "reply": "Reply", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log in to reply", + "login-to-view": "🔒 Log in to view", + "edit": "Edit", + "delete": "Delete", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Purge", + "restore": "Restore", + "move": "Move", + "change-owner": "Change Owner", + "fork": "Fork", + "link": "Link", + "share": "Share", + "tools": "Tools", + "locked": "Locked", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", + "following_topic.message": "You will now be receiving notifications when somebody posts to this topic.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Please register or log in in order to subscribe to this topic.", + "markAsUnreadForAll.success": "Topic marked as unread for all.", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "Watch", + "unwatch": "Unwatch", + "watch.title": "Be notified of new replies in this topic", + "unwatch.title": "Stop watching this topic", + "share_this_post": "Share this Post", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Topic Tools", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Pin Topic", + "thread_tools.unpin": "Unpin Topic", + "thread_tools.lock": "Lock Topic", + "thread_tools.unlock": "Unlock Topic", + "thread_tools.move": "Move Topic", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Move All", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Fork Topic", + "thread_tools.delete": "Delete Topic", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Are you sure you want to delete this topic?", + "thread_tools.restore": "Restore Topic", + "thread_tools.restore_confirm": "Are you sure you want to restore this topic?", + "thread_tools.purge": "Purge Topic", + "thread_tools.purge_confirm": "Are you sure you want to purge this topic?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Are you sure you want to delete this post?", + "post_restore_confirm": "Are you sure you want to restore this post?", + "post_purge_confirm": "Are you sure you want to purge this post?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Loading Categories", + "confirm_move": "Move", + "confirm_fork": "Fork", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Loading More Posts", + "move_topic": "Move Topic", + "move_topics": "Move Topics", + "move_post": "Move Post", + "post_moved": "Post moved!", + "fork_topic": "Fork Topic", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Click the posts you want to fork", + "fork_no_pids": "No posts selected!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Successfully forked topic! Click here to go to the forked topic.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Enter your topic title here...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Discard", + "composer.submit": "Submit", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Replying to %1", + "composer.new_topic": "New Topic", + "composer.editing": "Editing", + "composer.uploading": "uploading...", + "composer.thumb_url_label": "Paste a topic thumbnail URL", + "composer.thumb_title": "Add a thumbnail to this topic", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Or upload a file", + "composer.thumb_remove": "Clear fields", + "composer.drag_and_drop_images": "Drag and Drop Images Here", + "more_users_and_guests": "%1 more user(s) and %2 guest(s)", + "more_users": "%1 more user(s)", + "more_guests": "%1 more guest(s)", + "users_and_others": "%1 and %2 others", + "sort_by": "Sort by", + "oldest_to_newest": "Oldest to Newest", + "newest_to_oldest": "Newest to Oldest", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/unread.json b/public/language/en-x-pirate/unread.json new file mode 100644 index 0000000000..8f28de6b68 --- /dev/null +++ b/public/language/en-x-pirate/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Unread", + "no_unread_topics": "There be no unread topics.", + "load_more": "Giv'er more", + "mark_as_read": "Mark as Read", + "selected": "Selected", + "all": "All", + "all_categories": "All categories", + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/en-x-pirate/uploads.json b/public/language/en-x-pirate/uploads.json new file mode 100644 index 0000000000..3638aeb2d5 --- /dev/null +++ b/public/language/en-x-pirate/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Uploading the file...", + "select-file-to-upload": "Select a file to upload!", + "upload-success": "Ye file be uploaded!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/user.json b/public/language/en-x-pirate/user.json new file mode 100644 index 0000000000..f8ff37bb4a --- /dev/null +++ b/public/language/en-x-pirate/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Blackmarked", + "muted": "Muted", + "offline": "Asleep at the wheel", + "deleted": "Deleted", + "username": "User Name", + "joindate": "Join Date", + "postcount": "Post Count", + "email": "Email", + "confirm_email": "Confirm Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Account", + "ban_account_confirm": "Do you really want to ban this user?", + "unban_account": "Unban Account", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Delete Account", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Account deleted", + "account-content-deleted": "Account content deleted", + "fullname": "Full Name", + "website": "Website", + "location": "Location", + "age": "Age", + "joined": "Joined", + "lastonline": "Last Online", + "profile": "Profile", + "profile_views": "Profile views", + "reputation": "Reputation", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Watched", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Followers", + "following": "Following", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "About me", + "signature": "Signature", + "birthday": "Birthday", + "chat": "Chat", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Follow", + "unfollow": "Unfollow", + "more": "More", + "profile_update_success": "Profile has been updated successfully!", + "change_picture": "Change Picture", + "change_username": "Change Username", + "change_email": "Change Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Edit", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Uploaded Picture", + "upload_new_picture": "Upload New Picture", + "upload_new_picture_from_url": "Upload New Picture From URL", + "current_password": "Current Password", + "change_password": "Change Password", + "change_password_error": "Invalid Password!", + "change_password_error_wrong_current": "Your current password is not correct!", + "change_password_error_match": "Passwords must match!", + "change_password_error_privileges": "You do not have the rights to change this password.", + "change_password_success": "Your password is updated!", + "confirm_password": "Confirm Password", + "password": "Password", + "username_taken_workaround": "The username you requested was already taken, so we have altered it slightly. You are now known as %1", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "Upload picture", + "upload_a_picture": "Upload a picture", + "remove_uploaded_picture": "Remove Uploaded Picture", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Settings", + "show_email": "Show My Email", + "show_fullname": "Show My Full Name", + "restrict_chats": "Only allow chat messages from users I follow", + "digest_label": "Subscribe to Digest", + "digest_description": "Subscribe to email updates for this forum (new notifications and topics) according to a set schedule", + "digest_off": "Off", + "digest_daily": "Daily", + "digest_weekly": "Weekly", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Monthly", + "has_no_follower": "This user doesn't have any followers :(", + "follows_no_one": "This user isn't following anyone :(", + "has_no_posts": "This user hasn't posted anything yet.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "This user hasn't posted any topics yet.", + "has_no_watched_topics": "This user hasn't watched any topics yet.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Email Hidden", + "hidden": "hidden", + "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "topics_per_page": "Topics per Page", + "posts_per_page": "Posts per Page", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Browsing Settings", + "open_links_in_new_tab": "Open outgoing links in new tab", + "enable_topic_searching": "Enable In-Topic Searching", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + "select-skin": "Select a Skin", + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/en-x-pirate/users.json b/public/language/en-x-pirate/users.json new file mode 100644 index 0000000000..748f41943a --- /dev/null +++ b/public/language/en-x-pirate/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Land lubbers", + "top_posters": "Top mateys", + "most_reputation": "Most Reputation", + "most_flags": "Most Flags", + "search": "Search", + "enter_username": "Gimme y'er handle", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Load More", + "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", + "filter-by": "Filter By", + "online-only": "Online only", + "invite": "Invite", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "An invitation email has been sent to %1", + "user_list": "User List", + "recent_topics": "Recent Topics", + "popular_topics": "Popular Topics", + "unread_topics": "Unread Topics", + "categories": "Categories", + "tags": "Tags", + "no-users-found": "No users found!" +} \ No newline at end of file diff --git a/public/language/es/_DO_NOT_EDIT_FILES_HERE.md b/public/language/es/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/es/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/es/admin/admin.json b/public/language/es/admin/admin.json new file mode 100644 index 0000000000..57f5d25554 --- /dev/null +++ b/public/language/es/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "¿Está seguro que desea reconstruir y reiniciar NodeBB?", + "alert.confirm-restart": "¿Está seguro que desea reiniciar NodeBB?", + + "acp-title": "%1 | Panel de control de administrador NodeBB", + "settings-header-contents": "Contenidos", + "changes-saved": "Cambios guardados", + "changes-saved-message": "Tus cambios para la configuración de NodeBB han sido guardados.", + "changes-not-saved": "Cambios no guardados", + "changes-not-saved-message": "NodeBB ha encontrado un problema guardando tus cambios. (%1)" +} \ No newline at end of file diff --git a/public/language/es/admin/advanced/cache.json b/public/language/es/admin/advanced/cache.json new file mode 100644 index 0000000000..be68806428 --- /dev/null +++ b/public/language/es/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Publicar Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Completo", + "post-cache-size": "Tamaño de cache del post", + "items-in-cache": "Artículos en cache" +} \ No newline at end of file diff --git a/public/language/es/admin/advanced/database.json b/public/language/es/admin/advanced/database.json new file mode 100644 index 0000000000..b0c7a9da86 --- /dev/null +++ b/public/language/es/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Tiempo de acitividad en Segundos", + "uptime-days": "Tiempo de actividad en días", + + "mongo": "Mongo", + "mongo.version": "Versión MongoDB", + "mongo.storage-engine": "Motor de almacenamiento", + "mongo.collections": "Colecciones", + "mongo.objects": "Objetos", + "mongo.avg-object-size": "Tamaño promedio por Objeto", + "mongo.data-size": "Tamaño de los Datos", + "mongo.storage-size": "Tamaño del almacenamiento", + "mongo.index-size": "Tamaño del Índice", + "mongo.file-size": "Tamaño del fichero", + "mongo.resident-memory": "Memoria Residente", + "mongo.virtual-memory": "Memoria Virtual", + "mongo.mapped-memory": "Memoria Mapeada", + "mongo.bytes-in": "Entradas de Bytes", + "mongo.bytes-out": "Salidas de Bytes", + "mongo.num-requests": "Número de solicitudes", + "mongo.raw-info": "Fila de Información MongoDB", + "mongo.unauthorized": "NodeBB no pudo consultar las estadísticas relevantes en la base de datos MongoDB. Por favor verifique que el usuario usado por NodeBB contiene el roll "clusterMonitor" para la base de datos "admin".", + + "redis": "Redis", + "redis.version": "Versión de Redis", + "redis.keys": "Llaves", + "redis.expires": "Expira", + "redis.avg-ttl": "TTL Promedio", + "redis.connected-clients": "Clientes Conectados", + "redis.connected-slaves": "Esclavos Conectados", + "redis.blocked-clients": "Clientes Bloqueados", + "redis.used-memory": "Memoria Utilizada", + "redis.memory-frag-ratio": "Proporción de Fragmentación de la Memoria", + "redis.total-connections-recieved": "Total de Conexiones Recividas ", + "redis.total-commands-processed": "Total de Comandos Procesados", + "redis.iops": "Operaciones Instantáneas por Segundo", + "redis.iinput": "Entradas instantáneas por segundo", + "redis.ioutput": "Salidas instantáneas por segundo", + "redis.total-input": "Entrada Total", + "redis.total-output": "Salida Total", + + "redis.keyspace-hits": "Pulsaciones de espaciado del teclado", + "redis.keyspace-misses": "Fallos de espaciado del teclado", + "redis.raw-info": "Fila de Información de Redis", + + "postgres": "Postgres", + "postgres.version": "Versión PostgreSQL", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/es/admin/advanced/errors.json b/public/language/es/admin/advanced/errors.json new file mode 100644 index 0000000000..e053348013 --- /dev/null +++ b/public/language/es/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figura %1", + "error-events-per-day": "%1 eventos por dia", + "error.404": "404 No Encontrado", + "error.503": "503 Servicio no disponible", + "manage-error-log": "Administrar registro de errores", + "export-error-log": "Exportar registro de errores (CSV)", + "clear-error-log": "Cerrar Log de Error", + "route": "Ruta", + "count": "Contar", + "no-routes-not-found": "¡Hurra! ¡No hay errores 404!", + "clear404-confirm": "¿Estas seguro que desea borrar los registros de errores 404?", + "clear404-success": "\"404 no encontrado\" Errores borrados" +} \ No newline at end of file diff --git a/public/language/es/admin/advanced/events.json b/public/language/es/admin/advanced/events.json new file mode 100644 index 0000000000..ec26f7c11c --- /dev/null +++ b/public/language/es/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Eventos", + "no-events": "No hay eventos", + "control-panel": "Panel de control de eventos", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filtros", + "filters-apply": "Aplicar filtros", + "filter-type": "Tipo de evento", + "filter-start": "Fecha de inicio", + "filter-end": "Fecha fin", + "filter-perPage": "por página" +} \ No newline at end of file diff --git a/public/language/es/admin/advanced/logs.json b/public/language/es/admin/advanced/logs.json new file mode 100644 index 0000000000..006499bb65 --- /dev/null +++ b/public/language/es/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Registro", + "control-panel": "Panel de control del registro", + "reload": "Recargar registro", + "clear": "Borrar registro", + "clear-success": "¡Registros borrados!" +} \ No newline at end of file diff --git a/public/language/es/admin/appearance/customise.json b/public/language/es/admin/appearance/customise.json new file mode 100644 index 0000000000..f2eeeb071a --- /dev/null +++ b/public/language/es/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "CSS/LESS personalizado", + "custom-css.description": "Ingrese sus propias declaraciones de CSS/LESS aquí, las cuales serán aplicadas después de todos los demás estilos.", + "custom-css.enable": "Activar CSS/LESS personalizado", + + "custom-js": "Javascript personalizado", + "custom-js.description": "Introduzca su propio javascript aquí. Este será ejecutado cuando la página haya cargado por completo.", + "custom-js.enable": "Activar Javascript personalizado", + + "custom-header": "Cabezera personalizada", + "custom-header.description": "Ingrese HTML personalizado aquí (por ejemplo, Metaetiquetas, etc.), que se agregará al <head> sección del marcado de tu foro. Las etiquetas de script están permitidas, pero se desaconsejan, ya que Custom Javascript2 pestaña está disponible.", + "custom-header.enable": "Activar cabecera personalizada", + + "custom-css.livereload": "Activar Recargar en Vivo", + "custom-css.livereload.description": "Activar esto para forzar todas las sesiones en todos los dispositivos que recaen de tu cuenta a limpiar cada vez que tú haces clic en guardar" +} \ No newline at end of file diff --git a/public/language/es/admin/appearance/skins.json b/public/language/es/admin/appearance/skins.json new file mode 100644 index 0000000000..3f1a331fc2 --- /dev/null +++ b/public/language/es/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Cargando Temas...", + "homepage": "Pagina Principal", + "select-skin": "Selecciona el Tema", + "current-skin": "Tema Actual", + "skin-updated": "Tema Actualizado", + "applied-success": "El tema %1 se aplicó correctamente", + "revert-success": "El tema revierte los colores base" +} \ No newline at end of file diff --git a/public/language/es/admin/appearance/themes.json b/public/language/es/admin/appearance/themes.json new file mode 100644 index 0000000000..bcaf02cfe2 --- /dev/null +++ b/public/language/es/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Buscando los temas instalados...", + "homepage": "Pagina Principal", + "select-theme": "Tema Seleccionado", + "current-theme": "Tema Actual ", + "no-themes": "No se encontraron temas instalados", + "revert-confirm": "¿Estas seguro/a que quieres restaurar el tema de fabrica de NodeBB?", + "theme-changed": "Se Cambió el Tema", + "revert-success": "Has revertido con exito el tema de fabrica de NodeBB.", + "restart-to-activate": "Por favor recompila y reinicia NodeBB para activar por completo este tema." +} \ No newline at end of file diff --git a/public/language/es/admin/dashboard.json b/public/language/es/admin/dashboard.json new file mode 100644 index 0000000000..a1d11fab48 --- /dev/null +++ b/public/language/es/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Trafico del Foro", + "page-views": "Vistas de la Pagina", + "unique-visitors": "Visitantes Unicos", + "logins": "Logins", + "new-users": "New Users", + "posts": "Publicación", + "topics": "Temas", + "page-views-seven": "Últimos 7 Días", + "page-views-thirty": "Últimos 30 Días", + "page-views-last-day": "Últimas 24 horas", + "page-views-custom": "Rango de Fechas Personalizado", + "page-views-custom-start": "Comienzo del Rango", + "page-views-custom-end": "Final del Rango", + "page-views-custom-help": "Introduce un rango de fechas para las vistas de página que deseas ver. Si no hay ningún selector de fechas disponible, el formato aceptado es AAAA-MM-DD", + "page-views-custom-error": "Por favor, introduce un rango de fechas válido en el formato AAAA-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "Todos los Tiempos", + + "updates": "Actualizaciones", + "running-version": "Estas ejecutando NodeBB v%1.", + "keep-updated": "Asegúrate que tu NodeBB este al día en los últimos parches de seguridad y actualizaciones.", + "up-to-date": "

Estásactualizado/a

", + "upgrade-available": "

Una versión nueva (v%1) ha sido publicada. Consideraactualizar NodeBB.

", + "prerelease-upgrade-available": "

Esta es una versión pre-publicación anticuada. Una versión nueva(v%1) ha sido publicada. Consideraactualizar NodeBB.

", + "prerelease-warning": "

Esta es una versión depre-lanzamiento de NodeBB. Algunas fallas pueden ocurrir.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum esta siendo ejecutado en modo de desarrollador. El foro puede estar abierto a vulnerabilidades potenciales; por favor contacta tu administrador del sistema.", + "latest-lookup-failed": "

No se pudo encontrar la última versión disponible de NodeBB

", + + "notices": "Noticias", + "restart-not-required": "No se require reiniciar.", + "restart-required": "Se requiere reiniciar", + "search-plugin-installed": "El plug-in de búsqueda esta instalado.", + "search-plugin-not-installed": "El plug-in de busqueda no esta instalado", + "search-plugin-tooltip": "Instala el plug-in de búsqueda desde la pagina de plugins para activar esta funcionalidad.", + + "control-panel": "Control del Systema", + "rebuild-and-restart": "Reconstruye & Reinicia", + "restart": "Reinicia", + "restart-warning": "Reconstruir o Reiniciar tu NodeBB cerrará todas las conexiones por unos segundos.", + "restart-disabled": "Reconstruir y Reiniciar tu NodeBB ha sido deshabilitado, ya que parece que no lo estás ejecutando desde el daemon adecuado.", + "maintenance-mode": "Modo de Mantenimiento", + "maintenance-mode-title": "Haz clic aquí para activar el modo de mantenimiento de NodeBB", + "realtime-chart-updates": "Actualizar el Grafo en Tiempo Real", + + "active-users": "Usuarios Activos", + "active-users.users": "Usuarios", + "active-users.guests": "Invitados", + "active-users.total": "Total", + "active-users.connections": "Conexiones", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registrados", + + "user-presence": "Presencia del Usuario", + "on-categories": "Listado en Categorias", + "reading-posts": "Leer entradas", + "browsing-topics": "Explorar temas", + "recent": "Recientes", + "unread": "Sin Leer", + + "high-presence-topics": "Temas con Alta Presencia", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Vista de la Pagina", + "graphs.page-views-registered": "Vistas de la página registradas", + "graphs.page-views-guest": "Vistas de la página visitantes", + "graphs.page-views-bot": "Vistas de la página Bot", + "graphs.unique-visitors": "Visitantes Unicos", + "graphs.registered-users": "Usuarios Registrados", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Reiniciado por última vez por", + "no-users-browsing": "No hay usuarios explorando", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/es/admin/development/info.json b/public/language/es/admin/development/info.json new file mode 100644 index 0000000000..5f934d3a40 --- /dev/null +++ b/public/language/es/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Tú estas en %1:%2", + "ip": "IP %1", + "nodes-responded": "¡%1 nodos respondieron en %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "en-linea", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "carga del sistema", + "cpu-usage": "uso del cpu", + "uptime": "tiempo de actividad", + + "registered": "Registrado", + "sockets": "Toma", + "guests": "Invitados", + + "info": "Información" +} \ No newline at end of file diff --git a/public/language/es/admin/development/logger.json b/public/language/es/admin/development/logger.json new file mode 100644 index 0000000000..9a4396219c --- /dev/null +++ b/public/language/es/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Ajustes de registro", + "description": "Activando las casillas de verificación, recibirás logs/informes en tu terminal. Si especificas una ruta, en su lugar los logs/informes serán salvados a un archivo. los informes/logs HTTP son útiles para recolectar estadísticas sobre quien, cuando y qué accede a tu foro. Además crear informes de accesos mediante peticiones HTTP, podemos también informar sobre eventos socket.io. Los accesos socket.io, en co,binación con un monitor redis-cli, pueden ser muy útiles para aprender el funcionamiento interno de NodeBB.", + "explanation": "Simplemente marca/desmarca los ajustes de registro para activar o desactivar registro en el aire. No se necesita reinicio.", + "enable-http": "Activar registro HTTP", + "enable-socket": "Activar el evento de registro socket.io ", + "file-path": "Ruta al fichero log", + "file-path-placeholder": "/path/to/log/file.log ::: dejar en blanco para acceder a tu terminal.", + + "control-panel": "Panel de Control de Registro", + "update-settings": "Actualizar Ajustes de Registro" +} \ No newline at end of file diff --git a/public/language/es/admin/extend/plugins.json b/public/language/es/admin/extend/plugins.json new file mode 100644 index 0000000000..a639155f22 --- /dev/null +++ b/public/language/es/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Instalado", + "active": "Activo", + "inactive": "Inactivo ", + "out-of-date": "Desactualizado", + "none-found": "No se encontraron plugins.", + "none-active": "No hay Plug-ins activos", + "find-plugins": "Buscar Plug-in", + + "plugin-search": "Buscar", + "plugin-search-placeholder": "Búscando Plug-in", + "submit-anonymous-usage": "Enviar datos anónimos de uso de complementos.", + "reorder-plugins": "Re-ordenar Plug-ins", + "order-active": "Ordenar Plug-ins Activos", + "dev-interested": "¿Estas interesado en escribir plug-ins para NodeBB?", + "docs-info": "La documentación completa respecto a autoría de plugins puede encontrarse en NodeBB Docs Portal.", + + "order.description": "Algunos plug-in funcionan idealmente cuando son inicializados antes o despues de otros.", + "order.explanation": "Los plug-in son cargados en el orden especificado, de arriba a abajo.", + + "plugin-item.themes": "Temas", + "plugin-item.deactivate": "Desactivar", + "plugin-item.activate": "Activar", + "plugin-item.install": "Instalar", + "plugin-item.uninstall": "Desinstalar", + "plugin-item.settings": "Configuraciones", + "plugin-item.installed": "Instalados", + "plugin-item.latest": "Ultimos", + "plugin-item.upgrade": "Actualizar", + "plugin-item.more-info": "Para mas información:", + "plugin-item.unknown": "Desconocido", + "plugin-item.unknown-explanation": "El estado de este plug-in no puede determinsarse, posiblemente es debido a un error de configuración.", + "plugin-item.compatible": "Este complemento funciona en NodeBB %1", + "plugin-item.not-compatible": "Este complemento no tiene datos de compatibilidad, asegúrese de que funcione antes de instalarlo en su entorno de producción.", + + "alert.enabled": "El plug-in esta Activo", + "alert.disabled": "Plug-in Des-habilitado", + "alert.upgraded": "Plug-in Actualizado", + "alert.installed": "Plug-in Instalado", + "alert.uninstalled": "Plug-in Desinstalado", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin desactivado con éxito", + "alert.upgrade-success": "Por favor reconstruye y reinicia NodeBB para actualizar del todo este plugin.", + "alert.install-success": "Plugin instalado con éxito, por favor activa el plugin.", + "alert.uninstall-success": "El plugin ha sido desactivado y desinstalado con éxito.", + "alert.suggest-error": "

NodeBB no pudo acceder al administrador de paquetes. ¿Proceder con la instalación de la última versión?

El servidor retornó (%1):%2
", + "alert.package-manager-unreachable": "

NodeBB no pudo acceder al administrador de paquetes, se sugiere una actualización ahora.

", + "alert.incompatible": "

Tu versión de NodeBB (v%1) solo tiene permiso para actualizar a v%2 de este plugin. Por favor, actualiza NodeBB si deseas instalar una versión más actual de este plugin.

", + "alert.possibly-incompatible": "

No se encontró información sobre Compatibilidad

Este plugin no especificó una versión específica para instalación para tu versión de NodeBB. No se puede garantizar una compatibilidad completa, y puede causar a tu NodeBB que no arranque adecuadamente.

En caso de que NodeBB no puede inicarse adecuadamente:

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

¿Continuar con la instalación de la última versión de este plugin?

", + "alert.reorder": "Plugins Re-ordenados", + "alert.reorder-success": "Por favor reconstruya y reinicie su NodeBB para completar el proceso.", + + "license.title": "Información de Licencia de Plugin", + "license.intro": "El plugin %1 tiene licencia bajo %2. Por favor lea y comprenda los términos de licencia antes de activar este plugin.", + "license.cta": "¿Desea continuar con la activación de este plugin?" +} diff --git a/public/language/es/admin/extend/rewards.json b/public/language/es/admin/extend/rewards.json new file mode 100644 index 0000000000..98ded7cfa7 --- /dev/null +++ b/public/language/es/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Recompensas", + "condition-if-users": "Si el Usuario", + "condition-is": "Es:", + "condition-then": "Entonces:", + "max-claims": "Número de veces que una recompensa puede ser reclamada", + "zero-infinite": "Introduzca 0 para infinito", + "delete": "Eliminar", + "enable": "Habilitar", + "disable": "Deshabilitar", + + "alert.delete-success": "Recompensa eliminada con éxito", + "alert.no-inputs-found": "¡Recompensa ilegal - no se encontraron inputs!", + "alert.save-success": "Recompensas guardadas con éxito" +} \ No newline at end of file diff --git a/public/language/es/admin/extend/widgets.json b/public/language/es/admin/extend/widgets.json new file mode 100644 index 0000000000..21281e75e2 --- /dev/null +++ b/public/language/es/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Widgets Disponibles", + "explanation": "Selecciona un widget del menú desplegable y arrástralo hasta la plantilla de zona de widgets a la izquierda.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clonar widgets de", + "containers.available": "Contenedores Disponibles", + "containers.explanation": "Arrastra y suelta sobre cualquier widget activo", + "containers.none": "Ninguno", + "container.well": "Bien", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel - cabecera (Panel header)", + "container.panel-body": "Panel - cuerpo (panel body)", + "container.alert": "Alerta (alert)", + + "alert.confirm-delete": "Estás seguro/a de que deseas borrar este widget?", + "alert.updated": "Widgets actualizados", + "alert.update-success": "Widgets actualizados con éxito", + "alert.clone-success": "Widgets clonados con éxito", + + "error.select-clone": "Por favor selecciona una página de la cual clonar", + + "title": "Título", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Ocultar en móviles" +} \ No newline at end of file diff --git a/public/language/es/admin/manage/admins-mods.json b/public/language/es/admin/manage/admins-mods.json new file mode 100644 index 0000000000..6fd877c23d --- /dev/null +++ b/public/language/es/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administradores", + "global-moderators": "Moderadores Globales", + "moderators": "Moderators", + "no-global-moderators": "Sin Moderadores Globales", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Sin Moderadores", + "add-administrator": "Añadir Administrador", + "add-global-moderator": "Añadir Moderador Global", + "add-moderator": "Añadir Moderador" +} \ No newline at end of file diff --git a/public/language/es/admin/manage/categories.json b/public/language/es/admin/manage/categories.json new file mode 100644 index 0000000000..e069264095 --- /dev/null +++ b/public/language/es/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Configuración de Categoría", + "privileges": "Privilegios", + + "name": "Nombre de Categoría", + "description": "Descripción de Categoría", + "bg-color": "Color de Fondo", + "text-color": "Color del Texto", + "bg-image-size": "Tamaño de la Imagen de Fondo", + "custom-class": "Clase Personalizada", + "num-recent-replies": "# de Respuestas Recientes", + "ext-link": "Enlace Externo", + "subcategories-per-page": "Subcategorías por página", + "is-section": "Tratar esta categoría como una sección", + "post-queue": "Cola de publicación", + "tag-whitelist": "Etiquetas permitidas", + "upload-image": "Subir Imagen", + "delete-image": "Eliminar", + "category-image": "Imagen de Categoría", + "parent-category": "Categoría Superior", + "optional-parent-category": "(Opcional) Categoría Superior", + "top-level": "Nivel superior", + "parent-category-none": "(Ninguna)", + "copy-parent": "Copiar Padre", + "copy-settings": "Copiar Configuración Desde", + "optional-clone-settings": "(Opcional) Clonar Configuración De Categoría", + "clone-children": "Clonar Categorías Hijas y Configuraciones", + "purge": "Purgar Categoría", + + "enable": "Activar", + "disable": "Desactivar", + "edit": "Editar", + "analytics": "Analítica", + "view-category": "Ver categoría", + "set-order": "Establecer orden", + "set-order-help": "Configurar el orden de las categorías moverá esta categoría a ese orden y actualizará el orden de las demás categorías tal y como sea necesario. El valor mínimo de orden es 1, esto ubicará la categoría al principio.", + + "select-category": "Seleccionar Categoría", + "set-parent-category": "Fijar Categoría Superior", + + "privileges.description": "Puede configurar los privilegios de control de acceso para partes del sitio en esta sección. Los privilegios se pueden otorgar por usuario o por grupo. Seleccione el dominio de efecto de la lista desplegable a continuación.", + "privileges.category-selector": "Configurando privilegios para", + "privileges.warning": "Nota: La configuracion de los privilegios toma efecto inmediataente. No es necesario guardar la categoría despué de ajustar estas configuraciones.", + "privileges.section-viewing": "Privilegios de Visionado", + "privileges.section-posting": "Privilegios de Respuesta", + "privileges.section-moderation": "Privilegios de Moderación", + "privileges.section-other": "Otro", + "privileges.section-user": "Usuario", + "privileges.search-user": "Añadir Usuario", + "privileges.no-users": "No hay privilegios específicos de usuario en esta categoría.", + "privileges.section-group": "Grupo", + "privileges.group-private": "Éste grupo es privado", + "privileges.inheritance-exception": "Este grupo no hereda los privilegios del grupo de usuarios registrados", + "privileges.banned-user-inheritance": "Usuarios expulsados heredan privilegios del grupo de usuarios expulsados", + "privileges.search-group": "Añadir Grupo", + "privileges.copy-to-children": "Copiar a categorías inferiores", + "privileges.copy-from-category": "Copiar de Categoría", + "privileges.copy-privileges-to-all-categories": "Copiar a todas las Categorías", + "privileges.copy-group-privileges-to-children": "Copiar los privilegios de este grupo a los hijos de esta categoría.", + "privileges.copy-group-privileges-to-all-categories": "Copiar los privilegios de este grupo a todas las categorías.", + "privileges.copy-group-privileges-from": "Copiar los privilegios de este grupo desde otra categoría", + "privileges.inherit": "Si al grupo de los usuarios registrados se le otorga un privilegio específico, todos los otros grupos reciben un privilegio implícito , incluso si no están definidos/asignados de forma explícita. Este privilegio implícito se te muestra por que todos los usuarios son parte del grupo de usuarios usuarios registrados y, por tanto, los privilegios para grupos adicionales no deben de ser garantizados explícitamente.", + "privileges.copy-success": "Privilegios copiados!", + + "analytics.back": "Volver a lista de Categorías", + "analytics.title": "Analíticas para \"%1\" categoría", + "analytics.pageviews-hourly": " Figura1– Vistas de página por hora para esta categoría ", + "analytics.pageviews-daily": " Figura 2– Páginas vistas diarias para ésta categoría ", + "analytics.topics-daily": " Figura 3 –  Temas diarios creados en esta categoría", + "analytics.posts-daily": " Figura4 – Respuestas diarias en esta categoría ", + + "alert.created": "Creada", + "alert.create-success": "¡Categoría creada con éxito!", + "alert.none-active": "No tienes categorías activas.", + "alert.create": "Crear una Categoría", + "alert.confirm-purge": "

¿Realmente quieres purgar esta categoría\"%1\"?

¡Cuidado! ¡Todos los temas y respuestas en esta categoría serán purgados!

Purgar una categoría eliminará todos los temas y respuestas, y borrará la categoría de la base de datos. Si quieres eliminar una categoría temporalmente, deberías \"desactivar\" esa categoría en su lugar.

", + "alert.purge-success": "¡Categoría purgada!", + "alert.copy-success": "¡Configuración Copiada!", + "alert.set-parent-category": "Fijar Categoría Superior", + "alert.updated": "Categorías Actualizadas", + "alert.updated-success": "ID de categoría %1 actualizada con éxito", + "alert.upload-image": "Subir una imagen de categoría", + "alert.find-user": "Encontrar un Usuario", + "alert.user-search": "Buscar un usuario aquí...", + "alert.find-group": "Encontrar un Grupo", + "alert.group-search": "Buscar un grupo aquí...", + "alert.not-enough-whitelisted-tags": "Las etiquetas permitidas son menos de las mínimas permitidas, necesitas crear más etiqutas!", + "collapse-all": "Minimizar Todo", + "expand-all": "Expandir Todo", + "disable-on-create": "Desactivar en crear", + "no-matches": "No hay coincidencias" +} \ No newline at end of file diff --git a/public/language/es/admin/manage/digest.json b/public/language/es/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/es/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/es/admin/manage/groups.json b/public/language/es/admin/manage/groups.json new file mode 100644 index 0000000000..13190afc28 --- /dev/null +++ b/public/language/es/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Nombre del Grupo", + "badge": "Badge", + "properties": "Properties", + "description": "Descripción del Grupo", + "member-count": "Cuenta de Miembros", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Editar", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Buscar", + "create": "Crear Grupo", + "description-placeholder": "Descripción corta de vuestro grupo", + "create-button": "Crear", + + "alerts.create-failure": "Uy

Ha habido un problema creando el grupo. ¡Por favor inténtelo mas tarde!

", + "alerts.confirm-delete": "¿Está seguro/a de que desea eliminar este grupo?", + + "edit.name": "Nombre", + "edit.description": "Descripción", + "edit.user-title": "Título de Miembros", + "edit.icon": "Icono del Grupo", + "edit.label-color": "Color de la Etiqueta del Grupo", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Mostrar Distintivo", + "edit.private-details": "Si está activado, unirse a los grupos requiere la aprovación de un/a propietario/a del grupo.", + "edit.private-override": "Advertencia: Los grupos privados están deshabilitados a nivel del sistema, lo cual invalida esta opción.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Oculto", + "edit.hidden-details": "SI está activado, este grupo no podrá ser encontrado en la lista de grupos, y los usuarios tendrán que ser invitados manualmente.", + "edit.add-user": "Añadir Usuario al Grupo", + "edit.add-user-search": "Buscar Usuarios", + "edit.members": "Lista de Miembros", + "control-panel": "Panel de Control de Grupos", + "revert": "Revertir", + + "edit.no-users-found": "No se Encontraron Usuarios", + "edit.confirm-remove-user": "¿Estás seguro/a de que quieres eliminar a este/a usuario/a?", + "edit.save-success": "¡Cambios guardados!" +} \ No newline at end of file diff --git a/public/language/es/admin/manage/privileges.json b/public/language/es/admin/manage/privileges.json new file mode 100644 index 0000000000..c705a7312d --- /dev/null +++ b/public/language/es/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Subir imágenes", + "upload-files": "Subir Archivos", + "signature": "Firma", + "ban": "Banear", + "mute": "Mute", + "invite": "Invite", + "search-content": "Buscar Contenido", + "search-users": "Buscar Usuarios", + "search-tags": "Buscar Tags", + "view-users": "Ver Usuarios", + "view-tags": "Ver etiquetas", + "view-groups": "Ver grupos", + "allow-local-login": "Inicio de sesión local", + "allow-group-creation": "Crear grupo", + "view-users-info": "View Users Info", + "find-category": "Buscar Categoría", + "access-category": "Acceder Categoría", + "access-topics": "Acceder Temas", + "create-topics": "Crear Temas", + "reply-to-topics": "Responder a Temas", + "schedule-topics": "Schedule Topics", + "tag-topics": "Poner Tags (etiquetas) a Temas", + "edit-posts": "Editar Entradas", + "view-edit-history": "Ver Historial de Ediciones", + "delete-posts": "Borrar Entradas", + "view_deleted": "Ver Mensajes Eliminados", + "upvote-posts": "Votar Positivo en Entradas", + "downvote-posts": "Votar Negativo en Entradas", + "delete-topics": "Borrar Temas", + "purge": "Purgar", + "moderate": "Moderar", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/es/admin/manage/registration.json b/public/language/es/admin/manage/registration.json new file mode 100644 index 0000000000..21ee9c44f9 --- /dev/null +++ b/public/language/es/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Cola", + "description": "No hay usuarios en la cola de registro.
Para activar esta función, vaya a Configuración → Usuario → Registro de Usuarioy configureTipo de Registroa \"Aprobación de Administrador\".", + + "list.name": "Nombre", + "list.email": "Correo electrónico", + "list.ip": "IP", + "list.time": "Hora", + "list.username-spam": "Frecuencia: %1 Aparece: %2 Confianza: %3", + "list.email-spam": "Frecuencia: %1 Aparece: %2", + "list.ip-spam": "Frecuencia: %1 Aparece: %2", + + "invitations": "Invitaciones", + "invitations.description": "Abajo hay una lista completa de invitaciones enviadas. Use ctrl-f para buscar a través de la lista por correo electrónico o nombre de usuario.

EL nombre de usuario será mostrado a la derecha de los correos electrónicos para los usuarios que han aceptado sus invitaciones.", + "invitations.inviter-username": "Nombre del Usuario que Invita", + "invitations.invitee-email": "Correo Electrónico del Invitado", + "invitations.invitee-username": "Nombre del Usuario Invitado (si está registrado)", + + "invitations.confirm-delete": "¿Está seguro de que desea eliminar esta invitación?" +} \ No newline at end of file diff --git a/public/language/es/admin/manage/tags.json b/public/language/es/admin/manage/tags.json new file mode 100644 index 0000000000..9059f15130 --- /dev/null +++ b/public/language/es/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Tu foro todavía no tiene ningún tema con etiquetas (tags)", + "bg-color": "Color de Fondo", + "text-color": "Color del Texto", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Crear Etiqueta (tag)", + "modify": "Modificar Etiquetas (tags)", + "rename": "Renombrar Etiquetas (tags)", + "delete": "Borrar Etiquetas (tags) Seleccionadas", + "search": "Buscar etiquetas (tags)...", + "settings": "Tags Settings", + "name": "Nombre de Etiqueta (tag)", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "¿Quieres borrar las etiquetas (tags) seleccionadas?", + "alerts.update-success": "¡Etiqueta (tag) Actualizada!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/es/admin/manage/uploads.json b/public/language/es/admin/manage/uploads.json new file mode 100644 index 0000000000..1f96accb24 --- /dev/null +++ b/public/language/es/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Subir archivo", + "filename": "Nombre del archivo", + "usage": "Uso de Publicaciones", + "orphaned": "Huérfano", + "size/filecount": "Tamaño / Recuento de archivos", + "confirm-delete": "¿Realmente quieres borrar este archivo?", + "filecount": "%1 archivos", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/es/admin/manage/users.json b/public/language/es/admin/manage/users.json new file mode 100644 index 0000000000..3e6af3c465 --- /dev/null +++ b/public/language/es/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Usuarios", + "edit": "Actions", + "make-admin": "Hacer Administrador", + "remove-admin": "Eliminar Administrador", + "validate-email": "Validar Email", + "send-validation-email": "Enviar Email de Validación", + "password-reset-email": "Enviar Email para Recuperar la Contraseña", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Expulsar Usuario(s)", + "temp-ban": "Expulsar Usuario(s) Temporalmente", + "unban": "Eliminar Expulsión del Usuario(s)", + "reset-lockout": "Reiniciar Bloqueo", + "reset-flags": "Reiniciar Reportes", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Descargar CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "Nuevo Usuario", + "filter-by": "Filter by", + "pills.unvalidated": "No Validado", + "pills.validated": "Validated", + "pills.banned": "Baneado", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "Por ID de Usuario", + "search.uid-placeholder": "Introduce el ID del usuario a buscar", + "search.username": "Por Nombre de Usuario", + "search.username-placeholder": "Introduzca el nombre de usuario que quiere buscar", + "search.email": "Por Email", + "search.email-placeholder": "Introduzca el email a buscar", + "search.ip": "Por Dirección IP", + "search.ip-placeholder": "Introduzca la Dirección IP a buscar", + "search.not-found": "¡Usuario no encontrado!", + + "inactive.3-months": "3 meses", + "inactive.6-months": "6 meses", + "inactive.12-months": "12 meses", + + "users.uid": "uid", + "users.username": "nombre de usuario", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "cantidad de posts", + "users.reputation": "reputación", + "users.flags": "reportes", + "users.joined": "registrado", + "users.last-online": "última vez online", + "users.banned": "baneado", + + "create.username": "Nombre de Usuario", + "create.email": "Email", + "create.email-placeholder": "Email de este usuario", + "create.password": "Contraseña", + "create.password-confirm": "Confirmar Contraseña", + + "temp-ban.length": "Length", + "temp-ban.reason": "Razón (Opcional)", + "temp-ban.hours": "Horas", + "temp-ban.days": "Días", + "temp-ban.explanation": "Introduzca la duración de esta expulsión. Ten en cuenta que 0 se considera una expulsión permanente.", + + "alerts.confirm-ban": "¿Quiere realmente expulsar a este usuario permanentemente?", + "alerts.confirm-ban-multi": "¿Quiere realmente expulsar a estos usuarios permanentemente?", + "alerts.ban-success": "¡Usuario(s) expulsado(s)!", + "alerts.button-ban-x": "Expulsar %1 usuario(s)", + "alerts.unban-success": "¡Usuario(s) desbaneados!", + "alerts.lockout-reset-success": "¡Bloqueo(s) reseteado(s)!", + "alerts.flag-reset-success": "¡Reporte(s) reseteado(s)!", + "alerts.no-remove-yourself-admin": "¡No puedes eliminarte a ti mismo como Administrador!", + "alerts.make-admin-success": "El usuario es ahora administrador.", + "alerts.confirm-remove-admin": "¿Quiere realmente eliminar este administrador?", + "alerts.remove-admin-success": "El usuario ya no es administrador.", + "alerts.make-global-mod-success": "El usuario es ahora moderador global.", + "alerts.confirm-remove-global-mod": "¿Quiere realmente eliminar este moderador global?", + "alerts.remove-global-mod-success": "El usuario ya no es moderador global.", + "alerts.make-moderator-success": "El usuario es ahora moderador.", + "alerts.confirm-remove-moderator": "¿Quiere realmente eliminar este moderador?", + "alerts.remove-moderator-success": "El usuario ya no es moderador.", + "alerts.confirm-validate-email": "¿Quiere validar el/los email(s) de este/estos usuario(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validados", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "¿Quiere restablecer la contraseña del/los email(s) de este/estos usuario(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "¡Usuario(s) Borrado(s)!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Crear Usuario", + "alerts.button-create": "Crear", + "alerts.button-cancel": "Cancelar", + "alerts.error-passwords-different": "¡Las contraseñas deben coincidir!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "¡Usuario creado!", + + "alerts.prompt-email": "Correos electrónico:", + "alerts.email-sent-to": "Un email de invitación ha sido enviado a %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/es/admin/menu.json b/public/language/es/admin/menu.json new file mode 100644 index 0000000000..d59ea10b71 --- /dev/null +++ b/public/language/es/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Administrar", + "manage/categories": "Categorias", + "manage/privileges": "Privilegios", + "manage/tags": "Etiquetas", + "manage/users": "Usuarios", + "manage/admins-mods": "Administradores & Mods", + "manage/registration": "Cola de Registro", + "manage/post-queue": "Cola de mensajes", + "manage/groups": "Grupos", + "manage/ip-blacklist": "Lista negra de IP", + "manage/uploads": "Subidas", + "manage/digest": "Digests", + + "section-settings": "Opciones", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Invitados", + "settings/uploads": "Subidas", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Paginación", + "settings/tags": "Etiquetas", + "settings/notifications": "Notificaciones", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Rastreador web", + "settings/sockets": "Sockets", + "settings/advanced": "Avanzado", + + "settings.page-title": "%1 Opciones", + + "section-appearance": "Apariencia", + "appearance/themes": "Temas", + "appearance/skins": "Pieles", + "appearance/customise": "Contenido Personalizado (HTML/JS/CSS)", + + "section-extend": "Extender", + "extend/plugins": "Extensiones", + "extend/widgets": "Widgets", + "extend/rewards": "Recompensas", + + "section-social-auth": "Autentificación Social", + + "section-plugins": "Extensiones", + "extend/plugins.install": "Instalar extensiones", + + "section-advanced": "Avanzado", + "advanced/database": "Base de datos", + "advanced/events": "Eventos", + "advanced/hooks": "Hooks", + "advanced/logs": "Registros", + "advanced/errors": "Errores", + "advanced/cache": "Caché", + "development/logger": "Registro", + "development/info": "Información", + + "rebuild-and-restart-forum": "Reconstruir & Reiniciar Foro", + "restart-forum": "Reiniciar foro", + "logout": "Cerrar sesión", + "view-forum": "Ver foro", + + "search.placeholder": "Search settings", + "search.no-results": "Sin resultados...", + "search.search-forum": "Buscar en el foro ", + "search.keep-typing": "Escribe más para ver resultados...", + "search.start-typing": "Empieza a escribir para ver resultados...", + + "connection-lost": "La conexión a %1 se ha perdido, intentando reconectar...", + + "alerts.version": "Ejecutando NodeBB v%1", + "alerts.upgrade": "Actualizando a v%1" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/advanced.json b/public/language/es/admin/settings/advanced.json new file mode 100644 index 0000000000..4c912b4d14 --- /dev/null +++ b/public/language/es/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Modo de Mantenimiento", + "maintenance-mode.help": "Cuando este foro están en Modo de Mantenimiento, todas las peticiones serán redirigidas a una página estática de mantenimiento. Los administradores están exentos de esta redirección, y pueden acceder al sitio normalmente.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Mensaje de Mantenimiento", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Cabeceras", + "headers.allow-from": "Establecer ALLOW-FROM para poner NodeBB en un iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Personalizar la cabecera \"powered By\" enviada por NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Acceso-Controlar-Permitir-Expresión regular de origen", + "headers.acao-help": "Para denegar acceso a todos los sitios, dejar vacío", + "headers.acao-regex-help": "Ingrese expresiones regulares aquí para que coincidan con los orígenes dinámicos. Para denegar el acceso a todos los sitios, déjelo vacío", + "headers.acac": "Credenciales-Control-Permitir-Acceso", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Seguridad estricta del transporte", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Administración de Tráfico", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Habilitar Administración de Tráfico", + "traffic.event-lag": "Límite de Lag para el Event Loop (en milisegundos)", + "traffic.event-lag-help": "Bajar este valor disminuye los tiempos de espera para cargas de página, pero también mostrará el mensaje \"carga excesiva\" a mas usuarios. (Se requiere Reiniciar)", + "traffic.lag-check-interval": "Chequear intervalo (milisegundos)", + "traffic.lag-check-interval-help": "Bajar este valor causará que NodeBB sea más sensible a picos de carga, pero también causará que el chequeo sea muy sensible. (Requiere reinicio)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/es/admin/settings/api.json b/public/language/es/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/es/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/chat.json b/public/language/es/admin/settings/chat.json new file mode 100644 index 0000000000..dbdf26b7c5 --- /dev/null +++ b/public/language/es/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Ajustes de Chat", + "disable": "Deshabilitar chat", + "disable-editing": "Deshabilitar edición y borrado de mensajes de chat", + "disable-editing-help": "Los administradores y los moderadores globales están exentos de esta restricción", + "max-length": "Maxima longitud de mensajes de chat", + "max-room-size": "Máximo numero de usuarios en las salas de chat", + "delay": "Tiempo entre envío de mensajes de chat en milisegundos", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/cookies.json b/public/language/es/admin/settings/cookies.json new file mode 100644 index 0000000000..ff69117882 --- /dev/null +++ b/public/language/es/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Consentimiento EU", + "consent.enabled": "Habilitado", + "consent.message": "Notificación de mensaje", + "consent.acceptance": "Mensaje de aceptación", + "consent.link-text": "Texto de Enlace a Política", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Dejar en blanco para utilizar valores localizados por defecto de NodeBB", + "settings": "Configuraciones.", + "cookie-domain": "Dominio de cookie de sesión", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Dejar en blanco para valores por defecto" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/email.json b/public/language/es/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/es/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/es/admin/settings/general.json b/public/language/es/admin/settings/general.json new file mode 100644 index 0000000000..3ddadb3079 --- /dev/null +++ b/public/language/es/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Ajustes del Sitio", + "title": "Título del Sitio", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "La URL del título del sitio", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Nombre de tu Comunidad", + "title.show-in-header": "Mostrar Título del Sitio en el Encabezado", + "browser-title": "Título del Navegador", + "browser-title-help": "Si no se especifica el título del navegador, se utilizará el título del sitio", + "title-layout": "Plantilla del Sitio", + "title-layout-help": "Define cómo el se estructurará el título del explorador. Por ejemplo: {TítulodelaPágina} | {TítulodelExplorador}\n", + "description.placeholder": "Una descripción corta de tu comunidad", + "description": "Descripción del Sitio", + "keywords": "Palabras Clave (keywords) del Sitio", + "keywords-placeholder": "Palabras Clave (keywords) que describen tu comunidad, separadas por comas", + "logo": "Logo del Sitio", + "logo.image": "Imagen", + "logo.image-placeholder": "Ruta al logo que se mostrará en la cabecera del foro", + "logo.upload": "Subir", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "la URL del logo del sitio", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Texto alternativo (alt text)", + "log.alt-text-placeholder": "Texto alternativo para accesibilidad", + "favicon": "Favicon", + "favicon.upload": "Subir", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Subir", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Enlaces a sitios externos", + "outgoing-links.warning-page": "Usar Página de Advertencia para Enlaces a Sitios Externos", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Dominios permitidos que podrán evitar la página de advertencia", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/es/admin/settings/group.json b/public/language/es/admin/settings/group.json new file mode 100644 index 0000000000..cf0bc1be9e --- /dev/null +++ b/public/language/es/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Grupos Privados", + "private-groups.help": "Si se habilita, unirse a grupos requiere la aprobación del dueño del grupo (Por defecto: habilitado)", + "private-groups.warning": "¡Cuidado! Si esta opción está deshabilitada y tienes grupos privados, se convertirán en grupos públicos automáticamente.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "Esta opción puede ser usadas para permitir a los usuarios seleccionar múltiples medallas de grupo, requiere de soporte del theme/plantilla.", + "max-name-length": "Longitud Máxima de Nombre de Grupo", + "max-title-length": "Longitud máxima del título del grupo", + "cover-image": "Imagen de Portada de Grupo", + "default-cover": "Imágenes de Portada por Defecto", + "default-cover-help": "Añadir lista separada por comas de imágenes de portada por defecto para grupos que no han subido una imagen de portada" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/guest.json b/public/language/es/admin/settings/guest.json new file mode 100644 index 0000000000..0fbe97ac7b --- /dev/null +++ b/public/language/es/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Permitir manejo de visitantes", + "handles.enabled-help": "Esta opción expone un nuevo campo que permite a los invitados escoger un nombre para asociarse con cada entrada/respuesta que hagan. Si está desactivado, se les llamará simplemente \"Invitado\".", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/homepage.json b/public/language/es/admin/settings/homepage.json new file mode 100644 index 0000000000..178a101aa5 --- /dev/null +++ b/public/language/es/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Página Principal", + "description": "Escoge que pagina se muestra cuando los usuarios navegan en la raíz del foro.", + "home-page-route": "Ruta de la Pagina Principal", + "custom-route": "Ruta Personalizada", + "allow-user-home-pages": "Permitir Pagina de Perfil del Usuario", + "home-page-title": "Título de la página de inicio (por defecto, \"Home\" o \"Inicio\")" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/languages.json b/public/language/es/admin/settings/languages.json new file mode 100644 index 0000000000..df6d3843e5 --- /dev/null +++ b/public/language/es/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Configuración de Idiomas", + "description": "El idioma por defecto determina la configuración del idioma usado para todos los usuarios que visiten el foro.
Los usuarios, a nivel individual, pueden sobreescribir el idioma por defecto en la página de configuración de su cuenta.", + "default-language": "Idioma por defecto", + "auto-detect": "Auto Detectar Configuración de Idioma para Visitantes" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/navigation.json b/public/language/es/admin/settings/navigation.json new file mode 100644 index 0000000000..3b28dd115a --- /dev/null +++ b/public/language/es/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icono:", + "change-icon": "cambio", + "route": "Ruta:", + "tooltip": "Nota de ayuda:", + "text": "Texto:", + "text-class": "Clase de Texto: opcional", + "class": "Clase opcional", + "id": "ID: opcional", + + "properties": "Propiedades:", + "groups": "Grupos:", + "open-new-window": "Abrir en una ventana nueva", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Borrar", + "btn.disable": "Deshabilitar", + "btn.enable": "Habilitar", + + "available-menu-items": "Items de Menú Disponibles", + "custom-route": "Ruta Personalizada:", + "core": "núcleo", + "plugin": "plugin" +} diff --git a/public/language/es/admin/settings/notifications.json b/public/language/es/admin/settings/notifications.json new file mode 100644 index 0000000000..8d2509d75b --- /dev/null +++ b/public/language/es/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notificaciones", + "welcome-notification": "Notificación de Bienvenida", + "welcome-notification-link": "Enlace de Notificación de Bienvenida", + "welcome-notification-uid": "Usuario de Notificación de Bienvenida (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/pagination.json b/public/language/es/admin/settings/pagination.json new file mode 100644 index 0000000000..0a739c4e47 --- /dev/null +++ b/public/language/es/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Configuración de Paginación", + "enable": "Paginar temas y mensajes en vez de usar scroll infinito", + "posts": "Post Pagination", + "topics": "Paginación de Temas", + "posts-per-page": "Mensajes por página", + "max-posts-per-page": "Mensajes máximos por página", + "categories": "Paginación de Categorías", + "topics-per-page": "Temas por Página", + "max-topics-per-page": "Máximo de temas por página", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/post.json b/public/language/es/admin/settings/post.json new file mode 100644 index 0000000000..c6f532b019 --- /dev/null +++ b/public/language/es/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Ordenamiento de Respuestas", + "sorting.post-default": "Ordenamiento de Respuestas por Defecto", + "sorting.oldest-to-newest": "De más Antiguo a más Nuevo", + "sorting.newest-to-oldest": "De más Nuevo a Más Antiguo", + "sorting.most-votes": "Más Votado", + "sorting.most-posts": "Más Respondido", + "sorting.topic-default": "Ordenamiento de Temas por defecto", + "length": "Longitud de la entrada", + "post-queue": "Post Queue", + "restrictions": "Restricciones a las Respuestas", + "restrictions-new": "Restriciones a Nuevos Usuarios", + "restrictions.post-queue": "Permitir cola de respuestas", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Permitir restricciones a usuarios nuevos", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Segundos entre respuestas para nuevos usuarios", + "restrictions.rep-threshold": "Límite de reputación antes de que estas restricciones sean eliminadas", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Longitud Mínima del Título", + "restrictions.max-title-length": "Longitud Máxima del Título", + "restrictions.min-post-length": "Lóngitud Mínima de la Entrada o Respuesta", + "restrictions.max-post-length": "Longitud Máxima de la Entrada o Respuesta", + "restrictions.days-until-stale": "Días hasta que el tema se considera antiguo", + "restrictions.stale-help": "Si un tema es considerado \"antiguo\", se mostrará un aviso a los usuarios que quieran responder a ese tema.", + "timestamp": "Indicación de fecha y hora", + "timestamp.cut-off": "Fecha límite (en días)", + "timestamp.cut-off-help": "Las fechas & horas serán mostradas de una forma relativa (e.g. \"hace 3 horas\"/\"hace 5 días\"), y localizadas en varios\n\t\t\t\t\tlenguajes. Después de cierto punto, este texto puede ser cambiado para mostrar la propia fecha localizada\n\t\t\t\t\t\t(e.g. 5 Nov 2016 15:30).
(Por defecto: 30, o un mes). Ponla a 0 para mostrar siempre las fechas exactas. Déjala en blanco para siempre mostrar las fechas relativas.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Entrada de muestra", + "teaser.last-post": "Último – Muestra la última entrada, incluyendo la entrada original, si no hay respuestas.", + "teaser.last-reply": "Última – Muestra la última respuesta, o un texto \"No hay respuestas\" si no hay respuestas.", + "teaser.first": "Primera", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Configuraciones sin leer", + "unread.cutoff": "Días límite sin leer", + "unread.min-track-last": "Entradas mínimas en un tema antes de indicar la última leída.", + "recent": "Configuraciones recientes", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Desactivar el filtrado de teas en categorías ignoradas en la página /reciente", + "signature": "Configuraciones de Firma", + "signature.disable": "Desactivar firmas", + "signature.no-links": "Desactivar enlaces en firmas", + "signature.no-images": "Desactivar imágenes en firmas", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Longitud Máxima de Firma", + "composer": "Configuración del Editor de Texto", + "composer-help": "LAs configuraciones siguientes gobiernan la funcionalidad y/o apariencia del editor de entradas mostrado\n\t\t\t\ta los usuarios cuando crean nuevos temas, o responden a temas existentes.", + "composer.show-help": "Mostrar pestaña \"Ayuda\"", + "composer.enable-plugin-help": "Permitir a plugins añadir contenido a la pestaña de ayuda", + "composer.custom-help": "Texto de Ayuda Personalizado", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Seguimiento de IP", + "ip-tracking.each-post": "Seguir la IP para cada entrada/respuesta", + "enable-post-history": "Activar historial de respuestas" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/reputation.json b/public/language/es/admin/settings/reputation.json new file mode 100644 index 0000000000..acf5eb5ebd --- /dev/null +++ b/public/language/es/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Configuración de Reputación", + "disable": "Desactivar Sistema de Reputación", + "disable-down-voting": "Desactivar Votos Negativos", + "votes-are-public": "Todos los Votos son Públicos", + "thresholds": "Umbrales de Actividad", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Reputación mínima para votar negativamente", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Reputación negativa para denunciar", + "min-rep-website": "Reputación mínima para añadir \"Sitio web\" al perfil del usuario.", + "min-rep-aboutme": "Reputación mínima para añadir \"Sobre mi\" al perfil de usuario.", + "min-rep-signature": "Reputación mínima para añadir \"Firma\" al perfil de usuario", + "min-rep-profile-picture": "Reputación mínima para añadir \"Foto de Perfil\" al perfil de usuario.", + "min-rep-cover-picture": "Reputación mínima para añadir \"Foto de Portada\" al perfil del usuario", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/social.json b/public/language/es/admin/settings/social.json new file mode 100644 index 0000000000..b9a67b4758 --- /dev/null +++ b/public/language/es/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Compartir entradas", + "info-plugins-additional": "Los plugins pueden añadir redes adicionales para compartir entradas/respuestas.", + "save-success": "¡Redes de Compartir Entradas salvadas con éxito!" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/sockets.json b/public/language/es/admin/settings/sockets.json new file mode 100644 index 0000000000..3a1a28d760 --- /dev/null +++ b/public/language/es/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Configuración de Reconexión", + "max-attempts": "Máximo de Intentos de Reconexión", + "default-placeholder": "Por defecto: %1", + "delay": "Retraso de Reconexión" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/sounds.json b/public/language/es/admin/settings/sounds.json new file mode 100644 index 0000000000..4635433b80 --- /dev/null +++ b/public/language/es/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notificaciones", + "chat-messages": "Mensajes de Chat", + "play-sound": "Reproducir", + "incoming-message": "Mensaje Entrante", + "outgoing-message": "Mensaje Saliente", + "upload-new-sound": "Subir Sonido Nuevo", + "saved": "Configuración Guardada" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/tags.json b/public/language/es/admin/settings/tags.json new file mode 100644 index 0000000000..b0853840ee --- /dev/null +++ b/public/language/es/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Configuración de Etiqueta (tag)", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Etiquetas (tags) mínimas por tema", + "max-per-topic": "Etiquetas (tags) Máximas por Tema", + "min-length": "Longitud Mínima de Etiqueta (tag)", + "max-length": "Longitud Máxima de Etiqueta (tag)", + "related-topics": "Temas Relacionados", + "max-related-topics": "Máximo de temas relacionados para mostrar (si lo soporta el theme/plantilla)" +} \ No newline at end of file diff --git a/public/language/es/admin/settings/uploads.json b/public/language/es/admin/settings/uploads.json new file mode 100644 index 0000000000..2c2dca738f --- /dev/null +++ b/public/language/es/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Mensajes", + "orphans": "Orphaned Files", + "private": "Hacer las subidas de archivos privadas", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Extensiones de archivo para hacer privadas.", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Cambiar el tamaño de las imágenes si son más anchas que el ancho especificado", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Cambiar el tamaño de las imágenes hasta el ancho especificado", + "resize-image-width-help": "(En píxeles, predeterminado: 760 píxeles, configúrelo 0 para desactivar)", + "resize-image-quality": "Calidad a utlizar cuando se redimensionen imágenes", + "resize-image-quality-help": "Usar una calidad inferior para reducir el tamaño de archivo de las imágenes redimensionadas.", + "max-file-size": "Tamaño Máximo de Archivo (en KiB)", + "max-file-size-help": "(en kibibytes, por defecto: 2048 KiB)", + "reject-image-width": "Ancho máximo de la imágen (en píxeles)", + "reject-image-width-help": "Las imágenes más anchas que este valor serán rechazadas.", + "reject-image-height": "Altura máxima de la imágen (en píxeles)", + "reject-image-height-help": "Las imágenes más altas que este valor serán rechazadas.", + "allow-topic-thumbnails": "Permitir a los usuarios subir imágenes en miniatura para los temas", + "topic-thumb-size": "Tamaño de la Imagen en Miniatura para el Tema", + "allowed-file-extensions": "Permitir Extensiones de Archivo", + "allowed-file-extensions-help": "Introduzca una lista de extensiones de archivos, separadas por comas, aquí (por ejemplo: pdf,xls,doc). Una lista vacía significa que se permiten todas las extensiones.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Avatares de Perfil", + "allow-profile-image-uploads": "Permite a los usuarios subir imágenes de perfil", + "convert-profile-image-png": "Convierte las subidas de imágenes de perfil a PNG", + "default-avatar": "Avatar Personalizado Por Defecto", + "upload": "Subir", + "profile-image-dimension": "Dimensión de la Imagen de Perfil", + "profile-image-dimension-help": "(en píxeles, por defecto: 128 píxeles)", + "max-profile-image-size": "Tamaño Máximo de la Imagen de Perfil", + "max-profile-image-size-help": "(en kibibytes, por defecto: 256 KiB)", + "max-cover-image-size": "Tamaño Máximo de la Imagen de Portada", + "max-cover-image-size-help": "(en kibibytes, por defecto: 2048 KiB)", + "keep-all-user-images": "Mantener versiones antiguas de los avatares y portadas de los perfiles en el servidor.", + "profile-covers": "Portadas de Perfil", + "default-covers": "Portadas de Perfil por Defecto", + "default-covers-help": "Añadir imágenes de portada por defecto, separadas por coma, para cuentas que no hayan subido una imagen de portada" +} diff --git a/public/language/es/admin/settings/user.json b/public/language/es/admin/settings/user.json new file mode 100644 index 0000000000..60eebe04bf --- /dev/null +++ b/public/language/es/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Autenticación", + "email-confirm-interval": "El usuario no puede re-enviar una confirmación por email hasta", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Permitir login con", + "allow-login-with.username-email": "Nombre de usuario o Email", + "allow-login-with.username": "Solo Nombre de Usuario", + "account-settings": "Configuración de la Cuenta", + "gdpr_enabled": "Habilitar la recolección del consentimiento GDPR", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Desactivar cambios de nombre de usuario", + "disable-email-changes": "Desactivar cambios de email", + "disable-password-changes": "Desactivar cambios de contraseña", + "allow-account-deletion": "Permitir borrar cuenta", + "hide-fullname": "Esconder nombre completo de los usuarios", + "hide-email": "Esconder email de los usuarios", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Plantillas", + "disable-user-skins": "Impedir que los usuarios elijan plantilla", + "account-protection": "Protección de Cuenta", + "admin-relogin-duration": "Duración de re-acceso de administrador (minutos)", + "admin-relogin-duration-help": "Después de un determinado tiempo accediendo la sección de admin requiere volver a iniciar sesión, poner a 0 para desactivar", + "login-attempts": "Intentos de inicio de sesión por hora", + "login-attempts-help": "Si los intentos de acceso a la cuenta de un usuario superan este límite, esa cuenta será bloqueada durante un periodo de tiempo pre-configurado", + "lockout-duration": "Duración del Bloqueo de una Cuenta (minutos)", + "login-days": "Días para recordar los sesiones de inicio de usuario", + "password-expiry-days": "Forzar reseteo de contraseña después de un número de días determinado", + "session-time": "Tiempo de sesión", + "session-time-days": "Días", + "session-time-seconds": "Segundos", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutos después de que el usuario se considere inactivo", + "online-cutoff-help": "Si el usuario no realiza acciones durante este tiempo, se considerarán inactivos y no recibirán actualizaciones en tiempo real.", + "registration": "Registro de Usuario", + "registration-type": "Tipo de Registro", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Aprovación de Administrador", + "registration-type.admin-approval-ip": "Aprovación de Administrador para IPs", + "registration-type.invite-only": "Solo Invitación", + "registration-type.admin-invite-only": "Solo Invitación de Administrador", + "registration-type.disabled": "Sin Registro", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Máximo de Invitaciones por Usuario", + "max-invites": "Máximo de Invitaciones por Usuario", + "max-invites-help": "0 para no tener restricciones. Los administradores tienen derecho invitaciones infinitas
Solo aplicable a \"Solo con Invitación\"", + "invite-expiration": "Expiración de la invitación", + "invite-expiration-help": "# de días que en que las invitaciones expiran.", + "min-username-length": "Longitud Mínima de Nombre de Usuario", + "max-username-length": "Longitud Máxima de Nombre de Usuario", + "min-password-length": "Longitud Mínima de Contraseña", + "min-password-strength": "Fuerza Mínima de Contraseña", + "max-about-me-length": "Longitud Mínima de \"Sobre Mí\"", + "terms-of-use": "Términos de Uso del Foro (Dejar en blanco para desactivar)", + "user-search": "Búsqueda de Usuario\n", + "user-search-results-per-page": "Número de resultados para mostrar", + "default-user-settings": "Configuración de Usuario por Defecto", + "show-email": "Mostrar email", + "show-fullname": "Mostrar nombre completo", + "restrict-chat": "Solo permitir mensajes de chat de usuarios a los que sigo", + "outgoing-new-tab": "Abrir enlaces externos en una pestaña nueva", + "topic-search": "Habilitar Búsqueda Dentro de Tema", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Suscribirse a Informes", + "digest-freq.off": "Apagado", + "digest-freq.daily": "Diario", + "digest-freq.weekly": "Semanal", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Mensual", + "email-chat-notifs": "Enviar un correo electrónico si un mensaje de chat nuevo llega y no estoy conectado/a", + "email-post-notif": "Enviar un correo electrónico cuando se hacen respuestas a temas a los que estoy suscrito/a", + "follow-created-topics": "Seguir los temas que tu crees", + "follow-replied-topics": "Seguir los temas a los que contestas", + "default-notification-settings": "Configuración de notificación por defecto", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Vigilando ", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignorando" +} diff --git a/public/language/es/admin/settings/web-crawler.json b/public/language/es/admin/settings/web-crawler.json new file mode 100644 index 0000000000..111ab7182b --- /dev/null +++ b/public/language/es/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Configuración para Crawlers", + "robots-txt": "Personalizar Robots.txt Dejar en blanco para valores por defecto", + "sitemap-feed-settings": "Configuración de Sitemap & Feed", + "disable-rss-feeds": "Deshabilitar RSS Feeds", + "disable-sitemap-xml": "Deshabilitar Sitemap.xml", + "sitemap-topics": "Número de Temas para mostrar en el Sitemap", + "clear-sitemap-cache": "Limpiar Caché del Sitemap", + "view-sitemap": "Ver Sitemap" +} \ No newline at end of file diff --git a/public/language/es/category.json b/public/language/es/category.json new file mode 100644 index 0000000000..2990332fbd --- /dev/null +++ b/public/language/es/category.json @@ -0,0 +1,23 @@ +{ + "category": "Categoría", + "subcategories": "Subcategorías", + "new_topic_button": "Nuevo tema", + "guest-login-post": "Accede para escribir", + "no_topics": "No hay temas en esta categoría.
¿Por qué no te animas y publicas uno?", + "browsing": "viendo ahora", + "no_replies": "Nadie ha respondido aún", + "no_new_posts": "No hay mensajes nuevos.", + "watch": "Seguir", + "ignore": "Ignorar", + "watching": "Siguiendo", + "not-watching": "No siguiendo", + "ignoring": "Ignorando", + "watching.description": "Mostrar temas en no leídos y recientes", + "not-watching.description": "No mostrar temas en no leído, mostrar en reciente", + "ignoring.description": "No mostrar temas en no leídos y recientes.", + "watching.message": "Ahora estás viendo las actualizaciones de esta categoría y todas las subcategorías", + "notwatching.message": "No estás viendo las actualizaciones de esta categoría y todas las subcategorías.", + "ignoring.message": "Ahora estás ignorando las actualizaciones de esta categoría y todas las subcategorías", + "watched-categories": "Categorías seguidas", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/es/email.json b/public/language/es/email.json new file mode 100644 index 0000000000..aba236eb0d --- /dev/null +++ b/public/language/es/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "¡Restablecimiento de contraseña solicitada!", + "welcome-to": "Bienvenido a %1", + "invite": "Invitación de %1", + "greeting_no_name": "Hola", + "greeting_with_name": "Hola %1", + "email.verify-your-email.subject": "Favor de verificar su correo", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Gracias por registrarte con %1!", + "welcome.text2": "Para activar completamente tu cuenta, necesitamos verificar que la dirección email con la que te registraste te pertenece.", + "welcome.text3": "El administrador ha aceptado tu registro. Puedes acceder con tu usuario/contraseña ahora.", + "welcome.cta": "Cliquea aquí para confirmar tu dirección de email.", + "invitation.text1": "%1 te ha invitado a unirte a %2", + "invitation.text2": "Tu invitación expirará en %1 días.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Hemos recibido una solicitud para reiniciar tu contraseña, posiblemente porque la olvidaste. Si no es así, por favor, ignora este email.", + "reset.text2": "Para continuar con el reinicio de contraseña, por favor cliquea en el siguiente vínculo:", + "reset.cta": "Cliquea aquí para reiniciar tu contraseña", + "reset.notify.subject": "Se ha modificado correctamente la contraseña.", + "reset.notify.text1": "Te estamos notificando que en %1, tu contraseña ha sido cambiada correctamente.", + "reset.notify.text2": "Si no has sido tú, por favor notifica al administrador inmediatamente.", + "digest.latest_topics": "Últimos temas de %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Cliquea aquí para visitar %1", + "digest.unsub.info": "Este compendio te fue enviado debido a tus ajustes de subscripción.", + "digest.day": "día", + "digest.week": "semana", + "digest.month": "mes", + "digest.subject": "Resumen de %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Nuevo mensaje de chat recibido de %1", + "notif.chat.cta": "Haz click aquí para continuar la conversación", + "notif.chat.unsub.info": "Esta notificación de chat se te envió debido a tus ajustes de suscripción.", + "notif.post.unsub.info": "La notificación de este mensaje se te ha enviado debido a tus ajustes de subscripción.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Este es un email de prueba para verificar que el envío de email está ajustado correctamente para tu NodeBB", + "unsub.cta": "Haz click aquí para modificar los ajustes.", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Has sido baneado de %1", + "banned.text1": "El usuario %1 ha sido baneado de %2.", + "banned.text2": "Este ban dura hasta %1.", + "banned.text3": "La razón por la que has sido baneado: ", + "closing": "¡Gracias!" +} \ No newline at end of file diff --git a/public/language/es/error.json b/public/language/es/error.json new file mode 100644 index 0000000000..89111f9ce7 --- /dev/null +++ b/public/language/es/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Datos no válidos", + "invalid-json": "JSON no válido", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "No has iniciado sesión.", + "account-locked": "Tu cuenta ha sido bloqueada temporalmente.", + "search-requires-login": "¡Buscar requiere estar registrado! Por favor, entra o regístrate.", + "goback": "Pulsa \"atrás\" para volver a la página previa", + "invalid-cid": "Identificador de categoría no válido", + "invalid-tid": "Identificador de tema no válido", + "invalid-pid": "Identificador de publicación no válido", + "invalid-uid": "Identificador de usuario no válido", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Nombre de usuario no válido", + "invalid-email": "Correo electrónico no válido", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Título inválido", + "invalid-user-data": "Datos de usuario no válidos", + "invalid-password": "Contraseña no válida", + "invalid-login-credentials": "Datos de acceso no válidos", + "invalid-username-or-password": "Por favor especifica tanto un usuario como contraseña", + "invalid-search-term": "Término de búsqueda inválido", + "invalid-url": "URL inválida", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "El sistema de acceso local ha sido desactivado para usuarios con cuentas no privilegiadas.", + "csrf-invalid": "El acceso ha fallado porque tu sesión ha expirado. Por favor prueba otra vez.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Número de página inválido, debe estar entre %1 y %2", + "username-taken": "Nombre de usuario ocupado", + "email-taken": "Correo electrónico ocupado", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "No puedes usar el chat hasta que confirmes tu dirección de correo electrónico, por favor haz click aquí para confirmar tu correo.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "No se ha podido confirmar su email, por favor inténtelo de nuevo más tarde.", + "confirm-email-already-sent": "El email de confirmación ya ha sido enviado, por favor espera %1 minuto(s) para enviar otro.", + "sendmail-not-found": "El ejecutable \"sendmail\" no ha sido encontrado, por favor asegúrate de que esta instalado en tu sistema y es accesible por el usuario que ejecuta NodeBB. ", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Nombre de usuario es demasiado corto", + "username-too-long": "Nombre de usuario demasiado largo", + "password-too-long": "Contraseña muy corta", + "reset-rate-limited": "Demasiadas solicitudes de restablecimiento de contraseña (tasa limitada)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Usuario baneado", + "user-banned-reason": "Lo siento, esta cuenta ha sido baneada ( Razon: %1 )", + "user-banned-reason-until": "Lo siento, esta cuenta ha sido baneada hasta %1 ( Razon: %2 )", + "user-too-new": "Lo sentimos, es necesario que esperes %1 segundo(s) antes poder hacer tu primera publicación", + "blacklisted-ip": "Lo sentimos, tu dirección IP ha sido baneada de esta comunidad. Si crees que debe de haber un error, por favor contacte con un administrador.", + "ban-expiry-missing": "Por favor pon una fecha de fin del ban", + "no-category": "La categoría no existe", + "no-topic": "El tema no existe", + "no-post": "La publicación no existe", + "no-group": "El grupo no existe", + "no-user": "El usuario no existe", + "no-teaser": "El resumen no existe", + "no-flag": "Flag does not exist", + "no-privileges": "No tienes suficientes privilegios para realizar esta acción.", + "category-disabled": "Categoría deshabilitada", + "topic-locked": "Tema bloqueado", + "post-edit-duration-expired": "Sólo puedes editar mensajes durante %1 segundo(s) después de haberlo escrito", + "post-edit-duration-expired-minutes": "Sólo puedes editar mensajes durante %1 minuto(s) después de haberlo escrito", + "post-edit-duration-expired-minutes-seconds": "Solo puedes editar mensajes durante %1 minuto(s) y %2 segundo(s) después de haberlo escrito", + "post-edit-duration-expired-hours": "Solo puedes editar mensajes durante %1 hora(s) después de haberlo escrito", + "post-edit-duration-expired-hours-minutes": "Solo puedes editar mensajes durante %1 hora(s) y %2 minuto(s) después de haberlo escrito", + "post-edit-duration-expired-days": "Solo puedes editar mensajes durante %1 día(s) después de haberlo escrito", + "post-edit-duration-expired-days-hours": "Solo puedes editar mensajes durante %1 día(s) y %2 hora(s) después de haberlo escrito", + "post-delete-duration-expired": "Solo puedes borrar mensajes durante %1 segundo(s) después de haberlo escrito", + "post-delete-duration-expired-minutes": "Solo puedes borrar mensajes durante %1 minuto(s) después de haberlo escrito", + "post-delete-duration-expired-minutes-seconds": "Solo puedes borrar mensajes durante %1 minuto(s) y %2 segundo(s) después de haberlo escrito", + "post-delete-duration-expired-hours": "Solo puedes borrar mensajes durante %1 hora(s) después de haberlo escrito", + "post-delete-duration-expired-hours-minutes": "Solo puedes borrar mensajes durante %1 hora(s) y %2 minuto(s) después de haberlo escrito", + "post-delete-duration-expired-days": "Solo puedes borrar mensajes durante %1 día(s) después de haberlo escrito", + "post-delete-duration-expired-days-hours": "Solo puedes borrar mensajes durante %1 día(s) y %2 hora(s) después de haberlo escrito", + "cant-delete-topic-has-reply": "No puedes borrar tu tema después de que tenga respuestas", + "cant-delete-topic-has-replies": "No puedes borrar tu tema despues de que tenga ℅1 respuestas", + "content-too-short": "Por favor introduzca una publicación más larga. Las publicaciones deben contener al menos %1 caractere(s).", + "content-too-long": "Por favor introduzca un mensaje más corto. Los mensajes no pueden exceder los %1 caractere(s).", + "title-too-short": "Por favor introduzca un título más largo. Los títulos deben contener al menos %1 caractere(s).", + "title-too-long": "Por favor, introduce un título más corto, que no sobrepase los %1 caractere(s).", + "category-not-selected": "Categoría no seleccionada.", + "too-many-posts": "Solo puedes publicar una vez cada %1 segundo(s) - por favor espere antes de volver a publicar", + "too-many-posts-newbie": "Como nuevo usuario, solo puedes publicar una vez cada %1 segundo(s) hasta hayas ganado una reputación de %2 - por favor espera antes de volver a publicar", + "already-posting": "You are already posting", + "tag-too-short": "Por favor introduce una etiqueta más larga. Las etiquetas deben contener por lo menos %1 caractere(s)", + "tag-too-long": "Por favor introduce una etiqueta más corta. Las etiquetas no pueden exceder los %1 caractere(s)", + "not-enough-tags": "Etiquetas insuficientes. El tema debe tener al menos %1 etiqueta(s).", + "too-many-tags": "Demasiadas etiquetas. El tema no puede tener mas de %1 etiqueta(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Por favor, espera a que terminen las subidas.", + "file-too-big": "El tamaño de fichero máximo es de %1 kB - por favor, suba un fichero más pequeño", + "guest-upload-disabled": "Las subidas están deshabilitadas para los invitados", + "cors-error": "No se puede subir la imágen debido a CORS mal configurado", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Ya marcaste este mensaje", + "already-unbookmarked": "Ya desmarcarste este mensaje", + "cant-ban-other-admins": "¡No puedes expulsar a otros administradores!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Tu eres el unico administrador. Añade otro usuario como administrador antes de eliminarte a ti mismo.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Quitar privilegios de administrador de ésta cuenta antes de intentar borrarla", + "already-deleting": "Already deleting", + "invalid-image": "Imagen inválida", + "invalid-image-type": "Tipo de imagen inválido. Los tipos permitidos son: %1", + "invalid-image-extension": "Extensión de imagen inválida", + "invalid-file-type": "Tipo de fichero inválido. Los tipos permitidos son: %1", + "invalid-image-dimensions": "Las dimensiones de la imágen son demasiado grandes.", + "group-name-too-short": "Nombre del grupo es demasiado corto.", + "group-name-too-long": "Nombre de grupo demasiado largo", + "group-already-exists": "El grupo ya existe.", + "group-name-change-not-allowed": "El nombre del grupo deseado no está permitido.", + "group-already-member": "Ya eres miembro de este grupo", + "group-not-member": "No eres miembro de este grupo", + "group-needs-owner": "Este grupo requiere al menos un propietario", + "group-already-invited": "Este usuario ya ha sido invitado", + "group-already-requested": "Tu solicitud de miembro ya ha sido enviada", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Este publicación ya ha sido borrada", + "post-already-restored": "Esta publicación ya ha sido restaurada", + "topic-already-deleted": "Este tema ya ha sido borrado", + "topic-already-restored": "Este tema ya ha sido restaurado", + "cant-purge-main-post": "No puedes purgar el mensaje principal, por favor utiliza borrar tema", + "topic-thumbnails-are-disabled": "Las miniaturas de los temas están deshabilitadas.", + "invalid-file": "Archivo no válido", + "uploads-are-disabled": "Las subidas están deshabilitadas.", + "signature-too-long": "Lo sentimos, pero tu firma no puede ser más larga de %1 caractere(s).", + "about-me-too-long": "Lo sentimos, pero tu descripción no puede ser más larga de %1 caractere(s).", + "cant-chat-with-yourself": "¡No puedes conversar contigo mismo!", + "chat-restricted": "Este usuario tiene restringidos los mensajes de chat. Los usuarios deben seguirte antes de que pueda charlar con ellos", + "chat-disabled": "El sistema de chat está deshabilitado", + "too-many-messages": "Has enviado demasiados mensajes, por favor espera un poco.", + "invalid-chat-message": "Mensaje de Chat inválido", + "chat-message-too-long": "Los mensajes de chat no pueden ser mas largo de %1 caracteres.", + "cant-edit-chat-message": "No tienes permiso para editar este mensaje", + "cant-delete-chat-message": "No tienes permiso para eliminar este mensaje", + "chat-edit-duration-expired": "Sólo se te permite editar mensajes de chat durante %1 segundo(s) después de enviar el mensaje", + "chat-delete-duration-expired": "Sólo se te permite borrar mensajes de chat durante %1 segundo(s) después de enviar el mensaje", + "chat-deleted-already": "Este mensaje de chat ya ha sido borrado.", + "chat-restored-already": "Este mensaje de chat ya ha sido restaurado.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Ya has votado a este mensaje.", + "reputation-system-disabled": "El sistema de reputación está deshabilitado.", + "downvoting-disabled": "La votación negativa está deshabilitada.", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "No puedes votar en tu propio mensaje", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB encontró un problema al refrescar: \"%1\". NodeBB intentará cargar el resto de contenido, aunque deberías deshacer lo que hiciste justo antes.", + "registration-error": "Error de registro", + "parse-error": "Algo ha ido mal mientras se parseaba la respuesta del servidor", + "wrong-login-type-email": "Por favor emplea tu email para acceder", + "wrong-login-type-username": "Por favor introduce tu nombre de usuario para acceder", + "sso-registration-disabled": "El registro ha sido desactivado para %1 cuentas, por favor, regístrese con una cuenta de correo primero", + "sso-multiple-association": "No puedes asociar múltiples cuentas desde este servicio a tu cuenta NodeBB. Por favor, disocia tu cuenta ya existente y vuelve a intentarlo.", + "invite-maximum-met": "Has alcanzado el número máximo de personas invitadas (%1 de %2).", + "no-session-found": "¡No se ha encontrado ningún inicio de sesión!", + "not-in-room": "El usuario no está en la sala", + "cant-kick-self": "No te puedes expulsar a ti mismo del grupo", + "no-users-selected": "Ningun usuario(s) seleccionado", + "invalid-home-page-route": "Ruta de página de inicio invalida", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "¡No se han seleccionado temas!", + "cant-move-to-same-topic": "¡No puedes mover el mensaje al mismo tema!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "¡No puedes bloquearte a tí mismo!", + "cannot-block-privileged": "No puedes bloquear administradores o moderadores globales", + "cannot-block-guest": "Los invitados no pueden bloquear a otros usuarios", + "already-blocked": "Este usuario ya está bloqueado.", + "already-unblocked": "Este usuario ya está desbloqueado.", + "no-connection": "Parece haber un problema con tu conexión a internet", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/es/flags.json b/public/language/es/flags.json new file mode 100644 index 0000000000..af9aa33ad9 --- /dev/null +++ b/public/language/es/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Estado", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Yeah! No se encontraron indicadores", + "assignee": "Asignado", + "update": "Actualizar", + "updated": "Actualizado", + "resolved": "Resolved", + "target-purged": "El contenido al que se refiere este indicador ha sido purgado y ya no está disponible.", + + "graph-label": "Daily Flags", + "quick-filters": "Filtros rapidos", + "filter-active": "Hay uno o más filtros activos en esta lista de indicadores.", + "filter-reset": "Quitar filtros", + "filters": "Opciones de filtros", + "filter-reporterId": "UID del reportador", + "filter-targetUid": "Indicador UID", + "filter-type": "Tipo de indicador", + "filter-type-all": "Todo el contenido", + "filter-type-post": "Mensaje", + "filter-type-user": "Usuario", + "filter-state": "estado", + "filter-assignee": "UID asignado", + "filter-cid": "Categoria", + "filter-quick-mine": "Asignado a mí", + "filter-cid-all": "Todas las categorias", + "apply-filters": "Aplicar filtros", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Usuario marcado", + "view-profile": "Ver perfil", + "start-new-chat": "Empezar nuevo chat", + "go-to-target": "Ver objetivo marcado", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Ver perfil", + "user-edit": "Editar perfil", + + "notes": "Marcar notas", + "add-note": "Añadir nota", + "no-notes": "No hay notas compartidas", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Nota añadida", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No hay registro de marcadores", + + "state-all": "Todos los estados", + "state-open": "Nuevo/Abrir", + "state-wip": "Trabajo en proceso", + "state-resolved": "Resuelto", + "state-rejected": "Rechazado", + "no-assignee": "Sin asignar", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Por favor especifica tu razón para marcar %1 %2 para revisar. Alternativamente, usa una de los botones de reporte rápido si corresponde.", + "modal-reason-spam": "Correo no deseado", + "modal-reason-offensive": "Ofensivo", + "modal-reason-other": "Otro (especificar debajo)", + "modal-reason-custom": "Razón para reportar este contenido...", + "modal-submit": "Enviar reporte", + "modal-submit-success": "El contenido se ha reportado para moderación.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/es/global.json b/public/language/es/global.json new file mode 100644 index 0000000000..7cf340dc3f --- /dev/null +++ b/public/language/es/global.json @@ -0,0 +1,126 @@ +{ + "home": "Inicio", + "search": "Buscar", + "buttons.close": "Cerrar", + "403.title": "Acceso denegado", + "403.message": "Al parecer has llegado a una página a la cual no tienes permisos para acceder.", + "403.login": "¿Quizás deberías intentar acceder?", + "404.title": "No encontrado", + "404.message": "Al parecer has llegado a una página a la cual no tienes permisos para acceder. Volver a la página de inicio .", + "500.title": "Error interno.", + "500.message": "¡Ooops! ¡Parece que algo salió mal! No te preocupes, ¡nuestros simios hiperinteligentes lo solucionarán!", + "400.title": "Petición incorrecta.", + "400.message": "Parece que la dirección es errónea, por favor compruébala y prueba otra vez. En caso contrario vuelve al inicio.", + "register": "Registrarse", + "login": "Conectarse", + "please_log_in": "Por favor, identifíquese.", + "logout": "Salir", + "posting_restriction_info": "Para publicar se requiere ser usuario registrado, conéctate o regístrate.", + "welcome_back": "¡Bienvenido de nuevo!", + "you_have_successfully_logged_in": "Identificado satisfactoriamente", + "save_changes": "Guardar cambios", + "save": "Guardar", + "close": "Cerrar", + "pagination": "Paginación", + "pagination.out_of": "%1 de %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Administración", + "header.categories": "Categorías", + "header.recent": "Recientes", + "header.unread": "No leídos", + "header.tags": "Etiquetas", + "header.popular": "Popular", + "header.top": "Top", + "header.users": "Usuarios", + "header.groups": "Grupos", + "header.chats": "Chats", + "header.notifications": "Notificaciones", + "header.search": "Buscar", + "header.profile": "Perfil", + "header.navigation": "Navegación", + "notifications.loading": "Cargando notificaciones", + "chats.loading": "Cargando chats", + "motd.welcome": "Bienvenido a NodeBB, la plataforma de debate del futuro.", + "previouspage": "Página anterior", + "nextpage": "Página siguiente", + "alert.success": "¡Éxito!", + "alert.error": "Error", + "alert.banned": "Baneado", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Ya no estás siguiendo a %1!", + "alert.follow": "Ahora sigues a %1!", + "users": "Usuarios", + "topics": "Temas", + "posts": "Mensajes", + "x-posts": "%1 posts", + "best": "Mejor valorados", + "controversial": "Controversial", + "votes": "Votos", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Positivos", + "upvoted": "Votado positivamente", + "downvoters": "Negativos", + "downvoted": "Votado negativamente", + "views": "Visitas", + "posters": "Posters", + "reputation": "Reputación", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "leer más", + "more": "Más", + "none": "None", + "posted_ago_by_guest": "publicado %1 por Invitado", + "posted_ago_by": "publicado %1 por %2", + "posted_ago": "publicado %1", + "posted_in": "publicado en %1", + "posted_in_by": "publicado en %1 por %2", + "posted_in_ago": "publicado en %1 %2", + "posted_in_ago_by": "publicado en %1 %2 por %3", + "user_posted_ago": "%1 publicó %2", + "guest_posted_ago": "Invitado publicó %1", + "last_edited_by": "Última edición por %1", + "norecentposts": "No hay publicaciones recientes", + "norecenttopics": "No hay temas recientes", + "recentposts": "Publicaciones recientes", + "recentips": "IP's conectadas recientemente", + "moderator_tools": "Herramientas de Moderación", + "online": "Conectado", + "away": "Ausente", + "dnd": "No molestar", + "invisible": "Invisible", + "offline": "Desconectado", + "email": "Email", + "language": "Idioma", + "guest": "Invitado", + "guests": "Invitados", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Foro actualizado", + "updated.message": "El foro acaba de ser actualizado a la última versión. Haz click aquí para refrescar la página.", + "privacy": "Privacidad", + "follow": "Seguir", + "unfollow": "Dejar de Seguir", + "delete_all": "Eliminar todo", + "map": "Mapa", + "sessions": "Inicios de sesión", + "ip_address": "Direcciones IP", + "enter_page_number": "Escribe el número de página", + "upload_file": "Subir archivo", + "upload": "Subir", + "uploads": "Subidas", + "allowed-file-types": "Los tipos de archivos permitidos son: %1", + "unsaved-changes": "Tienes cambios sin guardar. Seguro que quieres salir?", + "reconnecting-message": "Has perdido la conexión. Reconectando a %1.", + "play": "Reproducir", + "cookies.message": "Esta web usa cookies para asegurar que usted recibe la mejor experiencia de navegación.", + "cookies.accept": "De Acuerdo!", + "cookies.learn_more": "Quiero saber más", + "edited": "Editado", + "disabled": "Desahabilitado", + "select": "Seleccionar", + "user-search-prompt": "Escriba algo aquí para encontrar usuarios..." +} \ No newline at end of file diff --git a/public/language/es/groups.json b/public/language/es/groups.json new file mode 100644 index 0000000000..a00c4035e4 --- /dev/null +++ b/public/language/es/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupos", + "view_group": "Ver Grupo", + "owner": "Propietario del Grupo", + "new_group": "Crear Nuevo Grupo", + "no_groups_found": "No hay grupos que ver", + "pending.accept": "Aceptar", + "pending.reject": "Rechazar", + "pending.accept_all": "Aceptar todo", + "pending.reject_all": "Rechazar todo", + "pending.none": "No hay miembros pendientes en este momento", + "invited.none": "No hay miembros invitados en este momento", + "invited.uninvite": "Cancelar invitación", + "invited.search": "Buscar un usuario para invitar a este grupo", + "invited.notification_title": "Te han invitado a unirte a %1", + "request.notification_title": "Petición de Membresía de Grupo de %1", + "request.notification_text": "%1 a pedido volverse miembro de %2", + "cover-save": "Guardar", + "cover-saving": "Guardando", + "details.title": "Detalles de Grupo", + "details.members": "Lista de Miembros", + "details.pending": "Miembros Pendientes", + "details.invited": "Miembros Invitados", + "details.has_no_posts": "Los miembros de este grupo no han hecho ninguna publicación.", + "details.latest_posts": "Últimas Publicaciones", + "details.private": "Privado", + "details.disableJoinRequests": "Desactivar las peticiones de unión", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Conceder/Rescindir Propiedad", + "details.kick": "Expulsar", + "details.kick_confirm": "¿ Estás seguro de que quieres eliminar a este miembro del grupo ?", + "details.add-member": "Agregar miembro", + "details.owner_options": "Administración De Grupo", + "details.group_name": "Nombre de Grupo", + "details.member_count": "Numero de Miembros", + "details.creation_date": "Fecha de Creacion", + "details.description": "Descripción", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Previsualización de Insignia", + "details.change_icon": "Cambiar Icono", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Texto de Insignia", + "details.userTitleEnabled": "Mostrar Insignia", + "details.private_help": "Si está habilitado, entrar en los grupos requiere aprobación de sus propietarios", + "details.hidden": "Oculto", + "details.hidden_help": "Si está habilitado, este grupo no aparecerá en los listados de grupos, y los usuarios tendrán que ser invitados manualmente", + "details.delete_group": "Eliminar grupo", + "details.private_system_help": "Los grupos privados están desactivados a nivel de sistema, esta opción no cambiará nada.", + "event.updated": "Los detalles del grupo han sido actualizados", + "event.deleted": "El grupo \"%1\" ha sido eliminado", + "membership.accept-invitation": "Aceptar Invitación", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitación Pendiente", + "membership.join-group": "Unirse al grupo", + "membership.leave-group": "Dejar el grupo", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Rechazar", + "new-group.group_name": "Nombre de Grupo:", + "upload-group-cover": "Cargar foto para el grupo", + "bulk-invite-instructions": "Escribe una lista de nombres de usuario separados por comas a invitar a este grupo", + "bulk-invite": "Invitación multiple", + "remove_group_cover_confirm": "¿ Estás seguro de que quieres eliminar la imagen de portada?" +} \ No newline at end of file diff --git a/public/language/es/ip-blacklist.json b/public/language/es/ip-blacklist.json new file mode 100644 index 0000000000..1fa3233d8a --- /dev/null +++ b/public/language/es/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configura tu lista negra de IPs aquí.", + "description": "Ocasionalmente, un ban de una cuenta de usuario no es suficiente disuasión. En otras ocasiones, restringir el acceso al foro a una IP concreta o a un rango de IPs es la mejor manera de proteger un foro. En estos escenarios, puedes añadir IP problemáticas o bloques CIDR enteros a esta lista negra, y no podrán acceder, hacer login ni registrarse con cuentas nuevas.", + "active-rules": "Reglas Activas", + "validate": "Validar Lista Negra", + "apply": "Aplicar Lista Negra", + "hints": "Pistas de Sintaxis", + "hint-1": "Define una única IP por línea. Puedes añadir bloques de IP siempre que tengan el formato CiDR ( por ejemplo: 192.168.100.0/22).", + "hint-2": "Puedes añadir comentarios comenzando las líneas con el símbolo #.", + + "validate.x-valid": "%1de%2regla(s) válidas.", + "validate.x-invalid": "Las siguientes %1 reglas son inválidas:", + + "alerts.applied-success": "Lista Negra Aplicada", + + "analytics.blacklist-hourly": "Figura 1– notificaciones de Lista Negra por hora.", + "analytics.blacklist-daily": "Figura 2– notificaciones de Lista Negra por dia", + "ip-banned": "IP baneada" +} \ No newline at end of file diff --git a/public/language/es/language.json b/public/language/es/language.json new file mode 100644 index 0000000000..a568ba27c3 --- /dev/null +++ b/public/language/es/language.json @@ -0,0 +1,5 @@ +{ + "name": "Español", + "code": "es", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/es/login.json b/public/language/es/login.json new file mode 100644 index 0000000000..6596ea35f0 --- /dev/null +++ b/public/language/es/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Usuario / Email", + "username": "Usuario", + "remember_me": "¿Recordarme?", + "forgot_password": "¿Olvidaste tu contraseña?", + "alternative_logins": "Accesos alternativos", + "failed_login_attempt": "Error al iniciar sesión", + "login_successful": "¡Identificado satisfactoriamente!", + "dont_have_account": "¿Aún no tienes cuenta?", + "logged-out-due-to-inactivity": "Debido a la inactividad has sido deslogueado del Panel de Control de Administradores", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/es/modules.json b/public/language/es/modules.json new file mode 100644 index 0000000000..d089f3213f --- /dev/null +++ b/public/language/es/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chatear con", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Enviar", + "chat.no_active": "No tiene conversaciones activas.", + "chat.user_typing": "%1 está escribiendo...", + "chat.user_has_messaged_you": "%1 te ha enviado un mensaje.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Por favor, selecciona un contacto para ver el historial de mensajes", + "chat.no-users-in-room": "No hay usuarios en esta sala", + "chat.recent-chats": "Chats recientes", + "chat.contacts": "Contactos", + "chat.message-history": "Historial de mensajes", + "chat.message-deleted": "Message Deleted", + "chat.options": "Opciones de chat", + "chat.pop-out": "Mostrar en ventana independiente", + "chat.minimize": "Minimizar", + "chat.maximize": "Maximizar", + "chat.seven_days": "7 días", + "chat.thirty_days": "30 días", + "chat.three_months": "3 meses", + "chat.delete_message_confirm": "¿Estás seguro de que deseas eliminar este mensaje?", + "chat.retrieving-users": "Cargando usuarios...", + "chat.manage-room": "Administrar Sala de Chat", + "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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "Este usuario está en modo No molestar. ¿ Estás seguro de que quieres chatear con él ?", + "chat.rename-room": "Renombrar Sala", + "chat.rename-placeholder": "Introduzca el nombre de su sala aquí", + "chat.rename-help": "El nombre de sala elegido será visible por todos los participantes en la sala", + "chat.leave": "Abandonar Chat", + "chat.leave-prompt": "Está seguro/a de que desea abandonar este chat?", + "chat.leave-help": "Abandonar este chat te excluirá de futuras interacciones en este chat. Si eres re-añadido en el futuro, no verás ningún historial de chat anterior a tu vuelta.", + "chat.in-room": "En esta sala", + "chat.kick": "Expulsar", + "chat.show-ip": "Mostrar IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Crear", + "composer.show_preview": "Ver Previsualización", + "composer.hide_preview": "Ocultar Previsualización", + "composer.user_said_in": "%1 dijo en %2:", + "composer.user_said": "%1 dijo:", + "composer.discard": "¿Estás seguro de que deseas descartar este mensaje?", + "composer.submit_and_lock": "Enviar y Bloquear", + "composer.toggle_dropdown": "Alternar desplegable", + "composer.uploading": "Subiendo %1", + "composer.formatting.bold": "Negrita", + "composer.formatting.italic": "Itálica", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Tachado", + "composer.formatting.code": "Código", + "composer.formatting.link": "Enlace", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Subir foto", + "composer.upload-file": "Subir archivo", + "composer.zen_mode": "Modo Zen", + "composer.select_category": "Selecciona una categoría", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancelar", + "bootbox.confirm": "Confirmar", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Colocar la foto de portada", + "cover.dragging_message": "Arrastra la foto de portada al lugar que quieras y haz click en \"Guardar\"", + "cover.saved": "Imagen y posición de la foto de portada guardadas.", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/es/notifications.json b/public/language/es/notifications.json new file mode 100644 index 0000000000..225a649f43 --- /dev/null +++ b/public/language/es/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notificaciones", + "no_notifs": "No tienes nuevas notificaciones", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Volver a %1", + "outgoing_link": "Enlace externo", + "outgoing_link_message": "Ahora estás saliendo %1", + "continue_to": "Continuar a %1", + "return_to": "Regresar a %1", + "new_notification": "Tú tienes una nueva notificación ", + "you_have_unread_notifications": "Tienes notificaciones sin leer.", + "all": "Todo", + "topics": "Temas", + "replies": "Respuestas", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Seguidores", + "upvote": "Votos positivos", + "new-flags": "Nuevos reportes", + "my-flags": "Reportado asignado a mí", + "bans": "Baneos", + "new_message_from": "Nuevo mensaje de %1", + "upvoted_your_post_in": "%1 ha votado positivamente tu respuesta en %2.", + "upvoted_your_post_in_dual": "%1 y %2 han votado positivamente tu respuesta en %3.", + "upvoted_your_post_in_multiple": "%1 y otras %2 personas han votado positivamente tu respuesta en %3.", + "moved_your_post": "%1 su tema ha sido movido a %2", + "moved_your_topic": "%1 ha movido %2", + "user_flagged_post_in": "%1 ha reportado una respuesta en %2", + "user_flagged_post_in_dual": "%1 y %2 han reportado un post en %3", + "user_flagged_post_in_multiple": "%1 y otras %2 personas han reportado un post en %3", + "user_flagged_user": "%1 reportó el perfil (%2) ", + "user_flagged_user_dual": "%1 y %2 reportaron el perfil (%3)", + "user_flagged_user_multiple": "%1 y otros %2 reportaron el perfil (%3) ", + "user_posted_to": "%1 ha respondido a: %2", + "user_posted_to_dual": "%1 y %2 han respondido a %3", + "user_posted_to_multiple": "%1 y otras %2 personas han respondido a: %3", + "user_posted_topic": "%1 ha publicado un nuevo tema: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 comenzó a seguirte.", + "user_started_following_you_dual": "%1 y %2 comenzaron a seguirte.", + "user_started_following_you_multiple": "%1 y otras %2 personas comenzaron a seguirte.", + "new_register": "%1 envió una solicitud de registro.", + "new_register_multiple": "Hay %1 peticiones de registros pendientes de revisión", + "flag_assigned_to_you": "Reporte %1 te ha sido asignado.", + "post_awaiting_review": "Entrada esperando revisión", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Correo electrónico confirmado", + "email-confirmed-message": "Gracias por validar tu correo electrónico. Tu cuenta ya está completamente activa.", + "email-confirm-error-message": "Hubo un problema al validar tu cuenta de correo electrónico. Quizá el código era erróneo o expiró...", + "email-confirm-sent": "Correo de confirmación enviado.", + "none": "Ninguno/a", + "notification_only": "Solo Notificación", + "email_only": "Solo Email", + "notification_and_email": "Notificación & Email", + "notificationType_upvote": "Cuando alguien vota positivamente en tu entrada", + "notificationType_new-topic": "Cuando alguien a quien sigues comenta en un tema", + "notificationType_new-reply": "Cuando hay una respuesta nueva en un tema que estás viendo", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "Cuando alguien comienza a seguirte", + "notificationType_new-chat": "Cuando recibes un mensaje de chat", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "Cuando recibes una invitación a un grupo", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "Cuando alguien es añadido a una cola de registro", + "notificationType_post-queue": "Cuando un mensaje nuevo entra en la cola", + "notificationType_new-post-flag": "Cuando un mensaje es denunciado", + "notificationType_new-user-flag": "Cuando un usuario es denunciado" +} \ No newline at end of file diff --git a/public/language/es/pages.json b/public/language/es/pages.json new file mode 100644 index 0000000000..a7767236c0 --- /dev/null +++ b/public/language/es/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Inicio", + "unread": "Temas no leídos", + "popular-day": "Temas populares hoy", + "popular-week": "Temas populares de la semana", + "popular-month": "Temas populares del mes", + "popular-alltime": "Temas populares de siempre", + "recent": "Temas recientes", + "top-day": "Temas más votados hoy", + "top-week": "Temas más votados de esta semana", + "top-month": "Temas más votados este mes", + "top-alltime": "Temas más votados", + "moderator-tools": "Herramientas de Moderadores", + "flagged-content": "Contenido reportado", + "ip-blacklist": "Lista negra de IPS", + "post-queue": "Cola de Mensajes", + "users/online": "Conectados", + "users/latest": "Últimos usuarios", + "users/sort-posts": "Top por mensajes", + "users/sort-reputation": "Más reputados", + "users/banned": "Usuarios baneados", + "users/most-flags": "Usuarios mas reportados", + "users/search": "Buscar", + "notifications": "Notificaciones", + "tags": "Etiquetas", + "tag": "Temas etiquetados (tag) bajo "%1"", + "register": "Registrar una cuenta", + "registration-complete": "Registro completado", + "login": "Acceder a tu cuenta", + "reset": "Restablecer contraseña", + "categories": "Categorías", + "groups": "Grupos", + "group": "Grupo de %1", + "chats": "Chats", + "chat": "Chatear con %1", + "flags": "Reportes", + "flag-details": "Detalle de reporte %1", + "account/edit": "Editar \"%1\"", + "account/edit/password": "Editar contraseña de \"%1\"", + "account/edit/username": "Editar nombre de usuario de \"%1\"", + "account/edit/email": "Editar email \"%1\"", + "account/info": "Información de cuenta", + "account/following": "Gente que sigue %1", + "account/followers": "Seguidores de %1", + "account/posts": "Publicados por %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Temas creados por %1", + "account/groups": "Grupos de %1", + "account/watched_categories": "%1 Categorías seguidas", + "account/bookmarks": "%1's Mensajes marcados", + "account/settings": "Preferencias", + "account/watched": "Temas seguidos por %1", + "account/ignored": "Temas ignorados por %1", + "account/upvoted": "Publicaciones votadas positivamente %1", + "account/downvoted": "Publicaciones votadas negativamente %1", + "account/best": "Mejores publicaciones hechas por %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Usuarios bloqueados por %1", + "account/uploads": "Subidas por %1", + "account/sessions": "Login Sessions", + "confirm": "Correo electrónico confirmado", + "maintenance.text": "%1 está en mantenimiento actualmente. Por favor vuelva en otro momento.", + "maintenance.messageIntro": "Además, la administración ha dejado este mensaje:", + "throttled.text": "%1 no está disponible debido a una carga excesiva. Por favor vuelva en otro momento" +} \ No newline at end of file diff --git a/public/language/es/post-queue.json b/public/language/es/post-queue.json new file mode 100644 index 0000000000..7ed6275827 --- /dev/null +++ b/public/language/es/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Cola de Mensajes", + "description": "No hay publicaciones en cola.
Para habilitar esta funcionalidad, ir a Ajustes/Publicar/Cola de Publicacionesy habilitar Cola de publicaciones.", + "user": "Usuario", + "category": "Categoría", + "title": "Título", + "content": "Contenido", + "posted": "Publicado", + "reply-to": "Responder a %1", + "content-editable": "Click en el contenido para editar", + "category-editable": "Click en la categoría para editar", + "title-editable": "Click en el título para editar", + "reply": "Responder", + "topic": "Tema", + "accept": "Aceptar", + "reject": "Rechazar", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/es/recent.json b/public/language/es/recent.json new file mode 100644 index 0000000000..8c959ca0d5 --- /dev/null +++ b/public/language/es/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Reciente", + "day": "Día", + "week": "Semana", + "month": "Mes", + "year": "Año", + "alltime": "Siempre", + "no_recent_topics": "No hay publicaciones recientes.", + "no_popular_topics": "No hay publicaciones populares", + "there-is-a-new-topic": "Hay una nueva publicación.", + "there-is-a-new-topic-and-a-new-post": "hay una nueva publicación y un nuevo mensaje.", + "there-is-a-new-topic-and-new-posts": "Hay una nueva publicación y %1 nuevos mensajes.", + "there-are-new-topics": "Hay %1 nuevos mensajes.", + "there-are-new-topics-and-a-new-post": "Hay %1 nuevas publicaciones y un nuevo mensaje.", + "there-are-new-topics-and-new-posts": "Hay %1 nuevas publicaciones y %2 nuevos mensajes.", + "there-is-a-new-post": "Hay un nuevo mensaje.", + "there-are-new-posts": "Hay %1 nuevos mensajes.", + "click-here-to-reload": "Click para recargar." +} \ No newline at end of file diff --git a/public/language/es/register.json b/public/language/es/register.json new file mode 100644 index 0000000000..e94120861a --- /dev/null +++ b/public/language/es/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registrarse", + "cancel_registration": "Cancelar registro", + "help.email": "Por defecto, tu cuenta de correo electrónico estará oculta al publico.", + "help.username_restrictions": "El nombre de usuario debe tener entre %1 y %2 carácteres. Los miembros pueden responderte escribiendo @usuario.", + "help.minimum_password_length": "Tu contraseña debe tener al menos %1 carácteres.", + "email_address": "Correo electrónico", + "email_address_placeholder": "Escribe tu correo electrónico", + "username": "Nombre de usuario", + "username_placeholder": "Introduce tu nombre de usuario", + "password": "Contraseña", + "password_placeholder": "Introduce tu contraseña", + "confirm_password": "Confirmar contraseña", + "confirm_password_placeholder": "Confirmar contraseña", + "register_now_button": "Registrarse ahora", + "alternative_registration": "Métodos de registro alternativos", + "terms_of_use": "Términos y Condiciones de uso", + "agree_to_terms_of_use": "Acepto los Términos y Condiciones de uso", + "terms_of_use_error": "Debes aceptar los términos de uso", + "registration-added-to-queue": "Tu registro se ha añadido a la cola de aprobación,. Recibirás un correo cuando este sea aceptado por un administrador. ", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Consiento la recolección y el procesamiento de mi información personal en este sitio web", + "gdpr_agree_email": "Consiento en recibir correos de informes y notificaciones de este sitio web.", + "gdpr_consent_denied": "Usted debe dar consentimiento a este sitio para recolectar/procesar su información, así como enviarle correos electrónicos.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/es/reset_password.json b/public/language/es/reset_password.json new file mode 100644 index 0000000000..bb42f9cdee --- /dev/null +++ b/public/language/es/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Restablecer contraseña", + "update_password": "Actualizar contraseña", + "password_changed.title": "Contraseña cambiada", + "password_changed.message": "

La contraseña fue modificada con éxito, por favor inicia sesión de nuevo.", + "wrong_reset_code.title": "Código de restablecimiento incorrecto", + "wrong_reset_code.message": "El código de restablecimiento ingresado no es correcto. Por favor inténtalo de nuevo o solicita un nuevo código.", + "new_password": "Nueva contraseña", + "repeat_password": "Confirmar contraseña", + "changing_password": "Changing Password", + "enter_email": "Por favor ingresa tu correo electrónico y te enviaremos un mensaje con indicaciones para restablecer tu cuenta.", + "enter_email_address": "Introduce tu correo electrónico", + "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.", + "invalid_email": "¡Correo electrónico no válido o inexistente!", + "password_too_short": "La contraseña introducida es demasiado corta, por favor introduzca una contraseña diferente.", + "passwords_do_not_match": "Las dos contraseñas introducidas no concuerdan.", + "password_expired": "Tu contraseña ha caducado, por favor elige una contraseña nueva" +} \ No newline at end of file diff --git a/public/language/es/search.json b/public/language/es/search.json new file mode 100644 index 0000000000..24220b902a --- /dev/null +++ b/public/language/es/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 resuldado(s) coinciden con \"%2\". (%3 segundos)", + "no-matches": "No se encontraron coincidencias", + "advanced-search": "Búsqueda Avanzada", + "in": "En", + "titles": "Títulos", + "titles-posts": "Títulos y posts", + "match-words": "Que coincidan las palabras", + "all": "Todos", + "any": "Cualquiera", + "posted-by": "Publicado por", + "in-categories": "En categorías", + "search-child-categories": "Buscar categorías hijas", + "has-tags": "Hay etiquetas", + "reply-count": "Número de Respuestas", + "at-least": "De mínimo", + "at-most": "De máximo", + "relevance": "Relevancia", + "post-time": "Fecha de publicación", + "votes": "Votos", + "newer-than": "Más reciente que", + "older-than": "Más antiguo que", + "any-date": "Cualquier fecha", + "yesterday": "Ayer", + "one-week": "Una semana", + "two-weeks": "Dos semanas", + "one-month": "Un mes", + "three-months": "Tres meses", + "six-months": "Seis meses", + "one-year": "Un año", + "sort-by": "Ordenar por", + "last-reply-time": "Fecha de última respuesta", + "topic-title": "Título de tema", + "topic-votes": "Votos de tema", + "number-of-replies": "Número de respuestas", + "number-of-views": "Número de visualizaciones", + "topic-start-date": "Fecha de inicio del tema", + "username": "Usuario", + "category": "Categoría", + "descending": "En orden descendente", + "ascending": "En orden ascendente", + "save-preferences": "Guardar preferencias", + "clear-preferences": "Descartar preferencias", + "search-preferences-saved": "Preferencias de búsqueda guardadas", + "search-preferences-cleared": "Preferencias de búsqueda descartadas", + "show-results-as": "Mostrar resultados como", + "see-more-results": "Ver mas resultados (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/es/success.json b/public/language/es/success.json new file mode 100644 index 0000000000..80dbf11c21 --- /dev/null +++ b/public/language/es/success.json @@ -0,0 +1,7 @@ +{ + "success": "¡Éxito!", + "topic-post": "Mensaje publicado satisfactoriamente.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Identificado satisfactoriamente", + "settings-saved": "¡Ajustes guardados satisfactoriamente!" +} \ No newline at end of file diff --git a/public/language/es/tags.json b/public/language/es/tags.json new file mode 100644 index 0000000000..c61d272cde --- /dev/null +++ b/public/language/es/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "No hay temas con esta etiqueta.", + "tags": "Etiquetas", + "enter_tags_here": "Introduce aquí las etiquetas, entre %1 y %2 caracteres cada una.", + "enter_tags_here_short": "Introduzca las etiquetas...", + "no_tags": "Aún no hay etiquetas.", + "select_tags": "Seleccionar Etiquetas" +} \ No newline at end of file diff --git a/public/language/es/top.json b/public/language/es/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/es/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/es/topic.json b/public/language/es/topic.json new file mode 100644 index 0000000000..caccbdd761 --- /dev/null +++ b/public/language/es/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tema", + "title": "Título", + "no_topics_found": "¡No se encontraron temas!", + "no_posts_found": "¡No se encontraron publicaciones!", + "post_is_deleted": "¡Esta publicación está eliminada!", + "topic_is_deleted": "¡Este tema ha sido eliminado!", + "profile": "Perfil", + "posted_by": "Publicado por %1", + "posted_by_guest": "Publicado por Invitado", + "chat": "Chat", + "notify_me": "Serás notificado cuando haya nuevas respuestas en este tema", + "quote": "Citar", + "reply": "Responder", + "replies_to_this_post": "%1 Respuestas", + "one_reply_to_this_post": "1 Respuesta", + "last_reply_time": "Última respuesta", + "reply-as-topic": "Responder como tema", + "guest-login-reply": "Accede para responder", + "login-to-view": "🔒 Inicie sesión para ver", + "edit": "Editar", + "delete": "Borrar", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Purgar", + "restore": "Restaurar", + "move": "Mover", + "change-owner": "Cambiar propietario", + "fork": "Dividir", + "link": "Link", + "share": "Compartir", + "tools": "Herramientas", + "locked": "Cerrado", + "pinned": "Fijo", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Programado", + "moved": "Movido", + "moved-from": "Moved from %1", + "copy-ip": "Copiar IP", + "ban-ip": "Banear IP", + "view-history": "Editar Historial", + "locked-by": "Bloqueado por", + "unlocked-by": "Desbloqueado por", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Borrado por", + "restored-by": "Restaurado por", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Haz click aquí para volver a tu último mensaje leído en este tema", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Este tema ha sido borrado. Solo los usuarios que tengan privilegios de administración de temas pueden verlo.", + "following_topic.message": "Ahora recibiras notificaciones cuando alguien publique en este tema.", + "not_following_topic.message": "Podras ver este tema en la lista de no leidos, pero no recibirás notificaciones cuando alguien escriba en él.", + "ignoring_topic.message": "Ya no verás este tema en no leídos. Serás notificado si te mencionan o te votan.", + "login_to_subscribe": "Por favor, conéctate para subscribirte a este tema.", + "markAsUnreadForAll.success": "Tema marcado como no leído para todos.", + "mark_unread": "Marcar no leído", + "mark_unread.success": "Tema marcado como no leído.", + "watch": "Seguir", + "unwatch": "Dejar de seguir", + "watch.title": "Serás notificado cuando haya nuevas respuestas en este tema", + "unwatch.title": "Dejar de seguir este tema", + "share_this_post": "Compartir este mensaje", + "watching": "Siguiendo", + "not-watching": "No siguiendo", + "ignoring": "Ignorando", + "watching.description": "Notificarme de nuevas respuestas.
Mostrar tema en no leídos. ", + "not-watching.description": "No notificarme de nuevas respuestas.
No mostrar tema en no leídos. ", + "ignoring.description": "No notificarme de nuevas respuestas.
No mostrar tema en no leídos. ", + "thread_tools.title": "Herramientas", + "thread_tools.markAsUnreadForAll": "Marcar todo como no leído", + "thread_tools.pin": "Adherir tema", + "thread_tools.unpin": "Despegar tema", + "thread_tools.lock": "Cerrar tema", + "thread_tools.unlock": "Reabrir tema", + "thread_tools.move": "Mover tema", + "thread_tools.move-posts": "Mover mensajes", + "thread_tools.move_all": "Mover todo", + "thread_tools.change_owner": "Cambiar propietario", + "thread_tools.select_category": "Seleccionar categoría", + "thread_tools.fork": "Dividir tema", + "thread_tools.delete": "Borrar tema", + "thread_tools.delete-posts": "Eliminar mensajes", + "thread_tools.delete_confirm": "¿Estás seguro que deseas eliminar este tema?", + "thread_tools.restore": "Restaurar tema", + "thread_tools.restore_confirm": "¿Estás seguro que deseas restaurar este tema?", + "thread_tools.purge": "Purgar tema", + "thread_tools.purge_confirm": "¿Está seguro que desea eliminar definitivamente (purgar) este tema?", + "thread_tools.merge_topics": "Fusionar temas", + "thread_tools.merge": "Fusionar", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "¿Estás seguro de que quieres eliminar este mensaje?", + "post_restore_confirm": "¿Estás seguro de que quieres restaurar este mensaje?", + "post_purge_confirm": "¡Estás seguro de que quieres purgar esta publicación?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Cargando categorías", + "confirm_move": "Mover", + "confirm_fork": "Dividir", + "bookmark": "Marcador", + "bookmarks": "Marcadores", + "bookmarks.has_no_bookmarks": "No tienes ningún marcador aun.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Cargando más mensajes", + "move_topic": "Mover tema", + "move_topics": "Mover temas", + "move_post": "Mover mensaje", + "post_moved": "¡Mensaje movido!", + "fork_topic": "Dividir tema", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Pulsa en los mensajes que quieres dividir", + "fork_no_pids": "¡No has seleccionado ningún mensaje!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 mensaje(s) seleccionados", + "fork_success": "¡Se ha creado un nuevo tema a partir del original! Haz click aquí para ir al nuevo tema.", + "delete_posts_instruction": "Haz click en los mensajes que quieres eliminar/limpiar", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Haz click en los mensajes que quieres asignar a otro usuario", + "composer.title_placeholder": "Ingresa el título de tu tema...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Descartar", + "composer.submit": "Enviar", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "En respuesta a %1", + "composer.new_topic": "Nuevo tema", + "composer.editing": "Editing", + "composer.uploading": "subiendo...", + "composer.thumb_url_label": "Agrega una URL de miniatura para el hilo", + "composer.thumb_title": "Agregar miniatura a este tema", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "O subir un fichero", + "composer.thumb_remove": "Limpiar campos", + "composer.drag_and_drop_images": "Arrastra las imagenes aqui", + "more_users_and_guests": "%1 usuario(s) y %2 invitado(s) más", + "more_users": "%1 usuario(s) más", + "more_guests": "%1 invitado(s) más", + "users_and_others": "%1 y otros %2", + "sort_by": "Ordenar", + "oldest_to_newest": "Más antiguo a más nuevo", + "newest_to_oldest": "Más nuevo a más antiguo", + "most_votes": "Mayor número de Votos", + "most_posts": "Mayor número de Posts", + "most_views": "Most Views", + "stale.title": "¿Crear un nuevo hilo en su lugar?", + "stale.warning": "El hilo al que estás respondiendo es muy antiguo. ¿Quieres crear un nuevo hilo en su lugar y añadir una referencia a este en tu mensaje?", + "stale.create": "Crear un nuevo hilo", + "stale.reply_anyway": "Publicar este hilo de todos modos.", + "link_back": "Re: [%1](%2)", + "diffs.title": "Historial de Ediciones", + "diffs.description": "Este post ha tenido %1 revisión(es). Pulsa una de ellas para ver el contenido del post en ese momento.", + "diffs.no-revisions-description": "Este post ha tenido %1 revisión(es).", + "diffs.current-revision": "revisión actual", + "diffs.original-revision": "revisión original", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 después", + "timeago_earlier": "%1 antes", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/es/unread.json b/public/language/es/unread.json new file mode 100644 index 0000000000..eb34ac660b --- /dev/null +++ b/public/language/es/unread.json @@ -0,0 +1,15 @@ +{ + "title": "No leído", + "no_unread_topics": "No hay temas nuevos para leer.", + "load_more": "Cargar más", + "mark_as_read": "Marcar como leído", + "selected": "Seleccionados", + "all": "Todos", + "all_categories": "Todos los foros", + "topics_marked_as_read.success": "¡Temas marcados como leídos!", + "all-topics": "Todos los Temas", + "new-topics": "Temas Nuevos", + "watched-topics": "Temas Suscritos", + "unreplied-topics": "Temas sin respuesta", + "multiple-categories-selected": "Múltiples seleccionadas" +} \ No newline at end of file diff --git a/public/language/es/uploads.json b/public/language/es/uploads.json new file mode 100644 index 0000000000..75e55e90b9 --- /dev/null +++ b/public/language/es/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Subiendo el archivo...", + "select-file-to-upload": "¡Selecciona un archivo para subir!", + "upload-success": "¡Archivo subido correctamente!", + "maximum-file-size": "Máximo %1 kb", + "no-uploads-found": "No se econtraron subidas", + "public-uploads-info": "Las subidas son públicas, todos los usuarios pueden verlas.", + "private-uploads-info": "Las subidas son privadas, solo los usuarios conectados pueden verlas." +} \ No newline at end of file diff --git a/public/language/es/user.json b/public/language/es/user.json new file mode 100644 index 0000000000..f8363dca1a --- /dev/null +++ b/public/language/es/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Baneado", + "muted": "Muted", + "offline": "Desconectado", + "deleted": "Borrado", + "username": "Nombre de usuario", + "joindate": "Fecha de registro", + "postcount": "Número De Publicaciones", + "email": "Correo electrónico", + "confirm_email": "Confirmar correo electrónico", + "account_info": "Información de cuenta", + "admin_actions_label": "Administrative Actions", + "ban_account": "Banear cuenta", + "ban_account_confirm": "Quieres confirmar el baneo de este usuario?", + "unban_account": "Desbanear cuenta", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Eliminar cuenta", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Cuenta borrada", + "account-content-deleted": "Account content deleted", + "fullname": "Nombre completo", + "website": "Sitio web", + "location": "Ubicación", + "age": "Edad", + "joined": "Registrado", + "lastonline": "Última vez conectado", + "profile": "Perfil", + "profile_views": "Visitas", + "reputation": "Reputación", + "bookmarks": "Marcadores", + "watched_categories": "Categorías seguidas", + "change_all": "Change All", + "watched": "Suscritos", + "ignored": "Ignorado", + "default-category-watch-state": "Estado default de vista de categoría", + "followers": "Seguidores", + "following": "Siguiendo", + "blocks": "Bloqueos", + "block_toggle": "Cambiar Bloqueo", + "block_user": "Bloquear usuario", + "unblock_user": "Desbloquear usuario", + "aboutme": "Sobre mí", + "signature": "Firma", + "birthday": "Cumpleaños", + "chat": "Chat", + "chat_with": "Continuar chat con %1", + "new_chat_with": "Empezar chat con %1", + "flag-profile": "Perfil de reporte", + "follow": "Seguir", + "unfollow": "Dejar de seguir", + "more": "Más", + "profile_update_success": "¡El perfil ha sido actualizado correctamente!", + "change_picture": "Cambiar imagen", + "change_username": "Cambiar nombre de usuario", + "change_email": "Cambiar email", + "email_same_as_password": "Por favor ingrese su contraseña actual para continuar – tú has introducido tu nuevo correo electrónico de nuevo", + "edit": "Editar", + "edit-profile": "Editar Perfil", + "default_picture": "Icono por defecto", + "uploaded_picture": "Imagen subida", + "upload_new_picture": "Subir nueva imagen", + "upload_new_picture_from_url": "Cargar desde URL", + "current_password": "Contraseña actual", + "change_password": "Cambiar contraseña", + "change_password_error": "¡Contraseña no válida!", + "change_password_error_wrong_current": "¡Su contraseña actual no es correcta!", + "change_password_error_match": "¡Las contraseñas deben coincidir!", + "change_password_error_privileges": "No tienes los permisos suficientes para cambiar esta contraseña.", + "change_password_success": "¡Tu contraseña ha sido actualizada!", + "confirm_password": "Confirmar contraseña", + "password": "Contraseña", + "username_taken_workaround": "El nombre de usuario que has solicitada ya está siendo usado, por tanto lo hemos alterado ligeramente. Ahora eres conocido como %1.", + "password_same_as_username": "Tu Constraseña es igual al nombre de Usuario, por favor seleccione otra Constraseña.", + "password_same_as_email": "Tu contraseña es igual que tu dirección de correo, por favor elige otra contraseña.", + "weak_password": "Clase débil", + "upload_picture": "Subir foto", + "upload_a_picture": "Subir una foto", + "remove_uploaded_picture": "Borrar Imagen subida", + "upload_cover_picture": "Subir imagen de portada", + "remove_cover_picture_confirm": "¿ Estás seguro de borrar la imágen de portada ?", + "crop_picture": "Recortar imágen", + "upload_cropped_picture": "Recortar y subir", + "avatar-background-colour": "Avatar background colour", + "settings": "Opciones", + "show_email": "Mostrar mi correo electrónico", + "show_fullname": "Mostrar mi nombre completo", + "restrict_chats": "Solo permitir mensajes de chat de usuarios a los que sigo", + "digest_label": "Suscribirse al resumen", + "digest_description": "Suscribirse a actualizaciones por correo electrónico a este foro (nuevas notificaciones y temas) de acuerdo a una recurrencia definida", + "digest_off": "Apagado", + "digest_daily": "Diariamente", + "digest_weekly": "Semanalmente", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Mensualmente", + "has_no_follower": "Este usuario no tiene seguidores :(", + "follows_no_one": "Este miembro no sigue a nadie :(", + "has_no_posts": "Este usuario no ha publicado nada aún.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Este usuario no ha publicado ninguna tema todavía.", + "has_no_watched_topics": "Este usuario no esta suscrito a ningún tema aún.", + "has_no_ignored_topics": "Este usuario no ha ignorado ningún tema aun.", + "has_no_upvoted_posts": "Este usuario todavía no ha votado ninguna publicación positivamente.", + "has_no_downvoted_posts": "Este usuario todavía no ha votado ninguna publicación negativamente.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "No tienes usuarios bloqueados.", + "email_hidden": "Correo electrónico oculto", + "hidden": "oculto", + "paginate_description": "Paginar hilos y mensajes en lugar de usar desplazamiento infinito", + "topics_per_page": "Temas por página", + "posts_per_page": "Post por página", + "max_items_per_page": "Máximo %1", + "acp_language": "Página de Lenguage del Administrador", + "notifications": "Notifications", + "upvote-notif-freq": "Frecuencia de notificación de votos positivos", + "upvote-notif-freq.all": "Todos los Votos Positivos", + "upvote-notif-freq.first": "Primero por Post", + "upvote-notif-freq.everyTen": "Cada Diez Votos Positivos", + "upvote-notif-freq.threshold": "En 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "A los 10, 100, 1000...", + "upvote-notif-freq.disabled": "Desactivado", + "browsing": "Preferencias de navegación.", + "open_links_in_new_tab": "Abrir los enlaces externos en una nueva pestaña", + "enable_topic_searching": "Activar la búsqueda \"dentro del tema\"", + "topic_search_help": "Si está activada, la búsqueda 'dentro del tema' al usar el buscador de la barra de navegación automáticamente buscaras solo en el tema actual.", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Luego de enviar una respuesta, mostrar el nuevo mensaje", + "follow_topics_you_reply_to": "Seguir temas a los que respondes", + "follow_topics_you_create": "Seguir temas creados por ti", + "grouptitle": "Título del grupo", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Sin título de grupo", + "select-skin": "Seleccionar una plantilla", + "select-homepage": "Seleccione una página de inicio", + "homepage": "Página de inicio", + "homepage_description": "Seleccione una página para su uso habitual como la página principal del foro o 'Ninguno' para utilizar la página de inicio.", + "custom_route": "Pagina de inicio personalizada", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Servicios de Inicio de sesión Único", + "sso.associated": "Asociado con", + "sso.not-associated": "Da clic aquí para asociarse con", + "sso.dissociate": "Disociado", + "sso.dissociate-confirm-title": "Confirmar Disociación", + "sso.dissociate-confirm": "Está seguro de que desea disociar su cuenta de %1?", + "info.latest-flags": "Ultimos reportes", + "info.no-flags": "Ningun mensaje reportado encontrado", + "info.ban-history": "Histórico reciente de bans", + "info.no-ban-history": "Este usuario nunca ha sido baneado", + "info.banned-until": "Baneado hasta %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Baneado permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Motivo no especificado", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Histórico de nombre de usuario", + "info.email-history": "HIstórico de Email", + "info.moderation-note": "Nota de Moderación", + "info.moderation-note.success": "Nota de moderación guardada", + "info.moderation-note.add": "Añadir nota", + "sessions.description": "Esta página le permite ver las sesiones activas en este foro y revocarlas si es necesario. Puedes revocar tu propia sesión cerrando sesión en tu cuenta.", + "consent.title": "Tus derechos & Consentimiento", + "consent.lead": "Este foro recolecta y procesa su información personal", + "consent.intro": "Usamos esta información estrictamente para personalizar su experiencia en esta comunidad, así como para asociar los mensajes que haga con su cuenta de usuario. Durante el registro se le pidió que proporcionara un nombre de usuario y dirección de correo electrónico, puede también proveer información adicional para completar su perfil en esta web.

Conservamos esta información mientras su cuenta de usuario exista, y podrá retirar su consentimiento en cualquier momento borrando esta cuenta de usuario. A su vez, puede pedir una copia de su contribución a este sitio través de la página de Derechos & Consentimiento.

Si tiene cualquier pregunta o preocupación, le animamos a dirigirse al equipo de administración de este foro.", + "consent.email_intro": "Ocasionalmente, puede que enviemos correos electrónicos a su dirección de correo electrónico para informarle de actualizaciones y/o de actividad pertinente a usted. Puede personalizar la frecuencia del informe de la comunidad (incluso deshabilitarlo directamente), así como seleccionar que tipos de notificaciones recibir por correo electrónico, a través de propia página de configuración de usuario.", + "consent.digest_frequency": "A no ser que lo cambie expresamente en su configuración de usuario, esta comunidad envía informes por correo electrónico cada %1.", + "consent.digest_off": "A no ser que lo cambie expresamente en su configuración de usuario, esta comunidad no envía informes por correo electrónico.", + "consent.received": "Usted ha dado consentimiento a este sitio web para recolectar y procesar su información. No se requieren acciones adicionales.", + "consent.not_received": "Usted no ha dado consentimiento para la recolección y procesamiento. En cualquier momento la administración de este sitio web puede elegir eliminar su cuenta para cumplir con la Regulación General de Protección de Datos.", + "consent.give": "Dar consentimiento", + "consent.right_of_access": "Usted tiene Derecho de Acceso", + "consent.right_of_access_description": "Usted tiene derecho a acceder a cualquier dato recolectado por este sitio si lo pide. Puede recuperar una copia de estos datos haciendo click en el botón apropiado abajo.", + "consent.right_to_rectification": "Usted tiene el Derecho a Rectificación", + "consent.right_to_rectification_description": "Usted tiene el derecho a cambiar o actualizar cualquier dato impreciso que se nos haya proporcionado. Su perfil puede ser actualizado editando su perfil, y el contenido de sus respuestas y entradas puede ser siempre editado. Si este no es el caso, por favor contacte con el equipo administrativo de este sitio.", + "consent.right_to_erasure": "Usted tiene derecho de supresión y derecho al olvido.", + "consent.right_to_erasure_description": "En cualquier momento, usted puede revocar su consentimiento a la recolección y/o procesado de datos mediante el borrado de su cuenta. Su perfil individual puede ser borrado, aunque sus respuestas y entradas permanecerán. Si desea borrar su cuenta y el contenido (entradas, temas, respuestas...), por favor contacte el equipo administrativo de este sitio web.", + "consent.right_to_data_portability": "Usted tiene el Derecho a la Portabilidad de Datos", + "consent.right_to_data_portability_description": "Puede pedir de nosotros una exportación legible por máquinas de cualquier dato recolectado sobre usted y su cuenta. Puede hacerlo haciendo click en el botón apropiado abajo.", + "consent.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Exportar Contenido Subido (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Exportar Entradas y Respuestas (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/es/users.json b/public/language/es/users.json new file mode 100644 index 0000000000..f16dcf326e --- /dev/null +++ b/public/language/es/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Últimos usuarios", + "top_posters": "Top por mensajes", + "most_reputation": "Más reputados", + "most_flags": "Más Reportados", + "search": "Buscar", + "enter_username": "Ingresa el nombre de usuario que quieres buscar", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Cargar más", + "users-found-search-took": "¡%1 usuario(s) encontrado! La búsqueda ha llevado %2 segundos.", + "filter-by": "Filtrar Por", + "online-only": "Sólo en línea", + "invite": "Invitar", + "prompt-email": "Correos electrónico:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Un correo de invitación ha sido enviado a %1", + "user_list": "Lista de Usuarios", + "recent_topics": "Temas Recientes", + "popular_topics": "Temas Populares", + "unread_topics": "Temas no leídos", + "categories": "Categorías ", + "tags": "Etiquetas", + "no-users-found": "¡No se encontraron usuarios!" +} \ No newline at end of file diff --git a/public/language/et/_DO_NOT_EDIT_FILES_HERE.md b/public/language/et/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/et/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/et/admin/admin.json b/public/language/et/admin/admin.json new file mode 100644 index 0000000000..9b57b550c7 --- /dev/null +++ b/public/language/et/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Oled kindel, et soovid taaslaadida NodeBB?", + + "acp-title": "%1 | NodeBB Administraatori kontrollpaneel", + "settings-header-contents": "Sisu", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/et/admin/advanced/cache.json b/public/language/et/admin/advanced/cache.json new file mode 100644 index 0000000000..f132ef4845 --- /dev/null +++ b/public/language/et/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Postituste vahemälu", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Täis", + "post-cache-size": "Postituse vahemälu suurus", + "items-in-cache": "Esemed vahemälus" +} \ No newline at end of file diff --git a/public/language/et/admin/advanced/database.json b/public/language/et/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/et/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/et/admin/advanced/errors.json b/public/language/et/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/et/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/et/admin/advanced/events.json b/public/language/et/admin/advanced/events.json new file mode 100644 index 0000000000..2b0df63877 --- /dev/null +++ b/public/language/et/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Sündmused", + "no-events": "Sündmused puuduvad", + "control-panel": "Sündmuste kontrollpaneel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/et/admin/advanced/logs.json b/public/language/et/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/et/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/et/admin/appearance/customise.json b/public/language/et/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/et/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/et/admin/appearance/skins.json b/public/language/et/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/et/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/et/admin/appearance/themes.json b/public/language/et/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/et/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/et/admin/dashboard.json b/public/language/et/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/et/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/et/admin/development/info.json b/public/language/et/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/et/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/et/admin/development/logger.json b/public/language/et/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/et/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/et/admin/extend/plugins.json b/public/language/et/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/et/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/et/admin/extend/rewards.json b/public/language/et/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/et/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/et/admin/extend/widgets.json b/public/language/et/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/et/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/et/admin/manage/admins-mods.json b/public/language/et/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/et/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/et/admin/manage/categories.json b/public/language/et/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/et/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/et/admin/manage/digest.json b/public/language/et/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/et/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/et/admin/manage/groups.json b/public/language/et/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/et/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/et/admin/manage/privileges.json b/public/language/et/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/et/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/et/admin/manage/registration.json b/public/language/et/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/et/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/et/admin/manage/tags.json b/public/language/et/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/et/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/et/admin/manage/uploads.json b/public/language/et/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/et/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/et/admin/manage/users.json b/public/language/et/admin/manage/users.json new file mode 100644 index 0000000000..c62c041122 --- /dev/null +++ b/public/language/et/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Kasutajad", + "edit": "Actions", + "make-admin": "Ülenda administraatoriks", + "remove-admin": "Eemalda administraator", + "validate-email": "Kinnita email", + "send-validation-email": "Saada kinnituskiri", + "password-reset-email": "Saada parooli taastamise email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Keelusta Kasutaja(d)", + "temp-ban": "Keelusta Kasutaja(d) ajutiselt", + "unban": "Tühista keeld Kasutaja(tel)", + "reset-lockout": "Taaslae blokeering", + "reset-flags": "Taasta raporteerimised", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Lae alla CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "Uus kasutaja", + "filter-by": "Filter by", + "pills.unvalidated": "Valideerimata", + "pills.validated": "Validated", + "pills.banned": "Keelustatud", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "Kasutajanime järgi", + "search.username-placeholder": "Sisesta kasutajanimi, keda soovid otsida", + "search.email": "Emaili kaudu", + "search.email-placeholder": "Sisesta email, mida soovid otsida", + "search.ip": "IP Aadressi järgi", + "search.ip-placeholder": "Sisesta IP Aadress, mida soovid otsida", + "search.not-found": "Kasutajat ei leitud!", + + "inactive.3-months": "3 kuud", + "inactive.6-months": "6 kuud", + "inactive.12-months": "12 kuud", + + "users.uid": "uid", + "users.username": "Kasutajanimi", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "Postituste arv", + "users.reputation": "Reputatsioon", + "users.flags": "Raporteerimised", + "users.joined": "Liitunud", + "users.last-online": "Viimati sees", + "users.banned": "keelustatud", + + "create.username": "Kasutajanimi", + "create.email": "Email", + "create.email-placeholder": "Antud kasutaja email", + "create.password": "Parool", + "create.password-confirm": "Kinnita parool", + + "temp-ban.length": "Length", + "temp-ban.reason": "Põhjus (valikuline)", + "temp-ban.hours": "Tunnid", + "temp-ban.days": "Päevad", + "temp-ban.explanation": "Sisesta keelustuse pikkus. Kui sisestad 0, siis seda loetakse igaveseks keelustuseks.", + + "alerts.confirm-ban": "Kas te tõesti soovite antud kasutajat igaveseks keelustada ?", + "alerts.confirm-ban-multi": "Kas te tõesti soovite antud kasutajaid igaveseks keelustada?", + "alerts.ban-success": "Kasutaja(d) keelustatud!", + "alerts.button-ban-x": "Keelusta %1 kasutaja(d)", + "alerts.unban-success": "Kasutaja(te) keelustus eemaldatud", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Märgistuse(te) taaslaadimine", + "alerts.no-remove-yourself-admin": "Te ei saa ennast Administraatorina eemaldada", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Kas te tahate antud kasutaja(te) emaili(d) kinnitada?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emailid kinnitatud", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Kas te tahate saata parooli taastamise emaili(d) antud kasutaja(te)le?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "Kasutaja(d) kustutatud!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Loo Kasutaja", + "alerts.button-create": "Loo", + "alerts.button-cancel": "Tühista", + "alerts.error-passwords-different": "Paroolid peavad kattuma!", + "alerts.error-x": "Viga

%1

", + "alerts.create-success": "Kasutaja tehtud!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "Kutse on saadetud %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/et/admin/menu.json b/public/language/et/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/et/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/advanced.json b/public/language/et/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/et/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/et/admin/settings/api.json b/public/language/et/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/et/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/chat.json b/public/language/et/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/et/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/cookies.json b/public/language/et/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/et/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/email.json b/public/language/et/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/et/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/et/admin/settings/general.json b/public/language/et/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/et/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/et/admin/settings/group.json b/public/language/et/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/et/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/guest.json b/public/language/et/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/et/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/homepage.json b/public/language/et/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/et/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/languages.json b/public/language/et/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/et/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/navigation.json b/public/language/et/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/et/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/et/admin/settings/notifications.json b/public/language/et/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/et/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/pagination.json b/public/language/et/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/et/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/post.json b/public/language/et/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/et/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/reputation.json b/public/language/et/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/et/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/social.json b/public/language/et/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/et/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/sockets.json b/public/language/et/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/et/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/sounds.json b/public/language/et/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/et/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/tags.json b/public/language/et/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/et/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/et/admin/settings/uploads.json b/public/language/et/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/et/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/et/admin/settings/user.json b/public/language/et/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/et/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/et/admin/settings/web-crawler.json b/public/language/et/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/et/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/et/category.json b/public/language/et/category.json new file mode 100644 index 0000000000..dfbc02431f --- /dev/null +++ b/public/language/et/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategooria", + "subcategories": "Alamkategooriad", + "new_topic_button": "Uus teema", + "guest-login-post": "Postitamiseks logi sisse", + "no_topics": "Kahjuks ei leidu siin kategoorias ühtegi teemat.
Soovid postitada?", + "browsing": "vaatab", + "no_replies": "Keegi pole vastanud", + "no_new_posts": "Uusi postitusi pole", + "watch": "Vaata", + "ignore": "Ignoreeri", + "watching": "Vaatab", + "not-watching": "Not Watching", + "ignoring": "Ignoreerib", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Jälgitavad kategooriad", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/et/email.json b/public/language/et/email.json new file mode 100644 index 0000000000..ae0b32b56a --- /dev/null +++ b/public/language/et/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Tere tulemast %1 foorumisse", + "invite": "Kutse %1-lt", + "greeting_no_name": "Tere", + "greeting_with_name": "Tere %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Täname et oled registreerinud %1 foorumisse!", + "welcome.text2": "Konto täielikuks aktiveerimiseks peame me kinnitama, et registreerimisel kasutatud e-mail kuulub teile.", + "welcome.text3": "Administraator aktsepteeris teie registreerimise. Te saate nüüd sisse logida oma kasutajanime/parooliga.", + "welcome.cta": "Vajuta siia, et kinnitada oma e-maili aadress", + "invitation.text1": "%1 kutsus teid gruppi %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Meile laekus päring parooli muutmiseks. Kui päring ei ole teie poolt esitatud või te ei soovi parooli muuta, siis võite antud kirja ignoreerida.", + "reset.text2": "Selleks, et jätkata parooli muutmisega vajuta järgnevale lingile:", + "reset.cta": "Vajuta siia, et taotleda uut parooli", + "reset.notify.subject": "Parool edukalt muudetud", + "reset.notify.text1": "Teatame, et sinu parooli muutmine kuupäeval %1 oli edukas.", + "reset.notify.text2": "Kui te ei ole lubanud seda, siis teavitage koheselt administraatorit.", + "digest.latest_topics": "Viimased teemad %1 poolt", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Vajuta siia et külastada %1", + "digest.unsub.info": "See uudiskiri on saadetud teile tellimuse seadistuse tõttu.", + "digest.day": "päev", + "digest.week": "nädal", + "digest.month": "kuu", + "digest.subject": "Ima %1 jaoks", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Sulle on saabunud uus sõnum kasutajalt %1", + "notif.chat.cta": "Vajuta siia, et jätkata vestlusega", + "notif.chat.unsub.info": "See chat teavitus on saadetud teile tellimuse seadistuse tõttu.", + "notif.post.unsub.info": "See postituse teavitus on saadetud teile tellimuse seadistuse tõttu.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "See on test e-mail kinnitamaks, et emailer on korrektselt seadistatud sinu NodeBB jaoks.", + "unsub.cta": "Vajuta siia, et muuta neid seadeid", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Aitäh!" +} \ No newline at end of file diff --git a/public/language/et/error.json b/public/language/et/error.json new file mode 100644 index 0000000000..46b6967ef2 --- /dev/null +++ b/public/language/et/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Vigased andmed", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Sa ei ole sisse logitud", + "account-locked": "Su kasutaja on ajutiselt lukustatud", + "search-requires-login": "Otsing nõuab kasutajat - palun registreeruge või logige sisse.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Vigane kategooria ID", + "invalid-tid": "Vigane teema ID", + "invalid-pid": "Vigane postituse ID", + "invalid-uid": "Vigane kasutaja ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Vigane kasutajanimi", + "invalid-email": "Vigane emaili aadress", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Vigased kasutaja andmed", + "invalid-password": "Vigane parool", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Palun täpsusta kasutajanime ja parooli", + "invalid-search-term": "Vigane otsingusõna", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "Me ei saanud Sind sisse logida, võimalik, et tänu aegunud sessioonile, palun proovi uuesti", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Väär lehekülje numeratsioon, peab olema vähemalt %1 ja kõige rohkem %2", + "username-taken": "Kasutajanimi on juba võetud", + "email-taken": "Email on võetud", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Sõnumeid ei ole võimalik enne saata kui sinu email on kinnitatud. Kinnitamiseks vajuta siia.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Meil ei õnnestunud sinu emaili kinnitada, proovi hiljem uuesti.", + "confirm-email-already-sent": "Kinnituskiri on juba saadetud, palun oota %1 minut(it) uue kirja saatmiseks.", + "sendmail-not-found": "Sendmail'i käivitatavat ei leitud, palun tee kindlaks, et see on installeeritud ja on käivitatav kasutaja poolt, kes käitab NodeBB't.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Kasutajanimi on liiga lühike", + "username-too-long": "Kasutajanimi on liiga pikk", + "password-too-long": "Parool liiga pikk", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Kasutaja bannitud", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Vabandust, te peate ootama %1 sekund(it) enne esimese postituse loomist.", + "blacklisted-ip": "Vabandust! Sinu IP-aadress on siin kogukonnas keelatud. Kui arvad, et see on eksitus, palun leia kontakti administraatoriga.", + "ban-expiry-missing": "Palun sisesta keelu lõpukuupäev", + "no-category": "Kategooriat ei eksisteeri", + "no-topic": "Teemat ei eksisteeri", + "no-post": "Postitust ei eksisteeri", + "no-group": "Gruppi ei eksisteeri", + "no-user": "Kasutajat ei eksisteeri", + "no-teaser": "Eelvaadet ei eksisteeri", + "no-flag": "Flag does not exist", + "no-privileges": "Sul pole piisavalt õigusi.", + "category-disabled": "Kategooria keelatud", + "topic-locked": "Teema lukustatud", + "post-edit-duration-expired": "Te peate ootama %1 sekund(it), enne kui oma postitust muudate.", + "post-edit-duration-expired-minutes": "Teil on lubatud muuta oma postitusi vaid %1 minuti jooksul peale postitamist", + "post-edit-duration-expired-minutes-seconds": "Teil on lubatud muuta oma postitusi vaid %1 minuti %2 sekundi jooksul peale postitamist", + "post-edit-duration-expired-hours": "Teil on lubatud muuta oma postitusi vaid %1 tunni jooksul peale postitamist", + "post-edit-duration-expired-hours-minutes": "Teil on lubatud muuta oma postitusi vaid %1 tunni %2 minuti jooksul peale postitamist", + "post-edit-duration-expired-days": "Teil on lubatud muuta oma postitusi vaid %1 päeva jooksul peale postitamist", + "post-edit-duration-expired-days-hours": "Teil on lubatud muuta oma postitusi vaid %1 päeva %2 tunni jooksul peale postitamist", + "post-delete-duration-expired": "Teil on lubatud kustutada oma postitusi vaid %1 sekundi jooksul peale postitamist", + "post-delete-duration-expired-minutes": "Teil on lubatud kustutada oma postitusi vaid %1 minuti jooksul peale postitamist", + "post-delete-duration-expired-minutes-seconds": "Teil on lubatud kustutada oma postitusi vaid %1 minuti %2 sekundi jooksul peale postitamist", + "post-delete-duration-expired-hours": "Teil on lubatud kustutada oma postitusi vaid %1 tunni jooksul peale postitamist", + "post-delete-duration-expired-hours-minutes": "Teil on lubatud kustutada oma postitusi vaid %1 tunni %2 minuti jooksul peale postitamist", + "post-delete-duration-expired-days": "Teil on lubatud kustutada oma postitusi vaid %1 päeva jooksul peale postitamist", + "post-delete-duration-expired-days-hours": "Teil on lubatud kustutada oma postitusi vaid %1 päeva %2 tunni jooksul peale postitamist", + "cant-delete-topic-has-reply": "Sa ei saa oma postitust kustutada, kui sellele on vastatud", + "cant-delete-topic-has-replies": "Sa ei saa oma postitust kustutada pärast seda, kui sellel on %1 vastust", + "content-too-short": "Palun tehke pikem postitus. Postituse pikkus peab olema vähemalt %1 tähemärk(i).", + "content-too-long": "Palun tehke lühem postitus. Postituse pikkus peab olema vähem kui %1 tähemärk(i).", + "title-too-short": "Palun sisesta pikem pealkiri. Pealkirjad ei saa olla lühemad kui %1 tähemärk(i).", + "title-too-long": "Palun sisesta lühem pealkiri. Pealkirjad ei saa olla pikemad kui %1 tähemärk(i).", + "category-not-selected": "Category not selected.", + "too-many-posts": "Te saate postitada %1 sekundi tagant - palun oodake enne uue postituse tegemist.", + "too-many-posts-newbie": "Uue kasutajana saadte postitada vaid iga %1 sekundi tagant, seniks kuni olete teeninud vähemalt %2 reputatsiooni - palun oodake enne uue postituse tegemist.", + "already-posting": "You are already posting", + "tag-too-short": "Palun sisestage pikem märksõna. Märksõna pikkus peab olema vähemalt %1 tähemärk(i).", + "tag-too-long": "Palun sisestage lühem märksõna. Märksõna pikkus peab olema vähem kui %1 tähemärk(i).", + "not-enough-tags": "Liiga vähe märksõnu. Teemadel peab olemalt vähemalt %1 märksõna", + "too-many-tags": "Liiga palju märksõnu. Teemadel ei tohi olla rohkem kui %1 märksõna", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Palun oota, kuni üleslaadimised on laetud.", + "file-too-big": "Maksimaalne üleslaetava faili suurus on %1 kB - valige väiksema mahuga fail.", + "guest-upload-disabled": "Külaliste üleslaadimine on keelatud.", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Sa ei saa bannida teisi administraatoreid!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Te olete ainus administraator. Lisage keegi teine administraatoriks, enne kui eemaldate endalt administraatori.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Eemalda sellelt kasutajalt administraatori õigused enne selle kustutamist", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Vigane pildi formaat. Lubatud formaadid on: %1", + "invalid-image-extension": "Vigane pildi formaat", + "invalid-file-type": "Vigane faili formaat. Lubatud formaadid on: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Grupi nimi liiga lühike", + "group-name-too-long": "Grupi nimi liiga pikk", + "group-already-exists": "Grupp juba eksisteerib", + "group-name-change-not-allowed": "Grupi nimevahetus ei ole lubatud", + "group-already-member": "Oled juba selles grupis", + "group-not-member": "Ei ole selle grupi liige", + "group-needs-owner": "See grupp nõuab vähemalt ühte omanikku", + "group-already-invited": "Antud kasutaja on juba kutsutud.", + "group-already-requested": "Teie liikmetaotlus on juba saadetud.", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Postitus on juba kustutatud", + "post-already-restored": "Postitus on juba taastatud", + "topic-already-deleted": "Teema on juba kustutatud", + "topic-already-restored": "Teema on juba taastatud", + "cant-purge-main-post": "Te ei saa eemaldada peamist postitust, pigem kustutage teema ära.", + "topic-thumbnails-are-disabled": "Teema thumbnailid on keelatud.", + "invalid-file": "Vigane fail", + "uploads-are-disabled": "Üleslaadimised on keelatud", + "signature-too-long": "Vabandage, teie signatuur ei saa olla pikem kui %1 tähemärk(i).", + "about-me-too-long": "Vabandage, teie tutvustus ei saa olaa pikem kui %1 tähemärk(i).", + "cant-chat-with-yourself": "Sa ei saa endaga vestelda!", + "chat-restricted": "Kasutaja on piiranud sõnumite saatmist. Privaatsõnumi saatmiseks peab kasutaja sind jälgima", + "chat-disabled": "Vestlus süsteem keelatud", + "too-many-messages": "Oled saatnud liiga palju sõnumeid, oota natukene.", + "invalid-chat-message": "Vigane vestluse sõnum", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "Sul ei ole lubatud antud sõnumit muuta", + "cant-delete-chat-message": "Sul ei ole lubatud antud sõnumit kustutada", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Sa oled juba hääletanud sellel postitusel.", + "reputation-system-disabled": "Reputatsiooni süsteem ei ole aktiveeritud", + "downvoting-disabled": "Negatiivsete häälte andmine ei ole võimaldatud", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "\"%1\" värskendamisel tekkis süsteemne viga. Foorum ei lakka töötamast, kuid peaksid kindlasti eemaldama enne värskendamist tehtud muudatused.", + "registration-error": "Viga registreerimisel", + "parse-error": "Midagi läks valesti...", + "wrong-login-type-email": "Sisse logimiseks kasuta oma emaili", + "wrong-login-type-username": "Sisse logimiseks kasuta oma kasutajanime", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "Sa oled kutsunud maksimaalse lubatud inimeste arvu (%1 %2 'st).", + "no-session-found": "Sisse logimis sessiooni ei leitud!", + "not-in-room": "Kasutaja pole ruumis", + "cant-kick-self": "Sa ei saa ennast ära visata gruppist", + "no-users-selected": "Ühtki kasutajat pole valitud", + "invalid-home-page-route": "Vigane avalehe suunamine", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/et/flags.json b/public/language/et/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/et/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/et/global.json b/public/language/et/global.json new file mode 100644 index 0000000000..3673db366b --- /dev/null +++ b/public/language/et/global.json @@ -0,0 +1,126 @@ +{ + "home": "Avaleht", + "search": "Otsi", + "buttons.close": "Sulge", + "403.title": "Ligipääs puudub", + "403.message": "Tundub, et sul pole piisvalt õigusi selle lehe vaatamiseks. ", + "403.login": "Äkki peaksid sisse logima?", + "404.title": "Ei leitud", + "404.message": "Tundub, et lehte mida otsid, ei eksisteeri. Mine tagasi avalehele.", + "500.title": "Süsteemne error.", + "500.message": "Oih! Midagi läks valesti!", + "400.title": "Vigane päring.", + "400.message": "Tundub, et see link on vigane, palun kontrolli see üle ja proovi uuesti. Võid ka minna tagasi avalehele.", + "register": "Registreeri", + "login": "Logi sisse", + "please_log_in": "Palun logi sisse", + "logout": "Logi välja", + "posting_restriction_info": "Siin foorumis on postitamine lubatud ainult registreeritud kasutajatel, palun logi sisse.", + "welcome_back": "Tere tulemast tagasi!", + "you_have_successfully_logged_in": "Edukalt sisse logitud", + "save_changes": "Salvesta muudatused", + "save": "Save", + "close": "Sulge", + "pagination": "Lehekülgede numeratsioon", + "pagination.out_of": "%1 kõigist %2-st", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Kategooriad", + "header.recent": "Hiljutised", + "header.unread": "Lugemata", + "header.tags": "Märksõnad", + "header.popular": "Populaarne", + "header.top": "Top", + "header.users": "Kasutajad", + "header.groups": "Grupid", + "header.chats": "Vestlused", + "header.notifications": "Teated", + "header.search": "Otsi", + "header.profile": "Profiil", + "header.navigation": "Navigatsioon", + "notifications.loading": "Laen teateid", + "chats.loading": "Laen vestlusi", + "motd.welcome": "Tere tulemast NodeBB foorumisse.", + "previouspage": "Eelmine leht", + "nextpage": "Järgmine leht", + "alert.success": "Õnnestus", + "alert.error": "Viga", + "alert.banned": "Bannitud", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Sa ei jälgi enam %1!", + "alert.follow": "Sa jälgid nüüd %1!", + "users": "Kasutajad", + "topics": "Teemat", + "posts": "Postitust", + "x-posts": "%1 posts", + "best": "Parim", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Poolt hääletajad", + "upvoted": "Kiideti heaks", + "downvoters": "Vastu hääletajad", + "downvoted": "Hääletas vastu", + "views": "Vaatamist", + "posters": "Posters", + "reputation": "Reputatsioon", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "loe veel", + "more": "Veel", + "none": "None", + "posted_ago_by_guest": "postitas %1 külaline", + "posted_ago_by": "postitas %1 kasutaja %2", + "posted_ago": "postitatud %1", + "posted_in": "postitas %1 'sse", + "posted_in_by": "postitati: %1 %2 poolt", + "posted_in_ago": "postitas kategooriasse %1 %2", + "posted_in_ago_by": "%3 postitas %2 kategooriasse %1", + "user_posted_ago": "%1 postitas %2", + "guest_posted_ago": "Külaline postitas %1", + "last_edited_by": "viimati muudetud %1 poolt", + "norecentposts": "Hiljutisi postitusi ei ole", + "norecenttopics": "Hiljutisi teemasid ei ole", + "recentposts": "Hiljutised postitused", + "recentips": "Hiljutised IP'd, millelt sisse logitud", + "moderator_tools": "Moderator Tools", + "online": "Sees", + "away": "Eemal", + "dnd": "Mitte segada", + "invisible": "Nähtamatu", + "offline": "Väljas", + "email": "Emaili aadress", + "language": "Keel", + "guest": "Külaline", + "guests": "Külalised", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Foorum on uuendatud", + "updated.message": "See foorum uuendati just kõige uuemale versioonile. Vajuta siia et värskendada veebilehte.", + "privacy": "Privaatsus", + "follow": "Jälgi", + "unfollow": "Ära jälgi", + "delete_all": "Kustuta kõik", + "map": "Kaart", + "sessions": "Logitud Sessioonid", + "ip_address": "IP Aadress", + "enter_page_number": "Sisesta lehekülje number", + "upload_file": "Lae fail üles", + "upload": "Lae üles", + "uploads": "Uploads", + "allowed-file-types": "Lubatud faili formaadid on %1", + "unsaved-changes": "Sul on salvestamata muudatusi. Oled kindel, et soovid lahkuda?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/et/groups.json b/public/language/et/groups.json new file mode 100644 index 0000000000..d1b5d7d701 --- /dev/null +++ b/public/language/et/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupid", + "view_group": "Vaata gruppi", + "owner": "Grupi omanik", + "new_group": "Loo uus grupp", + "no_groups_found": "Ei ole ühtegi gruppi", + "pending.accept": "Aktsepteeri", + "pending.reject": "Lükka tagasi", + "pending.accept_all": "Nõustu kõigega", + "pending.reject_all": "Lükka kõik tagasi", + "pending.none": "Hetkel ei ole ootel kasutajaid", + "invited.none": "Hetkel ei ole kutsutud kasutajaid", + "invited.uninvite": "Tühistage kutse", + "invited.search": "Otsige kasutajat, keda kutsuda antud gruppi.", + "invited.notification_title": "Sind on kutsutud liituma grupiga %1", + "request.notification_title": "Grupiga liitumise taotlus kasutajalt %1", + "request.notification_text": "%1 on avaldanud soovi liituda grupiga %2", + "cover-save": "Salvesta", + "cover-saving": "Salvestamine", + "details.title": "Grupi detailid", + "details.members": "Liikmete nimekiri", + "details.pending": "Otsust ootavad liikmed", + "details.invited": "Kutsutud liikmed", + "details.has_no_posts": "Selle grupi liikmed ei ole teinud ühtegi postitust.", + "details.latest_posts": "Viimased postitused", + "details.private": "Privaatne", + "details.disableJoinRequests": "Keela ühinemis taotlused", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Anna/võta omanikuõigused", + "details.kick": "Viska välja", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Grupi haldamine", + "details.group_name": "Grupi nimi", + "details.member_count": "Liikmete arv", + "details.creation_date": "Algatamise kuupäev", + "details.description": "Kirjeldus", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Embleemi eelvaade", + "details.change_icon": "Vaheta ikooni", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Embleemi kiri", + "details.userTitleEnabled": "Näita embleemi", + "details.private_help": "Kui sisse lülitatud, siis grupiga liitumine nõuab grupi omaniku nõusolekut", + "details.hidden": "Peidetud", + "details.hidden_help": "Kui sisse lülitatud, siis seda gruppi ei kuvata gruppide nimekirjas ning liikmed tuleb lisada manuaalselt", + "details.delete_group": "Kustuta grupp", + "details.private_system_help": "Privaatset gruppid on keelatud sellel süsteemi tasemel, see sätte ei tee midagi", + "event.updated": "Grupi lisainformatsiooni on uuendatud", + "event.deleted": "Grupp \"%1\" on kustutatud", + "membership.accept-invitation": "Võta kutse vastu", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Kutse ootel", + "membership.join-group": "Liitu grupiga", + "membership.leave-group": "Lahku grupist", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Lükka tagasi", + "new-group.group_name": "Grupi nimi:", + "upload-group-cover": "Lae gruppi pilt üles", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/et/ip-blacklist.json b/public/language/et/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/et/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/et/language.json b/public/language/et/language.json new file mode 100644 index 0000000000..5ebe9977ca --- /dev/null +++ b/public/language/et/language.json @@ -0,0 +1,5 @@ +{ + "name": "Estonian", + "code": "et", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/et/login.json b/public/language/et/login.json new file mode 100644 index 0000000000..331701929b --- /dev/null +++ b/public/language/et/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Kasutajanimi / E-mail", + "username": "Kasutajanimi", + "remember_me": "Mäleta mind?", + "forgot_password": "Unustasid parooli?", + "alternative_logins": "Alternatiivsed sisse logimise võimalused", + "failed_login_attempt": "Sisselogimine ebaõnnestus", + "login_successful": "Edukalt sisse logitud!", + "dont_have_account": "Pole veel kasutajat?", + "logged-out-due-to-inactivity": "Sind on Administraatori Juhtpaneelist ebaaktiivsuse tõttu välja logitud", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/et/modules.json b/public/language/et/modules.json new file mode 100644 index 0000000000..fca9882aef --- /dev/null +++ b/public/language/et/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Saada", + "chat.no_active": "Sul ei ole hetkel aktiivseid vestlusi.", + "chat.user_typing": "%1 kirjutab sõnumit...", + "chat.user_has_messaged_you": "%1 saatis sulle sõnumi.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Vali sõnumisaaja, et vaadata sõnumite ajalugu.", + "chat.no-users-in-room": "Ühtki kasutajat selles ruumis", + "chat.recent-chats": "Hiljutised vestlused", + "chat.contacts": "Kontaktid", + "chat.message-history": "Sõnumite ajalugu", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop-out vestlus", + "chat.minimize": "Minimize", + "chat.maximize": "Suurenda", + "chat.seven_days": "7 Päeva", + "chat.thirty_days": "30 Päeva", + "chat.three_months": "3 Kuud", + "chat.delete_message_confirm": "Oled kindel, et soovid selle sõnumi kustutada?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Koosta", + "composer.show_preview": "Kuva eelvaadet", + "composer.hide_preview": "Peida eelvaade", + "composer.user_said_in": "%1 ütles %2:", + "composer.user_said": "%1 ütles:", + "composer.discard": "Oled kindel, et soovid selle postituse tühistada?", + "composer.submit_and_lock": "Kinnita ja Lukusta", + "composer.toggle_dropdown": "Aktiveeri rippmenüü", + "composer.uploading": "%1 Üleslaadimine", + "composer.formatting.bold": "Paksult", + "composer.formatting.italic": "Kaldkiri", + "composer.formatting.list": "Nimekiri", + "composer.formatting.strikethrough": "Läbitõmmatud", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Lae pilt üles", + "composer.upload-file": "Lae fail üles", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "Olgu", + "bootbox.cancel": "Katkesta", + "bootbox.confirm": "Kinnita", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Kaanefoto Positsioneerimine", + "cover.dragging_message": "Vea kaanefoto soovitud kohta ja klikka \"Salvesta\"", + "cover.saved": "Kaanefoto ja paiknemine salvestatud", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/et/notifications.json b/public/language/et/notifications.json new file mode 100644 index 0000000000..c6479db247 --- /dev/null +++ b/public/language/et/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Teated", + "no_notifs": "Sul pole uusi teateid", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Tagasi %1", + "outgoing_link": "Väljaminev link", + "outgoing_link_message": "Lahkud %1", + "continue_to": "Jätka %1", + "return_to": "Pöördu tagasi %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Sul ei ole lugemata teateid.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Uus sõnum kasutajalt %1", + "upvoted_your_post_in": "%1 hääletas sinu postituse poolt teemas %2.", + "upvoted_your_post_in_dual": "%1 ja %2 kiitsid sinu postituse heaks: %3.", + "upvoted_your_post_in_multiple": "%1 ja %2 teist on kiitnud sinu postituse heaks: %3.", + "moved_your_post": "%1 liigutas sinu postituse %2 'sse", + "moved_your_topic": "%1 liigutas %2", + "user_flagged_post_in": "%1 raporteeris postitust %2", + "user_flagged_post_in_dual": "%1 ja %2 märgistasid postituse: %3", + "user_flagged_post_in_multiple": "%1 ja %2 teist märgistasid postituse: %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "Kasutaja %1 postitas vastuse teemasse %2", + "user_posted_to_dual": "%1 ja %2 on postitanud vastused: %3", + "user_posted_to_multiple": "%1 ja %2 teist on postitanud vastused: %3", + "user_posted_topic": "%1 on postitanud uue teema: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 hakkas sind jälgima.", + "user_started_following_you_dual": "%1 ja %2 hakkasid sind jälgima.", + "user_started_following_you_multiple": "%1 ja %2 hakkasid sind jälgima.", + "new_register": "%1 saatis registreerimistaotluse.", + "new_register_multiple": "%1 registreerimistaotlust ootavad ülevaadet.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Emaili aadress kinnitatud", + "email-confirmed-message": "Täname, et kinnitasite oma emaili aadressi. Teie kasutaja on nüüd täielikult aktiveeritud.", + "email-confirm-error-message": "Emaili aadressi kinnitamisel tekkis viga. Võibolla kinnituskood oli vale või aegunud.", + "email-confirm-sent": "Kinnituskiri on saadetud.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/et/pages.json b/public/language/et/pages.json new file mode 100644 index 0000000000..6aa52b6726 --- /dev/null +++ b/public/language/et/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Avaleht", + "unread": "Lugemata teemad", + "popular-day": "Populaarsed teemad täna", + "popular-week": "Populaarsed teemad sel nädalal", + "popular-month": "Populaarsed teemad sel kuul", + "popular-alltime": "Populaarseimad teemad üldse", + "recent": "Hiljutised teemad", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Sisseloginud kasutajad", + "users/latest": "Hiljutised kasutajad", + "users/sort-posts": "Kasutajad, kel on enim postitusi", + "users/sort-reputation": "Suurima reputatsiooniga kasutajad", + "users/banned": "Keelustatud Kasutajad", + "users/most-flags": "Enim raporteeritud kasutajad", + "users/search": "Kasutajate otsing", + "notifications": "Teated", + "tags": "Märksõnad", + "tag": "Topics tagged under "%1"", + "register": "Registreeri kasutaja", + "registration-complete": "Registration complete", + "login": "Logi oma kasutajasse sisse", + "reset": "Lähtesta oma kasutaja parool", + "categories": "Kategooriad", + "groups": "Grupid", + "group": "Kasutaja %1 grupp", + "chats": "Vestlused", + "chat": "Vestlus kasutajaga %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Muudan \"%1\"", + "account/edit/password": "Redigeerid \"%1\" parooli", + "account/edit/username": "Redigeerid \"%1\" kasutajanime", + "account/edit/email": "Redigeerid \"%1\" emaili", + "account/info": "Kasutaja info", + "account/following": "Kasutaja %1 jälgib", + "account/followers": "Kasutajad, kes jälgivad %1", + "account/posts": "Postitused, mis on tehtud kasutaja %1 poolt", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Teemad on kirjutanud %1", + "account/groups": "Kasutaja %1 grupid", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Kasutaja sätted", + "account/watched": "Teemasid jälgib %1 kasutajat", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Postitused %1 poolt heaks kiidetud", + "account/downvoted": "Postitused %1 poolt vastu hääletatud", + "account/best": "Parimad postitused %1 poolt", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Emaili aadress kinnitatud", + "maintenance.text": "%1 foorumil on käimas hooldustööd. Palun külastage meid mõne aja pärast uuesti.", + "maintenance.messageIntro": "Administraator on jätnud ka omaltpoolt sõnumi:", + "throttled.text": "%1 ei ole hetkel kättesaadav liigse koormuse tõttu. Palun tulge tagasi mõni teine kord." +} \ No newline at end of file diff --git a/public/language/et/post-queue.json b/public/language/et/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/et/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/et/recent.json b/public/language/et/recent.json new file mode 100644 index 0000000000..96882af6a3 --- /dev/null +++ b/public/language/et/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Hiljutised", + "day": "Päev", + "week": "Nädal", + "month": "Kuu", + "year": "Aasta", + "alltime": "Kogu aja vältel", + "no_recent_topics": "Hetkel ei ole hiljutisi teemasid.", + "no_popular_topics": "Ühtegi populaarset teemat ei leidu.", + "there-is-a-new-topic": "On loodud uus teema.", + "there-is-a-new-topic-and-a-new-post": "On loodud uus teema ning postitus.", + "there-is-a-new-topic-and-new-posts": "On loodud uus teema ning %1 uut postitust.", + "there-are-new-topics": "On loodud %1 uut teemat.", + "there-are-new-topics-and-a-new-post": "On loodud %1 uut teemat ning uus postitus.", + "there-are-new-topics-and-new-posts": "On loodud %1 uut teemat ning %2 uut postitust.", + "there-is-a-new-post": "On loodud uus postitus.", + "there-are-new-posts": "On loodud %1 uut postitust.", + "click-here-to-reload": "Värskendamiseks vajuta siia." +} \ No newline at end of file diff --git a/public/language/et/register.json b/public/language/et/register.json new file mode 100644 index 0000000000..0e7b671d59 --- /dev/null +++ b/public/language/et/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registreeri", + "cancel_registration": "Katkesta registreerimine", + "help.email": "Algsättena peidetakse sinu e-mail avalikuse eest.", + "help.username_restrictions": "Unikaalne kasutajanimi, mis on %1 - %2 tähemärki pikk. Teised saavad sind postitustes mainida kasutades @kasutajanime.", + "help.minimum_password_length": "Sinu parooli pikkus peab olema vähemalt %1 tähemärki pikk.", + "email_address": "Emaili aadress", + "email_address_placeholder": "Sisesta emaili aadress", + "username": "Kasutajanimi", + "username_placeholder": "Sisesta kasutajanimi", + "password": "Parool", + "password_placeholder": "Sisesta parool", + "confirm_password": "Kinnita parool", + "confirm_password_placeholder": "Kinnita parool", + "register_now_button": "Registreeri", + "alternative_registration": "Alternatiivne registreerimismeetod", + "terms_of_use": "Foorumi reeglid", + "agree_to_terms_of_use": "Nõustun foorumi reeglitega", + "terms_of_use_error": "Sa pead nõustuma Tingimustega", + "registration-added-to-queue": "Teie registreerimine vaadatakse üle. Te saate e-kirja kui administraator on aktsepteerinud registreermimise.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/et/reset_password.json b/public/language/et/reset_password.json new file mode 100644 index 0000000000..08eec7a8fb --- /dev/null +++ b/public/language/et/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Lähtesta parool", + "update_password": "Uuenda parooli", + "password_changed.title": "Parool muudetud", + "password_changed.message": "

Parool edukalt lähtestatud, palun logi uuesti sisse.", + "wrong_reset_code.title": "Vale kood", + "wrong_reset_code.message": "Sisestatud kood oil vale. Palun proovi uuesti või genereeri uus kood.", + "new_password": "Uus parool", + "repeat_password": "Kinnita parool", + "changing_password": "Changing Password", + "enter_email": "Palun sisesta oma emaili aadress ja me saadame sulle emaili koos õpetusega, kuidas oma parooli vahetada.", + "enter_email_address": "Sisesta emaili aadress", + "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.", + "invalid_email": "Vigane emaili aadress / emaili aadressi ei ekisteeri!", + "password_too_short": "Sisestatud parool on liiga lühike, palun vali teine parool.", + "passwords_do_not_match": "Sisestatud paroolid ei ühti.", + "password_expired": "Sinu parool on aegunud, palun vali uus parool" +} \ No newline at end of file diff --git a/public/language/et/search.json b/public/language/et/search.json new file mode 100644 index 0000000000..24de662022 --- /dev/null +++ b/public/language/et/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 tulemus(t) mis vastavad otsingule \"%2\", (%3 sekundit)", + "no-matches": "Vasteid ei leitud", + "advanced-search": "Täpsem otsing", + "in": "Kus kohast", + "titles": "Tiitlid", + "titles-posts": "Tiitlid ja postitused", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Autor", + "in-categories": "Kategooriates", + "search-child-categories": "Otsi vahekategooriatest", + "has-tags": "Has tags", + "reply-count": "Vastuste arv", + "at-least": "Rohkemalt", + "at-most": "Vähemalt", + "relevance": "Relevance", + "post-time": "Postitamise aeg", + "votes": "Votes", + "newer-than": "Uuem kui", + "older-than": "Vanem kui", + "any-date": "Kõik kuupäevad", + "yesterday": "Eile", + "one-week": "Üks nädal", + "two-weeks": "Kaks nädalat", + "one-month": "Üks kuu", + "three-months": "Kolm kuud", + "six-months": "Kuus kuud", + "one-year": "Üks aasta", + "sort-by": "Sorteeri", + "last-reply-time": "Viimase vastuse aeg", + "topic-title": "Teema tiitel", + "topic-votes": "Topic votes", + "number-of-replies": "Vastuste arv", + "number-of-views": "Vaatamiste arv", + "topic-start-date": "Teema alguskuupäev", + "username": "Kasutajanimi", + "category": "Kategooria", + "descending": "Kahanevas järjekorras", + "ascending": "Kasvavas järjekorras", + "save-preferences": "Salvesta eelistused", + "clear-preferences": "Kustuta eelistused", + "search-preferences-saved": "Otsingueelistused salvestatud", + "search-preferences-cleared": "Otsingueelistused kustutatud", + "show-results-as": "Näita tulemusi kui", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/et/success.json b/public/language/et/success.json new file mode 100644 index 0000000000..1d897c8083 --- /dev/null +++ b/public/language/et/success.json @@ -0,0 +1,7 @@ +{ + "success": "Õnnestus", + "topic-post": "Edukalt postitatud.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Sisse logimine õnnestus!", + "settings-saved": "Seaded salvestatud!" +} \ No newline at end of file diff --git a/public/language/et/tags.json b/public/language/et/tags.json new file mode 100644 index 0000000000..9ef34f6198 --- /dev/null +++ b/public/language/et/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Teemasid, mis sisaldaksid seda märksõna, ei eksisteeri.", + "tags": "Märksõnad", + "enter_tags_here": "Sisesta märksõnad siia, %1 kuni %2 tähemärki märksõna kohta.", + "enter_tags_here_short": "Sisesta märksõnu...", + "no_tags": "Siin ei ole veel ühtegi märksõna.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/et/top.json b/public/language/et/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/et/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/et/topic.json b/public/language/et/topic.json new file mode 100644 index 0000000000..9c19a15e35 --- /dev/null +++ b/public/language/et/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Teema", + "title": "Title", + "no_topics_found": "Teemasid ei leitud!", + "no_posts_found": "Postitusi ei leitud!", + "post_is_deleted": "See postitus on kustutatud!", + "topic_is_deleted": "Antud teema on kustutatud!", + "profile": "Profiil", + "posted_by": "Postitas %1", + "posted_by_guest": "Postitatud külalise ppolt", + "chat": "Vestlus", + "notify_me": "Saa teateid uutest postitustest selles teemas", + "quote": "Tsiteeri", + "reply": "Vasta", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Vasta teemana", + "guest-login-reply": "Logi sisse, et vastata", + "login-to-view": "🔒 Log in to view", + "edit": "Muuda", + "delete": "Kustuta", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Kustuta", + "restore": "Taasta", + "move": "Liiguta", + "change-owner": "Change Owner", + "fork": "Fork", + "link": "Ühenda", + "share": "Jaga", + "tools": "Tööriistad", + "locked": "Lukustatud", + "pinned": "Märgistatud", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Liigutatud", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Vajuta siia, et tagasi minna viimati loetud postituse juurde siin teemas.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "See teema on kustutatud. Ainult kasutajad kellel on piisavalt õigusi saavad seda näha.", + "following_topic.message": "Sulle ei edastata enam teateid uutest postitustest kui keegi postitab siia teemasse.", + "not_following_topic.message": "Sa näed seda postitust lugemata postituste nimekirjas, kuid sa ei näe selle kohta teateid, kui keegi sinna postitab.", + "ignoring_topic.message": "Sa ei näe seda teemat enam lugemata teemade nimekirjas. Sind teavitatakse, kui Sind mainitakse või Sinu postitust kiidetakse heaks.", + "login_to_subscribe": "Palun registreeru kasutajaks või logi sisse, et tellida teateid selle postituse kohta.", + "markAsUnreadForAll.success": "Teema märgitud mitte-loetuks kõikidele.", + "mark_unread": "Märgi lugematuks", + "mark_unread.success": "Teema märgitud mitteloetuks.", + "watch": "Vaata", + "unwatch": "Ära järgi", + "watch.title": "Saa teateid uutest postitustest siin teemas", + "unwatch.title": "Ära järgi enam seda teemat", + "share_this_post": "Jaga seda postitust", + "watching": "Vaatan", + "not-watching": "Ei vaata", + "ignoring": "Ignoreerin", + "watching.description": "Teavita mind uutest vastustest.
Näita teemat lugemata teemade hulgas.", + "not-watching.description": "Ära teavita mind uutest vastustest.
Näita teemat lugemata teemade hulgas, kui kategooria on ignoreeritud.", + "ignoring.description": "Ära teavita mind uutest vastustest.
Ära näita teemat lugemata teemade hulgas.", + "thread_tools.title": "Teema tööriistad", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Tõsta esile teema", + "thread_tools.unpin": "Märgista teema", + "thread_tools.lock": "Lukusta teema", + "thread_tools.unlock": "Taasava teema", + "thread_tools.move": "Liiguta teema", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Liiguta kõik", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Fork Topic", + "thread_tools.delete": "Kustuta teema", + "thread_tools.delete-posts": "Kustuta Postitusi", + "thread_tools.delete_confirm": "Oled kindel, et soovid selle teema kustutada?", + "thread_tools.restore": "Taasta teema", + "thread_tools.restore_confirm": "Oled kindel, et soovid selle teema taastada?", + "thread_tools.purge": "Kustuta teema täielikult", + "thread_tools.purge_confirm": "Oled kindel, et soovid puhastada selle teema?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Oled kindel, et soovid kustutada selle postituse?", + "post_restore_confirm": "Oled kindel, et soovid taastada antud postituse?", + "post_purge_confirm": "Oled kindel, et soovid täielikult selle teema kustutada?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Laen kategooriaid", + "confirm_move": "Liiguta", + "confirm_fork": "Fork", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Laen postitusi", + "move_topic": "Liiguta teemat", + "move_topics": "Liiguta teemasi", + "move_post": "Liiguta postitust", + "post_moved": "Postitus liigutatud!", + "fork_topic": "Fork Topic", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Vajuta postitustele, mida soovid forkida", + "fork_no_pids": "Sa ei ole postitusi valinud!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 postitus(t) valitud", + "fork_success": "Edukalt ''forkisid'' teema! Vajuta siia, et vaadata loodud teemat.", + "delete_posts_instruction": "Klikka postitustel, mida tahad kustutada/puhastada", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Sisesta teema pealkiri siia...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Katkesta", + "composer.submit": "Postita", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Vastad %1'le", + "composer.new_topic": "Uus teema", + "composer.editing": "Editing", + "composer.uploading": "laen üles...", + "composer.thumb_url_label": "Kleebi teema marge.", + "composer.thumb_title": "Lisa märge sellele teemale", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Või lae üles üks fail", + "composer.thumb_remove": "Puhasta väljad", + "composer.drag_and_drop_images": "Lohista pildid siia", + "more_users_and_guests": "%1 kasutaja(t) ja %2 külalist", + "more_users": "veel %1 kasutaja(t)", + "more_guests": "veel %1 külalist", + "users_and_others": "%1 ja %2 teist", + "sort_by": "Sorteeri", + "oldest_to_newest": "Vanematest uuemateni", + "newest_to_oldest": "Uuematest vanemateni", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Loo uus teema selle asemel?", + "stale.warning": "Teema, millele vastad on küllaltki vana. Kas sooviksid hoopiski uue teema luua ning viidata sellele sinu vastuses?", + "stale.create": "Loo uus teema/alapealkiri", + "stale.reply_anyway": "Vasta sellele teemale siiski", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/et/unread.json b/public/language/et/unread.json new file mode 100644 index 0000000000..037f49ea6d --- /dev/null +++ b/public/language/et/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Lugemata", + "no_unread_topics": "Siin ei ole lugemata teemasi.", + "load_more": "Lae rohkem", + "mark_as_read": "Märgi loetuks", + "selected": "Valitud", + "all": "Kõik", + "all_categories": "Kõik kategooriad", + "topics_marked_as_read.success": "Teemad märgitud loetuks!", + "all-topics": "Kõik teemad", + "new-topics": "Uued teemad", + "watched-topics": "Vaadatud teemad", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/et/uploads.json b/public/language/et/uploads.json new file mode 100644 index 0000000000..d4f7bbcca4 --- /dev/null +++ b/public/language/et/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Laen faili üles...", + "select-file-to-upload": "Vali fail mida üles laadida!", + "upload-success": "Fail üles laetud edukalt!", + "maximum-file-size": "Maksimaalselt %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/et/user.json b/public/language/et/user.json new file mode 100644 index 0000000000..4cd4865bea --- /dev/null +++ b/public/language/et/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banned", + "muted": "Muted", + "offline": "Väljas", + "deleted": "Deleted", + "username": "Kasutajanimi", + "joindate": "Liitumiskuupäev", + "postcount": "Postitusi", + "email": "Email", + "confirm_email": "Kinnita email", + "account_info": "Kasutaja info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Bannige kasutaja", + "ban_account_confirm": "Kas te tõesti soovite antud kasutajat bannida?", + "unban_account": "Eemaldage kontolt ban", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Kustuta kasutaja", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Kasutaja kustutatud", + "account-content-deleted": "Account content deleted", + "fullname": "Täisnimi", + "website": "Koduleht", + "location": "Asukoht", + "age": "Vanus", + "joined": "Liitunud", + "lastonline": "Viimati online", + "profile": "Profiil", + "profile_views": "Vaatamisi", + "reputation": "Reputatsioon", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Vaadatud", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Jälgijad", + "following": "Jälgimised", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "Minust", + "signature": "Allkiri", + "birthday": "Sünnipäev", + "chat": "Vestlus", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Jälgi", + "unfollow": "Ära jälgi enam", + "more": "Rohkem", + "profile_update_success": "Profiil edukalt uuendatud!", + "change_picture": "Vaheta pilti", + "change_username": "Vaheta kasutajanime", + "change_email": "Vaheta emaili", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Muuda", + "edit-profile": "Redigeeri profiili", + "default_picture": "Algne ikoon", + "uploaded_picture": "Üleslaetud pilt", + "upload_new_picture": "Laadi uus pilt", + "upload_new_picture_from_url": "Lae uus pilt üles URL'ilt", + "current_password": "Praegune parool", + "change_password": "Vaheta parooli", + "change_password_error": "Vigane parool!", + "change_password_error_wrong_current": "Su praegune parool on vale!", + "change_password_error_match": "Paroolid peavad kattuma!", + "change_password_error_privileges": "Sul ei ole piisavalt õigusi, et vahetada seda parooli.", + "change_password_success": "Sinu parool on uuendatud!", + "confirm_password": "Kinnita parool", + "password": "Parool", + "username_taken_workaround": "Kasutajanimi mida soovisid, ei olnud saadaval, seeg muutsime seda natukene. Sinu uus kasutajanimi on nüüd: %1", + "password_same_as_username": "Su parool kattub su kasutajanimega, palun vali mõni muu parool.", + "password_same_as_email": "Su parool kattub su e-mailiga, palun vali mõni muu parool.", + "weak_password": "Weak password.", + "upload_picture": "Laadi pilt", + "upload_a_picture": "Lae pilt üles", + "remove_uploaded_picture": "Eemalda üleslaetud pilt", + "upload_cover_picture": "Lae üles katte pilt", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Seaded", + "show_email": "Näita minu emaili", + "show_fullname": "Näita minu täisnime", + "restrict_chats": "Luba sõnumeid ainult kasutajatelt, keda järgin", + "digest_label": "Telli", + "digest_description": "Telli kõik teated emaili teel (uued teated ja teemad).", + "digest_off": "Väljas", + "digest_daily": "Igapäevaselt", + "digest_weekly": "Iga nädal", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Iga kuu", + "has_no_follower": "Sellel kasutajal pole ühtegi jälgijat :(", + "follows_no_one": "See kasutaja ei jälgi kedagi :(", + "has_no_posts": "Antud kasutaja pole veel midagi postitanud.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Antud kasutaja pole veel ühtegi teemat postitanud.", + "has_no_watched_topics": "Antud kasutaja pole veel ühtegi teemat vaadanud.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "Antud kasutaja pole veel ühtegi postitust kiitnud.", + "has_no_downvoted_posts": "Antud kasutaja pole veel ühtegi postitust laitnud.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Peidetud email", + "hidden": "peidetud", + "paginate_description": "Nummerda leheküljed ja postitused ning ära kasuta lõputut kerimist", + "topics_per_page": "Teemasi ühe lehekülje kohta", + "posts_per_page": "Postitusi ühe lehekülje kohta", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Sirvimis sätted", + "open_links_in_new_tab": "Ava väljaminevad lingid uues aknas", + "enable_topic_searching": "Võimalda teemasisene otsing", + "topic_search_help": "Kui see on sisse lükatud, siis teemasisene otsing võtab üle brauseri tavapärase otsingu ning võimaldab otsida ainult ekraanile mahtuva teema asemel terve teema ulatuses.", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Pärast vastuse postitamist, näita uut postitust", + "follow_topics_you_reply_to": "Jälgi teemasid, millele vastad", + "follow_topics_you_create": "Jälgi teemasid, mille lood", + "grouptitle": "Grupi tiitel", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Grupi tiitel puudub", + "select-skin": "Vali välimus", + "select-homepage": "Vali avaleht", + "homepage": "Avaleht", + "homepage_description": "Valige leht, mida kasutada foorumi esilehena või 'None', et kasutada vaikimisi esilehte.", + "custom_route": "Kohandatud Esilehe Teekond", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Ühekordse sisselogimisega teenused", + "sso.associated": "Seotud koos", + "sso.not-associated": "Kliki siia, et siduda koos", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Viimased raporteerimised", + "info.no-flags": "Raporteeritud postitusi ei leitud", + "info.ban-history": "Hiljutiste keeldude ajalugu", + "info.no-ban-history": "Seda kasutajat pole kunagi keelustatud", + "info.banned-until": "Keelustatud kuni %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Igavesti keelustatud", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/et/users.json b/public/language/et/users.json new file mode 100644 index 0000000000..0579549097 --- /dev/null +++ b/public/language/et/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Hilised kasutajad", + "top_posters": "Top postitajad", + "most_reputation": "Kõige rohkem reputatsiooni", + "most_flags": "Enim raporteerimisi", + "search": "Otsi", + "enter_username": "Sisesta kasutajanimi, keda soovid otsida", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Lae veel", + "users-found-search-took": "%1 kasutaja(t) leiti! Otsing kestis %2 sekundit.", + "filter-by": "Filtreeri", + "online-only": "Ainult seesolevad", + "invite": "Kutsuge", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Kutse on saadetud e-mailile %1", + "user_list": "Kasutajate list", + "recent_topics": "Viimased teemad", + "popular_topics": "Populaarsed teemad", + "unread_topics": "Lugemata teemad", + "categories": "Kategooriad", + "tags": "Märksõnad", + "no-users-found": "Ühtki kasutajat ei leitud!" +} \ No newline at end of file diff --git a/public/language/fa-IR/_DO_NOT_EDIT_FILES_HERE.md b/public/language/fa-IR/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/fa-IR/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/fa-IR/admin/admin.json b/public/language/fa-IR/admin/admin.json new file mode 100644 index 0000000000..adffa124c8 --- /dev/null +++ b/public/language/fa-IR/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "آیا شما مطمئن هستید که می خواهید NodeBB را بازسازی و مجدداً راه اندازی کنید؟", + "alert.confirm-restart": "آیا از راه اندازی مجدد نود‌بی‌بی مطمئن هستید؟", + + "acp-title": "%1 | کنترل پنل مدیر کل نود‌بی‌بی", + "settings-header-contents": "محتوا", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/advanced/cache.json b/public/language/fa-IR/admin/advanced/cache.json new file mode 100644 index 0000000000..7e2280f2bb --- /dev/null +++ b/public/language/fa-IR/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "کش دیدگاه ", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% تمام شده", + "post-cache-size": "سایز کش دیدگاه", + "items-in-cache": "موارد موجود در کش" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/advanced/database.json b/public/language/fa-IR/admin/advanced/database.json new file mode 100644 index 0000000000..ba2003eff0 --- /dev/null +++ b/public/language/fa-IR/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "آپتایم در ثانیه", + "uptime-days": "آپتایم در روز", + + "mongo": "پایگاه داده مونگو", + "mongo.version": "ورژن پایگاه داده مونگو دیبی", + "mongo.storage-engine": "سیستم ذخیره سازی", + "mongo.collections": "مجموعه ها", + "mongo.objects": "اشیا ", + "mongo.avg-object-size": "میانگین سایز اشیا", + "mongo.data-size": "سایز اطلاعات", + "mongo.storage-size": "اندازه محل ذخیره سازی", + "mongo.index-size": "اندازه شاخص", + "mongo.file-size": "اندازه فایل", + "mongo.resident-memory": "حافظه مقیم", + "mongo.virtual-memory": "حافظۀ مجازی", + "mongo.mapped-memory": "حافظه نقشه شده", + "mongo.bytes-in": "بایت های ورودی", + "mongo.bytes-out": "بایت های خروجی", + "mongo.num-requests": "تعداد درخواست ها", + "mongo.raw-info": "اطلاعات پایه پایگاه داده مونگو دی بی", + "mongo.unauthorized": "NodeBB نتوانست پرس و جوی آماری را از مونگو دی بی دریافت کند لطفا مطمئن شوید کاربر که NodeBB به وسیله آن به مونگو دیبی متصل شده است مجوز clusterMonitor را برای مدیر داشته باشد", + + "redis": "ردیس", + "redis.version": "ورژن ردیس", + "redis.keys": "کلید ها", + "redis.expires": "انقضا", + "redis.avg-ttl": "میانگین TTL", + "redis.connected-clients": "کلاینت های متصل شده", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/fa-IR/admin/advanced/errors.json b/public/language/fa-IR/admin/advanced/errors.json new file mode 100644 index 0000000000..de75745bf6 --- /dev/null +++ b/public/language/fa-IR/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "ارور 404 یافت نشد", + "error.503": "ارور 503 سرویس دردسترس نیست", + "manage-error-log": "مدیریت Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "پاک کردن Error Log", + "route": "مسیر", + "count": "شمارش", + "no-routes-not-found": "ایول! بدون ارور 404 !", + "clear404-confirm": "آیا از پاک کردن ارور های 404 اطمینان دارید؟", + "clear404-success": "ارور های 404 پاک شدند" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/advanced/events.json b/public/language/fa-IR/admin/advanced/events.json new file mode 100644 index 0000000000..71fc64b667 --- /dev/null +++ b/public/language/fa-IR/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "رویداد ها", + "no-events": "رویدادی موجود نیست", + "control-panel": "کنترل پنل رویداد ها", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/advanced/logs.json b/public/language/fa-IR/admin/advanced/logs.json new file mode 100644 index 0000000000..37846be559 --- /dev/null +++ b/public/language/fa-IR/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "گزارشات", + "control-panel": "کنترل پنل گزارشات", + "reload": "بارگزاری مجدد گزارش ها", + "clear": "حذف گزارشات", + "clear-success": "گزارش ها پاک شدند" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/appearance/customise.json b/public/language/fa-IR/admin/appearance/customise.json new file mode 100644 index 0000000000..abd4299b03 --- /dev/null +++ b/public/language/fa-IR/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "سفارشی کردن CSS/LESS", + "custom-css.description": "کد های CSS/LESS خود را در این قسمت وارد کنید . بعد از همه ی استایل های دیگر اعمال میشود", + "custom-css.enable": "به کار گرفتن CSS/LESS سفارشی", + + "custom-js": "جائا اسکریپت سفارشی", + "custom-js.description": "کد های جاوا اسکریپت خود را در این قسمت وارد کنید بعد از لود شدن تمام صفحه اجرا خواهند شد", + "custom-js.enable": "به کارگیری جاوا اسکریپت سفارشی ", + + "custom-header": "هدر سفارشی", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/appearance/skins.json b/public/language/fa-IR/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/fa-IR/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/appearance/themes.json b/public/language/fa-IR/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/fa-IR/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/dashboard.json b/public/language/fa-IR/admin/dashboard.json new file mode 100644 index 0000000000..7952eee216 --- /dev/null +++ b/public/language/fa-IR/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "نخوانده‌ها", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/fa-IR/admin/development/info.json b/public/language/fa-IR/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/fa-IR/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/development/logger.json b/public/language/fa-IR/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/fa-IR/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/extend/plugins.json b/public/language/fa-IR/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/fa-IR/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/fa-IR/admin/extend/rewards.json b/public/language/fa-IR/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/fa-IR/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/extend/widgets.json b/public/language/fa-IR/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/fa-IR/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/admins-mods.json b/public/language/fa-IR/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/fa-IR/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/categories.json b/public/language/fa-IR/admin/manage/categories.json new file mode 100644 index 0000000000..8cc20c9205 --- /dev/null +++ b/public/language/fa-IR/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "تنظیمات دسته‌بندی", + "privileges": "Privileges", + + "name": "نام دسته‌بندی", + "description": "توضیحات دسته‌بندی", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/digest.json b/public/language/fa-IR/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/fa-IR/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/fa-IR/admin/manage/groups.json b/public/language/fa-IR/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/fa-IR/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/privileges.json b/public/language/fa-IR/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/fa-IR/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/registration.json b/public/language/fa-IR/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/fa-IR/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/tags.json b/public/language/fa-IR/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/fa-IR/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/uploads.json b/public/language/fa-IR/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/fa-IR/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/users.json b/public/language/fa-IR/admin/manage/users.json new file mode 100644 index 0000000000..6a0fd92859 --- /dev/null +++ b/public/language/fa-IR/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "علت (اختیاری)", + "temp-ban.hours": "ساعت", + "temp-ban.days": "روز", + "temp-ban.explanation": "میزان مدت زمان اخراج را وارد کنید. توجه داشته باشید که مقدار 0 به عنوان اخراج مادام العمر در نظر گرفته خواهد شد.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/menu.json b/public/language/fa-IR/admin/menu.json new file mode 100644 index 0000000000..135e14ace0 --- /dev/null +++ b/public/language/fa-IR/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "عمومی", + + "section-manage": "Manage", + "manage/categories": "دسته‌بندی‌ها", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "کاربران", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "صف ثبت نام", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "رایانامه", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "برچسب ها", + "settings/notifications": "آگاه‌سازی‌ها", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "کوکی ها", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "گسترش", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "ورود با شبکه های اجتماعیث", + + "section-plugins": "Plugins\n", + "extend/plugins.install": "نصب افزونه ها", + + "section-advanced": "پیشرفته", + "advanced/database": "پایگاه داده", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "سیاهه ها", + "advanced/errors": "Errors", + "advanced/cache": "کش ", + "development/logger": "سیاهه ساز", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "هیچ نتیجه ای وجود ندارد", + "search.search-forum": "جستجو در انجمن برای ", + "search.keep-typing": "لطفا برای مشاهده نتیجه بیشتر بنویسید", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "به نظر می‌رسد اتصال شما به %1 از دست رفته. لطفا صبر کنید ما سعی می‌کنیم که دوباره شما را متصل کنیم.", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/advanced.json b/public/language/fa-IR/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/fa-IR/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/api.json b/public/language/fa-IR/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/fa-IR/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/chat.json b/public/language/fa-IR/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/fa-IR/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/cookies.json b/public/language/fa-IR/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/fa-IR/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/email.json b/public/language/fa-IR/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/fa-IR/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/fa-IR/admin/settings/general.json b/public/language/fa-IR/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/fa-IR/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/fa-IR/admin/settings/group.json b/public/language/fa-IR/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/fa-IR/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/guest.json b/public/language/fa-IR/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/fa-IR/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/homepage.json b/public/language/fa-IR/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/fa-IR/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/languages.json b/public/language/fa-IR/admin/settings/languages.json new file mode 100644 index 0000000000..352b3b69e0 --- /dev/null +++ b/public/language/fa-IR/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "تنظیمات زبان", + "description": "زبان پیشفرض، تنظیمات زبان همه کاربرانی که از انجمن شما بازدید میکنند را مشخص میکند.
کاربران میتونانند زبان پیشفرض را در صفحه تنظیمات شناسه کاربری خود تغییر دهند.", + "default-language": "زبان پیشفرض", + "auto-detect": "تشخیض خودکار تنظیمات زبان برای مهمانان" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/navigation.json b/public/language/fa-IR/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/fa-IR/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/fa-IR/admin/settings/notifications.json b/public/language/fa-IR/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/fa-IR/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/pagination.json b/public/language/fa-IR/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/fa-IR/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/post.json b/public/language/fa-IR/admin/settings/post.json new file mode 100644 index 0000000000..786f361ffc --- /dev/null +++ b/public/language/fa-IR/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "بیشترین پست", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/reputation.json b/public/language/fa-IR/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/fa-IR/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/social.json b/public/language/fa-IR/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/fa-IR/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/sockets.json b/public/language/fa-IR/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/fa-IR/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/sounds.json b/public/language/fa-IR/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/fa-IR/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/tags.json b/public/language/fa-IR/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/fa-IR/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/fa-IR/admin/settings/uploads.json b/public/language/fa-IR/admin/settings/uploads.json new file mode 100644 index 0000000000..2e84fbae21 --- /dev/null +++ b/public/language/fa-IR/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "پست‌ها", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/fa-IR/admin/settings/user.json b/public/language/fa-IR/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/fa-IR/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/fa-IR/admin/settings/web-crawler.json b/public/language/fa-IR/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/fa-IR/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/fa-IR/category.json b/public/language/fa-IR/category.json new file mode 100644 index 0000000000..c6a99107cd --- /dev/null +++ b/public/language/fa-IR/category.json @@ -0,0 +1,23 @@ +{ + "category": "دسته‌بندی", + "subcategories": "زیر دسته‌بندی‌", + "new_topic_button": "موضوع تازه", + "guest-login-post": "برای ارسال پست وارد شوید", + "no_topics": "هیچ پستی در این دسته‌بندی نیست.
چرا شما یکی نمی‌فرستید؟", + "browsing": "بیننده‌ها", + "no_replies": "هیچ کسی پاسخ نداده است.", + "no_new_posts": "هیچ پست جدیدی وجود ندارد.", + "watch": "پیگیری", + "ignore": "نادیده گرفتن", + "watching": "درحال پیگیری", + "not-watching": "درحال پیگیری نیستید", + "ignoring": "در حال نادیده گرفتن", + "watching.description": "موضوع ها را در بخش نخوانده ها و تازه ها نشان بده", + "not-watching.description": "موضوع ها را در بخش نخوانده ها نمایش نده و در بخش تازه ها نشان بده", + "ignoring.description": "موضوع ها را در بخش نخوانده ها و تازه ها نمایش نده", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "دسته بندی های پیگیری شده", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/fa-IR/email.json b/public/language/fa-IR/email.json new file mode 100644 index 0000000000..11a3bbe334 --- /dev/null +++ b/public/language/fa-IR/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "به %1 خوش آمدید", + "invite": "دعوتنامه از %1", + "greeting_no_name": "سلام", + "greeting_with_name": "سلام %1", + "email.verify-your-email.subject": "لطفا ایمیل خود را تأیید کنید", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "متشکریم بابت ثبت نام در %1!", + "welcome.text2": "برای فعال کردن کامل اکانت شما، ما نیاز داریم تا اطمینان حاصل کنیم که شما مالک ایمیلی که با ان ثبت نام کردید هستید.", + "welcome.text3": "ِک مدیر درخواست ثبت نام شما را قبول کرده. اکنون میتوانید با نام کاربری/رمز عبور خود وارد شوید", + "welcome.cta": "برای تأیید آدرس ایمیل خود اینجا کلیک کنید", + "invitation.text1": "%1 شما را برای پیوستن به %2 دعوت کرده", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "ما یک درخواست برای بازنشانی رمزعبور شما دریافت کرده ایم، احتمالا به این دلیل که شما آن را فراموش کرده اید. اگر این مورد نیست و شما رمز خود را به یاد دارید، لطفا این ایمیل را نادیده بگیرید.", + "reset.text2": "برای ادامه بازنشانی رمز، لطفابر روی این لینک کلیک کنید:", + "reset.cta": "برای تنظیم مجدد کلمه عبور‌ی خود اینجا کلیک کنید", + "reset.notify.subject": "کلمه عبور با موفقیت تغییر کرد", + "reset.notify.text1": "به شما اعلام میداریم که در %1، کلمه عبور شما با موفقیت بازنشانی شد.", + "reset.notify.text2": "اگر این را تایید نمیکنید، لطفا بلافاصله به یک مدیر اطلاع دهید.", + "digest.latest_topics": "آخرین پست های %1:", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "برای دیدن %1 اینجا کلیک کنید", + "digest.unsub.info": "این اعداد که برای شما فرستاده شده به علت تنظیمات اشترک شماست.", + "digest.day": "روز", + "digest.week": "هفته", + "digest.month": "ماه", + "digest.subject": "خلاصه برای %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "پیام چتی جدیدی از %1 دریافت شد", + "notif.chat.cta": "برای ادامه‌ی چت اینجا کلیک کنید", + "notif.chat.unsub.info": "این اطلاعیه ی چتیی که برای شما فرستاده شده به علت تنظیمات اشترک شماست.", + "notif.post.unsub.info": "این اطلاعیه ی پستی که برای شما فرستاده شده به علت تنظیمات اشترک شماست.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "این یک ایمیل امتحانی جهت تایید اینکه فرستنده ایمیل برای انجمن NodeBB شما به درستی تنظیم و نصب شده است", + "unsub.cta": "برای ویرایش آن تنظیمات اینجا کلیک کنید", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "سپاس!" +} \ No newline at end of file diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json new file mode 100644 index 0000000000..12123473d5 --- /dev/null +++ b/public/language/fa-IR/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "داده(های) نامعتبر", + "invalid-json": "JSON نامعتبر", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "وارد حساب کاربری نشده‌اید.", + "account-locked": "حساب کاربری شما موقتاً مسدود شده است.", + "search-requires-login": "استفاده از جستجو نیازمند ورود با نام‌کاربری و رمز‌عبور است. لطفا ابتدا وارد شوید.", + "goback": "Press back to return to the previous page", + "invalid-cid": "آی‌دی دسته‌بندی نامعتبر است.", + "invalid-tid": "شناسه موضوع نامعتبر است.", + "invalid-pid": "شناسه پست نامعتبر است.", + "invalid-uid": "شناسه کاربر نامعتبر است.", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "نام کاربری نامعتبر است.", + "invalid-email": "ایمیل نامعتبر است.", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "عنوان نامعتبر", + "invalid-user-data": "داده‌های کاربر نامعتبر است.", + "invalid-password": "کلمه عبور نامعتبر است.", + "invalid-login-credentials": "نام کاربری یا گذرواژه صحیح نیست", + "invalid-username-or-password": "لطفا هم نام کاربری و هم کلمه عبور را مشخص کنید", + "invalid-search-term": "کلمه جستجو نامعتبر است", + "invalid-url": "URL نامعتبر", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "اجازه ورود شما تمام شده است، لطفا دوباره وارد شوید.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "ارزش گذاری صفحه نامعتبر است، کمترین مقدار %1 و بیشترین مقدار %2 باید باشد", + "username-taken": "این نام کاربری گرفته شده است.", + "email-taken": "این ایمیل گرفته شده است.", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "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 may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "سیستم موفق به تایید ایمیل شما نشد، لطفا بعدا دوباره سعی کنید", + "confirm-email-already-sent": "ایمیل فعال‌سازی قبلا فرستاده شده، لطفا %1 دقیقه صبر کنید تا ایمیل دیگری فرستاده شود.", + "sendmail-not-found": "اجازه ارسال رایانامه پیدا نشد، لطفا مطمئن شوید این قابلیت نصب شده و توسط کاربر مد نظر در نود‌بی‌بی قابل اجرا است.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "نام کاربری خیلی کوتاه است.", + "username-too-long": "نام کاربری بسیار طولانیست", + "password-too-long": "کلمه عبور بسیار طولانیست", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "کاربر اخراج شد", + "user-banned-reason": "با عرض پوزش، این حساب کاربری از انجمن اخراج شده است (دلیل: %1)", + "user-banned-reason-until": "با عرض پوزش، این حساب کاربری تا %1 اخراج شده است (دلیل: %2)", + "user-too-new": "با عرض پوزش، شما باید %1 ثانیه پیش از فرستادن پست نخست خود صبر کنید", + "blacklisted-ip": "با عرض پوزش، آدرس IP شما در انجمن مسدود شده است، اگر فکر می‌کنید اشتباهی رخ داده با مدیریت انجمن تماس بگیرید.", + "ban-expiry-missing": "لطفا تاریخ پایان برای این مسدود کردن ارائه دهید", + "no-category": "دسته بندی وجود ندارد", + "no-topic": "موضوع وجود ندارد.", + "no-post": "پست وجود ندارد", + "no-group": "گروه وجود ندارد", + "no-user": "کاربر وجود ندارد", + "no-teaser": "تیزر وجود ندارد", + "no-flag": "Flag does not exist", + "no-privileges": "شما دسترسی کافی برای این کار را ندارید", + "category-disabled": "دسته غیر‌فعال شد.", + "topic-locked": "موضوع بسته شد.", + "post-edit-duration-expired": "شما تنها می توانید %1 ثانیه پس از فرستادن پست آن‌را ویرایش کنید", + "post-edit-duration-expired-minutes": "شما تنها می توانید %1 دقیقه(ها) پس از فرستادن پست آن‌ را ویرایش کنید", + "post-edit-duration-expired-minutes-seconds": "شما تنها می توانید %1 دقیقه(ها) %2 ثانیه(ها) پس از فرستادن پست آن‌ را ویرایش کنید", + "post-edit-duration-expired-hours": "شما تنها می توانید %1 ساعت(ها) پس از فرستادن پست آن‌ را ویرایش کنید", + "post-edit-duration-expired-hours-minutes": "شما تنها می توانید %1 ساعت(ها) %2 دقیقه(ها) پس از فرستادن پست آن‌ را ویرایش کنید", + "post-edit-duration-expired-days": "شما تنها می توانید %1 روز(ها) پس از فرستادن پست آن‌ را ویرایش کنید", + "post-edit-duration-expired-days-hours": "شما تنها می توانید %1 روز(ها) %2 ساعت(ها) پس از فرستادن پست آن‌ را ویرایش کنید", + "post-delete-duration-expired": "شما تنها می توانید %1 ثانیه(ها) پس از فرستادن پست آن‌ را پاک کنید", + "post-delete-duration-expired-minutes": "شما تنها می توانید %1 دقیقه(ها) پس از فرستادن پست آن‌ را پاک کنید", + "post-delete-duration-expired-minutes-seconds": "شما تنها می توانید %1 دقیقه(ها) %2 ثانیه(ها) پس از فرستادن پست آن‌ را پاک کنید", + "post-delete-duration-expired-hours": "شما تنها می توانید %1 ساعت(ها) پس از فرستادن پست آن‌ را پاک کنید", + "post-delete-duration-expired-hours-minutes": "شما تنها می توانید %1 ساعت(ها) %2 دقیقه(ها) پس از فرستادن پست آن‌ را پاک کنید", + "post-delete-duration-expired-days": "شما تنها می توانید %1 روز(ها) پس از فرستادن پست آن‌ را پاک کنید", + "post-delete-duration-expired-days-hours": "شما تنها می توانید %1 روز(ها) %2 ساعت(ها) پس از فرستادن پست آن‌ را پاک کنید", + "cant-delete-topic-has-reply": "اگر کسی به موضوع شما پاسخ داده باشد، نمیتوانید آنرا حذف نمائید", + "cant-delete-topic-has-replies": "اگر %1 به موضوع جواب داده شده باشد ، نمیتوانید آنرا حذف نمائید", + "content-too-short": "خواهشمندیم پست بلندتری بنویسید. پست‌ها دست‌کم باید %1 کاراکتر داشته باشند.", + "content-too-long": "لطفا طول مطلب را کوتاه تر کنید. طول پست نمیتواند بیشتر از %1 کاراکتر باشد.", + "title-too-short": "لطفا یک عنوان بلندتر وارد کنید. عنوان باید حداقل %1 کاراکتر داشته باشد.", + "title-too-long": "لطفا یک عنوان بلندتر وارد کنید. عنوان باید حداقل %1 کاراکتر داشته باشد.", + "category-not-selected": "هیچ دسته‌بندی انتخاب نشده.", + "too-many-posts": "شما می توانید هر %1 ثانیه یک پست ایجاد کنید - لطفا قبل از ارسال پست جدید صبر کنید", + "too-many-posts-newbie": "به عنوان یک کاربر جدید ، تا زمانی که شما %2 اعتبار کسب کنید می توانید هر %1 ثانیه یک پست ایجاد کنید - لطفا قبل از ایجاد پست جدید صبر کنید .", + "already-posting": "You are already posting", + "tag-too-short": "لطفا برچسب بلندتری وارد کنید. برچسبها باید حداقل %1 کاراکتر داشته باشند.", + "tag-too-long": "لطفا برچسب کوتاه تری وارد کنید . برچسب ها نباید بیشتر از %1 کاراکتر داشته باشند", + "not-enough-tags": "تعداد برچسب ها کافی نیست. موضوع ها یابد حداقل %1 برچسب داشته باشند", + "too-many-tags": "تعداد برچسب ها بیشتر از حد مجاز است. موضوع ها نمی توانند بیشتر از %1 برچسب داشته باشند", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "خواهشمندیم تا پایان بارگذاری‌ها شکیبا باشید.", + "file-too-big": "حداکثر مجاز حجم فایل %1 کیلوبایت می باشد - لطفا فایلی با حجم کمتر بارگذاری کنید", + "guest-upload-disabled": "بارگذاری برای مهمانان غیر فعال شده است", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "شما قبلا این پست را نشانک کرده‌اید", + "already-unbookmarked": "شما قبلا این پست را از نشانک در آوردید", + "cant-ban-other-admins": "شما نمی‌توانید دیگر مدیران را محروم کنید!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "شما تنها مدیر می باشید . شما باید قبل از عزل خود از مدیریت یک کاربر دیگر را مدیر کنید", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "قبل از حذف این کاربر دسترسی های مدیریت را از وی بگیرید.", + "already-deleting": "Already deleting", + "invalid-image": "عکس نامعتبر", + "invalid-image-type": "نوع تصویر نامعتبر است. نوعهای قابل قبول اینها هستند: %1", + "invalid-image-extension": "پسوند عکس نامعتبر است", + "invalid-file-type": "نوع پرونده نامعتبر است. نوعهای قابل قبول اینها هستند: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "نام گروه خیلی کوتاه است.", + "group-name-too-long": "نام گروه بسیار طولانی است.", + "group-already-exists": "این گروه از پیش وجود دارد.", + "group-name-change-not-allowed": "تغیر نام گروه نیاز به دسترسی دارد.", + "group-already-member": "شما عضوی از این گروه می باشید", + "group-not-member": "شما عضوی از این گروه نمی باشید", + "group-needs-owner": "این گروه حداقل یک مالک باید داشته باشد", + "group-already-invited": "این کاربر قبلا به گروه دعوت شده است", + "group-already-requested": "درخواست عضویت شما قبلا تایید شده است", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "این پست قبلا پاک شده است", + "post-already-restored": "پست قبلا بازگردانی شده است.", + "topic-already-deleted": "موضوع قبلا حذف شده است", + "topic-already-restored": "موضوع قبلا بازگردانی شده است", + "cant-purge-main-post": "شما نمی‌توانید پست اصلی را پاک کنید، لطفا موضوع را به جای آن پاک کنید.", + "topic-thumbnails-are-disabled": "چهرک‌های موضوع غیرفعال شده است.", + "invalid-file": "فایل نامعتبر است.", + "uploads-are-disabled": "امکان بارگذاری غیرفعال شده است.", + "signature-too-long": "با عرض پوزش ، امضای شما نمی تواند طولانی تر از %1 کاراکتر باشد", + "about-me-too-long": "با عرض پوزش محتوای 'درباره ی من' نمی تواند طولانی تر از %1 کاراکتر باشد", + "cant-chat-with-yourself": "شما نمی‌توانید با خودتان چت کنید!", + "chat-restricted": "این کاربر پیام های چتی خود را محدود کرده است . آنها بایدشما را دنبال کنند تا اینکه شما بتوانید به آنها پیامی بفرستید", + "chat-disabled": "سیستم گفتمان غیرفعال شده است", + "too-many-messages": "شما پیامهای خیلی زیادی فرستاده اید، لطفا مدتی صبر نمایید", + "invalid-chat-message": "پیام نامعتبر", + "chat-message-too-long": "پیام های چت نمی توانند بیشتر از %1 کاراکتر باشند.", + "cant-edit-chat-message": "شما اجازه ی ویرایش این پیام را ندارید", + "cant-delete-chat-message": "شما اجازه حذف این پیام را ندارید.", + "chat-edit-duration-expired": "شما قادر هستید پیام های چت را فقط بعد از %1 ثانیه ویرایش کنید", + "chat-delete-duration-expired": "شما قادر هستید پیام های چت را فقط بعد از %1 ثانیه پاک کنید", + "chat-deleted-already": "این پیام قبلا حذف شده است", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "شما قبلا به این پست رای داده اید.", + "reputation-system-disabled": "سیستم اعتبار غیر فعال شده است", + "downvoting-disabled": "رأی منفی غیر فعال شده است", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "شما قبلا این پست را گزارش دادید", + "user-already-flagged": "شما قبلا این کاربر را گزارش دادید", + "post-flagged-too-many-times": "این پست قبلا توسط دیگر کاربران گزارش شده", + "user-flagged-too-many-times": "این کاربر توسط دیگر کاربران گزارش شده", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "شما نمی توانید به پست خود رای بدهید", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB در هنگام بارگذاری مجدد با یک مشکل مواجه شده است: \"%1\". NodeBB سرویس رسانی به کلاینت های سرویس گیرنده را ادامه خواهد داد، اگرچه شما کاری را قبل از بارگیری مجدد انجام دادید بازگردانی کنید", + "registration-error": "خطای ثبت نام", + "parse-error": "هنگام تجزیه پاسخ سرور اشتباهی پیش امد", + "wrong-login-type-email": "لطفا از ایمیل خود برای ورود استفاده کنید", + "wrong-login-type-username": "لطفا از نام کاربری خود برای ورود استفاده کنید", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "شما نمی توانید چندین حساب گوگل را به حساب انجمن متصل کنید. لطفا اتصال حساب فعلی را لغو کنید و مجدد امتحان نمایید.", + "invite-maximum-met": "ظرفیت دعوت شما تکمیل شده است (%1 از %2)", + "no-session-found": "هیچ session ورودی یافت نشد!", + "not-in-room": "هیچ کاربری در این گفتگو نیست", + "cant-kick-self": "شما نمی توانید خودتان را از گروه کیک کنید", + "no-users-selected": "هیچ کاربر(های) انتخاب نشده", + "invalid-home-page-route": "مسیر صفحه اصلی نامعتبر است", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "هیچ موضوعی انتخاب نشده است !", + "cant-move-to-same-topic": "نمی توان پست یک موضوع را به همان موضوع انتقال داد !", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "شما نمی توانید خودتان را بلاک کنید!", + "cannot-block-privileged": "شما نمی توانید ادمین ها یا مدیر ها را بلاک کنید", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "به نظر می رسد اینترنت شما مشکل دارد", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/fa-IR/flags.json b/public/language/fa-IR/flags.json new file mode 100644 index 0000000000..eb51f18c93 --- /dev/null +++ b/public/language/fa-IR/flags.json @@ -0,0 +1,89 @@ +{ + "state": "وضعیت", + "reports": "گزارش ها", + "first-reported": "اولین گزارش", + "no-flags": "هووووورا ! هیچ گزارشی یافت نشد.", + "assignee": "اختصاص دادن ", + "update": "به روزرسانی", + "updated": "به روز رسانی شد", + "resolved": "Resolved", + "target-purged": "محتوای این گزارش حذف شده است و در دسترس نیست.", + + "graph-label": "آمار گزارش های روزانه", + "quick-filters": "فیلتر های سریع", + "filter-active": "یک یا تعداد بیشتری از فیلتر ها در لیست گزارش ها فعال هستند", + "filter-reset": "حذف فیلتر ها", + "filters": "گزینه های فیلتر", + "filter-reporterId": "UID گزارش دهنده", + "filter-targetUid": "UID گزارش", + "filter-type": "نوع گزارش", + "filter-type-all": "تمام محتوا", + "filter-type-post": "پست", + "filter-type-user": "کاربر", + "filter-state": "وضعیت", + "filter-assignee": "UID رسیدگی کننده", + "filter-cid": "دسته بندی", + "filter-quick-mine": "رسیدگی شده توسط من", + "filter-cid-all": "همه دسته بندی ها", + "apply-filters": "اعمال فیلتر ها", + "more-filters": "فیلترهای بیشتر", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "کاربر گزارش شده", + "view-profile": "نمایش پروفایل", + "start-new-chat": "شروع چت جدید", + "go-to-target": "مشاهده محتوای گزارش شده", + "assign-to-me": "اختصاص بده به من", + "delete-post": "حذف پست", + "purge-post": "پاک کردن پست", + "restore-post": "برگرداندن پست", + "delete": "Delete Flag", + + "user-view": "نمایش پروفایل", + "user-edit": "ویرایش پروفایل", + + "notes": "یادداشت های گزارش", + "add-note": "افزودن یادداشت", + "no-notes": "بدون یادداشت", + "delete-note-confirm": "آیا مطمئن هستید میخواهید این یادداشت را حذف کنید؟", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "یادداشت افزوده شد", + "note-deleted": "یادداشت حذف شد", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "بدون تاریخچه گزارش", + + "state-all": "همه وضعیت ها", + "state-open": "جدید / باز", + "state-wip": "در دست بررسی", + "state-resolved": "حل شده", + "state-rejected": "رد شده", + "no-assignee": "اختصاص داده نشده", + + "sort": "مرتب سازی بر اساس", + "sort-newest": "جدیدترین ها", + "sort-oldest": "قدیمی ترین ها", + "sort-reports": "بیشترین تعداد گزارش", + "sort-all": "انواع گزارش ها", + "sort-posts-only": "فقط پست ها", + "sort-downvotes": "بیشترین رای های منفی", + "sort-upvotes": "بیشترین رای های مثبت", + "sort-replies": "بیشترین پاسخ به پست", + + "modal-title": "Report Content", + "modal-body": "لطفا علت گزارش %2 %1 را برای بررسی مشخص کنید. همچنین می توانید از یکی از دکمه های ارسال سریع استفاده کنید.", + "modal-reason-spam": "هرزنامه", + "modal-reason-offensive": "توهین آمیز", + "modal-reason-other": "دیگر (در زیر مشخص کنید)", + "modal-reason-custom": "علت گزارش این محتوا...", + "modal-submit": "ارسال گزارش", + "modal-submit-success": "محتوا برای بررسی گزارش شد", + + "bulk-actions": "عملیات گروهی", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 گزارش بروزرسانی شد", + "flagged-timeago-readable": "گزارش شده (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/fa-IR/global.json b/public/language/fa-IR/global.json new file mode 100644 index 0000000000..5ccbedb8d5 --- /dev/null +++ b/public/language/fa-IR/global.json @@ -0,0 +1,126 @@ +{ + "home": "خانه", + "search": "جستجو", + "buttons.close": "بستن", + "403.title": "دسترسی ندارید", + "403.message": "به نظر می رسد شما به صفحه ای برخورد کرده اید که دسترسی به آن ندارید.", + "403.login": "شاید باید وارد شوید؟", + "404.title": "یافت نشد", + "404.message": "به نظر می رسد شما به صفحه ای برخورد کرده اید که وجود ندارد. بازگشت به صفحه ی خانه", + "500.title": "خطای داخلی.", + "500.message": "اوه! گویا اشتباهی رخ داده!", + "400.title": "درخواست بد.", + "400.message": "به نظر می‌رسد که این پیوند مشکل دارد، لطفا دوباره بررسی کنید که این پیوند صحیح است و دوباره تلاش کنید، در غیر این‌صورت به صفحه اصلی بازگردید.", + "register": "نام‌نویسی", + "login": "درون آمدن", + "please_log_in": "لطفا به درون بیایید", + "logout": "بیرون رفتن", + "posting_restriction_info": "دیدگاه گذاستن هم‌اکنون به اعضا محدود شده است، برای درون آمدن اینجا را بفشارید.", + "welcome_back": "خوش آمدید", + "you_have_successfully_logged_in": "با موفقیت درون آمده‌اید", + "save_changes": "اندوختن تغییرها", + "save": "ذخیره", + "close": "بستن", + "pagination": "صفحه‌بندی", + "pagination.out_of": "%1 از %2", + "pagination.enter_index": "Go to post index", + "header.admin": "مدیر", + "header.categories": "دسته‌بندی‌ها", + "header.recent": "تازه‌ها", + "header.unread": "نخوانده‌ها", + "header.tags": "برچسب‌ها", + "header.popular": "دوست‌داشتنی‌ها", + "header.top": "Top", + "header.users": "کاربران", + "header.groups": "گروه‌ها", + "header.chats": "چت ها", + "header.notifications": "آگاه‌سازی‌ها", + "header.search": "جستجو", + "header.profile": "نمایه", + "header.navigation": "Navigation", + "notifications.loading": "بارگذاری آگاه‌سازی‌ها", + "chats.loading": "بارگذاری گفتگوها", + "motd.welcome": "به NodeBB خوش آمدید، پلتفرم انجمن‌ساز آینده", + "previouspage": "برگهٔ پیشین", + "nextpage": "برگهٔ پسین", + "alert.success": "موفقیت", + "alert.error": "خطا", + "alert.banned": "بن شده ها", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "شما دیگر %1 را دنبال نمی‌کنید!", + "alert.follow": "اکنون %1 را دنبال می‌کنید.", + "users": "کاربران", + "topics": "موضوع ها", + "posts": "دیدگاه‌ها", + "x-posts": "%1 posts", + "best": "بهترین", + "controversial": "Controversial", + "votes": "رای ها", + "x-votes": "%1 votes", + "voters": "رای دهندگان", + "upvoters": "موافقین", + "upvoted": "رای مثبت", + "downvoters": "مخالفین", + "downvoted": "رای منفی", + "views": "بازدیدها", + "posters": "کاربران", + "reputation": "اعتبار", + "lastpost": "آخرین پست", + "firstpost": "اولین پست", + "read_more": "بیشتر بخوانید", + "more": "بیشتر", + "none": "None", + "posted_ago_by_guest": "ارسال شده در %1 توسط مهمان", + "posted_ago_by": "ارسال شده در %1 توسط %2", + "posted_ago": "ارسال شده در %1", + "posted_in": "پست شده در %1", + "posted_in_by": "پست شده در %1 توسط %2", + "posted_in_ago": "ارسال شده در %1 %2", + "posted_in_ago_by": "ارسال شده در %1 %2 توسط %3", + "user_posted_ago": "%1 در %2 ارسال کرده است", + "guest_posted_ago": "مهمان در %1 ارسال کرده است", + "last_edited_by": "آخرین ویرایش توسط %1 انجام شده", + "norecentposts": "هیچ دیدگاه تازه‌ای نیست", + "norecenttopics": "هیچ جستار تازه‌ای نیست", + "recentposts": "دیدگاه‌های تازه", + "recentips": "آخرین آی‌پی وارد شده", + "moderator_tools": "ابزار‌های مدیر", + "online": "آنلاین", + "away": "دور از دسترس", + "dnd": "مزاحم نشوید", + "invisible": "مخفی", + "offline": "آفلاین", + "email": "ایمیل", + "language": "زبان", + "guest": "مهمان", + "guests": "مهمان‌", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "انجمن بروزرسانی شد", + "updated.message": "این انجمن به آخرین نسخه بروزرسانی شد. برای بارگذاری مجدد صفحه اینجا کلیک کنید.", + "privacy": "حریم خصوصی", + "follow": "دنبال کن", + "unfollow": "دنبال نکن", + "delete_all": "حذف همه", + "map": "نقشه", + "sessions": "Session های ورود", + "ip_address": "آدرس IP", + "enter_page_number": "شماره صفحه را وارد کنید", + "upload_file": "بارگذاری فایل", + "upload": "بارگذاری", + "uploads": "آپلود ها", + "allowed-file-types": "فایل قابل قبول اینها هستند %1", + "unsaved-changes": "تغییرات شما ذخیره نشده. شما مطمئن هستید که میخواهید از اینجا دور شوید؟", + "reconnecting-message": "به نظر می‌رسد اتصال شما به %1 از دست رفته. لطفا صبر کنید ما سعی می‌کنیم که دوباره شما را متصل کنیم.", + "play": "پخش", + "cookies.message": "این وب‌سایت از کوکی شما برای اطمینان و تجربه استفاده بهتر از وب‌سایت ما استفاده می‌کند.", + "cookies.accept": "فهمیدم!", + "cookies.learn_more": "بیشتر بدانید", + "edited": "Edited", + "disabled": "Disabled", + "select": "انتخاب", + "user-search-prompt": "برای پیدا کردن کاربر اینجا چیزی بنویسید..." +} \ No newline at end of file diff --git a/public/language/fa-IR/groups.json b/public/language/fa-IR/groups.json new file mode 100644 index 0000000000..34116e987b --- /dev/null +++ b/public/language/fa-IR/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "گروه‌ها", + "view_group": "مشاهده گروه", + "owner": "مالک گروه", + "new_group": "ساخت گروه جدید", + "no_groups_found": "گروهی برای دیدن وجود ندارد", + "pending.accept": "قبول", + "pending.reject": "رد", + "pending.accept_all": "پذیرش همه", + "pending.reject_all": "رد همه", + "pending.none": "در حال حاضر هیچ عضوی در انتظار تایید نیست", + "invited.none": "در حال حاضر هیچ کسی دعوت نشده است", + "invited.uninvite": "لغو دعوت", + "invited.search": "جستجو به دنبال کاربرانی به جهت دعوت به این گروه", + "invited.notification_title": "شما برای پیوستن به %1 دعوت شده اید", + "request.notification_title": "درخواست عضویت در گروه از طرف %1", + "request.notification_text": "%1 درخواست عضویت در %2 را دارد", + "cover-save": "ذخیره", + "cover-saving": "در حال ذخیره کردن", + "details.title": "جزئیات گروه", + "details.members": "فهرست اعضا", + "details.pending": "اعضای در انتظار", + "details.invited": "اعضای دعوت شده", + "details.has_no_posts": "اعضای این گروه هیچ پستی ایجاد نکرده اند", + "details.latest_posts": "آخرین پست ها", + "details.private": "خصوصی", + "details.disableJoinRequests": "غیر فعال کردن درخواستهای عضویت", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "اعطاء/خلع مالکیت", + "details.kick": "بیرون انداختن", + "details.kick_confirm": "آیا شما مطمئن هستید که می خواهید این عضو از گروه را حذف کنید؟", + "details.add-member": "اضافه کردن عضو", + "details.owner_options": "مدیر گروه", + "details.group_name": "نام گروه", + "details.member_count": "تعداد اعضا", + "details.creation_date": "زمان ساخته شدن", + "details.description": "توضیحات", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "پیش نمایشِ نشان", + "details.change_icon": "تغییر آیکن", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "نوشته ای نشان", + "details.userTitleEnabled": "نمایش نشان", + "details.private_help": "اگر فعال باشد، پیوستن به گروه مستلزم موافقت مالک گروه است", + "details.hidden": "پنهان", + "details.hidden_help": "اگر فعال باشد، این گروه در فهرست گروه‌ها پیدا نمی‌شود و کاربران باید دستی فراخوانده شوند", + "details.delete_group": "حذف گروه", + "details.private_system_help": "گروه های خصوصی در این سطح سیستم غیر فعال هستند، این گزینه هیچ کاری انجام نمی دهد", + "event.updated": "جزییات گروه با موفقیت به روز شد", + "event.deleted": "گروه \"%1\" حدف شد", + "membership.accept-invitation": "دعوت را قبول میکنم", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "در انتظار تایید", + "membership.join-group": "ورود به گروه", + "membership.leave-group": "خروج از گروه", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "رد", + "new-group.group_name": "نام گروه:", + "upload-group-cover": "آپلود کاور گروه", + "bulk-invite-instructions": "برای دعوت به این گروه فهرستی از نام‌کاربری که با کاما جدا کنید را وارد کنید", + "bulk-invite": "دعوت گروهی", + "remove_group_cover_confirm": "آیا شما مطمئن هستید که می خواهید عکس کاور را حذف کنید؟" +} \ No newline at end of file diff --git a/public/language/fa-IR/ip-blacklist.json b/public/language/fa-IR/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/fa-IR/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/fa-IR/language.json b/public/language/fa-IR/language.json new file mode 100644 index 0000000000..53117369f8 --- /dev/null +++ b/public/language/fa-IR/language.json @@ -0,0 +1,5 @@ +{ + "name": "فارسی", + "code": "fa-IR", + "dir": "rtl" +} \ No newline at end of file diff --git a/public/language/fa-IR/login.json b/public/language/fa-IR/login.json new file mode 100644 index 0000000000..50a0d06f2a --- /dev/null +++ b/public/language/fa-IR/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "نام کاربری / ایمیل", + "username": "نام کاربری", + "remember_me": "مرا به یاد بسپار؟", + "forgot_password": "رمز عبور را فراموش کرده‌اید؟", + "alternative_logins": "روش‌های ثبت نام جایگزین", + "failed_login_attempt": "ورود ناموفق", + "login_successful": "شما با موفقیت وارد شده‌اید!", + "dont_have_account": "حساب کاربری ندارید؟", + "logged-out-due-to-inactivity": "شما به علت عدم فعالیت از کنترل پنل مدیر کل خارج شده اید ", + "caps-lock-enabled": "کلید Caps Lock فعال است" +} \ No newline at end of file diff --git a/public/language/fa-IR/modules.json b/public/language/fa-IR/modules.json new file mode 100644 index 0000000000..63afa4e3f1 --- /dev/null +++ b/public/language/fa-IR/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "چت با", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "شما در حال مشاهده پیام های قدیمی هستید، برای دیدن پیام های اخیر کلیک کنید.", + "chat.send": "ارسال", + "chat.no_active": "شما هیچ گفتگوی فعالی ندارید.", + "chat.user_typing": "%1 در حال نوشتن است...", + "chat.user_has_messaged_you": "%1 به شما پیام داده است.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "مشخص کنید تاریخچه گفتگوها با چه کاربری را می‌خواهید ببینید", + "chat.no-users-in-room": "هیچ کاربری در این گفتگو نیست", + "chat.recent-chats": "گفتگو های اخیر", + "chat.contacts": "تماس‌ها", + "chat.message-history": "تاریخچه پیام‌ها", + "chat.message-deleted": "Message Deleted", + "chat.options": "تنظیمات چت", + "chat.pop-out": "پاپ آپ گفتگو", + "chat.minimize": "کوچک کردن", + "chat.maximize": "تمام صفحه", + "chat.seven_days": "7 روز", + "chat.thirty_days": "30 روز", + "chat.three_months": "3 ماه", + "chat.delete_message_confirm": "آیا مطمئن هستید که می خواهید این پیام را حذف کنید؟", + "chat.retrieving-users": "Retrieving users...", + "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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "این کاربر وضعیت خود را روی حالت مزاحم نشوید قرار داده است. آیا همچنان می خواهید با او چت کنید؟", + "chat.rename-room": "تعویض اسم چت روم", + "chat.rename-placeholder": "اسم چت روم را اینجا وارد کنید", + "chat.rename-help": "اسم چت روم برای همه کاربران چت روم قابل رویت خواهد بود.", + "chat.leave": "ترک چت روم", + "chat.leave-prompt": "آیا شما مطمئن هستید که می خواهید چت روم را ترک کنید؟", + "chat.leave-help": "ترک این چت روم باعث از دست دادن ارتباط شما می شود. اگر شما بعدا به چت روم اضافه بشوید، قادر به مشاهده تاریخچه پیام ها نخواهید بود.", + "chat.in-room": "در این چت روم", + "chat.kick": "اخراج", + "chat.show-ip": "نشان دادن IP", + "chat.owner": "مدیر چت روم", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "ارسال", + "composer.show_preview": "نمایش پیش‌نمایش", + "composer.hide_preview": "مخفی کردن پیش‌نمایش", + "composer.user_said_in": "%1 در %2 گفته است:", + "composer.user_said": "%1 گفته است:", + "composer.discard": "آیا از دور انداختن این پست اطمینان دارید؟", + "composer.submit_and_lock": "ارسال و قفل", + "composer.toggle_dropdown": "باز و بسته کردن کرکره", + "composer.uploading": "در حال بارگذاری %1", + "composer.formatting.bold": "توپر", + "composer.formatting.italic": "کج", + "composer.formatting.list": "فهرست", + "composer.formatting.strikethrough": "خط خورده", + "composer.formatting.code": "کد", + "composer.formatting.link": "پیوند", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "بارگذاری عکس", + "composer.upload-file": "بارگذاری فایل", + "composer.zen_mode": "حالت ذن", + "composer.select_category": "یک دسته‌بندی انتخاب کنید", + "composer.textarea.placeholder": "محتوای پست خود را اینجا وارد کنید یا تصاویر را به اینجا بکشید و رها کنید", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "باشه", + "bootbox.cancel": "انصراف", + "bootbox.confirm": "تایید", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "تنظیم مکان عکس کاور", + "cover.dragging_message": "عکس کاور با کلیک موس گرفته و در مکان دلخواه رها کنید و بر روی \"ذخیره\" کلیک کنید", + "cover.saved": "عکس کاور و مکان آن ذخیره شد", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/fa-IR/notifications.json b/public/language/fa-IR/notifications.json new file mode 100644 index 0000000000..e1d6e5928b --- /dev/null +++ b/public/language/fa-IR/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "آگاه‌سازی‌ها", + "no_notifs": "هیچ آگاه‌سازی تازه‌ای ندارید", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "بازگشت به %1", + "outgoing_link": "پیوند برون‌رو", + "outgoing_link_message": "شما در حال ترک %1 هستید", + "continue_to": "ادامه به %1", + "return_to": "بازگشت به %1", + "new_notification": "شما یک آگاه‌سازی تازه دارید", + "you_have_unread_notifications": "شما آگاه‌سازی‌‌های نخوانده دارید.", + "all": "همه", + "topics": "موضوع ها", + "replies": "پاسخ ها", + "chat": "گفتگو ها", + "group-chat": "Group Chats", + "follows": "دنبال کننده ها", + "upvote": "رای های مثبت", + "new-flags": "گزارش های جدید", + "my-flags": "گزارش های اختصاص یافته به من", + "bans": "اخراجی ها", + "new_message_from": "پیام تازه از %1", + "upvoted_your_post_in": "%1 امتیاز مثبت به پست شما در %2 داده", + "upvoted_your_post_in_dual": "%1 و %2 رای مثبت به پست شما در\n %3.", + "upvoted_your_post_in_multiple": "%1و %2 دیگران به پست شما رای مثبت دادن در %3.", + "moved_your_post": "%1 پست شما را به %2 انتقال داده است", + "moved_your_topic": "%2 %1 را منتقل کرده است", + "user_flagged_post_in": "%1 پستی را در %2 گزارش کرده", + "user_flagged_post_in_dual": "%1 و %2 پستی را در %3 گزارش کرده اند", + "user_flagged_post_in_multiple": "%1 و %2 نفر دیگر این پست را در %3 گزارش کرده اند", + "user_flagged_user": "%1کاربری را برای بررسی گزارش کرد (%2)", + "user_flagged_user_dual": "%1و %2کاربری را برای بررسی گزارش کردند (%3)", + "user_flagged_user_multiple": "%1و %2 دیگران کاربری را برای بررسی گزارش کردند (%3)", + "user_posted_to": "پاسخ دادن به %2 از سوی %1", + "user_posted_to_dual": "%1 و %2 پاسخ به پست دادند در: %3", + "user_posted_to_multiple": "%1 و %2 نفر دیگر به پست شما پاسخ ارسال کرده‌اند در: %3", + "user_posted_topic": "%1 یک موضوع جدید ارسال کرده: %2", + "user_edited_post": "%1 پستی را در %2 ویرایش کرد", + "user_started_following_you": "%1 شروع به دنبال کردن شما کرده", + "user_started_following_you_dual": "%1 و %2 شروع به دنبال کردن شما کرده.", + "user_started_following_you_multiple": "%1 و %2 نفر دیگر شروع به دنبال کردن شما کرده.", + "new_register": "%1 یک درخواست ثبت نام ارسال کرده است", + "new_register_multiple": "تعداد %1 درخواست عضویت برای بررسی وجود دارد.", + "flag_assigned_to_you": "گزارش %1به شما تعلق گرفت", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "ایمیل تایید شد", + "email-confirmed-message": "بابت تایید ایمیلتان سپاس‌گزاریم. حساب کاربری شما اکنون به صورت کامل فعال شده است.", + "email-confirm-error-message": "خطایی در تایید آدرس ایمیل شما پیش آمده است. ممکن است کد نا‌معتبر و یا منقضی شده باشد.", + "email-confirm-sent": "ایمیل تایید ارسال شد.", + "none": "هیچکدام", + "notification_only": "فقط اعلان", + "email_only": "فقط ایمیل", + "notification_and_email": "اعلان و ایمیل", + "notificationType_upvote": "هنگامی که شخصی به پست شما رای مثبت می دهد", + "notificationType_new-topic": "هنگامی که شخصی که شما دنبال می کنید موضوعی ایجاد نماید", + "notificationType_new-reply": "هنگامی که پاسخ جدید در موضوعی که شما پیگیری می کنید فرستاده می شود", + "notificationType_post-edit": "وقتی در موضوعی که شما پیگیری می کنید پستی ویرایش می شود", + "notificationType_follow": "هنگامی که کسی شما را دنبال می کند", + "notificationType_new-chat": "هنگامی که شما پیام خصوصی دریافت می کنید", + "notificationType_new-group-chat": "هنگامی که شما پیام چت گروهی دریافت می کنید", + "notificationType_group-invite": "هنگامی که شما دعوتنامه گروه دریافت می کنید", + "notificationType_group-leave": "هنگامی که کاربری گروه شما را ترک می کند", + "notificationType_group-request-membership": "هنگامی که کسی درخواست ملحق شدن به گروه شما را می دهد", + "notificationType_new-register": "وقتی کسی به صف ثبت نام اضافه می شود", + "notificationType_post-queue": "هنگامی که پست جدیدی در صف قرار می گیرد", + "notificationType_new-post-flag": "هنگامی که پستی گزارش می شود", + "notificationType_new-user-flag": "هنگامی که کاربری گزارش می شود" +} \ No newline at end of file diff --git a/public/language/fa-IR/pages.json b/public/language/fa-IR/pages.json new file mode 100644 index 0000000000..4b7a4f977d --- /dev/null +++ b/public/language/fa-IR/pages.json @@ -0,0 +1,65 @@ +{ + "home": "خانه", + "unread": "جستاره‌های نخوانده", + "popular-day": "موضوعات پربازدید امروز", + "popular-week": "موضوعات پربازدید این هفته", + "popular-month": "موضوعات پربازدید این ماه", + "popular-alltime": "پربازدیدترین موضوعات", + "recent": "موضوع‌های تازه", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "ابزار مدیران", + "flagged-content": "محتوای گزارش شده", + "ip-blacklist": "لیست سیاه IP", + "post-queue": "صف پست", + "users/online": "کاربران آنلاین", + "users/latest": "آخرین کاربران", + "users/sort-posts": "کاربران با بیش‌ترین پست", + "users/sort-reputation": "کاربران دارای بیشترین اعتبار", + "users/banned": "کاربران مسدود شده", + "users/most-flags": "بیشترین کاربران گزارش شده", + "users/search": "جستجوی کاربر", + "notifications": "آگاه‌سازی‌ها", + "tags": "برچسب‌ها", + "tag": "Topics tagged under "%1"", + "register": "ثبت نام یک حساب", + "registration-complete": "ثبت نام تکمیل شد", + "login": "به حساب خوب وارد شوید", + "reset": "رمز عبور حساب خود را بازنشانی کنید", + "categories": "دسته‌بندی‌ها", + "groups": "گروه‌ها", + "group": "%1 گروه", + "chats": "چت ها", + "chat": "چت با %1", + "flags": "گزارش ها", + "flag-details": "جزئیات گزارش %1", + "account/edit": "ویرایش \"%1\"", + "account/edit/password": "ویرایش کلمه ی عبورِ \"%1\"", + "account/edit/username": "ویرایش نام کاربریِ \"%1\"", + "account/edit/email": "ویرایش ایمیلِ \"%1\"", + "account/info": "اطلاعات شناسه کاربری", + "account/following": "کاربرانی که %1 دنبال می‌کند", + "account/followers": "کاربرانی که %1 را دنبال می‌کنند", + "account/posts": "پست‌های %1", + "account/latest-posts": "آخرین پست های %1", + "account/topics": "موضوع های %1", + "account/groups": "گروه‌های %1", + "account/watched_categories": "دسته بندی های پیگیری شده %1", + "account/bookmarks": "%1 پست نشانک گذاری شده است", + "account/settings": "تنظیمات کاربر", + "account/watched": "موضوع های دیده شده توسط \"%1\"", + "account/ignored": "موضوع های نادیده گرفته شده توسط %1", + "account/upvoted": "رای مثبت داده شده به پست ها توسط %1", + "account/downvoted": "رای منفی داده شده به پست ها توسط %1", + "account/best": "بهترین پست های %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "کاربران مسدود شده توسط %1", + "account/uploads": "آپلود های %1", + "account/sessions": "Session های ورود", + "confirm": "ایمیل تایید شد", + "maintenance.text": "%1 در حال حاضر تحت تعمیر و نگهدارییست. لطفا زمان دیگری مراجعه کنید.", + "maintenance.messageIntro": "علاوه بر این، مدیر این پیام را گذاشته است:", + "throttled.text": "%1 به دلیل بارگذاری بیش از حد ، قابل دسترس نمی باشد. لطفا در زمان دیگری دوباره امتحان کنید" +} \ No newline at end of file diff --git a/public/language/fa-IR/post-queue.json b/public/language/fa-IR/post-queue.json new file mode 100644 index 0000000000..e029efb278 --- /dev/null +++ b/public/language/fa-IR/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "صف پست", + "description": "پستی در صف پست وجود ندارد.
برای فعال سازی این قابلیت، به تنظیمات → پست → صف پست بروید و صف پست را فعال کنید.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/fa-IR/recent.json b/public/language/fa-IR/recent.json new file mode 100644 index 0000000000..5cf0e71685 --- /dev/null +++ b/public/language/fa-IR/recent.json @@ -0,0 +1,19 @@ +{ + "title": "تازه‌ها", + "day": "روز", + "week": "هفته", + "month": "ماه", + "year": "سال", + "alltime": "همه زمانها", + "no_recent_topics": "هیچ موضوع تازه‌ای نیست.", + "no_popular_topics": "هیچ موضوع پربازدیدی وجود ندارد", + "there-is-a-new-topic": "یک موضوع جدید موجود است.", + "there-is-a-new-topic-and-a-new-post": "یک موضوع جدید و یک پست جدید موجود است.", + "there-is-a-new-topic-and-new-posts": "یک موضوع جدید و %1 پست جدید موجود است.", + "there-are-new-topics": "%1 موضوع جدید موجود است.", + "there-are-new-topics-and-a-new-post": "%1 موضوع جدید و یک پست جدید موجود است.", + "there-are-new-topics-and-new-posts": "%1 موضوع جدید و %2 پست جدید موجود است.", + "there-is-a-new-post": "یک پست جدید موجود است.", + "there-are-new-posts": "%1 پست جدید موجود است.", + "click-here-to-reload": "برای بارگذاری مجدد کلیک کنید." +} \ No newline at end of file diff --git a/public/language/fa-IR/register.json b/public/language/fa-IR/register.json new file mode 100644 index 0000000000..d7876aa359 --- /dev/null +++ b/public/language/fa-IR/register.json @@ -0,0 +1,32 @@ +{ + "register": "نام‌نویسی", + "cancel_registration": "انصراف ثبت نام", + "help.email": "به طور پیش‌فرض، ایمیل‌ی شما از دید همگان پنهان می‌شود.", + "help.username_restrictions": "یک نام کاربری یکتا بین %1 و %2 نویسه. دیگران می‌توانند با @نام‌کاربری به شما اشاره کنند.", + "help.minimum_password_length": "کلمه عبور شما باید دست‌کم %1 کاراکتر داشته باشد.", + "email_address": "نشانی رایانامه", + "email_address_placeholder": "نوشتن نشانی رایانامه", + "username": "نام کاربری", + "username_placeholder": "نوشتن نام کاربری", + "password": "گذرواژه", + "password_placeholder": "نوشتن گذرواژه", + "confirm_password": "تأیید گذرواژه", + "confirm_password_placeholder": "تکرار گذرواژه", + "register_now_button": "اکنون نام‌نویسی کنید", + "alternative_registration": "روش ثبت نام جایگزین", + "terms_of_use": "شرایط استفاده", + "agree_to_terms_of_use": "با شرایط استفاده موافقم", + "terms_of_use_error": "شما باید با شرایط انجمن موافقت کنید", + "registration-added-to-queue": "ثبت نام شما به صف تایید اضافه شد. وقتی توسط یک مدیر تایید شد شما ایمیلی دریافت خواهید کرد.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "من با جمع آوری و پرداز اطلاعات شخصی در این وبسایت موافقم.", + "gdpr_agree_email": "من با دریافت ایمیل ها و خلاصه ها از این وبسایت موافقم.", + "gdpr_consent_denied": "شما باید رضایت خود را برای جمع آوری/پردازش اطلاعاتتان و دریافت ایمیل را اعلام کنید.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/fa-IR/reset_password.json b/public/language/fa-IR/reset_password.json new file mode 100644 index 0000000000..3bc66b1940 --- /dev/null +++ b/public/language/fa-IR/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "بازیابی گذرواژه", + "update_password": "تازه‌سازی گذرواژه", + "password_changed.title": "گذرواژه تغییر کرد", + "password_changed.message": "

گذرواژه با موفقیت بازیابی شد، لطفا دوباره درون بیایید.", + "wrong_reset_code.title": "کد بازیابی نادرست است", + "wrong_reset_code.message": "کد بازیابی که دریافت شد، نادرست بود. لطفا دوباره تلاش کنید یا یک کد بازیابی تازه درخواست کنید.", + "new_password": "گذرواژهٔ تازه", + "repeat_password": "تکرار گذرواژه", + "changing_password": "Changing Password", + "enter_email": "لطفا نشانی رایانامهٔ خود را بنویسید و ما دستورکار بازیابی شناسه‌تان را به این رایانامه می‌فرستیم.", + "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.", + "invalid_email": "رایانامهٔ نامعتبر / رایانامه وجود ندارد!", + "password_too_short": "کلمه عبور وارد شده خیلی کوتاه است، لطفا یک گذر واژه طولانی تر انتخاب کنید.", + "passwords_do_not_match": "دو کلمه عبوری که وارد کرده اید مطابقت ندارند.", + "password_expired": "کلمه عبور شما منقضی شده، لطفا کلمه عبور جدیدی انتخاب کنید" +} \ No newline at end of file diff --git a/public/language/fa-IR/search.json b/public/language/fa-IR/search.json new file mode 100644 index 0000000000..9f4d57c17c --- /dev/null +++ b/public/language/fa-IR/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 نتیجهٔ هم‌خوان با \"%2\"، (%3 ثانیه)", + "no-matches": "هیچ موردی یافت نشد", + "advanced-search": "جستجوی پیشرفته", + "in": "در", + "titles": "عناوین", + "titles-posts": "عناوین و پست ها", + "match-words": "تطابق کلمات", + "all": "همه", + "any": "هرکدام", + "posted-by": "ارسال شده توسط", + "in-categories": "در دسته بندی ها", + "search-child-categories": "جستجوی زیر دسته ها", + "has-tags": "دارای برچسب های", + "reply-count": "تعداد پاسخ", + "at-least": "حداقل", + "at-most": "حداکثر", + "relevance": "ارتباط", + "post-time": "زمان ارسال", + "votes": "Votes", + "newer-than": "جدیدتر از", + "older-than": "قدیمی تر از", + "any-date": "هر زمانی", + "yesterday": "دیروز", + "one-week": "یک هفته", + "two-weeks": "دو هفته", + "one-month": "یک ماه", + "three-months": "سه ماه", + "six-months": "شش ماه", + "one-year": "یک سال", + "sort-by": "مرتب‌سازی بر اساس", + "last-reply-time": "زمان آخرین پاسخ", + "topic-title": "عنوان موضوع", + "topic-votes": "Topic votes", + "number-of-replies": "تعداد پاسخ‌ها", + "number-of-views": "تعداد مشاهده ها", + "topic-start-date": "زمان شروع موضوع", + "username": "نام کاربری", + "category": "دسته بندی", + "descending": "به ترتیب نزولی", + "ascending": "به ترتیب صعودی", + "save-preferences": "ذخیره تنظیمات", + "clear-preferences": "پاک کردن تنظیمات", + "search-preferences-saved": "تنظیمات جستحو ذخیره شد", + "search-preferences-cleared": "تنظیمات جستجو پاک شد", + "show-results-as": "نمایش نتایج به عنوان", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/fa-IR/success.json b/public/language/fa-IR/success.json new file mode 100644 index 0000000000..7d53b42af6 --- /dev/null +++ b/public/language/fa-IR/success.json @@ -0,0 +1,7 @@ +{ + "success": "موفقیت‌آمیز", + "topic-post": "پست شما باموفقیت فرستاده شد.", + "post-queued": "پست شما در صف تایید قرار گرفت. شما اعلانی بعد پذیرفته یا رد شدن آن دریافت خواهید کرد.", + "authentication-successful": "اعتبارسنجی موفق", + "settings-saved": "تنظیمات ذخیره شد." +} \ No newline at end of file diff --git a/public/language/fa-IR/tags.json b/public/language/fa-IR/tags.json new file mode 100644 index 0000000000..b5468b2e92 --- /dev/null +++ b/public/language/fa-IR/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "جُستاری با این برچسب وجود ندارد.", + "tags": "برچسب‌ها", + "enter_tags_here": "برچسب‌ها را اینجا وارد کنید، هر کدام بین %1 و %2 کاراکتر.", + "enter_tags_here_short": "برچسب‌ها را وارد کنید...", + "no_tags": "هنوز برچسبی وجود ندارد.", + "select_tags": "انتخاب برچسب ها" +} \ No newline at end of file diff --git a/public/language/fa-IR/top.json b/public/language/fa-IR/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/fa-IR/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/fa-IR/topic.json b/public/language/fa-IR/topic.json new file mode 100644 index 0000000000..8a6375e5fc --- /dev/null +++ b/public/language/fa-IR/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "جُستار", + "title": "Title", + "no_topics_found": "هیچ موضوعی یافت نشد!", + "no_posts_found": "پستی یافت نشد!", + "post_is_deleted": "این پست پاک شده!", + "topic_is_deleted": "موضوع حذف شده است!", + "profile": "پروفایل", + "posted_by": "ارسال شده توسط %1", + "posted_by_guest": "ارسال شده توسط مهمان", + "chat": "چت", + "notify_me": "از پاسخ‌های تازه در موضوع آگاه شوید", + "quote": "نقل قول", + "reply": "پاسخ", + "replies_to_this_post": "%1 پاسخ ", + "one_reply_to_this_post": "1 پاسخ", + "last_reply_time": "آخرین پاسخ", + "reply-as-topic": "پاسخ به موضوع", + "guest-login-reply": "وارد شوید تا پست بفرستید", + "login-to-view": "🔒 برای مشاهده وارد شوید ", + "edit": "ویرایش", + "delete": "حذف", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "پاک کردن", + "restore": "برگرداندن", + "move": "جابه‌جا کردن", + "change-owner": "تغییر مالک پست", + "fork": "شاخه ساختن", + "link": "پیوند", + "share": "اشتراک‌گذاری", + "tools": "ابزارها", + "locked": "قفل شده است", + "pinned": "سنجاق شده", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "منتقل شده", + "moved-from": "Moved from %1", + "copy-ip": "کپی IP", + "ban-ip": "مسدود کردن IP", + "view-history": "تاریخچه ویرایش", + "locked-by": "قفل شده توسط", + "unlocked-by": "باز شده توسط", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "برای بازگشت به آخرین پست در این موضوع اینجا را کلیک کنید.", + "flag-post": "گزارش این پست", + "flag-user": "گزارش این کاربر", + "already-flagged": "قبلا گزارش شده", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "این موضوع پاک شده است. تنها کاربرانِ با حق مدیریت موضوع می‌توانند آن را ببینند.", + "following_topic.message": "از این پس اگر کسی در این موضوع پست بگذارد، شما آگاه خواهید شد.", + "not_following_topic.message": "شما این موضوع را تو فهرست موضوعات خوانده نشده می‌بینید، اما وقتی پست جدیدی ارسال می‌شود آگاه‌سازی دریافت نمی‌کنید.", + "ignoring_topic.message": "شما دیگر نمی خواهید این موضوع را در فهرست عنوان های خوانده نشده ببینید. به شما اطلاع داده خواهد شد زمانی که به پست شما کسی رای بدهد.", + "login_to_subscribe": "برای دنبال کردن این موضوع، لطفا ثبت نام کنید و یا با نام کاربری خود وارد شوید", + "markAsUnreadForAll.success": "موضوع برای همگان نخوانده در نظر گرفته شد.", + "mark_unread": "علامت بزن به عنوان خوانده نشده", + "mark_unread.success": "موضوع را به عنوان خوانده نشده علامت بزن", + "watch": "پیگیری", + "unwatch": "عدم پیگیری", + "watch.title": "از پاسخ‌های تازه به این موضوع آگاه شوید.", + "unwatch.title": "توقف پیگیری این موضوع", + "share_this_post": "به اشتراک‌گذاری این موضوع", + "watching": "درحال پیگیری", + "not-watching": "درحال پیگیری نیستید", + "ignoring": "نادیده گرفتن", + "watching.description": "به من اطلاع بده برای پاسخ های جدید.
نشان بده موضوع های خوانده نشده را.", + "not-watching.description": "به من پس از ارسال هر پاسخی جدیدی اطلاع نده.
موضوع به صورت خوانده نشده قرار بگیرد ولی نادیده گرفته نشود.", + "ignoring.description": "به من پس از ارسال هر پاسخی جدیدی اطلاع نده.
دیگر موضوع را به صورت خوانده نشده نشان نده.", + "thread_tools.title": "ابزارهای موضوع", + "thread_tools.markAsUnreadForAll": "برای همه کاربران نخوانده شده علامت بزن", + "thread_tools.pin": "سنجاق زدن موضوع", + "thread_tools.unpin": "برداشتن سنجاق موضوع", + "thread_tools.lock": "قفل کردن موضوع", + "thread_tools.unlock": "باز کردن موضوع", + "thread_tools.move": "جابجا کردن موضوع", + "thread_tools.move-posts": "انتقال پست ها", + "thread_tools.move_all": "جابجایی همه", + "thread_tools.change_owner": "تغییر مالک پست", + "thread_tools.select_category": "انتخاب دسته", + "thread_tools.fork": "شاخه ساختن از موضوع", + "thread_tools.delete": "پاک کردن موضوع", + "thread_tools.delete-posts": "حذف پست ها", + "thread_tools.delete_confirm": "آیا مطمئنید می خواهید این موضوع را حذف کنید؟", + "thread_tools.restore": "برگرداندن موضوع", + "thread_tools.restore_confirm": "آیا مطمئنید که می خواهید این موضوع را بازگردانی کنید؟", + "thread_tools.purge": "پاک کردن موضوع", + "thread_tools.purge_confirm": "آیا مطمئنید که میمید این موضوع را پاکسازی کنید؟", + "thread_tools.merge_topics": "ادغام موضوع ها", + "thread_tools.merge": "ادغام", + "topic_move_success": "موضوع به \"%1\" منتقل خواهد شد. برای جلوگیری از انتقال کلیک کنید.", + "topic_move_multiple_success": "موضوع ها به \"%1\" منتقل خواهد شد. برای جلوگیری از انتقال کلیک کنید.", + "topic_move_all_success": "تمام موضوع ها به \"%1\" منتقل خواهند شد. برای جلوگیری از انتقال کلیک کنید.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "پست ها منتقل خواهند شد. برای جلوگیری از انتقال کلیک کنید.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "آیا از پاک کردن این پست اطمینان دارید؟", + "post_restore_confirm": "آیا از بازگردانی این پست اطمینان دارید؟", + "post_purge_confirm": "آیا از پاک کردن این پست اطمینان دارید؟", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "بارگذاری دسته‌ها", + "confirm_move": "جابه‌جا کردن", + "confirm_fork": "شاخه ساختن", + "bookmark": "نشانک", + "bookmarks": "نشانک‌ها", + "bookmarks.has_no_bookmarks": "شما هیچ پستی را نشانک نکردید", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "بارگذاری پست‌های بیش‌تر", + "move_topic": "جابه‌جایی موضوع", + "move_topics": "انتقال موضوع", + "move_post": "جابه‌جایی موضوع", + "post_moved": "پست جابه‌جا شد!", + "fork_topic": "شاخه ساختن از موضوع", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "پست‌هایی را که می‌خواهید به موضوع تازه ببرید، انتخاب کنید", + "fork_no_pids": "هیچ پستی انتخاب نشده!", + "no-posts-selected": "هیچ پستی انتخاب نشده!", + "x-posts-selected": "%1 پست انتخاب شده", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 پست انتخاب شده", + "fork_success": "موضوع با موفقیت منشعب شد! برای رفتن به موضوع انشعابی اینجا را کلیک کنید.", + "delete_posts_instruction": "با کلیک بر روی پست شما می خواهید به حذف/پاکسازی", + "merge_topics_instruction": "موضوع های مورد نظر برای ادغام را انتخاب کنید یا آن ها را جستجو کنید", + "merge-topic-list-title": "لیست موضوع هایی که با هم ادغام می شوند", + "merge-options": "تنظیمات ادغام", + "merge-select-main-topic": "موضوع اصلی را انتخاب کنید", + "merge-new-title-for-topic": "عنوان جدید برای موضوع", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "عنوان موضوعتان را اینجا بنویسید...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "انصراف", + "composer.submit": "ارسال", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "پاسخ به %1", + "composer.new_topic": "موضوع تازه", + "composer.editing": "Editing", + "composer.uploading": "بارگذاری...", + "composer.thumb_url_label": "چسباندن نشانی چهرک یک موضوع", + "composer.thumb_title": "افزودن یک چهرک به این موضوع", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "یا بارگذاری یک پرونده", + "composer.thumb_remove": "پاک کردن جعبه‌ها", + "composer.drag_and_drop_images": "تصویرها را به اینجا بکشید و رها کنید", + "more_users_and_guests": "%1 کاربر() و %2 مهمان()", + "more_users": "%1 کاربر()", + "more_guests": "%1 مهمان()", + "users_and_others": "%1 و %2 دیگر", + "sort_by": "مرتب‌سازی بر اساس", + "oldest_to_newest": "قدیمی‌ترین به جدید‌ترین", + "newest_to_oldest": "جدید‌ترین به قدیمی‌ترین", + "most_votes": "بیشترین رای ها", + "most_posts": "بیشترین پست", + "most_views": "Most Views", + "stale.title": "آیا مایلید به جای آن یک موضوع جدید ایجاد کنید؟", + "stale.warning": "موضوعی که شما در حال پاسخگویی به آن هستید قدیمی می باشد. آیا میلید به جای آن یک موضوع جدید ایجاد کنید و در آن به این موضوع ارجاع دهید؟", + "stale.create": "ایجاد یک موضوع جدید", + "stale.reply_anyway": "در هر صورت می خواهم به این موضوع پاسخ دهم", + "link_back": "پاسخ: [%1](%2)", + "diffs.title": "تاریخچه ویرایش پست", + "diffs.description": "این پست %1 نسخه دارد. بر روی یکی از نسخه ها کلیک کنید تا محتوای پست در آن زمان را ببینید.", + "diffs.no-revisions-description": "این پست %1 نسخه دارد.", + "diffs.current-revision": "نسخه فعلی", + "diffs.original-revision": "نسخه اصلی", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "پست با موفقیت به نسخه قبلی برگردانده شد", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "1% بعد", + "timeago_earlier": "%1 قبل", + "first-post": "اولین پست", + "last-post": "آخرین پست", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/fa-IR/unread.json b/public/language/fa-IR/unread.json new file mode 100644 index 0000000000..6dbfa319f1 --- /dev/null +++ b/public/language/fa-IR/unread.json @@ -0,0 +1,15 @@ +{ + "title": "نخوانده‌ها", + "no_unread_topics": "جستار خوانده نشده‌ای وجود ندارد.", + "load_more": "بارگذاری بیش‌تر", + "mark_as_read": "خوانده شده بگیر", + "selected": "برگزیده", + "all": "همه", + "all_categories": "تمام دسته ها", + "topics_marked_as_read.success": "همه موضوع ها خوانده شدند", + "all-topics": "همه موضوع ها", + "new-topics": "موضوع های جدید", + "watched-topics": "موضوع های پیگیری شده", + "unreplied-topics": "موضوع های بدون پاسخ", + "multiple-categories-selected": "انتخاب چندگانه" +} \ No newline at end of file diff --git a/public/language/fa-IR/uploads.json b/public/language/fa-IR/uploads.json new file mode 100644 index 0000000000..9b22c754b9 --- /dev/null +++ b/public/language/fa-IR/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "در حال بارگذاری فایل...", + "select-file-to-upload": "فایل مورد نظر را برای بارگذاری انتخاب کنید!", + "upload-success": "فایل با موفقیت بارگذاری شد!", + "maximum-file-size": "حداکثر %1 کیلوبایت", + "no-uploads-found": "No uploads found", + "public-uploads-info": "آپلود ها عمومی هستند، همه کاربران می توانند آن ها را ببینند.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/fa-IR/user.json b/public/language/fa-IR/user.json new file mode 100644 index 0000000000..8e7f8c15aa --- /dev/null +++ b/public/language/fa-IR/user.json @@ -0,0 +1,199 @@ +{ + "banned": "اخراج شده", + "muted": "Muted", + "offline": "آفلاین", + "deleted": "حذف شده", + "username": "نام کاربری", + "joindate": "تاریخ عضویت", + "postcount": "تعداد پست‌ها", + "email": "رایانامه", + "confirm_email": "تأیید ایمیل", + "account_info": "اطلاعات شناسه کاربری", + "admin_actions_label": "اقدامات مدیریتی", + "ban_account": "اخراج کاربر", + "ban_account_confirm": "از مسدود کردن این کاربر اطمینان دارید؟", + "unban_account": "آزاد کردن حساب کاربری", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "حذف حساب کاربری", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "حساب کاربری پاک شد", + "account-content-deleted": "Account content deleted", + "fullname": "نام و نام‌ خانوادگی", + "website": "تارنما", + "location": "محل سکونت", + "age": "سن", + "joined": "عضو شده", + "lastonline": "آخرین حضور", + "profile": "پروفایل", + "profile_views": "بازدیدهای نمایه", + "reputation": "اعتبار", + "bookmarks": "نشانک‌ها", + "watched_categories": "دسته بندی های پیگیری شده", + "change_all": "تغییر همه", + "watched": "موضوع های پیگیری شده", + "ignored": "نادیده گرفته شده", + "default-category-watch-state": "حالت پیشفرض مشاهده دسته بندی", + "followers": "دنبال‌کننده‌ها", + "following": "دنبال‌شونده‌ها", + "blocks": "کاربران مسدود شده", + "block_toggle": "مسدود کردن", + "block_user": "مسدود کردن کاربر", + "unblock_user": "رفع مسدودی کاربر", + "aboutme": "درباره ی من", + "signature": "امضا", + "birthday": "روز تولد", + "chat": "چت", + "chat_with": "ادامه چت با %1", + "new_chat_with": "شروع چت جدید با %1", + "flag-profile": "گزارش پروفایل", + "follow": "دنبال کن", + "unfollow": "دنبال نکن", + "more": "بیشتر", + "profile_update_success": "پروفایل باموفقیت به روز شده است!", + "change_picture": "تغییر تصویر", + "change_username": "تغییر نام کاربری", + "change_email": "تغییر ایمیل", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "ویرایش", + "edit-profile": "ویرایش پروفایل", + "default_picture": "آیکون پیش فرض", + "uploaded_picture": "تصویر بارشده", + "upload_new_picture": "بارگذاری تصویر تازه", + "upload_new_picture_from_url": "بارگذاری تصویر جدید از نشانی وب", + "current_password": "کلمه عبور کنونی", + "change_password": "تغیر کلمه عبور", + "change_password_error": "کلمه عبور نامعتبر!", + "change_password_error_wrong_current": "این کلمه عبورٔ شما نادرست است.", + "change_password_error_match": "کلمه عبور‌ها باید یکسان باشند.", + "change_password_error_privileges": "شما اجازه تغییر این کلمه عبور را ندارید.", + "change_password_success": "کلمه عبور‌تان تازه شد.", + "confirm_password": "تکرار کلمه عبور", + "password": "گذرواژه", + "username_taken_workaround": "نام کاربری درخواستی شما در حال حاضر گرفته شده است، بنابراین ما آن را کمی تغییر داده‌ایم. شما هم‌اکنون با نام %1 شناخته می‌شوید.", + "password_same_as_username": "کلمه ی عبور شما با نام کاربری شما یکسان می باشند ، لطفا کلمه ی عبور دیگری را انتخاب کنید", + "password_same_as_email": "کلمه ی عبور شما با ایمیل شما یکسان است، لطفا کلمه عبور دیگری را انتخاب کنید.", + "weak_password": "گذرواژه ضعیف", + "upload_picture": "بارگذاری تصویر", + "upload_a_picture": "یک تصویر بارگذاری کنید", + "remove_uploaded_picture": "پاک کردن عکس بارگذاری شده", + "upload_cover_picture": "بارگذاری عکس کاور", + "remove_cover_picture_confirm": "آیا شما مطمئن هستید که می خواهید عکس کاور را حذف کنید؟", + "crop_picture": "برش عکس", + "upload_cropped_picture": "برش و بارگذاری", + "avatar-background-colour": "Avatar background colour", + "settings": "تنظیمات", + "show_email": "نمایش ایمیل‌های من", + "show_fullname": "نمایش نام کامل من (نام و نام خانوادگی)", + "restrict_chats": "فقط از کاربرانی که دنبال می کنم پیام خصوصی دریافت کنم", + "digest_label": "مشترک شدن در چکیده", + "digest_description": "مشترک شدن برای دریافت جدیدترین‌های این انجمن (موضوع ها و آکاه‌سازی‌های تازه) با ایمیل روی یک برنامه زمان‌بندی", + "digest_off": "خاموش", + "digest_daily": "روزانه", + "digest_weekly": "هفتگی", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "ماهانه", + "has_no_follower": "این کاربر هیچ دنبال‌کننده‌ای ندارد :(", + "follows_no_one": "این کاربر هیچ کسی را دنبال نمی‌کند :(", + "has_no_posts": "این کاربر تا به حال هیچ چیزی ارسال نکرده است.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "این کاربر تا به حال هیچ موضوعی ارسال نکرده است", + "has_no_watched_topics": "این کاربر تا به حال هیچ موضوعی را پیگیری نکرده است", + "has_no_ignored_topics": "این کاربر هیچ موضوعی را نادیده نگرفته است", + "has_no_upvoted_posts": "این کاربر به هیچ پستی امتیاز نداده است.", + "has_no_downvoted_posts": "این کاربر به هیچ پستی رای منفی نداده است.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "شما هیچ کاربر مسدود شده ای ندارید.", + "email_hidden": "ایمیل پنهان شده", + "hidden": "پنهان", + "paginate_description": "صفحه بندی و نمایش موضوع ها و پست‌ها به جای نمایش بر اساس اسکرول موس", + "topics_per_page": "شمار موضوع ها در هر برگه", + "posts_per_page": "شمار پست‌ها در هر برگه", + "max_items_per_page": "حداکثر %1", + "acp_language": "زبان پنل ادمین", + "notifications": "آگاه‌سازی‌ها", + "upvote-notif-freq": "تنظیمات اعلان امتیاز مثبت", + "upvote-notif-freq.all": "همه امتیاز های مثبت", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "هر ده امتیاز مثبت", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "هر 10، 10، 1000 ...", + "upvote-notif-freq.disabled": "غیر فعال شده", + "browsing": "تنظیمات مرور", + "open_links_in_new_tab": "پیوندهای به بیرون را در برگ جدید باز کن", + "enable_topic_searching": "فعال کردن جستجوی درون موضوعی", + "topic_search_help": "اگر فعال باشد، \"جستجوی درون موضوعی\" جایگزین قابلیت جستجوی پیشفرض مرورگر خواهد شد و این امکان را خواهید داشت که بجای جستجوی آنچه که در صفحه نمایش می بینید، در سرتاسر موضوع جستجو کنید", + "update_url_with_post_index": "بروزرسانی آدرس پست در مرورگر هنگام گشت و گذار در موضوعات", + "scroll_to_my_post": "پس از ارسال پست، اولین پست جدید نشان بده", + "follow_topics_you_reply_to": "پیگیری موضوع هایی که به آن ها جواب دادید", + "follow_topics_you_create": "پیگیری موضوع هایی که ایجاد کردید", + "grouptitle": "عنوان گروه", + "group-order-help": "گروهی را انتخاب کرده و با استفاده از پیکان ها ترتیب عنوان ها را جابه‌جا کنید", + "no-group-title": "عنوان گروهی نیست", + "select-skin": "انتخاب یک پوسته", + "select-homepage": "انتخاب صفحه اصلی", + "homepage": "صفحه اصلی", + "homepage_description": "یک صفحه را به عنوان خانه انتخاب کنید یا با انتخاب \"هیچکدام\" صفحه‌ی پیش فرض برای شما انتخاب می‌شود. ", + "custom_route": "مسیر صفحه‌ی اختصاصی", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "متصل شده به", + "sso.not-associated": "اتصال حساب به", + "sso.dissociate": "لغو اتصال", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "آیا مطمئن هستید میخواهید اتصال حساب %1 به حساب انجمن را لغو کنید؟", + "info.latest-flags": "آخرین نشانه گذاری‌ها", + "info.no-flags": "پست گزارش شده ای یافت نشد", + "info.ban-history": "تاریخچه مسدودیت های اخیر", + "info.no-ban-history": "این کاربر هرگز مسدود نشده است", + "info.banned-until": "مسدود شده تا %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "مسدود شده به طور دائم", + "info.banned-reason-label": "دلیل", + "info.banned-no-reason": "هیچ دلیلی ارایه نشد.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "تاریخچه تعویض نام کاربری", + "info.email-history": "تاریخچه تعویض ایمیل", + "info.moderation-note": "یادداشت مدیر", + "info.moderation-note.success": "یادداشت مدیر ذخیره شد", + "info.moderation-note.add": "افزودن یادداشت", + "sessions.description": "این صفحه به شما امکان می دهد تا همه Session های فعال حساب خود در انجمن را ببینید و در صورت نیاز آن ها را باطل کنید. شما می توانید Session فعلی خود را با خروج از حساب خود باطل کنید.", + "consent.title": "Your Rights & Consent", + "consent.lead": "این انجمن اطلاعات شخصی شما را جمع‌آوری و پردازش می‌کند", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/fa-IR/users.json b/public/language/fa-IR/users.json new file mode 100644 index 0000000000..165b25b0a9 --- /dev/null +++ b/public/language/fa-IR/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "آخرین کاربران", + "top_posters": "برترین فرستنده‌ها", + "most_reputation": "بیشترین اعتبار", + "most_flags": "بیشترین پرچم‌ها", + "search": "جستجو", + "enter_username": "یک نام کاربری برای جستجو وارد کنید", + "search-user-for-chat": "Search a user to start chat", + "load_more": "بارگذاری بیش‌تر", + "users-found-search-took": "%1 کاربر(ها) یافت شد! جستجو %2 ثانیه زمان گرفت.", + "filter-by": "فیلتر با", + "online-only": "فقط آنلاین", + "invite": "دعوت", + "prompt-email": "ایمیل‌ها:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "رایانامه دعوت‌نامه برای %1 ارسال شد", + "user_list": "فهرست کاربران", + "recent_topics": "موضوع‌های اخیر", + "popular_topics": "موضوع‌های پربازدید", + "unread_topics": "موضوع‌های خوانده نشده", + "categories": "دسته‌بندی‌ها", + "tags": "برچسب‌ها", + "no-users-found": "کاربری پیدا نشد!" +} \ No newline at end of file diff --git a/public/language/fi/_DO_NOT_EDIT_FILES_HERE.md b/public/language/fi/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/fi/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/fi/admin/admin.json b/public/language/fi/admin/admin.json new file mode 100644 index 0000000000..73e4153e13 --- /dev/null +++ b/public/language/fi/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Haluatko varmasti uudelleenrakentaa ja uudelleenkäynnistää NodeBB:n?", + "alert.confirm-restart": "Haluatko varmasti uudelleenkäynnistää NodeBB:n?", + + "acp-title": "%1 | NodeBB admin hallintapaneeli", + "settings-header-contents": "Sisältö", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/fi/admin/advanced/cache.json b/public/language/fi/admin/advanced/cache.json new file mode 100644 index 0000000000..54a656b600 --- /dev/null +++ b/public/language/fi/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Viestivälimuisti", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Täynnä", + "post-cache-size": "Viestivälimuistin koko", + "items-in-cache": "Asioita välimuistissa" +} \ No newline at end of file diff --git a/public/language/fi/admin/advanced/database.json b/public/language/fi/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/fi/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/fi/admin/advanced/errors.json b/public/language/fi/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/fi/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/fi/admin/advanced/events.json b/public/language/fi/admin/advanced/events.json new file mode 100644 index 0000000000..472c08b024 --- /dev/null +++ b/public/language/fi/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Tapahtumat", + "no-events": "Ei tapahtumia.", + "control-panel": "Tapahtumien hallintapaneeli", + "delete-events": "Poista tapahtumia", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Suodattimet", + "filters-apply": "Lisää suodattimia", + "filter-type": "Tapahtuman tyyppi", + "filter-start": "Aloituspäivä", + "filter-end": "Lopetuspäivä", + "filter-perPage": "Per Sivu" +} \ No newline at end of file diff --git a/public/language/fi/admin/advanced/logs.json b/public/language/fi/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/fi/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/fi/admin/appearance/customise.json b/public/language/fi/admin/appearance/customise.json new file mode 100644 index 0000000000..799cf87c66 --- /dev/null +++ b/public/language/fi/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Kustomoi CSS/LESS -tiedostoja", + "custom-css.description": "Syötä haluamasi CSS/LESS -määritykset tähän. Nämä määritykset menevät kaikkien muiden tyylimäärittelyjen edelle.", + "custom-css.enable": "Salli CSS/LESS -kustomoinnit", + + "custom-js": "Kustomoi Javascriptiä", + "custom-js.description": "Syötä Javascript-sisältö tähän. Se suoritetaan, kun sivu on latautunut valmiiksi.", + "custom-js.enable": "Salli Javascript-kustomoinnit", + + "custom-header": "Kustomoi ylätunnistetta", + "custom-header.description": "Syötä ylätunnisteeseen eli <head>-osioon tulevat HTML-määrittelyt tähän (esim. meta-tagit ym.). Myös script-tagien käyttö on mahdollista, mutta ei suositeltavaa, sillä Kustomoi Javascriptiä-välilehti on saatavilla.", + "custom-header.enable": "Salli ylätunnisteen kustomointi", + + "custom-css.livereload": "Salli sivun päivitys livenä", + "custom-css.livereload.description": "Salli tämä, jos haluat mahdollistaa kaikkien tililläsi olevien laitteiden istuntojen päivittymisen, kun tallennat tekemäsi muutokset." +} \ No newline at end of file diff --git a/public/language/fi/admin/appearance/skins.json b/public/language/fi/admin/appearance/skins.json new file mode 100644 index 0000000000..1802516149 --- /dev/null +++ b/public/language/fi/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Ladataan ulkoasuja...", + "homepage": "Kotisivu", + "select-skin": "Valitse ulkoasu", + "current-skin": "Nykyinen ulkoasu", + "skin-updated": "Ulkoasu päivitetty", + "applied-success": "Ulkoasu \"%1\" valittiin onnistuneesti.", + "revert-success": "Ulkoasu palautettu oletusväreille." +} \ No newline at end of file diff --git a/public/language/fi/admin/appearance/themes.json b/public/language/fi/admin/appearance/themes.json new file mode 100644 index 0000000000..925d048d74 --- /dev/null +++ b/public/language/fi/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Haetaan asennettuja teemoja...", + "homepage": "Kotisivu", + "select-theme": "Valitse teema", + "current-theme": "Nykyinen teema", + "no-themes": "Asennettuja teemoja ei löytynyt.", + "revert-confirm": "Oletko varma, että haluat palauttaa foorumisi teeman NodeBB-oletusteemaan?", + "theme-changed": "Teema vaihdettu.", + "revert-success": "Palautit foorumisi onnistuneesti NodeBB-oletusteemalle.", + "restart-to-activate": "Sinun on uudelleenrakennettava ja -käynnistettävä NodeBB, jotta teeman aktivointi saadaan suoritettua loppuun." +} \ No newline at end of file diff --git a/public/language/fi/admin/dashboard.json b/public/language/fi/admin/dashboard.json new file mode 100644 index 0000000000..2fa46e3a70 --- /dev/null +++ b/public/language/fi/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Foorumin tietoliikenne", + "page-views": "Sivulataukset", + "unique-visitors": "Uniikkeja vierailijoita", + "logins": "Sisäänkirjautumiset", + "new-users": "Uudet käyttäjät", + "posts": "Viestit", + "topics": "Aiheet", + "page-views-seven": "Viimeiset 7 päivää", + "page-views-thirty": "Viimeiset 30 päivää", + "page-views-last-day": "Viimeiset 24 tuntia", + "page-views-custom": "Valitse oma aikaväli", + "page-views-custom-start": "Aikavälin alkupiste", + "page-views-custom-end": "Aikavälin loppupiste", + "page-views-custom-help": "Syötä aikaväli, jonka ajalta haluat tarkastella kävijätietoja. Jos päivämääränvalitsinta ei ole saatavilla, oikea päivämäärämuoto on VVVV-KK-PP.", + "page-views-custom-error": "Ole hyvä, ja syötä käypä aikaväli muodossa VVVV-KK-PP.", + + "stats.yesterday": "Eilen", + "stats.today": "Tänään", + "stats.last-week": "Viime viikolla", + "stats.this-week": "Tällä viikolla", + "stats.last-month": "Viime kuussa", + "stats.this-month": "Tässä kuussa", + "stats.all": "Alusta lähtien", + + "updates": "Päivitykset", + "running-version": "Sinulla on käytössäsi NodeBB v %1.", + "keep-updated": "Huolehdi aina, että sinulla on käytössäsi uusin NodeBB-versio tietoturvaparannusten ja bugikorjausten vuoksi.", + "up-to-date": "

Ohjelmistosi on ajan tasalla

", + "upgrade-available": "

Uusi versio (v%1) on julkaistu. Suositellaan NodeBB-version päivitystä.

", + "prerelease-upgrade-available": "

Tämä on vanhentunut NodeBB:n ennakkolevitysversio. Uusi versio (v%1) on julkaistu. Suositellaan NodeBB-version päivitystä.

", + "prerelease-warning": "

Tämä on NodeBB:n ennakkolevitysversio. Versio saattaa sisältää odottamattomia toimimattomuuksia.

", + "fallback-emailer-not-found": "Vara-sähköpostituslisäosaa ei löytynyt!", + "running-in-development": "Foorumi pyörii tällä hetkellä kehittäjäntilassa. Foorumi voi tässä tilassa olla altis tietoturvaongelmille; otathan yhteyttä järjestelmän ylläpitäjään.", + "latest-lookup-failed": "

Uusimman saatavilla olevan NodeBB-version haku ei onnistunut.

", + + "notices": "Huomautukset", + "restart-not-required": "Uudelleenkäynnistystä ei tarvita.", + "restart-required": "Uudelleenkäynnistys tarvitaan.", + "search-plugin-installed": "Haku-lisäosa asennettu.", + "search-plugin-not-installed": "Haku-lisäosaa ei ole asennettu.", + "search-plugin-tooltip": "Asenna haku-lisäosa lisäosasivulta ottaaksesi hakutoiminnon käyttöön.", + + "control-panel": "Järjestelmän hallinta", + "rebuild-and-restart": "Uudelleenrakenna & -käynnistä", + "restart": "Uudelleenkäynnistä", + "restart-warning": "Uudelleenrakentaminen tai -käynnistäminen pudottaa kaikki sivustoon käynnissä olevat yhteydet muutamaksi sekunniksi.", + "restart-disabled": "NodeBB:si udelleenrakennus ja -käynnistys on estetty; vaikuttaa siltä, ettet käytä siihen sopivaa daemonia.", + "maintenance-mode": "Huoltotila", + "maintenance-mode-title": "Valitse tämä pohjustaaksesi huoltotilan NodeBB:llesi.", + "realtime-chart-updates": "Reaaliaikaiset kuvaajien päivitykset", + + "active-users": "Aktiiviset käyttäjät", + "active-users.users": "Käyttäjät", + "active-users.guests": "Vieraat", + "active-users.total": "Kokonaisuudessaan", + "active-users.connections": "Yhteyttä", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Rekisteröitynyttä", + + "user-presence": "Käyttäjien sijainti", + "on-categories": "Kategorialistauksessa", + "reading-posts": "Lukee viestejä", + "browsing-topics": "Selaa aiheita", + "recent": "Viimeisimmät", + "unread": "Lukemattomat", + + "high-presence-topics": "Aiheet, joissa on eniten käyttäjiä paikalla", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Sivulataukset", + "graphs.page-views-registered": "Rekisteröityneiden käyttäjien sivulatausta", + "graphs.page-views-guest": "Vieraskäyttäjien sivulatausta", + "graphs.page-views-bot": "Bottien sivulatausta", + "graphs.unique-visitors": "Uniikkia vierailijaa", + "graphs.registered-users": "Rekisteröitynyttä käyttäjää", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Viimeksi uudelleenkäynnistetty", + "no-users-browsing": "Ei käyttäjiä selaamassa", + + "back-to-dashboard": "Takaisin ohjausnäkymään", + "details.no-users": "Ei liittyneitä käyttäjiä valitulla aikavälillä.", + "details.no-topics": "Ei luotuja aiheita valitulla aikavälillä.", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "Ei sisäänkirjautumisia valitulla aikavälillä.", + "details.logins-static": "NodeBB tallettaa istuntotiedot vain %1 päivän ajaksi, joten tämä kuvaaja näyttää vain viimeisimpänä aktiivisena olleet istunnot.", + "details.logins-login-time": "Sisäänkirjautumisaika" +} diff --git a/public/language/fi/admin/development/info.json b/public/language/fi/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/fi/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/fi/admin/development/logger.json b/public/language/fi/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/fi/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/fi/admin/extend/plugins.json b/public/language/fi/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/fi/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/fi/admin/extend/rewards.json b/public/language/fi/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/fi/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/fi/admin/extend/widgets.json b/public/language/fi/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/fi/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/admins-mods.json b/public/language/fi/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/fi/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/categories.json b/public/language/fi/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/fi/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/digest.json b/public/language/fi/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/fi/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/fi/admin/manage/groups.json b/public/language/fi/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/fi/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/privileges.json b/public/language/fi/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/fi/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/registration.json b/public/language/fi/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/fi/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/tags.json b/public/language/fi/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/fi/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/uploads.json b/public/language/fi/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/fi/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/fi/admin/manage/users.json b/public/language/fi/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/fi/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/fi/admin/menu.json b/public/language/fi/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/fi/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/advanced.json b/public/language/fi/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/fi/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/api.json b/public/language/fi/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/fi/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/chat.json b/public/language/fi/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/fi/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/cookies.json b/public/language/fi/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/fi/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/email.json b/public/language/fi/admin/settings/email.json new file mode 100644 index 0000000000..740998a605 --- /dev/null +++ b/public/language/fi/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Sähköpostin asetukset", + "address": "Sähköpostiosoitteet", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Sähköposti koosteet", + "subscriptions.disable": "Poista sähköpostin koosteet käytöstä", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/fi/admin/settings/general.json b/public/language/fi/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/fi/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/fi/admin/settings/group.json b/public/language/fi/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/fi/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/guest.json b/public/language/fi/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/fi/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/homepage.json b/public/language/fi/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/fi/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/languages.json b/public/language/fi/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/fi/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/navigation.json b/public/language/fi/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/fi/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/fi/admin/settings/notifications.json b/public/language/fi/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/fi/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/pagination.json b/public/language/fi/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/fi/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/post.json b/public/language/fi/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/fi/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/reputation.json b/public/language/fi/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/fi/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/social.json b/public/language/fi/admin/settings/social.json new file mode 100644 index 0000000000..2d6c8e5690 --- /dev/null +++ b/public/language/fi/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Viestin jakaminen", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/sockets.json b/public/language/fi/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/fi/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/sounds.json b/public/language/fi/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/fi/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/tags.json b/public/language/fi/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/fi/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/fi/admin/settings/uploads.json b/public/language/fi/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/fi/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/fi/admin/settings/user.json b/public/language/fi/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/fi/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/fi/admin/settings/web-crawler.json b/public/language/fi/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/fi/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/fi/category.json b/public/language/fi/category.json new file mode 100644 index 0000000000..7c46d01b8e --- /dev/null +++ b/public/language/fi/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategoria", + "subcategories": "Alikategoria", + "new_topic_button": "Uusi aihe", + "guest-login-post": "Kirjaudu sisään voidaksesi kirjoittaa viesti", + "no_topics": "Tässä kategoriassa ei ole yhtään aihetta.
Miksi et aloittaisi uutta?", + "browsing": "selaamassa", + "no_replies": "Kukaan ei ole vastannut", + "no_new_posts": "Ei uusia viestejä", + "watch": "Seuraa", + "ignore": "Sivuuta", + "watching": "Seurataan", + "not-watching": "Älä seuraa", + "ignoring": "Ohita", + "watching.description": "Näytä aiheet lukemattomissa ja viimeisimmissä", + "not-watching.description": "Älä näytä aiheita lukemattomissa, näytä viimeisimmissä", + "ignoring.description": "Älä näytä aiheita lukemattomissa ja viimeisimmissä", + "watching.message": "Seuraat nyt päivityksiä tästä kategoriasta ja kaikista alikategorioista", + "notwatching.message": "Et seuraa päivityksiää tästä kategoriasta tai alikategorioista", + "ignoring.message": "Ohitat kaikki päivitykset tästä kategoriasta ja kaikista alikategorioista", + "watched-categories": "Seuratut kategoriat", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/fi/email.json b/public/language/fi/email.json new file mode 100644 index 0000000000..9147b31642 --- /dev/null +++ b/public/language/fi/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Testisähköposti", + "password-reset-requested": "Salasanan nollaus pyydetty!", + "welcome-to": "%1 - Tervetuloa", + "invite": "Kutsu henkilöltä %1", + "greeting_no_name": "Hei", + "greeting_with_name": "Hei %1", + "email.verify-your-email.subject": "Tarkista sähköpostisi", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Kiitos rekisteröitymisestäsi sivustolle %1!", + "welcome.text2": "Meidän täytyy varmistaa, että omistat sen sähköpostiosoitteen, jolla rekisteröidyit, ennen kuin tilisi voidaan aktivoida kokonaan.", + "welcome.text3": "Ylläpitäjä hyväksyi rekisteröintipyyntösi. Voit nyt kirjautua käyttäjänimelläsi ja salasanallasi.", + "welcome.cta": "Napsauta tästä vahvistaaksesi sähköpostiosoitteesi", + "invitation.text1": "%1 pyysi sinua liittymään %2", + "invitation.text2": "Kutsusi vanhenee %1 päivässä.", + "invitation.cta": "Luo tili täältä", + "reset.text1": "Saimme pyynnön vaihtaa salasanasi, todennäkösesti koska olit unohtanut sen. Jos näin ei ollut käynyt, voit jättää tämän viestin huomiotta.", + "reset.text2": "Jatkaaksesi salasanan nollausta, napsauta seuraavaa linkkiä:", + "reset.cta": "Napsauta tästä vaihtaaksesi salasanasi", + "reset.notify.subject": "Salasana onnistuneesti vaihdettu", + "reset.notify.text1": "Ilmoitamme sinua että %1, salasanasi vaihdettiin onnistuneesti.", + "reset.notify.text2": "Jos et tunnista tätä toimintoa, ilmoita välittömästi ylläpitäjälle.", + "digest.latest_topics": "Viimeisimmät viestiketjut henkilöltä %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Click here to visit %1", + "digest.unsub.info": "Tämä kooste lähetettiin sinulle tilin tilausasetusten perusteella", + "digest.day": "päivä", + "digest.week": "viikko", + "digest.month": "kuukausi", + "digest.subject": "Koosteesi %1", + "digest.title.day": "Päivittäinen koosteesi", + "digest.title.week": "Viikottainen koosteesi", + "digest.title.month": "Kuukausittainen koosteesi", + "notif.chat.subject": "Uusi chatviesti henkilöltä %1", + "notif.chat.cta": "Klikkaa tästä jatkaaksesi keskustelua", + "notif.chat.unsub.info": "Tämä keskustelun ilmoitus on lähetettty viestiasetuksiesi johdosta", + "notif.post.unsub.info": "Tämä ilmoitus viestistä lähetettiin sinulle viestiasetuksistasi johtuen", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "Foorumille", + "notif.cta-new-reply": "Katso viesti", + "notif.cta-new-chat": "Katso keskustelu", + "notif.test.short": "Testi ilmoitus", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Tämä on testi sähköposti, jolla voit testata, että sähköpostin asetukset ovat oikein NodeBB:ssä", + "unsub.cta": "Muuta asetuksia täältä", + "unsubscribe": "Lopeta tilaus", + "unsub.success": "Sinut on poistettu %1 postituslistalta", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Kiitos!" +} \ No newline at end of file diff --git a/public/language/fi/error.json b/public/language/fi/error.json new file mode 100644 index 0000000000..b9b0f83ab9 --- /dev/null +++ b/public/language/fi/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Virheellinen data", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Et taida olla kirjautuneena sisään.", + "account-locked": "Käyttäjätilisi on lukittu väliaikaisesti", + "search-requires-login": "Haku vaatii tunnukset. Kirjaudu sisään tai luo tunnus.", + "goback": "Paina \"Takaisin\"-nappia palataksesi takaisin edelliselle sivulle.", + "invalid-cid": "Virheellinen kategorian ID", + "invalid-tid": "Virheellinen aiheen ID", + "invalid-pid": "Virheellinen viestin ID", + "invalid-uid": "Virheellinen käyttäjän ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Virheellinen käyttäjänimi", + "invalid-email": "Virheellinen sähköpostiosoite", + "invalid-fullname": "Kokonimi on virheellinen", + "invalid-location": "Virheellinen sijainti", + "invalid-birthday": "Virheellinen syntymäpäivä", + "invalid-title": "Virheellinen otsikko", + "invalid-user-data": "Virheellinen käyttäjätieto", + "invalid-password": "Virheellinen salasana", + "invalid-login-credentials": "Virheelliset kirjautumistiedot", + "invalid-username-or-password": "Ole hyvä ja anna sekä käyttäjänimi että salasana", + "invalid-search-term": "Virheellinen hakutermi", + "invalid-url": "Virheellinen URL-osoite", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "Käyttäjänimi varattu", + "email-taken": "Sähköpostiosoite varattu", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Et voi keskustella ennen kuin sähköpostiosoitteesi on vahvistettu, ole hyvä ja paina tästä vahvistaaksesi sen.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Käyttäjänimi on liian lyhyt", + "username-too-long": "Käyttäjänimi on liian pitkä", + "password-too-long": "Salasana on liian pitkä", + "reset-rate-limited": "Liian monta salasanan nollaus pyyntöä (määrärajoitus)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Käyttäjä on estetty", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Anteeksi, mutta sinun täytyy odottaa %1 sekunti(a) ennen sinun ensimmäisen viestin lähettämistä", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Kategoriaa ei ole olemassa", + "no-topic": "Aihetta ei ole olemassa", + "no-post": "Viestiä ei ole olemassa", + "no-group": "Ryhmää ei ole olemassa", + "no-user": "Käyttäjää ei ole olemassa", + "no-teaser": "Teaser does not exist", + "no-flag": "Flag does not exist", + "no-privileges": "Oikeutesi eivät riitä toiminnon suorittamiseen.", + "category-disabled": "Kategoria ei ole käytössä", + "topic-locked": "Aihe lukittu", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Ole hyvä ja syötä pidempi viesti. Sen pitäisi sisältää ainakin %1 merkki(ä).", + "content-too-long": "Ole hyvä ja syötä lyhyempi viesti. Sen voi sisältää vain %1 merkki(ä).", + "title-too-short": "Ole hyä ja syötä pidempi otsikko. Sen pitäisi sisältää anakin %1 merkki(ä).", + "title-too-long": "Ole hyvä ja syötä lyhyempi otsikko. Se voi sisältää vain %1 merkki(ä).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Ole hyvä ja odota tiedostojen lähettämisen valmistumista.", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Et voi estää muita ylläpitäjiä!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Ryhmän nimi on liian lyhyt", + "group-name-too-long": "Group name too long", + "group-already-exists": "Ryhmä on jo olemassa", + "group-name-change-not-allowed": "Et voi vaihtaa ryhmän nimeä", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Tämä viesti on jo poistettu", + "post-already-restored": "Tämä viesti on jo palautettu", + "topic-already-deleted": "Tämä aihe on jo poistettu", + "topic-already-restored": "Tämä aihe on jo palautettu", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "Aiheiden kuvakkeet eivät ole käytössä", + "invalid-file": "Virheellinen tiedosto", + "uploads-are-disabled": "Et voi lähettää tiedostoa", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "Et voi keskustella itsesi kanssa!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Keskustelujärjestelmä on pois käytöstä", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Virheellinen keskusteluviesti", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", + "registration-error": "Virhe rekisteröinnissä", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Käytä sähköpostiosoitettasi kirjautuaksesi sisään", + "wrong-login-type-username": "Käytä tunnusta kirjautuaksesi sisään", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "Käyttäjä ei ole huoneessa", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Ei aiheita valittuna", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/fi/flags.json b/public/language/fi/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/fi/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/fi/global.json b/public/language/fi/global.json new file mode 100644 index 0000000000..f4a9973905 --- /dev/null +++ b/public/language/fi/global.json @@ -0,0 +1,126 @@ +{ + "home": "Etusivu", + "search": "Hae", + "buttons.close": "Sulje", + "403.title": "Pääsy kielletty", + "403.message": "Olet päätynyt sivulle, johon sinulla ei ole tarvittavia oikeuksia.", + "403.login": "Sinun pitäisi kai kirjautua sisään?", + "404.title": "Ei löydy", + "404.message": "Olet päätynyt sivulle, jota ei ole olemassa. Palaa etusivulle.", + "500.title": "Palvelinvirhe", + "500.message": "Oho! Jotain meni pieleen!", + "400.title": "Bad Request.", + "400.message": "Näyttää siltä, että linkki on virheellinen. Tarkista vielä linkin muoton ja yritä uudestaan. Muusa tapauksessa palaa takaisin kotisivulle", + "register": "Rekisteröidy", + "login": "Kirjaudu", + "please_log_in": "Kirjaudu, ole hyvä", + "logout": "Kirjaudu ulos", + "posting_restriction_info": "Kirjoittaminen on tällä hetkellä rajattu vain rekisteröityneille käyttäjille. Napsauta tätä kirjautuaksesi.", + "welcome_back": "Tervetuloa takaisin", + "you_have_successfully_logged_in": "Olet onnistuneesti kirjautunut sisään", + "save_changes": "Tallenna muutokset", + "save": "Tallenna", + "close": "Sulje", + "pagination": "Sivutus", + "pagination.out_of": "%1/%2", + "pagination.enter_index": "Go to post index", + "header.admin": "Ylläpitäjä", + "header.categories": "Kategoriat", + "header.recent": "Viimeisimmät", + "header.unread": "Lukemattomat", + "header.tags": "Tagit", + "header.popular": "Suositut", + "header.top": "Top", + "header.users": "Käyttäjät", + "header.groups": "Ryhmät", + "header.chats": "Keskustelut", + "header.notifications": "Ilmoitukset", + "header.search": "Hae", + "header.profile": "Profiili", + "header.navigation": "Navigation", + "notifications.loading": "Ladataan ilmoituksia", + "chats.loading": "Ladataan keskusteluja", + "motd.welcome": "Tervetuloa NodeBB:hen, tulevaisuuden keskustelualustalle.", + "previouspage": "Edellinen sivu", + "nextpage": "Seuraava sivu", + "alert.success": "Onnistui", + "alert.error": "Virhe", + "alert.banned": "Estetty", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Et seuraa enää %1!", + "alert.follow": "Seuraat nyt %1!", + "users": "Käyttäjät", + "topics": "Aiheet", + "posts": "Viestit", + "x-posts": "%1 posts", + "best": "Paras", + "controversial": "Controversial", + "votes": "Ääniä", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Tykkääjät", + "upvoted": "Tykätyt", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Katsottu", + "posters": "Posters", + "reputation": "Maine", + "lastpost": "Viimeisin viesti", + "firstpost": "Ensimmäinen viesti", + "read_more": "lue lisää", + "more": "Lisää", + "none": "None", + "posted_ago_by_guest": "Vierailija lähettänyt %1", + "posted_ago_by": "posted %1 by %2", + "posted_ago": "lähetetty %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "posted in %1 %2", + "posted_in_ago_by": "posted in %1 %2 by %3", + "user_posted_ago": "%1 lähetti %2", + "guest_posted_ago": "Vieras kirjoitti %1", + "last_edited_by": "Viimeksi muokannut %1", + "norecentposts": "Ei viimeaikaisia viestejä", + "norecenttopics": "Ei viimeaikaisia aiheita", + "recentposts": "Viimeisimmät viestit", + "recentips": "Äskettäin kirjautuneet IP-osoitteet", + "moderator_tools": "Ylläpidon työkalut", + "online": "Online", + "away": "Poissa", + "dnd": "Älä häiritse", + "invisible": "Näkymätön", + "offline": "Offline", + "email": "Sähköposti", + "language": "Kieli", + "guest": "Vieras", + "guests": "Vieraat", + "former_user": "Entinen käyttäjä", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Foorumi päivitetty", + "updated.message": "Tämä foorumi on juuri päivitetty uusimpaan versioon. Paina tästä päivittääksesi sivun.", + "privacy": "Yksityisyys", + "follow": "Seuraa", + "unfollow": "Älä seuraa", + "delete_all": "Poista kaikki", + "map": "Kartta", + "sessions": "Login Sessions", + "ip_address": "IP osoite", + "enter_page_number": "Syötä sivunumero", + "upload_file": "Lähetä tiedosto", + "upload": "Lähetä", + "uploads": "Lähetykset", + "allowed-file-types": "Sallitut tiedostotyypit ovat %1", + "unsaved-changes": "Sinulla on tallentamatotmia muutoksia. Oletko varma, että haluat siirtyä pois näkymästä?", + "reconnecting-message": "Näyttää siltä, että yhteys %1 palveluun katosi. Odotas hetki kun yritän yhdistää.", + "play": "Toista", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Muokattu", + "disabled": "Disabled", + "select": "Valitse", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/fi/groups.json b/public/language/fi/groups.json new file mode 100644 index 0000000000..0346460360 --- /dev/null +++ b/public/language/fi/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Ryhmät", + "view_group": "Tarkaste ryhmää", + "owner": "Ryhmän omistaja", + "new_group": "Luo uusi ryhmä", + "no_groups_found": "Ei ryhmiä", + "pending.accept": "Hyväksy", + "pending.reject": "Hylkää", + "pending.accept_all": "Hyväksy kaikki", + "pending.reject_all": "Hylkää kaikki", + "pending.none": "There are no pending members at this time", + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Tallenna", + "cover-saving": "Tallennetaan", + "details.title": "Ryhmän tiedot", + "details.members": "Jäsenluettelo", + "details.pending": "Odottavat jäsenet", + "details.invited": "Kutsutut jäsenet", + "details.has_no_posts": "Tämän ryhmän jäsenet eivät ole luoneet vielä yhtään viestiä.", + "details.latest_posts": "Uusimmat viestit", + "details.private": "Yksityinen", + "details.disableJoinRequests": "Poista liittymispyynnöt", + "details.disableLeave": "Estä käyttäjiä poistumasta ryhmästä", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Potkaise", + "details.kick_confirm": "Oletko varma, että haluat poistaa käyttäjän ryhmästä?", + "details.add-member": "Lisää jäsen", + "details.owner_options": "Ryhmän ylläpito", + "details.group_name": "Ryhmän nimi", + "details.member_count": "Jäsenmäärä", + "details.creation_date": "Luontipäivämäärä", + "details.description": "Kuvaus", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Merkin esikatselu", + "details.change_icon": "Vaihda kuvake", + "details.change_label_colour": "Vaihda merkin väriä", + "details.change_text_colour": "Vaihda tekstin väriä", + "details.badge_text": "Merkin teksti", + "details.userTitleEnabled": "Näytä merkki", + "details.private_help": "Jos päällä, ryhmään liittyminen vaattii hyväksynnän ryhmän omistajalta.", + "details.hidden": "Piilotettu", + "details.hidden_help": "Jos päällä, niin ryhmä ei ole näkyvissä ryhmälistauksissa ja käyttäjät tulee lisätä ryhmään käsin.", + "details.delete_group": "Poista ryhmä", + "details.private_system_help": "Yksityiset ryhmät on poistettu käytöstä järjestelmän tasolla ja tämä valinta ei tee mitään.", + "event.updated": "Ryhmän tiedot on jo päivitetty", + "event.deleted": "Ryhmä \"%1\" on jo poistettu", + "membership.accept-invitation": "Hyväksy kutsu", + "membership.accept.notification_title": "Olet nyt %1 ryhmän jäsen", + "membership.invitation-pending": "Odottavat kutsut", + "membership.join-group": "Liity ryhmään", + "membership.leave-group": "Poistu ryhmästä", + "membership.leave.notification_title": "%1 on lähtenyt %2 ryhmästä", + "membership.reject": "Hylkää", + "new-group.group_name": "Ryhmän nimi", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Massa kutsu", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/fi/ip-blacklist.json b/public/language/fi/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/fi/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/fi/language.json b/public/language/fi/language.json new file mode 100644 index 0000000000..66b4cd6afb --- /dev/null +++ b/public/language/fi/language.json @@ -0,0 +1,5 @@ +{ + "name": "Finnish", + "code": "fi", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/fi/login.json b/public/language/fi/login.json new file mode 100644 index 0000000000..ac5163d08f --- /dev/null +++ b/public/language/fi/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Käyttäjätunnus / Sähköposti", + "username": "Käyttäjätunnus", + "remember_me": "Muista minut?", + "forgot_password": "Unohditko salasanasi?", + "alternative_logins": "Vaihtoehtoiset kirjautumistavat", + "failed_login_attempt": "Kirjautuminen epäonnistui", + "login_successful": "Olet onnistuneesti kirjautunut sisään!", + "dont_have_account": "Ei käyttäjätunnusta?", + "logged-out-due-to-inactivity": "Sinut on kirjattu ulos hallintapaneelista liian pitkän toimettomuuden takia.", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/fi/modules.json b/public/language/fi/modules.json new file mode 100644 index 0000000000..554a987c0d --- /dev/null +++ b/public/language/fi/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Lähetä", + "chat.no_active": "Sinulla ei ole aktiivisia keskusteluita.", + "chat.user_typing": "%1 kirjoittaa ...", + "chat.user_has_messaged_you": "%1 lähetti sinulle viestin.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Valitse vastaanottaja katsellaksesi keskusteluhistoriaa", + "chat.no-users-in-room": "Ei käyttäjiä tässä huoneessa", + "chat.recent-chats": "Viimeisimmät keskustelut", + "chat.contacts": "Contacts", + "chat.message-history": "Viestihistoria", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop out chat", + "chat.minimize": "Minimize", + "chat.maximize": "Suurenna", + "chat.seven_days": "7 päivää", + "chat.thirty_days": "30 päivää", + "chat.three_months": "3 kuukautta", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 said in %2:", + "composer.user_said": "%1 sanoi:", + "composer.discard": "Oletko varma, että haluat hylätä viestin?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/fi/notifications.json b/public/language/fi/notifications.json new file mode 100644 index 0000000000..ac012ccf62 --- /dev/null +++ b/public/language/fi/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Ilmoitukset", + "no_notifs": "Sinulla ei ole uusia ilmoituksia", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Palaa takaisin %1", + "outgoing_link": "Ulkopuolinen linkki", + "outgoing_link_message": "Olet nyt poistumassa %1", + "continue_to": "Jatka %1", + "return_to": "Palaa %1", + "new_notification": "Sinulle ei ole uusia ilmoituksia", + "you_have_unread_notifications": "Sinulla on lukemattomia ilmoituksia.", + "all": "Kaikki", + "topics": "Aiheet", + "replies": "Vastaukset", + "chat": "Keskustelut", + "group-chat": "Group Chats", + "follows": "Seuratut", + "upvote": "Tykkäykset", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Uusi viesti käyttäjältä %1", + "upvoted_your_post_in": "%1 käyttäjä tykkäsi viestistäsi aiheessa %2 ", + "upvoted_your_post_in_dual": "%1 ja %2 tykkäsivät viestistäsi aiheesssa %3", + "upvoted_your_post_in_multiple": "%1 ja %2 muuta tykkäsivät viestistäsi aiheessa %3", + "moved_your_post": "%1 on siirtänyt viestisi %2", + "moved_your_topic": "%1 on siirtänyt %2 alueelle", + "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 on vastannut viestiin: %2", + "user_posted_to_dual": "%1 ja %2 ovat vastanneet viestiin: %3", + "user_posted_to_multiple": "%1 ja %2 muuta ovat vastanneet viestiin: %3 ", + "user_posted_topic": "%1 on kirjoittanut uuden aiheen: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 alkoi seurata sinua.", + "user_started_following_you_dual": "%1 ja %2 alkoivat seurata sinua", + "user_started_following_you_multiple": "%1 ja %2 muuta alkoivat seurata sinua", + "new_register": "%1 lähetti rekisteröitymispyynnön", + "new_register_multiple": "%1 rekisteröintipyyntöä odottaa katselmointia", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Viesti odottaa katselmointia", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Sähköpostiosoite vahvistettu", + "email-confirmed-message": "Kiitos sähköpostiosoitteesi vahvistamisesta. Käyttäjätilisi on nyt täysin aktivoitu.", + "email-confirm-error-message": "Ongelma sähköpostiosoitteen vahvistamisessa. Ehkäpä koodi oli virheellinen tai vanhentunut.", + "email-confirm-sent": "Vahvistussähköposti lähetetty.", + "none": "Ei mitään", + "notification_only": "Vain ilmoitukset", + "email_only": "Vain sähköposti", + "notification_and_email": "Ilmoitukset & Sähköposti", + "notificationType_upvote": "Kun joku tykkää viestistäsi", + "notificationType_new-topic": "Kun joku seuraa viestejäsi aiheessa", + "notificationType_new-reply": "Kun uusi vastaus on lähetetty aiheeseen, jota seuraat", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "Kun joku alkaa seurata sinua", + "notificationType_new-chat": "Kun saat viestin keskusteluun", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "Kun saat kutsun ryhmään", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "Kun joku pyytää lupaa liittyä ryhmään, jonka omistaja olet", + "notificationType_new-register": "Kun joku lisätään rekisteröintijonoon", + "notificationType_post-queue": "Kun uusi viesti tulee jonoon", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/fi/pages.json b/public/language/fi/pages.json new file mode 100644 index 0000000000..52c4ac8cf3 --- /dev/null +++ b/public/language/fi/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Etusivu", + "unread": "Lukemattomat aiheet", + "popular-day": "Suositut aiheet tänään", + "popular-week": "Suositut aiheet tällä viikolla", + "popular-month": "Suositut aiheet tässä kuussa", + "popular-alltime": "Suositut aiheet koko ajalta", + "recent": "Viimeisimmät aiheet", + "top-day": "Eniten tykätyt aiheet tänään", + "top-week": "Eniten tykätyt aiheet tällä viikolla", + "top-month": "Eniten tykätyt aiheet tässä kuussa", + "top-alltime": "Eniten tykätyt aiheet", + "moderator-tools": "Ylläpidon työkalut", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Paikalla olevat käyttäjät", + "users/latest": "Viimeisimmat käyttäjät", + "users/sort-posts": "Käyttäjät joilla eniten viestejä", + "users/sort-reputation": "Käyttäjät joilla paras maine", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "Käyttäjähaku", + "notifications": "Ilmoitukset", + "tags": "Tunnisteet", + "tag": "Topics tagged under "%1"", + "register": "Luo käyttäjät", + "registration-complete": "Rekisteröinti valmis", + "login": "Kirjaudu käyttäjällesi", + "reset": "Nollaa tunnuksesi salasana", + "categories": "Kategoriat", + "groups": "Ryhmät", + "group": "%1 ryhmä", + "chats": "Keskustelut", + "chat": "Keskustelee %1 kanssa", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Muokkaa \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Käyttäjätilin tiedot", + "account/following": "Ihmiset, jota %1 seuraa", + "account/followers": "Ihmiset, jotka seuraavat %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "%1 viimeisimmät viestit", + "account/topics": "%1 luomat aiheet", + "account/groups": "%1 ryhmät", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Käyttäjän asetukset", + "account/watched": "%1 seuraamat aiheet", + "account/ignored": "%1 sivuuttamat aiheet", + "account/upvoted": "%1 tykkäämät viestit", + "account/downvoted": "Posts downvoted by %1", + "account/best": "%1 tekemät parhaat viestit", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "%1 lähetykset", + "account/sessions": "Login Sessions", + "confirm": "Sähköposti varmistettu", + "maintenance.text": "%1 sivustoa huolletaan parhaillaan. Tarkista sivusto hetken kuluttua uudestaan.", + "maintenance.messageIntro": "Lisäksi ylläpitäjä on jättänyt seuraavan viestin:", + "throttled.text": "%1 sivusto on tällähetkellä alhaalla johtuen liiasta kuormituksesta. Tule takaisin myöhemmin." +} \ No newline at end of file diff --git a/public/language/fi/post-queue.json b/public/language/fi/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/fi/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/fi/recent.json b/public/language/fi/recent.json new file mode 100644 index 0000000000..d7a3dfb5ce --- /dev/null +++ b/public/language/fi/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Viimeisimmät", + "day": "Päivä", + "week": "Viikko", + "month": "Kuukausi", + "year": "Vuosi", + "alltime": "Alusta lähtien", + "no_recent_topics": "Ei viimeisimpiä aiheita.", + "no_popular_topics": "Ei päivityksiä suosituimmissa aiheissa", + "there-is-a-new-topic": "Uusi aihe.", + "there-is-a-new-topic-and-a-new-post": "Uusi aihe ja uusi viesti.", + "there-is-a-new-topic-and-new-posts": "Uusi aihe ja %1 uutta viestiä.", + "there-are-new-topics": "%1 uutta aihetta.", + "there-are-new-topics-and-a-new-post": "%1 uutta aihetta ja uusi viesti.", + "there-are-new-topics-and-new-posts": "%1 uutta aihetta ja %2 uutta viestiä.", + "there-is-a-new-post": "Ei uusia viestejä", + "there-are-new-posts": "%1 uutta viestiä.", + "click-here-to-reload": "Päivitä napsauttamalla tätä." +} \ No newline at end of file diff --git a/public/language/fi/register.json b/public/language/fi/register.json new file mode 100644 index 0000000000..fb25e15981 --- /dev/null +++ b/public/language/fi/register.json @@ -0,0 +1,32 @@ +{ + "register": "Rekisteröidy", + "cancel_registration": "Peruuta rekisteröinti", + "help.email": "Oletuksena sähköpostiosoitettasi ei näytetä muille.", + "help.username_restrictions": "Yksilöllisen käyttäjätunnuksen pitää olla %1-%2 merkkiä pitkä. Toiset voivat mainita sinut @username.", + "help.minimum_password_length": "Salasanasi pitää olla vähintään %1 merkin mittainen.", + "email_address": "Sähköpostiosoite", + "email_address_placeholder": "Syötä sähköpostiosoitteesi", + "username": "Käyttäjätunnus", + "username_placeholder": "Syötä käyttäjätunnuksesi", + "password": "Salasana", + "password_placeholder": "Syötä salasanasi", + "confirm_password": "Vahvista salasanasi", + "confirm_password_placeholder": "Vahvista salasanasi", + "register_now_button": "Rekisteröidy nyt", + "alternative_registration": "Vaihtoehtoiset rekisteröitymistavat", + "terms_of_use": "Käyttöehdot", + "agree_to_terms_of_use": "Hyväksyn käyttöehdot", + "terms_of_use_error": "Sinun täytyy hyväksyä sopimusehdot", + "registration-added-to-queue": "Rekisteröintisi on lisätty listalle odottamaan hyväksyntää. Saat ilmoituksen sähköpostiisi, kun ylläpitäjä on hyväksynyt rekisteröitymisesin.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Annan hyväksyntäni henkilökohtaisten tietojen keräämiseen ja prosessointiin tälle verkkosivulle.", + "gdpr_agree_email": "Haluan vastaanottaa viestikoosteita ja ilmoituksia tältä verkkosivulta", + "gdpr_consent_denied": "Sinun täytyy antaa suostumus sivustolle, jotta se voi kerätä ja tallentaa tietosi ja lähettää sinulle tarvittaessa sähköpostia.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/fi/reset_password.json b/public/language/fi/reset_password.json new file mode 100644 index 0000000000..391fdc5700 --- /dev/null +++ b/public/language/fi/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Palauta salasana", + "update_password": "Päivitä salasana", + "password_changed.title": "Salasana muutettu", + "password_changed.message": "

Salasanasi on palautettu onnistuneesti, ole hyvä ja kirjaudu uudestaan.", + "wrong_reset_code.title": "Väärä palautuskoodi", + "wrong_reset_code.message": "Annettu palautuskoodi oli väärä. Ole hyvä ja yritä uudelleen tai pyydä uutta palautuskoodia.", + "new_password": "Uusi salasana", + "repeat_password": "Vahvista salasana", + "changing_password": "Changing Password", + "enter_email": "Syötä sähköpostiosoitteesi, niin me lähetämme sinulle sähköpostilla ohjeet käyttäjätilisi palauttamiseksi.", + "enter_email_address": "Syötä sähköpostiosoite", + "password_reset_sent": "Salasanan nollaus viesti on lähetetty annettuun osoitteeseen, jos osoite löytyy järjestelmän kannasta. Huomaa, että on mahdollista lähettää vain yksi palautusviesti minuutissa.", + "invalid_email": "Virheellinen sähköpostiosoite / Sähköpostiosoitetta ei ole olemassa!", + "password_too_short": "Salasana on liian lyhyt, käytä pidempää salasanaa.", + "passwords_do_not_match": "Salasana ja sen vahvistus eivät täsmää.", + "password_expired": "Salasanasi on vanhentunut. Luo uusi salasana." +} \ No newline at end of file diff --git a/public/language/fi/search.json b/public/language/fi/search.json new file mode 100644 index 0000000000..63c9c31768 --- /dev/null +++ b/public/language/fi/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 tulosta löytyi hakusanalla \"%2\" (%3 sekunnissa).", + "no-matches": "Ei hakuosumia", + "advanced-search": "Laajennettu haku", + "in": "In", + "titles": "Otsikot", + "titles-posts": "Otsikot ja Viestit", + "match-words": "Match words", + "all": "Kaikki", + "any": "Mikä tahansa", + "posted-by": "Kirjoittanut", + "in-categories": "Kategorioissa", + "search-child-categories": "Hae alikategorioista", + "has-tags": "Sisältää tagit", + "reply-count": "Vastausten määrä", + "at-least": "Vähintään", + "at-most": "Enintään", + "relevance": "Relevanssi", + "post-time": "Julkaisuaika", + "votes": "Ääniä", + "newer-than": "Uudemmat kuin", + "older-than": "Vanhemmat kuin", + "any-date": "Mikä tahansa päivä", + "yesterday": "Eilen", + "one-week": "Yksi viikko", + "two-weeks": "Kaksi viikkoa", + "one-month": "Yksi kuukausi", + "three-months": "Kolme kuukautta", + "six-months": "Kuusi kuukautta", + "one-year": "Yksi vuosi", + "sort-by": "Lajitteluperuste", + "last-reply-time": "Vastattu viimeksi", + "topic-title": "Aiheen otsikko", + "topic-votes": "Aiheiden äänet", + "number-of-replies": "Vastausten määrä", + "number-of-views": "Katselukertojen määrä", + "topic-start-date": "Aiheen aloituspäivä", + "username": "Käyttäjänimi", + "category": "Kategoria", + "descending": "Laskevassa järjestyksessä", + "ascending": "Nousevassa järjestyksessä", + "save-preferences": "Tallenna asetukset", + "clear-preferences": "Tyhjennä asetukset", + "search-preferences-saved": "Haun asetukset tallennettu", + "search-preferences-cleared": "Haun asetukset nollattu", + "show-results-as": "Näytä tulokset", + "see-more-results": "Näytä lisää tuloksia (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/fi/success.json b/public/language/fi/success.json new file mode 100644 index 0000000000..02864d93ff --- /dev/null +++ b/public/language/fi/success.json @@ -0,0 +1,7 @@ +{ + "success": "Onnistui", + "topic-post": "Viestin lähettäminen onnistui.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Tunnistautuminen onnistui", + "settings-saved": "Asetukset tallennettu!" +} \ No newline at end of file diff --git a/public/language/fi/tags.json b/public/language/fi/tags.json new file mode 100644 index 0000000000..c6e17ae86f --- /dev/null +++ b/public/language/fi/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Ei viimeisimpiä aiheita tällä tagilla.", + "tags": "Tagit", + "enter_tags_here": "Syötä tagit tähän merkkien %1 ja %2 väliin.", + "enter_tags_here_short": "Syötä tagit...", + "no_tags": "Ei vielä yhtään tagia.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/fi/top.json b/public/language/fi/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/fi/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/fi/topic.json b/public/language/fi/topic.json new file mode 100644 index 0000000000..4d20b476bc --- /dev/null +++ b/public/language/fi/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Aihe", + "title": "Title", + "no_topics_found": "Aiheita ei löytynyt!", + "no_posts_found": "Viestejä ei löytynyt!", + "post_is_deleted": "Tämä viesti poistettiin!", + "topic_is_deleted": "Tämä aihe on poistettu!", + "profile": "Profiili", + "posted_by": "%1 kirjoitti", + "posted_by_guest": "Vieras kirjoitti", + "chat": "Keskustele", + "notify_me": "Ilmoita, kun tähän keskusteluun tulee uusia viestejä", + "quote": "Lainaa", + "reply": "Vastaa", + "replies_to_this_post": "%1 vastauksia", + "one_reply_to_this_post": "1 vastaus", + "last_reply_time": "Viimeisin vastaus", + "reply-as-topic": "Vastaa aiheeseen", + "guest-login-reply": "Kirjaudu sisään voidaksesi vastata", + "login-to-view": "Kirjaudu sisään", + "edit": "Muokkaa", + "delete": "Poista", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Poista pysyvästi", + "restore": "Palauta", + "move": "Siirrä", + "change-owner": "Vaihda omistaja", + "fork": "Haaroita", + "link": "Linkitä", + "share": "Jaa", + "tools": "Työkalut", + "locked": "Lukittu", + "pinned": "Kiinnitetty", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Siirretty", + "moved-from": "Moved from %1", + "copy-ip": "Kopioi IP", + "ban-ip": "Ban IP", + "view-history": "Muokkaa historiaa", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Klikkaa tästä palataksesi viimeisimpään luettuun viestiin tässä aiheessa", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Tämä aihe on poistettu. Vain käyttäjät, joilla on aiheen hallintaoikeudet, voivat nähdä sen.", + "following_topic.message": "Saat nyt ilmoituksen, kun joku kirjoittaa tähän aiheeseen.", + "not_following_topic.message": "Et näe tätä aihetta lukemattomissa aiheissa, mutta saat ilmoituksen kun joku lähettää viestin tähän aiheeseen.", + "ignoring_topic.message": "Et enää näe tätä aihetta lukemattomat aiheet listauksessa. Saat ilmoituksen, kun joku mainitsee sinut tai viestistäsi tykätään.", + "login_to_subscribe": "Ole hyvä ja rekisteröidy tai kirjaudu sisään tilataksesi tämän aiheen.", + "markAsUnreadForAll.success": "Aihe merkitty lukemattomaksi kaikille.", + "mark_unread": "Merkitse lukemattomaksi", + "mark_unread.success": "Aihe on merkitty lukemattomaksi", + "watch": "Seuraa", + "unwatch": "Älä seuraa", + "watch.title": "Ilmoita, kun tähän keskusteluun tulee uusia viestejä", + "unwatch.title": "Lopeta tämän aiheen seuraaminen", + "share_this_post": "Jaa tämä viesti", + "watching": "Seurataan", + "not-watching": "Ei seurannassa", + "ignoring": "Sivuutettu", + "watching.description": "Ilmoita minulle uusista vastauksista.
Näytä aiheet lukemattomissa.", + "not-watching.description": "Älä ilmoita minulle uusista vastauksista.
Näytä aihe lukemattomissa jos kategoriaa ei ole sivuutettu.", + "ignoring.description": "Älä ilmoita minulle uusista vastauksista.", + "thread_tools.title": "Aiheen työkalut", + "thread_tools.markAsUnreadForAll": "Merkitse lukemattomaksi kaikille", + "thread_tools.pin": "Kiinnitä aihe", + "thread_tools.unpin": "Poista aiheen kiinnitys", + "thread_tools.lock": "Lukitse aihe", + "thread_tools.unlock": "Poista aiheen lukitus", + "thread_tools.move": "Siirrä aihe", + "thread_tools.move-posts": "Siirrä viestit", + "thread_tools.move_all": "Siirrä kaikki", + "thread_tools.change_owner": "Vaihda omistaja", + "thread_tools.select_category": "Valitse kategoria", + "thread_tools.fork": "Haaroita aihe", + "thread_tools.delete": "Poista aihe", + "thread_tools.delete-posts": "Poista viestit", + "thread_tools.delete_confirm": "Haluatko varmasti poistaa tämän aiheen?", + "thread_tools.restore": "Palauta aihe", + "thread_tools.restore_confirm": "Haluatko varmasti palauttaa tämän aiheen?", + "thread_tools.purge": "Poista aihe pysyvästi", + "thread_tools.purge_confirm": "Oletko varma, että haluat poistaa pysyvästi tämän aiheen?", + "thread_tools.merge_topics": "Sulauta aiheet", + "thread_tools.merge": "Sulauta", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Haluatko varmasti poistaa tämän viestin?", + "post_restore_confirm": "Haluatko varmasti palauttaa tämän viestin?", + "post_purge_confirm": "Oletko varma, että haluat poistaa pysyvästi tämän viestin?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Ladataan aihealueita", + "confirm_move": "Siirrä", + "confirm_fork": "Haaroita", + "bookmark": "Kirjanmerkki", + "bookmarks": "Kirjanmerkit", + "bookmarks.has_no_bookmarks": "Et ole kirjanmerkinnyt yhtään viestiä vielä.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Ladataan lisää viestejä", + "move_topic": "Siirrä aihe", + "move_topics": "Siirrä aiheet", + "move_post": "Siirrä viesti", + "post_moved": "Viestit siirretty!", + "fork_topic": "Haaroita keskustelu", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Napsauta viestejä, jotka haluat haaroittaa", + "fork_no_pids": "Ei valittuja viestejä!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 viesti(ä) valittuna", + "fork_success": "Aihe eriytetty onnistuneesti! Klikkaa täältä mennäkseksi uuteen aiheeseen.", + "delete_posts_instruction": "Valitse viestit jotka haluat poistaa (pysyvästi)", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Valitse viestit jotka haluat siirtää toiselle henkilölle", + "composer.title_placeholder": "Syötä aiheesi otsikko tähän...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Hylkää", + "composer.submit": "Lähetä", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Vastaus viestiin %1", + "composer.new_topic": "Uusi aihe", + "composer.editing": "Editing", + "composer.uploading": "ladataan palvelimelle...", + "composer.thumb_url_label": "Liitä aiheen aiheen kuvakkeen URL", + "composer.thumb_title": "Lisää kuvake tähän aiheeseen", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Tai lataa tiedosto palvelimelle", + "composer.thumb_remove": "Tyhjennä kentät", + "composer.drag_and_drop_images": "Vedä ja pudota kuvat tähän", + "more_users_and_guests": "%1 käyttäjä(ä) ja %2 vieras(sta)", + "more_users": "%1 more user(s)", + "more_guests": "%1 more guest(s)", + "users_and_others": "%1 and %2 others", + "sort_by": "Lajittele", + "oldest_to_newest": "Vanhimmasta uusimpaan", + "newest_to_oldest": "Uusimmasta vanhimpaan", + "most_votes": "Eniten ääniä", + "most_posts": "Eniten viestejä", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "Aihe johon olet vastaamassa on melko vanha. Haluaisitko luoda mieluummin uuden aiheen ja viitata siitä tähän viestissäsi?", + "stale.create": "Luo uusi aihe", + "stale.reply_anyway": "Vastaa kuitenkin tähän aiheeseen", + "link_back": "Vs: [%1] (%2)", + "diffs.title": "Viestin muokkaushistoria", + "diffs.description": "Tästä viestistä on %1 versiota. Klikkaa alempaa viestiä haluttua viestiä, jonka sisällön haluat nähdä.", + "diffs.no-revisions-description": "Tästä viestistä on %1 versiota", + "diffs.current-revision": "nykyinen versio", + "diffs.original-revision": "alkuperäinen versio", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 myöhempi", + "timeago_earlier": "%1 aiempi", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/fi/unread.json b/public/language/fi/unread.json new file mode 100644 index 0000000000..63b6b4b1be --- /dev/null +++ b/public/language/fi/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Lukematon", + "no_unread_topics": "Ei lukemattomia aiheita.", + "load_more": "Lataa lisää", + "mark_as_read": "Merkitse luetuksi", + "selected": "Valitut", + "all": "Kaikki", + "all_categories": "Kaikki kategoriat", + "topics_marked_as_read.success": "Aihe merkitty luetuksi!", + "all-topics": "Kaikki aiheet", + "new-topics": "Uudet aiheet", + "watched-topics": "Seuratut aiheet", + "unreplied-topics": "Vastaamattomat aiheet", + "multiple-categories-selected": "Useita valittuna" +} \ No newline at end of file diff --git a/public/language/fi/uploads.json b/public/language/fi/uploads.json new file mode 100644 index 0000000000..3cbe97b030 --- /dev/null +++ b/public/language/fi/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Lähetetään tiedostoa..", + "select-file-to-upload": "Valitse tiedosto lähetettäväksi!", + "upload-success": "Tiedoston lähetys onnistui", + "maximum-file-size": "Maksimi koko on %1 kt", + "no-uploads-found": "Lähetyksiä ei löytynyt", + "public-uploads-info": "Lähetettyjen tiedostojen näkyvyys on julkinen, kirjautumattomat käyttäjät voivat nähdä ne.", + "private-uploads-info": "Lähetettyjen tiedostojen näkyvyys on rajoitettu, vain kirjautuneet käyttäjät voivat nähdä ne." +} \ No newline at end of file diff --git a/public/language/fi/user.json b/public/language/fi/user.json new file mode 100644 index 0000000000..0d423311a4 --- /dev/null +++ b/public/language/fi/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Bannattu", + "muted": "Muted", + "offline": "Offline", + "deleted": "Poistettu", + "username": "Käyttäjän nimi", + "joindate": "Liittymispäivä", + "postcount": "Viestien määrä", + "email": "Sähköposti", + "confirm_email": "Vahvista sähköpostiosoite", + "account_info": "Tilin tiedot", + "admin_actions_label": "Administrative Actions", + "ban_account": "Bannaa käyttäjätili", + "ban_account_confirm": "Haluatko varmasti bannata käyttäjän?", + "unban_account": "Peru käyttäjätilin banni", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Poista käyttäjätili", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Tili poistettu", + "account-content-deleted": "Account content deleted", + "fullname": "Koko nimi", + "website": "Kotisivu", + "location": "Sijainti", + "age": "Ikä", + "joined": "Liittynyt", + "lastonline": "Viimeksi online", + "profile": "Profiili", + "profile_views": "Profiilia katsottu", + "reputation": "Maine", + "bookmarks": "Kirjanmerkit", + "watched_categories": "Seuratut kategoriat", + "change_all": "Muuta kaikki", + "watched": "Seurattu", + "ignored": "Ohitetut", + "default-category-watch-state": "Default category watch state", + "followers": "Seuraajat", + "following": "Seuratut", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "Tietoja minusta", + "signature": "Allekirjoitus", + "birthday": "Syntymäpäivä", + "chat": "Keskustele", + "chat_with": "Jatka keskustelua %1 kanssa", + "new_chat_with": "Aloita keskutelu %1 kanssa", + "flag-profile": "Flag Profile", + "follow": "Seuraa", + "unfollow": "Älä seuraa", + "more": "Lisää", + "profile_update_success": "Profiili päivitettiin onnistuneesti!", + "change_picture": "Vaihda kuva", + "change_username": "Vaihda käyttäjänimi", + "change_email": "Vaihda sähköpostiosoite", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Muokkaa", + "edit-profile": "Muokkaa profiiliasi", + "default_picture": "Oletus kuvake", + "uploaded_picture": "Ladattu kuva", + "upload_new_picture": "Lataa uusi kuva", + "upload_new_picture_from_url": "Lataa uusi kuva URL-osoitteesta", + "current_password": "Nykyinen salasana", + "change_password": "Vaihda salasana", + "change_password_error": "Virheellinen salasana", + "change_password_error_wrong_current": "Nykyinen salasanasi ei ole oikein!", + "change_password_error_match": "Salasanojen täytyy olla samat!", + "change_password_error_privileges": "Sinulla ei ole oikeuksia vaihtaa tätä salasanaa.", + "change_password_success": "Salasanasi on päivitetty!", + "confirm_password": "Vahvista salasana", + "password": "Salasana", + "username_taken_workaround": "Pyytämäsi käyttäjänimi oli jo varattu, joten muutimme sitä hieman. Käyttäjänimesi on siis nyt %1", + "password_same_as_username": "Salasanasi on sama kuin käytttänimesi. Valitse joku toinen salasana.", + "password_same_as_email": "Salasanasi on sama kuin sähköpostiosoitteesi. Valitse joku toinen salasana.", + "weak_password": "Heikko salasana", + "upload_picture": "Lataa kuva", + "upload_a_picture": "Lataa kuva", + "remove_uploaded_picture": "Poista lisätty kuva", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Rajaa kuvaa", + "upload_cropped_picture": "Rajaa ja lähetä", + "avatar-background-colour": "Avatar background colour", + "settings": "Asetukset", + "show_email": "Näytä sähköpostiosoitteeni", + "show_fullname": "Näytä koko nimeni", + "restrict_chats": "Salli pikaviestit vain seuraamiltani käyttäjiltä", + "digest_label": "Tilaa kooste", + "digest_description": "Tilaa päivitykset sähköpostilla tästä foorumista (uudet ilmoitukset ja aiheet) asetetun ajastuksen mukaan", + "digest_off": "Pois päältä", + "digest_daily": "Päivittäin", + "digest_weekly": "Viikottain", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Kuukausittain", + "has_no_follower": "Kukaan ei seuraa tätä käyttäjää :(", + "follows_no_one": "Tämä käyttäjä ei seuraa ketään :(", + "has_no_posts": "Käyttäjä ei ole vielä lähettänyt viestejä ", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Käyttäjä ei ole lähettänyt viestiä yhteenkään aiheeseen vielä.", + "has_no_watched_topics": "Käyttäjä ei seuraa mitään aihetta vielä.", + "has_no_ignored_topics": "Käyttäjä ei ole merkannut sivuutettavaksi yhtään aihetta.", + "has_no_upvoted_posts": "Käyttäjä ei ole tykännyt yhdestäkään viestistä vielä.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Käyttäjä ei ole estänyt käyttäjiä", + "email_hidden": "Sähköposti piilotettu", + "hidden": "piilotettu", + "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "topics_per_page": "Aihetta per sivu", + "posts_per_page": "Viestiä per sivu", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "Kaikki tykkäykset", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Poistettu käytöstä", + "browsing": "Selataan asetuksia", + "open_links_in_new_tab": "Avaa palvelunulkopuoliset linkit uuteen ikkunaan", + "enable_topic_searching": "Salli aiheen sisäiset haut", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Ryhmän nimi", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Ei ryhmän nimeä", + "select-skin": "Select a Skin", + "select-homepage": "Valitse kotisivu", + "homepage": "Kotisivu", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Syy ", + "info.banned-no-reason": "Syytä ei ole annettu", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Ylläpidon muistiinpano", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Lisää muistiinpano", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Anna suostumus", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/fi/users.json b/public/language/fi/users.json new file mode 100644 index 0000000000..13b22e8808 --- /dev/null +++ b/public/language/fi/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Viimeisimmät käyttäjät", + "top_posters": "Aktiivisimmat viestittelijät", + "most_reputation": "Eniten mainetta", + "most_flags": "Eniten ", + "search": "Hae", + "enter_username": "Syötä käyttäjätunnus hakeaksesi", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Lataa lisää", + "users-found-search-took": "%1 käyttäjä(ä) löytyi! Haku kesti %2 sekuntia.", + "filter-by": "Suodata", + "online-only": "Vain verkossa olevat", + "invite": "Kutsu", + "prompt-email": "Sähköpostit:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Kutsusähköposti on lähetetty %1", + "user_list": "Käyttäjälista", + "recent_topics": "Viimeisimmät aiheet", + "popular_topics": "Suositut aiheet", + "unread_topics": "Lukemattomat aiheet", + "categories": "Kategoriat", + "tags": "Tagit", + "no-users-found": "Ei käyttäjiä!" +} \ No newline at end of file diff --git a/public/language/fr/_DO_NOT_EDIT_FILES_HERE.md b/public/language/fr/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/fr/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/fr/admin/admin.json b/public/language/fr/admin/admin.json new file mode 100644 index 0000000000..df1a269e87 --- /dev/null +++ b/public/language/fr/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Êtes-vous sûr de vouloir régénérer et redémarrer NodeBB ?", + "alert.confirm-restart": "Êtes-vous sûr de vouloir redémarrer NodeBB ?", + + "acp-title": "%1 | Panneau d'administration NodeBB", + "settings-header-contents": "Contenus", + "changes-saved": "Changements sauvegardés !", + "changes-saved-message": "Vos modifications de la configuration NodeBB ont été enregistrées.", + "changes-not-saved": "Changements non sauvegardés !", + "changes-not-saved-message": "NodeBB a rencontré un problème lors de l'enregistrement de vos modifications ! (%1)" +} \ No newline at end of file diff --git a/public/language/fr/admin/advanced/cache.json b/public/language/fr/admin/advanced/cache.json new file mode 100644 index 0000000000..130034e50e --- /dev/null +++ b/public/language/fr/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Cache des messages", + "group-cache": "Cache de groupe", + "local-cache": "Cache Local", + "object-cache": "Cache d'objets", + "percent-full": "Plein à %1%", + "post-cache-size": "Taille du cache des messages", + "items-in-cache": "Objets en cache" +} \ No newline at end of file diff --git a/public/language/fr/admin/advanced/database.json b/public/language/fr/admin/advanced/database.json new file mode 100644 index 0000000000..7dbc132ec2 --- /dev/null +++ b/public/language/fr/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 Mb", + "x-gb": "%1 Gb", + "uptime-seconds": "Disponibilité en secondes", + "uptime-days": "Disponibilité en jours", + + "mongo": "Mongo", + "mongo.version": "Version de MongoDB", + "mongo.storage-engine": "Moteur de stockage", + "mongo.collections": "Collections", + "mongo.objects": "Objets", + "mongo.avg-object-size": "Taille moyenne des objets", + "mongo.data-size": "Taille des données", + "mongo.storage-size": "Taille du stockage", + "mongo.index-size": "Taille de l'index", + "mongo.file-size": "Taille de fichier", + "mongo.resident-memory": "Mémoire résidente", + "mongo.virtual-memory": "Mémoire virtuelle", + "mongo.mapped-memory": "Mémoire mappée", + "mongo.bytes-in": "Données entrées", + "mongo.bytes-out": "Données sorties", + "mongo.num-requests": "Nombre de requêtes", + "mongo.raw-info": "Informations brutes MongoDB", + "mongo.unauthorized": "NodeBB n'a pas pu interroger la base de données MongoDB pour obtenir des statistiques pertinentes. Assurez-vous que l'utilisateur utilisé par NodeBB contient le message "clusterMonitor" pour le rôle "admin" des bases de données.", + + "redis": "Redis", + "redis.version": "Version de Redis", + "redis.keys": "Clés", + "redis.expires": "Expire", + "redis.avg-ttl": "TTL moyen", + "redis.connected-clients": "Clients connectés", + "redis.connected-slaves": "Esclaves connectés", + "redis.blocked-clients": "Clients bloqués", + "redis.used-memory": "Mémoire utilisée", + "redis.memory-frag-ratio": "Ratio de fragmentation de la mémoire", + "redis.total-connections-recieved": "Connexions totales reçues", + "redis.total-commands-processed": "Commandes totales exécutées", + "redis.iops": "Opérations instantanées par seconde", + "redis.iinput": "Entrée instantanée par seconde", + "redis.ioutput": "Sortie instantanée par seconde", + "redis.total-input": "Total des entrées", + "redis.total-output": "Total des sorties", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Informations brutes Redis", + + "postgres": "Postgres", + "postgres.version": "Version PostgreSQL", + "postgres.raw-info": "Info brute Postgres" +} diff --git a/public/language/fr/admin/advanced/errors.json b/public/language/fr/admin/advanced/errors.json new file mode 100644 index 0000000000..c738081ead --- /dev/null +++ b/public/language/fr/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Graphe %1", + "error-events-per-day": "Évènements %1 par jour", + "error.404": "404 Introuvable", + "error.503": "503 Service indisponible", + "manage-error-log": "Gestion des journaux d'erreurs", + "export-error-log": "Exporter les journaux d'erreurs (CSV)", + "clear-error-log": "Effacer les journaux d'erreurs", + "route": "Chemin", + "count": "Nombre", + "no-routes-not-found": "Hourrah ! Aucune erreur 404 !", + "clear404-confirm": "Êtes-vous sûr de vouloir effacer les journaux d'erreurs 404 ?", + "clear404-success": "Erreurs \"404 non trouvé\" effacées" +} \ No newline at end of file diff --git a/public/language/fr/admin/advanced/events.json b/public/language/fr/admin/advanced/events.json new file mode 100644 index 0000000000..3514894bce --- /dev/null +++ b/public/language/fr/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Évènements", + "no-events": "Il n'y a aucun évènement.", + "control-panel": "Panneau de contrôle des évènements", + "delete-events": "Supprimer les évènements", + "confirm-delete-all-events": "Êtes-vous sûr de vouloir supprimer tous les événements enregistrés ?", + "filters": "Filtres", + "filters-apply": "Appliquer", + "filter-type": "Évènements", + "filter-start": "Date de début", + "filter-end": "Date de fin", + "filter-perPage": "Par page" +} \ No newline at end of file diff --git a/public/language/fr/admin/advanced/logs.json b/public/language/fr/admin/advanced/logs.json new file mode 100644 index 0000000000..79b282af06 --- /dev/null +++ b/public/language/fr/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Journaux", + "control-panel": "Panneau de contrôle des journaux", + "reload": "Recharger les journaux", + "clear": "Effacer les journaux", + "clear-success": "Journaux effacés !" +} \ No newline at end of file diff --git a/public/language/fr/admin/appearance/customise.json b/public/language/fr/admin/appearance/customise.json new file mode 100644 index 0000000000..f72d2576b8 --- /dev/null +++ b/public/language/fr/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "CSS/LESS personnalisé", + "custom-css.description": "Entrez vos propres déclarations CSS/LESS ici, qui seront appliquées après tous les autres styles.", + "custom-css.enable": "Activer le CSS/LESS personnalisé", + + "custom-js": "Javascript personnalisé", + "custom-js.description": "Entrez votre Javascript ici. Celui-ci sera exécuté après le chargement complet de la page.", + "custom-js.enable": "Activer le Javascript personnalisé", + + "custom-header": "En-tête personnalisé", + "custom-header.description": "Saisissez votre code HTML personnalisé ici (par exemple, les balises Meta, etc.), qui sera ajouté au section <head> du balisage de votre forum. Les mots clés sont autorisés, mais sont déconseillés, dans la mesure où le Javascript personnalisé est disponible.", + "custom-header.enable": "Activer les en-têtes personnalisés", + + "custom-css.livereload": "Activer le rechargement en direct", + "custom-css.livereload.description": "Activez cette option pour forcer toutes les sessions sur chaque appareil connecté à votre compte à se rafraichir lorsque vous cliquez sur Enregistrer." +} \ No newline at end of file diff --git a/public/language/fr/admin/appearance/skins.json b/public/language/fr/admin/appearance/skins.json new file mode 100644 index 0000000000..38fddfe42e --- /dev/null +++ b/public/language/fr/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Chargements des skins…", + "homepage": "Page d'accueil", + "select-skin": "Sélectionner le skin", + "current-skin": "Skin actuel", + "skin-updated": "Skin mis à jour", + "applied-success": "Le skin %1 a été appliqué avec succès.", + "revert-success": "Couleurs du skin remises par défaut" +} \ No newline at end of file diff --git a/public/language/fr/admin/appearance/themes.json b/public/language/fr/admin/appearance/themes.json new file mode 100644 index 0000000000..4295b24107 --- /dev/null +++ b/public/language/fr/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Vérification des thèmes installés…", + "homepage": "Page d'accueil", + "select-theme": "Sélectionner ce thème", + "current-theme": "Thème actuel", + "no-themes": "Aucun thème installé", + "revert-confirm": "Êtes-vous sûr de vouloir restaurer le thème NodeBB par défaut ?", + "theme-changed": "Thème changé", + "revert-success": "Vous avez restauré avec succès le thème par défaut de NodeBB.", + "restart-to-activate": "Veuillez régénérer et redémarrer votre NodeBB pour activer ce thème." +} \ No newline at end of file diff --git a/public/language/fr/admin/dashboard.json b/public/language/fr/admin/dashboard.json new file mode 100644 index 0000000000..7328224f86 --- /dev/null +++ b/public/language/fr/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Trafic du forum", + "page-views": "Pages vues", + "unique-visitors": "Visiteurs uniques", + "logins": "Connexions", + "new-users": "Nouvel utilisateur", + "posts": "Messages", + "topics": "Sujets", + "page-views-seven": "7 derniers jours", + "page-views-thirty": "30 derniers jours", + "page-views-last-day": "Dernières 24 heures", + "page-views-custom": "Dates personnalisées", + "page-views-custom-start": "Début", + "page-views-custom-end": "Fin", + "page-views-custom-help": "Entrez une plage de date pour les vues que vous souhaitez afficher. Si aucun sélecteur de date n'est disponible, le format de date accepté est YYYY-MM-DD.", + "page-views-custom-error": "Veuillez entrer une plage de date valide dans le format suivant : YYYY-MM-DD", + + "stats.yesterday": "Hier", + "stats.today": "Aujourd'hui", + "stats.last-week": "Semaine dernière", + "stats.this-week": "Cette semaine", + "stats.last-month": "Mois dernier", + "stats.this-month": "Ce mois", + "stats.all": "Tous les temps", + + "updates": "Mises à jour", + "running-version": "NodeBB v%1 est actuellement installé.", + "keep-updated": "Assurez-vous que votre version de NodeBB est à jour pour les derniers patchs de sécurité et correctifs de bugs.", + "up-to-date": "

Votre version est à jour

", + "upgrade-available": "

Une nouvelle version (v%1) est disponible. Veuillez mettre à jour NodeBB.

", + "prerelease-upgrade-available": "

Votre version est dépassée. Une nouvelle version (v%1) est disponible. Veuillez mettre à jour NodeBB.

", + "prerelease-warning": "

Ceci est une version préliminaire de NodeBB. Des bugs inattendus peuvent se produire.

", + "fallback-emailer-not-found": "Email de secours introuvable!", + "running-in-development": "Le forum est en mode développement. Il peut être sujet à certaines vulnérabilités, veuillez contacter votre administrateur système.", + "latest-lookup-failed": "

Erreur de vérification de la dernière version disponible de NodeBB

", + + "notices": "Informations", + "restart-not-required": "Pas de redémarrage nécessaire", + "restart-required": "Redémarrage requis", + "search-plugin-installed": "Le plugin de recherche est installé", + "search-plugin-not-installed": "Le plugin de recherche n'est pas installé", + "search-plugin-tooltip": "Installer un plugin de recherche depuis la page des plugins pour activer la fonctionnalité de recherche", + + "control-panel": "Contrôle du système", + "rebuild-and-restart": "Régénérer & Redémarrer", + "restart": "Redémarrer", + "restart-warning": "Régénérer ou redémarrer NodeBB coupera toutes les connexions existantes pendant quelques secondes. ", + "restart-disabled": "La régénération et le redémarrage de votre forum ont été désactivés car vous ne semblez pas les exécuter à l'aide du serveur approprié.", + "maintenance-mode": "Mode maintenance", + "maintenance-mode-title": "Cliquez ici pour passer NodeBB en mode maintenance", + "realtime-chart-updates": "Mises à jour des graphiques en temps réel", + + "active-users": "Utilisateurs actifs", + "active-users.users": "Utilisateurs", + "active-users.guests": "Invités", + "active-users.total": "Total", + "active-users.connections": "Connexions", + + "guest-registered-users": "Utilisateurs invités vs enregistrés", + "guest": "Invité", + "registered": "Enregistrés", + + "user-presence": "Présence des utilisateurs", + "on-categories": "Sur la liste des catégories", + "reading-posts": "Lit des messages", + "browsing-topics": "Parcourt les sujets", + "recent": "Récents", + "unread": "Non lus", + + "high-presence-topics": "Sujets populaires", + "popular-searches": "Recherches populaires", + + "graphs.page-views": "Pages vues", + "graphs.page-views-registered": "Membres", + "graphs.page-views-guest": "Invités", + "graphs.page-views-bot": "Robots", + "graphs.unique-visitors": "Visiteurs uniques", + "graphs.registered-users": "Utilisateurs enregistrés", + "graphs.guest-users": "Utilisateurs invités", + "last-restarted-by": "Redémarré par", + "no-users-browsing": "Aucun utilisateur connecté", + + "back-to-dashboard": "Retour au Tableau de bord", + "details.no-users": "Aucun utilisateur ne s'est joint dans le délai sélectionné", + "details.no-topics": "Aucun sujet n'a été publié dans la période sélectionnée", + "details.no-searches": "Aucune recherche n'a encore été effectuée", + "details.no-logins": "Aucune connexion n'a été enregistrée dans le délai sélectionné", + "details.logins-static": "NodeBB n'enregistre que les données de session pendant %1 jours, et le tableau ci-dessous n'affichera donc que les dernières sessions actives", + "details.logins-login-time": "Heure de connexion" +} diff --git a/public/language/fr/admin/development/info.json b/public/language/fr/admin/development/info.json new file mode 100644 index 0000000000..2b20a90ad6 --- /dev/null +++ b/public/language/fr/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Vous êtes sur %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 noeuds ont répondu en %2ms !", + "host": "hôte", + "primary": "Tâches principales / Exécuter", + "pid": "pid", + "nodejs": "nodejs", + "online": "en ligne", + "git": "git", + "process-memory": "mémoire de processus", + "system-memory": "mémoire système", + "used-memory-process": "Mémoire utilisée par processus", + "used-memory-os": "Mémoire système utilisée", + "total-memory-os": "Mémoire système totale", + "load": "Charge du système", + "cpu-usage": "Utilisation du processeur", + "uptime": "disponibilité", + + "registered": "Enregistré", + "sockets": "Sockets", + "guests": "Invités", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/fr/admin/development/logger.json b/public/language/fr/admin/development/logger.json new file mode 100644 index 0000000000..003effdb4c --- /dev/null +++ b/public/language/fr/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Réglages de la journalisation", + "description": "En activant les cases, vous recevrez des journaux dans votre terminal. Si vous spécifiez un chemin, les journaux y seront sauvegardés. La journalisation HTTP est utile pour collecter des statistiques sur les personnes qui accèdent à votre forum. En plus de la journalisation des requêtes HTTP, nous pouvons également journaliser les évènements socket.io. La journalisation socket.io, associée au monitoring redis-cli, peut être très utile pour apprendre les rouages de NodeBB.", + "explanation": "Cochez ou décochez simplement les réglages de la journalisation pour l'activer ou la désactiver. Aucun redémarrage n'est nécessaire.", + "enable-http": "Activer la journalisation HTTP", + "enable-socket": "Activer la journalisation des événements socket.io", + "file-path": "Chemin vers les fichiers journaux", + "file-path-placeholder": "/path/to/log/file.log ::: laissez vide pour journaliser vers votre terminal", + + "control-panel": "Panneau de contrôle de la journalisation", + "update-settings": "Mettre à jour la configuration" +} \ No newline at end of file diff --git a/public/language/fr/admin/extend/plugins.json b/public/language/fr/admin/extend/plugins.json new file mode 100644 index 0000000000..9b3bb6ce4a --- /dev/null +++ b/public/language/fr/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Tendance", + "installed": "Installé", + "active": "Actif", + "inactive": "Inactif", + "out-of-date": "Obsolète", + "none-found": "Aucun plugin trouvé", + "none-active": "Aucun plugin actif", + "find-plugins": "Chercher des plugins", + + "plugin-search": "Recherche de plugin", + "plugin-search-placeholder": "Rechercher un plugin…", + "submit-anonymous-usage": "Autoriser l'envoi anonyme des données d'utilisation des plugins", + "reorder-plugins": "Réordonner les plugins", + "order-active": "Trier les plugins actifs", + "dev-interested": "Êtes-vous intéressés par l'écriture de plugins pour NodeBB ?", + "docs-info": "La documentation complète concernant l’écriture de plugin peut être trouvée sur lePortail de Documentation de NodeBB.", + + "order.description": "Certains plugins fonctionnent mieux lorsqu'ils sont initialisés avant/après d'autres plugins.", + "order.explanation": "Les plugins se chargent dans l'ordre spécifié, ici de haut en bas.", + + "plugin-item.themes": "Thèmes", + "plugin-item.deactivate": "Désactiver", + "plugin-item.activate": "Activer", + "plugin-item.install": "Installer", + "plugin-item.uninstall": "Désinstaller", + "plugin-item.settings": "Réglages", + "plugin-item.installed": "Installé", + "plugin-item.latest": "Derniers", + "plugin-item.upgrade": "Mettre à jour", + "plugin-item.more-info": "Pour plus d'informations :", + "plugin-item.unknown": "Inconnu", + "plugin-item.unknown-explanation": "L'état de ce plugin n'a pas pu être déterminé, possiblement à cause d'une erreur de configuration.", + "plugin-item.compatible": "Ce plugin fonctionne sur NodeBB %1", + "plugin-item.not-compatible": "Attention, ce plugin n'est pas compatible avec votre version de NodeBB, assurez-vous qu'il fonctionne avant de l'installer sur votre environnement de production.", + + "alert.enabled": "Plugin activé", + "alert.disabled": "Plugin désactivé", + "alert.upgraded": "Plugin mis à jour", + "alert.installed": "Plugin installé", + "alert.uninstalled": "Plugin désinstallé", + "alert.activate-success": "Veuillez régénérer et redémarrer votre NodeBB pour activer complètement ce plugin", + "alert.deactivate-success": "Plugin désactivé avec succès", + "alert.upgrade-success": "Veuillez régénérer et redémarrer votre NodeBB pour finaliser la mise à jour de ce plugin.", + "alert.install-success": "Plugin installé avec succès, veuillez maintenant l'activer.", + "alert.uninstall-success": "Le plugin a été désactivé et désinstallé avec succès.", + "alert.suggest-error": "

NodeBB n'a pas pu joindre le gestionnaire de paquets, procéder à l'installation de la dernière version ?

Le serveur a répondu (%1) : %2
", + "alert.package-manager-unreachable": "

NodeBB n'a pas pu joindre le gestionnaire de paquets, une mise à jour n'est pas suggérée pour le moment.

", + "alert.incompatible": "

Votre version de NodeBB (v%1) ne peut mettre à jour que vers la version v%2 de ce plugin. Veuillez mettre à jour NodeBB si vous souhaitez installer une version plus récente de ce plugin.

", + "alert.possibly-incompatible": "

Aucune information de compatibilité trouvée

Ce plugin n'a pas spécifié de version pour une installation sur votre version de NodeBB. Aucune compatibilité ne peut être garantie et ce plugin pourrait empêcher NodeBB de démarrer correctement.

Dans l'éventualité où NodeBB ne pourrait pas démarrer proprement :

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

Voulez-vous continuer l'installation de ce plugin ?

", + "alert.reorder": "Réorganiser les plugins", + "alert.reorder-success": "Veuillez régénérer et redémarrer votre NodeBB pour finaliser le processus.", + + "license.title": "Information sur la licence du plugin", + "license.intro": "Le plugin %1 est sous licence %2. Veuillez lire et comprendre les termes de la licence avant d’activer ce plugin.", + "license.cta": "Voulez-vous poursuivre en activant ce plugin ?" +} diff --git a/public/language/fr/admin/extend/rewards.json b/public/language/fr/admin/extend/rewards.json new file mode 100644 index 0000000000..9fb342ce60 --- /dev/null +++ b/public/language/fr/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Récompenses", + "condition-if-users": "Si la propriété de l'utilisateur", + "condition-is": "Est :", + "condition-then": "Alors :", + "max-claims": "Nombre de fois que la récompense peut être obtenue", + "zero-infinite": "Entrez 0 pour infini", + "delete": "Supprimer", + "enable": "Activer", + "disable": "Désactiver", + + "alert.delete-success": "Récompense supprimée", + "alert.no-inputs-found": "Récompense invalide - aucune entrée trouvée !", + "alert.save-success": "Récompenses sauvegardées" +} \ No newline at end of file diff --git a/public/language/fr/admin/extend/widgets.json b/public/language/fr/admin/extend/widgets.json new file mode 100644 index 0000000000..ebdff25296 --- /dev/null +++ b/public/language/fr/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Widgets disponibles", + "explanation": "Sélectionnez un widget depuis le menu puis glissez-déposez le dans une zone template du widget à gauche.", + "none-installed": "Aucun widget trouvé ! Activez le plugin Widget Essentials dans le panneau de configuration des plugins.", + "clone-from": "Cloner le widget", + "containers.available": "Conteneurs disponibles", + "containers.explanation": "Glissez-déposez sur n'importe quel widget actif", + "containers.none": "Aucun", + "container.well": "Well", + "container.jumbotron": "Jombotron", + "container.panel": "Panneau", + "container.panel-header": "En-tête de panneau", + "container.panel-body": "Corps de panneau", + "container.alert": "Alerte", + + "alert.confirm-delete": "Êtes-vous sûr de vouloir supprimer ce widget ?", + "alert.updated": "Widgets mis à jour", + "alert.update-success": "Widgets mis à jour avec succès", + "alert.clone-success": "Widget cloné avec succès", + + "error.select-clone": "Veuillez sélectionner une page à cloner", + + "title": "Titre", + "title.placeholder": "Titre (indiqué uniquement sur certains blocs)", + "container": "Bloc", + "container.placeholder": "Glissez et déposez un bloc ou entrez HTML ici.", + "show-to-groups": "Visible pour les groupes", + "hide-from-groups": "Masquer pour les groupes", + "hide-on-mobile": "Masquer sur mobile" +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/admins-mods.json b/public/language/fr/admin/manage/admins-mods.json new file mode 100644 index 0000000000..9c0a5a22d9 --- /dev/null +++ b/public/language/fr/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrateurs", + "global-moderators": "Modérateurs Globaux", + "moderators": "Modérateurs", + "no-global-moderators": "Aucun Modérateur Global ", + "no-sub-categories": "Aucunes sous-catégories", + "subcategories": "%1 Sous-catégories", + "no-moderators": "Aucun Modérateur", + "add-administrator": "Ajouter un Administrateur", + "add-global-moderator": "Ajouter un Modérateur Global", + "add-moderator": "Ajouter un Modérateur" +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/categories.json b/public/language/fr/admin/manage/categories.json new file mode 100644 index 0000000000..39f68b7f6d --- /dev/null +++ b/public/language/fr/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Paramètres de la catégorie", + "privileges": "Privilèges", + + "name": "Nom de la catégorie", + "description": "Description de la catégorie", + "bg-color": "Couleur d'arrière plan", + "text-color": "Couleur du texte", + "bg-image-size": "Taille de l'image d'arrière plan", + "custom-class": "Classe personnalisée", + "num-recent-replies": "# de réponses récentes", + "ext-link": "Lien externe", + "subcategories-per-page": "Sous-catégories par page", + "is-section": "Traiter cette catégorie comme une section", + "post-queue": "File d'attente", + "tag-whitelist": "Liste blanche de mots clés", + "upload-image": "Envoyer une image", + "delete-image": "Enlever", + "category-image": "Image de la catégorie", + "parent-category": "Catégorie parente", + "optional-parent-category": "Catégorie parente (optionnel)", + "top-level": "Vers le haut", + "parent-category-none": "(Aucun)", + "copy-parent": "Copier Parent", + "copy-settings": "Copier les paramètres de", + "optional-clone-settings": "Copier les paramètres de la catégorie (optionnel)", + "clone-children": "Copier les catégories et les paramètres", + "purge": "Vider la catégorie", + + "enable": "Activer", + "disable": "Désactiver", + "edit": "Éditer", + "analytics": "Statistiques", + "view-category": "Voir la catégorie", + "set-order": "Définir l'ordre", + "set-order-help": "Configuration des catégories. Vous pouvez déplacer vos catégories dans l'ordre que vous le souhaitez. La commande minimum est de 1, ce qui place la catégorie au sommet.", + + "select-category": "Sélectionner une catégorie", + "set-parent-category": "Définissez une catégorie parente", + + "privileges.description": "Vous pouvez configurer les privilèges d'accès pour des sections du site. Les privilèges peuvent être accordés par utilisateur ou par groupe. Sélectionnez les droits d'accès dans le menu déroulant ci-dessous.", + "privileges.category-selector": "Configuration des privilèges pour", + "privileges.warning": "Note: Les paramètres de privilège prennent effet instantanément . Il n'est pas nécessaire de sauvegarder la catégorie après avoir ajusté ces paramètres.", + "privileges.section-viewing": "Afficher les Privilèges", + "privileges.section-posting": "Privilège de posting", + "privileges.section-moderation": "Privilèges de modération", + "privileges.section-other": "Autres", + "privileges.section-user": "Utilisateur", + "privileges.search-user": "Ajouter un utilisateur", + "privileges.no-users": "Aucun privilège spécifique aux utilisateurs dans cette catégorie.", + "privileges.section-group": "Groupe", + "privileges.group-private": "Ce groupe est privé", + "privileges.inheritance-exception": "Ce groupe n'hérite pas des privilèges du groupe d'utilisateurs enregistrés", + "privileges.banned-user-inheritance": "Les utilisateurs bannis héritent des privilèges du groupe d'utilisateurs bannis", + "privileges.search-group": "Ajouter un groupe", + "privileges.copy-to-children": "Copier aux enfants", + "privileges.copy-from-category": "Copier depuis une catégorie", + "privileges.copy-privileges-to-all-categories": "Copier dans toutes les catégories", + "privileges.copy-group-privileges-to-children": "Copiez les privilèges de ce groupe de cette catégorie.", + "privileges.copy-group-privileges-to-all-categories": "Copiez les privilèges de ce groupe dans toutes les catégories.", + "privileges.copy-group-privileges-from": "Copiez les privilèges de ce groupe à partir d'une autre catégorie.", + "privileges.inherit": "Si le groupe utilisateurs enregistrés bénéficie d'un privilège supplémentaire, tous les autres groupes recevront un privilège implicite, même s'ils ne sont pas explicitement définis. Ce privilège implicite vous est montré car tous les utilisateurs font partie du groupe utilisateurs enregistrés ainsi, les privilèges accordés aux autres groupes ne doivent pas nécessairement être explicitement accordés.", + "privileges.copy-success": "Privilèges copiés !", + + "analytics.back": "Revenir à la liste des catégories", + "analytics.title": "Analytique pour la catégorie \"%1\"", + "analytics.pageviews-hourly": "Image 1 – Pages vues par heure pour cette catégorie", + "analytics.pageviews-daily": "Image 2 – Pages vues par jour pour cette catégorie", + "analytics.topics-daily": "Image 3 – Sujets créés par jour dans catégorie", + "analytics.posts-daily": "Image 4 – Messages par jours postés dans cette catégorie", + + "alert.created": "Créée", + "alert.create-success": "Catégorie créée avec succès !", + "alert.none-active": "Vous n'avez aucune catégorie active.", + "alert.create": "Créer une catégorie", + "alert.confirm-purge": "

Voulez-vous vraiment purger cette catégorie \"%1\"?

Attention!Tous les sujets et posts dans cette catégorie vont être supprimés

Purger une catégorie va enlever tous les sujets et les posts, et supprimer la catégorie de la base de données. Si vous voulez seulement enlevez une catégorietemporairement, il faut plutôt \"désactiver\" la catégorie.", + "alert.purge-success": "Catégorie purgée !", + "alert.copy-success": "Paramètres copiés !", + "alert.set-parent-category": "Définir une catégorie parent", + "alert.updated": "Catégories mises à jour", + "alert.updated-success": "L' ID de la catégorie %1 a été mis à jour avec succès", + "alert.upload-image": "Uploader une image de catégorie", + "alert.find-user": "Trouver un utilisateur", + "alert.user-search": "Chercher un utilisateur ici...", + "alert.find-group": "Trouver un groupe", + "alert.group-search": "Chercher un groupe ici...", + "alert.not-enough-whitelisted-tags": "Vous devez ajouter plus de mot-clés dans votre liste blanche ! Le nombre minimal requis est inférieur à ceux de votre liste blanche", + "collapse-all": "Tout réduire", + "expand-all": "Tout développer", + "disable-on-create": "Désactiver lors de la création", + "no-matches": "Aucune correspondance trouvée" +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/digest.json b/public/language/fr/admin/manage/digest.json new file mode 100644 index 0000000000..2512debb1f --- /dev/null +++ b/public/language/fr/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Une liste des statistiques des lettres d'activités envoyées est affichée ci-dessous.", + "disclaimer": "Veuillez noter que l'envoi par email n'est pas garantie en raison de la nature de la technologie de votre serveur de messagerie. De nombreuses variables déterminent si un email envoyé au serveur destinataire arrive dans la boîte de réception de l'utilisateur, y compris la réputation du serveur, les adresses IP figurant sur la liste noire et si les DKIM / SPF / DMARC sont configurés.", + "disclaimer-continued": "Un envoi réussie signifie que le message a été envoyé avec succès par NodeBB et acquitté par le serveur destinataire. Cela ne signifie pas que l'e-mail a atterri dans la boîte de réception. Pour de meilleurs résultats, nous vous recommandons d'utiliser un service de messagerie tiers, tel que SendGrid.", + + "user": "Utilisateur", + "subscription": "Type d'abonnement", + "last-delivery": "Dernier envoi réussi", + "default": "Valeur par défaut", + "default-help": "La valeur par défaut signifie que l'utilisateur n'a pas explicitement modifié ses paramètres pour les lettres d'activités, qui sont : "%1"", + "resend": "Renvoi", + "resend-all-confirm": "Voulez-vous vraiment exécuter manuellement cette envoi ?", + "resent-single": "Lettre d'activité envoyée", + "resent-day": "Lettre d'activités quotidienne envoyée", + "resent-week": "Lettre d'activité hebdomadaire envoyée", + "resent-biweek": "Lettre d'activité envoyée deux fois par semaine", + "resent-month": "Lettre d'activité mensuel envoyé", + "null": "Jamais", + "manual-run": "Lancer manuellement l'envoi:", + + "no-delivery-data": "Aucune donnée d'envoi trouvée" +} diff --git a/public/language/fr/admin/manage/groups.json b/public/language/fr/admin/manage/groups.json new file mode 100644 index 0000000000..be41ad2d82 --- /dev/null +++ b/public/language/fr/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Nom du groupe", + "badge": "Badge", + "properties": "Propriétées", + "description": "Description du groupe", + "member-count": "Nombre de membres", + "system": "Système", + "hidden": "Caché", + "private": "Privé", + "edit": "Éditer", + "delete": "Supprimer", + "privileges": "Privilèges", + "download-csv": "CSV", + "search-placeholder": "Rechercher", + "create": "Créer un groupe", + "description-placeholder": "Une courte description de votre groupe", + "create-button": "Créer", + + "alerts.create-failure": "Oh-Oh

Une erreur s'est produite lors de la création de votre groupe. Veuillez réessayer ultérieurement !

", + "alerts.confirm-delete": "Êtes-vous sûr de vouloir supprimer ce groupe ?", + + "edit.name": "Nom", + "edit.description": "Description", + "edit.user-title": "Titre des membres", + "edit.icon": "Icône du groupe", + "edit.label-color": "Couleur du groupe", + "edit.text-color": "Couleur du groupe", + "edit.show-badge": "Afficher le badge", + "edit.private-details": "Si activé, rejoindre les groupes nécessitera l'approbation de l'un de leurs propriétaires.", + "edit.private-override": "Attention : Les groupes privés sont désactivés au niveau du système, ce qui annule cette option.", + "edit.disable-join": "Désactiver les demandes d'adhésion", + "edit.disable-leave": "Interdire aux utilisateurs de quitter le groupe.", + "edit.hidden": "Masqué", + "edit.hidden-details": "Si activé, ce groupe sera masqué de la liste des groupes et les utilisateurs devront être invités manuellement.", + "edit.add-user": "Ajouter l'utilisateur au groupe", + "edit.add-user-search": "Rechercher des utilisateurs", + "edit.members": "Liste des membres", + "control-panel": "Panneau de contrôle des groupes", + "revert": "Retour", + + "edit.no-users-found": "Aucun utilisateur trouvé", + "edit.confirm-remove-user": "Êtes-vous sûr de vouloir retirer cet utilisateur ?", + "edit.save-success": "Changements sauvegardés !" +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/privileges.json b/public/language/fr/admin/manage/privileges.json new file mode 100644 index 0000000000..a7b3eea13a --- /dev/null +++ b/public/language/fr/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Privilèges de groupe", + "user-privileges": "Privilèges d'utilisateur", + "edit-privileges": "Éditer les privilèges", + "select-clear-all": "Tout Sélectionner / Tout effacer", + "chat": "Chat", + "upload-images": "Images envoyées", + "upload-files": "Fichiers envoyés", + "signature": "Signature", + "ban": "Bannir", + "mute": "Muet", + "invite": "Inviter", + "search-content": "Rechercher un contenu", + "search-users": "Rechercher des utilisateurs", + "search-tags": "Rechercher les tags", + "view-users": "Afficher les Utilisateurs", + "view-tags": "Afficher les Tags", + "view-groups": "Afficher les Groupes", + "allow-local-login": "Identifiant local", + "allow-group-creation": "Groupe Créer", + "view-users-info": "Afficher les Utilisateurs", + "find-category": "Rechercher une catégorie", + "access-category": "Droits des catégories", + "access-topics": "Droits des sujets", + "create-topics": "Créer des sujets", + "reply-to-topics": "Répondre aux sujets", + "schedule-topics": "Planifier des sujets", + "tag-topics": "Tag des sujets", + "edit-posts": "Modifier les messages", + "view-edit-history": "Historique des modifications", + "delete-posts": "Supprimer les messages", + "view_deleted": "Voir les messages supprimés", + "upvote-posts": "Messages positifs", + "downvote-posts": "Messages négatifs", + "delete-topics": "Supprimer les sujets", + "purge": "Purger", + "moderate": "Modérer", + "admin-dashboard": "Tableau de bord", + "admin-categories": "Catégories", + "admin-privileges": "Privilèges", + "admin-users": "Utilisateurs", + "admin-admins-mods": "Admin & Modo", + "admin-groups": "Groupes", + "admin-tags": "Mots Clés", + "admin-settings": "Paramètres", + + "alert.confirm-moderate": "Voulez-vous vraiment accorder le privilège de modération à ce groupe d'utilisateurs ? Ce groupe est public et tous les utilisateurs peuvent le rejoindre à volonté.", + "alert.confirm-admins-mods": "Voulez-vous vraiment attribuer les droits aux & quot; d'Administrations & amp; Modérations & quot; à cet utilisateur / groupe? Les utilisateurs disposant de ce privilège peuvent promouvoir et rétrograder d'autres utilisateurs à des postes privilégiés, y compris le super administrateur", + "alert.confirm-save": "Veuillez confirmer votre intention de sauvegarder ces privilèges", + "alert.saved": "Changements de privilèges enregistrés et appliqués", + "alert.confirm-discard": "Êtes-vous sûr de vouloir annuler vos modifications de privilèges ?", + "alert.discarded": "Modifications de privilèges annulés", + "alert.confirm-copyToAll": "Voulez-vous vraiment appliquer cet ensemble de %1 à toutes les catégories?", + "alert.confirm-copyToAllGroup": "Voulez-vous vraiment appliquer l'ensemble de %1 de ce groupe à toutes les catégories?", + "alert.confirm-copyToChildren": "Voulez-vous vraiment appliquer cet ensemble de %1 à toutes les catégories incluses (enfants)?", + "alert.confirm-copyToChildrenGroup": "Voulez-vous vraiment appliquer l'ensemble de %1 de ce groupe à toutes les catégories incluses (enfants)?", + "alert.no-undo": "Cette action ne peut pas être annulée.", + "alert.admin-warning": "Les administrateurs obtiennent implicitement tous les privilèges", + "alert.copyPrivilegesFrom-title": "Sélectionnez une catégorie à copier", + "alert.copyPrivilegesFrom-warning": "Cela copiera %1 de la catégorie sélectionnée.", + "alert.copyPrivilegesFromGroup-warning": "Cela copiera l'ensemble de %1 de ce groupe à partir de la catégorie sélectionnée." +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/registration.json b/public/language/fr/admin/manage/registration.json new file mode 100644 index 0000000000..e4937da15c --- /dev/null +++ b/public/language/fr/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "File d'attente", + "description": "Il n'y a aucun utilisateur dans la file d'inscription.
Pour activer cette fonctionnalité, allez dans Paramètres → Utilisateurs → inscription des utilisateurs et définissez Type d'inscription en \"Approbation par l'administrateur\".", + + "list.name": "Nom", + "list.email": "E-mail", + "list.ip": "IP", + "list.time": "Date", + "list.username-spam": "Fréquence : %1 Apparait : %2 Confiance : %3", + "list.email-spam": "Fréquence : %1 Apparait : %2", + "list.ip-spam": "Fréquence : %1 Apparait : %2", + + "invitations": "Invitations", + "invitations.description": "Ci-dessous se trouve une liste complète des invitations envoyées. Utilisez CTRL + F pour rechercher un email ou nom d'utilisateur dans la liste.
\n
Le nom d'utilisateur sera affiché à droite des emails pour les utilisateurs qui ont accepté leur invitation.", + "invitations.inviter-username": "Invité par l'utilisateur", + "invitations.invitee-email": "Email invité", + "invitations.invitee-username": "Nom d'utilisateur de l'invité (si inscrit)", + + "invitations.confirm-delete": "Êtes-vous sûr de vouloir supprimer l'invitation ?" +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/tags.json b/public/language/fr/admin/manage/tags.json new file mode 100644 index 0000000000..5bf517ed79 --- /dev/null +++ b/public/language/fr/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Votre forum n'a pour l'instant aucun sujet avec mots-clés.", + "bg-color": "Couleur d'arrière plan", + "text-color": "Couleur du texte", + "description": "Pour une sélection multiple :\nSélectionnez des mots-clés en cliquant ou en sélectionnant avec votre souris ou utilisez la touche CTRL .", + "create": "Créer le mot-clés", + "modify": "Modifier les mots-clés", + "rename": "Renommer les mots-clés", + "delete": "Supprimer la sélection", + "search": "Chercher des mots-clés...", + "settings": "Paramètres", + "name": "Nom du mot-clés", + + "alerts.editing": "Édition des mots-clés", + "alerts.confirm-delete": "Vous-voulez réellement supprimer les mots-clés sélectionnés ?", + "alerts.update-success": "Mot-clés mis à jour !", + "reset-colors": "Réinitialiser les couleurs" +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/uploads.json b/public/language/fr/admin/manage/uploads.json new file mode 100644 index 0000000000..8f1e276a6f --- /dev/null +++ b/public/language/fr/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Envoyer un fichier", + "filename": "Nom du fichier", + "usage": "Utilisé dans le message", + "orphaned": "Orphelin", + "size/filecount": "Taille / nombre de fichiers", + "confirm-delete": "Voulez-vous vraiment supprimer ce fichier?", + "filecount": "%1 fichiers", + "new-folder": "Nouveau Dossier", + "name-new-folder": "Entrez un nom pour le nouveau dossier" +} \ No newline at end of file diff --git a/public/language/fr/admin/manage/users.json b/public/language/fr/admin/manage/users.json new file mode 100644 index 0000000000..9f603b68d5 --- /dev/null +++ b/public/language/fr/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Utilisateurs", + "edit": "Actions", + "make-admin": "Promouvoir Admin", + "remove-admin": "Retirer des Admins", + "validate-email": "Vérifier l'adresse e-mail", + "send-validation-email": "Envoyer un e-mail de vérification", + "password-reset-email": "Envoyer un e-mail de réinitialisation du mot de passe", + "force-password-reset": "Forcer la réinitialisation du mot de passe et déconnecter l'utilisateur", + "ban": "Utilisateur(s) banni(s)", + "temp-ban": "Utilisateur(s) temporairement banni(s)", + "unban": "Dé-bannir le(s) utilisateur(s)", + "reset-lockout": "Supprimer le blocage", + "reset-flags": "Supprimer les signalements", + "delete": "Supprimer le(s) utilisateur(s)", + "delete-content": "Supprimer le contenu du compte", + "purge": "Supprimer le(s) compte(s) et le contenu", + "download-csv": "Exporter en CSV", + "manage-groups": "Gérer les groupes", + "add-group": "Ajouter un groupe", + "create": "Créer un utilisateur", + "invite": "Inviter par mail", + "new": "Nouvel utilisateur", + "filter-by": "Filtrer par", + "pills.unvalidated": "Non vérifié", + "pills.validated": "Validé", + "pills.banned": "Banni", + + "50-per-page": "50 par page", + "100-per-page": "100 par page", + "250-per-page": "250 par page", + "500-per-page": "500 par page", + + "search.uid": "Par ID", + "search.uid-placeholder": "Rechercher avec l'ID", + "search.username": "Par nom d'utilisateur", + "search.username-placeholder": "Entrer un nom d'utilisateur à rechercher", + "search.email": "Par adresse e-mail", + "search.email-placeholder": "Entrez une adresse e-mail à rechercher", + "search.ip": "Par adresse IP", + "search.ip-placeholder": "Entrez une adresse IP à rechercher", + "search.not-found": "Utilisateur introuvable !", + + "inactive.3-months": "3 mois", + "inactive.6-months": "6 mois", + "inactive.12-months": "12 mois", + + "users.uid": "uid", + "users.username": "nom d'utilisateur", + "users.email": "e-mail", + "users.no-email": "(pas e-mail)", + "users.ip": "IP", + "users.postcount": "nombre de sujets", + "users.reputation": "réputation", + "users.flags": "signalements", + "users.joined": "inscription", + "users.last-online": "dernière connexion", + "users.banned": "banni", + + "create.username": "Nom d'utilisateur", + "create.email": "E-mail", + "create.email-placeholder": "Adresse e-mail de l'utilisateur", + "create.password": "Mot de passe", + "create.password-confirm": "Confirmer le mot de passe", + + "temp-ban.length": "Longueur", + "temp-ban.reason": "Raison (Optionel)", + "temp-ban.hours": "Heures", + "temp-ban.days": "Jours", + "temp-ban.explanation": "Entrez la durée du bannissement. Notez qu'une durée de 0 sera considérée comme un bannissement définitif.", + + "alerts.confirm-ban": "Voulez-vous réellement bannir définitivement cet utilisateur ?", + "alerts.confirm-ban-multi": "Voulez-vous réellement bannir définitivement ces utilisateurs ?", + "alerts.ban-success": "Utilisateur(s) banni(s)", + "alerts.button-ban-x": "Bannir %1 utilisateur(s)", + "alerts.unban-success": "Utilisateur(s) dé-banni(s) !", + "alerts.lockout-reset-success": "Blocage supprimé", + "alerts.flag-reset-success": "Signalement(s) réinitialisé(s) !", + "alerts.no-remove-yourself-admin": "Vous ne pouvez pas vous retirer vous-même des administrateurs !", + "alerts.make-admin-success": "L'utilisateur est maintenant administrateur.", + "alerts.confirm-remove-admin": "Voulez-vous vraiment supprimer cet administrateur?", + "alerts.remove-admin-success": "L'utilisateur n'est plus administrateur", + "alerts.make-global-mod-success": "L'utilisateur est maintenant modérateur global", + "alerts.confirm-remove-global-mod": "Voulez-vous vraiment supprimer ce modérateur global?", + "alerts.remove-global-mod-success": "L'utilisateur n'est plus un modérateur global.", + "alerts.make-moderator-success": "L'utilisateur est maintenant modérateur", + "alerts.confirm-remove-moderator": "Voulez-vous vraiment supprimer ce modérateur?", + "alerts.remove-moderator-success": "L'utilisateur n'est plus modérateur", + "alerts.confirm-validate-email": "Voulez-vous réellement vérifier les adresses e-mail de ces utilisateurs ?", + "alerts.confirm-force-password-reset": "Êtes-vous sûr de vouloir forcer la réinitialisation du mot de passe et déconnecter ces utilisateur(s) ?", + "alerts.validate-email-success": "Adresse e-mail vérifiée", + "alerts.validate-force-password-reset-success": "Les mots de passe des utilisateurs ont été réinitialisés et leurs sessions existantes ont été révoquées.", + "alerts.password-reset-confirm": "Voulez-vous réellement envoyer un e-mail de réinitialisation de mot de passe à ces utilisateurs ?", + "alerts.password-reset-email-sent": "Le mail pour réinitialiser votre mot de passe a était envoyer.", + "alerts.confirm-delete": "Attention !

Voulez-vous réellement supprimer le(s) utilisateur(s) ?

Cette action est irréversible ! Toutes les données de ces utilisateurs seront effacées !

", + "alerts.delete-success": "Utilisateur(s) supprimé(s) !", + "alerts.confirm-delete-content": "Attention !

Voulez-vous réellement supprimer le contenu de ces utilisateurs ?

Cette action est irréversible ! Toutes les données de ces utilisateurs seront effacées !

", + "alerts.delete-content-success": "Contenu utilisateur(s) supprimé(s) !", + "alerts.confirm-purge": "Attention !

Voulez-vous réellement supprimer ces utilisateurs ainsi que leurs contenus ?

Cette action est irréversible ! Toutes les données de ces utilisateurs seront effacées !

", + "alerts.create": "Créer un utilisateur", + "alerts.button-create": "Créer", + "alerts.button-cancel": "Annuler", + "alerts.error-passwords-different": "Les mots de passe doivent correspondre !", + "alerts.error-x": "Erreur

%1

", + "alerts.create-success": "Utilisateur créé !", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "Un e-mail d'invitation a été envoyé à %1", + "alerts.x-users-found": "%1 utilisateur(s) trouvé(s), (%2 secondes)", + "export-users-started": "L'exportation d'utilisateurs au format CSV peut prendre un certain temps. Vous recevrez une notification lorsqu'elle sera terminée.", + "export-users-completed": "Utilisateurs exportés au format CSV, cliquez ici pour télécharger." +} \ No newline at end of file diff --git a/public/language/fr/admin/menu.json b/public/language/fr/admin/menu.json new file mode 100644 index 0000000000..c24c461f4e --- /dev/null +++ b/public/language/fr/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Tableau de bord", + "dashboard/overview": "Aperçu", + "dashboard/logins": "Connexions", + "dashboard/users": "Utilisateurs", + "dashboard/topics": "Sujets", + "dashboard/searches": "Recherches", + "section-general": "Général", + + "section-manage": "Gestion", + "manage/categories": "Catégories", + "manage/privileges": "Privilèges", + "manage/tags": "Mots-clés", + "manage/users": "Utilisateurs", + "manage/admins-mods": "Modération", + "manage/registration": "File d'inscription", + "manage/post-queue": "File d’attente des messages", + "manage/groups": "Groupes", + "manage/ip-blacklist": "Liste noire d'IPs", + "manage/uploads": "Fichiers envoyés", + "manage/digest": "Lettres d'activités", + + "section-settings": "Réglages", + "settings/general": "Général", + "settings/homepage": "Page d'accueil", + "settings/navigation": "Navigation", + "settings/reputation": "Réputation", + "settings/email": "Email", + "settings/user": "Utilisateurs", + "settings/group": "Groupes", + "settings/guest": "Invités", + "settings/uploads": "Envois", + "settings/languages": "Langues", + "settings/post": "Messages", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Mots-clés", + "settings/notifications": "Notifications", + "settings/api": "Gestion API", + "settings/sounds": "Sons", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Indexation", + "settings/sockets": "Sockets", + "settings/advanced": "Avancé", + + "settings.page-title": "Réglages %1", + + "section-appearance": "Apparence", + "appearance/themes": "Thèmes", + "appearance/skins": "Apparence", + "appearance/customise": "Contenu personnalisé (HTML/JS/CSS)", + + "section-extend": "Extensions", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Récompenses", + + "section-social-auth": "Authentification via les réseaux sociaux", + + "section-plugins": "Plugins", + "extend/plugins.install": "Installer des plugins", + + "section-advanced": "Avancé", + "advanced/database": "Base de données", + "advanced/events": "Évènements", + "advanced/hooks": "Crochets", + "advanced/logs": "Journaux", + "advanced/errors": "Erreurs", + "advanced/cache": "Cache", + "development/logger": "Réglages journalisation", + "development/info": "Info", + + "rebuild-and-restart-forum": "Régénérer & Redémarrer votre forum", + "restart-forum": "Redémarrer le forum", + "logout": "Déconnexion ", + "view-forum": "Voir le forum", + + "search.placeholder": "Paramètres de recherche", + "search.no-results": "Aucun résultat…", + "search.search-forum": "Rechercher dans le forum", + "search.keep-typing": "Continuez de taper pour afficher les résultats…", + "search.start-typing": "Écrivez pour lancer la recherche...", + + "connection-lost": "La connexion à %1 a été perdue, tentative de reconnexion…", + + "alerts.version": "NodeBB v%1", + "alerts.upgrade": "Mettre à jour en v%1" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/advanced.json b/public/language/fr/admin/settings/advanced.json new file mode 100644 index 0000000000..13058af0d3 --- /dev/null +++ b/public/language/fr/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Mode maintenance", + "maintenance-mode.help": "Quand le forum est en mode maintenance, toutes les requêtes sont redirigées vers une page de garde statique. Les administrateurs sont exemptés de cette redirection et peuvent accéder normalement au site. ", + "maintenance-mode.status": "Mode maintenance", + "maintenance-mode.message": "Message de maintenance", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Sélectionner les groupes qui doivent être exemptés du mode maintenance", + "headers": "En-têtes", + "headers.allow-from": "Définissez ALLOW-FROM pour afficher NodeBB dans un iFrame", + "headers.csp-frame-ancestors": "Définir la politique de sécurité pour pouvoir intégrer des iframes", + "headers.csp-frame-ancestors-help": "'none', 'self' (par défaut) ou liste d'UrIs à autoriser.", + "headers.powered-by": "Personnaliser l'en-tête \"Propulsé par\" envoyé par NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Expression régulière", + "headers.acao-help": "Pour refuser l'accès à tous les sites, laissez vide", + "headers.acao-regex-help": "Entrez une expression régulière pour autoriser les pages de manière dynamique. Pour interdire l'accès à toutes les pages, laissez vide.", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "\nAccess-Control-Allow-Methods", + "headers.acah": "\nAccess-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "Lorsqu'il est activé (par défaut), définira l'en-tête sur require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Activer HSTS (recommandé)", + "hsts.maxAge": "HSTS Age Max", + "hsts.subdomains": "Inclure les sous-domaines dans l'en-tête HSTS", + "hsts.preload": "Autoriser le préchargement de l'en-tête HSTS", + "hsts.help": "Si activé, un en-tête HSTS sera défini pour ce site. Vous pouvez choisir d'inclure des sous-domaines et des indicateurs de préchargement dans votre en-tête. En cas de doute, ne cochez pas l'option. Plus d'informations", + "traffic-management": "Gestion du trafic", + "traffic.help": "NodeBB utilise un module qui refuse automatiquement les demandes dans les situations de fort trafic. Vous pouvez régler ces paramètres ici, bien que les valeurs par défaut soient un bon point de départ.", + "traffic.enable": "Activé la gestion du trafic", + "traffic.event-lag": "Seuil de lag des boucles d'événements (en millisecondes) ", + "traffic.event-lag-help": "Descendre cette valeur réduit le temps d'attente pour le chargement de s pages, mais montrera le message \"charge excessive\" à plus d'utilisateurs. (redémarrage requis)", + "traffic.lag-check-interval": "Vérifier l’intervalle (en millisecondes)", + "traffic.lag-check-interval-help": "Descendre cette valeur rend NodeBB plus sensible aux pics dans le chargement, mais rend aussi le contrôle trop sensible. (redémarrage requis)", + + "sockets.settings": "Configuration WebSocket", + "sockets.max-attempts": "Nombre maximum de tentatives de reconnexion", + "sockets.default-placeholder": "Défaut : %1", + "sockets.delay": "Délai de reconnexion", + + "analytics.settings": "Paramètres d'analytique", + "analytics.max-cache": "Valeur maximale du cache Analytique", + "analytics.max-cache-help": "Sur les installations à fort trafic, le cache peut être utilisé en permanence s'il y a plus d'utilisateurs actifs simultanément que la valeur Max Cache. (Redémarrage requis)", + "compression.settings": "Paramètres de compression", + "compression.enable": "Activer la compression", + "compression.help": "Ce paramètre active la compression gzip. Pour un site Web à fort trafic en production, la meilleure façon de mettre en place la compression est de l'implémenter au niveau du reverse proxy. Vous pouvez l'activer ici à des fins de test." +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/api.json b/public/language/fr/admin/settings/api.json new file mode 100644 index 0000000000..87ae70eff0 --- /dev/null +++ b/public/language/fr/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Paramètres", + "lead-text": "À partir de cette page, vous pouvez paramétrer l'accès à l'API dans NodeBB.", + "intro": "Par défaut, l'API authentifie les utilisateurs en fonction de leur cookie de session, mais NodeBB prend également en charge l'authentification du porteur via des tokens générés via cette page.", + "docs": "Cliquez ici pour accéder à la documentation de l'API", + + "require-https": "Forcer l'utilisation de l'API via HTTPS uniquement", + "require-https-caveat": "Remarque: certaines installations impliquant des load balancer peuvent transmettre leurs requêtes à NodeBB via HTTP, auquel cas cette option doit rester désactivée.", + + "uid": "ID Utilisateur", + "uid-help-text": "Spécifiez un ID utilisateur à associer à ce token. Si l'ID utilisateur est 0, il sera considéré comme un token maître, qui peut prendre l'identité d'autres utilisateurs en fonction du paramètre _uid", + "description": "Description", + "no-description": "Aucune description spécifiée.", + "token-on-save": "Le token sera généré une fois le formulaire enregistré" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/chat.json b/public/language/fr/admin/settings/chat.json new file mode 100644 index 0000000000..7efc130f0e --- /dev/null +++ b/public/language/fr/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Paramètres des discussions", + "disable": "Désactiver les discussions", + "disable-editing": "Désactiver l'édition/la suppression des messages des discussions", + "disable-editing-help": "Les administrateurs et modérateurs globaux sont dispensés de cette restriction", + "max-length": "Longueur maximales des messages de discussion", + "max-room-size": "Nombre maximum d'utilisateurs dans une même discussion", + "delay": "Temps entre chaque message de discussion (en millisecondes)", + "notification-delay": "Délai de notification pour les messages de chat. (0 pour aucun délai)", + "restrictions.seconds-edit-after": "Nombre de secondes pendant lesquelles un message de discussion reste modifiable. (0 désactivé)", + "restrictions.seconds-delete-after": "Nombre de secondes pendant lesquelles un message de discussion reste supprimable. (0 désactivé)" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/cookies.json b/public/language/fr/admin/settings/cookies.json new file mode 100644 index 0000000000..b337057dfc --- /dev/null +++ b/public/language/fr/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Consentement de l'Union européenne", + "consent.enabled": "Activé", + "consent.message": "Message de notification", + "consent.acceptance": "Message d'acceptation", + "consent.link-text": "Texte du lien vers la politique de confidentialité", + "consent.link-url": "URL du lien Policy", + "consent.blank-localised-default": "Laisser vide pour utiliser les textes localisés par défaut de NodeBB", + "settings": "Réglages", + "cookie-domain": "Domaine de session du cookie", + "max-user-sessions": "Nombre maximum de sessions actives par utilisateur", + "blank-default": "Laissez vide pour utiliser les réglages par défaut" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/email.json b/public/language/fr/admin/settings/email.json new file mode 100644 index 0000000000..42b64a4343 --- /dev/null +++ b/public/language/fr/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Paramètres E-mail", + "address": "Adresse e-mail", + "address-help": "L'adresse e-mail suivante fait référence à l'adresse que le destinataire verra dans les champs \"De :\" et \"Répondre à :\". ", + "from": "Nom de l’expéditeur", + "from-help": "Le nom de l’expéditeur à afficher dans l'e-mail", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Heures de validité du lien de confirmation par e-mail", + + "smtp-transport": "Protocole SMTP", + "smtp-transport.enabled": "Activer l'envoi via SMTP", + "smtp-transport-help": "Vous pouvez sélectionner depuis une liste de services ou entrer un service personnalisé.", + "smtp-transport.service": "Sélectionner un service", + "smtp-transport.service-custom": "Service personnalisé", + "smtp-transport.service-help": "Sélectionnez un nom de service ci-dessus afin d'utiliser les informations connues à son sujet. Vous pouvez également sélectionner "Service personnalisé" et entrez les détails ci-dessous.", + "smtp-transport.gmail-warning1": "Si vous utilisez GMail comme fournisseur de messagerie, vous devrez générer un "mot de passe d'application" afin que NodeBB s'authentifie avec succès. Vous pouvez en générer un sur la page Mots de passe .", + "smtp-transport.gmail-warning2": "Pour plus d'informations sur cette solution de contournement, veuillez consulter cet article de NodeMailer sur le problème. Une alternative serait d'utiliser un plug-in tiers d'e-mail tel que SendGrid, Mailgun, etc. Parcourez les plug-ins disponibles ici.", + "smtp-transport.auto-enable-toast": "Il semble que vous configuriez un serveur SMTP. Nous avons activé l'option \" SMTP\" pour vous.", + "smtp-transport.host": "Host SMTP", + "smtp-transport.port": "Port SMTP", + "smtp-transport.security": "Accès sécurisé", + "smtp-transport.security-encrypted": "Cryptage", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Aucun", + "smtp-transport.username": "Nom d'utilisateur", + "smtp-transport.username-help": "Pour Gmail, entrer l’adresse e-mail complète ici, surtout si vous utilisez un domaine géré par Google Apps.", + "smtp-transport.password": "Mot de passe", + "smtp-transport.pool": "Activer les connexions groupées", + "smtp-transport.pool-help": "Le regroupement des connexions empêche NodeBB de créer une nouvelle connexion pour chaque e-mail. Cette option s'applique uniquement si le transport SMTP est activé.", + + "template": "Modifier le modèle d'e-mail", + "template.select": "Sélectionner un modèle d'e-mail ", + "template.revert": "Retourner à l'original", + "testing": "Test d'e-mail", + "testing.select": "Sélectionner un modèle d'e-mail ", + "testing.send": "Envoyer un e-mail de test", + "testing.send-help": "Le test d'e-mail sera envoyé à l'adresse e-mail de l'utilisateur actuellement connecté.", + "subscriptions": "Actualités du forum ", + "subscriptions.disable": "Désactiver les actualités du forum ", + "subscriptions.hour": "Heure d'envoi", + "subscriptions.hour-help": "Veuillez entrer un nombre représentant l'heure à laquelle envoyer les lettres d'activités (c'est à dire 0 pour minuit, 17 pour 5:00 pm). Gardez à l'esprit qu'il s'agit de l'heure du serveur, et peut ne pas correspondre à votre heure locale.
L'heure du serveur est :
La prochaine lettre d'activités sera envoyée à ", + "notifications.remove-images": "Supprimer les images des notifications par e-mail", + "require-email-address": "Exiger une adresse e-mail aux nouveaux utilisateurs ", + "require-email-address-warning": "Par défaut, les utilisateurs peuvent refuser de saisir une adresse e-mail en laissant le champ vide. L'activation de cette option signifie qu'ils doivent entrer une adresse e-mail afin de procéder à l'inscription. Cela ne garantit pas que l'utilisateur entrera une adresse e-mail valide, ni même une adresse qu'il possède.", + "send-validation-email": "Envoyer une confirmation de validation lorsqu'un e-mail est ajouté ou modifié", + "include-unverified-emails": "Envoyer des mails aux destinataires qui n'ont pas explicitement confirmé leurs mails", + "include-unverified-warning": "Par défaut, les utilisateurs dont les mails sont associés à leur compte ont déjà été vérifiés, mais il existe des situations où ce n'est pas le cas (par exemple, les connexions SSO, les utilisateurs bénéficiant de droits acquis, etc.). Activez ce paramètre à vos risques et périls – l'envoi de mails à des adresses non vérifiées peut constituer une violation des lois anti-spam régionales.", + "prompt": "Inviter les utilisateurs à saisir ou à confirmer leurs emails", + "prompt-help": "Si un utilisateur n'a pas défini d'email ou si son email n'est pas confirmé, un avertissement s'affichera à l'écran.", + "sendEmailToBanned": "Envoyer des e-mails aux utilisateurs même s'ils ont été bannis" +} diff --git a/public/language/fr/admin/settings/general.json b/public/language/fr/admin/settings/general.json new file mode 100644 index 0000000000..0f871bec8c --- /dev/null +++ b/public/language/fr/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Réglages du site", + "title": "Titre du site", + "title.short": "Titre court", + "title.short-placeholder": "Si aucun titre court n'est spécifié, le titre du site sera utilisé", + "title.url": "URL du lien du titre", + "title.url-placeholder": "URL du titre du site", + "title.url-help": "Lorsque le titre est cliqué, il renvoi les utilisateurs à cette adresse. Si laissé vide, l'utilisateur sera envoyé à l'index du forum.
Remarque : il ne s'agit pas de l'URL externe utilisée dans les e-mails, etc. Elle est définie par la propriété url dans config.json", + "title.name": "Nom de votre communauté", + "title.show-in-header": "Afficher le titre du site dans l'en-tête", + "browser-title": "Titre dans le navigateur", + "browser-title-help": "Si aucun titre dans le navigateur n'est spécifié, le titre du site sera utilisé", + "title-layout": "Disposition du titre", + "title-layout-help": "Définissez la manière dont le titre est structuré dans le navigateur ex: { pageTitle} | {browserTitle}", + "description.placeholder": "Une courte description de votre communauté", + "description": "Description du site", + "keywords": "Mots-clés du site", + "keywords-placeholder": "Mots-clés décrivant votre communauté, séparés par des virgules", + "logo": "Logo du site", + "logo.image": "Image", + "logo.image-placeholder": "Chemin vers un logo à afficher dans l'en-tête du site", + "logo.upload": "Télécharger", + "logo.url": "URL du lien du logo", + "logo.url-placeholder": "L'URL du logo du site", + "logo.url-help": "Lorsque le logo est cliqué, il renvoi les utilisateurs à cette adresse. Si laissé vide, l'utilisateur sera envoyé à l'index du forum.
Remarque : il ne s'agit pas de l'URL externe utilisée dans les e-mails, etc. Elle est définie par la propriété url dans config.json", + "logo.alt-text": "Texte alternatif (alt)", + "log.alt-text-placeholder": "Texte alternatif pour l'accessibilité", + "favicon": "Favicon", + "favicon.upload": "Télécharger", + "pwa": "Progressive Web App", + "touch-icon": "Icône d'accueil", + "touch-icon.upload": "Télécharger", + "touch-icon.help": "Taille et format recommandés: 512x512, format PNG uniquement. Si aucun Icône d'accueil n'est spécifiée, le favicon NodeBB sera visible.", + "maskable-icon": "Icône masquable (écran d'accueil)", + "maskable-icon.help": "Taille et format recommandés: 512x512, format PNG uniquement. Si aucune icône masquable n'est spécifiée, le favicon NodeBB sera visible.", + "outgoing-links": "Liens sortants", + "outgoing-links.warning-page": "Utiliser la page d'avertissement pour liens sortants", + "search": "Rechercher", + "search-default-in": "Rechercher dans", + "search-default-in-quick": "Recherche rapide dans", + "search-default-sort-by": "Trier par", + "outgoing-links.whitelist": "Domaines à inclure dans la liste blanche pour passer la page d'avertissement.", + "site-colors": "Métadonnées des couleurs du site", + "theme-color": "Couleur du thème", + "background-color": "Couleur de l'arrière plan", + "background-color-help": "Couleur utilisée pour l'arrière-plan de l'écran de démarrage lorsque le site Web est installé en tant que PWA", + "undo-timeout": "Annuler le délai d'attente", + "undo-timeout-help": "Certaines opérations telles que le déplacement de sujets permettront au modérateur d'annuler son action dans un certain délai. Réglez sur 0 pour désactiver complètement l'annulation.", + "topic-tools": "Outils pour sujets" +} diff --git a/public/language/fr/admin/settings/group.json b/public/language/fr/admin/settings/group.json new file mode 100644 index 0000000000..c32ecfb66f --- /dev/null +++ b/public/language/fr/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Général", + "private-groups": "Groupes privés", + "private-groups.help": "Si cette case est cochée, rejoindre un groupe nécessitera l'accord d'un propriétaire du groupe (Par défaut : activé)", + "private-groups.warning": "Attention ! Si cette option est désactivée et que vous avez des groupes privés, ils deviendront automatiquement publics.", + "allow-multiple-badges": "Autoriser de multiple badges", + "allow-multiple-badges-help": "Cet affichage peut être utilisé pour permettre aux utilisateurs de sélectionner plusieurs badges de groupe, nécessite que votre thème le supporte.", + "max-name-length": "Longueur maximum des noms de groupes", + "max-title-length": "Longueur maximale du titre de groupe", + "cover-image": "Image de couverture du groupe", + "default-cover": "Images de couverture par défaut", + "default-cover-help": "Ajouter des images de couvertures par défaut séparées par des virgules pour les groupes n'ayant pas téléchargé d'image de couverture" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/guest.json b/public/language/fr/admin/settings/guest.json new file mode 100644 index 0000000000..8ddbeba3a4 --- /dev/null +++ b/public/language/fr/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Paramètres", + "handles.enabled": "Autoriser les invités à poster", + "handles.enabled-help": "Cette option affiche un nouveau champ qui permet aux invités de choisir un nom qui sera associé à chaque message qu'ils rédigent. Si désactivé, il seront simplement nommés \"Invité\".", + "topic-views.enabled": "Autoriser les invités à augmenter le nombre de consultations de sujets", + "reply-notifications.enabled": "Autoriser les invités à générer des notifications de réponse" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/homepage.json b/public/language/fr/admin/settings/homepage.json new file mode 100644 index 0000000000..3efe41fe65 --- /dev/null +++ b/public/language/fr/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Page d'accueil", + "description": "Choisissez la page affichée lorsque les utilisateurs naviguent à la racine de votre forum.", + "home-page-route": "Route de la page d'accueil", + "custom-route": "Route personnalisée", + "allow-user-home-pages": "Permettre aux utilisateurs de choisir une page d'accueil personnalisée", + "home-page-title": "Titre de la page d'accueil (par défaut \"Accueil\")" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/languages.json b/public/language/fr/admin/settings/languages.json new file mode 100644 index 0000000000..51ee9f7f01 --- /dev/null +++ b/public/language/fr/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Réglages linguistiques", + "description": "La langue par défaut détermine les réglages pour tous les utilisateurs qui visitent votre forum.
Les utilisateurs peuvent ensuite modifier la langue par défaut sur leur page de réglages.", + "default-language": "Langue par défaut", + "auto-detect": "Détection automatique de la langue pour les invités" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/navigation.json b/public/language/fr/admin/settings/navigation.json new file mode 100644 index 0000000000..1305aed145 --- /dev/null +++ b/public/language/fr/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icône :", + "change-icon": "changer", + "route": "Route :", + "tooltip": "Info-bulle :", + "text": "Texte :", + "text-class": "Classe de texte : optionnel", + "class": "Classe: facultatif", + "id": "ID : optionnel", + + "properties": "Propriétés :", + "groups": "Groupes:", + "open-new-window": "Ouvrir dans une nouvelle fenêtre", + "dropdown": "Menu déroulant", + "dropdown-placeholder": "Placez vos éléments de menu déroulant ci-dessous, par exemple :
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Supprimer", + "btn.disable": "Désactiver", + "btn.enable": "Activer", + + "available-menu-items": "Éléments de menu disponibles", + "custom-route": "Route personnalisée", + "core": "cœur", + "plugin": "plugin" +} diff --git a/public/language/fr/admin/settings/notifications.json b/public/language/fr/admin/settings/notifications.json new file mode 100644 index 0000000000..9a0978d167 --- /dev/null +++ b/public/language/fr/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Notification de bienvenue", + "welcome-notification-link": "Lien de notification de bienvenue", + "welcome-notification-uid": "Notification de bienvenue de l'utilisateur (UID)", + "post-queue-notification-uid": "File d'attente d l'Utilisateur (UID)" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/pagination.json b/public/language/fr/admin/settings/pagination.json new file mode 100644 index 0000000000..512f4471b1 --- /dev/null +++ b/public/language/fr/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Paramètres de pagination", + "enable": "Utiliser la pagination des sujets et messages au lieu du défilement infini", + "posts": "Pagination des messages", + "topics": "Pagination des sujets", + "posts-per-page": "Messages par page", + "max-posts-per-page": "Messages maximum par page", + "categories": "Pagination des categories", + "topics-per-page": "Sujets par page", + "max-topics-per-page": "Sujets maximum par page", + "categories-per-page": "Catégories par page" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/post.json b/public/language/fr/admin/settings/post.json new file mode 100644 index 0000000000..9546189788 --- /dev/null +++ b/public/language/fr/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Tri des messages", + "sorting.post-default": "Tri des messages par défaut", + "sorting.oldest-to-newest": "Du plus ancien au plus récent", + "sorting.newest-to-oldest": "Du plus récent au plus ancien", + "sorting.most-votes": "Avec le plus de votes", + "sorting.most-posts": "Avec le plus de messages", + "sorting.topic-default": "Tri des sujets par défaut", + "length": "Longueur de message", + "post-queue": "File d'attente", + "restrictions": "Restrictions d'envoi", + "restrictions-new": "Restrictions des nouveaux utilisateurs", + "restrictions.post-queue": "Activer la file d'attente des messages", + "restrictions.post-queue-rep-threshold": "Réputation requise pour contourner la file d'attente de publication", + "restrictions.groups-exempt-from-post-queue": "Sélectionnez les groupes qui devraient être exemptés de la file d'attente de publication", + "restrictions-new.post-queue": "Activer la restriction", + "restrictions.post-queue-help": "L'activation de la file d'attente de publication placera les messages de nouveaux utilisateurs dans une file d'attente pour approbation.", + "restrictions-new.post-queue-help": "L'activation de restrictions des nouveaux utilisateurs définira des restrictions sur les messages créées par de nouveaux utilisateurs.", + "restrictions.seconds-between": "Nombre de secondes entre les messages", + "restrictions.seconds-between-new": "Secondes entre les messages pour les nouveaux utilisateurs", + "restrictions.rep-threshold": "Seuil de réputation avant que ces restrictions ne soient levées", + "restrictions.seconds-before-new": "Secondes avant qu'un nouvel utilisateur puisse publier son premier message", + "restrictions.seconds-edit-after": "Nombre de secondes pendant lesquelles une publication reste modifiable (définissez la valeur sur 0 pour la désactiver)", + "restrictions.seconds-delete-after": "Nombre de secondes pendant lesquelles un message reste supprimable (définissez la valeur sur 0 pour la désactiver)", + "restrictions.replies-no-delete": "Nombre de réponses avant que les utilisateurs soient pas autorisés à supprimer leurs propres sujets (définissez la valeur sur 0 pour la désactiver)", + "restrictions.min-title-length": "Longueur minimum du titre", + "restrictions.max-title-length": "Longueur maximum du titre", + "restrictions.min-post-length": "Longueur minimum des messages", + "restrictions.max-post-length": "Longueur maximum des messages", + "restrictions.days-until-stale": "Jours jusqu'à ce que le sujet soit considéré comme périmé", + "restrictions.stale-help": "Si un sujet est considéré comme \"périmé\", un message sera affiché aux utilisateurs tentant de répondre au sujet", + "timestamp": "Horodatage", + "timestamp.cut-off": "Date de coupure (en jours)", + "timestamp.cut-off-help": "Les dates et heures seront affichées de façon relative (par exemple \"il y a 3 heures\", \"il y a 5 jours\"), et localisées. Après un certain point, le texte peut afficher la date emlle-même (par exemple 5 Novembre 2016, 15:30).
(Défaut : 30, ou un mois). Régler à 0 pour toujours afficher des dates, laisser vide pour toujours afficher des dates relatives.", + "timestamp.necro-threshold": "Seuil d'archivage (en jours)", + "timestamp.necro-threshold-help": "Un message sera affiché entre les messages si le temps qui les sépare est plus long que le seuil d'archivage. (Par défaut: 7 ou une semaine). Définissez sur 0 pour désactiver.", + "timestamp.topic-views-interval": "Incrémenter l'intervalle de consultation du sujet (en minutes)", + "timestamp.topic-views-interval-help": "Les conseultations de sujet ne s'incrémenteront qu'une fois toutes les X minutes, comme défini par ce paramètre.", + "teaser": "Message d'aperçu", + "teaser.last-post": "Dernier – Affiche le dernier message, ou celui d'origine, si il n'y a pas de réponse", + "teaser.last-reply": "Dernier – Affiche le dernier message, ou \"Aucune réponse\" si il n'y a pas de réponse", + "teaser.first": "Premier", + "showPostPreviewsOnHover": "Afficher un aperçu des messages au survol des liens", + "unread": "Paramètres des messages non lus", + "unread.cutoff": "Nombre de jours pour les messages non-lus", + "unread.min-track-last": "Nombre minimum de messages dans le sujet avant de garder en mémoire le dernier message lu", + "recent": "Réglages récents", + "recent.max-topics": "Maximum de sujets récents", + "recent.categoryFilter.disable": "Désactiver le filtrage des sujets dans les catégories ignorées sur la page /recent", + "signature": "Paramètres de signature", + "signature.disable": "Désactiver les signatures", + "signature.no-links": "Désactiver les liens en signature", + "signature.no-images": "Désactiver les images en signature ", + "signature.hide-duplicates": "Masquer les signatures en double dans les sujets", + "signature.max-length": "Longueur maximum des signatures", + "composer": "Paramètres Composer", + "composer-help": "Les réglages suivants permettent de choisir les fonctionnalités et/ou l'apparence du composeur de message affiché\n\t\t\t\ttaux utilisateurs quand ils créent de nouveaux sujets ou répondent à des sujets existants.", + "composer.show-help": "Afficher l'onglet \"Aide\"", + "composer.enable-plugin-help": "Autoriser les plugins à modifier l'onglet d'aide", + "composer.custom-help": "Message d'aide personnalisé", + "backlinks": "Backlinks", + "backlinks.enabled": "Activer les backlinks de sujet", + "backlinks.help": "Si un message fait référence à un autre sujet, un lien vers le message sera inséré dans le sujet référencé.", + "ip-tracking": "Suivi d'IP", + "ip-tracking.each-post": "Suivre l'adresse IP pour chaque message", + "enable-post-history": "Activer l'historique des publications" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/reputation.json b/public/language/fr/admin/settings/reputation.json new file mode 100644 index 0000000000..a888590efc --- /dev/null +++ b/public/language/fr/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Paramètre de réputation", + "disable": "Désactiver le système de réputation", + "disable-down-voting": "Désactiver les votes négatifs", + "votes-are-public": "Tous les votes sont publics", + "thresholds": "Seuils d'activité", + "min-rep-upvote": "Réputation minimale pour voter pour les publications", + "upvotes-per-day": "Votes positifs par jour (0 pour un nombre illimité)", + "upvotes-per-user-per-day": "Votes positifs par utilisateur et par jour (0 pour un nombre illimité)", + "min-rep-downvote": "Réputation minimum pour les votes négatifs", + "downvotes-per-day": "Votes négatif par jour (0 = illimités)", + "downvotes-per-user-per-day": "Votes négatif pour un utilisateur par jour (0 = illimités)", + "min-rep-chat": "Réputation minimum pour écrire un message", + "min-rep-flag": "Réputation minimum pour signaler un message", + "min-rep-website": "Réputation minimum pour ajouter \"Site internet\" au profil utilisateur", + "min-rep-aboutme": "Réputation minimum pour ajouter \"À propos\" au profil utilisateur", + "min-rep-signature": "Réputation minimum pour ajouter \"Signature\" au profil utilisateur", + "min-rep-profile-picture": "Réputation minimale pour ajouter une photo de profil", + "min-rep-cover-picture": "Réputation minimale pour ajouter une photo de couverture", + + "flags": "Paramètres de signalement", + "flags.limit-per-target": "Nombre maximum de fois qu'un élément peut être signalé", + "flags.limit-per-target-placeholder": "Défaut: 0", + "flags.limit-per-target-help": "Lorsqu'un message ou un utilisateur a été signalé plusieurs fois, chaque indicateur supplémentaire est considéré comme un \"rapport\". et ajouté au signalement d'origine. Définissez cette option sur un nombre autre que zéro pour limiter le nombre de rapports qu'un signalement peut admettre.", + "flags.auto-flag-on-downvote-threshold": "Nombre de votes négatifs pour les signalements (0 pour désactiver, par défaut : 0)", + "flags.auto-resolve-on-ban": "Résoudre automatiquement tous les tickets d'un utilisateur lorsqu'il est banni", + "flags.action-on-resolve": "Procédez comme suit lorsqu'un signalement est résolu", + "flags.action-on-reject": "Procédez comme suit lorsqu'un signalement est rejeté", + "flags.action.nothing": "Ne rien faire", + "flags.action.rescind": "Annuler la notification envoyée aux modérateurs/administrateurs" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/social.json b/public/language/fr/admin/settings/social.json new file mode 100644 index 0000000000..59e9e4e326 --- /dev/null +++ b/public/language/fr/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Partage de message", + "info-plugins-additional": "Les plugins peuvent ajouter de nouveaux réseaux pour partager des messages.", + "save-success": "Sauvegarde réussie !" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/sockets.json b/public/language/fr/admin/settings/sockets.json new file mode 100644 index 0000000000..99821f7cf2 --- /dev/null +++ b/public/language/fr/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Réglages de reconnexion", + "max-attempts": "Nombre maximum de tentatives de reconnexion", + "default-placeholder": "Défaut : %1", + "delay": "Délai de reconnexion" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/sounds.json b/public/language/fr/admin/settings/sounds.json new file mode 100644 index 0000000000..8ec037f62b --- /dev/null +++ b/public/language/fr/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Discussions", + "play-sound": "Jouer", + "incoming-message": "Message entrant", + "outgoing-message": "Message sortant", + "upload-new-sound": "Envoyer un nouveau son", + "saved": "Réglages sauvegardés" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/tags.json b/public/language/fr/admin/settings/tags.json new file mode 100644 index 0000000000..f3b90ddc05 --- /dev/null +++ b/public/language/fr/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Paramètres des mots-clés", + "link-to-manage": "Gérer les mots-clés", + "system-tags": "Gestion des mots-clés", + "system-tags-help": "Seuls les utilisateurs privilégiés pourront utiliser ces mots-clés.", + "min-per-topic": "Nombre minimum de mots-clés par sujet", + "max-per-topic": "Nombre maximum de mots-clés par sujet", + "min-length": "Longueur minimum des mots-clés", + "max-length": "Longueur maximum des mots-clés", + "related-topics": "Sujets connexes", + "max-related-topics": "Nombre maximum de sujets connexes à afficher (si supporté par le thème)" +} \ No newline at end of file diff --git a/public/language/fr/admin/settings/uploads.json b/public/language/fr/admin/settings/uploads.json new file mode 100644 index 0000000000..86384a3acc --- /dev/null +++ b/public/language/fr/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Sujets", + "orphans": "Fichiers orphelins", + "private": "Rendre privés les fichiers téléchargés", + "strip-exif-data": "Supprimer les données EXIF", + "preserve-orphaned-uploads": "Conserver les fichiers téléchargés après la suppression d'une publication.", + "orphanExpiryDays": "Jours pour garder les fichiers orphelins", + "orphanExpiryDays-help": "Les téléchargements orphelins seront supprimés du système de fichiers après ce délai :
Changer 0 Laissez vide pour désactiver.", + "private-extensions": "Rendre privé des extensions de fichier.", + "private-uploads-extensions-help": "Renseignez ici une liste d'extensions de fichiers séparées par des virgules pour les rendre privées (par exemple : pdf, xls, doc). Une liste vide signifie que tous les fichiers sont privés.", + "resize-image-width-threshold": "Redimensionner les images si elles sont plus larges que la largeur spécifiée", + "resize-image-width-threshold-help": "(En pixels, valeur par défaut: 1520 pixels, mettez 0 pour désactiver)", + "resize-image-width": "Redimensionner les images à la largeur spécifiée", + "resize-image-width-help": "(En pixels, valeur par défaut: 760 pixels, mettez 0 pour désactiver)", + "resize-image-quality": "Qualité utilisée des images redimensionnées", + "resize-image-quality-help": "Diminuer la qualité des images redimensionnées pour réduire leur taille.", + "max-file-size": "Taille maximum d'un fichier (en Ko)", + "max-file-size-help": "(en kibioctets, défaut : 2048 Kio)", + "reject-image-width": "Largeur maximale des images (en pixels)", + "reject-image-width-help": "Les images plus larges que cette valeur seront rejetées.", + "reject-image-height": "Hauteur maximale des images (en pixels)", + "reject-image-height-help": "Les images plus hautes que cette valeur seront rejetées.", + "allow-topic-thumbnails": "Autoriser les utilisateurs à télécharger des miniatures de sujet", + "topic-thumb-size": "Miniature du sujet", + "allowed-file-extensions": "Extensions de fichier autorisés", + "allowed-file-extensions-help": "Entrer une liste d’extensions de fichier séparés par une virgule (ex : pdf,xls,doc). Une liste vide signifie que toutes les extensions sont autorisées.", + "upload-limit-threshold": "Limite d'envoi de fichiers par utilisateurs:", + "upload-limit-threshold-per-minute": "Par %1 Minute", + "upload-limit-threshold-per-minutes": "Par %1 Minutes", + "profile-avatars": "Avatar", + "allow-profile-image-uploads": "Autoriser les utilisateurs à télécharger des avatars", + "convert-profile-image-png": "Convertir les avatars téléchargés au format PNG", + "default-avatar": "Modifier l'avatar par défaut", + "upload": "Télécharger", + "profile-image-dimension": "Dimensions de l'avatar", + "profile-image-dimension-help": "(En pixel, par défaut : 128 pixels)", + "max-profile-image-size": "Taille maximum des avatars", + "max-profile-image-size-help": "(en kibioctets, défaut : 256 Kio)", + "max-cover-image-size": "Taille maximum des images de couverture", + "max-cover-image-size-help": "(en kibioctets, défaut : 2048 Kio)", + "keep-all-user-images": "Garder les anciennes versions d'avatars et d'images de couverture sur le serveur", + "profile-covers": "Image de couverture", + "default-covers": "Image de couverture par défaut", + "default-covers-help": "Ajouter des images de couvertures par défaut séparées par des virgules pour les comptes n'ayant pas téléchargé d'image de couverture" +} diff --git a/public/language/fr/admin/settings/user.json b/public/language/fr/admin/settings/user.json new file mode 100644 index 0000000000..a6e6c9f25f --- /dev/null +++ b/public/language/fr/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentification", + "email-confirm-interval": "Les utilisateurs ne peuvent pas demander un e-mail de vérification avant", + "email-confirm-interval2": "minutes se sont écoulées", + "allow-login-with": "Autoriser l'identification avec", + "allow-login-with.username-email": "Nom d'utilisateur ou e-mail", + "allow-login-with.username": "Nom d'utilisateur uniquement", + "account-settings": "Paramètres du compte", + "gdpr_enabled": "Activer le consentement GRPD", + "gdpr_enabled_help": "Une fois activé, tous les nouveaux inscrits seront tenus de donner explicitement leur consentement pour la collecte et l'utilisation des données en vertu du Règlement Général sur la Protection des Données (RGPD). Remarque: l'activation du RGPD n'oblige pas les utilisateurs préexistants à donner leur consentement. Pour ce faire, vous devrez installer le plugin GDPR.", + "disable-username-changes": "Désactiver le changement de nom d'utilisateur", + "disable-email-changes": "Désactiver le changement d'adresse e-mail", + "disable-password-changes": "Désactiver le changement de mot de passe", + "allow-account-deletion": "Autoriser la suppression des comptes", + "hide-fullname": "Masquer le nom complet aux utilisateurs", + "hide-email": "Masquer les emails aux utilisateurs", + "show-fullname-as-displayname": "Afficher le nom complet de l'utilisateur en tant que nom d'affichage si disponible", + "themes": "Thèmes", + "disable-user-skins": "Empêcher les utilisateurs de choisir un skin personnalisé", + "account-protection": "Protection du compte", + "admin-relogin-duration": "Temps de reconnexion pour le compte administrateur (en minutes)", + "admin-relogin-duration-help": "Après un certain temps, l'accessibilité à la section d'administration nécessitera une reconnexion ; fixer le nombre à 0 pour désactiver cet effet.", + "login-attempts": "Tentatives de connexions par heure", + "login-attempts-help": "Si le nombre de tentatives de connexion à un compte dépasse ce seuil, le compte sera bloqué pour une durée pré-configurée", + "lockout-duration": "Durée du blocage (minutes)", + "login-days": "Nombre de jour pendant lesquels se souvenir des sessions d'identification utilisateurs", + "password-expiry-days": "Imposer un changement de mot de passe après un certain nombre de jours", + "session-time": "Temps de session", + "session-time-days": "Jours", + "session-time-seconds": "Secondes", + "session-time-help": "Ces valeurs permettent de définir la durée pendant laquelle un utilisateur reste connecté lorsqu'il consulte le lien \"Se souvenir de moi\". Notez que seulement une de ces valeurs sera utilisée. S'il n'y a pas de valeur en secondes, la valeur sera en jours. S'il n'y a pas de valeur en jours, la valeur sera par défaut est 14 jours.", + "online-cutoff": "Minutes après que l'utilisateur soit considéré comme inactif", + "online-cutoff-help": "Si l'utilisateur n'effectue aucune action pendant cette durée, il est considéré comme inactif et ne reçoit pas de mises à jour en temps réel.", + "registration": "Inscription des utilisateurs", + "registration-type": "Type d'inscription", + "registration-approval-type": "Type d'approbation d'inscription", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Approbation de administrateur", + "registration-type.admin-approval-ip": "Approbation de l'administrateur pour les adresses IP", + "registration-type.invite-only": "Uniquement sur invitation", + "registration-type.admin-invite-only": "Uniquement sur invitation d'un admin", + "registration-type.disabled": "Pas d'inscription", + "registration-type.help": "Normal - Les utilisateurs peuvent s'inscrire à partir de la page d'inscription.
\nInvitation uniquement - Les utilisateurs peuvent inviter d'autres personnes à partir de la page des utilisateurs.
\nInvitation administrateur uniquement - Seuls les administrateurs peuvent inviter d'autres personnes à partir des pages des utilisateurs et des pages d'administration.
\nAucune inscription - Aucune inscription d'utilisateur.
", + "registration-approval-type.help": "Normal - Les utilisateurs sont enregistrés immédiatement.
\nApprobation de l'administrateur - Les inscriptions des utilisateurs sont placées dans une file d'attente d'approbation pour les administrateurs.
\nApprobation par adresses IP pour les nouveaux utilisateurs, Approbation de l'administrateur pour les adresses IP ayant déjà un compte.
", + "registration-queue-auto-approve-time": "Durée d'approbation automatique", + "registration-queue-auto-approve-time-help": "Heures avant l'approbation automatique de l'utilisateur. 0 pour désactiver.", + "registration-queue-show-average-time": "Afficher aux utilisateurs le temps moyen nécessaire d'approbation", + "registration.max-invites": "Nombre maximum d'invitations par utilisateur", + "max-invites": "Nombre maximum d'invitations par utilisateur", + "max-invites-help": "0 pour supprimer cette restriction. Les admins n'ont aucune restriction
Valable uniquement pour \"Uniquement sur invitation\"", + "invite-expiration": "Expiration des invitations", + "invite-expiration-help": "nombre de jours avant que l'invitation n'expire.", + "min-username-length": "Longueur minimum du nom d'utilisateur", + "max-username-length": "Longueur maxmum du nom d'utilisateur", + "min-password-length": "Longueur minimum du mot de passe", + "min-password-strength": "Sécurité minimale du mot de passe", + "max-about-me-length": "Longueur maximum du À propos de moi", + "terms-of-use": "Conditions générales d'utilisation du forum (Laisser vide pour désactiver)", + "user-search": "Rechercher un utilisateur", + "user-search-results-per-page": "Nombre de résultats à afficher", + "default-user-settings": "Réglages par défaut des utilisateurs", + "show-email": "Afficher l'adresse e-mail", + "show-fullname": "Afficher le nom complet", + "restrict-chat": "Autoriser uniquement les discussions aux utilisateurs que je suis", + "outgoing-new-tab": "Ouvrir les liens sortants dans un nouvel onglet", + "topic-search": "Activer la recherche au sein des sujets", + "update-url-with-post-index": "Mettre à jour l'URL avec l'index des articles", + "digest-freq": "S'inscrire aux compte rendus", + "digest-freq.off": "Désactivé", + "digest-freq.daily": "Quotidien", + "digest-freq.weekly": "Hebdomadaire", + "digest-freq.biweekly": "Deux fois par semaine", + "digest-freq.monthly": "Mensuel", + "email-chat-notifs": "Envoyer un e-mail si un nouveau message de chat arrive lorsque je ne suis pas en ligne", + "email-post-notif": "Envoyer un email lors de réponses envoyées aux sujets auxquels que je suis", + "follow-created-topics": "S'abonner aux sujets que vous créez", + "follow-replied-topics": "S'abonner aux sujets auxquels vous répondez", + "default-notification-settings": "Paramètres des notifications par défaut", + "categoryWatchState": "Abonnement par défaut", + "categoryWatchState.watching": "Abonné", + "categoryWatchState.notwatching": "Non abonné", + "categoryWatchState.ignoring": "Ignoré" +} diff --git a/public/language/fr/admin/settings/web-crawler.json b/public/language/fr/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2c8c2fa771 --- /dev/null +++ b/public/language/fr/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Réglages de navigation", + "robots-txt": "Fichier Robots.txt personnalisé Laissez vide pour utiliser le fichier par défaut", + "sitemap-feed-settings": "Réglages du sitemap et du flux", + "disable-rss-feeds": "Désactiver les flux RSS", + "disable-sitemap-xml": "Désactiver le fichier sitemap.xml", + "sitemap-topics": "Nombre de sujets à afficher dans le sitemap", + "clear-sitemap-cache": "Effacer le cache du sitemap", + "view-sitemap": "Afficher le sitemap" +} \ No newline at end of file diff --git a/public/language/fr/category.json b/public/language/fr/category.json new file mode 100644 index 0000000000..d32a2a0f0c --- /dev/null +++ b/public/language/fr/category.json @@ -0,0 +1,23 @@ +{ + "category": "Catégorie", + "subcategories": "Sous-catégories", + "new_topic_button": "Nouveau sujet", + "guest-login-post": "Se connecter pour poster", + "no_topics": "Il n'y a aucun sujet dans cette catégorie.
Pourquoi ne pas en créer un ?", + "browsing": "parcouru par", + "no_replies": "Personne n'a répondu", + "no_new_posts": "Pas de nouveau message", + "watch": "S'abonner", + "ignore": "Ne plus surveiller", + "watching": "Suivi", + "not-watching": "Ne plus suivre", + "ignoring": "Ignoré", + "watching.description": "Afficher les sujets non lus et récents", + "not-watching.description": "Ne pas afficher les sujets non lus, afficher les récents", + "ignoring.description": "Ne pas afficher les sujets non lus et récents", + "watching.message": "Vous suivez les mises à jour de cette catégorie et de ses sous-catégories. ", + "notwatching.message": "Vous ne suivez aucunes mises à jour de cette catégorie et de ses sous-catégories.", + "ignoring.message": "Vous ignorez maintenant les mises à jour de cette catégorie et de toutes les sous-catégories.", + "watched-categories": "Catégories surveillées", + "x-more-categories": "%1 plus de catégories" +} \ No newline at end of file diff --git a/public/language/fr/email.json b/public/language/fr/email.json new file mode 100644 index 0000000000..21542e57d6 --- /dev/null +++ b/public/language/fr/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Email Test", + "password-reset-requested": "Demande de réinitialisation de mot de passe !", + "welcome-to": "Bienvenue sur %1", + "invite": "Invitation de %1", + "greeting_no_name": "Bonjour", + "greeting_with_name": "Bonjour %1", + "email.verify-your-email.subject": "Veuillez vérifier votre Email", + "email.verify.text1": "Vous avez demandé que nous modifiions ou confirmions votre adresse mail", + "email.verify.text2": "Pour des raisons de sécurité, nous ne modifions ou ne confirmons l'adresse mail enregistrée qu'une fois que sa propriété soit confirmée. Si vous ne l'avez pas demandé, aucune action n'est requise de votre part.", + "email.verify.text3": "Une fois que vous aurez confirmé ce mail, nous remplacerons votre adresse e-mail actuelle par celle-ci (%1).", + "welcome.text1": "Merci de vous être inscrit sur %1!", + "welcome.text2": "Pour activer totalement votre compte, nous devons vérifier que vous êtes bien propriétaire de l'adresse email que vous avez utilisé pour vous inscrire.", + "welcome.text3": "Un administrateur a accepté votre demande d'inscription. Vous pouvez maintenant vous connecter avec vos identifiants/mots de passe.", + "welcome.cta": "Cliquez ici pour confirmer votre adresse e-mail", + "invitation.text1": "%1 vous a invité à rejoindre %2", + "invitation.text2": "Votre invitation va expirer dans %1 jours.", + "invitation.cta": "Cliquez ici pour créer votre compte.", + "reset.text1": "Nous avons reçu une demande de réinitialisation de votre mot de passe, probablement parce que vous l'avez oublié. Si ce n'est pas le cas, veuillez ignorer cet email.", + "reset.text2": "Pour confirmer la réinitialisation de votre mot de passe, veuillez cliquer sur le lien suivant :", + "reset.cta": "Cliquez ici pour réinitialiser votre mot de passe", + "reset.notify.subject": "Mot de passe modifié", + "reset.notify.text1": "Nous vous informons que le %1, votre mot de passe a été modifié.", + "reset.notify.text2": "Si vous n'avez pas autorisé ceci, veuillez contacter immédiatement un administrateur.", + "digest.latest_topics": "Derniers sujets de %1 :", + "digest.top-topics": "Meilleurs sujets de %1", + "digest.popular-topics": "Sujets populaires de %1", + "digest.cta": "Cliquez ici pour aller sur %1", + "digest.unsub.info": "Ce message vous a été envoyé en raison de vos paramètres d'abonnement.", + "digest.day": "jour", + "digest.week": "semaine", + "digest.month": "mois", + "digest.subject": "Lettre d'activités de %1", + "digest.title.day": "Votre lettre d'activités quotidienne", + "digest.title.week": "Votre lettre d'activités hebdomadaire", + "digest.title.month": "Votre lettre d'activités mensuel", + "notif.chat.subject": "Nouveau message de chat reçu de %1", + "notif.chat.cta": "Cliquez ici pour continuer la conversation", + "notif.chat.unsub.info": "Cette notification de chat a été envoyé en raison de vos paramètres d'abonnement.", + "notif.post.unsub.info": "La notification de ce message vous a été envoyé en raison de vos paramètres d'abonnement.", + "notif.post.unsub.one-click": "Vous pouvez également vous désabonner des lettres d'activités, en cliquant sur", + "notif.cta": "Sur le forum", + "notif.cta-new-reply": "Voir les messages", + "notif.cta-new-chat": "Voir les discussions", + "notif.test.short": "Test des notifications", + "notif.test.long": "Ceci est un test de notifications de mail. Envoyez de l'aide!", + "test.text1": "Ceci est un e-mail de test pour vérifier que l'e-mailer est correctement configuré pour NodeBB.", + "unsub.cta": "Cliquez ici pour modifier ces paramètres", + "unsubscribe": "Se désinscrire", + "unsub.success": "Vous ne recevrez plus d'e-mails de la liste de diffusion %1", + "unsub.failure.title": "Impossible de se désinscrire", + "unsub.failure.message": "Malheureusement, nous n'avons pas pu vous désinscrire de la liste de diffusion, car il y avait un problème avec le lien. Cependant, vous pouvez modifier vos préférences de messagerie en accédant à vos paramètres utilisateur.

(erreur: %1)", + "banned.subject": "Vous avez été banni de %1", + "banned.text1": "L'utilisateur %1 a été banni de %2.", + "banned.text2": "Ce ban est effectif jusqu'au %1.", + "banned.text3": "Voici la raison pour laquelle vous avez été banni : ", + "closing": "Merci !" +} \ No newline at end of file diff --git a/public/language/fr/error.json b/public/language/fr/error.json new file mode 100644 index 0000000000..3d9027d164 --- /dev/null +++ b/public/language/fr/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Données invalides", + "invalid-json": "JSON invalide", + "wrong-parameter-type": "Une valeur de type %3 était attendue pour la propriété `%1`, mais %2 a été reçue à la place", + "required-parameters-missing": "Les paramètres requis étaient manquants dans cet appel d'API : %1", + "not-logged-in": "Vous ne semblez pas être connecté.", + "account-locked": "Votre compte a été temporairement suspendu", + "search-requires-login": "Rechercher nécessite d'avoir un compte. Veuillez vous identifier ou vous enregistrer.", + "goback": "Appuyez sur retour pour revenir à la page précédente", + "invalid-cid": "ID de catégorie invalide", + "invalid-tid": "ID de sujet invalide", + "invalid-pid": "ID de message invalide", + "invalid-uid": "ID d'utilisateur invalide", + "invalid-mid": "ID de message de discussion invalide", + "invalid-date": "Une date valide doit être fournie", + "invalid-username": "Nom d'utilisateur invalide", + "invalid-email": "Email invalide", + "invalid-fullname": "Nom erroné ", + "invalid-location": "Localisation erronée ", + "invalid-birthday": "Date d'anniversaire erronée", + "invalid-title": "Titre invalide", + "invalid-user-data": "Données utilisateur invalides", + "invalid-password": "Mot de passe invalide", + "invalid-login-credentials": "Certificat d'identification invalide", + "invalid-username-or-password": "Veuillez entrer un nom d'utilisateur et un mot de passe", + "invalid-search-term": "Données de recherche invalides", + "invalid-url": "URL invalide", + "invalid-event": "Événement non valide: %1", + "local-login-disabled": "Le système de connexion local a été désactivé pour les comptes sans privilèges.", + "csrf-invalid": "Nous ne pouvons pas vous connectez, probablement car votre session a expiré. Merci de réessayer.", + "invalid-path": "Chemin invalide", + "folder-exists": "Le dossier existe", + "invalid-pagination-value": "Valeur de pagination invalide. Celle-ci doit être comprise entre %1 et %2.", + "username-taken": "Nom d’utilisateur déjà utilisé", + "email-taken": "Email déjà utilisé", + "email-nochange": "Le mail saisi est déjà enregistré.", + "email-invited": "Cet utilisateur a déjà été invité.", + "email-not-confirmed": "La publication dans certaines catégories ou sujets sera activée après confirmation de e-mail, veuillez cliquer ici pour envoyer un e-mail de confirmation.", + "email-not-confirmed-chat": "Il ne vous est pas possible d'utiliser le chat tant que votre adresse email n'a pas été vérifiée. Veuillez cliquer ici pour confirmer votre adresse email.", + "email-not-confirmed-email-sent": "Votre email n'a pas encore été confirmé, veuillez vérifier votre boîte mail. Vous ne pourrez pas poster ou discuter avant que votre email ne soit confirmé.", + "no-email-to-confirm": "Votre compte n'a pas d'adresse mail définie. Un mail est nécessaire pour la récupération du compte. Veuillez cliquer ici pour entrer un courriel.", + "user-doesnt-have-email": "L'utilisateur « %1 » n'a pas d'adresse e-mail.", + "email-confirm-failed": "Votre adresse email n'a pas pu être vérifiée. Veuillez ré-essayer plus tard.", + "confirm-email-already-sent": "L'email de confirmation a déjà été envoyé. Veuillez attendre %1 minute(s) avant de redemander un nouvel envoi.", + "sendmail-not-found": "L'application d'envoi de mail est introuvable, assurez-vous qu'elle est installée et que l'utilisateur ayant démarré NodeBB ait des droits suffisants.", + "digest-not-enabled": "Les lettres d'activités de cet utilisateur ne sont pas activés ou la configuration système par défaut n’est pas configurée", + "username-too-short": "Nom d'utilisateur trop court", + "username-too-long": "Nom d'utilisateur trop long", + "password-too-long": "Mot de passe trop long", + "reset-rate-limited": "Trop de demandes de réinitialisation de mot de passe (demandes limitées)", + "reset-same-password": "Veuillez utiliser un mot de passe différent de votre mot de passe actuel", + "user-banned": "Utilisateur banni", + "user-banned-reason": "Désolé, ce compte a été banni (Raison : %1)", + "user-banned-reason-until": "Désolé, ce compte a été banni jusqu'au %1 (Raison : %2).", + "user-too-new": "Désolé, vous devez attendre encore %1 seconde(s) avant d'envoyer votre premier message", + "blacklisted-ip": "Désolé, votre adresse IP a été bannie de cette communauté. Si vous pensez que c'est une erreur, veuillez contacter un administrateur.", + "ban-expiry-missing": "Veuillez entrer une date de fin de banissement.", + "no-category": "Cette catégorie n'existe pas", + "no-topic": "Ce sujet n'existe pas", + "no-post": "Ce message n'existe pas", + "no-group": "Ce groupe n'existe pas", + "no-user": "Cet utilisateur n'existe pas", + "no-teaser": "L’aperçu n'existe pas", + "no-flag": "Le signalement n'existe pas", + "no-privileges": "Vous n'avez pas les privilèges nécessaires pour effectuer cette action.", + "category-disabled": "Catégorie désactivée", + "topic-locked": "Sujet verrouillé", + "post-edit-duration-expired": "Vous ne pouvez modifier un message que pendant %1 seconde(s) après l'avoir posté.", + "post-edit-duration-expired-minutes": "Vous ne pouvez éditer un message que pendant %1 minute(s) après l'avoir posté.", + "post-edit-duration-expired-minutes-seconds": "Vous ne pouvez éditer un message que pendant %1 minute(s) et %2 seconde(s) après l'avoir posté.", + "post-edit-duration-expired-hours": "Vous ne pouvez éditer un message que pendant %1 heure(s) après l'avoir posté.", + "post-edit-duration-expired-hours-minutes": "Vous ne pouvez éditer un message que pendant %1 heure(s) et %2 minute(s) après l'avoir posté.", + "post-edit-duration-expired-days": "Vous ne pouvez éditer un message que pendant %1 jours(s) après l'avoir posté.", + "post-edit-duration-expired-days-hours": "Vous ne pouvez éditer un message que pendant %1 jours(s) et %2 heures(s) après l'avoir posté.", + "post-delete-duration-expired": "Vous ne pouvez supprimer un message que pendant %1 seconde(s) après l'avoir posté.", + "post-delete-duration-expired-minutes": "Vous ne pouvez supprimer un message que pendant %1 minute(s) après l'avoir posté.", + "post-delete-duration-expired-minutes-seconds": "Vous ne pouvez supprimer un message que pendant %1 minute(s) et %2 seconde(s) après l'avoir posté.", + "post-delete-duration-expired-hours": "Vous ne pouvez supprimer un message que pendant %1 heure(s) après l'avoir posté.", + "post-delete-duration-expired-hours-minutes": "Vous ne pouvez supprimer un message que pendant %1 heure(s) et %2 minute(s) après l'avoir posté.", + "post-delete-duration-expired-days": "Vous ne pouvez supprimer un message que pendant %1 jour(s) après l'avoir posté.", + "post-delete-duration-expired-days-hours": "Vous ne pouvez supprimer un message que pendant %1 jour(s) et %2 heure(s) après l'avoir posté.", + "cant-delete-topic-has-reply": "Vous ne pouvez pas supprimer votre sujet s'il a au moins une réponse.", + "cant-delete-topic-has-replies": "Vous ne pouvez pas supprimer votre sujet s'il a au moins %1 réponses.", + "content-too-short": "Veuillez entrer un message plus long. Les messages doivent contenir au moins %1 caractère(s).", + "content-too-long": "Veuillez poster un message plus court. Les messages ne peuvent être plus long que %1 caractère(s).", + "title-too-short": "Veuillez entrer un titre plus long. Les titres doivent contenir au moins %1 caractère(s).", + "title-too-long": "Veuillez entrer un titre plus court. Les titres ne peuvent excéder %1 caractère(s).", + "category-not-selected": "Aucune catégorie sélectionnée", + "too-many-posts": "Vous ne pouvez poster que toutes les %1 seconde(s) - merci de patienter avant de publier à nouveau.", + "too-many-posts-newbie": "En tant que nouvel utilisateur, vous ne pouvez poster que toutes les %1 seconde(s) jusqu'à ce que vous obteniez une réputation de %2 - patientez avant de publier de nouveau.", + "already-posting": "You are already posting", + "tag-too-short": "Veuillez entrer un mot-clé plus long. Les mots-clés doivent contenir au moins %1 caractère(s).", + "tag-too-long": "Veuillez entrer un mot-clé plus court. Les mot-clés ne peuvent excéder %1 caractère(s).", + "not-enough-tags": "Pas assez de mots-clés. Les sujets doivent avoir au moins %1 mots-clé(s).", + "too-many-tags": "Trop de mots-clés. Les sujets ne peuvent avoir au plus que %1 mots-clé(s).", + "cant-use-system-tag": "Vous ne pouvez gérer les mots-clés.", + "cant-remove-system-tag": "Vous ne pouvez supprimer ces mots-clés.", + "still-uploading": "Veuillez patienter pendant l'envoi des fichiers.", + "file-too-big": "La taille maximale autorisée pour un fichier est de %1 ko. Veuillez envoyer un fichier plus petit.", + "guest-upload-disabled": "L'envoi de fichiers a été désactivé pour les invités", + "cors-error": "Impossible d'envoyer l'image en raison d'une erreur de configuration CORS", + "upload-ratelimit-reached": "Vous avez envoyé trop de fichiers à la fois. Veuillez réessayer plus tard.", + "scheduling-to-past": "Veuillez sélectionner une date ultérieure.", + "invalid-schedule-date": "Veuillez saisir une date et une heure valide.", + "cant-pin-scheduled": "Les sujets planifiés ne peuvent pas être (dé)épinglés.", + "cant-merge-scheduled": "Les sujets planifiés ne peuvent pas être fusionnés.", + "cant-move-posts-to-scheduled": "Impossible de déplacer les messages vers un sujet planifié.", + "cant-move-from-scheduled-to-existing": "Impossible de déplacer les publications d'un sujet planifié vers un sujet existant.", + "already-bookmarked": "Vous avez déjà mis un marque-page", + "already-unbookmarked": "Vous avez déjà retiré un marque-page", + "cant-ban-other-admins": "Vous ne pouvez pas bannir les autres administrateurs !", + "cant-mute-other-admins": "Vous ne pouvez pas désactiver les autres administrateurs !", + "user-muted-for-hours": "Vous avez été mis en silencieux, vous pourrez publier dans %1 heure(s)", + "user-muted-for-minutes": "Vous avez été mis en silencieux, vous pourrez publier dans %1 minute(s)", + "cant-make-banned-users-admin": "Vous ne pouvez pas mettre des utilisateurs bannis en administrateur.", + "cant-remove-last-admin": "Vous êtes le seul administrateur. Ajoutez un autre utilisateur en tant qu'administrateur avant de vous retirer.", + "account-deletion-disabled": "La suppression du compte est désactivée", + "cant-delete-admin": "Veuillez retirer les droits d'administration de ce compte avant de tenter de le supprimer.", + "already-deleting": "Déjà supprimé", + "invalid-image": "Image invalide", + "invalid-image-type": "Type d'image invalide. Les types autorisés sont: %1", + "invalid-image-extension": "Extension d'image invalide", + "invalid-file-type": "Type de fichier non valide. Les types autorisés sont : %1", + "invalid-image-dimensions": "Les dimensions des images sont trop grandes", + "group-name-too-short": "Nom de groupe trop court", + "group-name-too-long": "Nom du groupe trop long", + "group-already-exists": "Ce groupe existe déjà", + "group-name-change-not-allowed": "Modification du nom de groupe non permise", + "group-already-member": "Déjà membre du groupe", + "group-not-member": "Pas un membre de ce groupe", + "group-needs-owner": "Ce groupe nécessite au moins un propriétaire", + "group-already-invited": "Cet utilisateur a déjà été invité.", + "group-already-requested": "Votre demande d'adhésion a déjà été envoyée.", + "group-join-disabled": "Vous ne pouvez pas rejoindre ce groupe pour le moment.", + "group-leave-disabled": "Vous ne pouvez pas quitter ce groupe pour le moment.", + "post-already-deleted": "Message déjà supprimé", + "post-already-restored": "Message déjà restauré", + "topic-already-deleted": "Sujet déjà supprimé", + "topic-already-restored": "Sujet déjà restauré", + "cant-purge-main-post": "Il n'est pas possible d'effacer le message principal, veuillez supprimer le sujet entier à la place.", + "topic-thumbnails-are-disabled": "Les miniatures de sujet sont désactivés", + "invalid-file": "Fichier invalide", + "uploads-are-disabled": "Les envois sont désactivés", + "signature-too-long": "La signature ne peut dépasser %1 caractère(s).", + "about-me-too-long": "Votre texte \"à propos de moi\" ne peut dépasser %1 caractère(s).", + "cant-chat-with-yourself": "Vous ne pouvez discuter avec vous-même !", + "chat-restricted": "Cet utilisateur a restreint ses messages de chat. Il doit d'abord s'abonner à votre compte avant que vous puissiez discuter avec lui.", + "chat-disabled": "Système de chat désactivé", + "too-many-messages": "Vous avez envoyé trop de messages, veuillez patienter un instant.", + "invalid-chat-message": "Message de chat invalide", + "chat-message-too-long": "Les messages de discussion ne peuvent pas être plus longs que %1 caractères.", + "cant-edit-chat-message": "Vous n'avez pas l'autorisation de modifier ce message", + "cant-delete-chat-message": "Vous n'avez pas l'autorisation de supprimer ce message", + "chat-edit-duration-expired": "Vous n'êtes autorisé à modifier des messages que pendant %1 seconde(s) après les avoir postés", + "chat-delete-duration-expired": "Vous n'êtes autorisé à supprimer des messages que pendant %1 seconde(s) après les avoir postés", + "chat-deleted-already": "Ce message a déjà été supprimé.", + "chat-restored-already": "Ce message de discussion a déjà été restauré.", + "chat-room-does-not-exist": "Le salon de discussion n'existe pas.", + "already-voting-for-this-post": "Vous avez déjà voté pour ce message.", + "reputation-system-disabled": "Le système de réputation est désactivé", + "downvoting-disabled": "Les votes négatifs ne sont pas autorisés", + "not-enough-reputation-to-chat": "Vous avez besoin de %1 réputation pour signaler", + "not-enough-reputation-to-upvote": "Vous avez besoin de %1 réputation pour voter", + "not-enough-reputation-to-downvote": "Vous avez besoin de %1 réputation pour voter", + "not-enough-reputation-to-flag": "Vous avez besoin de %1 réputation pour faire un signalement", + "not-enough-reputation-min-rep-website": "Vous avez besoin de %1 réputation pour ajouter un site Web", + "not-enough-reputation-min-rep-aboutme": "Vous avez besoin de %1 réputation pour ajouter à propos de moi", + "not-enough-reputation-min-rep-signature": "Vous avez besoin de %1 réputation pour ajouter une signature", + "not-enough-reputation-min-rep-profile-picture": "Vous avez besoin de %1 réputation pour ajouter une photo de profil", + "not-enough-reputation-min-rep-cover-picture": "Vous avez besoin de %1 réputation pour ajouter une image de couverture", + "post-already-flagged": "Vous avez déjà signalé ce message", + "user-already-flagged": "Vous avez déjà signalé cet utilisateur", + "post-flagged-too-many-times": "Ce message a déjà été signalé par d'autres", + "user-flagged-too-many-times": "Cet utilisateur a déjà été signalé par d'autres", + "cant-flag-privileged": "Vous n'êtes pas autorisé à signaler les profils ou le contenu des utilisateurs privilégiés (modérateurs / modérateurs globaux / administrateurs)", + "self-vote": "Vous ne pouvez pas voter sur votre propre message", + "too-many-upvotes-today": "Vous ne pouvez voter %1 fois par jour", + "too-many-upvotes-today-user": "Vous ne pouvez voter pour un utilisateur %1 fois par jour", + "too-many-downvotes-today": "Vous ne pouvez noter négativement que %1 fois par jour", + "too-many-downvotes-today-user": "Vous ne pouvez noter négativement un utilisateur que %1 fois par jour", + "reload-failed": "NodeBB a rencontré un problème lors du rechargement : \"%1\" . NodeBB continuera de fonctionner côté client, même si vous devriez annuler ce que vous avez fait juste avant de recharger.", + "registration-error": "Erreur d'enregistrement", + "parse-error": "Une erreur est survenue en analysant la réponse du serveur", + "wrong-login-type-email": "Veuillez utiliser votre adresse email pour vous connecter", + "wrong-login-type-username": "Veuillez utiliser votre identifiant pour vous connecter", + "sso-registration-disabled": "L'enregistrement a été désactivé pour les comptes %1, merci de vous enregistrer avec une adresse mail avant", + "sso-multiple-association": "Vous ne pouvez pas associer plusieurs comptes de ce service à votre compte NodeBB. Veuillez dissocier votre compte existant et réessayer.", + "invite-maximum-met": "Vous avez invité la quantité maximale de personnes (%1 sur %2).", + "no-session-found": "Pas de session de connexion trouvée !", + "not-in-room": "L'utilisateur n'est pas dans cette salle", + "cant-kick-self": "Vous ne pouvez pas vous exclure vous-même du groupe", + "no-users-selected": "Aucun utilisateur sélectionné", + "invalid-home-page-route": "Chemin vers la page d'accueil invalide", + "invalid-session": "Session Invalide", + "invalid-session-text": "Il semblerait que votre session de connexion ne soit plus active. Merci de rafraîchir cette page.", + "session-mismatch": "Session Interrompue", + "session-mismatch-text": "Il semble que votre session ne soit plus active, ou que le serveur ne la reconnaisse plus. Merci de rafraîchir cette page.", + "no-topics-selected": "Aucun sujet sélectionné !", + "cant-move-to-same-topic": "Impossible de déplacer le message dans le même sujet !", + "cant-move-topic-to-same-category": "Impossible de déplacer le sujet dans la même catégorie !", + "cannot-block-self": "Vous ne pouvez pas vous bloquer !", + "cannot-block-privileged": "Vous ne pouvez pas bloquer les administrateurs ou les modérateurs global", + "cannot-block-guest": "Les Invités ne peuvent pas bloquer d'autres utilisateurs", + "already-blocked": "Cet utilisateur est déjà bloqué", + "already-unblocked": "Cet utilisateur est déjà débloqué", + "no-connection": "Il semble y avoir un problème avec votre connexion Internet", + "socket-reconnect-failed": "Serveur inaccessible pour le moment. Cliquez ici pour réessayer ou réessayez plus tard", + "plugin-not-whitelisted": "Impossible d'installer le plug-in – seuls les plugins mis en liste blanche dans le gestionnaire de packages NodeBB peuvent être installés via l'ACP", + "plugins-set-in-configuration": "Vous n'êtes pas autorisé à modifier l'état des plugins car ils sont définis au moment de l'exécution (config.json, variables d'environnement ou arguments de terminal), veuillez plutôt modifier la configuration.", + "theme-not-set-in-configuration": "Lors de la définition des plugins actifs, le changement de thèmes nécessite d'ajouter le nouveau thème à la liste des plugins actifs avant de le mettre à jour dans l'ACP", + "topic-event-unrecognized": "Événement du sujet '%1' non reconnu", + "cant-set-child-as-parent": "Cette catégorie ne peut être une catégorie principale", + "cant-set-self-as-parent": "Ne peut être définie comme catégorie principale", + "api.master-token-no-uid": "Un jeton principal a été reçu sans `_uid` correspondant dans le corps de la requête", + "api.400": "Quelque chose n'allait pas avec la requête que vous avez transmise.", + "api.401": "Aucune session de connexion valide trouvée. Veuillez vous connecter et réessayer.", + "api.403": "Vous n'êtes pas autorisé à réaliser cet appel", + "api.404": "Appel de l'API non valide", + "api.426": "HTTPS est requis pour les demandes d’écriture via l'API, veuillez renvoyer votre demande via HTTPS", + "api.429": "Vous avez fait trop de demandes, veuillez réessayer plus tard", + "api.500": "Une erreur inattendue s'est produite lors de la tentative de traitement de votre demande.", + "api.501": "L'accès n'est pas encore fonctionnel, veuillez réessayer demain", + "api.503": "L'accès n'est pas disponible actuellement en raison d'une configuration de serveur" +} \ No newline at end of file diff --git a/public/language/fr/flags.json b/public/language/fr/flags.json new file mode 100644 index 0000000000..b19944d108 --- /dev/null +++ b/public/language/fr/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Etat", + "reports": "Rapports", + "first-reported": "Premier rapport", + "no-flags": "Excellent ! Aucun signalement trouvé.", + "assignee": "Assigné", + "update": "Mettre à jour", + "updated": "Mis à jour", + "resolved": "Résolu", + "target-purged": "Le rapport pour ce signalement a été supprimé et n'est plus accessible", + + "graph-label": "Signalements du jour", + "quick-filters": "Filtres rapides", + "filter-active": "Il y a un ou plusieurs filtres actifs dans cette liste de signalements", + "filter-reset": "Supprimer les filtres", + "filters": "Options de filtre", + "filter-reporterId": "UID du reporteur", + "filter-targetUid": "UID signalé", + "filter-type": "Type de signalement", + "filter-type-all": "Tout le contenu", + "filter-type-post": "Message", + "filter-type-user": "Utilisateur", + "filter-state": "Etat", + "filter-assignee": "UID assigné", + "filter-cid": "Catégorie", + "filter-quick-mine": "Assigné à moi", + "filter-cid-all": "Toutes les catégories", + "apply-filters": "Appliquer les filtres", + "more-filters": "Plus de filtres", + "fewer-filters": "Enlever les filtres", + + "quick-actions": "Actions Rapide", + "flagged-user": "Utilisateurs signalés", + "view-profile": "Voir le profil", + "start-new-chat": "Démarrer un nouveau Chat", + "go-to-target": "Voir le signalement cible", + "assign-to-me": "Me l'assigner", + "delete-post": "Supprimer les messages", + "purge-post": "Supprimer définitivement", + "restore-post": "Restaurer les messages", + "delete": "Supprimer le signalement", + + "user-view": "Voir le profil", + "user-edit": "Éditer le profil", + + "notes": "Notes de signalement", + "add-note": "Ajouter une note", + "no-notes": "aucune note partagée", + "delete-note-confirm": "Êtes-vous sûr de bien vouloir supprimer cette note de signalement ?", + "delete-flag-confirm": "Êtes-vous sûr de bien vouloir supprimer ce signalement ?", + "note-added": "Note ajoutée", + "note-deleted": "Note supprimée", + "flag-deleted": "Signalement supprimé", + + "history": "Compte & Historique des signalements", + "no-history": "Aucun historique de signalements", + + "state-all": "Tous les états", + "state-open": "Nouveau/Ouvert", + "state-wip": "En cours", + "state-resolved": "Résolu", + "state-rejected": "Rejeté", + "no-assignee": "Non assigné", + + "sort": "Trier par", + "sort-newest": "Le plus récent", + "sort-oldest": "Le plus ancien", + "sort-reports": "Rapports", + "sort-all": "Tous les signalements...", + "sort-posts-only": "Messages seulement...", + "sort-downvotes": "Demandes négatives", + "sort-upvotes": "Demandes positives", + "sort-replies": "Réponses", + + "modal-title": "Contenu du rapport", + "modal-body": "Veuillez saisir le motif de votre signalement pour %1 %2 et valider à l'aide du bouton soumettre ci-dessous.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Choquant", + "modal-reason-other": "Autre (précisez ci-dessous)", + "modal-reason-custom": "Motif du signalement...", + "modal-submit": "Soumettre", + "modal-submit-success": "Le contenu a été soumis pour examen.", + + "bulk-actions": "Actions", + "bulk-resolve": "Signalement(s) résolu(s)", + "bulk-success": "%1 signalement mis à jour", + "flagged-timeago-readable": "Signalé (%2)", + "auto-flagged": "[Auto Signalement] A reçu %1 votes négatifs." +} \ No newline at end of file diff --git a/public/language/fr/global.json b/public/language/fr/global.json new file mode 100644 index 0000000000..6c94b1bd53 --- /dev/null +++ b/public/language/fr/global.json @@ -0,0 +1,126 @@ +{ + "home": "Accueil", + "search": "Recherche", + "buttons.close": "Fermer", + "403.title": "Accès refusé", + "403.message": "Il semble que vous ayez atteint une page à laquelle vous n'avez pas accès.", + "403.login": "Peut-être deviez vous essayer de vous connecter?", + "404.title": "Introuvable", + "404.message": "Il semble que vous ayez atteint une page qui n'existe pas. Retourner à la page d'accueil.", + "500.title": "Erreur Interne.", + "500.message": "Oops ! Il semblerait que quelque chose se soit mal passé !", + "400.title": "Requête erronée.", + "400.message": "Il semble que ce lien ne soit pas correct, merci de le vérifier. Sinon, retournez à la page d'accueil.", + "register": "S'inscrire", + "login": "Se connecter", + "please_log_in": "Veuillez vous connecter", + "logout": "Déconnexion", + "posting_restriction_info": "L'envoi de messages est réservé aux membres inscrits, cliquez ici pour vous connecter.", + "welcome_back": "Bienvenue", + "you_have_successfully_logged_in": "Vous vous êtes bien connecté", + "save_changes": "Enregistrer les changements", + "save": "Enregistrer", + "close": "Fermer", + "pagination": "Pagination", + "pagination.out_of": "%1 sur %2", + "pagination.enter_index": "Aller à l'index des messages", + "header.admin": "Admin", + "header.categories": "Catégories", + "header.recent": "Récent", + "header.unread": "Non lus", + "header.tags": "Mots-clés", + "header.popular": "Populaire", + "header.top": "Haut", + "header.users": "Utilisateurs", + "header.groups": "Groupes", + "header.chats": "Discussions", + "header.notifications": "Notifications", + "header.search": "Recherche", + "header.profile": "Profil", + "header.navigation": "Navigation", + "notifications.loading": "Chargement des notifications", + "chats.loading": "Chargement des discussions", + "motd.welcome": "Bienvenue sur NodeBB, la plate-forme de discussion du futur.", + "previouspage": "Page précédente", + "nextpage": "Page suivante", + "alert.success": "Succès", + "alert.error": "Erreur", + "alert.banned": "Bannis", + "alert.banned.message": "Vous venez d'être banni, votre accès est désormais restreint.", + "alert.unbanned": "Dé-banni", + "alert.unbanned.message": "Votre bannissement a été levé.", + "alert.unfollow": "Vous n'êtes plus abonné à %1 !", + "alert.follow": "Vous êtes désormais abonné à %1 !", + "users": "Utilisateurs", + "topics": "Sujets", + "posts": "Messages", + "x-posts": "%1 messages", + "best": "Meilleur sujets", + "controversial": "Contesté", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Votants", + "upvoters": "Votes positifs", + "upvoted": "Vote(s) positif(s)", + "downvoters": "Votes contre", + "downvoted": "Vote(s) négatif(s)", + "views": "Vues", + "posters": "Publieurs", + "reputation": "Réputation", + "lastpost": "Dernier message", + "firstpost": "Premier message", + "read_more": "En lire plus", + "more": "Plus", + "none": "Aucun", + "posted_ago_by_guest": "posté %1 par un invité", + "posted_ago_by": "posté %1 par %2", + "posted_ago": "posté %1", + "posted_in": "posté dans %1", + "posted_in_by": "posté dans %1 par %2", + "posted_in_ago": "posté dans %1 %2", + "posted_in_ago_by": "posté dans %1 %2 par %3", + "user_posted_ago": "%1 a posté %2", + "guest_posted_ago": "Un invité a posté %1", + "last_edited_by": "dernière édition par %1", + "norecentposts": "Aucun message récent", + "norecenttopics": "Aucun sujet récent", + "recentposts": "Messages récents", + "recentips": "Adresses IP récemment enregistées", + "moderator_tools": "Outils de modération", + "online": "En ligne", + "away": "Absent", + "dnd": "Occupé", + "invisible": "Invisible", + "offline": "Hors-ligne", + "email": "Email", + "language": "Langue", + "guest": "Invité", + "guests": "Invités", + "former_user": "Un Ancien Utilisateur", + "system-user": "Système", + "unknown-user": "Utilisateur Inconnu", + "updated.title": "Forum mis à jour", + "updated.message": "Ce forum a été mis à jour à la dernière version. Cliquez ici pour recharger la page.", + "privacy": "Vie privée", + "follow": "S'abonner", + "unfollow": "Se désabonner", + "delete_all": "Tout supprimer", + "map": "Carte", + "sessions": "Sessions de connexion", + "ip_address": "Adresse IP", + "enter_page_number": "Entrer un numéro de page", + "upload_file": "Envoyer un fichier", + "upload": "Envoyer", + "uploads": "Fichiers envoyés", + "allowed-file-types": "Les types de fichiers autorisés sont : %1", + "unsaved-changes": "Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir naviguer tout de même ?", + "reconnecting-message": "Il semble que votre connexion ait été perdue, veuillez patienter pendant que nous vous re-connectons.", + "play": "Lire", + "cookies.message": "Ce site utilise des cookies pour vous permettre d'avoir la meilleure expérience possible.", + "cookies.accept": "Compris !", + "cookies.learn_more": "En savoir plus", + "edited": "Modifié", + "disabled": "Désactivé", + "select": "Sélectionner", + "user-search-prompt": "Écrivez ici pour rechercher des utilisateurs ..." +} \ No newline at end of file diff --git a/public/language/fr/groups.json b/public/language/fr/groups.json new file mode 100644 index 0000000000..afae347ba1 --- /dev/null +++ b/public/language/fr/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Groupes", + "view_group": "Voir le groupe", + "owner": "Propriétaire du groupe", + "new_group": "Créer un nouveau groupe", + "no_groups_found": "Il n'y a aucun groupe", + "pending.accept": "Accepter", + "pending.reject": "Refuser", + "pending.accept_all": "Tout accepter", + "pending.reject_all": "Tout rejeter", + "pending.none": "Il n'y a aucun membre en attente pour le moment", + "invited.none": "Il n'y a aucun membre invité pour le moment", + "invited.uninvite": "Résilier l'invitation", + "invited.search": "Chercher un utilisateur a inviter dans ce groupe", + "invited.notification_title": "Vous avez été invité à rejoindre %1", + "request.notification_title": "Requête d'Adhésion au Groupe de %1", + "request.notification_text": "%1 a demandé à devenir membre de %2", + "cover-save": "Enregistrer", + "cover-saving": "Enregistrement", + "details.title": "Informations du groupe", + "details.members": "Liste des membres", + "details.pending": "Membres en attente", + "details.invited": "Inviter des Membres", + "details.has_no_posts": "Les membres de ce groupe n'ont envoyé aucun message.", + "details.latest_posts": "Derniers messages", + "details.private": "Privé", + "details.disableJoinRequests": "Désactiver les demandes d'adhésion", + "details.disableLeave": "Interdire aux utilisateurs de quitter le groupe", + "details.grant": "Promouvoir/rétrograder comme propriétaire", + "details.kick": "Exclure", + "details.kick_confirm": "Voulez-vous vraiment supprimer ce membre du groupe ?", + "details.add-member": "Ajouter un membre", + "details.owner_options": "Administration du groupe", + "details.group_name": "Nom du groupe", + "details.member_count": "Nombre de membres", + "details.creation_date": "Date de création", + "details.description": "Description", + "details.member-post-cids": "ID de la catégorie à partir de laquelle seront affichées les publications", + "details.badge_preview": "Aperçu du badge", + "details.change_icon": "Modifier l'icône", + "details.change_label_colour": "Changer la couleur de l'étiquette", + "details.change_text_colour": "Changer la couleur du texte", + "details.badge_text": "Texte du badge", + "details.userTitleEnabled": "Afficher le badge", + "details.private_help": "Si cette case est cochée, rejoindre un groupe nécessite l'accord d'un propriétaire du groupe.", + "details.hidden": "Masqué", + "details.hidden_help": "Si cette case est cochée, ce groupe n'est pas affiché dans la liste des groupes, et les utilisateurs devront être invités manuellement.", + "details.delete_group": "Supprimer le groupe", + "details.private_system_help": "Les groupes privés sont désactivés au niveau du système, cette option ne déclenche rien", + "event.updated": "Les détails du groupe ont été mis à jour", + "event.deleted": "Le groupe \"%1\" a été supprimé", + "membership.accept-invitation": "Accepter l'invitation", + "membership.accept.notification_title": "Vous êtes maintenant membre de %1", + "membership.invitation-pending": "Invitation en attente", + "membership.join-group": "Rejoindre le groupe", + "membership.leave-group": "Quitter le groupe", + "membership.leave.notification_title": "%1 a quitté le groupe %2", + "membership.reject": "Refuser", + "new-group.group_name": "Nom du groupe :", + "upload-group-cover": "Envoyer une image de groupe", + "bulk-invite-instructions": "Entrez une liste de nom d'utilisateurs séparés par des virgules pour les inviter à rejoindre ce groupe.", + "bulk-invite": "Invitation multiple", + "remove_group_cover_confirm": "Êtes-vous sûr de vouloir supprimer l'image de couverture ?" +} \ No newline at end of file diff --git a/public/language/fr/ip-blacklist.json b/public/language/fr/ip-blacklist.json new file mode 100644 index 0000000000..bf87222b0c --- /dev/null +++ b/public/language/fr/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configurez votre liste noire ici.", + "description": "Quelques fois, bannir un utilisateur ne suffit pas. Restreindre l'accès au forum à une adresse IP ou à un ensemble d'adresses IP peut être le meilleur moyen de protection. Dans ce cas, vous pouvez ajouter les adresses IP problématiques ou des plages d'adresses à cette liste noire, ce qui empêchera l'identification et la création de nouveau compte.", + "active-rules": "Règles actives", + "validate": "Valider la liste noire", + "apply": "Appliquer la liste noire", + "hints": "Astuces de syntaxe", + "hint-1": "Définissez une seule adresse IP par ligne. Vous pouvez ajouter des blocs IP, du moment qu'ils respectent le format CIDR (par ex. 192.168.100.0/22).", + "hint-2": "Vous pouvez ajouter en commentaire en commençant la ligne par le symbole #.", + + "validate.x-valid": "%1 sur %2 règle(s) valide(s).", + "validate.x-invalid": "Les règles suivantes %1 sont invalides:", + + "alerts.applied-success": "Liste noire appliquée", + + "analytics.blacklist-hourly": "Image 1 – Nombre de visites de la liste noire par heure", + "analytics.blacklist-daily": "Image 2 – Nombre de visites de la liste noire par jour", + "ip-banned": "IP bannies" +} \ No newline at end of file diff --git a/public/language/fr/language.json b/public/language/fr/language.json new file mode 100644 index 0000000000..7f25df4ec3 --- /dev/null +++ b/public/language/fr/language.json @@ -0,0 +1,5 @@ +{ + "name": "French (France)", + "code": "fr", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/fr/login.json b/public/language/fr/login.json new file mode 100644 index 0000000000..330bba51ed --- /dev/null +++ b/public/language/fr/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Identifiant ou email", + "username": "Identifiant", + "remember_me": "Se souvenir de moi ?", + "forgot_password": "Mot de passe oublié ?", + "alternative_logins": "Autres méthodes de connexion", + "failed_login_attempt": "Identification échouée", + "login_successful": "Vous êtes maintenant connecté !", + "dont_have_account": "Vous n'avez pas de compte ?", + "logged-out-due-to-inactivity": "Vous avez été déconnecté du Tableau de bord en raison de votre inactivité", + "caps-lock-enabled": "Le verrouillage des majuscules est activé" +} \ No newline at end of file diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json new file mode 100644 index 0000000000..3778a0ec49 --- /dev/null +++ b/public/language/fr/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Discuter avec", + "chat.placeholder": "Écrivez vos message ici, faites glisser / déposez les images, validez sur entrée pour envoyer", + "chat.scroll-up-alert": "Vous consultez des messages plus anciens, cliquez ici pour accéder au message le plus récent.", + "chat.send": "Envoyer", + "chat.no_active": "Vous n'avez aucune discussion en cours.", + "chat.user_typing": "%1 est en train d'écrire ...", + "chat.user_has_messaged_you": "%1 vous a envoyé un message.", + "chat.see_all": "Tous les chats", + "chat.mark_all_read": "Marquez tous comme lu", + "chat.no-messages": "Veuillez sélectionner un destinataire pour voir l'historique des discussions", + "chat.no-users-in-room": "Aucun participant à cette discussion", + "chat.recent-chats": "Discussions récentes", + "chat.contacts": "Contacts", + "chat.message-history": "Historique des messages", + "chat.message-deleted": "Message supprimé", + "chat.options": "Options", + "chat.pop-out": "Afficher la discussion", + "chat.minimize": "Réduire", + "chat.maximize": "Agrandir", + "chat.seven_days": "7 Jours", + "chat.thirty_days": "30 Jours", + "chat.three_months": "3 Mois", + "chat.delete_message_confirm": "Êtes-vous sûr de vouloir supprimer ce message ?", + "chat.retrieving-users": "Ajouter des utilisateurs ...", + "chat.manage-room": "Gérer l'espace de discussion", + "chat.add-user-help": "Rechercher des utilisateurs ici. Lorsque cette option est sélectionnée, l'utilisateur sera ajouté à l'espace de discussion. Le nouvel utilisateur ne pourra pas visualiser les échanges avant d'être ajoutés à la conversation. Seuls les propriétaires de l'espace de discussion () peuvent supprimer des utilisateurs.", + "chat.confirm-chat-with-dnd-user": "Cet utilisateur a son statut en mode \"Ne pas déranger\". Voulez-vous quand même discuter avec lui ?", + "chat.rename-room": "Renommer l'espace de discussion ", + "chat.rename-placeholder": "Entrer le nom ici ", + "chat.rename-help": "Le nom de l'espace de discussion défini ici sera visible par tous les participants à la discussion.", + "chat.leave": "Quitter la discussion", + "chat.leave-prompt": "Êtes-vous sûr de vouloir quitter la discussion ?", + "chat.leave-help": "Si vous quittez vous ne pourrez plus suivre la discussion. Si vous êtes de nouveau ajouté, vous ne verrez aucun historique de la discussion avant votre réintégration.", + "chat.in-room": "Dans cet espace de discussion", + "chat.kick": "Exclure", + "chat.show-ip": "Voir IP", + "chat.owner": "Espace Admin", + "chat.system.user-join": "%1 a rejoint la discussion", + "chat.system.user-leave": "%1 a quitté la discussion", + "chat.system.room-rename": "%2 a renommé la discussion: %1", + "composer.compose": "Écrire", + "composer.show_preview": "Afficher l'aperçu", + "composer.hide_preview": "Masquer l'aperçu", + "composer.user_said_in": "%1 a dit dans %2 :", + "composer.user_said": "%1 a dit :", + "composer.discard": "Êtes-vous sûr de bien vouloir supprimer ce message ?", + "composer.submit_and_lock": "Envoyer et verrouiller", + "composer.toggle_dropdown": "Afficher/masquer le menu", + "composer.uploading": "Envoi en cours %1", + "composer.formatting.bold": "Gras", + "composer.formatting.italic": "Italique", + "composer.formatting.list": "Liste", + "composer.formatting.strikethrough": "Barré", + "composer.formatting.code": "Code", + "composer.formatting.link": "Lien", + "composer.formatting.picture": "Lien d'image", + "composer.upload-picture": "Envoyer une image", + "composer.upload-file": "Envoyer un fichier", + "composer.zen_mode": "Mode Zen", + "composer.select_category": "Sélectionnez une catégorie", + "composer.textarea.placeholder": "Saisissez le contenu de votre message ici, faites glisser et déposez les images", + "composer.schedule-for": "Planifier le sujet pour", + "composer.schedule-date": "Date", + "composer.schedule-time": "Heure", + "composer.cancel-scheduling": "Annuler la planification", + "composer.set-schedule-date": "Régler la date", + "bootbox.ok": "OK", + "bootbox.cancel": "Annuler", + "bootbox.confirm": "Confirmer", + "bootbox.submit": "Envoyer", + "bootbox.send": "Envoyer", + "cover.dragging_title": "Positionnement de la photo de couverture", + "cover.dragging_message": "Déplacez la photo de couverture à la position désiré et cliquez sur \"Enregistrer\"", + "cover.saved": "Photo de couverture et position sauvegardé. ", + "thumbs.modal.title": "Gérer les vignettes des sujets", + "thumbs.modal.no-thumbs": "Aucune vignette trouvée.", + "thumbs.modal.resize-note": "Remarque: ce forum est configuré pour redimensionner les vignettes des sujets jusqu'à une largeur maximale de %1px", + "thumbs.modal.add": "Ajouter une vignette", + "thumbs.modal.remove": "Supprimer une vignette", + "thumbs.modal.confirm-remove": "Voulez-vous vraiment supprimer cette vignette ?" +} \ No newline at end of file diff --git a/public/language/fr/notifications.json b/public/language/fr/notifications.json new file mode 100644 index 0000000000..8a63968f19 --- /dev/null +++ b/public/language/fr/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notifications", + "no_notifs": "Vous n'avez aucune notification", + "see_all": "Toutes vos notifications", + "mark_all_read": "Marquez tous comme lu", + "back_to_home": "Revenir à %1", + "outgoing_link": "Lien sortant", + "outgoing_link_message": "Vous quittez %1", + "continue_to": "Continuer vers %1", + "return_to": "Revenir à %1", + "new_notification": "Vous avez une nouvelle notification", + "you_have_unread_notifications": "Vous avez des notifications non-lues", + "all": "Tout", + "topics": "Sujets", + "replies": "Réponses", + "chat": "Discussions", + "group-chat": "Groupe de discussions", + "follows": "Suivis", + "upvote": "Votes positifs", + "new-flags": "Nouveaux drapeaux", + "my-flags": "Drapeaux assignés à moi", + "bans": "Bannissements", + "new_message_from": "Nouveau message de %1", + "upvoted_your_post_in": "%1 a voté pour votre message dans %2.", + "upvoted_your_post_in_dual": "%1 et %2 ont voté pour votre message dans %3.", + "upvoted_your_post_in_multiple": "%1 et %2 autres on voté pour votre message dans %3.", + "moved_your_post": "%1 a déplacé votre message vers %2", + "moved_your_topic": "%1 a déplacé %2.", + "user_flagged_post_in": "%1 a signalé un message dans %2.", + "user_flagged_post_in_dual": "%1 et %2 ont signalé un message dans %3", + "user_flagged_post_in_multiple": "%1 et %2 autres on signalé un message dans %3", + "user_flagged_user": "%1 a signalé un profil utilisateur (%2)", + "user_flagged_user_dual": "%1 et %2 ont signalé un profil utilisateur (%3)", + "user_flagged_user_multiple": "%1 et %2 autres utilisateurs ont signalé un profil utilisateur (%3)", + "user_posted_to": "%1 a répondu à : %2", + "user_posted_to_dual": "%1 et %2 ont posté une réponse à : %3", + "user_posted_to_multiple": "%1 et %2 autres ont posté une réponse à : %3", + "user_posted_topic": "%1 a posté un nouveau sujet: %2.", + "user_edited_post": "%1 a édité un message dans %2", + "user_started_following_you": "%1 vous suit.", + "user_started_following_you_dual": "%1 et %2 se sont abonnés à votre compte.", + "user_started_following_you_multiple": "%1 et %2 autres se sont abonnés à votre compte.", + "new_register": "%1 a envoyé une demande d'incription.", + "new_register_multiple": "%1 inscription(s) est en attente de validation.", + "flag_assigned_to_you": "Drapeau %1 vous a été assigné", + "post_awaiting_review": "Message en attente de validation", + "profile-exported": "%1 profil exporté, cliquez pour le télécharger", + "posts-exported": "%1 messages exportés, cliquez pour les télécharger", + "uploads-exported": "%1 envois exportés, cliquez pour les télécharger", + "users-csv-exported": "Utilisateurs exportés en CSV, cliquez pour télécharger", + "post-queue-accepted": "Votre message a été accepté. Cliquez ici pour voir votre message.", + "post-queue-rejected": "Votre message a été rejeté.", + "post-queue-notify": "Vous avez reçu une notification:
\"%1\"", + "email-confirmed": "Email vérifié", + "email-confirmed-message": "Merci pour la validation de votre adresse email. Votre compte est désormais activé.", + "email-confirm-error-message": "Il y a un un problème dans la vérification de votre adresse email. Le code est peut être invalide ou a expiré.", + "email-confirm-sent": "Email de vérification envoyé.", + "none": "aucun", + "notification_only": "Seulement une notification", + "email_only": "Seulement un email", + "notification_and_email": "Notification et email", + "notificationType_upvote": "Lorsque quelqu'un a voté pour un de vos messages", + "notificationType_new-topic": "Lorsque quelqu'un que vous suivez publie un sujet", + "notificationType_new-reply": "Lorsqu'une nouvelle réponse est ajoutée dans un sujet que vous suivez", + "notificationType_post-edit": "Lorsqu'un article est modifié dans un sujet que vous regardez", + "notificationType_follow": "Lorsque quelqu'un commence à vous suivre", + "notificationType_new-chat": "Lorsque vous recevez un message du chat ", + "notificationType_new-group-chat": "Lorsque vous recevez un message de discussion de groupe", + "notificationType_group-invite": "Lorsque vous recevez une invitation d'un groupe", + "notificationType_group-leave": "Lorsqu'un utilisateur quitte votre groupe", + "notificationType_group-request-membership": "Quand quelqu'un demande à rejoindre un groupe que vous administrez", + "notificationType_new-register": "Lorsque quelqu'un est ajouté à la file d'attente d'inscription", + "notificationType_post-queue": "Lorsque un nouveau message est mis en file d'attente", + "notificationType_new-post-flag": "Lorsque un message est marqué", + "notificationType_new-user-flag": "Lorsque un utilisateur est marqué" +} \ No newline at end of file diff --git a/public/language/fr/pages.json b/public/language/fr/pages.json new file mode 100644 index 0000000000..35fcbf2367 --- /dev/null +++ b/public/language/fr/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Accueil", + "unread": "Sujets non lus", + "popular-day": "Sujets populaires aujourd'hui", + "popular-week": "Sujets populaires cette semaine", + "popular-month": "Sujets populaires ce mois-ci", + "popular-alltime": "Sujets populaires depuis toujours", + "recent": "Sujets récents", + "top-day": "Meilleurs votes du jour", + "top-week": "Meilleurs votes de la semaine", + "top-month": "Meilleurs votes du mois", + "top-alltime": "Sujets les mieux notés", + "moderator-tools": "Outils de modération", + "flagged-content": "Contenu signalé", + "ip-blacklist": "Liste noire d'IPs", + "post-queue": "File d'attente des messages", + "users/online": "Utilisateurs en ligne", + "users/latest": "Derniers inscrits", + "users/sort-posts": "Utilisateurs avec le plus de messages", + "users/sort-reputation": "Utilisateurs avec la plus grande réputation", + "users/banned": "Utilisateurs bannis", + "users/most-flags": "Utilisateurs les plus souvent signalés", + "users/search": "Rechercher des utilisateurs", + "notifications": "Notifications", + "tags": "Mots-clés", + "tag": "Sujets marqués comme \"%1\"", + "register": "Créer un compte", + "registration-complete": "Inscription terminée", + "login": "Connectez-vous à votre compte", + "reset": "Remettez à zéro votre mot de passe", + "categories": "Catégories", + "groups": "Groupes", + "group": "%1 groupe", + "chats": "Discussions", + "chat": "Conversation avec %1", + "flags": "Signalements", + "flag-details": "Détails signalement %1", + "account/edit": "Édition de \"%1\"", + "account/edit/password": "Édition du mot de passe de \"%1\"", + "account/edit/username": "Édition du nom d'utilisateur de \"%1\"", + "account/edit/email": "Édition de l'e-mail de \"%1\"", + "account/info": "Informations du compte", + "account/following": "Les personnes auxquelles %1 est abonné", + "account/followers": "Les personnes abonnées à %1", + "account/posts": "Messages postés par %1", + "account/latest-posts": "Derniers messages publiés par %1", + "account/topics": "Sujets créés par %1", + "account/groups": "Groupes auxquels appartient %1", + "account/watched_categories": "%1's Catégories surveillées", + "account/bookmarks": "Marque-pages de %1", + "account/settings": "Paramètres d'utilisateur", + "account/watched": "Sujets auxquels %1 est abonné", + "account/ignored": "Sujets ignorés par %1", + "account/upvoted": "Avis positifs de %1", + "account/downvoted": "Avis négatifs de %1", + "account/best": "Meilleurs messages postés par %1", + "account/controversial": "Messages contestés de %1", + "account/blocks": "Utilisateurs bloqués pour %1", + "account/uploads": "Envoyé par %1", + "account/sessions": "Sessions des connexions", + "confirm": "Email vérifié", + "maintenance.text": "%1 est en maintenance. Veuillez revenir un peu plus tard.", + "maintenance.messageIntro": "De plus, l'administrateur a laissé ce message :", + "throttled.text": "%1 est actuellement indisponible en raison d'une charge excessive. Merci de réessayer plus tard." +} \ No newline at end of file diff --git a/public/language/fr/post-queue.json b/public/language/fr/post-queue.json new file mode 100644 index 0000000000..34137f1c0b --- /dev/null +++ b/public/language/fr/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "File d’attente des messages", + "description": "Aucun messages dans la file d'attente.
Pour activer cette fonctionnalité, accédez aux Paramètres → Messages → File d'attente et activez la file d'attente.", + "user": "Utilisateur", + "category": "Catégorie", + "title": "Titre", + "content": "Contenu", + "posted": "Posté", + "reply-to": "Répondre à \"%1\"", + "content-editable": "Cliquez sur le contenu pour modifier", + "category-editable": "Cliquez sur la catégorie pour modifier", + "title-editable": "Cliquez sur le titre pour modifier", + "reply": "Répondre", + "topic": "Sujet", + "accept": "Accepter", + "reject": "Refuser", + "remove": "Enlever", + "notify": "Notifier", + "notify-user": "Notifier l'utilisateur", + "confirm-reject": "Voulez vous réellement rejeter ce message ?", + "bulk-actions": "Actions", + "accept-all": "Tout accepter", + "accept-selected": "Acceptation sélectionné", + "reject-all": "Tout rejeter", + "reject-all-confirm": "Voulez vous réellement rejeter tous ces messages ?", + "reject-selected": "Rejets sélectionnés", + "reject-selected-confirm": "Voulez vous réellement rejeter ces %1 messages ?", + "bulk-accept-success": "%1 messages acceptés", + "bulk-reject-success": "%1 messages rejetés" +} \ No newline at end of file diff --git a/public/language/fr/recent.json b/public/language/fr/recent.json new file mode 100644 index 0000000000..550a18c559 --- /dev/null +++ b/public/language/fr/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Récent", + "day": "Jour", + "week": "Semaine", + "month": "Mois", + "year": "An", + "alltime": "Toujours", + "no_recent_topics": "Il n'y a aucun sujet récent.", + "no_popular_topics": "Il n'y a pas de sujet populaire.", + "there-is-a-new-topic": "Il y a un nouveau sujet.", + "there-is-a-new-topic-and-a-new-post": "Il y a un nouveau sujet et un nouveau message.", + "there-is-a-new-topic-and-new-posts": "Il y a un nouveau sujet et %1 nouveaux messages.", + "there-are-new-topics": "Il y a %1 nouveaux sujets.", + "there-are-new-topics-and-a-new-post": "Il y a %1 nouveaux sujets et un nouveau message.", + "there-are-new-topics-and-new-posts": "Il y a %1 nouveaux sujets et %2 nouveaux messages.", + "there-is-a-new-post": "Il y a un nouveau message.", + "there-are-new-posts": "Il y a %1 nouveaux messages.", + "click-here-to-reload": "Cliquez ici pour recharger." +} \ No newline at end of file diff --git a/public/language/fr/register.json b/public/language/fr/register.json new file mode 100644 index 0000000000..3c7eef960c --- /dev/null +++ b/public/language/fr/register.json @@ -0,0 +1,32 @@ +{ + "register": "S'inscrire", + "cancel_registration": "Annuler l'inscription", + "help.email": "Par défaut, votre adresse e-mail est masquée au public.", + "help.username_restrictions": "Un nom d'utilisateur unique entre %1 et %2 caractères. Les autres utilisateurs peuvent vous mentionner avec @nom-d'utilisateur.", + "help.minimum_password_length": "Votre mot de passe doit avoir au moins %1 caractères.", + "email_address": "Adresse e-mail", + "email_address_placeholder": "Entrer votre adresse e-mail", + "username": "Nom d'utilisateur", + "username_placeholder": "Entrer votre nom d'utilisateur", + "password": "Mot de passe", + "password_placeholder": "Entrer votre mot de passe", + "confirm_password": "Confirmer le mot de passe", + "confirm_password_placeholder": "Confirmer votre mot de passe", + "register_now_button": "S'inscrire", + "alternative_registration": "Autres méthodes d'inscription", + "terms_of_use": "Conditions générales d'utilisation", + "agree_to_terms_of_use": "J'accepte les conditions générales d'utilisation", + "terms_of_use_error": "Vous devez accepter les conditions générales d'utilisation", + "registration-added-to-queue": "Votre inscription a été ajoutée à la liste d'approbation. Vous recevrez un email quand celle-ci sera acceptée par un administrateur.", + "registration-queue-average-time": "Temps moyen d'approbation des adhésions est de %1 heures %2 minutes.", + "registration-queue-auto-approve-time": "Votre adhésion à ce forum sera entièrement activée dans un maximum de %1 heures.", + "interstitial.intro": "Nous aimerions avoir des informations supplémentaires afin de mettre à jour votre compte…", + "interstitial.intro-new": "Nous aimerions avoir des informations supplémentaires avant de pouvoir créer votre compte…", + "interstitial.errors-found": "Veuillez vérifier les informations saisies :", + "gdpr_agree_data": "J'accepte la collecte et le traitement de mes données personnelles sur ce site.", + "gdpr_agree_email": "J'accepte de recevoir des courriels et des notifications de ce site Web.", + "gdpr_consent_denied": "Vous devez accepter que ce site puisse collecter et traiter vos données, et pour vous envoyer des courriels.", + "invite.error-admin-only": "Les inscriptions sont désactivés. Veuillez contacter un administrateur pour plus de détails.", + "invite.error-invite-only": "Les inscriptions sont désactivés. Seules les invitations sont acceptés pour accéder au forum.", + "invite.error-invalid-data": "Renseignements erronées. Veuillez contacter un administrateur pour plus de détails" +} \ No newline at end of file diff --git a/public/language/fr/reset_password.json b/public/language/fr/reset_password.json new file mode 100644 index 0000000000..c0ec57af7b --- /dev/null +++ b/public/language/fr/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Réinitialiser votre mot de passe", + "update_password": "Mettre à jour votre mot de passe", + "password_changed.title": "Mot de passe modifié", + "password_changed.message": "

Votre mot de passe a bien été réinitialisé, veuillez vous reconnecter.", + "wrong_reset_code.title": "Code de réinitialisation incorrect", + "wrong_reset_code.message": "Le code de réinitialisation est incorrect. Veuillez réessayer, ou demander un nouveau code de réinitialisation.", + "new_password": "Nouveau mot de passe", + "repeat_password": "Confirmer le mot de passe", + "changing_password": "Changer le mot de passe", + "enter_email": "Veuillez entrer votre adresse email pour recevoir un email contenant les instructions permettant de réinitialiser votre compte.", + "enter_email_address": "Entrer votre adresse email", + "password_reset_sent": "Si l'adresse spécifiée correspond à un compte d'utilisateur existant, un email de réinitialisation de mot de passe va être envoyé. Veuillez noter qu'un seul email sera envoyé par minute.", + "invalid_email": "Email invalide / L'email n'existe pas !", + "password_too_short": "Le mot de passe est trop court, veuillez entrer un mot de passe différent.", + "passwords_do_not_match": "Les deux mots de passe saisis ne correspondent pas.", + "password_expired": "Votre mot de passe a expiré, veuillez choisir un nouveau mot de passe." +} \ No newline at end of file diff --git a/public/language/fr/search.json b/public/language/fr/search.json new file mode 100644 index 0000000000..943e939d57 --- /dev/null +++ b/public/language/fr/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 résultat(s) correspondant(s) à \"%2\", (%3 secondes)", + "no-matches": "Aucune réponse trouvée", + "advanced-search": "Recherche avancée", + "in": "Dans", + "titles": "Titres", + "titles-posts": "Titres et Messages", + "match-words": "Correspondance", + "all": "Tous", + "any": "Aucun", + "posted-by": "Posté par", + "in-categories": "Dans les catégories", + "search-child-categories": "Rechercher également dans les sous catégories", + "has-tags": "Contient les mots-clés", + "reply-count": "Nombre de réponses", + "at-least": "Au moins", + "at-most": "Au plus", + "relevance": "Pertinence", + "post-time": "Date de message", + "votes": "Votes", + "newer-than": "Plus récent que", + "older-than": "Plus vieux que", + "any-date": "Toute dates", + "yesterday": "Hier", + "one-week": "Une semaine", + "two-weeks": "Deux semaines", + "one-month": "Un mois", + "three-months": "Trois mois", + "six-months": "Six mois", + "one-year": "Un an", + "sort-by": "Trier par", + "last-reply-time": "Date de dernière réponse", + "topic-title": "Titre de sujet", + "topic-votes": "Sujets votés", + "number-of-replies": "Nombre de réponses", + "number-of-views": "Nombre de vues", + "topic-start-date": "Date de création du sujet", + "username": "Nom d'utilisateur", + "category": "Catégorie", + "descending": "Par ordre décroissant", + "ascending": "Par ordre croissant", + "save-preferences": "Enregistrer les préférences", + "clear-preferences": "Réinitialiser les préférences", + "search-preferences-saved": "Préférences de recherche enregistrées", + "search-preferences-cleared": "Préférences de recherche réinitialisées", + "show-results-as": "Affichez les résultats comme", + "see-more-results": "Voir plus de résultats (%1)", + "search-in-category": "Rechercher dans \"%1\"" +} \ No newline at end of file diff --git a/public/language/fr/success.json b/public/language/fr/success.json new file mode 100644 index 0000000000..33e50aadfa --- /dev/null +++ b/public/language/fr/success.json @@ -0,0 +1,7 @@ +{ + "success": "Terminé", + "topic-post": "Le message a bien été envoyé.", + "post-queued": "Votre message est en attente d'approbation. Vous recevrez une notification lorsqu'il sera accepté ou rejeté.", + "authentication-successful": "Authentification réussie", + "settings-saved": "Paramètres enregistrés !" +} \ No newline at end of file diff --git a/public/language/fr/tags.json b/public/language/fr/tags.json new file mode 100644 index 0000000000..438c24c184 --- /dev/null +++ b/public/language/fr/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Il n'y a aucun sujet ayant ce mot-clé", + "tags": "Mots-clés", + "enter_tags_here": "Entrez les mots-clés ici. Chaque mot doit faire entre %1 et %2 caractères.", + "enter_tags_here_short": "Entrez des mots-clés...", + "no_tags": "Il n'y a pas encore de mots-clés.", + "select_tags": "Sélectionner les mots-clés" +} \ No newline at end of file diff --git a/public/language/fr/top.json b/public/language/fr/top.json new file mode 100644 index 0000000000..b182f2d4e9 --- /dev/null +++ b/public/language/fr/top.json @@ -0,0 +1,4 @@ +{ + "title": "Haut", + "no_top_topics": "Aucun sujet principal" +} \ No newline at end of file diff --git a/public/language/fr/topic.json b/public/language/fr/topic.json new file mode 100644 index 0000000000..a5289be8bc --- /dev/null +++ b/public/language/fr/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Sujet", + "title": "Titre", + "no_topics_found": "Aucun sujet n'a été trouvé !", + "no_posts_found": "Aucun message trouvé !", + "post_is_deleted": "Ce message a été supprimé !", + "topic_is_deleted": "Ce sujet a été supprimé !", + "profile": "Profil", + "posted_by": "Posté par %1", + "posted_by_guest": "Posté par un invité", + "chat": "Chat", + "notify_me": "Être notifié des réponses dans ce sujet", + "quote": "Citer", + "reply": "Répondre", + "replies_to_this_post": "%1 réponses", + "one_reply_to_this_post": "1 réponse", + "last_reply_time": "Dernière réponse", + "reply-as-topic": "Répondre à l'aide d'un nouveau sujet", + "guest-login-reply": "Se connecter pour répondre", + "login-to-view": "🔒 Se connectez-vous pour voir", + "edit": "Éditer", + "delete": "Supprimer", + "delete-event": "Supprimer l'événement", + "delete-event-confirm": "Êtes-vous sûr de vouloir supprimer cet événement ?", + "purge": "Supprimer définitivement", + "restore": "Restaurer", + "move": "Déplacer", + "change-owner": "Changer de propriétaire", + "fork": "Scinder", + "link": "Lien", + "share": "Partager", + "tools": "Outils", + "locked": "Verrouillé", + "pinned": "Épinglé", + "pinned-with-expiry": "Épinglé jusqu'au %1", + "scheduled": "Planifier", + "moved": "Déplacé", + "moved-from": "Déplacé de %1", + "copy-ip": "Copier l'IP", + "ban-ip": "Bannir l'IP", + "view-history": "Éditer l'historique", + "locked-by": "Verrouillé par", + "unlocked-by": "Déverrouiller par", + "pinned-by": "Épinglé par", + "unpinned-by": "Désépinglé par", + "deleted-by": "Effacé par", + "restored-by": "Restauré par", + "moved-from-by": "Déplacé de %1 par", + "queued-by": "Message en attente d'approbation →", + "backlink": "Référencé par", + "forked-by": "Dupliqué par", + "bookmark_instructions": "Cliquez ici pour aller au dernier message lu de ce fil.", + "flag-post": "Signaler ce message", + "flag-user": "Signaler cet utilisateur", + "already-flagged": "Déjà signalé", + "view-flag-report": "Voir le rapport de signalement", + "resolve-flag": "Signalement résolu", + "merged_message": "Ce sujet a été fusionné dans %2", + "deleted_message": "Ce sujet a été supprimé. Seuls les utilisateurs avec les droits d'administration peuvent le voir.", + "following_topic.message": "Vous recevrez désormais des notifications lorsque quelqu'un postera dans ce sujet.", + "not_following_topic.message": "Vous verrez ce sujet dans la liste des sujets non-lus, mais vous ne recevrez pas de notification lorsque quelqu'un postera dans ce sujet.", + "ignoring_topic.message": "Vous ne verrez plus ce sujet dans la liste des sujets non lus. Vous serez notifié lorsque vous serez mentionné ou que quelqu'un votera pour votre message.", + "login_to_subscribe": "Veuillez vous enregistrer ou vous connecter afin de vous abonner à ce sujet.", + "markAsUnreadForAll.success": "Sujet marqué comme non lu pour tout le monde.", + "mark_unread": "Marquer comme non-lu", + "mark_unread.success": "Sujet marqué comme non lu.", + "watch": "Suivre", + "unwatch": "Cesser de suivre", + "watch.title": "Être notifié des nouvelles réponses dans ce sujet", + "unwatch.title": "Cesser de suivre ce sujet", + "share_this_post": "Partager ce message", + "watching": "Suivi", + "not-watching": "Suivre", + "ignoring": "Ignoré", + "watching.description": "Me notifier les nouvelles réponses.
Afficher le sujet dans l'onglet \"Non lu\".", + "not-watching.description": "Ne pas me notifier les nouvelles réponses.
Afficher le sujet dans l'onglet \"Non lu\" si la catégorie n'est pas ignorée.", + "ignoring.description": "Ne pas me notifier les nouvelles réponses.
Ne pas afficher ce sujet dans l'onglet \"Non lu\".", + "thread_tools.title": "Outils pour sujets", + "thread_tools.markAsUnreadForAll": "Marquer non lu pour tous", + "thread_tools.pin": "Épingler le sujet", + "thread_tools.unpin": "Désépingler le sujet", + "thread_tools.lock": "Verrouiller le sujet", + "thread_tools.unlock": "Déverouiller le sujet", + "thread_tools.move": "Déplacer le sujet", + "thread_tools.move-posts": "Déplacer les messages", + "thread_tools.move_all": "Déplacer tout", + "thread_tools.change_owner": "Changer de propriétaire", + "thread_tools.select_category": "Sélectionner une catégorie", + "thread_tools.fork": "Scinder le sujet", + "thread_tools.delete": "Supprimer le sujet", + "thread_tools.delete-posts": "Supprimer les messages", + "thread_tools.delete_confirm": "Êtes-vous sûr de bien vouloir supprimer ce sujet ?", + "thread_tools.restore": "Restaurer le sujet", + "thread_tools.restore_confirm": "Êtes-vous sûr de bien vouloir restaurer ce sujet ?", + "thread_tools.purge": "Supprimer définitivement le(s) sujet(s)", + "thread_tools.purge_confirm": "Êtes-vous sûr de bien vouloir supprimer définitivement ce sujet ?", + "thread_tools.merge_topics": "Fusionner les Sujets", + "thread_tools.merge": "Fusionner", + "topic_move_success": "Ce sujet sera bientôt déplacé vers \"%1\". Cliquez ici pour annuler.", + "topic_move_multiple_success": "Ces sujets seront bientôt déplacés vers \"%1\". Cliquez ici pour annuler.", + "topic_move_all_success": "Tous les sujets seront déplacés vers \"%1\". Cliquez ici pour annuler.", + "topic_move_undone": "Déplacement de sujet annulé", + "topic_move_posts_success": "Les messages vont être déplacés. Cliquez ici pour annuler.", + "topic_move_posts_undone": "Déplacement annulé", + "post_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer ce message ?", + "post_restore_confirm": "Êtes-vous sûr de bien vouloir restaurer ce message ?", + "post_purge_confirm": "Êtes-vous sûr de bien vouloir supprimer définitivement ce sujet ?", + "pin-modal-expiry": "Date d'expiration", + "pin-modal-help": "Vous pouvez éventuellement définir une date d'expiration pour le(s) sujet(s) épinglé(s) ici. Vous pouvez également laisser ce champ vide pour que le sujet reste épinglé jusqu'à ce qu'il soit supprimé manuellement.", + "load_categories": "Chargement des catégories en cours", + "confirm_move": "Déplacer", + "confirm_fork": "Scinder", + "bookmark": "Marque-page", + "bookmarks": "Marque-pages", + "bookmarks.has_no_bookmarks": "Vous n'avez encore aucun marque-page.", + "copy-permalink": "Copier le permalien", + "loading_more_posts": "Charger plus de messages", + "move_topic": "Déplacer le sujet", + "move_topics": "Déplacer des sujets", + "move_post": "Déplacer", + "post_moved": "Message déplacé !", + "fork_topic": "Scinder le sujet", + "enter-new-topic-title": "Entrez un nouveau titre de sujet", + "fork_topic_instruction": "Cliquez sur les postes à scinder", + "fork_no_pids": "Aucun post sélectionné !", + "no-posts-selected": "Aucun(s) message(s) sélectionné(s) !", + "x-posts-selected": "%1 message(s) sélectionné(s)", + "x-posts-will-be-moved-to-y": "%1 message(s) seront déplacés vers \"%2\"", + "fork_pid_count": "%1 message(s) sélectionné(s)", + "fork_success": "Sujet copié avec succès ! Cliquez ici pour aller au sujet copié.", + "delete_posts_instruction": "Sélectionnez les messages que vous souhaitez supprimer/vider", + "merge_topics_instruction": "Cliquez sur les sujets que vous voulez fusionner", + "merge-topic-list-title": "Liste des sujets à fusionner", + "merge-options": "Options de fusion", + "merge-select-main-topic": "Sélectionnez le sujet principal", + "merge-new-title-for-topic": "Nouveau titre pour le sujet", + "topic-id": "Sujet ID", + "move_posts_instruction": "Cliquez sur les articles que vous souhaitez déplacer, puis entrez un ID de sujet ou accédez au sujet cible", + "change_owner_instruction": "Cliquez sur les messages que vous souhaitez attribuer à un autre utilisateur.", + "composer.title_placeholder": "Entrer le titre du sujet ici…", + "composer.handle_placeholder": "Entrez votre nom/identifiant ici", + "composer.discard": "Abandonner", + "composer.submit": "Envoyer", + "composer.additional-options": "Options additionnelles", + "composer.schedule": "Planification", + "composer.replying_to": "En réponse à %1", + "composer.new_topic": "Nouveau sujet", + "composer.editing": "Édition", + "composer.uploading": "envoi en cours…", + "composer.thumb_url_label": "Coller une URL de vignette du sujet", + "composer.thumb_title": "Ajouter une vignette à ce sujet", + "composer.thumb_url_placeholder": "http://exemple.com/vignette.png", + "composer.thumb_file_label": "Ou envoyer un fichier", + "composer.thumb_remove": "Effacer les champs", + "composer.drag_and_drop_images": "Glissez-déposez les images ici", + "more_users_and_guests": "%1 autre(s) utilisateur(s) et %2 invité(s)", + "more_users": "%1 autre(s) utilisateur(s)", + "more_guests": "%1 autre(s) invité(s)", + "users_and_others": "%1 et %2 autres", + "sort_by": "Trier", + "oldest_to_newest": "Du plus ancien au plus récent", + "newest_to_oldest": "Du plus récent au plus ancien", + "most_votes": "Les plus votés", + "most_posts": "Meilleurs messages", + "most_views": "Les plus vues", + "stale.title": "Créer un nouveau sujet à la place ?", + "stale.warning": "Le sujet auquel vous répondez est assez ancien. Ne voudriez-vous pas créer un nouveau sujet à la place et placer une référence vers celui-ci dans votre réponse ?", + "stale.create": "Créer un nouveau sujet", + "stale.reply_anyway": "Répondre à ce sujet quand même", + "link_back": "Re : [%1](%2)", + "diffs.title": "Historique", + "diffs.description": "Cet article a %1 révisions. Cliquez sur l'une des révisions ci-dessous pour voir le contenu du message.", + "diffs.no-revisions-description": "Cet article a %1 révisions.", + "diffs.current-revision": "Révision en cours", + "diffs.original-revision": "Révision originale", + "diffs.restore": "Restaurer cette révision", + "diffs.restore-description": "Une nouvelle révision sera ajoutée à l'historique des modifications de ce message après la restauration.", + "diffs.post-restored": "Restauration avec succès dans une révision antérieure", + "diffs.delete": "Supprimer cette révision", + "diffs.deleted": "Révision supprimé", + "timeago_later": "%1", + "timeago_earlier": "il y a %1", + "first-post": "Premier message", + "last-post": "Dernier message", + "go-to-my-next-post": "Aller à mon prochain message", + "no-more-next-post": "Vous n'avez plus de messages dans ce sujet", + "post-quick-reply": "Réponse rapide" +} \ No newline at end of file diff --git a/public/language/fr/unread.json b/public/language/fr/unread.json new file mode 100644 index 0000000000..445e8aaf20 --- /dev/null +++ b/public/language/fr/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Non lu", + "no_unread_topics": "Aucun sujet non lu.", + "load_more": "Charger la suite", + "mark_as_read": "Marquer comme lu", + "selected": "Sélectionnés", + "all": "Tous", + "all_categories": "Toutes Catégories", + "topics_marked_as_read.success": "Sujets marqués comme lus !", + "all-topics": "Tous les sujets", + "new-topics": "Nouveau sujet", + "watched-topics": "Sujets surveillés", + "unreplied-topics": "Sujets sans réponses", + "multiple-categories-selected": "Sélection multiple" +} \ No newline at end of file diff --git a/public/language/fr/uploads.json b/public/language/fr/uploads.json new file mode 100644 index 0000000000..43f9e8f4b3 --- /dev/null +++ b/public/language/fr/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Envoi d'un fichier…", + "select-file-to-upload": "Sélectionnez un ficher à envoyer", + "upload-success": "Fichier envoyé", + "maximum-file-size": "%1 Ko maximum", + "no-uploads-found": "Aucun fichiers envoyés", + "public-uploads-info": "Les téléchargements sont publics, tous les visiteurs peuvent les voir.", + "private-uploads-info": "Les envois sont privés, seuls les utilisateurs connectés peuvent les voir." +} \ No newline at end of file diff --git a/public/language/fr/user.json b/public/language/fr/user.json new file mode 100644 index 0000000000..bdede19408 --- /dev/null +++ b/public/language/fr/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banni", + "muted": "Muet", + "offline": "Hors-ligne", + "deleted": "Effacé", + "username": "Nom d'utilisateur", + "joindate": "Date d'inscription", + "postcount": "Nombre de messages", + "email": "Email", + "confirm_email": "Confirmer l'adresse email", + "account_info": "Informations du compte", + "admin_actions_label": "Modération", + "ban_account": "Bannir le compte", + "ban_account_confirm": "Êtes-vous sûr de bien vouloir bannir cet utilisateur ?", + "unban_account": "Rétablir le compte", + "mute_account": "Compte muet", + "unmute_account": "Compte actif", + "delete_account": "Supprimer le compte", + "delete_account_as_admin": "Supprimer le compte", + "delete_content": "Supprimer le contenu du compte", + "delete_all": "Supprimer le compte et le contenu", + "delete_account_confirm": "Êtes-vous sûr de vouloir supprimer votre compte?
Cette action est irréversible et vous ne pourrez récupérer aucune de vos données

Entrez votre mot de passe pour confirmer que vous souhaitez détruire ce compte.", + "delete_this_account_confirm": "Êtes-vous sûr de vouloir supprimer ce compte?
Cette action est irréversible, les messages deviendront anonymes et vous ne pourrez pas restaurer les messages associés avec le compte supprimé.

", + "delete_account_content_confirm": "Êtes-vous sûr de vouloir supprimer le contenu de ce compte (messages / sujets / fichiers envoyés)?
Cette action est irréversible et vous ne pourrez récupérer aucune donnée.

", + "delete_all_confirm": "Êtes-vous sûr de vouloir supprimer ce compte et tout son contenu (messages / sujets / fichiers envoyés)?
Cette action est irréversible et vous ne pourrez récupérer aucune donnée.

", + "account-deleted": "Compte supprimé", + "account-content-deleted": "Contenu du compte supprimé", + "fullname": "Nom", + "website": "Site web", + "location": "Localisation", + "age": "Âge", + "joined": "Inscrit", + "lastonline": "Dernière connexion", + "profile": "Profil", + "profile_views": "Vues", + "reputation": "Réputation", + "bookmarks": "Marque-pages", + "watched_categories": "Catégories surveillées", + "change_all": "Tout changer", + "watched": "Abonnements", + "ignored": "Ignorés", + "default-category-watch-state": "Abonnement par défaut des catégories", + "followers": "Abonnés", + "following": "Abonnements", + "blocks": "Bloqués", + "block_toggle": "Débloquer", + "block_user": "Bloquer l'utilisateur", + "unblock_user": "Débloquer l'utilisateur", + "aboutme": "À propos de moi", + "signature": "Signature", + "birthday": "Anniversaire", + "chat": "Discussion", + "chat_with": "Continuer la discussion avec %1", + "new_chat_with": "Commencer une nouvelle discussion avec %1", + "flag-profile": "Signaler le profil", + "follow": "S'abonner", + "unfollow": "Se désabonner", + "more": "Plus", + "profile_update_success": "Le profil a bien été mis à jour !", + "change_picture": "Changer l'image", + "change_username": "Changer le nom d'utilisateur", + "change_email": "Changer l'e-mail", + "email_same_as_password": "Veuillez entrer votre mot de passe actuel pour continuer – vous devez saisir à nouveau votre email", + "edit": "Éditer", + "edit-profile": "Éditer le profil", + "default_picture": "Icône par défaut", + "uploaded_picture": "Image envoyée", + "upload_new_picture": "Envoyer une nouvelle image", + "upload_new_picture_from_url": "Envoyer une nouvelle image depuis un URL", + "current_password": "Mot de passe actuel", + "change_password": "Changer le mot de passe", + "change_password_error": "Mot de passe invalide !", + "change_password_error_wrong_current": "Votre mot de passe est incorrect !", + "change_password_error_match": "Les mots de passe doivent être identiques !", + "change_password_error_privileges": "Vous n'avez pas les droits de changer ce mot de passe.", + "change_password_success": "Votre mot de passe a été mis à jour.", + "confirm_password": "Confirmer le mot de passe", + "password": "Mot de passe", + "username_taken_workaround": "Le nom d'utilisateur souhaité est déjà utilisé, nous l'avons donc légèrement modifié. Vous êtes maintenant connu comme %1", + "password_same_as_username": "Votre mot de passe est identique à votre nom d'utilisateur. Veuillez en choisir un autre.", + "password_same_as_email": "Votre mot de passe est identique à votre adresse email. Veuillez en choisir un autre.", + "weak_password": "Sécurité du mot de passe faible.", + "upload_picture": "Envoyer l'image", + "upload_a_picture": "Envoyer une image", + "remove_uploaded_picture": "Supprimer l'image envoyée", + "upload_cover_picture": "Envoyer une image de couverture", + "remove_cover_picture_confirm": "Êtes-vous sûr de vouloir supprimer l'image de couverture ?", + "crop_picture": "Découper l’image", + "upload_cropped_picture": "Découper et envoyer", + "avatar-background-colour": "Couleur d'arrière plan", + "settings": "Paramètres", + "show_email": "Afficher mon email", + "show_fullname": "Afficher mon nom complet", + "restrict_chats": "Autoriser la réception de messages ne provenant que des personnes auxquelles je suis abonné", + "digest_label": "S’inscrire aux lettres de suivi d'activités", + "digest_description": "S'abonner par email aux mises à jours de ce forum (nouvelles notifications et nouveaux sujets) selon le planning sélectionné.", + "digest_off": "Désactivé", + "digest_daily": "Quotidien", + "digest_weekly": "Hebdomadaire", + "digest_biweekly": "Deux fois par semaine", + "digest_monthly": "Mensuel", + "has_no_follower": "Cet utilisateur n'a pas encore d'abonné :(", + "follows_no_one": "Cet utilisateur n'est abonné à personne :(", + "has_no_posts": "Cet utilisateur n'a encore rien posté.", + "has_no_best_posts": "Cet utilisateur n'a pas encore d'avis positifs", + "has_no_topics": "Cet utilisateur n'a encore créé aucun sujet.", + "has_no_watched_topics": "Cet utilisateur ne s'est encore abonné à aucun sujet.", + "has_no_ignored_topics": "Cet utilisateur n'a encore ignoré aucun sujet.", + "has_no_upvoted_posts": "Cet utilisateur n'a donné d'avis positifs", + "has_no_downvoted_posts": "Cet utilisateur n'a pas donné d'avis négatifs", + "has_no_controversial_posts": "Cet utilisateur n'a pas encore d'avis négatifs.", + "has_no_blocks": "Vous n'avez bloqué aucun utilisateur.", + "email_hidden": "Email masqué", + "hidden": "masqué", + "paginate_description": "Utiliser la pagination des sujets et des messages à la place du défilement infini", + "topics_per_page": "Sujets par page", + "posts_per_page": "Messages par page", + "max_items_per_page": "Maximum %1", + "acp_language": "Page de gestion des langues", + "notifications": "Notifications", + "upvote-notif-freq": "Fréquence de notification des votes positif", + "upvote-notif-freq.all": "Tout les votes positif", + "upvote-notif-freq.first": "En premier", + "upvote-notif-freq.everyTen": "Tous les dix votes positifs", + "upvote-notif-freq.threshold": "Sur 1, 5, 10, 25, 50, 100, 150, 200 ...", + "upvote-notif-freq.logarithmic": "Les 10, 100, 1000...", + "upvote-notif-freq.disabled": "Désactivé", + "browsing": "Paramètres de navigation", + "open_links_in_new_tab": "Ouvrir les liens externes dans un nouvel onglet", + "enable_topic_searching": "Activer la recherche dans les sujets", + "topic_search_help": "Une fois activé, la recherche dans les sujets va remplacer la recherche de page du navigateur et vous permettra de rechercher dans l'intégralité d'un sujet au lieu des seuls posts affichés à l'écran.", + "update_url_with_post_index": "Mettre à jour l'URL avec l'index des articles", + "scroll_to_my_post": "Après avoir répondu, montrer le nouveau message", + "follow_topics_you_reply_to": "S'abonner aux sujets auxquels vous répondez", + "follow_topics_you_create": "S'abonner aux sujets que vous créez", + "grouptitle": "Nom du groupe", + "group-order-help": "Sélectionnez un groupe et utilisez les flèches pour organiser les titres", + "no-group-title": "Aucun titre de groupe", + "select-skin": "Sélectionner une apparence", + "select-homepage": "Sélectionner une page d'accueil", + "homepage": "Page d'accueil", + "homepage_description": "Sélectionnez une page à utiliser comme page d'accueil du forum, ou \"Aucune\" pour utiliser la page d'accueil par défaut.", + "custom_route": "Chemin personnalisé de la page d'accueil", + "custom_route_help": "Saisissez un nom de chemin ici, sans barre oblique précédente (par exemple, \"récent\" ou \"catégorie/2/discussion-générale\")", + "sso.title": "Services d'authentification unique", + "sso.associated": "Associé avec", + "sso.not-associated": "Cliquez ici pour associer", + "sso.dissociate": "Dissocier", + "sso.dissociate-confirm-title": "Confirmer la dissociation", + "sso.dissociate-confirm": "Êtes-vous sûr de vouloir dissocier votre compte de %1 ?", + "info.latest-flags": "Derniers signalements", + "info.no-flags": "Aucun signalement trouvé", + "info.ban-history": "Historique de bannissement récent", + "info.no-ban-history": "Cet utilisateur n'a jamais été banni", + "info.banned-until": "Banni jusqu'au %1", + "info.banned-expiry": "Expiration", + "info.banned-permanently": "Banni de façon permanente", + "info.banned-reason-label": "Raison", + "info.banned-no-reason": "Aucune raison donnée", + "info.mute-history": "Historique de mise en sourdine récent", + "info.no-mute-history": "Cette utilisateur n'a jamais était muet", + "info.muted-until": "Muet jusqu'à %1", + "info.muted-expiry": "Date d'expiration", + "info.muted-no-reason": "Aucune raison donnée.", + "info.username-history": "Historique des noms d'utilisateur", + "info.email-history": "Historique des adresses email", + "info.moderation-note": "Note de modération", + "info.moderation-note.success": "Note de modération enregistrée", + "info.moderation-note.add": "Ajouter une note", + "sessions.description": "Cette page vous permet de visualiser et de révoquer si nécessaire, toutes les sessions actives de ce forum. Vous pouvez révoquer votre propre session en vous déconnectant de votre compte.", + "consent.title": "Vos données personnelles", + "consent.lead": "Ce forum collecte et traite vos informations personnelles.", + "consent.intro": "Nous utilisons ces informations strictement pour personnaliser votre expérience dans cette communauté, ainsi que pour associer les messages que vous publiez à votre compte utilisateur. Lors de l'étape d'enregistrement, vous avez été invité à fournir un nom d'utilisateur et une adresse e-mail. Vous pouvez également fournir des informations supplémentaires pour compléter votre profil.

Nous conservons ces informations durant la durée de vie de votre compte utilisateur. À tout moment vous pouvez supprimer votre compte. À tout moment, vous pouvez demander une copie de vos contributions, via la page de vos données personnelles.

Si vous avez des questions ou préoccupations, nous vous encourageons à contacter l'équipe d'administration de ce forum.", + "consent.email_intro": "Occasionnellement, nous pouvons envoyer des courriels afin de fournir des mises à jour et / ou de vous informer de toute nouvelle activité qui vous concerne. Vous pouvez personnaliser la fréquence d'envoi (y compris la désactiver), ainsi que sélectionner les types de notifications à recevoir, via vos paramètres utilisateur.", + "consent.digest_frequency": "Par défaut, ce forum délivre des lettres d'activités tous les %1.", + "consent.digest_off": "Actuellement, ce forum n'envoi pas de lettre d'activités", + "consent.received": "Vous avez donné votre accord pour que ce site collecte et traite vos informations. Aucune action supplémentaire n'est requise.", + "consent.not_received": "Vous n'avez pas donné votre accord pour la collecte et le traitement des données. A tout moment, l'équipe d'administration de ce site peut choisir de supprimer votre compte afin de se conformer au règlement général sur la protection des données.", + "consent.give": "Donner son accord", + "consent.right_of_access": "Vous avez le droit d'accès", + "consent.right_of_access_description": "Vous avez la possibilité d'accéder à toutes les données collectées par ce site sur demande. Vous pouvez récupérer une copie de ces données en cliquant sur le bouton ci-dessous.", + "consent.right_to_rectification": "Vous avez le droit de rectification", + "consent.right_to_rectification_description": "Vous avez la possibilité de modifier ou de mettre à jour les données inexactes qui nous sont fournies. Votre profil peut être mis à jour en modifiant votre profil, et le contenu de l'article peut toujours être modifié. Si ce n'est pas le cas, veuillez contacter l'équipe d'administration.", + "consent.right_to_erasure": "Vous avez le droit d'effacer", + "consent.right_to_erasure_description": "Vous pouvez à tout moment révoquer votre accord à la collecte et/ou aux traitements de données en supprimant votre compte. Votre profil individuel peut être supprimé, bien que le contenu que vous avez publié restera affiché. Si vous souhaitez supprimer à la fois votre compte et votre contenu, veuillez contacter l'équipe administrative pour ce site.", + "consent.right_to_data_portability": "Vous avez la possibilité de portabilité des données.", + "consent.right_to_data_portability_description": "Vous pouvez exporter de toutes vos données collectées. Vous pouvez le faire en cliquant sur le bouton ci-dessous.", + "consent.export_profile": "Exporter Profile (.json)", + "consent.export-profile-success": "Exportation du profil, vous recevrez une notification lorsqu'elle sera terminée.", + "consent.export_uploads": "Exporter vos fichiers envoyés (.zip)", + "consent.export-uploads-success": "Exportation des envois, vous recevrez une notification lorsqu'elle sera terminée.", + "consent.export_posts": "Exporter vos messages (.csv)", + "consent.export-posts-success": "Exportation des messages, vous recevrez une notification lorsqu'elle sera terminée.", + "emailUpdate.intro": "Veuillez renseigner votre adresse mails. Ce forum utilise votre adresse mail pour suivre l'activité et les notifications programmés, ainsi que pour la récupération de compte en cas de perte de mot de passe.", + "emailUpdate.optional": "Ce champ est facultatif. Vous n'êtes pas obligé de fournir votre adresse e-mail, mais sans e-mail validé, vous ne pourrez pas récupérer votre compte ou vous connecter avec votre e-mail.", + "emailUpdate.required": "Ce champ est requis.", + "emailUpdate.change-instructions": "Un mail de confirmation sera envoyé à l'adresse mail saisie avec un lien unique. L'accès à ce lien confirmera votre propriété de mail et elle deviendra active sur votre compte. À tout moment, vous pouvez mettre à jour votre mail enregistré depuis la page de votre compte.", + "emailUpdate.password-challenge": "Veuillez entrer votre mot de passe pour confirmer la propriété du compte." +} \ No newline at end of file diff --git a/public/language/fr/users.json b/public/language/fr/users.json new file mode 100644 index 0000000000..526f188f75 --- /dev/null +++ b/public/language/fr/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Derniers inscrits", + "top_posters": "Utilisateurs les plus actifs", + "most_reputation": "Meilleur Réputation", + "most_flags": "Les plus signalés", + "search": "Rechercher", + "enter_username": "Entrez le nom d'un utilisateur", + "search-user-for-chat": "chercher un utilisateur pour commencer une discussion", + "load_more": "Charger la suite", + "users-found-search-took": "%1 utilisateur(s) trouvé(s)! La recherche a pris %2 secondes.", + "filter-by": "Filtrer par", + "online-only": "En ligne uniquement", + "invite": "Invitation", + "prompt-email": "Emails:", + "groups-to-join": "Groupes à rejoindre lorsque l'invitation est acceptée:", + "invitation-email-sent": "Un email d'invitation a été envoyé à %1", + "user_list": "Liste d'utilisateurs", + "recent_topics": "Sujets Récents", + "popular_topics": "Sujets Populaires", + "unread_topics": "Sujets Non-Lus", + "categories": "Catégories", + "tags": "Mots-clés", + "no-users-found": "Aucun utilisateur trouvé !" +} \ No newline at end of file diff --git a/public/language/gl/_DO_NOT_EDIT_FILES_HERE.md b/public/language/gl/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/gl/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/gl/admin/admin.json b/public/language/gl/admin/admin.json new file mode 100644 index 0000000000..39edffb66f --- /dev/null +++ b/public/language/gl/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/gl/admin/advanced/cache.json b/public/language/gl/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/gl/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/gl/admin/advanced/database.json b/public/language/gl/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/gl/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/gl/admin/advanced/errors.json b/public/language/gl/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/gl/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/gl/admin/advanced/events.json b/public/language/gl/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/gl/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/gl/admin/advanced/logs.json b/public/language/gl/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/gl/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/gl/admin/appearance/customise.json b/public/language/gl/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/gl/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/gl/admin/appearance/skins.json b/public/language/gl/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/gl/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/gl/admin/appearance/themes.json b/public/language/gl/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/gl/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/gl/admin/dashboard.json b/public/language/gl/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/gl/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/gl/admin/development/info.json b/public/language/gl/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/gl/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/gl/admin/development/logger.json b/public/language/gl/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/gl/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/gl/admin/extend/plugins.json b/public/language/gl/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/gl/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/gl/admin/extend/rewards.json b/public/language/gl/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/gl/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/gl/admin/extend/widgets.json b/public/language/gl/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/gl/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/admins-mods.json b/public/language/gl/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/gl/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/categories.json b/public/language/gl/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/gl/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/digest.json b/public/language/gl/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/gl/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/gl/admin/manage/groups.json b/public/language/gl/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/gl/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/privileges.json b/public/language/gl/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/gl/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/registration.json b/public/language/gl/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/gl/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/tags.json b/public/language/gl/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/gl/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/uploads.json b/public/language/gl/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/gl/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/gl/admin/manage/users.json b/public/language/gl/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/gl/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/gl/admin/menu.json b/public/language/gl/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/gl/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/advanced.json b/public/language/gl/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/gl/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/api.json b/public/language/gl/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/gl/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/chat.json b/public/language/gl/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/gl/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/cookies.json b/public/language/gl/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/gl/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/email.json b/public/language/gl/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/gl/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/gl/admin/settings/general.json b/public/language/gl/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/gl/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/gl/admin/settings/group.json b/public/language/gl/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/gl/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/guest.json b/public/language/gl/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/gl/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/homepage.json b/public/language/gl/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/gl/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/languages.json b/public/language/gl/admin/settings/languages.json new file mode 100644 index 0000000000..e2d0c561e0 --- /dev/null +++ b/public/language/gl/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Opcións de idioma", + "description": "O idioma por defecto determina as opcións de idioma para tódolos usuarios que visitan o foro. Os usuarios poden mudar manualmente o idioma nas opcións de conta.", + "default-language": "Idioma por defecto", + "auto-detect": "Auto Detectar Opcións de Idioma para Invitados" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/navigation.json b/public/language/gl/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/gl/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/gl/admin/settings/notifications.json b/public/language/gl/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/gl/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/pagination.json b/public/language/gl/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/gl/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/post.json b/public/language/gl/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/gl/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.
", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/reputation.json b/public/language/gl/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/gl/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/social.json b/public/language/gl/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/gl/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/sockets.json b/public/language/gl/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/gl/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/sounds.json b/public/language/gl/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/gl/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/tags.json b/public/language/gl/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/gl/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/gl/admin/settings/uploads.json b/public/language/gl/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/gl/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/gl/admin/settings/user.json b/public/language/gl/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/gl/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/gl/admin/settings/web-crawler.json b/public/language/gl/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/gl/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/gl/category.json b/public/language/gl/category.json new file mode 100644 index 0000000000..0318f2cab3 --- /dev/null +++ b/public/language/gl/category.json @@ -0,0 +1,23 @@ +{ + "category": "Categoría", + "subcategories": "Subcategoría", + "new_topic_button": "Novo tema", + "guest-login-post": "Inicia sesión para poder escribir mensaxes", + "no_topics": "Non hai temas nesta categoría.
Por que non abres un?", + "browsing": "vendo agora", + "no_replies": "Ninguén respondeu", + "no_new_posts": "Non hai publicacións novas.", + "watch": "Vixiar", + "ignore": "Ignorar", + "watching": "Seguindo", + "not-watching": "Not Watching", + "ignoring": "Ignorando", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Categorías vixiadas", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/gl/email.json b/public/language/gl/email.json new file mode 100644 index 0000000000..ca56e064bd --- /dev/null +++ b/public/language/gl/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Benvido a %1", + "invite": "Invitación de %1", + "greeting_no_name": "Ola", + "greeting_with_name": "Ola %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Grazas por rexistrarte %1!", + "welcome.text2": "Para activar a túa conta, precisamos que a verifiques co enderezo de correo electrónico co que te rexistraches.", + "welcome.text3": "Un administrador aceptou a túa solicitude de rexistro. Agora pódeste conectar co teu nome de usuario e contrasinal.", + "welcome.cta": "Fai clic aquí para confirmar o teu enderezo de correo electrónico", + "invitation.text1": "%1 convidoute a unirte %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Recibimos unha petición para reiniciar o teu contrasinal, posibelmente porque o esqueciche. Se non é o caso, ignora este correo.", + "reset.text2": "Para continuar co reincio do contrasinal, por favor pica no seguinte ligazón:", + "reset.cta": "Pica aquí para reiniciar o teu contrasinal", + "reset.notify.subject": "Contrasinal cambiado", + "reset.notify.text1": "Estámosche a notificar que nun %1, o seu contrasinal foi cambiado correctamente.", + "reset.notify.text2": "Se ti non autorizache isto, por favor notifica inmediatamente a un administrador.", + "digest.latest_topics": "Últimos temas de %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Pica aquí para ir %1", + "digest.unsub.info": "Envióuseche o seguinte resumo polas túas opcións de subscrición.", + "digest.day": "día", + "digest.week": "semana", + "digest.month": "mes", + "digest.subject": "Resumo de %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Nova charla recibida de %1", + "notif.chat.cta": "Pica aquí para continuar a conversación", + "notif.chat.unsub.info": "Esta notificación de charla foiche enviada polas túas opcións de subscrición.", + "notif.post.unsub.info": "Esta notificación de mensaxe foiche enviada polas túas opcións de subscrición.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Esta é unha mensaxe de proba para verificar que o envío de correo está configurado correctamente para o seu NodeBB.", + "unsub.cta": "Pica aquí para cambiar os axustes", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Grazas!" +} \ No newline at end of file diff --git a/public/language/gl/error.json b/public/language/gl/error.json new file mode 100644 index 0000000000..c3984cf2cc --- /dev/null +++ b/public/language/gl/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Datos non válidos", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Parece que estás desconectado.", + "account-locked": "A túa conta foi bloqueada temporalmente.", + "search-requires-login": "As buscas requiren unha conta. Por favor inicia sesión ou rexístrate.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Identificador de Categoría Inválido ", + "invalid-tid": "Identificador de Tema Inválido", + "invalid-pid": "Identificador de Publicación Inválido", + "invalid-uid": "Identificador de Usuario Inválido", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Nome de Usuario Inválido", + "invalid-email": "Enderezo electrónico inválido", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Datos de Usuario Inválidos", + "invalid-password": "Contrasinal Inválido", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Especifica ámbolos dous por favor, nome de usuario e contrasinal", + "invalid-search-term": "Termo de búsqueda inválido", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "Non fomos capaces de entrar, probablemente porque a que a sesión expirou. Por favor, téntao de novo", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Valor de paxinación incorreto, ten que estar entre %1 e %2", + "username-taken": "Nome de usuario en uso", + "email-taken": "Enderezo electrónico en uso", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Non podes charlar ata que confirmes o teu correo, por favor pica aquí para confirmalo.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Non podemos confirmar o teu enderezo, por favor téntao de novo máis tarde.", + "confirm-email-already-sent": "O correo de confirmación foi enviado, agarda %1 minute(s) para enviar outro.", + "sendmail-not-found": "Non se atopa o executable \"sendmail\", por favor, asegúrate de que está instalado no teu sistema e que é accesible polo usuario que executa NodeBB. ", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Nome de usuario demasiado curto", + "username-too-long": "Nome de usuario demasiado longo.", + "password-too-long": "Contrasinal moi longa", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Usuario expulsado", + "user-banned-reason": "Desculpa, esta conta foi baneada (Razón: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Desculpa, agarda %1 second(s) antes de facer a túa primeira publicación.", + "blacklisted-ip": "Sentímolo, o teu enderezo IP foi baneado desta comunidade. Se crees que se debe a un erro, por favor, contacte cun administrador.", + "ban-expiry-missing": "Por favor, engade unha data de fin do ban", + "no-category": "A categoría non existe", + "no-topic": "O tema non existe", + "no-post": "A publicación non existe", + "no-group": "O grupo non existe", + "no-user": "O usuario non existe", + "no-teaser": "A vista previa do tema non existe", + "no-flag": "Flag does not exist", + "no-privileges": "Non tes privilexios dabondo para ver este tema.", + "category-disabled": "Categoría deshabilitada", + "topic-locked": "Tema Pechado", + "post-edit-duration-expired": "Só podes editar as publicacións %1 segundo(s) despois de envialas. ", + "post-edit-duration-expired-minutes": "Só podes editar as publicacións %1 segundo(s) despois de envialas. ", + "post-edit-duration-expired-minutes-seconds": "Só podes editar as publicacións %1 minuto(s) %2 segundo(s) despois de envialas. ", + "post-edit-duration-expired-hours": "Só podes editar as publicacións %1 hora(s) despois de envialas. ", + "post-edit-duration-expired-hours-minutes": "Só podes editar as publicacións %1 hora(s) %2 segundo(s) despois de envialas. ", + "post-edit-duration-expired-days": "Só podes editar as publicacións %1 día(s) despois de envialas. ", + "post-edit-duration-expired-days-hours": "Só podes editar as publicacións %1 día(s) %2 hora(s) despois de envialas. ", + "post-delete-duration-expired": "Só podes borrar mensaxes %1 segundo(s) despois de escribilos.", + "post-delete-duration-expired-minutes": "Só podes borrar mensaxes %1 minuto(s) despois de escribilos.", + "post-delete-duration-expired-minutes-seconds": "Só podes borrar mensaxes %1 minuto(s) e 2% segundo(s) despois de escribilos.", + "post-delete-duration-expired-hours": "Só podes borrar mensaxes %1 hora(s) despois de escribilos.", + "post-delete-duration-expired-hours-minutes": "Só podes borrar mensaxes %1 hora(s) e %2 minuto(s) despois de escribilos.", + "post-delete-duration-expired-days": "Só podes borrar mensaxes %1 día(s) despois de escribilos.", + "post-delete-duration-expired-days-hours": "Só podes borrar mensaxes %1 día(s) e %2 hora(s) despois de escribilos.", + "cant-delete-topic-has-reply": "Non podes borrar o teu tema cando xa ten respostas", + "cant-delete-topic-has-replies": "Non podes borrar o teu tema cando xa ten %1 respostas", + "content-too-short": "Por favor, introduce unha publicación máis longa. Debe conter %1 carácter(es) como mínimo.", + "content-too-long": "Por favor, introduce unha publicación máis curta. As publicacións non poden conter máis de %1 carácter(es).", + "title-too-short": "Por favor, introduce un título máis longo. Os títulos deben conter %1 carácter(es) como mínimo.", + "title-too-long": "Por favor, introduce un título máis curto. Os títulos non poden conter máis de %1 carácter(es).", + "category-not-selected": "Categoría non seleccionada", + "too-many-posts": "Só podes postear unha vez cada %1 segundo(s) - por favor agarda antes de publicar de novo.", + "too-many-posts-newbie": "Como novo usuario, só podes publicar unha vez cada %1 segundo(s) ata que acades %2 de reputación -por favor, agarda para publicar de novo.", + "already-posting": "You are already posting", + "tag-too-short": "Por favor, introduce unha etiqueta máis longa. As etiquetas deben conter %1 carácter(es) como mínimo.", + "tag-too-long": "Por favor, introduce unha etiqueta máis curta. As etiquetas non poden conter máis de %1 carácter(es).", + "not-enough-tags": "Non hai etiquetas dabondas. Os temas deben ter %1 etiqueta(s) como mínimo.", + "too-many-tags": "Moitas etiquetas. Os temas non poden ter máis de %1 etiqueta(s).", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Por favor, agarda a que remate a subida.", + "file-too-big": "O tamaño máximo permitido é %1 kB - por favor, sube un arquivo máis pequeno", + "guest-upload-disabled": "As subidas están deshabilitadas para os convidados", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Xa marcaches esta mensaxe", + "already-unbookmarked": "Xa desmarcaches esta mensaxe", + "cant-ban-other-admins": "Non podes botar outros administradores!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Eres o único administrador. Engade outros administradores antes de quitarte a ti mesmo como administrador.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Retirar privilexios de administrador desta conta antes de intentar borrala", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Tipo de imaxe inválida. Tipos admitidos: %1", + "invalid-image-extension": "Extensión de imaxe inválida", + "invalid-file-type": "Tipo de arquivo inválido. Tipos admitidos: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Nome de grupo moi curto", + "group-name-too-long": "Nome de grupo demasiado longo", + "group-already-exists": "O grupo xa existe", + "group-name-change-not-allowed": "Cambio de nome do grupo non permitido", + "group-already-member": "Xa eres parte deste grupo", + "group-not-member": "Non eres membro deste grupo", + "group-needs-owner": "Este grupo require polo menos de un propietario", + "group-already-invited": "Este usuario xa foi convidado", + "group-already-requested": "A túa petición de membresía foi enviada", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "A publicación foi eliminada", + "post-already-restored": "A publicación foi restaurada", + "topic-already-deleted": "O tema foi borrado", + "topic-already-restored": "O tema foi restaurado", + "cant-purge-main-post": "Non podes purgar a publicación principal, por favor, elimínaa no seu canto.", + "topic-thumbnails-are-disabled": "Miniaturas do tema deshabilitadas.", + "invalid-file": "Arquivo Inválido", + "uploads-are-disabled": "As subidas están deshabilitadas", + "signature-too-long": "Desculpa, a firma non pode ser maior de %1 carácter(es).", + "about-me-too-long": "Desculpa, o teu \"sobre min\" non pode supera-los %1 caracteres,", + "cant-chat-with-yourself": "Non podes falar contigo mesmo!", + "chat-restricted": "Este usuario restrinxiu as charlas. Debedes seguirvos antes de que poidas falar con el. ", + "chat-disabled": "Charlas desactivadas", + "too-many-messages": "Estás a enviar moitas mensaxes, por favor, agarda un anaco. ", + "invalid-chat-message": "Mensaxe inválida", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "Non tes permitido editar esta mensaxe.", + "cant-delete-chat-message": "Non tes permitido borrar esta mensaxe.", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Xa votache esta mensaxe.", + "reputation-system-disabled": "O sistema de reputación está deshabilitado", + "downvoting-disabled": "Os votos negativos están deshabilitados", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB atopou un erro mentras recargaba: \"%1\". NodeBB seguirá a servir os activos dos clientes aínda que se deberá desfacer o que se fixo antes da descarga.", + "registration-error": "Erro de rexistro", + "parse-error": "Algo foi mal namentras se agardaba a resposta do servidor", + "wrong-login-type-email": "Por favor, emprega o teu correo para contectarte", + "wrong-login-type-username": "Por favor, usa o teu nome de usuario para conectarte", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "Convidaches á cantidade máxima de persoas (%1 de %2).", + "no-session-found": "Non se atopou ningún inicio de sesión!", + "not-in-room": "O usuario non se atopa nesta sala", + "cant-kick-self": "Non te podes expulsar a ti mesmo do grupo", + "no-users-selected": "Ningún usuario seleccionado", + "invalid-home-page-route": "Ruta de páxina de inicio inválida", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/gl/flags.json b/public/language/gl/flags.json new file mode 100644 index 0000000000..d4a3671ea8 --- /dev/null +++ b/public/language/gl/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Estado", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Un licorca, que non hai nada marcado para revisión.", + "assignee": "Encargado", + "update": "Actualizar", + "updated": "Actualizado", + "resolved": "Resolved", + "target-purged": "O contido marcado foi purgado e xa non está dispoñible", + + "graph-label": "Daily Flags", + "quick-filters": "Filtros rápidos", + "filter-active": "Hai un ou máis filtros na lista de avisos", + "filter-reset": "Eliminar filtros", + "filters": "Filtrar opcións", + "filter-reporterId": "UID do reportador", + "filter-targetUid": "UID marcada", + "filter-type": "Tipo de aviso", + "filter-type-all": "Todo o contido", + "filter-type-post": "Publicar", + "filter-type-user": "User", + "filter-state": "Estado", + "filter-assignee": "UID do encargado", + "filter-cid": "Categoría", + "filter-quick-mine": "Asignado a min", + "filter-cid-all": "Tódalas categorías", + "apply-filters": "Aplicar filtros", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Usuario marcado", + "view-profile": "Ver perfil", + "start-new-chat": "Comezar novo chat", + "go-to-target": "Ver contido marcado", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Ver perfil", + "user-edit": "Editar perfil", + + "notes": "Notas do aviso", + "add-note": "Engadir nota", + "no-notes": "Ningunha nota foi compartida", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Nota engadida", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Non hai historial de avisos", + + "state-all": "Tódolos estados", + "state-open": "Novo/Abrir", + "state-wip": "Traballo en progreso", + "state-resolved": "Resolto", + "state-rejected": "Rexeitado", + "no-assignee": "Non asignado", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Por favor, especifique o seu motivo para marcar %1 %2 para revisión. Alternativamente, empregue un dos botóns de reporte rápido se fose pertinente.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Ofensivo", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Motivo para reportar este contido...", + "modal-submit": "Enviar Reporte", + "modal-submit-success": "Contido marcado para moderación", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/gl/global.json b/public/language/gl/global.json new file mode 100644 index 0000000000..29a24d7cca --- /dev/null +++ b/public/language/gl/global.json @@ -0,0 +1,126 @@ +{ + "home": "Inicio", + "search": "Busca", + "buttons.close": "Pechar", + "403.title": "Acceso Denegado", + "403.message": "Ao parecer, non tes permisos para acceder a esta páxina.", + "403.login": "Quizais deberías tentar iniciar sesión?", + "404.title": "Non Atopado", + "404.message": "Ao parecer, esta páxina non existe. Volver ao Inicio.", + "500.title": "Erro interno.", + "500.message": "Ups! Parece que algo saíu mal!", + "400.title": "Petición incorrecta", + "400.message": "Parece que a dirección é errónea. Por favor, compróbaa e proba de novo. No caso contrario, volve ó inicio.", + "register": "Rexistrarse", + "login": "Conectarse", + "please_log_in": "Por favor, conéctate", + "logout": "Desconectarse", + "posting_restriction_info": "As publicacións están restrinxidas a membros rexistrados, pica aquí para rexistrarte.", + "welcome_back": "Benvido de novo!", + "you_have_successfully_logged_in": "Sentidiño!", + "save_changes": "Gardar Cambios", + "save": "Gardar", + "close": "Pechar ", + "pagination": "Paxinación", + "pagination.out_of": "%1 de %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Categorías", + "header.recent": "Recentes", + "header.unread": "Non lidas", + "header.tags": "Etiquetas", + "header.popular": "Populares", + "header.top": "Top", + "header.users": "Usuarios", + "header.groups": "Grupos", + "header.chats": "Charlas", + "header.notifications": "Notificacións", + "header.search": "Búsqueda ", + "header.profile": "Perfil", + "header.navigation": "Navegación", + "notifications.loading": "Cargando Notificacións", + "chats.loading": "Cargando Charlas", + "motd.welcome": "Benvido a NodeBB, a plataforma de discusión do futuro.", + "previouspage": "Páxina Anterior", + "nextpage": "Páxina Seguinte", + "alert.success": "Éxito", + "alert.error": "Erro", + "alert.banned": "Expulsado", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Xa non sigues a %1!", + "alert.follow": "Agora sigues a %1!", + "users": "Usuarios ", + "topics": "Temas", + "posts": "Publicacións", + "x-posts": "%1 posts", + "best": "Mellor", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Positivos", + "upvoted": "Votado positivamente", + "downvoters": "Negativos", + "downvoted": "Votado negativamente", + "views": "Vistas", + "posters": "Posters", + "reputation": "Reputación", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "ler máis", + "more": "Máis", + "none": "None", + "posted_ago_by_guest": "Publicado %1 por Invitado", + "posted_ago_by": "Publicado %1 por %2", + "posted_ago": "Publicado %1", + "posted_in": "publicado en %1", + "posted_in_by": "publicado en %1 por %2", + "posted_in_ago": "Publicado en %1 %2", + "posted_in_ago_by": "Publicado en %1 %2 por %3", + "user_posted_ago": "%1 publicado %2", + "guest_posted_ago": "Invitado publicou %1", + "last_edited_by": "última edición por %1", + "norecentposts": "Non hai mensaxes recentes", + "norecenttopics": "Non hai temas recentes", + "recentposts": "Mensaxes recentes", + "recentips": "Conectado recentemente en IPs", + "moderator_tools": "Ferramentas de Moderación", + "online": "En línea", + "away": "Fóra", + "dnd": "Non dispoñible", + "invisible": "Invisible", + "offline": "Desconectado", + "email": "Correo Electrónico", + "language": "Idioma", + "guest": "Invitado", + "guests": "Invitados", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Foro Actualizado", + "updated.message": "O foro acaba de ser actualizado á última versión. Pica aquí para actualizar a páxina.", + "privacy": "Privacidad", + "follow": "Seguir", + "unfollow": "Deixar de seguir", + "delete_all": "Borrar todo", + "map": "Mapa", + "sessions": "Inicios de sesión", + "ip_address": "Enderezo IP", + "enter_page_number": "Escribe o número da páxina", + "upload_file": "Subir arquivo ", + "upload": "Subir", + "uploads": "Uploads", + "allowed-file-types": "Os tipos de arquivos permitidos son: %1", + "unsaved-changes": "Non gardaches tódolos cambios. Queres continuar e saír da páxina?", + "reconnecting-message": "Conexión perdida. Reconectando a %1.", + "play": "Reproducir", + "cookies.message": "Esta web emprega cookies para asegurar que recibes unha mellor experiencia de navegación.", + "cookies.accept": "De Acordo!", + "cookies.learn_more": "Saber máis", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/gl/groups.json b/public/language/gl/groups.json new file mode 100644 index 0000000000..fd1229eb47 --- /dev/null +++ b/public/language/gl/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupos", + "view_group": "Ver grupo", + "owner": "Dono do grupo", + "new_group": "Crear un novo grupo", + "no_groups_found": "Non hai grupos que ver", + "pending.accept": "Aceptar", + "pending.reject": "Rexeitar", + "pending.accept_all": "Aceptar todo", + "pending.reject_all": "Rexeitar todo", + "pending.none": "Non hai membros pendentes", + "invited.none": "Non hai convidados pendentes", + "invited.uninvite": "Rexeitar invitación", + "invited.search": "Procurar un usuario para convidar ao grupo", + "invited.notification_title": "Fuches convidado a unirte%1", + "request.notification_title": "Petición de membresía ao grupo de %1", + "request.notification_text": "%1 convidoute a facerte membro de %2", + "cover-save": "Gardar", + "cover-saving": "Gardando", + "details.title": "Detalles do grupo", + "details.members": "Lista de membros", + "details.pending": "Membros Pendentes", + "details.invited": "Membros convidados", + "details.has_no_posts": "Non hai publicacións neste grupo", + "details.latest_posts": "Últimas Publicacións", + "details.private": "Privado", + "details.disableJoinRequests": "Desactivar as peticións de unión", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Outorgar/Rescindir propiedade", + "details.kick": "Expulsar", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Administración do Grupo", + "details.group_name": "Nome do Grupo", + "details.member_count": "Conta de Membros", + "details.creation_date": "Data de Creación", + "details.description": "Descripción", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Vista Previa da Insignia", + "details.change_icon": "Cambiar Icona", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Texto da Insignia", + "details.userTitleEnabled": "Amosar Insignia", + "details.private_help": "Se está habilitado, a unión de grupos require da aprobación do dono dun deles.", + "details.hidden": "Oculto", + "details.hidden_help": "Se está habilitado, este grupo non se poderá atopar na listaxe de grupos e os usuarios deberán ser convidados manualmente.", + "details.delete_group": "Eliminar Grupo", + "details.private_system_help": "Os grupos privados están desactivados ao nivel do sistema, esta opción non trocará nada.", + "event.updated": "Os detalles do grupo foron actualizados", + "event.deleted": "O grupo \"%1\" foi borrado.", + "membership.accept-invitation": "Aceptar ", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitación Pendente", + "membership.join-group": "Unirse ao grupo", + "membership.leave-group": "Marchar do grupo", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Rexeitar", + "new-group.group_name": "Nome do grupo:", + "upload-group-cover": "Cargar foto para o grupo", + "bulk-invite-instructions": "Escribe unha lista de nomes de usuario a convidar a este grupo separados por comas", + "bulk-invite": "Convite múltiple", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/gl/ip-blacklist.json b/public/language/gl/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/gl/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/gl/language.json b/public/language/gl/language.json new file mode 100644 index 0000000000..45618ca29e --- /dev/null +++ b/public/language/gl/language.json @@ -0,0 +1,5 @@ +{ + "name": "Galician", + "code": "gl", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/gl/login.json b/public/language/gl/login.json new file mode 100644 index 0000000000..b1508c9cda --- /dev/null +++ b/public/language/gl/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Usuario / Correo electrónico", + "username": "Usuario", + "remember_me": "Lembrarme?", + "forgot_password": "Esqueciches o teu contrasinal?", + "alternative_logins": "Métodos alternativos", + "failed_login_attempt": "Erro ao iniciar sesión", + "login_successful": "Sesión iniciada con éxito!", + "dont_have_account": "Aínda non tes conta?", + "logged-out-due-to-inactivity": "Debido a inactividade fuches desconectado do Panel de Control de Administradores", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/gl/modules.json b/public/language/gl/modules.json new file mode 100644 index 0000000000..b1e71f9472 --- /dev/null +++ b/public/language/gl/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Enviar", + "chat.no_active": "Non tes charlas activas.", + "chat.user_typing": "%1 está a escribir...", + "chat.user_has_messaged_you": "%1 enviouche unha mensaxe.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Por favor, seleccione un destinatario para ver o historial das mensaxes ", + "chat.no-users-in-room": "Non hai usuarios nesta sala", + "chat.recent-chats": "Charlas Recentes", + "chat.contacts": "Contactos", + "chat.message-history": "Historial de mensaxes", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Marchar do chat", + "chat.minimize": "Minimize", + "chat.maximize": "Agrandar", + "chat.seven_days": "7 Días", + "chat.thirty_days": "30 Días", + "chat.three_months": "3 Meses", + "chat.delete_message_confirm": "Estás seguro de que desexas eliminar esta mensaxe?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Elaborar", + "composer.show_preview": "Amosar vista previa", + "composer.hide_preview": "Agochar vista previa", + "composer.user_said_in": "%1 dixo en %2", + "composer.user_said": "%1 dixo:", + "composer.discard": "Estás seguro de que queres desfacer esta publicación?", + "composer.submit_and_lock": "Enviar e bloquear", + "composer.toggle_dropdown": "Alternar despregable", + "composer.uploading": "Subindo %1", + "composer.formatting.bold": "Negriña", + "composer.formatting.italic": "Itálica", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Tachado", + "composer.formatting.code": "Code", + "composer.formatting.link": "Ligazón", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Subir foto", + "composer.upload-file": "Subir arquivo", + "composer.zen_mode": "Modo Zen", + "composer.select_category": "Selecciona unha categoría", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "De acordo", + "bootbox.cancel": "Cancelar", + "bootbox.confirm": "Confirmar", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Colocar foto de portada", + "cover.dragging_message": "Arrastra a foto d portada ó lugar que desexes e fai clic en \"Gardar\"", + "cover.saved": "Imaxe e posición da foto de portada gardadas.", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/gl/notifications.json b/public/language/gl/notifications.json new file mode 100644 index 0000000000..9c36fc9ebd --- /dev/null +++ b/public/language/gl/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notificacións", + "no_notifs": "Non tes notificacións novas", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Volver ao %1", + "outgoing_link": "Ligazón saínte", + "outgoing_link_message": "Estás saíndo %1", + "continue_to": "Continuar a %1", + "return_to": "Volver a %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Tes notificacións non lidas", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Nova mensaxe de %1", + "upvoted_your_post_in": "%1 votoute positivo en %2.", + "upvoted_your_post_in_dual": "%1 e %2 votaron positivamente a túa mensaxe en %3.", + "upvoted_your_post_in_multiple": "%1 e %2 máis votaron positivamente a túa mensaxe en %3.", + "moved_your_post": "%1 moveu a túa publicación a%2", + "moved_your_topic": "%1 moveu %2", + "user_flagged_post_in": "%1 reportou unha mensaxe en %2", + "user_flagged_post_in_dual": "%1 e %2 reportaron a túa mensaxe en %3", + "user_flagged_post_in_multiple": "%1 e outras %2 persoas reportaron unha mensaxe en %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 publicou unha resposta en: %2", + "user_posted_to_dual": "%1 e %2 responderon a %3", + "user_posted_to_multiple": "%1 e outras %2 persoas responderon a: %3", + "user_posted_topic": "%1 publicou un novo tema: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 comezou a seguirte.", + "user_started_following_you_dual": "%1 e %2 comezaron a seguirte.", + "user_started_following_you_multiple": "%1 e %2 máis comezaron a seguirte.", + "new_register": "%1 enviou unha petición de rexistro.", + "new_register_multiple": "Hai %1 peticións de rexistros pendentes de revisión", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Correo confirmado", + "email-confirmed-message": "Grazas por validar o teu correo. A túa conta agora está activada.", + "email-confirm-error-message": "Houbo un problema validando o teu correo. Poida que o código fose inválido ou expirase. ", + "email-confirm-sent": "Correo de confirmación enviado.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/gl/pages.json b/public/language/gl/pages.json new file mode 100644 index 0000000000..b540b26e72 --- /dev/null +++ b/public/language/gl/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Inicio", + "unread": "Temas non lidos", + "popular-day": "Temas populares de hoxe", + "popular-week": "Temas populares da semana", + "popular-month": "Temas populares do mes", + "popular-alltime": "Temas populares de tódolos tempos", + "recent": "Temas recentes", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "Lista negra de IPs", + "post-queue": "Post Queue", + "users/online": "Usuarios conectados", + "users/latest": "Últimos usuarios", + "users/sort-posts": "Usuarios con máis temas", + "users/sort-reputation": "Usuarios máis reputados", + "users/banned": "Usuarios Expulsados", + "users/most-flags": "Usuarios máis reportados", + "users/search": "Búsqueda de usuarios", + "notifications": "Notificacións", + "tags": "Etiquetas", + "tag": "Topics tagged under "%1"", + "register": "Rexistrar conta", + "registration-complete": "Rexistro completado", + "login": "Ingresa coa túa conta", + "reset": "Reinicia o teu contrasinal", + "categories": "Categorías", + "groups": "Grupos", + "group": "%1 grupo", + "chats": "Charlas", + "chat": "Falando con %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editando \"%1\"", + "account/edit/password": "Editando contrasinal \"%1\"", + "account/edit/username": "Editando nome de usuario \"%1\"", + "account/edit/email": "Editando o correo \"%1\"", + "account/info": "Información da conta", + "account/following": "Xente %1 seguindo", + "account/followers": "Xente a quen segues %1", + "account/posts": "Publicación de %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Temas de %1", + "account/groups": "%1's Grupos", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Mensaxes marcadas", + "account/settings": "Opcións de Usuario", + "account/watched": "Temas vistos por %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Mensaxes votadas positivamente por %1", + "account/downvoted": "Mensaxes votadas negativamente por %1", + "account/best": "Mellores mensaxes escritas por %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Enderezo electrónico confirmado", + "maintenance.text": "%1 está baixo mantemento. Por favor, volve máis tarde.", + "maintenance.messageIntro": "A máis, o administrador deixou esta mensaxe: ", + "throttled.text": "&1 non está dispoñible debido a unha carga excesiva. Por favor, volva noutro momento" +} \ No newline at end of file diff --git a/public/language/gl/post-queue.json b/public/language/gl/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/gl/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/gl/recent.json b/public/language/gl/recent.json new file mode 100644 index 0000000000..433d893498 --- /dev/null +++ b/public/language/gl/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recentes", + "day": "Día", + "week": "Semana", + "month": "Mes", + "year": "Ano", + "alltime": "Dende o principio", + "no_recent_topics": "Non hai temas recentes", + "no_popular_topics": "Non hai temas populares", + "there-is-a-new-topic": "Hai un novo tema", + "there-is-a-new-topic-and-a-new-post": "Hai un novo tema e unha nova publicación", + "there-is-a-new-topic-and-new-posts": "Hai un novo tema e %1 nova publicación", + "there-are-new-topics": "Hai %1 novos temas.", + "there-are-new-topics-and-a-new-post": "Hay %1 novos temas e unha nova publicación.", + "there-are-new-topics-and-new-posts": "Hay %1 novos temas e %2 novas publicacións", + "there-is-a-new-post": "Hai unha nova publicación", + "there-are-new-posts": "Hay %1 novas publicacións.", + "click-here-to-reload": "Pica aquí para recargar." +} \ No newline at end of file diff --git a/public/language/gl/register.json b/public/language/gl/register.json new file mode 100644 index 0000000000..8c24a4e487 --- /dev/null +++ b/public/language/gl/register.json @@ -0,0 +1,32 @@ +{ + "register": "Rexistrarse", + "cancel_registration": "Cancelar rexistro", + "help.email": "Por defecto, o teu correo electrónico está oculto ao público.", + "help.username_restrictions": "O nome de usuario debe ter entre %1 e %2 caracteres. Os outros usuarios poden mencionarte escribindo @usuario.", + "help.minimum_password_length": "O teu contrasinal debe ter polo menos %1 caracteres.", + "email_address": "Correo electrónico", + "email_address_placeholder": "Introduce o teu correo electrónico", + "username": "Nome de Usuario", + "username_placeholder": "Introduce o teu nome de usuario", + "password": "Contrasinal", + "password_placeholder": "Introduce o teu contrasinal", + "confirm_password": "Confirma o teu contrasinal", + "confirm_password_placeholder": "Confirma o teu contrasinal", + "register_now_button": "Rexistrarse agora", + "alternative_registration": "Métodos de rexistro alternativos", + "terms_of_use": "Termos e Condicións de Uso", + "agree_to_terms_of_use": "Acepto os Termos e Condicións de Uso", + "terms_of_use_error": "Debes acepta-los termos de uso", + "registration-added-to-queue": "O teu rexistro foi engadido á cola de aprobación. Recibirás un correo electrónico cando sexa aceptado por un administrador.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/gl/reset_password.json b/public/language/gl/reset_password.json new file mode 100644 index 0000000000..ef1f8ff742 --- /dev/null +++ b/public/language/gl/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Restaurar contrasinal", + "update_password": "Actualiza o contrasinal", + "password_changed.title": "Contrasinal modificado", + "password_changed.message": "

Contrasinal modificado, por favor inicia sesión de novo.", + "wrong_reset_code.title": "Código incorrecto", + "wrong_reset_code.message": "O código de reinicio do contrasinal é incorrecto. Por favor, téntao de novo ou pide un novo código.", + "new_password": "Novo Contrasinal", + "repeat_password": "Confirma o teu contrasinal", + "changing_password": "Changing Password", + "enter_email": "Por favor, introduce o teucorreo electrónico e enviarémosche un correo coas instruccóns para restaurar a túa conta", + "enter_email_address": "Introduce o teu correo electrónico", + "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.", + "invalid_email": "Correo inválido / O correo non existe!", + "password_too_short": "O contrasinal é moi curto, por favor, escolle outro.", + "passwords_do_not_match": "Os contrasinais non coinciden.", + "password_expired": "O teu contrasinal expirou, por favor, escolle un novo." +} \ No newline at end of file diff --git a/public/language/gl/search.json b/public/language/gl/search.json new file mode 100644 index 0000000000..f5f8f12919 --- /dev/null +++ b/public/language/gl/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 resultado(s) coincide(n) con \"%2\", (%3 segundos)", + "no-matches": "Non se atoparon coincidencias", + "advanced-search": "Busca Avanzada", + "in": "En", + "titles": "Títulos", + "titles-posts": "Títulos e publicacións", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Publicado por", + "in-categories": "En categorías", + "search-child-categories": "Buscar categorías fillas", + "has-tags": "Has tags", + "reply-count": "Número de Respostas", + "at-least": "Como mínimo", + "at-most": "Como máximo", + "relevance": "Relevance", + "post-time": "Data de publicación", + "votes": "Votes", + "newer-than": "Máis recente que", + "older-than": "Máis antigo que", + "any-date": "Calquera data", + "yesterday": "Onte", + "one-week": "Unha semana", + "two-weeks": "Dúas semanas", + "one-month": "Un mes", + "three-months": "Tres meses", + "six-months": "Seis meses", + "one-year": "Un ano", + "sort-by": "Ordenar por", + "last-reply-time": "Data da última resposta", + "topic-title": "Título do tema", + "topic-votes": "Topic votes", + "number-of-replies": "Número de respostas", + "number-of-views": "Número de visualizacións", + "topic-start-date": "Data de inicio do tema", + "username": "Usuario", + "category": "Categoría", + "descending": "En orde descendente", + "ascending": "En orde ascendente", + "save-preferences": "Gardar preferencias", + "clear-preferences": "Desbotar preferencias", + "search-preferences-saved": "Preferencias de búsqueda gardadas", + "search-preferences-cleared": "Preferencias de búsqueda desbotadas", + "show-results-as": "Amosar resultados como", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/gl/success.json b/public/language/gl/success.json new file mode 100644 index 0000000000..ab75efd29b --- /dev/null +++ b/public/language/gl/success.json @@ -0,0 +1,7 @@ +{ + "success": "Éxito", + "topic-post": "Publicaches con éxito.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Autenticación exitosa", + "settings-saved": "Configuración gardada!" +} \ No newline at end of file diff --git a/public/language/gl/tags.json b/public/language/gl/tags.json new file mode 100644 index 0000000000..35254979db --- /dev/null +++ b/public/language/gl/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Non hai temas con esa etiqueta.", + "tags": "Etiquetas", + "enter_tags_here": "Pon as etiquetas aquí, entre %1 e %2 caracteres cada unha.", + "enter_tags_here_short": "Introduce as etiquetas", + "no_tags": "Non hai etiquetas todavía.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/gl/top.json b/public/language/gl/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/gl/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/gl/topic.json b/public/language/gl/topic.json new file mode 100644 index 0000000000..ae09fbae43 --- /dev/null +++ b/public/language/gl/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tema", + "title": "Title", + "no_topics_found": "Non se atoparon temas!", + "no_posts_found": "Non se atoparon publicacións!", + "post_is_deleted": "Esta publicación foi eliminada!", + "topic_is_deleted": "Este tema foi eliminado!", + "profile": "Perfil", + "posted_by": "Publicado por %1", + "posted_by_guest": "Publicado por Invitado", + "chat": "Chat", + "notify_me": "Serás notificado canto haxa novas respostas neste tema", + "quote": "Citar", + "reply": "Responder", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Responder como tema", + "guest-login-reply": "Identifícate para responder", + "login-to-view": "🔒 Log in to view", + "edit": "Editar", + "delete": "Borrar", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Purgar", + "restore": "Restaurar", + "move": "Mover", + "change-owner": "Change Owner", + "fork": "Dividir", + "link": "Ligazón", + "share": "Compartir", + "tools": "Ferramentas", + "locked": "Pechado", + "pinned": "Fixo", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Movido", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Pica aquí para volver á última mensaxe lida neste tema ", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Este tema foi borrado. Só os usuarios con privilexios administrativos poden velo.", + "following_topic.message": "Agora recibirás notificacións cando alguén publique neste tema.", + "not_following_topic.message": "Poderás ver este tema na lista de No Lidos, pero non recibirás notificacións cando alguén escriba nel.", + "ignoring_topic.message": "Xa non verás este fío na lista de fíos non lidos. Serás notificado cando sexas mencionado ou a túa publicación sexa votada.", + "login_to_subscribe": "Por favor, identifícate para subscribirte a este tema.", + "markAsUnreadForAll.success": "Publicación marcada como non lida para todos.", + "mark_unread": "Marcar coma non lido", + "mark_unread.success": "Tema marcado como non lido", + "watch": "Vixiar", + "unwatch": "Deixar de vixiar", + "watch.title": "Serás notificado canto haxa novas respostas neste tema", + "unwatch.title": "Deixar de seguir este tema", + "share_this_post": "Compartir esta publicación", + "watching": "Seguindo", + "not-watching": "Non seguindo", + "ignoring": "Ignorar", + "watching.description": "Notificádeme das novas repostas.
Amosa-lo fío nos non lidos.", + "not-watching.description": "Non me notifiquedes as novas respostas.
Amosa-lo fío en non lidos se a categoría non está ignorada.", + "ignoring.description": "Non me notifiquedes as novas respostas.
Non amosa-lo fío en non lidos.", + "thread_tools.title": "Ferramentas do tema", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Fixar Tema", + "thread_tools.unpin": "Despegar Tema", + "thread_tools.lock": "Pechar Tema", + "thread_tools.unlock": "Reabrir Tema", + "thread_tools.move": "Mover Tema", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Mover todo", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Dividir Tema", + "thread_tools.delete": "Borrar Tema", + "thread_tools.delete-posts": "Eliminar publicacións", + "thread_tools.delete_confirm": "Estás seguro de que desexas eliminar este tema?", + "thread_tools.restore": "Restaurar Tema", + "thread_tools.restore_confirm": "Estás seguro de que desexas restaurar este tema?", + "thread_tools.purge": "Purgar Tema", + "thread_tools.purge_confirm": "Estás seguro de que desexas eliminar definitivamente (purgar) este tema?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Estás seguro de que desexas eliminar esta publicación?", + "post_restore_confirm": "Estás seguro de que desexas restaurar esta publicación?", + "post_purge_confirm": "Estás seguro de que desexas purgar esta publicación??", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Cargando categorías", + "confirm_move": "Mover", + "confirm_fork": "Dividir", + "bookmark": "Marcador", + "bookmarks": "Marcadores", + "bookmarks.has_no_bookmarks": "Aínda non ten marcadores", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Cargando máis publicacións", + "move_topic": "Mover Tema", + "move_topics": "Mover Temas", + "move_post": "Mover publicación", + "post_moved": "Publicación movida correctamente!", + "fork_topic": "Dividir Tema", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Fai clic nas publicacións que queiras dividir", + "fork_no_pids": "Non seleccionaches ninguna publicación!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 mensaxe(s) seleccionada(s)", + "fork_success": "Creouse un novo tema a partir do orixinal! Fai clic aquí para ir ó novo tema.", + "delete_posts_instruction": "Fai clic nas mensaxes que queres eliminar/limpar", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Introduce o título do teu tema", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Descartar", + "composer.submit": "Enviar", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "En resposta a %1", + "composer.new_topic": "Novo tema", + "composer.editing": "Editing", + "composer.uploading": "subindo...", + "composer.thumb_url_label": "Agrega unha URL de miniatura para o tema", + "composer.thumb_title": "Agregar miniatura a este tema", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Ou subir un arquivo", + "composer.thumb_remove": "Limpar campos", + "composer.drag_and_drop_images": "Arrastra as imaxes aquí", + "more_users_and_guests": "%1 usuario(s) e %2 invitado(s) máis", + "more_users": "%1 usuario(s) máis", + "more_guests": "%1 invitado(s) máis", + "users_and_others": "%1 e outros %2", + "sort_by": "Ordenar por", + "oldest_to_newest": "Máis antigo a máis novo", + "newest_to_oldest": "Máis novo a máis antigo", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Crear un novo tema no seu lugar?", + "stale.warning": "O tema no que queres publicar é bastante vello. Queres crear un novo tema no seu lugar e incluir unha referencia a este na túa mensaxe?", + "stale.create": "Crear un novo tema", + "stale.reply_anyway": "Publicar neste tema de tódolos xeitos", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/gl/unread.json b/public/language/gl/unread.json new file mode 100644 index 0000000000..60179e9afa --- /dev/null +++ b/public/language/gl/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Non lidas", + "no_unread_topics": "Non hai temas non lidos", + "load_more": "Cargar máis", + "mark_as_read": "Marcar como lido", + "selected": "Seleccionado", + "all": "Todos", + "all_categories": "Tódalas categorías", + "topics_marked_as_read.success": "Temas marcados como lidos", + "all-topics": "Tódolos Temas", + "new-topics": "Temas Novos", + "watched-topics": "Temas Suscritos", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/gl/uploads.json b/public/language/gl/uploads.json new file mode 100644 index 0000000000..50f44d04bd --- /dev/null +++ b/public/language/gl/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Subindo o arquivo...", + "select-file-to-upload": "Selecciona un arquivo para subir!", + "upload-success": "Arquivo subido correctamente!", + "maximum-file-size": "Máximo %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/gl/user.json b/public/language/gl/user.json new file mode 100644 index 0000000000..95c0f45f50 --- /dev/null +++ b/public/language/gl/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Expulsado", + "muted": "Muted", + "offline": "Desconectado", + "deleted": "Deleted", + "username": "Nome de usuario", + "joindate": "Data de ingreso", + "postcount": "Reconto de mensaxes", + "email": "Enderezo Electrónico", + "confirm_email": "Confirma o teu enderezo electrónico", + "account_info": "Información da conta", + "admin_actions_label": "Administrative Actions", + "ban_account": "Suspender conta", + "ban_account_confirm": "Estás seguro de que desexas expulsar a este usuario?", + "unban_account": "Readmitir conta", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Borrar conta.", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Conta borrada", + "account-content-deleted": "Account content deleted", + "fullname": "Nome completo", + "website": "Páxina web", + "location": "Localización", + "age": "Idade", + "joined": "Unido", + "lastonline": "Última conexión:", + "profile": "Perfil", + "profile_views": "Visitas ao perfil:", + "reputation": "Reputación", + "bookmarks": "Marcadores", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Visto", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Seguidores", + "following": "Seguindo", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "Sobre min", + "signature": "Firma", + "birthday": "Aniversario", + "chat": "Chat", + "chat_with": "Continuar a falar con %1", + "new_chat_with": "Comezar a falar con %1", + "flag-profile": "Flag Profile", + "follow": "Seguir", + "unfollow": "Deixar de seguir", + "more": "máis", + "profile_update_success": "O perfil foi actualizado correctamente!", + "change_picture": "Cambia-la foto", + "change_username": "Cambia-lo nome de usuario", + "change_email": "Cambia-lo correo", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Editar", + "edit-profile": "Editar Perfil", + "default_picture": "Icona por defecto.", + "uploaded_picture": "Foto subida", + "upload_new_picture": "Subir unha nova foto", + "upload_new_picture_from_url": "Subir unha nova foto dende un ligazón", + "current_password": "Contrasinal actual", + "change_password": "Cambia-lo contrasinal", + "change_password_error": "Contrasinal inválido", + "change_password_error_wrong_current": "O contrasinal actual é incorrecto!", + "change_password_error_match": "Os contrasinais teñen que coincidir!", + "change_password_error_privileges": "Non tes autorización para cambia-lo contrasinal", + "change_password_success": "O teu contrasinal foi actualizado!", + "confirm_password": "Confirma o teu contrasinal", + "password": "Contrasinal", + "username_taken_workaround": "Ese nome de usuario xa estaba collido, así que o modificamos lixeiramente. Agora o teu nome é %1", + "password_same_as_username": "O teu contrasinal e o teu nome de usuario son os mesmos, por favor, escolle outro contrasinal.", + "password_same_as_email": "O teu contrasinal é igual que o teu enderezo electrónico, por favor, escolle outro contrasinal.", + "weak_password": "Weak password.", + "upload_picture": "Subir foto", + "upload_a_picture": "Subir unha foto", + "remove_uploaded_picture": "Borrar unha foto subida", + "upload_cover_picture": "Subir imaxen de portada", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Opcións", + "show_email": "Amosa-lo meu Email", + "show_fullname": "Amosa-lo meu Nome Completo", + "restrict_chats": "Permiti-lo chat só con usuarios aos que sigo.", + "digest_label": "Subscribirse ao resumo", + "digest_description": "Subscribirse as actualizacións por correo deste foro (novas notificacións e temas), segundo un calendario prefixado.", + "digest_off": "Desactivado", + "digest_daily": "Diariamente", + "digest_weekly": "Semanalmente", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Mensualmente", + "has_no_follower": "Ninguén segue a este usuario :(", + "follows_no_one": "Este usuario non sigue a ninguén :(", + "has_no_posts": "Este usuario aínda non posteu.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Este usuario aínda non publicou ningún tema.", + "has_no_watched_topics": "Este usuario aínda non viu ningún tema.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "Este usuario aínda non votou positivamente ningunha mensaxe.", + "has_no_downvoted_posts": "Este usuario aínda non votou negativamente ninguna mensaxe.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Correo Agochado", + "hidden": "Agochado", + "paginate_description": "Paxinar temas e publicacións no canto de usar scroll infinito", + "topics_per_page": "Temas por páxina", + "posts_per_page": "Mensaxes por páxina", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Opcións de busca", + "open_links_in_new_tab": "Abrir ligazóns externos nunca nova pestaña", + "enable_topic_searching": "Permitir buscar dentro dun tema", + "topic_search_help": "Se se activa, o buscador do NodeBB superporase ao propio do navegador dentro de cada tema, permitindo buscar en todo o tema e non só naquilo que se presenta na pantalla.", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Logo de enviar unha resposta, mostrar a nova mensaxe", + "follow_topics_you_reply_to": "Segui-los temas aos que respondes", + "follow_topics_you_create": "Segui-los temas que creaches ti", + "grouptitle": "Título do Grupo", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Sen titulo de grupo", + "select-skin": "Seleccionar apariencia", + "select-homepage": "Escolla unha páxina de inicio", + "homepage": "Páxina de inicio", + "homepage_description": "Escolla unha páxina para o seu uso habitual como a páxina principal do foro ou \"Ningún\" para empregar a páxina de inicio", + "custom_route": "Modificar páxina de ruta", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Servizos de Inicio de Sesión Único", + "sso.associated": "Asociado con", + "sso.not-associated": "Pica aquí para asociarte con", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Últimos reportes", + "info.no-flags": "Non se atopou ninguna mensaxe reportada", + "info.ban-history": "Histórico recente de bans", + "info.no-ban-history": "Este usuario nunca foi baneado", + "info.banned-until": "Baneado hasta %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Baneado permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Motivo non especificado", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Histórico de nome de usuario", + "info.email-history": "Histórico de Correo Electrónico", + "info.moderation-note": "Nota do Moderador", + "info.moderation-note.success": "Nota do moderador gardada", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/gl/users.json b/public/language/gl/users.json new file mode 100644 index 0000000000..d79d568af9 --- /dev/null +++ b/public/language/gl/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Últimos usuarios", + "top_posters": "Maiores Publicadores", + "most_reputation": "Máis Reputados", + "most_flags": "Máis reportados", + "search": "Busca", + "enter_username": "Introduce o nome de usuario a procurar", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Cargar máis", + "users-found-search-took": "%1 usuario(s) atopado! A procura tomou %2 segundos.", + "filter-by": "Filtrar por", + "online-only": "Só conectados", + "invite": "Convidar", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "A invitación foi enviada a %1", + "user_list": "Lista de Usuarios", + "recent_topics": "Temas recentes", + "popular_topics": "Temas Populares", + "unread_topics": "Temas non lidos", + "categories": "Categorías", + "tags": "Etiquetas", + "no-users-found": "Non se atoparon usuarios!" +} \ No newline at end of file diff --git a/public/language/he/_DO_NOT_EDIT_FILES_HERE.md b/public/language/he/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/he/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/he/admin/admin.json b/public/language/he/admin/admin.json new file mode 100644 index 0000000000..a8b9f1af40 --- /dev/null +++ b/public/language/he/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "האם אתה בטוח שאתה רוצה לבנות מחדש ולאתחל את NodeBB?", + "alert.confirm-restart": "האם אתה בטוח שאתה רוצה לאתחל מחדש את NodeBB?", + + "acp-title": "%1 | לוח בקרה למנהל NodeBB", + "settings-header-contents": "תוכן", + "changes-saved": "שינויים שנשמרו", + "changes-saved-message": "השינויים שלך בתצורת NodeBB נשמרו.", + "changes-not-saved": "השינויים לא נשמרו", + "changes-not-saved-message": "NodeBB נתקל בעיה בשמירת השינויים שלך. (%1)" +} \ No newline at end of file diff --git a/public/language/he/admin/advanced/cache.json b/public/language/he/admin/advanced/cache.json new file mode 100644 index 0000000000..35a5167de9 --- /dev/null +++ b/public/language/he/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "מטמון פוסטים", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "גודל מטמון פוסטים", + "items-in-cache": "פריטים במטמון" +} \ No newline at end of file diff --git a/public/language/he/admin/advanced/database.json b/public/language/he/admin/advanced/database.json new file mode 100644 index 0000000000..b18bbe3f6a --- /dev/null +++ b/public/language/he/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 ביטים", + "x-mb": "%1 מגה בייט", + "x-gb": "%1 ג'יגה בייט", + "uptime-seconds": "זמן מאתחול אחרון בשניות", + "uptime-days": "זמן מאתחול אחרון בימים", + + "mongo": "Mongo", + "mongo.version": "גרסת MongoDB", + "mongo.storage-engine": "מנוע אחסון", + "mongo.collections": "אוסף", + "mongo.objects": "אובייקטים", + "mongo.avg-object-size": "גודל אובייקט ממוצע", + "mongo.data-size": "גודל המידע", + "mongo.storage-size": "גודל האחסון", + "mongo.index-size": "גודל האינדקס", + "mongo.file-size": "גודל הקובץ", + "mongo.resident-memory": "זכרון קיים", + "mongo.virtual-memory": "זיכרון וירטואלי", + "mongo.mapped-memory": "זיכרון ממופה", + "mongo.bytes-in": "ביטים נכנסים", + "mongo.bytes-out": "ביטים יוצאים", + "mongo.num-requests": "מספר בקשות", + "mongo.raw-info": "מידע לא מעובד מ-MongoDB", + "mongo.unauthorized": "NodeBB לא הצליחה לקבל את המידע הדרוש מ-MongoDB. אנא בדוק שלמשתמש יש הרשאת clusterMonitor ל-admin database.", + + "redis": "Redis", + "redis.version": "גרסת Redis", + "redis.keys": "מפתחות", + "redis.expires": "פג תוקף", + "redis.avg-ttl": "זמן TTL ממוצע", + "redis.connected-clients": "לקוחות מחוברים", + "redis.connected-slaves": "לקוחות מחוברים", + "redis.blocked-clients": "לקוחות חסומים", + "redis.used-memory": "זכרון בשימוש", + "redis.memory-frag-ratio": "יחס פיצול זכרון", + "redis.total-connections-recieved": "סך כל החיבורים שהתקבלו", + "redis.total-commands-processed": "סך כל הפקודות שעובדו", + "redis.iops": "אפשרויות מידיות לשניה", + "redis.iinput": "קלט מיידי לשנייה", + "redis.ioutput": "פלט מיידי לשנייה", + "redis.total-input": "סך הכל מידע נכנס", + "redis.total-output": "סך הכל מידע יוצא", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "מידע לא מעובד מ-Redis", + + "postgres": "Postgres", + "postgres.version": "גרסאת PostgreSQL", + "postgres.raw-info": "מידע לא מעובד מ-Postgres" +} diff --git a/public/language/he/admin/advanced/errors.json b/public/language/he/admin/advanced/errors.json new file mode 100644 index 0000000000..ef5e1d870e --- /dev/null +++ b/public/language/he/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "דוגמא %1", + "error-events-per-day": "%1 ארועים ביום", + "error.404": "לא נמצא 404", + "error.503": "השירות אינו זמין 503", + "manage-error-log": "נהל רישום שגיאות", + "export-error-log": "יצא רישום שגיאות (CSV)", + "clear-error-log": "נקה רישום שגיאות", + "route": "נתיב", + "count": "ספירה", + "no-routes-not-found": "הידד! אין שגיאות 404!", + "clear404-confirm": "האם אתה בטוח שאתה רוצה לנקות את רישום שגיאות 404?", + "clear404-success": "שגיאות \"404 לא נמצא\" נוקו" +} \ No newline at end of file diff --git a/public/language/he/admin/advanced/events.json b/public/language/he/admin/advanced/events.json new file mode 100644 index 0000000000..37da405a88 --- /dev/null +++ b/public/language/he/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "ארועים", + "no-events": "אין ארועים", + "control-panel": "בקרת ארועים\n ", + "delete-events": "מחיקת ארועים", + "confirm-delete-all-events": "האם אתה בטוח שאתה רוצה למחוק את כל האירועים שנרשמו?", + "filters": "מסננים", + "filters-apply": "החל מסננים", + "filter-type": "סוג אירוע", + "filter-start": "מתאריך", + "filter-end": "עד תאריך", + "filter-perPage": "פריטים בכל דף" +} \ No newline at end of file diff --git a/public/language/he/admin/advanced/logs.json b/public/language/he/admin/advanced/logs.json new file mode 100644 index 0000000000..7c503395a1 --- /dev/null +++ b/public/language/he/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "רישומים", + "control-panel": "בקרת רישומים", + "reload": "טען רישומים מחדש", + "clear": "נקה רישומים", + "clear-success": "הרישומים נוקו!" +} \ No newline at end of file diff --git a/public/language/he/admin/appearance/customise.json b/public/language/he/admin/appearance/customise.json new file mode 100644 index 0000000000..bd14e03e43 --- /dev/null +++ b/public/language/he/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "CSS/LESS מותאם אישית", + "custom-css.description": "הכניסו כאן CSS / LESS משלכם, שיבוצע לאחר טעינת כל הסגנונות האחרים.", + "custom-css.enable": "הפעלת CSS/LESS מותאם אישית", + + "custom-js": "Javascript מותאם אישית", + "custom-js.description": "הכניסו כאן JavaScript משלכם, שיבוצע לאחר טעינת הדף לחלוטין.", + "custom-js.enable": "הפעלת Javascript מותאם אישית", + + "custom-header": "Header מותאם אישית", + "custom-header.description": "הכניסו כאן HTML משלכם (לדוגמא תגיות Meta), שיתווספו לתגית ה-head של הפורום. ניתן להכניס סקריפטים, אך מומלץ להכניס אותם בכרטיסיית Javascript מותאם אישית.", + "custom-header.enable": "הפעלת HTML מותאם אישית", + + "custom-css.livereload": "הפעלת טעינה מחדש אוטומטית.", + "custom-css.livereload.description": "הפעלה זו נועדה כדי לרענן את כל החיבורים מכל מכשיר, כאשר תשמרו את הדף המותאם אישית." +} \ No newline at end of file diff --git a/public/language/he/admin/appearance/skins.json b/public/language/he/admin/appearance/skins.json new file mode 100644 index 0000000000..ea33d800bc --- /dev/null +++ b/public/language/he/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "טוען עיצובים", + "homepage": "דף הפרוייקט", + "select-skin": "בחר עיצוב זה", + "current-skin": "עיצוב נוכחי", + "skin-updated": "עיצוב עודכן", + "applied-success": "עיצוב %1 הוחל בהצלחה", + "revert-success": "עיצוב הוחזר לצבעים בסיסיים" +} \ No newline at end of file diff --git a/public/language/he/admin/appearance/themes.json b/public/language/he/admin/appearance/themes.json new file mode 100644 index 0000000000..00210bdbd0 --- /dev/null +++ b/public/language/he/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "בודק ערכות נושא מותקנות...", + "homepage": "דף הבית", + "select-theme": "בחר ערכת נושא", + "current-theme": "ערכת נושא נוכחית", + "no-themes": "לא נמצאו ערכות נושא מותקנות", + "revert-confirm": "האם אתה בטוח שאתה רוצה לשחזר את ערכת הנושא הרגילה של NodeBB?", + "theme-changed": "ערכת הנושא שונתה", + "revert-success": "החזרת בהצלחה את הפורום שלך לערכת הנושא ברירת המחדל.", + "restart-to-activate": "אנא בצע בנייה והפעלה מחדש כדי להחיל את ערכת הנושא הזו." +} \ No newline at end of file diff --git a/public/language/he/admin/dashboard.json b/public/language/he/admin/dashboard.json new file mode 100644 index 0000000000..01837acb75 --- /dev/null +++ b/public/language/he/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "תעבורת הפורום", + "page-views": "צפיות בדפים", + "unique-visitors": "מבקרים ייחודיים", + "logins": "כניסות", + "new-users": "משתמשים חדשים", + "posts": "פוסטים חדשים", + "topics": "נושאים חדשים", + "page-views-seven": "ב-7 ימים אחרונים", + "page-views-thirty": "ב-30 ימים אחרונים", + "page-views-last-day": "ב-24 שעות אחרונות", + "page-views-custom": "טווח תאריכים מותאם אישית", + "page-views-custom-start": "תחילת טווח", + "page-views-custom-end": "סוף טווח", + "page-views-custom-help": "הכנס טווח תאריכים של התקופה בה תרצה לצפות בתעבורת הפורום. הפורמט הנדרש הוא YYYY-MM-DD", + "page-views-custom-error": "הזן טווח תאריכים תקין כדלהלן YYYY-MM-DD", + + "stats.yesterday": "אתמול", + "stats.today": "היום", + "stats.last-week": "בשבוע שעבר", + "stats.this-week": "בשבוע הזה", + "stats.last-month": "בחודש האחרון", + "stats.this-month": "בחודש זה", + "stats.all": "תמיד", + + "updates": "עדכונים", + "running-version": "הפורום מעודכן לגרסה %1", + "keep-updated": "לעדכוני אבטחה מעודכנים ותיקוני באגים, וודא שהפורום שלך עדכני לגרסה האחרונה.", + "up-to-date": "

אתה מעודכן

", + "upgrade-available": "

גרסה חדשה (v%1) שוחררה. שקול האם לעדכן את הפורום שלך.

", + "prerelease-upgrade-available": "

זוהי גירסת קדם-הפצה מיושנת של NodeBB. גרסה חדשה (v%1) שוחרר. שקול האם לעדכן את ה-NodeBB שלך.

", + "prerelease-warning": "

זוהי גירסת קדם-הפצה של NodeBB. באגים בלתי צפויים עלולים להתרחש.

", + "fallback-emailer-not-found": "Fallback emailer לא נמצא!", + "running-in-development": "הפורום פועל במצב פיתוח. הפורום עשוי להיות חשוף לפגיעות פוטנציאליות; פנה אל מנהל המערכת שלך.", + "latest-lookup-failed": "

נכשל בבדיקת זמינות גרסה חדשה של הפורום

", + + "notices": "התראות", + "restart-not-required": "לא נדרש אתחול מחדש", + "restart-required": "נדרש אתחול מחדש", + "search-plugin-installed": "תוסף חיפוש הותקן", + "search-plugin-not-installed": "תוסף חיפוש לא הותקן", + "search-plugin-tooltip": "התקן את תוסף החיפוש מעמוד התוספים על מנת להפעיל את אפשרות החיפוש", + + "control-panel": "שליטת מערכת", + "rebuild-and-restart": "בנייה והפעלה מחדש", + "restart": "הפעל מחדש", + "restart-warning": "הפעלה או בניה מחדש של הפורום תנתק את כל החיבורים הקיימים למספר שניות", + "restart-disabled": "הפעלה או בניה מחדש של הפורום בוטלה, נראה שאינך מפעיל את הפורום דרך שרת מתאים.", + "maintenance-mode": "מצב תחזוקה", + "maintenance-mode-title": "לחץ כאן על מנת להכניס את הפורום למצב תחזוקה", + "realtime-chart-updates": "עדכן תרשים בזמן אמת", + + "active-users": "משתמשים פעילים", + "active-users.users": "משתמשים", + "active-users.guests": "אורחים", + "active-users.total": "סך הכל", + "active-users.connections": "חיבורים", + + "guest-registered-users": "אורחים לעומת משתמשים רשומים", + "guest": "אורח", + "registered": "רשומים", + + "user-presence": "נוכחות משתמשים", + "on-categories": "רשימת הקטגוריות", + "reading-posts": "קריאת פוסטים", + "browsing-topics": "חיפוש נושאים", + "recent": "לאחרונה", + "unread": "לא נקראו", + + "high-presence-topics": "פוסטים עם הכי הרבה נוכחות", + "popular-searches": "חיפושים פופולריים", + + "graphs.page-views": "צפיות בדפים", + "graphs.page-views-registered": "צפיות בדפים-רשומים", + "graphs.page-views-guest": "צפיות בדפים-אורחים", + "graphs.page-views-bot": "צפיות בדפים-בוטים", + "graphs.unique-visitors": "מבקרים ייחודיים", + "graphs.registered-users": "משתמשים רשומים", + "graphs.guest-users": "משתמשים אורחים", + "last-restarted-by": "אותחל לארונה על ידי", + "no-users-browsing": "אין גולשים", + + "back-to-dashboard": "חזרה ללוח מחוונים", + "details.no-users": "אין משתמש שהצטרף במסגרת הזמן שנבחרה", + "details.no-topics": "לא פורסמו נושאים במסגרת הזמן שנבחרה", + "details.no-searches": "עדיין לא בוצעו חיפושים", + "details.no-logins": "לא נרשמו כניסות במסגרת הזמן שנבחרה", + "details.logins-static": "NodeBB שומר נתוני הפעלה עבור %1 ימים בלבד, ולכן טבלה זו תציג רק את הכניסות הפעילות האחרונות", + "details.logins-login-time": "זמן כניסה" +} diff --git a/public/language/he/admin/development/info.json b/public/language/he/admin/development/info.json new file mode 100644 index 0000000000..83bdfa2cc7 --- /dev/null +++ b/public/language/he/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "אתה נמצא ב %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 צמתים הגיבו בתוך %2מילי שניות!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "מקוון", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "טעינת מערכת", + "cpu-usage": "שימוש ב-CPU", + "uptime": "משך זמן פעולת המערכת ללא השבתה", + + "registered": "רשום", + "sockets": "Sockets", + "guests": "אורחים", + + "info": "מידע" +} \ No newline at end of file diff --git a/public/language/he/admin/development/logger.json b/public/language/he/admin/development/logger.json new file mode 100644 index 0000000000..e8ff9ac475 --- /dev/null +++ b/public/language/he/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "הגדרות מנהל הרישום", + "description": "על-ידי הפיכת תיבות הסימון לזמינות, תקבל יומני רישום למסוף שלך. אם תציין נתיב, יומני הרישום יישמרו בקובץ במקום זאת. רישום HTTP שימושי לאיסוף נתונים סטטיסטיים אודות מי ומתי אנשים נכנסים לפורום שלך. בנוסף לרישום בקשות ה-HTTP, אנו יכולים גם לרשום אירועי Socket.io, אשר בשילוב עם מודד redis-cli, יכול להיות מאוד מועיל ללימוד הפנימיים של NodeBB.", + "explanation": "הפעל או ​בטל את סימון הגדרות הרישום בכדי לאפשר או להשבית כניסה במהירות. אין צורך בהפעלה מחדש.", + "enable-http": "הפעל רישום HTTP", + "enable-socket": "הפעל רישום אירועים ב-socket.io", + "file-path": "נתיב קובץ יומן רישום", + "file-path-placeholder": "/path/to/log/file.log ::: השאר ריק כדי להיכנס לטרמינל שלך", + + "control-panel": "לוח בקרת מנהל רישום", + "update-settings": "עדכן הגדרות מנהל רישום" +} \ No newline at end of file diff --git a/public/language/he/admin/extend/plugins.json b/public/language/he/admin/extend/plugins.json new file mode 100644 index 0000000000..1c764d2ba7 --- /dev/null +++ b/public/language/he/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "פופולארי", + "installed": "הותקן", + "active": "פעיל", + "inactive": "לא-פעיל", + "out-of-date": "פג תוקף", + "none-found": "לא נמצאו תוספים", + "none-active": "אין תוספים פעילים", + "find-plugins": "מצא תוספים", + + "plugin-search": "חיפוש תוספים", + "plugin-search-placeholder": "חפש תוספים...", + "submit-anonymous-usage": "שלח נתוני שימוש אנונימיים בתוסף.", + "reorder-plugins": "סדר מחדש תוספים", + "order-active": "הזמן תוסף פעיל", + "dev-interested": "מתעניין בכתיבת תוספים ל-NodeBB?", + "docs-info": "ניתן למצוא תיעוד מלא בנוגע לכתיבת תוסף ב פורטל מסמכי NodeBB..", + + "order.description": "תוספים מסוימים פועלים באופן אידיאלי כאשר הם מאותחלים לפני/אחרי תוספים אחרים.", + "order.explanation": "תוספים נטענים בסדר שצוין כאן, מלמעלה למטה", + + "plugin-item.themes": "ערכות נושא", + "plugin-item.deactivate": "בטל", + "plugin-item.activate": "הפעל", + "plugin-item.install": "התקן", + "plugin-item.uninstall": "הסר התקנה", + "plugin-item.settings": "הגדרות", + "plugin-item.installed": "מותקן", + "plugin-item.latest": "אחרונים", + "plugin-item.upgrade": "שדרג", + "plugin-item.more-info": "מידע נוסף:", + "plugin-item.unknown": "לא ידוע", + "plugin-item.unknown-explanation": "לא היתה דרך לקבוע מצב תוסף זה, כנראה עקב שגיאת קביעת תצורה שגויה.", + "plugin-item.compatible": "תוסף זה פועל ב- NodeBB %1", + "plugin-item.not-compatible": "ג", + + "alert.enabled": "תוסף מופעל", + "alert.disabled": "תוסף מושבת", + "alert.upgraded": "תוסף שודרג", + "alert.installed": "תוסף הותקן", + "alert.uninstalled": "תוסף הוסר", + "alert.activate-success": "בנה והפעל מחדש את NodeBB בכדי לשדרג תוסף זה במלואו.", + "alert.deactivate-success": "התוסף הושבת בהצלחה", + "alert.upgrade-success": "בנה והפעל מחדש את NodeBB בכדי לשדרג תוסף זה במלואו.", + "alert.install-success": "תוסף הותקן בהצלחה, אנא הפעל את התוסף.", + "alert.uninstall-success": "התוסף בוטל והוסר בהצלחה.", + "alert.suggest-error": "

ל-NodeBB לא היתה אפשרות להגיע למנהל החבילות, המשך בהתקנה של הגירסה העדכנית ביותר?

השרת החזיר (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB לא הצליח להגיע למנהל החבילות, בשלב זה לא מומלץ לשדרג.

", + "alert.incompatible": "

הגרסה שלך של NodeBB (v%1) נוקה רק לשדרוג v%2 של התוסף הזה. אנא עדכן את NodeBB אם ברצונך להתקין גרסה חדשה יותר של תוסף זה.

", + "alert.possibly-incompatible": "

לא נמצא מידע על תאימות

תוסף זה לא ציין גרסה ספציפית להתקנה בגרסת NodeBB שלך. אין אפשרות להבטיח תאימות מלאה, והיא עלולה לגרום ל- NodeBB לא לפעול כראוי.

במקרה ש- NodeBB לא יכול לאתחל כראוי:

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

להמשיך בהתקנת הגרסה האחרונה של תוסף זה?

", + "alert.reorder": "תוספים שהוזמנו מחדש", + "alert.reorder-success": "אנא בנה והפעל מחדש את NodeBB בכדי להשלים את התהליך במלואו.", + + "license.title": "מידע רישיון התוסף", + "license.intro": "תוסף %1 מורשה תחת %2. אנא קרא והבן את תנאי הרשיון לפני הפעלת תוסף זה.", + "license.cta": "האם להמשיך בהפעלת התוסף הזה?" +} diff --git a/public/language/he/admin/extend/rewards.json b/public/language/he/admin/extend/rewards.json new file mode 100644 index 0000000000..12bc679568 --- /dev/null +++ b/public/language/he/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "תגמולים", + "condition-if-users": "אם", + "condition-is": "הינו:", + "condition-then": "תגמל ב:", + "max-claims": "מספר פעמים בה ניתן לדרוש תגמול", + "zero-infinite": "הזן 0 ללא הגבלה", + "delete": "מחק", + "enable": "הפעל", + "disable": "השבת", + + "alert.delete-success": "תגמול נמחק בהצלחה", + "alert.no-inputs-found": "תגמול לא חוקי - לא נמצא מידע!", + "alert.save-success": "תגמולים נשמרו בהצלחה" +} \ No newline at end of file diff --git a/public/language/he/admin/extend/widgets.json b/public/language/he/admin/extend/widgets.json new file mode 100644 index 0000000000..eb710c3543 --- /dev/null +++ b/public/language/he/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "וידג'טים זמינים", + "explanation": "בחר וידג'ט מהתפריט הנפתח ואז גרור ושחרר אותו באזור הווידג'ט של התבנית משמאל.", + "none-installed": "לא נמצאו וידג'טים! הפעל את תוספי הוידג'טים ב תוספים בלוח הבקרה.", + "clone-from": "וידג'טים משוכפלים מ", + "containers.available": "גורמים מכילים זמינים", + "containers.explanation": "גרור ושחרר מעל כל וידג'ט פעיל", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "פאנל", + "container.panel-header": "כותרת פאנל", + "container.panel-body": "גוף הפאנל", + "container.alert": "התראה", + + "alert.confirm-delete": "האם אתה בטוח שאתה רוצה למחוק את הוידג'ט?", + "alert.updated": "העלאת וידג'טים", + "alert.update-success": "הוידג'טים הועלו בהצלחה", + "alert.clone-success": "הוידג'טים שוכפלו בהצלחה", + + "error.select-clone": "בחר דף לשכפל ממנו", + + "title": "כותרת", + "title.placeholder": "כותרת (מוצגת רק בגורמים מכילים מסוימים)", + "container": "גורם מכיל", + "container.placeholder": "גרור ושחרר גורם מכיל (container) או הזן HTML כאן.", + "show-to-groups": "יוצג בקבוצות", + "hide-from-groups": "יוסתר מקבוצות", + "hide-on-mobile": "הסתר במובייל" +} \ No newline at end of file diff --git a/public/language/he/admin/manage/admins-mods.json b/public/language/he/admin/manage/admins-mods.json new file mode 100644 index 0000000000..80378690e1 --- /dev/null +++ b/public/language/he/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "מנהלים", + "global-moderators": "מודרטורים גולבליים", + "moderators": "Moderators", + "no-global-moderators": "אין מודרטורים גולבליים", + "no-sub-categories": "אין תתי קטגוריות", + "subcategories": "%1 תתי-קטגוריות", + "no-moderators": "אין מודרטורים", + "add-administrator": "הוסף מנהל", + "add-global-moderator": "הוסף מודרטור גלובלי", + "add-moderator": "הוסף מודרטור" +} \ No newline at end of file diff --git a/public/language/he/admin/manage/categories.json b/public/language/he/admin/manage/categories.json new file mode 100644 index 0000000000..054b555acb --- /dev/null +++ b/public/language/he/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "הגדרות קטגוריות", + "privileges": "הרשאות", + + "name": "שם קטגוריה", + "description": "תיאור קטגוריה", + "bg-color": "צבע רקע", + "text-color": "צבע טקסט", + "bg-image-size": "גודל תמונת רקע", + "custom-class": "Class מותאם אישית", + "num-recent-replies": "מספר תגובות אחרונות להצגה", + "ext-link": "הפנה לקישור חיצוני", + "subcategories-per-page": "קטגוריות משנה לדף", + "is-section": "הגדר קטגוריה זו כמקטע ללא אפשרות כניסה, רק לתתי קטגוריות.", + "post-queue": "תור פוסטים", + "tag-whitelist": "רשימה לבנה של תגיות", + "upload-image": "העלה תמונה", + "delete-image": "הסרה", + "category-image": "תמונת קטגוריה", + "parent-category": "קטגוריית אב", + "optional-parent-category": "קטגוריית הורים (אופציונלי)", + "top-level": "רמה עליונה", + "parent-category-none": "(ללא)", + "copy-parent": "העתק אב", + "copy-settings": "העתק הגדרות מ:", + "optional-clone-settings": "שכפול הגדרות מקטגוריה (אופציונלי)", + "clone-children": "שכפול קטגוריות והגדרות של צאצאים", + "purge": "מחיקת קטגוריה", + + "enable": "הפעלה", + "disable": "השבתה", + "edit": "עריכה", + "analytics": "ניתוח", + "view-category": "הצגת קטגוריה", + "set-order": "קביעת סדר", + "set-order-help": "הגדרת סדר הקטגוריה תעביר קטגוריה זו לסדר זה ותעדכן את סדר הקטגוריות האחרות לפי הצורך. מינימום קביעת סדר הוא 1 מה שמציב את הקטגוריה בראש.", + + "select-category": "בחרו קטגוריה", + "set-parent-category": "הגדרת קטגוריית אב", + + "privileges.description": "באפשרותך לקבוע את התצורה של הרשאות בקרת הגישה עבור חלקים מהפורום בסעיף זה. ניתן להעניק הרשאות על בסיס משתמש או על בסיס קבוצה. בחר את תחום ההשפעה מהרשימה הנפתחת שלהלן.", + "privileges.category-selector": "הגדרת הרשאות עבור", + "privileges.warning": "הערה: הגדרות ההרשאות נכנסות לתוקף באופן מיידי. אין צורך לשמור את הקטגוריה לאחר התאמת הגדרות אלה.", + "privileges.section-viewing": "הרשאות צפייה", + "privileges.section-posting": "הרשאות פוסטים", + "privileges.section-moderation": "הרשאות מודרטור", + "privileges.section-other": "אחר", + "privileges.section-user": "משתמש", + "privileges.search-user": "הוסף משתמש", + "privileges.no-users": "אין הרשאות ספציפיות למשתמש בקטגוריה זו.", + "privileges.section-group": "קבוצה", + "privileges.group-private": "קבוצה זו פרטית", + "privileges.inheritance-exception": "לקבוצה זו אין הרשאות של קבוצת משתמשים רשומים", + "privileges.banned-user-inheritance": "למשתמשים מורחקים יש הרשאות של קבוצת משתמשים מורחקים", + "privileges.search-group": "הוספת קבוצה", + "privileges.copy-to-children": "העתקה לצאצאים", + "privileges.copy-from-category": "העתקה מקטגוריה", + "privileges.copy-privileges-to-all-categories": "העתקה לכל הקטגוריות (זהירות!)", + "privileges.copy-group-privileges-to-children": "העתקת הרשאות קבוצה זו לצאצאי קטגוריה זו.", + "privileges.copy-group-privileges-to-all-categories": "העתקת הרשאות קבוצה זו לכל הקטגוריות (זהירות!).", + "privileges.copy-group-privileges-from": "העתקת הרשאות קבוצה זו מקטגוריה אחרת.", + "privileges.inherit": "אם קבוצת משתמשים רשומים מקבלים הרשאה כל-שהיא, יסומן הרשאה אוטומטית לכל הקבוצות האחרות. הרשאה אוטומטית זו תוגדר גם אם לא תסמנו אותה במפורש. מכיוון שכל המשתמשים הם חלק מקבוצת המשתמשים - משתמשים רשומים. ולכן, אין צורך להעניק במפורש הרשאות עבור קבוצות נוספות.", + "privileges.copy-success": "ההרשאות הועתקו!", + + "analytics.back": "חזרה לרשימת הקטגוריות", + "analytics.title": "ניתוח קטגוריית \"%1\"", + "analytics.pageviews-hourly": "תרשים 1 – תצוגות עמוד לפי שעה בקטגוריה זו", + "analytics.pageviews-daily": "תרשים 2 – תצוגות עמוד לפי יום בקטגוריה זו", + "analytics.topics-daily": "תרשים 3 – נושאים יומיים שנוצרו בקטגוריה זו", + "analytics.posts-daily": "תרשים 4 – פוסטים יומיים שפורסמו בקטגוריה זו", + + "alert.created": "נוצר", + "alert.create-success": "קטגוריה נוצרה בהצלחה!", + "alert.none-active": "אין לך קטגוריות פעילות.", + "alert.create": "יצירת קטגוריה", + "alert.confirm-purge": "

האם אתם בטוחים שאתם רוצים למחוק את קטגוריית \"%1\"?

אזהרה! כל הנושאים והפוסטים בקטגוריה זו ימחקו!

מחיקת קטגוריה תסיר את כל הנושאים והפוסטים ותמחק את הקטגוריה ממסד הנתונים. אם ברצונכם להסיר את הקטגוריה באופן זמני, בחרו ב\"השבתת\" הקטגוריה.

", + "alert.purge-success": "הקטגוריה נמחקה!", + "alert.copy-success": "ההגדרות הועתקו!", + "alert.set-parent-category": "הגדרת קטגוריית אב", + "alert.updated": "הקטגוריות מעודכנות", + "alert.updated-success": "מזהה ID של קטגוריה %1 עודכן בהצלחה.", + "alert.upload-image": "העלאת תמונת קטגוריה", + "alert.find-user": "מציאת משתמש", + "alert.user-search": "חפשו משתמש כאן...", + "alert.find-group": "מציאת קבוצה", + "alert.group-search": "חפשו קבוצה כאן...", + "alert.not-enough-whitelisted-tags": "מספר התגים שהכנסתם ברשימה הלבנה נמוך ממה שרשמתם בתגים המינימליים, עליכם להכניס יותר תגים ברשימה הלבנה!", + "collapse-all": "כיווץ הכל", + "expand-all": "הרחבת הכל", + "disable-on-create": "השביתו בעת היצירה", + "no-matches": "אין התאמה" +} \ No newline at end of file diff --git a/public/language/he/admin/manage/digest.json b/public/language/he/admin/manage/digest.json new file mode 100644 index 0000000000..78e865d394 --- /dev/null +++ b/public/language/he/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "רשימה של נתונים סטטיסטיים וזמנים של מסירת תקציר מוצג להלן.", + "disclaimer": "לידיעתך, לא ניתן להבטיח משלוח דוא\"ל, בשל אופייה של טכנולוגיית הדוא\"ל. משתנים רבים גורמים לשאלה האם דוא\"ל שנשלח לשרת הנמען מועבר בסופו של דבר לתיבת הדואר הנכנס של המשתמש, כולל מוניטין של שרת, כתובות IP ברשימה השחורה והאם מוגדר DKIM / SPF / DMARC.", + "disclaimer-continued": "שליחה מוצלחת אומר שההודעה נשלחה בהצלחה מNodeBB והוכר על ידי שרת הלקוח. זה לא אומר שהאימייל הגיע לתיבה שלו. לקבלת התוצאות הטובות ביותר אנו ממליצים להשתמש בשירות שליחת אימייל צד-שלישי כמו SendGrid.", + + "user": "משתמש", + "subscription": "סוג מנוי", + "last-delivery": "שליחה מוצלחת אחרונה", + "default": "ברירת מחדל של המערכת", + "default-help": "ברירת מחדל של המערכת אומר שהמשתמש לא עקף במפורש את הגדרות הפורום הכלליים לתקצירים , שהם כעת: "%1"", + "resend": "שלח תקציר מחדש", + "resend-all-confirm": "האם אתה בטוח שברצונך לבצע באופן ידני הפעלת תקציר זו?", + "resent-single": "שליחת התקציר מחדש באופן ידני בוצע בהצלחה", + "resent-day": "תקציר יומי נשלח", + "resent-week": "תקציר שבועי נשלח", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "תקציר חודשי נשלח", + "null": "אף פעם", + "manual-run": "הפעל תקציר ידני", + + "no-delivery-data": "לא נמצאו נתוני שליחה" +} diff --git a/public/language/he/admin/manage/groups.json b/public/language/he/admin/manage/groups.json new file mode 100644 index 0000000000..5b66702d33 --- /dev/null +++ b/public/language/he/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "שם קבוצה", + "badge": "תגית", + "properties": "נתוני קבוצה", + "description": "תיאור קבוצה", + "member-count": "מספר חברים", + "system": "מערכת", + "hidden": "מוסתר", + "private": "פרטי", + "edit": "ערוך", + "delete": "מחק", + "privileges": "הרשאות", + "download-csv": "הורד CSV", + "search-placeholder": "חפש", + "create": "צור קבוצה", + "description-placeholder": "תאור קצר על הקבוצה שלך", + "create-button": "צור", + + "alerts.create-failure": "Uh-Oh

יצירת הקבוצה נכשלה. נסה שוב מאוחר יותר!

", + "alerts.confirm-delete": "האם אתה בטוח שאתה רוצה למחוק את הקבוצה?", + + "edit.name": "שם", + "edit.description": "תיאור", + "edit.user-title": "כותרת חברי הקבוצה", + "edit.icon": "סמליל קבוצה", + "edit.label-color": "צבע תווית קבוצה", + "edit.text-color": "צבע טקסט קבוצה", + "edit.show-badge": "הצג תג", + "edit.private-details": "אם אפשרות זו מופעלת, הצטרפות לקבוצות ידרוש אישור מבעל הקבוצה.", + "edit.private-override": "אזהרה: קבוצות פרטיות מושבתות ברמת המערכת, דבר העוקף אפשרות זו.", + "edit.disable-join": "השבת בקשות הצטרפות", + "edit.disable-leave": "משתמשים לא יוכלו לעזוב את הקבוצה", + "edit.hidden": "מוסתר", + "edit.hidden-details": "אם אפשרות זו מופעלת, קבוצה זו לא תימצא ברשימת הקבוצות, יהיה ניתן להזמין משתמשים רק באופן ידני", + "edit.add-user": "הוסף משתמש לקבוצה", + "edit.add-user-search": "חפש משתמשים", + "edit.members": "רשימת חברי הקבוצה", + "control-panel": "ממשק ניהול קבוצות", + "revert": "בטל שינויים", + + "edit.no-users-found": "לא נמצאו משתמשים", + "edit.confirm-remove-user": "האם אתה בטוח שאתה רוצה להסיר משתמש זה?", + "edit.save-success": "השינויים נשמרו!" +} \ No newline at end of file diff --git a/public/language/he/admin/manage/privileges.json b/public/language/he/admin/manage/privileges.json new file mode 100644 index 0000000000..47840a1c11 --- /dev/null +++ b/public/language/he/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "כללי", + "admin": "מנהל", + "group-privileges": "הרשאות קבוצות", + "user-privileges": "הרשאות משתמש", + "edit-privileges": "עריכת הרשאות", + "select-clear-all": "בחר/נקה הכל", + "chat": "צ'אט", + "upload-images": "העלאת תמונות", + "upload-files": "העלאת קבצים", + "signature": "חתימה", + "ban": "הרחקה", + "mute": "השתקה", + "invite": "הזמנה", + "search-content": "חיפוש תוכן", + "search-users": "חיפוש משתמשים", + "search-tags": "חיפוש תגיות", + "view-users": "הצגת משתמשים", + "view-tags": "צפייה בתגיות", + "view-groups": "צפייה בקבוצות", + "allow-local-login": "התחברות מקומית", + "allow-group-creation": "יצירת קבוצות", + "view-users-info": "צפייה במידע משתמש", + "find-category": "הצגת קטגוריה", + "access-category": "גישה לקטגוריה", + "access-topics": "גישה לנושאים", + "create-topics": "יצירת נושאים", + "reply-to-topics": "תגובה לנושאים", + "schedule-topics": "תזמון נושאים", + "tag-topics": "תיוג נושאים", + "edit-posts": "עריכת פוסטים", + "view-edit-history": "הצגת היסטוריית עריכות", + "delete-posts": "מחיקת פוסטים", + "view_deleted": "הצגת פוסטים מחוקים", + "upvote-posts": "הצבעה לפוסטים", + "downvote-posts": "הצבעה נגד פוסטים", + "delete-topics": "מחיקת נושא", + "purge": "מחיקה לצמיתות", + "moderate": "הרשאות מודרטור", + "admin-dashboard": "לוח מחוונים", + "admin-categories": "קטגוריות", + "admin-privileges": "הרשאות", + "admin-users": "משתמשים", + "admin-admins-mods": "הרשאות & ניהול", + "admin-groups": "קבוצות", + "admin-tags": "תגיות", + "admin-settings": "הגדרות", + + "alert.confirm-moderate": "האם אתה בטוח שברצונך להעניק הרשאות מודרטור לקבוצת משתמשים זו? הקבוצה היא ציבורית, וכל משתמש יכול להצטרף כרצונו.", + "alert.confirm-admins-mods": "האם אתה בטוח שברצונך להעניק "הרשאות & ניהול" למשתמש/קבוצה זו? משתמשים עם הרשאה זו יכולים להוסיף ולהסיר הרשאות של של משתמשים אחרים, כולל מנהל ראשי", + "alert.confirm-save": "נא אשר את הגדרת ההרשאות", + "alert.saved": "שינויי הרשאות נשמרו והוחלו", + "alert.confirm-discard": "האם אתה בטוח שברצונך לבטל את שינויי ההרשאות שלך?", + "alert.discarded": "שינויי ההרשאות נמחקו", + "alert.confirm-copyToAll": "זהירות!! האם אתה בטוח שברצונך להחיל הגדרת הרשאות זו של %1 ל כל הקטגוריות?", + "alert.confirm-copyToAllGroup": "זהירות!! האם אתה בטוח שברצונך להחיל הרשאות קבוצה זו של%1 ל כל הקטגוריות?", + "alert.confirm-copyToChildren": "האם אתה בטוח שברצונך להחיל הגדרת הרשאות זו של %1 ל כל קטגוריות הצאצאים (ילדים)?", + "alert.confirm-copyToChildrenGroup": "האם אתה בטוח שברצונך להחיל הרשאות קבוצה זו של%1 ל לכל קטגוריות הצאצאים (ילדים)?", + "alert.no-undo": "לא ניתן לבטל פעולה זו.", + "alert.admin-warning": "מנהלים מקבלים את כל ההרשאות", + "alert.copyPrivilegesFrom-title": "בחר קטגוריה להעתקה ממנו", + "alert.copyPrivilegesFrom-warning": "פעולה זו תעתיק %1 מהקטגוריה שנבחרה.", + "alert.copyPrivilegesFromGroup-warning": "פעולה זו תעתיק את הגדרת הקבוצה של %1 מהקטגוריה שנבחרה." +} \ No newline at end of file diff --git a/public/language/he/admin/manage/registration.json b/public/language/he/admin/manage/registration.json new file mode 100644 index 0000000000..a9b24a6bce --- /dev/null +++ b/public/language/he/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "תור", + "description": "אין משתמשים בתור ההרשמה.
כדי לאפשר את תור ההרשמה, גשו להגדרות → משתמש → רישום משתמש והגדר את סוג רישום ל\"אישור מנהל\".", + + "list.name": "שם", + "list.email": "אימייל", + "list.ip": "IP", + "list.time": "זמן", + "list.username-spam": "תדירות: %1 מופיע: %2 אמון: %3", + "list.email-spam": "תדירות: %1 מופיע: %2", + "list.ip-spam": "תדירות: %1 מופיע: %2", + + "invitations": "הזמנות", + "invitations.description": "להלן רשימה של הזמנות שנשלחו. השתמש ב- Ctrl+F כדי לחפש בתוך הרשימה על פי אימייל או שם משתמש.

שם המשתמש יוצג בצד ימין של האימייל למשתמשים שממשו את הזמנתם.", + "invitations.inviter-username": "משתמש מזמין", + "invitations.invitee-email": "מייל מוזמן", + "invitations.invitee-username": "משתמש מוזמן (אם נרשם)", + + "invitations.confirm-delete": "האם אתה בטוח שאתה רוצה למחוק את ההזמנה?" +} \ No newline at end of file diff --git a/public/language/he/admin/manage/tags.json b/public/language/he/admin/manage/tags.json new file mode 100644 index 0000000000..adeb17dfd5 --- /dev/null +++ b/public/language/he/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "בפורום שלך אין עדיין נושאים עם תגים.", + "bg-color": "צבע רקע", + "text-color": "צבע טקסט", + "description": "בחר תגיות על ידי לחיצה או גרירה, השתמש ב- CTRL כדי לבחור תגיות מרובות.", + "create": "צור תג", + "modify": "שנה תג", + "rename": "שנה שם של תג", + "delete": "מחק תגים שנבחרו", + "search": "חפש תג...", + "settings": "הגדרות תגית", + "name": "שם תג", + + "alerts.editing": "ערוך תגי(ו)ת", + "alerts.confirm-delete": "האם תרצה למחוק את התגים שנבחרו?", + "alerts.update-success": "תג עודכן!", + "reset-colors": "אפס צבעים" +} \ No newline at end of file diff --git a/public/language/he/admin/manage/uploads.json b/public/language/he/admin/manage/uploads.json new file mode 100644 index 0000000000..0507edec6f --- /dev/null +++ b/public/language/he/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "העלה קובץ", + "filename": "שם קובץ", + "usage": "שימוש בפוסט", + "orphaned": "מיותם", + "size/filecount": "גודל / ספירת קבצים", + "confirm-delete": "האם אתה בטוח שאתה רוצה למחוק קובץ זה?", + "filecount": "%1 קבצים", + "new-folder": "תיקייה חדשה", + "name-new-folder": "הכנס שם לתיקייה החדשה" +} \ No newline at end of file diff --git a/public/language/he/admin/manage/users.json b/public/language/he/admin/manage/users.json new file mode 100644 index 0000000000..f84ad38851 --- /dev/null +++ b/public/language/he/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "משתמשים", + "edit": "פעולות", + "make-admin": "הפוך למנהל", + "remove-admin": "הסר הרשאת מנהל", + "validate-email": "סמן את דוא\"ל המשתמש/ים כמאומת", + "send-validation-email": "שלח בקשת אימות דוא\"ל למשתמש/ים", + "password-reset-email": "שלח דוא\"ל לאיפוס סיסמה", + "force-password-reset": "כפה איפוס סיסמה ונתק את המשתמש", + "ban": "הרחק משתמש(ים)", + "temp-ban": "הרחק משתמש(ים) באופן זמני", + "unban": "בטל הרחקת משתמש(ים)", + "reset-lockout": "שחרר נעילת חשבון", + "reset-flags": "אפס דגלים", + "delete": "מחק משתמש(ים)", + "delete-content": "מחק תוכן משתמש(ים)", + "purge": "מחק משתמש(ים) ותוכן", + "download-csv": "ייצא משתמשים כ-CSV", + "manage-groups": "נהל קבוצות", + "add-group": "הוסף קבוצה", + "create": "צור משתמש", + "invite": "הזמנה באמצעות דוא\"ל", + "new": "משתמש חדש", + "filter-by": "סנן לפי", + "pills.unvalidated": "לא מאומת", + "pills.validated": "מאומת", + "pills.banned": "מורחק", + + "50-per-page": "50 לעמוד", + "100-per-page": "100 לעמוד", + "250-per-page": "250 לעמוד", + "500-per-page": "500 לעמוד", + + "search.uid": "לפי זהות משתמש (ID)", + "search.uid-placeholder": "הזן מזהה משתמש (ID) לחיפוש", + "search.username": "לפי שם משתמש", + "search.username-placeholder": "הזן שם משתמש לחיפוש", + "search.email": "לפי דוא\"ל", + "search.email-placeholder": "הזן דוא\"ל לחיפוש", + "search.ip": "לפי כתובת IP", + "search.ip-placeholder": "הזן כתובת IP לחיפוש", + "search.not-found": "לא נמצא משתמש!", + + "inactive.3-months": "3 חודשים", + "inactive.6-months": "6 חודשים", + "inactive.12-months": "12 חודשים", + + "users.uid": "מזהה משתמש (ID)", + "users.username": "שם משתמש", + "users.email": "דוא\"ל", + "users.no-email": "(אין כתובת דוא\"ל)", + "users.ip": "IP", + "users.postcount": "מספר פוסטים", + "users.reputation": "מוניטין", + "users.flags": "דגלים", + "users.joined": "הצטרף ב:", + "users.last-online": "נראה לאחרונה", + "users.banned": "מורחק", + + "create.username": "שם משתמש", + "create.email": "דוא\"ל", + "create.email-placeholder": "דוא\"ל של משתמש זה", + "create.password": "סיסמה", + "create.password-confirm": "אשר סיסמה", + + "temp-ban.length": "זמן הרחקה", + "temp-ban.reason": "סיבה (אופציונאלי)", + "temp-ban.hours": "שעות", + "temp-ban.days": "ימים", + "temp-ban.explanation": "הזן זמן הרחקה. שים לב הזנת מספר 0 מהווה הרחקה לצמיתות.", + + "alerts.confirm-ban": "האם אתה רוצה להרחיק משתמש זה לצמיתות?", + "alerts.confirm-ban-multi": "האם אתה רוצה להרחיק את המשתמשים לצמיתות?", + "alerts.ban-success": "משתמש(ים) הורחק/ו!", + "alerts.button-ban-x": "הרחק %1 משתמש(ים)", + "alerts.unban-success": "משתמש(ים) הוחזר/ו!", + "alerts.lockout-reset-success": "נעילת חשבון שוחרר!", + "alerts.flag-reset-success": "דגלים אופסו!", + "alerts.no-remove-yourself-admin": "אינך יכול להסיר את עצמך כמנהל!", + "alerts.make-admin-success": "המשתמש הינו מנהל עכשיו.", + "alerts.confirm-remove-admin": "האם אתה בטוח שאתה רוצה להסיר מנהל זה?", + "alerts.remove-admin-success": "בוטל הרשאת מנהל למשתמש.", + "alerts.make-global-mod-success": "המשתמש הינו מודרטור גלובלי עכשיו.", + "alerts.confirm-remove-global-mod": "האם אתה בטוח שאתה רוצה להסיר מודרטור גלובלי זה?", + "alerts.remove-global-mod-success": "המשתמש אינו מודרטור גלובלי עוד.", + "alerts.make-moderator-success": "המשתמש מודרטור כעת.", + "alerts.confirm-remove-moderator": "האם אתה בטוח שאתה רוצה להסיר מודרטור זה?", + "alerts.remove-moderator-success": "המשתמש אינו מודרטור עוד.", + "alerts.confirm-validate-email": "האם אתה רוצה לאמת את הדוא\"ל למשתמש(ים)?", + "alerts.confirm-force-password-reset": "האם אתה בטוח שאתה רוצה לכפות את איפוס הסיסמה ולנתק משתמש(ים) אלו?", + "alerts.validate-email-success": "כתובות אימייל אומתו", + "alerts.validate-force-password-reset-success": "סיסמאות משתמשים אופסו והחיבורים שלהם נותקו.", + "alerts.password-reset-confirm": "האם אתה רוצה לשלוח אימייל לאיפוס סיסמה למשתמש(ים) אלו?", + "alerts.password-reset-email-sent": "דוא\"ל איפוס סיסמה נשלח", + "alerts.confirm-delete": "אזהרה!

האם אתה בטוח שברצונך למחוק משתמש(ים)?

פעולה זו אינה הפיכה! רק חשבון המשתמש יימחק, הפוסטים והנושאים שלהם יישארו.

", + "alerts.delete-success": "משתמש(ים) נמחק!", + "alerts.confirm-delete-content": "אזהרה!

האם אתה בטוח שברצונך למחוק את תוכןמשתמש(ים) אלו?

פעולה זו אינה הפיכה! חשבונות המשתמשים יישארו, אך הפוסטים והנושאים שלהם יימחקו.

", + "alerts.delete-content-success": "תוכן המשתמש(ים) נמחק!", + "alerts.confirm-purge": "אזהרה!

האם אתה בטוח שברצונך למחוק את המשתמש(ים) ואת התוכן שלהם?

פעולה זו אינה הפיכה! כל נתוני המשתמש והתוכן יימחקו!

", + "alerts.create": "צור משתמש", + "alerts.button-create": "צור", + "alerts.button-cancel": "בטל", + "alerts.error-passwords-different": "הסיסמאות אינן תואמות!", + "alerts.error-x": "שגיאה

%1

", + "alerts.create-success": "משתמש נוצר!", + + "alerts.prompt-email": "מיילים: ", + "alerts.email-sent-to": "מייל הזמנה נשלח ל-%1", + "alerts.x-users-found": "%1 משתמש(ים) נמצאו, (%2 שניות)", + "export-users-started": "מייצא משתמשים כ-csv, הדבר עשוי להימשך זמן מה. תקבל הודעה עם השלמתה.", + "export-users-completed": "משתמשים יוצאו כ-csv, לחץ כאן להורדה." +} \ No newline at end of file diff --git a/public/language/he/admin/menu.json b/public/language/he/admin/menu.json new file mode 100644 index 0000000000..30888f99b2 --- /dev/null +++ b/public/language/he/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "לוח מחוונים", + "dashboard/overview": "סקירה כללית", + "dashboard/logins": "כניסות", + "dashboard/users": "משתמשים", + "dashboard/topics": "נושאים", + "dashboard/searches": "חיפושים", + "section-general": "כללי", + + "section-manage": "ניהול", + "manage/categories": "קטגוריות", + "manage/privileges": "הרשאות", + "manage/tags": "תגיות", + "manage/users": "משתמשים", + "manage/admins-mods": "מנהלים ומנהלים כלליים", + "manage/registration": "תור הרשמה", + "manage/post-queue": "תור פוסטים", + "manage/groups": "קבוצות", + "manage/ip-blacklist": "רשימה שחורה של כתובות IP", + "manage/uploads": "העלאות", + "manage/digest": "תקצירים", + + "section-settings": "הגדרות", + "settings/general": "כללי", + "settings/homepage": "דף הבית", + "settings/navigation": "ניווט", + "settings/reputation": "דיווחים ומוניטין", + "settings/email": "דוא\"ל", + "settings/user": "משתמשים", + "settings/group": "קבוצות", + "settings/guest": "אורחים", + "settings/uploads": "העלאות", + "settings/languages": "שפות", + "settings/post": "פוסטים", + "settings/chat": "צ'אט", + "settings/pagination": "עמודים", + "settings/tags": "תגיות", + "settings/notifications": "התראות", + "settings/api": "גישת API", + "settings/sounds": "שמע", + "settings/social": "חברתי", + "settings/cookies": "עוגיות", + "settings/web-crawler": "סורק רשת", + "settings/sockets": "Sockets", + "settings/advanced": "מתקדם", + + "settings.page-title": "%1 הגדרות", + + "section-appearance": "מראה חיצוני", + "appearance/themes": "ערכות נושא", + "appearance/skins": "עיצובים", + "appearance/customise": "תוכן מותאם אישית (HTML/JS/CSS)", + + "section-extend": "הרחבות", + "extend/plugins": "תוספים", + "extend/widgets": "וידג'טים", + "extend/rewards": "תגמולים", + + "section-social-auth": "אימות חיצוני", + + "section-plugins": "תוספים", + "extend/plugins.install": "תוספים מותקנים", + + "section-advanced": "מתקדם", + "advanced/database": "מסד נתונים", + "advanced/events": "ארועים", + "advanced/hooks": "Hooks", + "advanced/logs": "רישומים", + "advanced/errors": "שגיאות", + "advanced/cache": "עוגיות", + "development/logger": "מנהל הרישומים", + "development/info": "מידע", + + "rebuild-and-restart-forum": "בנה והפעל מחדש את הפורום", + "restart-forum": "הפעל מחדש את הפורום", + "logout": "התנתק", + "view-forum": "כניסה לפורום", + + "search.placeholder": "Search settings", + "search.no-results": "אין תוצאות...", + "search.search-forum": "חפש בפורום ", + "search.keep-typing": "המשך להקליד על מנת למצוא תוצאות...", + "search.start-typing": "התחל להקליד על מנת לראות תוצאות...", + + "connection-lost": "החיבור ל-%1 אבד, מנסה להתחבר מחדש...", + + "alerts.version": "מעודכן ל-NodeBB v%1", + "alerts.upgrade": "שדרג ל v%1" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/advanced.json b/public/language/he/admin/settings/advanced.json new file mode 100644 index 0000000000..7be6b15566 --- /dev/null +++ b/public/language/he/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "מצב תחזוקה", + "maintenance-mode.help": "כאשר הפורום נמצא במצב תחזוקה, כל הבקשות יופנו לדף אחזקה סטטי. מנהלים לא יגיעו להפניה זו, והם יוכלו לגשת לאתר כרגיל.", + "maintenance-mode.status": "קוד מצב תחזוקה", + "maintenance-mode.message": "הודעת תחזוקה", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "כותרות", + "headers.allow-from": "הגדר ALLOW-FROM למקם NodeBB ב- iFrame", + "headers.csp-frame-ancestors": "הגדר את מדיניות האבטחה (Content-Security-Policy) עבור ההטמעה (frame-ancestors) של NodeBB בתוך Iframe", + "headers.csp-frame-ancestors-help": "בחר מילים שמורות כמו 'none' (ללא) 'self' (רק מהאתר שלי) או כתובת מלאה של אתר חיצוני", + "headers.powered-by": "התאם אישית את הכותרת \"מופעל ע\"י\" הברירת מחדל של נודביבי", + "headers.acao": "אתרים הרשאים לקרוא לאתר זה (Access-Control-Allow-Origin)", + "headers.acao-regex": "תבנית טקסט (Regex) עבור אתרים הרשאים לקרוא לאתר זה (Access-Control-Allow-Origin)", + "headers.acao-help": "כדי למנוע גישה לכל האתרים, השאר ריק", + "headers.acao-regex-help": "הכנס תבנית טקסט (Regex) כאן כדי לאפשר קריאה דינאמית מאתרים חיצוניים. אם ברצונך לחסום כל גישה חיצונית, השאר ריק.", + "headers.acac": "אתרים אשר אל בקשות אליהם, יתווספו גם נתוני כניסה כגוןCookie וכו'. ( Access-Control-Allow-Credentials)", + "headers.acam": "שיטות אפשריות בבקרת גישה", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "הפעל HSTS (מומלץ)", + "hsts.maxAge": "גיל כותרת HSTS", + "hsts.subdomains": "כלול תת-דומיינים בכותרת HSTS", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "ניהול תעבורה", + "traffic.help": "NodeBB משתמש במודול שדוחה אוטומטית בקשות במצבים עם תעבורה גבוהה. אתה יכול לכוונן את ההגדרות האלה כאן, למרות שברירות המחדל הן נקודת התחלה טובה.", + "traffic.enable": "הפעל ניהול תעבורה", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "הורדת ערך זה מקטינה את זמני ההמתנה לטעינת הדפים, אך גם תציג את ההודעה \"עומס מופרז\" ליותר משתמשים. (אתחול נדרש)", + "traffic.lag-check-interval": "מרווח זמן בין בדיקות (במילישניות)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "הגדרות חיבור WebSocket", + "sockets.max-attempts": "מקסימום מספר נסיונות חיבור מחדש", + "sockets.default-placeholder": "ברירת מחדל: %1", + "sockets.delay": "זמן השעייה בן נסיונות חיבור מחדש", + + "analytics.settings": "הגדרות אנליטיקס", + "analytics.max-cache": "גודל מקסימלי של מטמון Analytics", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "הגדרות דחיסה", + "compression.enable": "אפשר דחיסה", + "compression.help": "הגדרה זו מפעילה דחיסת gzip. עבור אתר אינטרנט מרובה תנועה בייצור, הדרך הטובה ביותר להפעיל דחיסה היא ליישם אותו ברמת פרוקסי הפוך. אתה יכול להפעיל אותו כאן למטרות בדיקה." +} \ No newline at end of file diff --git a/public/language/he/admin/settings/api.json b/public/language/he/admin/settings/api.json new file mode 100644 index 0000000000..3299de86fc --- /dev/null +++ b/public/language/he/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "טוקנים - Tokens", + "settings": "הגדרות", + "lead-text": "מעמוד זה תוכלו להגדיר גישת כתיבה ל-API ב- NodeBB.", + "intro": "כברירת מחדל, ה- API של כתיבה מאמת משתמשים בהתבסס על קובץ ה-cookie של ההפעלה שלהם, אך NodeBB תומך גם באימות נושא באמצעות טוקנים (אישורי אבטחה) שנוצרו באמצעות דף זה.", + "docs": "לחץ כאן כדי לגשת למפרט ה- API המלא", + + "require-https": "אפשר שימוש בAPI באמצעות HTTPS בלבד", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "ID משתמש", + "uid-help-text": "ציין מזהה משתמש בכדי לשייך לטוקן זה. אם מזהה המשתמש הוא 0, זה ייחשב כטוקןראשי, שיכול לשער את זהותם של משתמשים אחרים על בסיס פרמטר_uid .", + "description": "תיאור", + "no-description": "לא צוין תיאור.", + "token-on-save": "טוקן יוצר לאחר שמירת הטופס" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/chat.json b/public/language/he/admin/settings/chat.json new file mode 100644 index 0000000000..23fbc77000 --- /dev/null +++ b/public/language/he/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "הגדרות צ'אט", + "disable": "השבת צ'אט", + "disable-editing": "השבת עריכה/מחיקה של הודעות צ'אט", + "disable-editing-help": "מנהלים ומודרטורים גולבליים אינם נכללים בהגבלה זו", + "max-length": "אורך מקסימלי של הודעת צ'אט", + "max-room-size": "מספר המשתמשים המרבי בחדרי צ'אט", + "delay": "זמן המתנה בין הודעות צ'אט - באלפיות שניה", + "notification-delay": "עיכוב התראות להודעות צ'אט. (0 ללא עיכוב)", + "restrictions.seconds-edit-after": "מספר השניות בה ניתן לערוך הודעת צ'אט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", + "restrictions.seconds-delete-after": "מספר השניות בה ניתן למחוק הודעת צ'אט מרגע פרסומו (כתבו 0 להפוך ללא זמין)" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/cookies.json b/public/language/he/admin/settings/cookies.json new file mode 100644 index 0000000000..47181126f6 --- /dev/null +++ b/public/language/he/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "מופעל", + "consent.message": "התראות", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "הגדרות", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "מספר מרבי של סשנים פעילים לכל משתמש", + "blank-default": "השאר ריק לברירת המחדל" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/email.json b/public/language/he/admin/settings/email.json new file mode 100644 index 0000000000..1b37b35a28 --- /dev/null +++ b/public/language/he/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "הגדרות דוא\"ל", + "address": "כתובת דוא\"ל", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "מאת", + "from-help": "השם 'מאת' יוצג בדוא\"ל.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "ללא", + "smtp-transport.username": "שם משתמש", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "סיסמה", + "smtp-transport.pool": "אפשר חיבורים מאוגדים", + "smtp-transport.pool-help": "איחוד חיבורים מונע מ- NodeBB ליצור חיבור חדש לכל דואר אלקטרוני. אפשרות זו חלה רק אם SMTP תחבורה מופעלת.", + + "template": "ערוך תבנית דוא\"ל", + "template.select": "בחר תבנית דוא\"ל", + "template.revert": "Revert to Original", + "testing": "מייל בדיקה", + "testing.select": "בחר תבנית דוא\"ל", + "testing.send": "שלח מייל בדיקה", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "תקצירי דואר אלקטרוני", + "subscriptions.disable": "הפיכת תקצירי דואר אלקטרוני ללא זמינים", + "subscriptions.hour": "שעת תקציר", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "הסר תמונות מהודעות דוא\"ל", + "require-email-address": "דרוש ממשתמשים חדשים כתובת אימייל", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "שלח דוא\"ל גם למשתמשים שלא אימתו את הכתובת שלהם", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "בקש מהמשתמשים להגדיר/לאמת את כתובת הדוא\"ל שלהם", + "prompt-help": "הצג אזהרה למשתמשים שהדוא\"ל שלהם לא מוגדר/לא מאומת", + "sendEmailToBanned": "שלח אימיילים גם למשתמשים מורחקים" +} diff --git a/public/language/he/admin/settings/general.json b/public/language/he/admin/settings/general.json new file mode 100644 index 0000000000..97c974a552 --- /dev/null +++ b/public/language/he/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "הגדרות האתר", + "title": "כותרת האתר", + "title.short": "כותרת קצרה", + "title.short-placeholder": "אם לא הוגדר כותרת קצרה, כותרת האתר ישמש ככותרת", + "title.url": "כותרת קישור URL", + "title.url-placeholder": "ה-URL של כותרת האתר", + "title.url-help": "בעת לחיצה על הכותרת, המשתמשים ינותבו לכתובת זו. באם יישאר ריק, המשתמשים יישלחו לאינדקס הפורום.
הערה: זו אינה כתובת ה- URL החיצונית המשמשת בהודעות דוא\"ל וכד'. זה נקבע על ידי ה-url המאופיין ב- config.json", + "title.name": "שם הקהילה שלך", + "title.show-in-header": "הצג את כותרת האתר בכותרת העליונה", + "browser-title": "כותרת הדפדפן", + "browser-title-help": "אם לא צוין כותרת הדפדפן, כותרת האתר ישמש ככותרת", + "title-layout": "פריסת כותרת", + "title-layout-help": "הגדר כיצד כותרת הדפדפן תהיה מובנית לדוגמא. {pageTitle} | {browserTitle}", + "description.placeholder": "תיאור קצר על הקהילה שלך", + "description": "תיאור האתר", + "keywords": "מילות מפתח של האתר", + "keywords-placeholder": "מילות מפתח המתארות את הקהילה שלך, מופרדות באמצעות פסיקים", + "logo": "לוגו האתר", + "logo.image": "תמונה", + "logo.image-placeholder": "נתב ללוגו שיראה בכותרת הפורום", + "logo.upload": "העלה", + "logo.url": "קישור URL לאייקון", + "logo.url-placeholder": "כתובת לוגו האתר", + "logo.url-help": "בעת לחיצה על האייקון, המשתמשים ינותבו לכתובת זו. באם יישאר ריק, המשתמשים יישלחו לאינדקס הפורום.
הערה: זו אינה כתובת ה- URL החיצונית המשמשת בהודעות דוא\"ל וכד'. זה נקבע על ידי ה-url המאופיין ב- config.json", + "logo.alt-text": "טקסט חלופי", + "log.alt-text-placeholder": "הזן טקסט חלופי לנגישות", + "favicon": "פבאייקון - Favicon", + "favicon.upload": "העלה", + "pwa": "אפליקציית אינטרנט בסלולרי", + "touch-icon": "סמליל דף אינטרנט - Touch Icon", + "touch-icon.upload": "העלה", + "touch-icon.help": "סמליל דף אינטרנט מופיע כאשר מישהו מסמן את דף האינטרנט שלך או מוסיף את דף האינטרנט שלך למסך הבית שלו, גודל ותבנית מומלצים: 512x512, תבנית PNG בלבד. אם לא הוגדר סמליל דף אינטרנט, NodeBB יחזור להשתמש בסמליל הפבאייקון.", + "maskable-icon": "סמליל הניתן להסוואה (במסך הבית)", + "maskable-icon.help": "סמליל הניתן להסוואה מופיע בדף הבית של הסוללרי, זהו תמונה אטומה עם מעט ריפוד שהיישום דף הבית שלך יוכל לחתוך אחר כך לצורה ולגודל הרצוי. עדיף לא להסתמך על צורה מסוימת, מכיוון שהצורה שנבחרה בסופו של דבר יכולה להשתנות לפי סוגי מסך בית ופלטפורמה. גודל ותבנית מומלצים: 512x512, תבנית PNG בלבד. אם לא הוגדר אייקון הניתן להסוואה, NodeBB יחזור להשתמש בסמליל דף האינטרנט.", + "outgoing-links": "קישורים חיצוניים", + "outgoing-links.warning-page": "השתמש בדף האזהרה לקישורים יוצאים", + "search": "חיפוש", + "search-default-in": "חפש ב", + "search-default-in-quick": "חיפוש מהיר ב", + "search-default-sort-by": "מיין לפי", + "outgoing-links.whitelist": "תחומים לרשימה הלבנה לעקיפת דף האזהרה", + "site-colors": "מטה-נתונים של צבע אתר", + "theme-color": "צבע ערכת נושא", + "background-color": "צבע רקע", + "background-color-help": "צבע המשמש לרקע של מסך פתיחה כאשר אתר האינטרנט מותקן כ-PWA", + "undo-timeout": "פסק זמן לביטול", + "undo-timeout-help": "לפעולות מסוימות, כמו העברת נושאים, יאופשרו ביטול הפעולה במסגרת זמן מסוימת. הגדר ל- 0 כדי להשבית לחלוטין את האפשרות.", + "topic-tools": "כלי נושא" +} diff --git a/public/language/he/admin/settings/group.json b/public/language/he/admin/settings/group.json new file mode 100644 index 0000000000..3c1ca092de --- /dev/null +++ b/public/language/he/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "כללי", + "private-groups": "קבוצות פרטיות", + "private-groups.help": "אם אפשרות זו מופעל, צירוף לקבוצות מחייב את אישור הבעלים של הקבוצה (מופעל כברירת מחדל)", + "private-groups.warning": "אזהרה! אם אפשרות זו מושבתת ויש לך קבוצות פרטיות, הן יהפכו באופן אוטומטי לציבוריות.", + "allow-multiple-badges": "אפשר תגים מרובים", + "allow-multiple-badges-help": "ניתן להשתמש בדגל זה כדי לאפשר למשתמשים לבחור תגי קבוצה מרובים, דורש תמיכה בערכת נושא.", + "max-name-length": "אורך שם קבוצה מרבי", + "max-title-length": "אורך כותרת קבוצה מרבי", + "cover-image": "תמונת נושא של קבוצה", + "default-cover": "תמונות נושא ברירת מחדל", + "default-cover-help": "הוסף תמונות נושא ברירת מחדל מופרדות בפסיקים עבור קבוצות שאין להן תמונת נושא שהועלתה" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/guest.json b/public/language/he/admin/settings/guest.json new file mode 100644 index 0000000000..ec2440a1bf --- /dev/null +++ b/public/language/he/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "הגדרות", + "handles.enabled": "אפשר נקודות אחיזה לאורחים", + "handles.enabled-help": "אפשרות זו חושפת שדה חדש המאפשר לאורחים לבחור שם שישויך לכל פוסט שהם מבצעים. אם הם מושבתים, הם פשוט יקראו \"אורח\"", + "topic-views.enabled": "הגדל מספר צפיות בנושא על-ידי צפיות של אורחים", + "reply-notifications.enabled": "אפשר לאורחים ליצור הודעות תשובה" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/homepage.json b/public/language/he/admin/settings/homepage.json new file mode 100644 index 0000000000..012d9a07ff --- /dev/null +++ b/public/language/he/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "דף הבית", + "description": "בחר איזה דף יוצג כאשר מנווטים לכתובת ה-URL הראשית של הפורום.", + "home-page-route": "נתיב דף הבית", + "custom-route": "נתיב מותאם אישית", + "allow-user-home-pages": "אפשר בחירת דף הבית בהגדרות המשתמשים להתאמה אישית", + "home-page-title": "כותרת דף הבית (ברירת מחדל \"דף הבית\")" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/languages.json b/public/language/he/admin/settings/languages.json new file mode 100644 index 0000000000..83fd3bc4a0 --- /dev/null +++ b/public/language/he/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "הגדרות שפה", + "description": "שפת ברירת מחדל מתייחס להגדרות שפה לכל המשתמשים המבקרים בפורום.
עדיין המשתמשים יכולים לשנות את שפת ברירת המחדל בדף הגדרות המשתמש.", + "default-language": "שפת ברירת מחדל", + "auto-detect": "זיהוי הגדרת שפה אוטומטית לאורחים" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/navigation.json b/public/language/he/admin/settings/navigation.json new file mode 100644 index 0000000000..9d7e256e82 --- /dev/null +++ b/public/language/he/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "סמליל:", + "change-icon": "שנה", + "route": "נתיב:", + "tooltip": "טולטיפ:", + "text": "טקסט:", + "text-class": "Class לטקסט: אופציונאלי", + "class": "Class: אופציונאלי", + "id": "id: אופציונאלי", + + "properties": "הרשאות:", + "groups": "קבוצות:", + "open-new-window": "ייפתח בכרטיסייה חדשה", + "dropdown": "תפריט נפתח", + "dropdown-placeholder": "מקמו את פריטי התפריט הנפתח , באופן הבא:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "מחיקה", + "btn.disable": "השבתה", + "btn.enable": "הפעלה", + + "available-menu-items": "פריטי תפריט זמינים", + "custom-route": "נתיב מותאם אישית", + "core": "ליבה", + "plugin": "תוסף" +} diff --git a/public/language/he/admin/settings/notifications.json b/public/language/he/admin/settings/notifications.json new file mode 100644 index 0000000000..5e25d54217 --- /dev/null +++ b/public/language/he/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "התראות", + "welcome-notification": "הודעת ברוכים הבאים", + "welcome-notification-link": "קישור הודעת ברוכים הבאים", + "welcome-notification-uid": "הודעת ברוכים הבאים למשתמש (UID)", + "post-queue-notification-uid": "רשום משתמש בתור (UID)" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/pagination.json b/public/language/he/admin/settings/pagination.json new file mode 100644 index 0000000000..94adec9184 --- /dev/null +++ b/public/language/he/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "הגדרת חלוקת עמודים", + "enable": "חלק נושאים ופוסטים במקום עמוד גלילה אינסופית", + "posts": "חלוקת פוסטים", + "topics": "חלוקת נושאים", + "posts-per-page": "פוסטים לעמוד", + "max-posts-per-page": "מקסימום פוסטים לעמוד", + "categories": "חלוקת קטגוריות", + "topics-per-page": "נושאים לעמוד", + "max-topics-per-page": "מקסימום נושאים לעמוד", + "categories-per-page": "קטגוריות לעמוד" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/post.json b/public/language/he/admin/settings/post.json new file mode 100644 index 0000000000..5b39f4250c --- /dev/null +++ b/public/language/he/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "מיון פוסטים", + "sorting.post-default": "מיון ברירת מחדל של פוסטים", + "sorting.oldest-to-newest": "מישן לחדש", + "sorting.newest-to-oldest": "מחדש לישן", + "sorting.most-votes": "רוב ההצבעות", + "sorting.most-posts": "הכי הרבה פוסטים", + "sorting.topic-default": "מיון ברירת מחדל של נושאים", + "length": "אורך פוסט", + "post-queue": "תור פוסטים", + "restrictions": "הגבלות רישום", + "restrictions-new": "הגבלות משתמש חדש", + "restrictions.post-queue": "הפוך תור פוסט לזמין", + "restrictions.post-queue-rep-threshold": "מוניטין נדרש כדי לעקוף תור פוסט", + "restrictions.groups-exempt-from-post-queue": "בחר קבוצות פטורות מתור פוסטים", + "restrictions-new.post-queue": "הפיכת הגבלות משתמש חדשות לזמינות", + "restrictions.post-queue-help": "הפעלת תור פוסטים תכניס את ההודעות של משתמשים חדשים לתור לאישור", + "restrictions-new.post-queue-help": "הפעלת הגבלות משתמשים חדשים תגדיר הגבלות על פוסטים שנוצרו על-ידי משתמשים חדשים", + "restrictions.seconds-between": "מספר השניות בין פוסטים", + "restrictions.seconds-between-new": "שניות בין פוסטים עבור משתמשים חדשים", + "restrictions.rep-threshold": "סף המוניטין לפני ביטול המגבלות הללו", + "restrictions.seconds-before-new": "שניות לפני שמשתמש חדש יוכל לפרסם את הפוסט הראשון שלו", + "restrictions.seconds-edit-after": "מספר השניות בה ניתן לערוך פוסט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", + "restrictions.seconds-delete-after": "מספר השניות בה ניתן למחוק פוסט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", + "restrictions.replies-no-delete": "מספר תשובות בנושא שלאחריו לא יוכל מפרסם הנושא למחקו (כתבו 0 להפוך ללא זמין)", + "restrictions.min-title-length": "אורך כותרת מינימלי", + "restrictions.max-title-length": "אורך כותרת מקסימלי", + "restrictions.min-post-length": "אורך פוסט מינימלי", + "restrictions.max-post-length": "אורך פוסט מקסימלי", + "restrictions.days-until-stale": "ימים עד שנושא נחשב ישן", + "restrictions.stale-help": "אם נושא נחשב \"ישן\", תוצג אזהרה למשתמשים המנסים להשיב לנושא זה.", + "timestamp": "חותמת זמן", + "timestamp.cut-off": "תאריך ניתוק (בימים)", + "timestamp.cut-off-help": "Dates & הזמנים יוצגו באופן יחסי (למשל \"לפני 3 שעות\" / \"לפני 5 ימים\"), לפי שעה ושפה מקומית. לאחר זמן זה, יוצג התאריך המקומי עצמו\n\t\t\t\t\t(לדוגמא, 5 בנובמבר 2016, 15:30).
(ברירת מחדל: 30, או חודש אחד). הגדר ל 0 כדי להציג תמיד תאריכים, השאר ריק כדי להציג תמיד זמנים יחסית.", + "timestamp.necro-threshold": "סף ה-Necro (בימים)", + "timestamp.necro-threshold-help": "הודעה תוצג בין פוסטים אם הזמן ביניהם ארוך יותר מסף ה-necro . (ברירת מחדל: 7, או שבוע אחד). כתוב 0 בכדי להפוך ללא זמין.", + "timestamp.topic-views-interval": "מרווח תצוגות של נושא קבוע (בדקות)", + "timestamp.topic-views-interval-help": "תצוגות נושא יגדלו רק פעם ב- X דקות כפי שהוגדרו על-ידי הגדרה זו.", + "teaser": "פוסט טיזר", + "teaser.last-post": "Last – הצג את הפוסט האחרון, כולל הפוסט המקורי, אם אין תגובות", + "teaser.last-reply": "Last – הצג את התשובה האחרונה, או ציין \"ללא תשובות\" אם אין תשובות", + "teaser.first": "ראשון", + "showPostPreviewsOnHover": "הצג תצוגה מקדימה בריחוף על פוסט", + "unread": "הגדרות \"שלא נקראו\"", + "unread.cutoff": "ימי ניתוק שלא נקראו", + "unread.min-track-last": "פוסטים מינימליים בנושא לפני מעקב אחר קריאה אחרונה", + "recent": "הגדרות פוסטים אחרונים", + "recent.max-topics": "מקסימום נושאים בעמוד פוסטים אחרונים", + "recent.categoryFilter.disable": "הפיכת סינון נושאים ללא זמין בקטגוריות שהתעלמו מהן בדף פוסטים אחרונים", + "signature": "הגדרות חתימה", + "signature.disable": "השבת חתימות", + "signature.no-links": "השבת קישורים בחתימות", + "signature.no-images": "השבת תמונות בחתימות", + "signature.hide-duplicates": "הצג חתימות פעם אחת בלבד בכל נושא", + "signature.max-length": "אורך חתימה מרבי", + "composer": "הגדרות יצירת פוסט", + "composer-help": "ההגדרות הבאות חלות על הפונקציונליות ו/או המראה של יוצר הפוסט המוצג\n\t\t\t\tלמשתמשים בעת יצירת נושאים חדשים, או מענה לנושאים קיימים.", + "composer.show-help": "הצג כרטיסיית \"עזרה\"", + "composer.enable-plugin-help": "אפשר לתוספים להוסיף תוכן ללשונית עזרה", + "composer.custom-help": "טקסט עזרה מותאם אישית", + "backlinks": "קישורים נכנסים", + "backlinks.enabled": "אפשר קישורים נכנסים בנושא", + "backlinks.help": "אם פוסט מפנה לנושא אחר, קישור חזרה לפוסט יתווסף לנושא אליו בוצעה ההפניה בשלב זה.", + "ip-tracking": "IP מעקב", + "ip-tracking.each-post": "מעקב אחר כתובת IP על כל הודעה", + "enable-post-history": "הפוך היסטוריית פוסטים לזמינה" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/reputation.json b/public/language/he/admin/settings/reputation.json new file mode 100644 index 0000000000..9683ce420f --- /dev/null +++ b/public/language/he/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "הגדרות מוניטין", + "disable": "השבת מערכת המוניטין", + "disable-down-voting": "השבת דיסלייק", + "votes-are-public": "כל ההצבעות פומביות", + "thresholds": "סף פעילות", + "min-rep-upvote": "מוניטין מינימלי כדי להצביע בעד", + "upvotes-per-day": "כמה פעמים ביום משתמש יוכל להצביע למעלה (הגדר ל-0 כדי לאפשר ללא הגבלה)", + "upvotes-per-user-per-day": "כמה פעמים ביום משתמש יוכל להצביע בעד משתמש מסוים (הגדר ל-0 כדי לאפשר ללא הגבלה)", + "min-rep-downvote": "מוניטין מינימלי כדי להצביע נגד הודעות", + "downvotes-per-day": "הצבעות מטה ליום (מוגדרות ל -0 להצבעות למטה ללא הגבלה)", + "downvotes-per-user-per-day": "הצבעות למטה למשתמש ליום (מוגדרות ל -0 להצבעות למטה ללא הגבלה)", + "min-rep-chat": "מוניטין מינימלי כדי לשלוח הודעות בצ'אט", + "min-rep-flag": "מוניטין מינימלי כדי לדווח על הודעות", + "min-rep-website": "מוניטין מינימלי להוספת \"אתר\" לפרופיל המשתמש", + "min-rep-aboutme": "מוניטין מינימלי להוסיף \"אודותיי\" לפרופיל המשתמש", + "min-rep-signature": "מוניטין מינימלי להוספת \"חתימה\" לפרופיל המשתמש", + "min-rep-profile-picture": "מוניטין מינימלי להוסיף \"תמונת פרופיל\" לפרופיל המשתמש", + "min-rep-cover-picture": "מוניטין מינימלי להוסיף \"תמונת נושא\" לפרופיל המשתמש", + + "flags": "הגדרות דיווח", + "flags.limit-per-target": "מספר הפעמים המרבי שניתן לסמן משהו", + "flags.limit-per-target-placeholder": "ברירת מחדל: 0", + "flags.limit-per-target-help": "כשפוסט או משתמש מסומן כמה פעמים, כל דיווח נוסף נחשב ל "דיווח" ונוסף לדיווח הראשון. הגדר את האופציה הזאת לכל מספר שהוא לא 0 כדי להגביל את כמות הדיווחים שפוסט או משתמש יכול לקבל.", + "flags.auto-flag-on-downvote-threshold": "מספר הצבעות למטה כדי ליצור דיווח אטומטי (הגדר ל-0 כדי להשבית; ברירת מחדל: 0)", + "flags.auto-resolve-on-ban": "פתור אוטומטי כל כרטיסי משתמש כאשר הוא מוחרם", + "flags.action-on-resolve": "בצע את הפעולות הבאות כאשר דיווח נפתר", + "flags.action-on-reject": "בצע את הפעולות הבאות כאשר דיווח נדחה", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/social.json b/public/language/he/admin/settings/social.json new file mode 100644 index 0000000000..9c2a35e38f --- /dev/null +++ b/public/language/he/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "שיתוף פוסטים", + "info-plugins-additional": "תוספים יכולים להוסיף כאן רשתות נוספות לשיתוף פוסטים.", + "save-success": "רשתות שיתוף הפוסט נשמרו בהצלחה!" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/sockets.json b/public/language/he/admin/settings/sockets.json new file mode 100644 index 0000000000..464ef58d30 --- /dev/null +++ b/public/language/he/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "הגדרות חיבור מחדש", + "max-attempts": "ניסיון חיבור מחדש מרבי", + "default-placeholder": "ברירת מחדל: %1", + "delay": "עיכוב חיבור מחדש" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/sounds.json b/public/language/he/admin/settings/sounds.json new file mode 100644 index 0000000000..57cca017c5 --- /dev/null +++ b/public/language/he/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "התראות", + "chat-messages": "הודעות צ'אט", + "play-sound": "נגן", + "incoming-message": "הודעה נכנסת", + "outgoing-message": "הודעה יוצאת", + "upload-new-sound": "העלה צליל חדש", + "saved": "הגדרות נשמרו" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/tags.json b/public/language/he/admin/settings/tags.json new file mode 100644 index 0000000000..51d3fd4c1c --- /dev/null +++ b/public/language/he/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "הגדרות תגיות", + "link-to-manage": "נהל תגיות", + "system-tags": "תגיות מערכת", + "system-tags-help": "רק מנהלים יכולים להשתמש בתגית זו", + "min-per-topic": "מינימום תגיות לנושא", + "max-per-topic": "מקסימום תגיות לנושא", + "min-length": "אורך תגית מינימלי", + "max-length": "אורך תגית מקסימלי", + "related-topics": "נושאים קשורים", + "max-related-topics": "מספר מרבי של נושאים קשורים להצגה (אם נתמך על-ידי ערכת הנושא)" +} \ No newline at end of file diff --git a/public/language/he/admin/settings/uploads.json b/public/language/he/admin/settings/uploads.json new file mode 100644 index 0000000000..96d398a357 --- /dev/null +++ b/public/language/he/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "פוסטים", + "orphans": "קבצים יתומים", + "private": "הפוך קבצים שהועלו לפרטיים", + "strip-exif-data": "הפשט נתוני EXIF", + "preserve-orphaned-uploads": "שמור את הקבצים שהועלו בדיסק גם לאחר מחיקת הפוסט", + "orphanExpiryDays": "מספר ימים לשמירת קבצים יתומים", + "orphanExpiryDays-help": "לאחר מספר ימים אלה, העלאות מיותמות יימחקו ממערכת הקבצים.
הגדר ל-0 או השאר ריק כדי להשבית.", + "private-extensions": "סיומות קובצים להפוך לפרטיים", + "private-uploads-extensions-help": "הכנס כאן רשימה של פורמטי הקבצים, מופרדים בפסיק, כדי להפוך אותם לפרטיים (לדוגמא pdf,xls,doc). שורה ריקה פירושו שכל הקבצים פרטיים.", + "resize-image-width-threshold": "שנה גודל תמונות אם הם רחבים יותר מהרוחב המוגדר", + "resize-image-width-threshold-help": "(בפיקסלים, ברירת מחדל: 1520 פיקסלים, הגדר 0 כדי להשבית)", + "resize-image-width": "שנה גודל תמונות לגודל המוגדר", + "resize-image-width-help": "(בפיקסלים, ברירת מחדל: 760 פיקסלים, הגדר 0 כדי להשבית)", + "resize-image-quality": "באיזה איכות להשתמש כאשר משנים תמונה", + "resize-image-quality-help": "השתמש ברזולוציה נמוכה כדי להקטין את גודל התמונה הממוזערת", + "max-file-size": "גודל קובץ מירבית (בKiB)", + "max-file-size-help": "(בקיביביטס, ברירת מחדל: 2048 KiB)", + "reject-image-width": "רוחב תמונה מקסימלי (בפיקסלים)", + "reject-image-width-help": "תמונות גבוהות יותר יידחו", + "reject-image-height": "גובה תמונה מקסימלי (בפיקסלים)", + "reject-image-height-help": "תמונות גבוהות יותר יידחו", + "allow-topic-thumbnails": "אפשר למשתמשים להעלות תמונה ממוזערת לנושא", + "topic-thumb-size": "גודל תמונה ממוזערת לנושא", + "allowed-file-extensions": "סיומות קבצים מאושרים", + "allowed-file-extensions-help": "הכנס כאן רשימת פורמטי קבצים מאושרים (לדוגמא. pdf,xls,doc). רשימה ריקה פירושו שכל הקבצים מאושרים.", + "upload-limit-threshold": "הגבל שיעור העלאות מהמשתמשים ל:", + "upload-limit-threshold-per-minute": "ל %1 דקה", + "upload-limit-threshold-per-minutes": "ל %1 דקות", + "profile-avatars": "סמל פרופיל", + "allow-profile-image-uploads": "אפשר למשתמשים להעלאות תמונות פרופיל", + "convert-profile-image-png": "המר העלאות תמונות פרופיל לPNG", + "default-avatar": "סמל אישי ברירת מחדל", + "upload": "העלה", + "profile-image-dimension": "מימדי תמונות פרופיל", + "profile-image-dimension-help": "(בפיקסלים, ברירת מחדל: 128 פיקסלים)", + "max-profile-image-size": "גודל קובץ מקסימלי של תמונות פרופיל", + "max-profile-image-size-help": "(בקיביביטס, ברירת מחדל: 256 KiB)", + "max-cover-image-size": "גודל תמונת נושא מקסימלי", + "max-cover-image-size-help": "(בקיביביטס, ברירת מחדל: 2048 KiB)", + "keep-all-user-images": "השאר גרסה קודמת של הסמל ותמונת הנושא על השרת", + "profile-covers": "תמונות נושא", + "default-covers": "תמונות נושא ברירת מחדל", + "default-covers-help": "הוסף תמונות נושא ברירת מחדל מופרדות בפסיקים עבור משתמשים שאין להם תמונת נושא שהועלתה" +} diff --git a/public/language/he/admin/settings/user.json b/public/language/he/admin/settings/user.json new file mode 100644 index 0000000000..6601d20392 --- /dev/null +++ b/public/language/he/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "אימות", + "email-confirm-interval": "המשתמש לא יוכל לשלוח הודעת אישור מייל עד שיחלוף", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "אפשר התחברות עם", + "allow-login-with.username-email": "שם משתמש או סיסמא", + "allow-login-with.username": "שם משתמש בלבד", + "account-settings": "הגדרות חשבון", + "gdpr_enabled": "אפשר הסכמת איסוף נתונים GDPR", + "gdpr_enabled_help": "כאשר תאפשר, כל המשתמשים החדשים יידרשו לתת הסכמה באופן מפורש לאיסוף ושימוש בנתונים תחת ה התקנה הכללית להגנת נתונים (GDPR). הערה: הפעלת GDPR לא יכריח משתמשים קיימים לאשר הסכמה. כדי לעשות זאת, תצטרכו להתקין את התוסף GDPR.", + "disable-username-changes": "בטל שינויי שם משתמש", + "disable-email-changes": "בטל שינויי כתובת מייל", + "disable-password-changes": "בטל שינויי סיסמא", + "allow-account-deletion": "אפשר מחיקת חשבונות", + "hide-fullname": "החבא שם מלא ממשתמשים", + "hide-email": "החבא כתובת מייל ממשתמשים", + "show-fullname-as-displayname": "הצג את השם המלא של המשתמש כשם התצוגה שלו אם הוא זמין", + "themes": "ערכות נושא", + "disable-user-skins": "אל תאפשר למשתמשים לבחור ערכת נושא", + "account-protection": "הגנת חשבון", + "admin-relogin-duration": "משך חיבור של מנהל מערכת (דקות)", + "admin-relogin-duration-help": "לאחר פרק זמן מוגדר של גישה למקטע הניהול ידרוש כניסה מחדש, הגדר ל- 0 על-מנת להפוך ללא זמין", + "login-attempts": "נסיונות כניסה לשעה", + "login-attempts-help": "אם ניסיונות כניסה ל user's חורג מסף זה, החשבון יינעל לפרק זמן שנקבע מראש", + "lockout-duration": "משך נעילת חשבון (דקות)", + "login-days": "ימים לזכירת התחברות כניסה של משתמשים", + "password-expiry-days": "כפיית איפוס סיסמה לאחר מספר ימים מוגדר", + "session-time": "זמן סשן", + "session-time-days": "ימים", + "session-time-seconds": "שניות", + "session-time-help": "ערכים אלו משמשים כדי להגדיר כמה זמן משתמשים יישארו מחוברים כאשר הם סימנו "זכור אותי" בהתחברות. שים לב שייעשה שימוש רק באחד מהערכים האלו. אם אין ערך שניות נשתמש בערך ימים. אם אין ערך ימים הערך יחזור לברירת מחדל 14 יום.", + "online-cutoff": "אחרי כמה דקות דקות המשתמש ייחשב ללא פעיל", + "online-cutoff-help": "אם משתמש אינו מבצע פעולות במשך זמן זה, הוא נחשב כלא פעיל ואינו מקבל עדכונים בזמן אמת.", + "registration": "רישום משתמש", + "registration-type": "סוג הרשמה", + "registration-approval-type": "סוג אישור הרשמה", + "registration-type.normal": "רגיל", + "registration-type.admin-approval": "אישור מנהל", + "registration-type.admin-approval-ip": "אישור מנהל לכתובות IP", + "registration-type.invite-only": "הזמנה בלבד", + "registration-type.admin-invite-only": "הזמנת מנהל בלבד", + "registration-type.disabled": "בטל הרשמה", + "registration-type.help": "רגיל - משתמשים יכולים להירשם על ידי שימוש בדף ההרשמה (/register).
\nהזמנה בלבד - משתמשים אחרים יכולים להזמין משתמשים מדף המשתמשים.
\nהזמנת מנהל בלבד - רק מנהלים יכולים להזמין משתמשים אחרים מדף
המשתמשים ודף ניהול משתמשים.
\nבטל הרשמה - לא ניתן להירשם.
ד", + "registration-approval-type.help": "רגיל - משתמשים נרשמים באופן מיידי.
\nאישור מנהל - משתמשים אשר נרשמו מוכנסים לתוך רשימת אישור למנהלים.
\nאישור מנהל לכתובות IP - רגיל למשתמשים חדשים, אישור מנהל לכתובות IP אשר כבר מקושר אליהם חשבון.
", + "registration-queue-auto-approve-time": "זמן אישור אוטומטי", + "registration-queue-auto-approve-time-help": "שעות לפני שהמשתמש מאושר באופן אוטומטי. רשום 0 על-מנת להשבית.", + "registration-queue-show-average-time": "הצג למשתמשים זמן ממוצע שנדרש על-מנת לאשר משתמש חדש", + "registration.max-invites": "מרב ההזמנות למשתמש", + "max-invites": "מרב ההזמנות למשתמש", + "max-invites-help": "0 בשביל לבטל הגבלה. מנהלים מקבלים אינסוף הזמנות
תקף רק ל-\"הזמנה בלבד\"", + "invite-expiration": "תוקף ההזמנה", + "invite-expiration-help": "הזמנות יפוגו ב-# ימים.", + "min-username-length": "אורך שם משתמש מינימלי", + "max-username-length": "אורך שם משתמש מקסימלי", + "min-password-length": "אורך סיסמה מינימלי", + "min-password-strength": "חוזק מינימלי של הסיסמה", + "max-about-me-length": "אורך מקסימום של התיבה 'עלי'", + "terms-of-use": "תנאי השימוש של הפורום (השאר ריק כדי להשבית)", + "user-search": "חיפוש משתמשים", + "user-search-results-per-page": "מספר התוצאות להצגה", + "default-user-settings": "הגדרות משתמש ברירת מחדל", + "show-email": "הצג כתובת אימייל", + "show-fullname": "הצג שם מלא", + "restrict-chat": "אשר הודעות צ'אט רק ממשתמשים שאני עוקב אחריהם", + "outgoing-new-tab": "פתח קישורים חיצוניים בכרטיסייה חדשה", + "topic-search": "הפעל חיפוש בתוך נושא", + "update-url-with-post-index": "עדכן את כתובת הURL עם מספר הפוסט הנוכחי בזמן גלישה בנושאים", + "digest-freq": "הרשם לקבלת תקציר", + "digest-freq.off": "כבוי", + "digest-freq.daily": "יומי", + "digest-freq.weekly": "שבועי", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "חודשי", + "email-chat-notifs": "שלח לי הודעה למייל כאשר הודעת צ'אט נשלחה אלי בזמן שאינני מחובר", + "email-post-notif": "שלח לי הודעה למייל כאשר תגובות חדשות פורסמו לנושאים שאני עוקב אחריהם", + "follow-created-topics": "עקוב אחר נושאים שייצרת", + "follow-replied-topics": "עקוב אחר נושאים שהגבת עליהם", + "default-notification-settings": "הגדרות התראות ברירת מחדל", + "categoryWatchState": "מצב מעקב על קטגוריה בברירת מחדל", + "categoryWatchState.watching": "עוקב", + "categoryWatchState.notwatching": "לא עוקב", + "categoryWatchState.ignoring": "מתעלם" +} diff --git a/public/language/he/admin/settings/web-crawler.json b/public/language/he/admin/settings/web-crawler.json new file mode 100644 index 0000000000..10f4d432ea --- /dev/null +++ b/public/language/he/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "הגדרות סריקה", + "robots-txt": "Robots.txt מותאם אישית", + "sitemap-feed-settings": "הגדרות הזנת מידע ומפת האתר", + "disable-rss-feeds": "בטל הזנת RSS", + "disable-sitemap-xml": "בטל את Sitemap.xml", + "sitemap-topics": "מספר הנושאים להצגה במפת האתר", + "clear-sitemap-cache": "נקה את זכרון מפת האתר", + "view-sitemap": "צפייה במפת האתר" +} \ No newline at end of file diff --git a/public/language/he/category.json b/public/language/he/category.json new file mode 100644 index 0000000000..fc896b06b1 --- /dev/null +++ b/public/language/he/category.json @@ -0,0 +1,23 @@ +{ + "category": "קטגוריה", + "subcategories": "קטגוריות משנה", + "new_topic_button": "נושא חדש", + "guest-login-post": "התחבר בכדי לפרסם", + "no_topics": "קטגוריה זו ריקה מנושאים.
למה שלא תנסה להוסיף נושא חדש?", + "browsing": "צופים בנושא זה כעת", + "no_replies": "אין תגובות", + "no_new_posts": "אין פוסטים חדשים.", + "watch": "עקוב", + "ignore": "התעלם", + "watching": "עוקב", + "not-watching": "לא עוקב", + "ignoring": "מתעלם", + "watching.description": "הצג נושאים בנושאים שלא נקראו ובנושאים אחרונים", + "not-watching.description": "הסתר בנושאים שלא נקראו, הצג בנושאים אחרונים", + "ignoring.description": "הסתר נושאים בנושאים שלא נקראו ובנושאים אחרונים", + "watching.message": "בחרת לעקוב אחר עדכונים בקטגוריה זו וכל תת-הקטגוריות", + "notwatching.message": "בחרת לא לעקוב אחר עדכונים בקטגוריה זו וכל תת-הקטגוריות", + "ignoring.message": "בחרת להתעלם מעדכונים בקטגוריה זו וכל תת-הקטגוריות", + "watched-categories": "קטגוריות במעקב", + "x-more-categories": "%1 קטגוריות נוספות" +} \ No newline at end of file diff --git a/public/language/he/email.json b/public/language/he/email.json new file mode 100644 index 0000000000..bcd9ef989e --- /dev/null +++ b/public/language/he/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "מייל בדיקה", + "password-reset-requested": "נדרש איפוס סיסמה!", + "welcome-to": "ברוכים הבאים ל%1", + "invite": "הזמנה מ%1", + "greeting_no_name": "שלום", + "greeting_with_name": "שלום %1", + "email.verify-your-email.subject": "בבקשה אמת את המייל שלך.", + "email.verify.text1": "ביקשת לשנות או לאמת את כתובת הדוא\"ל שלך", + "email.verify.text2": "לצורכי אבטחה, אנו משנים או מאמתים את כתובת הדוא\"ל רק לאחר שבעלותך מאומת ע\"י דוא\"ל. אם לא אתה ביקשת זאת, תוכל להתעלם ממייל זו.", + "email.verify.text3": "לאחר שתאשר את כתובת הדוא\"ל, נשנה את כתובת דוא\"ל הנוכחית בכתובת הזו (%1).", + "welcome.text1": "תודה שנרשמת ל%1!", + "welcome.text2": "על מנת להפעיל את החשבון שלך, אנו צריכים לוודא שאתה בעל חשבון המייל שנרשמת איתו.", + "welcome.text3": "מנהל אישר את ההרשמה שלך.\nאתה יכול להתחבר עם השם משתמש והסיסמא שלך מעכשיו.", + "welcome.cta": "לחץ כאן על מנת לאשר את כתובת המייל שלך.", + "invitation.text1": "%1 הזמין אותך להצטרף ל%2", + "invitation.text2": "ההזמנה של תפוג ב %1 ימים", + "invitation.cta": "לחץ כאן ליצירת החשבון שלך.", + "reset.text1": "קיבלנו בקשה לאפס את הסיסמה לחשבון שלך, כנראה מפני ששכחת אותה. אם לא ביקשת לאפס את הסיסמה, אנא התעלם ממייל זה.", + "reset.text2": "על מנת להמשיך עם תהליך איפוס הסיסמה, אנא לחץ על הלינק הבא:", + "reset.cta": "לחץ כאן לאפס את הסיסמה שלך.", + "reset.notify.subject": "הסיסמה שונתה בהצלחה.", + "reset.notify.text1": "אנו מודיעים לך שב%1, סיסמתך שונתה בהצלחה.", + "reset.notify.text2": "אם לא אישרת בקשה זו, אנא הודע למנהל מיד.", + "digest.latest_topics": "נושאים אחרונים מ%1", + "digest.top-topics": "נושאים עם הכי הרבה הצבעות מ-%1", + "digest.popular-topics": "הנושאים הכי פופולריים מ-%1", + "digest.cta": "לחץ כאן כדי לבקר ב %1", + "digest.unsub.info": "תקציר זה נשלח אליך על-פי הגדרות החשבון שלך.", + "digest.day": "יום", + "digest.week": "שבוע", + "digest.month": "חודש", + "digest.subject": "מקבץ עבור %1", + "digest.title.day": "התקציר היומי שלך", + "digest.title.week": "התקציר השבועי שלך", + "digest.title.month": "התקציר החודשי שלך", + "notif.chat.subject": "הודעת צ'אט חדשה התקבלה מ%1", + "notif.chat.cta": "לחץ כאן כדי להמשיך את השיחה", + "notif.chat.unsub.info": "התראה הצ'אט הזו נשלחה אליך על-פי הגדרות החשבון שלך.", + "notif.post.unsub.info": "התראת הפוסט הזו נשלחה אליך על-פי הגדרות החשבון שלך.", + "notif.post.unsub.one-click": "ניתן גם להפסיק את קבלת המיילים כמו זה, בלחיצה על", + "notif.cta": "כניסה לפורום", + "notif.cta-new-reply": "הצג פוסט", + "notif.cta-new-chat": "הצג צ'אט", + "notif.test.short": "בדיקת התראות.", + "notif.test.long": "זוהי בדיקה של ההתראות במייל.", + "test.text1": "זהו אימייל ניסיון על מנת לוודא שהגדרות המייל בוצעו כהלכה בהגדרות NodeBB.", + "unsub.cta": "לחץ כאן לשנות הגדרות אלו", + "unsubscribe": "בטל רישום", + "unsub.success": "אתה לא תקבל יותר מיילים מרשימת התפוצה של %1", + "unsub.failure.title": "לא ניתן לבטל את המנוי", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "הורחקת מ %1", + "banned.text1": "המשתמש %1 הורחק מ %2.", + "banned.text2": "הרחקה זו תמשך עד %1", + "banned.text3": "זו הסיבה שבגללה הורחקת:", + "closing": "תודה!" +} \ No newline at end of file diff --git a/public/language/he/error.json b/public/language/he/error.json new file mode 100644 index 0000000000..78d7dd20ec --- /dev/null +++ b/public/language/he/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "נתונים שגויים", + "invalid-json": "אובייקט JSON לא תקין", + "wrong-parameter-type": "ערך מסוג %3 היה צפוי למאפיין `%1`, אבל %2 התקבל במקום זאת", + "required-parameters-missing": "פרמטרים נדרשים היו חסרים בקריאת API זו: %1", + "not-logged-in": "נראה שאינכם מחוברים למערכת.", + "account-locked": "חשבונכם נחסם באופן זמני", + "search-requires-login": "התחברו או הירשמו בכדי לבצע חיפוש.", + "goback": "לחצו back לחזור לעמוד הקודם", + "invalid-cid": "מזהה קטגוריה לא תקין", + "invalid-tid": "מזהה נושא לא תקין", + "invalid-pid": "מזהה פוסט לא תקין", + "invalid-uid": "מזהה משתמש לא תקין", + "invalid-mid": "מזהה הודעת הצ'אט לא חוקי", + "invalid-date": "יש לציין תאריך תקין", + "invalid-username": "שם משתמש לא תקין", + "invalid-email": "אימייל לא תקין", + "invalid-fullname": "שם מלא לא תקין", + "invalid-location": "מקום לא תקין", + "invalid-birthday": "יום הולדת לא תקין", + "invalid-title": "כותרת לא תקינה", + "invalid-user-data": "מידע משתמש לא תקין", + "invalid-password": "סיסמא לא תקינה", + "invalid-login-credentials": "פרטי ההתחברות שגויים", + "invalid-username-or-password": "אנא הגדירו שם משתמש וסיסמה", + "invalid-search-term": "מילת חיפוש לא תקינה", + "invalid-url": "שגיאה בכתובת URL", + "invalid-event": "אירוע לא תקין: %1", + "local-login-disabled": "מערכת הכניסה המקומית הושבתה עבור חשבונות שאינם מורשים.", + "csrf-invalid": "אין באפשרותנו לחבר אתכם למערכת, מכיוון שעבר זמן רב מידי. אנא נסו שנית.", + "invalid-path": "נתיב שגוי", + "folder-exists": "התיקיה קיימת", + "invalid-pagination-value": "ערך דף לא חוקי, חייב להיות לפחות %1 ולא מעל %2", + "username-taken": "שם משתמש תפוס", + "email-taken": "כתובת דוא\"ל תפוסה", + "email-nochange": "כתובת דוא\"ל שהוזן זהה לדוא\"ל שנמצא כבר", + "email-invited": "נשלחה כבר הזמנה לדוא\"ל זה", + "email-not-confirmed": "פרסום בקטגוריות או בנושאים מסוימים מופעל רק לאחר אישור הדוא\"ל שלכם, אנא לחצו כאן כדי לשלוח אימות לדוא\"ל שלכם.", + "email-not-confirmed-chat": "אין באפשרותכם לשוחח בצ'אט עד שהדוא\"ל שלכם יאושר, אנא לחצו כאן כדי לאשר את הדוא\"ל שלכם.", + "email-not-confirmed-email-sent": "הדוא\"ל שלכם עדיין לא אושר. אנא בדקו בתיבת הדואר בנוגע לאישור הדוא\"ל שנשלח לך על ידינו. לא תוכלו לכתוב פוסטים ולהשתמש בצ'אט לפני אימות הדוא\"ל שלכם.", + "no-email-to-confirm": "בחשבונך לא הוגדר דוא\"ל. כתובת דוא\"ל נחוץ לשחזור חשבון. אנא לחצו כאן כדי להכניס דוא\"ל.", + "user-doesnt-have-email": "למשתמש \"%1\" לא הוגדר כתובת דוא\"ל.", + "email-confirm-failed": "לא הצלחנו לאשר את הדוא\"ל שלך, נסו שוב מאוחר יותר.", + "confirm-email-already-sent": "דוא\"ל האישור כבר נשלח, אנא המתינו %1 דקות כדי לשלוח דוא\"ל נוסף.", + "sendmail-not-found": "תוכנת sendmail לא נמצאה, בדקו שהיא מותקת וניתנת להרצה על ידי המשתמש שמריץ את NodeBB.", + "digest-not-enabled": "משתמש זה ביטל את התקצירים, או שברירת המחדל של המערכת היא לכבות תקצירים.", + "username-too-short": "שם משתמש קצר מדי", + "username-too-long": "שם משתמש ארוך מדי", + "password-too-long": "הסיסמה ארוכה מדי", + "reset-rate-limited": "יותר מידי בקשות לאימות סיסמא (הקצב מוגבל)", + "reset-same-password": "אנא השתמשו בסיסמא שונה מהסיסמא הנוכחית שלכם.", + "user-banned": "המשתמש מורחק", + "user-banned-reason": "מצטערים, חשבון זה הורחק (סיבה: %1)", + "user-banned-reason-until": "מצטערים, חשבון זה הורחק עד %1 (סיבה: %2)", + "user-too-new": "אנא המתינו %1 שניות לפני פרסום ההודעה", + "blacklisted-ip": "מצטערים, אך הורחקתם מקהילה זו. אם הנכם סבורים שמדובר בטעות, אנא צרו קשר עם מנהלי הקהילה.", + "ban-expiry-missing": "אנא ספקו תאריך סיום להרחקה זו.", + "no-category": "קטגוריה אינה קיימת", + "no-topic": "נושא אינו קיים", + "no-post": "פוסט אינו קיים", + "no-group": "קבוצה לא קיימת", + "no-user": "משתמש אינו קיים", + "no-teaser": "תקציר אינו קיים", + "no-flag": "דיווח לא קיים", + "no-privileges": "ההרשאות שלכם אינן מספיקות לביצוע פעולה זו.", + "category-disabled": "קטגוריה לא פעילה", + "topic-locked": "נושא נעול", + "post-edit-duration-expired": "ניתן לערוך פוסטים עד %1 שניות מרגע כתיבת הפוסט", + "post-edit-duration-expired-minutes": "ניתן לערוך פוסטים עד %1 דקות מרגע כתיבת הפוסט", + "post-edit-duration-expired-minutes-seconds": "ניתן לערוך פוסטים עד %1 דקות %2 שניות מרגע כתיבת הפוסט", + "post-edit-duration-expired-hours": "ניתן לערוך פוסטים עד %1 שעות מרגע כתיבת הפוסט", + "post-edit-duration-expired-hours-minutes": "ניתן לערוך פוסטים עד %1 שעות %2 דקות מרגע כתיבת הפוסט", + "post-edit-duration-expired-days": "ניתן לערוך פוסטים עד %1 ימים מרגע כתיבת הפוסט", + "post-edit-duration-expired-days-hours": "ניתן לערוך פוסטים עד %1 ימים %2 שעות מרגע כתיבת הפוסט", + "post-delete-duration-expired": "ניתן למחוק פוסטים עד %1 שניות מרגע כתיבת הפוסט", + "post-delete-duration-expired-minutes": "ניתן למחוק פוסטים עד %1 דקות מרגע כתיבת הפוסט", + "post-delete-duration-expired-minutes-seconds": "ניתן למחוק פוסטים עד %1 דקות %2 שניות מרגע כתיבת הפוסט", + "post-delete-duration-expired-hours": "ניתן למחוק פוסטים עד %1 שעות מרגע כתיבת הפוסט", + "post-delete-duration-expired-hours-minutes": "ניתן למחוק פוסטים עד %1 שעות %2 דקות מרגע כתיבת הפוסט", + "post-delete-duration-expired-days": "ניתן למחוק פוסטים עד %1 ימים מרגע כתיבת הפוסט", + "post-delete-duration-expired-days-hours": "ניתן למחוק פוסטים עד %1 ימים %2 שעות מרגע כתיבת הפוסט", + "cant-delete-topic-has-reply": "לא ניתן למחוק נושא לאחר שהגיבו בו.", + "cant-delete-topic-has-replies": "לא ניתן למחוק נושא לאחר שקיבל %1 תגובות", + "content-too-short": "כתבו פוסט ארוך יותר. פוסטים חייבים להכיל לפחות %1 תווים.", + "content-too-long": "כתבו פוסט קצר יותר. פוסטים יכולים להיות רק עד %1 תווים.", + "title-too-short": "הכניסו כותרת ארוכה יותר. כותרות חייבות להכיל לפחות %1 תווים.", + "title-too-long": "הכניסו כותרת קצרה יותר. כותרות יכולות להיות רק עד %1 תווים.", + "category-not-selected": "לא נבחרה קטגוריה", + "too-many-posts": "ניתן לפרסם פוסט רק פעם ב-%1 שניות - אנא המתינו לפני פרסום נוסף", + "too-many-posts-newbie": "כמשתמשים חדשים, אתם יכולים לפרסם פוסט רק פעם ב-%1 שניות עד שיהיו לכם %2 נקודות מוניטין - אנא המתינו לפני פרסום נוסף", + "already-posting": "You are already posting", + "tag-too-short": "הכניסו תגית ארוכה יותר. תגיות חייבות להכיל לפחות %1 תווים", + "tag-too-long": "הכניסו תגית קצרה יותר. תגיות יכולות להיות רק עד %1 תווים", + "not-enough-tags": "אין מספיק תגיות. נושא חייב להכיל לפחות %1 תגיות", + "too-many-tags": "יותר מדי תגיות. נושאים יכולים להכיל רק %1 תגיות", + "cant-use-system-tag": "אינכם יכול להשתמש בתווית מערכת זו.", + "cant-remove-system-tag": "אינכם יכול להסיר תווית מערכת זו.", + "still-uploading": "אנא המתינו לסיום ההעלאות.", + "file-too-big": "הגודל המקסימלי המותר לקובץ הוא %1 קילובייט - אנא העלו קובץ קטן יותר", + "guest-upload-disabled": "אורחים אינם מאופשרים להעלות קבצים", + "cors-error": "לא ניתן להעלות את התמונה עקב שגיאת CORS.", + "upload-ratelimit-reached": "העלתם יותר מידי קבצים בפעם אחת. אנא נסו שוב במועד מאוחר יותר.", + "scheduling-to-past": "אנא בחרו תאריך עתידי.", + "invalid-schedule-date": "אנא הזינו תאריך ושעה תקינים.", + "cant-pin-scheduled": "נושא מתוזמן אינו יכול להיות (לא-) נעוץ.", + "cant-merge-scheduled": "נושא מתוזמן אינו יכול להתמזג.", + "cant-move-posts-to-scheduled": "אין אפשרות להעביר פוסטים לנושא מתוזמן.", + "cant-move-from-scheduled-to-existing": "אין אפשרות להעביר פוסטים מנושא מתוזמן לנושא קיים.", + "already-bookmarked": "הוספתם כבר פוסט זה לרשימת המועדפים", + "already-unbookmarked": "הסרתם כבר פוסט זה מרשימת המועדפים", + "cant-ban-other-admins": "אין אפשרות לחסום מנהלים אחרים!", + "cant-mute-other-admins": "אין אפשרות להשתיק מנהלים אחרים!", + "user-muted-for-hours": "הושתקתם, תוכלו לפרסם פוסט בעוד %1 שעות", + "user-muted-for-minutes": "הושתקתם, תוכלו לפרסם פוסט בעוד %1 דקות", + "cant-make-banned-users-admin": "אין אפשרות להפוך משתמשים מורחקים למנהלים.", + "cant-remove-last-admin": "אתם המנהלים היחידים. הוסיפו משתמש אחר לניהול לפני שאתם מוריד את עצמכם מניהול", + "account-deletion-disabled": "מחיקת החשבון מושבת", + "cant-delete-admin": "הסירו הרשאות מנהל מחשבון זה לפני שתנסו למחוק אותו.", + "already-deleting": "נמחק כבר", + "invalid-image": "תמונה לא תקינה", + "invalid-image-type": "פורמט תמונה לא תקין. הפורמטים המורשים הם: %1", + "invalid-image-extension": "פורמט תמונה לא תקין", + "invalid-file-type": "פורמט קובץ לא תקין. הפורמטים המורשים הם: %1", + "invalid-image-dimensions": "ממדי התמונה גדולים מדי", + "group-name-too-short": "שם קבוצה קצר מדי", + "group-name-too-long": "שם קבוצה ארוך מידי", + "group-already-exists": "קבוצה כבר קיימת", + "group-name-change-not-allowed": "לא ניתן לשנות את שם הקבוצה", + "group-already-member": "כבר חבר בקבוצה זו", + "group-not-member": "אינו חבר בקבוצה זו", + "group-needs-owner": "קבוצה זו חייבת לפחות מנהל אחד", + "group-already-invited": "משתמש זה כבר הוזמן", + "group-already-requested": "בקשת החברות שלכם כבר נשלחה", + "group-join-disabled": "אינכם רשאים להצטרף לקבוצה כרגע", + "group-leave-disabled": "אינכם רשאים לעזוב את הקבוצה כרגע", + "post-already-deleted": "פוסט זה נמחק כבר", + "post-already-restored": "פוסט זה כבר שוחזר", + "topic-already-deleted": "נושא זה כבר נמחק", + "topic-already-restored": "נושא זה כבר שוחזר", + "cant-purge-main-post": "אינכם יכולים למחוק את הפוסט הראשי, אנא מחקו את הנושא במקום", + "topic-thumbnails-are-disabled": "תמונות ממוזערות לנושא אינן מאופשרות.", + "invalid-file": "קובץ לא תקין", + "uploads-are-disabled": "העלאת קבצים אינה מאופשרת", + "signature-too-long": "מצטערים, החתימה שלכם אינה יכולה להכיל יותר מ-%1 תווים.", + "about-me-too-long": "מצטערים, דף האודות שלכם אינו יכול להיות ארוך מ-%1 תווים.", + "cant-chat-with-yourself": "לא ניתן לעשות צ'אט עם עצמכם!", + "chat-restricted": "משתמש זה חסם את הודעות הצ'אט שלו ממשתמשים זרים. המשתמש חייב לעקוב אחריכם לפני שתוכלו לשוחח איתו בצ'אט", + "chat-disabled": "מערכת הצ'אט לא פעילה", + "too-many-messages": "שלחתם יותר מדי הודעות, אנא המתינו זמן מה.", + "invalid-chat-message": "הודעת צ'אט לא תקינה", + "chat-message-too-long": "הודעות צ'אט לא יכולות להיות ארוכות מ %1 תווים.", + "cant-edit-chat-message": "אינכם רשאים לערוך הודעה זו", + "cant-delete-chat-message": "אינכם רשאים למחוק הודעה זו", + "chat-edit-duration-expired": "ניתן לערוך הודעות צ'אט עד %1 שניות מרגע כתיבת ההודעה", + "chat-delete-duration-expired": "ניתן למחוק הודעות צ'אט עד %1 שניות מרגע כתיבת ההודעה", + "chat-deleted-already": "הודעת צ'אט זו כבר נמחקה.", + "chat-restored-already": "הודעת צ'אט זו כבר שוחזרה.", + "chat-room-does-not-exist": "חדר צ'אט אינו קיים.", + "already-voting-for-this-post": "הצבעתם כבר בנושא זה.", + "reputation-system-disabled": "מערכת המוניטין לא פעילה.", + "downvoting-disabled": "היכולת להצביע נגד מושבתת", + "not-enough-reputation-to-chat": "נדרש %1 מוניטין כדי לכתוב בצ'אט", + "not-enough-reputation-to-upvote": "נדרש %1 מוניטין כדי להצביע בעד", + "not-enough-reputation-to-downvote": "נדרש %1 מוניטין כדי להצביע למטה", + "not-enough-reputation-to-flag": "נדרש %1 מוניטין כדי לדווח על פוסט", + "not-enough-reputation-min-rep-website": "נרדש %1 מוניטין כדי להוסיף אתר אינטרנט", + "not-enough-reputation-min-rep-aboutme": "נדרש %1 מוניטין כדי להוסיף תיאור", + "not-enough-reputation-min-rep-signature": "נדרש %1 מוניטין כדי להוסיף חתימה", + "not-enough-reputation-min-rep-profile-picture": "נדרש %1 מוניטין כדי להוסיף תמונת פרופיל", + "not-enough-reputation-min-rep-cover-picture": "נדרש %1 מוניטין כדי להוסיף תמונת רקע לפרופיל", + "post-already-flagged": "דיווחתם כבר על פוסט זה", + "user-already-flagged": "דיווחתם כבר על משתמש זה", + "post-flagged-too-many-times": "התקבל כבר דיווח על פוסט זה.", + "user-flagged-too-many-times": "התקבל דיווח על משתמש זה.", + "cant-flag-privileged": "לא ניתן לדווח על מנהלים או על תוכן שנכתב על ידי מנהלים.", + "self-vote": "אי אפשר להצביע על פוסט שיצרתם", + "too-many-upvotes-today": "ביכולתכם להצביע בעד רק %1 פעמים ביום", + "too-many-upvotes-today-user": "ביכולתכם להצביע בעד משתמש מסוים רק %1 פעמים ביום", + "too-many-downvotes-today": "ביכולתכם להצביע נגד %1 פעמים ביום", + "too-many-downvotes-today-user": "ביכולתכם להצביע נגד משתמש %1 פעמים ביום", + "reload-failed": "אירעה תקלה ב NodeBB בזמן הטעינה של: \"%1\". המערכת תמשיך להגיש דפים קיימים, אבל כדאי שתשחזרו את הפעולות שלכם מהפעם האחרונה בה המערכת עבדה כראוי.", + "registration-error": "שגיאה בהרשמה", + "parse-error": "אירעה שגיאה בעת ניתוח תגובת השרת", + "wrong-login-type-email": "אנא השתמשו בכתובת המייל שלכם להתחברות", + "wrong-login-type-username": "אנא השתמשו בשם המשתמש שלכם להתחברות", + "sso-registration-disabled": "ההרשמה בוטלה ל%1 מהחשבונות, תחילה הירשמו עם כתובת דוא\"ל בבקשה", + "sso-multiple-association": "לא ניתן לחבר מספר חשבונות משירות זה לחשבון שלכם. אנא בטלו את שיוך החשבון הקיים ונסו שוב.", + "invite-maximum-met": "הזמנתם את הכמות המירבית של משתשמים (%1 מתוך %2).", + "no-session-found": "לא נמצאו סשני התחברות!", + "not-in-room": "משתמש זה אינו בחדר הצ'אט", + "cant-kick-self": "אינכם יכולים להסיר את עצמכם מהקבוצה", + "no-users-selected": "לא נבחרו משתמשים", + "invalid-home-page-route": "כתובת דף הבית שגויה", + "invalid-session": "סשן לא תקין", + "invalid-session-text": "נראה שסשן ההתחברות שלכם אינה פעילה יותר. אנא רעננו את הדף.", + "session-mismatch": "סשן לא תואם", + "session-mismatch-text": "נראה שסשן ההתחברות שלכם אינו תואם לשרת. אנא רעננו את הדף.", + "no-topics-selected": "לא נבחרו נושאים!", + "cant-move-to-same-topic": "אינכם יכולים להעביר פוסט לאותו נושא!", + "cant-move-topic-to-same-category": "אינכם יכולים להעביר נושא לאותה קטגוריה!", + "cannot-block-self": "אינכם יכולים לחסום את עצמכם!", + "cannot-block-privileged": "אינכם יכולים לחסום מנהלים ראשיים ומנהלים גלובליים", + "cannot-block-guest": "אורחים אינם יכולים לחסום משתמשים אחרים", + "already-blocked": "המשתמש חסום כבר", + "already-unblocked": "המשתמש שוחרר כבר מהחסימה", + "no-connection": "נראה שיש בעיות בחיבור האינטרנט שלכם...", + "socket-reconnect-failed": "לא ניתן להגיע לשרת בשלב זה. לחצו כאן כדי לנסות שוב, או נסו שוב במועד מאוחר יותר", + "plugin-not-whitelisted": "לא ניתן להתקין את התוסף – ניתן להתקין דרך הניהול רק תוספים שנמצאים ברשימה הלבנה של מנהל החבילות של NodeBB.", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "אירוע הנושא '%1' לא מזוהה", + "cant-set-child-as-parent": "לא ניתן להגדיר קטגוריה משנה לקטגוריית אב", + "cant-set-self-as-parent": "לא ניתן להגדיר את עצמי כקטגוריית אב", + "api.master-token-no-uid": "token ראשי התקבל ללא corresponding `_uid` בגוף הבקשה", + "api.400": "משהו לא היה בסדר עם בקשת ה-payload שהעברתם.", + "api.401": "לא נמצא סשן התחברות פעילה. נא התחברו ונסו שוב.", + "api.403": "אינכם מורשים לבצע את החיוג", + "api.404": "קריאת API שגויה", + "api.426": "HTTPS נדרש לבקשות ל-API של הכתיבה, אנא שלחו מחדש את בקשתכם באמצעות HTTPS", + "api.429": "יותר מידי בקשות, אנא נסו שוב מאוחר יותר", + "api.500": "שגיאה בלתי צפויה אירעה בעת ניסיון להגיש את בקשתכם.", + "api.501": "הנתיב אליו אתם מנסים לתקשר עדיין לא מיושם, אנא נסו שוב מחר", + "api.503": "הנתיב אליו אתם מנסים לתקשר אינו זמין כעת עקב תצורת שרת" +} \ No newline at end of file diff --git a/public/language/he/flags.json b/public/language/he/flags.json new file mode 100644 index 0000000000..4633d59fe3 --- /dev/null +++ b/public/language/he/flags.json @@ -0,0 +1,89 @@ +{ + "state": "מצב", + "reports": "דוחות", + "first-reported": "דווח ראשון", + "no-flags": "הידד! לא נמצאו סימונים.", + "assignee": "מוקצה", + "update": "עדכון", + "updated": "עודכן", + "resolved": "הושלם", + "target-purged": "התוכן שסומן נוקה ולא קיים יותר.", + + "graph-label": "דיווחים יומיים", + "quick-filters": "סינון מהיר", + "filter-active": "קיים סנן אחד או יותר ברשימת הסימונים הזו", + "filter-reset": "הסר סינון", + "filters": "אפשרויות סינון", + "filter-reporterId": "UID של מדווח", + "filter-targetUid": "UID של סימונים", + "filter-type": "סוג סימון", + "filter-type-all": "כל התוכן", + "filter-type-post": "פרסום", + "filter-type-user": "משתמש", + "filter-state": "מצב", + "filter-assignee": "UID של הממונה", + "filter-cid": "קטגוריה", + "filter-quick-mine": "הוקצה עבורי", + "filter-cid-all": "כל הקטגוריות", + "apply-filters": "הפעל סינון", + "more-filters": "מסננים נוספים", + "fewer-filters": "פחות מסננים", + + "quick-actions": "פעולות מהירות", + "flagged-user": "משתמש מסומן", + "view-profile": "צפה בפרופיל", + "start-new-chat": "התחל שיחה חדשה", + "go-to-target": "צפה במטרת הסימון", + "assign-to-me": "הקצה עבורי", + "delete-post": "מחק פוסט", + "purge-post": "מחק לצמיתות", + "restore-post": "שחזור פוסט", + "delete": "מחק דיווח", + + "user-view": "צפה בפרופיל", + "user-edit": "ערוך פרופיל", + + "notes": "הערות הסימון", + "add-note": "הוסף הערה", + "no-notes": "אין הערות", + "delete-note-confirm": "האם אתה בטוח שאתה רוצה למחוק הערה זו?", + "delete-flag-confirm": "האם אתה בטוח שברצונך למחוק את הדיווח הזה?", + "note-added": "נוספה הערה", + "note-deleted": "ההערה נמחקה", + "flag-deleted": "הדיווח נמחק", + + "history": "היסטוריית הסימונים למשתמש", + "no-history": "אין הסיטוריית סימונים", + + "state-all": "כל המצבים", + "state-open": "חדש / פתח", + "state-wip": "תחת עבודה", + "state-resolved": "הושלם", + "state-rejected": "נדחה", + "no-assignee": "לא הוקצה", + + "sort": "סדר על-פי", + "sort-newest": "החדש ראשון", + "sort-oldest": "הישן ראשון", + "sort-reports": "הכי הרבה דיווחים", + "sort-all": "כל סוגי הדיווחים", + "sort-posts-only": "פוסטים בלבד", + "sort-downvotes": "הכי הרבה הצבעות נגד", + "sort-upvotes": "הכי הרבה הצבעות", + "sort-replies": "הכי הרבה תגובות", + + "modal-title": "תוכן הדיווח", + "modal-body": "אנא ציין את הסיבה לסימון %1 %2 לצורך בקרה. לחלופין, השתמש באחד מכפתורי הדיווח המהיר אם אפשר.", + "modal-reason-spam": "זבל", + "modal-reason-offensive": "פוגעני", + "modal-reason-other": "אחר (ציין מטה)", + "modal-reason-custom": "הסיבה לדיווח על התוכן...", + "modal-submit": "שלח דוח", + "modal-submit-success": "התוכן סומן לצרכי בקרה", + + "bulk-actions": "פעולות כלליות", + "bulk-resolve": "השלם דיווח(ים)", + "bulk-success": "%1 דיווחים עודכנו", + "flagged-timeago-readable": "מדווחים (%2)", + "auto-flagged": "[דיווח אוטומטי] פוסט זה קיבל %1 הצבעות למטה." +} \ No newline at end of file diff --git a/public/language/he/global.json b/public/language/he/global.json new file mode 100644 index 0000000000..e9f3feca1a --- /dev/null +++ b/public/language/he/global.json @@ -0,0 +1,126 @@ +{ + "home": "דף הבית", + "search": "חיפוש", + "buttons.close": "סגור", + "403.title": "גישה נדחתה", + "403.message": "הגעתם לעמוד שאין לכם הרשאת צפייה בו", + "403.login": "נסו להתחבר.", + "404.title": "לא נמצא", + "404.message": "הגעתם לעמוד שאינו קיים. חזרו לעמוד הבית", + "500.title": "שגיאה פנימית.", + "500.message": "אופס! נראה שמשהו השתבש!", + "400.title": "בקשה שגויה", + "400.message": "לינק לא תקין, בדקו ונסו שוב. או, חזרו לעמוד הבית.", + "register": "הרשמה", + "login": "התחברות", + "please_log_in": "נא להתחבר", + "logout": "יציאה", + "posting_restriction_info": "כתיבת פוסטים מאופשר למשתמשים רשומים בלבד, לחצו כאן כדי להתחבר.", + "welcome_back": "ברוך שובך", + "you_have_successfully_logged_in": "התחברתם בהצלחה", + "save_changes": "שמור שינויים", + "save": "שמור", + "close": "סגור", + "pagination": "הגדרות עמוד", + "pagination.out_of": "%1 מתוך %2", + "pagination.enter_index": "עבור למיקום פוסט", + "header.admin": "ניהול", + "header.categories": "קטגוריות", + "header.recent": "פוסטים אחרונים", + "header.unread": "לא נקרא", + "header.tags": "תגיות", + "header.popular": "פופולרי", + "header.top": "הכי הרבה הצבעות", + "header.users": "משתמשים", + "header.groups": "קבוצות", + "header.chats": "צ'אטים", + "header.notifications": "התראות", + "header.search": "חיפוש", + "header.profile": "פרופיל", + "header.navigation": "ניווט", + "notifications.loading": "טוען התראות", + "chats.loading": "טוען צ'אטים", + "motd.welcome": "ברוכים הבאים ל-NodeBB, פלטפורמות הדיון העתידני", + "previouspage": "העמוד הקודם", + "nextpage": "העמוד הבא", + "alert.success": "הצלחה", + "alert.error": "שגיאה", + "alert.banned": "מורחק", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "אינך עוקב יותר אחרי %1!", + "alert.follow": "אתה עכשיו עוקב אחרי %1!", + "users": "משתמשים", + "topics": "נושאים", + "posts": "פוסטים", + "x-posts": "%1 פוסטים", + "best": "הגבוה ביותר", + "controversial": "שנוי במחלוקת", + "votes": "הצבעות", + "x-votes": "%1 הצבעות", + "voters": "מצביעים", + "upvoters": "מצביעי בעד", + "upvoted": "הוצבע בעד", + "downvoters": "מצביעי נגד", + "downvoted": "הוצבע נגד", + "views": "צפיות", + "posters": "יוצרי הפוסטים", + "reputation": "מוניטין", + "lastpost": "פוסט אחרון", + "firstpost": "פוסט ראשון", + "read_more": "קרא עוד", + "more": "עוד", + "none": "ללא", + "posted_ago_by_guest": "הפוסט הועלה ב%1 על ידי אורח", + "posted_ago_by": "הפוסט עלה ב %1 על ידי %2", + "posted_ago": "הפוסט הועלה ב %1", + "posted_in": "פורסם ב%1", + "posted_in_by": "פורסם ב%1 על ידי %2", + "posted_in_ago": "הפוסט הועלה ב %1 %2", + "posted_in_ago_by": "הפוסט הועלה ב %1 %2 על ידי %3", + "user_posted_ago": "%1 העלה פוסט %2", + "guest_posted_ago": "אורח העלה פוסט %1", + "last_edited_by": "נערך לאחרונה על ידי %1", + "norecentposts": "אין פוסטים מהזמן האחרון", + "norecenttopics": "אין נושאים מהזמן החרון", + "recentposts": "פוסטים אחרונים", + "recentips": "כתובות IP שהתחברו למערכת לאחרונה", + "moderator_tools": "כלי מודרטור", + "online": "מחובר", + "away": "לא נמצא", + "dnd": "נא לא להפריע", + "invisible": "מוסתר", + "offline": "מנותק", + "email": "אימייל", + "language": "שפה", + "guest": "אורח", + "guests": "אורחים", + "former_user": "משתמש שנמחק", + "system-user": "מערכת", + "unknown-user": "משתמש לא ידוע", + "updated.title": "הפורום עודכן", + "updated.message": "הפורום עודכן לגרסא האחרונה. נא ללחוץ כאן לעדכון הדף.", + "privacy": "פרטיות", + "follow": "עקוב", + "unfollow": "הפסק לעקוב", + "delete_all": "מחק הכל", + "map": "מפה", + "sessions": "סשני התחברות", + "ip_address": "כתובת IP", + "enter_page_number": "הכנס מספר עמוד", + "upload_file": "העלה קובץ", + "upload": "העלה", + "uploads": "העלאות", + "allowed-file-types": "פורמטי הקבצים המורשים הם %1", + "unsaved-changes": "יש לך שינויים שאינם נשמרו. האם הנך בטוח שברצונך להמשיך?", + "reconnecting-message": "החיבור ל-%1 אבד, אנא המתינו בזמן שאנו מנסים לחבר אתכם מחדש", + "play": "נגן", + "cookies.message": "אתר זה משתמש ב cookies על מנת לשפר את חוויות המשתמש.", + "cookies.accept": "קיבלתי!", + "cookies.learn_more": "למד עוד", + "edited": "נערך", + "disabled": "מושבת", + "select": "בחר", + "user-search-prompt": "הקלד כאן משהו על מנת למצוא משתמשים..." +} \ No newline at end of file diff --git a/public/language/he/groups.json b/public/language/he/groups.json new file mode 100644 index 0000000000..7148ca56f2 --- /dev/null +++ b/public/language/he/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "קבוצות", + "view_group": "הצג קבוצה", + "owner": "מנהל קבוצה", + "new_group": "צור קבוצה חדשה", + "no_groups_found": "אין קבוצות להצגה", + "pending.accept": "אשר", + "pending.reject": "דחה", + "pending.accept_all": "אשר הכל", + "pending.reject_all": "דחה הכל", + "pending.none": "אין משתמשים בהמתנה כרגע", + "invited.none": "אין משתמשים מוזמנים כרגע", + "invited.uninvite": "בטל הזמנה", + "invited.search": "חפש משתמש להזמנה לקבוצה זו", + "invited.notification_title": "הוזמנת להצטרף ל%1", + "request.notification_title": "בקשת חברות בקבוצה מאת %1", + "request.notification_text": "%1 ביקש להיות חבר ב%2", + "cover-save": "שמור", + "cover-saving": "שומר", + "details.title": "פרטי קבוצה", + "details.members": "רשימת חברי הקבוצה", + "details.pending": "חברי קבוצה הממתינים לאישור", + "details.invited": "חברים מוזמנים", + "details.has_no_posts": "חברי הקבוצה הזו לא העלו אף פוסט.", + "details.latest_posts": "פוסטים אחרונים", + "details.private": "פרטי", + "details.disableJoinRequests": "השבת בקשות הצטרפות", + "details.disableLeave": "אל תאפשר למשתמשים לעזוב את הקבוצה", + "details.grant": "הענק/בטל בעלות", + "details.kick": "גרש", + "details.kick_confirm": "האם אתה בטוח שאתה רוצה להסיר משתמש זה מהקבוצה?", + "details.add-member": "הוסף משתמש", + "details.owner_options": "ניהול קבוצה", + "details.group_name": "שם קבוצה", + "details.member_count": "מספר חברים", + "details.creation_date": "תאריך יצירה", + "details.description": "תיאור", + "details.member-post-cids": "מזהי קטגוריות להצגת פוסטים מהם", + "details.badge_preview": "תצוגה מקדימה של התג", + "details.change_icon": "שנה אייקון", + "details.change_label_colour": "שנה צבע תווית", + "details.change_text_colour": "שנה צבע טקסט", + "details.badge_text": "טקסט תגית", + "details.userTitleEnabled": "הצג תגית", + "details.private_help": "אם אפשרות זו מופעלת, הצטרפות לקבוצות ידרוש אישור מבעל הקבוצה.", + "details.hidden": "מוסתר", + "details.hidden_help": "אם אפשרות זו מופעלת, קבוצה זו לא תימצא ברשימת הקבוצות, יהיה ניתן להזמין משתמשים רק באופן ידני", + "details.delete_group": "מחק קבוצה", + "details.private_system_help": "קבוצות פרטיות מושבתות ברמת המערכת, אפשרות זו אינה עושה דבר", + "event.updated": "פרטי הקבוצה עודכנו", + "event.deleted": "קבוצת \"%1\" נמחקה", + "membership.accept-invitation": "קבל הזמנה", + "membership.accept.notification_title": "אתה עכשיו חבר ב%1", + "membership.invitation-pending": "הזמנה ממתינה", + "membership.join-group": "הצטרף לקבוצה", + "membership.leave-group": "עזוב קבוצה", + "membership.leave.notification_title": "%1 עזב את קבוצת %2", + "membership.reject": "דחה", + "new-group.group_name": "שם קבוצה", + "upload-group-cover": "העלה תמונת נושא לקבוצה", + "bulk-invite-instructions": "הזן רשימה מופרדת בפסיק של משתמשים אותם תרצה להזמין לקבוצה זו.", + "bulk-invite": "הזמן מספר משתמשים", + "remove_group_cover_confirm": "האם אתה בטוח שאתה רוצה להסיר תמונת נושא?" +} \ No newline at end of file diff --git a/public/language/he/ip-blacklist.json b/public/language/he/ip-blacklist.json new file mode 100644 index 0000000000..7208f8842b --- /dev/null +++ b/public/language/he/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "רשימת IP שחורה", + "description": "לעיתים, הרחקת חשבון משתמש אינו מספיק כדי להרתיע. בפעמים אחרות, הגבלת הגישה לפורום ל- IP ספציפי או לטווח של IP היא הדרך הטובה ביותר להגן על הפורום. בתרחישים אלה, תוכלו להוסיף כתובות IP מטרידות או טווחי CIDR שלמים לרשימה השחורה הזו, וימנע מהם להיכנס לחשבון חדש או לרשום אותו.", + "active-rules": "כללים פעילים", + "validate": "בדיקת רשימה", + "apply": "החל רשימה", + "hints": "עזרה בתחביר", + "hint-1": "כתוב כתובת IP אחת בשורה. ניתן להוסיף טווחי IP בפורמט CIDR (לדוגמא 192.168.100.0/22).", + "hint-2": "ניתן להוסיף הערות באמצעות התחלת השורה ב#.", + + "validate.x-valid": "%1 מתוך %2 כלל(ים) תקינים.", + "validate.x-invalid": "%1 הכללים הבאים לא תקינים: ", + + "alerts.applied-success": "הרשימה השחורה נשמרה", + + "analytics.blacklist-hourly": "Figure 1 – כתובות נחסמו ביחס לשעה.", + "analytics.blacklist-daily": "Figure 2 – כתובות נחסמו ביחס ליום.", + "ip-banned": "כתובת הIP הורחקה." +} \ No newline at end of file diff --git a/public/language/he/language.json b/public/language/he/language.json new file mode 100644 index 0000000000..2c12710abf --- /dev/null +++ b/public/language/he/language.json @@ -0,0 +1,5 @@ +{ + "name": "עברית (ישראל)", + "code": "he", + "dir": "rtl" +} \ No newline at end of file diff --git a/public/language/he/login.json b/public/language/he/login.json new file mode 100644 index 0000000000..f8b86ca460 --- /dev/null +++ b/public/language/he/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "שם משתמש/אימייל", + "username": "שם משתמש", + "remember_me": "זכור אותי?", + "forgot_password": "שכחת סיסמתך?", + "alternative_logins": "התחבר באמצעות...", + "failed_login_attempt": "ההתחברות נכשלה", + "login_successful": "התחברת בהצלחה!", + "dont_have_account": "אין לך חשבון עדיין?", + "logged-out-due-to-inactivity": "התנתקת מפאנל האדמין בגלל חוסר אקטיביות", + "caps-lock-enabled": "Caps Lock מופעל" +} \ No newline at end of file diff --git a/public/language/he/modules.json b/public/language/he/modules.json new file mode 100644 index 0000000000..fefaec201a --- /dev/null +++ b/public/language/he/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "שוחחו בצ'אט עם", + "chat.placeholder": "כתבו תוכן הודעת הצ'אט כאן, ניתן לגרור ולשחרר כאן תמונות, הקישו אנטר לשליחה.", + "chat.scroll-up-alert": "הנכם צופים כעת בהודעות ישנות. לחצו כאן למעבר להודעה האחרונה.", + "chat.send": "שליחה", + "chat.no_active": "אין לכם צ'אטים פעילים.", + "chat.user_typing": "%1 מקליד...", + "chat.user_has_messaged_you": "ל%1 יש הודעה עבורכם.", + "chat.see_all": "צפו בכל הצ'אטים", + "chat.mark_all_read": "סמנו את כל הצ'אטים כ'נקראו'", + "chat.no-messages": "בחרו משתמש על מנת לראות את שיחות הצ'אט ביניכם", + "chat.no-users-in-room": "אין משתמשים בחדר הזה", + "chat.recent-chats": "צ'אטים אחרונים", + "chat.contacts": "אנשי קשר", + "chat.message-history": "היסטוריית הודעות", + "chat.message-deleted": "ההודעה נמחקה", + "chat.options": "אפשרויות צ'אט", + "chat.pop-out": "מזעור חלונית צ'אט", + "chat.minimize": "צמצום", + "chat.maximize": "הרחבה", + "chat.seven_days": "7 ימים", + "chat.thirty_days": "30 ימים", + "chat.three_months": "3 חודשים", + "chat.delete_message_confirm": "האם למחוק הודעה זו?", + "chat.retrieving-users": "מאחזר משתמשים...", + "chat.manage-room": "ניהול חדר צ'אט", + "chat.add-user-help": "חפשו משתמשים כאן. כאשר משתמש נבחר, הוא יצורף לצ'אט. המשתמש החדש לא יוכל לראות הודעות שנכתבו לפני הצטרפותו. רק מנהלי החדר () יכולים להסיר משתמשים מהצ'אט.", + "chat.confirm-chat-with-dnd-user": "משתמש זה שינה את הסטטוס שלו ל'לא להפריע'. אתם עדיין מעוניין לשוחח איתו?", + "chat.rename-room": "שינוי שם חדר", + "chat.rename-placeholder": "הזינו את שם החדר שלכם כאן", + "chat.rename-help": "שם החדר המוגדר כאן יהיה זמין לכל המשתתפים בחדר.", + "chat.leave": "עזיבת שיחה", + "chat.leave-prompt": "האם לעזוב שיחה זו?", + "chat.leave-help": "עזיבת שיחה, תסיר אתכם מהתכתבות עתידית בצ'אט זה. אם תצטרפו מחדש בעתיד, לא תראו את היסטוריית הצ'אט שלפני הצטרפותכם מחדש.", + "chat.in-room": "בתוך חדר זה", + "chat.kick": "הוצא", + "chat.show-ip": "הצג IP", + "chat.owner": "מנהלי החדר", + "chat.system.user-join": "%1 הצטרף לחדר", + "chat.system.user-leave": "%1 יצא מהחדר", + "chat.system.room-rename": "%2 שינה את שם החדר: %1", + "composer.compose": "יצירה", + "composer.show_preview": "הצגת תצוגה מקדימה", + "composer.hide_preview": "הסתרת תצוגה מקדימה", + "composer.user_said_in": "%1 כתב ב%2:", + "composer.user_said": "%1 כתב:", + "composer.discard": "האם לבטל את השינויים שנעשו בפוסט זה?", + "composer.submit_and_lock": "אשרו ונעלו", + "composer.toggle_dropdown": "הדליקו / כבו את התפריט הנפתח", + "composer.uploading": "העלאה %1", + "composer.formatting.bold": "מודגש", + "composer.formatting.italic": "נטוי", + "composer.formatting.list": "רשימה", + "composer.formatting.strikethrough": "קו פוסל", + "composer.formatting.code": "קוד", + "composer.formatting.link": "קישור", + "composer.formatting.picture": "קישור תמונה", + "composer.upload-picture": "העלאת תמונה", + "composer.upload-file": "העלאת קובץ", + "composer.zen_mode": "מסך מלא", + "composer.select_category": "בחירת קטגוריה", + "composer.textarea.placeholder": "כתבו את תוכן הפוסט כאן. ניתן גם לגרור ולשחרר כאן תמונות.", + "composer.schedule-for": "תזמון נושא ל", + "composer.schedule-date": "תאריך", + "composer.schedule-time": "שעה", + "composer.cancel-scheduling": "ביטול תיזמון", + "composer.set-schedule-date": "הגדרת תאריך", + "bootbox.ok": "אישור", + "bootbox.cancel": "ביטול", + "bootbox.confirm": "אישור", + "bootbox.submit": "שליחה", + "bootbox.send": "שליחה", + "cover.dragging_title": "מיקום תמונת נושא", + "cover.dragging_message": "גררו תמונת נושא למיקום הרצוי ולחצו על \"שמירה\"", + "cover.saved": "תמונת הנושא והמיקום שלה נשמרו", + "thumbs.modal.title": "ניהול תמונה ממוזערת של הנושא", + "thumbs.modal.no-thumbs": "לא נמצאו תמונות ממוזערות", + "thumbs.modal.resize-note": "הערה: הפורום מוגדר לשנות את התמונה הממוזערת לגודל מקסימום של %1px", + "thumbs.modal.add": "הוספת תמונה ממוזערת", + "thumbs.modal.remove": "הסרת תמונה ממוזערת", + "thumbs.modal.confirm-remove": "האם להסיר את התמונה הממוזערת?" +} \ No newline at end of file diff --git a/public/language/he/notifications.json b/public/language/he/notifications.json new file mode 100644 index 0000000000..27ce74af7c --- /dev/null +++ b/public/language/he/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "התראות", + "no_notifs": "אין התראות", + "see_all": "כל ההתראות", + "mark_all_read": "סמן הכל כנקרא", + "back_to_home": "חזרה ל%1", + "outgoing_link": "קישור יוצא", + "outgoing_link_message": "אתה עוזב עכשיו את %1", + "continue_to": "המשך ל %1", + "return_to": "חזור ל %1", + "new_notification": "יש לך התראה חדשה", + "you_have_unread_notifications": "יש לך התראות שלא נקראו.", + "all": "הכל", + "topics": "נושאים", + "replies": "תגובות", + "chat": "צ'אטים", + "group-chat": "צ'אט קבוצתי", + "follows": "עוקבים", + "upvote": "הצבעות בעד", + "new-flags": "דיווחים חדשים", + "my-flags": "דיווחים שהוקצו עבורי", + "bans": "הרחקות", + "new_message_from": "הודעה חדשה מ %1", + "upvoted_your_post_in": "%1 הצביע בעד הפוסט שלך ב %2", + "upvoted_your_post_in_dual": "%1 ו%2 הצביעו בעד הפוסט שלך ב%3", + "upvoted_your_post_in_multiple": "%1 ו%2 אחרים הצביעו לפוסט שלך ב%3.", + "moved_your_post": "%1 העביר את הפוסט שלך ל%2", + "moved_your_topic": "%1 הזיז את %2", + "user_flagged_post_in": "%1 דיווח על פוסט ב %2", + "user_flagged_post_in_dual": "%1 ו%2 סימנו פוסט ב%3", + "user_flagged_post_in_multiple": "%1 ו%2 נוספים סימנו פוסט ב%3", + "user_flagged_user": "%1 דיווח על משתמש (%2)", + "user_flagged_user_dual": "%1 ו - %2 דיווחו על משתמש (%3)", + "user_flagged_user_multiple": "%1 ו-%2 נוספים דיווחו על משתמש (%3)", + "user_posted_to": "%1 פרסם תגובה ל: %2", + "user_posted_to_dual": "%1 ו%2 הגיבו ל: %3", + "user_posted_to_multiple": "%1 ו%2 אחרים הגיבו ל: %3", + "user_posted_topic": "%1 העלה נושא חדש: %2", + "user_edited_post": "%1 ערך פוסט ב%2", + "user_started_following_you": "%1 התחיל לעקוב אחריך.", + "user_started_following_you_dual": "%1 ו-%2 התחילו לעקוב אחריך.", + "user_started_following_you_multiple": "%1 ו%2 התחילו לעקוב אחריך.", + "new_register": "%1 שלח בקשת הרשמה.", + "new_register_multiple": "ישנן %1 בקשות הרשמה שמחכות לבדיקה.", + "flag_assigned_to_you": "דיווח %1 הוקצה עבורך", + "post_awaiting_review": "הפוסט ממתין לאישור", + "profile-exported": "%1 פרופיל יוצא, לחץ כדי להוריד.", + "posts-exported": "%1 פוסטים יוצאו, לחץ כדי להוריד.", + "uploads-exported": "%1 העלאות יוצאו, לחץ כדי להוריד.", + "users-csv-exported": "משתמשים יוצאו כ-csv, לחץ כאן להורדה.", + "post-queue-accepted": "הפוסט ששלחת התקבל. לחץ כאן כדי לראות את הפוסט", + "post-queue-rejected": "הפוסט ששלחת נדחה", + "post-queue-notify": "פוסט ממתין בתור הפוסטים קיבל הודעה: \"%1\"", + "email-confirmed": "כתובת המייל אושרה", + "email-confirmed-message": "תודה שאישרת את כתובת המייל שלך. החשבון שלך פעיל כעת.", + "email-confirm-error-message": "אירעה שגיאה בעת אישור המייל שלך. ייתכן כי הקוד היה שגוי או פג תוקף.", + "email-confirm-sent": "מייל אישור נשלח.", + "none": "אף אחד", + "notification_only": "התראות בלבד", + "email_only": "דוא\"ל בלבד", + "notification_and_email": "התראות & דוא\"ל", + "notificationType_upvote": "כאשר מישהו מצביע בעד הפוסט שלך", + "notificationType_new-topic": "כשמישהו שאתה עוקב אחריו פרסם נושא", + "notificationType_new-reply": "כשתגובה חדשה מפורסמת בנושא שאתה עוקב אחריו", + "notificationType_post-edit": "כשפוסט נערך בנושא שאתה עוקב אחריו", + "notificationType_follow": "כשמישהו מתחיל לעקוב אחריך", + "notificationType_new-chat": "כשאתה מקבל הודעת צאט", + "notificationType_new-group-chat": "כשאתה מקבל הודעת צ'אט קבוצתית", + "notificationType_group-invite": "כשאתה מקבל הזמנה מקבוצה", + "notificationType_group-leave": "כאשר משתמש עוזב את הקבוצה שלך", + "notificationType_group-request-membership": "כשמישהו מבקש להירשם לקבוצה שאתה מנהל", + "notificationType_new-register": "כאשר מישהו מתווסף לתור הרישום", + "notificationType_post-queue": "כשהודעה חדשה נכנסת לתור", + "notificationType_new-post-flag": "כאשר פוסט מסומן", + "notificationType_new-user-flag": "כאשר משתמש מסומן" +} \ No newline at end of file diff --git a/public/language/he/pages.json b/public/language/he/pages.json new file mode 100644 index 0000000000..0e5aefc608 --- /dev/null +++ b/public/language/he/pages.json @@ -0,0 +1,65 @@ +{ + "home": "דף הבית", + "unread": "נושאים שלא נקראו", + "popular-day": "נושאים חמים היום", + "popular-week": "נושאים חמים השבוע", + "popular-month": "נושאים חמים החודש", + "popular-alltime": "הנושאים החמים בכל הזמנים", + "recent": "נושאים אחרונים", + "top-day": "הנושאים הנבחרים ביותר היום", + "top-week": "הנושאים הנבחרים ביותר השבוע", + "top-month": "הנושאים הנבחרים ביותר החודש", + "top-alltime": "הנושאים הנבחרים ביותר", + "moderator-tools": "כלי מודרטור", + "flagged-content": "תוכן מדווח", + "ip-blacklist": "רשימת IP שחורה", + "post-queue": "פוסטים ממתינים", + "users/online": "משתמשים מחוברים", + "users/latest": "משתמשים אחרונים", + "users/sort-posts": "משתמשים עם המונה הגבוה ביותר", + "users/sort-reputation": "משתמשים עם המוניטין הגבוה ביותר", + "users/banned": "משתמשים מורחקים", + "users/most-flags": "משתמשים שדווחו הכי הרבה", + "users/search": "חיפוש משתמשים", + "notifications": "התראות", + "tags": "תגיות", + "tag": "נושאים שתויגו תחת "%1"", + "register": "יצירת חשבון", + "registration-complete": "ההרשמה הושלמה", + "login": "התחברות לחשבון", + "reset": "איפוס סיסמה למשתמש", + "categories": "קטגוריות", + "groups": "קבוצות", + "group": "קבוצת %1", + "chats": "הודעות פרטיות", + "chat": "שלחו הודעה פרטית ל%1", + "flags": "דיווחים", + "flag-details": "פרטי דיווח %1", + "account/edit": "עריכת \"%1\"", + "account/edit/password": "עריכת סיסמה של \"%1\"", + "account/edit/username": "עריכת שם משתמש של \"%1\"", + "account/edit/email": "עריכת כתובת מייל של \"%1\"", + "account/info": "פרטי חשבון", + "account/following": "משתמשים ש%1 עוקב אחריהם", + "account/followers": "משתמשים שעוקבים אחרי %1", + "account/posts": "הודעות שפורסמו על ידי %1", + "account/latest-posts": "פוסטים אחרונים שנוצרו על ידי %1", + "account/topics": "נושאים שנוצרו על ידי %1", + "account/groups": "הקבוצות של %1", + "account/watched_categories": "הקטגוריות ש-%1 עוקב אחריהם", + "account/bookmarks": "הפוסטים המועדפים של %1", + "account/settings": "הגדרות משתמש", + "account/watched": "נושאים שנצפו על ידי %1", + "account/ignored": "נושאים ש%1 התעלמו מהם", + "account/upvoted": "פוסטים שהוצבעו לטובה על ידי %1", + "account/downvoted": "פוסטים שהוצבעו לרעה על ידי %1", + "account/best": "הפוסטים הטובים ביותר שנוצרו על ידי %1", + "account/controversial": "פוסטים השנויים במחלוקת שנוצרו על ידי %1", + "account/blocks": "המשתמשים ש-%1 חסם", + "account/uploads": "העלאות של %1", + "account/sessions": "סשני התחברות", + "confirm": "כתובת המייל אושרה", + "maintenance.text": "%1 כרגע תחת עבודות תחזוקה. אנא חזרו במועד מאוחר יותר.", + "maintenance.messageIntro": "בנוסף, המנהלים השאירו הודעה זו:", + "throttled.text": "%1 לא זמין כעת עקב טעינת יתר. אנא חזרו במועד מאוחר יותר." +} \ No newline at end of file diff --git a/public/language/he/post-queue.json b/public/language/he/post-queue.json new file mode 100644 index 0000000000..6246fe3244 --- /dev/null +++ b/public/language/he/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "פוסטים ממתינים", + "description": "אין פוסטים בתור.
כדי לאפשר את תור ההרשמה, גשו להגדרות → פוסט → תור פוסטים ואפשרו את תור פוסט.", + "user": "משתמש", + "category": "קטגוריה", + "title": "כותרת", + "content": "תוכן", + "posted": "נשלח", + "reply-to": "תגובה ל %1", + "content-editable": "לחץ על התוכן כדי לערוך", + "category-editable": "לחץ על הקטגוריה כדי לערוך", + "title-editable": "לחץ על הכותרת כדי לערוך", + "reply": "תגובה", + "topic": "נושא", + "accept": "אשר", + "reject": "דחה", + "remove": "הסרה", + "notify": "שלח הודעה", + "notify-user": "שלח התראה למשתמש", + "confirm-reject": "האם אתה בטוח שברצונך לדחות את הפוסט הזה?", + "bulk-actions": "פעולות כמותיות", + "accept-all": "אשר הכל", + "accept-selected": "אשר פוסטים נבחרים", + "reject-all": "דחה הכל", + "reject-all-confirm": "האם אתה בטוח שברצונך לדחות את כל הפוסטים?", + "reject-selected": "דחה פוסטים נבחרים", + "reject-selected-confirm": "האם אתה בטוח שברצונך לדחות את %1 הפוסטים שנבחרו?", + "bulk-accept-success": "%1 פוסטים אושרו", + "bulk-reject-success": "%1 פוסטים נדחו" +} \ No newline at end of file diff --git a/public/language/he/recent.json b/public/language/he/recent.json new file mode 100644 index 0000000000..913388686a --- /dev/null +++ b/public/language/he/recent.json @@ -0,0 +1,19 @@ +{ + "title": "אחרונים", + "day": "יום", + "week": "שבוע", + "month": "חודש", + "year": "שנה", + "alltime": "כל הזמן", + "no_recent_topics": "אין נושאים חדשים", + "no_popular_topics": "אין נושאים פופולריים.", + "there-is-a-new-topic": "יש נושא חדש.", + "there-is-a-new-topic-and-a-new-post": "יש נושא ופוסט חדש.", + "there-is-a-new-topic-and-new-posts": "יש נושא ו%1 פוסטים חדשים.", + "there-are-new-topics": "יש %1 נושאים חדשים.", + "there-are-new-topics-and-a-new-post": "יש %1 נושאים ופוסט חדש.", + "there-are-new-topics-and-new-posts": "יש %1 נושאים ו %2 פוסטים חדשים.", + "there-is-a-new-post": "יש פוסט חדש.", + "there-are-new-posts": "יש %1 פוסטים חדשים.", + "click-here-to-reload": "לחץ כאן על מנת לטעון מחדש." +} \ No newline at end of file diff --git a/public/language/he/register.json b/public/language/he/register.json new file mode 100644 index 0000000000..c1ee9e3bf3 --- /dev/null +++ b/public/language/he/register.json @@ -0,0 +1,32 @@ +{ + "register": "הרשמה", + "cancel_registration": "בטל רישום", + "help.email": "כברירת מחדל, כתובת האימייל שלך לא גלויה למשתמשים אחרים", + "help.username_restrictions": "שם משתמש ייחודי בין %1 ל %2 תווים. משתמשים אחרים יכולים לציין את שמך באמצעות @שם המשתמש שלך.", + "help.minimum_password_length": "הסיסמה שלך חייבת להיות לפחות באורך של %1 תווים.", + "email_address": "כתובת אימייל", + "email_address_placeholder": "הכנס כתובת אימייל", + "username": "שם משתמש", + "username_placeholder": "הכנס שם משתמש", + "password": "סיסמה", + "password_placeholder": "הכנס סיסמה", + "confirm_password": "אמת סיסמה", + "confirm_password_placeholder": "אמת סיסמה", + "register_now_button": "הרשם עכשיו", + "alternative_registration": "הרשם באמצעות...", + "terms_of_use": "תנאי שימוש", + "agree_to_terms_of_use": "אני מסכים לתנאי השימוש", + "terms_of_use_error": "אתה מוכרח להסכים לתנאי השימוש", + "registration-added-to-queue": "בקשתך להרשמה נשלחה. במידה ובקשתך יאושר, ישלח אישור לכתובת האימייל שהכנסת.", + "registration-queue-average-time": "הזמן הממוצע לאישור משתמשים הוא %1 שעות ו-%2 דקות.", + "registration-queue-auto-approve-time": "חשבונך יאושר תוך %1 שעות.", + "interstitial.intro": "אנו מבקשים עוד מידע כדי לעדכן את חשבונך…", + "interstitial.intro-new": "אנו מבקשים עוד מידע לפני שנוכל ליצור את חשבונך…", + "interstitial.errors-found": "אנא בדוק את המידע שהוזן:", + "gdpr_agree_data": "אני מסכים שפורום זה יאגור ויעבד את נתוני האישיים", + "gdpr_agree_email": "אני מסכים לקבל מדי פעם מיילים מפורום זה עם סיכום נושאים מעניינים שפורסמו", + "gdpr_consent_denied": "אין אפשרות להירשם ללא אישור הסכמה על תנאים אלו.", + "invite.error-admin-only": "רישום משתמשים ישיר הושבת. אנא פנה למנהל לקבלת פרטים נוספים.", + "invite.error-invite-only": "רישום משתמשים ישיר הושבת. עליך להיות מוזמן על ידי משתמש קיים על מנת לגשת לפורום זה.", + "invite.error-invalid-data": "נתוני הרישום שהתקבלו אינם תואמים את הרשומות שלנו. אנא פנה למנהל לקבלת פרטים נוספים." +} \ No newline at end of file diff --git a/public/language/he/reset_password.json b/public/language/he/reset_password.json new file mode 100644 index 0000000000..38f8fa4b3e --- /dev/null +++ b/public/language/he/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "אפס סיסמה", + "update_password": "עדכן סיסמה", + "password_changed.title": "סיסמתך שונתה", + "password_changed.message": "

סיסמתך שונתה בהצלחה, אנא התחבר שוב.", + "wrong_reset_code.title": "קוד איפוס שגוי", + "wrong_reset_code.message": "קוד האיפוס שקבלנו שגוי. אנא נסה שוב, או בקש קוד איפוס חדש.", + "new_password": "סיסמה חדשה", + "repeat_password": "אמת סיסמה", + "changing_password": "משנה סיסמה", + "enter_email": "אנא הקלד את כתובת האימייל שלך ואנו נשלח לך הוראות כיצד לאפס את חשבונך", + "enter_email_address": "הכנס כתובת אימייל", + "password_reset_sent": "אם כתובת המייל משוייכת לחשבון קיים, לכתובת המוגדרת נשלח מייל לשחזור חשבון. שים לב שרק מייל שחזור אחד ישלח כל דקה.", + "invalid_email": "מייל שגוי / כתובת מייל לא נמצאה", + "password_too_short": "הסיסמה שבחרת קצרה מדי, אנא בחר סיסמה שונה.", + "passwords_do_not_match": "הסיסמאות שהזנת אינן תואמות.", + "password_expired": "הסיסמא שבחרת פגת תוקף, אנא בחר סיסמא חדשה." +} \ No newline at end of file diff --git a/public/language/he/search.json b/public/language/he/search.json new file mode 100644 index 0000000000..fcad1a9ea0 --- /dev/null +++ b/public/language/he/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "נמצאו %1 תוצאות עבור החיפוש \"%2\", (%3 שניות)", + "no-matches": "לא נמצאו תוצאות", + "advanced-search": "חיפוש מתקדם", + "in": "ב", + "titles": "כותרות", + "titles-posts": "כותרות ופוסטים", + "match-words": "התאם מילים", + "all": "הכל", + "any": "כל", + "posted-by": "פורסם על-ידי", + "in-categories": "בקטגוריות", + "search-child-categories": "חפש בתת קטגוריות", + "has-tags": "עם תגיות", + "reply-count": "כמות תגובות", + "at-least": "לפחות", + "at-most": "לכל היותר", + "relevance": "רלוונטיות", + "post-time": "זמן הפוסט", + "votes": "הצבעות", + "newer-than": "חדש מ", + "older-than": "ישן מ", + "any-date": "כל תאריך", + "yesterday": "אתמול", + "one-week": "שבוע אחד", + "two-weeks": "שבועיים", + "one-month": "חודש אחד", + "three-months": "שלושה חודשים", + "six-months": "שישה חודשים", + "one-year": "שנה אחת", + "sort-by": "סדר על-פי", + "last-reply-time": "תאריך תגובה אחרון", + "topic-title": "כותרת הנושא", + "topic-votes": "הצבעות בנושא", + "number-of-replies": "מספר התגובות", + "number-of-views": "מספר הצפיות", + "topic-start-date": "זמן תחילת הנושא", + "username": "שם משתמש", + "category": "קטגוריה", + "descending": "בסדר יורד", + "ascending": "בסדר עולה", + "save-preferences": "שמור העדפות", + "clear-preferences": "נקה העדפות", + "search-preferences-saved": "חפש העדפות שנשמרו", + "search-preferences-cleared": "חפש העדפות שנוקו", + "show-results-as": "צפה בתוצאות בתור", + "see-more-results": "צפה בתוצאות נוספות (%1)", + "search-in-category": "חפש ב-\"%1\"" +} \ No newline at end of file diff --git a/public/language/he/success.json b/public/language/he/success.json new file mode 100644 index 0000000000..325b80afd6 --- /dev/null +++ b/public/language/he/success.json @@ -0,0 +1,7 @@ +{ + "success": "הצלחה", + "topic-post": "שלחת את הפוסט בהצלחה.", + "post-queued": "הפוסט שלך נשלח. תקבל התראה הוא יתקבל או ידחה.", + "authentication-successful": "הנתונים אומתו בהצלחה", + "settings-saved": "הנתונים נשמרו!" +} \ No newline at end of file diff --git a/public/language/he/tags.json b/public/language/he/tags.json new file mode 100644 index 0000000000..6604da7815 --- /dev/null +++ b/public/language/he/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "אין פוסטים עם תגית זו.", + "tags": "תגיות", + "enter_tags_here": "הכנס תגים כאן, כאשר כל אחד בין %1 ל%2 תווים.", + "enter_tags_here_short": "הכנס תגיות", + "no_tags": "אין עדיין תגיות.", + "select_tags": "בחר תגיות" +} \ No newline at end of file diff --git a/public/language/he/top.json b/public/language/he/top.json new file mode 100644 index 0000000000..c942bb51a7 --- /dev/null +++ b/public/language/he/top.json @@ -0,0 +1,4 @@ +{ + "title": "הכי פופולארי", + "no_top_topics": "אין כותרות פופולאריות" +} \ No newline at end of file diff --git a/public/language/he/topic.json b/public/language/he/topic.json new file mode 100644 index 0000000000..f36bf4277e --- /dev/null +++ b/public/language/he/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "נושא", + "title": "כותרת", + "no_topics_found": "לא נמצאו נושאים!", + "no_posts_found": "לא נמצאו פוסטים!", + "post_is_deleted": "פוסט זה נמחק!", + "topic_is_deleted": "נושא זה נמחק!", + "profile": "פרופיל", + "posted_by": "פורסם על ידי %1", + "posted_by_guest": "פורסם על ידי אורח", + "chat": "צ'אט", + "notify_me": "קבלת התראה כאשר יש תגובות חדשות בנושא זה", + "quote": "ציטוט", + "reply": "תגובה", + "replies_to_this_post": "%1 תגובות", + "one_reply_to_this_post": "תגובה 1", + "last_reply_time": "תגובה אחרונה", + "reply-as-topic": "הגיבו כנושא", + "guest-login-reply": "התחברו בכדי לפרסם תגובה", + "login-to-view": "🔒 התחברו בכדי לצפות", + "edit": "עריכה", + "delete": "מחיקה", + "delete-event": "מחיקת ארוע", + "delete-event-confirm": "האם למחוק אירוע זה?", + "purge": "מחיקה לצמיתות", + "restore": "שחזור", + "move": "העברה", + "change-owner": "שינוי שם בעל הפוסט", + "fork": "פיצול", + "link": "קישור", + "share": "שיתוף", + "tools": "כלים", + "locked": "נעול", + "pinned": "נעוץ", + "pinned-with-expiry": "נעוץ עד %1", + "scheduled": "מתוזמן", + "moved": "הועבר", + "moved-from": "הועבר מ-%1", + "copy-ip": "העתקת IP", + "ban-ip": "הרחקת IP", + "view-history": "עריכת היסטוריה", + "locked-by": "ננעל על ידי", + "unlocked-by": "נעילה הוסרה על ידי", + "pinned-by": "ננעץ על ידי", + "unpinned-by": "נעיצה הוסרה על ידי", + "deleted-by": "נמחק על ידי", + "restored-by": "שוחזר על ידי", + "moved-from-by": "הועבר מ %1 ע\"י", + "queued-by": "הפוסט ממתין לאישור →", + "backlink": "הוזכר על-ידי", + "forked-by": "פוצל על-ידי", + "bookmark_instructions": "לחצו כאן בכדי לחזור לפוסט האחרון שקראתם בנושא זה.", + "flag-post": "דווחו על פוסט זה", + "flag-user": "דווחו על משתמש זה", + "already-flagged": "דווח כבר", + "view-flag-report": "הצגת דוח דיווחים", + "resolve-flag": "השלמת דיווח", + "merged_message": "נושא זה מוזג בתוך %2", + "deleted_message": "נושא זה נמחק. רק משתמשים עם הרשאות מתאימות יוכלו לצפות בו.", + "following_topic.message": "תקבלו התראות כאשר יפורסם פוסט חדש בנושא זה.", + "not_following_topic.message": "נושא זה יופיע ברשימת הנושאים שלא נקראו, אולם לא תקבלו התראה כשיפורסם פוסט בנושא זה.", + "ignoring_topic.message": "נושא זה לא יופיע יותר ברשימת הנושאים שלא נקראו. תקבלו הודעה כשיזכירו אתכם או כשהפוסט שלכם יקבל הצבעה חיובית", + "login_to_subscribe": "הירשמו או התחברו בכדי לעקוב אחר נושא זה.", + "markAsUnreadForAll.success": "נושא זה סומן כלא נקרא לכולם.", + "mark_unread": "סימון כלא נקרא", + "mark_unread.success": "הנושא סומן כלא נקרא.", + "watch": "מעקב", + "unwatch": "הפסקת מעקב", + "watch.title": "קבלת התראה כאשר יש תגובות חדשות בנושא זה", + "unwatch.title": "הפסקת מעקב אחר נושא זה", + "share_this_post": "שתפו פוסט זה", + "watching": "במעקב", + "not-watching": "לא במעקב", + "ignoring": "התעלמות", + "watching.description": "עדכנו אותי על תגובות חדשות.
הצגת נושא ברשימת לא נקראו.", + "not-watching.description": "אל תעדכנו אותי על תגובות חדשות.
הצגת נושא ברשימת לא נקראו במידה ובחרתי לא להתעלם מקטגוריה זו", + "ignoring.description": "אל תעדכנו אותי על תגובות חדשות.
אל תציגו את הנושא ברשימת לא נקראו ", + "thread_tools.title": "כלי נושא", + "thread_tools.markAsUnreadForAll": "סימון לכולם כלא נקרא", + "thread_tools.pin": "נעיצת נושא", + "thread_tools.unpin": "הסרת נעיצה", + "thread_tools.lock": "נעילת נושא", + "thread_tools.unlock": "הסרת נעילה", + "thread_tools.move": "הזזת נושא", + "thread_tools.move-posts": "הזזת פוסטים", + "thread_tools.move_all": "הזזת הכל", + "thread_tools.change_owner": "שינוי שם כותב הפוסט", + "thread_tools.select_category": "בחירת קטגוריה", + "thread_tools.fork": "פיצול נושא", + "thread_tools.delete": "מחיקת נושא", + "thread_tools.delete-posts": "מחיקת פוסטים", + "thread_tools.delete_confirm": "האם למחוק נושא זה?", + "thread_tools.restore": "שיחזור נושא", + "thread_tools.restore_confirm": "האם לשחזר נושא זה?", + "thread_tools.purge": "מחיקת נושא לצמיתות", + "thread_tools.purge_confirm": "האם למחוק לצמיתות נושא זה?", + "thread_tools.merge_topics": "מיזוג נושאים", + "thread_tools.merge": "מיזוג", + "topic_move_success": "נושא זה יועבר ל\"%1\". לחצו כאן לביטול.", + "topic_move_multiple_success": "נושאים אלו יועברו ל\"%1\" . לחצו כאן לביטול.", + "topic_move_all_success": "כל הנושאים יועברו ל\"%1\". לחצו כאן לביטול.", + "topic_move_undone": "העברת הנושא בוטלה", + "topic_move_posts_success": "הפוסטים יועברו מיד. לחצו כאן לביטול.", + "topic_move_posts_undone": "העברת הפוסט בוטלה", + "post_delete_confirm": "האם למחוק פוסט זה?", + "post_restore_confirm": "האם לשחזר פוסט זה?", + "post_purge_confirm": "האם למחוק לצמיתות פוסט זה?", + "pin-modal-expiry": "תאריך תפוגה", + "pin-modal-help": "באפשרותכם להגדיר כאן תאריך תפוגה לנושאים המוצמדים. לחלופין, ביכולתכם להשאיר שדה זו ריקה, כדי שהנושא יישאר נעוץ עד לביטול ההצמדה ידנית.", + "load_categories": "טוען קטגוריות", + "confirm_move": "העברה", + "confirm_fork": "פיצול", + "bookmark": "הוספה למועדפים", + "bookmarks": "מועדפים", + "bookmarks.has_no_bookmarks": "לא צירפתם פוסט למועדפים עדיין", + "copy-permalink": "העתקת קישור פוסט", + "loading_more_posts": "טוען פוסטים נוספים", + "move_topic": "העברת נושא", + "move_topics": "העברת נושאים", + "move_post": "העברת פוסט", + "post_moved": "הפוסט הועבר!", + "fork_topic": "פיצול נושא", + "enter-new-topic-title": "הכניסו כותרת נושא חדשה", + "fork_topic_instruction": "לחצו על הפוסטים שברצונכם לפצל", + "fork_no_pids": "לא נבחרו פוסטים!", + "no-posts-selected": "לא נבחרו פוסטים!", + "x-posts-selected": "%1 פוסטים נבחרו", + "x-posts-will-be-moved-to-y": "%1 פוסטים יועברו ל-\"%2\"", + "fork_pid_count": "%1 פוסטים נבחרו", + "fork_success": "הפוסט פוצל בהצלחה! לחצו כאן על מנת לעבור לפוסט המפוצל.", + "delete_posts_instruction": "לחצו על הפוסטים שברצונכם למחוק", + "merge_topics_instruction": "לחצו על הנושאים שברצונכם למזג או חפשו אותם", + "merge-topic-list-title": "רשימת הנושאים למיזוג", + "merge-options": "אפשרויות מיזוג", + "merge-select-main-topic": "בחרו את הנושא הראשי", + "merge-new-title-for-topic": "כותרת חדשה לנושא", + "topic-id": "מזהה נושא", + "move_posts_instruction": "לחצו על הפוסטים שברצונכם להסיר ואז הכניסו מזהה נושא או עברו לנושא היעד", + "change_owner_instruction": "לחצו על הפוסטים בהם תרצו לשנות את שם כותב ההודעה", + "composer.title_placeholder": "הכניסו את כותרת הנושא כאן...", + "composer.handle_placeholder": "הזינו את שמכם / כינוי שלכם כאן", + "composer.discard": "ביטול", + "composer.submit": "שליחה", + "composer.additional-options": "אפשרויות נוספות", + "composer.schedule": "תיזמון", + "composer.replying_to": "תגובה ל%1", + "composer.new_topic": "נושא חדש", + "composer.editing": "עורך", + "composer.uploading": "מעלה...", + "composer.thumb_url_label": "הדביקו את כתובת ה-URL לתמונה מוקטנת עבור הנושא", + "composer.thumb_title": "הוסיפו תמונה מוקטנת לנושא זה", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "או העלו קובץ", + "composer.thumb_remove": "ניקוי שדות", + "composer.drag_and_drop_images": "גררו תמונות לכאן", + "more_users_and_guests": "%1 משתמשים נוספים ו-%2 אורחים", + "more_users": "%1 משתמשים נוספים", + "more_guests": "%1 אורחים נוספים", + "users_and_others": "%1 ו-%2 אחרים", + "sort_by": "סדרו על-פי", + "oldest_to_newest": "מהישן לחדש", + "newest_to_oldest": "מהחדש לישן", + "most_votes": "הכי הרבה הצבעות", + "most_posts": "הכי הרבה פוסטים", + "most_views": "הכי הרבה צפיות", + "stale.title": "ליצור נושא חדש במקום זאת?", + "stale.warning": "הנושא בו אתם מגיבים הוא די ישן. האם ברצונכם לפתוח נושא חדש, ולהזכיר את נושא זה בתגובתכם?", + "stale.create": "יצירת נושא חדש", + "stale.reply_anyway": "הגיבו לנושא זה בכל מקרה", + "link_back": "תגובה: [%1](%2)", + "diffs.title": "היסטוריית עריכת הפוסט", + "diffs.description": "להודעה זו יש %1 עריכות. לחצו על אחת מהעריכות להלן כדי לראות את תוכן ההודעה בנקודת זמן זו.", + "diffs.no-revisions-description": "לפוסט זה יש %1גרסאות", + "diffs.current-revision": "גרסה נוכחית", + "diffs.original-revision": "גרסה מקורית", + "diffs.restore": "שחזרו לגרסה זו", + "diffs.restore-description": "יתווסף גרסה חדשה להיסטוריית העריכה אחרי השיחזור", + "diffs.post-restored": "הפוסט שוחזר בהצלחה לגרסה קודמת", + "diffs.delete": "מחיקת גרסה זו", + "diffs.deleted": "גרסה זו נמחקה", + "timeago_later": "אחרי %1", + "timeago_earlier": "לפני %1 ", + "first-post": "פוסט ראשון", + "last-post": "פוסט אחרון", + "go-to-my-next-post": "מעבר לפוסט הבא שלי", + "no-more-next-post": "אין לכם יותר פוסטים בנושא זה", + "post-quick-reply": "שליחת תגובה מהירה" +} \ No newline at end of file diff --git a/public/language/he/unread.json b/public/language/he/unread.json new file mode 100644 index 0000000000..4735920e80 --- /dev/null +++ b/public/language/he/unread.json @@ -0,0 +1,15 @@ +{ + "title": "לא נקרא", + "no_unread_topics": "אין נושאים שלא נקראו", + "load_more": "טען עוד", + "mark_as_read": "סמן כנקרא", + "selected": "נבחר", + "all": "הכל", + "all_categories": "כל הקטגוריות", + "topics_marked_as_read.success": "הנושאים שבחרת סומנו כנקרא!", + "all-topics": "כל הנושאים", + "new-topics": "נושאים חדשים", + "watched-topics": "נושאים שאתה עוקב אחריהם", + "unreplied-topics": "נושאים ללא תגובות", + "multiple-categories-selected": "בחירות מרובות" +} \ No newline at end of file diff --git a/public/language/he/uploads.json b/public/language/he/uploads.json new file mode 100644 index 0000000000..57c63805f4 --- /dev/null +++ b/public/language/he/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "מעלה את הקובץ...", + "select-file-to-upload": "בחר קובץ להעלאה!", + "upload-success": "הקובץ הועלה בהצלחה!", + "maximum-file-size": "מקסימום %1 קילובייט", + "no-uploads-found": "לא נמצאו העלאות!", + "public-uploads-info": "העלאות הינם ציבוריות. כל הגולשים יוכלו לראותם.", + "private-uploads-info": "העלאות הינם פרטיות. רק משתמשים מחוברים יוכלו לראותם." +} \ No newline at end of file diff --git a/public/language/he/user.json b/public/language/he/user.json new file mode 100644 index 0000000000..738821cd16 --- /dev/null +++ b/public/language/he/user.json @@ -0,0 +1,199 @@ +{ + "banned": "מורחק", + "muted": "מושתק", + "offline": "לא מחובר", + "deleted": "נמחק", + "username": "שם משתמש", + "joindate": "תאריך הצטרפות", + "postcount": "כמות פוסטים", + "email": "כתובת אימייל", + "confirm_email": "אשר מייל", + "account_info": "פרטי חשבון", + "admin_actions_label": "פעולות ניהול", + "ban_account": "הרחק חשבון", + "ban_account_confirm": "האם אתה בטוח שאתה רוצה להרחיק משתמש זה?", + "unban_account": "בטל את הרחקת החשבון", + "mute_account": "השתק חשבון", + "unmute_account": "בטל השתקת חשבון", + "delete_account": "מחק חשבון", + "delete_account_as_admin": "מחק חשבון", + "delete_content": "מחק תוכן חשבון", + "delete_all": "מחק חשבון ותוכן", + "delete_account_confirm": "האם אתה בטוח שברצונך להפוך את הפוסטים שלך לאנונימיים ולמחוק את החשבון שלך?
פעולה זו היא בלתי הפיכה ולא תוכל לשחזר את הנתונים שלך

הזן את הסיסמה שלך על מנת לאשר שברצונך להשמיד חשבון זה.", + "delete_this_account_confirm": "האם אתה בטוח שברצונך למחוק חשבון זה תוך השארת התוכן שלו?
פעולה זו היא בלתי הפיכה, הפוסטים יהפכו לאנונימיים, ולא תוכל לשחזר שיוכי הפוסטים עם החשבון שנמחק

", + "delete_account_content_confirm": "האם אתה בטוח שברצונך למחוק את התוכן של חשבון זה (פוסטים/נושאים/העלאות)?
פעולה זו היא בלתי הפיכה ולא תוכל לשחזר שום נתונים

", + "delete_all_confirm": "האם אתה בטוח שברצונך למחוק חשבון זה ואת כל התוכן שלו (פוסטים/נושאים/העלאות)?
פעולה זו היא בלתי הפיכה ולא תוכל לשחזר שום נתונים

", + "account-deleted": "החשבון נמחק", + "account-content-deleted": "תוכן החשבון נמחק", + "fullname": "שם מלא", + "website": "אתר", + "location": "מיקום", + "age": "גיל", + "joined": "הצטרף ב-", + "lastonline": "התחבר לאחרונה", + "profile": "פרופיל", + "profile_views": "צפיות בפרופיל", + "reputation": "מוניטין", + "bookmarks": "מועדפים", + "watched_categories": "קטגוריות במעקב", + "change_all": "שנה הכל", + "watched": "נצפה", + "ignored": "התעלם", + "default-category-watch-state": "מצב מעקב על קטגוריה בברירת מחדל", + "followers": "עוקבים", + "following": "עוקב אחרי", + "blocks": "חסימות", + "block_toggle": " חסום/בטל חסימה", + "block_user": "חסום משתמש", + "unblock_user": "בטל חסימת משתמש", + "aboutme": "אודותי", + "signature": "חתימה", + "birthday": "יום הולדת", + "chat": "צ'אט", + "chat_with": "המשך צ'אט עם %1", + "new_chat_with": "התחל צ'אט עם %1", + "flag-profile": "דווח על משתמש", + "follow": "עקוב", + "unfollow": "הפסק לעקוב", + "more": "עוד", + "profile_update_success": "הפרופיל עודכן בהצלחה!", + "change_picture": "שנה תמונה", + "change_username": "שנה שם משתמש", + "change_email": "שנה מייל", + "email_same_as_password": "הכנס את הסיסמא הנוכחית שלך על מנת להמשיך – כתבת את כתובת המייל החדשה במקום.", + "edit": "ערוך", + "edit-profile": "ערוך פרופיל", + "default_picture": "אייקון ברירת מחדל", + "uploaded_picture": "התמונה הועלתה", + "upload_new_picture": "העלה תמונה חדשה", + "upload_new_picture_from_url": "העלה תמונה חדשה מ-URL", + "current_password": "סיסמה נוכחית", + "change_password": "שנה סיסמה", + "change_password_error": "סיסמה לא תקינה!", + "change_password_error_wrong_current": "סיסמתך הנוכחית אינה נכונה!", + "change_password_error_match": "הסיסמאות לא תואמות!", + "change_password_error_privileges": "אין לך את ההרשאות המתאימות לשנות סיסמה זו.", + "change_password_success": "הסיסמה שלך עודכנה!", + "confirm_password": "אימות סיסמה", + "password": "סיסמה", + "username_taken_workaround": "שם המשתמש שבחרת כבר תפוס, ולכן שינינו אותו מעט. שם המשתמש שלך כעת הוא: %1", + "password_same_as_username": "הסיסמה שלך זהה לשם המשתמש, בחר סיסמה שונה.", + "password_same_as_email": "הסיסמה שלך זהה לכתובת המייל שלך, בחר סיסמה שונה.", + "weak_password": "סיסמה חלשה.", + "upload_picture": "העלה תמונה", + "upload_a_picture": "העלה תמונה", + "remove_uploaded_picture": "מחק את התמונה שהועלתה", + "upload_cover_picture": "העלה תמונת נושא", + "remove_cover_picture_confirm": "האם אתה בטוח שאתה רוצה למחוק את תמונת נושא?", + "crop_picture": "חתוך תמונה", + "upload_cropped_picture": "חתוך והעלה", + "avatar-background-colour": "צבע רקע של תמונת נושא", + "settings": "הגדרות", + "show_email": "הצג את כתובת האימייל שלי", + "show_fullname": "הצג את שמי המלא", + "restrict_chats": "אשר הודעות צ'אט ממשתמשים שאני עוקב אחריהם בלבד", + "digest_label": "הרשמה לקבלת תקציר", + "digest_description": "הרשמה לקבלת עדכונים בדואר אלקטרוני מפורום זה (הודעות ונושאים חדשים) בהתאם ללוח זמנים מוגדר מראש", + "digest_off": "כבוי", + "digest_daily": "יומי", + "digest_weekly": "שבועי", + "digest_biweekly": "דו שבועי", + "digest_monthly": "חודשי", + "has_no_follower": "למשתמש זה אין עוקבים :(", + "follows_no_one": "משתמש זה אינו עוקב אחרי אחרים", + "has_no_posts": "משתמש זה טרם יצר פוסטים כלשהם.", + "has_no_best_posts": "למשתמש זה אין עדיין פוסטים עם הצבעה בעד.", + "has_no_topics": "המשתמש טרם יצר נושאים כלשהם.", + "has_no_watched_topics": "המשתמש טרם צפה בנושאים כלשהם.", + "has_no_ignored_topics": "המשתמש הזה טרם התעלם מנושאים.", + "has_no_upvoted_posts": "המשתמש טרם הצביע בעד פוסטים כלשהם.", + "has_no_downvoted_posts": "המשתמש טרם הצביע נגד פוסטים כלשהם.", + "has_no_controversial_posts": "למשתמש זה אין עדיין פוסטים עם הצבעה למטה.", + "has_no_blocks": "לא חסמתם אף משתמש.", + "email_hidden": "כתובת אימייל מוסתרת", + "hidden": "מוסתר", + "paginate_description": "הצגת נושאים ופוסטים בעמודים במקום כרשימת גלילה אין-סופית", + "topics_per_page": "כמות נושאים בעמוד", + "posts_per_page": "כמות פוסטים בעמוד", + "max_items_per_page": "מקסימום %1", + "acp_language": "שפת עמוד הניהול", + "notifications": "התראות", + "upvote-notif-freq": "תדירות התראת הצבעה חיובית", + "upvote-notif-freq.all": "כל ההצבעות החיוביות", + "upvote-notif-freq.first": "הראשון בפוסט", + "upvote-notif-freq.everyTen": "כל 10 הצבעות חיוביות", + "upvote-notif-freq.threshold": "ב-1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "ב-10, 100, 1000...", + "upvote-notif-freq.disabled": "מושבת", + "browsing": "הגדרות ניווט", + "open_links_in_new_tab": "פתח קישורים חיצוניים בכרטיסייה חדשה", + "enable_topic_searching": "הפעל חיפוש בתוך נושא", + "topic_search_help": "החיפוש בתוך הנושא יעקוף את שיטת החיפוש של הדפדפן, ויאפשר לך לחפש בכל הנושא - ולא רק במה שמוצג על המסך, עם זאת בלחיצה נוספת על Ctrl+5 ייפתח לך החיפוש הרגיל של הדפדפן", + "update_url_with_post_index": "עדכון כתובת ה-URL עם אינדקס הפוסט בעת גלישה בנושאים", + "scroll_to_my_post": "הצג את הפוסט לאחר פרסום התגובה", + "follow_topics_you_reply_to": "עקוב אחר נושאים שהגבת עליהם", + "follow_topics_you_create": "עקוב אחר נושאים שייצרת", + "grouptitle": "כותרת הקבוצה", + "group-order-help": "בחר קבוצה והשתמש בחצים על מנת לארגן כותרות", + "no-group-title": "ללא כותרת לקבוצה", + "select-skin": "בחר מראה", + "select-homepage": "בחר דף בית", + "homepage": "דף הבית", + "homepage_description": "בחר דף שיוגדר כדף הבית של הפורום או בחר ב\"כלום\" על מנת להשתמש בדף הבית הברירת מחדל.", + "custom_route": "נתיב דף הבית המותאם-אישית", + "custom_route_help": "הזן שם נתיב כאן ללא קו נטוי לפני (לדוגמה \"recent\" או \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "משוייך עם", + "sso.not-associated": "לחץ כאן כדי לשייך", + "sso.dissociate": "ביטול שיוך", + "sso.dissociate-confirm-title": "אשר ביטול שיוך", + "sso.dissociate-confirm": "האם אתה בטוח שאתה רוצה לבטל שיוך חשבונך מ%1?", + "info.latest-flags": "דיווחים אחרונים", + "info.no-flags": "לא נמצאו פוסטים מדווחים", + "info.ban-history": "היסטוריית הרחקות", + "info.no-ban-history": "משתמש זה לא הורחק מעולם", + "info.banned-until": "הורחק עד %1", + "info.banned-expiry": "פג תוקף", + "info.banned-permanently": "הורחק לצמיתות", + "info.banned-reason-label": "סיבה", + "info.banned-no-reason": "לא ניתנה סיבה.", + "info.mute-history": "הסטוריית השתקות", + "info.no-mute-history": "משתמש זה מעולם לא הושתק", + "info.muted-until": "מושתק עד %1", + "info.muted-expiry": "תפוגה", + "info.muted-no-reason": "לא סופקה סיבה.", + "info.username-history": "היסטוריית שם משתמש", + "info.email-history": "היסטוריית אימייל", + "info.moderation-note": "הערת מודרטור", + "info.moderation-note.success": "הערת מודרטור נשמרה", + "info.moderation-note.add": "הוסף הערה", + "sessions.description": "דף זה מאפשר לך לראות את כל הסשנים הפעילים בפורום זה ולבטל אותם במידת הצורך. אתה יכול לבטל את הסשן שלך על ידי התנתקותך מהחשבון.", + "consent.title": "תנאי השימוש באתר", + "consent.lead": "אתר זה אוסף ומעבד נתונים הכוללים בחלקם את המידע האישי שלך.", + "consent.intro": "אנו משתמשים במידע שנאסף כדי להתאים אישית את החוויה שלך, וכן לקשר את ההודעות שאתה מבצע לחשבון המשתמש שלך. במהלך שלב ההרשמה התבקשת לספק שם משתמש וכתובת דוא\"ל, תוכל גם לספק מידע נוסף כדי להשלים את פרופיל המשתמש שלך באתר זה.

אנו שומרים ומעבדים מידע זה. אתה יכול לבטל את הסכמתך בכל עת על ידי מחיקת החשבון שלך. בכל עת תוכל לבקש עותק של חשבונך לאתר זה, באמצעות דף זה.

אם יש לך שאלות או חששות, אנו ממליצים לך ליצור קשר עם צוות הניהול של האתר.", + "consent.email_intro": "אנו עשויים מדי פעם לשלוח הודעות לכתובת הדוא\"ל שלך על מנת לספק לך עדכונים ו/או להודיע ​​לך על פעילות חדשה הרלוונטית עבורך. ניתן להתאים אישית את התדירות של העדכונים (כולל השבתתם), וכן לבחור אילו סוגי הודעות לקבל באמצעות הדוא\"ל דרך דף הגדרות המשתמש שלך.", + "consent.digest_frequency": " אתר זה מספק עדכוני דוא\"ל בכל %1. אם תשבית את האפשרות הזאת בהגדרות המשתמש שלך לא תקבל עדכונים אלו.", + "consent.digest_off": "האתר לא ישלח הודעות תקציר, אלא אם כן תשנה זאת במפורש בהגדרות המשתמש שלך.", + "consent.received": "הסכמתך לאפשר לאתר לאסוף ולעבד את המידע שלך התקבלה. אין צורך בפעולה נוספת.", + "consent.not_received": "לא סיפקת אישור לאיסוף ועיבוד נתונים. בכל עת הנהלת האתר רשאית למחוק את חשבונך בכדי להתאים את עצמה בתקנה הכללית להגנת נתונים.", + "consent.give": "הסכם", + "consent.right_of_access": "זכותך לנגישות", + "consent.right_of_access_description": "שמורה לך הזכות לגשת לנתונים שנאספו על ידי האתר. תוכל לאחזר עותק של נתונים אלה על ידי לחיצה על הלחצן מטה.", + "consent.right_to_rectification": "זכותך לתקן טעויות", + "consent.right_to_rectification_description": "יש לך זכות לשנות או לעדכן נתונים שנאספו. ניתן לעדכן את הפרופיל שלך וכן לערוך כל תוכן שפורסם. במידת הצורך, אנא צור קשר עם צוות ניהול האתר.", + "consent.right_to_erasure": "זכותך למחוק את חשבונך", + "consent.right_to_erasure_description": "בכל עת תוכל לבטל את הסכמתך לאיסוף נתונים ו/או עיבודם על ידי מחיקת חשבונך. מחיקת הפרופיל שלך לא תגרום למחיקת התוכנים שפרסמת. על מנת למחוק את חשבונך ואת התוכן המקושר לו צור קשר עם צוות הניהול של האתר.", + "consent.right_to_data_portability": "זכותך לניוד הנתונים", + "consent.right_to_data_portability_description": "באפרותך לבקש ייצוא של כל הנתונים שנאספו מחשבונך אודותיך. תוכל לעשות זאת על ידי לחיצה על הלחצן המתאים מטה.", + "consent.export_profile": "יצוא פרופיל (json.)", + "consent.export-profile-success": "ייצוא הפרופיל מתבצע כעת. תקבל התראה כאשר הייצוא יסתיים.", + "consent.export_uploads": "יצוא תוכן שהועלה (ZIP.)", + "consent.export-uploads-success": "ייצוא ההעלאות מתבצע כעת. תקבל התראה כאשר הייצוא יסתיים.", + "consent.export_posts": "יצוא פוסטים (CVS.)", + "consent.export-posts-success": "ייצוא הפוסטים מתבצע כעת. תקבל התראה כאשר הייצוא יסתיים.", + "emailUpdate.intro": "אנא הכנס את כתובת הדוא\"ל שלך. הפורום משתמש בדוא\"ל שלך לשליחת תקציר מתוזמן והתראות, כמו כן לשחזור חשבון במקרה ששוכחים את הסיסמה.", + "emailUpdate.optional": "שדה זה הוא אופציונלי. אינך מחויב לספק את כתובת הדוא\"ל שלך, אך ללא דוא\"ל מאומת לא תוכל לשחזר את חשבונך או להתחבר באמצעות הדוא\"ל שלך.", + "emailUpdate.required": "זהו שדה חובה", + "emailUpdate.change-instructions": "מייל אימות יישלח לכתובת דוא\"ל שהכנסת עם קישור ייחודי. לחיצה על הקישור יאמת את בעלותך על הדוא\"ל ותקבל גישה לחשבונך. בכל זמן, תוכל לעדכן את כתובת הדוא\"ל שלך בדף החשבון שלך.", + "emailUpdate.password-challenge": "אנא הזן את הסיסמה שלך כדי לאמת את הבעלות על החשבון." +} \ No newline at end of file diff --git a/public/language/he/users.json b/public/language/he/users.json new file mode 100644 index 0000000000..459dfa1d61 --- /dev/null +++ b/public/language/he/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "משתמשים אחרונים", + "top_posters": "מפרסמים הכי הרבה", + "most_reputation": "המוניטין הגבוה ביותר", + "most_flags": "הכי הרבה דיווחי משתמשים", + "search": "חיפוש", + "enter_username": "הכנס שם משתמש לחיפוש", + "search-user-for-chat": "חפש משתמש כדי להתחיל איתו צאט", + "load_more": "טען עוד", + "users-found-search-took": "%1 משתמשים נמצאו! החיפוש ערך %2 שניות.", + "filter-by": "פלטר על-פי", + "online-only": "אונליין בלבד", + "invite": "הזמן", + "prompt-email": "מיילים:", + "groups-to-join": "קבוצות שתירשם אליהם כאשר ההזמנה תאושר:", + "invitation-email-sent": "מייל הזמנה נשלח ל%1", + "user_list": "רשימת משתמשים", + "recent_topics": "נושאים אחרונים", + "popular_topics": "נושאים פופולריים", + "unread_topics": "נושאים שלא נקראו", + "categories": "קטגוריות", + "tags": "תגיות", + "no-users-found": "לא נמצאו משתמשים!" +} \ No newline at end of file diff --git a/public/language/hr/_DO_NOT_EDIT_FILES_HERE.md b/public/language/hr/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/hr/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/hr/admin/admin.json b/public/language/hr/admin/admin.json new file mode 100644 index 0000000000..9694790eb2 --- /dev/null +++ b/public/language/hr/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Sigurni ste da želite ponovno pokrenuti NodeBB?", + + "acp-title": "%1 | NodeBB Administratorska kontrolna ploča", + "settings-header-contents": "Sadržaj", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/cache.json b/public/language/hr/admin/advanced/cache.json new file mode 100644 index 0000000000..34f7f2575a --- /dev/null +++ b/public/language/hr/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Objava predmemorija", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Puno", + "post-cache-size": "Veličina predmemorije objave", + "items-in-cache": "Artikli u predmemoriji" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/database.json b/public/language/hr/admin/advanced/database.json new file mode 100644 index 0000000000..e146eba570 --- /dev/null +++ b/public/language/hr/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Na mreži u sekundama", + "uptime-days": "Na mreži u danima", + + "mongo": "Mongo", + "mongo.version": "Verzija MongoDB", + "mongo.storage-engine": "Način pohrane", + "mongo.collections": "Kolekcije", + "mongo.objects": "Objekti", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Veličina datoteke", + "mongo.storage-size": "Veličina pohrane", + "mongo.index-size": "Veličina indexa", + "mongo.file-size": "Veličina datoteke", + "mongo.resident-memory": "Rezidentna memorija", + "mongo.virtual-memory": "Virtualna memorija", + "mongo.mapped-memory": "Mapirana memorija", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB sirove informacije", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis verzija", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Spojeni klijenti", + "redis.connected-slaves": "Povezani robovi", + "redis.blocked-clients": "Blokirani klijenti", + "redis.used-memory": "Iskorištena memorija", + "redis.memory-frag-ratio": "Omjer fragmentiranja memorije", + "redis.total-connections-recieved": "Ukupno primljeni veza", + "redis.total-commands-processed": "Ukupne prcesirane komande", + "redis.iops": "Instante operacije po sekundi", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis sirova informacija", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/hr/admin/advanced/errors.json b/public/language/hr/admin/advanced/errors.json new file mode 100644 index 0000000000..04e9e32776 --- /dev/null +++ b/public/language/hr/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figura %1", + "error-events-per-day": "%1 događaja po danu", + "error.404": "404 Nije pronađeno", + "error.503": "503 Usluga nedostupna", + "manage-error-log": "Upravljaj dnevnikom grešaka", + "export-error-log": "Izvedi dnevnik grešaka (CSV)", + "clear-error-log": "Očisti dnevnik grešaka", + "route": "Putanja", + "count": "Zbroj", + "no-routes-not-found": "Huura! Nema 404 grešaka!", + "clear404-confirm": "Sigurni ste da želite očistiti 404 greše iz dnevnika?", + "clear404-success": "\"404 Nije pronađen\" greške očišćene" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/events.json b/public/language/hr/admin/advanced/events.json new file mode 100644 index 0000000000..f648110f4d --- /dev/null +++ b/public/language/hr/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Događanja", + "no-events": "Nema događaja", + "control-panel": "Kontrolna ploča događanja", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/logs.json b/public/language/hr/admin/advanced/logs.json new file mode 100644 index 0000000000..8d87365ce6 --- /dev/null +++ b/public/language/hr/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Dnevnik", + "control-panel": "Dnevnik kontrolne ploče", + "reload": "Učitaj dnevnik ponovno", + "clear": "Očisti dnevnik ", + "clear-success": "Dnevnik čist!" +} \ No newline at end of file diff --git a/public/language/hr/admin/appearance/customise.json b/public/language/hr/admin/appearance/customise.json new file mode 100644 index 0000000000..db241ab70b --- /dev/null +++ b/public/language/hr/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Uobičajno zaglavlje", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Omogući uobičajeno zaglavlje", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/hr/admin/appearance/skins.json b/public/language/hr/admin/appearance/skins.json new file mode 100644 index 0000000000..1b68ae2ecd --- /dev/null +++ b/public/language/hr/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Učitavam Izgled ...", + "homepage": "Naslovnica", + "select-skin": "Odaberi izgled", + "current-skin": "Trenutni izgled", + "skin-updated": "Izgled promijenjen", + "applied-success": "%1 izgled je primjenjen", + "revert-success": "Izgled povraćen na osnovne boje" +} \ No newline at end of file diff --git a/public/language/hr/admin/appearance/themes.json b/public/language/hr/admin/appearance/themes.json new file mode 100644 index 0000000000..5eff189d93 --- /dev/null +++ b/public/language/hr/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Provjeravam instalirane teme ...", + "homepage": "Naslovnica", + "select-theme": "Odaberi temu", + "current-theme": "Trenutna tema", + "no-themes": "Nisu pronađene instalirane teme", + "revert-confirm": "Sigurni ste da želite povratiti zadani NodeBB izgled ?", + "theme-changed": "Tema promijenjena", + "revert-success": "Uspješno ste vratili vaš NodeBB u početno zadanu temu.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/hr/admin/dashboard.json b/public/language/hr/admin/dashboard.json new file mode 100644 index 0000000000..8072389569 --- /dev/null +++ b/public/language/hr/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Promet foruma", + "page-views": "Broj pogleda", + "unique-visitors": "Jedinstveni posjetitelji", + "logins": "Logins", + "new-users": "New Users", + "posts": "Objave", + "topics": "Teme", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "Sve vrijeme", + + "updates": "Nadogradnje", + "running-version": "Ovo je verzija NodeBB v%1.", + "keep-updated": "Uvijek se pobrinite da je Vaš NodeBB na najnovijoj verziji za najnovije sigurnosne mjere i popravke grešaka.", + "up-to-date": "

Vaš NodeBB je na najnovijoj verziji

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

Ovo je pre-release verzija NodeBB. Nenamjerne greške su moguće.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum je u razvojnom stanju. Forum bi mogao biti otvoren za napade; Molimo kontaktirajte vašeg sistemskog administratora", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Obavijest", + "restart-not-required": "Restart nije potreban", + "restart-required": "Potrebno je ponovno pokretanje", + "search-plugin-installed": "Dodatak pretrage instaliran", + "search-plugin-not-installed": "Dodatak pretrage nije instaliran", + "search-plugin-tooltip": "Instalirajte dodatak za pretragu sa stranice za upravljanje dodatcima da aktivirate mogućnost pretrage foruma.", + + "control-panel": "Kontrola sistema", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Održavanje", + "maintenance-mode-title": "Postavite mod za održavanje foruma", + "realtime-chart-updates": "Ažuriranja u stvarnom vremenu", + + "active-users": "Aktivni korisnici", + "active-users.users": "Korisnici", + "active-users.guests": "Gosti", + "active-users.total": "Ukupno", + "active-users.connections": "Veze", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registriran", + + "user-presence": "Korisnik prisutan", + "on-categories": "Na listi kategorija", + "reading-posts": "Čita objave", + "browsing-topics": "Pretražuj teme", + "recent": "Nedavno", + "unread": "Nepročitano", + + "high-presence-topics": "Teme visoke prisutnosti", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Pregled stranica", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Jedninstveni posjetitelji", + "graphs.registered-users": "Registrirani korisnici", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/hr/admin/development/info.json b/public/language/hr/admin/development/info.json new file mode 100644 index 0000000000..0eb118a954 --- /dev/null +++ b/public/language/hr/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "Domaćin", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "Na mreži", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registriran", + "sockets": "Sockets", + "guests": "Gosti", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/hr/admin/development/logger.json b/public/language/hr/admin/development/logger.json new file mode 100644 index 0000000000..e4c9b130f6 --- /dev/null +++ b/public/language/hr/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Postavke dnevnika", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Jednostavno potvrdite ili onemogućite postavke prijave da upalite ili ugasite prijave na brzinu.Ponovno pokretanje nije potrebno.", + "enable-http": "Dozvoli HTTP dnevnik", + "enable-socket": "Omogući socket.io dnevnik događanja ", + "file-path": "Putanja da datoteke dnevnika", + "file-path-placeholder": "/path/to/log/file.log ::: Ostavite prazno kako bi ste se mogli ulogirati u vaš terminal", + + "control-panel": "Kontrolna ploča dnevnika", + "update-settings": "Obnovi postavke dnevnika " +} \ No newline at end of file diff --git a/public/language/hr/admin/extend/plugins.json b/public/language/hr/admin/extend/plugins.json new file mode 100644 index 0000000000..1413bc4b2c --- /dev/null +++ b/public/language/hr/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Instalirano", + "active": "Aktivno", + "inactive": "Neaktivan", + "out-of-date": "Izvan datuma", + "none-found": "Dodatci nisu pronađeni.", + "none-active": "Nema aktivnih dodataka", + "find-plugins": "Pronađi dodatke", + + "plugin-search": "Pretraga dodataka", + "plugin-search-placeholder": "Pretraži za dodatak ...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Promjenite redosljed dodataka", + "order-active": "Posloži aktivne dodatke", + "dev-interested": "Interesira vas pisanje dodataka za NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Određeni dodatci rade idealno kada su pokrenuti prije/poslije drugih dodataka.", + "order.explanation": "Dodatci se učitavaju u slijedu zadanom ovdje,od vrha prema dnu.", + + "plugin-item.themes": "Predlošci", + "plugin-item.deactivate": "Deaktiviraj", + "plugin-item.activate": "Aktiviraj", + "plugin-item.install": "Instaliraj", + "plugin-item.uninstall": "Deinstaliraj", + "plugin-item.settings": "Postavke", + "plugin-item.installed": "Instalirano", + "plugin-item.latest": "Najnovije", + "plugin-item.upgrade": "Nadogradnja", + "plugin-item.more-info": "Za više informacija:", + "plugin-item.unknown": "Nepoznato", + "plugin-item.unknown-explanation": "Stanje ovog dodatka se nemože utvrditi, vjerovatno zbog greške u konfiguraciji.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Dodatak omogućen", + "alert.disabled": "Dodatak onemogućen", + "alert.upgraded": "Dodatak nadograđen", + "alert.installed": "Dodatak instaliran", + "alert.uninstalled": "Dodatak deinstaliran", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Dodatak uspjepno deaktiviran", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Dodatak instaliran, aktivirajte ga.", + "alert.uninstall-success": "Dodatak je uspješno deaktiviran i deinstaliran.", + "alert.suggest-error": "

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

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

NodeBB nemože uspostaviti komunikaciju sa upraviteljem paketa, nadogradnja se ne preporučuje u ovom trenutku.

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/hr/admin/extend/rewards.json b/public/language/hr/admin/extend/rewards.json new file mode 100644 index 0000000000..d8198466a2 --- /dev/null +++ b/public/language/hr/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Nagrade", + "condition-if-users": "Ako korisnici", + "condition-is": "ls:", + "condition-then": "Tada:", + "max-claims": "Koliko puta nagrada može biti osvojena.", + "zero-infinite": "Upišite 0 za beskonačno", + "delete": "Obriši", + "enable": "Omogući", + "disable": "onemogući", + + "alert.delete-success": "Uspješno obrisana nagrada", + "alert.no-inputs-found": "Ilegalna nagrada - nije pronađen unos!", + "alert.save-success": "Uspješno spremljene nagrade" +} \ No newline at end of file diff --git a/public/language/hr/admin/extend/widgets.json b/public/language/hr/admin/extend/widgets.json new file mode 100644 index 0000000000..71d6348bfa --- /dev/null +++ b/public/language/hr/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Dostupni dodatci", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Dostupni kontejneri", + "containers.explanation": "Povucite i ispustite na vrhu bilo kojeg aktivnog widgeta", + "containers.none": "Ništa", + "container.well": "`", + "container.jumbotron": "Jumbotron", + "container.panel": "Ploča", + "container.panel-header": "Ploča zaglavlja", + "container.panel-body": "Tijelo ploče", + "container.alert": "Upozorenje", + + "alert.confirm-delete": "Sigurni ste da želite obrisati ovaj widget?", + "alert.updated": "Widgeti ažurirani", + "alert.update-success": "Uspješno promijenjeni widgeti", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/admins-mods.json b/public/language/hr/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/hr/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/categories.json b/public/language/hr/admin/manage/categories.json new file mode 100644 index 0000000000..6108057152 --- /dev/null +++ b/public/language/hr/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Postavke kategorije", + "privileges": "Privilegije", + + "name": "Ime kategorije", + "description": "Opis kategorije", + "bg-color": "Pozadniska boja", + "text-color": "Boja teksta", + "bg-image-size": "Veličina pozadinske slike", + "custom-class": "Obična klasa", + "num-recent-replies": "# nedavnih objava", + "ext-link": "Vanjska poveznica", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Učitaj sliku", + "delete-image": "Ukloni", + "category-image": "Slika kategorije", + "parent-category": "Roditeljska kategorija", + "optional-parent-category": "(Opcionalno) Roditeljska kategorija", + "top-level": "Top Level", + "parent-category-none": "(Ništa)", + "copy-parent": "Copy Parent", + "copy-settings": "Kopiraj postavke iz ", + "optional-clone-settings": "(Opcionalno) Kloniraj postavke iz kategorije", + "clone-children": "Clone Children Categories And Settings", + "purge": "Odbaci kategoriju", + + "enable": "Omogući", + "disable": "Onemogući", + "edit": "Uredi", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Odabri kategoriju", + "set-parent-category": "Postavi roditeljsku kategoriju ", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Privilegije pogleda", + "privileges.section-posting": "Privilegije objave", + "privileges.section-moderation": "Dozvole moderiranja", + "privileges.section-other": "Other", + "privileges.section-user": "Korisnik", + "privileges.search-user": "Dodaj korisnika", + "privileges.no-users": "U ovoj kategoriji nema privilegije za korisnika.", + "privileges.section-group": "Grupa", + "privileges.group-private": "Ova grupa je privatna", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Dodaj grupu", + "privileges.copy-to-children": "Kopiraj u dijete", + "privileges.copy-from-category": "Kopiraj iz kategorije", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Povratak na listu kategorija", + "analytics.title": "Analitika za \"%1\" kategoriju", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Oblik 2 – Pregledi po danu za ovu kategoriju", + "analytics.topics-daily": "Oblik 3 – Dnevne teme kreirane u ovoj kategoriji", + "analytics.posts-daily": "Oblik 4 – Dnevne objave u ovoj kategoriji", + + "alert.created": "Kreirano", + "alert.create-success": "Kategorija uspješno kreirana!", + "alert.none-active": "Nemate aktivnih kategorija.", + "alert.create": "Napravi kategoriju", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Kategorija odbačena!", + "alert.copy-success": "Postavke kopirane!", + "alert.set-parent-category": "Postavi roditeljsku kategoriju", + "alert.updated": "Promijenjene kategorije", + "alert.updated-success": "ID kategorije %1 uspješno promijenjen", + "alert.upload-image": "Učitaj sliku kategorije", + "alert.find-user": "Pronađi korisnika", + "alert.user-search": "Pretraži korisnika ovdje ...", + "alert.find-group": "Pronađi grupu", + "alert.group-search": "Pretraži grupu ovdje ...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/digest.json b/public/language/hr/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/hr/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/hr/admin/manage/groups.json b/public/language/hr/admin/manage/groups.json new file mode 100644 index 0000000000..91d481eb23 --- /dev/null +++ b/public/language/hr/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Ime grupe", + "badge": "Badge", + "properties": "Properties", + "description": "Opis grupe", + "member-count": "Broj članova", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Uredi", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Pretraga", + "create": "Kreiraj grupu", + "description-placeholder": "Kratki opis grupe", + "create-button": "Napravi", + + "alerts.create-failure": "Uh-Oh

Nastao je problem sa stvaranjem Vaše grupe.Molimo probajte ponovo kasnije!

", + "alerts.confirm-delete": "Sigurni ste da želite obrisati ovu grupu?", + + "edit.name": "Ime", + "edit.description": "Opis", + "edit.user-title": "Naslov članova", + "edit.icon": "Ikona grupe", + "edit.label-color": "Boja oznake grupe", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Prikaži beđ", + "edit.private-details": "Ako je omogućeno, pridruživanje grupi zahtjeva dozvolu vlasnika grupe.", + "edit.private-override": "Upozorenje:Privatne grupe su onemogućene na sistemskoj razini,koje onemogućavaju ovu opciju", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Skriveno", + "edit.hidden-details": "Ako je uključeno,ova grupa neće biti prikazana u listi grupa i korisnici će morati biti pozvani ručno", + "edit.add-user": "Dodaj korisnika u grupu", + "edit.add-user-search": "Pretraži korisnike", + "edit.members": "Lista članova", + "control-panel": "Kontrolna ploča grupa", + "revert": "Povrati", + + "edit.no-users-found": "Korisnik nije pronađen", + "edit.confirm-remove-user": "Sigurni ste da želite ukloniti ovog korisnika?", + "edit.save-success": "Promjene spremljene!" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/privileges.json b/public/language/hr/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/hr/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/registration.json b/public/language/hr/admin/manage/registration.json new file mode 100644 index 0000000000..aba7529be9 --- /dev/null +++ b/public/language/hr/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Na čekanju", + "description": "Nema korisnika na čekanju za registraciju.
Za pokretanje ove mogućnosti odite na Settings → User → User Registration i postavite tip registracije u \"Admin Approval\".", + + "list.name": "Ime", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Vrijeme", + "list.username-spam": "Učestalost %1 Pojavljivanje %2 Samouvjerenost %3", + "list.email-spam": "Učestalost %1 Pojavljivanje %2", + "list.ip-spam": "Učestalost: %1 Pojavljivanje: %2", + + "invitations": "Pozivnice", + "invitations.description": "Ispod je potpuni popis poslanih pozivnica.Koristite ctrl + f za pretragu liste po emailu ili korisničkom imenu.

Korisničko ime će biti prikazano na desno od emaila za korisnike koji su iskoristili svoje pozivnice.", + "invitations.inviter-username": "Korisničko ime pozivatelja", + "invitations.invitee-email": "Email adresa pozivatelja", + "invitations.invitee-username": "Korisničko ime pozivatelja (ako je registriran)", + + "invitations.confirm-delete": "Sigurni ste da želite obrisati ovu pozivnicu?" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/tags.json b/public/language/hr/admin/manage/tags.json new file mode 100644 index 0000000000..37e060f83f --- /dev/null +++ b/public/language/hr/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Vaš forum nema tema sa oznakama", + "bg-color": "Pozadinska boja", + "text-color": "Boja teksta", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Napravi oznaku", + "modify": "Uredi oznake", + "rename": "Rename Tags", + "delete": "Obriši odabrane oznake", + "search": "Pretraži za oznake ...", + "settings": "Tags Settings", + "name": "Ime oznake", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Želite li obrisati odabrane oznake?", + "alerts.update-success": "Oznake promijenjene!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/uploads.json b/public/language/hr/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/hr/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/users.json b/public/language/hr/admin/manage/users.json new file mode 100644 index 0000000000..1e0fb7edee --- /dev/null +++ b/public/language/hr/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Korisnici", + "edit": "Actions", + "make-admin": "Dodaj administratora", + "remove-admin": "Makni administratora", + "validate-email": "Potvrdite email", + "send-validation-email": "Pošalji email potvrde", + "password-reset-email": "Poslan email zahtjev za resetiranje lozinke", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Blokiraj korisnika", + "temp-ban": "Blokiraj korisnika privremeno", + "unban": "Odblokiraj korisnika", + "reset-lockout": "Resetiraj zaključavanje", + "reset-flags": "Resetiraj zastave", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Preuzmi CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "Novi korisnik", + "filter-by": "Filter by", + "pills.unvalidated": "Nije potvrđen", + "pills.validated": "Validated", + "pills.banned": "Blokirani", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "Po korisničkom imenu", + "search.username-placeholder": "Unesite korisničko ime za pretragu", + "search.email": "Sa email-om", + "search.email-placeholder": "Unesite email za pretragu", + "search.ip": "Po IP adresi", + "search.ip-placeholder": "Unesite IP adresu za pretragu", + "search.not-found": "Korisnik nije pronađen!", + + "inactive.3-months": "3 mjeseca", + "inactive.6-months": "6 mjeseci", + "inactive.12-months": "12 mjeseci", + + "users.uid": "uid", + "users.username": "korisničko ime", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputacija", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "Zadnji online", + "users.banned": "blokiran", + + "create.username": "Korisničko ime", + "create.email": "Email", + "create.email-placeholder": "Email korisnika", + "create.password": "Lozinka", + "create.password-confirm": "Potvdri lozinku", + + "temp-ban.length": "Length", + "temp-ban.reason": "Razlog (Opcionalno)", + "temp-ban.hours": "Sati", + "temp-ban.days": "Dani", + "temp-ban.explanation": "Unesite dužinu trajana blokade. Ukoliko je vrijeme 0 smatra se permanentnom blokadom.", + + "alerts.confirm-ban": "Sigurni ste da želite blokirati ovo korisnika trajno?", + "alerts.confirm-ban-multi": "Sigurni ste da želite blokirati korisnika permanentno?", + "alerts.ban-success": "Korisnik blokiran!", + "alerts.button-ban-x": "Blokiraj %1 korisnika", + "alerts.unban-success": "Korisnik odblokiran!", + "alerts.lockout-reset-success": "Zaključavanje resetirano!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "Nemoguće je maknuti samog sebe iz administracije!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Želite li potvrditi email ovih korisnika?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Email potvrđen", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Želite li poslati email za reset lozinke korisniku ?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "Korisnici obrisani!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Napravi korisnika", + "alerts.button-create": "Napravi", + "alerts.button-cancel": "Odustani", + "alerts.error-passwords-different": "Lozinke se moraju podudarati!", + "alerts.error-x": "Greška

%1

", + "alerts.create-success": "Korisnik kreiran!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "Email pozivnica je poslana %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/hr/admin/menu.json b/public/language/hr/admin/menu.json new file mode 100644 index 0000000000..7145bdc4fc --- /dev/null +++ b/public/language/hr/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Glavno", + + "section-manage": "Upravljanje", + "manage/categories": "Kategorije", + "manage/privileges": "Privileges", + "manage/tags": "Oznake", + "manage/users": "Korisnici", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Lista zahtjeva za registraciju", + "manage/post-queue": "Post Queue", + "manage/groups": "Grupe", + "manage/ip-blacklist": "IP blokade", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Postavke", + "settings/general": "Generalno", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Gosti", + "settings/uploads": "Slanje", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Numeriranje", + "settings/tags": "Oznake", + "settings/notifications": "Obavijesti", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Kolačići", + "settings/web-crawler": "Web puzač", + "settings/sockets": "Utičnice", + "settings/advanced": "Napredno", + + "settings.page-title": "%1 Postavke", + + "section-appearance": "Izgled", + "appearance/themes": "Predlošci", + "appearance/skins": "Izgled", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Proširi", + "extend/plugins": "Dodatci", + "extend/widgets": "Widgeti", + "extend/rewards": "Nagrade", + + "section-social-auth": "Socijalna provjera autentičnosti", + + "section-plugins": "Dodatci", + "extend/plugins.install": "Instaliraj dodatke", + + "section-advanced": "Napredno", + "advanced/database": "Baza podataka", + "advanced/events": "Događanja", + "advanced/hooks": "Hooks", + "advanced/logs": "Dnevnik", + "advanced/errors": "Greške", + "advanced/cache": "Cache", + "development/logger": "Dnevnik", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "ponovno pokreni forum", + "logout": "Odjava", + "view-forum": "Pogledaj forum", + + "search.placeholder": "Search settings", + "search.no-results": "Nema rezultata ...", + "search.search-forum": "Pretraži forum za ", + "search.keep-typing": "Upiši više da vidiš rezultate ...", + "search.start-typing": "Počni pisati da bi vidio rezultate...", + + "connection-lost": "Veza sa %1 je prekinuta, pokušavam se spojiti ...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/advanced.json b/public/language/hr/admin/settings/advanced.json new file mode 100644 index 0000000000..cdf6f454c4 --- /dev/null +++ b/public/language/hr/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Održavanje u toku", + "maintenance-mode.help": "Kada je forum u stanju održavanja,svi zahtjevi će biti preusmjereni statičnoj stranici.Administratori su izuzeti od ovog preusmjerenja i mogu normalno koristiti stranicu.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Poruka održavanja", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Zaglavlje", + "headers.allow-from": "Izaberi ALLOW-FROM da bi ste postavili NodeBB u iFrame.", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Uredi \"Powered by\" zaglavlje koje šalje NodeBB", + "headers.acao": "Pristup-Kontrola-Dozvoli-Izvor", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "Za zabranu pristupa svim stranicama ostavi prazno", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Upravljanje prometom", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Omogući upravljanje prometom", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Smanjivanje ove vrijednosti smanjuje vrijeme čekanja za učitavanje stranica,ali će također pokazivati poruku \"prekomjerno opterećenje\" više korisnika(u takvim slučajevima potrebno je ponovo pokretanje).", + "traffic.lag-check-interval": "Provjeri interval (u milisekundama)", + "traffic.lag-check-interval-help": "Smanjivanje ove vrijednosti uzrokuje da NodeBB postane osjetljivji na oscilacije u prometu,takodjer može uzrokovati da provjere postanu preosjetljive(Biti će potrebno ponovno pokretanje).", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/api.json b/public/language/hr/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/hr/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/chat.json b/public/language/hr/admin/settings/chat.json new file mode 100644 index 0000000000..92c8248d74 --- /dev/null +++ b/public/language/hr/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Postavke razgovora", + "disable": "Onemogući razgovor", + "disable-editing": "Onemogući uređivanje/brisanje poruka razgovora", + "disable-editing-help": "Administratori i moderatori su izuzeti od ovih restrikcija", + "max-length": "Maksimalna dužina poruka u razgovoru", + "max-room-size": "Maksimalan broj korisnika u sobama za razgovor", + "delay": "Vrijeme između poruka razgovora u milisekundama", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/cookies.json b/public/language/hr/admin/settings/cookies.json new file mode 100644 index 0000000000..226a2575de --- /dev/null +++ b/public/language/hr/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Suglasnost EU", + "consent.enabled": "Omogućeno", + "consent.message": "Poruka obavijesti", + "consent.acceptance": "Poruka prihvaćanja", + "consent.link-text": "Odrednice Poveznice Tekst", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Pusti prazno za zadanu NodeBB lokalizaciju", + "settings": "Postavke", + "cookie-domain": "Sesija kolačić domene", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Ostavi prazno za osnovno" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/email.json b/public/language/hr/admin/settings/email.json new file mode 100644 index 0000000000..3030ec0a35 --- /dev/null +++ b/public/language/hr/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Postavke emaila", + "address": "Email adresa", + "address-help": "Sljedeća email adresa je adresa koju će primatelj vidjeti u \"Od\" i \"Odgovori na\" poljima.", + "from": "Od imena", + "from-help": "Ime prikazano u dolaznom emailu.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Uredi predložak emaila", + "template.select": "Odaberi predložak emaila", + "template.revert": "Povrati na original ", + "testing": "Testiranje emaila", + "testing.select": "Odaberi email predložak ", + "testing.send": "Pošalji testni email", + "testing.send-help": "Ovaj test mail će biti poslan svim trenutačno prijavljenim korisnicima na njihovu email adresu.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Pregled Sati.", + "subscriptions.hour-help": "Unesite broj koji pretstavlja vrijeme kada će se poslati pregled mailom (npr. 0 za ponoć, 17za 5 popodne).Imajte na umu da to vrijeme predstavlja vrijeme servera te ne mora predstavljati vrijeme na Vašem sistemu. Vrijeme servera je:
Sljedeći pregled će biti poslan .", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/hr/admin/settings/general.json b/public/language/hr/admin/settings/general.json new file mode 100644 index 0000000000..56f975324f --- /dev/null +++ b/public/language/hr/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Postavke stranice", + "title": "Naslov stranice", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Ime Vaše zajednice", + "title.show-in-header": "Prikaži naslov stranice u zaglavlju", + "browser-title": "Naslov pretraživača", + "browser-title-help": "Ako naslov pretraživača nije postavljen, koristit će se naziv foruma", + "title-layout": "Raspored naslova", + "title-layout-help": "Definiraj kako će naslov pretraživača biti strukturiran npr.: {pageTitle} | {browserTitle}", + "description.placeholder": "Kratak opis zajednice", + "description": "Opis foruma", + "keywords": "Ključne riječi", + "keywords-placeholder": "Ključne riječi koje opisuju Vašu zajednicu, odvojeni zarezom", + "logo": "Logo foruma", + "logo.image": "Slika", + "logo.image-placeholder": "Putanja logotipa za zaglavlje foruma", + "logo.upload": "Učitaj", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "URL loga stranice", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt tekst", + "log.alt-text-placeholder": "Alternativni tekst za dostupnost", + "favicon": "Favicon", + "favicon.upload": "Učitaj", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Učitaj", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Odlazne poveznice", + "outgoing-links.warning-page": "Koristi upozorenje za odlazne poveznice", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domene za koje se ne koristi odlazno upozorenje", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/hr/admin/settings/group.json b/public/language/hr/admin/settings/group.json new file mode 100644 index 0000000000..7d82af1610 --- /dev/null +++ b/public/language/hr/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Glavno", + "private-groups": "Privatne grupe", + "private-groups.help": "Ako je uključeno,ulazak u grupe zahtjevati će odobrenje vlasnika grupe (Default: enabled)", + "private-groups.warning": "Pazi! Ako je ova opcija isključena,a imate privatne grupe,automatski će postati javne.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maksimalna dužina imena grupe", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Slika grupe", + "default-cover": " ", + "default-cover-help": "Dodaj slike sa zarezima između za grupe koje nemaju učitanu naslovnu sliku" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/guest.json b/public/language/hr/admin/settings/guest.json new file mode 100644 index 0000000000..78c75a43e2 --- /dev/null +++ b/public/language/hr/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Dozvoli upravljanje gostima", + "handles.enabled-help": "Ova opcija omogućava gostima da izaberi ime za svaku objavu koju naprave.Ako je onemogućena gosti će se zvati \"gost\".", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/homepage.json b/public/language/hr/admin/settings/homepage.json new file mode 100644 index 0000000000..4c4d323a2f --- /dev/null +++ b/public/language/hr/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Naslovnica", + "description": "Izaberi koja stranica će se prikazivati kada korisnici navigiraju u root URL Vašeg foruma", + "home-page-route": "Putanja naslovnice", + "custom-route": "Uobičajna putanja", + "allow-user-home-pages": "Dozvoli korisničke naslovnice", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/languages.json b/public/language/hr/admin/settings/languages.json new file mode 100644 index 0000000000..a20b3c705d --- /dev/null +++ b/public/language/hr/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Postavke jezika", + "description": "Zadani jezik odlučuje o postavkama jezika za sve korisnike foruma.
.Korisnici mogu sami odabrati jezik na stranici postavki jezika.", + "default-language": "Zadani jezik", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/navigation.json b/public/language/hr/admin/settings/navigation.json new file mode 100644 index 0000000000..00f84662dd --- /dev/null +++ b/public/language/hr/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ikona:", + "change-icon": "promjena", + "route": "Putanja:", + "tooltip": "Napomena:", + "text": "Tekst:", + "text-class": "Text Class: opcija", + "class": "Class: optional", + "id": "ID: opcionalno", + + "properties": "Postavke", + "groups": "Groups:", + "open-new-window": "Otvori u novom prozoru", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Obriši", + "btn.disable": "Onemogući", + "btn.enable": "Omogući", + + "available-menu-items": "Dostupni artikli menija", + "custom-route": "Uobičajna putanja", + "core": "jezgra", + "plugin": "dodatak" +} diff --git a/public/language/hr/admin/settings/notifications.json b/public/language/hr/admin/settings/notifications.json new file mode 100644 index 0000000000..9b33fa7397 --- /dev/null +++ b/public/language/hr/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Obavijesti", + "welcome-notification": "Obavijest dobrodošlice", + "welcome-notification-link": "Poveznica objave dobrodošlice", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/pagination.json b/public/language/hr/admin/settings/pagination.json new file mode 100644 index 0000000000..01a73cb93d --- /dev/null +++ b/public/language/hr/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Postavke numeriranja", + "enable": "Numeriraj teme i objave umjesto beskrajnog skrolanja.", + "posts": "Post Pagination", + "topics": "Numeriranje tema", + "posts-per-page": "Objava po stranici ", + "max-posts-per-page": "Maximum posts per page", + "categories": "Numeriranje kategorija", + "topics-per-page": "Tema po stranici", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/post.json b/public/language/hr/admin/settings/post.json new file mode 100644 index 0000000000..fa4eaaefcf --- /dev/null +++ b/public/language/hr/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Redosljed objava", + "sorting.post-default": "Zadano sortiranje objava", + "sorting.oldest-to-newest": "Starije prema Novijem", + "sorting.newest-to-oldest": "Novije prema Starijem", + "sorting.most-votes": "Najviše glasova", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Uobičajeno sortiranje tema", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Restrikcije objave", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimalna dužina naslova", + "restrictions.max-title-length": "Maksimalna dužina naslova", + "restrictions.min-post-length": "Minimalna dužina objave", + "restrictions.max-post-length": "Maksimalna dužina objave", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "Ako je tema smatran neaktivnim,upozorenje će biti prikazano svim korisnicima koji pokušaju odgovoriti na temu", + "timestamp": "Vremenska oznaka", + "timestamp.cut-off": "Datum prekida (u danima)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Zadirkivač objava", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "Prvi", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Nepročitane postavke", + "unread.cutoff": "Nepročitano dani prekinutosti", + "unread.min-track-last": "Minimalni broj objava u temi prije praćenja zadnje pročitanog", + "recent": "Nedavne postavke", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "onemogući filtriranje tema u ignoriranim kategorijama na stranici /nedavno", + "signature": "Postavke potpisa", + "signature.disable": "Onemogući potpise", + "signature.no-links": "Onemogući odlazne poveznice u potpisima ", + "signature.no-images": "Onemogući slike u potpisima", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Minimalna dužina potpisa", + "composer": "Postavke Composer-a", + "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.show-help": "Prikaži \"Pomoć\"", + "composer.enable-plugin-help": "Dozvoli dodatcima da dodaju sadržaj u \"Pomoć\"", + "composer.custom-help": "Tekst \"Pomoć\"", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP praćenje", + "ip-tracking.each-post": "Prati IP adresu za svaku objavu", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/reputation.json b/public/language/hr/admin/settings/reputation.json new file mode 100644 index 0000000000..7a2912931e --- /dev/null +++ b/public/language/hr/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Postavke reputacije", + "disable": "Onemogući reputacije", + "disable-down-voting": "Onemogući oduzimanje glasova", + "votes-are-public": "Svi glasovi su javni", + "thresholds": "Prag aktivnosti", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimalna reputacija za glasanje protiv", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimalna reputacija za označavanje objava", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/social.json b/public/language/hr/admin/settings/social.json new file mode 100644 index 0000000000..b6f1c3ee29 --- /dev/null +++ b/public/language/hr/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Dijeljenje objave", + "info-plugins-additional": "Dodaci mogu dodati dodatne mreže za dijeljenje objava.", + "save-success": "Uspješno spremljene mreže za razmjenu objava!" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/sockets.json b/public/language/hr/admin/settings/sockets.json new file mode 100644 index 0000000000..7528fef036 --- /dev/null +++ b/public/language/hr/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Postavke ponovnog spajanja", + "max-attempts": "Max pokušaji spajanja", + "default-placeholder": "Zadano: %1", + "delay": "Stanka u ponovnom spajanju" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/sounds.json b/public/language/hr/admin/settings/sounds.json new file mode 100644 index 0000000000..21bf8e26ff --- /dev/null +++ b/public/language/hr/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Obavijesti", + "chat-messages": "Poruke", + "play-sound": "Pokreni", + "incoming-message": "Dolazna poruka", + "outgoing-message": "Odlazna poruka", + "upload-new-sound": "Učitaj novi zvuk", + "saved": "Postavke spremljene" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/tags.json b/public/language/hr/admin/settings/tags.json new file mode 100644 index 0000000000..0a58e9ad9c --- /dev/null +++ b/public/language/hr/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Postavke oznaka", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Najmanje oznaka za temu", + "max-per-topic": "Maksimalno oznaka po temi", + "min-length": "Minimalna dužina oznake", + "max-length": "Maksimalna dužina oznaka", + "related-topics": "Slične teme", + "max-related-topics": "Maksimalni broj povezanih tema za prikaz(ako je podržano unutar predloška)" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/uploads.json b/public/language/hr/admin/settings/uploads.json new file mode 100644 index 0000000000..894efe4fd8 --- /dev/null +++ b/public/language/hr/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Objave", + "orphans": "Orphaned Files", + "private": "Učini datoteke privatnim", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maksimalna veličina datoteka (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Dozvoli korisnicima da učitaju sliku teme", + "topic-thumb-size": "Veličina slike teme", + "allowed-file-extensions": "Dozvoljene ekstenzije datoteka", + "allowed-file-extensions-help": "Unesite popis dozvoljenih ekstenzija datoteka sa zarezima između (npr. pdf,xls,doc ).Prazan popis znači da su sve ekstenzije dozvoljene.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Avatar profila", + "allow-profile-image-uploads": "Dozvoli korisnicima da učitaju sliku profila", + "convert-profile-image-png": "Konvertiraj profilne slike u PNG", + "default-avatar": "Zadani osnovni avatar", + "upload": "Učitaj", + "profile-image-dimension": "Dimenzije slike profila", + "profile-image-dimension-help": "(u pikselima, zadano: 128 piksela)", + "max-profile-image-size": "Maksimalna veličina profilne slike", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maksimalna veličina slike za naslovnicu", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Zadrži stare verzije avatara i slike profila na serveru", + "profile-covers": "Slika profila", + "default-covers": "Osnovne slike naslovnica", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/hr/admin/settings/user.json b/public/language/hr/admin/settings/user.json new file mode 100644 index 0000000000..e1aa153b52 --- /dev/null +++ b/public/language/hr/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Autentifikacija", + "email-confirm-interval": "Korisnik ne može ponovno poslati potvrdni email do ", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Dozvoli prijavu sa", + "allow-login-with.username-email": "Korisničko ime ili Email", + "allow-login-with.username": "Korisničko ime", + "account-settings": "Postavke računa", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "onemogući promjenu korisničkog imena", + "disable-email-changes": "Onemogući promjenu emaila", + "disable-password-changes": "Onemogući promjenu lozinke", + "allow-account-deletion": "Dozvoli brisanje računa korisnicima", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Predlošci", + "disable-user-skins": "Onemogući korisnicima odabir predloška", + "account-protection": "Zaštita računa", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Pokušaji prijave po satu", + "login-attempts-help": "U slučaju pokušaja prijave na račun user's u tolikoj količini da prelazi ovaj prag,račun će biti zaključan na pre-konfigurirano vrijeme", + "lockout-duration": "Broj minuta u slučaju zaključavanja računa", + "login-days": "Dani za zapamtiti sesiju korisničke prijave", + "password-expiry-days": "Forsiraj reset lozinke nakon broja dana", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "Korisnička registracija", + "registration-type": "Tip registracije", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Nromalno", + "registration-type.admin-approval": "Administratorsko dopuštenje", + "registration-type.admin-approval-ip": "Administratorska dozovola za IP", + "registration-type.invite-only": "Samo uz pozivnicu", + "registration-type.admin-invite-only": "Samo uz pozivnicu administratora", + "registration-type.disabled": "Bez registracije", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maksimalan broj pozivnica po korisniku", + "max-invites": "Maksimalan broj pozivnica po korisniku", + "max-invites-help": "0 bez restrikcija. Administrator ima neograničeno pozivnica
Primjenjivo samo za \"poziv na forum\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimalna dužina korisničkog imena", + "max-username-length": "Maksimalna dužina korisničkog imena", + "min-password-length": "Minimalna dužina lozinke", + "min-password-strength": "Minimalna snaga lozinke", + "max-about-me-length": "Maksimalna dužina \"O meni\"", + "terms-of-use": "Pravila korištenja foruma (ostavi prazno za isključeno)", + "user-search": "Korisnička pretraga", + "user-search-results-per-page": "Broj rezultata za prikaz", + "default-user-settings": "Osnovne korisničke postavke", + "show-email": "Prikaži email", + "show-fullname": "Prikaži puno ime", + "restrict-chat": "Dozvoli poruke samo od ljudi koje praim", + "outgoing-new-tab": "Otvori odlazne poveznive u novom prozoru ", + "topic-search": "Dopusti pretragu po temama", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Pretplatite se na pregled", + "digest-freq.off": "Isključi", + "digest-freq.daily": "Dnevno", + "digest-freq.weekly": "Tjedno", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Mjesečno", + "email-chat-notifs": "Pošalji email ukoliko stigne nova poruka dok nisam na mreži", + "email-post-notif": "Pošalji email pri odgovoru u teme na koje pratim", + "follow-created-topics": "Prati teme koje kreiram", + "follow-replied-topics": "Prati teme na koje odgovorim", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/hr/admin/settings/web-crawler.json b/public/language/hr/admin/settings/web-crawler.json new file mode 100644 index 0000000000..1bcfcd2409 --- /dev/null +++ b/public/language/hr/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Postavke pretraživanja", + "robots-txt": "Obični Robots.txt ostavi prazno za osnovno", + "sitemap-feed-settings": "Sitemap i postavke feeda", + "disable-rss-feeds": "Onemogući RSS", + "disable-sitemap-xml": "Onemogući Sitemap.xml", + "sitemap-topics": "Broj tema za prikaz u mapi foruma", + "clear-sitemap-cache": "Očisti mapu foruma iz predmemorije", + "view-sitemap": "Pogledaj mapu foruma" +} \ No newline at end of file diff --git a/public/language/hr/category.json b/public/language/hr/category.json new file mode 100644 index 0000000000..c87d3e2507 --- /dev/null +++ b/public/language/hr/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategorija", + "subcategories": "Podkategorije", + "new_topic_button": "Nova Tema", + "guest-login-post": "Prijavi se za objavu", + "no_topics": "Nema tema u ovoj kategoriji.
Zašto ne probate napisati novu?", + "browsing": "pregledavanje", + "no_replies": "Nema odgovora", + "no_new_posts": "Nema novih tema.", + "watch": "Prati", + "ignore": "Ignoriraj", + "watching": "Pratim", + "not-watching": "Not Watching", + "ignoring": "Ignoriram", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Praćene Kategorije", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/hr/email.json b/public/language/hr/email.json new file mode 100644 index 0000000000..3367f4ef06 --- /dev/null +++ b/public/language/hr/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Dobrodošli na %1", + "invite": "Poziv s %1", + "greeting_no_name": "Zdravo", + "greeting_with_name": "Zdravo %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Zahvaljujemo na registraciji na %1!", + "welcome.text2": "Da bi u potpunosti aktivirali Vaš račun, moramo provjeriti da li ste Vi pravi vlasnik email adrese sa kojom ste se registrirali.", + "welcome.text3": "Administrator je prihvatio vaš zahtjev za registraciju. Možete se prijaviti koristeći svoje korisničko ime i lozinku.", + "welcome.cta": "Kliknite ovdje da bi potvrdili email adresu", + "invitation.text1": "%1 vas je pozvao da se pridružite %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Dobili smo zahtjev za ponovnim kreiranjem lozinke, vjerojatno jer ste ju zaboravili. Ako niste, molimo vas da ignorirate ovaj email.", + "reset.text2": "Da bi nastavili sa ponovnim kreiranjem lozinke, kliknite na ovaj link:", + "reset.cta": "Kliknite ovdje kako biste postavili novu lozinku.", + "reset.notify.subject": "Lozinka uspješno promijenjena.", + "reset.notify.text1": "Obavještavamo vas da vam je lozinka na %1 uspješno promijenjena.", + "reset.notify.text2": "Ako niste ovo odobrili, molimo vas obavijestite administratora.", + "digest.latest_topics": "Posljednje teme s %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Kliknite ovdje kako biste posjetili %1", + "digest.unsub.info": "Ovaj pregled je poslan zbog Vaših postavki pretplata.", + "digest.day": "Dan", + "digest.week": "Tjedan", + "digest.month": "Mjesec", + "digest.subject": "Pregled za %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Nova poruka od %1", + "notif.chat.cta": "Klikni ovdje za nastavak razgovora ", + "notif.chat.unsub.info": "Ova obavijest razgovora Vam je poslana na temelju vaših postavki pretplate.", + "notif.post.unsub.info": "Ova objava Vam je poslana na temelju vaših postavki pretplate.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Ovo je test email za provjeru Vaše konfiguracije.", + "unsub.cta": "Klikni ovdje za promjenu postavki", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Blokirani se na %1", + "banned.text1": "Korisnik %2 je blokirao %1.", + "banned.text2": "Blok će trajati do %1.", + "banned.text3": "Blokirani ste zbog:", + "closing": "Hvala!" +} \ No newline at end of file diff --git a/public/language/hr/error.json b/public/language/hr/error.json new file mode 100644 index 0000000000..48a227155b --- /dev/null +++ b/public/language/hr/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Nevažeći podaci", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Izgleda da niste prijavljeni.", + "account-locked": "Vaš račun je privremeno blokiran", + "search-requires-login": "Pretraga zahtijeva prijavu - prijavite se ili se registrirajte.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Netočan ID kategorije", + "invalid-tid": "Netočan ID teme", + "invalid-pid": "Netočan ID objave", + "invalid-uid": "Netočan ID korisnika", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Netočno korisničko ime", + "invalid-email": "Netočan email", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Netočni korisnički podatci", + "invalid-password": "Netočna lozinka", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Upišite oboje, korisničko ime i lozinku", + "invalid-search-term": "Netočan upit pretraživanja", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "Nismo Vas uspjeli prijaviti, najvjerovatnije zbog istekle sesije. Molimo pokušajte ponovno", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Netočno numeriranje stranica, mora biti %1 ili %2", + "username-taken": "Korisničko ime je zauzeto", + "email-taken": "Email je zauzet", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Ne možete razgovarati dok Vaš email nije potvrđen. Kliknite ovdje da biste potvrdili svoj email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Nismo u mogućnosti potvrditi Vaš email, pokušajte ponovno kasnije.", + "confirm-email-already-sent": "Potvrdni email je poslan, počekajte %1 minuta za ponovni pokušaj.", + "sendmail-not-found": "Sendmail nije pronađen, provjerite da li je instaliran?", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Korisničko ime prekratko", + "username-too-long": "Korisničko ime predugo", + "password-too-long": "Lozinka je preduga", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Molimo, unesite lozinku različitu od Vaše postojeće", + "user-banned": "Korisnik blokiran", + "user-banned-reason": "Ovaj račun je blokiran (Razlog: %1)", + "user-banned-reason-until": "Ovaj račun je blokiran do %1 (Razlog: %2)", + "user-too-new": "Pričekajte %1 sekundi prije prve objave", + "blacklisted-ip": "Vaša IP adresa je blokirana. Ako mislite da je ovo greška, kontaktirajte administratora.", + "ban-expiry-missing": "Postavite datum isteka blokade", + "no-category": "Kategorija ne postoji", + "no-topic": "Tema ne postoji", + "no-post": "Objava ne postoji", + "no-group": "Grupa ne postoji", + "no-user": "Korisnik ne postoji", + "no-teaser": "Zadirkivač ne postoji", + "no-flag": "Flag does not exist", + "no-privileges": "Nemate privilegije za ovu radnju.", + "category-disabled": "Kategorija onemogućena", + "topic-locked": "Tema zaključana", + "post-edit-duration-expired": "Dozvoljeno vam je uređivanje %1 sekundi nakon objave", + "post-edit-duration-expired-minutes": "Dozvoljeno vam je uređivanje %1 minuta nakon objave", + "post-edit-duration-expired-minutes-seconds": "Dozvoljeno vam je uređivanje %1 minuta %2 sekunde nakon objave", + "post-edit-duration-expired-hours": "Dozvoljeno vam je uređivanje %1 sat nakon objave", + "post-edit-duration-expired-hours-minutes": "Dozvoljeno vam je uređivanje %1 sat %2 minute nakon objave", + "post-edit-duration-expired-days": "Dozvoljeno vam je uređivanje %1 dan nakon objave", + "post-edit-duration-expired-days-hours": "Dozvoljeno vam je uređivanje %1 dan %2 sata nakon objave", + "post-delete-duration-expired": "Dozvoljeno vam je brisanje %1 sekundi nakon objave", + "post-delete-duration-expired-minutes": "Dozvoljeno vam je brisanje %1 minute nakon objave", + "post-delete-duration-expired-minutes-seconds": "Dozvoljeno vam je brisanje %1 minute %2 sekunde nakon objave", + "post-delete-duration-expired-hours": "Dozvoljeno vam je brisanje %1 sat nakon objave", + "post-delete-duration-expired-hours-minutes": "Dozvoljeno vam je brisanje %1 sat i %2 minute nakon objave", + "post-delete-duration-expired-days": "Dozvoljeno vam je brisanje %1 dan nakon objave", + "post-delete-duration-expired-days-hours": "Dozvoljeno vam je brisanje %1 dan %2 sata nakon objave", + "cant-delete-topic-has-reply": "Ne možete obrisati svoju temu nakon primljenog odgovora", + "cant-delete-topic-has-replies": "Ne možete obrisati svoju temu nakon što ima %1 odgovora", + "content-too-short": "Unesite dužu objavu. Objava mora sadržavati bar %1 znak(ova). ", + "content-too-long": "Unestie kraću objavu. Objave ne mogu biti duže od %1 znak(ova).", + "title-too-short": "Unesite duži naslov. Naslovi moraju imati najmanje %1 znak(ova).", + "title-too-long": "Unesite kraći naslov. Naslovi ne mogu imati više od %1 znak(ova).", + "category-not-selected": "Kategorija nije odabrana.", + "too-many-posts": "Možete objavljivati svakih %1 skeundi, pričekajte prije ponovne objave", + "too-many-posts-newbie": "Kao novi korisnik, možete objavljivati svakih %1 sekundi dok ne steknete reputaciju %2 - molimo pričekajte prije ponovne objave", + "already-posting": "You are already posting", + "tag-too-short": "Unesite dužu oznaku. Oznake moraju sadržavati najmanje %1 znak(ova)", + "tag-too-long": "Unesite kraću oznaku. Oznake me mogu imati više od %1 znak(ova)", + "not-enough-tags": "Nema dovoljno oznaka. Teme moraju imate bar %1 oznaku", + "too-many-tags": "Previše oznaka. Teme ne mogu imati više od %1 oznaka", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Pričekajte da se prijenos završi.", + "file-too-big": "Maksimalna veličina datoteke je %1 kB - učitajte manju datoteku", + "guest-upload-disabled": "Učitavanje datoteka za goste je isključeno", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Već ste zabilježili ovu objavu", + "already-unbookmarked": "Već ste odbilježili ovu objavu", + "cant-ban-other-admins": "Nemožete blokirati ostale administratore!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Vi ste jedini administrator. Dodajte korisnika kao administratora prije nego sebe odjavite kao administratora.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Ukloni administratorske privilegije sa ovog računa prije brisanja.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Pogrešan format slike. Dozvoljeni formati: %1", + "invalid-image-extension": "Kriva ekstezija slike", + "invalid-file-type": "Netočan tip datoteke. Dozvoljeni formati su: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Prekratko ime grupe", + "group-name-too-long": "Predugo ime Grupe", + "group-already-exists": "Grupa postoji", + "group-name-change-not-allowed": "Promjena imena grupe nije dozvoljena", + "group-already-member": "Već ste član ove grupe", + "group-not-member": "Niste član ove grupe", + "group-needs-owner": "Ova grupa zahtjeva bar jednog vlasnika", + "group-already-invited": "Ovaj korisnik je već pozvan", + "group-already-requested": "Vaš zahtjev za članstvom je već podnesen", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Ova objava je već obrisana", + "post-already-restored": "Ova objava je povraćena", + "topic-already-deleted": "Ova tema je već obrisana", + "topic-already-restored": "Ova tema je povraćena", + "cant-purge-main-post": "Nemožete odbaciti glavnu objavu, obrišite temu za brisanje", + "topic-thumbnails-are-disabled": "Slike tema su onemogućene", + "invalid-file": "Pogrešna datoteka", + "uploads-are-disabled": "Pohrana je onemogućena", + "signature-too-long": "Vaš potpis neže biti duži od %1 znaka", + "about-me-too-long": "O vama nemože biti duže od %1 znaka", + "cant-chat-with-yourself": "Nemoguće je razgovarati sam sa sobom!", + "chat-restricted": "Korisnik je ograničio razgovore. Mora vas pratiti prije nego možete razgovarati", + "chat-disabled": "Razgovor onemogućen", + "too-many-messages": "Poslali ste previše poruka, pričekajte.", + "invalid-chat-message": "Netočna poruka.", + "chat-message-too-long": "Poruka je preduga.Mora imati manje od %1 znakova", + "cant-edit-chat-message": "Nemate dopuštenje uređivati ovu poruku", + "cant-delete-chat-message": "Nije dozvoljeno brisanje ove poruke", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Već ste glasali za ovu objavu", + "reputation-system-disabled": "Sistem reputacije onemogućen.", + "downvoting-disabled": "Oduzimanje glasova je onemogućeno", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "Problem kod ponovnog podizanja: \"%1\" will continue to serve the existing client-side assets.", + "registration-error": "Greška prilikom registracije", + "parse-error": "Došlo je do pogreške u komunikaciji sa serverom", + "wrong-login-type-email": "Upišite Vaš email za prijavu", + "wrong-login-type-username": "Upišite Vaše korisničko ime za prijavu", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "Pozvali ste maksimalan broj ljudi (%1 od %2).", + "no-session-found": "Nije pronađena sesija prijave!", + "not-in-room": "Korisnik nije u sobi", + "cant-kick-self": "Ne možete sebe izbaciti iz grupe", + "no-users-selected": "Korisnici nisu odabrani", + "invalid-home-page-route": "Netočna putanja naslovnice", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Ne možete blokirati sami sebe", + "cannot-block-privileged": "Ne možete blokirati administratore ni globalne administratore", + "cannot-block-guest": "Gosti ne mogu blokirati druge korisnike", + "already-blocked": "Ovaj korisnik je već blokiran", + "already-unblocked": "Ovaj korisnik je več odblokiran", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/hr/flags.json b/public/language/hr/flags.json new file mode 100644 index 0000000000..673ba3806c --- /dev/null +++ b/public/language/hr/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Stanje", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Huura! Nema pronađenih zastavica.", + "assignee": "Dodijeljeni", + "update": "Nadogradnja", + "updated": "Nadograđeno", + "resolved": "Resolved", + "target-purged": "Sadržaj koji je označen zastavom je odbačen i više nije dostupan.", + + "graph-label": "Daily Flags", + "quick-filters": "Brzi filteri", + "filter-active": "Postoje jedan ili više filtera aktivnih u popisu zastava", + "filter-reset": "Ukloni filtere", + "filters": "Opcije filtera", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Vrsta zastave", + "filter-type-all": "Sav sadržaj", + "filter-type-post": "Objave", + "filter-type-user": "User", + "filter-state": "Stanje", + "filter-assignee": "Asignee UID", + "filter-cid": "Kategorija", + "filter-quick-mine": "Dodijeljeno meni", + "filter-cid-all": "Sve kategorije", + "apply-filters": "Primjeni filtere", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Označeni korisnici", + "view-profile": "Pogledaj profil", + "start-new-chat": "Pokreni novi razgovor", + "go-to-target": "Pogledaj metu zastave", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Pogledaj profil", + "user-edit": "Uredi profil", + + "notes": "Bilješke zastave", + "add-note": "Dodaj bilješku", + "no-notes": "Nema podijeljenih bilješki", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Bilješka dodana", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Nema povijesti zastava.", + + "state-all": "Sva stanja", + "state-open": "Novo/Otvori", + "state-wip": "Rad u tijeku", + "state-resolved": "Riješeno", + "state-rejected": "Odbijeno", + "no-assignee": "Nije dodijeljeno", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Navedite razlog označavanja zastavom %1 %2 .U suprotnom koristite jedan od dugmića za brzo prijavljivanje.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Uvredljivo", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Razlog prijavljivanja ovog sadržaja", + "modal-submit": "Podnesi izvještaj", + "modal-submit-success": "Ovaj sadržaj je označen zastavom u svrhu moderiranja,", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/hr/global.json b/public/language/hr/global.json new file mode 100644 index 0000000000..f673e499f4 --- /dev/null +++ b/public/language/hr/global.json @@ -0,0 +1,126 @@ +{ + "home": "Naslovna", + "search": "Pretraga", + "buttons.close": "Zatvori", + "403.title": "Pristup onemogućen", + "403.message": "Nemate pristup ovoj stranici .", + "403.login": "Pokušajte se prijaviti?", + "404.title": "Nije pronadjeno", + "404.message": "Ova stranica ne postoji. Vrati se na početnu.", + "500.title": "Interna greška.", + "500.message": "Ups! Čini se da nešto nije u redu.", + "400.title": "Krivi zahtjev.", + "400.message": "Izgleda da ovaj link nije ispravan, molimo provjerite i ponovo pokušajte. Ili se vratite na početnu stranicu.", + "register": "Registracija", + "login": "Prijava", + "please_log_in": "Molimo prijavite se.", + "logout": "Odjava", + "posting_restriction_info": "Objave su trenutačno omogućene samo registriranim korisnicima,kliknite ovdje za prijavu.", + "welcome_back": "Dobrodošli natrag", + "you_have_successfully_logged_in": "Uspješno ste se prijavili", + "save_changes": "Spremi promjene", + "save": "Spremi", + "close": "Zatvori", + "pagination": "Stranice", + "pagination.out_of": "%1 od %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Kategorije", + "header.recent": "Posljednje", + "header.unread": "Nepročitano", + "header.tags": "Tagovi", + "header.popular": "Popularno", + "header.top": "Top", + "header.users": "Korisnici", + "header.groups": "Grupe", + "header.chats": "Razgovori", + "header.notifications": "Obavijesti", + "header.search": "Pretraga", + "header.profile": "Profil", + "header.navigation": "Navigacija", + "notifications.loading": "Učitavanje obavijesti", + "chats.loading": "Učitavam razgovore", + "motd.welcome": "Dobrodošli na Silicon Island Rijeka 2020 forum.", + "previouspage": "Prethodna stranica", + "nextpage": "Sljedeća stranica", + "alert.success": "Uspjeh!", + "alert.error": "Greška", + "alert.banned": "Blokiran", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Više ne pratite %1", + "alert.follow": "Sada pratite %1", + "users": "Korisnici", + "topics": "Teme", + "posts": "Objave", + "x-posts": "%1 posts", + "best": "Najbolje", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Pozitivni glasači", + "upvoted": "Glasova za", + "downvoters": "Glasači protiv", + "downvoted": "Glasova protiv", + "views": "Pregleda", + "posters": "Posters", + "reputation": "Reputacija", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "pročitaj više", + "more": "Više", + "none": "None", + "posted_ago_by_guest": "postao gost prije %1", + "posted_ago_by": "postao %2 prije %1 ", + "posted_ago": "Objavljeno prije %1", + "posted_in": "Objavljeno u %1", + "posted_in_by": "Objavljeno u %1 od &2", + "posted_in_ago": "Objavljeno u %1 %2", + "posted_in_ago_by": "Objavljeno u %1 &2 od %3", + "user_posted_ago": "%1 je objavio %2", + "guest_posted_ago": "Gost je objavio %1", + "last_edited_by": "Zadnji put uređeno &1", + "norecentposts": "Nema nedavnih objava", + "norecenttopics": "Nema nedavnih tema", + "recentposts": "Posljednji postovi", + "recentips": "Posljednje prijavljeni IPovi", + "moderator_tools": "Moderatorski alati", + "online": "Na mreži", + "away": "Odustan", + "dnd": "Ne smetaj", + "invisible": "Nevidljiv", + "offline": "Odjavljen", + "email": "Email", + "language": "Jezik", + "guest": "Gost", + "guests": "Gosti", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum je nadograđen", + "updated.message": "Ovaj forum je upravo nadograđen na posljednju verziju. Klikni ovdje za ponovno učitavanje stranice.", + "privacy": "Privatnost", + "follow": "Prati", + "unfollow": "Prestani pratiti", + "delete_all": "Obriši sve", + "map": "Mapa", + "sessions": "Prijavljene sesije", + "ip_address": "IP adresa", + "enter_page_number": "Unesi broj stranice", + "upload_file": "Učitaj datoteku", + "upload": "Učitavanje", + "uploads": "Uploads", + "allowed-file-types": "Dozvoljeni tipovi datoteke su %1", + "unsaved-changes": "Imate nespremljenih promjena. Jeste li sigurni da želite napustiti stranicu?", + "reconnecting-message": "Izgleda da je veza na %1 prekinuta, molimo pričekajte dok se pokušamo ponovo spojiti.", + "play": "Pokreni", + "cookies.message": "Ova stranica koristi kolačiće kako bi osigurala najbolje korisničko iskustvo.", + "cookies.accept": "Shvaćam!", + "cookies.learn_more": "Saznaj više", + "edited": "Uređeno", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/hr/groups.json b/public/language/hr/groups.json new file mode 100644 index 0000000000..bfdbf2a416 --- /dev/null +++ b/public/language/hr/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupe", + "view_group": "Pogledaj grupu", + "owner": "Vlasnik grupe", + "new_group": "Napravi novu grupu", + "no_groups_found": "Nema grupa za pregled", + "pending.accept": "Prihvaćam", + "pending.reject": "Odbij", + "pending.accept_all": "Prihvati sve", + "pending.reject_all": "Odbij sve", + "pending.none": "Trenutno nema korisnika na čekanju", + "invited.none": "Trenutno nema pozvanih članova", + "invited.uninvite": "Povuci pozivnicu", + "invited.search": "Pretraži korisnike za poziv u grupu", + "invited.notification_title": "Pozvani ste da se pridružite%1", + "request.notification_title": "Zahtjev za pristup grupi od %1", + "request.notification_text": "%1 je poslao zahtjev da postane član %2", + "cover-save": "Spremi", + "cover-saving": "Spremanje", + "details.title": "Detalji Grupe", + "details.members": "Popis članova", + "details.pending": "Korisnici na čekanju", + "details.invited": "Pozvani korisnici", + "details.has_no_posts": "Članovi ove grupe nisu objavljivali.", + "details.latest_posts": "Zadnje objave", + "details.private": "Privatno", + "details.disableJoinRequests": "Onemogući zahtjeve za pristup", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Dozvoli/Ukini vlasništvo", + "details.kick": "Izbaci", + "details.kick_confirm": "Jeste li sigurni da želite izbaciti ovog člana iz grupe?", + "details.add-member": "Add Member", + "details.owner_options": "\"Administracija grupe", + "details.group_name": "Ime grupe", + "details.member_count": "Broj članova", + "details.creation_date": "Kreirano", + "details.description": "Opis", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Prikaz značke", + "details.change_icon": "Promjeni ikonu", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Tekst značke", + "details.userTitleEnabled": "Pokaži značku", + "details.private_help": "Ako je uključeno, ulazak korisnika u grupu zahtjeva odobrenje vlasnika grupe", + "details.hidden": "Sakriveno", + "details.hidden_help": "Ako je uključeno, ova grupa neće biti na popisu grupa i korisnici će morati biti pozvani ručno", + "details.delete_group": "Obriši ovu grupu", + "details.private_system_help": "Privatne grupe su isključene na sistemskoj razini", + "event.updated": "Detalji grupe su promjenjeni", + "event.deleted": "Grupa \\\"%1\\\" je obrisana", + "membership.accept-invitation": "Prihvati pozivnicu", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Pozivnica na čekanju", + "membership.join-group": "Priključi se u grupu", + "membership.leave-group": "Izađi iz grupe", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Odbij", + "new-group.group_name": "Ime grupe:", + "upload-group-cover": "Promjeni naslovnicu grupe", + "bulk-invite-instructions": "Unesi popis korisnika sa zarezima između korisničkih imena za poziv u ovu grupu", + "bulk-invite": "Masovni poziv", + "remove_group_cover_confirm": "Jeste li sigurni da želite obrisati sliku naslovnice?" +} \ No newline at end of file diff --git a/public/language/hr/ip-blacklist.json b/public/language/hr/ip-blacklist.json new file mode 100644 index 0000000000..6da34bc7a1 --- /dev/null +++ b/public/language/hr/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Upišite IP za blokiranje ovdje.", + "description": "Blokiranje korisničkog računa neki put nije dovoljno za odbiti nepoželjno osobu.U tom slučaju najbolji način da se zaštiti forum je onemogućavanje spajanja na forum sa određene IP adrese ili spektrom IP adresa.", + "active-rules": "Aktivna pravila", + "validate": "Potvrdite blokade ", + "apply": "Primjeni blokade", + "hints": "Sintaktički savjeti", + "hint-1": "Odredite jednu IP adresu po liniji. Možete dodati IP blokove dokle god su upisani u CIDR formatu (npr. 192.168.100.0/22).", + "hint-2": "Možete dodati komentare tako da u početku reda upišete simbol ljestvi code>#
", + + "validate.x-valid": "%1 od %2 pravila valjano.", + "validate.x-invalid": "Sljedeća %1 pravila su nevažeća:", + + "alerts.applied-success": "Blokiranje omogućeno", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/hr/language.json b/public/language/hr/language.json new file mode 100644 index 0000000000..bdc95e799c --- /dev/null +++ b/public/language/hr/language.json @@ -0,0 +1,5 @@ +{ + "name": "Hrvatski", + "code": "hr", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/hr/login.json b/public/language/hr/login.json new file mode 100644 index 0000000000..46d817fdb4 --- /dev/null +++ b/public/language/hr/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Korisničko ime / Email", + "username": "Korisničko ime", + "remember_me": "Zapamti me?", + "forgot_password": "Zaboravljena lozinka?", + "alternative_logins": "Alternativne prijave", + "failed_login_attempt": "Neuspješna prijava", + "login_successful": "Uspješno ste prijavljeni!", + "dont_have_account": "Nemate korisnički račun?", + "logged-out-due-to-inactivity": "Odjavljeni ste iz administratorske kontrolne ploče zbog neaktivnosti.", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/hr/modules.json b/public/language/hr/modules.json new file mode 100644 index 0000000000..8afe7e77d2 --- /dev/null +++ b/public/language/hr/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Pošalji", + "chat.no_active": "Nemate aktivnih razgovora.", + "chat.user_typing": "%1 piše poruku ...", + "chat.user_has_messaged_you": "%1 vam je poslao poruku.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Odaberite primatelja da vidite povijest razgovora", + "chat.no-users-in-room": "Nema korisnika u ovoj sobi", + "chat.recent-chats": "Nedavni razgovori", + "chat.contacts": "Kontakti", + "chat.message-history": "Povijest razgovora", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop out razgovor", + "chat.minimize": "Smanji", + "chat.maximize": "Povećaj", + "chat.seven_days": "7 Dana", + "chat.thirty_days": "30 Dana", + "chat.three_months": "3 Mjeseca", + "chat.delete_message_confirm": "Sigurni ste da želite izbrisati ovu poruku?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "Korisnik ne želi biti ometan. Jeste li sigurno da mu želite poslati poruku?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Sastavi", + "composer.show_preview": "Prikaz", + "composer.hide_preview": "Sakrij prikaz", + "composer.user_said_in": "%1 je rekao u %2:", + "composer.user_said": "%1 je rekao:", + "composer.discard": "Sigurni ste da želite odbaciti ovu objavu?", + "composer.submit_and_lock": "Objavi i zaključaj", + "composer.toggle_dropdown": "Promjeni padajuće", + "composer.uploading": "Šaljem %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "Popis", + "composer.formatting.strikethrough": "Precrtano", + "composer.formatting.code": "Code", + "composer.formatting.link": "Poveznica", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Učitaj sliku", + "composer.upload-file": "Učitaj datoteku", + "composer.zen_mode": "Zen", + "composer.select_category": "Odaberi kategoriju", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Odbaci", + "bootbox.confirm": "Potvrdi", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Pozicija naslovne slike", + "cover.dragging_message": "Povucite sliku na željenu poziciju i spremite \\\"Save\\\"", + "cover.saved": "Spremljeno", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/hr/notifications.json b/public/language/hr/notifications.json new file mode 100644 index 0000000000..85d9b0f64f --- /dev/null +++ b/public/language/hr/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Obavijesti", + "no_notifs": "Nema novih obavijesti", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Povratak na %1", + "outgoing_link": "Odlazna poveznica", + "outgoing_link_message": "Napuštate %1", + "continue_to": "Nastavite na %1", + "return_to": "Vratite se na %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Nepročitane obavijesti.", + "all": "Sve", + "topics": "Teme", + "replies": "Odgovori", + "chat": "Razgovori", + "group-chat": "Grupni Chat", + "follows": "Pratitelji", + "upvote": "Glasači za", + "new-flags": "Nove zastave", + "my-flags": "Zastave označene na mene", + "bans": "Blokirani", + "new_message_from": "Poruka od %1", + "upvoted_your_post_in": "%1 je glasao za u %2.", + "upvoted_your_post_in_dual": "%1 i %2 Glasalo je za Vašu objavu in %3.", + "upvoted_your_post_in_multiple": "%1 i %2 ostalih glasalo je za Vašu objavu %3.", + "moved_your_post": "%1 je premjestio Vašu objavu u %2", + "moved_your_topic": "%1 je premjestio %2", + "user_flagged_post_in": "%1 je označio objavu u %2", + "user_flagged_post_in_dual": "%1 i %2 označio objavu u %3", + "user_flagged_post_in_multiple": "%1 i %2 ostalih označio objavu u %3", + "user_flagged_user": "%1 označio je profil (%2)", + "user_flagged_user_dual": "%1 i %2su označili profil (%3)", + "user_flagged_user_multiple": "%1 i %2 ostalih su označili korisnički profil (%3)", + "user_posted_to": "%1 je odgovorio/la na: %2", + "user_posted_to_dual": "%1 i %2 ostalih su odgovorili na objavu u: %3", + "user_posted_to_multiple": "%1 i %2 drugih su odgovorili na: %3", + "user_posted_topic": "%1 je otvorio novu temu: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 Vas sada prati.", + "user_started_following_you_dual": "%1 i %2 vas sada prate.", + "user_started_following_you_multiple": "%1 i %2 ostalih vas sada prate.", + "new_register": "%1 je poslao zahtjev za registraciju.", + "new_register_multiple": "%1 registracija čeka odobrenje.", + "flag_assigned_to_you": "Zastava%1 je dodijeljena vama.", + "post_awaiting_review": "Objava čeka pregled", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email potvrđen", + "email-confirmed-message": "Hvala na potvrdi emaila. Vaš račun je sada aktivan.", + "email-confirm-error-message": "Nastao je problem pri potvrdi Vaše email adrese. Provjerite kod ili zatražite novi.", + "email-confirm-sent": "Provjera korisničkog emaila poslana.", + "none": "None", + "notification_only": "Obavijest samo", + "email_only": "Email samo", + "notification_and_email": "Obavijest i Email", + "notificationType_upvote": "Kada netko ocijeni vašu objavi", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/hr/pages.json b/public/language/hr/pages.json new file mode 100644 index 0000000000..26d36c96a1 --- /dev/null +++ b/public/language/hr/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Naslovna", + "unread": "Nepročitane teme", + "popular-day": "Popularne teme danas", + "popular-week": "Popularne teme ovaj tjedan", + "popular-month": "Popularne teme ovaj mjesec", + "popular-alltime": "Najpopularnije teme ", + "recent": "Nedavne teme", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Označene objave", + "ip-blacklist": "IP crna lista", + "post-queue": "Post Queue", + "users/online": "Online korisnici", + "users/latest": "Posljednji korisnici", + "users/sort-posts": "Korisnici s najviše objava", + "users/sort-reputation": "Korisnici s najvećom reputacijom", + "users/banned": "Blokirani korisnici", + "users/most-flags": "Najviše označeni korisnici", + "users/search": "Pretraga korisnika", + "notifications": "Obavijesti", + "tags": "Oznake", + "tag": "Topics tagged under "%1"", + "register": "Registrirajte se", + "registration-complete": "Registracija uspješna", + "login": "Prijavite se na Vaš račun", + "reset": "Promijenite lozinku", + "categories": "Kategorije", + "groups": "Grupe", + "group": "%1 grupa", + "chats": "Razgovori", + "chat": "Razgovor s %1", + "flags": "Zastave", + "flag-details": "Detalji zastave %1", + "account/edit": "Uređivanje \\\"%1\\\"", + "account/edit/password": "Uređivanje lozinke \\\"%1\\", + "account/edit/username": "Uređivanje korisnika \\\"%1\\\"", + "account/edit/email": "Uređivanje email \\\"%1\\\"", + "account/info": "Informacija o računu", + "account/following": "Ljudi %1 prati", + "account/followers": "Ljudi koji prate %1", + "account/posts": "Objavio %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Teme od %1", + "account/groups": "%1 grupe", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1 zabilježene objave", + "account/settings": "Korisničke postavke", + "account/watched": "Teme prati %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "%1 glasao za", + "account/downvoted": "%1 glasao protiv", + "account/best": "Najbolje objave od %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email potvrđen!", + "maintenance.text": "%1 Održavanje u toku. Posjetite nas uskoro.", + "maintenance.messageIntro": "Poruka administratora:", + "throttled.text": "%1: Preopterećenje sustava. Pričekajte nekoliko trenutaka." +} \ No newline at end of file diff --git a/public/language/hr/post-queue.json b/public/language/hr/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/hr/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/hr/recent.json b/public/language/hr/recent.json new file mode 100644 index 0000000000..ac247883e2 --- /dev/null +++ b/public/language/hr/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Nedavno", + "day": "Dan", + "week": "Tjedan", + "month": "Mjesec", + "year": "Godina", + "alltime": "Sve vrijeme", + "no_recent_topics": "Nema nedavnih tema.", + "no_popular_topics": "Nema popularnih tema.", + "there-is-a-new-topic": "Nova tema.", + "there-is-a-new-topic-and-a-new-post": "Nova tema i nova objava.", + "there-is-a-new-topic-and-new-posts": "Nova tema i %1 nova objava", + "there-are-new-topics": "%1 nova tema", + "there-are-new-topics-and-a-new-post": "%1 nova tema i nova objava", + "there-are-new-topics-and-new-posts": "%1 nova tema i %2 nova objava", + "there-is-a-new-post": "Nova objava.", + "there-are-new-posts": "%1 nova objava.", + "click-here-to-reload": "Klikni ovdje za ponovno učitavanje." +} \ No newline at end of file diff --git a/public/language/hr/register.json b/public/language/hr/register.json new file mode 100644 index 0000000000..c79ad6d09e --- /dev/null +++ b/public/language/hr/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registracija", + "cancel_registration": "Obustavi registraciju", + "help.email": "Vaš email će biti skriven od javnosti.", + "help.username_restrictions": "Unikatno korisničko ime između %1 i %2 znaka. Ostali Vas mogu spomenuti sa @username.", + "help.minimum_password_length": "Dužina lozinke mora biti %1 znakova.", + "email_address": "Email adresa", + "email_address_placeholder": "Unesite email adresu", + "username": "Korisničko ime", + "username_placeholder": "Unesite korisničko ime", + "password": "Lozinka", + "password_placeholder": "Unesite lozinku", + "confirm_password": "Potvrdite lozinku", + "confirm_password_placeholder": "Potvrdite lozinku", + "register_now_button": "Registrirajte se", + "alternative_registration": "Alternativna registracija:", + "terms_of_use": "Uvjeti korištenja", + "agree_to_terms_of_use": "Prihvaćam uvjete korištenja", + "terms_of_use_error": "Morate prihvatiti uvjete korištenja", + "registration-added-to-queue": "Vaša registracija je dodana u listu zahtjeva za registraciju. Biti ćete obaviješteni kad Vas administrator prihvati.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/hr/reset_password.json b/public/language/hr/reset_password.json new file mode 100644 index 0000000000..5fa64ff81a --- /dev/null +++ b/public/language/hr/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Resetiranje lozinke", + "update_password": "Promjeni lozinku", + "password_changed.title": "Lozinka promijenjena", + "password_changed.message": "

Lozink uspješno promijenjena, prijavite se ponovno!.", + "wrong_reset_code.title": "Netočan kod za resetiranje", + "wrong_reset_code.message": "Netočan kod za resetiranje. Probaj ponovno ili zatraži novi kod.", + "new_password": "Nova lozinka", + "repeat_password": "Potvrdi lozinku", + "changing_password": "Changing Password", + "enter_email": "Unesite Vašu email adresu i poslati ćemo Vam email sa uputstvima kako resetirati lozinku.", + "enter_email_address": "Unesite email adresu", + "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.", + "invalid_email": "Netočan email / email ne postoji!", + "password_too_short": "Lozinka koju ste unijeli je prekratka, izaberite drugu lozinku.", + "passwords_do_not_match": "Lozinke se ne podudaraju!", + "password_expired": "Vaša lozinka je istekla, izaberite novu lozinku" +} \ No newline at end of file diff --git a/public/language/hr/search.json b/public/language/hr/search.json new file mode 100644 index 0000000000..4d69e6db1f --- /dev/null +++ b/public/language/hr/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 rezultat odgovara \"%2\", (%3 sekunde)", + "no-matches": "Nema rezultata", + "advanced-search": "Napredna pretraga", + "in": "U", + "titles": "Naslovi", + "titles-posts": "Naslovi i objave", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Objavio", + "in-categories": "U kategoriji", + "search-child-categories": "Pretraži podkategorije", + "has-tags": "Ima oznake", + "reply-count": "Broj odgovora", + "at-least": "Najmanje", + "at-most": "Najviše", + "relevance": "Relevantno", + "post-time": "Vrijeme objave", + "votes": "Votes", + "newer-than": "Novije od", + "older-than": "Starije od", + "any-date": "Bilo kada", + "yesterday": "Jučer", + "one-week": "Tjedan", + "two-weeks": "Dva tjedna", + "one-month": "Mjesec", + "three-months": "Tri mjeseca", + "six-months": "Šest mjeseci", + "one-year": "Godina", + "sort-by": "Sortiraj po", + "last-reply-time": "Vrijeme zadnje odgovora", + "topic-title": "Naslov teme", + "topic-votes": "Topic votes", + "number-of-replies": "Broj odgovora", + "number-of-views": "Broj pogleda", + "topic-start-date": "Početak teme", + "username": "Korisničko ime", + "category": "Kategorija", + "descending": "U silaznom redu", + "ascending": "Po uzlaznom redu", + "save-preferences": "Spremi postavke", + "clear-preferences": "Očisti postavke", + "search-preferences-saved": "Postavke pretraživanja spremljene", + "search-preferences-cleared": "Postavke pretraživanja očišćene ", + "show-results-as": "Prikaži rezultate kao", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/hr/success.json b/public/language/hr/success.json new file mode 100644 index 0000000000..d00c3087ef --- /dev/null +++ b/public/language/hr/success.json @@ -0,0 +1,7 @@ +{ + "success": "Uspijeh", + "topic-post": "Uspješna objava", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Autentifikacija uspješna", + "settings-saved": "Postavke spremljene!" +} \ No newline at end of file diff --git a/public/language/hr/tags.json b/public/language/hr/tags.json new file mode 100644 index 0000000000..59f324fe04 --- /dev/null +++ b/public/language/hr/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nema tema sa ovom oznakom", + "tags": "Oznake", + "enter_tags_here": "Unesite oznake, između %1 i %2 znaka.", + "enter_tags_here_short": "Unestie oznake ...", + "no_tags": "Još nema oznaka.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/hr/top.json b/public/language/hr/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/hr/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/hr/topic.json b/public/language/hr/topic.json new file mode 100644 index 0000000000..8d6bff9788 --- /dev/null +++ b/public/language/hr/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tema", + "title": "Title", + "no_topics_found": "Tema nije pronađena!", + "no_posts_found": "Objave nisu pronađene!", + "post_is_deleted": "Ova objava je obrisana!", + "topic_is_deleted": "Ova tema je obrisana!", + "profile": "Profil", + "posted_by": "Objavio %1", + "posted_by_guest": "Objavio gost", + "chat": "Razgovor", + "notify_me": "Budi obavješten o novim odgovorima na ovu temu", + "quote": "Citat", + "reply": "Odgovor", + "replies_to_this_post": "%1 je odgovorio", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Zadnji odgovor", + "reply-as-topic": "Odgovori kao temu", + "guest-login-reply": "Prijavi se za objavu", + "login-to-view": "🔒 Log in to view", + "edit": "Uredi", + "delete": "Obriši", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Očisti sve", + "restore": "Obnovi", + "move": "Premjesti", + "change-owner": "Change Owner", + "fork": "Dupliraj", + "link": "Poveznica", + "share": "Podijeli", + "tools": "Alati", + "locked": "Zaključano", + "pinned": "Zakačeno", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Premješteno", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Klikni ovdje za povratak na zadnji pročitani post.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Ova tema je obrisana. Samo korisnici sa privilegijom upravljanja tema je mogu vidjeti.", + "following_topic.message": "Od sada ćete primati obavijesti kada netko objavi objavu u ovoj temi.", + "not_following_topic.message": "Od sada ćete vidjeti ovu temu u popisu nepročitanih tema,ali nećete dobivati obavijesti kada netko objavi objavu u temi.", + "ignoring_topic.message": "Od sada više nećete vidjeti ovu temu u popisu nepročitanih tema.Bit će te obaviješteni kada ste spomenuti ili je netko glasao za vašu objavu.", + "login_to_subscribe": "Registriraj se ili prijavi kako bi se mogao pretplatit na ovu temu.", + "markAsUnreadForAll.success": "Tema označena kao nepročitana za sve.", + "mark_unread": "Označi kao nepročitano", + "mark_unread.success": "Tema označena kao nepročitana", + "watch": "Prati", + "unwatch": "Prestani pratiti", + "watch.title": "Budi obaviješten o novim objavama u ovoj temi", + "unwatch.title": "Prestani pratiti ovu temu", + "share_this_post": "Podijeli ovu objavu", + "watching": "Prati", + "not-watching": "Ne pratiš", + "ignoring": "Ignoriraš", + "watching.description": "Obavijesti me o novim odgovorima .
Prikaži temu u nepročitanim ako kategorija nije ignorirana.", + "ignoring.description": "Nemoj slati obavijesti o novim odgovorima.
Ne prikazuj temu u nepročitanom.", + "thread_tools.title": "Alati teme", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Zakači temu", + "thread_tools.unpin": "Otkači temu", + "thread_tools.lock": "Zaključaj temu", + "thread_tools.unlock": "Odključaj temu", + "thread_tools.move": "Premjesti temu", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Premjesti sve", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Dupliraj temu", + "thread_tools.delete": "Obriši temu", + "thread_tools.delete-posts": "Obriši objavu", + "thread_tools.delete_confirm": "Sigurni ste da želite obrisati ovu temu?", + "thread_tools.restore": "Povrati temu", + "thread_tools.restore_confirm": "Sigurni ste da želite povratiti ovu temu?", + "thread_tools.purge": "Odbaci temu", + "thread_tools.purge_confirm": "Sigurni ste da želite odbaciti ovu temu?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Sigurni ste da želite obrisati ovu objavu?", + "post_restore_confirm": "Sigurni ste da želite povratiti ovu objavu?", + "post_purge_confirm": "Sigurni ste da želite odbaciti ovu objavu?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Učitavam kategorije", + "confirm_move": "Pomakni", + "confirm_fork": "Dupliraj", + "bookmark": "Zabilježi", + "bookmarks": "Zabilješke", + "bookmarks.has_no_bookmarks": "Nemate zabiježenih objava.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Učitavam više objava", + "move_topic": "Pomakni temu", + "move_topics": "Pomakni teme", + "move_post": "Pomakni objavu", + "post_moved": "Objava pomaknuta!", + "fork_topic": "Dupliraj temu", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Označi objave koje želite duplirati", + "fork_no_pids": "Objave nisu odabrane!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 objava odabrana", + "fork_success": "Uspješno duplirana tema. Kliknite ovdje za dupliranu temu.", + "delete_posts_instruction": "Označite objave koje želite obrisati/odbaciti", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Unesite naslov teme ovdje ...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Odbaci", + "composer.submit": "Podnesi", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Odgovori na %1", + "composer.new_topic": "Nova tema", + "composer.editing": "Editing", + "composer.uploading": "slanje...", + "composer.thumb_url_label": "Zaljepite URL slike za temu", + "composer.thumb_title": "Dodajte slike ovoj temi", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Ili učitajte datoteku", + "composer.thumb_remove": "Očisti polja", + "composer.drag_and_drop_images": "Dovuci i pusti sliku ovdje", + "more_users_and_guests": "%1 korisnik i %2 gosta", + "more_users": "%1 korisnik", + "more_guests": "%1 gost", + "users_and_others": "%1 i %2 druga", + "sort_by": "Sortitaj po", + "oldest_to_newest": "Starije prema Novom", + "newest_to_oldest": "Novije prema Starom", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Otvori novu temu?", + "stale.warning": "Tema na koju odgovarate je stara. Želite li otvoriti novu temu i postaviti referencu u vašem odgovoru?", + "stale.create": "Otvori novu temu", + "stale.reply_anyway": "Odgovori na ovu temu svejedno", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/hr/unread.json b/public/language/hr/unread.json new file mode 100644 index 0000000000..331fceb25c --- /dev/null +++ b/public/language/hr/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Nepročitano", + "no_unread_topics": "Sve teme su pročitane", + "load_more": "Učitaj više", + "mark_as_read": "Označi kao pročitano", + "selected": "Odabrano", + "all": "Sve", + "all_categories": "Sve kategorije", + "topics_marked_as_read.success": "Teme označene kao pročitane!", + "all-topics": "Sve teme", + "new-topics": "Nove teme", + "watched-topics": "Praćene teme", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/hr/uploads.json b/public/language/hr/uploads.json new file mode 100644 index 0000000000..68cc3cd936 --- /dev/null +++ b/public/language/hr/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Učitavam datoteku ...", + "select-file-to-upload": "Izaberite datoteku!", + "upload-success": "Prijenos datoteka uspješan!", + "maximum-file-size": "Maksimum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/hr/user.json b/public/language/hr/user.json new file mode 100644 index 0000000000..985402a9b8 --- /dev/null +++ b/public/language/hr/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Blokiran", + "muted": "Muted", + "offline": "Nije na mreži", + "deleted": "Deleted", + "username": "Korisničko ime", + "joindate": "Datum prijave", + "postcount": "Broj objava", + "email": "Email", + "confirm_email": "Potvrdi email", + "account_info": "Informacije o računu", + "admin_actions_label": "Administrative Actions", + "ban_account": "Blokiraj račun", + "ban_account_confirm": "Da li zaista želite blokirati ovog korisnika", + "unban_account": "Odblokiraj račun", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Obriši račun", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Račun obrisan", + "account-content-deleted": "Account content deleted", + "fullname": "Puno ime", + "website": "Web stranica", + "location": "Lokacija", + "age": "Dob", + "joined": "Priključio", + "lastonline": "Viđen na mreži", + "profile": "Profil", + "profile_views": "Pregled profila", + "reputation": "Reputacija", + "bookmarks": "Zabilješke", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Gledano", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Pratitelji", + "following": "Prati", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "O meni", + "signature": "Potpis", + "birthday": "Rođendan", + "chat": "Razgovor", + "chat_with": "Nastavi razgovor sa %1!", + "new_chat_with": "Pokreni novi razgovor sa %1", + "flag-profile": "Označi profil", + "follow": "Prati", + "unfollow": "Prestani pratiti", + "more": "Više", + "profile_update_success": "Profil je uspješno promijenjen!", + "change_picture": "Promjeni sliku", + "change_username": "Promjeni korisničko ime", + "change_email": "Promjeni email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Uredi", + "edit-profile": "Uredi profil", + "default_picture": "Zadana ikona", + "uploaded_picture": "Učitaj sliku", + "upload_new_picture": "Učitaj novu sliku", + "upload_new_picture_from_url": "Učitaj sliku iz URL", + "current_password": "Sadašnja lozinka", + "change_password": "Promjeni lozinku", + "change_password_error": "Netočna lozinka!", + "change_password_error_wrong_current": "Vaša trenutačna lozinka nije točna!", + "change_password_error_match": "Lozinke se moraju podudarati!", + "change_password_error_privileges": "Nemate pravo mijenjati ovu lozinku.", + "change_password_success": "Vaša lozinka je promijenjena!", + "confirm_password": "Potvrdi lozinku", + "password": "Lozinka", + "username_taken_workaround": "Korisničko ime koje ste izabrali je već zauzeto. Zbog toga smo ga malo promjenili. Sada je vaše korisničko ime%1", + "password_same_as_username": "Vaša lozinka je ista kao i vaše korisničko ime, molimo upišite drugu lozinku.", + "password_same_as_email": "Vaša lozinka je ista kao vaš email, molimo upišite drugu lozinku.", + "weak_password": "Slaba lozinka", + "upload_picture": "Učitaj sliku", + "upload_a_picture": "Učitaj sliku", + "remove_uploaded_picture": "Ukloni učitanu sliku", + "upload_cover_picture": "Učitaj naslovnu sliku", + "remove_cover_picture_confirm": "Jeste li sigurno da želite ukloniti naslovnu sliku", + "crop_picture": "Skratite sliku", + "upload_cropped_picture": "Skrati i učitaj", + "avatar-background-colour": "Avatar background colour", + "settings": "Postavke", + "show_email": "Prikaži email", + "show_fullname": "Prikaži puno ime", + "restrict_chats": "Dopusti poruke o korisnika koje pratim", + "digest_label": "Pretplati se na izvještaje", + "digest_description": "Pretplati se na email izvještaje od ovog foruma (nove obavjesti i teme) prema zadanom rasporedu", + "digest_off": "Isključi", + "digest_daily": "Dnevno", + "digest_weekly": "Tjedno", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Mjesečno", + "has_no_follower": "Ovaj korisnik nema pratitelja :(.", + "follows_no_one": "Ovaj korisnik nikog ne prati :(", + "has_no_posts": "Ovaj korisnik nema objava.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Ovaj korisnik nema objavljenih tema.", + "has_no_watched_topics": "Ovaj korisnik ne prati teme.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "Ovaj korisnik nije glasao za na objavama.", + "has_no_downvoted_posts": "Ovaj korisnik nije glasao protiv na objavama.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Email sakriven", + "hidden": "Sakriven", + "paginate_description": "Numeriraj teme i objave umjesto scrollanja", + "topics_per_page": "Teme po stranici", + "posts_per_page": "Objave po stranici", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Postavke pretraživanja", + "open_links_in_new_tab": "Otvori odlazne poveznice u novom tabu", + "enable_topic_searching": "Omogući pretragu unutar tema", + "topic_search_help": "Ako uključeno,pretraga unutar tema će zamijeniti pretragu ključnih riječi vašeg pretraživača kojemu je omogućeno pretraživanje samo onoga što je na ekranu,za razliku od ove opcije koja omogućava pretragu na cijeloj temi", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Nakon objavljivanja,prikaži objavu", + "follow_topics_you_reply_to": "Prati teme na koje objavljuješ", + "follow_topics_you_create": "Prati teme koje si napravio", + "grouptitle": "Ime Grupe", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Nema imena grupe", + "select-skin": "Izaberi izgled", + "select-homepage": "Izaberi naslovnu", + "homepage": "Naslovna", + "homepage_description": "Izaberi stranicu ", + "custom_route": "Uobičajena putanja naslovnice", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Jednokratne usluge prijave", + "sso.associated": "Povezano sa", + "sso.not-associated": "Klikni ovdje za povezivanje sa", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Zadnja zastava", + "info.no-flags": "Nema objava sa zastavama", + "info.ban-history": "Povijest nedavno blokiranih", + "info.no-ban-history": "Ovaj korisnik nikad nije bio blokiran", + "info.banned-until": "Blokiran do %1!", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Trajno blokiran", + "info.banned-reason-label": "Razlog", + "info.banned-no-reason": "Razlog nije dan.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Povijest korisničkog imena", + "info.email-history": "Povijest emaila", + "info.moderation-note": "Poruka moderiranja", + "info.moderation-note.success": "Poruka moderiranja spremljena", + "info.moderation-note.add": "Dodaj bilješku", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/hr/users.json b/public/language/hr/users.json new file mode 100644 index 0000000000..d19e05f8f8 --- /dev/null +++ b/public/language/hr/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Posljednji korisnici", + "top_posters": "Najviše objava", + "most_reputation": "Najveća reputacija", + "most_flags": "Najviše zastava", + "search": "Pretraga", + "enter_username": "Unesi korisničko ime za pretragu", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Učitaj više", + "users-found-search-took": "%1user(s) pronađeni! Pretraga je trajala %2 sekundi.", + "filter-by": "Filtriraj po", + "online-only": "Samo na mreži", + "invite": "Pozovi", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Pozivnica poslana %1", + "user_list": "Popis korisnika", + "recent_topics": "Zadnje teme", + "popular_topics": "Popularne teme", + "unread_topics": "Nepročitane teme", + "categories": "Kategorije", + "tags": "Tagovi", + "no-users-found": "Korisnici nisu pronađeni!" +} \ No newline at end of file diff --git a/public/language/hu/_DO_NOT_EDIT_FILES_HERE.md b/public/language/hu/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/hu/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/hu/admin/admin.json b/public/language/hu/admin/admin.json new file mode 100644 index 0000000000..d7d91c2e48 --- /dev/null +++ b/public/language/hu/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Biztosan újra kívánod építeni majd újraindítod a NodeBB-t?", + "alert.confirm-restart": "Biztosan újra szeretnéd indítani a NodeBB-t?", + + "acp-title": "%1 | NodeBB Adminisztrációs vezérlőpult", + "settings-header-contents": "Tartalmak", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/hu/admin/advanced/cache.json b/public/language/hu/admin/advanced/cache.json new file mode 100644 index 0000000000..0bb92bb8e4 --- /dev/null +++ b/public/language/hu/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Hozzászólás gyorsítótár", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Tele", + "post-cache-size": "Hozzászólás gyorsítótáras mérete", + "items-in-cache": "Elemek a gyorsítótárban" +} \ No newline at end of file diff --git a/public/language/hu/admin/advanced/database.json b/public/language/hu/admin/advanced/database.json new file mode 100644 index 0000000000..517a65d68b --- /dev/null +++ b/public/language/hu/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Üzemidő másodpercben", + "uptime-days": "Üzemidő napban", + + "mongo": "Mongo", + "mongo.version": "MongoDB Verzió", + "mongo.storage-engine": "Tárolómotor", + "mongo.collections": "Gyűjtemények", + "mongo.objects": "Objektumok", + "mongo.avg-object-size": "Átl. objektumméret", + "mongo.data-size": "Adatméret", + "mongo.storage-size": "Tárolóméret", + "mongo.index-size": "Indexméret", + "mongo.file-size": "Fájlméret", + "mongo.resident-memory": "Rezidens memória", + "mongo.virtual-memory": "Virtuális memória", + "mongo.mapped-memory": "Leképezett memória", + "mongo.bytes-in": "Bejövő bájtok", + "mongo.bytes-out": "Kimenő bájtok", + "mongo.num-requests": "Kérések száma", + "mongo.raw-info": "MongoDB nyers információ", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis verzió", + "redis.keys": "Kulcsok", + "redis.expires": "Lejárat", + "redis.avg-ttl": "Átlagos válaszidő", + "redis.connected-clients": "Csatlakozott kliensek", + "redis.connected-slaves": "Csatlakozott szolgák", + "redis.blocked-clients": "Blokkolt kliensek", + "redis.used-memory": "Használt memória", + "redis.memory-frag-ratio": "Töredezett memória aránya", + "redis.total-connections-recieved": "Összes fogadott csatlakozás", + "redis.total-commands-processed": "Összes feldolgozott parancs", + "redis.iops": "Pillanatnyi művelet mpercenként", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Kulcstér találatok", + "redis.keyspace-misses": "Kulcstér tévesztések", + "redis.raw-info": "Redis nyers információ", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL verzió", + "postgres.raw-info": "Postgres nyers információ" +} diff --git a/public/language/hu/admin/advanced/errors.json b/public/language/hu/admin/advanced/errors.json new file mode 100644 index 0000000000..9ad4a82d61 --- /dev/null +++ b/public/language/hu/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Ábra %1", + "error-events-per-day": "%1 esemény naponta", + "error.404": "404 Nem található", + "error.503": "503 A szolgáltatás nem elérhető", + "manage-error-log": "Hibanapló kezelése", + "export-error-log": "Hibanapló exportálása (CSV)", + "clear-error-log": "Hibanapló törlése", + "route": "Útvonal", + "count": "Összeg", + "no-routes-not-found": "Hurrá! Nincs egy 404-es hiba se!", + "clear404-confirm": "Biztosan törölni kívánod a 404-es hibanaplót?", + "clear404-success": "\"404 Nem található\" hibák törölve" +} \ No newline at end of file diff --git a/public/language/hu/admin/advanced/events.json b/public/language/hu/admin/advanced/events.json new file mode 100644 index 0000000000..b824e98cb9 --- /dev/null +++ b/public/language/hu/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Események", + "no-events": "Nem voltak események", + "control-panel": "Esemény vezérlőpult", + "delete-events": "Események törlése", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Szűrők", + "filters-apply": "Szűrők érvényesítése", + "filter-type": "Esemény típus", + "filter-start": "Kezdő dátum", + "filter-end": "Befejező dátum", + "filter-perPage": "Oldalanként" +} \ No newline at end of file diff --git a/public/language/hu/admin/advanced/logs.json b/public/language/hu/admin/advanced/logs.json new file mode 100644 index 0000000000..5cd4370d8d --- /dev/null +++ b/public/language/hu/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Naplók", + "control-panel": "Naplók vezérlőpult", + "reload": "Naplók újratöltése", + "clear": "Naplók törlése", + "clear-success": "Naplók törölve!" +} \ No newline at end of file diff --git a/public/language/hu/admin/appearance/customise.json b/public/language/hu/admin/appearance/customise.json new file mode 100644 index 0000000000..53583244f6 --- /dev/null +++ b/public/language/hu/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Egyéni CSS/LESS", + "custom-css.description": "Adj meg saját CSS/LESS leírókat, melyek minden egyéb stílus után kerülnek alkalmazásra.", + "custom-css.enable": "Egyéni CSS/LESS engedélyezése", + + "custom-js": "Egyéni Javascript", + "custom-js.description": "Adj meg saját javascript-et. Végrehajtására az oldal teljes betöltése után kerül sor.", + "custom-js.enable": "Egyéni Javascript engedélyezése", + + "custom-header": "Egyéni fejléc", + "custom-header.description": "Adj meg egyéni HTML-t (pl. </meta> tag-ek, stb.), amik közvetlenül <head> szekció után kerülnek be a fórum kódjába. Script tag-ek használata engedélyezett, azonban erősen ellenjavallott, mivel az Egyéni Javascript panel elérhető.", + "custom-header.enable": "Egyéni fejléc engedélyezése", + + "custom-css.livereload": "Élő újratöltés engedélyezése", + "custom-css.livereload.description": "Engedélyezésével fiókod alá eső minden eszközön az összes munkamenet kényszerűen frissül akárhányszor a mentésre kattintasz" +} \ No newline at end of file diff --git a/public/language/hu/admin/appearance/skins.json b/public/language/hu/admin/appearance/skins.json new file mode 100644 index 0000000000..9d9e0721f3 --- /dev/null +++ b/public/language/hu/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Felületek betöltése...", + "homepage": "Kezdőoldal", + "select-skin": "Felület választása", + "current-skin": "Jelenlegi felület", + "skin-updated": "Felület frissítve", + "applied-success": "%1 felület sikeresen alkalmazva", + "revert-success": "Felület visszaállítva az alap színekre" +} \ No newline at end of file diff --git a/public/language/hu/admin/appearance/themes.json b/public/language/hu/admin/appearance/themes.json new file mode 100644 index 0000000000..07f5cd2e5c --- /dev/null +++ b/public/language/hu/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Telepített témák ellenőrzése...", + "homepage": "Kezdőoldal", + "select-theme": "Téma választása", + "current-theme": "Aktuális téma", + "no-themes": "Nem található telepített téma", + "revert-confirm": "Biztos vissza akarod állítani az alapértelmezett NodeBB témát?", + "theme-changed": "Téma módosítva", + "revert-success": "Sikeresen visszaállítottad a NodeBB alapértelmezett témáját.", + "restart-to-activate": "A téma teljes aktiválásához kérlek építsd újra majd indítsd újra a NodeBB-t." +} \ No newline at end of file diff --git a/public/language/hu/admin/dashboard.json b/public/language/hu/admin/dashboard.json new file mode 100644 index 0000000000..e02ccff6de --- /dev/null +++ b/public/language/hu/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Fórum forgalma", + "page-views": "Oldal megtekintések", + "unique-visitors": "Egyedi látogatók", + "logins": "Bejelentkezések", + "new-users": "Új felhasználók", + "posts": "Hozzászólások", + "topics": "Témakörök", + "page-views-seven": "Az utóbbi 7 napban", + "page-views-thirty": "Az utóbbi 30 napban", + "page-views-last-day": "Az utóbbi 24 órában", + "page-views-custom": "Egyéni dátum tartomány", + "page-views-custom-start": "Tartomény kezdete", + "page-views-custom-end": "Tartomány vége", + "page-views-custom-help": "Adj meg egy dátum tartományt a kívánt oldal megtekintések megtekintéséhez. Ha nem áll rendelkezésre dátumválasztó, az elfogadott formátum ÉÉÉÉ-HH-NN", + "page-views-custom-error": "Kérlek, érvényes dátum tartományt adj meg ÉÉÉÉ-HH-NN formátumban.", + + "stats.yesterday": "Tegnap", + "stats.today": "Ma", + "stats.last-week": "Előző hét", + "stats.this-week": "Aktuális hét", + "stats.last-month": "Előző hónap", + "stats.this-month": "Aktuális hónap", + "stats.all": "Mindenkori", + + "updates": "Frissítések", + "running-version": "Jelenleg a NodeBB v%1 verzióját futtatod.", + "keep-updated": "Mindig tégy róla, hogy a NodeBB naprakész a legfrissebb biztonsági javítások és hibajavítások végett.", + "up-to-date": "

Naprakész vagy

", + "upgrade-available": "

Kiadásra került egy új verzió (v%1). Vedd fontolóra a NodeBB frissítését.

", + "prerelease-upgrade-available": "

Ez egy elavult kiadás előtti verzió. Elérhető egy új (v%1) verzió. Vedd fontolóra a NodeBB frissítését.

", + "prerelease-warning": "

Ez egy elavult kiadás előtti verzió. Nem szándékos hibás működés előfordulhat.

", + "fallback-emailer-not-found": "Tartalék email kezelő nem található", + "running-in-development": "A fórum fejlesztői módban fut. A fórum jelenleg sebezhető; vedd fel a kapcsolatot az adminisztrátorokkal.", + "latest-lookup-failed": "

A legfrissebb NodeBB verzió lekérdezése sikertelen

", + + "notices": "Értesítések", + "restart-not-required": "Nem szükséges az újraindítás", + "restart-required": "Újraindítás szükséges", + "search-plugin-installed": "Kereső beépülő telepítve", + "search-plugin-not-installed": "Kereső beépülő nincs telepítve", + "search-plugin-tooltip": "Telepíts egy kereső beépülőt a beépülők oldaláról a keresési funkciók aktiválásához", + + "control-panel": "Rendszervezérlés", + "rebuild-and-restart": "Újraépítés & újraindítás", + "restart": "Újraindítás", + "restart-warning": "A NodeBB újraépítésével ill. újraindításával pár másodpercre elvész minden meglévő kapcsolat.", + "restart-disabled": "A NodeBB újraépítése és újraindítása letiltásra került, mivel úgy néz ki nem a megfelelő daemon-al futtatod.", + "maintenance-mode": "Karbantartási mód", + "maintenance-mode-title": "Kattints ide a NodeBB karbantartási módjának beállításához", + "realtime-chart-updates": "Valós idejű grafikon frissítések ", + + "active-users": "Aktív felhasználók", + "active-users.users": "Felhasználók", + "active-users.guests": "Vendégek", + "active-users.total": "Összesen", + "active-users.connections": "Kapcsolatok", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Regisztrált", + + "user-presence": "Felhasználói jelenlét", + "on-categories": "A kategória listán", + "reading-posts": "Hozzászólásokat olvas", + "browsing-topics": "Témaköröket böngész", + "recent": "Mostanában", + "unread": "Olvasatlan", + + "high-presence-topics": "Témakörök nagy jelenléttel", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Oldal megtekintések", + "graphs.page-views-registered": "Regisztrált látogatások", + "graphs.page-views-guest": "Vendég látogatások", + "graphs.page-views-bot": "Bot látogatások", + "graphs.unique-visitors": "Egyedi látogatók", + "graphs.registered-users": "Regisztrált felhasználók", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Utoljára újraindította:", + "no-users-browsing": "Jelenleg nem böngész senki", + + "back-to-dashboard": "Vissza a vezérlőpultra", + "details.no-users": "Nem csatlakozott egy felhasználó sem a kiválasztott időszakban", + "details.no-topics": "Nem voltak új témakörök a kiválasztott időszakban", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "Nem volt bejelentkezés a kiválasztott időszakban", + "details.logins-static": "A NodeBB csak %1 napig menti a munkamenet adatokat és az alábbi táblázat csak a legutóbbi aktív munkameneteket tartalmazza", + "details.logins-login-time": "Bejelentkezés ideje" +} diff --git a/public/language/hu/admin/development/info.json b/public/language/hu/admin/development/info.json new file mode 100644 index 0000000000..d67e35f340 --- /dev/null +++ b/public/language/hu/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 csomópont válaszolt %2mp-n belül!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "üzemidő", + + "registered": "Regisztrált", + "sockets": "Sockets", + "guests": "Vendégek", + + "info": "Információ" +} \ No newline at end of file diff --git a/public/language/hu/admin/development/logger.json b/public/language/hu/admin/development/logger.json new file mode 100644 index 0000000000..06ab9de008 --- /dev/null +++ b/public/language/hu/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Naplózó beállítások", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Egyszerűen jelöld be/töröld a jelölést a naplózó beállításai elől azok menet közbeni engedélyezéséhez illetve tiltásához.", + "enable-http": "HTTP naplózás engedélyezése", + "enable-socket": "A socket.io eseménynaplózás engedélyezése", + "file-path": "A naplófájl elérési útja", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Naplózó vezérlőpult", + "update-settings": "Naplózó beállításainak frissítése" +} \ No newline at end of file diff --git a/public/language/hu/admin/extend/plugins.json b/public/language/hu/admin/extend/plugins.json new file mode 100644 index 0000000000..18f559a666 --- /dev/null +++ b/public/language/hu/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Népszerűek", + "installed": "Telepített", + "active": "Aktív", + "inactive": "Inaktív", + "out-of-date": "Elavult", + "none-found": "Nem találhatók beépülők.", + "none-active": "Nincsenek aktív beépülők.", + "find-plugins": "Beépülők keresése", + + "plugin-search": "Beépülő keresés", + "plugin-search-placeholder": "Beépülő keresése...", + "submit-anonymous-usage": "Névtelen belépülő használati statisztika küldése.", + "reorder-plugins": "Beépülők átrendezése", + "order-active": "Aktív beépülők elrendezése", + "dev-interested": "Szeretnél beépülő modulokat írni a NodeBB-hez?", + "docs-info": "Beépülő készítéshez tartozó teljes dokumentáció elérhető a NodeBB dokumentációs portálon.", + + "order.description": "Némely beépülők akkor működnek jól, ha más beépülők előtt/után töltődnek be.", + "order.explanation": "A beépülők az itt megadott sorrendben töltődnek be fentről lefelé haladva", + + "plugin-item.themes": "Témák", + "plugin-item.deactivate": "Deaktiválás", + "plugin-item.activate": "Aktiválás", + "plugin-item.install": "Telepítés", + "plugin-item.uninstall": "Eltávolítás", + "plugin-item.settings": "Beállítások", + "plugin-item.installed": "Telepítve", + "plugin-item.latest": "Legutóbbi", + "plugin-item.upgrade": "Frissítés", + "plugin-item.more-info": "További információért:", + "plugin-item.unknown": "Ismeretlen", + "plugin-item.unknown-explanation": "A beépülő állapotát nem lehet meghatározni, valószínűleg egy konfigurációs hiba következtében.", + "plugin-item.compatible": "Ez a beépülő működik a NodeBB %1 verziójával", + "plugin-item.not-compatible": "Ennek a beépülőnek nincs kompatibilitási információja. Győződj meg róla, hogy működik mielőtt produkciós környezetben telepíted.", + + "alert.enabled": "Beépülő engedélyezve", + "alert.disabled": "Beépülő letiltva", + "alert.upgraded": "Beépülő frissítve", + "alert.installed": "Beépülő telepítve", + "alert.uninstalled": "Beépülő eltávolítva", + "alert.activate-success": "Kérlek építsd újra és indítsd újra a NodeBB-t, hogy teljesen engedélyezd ezt a beépülőt", + "alert.deactivate-success": "A beépülő sikeresen deaktiválva", + "alert.upgrade-success": "A beépülő teljes frissítéséhez kérlek építsd újra majd indítsd újra a NodeBB-t.", + "alert.install-success": "A beépülő sikeresen telepítve, kérlek aktiváld.", + "alert.uninstall-success": "A beépülő sikeresen deaktiválásra majd eltávolításra került.", + "alert.suggest-error": "

A NodeBB nem tudta elérni a csomagkezelőt, a telepítéssel haladjunk tovább az előző verzió alapján?

A szerver ezt válaszolta: (%1): %2
", + "alert.package-manager-unreachable": "

A NodeBB nem tudta elérni a csomagkezelőt, nem javasolt frissítés futtatása most.

", + "alert.incompatible": "

Az általad futtatott NodeBB verzió (v%1) ennek a beépülőnek csak a v%2 verzióját támogatja. Kérjük frissítsd a NodeBB-det, hogy telepíthesd a beépülő újabb verzióit.

", + "alert.possibly-incompatible": "

Nem található kompatibilitási információ

Ez a beépülő nem adja meg, hogy milyen NodeBB verziókkal képes működni. A teljes kompatibilitás nem garantált és meggátolhatja a NodeBB-t a helyes indulásban.

Abban az esetben, ha a NodeBB nem indul el megfelelően:

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

Foyltassuk a beépülő legfrissebb verziójának telepítését?

", + "alert.reorder": "Beépülők átrendezve", + "alert.reorder-success": "A folyamat teljes befejezéséhez kérlek építsd újra és indítsd újra a NodeBB-t.", + + "license.title": "Beépülő licencinformáció", + "license.intro": "A %1 beépülő a %2 liszensz szerződést használja. Kérjük olvasd el és vedd tudomásul a szerződésben leírtakat, mielőtt használatba veszed a beépülőt.", + "license.cta": "Biztosan szeretnéd folytatni a beépülő aktiválását?" +} diff --git a/public/language/hu/admin/extend/rewards.json b/public/language/hu/admin/extend/rewards.json new file mode 100644 index 0000000000..d5fe130fe8 --- /dev/null +++ b/public/language/hu/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Jutalmak", + "condition-if-users": "Ha a felhasználó", + "condition-is": "Kisebb/nagyobb/egyenlő:", + "condition-then": "Akkor:", + "max-claims": "Hány alkalommal igényelhető a jutalom", + "zero-infinite": "Írj 0-t a végtelenhez", + "delete": "Törlés", + "enable": "Engedélyezés", + "disable": "Tiltás", + + "alert.delete-success": "Jutalom sikeresen törölve", + "alert.no-inputs-found": "Helytelen jutalom - nem található bevitel!", + "alert.save-success": "Jutalmak sikeresen elmentve" +} \ No newline at end of file diff --git a/public/language/hu/admin/extend/widgets.json b/public/language/hu/admin/extend/widgets.json new file mode 100644 index 0000000000..a86018b3b8 --- /dev/null +++ b/public/language/hu/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Elérhető modulok", + "explanation": "Válassz egy modult a legördülő menüből, majd húzd rá az egyik sablon modul területére a bal oldalon.", + "none-installed": "Nincs elérhető modul! Aktiváld a widget essentials beépülőt a beépülők oldalon.", + "clone-from": "Modul klónozása innen", + "containers.available": "Elérhető tárolók", + "containers.explanation": "Húzd rá az alábbiakat bármely aktív modulra", + "containers.none": "Nincs", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel fejléc", + "container.panel-body": "Panel test", + "container.alert": "Riasztás", + + "alert.confirm-delete": "Biztosan szeretnéd törölni ezt a modult?", + "alert.updated": "Modulok frissítve", + "alert.update-success": "Modulok sikeresen frissítve", + "alert.clone-success": "Modulok sikersen klónozva", + + "error.select-clone": "Kérlek válassz oldalt a klónozáshoz", + + "title": "Cím", + "title.placeholder": "Cím (csak némely konténeren jelenik meg)", + "container": "Konténer", + "container.placeholder": "Húzz ide egy konténert vagy adj meg HTML-t.", + "show-to-groups": "Megjelenítés csoportoknak", + "hide-from-groups": "Elrejtés csoportoknak", + "hide-on-mobile": "Elrejtés mobilon" +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/admins-mods.json b/public/language/hu/admin/manage/admins-mods.json new file mode 100644 index 0000000000..4728e654d6 --- /dev/null +++ b/public/language/hu/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Adminisztrátorok", + "global-moderators": "Globális moderátorok", + "moderators": "Moderators", + "no-global-moderators": "Nincsenek Globális moderátorok", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Nincsenek Moderátorok", + "add-administrator": "Adminisztrátor hozzáadása", + "add-global-moderator": "Globális moderátor hozzáadása", + "add-moderator": "Moderátor hozzáadása" +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/categories.json b/public/language/hu/admin/manage/categories.json new file mode 100644 index 0000000000..42b11ebd47 --- /dev/null +++ b/public/language/hu/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Kategória beállítások", + "privileges": "Jogosultságok", + + "name": "Kategória neve", + "description": "Kategória leírása", + "bg-color": "Háttérszín", + "text-color": "Szövegszín", + "bg-image-size": "Háttérkép mérete", + "custom-class": "Egyedi CSS osztály", + "num-recent-replies": "Utóbbi válaszok száma", + "ext-link": "Külső hivatkozás", + "subcategories-per-page": "Alkategóriák oldalanként", + "is-section": "Ez a kategória legyen szakaszként kezelve", + "post-queue": "Bejegyzés várólista", + "tag-whitelist": "Engedélyezett címkék", + "upload-image": "Kép feltöltése", + "delete-image": "Kép törlése", + "category-image": "Kategóriakép", + "parent-category": "Szülő kategória", + "optional-parent-category": "(Nem kötelező) Szülő kategória", + "top-level": "Legfelső szint", + "parent-category-none": "(Nincs)", + "copy-parent": "Szülő másolása", + "copy-settings": "Beállítások másolása", + "optional-clone-settings": "(Nem kötelező) Beállítások klónozása az alábbi kategóriából", + "clone-children": "Leszármazott kategóriák és beállítások klónozása", + "purge": "Kategória törlése", + + "enable": "Engedélyezés", + "disable": "Letiltás", + "edit": "Szerkesztés", + "analytics": "Analitika", + "view-category": "Kategória megtekintése", + "set-order": "Sorrend beállítása", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Kategória kiválasztása", + "set-parent-category": "Szülő kategória beállítása", + + "privileges.description": "Itt megadhatod az oldal egyes részeihez tartozó jogosultságokat. Jogosultságok felhasználónként és csoportonként is beállíthatóak. Válaszd ki a legördülőből mit szeretnél beállítani.", + "privileges.category-selector": "Jogosultságok beállítása ennek: ", + "privileges.warning": "Megjegyzés: A jogosultságok azonnal érvényesülnek. Nem szükséges a kategória mentése, miután beállítottál mindent.", + "privileges.section-viewing": "Megtekintési jogosultságok", + "privileges.section-posting": "Hozzászólási jogosultságok", + "privileges.section-moderation": "Moderációs jogosultságok", + "privileges.section-other": "Egyéb", + "privileges.section-user": "Felhasználó", + "privileges.search-user": "Felhasználó hozzáadása", + "privileges.no-users": "Nincsenek felhasználó-specifikus jogosultságok ebben a kategóriában.", + "privileges.section-group": "Csoport", + "privileges.group-private": "Ez a csoport privát", + "privileges.inheritance-exception": "Ez a csoport nem örököl jogosultságokat a \"registered-users\" csoporttól", + "privileges.banned-user-inheritance": "A kitiltott felhasználók jogosultságokat örökölnek a \"banned-users\" csoportból", + "privileges.search-group": "Csoport hozzáadása", + "privileges.copy-to-children": "Másolás utódokra", + "privileges.copy-from-category": "Másolás kategóriából", + "privileges.copy-privileges-to-all-categories": "Másolás minden kategóriához", + "privileges.copy-group-privileges-to-children": "A csoport jogosultságainak másolása a kategórián belüli leszármazottakhoz", + "privileges.copy-group-privileges-to-all-categories": "A kategória jogosultságainak másolása minden kategóriához.", + "privileges.copy-group-privileges-from": "Másold be egy másik kategória jogosultságait ehhez a kategóriához.", + "privileges.inherit": "Ha a registered-users csoportnak speciális jogosultságok vannak beállítva, akkor minden csoport automatikusan megkapja függetlenül attól, hogy közvetlenül nincs beállítva. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Jogosultságok átmásolva!", + + "analytics.back": "Visszatérés a kategórialistához", + "analytics.title": "Elemzés a \"%1\" kategóriához", + "analytics.pageviews-hourly": "Ábra 1 – Óránkénti oldal megtekintések ehhez a kategóriához", + "analytics.pageviews-daily": "Ábra 2 – Napi oldal megtekintések ehhez a kategóriához", + "analytics.topics-daily": "Ábra 3 – Napi témakörök létrehozva ebben a kategóriában", + "analytics.posts-daily": "Ábra 4 – Napi hozzászólások létrehozva ebben a kategóriában", + + "alert.created": "Létrehozva", + "alert.create-success": "Kategória sikeresen létrehozva!", + "alert.none-active": "Nincsenek aktív kategóriáid.", + "alert.create": "Kategória létrehozása", + "alert.confirm-purge": "

Biztosan szeretnéd teljesen törölni ezt a kategóriát \"%1\"?

Figyelem! Minden témakör és hozzászólás teljesen törlésre kerül ebben a kategóriában!

Egy kategória teljes törlése eltávolítja a témaköröket és hozzászólásokat, valamint törli a kategóriát az adatbázisból. Amennyiben szeretnél egy kategóriát ideiglenesen törölni, használd a kategória \"kikapcsolása\" funkciót.

", + "alert.purge-success": "Kategória törölve!", + "alert.copy-success": "Beállítások másolva!", + "alert.set-parent-category": "Szülő kategória beállítása", + "alert.updated": "Kategóriák frissítve", + "alert.updated-success": "A kategória azonosítók %1 sikeresen frissítve.", + "alert.upload-image": "Kategóriakép feltöltése", + "alert.find-user": "Felhasználó keresése", + "alert.user-search": "Itt megkereshetsz egy felhasználót...", + "alert.find-group": "Csoport keresése", + "alert.group-search": "Itt megkereshetsz egy csoportot...", + "alert.not-enough-whitelisted-tags": "Kevesebb engedélyezett címke van, mint amennyi a minimum. Hozz létre több engedélyezett címkét!", + "collapse-all": "Mind összecsukása", + "expand-all": "Mind kibontása", + "disable-on-create": "Letiltás létrehozás után", + "no-matches": "Nincs egyezés" +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/digest.json b/public/language/hu/admin/manage/digest.json new file mode 100644 index 0000000000..dced3f927a --- /dev/null +++ b/public/language/hu/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Alább láthatóak az összefoglalók kézbesítési statisztikáinak és idejeinek listája", + "disclaimer": "Kérjük vegye figyelembe, hogy az email kézbesítés nem garantált az email technológia természete miatt. Sok változó tényező befolyásolja, hogy egy email elküldése a címzettnek végül valóban sikeresen megérkezik-e a felhasználó postaládájába. Ide értendő a szerver ismertsége, a feketelistázott IP címek és hogy a DKIM/SPD/DMARC konfigurált-e.", + "disclaimer-continued": "A sikeres kézbesítést azt jelenti, hogy az üzenet elküldésre került a NodeBB által és a címzett szerver azt tudomásul vette. Azt nem jelenti, hogy az email megérkezett a felhasználó postafiókjába. A legjobb eredmény elérésének érdekében ajánljuk harmadik féltől származó email kézbesítő rendszer használatát, mint például a SendGrid.", + + "user": "Felhasználó", + "subscription": "Feliratkozás típusa", + "last-delivery": "Utolsó sikeres kézbesítés", + "default": "Rendszer alapértelmezés", + "default-help": "Rendszer alapértelmezés azt jelenti, hogy a felhasználó nem írta felül a globális fórum beállítást, ami jelenleg az alábbi: "%1"", + "resend": "Összefoglaló újra küldése", + "resend-all-confirm": "Biztosan szeretnéd manuálisan futtatni ezt az összefoglalást?", + "resent-single": "Manuális összefoglaló küldés sikeres", + "resent-day": "Napi összefoglaló újraküldés sikeres", + "resent-week": "Heti összefoglaló újraküldés sikeres", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Havi összefoglaló újraküldés sikeres", + "null": "Soha", + "manual-run": "Manuális összefoglaló futtatás:", + + "no-delivery-data": "Nincs elérhető kézbesítési információ" +} diff --git a/public/language/hu/admin/manage/groups.json b/public/language/hu/admin/manage/groups.json new file mode 100644 index 0000000000..7a921cafb9 --- /dev/null +++ b/public/language/hu/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Csoport neve", + "badge": "Jelvény", + "properties": "Tulajdonságok", + "description": "Csoport leírása", + "member-count": "Tagok száma", + "system": "Rendszer", + "hidden": "Rejtett", + "private": "Privát", + "edit": "Szerkesztés", + "delete": "Törlés", + "privileges": "Jogosultságok", + "download-csv": "CSV", + "search-placeholder": "Keresés", + "create": "Csoport létrehozása", + "description-placeholder": "A csoport rövid leírása", + "create-button": "Létrehozás", + + "alerts.create-failure": "Aj-aj

Hiba lépett fel a csoport létrehozása közben. Kérjük próbáld újra később!

", + "alerts.confirm-delete": "Biztosan törölni szeretnéd ezt a csoportot?", + + "edit.name": "Név", + "edit.description": "Leírás", + "edit.user-title": "Megjelenítés a felhasználóknál", + "edit.icon": "Csoport ikonja", + "edit.label-color": "Csoport háttérszíne", + "edit.text-color": "Csoport szöveg színe", + "edit.show-badge": "Jelvény megjelenítése", + "edit.private-details": "Ha engedélyezett, a csoportba való csatlakozást a csoport tulajdonosának engedélyeznie kell.", + "edit.private-override": "Figyelmeztetés: Privát csoportok rendszer szinten le lettek tiltva, ami felülírja ezt a beállítást.", + "edit.disable-join": "Csatlakozási kérelmek letiltása", + "edit.disable-leave": "Felhasználók csoportból kilépésének letiltása", + "edit.hidden": "Rejtett", + "edit.hidden-details": "Ha engedélyezett, akkor ez a csoport nem látszódik a csoportok listájában és a felhasználók meghívása a csoportba csatlakozásra csak kézzel lehetséges.", + "edit.add-user": "Felhasználó hozzáadása a csoporthoz", + "edit.add-user-search": "Felhasználók keresése", + "edit.members": "Tagok listája", + "control-panel": "Csoport vezérlőpult", + "revert": "Visszavonás", + + "edit.no-users-found": "Nem található felhasználó", + "edit.confirm-remove-user": "Biztosan eltávolítod ezt a felhasználót?", + "edit.save-success": "Változtatások mentve!" +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/privileges.json b/public/language/hu/admin/manage/privileges.json new file mode 100644 index 0000000000..23c048a409 --- /dev/null +++ b/public/language/hu/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Globális", + "admin": "Adminisztrátor", + "group-privileges": "Csoport jogosultságok", + "user-privileges": "Felhasználói jogosultságok", + "edit-privileges": "Jogosultságok szerkesztése", + "select-clear-all": "Mind kiválasztása/törlése", + "chat": "Csevegés", + "upload-images": "Képek feltöltése", + "upload-files": "Fájlok feltöltése", + "signature": "Aláírás", + "ban": "Kitiltás", + "mute": "Mute", + "invite": "Meghívás", + "search-content": "Tartalom keresése", + "search-users": "Felhasználó keresése", + "search-tags": "Címke keresése", + "view-users": "Felhasználók megtekintése", + "view-tags": "Címkék megtekintése", + "view-groups": "Csoportok megtekintése", + "allow-local-login": "Helyi bejelentkezés", + "allow-group-creation": "Csoport létrehozása", + "view-users-info": "Felhasználói információk megtekintése", + "find-category": "Kategória keresése", + "access-category": "Hozzáférés kategóriához", + "access-topics": "Hozzáférés témákhoz", + "create-topics": "Téma létrehozása", + "reply-to-topics": "Hozzászólás témához", + "schedule-topics": "Témakörök időzítése", + "tag-topics": "Téma címke hozzáadása", + "edit-posts": "Bejegyzés szerkesztése", + "view-edit-history": "Szerkesztési előzmény megtekintése", + "delete-posts": "Bejegyzés törlése", + "view_deleted": "Törölt bejegyzések megtekintése", + "upvote-posts": "Bejegyzés kedvelése", + "downvote-posts": "Bejegyzés nem kedvelése", + "delete-topics": "Téma törlése", + "purge": "Teljes törlés", + "moderate": "Moderáció", + "admin-dashboard": "Vezérlőpult", + "admin-categories": "Kategóriák", + "admin-privileges": "Jogosultságok", + "admin-users": "Felhasználók", + "admin-admins-mods": "Adminisztrátorok és moderátorok", + "admin-groups": "Csoportok", + "admin-tags": "Címkék", + "admin-settings": "Beállítások", + + "alert.confirm-moderate": "Biztosan beállítod ezeket a moderációs jogosultságokat ennek a csoportnak? Ez a csoport nyilvános, a felhasználók szabadon beléphetnek.", + "alert.confirm-admins-mods": "Biztosan beállítod az "Adminisztrátorok és moderátorok" jogosultságot ennek a felhasználónak/csoportnak? Felhasználók ezzel a jogosultsággal képesek más felhasználókat különböző jogosultságokkal felruházni, beleértve a szuper adminisztrátort is", + "alert.confirm-save": "Erősítsd meg, hogy frissíteni szeretnéd ezeket a jogosultságokat", + "alert.saved": "Jogosultságok módosításai mentésre kerültek", + "alert.confirm-discard": "Biztosan el szeretnéd vetni ezeket a jogosultsági módosításokat?", + "alert.discarded": "Jogosultságok módosításai elvetve", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "Ez a művelet nem vonható vissza.", + "alert.admin-warning": "Az adminisztrátorok automatikusan megkapnak minden jogosultságot", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/registration.json b/public/language/hu/admin/manage/registration.json new file mode 100644 index 0000000000..d10ebc752e --- /dev/null +++ b/public/language/hu/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Várólista", + "description": "Nincs felhasználó a regisztrációs várólistán.
A funkció engedélyezéséhez menj a Beállítások → Felhasználók → Felhasználó regisztráció menüpontba és a Regisztráció típusa lehetőségnél válaszd az \"Adminisztrátori jóváhagyás\"-t.", + + "list.name": "Név", + "list.email": "Email", + "list.ip": "IP", + "list.time": "idő", + "list.username-spam": "Gyakoriság: %1 Megjelenik: %2 Magabiztosság: %3", + "list.email-spam": "Gyakoriság: %1 Megjelenik: %2", + "list.ip-spam": "Gyakoriság: %1 Megjelenik: %2", + + "invitations": "Meghívások", + "invitations.description": "Alább a teljes lista a kiküldött meghívókról. Használd a ctrl-f billentyűkombinációt hogy keress az email címek és felhasználónevek között.

Az email címek mellett látható felhasználónév azoknál jelenik meg, akik már felhasználták a meghívójukat.", + "invitations.inviter-username": "Meghívó felhasználóneve", + "invitations.invitee-email": "Meghívott email címe", + "invitations.invitee-username": "Meghívott felhasználóneve (ha regisztrált)", + + "invitations.confirm-delete": "Biztosan törölni szeretnéd ezt a meghívót?" +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/tags.json b/public/language/hu/admin/manage/tags.json new file mode 100644 index 0000000000..c28d1e9b82 --- /dev/null +++ b/public/language/hu/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Fórumod még nem rendelkezik egy címkékkel ellátott témakörrel sem.", + "bg-color": "Háttérszín", + "text-color": "Szövegszín", + "description": "Válassz ki címkéket kattintással vagy húzással, használd a CTRL-t több cí>mke kiválasztásához.", + "create": "Címke létrehozása", + "modify": "Címkék módosítása", + "rename": "Címkék átnevezése", + "delete": "Kijelölt címkék törlése", + "search": "Címkék keresése...", + "settings": "Címkék beállításai", + "name": "Címke neve", + + "alerts.editing": "Címkék szerkesztése", + "alerts.confirm-delete": "Biztosan törölni akarod a kijelölt címkéket?", + "alerts.update-success": "Címke frissítve!", + "reset-colors": "Színek visszaállítása" +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/uploads.json b/public/language/hu/admin/manage/uploads.json new file mode 100644 index 0000000000..4a047ee60a --- /dev/null +++ b/public/language/hu/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Fájl feltöltése", + "filename": "Fájlnév", + "usage": "Használat hozzászólásban", + "orphaned": "Elárvult", + "size/filecount": "Méret / fájlok száma", + "confirm-delete": "Biztosan törölni szeretnéd ezt a fájlt?", + "filecount": "%1 fájl", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/hu/admin/manage/users.json b/public/language/hu/admin/manage/users.json new file mode 100644 index 0000000000..3eacbead0b --- /dev/null +++ b/public/language/hu/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Felhasználók", + "edit": "Actions", + "make-admin": "Adminná léptetés", + "remove-admin": "Admin törlése", + "validate-email": "Email érvényesítése", + "send-validation-email": "Érvényesítő email küldése", + "password-reset-email": "Jelszó helyreállító email küldése", + "force-password-reset": "Felhasználó jelszavának helyreállítása és kijelentkeztetése", + "ban": "Felhasználó(k) kitiltása", + "temp-ban": "Felhasználó(k) kitiltása átmenetileg", + "unban": "Felhasználó(k) kitiltásának feloldása", + "reset-lockout": "Kizárás visszaállítása", + "reset-flags": "Megjelölések visszaállíátsa", + "delete": "Felhasználó(k) törlése", + "delete-content": "Felhasználó(k) minden tartalmának törlése", + "purge": "Felhasználó(k) és minden tartalmának törlése", + "download-csv": "CSV letöltése", + "manage-groups": "Csoportok kezelése", + "add-group": "Csoport létrehozása", + "create": "Create User", + "invite": "Invite by Email", + "new": "Új felhasználó", + "filter-by": "Szűrés", + "pills.unvalidated": "Nem ellenőrzöttek", + "pills.validated": "Ellenőrzöttek", + "pills.banned": "Kitiltottak", + + "50-per-page": "50 oldalanként", + "100-per-page": "100 oldalanként", + "250-per-page": "250 oldalanként", + "500-per-page": "500 oldalanként", + + "search.uid": "Azonosító alapján", + "search.uid-placeholder": "Írj be egy azonosítót a kereséshez", + "search.username": "Felhasználónév szerint", + "search.username-placeholder": "Írj be egy felhasználónevet a kereséshez", + "search.email": "Email szerint", + "search.email-placeholder": "Írj be egy email-t a kereséshez", + "search.ip": "IP-cím szerint", + "search.ip-placeholder": "Írj be egy IP-címet a kereséshez", + "search.not-found": "Nem található felhasználó!", + + "inactive.3-months": "3 hónap", + "inactive.6-months": "6 hónap", + "inactive.12-months": "12 hónap", + + "users.uid": "uid", + "users.username": "felhasználónév", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "hozzászólások száma", + "users.reputation": "hírnév", + "users.flags": "megjelölések", + "users.joined": "csatlakozott", + "users.last-online": "legutóbb online", + "users.banned": "kitiltva", + + "create.username": "Felhasználónév", + "create.email": "Email", + "create.email-placeholder": "A felhasználó emailje", + "create.password": "Jelszó", + "create.password-confirm": "Jelszó megerősítése", + + "temp-ban.length": "Length", + "temp-ban.reason": "Indok (Nem kötelező)", + "temp-ban.hours": "Óra", + "temp-ban.days": "Nap", + "temp-ban.explanation": "Add meg a kitiltás idejének hosszát. Vedd figyelembe, hogy a 0 végleges kitiltásnak minősül.", + + "alerts.confirm-ban": "Biztosan ki szeretnéd tiltani ezt a felhasználót örökre?", + "alerts.confirm-ban-multi": "Biztosan ki szeretnéd tiltani ezt a felhasználókat örökre?", + "alerts.ban-success": "Felhasználó(k) kitiltva!", + "alerts.button-ban-x": "%1 felhasználó kitiltása", + "alerts.unban-success": "Felhasználó(k) kitiltása feloldva!", + "alerts.lockout-reset-success": "Kizárás(ok) visszaállítva!", + "alerts.flag-reset-success": "Megjelölés(ek) visszaállítva!", + "alerts.no-remove-yourself-admin": "Adminisztrátorként nem távolíthatod el saját magad!", + "alerts.make-admin-success": "A felhasználó mostmár adminisztrátor.", + "alerts.confirm-remove-admin": "Biztosan szeretnéd eltávolítani ezt az adminisztrátort?", + "alerts.remove-admin-success": "A felhasználó többé már nem adminisztrátor.", + "alerts.make-global-mod-success": "A felhasználó mostmár globális moderátor.", + "alerts.confirm-remove-global-mod": "Biztosan szeretnéd eltávolítani ezt a globális moderátort?", + "alerts.remove-global-mod-success": "A felhasználó többé már nem globális moderátor.", + "alerts.make-moderator-success": "A felhasználó mostmár moderátor.", + "alerts.confirm-remove-moderator": "Biztosan szeretnéd eltávolítani ezt a moderátort?", + "alerts.remove-moderator-success": "A felhasználó többé már nem moderátor.", + "alerts.confirm-validate-email": "Biztosan szeretnéd megerősíteni ezen felhasználó(k) email címét?", + "alerts.confirm-force-password-reset": "Biztosan szeretnéd kényszeríteni jelszó változtatásra ezen felhasználó(ka)t és kijelentkeztetni?", + "alerts.validate-email-success": "Email cím(ek) megerősítve", + "alerts.validate-force-password-reset-success": "Felhasználó(k) jelszava törölve és a hozzá(juk) tartozó minden munkamenet érvénytelenítve.", + "alerts.password-reset-confirm": "Szeretnél jelszó módosítási emailt küldeni ezen felhasználó(k)nak?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Figyelem!

Biztosan szeretnéd törölni ezen felhasználó(ka)-t?

Ez a művelet nem visszavonható! Csak a felhasználó(k) fiókja kerül törlésre, a témakörök és hozzászólások nem.

", + "alerts.delete-success": "Felhasználó(k) törölve!", + "alerts.confirm-delete-content": "Figyelem!

Biztosan szeretnéd törölni ezen felhasználó(k) minden tartalmát?

Ez a művelet nem visszavonható! A felhasználó(k) fiókja megmarad, csak a témakörök és hozzászólások törlődnek.

", + "alerts.delete-content-success": "Felhasználó(k) minden tartalma törölve!", + "alerts.confirm-purge": "Figyelem!

Biztosan szeretnéd törölni ezen felhasználó(ka)t és a hozzá(juk) tartozó összes tartalmat?

Ez a művelet nem visszavonható! Minden felhasználói információ és tartalom törlésre kerül!

", + "alerts.create": "Felhasználó létrehozása", + "alerts.button-create": "Létrehozás", + "alerts.button-cancel": "Mégse", + "alerts.error-passwords-different": "A jelszavaknak egyezniük kell!", + "alerts.error-x": "Hiba

%1

", + "alerts.create-success": "Felhasználó létrehozva!", + + "alerts.prompt-email": "Emailek: ", + "alerts.email-sent-to": "Meghívó email elküldve %1 részére", + "alerts.x-users-found": "%1 talált felhasználó (%2 másodperc)", + "export-users-started": "Felhasználók exportálása CSV formátumban. Ez eltarthat egy darabig. Értesítést fogsz kapni, ha elkészült.", + "export-users-completed": "Felhasználók exportálva CSV formátumban, kattints ide a letöltéshez." +} \ No newline at end of file diff --git a/public/language/hu/admin/menu.json b/public/language/hu/admin/menu.json new file mode 100644 index 0000000000..b3340a00da --- /dev/null +++ b/public/language/hu/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Vezérlőpultok", + "dashboard/overview": "Áttekintés", + "dashboard/logins": "Bejelentkezések", + "dashboard/users": "Felhasználók", + "dashboard/topics": "Témakörök", + "dashboard/searches": "Searches", + "section-general": "Általános", + + "section-manage": "Kezelés", + "manage/categories": "Kategóriák", + "manage/privileges": "Jogosultságok", + "manage/tags": "Címkék", + "manage/users": "Felhasználók", + "manage/admins-mods": "Adminok & Moderátorok", + "manage/registration": "Regisztrációs várólista", + "manage/post-queue": "Hozzászólási várólista", + "manage/groups": "Csoportok", + "manage/ip-blacklist": "IP tiltólista", + "manage/uploads": "Feltöltések", + "manage/digest": "Összefoglalók", + + "section-settings": "Beállítások", + "settings/general": "Általános", + "settings/homepage": "Főoldal", + "settings/navigation": "Navigáció", + "settings/reputation": "Hírnév és jelölések", + "settings/email": "E-mail", + "settings/user": "Felhasználók", + "settings/group": "Csoportok", + "settings/guest": "Vendégek", + "settings/uploads": "Feltöltések", + "settings/languages": "Nyelvek", + "settings/post": "Hozzászólások", + "settings/chat": "Csevegések", + "settings/pagination": "Lapszámozás", + "settings/tags": "Címkék", + "settings/notifications": "Értesítések", + "settings/api": "API hozzáférés", + "settings/sounds": "Hangok", + "settings/social": "Közösség", + "settings/cookies": "Süti", + "settings/web-crawler": "Web indexelő", + "settings/sockets": "Csatlakozók", + "settings/advanced": "Haladó", + + "settings.page-title": "%1 Beállítások", + + "section-appearance": "Megjelenés", + "appearance/themes": "Témák", + "appearance/skins": "Téma variációk", + "appearance/customise": "Egyéni tartalom (HTML/JS/CSS)", + + "section-extend": "Bővítés", + "extend/plugins": "Beépülők", + "extend/widgets": "Modulok", + "extend/rewards": "Jutalmak", + + "section-social-auth": "Közösségi hitelesítés", + + "section-plugins": "Beépülők", + "extend/plugins.install": "Beépülők telepítése", + + "section-advanced": "Haladó", + "advanced/database": "Adatbázis", + "advanced/events": "Események", + "advanced/hooks": "Hook-ok", + "advanced/logs": "Naplók", + "advanced/errors": "Hibák", + "advanced/cache": "Gyorsítótár", + "development/logger": "Naplózó", + "development/info": "Információ", + + "rebuild-and-restart-forum": "Fórum újraépítése & újraindítása", + "restart-forum": "Fórum újraindítása", + "logout": "Kilépés", + "view-forum": "Fórum megtekintése", + + "search.placeholder": "Search settings", + "search.no-results": "Nincs eredmény...", + "search.search-forum": " keresése a fórumon", + "search.keep-typing": "Gépelj többet az eredményekért...", + "search.start-typing": "Kezdj el gépelni az eredményekért...", + + "connection-lost": "A kapcsolat megszakadt a következővel: %1, kísérlet az újracsatlakozásra...", + + "alerts.version": "A NodeBB v%1 verziója fut", + "alerts.upgrade": "Frissítés v%1 verzióra" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/advanced.json b/public/language/hu/admin/settings/advanced.json new file mode 100644 index 0000000000..24a7537fe3 --- /dev/null +++ b/public/language/hu/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Karbantartási mód", + "maintenance-mode.help": "Amikor a fórum karbantartási módban van, minden kérés átirányításra kerül egy statikus feltartóztató oldalra. Az adminisztrátorok kivételnek számítanak ez alól, és szokásos módon hozzáférhetnek az oldalhoz.", + "maintenance-mode.status": "Karbantartási mód HTTP státuszkódja", + "maintenance-mode.message": "A karbantartás üzenete", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Fejlécek", + "headers.allow-from": "ALLOW-FROM beállítása, hogy a NodeBB egy iFrame-be kerüljön", + "headers.csp-frame-ancestors": "Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(alapértelmezett) vagy engedélyezett URI-k listája.", + "headers.powered-by": "NodeBB által küldött \\\"Powered By\\\" fejléc testreszabása", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin reguláris kifejezés", + "headers.acao-help": "Hagyd üresen, hogy letiltsd a hozzáférést az összes oldalhoz", + "headers.acao-regex-help": "Adj meg reguláris kifejezéseket a dinamikus forrás beállításához. Hagyd üresen, hogy letiltsd a hozzáférést az összes oldalhoz", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Szigorú HTTP biztonság (HSTS)", + "hsts.enabled": "Szigorú HTTP biztonság (HSTS) bekapcsolása (ajánlott)", + "hsts.maxAge": "HSTS maximális kora", + "hsts.subdomains": "Aldomain-ek hozzáadása a HSTS fejléchez", + "hsts.preload": "HSTS fejléc előtöltésének engedélyezése", + "hsts.help": "Ha engedélyezett, akkor HSTS fejléc kerül beállításra ehhez az oldalhoz. Eldöntheted, hogy szeretnél-e aldomain-eket és előtöltési flag-eket hozzáadni a fejléchez. Ha nem vagy biztos ebben, akkor inkább hagyd őket érintetlenül. További információk ", + "traffic-management": "Forgalomirányítás", + "traffic.help": "A NodeBB egy modult használ a kérelmek automatikus elutasítására nagy terhelés esetén. Itt testreszabhatod ennek a beállításait, azonban az alapértelmezett értékek jó kiindulási pontok.", + "traffic.enable": "Forgalomirányítás engedélyezése", + "traffic.event-lag": "Eseményciklus késési küszübértéke (ezredmásodpercben)", + "traffic.event-lag-help": "Az érték csökkentésével csökken az oldalbetöltések várakozási ideje, ám több felhasználó fogja tapasztalni a \"túlzott terhelés\" üzenetet. (Újraindítást igényel)", + "traffic.lag-check-interval": "Ellenőrzési időköz (ezredmásodpercben)", + "traffic.lag-check-interval-help": "Az érték csökkentésével a NodeBB érzékenyebbé válik a tüskékkel szemben terheléskor, ám az ellenőrzés túlérzékenységét is okozhatja. (Újraindítást igényel)", + + "sockets.settings": "WebSocket beállítások", + "sockets.max-attempts": "Újracsatlakozási kísérletek maximális száma", + "sockets.default-placeholder": "Alapértelmezett: %1", + "sockets.delay": "Újracsatlakozási késleltetés", + + "analytics.settings": "Analitikai beállítások", + "analytics.max-cache": "Analitikai gyorsítótár maximális értéke", + "analytics.max-cache-help": "Nagy forgalmú telepítéseknél ez a gyorsítótár hamar megtelik, ha az oldalt egyszerrre használó felhasználók száma nagyobb mint a gyorsítótár maximális értéke. (Újraindítás szükséges)", + "compression.settings": "Tömörítési beállítások", + "compression.enable": "Tömörítés bekapcsolása", + "compression.help": "Ez a beállítás engedélyezi a gzip tömörítést. Magas forgalmú weboldal esetén éles környezetbe a legjobb megoldás, ha a reverse proxy szintjén történik ez meg. Bekapcsolhatod kipróbálási céllal." +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/api.json b/public/language/hu/admin/settings/api.json new file mode 100644 index 0000000000..ec8ad3ad4c --- /dev/null +++ b/public/language/hu/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokenek", + "settings": "Beállítások", + "lead-text": "Ezen az oldalon beállíthatod a NodeBB Write API-jának hozzáférését.", + "intro": "Alapértelmezetten a Write API a felhasználókat a munkamenet sütijükön keresztül authentikálja, azonban a NodeBB támogatja a Bearer authentikációt tokeneken keresztül, amiket ezen az oldalon generálhatsz.", + "docs": "Kattints ide a teljes API specifikáció megtekintéséhez", + + "require-https": "API használat korlátozása HTTPS protokollra", + "require-https-caveat": "Megjegyzés: Némely telepítések terherléselosztás miatt a kéréseket a NodeBB felé HTTP-n keresztül továbbítják. Ilyen esetekben ezt a beállítást nem szabad bekapcsolni.", + + "uid": "Felhasználó azonosító", + "uid-help-text": "Adj meg egy felhasználó azonosítót, hogy társítsd ehhez a tokenhez. Ha a felhasználó azonosító 0, akkor mester tokenként lesz kezelve, ami bármelyik felhasználó identitását képes felvenni a _uid paraméter alapján", + "description": "Leírás", + "no-description": "Nincs leírás megadva.", + "token-on-save": "A token az űrlap mentésekor generálódik" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/chat.json b/public/language/hu/admin/settings/chat.json new file mode 100644 index 0000000000..7565f0dd5a --- /dev/null +++ b/public/language/hu/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Csevegési beállítások", + "disable": "Csevegés letiltása", + "disable-editing": "Csevegési üzenetek szerkesztésének/törlésének letiltása", + "disable-editing-help": "Az adminisztrátorok és globális moderátorok kivételnek számítanak ezen korlátozás alól", + "max-length": "Csevegési üzenetek maximális hossza", + "max-room-size": "A csevegési szobákban lévő felhasználók maximális száma", + "delay": "Csevegési üzenetek közötti idő ezredmásodpercben", + "notification-delay": "Értesítési késleltetés csevegési üzenetekhez. (0: nincs késleltetés)", + "restrictions.seconds-edit-after": "Hány másodpercig marad szerkeszthető egy üzenet az elküldése után. (0: nincs korlátozás)", + "restrictions.seconds-delete-after": "Hány másodpercig marad törölhető egy üzenet az elküldése után. (0: nincs korlátozás)" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/cookies.json b/public/language/hu/admin/settings/cookies.json new file mode 100644 index 0000000000..90324dc554 --- /dev/null +++ b/public/language/hu/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU hozzájárulás", + "consent.enabled": "Engedélyezve", + "consent.message": "Értesítés szövege", + "consent.acceptance": "Elfogadás szövege", + "consent.link-text": "Irányelv link felirata", + "consent.link-url": "Irányelv link URL-je", + "consent.blank-localised-default": "Hagyd üresen a NodeBB alapértelmezett használatához (lefordított)", + "settings": "Beállítások", + "cookie-domain": "Munkamenet süti domain", + "max-user-sessions": "Aktív munkamenetek maximális száma felhasználónként", + "blank-default": "Hagyd üresen az alapértelmezett beállítás használatához" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/email.json b/public/language/hu/admin/settings/email.json new file mode 100644 index 0000000000..f458051251 --- /dev/null +++ b/public/language/hu/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email beállítások", + "address": "Email cím", + "address-help": "Az alábbi email cím lesz az, amit a címzett látni fog a \\\"Feladó\\\" és \\\"Válasz neki\\\" mezőkben.", + "from": "Feladó neve", + "from-help": "Az emailben megjelenített feladói név.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP beállítások", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Válogathatsz a jól ismert szolgáltatások listájából vagy megadhatsz sajátot.", + "smtp-transport.service": "Válassz egy szolgáltatást", + "smtp-transport.service-custom": "Egyedi szolgáltatás", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP kiszolgáló", + "smtp-transport.port": "SMTP port", + "smtp-transport.security": "Kapcoslatbiztonság", + "smtp-transport.security-encrypted": "Titkosított", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Nincs", + "smtp-transport.username": "Felhasználónév", + "smtp-transport.username-help": "A Gmail szolgáltatáshoz, add meg a teljes email címet. Főleg abban az esetben, ha Google Apps által kezelt domain-t használsz.", + "smtp-transport.password": "Jelszó", + "smtp-transport.pool": "Kapcsolat megőrzés engedélyezése", + "smtp-transport.pool-help": "A kapcsolat megőrzés megakadályozza a NodeBB-t abban, hogy minden email-hez új kapcsolatot nyisson. Ez a beállítás csak akkor érvényesül, ha az SMTP engedélyezett.", + + "template": "Email sablon szerkesztése", + "template.select": "Válassz email sablont", + "template.revert": "Eredeti visszaálítása", + "testing": "Email tesztelés", + "testing.select": "Válassz email sablont", + "testing.send": "Teszt email küldése", + "testing.send-help": "A teszt email a most használt felhasználó email címére fog megérkezni.", + "subscriptions": "Email összefoglalások", + "subscriptions.disable": "Minden email összefoglalás letiltása", + "subscriptions.hour": "Összefoglalások küldési időpontja", + "subscriptions.hour-help": "Kérjük adj meg egy számot, ami azt az órát jelenti, amikor az ütemezett összefoglalókat kiküldi a rendszer (0 az éjfél, 17 a délután 5 óra). Tartsd észben, hogy ez az időpont a szerver idejét veszi figyelembe és előfordulhat, hogy nem egyezik meg a Te gépeden jelzett idővel. A becsült szerver idő jelenleg:
A következő napi összefoglalás tervezett kiküldési ideje ", + "notifications.remove-images": "Képek eltávolítása az email értesítésekből", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/hu/admin/settings/general.json b/public/language/hu/admin/settings/general.json new file mode 100644 index 0000000000..46beeaf4b7 --- /dev/null +++ b/public/language/hu/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Weboldal beállítások", + "title": "Weboldal címe", + "title.short": "Rövid cím", + "title.short-placeholder": "Ha nincs rövid cím beállítva, akkor a weboldal címét fogjuk használni", + "title.url": "Title Link URL", + "title.url-placeholder": "A weboldal címre kattintáskor megnyitandó URL", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "A közösséged neve", + "title.show-in-header": "A weboldal címének megjelenítése a fejlécben", + "browser-title": "Böngésző cím", + "browser-title-help": "Ha nincs böngésző cím beállítva, akkor a weboldal címét fogjuk használni", + "title-layout": "Cím formátuma", + "title-layout-help": "Add meg, hogy a böngésző cím hogyan épüljön fel. Pl.: {pageTitle} | {browserTitle}", + "description.placeholder": "A közösséged rövid leírása", + "description": "Weboldal leírása", + "keywords": "Weboldal kulcsszavak", + "keywords-placeholder": "A közösségedet leíró kulcsszavak, vesszővel elválasztva", + "logo": "Weboldal logó", + "logo.image": "Kép", + "logo.image-placeholder": "A logó elérési útvonala, amit a fórum fejlécében fogunk megjeleníteni", + "logo.upload": "Feltöltés", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "A weboldal logójának URL-je", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt szöveg", + "log.alt-text-placeholder": "Alternatív szöveg", + "favicon": "Favicon", + "favicon.upload": "Feltöltés", + "pwa": "Progressive Web App", + "touch-icon": "Alkalmazás ikon", + "touch-icon.upload": "Feltöltés", + "touch-icon.help": "Ajánlott méret és formátum: 512x512, csak PNG formátum. Ha nincs beállítva, a NodeBB a favicon-t fogja használni.", + "maskable-icon": "Maszkolható (főképernyő) ikon", + "maskable-icon.help": "Ajánlott méret és formátum: 512x512, csak PNG formátum. Ha nincs beállítva, a NodeBB a favicon-t fogja használni", + "outgoing-links": "Kimenő linkek", + "outgoing-links.warning-page": "Kimenő link figyelmeztető oldal használata", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domain-ek, amik figyelmen kívül hagyják a kimenő link figyelmeztető oldalt", + "site-colors": "Weboldal szín metainformáció", + "theme-color": "Téma színe", + "background-color": "Háttér színe", + "background-color-help": "A szín ami akkor jelenik meg alkalmazás indulásnál, ha a weboldal egy okostelefonon PWA-ként van telepítve", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/hu/admin/settings/group.json b/public/language/hu/admin/settings/group.json new file mode 100644 index 0000000000..9cf8084e93 --- /dev/null +++ b/public/language/hu/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Általános", + "private-groups": "Privát csoportok", + "private-groups.help": "Ha engedélyezve van, a csoporthoz való csatlakozáshoz szükség van a csoport tulajdonosának jóváhagyására (Alapértelmezett: engedélyezve)", + "private-groups.warning": "Vigyázat! Ha ez a lehetőség le van tiltva, és vannak privát csoportjaid, azok automatikusan nyilvánosak lesznek.", + "allow-multiple-badges": "Több jelvény megjelenítésének engedélyezése", + "allow-multiple-badges-help": "Ez a beállítás engedélyezi a felhasználóknak, hogy több csoport jelvényt választhassanak. Téma támogatást igényel.", + "max-name-length": "Maximális csoport név hossz", + "max-title-length": "Maximuális csoport cím hossza", + "cover-image": "Csoport borítókép", + "default-cover": "Alapértelmezett borítóképek", + "default-cover-help": "Alapértelmezett borítóképek hozzáadása vesszővel elválasztva olyan csoportokhoz, amelyeknek nincs feltöltött borítóképük." +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/guest.json b/public/language/hu/admin/settings/guest.json new file mode 100644 index 0000000000..07b9740854 --- /dev/null +++ b/public/language/hu/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Beállítások", + "handles.enabled": "Vendég név beállítás engedélyezése", + "handles.enabled-help": "Ez a beállítás engedélyez egy új mezőt, amivel a vendégek minden hozzászólásnál választhatnak egy nevet ami megjelenik ott. Ha nincs engedélyezve, egyszerűen \"Vendég\"-ként jelennek meg.", + "topic-views.enabled": "Témakör látogatások számának növelésének engedélyezése vendégek számára", + "reply-notifications.enabled": "Válasz értesítések generálásának engedélyezése vendégek számára" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/homepage.json b/public/language/hu/admin/settings/homepage.json new file mode 100644 index 0000000000..91b0287147 --- /dev/null +++ b/public/language/hu/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Főoldal", + "description": "Válaszd ki milyen oldal jelenjen meg, amikor a felhasználók fórumod gyökér URL címéhez navigálnak.", + "home-page-route": "Főoldal útvonala", + "custom-route": "Egyéni útvonal", + "allow-user-home-pages": "Felhasználói főoldalak engedélyezése", + "home-page-title": "A főoldal címe (alapértelmezés \"Kezdőlap\")" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/languages.json b/public/language/hu/admin/settings/languages.json new file mode 100644 index 0000000000..63de413af4 --- /dev/null +++ b/public/language/hu/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Nyelvi beállítások", + "description": "Az alapértelmezett nyelv meghatározza a nyelvi beállításokat minden fórumot látogató számára.
Ezt az egyes felhasználók felülírhatják fiókjuk beállításaiban.", + "default-language": "Alapértelmezett nyelv", + "auto-detect": "Nyelvi beállítás automatikus észlelése vendégeknek" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/navigation.json b/public/language/hu/admin/settings/navigation.json new file mode 100644 index 0000000000..704bea5c2a --- /dev/null +++ b/public/language/hu/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ikon:", + "change-icon": "módosítás", + "route": "Útvonal:", + "tooltip": "Elemleírás:", + "text": "Szöveg:", + "text-class": "CSS osztály: nem kötelező", + "class": "Osztály: nem kötelező", + "id": "HTML azonosító: nem kötelező", + + "properties": "Tulajdonságok:", + "groups": "Csoportok:", + "open-new-window": "Megnyitás új ablakban", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Törlés", + "btn.disable": "Tiltás", + "btn.enable": "Engedélyezés", + + "available-menu-items": "Rendelkezésre álló menüelemek", + "custom-route": "Egyéni útvonal", + "core": "alapvető", + "plugin": "beépülő" +} diff --git a/public/language/hu/admin/settings/notifications.json b/public/language/hu/admin/settings/notifications.json new file mode 100644 index 0000000000..1e19679d4a --- /dev/null +++ b/public/language/hu/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Értesítések", + "welcome-notification": "Üdvözlő értesítés", + "welcome-notification-link": "Üdvözlő értesítés linkje", + "welcome-notification-uid": "Felhasználói üdvözlő értesítés (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/pagination.json b/public/language/hu/admin/settings/pagination.json new file mode 100644 index 0000000000..ea38cdf25b --- /dev/null +++ b/public/language/hu/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Lapozási beállítások", + "enable": "Témakörök és hozzászólások lapozása végtelen görgetés helyett.", + "posts": "Hozzászólás lapozás", + "topics": "Témakör lapozás", + "posts-per-page": "Hozzászólások oldalanként", + "max-posts-per-page": "Hhozzászólások maximális száma oldalanként", + "categories": "Kategória lapozás", + "topics-per-page": "Témakörök oldalanként", + "max-topics-per-page": "Témakörök maximális száma oldalanként", + "categories-per-page": "Kategóriák oldalanként" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/post.json b/public/language/hu/admin/settings/post.json new file mode 100644 index 0000000000..f46f0a12af --- /dev/null +++ b/public/language/hu/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Hozzászólások rendezése", + "sorting.post-default": "Alapértelmezett rendezés", + "sorting.oldest-to-newest": "Régebbitől az újabb felé", + "sorting.newest-to-oldest": "Újabbtól a régebbi felé", + "sorting.most-votes": "Legtöbb szavazat", + "sorting.most-posts": "Legtöbb hozzászólás", + "sorting.topic-default": "Alapértelmezett témekör rendezés", + "length": "Hozzászólás hossza", + "post-queue": "Hozzászólás várólista", + "restrictions": "Hozzászólás korlátozás", + "restrictions-new": "Új felhasználók korlátozása", + "restrictions.post-queue": "Hozzászólás várólista engedélyezése", + "restrictions.post-queue-rep-threshold": "Milyen hírnév szint szükséges a hozzászólás várólista megkerüléséhez", + "restrictions.groups-exempt-from-post-queue": "Válaszd ki a csoportokat, amiket nem korlátoz a hozzászólás várólista", + "restrictions-new.post-queue": "Új felhasználók korlátozásának bekapcsolása", + "restrictions.post-queue-help": "A hozzászólás várólista engedélyezése az új felhasználók hozzászólásait várólistára teszi és engedélyezés szükséges a megjelenítése előtt", + "restrictions-new.post-queue-help": "Az új felhasználók korlátozásának engedélyezése korlátokat ad meg az új felhasználók által létrehozott hozzászólásokra", + "restrictions.seconds-between": "Hozzászólások közötti kötelező szünet (másodpercben)", + "restrictions.seconds-between-new": "Hozzászólások közötti kötelező szünet új felhasználóknak (másodpercben)", + "restrictions.rep-threshold": "Szükséges hírnév szint ezen korlátozások feloldásához", + "restrictions.seconds-before-new": "Szükséges eltelt idő, mielőtt egy új felhasználó hozzászólást írhat (másodpercben)", + "restrictions.seconds-edit-after": "Hány másodpercig maradjanak a hozzászlóások szerkeszthetőek (0: nincs korlátozás)", + "restrictions.seconds-delete-after": "Hány másodpercig maradjanak a hozzászólások törölhetőek (0: nincs korlátozás)", + "restrictions.replies-no-delete": "Hány hozzászólás után ne törölhessék a felhasználók a saját témaköreiket (0: nincs korlátozás)", + "restrictions.min-title-length": "Cím minimális hossza", + "restrictions.max-title-length": "Cím maximális hossza", + "restrictions.min-post-length": "Hozzászólások minimális hossza", + "restrictions.max-post-length": "Hozzászólások maximális hossza", + "restrictions.days-until-stale": "Hány nap elteltével számítson egy témakör elhagyatottnak", + "restrictions.stale-help": "Ha egy témakör \"elhagyatottnak\" számít, akkor a felhasználók hozzászóláskor egy figyelmeztetést fognak kapni róla.", + "timestamp": "Időbélyegek", + "timestamp.cut-off": "Dátum levágása (napokban)", + "timestamp.cut-off-help": "A dátumok és idők relatív értelemben jelennek meg (pl.: \"3 órával ezelőtt\" / \"5 nappal ezelőtt\") lefordítva a különböző\n\t\t\t\t\tnyelvekre. Egy bizonyos idő elteltével a szöveg a lokalizált dátumot fogja mutatni\n\t\t\t\t\t(pl.: 5 Nov 2016 15:30).
(Alapértelmezett: 30, avagy egy hónap). Állítsd 0-ra, hogy mindig a dátumok jelenjenek meg vagy hagyd üresen, hogy mindig a relatív idők jelenjenek meg.", + "timestamp.necro-threshold": "Tétlenségi küszöb (napokban)", + "timestamp.necro-threshold-help": "Egy üzenet jelenik meg két hozzászólás között, ha az itt megadott értéknél több nap telt el a két hozzászólás között. (Alapértelmezett: 7, avagy egy hét). Ha az érték 0, akkor ez a funkció kikapcsolt.", + "timestamp.topic-views-interval": "Témakör látogatottság növelésének időköze (percben)", + "timestamp.topic-views-interval-help": "A témakör látogatottsága csak akkor növekszik, ha a legutóbbi növekedés óta eltelt X perc.", + "teaser": "Bevezető hozzászólás", + "teaser.last-post": "Utolsó – Utolsó hozzászólás megjelenítése, az eredeti hozzászólást is beleértve, ha nincsenek válaszok", + "teaser.last-reply": "Utolsó – Utolsó válasz vagy, ha nincsenek válaszok, akkor \"Nincs válasz\" szöveg megjelenítése", + "teaser.first": "Első", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Olvasatlansági beállítások", + "unread.cutoff": "Hány napig legyen olvasatlan egy hozzászólás", + "unread.min-track-last": "Hozzászólások minimális száma egy témakörben, mielőtt a legutóbbi olvasás követése elkezdődik", + "recent": "Legutóbbiak beállításai", + "recent.max-topics": "Témakörök maximális száma a /recent oldalon", + "recent.categoryFilter.disable": "Témakörök szűrésének kikapcsolása a figyelmen kívül hagyott kategóriákban a /recent oldalon", + "signature": "Aláírás beállítások", + "signature.disable": "Aláírások kikapcsolása", + "signature.no-links": "Linkek letiltása az aláírásokban", + "signature.no-images": "Képek letiltása az aláírásokban", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Aláírás maximális hossza", + "composer": "Szövegszerkesztő beállításai", + "composer-help": "Az alábbi beállítások a felhasználóknak megjelenített szövegszerkesztő (témakör, hozzászólás vagy válasz írásánál)\n\t\t\t\tfunkcióit és/vagy megjelenését szabályozzák.", + "composer.show-help": "\"Segítség\" panel megjelenítése", + "composer.enable-plugin-help": "Beépülők hozzáadhassanak saját tartalmaz a segítség panelhoz", + "composer.custom-help": "Egyedi szöveg a segítségnél", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP nyomonkövetés", + "ip-tracking.each-post": "IP cím követése minden hozzászólásnál", + "enable-post-history": "Hozzászólás történetiség engedélyezése" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/reputation.json b/public/language/hu/admin/settings/reputation.json new file mode 100644 index 0000000000..7d55bfd5e3 --- /dev/null +++ b/public/language/hu/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Hírnév beállítások", + "disable": "Hírnév rendszer kikapcsolása", + "disable-down-voting": "Leszavazás kikapcsolása", + "votes-are-public": "Minden szavazat nyilvános", + "thresholds": "Aktivitás küszöb értékek", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Szükséges minimális hírnév a leszavazás használatához", + "downvotes-per-day": "Leszavazások naponta (adj meg 0-t, hogy ne legyen korlátozás)", + "downvotes-per-user-per-day": "Leszavazások felhasználónként naponta (adj meg 0-t, hogy ne legyen korlátozás)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Szükséges minimális hírnév hozzászólások megjelöléséhez", + "min-rep-website": "Szükséges minimális hírnév \"weboldal\" megadásához a felhasználói profilon", + "min-rep-aboutme": "Szükséges minimális hírnév \"bemutatkozás\" megadásához a felhasználói profilon", + "min-rep-signature": "Szükséges minimális hírnév \"aláírás\" megadásához a felhasználói profilon", + "min-rep-profile-picture": "Szükséges minimális hírnév \"profilkép\" megadásához a felhasználói profilon", + "min-rep-cover-picture": "Szükséges minimális hírnév \"borítókép\" megadásához a felhasználói profilon", + + "flags": "Megjelölés beállítások", + "flags.limit-per-target": "Bizonyos dolgokat legfeljebb hányszor lehessen megjelölni", + "flags.limit-per-target-placeholder": "Alapértelmezett: 0", + "flags.limit-per-target-help": "Amikor egy hozzászólás vagy felhasználó több megjelölést kap, akkor minden megjelölés "jelentésnek" számít és hozzáadódik az eredeti megjelöléshez. Adj meg 0-tól eltérő értéket egy maximális jelölési szám beállításához.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "A felhasználó összes megjelölésének feloldása, amikor kitiltásra kerül", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/social.json b/public/language/hu/admin/settings/social.json new file mode 100644 index 0000000000..8d68baf6c0 --- /dev/null +++ b/public/language/hu/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Hozzászólások megosztása", + "info-plugins-additional": "Beépülőkkel további hálózatok adhatók hozzá hozzászólások megosztásához.", + "save-success": "Hozzászólás megosztási beállítások sikeresen elmentve!" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/sockets.json b/public/language/hu/admin/settings/sockets.json new file mode 100644 index 0000000000..0b6c196ede --- /dev/null +++ b/public/language/hu/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Újracsatlakozási beállítások", + "max-attempts": "Újracsatlakozási próbálkozások maximális száma", + "default-placeholder": "Alapértelmezett: %1", + "delay": "Újracsatlakozási késleltetés" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/sounds.json b/public/language/hu/admin/settings/sounds.json new file mode 100644 index 0000000000..fc9943fc82 --- /dev/null +++ b/public/language/hu/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Értesítések", + "chat-messages": "Chat üzenetek", + "play-sound": "Lejátszás", + "incoming-message": "Bejövő üzenet", + "outgoing-message": "Kimenő üzenet", + "upload-new-sound": "Új hang feltöltése", + "saved": "Beállítások elmentve" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/tags.json b/public/language/hu/admin/settings/tags.json new file mode 100644 index 0000000000..b4d77f44bb --- /dev/null +++ b/public/language/hu/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Címke beállítások", + "link-to-manage": "Címkék kezelése", + "system-tags": "Rendszer címkék", + "system-tags-help": "Csak jogosultsággal rendelkező felhasználók láthatják ezeket a címkéket.", + "min-per-topic": "Címkék minimum száma témakörönként", + "max-per-topic": "Címkék maximális száma témakörönként", + "min-length": "Címke minimális hossza", + "max-length": "Címke maximális hossza", + "related-topics": "Kapcsolódó témakörök", + "max-related-topics": "Megjelenítendő kapcsolódó témakörök maximális száma (ha a téma támogatja)" +} \ No newline at end of file diff --git a/public/language/hu/admin/settings/uploads.json b/public/language/hu/admin/settings/uploads.json new file mode 100644 index 0000000000..54262a18cc --- /dev/null +++ b/public/language/hu/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Hozzászólások", + "orphans": "Orphaned Files", + "private": "Feltöltött fájlok priváttá tevése", + "strip-exif-data": "EXIF adatok törlése", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Privát kiterjesztések", + "private-uploads-extensions-help": "Add meg vesszővel elválasztva a privát kiterjesztések listáját (pl.: pdf,xls,doc) Az üres lista azt jelenti, hogy minden fájl privát.", + "resize-image-width-threshold": "Képek átméretezése, ha szélesebbek, mint a megadott szélesség", + "resize-image-width-threshold-help": "(pixelben, alapértelmezett: 1520 pixel, állítsd 0-ra a kikapcsoláshoz)", + "resize-image-width": "Kép átméretezése megadott szélességre", + "resize-image-width-help": "(pixelben, alapértelmezett: 760 pixel, állítsd 0-ra a kikapcsoláshoz)", + "resize-image-quality": "Használandó minőség képek átméretezésekor", + "resize-image-quality-help": "Alacsonyabb minőség beállítás használata az átméretezett képek fájlméretének csökkentésére.", + "max-file-size": "Maximum fájlméret (KB-ban)", + "max-file-size-help": "(kilobájtban, alapérték: 2048 KB)", + "reject-image-width": "Képek maximális szélessége (pixelben)", + "reject-image-width-help": "Azon képek, amik szélesebbek ennél az értéknél visszautasításra kerülnek.", + "reject-image-height": "Képek maximális magassága (pixelben)", + "reject-image-height-help": "Azon képek, amik magasabbak ennél az értéknél visszautasításra kerülnek.", + "allow-topic-thumbnails": "Kis képek feltöltésének engedélyezése témakörhöz a felhasználók számára", + "topic-thumb-size": "Témakörkép mérete", + "allowed-file-extensions": "Megengedett fájlkiterjesztések", + "allowed-file-extensions-help": "Itt adj meg fájlkiterjesztési listát, vesszővel elválasztva (pl. pdf,xls,doc). Az üres lista azt jelenti, hogy minden kiterjesztés megengedett.", + "upload-limit-threshold": "Felhasználó limit feltöltésekre:", + "upload-limit-threshold-per-minute": "%1 percenként", + "upload-limit-threshold-per-minutes": "%1 percenként", + "profile-avatars": "Profil avatárok", + "allow-profile-image-uploads": "Profilképek feltöltésének engedélyezése a felhasználók számára", + "convert-profile-image-png": "Profilkép feltöltések átkonvertálása PNG-be", + "default-avatar": "Egyéni alapértelmezett avatár", + "upload": "Feltöltés", + "profile-image-dimension": "Profilkép dimenziója", + "profile-image-dimension-help": "(pixelben, alapértelmezett: 128 pixel)", + "max-profile-image-size": "Profilkép maximális fájlmérete", + "max-profile-image-size-help": "(kibibájtban, alapértelmezett: 256 KiB)", + "max-cover-image-size": "Borítókép maximális fájlmérete", + "max-cover-image-size-help": "(kibibájtban, alapértelmezett: 2.048 KiB)", + "keep-all-user-images": "Az avatárok és profil borítók régi változatainak megtartása a szerveren", + "profile-covers": "Profil borítók", + "default-covers": "Alapértelmezett borítóképek", + "default-covers-help": "Alapértelmezett borítóképek hozzáadása fiókokhoz, amik nem rendelkeznek feltöltött borítóképpel, vesszővel elválasztva" +} diff --git a/public/language/hu/admin/settings/user.json b/public/language/hu/admin/settings/user.json new file mode 100644 index 0000000000..f679e5b510 --- /dev/null +++ b/public/language/hu/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Hitelesítés", + "email-confirm-interval": "A felhasználó nem küldetheti újra az emailt ameddig nem telt el", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Bejelentkezés engedélyezése ezzel:", + "allow-login-with.username-email": "Felhasználónév vagy email cím", + "allow-login-with.username": "Csak felhasználónév", + "account-settings": "Fiók beállítások", + "gdpr_enabled": "GDPR hozzájárulás gyűjtésének engedélyezése", + "gdpr_enabled_help": "Ha engedélyezett, minden új regisztrációnál hozzájárulását kell adnia a felhasználónak az adatai mentéséhez és felhasználásához az Általános adatvédelmi rendelet(GDPR) értelmében. Megjegyzés: A GDPR engedélyezése nem kéri a már meglévő felhasználóktól, hogy fogadják el az adatgyűjtést és felhasználást. Ehhez telepítened kell a GDPR beépülőt.", + "disable-username-changes": "Felhasználónév módosítás letiltása", + "disable-email-changes": "Email cím módosítás letiltása", + "disable-password-changes": "Jelszó módosítás letiltása", + "allow-account-deletion": "Fiók törlés engedélyezése", + "hide-fullname": "Teljes név elrejtése a felhasználók elől", + "hide-email": "Email cím elrejtése a felhasználók elől", + "show-fullname-as-displayname": "A felhasználók teljes nevének megjelenítése mindenhol, ha kitöltötte azt", + "themes": "Témák", + "disable-user-skins": "Egyedi kinézet választásának letiltása a felhasználók részére", + "account-protection": "Fiók védelem", + "admin-relogin-duration": "Adminisztrátori felület újrahitelesítési korlátja (percben)", + "admin-relogin-duration-help": "A megadott idő után az adminisztrációs felület eléréséhez újra be kell jelentkezni. Állítsd ezt 0-ra, ha nem szeretnéd ezt.", + "login-attempts": "Óránkénti bejelentkezések száma", + "login-attempts-help": "Ha az óránkénti bejelentkezések száma meghaladja ezt a küszöb értéket, akkor a fiók lezárásra kerül az előre megadott időtartamra.", + "lockout-duration": "Fiók lezárás időtartama (percben)", + "login-days": "Hány napig maradjon meg egy felhasználói munkamenet", + "password-expiry-days": "Jelszóváltoztatás kényszerítése ennyi nap után", + "session-time": "Munkamenet hossza", + "session-time-days": "Nap", + "session-time-seconds": "Másodperc", + "session-time-help": "Ezek az értékek határozzák meg, hogy mennyi ideig maradjanak bejelentkezve, ha bekapcsolják az \"Emlékezzen rám\" lehetőséget bejelentkezésnél. Ha nincs másodperc érték megadva, akkor a nap értékét vesszük figyelembe. Ha nincs nap érték megadva, akkor az alapértelmezett 14 nap kerül beállításra.", + "online-cutoff": "Hány perc elteltével számítson egy felhasználó inaktívnak", + "online-cutoff-help": "Ha egy felhasználó nem csinál semmit az oldalon, akkor inaktívnak nyilvánítjuk és nem kapnak valós idejű frissítéseket.", + "registration": "Felhasználó regisztráció", + "registration-type": "Regisztráció típusa", + "registration-approval-type": "Regisztráció jóváhagyásának módja", + "registration-type.normal": "Normális", + "registration-type.admin-approval": "Adminisztrátori jóváhagyás", + "registration-type.admin-approval-ip": "Adminisztrátori jóváhagyás IP címekre", + "registration-type.invite-only": "Csak meghívóval", + "registration-type.admin-invite-only": "Csak adminisztrátori meghívóval", + "registration-type.disabled": "Nincs regisztráció", + "registration-type.help": "Normális - A felhasználók regisztrálhatnak a regisztrációs oldalon.
\nCsak meghívóval - A felhasználók meghívhatnak másokat a felhasználók oldalon.
\nCsak adminisztrátori meghívóval - Csak adminisztrátorok képesek meghívni másokat a felhasználók és admin/kezelés/felhasználók oldalakon.
\nNincs regisztráció - Nem regisztrálhat senki.
", + "registration-approval-type.help": "Normális - A felhasználók regisztrációja azonnali.
\nAdminisztrátori jóváhagyás - A felhasználói regisztrációk felkerülnek egy jóváhagyási várólistára, amit az adminisztrátorok kezelhetnek.
\nAdminisztrátori jóváhagyás IP címekre - Új felhasználóknak normális, Adminisztrátori jóváhagyás azon IP címeknek, amikre már van fiók bejegyezve.
", + "registration-queue-auto-approve-time": "Automatikus jóváhagyás ideje", + "registration-queue-auto-approve-time-help": "Hány óra elteltével kerüljön automatikusan jóváhagyásra egy regisztráció. Állítsd 0-ra, hogy ne legyen automatikus jóváhagyás.", + "registration-queue-show-average-time": "Jelenjen meg a felhasználóknak az átlagos jóváhagyási idő", + "registration.max-invites": "Meghívások maximális száma felhasználónként", + "max-invites": "Meghívások maximális száma felhasználónként", + "max-invites-help": "0: nincs korlátozás. Az adminisztrátorok végtelen mennyiségű meghívót kapnak.
Kizárólag \"Csak meghívóvaly\" módban érhető el.", + "invite-expiration": "Meghívó lejárat", + "invite-expiration-help": "Hány nap múlva járjon le egy meghívó.", + "min-username-length": "Felhasználónév minimális hossza", + "max-username-length": "Felhasználónév maximális hossza", + "min-password-length": "Jelszó minimális hossza", + "min-password-strength": "Jelszó minimális erőssége", + "max-about-me-length": "Bemutatkozás maximális hossza", + "terms-of-use": "Fórum felhasználási feltételek (Hagyd üresen a kikapcsoláshoz)", + "user-search": "Felhasználó keresés", + "user-search-results-per-page": "Megjelenítendő eredmények száma", + "default-user-settings": "Alapértelmezett felhasználói beállítások", + "show-email": "Email cím megjelenítése", + "show-fullname": "Teljes név megjelenítése", + "restrict-chat": "Csak olyan felhasználó írhasson csevegő üzenetet nekem, akit követek", + "outgoing-new-tab": "Kimenő linkek megnyitása új lapon", + "topic-search": "Témakörön belüli keresés engedélyezése", + "update-url-with-post-index": "URL frissítése a hozzászólás indexével témakörök böngészése közben", + "digest-freq": "Feliratkozás összefoglalókra", + "digest-freq.off": "Kikapcsolt", + "digest-freq.daily": "Napi", + "digest-freq.weekly": "Heti", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Havi", + "email-chat-notifs": "Email küldése, ha új csevegési üzenet érkezik miközben nem vagyok elérhető", + "email-post-notif": "Email küldése, ha válasz érkezik olyan témakörhöz amire feliratkoztam", + "follow-created-topics": "Általad létrehozott témakör követése", + "follow-replied-topics": "Minden témakör követése, amire válaszoltál", + "default-notification-settings": "Alapértelmezett értesítési beállítások", + "categoryWatchState": "Alapértelmezett kategóriafigyelés", + "categoryWatchState.watching": "Figyelés", + "categoryWatchState.notwatching": "Nem megfigyelt", + "categoryWatchState.ignoring": "Mellőzés" +} diff --git a/public/language/hu/admin/settings/web-crawler.json b/public/language/hu/admin/settings/web-crawler.json new file mode 100644 index 0000000000..b229c2521b --- /dev/null +++ b/public/language/hu/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Feltérképezhetőségi beállítások", + "robots-txt": "Egyedi Robots.txt Hagyd üresen a kikapcsoláshoz", + "sitemap-feed-settings": "Oldaltérkép és hírcsatorna beállítások", + "disable-rss-feeds": "RSS hírcsatorna kikapcsolása", + "disable-sitemap-xml": "Sitemap.xml kikapcsolása", + "sitemap-topics": "Oldaltérképen megjelenítendő témakörök száma", + "clear-sitemap-cache": "Oldaltérkép gyorsítótár kiűrítése", + "view-sitemap": "Oldaltérkép megtekintése" +} \ No newline at end of file diff --git a/public/language/hu/category.json b/public/language/hu/category.json new file mode 100644 index 0000000000..310d5870e3 --- /dev/null +++ b/public/language/hu/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategória", + "subcategories": "Alkategóriák", + "new_topic_button": "Új témakör", + "guest-login-post": "Lépj be a hozzászóláshoz", + "no_topics": "Nincs témakör a kategóriában.Miért nem próbálsz létrehozni egyet?", + "browsing": "böngészés", + "no_replies": "Nem érkezett válasz", + "no_new_posts": "Nincs új hozzászólás.", + "watch": "Figyelés", + "ignore": "Mellőzés", + "watching": "Figyelés", + "not-watching": "Nem megfigyelt", + "ignoring": "Mellőzés", + "watching.description": "Témakörök mutatása a friss és olvasatlanok között", + "not-watching.description": "Olvasatlan témakörök elrejtése, csak a friss témák mutatása", + "ignoring.description": "Olvasatlan és friss témakörök elrejtése", + "watching.message": "Most már figyeled ennek a kategóriának és az alkategóriáinak a frissítéseit", + "notwatching.message": "Nem figyeled ennek a kategóriának és alkategóriáinak frissítéseit", + "ignoring.message": "Nem kapsz most már frissítéseket erről a kategóriáról és az alkategóriáiról", + "watched-categories": "Figyelt kategóriák", + "x-more-categories": "%1 további kategória" +} \ No newline at end of file diff --git a/public/language/hu/email.json b/public/language/hu/email.json new file mode 100644 index 0000000000..c2594b9d37 --- /dev/null +++ b/public/language/hu/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Teszt Email", + "password-reset-requested": "Jelszó megváltoztatás kérvényezve!", + "welcome-to": "Üdvözlet a(z) %1 oldalon", + "invite": "Meghívó a(z) %1 oldalra", + "greeting_no_name": "Helló", + "greeting_with_name": "Helló %1", + "email.verify-your-email.subject": "Kérlek erősítsd meg az email címed.", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Köszönjük a regisztrációt %1!", + "welcome.text2": "A fiók aktiválásához ellenőriznünk kell, hogy valós e-mail cím lett e megadva.", + "welcome.text3": "Egy adminisztrátor elfogadta a regisztrációdat. Mostantól a felhasználónév/jelszó párosoddal be tudsz lépni.", + "welcome.cta": "Kattints ide az e-mail címed megerősítéséhez", + "invitation.text1": "%1 meghívott ide: %2", + "invitation.text2": "A meghívó %1 napon belül lejár.", + "invitation.cta": "Kattints ide a fiók létrehozásához.", + "reset.text1": "Kaptunk egy kérést jelszavad visszaállítására, valószínűleg azért, mert elfelejtetted azt. Ha ez nem így van, hagyd figyelmen kívül ezt a levelet.", + "reset.text2": "A jelszó visszaállításának folytatásához kattints az alábbi linkre:", + "reset.cta": "Kattints ide a jelszavad visszaállításához", + "reset.notify.subject": "Jelszó sikeresen módosítva", + "reset.notify.text1": "Értesítünk, hogy a(z) %1 névhez tartozó jelszavad sikeresen megváltozott.", + "reset.notify.text2": "Ha nem te voltál az, kérlek, azonnal értesíts egy adminisztrátort.", + "digest.latest_topics": "Legutóbbi témakörök a következőből: %1", + "digest.top-topics": "Legfontosabb témakörök innen: %1", + "digest.popular-topics": "Népszerű témakörök innen: %1", + "digest.cta": "Kattints ide a(z) %1 meglátogatásához", + "digest.unsub.info": "Ez a hírlevél a feliratkozási beállításaid miatt lett kiküldve.", + "digest.day": "napban", + "digest.week": "héten", + "digest.month": "hónapban", + "digest.subject": "%1 hírlevél", + "digest.title.day": "Napi összefoglalód", + "digest.title.week": "Heti összefoglalód", + "digest.title.month": "Havi összefoglalód", + "notif.chat.subject": "Új chat üzenet érkezett a következőtől: %1", + "notif.chat.cta": "Kattints ide a beszélgetés folytatásához", + "notif.chat.unsub.info": "Ez a chat-értesítés a feliratkozási beállításaid miatt lett kiküldve.", + "notif.post.unsub.info": "Ez a hozzászólás-értesítés a feliratkozási beállításaid miatt lett kiküldve.", + "notif.post.unsub.one-click": "Alternatív megoldás, leiratkozás hasonló levelekről a jövőben mint ez, kattintás után.", + "notif.cta": "A fórumra", + "notif.cta-new-reply": "Hozzászólás megnézése", + "notif.cta-new-chat": "Csevegés megnézése", + "notif.test.short": "Értesítések tesztelése", + "notif.test.long": "Ez egy teszt email az értesítésekről. Küldj segítséget!", + "test.text1": "Ez egy teszt levél, ami által ellenőrizzük, hogy a levelező helyesen lett e beállítva a NodeBB-ben.", + "unsub.cta": "Kattintson ide a beállítások módosításához", + "unsubscribe": "Leiratkozás", + "unsub.success": "Nem fog kapni több email a következő %1 email fiókból", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Ki lettél tiltva a(z) %1 oldalról", + "banned.text1": "%1 nevű felhasználó ki lett tiltva a(z) %2 oldalról.", + "banned.text2": "A kitiltás lejárta: %1.", + "banned.text3": "A kitiltásod oka:", + "closing": "Köszönjük!" +} \ No newline at end of file diff --git a/public/language/hu/error.json b/public/language/hu/error.json new file mode 100644 index 0000000000..b59878f00e --- /dev/null +++ b/public/language/hu/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Érvénytelen adat", + "invalid-json": "Érvénytelen JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Úgy tűnik, nem vagy bejelentkezve.", + "account-locked": "A fiókod ideiglenesen zárolva lett.", + "search-requires-login": "A kereséshez fiók szükséges - kérlek, lépj be vagy regisztrálj.", + "goback": "A vissza gombbal átlépsz az előző oldalra", + "invalid-cid": "Érvénytelen kategória azonosító", + "invalid-tid": "Érvénytelen témakör azonosító", + "invalid-pid": "Érvénytelen hozzászólás azonosító", + "invalid-uid": "Érvénytelen felhasználó azonosító", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "Egy valós dátumot muszáj megadni.", + "invalid-username": "Érvénytelen felhasználónév", + "invalid-email": "Érvénytelen e-mail cím", + "invalid-fullname": "Érvénytelen név", + "invalid-location": "Érvénytelen hely", + "invalid-birthday": "Érvénytelen születési dátum", + "invalid-title": "Érvénytelen cím", + "invalid-user-data": "Érvénytelen felhasználói adatok", + "invalid-password": "Érvénytelen jelszó", + "invalid-login-credentials": "Érvénytelen belépési hitelesítés", + "invalid-username-or-password": "Kérlek, adj meg egy felhasználónevet és egy jelszót", + "invalid-search-term": "Érvénytelen keresési feltétel", + "invalid-url": "Érvénytelen URL", + "invalid-event": "Érvénytelen esemény: %1", + "local-login-disabled": "A helyi bejelentkezés letiltva fel nem hatalmazott felhasználóknál.", + "csrf-invalid": "Sikertelen bejelentkezés, feltételezhetően lejárt a munkamenet. Próbálkozz újra!", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Érvénytelen lapozási érték, legalább %1 kell lennie és legfeljebb %2 -nak/nek", + "username-taken": "Foglalt felhasználónév", + "email-taken": "Foglalt e-mail", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Ez az email cím már meg lett hívva", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Nem küldhetsz üzenetet amíg nem erősíted meg az email címed, kattints ide az email cím megerősítéséhez!", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Nem tudtuk ellenőrizni az e-mail címedet, kérlek próbálkozz később.", + "confirm-email-already-sent": "A megerősítéshez szükséges email már el lett küldve, kérlek várj %1 percet az újraküldéshez.", + "sendmail-not-found": "A levél küldés végrehajtása nem található, kérlek bizonyosodj meg róla, hogy telepítve van és végrehajtható a felhasználó által NodeBB-t futtatva.", + "digest-not-enabled": "Ennek a felhasználónak nincs engedélyezve az összefoglaló vagy a rendszer alapértelmezett beállítása nem küld összefoglalókat.", + "username-too-short": "Túl rövid felhasználónév", + "username-too-long": "Túl hosszú felhasználónév", + "password-too-long": "Jelszó túl hosszú", + "reset-rate-limited": "Túl sok jelszó visszaállítási kérelem lett küldve (megszabott érték)", + "reset-same-password": "Kérlek használj egy olyan jelszót ami eltér a mostanitól.", + "user-banned": "Kitiltott felhasználó", + "user-banned-reason": "Ez a fiók ki lett titlva (Indoklás: %1)", + "user-banned-reason-until": "Ez a fiók ki lett tiltva %1 -ig (Indoklás: %2)", + "user-too-new": "Várnod kell %1 másodpercet mielőtt létre tudod hozni az első hozzászólásod.", + "blacklisted-ip": "Az IP címed ki van tiltva ebből a közösségből. Ha úgy érzed, hogy ez valami hiba akkor lépj kapcsolatba egy adminisztrátorral.", + "ban-expiry-missing": "Kérlek adj meg lejárati dátumot a kitiltáshoz.", + "no-category": "Nem létező kategória", + "no-topic": "Nem létező téma", + "no-post": "Nem létező hozzászólás", + "no-group": "Nem létező csoport", + "no-user": "Nem létező felhasználó", + "no-teaser": "A bevezető nem létezik", + "no-flag": "Flag does not exist", + "no-privileges": "Nincs elég jogod ehhez a művelethez.", + "category-disabled": "Kategória kikapcsolva", + "topic-locked": "Téma lezárva", + "post-edit-duration-expired": "Bejegyzés létrehozását követően csak %1 másodperc elteltével válik szerkeszthetővé", + "post-edit-duration-expired-minutes": "Bejegyzés létrehozását követően %1 percig szerkesztheted még", + "post-edit-duration-expired-minutes-seconds": "Bejegyzés létrehozását követően %1 perc %2 másodpercig szerkesztheted", + "post-edit-duration-expired-hours": "Bejegyzés létrehozását követően %1 órán át szerkesztheted még", + "post-edit-duration-expired-hours-minutes": "Bejegyzés létrehozását követően %1 óra %2 percig szerkesztheted", + "post-edit-duration-expired-days": "Bejegyzés létrehozását követően %1 napig szerkesztheted", + "post-edit-duration-expired-days-hours": "Bejegyzés létrehozását követően %1 nap %2 óráig szerkesztheted", + "post-delete-duration-expired": "Bejegyzés létrehozását követően csak %1 másodpercen belül törölheted", + "post-delete-duration-expired-minutes": "Bejegyzés létrehozását követően csak %1 percen belül törölheted", + "post-delete-duration-expired-minutes-seconds": "Bejegyzés létrehozását követően csak %1 perc %2 másodpercen belül törölheted", + "post-delete-duration-expired-hours": "Bejegyzés létrehozását követően csak %1 órán belül törölheted", + "post-delete-duration-expired-hours-minutes": "Bejegyzés létrehozását követően %1 óra %2 percen belül törölheted", + "post-delete-duration-expired-days": "Bejegyzés létrehozását követően csak %1 napon belül törölheted", + "post-delete-duration-expired-days-hours": "Bejegyzés létrehozását követően csak %1 nap %2 órán belül törölheted", + "cant-delete-topic-has-reply": "Nem törölheted a témakört miután válaszoltak rá", + "cant-delete-topic-has-replies": "Nem törölheted a témaköröd miután %1 válasz van rajta", + "content-too-short": "Kérlek adj meg több karaktert a bejegyzés létrehozásához. A bejegyzéseknek legalább %1 karaktert kell tartalmazniuk.", + "content-too-long": "Kérlek kevesebb karaktert adj meg a bejegyzés létrehozásához. A bejegyzés nem tartalmazhat több karaktert mint %1 .", + "title-too-short": "Kérlek adj meg egy hosszabb címet. A címek legalább %1 karaktert kell tartalmazniuk.", + "title-too-long": "Kérlek adj meg egy rövidebb címet. A címek nem lehetnek hosszabbak %1 karakternél.", + "category-not-selected": "A kategória nincs kiválasztva.", + "too-many-posts": "Csak %1 másodpercenként hozhatsz létre új bejegyzést - kérlek várj egy kicsit mielőtt új bejegyzést tennél közzé", + "too-many-posts-newbie": "Új felhasználóként csak egyszer készíthetsz bejegyzést %1 másodpercen belül, amíg el nem éred a %2 szintet - kérlek várj egy kicsit mielőtt új bejegyzést tennél közzé", + "already-posting": "You are already posting", + "tag-too-short": "Kérlek hosszabb címkét adj meg. A címke legalább %1 karaktert kell, hogy tartalmazzon", + "tag-too-long": "Kérlek rövidebb címkét adj meg. A címkék nem lehetnek hosszabbak %1 karakternél", + "not-enough-tags": "Nincs elég címke. A bejegyzésnek legalább %1 címkét kell tartalmaznia", + "too-many-tags": "Túl sok címke. A bejegyzés nem tartalmazhat több címkét mint %1", + "cant-use-system-tag": "Nem használhatod ezt a rendszer címkét.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Kérlek várj, amíg a feltöltés befejeződik.", + "file-too-big": "A maximális megengedett fájl méret %1 kB - kérlek kisebb méretű fájlt tölts fel", + "guest-upload-disabled": "Vendég általi feltöltés kikapcsolva", + "cors-error": "Nem sikerült a kép feltöltés a rosszul konfigurált CORS miatt", + "upload-ratelimit-reached": "Egyszerre túl sok fájlt töltöttél fel. Kérlek próbáld újra később.", + "scheduling-to-past": "Kérlek adj meg egy jövőbeli időpontot.", + "invalid-schedule-date": "Kérlek valós dátumot és időt adj meg.", + "cant-pin-scheduled": "Időzített témakörök rögzítése nem oldható fel.", + "cant-merge-scheduled": "Időzített témakörök nem olvaszthatóak össze.", + "cant-move-posts-to-scheduled": "Hozzászólások nem mozgathatóak időzített témakörbe.", + "cant-move-from-scheduled-to-existing": "Időzített témakörből létező témakörbe nem lehet hozzászólásokat mozgatni.", + "already-bookmarked": "Már elmentetted ezt a hozzászólást a könyvjelzők közé", + "already-unbookmarked": "Már eltávolítottad ezt a hozzászólást a könyvjelzők közül", + "cant-ban-other-admins": "Nem tilthatsz ki másik adminisztrátort!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Te vagy az egyedüli adminisztrátor. Adj hozzá egy másik felhasználót az adminisztrátori szerepkörhöz, hogy levehesd magadról az adminisztrátori rangot", + "account-deletion-disabled": "Fiók törlése ki van kapcsolva", + "cant-delete-admin": "Vedd el az adminisztrátori jogokat ettől a fióktól mielőtt törölni szeretnéd.", + "already-deleting": "Már törölve", + "invalid-image": "Érvénytelen kép", + "invalid-image-type": "Érvénytelen a kép típusa. Engedett kiterjesztések: %1", + "invalid-image-extension": "Érvénytelen a kép kiterjesztése", + "invalid-file-type": "Érvénytelen a fájl típusa. Engedélyezett kiterjesztések: %1", + "invalid-image-dimensions": "A képméret túl nagy", + "group-name-too-short": "A csoport név túl rövid", + "group-name-too-long": "A csoport név túl hosszú", + "group-already-exists": "A csoport nem létezik", + "group-name-change-not-allowed": "A csoport névváltoztatás nem engedélyezett", + "group-already-member": "Már a tagja a csoportnak", + "group-not-member": "Nem tagja a csoportnak", + "group-needs-owner": "Ennek a csoportnak lennie kell legalább egy tulajdonosnak.", + "group-already-invited": "Ez a felhasználó már meg lett hívva", + "group-already-requested": "A tagság kérelmed már be lett nyújtva", + "group-join-disabled": "Most nem csatlakozhatsz ehhez a csoporthoz", + "group-leave-disabled": "Most nem hagyhatod el ezt a csoportot", + "post-already-deleted": "Ez a bejegyzés mát törlésre került", + "post-already-restored": "Ez a bejegyzés már visszaállításra került", + "topic-already-deleted": "Ezt a témakör már törlésre került", + "topic-already-restored": "Ez a témakör már helyreállításra került", + "cant-purge-main-post": "Nem tisztíthatod ki ezt a témakört, inkább töröld", + "topic-thumbnails-are-disabled": "Témakör bélyegképek tíltásra kerültek.", + "invalid-file": "Érvénytelen fájl", + "uploads-are-disabled": "A feltöltés nem engedélyezett", + "signature-too-long": "Az aláírásod nem lehet hosszabb %1 karakternél.", + "about-me-too-long": "A bemutatkozás nem lehet hosszabb %1 karakternél.", + "cant-chat-with-yourself": "Nem cseveghetsz magaddal!", + "chat-restricted": "Ez a felhasználó korlátozta a chat beállításait. Csak akkor cseveghetsz vele, miután felvett a követettek közé téged", + "chat-disabled": "Csevegés funkció kikapcsolva", + "too-many-messages": "Túl sok üzenetet küldtél, kérlek várj egy picit.", + "invalid-chat-message": "Érvénytelen üzenet", + "chat-message-too-long": "Üzenet nem lehet hosszabb %1 karakternél.", + "cant-edit-chat-message": "Nem módosíthatod ezt az üzenetet", + "cant-delete-chat-message": "Nem törölheted ezt az üzenetet", + "chat-edit-duration-expired": "Üzenetet a beküldés után csak %1 másodpercig szerkeszthetsz", + "chat-delete-duration-expired": "Üzenetet a beküldés után csak %1 másodpecen belül törölhetsz", + "chat-deleted-already": "Ez az üzenet már törölve lett.", + "chat-restored-already": "Ez az üzenet már vissza van állítva.", + "chat-room-does-not-exist": "Csevegő szoba nem létezik.", + "already-voting-for-this-post": "Már szavaztál erre a hozzászólásra.", + "reputation-system-disabled": "Hírnév funkció kikapcsolva.", + "downvoting-disabled": "Leszavazás funkció kikapcsolva", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "Már megjelölted ezt a hozzászólást", + "user-already-flagged": "Már megjelölted ez a felhasználót", + "post-flagged-too-many-times": "Ez a bejegyzés már meg lett jelölve egy másik felhasználó által", + "user-flagged-too-many-times": "Ez a felhasználó már meg lett jelölve egy másik felhasználó által", + "cant-flag-privileged": "Nem jelentheted be felhatalmazott felhasználókat vagy a bejegyzéseik tartalmát (moderátor/globális moderátor/adminok)", + "self-vote": "Nem szavazhatsz a saját hozzászólásodra", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "Naponat csak %1 alkalommal szavazhatsz", + "too-many-downvotes-today-user": "Naponta csak %1 alkalommal szavazhatsz le felhasználókat", + "reload-failed": "NodeBB egy hibát észlelt újratöltés közben: \"%1\". A fórum továbbra is kiszolgálja a kliens-oldali eszközöket, bár vissza kellene csinálnod amit az újratöltés előtt elállítottál.", + "registration-error": "Regisztrációs hiba", + "parse-error": "Hiba történt a szerver válaszának feldolgozása közben", + "wrong-login-type-email": "Kérlek az e-mail címedet használd a belépéshez", + "wrong-login-type-username": "Kérlek a felhasználónevedet használd a belépéshez", + "sso-registration-disabled": "A regisztráció kikapcsolva %1 felhasználóknak, kérlek email címmel regisztrálj", + "sso-multiple-association": "Ebből a szolgáltatásból nem társíthatsz több felhasználót a NodeBB-fiókhoz. Válaszd szét a meglévő fiókod, és próbálja újra.", + "invite-maximum-met": "Nem küldhetsz több meghívót (%1 a %2 -ból/ből).", + "no-session-found": "Nem található bejelentkezési munkamenet", + "not-in-room": "A felhasználó nincs a szobában", + "cant-kick-self": "Nem rúghatod ki magad a csoportból", + "no-users-selected": "Nincs felhasználó kiválasztva", + "invalid-home-page-route": "Érvénytelen főoldal elérési útvonal", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Nincs témakör kiválasztva", + "cant-move-to-same-topic": "Nem mozgathatsz hozzászólást azonos témakörbe!", + "cant-move-topic-to-same-category": "Nem mozgathatod a témakört azonos kategóriába!", + "cannot-block-self": "Nem tudod letiltani magad!", + "cannot-block-privileged": "Nem tilthatsz le adminisztrátort és moderátort", + "cannot-block-guest": "Vendégek nem tilthatnak le felhasználókat", + "already-blocked": "Ez a felhasználó már le van tiltva", + "already-unblocked": "Ennek a felhasználóknak már fel van oldva a tiltása", + "no-connection": "Probléma van az internet kapcsolatoddal", + "socket-reconnect-failed": "Nem lehet elérni a szervert. Kattints ide az újra próbáláshoz vagy várj egy kicsit", + "plugin-not-whitelisted": "Ez a bővítmény nem telepíthető – csak olyan bővítmények telepíthetőek amiket a NodeBB Package Manager az ACP-n keresztül tud telepíteni", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Témakör esemény '%1' ismeretlen", + "cant-set-child-as-parent": "Leszármazottat nem adhatsz meg szülő kategóriaként", + "cant-set-self-as-parent": "Saját magát nem adhatod meg szülő kategóriaként", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/hu/flags.json b/public/language/hu/flags.json new file mode 100644 index 0000000000..1ee967e128 --- /dev/null +++ b/public/language/hu/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Állapot", + "reports": "Jelentés", + "first-reported": "Először jelentve", + "no-flags": "Hurrá! Nincs megjelölés.", + "assignee": "Engedményes", + "update": "Frissítés", + "updated": "Frissítve", + "resolved": "Megoldva", + "target-purged": "A tartalom amire a jelölő mutat már meg lett tiszítva vagy nem létezik.", + + "graph-label": "Napi jelölők", + "quick-filters": "Gyors szűrő", + "filter-active": "Egy vagy több szűrő be van kapcsolva a jelölők között", + "filter-reset": "Szűrők eltüntetése", + "filters": "Szűrők beállítása", + "filter-reporterId": "Jelentő UID", + "filter-targetUid": "Megjelölt UID", + "filter-type": "Jelölő típus", + "filter-type-all": "Összes tartalom", + "filter-type-post": "Hozzászólás", + "filter-type-user": "Felhasználó", + "filter-state": "Állapot", + "filter-assignee": "Engedményes UID", + "filter-cid": "Kategória", + "filter-quick-mine": "Hozzám rendelt", + "filter-cid-all": "Minden kategória", + "apply-filters": "Szűrők alkalmazása", + "more-filters": "Több szűrő", + "fewer-filters": "Kevesebb szűrő", + + "quick-actions": "Gyors akciók", + "flagged-user": "Megjelölt felhasználó", + "view-profile": "Profil megtekintése", + "start-new-chat": "Új chat indítása", + "go-to-target": "Jelölő célpont megnézése", + "assign-to-me": "Rendeld hozzám", + "delete-post": "Bejegyzés törlése", + "purge-post": "Bejegyzés megtisztítása", + "restore-post": "Bejegyzés helyreállítása", + "delete": "Delete Flag", + + "user-view": "Profil megtekintése", + "user-edit": "Profil szerkesztése", + + "notes": "Jegyzetek megjelölése", + "add-note": "Új jegyzet hozzáadása", + "no-notes": "Nincsenek megosztott jegyzetek.", + "delete-note-confirm": "Biztosan törölni akarod ezt a jegyzet jelölőt?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Jegyzet hozzá adva", + "note-deleted": "Jegyzet törölve", + "flag-deleted": "Flag Deleted", + + "history": "Felhasználó & Előzmény jelölő", + "no-history": "Nincs előzmény jelölő.", + + "state-all": "Összes állapot", + "state-open": "Új/nyitott", + "state-wip": "Munka folyamatban", + "state-resolved": "Megoldva", + "state-rejected": "Elutasítva", + "no-assignee": "Nincs hozzá rendelve", + + "sort": "Rendezés", + "sort-newest": "Legújabb előre", + "sort-oldest": "Legrégebbi előre", + "sort-reports": "Legtöbb jelentés", + "sort-all": "Az összes jelölő típus...", + "sort-posts-only": "Csak bejegyzés...", + "sort-downvotes": "Legtöbb leszavazás", + "sort-upvotes": "Legtöbb pozitív szavazat", + "sort-replies": "Legtöbb válasz", + + "modal-title": "Tartalom jelentése", + "modal-body": "Kérlek fejezd ki a megjelölés indokát az átnézéshez %1 %2 . Alternatívaként használd a gyors jelentés gombot ha lehetséges.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Támadó jellegű", + "modal-reason-other": "Más (fejtsd ki lent)", + "modal-reason-custom": "Tartalom jelentésének az indoka...", + "modal-submit": "Jelentés beküldése", + "modal-submit-success": "A tartalom meg lett jelölve egy moderátornak.", + + "bulk-actions": "Tömeges műveletek", + "bulk-resolve": "Megoldott jelölés", + "bulk-success": "%1 jelölő frissítve", + "flagged-timeago-readable": "Megjelölve (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/hu/global.json b/public/language/hu/global.json new file mode 100644 index 0000000000..fd340e9ed6 --- /dev/null +++ b/public/language/hu/global.json @@ -0,0 +1,126 @@ +{ + "home": "Kezdőlap", + "search": "Keresés", + "buttons.close": "Bezárás", + "403.title": "Hozzáférés megtagadva", + "403.message": "Úgy tűnik, hogy rábukkantál egy olyan oldalra, amihez nincs hozzáférésed.", + "403.login": "Talán próbálj meg belépni?", + "404.title": "Nincs találat", + "404.message": "Úgy tűnik, hogy rábukkantál egy olyan oldalra, ami nem létezik. Visszatérés a kezdőoldalra", + "500.title": "Belső hiba.", + "500.message": "Hoppá! Úgy tűnik, valami hiba történt!", + "400.title": "Hibás kérelem.", + "400.message": "Úgy látszik, a hivatkozás formátuma hibás, ellenőrizd és próbáld újra. Egyéb esetben térj vissza a kezdőoldalra.", + "register": "Regisztrálás", + "login": "Belépés", + "please_log_in": "Jelentkezz be", + "logout": "Kijelentkezés", + "posting_restriction_info": "A hozzászólás regisztrációhoz kötött, kérlek kattints ide a belépéshez.", + "welcome_back": "Üdvözlünk újra közöttünk", + "you_have_successfully_logged_in": "Sikeresen beléptél", + "save_changes": "Változások mentése", + "save": "Mentés", + "close": "Bezárás", + "pagination": "Lapozás", + "pagination.out_of": "%1 / %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Kategóriák", + "header.recent": "Legutóbbi", + "header.unread": "Olvasatlan", + "header.tags": "Címkék", + "header.popular": "Népszerű", + "header.top": "Top", + "header.users": "Felhasználók", + "header.groups": "Csoportok", + "header.chats": "Chat", + "header.notifications": "Értesítések", + "header.search": "Keresés", + "header.profile": "Profil", + "header.navigation": "Navigáció", + "notifications.loading": "Értesítések betöltése", + "chats.loading": "Chat betöltése", + "motd.welcome": "Üdvözlet a NodeBB-n, a jövő fórum platformján.", + "previouspage": "Előző oldal", + "nextpage": "Következő oldal", + "alert.success": "Sikeres", + "alert.error": "Hiba", + "alert.banned": "Kitiltva", + "alert.banned.message": "Kitiltottak, ezért most ki leszel léptetve.", + "alert.unbanned": "Kitiltás feloldva", + "alert.unbanned.message": "A kitiltásodat feloldották.", + "alert.unfollow": "Nem követed tovább: %1!", + "alert.follow": "Mostantól követed: %1!", + "users": "Felhasználók", + "topics": "Témakörök", + "posts": "Hozzászólások", + "x-posts": "%1 posts", + "best": "Legjobb", + "controversial": "Controversial", + "votes": "Szavazatok", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Kedvelők", + "upvoted": "Kedvelt", + "downvoters": "Utálók", + "downvoted": "Utálva", + "views": "Megtekintések", + "posters": "Posters", + "reputation": "Hírnév", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "további olvasása", + "more": "Több", + "none": "None", + "posted_ago_by_guest": "%1 vendég hozzászólás", + "posted_ago_by": "%2 hozzászólás %1", + "posted_ago": "%1 hozzászólás", + "posted_in": "hozzászólt itt: %1", + "posted_in_by": "%2 hozzászólt itt: %1", + "posted_in_ago": "hozzászólva: %1, %2", + "posted_in_ago_by": "%3 hozzászólt: %1, %2", + "user_posted_ago": "%1 hozzászólt %2", + "guest_posted_ago": "Vendég hozzászólás %1", + "last_edited_by": "utoljára %1 szerkesztette", + "norecentposts": "Nincs legutóbbi hozzászólás", + "norecenttopics": "Nincs legutóbbi témakör", + "recentposts": "Legutóbbi hozzászólások", + "recentips": "Utoljára bejelentkezett IP címek", + "moderator_tools": "Moderátori eszközök", + "online": "Elérhető", + "away": "Nincs a gépnél", + "dnd": "Ne zavarj", + "invisible": "Láthatatlan", + "offline": "Nem elérhető", + "email": "E-mail", + "language": "Nyelv", + "guest": "Vendég", + "guests": "Vendég", + "former_user": "A Former User", + "system-user": "Rendszer", + "unknown-user": "Ismeretlen felhasználó", + "updated.title": "Fórum frissítve", + "updated.message": "A fórum frissítve lett a legutolsó verzióra. Kattints ide az oldal újratöltéséhez.", + "privacy": "Titoktartás", + "follow": "Követés", + "unfollow": "Nincs követés", + "delete_all": "Összes törlése", + "map": "Térkép", + "sessions": "Belépési munkamenetek", + "ip_address": "IP-cím", + "enter_page_number": "Oldalszám megadása", + "upload_file": "Fájl feltöltése", + "upload": "Feltöltés", + "uploads": "Feltöltések", + "allowed-file-types": "Támogatott fájltípusok: %1", + "unsaved-changes": "Mentetlen módosításaid vannak. Biztos el akarsz innen menni?", + "reconnecting-message": "Úgy látszik, a(z) %1 csatlakozásod megszakadt, várj, míg megpróbáljuk helyreállítani.", + "play": "Lejátszás", + "cookies.message": "A weboldal sütiket használ, a legjobb weboldalas élmény érdekében.", + "cookies.accept": "Értem!", + "cookies.learn_more": "Tudnivalók", + "edited": "Szerkesztett", + "disabled": "Letiltva", + "select": "Kiválaszt", + "user-search-prompt": "Írj be valamit, hogy felhasználókra keress..." +} \ No newline at end of file diff --git a/public/language/hu/groups.json b/public/language/hu/groups.json new file mode 100644 index 0000000000..eb77e6f9f7 --- /dev/null +++ b/public/language/hu/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Csoportok", + "view_group": "Csoport megtekintés", + "owner": "Csoport tulajdonosa", + "new_group": "Új csoport létrehozása", + "no_groups_found": "Nincs megjeleníthető csoport", + "pending.accept": "Elfogad", + "pending.reject": "Elutasít", + "pending.accept_all": "Mind elfogad", + "pending.reject_all": "Mind elutasít", + "pending.none": "Jelenleg nincsen függő tagság", + "invited.none": "Jelenleg nincs meghívott tag", + "invited.uninvite": "Meghívás törlése", + "invited.search": "Felhasználó keresése a csoportba invitáláshoz", + "invited.notification_title": "Meghívtak, hogy csatlakozz a(z) %1 csoporthoz", + "request.notification_title": "%1 csoporttagsági kérelmet küldött", + "request.notification_text": "%1 kérvényezi, hogy a(z) %2 tagja lehessen", + "cover-save": "Mentés", + "cover-saving": "Mentés", + "details.title": "Csoport részletei", + "details.members": "Tagok listája", + "details.pending": "Függőben levő tagok", + "details.invited": "Meghívott tagok", + "details.has_no_posts": "A csoport tagjai nem tettek még közzé hozzászólást.", + "details.latest_posts": "Legutóbbi hozzászólások", + "details.private": "Privát", + "details.disableJoinRequests": "Csatlakozási kérelem kikapcsolva", + "details.disableLeave": "Felhasználók nem hagyhatják el ezt a csoportot", + "details.grant": "Tulajdonjog megadása/törlése", + "details.kick": "Kirúgás", + "details.kick_confirm": "Biztos el akarod távolítani ezt a tagot a csoportból?", + "details.add-member": "Tag hozzá adása", + "details.owner_options": "Csoportadminisztrátor", + "details.group_name": "Csoport neve", + "details.member_count": "Tagok száma", + "details.creation_date": "Létrehozás dátuma", + "details.description": "Leírás", + "details.member-post-cids": "Kategóriák megjelenítése bejegyzésekből", + "details.badge_preview": "Jelvény előnézet", + "details.change_icon": "Ikon módosítása", + "details.change_label_colour": "Címke színének megváltoztatása", + "details.change_text_colour": "Szöveg színének megváltoztatása", + "details.badge_text": "Jelvény szövege", + "details.userTitleEnabled": "Jelvény megjelenítése", + "details.private_help": "Ha engedélyezett, a csoport tulajdonosa hagyja jóvá a csoporthoz csatlakozást", + "details.hidden": "Rejtett", + "details.hidden_help": "Ha engedélyezett, a csoport nem jelenik meg a csoportlistán, és a felhasználókat manuálisan kell meghívni", + "details.delete_group": "Csoport törlése", + "details.private_system_help": "A privát csoportok rendszerszinten vannak kikapcsolva, így ez a beállítás semmit sem csinál", + "event.updated": "A csoport részletei frissítve", + "event.deleted": "A(z) \"%1\" csoport törölve", + "membership.accept-invitation": "Meghívás elfogadása", + "membership.accept.notification_title": "Most már a tagja vagy a(z) %1", + "membership.invitation-pending": "Függőben levő meghívás", + "membership.join-group": "Csoporthoz csatlakozás", + "membership.leave-group": "Csoport elhagyása", + "membership.leave.notification_title": "%1 elhagyta a csoportot %2", + "membership.reject": "Elutasítás", + "new-group.group_name": "Csoport neve:", + "upload-group-cover": "Csoport fedőkép feltöltése", + "bulk-invite-instructions": "Add meg vesszővel elválasztva a csoportba meghívandó felhasználóneveket", + "bulk-invite": "Tömeges meghívás", + "remove_group_cover_confirm": "Biztos el akarod távolítani a fedőképet?" +} \ No newline at end of file diff --git a/public/language/hu/ip-blacklist.json b/public/language/hu/ip-blacklist.json new file mode 100644 index 0000000000..f49cb9eeeb --- /dev/null +++ b/public/language/hu/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Az IP cím által tiltottak listájának szerkesztése.", + "description": "Esetenként a felhasználói fiók tiltása nem elég visszatartó erejű . Időnként megvonni a fórumhoz való hozzáférést egy IP címtől vagy egy körzetbe tartozó IP címtől sokkal hatásosabb. Ezekben az esetekben a problémás IP címeket vagy egész CIDR tömböket adhatsz hozzá a tiltólistához és tőlük meg lesz vonva az új fiók készítése vagy a bejelentkezés.", + "active-rules": "Aktív szabályok", + "validate": "Tiltólista átvizsgálása", + "apply": "Tiltólista engedélyezése", + "hints": "Szintaxis tippek", + "hint-1": "Soronként egy IP címet tüntess fel. Megadhatsz IP cím blokkokat is amíg azok követik a CIDR formátumot (e.g. 192.168.100.0/22).", + "hint-2": "Adhatsz hozzá megjegyzést, hogyha a # szimbólumot a szöveg elé rakod.", + + "validate.x-valid": "%1 kívül %2 szabály(ok) érvényesek.", + "validate.x-invalid": "A következő szabályok érvénytelenek: %1 ", + + "alerts.applied-success": "Tiltólista alkalmazva", + + "analytics.blacklist-hourly": "1. ábra – Óránként a feketelista találatai ", + "analytics.blacklist-daily": "2. ábra – Napi feketelista találatok ", + "ip-banned": "IP cím letiltva" +} \ No newline at end of file diff --git a/public/language/hu/language.json b/public/language/hu/language.json new file mode 100644 index 0000000000..64000bf03d --- /dev/null +++ b/public/language/hu/language.json @@ -0,0 +1,5 @@ +{ + "name": "Magyar (Hungarian)", + "code": "hu", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/hu/login.json b/public/language/hu/login.json new file mode 100644 index 0000000000..d9874de691 --- /dev/null +++ b/public/language/hu/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Felhasználónév / E-mail", + "username": "Felhasználónév", + "remember_me": "Emlékezzen rám?", + "forgot_password": "Elfelejtetted a jelszót?", + "alternative_logins": "Alternatív belépés", + "failed_login_attempt": "Sikertelen belépés", + "login_successful": "Sikeresen beléptél!", + "dont_have_account": "Még nincs fiókod?", + "logged-out-due-to-inactivity": "Inaktivitás miatt ki lettél jelentkeztetve az Adminisztrációs vezérlőpultból", + "caps-lock-enabled": "Caps Lock bekapcsolva" +} \ No newline at end of file diff --git a/public/language/hu/modules.json b/public/language/hu/modules.json new file mode 100644 index 0000000000..5d7f764cf5 --- /dev/null +++ b/public/language/hu/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat vele", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "Régebbi üzeneteket nézel, kattints ide a legfrissebbekhez.", + "chat.send": "Küldés", + "chat.no_active": "Nincs aktív csevegésed.", + "chat.user_typing": "%1 éppen ír ...", + "chat.user_has_messaged_you": "%1 üzenetet küldött.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Válasszuk ki a címzettet és tekintsük meg a chat előzményeket", + "chat.no-users-in-room": "Nincs felhasználó a szobában", + "chat.recent-chats": "Legutóbbi csevegések", + "chat.contacts": "Névjegyzék", + "chat.message-history": "Üzenet előzmények", + "chat.message-deleted": "Üzenet törölve", + "chat.options": "Chat beállítások", + "chat.pop-out": "Felugró csevegés", + "chat.minimize": "Kis méret", + "chat.maximize": "Teljes méret", + "chat.seven_days": "7 nap", + "chat.thirty_days": "30 nap", + "chat.three_months": "3 hónap", + "chat.delete_message_confirm": "Biztos törölni akarod az üzenetet?", + "chat.retrieving-users": "Felhasználók lekérése...", + "chat.manage-room": "Chat szoba kezelése", + "chat.add-user-help": "Itt keress felhasználókat. Kiválasztás után a felhasználó hozzá lesz adva a chathez. Az új felhasználó nem fogja látni az üzenet előzményeket az előttről, hogy hozzá lett adva a beszélgetéshez. Csak a szoba tulajdonosai () távolíthatnak el felhasználókat a beszélgetésből.", + "chat.confirm-chat-with-dnd-user": "A felhasználó \"ne zavarj\"-ra állította az állapotukat. Még így is csevegni akarsz velük?", + "chat.rename-room": "Szoba átnevezése", + "chat.rename-placeholder": "Add meg a szoba nevét", + "chat.rename-help": "A megadott szoba név az összes szobában tartózkodó által megtekinthező lesz.", + "chat.leave": "Chat elhagyása", + "chat.leave-prompt": "Biztosan el akarod hagyni a beszélgetést?", + "chat.leave-help": "A szoba elhagyását követően nem fogod látni az oda érkező üzeneteket. Ha újra hozzá leszel adva a szobához akkor nem fogod látni a beszélgetés előzményeit a kilépésed előttről sem.", + "chat.in-room": "Ebben a szobában", + "chat.kick": "Kirúgás", + "chat.show-ip": "IP cím mutatása", + "chat.owner": "Szoba tulajdonos", + "chat.system.user-join": "%1 csatlakozott a szobához", + "chat.system.user-leave": "%1 elhagyta a szobát", + "chat.system.room-rename": "%2 megváltoztatta ennek a szobának a nevét: %1", + "composer.compose": "Üzenetírás", + "composer.show_preview": "Előnézet megjelenítése", + "composer.hide_preview": "Előnézet elrejtése", + "composer.user_said_in": "%1 válasza, %2:", + "composer.user_said": "%1 válasza:", + "composer.discard": "Biztosan el akarod vetni a hozzászólást?", + "composer.submit_and_lock": "Küldés és zárolás", + "composer.toggle_dropdown": "Legördülő kapcsoló", + "composer.uploading": "%1 feltöltése", + "composer.formatting.bold": "Félkövér", + "composer.formatting.italic": "Dőlt", + "composer.formatting.list": "Felsorolás", + "composer.formatting.strikethrough": "Áthúzás", + "composer.formatting.code": "Code", + "composer.formatting.link": "Hivatkozás", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Kép feltöltése", + "composer.upload-file": "Fájl feltöltése", + "composer.zen_mode": "Zen mód", + "composer.select_category": "Kategória választása", + "composer.textarea.placeholder": "Add meg a bejegyzés tartalmát, húzd be ide a képet", + "composer.schedule-for": "Témakör időzítése", + "composer.schedule-date": "Dátum", + "composer.schedule-time": "Idő", + "composer.cancel-scheduling": "Időzítés elvetése", + "composer.set-schedule-date": "Dátum beállítása", + "bootbox.ok": "OK", + "bootbox.cancel": "Mégse", + "bootbox.confirm": "Megerősítés", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Fedőkép pozíciója", + "cover.dragging_message": "Húzd a fedőképet a kívánt pozícióba, majd \"Mentés\"", + "cover.saved": "Fedőkép és annak pozíciója elmentve", + "thumbs.modal.title": "Téma indexképének kezelése", + "thumbs.modal.no-thumbs": "Nem található indexkép.", + "thumbs.modal.resize-note": "Megjegyzés: A fórum beállítása csak %1px szélességű indexképet engedélyez", + "thumbs.modal.add": "Indexkép hozzáadása", + "thumbs.modal.remove": "Indexkép eltávolítása", + "thumbs.modal.confirm-remove": "Biztosan el akarod távolítani az indexképet?" +} \ No newline at end of file diff --git a/public/language/hu/notifications.json b/public/language/hu/notifications.json new file mode 100644 index 0000000000..52738c0c57 --- /dev/null +++ b/public/language/hu/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Értesítések", + "no_notifs": "Nincsenek új értesítéseid", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Vissza - %1", + "outgoing_link": "Külső hivatkozás", + "outgoing_link_message": "Most elhagyod az oldalt: %1", + "continue_to": "%1 megnyitása", + "return_to": "Vissza - %1", + "new_notification": "Új értesítésed érkezett", + "you_have_unread_notifications": "Olvasatlan értesítéseid vannak.", + "all": "Mind", + "topics": "Témakör", + "replies": "Válasz", + "chat": "Chat", + "group-chat": "Group Chats", + "follows": "Követés", + "upvote": "Kedvelés", + "new-flags": "Új megjelölés", + "my-flags": "Hozzám társított megjelölés", + "bans": "Kitiltás", + "new_message_from": "Új üzenet, feladó: %1", + "upvoted_your_post_in": "%1 kedvelte a hozzászólásod itt: %2.", + "upvoted_your_post_in_dual": "%1 és %2 kedvelte a hozzászólásod itt: %3.", + "upvoted_your_post_in_multiple": "%1 és %2 másik kedvelte a hozzászólásod itt: %3.", + "moved_your_post": "%1 áthelyezte a hozzászólásod ide: %2", + "moved_your_topic": "%1 áthelyezve: %2", + "user_flagged_post_in": "%1 megjelölt egy hozzászólást itt: %2", + "user_flagged_post_in_dual": "%1 és%2 megjelölt egy hozzászólást itt: %3", + "user_flagged_post_in_multiple": "%1 és %2 másik megjelölt egy hozzászólást itt: %3", + "user_flagged_user": "%1 megjelölt egy felhasználói profilt (%2)", + "user_flagged_user_dual": "%1 és %2 megjelölt egy felhasználói profilt (%3)", + "user_flagged_user_multiple": "%1 és %2 másik megjelölt egy felhasználói profilt (%3)", + "user_posted_to": "%1 választ írt neki: %2", + "user_posted_to_dual": "%1 és%2 választ írt neki: %3", + "user_posted_to_multiple": "%1 és %2 másik választ írt neki: %3", + "user_posted_topic": "%1 új témakört hozott létre: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 elkezdett követni téged.", + "user_started_following_you_dual": "%1 és%2 elkezdett követni téged.", + "user_started_following_you_multiple": "%1 és %2 másik elkezdett követni téged.", + "new_register": "%1 regisztrációs kérvényt nyújtott be.", + "new_register_multiple": "Jelenleg %1 regisztrációs kérvény vár elbírálásra.", + "flag_assigned_to_you": "%1 megjelölés hozzád van társítva", + "post_awaiting_review": "A hozzászólás átnézésre vár", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "A Users csv exportálva, kattints ide a letöltéshez", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-mail megerősítve", + "email-confirmed-message": "Köszönjük az e-mail címed megerősítését. A fiókod mostantól teljesen aktiválva van.", + "email-confirm-error-message": "Probléma lépett fel az e-mail címed megerősítésekor. Talán a kód érvénytelen volt vagy lejárt.", + "email-confirm-sent": "Megerősítő e-mail elküldve.", + "none": "Nincs", + "notification_only": "Csak értesítés", + "email_only": "Csak e-mail", + "notification_and_email": "Értesítés és e-mail", + "notificationType_upvote": "Mikor valaki kedveli a hozzászólásod", + "notificationType_new-topic": "Mikor egy követett felhasználód hozzászól", + "notificationType_new-reply": "Mikor egy általad figyelt témakörre válasz érkezik", + "notificationType_post-edit": "Mikor egy a megfigyelt témakörön belül módosítanak egy bejegyzést", + "notificationType_follow": "Mikor valaki elkezd követni téged", + "notificationType_new-chat": "Mikor chat üzenetet kapsz", + "notificationType_new-group-chat": "Mikor kapsz egy csoportos chat üzenetet", + "notificationType_group-invite": "Mikor csoportmeghívást kapsz", + "notificationType_group-leave": "Mikor egy felhasználó elhagyja a csoportot", + "notificationType_group-request-membership": "Mikor valaki jelentkezni szeretne a csoportba ami a tiéd", + "notificationType_new-register": "Mikor valaki a regisztrációs várólistára kerül", + "notificationType_post-queue": "Mikor egy új hozzászólás várólistára kerül", + "notificationType_new-post-flag": "Mikor egy hozzászólás megjelölésre kerül", + "notificationType_new-user-flag": "Mikor egy felhasználó megjelölésre kerül" +} \ No newline at end of file diff --git a/public/language/hu/pages.json b/public/language/hu/pages.json new file mode 100644 index 0000000000..4632eb5d20 --- /dev/null +++ b/public/language/hu/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Kezdőlap", + "unread": "Olvasatlan témakörök", + "popular-day": "Mai népszerű témakörök", + "popular-week": "Heti népszerű témakörök", + "popular-month": "Havi népszerű témakörök", + "popular-alltime": "Mindenkori legnépszerűbb témakörök", + "recent": "Legfrissebb témakörök", + "top-day": "Mai legtöbb szavazatot kapott témakör", + "top-week": "Ezen a héten legtöbb szavazatot kapott témakör", + "top-month": "Ebben a hónapban legtöbb szavazatot kapott témakör", + "top-alltime": "Legtöbb szavazatot kapott témakörök", + "moderator-tools": "Moderátori eszközök", + "flagged-content": "Megjelölt tartalom", + "ip-blacklist": "IP tiltólista", + "post-queue": "Hozzászólás várólista", + "users/online": "Elérhető felhasználok", + "users/latest": "Legújabb felhasználók", + "users/sort-posts": "Legtöbbet hozzászóló felhasználók", + "users/sort-reputation": "Legnagyobb hírnévvel rendelkező felhasználók", + "users/banned": "Kitiltott felhasználók", + "users/most-flags": "Legtöbbet megjelölt felhasználók", + "users/search": "Felhasználói keresés", + "notifications": "Értesítések", + "tags": "Címkék", + "tag": ""%1" címkével ellátott témakörök", + "register": "Fiók regisztrálása", + "registration-complete": "Regisztráció befejezve", + "login": "Belépés a fiókodba", + "reset": "Fiókod jelszavának visszaállítása", + "categories": "Kategóriák", + "groups": "Csoportok", + "group": "%1 csoport", + "chats": "Chat", + "chat": "Chatelés %1 felhasználóval", + "flags": "Megjelölés", + "flag-details": "%1 megjelölés részletei", + "account/edit": "\"%1\" szerkesztése", + "account/edit/password": "\"%1\" jelszavának szerkesztése", + "account/edit/username": "\"%1\" felhasználói nevének szerkesztése", + "account/edit/email": "\"%1\" e-mail címének szerkesztése", + "account/info": "Fiók információ", + "account/following": "Tagok, akiket %1 követ", + "account/followers": "Tagok, akik %1 felhasználót követik", + "account/posts": "%1 által írt hozzászólások", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "%1 által létrehozott témakörök", + "account/groups": "%1 csoportjai", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1 könyvjelzőzött hozzászólásai", + "account/settings": "Felhasználói beállítások", + "account/watched": "%1 által figyelt témakörök", + "account/ignored": "%1 által mellőzött témakörök", + "account/upvoted": "%1 által kedvelt témakörök", + "account/downvoted": "%1 által utált témakörök", + "account/best": "%1 által írt legjobb hozzászólások", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Felhasználó letiltva erről: %1", + "account/uploads": "Feltöltések általa: %1", + "account/sessions": "Bejelentkezések munkamenete", + "confirm": "E-mail megerősítve", + "maintenance.text": "%1 jelenleg karbantartás alatt van. Kérlek, nézz vissza később!", + "maintenance.messageIntro": "Ezenkívúl, az adminisztrátor ezt az üzenetet hagyta:", + "throttled.text": "A(z) %1 jelenleg nem érhető el túlterheltség miatt. Kérlek, nézz vissza később." +} \ No newline at end of file diff --git a/public/language/hu/post-queue.json b/public/language/hu/post-queue.json new file mode 100644 index 0000000000..7217428448 --- /dev/null +++ b/public/language/hu/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Hozzászólási várósor", + "description": "Nem várakozik egy bejegyzés sem a sorban.
Hogy bekapcsold ezt a funckiót, menj a Beállítások → Bejegyzés → Bejegyzés sor és fogadd el a Bejegyzés sor. -t", + "user": "Felhasználó", + "category": "Kategória", + "title": "Cím", + "content": "Tartalom", + "posted": "Bejegyzés létrehozva", + "reply-to": "Válasz neki \"%1\"", + "content-editable": "Kattints a tartalomra, hogy szerkeszthesd", + "category-editable": "Kattints a kategóriára, hogy szerkeszthesd", + "title-editable": "Kattints a címre, hogy szerkeszthesd", + "reply": "Válasz", + "topic": "Témakör", + "accept": "Elfogad", + "reject": "Elutasít", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/hu/recent.json b/public/language/hu/recent.json new file mode 100644 index 0000000000..95004a40a6 --- /dev/null +++ b/public/language/hu/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Legrissebb", + "day": "Nap", + "week": "Hét", + "month": "Hónap", + "year": "Év", + "alltime": "Bármikor", + "no_recent_topics": "Nincs friss témakör.", + "no_popular_topics": "Nincs népszerű témakör.", + "there-is-a-new-topic": "Van egy új témakör.", + "there-is-a-new-topic-and-a-new-post": "Van egy új témakör és új hozzászólás.", + "there-is-a-new-topic-and-new-posts": "Van egy új témakör és %1 új hozzászólás.", + "there-are-new-topics": "Van %1 új témakör.", + "there-are-new-topics-and-a-new-post": "Van %1 új témakör és egy új hozzászólás.", + "there-are-new-topics-and-new-posts": "Van %1 új témakör és %2 új hozzászólás.", + "there-is-a-new-post": "Van egy új hozzászólás.", + "there-are-new-posts": "Van %1 új hozzászólás.", + "click-here-to-reload": "Újratöltéshez kattints ide." +} \ No newline at end of file diff --git a/public/language/hu/register.json b/public/language/hu/register.json new file mode 100644 index 0000000000..31eb8e7e74 --- /dev/null +++ b/public/language/hu/register.json @@ -0,0 +1,32 @@ +{ + "register": "Regisztráció", + "cancel_registration": "Regisztráció megszakítása", + "help.email": "Alapértelmezetten az e-mail címed rejtve van a nyilvánosság előtt.", + "help.username_restrictions": "Egyedi felhasználói név %1 és %2 karakterek között. A többiek az alábbi módon említhetnek meg: @becenév.", + "help.minimum_password_length": "A jelszónak legalább %1 karakter hosszúnak kell lennie.", + "email_address": "E-mail cím", + "email_address_placeholder": "E-mail cím megadása", + "username": "Felhasználónév", + "username_placeholder": "Felhasználónév megadása", + "password": "Jelszó", + "password_placeholder": "Jelszó megadása", + "confirm_password": "Jelszó megerősítése", + "confirm_password_placeholder": "Jelszó megerősítése", + "register_now_button": "Regisztrálás", + "alternative_registration": "Alternatív regisztráció", + "terms_of_use": "Használati feltételek", + "agree_to_terms_of_use": "Elfogadom a Használati feltételeket", + "terms_of_use_error": "El kell fogadnod a Használati feltételeket", + "registration-added-to-queue": "A regisztráció jóváhagyásra vár. Kapni fogsz egy e-mailt, amint az adminisztrátor elfogadja.", + "registration-queue-average-time": "A tagság átlagos elfogadási ideje %1 óra %2 perc.", + "registration-queue-auto-approve-time": "A fórum tagságod aktiválva lesz az elkövetkezendő %1 órában.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Hozzájárulok ahhoz, hogy személyes adataimat ez a weboldal gyűjtse és feldolgozza.", + "gdpr_agree_email": "Hozzájárulok, hogy kapjak erről az oldalról összefoglalókat és értesítő emaileket.", + "gdpr_consent_denied": "Hozzá kell járulnod ahhoz, hogy ez a feboldal megkapja a személyes információidat és emaileket küldhessen neked.", + "invite.error-admin-only": "Közvetlen felhasználó regisztráció letiltva. Kérjük vegye fel a kapcsolatot egy adminisztrátorral további információkért.", + "invite.error-invite-only": "Közvetlen felhasználó regisztráció letiltva. Egy már regisztrált felhasználó meg kell hívjon téged, hogy hozzáférhess ehhez a fórumhoz.", + "invite.error-invalid-data": "A regisztrációs adatok nem egyeznek a mi adatainkkal. Kérjük vegye fel a kapcsolatot egy adminisztrátorral további információkért." +} \ No newline at end of file diff --git a/public/language/hu/reset_password.json b/public/language/hu/reset_password.json new file mode 100644 index 0000000000..69d40f4efe --- /dev/null +++ b/public/language/hu/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Jelszó visszaállítása", + "update_password": "Jelszó frissítése", + "password_changed.title": "A jelszó megváltozott", + "password_changed.message": "

A jelszavad sikereresen visszaállítva, kérlek lép be újra.", + "wrong_reset_code.title": "Helytelen visszaállítási-kód", + "wrong_reset_code.message": "A visszaállítási-kód helytelen. Kérlek próbáld újra, vagy kérj egy új kódot.", + "new_password": "Új jelszó", + "repeat_password": "Jelszó megerősítése", + "changing_password": "Jelszó megváltoztatása", + "enter_email": "Kérlek add meg az e-mail címedet, ahová elküldjük a további teendőket a jelszavad visszaállításával kapcsolatban.", + "enter_email_address": "E-mail cím megadása", + "password_reset_sent": "Ha a megadott cím egyezik a felhasználóval, akkor a jelszó helyreállításáról egy email fog érkezni hamarosan. Kérlek vedd figyelembe, hogy percenként csak egy email küldhető.", + "invalid_email": "Helytelen e-mail cím / Nem létező e-mail cím!", + "password_too_short": "A megadott jelszó túl rövid, válassz másik jelszót.", + "passwords_do_not_match": "A két megadott jelszó nem egyezik.", + "password_expired": "Lejárt a jelszavad, válassz új jelszót." +} \ No newline at end of file diff --git a/public/language/hu/search.json b/public/language/hu/search.json new file mode 100644 index 0000000000..52630083c9 --- /dev/null +++ b/public/language/hu/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 eredmény a következőre: \"%2\" (%3 másodperc)", + "no-matches": "Nem található egyezés", + "advanced-search": "Részletes keresés", + "in": "Itt:", + "titles": "Címek", + "titles-posts": "Címek és hozzászólások", + "match-words": "Egyező szavak", + "all": "Összes", + "any": "Bármelyik", + "posted-by": "Írta", + "in-categories": "Kategóriában", + "search-child-categories": "Keresés az alkategóriában is", + "has-tags": "Címkéje", + "reply-count": "Válaszok száma", + "at-least": "Legalább", + "at-most": "Legfeljebb", + "relevance": "Találati pontosság", + "post-time": "Hozzászólás ideje", + "votes": "Szavazatok", + "newer-than": "Újabb, mint", + "older-than": "Régebbi, mint", + "any-date": "Bármikor", + "yesterday": "Tegnap", + "one-week": "Egy hét", + "two-weeks": "Két hét", + "one-month": "Egy hónap", + "three-months": "Három hónap", + "six-months": "Hat hónap", + "one-year": "Egy év", + "sort-by": "Rendezés", + "last-reply-time": "Utolsó válasz ideje", + "topic-title": "Témakör címe", + "topic-votes": "Témakör szavazatok", + "number-of-replies": "Válaszok száma", + "number-of-views": "Megtekintések száma", + "topic-start-date": "Témakör indulási napja", + "username": "Felhasználónév", + "category": "Kategória", + "descending": "Csökkenő sorrendben", + "ascending": "Növekvő sorrendben", + "save-preferences": "Beállítások mentése", + "clear-preferences": "Beállítások törlése", + "search-preferences-saved": "Keresési beállítások mentve", + "search-preferences-cleared": "Keresési beállítások törölve", + "show-results-as": "Megjelenő találatok", + "see-more-results": "Több találat megjelenítése (%1)", + "search-in-category": "Keresés \"%1\"-ban/ben" +} \ No newline at end of file diff --git a/public/language/hu/success.json b/public/language/hu/success.json new file mode 100644 index 0000000000..5a2d590191 --- /dev/null +++ b/public/language/hu/success.json @@ -0,0 +1,7 @@ +{ + "success": "Sikeres", + "topic-post": "Sikeres hozzászólás.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Sikeres hitelesítés", + "settings-saved": "Beállítások mentve!" +} \ No newline at end of file diff --git a/public/language/hu/tags.json b/public/language/hu/tags.json new file mode 100644 index 0000000000..33dce52220 --- /dev/null +++ b/public/language/hu/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nem létezik témakör ezzel a címkével.", + "tags": "Címkék", + "enter_tags_here": "%1 és %2 karakterek között itt add meg a címkét.", + "enter_tags_here_short": "Címke megadása...", + "no_tags": "Még nincsenek címkék.", + "select_tags": "Címkék kiválasztása" +} \ No newline at end of file diff --git a/public/language/hu/top.json b/public/language/hu/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/hu/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/hu/topic.json b/public/language/hu/topic.json new file mode 100644 index 0000000000..8d124f224f --- /dev/null +++ b/public/language/hu/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Témakör", + "title": "Cím", + "no_topics_found": "Nem található témakör!", + "no_posts_found": "Nem található hozzászólás!", + "post_is_deleted": "A hozzászólás törlésre került!", + "topic_is_deleted": "A témakör törlésre került!", + "profile": "Profil", + "posted_by": "%1 írta", + "posted_by_guest": "Vendég írta", + "chat": "Chat", + "notify_me": "Értesítést kérek a témakörhöz érkező új hozzászólásokról", + "quote": "Idézés", + "reply": "Válasz", + "replies_to_this_post": "%1 válasz", + "one_reply_to_this_post": "1 válasz", + "last_reply_time": "Utolsó válasz", + "reply-as-topic": "Válasz témakörként ", + "guest-login-reply": "Lépj be a válaszoláshoz", + "login-to-view": "🔒 Jelentkezz be, hogy megtekinthesd", + "edit": "Szerkesztés", + "delete": "Törlés", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Végleges törlés", + "restore": "Visszaállítás", + "move": "Áthelyezés", + "change-owner": "Tulajdonos megváltoztatása", + "fork": "Szétszedés", + "link": "Hivatkozás", + "share": "Megosztás", + "tools": "Eszközök", + "locked": "Zárolva", + "pinned": "Rögzített", + "pinned-with-expiry": "Kitűzve eddig: %1", + "scheduled": "Időzített", + "moved": "Áthelyezett", + "moved-from": "Áthelyezés innen %1", + "copy-ip": "IP-cím másolása", + "ban-ip": "IP-cím kitiltása", + "view-history": "Előzmények szerkesztése", + "locked-by": "Lezárta", + "unlocked-by": "Kinyitotta", + "pinned-by": "Rögzítette", + "unpinned-by": "Rögzítését levette", + "deleted-by": "Törölte", + "restored-by": "Visszaállította", + "moved-from-by": "Moved from %1 by", + "queued-by": "Hozzászólás jóváhagyásra bejegyezve →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Kattints ide a beszélgetés utolsó hozzászólására ugráshoz.", + "flag-post": "Jelöld meg ezt a bejegyzést", + "flag-user": "Jelöld meg ezt a felhasználót", + "already-flagged": "Már meg lett jelölve", + "view-flag-report": "Jelölésekről szóló jelentés megtekintése", + "resolve-flag": "Megjelölés megoldása", + "merged_message": "Ezt a témát beolvasztották %2", + "deleted_message": "A témakör törölve lett. Csak a témakör-kezelési joggal rendelkező felhasználók láthatják.", + "following_topic.message": "Mostantól értesítést kapsz, mikor valaki hozzászól ehhez a témakörhöz.", + "not_following_topic.message": "Látni fogod ezt a témakört az olvasatlan témakörök listáján, de nem kapsz értesítést, mikor valaki hozzászól a témakörhöz.", + "ignoring_topic.message": "Nem fogod látni ezt a témakört az olvasatlan témakörök listáján. Értesítést fogsz kapni, mikor valaki megemlít téged, vagy kedveli a hozzászólásod.", + "login_to_subscribe": "Kérlek, regisztrálj vagy lépj be, hogy feliratkozz erre a témakörre.", + "markAsUnreadForAll.success": "Témakör olvasatlannak jelölve mindenki számára.", + "mark_unread": "Megjelölés olvasatlanként", + "mark_unread.success": "Témakör olvasatlannak jelölve.", + "watch": "Figyelés", + "unwatch": "Nincs figyelés", + "watch.title": "Értesítsen a témakör új válaszairól", + "unwatch.title": "Témakör figyelésének leállítása.", + "share_this_post": "Hozzászólás megosztása", + "watching": "Figyelés", + "not-watching": "Nincs figyelés", + "ignoring": "Mellőzés", + "watching.description": "Értesítsen az új válaszokról.
Témakör megjelenítése olvasatlanként.", + "not-watching.description": "Ne értesítsen az új válaszokról.
Témakör megjelenítése olvasatlanként, ha a kategória nincs mellőzve.", + "ignoring.description": "Ne értesítsen az új válaszokról.
Témakör ne jelenjen meg olvasatlanként.", + "thread_tools.title": "Témaköri eszközök", + "thread_tools.markAsUnreadForAll": "Mind megjelölése olvasatlanként", + "thread_tools.pin": "Témakör rögzítése", + "thread_tools.unpin": "Témakör rögzítésének feloldása", + "thread_tools.lock": "Témakör zárolása", + "thread_tools.unlock": "Témakör feloldása", + "thread_tools.move": "Témakör áthelyezése", + "thread_tools.move-posts": "Bejegyzések mozgatása", + "thread_tools.move_all": "Mind áthelyezése", + "thread_tools.change_owner": "Tulaj megváltoztatása", + "thread_tools.select_category": "Kategória kiválasztása", + "thread_tools.fork": "Témakör szétszedése", + "thread_tools.delete": "Témakör törlése", + "thread_tools.delete-posts": "Hozzászólások törlése", + "thread_tools.delete_confirm": "Biztos törölni akarod ezt a témakört?", + "thread_tools.restore": "Témakör visszaállítása", + "thread_tools.restore_confirm": "Biztos vissza akarod állítani a témakört?", + "thread_tools.purge": "Témakör végleges törlése", + "thread_tools.purge_confirm": "Biztos végleg törölni akarod a témakört?", + "thread_tools.merge_topics": "Témakörök összevonása", + "thread_tools.merge": "Összevonás", + "topic_move_success": "Ez a témakör hamarosan a(z) \"%1\" lesz áthelyezve. Kattints ide a visszavonáshoz.", + "topic_move_multiple_success": "Ezek a témakör hamarosan a(z) \"%1\" lesznek áthelyezve. Kattints ide a visszavonáshoz.", + "topic_move_all_success": "Hamarosan az összes témakör \"%1\" át lesz helyezve. Kattints ide a visszavonáshoz.", + "topic_move_undone": "Témakör áthelyezése visszavonva", + "topic_move_posts_success": "A bejegyzés hamarosan át lesz helyezve. Kattints ide a visszavonáshoz.", + "topic_move_posts_undone": "Bejegyzés áthelyezése visszavonva", + "post_delete_confirm": "Biztos törölni akarod a hozzászólást?", + "post_restore_confirm": "Biztos vissza akarod állítani a hozzászólást?", + "post_purge_confirm": "Biztos végleg törölni akarod a hozzászólást?", + "pin-modal-expiry": "Lejárati dátum", + "pin-modal-help": "Itt beállíthatod a lejárat idejét a kitűzött témaköröknek. Ha a mezőt üresen hagyod akkor témakör kitűzve marad amíg manuálisan le nem szedik.", + "load_categories": "Kategóriák betöltése", + "confirm_move": "Áthelyezés", + "confirm_fork": "Szétszedés", + "bookmark": "Könyvjelző", + "bookmarks": "Könyvjelzők", + "bookmarks.has_no_bookmarks": "Még nem tettél egyetlen hozzászólást sem könyvjelzőbe.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "További hozzászólások betöltése", + "move_topic": "Témakör áthelyezése", + "move_topics": "Témakörök áthelyezése", + "move_post": "Hozzászólás áthelyezése", + "post_moved": "Hozzászólás áthelyezve!", + "fork_topic": "Témakör szétszedése", + "enter-new-topic-title": "Adj meg új témakör címet", + "fork_topic_instruction": "Kattints a hozzászólásokra, melyeket szét akarsz szedni", + "fork_no_pids": "Nincs hozzászólás kiválasztva!", + "no-posts-selected": "Nincs bejegyzés kiválasztva!", + "x-posts-selected": "%1 bejegyzés kiválasztva", + "x-posts-will-be-moved-to-y": "%1 bejegyzés mozgatva lesz ide \"%2\"", + "fork_pid_count": "%1 kiválasztott hozzászólás", + "fork_success": "Témakör sikeresen szétválasztva! Kattints ide a szétválasztott témakörre ugráshoz.", + "delete_posts_instruction": "Kattints a törlendő/véglegesen törlendő hozzászólásokra", + "merge_topics_instruction": "Kattints a témakörre amelyiket össze szeretnéd olvasztani vagy keresni szeretnél benne", + "merge-topic-list-title": "Összeolvasztása váró témakörök listája", + "merge-options": "Összeolvasztás beállíts", + "merge-select-main-topic": "Válaszd ki a fő témakört", + "merge-new-title-for-topic": "Új cím a témának", + "topic-id": "Témakör azonosító", + "move_posts_instruction": "Kattints az áthelyezni kívánt témakörre és a célhelyen kattints a mozgatás ide gombra.", + "change_owner_instruction": "Kattints a bejegyzésre amelyiket hozzá szeretnéd utalni egy felhasználóhoz", + "composer.title_placeholder": "Add meg a témakör címét...", + "composer.handle_placeholder": "Adj meg egy nevet/kezelőt", + "composer.discard": "Elvet", + "composer.submit": "Küldés", + "composer.additional-options": "Additional Options", + "composer.schedule": "Időzítés", + "composer.replying_to": "Válasz erre: %1", + "composer.new_topic": "Új témakör", + "composer.editing": "Szerkesztés", + "composer.uploading": "feltöltés...", + "composer.thumb_url_label": "Bélyegkép URL beszúrása", + "composer.thumb_title": "Bélyegkép hozzáadása a témakörhöz", + "composer.thumb_url_placeholder": "http://minta.hu/kep.png", + "composer.thumb_file_label": "Vagy fájl feltöltése", + "composer.thumb_remove": "Mezők törlése", + "composer.drag_and_drop_images": "Ide húzd a képeket", + "more_users_and_guests": "%1 felhasználó és %2 vendég", + "more_users": "%1 felhasználó", + "more_guests": "%1 vendég", + "users_and_others": "%1 és %2 másik", + "sort_by": "Rendezés", + "oldest_to_newest": "Régebbiek elől", + "newest_to_oldest": "Újabbak elől", + "most_votes": "Legtöbb szavazat", + "most_posts": "Legtöbb bejegyzés", + "most_views": "Most Views", + "stale.title": "Inkább új témakör létrehozása?", + "stale.warning": "A témakör, melyre válaszolsz, elég régi. Szeretnél helyette inkább új témakört létrehozni, és erre hivatkozni a válaszodban?", + "stale.create": "Új témakör létrehozása", + "stale.reply_anyway": "Mindenképp erre a témakörre válaszolás", + "link_back": "Válasz: [%1](%2)", + "diffs.title": "Szerkesztett bejegyzések előzményei", + "diffs.description": "Ezt a bejegyzést %1 felülvizsgálják. Kattints lent a felülvizsgálások egyikére a bejegyzés tartalmának az adott időpontban történő megtekintéséhez .", + "diffs.no-revisions-description": "Ennek a posztnak %1 felülvizsgálásai vannak.", + "diffs.current-revision": "Jelenlegi felülvizsgálások", + "diffs.original-revision": "Eredeti felülvizsgálások", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "Egy új felülvizsgálás mellékelve lesz ennek a bejegyzésnek a szerkesztési előzményeiben.", + "diffs.post-restored": "A bejegyzés sikeresen visszaállítva az előző felülvizsgálatra", + "diffs.delete": "Verzió törlése", + "diffs.deleted": "Verzió törölve", + "timeago_later": "%1 később", + "timeago_earlier": "%1 korábban", + "first-post": "Első bejegyzés", + "last-post": "Utolsó bejegyzés", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/hu/unread.json b/public/language/hu/unread.json new file mode 100644 index 0000000000..f251736a36 --- /dev/null +++ b/public/language/hu/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Olvasatlan", + "no_unread_topics": "Nincs olvasatlan témakör.", + "load_more": "További betöltése", + "mark_as_read": "Megjelölés olvasottként", + "selected": "Kiválasztva", + "all": "Mind", + "all_categories": "Minden kategória", + "topics_marked_as_read.success": "Témakör olvasottnak jelölve!", + "all-topics": "Minden témakör", + "new-topics": "Új témakör", + "watched-topics": "Figyelt témakör", + "unreplied-topics": "Meg nem válaszolt témakör", + "multiple-categories-selected": "Többszörös kijelölés" +} \ No newline at end of file diff --git a/public/language/hu/uploads.json b/public/language/hu/uploads.json new file mode 100644 index 0000000000..8d82cb9a15 --- /dev/null +++ b/public/language/hu/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Fájl feltöltése...", + "select-file-to-upload": "Válassz feltöltendő fájlt!", + "upload-success": "Fájl feltöltése sikeres!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "Nem találhatók feltöltések", + "public-uploads-info": "A feltöltések nyilvánosak, minden látogató megtekintheti őket.", + "private-uploads-info": "A feltöltések privátak, csak a bejelentkezett felhasználók tekinthetik meg őket." +} \ No newline at end of file diff --git a/public/language/hu/user.json b/public/language/hu/user.json new file mode 100644 index 0000000000..b0e315cc85 --- /dev/null +++ b/public/language/hu/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Kitiltva", + "muted": "Muted", + "offline": "Nem elérhető", + "deleted": "Törölve", + "username": "Felhasználónév", + "joindate": "Regisztráció dátuma", + "postcount": "Hozzászólás megtekintés", + "email": "E-mail", + "confirm_email": "E-mail megerősítése", + "account_info": "Fiók információ", + "admin_actions_label": "Adminisztratív intézkedés", + "ban_account": "Fiók tiltása", + "ban_account_confirm": "Biztos ki akarod tiltani a felhasználót?", + "unban_account": "Fiók feloldása", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Fiók törlése", + "delete_account_as_admin": "Fiók törlése", + "delete_content": "Fiók tartalmának törlése", + "delete_all": "Fiók és tarlamának törlése", + "delete_account_confirm": "Biztosan névteleníteni szeretnéd a bejegyzéseidet és törlöd a fiókodat?
Ez a lépés visszafordíthatatlan és nem lehet bármilyen elveszett információt visszaállítani

Add meg a jelszavadat, hogyha biztosan végleg törölni szeretnéd ezt a fiókot.", + "delete_this_account_confirm": "Biztosan törölni szeretnéd ezt a fiókot, úgy-hogy minden eddigi bejegyzést megtartasz?
Ez a lépés visszafordíthatatlan és az összes hozzá kapcsolódó bejegyzés névtelenítve lesz

", + "delete_account_content_confirm": "Biztosan törölni szeretnéd a fiók tartalmát (bejegyzések/témakörök/feltöltések)?
Ez a lépés visszafordíthatatlan és nem lehet bármilyen elveszett információt visszaállítani

", + "delete_all_confirm": "Biztosan törölni szeretnéd ezt a felhasználót és minden tartalmát (bejegyzések/témakörök/feltöltések)?
Ez a lépés visszafordíthatatlan és nem lehet bármilyen elveszett információt visszaállítani

", + "account-deleted": "Fiók törölve", + "account-content-deleted": "Fiók tartalma törölve", + "fullname": "Teljes név", + "website": "Weboldal", + "location": "Lakhely", + "age": "Kor", + "joined": "Csatlakozott", + "lastonline": "Utoljára elérhető", + "profile": "Profil", + "profile_views": "Profil megtekintések", + "reputation": "Hírnév", + "bookmarks": "Könyvjelzők", + "watched_categories": "Megfigyelt kategóriák", + "change_all": "Minden megváltoztatása", + "watched": "Figyelve", + "ignored": "Mellőzve", + "default-category-watch-state": "Alapértelmezett kategória megfigyelési állapot", + "followers": "Követők", + "following": "Követve", + "blocks": "Blokkolások", + "block_toggle": "Blokkolás ki-/bekapcsolása", + "block_user": "Felhasználó tiltása", + "unblock_user": "Felhasználó tiltásának feloldása", + "aboutme": "Rólam", + "signature": "Aláírás", + "birthday": "Születésnap", + "chat": "Chat", + "chat_with": "Chat folytatása %1 felhasználóval", + "new_chat_with": "Új chat indítása %1 felhasználóval", + "flag-profile": "Profil megjelölése", + "follow": "Követés", + "unfollow": "Nincs követés", + "more": "Több", + "profile_update_success": "Profil sikeresen frissítve!", + "change_picture": "Kép módosítása", + "change_username": "Felhasználónév módosítása", + "change_email": "E-mail módosítása", + "email_same_as_password": "Kérlek add meg a jelenlegi jelszavadat a folytatáshoz – újra megadtad az új email címed", + "edit": "Szerkesztés", + "edit-profile": "Profil szerkesztése", + "default_picture": "Alapértelmezett ikon", + "uploaded_picture": "Feltöltött kép", + "upload_new_picture": "Új kép feltöltése", + "upload_new_picture_from_url": "Új kép feltöltése hivatkozásról", + "current_password": "Jelenlegi jelszó", + "change_password": "Jelszó módosítása", + "change_password_error": "Érvénytelen jelszó!", + "change_password_error_wrong_current": "A jelenlegi jelszavad nem megfelelő!", + "change_password_error_match": "A jelszavak nem egyeznek!", + "change_password_error_privileges": "Nincs jogod megváltoztatni ezt a jelszót.", + "change_password_success": "A jelszavad frissítve!", + "confirm_password": "Jelszó megerősítése", + "password": "Jelszó", + "username_taken_workaround": "A kívánt felhasználónév már foglalt, így változtatnunk kellett rajta egy kicsit. Mostantól %1 név alatt vagy ismert.", + "password_same_as_username": "A jelszavad megegyezik a felhasználóneveddel, kérlek válassz másik jelszót.", + "password_same_as_email": "A jelszavad megegyezik az e-mail címeddel, kérlek válassz másik jelszót.", + "weak_password": "Gyenge jelszó.", + "upload_picture": "Kép feltöltése", + "upload_a_picture": "Egy kép feltöltése", + "remove_uploaded_picture": "Feltöltött kép eltávolítása", + "upload_cover_picture": "Fedőkép feltöltése", + "remove_cover_picture_confirm": "Biztos el akarod távolítani a fedőképet?", + "crop_picture": "Kép vágása", + "upload_cropped_picture": "Vágás és feltöltés", + "avatar-background-colour": "Avatár háttér színe", + "settings": "Beállítások", + "show_email": "E-mail címem megjelenítése", + "show_fullname": "Teljes nevem megjelenítése", + "restrict_chats": "Csak az általam követett felhasználók tudnak chat üzenetet küldeni", + "digest_label": "Feliratkozás a hírlevélre", + "digest_description": "E-mailben kapott frissítésekre (új értesítések, témák esetében) való feliratkozás, a beállított időintervallum szerint", + "digest_off": "Ki", + "digest_daily": "Napi", + "digest_weekly": "Heti", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Havi", + "has_no_follower": "Ezt a felhasználót nem követi senki :(", + "follows_no_one": "Ez a felhasználó nem követ senkit :(", + "has_no_posts": "A felhasználó még nem szólt hozzá semmihez.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "A felhasználó még nem szólt hozzá egyik témakörhöz sem.", + "has_no_watched_topics": "A felhasználó még nem nézett meg egy témakört sem.", + "has_no_ignored_topics": "A felhasználó még nem mellőzött témakört.", + "has_no_upvoted_posts": "A felhasználó még egy hozzászólást sem kedvelt.", + "has_no_downvoted_posts": "A felhasználó még egy hozzászólást sem utált.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Nem blokkoltál egy felhasználót sem.", + "email_hidden": "E-mail rejtett", + "hidden": "rejtett", + "paginate_description": "Témakörök és hosszászólasok lapozása a végtelen görgetés helyett.", + "topics_per_page": "Témakörök oldalanként", + "posts_per_page": "Hozzászólások oldalanként", + "max_items_per_page": "Maximum %1", + "acp_language": "Adminisztrációs oldal nyelve", + "notifications": "Értesítések", + "upvote-notif-freq": "Kedvelési értesítés gyakorisága", + "upvote-notif-freq.all": "Összes kedvelés", + "upvote-notif-freq.first": "Első bejegyzésenként", + "upvote-notif-freq.everyTen": "Minden tizedik kedvelés", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "10, 100, 1000...", + "upvote-notif-freq.disabled": "Kikapcsolva", + "browsing": "Böngészési beállítások", + "open_links_in_new_tab": "Kimenő hivatkozások megnyitása új lapon", + "enable_topic_searching": "Témakörön belüli keresés engedélyezése", + "topic_search_help": "Ha engedélyezett, a témakörön belüli keresés felülírja az alapértelmezett keresési viselkedést, és ezáltal az egész témakörben keresel, nem csak a képernyőn megjelenőkben", + "update_url_with_post_index": "Témák böngészése közben frissítse az URL-t a bejegyzés indexével", + "scroll_to_my_post": "Válaszolást követően az új hozzászólás megjelenítése", + "follow_topics_you_reply_to": "Témakör figyelése, melyre válaszolsz", + "follow_topics_you_create": "Témakör figyelése, amit létrehozol", + "grouptitle": "Csoport címe", + "group-order-help": "Válassz ki egy csoportot és használd a nyilakat, hogy elrendezd a címeket", + "no-group-title": "Nincs csoportcím", + "select-skin": "Válassz egy kinézetet", + "select-homepage": "Válasz egy kezdőlapot", + "homepage": "Kezdőlap", + "homepage_description": "Válasz egy oldalt a fórum kezdőlapjához, vagy az alapértelmezett kezdőlaphoz a 'Nincs' lehetőséget.", + "custom_route": "Egyéni kezdőlap útvonal", + "custom_route_help": "Adj meg egy útvonalnevet (pl. \"legújabb\", vagy \"népszerű\")", + "sso.title": "Egyszeri bejelentkezési szolgáltatás", + "sso.associated": "Társítás", + "sso.not-associated": "Kattints ide a társításhoz", + "sso.dissociate": "Leválasztás", + "sso.dissociate-confirm-title": "Leválasztás megerősítése", + "sso.dissociate-confirm": "Biztos le akarod választani a fiókod (%1) ?", + "info.latest-flags": "Legutóbbi megjelölések", + "info.no-flags": "Nem található megjelölt hozzászólás", + "info.ban-history": "Kitiltási előzmény", + "info.no-ban-history": "A felhasználó sosem volt kitiltva", + "info.banned-until": "Kitiltás lejárata: %1", + "info.banned-expiry": "Lejárat", + "info.banned-permanently": "Végleges kitiltás", + "info.banned-reason-label": "Oka", + "info.banned-no-reason": "Az oka nincs megadva.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Felhasználónév előzmény", + "info.email-history": "E-mail előzmény", + "info.moderation-note": "Moderálási megjegyzés", + "info.moderation-note.success": "Moderálási megjegyzés elmentve", + "info.moderation-note.add": "Megjegyzés hozzáadása", + "sessions.description": "Ez az oldal hozzáférést biztosít a fórumon történő összes munkamenet megfigyeléséhez és ha szükséges azok megszüntéséhez. A saját munkameneted úgy tudod csak megszakítani, hogyha kijelentkezel.", + "consent.title": "Jogaid & hozzájárulásod", + "consent.lead": "Ezen közösségi fórum összegyűjti és feldolgozza személyes információid.", + "consent.intro": "Ezen információkat szigorúan csakis arra használjuk, hogy élményedet személyre szólóvá tegyük a közösségben, valamint hogy hozzászólásaidat társítsuk felhasználói fiókoddal. A regisztrációs lépés során egy felhasználónév és email cím megadására kértünk, a weboldalon nem kötelezően megadhatsz még további információkat is felhasználói profilod kiegészítéséhez.

Ezen információkat fiókod létezéséig megőrizzük, fiókod törlésével ugyanakkor jóváhagyásodat bármikor visszavonhatod. Bármikor kérelmezhetsz másolatot a weboldalhoz való hozzájárulásodról a Jogok & Jóváhagyás oldalon.

Ha bármi kérdésed vagy gondod adódna, azt javasoljuk érd el a fórum adminisztratív csapatát.", + "consent.email_intro": "Alkalomadtán email-eket küldhetünk regisztrált email címedre annak érdekében, hogy frissítésekkel lássunk el és/vagy hogy értesítsünk a számodra releváns tevékenységekről. Testreszabhatod a közösségi kivonatot (beleértve annak azonnali letiltását), valamint kiválaszthatod, hogy mely értesítés típusokat kapd email-ben, a felhasználói beállítások lapon keresztül.", + "consent.digest_frequency": "Hacsak nincs kifejezetten beállítva felhasználói beállításokban, ez a közösség email kivonatokat kézbesít minden %1.", + "consent.digest_off": "Hacsak nincs kifejezetten beállítva felhasználói beállításokban, ez a közösség nem küld ki email kivonatokat", + "consent.received": "Jóváhagytad a weboldal számára, hogy információt gyűjtsön rólad majd feldolgozza azt. Nincs szükség további intézkedésre.", + "consent.not_received": "Nem adtad jóváhagyásod az adatgyűjtésre és -feldolgozásra. A weboldal adminisztrációja bármikor úgy határozhat, hogy törli fiókodat az Általános adatvédelmi rendeletnek való elégtétel érdekében.", + "consent.give": "Hozzájárulás", + "consent.right_of_access": "Jogodban áll a hozzáférés", + "consent.right_of_access_description": "Jogodban áll kérésre hozzáférni bármilyen, a weboldal által gyűjtött adathoz. Másolatot kaphatsz ezen adatokról alább a megfelelő gombra kattintva.", + "consent.right_to_rectification": "Jogodban áll helyesbíteni", + "consent.right_to_rectification_description": "Jogodban áll módosítani vagy frissíteni bármilyen, részünkre átnyújtott pontatlan adatot. Profilod annak szerkesztésével frissíthető, ugyanúgy a hozzászólások tartalma is. Ha ez nem így volna, kérlek vedd fel a kapcsolatot az oldal adminisztratív csapatával.", + "consent.right_to_erasure": "Jogodban áll törölni", + "consent.right_to_erasure_description": "Az adatgyűjtésre és/vagy feldolgozásra adott jóváhagyásodat bármikor hatálytalaníthatod fiókod törlésével. Noha egyéni profilod törlésre ítélhető, közzétett tartalmaid megmaradnak. Ha törölni kívánod mind profilod és tartalmaid, kérlek lépj kapcsolatba az oldal adminisztratív csapatával.", + "consent.right_to_data_portability": "Jogodban áll az adathordozhatóság", + "consent.right_to_data_portability_description": "Kérelmezhetsz tőlünk egy gép által olvasható kivonatot bármilyen, a rólad és fiókodról gyűjtött adatról. Ezt alább a megfelelő gomb megnyomásával teheted meg.", + "consent.export_profile": "Profil exportálása (.json)", + "consent.export-profile-success": "Profil exportálása, értesítéssel, hogyha végzett a művelet.", + "consent.export_uploads": "Feltöltött tartalom exportálása (.zip)", + "consent.export-uploads-success": "Feltöltött tartalom exportálása, értesítéssel, hogyha végzett a művelet.", + "consent.export_posts": "Bejegyzések exportálása (.csv)", + "consent.export-posts-success": "Bejegyzések exportálása, értesítéssel, hogyha végzett a művelet.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/hu/users.json b/public/language/hu/users.json new file mode 100644 index 0000000000..7368d05ea8 --- /dev/null +++ b/public/language/hu/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Legújabb felhasználók", + "top_posters": "Legaktívabbak", + "most_reputation": "Legnépszerűbbek", + "most_flags": "Legtöbb megjelölés", + "search": "Keresés", + "enter_username": "Írj be egy felhasználónevet kereséshez", + "search-user-for-chat": "Search a user to start chat", + "load_more": "További betöltése", + "users-found-search-took": "%1 talált felhasználó! A keresés %2 másodpercig tartott.", + "filter-by": "Szűrés", + "online-only": "Csak elérhető", + "invite": "Meghívás", + "prompt-email": "Emailek:", + "groups-to-join": "Csatlakozásra váró csoportok miután a meghívás el lett fogadva:", + "invitation-email-sent": "Egy meghívó e-mail el lett küldve %1 részére", + "user_list": "Felhasználói lista", + "recent_topics": "Legutóbbi témakörök", + "popular_topics": "Népszerű témakörök", + "unread_topics": "Olvasatlan témakörök", + "categories": "Kategóriák", + "tags": "Címkék", + "no-users-found": "Nem található ilyen felhasználó!" +} \ No newline at end of file diff --git a/public/language/hy/_DO_NOT_EDIT_FILES_HERE.md b/public/language/hy/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/hy/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/hy/admin/admin.json b/public/language/hy/admin/admin.json new file mode 100644 index 0000000000..f0d9c0c48c --- /dev/null +++ b/public/language/hy/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Վստա՞հ եք, որ ցանկանում եք վերակառուցել ու վերագործարկել NodeBB-ն։", + "alert.confirm-restart": "Վստա՞հ եք, որ ցանկանում եք վերագործարկել NodeBB-ն։", + + "acp-title": "%1 | NodeBB Կառավարման Վահանակ", + "settings-header-contents": "Պարունակություն", + "changes-saved": "Փոփոխությունները պահպանված են", + "changes-saved-message": "Կարգավորումների փոփոխությունները պահպանված են", + "changes-not-saved": "Փոփոխությունները պահպանված չեն", + "changes-not-saved-message": "Փոփոխությունների պահպանման հետ խնդիր կա (%1)" +} \ No newline at end of file diff --git a/public/language/hy/admin/advanced/cache.json b/public/language/hy/admin/advanced/cache.json new file mode 100644 index 0000000000..8fdec405c2 --- /dev/null +++ b/public/language/hy/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Գրառման քեշ", + "group-cache": "Խմբի քեշ", + "local-cache": "Տեղական քեշ", + "object-cache": "Օբյեկտի քեշ", + "percent-full": "%1%Լրիվ", + "post-cache-size": "Գրառման քեշի չափը", + "items-in-cache": "Նյութեր քեշում" +} \ No newline at end of file diff --git a/public/language/hy/admin/advanced/database.json b/public/language/hy/admin/advanced/database.json new file mode 100644 index 0000000000..8ce5449645 --- /dev/null +++ b/public/language/hy/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime վայրկյաններով", + "uptime-days": "Աշխատանքի ժամանակ օրերով", + + "mongo": "Mongo", + "mongo.version": "MongoDB տարբերակ", + "mongo.storage-engine": "Պահեստի շարժիչ", + "mongo.collections": "Հավաքածուներ", + "mongo.objects": "Օբյեկտներ", + "mongo.avg-object-size": "Օբյեկտի չափը", + "mongo.data-size": "Տվյալների ծավալ ", + "mongo.storage-size": "Պահեստի ծավալ", + "mongo.index-size": "Ցուցանիշի չափը", + "mongo.file-size": "Ֆայլի չափ", + "mongo.resident-memory": " Resident հիշողություն", + "mongo.virtual-memory": "Վիրտուալ հիշողություն", + "mongo.mapped-memory": "Քարտեզագրված հիշողություն", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Հարցումների քանակը", + "mongo.raw-info": "MongoDB Raw տեղեկատվություն", + "mongo.unauthorized": "NodeBB-ն չկարողացավ հարցումներ կատարել MongoDB տվյալների բազայում համապատասխան վիճակագրության համար: Խնդրում ենք համոզվել, որ NodeBB-ի կողմից օգտագործվող օգտատերը պարունակում է «clusterMonitor» դերը «ադմինիստրատորի» տվյալների բազա։", + + "redis": "Redis", + "redis.version": "Redis տարբերակ ", + "redis.keys": "Բանալիներ", + "redis.expires": "Ժամկետը լրանում է", + "redis.avg-ttl": "Միջին TTL", + "redis.connected-clients": "Կապակցված հաճախորդներ", + "redis.connected-slaves": "Կապակցված Slaves", + "redis.blocked-clients": "Արգելափակված հաճախորդներ", + "redis.used-memory": "Օգտագործված հիշողություն", + "redis.memory-frag-ratio": "Հիշողության մասնատման հարաբերակցությունը", + "redis.total-connections-recieved": "Ընդամենը ստացված կապեր", + "redis.total-commands-processed": "Ընդհանուր հրամանները մշակված են", + "redis.iops": "Ակնթարթային օպերացիա.", + "redis.iinput": "Մեկ վայրկյանում ակնթարթային մուտքագրում", + "redis.ioutput": "Ակնթարթային ելք մեկ վայրկյանում", + "redis.total-input": "Ընդհանուր մուտքագրում", + "redis.total-output": "Ընդհանուր արտադրանք", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw ինֆորմացիա", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/hy/admin/advanced/errors.json b/public/language/hy/admin/advanced/errors.json new file mode 100644 index 0000000000..8b3d41ab7c --- /dev/null +++ b/public/language/hy/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 իրադարձություն օրական", + "error.404": "404 Չի գտնվել", + "error.503": "503 Ծառայությունն անհասանելի է", + "manage-error-log": "Կարգավորել Սխալների մատյանը", + "export-error-log": "Արտահանման սխալների մատյան (CSV)", + "clear-error-log": "Մաքրել սխալների գրանցամատյանը", + "route": "Ուղագիծ", + "count": "Հաշիվ", + "no-routes-not-found": "Ուռա՜ 404 սխալ չկա:", + "clear404-confirm": "Վստա՞հ եք, որ ցանկանում եք ջնջել 404 սխալի մատյանները:", + "clear404-success": "«404 Չի գտնվել» սխալները ջնջվեցին" +} \ No newline at end of file diff --git a/public/language/hy/admin/advanced/events.json b/public/language/hy/admin/advanced/events.json new file mode 100644 index 0000000000..d293e7829a --- /dev/null +++ b/public/language/hy/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Իրադարձություններ", + "no-events": "Իրադարձություններ չկան", + "control-panel": "Իրադարձությունների կառավարման վահանակ", + "delete-events": "Ջնջել իրադարձությունները ", + "confirm-delete-all-events": "վստա՞հ եք, որ ուզում եք ջնջել գրանցված բոլոր իրադարձությունները:", + "filters": "Ֆիլտրներ", + "filters-apply": "Կիրառել ֆիլտրներ", + "filter-type": "Իրադարձության տեսակը", + "filter-start": "Մեկնարկի ամսաթիվ", + "filter-end": "Ավարտի ամսաթիվ", + "filter-perPage": "Մեկ էջի համար" +} \ No newline at end of file diff --git a/public/language/hy/admin/advanced/logs.json b/public/language/hy/admin/advanced/logs.json new file mode 100644 index 0000000000..930bd5434a --- /dev/null +++ b/public/language/hy/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Վերբեռնել տեղեկամատյաններ", + "control-panel": "Տեղեկամատյաններ կառավարման վահանակ", + "reload": "Վերբեռնել տեղեկամատյանները", + "clear": "Մաքրել տեղեկամատյանները", + "clear-success": "Տեղեկամատյանները մաքրված են:" +} \ No newline at end of file diff --git a/public/language/hy/admin/appearance/customise.json b/public/language/hy/admin/appearance/customise.json new file mode 100644 index 0000000000..31ac96dd6a --- /dev/null +++ b/public/language/hy/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Հատուկ CSS/LESS", + "custom-css.description": "Մուտքագրեք ձեր սեփական CSS/LESS հայտարարագրերն այստեղ, որոնք կկիրառվեն բոլոր մյուս ոճերից հետո:", + "custom-css.enable": "Միացնել Custom CSS/LESS-ը", + + "custom-js": "Custom Javascript", + "custom-js.description": "Մուտքագրեք ձեր սեփական javascript-ն այստեղ: Այն կկատարվի էջն ամբողջությամբ բեռնվելուց հետո:", + "custom-js.enable": "Միացնել Custom Javascript-ը", + + "custom-header": "Պատվերով վերնագիր", + "custom-header.description": "Մուտքագրեք հատուկ HTML-ն այստեղ (օրինակ՝ Meta Tags և այլն), որը կկցվի <head> ձեր ֆորումի նշագրման բաժինը: Սկրիպտի պիտակները թույլատրվում են, բայց չեն խրախուսվում, քանի որ հասանելի է Custom Javascript ներդիրը:", + "custom-header.enable": "Միացնել հատուկ Վերնագիրը", + + "custom-css.livereload": "Միացնել Live Reload-ը", + "custom-css.livereload.description": "Միացրեք սա՝ ձեր հաշվի տակ գտնվող յուրաքանչյուր սարքի բոլոր աշխատաշրջանները ստիպելու համար թարմացնել, երբ սեղմեք «Պահել» կոճակը" +} \ No newline at end of file diff --git a/public/language/hy/admin/appearance/skins.json b/public/language/hy/admin/appearance/skins.json new file mode 100644 index 0000000000..8d5e163d25 --- /dev/null +++ b/public/language/hy/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Բեռնվում է Skins...", + "homepage": "Գլխավոր էջ", + "select-skin": "Ընտրել շապիկ ", + "current-skin": "Ընթացիկ շապիկ ", + "skin-updated": "Շապիկը թարմացվել է", + "applied-success": "%1 շապիկը հաջողությամբ կիրառվեց", + "revert-success": "Շապիկը վերադարձավ հիմնական գույներին" +} \ No newline at end of file diff --git a/public/language/hy/admin/appearance/themes.json b/public/language/hy/admin/appearance/themes.json new file mode 100644 index 0000000000..1075a0190d --- /dev/null +++ b/public/language/hy/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Տեղադրված թեմաների ստուգում...", + "homepage": "Գլխավոր էջ", + "select-theme": "Ընտրեք թեմա", + "current-theme": "Ընթացիկ թեմա", + "no-themes": "Տեղադրված թեմաներ չեն գտնվել", + "revert-confirm": "Վստա՞հ եք, որ ցանկանում եք վերականգնել կանխադրված NodeBB թեման:", + "theme-changed": "Թեման փոխվել է", + "revert-success": "Դուք հաջողությամբ վերադարձրել եք ձեր NodeBB-ն իր default թեմային:", + "restart-to-activate": "Խնդրում ենք վերակառուցել և վերագործարկել ձեր NodeBB-ը՝ այս թեման ամբողջությամբ ակտիվացնելու համար:" +} \ No newline at end of file diff --git a/public/language/hy/admin/dashboard.json b/public/language/hy/admin/dashboard.json new file mode 100644 index 0000000000..6ea320876a --- /dev/null +++ b/public/language/hy/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Ֆորումի թրաֆիկ ", + "page-views": "Էջի դիտումներ", + "unique-visitors": "Եզակի այցելուներ", + "logins": "Մուտքագրումներ", + "new-users": "Նոր օգտատերեր", + "posts": "Գրառումներ", + "topics": "Թեմաներ", + "page-views-seven": "Վերջին 7 օրը", + "page-views-thirty": "Վերջին 30 օրը", + "page-views-last-day": "Վերջին 24 ժամը", + "page-views-custom": "Հատուկ ամսաթվերի միջակայք", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Միջակայքի վերջ", + "page-views-custom-help": "Մուտքագրեք էջի դիտումների ամսաթվերի միջակայքը, որը ցանկանում եք դիտել: Եթե ամսաթվերի ընտրիչ չկա, ապա ընդունված ձևաչափն է՝ ՏՏՏՏ-ԱՄ-ՕՕ", + "page-views-custom-error": "Խնդրում ենք մուտքագրել վավեր ամսաթվերի միջակայք՝ YYYY-MM-DD ձևաչափով", + + "stats.yesterday": "Երեկ", + "stats.today": "Այսօր ", + "stats.last-week": "Անցած շաբաթ", + "stats.this-week": "Այս շաբաթ", + "stats.last-month": "Անցած ամիս", + "stats.this-month": "Այս ամիս", + "stats.all": "Ամբողջ ժամանակ", + + "updates": "Թարմացումներ", + "running-version": "Դուք աշխատում եք NodeBB v%1-ում:", + "keep-updated": "Միշտ համոզվեք, որ ձեր NodeBB-ն արդիական է անվտանգության վերջին պատչերի և վրիպակների շտկման համար:", + "up-to-date": "Դուք up-to-date-եք", + "upgrade-available": "Թողարկվել է նոր տարբերակ (v%1): Մտածեք ձեր NodeBB-ի թարմացման մասին:", + "prerelease-upgrade-available": "Սա NodeBB-ի նախնական թողարկման հնացած տարբերակն է: Թողարկվել է նոր տարբերակ (v%1): Մտածեք ձեր NodeBB-ի թարմացման մասին:", + "prerelease-warning": "Սա NodeBB-ի նախնական թողարկումն է: Կարող են առաջանալ չնախատեսված սխալներ:", + "fallback-emailer-not-found": "Հետադարձ էլփոստի ուղարկողը չի գտնվել:", + "running-in-development": "Ֆորումն աշխատում է զարգացման ռեժիմում: Ֆորումը կարող է բաց լինել հնարավոր խոցելիության համար. խնդրում ենք կապվել ձեր համակարգի ադմինիստրատորի հետ:", + "latest-lookup-failed": "Չհաջողվեց փնտրել NodeBB-ի վերջին հասանելի տարբերակը", + + "notices": "Ծանուցումներ", + "restart-not-required": "Վերագործարկումը պարտադիր չէ", + "restart-required": "Պահանջվում է վերագործարկում", + "search-plugin-installed": "Տեղադրված է Search Plugin-ը", + "search-plugin-not-installed": "Որոնման հավելվածը տեղադրված չէ", + "search-plugin-tooltip": "Տեղադրեք որոնման պլագին հավելվածի էջից՝ որոնման գործառույթն ակտիվացնելու համար", + + "control-panel": "Համակարգի վերահսկում", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Վերսկսել", + "restart-warning": "Ձեր NodeBB-ի վերակառուցումը կամ վերագործարկումը մի քանի վայրկյանով կգցեն բոլոր գոյություն ունեցող կապերը:", + "restart-disabled": "Ձեր NodeBB-ի վերակառուցումն ու վերագործարկումն անջատված է, քանի որ դուք, կարծես, այն չեք աշխատում համապատասխան դեյմոնի միջոցով:", + "maintenance-mode": "Սպասարկման ռեժիմ", + "maintenance-mode-title": "Սեղմեք այստեղ՝ NodeBB-ի սպասարկման ռեժիմը կարգավորելու համար", + "realtime-chart-updates": "Իրական ժամանակի գծապատկերների թարմացումներ", + + "active-users": "Ակտիվ Օգտատերեր", + "active-users.users": "Օգտատերեր", + "active-users.guests": "Հյուրեր", + "active-users.total": "Ընդամենը", + "active-users.connections": "Կապեր", + + "guest-registered-users": "Հյուր ընդդեմ գրանցված օգտատերի", + "guest": "Հյուր", + "registered": "Գրանցված", + + "user-presence": "Օգտատիրոջ ներկայությունը", + "on-categories": "Կատեգորիաների ցանկում", + "reading-posts": "Գրառումներ կարդալը", + "browsing-topics": "Քննարկվող թեմաներ", + "recent": "Թարմ ", + "unread": "Չկարդացած", + + "high-presence-topics": "Բարձր ներկայության թեմաներ", + "popular-searches": "Հանրաճանաչ որոնումներ", + + "graphs.page-views": "Էջի դիտումներ", + "graphs.page-views-registered": "Գրանցված Էջի դիտումներ ", + "graphs.page-views-guest": "Էջի դիտումներ Հյուր", + "graphs.page-views-bot": "Էջի դիտումների բոտ", + "graphs.unique-visitors": "Եզակի այցելուներ", + "graphs.registered-users": "Գրանցված օգտատերեր", + "graphs.guest-users": "Հյուր օգտատերեր", + "last-restarted-by": "Վերջին անգամ վերագործարկվել է", + "no-users-browsing": "Օգտատերեր չկան", + + "back-to-dashboard": "Վերադառնալ կառավարման վահանակ", + "details.no-users": "Ընտրված ժամկետում ոչ մի օգտատեր չի միացել", + "details.no-topics": "Ընտրված ժամկետում ոչ մի թեմա չի տեղադրվել", + "details.no-searches": "Որոնումներ դեռ չեն կատարվել", + "details.no-logins": "Ընտրված ժամկետում մուտքեր չեն գրանցվել", + "details.logins-static": "NodeBB-ն պահում է միայն %1 օրվա սեսիայի տվյալները, և այսպիսով, ստորև բերված աղյուսակը ցույց կտա միայն վերջին ակտիվ աշխատաշրջանները", + "details.logins-login-time": "Մուտք գործելու ժամանակը" +} diff --git a/public/language/hy/admin/development/info.json b/public/language/hy/admin/development/info.json new file mode 100644 index 0000000000..881192a02e --- /dev/null +++ b/public/language/hy/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Դուք %1:% 2-ում եք", + "ip": " IP % 1", + "nodes-responded": "%1 հանգույցներ արձագանքեցին %2ms-ի սահմաններում:", + "host": "host", + "primary": "առաջնային / գործարկվող աշխատատեղեր", + "pid": "pid", + "nodejs": "nodejs", + "online": "առցանց", + "git": "git", + "process-memory": "գործընթացի հիշողություն", + "system-memory": "Համակարգի հիշողություն", + "used-memory-process": "Օգտագործված հիշողությունը ըստ գործընթացի", + "used-memory-os": "Օգտագործված համակարգի հիշողություն", + "total-memory-os": "Համակարգի ընդհանուր հիշողություն", + "load": "համակարգի ծանրաբեռնվածություն", + "cpu-usage": "cpu օգտագործումը", + "uptime": "գործարկման ժամանակ", + + "registered": "Գրանցված", + "sockets": "Վարդակներ", + "guests": "Հյուրեր", + + "info": "տեղեկատվություն" +} \ No newline at end of file diff --git a/public/language/hy/admin/development/logger.json b/public/language/hy/admin/development/logger.json new file mode 100644 index 0000000000..5ddbe9fb22 --- /dev/null +++ b/public/language/hy/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Լոգերի կարգավորումներ", + "description": "Միացնելով վանդակները, դուք կստանաք տեղեկամատյաններ ձեր տերմինալում: Եթե նշեք ուղի, ապա դրա փոխարեն տեղեկամատյանները կպահվեն ֆայլում: HTTP-ի գրանցումն օգտակար է վիճակագրություն հավաքելու համար, թե ով, երբ և ինչ են մարդիկ մուտք գործում ձեր ֆորումում: Բացի HTTP հարցումները գրանցելուց, մենք կարող ենք նաև գրանցել socket.io իրադարձությունները: Socket.io-ի գրանցումը, redis-cli մոնիտորի հետ համատեղ, կարող է շատ օգտակար լինել NodeBB-ի ինտերիերը սովորելու համար:", + "explanation": "Պարզապես ստուգեք/անջատեք գրանցման կարգավորումները՝ արագ գրանցումը միացնելու կամ անջատելու համար: Վերագործարկման կարիք չկա:", + "enable-http": "Միացնել HTTP գրանցումը", + "enable-socket": "Միացնել socket.io-ի իրադարձությունների գրանցումը", + "file-path": "Ուղի դեպի ֆայլի մատյան ", + "file-path-placeholder": "/path/to/log/file.log ::: թողեք դատարկ՝ ձեր տերմինալ մուտք գործելու համար", + + "control-panel": "Լոգերի կառավարման վահանակ", + "update-settings": "Թարմացրեք լոգերի կարգավորումները" +} \ No newline at end of file diff --git a/public/language/hy/admin/extend/plugins.json b/public/language/hy/admin/extend/plugins.json new file mode 100644 index 0000000000..afe9e21040 --- /dev/null +++ b/public/language/hy/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "թրենդային", + "installed": "Տեղադրված", + "active": "Ակտիվ", + "inactive": "Ոչ ակտիվ", + "out-of-date": "Ժամկետն անց", + "none-found": "Պլագիններ չեն գտնվել:", + "none-active": "Ակտիվ պլագիններ չկան", + "find-plugins": "Գտեք պլագիններ", + + "plugin-search": "Փլագինների որոնում", + "plugin-search-placeholder": "Փնտրեք plugin...", + "submit-anonymous-usage": "Ներկայացրեք հավելումների օգտագործման անանուն տվյալներ:", + "reorder-plugins": "Կրկին կարգավորել պլագինները", + "order-active": "Կարգավորել ակտիվ պլագինները", + "dev-interested": "Հետաքրքրվա՞ծ եք NodeBB-ի համար հավելվածներ գրելով:", + "docs-info": "Փլագինների հեղինակման վերաբերյալ ամբողջական փաստաթղթերը կարելի է գտնել NodeBB Docs Portal-ում:", + + "order.description": "Որոշ պլագիններ իդեալականորեն աշխատում են, երբ դրանք սկզբնավորվում են այլ պլագիններից առաջ/հետո:", + "order.explanation": "Փլագինները բեռնվում են այստեղ նշված հերթականությամբ՝ վերևից ներքև", + + "plugin-item.themes": "Թեմաներ", + "plugin-item.deactivate": "Ապաակտիվացնել", + "plugin-item.activate": "Ակտիվացնել", + "plugin-item.install": "Տեղադրել", + "plugin-item.uninstall": "Հեռացնել", + "plugin-item.settings": "Կարգավորումներ", + "plugin-item.installed": "Տեղադրված", + "plugin-item.latest": "Վերջին", + "plugin-item.upgrade": "Թարմացնել", + "plugin-item.more-info": "Լրացուցիչ տեղեկությունների համար:", + "plugin-item.unknown": "Անհայտ", + "plugin-item.unknown-explanation": "Այս փլագինի վիճակը չհաջողվեց որոշել, հնարավոր է սխալ կազմաձևման սխալի պատճառով:", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Փլագինը միացված է", + "alert.disabled": "Փլագինը անջատված է", + "alert.upgraded": "Փլագինը թարմացվեց", + "alert.installed": "Փլագինը տեղադրված է", + "alert.uninstalled": "Փլագինը ապատեղադրված է", + "alert.activate-success": "Խնդրում ենք վերակառուցել և վերագործարկել ձեր NodeBB-ն՝ այս հավելվածն ամբողջությամբ ակտիվացնելու համար", + "alert.deactivate-success": "Փլագինը հաջողությամբ ապաակտիվացվեց", + "alert.upgrade-success": "Խնդրում ենք վերակառուցել և վերագործարկել ձեր NodeBB-ն՝ այս հավելվածն ամբողջությամբ թարմացնելու համար:", + "alert.install-success": "Plugin-ը հաջողությամբ տեղադրվեց, խնդրում ենք ակտիվացնել plugin-ը:", + "alert.uninstall-success": "Փլագինը հաջողությամբ ապաակտիվացվել և ապատեղադրվել է:", + "alert.suggest-error": "NodeBB-ն չկարողացավ հասնել փաթեթի կառավարիչին, շարունակեք վերջին տարբերակի տեղադրումը: Սերվերը վերադարձվեց (%1). %2", + "alert.package-manager-unreachable": "NodeBB-ն չկարողացավ հասնել փաթեթի կառավարիչին, այս պահին թարմացում չի առաջարկվում:", + "alert.incompatible": "NodeBB-ի ձեր տարբերակը (v%1) ջնջվում է միայն այս փլագինի v% 2-ին թարմացնելու համար: Խնդրում ենք թարմացնել ձեր NodeBB-ն, եթե ցանկանում եք տեղադրել այս հավելվածի ավելի նոր տարբերակը:", + "alert.possibly-incompatible": "Համատեղելիության մասին տեղեկություն չի գտնվել Այս փլագինը չի նշել տեղադրման հատուկ տարբերակ՝ հաշվի առնելով ձեր NodeBB տարբերակը: Լրիվ համատեղելիությունը չի կարող երաշխավորվել, և կարող է պատճառ դառնալ, որ ձեր NodeBB-ն այլևս պատշաճ կերպով չգործարկվի: Այն դեպքում, երբ NodeBB-ն չի կարող ճիշտ բեռնել:$ ./nodebb reset plugin=\"%1\"Շարունակե՞լ տեղադրել այս փլագինի վերջին տարբերակը:", + "alert.reorder": "Փլագինները նորից պատվիրված են", + "alert.reorder-success": "Խնդրում ենք վերակառուցել և վերագործարկել ձեր NodeBB-ն՝ գործընթացն ամբողջությամբ ավարտելու համար:", + + "license.title": "Plugin-ի լիցենզիայի մասին տեղեկատվություն", + "license.intro": "%1 հավելվածը լիցենզավորված է %2-ի ներքո: Խնդրում ենք կարդալ և հասկանալ լիցենզիայի պայմանները նախքան այս փլագինը ակտիվացնելը:", + "license.cta": "Ցանկանու՞մ եք շարունակել ակտիվացնել այս plugin-ը:" +} diff --git a/public/language/hy/admin/extend/rewards.json b/public/language/hy/admin/extend/rewards.json new file mode 100644 index 0000000000..d6cc07117f --- /dev/null +++ b/public/language/hy/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Պարգևներ", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Պահանջվում է պարգևատրման գումարը ", + "zero-infinite": "Մուտքագրեք 0 անսահմանության համար", + "delete": "Ջնջել", + "enable": "Միացնել", + "disable": "Անջատել", + + "alert.delete-success": "Պարգևը հաջողությամբ ջնջվեց", + "alert.no-inputs-found": "Անօրինական պարգև. մուտքեր չեն գտնվել:", + "alert.save-success": "Պարգևները հաջողությամբ պահվեցին" +} \ No newline at end of file diff --git a/public/language/hy/admin/extend/widgets.json b/public/language/hy/admin/extend/widgets.json new file mode 100644 index 0000000000..0418f90bf3 --- /dev/null +++ b/public/language/hy/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Հասանելի վիդջեթներ", + "explanation": "Բացվող ընտրացանկից ընտրեք վիջեթ, այնուհետև քաշեք և թողեք այն ձախ կողմում գտնվող ձևանմուշի վիդջեթի տարածք:", + "none-installed": "Վիջեթներ չեն գտնվել: Ակտիվացրեք վիջեթի հիմնական հավելվածը plugins կառավարման վահանակում:", + "clone-from": "Կլոնավորել վիջեթներ-ից", + "containers.available": "Առկա Containers", + "containers.explanation": "Քաշեք և թողեք ցանկացած ակտիվ վիջեթի վերևում", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel-ի վերնագիր", + "container.panel-body": "Panel Body", + "container.alert": "Զգուշացում", + + "alert.confirm-delete": "Վստա՞հ եք, որ ցանկանում եք ջնջել այս վիջեթը:", + "alert.updated": "Վիջեթները թարմացվել են", + "alert.update-success": "Վիջեթները հաջողությամբ թարմացվեցին", + "alert.clone-success": "Վիդջեթները հաջողությամբ կլոնավորվեցին", + + "error.select-clone": "Խնդրում ենք ընտրել էջ, որտեղից կլոնավորվել", + + "title": "Վերնագիր", + "title.placeholder": "Վերնագիր (ցուցադրված է միայն որոշ կոնտեյներների վրա)", + "container": "Կոնտեյներ", + "container.placeholder": "Քաշեք և գցեք կոնտեյներ կամ մուտքագրեք HTML այստեղ:", + "show-to-groups": "Ցույց տալ խմբերին ", + "hide-from-groups": "Թաքցնել խմբերից", + "hide-on-mobile": "Թաքցնել բջջայինի վրա" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/admins-mods.json b/public/language/hy/admin/manage/admins-mods.json new file mode 100644 index 0000000000..290105ef43 --- /dev/null +++ b/public/language/hy/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Ադմինիստրատորներ", + "global-moderators": "Ընդհանուր մոդերատորներ", + "moderators": "Մոդերատորներ ", + "no-global-moderators": "Ընդհանուր մոդերատորներ չկան", + "no-sub-categories": "Ենթակատեգորիաներ չկան", + "subcategories": " %1 ենթակատեգորիաներ", + "no-moderators": "Մոդերատորներ չկան", + "add-administrator": "Ավելացնել ադմինիստրատոր", + "add-global-moderator": "Ավելացնել ընդհանուր մոդերատոր", + "add-moderator": "Ավելացնել Մոդերատոր" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/categories.json b/public/language/hy/admin/manage/categories.json new file mode 100644 index 0000000000..e14f435c7c --- /dev/null +++ b/public/language/hy/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Կատեգորիայի կարգավորումներ", + "privileges": "Արտոնություններ", + + "name": "Կատեգորիայի անվանումը", + "description": "Կատեգորիայի նկարագրություն", + "bg-color": "Ֆոնի գույնը", + "text-color": "Տեքստի գույն ", + "bg-image-size": "Ֆոնային նկարի չափը", + "custom-class": "Հարմարեցված դաս", + "num-recent-replies": "Վերջին պատասխանների թիվը", + "ext-link": "Արտաքին հղում", + "subcategories-per-page": "Ենթակատեգորիաներ մեկ էջի համար", + "is-section": "Վերաբերվեք այս կատեգորիային որպես բաժին", + "post-queue": "Գրառման հերթ", + "tag-whitelist": "Նշեք Whitelist", + "upload-image": "Վերբեռնել նկար", + "delete-image": "Հեռացնել ", + "category-image": "Կատեգորիայի նկար ", + "parent-category": "Ծնողի կատեգորիա", + "optional-parent-category": "(Ըստ ցանկության) Ծնողի կատեգորիա", + "top-level": "Բարձր մակարդակ", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Պատճենել կարգավորումները ", + "optional-clone-settings": "(Ըստ ցանկության) Կլոնի կարգավորումներ կատեգորիայից", + "clone-children": "Կլոնավորել դուստր կատեգորիաները և կարգավորումները", + "purge": "Մաքրման կատեգորիա", + + "enable": "Միացնել ", + "disable": "Անջատել", + "edit": "Խմբագրել ", + "analytics": "Վերլուծություն", + "view-category": "Դիտել կատեգորիա ", + "set-order": "Սահմանել կարգը", + "set-order-help": "Կատեգորիայի կարգը սահմանելը այս կատեգորիան կտեղափոխի այդ կարգը և անհրաժեշտության դեպքում կթարմացնի այլ կատեգորիաների հերթականությունը: Նվազագույն պատվերը 1-ն է, որը դասակարգում է վերևում:", + + "select-category": "Ընտրել կատեգորիա ", + "set-parent-category": "Սահմանել ծնողների կատեգորիա", + + "privileges.description": "Դուք կարող եք կարգավորել մուտքի վերահսկման արտոնությունները կայքի մասերի համար այս բաժնում: Արտոնությունները կարող են տրվել յուրաքանչյուր օգտատիրոջ կամ խմբի համար: Ընտրեք ազդեցության տիրույթը ներքևի բացվող ցանկից:", + "privileges.category-selector": "Արտոնությունների կարգավորում", + "privileges.warning": "Նշում. արտոնությունների կարգավորումներն ուժի մեջ են մտնում անմիջապես: Այս կարգավորումները կարգավորելուց հետո անհրաժեշտ չէ պահպանել կատեգորիան:", + "privileges.section-viewing": "Դիտման արտոնություններ", + "privileges.section-posting": "Արտոնությունների հրապարակում ", + "privileges.section-moderation": "Չափավորության արտոնություններ", + "privileges.section-other": "Այլ", + "privileges.section-user": "Օգտատեր", + "privileges.search-user": "Ավելացնել օգտատեր", + "privileges.no-users": "Այս կատեգորիայում օգտատիրոջ հատուկ արտոնություններ չկան:", + "privileges.section-group": "Խումբ", + "privileges.group-private": "Այս խումբը մասնավոր է", + "privileges.inheritance-exception": "Այս խումբը արտոնություններ չի ստանում գրանցված օգտվողների խմբից", + "privileges.banned-user-inheritance": "Արգելված օգտվողները ստանում են արտոնություններ արգելված օգտվողների խմբից", + "privileges.search-group": "Ավելացնել խումբ", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Պատճենել կատեգորիայից", + "privileges.copy-privileges-to-all-categories": "Պատճենել բոլոր կատեգորիաներին", + "privileges.copy-group-privileges-to-children": "Արտոնությունները պատճենված են.", + "privileges.copy-group-privileges-to-all-categories": "Պատճենեք այս խմբի արտոնությունները բոլոր կատեգորիաներում:", + "privileges.copy-group-privileges-from": "Պատճենեք այս խմբի արտոնությունները մեկ այլ կատեգորիայից:", + "privileges.inherit": "Եթե գրանցված օգտատերերի խմբին տրվում է հատուկ արտոնություն, բոլոր մյուս խմբերը ստանում են անուղղակի արտոնություն, նույնիսկ եթե դրանք հստակորեն սահմանված/ստուգված չեն: Այս անուղղակի արտոնությունը ցույց է տրված ձեզ, քանի որ բոլոր օգտվողները գրանցված օգտվողների օգտատերերի խմբի մաս են կազմում, և, հետևաբար, լրացուցիչ խմբերի համար արտոնություններ պետք չէ հստակորեն տրամադրել:", + "privileges.copy-success": "Արտոնությունները պատճենված են:", + + "analytics.back": "Վերադարձ դեպի Կատեգորիաներ", + "analytics.title": "Վերլուծություն «%1» կատեգորիայի համար", + "analytics.pageviews-hourly": "Նկար 1 & ndash; Էջի ժամային դիտումներ այս կատեգորիայի համար", + "analytics.pageviews-daily": "Նկար 2 & ndash; Էջի ամենօրյա դիտումներ այս կատեգորիայի համար", + "analytics.topics-daily": "Նկար 3 & ndash; Այս կատեգորիայում ստեղծված ամենօրյա թեմաներ", + "analytics.posts-daily": "Նկար 4 & ndash; Այս կատեգորիայի ամենօրյա գրառումները", + + "alert.created": "Ստեղծվել է ", + "alert.create-success": "Կատեգորիան հաջողությամբ ստեղծվեց:", + "alert.none-active": "Դուք չունեք ակտիվ կատեգորիաներ:", + "alert.create": "Ստեղծել կատեգորիա", + "alert.confirm-purge": "Վստա՞հ եք, որ ուզում եք մաքրել այս «%1» կատեգորիան: Զգուշացում: Այս կատեգորիայի բոլոր թեմաներն ու գրառումները կջնջվեն: Կատեգորիայի մաքրումը կհեռացնի բոլոր թեմաներն ու գրառումները և կջնջի կատեգորիան տվյալների բազայից: Եթե ցանկանում եք ժամանակավորապես հեռացնել կատեգորիան, փոխարենը կցանկանաք «անջատել» կատեգորիան:", + "alert.purge-success": "Կատեգորիան մաքրվել է:", + "alert.copy-success": "Կարգավորումները պատճենվեցին:", + "alert.set-parent-category": "Սահմանել ծնողների կատեգորիա", + "alert.updated": "Թարմացված կատեգորիաներ", + "alert.updated-success": "Կատեգորիայի ID-ները %1 հաջողությամբ թարմացվեցին:", + "alert.upload-image": "Վերբեռնեք կատեգորիայի նկարը", + "alert.find-user": "Որոնել օգտատիրոջ", + "alert.user-search": "Որոնել օգտատիրոջը այստեղ...", + "alert.find-group": "Գտնել խումբ", + "alert.group-search": "Որոնեք խումբ այստեղ...", + "alert.not-enough-whitelisted-tags": "Whitelist-ում պիտակները պակաս են, քան նվազագույն պիտակները, դուք պետք է ավելի շատ պիտակներ ստեղծեք Whitelist-ում:", + "collapse-all": "Փակել բոլորը", + "expand-all": "Ընդարձակել Բոլորը", + "disable-on-create": "Անջատել ստեղծելիս", + "no-matches": "Համընկնումներ չկան" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/digest.json b/public/language/hy/admin/manage/digest.json new file mode 100644 index 0000000000..f6179343b0 --- /dev/null +++ b/public/language/hy/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Ստորև ցուցադրվում է առաքման վիճակագրության և ժամերի ցանկը:", + "disclaimer": "Խնդրում ենք նկատի ունենալ, որ էլփոստի առաքումը երաշխավորված չէ էլփոստի տեխնոլոգիայի բնույթի պատճառով: Շատ փոփոխականներ ազդում են այն բանի վրա, թե արդյոք ստացող սերվերին ուղարկված նամակն ի վերջո առաքվում է օգտվողի մուտքի արկղ, ներառյալ սերվերի հեղինակությունը, սև ցուցակում ներառված IP հասցեները և արդյոք DKIM/SPF/DMARC կազմաձևված է:", + "disclaimer-continued": "Հաջող առաքումը նշանակում է, որ հաղորդագրությունը հաջողությամբ ուղարկվել է NodeBB-ի կողմից և հաստատվել է ստացողի սերվերի կողմից: Դա չի նշանակում, որ նամակը հայտնվել է մուտքի արկղում: Լավագույն արդյունքների համար խորհուրդ ենք տալիս օգտագործել երրորդ կողմի էլփոստի առաքման ծառայություն, ինչպիսին է SendGrid-ը:", + + "user": "Օգտատեր", + "subscription": "Բաժանորդագրության տեսակը", + "last-delivery": "Վերջին հաջող առաքում", + "default": "System default", + "default-help": "Համակարգի հիմնական կարգավորումը նշանակում է, որ օգտատերը բացահայտորեն չի անտեսել ամփոփումների համընդհանուր ֆորումի կարգավորումը, որն այժմ հետևյալն է՝ «%1»", + "resend": "Կրկին ուղարկել ամփոփագիրը", + "resend-all-confirm": "Համոզվա՞ծ եք, որ ցանկանում եք ձեռքով կատարել այս ամփոփումը:", + "resent-single": "Ձեռքով ամփոփագրի վերաուղարկումն ավարտված է", + "resent-day": "Օրական ամփոփագրեր ", + "resent-week": "Շաբաթական ամփոփագրի resent", + "resent-biweek": "Երկու շաբաթը մեկ ամփոփագրի resent", + "resent-month": "Ամսեկան ամփոփագրի resent", + "null": "Երբեք", + "manual-run": "Ձեռքով ամփոփում.", + + "no-delivery-data": "Առաքման տվյալներ չեն գտնվել" +} diff --git a/public/language/hy/admin/manage/groups.json b/public/language/hy/admin/manage/groups.json new file mode 100644 index 0000000000..de2383b234 --- /dev/null +++ b/public/language/hy/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Խմբի անուն", + "badge": "Նշան", + "properties": "Հատկություններ", + "description": "Խմբի նկարագրություն", + "member-count": "Անդամների թիվը", + "system": "Համակարգ", + "hidden": "Թակնված", + "private": "Անձնական ", + "edit": "Խմբագրել ", + "delete": "Ջնջել", + "privileges": "Արտոնություններ ", + "download-csv": "CSV", + "search-placeholder": "Որոնում ", + "create": "Ստեղծել խումբ", + "description-placeholder": "Ձեր խմբի մասին կարճ նկարագրություն", + "create-button": "Ստեղծել ", + + "alerts.create-failure": "Uh-Oh Ձեր խումբը ստեղծելիս խնդիր առաջացավ: Խնդրում ենք փորձել ավելի ուշ!", + "alerts.confirm-delete": "Վստա՞հ եք, որ ցանկանում եք ջնջել այս խումբը:", + + "edit.name": "Անուն ", + "edit.description": "Նկարագրություն ", + "edit.user-title": "Անդամների կոչում", + "edit.icon": "Խմբի պատկերակ", + "edit.label-color": "Խմբի պիտակի գույնը", + "edit.text-color": "Խմբի տեքստի գույնը ", + "edit.show-badge": "Ցույց տալ նշանը", + "edit.private-details": "Եթե միացված է, խմբերին միանալը պահանջում է խմբի սեփականատիրոջ թույլտվությունը:", + "edit.private-override": "Զգուշացում․ մասնավոր խմբերն անջատված են համակարգի մակարդակով, ինչը անտեսում է այս տարբերակը։", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Արգելել օգտատերերին դուրս գալ խմբից", + "edit.hidden": "Թաքնված", + "edit.hidden-details": "Եթե միացված է, այս խումբը չի գտնվի խմբերի ցանկում, և օգտատերերը պետք է ձեռքով հրավիրվեն", + "edit.add-user": "Ավելացնել Օգտատիրոջը խումբ ", + "edit.add-user-search": "Օգտատերերի որոնում ", + "edit.members": "Անդամների ցուցակ", + "control-panel": "Խմբերի կառավարման վահանակ", + "revert": "Վերադարձ", + + "edit.no-users-found": "Օգտատերեր չեն գտնվել", + "edit.confirm-remove-user": "Վստա՞հ եք, որ ուզում եք հեռացնել այս օգտվողին:", + "edit.save-success": "Փոփոխությունները պահված են:" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/privileges.json b/public/language/hy/admin/manage/privileges.json new file mode 100644 index 0000000000..ea5a069241 --- /dev/null +++ b/public/language/hy/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Ընդհանուր", + "admin": "Ադմին", + "group-privileges": "Group Privileges", + "user-privileges": "Օգտատերերի արտոնություններ ", + "edit-privileges": "Խմբագրել արտոնությունները", + "select-clear-all": "Ընտրել/Մաքրել բոլորը", + "chat": "Զրույց", + "upload-images": "Վերբեռնեք պատկերներ", + "upload-files": "Վերբեռնել Ֆայլեր", + "signature": "Ստորագրություն", + "ban": "Ալգելք", + "mute": "Անձայն ", + "invite": "Հրավիրել ", + "search-content": "Որոնել կոնտենտ ", + "search-users": "Որոնել օգտատերերին ", + "search-tags": "Որոնել պիտակներ", + "view-users": "Դիտել օգտատերերին ", + "view-tags": "Դիտել թագերը", + "view-groups": "Դիտել Խմբերը", + "allow-local-login": "Տեղական մուտք", + "allow-group-creation": "Խմբի ստեղծում", + "view-users-info": "Դիտեք օգտատերերի տվյալները", + "find-category": "Գտնել Կատեգորիա", + "access-category": "Մուտքի կատեգորիա", + "access-topics": "Մուտք գործել թեմաներ", + "create-topics": "Ստեղծել Թեմաներ", + "reply-to-topics": "Պատասխանել թեմաներին", + "schedule-topics": "Ժամանակացույցի թեմաներ", + "tag-topics": "Նշեք թեմաները", + "edit-posts": "Խմբագրել գրառումները", + "view-edit-history": "Դիտեք խմբագրման պատմությունը", + "delete-posts": "Ջնջել Գրառումները", + "view_deleted": "Դիտեք ջնջված գրառումները", + "upvote-posts": "Կողմ քվեարկել գրառումներին ", + "downvote-posts": "Դեմ քվեարկեք գրառումներին ", + "delete-topics": "Ջնջել թեմաները ", + "purge": "Մաքրում", + "moderate": "Չափավորել", + "admin-dashboard": "Ղեկավարման վահան", + "admin-categories": "Կատեգորիաներ", + "admin-privileges": "Արտոնություններ:", + "admin-users": "Օգտատերեր", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Խմբեր ", + "admin-tags": "Թագեր", + "admin-settings": "Կարգավորումներ", + + "alert.confirm-moderate": "Համոզվա՞ծ եք, որ ցանկանում եք վերահսկման արտոնություն տրամադրել այս օգտվողների խմբին: Այս խումբը հանրային է, և ցանկացած օգտատեր կարող է միանալ ըստ ցանկության:", + "alert.confirm-admins-mods": "Վստա՞հ եք, որ ցանկանում եք տրամադրել «Ադմինիստրատորներին & Mods» արտոնություն այս օգտատերին/խմբի՞ն: Այս արտոնություն ունեցող օգտատերերը կարող են խթանել և իջեցնել այլ օգտատերերի արտոնյալ դիրքերում, ներառյալ սուպեր ադմինիստրատորը", + "alert.confirm-save": "Խնդրում ենք հաստատել այս արտոնությունները պահպանելու ձեր մտադրությունը", + "alert.saved": "Արտոնությունների փոփոխությունները պահվեցին և կիրառվեցին", + "alert.confirm-discard": "Իսկապե՞ս ցանկանում եք հրաժարվել ձեր արտոնությունների փոփոխություններից:", + "alert.discarded": "Արտոնությունների փոփոխությունները չեղարկվեցին", + "alert.confirm-copyToAll": "Վստա՞հ եք, որ ցանկանում եք կիրառել այս %1 հավաքածուն բոլոր կատեգորիաների վրա:", + "alert.confirm-copyToAllGroup": "Վստա՞հ եք, որ ցանկանում եք կիրառել այս խմբի %1 հավաքածուն բոլոր կատեգորիաների վրա:", + "alert.confirm-copyToChildren": "Համոզվա՞ծ եք, որ ցանկանում եք կիրառել %1-ի այս հավաքածուն հետնորդների (դուստր) բոլոր կատեգորիաների վրա:", + "alert.confirm-copyToChildrenGroup": "Համոզվա՞ծ եք, որ ցանկանում եք կիրառել այս խմբի %1 հավաքածուն բոլոր հետնորդների (դուստր) կատեգորիաների վրա:", + "alert.no-undo": "Այս գործողությունը հնարավոր չէ հետարկել:", + "alert.admin-warning": "Ադմինիստրատորները անուղղակիորեն ստանում են բոլոր արտոնությունները", + "alert.copyPrivilegesFrom-title": "Ընտրեք կատեգորիա, որից պետք է պատճենել", + "alert.copyPrivilegesFrom-warning": "սա կպատճենի %1 ընտրված կատեգորիայից:", + "alert.copyPrivilegesFromGroup-warning": "Սա կպատճենի այս խմբի %1 հավաքածուն ընտրված կատեգորիայից:" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/registration.json b/public/language/hy/admin/manage/registration.json new file mode 100644 index 0000000000..99356e0901 --- /dev/null +++ b/public/language/hy/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Հերթ", + "description": "Գրանցման հերթում օգտատերեր չկան: Այս գործառույթը միացնելու համար անցեք Կարգավորումներ → Օգտատեր → Օգտագործողի գրանցում և գրանցման տեսակը սահմանեք «Ադմինիստրատորի հաստատում»:", + + "list.name": "Անուն ", + "list.email": "Էլ. փոստ", + "list.ip": "IP", + "list.time": "Ժամանակ", + "list.username-spam": "Հաճախականություն՝ %1 Հայտնվում է՝ %2 Վստահություն՝ %3", + "list.email-spam": "Հաճախականություն՝ %1 Հայտնվում է՝ %2", + "list.ip-spam": "Հաճախականություն՝ %1 Հայտնվում է՝ %2", + + "invitations": "Հրավերներ", + "invitations.description": "Ստորև ներկայացված է ուղարկված հրավերների ամբողջական ցանկը: Օգտագործեք ctrl-f՝ ցանկը էլեկտրոնային փոստով կամ օգտատիրոջ անունով որոնելու համար: Օգտատիրոջ անունը կցուցադրվի նամակների աջ կողմում այն օգտատերերի համար, ովքեր օգտագործել են իրենց հրավերները:", + "invitations.inviter-username": "Հրավիրողի օգտանունը", + "invitations.invitee-email": "Հրավիրվածի էլ. փոստը", + "invitations.invitee-username": "Հրավիրողի օգտանունը (եթե գրանցված է)", + + "invitations.confirm-delete": "Վստա՞հ եք, որ ցանկանում եք ջնջել այս հրավերը:" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/tags.json b/public/language/hy/admin/manage/tags.json new file mode 100644 index 0000000000..94f9c5f199 --- /dev/null +++ b/public/language/hy/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Ձեր ֆորումում դեռևս պիտակներով թեմաներ չկան:", + "bg-color": "Ֆոնի գույն ", + "text-color": "Տեքստի գույն ", + "description": "Ընտրեք պիտակներ՝ սեղմելով կամ քաշելով, օգտագործեք CTRL՝ մի քանի պիտակներ ընտրելու համար:", + "create": "Ստեղծել պիտակ ", + "modify": "Փոփոխել պիտակները", + "rename": "Վերանվանել Tags", + "delete": "Ջնջել ընտրված պիտակները", + "search": "Որոնել պիտակներ...", + "settings": "Tags Կարգավորումներ", + "name": "Պիտակի անուն ", + + "alerts.editing": "Թեգ(ներ)ի խմբագրում", + "alerts.confirm-delete": "Ցանկանու՞մ եք ջնջել ընտրված պիտակները:", + "alerts.update-success": "Պիտակը թարմացվեց:", + "reset-colors": "Վերականգնել գույները" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/uploads.json b/public/language/hy/admin/manage/uploads.json new file mode 100644 index 0000000000..e108e1704d --- /dev/null +++ b/public/language/hy/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Ներբեռնել ֆայլ", + "filename": "Ֆայլի անունը", + "usage": "Գրառման օգտագործումը", + "orphaned": "Մերժված", + "size/filecount": "Չափ / Ֆայլի հաշվարկ", + "confirm-delete": "Իսկապե՞ս ցանկանում եք ջնջել այս ֆայլը:", + "filecount": "%1 ֆայլեր", + "new-folder": "Նոր թղթապանակ", + "name-new-folder": "Մուտքագրեք նոր թղթապանակի անուն" +} \ No newline at end of file diff --git a/public/language/hy/admin/manage/users.json b/public/language/hy/admin/manage/users.json new file mode 100644 index 0000000000..6ad3a29d03 --- /dev/null +++ b/public/language/hy/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Օգտատերեր", + "edit": "Գործողություններ", + "make-admin": "Դարձնել Ադմին", + "remove-admin": "Հեռացնել ադմինիստրատորին", + "validate-email": "Վավերացնել էլ. փոստը", + "send-validation-email": "Ուղարկել վավերացման էլ. փոստ", + "password-reset-email": "Ուղարկել գաղտնաբառը վերականգնելու էլ. փոստ", + "force-password-reset": "Ստիպել գաղտնաբառի վերակայում և օգտատերից դուրս գալ", + "ban": "Արգելել օգտատիրոջը(ին)", + "temp-ban": "Ժամանակավորապես արգելել օգտատեր(ներին):", + "unban": "Արգելահանել օգտատեր(ներ)ին", + "reset-lockout": "Վերականգնել Lockout", + "reset-flags": "Վերականգնել դրոշները", + "delete": "Ջնջել օգտատիրոջը", + "delete-content": "Ջնջել օգտատեր(ների) կոնտենտը", + "purge": "Ջնջել օգտատերին(ներ) և բովանդակությունը", + "download-csv": "Ներբեռնեք CSV", + "manage-groups": "Կառավարել Խմբերը", + "add-group": "Ավելացնել խումբ ", + "create": "Ստեղծել օգտատեր", + "invite": "հրավիրել էլ. փոստով", + "new": "Նոր օգտատեր ", + "filter-by": "Զտել ըստ", + "pills.unvalidated": "Վավերացված չէ", + "pills.validated": "Վավերացված է", + "pills.banned": "Արգելված", + + "50-per-page": "50 մեկ էջի համար", + "100-per-page": "100 մեկ էջի համար", + "250-per-page": "250 մեկ էջի համար", + "500-per-page": "500 per page", + + "search.uid": "Օգտատիրոջ ID-ով", + "search.uid-placeholder": "Որոնման համար մուտքագրեք օգտվողի ID", + "search.username": "Ըստ օգտատիրոջ անվան ", + "search.username-placeholder": "Մուտքագրեք օգտատիրոջ անուն որոնման համար", + "search.email": "Էլ. փոստով", + "search.email-placeholder": "Որոնման համար մուտքագրեք էլ. փոստ", + "search.ip": "IP հասցեով", + "search.ip-placeholder": "Մուտքագրեք IP հասցե որոնման համար", + "search.not-found": "Օգտատերը չի գտնվել ", + + "inactive.3-months": "3 ամիս", + "inactive.6-months": "6 ամիս", + "inactive.12-months": "12 ամիս", + + "users.uid": "uid", + "users.username": "օգտատիրոջ անուն ", + "users.email": "էլ. փոստ", + "users.no-email": "էլ. փոստ չկա", + "users.ip": "IP", + "users.postcount": "հետհաշվարկ", + "users.reputation": "վարկանիշ", + "users.flags": "դրոշներ", + "users.joined": "միացել է", + "users.last-online": "վերջին առցանց", + "users.banned": "արգելված", + + "create.username": "Օգտատիրոջ անուն ", + "create.email": "Էլ. փոստ", + "create.email-placeholder": "Տվյալ օգտատիրոջ էլ. փոստը", + "create.password": "Գաղտնաբառ", + "create.password-confirm": "Հաստատել գաղտնաբառը", + + "temp-ban.length": "Երկարություն", + "temp-ban.reason": "Պատճառը (ըստ ցանկության)", + "temp-ban.hours": "Ժամեր", + "temp-ban.days": "Օրեր", + "temp-ban.explanation": "Մուտքագրեք արգելքի ժամկետը: Նկատի ունեցեք, որ 0-ի ժամանակը համարվում է մշտական արգելք:", + + "alerts.confirm-ban": "Իսկապե՞ս ցանկանում եք ընդմիշտ արգելել այս օգտատիրոջը:", + "alerts.confirm-ban-multi": "Իսկապե՞ս ցանկանում եք ընդմիշտ արգելել այս օգտատերերին:", + "alerts.ban-success": "Օգտատերերն արգելված են:", + "alerts.button-ban-x": "Արգելել %1 օգտվող(ներ)", + "alerts.unban-success": "Օգտատեր(ներ)ն արգելված չէ:", + "alerts.lockout-reset-success": "Արգելափակում(ներ)ը վերակայվել է:", + "alerts.flag-reset-success": "Դրոշ(ներ)ը վերակայվել են:", + "alerts.no-remove-yourself-admin": "Դուք չեք կարող ձեզ հեռացնել որպես Ադմինիստրատոր:", + "alerts.make-admin-success": "Օգտատերը այժմ ադմինիստրատոր է:", + "alerts.confirm-remove-admin": "Իսկապե՞ս ուզում եք հեռացնել այս ադմինիստրատորին:", + "alerts.remove-admin-success": "Օգտատերը այլևս ադմինիստրատոր չէ:", + "alerts.make-global-mod-success": "Օգտատերը այժմ համաշխարհային մոդերատոր է:", + "alerts.confirm-remove-global-mod": "Իսկապե՞ս ցանկանում եք հեռացնել այս ընդհանուր մոդերատորին:", + "alerts.remove-global-mod-success": "Օգտատերը այլևս ընդհանուր մոդերատոր չէ:", + "alerts.make-moderator-success": "Օգտատերն այժմ մոդերատոր է:", + "alerts.confirm-remove-moderator": "Իսկապե՞ս ուզում եք հեռացնել այս մոդերատորին:", + "alerts.remove-moderator-success": "Օգտատերը այլևս մոդերատոր չէ:", + "alerts.confirm-validate-email": "Ցանկանու՞մ եք վավերացնել այս օգտատեր(ների) էլ.փոստ(ները):", + "alerts.confirm-force-password-reset": "Վստա՞հ եք, որ ցանկանում եք ստիպել վերականգնել գաղտնաբառը և դուրս գալ այս օգտատեր(ներ)ից:", + "alerts.validate-email-success": "Էլ.նամակները վավերացված են", + "alerts.validate-force-password-reset-success": "Օգտատեր(ների) գաղտնաբառերը վերակայվել են, և նրանց առկա աշխատաշրջանները չեղարկվել են:", + "alerts.password-reset-confirm": "Ցանկանու՞մ եք գաղտնաբառի վերակայման նամակներ ուղարկել այս օգտատերին (ներին);", + "alerts.password-reset-email-sent": "Գաղտնաբառի վերակայման նամակն ուղարկվել է:", + "alerts.confirm-delete": "Զգուշացում: Իսկապե՞ս ցանկանում եք ջնջել օգտատիրոջը(ներ): Այս գործողությունը հետադարձելի չէ: Միայն օգտվողի հաշիվը կջնջվի, նրանց գրառումներն ու թեմաները կմնան։", + "alerts.delete-success": "Օգտատեր(ները) ջնջված է:", + "alerts.confirm-delete-content": "Զգուշացում: Իսկապե՞ս ցանկանում եք ջնջել այս օգտատեր(ներ)ի կոնտենտը: Այս գործողությունը հետադարձելի չէ: Օգտատերերի հաշիվները կմնան, բայց նրանց գրառումներն ու թեմաները կջնջվեն։", + "alerts.delete-content-success": "Օգտատեր(ների) կոնտենտը ջնջվել է ", + "alerts.confirm-purge": "Զգուշացում: Իսկապե՞ս ցանկանում եք ջնջել օգտատեր(ներին) և նրանց բովանդակությունը: Այս գործողությունը հետադարձելի չէ: Օգտատիրոջ բոլոր տվյալները և բովանդակությունը կջնջվեն:", + "alerts.create": "Ստեղծել Օգտատեր ", + "alerts.button-create": "Ստեղծել ", + "alerts.button-cancel": "չեղարկել", + "alerts.error-passwords-different": "Գաղտնաբառերը պետք է համընկնեն!", + "alerts.error-x": "Սխալ% 1", + "alerts.create-success": "Օգտատերը ստեղծված է", + + "alerts.prompt-email": "Էլ. փոստեր", + "alerts.email-sent-to": "Հրավերի նամակ է ուղարկվել %1-ին", + "alerts.x-users-found": "Գտնվել է %1 օգտատեր, (%2 վայրկյան)", + "export-users-started": "Օգտագործողների արտահանում որպես csv, դա կարող է որոշ ժամանակ տևել: Դուք ծանուցում կստանաք, երբ այն ավարտվի:", + "export-users-completed": "Օգտատերերը արտահանվել են որպես csv, ներբեռնելու համար սեղմեք այստեղ:" +} \ No newline at end of file diff --git a/public/language/hy/admin/menu.json b/public/language/hy/admin/menu.json new file mode 100644 index 0000000000..f1d71bea7b --- /dev/null +++ b/public/language/hy/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Ղեկավարման վահաններ", + "dashboard/overview": "Ընդհանուր ակնարկ", + "dashboard/logins": "Մուտքագրումներ", + "dashboard/users": "Օգտատերեր", + "dashboard/topics": "Թեմաներ", + "dashboard/searches": "Որոնումներ", + "section-general": "Ընդհանուր", + + "section-manage": "Կարգավորել", + "manage/categories": "Կատեգորիաներ", + "manage/privileges": "Արտոնություններ", + "manage/tags": "Պիտակներ", + "manage/users": "Օգտատերեր", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Գրանցման հերթ", + "manage/post-queue": "Գրառման Queue", + "manage/groups": "Խմբեր", + "manage/ip-blacklist": "IP սև ցուցակ", + "manage/uploads": "Վերբեռնումներ", + "manage/digest": "Ամփոփագրեր ", + + "section-settings": "Կարգավորումներ", + "settings/general": "Ընդհանուր", + "settings/homepage": "Գլխավոր էջ", + "settings/navigation": "Նավիգացիա", + "settings/reputation": "Վարկանիշ և դրոշներ", + "settings/email": "Էլ. փոստ", + "settings/user": "Օգտատերեր", + "settings/group": "Խմբեր", + "settings/guest": "Հյուրեր", + "settings/uploads": "Վերբեռնումներ", + "settings/languages": "Լեզուներ ", + "settings/post": "Գրառումներ", + "settings/chat": "Զրույցներ", + "settings/pagination": "Էջավորում", + "settings/tags": "Պիտակներ", + "settings/notifications": "Ծանուցումներ", + "settings/api": "API մուտք", + "settings/sounds": "Ձայներ", + "settings/social": "Սոցիալական ", + "settings/cookies": "Cookies", + "settings/web-crawler": "Վեբ սողուն", + "settings/sockets": "Վարդակներ", + "settings/advanced": "Ընդլայնված", + + "settings.page-title": "%1 Կարգավորումներ", + + "section-appearance": "Արտաքին տեսք", + "appearance/themes": "Թեմաներ", + "appearance/skins": "Շապիկներ", + "appearance/customise": "Հատուկ բովանդակություն (HTML/JS/CSS)", + + "section-extend": "Ընդլայնել", + "extend/plugins": "Փլագիններ", + "extend/widgets": "Վիդջեթներ", + "extend/rewards": "Պարգևներ", + + "section-social-auth": "Սոցիալական վավերացում", + + "section-plugins": "Փլագիններ", + "extend/plugins.install": "Տեղադրեք Plugins", + + "section-advanced": "Ընդլայնված", + "advanced/database": "Տվյալների բազա", + "advanced/events": "Իրադարձություններ", + "advanced/hooks": "Hooks", + "advanced/logs": "Մատյաններ", + "advanced/errors": "Սխալներ ", + "advanced/cache": "Քեշ", + "development/logger": "Logger", + "development/info": "Տեղեկատվություն", + + "rebuild-and-restart-forum": "Վերակառուցել և վերագործարկել ֆորումը", + "restart-forum": "Վերագործարկել Ֆորումը", + "logout": "Դուրս գալ", + "view-forum": "Դիտել ֆորումը", + + "search.placeholder": "Որոնման կարգավորումներ", + "search.no-results": "Արդյունքներ չեն գտնվել", + "search.search-forum": "Որոնել ֆորումում", + "search.keep-typing": "Արդյունքները տեսնելու համար գրեք ավելին...", + "search.start-typing": "Արդյունքները տեսնելու համար սկսեք մուտքագրել...", + + "connection-lost": "%1-ի միացումը կորել է, փորձում է նորից միանալ...", + + "alerts.version": "Գործող NodeBB v%1", + "alerts.upgrade": "Թարմացրեք մինչև v%1" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/advanced.json b/public/language/hy/admin/settings/advanced.json new file mode 100644 index 0000000000..e18468591b --- /dev/null +++ b/public/language/hy/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Սպասարկման ռեժիմ", + "maintenance-mode.help": "Երբ ֆորումը սպասարկման ռեժիմում է, բոլոր հարցումները կվերահղվեն դեպի ստատիկ պահման էջ: Ադմինիստրատորները ազատված են այս վերահղումից և կարող են սովորական կերպով մուտք գործել կայք:", + "maintenance-mode.status": "Սպասարկման ռեժիմի կարգավիճակի կոդը", + "maintenance-mode.message": "Սպասարկման հաղորդագրություն", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Վերնագրեր", + "headers.allow-from": "Սահմանեք ALLOW-FROM-ը, որպեսզի NodeBB-ն տեղադրվի iFrame-ում", + "headers.csp-frame-ancestors": "Սահմանեք Content-Security-Policy frame-ancestors վերնագիրը որպես Place NodeBB iFrame-ում", + "headers.csp-frame-ancestors-help": "«ոչ մեկը», «ինքնուրույն» (հիմնական) կամ թույլատրելի URI-ների ցանկ:", + "headers.powered-by": "Անհատականացրեք NodeBB-ի կողմից ուղարկված «Powered By» վերնագիրը", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin կանոնավոր արտահայտություն", + "headers.acao-help": "Բոլոր կայքերի մուտքը մերժելու համար թողեք դատարկ", + "headers.acao-regex-help": "Մուտքագրեք կանոնավոր արտահայտություններ այստեղ՝ դինամիկ սկզբնաղբյուրներին համապատասխանելու համար: Բոլոր կայքերի մուտքը մերժելու համար թողեք դատարկ", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "Երբ միացված է (հիմնական), վերնագիրը կսահմանի require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Միացված է HSTS (խորհուրդ է տրվում)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Ներառեք ենթադոմեյնները HSTS վերնագրում", + "hsts.preload": "Թույլատրել HSTS վերնագրի նախաբեռնումը", + "hsts.help": "Եթե միացված է, այս կայքի համար HSTS վերնագիր կսահմանվի: Դուք կարող եք ընտրել ենթատիրույթներ և նախապես բեռնվող դրոշներ ներառել ձեր վերնագրում: Եթե կասկածում եք, կարող եք դրանք թողնել չստուգված: Լրացուցիչ տեղեկություններ", + "traffic-management": "Թրաֆիկի կառավարում ", + "traffic.help": "NodeBB-ն օգտագործում է մոդուլ, որն ինքնաբերաբար մերժում է հարցումները բարձր երթևեկության իրավիճակներում: Դուք կարող եք կարգավորել այս կարգավորումները այստեղ, թեև կանխադրվածները լավ մեկնարկային կետ են:", + "traffic.enable": "Միացնել թրաֆիկի կառավարումը", + "traffic.event-lag": "Իրադարձությունների հանգույցի հետաձգման շեմը (միլիվայրկյաններով)", + "traffic.event-lag-help": "Այս արժեքի իջեցումը նվազեցնում է էջի բեռնման սպասման ժամանակը, բայց նաև ցույց կտա «չափազանց ծանրաբեռնվածություն» հաղորդագրությունը ավելի շատ օգտատերերի համար: (Պահանջվում է վերագործարկել)", + "traffic.lag-check-interval": "Ստուգեք միջակայքը (մլիվայրկյաններով)", + "traffic.lag-check-interval-help": "Այս արժեքի իջեցումը հանգեցնում է նրան, որ NodeBB-ն ավելի զգայուն է դառնում բեռի բարձրությունների նկատմամբ, բայց կարող է նաև հանգեցնել չեկի չափազանց զգայուն դառնալուն: (Պահանջվում է վերագործարկել)", + + "sockets.settings": "WebSocket-ի կարգավորումներ", + "sockets.max-attempts": "Վերամիացման առավելագույն փորձեր", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Վերլուծության կարգավորումներ", + "analytics.max-cache": "Վերլուծության քեշի առավելագույն արժեքը", + "analytics.max-cache-help": "Մեծ թրաֆիկի տեղադրումների դեպքում քեշը կարող է շարունակաբար սպառվել, եթե միաժամանակ ակտիվ օգտագործողներն ավելի շատ լինեն, քան Max Cache արժեքը: (Պահանջվում է վերագործարկել)", + "compression.settings": "Սեղմման կարգավորումներ", + "compression.enable": "Միացնել սեղմումը", + "compression.help": "արտադրություն, սեղմումը տեղադրելու լավագույն միջոցը այն իրականացնելն է հակառակ պրոքսի մակարդակով: Դուք կարող եք այն միացնել այստեղ՝ թեստավորման նպատակով:" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/api.json b/public/language/hy/admin/settings/api.json new file mode 100644 index 0000000000..d857197ecb --- /dev/null +++ b/public/language/hy/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Նշաններ/ձևաթղթեր", + "settings": "Կարգավորումներ ", + "lead-text": "Այս էջից դուք կարող եք կարգավորել մուտքը դեպի «Write API» NodeBB-ում:", + "intro": " Write API-ն նույնականացնում է օգտատերերին՝ հիմնվելով նրանց նստաշրջանի cookie-ի վրա, սակայն NodeBB-ն աջակցում է նաև կրող նույնականացումը այս էջի միջոցով ստեղծվող նշանների միջոցով:", + "docs": "Սեղմեք այստեղ՝ API-ի ամբողջական ճշգրտմանը մուտք գործելու համար", + + "require-https": "Պահանջել API-ի օգտագործում միայն HTTPS-ի միջոցով", + "require-https-caveat": "Նշում․ որոշ տեղադրումներ, որոնք ներառում են բեռի հավասարակշռողներ, կարող են իրենց հարցումները փոխանցել NodeBB-ին՝ օգտագործելով HTTP, որի դեպքում այս տարբերակը պետք է մնա անջատված։", + + "uid": "Օգտատիրոջ ID", + "uid-help-text": "Նշեք Օգտատիրոջ ID՝ այս նշանի հետ կապելու համար: Եթե օգտատիրոջ ID-ն 0 է, ապա այն կհամարվի հիմնական նշան, որը կարող է ենթադրել այլ օգտատերերի ինքնությունը՝ հիմնվելով _uid պարամետրի վրա:", + "description": "Նկարագրություն", + "no-description": "Ոչ մի նկարագրություն նշված չէ:", + "token-on-save": "Ձևաթուղթը կստեղծվի, երբ ձևը պահպանվի" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/chat.json b/public/language/hy/admin/settings/chat.json new file mode 100644 index 0000000000..517f702a81 --- /dev/null +++ b/public/language/hy/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Զրույցի կարգավորումներ", + "disable": "Անջատել զրույցը", + "disable-editing": "Անջատել զրույցի հաղորդագրությունների խմբագրումը/ջնջումը", + "disable-editing-help": "Ադմինիստրատորները և ամընդհանուր մոդերատորները ազատված են այս սահմանափակումից", + "max-length": "Զրույցի հաղորդագրությունների առավելագույն երկարությունը", + "max-room-size": "Զրուցարաններում օգտատերերի առավելագույն քանակը", + "delay": "Զրույցի հաղորդագրությունների միջև ընկած ժամանակը միլիվայրկյաններով", + "notification-delay": "Զրույցի հաղորդագրությունների ծանուցման հետաձգում: (0 առանց ուշացման)", + "restrictions.seconds-edit-after": "Վայրկյանների քանակը զրույցի հաղորդագրությունը կմնա խմբագրելի: (0 անջատված)", + "restrictions.seconds-delete-after": "Վայրկյանների քանակը զրույցի հաղորդագրությունը կմնա ջնջելի: (0 անջատված)" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/cookies.json b/public/language/hy/admin/settings/cookies.json new file mode 100644 index 0000000000..59afb258da --- /dev/null +++ b/public/language/hy/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "ԵՄ համաձայնությունը", + "consent.enabled": "Միացված է", + "consent.message": "Ծանուցման հաղորդագրություն", + "consent.acceptance": "Ընդունման հաղորդագրություն", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Քաղաքականության հղման URL", + "consent.blank-localised-default": "Թողեք դատարկ՝ NodeBB տեղայնացված հիմնական օգտագործման համար", + "settings": "Կարգավորումներ", + "cookie-domain": "Աշխատաշրջանի cookie տիրույթ", + "max-user-sessions": "Առավելագույն ակտիվ նստաշրջանները մեկ օգտատերի համար", + "blank-default": " Թողնել դատարկ հիմնականի համար " +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/email.json b/public/language/hy/admin/settings/email.json new file mode 100644 index 0000000000..a1eb605c6f --- /dev/null +++ b/public/language/hy/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Էլփոստի կարգավորումներ ", + "address": "Էլեկտրոնային հասցե", + "address-help": "Հետևյալ էլփոստի հասցեն վերաբերում է այն էլ.փոստին, որը ստացողը կտեսնի «Ումից» և «Պատասխանել» դաշտերում:", + "from": "Ում Անունից", + "from-help": "Նամակում ցուցադրվող «ում անունից»:", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Դուք կարող եք ընտրել հայտնի ծառայությունների ցանկից կամ մուտքագրել հատուկ մեկը:", + "smtp-transport.service": "Ընտրել ծառայություն", + "smtp-transport.service-custom": "Հատուկ ծառայություն", + "smtp-transport.service-help": "Ընտրեք վերը նշված ծառայության անունը՝ դրա մասին հայտնի տեղեկատվությունը օգտագործելու համար: Որպես այլընտրանք, ընտրեք «Մաքսային ծառայություն» և մուտքագրեք մանրամասները ստորև:", + "smtp-transport.gmail-warning1": "Եթե դուք օգտագործում եք GMail-ը որպես ձեր էլփոստի մատակարար, դուք պետք է ստեղծեք «Հավելվածի գաղտնաբառ» որպեսզի NodeBB-ն հաջողությամբ վավերացվի: Դուք կարող եք ստեղծել մեկը App Passwords էջում:", + "smtp-transport.gmail-warning2": "Այս խնդրի վերաբերյալ լրացուցիչ տեղեկությունների համար խնդրում ենք ծանոթանալ NodeMailer-ի այս հոդվածին: Այլընտրանք կարող է լինել երրորդ կողմի էլփոստի հավելվածի օգտագործումը, ինչպիսիք են SendGrid-ը, Mailgun-ը և այլն: Փնտրեք հասանելի հավելվածները այստեղ:", + "smtp-transport.auto-enable-toast": "Կարծես թե դուք կարգավորում եք SMTP տրանսպորտը: Մենք ձեզ համար միացրել ենք «SMTP Transport» տարբերակը:", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Միացման անվտանգություն", + "smtp-transport.security-encrypted": "Կոդավորված", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Ոչ մեկը ", + "smtp-transport.username": "Օգտանուն", + "smtp-transport.username-help": "Gmail ծառայության համար այստեղ մուտքագրեք էլփոստի ամբողջական հասցեն, հատկապես եթե օգտագործում եք Google Apps-ի կառավարվող տիրույթ:", + "smtp-transport.password": "Գաղտնաբառ", + "smtp-transport.pool": "Միացնել միավորված կապերը", + "smtp-transport.pool-help": "Կապերի միավորումը թույլ չի տալիս NodeBB-ին նոր կապ ստեղծել յուրաքանչյուր էլփոստի համար: Այս տարբերակը կիրառվում է միայն այն դեպքում, եթե SMTP Transport-ը միացված է:", + + "template": "Խմբագրել էլփոստի ձևանմուշը", + "template.select": "Ընտրեք էլփոստի ձևանմուշ", + "template.revert": "Վերադառնալ բնօրինակին", + "testing": "Էլփոստի փորձարկում", + "testing.select": "Ընտրեք էլփոստի ձևանմուշ", + "testing.send": "Ուղարկեք փորձնական էլ.նամակ", + "testing.send-help": "Փորձնական նամակը կուղարկվի տվյալ պահին մուտք գործած օգտատերի էլ.փոստի հասցեին:", + "subscriptions": "Էլփոստի ամփոփագրեր", + "subscriptions.disable": "Անջատել էլփոստի ամփոփումները", + "subscriptions.hour": "Digest Ժամ", + "subscriptions.hour-help": "Խնդրում ենք մուտքագրել համարը, որը ներկայացնում է պլանավորված էլփոստի ամփոփագրեր ուղարկելու ժամը (օրինակ՝ 0 կեսգիշերին, 17-ը 17:00-ից): Հիշեք, որ սա ժամն է ըստ սերվերի, և կարող է ճիշտ չհամընկնել ձեր համակարգի ժամացույցի հետ: Սերվերի մոտավոր ժամանակը հետևյալն է. նախատեսվում է ուղարկել հաջորդ օրական ամփոփագիրը", + "notifications.remove-images": "Հեռացրեք պատկերները էլփոստի ծանուցումներից", + "require-email-address": "Պահանջել նոր օգտատերերից նշել էլփոստի հասցե", + "require-email-address-warning": "Հիմնականում, օգտվողները կարող են հրաժարվել էլփոստի հասցե մուտքագրելուց՝ դաշտը դատարկ թողնելով: Այս ընտրանքը միացնելը նշանակում է, որ նրանք պետք է մուտքագրեն էլփոստի հասցե՝ գրանցումը շարունակելու համար: Այն չի ապահովում, որ օգտվողը մուտքագրի իրական էլփոստի հասցե, ոչ էլ նույնիսկ իր սեփական հասցե:", + "send-validation-email": "Համակարգի ընդհանուր հիշողություն", + "include-unverified-emails": "Նամակներ ուղարկեք հասցեատերերին, ովքեր հստակորեն չեն հաստատել իրենց էլ.փոստը", + "include-unverified-warning": "Հիմնականում, իրենց հաշվի հետ կապված էլփոստով օգտատերերն արդեն ստուգված են, սակայն կան իրավիճակներ, երբ դա այդպես չէ (օրինակ՝ SSO մուտքեր, մեծահայր օգտատերեր և այլն): Միացնել այս կարգավորումը ձեր սեփական ռիսկով – Չստուգված հասցեներով էլ-նամակներ ուղարկելը կարող է հակասպամի մասին տարածաշրջանային օրենքների խախտում լինել:", + "prompt": "Հորդորեք օգտատերերին մուտքագրել կամ հաստատել իրենց էլ.փոստերը", + "prompt-help": "Եթե օգտատերը չունի էլ.փոստ կամ էլ.փոստը չի հաստատվել, ապա էկրանին կցուցադրվի նախազգուշացում:", + "sendEmailToBanned": "Նամակներ ուղարկեք օգտատերերին, նույնիսկ եթե դրանք արգելված են" +} diff --git a/public/language/hy/admin/settings/general.json b/public/language/hy/admin/settings/general.json new file mode 100644 index 0000000000..b9aa42fb91 --- /dev/null +++ b/public/language/hy/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Կայքի կարգավորումներ", + "title": "Կայքի անվանումը", + "title.short": "Կարճ վերնագիր", + "title.short-placeholder": "Եթե կարճ վերնագիր նշված չէ, կայքի անվանումը կօգտագործվի", + "title.url": "Վերնագրի հղումի URL", + "title.url-placeholder": "Կայքի վերնագրի URL-ը", + "title.url-help": "Երբ վերնագիրը սեղմված է, ուղարկեք օգտատերերին այս հասցեով: Եթե դատարկ մնա, օգտատերիրոջը կուղարկվի ֆորումի ինդեքս: Նշում. սա էլ-նամակներում օգտագործվող արտաքին URL-ը չէ և այլն: Այն սահմանված է config.json-ի url հատկությամբ:", + "title.name": "Ձեր համայնքի անունը", + "title.show-in-header": "Ցույց տալ կայքի անվանումը վերնագրում", + "browser-title": "Բրաուզերի անվանումը", + "browser-title-help": "Եթե դիտարկիչի անվանումը նշված չէ, կայքի անվանումը կօգտագործվի", + "title-layout": "Վերնագրի դասավորություն", + "title-layout-help": "Սահմանեք, թե ինչպես է կառուցված բրաուզերի վերնագիրը, այսինքն. {էջի վերնագիր} | {browserTitle}", + "description.placeholder": "Ձեր համայնքի մասին կարճ նկարագրություն", + "description": "Կայքի նկարագրություն", + "keywords": "Կայքի հիմնաբառեր", + "keywords-placeholder": "Ձեր համայնքը նկարագրող հիմնաբառեր՝ բաժանված ստորակետերով", + "logo": "Կայքի լոգոն", + "logo.image": "Նկար ", + "logo.image-placeholder": "Ճանապարհ դեպի լոգո՝ ֆորումի վերնագրում ցուցադրելու համար", + "logo.upload": "Վերբեռնել", + "logo.url": "Լոգոյի հղման URL", + "logo.url-placeholder": "Կայքի լոգոյի URL-ը", + "logo.url-help": "Երբ լոգոն սեղմվում է, ուղարկեք օգտատերերին այս հասցեով: Եթե դատարկ մնա, օգտատերը կուղարկվի ֆորումի ինդեքս: Նշում. սա էլ-նամակներում օգտագործվող արտաքին URL-ը չէ և այլն: Այն սահմանված է config.json-ի url հատկությամբ:", + "logo.alt-text": "Alt Թեքստ", + "log.alt-text-placeholder": "Այլընտրանքային տեքստ մատչելիության համար", + "favicon": "Ֆավիկոն ", + "favicon.upload": "Վերբեռնել", + "pwa": "առաջադեմ վեբ հավելված", + "touch-icon": "Հպման պատկերակ", + "touch-icon.upload": "Վերբեռնել", + "touch-icon.help": "Առաջարկվող չափը և ձևաչափը՝ 512x512, միայն PNG ձևաչափ: Եթե որևէ հպման պատկերակ նշված չէ, NodeBB-ը կվերադառնա ֆավիկոնի օգտագործմանը:", + "maskable-icon": "Դիմակելի (հիմնական էկրան) պատկերակ", + "maskable-icon.help": "Առաջարկվող չափը և ձևաչափը՝ 512x512, միայն PNG ձևաչափ: Եթե ոչ մի դիմակավոր պատկերակ նշված չէ, NodeBB-ը կվերադառնա Touch Icon-ին:", + "outgoing-links": "Ելքային հղումներ", + "outgoing-links.warning-page": "Օգտագործեք ելքային հղումների նախազգուշացման էջը", + "search": "Որոնում", + "search-default-in": "Որոնել", + "search-default-in-quick": "Արագ որոնում", + "search-default-sort-by": "Դասավորել ըստ", + "outgoing-links.whitelist": "Դոմենները սպիտակ ցուցակում՝ նախազգուշացման էջը շրջանցելու համար", + "site-colors": "Կայքի գույնի մետատվյալներ", + "theme-color": "Թեմայի գույնը", + "background-color": "Ֆոնի գույնը", + "background-color-help": "Գույնը, որն օգտագործվում է շաղ տալ էկրանի ֆոնի համար, երբ կայքը տեղադրվում է որպես PWA", + "undo-timeout": "Հետարկել ժամանակի դադարը", + "undo-timeout-help": "Որոշ գործողություններ, ինչպիսիք են թեմաների տեղափոխումը, մոդերատորին թույլ կտան հետարկել իրենց գործողությունները որոշակի ժամկետում: Սահմանեք 0՝ ամբողջությամբ հետարկելը անջատելու համար:", + "topic-tools": "Թեմայի գործիքներ" +} diff --git a/public/language/hy/admin/settings/group.json b/public/language/hy/admin/settings/group.json new file mode 100644 index 0000000000..1f99c0475f --- /dev/null +++ b/public/language/hy/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Գլխավոր ", + "private-groups": "Մասնավոր Խմբեր", + "private-groups.help": "Եթե միացված է, խմբերին միանալու համար անհրաժեշտ է խմբի սեփականատիրոջ հաստատումը (հիմնական՝ միացված է)", + "private-groups.warning": "Զգուշացեք. Եթե այս տարբերակն անջատված է, և դուք ունեք մասնավոր խմբեր, դրանք ավտոմատ կերպով դառնում են հանրային:", + "allow-multiple-badges": "Թույլատրել մի քանի նշաններ", + "allow-multiple-badges-help": "Այս դրոշը կարող է օգտագործվել՝ օգտատերերին թույլատրելու համար ընտրել մի քանի խմբի կրծքանշաններ, պահանջում է թեմաների աջակցություն:", + "max-name-length": "Խմբի անվան առավելագույն երկարությունը", + "max-title-length": "Խմբի վերնագրի առավելագույն երկարությունը", + "cover-image": "Խմբի շապիկի նկար ", + "default-cover": "Հիմնական շապիկի նկարներ", + "default-cover-help": "Ավելացրեք ստորակետերով բաժանված հիմնական շապիկի պատկերներ խմբերի համար, որոնք չունեն վերբեռնված շապիկի պատկեր" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/guest.json b/public/language/hy/admin/settings/guest.json new file mode 100644 index 0000000000..53f958a6e0 --- /dev/null +++ b/public/language/hy/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Կարգավորումներ", + "handles.enabled": "Թույլատրել guest handles", + "handles.enabled-help": "Այս ընտրանքը բացահայտում է նոր դաշտ, որը թույլ է տալիս հյուրերին ընտրել անուն՝ իրենց կատարած յուրաքանչյուր գրառման հետ կապելու համար: Եթե անջատված են, նրանք պարզապես կանվանվեն «Հյուր»", + "topic-views.enabled": "Թույլ տվեք հյուրերին ավելացնել թեմայի դիտումների քանակը", + "reply-notifications.enabled": "Թույլ տվեք հյուրերին ստեղծել պատասխանների ծանուցումներ" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/homepage.json b/public/language/hy/admin/settings/homepage.json new file mode 100644 index 0000000000..f0753ab387 --- /dev/null +++ b/public/language/hy/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Գլխավոր էջ", + "description": "Ընտրեք, թե որ էջը կցուցադրվի, երբ օգտվողները անցնեն դեպի ձեր ֆորումի արմատային URL-ը:", + "home-page-route": "Գլխավոր Էջի ուղեգիծ", + "custom-route": "Հատուկ ուղեգիծ", + "allow-user-home-pages": "Թույլատրել օգտատիրոջ գլխավոր էջերը", + "home-page-title": "Գլխավոր էջի անվանումը (կանխադրված «Գլխավոր»)" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/languages.json b/public/language/hy/admin/settings/languages.json new file mode 100644 index 0000000000..99040d02f9 --- /dev/null +++ b/public/language/hy/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Լեզվի կարգավորումներ", + "description": "Հիմնական լեզուն որոշում է լեզվի կարգավորումները բոլոր օգտվողների համար, ովքեր այցելում են ձեր ֆորում: Առանձին օգտատերերը կարող են փոխել հիմնական լեզուն իրենց հաշվի կարգավորումների էջում:", + "default-language": "Հիմնական լեզու ", + "auto-detect": "Լեզվի Ավտոմատ Հայտնաբերման կարգավորում հյուրերի համար" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/navigation.json b/public/language/hy/admin/settings/navigation.json new file mode 100644 index 0000000000..2882dd3646 --- /dev/null +++ b/public/language/hy/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Պատկեր", + "change-icon": "փոփոխություն ", + "route": "Ուղեգիծ", + "tooltip": "Գործիքների հուշում.", + "text": "Տեքստ", + "text-class": "Տեքստի դաս՝ ընտրովի ", + "class": "Դասը՝ ընտրովի", + "id": "ID: ընտրովի ", + + "properties": "Հատկություններ", + "groups": "Խմբեր", + "open-new-window": "Բացել նոր պատուհանում", + "dropdown": "Բացվող", + "dropdown-placeholder": "Տեղադրեք ձեր բացվող ընտրացանկի տարրերը ստորև, այսինքն՝ <li><a href="https://myforum.com">Հղում 1</a></li>", + + "btn.delete": "Ջնջել", + "btn.disable": "Անջատել", + "btn.enable": "Միացնել", + + "available-menu-items": "Մենյուի առկա տարրեր", + "custom-route": "Հատուկ ուղեգիծ", + "core": "միջուկը", + "plugin": "պլագին" +} diff --git a/public/language/hy/admin/settings/notifications.json b/public/language/hy/admin/settings/notifications.json new file mode 100644 index 0000000000..f88d8570ba --- /dev/null +++ b/public/language/hy/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Ծանուցումներ", + "welcome-notification": "Ողջույնի ծանուցում", + "welcome-notification-link": "Ողջույնի ծանուցման հղում", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Գրառումների հերթի օգտատեր (UID)" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/pagination.json b/public/language/hy/admin/settings/pagination.json new file mode 100644 index 0000000000..a514af138a --- /dev/null +++ b/public/language/hy/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Էջավորման կարգավորումներ", + "enable": "Էջադրեք թեմաներն ու գրառումները՝ անսահման որոնում կատարելու փոխարեն:", + "posts": "Գրառման էջադրում", + "topics": "Թեմայի էջադրում", + "posts-per-page": "Գրառումներ մեկ էջի համար", + "max-posts-per-page": "Մեկ էջում առավելագույն գրառումներ ", + "categories": "Կատեգորիայի էջադրում", + "topics-per-page": "Թեմաներ մեկ էջի համար", + "max-topics-per-page": "Մեկ էջում առավելագույն թեմաներ", + "categories-per-page": "Կատեգորիաներ մեկ էջի համար" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/post.json b/public/language/hy/admin/settings/post.json new file mode 100644 index 0000000000..d7456322f2 --- /dev/null +++ b/public/language/hy/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Գրառումների տեսակավորում", + "sorting.post-default": "Գրառումների հիմնական տեսակավորում", + "sorting.oldest-to-newest": "Ամենահնից նորագույնը", + "sorting.newest-to-oldest": "Նորից հինը", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Ամենաշատ գրառումները", + "sorting.topic-default": "Թեմայի կանխադրված տեսակավորում", + "length": "Գրառման երկարությունը", + "post-queue": "Գրառման հերթ", + "restrictions": "Posting Restrictions", + "restrictions-new": "Նոր Օգտատիրոջ սահմանափակումներ", + "restrictions.post-queue": "Միացնել գրառումների հերթը", + "restrictions.post-queue-rep-threshold": "Փոստի հերթը շրջանցելու համար պահանջվում է վարկանիշ", + "restrictions.groups-exempt-from-post-queue": "Ընտրեք խմբեր, որոնք պետք է ազատվեն հաղորդագրությունների հերթից", + "restrictions-new.post-queue": "Միացնել նոր օգտատերերի սահմանափակումները", + "restrictions.post-queue-help": "Գրառումների հերթի ակտիվացումը նոր օգտատերերի գրառումները հերթում կդնի հաստատման համար", + "restrictions-new.post-queue-help": "Օգտատերերի նոր սահմանափակումների ակտիվացումը սահմանափակումներ կսահմանի նոր օգտատերերի կողմից ստեղծված գրառումների վրա", + "restrictions.seconds-between": "Գրառումների միջև ընկած վայրկյանների քանակը", + "restrictions.seconds-between-new": "Նոր օգտատերերի համար գրառումների միջև ընկած վայրկյաններ", + "restrictions.rep-threshold": "Վարկանիշի շեմը՝ մինչև այս սահմանափակումների վերացումը", + "restrictions.seconds-before-new": "Վայրկյաններ առաջ, երբ նոր օգտատերը կարող է կատարել իր առաջին գրառումը", + "restrictions.seconds-edit-after": "Գրառման վայրկյանների քանակը մնում է խմբագրելի (անջատելու համար սահմանել 0)", + "restrictions.seconds-delete-after": "Գրառման համար ջնջելի մնալու վայրկյանների քանակը (անջատելու համար դրված է 0)", + "restrictions.replies-no-delete": "Պատասխանների քանակը այն բանից հետո, երբ օգտատերերին թույլ չեն տվել ջնջել իրենց սեփական թեմաները (անջատելու համար սահմանվել է 0)", + "restrictions.min-title-length": "Վերնագրի նվազագույն երկարությունը", + "restrictions.max-title-length": "Վերնագրի առավելագույն երկարությունը", + "restrictions.min-post-length": "Գրառման նվազագույն երկարությունը", + "restrictions.max-post-length": "Գրառման առավելագույն երկարությունը", + "restrictions.days-until-stale": "Օրեր, մինչև թեման հնացած համարվի", + "restrictions.stale-help": "Եթե թեման համարվում է «հնացած», ապա նախազգուշացում կցուցադրվի այն օգտատերերին, ովքեր կփորձեն պատասխանել այդ թեմային:", + "timestamp": "Ժամացույց", + "timestamp.cut-off": "Ամսաթիվների կրճատում (օրերով)", + "timestamp.cut-off-help": "Ամսաթվերը & AMP; ժամանակները կցուցադրվեն հարաբերական ձևով (օրինակ՝ «3 ժամ առաջ» / «5 օր առաջ») և կտեղայնացվեն տարբեր լեզուները։ Որոշակի պահից հետո այս տեքստը կարող է փոխարկվել՝ ցուցադրելու տեղայնացված ամսաթիվը (օրինակ՝ 5 նոյեմբերի 2016թ. 15:30): (Լռելյայն՝ 30 կամ մեկ ամիս): Սահմանեք 0՝ միշտ ցուցադրելու ամսաթվերը, թողեք դատարկ՝ հարաբերական ժամանակները միշտ ցուցադրելու համար:", + "timestamp.necro-threshold": "Նեկրո շեմ (օրերով)", + "timestamp.necro-threshold-help": "Հաղորդագրություն կցուցադրվի հաղորդագրությունների միջև, եթե դրանց միջև ընկած ժամանակն ավելի երկար է, քան նեկրո շեմը: (հիմնական՝ 7 կամ մեկ շաբաթ): Անջատելու համար դրեք 0:", + "timestamp.topic-views-interval": "Մեծացնել թեմայի դիտումների միջակայքը (րոպեներով)", + "timestamp.topic-views-interval-help": "Թեմայի դիտումները կավելանան միայն յուրաքանչյուր X րոպեն մեկ, ինչպես սահմանված է այս պարամետրով:", + "teaser": "Teaser գրառում", + "teaser.last-post": "Վերջին & ndash; Ցույց տալ վերջին գրառումը, ներառյալ բնօրինակը, եթե պատասխաններ չկան", + "teaser.last-reply": "Վերջին & ndash; Ցույց տալ վերջին պատասխանը կամ «Պատասխաններ չկան» տեղապահ, եթե պատասխաններ չկան", + "teaser.first": "Առաջին", + "showPostPreviewsOnHover": "Ցույց տալ հաղորդագրությունների նախադիտումը, երբ մկնիկը սեղմում է", + "unread": "Չընթերցված կարգավորումներ", + "unread.cutoff": "Չընթերցված անջատման օրեր", + "unread.min-track-last": "Նվազագույն գրառումները թեմայում մինչև վերջին ընթերցվածը հետևելը", + "recent": "Վերջին կարգավորումները", + "recent.max-topics": "Առավելագույն թեմաները /վերջին", + "recent.categoryFilter.disable": "Անջատել թեմաների զտումը անտեսված կատեգորիաներում /վերջին էջում", + "signature": "Ստորագրության կարգավորումներ", + "signature.disable": "Անջատել ստորագրությունները", + "signature.no-links": "Անջատել հղումները ստորագրությունների մեջ", + "signature.no-images": "Անջատել նկարները ստորագրություններում", + "signature.hide-duplicates": "Թաքցնել կրկնօրինակ ստորագրությունները թեմաներում", + "signature.max-length": "Ստորագրության առավելագույն երկարությունը", + "composer": "Շարադրողի կարգավորումներ", + "composer-help": "Հետևյալ կարգավորումները կարգավորում են ցուցադրվող գրառումն շարադրողի ֆունկցիոնալությունը և/կամ տեսքը օգտատերերին, երբ նրանք ստեղծում են նոր թեմաներ կամ պատասխանում առկա թեմաներին:", + "composer.show-help": "Ցույց տալ «Օգնություն» ներդիրը", + "composer.enable-plugin-help": "Թույլատրել հավելվածներին ավելացնել բովանդակություն օգնության ներդիրում", + "composer.custom-help": "Հատուկ օգնության տեքստ", + "backlinks": "Հետադարձ կապեր", + "backlinks.enabled": "Միացնել թեմայի հետադարձ կապերը", + "backlinks.help": "Եթե գրառումը հղում է կատարում մեկ այլ թեմայի, ապա այդ պահին հղումը դեպի գրառում կտեղադրվի նշված թեմայի մեջ:", + "ip-tracking": "IP Հետևում", + "ip-tracking.each-post": "Հետևեք IP հասցեին յուրաքանչյուր գրառման համար", + "enable-post-history": "Միացնել գրառումների պատմությունը" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/reputation.json b/public/language/hy/admin/settings/reputation.json new file mode 100644 index 0000000000..27df7d5fbc --- /dev/null +++ b/public/language/hy/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Վարկանիշի կարգավորումներ", + "disable": "Անջատել վարկանիշի համակարգը", + "disable-down-voting": "Անջատել Down Voting-ը", + "votes-are-public": "Բոլոր ձայները հրապարակային են", + "thresholds": "Անջատել վարկանիշի համակարգը", + "min-rep-upvote": "Նվազագույն հեղինակություն դրական քվեարկության համար", + "upvotes-per-day": "Օրական կողմ ձայներ (սահմանված է 0 անսահմանափակ կողմ ձայների համար)", + "upvotes-per-user-per-day": "Կողմ ձայներ մեկ օգտատիրոջ համար մեկ օրում (սահմանված է 0՝ անսահմանափակ կողմ ձայների համար)", + "min-rep-downvote": "Նվազագույն վարկաիշ դեմ քվեարկության համար", + "downvotes-per-day": "Օրական դեմ ձայներ (սահմանված է 0՝ անսահմանափակ դեմ ձայների համար)", + "downvotes-per-user-per-day": "Դեմ ձայներ մեկ օգտատիրոջ համար մեկ օրում (սահմանված է 0՝ անսահմանափակ դեմ ձայների համար)", + "min-rep-chat": "Զրույցի հաղորդագրություններ ուղարկելու նվազագույն վարկանիշ", + "min-rep-flag": "Նվազագույն վարկանիշ դրոշի գրառումների համար", + "min-rep-website": "«Վեբկայք» օգտատերի պրոֆիլին ավելացնելու նվազագույն վարկանիշ", + "min-rep-aboutme": "«Իմ մասին» օգտատիրոջ պրոֆիլին ավելացնելու նվազագույն վարկանիշ", + "min-rep-signature": "Օգտատիրոջ պրոֆիլում «Ստորագրություն» ավելացնելու նվազագույն վարկանիշ", + "min-rep-profile-picture": "Օգտատիրոջ պրոֆիլում «Պրոֆիլի նկար» ավելացնելու նվազագույն վարկանիշ", + "min-rep-cover-picture": "Օգտատիրոջ պրոֆիլում «Cover Picture» ավելացնելու նվազագույն վարկանիշ", + + "flags": "Դրոշի կարգավորումներ", + "flags.limit-per-target": "Առավելագույն թվով անգամներ կարելի է նշել ինչ-որ բան", + "flags.limit-per-target-placeholder": "Հիմնական: 0", + "flags.limit-per-target-help": "Երբ գրառումը կամ օգտատերը մի քանի անգամ դրոշակվում է, յուրաքանչյուր լրացուցիչ դրոշակ համարվում է «հաշվետվություն» և ավելացվել է բնօրինակ դրոշին: Սահմանեք այս ընտրանքը զրոյից տարբեր թվերի վրա՝ սահմանափակելու համար նյութի ստացած հաշվետվությունների քանակը:", + "flags.auto-flag-on-downvote-threshold": "Ավտոմատ դրոշակավորող գրառումներին դեմ ձայների քանակը (Անջատելու համար սահմանել 0, լռելյայն՝ 0)", + "flags.auto-resolve-on-ban": "Ավտոմատ կերպով լուծել օգտվողի բոլոր տոմսերը, երբ դրանք արգելված են", + "flags.action-on-resolve": "Երբ դրոշը լուծվում է, արեք հետևյալը", + "flags.action-on-reject": "Երբ դրոշը մերժվում է, արեք հետևյալը", + "flags.action.nothing": "Ոչինչ Չանել ", + "flags.action.rescind": "Չեղարկել մոդերատորներին/ադմինիստրատորներին ուղարկված ծանուցումը" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/social.json b/public/language/hy/admin/settings/social.json new file mode 100644 index 0000000000..26814f1310 --- /dev/null +++ b/public/language/hy/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Կիսվել հրապարակումով", + "info-plugins-additional": "Փլագինները կարող են ավելացնել լրացուցիչ ցանցեր՝ հրապարակումների փոխանակման համար:", + "save-success": "Հրապարակումների փոխանակման ցանցերը հաջողությամբ պահպանվեցին:" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/sockets.json b/public/language/hy/admin/settings/sockets.json new file mode 100644 index 0000000000..32e9137857 --- /dev/null +++ b/public/language/hy/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Վերամիացման կարգավորումներ", + "max-attempts": "Վերամիացման առավելագույն փորձեր", + "default-placeholder": "Հիմնական: %1", + "delay": "Վերամիացման հետաձգում" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/sounds.json b/public/language/hy/admin/settings/sounds.json new file mode 100644 index 0000000000..85d7ebd881 --- /dev/null +++ b/public/language/hy/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Ծանուցումներ", + "chat-messages": "Զրույցի հաղորդագրություններ", + "play-sound": "Նվագել ", + "incoming-message": "Մուտքային հաղորդագրություն", + "outgoing-message": "Ելքային հաղորդագրություն", + "upload-new-sound": "Վերբեռնեք նոր ձայն", + "saved": "Կարգավորումները պահպանված են" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/tags.json b/public/language/hy/admin/settings/tags.json new file mode 100644 index 0000000000..cb64618da0 --- /dev/null +++ b/public/language/hy/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Պիտակի Կարգավորումներ", + "link-to-manage": "Կառավարել թագերը", + "system-tags": "Համակարգի պիտակներ", + "system-tags-help": "Միայն արտոնյալ օգտատերերերը կկարողանան օգտագործել այս պիտակները: ", + "min-per-topic": "Նվազագույն պիտակներ մեկ թեմայի համար", + "max-per-topic": "Առավելագույն պիտակներ յուրաքանչյուր թեմայի համար", + "min-length": "Պիտակի նվազագույն երկարությունը", + "max-length": "Պիտակի առավելագույն երկարությունը", + "related-topics": "Առնչվող թեմաներ", + "max-related-topics": "Ցուցադրվող առավելագույն առնչվող թեմաներ (եթե աջակցվում է թեմայի կողմից)" +} \ No newline at end of file diff --git a/public/language/hy/admin/settings/uploads.json b/public/language/hy/admin/settings/uploads.json new file mode 100644 index 0000000000..efa15c8a02 --- /dev/null +++ b/public/language/hy/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Գրառումներ", + "orphans": "Լքված Ֆայլեր", + "private": "Վերբեռնված ֆայլերը դարձրեք մասնավոր", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Վերբեռնված ֆայլերը պահեք դիսկի վրա գրառումը մաքրելուց հետո", + "orphanExpiryDays": "լքված ֆայլեր պահելու օրեր", + "orphanExpiryDays-help": "Այսքան օրեր անց, լքված վերբեռնումները կջնջվեն ֆայլային համակարգից: Սահմանեք 0 կամ թողեք դատարկ՝ անջատելու համար:", + "private-extensions": "Ֆայլերի ընդարձակումներ՝ մասնավոր դարձնելու համար", + "private-uploads-extensions-help": "Մուտքագրեք ստորակետերով բաժանված ֆայլերի ընդլայնումների ցանկը՝ մասնավոր դարձնելու համար այստեղ (օրինակ՝ pdf, xls, doc): Դատարկ ցուցակը նշանակում է, որ բոլոր ֆայլերը մասնավոր են:", + "resize-image-width-threshold": "Նկարների չափափոխում, եթե դրանք ավելի լայն են, քան նշված լայնությունը", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Չափափոխել պատկերները մինչև նշված լայնությունը", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Նկարների չափափոխման ժամանակ օգտագործելու որակ", + "resize-image-quality-help": "Օգտագործեք ավելի ցածր որակի կարգավորում՝ չափափոխված պատկերների ֆայլի չափը նվազեցնելու համար:", + "max-file-size": "Ֆայլի առավելագույն չափը (կիբ-ով)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Նկարի առավելագույն լայնությունը (պիքսելներով)", + "reject-image-width-help": "Այս արժեքից ավելի լայն նկարները կմերժվեն:", + "reject-image-height": "Նկարի առավելագույն բարձրությունը (պիքսելներով)", + "reject-image-height-help": "Այս արժեքից բարձր նկարները կմերժվեն:", + "allow-topic-thumbnails": "Թույլ տվեք օգտատերերին վերբեռնել թեմայի մանրապատկերները", + "topic-thumb-size": "Թեմայի Thumb չափ", + "allowed-file-extensions": "Թույլատրված ֆայլերի ընդարձակումներ", + "allowed-file-extensions-help": "Մուտքագրեք ստորակետերով բաժանված ֆայլերի ընդարձակման ցանկն այստեղ (օրինակ՝ pdf, xls, doc): Դատարկ ցուցակը նշանակում է, որ բոլոր ընդլայնումները թույլատրված են:", + "upload-limit-threshold": "Սահմանափակել օգտատերերի վերբեռնումները հետևյալ հասցեով՝", + "upload-limit-threshold-per-minute": "%1 րոպեում", + "upload-limit-threshold-per-minutes": "%1 րոպեի դիմաց", + "profile-avatars": "Պրոֆիլների Ավատարներ", + "allow-profile-image-uploads": "Թույլ տվեք օգտատերերին վերբեռնել պրոֆիլի նկարներ", + "convert-profile-image-png": "Փոխարկել պրոֆիլի պատկերների վերբեռնումները PNG-ի", + "default-avatar": "Պատվերով հիմնական ավատար", + "upload": "Վերբեռնել", + "profile-image-dimension": "Պրոֆիլի նկարի չափս", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Պրոֆիլի պատկերի ֆայլի առավելագույն չափը", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Շապիկի պատկերի ֆայլի առավելագույն չափը", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Սերվերում պահեք ավատարների և պրոֆիլների շապիկների հին տարբերակները", + "profile-covers": "Պրոֆիլի շապիկներ ", + "default-covers": "Հիմնական շապիկի նկար ", + "default-covers-help": "Ավելացրեք ստորակետերով բաժանված հիմնակակ շապիկի պատկերներ այն հաշիվների համար, որոնք չունեն վերբեռնված շապիկի նկար" +} diff --git a/public/language/hy/admin/settings/user.json b/public/language/hy/admin/settings/user.json new file mode 100644 index 0000000000..81d4337eec --- /dev/null +++ b/public/language/hy/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Նույնականացում", + "email-confirm-interval": "Օգտատերը չի կարող նորից ուղարկել հաստատման էլ.փոստ", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Թույլատրել մուտք գործել", + "allow-login-with.username-email": "Օգտանուն կամ էլ.փոստ", + "allow-login-with.username": "Միայն օգտանունը", + "account-settings": "Հաշվի կարգավորումներ ", + "gdpr_enabled": "Միացնել GDPR-ի համաձայնության հավաքագրումը", + "gdpr_enabled_help": "Երբ միացված է, բոլոր նոր գրանցողներից կպահանջվի հստակ համաձայնություն տալ տվյալների հավաքագրման և օգտագործման համար՝ համաձայն Տվյալների պաշտպանության ընդհանուր կանոնակարգի (GDPR): Նշում. GDPR-ի միացումը չի ստիպում նախկինում գոյություն ունեցող օգտվողներին տրամադրել համաձայնություն: Դա անելու համար ձեզ հարկավոր է տեղադրել GDPR հավելվածը:", + "disable-username-changes": "Անջատել օգտատիրոջ անվան փոփոխությունները", + "disable-email-changes": "Անջատել էլ.փոստի փոփոխությունները", + "disable-password-changes": "Անջատել գաղտնաբառի փոփոխությունները", + "allow-account-deletion": "Թույլատրել հաշվի ջնջումը", + "hide-fullname": "Թաքցնել լրիվ անունը օգտատերերից", + "hide-email": "Թաքցնել էլփոստը օգտատերերց", + "show-fullname-as-displayname": "Ցուցադրել օգտատերի լրիվ անունը որպես ցուցադրվող անուն, եթե առկա է", + "themes": "Թեմաներ", + "disable-user-skins": "Օգտատերերին թույլ չտալ ընտրելու հատուկ շապիկ ", + "account-protection": "Հաշվի պաշտպանություն", + "admin-relogin-duration": "Ադմինիստրատորի վերագրանցման տևողությունը (րոպե)", + "admin-relogin-duration-help": "Որոշ ժամանակ անց ադմինիստրատորի բաժին մուտք գործելու համար կպահանջվի նորից մուտք գործել, անջատելու համար սահմանեք 0", + "login-attempts": "Մուտքի փորձեր մեկ ժամում", + "login-attempts-help": "Գաղտնաբառի նվազագույն երկարությունը, եթե օգտատիրոջ հաշիվ մուտք գործելու փորձերը գերազանցում են այս շեմը, այդ հաշիվը կկողպվի նախապես կազմաձևված ժամանակով", + "lockout-duration": "Հաշվի արգելափակման տևողությունը (րոպե)", + "login-days": "Օգտատիրոջ մուտքի նիստերը հիշելու օրեր", + "password-expiry-days": "Ստիպել գաղտնաբառի վերակայում որոշակի օրերից հետո", + "session-time": "Սեսիայի Ժամանակ", + "session-time-days": "Օրեր", + "session-time-seconds": "Վայրկյաններ ", + "session-time-help": "Այս արժեքներն օգտագործվում են որոշելու համար, թե որքան ժամանակ է օգտվողը մնում մուտք գործած, երբ նա ստուգում է «Հիշիր ինձ» մուտքի վրա: Նշենք, որ այս արժեքներից միայն մեկը կօգտագործվի: Եթե վայրկյանների արժեք չկա, մենք վերադառնում ենք օրերի: Եթե օրերի արժեք չկա, մենք լռելյայն սահմանում ենք 14 օր:", + "online-cutoff": "Րոպեներ անց Օգտագործողը համարվում է ոչ ակտիվ, ", + "online-cutoff-help": "Եթե օգտատերը այս տևողության համար որևէ գործողություններ չի կատարում, նա համարվում է ոչ ակտիվ և իրական ժամանակում թարմացումներ չի ստանում:", + "registration": "Օգտատերի գրանցում ", + "registration-type": "Գրանցման տեսակը", + "registration-approval-type": "Գրանցման հաստատման տեսակը", + "registration-type.normal": "Նորմալ ", + "registration-type.admin-approval": "Ադմինիստրատորի հաստատում", + "registration-type.admin-approval-ip": "Ադմինիստրատորի հաստատում IP-ների համար", + "registration-type.invite-only": "Միայն Հրավիրել ", + "registration-type.admin-invite-only": "Միայն ադմինիստրատորի հրավեր", + "registration-type.disabled": "Գրանցում չկա", + "registration-type.help": "Նորմալ - Օգտատերերը կարող են գրանցվել \"գրանցվել\" էջից: Միայն հրավիրել - օգտատերերը կարող են հրավիրել ուրիշներին օգտատերերի էջից: Միայն ադմինիստրատորի հրավեր - Միայն ադմինիստրատորները կարող են հրավիրել ուրիշներին օգտատերերից և ադմինիստրատորի/կառավարման/օգտատերերի էջերից: Ոչ գրանցում - Օգտատիրոջ գրանցում չկա:", + "registration-approval-type.help": "Նորմալ - օգտարերերը գրանցվում են անմիջապես: Ադմինիստրատորի հաստատում - Օգտատերերի գրանցումները տեղադրվում են ադմինիստրատորների հաստատման հերթում: Ադմինիստրատորի հաստատում IP-ների համար - Նորմալ է նոր օգտատերերի համար, Ադմինիստրատորի հաստատում IP հասցեների համար, որոնք արդեն ունեն հաշիվ:", + "registration-queue-auto-approve-time": "Ավտոմատ հաստատման ժամանակը", + "registration-queue-auto-approve-time-help": "Օգտագործողի ինքնաբերաբար հաստատումից ժամեր առաջ: 0 անջատելու համար:", + "registration-queue-show-average-time": "Ցույց տալ օգտատերերին միջին ժամանակը, որը հարկավոր է նոր օգտատիրոջը հաստատելու համար", + "registration.max-invites": "Մեկ օգտատերի համար առավելագույն հրավերներ", + "max-invites": "Մեկ օգտվողի համար առավելագույն հրավերներ", + "max-invites-help": "Առանց սահմանափակման: Ադմինները ստանում են անսահման հրավերներ, Միայն կիրառելի է «Միայն հրավիրել»", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Օգտանունի նվազագույն երկարությունը", + "max-username-length": "Օգտանունի առավելագույն երկարություն", + "min-password-length": "Գաղտնաբառի նվազագույն երկարությունը", + "min-password-strength": "Գաղտնաբառի նվազագույն հզորությունը", + "max-about-me-length": " Իմ մասին առավելագույն երկարությունը", + "terms-of-use": "Ֆորումի Օգտագործման պայմաններ (Անջատելու համար դատարկ թողեք)", + "user-search": "Օգտատիրոջ որոնում", + "user-search-results-per-page": "Ցուցադրվող արդյունքների քանակը", + "default-user-settings": "Օգտատիրոջ հիմնական կարգավորումներ", + "show-email": "Ցույց տալ էլ.նամակը", + "show-fullname": "Ցույց տալ լրիվ անունը", + "restrict-chat": "Թույլատրել զրույցի հաղորդագրությունները միայն այն օգտվողներից, որոնց ես հետևում եմ", + "outgoing-new-tab": "Բացել ելքային հղումները նոր ներդիրում", + "topic-search": "Միացնել թեմայում որոնումը", + "update-url-with-post-index": "Թեմաներ զննարկելիս թարմացրեք url-ը գրառումների ինդեքսով", + "digest-freq": "Բաժանորդագրվել Digest-ին", + "digest-freq.off": "Անջատված", + "digest-freq.daily": "Օրական", + "digest-freq.weekly": "Շաբաթական", + "digest-freq.biweekly": "Երկու շաբաթը մեկ ", + "digest-freq.monthly": "Ամսեկան", + "email-chat-notifs": "Ուղարկել էլ.նամակ, եթե նոր զրույցի հաղորդագրություն է գալիս, և ես առցանց չեմ", + "email-post-notif": "Ուղարկել էլ.նամակ, երբ պատասխաններ են տրվում այն թեմաներին, որոնց ես բաժանորդագրված եմ", + "follow-created-topics": "Հետևեք ձեր ստեղծած թեմաներին", + "follow-replied-topics": "Հետևեք այն թեմաներին, որոնց պատասխանում եք", + "default-notification-settings": "Հիմնական ծանուցման կարգավորումներ", + "categoryWatchState": "Հիմնական կատեգորիայի դիտման վիճակը", + "categoryWatchState.watching": "Դիտում ", + "categoryWatchState.notwatching": "Չեն դիտում ", + "categoryWatchState.ignoring": "Անտեսել " +} diff --git a/public/language/hy/admin/settings/web-crawler.json b/public/language/hy/admin/settings/web-crawler.json new file mode 100644 index 0000000000..e756979d73 --- /dev/null +++ b/public/language/hy/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability կարգավորումներ", + "robots-txt": "Պատվերով Robots.txt Թողնել դատարկ հիմնականի համար ", + "sitemap-feed-settings": "Կայքի քարտեզ և լրահոսի կարգավորումներ", + "disable-rss-feeds": "Անջատել RSS Feeds", + "disable-sitemap-xml": "Անջատել Sitemap.xml", + "sitemap-topics": "Կայքի քարտեզում ցուցադրվող թեմաների քանակը", + "clear-sitemap-cache": "Մաքրել կայքի քարտեզի քեշը", + "view-sitemap": "Դիտել կայքի քարտեզը" +} \ No newline at end of file diff --git a/public/language/hy/category.json b/public/language/hy/category.json new file mode 100644 index 0000000000..cf6c927dca --- /dev/null +++ b/public/language/hy/category.json @@ -0,0 +1,23 @@ +{ + "category": "Կատեգորիա", + "subcategories": "Ենթակատեգորիաներ", + "new_topic_button": "Նոր թեմա", + "guest-login-post": "Մուտք գործեք՝ գրառում կատարելու համար", + "no_topics": "Այս բաժնում ոչ մի թեմա չկա։
Գուցե հենց Դո՞ւք ստեղծեք մեկը։", + "browsing": "դիտում են", + "no_replies": "Ոչ ոք չի պատասխանել", + "no_new_posts": "Նոր գրառումներ չկան։", + "watch": "Դիտել", + "ignore": "Անտեսել", + "watching": "Դիտում", + "not-watching": "Չեն դիտում ", + "ignoring": "Անտեսել", + "watching.description": "Ցույց տալ թեմաները չկարդացված և վերջին բաժնում ", + "not-watching.description": "Չընթերցված թեմաները չցուցադրել, ցուցադրել վերջինները", + "ignoring.description": "Մի ցուցադրեք թեմաները չընթերցված և վերջին բաժնում ", + "watching.message": "Դուք այժմ դիտում եք թարմացումներ այս կատեգորիայից և բոլոր ենթակատեգորիաներից", + "notwatching.message": "Դուք չեք դիտում այս կատեգորիայի և բոլոր ենթակատեգորիաների թարմացումները", + "ignoring.message": "Դուք այժմ անտեսում եք այս կատեգորիայի և բոլոր ենթակատեգորիաների թարմացումները", + "watched-categories": "Դիտված կատեգորիաներ", + "x-more-categories": "Եվս %1 կատեգորիա" +} \ No newline at end of file diff --git a/public/language/hy/email.json b/public/language/hy/email.json new file mode 100644 index 0000000000..891b72ad04 --- /dev/null +++ b/public/language/hy/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Փորձնական էլ.նամակ", + "password-reset-requested": "Գաղտնաբառի վերականգնում է պահանջվում", + "welcome-to": "Բարի գալուստ %1", + "invite": "Հրավեր %1-ի կողմից", + "greeting_no_name": "Ողջույն", + "greeting_with_name": "Ողջույն %1", + "email.verify-your-email.subject": "Խնդրում ենք ստուգել ձեր էլփոստը", + "email.verify.text1": "Դուք խնդրել եք փոխել կամ հաստատել ձեր էլ.փոստի հասցեն", + "email.verify.text2": "Անվտանգության նկատառումներից ելնելով, մենք փոխում կամ հաստատում ենք ֆայլում առկա էլփոստի հասցեն միայն այն բանից հետո, երբ դրա սեփականության իրավունքը հաստատվի էլփոստի միջոցով: Եթե դուք չեք խնդրել դա, ձեր կողմից որևէ գործողություն չի պահանջվում:", + "email.verify.text3": "այս էլփոստի հասցեն հաստատելուց հետո մենք կփոխարինենք ձեր ընթացիկ էլ. հասցեն այս հասցեով (%1):", + "welcome.text1": "Շնորհակալություն %1-ի միջոցով գրանցվելու համար", + "welcome.text2": "Ձեր հաշիվն ամբողջությամբ ակտիվացնելու համար մենք պետք է հաստատենք, որ ձեզ է պատկանում էլփոստի հասցեն, որով գրանցվել եք", + "welcome.text3": "Ադմինիստրատորն ընդունել է ձեր գրանցման դիմումը: Այժմ կարող եք մուտք գործել ձեր օգտանունով/գաղտնաբառով:", + "welcome.cta": "Սեղմեք այստեղ, որպեսզի հաստատեք ձեր էլ․ հասցեն", + "invitation.text1": "%1-ը հրավիրել է ձեզ %2-ին միանալու", + "invitation.text2": "Ձեր հրավերի ժամկետը կլրանա %1 օրից:", + "invitation.cta": "Սեղմեք այստեղ՝ ձեր հաշիվը ստեղծելու համար:", + "reset.text1": "Մենք ստացել ենք ձեր գաղտնաբառը վերականգնելու հարցում, հնարավոր է, որ դուք մոռացել եք այն: Եթե դա այդպես չէ, խնդրում ենք անտեսել այս էլ. նամակը", + "reset.text2": "Գաղտնաբառի վերականգնումը շարունակելու համար սեղմեք հետևյալ հղումը.", + "reset.cta": "Սեղմեք այստեղ, որպեսզի զրոյացնեք ձեր գաղտնաբառը", + "reset.notify.subject": "Գաղտնաբառը հաջողությամբ փոխված է", + "reset.notify.text1": "Մենք ծանուցում ենք ձեզ, որ %1-ում ձեր գաղտնաբառը հաջողությամբ փոխվել է:", + "reset.notify.text2": "Եթե դուք չեք թույլատրել սա, խնդրում ենք անմիջապես տեղեկացնել ադմինիստրատորին:", + "digest.latest_topics": "Վերջին թեմաները %1-ից", + "digest.top-topics": "Հիմնական թեմաները %1-ից", + "digest.popular-topics": "Հանրաճանաչ թեմաներ %1-ից", + "digest.cta": "Սեղմեք այստեղ՝ %1 այցելելու համար", + "digest.unsub.info": "Այս ամփոփագիրն ուղարկվել է ձեզ՝ ձեր բաժանորդագրության կարգավորումների պատճառով:", + "digest.day": "օր", + "digest.week": "շաբաթ", + "digest.month": "ամիս", + "digest.subject": "Ամփոփագիր՝ %1-ի համար", + "digest.title.day": "Ձեր ամենօրյա ամփոփագիրը", + "digest.title.week": "Ձեր շաբաթական ամփոփագիրը", + "digest.title.month": "Ձեր ամսական ամփոփագիրը", + "notif.chat.subject": "Նոր զրույցի հաղորդագրություն ստացվել է %1-ից", + "notif.chat.cta": "Սեղմեք այստեղ՝ զրույցը շարունակելու համար", + "notif.chat.unsub.info": "Այս զրույցի ծանուցումն ուղարկվել է ձեզ՝ ձեր բաժանորդագրության կարգավորումների պատճառով:", + "notif.post.unsub.info": "Գրառման այս ծանուցումն ուղարկվել է ձեզ՝ ձեր բաժանորդագրության կարգավորումների պատճառով:", + "notif.post.unsub.one-click": "Այլապես, ապաբաժանորդագրվեք նման ապագա նամակների ստանալու համար ՝ սեղմելով", + "notif.cta": "Դեպի ֆորում", + "notif.cta-new-reply": "Դիտել գրառումը ", + "notif.cta-new-chat": "Դիտել զրույցը ", + "notif.test.short": "Փորձարկման ծանուցումներ", + "notif.test.long": "Սա ծանուցումների էլ.փոստի փորձարկում է: Ուղարկե՛ք օգնություն:", + "test.text1": "Սա փորձնական նամակ է՝ ստուգելու, որ էլփոստի ուղարկողը ճիշտ է կարգավորվել ձեր NodeBB-ի համար:", + "unsub.cta": "Սեղմեք այստեղ՝ այդ կարգավորումները փոխելու համար", + "unsubscribe": "դուրս գալ բաժանորդագրությունից", + "unsub.success": "Դուք այլևս նամակներ չեք ստանա %1 փոստային ցուցակից", + "unsub.failure.title": "Չհաջողվեց չեղարկել բաժանորդագրությունը", + "unsub.failure.message": "Ցավոք, մենք չկարողացանք հեռացնել ձեզ փոստային ցուցակից, քանի որ հղման հետ կապված խնդիր կար: Այնուամենայնիվ, դուք կարող եք փոխել ձեր էլփոստի նախապատվությունները՝ անցնելով ձեր օգտվողի կարգավորումները: (սխալ՝ %1)", + "banned.subject": "Ձեզ արգելափակել են %1-ից", + "banned.text1": "%1 օգտվողին արգելվել է %2-ից:", + "banned.text2": "Այս արգելքը կտևի մինչև %1:", + "banned.text3": "Սա է պատճառը, որ դուք արգելվել եք.", + "closing": "Շնորհակալությո՛ւն" +} \ No newline at end of file diff --git a/public/language/hy/error.json b/public/language/hy/error.json new file mode 100644 index 0000000000..9c7fb41cf3 --- /dev/null +++ b/public/language/hy/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Սխալ տվյալ", + "invalid-json": "Անվավեր JSON", + "wrong-parameter-type": "«%1» հատկության համար սպասվում էր %3 տիպի արժեք, բայց փոխարենը ստացվեց %2", + "required-parameters-missing": "Պահանջվող պարամետրերը բացակայում էին այս API զանգից՝ %1", + "not-logged-in": "Դուք, կարծես, մուտք չեք գործել:", + "account-locked": "Ձեր հաշիվը ժամանակավորապես արգելափակվել է", + "search-requires-login": "Որոնումը պահանջում է հաշիվ. խնդրում ենք մուտք գործել կամ գրանցվել:", + "goback": "Սեղմեք հետ՝ նախորդ էջ վերադառնալու համար", + "invalid-cid": "Անվավեր կատեգորիայի ID", + "invalid-tid": "Անվավեր թեմայի ID", + "invalid-pid": "Անվավեր գրառման ID", + "invalid-uid": "Օգտվողի անվավեր ID", + "invalid-mid": "Զրույցի հաղորդագրության անվավեր ID", + "invalid-date": "Պետք է տրամադրվի վավեր ամսաթիվ", + "invalid-username": "Մուտքանվան անվավեր ID", + "invalid-email": "Սխալ Էլեկտրոնային փոստի հասցե", + "invalid-fullname": "Անվավեր լրիվ անուն", + "invalid-location": "Անվավեր դիրք", + "invalid-birthday": "Անվավեր ծննդյան օր", + "invalid-title": "Անվավեր վերնագիր", + "invalid-user-data": "Օգտվողի անվավեր տվյալներ", + "invalid-password": "Անվավեր գաղտնաբառ", + "invalid-login-credentials": "Անվավեր մուտքի հավատարմագրեր", + "invalid-username-or-password": "Նշեք օգտվողի և՛ անունը, և՛ գաղտնաբառը", + "invalid-search-term": "Անվավեր որոնման տերմին", + "invalid-url": "Անվավեր URL", + "invalid-event": "Անվավեր իրադարձություն՝ %1", + "local-login-disabled": "Տեղական մուտքի համակարգը անջատվել է ոչ արտոնյալ հաշիվների համար:", + "csrf-invalid": "Մենք չկարողացանք մուտք գործել ձեզ, հավանաբար ժամկետանց աշխատաշրջանի պատճառով: Խնդրում եմ կրկին փորձեք", + "invalid-path": "Անվավեր ուղի", + "folder-exists": "Թղթապանակ գոյություն ունի", + "invalid-pagination-value": "Էջավորման անվավեր արժեքը, պետք է լինի առնվազն %1 և առավելագույնը %2", + "username-taken": "Օգտագործողի անունը վերցված է", + "email-taken": "Էլփոստը վերցված է", + "email-nochange": "Մուտքագրված էլփոստը նույնն է, ինչ ֆայլում արդեն առկա էլ.", + "email-invited": "Էլփոստն արդեն հրավիրված էր", + "email-not-confirmed": "Որոշ կատեգորիաներում կամ թեմաներում հրապարակելը միացված կլինի, երբ ձեր էլփոստը հաստատվի, խնդրում ենք սեղմել այստեղ՝ հաստատող էլփոստը ուղարկելու համար:", + "email-not-confirmed-chat": "Դուք չեք կարող զրուցել, քանի դեռ ձեր էլ․ հասցեն չի հաստատվել, խնդրում ենք սեղմել այստեղ՝ ձեր էլ.հասցեն հաստատելու համար։", + "email-not-confirmed-email-sent": "Ձեր էլ.փոստը դեռ հաստատված չէ, խնդրում ենք ստուգել ձեր մուտքի արկղը՝ հաստատման էլ.նամակի համար: Հնարավոր է, որ չկարողանաք գրառում կատարել որոշ կատեգորիաներում կամ զրուցել, մինչև ձեր էլ.փոստը չհաստատվի:", + "no-email-to-confirm": "Ձեր հաշվում էլ.փոստ չկա: Հաշիվը վերականգնելու ,ինչպես նաև որոշ կատեգորիաներում գրառում կատարելու և զրուցելու համար անհրաժեշտ է էլ.հասցե: Խնդրում ենք սեղմել այստեղ՝ էլ. հասցե մուտքագրելու համար:", + "user-doesnt-have-email": "Օգտատերը «%1» չունի էլփոստի հավաքածու:", + "email-confirm-failed": "Մենք չկարողացանք հաստատել Ձեր էլ.փոստը, խնդրում ենք փորձել ավելի ուշ։", + "confirm-email-already-sent": "Հաստատման էլ.նամակն արդեն ուղարկվել է, խնդրում ենք սպասել %1 րոպե՝ ևս մեկ ուղարկելու համար:", + "sendmail-not-found": "Sendmail գործարկիչը չի գտնվել, համոզվեք, որ այն տեղադրված է և գործարկվում է NodeBB-ով աշխատող օգտատիրոջ կողմից:", + "digest-not-enabled": "Այս օգտատիրոջը միացված չեն ամփոփումները, կամ համակարգի հիմնական կազմաձևված չէ ամփոփումներ ուղարկելու համար", + "username-too-short": "Մուտքանունը շատ կարճ է", + "username-too-long": "Օգտվողի անունը չափազանց երկար է", + "password-too-long": "Գաղտնաբառը չափազանց երկար է", + "reset-rate-limited": "Գաղտնաբառի վերակայման չափազանց շատ հարցումներ (դրույքաչափը սահմանափակ է)", + "reset-same-password": "Խնդրում ենք օգտագործել գաղտնաբառ, որը տարբերվում է ձեր ներկայիս գաղտնաբառից", + "user-banned": "Օգտվողը արգելված է", + "user-banned-reason": "Ներողություն, այս հաշիվն արգելվել է (պատճառը՝ %1)", + "user-banned-reason-until": "Ներողություն, այս հաշիվն արգելված է մինչև %1 (պատճառը՝ %2)", + "user-too-new": "Ներողություն, ձեզնից պահանջվում է սպասել %1 վայրկյան(եր) նախքան ձեր առաջին գրառումը կատարելը", + "blacklisted-ip": "Ներողություն, ձեր IP հասցեն արգելվել է այս համայնքում: Եթե կարծում եք, որ սա սխալ է, դիմեք ադմինիստրատորին:", + "ban-expiry-missing": "Խնդրում ենք նշել այս արգելքի ավարտի ամսաթիվը", + "no-category": "Կատեգորիա գոյություն չունի", + "no-topic": "Թեման գոյություն չունի", + "no-post": "Գրառումը գոյություն չունի", + "no-group": "Խումբը գոյություն չունի", + "no-user": "Օգտվողը գոյություն չունի", + "no-teaser": "Թիզերը գոյություն չունի", + "no-flag": "Դրոշ գոյություն չունի", + "no-privileges": "Դուք չունեք բավարար արտոնություններ այս գործողության համար:", + "category-disabled": "Կատեգորիան անջատված է", + "topic-locked": "Թեման փակված է", + "post-edit-duration-expired": "Ձեզ թույլատրվում է խմբագրել հաղորդագրությունները կիսվելուց միայն %1 վայրկյան հետո։", + "post-edit-duration-expired-minutes": "Ձեզ թույլատրվում է խմբագրել հաղորդագրությունները միայն %1 րոպե (ներ) փակցնելուց հետո", + "post-edit-duration-expired-minutes-seconds": "Ձեզ թույլատրվում է խմբագրել գրառումները միայն %1 րոպե(ներ) %2 վայրկյան(ներ) փակցնելուց հետո", + "post-edit-duration-expired-hours": "Ձեզ թույլատրվում է խմբագրել գրառումները փակցնելուց միայն %1 ժամ հետո ", + "post-edit-duration-expired-hours-minutes": "Ձեզ թույլատրվում է խմբագրել գրառումները միայն դրանք %1 ժամ(եր) %2 րոպե(ներ) հրապարակելուց հետո", + "post-edit-duration-expired-days": "Ձեզ թույլատրվում է խմբագրել գրառումները հրապարակելուց հետո միայն %1 օր(եր):", + "post-edit-duration-expired-days-hours": "Ձեզ թույլատրվում է խմբագրել հաղորդագրությունները միայն %1 օր(եր) %2 ժամ(եր) դրանք հրապարակելուց հետո", + "post-delete-duration-expired": "Ձեզ թույլատրվում է ջնջել գրառումները հրապարակելուց հետո միայն %1 վայրկյանի ընթացքում", + "post-delete-duration-expired-minutes": "Ձեզ թույլատրվում է ջնջել գրառումները միայն %1 րոպեով հրապարակելուց հետո", + "post-delete-duration-expired-minutes-seconds": "Ձեզ թույլատրվում է ջնջել գրառումները միայն %1 րոպե(ով) %2 վայրկյան(ով) հրապարակելուց հետո", + "post-delete-duration-expired-hours": "Ձեզ թույլատրվում է ջնջել գրառումները %1 ժամով միայն հրապարակելուց հետո ", + "post-delete-duration-expired-hours-minutes": "Դուք կարող եք ջնջել գրառումները միայն %1 ժամ(ով) %2 րոպե(ով) հրապարակելուց հետո", + "post-delete-duration-expired-days": "Ձեզ թույլատրվում է ջնջել գրառումները փակցնելուց հետո միայն %1 օրվա ընթացքում", + "post-delete-duration-expired-days-hours": "Դուք կարող եք ջնջել գրառումները միայն %1 օր(ով) %2 ժամ(ով) հրապարակելուց հետո", + "cant-delete-topic-has-reply": "Դուք չեք կարող ջնջել ձեր թեման պատասխան ստանալուց հետո", + "cant-delete-topic-has-replies": "Դուք չեք կարող ջնջել ձեր թեման %1 պատասխան ստանալուց հետո", + "content-too-short": "Խնդրում ենք մուտքագրել ավելի երկար գրառում: Գրառումները պետք է պարունակեն առնվազն %1 նիշ(եր):", + "content-too-long": "Խնդրում ենք մուտքագրել ավելի կարճ գրառում: Գրառումները չեն կարող ավելի երկար լինել, քան %1 նիշ(ներ):", + "title-too-short": "Խնդրում ենք մուտքագրել ավելի երկար վերնագիր: Վերնագրերը պետք է պարունակեն առնվազն %1 նիշ(ներ):", + "title-too-long": "Խնդրում ենք մուտքագրել ավելի կարճ վերնագիր: Վերնագրերը չեն կարող ավելի երկար լինել, քան %1 նիշ(ներ):", + "category-not-selected": "Կատեգորիան ընտրված չէ:", + "too-many-posts": "Դուք կարող եք գրառում անել միայն յուրաքանչյուր %1 վայրկյան(եր) մեկ անգամ. խնդրում ենք սպասել նորից գրառում անելուց առաջ", + "too-many-posts-newbie": "Որպես նոր օգտատեր, դուք կարող եք հրապարակել միայն յուրաքանչյուր %1 վայրկյան(եր) մեկ անգամ, քանի դեռ չեք վաստակել %2 վարկանիշ, խնդրում ենք սպասել՝ նորից գրառում կատարելուց առաջ:", + "already-posting": "You are already posting", + "tag-too-short": "Խնդրում ենք մուտքագրել ավելի երկար թեգ: Թեգերը պետք է պարունակեն առնվազն %1 նիշ(ներ)", + "tag-too-long": "Խնդրում ենք մուտքագրել ավելի կարճ թեգ: Թեգերը չեն կարող ավելի երկար լինել, քան %1 նիշ(ներ)", + "not-enough-tags": "Ոչ բավարար թեգեր: Թեմաները պետք է ունենան առնվազն %1 թեգ(ներ)", + "too-many-tags": "Չափազանց շատ թեգեր: Թեմաները չեն կարող ունենալ ավելի քան %1 թեգ(ներ)", + "cant-use-system-tag": "Դուք չեք կարող օգտագործել այս համակարգի պիտակը:", + "cant-remove-system-tag": "Դուք չեք կարող հեռացնել այս համակարգի թագը:", + "still-uploading": "Խնդրում ենք սպասել վերբեռնումների ավարտին:", + "file-too-big": "Ֆայլի առավելագույն թույլատրելի չափը %1 կբ է. խնդրում ենք վերբեռնել ավելի փոքր ֆայլ", + "guest-upload-disabled": "Հյուրերի վերբեռնումն անջատված է", + "cors-error": "Չհաջողվեց վերբեռնել նկարը սխալ կազմաձևված CORS-ի պատճառով", + "upload-ratelimit-reached": "Դուք միանգամից չափազանց շատ ֆայլեր եք վերբեռնել: Խնդրում ենք փորձել ավելի ուշ.", + "scheduling-to-past": "Ընտրեք ամսաթիվ ապագայում:", + "invalid-schedule-date": "Խնդրում ենք մուտքագրել վավեր ամսաթիվ և ժամ:", + "cant-pin-scheduled": "Պլանավորված թեմաները չեն կարող (ապ)ամրացվել:", + "cant-merge-scheduled": "Պլանավորված թեմաները չեն կարող միավորվել:", + "cant-move-posts-to-scheduled": "Հնարավոր չէ հաղորդագրությունները տեղափոխել պլանավորված թեմա:", + "cant-move-from-scheduled-to-existing": "Հնարավոր չէ հաղորդագրությունները տեղափոխել պլանավորված թեմայից գոյություն ունեցող թեմա:", + "already-bookmarked": "Դուք արդեն էջանշել եք այս գրառումը", + "already-unbookmarked": "Դուք արդեն հանել եք այս գրառումը", + "cant-ban-other-admins": "Դուք չեք կարող բլոկել այլ ադմինների:", + "cant-mute-other-admins": "Դուք չեք կարող անջատել այլ ադմինիստրատորների ձայնը", + "user-muted-for-hours": "Ձեր ձայնը անջատել են, դուք կկարողանաք փակցնել %1 ժամից", + "user-muted-for-minutes": "Ձեր ձայնը անջատել են, դուք կկարողանաք փակցնել %1 րոպեից", + "cant-make-banned-users-admin": "Դուք չեք կարող արգելված օգտատերերին դարձնել ադմինիստրատոր:", + "cant-remove-last-admin": "Դուք միակ ադմինն եք: Ավելացրեք մեկ այլ օգտատեր որպես ադմինիստրատոր՝ նախքան ձեզ որպես ադմինիստրատոր հեռացնելը", + "account-deletion-disabled": "Հաշվի ջնջումն անջատված է", + "cant-delete-admin": "Հեռացրեք ադմինիստրատորի իրավունքները այս հաշվից՝ նախքան այն ջնջելը:", + "already-deleting": "Արդեն ջնջվում է", + "invalid-image": "Անվավեր նկար", + "invalid-image-type": "Անվավեր տեսակի պատկեր: Թույլատրված տեսակներն են՝ %1", + "invalid-image-extension": "Պատկերի անվավեր ընդլայնում", + "invalid-file-type": "Ֆայլի անվավեր տեսակ: Թույլատրված տեսակներն են՝ %1", + "invalid-image-dimensions": "Նկարի չափսերը չափազանց մեծ են", + "group-name-too-short": "Խմբի անունը շատ կարճ է:", + "group-name-too-long": "Խմբի անունը չափազանց երկար է", + "group-already-exists": "Խումբը արդեն գոյություն ունի", + "group-name-change-not-allowed": "Խմբի անվան փոփոխությունն անթույլատրելի է", + "group-already-member": "Արդեն այս խմբի անդամ ", + "group-not-member": "Այս խմբի անդամ չէ", + "group-needs-owner": "Այս խմբին անհրաժեշտ է առնվազն մեկ սեփականատեր։", + "group-already-invited": "Այս օգտատերը արդեն հրավիրված է", + "group-already-requested": "Ձեր անդամակցության հարցումն արդեն ներկայացվել է", + "group-join-disabled": "Դուք այս պահին չեք կարող միանալ այս խմբին", + "group-leave-disabled": "Դուք այս պահին չեք կարող դուրս գալ այս խմբից", + "post-already-deleted": "Այս գրառումն արդեն ջնջված է", + "post-already-restored": "Այս գրառումն արդեն վերականգնվել է", + "topic-already-deleted": "Այս թեման արդեն ջնջված է", + "topic-already-restored": "Այս թեման արդեն վերականգնվել է", + "cant-purge-main-post": "Դուք չեք կարող մաքրել հիմնական գրառումը, փոխարենը ջնջեք թեման", + "topic-thumbnails-are-disabled": "Թեմայի մանրապատկերներն անջատված են:", + "invalid-file": "Անվավեր ֆայլ", + "uploads-are-disabled": "Վերբեռնումն անջատված է", + "signature-too-long": "Ներեցեք, ձեր ստորագրությունը չի կարող լինել ավելի քան %1 նիշ(ներ):", + "about-me-too-long": "Ներեցեք, ձեր իմ մասին չի կարող լինել ավելի քան %1 նիշ(ներ):", + "cant-chat-with-yourself": "Դուք չեք կարող զրուցել ինքներդ ձեզ հետ:", + "chat-restricted": "Նրանք պետք է հետևեն ձեզ, որպեսզի կարողանաք զրուցել նրանց հետ", + "chat-disabled": "Զրույցի համակարգն անջատված է", + "too-many-messages": "Դուք չափազանց շատ հաղորդագրություններ եք ուղարկել, խնդրում ենք սպասել մի քիչ:", + "invalid-chat-message": "Զրույցի անվավեր հաղորդագրություն", + "chat-message-too-long": "Զրույցի հաղորդագրությունները չեն կարող լինել ավելի քան %1 նիշ:", + "cant-edit-chat-message": "Ձեզ չի թույլատրվում խմբագրել այս հաղորդագրությունը", + "cant-delete-chat-message": "Ձեզ չի թույլատրվում ջնջել այս հաղորդագրությունը", + "chat-edit-duration-expired": "Ձեզ թույլատրվում է խմբագրել զրույցի հաղորդագրությունները փակցնելուց հետո միայն %1 վայրկյան", + "chat-delete-duration-expired": "Ձեզ թույլատրվում է ջնջել զրույցի հաղորդագրությունները փակցնելուց հետո միայն %1 վայրկյանի ընթացքում", + "chat-deleted-already": "Այս զրույցի հաղորդագրությունն արդեն ջնջված է", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Այս զրուցարանը գոյություն չունի:", + "already-voting-for-this-post": "Դուք արդեն քվեարկել եք այս գրառման օգտին:", + "reputation-system-disabled": "Վարկանիշի համակարգը անջատված է:", + "downvoting-disabled": "Դեմ քվեարկությունն անջատված է", + "not-enough-reputation-to-chat": "Ձեզ անհրաժեշտ է %1 վարկանիշ զրուցելու համար", + "not-enough-reputation-to-upvote": "Ձեզ անհրաժեշտ է %1 վարկանիշ՝ կողմ քվեարկելու համար", + "not-enough-reputation-to-downvote": "Դեմ քվեարկելու համար ձեզ պետք է %1 վարկանիշ", + "not-enough-reputation-to-flag": "Այս գրառումը դրոշակելու համար ձեզ պետք է %1 հեղինակություն", + "not-enough-reputation-min-rep-website": "Ձեզ անհրաժեշտ է %1 վարկանիշ՝ կայք ավելացնելու համար", + "not-enough-reputation-min-rep-aboutme": "Ինձ պետք է %1 վարկանիշ՝ իմ մասին ավելացնելու համար", + "not-enough-reputation-min-rep-signature": "Ձեզ անհրաժեշտ է %1 վարկանիշ՝ ստորագրություն ավելացնելու համար", + "not-enough-reputation-min-rep-profile-picture": "Ձեզ անհրաժեշտ է %1 վարկանիշ՝ պրոֆիլի նկար ավելացնելու համար", + "not-enough-reputation-min-rep-cover-picture": "Շապիկի նկար ավելացնելու համար պետք է %1 վարկանիշ", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "Դուք արդեն նշել եք այս օգտատիրոջը", + "post-flagged-too-many-times": "Այս գրառումն արդեն նշվել է ուրիշների կողմից", + "user-flagged-too-many-times": "Այս օգտատերն արդեն դրոշակվել է ուրիշների կողմից", + "cant-flag-privileged": "Ձեզ չի թույլատրվում նշել արտոնյալ օգտատերերի պրոֆիլները կամ բովանդակությունը (մոդերատորներ/համաշխարհային մոդերատորներ/ադմիններ)", + "self-vote": "Դուք չեք կարող քվեարկել ձեր սեփական գրառման վրա", + "too-many-upvotes-today": "Դուք կարող եք օրական միայն %1 անգամ կողմ քվեարկել", + "too-many-upvotes-today-user": "Դուք կարող եք միայն օրական %1 անգամ կողմ քվեարկել օգտատիրոջը", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "Դուք կարող եք օրական միայն %1 անգամ դեմ քվեարկել օգտատիրոջը", + "reload-failed": "NodeBB-ը վերաբեռնելիս խնդիր առաջացավ՝ «%1»: NodeBB-ն կշարունակի սպասարկել հաճախորդի կողմից առկա ակտիվները, թեև դուք պետք է չեղարկեք այն, ինչ արել եք հենց վերաբեռնումից առաջ", + "registration-error": "Գրանցման սխալ", + "parse-error": "Սերվերի պատասխանը վերլուծելիս սխալ առաջացավ", + "wrong-login-type-email": "Մուտք գործելու համար խնդրում ենք օգտագործել ձեր էլ. փոստը", + "wrong-login-type-username": "Խնդրում ենք օգտագործել ձեր օգտանունը մուտք գործելու համար", + "sso-registration-disabled": "Գրանցումն անջատված է %1 հաշիվների համար, խնդրում ենք նախ գրանցվել էլ.հասցեով", + "sso-multiple-association": "Դուք չեք կարող այս ծառայությունից մի քանի հաշիվներ կապել ձեր NodeBB հաշվի հետ: Խնդրում ենք անջատել ձեր գոյություն ունեցող հաշիվը և նորից փորձեք:", + "invite-maximum-met": "Դուք հրավիրել եք առավելագույն թվով մարդկանց (% 1 %2-ից):", + "no-session-found": "Մուտքի սեսիա չի գտնվել:", + "not-in-room": "Օգտատերը սենյակում չէ", + "cant-kick-self": "Դուք չեք կարող ձեզ հեռացնել խմբից", + "no-users-selected": "Ընտրված օգտատեր(ներ) չկա", + "invalid-home-page-route": "Գլխավոր էջի անվավեր ուղեգիծ", + "invalid-session": "Անվավեր սեսիա", + "invalid-session-text": "Կարծես թե ձեր մուտքի սեսիան այլևս ակտիվ չէ: Խնդրում ենք թարմացնել այս էջը:", + "session-mismatch": "Նիստի անհամապատասխանություն", + "session-mismatch-text": "Կարծես թե ձեր մուտքի աշխատաշրջանն այլևս չի համընկնում սերվերի հետ: Խնդրում ենք թարմացնել այս էջը:", + "no-topics-selected": "Ընտրված թեմաներ չկան:", + "cant-move-to-same-topic": "Հնարավոր չէ հաղորդագրությունը տեղափոխել նույն թեմա:", + "cant-move-topic-to-same-category": "Հնարավոր չէ թեման տեղափոխել նույն կատեգորիա:", + "cannot-block-self": "Դուք չեք կարող արգելափակել ինքներդ ձեզ:", + "cannot-block-privileged": "Դուք չեք կարող արգելափակել ադմինիստրատորներին կամ ընդհանուր մոդերատորներին", + "cannot-block-guest": "Հյուրը չի կարող արգելափակել այլ օգտատերին", + "already-blocked": "Այս օգտատերն արդեն արգելափակված է", + "already-unblocked": "Այս օգտատերն արդեն ապաարգելափակված է", + "no-connection": "Կարծես թե ինտերնետ կապի հետ կապված խնդիր կա", + "socket-reconnect-failed": "Այս պահին հնարավոր չէ միանալ սերվերին: Սեղմեք այստեղ՝ նորից փորձելու համար, կամ ավելի ուշ նորից փորձեք", + "plugin-not-whitelisted": "Հնարավոր չէ տեղադրել plugin – ACP-ի միջոցով կարող են տեղադրվել միայն NodeBB Package Manager-ի կողմից սպիտակ ցուցակում ներառված պլագինները", + "plugins-set-in-configuration": "Ձեզ չի թույլատրվում փոխել plugin-ի վիճակը, քանի որ դրանք սահմանված են գործարկման ժամանակ (config.json, շրջակա միջավայրի փոփոխականներ կամ տերմինալի արգումենտներ), փոխարենը փոխեք կազմաձևը:", + "theme-not-set-in-configuration": "Կազմաձևում ակտիվ պլագիններ սահմանելիս, թեմաները փոխելիս անհրաժեշտ է ավելացնել նոր թեման ակտիվ հավելումների ցանկում՝ նախքան այն թարմացնելը ACP-ում:", + "topic-event-unrecognized": "Թեմայի իրադարձությունը «% 1» անհայտ է", + "cant-set-child-as-parent": "Հնարավոր չէ երեխային որպես ծնողի/գլխավոր կատեգորիա սահմանել", + "cant-set-self-as-parent": "Ինքն իրեն որպես ծնողի/գլխավոր կատեգորիա չի կարող սահմանվել", + "api.master-token-no-uid": "Հիմնական նշան է ստացվել առանց համապատասխան «_uid» հարցման մարմնում", + "api.400": "Ինչ-որ բան այն չէր, որում դուք փոխանցել եք խնդրանքը:", + "api.401": "Մուտքի վավեր նիստ չի գտնվել: Խնդրում ենք մուտք գործել և նորից փորձել:", + "api.403": "Դուք իրավասու չեք կատարել այս զանգը", + "api.404": "Անվավեր API զանգ", + "api.426": "HTTPS-ն անհրաժեշտ է գրելու api-ին ուղղված հարցումների համար, խնդրում ենք նորից ուղարկել ձեր հարցումը HTTPS-ի միջոցով", + "api.429": "Դուք չափազանց շատ հարցումներ եք կատարել, խնդրում ենք փորձել ավելի ուշ", + "api.500": "Ձեր հարցումը սպասարկելիս անսպասելի սխալ է տեղի ունեցել:", + "api.501": "Ուղեգիծը, որով փորձում եք զանգահարել, դեռ չի իրականացվել, խնդրում ենք վաղը նորից փորձեք", + "api.503": "Երթուղին, որը փորձում եք զանգահարել, ներկայումս հասանելի չէ սերվերի կազմաձևման պատճառով" +} \ No newline at end of file diff --git a/public/language/hy/flags.json b/public/language/hy/flags.json new file mode 100644 index 0000000000..fb09e72956 --- /dev/null +++ b/public/language/hy/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Փուլ", + "reports": "Զեկույցներ", + "first-reported": "Առաջին զեկույցը", + "no-flags": "Դրոշներ չեն գտնվել:", + "assignee": "Հանձնարարող", + "update": "Թարմացում ", + "updated": "Updated", + "resolved": "Լուծվել է", + "target-purged": "Բովանդակությունը, որին անդրադարձել է այս դրոշը, մաքրվել է և այլևս հասանելի չէ:", + + "graph-label": "Ամենօրյա դրոշներ", + "quick-filters": "Արագ ֆիլտրներ", + "filter-active": "Դրոշների այս ցանկում կա մեկ կամ ավելի ակտիվ զտիչներ", + "filter-reset": "Հեռացնել ֆիլտրները ", + "filters": "Ֆիլտրել տարբերակները", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Դրոշի տիպ ", + "filter-type-all": "Ամբողջ կոնտենտը", + "filter-type-post": "Գրառում ", + "filter-type-user": "Օգտատեր", + "filter-state": "Փուլ", + "filter-assignee": "Assignee UID", + "filter-cid": "Կատեգորիա", + "filter-quick-mine": "Ինձ հանձնարարված է", + "filter-cid-all": "Բոլոր կատեգորիաները", + "apply-filters": "Կիրառել ֆիլտրերը", + "more-filters": "Ավելի շատ Ֆիլտրներ", + "fewer-filters": "Ավելի քիչ ֆիլտրներ", + + "quick-actions": "Արագ գործողություններ", + "flagged-user": "Նշված օգտվող", + "view-profile": "Դիտել պրոֆիլը", + "start-new-chat": "Սկսել նոր զրույց", + "go-to-target": "Դիտել դրոշի թիրախը", + "assign-to-me": "Հանձնարարել Ինձ", + "delete-post": "Ջնջել գրառումը", + "purge-post": "Մաքրել փոստը", + "restore-post": "Վերականգնել գրառումը", + "delete": "Ջնջել դրոշը", + + "user-view": "Դիտել պրոֆիլը", + "user-edit": "Խմբագրել պրոֆիլը", + + "notes": "Դրոշի նշումներ", + "add-note": "Ավելացնել նշում", + "no-notes": "Համօգտագործվող նշումներ չկան:", + "delete-note-confirm": "Իսկապե՞ս ուզում եք ջնջել այս դրոշակի նշումը:", + "delete-flag-confirm": "վստա՞հ եք, որ ուզում եք ջնջել այս դրոշը:", + "note-added": "Նշումը ավելացված է", + "note-deleted": "Նշումը ջնջված է", + "flag-deleted": "Դրոշը ջնջված է ", + + "history": "Հաշիվ & AMP; Դրոշի պատմություն", + "no-history": "Դրոշի պատմություն չկա:", + + "state-all": "Բոլոր փուլերը", + "state-open": "Նոր/ Բացել", + "state-wip": "Աշխատանքն ընթացքի մեջ է", + "state-resolved": "Լուծվել է", + "state-rejected": "Մերժված", + "no-assignee": "Նշանակված չէ", + + "sort": "Դասավորել ըստ ", + "sort-newest": "Առաջին հերթին նորագույնը", + "sort-oldest": "Նախ ամենահինը", + "sort-reports": "Զեկույցների մեծ մասը", + "sort-all": "Դրոշի բոլոր տեսակները...", + "sort-posts-only": "Միայն գրառումներ...", + "sort-downvotes": "Ամենաշատ դեմ ձայները", + "sort-upvotes": "Ամենաշատ կողմ ձայները", + "sort-replies": "Պատասխանների մեծ մասը", + + "modal-title": "Report Content", + "modal-body": "Խնդրում ենք նշել, թե ինչու եք դրոշակում %1 %2 վերանայման համար: Որպես այլընտրանք, օգտագործեք արագ հաշվետվության կոճակներից մեկը, եթե կիրառելի է:", + "modal-reason-spam": "Սպամ ", + "modal-reason-offensive": "Վիրավորական", + "modal-reason-other": "Այլ (նշեք ստորև)", + "modal-reason-custom": "Այս բովանդակության հաղորդման պատճառը...", + "modal-submit": "Ներկայացնել հաշվետվություն", + "modal-submit-success": "Կոնտենտը նշվել է չափավորություն համար:", + + "bulk-actions": "Զանգվածային գործողություններ", + "bulk-resolve": "Լուծել դրոշակ(ներ)ը", + "bulk-success": "%1 դրոշները թարմացվել են", + "flagged-timeago-readable": "Նշված (% 2)", + "auto-flagged": "[Auto Flagged] Ստացել է %1 դեմ ձայն:" +} \ No newline at end of file diff --git a/public/language/hy/global.json b/public/language/hy/global.json new file mode 100644 index 0000000000..3afb1583e3 --- /dev/null +++ b/public/language/hy/global.json @@ -0,0 +1,126 @@ +{ + "home": "Գլխավոր", + "search": "Որոնել", + "buttons.close": "փակել", + "403.title": "Մուտքն արգելված է", + "403.message": "Դուք, կարծես, պատահաբար հայտնվել եք մի էջի վրա, որը դուք մուտք չունեք:", + "403.login": "Միգուցե փորձե՞ք մուտք գործել:", + "404.title": "Գտնված չէ", + "404.message": "Կարծես թե պատահել ես մի էջի, որը գոյություն չունի։ Վերադարձ դեպի գլխավոր էջ։", + "500.title": "Ներքին սխալ.", + "500.message": "Վա՜յ Կարծես ինչ-որ բան սխալ ստացվեց։", + "400.title": "Վատ խնդրանք.", + "400.message": "Կարծես թե այս հղումը սխալ ձևավորված է, խնդրում ենք կրկնակի ստուգել և նորից փորձել: Հակառակ դեպքում վերադարձեք գլխավոր էջ:", + "register": "Գրանցվել", + "login": "Մուտք", + "please_log_in": "Խնդրում ենք մուտք գործել", + "logout": "Ելք", + "posting_restriction_info": "Գրառումները ներկայումս սահմանափակված են միայն գրանցված անդամների համար: Մուտք գործելու համար սեղմեք այստեղ:", + "welcome_back": "Բարի վերադարձ", + "you_have_successfully_logged_in": "Դուք հաջողությամբ մուտք գործեցիք", + "save_changes": "Պահպանել փոփոխությունները", + "save": "Պահպանել", + "close": "Փակել", + "pagination": "Էջադրում", + "pagination.out_of": "%1 %2-ից", + "pagination.enter_index": "Գնալ գրառման ինդեքս", + "header.admin": "Ադմին", + "header.categories": "Կատեգորիաներ", + "header.recent": "Վերջինները", + "header.unread": "Չկարդացված", + "header.tags": "Թեգեր", + "header.popular": "հայտնի", + "header.top": "Տոպ", + "header.users": "Օգտվողներ", + "header.groups": "Խմբեր", + "header.chats": "Նամակներ", + "header.notifications": "Ծանուցումներ", + "header.search": "Որոնել", + "header.profile": "Անձնական էջ ", + "header.navigation": "Նավիգացիա", + "notifications.loading": "Բեռնվում են ծանուցումները", + "chats.loading": "Բեռնվում են նամակները", + "motd.welcome": "Բարի գալուստ ֆորում՝ ապագայի քննարկումների հարթակ:", + "previouspage": "նախորդ էջ", + "nextpage": "հաջորդ էջ", + "alert.success": "Կատարված է", + "alert.error": "Սխալ", + "alert.banned": "Արգելված", + "alert.banned.message": "Ձեզ հենց նոր արգելել են, ձեր մուտքն այժմ սահմանափակված է:", + "alert.unbanned": "Չարգելված", + "alert.unbanned.message": "Ձեր արգելքը հանվել է։", + "alert.unfollow": "Դուք այլևս չեք հետևում %1 - ին:", + "alert.follow": "Դուք արդեն հետևումեք %1 - ին:", + "users": "Օգտվողներ", + "topics": "Թեմաներ", + "posts": "Գրառումներ", + "x-posts": "%1 գրառում", + "best": "Լավագույնը", + "controversial": "Հակասական", + "votes": "Ձայներ ", + "x-votes": "%1 ձայն", + "voters": "Ընտրողներ", + "upvoters": "Վերընտրողներ", + "upvoted": "Կողմ է քվեարկել", + "downvoters": "Դաունվոյթեր", + "downvoted": "Դեմ է քվեարկել", + "views": "Դիտումներ", + "posters": "Պաստառներ", + "reputation": "Վարկանիշ", + "lastpost": "Վերջին գրառում ", + "firstpost": "Առաջին գրառում", + "read_more": "Կարդալ ավելին", + "more": "Ավելին", + "none": "Ոչ մեկը", + "posted_ago_by_guest": "հրապարակված է %1 Հյուրի կողմից", + "posted_ago_by": "հրապարակված է %1 %2-ի կողմից", + "posted_ago": "Հրապարակված է %1", + "posted_in": "Հրապարակված է %1-ում", + "posted_in_by": "Հրապարակված է %1-ում և %2-ում", + "posted_in_ago": "հրապարակված է %1 %2-ում", + "posted_in_ago_by": "հրապարակված է %1 %2-ում %3-ի կողմից", + "user_posted_ago": "%1 հրապարակել է %2", + "guest_posted_ago": "Հյուրը հրապարակել է %1", + "last_edited_by": "վերջին անգամ խմբագրվել է %1-ի կողմից", + "norecentposts": "Վերջին գրառումներ չկան", + "norecenttopics": "Վերջին թեմաներ չկան", + "recentposts": "Վերջին գրառումներ", + "recentips": "Վերջերս մուտք գործած IP-ներ", + "moderator_tools": "Մոդերատորի գործիքներ", + "online": "Առցանց", + "away": "Հեռու", + "dnd": "Չանհանգստացնել", + "invisible": "Չերևացող", + "offline": "Անցանց", + "email": "էլ. փոստ", + "language": "լեզու", + "guest": "Հյուր", + "guests": "Հյուրեր", + "former_user": "Նախկին օգտվող", + "system-user": "Համակարգ", + "unknown-user": "Անհայտ օգտվող", + "updated.title": "Ֆորումը թարմացվել է", + "updated.message": "Այս ֆորումը նոր է թարմացվել վերջին տարբերակին: Սեղմեք այստեղ՝ էջը թարմացնելու համար:", + "privacy": "Գաղտնիություն", + "follow": "Հետևել", + "unfollow": "Չհետևել", + "delete_all": "Ջնջել", + "map": "Քարտեզ", + "sessions": "Մուտք գործելու սեսիաներ", + "ip_address": "IP հասցե", + "enter_page_number": "Մուտքագրեք էջի համարը", + "upload_file": "Ներբեռնել ֆայլ", + "upload": "Վերբեռնել", + "uploads": "Վերբեռնումներ", + "allowed-file-types": "Թույլատրված ֆայլերի տեսակներն են՝ %1", + "unsaved-changes": "Դուք չպահված փոփոխություններ ունեք: Վստա՞հ եք, որ ցանկանում եք հեռանալ:", + "reconnecting-message": "Կարծես թե %1-ի հետ ձեր կապը կորել է, խնդրում ենք սպասել, մինչ մենք կփորձենք նորից միանալ:", + "play": "Նվագել", + "cookies.message": "Այս կայքը օգտագործում է cookines՝ ապահովելու համար, որ դուք ստանում եք լավագույն փորձը մեր կայքում:", + "cookies.accept": "Հասկացա!", + "cookies.learn_more": "Իմացեք ավելին", + "edited": "Խմբագրված", + "disabled": "Անջատված", + "select": "Ընտրել", + "user-search-prompt": "Մուտքագրեք ինչ-որ բան այստեղ՝ օգտատերեր գտնելու համար..." +} \ No newline at end of file diff --git a/public/language/hy/groups.json b/public/language/hy/groups.json new file mode 100644 index 0000000000..19660fee0d --- /dev/null +++ b/public/language/hy/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Խմբեր", + "view_group": "Դիտել խումբը", + "owner": "Խմբի սեփականատեր", + "new_group": "Ստեղծել նոր խումբ", + "no_groups_found": "Տեսնելու խմբեր չկան", + "pending.accept": "Ընդունել ", + "pending.reject": "Մերժել", + "pending.accept_all": "Ընդունել բոլորը", + "pending.reject_all": "Մերժել բոլորին", + "pending.none": "Այս պահին սպասվող անդամներ չկան", + "invited.none": "Այս պահին հրավիրված անդամներ չկան", + "invited.uninvite": "Չեղարկել հրավերը", + "invited.search": "Փնտրեք օգտատերին այս խումբ հրավիրելու համար", + "invited.notification_title": "Դուք հրավիրվել եք միանալու %1-ին", + "request.notification_title": "Խմբի անդամակցության հարցում %1-ից", + "request.notification_text": "%1-ը խնդրել է դառնալ %2-ի անդամ", + "cover-save": "Պահպանել", + "cover-saving": "Պահպանել ", + "details.title": "Խմբի մանրամասները", + "details.members": "Անդամների ցուցակ", + "details.pending": "Սպասող Անդամներ", + "details.invited": "Հրավիրված անդամներ", + "details.has_no_posts": "Այս խմբի անդամները ոչ մի գրառում չեն արել:", + "details.latest_posts": "Վերջին գրառումները", + "details.private": "Անձնական ", + "details.disableJoinRequests": "Անջատել միանալու հարցումները", + "details.disableLeave": "Արգելել օգտատերերին դուրս գալ խմբից", + "details.grant": "Տրամադրել/վերացնել սեփականության իրավունքը", + "details.kick": "Բողոքել", + "details.kick_confirm": "Վստա՞հ եք, որ ուզում եք հեռացնել այս անդամին խմբից:", + "details.add-member": "Ավելացնել անդամ", + "details.owner_options": "Խմբի ադմինիստրացիա", + "details.group_name": "Խմբի անվանումը", + "details.member_count": "Անդամների թիվը", + "details.creation_date": "Ստեղծման ամսաթիվը", + "details.description": "Նկարագրություն", + "details.member-post-cids": "Կատեգորիայի ID-ներ, որտեղից ցուցադրվում են հաղորդագրություններ", + "details.badge_preview": "Նշանակի նախադիտում", + "details.change_icon": "Փոխել պատկերակը", + "details.change_label_colour": "Փոխել պիտակի գույնը", + "details.change_text_colour": "Փոխել տեքստի գույնը ", + "details.badge_text": "Նշանակի տեքստ", + "details.userTitleEnabled": "Ցույց տալ նշանակը", + "details.private_help": "Եթե միացված է, խմբերին միանալը պահանջում է խմբի սեփականատիրոջ թույլտվությունը", + "details.hidden": "Թաքնված", + "details.hidden_help": "Եթե միացված է, այս խումբը չի գտնվի խմբերի ցանկում, և օգտվողները պետք է ձեռքով հրավիրվեն", + "details.delete_group": "Ջնջել խումբը", + "details.private_system_help": "Մասնավոր խմբերն անջատված են համակարգի մակարդակով, այս տարբերակը ոչինչ չի անում", + "event.updated": "Խմբի մանրամասները թարմացվել են", + "event.deleted": "«% 1» խումբը ջնջվել է", + "membership.accept-invitation": "Ընդունել հրավերը", + "membership.accept.notification_title": "Դուք այժմ %1-ի անդամ եք", + "membership.invitation-pending": "Հրավեր սպասվում է ", + "membership.join-group": "Միանալ խմբին", + "membership.leave-group": "Լքել խումբը", + "membership.leave.notification_title": "%1 դուրս է եկել %2 խմբից", + "membership.reject": "Մերժել", + "new-group.group_name": "Խմբի անվանումը:", + "upload-group-cover": "Վերբեռնեք խմբի շապիկը", + "bulk-invite-instructions": "Մուտքագրեք ստորակետերով բաժանված օգտանունների ցանկը՝ այս խումբ հրավիրելու համար", + "bulk-invite": "Զանգվածային հրավեր", + "remove_group_cover_confirm": "Իսկապե՞ս ուզում եք հեռացնել շապիկի նկարը:" +} \ No newline at end of file diff --git a/public/language/hy/ip-blacklist.json b/public/language/hy/ip-blacklist.json new file mode 100644 index 0000000000..f1e7e37172 --- /dev/null +++ b/public/language/hy/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Կարգավորեք ձեր IP-ի սև ցուցակն այստեղ:", + "description": "Երբեմն օգտատերերի հաշվի արգելքը բավականաչափ զսպող չէ: Այլ դեպքերում, ֆորումի մուտքի սահմանափակումը կոնկրետ IP-ի կամ IP-ների մի շարքի համար լավագույն միջոցն է պաշտպանելու ֆորումը: Այս սցենարներում դուք կարող եք ավելացնել անհանգիստ IP հասցեներ կամ ամբողջ CIDR բլոկներ այս սև ցուցակում, և նրանց թույլ չի տրվի մուտք գործել կամ գրանցել նոր հաշիվ:", + "active-rules": "Ակտիվ կանոններ", + "validate": "Վավերացնել սև ցուցակը", + "apply": "Կիրառել սև ցուցակը", + "hints": "Syntax Hints", + "hint-1": "Սահմանեք մեկ IP հասցե յուրաքանչյուր տողի համար: Դուք կարող եք ավելացնել IP բլոկներ, քանի դեռ դրանք հետևում են CIDR ձևաչափին (օրինակ՝ 192.168.100.0/22):", + "hint-2": "Մեկնաբանություններում կարող եք ավելացնել՝ սկսած տողերից # նշանով:", + + "validate.x-valid": "%1-ը %2 կանոն(ներ)ից վավեր է:", + "validate.x-invalid": "Հետևյալ %1 կանոններն անվավեր են.", + + "alerts.applied-success": "Կիրառվել է սև ցուցակը", + + "analytics.blacklist-hourly": "Figure 11 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 21 – Blacklist hits per day", + "ip-banned": "IP-ն արգելված է" +} \ No newline at end of file diff --git a/public/language/hy/language.json b/public/language/hy/language.json new file mode 100644 index 0000000000..60c45dd7f6 --- /dev/null +++ b/public/language/hy/language.json @@ -0,0 +1,5 @@ +{ + "name": "Անգլերեն (Միացյան Թագավորություն / Կանադա)", + "code": "hy", + "dir": "Itr" +} \ No newline at end of file diff --git a/public/language/hy/login.json b/public/language/hy/login.json new file mode 100644 index 0000000000..13a80bb5a9 --- /dev/null +++ b/public/language/hy/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Օգտանուն / Էլ. փոստ", + "username": "Օգտանուն", + "remember_me": "Հիշե՞լ ինձ։", + "forgot_password": "Մոռացե՞լ եք գաղտնաբառը։", + "alternative_logins": "Մուտքի այլ եղանակներ", + "failed_login_attempt": "Մուտքը չհաջողվեց", + "login_successful": "Դուք բարեհաջող մուտք գործեցիք։", + "dont_have_account": "Չունե՞ք հաշիվ։", + "logged-out-due-to-inactivity": "Դուք դուրս եք գրվել ադմինիստրատորի ղեկավարման վահանակից՝ ակտիվություն չցուցաբերելու պատճառով", + "caps-lock-enabled": "Caps Lock-ը միացված է" +} \ No newline at end of file diff --git a/public/language/hy/modules.json b/public/language/hy/modules.json new file mode 100644 index 0000000000..eac3dc05c4 --- /dev/null +++ b/public/language/hy/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Զրուցել ", + "chat.placeholder": "Գրեք հաղորդագրություն այստեղ, տեղադրեք նկարներ, սեղմեք \"enter\" ուղարկելու համար", + "chat.scroll-up-alert": "Դուք նայում եք ավելի հին հաղորդագրություններ, սեղմեք այստեղ՝ վերջին հաղորդագրությանը գնալու/տեսնելու համար:", + "chat.send": "Ուղարկել", + "chat.no_active": "Դուք չունեք որևէ ակտիվ չաթ", + "chat.user_typing": "%1-ը գրում է...", + "chat.user_has_messaged_you": "%1-ը ձեզ հաղորդագրություն է ուղարկել:", + "chat.see_all": "Բոլոր չաթերը", + "chat.mark_all_read": "Նշել բոլորը կարդացված", + "chat.no-messages": "Խնդրում ենք ընտրել ստացող՝ զրույցի հաղորդագրության պատմությունը դիտելու համար", + "chat.no-users-in-room": "Այս սենյակում օգտվողներ չկան", + "chat.recent-chats": "Վերջին զրույցները", + "chat.contacts": "Կոնտակտներ", + "chat.message-history": "Հաղորդագրության պատմություն", + "chat.message-deleted": "Հաղորդագրությունը ջնջված է", + "chat.options": "Զրույցի ընտրանքներ", + "chat.pop-out": "Pop out զրույց", + "chat.minimize": "Փոքրացնել", + "chat.maximize": "Մեծացնել ", + "chat.seven_days": "7 օր", + "chat.thirty_days": "30 օր", + "chat.three_months": "3 ամիս", + "chat.delete_message_confirm": "Վստա՞հ եք, որ ցանկանում եք ջնջել այս հաղորդագրությունը:", + "chat.retrieving-users": "Օգտատերերի վերականգնում ", + "chat.manage-room": "Կարգավորել Զրուցասենյակը", + "chat.add-user-help": "Որոնել օգտերերին այստեղ: Ընտրվելուց հետո օգտատերը կավելացվի զրուցարանում: Նոր օգտատերը չի կարողանա տեսնել զրույցի հաղորդագրությունները, որոնք գրված են նախքան դրանք ավելացվելը խոսակցությանը: Միայն սենյակների սեփականատերերը կարող են օգտատերերին հեռացնել զրուցարաններից:", + "chat.confirm-chat-with-dnd-user": "Այս օգտվողը դրել է իր կարգավիճակը DnD (Մի խանգարեք): Դեռ ցանկանու՞մ եք զրուցել նրանց հետ:", + "chat.rename-room": "Վերանվանել սենյակը", + "chat.rename-placeholder": "Մուտքագրեք ձեր սենյակի անունը այստեղ", + "chat.rename-help": "Այստեղ սահմանված սենյակի անունը տեսանելի կլինի սենյակի բոլոր մասնակիցների համար:", + "chat.leave": "Դուրս գալ զրույցից.", + "chat.leave-prompt": "Վստա՞հ եք, որ ցանկանում եք լքել այս զրույցը:", + "chat.leave-help": "Այս զրույցից դուրս գալը ձեզ կհեռացնի այս զրույցի հետագա նամակագրությունից: Եթե ապագայում ձեզ նորից ավելացնեն, դուք չեք տեսնի զրույցի պատմություն, որը տեղի է ունեցել մինչ ձեր նորից միանալը:", + "chat.in-room": "Այս սենյակում ", + "chat.kick": "Kick", + "chat.show-ip": "Ցույց տալ IP", + "chat.owner": "Սենյակի սեփականատեր", + "chat.system.user-join": "%1-ը միացել է սենյակին", + "chat.system.user-leave": "%1 դուրս է եկել սենյակից", + "chat.system.room-rename": "%2-ը վերանվանել է այս սենյակը՝ %1", + "composer.compose": "Կազմել", + "composer.show_preview": "Ցույց տալ նախադիտումը", + "composer.hide_preview": "Թաքցնել նախադիտումը", + "composer.user_said_in": "%1-ն ասաց %2-ում:", + "composer.user_said": "%1 -ը ասաց.", + "composer.discard": "Վստա՞հ եք որ ցանկանում եք հրաժարվել այս գրառումից:", + "composer.submit_and_lock": "Ներկայացնել և փակել", + "composer.toggle_dropdown": "Փոխարկել բացվող պատուհանը", + "composer.uploading": "Վերբեռնվում է %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Շեղագիր", + "composer.formatting.list": "Ցուցակ", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Կոդ", + "composer.formatting.link": "Հղում", + "composer.formatting.picture": "Նկարի հղում", + "composer.upload-picture": "Վերբեռնել նկարը ", + "composer.upload-file": "Վերբեռնել ֆայլ ", + "composer.zen_mode": "Զեն ռեժիմ", + "composer.select_category": "Ընտրեք կատեգորիա", + "composer.textarea.placeholder": "Մուտքագրեք ձեր գրառման կոնտենտը այստեղ, քաշեք և թողեք նկարները", + "composer.schedule-for": "Պլանավորեք թեման", + "composer.schedule-date": "Ամսաթիվ", + "composer.schedule-time": "Ժամանակ", + "composer.cancel-scheduling": "Չեղարկել ժամանակացույցը", + "composer.set-schedule-date": "Սահմանել ամսաթիվը", + "bootbox.ok": "Լավ", + "bootbox.cancel": "Չեղարկել", + "bootbox.confirm": "Հաստատել", + "bootbox.submit": "Հաստատել ", + "bootbox.send": "Ուղարկել ", + "cover.dragging_title": "Շապիկի լուսանկարի դիրքավորում", + "cover.dragging_message": "Քաշեք շապիկի լուսանկարը ցանկալի դիրքի վրա և սեղմեք «Պահպանել»", + "cover.saved": "Շապիկի լուսանկարի պատկերը և դիրքը պահպանված են", + "thumbs.modal.title": "Կառավարեք թեմայի մանրապատկերները", + "thumbs.modal.no-thumbs": "Մանրապատկերներ չեն գտնվել:", + "thumbs.modal.resize-note": "Նշում. Այս ֆորումը կազմաձևված է թեմայի մանրապատկերների չափը մինչև x առավելագույն լայնությունը փոխելու համար", + "thumbs.modal.add": "Ավելացնել մանրապատկեր", + "thumbs.modal.remove": "Հեռացնել մանրապատկերը", + "thumbs.modal.confirm-remove": "Վստա՞հ եք, որ ուզում եք հեռացնել այս մանրապատկերը:" +} \ No newline at end of file diff --git a/public/language/hy/notifications.json b/public/language/hy/notifications.json new file mode 100644 index 0000000000..d1eda2e55f --- /dev/null +++ b/public/language/hy/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Ծանուցումներ", + "no_notifs": "Նոր ծանուցումներ չկան", + "see_all": "Բոլոր ծանուցումները", + "mark_all_read": "Նշել բոլորը կարդացված", + "back_to_home": "Վերադառնալ %1 - ին", + "outgoing_link": "Ելքային հղում", + "outgoing_link_message": "Դուք հիմա հեռանում եք %1-ից", + "continue_to": "Շարունակեք դեպի %1", + "return_to": "Վերադառնալ %1 - ին", + "new_notification": "Դուք ունեք նոր ծանուցում", + "you_have_unread_notifications": "Դուք չկարդացված ծանուցումներ ունեք:", + "all": "Բոլորը", + "topics": "Թեմաներ", + "replies": "Պատասխաններ", + "chat": "Զրույցներ", + "group-chat": "Խմբային զրույցներ", + "follows": "Հետևորդներ", + "upvote": "Կողմ ձայներ", + "new-flags": "Նոր դրոշներ ", + "my-flags": "Ինձ հանձնարարված դրոշներ", + "bans": "Արգելքներ", + "new_message_from": "Նոր հաղորդագրություն %1-ից", + "upvoted_your_post_in": "%1-ը դրական է քվեարկել ձեր գրառմանը %2-ում:", + "upvoted_your_post_in_dual": "%1-ը և %2-ը դրական են քվեարկել ձեր գրառմանը %3-ում:", + "upvoted_your_post_in_multiple": "%1 և %2 ուրիշներ կողմ են քվեարկել ձեր գրառմանը %3-ում:", + "moved_your_post": "%1-ը ձեր գրառումը տեղափոխել է %2", + "moved_your_topic": "%1-ը տեղափոխվել է %2", + "user_flagged_post_in": "% 1 դրոշակավորել է գրառումը %2-ում", + "user_flagged_post_in_dual": "%1-ը և %2-ը դրոշակեցին գրառումը %3-ում", + "user_flagged_post_in_multiple": "%1-ը և %2-ը ևս դրոշակել են գրառումը %3-ում", + "user_flagged_user": "%1-ը դրոշակեց օգտվողի պրոֆիլը (% 2)", + "user_flagged_user_dual": "%1-ը և %2-ը դրոշակել են օգտվողի պրոֆիլը (%3)", + "user_flagged_user_multiple": "%1-ը և ևս %2-ը նշել են օգտվողի պրոֆիլը (%3)", + "user_posted_to": "%1-ը պատասխանել է %2-ին", + "user_posted_to_dual": "%1-ը և %2-ը հրապարակել են պատասխաններ %3-ին", + "user_posted_to_multiple": "%1 և %2 ուրիշներ հրապարակել են պատասխաններ %3-ին", + "user_posted_topic": "%1-ը նոր թեմա է տեղադրել՝ %2", + "user_edited_post": "%1-ը խմբագրել է գրառում %2-ում", + "user_started_following_you": "%1 սկսեց հետևել ձեզ", + "user_started_following_you_dual": "%1 և %2 սկսեցին հետևել ձեզ:", + "user_started_following_you_multiple": "%1 և %2 ուրիշներ սկսեցին հետևել ձեզ:", + "new_register": "%1 գրանցման հարցում ուղարկեց:", + "new_register_multiple": "Կան %1 գրանցման հարցումներ, որոնք սպասում են վերանայմանը:", + "flag_assigned_to_you": "Դրոշ % 1 նշանակվել է ձեզ", + "post_awaiting_review": "Գրառումը սպասում է վերանայման", + "profile-exported": "%1 պրոֆիլն արտահանվեց, սեղմեք ներբեռնելու համար", + "posts-exported": "%1 գրառում արտահանվեց, սեղմեք ներբեռնելու համար", + "uploads-exported": "%1 վերբեռնումներ արտահանվեցին, սեղմեք ներբեռնելու համար", + "users-csv-exported": "Օգտատերերի csv-ն արտահանվել է, սեղմեք ներբեռնելու համար", + "post-queue-accepted": "Ձեր հերթագրված գրառումն ընդունվել է: Սեղմեք այստեղ՝ ձեր գրառումը տեսնելու համար:", + "post-queue-rejected": "Ձեր հերթագրված գրառումը մերժվել է:", + "post-queue-notify": "Հերթագրված գրառումը ստացել է ծանուցում. «% 1»", + "email-confirmed": "Էլփոստը հաստատված է", + "email-confirmed-message": "Շնորհակալություն ձեր էլփոստը հաստատելու համար: Ձեր հաշիվն այժմ ամբողջությամբ ակտիվացված է:", + "email-confirm-error-message": "Ձեր էլփոստի հասցեն վավերացնելիս խնդիր առաջացավ: Հավանաբար կոդը անվավեր է կամ ժամկետանց է:", + "email-confirm-sent": "Հաստատման էլփոստը ուղարկվել է", + "none": "None", + "notification_only": "Միայն ծանուցում", + "email_only": "Միայն էլ.նամակ", + "notification_and_email": "Ծանուցում և էլ.նամակ", + "notificationType_upvote": "Երբ ինչ-որ մեկը կողմ է քվեարկում ձեր գրառմանը", + "notificationType_new-topic": "Երբ մեկը, ում հետևում եք, թեմա է հրապարակում", + "notificationType_new-reply": "Երբ ձեր դիտած թեմայում տեղադրվում է նոր պատասխան", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "Երբ ինչ-որ մեկը սկսում է հետևել քեզ", + "notificationType_new-chat": "Երբ դուք ստանում եք զրույցի հաղորդագրություն", + "notificationType_new-group-chat": "Երբ դուք ստանում եք խմբային զրույցի հաղորդագրություն", + "notificationType_group-invite": "Երբ դուք ստանում եք խմբի հրավեր", + "notificationType_group-leave": "Երբ օգտատերը լքում է ձեր խումբը", + "notificationType_group-request-membership": "Երբ ինչ-որ մեկը խնդրում է միանալ ձեզ պատկանող խմբին", + "notificationType_new-register": "Երբ ինչ-որ մեկը ավելանում է գրանցման հերթում", + "notificationType_post-queue": "Երբ նոր գրառումը հերթագրվում է", + "notificationType_new-post-flag": "Երբ գրառումը դրոշակված է", + "notificationType_new-user-flag": "Երբ օգտվողը դրոշակված է" +} \ No newline at end of file diff --git a/public/language/hy/pages.json b/public/language/hy/pages.json new file mode 100644 index 0000000000..42bdead4d2 --- /dev/null +++ b/public/language/hy/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Գլխավոր", + "unread": "Չընթերցված թեմաներ", + "popular-day": "Այսօրվա հանրաճանաչ թեմաներ", + "popular-week": "Այս շաբաթվա հանրաճանաչ թեմաներ", + "popular-month": "Այս ամսվա հանրաճանաչ թեմաներ", + "popular-alltime": "Բոլոր ժամանակների հանրաճանաչ թեմաները", + "recent": "Վերջին թեմաներ", + "top-day": "Այսօրվա ամենաշատ քվեարկված թեմաները", + "top-week": "Այս շաբաթվա լավագույն քվեարկված թեմաները", + "top-month": "Այս ամսվա ամենաշատ քվեարկված թեմաները", + "top-alltime": "Լավագույն քվեարկված թեմաները", + "moderator-tools": "Մոդերատորի գործիքներ", + "flagged-content": "Դրոշված կոնտենտ", + "ip-blacklist": "IP սև ցուցակ", + "post-queue": "Գրառման Queue", + "users/online": "Առցանց օգտատերեր", + "users/latest": "Ամենավերջին օգտատերերը", + "users/sort-posts": "Ամենաշատ գրառումներով օգտատերերը", + "users/sort-reputation": "Առավել վարկանիշ ունեցող օգտատերեր", + "users/banned": "Արգելված օգտատերեր", + "users/most-flags": "Դրոշակված օգտատերերի մեծ մասը", + "users/search": "Օգտատիրոջ որոնում", + "notifications": "Ծանուցումներ", + "tags": "Պիտակներ", + "tag": "Թեմաներ, որոնք պիտակված են «%1»", + "register": "Գրանցեք հաշիվ", + "registration-complete": "Գրանցումն ավարտված է", + "login": "Մուտք գործեք ձեր հաշիվ", + "reset": "Վերականգնել ձեր հաշվի գաղտնաբառը", + "categories": "Կատեգորիաներ", + "groups": "Խմբեր", + "group": "%1 խումբ", + "chats": "Զրույցներ", + "chat": "Զրույց %1-ի հետ", + "flags": "Դրոշներ ", + "flag-details": "Flag %1 Details", + "account/edit": "«% 1»-ի խմբագրում", + "account/edit/password": "«% 1»-ի գաղտնաբառի խմբագրում", + "account/edit/username": "«% 1»-ի օգտանունը խմբագրվում է", + "account/edit/email": "«% 1»-ի էլփոստի խմբագրում", + "account/info": "Հաշվի տեղեկատվություն", + "account/following": "Մարդիկ, ում % 1 հետևում է ", + "account/followers": "%1-ին հետևող մարդիկ", + "account/posts": "%1-ի կողմից արված գրառումները", + "account/latest-posts": "%1-ի կողմից արված վերջին գրառումները", + "account/topics": "%1-ի կողմից ստեղծված թեմաներ", + "account/groups": "%1-ի Խմբեր", + "account/watched_categories": "%1's Դիտված կատեգորիաներ", + "account/bookmarks": "%1-ի էջանշված գրառումները", + "account/settings": "Օգտատիրոջ կարգավորումներ", + "account/watched": "Թեմաներ, որոնք դիտել է %1-ը", + "account/ignored": "%1-ի կողմից անտեսված թեմաներ", + "account/upvoted": "%1-ի կողմից քվեարկված գրառումները", + "account/downvoted": "%1-ի կողմից դեմ քվեարկված գրառումները", + "account/best": "%1-ի կողմից արված լավագույն գրառումները", + "account/controversial": "%1-ի կողմից արված հակասական գրառումներ", + "account/blocks": "Արգելափակված օգտվողներ %1-ի համար", + "account/uploads": "Վերբեռնումներ % 1-ով", + "account/sessions": "Մուտք գործելու սեանս", + "confirm": "Էլ. փոստը հաստատված է", + "maintenance.text": "%1-ը ներկայումս սպասարկում է անցնում: Խնդրում եմ վերադարձեք մեկ այլ անգամ:", + "maintenance.messageIntro": "Ի հավելումն, ադմինիստրատորը լքել է այս հաղորդագրությունը", + "throttled.text": "%1-ը ներկայումս անհասանելի է չափազանց ծանրաբեռնվածության պատճառով: Խնդրում ենք վերադարձեք մեկ այլ անգամ:" +} \ No newline at end of file diff --git a/public/language/hy/post-queue.json b/public/language/hy/post-queue.json new file mode 100644 index 0000000000..d6ef615a22 --- /dev/null +++ b/public/language/hy/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Գրառումների հերթ", + "description": "Գրառումների հերթում գրառումներ չկան: Այս գործառույթը միացնելու համար անցեք Կարգավորումներ → Գրառում → Post Queue և միացրեք Post Queue:", + "user": "Օգտատեր", + "category": "Կատեգորիա", + "title": "Կոչում", + "content": "Կոնտենտ", + "posted": "Հրապարակված", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Սեղմեք կատեգորիայի վրա՝ խմբագրելու համար", + "title-editable": "Սեղմեք վերնագրի վրա՝ խմբագրելու համար", + "reply": "Պատասխանել ", + "topic": "Թեմա", + "accept": "Ընդունել ", + "reject": "Մերժել ", + "remove": "Հեռացնել ", + "notify": "Տեղեկացնել", + "notify-user": "Տեղեկացնել օգտատիրոջը", + "confirm-reject": "Ցանկանու՞մ եք մերժել այս գրառումը:", + "bulk-actions": "Զանգվածային գործողություններ", + "accept-all": "Ընդունել բոլորը ", + "accept-selected": "Ընդունել ընտրվածը", + "reject-all": "Մերջել բոլորը ", + "reject-all-confirm": "Ցանկանու՞մ եք մերժել բոլոր գրառումները:", + "reject-selected": "Մերժել ընտրվածը", + "reject-selected-confirm": "Ցանկանու՞մ եք մերժել %1 ընտրված գրառումները:", + "bulk-accept-success": "Ընդունված է %1 գրառում", + "bulk-reject-success": "%1 գրառում մերժվել է" +} \ No newline at end of file diff --git a/public/language/hy/recent.json b/public/language/hy/recent.json new file mode 100644 index 0000000000..7b8d213034 --- /dev/null +++ b/public/language/hy/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Վերջինները", + "day": "օր", + "week": "շաբաթ", + "month": "ամիս", + "year": "Տարի", + "alltime": "Ամբողջ ժամանակ", + "no_recent_topics": "Վերջին թեմաներ չկան։", + "no_popular_topics": "Հանրաճանաչ թեմաներ չկան։", + "there-is-a-new-topic": "Առկա է նոր թեմա", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "Կա նոր թեմա և %1 նոր գրառում:", + "there-are-new-topics": "Առկա են %1 նոր թեմաներ:", + "there-are-new-topics-and-a-new-post": "Առկա են %1 նոր թեմաներ և նոր գրառում:", + "there-are-new-topics-and-new-posts": "Առկա են %1 նոր թեմաներ և %2 նոր գրառումներ:", + "there-is-a-new-post": "Առկա է նոր գրառում։", + "there-are-new-posts": "Առկա են %1 նոր գրառումներ:", + "click-here-to-reload": "Սեղմեք այստեղ՝ վերաբեռնելու համար:" +} \ No newline at end of file diff --git a/public/language/hy/register.json b/public/language/hy/register.json new file mode 100644 index 0000000000..7a15f462d5 --- /dev/null +++ b/public/language/hy/register.json @@ -0,0 +1,32 @@ +{ + "register": "Գրանցվել ", + "cancel_registration": "Չեղարկել գրանցումը", + "help.email": "Ձեր էլեկտրոնային փոստը չի արտացոլվի ուրիշներին", + "help.username_restrictions": "Եզակի օգտվողի անուն %1-ից %2 նիշերի միջև: Մյուսները կարող են ձեզ նշել @username-ով:", + "help.minimum_password_length": "Ձեր գաղտնաբառի երկարությունը առնվազն պետք է լինի %1 նշան", + "email_address": "Էլեկտրոնային հասցե", + "email_address_placeholder": "Մուտքագրեք էլեկտրոնային փոստի հասցեն", + "username": "Մուտքանուն", + "username_placeholder": "Մուտքագրեք մուտքանունը", + "password": "գաղտնաբառ", + "password_placeholder": "Մուտքագրեք գաղտնաբառը", + "confirm_password": "Հաստատել գաղտնաբառը ", + "confirm_password_placeholder": "Հաստատել գաղտնաբառը ", + "register_now_button": "Գրանցվել հիմա ", + "alternative_registration": "Գրանցման այլ տարբերակ", + "terms_of_use": "Օգտվելու կանոններ", + "agree_to_terms_of_use": "Ես համաձայն եմ Օգտագործման պայմաններին", + "terms_of_use_error": "Դուք պետք է համաձայնեք Օգտագործման պայմաններին", + "registration-added-to-queue": "Ձեր գրանցումն ավելացվել է հաստատման հերթում: Դուք էլ.նամակ կստանաք, երբ այն ընդունվի ադմինիստրատորի կողմից:", + "registration-queue-average-time": "Անդամակցությունները հաստատելու մեր միջին ժամանակը %1 ժամ %2 րոպե է:", + "registration-queue-auto-approve-time": "Ձեր անդամակցությունն այս ֆորումին ամբողջությամբ կակտիվանա մինչև %1 ժամից:", + "interstitial.intro": "Մենք լրացուցիչ տեղեկություններ ենք ուզում՝ ձեր հաշիվը թարմացնելու համար …", + "interstitial.intro-new": "Մենք լրացուցիչ տեղեկություններ ենք ուզում՝ նախքան ձեր հաշիվը ստեղծելը…", + "interstitial.errors-found": "Խնդրում ենք վերանայել մուտքագրված տվյալները.", + "gdpr_agree_data": "Ես համաձայնում եմ այս կայքում իմ անձնական տեղեկատվության հավաքագրմանը և մշակմանը:", + "gdpr_agree_email": "Ես համաձայն եմ ստանալ ամփոփագիր և ծանուցման նամակներ այս կայքից:", + "gdpr_consent_denied": "Դուք պետք է համաձայնություն տաք այս կայքին ձեր տեղեկությունները հավաքելու/մշակելու և ձեզ էլ-նամակներ ուղարկելու համար:", + "invite.error-admin-only": "Օգտատիրոջ ուղղակի գրանցումն անջատված է: Լրացուցիչ մանրամասների համար խնդրում ենք կապվել ադմինիստրատորի հետ:", + "invite.error-invite-only": "Օգտատիրոջ ուղղակի գրանցումն անջատված է: Այս ֆորում մուտք գործելու համար դուք պետք է հրավիրված լինեք գոյություն ունեցող օգտվողի կողմից:", + "invite.error-invalid-data": "Ստացված գրանցման տվյալները չեն համապատասխանում մեր գրառումներին: Լրացուցիչ մանրամասների համար խնդրում ենք կապվել ադմինիստրատորի հետ" +} \ No newline at end of file diff --git a/public/language/hy/reset_password.json b/public/language/hy/reset_password.json new file mode 100644 index 0000000000..d4fdc527a5 --- /dev/null +++ b/public/language/hy/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Վերականգնել գաղտնաբառը", + "update_password": "Թարմացնել գաղտնաբառը", + "password_changed.title": "Գաղտնաբառը փոխվել է", + "password_changed.message": "

Գաղտնաբառը հաջողությամբ վերականգնվել է, խնդրում ենք կրկին մուտք գործել", + "wrong_reset_code.title": "Սխալ վերակայման կոդը", + "wrong_reset_code.message": "Ստացված վերակայման կոդը սխալ էր: Խնդրում ենք կրկին փորձել կամ պահանջել վերակայման նոր կոդ:", + "new_password": "նոր գաղտնաբառ", + "repeat_password": "հաստատել գաղտնաբառ", + "changing_password": "Գաղտնաբառի փոփոխություն", + "enter_email": "Խնդրում ենք մուտքագրել ձեր էլ. փոստը և մենք ձեզ էլ. փոստ կուղարկենք՝ ձեր հաշիվը վերականգնելու հրահանգներով:", + "enter_email_address": "Մուտքագրեք էլեկտրոնային հասցեն", + "password_reset_sent": "Եթե ​​նշված էլ. փոստը համապատասխանում է գոյություն ունեցող օգտվողի հաշվին, ապա ուղարկվել է գաղտնաբառ վերակայման էլ. փոստը: Խնդրում ենք նկատի ունենալ, որ 1 րոպեում կուղարկվի միայն մեկ նամակ:", + "invalid_email": "Անվավեր էլ. փոստ / էլ. փոստ գոյություն չունի:", + "password_too_short": "Մուտքագրված գաղտնաբառը չափազանց կարճ է, խնդրում ենք ընտրել այլ գաղտնաբառ:", + "passwords_do_not_match": "Ձեր մուտքագրած երկու գաղտնաբառերը չեն համընկնում:", + "password_expired": "Ձեր գաղտնաբառը սպառվել է, խնդրում ենք ընտրել նոր գաղտնաբառ" +} \ No newline at end of file diff --git a/public/language/hy/search.json b/public/language/hy/search.json new file mode 100644 index 0000000000..1b3b664da1 --- /dev/null +++ b/public/language/hy/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "Համընկնումներ չեն գտնվել", + "advanced-search": "Ընդլայնված որոնում", + "in": "Մեջ", + "titles": "Վերնագրեր", + "titles-posts": "Վերնագրեր", + "match-words": "Համապատասխանեցրեք բառերը", + "all": "Բոլորը", + "any": "Ցանկացած", + "posted-by": "Տեղադրվել է", + "in-categories": "Կատեգորիաներում", + "search-child-categories": "Որոնել դուստր կատեգորիաներ", + "has-tags": "Հաշթեգներ", + "reply-count": "Պատասխանների քանակը", + "at-least": "Գոնե", + "at-most": "Առավելագույնը", + "relevance": "Relevance", + "post-time": "Գրառման ժամանակը", + "votes": "Ձայներ", + "newer-than": "Ավելի նոր քան", + "older-than": "Ավելի հին քան", + "any-date": "Ցանկացած ամսաթիվ", + "yesterday": "Երեկ", + "one-week": "Մեկ շաբաթ", + "two-weeks": "Երկու շաբաթ", + "one-month": "Մեկ ամիս ", + "three-months": "Երեք ամիս ", + "six-months": "Վեց ամիս ", + "one-year": "Մեկ տարի ", + "sort-by": "Դասավորել ըստ", + "last-reply-time": "Վերջին պատասխանի ժամանակը", + "topic-title": "Թեմայի վերնագիր", + "topic-votes": "Թեմայի քվեարկություններ", + "number-of-replies": "Պատասխանների քանակը", + "number-of-views": "Դիտումների քանակը", + "topic-start-date": "Թեմայի մեկնարկի ամսաթիվը", + "username": "Օգտատիրոջ անունը", + "category": "Կատեգորիա", + "descending": "Նվազման կարգով", + "ascending": "Աճման կարգով", + "save-preferences": "Պահպանել նախապատվությունները", + "clear-preferences": "Մաքրել նախապատվությունները", + "search-preferences-saved": "Որոնման նախապատվությունները պահպանված են", + "search-preferences-cleared": "Որոնման նախապատվությունները ջնջվեցին", + "show-results-as": "Ցույց տալ արդյունքները որպես", + "see-more-results": "Տեսնել ավելի շատ արդյունքներ (% 1)", + "search-in-category": "Որոնել «% 1»-ում" +} \ No newline at end of file diff --git a/public/language/hy/success.json b/public/language/hy/success.json new file mode 100644 index 0000000000..75fdbb4eaa --- /dev/null +++ b/public/language/hy/success.json @@ -0,0 +1,7 @@ +{ + "success": "Հաջողություն", + "topic-post": "Դուք հաջողությամբ հրապարակել եք:", + "post-queued": "Ձեր գրառումը հերթագրված է հաստատման համար: Դուք ծանուցում կստանաք, երբ այն ընդունվի կամ մերժվի:", + "authentication-successful": "Նույնականացումը հաջողվեց", + "settings-saved": "Կարգավորումները պահված են:" +} \ No newline at end of file diff --git a/public/language/hy/tags.json b/public/language/hy/tags.json new file mode 100644 index 0000000000..1b0f98f8ff --- /dev/null +++ b/public/language/hy/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Այս թեգով թեմաներ չկան", + "tags": "Թեգեր", + "enter_tags_here": "Մուտքագրեք թեգերն այստեղ՝ %1 և %2 քանակությամբ նշանների միջակայքում", + "enter_tags_here_short": "Մուտքագրեք թեգերը...", + "no_tags": "Դեռևս թեգեր չկան", + "select_tags": "Ընտրել թեգեր" +} \ No newline at end of file diff --git a/public/language/hy/top.json b/public/language/hy/top.json new file mode 100644 index 0000000000..6b3ceb00f8 --- /dev/null +++ b/public/language/hy/top.json @@ -0,0 +1,4 @@ +{ + "title": "Տոպ", + "no_top_topics": "Լավագույն թեմաներ չկան" +} \ No newline at end of file diff --git a/public/language/hy/topic.json b/public/language/hy/topic.json new file mode 100644 index 0000000000..500fc029f9 --- /dev/null +++ b/public/language/hy/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Թեմա", + "title": "Վերնագիր", + "no_topics_found": "Թեմաներ չեն գտնվել։", + "no_posts_found": "Գրառումներ չեն գտնվել։", + "post_is_deleted": "Այս գրառումը ջնջված է։", + "topic_is_deleted": "Այս թեման ջնջված է։", + "profile": "Անձնական էջ", + "posted_by": "Գրառող՝ %1", + "posted_by_guest": "Գրառող՝ Հյուր", + "chat": "Չաթ", + "notify_me": "Տեղեկացեք այս թեմայում նոր պատասխանների մասին", + "quote": "Մեջբերել", + "reply": "Պատասխանել", + "replies_to_this_post": "%1 Պատասխաններ", + "one_reply_to_this_post": "1 Պատասխան", + "last_reply_time": "Վերջին պատասխանը", + "reply-as-topic": "Պատասխանել թեմայի տեսքով", + "guest-login-reply": "Մուտք գործեք պատասխանելու համար", + "login-to-view": "🔒 Դիտելու համար մուտք գործեք", + "edit": "Խմբագրել", + "delete": "Ջնջել", + "delete-event": "Ջնջել իրադարձությունը ", + "delete-event-confirm": "Վստա՞հ եք, որ ուզում եք ջնջել այս իրադարձությունը։", + "purge": "Մաքրել", + "restore": "Վերականգնել", + "move": "Տեղափոխել", + "change-owner": "Փոխել սեփականատիրոջը", + "fork": "Մասնատել", + "link": "Հղում", + "share": "Կիսվել", + "tools": "Գործիքներ", + "locked": "Բլոկավորված", + "pinned": "Ամրացված", + "pinned-with-expiry": "Ամրացված է մինչև %1", + "scheduled": "Պլանավորված", + "moved": "Տեղափոխվել է", + "moved-from": "Տեղափոխվել է %1-ից", + "copy-ip": "Պատճենել IP", + "ban-ip": "Արգելել IP-ն", + "view-history": "Խմբագրել պատմությունը", + "locked-by": "Փակված է", + "unlocked-by": "Ապակողպված է", + "pinned-by": "Ամրացված է", + "unpinned-by": "Ապաամրացված է", + "deleted-by": "Ջնջվել է", + "restored-by": "Վերականգնվել է", + "moved-from-by": "Moved from %1 by", + "queued-by": "Հաղորդագրությունը հերթագրվել է հաստատման համար →", + "backlink": "Հղում է", + "forked-by": "Ճեղքված", + "bookmark_instructions": "Սեղմեք այստեղ՝ այս թեմայի վերջին ընթերցված գրառմանը վերադառնալու համար:", + "flag-post": "Դրոշակել այց գրառումը ", + "flag-user": "Դրոշակել այս օգտատերին ", + "already-flagged": "Արդեն դրոշշված", + "view-flag-report": "Դիտել դրոշի հաշվետվությունը", + "resolve-flag": "Լուծել դրոշը", + "merged_message": "Այս թեման միավորվել է %2-ում", + "deleted_message": "Այս թեման ջնջվել է։ Այն կարող են տեսնել միայն թեմաների կառավարման արտոնություններ ունեցող օգտվողները:", + "following_topic.message": "Այժմ դուք ծանուցումներ կստանաք, երբ ինչ-որ մեկը գրառում անի այս թեմայում:", + "not_following_topic.message": "Դուք կտեսնեք այս թեման չընթերցված թեմաների ցանկում, բայց ծանուցումներ չեք ստանա, երբ ինչ-որ մեկը գրառում է անում այս թեմայում:", + "ignoring_topic.message": "Դուք այլևս չեք տեսնի այս թեման չկարդացված թեմաների ցանկում: Դուք կտեղեկացվեք, երբ ձեզ հիշատակեն կամ ձեր գրառումը քվեարկվի:", + "login_to_subscribe": "Խնդրում ենք գրանցվել կամ մուտք գործել՝ այս թեմային բաժանորդագրվելու համար:", + "markAsUnreadForAll.success": "Թեման նշված է որպես չկարդացված բոլորի համար:", + "mark_unread": "Նշել որպես չընթերցված", + "mark_unread.success": "Թեման նշվեց որպես չընթերցված", + "watch": "Դիտել", + "unwatch": "Չդիտել", + "watch.title": "Տեղեկացեք այս թեմայի նոր պատասխանների մասին", + "unwatch.title": "Դադարեք դիտել այս թեման", + "share_this_post": "Տարածեք այս գրառումը", + "watching": "Դիտում", + "not-watching": "Չեն դիտում", + "ignoring": "Անտեսել ", + "watching.description": "Տեղեկացրեք ինձ նոր պատասխանների մասին: Ցուցադրել չընթերցված թեման:", + "not-watching.description": "Ինձ մի տեղեկացրեք նոր պատասխանների մասին: Ցուցադրեք թեման չկարդացված վիճակում, եթե կատեգորիան անտեսված չէ:", + "ignoring.description": "Ինձ մի տեղեկացրեք նոր պատասխանների մասին: Մի ցուցադրեք թեման չկարդացված վիճակում:", + "thread_tools.title": "Թեմայի գործիքներ", + "thread_tools.markAsUnreadForAll": "Նշել չկարդացված բոլորի համար", + "thread_tools.pin": "Ամրացնել թեման", + "thread_tools.unpin": "Արձակել թեման", + "thread_tools.lock": "Փակել թեման", + "thread_tools.unlock": "Վերաբացել թեման", + "thread_tools.move": "Տեղափոխել թեման", + "thread_tools.move-posts": "Տեղափոխել գրառումները", + "thread_tools.move_all": "Տեղափոխել բոլորը", + "thread_tools.change_owner": "Փոխել սեփականատիրոջը", + "thread_tools.select_category": "Ընտրել կատեգորիա", + "thread_tools.fork": "Մասնատել թեման", + "thread_tools.delete": "Ջնջել թեման", + "thread_tools.delete-posts": "Ջնջել գրառումները", + "thread_tools.delete_confirm": "Վստա՞հ եք, որ ուզում եք ջնջել այս թեման։", + "thread_tools.restore": "Վերականգնել թեման", + "thread_tools.restore_confirm": "Վստա՞հ եք, որ ուզում եք վերականգնել այս թեման։", + "thread_tools.purge": "Մաքրել թեման", + "thread_tools.purge_confirm": "Վստա՞հ եք, որ ցանկանում եք մաքրել այս թեման:", + "thread_tools.merge_topics": "Միավորել թեմաները", + "thread_tools.merge": "Միավորել ", + "topic_move_success": "Այս թեման շուտով կտեղափոխվի «%1»: Սեղմեք այստեղ՝ հետարկելու համար:", + "topic_move_multiple_success": "Այս թեմաները շուտով կտեղափոխվեն «% 1»: Սեղմեք այստեղ՝ հետարկելու համար:", + "topic_move_all_success": "Բոլոր թեմաները շուտով կտեղափոխվեն «% 1»: Սեղմեք այստեղ՝ հետարկելու համար:", + "topic_move_undone": "Թեմայի տեղափոխումը չեղարկվեց", + "topic_move_posts_success": "Գրառումները շուտով կտեղափոխվեն։ Սեղմեք այստեղ՝ հետարկելու համար:", + "topic_move_posts_undone": "Գրառման տեղափոխումը չեղարկվեց", + "post_delete_confirm": "Վստա՞հ եք, որ ուզում եք ջնջել այս գրառումը։", + "post_restore_confirm": "Վստա՞հ եք, որ ուզում եք վերականգնել այս գրառումը։", + "post_purge_confirm": "Վստա՞հ եք, որ ցանկանում եք մաքրել այս գրառումը:", + "pin-modal-expiry": "Սպառման ամսաթիվ", + "pin-modal-help": "Դուք կարող եք ըստ ցանկության սահմանել ամրացված թեմայի (թեմայի) պիտանելիության ժամկետը այստեղ: Որպես այլընտրանք, դուք կարող եք թողնել այս դաշտը դատարկ, որպեսզի թեման մնա ամրացված, մինչև այն ձեռքով չապամրացվի:", + "load_categories": "Կատեգորիաների բեռնում", + "confirm_move": "Տեղափոխել", + "confirm_fork": "Մասնատել", + "bookmark": "Էջանիշ", + "bookmarks": "Էջանիշեր", + "bookmarks.has_no_bookmarks": "Դուք դեռ ոչ մի գրառում չեք էջանշել:", + "copy-permalink": "Պատճենել մշտական հղումը", + "loading_more_posts": "Լրացուցիչ գրառումների բեռնում", + "move_topic": "Տեղափոխել թեման", + "move_topics": "Տեղափոխել թեմաները", + "move_post": "Տեղափոխել գրառումը", + "post_moved": "Գրառումը տեղափոխված է։", + "fork_topic": "Մասնատել թեման", + "enter-new-topic-title": "Մուտքագրեք նոր թեմայի վերնագիր", + "fork_topic_instruction": "Ընտրեք այն գրառումները, որոնք ցանկանում եք մասնատել", + "fork_no_pids": "Ընտրված գրառումներ չկան:", + "no-posts-selected": "Ընտրված գրառումներ չկան:", + "x-posts-selected": "Ընտրված է %1 գրառում(ներ):", + "x-posts-will-be-moved-to-y": "%1 գրառում(ներ) կտեղափոխվի «%2»", + "fork_pid_count": "Ընտրված է %1 գրառում(ներ):", + "fork_success": "Թեման հաջողությամբ մասնատվեց: Սեղմեք այստեղ՝ ճեղքված թեմային անցնելու համար:", + "delete_posts_instruction": "Սեղմեք այն գրառումները, որոնք ցանկանում եք ջնջել/մաքրել", + "merge_topics_instruction": "Սեղմեք այն թեմաները, որոնք ցանկանում եք միավորել կամ որոնել դրանք", + "merge-topic-list-title": "Միավորվող թեմաների ցանկ", + "merge-options": "Միավորել տարբերակները", + "merge-select-main-topic": "Ընտրել հիմնական թեման ", + "merge-new-title-for-topic": "Թեմայի նոր վերնագիր", + "topic-id": "Թեմայի ID", + "move_posts_instruction": "Սեղմեք այն գրառումները, որոնք ցանկանում եք տեղափոխել, ապա մուտքագրեք թեմայի ID կամ գնացեք թիրախային թեմա", + "change_owner_instruction": "Սեղմեք այն գրառումները, որոնք ցանկանում եք վերագրել մեկ այլ օգտատիրոջ", + "composer.title_placeholder": "Մուտքագրեք ձեր թեմայի վերնագիրը այստեղ...", + "composer.handle_placeholder": "Մուտքագրեք ձեր անունը/բռնակը այստեղ", + "composer.discard": "Հրաժարվել", + "composer.submit": "Հաստատել", + "composer.additional-options": "Լրացուցիչ տարբերակներ", + "composer.schedule": "Ժամանակացույց", + "composer.replying_to": "Պատասխանում է %1-ին", + "composer.new_topic": "Նոր թեմա", + "composer.editing": "Խմբագրում", + "composer.uploading": "վերբեռնում...", + "composer.thumb_url_label": "Տեղադրեք թեմայի մանրապատկերի URL", + "composer.thumb_title": "Ավելացրեք մանրապատկեր այս թեմային", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Կամ վերբեռնեք ֆայլ", + "composer.thumb_remove": "Մաքրելը դաշտերը", + "composer.drag_and_drop_images": "Տեղափոխեք և տեղադրեք նկարներն այստեղ", + "more_users_and_guests": "Եվս %1 օգտվող(ներ) և %2 հյուր(ներ)", + "more_users": "Եվս %1 օգտվող(ներ)", + "more_guests": "Եվս %1 հյուր(եր)", + "users_and_others": "%1 և %2 ուրիշներ", + "sort_by": "Դասավորել…", + "oldest_to_newest": "Հնից դեպի նոր", + "newest_to_oldest": "Նորից դեպի հին", + "most_votes": "Առավելագույն ձայներ", + "most_posts": "Ամենաշատ գրառումները", + "most_views": "Ամենաշատ դիտումները", + "stale.title": "Փոխարենը ստեղծե՞լ նոր թեմա։", + "stale.warning": "Թեման, որում գրառում եք կատարում բավականին հին է։ Կուզե՞ք այստեղ գրելու փոխարեն ստեղծել նոր թեմա՝ Ձեր պատասխանում հղելով այս մեկին։", + "stale.create": "Ստեղծել նոր թեմա", + "stale.reply_anyway": "Այնուամենայնիվ պատասխանել այստեղ", + "link_back": "Պատասխան՝ [%1](%2)", + "diffs.title": "Հրապարակման խմբագրման պատմություն", + "diffs.description": "Այս գրառումն ունի %1 վերանայում: Սեղմեք ստորև ներկայացված վերանայումներից մեկը՝ այդ պահին հրապարակման բովանդակությունը տեսնելու համար:", + "diffs.no-revisions-description": "Այս գրառումն ունի %1 վերանայում:", + "diffs.current-revision": "current revision", + "diffs.original-revision": "բնօրինակ վերանայում", + "diffs.restore": "Վերականգնել այս վերանայումը", + "diffs.restore-description": "Վերականգնելուց հետո այս գրառման խմբագրման պատմությանը կավելացվի նոր վերանայում:", + "diffs.post-restored": "Հաղորդագրությունը հաջողությամբ վերականգնվեց ավելի վաղ վերանայման տարբերակի", + "diffs.delete": "Ջնջել այս վերանայումը", + "diffs.deleted": "Վերանայումը ջնջված է", + "timeago_later": "%1 ավելի ուշ", + "timeago_earlier": "%1 ավելի վաղ", + "first-post": "Առաջին գրառում ", + "last-post": "Վերջին գրառում ", + "go-to-my-next-post": "Անցնել իմ հաջորդ գրառմանը", + "no-more-next-post": "Այս թեմայում այլ գրառումներ չունեք", + "post-quick-reply": "Տեղադրեք արագ պատասխան" +} \ No newline at end of file diff --git a/public/language/hy/unread.json b/public/language/hy/unread.json new file mode 100644 index 0000000000..c93707c6f7 --- /dev/null +++ b/public/language/hy/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Չկարդացված", + "no_unread_topics": "Չկարդացված թեմաներ չկան", + "load_more": "Բեռնել ավելին ", + "mark_as_read": "Նշել որպես կարդացված", + "selected": "Ընտրված", + "all": "Բոլորը", + "all_categories": "Բոլոր կատեգորիաները", + "topics_marked_as_read.success": "Թեմաները նշված են որպես կարդացված:", + "all-topics": "Բոլոր թեմաները", + "new-topics": "Նոր թեմաներ", + "watched-topics": "Դիտված թեմաներ", + "unreplied-topics": "Անպատասխան թեմաներ", + "multiple-categories-selected": "Բազմակի ընտրված" +} \ No newline at end of file diff --git a/public/language/hy/uploads.json b/public/language/hy/uploads.json new file mode 100644 index 0000000000..64106e2613 --- /dev/null +++ b/public/language/hy/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Ֆայլը վերբեռնվում է…", + "select-file-to-upload": "Ընտրեք ֆայլ՝ վերբեռնման համար։", + "upload-success": "Ֆայլը բարեհաջող վերբեռնվել է։", + "maximum-file-size": "Առավելագույնը՝ %1 ԿԲ", + "no-uploads-found": "Վերբեռնումներ չեն գտնվել", + "public-uploads-info": "Վերբեռնումները հրապարակային են, բոլոր այցելուները կարող են տեսնել դրանք:", + "private-uploads-info": "Վերբեռնումները մասնավոր են, դրանք կարող են տեսնել միայն մուտք գործած օգտատերերը:" +} \ No newline at end of file diff --git a/public/language/hy/user.json b/public/language/hy/user.json new file mode 100644 index 0000000000..55a4143420 --- /dev/null +++ b/public/language/hy/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Բլոկավորված", + "muted": "Ձայն անջատված", + "offline": "Օֆլայն", + "deleted": "Ջնջված", + "username": "Օգտատիրոջ անունը", + "joindate": "Միանալու ամսաթիվ", + "postcount": "Գրառումների հաշվարկ", + "email": "էլ. փոստ", + "confirm_email": "Հաստատել էլ. փոստ", + "account_info": "Հաշվի տեղեկատվություն", + "admin_actions_label": "Ադմինիստատիվ գործողություններ", + "ban_account": "Արգելափակված հաշիվ", + "ban_account_confirm": "Իսկապե՞ս ուզում եք արգելափակել այս օգտվողին:", + "unban_account": "Արգելահանել հաշիվը", + "mute_account": "Անջատել հաշվի ձայնը", + "unmute_account": "Միացնել հաշվի ձայնը", + "delete_account": "Ջնջել հաշիվը", + "delete_account_as_admin": "Ջնջել Հաշիվը", + "delete_content": "Ջնջել հաշվի կոնտենտը", + "delete_all": "Ջնջել հաշիվը և կոնտենտը", + "delete_account_confirm": "Համոզվա՞ծ եք, որ ցանկանում եք անանուն դարձնել ձեր գրառումները և ջնջել ձեր հաշիվը: Այս գործողությունն անշրջելի է, և դուք չեք կարողանա վերականգնել ձեր տվյալները: Մուտքագրեք ձեր գաղտնաբառը՝ հաստատելու, որ ցանկանում եք ոչնչացնել այս հաշիվը:", + "delete_this_account_confirm": "Վստա՞հ եք, որ ցանկանում եք ջնջել այս հաշիվը՝ թողնելով դրա բովանդակությունը: Այս գործողությունն անշրջելի է, հաղորդագրություններն անանուն կլինեն, և դուք չեք կարողանա վերականգնել ջնջված հաշվի հետ գրառումների կապերը:", + "delete_account_content_confirm": "Իսկապե՞ս ուզում եք ջնջել այս հաշվի կոնտենտը (գրառումներ/թեմաներ/վերբեռնումներ): Այս գործողությունն անշրջելի է, և դուք չեք կարողանա վերականգնել որևէ տվյալ", + "delete_all_confirm": "Իսկապե՞ս ուզում եք ջնջել այս հաշիվը և դրա ողջ կոնտենտը (գրառումներ/թեմաներ/վերբեռնումներ): Այս գործողությունն անշրջելի է, և դուք չեք կարողանա վերականգնել որևէ տվյալ", + "account-deleted": "Հաշիվը ջնջված է", + "account-content-deleted": "Հաշվի կոնտենտը ջնջվել է ", + "fullname": "Անուն", + "website": "Վեբ կայք", + "location": "Գտնվելու վայրը", + "age": "տարիք", + "joined": "Միացած է", + "lastonline": "Վերջին առցանց", + "profile": "Անձնական էջ", + "profile_views": "Պրոֆիլի դիտումներ", + "reputation": "վարկանիշ", + "bookmarks": "Էջանիշեր", + "watched_categories": "Դիտված կատեգորիաներ", + "change_all": "Փոխել բոլորը", + "watched": "Դիտված", + "ignored": "Անտեսված", + "default-category-watch-state": "Հիմնական կատեգորիայի դիտման վիճակը", + "followers": "Հետևորդներ", + "following": "Հետևող", + "blocks": "Արգելափակումներ", + "block_toggle": "Միացնել արգելափակումը", + "block_user": "Արգելափակել Օգտատիրոջը", + "unblock_user": "Արգելափակել Օգտատիրոջը", + "aboutme": "Իմ մասին ", + "signature": "Ստորագրություն", + "birthday": "Ծննդյան ամսաթիվ", + "chat": "Չաթ", + "chat_with": "Շարունակել զրուցել %1-ի հետ", + "new_chat_with": "Սկսեք նոր զրույց %1-ով", + "flag-profile": "Նշել պրոֆիլը", + "follow": "հետեւել", + "unfollow": "Չհետևել", + "more": "Ավելին", + "profile_update_success": "Պրոֆիլը հաջողությամբ թարմացվել է:", + "change_picture": "Փոխել նկարը", + "change_username": "Փոխել օգտատիրոջ անունը", + "change_email": "Փոխել էլ. փոստը", + "email_same_as_password": "Խնդրում ենք մուտքագրել ձեր ընթացիկ գաղտնաբառը՝ շարունակելու համար – դուք կրկին մուտքագրել եք ձեր նոր էլ.փոստը", + "edit": "Խմբագրել", + "edit-profile": "Խմբագրել պրոֆիլը", + "default_picture": "Կանխադրված պատկերակ", + "uploaded_picture": "Վերբեռնված նկար", + "upload_new_picture": "Վերբեռնել նոր նկար", + "upload_new_picture_from_url": "Վերբեռնեք նոր նկար URL-ից", + "current_password": "ներկայիս գաղտնաբառը", + "change_password": "փոխել գաղտնաբառը ", + "change_password_error": "Սխալ գաղտնաբառ", + "change_password_error_wrong_current": "Ձեր ընթացիկ գաղտնաբառը սխալ է", + "change_password_error_match": "Գաղտնաբառը պետք է համընկնի", + "change_password_error_privileges": "Դուք իրավունք չունեք փոխելու այս գաղտնաբառը:", + "change_password_success": "Ձեր գաղտնաբառը թարմացվել է:", + "confirm_password": "հաստատել գաղտնաբառը ", + "password": "գաղտնաբառ", + "username_taken_workaround": "Ձեր խնդրած օգտանունն արդեն վերցված է, ուստի մենք այն մի փոքր փոփոխել ենք: Դուք այժմ հայտնի եք որպես %1", + "password_same_as_username": "Ձեր գաղտնաբառը նույնն է, ինչ ձեր օգտանունը, խնդրում ենք ընտրել այլ գաղտնաբառ:", + "password_same_as_email": "Ձեր գաղտնաբառը նույնն է, ինչ ձեր էլ.փոստը, խնդրում ենք ընտրել այլ գաղտնաբառ:", + "weak_password": "Թույլ Գաղտնաբառ.", + "upload_picture": "Վերբեռնել նկար", + "upload_a_picture": "Վերբեռնել նկար", + "remove_uploaded_picture": "Հեռացնել վերբեռնված նկարը", + "upload_cover_picture": "Վերբեռնեք շապիկի նկարը", + "remove_cover_picture_confirm": "Իսկապե՞ս ուզում եք հեռացնել շապիկի նկարը:", + "crop_picture": "Կտրել նկարը", + "upload_cropped_picture": "Կտրել և վերբեռնել", + "avatar-background-colour": "Ավատարի ֆոնի գույնը", + "settings": "Կարգավորումներ", + "show_email": "Ցույց տալ իմ էլ. փոստը", + "show_fullname": "Ցույց տալ իմ լրիվ անունը", + "restrict_chats": "Թույլատրել զրույցի հաղորդագրությունները միայն այն օգտվողներից, որոնց ես հետևում եմ", + "digest_label": "Բաժանորդագրվել Digest-ին", + "digest_description": "Բաժանորդագրվեք այս ֆորումի էլեկտրոնային թարմացումներին (նոր ծանուցումներ և թեմաներ) ըստ սահմանված ժամանակացույցի", + "digest_off": "Անջատված", + "digest_daily": "Օրական", + "digest_weekly": "Շաբաթական", + "digest_biweekly": "Երկու շաբաթը մեկ ", + "digest_monthly": "ամսական", + "has_no_follower": "Այս օգտվողը հետևորդներ չունի :(", + "follows_no_one": "Այս օգտվողը ոչ մեկին չի հետևում :(", + "has_no_posts": "Այս օգտվողը դեռ ոչինչ չի հրապարակել:", + "has_no_best_posts": "Այս օգտատերը դեռ չունի դրական քվեարկված գրառումներ:", + "has_no_topics": "Այս օգտվողը դեռ ոչ մի թեմա չի հրապարակել:", + "has_no_watched_topics": "Այս օգտվողը դեռ ոչ մի թեմա չի դիտել:", + "has_no_ignored_topics": "Այս օգտատերը դեռ ոչ մի թեմա չի անտեսել:", + "has_no_upvoted_posts": "Այս օգտատերը դեռևս ոչ մի գրառման օգտին չի քվեարկել:", + "has_no_downvoted_posts": "Այս օգտատերը դեռևս ոչ մի գրառման դեմ չի քվեարկել:", + "has_no_controversial_posts": "Այս օգտատերը դեռ չունի դեմ քվեարկած գրառումներ:", + "has_no_blocks": "Դուք ոչ մի օգտատեր չեք արգելափակել:", + "email_hidden": "Էլեկտրոնային փոստը թաքցված է", + "hidden": "Թաքնված է", + "paginate_description": "Էջադրեք թեմաներն ու գրառումները՝ անսահման ոլորման փոխարեն", + "topics_per_page": "Թեմաներ մեկ էջի համար", + "posts_per_page": "Գրառումներ մեկ էջի համար", + "max_items_per_page": "Առավելագույնը %1", + "acp_language": "Ադմինիստրատորի էջի լեզուն", + "notifications": "Ծանուցումներ", + "upvote-notif-freq": "Կողմ քվեարկության ծանուցման հաճախականությունը", + "upvote-notif-freq.all": "Բոլոր կողմ ձայները", + "upvote-notif-freq.first": "Առաջին գրառումը", + "upvote-notif-freq.everyTen": "Յուրաքանչյուր տասը կողմ ձայն", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Անջատված", + "browsing": "Զննման կարգավորումներ", + "open_links_in_new_tab": "Բացեք ելքային հղումները նոր ներդիրում", + "enable_topic_searching": "Միացնել թեմայում որոնումը", + "topic_search_help": "Եթե միացված է, թեմայում որոնումը կշրջանցի դիտարկիչի էջի որոնման վարքագիծը և թույլ կտա ձեզ որոնել ամբողջ թեման՝ միայն էկրանին ցուցադրվածի փոխարեն:", + "update_url_with_post_index": "Թեմաներ զննարկելիս թարմացրեք url-ը գրառումների ինդեքսով", + "scroll_to_my_post": "Պատասխան փակցնելուց հետո ցույց տվեք նոր գրառումը", + "follow_topics_you_reply_to": "Դիտեք այն թեմաները, որոնց պատասխանում եք", + "follow_topics_you_create": "Դիտեք ձեր ստեղծած թեմաները", + "grouptitle": "Խմբի անվանումը", + "group-order-help": "Ընտրեք խումբ և օգտագործեք սլաքները վերնագրեր պատվիրելու համար", + "no-group-title": "Խմբի վերնագիր չկա", + "select-skin": "Ընտրեք շապիկ", + "select-homepage": "Ընտրեք գլխավոր էջ", + "homepage": "Գլխավոր էջ", + "homepage_description": "Ընտրեք էջ՝ որպես ֆորումի գլխավոր էջ օգտագործելու համար, կամ «Ոչ մեկը»՝ կանխադրված գլխավոր էջն օգտագործելու համար:", + "custom_route": "Պատվերով Գլխավոր էջի ուղեգիծ", + "custom_route_help": "Մուտքագրեք ուղեգծի անունը այստեղ՝ առանց որևէ նախորդ կտրվածքի (օրինակ՝ «վերջին» կամ «կատեգորիա/2/ընդհանուր քննարկում»)", + "sso.title": "Մեկ մուտքի ծառայություններ", + "sso.associated": "Առնչվում է", + "sso.not-associated": "Սեղմեք այստեղ՝ հետ կապնվելու համար", + "sso.dissociate": "Անջատվել", + "sso.dissociate-confirm-title": "Հաստատեք տարանջատումը", + "sso.dissociate-confirm": "Վստա՞հ եք, որ ցանկանում եք անջատել ձեր հաշիվը %1-ից:", + "info.latest-flags": "Վերջին դրոշները", + "info.no-flags": "Դրոշակավորված գրառումներ չեն գտնվել", + "info.ban-history": "Արգելքի վերջին պատմությունը", + "info.no-ban-history": "Այս օգտատերը երբեք չի արգելափակվել", + "info.banned-until": "Արգելված է մինչև %1", + "info.banned-expiry": "Ժամկետը", + "info.banned-permanently": "Ընդմիշտ արգելված է", + "info.banned-reason-label": "Պատճառ", + "info.banned-no-reason": "Ոչ մի պատճառ չի նշվում:", + "info.mute-history": "Ձայնի անջատման վերջին պատմությունը", + "info.no-mute-history": "Այս օգտատիրոջ ձայնը երբեք չի անջատվել", + "info.muted-until": "Ձայնը անջատված է մինչև %1", + "info.muted-expiry": "Ժամկետը", + "info.muted-no-reason": "Ոչ մի պատճառ չի նշվում:", + "info.username-history": "Օգտատիրոջ անունի պատմություն", + "info.email-history": "էլ.փոստի պատմություն", + "info.moderation-note": "Մոդերացիոն նշում", + "info.moderation-note.success": "Մոդերացիայի նշումը պահվեց", + "info.moderation-note.add": "Ավելացնել նշում", + "sessions.description": "Այս էջը թույլ է տալիս դիտել ցանկացած ակտիվ սեանս այս ֆորումում և անհրաժեշտության դեպքում չեղարկել դրանք: Դուք կարող եք չեղարկել ձեր սեփական սեանսը՝ դուրս գալով ձեր հաշվից:", + "consent.title": "Your Rights & Consent", + "consent.lead": "Այս համայնքի ֆորումը հավաքում և մշակում է ձեր անձնական տվյալները:", + "consent.intro": "Մենք օգտագործում ենք այս տեղեկատվությունը խստորեն այս համայնքում ձեր փորձառությունն անհատականացնելու, ինչպես նաև ձեր կատարած գրառումները ձեր օգտատիրոջ հաշվին կապելու համար: Գրանցման քայլի ընթացքում ձեզանից պահանջվել է տրամադրել օգտատիրոջ անուն և էլ.փոստի հասցե, դուք կարող եք նաև լրացուցիչ տեղեկություններ տրամադրել այս կայքում ձեր օգտատիրոջ պրոֆիլը լրացնելու համար: Մենք պահպանում ենք այս տեղեկատվությունը ձեր օգտատիրոջ հաշվի ողջ կյանքի ընթացքում, և դուք կարող եք հետ վերցնել համաձայնությունը: ցանկացած պահի ջնջելով ձեր հաշիվը: Ցանկացած ժամանակ դուք կարող եք պահանջել ձեր ներդրման պատճենը այս կայքում՝ ձեր իրավունքների և amp; Համաձայնության էջ: Եթե ունեք հարցեր կամ մտահոգություններ, խորհուրդ ենք տալիս դիմել այս ֆորումի ադմինիստրատիվ թիմին:", + "consent.email_intro": "Երբեմն, մենք կարող ենք նամակներ ուղարկել ձեր գրանցված էլ․ հասցեին՝ թարմացումներ տրամադրելու և/կամ ձեզ ծանուցելու նոր գործունեության մասին, որը վերաբերում է ձեզ: Դուք կարող եք հարմարեցնել համայնքի ամփոփման հաճախականությունը (ներառյալ այն ուղղակիորեն անջատելը), ինչպես նաև ընտրել, թե ինչ տեսակի ծանուցումներ պետք է ստանալ էլփոստի միջոցով՝ ձեր օգտվողի կարգավորումների էջի միջոցով:", + "consent.digest_frequency": "Եթե ձեր օգտատիրոջ կարգավորումներում բացահայտորեն չփոխվեն, այս համայնքը տրամադրում է էլփոստի ամփոփագրեր ամեն %1:", + "consent.digest_off": "Եթե ձեր օգտատիրոջ կարգավորումներում բացահայտորեն չփոխվեն կաևգավորումները, այս համայնքը էլփոստի ամփոփագրեր չի ուղարկում", + "consent.received": "Դուք համաձայնություն եք տվել այս կայքին ձեր տեղեկությունները հավաքելու և մշակելու համար: Լրացուցիչ գործողություն չի պահանջվում:", + "consent.not_received": "Դուք համաձայնություն չեք տվել տվյալների հավաքագրման և մշակման համար: Ցանկացած ժամանակ այս կայքի ադմինիստրացիան կարող է որոշել ջնջել ձեր հաշիվը՝ Տվյալների պաշտպանության ընդհանուր կանոնակարգին համապատասխանելու համար:", + "consent.give": "Համաձայնություն տվեք", + "consent.right_of_access": "Դուք ունեք մուտքի իրավունք", + "consent.right_of_access_description": "Դուք իրավունք ունեք մուտք գործելու այս կայքի կողմից հավաքված ցանկացած տվյալ՝ ըստ պահանջի: Դուք կարող եք առբերել այս տվյալների պատճենը՝ սեղմելով ստորև նշված համապատասխան կոճակը:", + "consent.right_to_rectification": "Դուք ուղղման իրավունք ունեք", + "consent.right_to_rectification_description": "Դուք իրավունք ունեք փոխել կամ թարմացնել մեզ տրամադրված ցանկացած ոչ ճշգրիտ տվյալ: Ձեր պրոֆիլը կարող է թարմացվել՝ խմբագրելով ձեր պրոֆիլը, և գրառման բովանդակությունը միշտ կարող է խմբագրվել: Եթե դա այդպես չէ, խնդրում ենք կապվել այս կայքի ադմինիստրատիվ թիմի հետ:", + "consent.right_to_erasure": "Դուք իրավունք ունեք ջնջելու", + "consent.right_to_erasure_description": "Ցանկացած ժամանակ դուք կարող եք չեղարկել տվյալների հավաքագրման և/կամ մշակման ձեր համաձայնությունը՝ ջնջելով ձեր հաշիվը: Ձեր անհատական պրոֆիլը կարող է ջնջվել, թեև ձեր տեղադրած բովանդակությունը կմնա: Եթե ցանկանում եք ջնջել և՛ ձեր հաշիվը, և՛ ձեր բովանդակությունը, դիմեք այս կայքի ադմինիստրատիվ թիմին:", + "consent.right_to_data_portability": "Դուք իրավունք ունեք տվյալների տեղափոխելիության", + "consent.right_to_data_portability_description": "Դուք կարող եք մեզանից պահանջել ձեր և ձեր հաշվի վերաբերյալ հավաքագրված ցանկացած տվյալների արտահանում մեքենայաընթեռնելի: Դուք կարող եք դա անել՝ սեղմելով ստորև նշված համապատասխան կոճակը:", + "consent.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Պրոֆիլի արտահանում, դուք ծանուցում կստանաք, երբ այն ավարտվի:", + "consent.export_uploads": "Արտահանել վերբեռնված բովանդակությունը (.zip)", + "consent.export-uploads-success": "Վերբեռնումների արտահանման ընթացքում դուք ծանուցում կստանաք, երբ այն ավարտվի:", + "consent.export_posts": "Արտահանել գրառումներ (.csv)", + "consent.export-posts-success": "Հաղորդագրություններ արտահանելով, դուք ծանուցում կստանաք, երբ այն ավարտվի:", + "emailUpdate.intro": "Խնդրում ենք մուտքագրել ձեր էլ.փոստի հասցեն ստորև: Այս ֆորումն օգտագործում է ձեր էլ․ հասցեն՝ պլանավորված ամփոփագրի և ծանուցումների համար, ինչպես նաև գաղտնաբառի կորստի դեպքում հաշիվը վերականգնելու համար:", + "emailUpdate.optional": "Այս դաշտը պարտադիր չէ: Դուք պարտավոր չեք տրամադրել ձեր էլ.փոստի հասցեն, սակայն առանց վավերացված էլ.փոստի դուք չեք կարողանա վերականգնել ձեր հաշիվը կամ մուտք գործել ձեր էլ.", + "emailUpdate.required": "Այս դաշտը պարտադիր է:", + "emailUpdate.change-instructions": "Մուտքագրված էլ. հասցեին կուղարկվի հաստատման նամակ՝ եզակի հղումով: Այդ հղումը մուտք գործելը կհաստատի էլփոստի հասցեի ձեր սեփականությունը, և այն կակտիվանա ձեր հաշվում: Ցանկացած ժամանակ դուք կարող եք թարմացնել ձեր էլ.փոստը ձեր հաշվի էջից:", + "emailUpdate.password-challenge": "Խնդրում ենք մուտքագրել ձեր գաղտնաբառը՝ հաշվի սեփականության իրավունքը հաստատելու համար:" +} \ No newline at end of file diff --git a/public/language/hy/users.json b/public/language/hy/users.json new file mode 100644 index 0000000000..c6b8293ef5 --- /dev/null +++ b/public/language/hy/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Վերջին օգտատերերը", + "top_posters": "Ամենաշատ գրառողները", + "most_reputation": "Ամենաբարձր վարկանիշը", + "most_flags": "Դրոշակներ", + "search": "Որոնում", + "enter_username": "Գրեք անուն և որոնեք", + "search-user-for-chat": "Փնտրեք օգտվողի չաթը սկսելու համար", + "load_more": "Բեռնել ավելին", + "users-found-search-took": "Գտնվեց %1 օգտվող։ Որոնումը տևեց %2 վայրկյան։", + "filter-by": "Ֆիլտրել ըստ", + "online-only": "Միայն առցանցները", + "invite": "Հրավիրել", + "prompt-email": "Էլ. հասցեներ՝", + "groups-to-join": "Միանալու խմբեր հրավերի հաստատումից հետո՝", + "invitation-email-sent": "%1-ին էլ. փոստով ուղարկվել է հրավեր", + "user_list": "Օգտվողների ցանկ", + "recent_topics": "Վերջին թեմաներ", + "popular_topics": "Հանրաճանաչ թեմաներ", + "unread_topics": "Չընթերցված թեմաներ", + "categories": "Բաժիններ", + "tags": "Պիտակներ", + "no-users-found": "Որևէ օգտվող չի գտնվել։" +} \ No newline at end of file diff --git a/public/language/id/_DO_NOT_EDIT_FILES_HERE.md b/public/language/id/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/id/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/id/admin/admin.json b/public/language/id/admin/admin.json new file mode 100644 index 0000000000..7085289bc8 --- /dev/null +++ b/public/language/id/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Anda yakin ingin membangun ulang dan mulai ulang NodeBB?", + "alert.confirm-restart": "Anda yakin ingin mulai ulang NodeBB?", + + "acp-title": "%1 | Kontrol Panel Admin NodeBB", + "settings-header-contents": "Konten", + "changes-saved": "Perubahan Disimpan", + "changes-saved-message": "Perubahan konfigurasi NodeBB Anda telah disimpan.", + "changes-not-saved": "Perubahan Tidak Disimpan", + "changes-not-saved-message": "NodeBB mengalami masalah saat menyimpan perubahan Anda. (%1)" +} \ No newline at end of file diff --git a/public/language/id/admin/advanced/cache.json b/public/language/id/admin/advanced/cache.json new file mode 100644 index 0000000000..edbb7f87ba --- /dev/null +++ b/public/language/id/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Cache Kiriman", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Penuh", + "post-cache-size": "Ukuran Cache Kiriman", + "items-in-cache": "Item di Cache" +} \ No newline at end of file diff --git a/public/language/id/admin/advanced/database.json b/public/language/id/admin/advanced/database.json new file mode 100644 index 0000000000..f7d8650db8 --- /dev/null +++ b/public/language/id/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Menyala dalam Detik", + "uptime-days": "Menyala dalam Hari", + + "mongo": "Mongo", + "mongo.version": "Versi MongoDB", + "mongo.storage-engine": "Mesin Penyimpanan", + "mongo.collections": "Koleksi", + "mongo.objects": "Objek", + "mongo.avg-object-size": "Rata-rata Ukuran Objek", + "mongo.data-size": "Ukuran Data", + "mongo.storage-size": "Ukuran Penyimpanan", + "mongo.index-size": "Ukuran Indeks", + "mongo.file-size": "Ukuran File", + "mongo.resident-memory": "Pemukim Memori", + "mongo.virtual-memory": "Memori Virtual", + "mongo.mapped-memory": "Memori Terpetakan", + "mongo.bytes-in": "Bytes Masuk", + "mongo.bytes-out": "Bytes Keluar", + "mongo.num-requests": "Jumlah Permintaan", + "mongo.raw-info": "Info Asali MongoDB", + "mongo.unauthorized": "NodeBB tidak dapat mengquery database MongoDB untuk statistik yang relevan. Harap pastikan bahwa pengguna yang digunakan oleh NodeBB berisi & quot; clusterMonitor & quot; peran & quot; admin & quot; database.", + + "redis": "Redis", + "redis.version": "Versi Redis", + "redis.keys": "Kunci", + "redis.expires": "Kadaluarsa", + "redis.avg-ttl": "Rata - rata TTL", + "redis.connected-clients": "Klien yang Terhubung", + "redis.connected-slaves": "Slave yang Terhubung", + "redis.blocked-clients": "Klien yang di Block", + "redis.used-memory": "Memori yang Terpakai", + "redis.memory-frag-ratio": "Rasio Fragmentasi Memori", + "redis.total-connections-recieved": "Jumlah Total koneksi yang Diterima", + "redis.total-commands-processed": "Jumlah Total Perintah yang Telah Terproses", + "redis.iops": "Operasi Instan per Detik", + "redis.iinput": "Input Instan per Detik", + "redis.ioutput": "Output Instan per Detik", + "redis.total-input": "Total Input", + "redis.total-output": "Total Output", + + "redis.keyspace-hits": "Hit pada Keyspace", + "redis.keyspace-misses": "Keyspace Terlewat", + "redis.raw-info": "Info Asali Redis", + + "postgres": "Postgres", + "postgres.version": "Versi PostgreSQL", + "postgres.raw-info": "Info Asali Postfres" +} diff --git a/public/language/id/admin/advanced/errors.json b/public/language/id/admin/advanced/errors.json new file mode 100644 index 0000000000..d6b5af4eaa --- /dev/null +++ b/public/language/id/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figur %1", + "error-events-per-day": "%1 tindakan per hari", + "error.404": "404 Tidak Ditemukan", + "error.503": "503 Layanan Tidak Tersedia", + "manage-error-log": "Kelola Log Galat", + "export-error-log": "Ekspor Log Galat (CSV)", + "clear-error-log": "Bersihkan Log Galat", + "route": "Rute", + "count": "Hitung", + "no-routes-not-found": "Hore! Tidak ada galat 404!", + "clear404-confirm": "Anda yakin ingin membersihkan log galat 404?", + "clear404-success": "Galat \"404 Tidak Ditemukan\" telah dibersihkan" +} \ No newline at end of file diff --git a/public/language/id/admin/advanced/events.json b/public/language/id/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/id/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/id/admin/advanced/logs.json b/public/language/id/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/id/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/id/admin/appearance/customise.json b/public/language/id/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/id/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/id/admin/appearance/skins.json b/public/language/id/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/id/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/id/admin/appearance/themes.json b/public/language/id/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/id/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/id/admin/dashboard.json b/public/language/id/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/id/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/id/admin/development/info.json b/public/language/id/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/id/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/id/admin/development/logger.json b/public/language/id/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/id/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/id/admin/extend/plugins.json b/public/language/id/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/id/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/id/admin/extend/rewards.json b/public/language/id/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/id/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/id/admin/extend/widgets.json b/public/language/id/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/id/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/id/admin/manage/admins-mods.json b/public/language/id/admin/manage/admins-mods.json new file mode 100644 index 0000000000..8e322def29 --- /dev/null +++ b/public/language/id/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrator", + "global-moderators": "Moderator Global", + "moderators": "Moderators", + "no-global-moderators": "Tidak ada Moderator Global", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Tidak ada Moderator", + "add-administrator": "Tambah Administrator", + "add-global-moderator": "Tambah Moderator Global", + "add-moderator": "Tambah Moderator" +} \ No newline at end of file diff --git a/public/language/id/admin/manage/categories.json b/public/language/id/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/id/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/id/admin/manage/digest.json b/public/language/id/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/id/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/id/admin/manage/groups.json b/public/language/id/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/id/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/id/admin/manage/privileges.json b/public/language/id/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/id/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/id/admin/manage/registration.json b/public/language/id/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/id/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/id/admin/manage/tags.json b/public/language/id/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/id/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/id/admin/manage/uploads.json b/public/language/id/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/id/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/id/admin/manage/users.json b/public/language/id/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/id/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/id/admin/menu.json b/public/language/id/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/id/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/advanced.json b/public/language/id/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/id/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/id/admin/settings/api.json b/public/language/id/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/id/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/chat.json b/public/language/id/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/id/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/cookies.json b/public/language/id/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/id/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/email.json b/public/language/id/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/id/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/id/admin/settings/general.json b/public/language/id/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/id/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/id/admin/settings/group.json b/public/language/id/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/id/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/guest.json b/public/language/id/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/id/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/homepage.json b/public/language/id/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/id/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/languages.json b/public/language/id/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/id/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/navigation.json b/public/language/id/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/id/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/id/admin/settings/notifications.json b/public/language/id/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/id/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/pagination.json b/public/language/id/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/id/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/post.json b/public/language/id/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/id/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/reputation.json b/public/language/id/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/id/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/social.json b/public/language/id/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/id/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/sockets.json b/public/language/id/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/id/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/sounds.json b/public/language/id/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/id/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/tags.json b/public/language/id/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/id/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/id/admin/settings/uploads.json b/public/language/id/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/id/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/id/admin/settings/user.json b/public/language/id/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/id/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/id/admin/settings/web-crawler.json b/public/language/id/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/id/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/id/category.json b/public/language/id/category.json new file mode 100644 index 0000000000..b7587d8ef6 --- /dev/null +++ b/public/language/id/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategori", + "subcategories": "Subkategori", + "new_topic_button": "Topik Baru", + "guest-login-post": "Masuk untuk memposting", + "no_topics": "Tidak ada topik dikategori ini
Mengapa anda tidak mencoba membuat yang baru?", + "browsing": "penjelajahan", + "no_replies": "Belum ada orang yang menjawab", + "no_new_posts": "Tidak ada post terbaru", + "watch": "mengamati", + "ignore": "Abaikan", + "watching": "mengamati", + "not-watching": "Not Watching", + "ignoring": "Abaikan", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Kategori yang diamati", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/id/email.json b/public/language/id/email.json new file mode 100644 index 0000000000..7ccd3ff608 --- /dev/null +++ b/public/language/id/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Selamat datang di %1", + "invite": "Undangan dari %1", + "greeting_no_name": "Hai", + "greeting_with_name": "Hai %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Terima kasih anda telah mendaftarkan diri anda dengan %1!", + "welcome.text2": "Untuk mengaktifkan akun anda sepenuhnya, kami perlu memverifkasi bahwa anda adalah pemilik email yang terdaftar.", + "welcome.text3": "Administrator telah menerima aplikasi pendaftaran anda. Anda dapat masuk dengan username/password anda sekarang", + "welcome.cta": "Klik disini untuk mengkonfirmasi alamat email anda.", + "invitation.text1": "%1 telah mengundang anda untuk bergabung %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Kami menerima permintan untuk mengatur ulang kata sandi anda, Ini dikarenakan anda telah lupa akan kata sandi anda. Tolong abaikan email ini jika sebaliknya.", + "reset.text2": "Mohon klik link berikut untuk mengatur ulang kata sandi anda.", + "reset.cta": "Klik di sini untuk mengatur ulang kata sandi anda", + "reset.notify.subject": "Kata Sandi berhasil diganti", + "reset.notify.text1": "Kami beritahukan bahwa pada %1 password anda berhasil diubah.", + "reset.notify.text2": "Jika ini bukan kehendak anda, silakan segera hubungi administrator.", + "digest.latest_topics": "Topik-topik terbaru dari %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Klik di sini untuk mengunjungi %1", + "digest.unsub.info": "Sesuai pengaturan langganan anda, maka ringkasan ini di kirimkan untuk anda ", + "digest.day": "day", + "digest.week": "week", + "digest.month": "month", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Pesan yang baru diterima dari %1", + "notif.chat.cta": "Klik di sini untuk melanjutkan percakapan", + "notif.chat.unsub.info": "Sesuai pengaturan langganan anda, notifikasi obrolan ini dikirmkan kepada anda", + "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Ini hanya email percobaan untuk menverifkasi pengiriman email telah diatur oleh NodeBB secara benar", + "unsub.cta": "Klik di sini untuk mengubah pengaturan-pengaturan tersebut.", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Terima kasih!" +} \ No newline at end of file diff --git a/public/language/id/error.json b/public/language/id/error.json new file mode 100644 index 0000000000..d82708bf4a --- /dev/null +++ b/public/language/id/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Data Salah", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Kamu terlihat belum login", + "account-locked": "Akun kamu dikunci sementara", + "search-requires-login": "Searching requires an account - please login or register.", + "goback": "Press back to return to the previous page", + "invalid-cid": "ID Kategori Salah", + "invalid-tid": "ID Topik Salah", + "invalid-pid": "ID Post Salah", + "invalid-uid": "ID User Salah", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Username Salah", + "invalid-email": "Email Salah", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Judul tidak valid", + "invalid-user-data": "Data Pengguna Salah", + "invalid-password": "Password Salah", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Mohon spesifikasikan username dan password", + "invalid-search-term": "Kata pencarian salah", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Nomor pagination tidak valid, minimal %1 dan maksimal %2", + "username-taken": "Username sudah terdaftar", + "email-taken": "Email sudah terdaftar", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Username terlalu pendek", + "username-too-long": "Username terlalu panjang", + "password-too-long": "Password too long", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Pengguna dibanned", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Kategori tidak ditemukan", + "no-topic": "Topik tidak ditemukan", + "no-post": "Post tidak ditemukan", + "no-group": "Grup tidak ditemukan", + "no-user": "Pengguna tidak ditemukan", + "no-teaser": "Teaser tidak ditemukan", + "no-flag": "Flag does not exist", + "no-privileges": "Kamu tidak punya cukup izin untuk melakukan ini", + "category-disabled": "Kategori ditiadakan", + "topic-locked": "Topik dikunci", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Silahkan tulis postingan yang lebih panjang. Posting harus mengandung setidaknya %1 karakter().", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Silahkan tulis judul yang lebih panjang. Judul harus mengandung setidaknya %1 karakter().", + "title-too-long": "Silahkan tulis judul yang lebih pendek. Judul tidak boleh lebih dari %1 karakter().", + "category-not-selected": "Category not selected.", + "too-many-posts": "Anda hanya dapat memposting sekali setiap %1 detik() - harap tunggu sebelum memposting lagi", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Tunggu proses upload sampai selesai", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Kamu tidak dapat ban admin lainnya!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Nama grup terlalu pendek", + "group-name-too-long": "Group name too long", + "group-already-exists": "Grup sudah ada", + "group-name-change-not-allowed": "Perubahan nama grup tidak dibolehkan", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Postingan ini sudah dihapus", + "post-already-restored": "Postingan ini sudah direstore", + "topic-already-deleted": "Topik ini sudah dihapus", + "topic-already-restored": "Topik ini sudah direstore", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "Thumbnail di topik ditiadakan", + "invalid-file": "File Salah", + "uploads-are-disabled": "Upload ditiadakan", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "Kamu tidak dapat chat dengan akun sendiri", + "chat-restricted": "Pengguna ini telah membatasi percakapa mereka. Mereka harus mengikutimu sebelum kamu dapat melakukan percakapan dengan mereka ", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Sistem reputasi ditiadakan.", + "downvoting-disabled": "Downvoting ditiadakan", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB mengalami masalah saat memuat \"%1\". NodeBB akan melanjutkan pemuatan, kamu harus membatalkan tindakanmu sebelum pemuatan kembali dilakukan.", + "registration-error": "Registrasti Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/id/flags.json b/public/language/id/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/id/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/id/global.json b/public/language/id/global.json new file mode 100644 index 0000000000..17706017db --- /dev/null +++ b/public/language/id/global.json @@ -0,0 +1,126 @@ +{ + "home": "Beranda", + "search": "Cari", + "buttons.close": "Tutup", + "403.title": "Akses ditolak", + "403.message": "Kamu kelihatan mengakses halaman yang kamu tidak memiliki akses", + "403.login": "Mungkin kamu harus mencoba untuk login?", + "404.title": "Tidak ditemukan", + "404.message": "Kamu kelihatan mengakses halaman yang tidak ada. Kembali ke beranda.", + "500.title": "Kesalahan Internal.", + "500.message": "Oops! Terjadi kesalahan", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Daftar", + "login": "Login", + "please_log_in": "Silakan Log In", + "logout": "Logout", + "posting_restriction_info": "Posting hanya boleh dilakukan oleh pengguna terdaftar, klik disini untuk log in.", + "welcome_back": "Selamat Datang Kembali", + "you_have_successfully_logged_in": "Kamu sudah login", + "save_changes": "Menyimpan perubahan", + "save": "Save", + "close": "Tutup", + "pagination": "Halaman", + "pagination.out_of": "%1 dari %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Kategori", + "header.recent": "Terbaru", + "header.unread": "Belum dibaca", + "header.tags": "Tag", + "header.popular": "Populer", + "header.top": "Top", + "header.users": "Pengguna", + "header.groups": "Grup", + "header.chats": "Chat", + "header.notifications": "Pemberitahuan", + "header.search": "Cari", + "header.profile": "Profil", + "header.navigation": "Navigasi", + "notifications.loading": "Memuat Pemberitahuan", + "chats.loading": "Memuat Chat", + "motd.welcome": "Selamat datang di NodeBB, platform diskusi masa depan.", + "previouspage": "Halaman Sebelumnya", + "nextpage": "Halaman Selanjutnya", + "alert.success": "Sukses", + "alert.error": "Error", + "alert.banned": "Banned", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Kamu tidak mengikuti %1", + "alert.follow": "Kamu mengikuti %1", + "users": "Pengguna", + "topics": "Topik", + "posts": "Post", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Views", + "posters": "Posters", + "reputation": "Reputasi", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "selengkapnya", + "more": "Lebih banyak", + "none": "None", + "posted_ago_by_guest": "dibuat %1 oleh Guest", + "posted_ago_by": "dibuat %1 oleh %2", + "posted_ago": "dibuat %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "dibuat di %1 %2", + "posted_in_ago_by": "dibuat di %1 %2 oleh %3", + "user_posted_ago": "Dibuat oleh %1 %2", + "guest_posted_ago": "Dibuat oleh Tamu %1", + "last_edited_by": "last edited by %1", + "norecentposts": "Tidak ada post terbaru", + "norecenttopics": "Tidak ada topik terbaru", + "recentposts": "Post Terbaru", + "recentips": "Beberapa IP yang digunakan untuk login baru-baru ini", + "moderator_tools": "Moderator Tools", + "online": "Online", + "away": "Tidak Ditempat", + "dnd": "Jangan ganggu", + "invisible": "Tidak Terlihat", + "offline": "Offline", + "email": "Email", + "language": "Bahasa", + "guest": "Tamu", + "guests": "Tamu", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum telah diupdate", + "updated.message": "Forum ini telah diupdate ke versi terbaru. Klik disini untuk memuat halaman.", + "privacy": "Privasi", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "Hapus Semua", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/id/groups.json b/public/language/id/groups.json new file mode 100644 index 0000000000..47065e357e --- /dev/null +++ b/public/language/id/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grup", + "view_group": "Tampilkan Grup", + "owner": "Group Owner", + "new_group": "Create New Group", + "no_groups_found": "There are no groups to see", + "pending.accept": "Accept", + "pending.reject": "Reject", + "pending.accept_all": "Accept All", + "pending.reject_all": "Reject All", + "pending.none": "There are no pending members at this time", + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Save", + "cover-saving": "Saving", + "details.title": "Rincian Grup", + "details.members": "Daftar Anggota", + "details.pending": "Pending Members", + "details.invited": "Invited Members", + "details.has_no_posts": "Anggota grup ini belum membuat posting satupun.", + "details.latest_posts": "Posting Terkini", + "details.private": "Private", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.member_count": "Member Count", + "details.creation_date": "Creation Date", + "details.description": "Description", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Badge Text", + "details.userTitleEnabled": "Show Badge", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Delete Group", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted", + "membership.accept-invitation": "Accept Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitation Pending", + "membership.join-group": "Join Group", + "membership.leave-group": "Leave Group", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Reject", + "new-group.group_name": "Group Name:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/id/ip-blacklist.json b/public/language/id/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/id/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/id/language.json b/public/language/id/language.json new file mode 100644 index 0000000000..1bcec669d0 --- /dev/null +++ b/public/language/id/language.json @@ -0,0 +1,5 @@ +{ + "name": "Bahasa Indonesia", + "code": "id", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/id/login.json b/public/language/id/login.json new file mode 100644 index 0000000000..15c4c5c63a --- /dev/null +++ b/public/language/id/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Username / Email", + "username": "Username", + "remember_me": "Ingin Diingat?", + "forgot_password": "Lupa Password?", + "alternative_logins": "Login Alternatif", + "failed_login_attempt": "Login Tidak Berhasil", + "login_successful": "Kamu telah berhasil login!", + "dont_have_account": "Belum memiliki akun?", + "logged-out-due-to-inactivity": "Anda sekarang sudah keluar dari Panel Kontrol Admin karena tidak aktif", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/id/modules.json b/public/language/id/modules.json new file mode 100644 index 0000000000..1b29cae734 --- /dev/null +++ b/public/language/id/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Berbincang dengan", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Kirim", + "chat.no_active": "Kamu tidak memiliki percakapan yang aktif.", + "chat.user_typing": "%1 sedang menulis ...", + "chat.user_has_messaged_you": "%1 telah mengirimkan pesan untukmu.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Mohon pilih satu penerima untuk melihat riwayat pesan percakapan", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Percakapan terbaru", + "chat.contacts": "Kontak", + "chat.message-history": "Riwayat Pesan", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Munculkan pesan", + "chat.minimize": "Minimize", + "chat.maximize": "Maksimalkan", + "chat.seven_days": "7 Hari", + "chat.thirty_days": "30 Hari", + "chat.three_months": "3 Bulan", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 berkata di %2: ", + "composer.user_said": "%1 berkata:", + "composer.discard": "Kamu yakin akan membuang posting ini?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/id/notifications.json b/public/language/id/notifications.json new file mode 100644 index 0000000000..1e07bb9034 --- /dev/null +++ b/public/language/id/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Pemberitahuan", + "no_notifs": "Kamu tidak memiliki pemberitahuan baru", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Kembali ke %1", + "outgoing_link": "Tautan Keluar", + "outgoing_link_message": "Kamu telah meninggalkan %1", + "continue_to": "Lanjut ke %1", + "return_to": "Kembali ke %1", + "new_notification": "Anda memiliki notifikasi baru", + "you_have_unread_notifications": "Kamu memiliki pemberitahuan yang belum dibaca.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Pesan baru dari %1", + "upvoted_your_post_in": "%1 telah melakukan upvote untuk posting kamu di %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 menandai sebuah posting di %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 telah mengirim sebuah balasan kepada: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 telah membuat topik baru: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 mulai mengikutimu.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 mengirim permintaan registrasi.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email telah Dikonfirmasi", + "email-confirmed-message": "Terimakasih telah melakukan validasi email. Akunmu saat ini telah aktif sepenuhnya.", + "email-confirm-error-message": "Terjadi masalah saat melakukan validasi emailmu. Mungkin terjadi kesalahan kode atau waktu habis.", + "email-confirm-sent": "Email konfirmasi telah dikirim.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/id/pages.json b/public/language/id/pages.json new file mode 100644 index 0000000000..bab11fe375 --- /dev/null +++ b/public/language/id/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Beranda", + "unread": "Topik belum Dibaca", + "popular-day": "Popular topics today", + "popular-week": "Popular topics this week", + "popular-month": "Popular topics this month", + "popular-alltime": "All time popular topics", + "recent": "Topik Terkini", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Online Users", + "users/latest": "Latest Users", + "users/sort-posts": "Users with the most posts", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "User Search", + "notifications": "Pemberitahuan", + "tags": "Tags", + "tag": "Topics tagged under "%1"", + "register": "Register an account", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Reset your account password", + "categories": "Categories", + "groups": "Groups", + "group": "%1 group", + "chats": "Chats", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "User Settings", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Confirmed", + "maintenance.text": "%1 saat ini sedang dalam masa pemeliharaan. Silahkan kembali lain waktu.", + "maintenance.messageIntro": "Tambahan, Administrator meninggalkan pesan ini:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/id/post-queue.json b/public/language/id/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/id/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/id/recent.json b/public/language/id/recent.json new file mode 100644 index 0000000000..896583950f --- /dev/null +++ b/public/language/id/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Terkini", + "day": "Hari", + "week": "Pekan", + "month": "Bulan", + "year": "Tahun", + "alltime": "Sepanjang Waktu", + "no_recent_topics": "Tidak ada topik terbaru.", + "no_popular_topics": "There are no popular topics.", + "there-is-a-new-topic": "There is a new topic.", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", + "there-are-new-topics": "There are %1 new topics.", + "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", + "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", + "there-is-a-new-post": "There is a new post.", + "there-are-new-posts": "There are %1 new posts.", + "click-here-to-reload": "Click here to reload." +} \ No newline at end of file diff --git a/public/language/id/register.json b/public/language/id/register.json new file mode 100644 index 0000000000..da2db909ce --- /dev/null +++ b/public/language/id/register.json @@ -0,0 +1,32 @@ +{ + "register": "Daftar", + "cancel_registration": "Cancel Registration", + "help.email": "Secara default, emailmu akan disembunyikan dari publik.", + "help.username_restrictions": "Nama Pengguna yang unik antara %1 dan %2 karakter. Pengguna lain dapat menyebutmu dengan menggunakan @nama pengguna.", + "help.minimum_password_length": "Panjang password harus setidaknya %1 karakter.", + "email_address": "Alamat Email", + "email_address_placeholder": "Masukkan Alamat Email", + "username": "Nama Pengguna", + "username_placeholder": "Masukkan Nama Pengguna", + "password": "Kata Sandi", + "password_placeholder": "Masukkan Kata Sandi", + "confirm_password": "Konfirmasi Kata Sandi", + "confirm_password_placeholder": "Konfirmasi Kata Sandi", + "register_now_button": "Daftar Sekarang", + "alternative_registration": "Pendaftaran Alternatif", + "terms_of_use": "Aturan Penggunaan", + "agree_to_terms_of_use": "Saya menyetujui Aturan Penggunaan", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Pendaftaranmu telah ditambahkan dalam daftar persetujuan. Kamu akan menerima email ketika pendaftaranmu disetujui oleh administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/id/reset_password.json b/public/language/id/reset_password.json new file mode 100644 index 0000000000..cf5831b3a0 --- /dev/null +++ b/public/language/id/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Atur Ulang Kata Sandi", + "update_password": "Perbarui Kata Sandi", + "password_changed.title": "Kata Sandi telah Diganti", + "password_changed.message": "

Kata Sandi berhasil diatur ulang, silakan login kembali.", + "wrong_reset_code.title": "Kode Pengaturan Ulang Salah", + "wrong_reset_code.message": "Kode pengaturan ulang salah. Silakan coba lagi, atauminta kode pengaturan ulang baru.", + "new_password": "Kata Sandi Baru", + "repeat_password": "Konfirmasi Kata Sandi", + "changing_password": "Changing Password", + "enter_email": "Mohon masukkan alamat emailmu dan kami akan mengirimkan mu sebuah email dengan instruksi mengenai cara pengaturan ulang akunmu.", + "enter_email_address": "Masukkan Alamat Email", + "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.", + "invalid_email": "Email Salah / Email tidak ada!", + "password_too_short": "Password terlalu pendek, silahkan pilih password lain.", + "passwords_do_not_match": "Kedua password yang kamu masukkan tidak sama.", + "password_expired": "Password kamu sudah expired, silahkan masukkan password baru" +} \ No newline at end of file diff --git a/public/language/id/search.json b/public/language/id/search.json new file mode 100644 index 0000000000..ab59b9249b --- /dev/null +++ b/public/language/id/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 hasil yang sesuai dengan \"%2\", (%3 detik)", + "no-matches": "No matches found", + "advanced-search": "Pencarian Lanjut", + "in": "Dalam", + "titles": "Judul", + "titles-posts": "Judul dan Post", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Diposting oleh", + "in-categories": "Dalam Kategori", + "search-child-categories": "Search child categories", + "has-tags": "Has tags", + "reply-count": "Reply Count", + "at-least": "At least", + "at-most": "At most", + "relevance": "Relevance", + "post-time": "Post time", + "votes": "Votes", + "newer-than": "Newer than", + "older-than": "Older than", + "any-date": "Any date", + "yesterday": "Yesterday", + "one-week": "One week", + "two-weeks": "Two weeks", + "one-month": "One month", + "three-months": "Three months", + "six-months": "Six months", + "one-year": "One year", + "sort-by": "Sort by", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "topic-votes": "Topic votes", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Username", + "category": "Category", + "descending": "In descending order", + "ascending": "In ascending order", + "save-preferences": "Save preferences", + "clear-preferences": "Clear preferences", + "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)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/id/success.json b/public/language/id/success.json new file mode 100644 index 0000000000..266e5e27ad --- /dev/null +++ b/public/language/id/success.json @@ -0,0 +1,7 @@ +{ + "success": "Sukses", + "topic-post": "Kamu berhasil melakukan posting.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Otentikasi Sukses", + "settings-saved": "Pengaturan disimpan!" +} \ No newline at end of file diff --git a/public/language/id/tags.json b/public/language/id/tags.json new file mode 100644 index 0000000000..315df1f61a --- /dev/null +++ b/public/language/id/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Tidak ada topik dengan tag ini.", + "tags": "Tag", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "Masukkan tag...", + "no_tags": "Belum ada tag.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/id/top.json b/public/language/id/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/id/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/id/topic.json b/public/language/id/topic.json new file mode 100644 index 0000000000..423af14c4b --- /dev/null +++ b/public/language/id/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Topik", + "title": "Title", + "no_topics_found": "Topik tidak ditemukan!", + "no_posts_found": "Tidak ada posting yang ditemukan!", + "post_is_deleted": "Posting ini telah dihapus!", + "topic_is_deleted": "Topik terhapus!", + "profile": "Profil", + "posted_by": "Dibuat oleh %1", + "posted_by_guest": "Dibuat oleh Tamu", + "chat": "Percakapan", + "notify_me": "Beritahukan balasan baru untuk topik ini", + "quote": "Kutip", + "reply": "Balas", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log in untuk membalas", + "login-to-view": "🔒 Log in to view", + "edit": "Ubah", + "delete": "Hapus", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Musnahkan", + "restore": "Kembalikan", + "move": "Pindah", + "change-owner": "Change Owner", + "fork": "Cabangkan", + "link": "Tautan", + "share": "Bagikan", + "tools": "Perangkat", + "locked": "Terkunci", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Klik di sini untuk kembali ke posting yang terakhir kali dibaca pada topik ini.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Topik ini telah dihapus. Hanya pengguna dengan hak manajemen topik yang dapat melihatnya.", + "following_topic.message": "Saat ini kamu akan menerima pemberitahuan saat seseorang membuat posting di dalam topik ini.", + "not_following_topic.message": "Anda akan melihat topik ini di daftar topik yang belum dibaca, tetapi Anda tidak akan menerima pemberitahuan ketika seseorang memposting ke topik ini.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Daftar atau login untuk berlangganan topik ini.", + "markAsUnreadForAll.success": "Topik ditandai Belum Dibaca seluruhnya", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "Pantau", + "unwatch": "Batalkan Pantau", + "watch.title": "Beritahukan balasan baru untuk topik ini", + "unwatch.title": "Berhenti memantau topik ini", + "share_this_post": "Bagikan Posting ini", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Perangkat Topik", + "thread_tools.markAsUnreadForAll": "Tandai Belum Dibaca Untuk Semua", + "thread_tools.pin": "Tempel Topik", + "thread_tools.unpin": "Copot Topik", + "thread_tools.lock": "Kunci Topik", + "thread_tools.unlock": "Lepas Topik", + "thread_tools.move": "Pindah Topik", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Pindah Semua", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Cabangkan Topik", + "thread_tools.delete": "Hapus Topik", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Kamu yakin ingin menghapus topik ini?", + "thread_tools.restore": "Kembalikan Topik", + "thread_tools.restore_confirm": "Kamu yakin ingin mengembalikan topik ini?", + "thread_tools.purge": "Musnahkan Topik", + "thread_tools.purge_confirm": "Kamu yakin ingin memusnahkan topik ini?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Kamu yakin ingin menghapus posting ini?", + "post_restore_confirm": "Kamu yakin ingin mengembalikan posting ini?", + "post_purge_confirm": "Kamu yakin ingin memusnahkan posting ini?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Memuat Kategori", + "confirm_move": "Pindah", + "confirm_fork": "Cabangkan", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Memuat Lebih Banyak Posting", + "move_topic": "Pindahkan Topik", + "move_topics": "Pindahkan Beberapa Topik", + "move_post": "Pindahkan Posting", + "post_moved": "Posting dipindahkan!", + "fork_topic": "Cabangkan Topik", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Klik posting yang kamu ingin cabangkan", + "fork_no_pids": "Tidak ada posting yang dipilih!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Topik berhasil dicabangkan! Klik disini untuk menuju topik yang telah dicabangkan.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Masukkan judul topik di sini...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Buang", + "composer.submit": "Kirim", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Membalas ke %1", + "composer.new_topic": "Topik Baru", + "composer.editing": "Editing", + "composer.uploading": "mengunggah...", + "composer.thumb_url_label": "Tempelkan URL gambar mini topik", + "composer.thumb_title": "Tambah gambar mini untuk topik ini", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Atau unggah sebuah berkas", + "composer.thumb_remove": "Hapus kolom", + "composer.drag_and_drop_images": "Geser dan Lepas Gambar di sini", + "more_users_and_guests": "%1 lebuh pengguna dan %2 tamu", + "more_users": "%1 lebih pengguna", + "more_guests": "%1 lebih tamu", + "users_and_others": "%1 dan %2 lainnya", + "sort_by": "Urutkan berdasakan", + "oldest_to_newest": "Terlama ke Terbaru", + "newest_to_oldest": "Terbaru ke Terlama", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/id/unread.json b/public/language/id/unread.json new file mode 100644 index 0000000000..f89483c163 --- /dev/null +++ b/public/language/id/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Belum Dibaca", + "no_unread_topics": "Tidak ada topik yang belum dibaca.", + "load_more": "Tampilkan Lebih Banyak", + "mark_as_read": "Tandai Sudah Dibaca", + "selected": "Terpilih", + "all": "Semua", + "all_categories": "Semua Kategori", + "topics_marked_as_read.success": "Topik ditandai sudah dibaca!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/id/uploads.json b/public/language/id/uploads.json new file mode 100644 index 0000000000..651a839876 --- /dev/null +++ b/public/language/id/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Uploading the file...", + "select-file-to-upload": "Select a file to upload!", + "upload-success": "File uploaded successfully!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/id/user.json b/public/language/id/user.json new file mode 100644 index 0000000000..9a584fe73f --- /dev/null +++ b/public/language/id/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banned", + "muted": "Muted", + "offline": "Offline", + "deleted": "Deleted", + "username": "Nama Pengguna", + "joindate": "Join Date", + "postcount": "Post Count", + "email": "Email", + "confirm_email": "Konfirmasi Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Account", + "ban_account_confirm": "Do you really want to ban this user?", + "unban_account": "Unban Account", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Hapus Akun", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Account deleted", + "account-content-deleted": "Account content deleted", + "fullname": "Nama Lengkap", + "website": "Website", + "location": "Lokasi", + "age": "Umur", + "joined": "Tergabung", + "lastonline": "Online Terakhir", + "profile": "Profil", + "profile_views": "Tampilan profil", + "reputation": "Reputasi", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Watched", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Pengikut", + "following": "Mengikuti", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "About me", + "signature": "Tanda Pengenal", + "birthday": "Hari Lahir", + "chat": "Percakapan", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Ikuti", + "unfollow": "Tinggalkan", + "more": "More", + "profile_update_success": "Profil berhasil diperbarui!", + "change_picture": "Ganti Gambar/Foto", + "change_username": "Change Username", + "change_email": "Change Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Perbarui", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Gambar/Foto yang Diunggah", + "upload_new_picture": "Unggah Gambar/Foto Baru", + "upload_new_picture_from_url": "Unggah Gambar/Foto Baru dari URL", + "current_password": "Kata Sandi Saat Ini", + "change_password": "Ganti Kata Sandi", + "change_password_error": "Kata Sandi Salah!", + "change_password_error_wrong_current": "Kata Sandi kamu saat ini salah!", + "change_password_error_match": "Kata Sandi harus sesuai!", + "change_password_error_privileges": "Kamu tidak memiliki hak untuk mengganti kata sandi ini.", + "change_password_success": "Kata Sandi kamu telah diperbarui!", + "confirm_password": "Konfirmasi Kata Sandi", + "password": "Kata Sandi", + "username_taken_workaround": "Nama pengguna yang kamu inginkan telah diambil, jadi kami merubahnya sedikit. Kamu saat ini dikenal sebagai %1", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "Unggah gambar/foto", + "upload_a_picture": "Unggah sebuah gambar/foto", + "remove_uploaded_picture": "Remove Uploaded Picture", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Pengaturan", + "show_email": "Tampilkan Email Saya", + "show_fullname": "Tampilkan Nama Lengkap Saya", + "restrict_chats": "Hanya ijinkan pesan percakapan dari pengguna yang saya ikuti.", + "digest_label": "Berlangganan Digest", + "digest_description": "Berlangganan melalui email untuk forum ini (pemberitahuan baru dan topik) sesuai dengan pengaturan jadwal", + "digest_off": "Off", + "digest_daily": "Harian", + "digest_weekly": "Mingguan", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Bulanan", + "has_no_follower": "User ini tidak memiliki pengikut :(", + "follows_no_one": "User ini tidak mengikuti seorangpun :(", + "has_no_posts": "Pengguna ini belum memposting apa pun.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Pengguna ini belum memposting topik apa pun.", + "has_no_watched_topics": "This user hasn't watched any topics yet.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Email Disembunyikan", + "hidden": "disembunyikan", + "paginate_description": "Paginate topik dan post daripada menggunakan infinite scroll", + "topics_per_page": "Topik per Halaman", + "posts_per_page": "Posting per Halaman", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Pengaturan Penelusuran", + "open_links_in_new_tab": "Open outgoing links in new tab", + "enable_topic_searching": "Gunakan Pencarian Di dalam Topik", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + "select-skin": "Select a Skin", + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/id/users.json b/public/language/id/users.json new file mode 100644 index 0000000000..f78ac6c660 --- /dev/null +++ b/public/language/id/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Pengguna Terakhir", + "top_posters": "Posting Terbanyak", + "most_reputation": "Reputasi Terbanyak", + "most_flags": "Most Flags", + "search": "Pencarian", + "enter_username": "Masukkan nama pengguna untuk mencari", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Tampilkan Lebih Banyak", + "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", + "filter-by": "Filter By", + "online-only": "Online only", + "invite": "Invite", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "An invitation email has been sent to %1", + "user_list": "User List", + "recent_topics": "Recent Topics", + "popular_topics": "Popular Topics", + "unread_topics": "Unread Topics", + "categories": "Categories", + "tags": "Tags", + "no-users-found": "No users found!" +} \ No newline at end of file diff --git a/public/language/it/_DO_NOT_EDIT_FILES_HERE.md b/public/language/it/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/it/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/it/admin/admin.json b/public/language/it/admin/admin.json new file mode 100644 index 0000000000..74dd839368 --- /dev/null +++ b/public/language/it/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Sei sicuro di voler riorganizza e riavviare NodeBB?", + "alert.confirm-restart": "Sei sicuro di voler riavviare NodeBB?", + + "acp-title": "%1 | Pannello di controllo amministratore NodeBB", + "settings-header-contents": "Contenuti", + "changes-saved": "Modifiche salvate", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Modifiche non salvate", + "changes-not-saved-message": "NodeBB ha incontrato un problema nel salvare le tue modifiche. (%1)" +} \ No newline at end of file diff --git a/public/language/it/admin/advanced/cache.json b/public/language/it/admin/advanced/cache.json new file mode 100644 index 0000000000..2a023ace90 --- /dev/null +++ b/public/language/it/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Cache Post", + "group-cache": "Cache di gruppo", + "local-cache": "Cache locale", + "object-cache": "Cache oggetti", + "percent-full": "%1% Pieno", + "post-cache-size": "Dimensione Cache dei Post", + "items-in-cache": "Elementi nella Cache" +} \ No newline at end of file diff --git a/public/language/it/admin/advanced/database.json b/public/language/it/admin/advanced/database.json new file mode 100644 index 0000000000..ca0a1cb8aa --- /dev/null +++ b/public/language/it/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Tempo di caricamento in secondi", + "uptime-days": "Tempo di caricamento in giorni", + + "mongo": "Mongo", + "mongo.version": "Versione MongoDB", + "mongo.storage-engine": "Motore di archiviazione", + "mongo.collections": "Collezioni", + "mongo.objects": "Oggetti", + "mongo.avg-object-size": "Dimensione media dell'oggetto", + "mongo.data-size": "Dimensione dei dati", + "mongo.storage-size": "Dimensione di archiviazione", + "mongo.index-size": "Dimensione dell'indice", + "mongo.file-size": "Dimensione del file", + "mongo.resident-memory": "Memoria allocata", + "mongo.virtual-memory": "Memoria virtuale", + "mongo.mapped-memory": "Memoria mappata", + "mongo.bytes-in": "Byte in ingresso", + "mongo.bytes-out": "Byte in uscita", + "mongo.num-requests": "Numero di richieste", + "mongo.raw-info": "Info MongoDB Raw", + "mongo.unauthorized": "NodeBBB non è stato in grado di interrogare il database MongoDB per le statistiche pertinenti. Assicurati che l'utente in uso da NodeBBB contenga il "clusterMonitor" ruolo per l' "admin" database.", + + "redis": "Redis", + "redis.version": "Versione Redis", + "redis.keys": "Chiavi", + "redis.expires": "Scaduti", + "redis.avg-ttl": "TTL media", + "redis.connected-clients": "Client connessi", + "redis.connected-slaves": "Slave connessi", + "redis.blocked-clients": "Client bloccati", + "redis.used-memory": "Memoria usata", + "redis.memory-frag-ratio": "Rapporto di frammentazione della memoria", + "redis.total-connections-recieved": "Totale connessioni ricevute", + "redis.total-commands-processed": "Totale comandi processati", + "redis.iops": "Operazioni istantanee al secondo", + "redis.iinput": "Ingressi istantanei al secondo", + "redis.ioutput": "Uscite istantanee al secondo", + "redis.total-input": "Totale ingressi", + "redis.total-output": "Totale uscite", + + "redis.keyspace-hits": "Keyspace riuscite", + "redis.keyspace-misses": "Keyspace perse", + "redis.raw-info": "Info Redis Raw", + + "postgres": "Postgres", + "postgres.version": "Versione di PostgreSQL", + "postgres.raw-info": "Info Postgres Raw" +} diff --git a/public/language/it/admin/advanced/errors.json b/public/language/it/admin/advanced/errors.json new file mode 100644 index 0000000000..aff94ff327 --- /dev/null +++ b/public/language/it/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figura %1", + "error-events-per-day": "%1 eventi per giorno", + "error.404": "404 Non trovato", + "error.503": "503 Servizio non disponibile", + "manage-error-log": "Gestisci il registro degli errori", + "export-error-log": "Esporta il registro degli errori (CSV)", + "clear-error-log": "Cancella il registro degli errori", + "route": "Strada", + "count": "Conteggio", + "no-routes-not-found": "Evviva! Nessun errore 404!", + "clear404-confirm": "Sei sicuro di voler cancellare il registro degli errori 404?", + "clear404-success": "Errori \"404 Non trovato\" cancellati" +} \ No newline at end of file diff --git a/public/language/it/admin/advanced/events.json b/public/language/it/admin/advanced/events.json new file mode 100644 index 0000000000..6f202cbdb1 --- /dev/null +++ b/public/language/it/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Eventi", + "no-events": "Non ci sono eventi", + "control-panel": "Pannello di controllo eventi", + "delete-events": "Elimina eventi", + "confirm-delete-all-events": "Sei sicuro di voler eliminare tutti gli eventi registrati?", + "filters": "Filtri", + "filters-apply": "Applica filtri", + "filter-type": "Tipo evento", + "filter-start": "Data d'inizio", + "filter-end": "Data di fine", + "filter-perPage": "Per pagina" +} \ No newline at end of file diff --git a/public/language/it/admin/advanced/logs.json b/public/language/it/admin/advanced/logs.json new file mode 100644 index 0000000000..4347994ef0 --- /dev/null +++ b/public/language/it/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Registri", + "control-panel": "Pannello di controllo dei registri", + "reload": "Ricarica i registri", + "clear": "Cancella i registri", + "clear-success": "Registri cancellati!" +} \ No newline at end of file diff --git a/public/language/it/admin/appearance/customise.json b/public/language/it/admin/appearance/customise.json new file mode 100644 index 0000000000..10f7e5ef04 --- /dev/null +++ b/public/language/it/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "CSS/LESS personalizzato", + "custom-css.description": "Inserisci qui le tue dichiarazioni CSS /LESS, che saranno applicate dopo tutti gli altri stili.", + "custom-css.enable": "Abilita CSS/LESS personalizzati", + + "custom-js": "Javascript personalizzato", + "custom-js.description": "Inserisci qui il tuo javascript. Sarà eseguito dopo che la pagina è stata caricata completamente.", + "custom-js.enable": "Abilita Javascript personalizzato", + + "custom-header": "Intestazione personalizzata", + "custom-header.description": "Inserire l'HTML personalizzato qui (es. Meta Tags, ecc), che sarà allegato al <head>sezione del markup del tuo forum. I tag degli script sono permessi, ma sono sconsigliati, in quanto è disponibile la scheda Javascript personalizzato.", + "custom-header.enable": "Abilita Intestazione personalizzata", + + "custom-css.livereload": "Abilita Ricarica Istantanea", + "custom-css.livereload.description": "Abilitala per forzare tutte le sessioni su ogni dispositivo sotto il tuo account ad aggiornarsi ogni volta che si fa clic su Salva" +} \ No newline at end of file diff --git a/public/language/it/admin/appearance/skins.json b/public/language/it/admin/appearance/skins.json new file mode 100644 index 0000000000..fee91909b9 --- /dev/null +++ b/public/language/it/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Caricamento Skin...", + "homepage": "Pagina Iniziale", + "select-skin": "Seleziona la Skin", + "current-skin": "Skin corrente", + "skin-updated": "Skin aggiornata", + "applied-success": "%1 skin è stata applicata con successo", + "revert-success": "Skin riportata ai colori base" +} \ No newline at end of file diff --git a/public/language/it/admin/appearance/themes.json b/public/language/it/admin/appearance/themes.json new file mode 100644 index 0000000000..27ab006674 --- /dev/null +++ b/public/language/it/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Verifica dei temi installati.....", + "homepage": "Pagina Iniziale", + "select-theme": "Seleziona Tema", + "current-theme": "Tema corrente", + "no-themes": "Nessun tema installato trovato", + "revert-confirm": "Sei sicuro di voler ripristinare al tema predefinito di NodeBB?", + "theme-changed": "Tema cambiato", + "revert-success": "Hai correttamente ripristinato il tuo NodeBB al tema predefinito.", + "restart-to-activate": "Per favore, riorganizza e riavvia il tuo NodeBB per attivare completamente questo tema." +} \ No newline at end of file diff --git a/public/language/it/admin/dashboard.json b/public/language/it/admin/dashboard.json new file mode 100644 index 0000000000..329c11dd7c --- /dev/null +++ b/public/language/it/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Traffico Forum", + "page-views": "Pagine viste", + "unique-visitors": "Visitatori Unici", + "logins": "Accessi", + "new-users": "Nuovi utenti", + "posts": "Post", + "topics": "Discussioni", + "page-views-seven": "Ultimi 7 giorni", + "page-views-thirty": "Ultimi 30 giorni", + "page-views-last-day": "Ultime 24 ore", + "page-views-custom": "Intervallo data personalizzato", + "page-views-custom-start": "Inizio intervallo", + "page-views-custom-end": "Fine intervallo", + "page-views-custom-help": "Immettere un intervallo di date, delle pagine viste, che si desidera visualizzare. Se non è disponibile un selezionatore di date, il formato accettato è il seguente YYYY-MM-DD", + "page-views-custom-error": "Si prega di inserire un intervallo di date valido nel formato YYYY-MM-DD", + + "stats.yesterday": "Ieri", + "stats.today": "Oggi", + "stats.last-week": "Ultima settimana", + "stats.this-week": "Questa settimana", + "stats.last-month": "Ultimo mese", + "stats.this-month": "Questo mese", + "stats.all": "Sempre", + + "updates": "Aggiornamenti", + "running-version": "Stai eseguendo NodeBB v%1.", + "keep-updated": "Assicurati sempre che il tuo NodeBB sia aggiornato con le ultime patch di sicurezza e correzioni per bug.", + "up-to-date": "

Seiaggiornato

", + "upgrade-available": "

È stata rilasciata una nuova versione (v%1). Considera di aggiornare il tuo NodeBB.

", + "prerelease-upgrade-available": "

Questa è una versione pre-release sorpassata di NodeBB. È stata rilasciata una nuova versione (v%1). Considerare di aggiornare il tuo NodeBB.

", + "prerelease-warning": "

Questa è una versione pre-release di NodeBB. Possono verificarsi bug non intenzionali.

", + "fallback-emailer-not-found": "Email di recupero non trovata!", + "running-in-development": "Forum è in esecuzione in modalità sviluppo. Il forum potrebbe essere aperto a potenziali vulnerabilità; si prega di contattare il proprio amministratore di sistema.", + "latest-lookup-failed": "

Ricerca dell'ultima versione disponibile di NodeBB non riuscita

", + + "notices": "Annunci", + "restart-not-required": "Riavvio non richiesto", + "restart-required": "Riavvio richiesto", + "search-plugin-installed": "Ricerca Plugin installato", + "search-plugin-not-installed": "Ricerca Plugin non installato", + "search-plugin-tooltip": "Installa un plugin di ricerca dalla pagina plugin per attivare la funzionalità di ricerca", + + "control-panel": "Controllo sistema", + "rebuild-and-restart": "Riorganizza & Riavvia", + "restart": "Riavvia", + "restart-warning": "Riorganizzando o Riavviando il tuo NodeBB cadranno tutte le connessioni esistenti per alcuni secondi.", + "restart-disabled": "La Riorganizzazione e il Riavvio del tuo NodeBB sono stati disabilitati in quanto non sembra che tu lo stia eseguendo tramite il demone appropriato.", + "maintenance-mode": "Modalità Manutenzione", + "maintenance-mode-title": "Clicca qui per impostare la modalità di manutenzione per NodeBB", + "realtime-chart-updates": "Aggiornamento grafici in tempo reale", + + "active-users": "Utenti Attivi", + "active-users.users": "Utenti", + "active-users.guests": "Ospiti", + "active-users.total": "Totale", + "active-users.connections": "Connessioni", + + "guest-registered-users": "Ospite vs Utenti Registrati", + "guest": "Ospite", + "registered": "Registrati", + + "user-presence": "Presenza utente", + "on-categories": "Nella lista delle categorie", + "reading-posts": "Lettura post", + "browsing-topics": "Navigazione discussioni", + "recent": "Recenti", + "unread": "Non letto", + + "high-presence-topics": "Alta presenza discussioni", + "popular-searches": "Ricerche popolari", + + "graphs.page-views": "Pagine viste", + "graphs.page-views-registered": "Pagine viste Registrati", + "graphs.page-views-guest": "Pagine viste Ospite", + "graphs.page-views-bot": "Pagine viste Bot", + "graphs.unique-visitors": "Visitatori Unici", + "graphs.registered-users": "Utenti Registrati", + "graphs.guest-users": "Utenti ospiti", + "last-restarted-by": "Ultimo riavvio di", + "no-users-browsing": "Nessun utente sta navigando", + + "back-to-dashboard": "Torna alla dashboard", + "details.no-users": "Nessun utente si è iscritto nell'arco di tempo selezionato", + "details.no-topics": "Nessuna discussione è stata postata nell'arco di tempo selezionato", + "details.no-searches": "Nessuna ricerca è ancora stata fatta", + "details.no-logins": "Non sono stati registrati accessi nell'arco di tempo selezionato", + "details.logins-static": "NodeBB salva solo i dati di sessione per %1 giorni, quindi la tabella qui sotto mostrerà solo le sessioni attive più recenti", + "details.logins-login-time": "Tempo di accesso" +} diff --git a/public/language/it/admin/development/info.json b/public/language/it/admin/development/info.json new file mode 100644 index 0000000000..28b9bf2831 --- /dev/null +++ b/public/language/it/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Sei su %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodi hanno risposto entro %2ms!", + "host": "host", + "primary": "processi primari/eseguiti", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "memoria di processo", + "system-memory": "memoria di sistema", + "used-memory-process": "Memoria usata dal processo", + "used-memory-os": "Memoria di sistema usata", + "total-memory-os": "Memoria totale del sistema", + "load": "carico sistema", + "cpu-usage": "uso CPU", + "uptime": "tempo di caricamento", + + "registered": "Registrato", + "sockets": "Socket", + "guests": "Ospiti", + + "info": "Informazioni" +} \ No newline at end of file diff --git a/public/language/it/admin/development/logger.json b/public/language/it/admin/development/logger.json new file mode 100644 index 0000000000..2e3d19ccd7 --- /dev/null +++ b/public/language/it/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Impostazioni del Registratore", + "description": "Abilitando le caselle di controllo, riceverai i registri sul tuo terminale. Se specifichi un percorso, i registri saranno invece salvati in un file. La registrazione HTTP è utile per raccogliere statistiche su chi, quando e quali persone accedono al tuo forum. Oltre a registrare le richieste HTTP, possiamo anche registrare gli eventi socket.io. La registrazione Socket.io, in combinazione con il monitoraggio redis-cli, può essere molto utile per l'apprendimento dell'interno di NodeBBB.", + "explanation": "È sufficiente selezionare/deselezionare le impostazioni di registrazione per abilitare o disabilitare la registrazione al volo. Non è necessario riavviare.", + "enable-http": "Abilita la registrazione HTTP", + "enable-socket": "Abilita la registrazione degli eventi socket.io", + "file-path": "Percorso del file di registro", + "file-path-placeholder": "/path/to/log/file.log ::: lascia vuoto per accedere al tuo terminale", + + "control-panel": "Pannello di controllo registratore", + "update-settings": "Aggiornare impostazioni registratore" +} \ No newline at end of file diff --git a/public/language/it/admin/extend/plugins.json b/public/language/it/admin/extend/plugins.json new file mode 100644 index 0000000000..c5841b2ad2 --- /dev/null +++ b/public/language/it/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Di tendenza", + "installed": "Installato", + "active": "Attivo", + "inactive": "Inattivo", + "out-of-date": "Obsoleto", + "none-found": "Nessun plugin trovato.", + "none-active": "Nessun plugin attivo", + "find-plugins": "Trova Plugin", + + "plugin-search": "Ricerca Plugin", + "plugin-search-placeholder": "Ricerca per plugin...", + "submit-anonymous-usage": "Invia dati anonimi sull'utilizzo dei plugin.", + "reorder-plugins": "Riordina Plugin", + "order-active": "Ordina Plugin attivi", + "dev-interested": "Sei interessato a scrivere plugin per NodeBB?", + "docs-info": "La documentazione completa riguardante la creazione di plugin può essere trovata nel portale NodeBBB Docs Portal.", + + "order.description": "Alcuni plugin funzionano perfettamente quando sono inizializzati prima/dopo altri plugin.", + "order.explanation": "Caricamento dei Plugin nell'ordine qui specificato, dall'alto verso il basso", + + "plugin-item.themes": "Temi", + "plugin-item.deactivate": "Disattivare", + "plugin-item.activate": "Attivare", + "plugin-item.install": "Installa", + "plugin-item.uninstall": "Disinstallare", + "plugin-item.settings": "Impostazioni", + "plugin-item.installed": "Installato", + "plugin-item.latest": "Ultimo", + "plugin-item.upgrade": "Aggiornamento", + "plugin-item.more-info": "Per ulteriori informazioni:", + "plugin-item.unknown": "Sconosciuto", + "plugin-item.unknown-explanation": "Lo stato di questo plugin non può essere determinato, forse a causa di un errore di configurazione.", + "plugin-item.compatible": "Questo plugin funziona su NodeBB %1", + "plugin-item.not-compatible": "Questo plugin non ha dati di compatibilità, assicuratevi che funzioni prima di installarlo sul vostro ambiente di produzione.", + + "alert.enabled": "Plugin abilitato", + "alert.disabled": "Plugin disabilitato", + "alert.upgraded": "Plugin aggiornato", + "alert.installed": "Plugin installato", + "alert.uninstalled": "Plugin disinstallato", + "alert.activate-success": "Ricostruisci e riavvia NodeBB per attivare completamente questo plugin", + "alert.deactivate-success": "Plugin disattivato con successo", + "alert.upgrade-success": "Per favore, riorganizza e riavvia il tuo NodeBB per aggiornare completamente questo plugin.", + "alert.install-success": "Plugin installato correttamente, per favore attiva il plugin.", + "alert.uninstall-success": "Il plugin è stato disattivato e disinstallato con successo.", + "alert.suggest-error": "

NodeBB non è riuscito a contattare il gestore pacchetti, procedere con l'installazione dell'ultima versione?

il Server ha restituito (%1): %2", + "alert.package-manager-unreachable": "

NodeBB non è riuscito a contattare il gestore pacchetti, un aggiornamento non è suggerito in questo momento.

", + "alert.incompatible": "

La tua versione di NodeBB (v%1) è autorizzata solo per l'aggiornamento alla v%2 di questo plugin. Si prega di aggiornare il proprio NodeBB se si desidera installare una nuova versione di questo plugin.

", + "alert.possibly-incompatible": "

Nessuna informazione trovata sulla compatibilità

Questo plugin non ha specificato una versione specifica per l'installazione data la tua versione di NodeBB. La piena compatibilità non può essere garantita, e potrebbe causare l'avvio non corretto di NodeBB.

Nel caso in cui NodeBB non possa avviarsi correttamente:

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

Continuare l'installazione dell'ultima versione di questo plugin?

", + "alert.reorder": "Plugin riordinati", + "alert.reorder-success": "Per favore, riorganizza e riavvia il tuo NodeBB per completare completamente il processo.", + + "license.title": "Informazioni Licenza Plugin", + "license.intro": "Il plugin %1 è concesso in licenza ai sensi della %2. Si prega di leggere e comprendere i termini della licenza prima di attivare questo plugin.", + "license.cta": "Vuoi continuare con l'attivazione di questo plugin?" +} diff --git a/public/language/it/admin/extend/rewards.json b/public/language/it/admin/extend/rewards.json new file mode 100644 index 0000000000..7778999fbd --- /dev/null +++ b/public/language/it/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Premi", + "condition-if-users": "Se l'utente", + "condition-is": "È:", + "condition-then": "Allora:", + "max-claims": "Numero di volte che il premio è reclamabile", + "zero-infinite": "Inserisci 0 per infinito", + "delete": "Elimina", + "enable": "Abilita", + "disable": "Disabilita", + + "alert.delete-success": "Premi eliminati con successo", + "alert.no-inputs-found": "Premio illegale - immissioni non trovate!", + "alert.save-success": "Premi salvati con successo" +} \ No newline at end of file diff --git a/public/language/it/admin/extend/widgets.json b/public/language/it/admin/extend/widgets.json new file mode 100644 index 0000000000..6419142353 --- /dev/null +++ b/public/language/it/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Widget disponibili", + "explanation": "Selezionare un widget dal menu a discesa e poi trascinalo e rilascialo nell'area widget di un modello a sinistra.", + "none-installed": "Nessun widget trovato! Attivare il plugin essenziale per i widget nel pannello di controllo dei plugin.", + "clone-from": "Clona i widget da", + "containers.available": "Contenitori disponibili", + "containers.explanation": "Trascina e rilascia su qualsiasi widget attivo", + "containers.none": "Nessuno", + "container.well": "Bene", + "container.jumbotron": "Jumbotron", + "container.panel": "Pannello", + "container.panel-header": "Intestazione Pannello", + "container.panel-body": "Corpo Pannello", + "container.alert": "Avviso", + + "alert.confirm-delete": "Sei sicuro di voler eliminare questo widget?", + "alert.updated": "Widget aggiornati", + "alert.update-success": "Widget aggiornati con successo", + "alert.clone-success": "Widget clonati con successo", + + "error.select-clone": "Si prega di selezionare una pagina da clonare da", + + "title": "Titolo", + "title.placeholder": "Titolo (indicato solo su alcuni contenitori)", + "container": "Contenitori", + "container.placeholder": "Trascina un contenitore o inserisci l'html quì.", + "show-to-groups": "Mostra i gruppi", + "hide-from-groups": "Nascondi dai gruppi", + "hide-on-mobile": "Nascondi su mobile" +} \ No newline at end of file diff --git a/public/language/it/admin/manage/admins-mods.json b/public/language/it/admin/manage/admins-mods.json new file mode 100644 index 0000000000..076b8d9043 --- /dev/null +++ b/public/language/it/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Amministratori", + "global-moderators": "Moderatori Globali", + "moderators": "Moderatori", + "no-global-moderators": "Nessun Moderatore Globale", + "no-sub-categories": "Nessuna sottocategoria", + "subcategories": "%1 sottocategorie", + "no-moderators": "Nessun Moderatore", + "add-administrator": "Aggiungi Amministratore", + "add-global-moderator": "Aggiungi Moderatore Globale", + "add-moderator": "Aggiungi Moderatore" +} \ No newline at end of file diff --git a/public/language/it/admin/manage/categories.json b/public/language/it/admin/manage/categories.json new file mode 100644 index 0000000000..b46897636f --- /dev/null +++ b/public/language/it/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Impostazioni Categoria", + "privileges": "Privilegi", + + "name": "Nome Categoria", + "description": "Descrizione categoria", + "bg-color": "Colore sfondo", + "text-color": "Colore testo", + "bg-image-size": "Dimensione dell'immagine di sfondo", + "custom-class": "Classe personalizzata", + "num-recent-replies": "# di Repliche Recenti", + "ext-link": "Link esterni", + "subcategories-per-page": "Sottocategorie per pagina", + "is-section": "Tratta questa categoria come una sezione", + "post-queue": "Coda post", + "tag-whitelist": "Whitelist tag", + "upload-image": "Caricamento Immagine", + "delete-image": "Rimuove", + "category-image": "Immagine di Categoria", + "parent-category": "Categoria Padre", + "optional-parent-category": "Categoria Padre (Opzionale)", + "top-level": "Livello superiore", + "parent-category-none": "(Nessuna)", + "copy-parent": "Copia Padre", + "copy-settings": "Copia Impostazioni Da", + "optional-clone-settings": "Copia Impostazioni Dalla Categoria (Opzionale)", + "clone-children": "Copia le Categorie Figlie e Impostazioni", + "purge": "Elimina definitivamente categoria", + + "enable": "Abilita", + "disable": "Disabilita", + "edit": "Modifica", + "analytics": "Analitica", + "view-category": "Visualizza categoria", + "set-order": "Imposta ordine", + "set-order-help": "L'impostazione dell'ordine della categoria sposterà questa categoria in quell'ordine e aggiornerà l'ordine delle altre categorie, se necessario. L'ordine minimo è 1 che mette la categoria in cima.", + + "select-category": "Seleziona Categoria", + "set-parent-category": "Imposta la Categoria Padre", + + "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.warning": "Nota: 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-posting": "Privilegi di pubblicazione", + "privileges.section-moderation": "Privilegi di Moderazione", + "privileges.section-other": "Altro", + "privileges.section-user": "Utente", + "privileges.search-user": "Aggiungi Utente", + "privileges.no-users": "Nessun privilegio specifico dell'utente in questa categoria.", + "privileges.section-group": "Gruppo", + "privileges.group-private": "Questo gruppo è privato", + "privileges.inheritance-exception": "Questo gruppo non eredita privilegi dal gruppo utenti registrati", + "privileges.banned-user-inheritance": "Gli utenti bannati ereditano i privilegi dal gruppo utenti bannati", + "privileges.search-group": "Aggiungi gruppo", + "privileges.copy-to-children": "Copia i Figli", + "privileges.copy-from-category": "Copia da Categoria", + "privileges.copy-privileges-to-all-categories": "Copia tutte le Categorie", + "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-from": "Copia questo gruppo di privilegi da un altra categoria.", + "privileges.inherit": "Se l' utente registrato al gruppo viene concesso un privilegio specifico, tutti gli altri gruppi ricevono unprivilegio implicito,anche se non sono esplicitamente definiti / controllati. Questo privilegio implicito ti viene mostrato perché tutti gli utenti fanno parte digruppo di utenti registrati e quindi i privilegi per gruppi aggiuntivi non devono essere esplicitamente concessi.", + "privileges.copy-success": "Privilegi copiati!", + + "analytics.back": "Torna all'Elenco delle Categorie", + "analytics.title": "Statistiche per la categoria \"%1\"", + "analytics.pageviews-hourly": "Figura 1 – Vista delle visualizzazioni orarie per questa categoria", + "analytics.pageviews-daily": "Figura 2 – Vista delle visualizzazioni giornaliere per questa categoria", + "analytics.topics-daily": "Figura 3 – Discussioni giornaliere create in questa categoria", + "analytics.posts-daily": "Figura 4dash; Post giornalieri pubblicati in questa categoria", + + "alert.created": "Creato", + "alert.create-success": "Categoria creata con successo!", + "alert.none-active": "Hai una categoria non attiva.", + "alert.create": "Crea una Categoria", + "alert.confirm-purge": "

Vuoi davvero eliminare definitivamente questa categoria \"%1\"?

Attenzione!Tutte le discussioni e i post in questa categoria saranno eliminati definitivamente!

Eliminare definitivamente una categoria rimuoverà tutte le discussioni e i post ed eliminerà la categoria dal database. Se vuoi rimuovere una categoria temporaneamente, puoi invece \"disabilitare\" la categoria.", + "alert.purge-success": "Categoria eliminata definitivamente!", + "alert.copy-success": "Impostazioni copiate!", + "alert.set-parent-category": "Imposta la Categoria padre", + "alert.updated": "Categorie aggiornate", + "alert.updated-success": "ID categoria %1 aggiornati correttamente.", + "alert.upload-image": "Carica immagine categoria", + "alert.find-user": "Trova un Utente", + "alert.user-search": "Cerca un utente qui...", + "alert.find-group": "Trova un Gruppo", + "alert.group-search": "Cerca un gruppo qui...", + "alert.not-enough-whitelisted-tags": "I tag della whitelist sono meno dei tag minimi, è necessario creare più tag della whitelist!", + "collapse-all": "Collassa Tutto", + "expand-all": "Espandi Tutto", + "disable-on-create": "Disabilita alla creazione", + "no-matches": "Nessuna corrispondenza" +} \ No newline at end of file diff --git a/public/language/it/admin/manage/digest.json b/public/language/it/admin/manage/digest.json new file mode 100644 index 0000000000..9e66d7500c --- /dev/null +++ b/public/language/it/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Di seguito sarà visualizzato un elenco di statistiche e tempi di consegna del riepilogo.", + "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": "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 SendGrid.", + + "user": "Utente", + "subscription": "Tipo di Abbonamento", + "last-delivery": "Ultima consegna riuscita", + "default": "Sistema predefinito", + "default-help": "Sistema predefinito significa che l'utente non ha esplicitamente sovrascritto l'impostazione del forum globale per i riepiloghi, che attualmente è: & quot;%1"", + "resend": "Rinvia riepilogo", + "resend-all-confirm": "Sei sicuro di voler eseguire manualmente questa esecuzione del riepilogo?", + "resent-single": "Invio del riepilogo manuale completato", + "resent-day": "Rinvio riepilogo giornaliero", + "resent-week": "Rinvio del riepilogo settimanale", + "resent-biweek": "Re invio riepilogo bisettimanale", + "resent-month": "Rinvio del riepilogo mensile", + "null": "Mai", + "manual-run": "Esecuzione riepilogo manuale:", + + "no-delivery-data": "Nessun dato di consegna trovato" +} diff --git a/public/language/it/admin/manage/groups.json b/public/language/it/admin/manage/groups.json new file mode 100644 index 0000000000..eb86b9826d --- /dev/null +++ b/public/language/it/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Nome del gruppo", + "badge": "Badge", + "properties": "Proprieta", + "description": "Descrizione del gruppo", + "member-count": "Numero Membri", + "system": "Sistema", + "hidden": "Nascosto", + "private": "Privato", + "edit": "Modifica", + "delete": "Cancella", + "privileges": "Privilegi", + "download-csv": "CSV", + "search-placeholder": "Cerca", + "create": "Crea Gruppo", + "description-placeholder": "Una breve descrizione del tuo gruppo", + "create-button": "Crea", + + "alerts.create-failure": "Uh-Oh

C'è stato un problema nel creare il tuo gruppo. Riprova più tardi!

", + "alerts.confirm-delete": "Sei sicuro di voler eliminare questo gruppo?", + + "edit.name": "Nome", + "edit.description": "Descrizione", + "edit.user-title": "Titolo dei Membri", + "edit.icon": "Icona Gruppo", + "edit.label-color": "Colore etichetta gruppo", + "edit.text-color": "Colore testo gruppo", + "edit.show-badge": "Mostra Badge", + "edit.private-details": "Se abilitato, l'iscrizione ai gruppi richiede l'approvazione del proprietario del gruppo.", + "edit.private-override": "Attenzione: I gruppi privati sono disabilitati a livello di sistema, il che prevale su questa opzione.", + "edit.disable-join": "Disattiva le richieste di iscrizione", + "edit.disable-leave": "Impedisce agli utenti di lasciare il gruppo", + "edit.hidden": "Nascosto", + "edit.hidden-details": "Se abilitato, questo gruppo non apparirà nella lista dei gruppi, e gli utenti dovranno essere invitati manualmente", + "edit.add-user": "Aggiungi utente al gruppo", + "edit.add-user-search": "Cerca utenti", + "edit.members": "Lista Membri", + "control-panel": "Pannello di controllo dei gruppi", + "revert": "Ritorno", + + "edit.no-users-found": "Nessun utente trovato", + "edit.confirm-remove-user": "Sei sicuro di voler rimuovere questo utente?", + "edit.save-success": "Modifiche salvate!" +} \ No newline at end of file diff --git a/public/language/it/admin/manage/privileges.json b/public/language/it/admin/manage/privileges.json new file mode 100644 index 0000000000..27383c17c7 --- /dev/null +++ b/public/language/it/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Globale", + "admin": "Amministratore", + "group-privileges": "Privilegi di gruppo", + "user-privileges": "Privilegi utente", + "edit-privileges": "Modifica privilegi", + "select-clear-all": "Seleziona/Cancella tutto", + "chat": "Chat", + "upload-images": "Carica immagini", + "upload-files": "Carica file", + "signature": "Firma", + "ban": "Ban", + "mute": "Silenzioso", + "invite": "Invita", + "search-content": "Cerca contenuto", + "search-users": "Cerca utenti", + "search-tags": "Cerca tag", + "view-users": "Visualizza utenti", + "view-tags": "Visualizza tag", + "view-groups": "Visualizza gruppi", + "allow-local-login": "Accesso locale", + "allow-group-creation": "Crea gruppo", + "view-users-info": "Visualizza informazioni utenti", + "find-category": "Trova categoria", + "access-category": "Accesso categoria", + "access-topics": "Accesso discussioni", + "create-topics": "Crea discussioni", + "reply-to-topics": "Risposta alle discussioni", + "schedule-topics": "Pianificazione discussioni", + "tag-topics": "Tag discussioni", + "edit-posts": "Modifica i post", + "view-edit-history": "Visualizza cronologia modifiche", + "delete-posts": "Elimina post", + "view_deleted": "Visualizza post eliminati", + "upvote-posts": "Post negativi", + "downvote-posts": "Post votati negativamente", + "delete-topics": "Elimina discussioni", + "purge": "Elimina definitivamente", + "moderate": "Moderata", + "admin-dashboard": "Dashboard", + "admin-categories": "Categorie", + "admin-privileges": "Privilegi", + "admin-users": "Utenti", + "admin-admins-mods": "Amministratore & Moderatori", + "admin-groups": "Gruppi", + "admin-tags": "Tag", + "admin-settings": "Impostazioni", + + "alert.confirm-moderate": "Sei sicuro di voler concedere il privilegio di moderazione a questo gruppo di utenti? Questo gruppo è pubblico e tutti gli utenti possono iscriversi a piacimento.", + "alert.confirm-admins-mods": "Sei sicuro di voler concedere i privilegi di "Amministratori & Moderatori" a questo utente/gruppo? Gli utenti con questo privilegio possono promuovere e retrocedere altri utenti in posizioni privilegiate, compreso il super amministratore", + "alert.confirm-save": "Si prega di confermare l'intenzione di salvare questi privilegi", + "alert.saved": "Modifiche ai privilegi salvate e applicate", + "alert.confirm-discard": "Sei sicuro di voler annullare le modifiche ai privilegi?", + "alert.discarded": "Modifiche ai privilegi ignorate", + "alert.confirm-copyToAll": "Sei sicuro di voler applicare questa serie di %1 a tutte le categorie?", + "alert.confirm-copyToAllGroup": "Sei sicuro di voler applicare questa serie di %1 del gruppo a tutte le categorie?", + "alert.confirm-copyToChildren": "Sei sicuro di voler applicare questa serie di %1 a tutte le categorie discendenti (figli)?", + "alert.confirm-copyToChildrenGroup": "Sei sicuro di voler applicare questa serie di %1 del questo gruppo a tutte le categorie discendenti (figli)?", + "alert.no-undo": "Questa azione non può essere annullata.", + "alert.admin-warning": "Gli amministratori ottengono implicitamente tutti i privilegi", + "alert.copyPrivilegesFrom-title": "Seleziona una categoria da cui copiare", + "alert.copyPrivilegesFrom-warning": "Questo copierà 1% dalla categoria selezionata.", + "alert.copyPrivilegesFromGroup-warning": "Questo copierà la serie di %1 da questo gruppo dalla categoria selezionata." +} \ No newline at end of file diff --git a/public/language/it/admin/manage/registration.json b/public/language/it/admin/manage/registration.json new file mode 100644 index 0000000000..11d0329b87 --- /dev/null +++ b/public/language/it/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Coda", + "description": "Non ci sono utenti nella coda di registrazione.
Per abilitare questa funzione, vai in Impostazioni → Utente → Registrazione Utente e imposta Tipo Registrazione su \"Approvazione Amministratore\".", + + "list.name": "Nome", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Tempo", + "list.username-spam": "Frequenza: %1 Apparsi: %2 Confidenza: %3", + "list.email-spam": "Frequenza: %1 Apparsi: %2", + "list.ip-spam": "Frequenza: %1 Apparsi: %2", + + "invitations": "Inviti", + "invitations.description": "Di seguito è riportato l'elenco completo degli inviti inviati. Usa Ctrl-f per cercare attraverso la lista via email o nome utente.

Il nome utente sarà visualizzato a destra delle email per gli utenti che hanno riscattato i loro inviti.", + "invitations.inviter-username": "Nome dell'utente che invita", + "invitations.invitee-email": "Email dell'invitato", + "invitations.invitee-username": "Nome utente invitato (se registrato)", + + "invitations.confirm-delete": "Sei sicuro di voler eliminare questo invito?" +} \ No newline at end of file diff --git a/public/language/it/admin/manage/tags.json b/public/language/it/admin/manage/tags.json new file mode 100644 index 0000000000..836d2b525e --- /dev/null +++ b/public/language/it/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Il tuo forum non ha ancora discussioni con tag.", + "bg-color": "Colore di sfondo", + "text-color": "Colore del testo", + "description": "Seleziona i tag facendo clic o trascinando, utilizza CTRL per selezionare più tag.", + "create": "Crea tag", + "modify": "Modifica i tag", + "rename": "Rinomina i tag", + "delete": "Elimina i tag selezionati", + "search": "Ricerca per tag...", + "settings": "Impostazioni tag", + "name": "Nome Tag", + + "alerts.editing": "Modifica tag(s)", + "alerts.confirm-delete": "Vuoi eliminare i tag selezionati?", + "alerts.update-success": "Tag aggiornato!", + "reset-colors": "Reimposta i colori" +} \ No newline at end of file diff --git a/public/language/it/admin/manage/uploads.json b/public/language/it/admin/manage/uploads.json new file mode 100644 index 0000000000..5d992eec52 --- /dev/null +++ b/public/language/it/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Carica file", + "filename": "Nome file", + "usage": "Uso post", + "orphaned": "Orfano", + "size/filecount": "Dimensione / Numero file", + "confirm-delete": "Vuoi davvero cancellare questo file?", + "filecount": "%1 file", + "new-folder": "Nuova cartella", + "name-new-folder": "Inserisci un nome per la nuova cartella" +} \ No newline at end of file diff --git a/public/language/it/admin/manage/users.json b/public/language/it/admin/manage/users.json new file mode 100644 index 0000000000..3c9d678bef --- /dev/null +++ b/public/language/it/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Utenti", + "edit": "Azioni", + "make-admin": "Crea Amministratore", + "remove-admin": "Rimuovi Amministratore", + "validate-email": "Valida Email", + "send-validation-email": "Invia Email di Validazione", + "password-reset-email": "Invia Email per resettare la password", + "force-password-reset": "Forzare il reset della password e Logout dell'utente", + "ban": "Ban Utente(i)", + "temp-ban": "Ban Utente(i) Temporaneamente", + "unban": "Togli Ban Utente(i)", + "reset-lockout": "Reset Blocco", + "reset-flags": "Reset segnalazioni", + "delete": "EliminaUtente(i)", + "delete-content": "Elimina contenuto utente/i", + "purge": "Elimina Utenti e Contenuto", + "download-csv": "Scarica CSV", + "manage-groups": "Gestisci Gruppi", + "add-group": "Aggiungi Gruppo", + "create": "Crea utente", + "invite": "Invita via email", + "new": "Nuovo utente", + "filter-by": "Filtra per", + "pills.unvalidated": "Non Validato", + "pills.validated": "Convalidato", + "pills.banned": "Bannato", + + "50-per-page": "50 per pagina", + "100-per-page": "100 per pagina", + "250-per-page": "250 per pagina", + "500-per-page": "500 per pagina", + + "search.uid": "Da ID Utente", + "search.uid-placeholder": "Inserisci l'ID utente da cercare", + "search.username": "Da Nome Utente", + "search.username-placeholder": "Inserisci un nome utente da cercare", + "search.email": "Da Email", + "search.email-placeholder": "Inserisci un'email da cercare", + "search.ip": "Da Indirizzo IP", + "search.ip-placeholder": "Inserisci un indirizzo IP da cercare", + "search.not-found": "Utente non trovato!", + + "inactive.3-months": "3 Mesi", + "inactive.6-months": "6 Mesi", + "inactive.12-months": "12 Mesi", + + "users.uid": "id utente", + "users.username": "username", + "users.email": "email", + "users.no-email": "(nessuna email)", + "users.ip": "IP", + "users.postcount": "numero di post", + "users.reputation": "reputazione", + "users.flags": "segnalazioni", + "users.joined": "Iscrizione", + "users.last-online": "ultima volta online", + "users.banned": "bannato", + + "create.username": "Nome Utente", + "create.email": "Email", + "create.email-placeholder": "Email di questo utente", + "create.password": "Password", + "create.password-confirm": "Conferma Password", + + "temp-ban.length": "Lunghezza", + "temp-ban.reason": "Ragione (Opzionale)", + "temp-ban.hours": "Ore", + "temp-ban.days": "Giorni", + "temp-ban.explanation": "Inserisci la lunghezza del tempo di ban. Nota: quando il tempo è 0 il ban è considerato permanente.", + + "alerts.confirm-ban": "Vuoi realmente bannare questo utente permanentemente?", + "alerts.confirm-ban-multi": "Vuoi realmente bannare questi utenti permanentemente?", + "alerts.ban-success": "Utente(i) bannati!", + "alerts.button-ban-x": "Ban %1 utente(i)", + "alerts.unban-success": "Utente(i) a cui è stato tolto il ban!", + "alerts.lockout-reset-success": "Reset Blocchi(o)", + "alerts.flag-reset-success": "Segnalazione(i) resettate!", + "alerts.no-remove-yourself-admin": "Tu non puoi rimuovere te stesso da Amministratore!", + "alerts.make-admin-success": "L'utente adesso è amministratore.", + "alerts.confirm-remove-admin": "Vuoi realmente rimuovere questo amministratore?", + "alerts.remove-admin-success": "L'utente non è più amministratore.", + "alerts.make-global-mod-success": "L'utente adesso è moderatore globalmente.", + "alerts.confirm-remove-global-mod": "Vuoi realmente rimuovere questo moderatore globale?", + "alerts.remove-global-mod-success": "L'utente non è più moderatore globale.", + "alerts.make-moderator-success": "L'utente adesso è moderatore.", + "alerts.confirm-remove-moderator": "Vuoi realmente rimuovere questo moderatore?", + "alerts.remove-moderator-success": "L'utente non è più moderatore.", + "alerts.confirm-validate-email": "Vuoi realmente validare la/le mail di questo(i) utento(i)?", + "alerts.confirm-force-password-reset": "Sei sicuro di voler forzare il reset della password e disconnettere questo(i) utente(i)?", + "alerts.validate-email-success": "Email validate", + "alerts.validate-force-password-reset-success": "Le password degli utenti sono resettate e la loro sessione è revocata.", + "alerts.password-reset-confirm": "Vuoi realmente inviare il reset della(e) password via email per questo(i) utente(i)", + "alerts.password-reset-email-sent": "Email per reimpostare la password inviata.", + "alerts.confirm-delete": "Avvertimento!

Vuoi davvero eliminare l'utente(i)?

Questa azione non è reversibile! Solo l'account utente sarà eliminato, i suoi post e le sue discussioni rimarranno.

", + "alerts.delete-success": "Utente(i) Cancellato(i)", + "alerts.confirm-delete-content": "Avvertimento!Vuoi davvero eliminare il contenuto di questo utente(i)?

Questa azione non è reversibile! Gli account degli utenti rimarranno, ma i loro post e discussioni saranno eliminati.", + "alerts.delete-content-success": "Contenuto dell'utente(i) eliminato!", + "alerts.confirm-purge": "Avvertimento!

Vuoi davvero eliminare l'utente(i) e il suo contenuto?

Questa azione non è reversibile! Tutti i dati e i contenuti dell'utente saranno cancellati!

", + "alerts.create": "Utente creato", + "alerts.button-create": "Crea", + "alerts.button-cancel": "Cancella", + "alerts.error-passwords-different": "Le Password devono coincidere!", + "alerts.error-x": "Errore

%1

", + "alerts.create-success": "Utente creato!", + + "alerts.prompt-email": "Emails:", + "alerts.email-sent-to": "Un invito è stato inviato tramite mail a %1", + "alerts.x-users-found": "%1 utente(i) trovato(i), (%2 secondi)", + "export-users-started": "L'esportazione di utenti come csv potrebbe richiedere del tempo. Riceverai una notifica al termine.", + "export-users-completed": "Utenti esportati come csv, clicca qui per scaricare." +} \ No newline at end of file diff --git a/public/language/it/admin/menu.json b/public/language/it/admin/menu.json new file mode 100644 index 0000000000..82b3085b89 --- /dev/null +++ b/public/language/it/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboard", + "dashboard/overview": "Panoramica", + "dashboard/logins": "Accessi", + "dashboard/users": "Utenti", + "dashboard/topics": "Discussioni", + "dashboard/searches": "Ricerche", + "section-general": "Generale", + + "section-manage": "Gestisci", + "manage/categories": "Categorie", + "manage/privileges": "Privilegi", + "manage/tags": "Tabs", + "manage/users": "Utenti", + "manage/admins-mods": "Amministratori e Moderatori", + "manage/registration": "Coda di registrazione", + "manage/post-queue": "Coda post", + "manage/groups": "Gruppi", + "manage/ip-blacklist": "Lista degli IP bloccati", + "manage/uploads": "Uploads", + "manage/digest": "Riepilogo", + + "section-settings": "Impostazioni", + "settings/general": "Generale", + "settings/homepage": "Pagina Principale", + "settings/navigation": "Navigazione", + "settings/reputation": "Reputazione e segnalazioni", + "settings/email": "Email", + "settings/user": "Utenti", + "settings/group": "Gruppi", + "settings/guest": "Ospiti", + "settings/uploads": "Uploads", + "settings/languages": "Lingue", + "settings/post": "Post", + "settings/chat": "Chat", + "settings/pagination": "Paginazione", + "settings/tags": "Tabs", + "settings/notifications": "Notifiche", + "settings/api": "Accesso API", + "settings/sounds": "Suoni", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Avanzato", + + "settings.page-title": "%1 Impostazioni", + + "section-appearance": "Stile", + "appearance/themes": "Themi", + "appearance/skins": "Skins", + "appearance/customise": "Contenuto Personalizato (HTML/JS/CSS)", + + "section-extend": "Estendere", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Premi", + + "section-social-auth": "Autenticazione Social", + + "section-plugins": "Plugins", + "extend/plugins.install": "Installazione Plugin", + + "section-advanced": "Avanzato", + "advanced/database": "Base di dati", + "advanced/events": "Eventi", + "advanced/hooks": "Hooks", + "advanced/logs": "Registri", + "advanced/errors": "Errori", + "advanced/cache": "Cache", + "development/logger": "Registratore", + "development/info": "Informazioni", + + "rebuild-and-restart-forum": "Rebuild & Riavvia Forum", + "restart-forum": "Riavvia Forum", + "logout": "Esci", + "view-forum": "Vista Forum", + + "search.placeholder": "Impostazioni di ricerca", + "search.no-results": "Niente risultati...", + "search.search-forum": "Cerca nel forum per ", + "search.keep-typing": "Scrivi altro per vedere risultati...", + "search.start-typing": "Inizia a digitare per vedere i risultati...", + + "connection-lost": "Connessione da %1 è persa, tento di riconnettermi...", + + "alerts.version": "Avvio NodeBB v%1", + "alerts.upgrade": "Aggiornato a v%1" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/advanced.json b/public/language/it/admin/settings/advanced.json new file mode 100644 index 0000000000..2948f4fe4f --- /dev/null +++ b/public/language/it/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Modalità manutenzione", + "maintenance-mode.help": "Quando il forum è in modalità manutenzione, tutte le richieste saranno reindirizzate ad una pagina di attesa statica. Gli amministratori sono esenti da questo reindirizzamento e sono in grado di accedere al sito normalmente.", + "maintenance-mode.status": "Codice stato modalità manutenzione", + "maintenance-mode.message": "Messaggio di manutenzione", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Seleziona i gruppi che dovrebbero essere esenti dalla modalità di manutenzione", + "headers": "Intestazioni", + "headers.allow-from": "Imposta ALLOW-FROM per posizionare NodeBBB in un iFrame", + "headers.csp-frame-ancestors": "Imposta l'intestazione Content-Security-Policy frame-ancestors su Place NodeBB in un iFrame", + "headers.csp-frame-ancestors-help": "'nessuno', 'uguale'(predefinito) o elenco di URI da consentire.", + "headers.powered-by": "Personalizza l'intestazione \"Fornito da\" inviata da NodeBBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Espressione regolare", + "headers.acao-help": "Per negare l'accesso a tutti i siti, lascia vuoto", + "headers.acao-regex-help": "Inserisci qui le espressioni regolari per abbinare le origini dinamiche. Per negare l'accesso a tutti i siti, lascia vuoto", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "Se abilitato (impostazione predefinita), imposterà l'intestazione su require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Consente di impostare l'intestazione dei criteri di autorizzazione, ad esempio \"geolocation=*, camera=()\", guardare questo per maggiori informazioni.", + "hsts": "Rigorosa sicurezza trasporto", + "hsts.enabled": "Abilita HSTS (consigliato)", + "hsts.maxAge": "Età massima HSTS", + "hsts.subdomains": "Includi i sottodomini nell'intestazione HSTS", + "hsts.preload": "Consenti la precarica dell'intestazione HSTS", + "hsts.help": "Se abilitato, sarà impostata un'intestazione HSTS per questo sito. Puoi scegliere di includere sottodomini e segnalazioni di precaricamento nell'intestazione. In caso di dubbio, puoi lasciarle deselezionate. Più informazioni ", + "traffic-management": "Gestione Traffico", + "traffic.help": "NodeBB utilizza un modulo che nega automaticamente le richieste in situazioni di traffico elevato. È possibile regolare queste impostazioni qui, anche se le impostazioni predefinite sono un buon punto di partenza.", + "traffic.enable": "Abilita Gestione Traffico", + "traffic.event-lag": "Soglia ritardo ciclo eventi (in millisecondi)", + "traffic.event-lag-help": "L'abbassamento di questo valore diminuisce i tempi di attesa per il caricamento della pagina, ma mostrerà il messaggio \"carico eccessivo\" a più utenti. (Necessario riavviare)", + "traffic.lag-check-interval": "Intervallo di controllo (in millisecondi)", + "traffic.lag-check-interval-help": "L'abbassamento di questo valore fa sì che NodeBBB diventi più sensibile ai picchi di carico, ma può anche far sì che il controllo diventi troppo sensibile. (Necessario riavviare)", + + "sockets.settings": "Impostazioni WebSocket", + "sockets.max-attempts": "Tentativi massimi di riconnessione ", + "sockets.default-placeholder": "Predefinito: %1", + "sockets.delay": "Ritardo di riconnessione", + + "analytics.settings": "Impostazioni di analisi", + "analytics.max-cache": "Analisi valore massimo cache", + "analytics.max-cache-help": "Nelle installazioni ad alto traffico, la cache potrebbe esaurirsi continuamente se ci sono più utenti attivi contemporanei rispetto al valore Max Cache. (Riavvio richiesto)", + "compression.settings": "Impostazioni compressione", + "compression.enable": "Abilita compressione", + "compression.help": "Questa impostazione abilita la compressione gzip. Per un sito Web ad alto traffico in produzione, il modo migliore per implementare la compressione a livello di proxy inverso. È possibile abilitarlo qui a scopo di test." +} \ No newline at end of file diff --git a/public/language/it/admin/settings/api.json b/public/language/it/admin/settings/api.json new file mode 100644 index 0000000000..74e57044a0 --- /dev/null +++ b/public/language/it/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Token", + "settings": "Impostazioni", + "lead-text": "Da questa pagina è possibile configurare l'accesso alle API di scrittura in NodeBB.", + "intro": "Per impostazione predefinita, l'API di scrittura autentica gli utenti in base al cookie di sessione, ma NodeBB supporta anche l'autenticazione Bearer tramite token generati tramite questa pagina.", + "docs": "Clicca qui per accedere alle specifiche complete dell'API", + + "require-https": "Richiedi utilizzo API solo tramite HTTPS", + "require-https-caveat": "Nota:Alcune installazioni che coinvolgono bilanciatori del carico possono inviare tramite proxy le loro richieste a NodeBB utilizzando HTTP, nel qual caso questa opzione dovrebbe rimanere disabilitata.", + + "uid": "ID utente", + "uid-help-text": "Specificare un ID utente da associare a questo token. Se l'ID utente è 0, sarà considerato un token master, che può assumere l'identità di altri utenti in base al parametro _uid", + "description": "Descrizione", + "no-description": "Nessuna descrizione specificata.", + "token-on-save": "Il token sarà generato una volta salvato il modulo" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/chat.json b/public/language/it/admin/settings/chat.json new file mode 100644 index 0000000000..0faf77ab75 --- /dev/null +++ b/public/language/it/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Impostazioni chat", + "disable": "Disabilita chat", + "disable-editing": "Disabilita modifica/cancellazione messaggio chat", + "disable-editing-help": "Gli amministratori e i moderatori globali sono esenti da questa restrizione.", + "max-length": "Lunghezza massima dei messaggi della chat", + "max-room-size": "Numero massimo di utenti nelle stanza chat", + "delay": "Tempo tra i messaggi della chat in millisecondi", + "notification-delay": "Ritardo di notifica per i messaggi di chat. (0 per nessun ritardo)", + "restrictions.seconds-edit-after": "Numero di secondi in cui un messaggio di chat rimane modificabile. (0 disabilitato)", + "restrictions.seconds-delete-after": "Numero di secondi in cui un messaggio di chat rimane cancellabile. (0 disabilitato)" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/cookies.json b/public/language/it/admin/settings/cookies.json new file mode 100644 index 0000000000..be320e5cdf --- /dev/null +++ b/public/language/it/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Consenso UE", + "consent.enabled": "Abilitato", + "consent.message": "Messaggio di notifica", + "consent.acceptance": "Messaggio di accettazione", + "consent.link-text": "Testo del link all'informativa sulla privacy", + "consent.link-url": "URL del link all'informativa sulla privacy", + "consent.blank-localised-default": "Lascia vuoto per usare i valori predefiniti localizzati di NodeBB", + "settings": "Impostazioni", + "cookie-domain": "Dominio cookie di sessione", + "max-user-sessions": "Sessioni attive massime per utente", + "blank-default": "Lascia vuoto per predefinito" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/email.json b/public/language/it/admin/settings/email.json new file mode 100644 index 0000000000..adbca929b5 --- /dev/null +++ b/public/language/it/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Impostazioni Email", + "address": "Indirizzo Email", + "address-help": "Il seguente indirizzo email si riferisce all'email che il destinatario vedrà nei campi \"Da\" e \"Rispondi a\".", + "from": "Da Nome", + "from-help": "Il nome da visualizzare nell'email.", + + "confirmation-settings": "Conferma", + "confirmation.expiry": "Ore per mantenere valido il link di conferma dell'email", + + "smtp-transport": "Trasporto SMTP", + "smtp-transport.enabled": "Abilita trasporto SMTP", + "smtp-transport-help": "Puoi selezionare da un elenco di servizi noti o inserirne uno personalizzato.", + "smtp-transport.service": "Seleziona un servizio", + "smtp-transport.service-custom": "Servizio personalizzato", + "smtp-transport.service-help": "Selezionare il nome di un servizio per utilizzare le informazioni note su di esso. In alternativa, selezionare "Servizio personalizzato" e inserire i dettagli qui sotto.", + "smtp-transport.gmail-warning1": "Se si utilizza GMail come provider di posta elettronica, è necessario generare una "Password dell'app" affinché NodeBB possa autenticarsi con successo. Puoi generarne una alla pagina Password dell'app .", + "smtp-transport.gmail-warning2": "Per ulteriori informazioni su questa soluzione alternativa, si prega di consultare questo articolo NodeMailer sulla questione. Un'alternativa sarebbe utilizzare un plug-in di posta elettronica di terze parti come SendGrid, Mailgun, ecc. Sfoglia i plugin disponibili qui.", + "smtp-transport.auto-enable-toast": "Sembra che tu stia configurando un trasporto SMTP. Abbiamo abilitato l'opzione \"Trasporto SMTP\" per te.", + "smtp-transport.host": "Host SMTP", + "smtp-transport.port": "Porta SMTP", + "smtp-transport.security": "Sicurezza connessione", + "smtp-transport.security-encrypted": "Crittografata", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Nessuna", + "smtp-transport.username": "Nome utente", + "smtp-transport.username-help": "Per il servizio Gmail, inserisci qui l'indirizzo email completo, specialmente se stai usando un dominio gestito da Google Apps.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Abilita le connessioni in pool", + "smtp-transport.pool-help": "Il pooling delle connessioni impedisce a NodeBB di creare una nuova connessione per ogni email. Questa opzione si applica solo se è abilitato il trasporto SMTP.", + + "template": "Modifica Modello Email", + "template.select": "Seleziona Modello Email", + "template.revert": "Torna all'originale", + "testing": "Prova Email", + "testing.select": "Seleziona Modello Email", + "testing.send": "Invia Email di prova", + "testing.send-help": "L'email di prova sarà inviata all'indirizzo email dell'utente attualmente connesso.", + "subscriptions": "Email riepilogo", + "subscriptions.disable": "Disabilita email riepilogo", + "subscriptions.hour": "Orario riepilogo", + "subscriptions.hour-help": "Si prega di inserire un numero che rappresenta l'ora per l'invio dell'email programmate (es. 0per mezzanotte, 17per le 17: 00). Tieni presente che questa è l'ora secondo il server stesso, e potrebbe non combaciare esattamente al tuo orologio di sistema.
L'orario approssimativo del server è:
La prossima trasmissione giornaliera è prevista alle ", + "notifications.remove-images": "Rimuovi le immagini dalle notifiche email", + "require-email-address": "Richiedere ai nuovi utenti di specificare un indirizzo email", + "require-email-address-warning": "Per impostazione predefinita, gli utenti possono rinunciare a inserire un indirizzo email lasciando il campo vuoto. Abilitare questa opzione significa che devono inserire un indirizzo email per procedere con la registrazione. Non assicura che l'utente inserisca un indirizzo email reale, e nemmeno un indirizzo che possiede.", + "send-validation-email": "Invia email di convalida quando un'email viene aggiunta o modificata", + "include-unverified-emails": "Invia email a destinatari che non hanno confermato esplicitamente le loro email", + "include-unverified-warning": "Per impostazione predefinita, gli utenti con email associate al loro account sono già stati verificati, ma ci sono situazioni in cui ciò non è vero (ad esempio accessi SSO, vecchi utenti, ecc.). Abilita questa impostazione a tuo rischio e pericolo – l'invio di email a indirizzi non verificati può essere una violazione delle leggi regionali anti-spam.", + "prompt": "Chiedi agli utenti di inserire o confermare le loro email", + "prompt-help": "Se un utente non ha impostato un'email, o la sua email non è confermata, sarà mostrato un avviso sullo schermo.", + "sendEmailToBanned": "Invia email agli utenti anche se sono stati bannati" +} diff --git a/public/language/it/admin/settings/general.json b/public/language/it/admin/settings/general.json new file mode 100644 index 0000000000..c556adf46c --- /dev/null +++ b/public/language/it/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Impostazioni Sito", + "title": "Titolo Sito", + "title.short": "Titolo abbreviato", + "title.short-placeholder": "Se non specifichi un titolo abbreviato, verrà utilizzato il titolo completo", + "title.url": "Link URL Titolo", + "title.url-placeholder": "L'URL del titolo del sito", + "title.url-help": "Quando il titolo viene cliccato, invia gli utenti a questo indirizzo. Se lasciato vuoto, l'utente sarà inviato all'indice del forum.
Nota: Questo non è l'URL esterno usato nelle email, ecc. Questo è impostato dalla proprietà url in config.json", + "title.name": "Il Nome della Comunità", + "title.show-in-header": "Mostra Titolo Sito nell'Intestazione", + "browser-title": "Titolo Browser", + "browser-title-help": "Se nessun titolo browser è specificato, sarà utilizzato il titolo del sito", + "title-layout": "Layout del Titolo", + "title-layout-help": "Definire come sarà strutturato il titolo del browser, ad es. {pageTitle} | {browserTitle}", + "description.placeholder": "Una breve descrizione della tua comunità", + "description": "Descrizione del sito", + "keywords": "Parole chiave del sito", + "keywords-placeholder": "Parole chiave che descrivono la vostra comunità, separate da virgole", + "logo": "Logo del sito", + "logo.image": "Immagine", + "logo.image-placeholder": "Percorso del logo da visualizzare sull'intestazione del forum", + "logo.upload": "Carica", + "logo.url": "Link URL Logo", + "logo.url-placeholder": "L'URL del logo del sito", + "logo.url-help": "Quando il logo viene cliccato, invia gli utenti a questo indirizzo. Se lasciato vuoto, l'utente sarà inviato all'indice del forum.
Nota: Questo non è l'URL esterno usato nelle email, ecc. Questo è impostato dalla proprietà url in config.json", + "logo.alt-text": "Testo alternativo", + "log.alt-text-placeholder": "Testo alternativo per l'accessibilità", + "favicon": "Favicon", + "favicon.upload": "Carica", + "pwa": "App Web Progressiva", + "touch-icon": "Icona Touch", + "touch-icon.upload": "Carica", + "touch-icon.help": "Dimensioni e formato consigliati: 512x512, solo formato PNG. Se non è specificata alcuna icona touch, NodeBB tornerà a utilizzare la favicon.", + "maskable-icon": "Icona Mascherabile (Schermata Iniziale)", + "maskable-icon.help": "Dimensioni e formato consigliati: 512x512, solo formato PNG. Se non è specificata alcuna icona mascherabile, NodeBB tornerà a utilizzare l'Icona Touch.", + "outgoing-links": "Collegamenti in uscita", + "outgoing-links.warning-page": "Usa pagina di avviso collegamenti in uscita", + "search": "Cerca", + "search-default-in": "Cerca in", + "search-default-in-quick": "Ricerca rapida in", + "search-default-sort-by": "Ordina per", + "outgoing-links.whitelist": "Domini nella whitelist per aggirare la pagina di avviso", + "site-colors": "Colore Metadati del Sito", + "theme-color": "Colore del Tema", + "background-color": "Colore di sfondo", + "background-color-help": "Colore utilizzato per lo sfondo della schermata iniziale quando il sito Web è installato come PWA", + "undo-timeout": "Annulla timeout", + "undo-timeout-help": "Alcune operazioni come lo spostamento delle discussioni permetteranno al moderatore di annullare la sua azione entro un certo periodo di tempo. Imposta a 0 per disabilitare completamente l'annullamento.", + "topic-tools": "Strumenti discussione" +} diff --git a/public/language/it/admin/settings/group.json b/public/language/it/admin/settings/group.json new file mode 100644 index 0000000000..651295843c --- /dev/null +++ b/public/language/it/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Generale", + "private-groups": "Gruppi Privati", + "private-groups.help": "Se abilitato, l'iscrizione ai gruppi richiede l'approvazione del proprietario del gruppo (Predefinito: abilitato)", + "private-groups.warning": "Attenzione! Se questa opzione è disattivata e si hanno gruppi privati, questi diventano automaticamente pubblici.", + "allow-multiple-badges": "Consenti più badge", + "allow-multiple-badges-help": "Questo flag può essere usato per consentire agli utenti di selezionare più badge di gruppo, richiede il supporto del tema.", + "max-name-length": "Lunghezza massima Nome Gruppo", + "max-title-length": "Lunghezza massima Titolo Gruppo", + "cover-image": "Immagine Copertina Gruppo", + "default-cover": "Immagini Copertina Predefinite", + "default-cover-help": "Aggiungi immagini di copertina separate da virgole per i gruppi che non hanno caricato un'immagine copertina." +} \ No newline at end of file diff --git a/public/language/it/admin/settings/guest.json b/public/language/it/admin/settings/guest.json new file mode 100644 index 0000000000..273aa7bf3f --- /dev/null +++ b/public/language/it/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Impostazioni", + "handles.enabled": "Consenti nome utente ospite", + "handles.enabled-help": "Questa opzione mostra un nuovo campo che permette agli ospiti di scegliere un nome da associare ad ogni post che fanno. Se disabilitata, saranno semplicemente chiamati \"Ospite\".", + "topic-views.enabled": "Consentire agli ospiti di aumentare il numero di visualizzazioni della discussione", + "reply-notifications.enabled": "Consenti agli ospiti di generare notifiche di risposta" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/homepage.json b/public/language/it/admin/settings/homepage.json new file mode 100644 index 0000000000..93c5b3e964 --- /dev/null +++ b/public/language/it/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Pagina Iniziale", + "description": "Scegliere quale pagina visualizzare quando gli utenti navigano all'URL principale del forum.", + "home-page-route": "Percorso Pagina Iniziale", + "custom-route": "Percorso personalizzato", + "allow-user-home-pages": "Consenti Pagina Iniziale Utente", + "home-page-title": "Titolo della pagina iniziale (impostazione predefinita \"Home\")" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/languages.json b/public/language/it/admin/settings/languages.json new file mode 100644 index 0000000000..321d12f8e4 --- /dev/null +++ b/public/language/it/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Impostazioni lingua", + "description": "La lingua predefinita determina le impostazioni della lingua per tutti gli utenti che visitano il tuo forum.
I singoli utenti possono sovrascrivere la lingua predefinita nella pagina delle impostazioni dell'account.", + "default-language": "Lingua predefinita", + "auto-detect": "Rilevazione automatica della lingua impostata per gli Ospiti" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/navigation.json b/public/language/it/admin/settings/navigation.json new file mode 100644 index 0000000000..75607ccce6 --- /dev/null +++ b/public/language/it/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icona:", + "change-icon": "modifica", + "route": "Percorso:", + "tooltip": "Suggerimento:", + "text": "Testo:", + "text-class": "Classe Testo: opzionale", + "class": "Classe: opzionale", + "id": "ID: opzionale", + + "properties": "Proprietà:", + "groups": "Gruppi:", + "open-new-window": "Apri in una nuova finestra", + "dropdown": "Menu a tendina", + "dropdown-placeholder": "Posiziona le voci del tuo menu a tendina in basso, ad esempio:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Elimina", + "btn.disable": "Disabilita", + "btn.enable": "Abilita", + + "available-menu-items": "Voci di Menu disponibili", + "custom-route": "Percorso personalizzato", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/it/admin/settings/notifications.json b/public/language/it/admin/settings/notifications.json new file mode 100644 index 0000000000..9574902ce0 --- /dev/null +++ b/public/language/it/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifiche", + "welcome-notification": "Notifica di benvenuto", + "welcome-notification-link": "Collegamento a Notifica di benvenuto", + "welcome-notification-uid": "Notifica di benvenuto utente (UID)", + "post-queue-notification-uid": "Coda post utente (UID)" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/pagination.json b/public/language/it/admin/settings/pagination.json new file mode 100644 index 0000000000..1292c4fbba --- /dev/null +++ b/public/language/it/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Impostazioni di impaginazione", + "enable": "Impaginare discussioni e post al posto di usare lo scorrimento infinito", + "posts": "Impaginazione dei post", + "topics": "Impaginazione Discussione", + "posts-per-page": "Post per pagina", + "max-posts-per-page": "Numero massimo di post per pagina", + "categories": "Categoria Impaginazione", + "topics-per-page": "Discussioni per pagina", + "max-topics-per-page": "Numero massimo di discussioni per pagina", + "categories-per-page": "Categorie per pagina" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/post.json b/public/language/it/admin/settings/post.json new file mode 100644 index 0000000000..76cd524c6b --- /dev/null +++ b/public/language/it/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Ordinamento Post", + "sorting.post-default": "Ordinamento Post Predefinito", + "sorting.oldest-to-newest": "Dal meno recente al più recente", + "sorting.newest-to-oldest": "Dal più recente al meno recente", + "sorting.most-votes": "Più Voti", + "sorting.most-posts": "Più Post", + "sorting.topic-default": "Ordinamento Discussione Predefinito", + "length": "Lunghezza Post", + "post-queue": "Coda post", + "restrictions": "Restrizioni pubblicazione", + "restrictions-new": "Restrizioni Nuovo Utente", + "restrictions.post-queue": "Abilita coda post", + "restrictions.post-queue-rep-threshold": "Reputazione necessaria a superare la coda dei post", + "restrictions.groups-exempt-from-post-queue": "Seleziona i gruppi che dovrebbero essere esclusi dalla coda dei post", + "restrictions-new.post-queue": "Abilita le restrizioni per i nuovi utenti", + "restrictions.post-queue-help": "Abilitando la coda dei post, i post dei nuovi utenti, saranno messi in coda per l'approvazione", + "restrictions-new.post-queue-help": "Abilitando le restrizioni per i nuovi utenti verranno impostate le restrizioni sui post dei nuovi utenti", + "restrictions.seconds-between": "Numero di secondi tra i post", + "restrictions.seconds-between-new": "Numero di secondi tra i post per i nuovi utenti", + "restrictions.rep-threshold": "Soglia di reputazione dopo la quale queste restrizioni vengono rimosse", + "restrictions.seconds-before-new": "Secondi dopo i quali un nuovo utente può creare il suo primo post", + "restrictions.seconds-edit-after": "Numero di secondi per i quali il post rimane modificabile (imposta a 0 per disabilitare)", + "restrictions.seconds-delete-after": "Numero di secondi per i quali il post rimane cancellabile (imposta a 0 per disabilitare)", + "restrictions.replies-no-delete": "Numero di risposte dopo le quali l'utente non può più cancellare le proprie discussioni (imposta a 0 per disabilitare)", + "restrictions.min-title-length": "Lunghezza Minima Titolo", + "restrictions.max-title-length": "Lunghezza Massima Titolo", + "restrictions.min-post-length": "Lunghezza Minima Post", + "restrictions.max-post-length": "Lunghezza Massima Post", + "restrictions.days-until-stale": "Giorni prima che l'argomento sia considerato vecchio", + "restrictions.stale-help": "Se un argomento è considerato \"non aggiornato\", verrà mostrato un avviso agli utenti che tentano di rispondere a tale argomento.", + "timestamp": "Data e Ora", + "timestamp.cut-off": "Data di interruzione (in giorni)", + "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).
(Predefinito: 30, o un mese).Impostare su 0 per visualizzare sempre le date, lasciare vuoto per visualizzare sempre i tempi relativi.", + "timestamp.necro-threshold": "Necro Threshold (in giorni)", + "timestamp.necro-threshold-help": "Un messaggio verrà mostrato tra i post se il tempo tra loro è più lungo della soglia necro. (Predefinito: 7, o una settimana). Impostare su 0 per disabilitare.", + "timestamp.topic-views-interval": "Incremento dell'intervallo di visualizzazione della discussione (in minuti)", + "timestamp.topic-views-interval-help": "Le visualizzazioni della discussione aumenteranno solo una volta ogni X minuti, come definito da questa impostazione.", + "teaser": "Post Inopportuno", + "teaser.last-post": "Ultimo – Mostra l'ultimo post, incluso il post originale, se non ci sono risposte", + "teaser.last-reply": "Ultimo – Mostra l'ultima risposta o un segnaposto \"Nessuna risposta\" se non risposto", + "teaser.first": "Primo", + "showPostPreviewsOnHover": "Mostra un'anteprima dei post quando il mouse ci passa sopra", + "unread": "Impostazioni non Lette", + "unread.cutoff": "Giorni di interruzione non letti", + "unread.min-track-last": "Post minimi nell'argomento prima del monitoraggio dell'ultima lettura", + "recent": "Impostazioni Recenti", + "recent.max-topics": "Numero massimo di discussioni in atto/recenti", + "recent.categoryFilter.disable": "Disabilita il filtro delle discussioni nelle categorie ignorate nella /pagina recente", + "signature": "Impostazioni della Firma", + "signature.disable": "Disabilita le firme", + "signature.no-links": "Disabilita i collegamenti nelle firme", + "signature.no-images": "Disabilita le immagini nelle firme", + "signature.hide-duplicates": "Nascondi firme duplicate nelle discussioni", + "signature.max-length": "Lunghezza massima della firma", + "composer": "Impostazioni del compositore", + "composer-help": "Le seguenti impostazioni regolano la funzionalità e/o l'aspetto del post compositore mostrato\n\t\t\t\tagli utenti quando creano nuove discussioni o rispondono a discussioni esistenti.", + "composer.show-help": "Mostra la scheda \"Aiuto\"", + "composer.enable-plugin-help": "Consenti ai plug-in di aggiungere contenuti alla scheda Guida", + "composer.custom-help": "Testo di aiuto personalizzato", + "backlinks": "Backlink", + "backlinks.enabled": "Abilita backlink discussione", + "backlinks.help": "Se un post fa riferimento ad un altra discussione, un link al post sarà inserito nella discussione di riferimento in quel momento.", + "ip-tracking": "Monitoraggio IP", + "ip-tracking.each-post": "Traccia l'indirizzo IP per ogni post", + "enable-post-history": "Abilita Cronologia post" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/reputation.json b/public/language/it/admin/settings/reputation.json new file mode 100644 index 0000000000..e98fc9bcec --- /dev/null +++ b/public/language/it/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Impostazioni reputazione", + "disable": "Disabilita sistema reputazione", + "disable-down-voting": "Disabilita voto negativo", + "votes-are-public": "Tutti i voti sono pubblici", + "thresholds": "Soglie di attività", + "min-rep-upvote": "Reputazione minima per votare positivamente i post", + "upvotes-per-day": "Voti positivi al giorno (impostare a 0 per i voti positivi illimitati)", + "upvotes-per-user-per-day": "Voti positivi per utente al giorno (impostare a 0 per voti positivi illimitati)", + "min-rep-downvote": "Reputazione minima per votare negativamente i post", + "downvotes-per-day": "Voti negativi al giorno (imposta a 0 per voti negativi illimitati)", + "downvotes-per-user-per-day": "Voti negativi per utenti al giorno (imposta a 0 per voti negativi illimitati)", + "min-rep-chat": "Reputazione minima per inviare messaggi di chat", + "min-rep-flag": "Reputazione minima per segnalare i post", + "min-rep-website": "Reputazione minima per aggiungere \"Sito Web\" al profilo utente", + "min-rep-aboutme": "Reputazione minima per aggiungere \"Su di me\" al profilo utente", + "min-rep-signature": "Reputazione minima per aggiungere \"Firma\" al profilo utente", + "min-rep-profile-picture": "Reputazione minima per aggiungere \"Immagine profilo\" al profilo utente", + "min-rep-cover-picture": "Reputazione minima per aggiungere \"Immagine copertina\" al profilo utente", + + "flags": "Impostazioni segnalazioni", + "flags.limit-per-target": "Numero massimo di volte che qualcosa può essere segnalato", + "flags.limit-per-target-placeholder": "Predefinito: 0", + "flags.limit-per-target-help": "Quando un post o un utente viene segnalato più volte, ogni segnalazione aggiuntiva è considerata una "report" e aggiunto alla segnalazione originale. Imposta questa opzione su un numero diverso da zero per limitare il numero di rapporti che un elemento può ricevere.", + "flags.auto-flag-on-downvote-threshold": "Numero di voti negativi per contrassegnare automaticamente i post (impostare a 0 per disabilitare, predefinito: 0)", + "flags.auto-resolve-on-ban": "Risolvi automaticamente tutti i ticket di un utente quando vengono bannati", + "flags.action-on-resolve": "Esegui le seguenti operazioni quando una segnalazione viene risolta", + "flags.action-on-reject": "Esegui le seguenti operazioni quando una segnalazione viene rifiutata", + "flags.action.nothing": "Non fare nulla", + "flags.action.rescind": "Annulla l'invio della notifica ai moderatori/amministratori" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/social.json b/public/language/it/admin/settings/social.json new file mode 100644 index 0000000000..0a2eeb5181 --- /dev/null +++ b/public/language/it/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Condivisione Post", + "info-plugins-additional": "I plugin possono aggiungere reti aggiuntive per la condivisione dei post.", + "save-success": "Salvato con successo Reti Condivisione Post!" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/sockets.json b/public/language/it/admin/settings/sockets.json new file mode 100644 index 0000000000..43ea82f06c --- /dev/null +++ b/public/language/it/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Impostazioni riconnessione", + "max-attempts": "Massimi tentativi di riconnessione", + "default-placeholder": "Predefinito: %1", + "delay": "Ritardo riconnessione" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/sounds.json b/public/language/it/admin/settings/sounds.json new file mode 100644 index 0000000000..0392f2954b --- /dev/null +++ b/public/language/it/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifiche", + "chat-messages": "Messaggi di chat", + "play-sound": "Play", + "incoming-message": "Messaggio in arrivo", + "outgoing-message": "Messaggio in uscita", + "upload-new-sound": "Carica nuovo suono", + "saved": "Impostazioni salvate" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/tags.json b/public/language/it/admin/settings/tags.json new file mode 100644 index 0000000000..aa2cf0e354 --- /dev/null +++ b/public/language/it/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Impostazioni Tag", + "link-to-manage": "Gestisci tag", + "system-tags": "Tag del sistema", + "system-tags-help": "Solo gli utenti privilegiati potranno usare questi tag.", + "min-per-topic": "Tag minimi per discussione", + "max-per-topic": "Tag massimi per discussione", + "min-length": "Lunghezza minima tag", + "max-length": "Lunghezza massima tag", + "related-topics": "Discussioni correlate", + "max-related-topics": "Numero massimo di discussioni correlate da visualizzare (se supportati dal tema)" +} \ No newline at end of file diff --git a/public/language/it/admin/settings/uploads.json b/public/language/it/admin/settings/uploads.json new file mode 100644 index 0000000000..0c67ddd9c9 --- /dev/null +++ b/public/language/it/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "File orfani", + "private": "Rendi privati i file caricati", + "strip-exif-data": "Togli EXIF Data", + "preserve-orphaned-uploads": "Mantieni i file caricati su disco dopo l'eliminazione di un post", + "orphanExpiryDays": "Giorni di conservazione dei file orfani", + "orphanExpiryDays-help": "Dopo questo numero di giorni, i caricamenti orfani saranno eliminati dal file system.
Imposta 0 o lascia vuoto per disabilitarla.", + "private-extensions": "Estensione dei file da rendere privata", + "private-uploads-extensions-help": "Inserisci la lista di estensioni separati da virgola quì (es. pdf,xls,doc). Una lista vuota significa che tutti i file sono privati.", + "resize-image-width-threshold": "Ridimensiona le immagini se sono più grandi della larghezza specificata", + "resize-image-width-threshold-help": "(in pixel, predefinito: 1520 pixel, imposta 0 per disabilitare)", + "resize-image-width": "Ridimensiona le immagini sotto specificando la larghezza", + "resize-image-width-help": "(in pixel, predefinito: 760 pixel, imposta 0 per disabilitare)", + "resize-image-quality": "Qualità da utilizzare nel ridimensionamento delle immagini", + "resize-image-quality-help": "Utilizzare un'impostazione di qualità inferiore per ridurre la dimensione dei file delle immagini ridimensionate.", + "max-file-size": "Dimensione File Massima (in KiB)", + "max-file-size-help": "(in kibibytes, predefinito: 2048 KiB)", + "reject-image-width": "Larghezza Massima Immagine (in pixel)", + "reject-image-width-help": "Le immagini più grandi di questo valore saranno rifiutate.", + "reject-image-height": "Lunghezza Massima Immagine (in pixel)", + "reject-image-height-help": "Le immagini più alte di questo valore saranno rifiutate.", + "allow-topic-thumbnails": "Consenti agli utenti di caricare le miniature degli argomenti", + "topic-thumb-size": "Dimensione miniatura Argomento", + "allowed-file-extensions": "Abilita Estensioni File", + "allowed-file-extensions-help": "Inserisci una lista di estensioni separati da virgola quì (es. pdf,xls,doc). Una lista vuota indica che tutte le estensioni sono abilitate.", + "upload-limit-threshold": "Limita i caricamenti degli utenti a:", + "upload-limit-threshold-per-minute": "Per %1 minuto", + "upload-limit-threshold-per-minutes": "Per %1 minuti", + "profile-avatars": "Avatar del Profilo", + "allow-profile-image-uploads": "Abilita gli utenti ad effettuare il caricamento delle immagini nel profilo", + "convert-profile-image-png": "Converti l'immagine del profilo a PNG", + "default-avatar": "Personalizzazione Predefinita Avatar", + "upload": "Caricamento", + "profile-image-dimension": "Dimensione Immagine del profilo", + "profile-image-dimension-help": "(in pixel, predefinito: 128 pixel)", + "max-profile-image-size": "Dimensione Massima Immagine del Profile", + "max-profile-image-size-help": "(in kibibytes, predefinito: 256 KiB)", + "max-cover-image-size": "Dimensione massima dell'immagine di copertina", + "max-cover-image-size-help": "(in kibibytes, predefinito: 2048 KiB)", + "keep-all-user-images": "Mantenere vecchie versioni di avatar e cover dei profili sul server.", + "profile-covers": "Copertina dei profili", + "default-covers": "Immagini di copertina predefinite", + "default-covers-help": "Aggiungi immagini di copertina predefinite separate da virgole per gli account che non hanno un'immagine di copertina caricata" +} diff --git a/public/language/it/admin/settings/user.json b/public/language/it/admin/settings/user.json new file mode 100644 index 0000000000..b418763c86 --- /dev/null +++ b/public/language/it/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Autenticazione", + "email-confirm-interval": "L'utente non può mandare una nuova email di conferma fino a", + "email-confirm-interval2": "sono trascorsi minuti", + "allow-login-with": "Consenti l'accesso con", + "allow-login-with.username-email": "Username o Email", + "allow-login-with.username": "Solo Username", + "account-settings": "Impostazioni Account", + "gdpr_enabled": "Abilita la raccolta di consensi GDPR", + "gdpr_enabled_help": "Quando è abilitato, tutti i nuovi registranti dovranno dare il loro consenso esplicito per la raccolta e l'utilizzo dei dati ai sensi del regolamento generale sulla protezione dei dati (GDPR). Nota: L'abilitazione del GDPR non obbliga gli utenti preesistenti a fornire il consenso. Per farlo, è necessario installare il plugin GDPR.", + "disable-username-changes": "Disabilita il cambio dello username", + "disable-email-changes": "Disabilita il cambio di email", + "disable-password-changes": "Disabilita cambio password", + "allow-account-deletion": "Abilita eliminazione dell'account", + "hide-fullname": "Nascondi nome completo agli utenti", + "hide-email": "Nascondi l'email dagli utenti", + "show-fullname-as-displayname": "Mostra il nome completo dell'utente come nome visualizzato, se disponibile", + "themes": "Temi", + "disable-user-skins": "Non permettere agli utenti di scegliere una skin personalizzata", + "account-protection": "Protezione Account", + "admin-relogin-duration": "Durata di riaccesso dell'amministratore (minuti)", + "admin-relogin-duration-help": "Dopo un determinato periodo di tempo l'accesso alla sezione amministratore richiederà un nuovo accesso, impostandolo a 0 si disabilita", + "login-attempts": "Tentativi di accesso all'ora", + "login-attempts-help": "Se il numero di tentativi di accesso a un account supera questa soglia, l'account sarà bloccato per un periodo di tempo preconfigurato.", + "lockout-duration": "Account Lockout Duration (minuti)", + "login-days": "Giorni per ricordare le sessioni di accesso utente", + "password-expiry-days": "Forza il reset della password a un numero di giorni", + "session-time": "Tempo di Sessione", + "session-time-days": "Giorni", + "session-time-seconds": "Secondi", + "session-time-help": "Questi valori vengono utilizzati per definire per quanto tempo un utente rimane loggato quando spuntano "Remember Me" al login. Nota che solo uno di questi valori verrà utilizzato. Se non ci sono valori per secondi si passerà ai giorni. Se non ci sono valori per igiorni si passerà al valore di dafault di 14 giorni.", + "online-cutoff": "Minuti dopo per cui l'utente è considerato inattivo", + "online-cutoff-help": "Se l'utente non esegue alcuna azione per questa durata di tempo, vengono considerati inattivi e non ricevono aggiornamenti in tempo reale.", + "registration": "Registrazione Utente", + "registration-type": "Tipo Registrazione", + "registration-approval-type": "Tipo di Approvazione registrazione", + "registration-type.normal": "Normale", + "registration-type.admin-approval": "Approvazione Amministratore", + "registration-type.admin-approval-ip": "Approvazione Amministratore per gli IP", + "registration-type.invite-only": "Solo Invito", + "registration-type.admin-invite-only": "Solo invito per Amministratori", + "registration-type.disabled": "Niente registrazione", + "registration-type.help": "Normale: gli utenti possono registrarsi dalla pagina/di registrazione.
\nSolo invito: gli utenti possono invitare altri dalla pagina utenti.
\nSolo su invito amministratore: solo gli amministratori possono invitare altri utenti edalle pagine amministratore/gestione/utenti.
\nNessuna registrazione - Nessuna registrazione dell'utente.
", + "registration-approval-type.help": "Normale: gli utenti vengono registrati immediatamente.
\nApprovazione amministratore - Le registrazioni degli utenti sono inserite in una coda di approvazione per amministratori.
\nApprovazione amministratore per IP - Normale per i nuovi utenti, Approvazione amministratore per indirizzi IP che dispongono già di un account.
", + "registration-queue-auto-approve-time": "Tempo di approvazione automatico", + "registration-queue-auto-approve-time-help": "Ore prima che l'utente venga approvato automaticamente. 0 per disabilitare.", + "registration-queue-show-average-time": "Mostra agli utenti il tempo medio necessario per approvare un nuovo utente", + "registration.max-invites": "Numero massimo di inviti per Utente", + "max-invites": "Numero massimo di inviti per Utente", + "max-invites-help": "0 per nessuna restrizione. Gli amministratori ricevono infiniti inviti
Applicabile solo per \"Solo invito\"", + "invite-expiration": "Invito scaduto", + "invite-expiration-help": "Il tuo invito scadrà tra %1 giorni.", + "min-username-length": "Lunghezza Minima Username", + "max-username-length": "Lunghezza Massima Username", + "min-password-length": "Lunghezza Minima Password", + "min-password-strength": "Lunghezza Minima Password", + "max-about-me-length": "Massima Lunghezza Riguardo a Me", + "terms-of-use": "Termini di Utilizzo del Forum (Lasciare vuoto per disabilitare)", + "user-search": "Ricerca Utente", + "user-search-results-per-page": "Numero di risultati per visualizzazioni", + "default-user-settings": "Impostazioni Utente Predefinite", + "show-email": "Mostra email ", + "show-fullname": "Mostra nome completo", + "restrict-chat": "Permetti messaggi in chat solo da utenti che seguo", + "outgoing-new-tab": "Apri link esterni in una nuova scheda", + "topic-search": "Abilita ricerca nella Discussione", + "update-url-with-post-index": "Aggiorna l'url con l'indice dei posti durante la navigazione delle discussioni", + "digest-freq": "Iscriviti al Riepilogo", + "digest-freq.off": "Spento", + "digest-freq.daily": "Quotidiano", + "digest-freq.weekly": "Settimanale", + "digest-freq.biweekly": "Bisettimanale", + "digest-freq.monthly": "Mensile", + "email-chat-notifs": "Manda una email se arriva un nuovo messaggio di chat e non sono online", + "email-post-notif": "Manda una email quando ci sono nuove risposte a discussioni a cui sono iscritto", + "follow-created-topics": "Segui le discussioni che tu crei", + "follow-replied-topics": "Segui discussioni a cui rispondi tu", + "default-notification-settings": "Impostazioni di notifica predefinite", + "categoryWatchState": "Stato predefinito della categoria di controllo", + "categoryWatchState.watching": "Guardare", + "categoryWatchState.notwatching": "Non Guardare", + "categoryWatchState.ignoring": "Ignorato" +} diff --git a/public/language/it/admin/settings/web-crawler.json b/public/language/it/admin/settings/web-crawler.json new file mode 100644 index 0000000000..9fa4708e84 --- /dev/null +++ b/public/language/it/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Impostazioni Crawlability", + "robots-txt": "Robots.txt personalizzato Lascia vuoto per predefinito", + "sitemap-feed-settings": "Impostazioni Mappa Sito & Feed", + "disable-rss-feeds": "Disabilita Feed RSS", + "disable-sitemap-xml": "Disabilita Sitemap.xml", + "sitemap-topics": "Numero di Discussioni da visualizzare in Mappa Sito", + "clear-sitemap-cache": "Cancella Cache Mappa Sito", + "view-sitemap": "Visualizza Mappa Sito" +} \ No newline at end of file diff --git a/public/language/it/category.json b/public/language/it/category.json new file mode 100644 index 0000000000..1d172a0abf --- /dev/null +++ b/public/language/it/category.json @@ -0,0 +1,23 @@ +{ + "category": "Categoria", + "subcategories": "Sottocategorie", + "new_topic_button": "Nuova Discussione", + "guest-login-post": "Accedi per postare", + "no_topics": "Non ci sono discussioni in questa categoria.
Perché non ne inizi una?", + "browsing": "navigazione", + "no_replies": "Nessuno ha risposto", + "no_new_posts": "Nessuna nuova discussione.", + "watch": "Segui", + "ignore": "Ignora", + "watching": "Seguito", + "not-watching": "Non seguito", + "ignoring": "Ignorato", + "watching.description": "Mostra discussioni in non letti e recenti", + "not-watching.description": "Non mostrare discussioni in non letti, mostra in recenti", + "ignoring.description": "Non mostrare discussioni in non letti e recenti", + "watching.message": "Ora stai seguendo gli aggiornamenti di questa categoria e di tutte le sottocategorie", + "notwatching.message": "Ora non stai seguendo gli aggiornamenti di questa categoria e di tutte le sottocategorie", + "ignoring.message": "Ora stai ignorando gli aggiornamenti di questa categoria e di tutte le sottocategorie", + "watched-categories": "Categorie seguite", + "x-more-categories": "Altre %1 categorie" +} \ No newline at end of file diff --git a/public/language/it/email.json b/public/language/it/email.json new file mode 100644 index 0000000000..e6cc55ba1b --- /dev/null +++ b/public/language/it/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Email di prova", + "password-reset-requested": "Richiesto Reset Password!", + "welcome-to": "Benvenuto in %1", + "invite": "Invito da %1", + "greeting_no_name": "Ciao", + "greeting_with_name": "Ciao %1", + "email.verify-your-email.subject": "Per favore verificare la tua email", + "email.verify.text1": "Hai richiesto di modificare o confermare il tuo indirizzo email", + "email.verify.text2": "Per motivi di sicurezza, cambiamo o confermiamo l'indirizzo email in archivio solo dopo che la sua proprietà è stata confermata via email. Se non l'hai richiesto, non è necessaria alcuna azione da parte tua.", + "email.verify.text3": "Una volta confermato questo indirizzo email, sostituiremo il tuo attuale indirizzo email con questo (%1).", + "welcome.text1": "Grazie per esserti registrato su %1!", + "welcome.text2": "Per attivare completamente il tuo account dobbiamo verificare che sei il proprietario dell'indirizzo email con cui ti sei registrato.", + "welcome.text3": "Un amministratore ha accettato la tua richiesta di registrazione. Adesso puoi accedere con il tuo nome utente/password.", + "welcome.cta": "Clicca qui per confermare il tuo indirizzo email", + "invitation.text1": "%1 ti ha invitato a iscriverti a %2", + "invitation.text2": "Il tuo invito scadrà tra %1 giorni.", + "invitation.cta": "Clicca qui per creare il tuo account.", + "reset.text1": "Abbiamo ricevuto una richiesta di reset della tua password, probabilmente perché l'hai dimenticata. Se non è così si prega di ignorare questa email.", + "reset.text2": "Per confermare il reset della password per favore clicca il seguente link:", + "reset.cta": "Clicca qui per resettare la tua password", + "reset.notify.subject": "Password modificata con successo.", + "reset.notify.text1": "Ti informiamo che il %1, la password è stata cambiata con successo.", + "reset.notify.text2": "Se non hai autorizzato questo, per favore informa immediatamente l'amministratore.", + "digest.latest_topics": "Ultime discussioni da %1", + "digest.top-topics": "Argomenti principali da %1", + "digest.popular-topics": "Argomenti popolari da %1", + "digest.cta": "Clicca qui per visitare %1", + "digest.unsub.info": "Questo riepilogo ti è stato inviato perché lo hai sottoscritto nelle tue impostazioni.", + "digest.day": "giorno", + "digest.week": "settimana", + "digest.month": "mese", + "digest.subject": "Riepilogo per %1", + "digest.title.day": "Il tuo riepilogo quotidiano", + "digest.title.week": "Il tuo riepilogo settimanale", + "digest.title.month": "Il tuo riepilogo mensile", + "notif.chat.subject": "Nuovo messaggio chat ricevuto da %1", + "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.post.unsub.info": "Questa notifica di discussione ti è stata inviata perché l'hai sottoscritta nelle impostazioni.", + "notif.post.unsub.one-click": "In alternativa, annulla l'iscrizione a future email come questa, facendo clic", + "notif.cta": "Al forum", + "notif.cta-new-reply": "Visualizza Post", + "notif.cta-new-chat": "Visualizza Chat", + "notif.test.short": "Notifiche di test", + "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.", + "unsub.cta": "Clicca qui per modificare queste impostazioni", + "unsubscribe": "Annulla l'iscrizione", + "unsub.success": "Non riceverai più email dalla %1 mailing list", + "unsub.failure.title": "Impossibile annullare l'iscrizione", + "unsub.failure.message": "Sfortunatamente, non siamo stati in grado di cancellarti dalla mailing list, perché c'era un problema con il link. Tuttavia, puoi modificare le preferenze dell'email andando nelle impostazioni utente.

(errore: %1)", + "banned.subject": "Sei stato bannato da %1", + "banned.text1": "L'utente %1 è stato bannato da %2", + "banned.text2": "Questo ban durerà fino a %1.", + "banned.text3": "Questo è il motivo per cui sei stato bannato:", + "closing": "Grazie!" +} \ No newline at end of file diff --git a/public/language/it/error.json b/public/language/it/error.json new file mode 100644 index 0000000000..8fb31b0069 --- /dev/null +++ b/public/language/it/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Dati non validi", + "invalid-json": "JSON non valido", + "wrong-parameter-type": "Era previsto un valore di tipo %3 per la proprietà '%1', ma invece è stato ricevuto %2", + "required-parameters-missing": "I parametri richiesti sono mancanti in questa chiamata API: %1", + "not-logged-in": "Non sembra che tu abbia effettuato l'accesso.", + "account-locked": "Il tuo account è stato bloccato temporaneamente", + "search-requires-login": "La ricerca richiede un account! Si prega di effettuare l'accesso o registrarsi!", + "goback": "Premi indietro per tornare alla pagina precedente", + "invalid-cid": "ID Categoria non valido", + "invalid-tid": "ID Topic non valido", + "invalid-pid": "ID Post non valido", + "invalid-uid": "ID Utente non valido", + "invalid-mid": "ID messaggio chat non valido", + "invalid-date": "Deve essere fornita una data valida", + "invalid-username": "Nome utente non valido", + "invalid-email": "Email non valida", + "invalid-fullname": "Nome completo non valido", + "invalid-location": "Posizione non valida", + "invalid-birthday": "Compleanno non valido", + "invalid-title": "Titolo non valido", + "invalid-user-data": "Dati utente non validi", + "invalid-password": "Password non valida", + "invalid-login-credentials": "Credenziali di accesso non valide", + "invalid-username-or-password": "Si prega di specificare sia un nome utente sia la password", + "invalid-search-term": "Termine di ricerca non valido", + "invalid-url": "URL non valido", + "invalid-event": "Evento non valido: %1", + "local-login-disabled": "Il sistema di accesso locale è stato disabilitato per gli account senza privilegi.", + "csrf-invalid": "Non siamo riusciti a farti accedere, probabilmente perché la sessione è scaduta. Per favore riprova.", + "invalid-path": "Percorso non valido", + "folder-exists": "La cartella esiste", + "invalid-pagination-value": "Valore di impaginazione non valido, deve essere almeno %1 ed al massimo %2", + "username-taken": "Nome utente già esistente", + "email-taken": "Email già esistente", + "email-nochange": "L'email inserita è la stessa dell'email già presente in archivio.", + "email-invited": "L'email è già stata invitata", + "email-not-confirmed": "Sarai abilitato a postare in alcune categorie o discussioni una volta che la tua email sarà confermata, per favore clicca qui per inviare una email di conferma.", + "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": "La tua email non è stata ancora confermata, controlla la tua casella di posta per l'email di conferma. Potresti non essere in grado di postare in alcune categorie o chattare fino a quando la tua email non sarà confermata.", + "no-email-to-confirm": "Il tuo account non ha un'email impostata. Un'email è necessaria per il recupero dell'account, e può essere necessaria per chattare e postare in alcune categorie. Clicca qui per inserire un'email.", + "user-doesnt-have-email": "L'utente \"%1\" non ha impostato un email.", + "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.", + "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": "Questo utente non ha riepiloghi attivi o l'impostazione predefinita del sistema non è configurata per l'invio di riepiloghi", + "username-too-short": "Nome utente troppo corto", + "username-too-long": "Nome utente troppo lungo", + "password-too-long": "Password troppo lunga", + "reset-rate-limited": "Troppe richieste di reimpostazione password (richieste limitate)", + "reset-same-password": "Si prega di utilizzare una password diversa da quella attuale", + "user-banned": "Utente bannato", + "user-banned-reason": "Spiacente, questo account è stato bannato (Motivazione: %1)", + "user-banned-reason-until": "Spiacente, questo account è stato bannato fino a %1 (Motivazione: %2)", + "user-too-new": "Spiacente, devi attendere %1 secondo(i) prima di creare il tuo primo post", + "blacklisted-ip": "Spiacente, il tuo indirizzo IP è stato bannato da questa comunità. Se ritieni che si tratti di un errore, contatta un amministratore.", + "ban-expiry-missing": "Per favore fornire una data di termine per questo ban", + "no-category": "La Categoria non esiste", + "no-topic": "La Discussione non esiste", + "no-post": "Il Post non esiste", + "no-group": "Il Gruppo non esiste", + "no-user": "L'Utente non esiste", + "no-teaser": "Teaser non esiste", + "no-flag": "Segnalazione non esiste", + "no-privileges": "Non hai abbastanza privilegi per questa azione.", + "category-disabled": "Categoria disabilitata", + "topic-locked": "Discussione Bloccata", + "post-edit-duration-expired": "Puoi modificare i post solo per %1 secondo(i) dopo la pubblicazione", + "post-edit-duration-expired-minutes": "Puoi modificare i post solo per %1 minuto(i) dopo la pubblicazione", + "post-edit-duration-expired-minutes-seconds": "Puoi modificare i post solo per %1 secondo(i) %2 secondo(i) dopo la pubblicazione", + "post-edit-duration-expired-hours": "Puoi modificare i post solo per %1 ora(e) dopo la pubblicazione", + "post-edit-duration-expired-hours-minutes": "Puoi modificare i post solo per %1 ora(e) %2 minuto(i) dopo la pubblicazione", + "post-edit-duration-expired-days": "Puoi modificare i post solo per %1 giorno(i) dopo la pubblicazione", + "post-edit-duration-expired-days-hours": "Puoi modificare i post solo per %1 giorno(i) %2 ora(e) dopo la pubblicazione", + "post-delete-duration-expired": "Puoi eliminare i post solo per %1 secondo(i) dopo la pubblicazione", + "post-delete-duration-expired-minutes": "Puoi eliminare i post solo per %1 minuto(i) dopo la pubblicazione", + "post-delete-duration-expired-minutes-seconds": "Puoi eliminare i post solo per %1 minuto(i) %2 secondo(i) dopo la pubblicazione", + "post-delete-duration-expired-hours": "Puoi eliminare i post solo per %1 ora(e) dopo la pubblicazione", + "post-delete-duration-expired-hours-minutes": "Puoi eliminare i post solo per %1 ora(e) %2 minuto(i) dopo la pubblicazione", + "post-delete-duration-expired-days": "Puoi eliminare i post solo per %1 giorno(i) dopo la pubblicazione", + "post-delete-duration-expired-days-hours": "Puoi eliminare i post solo per %1 giorno(i) %2 ora(e) dopo la pubblicazione", + "cant-delete-topic-has-reply": "Non puoi eliminare la tua discussione se ha una risposta", + "cant-delete-topic-has-replies": "Non puoi eliminare la tua discussione se ha %1 risposte", + "content-too-short": "Inserisci un testo più lungo. Il messaggio deve contenere almeno %1 caratteri.", + "content-too-long": "Inserisci un post più breve. I post non possono essere più lunghi di %1 caratteri.", + "title-too-short": "Inserisci un titolo più lungo. I titoli devono contenere almeno %1 caratteri.", + "title-too-long": "Inserisci un titolo più corto. I titoli non possono essere più lunghi di %1 caratteri.", + "category-not-selected": "Categoria non selezionata.", + "too-many-posts": "È possibile inserire un Post ogni %1 secondi - si prega di attendere prima di postare di nuovo", + "too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondi finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di scrivere ancora", + "already-posting": "You are already posting", + "tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.", + "tag-too-long": "Per favore inserisci un tag più corto. I tags non dovrebbero essere più lunghi di %1 caratteri", + "not-enough-tags": "Tag non sufficienti. Le discussioni devono avere almeno %1 Tag", + "too-many-tags": "Troppi Tag. Le discussioni non possono avere più di %1 Tag", + "cant-use-system-tag": "Non puoi usare questo tag di sistema.", + "cant-remove-system-tag": "Non puoi rimuovere questo tag di sistema.", + "still-uploading": "Per favore attendere il completamento degli uploads.", + "file-too-big": "La dimensione massima consentita è di %1 kB - si prega di caricare un file più piccolo", + "guest-upload-disabled": "Il caricamento da ospite è stato disattivato", + "cors-error": "Impossibile caricare immagine a causa di CORS non configurato opportunamente", + "upload-ratelimit-reached": "Hai caricato troppi file contemporaneamente. Per favore riprova più tardi.", + "scheduling-to-past": "Si prega di selezionare una data nel futuro.", + "invalid-schedule-date": "Si prega di inserire una data e ora valida.", + "cant-pin-scheduled": "Le discussioni pianificate non possono essere (s)bloccate.", + "cant-merge-scheduled": "Le discussioni pianificate non possono essere unite.", + "cant-move-posts-to-scheduled": "Non è possibile spostare i post in una discussione pianificata.", + "cant-move-from-scheduled-to-existing": "Non è possibile spostare i post da una discussione pianificata a una discussione esistente.", + "already-bookmarked": "Hai già aggiunto questa discussione ai preferiti.", + "already-unbookmarked": "Hai già rimosso questa discussione dai preferiti", + "cant-ban-other-admins": "Non puoi bannare altri amministratori!", + "cant-mute-other-admins": "Non puoi silenziare gli altri amministratori!", + "user-muted-for-hours": "Sei stato silenziato, potrai postare tra %1 ora(e)", + "user-muted-for-minutes": "Sei stato silenziato, potrai postare tra %1 minuto(i)", + "cant-make-banned-users-admin": "Non puoi rendere amministratori gli utenti bannati.", + "cant-remove-last-admin": "Sei l'unico Amministratore. Aggiungi un altro amministratore prima di rimuovere il tuo ruolo", + "account-deletion-disabled": "L'eliminazione dell'account è disabilitata", + "cant-delete-admin": "Togli i privilegi amministrativi da questo account prima di provare ad eliminarlo.", + "already-deleting": "Sto già eliminando", + "invalid-image": "Immagine non Valida", + "invalid-image-type": "Tipo dell'immagine non valido. I tipi permessi sono: %1", + "invalid-image-extension": "Estensione immagine non valida", + "invalid-file-type": "Tipo di file non valido. I formati consentiti sono: %1", + "invalid-image-dimensions": "Dimensione immagine troppo grande", + "group-name-too-short": "Nome del Gruppo troppo corto", + "group-name-too-long": "Il nome del gruppo è troppo lungo", + "group-already-exists": "Il Gruppo esiste già", + "group-name-change-not-allowed": "Il cambio di nome al Gruppo non è consentito", + "group-already-member": "Fa già parte di questo gruppo", + "group-not-member": "Non è membro di questo gruppo", + "group-needs-owner": "Questo gruppo richiede almeno un proprietario.", + "group-already-invited": "Questo utente è già stato invitato", + "group-already-requested": "La tua richiesta di iscrizione è già stata inviata", + "group-join-disabled": "Non sei in grado di iscriverti a questo gruppo in questo momento", + "group-leave-disabled": "Non sei in grado di lasciare questo gruppo in questo momento.", + "post-already-deleted": "Questo post è già stato eliminato", + "post-already-restored": "Questo Post è già stato ripristinato", + "topic-already-deleted": "Questo topic è già stato eliminato", + "topic-already-restored": "Questo Topic è già stato ripristinato", + "cant-purge-main-post": "Non puoi eliminare definitivamente il post principale, per favore elimina invece la discussione", + "topic-thumbnails-are-disabled": "Le miniature della Discussione sono disabilitate.", + "invalid-file": "File non valido", + "uploads-are-disabled": "Uploads disabilitati", + "signature-too-long": "Spiacenti, la tua firma non può essere più lunga di %1 caratteri.", + "about-me-too-long": "Spiacenti, il testo non può essere più lungo di %1 caratteri.", + "cant-chat-with-yourself": "Non puoi chattare con te stesso!", + "chat-restricted": "Questo utente ha ristretto i suoi messaggi in chat alle persone che segue. Per poter chattare con te ti deve prima seguire.", + "chat-disabled": "Il sistema di chat è stato disabilitato", + "too-many-messages": "Hai inviato troppi messaggi, aspetta un attimo.", + "invalid-chat-message": "Messaggio chat non valido", + "chat-message-too-long": "I messaggi in chat non possono superare i %1 caratteri.", + "cant-edit-chat-message": "Non ti è permesso di modificare questo messaggio", + "cant-delete-chat-message": "Non ti è permesso di eliminare questo messaggio", + "chat-edit-duration-expired": "Sei l'unico che ha il permesso di editare i messaggi per %1 secondi(o) dopo il loro invio", + "chat-delete-duration-expired": "Sei l'unico che ha il permesso di eliminare i messaggi per %1 secondi(o) dopo il loro invio", + "chat-deleted-already": "Il messaggio è già stato eliminato.", + "chat-restored-already": "Questo messaggio della chat è già stato ripristinato.", + "chat-room-does-not-exist": "La stanza chat non esiste.", + "already-voting-for-this-post": "Hai già votato per questo post", + "reputation-system-disabled": "Il sistema di reputazione è disabilitato.", + "downvoting-disabled": "Votata negativamente è disabilitato", + "not-enough-reputation-to-chat": "Hai bisogno di %1 reputazione per chattare", + "not-enough-reputation-to-upvote": "Hai bisogno di %1 reputazione/i per votare positivamente", + "not-enough-reputation-to-downvote": "Hai bisogno di %1 reputazione/i per effettuare un voto negativo", + "not-enough-reputation-to-flag": "Hai bisogno di %1 reputazione/i per segnalare questo post", + "not-enough-reputation-min-rep-website": "Hai bisogno di %1 reputazione/i per aggiungere un sito web", + "not-enough-reputation-min-rep-aboutme": "Hai bisogno di %1 reputazione/i per aggiungere un Su di me", + "not-enough-reputation-min-rep-signature": "Hai bisogno di %1 reputazione/i per aggiungere una firma", + "not-enough-reputation-min-rep-profile-picture": "Hai bisogno di %1 reputazione/i per aggiungere una foto del profilo", + "not-enough-reputation-min-rep-cover-picture": "Hai bisogno di %1 reputazione/i per aggiungere un'immagine di copertina", + "post-already-flagged": "Hai già segnalato questo post", + "user-already-flagged": "Hai già segnalato questo utente", + "post-flagged-too-many-times": "Questo post è già stato segnalato da altri", + "user-flagged-too-many-times": "Questo utente è già stato segnalato da altri", + "cant-flag-privileged": "Non è consentito contrassegnare i profili o il contenuto degli utenti privilegiati (moderatori/moderatori globali/amministratori)", + "self-vote": "Non puoi votare la tua stessa discussione", + "too-many-upvotes-today": "Puoi votare positivamente solo %1 volte al giorno", + "too-many-upvotes-today-user": "Puoi votare positivamente un utente solo %1 volte al giorno", + "too-many-downvotes-today": "È possibile votare negativamente solo %1 volta al giorno", + "too-many-downvotes-today-user": "È possibile votare negativamente un utente solo %1 volta al giorno", + "reload-failed": "NodeBB ha incontrato un problema durante il ricaricamento: \"%1\". NodeBB continuerà a servire gli assets esistenti lato client, così puoi annullare quello che hai fatto prima di ricaricare.", + "registration-error": "Errore nella registrazione", + "parse-error": "Qualcosa è andato storto durante l'analisi della risposta proveniente dal server", + "wrong-login-type-email": "Per favore usa la tua email per accedere", + "wrong-login-type-username": "Per favore usa il tuo nome utente per accedere", + "sso-registration-disabled": "Registrazione disabilitata per %1 accounts, registrati prima con un'indirizzo email.", + "sso-multiple-association": "Non puoi associare più di un account di questo servizio al tuo account NodeBB. Disassocia prima quello esistente e riprova.", + "invite-maximum-met": "Hai invitato il massimo numero di persone possibili (%1 su %2).", + "no-session-found": "Nessuna sessione di accesso trovata!", + "not-in-room": "L'utente non è in questa stanza", + "cant-kick-self": "Non puoi espellerti dal gruppo", + "no-users-selected": "Nessun utente selezionato", + "invalid-home-page-route": "Percorso della pagina iniziale non valido", + "invalid-session": "Sessione non valida", + "invalid-session-text": "Sembra che la tua sessione di accesso non sia più attiva. Si prega di aggiornare questa pagina.", + "session-mismatch": "Mancata corrispondenza della sessione", + "session-mismatch-text": "Sembra che la tua sessione di accesso non corrisponda più al server. Si prega di aggiornare questa pagina.", + "no-topics-selected": "Nessuna discussione selezionata!", + "cant-move-to-same-topic": "Non puoi spostare il post nella stessa discussione!", + "cant-move-topic-to-same-category": "Non si può spostare la discussione nella stessa categoria!", + "cannot-block-self": "Non puoi auto bloccarti!", + "cannot-block-privileged": "Impossibile bloccare amministratori o moderatori globali", + "cannot-block-guest": "Gli Ospiti non sono in grado di bloccare altri utenti", + "already-blocked": "Questo utente è già bloccato", + "already-unblocked": "Questo utente è già sbloccato", + "no-connection": "Sembra ci sia un problema con la tua connessione internet", + "socket-reconnect-failed": "Impossibile raggiungere il server al momento. Clicca qui per riprovare o riprova in un secondo momento", + "plugin-not-whitelisted": "Impossibile installare il plug-in & solo i plugin nella whitelist del Gestione Pacchetti di NodeBB possono essere installati tramite ACP", + "plugins-set-in-configuration": "Non è possibile modificare lo stato dei plugin, poiché sono definiti in fase di esecuzione. (config.json, variabili ambientali o argomenti del terminale); modificare invece la configurazione.", + "theme-not-set-in-configuration": "Quando si definiscono i plugin attivi nella configurazione, la modifica dei temi richiede l'aggiunta del nuovo tema all'elenco dei plugin attivi prima di aggiornarlo nell'ACP", + "topic-event-unrecognized": "Evento discussione '%1' non riconosciuto", + "cant-set-child-as-parent": "Impossibile impostare figlio come categoria padre", + "cant-set-self-as-parent": "Impossibile impostare se stessi come categoria padre", + "api.master-token-no-uid": "Un token master è stato ricevuto senza un corrispondente `_uid` nel corpo della richiesta", + "api.400": "C'era qualcosa di sbagliato nel payload della richiesta che hai passato.", + "api.401": "Non è stata trovata una sessione di accesso valida. Per favore, accedi e riprova.", + "api.403": "Non sei autorizzato a fare questa chiamata", + "api.404": "Chiamata API non valida", + "api.426": "HTTPS è necessario per le richieste all'API di scrittura, si prega di inviare nuovamente la richiesta via HTTPS", + "api.429": "Hai fatto troppe richieste, riprova più tardi", + "api.500": "È stato riscontrato un errore inaspettato durante il tentativo di soddisfare la tua richiesta.", + "api.501": "Il percorso che stai cercando di chiamare non è ancora implementato, riprova domani", + "api.503": "Il percorso che stai cercando di chiamare non è attualmente disponibile a causa di una configurazione del server" +} \ No newline at end of file diff --git a/public/language/it/flags.json b/public/language/it/flags.json new file mode 100644 index 0000000000..3704f4d2be --- /dev/null +++ b/public/language/it/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Stato", + "reports": "Segnalazioni", + "first-reported": "Prima segnalazione", + "no-flags": "Evviva! Nessuna segnalazione trovata.", + "assignee": "Assegnatario", + "update": "Aggiorna", + "updated": "Aggiornato", + "resolved": "Risolto", + "target-purged": "Il contenuto di questa segnalazione è stato eliminato e non è più disponibile.", + + "graph-label": "Segnalazioni Giornaliere", + "quick-filters": "Filtri Rapidi", + "filter-active": "Ci sono uno o più filtri attivi in questa lista di segnalazioni", + "filter-reset": "Rimuovi Filtri", + "filters": "Opzioni Filtri", + "filter-reporterId": "UID segnalatore", + "filter-targetUid": "UID segnalato", + "filter-type": "Tipo Segnalazione", + "filter-type-all": "Tutto il Contenuto", + "filter-type-post": "Post", + "filter-type-user": "Utente", + "filter-state": "Stato", + "filter-assignee": "UID assegnato", + "filter-cid": "Categoria", + "filter-quick-mine": "Assegnato a me", + "filter-cid-all": "Tutte le categorie", + "apply-filters": "Applica Filtri", + "more-filters": "Altri filtri", + "fewer-filters": "Meno filtri", + + "quick-actions": "Azioni rapide", + "flagged-user": "Utente Segnalato", + "view-profile": "Vedi Profilo", + "start-new-chat": "Inizia Nuova Chat", + "go-to-target": "Visualizza oggetto segnalazione", + "assign-to-me": "Assegna a me", + "delete-post": "Elimina post", + "purge-post": "Elimina Post", + "restore-post": "Ripristina post", + "delete": "Elimina segnalazione", + + "user-view": "Vedi Profilo", + "user-edit": "Modifica Profilo", + + "notes": "Note Segnalazione", + "add-note": "Aggiungi Nota", + "no-notes": "Nessuna nota condivisa", + "delete-note-confirm": "Sei sicuro di voler eliminare questa nota di segnalazione?", + "delete-flag-confirm": "Sei sicuro di voler eliminare questa segnalazione?", + "note-added": "Nota aggiunta", + "note-deleted": "Nota eliminata", + "flag-deleted": "Segnalazione eliminata", + + "history": "Cronologia segnalazioni account", + "no-history": "Nessuna cronologia segnalazione.", + + "state-all": "Tutti gli stati", + "state-open": "Nuovo/Apri", + "state-wip": "Lavori in corso", + "state-resolved": "Risolto", + "state-rejected": "Rifiutato", + "no-assignee": "Non assegnato", + + "sort": "Ordina per", + "sort-newest": "Prima i più recenti", + "sort-oldest": "Prima il meno recente", + "sort-reports": "Più segnalazioni", + "sort-all": "Tutti i tipi di segnalazione...", + "sort-posts-only": "Solo post...", + "sort-downvotes": "Più voti negativi", + "sort-upvotes": "Più voti positivi", + "sort-replies": "Più risposte", + + "modal-title": "Segnala il contenuto", + "modal-body": "Specifica il motivo per cui contrassegni %1 %2 per la revisione. In alternativa, utilizza uno dei pulsanti di segnalazione rapida, se applicabile.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensivo", + "modal-reason-other": "Altro (specifica di seguito)", + "modal-reason-custom": "Motivo per cui segnali questo contenuto...", + "modal-submit": "Invia segnalazione", + "modal-submit-success": "Il contenuto è stato segnalato per la moderazione.", + + "bulk-actions": "Azioni in blocco", + "bulk-resolve": "Risolvi segnalazione(i)", + "bulk-success": "%1 segnalazioni aggiornate", + "flagged-timeago-readable": "Segnalato (%2)", + "auto-flagged": "[Contrassegnato automaticamente] Ha ricevuto %1 voti negativi." +} \ No newline at end of file diff --git a/public/language/it/global.json b/public/language/it/global.json new file mode 100644 index 0000000000..46ee51179b --- /dev/null +++ b/public/language/it/global.json @@ -0,0 +1,126 @@ +{ + "home": "Home", + "search": "Cerca", + "buttons.close": "Chiudi", + "403.title": "Accesso Negato", + "403.message": "Sembra tu sia arrivato ad una pagina a cui non hai accesso.", + "403.login": "Forse dovresti effettuare l'accesso?", + "404.title": "Non Trovato", + "404.message": "Sembra tu sia arrivato ad una pagina che non esiste. Torna alla pagina iniziale.", + "500.title": "Errore interno.", + "500.message": "Oops! Qualcosa non funziona come si deve!", + "400.title": "Richiesta non valida.", + "400.message": "Sembra che questo link non sia corretto, per favore ricontrolla e riprova. Altrimenti ritorna alla pagina iniziale.", + "register": "Registrati", + "login": "Accedi", + "please_log_in": "Per favore Accedi", + "logout": "Logout", + "posting_restriction_info": "L'inserimento di nuovi post è attualmente limitato ai soli utenti registrati, clicca qui per effettuare l'accesso.", + "welcome_back": "Bentornato", + "you_have_successfully_logged_in": "Accesso effettuato con successo", + "save_changes": "Salva Modifiche", + "save": "Salva", + "close": "Chiudi", + "pagination": "Impaginazione", + "pagination.out_of": "%1 di %2", + "pagination.enter_index": "Vai all'indice dei post", + "header.admin": "Amministratore", + "header.categories": "Categorie", + "header.recent": "Recenti", + "header.unread": "Non letti", + "header.tags": "Tag", + "header.popular": "Popolare", + "header.top": "In alto", + "header.users": "Utenti", + "header.groups": "Gruppi", + "header.chats": "Chat", + "header.notifications": "Notifiche", + "header.search": "Cerca", + "header.profile": "Profilo", + "header.navigation": "Navigazione", + "notifications.loading": "Caricamento Notifiche", + "chats.loading": "Caricamento Messaggi", + "motd.welcome": "Benvenuti in NodeBB, la piattaforma di discussione del futuro.", + "previouspage": "Pagina Precedente", + "nextpage": "Pagina Successiva", + "alert.success": "Riuscito", + "alert.error": "Errore", + "alert.banned": "Bannato", + "alert.banned.message": "Sei stato appena bannato, il tuo accesso è ora limitato.", + "alert.unbanned": "Non bannato", + "alert.unbanned.message": "Il tuo ban è stato revocato.", + "alert.unfollow": "Non stai più seguendo %1!", + "alert.follow": "Stai seguendo %1!", + "users": "Utenti", + "topics": "Discussioni", + "posts": "Post", + "x-posts": "%1 post", + "best": "Migliore", + "controversial": "Controverso", + "votes": "Votazioni", + "x-votes": "%1 voti", + "voters": "Votanti", + "upvoters": "Hanno votato positivamente", + "upvoted": "Votato positivamente", + "downvoters": "Hanno votato negativamente", + "downvoted": "Votato negativamente", + "views": "Visualizzazioni", + "posters": "Autori", + "reputation": "Reputazione", + "lastpost": "Ultimo post", + "firstpost": "Primo post", + "read_more": "per saperne di più", + "more": "Altro", + "none": "Nessuno", + "posted_ago_by_guest": "scritto %1 da Ospite", + "posted_ago_by": "scritto %1 da %2", + "posted_ago": "postato %1", + "posted_in": "postato in %1", + "posted_in_by": "postato in %1 da %2", + "posted_in_ago": "postato in %1 %2", + "posted_in_ago_by": "postato in %1 %2 da %3", + "user_posted_ago": "%1 ha postato %2", + "guest_posted_ago": "Ospite ha scritto %1", + "last_edited_by": "ultima modifica di %1", + "norecentposts": "Nessun Post Recente", + "norecenttopics": "Nessuna Discussione Recente", + "recentposts": "Post Recenti", + "recentips": "IP recentemente registrati", + "moderator_tools": "Strumenti di amministrazione", + "online": "Online", + "away": "Non disponibile", + "dnd": "Non disturbare", + "invisible": "Invisibile", + "offline": "Non in linea", + "email": "Email", + "language": "Lingua", + "guest": "Ospite", + "guests": "Ospiti", + "former_user": "Un Ex Utente", + "system-user": "Sistema", + "unknown-user": "Utente sconosciuto", + "updated.title": "Forum Aggiornato", + "updated.message": "Questo forum è stato aggiornato all'ultima versione. Clicca qui per ricaricare la pagina.", + "privacy": "Privacy", + "follow": "Segui", + "unfollow": "Non seguire", + "delete_all": "Elimina Tutto", + "map": "Mappa", + "sessions": "Sessioni di accesso", + "ip_address": "Indirizzo IP", + "enter_page_number": "Inserisci il numero della pagina", + "upload_file": "Carica file", + "upload": "Carica", + "uploads": "Caricati", + "allowed-file-types": "Le estensioni permesse dei file sono %1", + "unsaved-changes": "Hai delle modifiche non salvate. Sei sicuro che vuoi lasciare la pagina?", + "reconnecting-message": "Sembra che la tua connessione a %1 sia stata persa, per favore attendi mentre proviamo a riconnetterti.", + "play": "Play", + "cookies.message": "Questo sito utilizza i cookie per garantirti la miglior esperienza di navigazione possibile", + "cookies.accept": "Ho capito!", + "cookies.learn_more": "Scopri di più", + "edited": "Modificato", + "disabled": "Disabilitato", + "select": "Seleziona", + "user-search-prompt": "Scrivi qui per avviare la ricerca utenti" +} \ No newline at end of file diff --git a/public/language/it/groups.json b/public/language/it/groups.json new file mode 100644 index 0000000000..5671327861 --- /dev/null +++ b/public/language/it/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Gruppi", + "view_group": "Vedi Gruppo", + "owner": "Proprietario del Gruppo", + "new_group": "Crea Nuovo Gruppo", + "no_groups_found": "Non ci sono gruppi da vedere", + "pending.accept": "Accetta", + "pending.reject": "Rifiuta", + "pending.accept_all": "Accetta tutti", + "pending.reject_all": "Rifiuta tutti", + "pending.none": "Non ci sono membri in attesa in questo momento", + "invited.none": "Non ci sono membri invitati in questo momento", + "invited.uninvite": "Revoca invito", + "invited.search": "Cerca un utente da invitare in questo gruppo", + "invited.notification_title": "Sei stato invitato ad iscriverti a %1", + "request.notification_title": "Richiesta di iscrizione al gruppo da %1", + "request.notification_text": "%1 ha chiesto di diventare membro di %2", + "cover-save": "Salva", + "cover-saving": "Salvataggio", + "details.title": "Dettagli Gruppo", + "details.members": "Lista Membri", + "details.pending": "Membri in attesa", + "details.invited": "Membri invitati", + "details.has_no_posts": "I membri di questo gruppo non hanno creato nessun post.", + "details.latest_posts": "Ultimi Post", + "details.private": "Privato", + "details.disableJoinRequests": "Disabilita le richieste d'iscrizione", + "details.disableLeave": "Impedisce agli utenti di lasciare il gruppo", + "details.grant": "Concedi/Revoca la Proprietà", + "details.kick": "Espelli", + "details.kick_confirm": "Sei sicuro di voler rimuovere questo membro dal gruppo?", + "details.add-member": "Aggiungi Membro", + "details.owner_options": "Amministratore Gruppo", + "details.group_name": "Nome Gruppo", + "details.member_count": "Numero Membri", + "details.creation_date": "Data Creazione", + "details.description": "Descrizione", + "details.member-post-cids": "ID categoria da cui visualizzare i post", + "details.badge_preview": "Anteprima Badge", + "details.change_icon": "Cambia Icona", + "details.change_label_colour": "Cambia colore etichetta", + "details.change_text_colour": "Cambia colore testo", + "details.badge_text": "Testo Badge", + "details.userTitleEnabled": "Mostra Badge", + "details.private_help": "Se abilitato, l'iscrizione ai gruppi richiede l'approvazione del proprietario del gruppo.", + "details.hidden": "Nascosto", + "details.hidden_help": "Se abilitato, questo gruppo non sarà visibile nella lista dei gruppi e gli utenti dovranno essere invitati manualmente", + "details.delete_group": "Elimina Gruppo", + "details.private_system_help": "I gruppi privati sono disabilitati a livello di sistema, questa opzione non fa nulla", + "event.updated": "I dettagli del Gruppo sono stati aggiornati", + "event.deleted": "Il gruppo \"%1\" è stato eliminato", + "membership.accept-invitation": "Accetta l'invito", + "membership.accept.notification_title": "Ora sei un membro di %1", + "membership.invitation-pending": "Invito in sospeso", + "membership.join-group": "Iscriviti al Gruppo", + "membership.leave-group": "Lascia il Gruppo", + "membership.leave.notification_title": "%1 ha lasciato il gruppo %2", + "membership.reject": "Rifiuta", + "new-group.group_name": "Nome Gruppo:", + "upload-group-cover": "Carica copertina gruppo", + "bulk-invite-instructions": "Inserisci una lista di nomi utente da invitare in questo gruppo separati da virgole", + "bulk-invite": "Invito Collettivo", + "remove_group_cover_confirm": "Sei sicuro di voler rimuovere l'immagine copertina?" +} \ No newline at end of file diff --git a/public/language/it/ip-blacklist.json b/public/language/it/ip-blacklist.json new file mode 100644 index 0000000000..3bdcdf32c0 --- /dev/null +++ b/public/language/it/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configura qui la tua blacklist degli IP.", + "description": "Occasionalmente, il ban di un account utente non è un deterrente sufficiente. Altre volte, limitare l'accesso al forum a un IP specifico o a una serie di IP è il modo migliore per proteggere il forum. In questi scenari, è possibile aggiungere a questa blacklist indirizzi IP fastidiosi o interi blocchi CIDR, che non potranno accedere o registrare un nuovo account.", + "active-rules": "Regole attive", + "validate": "Convalida la Blacklist", + "apply": "Applica la Blacklist", + "hints": "Suggerimenti per la sintassi", + "hint-1": "Definisci un singolo indirizzo IP per linea. È possibile aggiungere blocchi IP a condizione che seguano il formato CIDR. (es. 192.168.100.0/22).", + "hint-2": "Puoi aggiungere commenti iniziando le righe con il simbolo #.", + + "validate.x-valid": "%1 su %2 regola(e) valide.", + "validate.x-invalid": "Le seguenti regole %1 non sono valide:", + + "alerts.applied-success": "Blacklist applicata", + + "analytics.blacklist-hourly": "La figura 1 – Numero di visite in Blacklist per ora", + "analytics.blacklist-daily": "Figura 2 – Numero di visite in Blacklist per ora", + "ip-banned": "IP bannati" +} \ No newline at end of file diff --git a/public/language/it/language.json b/public/language/it/language.json new file mode 100644 index 0000000000..677ab1f2e6 --- /dev/null +++ b/public/language/it/language.json @@ -0,0 +1,5 @@ +{ + "name": "Italiano (Italia)", + "code": "it", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/it/login.json b/public/language/it/login.json new file mode 100644 index 0000000000..e4907ff99a --- /dev/null +++ b/public/language/it/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Nome utente / Email", + "username": "Nome utente", + "remember_me": "Ricordami?", + "forgot_password": "Password dimenticata?", + "alternative_logins": "Accessi alternativi", + "failed_login_attempt": "Accesso non riuscito", + "login_successful": "Hai effettuato l'accesso con successo!", + "dont_have_account": "Non hai un account?", + "logged-out-due-to-inactivity": "Sei stato disconnesso dal Pannello di Controllo Amministratore per inattività", + "caps-lock-enabled": "Il blocco delle maiuscole è abilitato" +} \ No newline at end of file diff --git a/public/language/it/modules.json b/public/language/it/modules.json new file mode 100644 index 0000000000..7f1533d729 --- /dev/null +++ b/public/language/it/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Messaggia con", + "chat.placeholder": "Digita il messaggio di chat qui, trascina le immagini, premi invio per inviare", + "chat.scroll-up-alert": "Stai guardando i messaggi più vecchi, clicca qui per andare al messaggio più recente.", + "chat.send": "Invia", + "chat.no_active": "Non hai chat attive.", + "chat.user_typing": "%1 sta scrivendo...", + "chat.user_has_messaged_you": "%1 ti ha scritto.", + "chat.see_all": "Tutte le chat", + "chat.mark_all_read": "Segna tutto come letto", + "chat.no-messages": "Si prega di selezionare un destinatario per vedere la cronologia dei messaggi", + "chat.no-users-in-room": "Nessun utente in questa stanza", + "chat.recent-chats": "Chat Recenti", + "chat.contacts": "Contatti", + "chat.message-history": "Cronologia Messaggi", + "chat.message-deleted": "Messaggio cancellato", + "chat.options": "Opzioni chat", + "chat.pop-out": "Chat in finestra", + "chat.minimize": "Minimizza", + "chat.maximize": "Ingrandisci", + "chat.seven_days": "7 Giorni", + "chat.thirty_days": "30 Giorni", + "chat.three_months": "3 Mesi", + "chat.delete_message_confirm": "Sei sicuro di voler eliminare questo messaggio?", + "chat.retrieving-users": "Estrapolando gli utenti...", + "chat.manage-room": "Gestisci stanza chat", + "chat.add-user-help": "Cerca qui gli utenti. Quando selezionato, l'utente sarà aggiunto alla chat.\nIl nuovo utente non sarà in grado di vedere i messaggi della chat scritti prima della sua partecipazione alla conversazione.\nSolo i proprietari della stanza () possono rimuovere gli utenti dalla stanza della chat.", + "chat.confirm-chat-with-dnd-user": "Questo utente ha impostato il suo stato su Non Disturbare. Sei sicuro di voler iniziare una conversazione?", + "chat.rename-room": "Rinomina stanza", + "chat.rename-placeholder": "Inserisci qui il nome della stanza", + "chat.rename-help": "Il nome della stanza qui impostato sarà visibile da tutti i partecipanti nella stanza.", + "chat.leave": "Abbandona Chat", + "chat.leave-prompt": "Sei sicuro di volere abbandonare questa chat?", + "chat.leave-help": "Abbandonando questa chat perderai ogni sua traccia. Anche dopo un tuo eventuale rientro, non vedrai nessun messaggio precedente.", + "chat.in-room": "In questa stanza", + "chat.kick": "Butta fuori", + "chat.show-ip": "Mostra indirizzo IP", + "chat.owner": "Propietario stanza", + "chat.system.user-join": "%1 si è iscritto alla stanza", + "chat.system.user-leave": "%1 ha lasciato la stanza", + "chat.system.room-rename": "%2 ha rinominato questa stanza: %1", + "composer.compose": "Componi", + "composer.show_preview": "Visualizza Anteprima", + "composer.hide_preview": "Nascondi Anteprima", + "composer.user_said_in": "%1 ha detto in %2:", + "composer.user_said": "%1 ha detto:", + "composer.discard": "Sei sicuro di voler scartare questo post?", + "composer.submit_and_lock": "Invia e Blocca", + "composer.toggle_dropdown": "Mostra/Nascondi menu a discesa", + "composer.uploading": "Caricamento %1", + "composer.formatting.bold": "Grassetto", + "composer.formatting.italic": "Corsivo", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Barrato", + "composer.formatting.code": "Codice", + "composer.formatting.link": "Collegamento", + "composer.formatting.picture": "Link immagine", + "composer.upload-picture": "Carica immagine", + "composer.upload-file": "Carica file", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Seleziona una categoria", + "composer.textarea.placeholder": "Inserisci qui il contenuto del tuo post, trascina e rilascia le immagini", + "composer.schedule-for": "Discussione pianificata per", + "composer.schedule-date": "Data", + "composer.schedule-time": "Orario", + "composer.cancel-scheduling": "Annulla pianificazione", + "composer.set-schedule-date": "Imposta data", + "bootbox.ok": "OK", + "bootbox.cancel": "Annulla", + "bootbox.confirm": "Conferma", + "bootbox.submit": "Invia", + "bootbox.send": "Invia", + "cover.dragging_title": "Posizionando la foto copertina", + "cover.dragging_message": "Trascina l'immagine di copertina nella posizione desiderata e clicca su \"Salva\"", + "cover.saved": "Immagine di copertina e posizione salvati", + "thumbs.modal.title": "Gestisci le miniature della discussione", + "thumbs.modal.no-thumbs": "Non sono state trovate miniature.", + "thumbs.modal.resize-note": "Nota: Questo forum è configurato per ridimensionare le miniature degli argomenti fino ad una larghezza massima di %1px", + "thumbs.modal.add": "Aggiungi miniatura", + "thumbs.modal.remove": "Rimuovi miniatura", + "thumbs.modal.confirm-remove": "Sei sicuro di voler rimuovere questa miniatura?" +} \ No newline at end of file diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json new file mode 100644 index 0000000000..c0e0f8a4c7 --- /dev/null +++ b/public/language/it/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notifiche", + "no_notifs": "Non hai nuove notifiche", + "see_all": "Tutte le notifiche", + "mark_all_read": "Segna tutto come letto", + "back_to_home": "Indietro a %1", + "outgoing_link": "Link in uscita", + "outgoing_link_message": "Stai lasciando %1", + "continue_to": "Continua a %1", + "return_to": "Ritorna a %1", + "new_notification": "Hai una nuova notifica", + "you_have_unread_notifications": "Hai notifiche non lette.", + "all": "Tutte", + "topics": "Discussioni", + "replies": "Risposte", + "chat": "Chat", + "group-chat": "Chat di gruppo", + "follows": "Segui", + "upvote": "Voti", + "new-flags": "Nuove segnalazioni", + "my-flags": "Segnalazioni assegnate a me", + "bans": "Espulsioni", + "new_message_from": "Nuovo messaggio da %1", + "upvoted_your_post_in": "%1 ha votato positivamente il tuo post in %2.", + "upvoted_your_post_in_dual": "%1 e %2 hanno apprezzato il tuo post in %3.", + "upvoted_your_post_in_multiple": "%1 ed altri %2 hanno apprezzato il tuo post in %3.", + "moved_your_post": "%1 ha spostato il tuo post su %2", + "moved_your_topic": "%1 è stato spostato %2", + "user_flagged_post_in": "%1 ha segnalato un post in %2", + "user_flagged_post_in_dual": "%1 e %2 hanno segnalato un post in %3", + "user_flagged_post_in_multiple": "%1 ed altri %2 hanno segnalato un post in %3", + "user_flagged_user": "%1 ha segnalato un utente (%2)", + "user_flagged_user_dual": "%1 e %2 hanno segnalato un utente (%3)", + "user_flagged_user_multiple": "%1 e altri %2 hanno segnalato un utente (%3)", + "user_posted_to": "%1 ha postato una risposta a: %2", + "user_posted_to_dual": "%1 e %2 hanno postato una risposta su: %3", + "user_posted_to_multiple": "%1 ed altri %2 hanno postato una risposta su: %3", + "user_posted_topic": "%1 ha postato una nuova discussione: %2", + "user_edited_post": "%1 ha modificato un post in %2", + "user_started_following_you": "%1 ha iniziato a seguirti.", + "user_started_following_you_dual": "%1 e %2 hanno iniziato a seguirti.", + "user_started_following_you_multiple": "%1 ed altri %2 hanno iniziato a seguirti.", + "new_register": "%1 ha inviato una richiesta di registrazione.", + "new_register_multiple": "Ci sono %1 richieste di registrazione che attendono di essere esaminate.", + "flag_assigned_to_you": "Segnalazione %1 ti è stata assegnata", + "post_awaiting_review": "Post in attesa di revisione", + "profile-exported": "%1 profilo esportato, clicca per scaricare", + "posts-exported": "%1 post esportati, clicca per scaricare", + "uploads-exported": "%1 caricamenti esportati, clicca per scaricare", + "users-csv-exported": "Utenti esportati in CSV, clicca per scaricare", + "post-queue-accepted": "Il tuo post in coda è stato accettato. Clicca qui per vedere il tuo post.", + "post-queue-rejected": "Il tuo post in coda è stato rifiutato.", + "post-queue-notify": "Il post in coda ha ricevuto una notifica:
\"%1\"", + "email-confirmed": "Email Confermata", + "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", + "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", + "email-confirm-sent": "Email di conferma inviata.", + "none": "Nessuna", + "notification_only": "Solo Notifiche", + "email_only": "Solo Email", + "notification_and_email": "Email e Notifica", + "notificationType_upvote": "Quando il tuo post riceve un Mi Piace", + "notificationType_new-topic": "Quando qualcuno che segui pubblica un argomento", + "notificationType_new-reply": "Quando viene pubblicata una nuova risposta in un argomento che stai seguendo", + "notificationType_post-edit": "Quando un post viene modificato in un topic che stai guardando", + "notificationType_follow": "Quando qualcuno inizia a seguirti", + "notificationType_new-chat": "Quando ricevi un messaggio in chat", + "notificationType_new-group-chat": "Quando ricevi un messaggio di chat di gruppo", + "notificationType_group-invite": "Quando ricevi un invito ad un gruppo", + "notificationType_group-leave": "Quando un utente lascia il gruppo", + "notificationType_group-request-membership": "Quando qualcuno richiede di iscriversi a un gruppo di tua proprietà", + "notificationType_new-register": "Quando qualcuno viene aggiunto alla coda di registrazione", + "notificationType_post-queue": "Quando un nuovo post è in coda", + "notificationType_new-post-flag": "Quando un post viene segnalato", + "notificationType_new-user-flag": "Quando un utente viene segnalato" +} \ No newline at end of file diff --git a/public/language/it/pages.json b/public/language/it/pages.json new file mode 100644 index 0000000000..08f38dd5ab --- /dev/null +++ b/public/language/it/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Home", + "unread": "Discussioni non lette", + "popular-day": "Discussioni popolari oggi", + "popular-week": "Discussioni popolari questa settimana", + "popular-month": "Discussioni popolari questo mese", + "popular-alltime": "Discussioni più popolari di sempre", + "recent": "Discussioni Recenti", + "top-day": "Discussioni più votate oggi", + "top-week": "Discussioni più votate questa settimana", + "top-month": "Discussioni più votate questo mese", + "top-alltime": "Discussioni più votate", + "moderator-tools": "Strumenti di moderazione", + "flagged-content": "Contenuti Segnalati", + "ip-blacklist": "Blacklist degli IP", + "post-queue": "Coda post", + "users/online": "Utenti Online", + "users/latest": "Ultimi Utenti", + "users/sort-posts": "Utenti maggiori contributori", + "users/sort-reputation": "Utenti con la reputazione più alta", + "users/banned": "Utenti Bannati", + "users/most-flags": "Gli utenti più segnalati", + "users/search": "Ricerca Utente", + "notifications": "Notifiche", + "tags": "Tags", + "tag": "Discussioni contrassegnate come \"%1\"", + "register": "Registrati", + "registration-complete": "Registrazione completata", + "login": "Accedi al tuo account", + "reset": "Resetta password", + "categories": "Categorie", + "groups": "Gruppi", + "group": "Gruppo %1", + "chats": "Chat", + "chat": "In chat con %1", + "flags": "Segnalazioni", + "flag-details": "Dettagli segnalazione %1", + "account/edit": "Modifica di \"%1\"", + "account/edit/password": "Modificando la password di \"%1\"", + "account/edit/username": "Modificando il nome utente di \"%1\"", + "account/edit/email": "Modificando l'email di \"%1\"", + "account/info": "Informazioni dell'account", + "account/following": "Persone seguite da %1", + "account/followers": "Persone che seguono %1", + "account/posts": "Post creati da %1", + "account/latest-posts": "Ultimi post creati da %1", + "account/topics": "Discussioni create da %1", + "account/groups": "Gruppi di %1", + "account/watched_categories": "Categorie seguite da %1'", + "account/bookmarks": "%1 Post tra i favoriti", + "account/settings": "Impostazioni Utente", + "account/watched": "Discussioni seguite da %1", + "account/ignored": "Discussioni ignorate da %1", + "account/upvoted": "Post apprezzati da %1", + "account/downvoted": "Post votati negativamente da %1", + "account/best": "I migliori post di %1", + "account/controversial": "Post controversi scritti da %1", + "account/blocks": "Utenti bloccati per %1", + "account/uploads": "Inviati da %1", + "account/sessions": "Sessioni di accesso", + "confirm": "Email Confermata", + "maintenance.text": "%1 è attualmente in manutenzione. Per favore ritorna più tardi.", + "maintenance.messageIntro": "Inoltre, l'amministratore ha lasciato questo messaggio:", + "throttled.text": "%1 non è al momento disponibile a causa di un carico eccessivo. Per favore ritorna più tardi." +} \ No newline at end of file diff --git a/public/language/it/post-queue.json b/public/language/it/post-queue.json new file mode 100644 index 0000000000..ac1edc0bcb --- /dev/null +++ b/public/language/it/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Coda post", + "description": "Non ci sono post nella coda dei post.
Per abilitare questa funzione, vai in Impostazioni → Post → Coda post e abilita Coda post.", + "user": "Utente", + "category": "Categoria", + "title": "Titolo", + "content": "Contenuto", + "posted": "Pubblicato", + "reply-to": "Rispondi a \"%1\"", + "content-editable": "Clicca sul contenuto da modificare", + "category-editable": "Clicca sulla categoria da modificare", + "title-editable": "Clicca sul titolo da modificare", + "reply": "Rispondi", + "topic": "Discussione", + "accept": "Accetta", + "reject": "Rifiuta", + "remove": "Rimuovi", + "notify": "Notifica", + "notify-user": "Notifica all'utente", + "confirm-reject": "Vuoi rifiutare questo post?", + "bulk-actions": "Azioni in blocco", + "accept-all": "Accetta tutti", + "accept-selected": "Accetta selezionato", + "reject-all": "Rifiuta tutti", + "reject-all-confirm": "Vuoi rifiutare tutti i post?", + "reject-selected": "Rifiuta selezionato", + "reject-selected-confirm": "Vuoi rifiutare %1 post selezionati?", + "bulk-accept-success": "%1 post accettati", + "bulk-reject-success": "%1 post rifiutati" +} \ No newline at end of file diff --git a/public/language/it/recent.json b/public/language/it/recent.json new file mode 100644 index 0000000000..c5a3e9eae4 --- /dev/null +++ b/public/language/it/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recenti", + "day": "Giorno", + "week": "Settimana", + "month": "Mese", + "year": "Anno", + "alltime": "Sempre", + "no_recent_topics": "Non ci sono discussioni recenti.", + "no_popular_topics": "Non ci sono discussioni popolari.", + "there-is-a-new-topic": "C'è un nuova discussione.", + "there-is-a-new-topic-and-a-new-post": "C'è una nuova discussione e un nuovo post.", + "there-is-a-new-topic-and-new-posts": "C'è una nuova discussione e %1 nuovi post.", + "there-are-new-topics": "Ci sono %1 nuove discussioni.", + "there-are-new-topics-and-a-new-post": "Ci sono %1 nuove discussioni e un nuovo post.", + "there-are-new-topics-and-new-posts": "Ci sono %1 nuove discussioni e %2 nuovi post.", + "there-is-a-new-post": "C'è un nuovo post.", + "there-are-new-posts": "Ci sono %1 nuovi post.", + "click-here-to-reload": "Clicca qui per ricaricare." +} \ No newline at end of file diff --git a/public/language/it/register.json b/public/language/it/register.json new file mode 100644 index 0000000000..7faf7b1488 --- /dev/null +++ b/public/language/it/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registrati", + "cancel_registration": "Cancella Registrazione", + "help.email": "Per impostazione predefinita, la tua email sarà nascosta al pubblico.", + "help.username_restrictions": "Un nome utente univoco tra %1 e %2 caratteri. Altri possono citarti con @nome utente.", + "help.minimum_password_length": "La lunghezza della password deve essere di almeno %1 caratteri.", + "email_address": "Indirizzo Email", + "email_address_placeholder": "Inserisci l'indirizzo email", + "username": "Nome utente", + "username_placeholder": "Inserisci il Nome utente", + "password": "Password", + "password_placeholder": "Inserisci la Password", + "confirm_password": "Conferma la Password", + "confirm_password_placeholder": "Conferma la Password", + "register_now_button": "Registrati Ora", + "alternative_registration": "Altri metodi di registrazione", + "terms_of_use": "Termini di Utilizzo", + "agree_to_terms_of_use": "Accetto i Termini di Utilizzo", + "terms_of_use_error": "Devi accettare i Termini d'Utilizzo", + "registration-added-to-queue": "La tua registrazione è stata aggiunta alla coda di approvazione. Riceverai un'email quando sarà accettata da un amministratore.", + "registration-queue-average-time": "Il nostro tempo medio per l'approvazione delle iscrizioni è di %1 ore %2 minuti.", + "registration-queue-auto-approve-time": "La tua iscrizione a questo forum sarà completamente attivata entro un massimo di %1 ore.", + "interstitial.intro": "Vorremmo alcune informazioni aggiuntive per aggiornare il tuo account…", + "interstitial.intro-new": "Vorremmo alcune informazioni aggiuntive prima di poter creare il tuo account…", + "interstitial.errors-found": "Si prega di rivedere le informazioni inserite:", + "gdpr_agree_data": "Acconsento alla raccolta e al trattamento dei miei dati personali su questo sito web.", + "gdpr_agree_email": "Acconsento a ricevere email di riepilogo e notifiche da questo sito web.", + "gdpr_consent_denied": "È necessario dare il consenso a questo sito per raccogliere/elaborare i tuoi dati e per inviarti email.", + "invite.error-admin-only": "La registrazione diretta degli utenti è stata disabilitata. Si prega di contattare un amministratore per maggiori dettagli.", + "invite.error-invite-only": "La registrazione diretta degli utenti è stata disabilitata. Devi essere invitato da un utente esistente per accedere a questo forum.", + "invite.error-invalid-data": "I dati di registrazione ricevuti non corrispondono ai nostri registri. Si prega di contattare un amministratore per maggiori dettagli" +} \ No newline at end of file diff --git a/public/language/it/reset_password.json b/public/language/it/reset_password.json new file mode 100644 index 0000000000..7fb0ba28c4 --- /dev/null +++ b/public/language/it/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Resetta Password", + "update_password": "Aggiorna Password", + "password_changed.title": "Password Modificata", + "password_changed.message": "

La password è stata resettata con successo. Effettua di nuovo l'accesso.", + "wrong_reset_code.title": "Codice di reset non corretto", + "wrong_reset_code.message": "Il codice di reset ricevuto non è corretto. Prova ancora, oppure richiedi un nuovo codice.", + "new_password": "Nuova Password", + "repeat_password": "Conferma Password", + "changing_password": "Modifica della password", + "enter_email": "Per favore inserisci il tuo indirizzo email e ti invieremo un'email con le istruzioni per resettare il tuo account.", + "enter_email_address": "Inserisci l'Indirizzo Email", + "password_reset_sent": "Se l'indirizzo specificato corrisponde ad un account utente esistente, è stata inviata un'email di reset della password. Si prega di notare che sarà inviata una sola email al minuto.", + "invalid_email": "Email invalida / L'email non esiste!", + "password_too_short": "La password inserita è troppo corta, per favore inserisci una password differente.", + "passwords_do_not_match": "Le due password che hai inserito non corrispondono.", + "password_expired": "La tua password è scaduta, per favore scegline una nuova" +} \ No newline at end of file diff --git a/public/language/it/search.json b/public/language/it/search.json new file mode 100644 index 0000000000..c525455b05 --- /dev/null +++ b/public/language/it/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 risultato(i) corrispondente(i) \"%2\", (%3 secondi)", + "no-matches": "Nessuna corrispondenza trovata", + "advanced-search": "Ricerca Avanzata", + "in": "In", + "titles": "Titoli", + "titles-posts": "Titoli e Post", + "match-words": "Parole corrispondenti", + "all": "Tutti", + "any": "Chiunque", + "posted-by": "Postato da", + "in-categories": "In Categorie", + "search-child-categories": "Cerca nelle sottocategorie", + "has-tags": "Ha i tag", + "reply-count": "Numero Risposte", + "at-least": "Almeno", + "at-most": "Al massimo", + "relevance": "Rilevanza", + "post-time": "Ora Post", + "votes": "Voti", + "newer-than": "Più recente di", + "older-than": "Più vecchi di", + "any-date": "Qualsiasi data", + "yesterday": "Ieri", + "one-week": "Una settimana", + "two-weeks": "Due settimane", + "one-month": "Un mese", + "three-months": "Tre mesi", + "six-months": "Sei mesi", + "one-year": "Un anno", + "sort-by": "Ordina per", + "last-reply-time": "Ora dell'ultima risposta", + "topic-title": "Titolo discussione", + "topic-votes": "Voti discussione", + "number-of-replies": "Numero di risposte", + "number-of-views": "Numero di visite", + "topic-start-date": "Data inizio discussione", + "username": "Nome utente", + "category": "Categoria", + "descending": "In ordine decrescente", + "ascending": "In ordine crescente", + "save-preferences": "Salva preferenze", + "clear-preferences": "Cancella preferenze", + "search-preferences-saved": "Cerca nelle preferenze salvate", + "search-preferences-cleared": "Cerca nelle preferenze cancellate", + "show-results-as": "Mostra i risultati come", + "see-more-results": "Vedi altri risultati (%1)", + "search-in-category": "Cerca in \"%1\"" +} \ No newline at end of file diff --git a/public/language/it/success.json b/public/language/it/success.json new file mode 100644 index 0000000000..d5d2534894 --- /dev/null +++ b/public/language/it/success.json @@ -0,0 +1,7 @@ +{ + "success": "Riuscito", + "topic-post": "Hai postato correttamente.", + "post-queued": "Il tuo post è in coda per l'approvazione. Riceverai una notifica quando sarà accettato o rifiutato.", + "authentication-successful": "Autenticazione Riuscita", + "settings-saved": "Impostazioni salvate!" +} \ No newline at end of file diff --git a/public/language/it/tags.json b/public/language/it/tags.json new file mode 100644 index 0000000000..dcdd2a3e23 --- /dev/null +++ b/public/language/it/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Non ci sono discussioni con questo tag.", + "tags": "Tag", + "enter_tags_here": "Inserisci qui i tag, tra %1 e %2 caratteri ciascuno.", + "enter_tags_here_short": "Inserisci i tag...", + "no_tags": "Non ci sono ancora tag.", + "select_tags": "Seleziona tag" +} \ No newline at end of file diff --git a/public/language/it/top.json b/public/language/it/top.json new file mode 100644 index 0000000000..5146312ddf --- /dev/null +++ b/public/language/it/top.json @@ -0,0 +1,4 @@ +{ + "title": "In alto", + "no_top_topics": "Nessuna discussione principale" +} \ No newline at end of file diff --git a/public/language/it/topic.json b/public/language/it/topic.json new file mode 100644 index 0000000000..2c8c86d64d --- /dev/null +++ b/public/language/it/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Discussione", + "title": "Titolo", + "no_topics_found": "Nessuna discussione trovata!", + "no_posts_found": "Nessun post trovato!", + "post_is_deleted": "Questo post è eliminato!", + "topic_is_deleted": "Questa discussione è stata eliminata", + "profile": "Profilo", + "posted_by": "Postato da: %1", + "posted_by_guest": "Scritto da Ospite", + "chat": "Chat", + "notify_me": "Ricevi notifiche di nuove risposte in questa discussione", + "quote": "Cita", + "reply": "Rispondi", + "replies_to_this_post": "%1 Risposte", + "one_reply_to_this_post": "1 Risposta", + "last_reply_time": "Ultima Risposta", + "reply-as-topic": "Topic risposta", + "guest-login-reply": "Effettua l'accesso per rispondere", + "login-to-view": "Accedi per visualizzare", + "edit": "Modifica", + "delete": "Elimina", + "delete-event": "Elimina evento", + "delete-event-confirm": "Sei sicuro di voler cancellare questo evento?", + "purge": "Elimina definitivamente", + "restore": "Ripristina", + "move": "Muovi", + "change-owner": "Cambia proprietario", + "fork": "Dividi", + "link": "Collegamento", + "share": "Condividi", + "tools": "Strumenti", + "locked": "Bloccato", + "pinned": "Fissato", + "pinned-with-expiry": "Fissato fino al %1", + "scheduled": "Pianificato", + "moved": "Spostato", + "moved-from": "Spostato da %1", + "copy-ip": "Copia indirizzo IP", + "ban-ip": "Banna indirizzo IP", + "view-history": "Modifica storico", + "locked-by": "Bloccato da", + "unlocked-by": "Sbloccata da", + "pinned-by": "Fissato da", + "unpinned-by": "Liberato da", + "deleted-by": "Eliminato da", + "restored-by": "Ripristinato da", + "moved-from-by": "Spostato da %1 da", + "queued-by": "Post in coda per l'approvazione →", + "backlink": "Referenziato da", + "forked-by": "Diviso da", + "bookmark_instructions": "Clicca qui per tornare all'ultimo post letto in questa discussione.", + "flag-post": "Segnala questo post", + "flag-user": "Segnala questo utente", + "already-flagged": "Già segnalato", + "view-flag-report": "Visualizza rapporto segnalazione", + "resolve-flag": "Risolvi segnalazione", + "merged_message": "Questa discussione è stata unita a %2", + "deleted_message": "Questa discussione è stata eliminata. Solo gli utenti con diritti di gestione possono vederla.", + "following_topic.message": "Da ora riceverai notifiche quando qualcuno posterà in questa discussione.", + "not_following_topic.message": "Vedrai questa discussione nella lista delle discussioni non lette, ma non riceverai notifiche quando qualcuno risponde a questa discussione.", + "ignoring_topic.message": "Non vedrai più questa discussione tra la lista dei non letti. Sarai notificato in caso qualcuno ti menzioni o se un tuo post viene votato positivamente.", + "login_to_subscribe": "Si prega di accedere o registrarsi per potersi iscrivere a questa discussione.", + "markAsUnreadForAll.success": "Discussione segnata come non letta per tutti.", + "mark_unread": "Segna come non letto", + "mark_unread.success": "Discussione è stata marcata come non letta.", + "watch": "Segui", + "unwatch": "Non osservare più", + "watch.title": "Ricevi notifiche di nuove risposte in questa discussione", + "unwatch.title": "Smetti di osservare questa discussione", + "share_this_post": "Condividi questo Post", + "watching": "Seguito", + "not-watching": "Non Seguito", + "ignoring": "Ignorato", + "watching.description": "Notificami sulle nuove risposte.
Mostra la discussione tra le non lette.", + "not-watching.description": "Non notificarmi sulle nuove risposte.
Mostra la discussione fra le non lette se la categoria non è ignorata.", + "ignoring.description": "Non notificarmi sulle nuove risposte.
Non mostrare la discussione fra le non lette.", + "thread_tools.title": "Strumenti per la Discussione", + "thread_tools.markAsUnreadForAll": "Marca come Non Letta per tutti", + "thread_tools.pin": "Fissa Discussione", + "thread_tools.unpin": "Libera Discussione", + "thread_tools.lock": "Blocca Discussione", + "thread_tools.unlock": "Sblocca Discussione", + "thread_tools.move": "Sposta Discussione", + "thread_tools.move-posts": "Sposta Post", + "thread_tools.move_all": "Sposta Tutto", + "thread_tools.change_owner": "Cambia proprietario", + "thread_tools.select_category": "Seleziona Categoria", + "thread_tools.fork": "Dividi Discussione", + "thread_tools.delete": "Elimina Discussione", + "thread_tools.delete-posts": "Elimina Post", + "thread_tools.delete_confirm": "Sei sicuro di voler eliminare questa discussione?", + "thread_tools.restore": "Ripristina Discussione", + "thread_tools.restore_confirm": "Sei sicuro di voler ripristinare questa discussione?", + "thread_tools.purge": "Elimina definitivamente discussione", + "thread_tools.purge_confirm": "Sei sicuro di voler eliminare definitivamente questa discussione?", + "thread_tools.merge_topics": "Unisci le Discussioni", + "thread_tools.merge": "Unisci", + "topic_move_success": "Questa discussione sarà spostata in \"%1\" a breve. Clicca qui per annullare.", + "topic_move_multiple_success": "Queste discussioni saranno spostata in \"%1\" a breve. Clicca qui per annullare.", + "topic_move_all_success": "Tutte le discussioni saranno spostata in \"%1\" a breve. Clicca qui per annullare.", + "topic_move_undone": "Spostamento della discussione annullato", + "topic_move_posts_success": "I post saranno spostati a breve. Fare clic qui per annullare.", + "topic_move_posts_undone": "Spostamento post annullato", + "post_delete_confirm": "Sei sicuro di voler eliminare questo post?", + "post_restore_confirm": "Sei sicuro di voler ripristinare questo post?", + "post_purge_confirm": "Sei sicuro di voler eliminare definitivamente questo post?", + "pin-modal-expiry": "Data di scadenza", + "pin-modal-help": "Facoltativamente, è possibile impostare una data di scadenza per le discussioni fissate qui. In alternativa, è possibile lasciare vuoto questo campo per mantenere la discussione fissata fino a quando non viene liberata manualmente.", + "load_categories": "Caricamento Categorie", + "confirm_move": "Sposta", + "confirm_fork": "Dividi", + "bookmark": "Favorito", + "bookmarks": "Favoriti", + "bookmarks.has_no_bookmarks": "Non hai nessun post tra i favoriti", + "copy-permalink": "Copia collegamento permanente", + "loading_more_posts": "Caricamento altri post", + "move_topic": "Sposta Discussione", + "move_topics": "Sposta Discussioni", + "move_post": "Sposta Post", + "post_moved": "Post spostato!", + "fork_topic": "Dividi Discussione", + "enter-new-topic-title": "Inserisci il nuovo titolo della discussione", + "fork_topic_instruction": "Clicca sui post che vuoi dividere", + "fork_no_pids": "Nessun post selezionato!", + "no-posts-selected": "Nessun post selezionato!", + "x-posts-selected": "%1 post selezionato(i)", + "x-posts-will-be-moved-to-y": "%1 post sarà(anno) spostato(i) in \"%2\"", + "fork_pid_count": "%1 post selezionati", + "fork_success": "Topic Diviso con successo ! Clicca qui per andare al Topic Diviso.", + "delete_posts_instruction": "Clicca sui post che vuoi eliminare/eliminare definitivamente", + "merge_topics_instruction": "Clicca sulle discussioni che vuoi unire o cercare", + "merge-topic-list-title": "Elenco delle discussioni da unire", + "merge-options": "Opzioni di unione", + "merge-select-main-topic": "Seleziona discussione principale", + "merge-new-title-for-topic": "Nuovo titolo per la discussione", + "topic-id": "ID discussione", + "move_posts_instruction": "Clicca sui post da spostare, poi inserisci l'ID della discussione o vai alla discussione di destinazione", + "change_owner_instruction": "Clicca sui post che vuoi assegnare ad un altro utente", + "composer.title_placeholder": "Inserisci qui il titolo della discussione...", + "composer.handle_placeholder": "Inserisci qui il tuo nome/nome utente ospite", + "composer.discard": "Annulla", + "composer.submit": "Invia", + "composer.additional-options": "Opzioni aggiuntive", + "composer.schedule": "Pianifica", + "composer.replying_to": "Rispondendo a %1", + "composer.new_topic": "Nuova Discussione", + "composer.editing": "Modifica", + "composer.uploading": "caricamento...", + "composer.thumb_url_label": "Incolla l'URL della miniatura per la discussione", + "composer.thumb_title": "Aggiungi una miniatura a questa discussione", + "composer.thumb_url_placeholder": "http://esempio.com/immagine.png", + "composer.thumb_file_label": "Oppure carica un file", + "composer.thumb_remove": "Resetta i campi", + "composer.drag_and_drop_images": "Trascina e rilascia le immagini qui", + "more_users_and_guests": "%1 altro(i) utente(i) e %2 ospite(i)", + "more_users": "%1 altro(i) utente(i)", + "more_guests": "%1 altro(i) ospite(i)", + "users_and_others": "%1 e %2 altri", + "sort_by": "Ordina per", + "oldest_to_newest": "Da Vecchi a Nuovi", + "newest_to_oldest": "Da Nuovi a Vecchi", + "most_votes": "Più Voti", + "most_posts": "Più Post", + "most_views": "Più visualizzazioni", + "stale.title": "Preferisci creare una nuova discussione?", + "stale.warning": "Il topic al quale stai rispondendo è abbastanza vecchio. Vorresti piuttosto creare un nuovo topic in riferimento a questo nella tua risposta?", + "stale.create": "Crea una nuova discussione", + "stale.reply_anyway": "Rispondi comunque a questa discussione", + "link_back": "Re: [%1](%2)", + "diffs.title": "Cronologia modifiche del Post", + "diffs.description": "Questo post ha %1 revisioni. Clicca su una revisione in basso per vederne il contenuto in un momento precedente.", + "diffs.no-revisions-description": "Questo post ha %1 revisioni.", + "diffs.current-revision": "revisione corrente", + "diffs.original-revision": "revisione originale", + "diffs.restore": "Ripristina questa revisione", + "diffs.restore-description": "Una nuova revisione sarà aggiunta alla cronologia delle modifiche di questo post dopo il ripristino.", + "diffs.post-restored": "Post ripristinato con successo alla revisione precedente", + "diffs.delete": "Elimina questa revisione", + "diffs.deleted": "Revisione eliminata", + "timeago_later": "%1 dopo", + "timeago_earlier": "%1 precedente", + "first-post": "Primo post", + "last-post": "Ultimo post", + "go-to-my-next-post": "Vai al mio prossimo post", + "no-more-next-post": "Non hai più post in questa discussione", + "post-quick-reply": "Invia una risposta rapida" +} \ No newline at end of file diff --git a/public/language/it/unread.json b/public/language/it/unread.json new file mode 100644 index 0000000000..162a4cb59c --- /dev/null +++ b/public/language/it/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Non letto", + "no_unread_topics": "Non ci sono discussioni non lette.", + "load_more": "Carica di più", + "mark_as_read": "Segna come Letto", + "selected": "Selezionato", + "all": "Tutti", + "all_categories": "Tutte le categorie", + "topics_marked_as_read.success": "Discussione marcata come letta!", + "all-topics": "Tutte le Discussioni", + "new-topics": "Nuova Discussione", + "watched-topics": "Discussioni seguite", + "unreplied-topics": "Discussioni senza risposta", + "multiple-categories-selected": "Selezione multipla" +} \ No newline at end of file diff --git a/public/language/it/uploads.json b/public/language/it/uploads.json new file mode 100644 index 0000000000..60e4142f91 --- /dev/null +++ b/public/language/it/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Caricamento del file...", + "select-file-to-upload": "Seleziona un file da caricare!", + "upload-success": "File caricato con successo!", + "maximum-file-size": "Massimo %1 kb", + "no-uploads-found": "Nessun caricamento trovato", + "public-uploads-info": "I caricamenti sono pubblici, tutti i visitatori possono vederli.", + "private-uploads-info": "I caricamenti sono privati, solo gli utenti registrati possono vederli." +} \ No newline at end of file diff --git a/public/language/it/user.json b/public/language/it/user.json new file mode 100644 index 0000000000..d9055d7618 --- /dev/null +++ b/public/language/it/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Bannato", + "muted": "Silenziato", + "offline": "Non in linea", + "deleted": "Eliminato", + "username": "Nome Utente", + "joindate": "Data di iscrizione", + "postcount": "Numero Post", + "email": "Email", + "confirm_email": "Conferma Email", + "account_info": "Informazioni dell'account", + "admin_actions_label": "Azioni amministrative", + "ban_account": "BAN dell'account", + "ban_account_confirm": "Sei sicuro di voler bannare questo utente?", + "unban_account": "Togli il BAN", + "mute_account": "Silenzia account", + "unmute_account": "Disattiva silenzia account", + "delete_account": "Elimina Account", + "delete_account_as_admin": "Elimina account", + "delete_content": "Elimina contenuto account", + "delete_all": "Elimina account e contenuto", + "delete_account_confirm": "Sei sicuro di voler rendere anonimi i tuoi post ed eliminare il tuo account?\n
Questa azione è irreversibile e non sarà possibile recuperare nessuno dei tuoi dati

Inserisci la tua password per confermare che vuoi eliminare questo account.", + "delete_this_account_confirm": "Sei sicuro di voler eliminare questo account abbandonando il suo contenuto?
Questa azione è irreversibile, i post saranno resi anonimi e non sarà possibile ripristinare le associazioni dei post con l'account eliminato

", + "delete_account_content_confirm": "Sei sicuro di voler eliminare il contenuto di questo account (post/discussioni/upload)?
Questa azione è irreversibile e non sarà possibile recuperare i dati

", + "delete_all_confirm": "Sei sicuro di voler eliminare questo account e tutto il suo contenuto (post/discussioni/upload)?
Questa azione è irreversibile e non sarà possibile recuperare i dati

", + "account-deleted": "Account eliminato", + "account-content-deleted": "Contenuto dell'account eliminato", + "fullname": "Nome e Cognome", + "website": "Sito Internet", + "location": "Località", + "age": "Età", + "joined": "Iscrizione", + "lastonline": "Ultimo Accesso", + "profile": "Profilo", + "profile_views": "Visite al profilo", + "reputation": "Reputazione", + "bookmarks": "Preferiti", + "watched_categories": "Categorie seguite", + "change_all": "Cambia Tutto", + "watched": "Seguiti", + "ignored": "Ignorati", + "default-category-watch-state": "Stato di controllo della categoria predefinita", + "followers": "Da chi è seguito", + "following": "Chi segue", + "blocks": "Blocchi", + "block_toggle": "Gestisci blocco", + "block_user": "Blocca utente", + "unblock_user": "Sblocca utente", + "aboutme": "Su di me", + "signature": "Firma", + "birthday": "Data di nascita", + "chat": "Chat", + "chat_with": "Continua la chat con %1", + "new_chat_with": "Inizia una nuova chat con %1", + "flag-profile": "Segnala Profilo", + "follow": "Segui", + "unfollow": "Smetti di seguire", + "more": "Altro", + "profile_update_success": "Profilo aggiornato correttamente!", + "change_picture": "Cambia Foto", + "change_username": "Modifica il nome utente", + "change_email": "Modifica Email", + "email_same_as_password": "Inserisci la tua password attuale per continuare – hai inserito di nuovo la tua nuova email", + "edit": "Modifica", + "edit-profile": "Modifica Profilo", + "default_picture": "Icona di default", + "uploaded_picture": "Foto caricata", + "upload_new_picture": "Carica una nuova foto", + "upload_new_picture_from_url": "Carica nuova immagine da URL", + "current_password": "Password corrente", + "change_password": "Cambia la Password", + "change_password_error": "Password non valida!", + "change_password_error_wrong_current": "La tua password corrente non è corretta!", + "change_password_error_match": "Le password devono coincidere!", + "change_password_error_privileges": "Non hai il permesso di cambiare questa password.", + "change_password_success": "La tua password è stata aggiornata!", + "confirm_password": "Conferma la Password", + "password": "Password", + "username_taken_workaround": "Il nome utente che hai richiesto era già stato utilizzato, quindi lo abbiamo modificato leggermente. Ora il tuo è %1", + "password_same_as_username": "La tua password è uguale al tuo username, per piacere scegli un'altra password", + "password_same_as_email": "La tua password sembra coincidere con la tua email, per favore fornisci un'altra password.", + "weak_password": "Password debole.", + "upload_picture": "Carica foto", + "upload_a_picture": "Carica una foto", + "remove_uploaded_picture": "Elimina foto caricata", + "upload_cover_picture": "Carica immagine di copertina", + "remove_cover_picture_confirm": "Sei sicuro di voler eliminare l'immagine di copertina?", + "crop_picture": "Ritaglia immagine", + "upload_cropped_picture": "Ritaglia e carica", + "avatar-background-colour": "Colore di sfondo dell'avatar", + "settings": "Impostazioni", + "show_email": "Mostra la mia Email", + "show_fullname": "Mostra il mio nome completo", + "restrict_chats": "Abilita messaggi in chat soltanto dagli utenti che seguo", + "digest_label": "Iscriviti al Riepilogo", + "digest_description": "Abbonati agli aggiornamenti via email di questo forum (nuove notifiche e discussioni) secondo una pianificazione impostata", + "digest_off": "Spento", + "digest_daily": "Quotidiano", + "digest_weekly": "Settimanale", + "digest_biweekly": "Bisettimanale", + "digest_monthly": "Mensile", + "has_no_follower": "Questo utente non è seguito da nessuno :(", + "follows_no_one": "Questo utente non segue nessuno :(", + "has_no_posts": "Questo utente non ha ancora scritto niente.", + "has_no_best_posts": "Questo utente non ha ancora post votati positivamente.", + "has_no_topics": "Questo utente non ha ancora avviato discussioni.", + "has_no_watched_topics": "Questo utente non sta seguendo discussioni.", + "has_no_ignored_topics": "Questo utente non sta ignorando discussioni.", + "has_no_upvoted_posts": "Questo utente non ha ancora apprezzato nessun post.", + "has_no_downvoted_posts": "Questo utente non ha ancora votato negativamente alcun post", + "has_no_controversial_posts": "Questo utente non ha ancora nessun post votato negativamente.", + "has_no_blocks": "Non hai bloccato utenti.", + "email_hidden": "Email Nascosta", + "hidden": "nascosta", + "paginate_description": "Non utilizzare lo scroll infinito per discussioni e messaggi", + "topics_per_page": "Discussioni per Pagina", + "posts_per_page": "Post per Pagina", + "max_items_per_page": "Massimo %1", + "acp_language": "Lingua pagina Admin", + "notifications": "Notifiche", + "upvote-notif-freq": "Frequenza Notifiche dei Mi Piace ", + "upvote-notif-freq.all": "Tutti i Mi Piace", + "upvote-notif-freq.first": "Primo per post", + "upvote-notif-freq.everyTen": "Ogni Dieci Mi Piace", + "upvote-notif-freq.threshold": "In 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Ogni 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabilitate", + "browsing": "Impostazioni di Navigazione", + "open_links_in_new_tab": "Apri i link web in una nuova pagina", + "enable_topic_searching": "Abilita la ricerca negli argomenti", + "topic_search_help": "Se abilitata, la ricerca negli argomenti ignorerà il comportamento predefinito del browser per consentirti di cercare all'interno delle discussioni, anziché soltanto nel contenuto visibile a schermo", + "update_url_with_post_index": "Aggiorna l'url con l'indice dei posti durante la navigazione delle discussioni", + "scroll_to_my_post": "Dopo aver postato una risposta, mostra il nuovo post", + "follow_topics_you_reply_to": "Segui le discussioni a cui rispondi", + "follow_topics_you_create": "Segui le discussioni che crei", + "grouptitle": "Titolo del Gruppo", + "group-order-help": "Seleziona un gruppo e usa le frecce per ordinare i titoli", + "no-group-title": "Nessun titolo al gruppo", + "select-skin": "Seleziona uno Skin", + "select-homepage": "Seleziona una Pagina Iniziale", + "homepage": "Pagina Iniziale", + "homepage_description": "Seleziona una pagina da usare come pagina iniziale o \"Nessuna\" per usare quella di default.", + "custom_route": "Percorso Pagina Iniziale Personalizzato", + "custom_route_help": "Immettere un nome di percorso qui, senza alcuna barra precedente (es. \"recente\" o \"categoria/2/discussione-generale\")", + "sso.title": "Servizi Single-Sign-On", + "sso.associated": "Associa con", + "sso.not-associated": "Clicca qui per associare con", + "sso.dissociate": "Dissocia", + "sso.dissociate-confirm-title": "Conferma dissociazione", + "sso.dissociate-confirm": "Sei sicuro di voler dissociare il tuo account da %1?", + "info.latest-flags": "Ultime segnalazioni", + "info.no-flags": "Non è stato trovato nessun post segnalato", + "info.ban-history": "Storico dei Ban recenti", + "info.no-ban-history": "Questo utente non è mai stato bannato", + "info.banned-until": "Bannato fino %1", + "info.banned-expiry": "Scadenza", + "info.banned-permanently": "Bannato permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Non è stata data nessuna motivazione.", + "info.mute-history": "Cronologia recente Silenziato", + "info.no-mute-history": "Questo utente non è mai stato silenziato", + "info.muted-until": "Silenziato fino a %1", + "info.muted-expiry": "Scadenza", + "info.muted-no-reason": "Nessuna motivazione fornita.", + "info.username-history": "Storico del nome utente", + "info.email-history": "Storico dell'Email", + "info.moderation-note": "Nota di moderazione", + "info.moderation-note.success": "Nota di moderazione salvata", + "info.moderation-note.add": "Aggiungi nota", + "sessions.description": "Questa pagina ti permette di vedere tutte le sessioni attive nel forum ed eventualmente revocarle. Puoi revocare la tua sessione disconnettendoti dal tuo account.", + "consent.title": "I tuoi dati personali", + "consent.lead": "Questo forum raccoglie ed elabora i tuoi dati personali.", + "consent.intro": "Utilizziamo queste informazioni per personalizzare rigorosamente la tua esperienza in questa comunità, così come per associare i post che fai dal tuo account utente. Durante la fase di registrazione ti è stato chiesto di fornire un nome utente e un indirizzo e-mail, è anche possibile fornire informazioni aggiuntive per completare il tuo profilo utente su questo sito web.

Conserviamo queste informazioni per la durata del tuo account utente e puoi ritirare il consenso in qualsiasi momento cancellando il tuo account. In qualsiasi momento è possibile richiedere una copia del proprio contributo a questo sito web, tramite la pagina I tuoi dati personali.

Se hai domande o dubbi, ti invitiamo a contattare il team amministrativo di questo forum.", + "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_off": "A meno che non sia stato modificato esplicitamente nelle impostazioni utente, questa comunità non invia email riepilogative", + "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": "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.right_of_access": "Hai i privilegi di accesso", + "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_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_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 e 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_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 profilo (.json)", + "consent.export-profile-success": "Esportazione del profilo, riceverai una notifica al termine.", + "consent.export_uploads": "Esporta i contenuti caricati (.zip)", + "consent.export-uploads-success": "Esportazione dei caricamenti, riceverai una notifica al termine.", + "consent.export_posts": "Esporta i post (.csv)", + "consent.export-posts-success": "Esportazione dei post, riceverai una notifica al termine.", + "emailUpdate.intro": "Inserisci il tuo indirizzo email qui sotto. Questo forum utilizza il tuo indirizzo email per il riepilogo programmato e le notifiche, così come per il recupero dell'account in caso di perdita della password.", + "emailUpdate.optional": "Questo campo è facoltativo. Non sei obbligato a fornire il tuo indirizzo email, ma senza un'email convalidata non sarai in grado di recuperare il tuo account o di accedere con la tua email.", + "emailUpdate.required": "Questo campo è obbligatorio.", + "emailUpdate.change-instructions": "Un'email di conferma sarà inviata all'indirizzo email inserito con un link unico. Accedendo a quel link confermerai la tua proprietà dell'indirizzo email e questo diventerà attivo sul tuo account. In qualsiasi momento, sei in grado di aggiornare la tua email in archivio dalla pagina del tuo account.", + "emailUpdate.password-challenge": "Inserisci la tua password per verificare la proprietà dell'account." +} \ No newline at end of file diff --git a/public/language/it/users.json b/public/language/it/users.json new file mode 100644 index 0000000000..c843ef0433 --- /dev/null +++ b/public/language/it/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Ultimi Utenti", + "top_posters": "Utenti più attivi", + "most_reputation": "Reputazione più alta", + "most_flags": "Più segnalati", + "search": "Cerca", + "enter_username": "Inserisci il nome utente da cercare", + "search-user-for-chat": "Cerca un utente per iniziare la chat", + "load_more": "Carica di più", + "users-found-search-took": "%1 utente(i) trovato! La ricerca ha impiegato %2 secondi.", + "filter-by": "Filtra per", + "online-only": "Solo online", + "invite": "Invita", + "prompt-email": "Email:", + "groups-to-join": "Gruppi a cui iscriversi quando si accetta l'invito:", + "invitation-email-sent": "Una mail di invito è stata inviata a %1", + "user_list": "Lista Utenti", + "recent_topics": "Discussioni Recenti", + "popular_topics": "Discussioni Popolari", + "unread_topics": "Discussioni non lette", + "categories": "Categorie", + "tags": "Tag", + "no-users-found": "Nessun utente trovato!" +} \ No newline at end of file diff --git a/public/language/ja/_DO_NOT_EDIT_FILES_HERE.md b/public/language/ja/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/ja/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/ja/admin/admin.json b/public/language/ja/admin/admin.json new file mode 100644 index 0000000000..a6ae3d96f6 --- /dev/null +++ b/public/language/ja/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "NodeBBを再構築して再起動してもよろしいですか?", + "alert.confirm-restart": "NodeBBを本当に再起動しますか?", + + "acp-title": "%1| NodeBB管理画面", + "settings-header-contents": "コンテンツ", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/ja/admin/advanced/cache.json b/public/language/ja/admin/advanced/cache.json new file mode 100644 index 0000000000..bd02037b88 --- /dev/null +++ b/public/language/ja/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "投稿キャッシュ", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% がフル", + "post-cache-size": "投稿キャッシュのサイズ", + "items-in-cache": "キャッシュ内のアイテム" +} \ No newline at end of file diff --git a/public/language/ja/admin/advanced/database.json b/public/language/ja/admin/advanced/database.json new file mode 100644 index 0000000000..89ac74de61 --- /dev/null +++ b/public/language/ja/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 GB", + "uptime-seconds": "秒単位の稼働時間", + "uptime-days": "日単位の稼働時間", + + "mongo": "Mongo", + "mongo.version": "MongoDBのバージョン", + "mongo.storage-engine": "ストレージエンジン", + "mongo.collections": "コレクション", + "mongo.objects": "オブジェクト", + "mongo.avg-object-size": "平均のオブジェクトサイズ", + "mongo.data-size": "データサイズ", + "mongo.storage-size": "ストレージサイズ", + "mongo.index-size": "インデックスサイズ", + "mongo.file-size": "ファイルサイズ", + "mongo.resident-memory": "常駐メモリ", + "mongo.virtual-memory": "仮想メモリ", + "mongo.mapped-memory": "マップされたメモリ", + "mongo.bytes-in": "バイト数", + "mongo.bytes-out": "バイトアウト", + "mongo.num-requests": "リクエスト数", + "mongo.raw-info": "MongoDBのRaw情報", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redisのバージョン", + "redis.keys": "キー", + "redis.expires": "期限切れ", + "redis.avg-ttl": "平均TTL", + "redis.connected-clients": "接続されたクライアント", + "redis.connected-slaves": "接続されたスレーヴ", + "redis.blocked-clients": "ブロックされたクライアント", + "redis.used-memory": "使用されたメモリ", + "redis.memory-frag-ratio": "メモリの断片化率", + "redis.total-connections-recieved": "受け取った総接続数", + "redis.total-commands-processed": "処理された総コマンド数", + "redis.iops": "秒ごとの瞬間操作数", + "redis.iinput": "瞬時入力/秒", + "redis.ioutput": "瞬時出力/秒", + "redis.total-input": "合計入力", + "redis.total-output": "合計出力", + + "redis.keyspace-hits": "ヒットしたキー・スペース", + "redis.keyspace-misses": "見逃したキー・スペース", + "redis.raw-info": "RedisのRaw情報", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres のRaw情報" +} diff --git a/public/language/ja/admin/advanced/errors.json b/public/language/ja/admin/advanced/errors.json new file mode 100644 index 0000000000..7ca0092ff5 --- /dev/null +++ b/public/language/ja/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "%1を見つける", + "error-events-per-day": "%1 日あたりのイベント", + "error.404": "404 Not Found", + "error.503": "503 サービスは利用できません", + "manage-error-log": "エラーログの管理", + "export-error-log": "エラーログのエクスポート (CSV)", + "clear-error-log": "エラーログの消去", + "route": "ルート", + "count": "カウント", + "no-routes-not-found": "ストップ!No 404 エラーです!", + "clear404-confirm": "本当に404エラーログを消去してもよろしいですか?", + "clear404-success": "\"404 Not Found\"エラーは消去されました" +} \ No newline at end of file diff --git a/public/language/ja/admin/advanced/events.json b/public/language/ja/admin/advanced/events.json new file mode 100644 index 0000000000..e149a72c5c --- /dev/null +++ b/public/language/ja/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "イベント", + "no-events": "イベントがありません", + "control-panel": "イベントのコントロールパネル", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/ja/admin/advanced/logs.json b/public/language/ja/admin/advanced/logs.json new file mode 100644 index 0000000000..c6be55b672 --- /dev/null +++ b/public/language/ja/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "ログ", + "control-panel": "ログのコントロールパネル", + "reload": "ログを再読み込み", + "clear": "ログをクリア", + "clear-success": "ログはクリアされました!" +} \ No newline at end of file diff --git a/public/language/ja/admin/appearance/customise.json b/public/language/ja/admin/appearance/customise.json new file mode 100644 index 0000000000..0c3f036726 --- /dev/null +++ b/public/language/ja/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "カスタムヘッダー", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "カスタムヘッダーを有効にする", + + "custom-css.livereload": "ライブリロードを有効にする", + "custom-css.livereload.description": "これを有効にすると、保存ボタンをクリックするたびにアカウントのすべてのデバイスのすべてのセッションが強制的に更新されます。" +} \ No newline at end of file diff --git a/public/language/ja/admin/appearance/skins.json b/public/language/ja/admin/appearance/skins.json new file mode 100644 index 0000000000..045a17ecd7 --- /dev/null +++ b/public/language/ja/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "スキンを読み込んでいます...", + "homepage": "ホームページ", + "select-skin": "スキン選択", + "current-skin": "現在のスキン", + "skin-updated": "スキンがアップデートされました", + "applied-success": "スキン %1 が正常に適用されました", + "revert-success": "スキンがベースカラーに戻りました" +} \ No newline at end of file diff --git a/public/language/ja/admin/appearance/themes.json b/public/language/ja/admin/appearance/themes.json new file mode 100644 index 0000000000..198684fe29 --- /dev/null +++ b/public/language/ja/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "インストール済みテーマをチェックしています...", + "homepage": "ホームページ", + "select-theme": "テーマを選択", + "current-theme": "現在のテーマ", + "no-themes": "インストールされたテーマが見つかりませんでした", + "revert-confirm": "本当にNodeBBのテーマをデフォルトに復元してもよろしいですか?", + "theme-changed": "テーマが変更されました", + "revert-success": "NodeBBは正常にデフォルトテーマに戻りました。", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/ja/admin/dashboard.json b/public/language/ja/admin/dashboard.json new file mode 100644 index 0000000000..b21cbb4cc2 --- /dev/null +++ b/public/language/ja/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "フォーラムのトラフィック", + "page-views": "ページビュー", + "unique-visitors": "ユニークな訪問者", + "logins": "Logins", + "new-users": "New Users", + "posts": "投稿", + "topics": "スレッド", + "page-views-seven": "過去7日間", + "page-views-thirty": "過去30日間", + "page-views-last-day": "過去24時間", + "page-views-custom": "カスタム期間", + "page-views-custom-start": "期間開始", + "page-views-custom-end": "期間終了", + "page-views-custom-help": "表示したいページビューの日付範囲を入力します。日付選択ツールが使用できない場合、受け入れ可能な形式は次のとおりです。YYYY-MM-DD", + "page-views-custom-error": "有効な期間をフォーマットで入力してくださいYYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "全て", + + "updates": "更新", + "running-version": "NodeBB v%1 を実行しています。", + "keep-updated": "常に最新のセキュリティパッチとバグ修正のためにNodeBBが最新であることを確認してください。", + "up-to-date": "

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

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

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

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "フォーラムが開発モードで動作しています。フォーラムの動作が脆弱かもしれませんので、管理者に問い合わせてください。", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "通知", + "restart-not-required": "再起動は必要ありません", + "restart-required": "再起動が必要です", + "search-plugin-installed": "検索プラグインのインストール", + "search-plugin-not-installed": "検索プラグインがインストールされていません", + "search-plugin-tooltip": "検索機能を有効にするには、プラグインページから検索プラグインをインストールしてください", + + "control-panel": "システムコントロール", + "rebuild-and-restart": "再構築 & 再起動", + "restart": "再起動", + "restart-warning": "NodeBBを再構築または再起動すると、数秒間既存の接続がすべて切断されます。", + "restart-disabled": "適切なデーモンを介してNodeBBを実行しているようには見えないため、NodeBBの再構築および再起動は無効になっています。", + "maintenance-mode": "メンテナンスモード", + "maintenance-mode-title": "NodeBBのメンテナンスモードを設定するには、ここをクリックしてください", + "realtime-chart-updates": "リアルタイムチャートの更新", + + "active-users": "アクティブユーザー", + "active-users.users": "ユーザー", + "active-users.guests": "ゲスト", + "active-users.total": "総合", + "active-users.connections": "接続", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "登録数", + + "user-presence": "ユーザープレゼンス", + "on-categories": "カテゴリ一覧", + "reading-posts": "記事を読む", + "browsing-topics": "スレッドを閲覧", + "recent": "最近", + "unread": "未読", + + "high-presence-topics": "ハイプレゼンススレッド", + "popular-searches": "Popular Searches", + + "graphs.page-views": "ページビュー", + "graphs.page-views-registered": "ページビュー登録済み", + "graphs.page-views-guest": "ページビューゲスト", + "graphs.page-views-bot": "ページビューBot", + "graphs.unique-visitors": "ユニークな訪問者", + "graphs.registered-users": "登録したユーザー", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "最後に再起動された順", + "no-users-browsing": "閲覧中のユーザーなし", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/ja/admin/development/info.json b/public/language/ja/admin/development/info.json new file mode 100644 index 0000000000..f70dd00849 --- /dev/null +++ b/public/language/ja/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1ノードは%2ms以内に応答しました!", + "host": "ホスト", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "オンライン", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "稼働時間", + + "registered": "登録数", + "sockets": "ソケット数", + "guests": "ゲスト数", + + "info": "情報" +} \ No newline at end of file diff --git a/public/language/ja/admin/development/logger.json b/public/language/ja/admin/development/logger.json new file mode 100644 index 0000000000..864efda349 --- /dev/null +++ b/public/language/ja/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "ロガー設定", + "description": "チェックボックスをオンにすると、ターミナルにログが送信されます。パスを指定した場合、ログはファイルに保存されます。HTTPロギングは誰が、いつ、どんなユーザがあなたのフォーラムにアクセスしたかに関する統計を収集するのに便利です。HTTPリクエストだけでなく、socket.ioイベントのロギングをすることもできます。redis-cliモニタと組み合わせたsocket.ioロギングは、NodeBBの内部を学習するのに非常に役立ちます。", + "explanation": "ロギング設定をオンまたはオフにするだけで、瞬時にロギングを有効または無効にすることができます。再起動する必要はありません。", + "enable-http": "HTTPロギングを有効にする", + "enable-socket": "socket.ioイベントのロギングを有効にする", + "file-path": "ログファイルのパス", + "file-path-placeholder": "/path/to/log/file.log ::: 空白の状態でターミナルにログを表示する", + + "control-panel": "ロガーのコントロールパネル", + "update-settings": "ロガー設定を更新する" +} \ No newline at end of file diff --git a/public/language/ja/admin/extend/plugins.json b/public/language/ja/admin/extend/plugins.json new file mode 100644 index 0000000000..67a7e90ae1 --- /dev/null +++ b/public/language/ja/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "インストール済み", + "active": "アクティブ", + "inactive": "非アクティブ", + "out-of-date": "期限切れ", + "none-found": "プラグインが見つかりませんでした", + "none-active": "アクティブなプラグインが見つかりませんでした", + "find-plugins": "プラグイン一覧", + + "plugin-search": "プラグインの検索", + "plugin-search-placeholder": "プラグインを検索します...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "プラグインの並び替え", + "order-active": "アクティブなプラグインの並び替え", + "dev-interested": "NodeBBのプラグインの作成に興味がありますか?", + "docs-info": "プラグインオーサリングに関する完璧な文書はNodeBB Docs Portalにあります。", + + "order.description": "特定のプラグインは他のプラグインの前後で初期化された際に理想的な動作をします。", + "order.explanation": "プラグインはここに上から下へ指定された順序でロードされます", + + "plugin-item.themes": "テーマ", + "plugin-item.deactivate": "非アクティブ化", + "plugin-item.activate": "アクティブ化", + "plugin-item.install": "インストール", + "plugin-item.uninstall": "アンインストール", + "plugin-item.settings": "設定", + "plugin-item.installed": "インストール済み", + "plugin-item.latest": "最新", + "plugin-item.upgrade": "アップグレード", + "plugin-item.more-info": "より詳細な情報:", + "plugin-item.unknown": "不明", + "plugin-item.unknown-explanation": "このプラグインの状態を判断できませんでした。設定にミスがある可能性があります。", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "プラグインは有効化されました", + "alert.disabled": "プラグインは無効化されました", + "alert.upgraded": "プラグインはアップグレードされました", + "alert.installed": "プラグインはインストールされました", + "alert.uninstalled": "プラグインはアンインストールされました", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "プラグインは正常に非アクティブ化されました", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "プラグインは正常にインストールされました。プラグインをアクティブにしてください", + "alert.uninstall-success": "プラグインは正常に非アクティブ化とアンインストールされました。", + "alert.suggest-error": "

NodeBBはパッケージマネージャに到達できませんでした。最新バージョンのインストールを続行しましたか?

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

NodeBBはパッケージマネージャに到達できませんでした。今アップグレードすることはおすすめしません。

", + "alert.incompatible": "

NodeBBのバージョン(v%1)を v%2 にアップデートする必要があります。このプラグインの新しいバージョンをインストールするにはNodeBBをアップデートしてください。

", + "alert.possibly-incompatible": "

No Compatibility Information Found

このプラグインはインストールに必要なNodeBBのバージョンの指定がされていませんでした。完全な互換性は保証されず、NodeBBが正常に起動しなくなる可能性があります。

NodeBBが正常に起動できない場合:

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

このプラグインの最新バージョンのインストールを続行しますか?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "プラグインライセンス情報", + "license.intro": "%1のプラグインは%2の下でライセンスされています。このプラグインを有効にする前にライセンス条項を熟読してください。", + "license.cta": "このプラグインを有効にし続けますか?" +} diff --git a/public/language/ja/admin/extend/rewards.json b/public/language/ja/admin/extend/rewards.json new file mode 100644 index 0000000000..72ef6500b8 --- /dev/null +++ b/public/language/ja/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "報酬", + "condition-if-users": "ユーザーの", + "condition-is": ":", + "condition-then": "それから:", + "max-claims": "報酬が請求可能な金額", + "zero-infinite": "無限に0を入力します。", + "delete": "削除", + "enable": "有効", + "disable": "無効", + + "alert.delete-success": "報酬を削除しました", + "alert.no-inputs-found": "違法報酬 - 入力が見つかりません!", + "alert.save-success": "報酬を保存しました" +} \ No newline at end of file diff --git a/public/language/ja/admin/extend/widgets.json b/public/language/ja/admin/extend/widgets.json new file mode 100644 index 0000000000..4a847ba11e --- /dev/null +++ b/public/language/ja/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "利用可能なウィジェット", + "explanation": "ドロップダウンメニューからウィジェットを選択し、左のテンプレートのウィジェットエリアにドラッグ&ドロップします。", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "利用可能なコンテナ", + "containers.explanation": "アクティブなウィジェットの上にドラッグアンドドロップしてください", + "containers.none": "なし", + "container.well": "十分", + "container.jumbotron": "ジャンボトロン", + "container.panel": "パネル", + "container.panel-header": "パネルヘッダー", + "container.panel-body": "パネル本体", + "container.alert": "警告", + + "alert.confirm-delete": "このウィジェットを削除してもよろしいですか?", + "alert.updated": "ウィジェットが更新されました。", + "alert.update-success": "ウィジェットを保存しました", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/admins-mods.json b/public/language/ja/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/ja/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/categories.json b/public/language/ja/admin/manage/categories.json new file mode 100644 index 0000000000..388c342602 --- /dev/null +++ b/public/language/ja/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "カテゴリ設定", + "privileges": "特権", + + "name": "カテゴリ名", + "description": "カテゴリの説明", + "bg-color": "背景色", + "text-color": "テキストカラー", + "bg-image-size": "背景画像サイズ", + "custom-class": "カスタムClass", + "num-recent-replies": "# 最近の返信数", + "ext-link": "外部リンク", + "subcategories-per-page": "Subcategories per page", + "is-section": "このカテゴリをセクションとして扱う", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "画像をアップロード", + "delete-image": "削除", + "category-image": "カテゴリ画像", + "parent-category": "親カテゴリ", + "optional-parent-category": "(任意)親カテゴリ", + "top-level": "Top Level", + "parent-category-none": "(なし)", + "copy-parent": "親をコピー", + "copy-settings": "設定をコピー", + "optional-clone-settings": "カテゴリからのクローン設定(任意)", + "clone-children": "子カテゴリを複製して設定", + "purge": "カテゴリを切り離す", + + "enable": "有効", + "disable": "無効", + "edit": "編集", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "カテゴリを選択", + "set-parent-category": "親カテゴリとして設定", + + "privileges.description": "このセクションでは、サイトの一部にアクセス制御権限を設定できます。 特権は、ユーザーごとまたはグループごとに付与できます。 下のドロップダウンから有効なドメインを選択してください。", + "privileges.category-selector": "権限を設定", + "privileges.warning": ":特権の設定はすぐに有効になります。これらの設定を調整した後は、カテゴリを保存する必要はありません。", + "privileges.section-viewing": "特権の表示", + "privileges.section-posting": "権限の譲渡", + "privileges.section-moderation": "モデレート特権", + "privileges.section-other": "その他", + "privileges.section-user": "ユーザー", + "privileges.search-user": "ユーザーを追加", + "privileges.no-users": "このカテゴリにはユーザー固有の権限はありません。", + "privileges.section-group": "グループ", + "privileges.group-private": "このグループはプライベートです", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "グループを追加", + "privileges.copy-to-children": "子要素にコピーする", + "privileges.copy-from-category": "カテゴリからのコピー", + "privileges.copy-privileges-to-all-categories": "すべてのカテゴリを選択", + "privileges.copy-group-privileges-to-children": "このグループの権限をこのカテゴリの子にコピーします", + "privileges.copy-group-privileges-to-all-categories": "このグループの権限をすべてのカテゴリにコピーしてください", + "privileges.copy-group-privileges-from": "このグループの権限を別のカテゴリからコピーしてください", + "privileges.inherit": "登録済ユーザーグループに特定の権限が与えられている場合、他のすべてのグループはたとえ明示的に定義/検査されていなくても暗黙の特権があります。すべてのユーザーは登録済ユーザーのユーザーグループの一部で、この暗黙の特権が表示されるため、追加グループの特権を明示的に付与する必要はありません。", + "privileges.copy-success": "特権がコピーされました!", + + "analytics.back": "カテゴリ一覧に戻る", + "analytics.title": "カテゴリ\"%1\"のアナリティクス", + "analytics.pageviews-hourly": "図 1 –このカテゴリの時間別ページビュー", + "analytics.pageviews-daily": "図2 &ndash;このカテゴリの日ごとのページビュー数", + "analytics.topics-daily": "図3 &ndash;このカテゴリで作成された日別のスレッド", + "analytics.posts-daily": "図4 &ndash;このカテゴリで作成された日ごとの投稿", + + "alert.created": "作成されました", + "alert.create-success": "カテゴリが正常に作成されました!", + "alert.none-active": "アクティブなカテゴリがありません。", + "alert.create": "カテゴリを作成", + "alert.confirm-purge": "

本当にこのカテゴリ \"%1\"を切り離しますか?

警告!このカテゴリのすべてのスレッドと投稿が削除されます。

カテゴリをパージすると、すべてのスレッドと投稿が削除され、データベースからカテゴリが削除されます。一時的にカテゴリを削除する場合は、代わりにカテゴリを無効にすることをおすすめします。

", + "alert.purge-success": "カテゴリが切り離されました!", + "alert.copy-success": "設定をコピーしました。", + "alert.set-parent-category": "親カテゴリとして設定", + "alert.updated": "カテゴリが更新されました", + "alert.updated-success": "カテゴリID%1が正常に更新されました。", + "alert.upload-image": "カテゴリ画像をアップロード", + "alert.find-user": "ユーザーの検索", + "alert.user-search": "ここでユーザーを検索...", + "alert.find-group": "グループを探す", + "alert.group-search": "ここでグループを検索する...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "すべて折りたたむ", + "expand-all": "すべて展開する", + "disable-on-create": "作成時に無効にする", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/digest.json b/public/language/ja/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/ja/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/ja/admin/manage/groups.json b/public/language/ja/admin/manage/groups.json new file mode 100644 index 0000000000..96fe72abb7 --- /dev/null +++ b/public/language/ja/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "グループ名", + "badge": "Badge", + "properties": "Properties", + "description": "グループの説明", + "member-count": "メンバー数", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "編集", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "検索", + "create": "グループを作成", + "description-placeholder": "あなたのグループについての簡単な説明", + "create-button": "作成", + + "alerts.create-failure": "おっと

グループを作成する際に問題が発生しました。後でもう一度お試しください!", + "alerts.confirm-delete": "このグループを削除してもよろしいですか?", + + "edit.name": "名前", + "edit.description": "説明", + "edit.user-title": "メンバーのタイトル", + "edit.icon": "グループアイコン", + "edit.label-color": "グループのラベル色", + "edit.text-color": "Group Text Color", + "edit.show-badge": "バッジを表示", + "edit.private-details": "有効になっている場合、グループの参加にはグループオーナーの承認が必要です。", + "edit.private-override": "警告:プライベートグループはシステムレベルで無効になっており、このオプションは無効になります。", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "非表示", + "edit.hidden-details": "有効の場合、このグループはグループ一覧で発見することは出来ず、ユーザーが手動で招待する必要があります。", + "edit.add-user": "グループにユーザーを追加", + "edit.add-user-search": "ユーザー検索", + "edit.members": "メンバー一覧", + "control-panel": "グループのコントロールパネル", + "revert": "元に戻す", + + "edit.no-users-found": "ユーザーが見つかりません", + "edit.confirm-remove-user": "このユーザーを削除してもよろしいですか?", + "edit.save-success": "設定を保存しました。" +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/privileges.json b/public/language/ja/admin/manage/privileges.json new file mode 100644 index 0000000000..8b9ce40a8e --- /dev/null +++ b/public/language/ja/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "グローバル", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "チャット", + "upload-images": "画像をアップロード", + "upload-files": "ファイルをアップロード", + "signature": "署名", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "コンテンツを検索", + "search-users": "ユーザー検索", + "search-tags": "タグ検索", + "view-users": "ユーザーを表示", + "view-tags": "タグを表示", + "view-groups": "グループを表示", + "allow-local-login": "ローカルログイン", + "allow-group-creation": "グループを作成", + "view-users-info": "View Users Info", + "find-category": "カテゴリを検索", + "access-category": "カテゴリにアクセス", + "access-topics": "トピックスにアクセス", + "create-topics": "トピックスを作成", + "reply-to-topics": "トピックスに返信", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/registration.json b/public/language/ja/admin/manage/registration.json new file mode 100644 index 0000000000..77f1fde08b --- /dev/null +++ b/public/language/ja/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "キュー", + "description": "登録キューにはユーザーが居ません。
この機能を有効にするには、設定 → ユーザー → ユーザー登録 → へ移動し、登録タイプ の項目を \"管理者承認\"にしてください。", + + "list.name": "名前", + "list.email": "メール", + "list.ip": "IP", + "list.time": "時間", + "list.username-spam": "周波数:%1 出現: %2 信頼度: %3", + "list.email-spam": "周波数:%1 出現: %2", + "list.ip-spam": "周波数:%1 出現: %2", + + "invitations": "招待状", + "invitations.description": "以下は送信された招待状の完全なリストです。Ctrl-Fを使用して、電子メールまたはユーザー名でリストを検索します。

ユーザー名は、招待状を引き換えたユーザーのメールの右側に表示されます。", + "invitations.inviter-username": "招待者のユーザー名", + "invitations.invitee-email": "招待メール", + "invitations.invitee-username": "招待されたユーザー名(登録されている場合)", + + "invitations.confirm-delete": "この招待状を削除してもよろしいですか?" +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/tags.json b/public/language/ja/admin/manage/tags.json new file mode 100644 index 0000000000..2faef60272 --- /dev/null +++ b/public/language/ja/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "あなたのフォーラムにはまだタグが付いていません。", + "bg-color": "背景カラー", + "text-color": "テキストカラー", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "タグを作成", + "modify": "タグを変更", + "rename": "Rename Tags", + "delete": "指定されたタグを削除", + "search": "タグを検索します...", + "settings": "Tags Settings", + "name": "タグ名", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "選択したタグを削除しますか?", + "alerts.update-success": "タグが更新されました!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/uploads.json b/public/language/ja/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/ja/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/ja/admin/manage/users.json b/public/language/ja/admin/manage/users.json new file mode 100644 index 0000000000..99127f3f8d --- /dev/null +++ b/public/language/ja/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "ユーザー", + "edit": "Actions", + "make-admin": "管理者にする", + "remove-admin": "管理者を削除", + "validate-email": "電子メールの", + "send-validation-email": "確認メールを送信", + "password-reset-email": "パスワードリセットメールを送信する", + "force-password-reset": "パスワードのリセットとユーザーのログアウトを強制する", + "ban": "BANされたユーザー(s)", + "temp-ban": "一時的にユーザー(s)を禁止する", + "unban": "BANを解除されたユーザー(s)", + "reset-lockout": "ロックアウトのリセット", + "reset-flags": "最近のフラグ", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "CSVでダウンロード", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "新しいユーザー", + "filter-by": "Filter by", + "pills.unvalidated": "検証されていない", + "pills.validated": "Validated", + "pills.banned": "BANされた", + + "50-per-page": "1ページあたり50 件", + "100-per-page": "1ページあたり100 件", + "250-per-page": "1ページあたり250 件", + "500-per-page": "1ページあたり500 件", + + "search.uid": "ユーザーID別", + "search.uid-placeholder": "検索するユーザーIDを入力してください", + "search.username": "ユーザー名別", + "search.username-placeholder": "検索するユーザー名を入力してください", + "search.email": "Eメール別", + "search.email-placeholder": "検索するメールアドレスを入力してください", + "search.ip": "IP アドレス別", + "search.ip-placeholder": "検索するIPアドレスを入力してください", + "search.not-found": "ユーザーが見つかりません!", + + "inactive.3-months": "3ヶ月", + "inactive.6-months": "6ヶ月", + "inactive.12-months": "12ヶ月", + + "users.uid": "ユーザーID", + "users.username": "ユーザー名", + "users.email": "メール", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "投稿カウント", + "users.reputation": "評価", + "users.flags": "フラグ", + "users.joined": "参加", + "users.last-online": "最後オンライン", + "users.banned": "停止した", + + "create.username": "ユーザー名", + "create.email": "メール", + "create.email-placeholder": "このユーザーのメール", + "create.password": "パスワード", + "create.password-confirm": "パスワードを確認", + + "temp-ban.length": "Length", + "temp-ban.reason": "理由(任意)", + "temp-ban.hours": "時間", + "temp-ban.days": "日", + "temp-ban.explanation": "禁止期間の長さを入力します。0にすると永久に禁止と解釈されますのでご注意ください。", + + "alerts.confirm-ban": "あなたは本当にこのユーザーを永久に禁止しますか?", + "alerts.confirm-ban-multi": "あなたは本当にこれらのユーザーを恒久的に禁止しますか?", + "alerts.ban-success": "ユーザー(s)は停止されました!", + "alerts.button-ban-x": "Banされた %1 ユーザー(s)", + "alerts.unban-success": "ユーザー(s)は禁止されています!", + "alerts.lockout-reset-success": "ロックアウト(s)がリセットされました!", + "alerts.flag-reset-success": "フラグ(s)をリセット!", + "alerts.no-remove-yourself-admin": "あなたは管理者なので自分自身を削除することはできません!", + "alerts.make-admin-success": "ユーザーは管理者です", + "alerts.confirm-remove-admin": "本当にこの管理者を削除しますか?", + "alerts.remove-admin-success": "ユーザーは管理者ではなくなりました", + "alerts.make-global-mod-success": "ユーザーはグローバルモデレーターです", + "alerts.confirm-remove-global-mod": "本当にこのグローバルモデレーターを削除しますか?", + "alerts.remove-global-mod-success": "ユーザーはグローバルモデレータではなくなりました", + "alerts.make-moderator-success": "ユーザーはモデレーターです", + "alerts.confirm-remove-moderator": "このモデレーターを本当に削除しますか?", + "alerts.remove-moderator-success": "ユーザーはモデレータではなくなりました", + "alerts.confirm-validate-email": "これらのユーザー(s)の電子メール(s)を検証しますか?", + "alerts.confirm-force-password-reset": "パスワードを強制的にリセットしてこれらのユーザー(s)をログアウトさせますか?", + "alerts.validate-email-success": "電子メールが検証されました", + "alerts.validate-force-password-reset-success": "ユーザー(s)のパスワードがリセットされ、既存のセッションが取り消されました。", + "alerts.password-reset-confirm": "これらのユーザー(s)にパスワードリセットのメール(s)を送信しますか?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "ユーザー(s)は削除されました!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "ユーザーを作成", + "alerts.button-create": "作成", + "alerts.button-cancel": "キャンセル", + "alerts.error-passwords-different": "パスワードが一致する必要があります!", + "alerts.error-x": "エラー

%1

", + "alerts.create-success": "ユーザーが作成されました!", + + "alerts.prompt-email": "メール:", + "alerts.email-sent-to": "招待メールが%1に送られました。", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/ja/admin/menu.json b/public/language/ja/admin/menu.json new file mode 100644 index 0000000000..8769bc3143 --- /dev/null +++ b/public/language/ja/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "一般", + + "section-manage": "管理", + "manage/categories": "カテゴリ", + "manage/privileges": "Privileges", + "manage/tags": "タグ", + "manage/users": "ユーザー", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "登録キュー", + "manage/post-queue": "投稿キュー", + "manage/groups": "グループ", + "manage/ip-blacklist": "IPブラックリスト", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "設定", + "settings/general": "一般", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "メール", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "ゲスト", + "settings/uploads": "アップロード", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "ページ", + "settings/tags": "タグ", + "settings/notifications": "通知", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "クッキー", + "settings/web-crawler": "Webクローラー", + "settings/sockets": "接続数", + "settings/advanced": "高度", + + "settings.page-title": "%1の設定", + + "section-appearance": "外観", + "appearance/themes": "テーマ", + "appearance/skins": "スキン", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "拡張", + "extend/plugins": "プラグイン", + "extend/widgets": "ウィジェット", + "extend/rewards": "報酬", + + "section-social-auth": "ソーシャル認証", + + "section-plugins": "プラグイン", + "extend/plugins.install": "プラグインをインストール", + + "section-advanced": "高度", + "advanced/database": "データベース", + "advanced/events": "イベント", + "advanced/hooks": "Hooks", + "advanced/logs": "ログ", + "advanced/errors": "エラー", + "advanced/cache": "キャッシュ", + "development/logger": "ロガー", + "development/info": "情報", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "フォーラムを再開", + "logout": "ログアウト", + "view-forum": "フォーラムを表示", + + "search.placeholder": "Search settings", + "search.no-results": "結果がありません...", + "search.search-forum": "フォーラムでを検索", + "search.keep-typing": "結果を見るにはもっと入力してください...", + "search.start-typing": "結果を見るために入力を開始...", + + "connection-lost": "%1への接続が切れたので、再接続しています...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/advanced.json b/public/language/ja/admin/settings/advanced.json new file mode 100644 index 0000000000..72ad0891bf --- /dev/null +++ b/public/language/ja/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "メンテナンスモード", + "maintenance-mode.help": "フォーラムがメンテナンスモードの場合、すべてのリクエストは静的な一時ページにリダイレクトされます。管理者はこのリダイレクトから免除され、通常のサイトにアクセスできます。", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "メンテナンスメッセージ", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "ヘッダー", + "headers.allow-from": "NodeBBをインラインフレーム内に配置するようALLOW-FROMを設定する", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "NodeBBから送信された「Powered By」ヘッダーをカスタマイズする", + "headers.acao": "アクセス-制御-有効-原点", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "すべてのサイトへのアクセスを拒否する場合、空のままにしておいてください。", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "アクセス-制御-有効-メソッド", + "headers.acah": "アクセス-制御-有効-ヘッダー", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "トラフィック管理", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "トラフィック管理を有効にする", + "traffic.event-lag": "イベントループの場所のしきい値(ミリ秒単位)", + "traffic.event-lag-help": "この値を下げるとページの読み込み時間が短縮されますが、さらに多くのユーザーには「過剰な読み込み」メッセージが表示されます。(再起動が必要)", + "traffic.lag-check-interval": "チェック間隔(ミリ秒単位)", + "traffic.lag-check-interval-help": "この値を小さくすると、NodeBBは負荷のスパイクに対してより敏感になりますが、チェックが過敏になる可能性もあります。(再起動が必要)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/api.json b/public/language/ja/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/ja/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/chat.json b/public/language/ja/admin/settings/chat.json new file mode 100644 index 0000000000..a0551c713f --- /dev/null +++ b/public/language/ja/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "チャット設定", + "disable": "チャットは無効です", + "disable-editing": "チャットメッセージの編集/削除を無効にする", + "disable-editing-help": "管理者およびグローバルモデレーターはこの制限を免除されます", + "max-length": "チャットメッセージの最大の長さ", + "max-room-size": "チャットルームの最大ユーザー数", + "delay": "ミリ秒単位のチャットメッセージ間の時間", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/cookies.json b/public/language/ja/admin/settings/cookies.json new file mode 100644 index 0000000000..dd18946d78 --- /dev/null +++ b/public/language/ja/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU承諾", + "consent.enabled": "有効", + "consent.message": "通知メッセージ", + "consent.acceptance": "メッセージを受け取る", + "consent.link-text": "ポリシーリンクテキスト", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "空白のままにして、NodeBBのローカライズされたデフォルトを使用する", + "settings": "設定", + "cookie-domain": "セッションCookieドメイン", + "max-user-sessions": "Max active sessions per user", + "blank-default": "デフォルトの場合は空白のまま" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/email.json b/public/language/ja/admin/settings/email.json new file mode 100644 index 0000000000..c08a7faccb --- /dev/null +++ b/public/language/ja/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Eメール設定", + "address": "Eメールアドレス", + "address-help": "次の電子メールアドレスは「送信者」と「返信先」の欄に受信者が表示する電子メールを指します。", + "from": "名前から", + "from-help": "メールからの名前が表示されます。", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "電子メールテンプレートの編集", + "template.select": "電子メールテンプレートを選択", + "template.revert": "オリジナルに戻す", + "testing": "Eメールテスト", + "testing.select": "電子メールテンプレートを選択", + "testing.send": "テスト電子メールを送信する", + "testing.send-help": "テスト電子メールは、現在ログインしているユーザーの電子メールアドレスに送信されます。", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "ダイジェストアワー", + "subscriptions.hour-help": "スケジュールされたメールのダイジェストを送信する時間を表す数字を入力してください(深夜は0、午後5:00は17)これはサーバー自体に基づく時間であり、システムの時計と正確に一致しない場合があります。
次の日のダイジェストは", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/ja/admin/settings/general.json b/public/language/ja/admin/settings/general.json new file mode 100644 index 0000000000..d37f057bc1 --- /dev/null +++ b/public/language/ja/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "サイト設定", + "title": "サイトタイトル", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "サイトタイトルのURL", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "あなたのコミュニティ名", + "title.show-in-header": "ヘッダーにサイトタイトルを表示する", + "browser-title": "ブラウザ", + "browser-title-help": "ブラウザのタイトルが指定されていない場合、サイトのタイトルが使用されます。", + "title-layout": "タイトル配置", + "title-layout-help": "ブラウザのタイトルがどのように構成されるかを定義します。{pageTitle} | {browserTitle}", + "description.placeholder": "あなたのコミュニティについての簡単な説明", + "description": "サイトの説明", + "keywords": "サイトのキーワード", + "keywords-placeholder": "あなたのコミュニティを記述するキーワード、カンマ区切り", + "logo": "サイトロゴ", + "logo.image": "画像", + "logo.image-placeholder": "フォーラムのヘッダーに表示するロゴのパス", + "logo.upload": "アップロード", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "サイトロゴのURL", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "全てのテキスト:", + "log.alt-text-placeholder": "アクセシビリティのための代替テキスト", + "favicon": "お気に入りアイコン", + "favicon.upload": "アップロード", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "アップロード", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "外部サイトへのリンク", + "outgoing-links.warning-page": "送信リンクの警告ページを使用", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "警告ページをバイパスするためのホワイトリストへのドメイン", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/ja/admin/settings/group.json b/public/language/ja/admin/settings/group.json new file mode 100644 index 0000000000..d69f3a5421 --- /dev/null +++ b/public/language/ja/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "一般", + "private-groups": "プライベートグループ", + "private-groups.help": "有効の場合、グループへの参加はグループ管理人からの承認が必要です。(デフォルト: 有効)", + "private-groups.warning": "注意!このオプションが無効で、プライベートグループがある場合、自動的に公開されます。", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "グループ名の最大文字数", + "max-title-length": "Maximum Group Title Length", + "cover-image": "グループ表紙イメージ", + "default-cover": "デフォルトのカバー画像", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image\n日本語\nアップロードされたカバー画像を持たないグループで、カンマ区切りのデフォルト表紙画像を追加する。" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/guest.json b/public/language/ja/admin/settings/guest.json new file mode 100644 index 0000000000..629e93d809 --- /dev/null +++ b/public/language/ja/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "ゲストハンドルを有効にする", + "handles.enabled-help": "このオプションでは新しい投稿が表示される時に、ゲストは自分が投稿する各投稿に関連付ける名前を選択できます。無効にすると、単に「ゲスト」と呼ばれます。", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/homepage.json b/public/language/ja/admin/settings/homepage.json new file mode 100644 index 0000000000..f033b87c51 --- /dev/null +++ b/public/language/ja/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "ホームページ", + "description": "ユーザーがあなたのフォーラムのルートURLに移動するときに表示されるページを選択します。", + "home-page-route": "ホームページのルート", + "custom-route": "カスタムルート", + "allow-user-home-pages": "ユーザーホームページを有効にする", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/languages.json b/public/language/ja/admin/settings/languages.json new file mode 100644 index 0000000000..1d2f019640 --- /dev/null +++ b/public/language/ja/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "言語設定", + "description": "デフォルトの言語は、フォーラムにアクセスしているすべてのユーザーの言語表示を決定します。
個々のユーザーは、アカウント設定ページでデフォルトの言語を上書きできます。", + "default-language": "デフォルトの言語", + "auto-detect": "ゲストの自動検出言語設定" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/navigation.json b/public/language/ja/admin/settings/navigation.json new file mode 100644 index 0000000000..f3d7c35e87 --- /dev/null +++ b/public/language/ja/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "アイコン:", + "change-icon": "変更", + "route": "ルート:", + "tooltip": "ツールチップ:", + "text": "テキスト:", + "text-class": " テキストのClass:任意", + "class": "Class: optional", + "id": "ID: 任意", + + "properties": "プロパティ:", + "groups": "Groups:", + "open-new-window": "新しいウィンドウで開く", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "削除", + "btn.disable": "無効", + "btn.enable": "有効", + + "available-menu-items": "利用可能なメニューアイテム", + "custom-route": "カスタムルート", + "core": "コア", + "plugin": "プラグイン" +} diff --git a/public/language/ja/admin/settings/notifications.json b/public/language/ja/admin/settings/notifications.json new file mode 100644 index 0000000000..9be8707a2b --- /dev/null +++ b/public/language/ja/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "通知", + "welcome-notification": "ウェルカム通知", + "welcome-notification-link": "ウェルカム通知のリンク", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/pagination.json b/public/language/ja/admin/settings/pagination.json new file mode 100644 index 0000000000..61daab49e5 --- /dev/null +++ b/public/language/ja/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "ページ設定", + "enable": "スクロールでのページ自動ロードはしない", + "posts": "Post Pagination", + "topics": "スレッドページ", + "posts-per-page": "ページごとの投稿数", + "max-posts-per-page": "Maximum posts per page", + "categories": "カテゴリページ", + "topics-per-page": "ページごとのスレッド数", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/post.json b/public/language/ja/admin/settings/post.json new file mode 100644 index 0000000000..9562312fd7 --- /dev/null +++ b/public/language/ja/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "投稿の並び順", + "sorting.post-default": "標準のポスト並び順", + "sorting.oldest-to-newest": "新しい順に", + "sorting.newest-to-oldest": "新しいものから古いものへ", + "sorting.most-votes": "最も多い評価", + "sorting.most-posts": "最大投稿", + "sorting.topic-default": "デフォルトのスレッドの並び順", + "length": "投稿の長さ", + "post-queue": "Post Queue", + "restrictions": "転記の制限", + "restrictions-new": "新しいユーザー制限", + "restrictions.post-queue": "投稿キューを有効にする", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "新しいユーザー制限を有効にする", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "タイトルの最小文字数", + "restrictions.max-title-length": "タイトルの最大文字数", + "restrictions.min-post-length": "投稿の最小文字数", + "restrictions.max-post-length": "投稿の最大文字数", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "スレッドが「古い」とみなされた場合、そのスレッドに返信しようとするユーザーに警告が表示されます。", + "timestamp": "タイムスタンプ", + "timestamp.cut-off": "日付のカットオフ(日数)", + "timestamp.cut-off-help": "日付&時間は相対的な方法で表示されます(例:「3時間前」/「5日前」)。そしてさまざまな地域にローカライズされています。\n\\t\\t\\t\\t\\t言語。特定のポイントの後、このテキストは、ローカライズされた日付自体を表示するように切り替えることができます。\n\\t\\t\\t\\t\\t(例:2016年11月5日15:30)
(デフォルト:30 、または1か月)。日付を常に表示するには0に設定し、常に相対時間を表示するには空白のままにします。", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.
", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "ティーザーの投稿", + "teaser.last-post": "最後&ndash;返信がない場合は、元の投稿を含む最新の投稿を表示", + "teaser.last-reply": "最後&ndash;最新の返信を表示するか、返信がない場合は「返信なし」のプレースホルダを表示する", + "teaser.first": "最初", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "未読の設定", + "unread.cutoff": "未読のカットオフ日", + "unread.min-track-last": "最後に読み込みを行う前に追跡するスレッドの最小投稿数", + "recent": "最近の設定", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "最近のページで無視されたカテゴリのトピックのフィルタリングを無効にする", + "signature": "署名の設定", + "signature.disable": "署名を無効にする", + "signature.no-links": "署名内のリンクを無効にする", + "signature.no-images": "署名内の画像を無効にする", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "署名の最大文字数", + "composer": "Composerの設定", + "composer-help": "次の設定は、投稿者の機能や外観を制御します。\n\\t\\t\\t\\tユーザーに新しいスレッドを作成したり、既存のトピックに返信したりできます。", + "composer.show-help": "「ヘルプ」タグを表示", + "composer.enable-plugin-help": "プラグインがヘルプタブにコンテンツを追加できるようにする", + "composer.custom-help": "カスタムヘルプテキスト", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IPトラッキング", + "ip-tracking.each-post": "各投稿のトラックIPアドレス", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/reputation.json b/public/language/ja/admin/settings/reputation.json new file mode 100644 index 0000000000..0e3e3dfcd8 --- /dev/null +++ b/public/language/ja/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "評価の設定", + "disable": "レピュテーションシステムを無効にする", + "disable-down-voting": "低評価を無効にする", + "votes-are-public": "すべての投票は公開されています", + "thresholds": "アクティビティのしきい値", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "投稿をdownvoteするための最低評価", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "フラグの投稿に低評価", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/social.json b/public/language/ja/admin/settings/social.json new file mode 100644 index 0000000000..211d840d69 --- /dev/null +++ b/public/language/ja/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "投稿共有", + "info-plugins-additional": "プラグインは投稿を共有するために追加のネットワークを設定することができます", + "save-success": "投稿共有ネットワークを正常に保存しました!" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/sockets.json b/public/language/ja/admin/settings/sockets.json new file mode 100644 index 0000000000..f2940ef4f2 --- /dev/null +++ b/public/language/ja/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "再接続の設定", + "max-attempts": "最大再接続数の試行", + "default-placeholder": "デフォルト: %1", + "delay": "再接続遅延" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/sounds.json b/public/language/ja/admin/settings/sounds.json new file mode 100644 index 0000000000..b03597c4de --- /dev/null +++ b/public/language/ja/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "通知", + "chat-messages": "チャットメッセージ", + "play-sound": "再生", + "incoming-message": "受信メッセージ", + "outgoing-message": "送信メッセージ", + "upload-new-sound": "新しい音声のアップロード", + "saved": "設定を保存しました" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/tags.json b/public/language/ja/admin/settings/tags.json new file mode 100644 index 0000000000..085327dd72 --- /dev/null +++ b/public/language/ja/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "タグ設定", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "スレッドごとの最小タグ数", + "max-per-topic": "スレッドごとの最大タグ数", + "min-length": "タグの最小文字数", + "max-length": "タグの最大文字数", + "related-topics": "関連スレッド", + "max-related-topics": "表示する関連スレッドの最大数(テーマでサポートされている場合)" +} \ No newline at end of file diff --git a/public/language/ja/admin/settings/uploads.json b/public/language/ja/admin/settings/uploads.json new file mode 100644 index 0000000000..f621039a2b --- /dev/null +++ b/public/language/ja/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "投稿", + "orphans": "Orphaned Files", + "private": "アップロードしたファイルを非公開にする", + "strip-exif-data": "EXIFデータを削除", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "非公開にするファイル拡張子", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "指定した幅より広い場合は画像のサイズを変更します", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "最大ファイルサイズ(KB)", + "max-file-size-help": "(キロバイト,デフォルト:2048 KB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "ユーザーがスレッドのサムネイルをアップロードできるようにする", + "topic-thumb-size": "スレッドのサムネイルの大きさ", + "allowed-file-extensions": "ファイル拡張子が有効になりました。", + "allowed-file-extensions-help": "ここにファイル拡張子のカンマ区切りリストを入力します(例: pdf,xls,doc )。空のリストは、すべての拡張が許可されていることを意味します。", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "プロフィールの顔写真", + "allow-profile-image-uploads": "ユーザーがプロフィール画像をアップロードできるようにする。", + "convert-profile-image-png": "プロフィール画像のアップロードをPNGに変換する", + "default-avatar": "カスタムデフォルトアバター", + "upload": "アップロード", + "profile-image-dimension": "プロファイル画像の寸法", + "profile-image-dimension-help": "(ピクセルで、デフォルト:128px)", + "max-profile-image-size": "プロフィール画像の最大ファイルサイズ", + "max-profile-image-size-help": "(キロバイト単位,デフォルト:256 KB)", + "max-cover-image-size": "カバー画像の最大サイズ", + "max-cover-image-size-help": "(キロバイト,デフォルト:2,048 KB)", + "keep-all-user-images": "古いバージョンのアバターとプロファイルカバーをサーバーに保管", + "profile-covers": "プロフィールのカバー", + "default-covers": "デフォルトのカバー画像", + "default-covers-help": "アップロードされたカバー画像を持たないアカウントのカンマ区切りのデフォルト表紙画像を追加する" +} diff --git a/public/language/ja/admin/settings/user.json b/public/language/ja/admin/settings/user.json new file mode 100644 index 0000000000..dcd8a4edaf --- /dev/null +++ b/public/language/ja/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "認証", + "email-confirm-interval": "ユーザーが確認するまでEメールを再送信しない", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "ログインを許可", + "allow-login-with.username-email": "ユーザー名または電子メール", + "allow-login-with.username": "ユーザー名のみ", + "account-settings": "アカウント設定", + "gdpr_enabled": "GDPR同意収集を有効にする", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "ユーザー名の変更を無効にする", + "disable-email-changes": "Eメールの変更を無効にする", + "disable-password-changes": "パスワードの変更を無効にする", + "allow-account-deletion": "アカウントが解除されました", + "hide-fullname": "ユーザーから、フルネームが見えないようにする。", + "hide-email": "ユーザーから、Emailが見えないようにする。", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "テーマ", + "disable-user-skins": "ユーザーがカスタムスキンを選択できないようにする", + "account-protection": "アカウント保護", + "admin-relogin-duration": "管理者の再ログイン期間(分)", + "admin-relogin-duration-help": "管理セクションにアクセスするために一定時間アクセスすると再ログインが必要になるため、無効にするには0に設定します", + "login-attempts": "時間ごとのログイン試行", + "login-attempts-help": "ユーザのアカウントへのログイン試行数がこの値を超える場合、そのアカウントは予め設定された時間だけロックされます。", + "lockout-duration": "アカウントロックアウト期間(分)", + "login-days": "ユーザーのログインセッションを覚える日数", + "password-expiry-days": "指定した日数後にパスワードを強制的にリセットする", + "session-time": "セッション時間", + "session-time-days": "日", + "session-time-seconds": "秒", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "ユーザーが非アクティブと見なされてからの分数", + "online-cutoff-help": "この期間中にユーザーが何も操作を行わなかった場合、非アクティブと見なされ、リアルタイムの更新を受け取れません", + "registration": "ユーザー登録", + "registration-type": "登録タイプ", + "registration-approval-type": "登録承認タイプ", + "registration-type.normal": "標準", + "registration-type.admin-approval": "管理者承認", + "registration-type.admin-approval-ip": "IPの管理者承認", + "registration-type.invite-only": "招待のみ", + "registration-type.admin-invite-only": "管理者招待のみ", + "registration-type.disabled": "登録なし", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "ユーザーごとの最大招待数", + "max-invites": "ユーザーごとの最大招待数", + "max-invites-help": "無制限の場合は0です。管理者は無限の招待を受ける
「招待のみ」にのみ適用されます", + "invite-expiration": "招待の有効期限", + "invite-expiration-help": "#日の招待状は期限切れです。", + "min-username-length": "ユーザー名の最小文字数", + "max-username-length": "ユーザー名の最大文字数", + "min-password-length": "パスワードの最小文字数", + "min-password-strength": "最低限のパスワード強度", + "max-about-me-length": "概要の最大文字数", + "terms-of-use": "フォーラム利用規約(空白のままにしておくと無効になります)", + "user-search": "ユーザーを検索", + "user-search-results-per-page": "結果数を表示", + "default-user-settings": "デフォルトユーザー設定", + "show-email": "メールを表示", + "show-fullname": "フルネームで表示", + "restrict-chat": "フォローしたユーザーからのチャットメッセージだけを許可する", + "outgoing-new-tab": "外部リンクを新しいタブで開く", + "topic-search": "スレッド内検索を有効にする", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "お知らせを購読する", + "digest-freq.off": "オフ", + "digest-freq.daily": "デイリー", + "digest-freq.weekly": "ウィークリー", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "マンスリー", + "email-chat-notifs": "オンラインではない時に新しいチャットメッセージを受信した場合、通知メールを送信する。", + "email-post-notif": "購読中のスレッドに返信があった場合、メールで通知する。", + "follow-created-topics": "投稿したスレッドをフォローします", + "follow-replied-topics": "返信したスレッドをフォローします", + "default-notification-settings": "デフォルトの通知設定", + "categoryWatchState": "デフォルトのカテゴリウォッチ状態", + "categoryWatchState.watching": "ウォッチ中", + "categoryWatchState.notwatching": "未ウォッチ", + "categoryWatchState.ignoring": "無視中" +} diff --git a/public/language/ja/admin/settings/web-crawler.json b/public/language/ja/admin/settings/web-crawler.json new file mode 100644 index 0000000000..07f2dc0afb --- /dev/null +++ b/public/language/ja/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "クロール性の設定", + "robots-txt": "カスタムRobots.txtデフォルトの場合は空白のままにしてください", + "sitemap-feed-settings": "サイトマップとフィードの設定", + "disable-rss-feeds": "RSSフィードを無効にする", + "disable-sitemap-xml": "Sitemap.xmlを無効にする", + "sitemap-topics": "サイトマップに表示するスレッドの数", + "clear-sitemap-cache": "サイトマップのキャッシュをクリア", + "view-sitemap": "サイトマップを表示" +} \ No newline at end of file diff --git a/public/language/ja/category.json b/public/language/ja/category.json new file mode 100644 index 0000000000..cb3853e77f --- /dev/null +++ b/public/language/ja/category.json @@ -0,0 +1,23 @@ +{ + "category": "カテゴリ", + "subcategories": "サブカテゴリ", + "new_topic_button": "新規スレッド", + "guest-login-post": "投稿するにはログインしてください", + "no_topics": "まだスレッドはありません
最初のスレッドを書いてみませんか?", + "browsing": "閲覧中", + "no_replies": "返事はまだありません", + "no_new_posts": "新しい投稿はありません", + "watch": "ウォッチする", + "ignore": "無視する", + "watching": "ウォッチ中", + "not-watching": "Not Watching", + "ignoring": "無視中", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "ウォッチ中のカテゴリ", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/ja/email.json b/public/language/ja/email.json new file mode 100644 index 0000000000..4a26703cda --- /dev/null +++ b/public/language/ja/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "%1へようこそ!", + "invite": "%1からの招待です", + "greeting_no_name": "こんにちは", + "greeting_with_name": "%1さん、こんにちは", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "%1に登録していただき、ありがとうございます!", + "welcome.text2": "あなたのアカウントを完全に有効化するには、アドレスが正しいことを確認する必要があります。", + "welcome.text3": "管理者があなたの登録申請を承認しました。これから、自分のユーザ名とパスワードでログインできます。", + "welcome.cta": "ここをクリックしてメールアドレスの確認を行ってください", + "invitation.text1": "%1さんがあなたを%2に招待しました", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "パスワードリセットのリクエストを受け付けました。リクエストしていない場合はこのメールは無視してください。", + "reset.text2": "パスワードをリセットするには、次のリンクにクリックしてください:", + "reset.cta": "パスワードをリセットするには、ここをクリックしてください", + "reset.notify.subject": "パスワードをリセットしました", + "reset.notify.text1": "%1にてパスワードのリセットが行われたことをお知らせします。", + "reset.notify.text2": "もしあなたがリセットを行っていない場合は、すぐに管理者に通報してください。", + "digest.latest_topics": "%1からの新しいスレッド", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "クリックで%1を見る", + "digest.unsub.info": "このまとめはあなたの購読設定により送られました。", + "digest.day": "日", + "digest.week": "週", + "digest.month": "月", + "digest.subject": "%1のダイジェスト", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "%1さんからの新しいチャットメッセージがあります。", + "notif.chat.cta": "クリックして会話を続ける", + "notif.chat.unsub.info": "このチャットの通知はあなたの購読設定により送られました。", + "notif.post.unsub.info": "この投稿の通知はあなたの購読設定により送られました。", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "このメールはNodeBBのメーラー(emailer)が正しく設定されているか確認をするためのメールです。", + "unsub.cta": "ここをクリックして設定を変更する", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "%1さんからBANされました。", + "banned.text1": "%1さんは%2さんにBANされています。", + "banned.text2": "このBANは%1まで続きます。", + "banned.text3": "あなたがBANされた理由:", + "closing": "ありがとうございます!" +} \ No newline at end of file diff --git a/public/language/ja/error.json b/public/language/ja/error.json new file mode 100644 index 0000000000..c60b3dccc9 --- /dev/null +++ b/public/language/ja/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "無効なデータ", + "invalid-json": "無効なJSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "ログインしていません。", + "account-locked": "あなたのアカウントは一時的にロックされています", + "search-requires-login": "検索するにはアカウントが必要です - ログインするかアカウントを作成してください。", + "goback": "戻るを押すと、前のページに戻ります", + "invalid-cid": "無効なカテゴリID", + "invalid-tid": "無効なスレッドID", + "invalid-pid": "無効な投稿ID", + "invalid-uid": "無効なユーザーID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "無効なユーザー名", + "invalid-email": "無効なメール", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "無効なタイトル", + "invalid-user-data": "無効なユーザーデータ", + "invalid-password": "無効なパスワード", + "invalid-login-credentials": "ログイン資格情報が無効です", + "invalid-username-or-password": "ユーザー名とパスワードの両方を指定してください", + "invalid-search-term": "無効な検索ワード", + "invalid-url": "無効なURL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "ローカルログインシステムは、非特権アカウントに対して無効になっています", + "csrf-invalid": "セッションの期限切れと思われるため、私達はあなたのログイン状態を確認できませんでした。もう一度お試しください。", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "無効なページネーション値です。%1 から%2の値でなければありません。", + "username-taken": "ユーザー名は既に使われています", + "email-taken": "メールアドレスは既に使われています", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "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 may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "メールアドレスの確認が出来ませんでした。再度お試しください。", + "confirm-email-already-sent": "確認のメールは既に送信されています。再度送信するには、%1分後に再度お試しください。", + "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", + "username-too-short": "ユーザー名が短すぎます", + "username-too-long": "ユーザー名が長すぎます", + "password-too-long": "パスワードが長すぎます", + "reset-rate-limited": "パスワードのリセット要求が多すぎます。(料金制限あり)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "ユーザーは停止されています", + "user-banned-reason": "申し訳ありませんが、このアカウントは停止されています。 (理由: %1)", + "user-banned-reason-until": "申し訳ありませんが、このアカウントは%1(理由:%2)まで禁止されています。", + "user-too-new": "申し訳ありません。登録後に投稿を行うには%1秒お待ち下さい。", + "blacklisted-ip": "申し訳ありませんがあなたのIPアドレスは当コミュニティで停止されています。もし誤ったエラーだと思われる場合は管理者にお問い合わせください。", + "ban-expiry-missing": "この停止の終了日を入力してください。", + "no-category": "カテゴリは存在しません", + "no-topic": "スレッドは存在しません", + "no-post": "投稿は存在しません", + "no-group": "グループは存在しません", + "no-user": "ユーザーは存在しません", + "no-teaser": "ティーザーが存在しません", + "no-flag": "Flag does not exist", + "no-privileges": "あなたがこの行為する権利がありません。", + "category-disabled": "この板は無効された", + "topic-locked": "スレッドがロックされた", + "post-edit-duration-expired": "あなたが%1秒後に投稿を編集する事が許されます", + "post-edit-duration-expired-minutes": "あなたは投稿後%1 分(s)後に編集できます。", + "post-edit-duration-expired-minutes-seconds": "あなたは投稿後%1 分(s) %2 秒(s)後に編集できます。", + "post-edit-duration-expired-hours": "あなたは投稿後%1 時間(s)後に編集できます。", + "post-edit-duration-expired-hours-minutes": "あなたは投稿後%1 時間(s) %2 分(s) 後に編集できます。", + "post-edit-duration-expired-days": "あなたは投稿後%1 日(s)後に編集できます。", + "post-edit-duration-expired-days-hours": "あなたは投稿後%1 日(s) %2 時間(s)後に編集できます。", + "post-delete-duration-expired": "あなたは%1 秒(s)後に投稿を削除することが許可されています。", + "post-delete-duration-expired-minutes": "あなたは%1 分(s)後に投稿を削除することが許可されています。", + "post-delete-duration-expired-minutes-seconds": "あなたは%1 分(s) %2 秒(s) 後に投稿を削除することが許可されています。", + "post-delete-duration-expired-hours": "あなたは%1 時間(s)後に投稿を削除することが許可されています。", + "post-delete-duration-expired-hours-minutes": "あなたは%1 時間(s) %2 分(s)後に投稿を削除することが許可されています。", + "post-delete-duration-expired-days": "あなたは%1 日(s)後に投稿を削除することが許可されています。", + "post-delete-duration-expired-days-hours": "あなたは%1 日(s) %2 時間(s)後に投稿を削除することが許可されています。", + "cant-delete-topic-has-reply": "応答待ちの場合、あなたのスレッドは削除できません。", + "cant-delete-topic-has-replies": "%1 の応答待ちの場合、あなたのスレッドは削除できません。", + "content-too-short": "より長く投稿を書いて下さい。投稿にはせめて%1文字が必要です。", + "content-too-long": "より短く投稿を書いて下さい。投稿が%1文字以上が許されません。", + "title-too-short": "より長くタイトルを書いて下さい。タイトルはせめて%1文字が必要です。", + "title-too-long": "より短くタイトルを書いて下さい。タイトルは%1文字以上が許されません。", + "category-not-selected": "カテゴリが選択されていません。", + "too-many-posts": "あなたは%1秒間に一つの投稿しか許されます-少し待ってまた投稿してください", + "too-many-posts-newbie": "あなたは%2評判を得ているまで、新しいユーザーとしては、一度だけごとに%1秒を投稿することができます - 再び投稿する前にお待ちください", + "already-posting": "You are already posting", + "tag-too-short": "%1文字(s)以上でタグを入力してください。", + "tag-too-long": "%1文字(s)以内でタグを入力してください。", + "not-enough-tags": "タグが足りません。スレッドはせめて%1のタグ(s)が必要です。", + "too-many-tags": "タグが多すぎます。スレッドは%1のタグ(s)以上が許されません。", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "アップロードが完成するまでお待ちください。", + "file-too-big": "%1kBより大きいサイズファイルが許されません-より小さいファイルをアップして下さい。", + "guest-upload-disabled": "ゲストさんからのアップを無効にしています", + "cors-error": "CORSの設定が誤っているため、画像をアップロードできません", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "あなたは、この投稿をすでにブックマークしています", + "already-unbookmarked": "あなたは、この投稿をすでにブックマークから外しています", + "cant-ban-other-admins": "ほかの管理者を停止することはできません!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "あなたが唯一の管理者です。管理者としてあなた自身を削除する前に、管理者として別のユーザーを追加します。", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "削除する前に、このアカウントから管理者権限を削除してください。", + "already-deleting": "Already deleting", + "invalid-image": "無効な画像", + "invalid-image-type": "無効なイメージタイプです。許可された種類は: %1", + "invalid-image-extension": "無効なイメージのエクステンション", + "invalid-file-type": "無効なファイルタイプです。許可された種類は: %1", + "invalid-image-dimensions": "画像が大きすぎます", + "group-name-too-short": "グループ名は短すぎます。", + "group-name-too-long": "グループ名が長すぎます", + "group-already-exists": "グループ名はすでに存在しています", + "group-name-change-not-allowed": "グループ名の修正はできません", + "group-already-member": "既にこのグループの一部であります", + "group-not-member": "このグループの一部ではありません", + "group-needs-owner": "このグループには少なくとも一人のオーナーが必要です", + "group-already-invited": "このユーザーが既に招待されました", + "group-already-requested": "あなたのメンバーシップの要求が既に提出されました", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "この投稿が既に削除されました", + "post-already-restored": "この投稿が既に復元されました", + "topic-already-deleted": "このスレッドは既に削除されました", + "topic-already-restored": "このスレッドは既に復元されました", + "cant-purge-main-post": "メインの投稿を削除することはできません。代わりにスレッドを削除してください", + "topic-thumbnails-are-disabled": "スレッドのサムネイルが無効された", + "invalid-file": "無効なファイル", + "uploads-are-disabled": "アップロードが無効された", + "signature-too-long": "申し訳ありませんが、あなたの署名が%1文字より長くすることができません。", + "about-me-too-long": "申し訳ありませんが、あなたの私についての項目が%1より長くすることができません。", + "cant-chat-with-yourself": "自分にチャットすることはできません!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "チャットメッセージは%1文字より長くすることはできません。", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "あなたはこのメッセージを削除する権限を持っていません。", + "chat-edit-duration-expired": "投稿後、あなたは %1秒間(s)だけチャットメッセージを編集することを許可されています", + "chat-delete-duration-expired": "投稿後、あなたは %1秒間(s)だけチャットメッセージを削除することを許可されています", + "chat-deleted-already": "このチャットメッセージは既に削除されています", + "chat-restored-already": "このチャットメッセージは既に削除されています", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "あなたはすでにこの投稿を評価しました。", + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "自分のポストに評価することはできません。", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", + "registration-error": "Registration Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "%1アカウントの登録が無効になっています。最初にメールアドレスで登録してください", + "sso-multiple-association": "このサービスから複数のアカウントをNodeBBアカウントに関連付けることはできません。 既存のアカウントの関連付けを解除してからもう一度お試しください。", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "ユーザーが部屋にいません", + "cant-kick-self": "あなたは、グループから自分自身をキックすることが出来ません", + "no-users-selected": "ユーザー(s)が選択されていません", + "invalid-home-page-route": "ホームページのルートが無効", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "スレッドが選択されていません!!", + "cant-move-to-same-topic": "同じスレッドに投稿を移動することはできません!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "自分をブロックすることは出来ません!", + "cannot-block-privileged": "管理者またはグローバルモデレーターはブロックできません", + "cannot-block-guest": "ゲストは他のユーザーをブロックできません", + "already-blocked": "このユーザーは既にブロックされています", + "already-unblocked": "このユーザーは既にブロック解除されています", + "no-connection": "インターネット接続に問題があるようです", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/ja/flags.json b/public/language/ja/flags.json new file mode 100644 index 0000000000..5e2924c7d1 --- /dev/null +++ b/public/language/ja/flags.json @@ -0,0 +1,89 @@ +{ + "state": "状態", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "おめでとう!フラグは見つかりませんでした。", + "assignee": "譲受人", + "update": "更新", + "updated": "更新されました", + "resolved": "Resolved", + "target-purged": "このフラグが参照しているコンテンツは切り離されており、利用できません。", + + "graph-label": "Daily Flags", + "quick-filters": "クイックフィルター", + "filter-active": "このフラグのリストには1つまたは複数のフィルタが有効です。", + "filter-reset": "フィルターを削除", + "filters": "フィルターオプション", + "filter-reporterId": "報告者のユーザーID", + "filter-targetUid": "フラグを立てたユーザーID", + "filter-type": "フラグの種類", + "filter-type-all": "すべてのコンテンツ", + "filter-type-post": "投稿", + "filter-type-user": "User", + "filter-state": "状態", + "filter-assignee": "譲受人のユーザーID", + "filter-cid": "カテゴリ", + "filter-quick-mine": "私に割り当てられました", + "filter-cid-all": "全てのカテゴリ", + "apply-filters": "フィルターを追加", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "フラグを立てたユーザー", + "view-profile": "プロフィールを見る", + "start-new-chat": "新しいチャットを開始", + "go-to-target": "フラグのターゲットを表示", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "プロフィールを見る", + "user-edit": "プロフィールを編集", + + "notes": "ノートにフラグをつける", + "add-note": "ノートを追加", + "no-notes": "共有ノートはありません。", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "ノートが追加されました", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "フラグ履歴がありません", + + "state-all": "全ての状態", + "state-open": "新規/開く", + "state-wip": "進行中の作業", + "state-resolved": "解決済み", + "state-rejected": "拒否済", + "no-assignee": "割り当てられていない", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "レビューのために%1 %2 にフラグを付ける理由を指定してください。または必要に応じてクイックレポートボタンの1つを使用します。", + "modal-reason-spam": "スパム", + "modal-reason-offensive": "攻撃", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "このコンテンツを報告する理由...", + "modal-submit": "レポートを提出", + "modal-submit-success": "コンテンツはモデレーションにフラグ付けされています。", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/ja/global.json b/public/language/ja/global.json new file mode 100644 index 0000000000..5803512829 --- /dev/null +++ b/public/language/ja/global.json @@ -0,0 +1,126 @@ +{ + "home": "ホーム", + "search": "検索", + "buttons.close": "閉じる", + "403.title": "アクセス拒否", + "403.message": "アクセス権限が無いページを閲覧しようとしています。", + "403.login": "権限を持っている場合はログインすると閲覧出来ます。", + "404.title": "見つかりません", + "404.message": "あなたは存在してないページを訪問してます。ホームページに戻ります。", + "500.title": "内部エラー", + "500.message": "何か問題が発生しているようです。", + "400.title": "無効なリクエスト", + "400.message": "このリンクは不正な形式と思われます。ダブルクリックで再度お試し下さい。またはホームページに戻ります。", + "register": "登録", + "login": "ログイン", + "please_log_in": "ログインください", + "logout": "ログアウト", + "posting_restriction_info": "登録ユーザーのみが投稿可能となります.こちらからログインください。", + "welcome_back": "おかえりなさい", + "you_have_successfully_logged_in": "ログインできました", + "save_changes": "保存する", + "save": "保存", + "close": "閉じる", + "pagination": "ページ", + "pagination.out_of": "%2件中%1件目", + "pagination.enter_index": "Go to post index", + "header.admin": "管理", + "header.categories": "カテゴリ", + "header.recent": "最近", + "header.unread": "未読", + "header.tags": "タグ", + "header.popular": "人気", + "header.top": "Top", + "header.users": "ユーザー", + "header.groups": "グループ", + "header.chats": "チャット", + "header.notifications": "通知", + "header.search": "検索", + "header.profile": "プロフィール", + "header.navigation": "ナビゲーション", + "notifications.loading": "通知をロード中", + "chats.loading": "チャットをロード中", + "motd.welcome": "次世代の掲示板システムNodeBBへようこそ!", + "previouspage": "前のページ", + "nextpage": "次のページ", + "alert.success": "成功", + "alert.error": "エラー", + "alert.banned": "停止した", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "%1へのフォローを停止しました!", + "alert.follow": "%1をフォローしています!", + "users": "ユーザー", + "topics": "スレッド", + "posts": "投稿", + "x-posts": "%1 posts", + "best": "ベスト", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "高評価したユーザー", + "upvoted": "高評価", + "downvoters": "低評価したユーザー", + "downvoted": "低評価", + "views": "閲覧数", + "posters": "Posters", + "reputation": "評価", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "続きを読む", + "more": "詳しく", + "none": "None", + "posted_ago_by_guest": "%1にゲストが投稿", + "posted_ago_by": "%1に%2が投稿", + "posted_ago": "%1に投稿された", + "posted_in": "%1に投稿されました", + "posted_in_by": "%1に%2に投稿されました", + "posted_in_ago": "%1に投稿されました %2", + "posted_in_ago_by": "%1 %2に %3 が投稿", + "user_posted_ago": "%1 が%2に投稿", + "guest_posted_ago": "ゲストが%1に投稿", + "last_edited_by": "最後に編集した時間%1", + "norecentposts": "最近の投稿はありません", + "norecenttopics": "最近のスレッドはありません", + "recentposts": "最近の投稿", + "recentips": "最近ログインしたIPアドレス", + "moderator_tools": "モデレーターツール", + "online": "オンライン", + "away": "退席中", + "dnd": "取り込み中", + "invisible": "オフライン表示", + "offline": "オフライン", + "email": "メール", + "language": "言語", + "guest": "ゲスト", + "guests": "ゲスト", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum Updated", + "updated.message": "This forum has just been updated to the latest version. Click here to refresh the page.", + "privacy": "プライバシー設定", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "Delete All", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "ページ番号を入力", + "upload_file": "ファイルをアップロード", + "upload": "アップロード", + "uploads": "Uploads", + "allowed-file-types": "有効なファイル形式は %1 です。", + "unsaved-changes": "変更はまだ保存されていません。本当にこのページから離れますか?", + "reconnecting-message": "%1への接続が失われたと思われます。再接続されるまでしばらくお待ちください。", + "play": "再生", + "cookies.message": "このWEBサイトは、心地良くご使用頂くためにクッキーを使用しています。", + "cookies.accept": "了解!", + "cookies.learn_more": "もっと詳しく", + "edited": "編集されました", + "disabled": "無効", + "select": "選択", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/ja/groups.json b/public/language/ja/groups.json new file mode 100644 index 0000000000..24ea04d538 --- /dev/null +++ b/public/language/ja/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "グループ", + "view_group": "グループを閲覧", + "owner": "グループ管理人", + "new_group": "新しいグループを作成", + "no_groups_found": "グループはありません", + "pending.accept": "承認", + "pending.reject": "拒否", + "pending.accept_all": "すべて承認", + "pending.reject_all": "すべて拒否", + "pending.none": "保留中のメンバーは現在居ません", + "invited.none": "招待しているメンバーは現在居ません。", + "invited.uninvite": "招待を取り消す", + "invited.search": "このグループに招待しているユーザーを検索", + "invited.notification_title": "%1に招待されました", + "request.notification_title": "%1から、グループメンバーへのリクエストです。", + "request.notification_text": "%1は、%2のメンバーになることをリクエストしています。", + "cover-save": "保存", + "cover-saving": "保存しています", + "details.title": "グループ詳細", + "details.members": "メンバー一覧", + "details.pending": "保留中のメンバー", + "details.invited": "招待メンバー", + "details.has_no_posts": "これらのメンバーは、まだ投稿を行っていません。", + "details.latest_posts": "最近の投稿", + "details.private": "プライベート", + "details.disableJoinRequests": "参加申請を無効にする", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "寄贈/取り消す管理権限", + "details.kick": "キック", + "details.kick_confirm": "このメンバーをグループから削除", + "details.add-member": "Add Member", + "details.owner_options": "グループの管理", + "details.group_name": "グループ名", + "details.member_count": "メンバー数", + "details.creation_date": "作成日", + "details.description": "説明", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "バッジプレビュー", + "details.change_icon": "アイコンを変更", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "バッジテキスト", + "details.userTitleEnabled": "バッジを表示", + "details.private_help": "有効の場合、グループへの参加はグループ管理人からの承認が必要です。", + "details.hidden": "非表示", + "details.hidden_help": "有効の場合、このグループはグループ一覧で発見することは出来ず、ユーザーが手動で招待する必要があります。", + "details.delete_group": "グループを削除", + "details.private_system_help": "プライベートグループは、システムレベルで無効です。このオプションは何もしません。", + "event.updated": "グループ詳細が更新されました。", + "event.deleted": "グループ\"%1\"は削除されました。", + "membership.accept-invitation": "招待を受ける", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "招待を保留", + "membership.join-group": "グループへ参加", + "membership.leave-group": "グループから離脱", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "拒否", + "new-group.group_name": "グループ名:", + "upload-group-cover": "グループのカバーをアップロード", + "bulk-invite-instructions": "ユーザー名をカンマ区切りして入力することで、このグループへ招待します。", + "bulk-invite": "バルク招待", + "remove_group_cover_confirm": "カバー写真を削除してもよろしいですか?" +} \ No newline at end of file diff --git a/public/language/ja/ip-blacklist.json b/public/language/ja/ip-blacklist.json new file mode 100644 index 0000000000..2bcc4d5b9a --- /dev/null +++ b/public/language/ja/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "IPブラックリストをこちらで設定します。", + "description": "場合によては、ユーザーアカウントを禁止せざるを得ないこともあります。フォーラムを保護するための最善の方法は、特定のIPまたはIPの範囲へのアクセスを制限することです。このような場合は、厄介なIPアドレスまたはCIDRブロック全体をこのブラックリストに追加することができ、新しいアカウントにログインしたり登録することができなくなります。", + "active-rules": "アクティブルール", + "validate": "ブラックリストの検証", + "apply": "ブラックリスト", + "hints": "構文のヒント", + "hint-1": "1行に1つのIPアドレスを定義します。あなたはCIDR形式(例: 192.168.100.0/22 )に従っている限り、IPブロックを追加できます。", + "hint-2": "記号でコメントを追加することができます。", + + "validate.x-valid": "%2のルールのうち%1 のルール(s) が有効です。", + "validate.x-invalid": "次の%1 ルールは無効:", + + "alerts.applied-success": "ブラックリストに適用されました", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/ja/language.json b/public/language/ja/language.json new file mode 100644 index 0000000000..ecd635ee30 --- /dev/null +++ b/public/language/ja/language.json @@ -0,0 +1,5 @@ +{ + "name": "日本語", + "code": "ja", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/ja/login.json b/public/language/ja/login.json new file mode 100644 index 0000000000..3cbfb4beee --- /dev/null +++ b/public/language/ja/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "ユーザー名またはメールアドレス", + "username": "ユーザー名", + "remember_me": "ログイン情報を記憶", + "forgot_password": "パスワードを忘れましたか?", + "alternative_logins": "ほかのログイン方法", + "failed_login_attempt": "ログインに成功", + "login_successful": "ログインしました!", + "dont_have_account": "アカウントをもっていませんか?", + "logged-out-due-to-inactivity": "しばらく操作されていなかったため、管理パネルよりログアウトされました。", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/ja/modules.json b/public/language/ja/modules.json new file mode 100644 index 0000000000..5873c2e698 --- /dev/null +++ b/public/language/ja/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "とチャット", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "送信", + "chat.no_active": "チャットはありません。", + "chat.user_typing": "%1 が入力中 ...", + "chat.user_has_messaged_you": "%1さんからメッセージが届いています。", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "チャットメッセージの履歴を表示するには、受信者を選択してください", + "chat.no-users-in-room": "ルームには誰も居ません", + "chat.recent-chats": "最近のチャット", + "chat.contacts": "お問い合わせ", + "chat.message-history": "メッセージ履歴", + "chat.message-deleted": "Message Deleted", + "chat.options": "チャット設定", + "chat.pop-out": "チャットを別ウィンドウで表示する", + "chat.minimize": "最小化", + "chat.maximize": "最大化", + "chat.seven_days": "7日間", + "chat.thirty_days": "30日間", + "chat.three_months": "3ヶ月", + "chat.delete_message_confirm": "本当にこのメッセージを削除しますか?", + "chat.retrieving-users": "ユーザーを所得しています…", + "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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "このユーザーのステータスはDnD(Do not disturb:取り込み中)に設定されています。あなたはまだチャットしたいですか?", + "chat.rename-room": "部屋名を変更", + "chat.rename-placeholder": "部屋名を入力", + "chat.rename-help": "設定した部屋名は、この部屋のすべての参加者に表示されます", + "chat.leave": "チャットから退出", + "chat.leave-prompt": "このチャットから退出しますか?", + "chat.leave-help": "このチャットを終了すると、このチャットからあなたが削除されます。 再度参加しても、前のチャット履歴は表示されません。", + "chat.in-room": "この部屋内", + "chat.kick": "キック", + "chat.show-ip": "IP表示", + "chat.owner": "部屋の管理者", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "構成", + "composer.show_preview": "プレビュー表示", + "composer.hide_preview": "プレビュー非表示", + "composer.user_said_in": "%2で%1が発言 :", + "composer.user_said": "%1 の発言:", + "composer.discard": "本当にこの投稿を破棄しても構いませんか?", + "composer.submit_and_lock": "送信してロック", + "composer.toggle_dropdown": "ドロップダウンの表示切り替え", + "composer.uploading": " %1をアップロード中", + "composer.formatting.bold": "太字", + "composer.formatting.italic": "斜体", + "composer.formatting.list": "一覧", + "composer.formatting.strikethrough": "取り消し線", + "composer.formatting.code": "コード", + "composer.formatting.link": "リンク", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "画像をアップロード", + "composer.upload-file": "ファイルをアップロード", + "composer.zen_mode": "Zen モード", + "composer.select_category": "カテゴリを選択", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "キャンセル", + "bootbox.confirm": "確認", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "カバー写真の配置について", + "cover.dragging_message": "カバー写真をドラッグで目的位置に移動し、\"保存\"をクリックします。", + "cover.saved": "カバー写真と配置設定を保存しました。", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/ja/notifications.json b/public/language/ja/notifications.json new file mode 100644 index 0000000000..5bdd78e97b --- /dev/null +++ b/public/language/ja/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "お知らせ", + "no_notifs": "新しい通知はありません", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "%1へ戻る", + "outgoing_link": "外部サイトへのリンク", + "outgoing_link_message": "%1から離れようとしています", + "continue_to": "%1へ行く", + "return_to": "%1へ戻る", + "new_notification": "新しい通知です", + "you_have_unread_notifications": "未読の通知があります。", + "all": "全て", + "topics": "スレッド", + "replies": "返信", + "chat": "チャット", + "group-chat": "Group Chats", + "follows": "フォロー", + "upvote": "高評価", + "new-flags": "新しいフラグ", + "my-flags": "あなたにフラグがつきました", + "bans": "Ban", + "new_message_from": "%1からの新しいメッセージ", + "upvoted_your_post_in": "%1さんが%2に高評価をつけました。", + "upvoted_your_post_in_dual": "%1さんと%2さんが%3に高評価をつけました。", + "upvoted_your_post_in_multiple": "%1 と%2 などのユーザーが、あなたの投稿 %3 に高評価をつけました。", + "moved_your_post": "%1 は、あなたの投稿 %2 に移動しました。", + "moved_your_topic": "%1%2 を移動しました。", + "user_flagged_post_in": "%1%2 の投稿にフラグを付けました。", + "user_flagged_post_in_dual": "%1%2%3 の投稿にフラグを立てました。", + "user_flagged_post_in_multiple": "%1 と %2 または他のユーザーが投稿 %3にフラグをつけました。", + "user_flagged_user": "%1さんはユーザープロフィールにフラグを付けました(%2)", + "user_flagged_user_dual": "%1さんと%2さんは、ユーザープロフィール(%3)にフラグをつけました。", + "user_flagged_user_multiple": "%1さんと%2さんなどのユーザーがユーザープロフィール(%3)にフラグをつけました。", + "user_posted_to": "%1さんは %2に返信しました。", + "user_posted_to_dual": "%1%2 は、返信しました: %3", + "user_posted_to_multiple": "%1 と %2 または他のユーザーが返信しました: %3", + "user_posted_topic": "%1 が新しいスレッドを投稿しました。: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1があなたをフォローしました。", + "user_started_following_you_dual": "%1%2 があなたをフォローしました。", + "user_started_following_you_multiple": "%1 と %2 または他のユーザーがあなたをフォローしました。", + "new_register": "%1が登録リクエストを送りました。", + "new_register_multiple": "%1の登録リクエストがレビュー待ちです。", + "flag_assigned_to_you": "フラグ %1はあなたに割当てられました", + "post_awaiting_review": "レビュー待ちの投稿", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Eメールが確認されました", + "email-confirmed-message": "メールアドレス検証をして頂き、ありがとうございます。あなたのアカウントは完全にアクティブになりました。", + "email-confirm-error-message": "あなたのEメールアドレス検証に問題があります。コードが無効か、期限切れです。", + "email-confirm-sent": "確認メールが送信されました。", + "none": "なし", + "notification_only": "通知のみ", + "email_only": "メールのみ", + "notification_and_email": "通知 & メール", + "notificationType_upvote": "誰かがあなたの投稿を評価したとき", + "notificationType_new-topic": "フォロワーがスレッドを投稿したとき", + "notificationType_new-reply": "あなたが見ているトピックに新しい返信が投稿されたとき", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "誰かがあなたをフォローしたとき", + "notificationType_new-chat": "チャットメッセージを受信したとき", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "グループ招待を受けたとき", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "誰かがあなたのグループへの参加を要求したとき", + "notificationType_new-register": "誰かが登録キューに追加されたとき", + "notificationType_post-queue": "新しい投稿がキューに入ったとき", + "notificationType_new-post-flag": "投稿にフラグが立てられたとき", + "notificationType_new-user-flag": "ユーザーにフラグが立てられたとき" +} \ No newline at end of file diff --git a/public/language/ja/pages.json b/public/language/ja/pages.json new file mode 100644 index 0000000000..e75f73c21a --- /dev/null +++ b/public/language/ja/pages.json @@ -0,0 +1,65 @@ +{ + "home": "ホーム", + "unread": "未読スレッド", + "popular-day": "本日人気のスレッド", + "popular-week": "今週人気のスレッド", + "popular-month": "今月人気のスレッド", + "popular-alltime": "人気のスレッド", + "recent": "最新スレッド", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "モデレーターツール", + "flagged-content": "フラグ付きコンテンツ", + "ip-blacklist": "IPブラックリスト", + "post-queue": "投稿キュー", + "users/online": "オンラインのユーザー", + "users/latest": "最近のユーザー", + "users/sort-posts": "ほとんどの投稿を持つユーザー", + "users/sort-reputation": "一番評価の高いユーザー", + "users/banned": "BANされたユーザー", + "users/most-flags": "最もフラグのついたユーザー", + "users/search": "ユーザーを検索", + "notifications": "通知", + "tags": "タグ", + "tag": "Topics tagged under "%1"", + "register": "アカウントを登録", + "registration-complete": "登録完了", + "login": "あなたのアカウントでログイン", + "reset": "あなたのアカウントをリセット", + "categories": "カテゴリ", + "groups": "グループ", + "group": "%1 グループ", + "chats": "チャット", + "chat": "%1とチャットします", + "flags": "フラグ", + "flag-details": "フラグ%1の詳細", + "account/edit": "編集中 \"%1\"", + "account/edit/password": "\"%1\"のパスワードを編集中", + "account/edit/username": "\"%1\"のユーザー名を編集中", + "account/edit/email": "\"%1\"のEmailを編集中", + "account/info": "アカウント情報", + "account/following": "%1がフォロー中", + "account/followers": "%1のフォロワー", + "account/posts": "%1さんの投稿", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "%1がスレッドを作成しました", + "account/groups": "%1 グループ", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1のブックマークされた投稿", + "account/settings": "ユーザー設定", + "account/watched": " %1がスレッドをウォッチ済みに設定しました", + "account/ignored": "%1がスレッドを無視済みに設定しました", + "account/upvoted": "%1が投稿を高評価しました", + "account/downvoted": "%1が投稿を低評価しました", + "account/best": "%1のベストな投稿", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "%1のユーザーをブロックしました", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Eメールが確認されました", + "maintenance.text": "%1 は現在メンテナンスを実行中です。お手数ですが、時間をずらしてお越しください。", + "maintenance.messageIntro": "さらに、管理者はこちらのメッセージを残しました:", + "throttled.text": "%1は現在、過負荷のため使用できません。お手数ですが、時間をずらしてお越しください。" +} \ No newline at end of file diff --git a/public/language/ja/post-queue.json b/public/language/ja/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/ja/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/ja/recent.json b/public/language/ja/recent.json new file mode 100644 index 0000000000..57aed3f560 --- /dev/null +++ b/public/language/ja/recent.json @@ -0,0 +1,19 @@ +{ + "title": "最近の更新", + "day": "1日以内", + "week": "1週間以内", + "month": "1ヶ月以内", + "year": "年", + "alltime": "全て", + "no_recent_topics": "最近のスレッドはありません。", + "no_popular_topics": "人気スレッドはありません。", + "there-is-a-new-topic": "新しいスレッドがあります。", + "there-is-a-new-topic-and-a-new-post": "新しいスレッドと投稿があります。", + "there-is-a-new-topic-and-new-posts": "新しいスレッドと%1件の投稿があります。", + "there-are-new-topics": "新しいスレッドが%1個あります。", + "there-are-new-topics-and-a-new-post": "新しいスレッドがと投稿が%1件あります。", + "there-are-new-topics-and-new-posts": "新しいスレッドが%1件、新しい投稿が%2件あります。", + "there-is-a-new-post": "新しい投稿があります。", + "there-are-new-posts": "新しい投稿が%1件あります。", + "click-here-to-reload": "ここを押して、更新します。" +} \ No newline at end of file diff --git a/public/language/ja/register.json b/public/language/ja/register.json new file mode 100644 index 0000000000..926f1db44f --- /dev/null +++ b/public/language/ja/register.json @@ -0,0 +1,32 @@ +{ + "register": "登録", + "cancel_registration": "登録をキャンセル", + "help.email": "初期設定ではメールアドレスは公開されません。", + "help.username_restrictions": "%1から%2 文字までのユニークなユーザー名.ツイッター(twitter)の@username 方式でメンションすることができます。", + "help.minimum_password_length": "パスワードには最小 %1 文字が必要です。", + "email_address": "メールアドレス", + "email_address_placeholder": "メールアドレスを入力ください", + "username": "ユーザー名", + "username_placeholder": "ユーザー名を入力してください", + "password": "パスワード", + "password_placeholder": "パスワードを入力してください", + "confirm_password": "パスワード再入力", + "confirm_password_placeholder": "パスワード再入力してください", + "register_now_button": "今すぐ登録する", + "alternative_registration": "ほかの登録方法", + "terms_of_use": "利用規約", + "agree_to_terms_of_use": "利用規約に同意する", + "terms_of_use_error": "あなたは利用規約に同意する必要があります", + "registration-added-to-queue": "あなたの登録申請は承認キューに追加されました。管理者によって受け入れられた時に、メールを受信します。", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/ja/reset_password.json b/public/language/ja/reset_password.json new file mode 100644 index 0000000000..7f26204d4c --- /dev/null +++ b/public/language/ja/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "パスワードをリセット", + "update_password": "パスワードを更新", + "password_changed.title": "パスワードを更新しました", + "password_changed.message": "

パスワードをリセットできました.こちらでログインしてください。", + "wrong_reset_code.title": "リセットコードが正しくありません", + "wrong_reset_code.message": "リセットコードは正しくありません。もう一度入力するか、新しいリセットコードをリクエストすることができます。", + "new_password": "新しいパスワード", + "repeat_password": "パスワードを再入力", + "changing_password": "Changing Password", + "enter_email": "メールアドレスを入力してください。パスワードをリセットする方法をメールで送信します。", + "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.", + "invalid_email": "このメールアドレスは存在しません", + "password_too_short": "パスワードが短すぎますので、違うパスワードを選んでください。", + "passwords_do_not_match": "パスワードが一致しません。", + "password_expired": "パスワードが期限切れになりましたので、新しいパスワードを選んでください。" +} \ No newline at end of file diff --git a/public/language/ja/search.json b/public/language/ja/search.json new file mode 100644 index 0000000000..bc2452a386 --- /dev/null +++ b/public/language/ja/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 件の結果(s) キーワード \"%2\", (検索時間 %3 秒)", + "no-matches": "見つかりませんでした", + "advanced-search": "高度な検索", + "in": "検索範囲", + "titles": "タイトル", + "titles-posts": "タイトルと投稿", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "投稿者:", + "in-categories": "カテゴリ", + "search-child-categories": "チャイルドカテゴリを検索する", + "has-tags": "タグあり", + "reply-count": "返信数", + "at-least": "最低", + "at-most": "一番", + "relevance": "妥当性", + "post-time": "投稿時間", + "votes": "Votes", + "newer-than": "より新しい", + "older-than": "より古い", + "any-date": "すべて", + "yesterday": "昨日", + "one-week": "1週間", + "two-weeks": "2週間", + "one-month": "1ヶ月", + "three-months": "3ヶ月", + "six-months": "6ヶ月", + "one-year": "1年", + "sort-by": "並び替え", + "last-reply-time": "最後の返信時間", + "topic-title": "スレッドのタイトル", + "topic-votes": "Topic votes", + "number-of-replies": "返信数", + "number-of-views": "表示数", + "topic-start-date": "スレッド開始日", + "username": "ユーザー名", + "category": "カテゴリ", + "descending": "降順", + "ascending": "昇順", + "save-preferences": "設定を保存", + "clear-preferences": "設定をクリア", + "search-preferences-saved": "検索設定は保存されました", + "search-preferences-cleared": "検索設定をクリア", + "show-results-as": "結果を表示", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/ja/success.json b/public/language/ja/success.json new file mode 100644 index 0000000000..ad6f53e5ae --- /dev/null +++ b/public/language/ja/success.json @@ -0,0 +1,7 @@ +{ + "success": "成功しました", + "topic-post": "投稿に成功しました", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "認証に成功しました", + "settings-saved": "設定を保存しました。" +} \ No newline at end of file diff --git a/public/language/ja/tags.json b/public/language/ja/tags.json new file mode 100644 index 0000000000..139e95c2dc --- /dev/null +++ b/public/language/ja/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "このタグに関連するスレッドはありません。", + "tags": "タグ", + "enter_tags_here": "ここにタグを入力します。一つのタグが%1から%2までの文字にして下さい。", + "enter_tags_here_short": "タグを入れます…", + "no_tags": "タグがありません", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/ja/top.json b/public/language/ja/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/ja/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/ja/topic.json b/public/language/ja/topic.json new file mode 100644 index 0000000000..9d9fd211c9 --- /dev/null +++ b/public/language/ja/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "スレッド", + "title": "Title", + "no_topics_found": "スレッドが見つかりません!", + "no_posts_found": "投稿はありません!", + "post_is_deleted": "この投稿が削除されました!", + "topic_is_deleted": "このスレッドは削除されました!", + "profile": "プロフィール", + "posted_by": "%1さんが投稿", + "posted_by_guest": "ゲストさんが投稿", + "chat": "チャット", + "notify_me": "このスレッドに新しく投稿された時に通知する", + "quote": "引用", + "reply": "返信", + "replies_to_this_post": "%1 件の返信", + "one_reply_to_this_post": "1 件の返信", + "last_reply_time": "最後の返信", + "reply-as-topic": "スレッドとして返信する", + "guest-login-reply": "投稿するのにログインして下さい", + "login-to-view": "🔒 Log in to view", + "edit": "編集", + "delete": "削除", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "切り離し", + "restore": "リストア", + "move": "移動", + "change-owner": "Change Owner", + "fork": "フォーク", + "link": "リンク", + "share": "シェア", + "tools": "ツール", + "locked": "ロック", + "pinned": "ピンされた", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "移動しました", + "moved-from": "Moved from %1", + "copy-ip": "IPをコピー", + "ban-ip": "IPをBan", + "view-history": "履歴を編集", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "ここをクリックすると、このスレッドの最後に読んでいた投稿へ移動します。", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "このスレッドが削除されました。スレッド管理権を持っているユーザーにしか読めません。", + "following_topic.message": "このスレッドが更新された際に通知を受け取ります。", + "not_following_topic.message": "あなたはスレッド一覧を未読にすると、このスレッドを参照できます。ただし誰かがこのスレッドに投稿したときは通知を受信できません。", + "ignoring_topic.message": "あなたは、これ以上この未読スレッドを一覧に表示しておくことが出来なくなります。追跡するか、あなたの投稿が高評価を受けると通知されます。", + "login_to_subscribe": "このスレッドを購読するためにログインが必要です。", + "markAsUnreadForAll.success": "すべてのスレッドを未読にしました。", + "mark_unread": "未読としてマーク", + "mark_unread.success": "スレッドは未読にマークされました。", + "watch": "ウォッチ", + "unwatch": "ウォッチ解除", + "watch.title": "新しい投稿の通知を受ける", + "unwatch.title": "このスレッドの通知を停止します", + "share_this_post": "投稿を共有", + "watching": "ウォッチ中", + "not-watching": "未ウォッチ", + "ignoring": "無視中", + "watching.description": "新しい返信のお知らせです。
未読のスレッドを表示", + "not-watching.description": "新しく返信通知を受け取らない。
カテゴリが無視されていない場合、未読のスレッドを表示します。", + "ignoring.description": "新しく返信通知を受け取らない。
未読のスレッドは表示されません。", + "thread_tools.title": "スレッドツール", + "thread_tools.markAsUnreadForAll": "未読にする", + "thread_tools.pin": "スレッドを最上部に固定", + "thread_tools.unpin": "スレッドの固定を解除", + "thread_tools.lock": "スレッドをロック", + "thread_tools.unlock": "スレッドをアンロック", + "thread_tools.move": "スレッドを移動", + "thread_tools.move-posts": "投稿を移動", + "thread_tools.move_all": "すべてを移動", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "カテゴリを選択", + "thread_tools.fork": "スレッドをフォーク", + "thread_tools.delete": "スレッドを削除", + "thread_tools.delete-posts": "投稿を削除します", + "thread_tools.delete_confirm": "本当にこの投稿を削除しますか?", + "thread_tools.restore": "スレッドをリストア", + "thread_tools.restore_confirm": "本当にこのスレッドを戻しますか?", + "thread_tools.purge": "スレッドを切り離します", + "thread_tools.purge_confirm": "本当にこのスレッドを切り離しますか?", + "thread_tools.merge_topics": "トピックを置き換える", + "thread_tools.merge": "置き換え", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "本当にこの投稿を削除しますか?", + "post_restore_confirm": "本当にこの投稿を元に戻しますか?", + "post_purge_confirm": "本当にこの投稿を切り離しますか?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "板をローディング中...", + "confirm_move": "移動", + "confirm_fork": "フォーク", + "bookmark": "ブックマーク", + "bookmarks": "ブックマーク", + "bookmarks.has_no_bookmarks": "まだ投稿をブックマークしていません。", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "もっと見る", + "move_topic": "スレッドを移動", + "move_topics": "スレッドを移動する", + "move_post": "投稿を移動", + "post_moved": "投稿を移動しました!", + "fork_topic": "スレッドをフォーク", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "フォークしたい投稿をクリックして", + "fork_no_pids": "投稿が選択されていません!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 投稿(s)が選択されました", + "fork_success": "スレッドをフォークするのに成功しました。ここを押して、このフォークしたスレッドに行きます。", + "delete_posts_instruction": "削除または切り離するには、当てはまる投稿を押してください", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "スレッドのタイトルを入力...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "破棄する", + "composer.submit": "保存する", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "%1へ返答中", + "composer.new_topic": "新規スレッド", + "composer.editing": "Editing", + "composer.uploading": "アップロード中...", + "composer.thumb_url_label": "スレッドのサムネイルのURLを入力して", + "composer.thumb_title": "スレッドにサムネイルを追加", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "またはファイルをアップロード", + "composer.thumb_remove": "入力をクリア", + "composer.drag_and_drop_images": "こちらへ画像をドラッグ&ドロップ", + "more_users_and_guests": "ユーザーが%1人でゲストさんが%2人", + "more_users": "ユーザーが%1人", + "more_guests": "ゲストさんが%1人", + "users_and_others": "%1と他は%2", + "sort_by": "並び替え", + "oldest_to_newest": "古いものから新しい順", + "newest_to_oldest": "新しいものから古い順", + "most_votes": "最高評価", + "most_posts": "最大投稿", + "most_views": "Most Views", + "stale.title": "新しいスレッドを作りますか?", + "stale.warning": "あなたが返信しようとしてるスレッドが古いスレッドです。新しいスレッドを作って、そしてこのスレッドが参考として入れた方を勧めます。そうしますか?", + "stale.create": "新しいスレッドを作ります。", + "stale.reply_anyway": "とにかく、このスレッドに返信します", + "link_back": "返信: [%1](%2)", + "diffs.title": "投稿の編集履歴", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "現在のリビジョン", + "diffs.original-revision": "元のリビジョン", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/ja/unread.json b/public/language/ja/unread.json new file mode 100644 index 0000000000..ca73cc72e3 --- /dev/null +++ b/public/language/ja/unread.json @@ -0,0 +1,15 @@ +{ + "title": "未読", + "no_unread_topics": "未読のスレッドはありません。", + "load_more": "もっと見る", + "mark_as_read": "既読にする", + "selected": "選択済み", + "all": "全て", + "all_categories": "全てのカテゴリ", + "topics_marked_as_read.success": "すべてのスレッドを既読にしました。", + "all-topics": "すべてのスレッド", + "new-topics": "新しいスレッド", + "watched-topics": "ウォッチ済みのスレッド", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/ja/uploads.json b/public/language/ja/uploads.json new file mode 100644 index 0000000000..6540439e65 --- /dev/null +++ b/public/language/ja/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "ファイルをアップロード中...", + "select-file-to-upload": "アップロードするファイルを選択してください!", + "upload-success": "ファイルのアップロードに成功しました!", + "maximum-file-size": "最大 %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/ja/user.json b/public/language/ja/user.json new file mode 100644 index 0000000000..06431706de --- /dev/null +++ b/public/language/ja/user.json @@ -0,0 +1,199 @@ +{ + "banned": "BANされた", + "muted": "Muted", + "offline": "オフライン", + "deleted": "削除されました", + "username": "ユーザー名", + "joindate": "参加日", + "postcount": "投稿数", + "email": "メール", + "confirm_email": "メールアドレスを確認", + "account_info": "アカウント情報", + "admin_actions_label": "Administrative Actions", + "ban_account": "BANアカウント", + "ban_account_confirm": "本当にこのユーザーをBANしますか?", + "unban_account": "禁止アカウント解除します", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "アカウント削除します", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "アカウントが解除されました", + "account-content-deleted": "Account content deleted", + "fullname": "フルネーム", + "website": "ウェブサイト", + "location": "ロケーション", + "age": "年齢", + "joined": "参加", + "lastonline": "最後オンライン", + "profile": "プロフィール", + "profile_views": "閲覧数", + "reputation": "評価", + "bookmarks": "ブックマーク", + "watched_categories": "ウォッチ中のカテゴリ", + "change_all": "Change All", + "watched": "ウォッチ済み", + "ignored": "無視済み", + "default-category-watch-state": "デフォルトのカテゴリウォッチ状態", + "followers": "フォロワー", + "following": "フォロー中", + "blocks": "ブロックの設定", + "block_toggle": "ブロックを切替", + "block_user": "ユーザーをブロック", + "unblock_user": "ブロックを解除", + "aboutme": "About me", + "signature": "署名", + "birthday": "誕生日", + "chat": "チャット", + "chat_with": "%1とチャットを続ける", + "new_chat_with": "%1とチャットを始める", + "flag-profile": "プロフィールを報告する", + "follow": "フォロー", + "unfollow": "フォロー解除", + "more": "つづき", + "profile_update_success": "プロフィールを更新しました!", + "change_picture": "画像を変更", + "change_username": "ユーザー名の変更", + "change_email": "メール変更", + "email_same_as_password": "現在のパスワードを入力して続行してください – 新しいメールアドレスをもう一度入力しました", + "edit": "編集", + "edit-profile": "プロフィールを編集", + "default_picture": "元のアイコン", + "uploaded_picture": "アップロード済みの画像", + "upload_new_picture": "新しい画像をアップロード", + "upload_new_picture_from_url": "URLにより新しい写真をアップします", + "current_password": "現在のパスワード", + "change_password": "パスワードを変更", + "change_password_error": "無効のパスワード!", + "change_password_error_wrong_current": "現在のパスワードは正しくありません!", + "change_password_error_match": "パスワードは一致しません!", + "change_password_error_privileges": "パスワードを更新する権限はありません。", + "change_password_success": "パスワードを更新しました!", + "confirm_password": "パスワードを再入力", + "password": "パスワード", + "username_taken_workaround": "このユーザー名はすでに使用されています。いまのユーザー名は %1 です。", + "password_same_as_username": "パスワードがユーザー名と同じですから、他のパスワードを使って下さい。", + "password_same_as_email": "パスワードがメールアドレスと同じです。他のパスワードを使って下さい。", + "weak_password": "弱いパスワード", + "upload_picture": "画像をアップロード", + "upload_a_picture": "画像をアップロード", + "remove_uploaded_picture": "アップした写真を取り消します", + "upload_cover_picture": "カバー写真をアップロード", + "remove_cover_picture_confirm": "カバー写真を削除してもよろしいですか?", + "crop_picture": "画像を切り抜く", + "upload_cropped_picture": "切り抜いてアップロード", + "avatar-background-colour": "Avatar background colour", + "settings": "設定", + "show_email": "メールアドレスを表示", + "show_fullname": "フルネームで表示", + "restrict_chats": "フォローしたユーザーからのチャットメッセージだけを許可する", + "digest_label": "お知らせを購読する", + "digest_description": "この掲示板のアップデートを受信する", + "digest_off": "オフ", + "digest_daily": "デイリー", + "digest_weekly": "ウィークリー", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "マンスリー", + "has_no_follower": "フォロワーはまだいません :(", + "follows_no_one": "フォロー中のユーザーはまだいません :(", + "has_no_posts": "このユーザーはまだ一つも投稿していません", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "このユーザーはまだ一つもスレッドを作っていません", + "has_no_watched_topics": "このユーザーはまだ一つもスレッドをウォッチしていません", + "has_no_ignored_topics": "この利用者はまだトピックを無視していません。", + "has_no_upvoted_posts": "このユーザーはまだ一つも投稿に高評価を付けていません。", + "has_no_downvoted_posts": "このユーザーはまだ一つも投稿に低評価を付けていません。", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "ブロック中のユーザーはいません。", + "email_hidden": "メールアドレスを非表示", + "hidden": "非表示", + "paginate_description": "無限スクロールの代わりに、投稿やスレッドをページ別で切り替える。", + "topics_per_page": "ページごとのスレッド数", + "posts_per_page": "ページごとの投稿数", + "max_items_per_page": "最大 %1", + "acp_language": "ページ言語の管理", + "notifications": "Notifications", + "upvote-notif-freq": "投票の通知頻度", + "upvote-notif-freq.all": "すべての高評価", + "upvote-notif-freq.first": "はじめの投稿", + "upvote-notif-freq.everyTen": "10の投票数", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "無効", + "browsing": "ブラウジングの設定", + "open_links_in_new_tab": "外部リンクを新しいタブで開く", + "enable_topic_searching": "スレッド内検索を有効にする", + "topic_search_help": "有効にしたら、インースレッドの検索はブラウザの既定機能を無視して、スクリーンに示したよりスレッド内からの全部を検索します", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "返信を投稿した後、新しい投稿を表示する", + "follow_topics_you_reply_to": "あなたが返信したスレッドをウォッチする", + "follow_topics_you_create": "あなたが作成したスレッドをウォッチする", + "grouptitle": "グループ題名", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "グループ名がありません", + "select-skin": "スキンを選んで下さい", + "select-homepage": "ホームページの設定", + "homepage": "ホームページ", + "homepage_description": "フォーラムのホームに指定するページを選んで下さい。デフォルトのホームページを使用する場合は’None’を選んで下さい。", + "custom_route": "カスタムホームページルート", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "シングルサインオンサービス", + "sso.associated": "関連付けられています", + "sso.not-associated": "ここを押して、関連付けられています", + "sso.dissociate": "離脱する", + "sso.dissociate-confirm-title": "離脱の際に確認する", + "sso.dissociate-confirm": "アカウントと %1 の関連付けを解除しますか?", + "info.latest-flags": "最近のフラグ", + "info.no-flags": "フラグのついた投稿はありません", + "info.ban-history": "最近停止した履歴", + "info.no-ban-history": "このユーザーは停止されていません", + "info.banned-until": "%1まで停止", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "永久に停止", + "info.banned-reason-label": "理由", + "info.banned-no-reason": "理由なし。", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "ユーザー名の履歴", + "info.email-history": "Eメール履歴", + "info.moderation-note": "モデレーションノート", + "info.moderation-note.success": "モデレーションは保存されませんでした", + "info.moderation-note.add": "ノートに追加", + "sessions.description": "このページでは、このフォーラムでアクティブなセッションを表示し、必要に応じてそれらを取り消すことができます。あなたのアカウントからログアウトすることによって自分のセッションを取り消すことができます。", + "consent.title": "あなたの権利& 同意", + "consent.lead": "このコミュニティフォーラムはあなたの個人情報を収集し処理します。", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "ユーザー設定で明示的に変更されていない限り、このコミュニティは %1 ごとに電子メールダイジェストを配信します。", + "consent.digest_off": "ユーザー設定で明示的に変更されていない限り、このコミュニティは電子メールダイジェストを送信しません。", + "consent.received": "あなたはあなたの情報を収集し処理するためにこのウェブサイトに同意を提供しました。 追加の操作は必要ありません。", + "consent.not_received": "データの収集と処理に関する同意を提供していません。 このウェブサイトの管理者は、いつでも一般データ保護規則に準拠するためにあなたのアカウントを削除することを選択することができます。", + "consent.give": "同意を与える", + "consent.right_of_access": "あなたにはアクセス権があります", + "consent.right_of_access_description": "あなたは要求に応じてこのウェブサイトによって収集されたデータにアクセスする権利があります。 下の適切なボタンをクリックしてこのデータのコピーを取得することができます。", + "consent.right_to_rectification": "あなたには矯正の権利があります", + "consent.right_to_rectification_description": "あなたは私たちに提供された不正確なデータを変更または更新する権利を有します。 あなたのプロフィールは編集して更新することができ、投稿内容はいつでも編集することができます。 そうでない場合は、このサイトの管理チームにお問い合わせください。", + "consent.right_to_erasure": "あなたには消去する権利があります", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "あなたはデータを移動する権利があります", + "consent.right_to_data_portability_description": "あなたは私たちにあなたとあなたのアカウントに関して収集されたデータの機械読み取り可能なエクスポートを要求することができます。 下の適切なボタンをクリックしてそれを行うことができます。", + "consent.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "アップデートしたコンテンツをエクスポート(.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "投稿をエクスポート (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/ja/users.json b/public/language/ja/users.json new file mode 100644 index 0000000000..364128e16b --- /dev/null +++ b/public/language/ja/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "新しいユーザー", + "top_posters": "最も投稿したユーザー", + "most_reputation": "最も評価されたユーザー", + "most_flags": "最も多いフラグ", + "search": "検索", + "enter_username": "ユーザー名を入力", + "search-user-for-chat": "Search a user to start chat", + "load_more": "もっと見る", + "users-found-search-took": "%1人のユーザーを見つけました!(検索まで%2秒掛かりました。)", + "filter-by": "フィルタ", + "online-only": "オンラインのみ", + "invite": "招待", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "招待メールが%1に送られました。", + "user_list": "ユーザー一覧", + "recent_topics": "最新スレッド", + "popular_topics": "人気のスレッド", + "unread_topics": "未読スレッド", + "categories": "カテゴリ", + "tags": "タグ", + "no-users-found": "ユーザーが見つかりません!" +} \ No newline at end of file diff --git a/public/language/ko/_DO_NOT_EDIT_FILES_HERE.md b/public/language/ko/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/ko/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/ko/admin/admin.json b/public/language/ko/admin/admin.json new file mode 100644 index 0000000000..2e48cacd43 --- /dev/null +++ b/public/language/ko/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "NodeBB를 리빌드 후 재시작하시겠습니까?", + "alert.confirm-restart": "NodeBB를 다시 시작하시겠습니까?", + + "acp-title": "%1 | NodeBB 관리자 제어판", + "settings-header-contents": "컨텐츠", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/ko/admin/advanced/cache.json b/public/language/ko/admin/advanced/cache.json new file mode 100644 index 0000000000..8b8b37cf3c --- /dev/null +++ b/public/language/ko/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "포스트 캐시", + "group-cache": "그룹 캐시", + "local-cache": "자체 캐시", + "object-cache": "오프젝트 캐시", + "percent-full": "%1%꽉참", + "post-cache-size": "포스트 캐시 크기", + "items-in-cache": "캐시된 항목들" +} \ No newline at end of file diff --git a/public/language/ko/admin/advanced/database.json b/public/language/ko/admin/advanced/database.json new file mode 100644 index 0000000000..c950133824 --- /dev/null +++ b/public/language/ko/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 B", + "x-mb": "%1 MB", + "x-gb": "%1 GB", + "uptime-seconds": "초 단위의 가동 시간", + "uptime-days": "일간 가동시간", + + "mongo": "Mongo", + "mongo.version": "MongoDB 버젼", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "객체", + "mongo.avg-object-size": "평균 객체 크기", + "mongo.data-size": "데이터 크기", + "mongo.storage-size": "저장공간 크기", + "mongo.index-size": "인덱스 크기", + "mongo.file-size": "파일 크기", + "mongo.resident-memory": "실제 사용 중인 메모리", + "mongo.virtual-memory": "가상 메모리", + "mongo.mapped-memory": "매핑 메모리", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "요청 횟수", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB가 MongoDB의 통계 데이터를 불러올 수 없습니다. NodeBB에서 사용중인 사용자에 "admin" DB에 대한 "clusterMonitor" 역할이 포함되어있는지 확인하세요.", + + "redis": "Redis", + "redis.version": "Redis 버전", + "redis.keys": "키", + "redis.expires": "만료일", + "redis.avg-ttl": "평균 TTL", + "redis.connected-clients": "연결된 클라이언트", + "redis.connected-slaves": "연결된 slaves", + "redis.blocked-clients": "차단된 클라이언트", + "redis.used-memory": "사용된 메모리", + "redis.memory-frag-ratio": "메모리 조각화 비율", + "redis.total-connections-recieved": "받은 총 커넥션 수", + "redis.total-commands-processed": "처리된 총 커맨드 수", + "redis.iops": "초당 순간 Ops", + "redis.iinput": "초당 순간 입력", + "redis.ioutput": "초당 순간 출력", + "redis.total-input": "총 입력", + "redis.total-output": "총 출력", + + "redis.keyspace-hits": "Keyspace 히트", + "redis.keyspace-misses": "Keyspace 미스", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL 버전", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/ko/admin/advanced/errors.json b/public/language/ko/admin/advanced/errors.json new file mode 100644 index 0000000000..12512e51d4 --- /dev/null +++ b/public/language/ko/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "그래프 %1", + "error-events-per-day": "일일 %1 이벤트 발생 횟수", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "오류 로그 관리", + "export-error-log": "오류 로그 저장 (CSV)", + "clear-error-log": "오류 로그 초기화", + "route": "경로", + "count": "횟수", + "no-routes-not-found": "만세! 404 오류 없음!", + "clear404-confirm": "404 오류 로그를 초기화하시겠습니까?", + "clear404-success": "\"404 Not Found\" 오류 로그 초기화 완료" +} \ No newline at end of file diff --git a/public/language/ko/admin/advanced/events.json b/public/language/ko/admin/advanced/events.json new file mode 100644 index 0000000000..3f96674ed5 --- /dev/null +++ b/public/language/ko/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "이벤트", + "no-events": "이벤트가 없습니다", + "control-panel": "이벤트 제어판", + "delete-events": "이벤트 삭제", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "필터", + "filters-apply": "필터 적용", + "filter-type": "이벤트 유형", + "filter-start": "시작일", + "filter-end": "종료일", + "filter-perPage": "페이지 당" +} \ No newline at end of file diff --git a/public/language/ko/admin/advanced/logs.json b/public/language/ko/admin/advanced/logs.json new file mode 100644 index 0000000000..283974b98d --- /dev/null +++ b/public/language/ko/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "로그", + "control-panel": "로그 제어판", + "reload": "로그 리로드", + "clear": "로그 초기화", + "clear-success": "로그 초기화 완료!" +} \ No newline at end of file diff --git a/public/language/ko/admin/appearance/customise.json b/public/language/ko/admin/appearance/customise.json new file mode 100644 index 0000000000..95ed139400 --- /dev/null +++ b/public/language/ko/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "사용자 정의 CSS/LESS ", + "custom-css.description": "사용자 정의 CSS/LESS 코드를 입력하세요. 모든 스타일 다음에 적용됩니다.", + "custom-css.enable": "사용자 정의 CSS/LESS 활성화", + + "custom-js": "사용자 정의 Javascript", + "custom-js.description": "사용자 정의 Javascript를 넣으세요. 페이지 로딩이 완료된 후 실행됩니다.", + "custom-js.enable": "사용자 정의 Javascript 활성화", + + "custom-header": "사용자 정의 헤더", + "custom-header.description": "사용자 정의 HTML(메타 태그 등)를 입력하면 포럼의 <head> 부분에 추가됩니다. 스크립트 태그의 사용도 가능하지만 사용자 정의 Javascript 기능이 있기 때문에 추천하지 않습니다.", + "custom-header.enable": "사용자 정의 헤더 활성화", + + "custom-css.livereload": "실시간 새로고침 허용", + "custom-css.livereload.description": "세이브를 누를 때마다 당신의 계정에 속한 디바이스의 모든 세션들이 새로고침 되게 하려면 이것을 활성화하세요." +} \ No newline at end of file diff --git a/public/language/ko/admin/appearance/skins.json b/public/language/ko/admin/appearance/skins.json new file mode 100644 index 0000000000..f365ad8c9e --- /dev/null +++ b/public/language/ko/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "스킨 불러오는 중...", + "homepage": "홈페이지", + "select-skin": "스킨 선택", + "current-skin": "현재 스킨", + "skin-updated": "스킨 업데이트 됨", + "applied-success": "%1 스킨 적용 완료", + "revert-success": "기본 색상으로 스킨 복구됨" +} \ No newline at end of file diff --git a/public/language/ko/admin/appearance/themes.json b/public/language/ko/admin/appearance/themes.json new file mode 100644 index 0000000000..a596dc76ef --- /dev/null +++ b/public/language/ko/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "설치된 테마 확인 중...", + "homepage": "홈페이지", + "select-theme": "테마 선택", + "current-theme": "현재 테마", + "no-themes": "설치된 테마 없음", + "revert-confirm": "정말 NodeBB 기본 테마로 복원하시겠습니까?", + "theme-changed": "테마 변경 완료", + "revert-success": "성공적으로 NodeBB 기본 테마로 복원됐습니다.", + "restart-to-activate": "변경된 테마를 완전히 활성화하기 위해 NodeBB를 리빌드하고 재시작 해주세요." +} \ No newline at end of file diff --git a/public/language/ko/admin/dashboard.json b/public/language/ko/admin/dashboard.json new file mode 100644 index 0000000000..aee85929ae --- /dev/null +++ b/public/language/ko/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "포럼 트래픽", + "page-views": "페이지 뷰", + "unique-visitors": "고유 방문자", + "logins": "로그인 기록", + "new-users": "신규 사용자", + "posts": "포스트", + "topics": "화제", + "page-views-seven": "지난 7일간", + "page-views-thirty": "지난 30일간", + "page-views-last-day": "지난 24시간 동안", + "page-views-custom": "사용자 정의 기간", + "page-views-custom-start": "기간 시작", + "page-views-custom-end": "기간 끝", + "page-views-custom-help": "페이지 뷰를 확인하고 싶은 기간을 입력하세요. 만약 데이트 피커를 사용할 수 없다면, YYYY-MM-DD 포맷으로 입력해주세요.", + "page-views-custom-error": "유효한 기간을 다음과 같은 포맷으로 입력하세요 YYYY-MM-DD", + + "stats.yesterday": "어제", + "stats.today": "오늘", + "stats.last-week": "지난 주", + "stats.this-week": "이번 주", + "stats.last-month": "지난 달", + "stats.this-month": "이번 달", + "stats.all": "항상", + + "updates": "업데이트", + "running-version": "NodeBB v%1를 사용 중입니다.", + "keep-updated": "사용 중인 NodeBB의 보안 및 오류 해결을 위해 항상 최신 버전으로 유지하세요.", + "up-to-date": "

최신 버전입니다

", + "upgrade-available": "

새로운 버전(v%1)이 출시되었습니다. 사용중인 NodeBB의 업데이트를 고려해보세요.

", + "prerelease-upgrade-available": "

사용하는 NodeBB 버전이 오래되었습니다. 새로운 버전(v%1)이 출시되었습니다. 사용중인 NodeBB의 업데이트를 고려해보세요.

", + "prerelease-warning": "

이것은 정식 발표 전 버전의 NodeBB입니다. 예상치 못한 오류가 발생할 수 있습니다.

", + "fallback-emailer-not-found": "대체 이메일이 없습니다!", + "running-in-development": "포럼이 개발자 모드로 실행되고 있습니다. 잠재적 취약점에 노출되어 있을 수 있으니 시스템 관리자에게 문의하세요.", + "latest-lookup-failed": "

NodeBB의 최신 버전을 확인하는데 실패했습니다.

", + + "notices": "알림", + "restart-not-required": "재시작 필요 없음", + "restart-required": "재시작 필요", + "search-plugin-installed": "설치된 플러그인 검색", + "search-plugin-not-installed": "설치되지 않은 플러그인 검색", + "search-plugin-tooltip": "검색 기능을 활성화하시려면 플러그인 페이지에서 검색 플러그인을 설치하세요.", + + "control-panel": "시스템 제어", + "rebuild-and-restart": "리빌드 & 재시작", + "restart": "재시작", + "restart-warning": "NodeBB가 리빌드 또는 재시작을 하고 있습니다. 수 초 내에 연결된 모든 접속을 종료합니다.", + "restart-disabled": "정상적인 데몬으로 판단할 수 없어 리빌드와 재시작을 할 수 없습니다.", + "maintenance-mode": "점검 모드", + "maintenance-mode-title": "NodeBB 점검 모드를 설정하시려면 이곳을 클릭하세요.", + "realtime-chart-updates": "실시간 차트 업데이트", + + "active-users": "활동 중인 사용자", + "active-users.users": "사용자", + "active-users.guests": "비회원", + "active-users.total": "총", + "active-users.connections": "연결", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "가입한 사용자", + + "user-presence": "사용자 활동", + "on-categories": "카테고리 보는 중", + "reading-posts": "포스트 읽는 중", + "browsing-topics": "화제 읽는 중", + "recent": "최근", + "unread": "읽지 않음", + + "high-presence-topics": "활동량이 많은 화제", + "popular-searches": "Popular Searches", + + "graphs.page-views": "페이지 뷰", + "graphs.page-views-registered": "가입한 사용자의 페이지 뷰", + "graphs.page-views-guest": "비회원 페이지 뷰", + "graphs.page-views-bot": "봇의 페이지 뷰", + "graphs.unique-visitors": "고유 방문자", + "graphs.registered-users": "등록된 사용자", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "최근 재시작 시점", + "no-users-browsing": "보고있는 사용자 없음", + + "back-to-dashboard": "대시보드로 돌아가기", + "details.no-users": "설정한 기간에 가입한 사용자 없음", + "details.no-topics": "설정한 기간에 생성된 화제 없음", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "설정한 기간에 로그인 기록 없음", + "details.logins-static": "NodeBB는 세션 정보를 %1일 동안만 저장합니다. 따라서 아래의 표는 최근 활성화된 세션 정보만을 표시합니다.", + "details.logins-login-time": "로그인 시점" +} diff --git a/public/language/ko/admin/development/info.json b/public/language/ko/admin/development/info.json new file mode 100644 index 0000000000..640110f2d0 --- /dev/null +++ b/public/language/ko/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "현재 %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 노드가 %2ms 내로 응답했습니다.", + "host": "호스트", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "온라인", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "시스템 로드", + "cpu-usage": "cpu 사용량", + "uptime": "업타임", + + "registered": "등록됨", + "sockets": "소켓", + "guests": "비회원", + + "info": "정보" +} \ No newline at end of file diff --git a/public/language/ko/admin/development/logger.json b/public/language/ko/admin/development/logger.json new file mode 100644 index 0000000000..775ddb7564 --- /dev/null +++ b/public/language/ko/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "로그 설정", + "description": "체크 박스를 활성화하면, 터미널에서 로그를 볼 수 있게 됩니다. 만약 파일 경로를 지정하면, 로그가 지정한 파일에 대신 저장됩니다. HTTP 기록은 누가, 언제, 무엇을 포럼에서 했는지에 대한 통계를 내는 데 유용합니다. HTTP 리퀘스트들을 기록할 뿐 아니라, socket.io 이벤트들도 기록할 수 있습니다. Socket.io 기록은 redis-cli 모니터와 함께 사용하면 NodeBB의 내부 사항을 모니터하는 데 아주 유용할 수 있습니다.", + "explanation": "원하실 때 로그 설정을 활성화/비활성화 하십시오. 재시작할 필요는 없습니다.", + "enable-http": "HTTP 로깅 허용", + "enable-socket": "socket.io 이벤트 로깅 허용", + "file-path": "로그 파일 경로", + "file-path-placeholder": "/path/to/log/file.log ::: 터미널에서 로그를 보시려면 빈칸으로 두세요", + + "control-panel": "로그 설정 업데이트", + "update-settings": "업데이트" +} \ No newline at end of file diff --git a/public/language/ko/admin/extend/plugins.json b/public/language/ko/admin/extend/plugins.json new file mode 100644 index 0000000000..aea2277f7e --- /dev/null +++ b/public/language/ko/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "인기 플러그인", + "installed": "설치됨", + "active": "활성화", + "inactive": "비활성화", + "out-of-date": "업데이트 필요", + "none-found": "플러그인을 찾을 수 없습니다.", + "none-active": "사용 중인 플러그인이 없습니다.", + "find-plugins": "플러그인 검색", + + "plugin-search": "플러그인 검색", + "plugin-search-placeholder": "검색할 플러그인 입력", + "submit-anonymous-usage": "익명의 사용 데이터를 등록합니다.", + "reorder-plugins": "플러그인 작동 순서 재배열", + "order-active": "활성화 플러그인 재배열", + "dev-interested": "NodeBB 플러그인을 만드는 데 관심이 있으십니까?", + "docs-info": "플러그인 제작 관련 문서는 NodeBB Docs Portal에서 찾아보실 수 있습니다.", + + "order.description": "특정 플러그인은 다른 플러그인의 초기화 전/후에 가장 이상적으로 작동합니다.", + "order.explanation": "플러그인들은 여기에 나열된 순서로 로드됩니다.", + + "plugin-item.themes": "테마", + "plugin-item.deactivate": "비활성화", + "plugin-item.activate": "활성화", + "plugin-item.install": "설치", + "plugin-item.uninstall": "제거", + "plugin-item.settings": "설정", + "plugin-item.installed": "설치됨", + "plugin-item.latest": "최신", + "plugin-item.upgrade": "업그레이드", + "plugin-item.more-info": "추가적인 정보를 원하시면:", + "plugin-item.unknown": "알 수 없음", + "plugin-item.unknown-explanation": "이 플러그인의 상태를 알 수 없습니다. 환경 설정에서 발생한 오류 때문일 수 있습니다.", + "plugin-item.compatible": "이 플러그인은 NodeBB %1에서 작동합니다.", + "plugin-item.not-compatible": "이 플러그인의 호환성 데이터가 없습니다. 설치 전 작동 여부를 확인해보세요.", + + "alert.enabled": "플러그인 활성화", + "alert.disabled": "플러그인 비활성화", + "alert.upgraded": "플러그인 업그레이드 완료", + "alert.installed": "플러그인 설치 완료", + "alert.uninstalled": "플러그인 제거 완료", + "alert.activate-success": "해당 플러그인을 완벽하게 활성화하기 위해 NodeBB를 리빌드하고 다시 시작해주세요.", + "alert.deactivate-success": "플러그인이 성공적으로 비활성화됐습니다.", + "alert.upgrade-success": "이 플러그인을 업그레이드 하려면 NodeBB를 리빌드하고 다시 시작해주세요.", + "alert.install-success": "플러그인이 성공적으로 설치됐습니다. 플러그인을 활성화 해주세요.", + "alert.uninstall-success": "플러그인이 성공적으로 비활성화되고 삭제됐습니다.", + "alert.suggest-error": "

NodeBB가 패키지 매니저 접근에 실패하였습니다. 최신 버전을 설치하시겠습니까?

서버의 응답 (%1):%2
", + "alert.package-manager-unreachable": "

NodeBB가 패키지 매니저 접근에 실패했습니다. 지금 업그레이드하는 것을 추천하지 않습니다.

", + "alert.incompatible": "

지금 사용하는 NodeBB 버전(v%1)에서는 이 플러그인을 v%2 버전까지만 업그레이드할 수 있습니다. 이 플러그인의 최신 버전을 설치하고 싶다면 먼저 NodeBB를 업그레이드 해주세요.

", + "alert.possibly-incompatible": "

호환성 관련 정보를 찾지 못했습니다.

이 플러그인은 현재 사용 중인 NodeBB 버전에 적합한 버전을 명시하지 않았습니다. 따라서 완전한 호환성을 보장할 수 없고, 결과적으로 지금 사용중인 NodeBB에 오류를 일으킬 수도 있습니다.

만약 NodeBB가 제대로 시작되지 않는다면:

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

이 플러그인의 최신 버전을 설치를 계속 하시겠습니까?

", + "alert.reorder": "플러그인 재정렬", + "alert.reorder-success": "프로세스를 완료하려면 NodeBB를 리빌드하고 다시 시작해주세요.", + + "license.title": "플러그인 라이센스 정보", + "license.intro": "%1 플러그인은 %2 라이센스입니다. 이 플러그인을 활성화하기 전에 라이센스를 확인하세요", + "license.cta": "이 플러그인을 활성화 하시겠습니까?" +} diff --git a/public/language/ko/admin/extend/rewards.json b/public/language/ko/admin/extend/rewards.json new file mode 100644 index 0000000000..be8bd8cf85 --- /dev/null +++ b/public/language/ko/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "보상", + "condition-if-users": "만약 사용자의", + "condition-is": "다음의 조건을 충족한다면:", + "condition-then": "다음과 같은 행동을 취합니다:", + "max-claims": "보상을 받을 수 있는 횟수", + "zero-infinite": "무제한으로 설정하려면 0으로 설정", + "delete": "삭제", + "enable": "활성화", + "disable": "비활성화", + + "alert.delete-success": "성공적으로 보상을 삭제했습니다.", + "alert.no-inputs-found": "잘못된 보상 - 입력값이 없습니다!", + "alert.save-success": "성공적으로 보상을 저장했습니다." +} \ No newline at end of file diff --git a/public/language/ko/admin/extend/widgets.json b/public/language/ko/admin/extend/widgets.json new file mode 100644 index 0000000000..13bc81e659 --- /dev/null +++ b/public/language/ko/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "사용 가능한 위젯", + "explanation": "드롭다운 메뉴에서 위젯을 선택하고 왼쪽에 있는 템플릿의 위젯 위치로 드래그하여 옮기세요.", + "none-installed": "위젯이 없습니다! 플러그인 설정 메뉴에서 widget essentials 플러그인을 설치하세요.", + "clone-from": "복제할 위젯 선택", + "containers.available": "사용 가능한 컨테이너", + "containers.explanation": "활성화된 위젯 위로 드래그&드롭하세요.", + "containers.none": "없음", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "제어판", + "container.panel-header": "제어판 헤더", + "container.panel-body": "제어판 몸통", + "container.alert": "경고", + + "alert.confirm-delete": "정말 이 위젯을 삭제하시겠습니까?", + "alert.updated": "위젯 업데이트 완료", + "alert.update-success": "위젯 업데이트 완료", + "alert.clone-success": "위젯 복제 완료", + + "error.select-clone": "복제할 페이지 선택", + + "title": "제목", + "title.placeholder": "제목 (일부 컨테이너에서만 표시)", + "container": "컨테이너", + "container.placeholder": "컨테이너를 드래그&드롭하거나 HTML을 입력하세요.", + "show-to-groups": "해당 그룹에 표시", + "hide-from-groups": "해당 그룹에 숨김", + "hide-on-mobile": "모바일에서 숨김" +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/admins-mods.json b/public/language/ko/admin/manage/admins-mods.json new file mode 100644 index 0000000000..ffd10f957b --- /dev/null +++ b/public/language/ko/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "관리자", + "global-moderators": "통합 조정자", + "moderators": "Moderators", + "no-global-moderators": "통합 조정자 없음", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "조정자 없음", + "add-administrator": "관리자 추가", + "add-global-moderator": "통합 조정자 추가", + "add-moderator": "조정자 추가" +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/categories.json b/public/language/ko/admin/manage/categories.json new file mode 100644 index 0000000000..692d1ee508 --- /dev/null +++ b/public/language/ko/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "카테고리 설정", + "privileges": "권한", + + "name": "카테고리 이름", + "description": "카테고리 설명", + "bg-color": "배경 색상", + "text-color": "텍스트 색상", + "bg-image-size": "배경 이미지 크기", + "custom-class": "사용자 정의 클래스", + "num-recent-replies": "최근 답글 갯수", + "ext-link": "외부 링크", + "subcategories-per-page": "페이지 당 하위 카테고리", + "is-section": "이 카테고리를 섹션으로 취급", + "post-queue": "게시 대기열", + "tag-whitelist": "태그 화이트리스트", + "upload-image": "이미지 업로드", + "delete-image": "제거", + "category-image": "카테고리 이미지", + "parent-category": "상위 카테고리", + "optional-parent-category": "(선택) 상위 카테고리", + "top-level": "최고 레벨", + "parent-category-none": "(없음)", + "copy-parent": "상위 카테고리 복사", + "copy-settings": "설정을 복사할 대상 지정", + "optional-clone-settings": "(선택) 다른 카테고리 설정 복사", + "clone-children": "하위 카테고리 및 설정 복사", + "purge": "카테고리 삭제", + + "enable": "활성화", + "disable": "비활성화", + "edit": "편집", + "analytics": "애널리틱스", + "view-category": "카테고리 보기", + "set-order": "순서 설정", + "set-order-help": "카테고리의 순서를 설정하면 해당 위치로 순서가 변경되며 다른 카테고리의 순서도 함께 변경됩니다. 최소 설정값은 1이며 최상단에 위치됩니다.", + + "select-category": "카테고리 선택", + "set-parent-category": "상위 카테고리 설정", + + "privileges.description": "이 화면에서 사이트 일부에 대한 접근 제어 권한을 설정할 수 있습니다. 권한은 사용자별 또는 그룹별로 부여될 수 있습니다. 아래 드롭다운 메뉴에서 적용할 카테고리를 선택합니다.", + "privileges.category-selector": "다음 카테고리에 대한 권한 설정", + "privileges.warning": "참고: 권한 설정은 즉시 적용됩니다. 설정을 변경한 후 게시판을 따로 저장할 필요가 없습니다.", + "privileges.section-viewing": "열람 권한", + "privileges.section-posting": "게시 권한", + "privileges.section-moderation": "관리 권한", + "privileges.section-other": "기타", + "privileges.section-user": "사용자", + "privileges.search-user": "사용자 추가", + "privileges.no-users": "이 게시판에는 사용자별 권한이 없습니다.", + "privileges.section-group": "그룹", + "privileges.group-private": "이 그룹은 비공개 그룹입니다", + "privileges.inheritance-exception": "이 그룹은 registered-users 그룹의 권한에 종속되지 않습니다.", + "privileges.banned-user-inheritance": "차단된 사용자는 banned-users 그룹의 권한이 적용됩니다.", + "privileges.search-group": "그룹 추가", + "privileges.copy-to-children": "하위 카테고리로 복사", + "privileges.copy-from-category": "카테고리에서 복사", + "privileges.copy-privileges-to-all-categories": "모든 카테고리로 복사", + "privileges.copy-group-privileges-to-children": "이 그룹의 권한을 모든 하위 카테고리에 적용", + "privileges.copy-group-privileges-to-all-categories": "이 그룹의 권한을 모든 카테고리에 적용", + "privileges.copy-group-privileges-from": "다른 카테고리에서의 권한을 이 그룹에 적용", + "privileges.inherit": "만약 registered-users그룹이 특정 권한을 허가 받는다면 모든 다른 그룹들 또한 따로 추가하거나 체크하지 않더라도 암시적 권한을 얻게 됩니다. 모든 유저가 registered-users 그룹의 멤버이기 때문에 다른 추가적인 그룹에 대한 권한은 따로 허가 받을 필요가 없습니다.", + "privileges.copy-success": "권한 복사 완료!", + + "analytics.back": "카테고리 목록으로 돌아가기", + "analytics.title": "\"%1\" 카테고리 분석 결과", + "analytics.pageviews-hourly": "그래프 1 – 이 카테고리 시간당 페이지 뷰
", + "analytics.pageviews-daily": "그래프 2 – 이 카테고리 일일 페이지 뷰", + "analytics.topics-daily": "그래프 3 – 오늘 이 카테고리에 생성된 화제", + "analytics.posts-daily": "그래프 4 – 오늘 이 카테고리 생성된 포스트", + + "alert.created": "생성 완료", + "alert.create-success": "카테고리가 성공적으로 생성되었습니다!", + "alert.none-active": "활성화된 카테고리가 없습니다.", + "alert.create": "카테고리 생성", + "alert.confirm-purge": "

정말로 \"%1\" 카테고리를 제거하시겠습니까?

경고!이 카테고리에 속한 모든 화제와 포스트가 삭제됩니다!

카테고리를 제거하면 모든 화제와 포스트가 삭제되고 데이터베이스에서도 이 카테고리가 삭제됩니다. 만약 일시적으로 카테고리를 없애고 싶다면 삭제 대신 \"비활성화\"를 해주세요.

", + "alert.purge-success": "카테고리 제거 완료!", + "alert.copy-success": "설정 복사 완료!", + "alert.set-parent-category": "상위 카테고리 설정", + "alert.updated": "업데이트 된 카테고리", + "alert.updated-success": "게시판 ID %1 성공적으로 업데이트 완료", + "alert.upload-image": "카테고리 이미지 업로드", + "alert.find-user": "사용자 검색", + "alert.user-search": "여기서 사용자를 검색하세요...", + "alert.find-group": "그룹 검색", + "alert.group-search": "여기서 그룹을 검색하세요...", + "alert.not-enough-whitelisted-tags": "화이트리스트 태그의 수가 작성을 위한 최소 태그 수보다 저 적습니다. 화이트리스트 태그를 더 생성해주세요!", + "collapse-all": "모두 축소", + "expand-all": "모두 확장", + "disable-on-create": "생성 시 비활성화", + "no-matches": "일치하는 결과 없음" +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/digest.json b/public/language/ko/admin/manage/digest.json new file mode 100644 index 0000000000..db9f7dc963 --- /dev/null +++ b/public/language/ko/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "포럼 메일 통계 및 시간 목록이 아래에 표시됩니다.", + "disclaimer": "이메일 기술의 특성상 이메일 전송이 보장되지 않습니다. 서버 평판, 블랙리스트로 설정된 IP 주소, DKIM/SPF/DMARC 구성 여부 등 사용자의 이메일 서버에서 받은 편지함으로의 전송 여부를 결정하는 변수가 많습니다.", + "disclaimer-continued": "전송이 성공됐다는 것은 메일이 NodeBB에 의해 성공적으로 전송되고 수신자의 이메일 서버가 이를 수신했음을 의미합니다. 이는 이메일이 수신자의 받은 편지함에 도착했다는 것을 의미하지 않습니다. 최상의 결과를 얻으려면 SendGrid와 같은 서드파티 이메일 서비스를 이용하는 것이 좋습니다.", + + "user": "사용자", + "subscription": "구독 유형", + "last-delivery": "최근 전송 성공 시각", + "default": "시스템 기본 설정", + "default-help": "시스템 기본 설정은 사용자가 현재 "%1"로 설정되어있는 포럼 메일의 기본 구독 설정을 변경하지 않았다는 뜻입니다.", + "resend": "포럼 메일 재전송", + "resend-all-confirm": "정말 포럼 메일 전송을 수동으로 진행합니까?", + "resent-single": "포럼 메일 수동 전송 완료", + "resent-day": "일간 포럼 메일 재전송 완료", + "resent-week": "주간 포럼 메일 재전송 완료", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "월간 포럼 메일 재전송 완료", + "null": "기록 없음", + "manual-run": "포럼 메일 수동 전송", + + "no-delivery-data": "전송 데이터 없음" +} diff --git a/public/language/ko/admin/manage/groups.json b/public/language/ko/admin/manage/groups.json new file mode 100644 index 0000000000..56e8a75f51 --- /dev/null +++ b/public/language/ko/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "그룹 이름", + "badge": "뱃지", + "properties": "속성", + "description": "그룹 설명", + "member-count": "멤버 수", + "system": "시스템", + "hidden": "숨김", + "private": "비공개", + "edit": "수정", + "delete": "제거", + "privileges": "권한", + "download-csv": "CSV", + "search-placeholder": "검색", + "create": "그룹 생성", + "description-placeholder": "그룹에 대한 짧은 설명", + "create-button": "생성", + + "alerts.create-failure": "이런!

그룹을 생성하는데 문제가 발생했습니다. 잠시 후 다시 시도해주세요!

", + "alerts.confirm-delete": "이 그룹을 삭제하시겠습니까?", + + "edit.name": "이름", + "edit.description": "설명", + "edit.user-title": "멤버 타이틀", + "edit.icon": "그룹 아이콘", + "edit.label-color": "그룹 라벨 색상", + "edit.text-color": "그룹 텍스트 색상", + "edit.show-badge": "뱃지 보여주기", + "edit.private-details": "활성화되면 그룹에 가입하기 위해 그룹 관리자의 승인이 필요합니다.", + "edit.private-override": "경고: 비공개 그룹은 시스템에 의해 비활성화되었으며, 시스템 설정은 이 옵션보다 우위를 가집니다.", + "edit.disable-join": "가입 요청 비활성화", + "edit.disable-leave": "그룹 탈퇴 비활성화", + "edit.hidden": "숨김", + "edit.hidden-details": "활성화되면 그룹 목록에 노출되지 않습니다. 또한 새로운 멤버는 초대를 통해서만 가입이 가능합니다.", + "edit.add-user": "그룹 멤버 추가", + "edit.add-user-search": "사용자 검색", + "edit.members": "멤버 목록", + "control-panel": "그룹 관리", + "revert": "되돌리기", + + "edit.no-users-found": "사용자를 찾을 수 없습니다.", + "edit.confirm-remove-user": "이 멤버를 추방하시겠습니까?", + "edit.save-success": "변경 사항을 저장했습니다!" +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/privileges.json b/public/language/ko/admin/manage/privileges.json new file mode 100644 index 0000000000..e150a7dee6 --- /dev/null +++ b/public/language/ko/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "글로벌", + "admin": "관리자", + "group-privileges": "그룹 권한 설정", + "user-privileges": "사용자 권한 설정", + "edit-privileges": "권한 수정", + "select-clear-all": "전체 선택/해제", + "chat": "채팅", + "upload-images": "이미지 업로드", + "upload-files": "파일 업로드", + "signature": "서명", + "ban": "차단", + "mute": "Mute", + "invite": "초대", + "search-content": "콘텐츠 검색", + "search-users": "사용자 검색", + "search-tags": "태그 검색", + "view-users": "사용자 보기", + "view-tags": "태그 보기", + "view-groups": "그룹 보기", + "allow-local-login": "로컬 로그인", + "allow-group-creation": "그룹 생성", + "view-users-info": "사용자 정보 열람", + "find-category": "카테고리 찾기", + "access-category": "카테고리 접근", + "access-topics": "화제 접근", + "create-topics": "게시글 작성", + "reply-to-topics": "답글 작성", + "schedule-topics": "화제 예약", + "tag-topics": "태그 달기", + "edit-posts": "글 수정", + "view-edit-history": "편집 기록 보기", + "delete-posts": "글 삭제", + "view_deleted": "삭제된 게시물 보기", + "upvote-posts": "글 추천", + "downvote-posts": "글 비추천", + "delete-topics": "화제 삭제", + "purge": "완전 삭제", + "moderate": "조정", + "admin-dashboard": "대시보드", + "admin-categories": "카테고리", + "admin-privileges": "권한", + "admin-users": "사용자", + "admin-admins-mods": "관리자 & 조정자", + "admin-groups": "그룹", + "admin-tags": "태그", + "admin-settings": "설정", + + "alert.confirm-moderate": "해당 그룹에 조정 권한을 주려는 게 확실하십니까? 이 그룹은 공개 그룹이기 때문에 특별한 제한 없이 모든 사용자들이 가입할 수 있습니다.", + "alert.confirm-admins-mods": "해당 그룹에 "관리 & 조정" 권한을 주려는 게 확실하십니까? 해당 권한이 있는 그룹의 사용자들은 다른 조정자를 추가하거나 조정 권한을 해제할 수 있습니다. 최고 관리자를 포함!", + "alert.confirm-save": "권한을 적용하기 전 다시 한번 확인해주세요.", + "alert.saved": "변경된 권한이 적용되었습니다.", + "alert.confirm-discard": "권한 변경을 취소하시겠습니까?", + "alert.discarded": "권한 변경이 취소되었습니다.", + "alert.confirm-copyToAll": "%1의 설정을 모든 카테고리에 적용하시겠습니까?", + "alert.confirm-copyToAllGroup": "%1 그룹의 설정을 모든 카테고리에 적용하시겠습니까?", + "alert.confirm-copyToChildren": "%1의 설정을 모든 하위 카테고리에 적용하시겠습니까?", + "alert.confirm-copyToChildrenGroup": "%1 그룹의 설정을 모든 하위 카테고리에 적용하시겠습니까?", + "alert.no-undo": "이 행동은 되돌릴 수 없습니다.", + "alert.admin-warning": "관리자에게는 절대적인 권한이 부여됩니다.", + "alert.copyPrivilegesFrom-title": "복사할 카테고리 설정", + "alert.copyPrivilegesFrom-warning": "이 작업은 %1의 설정을 선택한 카테고리에서 복사합니다.", + "alert.copyPrivilegesFromGroup-warning": "이 작업은 %1 그룹의 설정을 선택한 카테고리에서 복사합니다." +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/registration.json b/public/language/ko/admin/manage/registration.json new file mode 100644 index 0000000000..00c840cc73 --- /dev/null +++ b/public/language/ko/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "대기열", + "description": "가입 대기열에 사용자가 없습니다.
이 기능을 사용하려면, 설정 → 사용자 → 사용자 등록으로 가서, 가입 유형을 \"관리자 승인\"으로 바꾸세요.", + + "list.name": "이름", + "list.email": "이메일", + "list.ip": "IP 주소", + "list.time": "시간", + "list.username-spam": "빈도: %1 출연 유무: %2 신뢰도: %3", + "list.email-spam": "빈도: %1 출연 유무: %2", + "list.ip-spam": "빈도: %1 출연 유무: %2", + + "invitations": "초대", + "invitations.description": "발송된 초대의 목록을 아래에서 보실 수 있습니다. ctrl-f을 이용해서 이메일이나 사용자명으로 목록을 검색하세요.

초대에 응답한 사용자들은 이메일 옆에 사용자명이 표시됩니다.", + "invitations.inviter-username": "초대자 사용자명", + "invitations.invitee-email": "초대 수신인 이메일", + "invitations.invitee-username": "초대 수신인 사용자명 (가입 시)", + + "invitations.confirm-delete": "이 초대를 삭제하시겠습니까?" +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/tags.json b/public/language/ko/admin/manage/tags.json new file mode 100644 index 0000000000..a183a1fb21 --- /dev/null +++ b/public/language/ko/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "현재 포럼에 태그가 달린 화제가 없습니다.", + "bg-color": "배경 색상", + "text-color": "텍스트 색상", + "description": "클릭이나 드래그로 태그를 선택하고, CTRL로 여러 개의 태그를 선택하세요.", + "create": "태그 생성", + "modify": "태그 수정", + "rename": "태그 이름 바꾸기", + "delete": "선택된 태그 삭제", + "search": "태그 검색", + "settings": "태그 설정", + "name": "태그 이름", + + "alerts.editing": "태그 수정", + "alerts.confirm-delete": "선택된 태그들을 삭제하시겠습니까?", + "alerts.update-success": "태그가 업데이트 됐습니다! ", + "reset-colors": "색상 초기화" +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/uploads.json b/public/language/ko/admin/manage/uploads.json new file mode 100644 index 0000000000..6a5fcf9282 --- /dev/null +++ b/public/language/ko/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "파일 업로드", + "filename": "파일명", + "usage": "등록된 글", + "orphaned": "미등록", + "size/filecount": "크기 / 파일 수", + "confirm-delete": "이 파일을 정말로 삭제하시겠습니까?", + "filecount": "%1 파일", + "new-folder": "새로운 폴더", + "name-new-folder": "폴더의 이름을 입력해주세요." +} \ No newline at end of file diff --git a/public/language/ko/admin/manage/users.json b/public/language/ko/admin/manage/users.json new file mode 100644 index 0000000000..a19dc135e1 --- /dev/null +++ b/public/language/ko/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "사용자", + "edit": "작업", + "make-admin": "관리자 등록", + "remove-admin": "관리자 해제", + "validate-email": "이메일 인증", + "send-validation-email": "인증 이메일 발송", + "password-reset-email": "비밀번호 초기화 이메일 발송", + "force-password-reset": "비밀번호 강제 초기화 & 사용자 로그아웃", + "ban": "사용자 차단", + "temp-ban": "일시적으로 사용자 차단", + "unban": "사용자 차단 해제", + "reset-lockout": "잠금 초기화", + "reset-flags": "신고 초기화", + "delete": "선택한 계정(들) 삭제", + "delete-content": "선택한 계정(들)의 컨텐츠 삭제", + "purge": "선택한 계정(들)컨텐츠 삭제", + "download-csv": "CSV 다운로드", + "manage-groups": "그룹 관리", + "add-group": "그룹 추가", + "create": "Create User", + "invite": "Invite by Email", + "new": "새로운 사용자", + "filter-by": "필터", + "pills.unvalidated": "인증되지 않음", + "pills.validated": "인증됨", + "pills.banned": "차단됨", + + "50-per-page": "페이지 당 50", + "100-per-page": "페이지 당 100", + "250-per-page": "페이지 당 250", + "500-per-page": "페이지 당 500", + + "search.uid": "사용자 ID", + "search.uid-placeholder": "검색할 사용자 ID 입력", + "search.username": "사용자명", + "search.username-placeholder": "검색할 사용자명 입력", + "search.email": "이메일", + "search.email-placeholder": "검색할 이메일 입력", + "search.ip": "IP 주소", + "search.ip-placeholder": "검색할 IP 주소 입력", + "search.not-found": "사용자를 찾을 수 없습니다!", + + "inactive.3-months": "3개월", + "inactive.6-months": "6개월", + "inactive.12-months": "12개월", + + "users.uid": "uid", + "users.username": "사용자명", + "users.email": "이메일", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "글 개수", + "users.reputation": "인지도", + "users.flags": "신고", + "users.joined": "가입일", + "users.last-online": "최근 접속", + "users.banned": "차단일", + + "create.username": "이름", + "create.email": "이메일", + "create.email-placeholder": "사용자의 이메일", + "create.password": "비밀번호", + "create.password-confirm": "비밀번호 재입력", + + "temp-ban.length": "Length", + "temp-ban.reason": "사유 (선택 사항)", + "temp-ban.hours": "시간", + "temp-ban.days": "일", + "temp-ban.explanation": "차단할 기간을 입력하세요. 0을 입력하면 영구적인 차단으로 간주됩니다.", + + "alerts.confirm-ban": "정말 이 사용자를 영구적으로 차단하시겠습니까?", + "alerts.confirm-ban-multi": "정말 이 사용자들을 영구적으로 차단하시겠습니까?", + "alerts.ban-success": "사용자(들)이 차단됐습니다!", + "alerts.button-ban-x": "%1명의 사용자를 차단", + "alerts.unban-success": "사용자의 차단이 해제됐습니다!", + "alerts.lockout-reset-success": "잠금이 초기화됐습니다!", + "alerts.flag-reset-success": "신고가 초기화됐습니다!", + "alerts.no-remove-yourself-admin": "관리자이기 때문에 본인을 삭제할 수 없습니다!", + "alerts.make-admin-success": "사용자는 이제 관리자입니다.", + "alerts.confirm-remove-admin": "정말 관리자 권한을 해제하시겠습니까?", + "alerts.remove-admin-success": "사용자는 더 이상 관리자가 아닙니다.", + "alerts.make-global-mod-success": "사용자는 이제 통합 조정자입니다.", + "alerts.confirm-remove-global-mod": "정말 통합 조정자 권한을 해제하시겠습니까?", + "alerts.remove-global-mod-success": "사용자는 더 이상 통합 조정자가 아닙니다.", + "alerts.make-moderator-success": "사용자는 이제 조정자입니다.", + "alerts.confirm-remove-moderator": "정말 조정자 권한을 해제하시겠습니까?", + "alerts.remove-moderator-success": "사용자는 더 이상 조정자가 아닙니다.", + "alerts.confirm-validate-email": "이 사용자(들)의 이메일을 인증하시겠습니까?", + "alerts.confirm-force-password-reset": "정말 비밀번호를 강제로 초기화하고 사용자(들)의 로그아웃을 진행합니까?", + "alerts.validate-email-success": "이메일 인증 완료", + "alerts.validate-force-password-reset-success": "사용자(들)의 비밀번호가 초기화되고 세션이 초기화되었습니다.", + "alerts.password-reset-confirm": "이 사용자(들)에게 비밀번호 초기화 이메일을 보내시겠습니까?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "경고!

정말 계정(들)의 삭제를 진행합니까?

이 행동은 되돌릴 수 없습니다! 계정만 삭제되고 화제와 포스트는 삭제되지 않습니다.

", + "alerts.delete-success": "계정 삭제 완료!", + "alerts.confirm-delete-content": "경고!

정말 이 사용자(들)의게시물 삭제를 진행합니까?

이 행동은 되돌릴 수 없습니다! 계정은 삭제되지 않지만 모든 포스트와 화제가 삭제됩니다.

", + "alerts.delete-content-success": "사용자 정보 삭제 완료!", + "alerts.confirm-purge": "경고!

정말 이 사용자(들)의 계정과 게시물의 삭제를 진행합니까?

이 행동은 되돌릴 수 없습니다! 모든 계정 정보와 게시물이 삭제됩니다!

", + "alerts.create": "사용자 생성", + "alerts.button-create": "생성", + "alerts.button-cancel": "취소", + "alerts.error-passwords-different": "비밀번호가 일치하지 않습니다!", + "alerts.error-x": "오류

%1

", + "alerts.create-success": "사용자 생성 완료!", + + "alerts.prompt-email": "이메일:", + "alerts.email-sent-to": "%1에게 초대 이메일이 발송됐습니다.", + "alerts.x-users-found": "%1 사용자 해당, (%2초)", + "export-users-started": "사용자 리스트를 csv 파일로 내보내기합니다. 이 과정은 약간의 시간이 소요되며 완료되면 알림을 수신합니다.", + "export-users-completed": "사용자 리스트 내보내기 완료, 여기를 눌러 다운로드" +} \ No newline at end of file diff --git a/public/language/ko/admin/menu.json b/public/language/ko/admin/menu.json new file mode 100644 index 0000000000..f495308092 --- /dev/null +++ b/public/language/ko/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "대시보드", + "dashboard/overview": "둘러보기", + "dashboard/logins": "로그인 기록", + "dashboard/users": "사용자", + "dashboard/topics": "화제", + "dashboard/searches": "Searches", + "section-general": "일반", + + "section-manage": "관리", + "manage/categories": "카테고리", + "manage/privileges": "권한", + "manage/tags": "태그", + "manage/users": "사용자", + "manage/admins-mods": "관리자 & 조정자", + "manage/registration": "가입 승인 대기열", + "manage/post-queue": "게시 대기열", + "manage/groups": "그룹", + "manage/ip-blacklist": "IP 블랙리스트", + "manage/uploads": "업로드", + "manage/digest": "포럼 메일", + + "section-settings": "설정", + "settings/general": "일반", + "settings/homepage": "홈페이지", + "settings/navigation": "바로가기", + "settings/reputation": "인지도 & 신고", + "settings/email": "이메일", + "settings/user": "사용자", + "settings/group": "그룹", + "settings/guest": "비회원", + "settings/uploads": "업로드", + "settings/languages": "언어", + "settings/post": "포스트", + "settings/chat": "채팅", + "settings/pagination": "페이지", + "settings/tags": "태그", + "settings/notifications": "알림", + "settings/api": "API 연결", + "settings/sounds": "소리", + "settings/social": "SNS 공유", + "settings/cookies": "쿠키", + "settings/web-crawler": "웹 크롤러", + "settings/sockets": "소켓", + "settings/advanced": "고급", + + "settings.page-title": "%1 설정", + + "section-appearance": "스타일", + "appearance/themes": "테마", + "appearance/skins": "스킨", + "appearance/customise": "사용자 정의 콘텐츠 (HTML/JS/CSS)", + + "section-extend": "확장 기능", + "extend/plugins": "플러그인", + "extend/widgets": "위젯", + "extend/rewards": "보상", + + "section-social-auth": "외부 로그인", + + "section-plugins": "플러그인", + "extend/plugins.install": "플러그인 설치", + + "section-advanced": "고급", + "advanced/database": "데이터베이스", + "advanced/events": "이벤트", + "advanced/hooks": "훅", + "advanced/logs": "로그", + "advanced/errors": "에러", + "advanced/cache": "캐시", + "development/logger": "로그 설정", + "development/info": "정보", + + "rebuild-and-restart-forum": "리빌드 & 포럼 재시작", + "restart-forum": "포럼 재시작", + "logout": "로그아웃", + "view-forum": "포럼 보기", + + "search.placeholder": "Search settings", + "search.no-results": "검색 결과 없음...", + "search.search-forum": "포럼에서 검색", + "search.keep-typing": "검색 결과를 보기 위해 더 입력하세요...", + "search.start-typing": "검색 결과를 보기 위해 여기 입력하세요...", + + "connection-lost": "%1과의 연결이 끊어졌습니다. 다시 연결 시도 중...", + + "alerts.version": "NodeBB v%1 실행 중", + "alerts.upgrade": "v%1로 업그레이드" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/advanced.json b/public/language/ko/admin/settings/advanced.json new file mode 100644 index 0000000000..9d6966eab9 --- /dev/null +++ b/public/language/ko/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "점검 모드", + "maintenance-mode.help": "포럼이 점검 모드일 경우 모든 접속 요청은 정적 페이지로 리다이렉트됩니다. 관리자는 이 리다이렉션에 적용되지 않고 사이트에 접속하는 것이 가능합니다.", + "maintenance-mode.status": "점검 모드 상태 코드", + "maintenance-mode.message": "점검 공지", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "헤더", + "headers.allow-from": "NodeBB를 iFrame에 삽입할 수 있게 하시려면, ALLOW-FROM(NodeBB를 Embedding할 수 있는 도메인)을 설정하세요.", + "headers.csp-frame-ancestors": "NodeBB를 iFrame에 삽입하기 위한 컨텐츠 보안 정책 frame-ancestors 헤더 설정", + "headers.csp-frame-ancestors-help": "'none', 'self(기본값)'으로 설정하거나 허용할 URI 목록 작성", + "headers.powered-by": "NodeBB의 \"Powered By\" 헤더 커스터마이징", + "headers.acao": "Access-Control-Allow-Origin 응답 헤더", + "headers.acao-regex": "Access-Control-Allow-Origin 정규식", + "headers.acao-help": "모든 사이트의 접근을 거부하려면 빈칸으로 설정", + "headers.acao-regex-help": "동적 출처를 확인하기 위한 정규식을 입력하세요. 빈칸으로 설정하면 모든 사이트의 접근을 거부합니다.", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "HSTS 활성화 (권장)", + "hsts.maxAge": "HSTS 유효 기간", + "hsts.subdomains": "하위 도메인에 HSTS 적용", + "hsts.preload": "HSTS preload 허용", + "hsts.help": "활성화하면 이 사이트에 HSTS 헤더가 적용됩니다. 옵션 활성화를 통해 하위 도메인에도 적용하거나 브라우저에서 제공하는 목록을 불러올 수 있습니다. 신뢰할 수 없다면 옵션을 활성화하지 마세요. 더 많은 정보는 여기를 눌러 확인하세요. ", + "traffic-management": "트래픽 관리", + "traffic.help": "NodeBB는 트래픽이 많은 상황에서 자동으로 요청을 거부하는 모듈을 사용합니다. 여기서 설정을 변경할 수 있지만 기본값도 나쁘지 않은 선택입니다.", + "traffic.enable": "트래픽 관리 허용", + "traffic.event-lag": "이벤트 루프 간격(단위: 1/1000초)", + "traffic.event-lag-help": "이 값을 낮추게 되면 페이지 로딩에 걸리는 시간이 단축되지만, 더 많은 사용자들이 \"과도한 로딩\"이라는 메시지를 보게됩니다. (재시작 필요)", + "traffic.lag-check-interval": "트래픽 체크 간격(단위: 1/1000초)", + "traffic.lag-check-interval-help": "이 값을 낮추게 되면 갑작스런 로딩값 변화에 더 민감해지지만, 과하게 예민한 반응을 야기할 수 있습니다. (재시작 필요)", + + "sockets.settings": "웹소켓 설정", + "sockets.max-attempts": "최대 연결 시도 횟수", + "sockets.default-placeholder": "기본: %1", + "sockets.delay": "재접속 지연", + + "analytics.settings": "애널리틱스 설정", + "analytics.max-cache": "애널리틱스 캐시 한도", + "analytics.max-cache-help": "트래픽이 많은 설치에서 동시 접속자가 캐시 한도보다 많을 경우 캐시가 연속적으로 소진될 수 있습니다. (재시작 필요)", + "compression.settings": "압축 설정", + "compression.enable": "압축 활성화", + "compression.help": "이 설정으로 gzip 압축을 활성화할 수 있습니다. 트래픽이 많은 웹사이트에서 압축을 적용하는 가장 좋은 방법은 리버스 프록시 레벨에서 구현하는 것입니다. 여기서 테스트 목적으로 활성화할 수 있습니다." +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/api.json b/public/language/ko/admin/settings/api.json new file mode 100644 index 0000000000..81affcdb80 --- /dev/null +++ b/public/language/ko/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "토큰", + "settings": "설정", + "lead-text": "이 설정 화면에서 NodeBB에 Write API의 연결을 설정할 수 있습니다.", + "intro": "기본적으로 Write API는 세션 쿠키를 기반으로 사용자를 인증하지만 NodeBB는 이 페이지를 통해 생성된 토큰을 통해 Bearer 인증도 지원합니다.", + "docs": "여기를 클릭해서 자세한 API 설정 방법 확인", + + "require-https": "API 사용을 HTTPS 접속으로만 허용", + "require-https-caveat": "참고: Load balancer와 관련된 일부 설치에서는 HTTP를 사용하여 요청을 NodeBB에 프록시하므로 이 옵션을 사용하지 않도록 설정해야 합니다.", + + "uid": "User ID", + "uid-help-text": "이 토큰과 연결할 User ID를 지정하세요. User ID가 0일 경우 master 토큰으로 간주되어 다른 사용자의 정보를 _uid 패러미터를 통해 알 수 있게 됩니다.", + "description": "설명", + "no-description": "설명 없음", + "token-on-save": "현재 설정 저장 후 토큰 생성" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/chat.json b/public/language/ko/admin/settings/chat.json new file mode 100644 index 0000000000..979cd5c488 --- /dev/null +++ b/public/language/ko/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "채팅 설정", + "disable": "채팅 비활성화", + "disable-editing": "채팅 메시지 수정/삭제 비활성화", + "disable-editing-help": "관리자와 조정자는 제한되지 않습니다.", + "max-length": "채팅 메시지의 최대 길이", + "max-room-size": "채팅방 최대 인원", + "delay": "채팅 메시지 발송 지연 (단위: 1/1000초)", + "notification-delay": "채팅 메시지 알림 지연 (0으로 놔둘 경우 지연 없음)", + "restrictions.seconds-edit-after": "채팅 메시지 수정 허용 시간 (0일 경우 비활성화)", + "restrictions.seconds-delete-after": "채팅 메시지 삭제 허용 시간 (0일 경우 비활성화)" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/cookies.json b/public/language/ko/admin/settings/cookies.json new file mode 100644 index 0000000000..8dce6dade9 --- /dev/null +++ b/public/language/ko/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU 법률 관련", + "consent.enabled": "활성화됨", + "consent.message": "알림 메세지", + "consent.acceptance": "허가 메세지", + "consent.link-text": "약관 조항 링크에 표시할 텍스트", + "consent.link-url": "약관 조항 링크", + "consent.blank-localised-default": "NodeBB의 번역을 사용하려면 빈칸으로 두세요.", + "settings": "설정", + "cookie-domain": "세션 쿠키 도메인", + "max-user-sessions": "사용자 당 최대 활성 세션", + "blank-default": "기본값을 사용하려면 빈칸으로 두세요." +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/email.json b/public/language/ko/admin/settings/email.json new file mode 100644 index 0000000000..9c90b4b582 --- /dev/null +++ b/public/language/ko/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "이메일 설정", + "address": "포럼 메일 주소", + "address-help": "아래 이메일 주소는 수신인의 \"보낸 사람\"과 \"답장하기\" 항목에서 보이게 됩니다.", + "from": "포럼 메일 발신자 이름", + "from-help": "이메일에 표시할 발신자 이름", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "자주 사용되는 서비스 목록 중에 하나를 선택하거나 직접 입력할 수 있습니다.", + "smtp-transport.service": "서비스 선택", + "smtp-transport.service-custom": "직접 입력", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP 호스트", + "smtp-transport.port": "SMTP 포트", + "smtp-transport.security": "연결 보안", + "smtp-transport.security-encrypted": "암호화됨", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "사용자명", + "smtp-transport.username-help": "Gmail로 등록할 경우 이메일 주소를 입력하세요. Google Apps에서 관리하는 도메인을 사용할 경우에는 필수입니다.", + "smtp-transport.password": "비밀번호", + "smtp-transport.pool": "Connection pool 활성화", + "smtp-transport.pool-help": "Connection pool은 NodeBB가 이메일마다 새로운 연결을 생성하는 것을 방지합니다. 이 옵션은 SMTP Transport 기능이 활성화 상태일 때만 사용할 수 있습니다.", + + "template": "이메일 템플릿 수정", + "template.select": "이메일 템플릿 선택", + "template.revert": "원본으로 되돌리기", + "testing": "이메일 발신 테스트", + "testing.select": "이메일 템플릿 선택", + "testing.send": "테스트 이메일 보내기", + "testing.send-help": "현재 로그인 중인 사용자의 이메일로 테스트 이메일을 보냅니다.", + "subscriptions": "포럼 메일 설정", + "subscriptions.disable": "포럼 메일 비활성화", + "subscriptions.hour": "포럼 메일 발송 시간", + "subscriptions.hour-help": "정기 포럼 메일을 보낼 시간을 입력해주세요. (예: 0은 자정, 17은 오후 5시 입니다. 이 시간은 서버 시간 기준이며, 사용자의 시스템 시간과 일치하지 않을 수 있습니다.
서버 시간은 입니다.
다음 정기 포럼 메일은 에 발송 예정입니다.", + "notifications.remove-images": "이메일 알림에서 이미지 제거", + "require-email-address": "신규 사용자에게 이메일 주소 설정 요구", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "전자 메일을 명시적으로 확인하지 않은 수신자에게 전자 메일 보내기", + "include-unverified-warning": "기본적으로 계정과 연결된 전자 메일이 있는 사용자는 이미 확인되었지만 그렇지 않은 경우가 있습니다(예: SSO 로그인, 약관으로부터 제외된 사용자 등). 사용자가 위험을 감수하고 이 설정을 사용하도록 설정합니다. – 확인되지 않은 주소로 이메일을 보내는 것은 지역별 스팸 방지법을 위반하는 것일 수 있습니다.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/ko/admin/settings/general.json b/public/language/ko/admin/settings/general.json new file mode 100644 index 0000000000..c20abb4195 --- /dev/null +++ b/public/language/ko/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "사이트 설정", + "title": "사이트 이름", + "title.short": "짧은 이름", + "title.short-placeholder": "짧은 제목이 설정되지 않으면 일반 사이트 이름을 로고처럼 사용합니다.", + "title.url": "Title Link URL", + "title.url-placeholder": "사이트 이름을 눌렀을 때 이동할 URL", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "커뮤니티 이름", + "title.show-in-header": "상단바에 사이트 이름 표시", + "browser-title": "브라우저 타이틀", + "browser-title-help": "브라우저 타이틀이 입력되지 않으면 사이트 이름이 사용됩니다.", + "title-layout": "브라우저 타이틀 레이아웃", + "title-layout-help": "브라우저 타이틀이 어떻게 표기 될지 설정해 주세요. 예: {pageTitle} | {browserTitle} ", + "description.placeholder": "커뮤니티에 대한 간략한 설명", + "description": "사이트 설명", + "keywords": "사이트 키워드", + "keywords-placeholder": "콤마(,)로 분리된 커뮤니티를 묘사하는 키워드들", + "logo": "사이트 로고", + "logo.image": "사진", + "logo.image-placeholder": "로고 파일 저장 위치", + "logo.upload": "업로드", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "사이트 로고 URL", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "대체 텍스트", + "log.alt-text-placeholder": "대체 텍스트", + "favicon": "파비콘", + "favicon.upload": "업로드", + "pwa": "프로그레시브 웹 앱", + "touch-icon": "터치 아이콘", + "touch-icon.upload": "업로드", + "touch-icon.help": "권장 사항: 512x512, PNG 확장자만 가능, 지정되지 않을 경우 파비콘 사용", + "maskable-icon": "웹 앱(홈 화면) 아이콘", + "maskable-icon.help": "권장 사항: 512x512, PNG 확장자만 가능, 지정되지 않을 경우 터치 아이콘 사용", + "outgoing-links": "외부 링크", + "outgoing-links.warning-page": "외부 링크 경고페이지 사용", + "search": "검색", + "search-default-in": "범위 검색", + "search-default-in-quick": "빠른 범위 검색", + "search-default-sort-by": "분류", + "outgoing-links.whitelist": "경고 창이 필요 없는 외부 링크 도메인 whitelist", + "site-colors": "사이트 색상 설정", + "theme-color": "테마 색상", + "background-color": "배경 색상", + "background-color-help": "사이트가 PWA로 설치될 때 스플래시 화면 배경에 사용되는 색상", + "undo-timeout": "되돌리기 시간 초과", + "undo-timeout-help": "조정자는 주제 이동과 같은 일부 작업을 통해 특정 기간 내에 작업을 취소할 수 있습니다. 되돌리기를 완전히 비활성화하려면 0으로 설정합니다.", + "topic-tools": "주제 도구" +} diff --git a/public/language/ko/admin/settings/group.json b/public/language/ko/admin/settings/group.json new file mode 100644 index 0000000000..f56ce9991f --- /dev/null +++ b/public/language/ko/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "일반", + "private-groups": "비공개 그룹", + "private-groups.help": " 활성화 되어있다면 그룹에 가입하는 것은 그룹 관리자의 허가를 필요로 합니다. (기본 설정: 활성화)", + "private-groups.warning": "주의 이 옵션이 비활성화 돼있고 당신에게 비공개 그룹이 있다면 그 그룹들은 모두 공개로 전환될 것입니다.", + "allow-multiple-badges": "여러 개의 뱃지 허용", + "allow-multiple-badges-help": "사용자가 여러 개의 뱃지를 선택할 수 있지만 해당 기능을 지원하는 테마에서만 사용할 수 있습니다.", + "max-name-length": "그룹명 최대 길이", + "max-title-length": "그룹 제목 최대 길이", + "cover-image": "그룹 커버 사진", + "default-cover": "기본 커버 사진", + "default-cover-help": "기본 커버 사진 목록을 콤마(,) 로 구분지어 입력해 주세요. " +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/guest.json b/public/language/ko/admin/settings/guest.json new file mode 100644 index 0000000000..2406fdffe3 --- /dev/null +++ b/public/language/ko/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "설정", + "handles.enabled": "비회원 닉네임 설정 허가", + "handles.enabled-help": "이 옵션은 비회원들이 포스트를 작성할 때 이름을 적는 공간을 제공합니다. 이 옵션이 비활성화 상태라면 \"Guest\" 라고 표시될 것입니다.", + "topic-views.enabled": "비회원의 방문으로 화제 조회수 증가", + "reply-notifications.enabled": "비회원의 답글 알림 허용" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/homepage.json b/public/language/ko/admin/settings/homepage.json new file mode 100644 index 0000000000..eaf2490bec --- /dev/null +++ b/public/language/ko/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "홈페이지", + "description": "사용자가 루트 URL에 들어갔을 때 어떤 페이지를 보여줄지 선택하세요.", + "home-page-route": "홈페이지 경로", + "custom-route": "사용자 정의 경로", + "allow-user-home-pages": "사용자가 직접 홈페이지를 설정할 수 있게 허용", + "home-page-title": "홈페이지의 타이틀 (기본값 \"Home\")" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/languages.json b/public/language/ko/admin/settings/languages.json new file mode 100644 index 0000000000..84b52efba2 --- /dev/null +++ b/public/language/ko/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "언어 설정", + "description": "기본 언어 설정은 사이트를 방문하는 모든 사용자들에게 적용됩니다.
하지만 사용자들이 직접 본인의 계정 설정 페이지에서 언어 설정을 바꿀 수 있습니다.", + "default-language": "기본 언어", + "auto-detect": "비회원의 언어 설정을 자동으로 감지합니다." +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/navigation.json b/public/language/ko/admin/settings/navigation.json new file mode 100644 index 0000000000..6c6f584c78 --- /dev/null +++ b/public/language/ko/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "아이콘:", + "change-icon": "변경", + "route": "경로:", + "tooltip": "툴팁:", + "text": "텍스트:", + "text-class": "텍스트 클래스: 선택사항", + "class": "클래스: 선택사항", + "id": "ID: 선택사항", + + "properties": "속성:", + "groups": "그룹:", + "open-new-window": "새 창에서 열기", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "삭제", + "btn.disable": "비활성화", + "btn.enable": "활성화", + + "available-menu-items": "이용 가능한 메뉴 항목", + "custom-route": "사용자 정의 경로", + "core": "코어", + "plugin": "플러그인" +} diff --git a/public/language/ko/admin/settings/notifications.json b/public/language/ko/admin/settings/notifications.json new file mode 100644 index 0000000000..c9a3ebb946 --- /dev/null +++ b/public/language/ko/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "알림", + "welcome-notification": "환영 알림", + "welcome-notification-link": "환영 알림 링크", + "welcome-notification-uid": "환영 알림 사용자 (UID)", + "post-queue-notification-uid": "게시 대기 중인 사용자 (UID)" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/pagination.json b/public/language/ko/admin/settings/pagination.json new file mode 100644 index 0000000000..d3797d60a5 --- /dev/null +++ b/public/language/ko/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "페이지 설정", + "enable": "무한 스크롤 대신 페이지로 화제와 포스트 보여주기", + "posts": "포스트 페이지", + "topics": "화제 페이지", + "posts-per-page": "페이지 당 포스트", + "max-posts-per-page": "페이지 당 최대 포스트", + "categories": "카테고리 페이지", + "topics-per-page": "페이지 당 화제", + "max-topics-per-page": "페이지 당 최대 화제", + "categories-per-page": "페이지 당 카테고리" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/post.json b/public/language/ko/admin/settings/post.json new file mode 100644 index 0000000000..4635b87996 --- /dev/null +++ b/public/language/ko/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "포스트 정렬", + "sorting.post-default": "기본 포스트 정렬", + "sorting.oldest-to-newest": "오래된순", + "sorting.newest-to-oldest": "최신순", + "sorting.most-votes": "투표순", + "sorting.most-posts": "포스트순", + "sorting.topic-default": "기본 화제 정렬", + "length": "포스트 길이", + "post-queue": "게시 대기열", + "restrictions": "글 작성 제한", + "restrictions-new": "신규 사용자 제한", + "restrictions.post-queue": "게시 대기열 활성화", + "restrictions.post-queue-rep-threshold": "게시 대기 대상에서 제외되는 최소 인지도", + "restrictions.groups-exempt-from-post-queue": "선택한 그룹은 게시 대기 대상에서 제외됩니다.", + "restrictions-new.post-queue": "신규 사용자 제한 활성화", + "restrictions.post-queue-help": "게시 대기열을 활성화하면 신규 사용자들이 포스트를 작성할 때 게시 대기열에서 승인이 필요합니다.", + "restrictions-new.post-queue-help": "신규 사용자 제한을 활성화할 경우 신규 사용자들의 포스트 생성이 제한됩니다.", + "restrictions.seconds-between": "포스트 작성 지연(단위: 초)", + "restrictions.seconds-between-new": "신규 사용자 포스트 작성 지연(단위: 초)", + "restrictions.rep-threshold": "제한이 해제되는 최소 인지도", + "restrictions.seconds-before-new": "신규 사용자 첫 포스트 작성 대기 시간(단위: 초)", + "restrictions.seconds-edit-after": "포스트 수정 가능 시간(단위: 초, 0일 경우 비활성화)", + "restrictions.seconds-delete-after": "포스트 삭제 가능 시간(단위: 초, 0일 경우 비활성화)", + "restrictions.replies-no-delete": "화제 삭제 금지 답글 수(0일 경우 비활성화)", + "restrictions.min-title-length": "최소 제목 길이", + "restrictions.max-title-length": "최대 제목 길이", + "restrictions.min-post-length": "최소 포스트 길이", + "restrictions.max-post-length": "최대 포스트 길이", + "restrictions.days-until-stale": "신선한 화제 지속 기간", + "restrictions.stale-help": "게시글이 신선한 화제 지속 기간을 지나면, 지루한 화제로 판단하고 해당 화제에 답글을 작성하는 모든 사용자에게 경고 메세지를 발송합니다.", + "timestamp": "시간 표기", + "timestamp.cut-off": "상대시간 표기 기간(일)", + "timestamp.cut-off-help": "날짜 및 시간을 상대시간으로 표기.(예: \"3시간 전\" / \"5일 전\") 표기 기간이 지나면 지역시간으로 변환.(예: 2016년 11월 5일 15:30)
(기본값: 30일, 또는 한달). 0으로 지정 시 항상 날짜 표기, 비워둘 경우 항상 상대시간 표기.", + "timestamp.necro-threshold": "사망 기간(단위: 일)", + "timestamp.necro-threshold-help": "사망 기간보다 오래 죽어있던 화제의 포스트 사이에 메시지를 표시합니다. (기본값: 7일 or 1주) 0일 경우 비활성화.", + "timestamp.topic-views-interval": "화제 조회 지연(단위: 분)", + "timestamp.topic-views-interval-help": "설정한 시간동안 1회의 조회수만 증가합니다.", + "teaser": "미리보기", + "teaser.last-post": "최근 - 최근 작성된 포스트를 보여주고 답글이 없을 경우 포스트 본문 보여주기", + "teaser.last-reply": "최근 - 최근 작성된 답글을 보여주고 답글이 없을 경우 \"답글 없음\" 표시", + "teaser.first": "첫 글", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "읽지 않음 목록 설정", + "unread.cutoff": "읽지 않음 표시 기간", + "unread.min-track-last": "마지막으로 읽은 글 추적 기능을 사용할 최소 글 수", + "recent": "최근 목록 설정", + "recent.max-topics": "최근 목록에 표시할 화제 갯수", + "recent.categoryFilter.disable": "최근 목록에서 무시 중인 카테고리의 화제 포함", + "signature": "서명 설정", + "signature.disable": "서명 비활성화", + "signature.no-links": "서명에 포함된 바로가기 비활성화", + "signature.no-images": "서명에 포함된 이미지 비활성화", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "최대 서명 길이", + "composer": "에디터 설정", + "composer-help": "아래의 설정들은 사용자가 새로운 화제나 답글을 작성할 때 나타나는 에디터 화면의 기능과 외형에 영향을 끼칩니다.", + "composer.show-help": "\"도움말\" 탭 표시", + "composer.enable-plugin-help": "플러그인의 도움말 탭 내용 추가 허용", + "composer.custom-help": "사용자 정의 \"도움말\" 텍스트", + "backlinks": "역링크", + "backlinks.enabled": "화제 역링크 활성화", + "backlinks.help": "포스트가 다른 화제를 참조할 경우 참조한 화제에 해당 포스트의 역링크가 표시됩니다.", + "ip-tracking": "IP 추적", + "ip-tracking.each-post": "모든 포스트 IP 추적", + "enable-post-history": "게시글 편집 기록 활성화" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/reputation.json b/public/language/ko/admin/settings/reputation.json new file mode 100644 index 0000000000..42ccf3753b --- /dev/null +++ b/public/language/ko/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "인지도 설정", + "disable": "인지도 시스템 비활성화", + "disable-down-voting": "비추천 비활성화", + "votes-are-public": "모든 투표 비익명화", + "thresholds": "포럼 활동 기준선", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "포스트 비추천에 필요한 최소 인지도", + "downvotes-per-day": "일일 최대 비추천 (0일 경우 무제한)", + "downvotes-per-user-per-day": "개인 일일 최대 비추천 (0일 경우 무제한)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "포스트 신고에 필요한 최소 인지도", + "min-rep-website": "\"웹사이트\" 등록에 필요한 최소 인지도", + "min-rep-aboutme": "\"설명\" 작성에 필요한 최소 인지도", + "min-rep-signature": "\"서명\" 작성에 필요한 최소 인지도", + "min-rep-profile-picture": "\"프로필 사진\" 등록에 필요한 최소 인지도", + "min-rep-cover-picture": "\"커버 사진\" 등록에 필요한 최소 인지도", + + "flags": "신고 설정", + "flags.limit-per-target": "포스트 혹은 사용자 최대 신고 횟수", + "flags.limit-per-target-placeholder": "기본값: 0", + "flags.limit-per-target-help": "포스트나 사용자가 다수의 신고를 받을 경우 각각의 신고가 최초의 신고와 함께 표시됩니다. 이 옵션을 변경해서 하나의 대상에 누적될 신고의 최대 횟수를 지정할 수 있습니다.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "차단될 경우 사용자의 모든 기회 박탈", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/social.json b/public/language/ko/admin/settings/social.json new file mode 100644 index 0000000000..56a8fe660b --- /dev/null +++ b/public/language/ko/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "포스트 공유", + "info-plugins-additional": "플러그인을 이용해서 포스트를 공유할 수 있는 네트워크를 추가할 수 있습니다.", + "save-success": "포스트를 공유할 네트워크 추가 완료!" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/sockets.json b/public/language/ko/admin/settings/sockets.json new file mode 100644 index 0000000000..905ab55851 --- /dev/null +++ b/public/language/ko/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "재접속 설정", + "max-attempts": "최대 재접속 시도 횟수", + "default-placeholder": "기본: %1", + "delay": "재접속 지연" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/sounds.json b/public/language/ko/admin/settings/sounds.json new file mode 100644 index 0000000000..1d9031cb19 --- /dev/null +++ b/public/language/ko/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "알림", + "chat-messages": "채팅 메시지", + "play-sound": "재생", + "incoming-message": "수신 메시지", + "outgoing-message": "발신 메시지", + "upload-new-sound": "새로운 사운드 업로드", + "saved": "설정 저장됨" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/tags.json b/public/language/ko/admin/settings/tags.json new file mode 100644 index 0000000000..0e90bbf493 --- /dev/null +++ b/public/language/ko/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "태그 설정", + "link-to-manage": "태그 관리", + "system-tags": "시스템 태그", + "system-tags-help": "관리자와 조정자들만 해당 태그들을 사용할 수 있습니다.", + "min-per-topic": "화제 별 최소 태그", + "max-per-topic": "화제 별 최대 태그", + "min-length": "태그 최소 길이", + "max-length": "태그 최대 길이", + "related-topics": "관련 화제", + "max-related-topics": "(테마가 지원할 경우) 보여질 화제의 최대 개수" +} \ No newline at end of file diff --git a/public/language/ko/admin/settings/uploads.json b/public/language/ko/admin/settings/uploads.json new file mode 100644 index 0000000000..ed61971356 --- /dev/null +++ b/public/language/ko/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "포스트", + "orphans": "Orphaned Files", + "private": "가입된 사용자만 파일 열람 허용", + "strip-exif-data": "이미지 EXIF 데이터 제거", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "파일 확장자 숨김", + "private-uploads-extensions-help": "비공개로 설정할 파일 확장자 목록을 쉼표로 구분해서 입력하세요. (예: pdf, xls, doc). 빈 목록은 모든 파일이 비공개임을 의미합니다.", + "resize-image-width-threshold": "설정한 너비보다 넓은 이미지의 크기 조정", + "resize-image-width-threshold-help": "(단위: px, 기본값: 1520px, 0일 경우 비활성화)", + "resize-image-width": "조정할 이미지의 넓이", + "resize-image-width-help": "(단위: px, 기본값: 760px, 0일 경우 비활성화)", + "resize-image-quality": "크기를 조정한 이미지의 품질", + "resize-image-quality-help": "이미지의 용량을 줄이려면 낮은 품질을 선택하세요.", + "max-file-size": "최대 파일 사이즈(KB)", + "max-file-size-help": "(키비바이트로, 기본: 2048 KiB)", + "reject-image-width": "이미지 최대 너비(단위: px)", + "reject-image-width-help": "해당 수치보다 넓은 이미지는 업로드되지 않습니다.", + "reject-image-height": "이미지 최대 높이(단위: px)", + "reject-image-height-help": "해당 수치보다 높은 이미지는 업로드되지 않습니다.", + "allow-topic-thumbnails": "사용자들의 화제 썸네일 업로드 허용", + "topic-thumb-size": "화제 썸네일 크기", + "allowed-file-extensions": "사용 가능한 파일 확장자", + "allowed-file-extensions-help": "파일 확장자 목록을 콤마(,) 로 구분지어 입력해주세요.(예: pdf, xls, doc) 빈칸으로 남기면 모든 확장자를 허용합니다. ", + "upload-limit-threshold": "업로드 속도 제한:", + "upload-limit-threshold-per-minute": "%1분 기준", + "upload-limit-threshold-per-minutes": "%1분 기준", + "profile-avatars": "프로필 사진", + "allow-profile-image-uploads": "사용자들이 프로필 사진 업로드 하는것을 허용", + "convert-profile-image-png": "업로드 된 프로필 사진 확장자를 PNG로 변환", + "default-avatar": "사용자 설정 기본 프로필 사진", + "upload": "업로드", + "profile-image-dimension": "프로필 사진 규격", + "profile-image-dimension-help": "(단위: px, 기본값: 128px)", + "max-profile-image-size": "프로필 사진 최대 크기", + "max-profile-image-size-help": "(키비바이트로, 기본: 256 KiB)", + "max-cover-image-size": "커버 사진 최대 크기", + "max-cover-image-size-help": "(키비바이트로, 기본: 2,048 KiB)", + "keep-all-user-images": "이전 프로필 사진과 커버 사진 서버에 저장", + "profile-covers": "프로필 커버 사진", + "default-covers": "기본 커버 사진", + "default-covers-help": "기본 커버 사진 목록을 콤마(,)로 구분지어 입력해주세요. " +} diff --git a/public/language/ko/admin/settings/user.json b/public/language/ko/admin/settings/user.json new file mode 100644 index 0000000000..ac9db811b2 --- /dev/null +++ b/public/language/ko/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "인증", + "email-confirm-interval": "사용자는", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "로그인 허용 수단", + "allow-login-with.username-email": "사용자명 또는 이메일", + "allow-login-with.username": "사용자명", + "account-settings": "계정 관리", + "gdpr_enabled": "GDPR 동의 수집 활성화", + "gdpr_enabled_help": "활성화되면 모든 신규 등록자는 General Data Protection Regulation (GDPR)에 따라 데이터 수집 및 사용에 대해 명시적으로 동의해야 합니다. 참고: GDPR을 활성화해도 기존 사용자가 동의하지 않을 수 있습니다. 동의를 강제하려면 GDPR 플러그인을 설치해야 합니다.", + "disable-username-changes": "사용자명 변경 비활성화", + "disable-email-changes": "이메일 주소 변경 비활성화", + "disable-password-changes": "비밀번호 변경 비활성화", + "allow-account-deletion": "계정 삭제 허용", + "hide-fullname": "사용자 실명 숨기기", + "hide-email": "사용자 이메일 숨기기", + "show-fullname-as-displayname": "사용자의 실명을 사용자명으로 적용", + "themes": "테마", + "disable-user-skins": "일반 사용자의 스킨 지정 금지", + "account-protection": "계정 보호", + "admin-relogin-duration": "관리자 로그인 지속 시간 (분)", + "admin-relogin-duration-help": "지정한 시간이 지나면 관리자 화면에서 로그인을 다시 요청, 0으로 지정할 경우 비활성화", + "login-attempts": "시간 당 가능한 로그인 시도 횟수", + "login-attempts-help": "사용자의 로그인 시도가 이 횟수제한을 초과하면 정해진 시간만큼 해당 계정이 잠깁니다.", + "lockout-duration": "계정 잠금 기간 (분)", + "login-days": "사용자 로그인 세션 유지일", + "password-expiry-days": "주기적으로 비밀번호 초기화", + "session-time": "세션 시간", + "session-time-days": "일", + "session-time-seconds": "초", + "session-time-help": "사용자가 "로그인 유지" 항목을 활성화할 경우 해당 수치만큼 사용자의 로그인 상태를 유지합니다. 다음 값들 중 한 가지를 사용합니다. 에 해당되는 값이 없을 경우 에 해당되는 값을 적용하고, 에 해당되는 값도 없을 경우 기본값인 14일을 적용합니다.", + "online-cutoff": "사용자를 비접속 상태로 간주할 시간 (분)", + "online-cutoff-help": "해당 시간동안 사용자의 행동이 없을 경우 비접속 상태로 간주하고 실시간 업데이트를 적용하지 않습니다.", + "registration": "회원가입", + "registration-type": "가입 유형", + "registration-approval-type": "가입 대기 유형", + "registration-type.normal": "일반", + "registration-type.admin-approval": "관리자 승인", + "registration-type.admin-approval-ip": "관리자 IP 승인", + "registration-type.invite-only": "초대 가입", + "registration-type.admin-invite-only": "관리자 초대 가입", + "registration-type.disabled": "신규 가입 불가", + "registration-type.help": "일반 - 회원가입 페이지를 통해 가입할 수 있습니다.
\n초대 가입 - 사용자 페이지에서 기존 사용자가 초대를 해야 가입할 수 있습니다.
\n관리자 초대 가입 - 관리자만 사용자 페이지와 관리자/설정/사용자 페이지에서 초대할 수 있습니다.
\n신규 가입 불가 - 신규 가입이 불가능합니다.
", + "registration-approval-type.help": "일반 - 회원가입을 신청하는 즉시 가입됩니다.
\n관리자 승인 - 가입 승인 대기열에서 관리자의 승인이 있어야 가입이 완료됩니다.
\n관리자 IP 승인 - 등록된 계정이 존재하는 IP에서 가입을 신청할 경우 관리자의 승인이 필요하고, 처음 가입하는 IP에서는 관리자의 승인이 필요하지 않습니다.
", + "registration-queue-auto-approve-time": "자동 승인 시간", + "registration-queue-auto-approve-time-help": "지정한 시간 뒤에 사용자의 가입이 자동으로 승인됩니다. 0시간으로 지정할 경우 비활성화됩니다.", + "registration-queue-show-average-time": "가입 승인까지 평균적으로 걸리는 시간을 사용자에게 표시", + "registration.max-invites": "개인 별 최대 초대 횟수", + "max-invites": "개인 별 최대 초대 횟수", + "max-invites-help": "0으로 지정하면 제한이 없습니다. 관리자는 횟수제한이 없습니다.
\"초대 가입\" 설정에서만 적용됩니다.", + "invite-expiration": "초대장 유효 기간", + "invite-expiration-help": "설정한 기간(단위: 일)이 지나면 초대가 만료됩니다.", + "min-username-length": "사용자명 최소 길이", + "max-username-length": "사용자명 최대 길이", + "min-password-length": "비밀번호 최소 길이", + "min-password-strength": "비밀번호 최소 강도", + "max-about-me-length": "자기소개 최대 길이", + "terms-of-use": "이용약관(미입력 시 비활성화)", + "user-search": "사용자 검색", + "user-search-results-per-page": "표시할 결과 수", + "default-user-settings": "사용자 설정 기본값", + "show-email": "이메일 공개", + "show-fullname": "실명 공개", + "restrict-chat": "내가 팔로우하는 사용자로부터만 채팅 허용", + "outgoing-new-tab": "외부 링크를 새로운 탭에서 열람", + "topic-search": "화제 내 검색 허용", + "update-url-with-post-index": "화제를 보고 있을 때 포스트마다 url 업데이트", + "digest-freq": "포럼 메일 정기구독", + "digest-freq.off": "해제", + "digest-freq.daily": "매일", + "digest-freq.weekly": "매주", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "매달", + "email-chat-notifs": "오프라인일 때 채팅 메시지가 도착하면 알림 메일 보내기", + "email-post-notif": "내가 관심있는 화제에 답글이 달리면 메일 보내기", + "follow-created-topics": "내가 작성한 화제 팔로우", + "follow-replied-topics": "내가 답글을 작성한 화제 팔로우", + "default-notification-settings": "기본 알림 설정", + "categoryWatchState": "기본 카테고리 관심 상태", + "categoryWatchState.watching": "관심", + "categoryWatchState.notwatching": "관심 해제", + "categoryWatchState.ignoring": "무시" +} diff --git a/public/language/ko/admin/settings/web-crawler.json b/public/language/ko/admin/settings/web-crawler.json new file mode 100644 index 0000000000..85b0e7e20a --- /dev/null +++ b/public/language/ko/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "크롤링 설정", + "robots-txt": "사용자 지정 Robots.txt 기본값을 쓰시려면 비워두세요", + "sitemap-feed-settings": "사이트맵 & 피드 관리", + "disable-rss-feeds": "RSS 피드 비활성화", + "disable-sitemap-xml": "Sitemap.xml 비활성화", + "sitemap-topics": "사이트맵에 표시할 화제 수", + "clear-sitemap-cache": "사이트맵 캐시 삭제", + "view-sitemap": "사이트맵" +} \ No newline at end of file diff --git a/public/language/ko/category.json b/public/language/ko/category.json new file mode 100644 index 0000000000..b5e9a6ac3b --- /dev/null +++ b/public/language/ko/category.json @@ -0,0 +1,23 @@ +{ + "category": "카테고리", + "subcategories": "하위 카테고리", + "new_topic_button": "새로운 화제 생성", + "guest-login-post": "작성을 위해 로그인", + "no_topics": "이 카테고리에는 생성된 화제가 없습니다.
화제를 생성해 보세요.", + "browsing": "읽는 중", + "no_replies": "답글이 없습니다.", + "no_new_posts": "새로운 글이 없습니다.", + "watch": "관심 화제", + "ignore": "관심 해제", + "watching": "관심 카테고리", + "not-watching": "관심 해제 카테고리", + "ignoring": "카테고리 무시", + "watching.description": "읽지 않음과 최근 목록에 화제 표시", + "not-watching.description": "읽지 않음 제외, 최근 목록에만 화제 표시", + "ignoring.description": "읽지 않음과 최근 목록에서 화제 제외", + "watching.message": "이 카테고리 및 모든 하위 카테고리를 관심 등록했습니다.", + "notwatching.message": "이 카테고리 및 모든 하위 카테고리를 관심 해제했습니다.", + "ignoring.message": "이 카테고리 및 모든 하위 카테고리를 무시하고 있습니다", + "watched-categories": "관심 카테고리", + "x-more-categories": "%1 더 많은 카테고리" +} \ No newline at end of file diff --git a/public/language/ko/email.json b/public/language/ko/email.json new file mode 100644 index 0000000000..8209341327 --- /dev/null +++ b/public/language/ko/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "이메일 테스트", + "password-reset-requested": "비밀번호 재설정을 요청했습니다!", + "welcome-to": "%1에 오신 것을 환영합니다.", + "invite": "%1님이 초대하였습니다.", + "greeting_no_name": "안녕하세요", + "greeting_with_name": "안녕하세요 %1님", + "email.verify-your-email.subject": "사용자의 이메일을 인증해주세요.", + "email.verify.text1": "이메일 주소 변경 또는 확인을 요청했습니다.", + "email.verify.text2": "보안을 위해 이메일이 인증되어야만 변경이 가능합니다. 요청을 하지 않은 경우 사용자 측에서 수행할 작업이 없습니다.", + "email.verify.text3": "이 전자 메일 주소를 확인하면 현재 전자 메일 주소를 %1로 바꿉니다.", + "welcome.text1": "%1님 가입해주셔서 감사합니다.", + "welcome.text2": "계정을 활성화하려면 등록한 메일 주소의 인증이 필요합니다.", + "welcome.text3": "관리자에 의해 승인되었습니다. 이제 사용자명/비밀번호를 통해 로그인 하실 수 있습니다.", + "welcome.cta": "메일 주소를 확인하려면 여기를 클릭하세요.", + "invitation.text1": "%1님이 %2에 귀하를 초대하였습니다.", + "invitation.text2": "사용자의 초대장은 %1 일 후에 만료됩니다.", + "invitation.cta": "계정을 생성하려면 여기를 클릭하십시오.", + "reset.text1": "비밀번호 재설정 요청을 받았습니다. 비밀번호를 분실해서 요청한 것이 아니라면 이 메일을 무시하셔도 좋습니다.", + "reset.text2": "비밀번호를 재설정하려면 다음 링크를 클릭하세요.", + "reset.cta": "비밀번호를 재설정하려면 여기를 클릭하세요.", + "reset.notify.subject": "비밀번호가 성공적으로 변경되었습니다.", + "reset.notify.text1": "%1에 관해 통지합니다. 귀하의 비밀번호가 성공적으로 변경되었습니다.", + "reset.notify.text2": "만약 이 인증을 요청하지 않았다면 즉시 관리자에게 통보하시기 바랍니다.", + "digest.latest_topics": "%1의 최근 주제", + "digest.top-topics": "%1의 TOP 주제", + "digest.popular-topics": "%1의 인기 주제", + "digest.cta": "%1에 방문하시려면 클릭하세요.", + "digest.unsub.info": "이 포럼 메일은 사용자의 구독 설정에 따라 전송되었습니다.", + "digest.day": "일", + "digest.week": "주", + "digest.month": "월", + "digest.subject": "%1님을 위한 포럼 메일", + "digest.title.day": "일간 포럼 메일", + "digest.title.week": "주간 포럼 메일", + "digest.title.month": "월간 포럼 메일", + "notif.chat.subject": "%1님이 채팅 메시지를 보냈습니다.", + "notif.chat.cta": "채팅을 계속하려면 여기를 클릭하세요.", + "notif.chat.unsub.info": "이 채팅 알림은 사용자의 구독 설정에 따라 전송되었습니다.", + "notif.post.unsub.info": "이 포스트 알림은 사용자의 구독 설정에 따라 전송되었습니다.", + "notif.post.unsub.one-click": "이와 같은 메일의 구독을 해제하려면 여기를 클릭하세요.", + "notif.cta": "포럼으로", + "notif.cta-new-reply": "포스트 보기", + "notif.cta-new-chat": "채팅 보기", + "notif.test.short": "테스트 알림", + "notif.test.long": "이것은 알림 이메일의 테스트입니다. 관리자에게 알려주세요!", + "test.text1": "이 시험용 메일은 NodeBB에 설정된 메일 송신자가 정상적으로 메일을 송신할 수 있는지 시험할 목적으로 발송되었습니다.", + "unsub.cta": "설정을 변경하려면 여기를 클릭하세요.", + "unsubscribe": "구독 해제", + "unsub.success": "지금부터 %1 의 메일을 수신하지 않습니다.", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "사용자는 %1에서 차단되었습니다.", + "banned.text1": "사용자 %1는 %2에서 차단되었습니다.", + "banned.text2": "차단은 %1까지 유효합니다.", + "banned.text3": "사용자의 차단 사유는:", + "closing": "감사합니다!" +} \ No newline at end of file diff --git a/public/language/ko/error.json b/public/language/ko/error.json new file mode 100644 index 0000000000..dc5e0540b8 --- /dev/null +++ b/public/language/ko/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "올바르지 않은 정보입니다.", + "invalid-json": "올바르지 않은 JSON 형식입니다.", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "로그인하지 않았습니다.", + "account-locked": "계정이 임시 잠금 상태입니다.", + "search-requires-login": "검색을 위해 로그인이 필요합니다. 로그인하거나 가입해주세요.", + "goback": "이전 페이지로 돌아가려면 뒤로 가기 버튼을 누르세요.", + "invalid-cid": "올바르지 않은 카테고리 ID입니다.", + "invalid-tid": "올바르지 않은 화제 ID입니다.", + "invalid-pid": "올바르지 않은 포스트 ID입니다.", + "invalid-uid": "올바르지 않은 사용자 ID입니다.", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "유효한 날짜가 제공되어야 합니다.", + "invalid-username": "올바르지 않은 사용자명입니다.", + "invalid-email": "올바르지 않은 이메일입니다.", + "invalid-fullname": "올바르지 않은 이름입니다.", + "invalid-location": "올바르지 않은 위치입니다.", + "invalid-birthday": "올바르지 않은 생년월일입니다.", + "invalid-title": "올바르지 않은 제목입니다.", + "invalid-user-data": "올바르지 않은 사용자 정보입니다.", + "invalid-password": "올바르지 않은 비밀번호입니다.", + "invalid-login-credentials": "올바르지 않은 로그인 정보입니다.", + "invalid-username-or-password": "사용자명과 패스워드를 모두 설정해주세요.", + "invalid-search-term": "올바르지 않은 검색어입니다.", + "invalid-url": "올바르지 않은 URL 입니다.", + "invalid-event": "올바르지 않은 이벤트: %1", + "local-login-disabled": "권한이 없는 계정에서의 로컬 로그인이 비활성화 되었습니다.", + "csrf-invalid": "세션이 만료되어 로그인에 실패하였습니다. 다시 시도해주세요.", + "invalid-path": "올바르지 않은 경로입니다.", + "folder-exists": "폴더가 이미 존재합니다.", + "invalid-pagination-value": "올바르지 않은 페이지 값입니다. 최소 %1에서 최대 2% 사이로 설정해야 합니다.", + "username-taken": "이미 사용 중인 사용자명입니다.", + "email-taken": "이미 사용 중인 이메일입니다.", + "email-nochange": "입력한 전자 메일이 이미 등록되어 있는 전자 메일과 동일합니다.", + "email-invited": "해당 이메일의 사용자는 이미 초대되었습니다.", + "email-not-confirmed": "이메일 인증이 완료된 후 카테고리나 화제에 새로운 포스트를 작성할 수 있습니다. 여기를 눌러 인증 메일을 다시 발송할 수 있습니다.", + "email-not-confirmed-chat": "아직 이메일이 인증되지 않아 채팅 기능을 사용할 수 없습니다. 여기를 눌러 이메일 인증을 진행하세요.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "이메일 인증이 실패하였습니다. 잠시 후에 다시 시도하세요.", + "confirm-email-already-sent": "인증 메일이 이미 발송되었습니다. 다시 보내려면 %1분을 기다리세요.", + "sendmail-not-found": "Sendmail 실행파일을 찾을 수 없었습니다. 관리자가 sendmail을 설치했고 실행이 가능한 상태인지 확인해 주시기 바랍니다.", + "digest-not-enabled": "사용자가 다이제스트를 비활성화했거나 시스템 기본값이 다이제스트를 보내도록 활성화되어있지 않습니다.", + "username-too-short": "사용자명이 너무 짧습니다.", + "username-too-long": "사용자명이 너무 깁니다.", + "password-too-long": "비밀번호가 너무 깁니다.", + "reset-rate-limited": "비밀번호 초기화를 너무 자주 시도하셨습니다. (한도 초과)", + "reset-same-password": "현재의 비밀번호와 다른 비밀번호를 입력해주세요.", + "user-banned": "차단된 사용자입니다.", + "user-banned-reason": "죄송합니다. 해당 계정은 차단되었습니다. (사유: %1)", + "user-banned-reason-until": "죄송합니다. 해당 계정은 %1까지 차단되었습니다. (사유: %2)", + "user-too-new": "죄송합니다. 첫 번째 게시물은 %1초 후에 작성할 수 있습니다.", + "blacklisted-ip": "죄송합니다. 당신의 IP는 이 커뮤니티로부터 차단되었습니다. 만약 오류라고 생각되시면 관리자에게 연락해주세요.", + "ban-expiry-missing": "해당 차단의 만료일을 설정해주세요.", + "no-category": "존재하지 않는 카테고리입니다.", + "no-topic": "존재하지 않는 화제입니다.", + "no-post": "존재하지 않는 포스트입니다.", + "no-group": "존재하지 않는 그룹입니다.", + "no-user": "존재하지 않는 사용자입니다.", + "no-teaser": "존재하지 않는 미리보기입니다.", + "no-flag": "Flag does not exist", + "no-privileges": "이 작업을 할 수 있는 권한이 없습니다.", + "category-disabled": "카테고리가 비활성화 되었습니다.", + "topic-locked": "게시물이 잠금 상태입니다.", + "post-edit-duration-expired": "포스트의 수정은 작성한 시간으로부터 %1초 후에 가능합니다.", + "post-edit-duration-expired-minutes": "포스트의 수정은 작성한 시간으로부터 %1분 후에 가능합니다.", + "post-edit-duration-expired-minutes-seconds": "포스트의 수정은 작성한 시간으로부터 %1분 %2초 후에 가능합니다.", + "post-edit-duration-expired-hours": "포스트의 수정은 작성한 시간으로부터 %1시간 후에 가능합니다.", + "post-edit-duration-expired-hours-minutes": "포스트의 수정은 작성한 시간으로부터 %1시간 %2분 후에 가능합니다.", + "post-edit-duration-expired-days": "포스트의 수정은 작성한 시간으로부터 %1일 후에 가능합니다.", + "post-edit-duration-expired-days-hours": "포스트의 수정은 작성한 시간으로부터 %1일 %2시간 후에 가능합니다.", + "post-delete-duration-expired": "포스트의 삭제는 작성한 시간으로부터 %1초 후에 가능합니다.", + "post-delete-duration-expired-minutes": "포스트의 삭제는 작성한 시간으로부터 %1분 후에 가능합니다.", + "post-delete-duration-expired-minutes-seconds": "포스트의 삭제는 작성한 시간으로부터 %1분 %2초 후에 가능합니다.", + "post-delete-duration-expired-hours": "포스트의 삭제는 작성한 시간으로부터 %1시간 후에 가능합니다.", + "post-delete-duration-expired-hours-minutes": "포스트의 삭제는 작성한 시간으로부터 %1시간 %2분 후에 가능합니다.", + "post-delete-duration-expired-days": "포스트의 삭제는 작성한 시간으로부터 %1일 후에 가능합니다.", + "post-delete-duration-expired-days-hours": "포스트의 삭제는 작성한 시간으로부터 %1일 %2시간 후에 가능합니다.", + "cant-delete-topic-has-reply": "답글이 달린 화제는 삭제하실 수 없습니다.", + "cant-delete-topic-has-replies": "답글이 %1개 이상 달린 화제는 삭제하실 수 없습니다.", + "content-too-short": "포스트의 내용이 너무 짧습니다. 내용은 최소 %1자 이상이어야 합니다.", + "content-too-long": "포스트의 내용이 너무 깁니다. 내용은 최대 %1자 이내로 작성할 수 있습니다.", + "title-too-short": "제목이 너무 짧습니다. 제목은 최소 %1자 이상이어야 합니다.", + "title-too-long": "제목이 너무 깁니다. 제목은 최대 %1자 이내로 작성할 수 있습니다.", + "category-not-selected": "선택된 카테고리가 없습니다.", + "too-many-posts": "새 게시물 작성은 %1초마다 가능합니다. 조금 천천히 작성해주세요.", + "too-many-posts-newbie": "신규 사용자는 %2만큼의 인지도를 얻기 전까지 %1초마다 게시물을 작성할 수 있습니다. 조금 천천히 작성해주세요.", + "already-posting": "You are already posting", + "tag-too-short": "태그가 너무 짧습니다. 태그는 최소 %1자 이상이어야 합니다.", + "tag-too-long": "태그가 너무 깁니다. 태그는 최대 %1자 이내로 사용 가능합니다.", + "not-enough-tags": "태그가 없거나 부족합니다. 게시물은 %1개 이상의 태그를 사용해야 합니다.", + "too-many-tags": "태그가 너무 많습니다. 게시물은 %1개 이하의 태그를 사용할 수 있습니다.", + "cant-use-system-tag": "관리자용 태그를 사용하실 수 없습니다.", + "cant-remove-system-tag": "이 시스템 태그를 제거할 수 없습니다.", + "still-uploading": "업로드가 끝날 때까지 기다려주세요.", + "file-too-big": "업로드 가능한 파일크기는 최대 %1 KB 입니다. 파일의 용량을 줄이거나 압축을 활용하세요.", + "guest-upload-disabled": "비회원의 파일 업로드는 제한되어 있습니다.", + "cors-error": "잘못 구성된 CORS로 인해 이미지를 업로드 할 수 없습니다.", + "upload-ratelimit-reached": "한 번에 너무 많은 파일을 업로드하셨습니다. 나중에 다시 시도해주세요.", + "scheduling-to-past": "내일 이후의 날짜를 선택해주세요.", + "invalid-schedule-date": "적합한 형식의 날짜와 시간을 입력해주세요.", + "cant-pin-scheduled": "예약된 화제는 상단에 고정(해제)할 수 없습니다.", + "cant-merge-scheduled": "예약된 화제는 병합할 수 없습니다.", + "cant-move-posts-to-scheduled": "예약된 화제로는 포스트를 옮길 수 없습니다.", + "cant-move-from-scheduled-to-existing": "예약된 화제의 포스트는 옮길 수 없습니다.", + "already-bookmarked": "이미 즐겨찾기에 추가한 포스트 입니다.", + "already-unbookmarked": "이미 즐겨찾기를 해제한 포스트 입니다.", + "cant-ban-other-admins": "다른 관리자를 차단할 수 없습니다!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "당신은 유일한 관리자입니다. 관리자를 그만두기 전에 다른 사용자를 관리자로 임명하세요.", + "account-deletion-disabled": "계정 삭제 기능이 비활성화 상태입니다.", + "cant-delete-admin": "해당 계정을 삭제하기 전에 관리자 권한을 해제해주십시오.", + "already-deleting": "이미 삭제 중입니다.", + "invalid-image": "올바르지 않은 이미지입니다.", + "invalid-image-type": "올바르지 않은 이미지입니다. 사용가능한 유형: %1", + "invalid-image-extension": "올바르지 않은 이미지 확장자입니다.", + "invalid-file-type": "올바르지 않은 파일 유형입니다. 사용가능한 유형: %1", + "invalid-image-dimensions": "이미지 크기가 너무 큽니다.", + "group-name-too-short": "그룹 이름이 너무 짧습니다.", + "group-name-too-long": "그룹 이름이 너무 깁니다.", + "group-already-exists": "이미 존재하는 그룹입니다.", + "group-name-change-not-allowed": "그룹 이름의 변경이 불가능합니다.", + "group-already-member": "이미 이 그룹에 속해있습니다.", + "group-not-member": "이 그룹의 멤버가 아닙니다.", + "group-needs-owner": "이 그룹은 적어도 한 명의 소유자가 필요합니다.", + "group-already-invited": "이 사용자는 이미 초대됐습니다.", + "group-already-requested": "가입 요청이 이미 제출되었습니다.", + "group-join-disabled": "현재 이 그룹에 가입할 수 없습니다.", + "group-leave-disabled": "현재 이 그룹을 떠날 수 없습니다.", + "post-already-deleted": "이미 삭제된 포스트입니다.", + "post-already-restored": "이미 복원된 포스트입니다.", + "topic-already-deleted": "이미 삭제된 화제입니다.", + "topic-already-restored": "이미 복원된 화제입니다.", + "cant-purge-main-post": "메인 포스트는 삭제할 수 없습니다. 대신 포스트를 삭제하세요.", + "topic-thumbnails-are-disabled": "화제 썸네일이 비활성화 되었습니다.", + "invalid-file": "올바르지 않은 파일입니다.", + "uploads-are-disabled": "업로드가 비활성화 되었습니다.", + "signature-too-long": "서명은 %1자를 넘길 수 없습니다.", + "about-me-too-long": "자기소개는 %1자를 넘길 수 없습니다.", + "cant-chat-with-yourself": "자신과는 채팅할 수 없습니다!", + "chat-restricted": "이 사용자는 채팅을 제한하고 있습니다. 채팅하려면 해당 사용자가 당신을 팔로우해야 합니다.", + "chat-disabled": "채팅 시스템이 비활성화 되었습니다.", + "too-many-messages": "짧은 시간동안 너무 많은 메시지를 전송하였습니다. 잠시 후에 다시 시도하세요.", + "invalid-chat-message": "올바르지 않은 메시지입니다.", + "chat-message-too-long": "채팅 메세지는 최대 %1자로 제한됩니다.", + "cant-edit-chat-message": "이 메세지를 수정 할 권한이 없습니다.", + "cant-delete-chat-message": "이 메세지를 삭제할 권한이 없습니다.", + "chat-edit-duration-expired": "채팅 메시지를 게시한 뒤 %1초 뒤부터 메시지를 수정할 수 있습니다.", + "chat-delete-duration-expired": "채팅 메시지를 게시한 뒤 %1초 뒤부터 삭제가 가능합니다.", + "chat-deleted-already": "이미 삭제된 채팅 메시지입니다.", + "chat-restored-already": "이 채팅 메시지는 이미 복원되었습니다.", + "chat-room-does-not-exist": "채팅이 존재하지 않습니다.", + "already-voting-for-this-post": "이미 이 포스트에 투표하셨습니다.", + "reputation-system-disabled": "인지도 시스템이 비활성화되어있습니다.", + "downvoting-disabled": "비추천 기능이 비활성 상태입니다.", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "오직 1%", + "not-enough-reputation-to-flag": "이 포스트를 플래그하려면 1%가 더 필요합니다.", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "이미 해당 게시물을 신고했습니다.", + "user-already-flagged": "이미 해당 사용자를 신고했습니다.", + "post-flagged-too-many-times": "해당 게시물은 다른 사용자에 의해 신고되었습니다.", + "user-flagged-too-many-times": "해당 사용자는 다른 사용자에 의해 신고되었습니다.", + "cant-flag-privileged": "관리자를 신고할 수 없습니다. (조정자/통합 조정자/관리자)", + "self-vote": "자신의 게시물에는 투표할 수 없습니다.", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "비추천은 하루에 %1회만 가능합니다.", + "too-many-downvotes-today-user": "사용자 비추천은 하루에 %1회만 가능합니다.", + "reload-failed": "NodeBB 서버를 다시 읽어들이는 중 다음과 같은 문제가 발생했습니다. 오류 문구: \\\"%1\\\" NodeBB에서는 클라이언트 측 자원을 지속적으로 제공하지만, 문제를 해결하시려면 다시 읽어들이기 전의 수정사항을 원래대로 되돌려주세요.", + "registration-error": "등록 오류", + "parse-error": "서버에서의 응답을 읽는 동안 문제가 발생했습니다.", + "wrong-login-type-email": "이메일 주소를 통해 로그인하세요.", + "wrong-login-type-username": "사용자명을 통해 로그인하세요.", + "sso-registration-disabled": "%1 계정의 가입이 비활성화되었습니다. 이메일 주소로 먼저 가입하세요.", + "sso-multiple-association": "같은 종류의 계정을 여러 개 연동할 수 없습니다. 기존에 연동한 계정의 연동을 해제해주세요.", + "invite-maximum-met": "초대할 수 있는 사용자 수의 한도에 도달했습니다. (%2명 중 %1을 초대)", + "no-session-found": "로그인 세션을 찾을 수 없습니다.", + "not-in-room": "채팅방에 사용자 없음", + "cant-kick-self": "스스로 이 그룹을 탈퇴할 수 없습니다.", + "no-users-selected": "선택된 사용자가 없습니다.", + "invalid-home-page-route": "올바르지 않은 홈페이지 경로입니다.", + "invalid-session": "세션 오류", + "invalid-session-text": "로그인 세션이 종료됐습니다. 페이지를 새로고침 해주세요.", + "session-mismatch": "세션 불일치", + "session-mismatch-text": "로그인 세션이 서버와 일치하지 않습니다. 페이지를 새로고침 해주세요.", + "no-topics-selected": "선택된 화제가 없습니다!", + "cant-move-to-same-topic": "동일한 화제로 포스트를 이동할 수 없습니다!", + "cant-move-topic-to-same-category": "동일한 카테고리로 화제를 이동할 수 없습니다!", + "cannot-block-self": "자신을 차단할 수 없습니다!", + "cannot-block-privileged": "관리자나 통합 조정자는 차단할 수 없습니다!", + "cannot-block-guest": "비회원은 다른 사용자를 차단할 수 없습니다!", + "already-blocked": "이 사용자는 이미 차단되었습니다.", + "already-unblocked": "이 사용자는 이미 차단 해제되었습니다.", + "no-connection": "사용자의 인터넷 연결에 문제가 있는 것 같습니다.", + "socket-reconnect-failed": "현재 서버에 접속할 수 없습니다. 여기를 눌러 다시 시도하거나 나중에 다시 시도해주세요.", + "plugin-not-whitelisted": "플러그인을 설치할 수 없습니다. – ACP에서는 NodeBB 패키지 관리자에 의해 승인된 플러그인만 설치할 수 있습니다.", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "화제 이벤트 '%1'를 인식할 수 없습니다.", + "cant-set-child-as-parent": "하위 카테고리를 상위로 등록할 수 없습니다.", + "cant-set-self-as-parent": "같은 카테고리를 상위로 등록할 수 없습니다.", + "api.master-token-no-uid": "요청 본문에 해당하는 `_uid` 없이 마스터 토큰이 수신되었습니다.", + "api.400": "당신이 전달한 요청 페이로드에 문제가 있습니다.", + "api.401": "올바른 로그인 세션을 찾을 수 없습니다. 로그인한 후 다시 시도하십시오.", + "api.403": "호출 할 수 있는 권한이 없습니다.", + "api.404": "잘못된 API 호출", + "api.426": "API를 쓰기 위한 요청에 HTTPS가 필요합니다. HTTPS를 통해 요청을 다시 보내십시오.", + "api.429": "요청이 너무 많습니다. 나중에 다시 시도하십시오.", + "api.500": "요청을 처리하는 동안 예기치 않은 오류가 발생했습니다.", + "api.501": "호출하려는 경로가 아직 구현되지 않았습니다. 내일 다시 시도하십시오.", + "api.503": "서버 구성으로 인해 호출하려는 경로를 현재 사용할 수 없습니다." +} \ No newline at end of file diff --git a/public/language/ko/flags.json b/public/language/ko/flags.json new file mode 100644 index 0000000000..0db0148ada --- /dev/null +++ b/public/language/ko/flags.json @@ -0,0 +1,89 @@ +{ + "state": "처리 상태", + "reports": "보고", + "first-reported": "최초 보고", + "no-flags": "만세! 들어온 신고가 없습니다.", + "assignee": "담당자", + "update": "업데이트", + "updated": "업데이트 완료", + "resolved": "해결됨", + "target-purged": "해당 신고된 컨텐츠는 완전 삭제 되었으며, 더 이상 존재하지 않습니다.", + + "graph-label": "일일 신고", + "quick-filters": "간편 필터", + "filter-active": "해당 신고 목록에 하나 이상의 필터가 적용되었습니다.", + "filter-reset": "필터 제거", + "filters": "필터 옵션", + "filter-reporterId": "신고자 ID", + "filter-targetUid": "신고된 글 ID", + "filter-type": "신고 유형", + "filter-type-all": "모든 컨텐츠", + "filter-type-post": "포스트", + "filter-type-user": "사용자", + "filter-state": "처리 상태", + "filter-assignee": "담당자 ID", + "filter-cid": "카테고리", + "filter-quick-mine": "나에게 배정된 신고", + "filter-cid-all": "모든 카테고리", + "apply-filters": "필터 적용", + "more-filters": "더 많은 필터", + "fewer-filters": "기본 필터", + + "quick-actions": "빠른 신고", + "flagged-user": "신고된 사용자", + "view-profile": "프로필 보기", + "start-new-chat": "새로운 채팅 시작", + "go-to-target": "신고된 글 바로가기", + "assign-to-me": "나에게 할당", + "delete-post": "포스트 삭제", + "purge-post": "포스트 완전 삭제", + "restore-post": "포스트 복원", + "delete": "Delete Flag", + + "user-view": "프로필 보기", + "user-edit": "프로필 수정", + + "notes": "관리자 메모", + "add-note": "메모 추가", + "no-notes": "공유된 메모가 없습니다.", + "delete-note-confirm": "이 메모를 삭제하시겠습니까?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "메모 추가됨", + "note-deleted": "메모 삭제됨", + "flag-deleted": "Flag Deleted", + + "history": "계정 & 신고 기록", + "no-history": "신고 기록이 없습니다.", + + "state-all": "모든 상태", + "state-open": "새로운 신고", + "state-wip": "처리중", + "state-resolved": "처리됨", + "state-rejected": "거절됨", + "no-assignee": "담당자 미정", + + "sort": "분류", + "sort-newest": "최신순", + "sort-oldest": "오래된순", + "sort-reports": "신고순", + "sort-all": "모든 신고 유형...", + "sort-posts-only": "포스트만...", + "sort-downvotes": "비추천순", + "sort-upvotes": "추천순", + "sort-replies": "답글순", + + "modal-title": "신고 사유", + "modal-body": "%1 %2 에 대한 신고 사유를 적어주시거나, 빠른 신고 버튼 중 하나를 사용해 주세요.", + "modal-reason-spam": "스팸", + "modal-reason-offensive": "부적절한 글", + "modal-reason-other": "기타 (아래에 작성)", + "modal-reason-custom": "신고 사유", + "modal-submit": "신고 제출", + "modal-submit-success": "이 컨텐츠는 신고되었습니다.", + + "bulk-actions": "대량 작업", + "bulk-resolve": "해결된 신고", + "bulk-success": "%1 신고 업데이트됨", + "flagged-timeago-readable": "신고됨 (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/ko/global.json b/public/language/ko/global.json new file mode 100644 index 0000000000..efe647adcb --- /dev/null +++ b/public/language/ko/global.json @@ -0,0 +1,126 @@ +{ + "home": "홈", + "search": "검색", + "buttons.close": "닫기", + "403.title": "접근이 거부되었습니다.", + "403.message": "권한이 없는 페이지에 접속을 시도하였습니다.", + "403.login": "로그인 되어 있는지 확인해주세요.", + "404.title": "페이지를 찾을 수 없습니다.", + "404.message": "존재하지 않는 페이지에 접근하였습니다. 홈 페이지로 이동합니다.", + "500.title": "내부 오류", + "500.message": "이런! 알 수 없는 오류가 발생했습니다!", + "400.title": "잘못된 요청", + "400.message": "해당 링크는 잘못된 형식입니다. 다시 한번 확인 후 시도해 주십시오. 아니면 홈페이지로 이동합니다. ", + "register": "회원가입", + "login": "로그인", + "please_log_in": "로그인 해주세요.", + "logout": "로그아웃", + "posting_restriction_info": "현재 회원들만 작성할 수 있습니다. 여기를 누르면 로그인 페이지로 이동합니다.", + "welcome_back": "환영합니다.", + "you_have_successfully_logged_in": "성공적으로 로그인했습니다.", + "save_changes": "변경사항 저장", + "save": "저장", + "close": "닫기", + "pagination": "페이지", + "pagination.out_of": "현재: %1 / 전체: %2", + "pagination.enter_index": "포스트 인덱스로", + "header.admin": "관리자", + "header.categories": "카테고리", + "header.recent": "최근", + "header.unread": "읽지 않음", + "header.tags": "태그", + "header.popular": "인기", + "header.top": "TOP", + "header.users": "사용자", + "header.groups": "그룹", + "header.chats": "채팅", + "header.notifications": "알림", + "header.search": "검색", + "header.profile": "프로필", + "header.navigation": "바로가기", + "notifications.loading": "알림을 불러오는 중입니다.", + "chats.loading": "대화를 불러오는 중입니다.", + "motd.welcome": "NodeBB에 오신 것을 환영합니다.", + "previouspage": "이전 페이지", + "nextpage": "다음 페이지", + "alert.success": "성공", + "alert.error": "오류", + "alert.banned": "차단됨", + "alert.banned.message": "당신은 차단되었습니다. 당신의 접근이 제한됩니다.", + "alert.unbanned": "차단 해제", + "alert.unbanned.message": "당신의 차단이 해제되었습니다.", + "alert.unfollow": "더 이상 %1님을 팔로우하지 않습니다!", + "alert.follow": "%1님을 팔로우 합니다!", + "users": "사용자", + "topics": "화제", + "posts": "포스트", + "x-posts": "%1 포스트", + "best": "베스트", + "controversial": "Controversial", + "votes": "투표", + "x-votes": "%1 투표", + "voters": "투표자", + "upvoters": "추천한 사용자", + "upvoted": "추천된 게시물", + "downvoters": "비추천한 사용자", + "downvoted": "비추천된 게시물", + "views": "조회수", + "posters": "게시자", + "reputation": "인지도", + "lastpost": "최근 포스트", + "firstpost": "첫 포스트", + "read_more": "더 보기", + "more": "더 보기", + "none": "없음", + "posted_ago_by_guest": "비회원이 %1에 작성했습니다.", + "posted_ago_by": "%2님이 %1에 작성했습니다.", + "posted_ago": "%1에 작성되었습니다.", + "posted_in": "%1에 작성되었습니다.", + "posted_in_by": "%2님이 %1에 작성했습니다.", + "posted_in_ago": "%2 %1에 작성되었습니다. ", + "posted_in_ago_by": "%3님이 %2 %1에 작성했습니다.", + "user_posted_ago": "%1님이 %2에 작성했습니다.", + "guest_posted_ago": "비회원이 %1에 작성했습니다.", + "last_edited_by": "%1님이 마지막으로 수정했습니다.", + "norecentposts": "최근 작성된 포스트가 없습니다.", + "norecenttopics": "최근 작성된 화제가 없습니다.", + "recentposts": "최근 포스트", + "recentips": "최근 접속 IP", + "moderator_tools": "조정 도구", + "online": "온라인", + "away": "자리 비움", + "dnd": "방해 금지", + "invisible": "오프라인으로 표시", + "offline": "오프라인", + "email": "이메일", + "language": "언어", + "guest": "비회원", + "guests": "비회원", + "former_user": "이전 사용자", + "system-user": "시스템", + "unknown-user": "알 수 없는 사용자", + "updated.title": "포럼 업데이트 완료", + "updated.message": "이 포럼은 지금 최신 버전으로 업데이트되었습니다. 페이지를 새로고침하시려면 여기를 클릭해주세요.", + "privacy": "개인정보", + "follow": "팔로우", + "unfollow": "언팔로우", + "delete_all": "모두 삭제하기", + "map": "맵", + "sessions": "로그인 세션", + "ip_address": "IP 주소", + "enter_page_number": "페이지 번호 입력", + "upload_file": "파일 업로드", + "upload": "업로드", + "uploads": "업로드", + "allowed-file-types": "사용가능한 파일 유형: %1", + "unsaved-changes": "저장되지 않은 변경사항이 있습니다. 저장하지 않고 페이지를 떠나시겠습니까?", + "reconnecting-message": "%1 사이트로의 연결이 끊어졌습니다. 다시 연결을 시도하는동안 잠시만 기다려 주십시오.", + "play": "재생", + "cookies.message": "이 웹사이트는 최적의 사용환경을 위해 쿠키를 활용합니다.", + "cookies.accept": "알겠습니다!", + "cookies.learn_more": "더 보기", + "edited": "수정되었습니다.", + "disabled": "비활성화", + "select": "선택", + "user-search-prompt": "사용자를 찾기 위해 여기에 검색어를 입력하십시오..." +} \ No newline at end of file diff --git a/public/language/ko/groups.json b/public/language/ko/groups.json new file mode 100644 index 0000000000..71a9ffb9f4 --- /dev/null +++ b/public/language/ko/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "그룹", + "view_group": "그룹 보기", + "owner": "그룹 관리자", + "new_group": "새로운 그룹 생성", + "no_groups_found": "그룹이 없습니다.", + "pending.accept": "수락", + "pending.reject": "거절", + "pending.accept_all": "전체 수락", + "pending.reject_all": "전체 거절", + "pending.none": "지금은 승인 대기 중인 회원이 없습니다.", + "invited.none": "지금은 초대된 회원이 없습니다.", + "invited.uninvite": "초대 취소", + "invited.search": "그룹에 초대할 사용자 검색", + "invited.notification_title": "%1 그룹에 초대되었습니다.", + "request.notification_title": "%1님으로부터 그룹 가입 요청이 들어왔습니다.", + "request.notification_text": "%1님이 %2에 가입을 신청했습니다.", + "cover-save": "저장", + "cover-saving": "저장 중", + "details.title": "그룹 상세정보", + "details.members": "멤버 목록", + "details.pending": "승인 대기 중인 멤버", + "details.invited": "초대된 멤버", + "details.has_no_posts": "이 그룹의 멤버가 작성한 글이 없습니다.", + "details.latest_posts": "최근 포스트", + "details.private": "비공개", + "details.disableJoinRequests": "가입 신청 비활성화", + "details.disableLeave": "그룹 탈퇴 비활성화", + "details.grant": "소유권 이전/포기", + "details.kick": "내보내기", + "details.kick_confirm": "이 멤버를 그룹에서 제외하시겠습니까?", + "details.add-member": "멤버 추가", + "details.owner_options": "그룹 관리", + "details.group_name": "그룹 이름", + "details.member_count": "인원", + "details.creation_date": "생성일", + "details.description": "설명", + "details.member-post-cids": "글을 보여줄 카테고리 ID", + "details.badge_preview": "뱃지 미리보기", + "details.change_icon": "아이콘 변경", + "details.change_label_colour": "라벨 색상 변경", + "details.change_text_colour": "텍스트 색상 변경", + "details.badge_text": "뱃지 문구", + "details.userTitleEnabled": "뱃지 보이기", + "details.private_help": "활성화하면 멤버 가입 시 그룹 관리자의 승인이 필요합니다.", + "details.hidden": "숨김", + "details.hidden_help": "활성화하면 그룹 목록에 노출되지 않습니다. 또한 멤버는 초대를 통해서만 가입이 가능합니다.", + "details.delete_group": "그룹 삭제", + "details.private_system_help": "비공개 그룹은 시스템에 의해 비활성화 되었으며, 이 옵션은 아무 기능도 하지 않습니다", + "event.updated": "그룹 정보가 업데이트되었습니다.", + "event.deleted": "%1 그룹이 삭제되었습니다.", + "membership.accept-invitation": "초대 수락", + "membership.accept.notification_title": "사용자는 이제 %1의 멤버입니다.", + "membership.invitation-pending": "보류중인 초대 수락", + "membership.join-group": "그룹 가입", + "membership.leave-group": "그룹 탈퇴", + "membership.leave.notification_title": "%1%2 그룹에서 탈퇴했습니다.", + "membership.reject": "거절", + "new-group.group_name": "그룹 이름:", + "upload-group-cover": "그룹 커버 사진 업로드", + "bulk-invite-instructions": "초대하고자 하는 사용자 목록을 콤마(,)로 구분하여 입력해주세요.", + "bulk-invite": "여러 명의 사용자 초대", + "remove_group_cover_confirm": "해당 커버 사진을 제거하시겠습니까?" +} \ No newline at end of file diff --git a/public/language/ko/ip-blacklist.json b/public/language/ko/ip-blacklist.json new file mode 100644 index 0000000000..10a5c9606a --- /dev/null +++ b/public/language/ko/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "여기에 IP 차단 목록을 구성하세요.", + "description": "가끔 사용자 계정 차단으로는 부족할 때가 있습니다. 그럴 경우에는 특정 IP 또는 IP 범위로 접근을 차단하는 것이 포럼을 보호하는 가장 좋은 방법입니다. 문제가 되는 IP 주소들이나 CIDR 블록 전체를 블랙리스트에 추가하면 로그인하거나 새 계정을 등록할 수 없습니다.", + "active-rules": "사용 중인 규칙", + "validate": "블랙리스트 확인", + "apply": "블랙리스트 적용", + "hints": "구문 힌트", + "hint-1": "각 줄마다 IP 주소 하나를 정의하십시오. IP 블록을 CIDR 형식 (예: 192.168.100.0/22)을 따르는 한 추가 할 수 있습니다. ", + "hint-2": "#기호로 행을 시작하여 주석을 추가 할 수 있습니다.", + + "validate.x-valid": "%2 룰(들) 중 %1 유효함.", + "validate.x-invalid": "다음 %1 규칙들은 유효하지 않습니다:", + + "alerts.applied-success": "블랙리스트 적용됨", + + "analytics.blacklist-hourly": "그래프 1 – 시간 당 블랙리스트 방문 횟수", + "analytics.blacklist-daily": "그래프 2 – 일간 블랙리스트 방문 횟수", + "ip-banned": "IP 차단됨" +} \ No newline at end of file diff --git a/public/language/ko/language.json b/public/language/ko/language.json new file mode 100644 index 0000000000..4e3a5a6ef7 --- /dev/null +++ b/public/language/ko/language.json @@ -0,0 +1,5 @@ +{ + "name": "한국어 (대한민국)", + "code": "ko", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/ko/login.json b/public/language/ko/login.json new file mode 100644 index 0000000000..4a3f9a875b --- /dev/null +++ b/public/language/ko/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "사용자명 / 이메일", + "username": "사용자명", + "remember_me": "로그인 유지", + "forgot_password": "비밀번호 초기화", + "alternative_logins": "다른 방법으로 로그인", + "failed_login_attempt": "로그인 실패", + "login_successful": "성공적으로 로그인했습니다.", + "dont_have_account": "계정이 없으신가요?", + "logged-out-due-to-inactivity": "일정시간 활동하지 않아 관리자 제어판에서 로그아웃 되었습니다.", + "caps-lock-enabled": "Caps Lock 활성화" +} \ No newline at end of file diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json new file mode 100644 index 0000000000..b62472aa1f --- /dev/null +++ b/public/language/ko/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "채팅", + "chat.placeholder": "여기에 메시지를 쓰고, 이미지를 드래그 앤 드롭하고, Enter를 눌러 보내세요!", + "chat.scroll-up-alert": "오래된 메시지를 보고 있습니다. 여기를 눌러 최신 메시지로 이동하세요.", + "chat.send": "전송", + "chat.no_active": "활성화된 채팅이 없습니다.", + "chat.user_typing": "%1님이 입력 중...", + "chat.user_has_messaged_you": "%1님이 메시지를 보냈습니다.", + "chat.see_all": "모든 채팅", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "채팅 기록을 보려면 채팅 상대를 선택하세요.", + "chat.no-users-in-room": "채팅방에 사용자 없음", + "chat.recent-chats": "최근 채팅", + "chat.contacts": "연락처", + "chat.message-history": "채팅 기록", + "chat.message-deleted": "메시지 삭제됨", + "chat.options": "채팅 옵션", + "chat.pop-out": "채팅 팝업", + "chat.minimize": "최소화", + "chat.maximize": "최대화", + "chat.seven_days": "7일", + "chat.thirty_days": "30일", + "chat.three_months": "3개월", + "chat.delete_message_confirm": "이 메세지를 삭제하시겠습니까?", + "chat.retrieving-users": "사용자 불러오는 중...", + "chat.manage-room": "채팅 관리", + "chat.add-user-help": "여기에서 사용자를 검색하세요. 선택한 사용자를 채팅에 초대합니다. 새로운 사용자는 이전에 주고받은 채팅을 확인할 수 없습니다. 채팅 관리자들()만 사용자를 채팅방에서 추방할 수 있습니다.", + "chat.confirm-chat-with-dnd-user": "이 사용자는 자신의 상태를 방해 금지로 설정했습니다. 그래도 대화를 요청하시겠습니까?", + "chat.rename-room": "채팅방 이름 변경", + "chat.rename-placeholder": "여기에 채팅방 이름을 입력하세요.", + "chat.rename-help": "여기에서 설정된 채팅방 이름은 모든 참여자들에게 보여집니다.", + "chat.leave": "채팅방 나가기", + "chat.leave-prompt": "정말로 이 채팅에서 나가시겠어요?", + "chat.leave-help": "이 채팅방에서 퇴장하면 더 이상 이 채팅방의 메시지를 수신할 수 없습니다. 나중에 다시 입장하게 되더라도 퇴장한 동안의 메시지는 확인할 수 없습니다.", + "chat.in-room": "채팅 참여자", + "chat.kick": "추방", + "chat.show-ip": "IP 보이기", + "chat.owner": "채팅 관리자", + "chat.system.user-join": "%1님이 입장하셨습니다.", + "chat.system.user-leave": "%1님이 퇴장하셨습니다.", + "chat.system.room-rename": "%2님의 채팅방 이름 변경: %1", + "composer.compose": "작성", + "composer.show_preview": "미리보기", + "composer.hide_preview": "미리보기 숨김", + "composer.user_said_in": "%1님이 %2에서 보낸 메세지:", + "composer.user_said": "%1님의 메세지:", + "composer.discard": "정말 이 포스트를 삭제하시겠습니까?", + "composer.submit_and_lock": "게시 후 잠금", + "composer.toggle_dropdown": "내려서 확인하기", + "composer.uploading": "%1 업로드 중", + "composer.formatting.bold": "굵게", + "composer.formatting.italic": "기울임", + "composer.formatting.list": "목록", + "composer.formatting.strikethrough": "취소선", + "composer.formatting.code": "코드", + "composer.formatting.link": "링크", + "composer.formatting.picture": "이미지 링크", + "composer.upload-picture": "이미지 업로드", + "composer.upload-file": "파일 업로드", + "composer.zen_mode": "전체화면", + "composer.select_category": "카테고리 선택", + "composer.textarea.placeholder": "포스트의 내용을 입력하세요. 드래그&드롭으로 이미지를 추가할 수 있습니다.", + "composer.schedule-for": "화제 예약", + "composer.schedule-date": "날짜", + "composer.schedule-time": "시간", + "composer.cancel-scheduling": "예약 취소", + "composer.set-schedule-date": "날짜 설정", + "bootbox.ok": "확인", + "bootbox.cancel": "취소", + "bootbox.confirm": "확인", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "커버 사진 위치변경", + "cover.dragging_message": "원하는 위치로 커버 사진을 드래그한 후 \"저장\" 버튼을 클릭하세요.", + "cover.saved": "커버 사진을 저장하였습니다.", + "thumbs.modal.title": "화제 썸네일 설정", + "thumbs.modal.no-thumbs": "썸네일 없음", + "thumbs.modal.resize-note": "참고: 이 포럼에서는 너비 %1px 이상의 이미지의 크기를 조정합니다.", + "thumbs.modal.add": "썸네일 추가", + "thumbs.modal.remove": "썸네일 제거", + "thumbs.modal.confirm-remove": "정말 이 썸네일을 제거하시겠습니까?" +} \ No newline at end of file diff --git a/public/language/ko/notifications.json b/public/language/ko/notifications.json new file mode 100644 index 0000000000..658b8b0c2c --- /dev/null +++ b/public/language/ko/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "알림", + "no_notifs": "새로운 알림이 없습니다.", + "see_all": "모든 알림", + "mark_all_read": "모두 읽음으로 표시", + "back_to_home": "%1(으)로 돌아가기", + "outgoing_link": "외부 링크", + "outgoing_link_message": "%1(을)를 떠납니다.", + "continue_to": "%1(으)로 이동", + "return_to": "%1(으)로 돌아가기", + "new_notification": "새로운 알림이 있습니다.", + "you_have_unread_notifications": "읽지 않은 알림이 있습니다.", + "all": "모든 알림", + "topics": "화제", + "replies": "답글", + "chat": "채팅", + "group-chat": "그룹 채팅", + "follows": "팔로우", + "upvote": "추천", + "new-flags": "새로 들어온 신고", + "my-flags": "내게 배정된 신고", + "bans": "차단", + "new_message_from": "%1님이 메시지를 보냈습니다.", + "upvoted_your_post_in": "%1님이 %2의 내 포스트를 추천했습니다.", + "upvoted_your_post_in_dual": "%1님과 %2님이 %3의 내 포스트를 추천했습니다.", + "upvoted_your_post_in_multiple": "%1님과 다른 %2 명이 %3의 내 포스트를 추천했습니다.", + "moved_your_post": "%1님이 내 포스트를 %2로 옮겼습니다.", + "moved_your_topic": "%1%2를 옮겼습니다.", + "user_flagged_post_in": "%1님이 %2에 속한 포스트를 신고했습니다.", + "user_flagged_post_in_dual": "%1님과 %2님이 %3에 속한 포스트를 신고했습니다.", + "user_flagged_post_in_multiple": "%1님과 %2명의 다른 유저들이 %3에 속한 포스트를 신고했습니다.", + "user_flagged_user": "%1님이 %2님의 프로필을 신고했습니다.", + "user_flagged_user_dual": "%1님과 %2님이 %3님의 프로필을 신고했습니다.", + "user_flagged_user_multiple": "%1님과 다른 %2명이 %3의 프로필을 신고했습니다.", + "user_posted_to": "%1님이 %2에 답글을 달았습니다.", + "user_posted_to_dual": "%1님과 %2님이 %3에 답글을 달았습니다.", + "user_posted_to_multiple": "%1님과 %2명의 다른 유저들이 %3에 답글을 달았습니다.", + "user_posted_topic": "%1님이 새 게시물을 작성했습니다: %2", + "user_edited_post": "%1님이 %2에 속한 포스트를 편집했습니다.", + "user_started_following_you": "%1님이 나를 팔로우 합니다.", + "user_started_following_you_dual": "%1님과 %2님이 나를 팔로우 합니다.", + "user_started_following_you_multiple": "%1님외 %2명이 나를 팔로우 합니다.", + "new_register": "%1님이 가입을 요청했습니다.", + "new_register_multiple": "%1개의 회원 가입 요청이 승인 대기 중입니다.", + "flag_assigned_to_you": "신고 ID %1(이)가 나에게 배정되었습니다.", + "post_awaiting_review": "검토중인 게시물", + "profile-exported": "%1의 프로필 내보내기 완료, 클릭해서 다운로드", + "posts-exported": "%1의 포스트 내보내기 완료, 클릭해서 다운로드 ", + "uploads-exported": "%1의 업로드 내보내기 완료, 클릭해서 다운로드 ", + "users-csv-exported": "사용자 csv 내보내기 완료, 클릭해서 다운로드", + "post-queue-accepted": "게시 대기 중인 게시물이 승인되었습니다. 여기를 눌러 포스트를 확인할 수 있습니다.", + "post-queue-rejected": "게시 대기 중인 게시물이 거절되었습니다.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "이메일 인증이 완료되었습니다.", + "email-confirmed-message": "이메일을 인증해주셔서 감사합니다. 계정이 완전히 활성화되었습니다.", + "email-confirm-error-message": "이메일 주소를 인증하지 못했습니다. 코드가 올바르지 않거나 만료되었을 수 있습니다.", + "email-confirm-sent": "인증 이메일이 발송되었습니다.", + "none": "없음", + "notification_only": "알림만", + "email_only": "이메일만", + "notification_and_email": "알림 & 이메일", + "notificationType_upvote": "누군가 내 글을 추천", + "notificationType_new-topic": "팔로우 하는 사람이 새로운 화제 작성", + "notificationType_new-reply": "관심 화제에 새로운 답글", + "notificationType_post-edit": "관심 화제의 포스트 수정", + "notificationType_follow": "누군가 나를 팔로우", + "notificationType_new-chat": "채팅 메시지 수신", + "notificationType_new-group-chat": "그룹 채팅 메시지 수신", + "notificationType_group-invite": "그룹 초대", + "notificationType_group-leave": "그룹에서 탈퇴자 발생", + "notificationType_group-request-membership": "누군가 당신이 관리하는 그룹에 참여 요청", + "notificationType_new-register": "누군가 회원가입 승인 대기 중", + "notificationType_post-queue": "새로운 포스트가 게시 대기 중", + "notificationType_new-post-flag": "포스트 신고", + "notificationType_new-user-flag": "사용자 신고" +} \ No newline at end of file diff --git a/public/language/ko/pages.json b/public/language/ko/pages.json new file mode 100644 index 0000000000..5694735898 --- /dev/null +++ b/public/language/ko/pages.json @@ -0,0 +1,65 @@ +{ + "home": "홈", + "unread": "읽지 않은 화제", + "popular-day": "일간 인기 화제", + "popular-week": "주간 인기 화제", + "popular-month": "월간 인기 화제", + "popular-alltime": "베스트 인기 화제", + "recent": "최근 화제", + "top-day": "일간 투표 화제", + "top-week": "주간 투표 화제", + "top-month": "월간 투표 화제", + "top-alltime": "베스트 투표 화제", + "moderator-tools": "조정 도구", + "flagged-content": "신고된 컨텐츠", + "ip-blacklist": "IP 블랙리스트", + "post-queue": "게시 대기열", + "users/online": "온라인 사용자", + "users/latest": "최근 사용자", + "users/sort-posts": "최다 작성 사용자", + "users/sort-reputation": "최고 인지도 사용자", + "users/banned": "차단된 사용자", + "users/most-flags": "최다 신고 사용자", + "users/search": "사용자 검색", + "notifications": "알림", + "tags": "태그", + "tag": ""%1" 태그의 화제", + "register": "회원가입", + "registration-complete": "회원가입 완료", + "login": "로그인", + "reset": "비밀번호 재설정", + "categories": "카테고리", + "groups": "그룹", + "group": "%1 그룹", + "chats": "채팅", + "chat": "%1님과 채팅", + "flags": "신고 목록", + "flag-details": "신고 ID %1의 세부내용", + "account/edit": "\"%1\" 수정", + "account/edit/password": "\"%1\"의 패스워드 수정", + "account/edit/username": "\"%1\"의 사용자명 수정", + "account/edit/email": "\"%1\"의 이메일 수정", + "account/info": "계정 정보", + "account/following": "%1님이 팔로우 하는 사용자", + "account/followers": "%1님을 팔로우 하는 사용자", + "account/posts": "%1님이 작성한 포스트", + "account/latest-posts": "%1님이 최근 작성한 글", + "account/topics": "%1님이 생성한 화제", + "account/groups": "%1님의 그룹", + "account/watched_categories": "%1님의 관심 카테고리", + "account/bookmarks": "%1님의 즐겨찾기 포스트", + "account/settings": "사용자 설정", + "account/watched": "%1님의 관심 화제", + "account/ignored": "%1님이 무시 중인 화제", + "account/upvoted": "%1님이 추천한 포스트", + "account/downvoted": "%1님이 비추천한 포스트", + "account/best": "%1님의 베스트 포스트", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "%1님이 차단한 사용자", + "account/uploads": "%1님의 업로드", + "account/sessions": "로그인 세션", + "confirm": "이메일 인증 완료", + "maintenance.text": "%1은(는) 현재 점검 중입니다. 나중에 다시 방문해주세요.", + "maintenance.messageIntro": "관리자 메시지:", + "throttled.text": "과도한 서버 부하로 %1을(를) 불러올 수 없습니다. 잠시 후에 다시 시도해주세요." +} \ No newline at end of file diff --git a/public/language/ko/post-queue.json b/public/language/ko/post-queue.json new file mode 100644 index 0000000000..3d26027b42 --- /dev/null +++ b/public/language/ko/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "게시 대기열", + "description": "게시 대기열에 글이 없습니다.
이 기능을 활성화하기 위해서는 설정 → 포스트 → 게시 대기열로 이동해서 게시 대기열 기능을 활성화해주세요.", + "user": "사용자", + "category": "카테고리", + "title": "제목", + "content": "내용", + "posted": "게시됨", + "reply-to": "'%1'에 대한 답글", + "content-editable": "내용을 눌러 편집", + "category-editable": "카테고리를 눌러 편집", + "title-editable": "제목을 눌러 편집", + "reply": "답글", + "topic": "화제", + "accept": "승인", + "reject": "거절", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/ko/recent.json b/public/language/ko/recent.json new file mode 100644 index 0000000000..4e6e6b4cb3 --- /dev/null +++ b/public/language/ko/recent.json @@ -0,0 +1,19 @@ +{ + "title": "최근", + "day": "일간", + "week": "주간", + "month": "월간", + "year": "연간", + "alltime": "전체", + "no_recent_topics": "최근 생성된 화제가 없습니다.", + "no_popular_topics": "인기 화제가 없습니다.", + "there-is-a-new-topic": "새로운 화제가 있습니다.", + "there-is-a-new-topic-and-a-new-post": "새로운 화제와 포스트가 있습니다.", + "there-is-a-new-topic-and-new-posts": "새로운 화제와 %1개의 포스트가 있습니다.", + "there-are-new-topics": "새로운 %1개의 화제가 있습니다.", + "there-are-new-topics-and-a-new-post": "새로운 %1개의 화제와 포스트가 있습니다.", + "there-are-new-topics-and-new-posts": "새로운 %1개의 화제와 %2개의 포스트가 있습니다.", + "there-is-a-new-post": "새로운 포스트가 있습니다.", + "there-are-new-posts": "새로운 %1개의 포스트가 있습니다.", + "click-here-to-reload": "여기를 클릭해서 새로고침하세요." +} \ No newline at end of file diff --git a/public/language/ko/register.json b/public/language/ko/register.json new file mode 100644 index 0000000000..f9249dfbb5 --- /dev/null +++ b/public/language/ko/register.json @@ -0,0 +1,32 @@ +{ + "register": "회원가입", + "cancel_registration": "회원가입 취소", + "help.email": "입력하신 이메일 주소는 공개되지 않으며, 설정을 통해 공개하실 수 있습니다.", + "help.username_restrictions": "%1자 이상 %2자 이하의 고유한 사용자명을 입력하세요. @username 같은 방식으로 다른 사람들을 언급할 수 있습니다.", + "help.minimum_password_length": "비밀번호는 최소 %1자로 제한됩니다.", + "email_address": "이메일", + "email_address_placeholder": "여기에 이메일 주소를 입력하세요.", + "username": "사용자명", + "username_placeholder": "여기에 사용자명을 입력하세요.", + "password": "비밀번호", + "password_placeholder": "여기에 비밀번호를 입력하세요.", + "confirm_password": "비밀번호 확인", + "confirm_password_placeholder": "여기에 비밀번호 확인을 입력하세요.", + "register_now_button": "가입하기", + "alternative_registration": "다른 방법으로 회원가입", + "terms_of_use": "이용약관", + "agree_to_terms_of_use": "이용약관에 동의합니다.", + "terms_of_use_error": "이용약관에 동의하셔야 합니다.", + "registration-added-to-queue": "회원가입이 요청되었습니다. 관리자의 승인 후 메일이 발송됩니다.", + "registration-queue-average-time": "가입 승인에는 평균적으로 %1시간 %2분이 소요됩니다.", + "registration-queue-auto-approve-time": "%1시간 내로 이 포럼에서 사용자의 계정이 완전히 활성화될 예정입니다.", + "interstitial.intro": "계정 정보를 수정하기 전 추가 정보가 필요합니다.", + "interstitial.intro-new": "계정을 생성하기 전 추가 정보가 필요합니다.", + "interstitial.errors-found": "입력한 정보를 다시 확인해주세요.", + "gdpr_agree_data": "나는 이 웹사이트에서 개인 정보를 수집하고 처리하는데 동의합니다.", + "gdpr_agree_email": "나는 이 웹사이트에서 포럼 메일 및 알림 메일을 수신하는데 동의합니다.", + "gdpr_consent_denied": "사용자는 이 사이트가 사용자의 정보를 수집/처리하고 이메일을 보내는 것에 동의해야 합니다.", + "invite.error-admin-only": "회원가입이 비활성화되었습니다. 포럼 관리 팀에 연락해보세요.", + "invite.error-invite-only": "회원가입이 비활성화되었습니다. 가입하고 이 포럼에 접속하려면 기존 가입자의 초대가 필요합니다.", + "invite.error-invalid-data": "입력된 가입 데이터가 가입 조건을 충족하지 못합니다. 포럼 관리 팀에 연락해보세요." +} \ No newline at end of file diff --git a/public/language/ko/reset_password.json b/public/language/ko/reset_password.json new file mode 100644 index 0000000000..d93d8d75bc --- /dev/null +++ b/public/language/ko/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "비밀번호 재설정", + "update_password": "비밀번호 변경", + "password_changed.title": "비밀번호 변경 완료", + "password_changed.message": "

비밀번호가 성공적으로 초기화되었습니다. 다시 로그인해주세요.", + "wrong_reset_code.title": "올바르지 않은 초기화 코드입니다.", + "wrong_reset_code.message": "올바르지 않은 초기화 코드입니다. 다시 시도하거나 새로운 초기화 코드를 요청하세요.", + "new_password": "새로운 비밀번호", + "repeat_password": "비밀번호 확인", + "changing_password": "비밀번호 변경", + "enter_email": "이메일 주소를 입력하면 비밀번호를 초기화하는 방법을 메일로 알려드립니다.", + "enter_email_address": "이메일 주소 입력", + "password_reset_sent": "입력한 이메일 주소가 사용자의 정보와 일치할 경우 비밀번호 초기화 이메일을 발송합니다. 1분에 1회만 발송할 수 있습니다.", + "invalid_email": "올바르지 않거나 가입되지 않은 이메일입니다.", + "password_too_short": "입력한 비밀번호가 너무 짧습니다. 다시 입력하세요.", + "passwords_do_not_match": "비밀번호와 비밀번호 확인이 일치하지 않습니다.", + "password_expired": "비밀번호가 만료되었습니다. 새로운 비밀번호를 입력하세요." +} \ No newline at end of file diff --git a/public/language/ko/search.json b/public/language/ko/search.json new file mode 100644 index 0000000000..56f4e1fe4d --- /dev/null +++ b/public/language/ko/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "\"%2\"와 일치하는 %1개의 결과를 찾았습니다 (검색시간: %3초)", + "no-matches": "일치하는 결과가 없습니다.", + "advanced-search": "고급 검색", + "in": "검색 기준", + "titles": "제목", + "titles-posts": "제목과 내용", + "match-words": "일치단어순", + "all": "전체", + "any": "아무거나", + "posted-by": "작성자", + "in-categories": "카테고리 지정", + "search-child-categories": "하위 카테고리 포함 검색", + "has-tags": "태그 검색", + "reply-count": "답글 수", + "at-least": "최소", + "at-most": "최대", + "relevance": "관련성", + "post-time": "작성일", + "votes": "추천", + "newer-than": "이후", + "older-than": "이전", + "any-date": "전체", + "yesterday": "어제", + "one-week": "1주", + "two-weeks": "2주", + "one-month": "1개월", + "three-months": "3개월", + "six-months": "6개월", + "one-year": "1년", + "sort-by": "정렬 기준", + "last-reply-time": "최근 답글 작성 시간", + "topic-title": "게시물 제목", + "topic-votes": "게시물 투표", + "number-of-replies": "답글 수", + "number-of-views": "조회수", + "topic-start-date": "화제 작성일", + "username": "사용자명", + "category": "게시판", + "descending": "내림차순", + "ascending": "오름차순", + "save-preferences": "설정 저장", + "clear-preferences": "설정 초기화", + "search-preferences-saved": "검색 설정 저장 완료", + "search-preferences-cleared": "검색 설정 초기화", + "show-results-as": "검색 결과 표시 방법", + "see-more-results": "더 많은 결과 (%1)", + "search-in-category": "\"%1\"에서 검색" +} \ No newline at end of file diff --git a/public/language/ko/success.json b/public/language/ko/success.json new file mode 100644 index 0000000000..a627bcb5af --- /dev/null +++ b/public/language/ko/success.json @@ -0,0 +1,7 @@ +{ + "success": "성공", + "topic-post": "성공적으로 작성했습니다.", + "post-queued": "포스트가 게시 대기열에 등록되었습니다. 승인 혹은 거절될 경우 알림이 전송됩니다.", + "authentication-successful": "인증에 성공했습니다.", + "settings-saved": "설정이 저장되었습니다!" +} \ No newline at end of file diff --git a/public/language/ko/tags.json b/public/language/ko/tags.json new file mode 100644 index 0000000000..4ef8d46d26 --- /dev/null +++ b/public/language/ko/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "이 태그가 달린 게시물이 없습니다.", + "tags": "태그 목록", + "enter_tags_here": "%1에서 %2자 안으로 태그를 입력하세요.", + "enter_tags_here_short": "태그 입력...", + "no_tags": "아직 태그가 달리지 않았습니다.", + "select_tags": "태그 선택" +} \ No newline at end of file diff --git a/public/language/ko/top.json b/public/language/ko/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/ko/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/ko/topic.json b/public/language/ko/topic.json new file mode 100644 index 0000000000..ebb4fc90f2 --- /dev/null +++ b/public/language/ko/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "토픽", + "title": "제목", + "no_topics_found": "토픽이 없습니다!", + "no_posts_found": "포스트가 없습니다!", + "post_is_deleted": "이 포스트는 삭제됐습니다!", + "topic_is_deleted": "이 화제는 삭제됐습니다!", + "profile": "프로필", + "posted_by": "%1님에 의해 작성됨", + "posted_by_guest": "비회원에 의해 작성됨", + "chat": "채팅", + "notify_me": "이 화제의 새 답글에 대한 알림 받기", + "quote": "인용", + "reply": "답글", + "replies_to_this_post": "%1개의 답글", + "one_reply_to_this_post": "1개의 답글", + "last_reply_time": "마지막 답글", + "reply-as-topic": "화제로 답글 작성", + "guest-login-reply": "답글 작성을 위해 로그인", + "login-to-view": "🔒 열람을 위해 로그인", + "edit": "수정", + "delete": "삭제", + "delete-event": "이벤트 삭제", + "delete-event-confirm": "이 이벤트를 삭제하시겠습니까?", + "purge": "완전 삭제", + "restore": "복원", + "move": "이동", + "change-owner": "작성자 변경", + "fork": "분리", + "link": "바로가기", + "share": "공유", + "tools": "도구", + "locked": "잠긴 게시물", + "pinned": "고정된 게시물", + "pinned-with-expiry": "%1까지 상단 고정", + "scheduled": "예약됨", + "moved": "이동된 게시물", + "moved-from": "%1부터 상단 고정 해제", + "copy-ip": "IP 복사", + "ban-ip": "IP 차단", + "view-history": "편집 기록", + "locked-by": "잠금한 사용자:", + "unlocked-by": "잠금 해제한 사용자:", + "pinned-by": "상단 고정한 사용자:", + "unpinned-by": "상단 고정을 해제한 사용자:", + "deleted-by": "삭제한 사용자:", + "restored-by": "복원한 사용자:", + "moved-from-by": "%1에서 이동됨 -", + "queued-by": "게시 승인 대기 중 →", + "backlink": "참조 -", + "forked-by": "Forked by", + "bookmark_instructions": "이 쓰레드에서 읽은 마지막 포스트로 이동하려면 여기를 클릭 하세요.", + "flag-post": "해당 포스트 신고", + "flag-user": "해당 유저 신고", + "already-flagged": "이미 신고 처리됨", + "view-flag-report": "신고 기록 보기", + "resolve-flag": "신고 해결", + "merged_message": "이 화제는 %2로 병합되었습니다.", + "deleted_message": "이 화제는 삭제됐습니다. 게시물 관리 권한이 있는 사용자만 볼 수 있습니다.", + "following_topic.message": "이제 이 화제에 새 답글이 달리면 알림을 받습니다.", + "not_following_topic.message": "이 화제를 읽지 않음 목록에서 볼 수 있지만, 이 화제에 달린 포스트에 대해서는 알림을 받지 않습니다.", + "ignoring_topic.message": "이 화제는 이제 읽지 않음 목록에서 보이지 않습니다. 누군가 나를 언급하거나 내 포스트가 추천 받으면 알림을 받습니다.", + "login_to_subscribe": "이 화제를 관심 목록에 추가하기 위해서는 로그인이 필요합니다.", + "markAsUnreadForAll.success": "모든 사용자에 대해 읽지 않음으로 표시했습니다.", + "mark_unread": "읽지 않음으로 표시", + "mark_unread.success": "화제를 읽지 않음으로 표시했습니다.", + "watch": "관심", + "unwatch": "관심 해제", + "watch.title": "이 화제의 새 답글에 대해 알림 받기", + "unwatch.title": "이 화제의 새 답글에 대한 알림 해제", + "share_this_post": "포스트 공유", + "watching": "관심", + "not-watching": "관심 해제", + "ignoring": "무시", + "watching.description": "새로운 답글에 대한 알림 받기.
\"읽지 않음\" 목록에 보여주기.", + "not-watching.description": "새로운 답글에 대해 알림 받지 않기. 해당 게시판을 팔로우 중이라면 \"읽지 않음\" 에서 보여주기.", + "ignoring.description": "새로운 답글에 대한 알림 받지 않기. \"읽지 않음\"에서 보여주지 않기.", + "thread_tools.title": "화제 관리", + "thread_tools.markAsUnreadForAll": "모두에게 읽지 않음으로 표시", + "thread_tools.pin": "상단 고정", + "thread_tools.unpin": "상단 고정 해제", + "thread_tools.lock": "잠금", + "thread_tools.unlock": "잠금 해제", + "thread_tools.move": "화제 이동", + "thread_tools.move-posts": "포스트 이동", + "thread_tools.move_all": "모두 이동", + "thread_tools.change_owner": "작성자 변경", + "thread_tools.select_category": "카테고리 선택", + "thread_tools.fork": "화제 분리", + "thread_tools.delete": "화제 삭제", + "thread_tools.delete-posts": "포스트 삭제", + "thread_tools.delete_confirm": "이 화제를 삭제하시겠습니까?", + "thread_tools.restore": "화제 복원", + "thread_tools.restore_confirm": "이 화제를 복원하시겠습니까?", + "thread_tools.purge": "화제 완전 삭제", + "thread_tools.purge_confirm": "이 화제를 완전히 삭제하시겠습니까?", + "thread_tools.merge_topics": "화제 병합", + "thread_tools.merge": "병합", + "topic_move_success": "이 화제는 잠시 후에 \"%1\"로 옮겨집니다. 여기를 눌러 취소하세요.", + "topic_move_multiple_success": "이 화제들은 잠시 후에 \"%1\"로 옮겨집니다. 여기를 눌러 취소하세요.", + "topic_move_all_success": "모든 화제가 잠시 후에 \"%1\"로 옮겨집니다. 여기를 눌러 취소하세요.", + "topic_move_undone": "화제 이동 취소", + "topic_move_posts_success": "포스트가 곧 옮겨집니다. 여기를 눌러 취소하세요.", + "topic_move_posts_undone": "포스트 이동 취소", + "post_delete_confirm": "이 포스트를 삭제 하시겠습니까?", + "post_restore_confirm": "이 포스트를 복원 하시겠습니까?", + "post_purge_confirm": "이 포스트를 폐기 하시겠습니까?", + "pin-modal-expiry": "만료 일자", + "pin-modal-help": "여기에서 상단 고정할 화제(들)의 만료 일자를 선택할 수 있습니다. 선택하지 않으면 직접 고정을 해제하기 전까지 유지됩니다.", + "load_categories": "카테고리를 읽어오는 중입니다.", + "confirm_move": "이동", + "confirm_fork": "분리", + "bookmark": "즐겨찾기", + "bookmarks": "즐겨찾기 목록", + "bookmarks.has_no_bookmarks": "즐겨찾기에 추가한 글이 없습니다.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "더 많은 글 불러오는 중", + "move_topic": "화제 이동", + "move_topics": "화제 이동", + "move_post": "포스트 이동", + "post_moved": "포스트 이동 완료!", + "fork_topic": "화제 분리", + "enter-new-topic-title": "새로운 화제 제목 입력", + "fork_topic_instruction": "분리할 포스트를 선택하세요.", + "fork_no_pids": "선택된 포스트가 없습니다.", + "no-posts-selected": "선택된 포스트가 없습니다!", + "x-posts-selected": "%1개의 포스트 선택됨", + "x-posts-will-be-moved-to-y": "%1개의 포스트가 \"%2\"로 옮겨집니다.", + "fork_pid_count": "%1 개의 포스트(들)이 선택되었습니다", + "fork_success": "게시물이 분리되었습니다! 분리된 게시물을 보려면 여기를 클릭 하세요.", + "delete_posts_instruction": "삭제/완전 삭제할 포스트를 선택하세요.", + "merge_topics_instruction": "병합할 화제를 선택하거나 검색하세요.", + "merge-topic-list-title": "병합될 화제 목록", + "merge-options": "병합 옵션", + "merge-select-main-topic": "주 화제를 선택하세요.", + "merge-new-title-for-topic": "변경할 제목", + "topic-id": "화제 ID", + "move_posts_instruction": "옮길 포스트를 선택하고 목표 화제의 ID를 입력하거나 해당 화제로 직접 이동하세요.", + "change_owner_instruction": "다른 사용자에게 할당할 포스트를 선택하세요.", + "composer.title_placeholder": "화제 제목을 입력하세요.", + "composer.handle_placeholder": "이름을 입력하세요.", + "composer.discard": "취소", + "composer.submit": "등록", + "composer.additional-options": "Additional Options", + "composer.schedule": "예약", + "composer.replying_to": "'%1'에 대한 답글", + "composer.new_topic": "새 화제 생성", + "composer.editing": "편집 중", + "composer.uploading": "업로드 중...", + "composer.thumb_url_label": "썸네일 URL을 붙여넣으세요", + "composer.thumb_title": "이 게시물에 썸네일 추가", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "혹은 파일을 업로드", + "composer.thumb_remove": "썸네일 제거", + "composer.drag_and_drop_images": "이미지를 여기에 드래그&드롭하세요.", + "more_users_and_guests": "%1명 이상의 회원과 %2명의 비회원", + "more_users": "%1명 이상의 회원", + "more_guests": "%1명 이상의 비회원", + "users_and_others": "%1님 외 %2명", + "sort_by": "정렬 기준", + "oldest_to_newest": "오래된순", + "newest_to_oldest": "최신순", + "most_votes": "투표순", + "most_posts": "포스트순", + "most_views": "조회수순", + "stale.title": "새로운 화제를 생성하시겠습니까?", + "stale.warning": "현재 답글을 작성 중인 화제는 오래전에 작성 되었습니다. 새로 화제를 생성하고 이 게시물을 인용하시겠습니까?", + "stale.create": "새로운 화제 작성", + "stale.reply_anyway": "이 화제에 답글 작성", + "link_back": "답글: [%1](%2)", + "diffs.title": "편집 기록", + "diffs.description": "이 포스트에는 %1개의 리비전이 있습니다. 클릭해서 해당 리비전 시점의 내용을 확인할 수 있습니다.", + "diffs.no-revisions-description": "이 포스트에는 %1개의 리비전이 있습니다.", + "diffs.current-revision": "현재 리비전", + "diffs.original-revision": "원래의 리비전", + "diffs.restore": "리비전 복구", + "diffs.restore-description": "초기화 후에 새로운 리비전이 포스트의 편집 기록에 덧붙여집니다.", + "diffs.post-restored": "이전 리비전으로의 복구가 완료되었습니다. ", + "diffs.delete": "리비전 삭제", + "diffs.deleted": "리비전 삭제됨", + "timeago_later": "%1 이후", + "timeago_earlier": "%1 이전", + "first-post": "첫 포스트", + "last-post": "마지막 포스트", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "빠른 답글" +} \ No newline at end of file diff --git a/public/language/ko/unread.json b/public/language/ko/unread.json new file mode 100644 index 0000000000..c47172fc5e --- /dev/null +++ b/public/language/ko/unread.json @@ -0,0 +1,15 @@ +{ + "title": "읽지 않음", + "no_unread_topics": "읽지 않은 화제가 없습니다.", + "load_more": "더 보기", + "mark_as_read": "읽음으로 표시", + "selected": "선택됨", + "all": "전체", + "all_categories": "모든 카테고리", + "topics_marked_as_read.success": "화제들을 읽음으로 표시했습니다.", + "all-topics": "모든 화제", + "new-topics": "새 화제", + "watched-topics": "읽은 화제", + "unreplied-topics": "답글이 없는 화제", + "multiple-categories-selected": "다중선택됨" +} \ No newline at end of file diff --git a/public/language/ko/uploads.json b/public/language/ko/uploads.json new file mode 100644 index 0000000000..ff88ede2de --- /dev/null +++ b/public/language/ko/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "파일 업로드 중...", + "select-file-to-upload": "업로드할 파일을 선택해주세요!", + "upload-success": "파일이 성공적으로 업로드 되었습니다!", + "maximum-file-size": "최대 %1 kb", + "no-uploads-found": "업로드한 파일 없음", + "public-uploads-info": "업로드는 공개되어 모든 방문자가 볼 수 있습니다.", + "private-uploads-info": "업로드는 비공개이며 로그인한 사용자만 볼 수 있습니다." +} \ No newline at end of file diff --git a/public/language/ko/user.json b/public/language/ko/user.json new file mode 100644 index 0000000000..0e6f32e9c3 --- /dev/null +++ b/public/language/ko/user.json @@ -0,0 +1,199 @@ +{ + "banned": "차단됨", + "muted": "Muted", + "offline": "오프라인", + "deleted": "삭제됨", + "username": "사용자명", + "joindate": "가입일", + "postcount": "포스트 수", + "email": "이메일", + "confirm_email": "이메일 인증", + "account_info": "계정 정보", + "admin_actions_label": "사용자 관리", + "ban_account": "계정 차단", + "ban_account_confirm": "이 사용자를 차단하시겠습니까?", + "unban_account": "차단 해제", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "계정 삭제", + "delete_account_as_admin": "계정 삭제", + "delete_content": "계정 컨텐츠 삭제", + "delete_all": " 계정컨텐츠 삭제", + "delete_account_confirm": "정말 지금까지 작성한 글들을 익명으로 처리하고 계정을 삭제하시겠습니까?
이 행동은 되돌릴 수 없고 당신의 모든 데이터는 복구할 수 없습니다.

당신이 정말로 바란다면 비밀번호를 입력해서 이 계정을 삭제하세요.", + "delete_this_account_confirm": "정말 게시물을 남기고 계정을 삭제하시겠습니까?
이 행동은 되돌릴 수 없고 모든 글들은 익명으로 처리되며, 삭제된 계정에서 당신이 기여한 기록은 복구할 수 없습니다.

", + "delete_account_content_confirm": "정말 이 계정의 모든 데이터(포스트/화제/업로드)를 삭제하시겠습니까?
이 행동은 되돌릴 수 없고 모든 데이터는 복구할 수 없습니다.

", + "delete_all_confirm": "정말 이 계정과 모든 데이터(포스트/화제/업로드)를 삭제하시겠습니까?
이 행동은 되돌릴 수 없고 모든 데이터는 복구할 수 없습니다.

", + "account-deleted": "계정 삭제 완료", + "account-content-deleted": "계정 데이터 삭제 완료", + "fullname": "이름", + "website": "웹사이트", + "location": "위치", + "age": "나이", + "joined": "가입일", + "lastonline": "최근 접속", + "profile": "프로필", + "profile_views": "프로필 조회수", + "reputation": "인지도", + "bookmarks": "즐겨찾기", + "watched_categories": "관심있는 카테고리", + "change_all": "전체 바꾸기", + "watched": "관심있는 화제", + "ignored": "무시 중인 화제", + "default-category-watch-state": "기본 카테고리 관심 상태", + "followers": "팔로워", + "following": "팔로잉", + "blocks": "차단", + "block_toggle": "차단 전환", + "block_user": "사용자 차단", + "unblock_user": "사용자 차단 해제", + "aboutme": "자기소개", + "signature": "서명", + "birthday": "생일", + "chat": "채팅", + "chat_with": "%1과/와 채팅 이어가기", + "new_chat_with": "%1과/와 새로운 채팅", + "flag-profile": "프로필 신고", + "follow": "팔로우", + "unfollow": "팔로우 취소", + "more": "더 보기", + "profile_update_success": "프로필을 성공적으로 업데이트했습니다!", + "change_picture": "사진 변경", + "change_username": "사용자명 변경", + "change_email": "이메일 변경", + "email_same_as_password": "비밀번호를 입력해서 진행하세요. – 새로운 이메일 주소를 다시 입력했습니다.", + "edit": "수정", + "edit-profile": "프로필 수정", + "default_picture": "기본 아이콘", + "uploaded_picture": "업로드된 사진", + "upload_new_picture": "새 사진 업로드", + "upload_new_picture_from_url": "URL을 통해 새 사진 업로드", + "current_password": "현재 비밀번호", + "change_password": "비밀번호 변경", + "change_password_error": "올바르지 않은 비밀번호입니다!", + "change_password_error_wrong_current": "현재 비밀번호가 일치하지 않습니다!", + "change_password_error_match": "재입력한 비밀번호가 새 비밀번호와 일치하지 않습니다!", + "change_password_error_privileges": "비밀번호를 바꿀 권한이 없습니다.", + "change_password_success": "비밀번호를 변경했습니다.", + "confirm_password": "비밀번호 확인", + "password": "비밀번호", + "username_taken_workaround": "새 사용자명이 이미 존재하여 %1로 저장되었습니다.", + "password_same_as_username": "비밀번호가 사용자명과 동일합니다. 다른 비밀번호를 입력하세요.", + "password_same_as_email": "비밀번호가 이메일 주소와 동일합니다. 다른 비밀번호를 입력하세요.", + "weak_password": "보안이 취약한 비밀번호입니다.", + "upload_picture": "사진 업로드", + "upload_a_picture": "사진 업로드", + "remove_uploaded_picture": "업로드한 사진 삭제", + "upload_cover_picture": "커버 사진 업로드", + "remove_cover_picture_confirm": "커버 사진을 제거하시겠습니까?", + "crop_picture": "사진 잘라내기", + "upload_cropped_picture": "잘라내고 업로드", + "avatar-background-colour": "아바타 배경 색상", + "settings": "설정", + "show_email": "이메일 공개", + "show_fullname": "이름 공개", + "restrict_chats": "내가 팔로우하는 사용자들로부터만 채팅 허용", + "digest_label": "포럼 이메일 구독", + "digest_description": "주기적으로 포럼 메일(새 알림과 게시물)을 구독", + "digest_off": "해제", + "digest_daily": "매일", + "digest_weekly": "매주", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "매월", + "has_no_follower": "이 사용자는 팔로워가 없습니다 :(", + "follows_no_one": "이 사용자는 아무도 팔로우하고 있지 않습니다 :(", + "has_no_posts": "이 사용자가 작성한 포스트가 없습니다.", + "has_no_best_posts": "해당 유저는 아직까지 추천을 받은 포스트가 없습니다.", + "has_no_topics": "이 사용자가 작성한 화제가 없습니다.", + "has_no_watched_topics": "이 사용자가 관심 목록에 추가한 화제가 없습니다.", + "has_no_ignored_topics": "이 사용자는 아직 무시 중인 화제가 없습니다.", + "has_no_upvoted_posts": "이 사용자가 추천한 포스트가 없습니다.", + "has_no_downvoted_posts": "이 사용자가 비추천한 포스트가 없습니다.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "차단한 사용자가 없습니다.", + "email_hidden": "이메일 비공개", + "hidden": "비공개", + "paginate_description": "주제와 게시물을 페이지로 정리 (기본: 무한 스크롤)", + "topics_per_page": "페이지 당 화제 수", + "posts_per_page": "페이지 당 포스트 수", + "max_items_per_page": "최대 %1 ", + "acp_language": "관리 페이지 언어", + "notifications": "알림", + "upvote-notif-freq": "추천 알림 빈도", + "upvote-notif-freq.all": "모든 추천에 알림 사용", + "upvote-notif-freq.first": "포스트마다 최초 1회", + "upvote-notif-freq.everyTen": "10개의 추천마다 알림", + "upvote-notif-freq.threshold": "1, 5, 10, 25, 50, 100, 150, 200... 마다 알림", + "upvote-notif-freq.logarithmic": "10, 100, 1000... 마다 알림", + "upvote-notif-freq.disabled": "비활성화", + "browsing": "브라우징 설정", + "open_links_in_new_tab": "외부 링크를 새로운 탭에서 열람", + "enable_topic_searching": "게시물 내 검색 허용", + "topic_search_help": "만약 활성화된다면, 브라우저의 기본 검색 기능은 무효화되고 게시물 내 검색을 통해 화면에 보여지는 것 뿐만 아니라 게시물 전체의 내용을 검색할 수 있습니다.", + "update_url_with_post_index": "화제를 보고 있을 때 포스트마다 url 업데이트", + "scroll_to_my_post": "답글 게시 후 새 포스트 보여주기", + "follow_topics_you_reply_to": "내가 답글을 단 화제를 관심 목록에 추가", + "follow_topics_you_create": "내가 작성한 화제를 관심 목록에 추가", + "grouptitle": "그룹 이름", + "group-order-help": "그룹 선택 후 화살표로 순서 지정", + "no-group-title": "그룹 이름이 없습니다.", + "select-skin": "스킨 선택", + "select-homepage": "홈페이지 선택", + "homepage": "홈페이지", + "homepage_description": "포럼 홈페이지로 사용할 페이지를 선택하거나 'None'으로 설정하여 기본 홈페이지를 사용합니다.", + "custom_route": "사용자 정의 홈페이지 경로", + "custom_route_help": "첫 슬래시를 제외한 경로 입력 (예시. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "통합 인증 서비스", + "sso.associated": "와/과 연동된", + "sso.not-associated": "이 곳을 클릭하여 연동시키세요.", + "sso.dissociate": "연동 해제", + "sso.dissociate-confirm-title": "연동 해제 확정", + "sso.dissociate-confirm": "%1로부터 계정의 연동을 해제하시겠습니까?", + "info.latest-flags": "최근에 들어온 신고", + "info.no-flags": "신고된 포스트 없음", + "info.ban-history": "최근 차단 기록", + "info.no-ban-history": "이 사용자는 차단된 적이 없습니다.", + "info.banned-until": "%1까지 차단됨", + "info.banned-expiry": "만료일", + "info.banned-permanently": "영구 차단", + "info.banned-reason-label": "사유", + "info.banned-no-reason": "사유 없음", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "사용자명 변경 기록", + "info.email-history": "이메일 변경 기록", + "info.moderation-note": "관리자 메모", + "info.moderation-note.success": "관리자 메모 저장 완료", + "info.moderation-note.add": "메모 추가", + "sessions.description": "이 페이지에서는 이 포럼의 모든 활성 세션을 보고 필요할 경우 취소할 수 있습니다. 계정에서 로그아웃하여 자신의 세션을 취소할 수 있습니다.", + "consent.title": "권리 동의", + "consent.lead": "이 커뮤니티 포럼은 사용자의 개인 정보를 수집하고 처리합니다.", + "consent.intro": "포럼은 이 정보를 엄격히 관리하며 커뮤니티에서 사용자의 행동을 개인화하고 게시물과 계정을 연동하는데 사용합니다. 회원가입 단계에서 사용자 이름과 전자 메일 주소를 제공하도록 요청 받은 경우 이 웹사이트에서 사용자 프로필을 완료하는데 필요한 추가 정보를 선택적으로 제공할 수도 있습니다.

저희는 사용자의 계정이 삭제되기 전까지 이 정보를 보관하며, 사용자는 계정을 삭제하여 언제든지 동의를 철회할 수 있습니다. 또한 언제든지 권리 및 동의 페이지를 통해 이 웹 사이트에 대한 기여도 사본을 요청할 수 있습니다.

문의사항은 이 포럼의 관리 팀에 연락 바랍니다.", + "consent.email_intro": "가끔 포럼은 사용자가 등록한 이메일로 사용자에게 중요할 수 있는, 새로운 활동이나 갱신 사항을 알리기 위해 포럼 메일을 보낼 수도 있습니다. 사용자는 이메일로 전송 받을 알림의 종류와 포럼 메일(완전 비활성화를 포함해서)의 주기를 사용자 설정 페이지에서 선택할 수 있습니다.", + "consent.digest_frequency": "사용자 설정에서 변경하지 않으면 이 포럼은 %1마다 포럼 메일을 전송합니다.", + "consent.digest_off": "사용자 설정에서 명시적으로 변경하지 않는 한 이 포럼은 포럼 메일을 발송하지 않습니다", + "consent.received": "사용자는 이 포럼에서 사용자의 정보를 수집하고 처리하는 것에 동의했습니다. 추가 조치가 필요하지 않습니다.", + "consent.not_received": "사용자는 데이터 수집 및 처리에 대해 동의하지 않았습니다. 이 포럼은 언제든지 일반 데이터 보호 규정을 준수하기 위해 사용자의 계정을 삭제할 수 있습니다.", + "consent.give": "제공 동의", + "consent.right_of_access": "사용자의 접근 권한이 있습니다.", + "consent.right_of_access_description": "사용자는 우리가 수집한 사용자의 계정에 대한 어떠한 수집 데이터라도 기계가 읽을 수 있는 형태로 출력본을 요청할 수 있습니다. 아래에 있는 버튼 중 적절한 버튼을 클릭하여 해당 처리를 수행할 수 있습니다.", + "consent.right_to_rectification": "사용자의 교정 권한이 있습니다.", + "consent.right_to_rectification_description": "사용자는 포럼에 제공된 부정확한 데이터를 교체하거나 갱신할 권한이 있습니다. 당신의 프로필을 프로필 편집을 통해 갱신할 수 있으며, 게시물의 내용 또한 언제나 편집 가능합니다. 만약 불가능한 경우에는, 이 포럼의 관리 팀에게 연락해주세요.", + "consent.right_to_erasure": "사용자의 삭제 권한이 있습니다.", + "consent.right_to_erasure_description": "언제든지 계정을 삭제하여 데이터 수집 및/또는 처리에 대한 동의를 취소할 수 있습니다. 게시한 내용은 그대로 유지되지만 개인 프로필은 삭제할 수 있습니다. 계정 내용을 모두 삭제하려면 이 포럼의 관리 팀에 문의하십시오.", + "consent.right_to_data_portability": "사용자의 데이터 이동 권한이 있습니다.", + "consent.right_to_data_portability_description": "사용자는 이 포럼이 수집한 사용자와 사용자의 계정에 대한 어떠한 수집 데이터라도 기계가 읽을 수 있는 형태로 출력본을 요청할 수 있습니다. 아래에 버튼 중 적절한 버튼을 클릭하여 해당 처리를 수행할 수 있습니다.", + "consent.export_profile": "프로필 내보내기 (.json)", + "consent.export-profile-success": "프로필을 내보내기 합니다. 완료되면 알림을 수신합니다.", + "consent.export_uploads": "업로드한 컨텐츠 내보내기 (.zip)", + "consent.export-uploads-success": "업로드한 컨텐츠를 내보내기 합니다. 완료되면 알림을 수신합니다.", + "consent.export_posts": "포스트 내보내기 (.csv)", + "consent.export-posts-success": "포스트를 내보내기 합니다. 완료되면 알림을 수신합니다.", + "emailUpdate.intro": "아래에 이메일 주소를 입력하세요. 해당 포럼은 입력한 이메일 주소로 정기 알림 메일과 그 외의 알림을 전송하고, 계정 복구 작업에도 해당 이메일 주소를 사용합니다.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "입력하신 이메일 주소로 가입 인증 메일이 발송되었습니다. 메일 내의 링크에 접속할 경우 메일 소유자를 확인하고 계정이 활성화됩니다. 활성화 후에도 계정 페이지에서 이메일 주소를 변경할 수 있습니다.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/ko/users.json b/public/language/ko/users.json new file mode 100644 index 0000000000..10fc8c116b --- /dev/null +++ b/public/language/ko/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "최근가입순", + "top_posters": "작성글순", + "most_reputation": "인지도순", + "most_flags": "신고순", + "search": "검색", + "enter_username": "검색할 사용자명을 입력하세요.", + "search-user-for-chat": "Search a user to start chat", + "load_more": "더 보기", + "users-found-search-took": "%1명의 사용자를 찾았습니다. 검색 소요 시간 %2초", + "filter-by": "필터 기준", + "online-only": "온라인", + "invite": "초대", + "prompt-email": "이메일:", + "groups-to-join": "초대 수락 시 가입될 그룹들:", + "invitation-email-sent": "%1님에게 초대 메일을 보냈습니다.", + "user_list": "사용자 목록", + "recent_topics": "최근", + "popular_topics": "인기", + "unread_topics": "읽지 않음", + "categories": "카테고리", + "tags": "태그", + "no-users-found": "사용자를 찾을 수 없습니다!" +} \ No newline at end of file diff --git a/public/language/lt/_DO_NOT_EDIT_FILES_HERE.md b/public/language/lt/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/lt/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/lt/admin/admin.json b/public/language/lt/admin/admin.json new file mode 100644 index 0000000000..dcc3bc91b7 --- /dev/null +++ b/public/language/lt/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Ar tikrai norite perkrauti NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/lt/admin/advanced/cache.json b/public/language/lt/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/lt/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/lt/admin/advanced/database.json b/public/language/lt/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/lt/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/lt/admin/advanced/errors.json b/public/language/lt/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/lt/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/lt/admin/advanced/events.json b/public/language/lt/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/lt/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/lt/admin/advanced/logs.json b/public/language/lt/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/lt/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/lt/admin/appearance/customise.json b/public/language/lt/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/lt/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/lt/admin/appearance/skins.json b/public/language/lt/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/lt/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/lt/admin/appearance/themes.json b/public/language/lt/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/lt/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/lt/admin/dashboard.json b/public/language/lt/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/lt/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/lt/admin/development/info.json b/public/language/lt/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/lt/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/lt/admin/development/logger.json b/public/language/lt/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/lt/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/lt/admin/extend/plugins.json b/public/language/lt/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/lt/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/lt/admin/extend/rewards.json b/public/language/lt/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/lt/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/lt/admin/extend/widgets.json b/public/language/lt/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/lt/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/admins-mods.json b/public/language/lt/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/lt/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/categories.json b/public/language/lt/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/lt/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/digest.json b/public/language/lt/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/lt/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/lt/admin/manage/groups.json b/public/language/lt/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/lt/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/privileges.json b/public/language/lt/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/lt/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/registration.json b/public/language/lt/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/lt/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/tags.json b/public/language/lt/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/lt/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/uploads.json b/public/language/lt/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/lt/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/lt/admin/manage/users.json b/public/language/lt/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/lt/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/lt/admin/menu.json b/public/language/lt/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/lt/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/advanced.json b/public/language/lt/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/lt/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/api.json b/public/language/lt/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/lt/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/chat.json b/public/language/lt/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/lt/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/cookies.json b/public/language/lt/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/lt/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/email.json b/public/language/lt/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/lt/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/lt/admin/settings/general.json b/public/language/lt/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/lt/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/lt/admin/settings/group.json b/public/language/lt/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/lt/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/guest.json b/public/language/lt/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/lt/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/homepage.json b/public/language/lt/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/lt/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/languages.json b/public/language/lt/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/lt/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/navigation.json b/public/language/lt/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/lt/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/lt/admin/settings/notifications.json b/public/language/lt/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/lt/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/pagination.json b/public/language/lt/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/lt/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/post.json b/public/language/lt/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/lt/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/reputation.json b/public/language/lt/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/lt/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/social.json b/public/language/lt/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/lt/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/sockets.json b/public/language/lt/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/lt/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/sounds.json b/public/language/lt/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/lt/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/tags.json b/public/language/lt/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/lt/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/lt/admin/settings/uploads.json b/public/language/lt/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/lt/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/lt/admin/settings/user.json b/public/language/lt/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/lt/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/lt/admin/settings/web-crawler.json b/public/language/lt/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/lt/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/lt/category.json b/public/language/lt/category.json new file mode 100644 index 0000000000..fa2255f0b1 --- /dev/null +++ b/public/language/lt/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategorija", + "subcategories": "Subkategorijos", + "new_topic_button": "Nauja tema", + "guest-login-post": "Prisijungti įrašų paskelbimui", + "no_topics": "Šioje kategorijoje temų nėra.
Kodėl gi jums nesukūrus naujos?", + "browsing": "naršo", + "no_replies": "Nėra atsakymų", + "no_new_posts": "Nėra naujų pranešimų.", + "watch": "Stebėti", + "ignore": "Ignoruoti", + "watching": "Stebima", + "not-watching": "Not Watching", + "ignoring": "Ignoruojama", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Stebimos kategorijos", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/lt/email.json b/public/language/lt/email.json new file mode 100644 index 0000000000..0a19bac268 --- /dev/null +++ b/public/language/lt/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Sveiki atvykę į %1", + "invite": "Pakvietimas nuo %1", + "greeting_no_name": "Sveiki", + "greeting_with_name": "Sveiki %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Ačiū kad užsiregistravote %1", + "welcome.text2": "Kad pilnai aktyvuoti jūsų paskira, mums reikia įsitikinti kad jūs tikrai esate el.pašto valdytojas", + "welcome.text3": "Administratorius priemė jūsų prašymą prisijungti prie mūsų. Dabar galite prisijungti su savo slapyvardžiu/slaptažodžiu", + "welcome.cta": "El. adreso patvirtinimui spauskite čia", + "invitation.text1": "%1 pakvietė tave prisijungti į %2", + "invitation.text2": "Jūsų pakvietimas baigs galioti už %1 dienų.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Mes, gavome prašymą atstatyti jūsų slaptažodį, tikriausiai jūs jį pamiršote. Jeigu problema ne tame, prašome ignoruoti šį laišką", + "reset.text2": "Kad tęsti slaptažodžio atstatymą, prašome paspausti šią nuorodą", + "reset.cta": "Slaptažodžio atstatymui spauskite čia", + "reset.notify.subject": "Slaptažodis sėkmingai pakeistas", + "reset.notify.text1": "Mes tikriname ar jūs tikrai esate %1, jūsų slaptažodis buvo pakeistas sėkmingai", + "reset.notify.text2": "Jeigu jūs neprašėte šito, prašome perspėti administratoriu nedelsiant", + "digest.latest_topics": "Paskutinės temos iš %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Kad aplankyti %1, spauskite čia", + "digest.unsub.info": "Ši santrauka buvo išsiųsta į tavo prenumeratos nustatymus", + "digest.day": "diena", + "digest.week": "savaitė", + "digest.month": "mėnuo", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Nauja pokalbio žinutė gauta iš %1", + "notif.chat.cta": "Pokalbio pratęsimui spauskite čia", + "notif.chat.unsub.info": "Šios žinutės perpėjimas buvo išsiųstas į tavo prenumeratos nustatymus", + "notif.post.unsub.info": "Šios žinutės perspėjimas buvo išsiųstas į tavo prenumeratos nustatymus", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Ši žinutė yra bandomoji kad įsitikint, kad vartotojas teisingai nustatė nustatymus tavo NodeBB", + "unsub.cta": "Spauskite čia norėdami pakeisti šiuos nustatymus", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Jūs buvote užblokuotas iš %1", + "banned.text1": "Vartotojas %1 buvo užblokuotas iš %2", + "banned.text2": "Jūs užblokuotas iki %1.", + "banned.text3": "Jūsų užblokavimo priežastis:", + "closing": "Ačiū!" +} \ No newline at end of file diff --git a/public/language/lt/error.json b/public/language/lt/error.json new file mode 100644 index 0000000000..44a93c7315 --- /dev/null +++ b/public/language/lt/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Klaidingi duomenys", + "invalid-json": "Nevalidus JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Atrodo, kad jūs neesate prisijungęs.", + "account-locked": "Jūsų paskyra buvo laikinai užrakinta", + "search-requires-login": "Paieška reikalauja vartotojo - prašome prisijungti arba užsiregistruoti", + "goback": "Spauskite atgal, norėdami grįžti į praeitą puslapį", + "invalid-cid": "Klaidingas kategorijos ID", + "invalid-tid": "Klaidingas temos ID", + "invalid-pid": "Klaidingas pranešimo ID", + "invalid-uid": "Klaidingas vartotojo ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Klaidingas vartotojo vardas", + "invalid-email": "Klaidingas el. pašto adresas", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Nevalidus pavadinimas", + "invalid-user-data": "Klaidingi vartotojo duomenys", + "invalid-password": "Klaidingas slaptažodis", + "invalid-login-credentials": "Blogi prisijungimo duomenys", + "invalid-username-or-password": "Prašome nurodyti tiek vartotojo vardą, tiek ir slaptažodį", + "invalid-search-term": "Neteisingas paieškos terminas", + "invalid-url": "Neteisingas URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Vietinė prisijungimo sistema išjungta neprivilegijuotoms paskyroms.", + "csrf-invalid": "Nepavyko jūsų prijungti tikriausiai dėl pasibaigusios sesijos. Bandykite dar kartą", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Bloga puslapių išdėstymo reikšmė. Ji turėtų būti ne mažesnė nei %1 ir ne didesnė nei %2", + "username-taken": "Vartotojo vardas jau užimtas", + "email-taken": "El. pašto adresas jau užimtas", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Jūs negalite bendrauti, kol jūsų el.paštas nėra patvirtintas, prašome spausti čia kad aktyvuoti jūsų el.paštą", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Negalime patvirtinti jūsų el. adreso, prašom bandyti vėliau.", + "confirm-email-already-sent": "Patvirtinimo laiškas išsiųstas, prašome palaukti %1 minute(s) kad išsiųstume kita", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Slapyvardis per trumpas", + "username-too-long": "Vartotojo vardas per ilgas", + "password-too-long": "Slaptažodis per ilgas", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Vartotojas užblokuotas", + "user-banned-reason": "Atsiprašome, ši paskyra buvo užblokuota (Priežastis: %1)", + "user-banned-reason-until": "Atsiprašome, ši paskyra užblokuota iki %1 (Priežastis: %2)", + "user-too-new": "Atsiprašome, jūs įpareigoti palaukti %1 sekunde(s) prieš rašant pirmą pranešimą", + "blacklisted-ip": "Atsiprašome, jūsų IP adresas yra užblokuotas. Jei manote, jog tai klaida, susisiekite su administratoriumi.", + "ban-expiry-missing": "Užpildykite šio blokavimo pabaigos datą", + "no-category": "Tokios kategorijos nėra", + "no-topic": "Tokios temos nėra", + "no-post": "Tokio įrašo nėra", + "no-group": "Grupė neegzistuoja", + "no-user": "Tokio vartotojo nėra", + "no-teaser": "Anonsas neegzistuoja", + "no-flag": "Flag does not exist", + "no-privileges": "Šiam veiksmui jūs neturite pakankamų privilegijų.", + "category-disabled": "Kategorija išjungta", + "topic-locked": "Tema užrakinta", + "post-edit-duration-expired": "Jums galima redaguoti pranešims tik %1 sekunde(s) po parašymo", + "post-edit-duration-expired-minutes": "Redaguoti įrašus galima %1 minutę(-es) po paskelbimo", + "post-edit-duration-expired-minutes-seconds": "Redaguoti įrašus galima %1 minutę(-es) %2 sekundę(-es) po paskelbimo", + "post-edit-duration-expired-hours": "Redaguoti įrašus galima %1 valandą(-as) po paskelbimo", + "post-edit-duration-expired-hours-minutes": "Redaguoti įrašus galima %1 valandą(-as) %2 minutę(-es) po paskelbimo", + "post-edit-duration-expired-days": "Redaguoti įrašus galima %1 dieną(-as) po paskelbimo", + "post-edit-duration-expired-days-hours": "Redaguoti įrašus galima %1 dieną(-as) %2 valandą(-as) po paskelbimo", + "post-delete-duration-expired": "Trinti įrašus galima %1 sekundę(-es) po paskelbimo", + "post-delete-duration-expired-minutes": "Trinti įrašus galima %1 minutę(-es) po paskelbimo", + "post-delete-duration-expired-minutes-seconds": "Trinti įrašus galima %1 sekundę(-es) %2 sekundę(-es) po paskelbimo", + "post-delete-duration-expired-hours": "Trinti įrašus galima %1 valandą(-as) po paskelbimo", + "post-delete-duration-expired-hours-minutes": "Trinti įrašus galima %1 valandą(-as) %2 minutę(-es) po paskelbimo", + "post-delete-duration-expired-days": "Trinti įrašus galima %1 dieną(-as) po paskelbimo", + "post-delete-duration-expired-days-hours": "Trinti įrašus galima %1 dieną(-as) %2 valand1(-as) po paskelbimo", + "cant-delete-topic-has-reply": "Negalima ištrinti temos, kai yra atsakymų į ją", + "cant-delete-topic-has-replies": "Negalima ištrinti temos, kai į ji turi %1 atsakymų", + "content-too-short": "Prašome parašyti ilgesni pranešimą. Pranešimas turi sudaryti mažiausiai %1 simboli(us)", + "content-too-long": "Prašome parašyti trumpesnį pranešimą. Pranešimas negali sudaryti daugiau %1 simboli(us)", + "title-too-short": "Prašome įvesti ilgesni pavadinimą. Pavadinimas turi sudaryti mažiausiai %1 simboli(us)", + "title-too-long": "Prašome įvesti trumpersnį pavadinimą. Pavadinimas negali sudaryti daugiau %1 simboli(us)", + "category-not-selected": "Nepasirinkta kategorija.", + "too-many-posts": "Jus galite rašyti kas %1 sekunde(s) - prašome palaukti prieš rašant dar kartą", + "too-many-posts-newbie": "Kadangi esate naujas narys, jūs galite tik rašyti kas %1 sekunde(s) kol jūs pasieksite %2 reputacija - prašome palaukti prieš rašant dar kartą", + "already-posting": "You are already posting", + "tag-too-short": "Prašome įvesti ilgesnę žymą. Žyma turi sudaryti mažiausiai %1 simboli(us)", + "tag-too-long": "Prašome įvesti trumpesnę žymą. Žyma turi būti ne ilgesni negu %1 simboli(us)", + "not-enough-tags": "Neužteka žymių. Temos turi turėti mažiausiai %1 žyme(s)", + "too-many-tags": "Per daug žymių. Temos turi turėti daugiausiai %1 žyme(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Prašome palaukti kol bus baigti visi kėlimai į serverį", + "file-too-big": "Didžiausias įkelimo dydis yra %1 kB - prašome kelti mažesni failą", + "guest-upload-disabled": "Failų įkėlimas svečiams išjungtas", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Jūs jau turite žymekelį šiam įrašui", + "already-unbookmarked": "Jūs jau nuėmėte žymeklį šiam įrašui", + "cant-ban-other-admins": "Jūs negalite užblokuoti kitų administratorių!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Jūs esate vienintelis administratorius. Pridėkite kitą vartotoja kaip administratorių prieš pašalindamas save", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Pašalinkite administratoriaus teises šiai paskyrai prieš bandydami ją ištrinti.", + "already-deleting": "Already deleting", + "invalid-image": "Blogas paveikslėlis", + "invalid-image-type": "Neteisingas vaizdo tipas. Leidžiami tipai :%1", + "invalid-image-extension": "Neteisingas vaizdo plėtinys", + "invalid-file-type": "Neteisingas failo tipas. Leidžiami tipai: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Grupės pavadinimas per trumpas", + "group-name-too-long": "Grupės pavadinimas per ilgas", + "group-already-exists": "Tokia grupė jau egzistuoja", + "group-name-change-not-allowed": "Grupės pavadinimas keitimas neleidžiamas", + "group-already-member": "Jau yra šios grupės dalis", + "group-not-member": "Ne šios grupės narys", + "group-needs-owner": "Ši grupė reikalauja mažiausiai vieno savininko", + "group-already-invited": "Šis vartotojas jau buvo pakviestas", + "group-already-requested": "Jūsų prašymas dėl narystes jau buvo pateiktas", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Šis įrašas jau buvo ištrintas", + "post-already-restored": "Šis įrašas jau atstatytas", + "topic-already-deleted": "Ši tema jau ištrinta", + "topic-already-restored": "Ši tema jau atkurta", + "cant-purge-main-post": "Jūs negalite išvalyti pagrindinio pranešimo, prašome ištrinkite temą nedelsiant", + "topic-thumbnails-are-disabled": "Temos paveikslėliai neleidžiami.", + "invalid-file": "Klaidingas failas", + "uploads-are-disabled": "Įkėlimai neleidžiami", + "signature-too-long": "Atsiprašome, bet jūsų parašas negali būti ilgesnis negu %1 simbolis (ių)", + "about-me-too-long": "Atsiprašome, bet aprašymas apie jus negali būti ilgesnis negu %1 simbolis (ių)", + "cant-chat-with-yourself": "Jūs negalite susirašinėti su savimi!", + "chat-restricted": "Šis vartotojas apribojo savo žinutes. Jie turi sekti jus kad jūs galėtumėte pradėti bendrauti su jais", + "chat-disabled": "Susirašinėjimų sistema išjungta", + "too-many-messages": "Išsiuntėte per daug pranešimų, kurį laiką prašome palaukti.", + "invalid-chat-message": "Bloga žinutė", + "chat-message-too-long": "Žinutės negali būti ilgesnės nei %1 simbolių.", + "cant-edit-chat-message": "Jūs neturite teisės redaguoti šios žinutės", + "cant-delete-chat-message": "Jūs neturite teisės trinti šios žinutės", + "chat-edit-duration-expired": "Redaguoti susirašinėjimo žinutes galima tik %1 sekundę(-es/-ių) po paskelbimo", + "chat-delete-duration-expired": "Trinti žinutes galima tik %1 sekundę(-es/-ių) po paskelbimo", + "chat-deleted-already": "Ši žinutė buvo pašalinta", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Jūs jau balsavote už šį pranešimą.", + "reputation-system-disabled": "Reputacijos sistema išjungta.", + "downvoting-disabled": "Downvoting yra išjungtas", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Negalima balsuoti už savo įrašą", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB susidūrė su problema persikraunant: \"%1\", NodeBB pratęs veikti su šiuo klientu. bet jums reiktu patikrinti ką jūs darėte prieš perkraunant NodeBB", + "registration-error": "Registracijos klaida", + "parse-error": "Kažkokia klaida įvyko bandant gaut serverio atsaykmą", + "wrong-login-type-email": "Prisijungimui prašom naudoti jūsų el. adresą", + "wrong-login-type-username": "Prisijungimui prašome naudoti vartotojo vardą", + "sso-registration-disabled": "Reputacija išjungta %1 paskyroms. Prašome pirmiausiai užsiregistruoti su el. paštu", + "sso-multiple-association": "Jūs negalite sujungti kelių paskyrų iš šio serviso su NodeBB paskyra. Prašome atskirti egzistuojančią paskyrą ir bandyti iš naujo.", + "invite-maximum-met": "Jūs pakvietėte maksimalų skaičių žmonių (%1 iš %2).", + "no-session-found": "Prisijungimo sesija nerasta!", + "not-in-room": "Vartotojas ne svetainėje", + "cant-kick-self": "Negalite išmesti savęs iš grupės", + "no-users-selected": "Nepasirinktas joks vartotojas", + "invalid-home-page-route": "Blogas kelias į pagrindinį puslapį", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Nepasirinkta jokia tema!", + "cant-move-to-same-topic": "Negalima perkelti įrašo į tą pačią temą!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Savęs užblokuoti negalima!", + "cannot-block-privileged": "Negalima blokuoti administratorių arba visuotinių moderatorių", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "Panašu, jog yra problema su jūsų interneto prieiga", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/lt/flags.json b/public/language/lt/flags.json new file mode 100644 index 0000000000..c506a1a994 --- /dev/null +++ b/public/language/lt/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Atnaujinti", + "updated": "Atnaujinta", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "Visas turinys", + "filter-type-post": "Pranešimas", + "filter-type-user": "Vartotojas", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Atmesta", + "no-assignee": "Nepriskirta", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Šlamštas", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/lt/global.json b/public/language/lt/global.json new file mode 100644 index 0000000000..00a6bde041 --- /dev/null +++ b/public/language/lt/global.json @@ -0,0 +1,126 @@ +{ + "home": "Namai", + "search": "Ieškoti", + "buttons.close": "Uždaryti", + "403.title": "Prieiga negalima", + "403.message": "Matosi užklupai į ta puslapį kur neturi tam tikrų teisių jį peržiūrėti", + "403.login": "Tikriausiai tu turėtum pabandyt prisijungt?", + "404.title": "Nerasta", + "404.message": "Pasirodo sėdi puslapyje kurio net nėra. Grįžk į namų puslapį.", + "500.title": "Internal Error.", + "500.message": "Oops! Atrodo, kad kažkas nutiko!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Registruotis", + "login": "Prisijungti", + "please_log_in": "Prašome prisijungti", + "logout": "Atsijungti", + "posting_restriction_info": "Naujų pranešimų kūrimas galimas tik registruotiems vartotojams. Spauskite čia norėdami prisijungti.", + "welcome_back": "Sveiki sugrįžę", + "you_have_successfully_logged_in": "Jūs sėkmingai prisijungėte", + "save_changes": "Išsaugoti pakeitimus", + "save": "Save", + "close": "Uždaryti", + "pagination": "Numeracija", + "pagination.out_of": "%1 iš %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Administratorius", + "header.categories": "Kategorijos", + "header.recent": "Naujausi", + "header.unread": "Neskaityti", + "header.tags": "Žymos", + "header.popular": "Populiarūs", + "header.top": "Top", + "header.users": "Vartotojai", + "header.groups": "Grupės", + "header.chats": "Susirašinėjimai", + "header.notifications": "Pranešimai", + "header.search": "Ieškoti", + "header.profile": "Profilis", + "header.navigation": "Navigation", + "notifications.loading": "Įkeliami pranešimai", + "chats.loading": "Įkeliami susirašinėjimai", + "motd.welcome": "Sveiki atvykę į NodeBB- ateities diskusijų platformą.", + "previouspage": "Ankstesnis puslapis", + "nextpage": "Kitas puslapis", + "alert.success": "Pavyko", + "alert.error": "Klaida", + "alert.banned": "Užblokuotas", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Jūs jau nebesekate %1! ", + "alert.follow": "Jūs sekate vartotoją %1!", + "users": "Vartotojai", + "topics": "Temos", + "posts": "Pranešimai", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Peržiūros", + "posters": "Posters", + "reputation": "Reputacija", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "skaityti plačiau", + "more": "Daugiau", + "none": "None", + "posted_ago_by_guest": "parašyta %2 nuo svečio", + "posted_ago_by": "parašyta %1 nuo %2", + "posted_ago": "parašyta %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "parašyta temoje %1 %2", + "posted_in_ago_by": "parašyta temoje %1 %2 nuo %3", + "user_posted_ago": "%1 parašė %2", + "guest_posted_ago": "Svečias parašė %1", + "last_edited_by": "last edited by %1", + "norecentposts": "Paskutinių pranešimų nėra.", + "norecenttopics": "Paskutinių temų nėra", + "recentposts": "Paskutiniai pranešimai", + "recentips": "Paskutiniai prisijungimų IP adresai", + "moderator_tools": "Moderator Tools", + "online": "Prisijungęs", + "away": "Pasišalinęs", + "dnd": "Do not disturb", + "invisible": "Nematomas", + "offline": "Atsijungęs", + "email": "El. paštas", + "language": "Kalba", + "guest": "Svečias", + "guests": "Svečiai", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forumas atnaujintas", + "updated.message": "Forumas buvo atnaujintas iki naujausios versijos. Paspauskite čia norėdami perkrauti puslapį.", + "privacy": "Privatumas", + "follow": "Sekti", + "unfollow": "Nebesekti", + "delete_all": "Viską ištrinti", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/lt/groups.json b/public/language/lt/groups.json new file mode 100644 index 0000000000..fc4f6291c4 --- /dev/null +++ b/public/language/lt/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupės", + "view_group": "Grupės peržiūra", + "owner": "Grupės savininkas", + "new_group": "Kurti naują grupę", + "no_groups_found": "Nėra grupių kurias būtu galima matyti", + "pending.accept": "Priimti", + "pending.reject": "Atmesti", + "pending.accept_all": "Priimti visus", + "pending.reject_all": "Atmesti visus", + "pending.none": "Nėra pretenduojančių narių šiuo momentu", + "invited.none": "Nėra pakviestu narių šiuo momentu", + "invited.uninvite": "Atšaukti pakvietimą", + "invited.search": "Ieškoti nario kad pakviesti į šią grupę", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Saugoti", + "cover-saving": "Išsaugoma", + "details.title": "Grupės detalės", + "details.members": "Narių sąrašas", + "details.pending": "Laukiantys nariai", + "details.invited": "Pakviesti nariai", + "details.has_no_posts": "Šios grupės nariai neatliko jokių įrašų.", + "details.latest_posts": "Vėliausi įrašai", + "details.private": "Asmeniška", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Duoti/Atšaukti Nuosavybę", + "details.kick": "Išmesti", + "details.kick_confirm": "Ar tikrai šį narį norite pašalinti iš grupės?", + "details.add-member": "Add Member", + "details.owner_options": "Grupės Administratorius", + "details.group_name": "Grupės pavadinimas", + "details.member_count": "Narių skaičiuotuvas", + "details.creation_date": "Sukūrimo Data", + "details.description": "Aprašymas", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Ženklelio Peržiūra", + "details.change_icon": "Pakeisti paveikslėlį", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Ženklelio Tekstas", + "details.userTitleEnabled": "Parodyti Ženklelį", + "details.private_help": "Jeigu įjungta, prisijungt prie grupių reikalingas patvirtinimas iš grupės administratoriaus", + "details.hidden": "Paslėptas", + "details.hidden_help": "Jeigu įjungta, ši grupė bus nerodo grupių sąraše, ir vartotojus reikės kviest rankiniu būdu", + "details.delete_group": "Ištrinti grupe", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Grupės informacija atnaujinta", + "event.deleted": "Grupė \"%1\" pašalinta", + "membership.accept-invitation": "Priimti Kvietimą", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Pakvietimas Laukiamas", + "membership.join-group": "Prisijungti Prie Grupės", + "membership.leave-group": "Palikti Grupę", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Atšaukti", + "new-group.group_name": "Grupės pavadinimas:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/lt/ip-blacklist.json b/public/language/lt/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/lt/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/lt/language.json b/public/language/lt/language.json new file mode 100644 index 0000000000..2c04906b94 --- /dev/null +++ b/public/language/lt/language.json @@ -0,0 +1,5 @@ +{ + "name": "Lietuvių", + "code": "lt", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/lt/login.json b/public/language/lt/login.json new file mode 100644 index 0000000000..b51df6c453 --- /dev/null +++ b/public/language/lt/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Vartotojo vardas / El. paštas", + "username": "Vartotojo vardas", + "remember_me": "Prisiminti?", + "forgot_password": "Užmiršote slaptažodį?", + "alternative_logins": "Alternatyvūs prisijungimo būdai", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "Jūs sėkmingai prisijungėte!", + "dont_have_account": "Neturite paskyros?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json new file mode 100644 index 0000000000..c9e20e68ec --- /dev/null +++ b/public/language/lt/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Siųsti", + "chat.no_active": "Jūs neturite aktyvių susirašinėjimų.", + "chat.user_typing": "%1 dabar rašo...", + "chat.user_has_messaged_you": "%1 parašė jums.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Prašome pasirikti gavėją, norėdami peržiūrėti žinučių istoriją", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Paskutiniai susirašinėjimai", + "chat.contacts": "Kontaktai", + "chat.message-history": "Žinučių istorija", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Iššokančio lango pokalbiai", + "chat.minimize": "Minimize", + "chat.maximize": "Padininti", + "chat.seven_days": "7 dienos", + "chat.thirty_days": "30 dienų", + "chat.three_months": "3 mėnesiai", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Sukomponuoti", + "composer.show_preview": "Rodyti pavyzdį", + "composer.hide_preview": "Slėpti pavyzdį", + "composer.user_said_in": "%1 parašė į %2:", + "composer.user_said": "%1 parašė:", + "composer.discard": "Ar tikrai norite sunaikinti šį pranešimą?", + "composer.submit_and_lock": "Pateikti ir užrakinti", + "composer.toggle_dropdown": "Perjungti Nukritimą", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/lt/notifications.json b/public/language/lt/notifications.json new file mode 100644 index 0000000000..7333c15855 --- /dev/null +++ b/public/language/lt/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Pranešimai", + "no_notifs": "Jūs neturite naujų pranešimų", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Atgal į %1", + "outgoing_link": "Išeinanti nuoroda", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "Tęsti į %1", + "return_to": "Grįžti į %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Jūs turite neperskaitytų pranešimų.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Nauja žinutė nuo %1", + "upvoted_your_post_in": "%1 užbalsavo už jūsų pranešima čia %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1pagrįso nuomone čia %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 parašė atsaką %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 paskelbė naują temą: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 pradėjo sekti tave", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 atsiuntė registracijos prašymą", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "El. paštas patvirtintas", + "email-confirmed-message": "Dėkojame už el. pašto patvirtinimą. Jūsų paskyra pilnai aktyvuota.", + "email-confirm-error-message": "Įvyko klaida mėginant patvirtinti Jūsų el. pašto adresą. Galbūt kodas yra neteisingas, arba nebegalioajantis.", + "email-confirm-sent": "Patvirtinimo laiškas išsiųstas.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/lt/pages.json b/public/language/lt/pages.json new file mode 100644 index 0000000000..a1f10b4416 --- /dev/null +++ b/public/language/lt/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Namai", + "unread": "Neskaitytos temos", + "popular-day": "Populiarios temos šiandien", + "popular-week": "Populiarios temos šią savaitę", + "popular-month": "Populiarios temos šį mėnesį", + "popular-alltime": "Visų laikų populiarios temos", + "recent": "Paskutinės temos", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderavimo įrankiai", + "flagged-content": "Pažymėtas turinys", + "ip-blacklist": "IP Juodasis Sąrašas", + "post-queue": "Įrašų eilė", + "users/online": "Prisijungę vartotojai", + "users/latest": "Naujausi vartotojai", + "users/sort-posts": "Vartotojai, turintis daugiausiai įrašų", + "users/sort-reputation": "Vartotojai, turintys geriausią reputaciją", + "users/banned": "Blokuoti vartotojai", + "users/most-flags": "Daugiausiai pažymėti vartotojai", + "users/search": "Vartotojų paieška", + "notifications": "Pranešimai", + "tags": "Žymos", + "tag": "Temos, pažymėtos "%1"", + "register": "Registruoti paskyrą", + "registration-complete": "Registracija baigta", + "login": "Prisijunkite į savo paskyrą", + "reset": "Atstatyti savo paskyros slaptažodį", + "categories": "Kategorijos", + "groups": "Grupės", + "group": "%1 grupė", + "chats": "Susirašinėjimai", + "chat": "Susirašinėja su %1", + "flags": "Vėliavos", + "flag-details": "Flag %1 Details", + "account/edit": "Redaguoja \"%1\"", + "account/edit/password": "Redaguoja \"%1\" slaptažodį", + "account/edit/username": "redaguoja \"%1\" vartotojo vardą", + "account/edit/email": "Redaguoja \"%1\" el. paštą", + "account/info": "Paskyros informacija", + "account/following": "Vartotojas %1 seka", + "account/followers": "Žmonės, kurie seka %1", + "account/posts": "Pranešimai, kuriuos parašė %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Temos, kurias sukūrė %1", + "account/groups": "%1 Grupės", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Vartotojo nustatymai", + "account/watched": "Temos stebimos %1", + "account/ignored": "Temos ignoruojamos %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Geriausi pranešimai, kuriuos parašė %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Užblokuoti vartotojai dėl %1", + "account/uploads": "%1 Įkėlimai", + "account/sessions": "Login Sessions", + "confirm": "El. paštas patvirtintas", + "maintenance.text": "%1 dabar atnaujinimo darbuose. Prašome grįžti vėliau", + "maintenance.messageIntro": "Be to, administratorius paliko šį pranešimą:", + "throttled.text": "%1 dabar nepasiekiamas dėl per didelės apkrovos. Prašome sugrįžti vėliau." +} \ No newline at end of file diff --git a/public/language/lt/post-queue.json b/public/language/lt/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/lt/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/lt/recent.json b/public/language/lt/recent.json new file mode 100644 index 0000000000..bebb9353fa --- /dev/null +++ b/public/language/lt/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Naujausi", + "day": "Diena", + "week": "Savaitė", + "month": "Mėnesis", + "year": "Metai", + "alltime": "Per visą laiką", + "no_recent_topics": "Paskutinių temų nėra", + "no_popular_topics": "Populiarių temų nėra.", + "there-is-a-new-topic": "Yra nauja tema.", + "there-is-a-new-topic-and-a-new-post": "Yra nauja tema ir naujas įrašas.", + "there-is-a-new-topic-and-new-posts": "Yra nauja tema ir %1 nauji įrašai.", + "there-are-new-topics": "Yra %1 naujos temos.", + "there-are-new-topics-and-a-new-post": "Yra %1 naujos temos ir naujas įrašas.", + "there-are-new-topics-and-new-posts": "Yra %1 naujos temos ir %2 nauji įrašai.", + "there-is-a-new-post": "Yra naujas įrašas.", + "there-are-new-posts": "Yra %1 naujas pranešimas.", + "click-here-to-reload": "Spauskite čia norėdami perkrauti." +} \ No newline at end of file diff --git a/public/language/lt/register.json b/public/language/lt/register.json new file mode 100644 index 0000000000..28698ba604 --- /dev/null +++ b/public/language/lt/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registruotis", + "cancel_registration": "Cancel Registration", + "help.email": "Pagal nutylėjimą, jūsų el. paštas nebus viešai matomas.", + "help.username_restrictions": "Unikalus vartotojo vardas %1-%2 simbolių ilgio. Kiti vartotojai galės jus minėti @vartotojas.", + "help.minimum_password_length": "Jūsų slaptažodis turi būti mažiausiai %1 simbolių.", + "email_address": "El. paštas", + "email_address_placeholder": "Įrašykite el. pašto adresą", + "username": "Vartotojo vardas", + "username_placeholder": "Įrašykite vartotojo vardą", + "password": "Slaptažodis", + "password_placeholder": "Įrašykite slaptažodį", + "confirm_password": "Patvirtinkite slaptažodį", + "confirm_password_placeholder": "Patvirtinkite slaptažodį", + "register_now_button": "Registruotis", + "alternative_registration": "Alternatyvūs registracijos būdai", + "terms_of_use": "Naudojimo sąlygos", + "agree_to_terms_of_use": "Aš sutinku su vartojimo sąlygomis", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Jūsų registracijos prašymas buvo pridėtas į laukiančiųjų sąrašą. Jūs gausite el.paštu laišką kada administratorius patvirtins jus", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/lt/reset_password.json b/public/language/lt/reset_password.json new file mode 100644 index 0000000000..4341a986f4 --- /dev/null +++ b/public/language/lt/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Atstatyti slaptažodį", + "update_password": "Atnaujinti slaptažodį", + "password_changed.title": "Slaptažodis pakeistas", + "password_changed.message": "

Slaptažodis sėkmingai pakeistas, prašome prisijungti.", + "wrong_reset_code.title": "Neteisingas atstatymo kodas", + "wrong_reset_code.message": "Neteisingas atstatymo kodas. Prašome bandyti dar kartą arba prašyti naujo atstatymo kodo.", + "new_password": "Naujas slaptažodis", + "repeat_password": "Patvirtinkite slaptažodį", + "changing_password": "Changing Password", + "enter_email": "Prašome įrašyti el. pašto adresą ir mes atsiųsime jums instrukciją, kaip atstatyti jūsų paskyrą.", + "enter_email_address": "Įrašykite el. pašto adresą", + "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.", + "invalid_email": "Klaidingas arba neegzistuojantis el. pašto adresas!", + "password_too_short": "Įvestas slaptažodis yra per trumpas, prašome pasirinkti kitą slaptažodį.", + "passwords_do_not_match": "Du slaptažodžiai, kuriuos įvedėte, nesutampa.", + "password_expired": "Jūsų slaptažodžio laikas baigėsi, pasirinkite nauja slaptažodį" +} \ No newline at end of file diff --git a/public/language/lt/search.json b/public/language/lt/search.json new file mode 100644 index 0000000000..32f1c16731 --- /dev/null +++ b/public/language/lt/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 rezultatas(ai) atitinka \"%2\", (%3 sekundes)", + "no-matches": "Atitikmenų nerasta", + "advanced-search": "Išplėstinė paieška", + "in": "Į", + "titles": "Antraštės", + "titles-posts": "Antraštės ir įrašai", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Parašė", + "in-categories": "Kategorijose", + "search-child-categories": "Ieškoti vaikų kategorijas", + "has-tags": "Has tags", + "reply-count": "Atsakymų skaičiavimas", + "at-least": "Mažiausiai", + "at-most": "Daugiausia", + "relevance": "Relevance", + "post-time": "Įrašo laikas", + "votes": "Votes", + "newer-than": "Naujesni kaip", + "older-than": "Senesni kaip", + "any-date": "Bet kokia data", + "yesterday": "Vakar", + "one-week": "Viena savaitė", + "two-weeks": "Dvi savaitės", + "one-month": "Mėnuo", + "three-months": "Trys mėnesiai", + "six-months": "Šeši mėnesiai", + "one-year": "Vieneri metai", + "sort-by": "Rūšiuoti pagal", + "last-reply-time": "Paskutinis atsakymo laikas", + "topic-title": "Temos pavadinimas", + "topic-votes": "Topic votes", + "number-of-replies": "Atsakymų skaičius", + "number-of-views": "Peržiūrų skaičius", + "topic-start-date": "Temos pradžia", + "username": "Vartotojo vardas", + "category": "Kategorija", + "descending": "Mažėjančia tvarka", + "ascending": "Didėjančia tvarka", + "save-preferences": "Išsaugoti nustatymus", + "clear-preferences": "Išvalyti nustatymus", + "search-preferences-saved": "Paieškos nustatymai išsaugoti", + "search-preferences-cleared": "Paieškos nuostatos išvalytos", + "show-results-as": "Rodyti rezultatus kaip", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/lt/success.json b/public/language/lt/success.json new file mode 100644 index 0000000000..818476c6a5 --- /dev/null +++ b/public/language/lt/success.json @@ -0,0 +1,7 @@ +{ + "success": "Pavyko", + "topic-post": "Sėkmingai parašėte pranešimą", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Autentifikacija sėkminga", + "settings-saved": "Nustatymai išsaugoti!" +} \ No newline at end of file diff --git a/public/language/lt/tags.json b/public/language/lt/tags.json new file mode 100644 index 0000000000..0ed13c37b6 --- /dev/null +++ b/public/language/lt/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Temų su šią žyma nėra.", + "tags": "Žymos", + "enter_tags_here": "Įveskite žymas čia, tarp %1 ir %2 simbolių kiekvienam", + "enter_tags_here_short": "Įveskite žymas...", + "no_tags": "Žymų kolkas nėra.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/lt/top.json b/public/language/lt/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/lt/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/lt/topic.json b/public/language/lt/topic.json new file mode 100644 index 0000000000..18c6539c0c --- /dev/null +++ b/public/language/lt/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tema", + "title": "Title", + "no_topics_found": "Temų nerasta!", + "no_posts_found": "Įrašų nerasta!", + "post_is_deleted": "Šis įrašas ištrintas!", + "topic_is_deleted": "Ši tema yra ištrinta!", + "profile": "Profilis", + "posted_by": "Parašė %1", + "posted_by_guest": "Parašė svečias", + "chat": "Susirašinėti", + "notify_me": "Gauti pranešimus apie naujus atsakymus šioje temoje", + "quote": "Cituoti", + "reply": "Atsakyti", + "replies_to_this_post": "%1 atsakymai", + "one_reply_to_this_post": "1 Atsakymas", + "last_reply_time": "Paskutinis atsakymas", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Norėdami atsakyti, prisijunkite", + "login-to-view": "🔒 Log in to view", + "edit": "Redaguoti", + "delete": "Ištrinti", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Išvalyti", + "restore": "Atkurti", + "move": "Perkelti", + "change-owner": "Change Owner", + "fork": "Išskaidyti", + "link": "Nuoroda", + "share": "Dalintis", + "tools": "Įrankiai", + "locked": "Užrakinta", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Perkelta", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Blokuoti IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Ši tema buvo ištrinta. Tik Vartotojai su temos redagavimo privilegijomis gali matyti ja", + "following_topic.message": "Dabar jūs gausite pranešimus kai kas nors atrašys šioje temoje.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Norėdami prenumeruoti šią temą, prašome prisiregistruoti arba prisijungti.", + "markAsUnreadForAll.success": "Tema visiems vartotojams pažymėta kaip neskaityta.", + "mark_unread": "Mark unread", + "mark_unread.success": "Tema pažymėta kaip neskaityta.", + "watch": "Žiūrėti", + "unwatch": "Nebesekti", + "watch.title": "Gauti pranešimą apie naujus įrašus šioje temoje", + "unwatch.title": "Baigti šios temos stebėjimą", + "share_this_post": "Dalintis šiuo įrašu", + "watching": "Stebima", + "not-watching": "Not Watching", + "ignoring": "Ignoruojama", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Temos priemonės", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Prisegti temą", + "thread_tools.unpin": "Atsegti temą", + "thread_tools.lock": "Užrakinti temą", + "thread_tools.unlock": "Atrakinti temą", + "thread_tools.move": "Perkelti temą", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Perkelti visus", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Išskaidyti temą", + "thread_tools.delete": "Ištrinti temą", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Ar jūs tikrai norite ištrinti šią temą?", + "thread_tools.restore": "Atkurti temą", + "thread_tools.restore_confirm": "Ar jūs tikrai norite atkurti šią temą?", + "thread_tools.purge": "Išvalyti temą", + "thread_tools.purge_confirm": "Ar tikrai norite išvalyti šią temą?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Ar jūs tikrai norite ištrinti šį įrašą?", + "post_restore_confirm": "Ar jūs tikrai norite atkurti šį įrašą?", + "post_purge_confirm": "Ar tikrai norite išvalyti šį pranešimą?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Įkeliamos kategorijos", + "confirm_move": "Perkelti", + "confirm_fork": "Išskaidyti", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Įkeliama daugiau įrašų", + "move_topic": "Perkelti temą", + "move_topics": "Perkelti temas", + "move_post": "Perkelti įrašą", + "post_moved": "Pranešimas perkeltas!", + "fork_topic": "Išskaidyti temą", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Pažymėkite ant įrašų, kuriuos norite perkelti į naują temą", + "fork_no_pids": "Nepasirinktas joks įrašas!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Sėkmingai išsišakota iš temos! Spausk čia kad nueitu į išsišakota temą", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Įrašykite temos pavadinimą...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Atšaukti", + "composer.submit": "Patvirtinti", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Atsakymas %1", + "composer.new_topic": "Nauja tema", + "composer.editing": "Editing", + "composer.uploading": "įkeliama...", + "composer.thumb_url_label": "Įklijuokite temos paveikslėlio URL", + "composer.thumb_title": "Pridėti paveikslėlį šiai temai", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Arba įkelkite failą", + "composer.thumb_remove": "Ištuštinti laukus", + "composer.drag_and_drop_images": "Nutempkite paveikslėlius čia", + "more_users_and_guests": "dar %1 vartotojai(-ų) ir %2 svečiai(-ių)", + "more_users": "dar %1 vartotojai(-ų)", + "more_guests": "dar %1 svečiai(-ių)", + "users_and_others": "%1 ir kiti %2", + "sort_by": "Rūšiuoti pagal", + "oldest_to_newest": "Nuo seniausių iki naujausių", + "newest_to_oldest": "Nuo naujausių iki seniausių", + "most_votes": "Daugiausiai Balsų", + "most_posts": "Daugiausiai Įrašų", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Sukurti naują temą", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/lt/unread.json b/public/language/lt/unread.json new file mode 100644 index 0000000000..b0dc056b15 --- /dev/null +++ b/public/language/lt/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Neskaityti", + "no_unread_topics": "Neskaitytų temų nėra.", + "load_more": "Įkelti daugiau", + "mark_as_read": "Pažymėti kaip perskaitytus", + "selected": "Pasirinkti", + "all": "Visi", + "all_categories": "Visos kategorijos", + "topics_marked_as_read.success": "Temos pažymėtos kaip perskaitytos.", + "all-topics": "Visos Temos", + "new-topics": "Naujos Temos", + "watched-topics": "Peržiūrėtos Temos", + "unreplied-topics": "Neatsakytos Temos", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/lt/uploads.json b/public/language/lt/uploads.json new file mode 100644 index 0000000000..56b31c57a4 --- /dev/null +++ b/public/language/lt/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "įkeliama...", + "select-file-to-upload": "Pasirinkite failą, kurį norite įkelti.", + "upload-success": "Failas įkeltas sėkmingai!", + "maximum-file-size": "Daugiausiai %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/lt/user.json b/public/language/lt/user.json new file mode 100644 index 0000000000..cadb36a6e9 --- /dev/null +++ b/public/language/lt/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Užblokuotas", + "muted": "Muted", + "offline": "Atsijungęs", + "deleted": "Ištrinti", + "username": "Vartotojo vardas", + "joindate": "Prisijungimo data", + "postcount": "Įrašų kiekis", + "email": "El. paštas", + "confirm_email": "Patvirtinti el. paštą", + "account_info": "Paskyros informacija", + "admin_actions_label": "Administrative Actions", + "ban_account": "Užblokuoti Paskyrą", + "ban_account_confirm": "Jūs tikrai norite užblokuoti šį vartotoją?", + "unban_account": "Atblokuoti Paskyrą", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Ištrinti paskyrą", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Paskyra ištrinta", + "account-content-deleted": "Account content deleted", + "fullname": "Vardas ir pavardė", + "website": "Tinklalapis", + "location": "Vieta", + "age": "Amžius", + "joined": "Prisijungė", + "lastonline": "Paskutinį kartą prisijungė", + "profile": "Profilis", + "profile_views": "Profilio peržiūros", + "reputation": "Reputacija", + "bookmarks": "Žymės", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Peržiūrėjo", + "ignored": "Ignoruojami", + "default-category-watch-state": "Default category watch state", + "followers": "Sekėjai", + "following": "Seka", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "Apie mane", + "signature": "Parašas", + "birthday": "Gimimo diena", + "chat": "Susirašinėti", + "chat_with": "Tęsti pokalbį su %1", + "new_chat_with": "Pradėti naują susirašinėjimą su %1", + "flag-profile": "Flag Profile", + "follow": "Sekti", + "unfollow": "Nesekti", + "more": "Daugiau", + "profile_update_success": "Profilis sėkmingai atnaujintas!", + "change_picture": "Pakeisti paveikslėlį", + "change_username": "Keisti vartotojo vardą", + "change_email": "Keisti el. pašto adresą", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Redaguoti", + "edit-profile": "Redaguoti profilį", + "default_picture": "Standartinis paveikslėlis", + "uploaded_picture": "Įkeltas paveikslėlis", + "upload_new_picture": "Įkelti naują paveikslėlį", + "upload_new_picture_from_url": "Įkelti naują paveikslėlį iš URL", + "current_password": "Dabartinis slaptažodis", + "change_password": "Pakeisti slaptažodį", + "change_password_error": "Negalimas slaptažodis!", + "change_password_error_wrong_current": "Jūsų dabartinis slaptažodis neteisingas!", + "change_password_error_match": "Slaptažodžiai privalo sutapti!", + "change_password_error_privileges": "Jūs neturite teisių pakeisti šį slaptažodį.", + "change_password_success": "Jūsų slaptažodis atnaujintas!", + "confirm_password": "Patvirtinkite slaptažodį", + "password": "Slaptažodis", + "username_taken_workaround": "Jūsų norimas vartotojo vardas jau užimtas, todėl mes jį šiek tiek pakeitėme. Dabar jūs esate žinomas kaip %1", + "password_same_as_username": "Jūsų slaptažodis sutampa su Jūsų vartotojo vardu. Dėl saugumo, prašome naudoti kitą slaptažodį.", + "password_same_as_email": "Jūsų slaptažodis sutampa su Jūsų el. pašto adresu. Dėl saugumo, prašome naudoti kitą slaptažodį.", + "weak_password": "Silpnas slaptažodis.", + "upload_picture": "Įkelti paveikslėlį", + "upload_a_picture": "Įkelti paveikslėlį", + "remove_uploaded_picture": "Ištrinti paveikslėlį", + "upload_cover_picture": "Įkelti viršelio nuotrauką", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Apkarpyti paveikslėlį", + "upload_cropped_picture": "Apkarpyti ir įkelti", + "avatar-background-colour": "Avatar background colour", + "settings": "Nustatymai", + "show_email": "Rodyti mano el. paštą viešai", + "show_fullname": "Rodyti mano vardą ir pavardę", + "restrict_chats": "Gauti pokalbių žinutes tik iš tų narių, kuriuos seku", + "digest_label": "Prenumeruoti įvykių santrauką", + "digest_description": "Gauti naujienas apie naujus pranešimus ir temas į el. paštą pagal nustatytą grafiką", + "digest_off": "Išjungta", + "digest_daily": "Kas dieną", + "digest_weekly": "Kas savaitę", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Kas mėnesį", + "has_no_follower": "Šis vartotojas neturi jokių sekėjų :(", + "follows_no_one": "Šis vartotojas nieko neseka :(", + "has_no_posts": "Šis vartotojas pakolkas neparašė jokių pranešimų", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Šis vartotojas pakolkas nesukūrė jokių temų", + "has_no_watched_topics": "Šis vartotojas pakolkas nestebėjo jokių temų", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "Šis narys dar neturi teigiamai įvertintų pranešimų.", + "has_no_downvoted_posts": "Šis narys dar neturi neigiamai įvertintų pranešimų.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "El. paštas paslėptas", + "hidden": "paslėptas", + "paginate_description": "Puslapiavimas temų ir pranešimų, vietoj kad naudoti judėjimą su pelytė į viršų ir į apačia", + "topics_per_page": "Temų puslapyje", + "posts_per_page": "Pranešimų puslapyje", + "max_items_per_page": "Maximum %1", + "acp_language": "Administratoriaus puslapio kalba", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Naršymo nustatymai", + "open_links_in_new_tab": "Atidaryti išeinančias nuorodas naujam skirtuke", + "enable_topic_searching": "Įjungti Temų Ieškojimą ", + "topic_search_help": "Jeigu įjungtas, temų ieškojimas, nepaisys naršyklės puslapio ieškojimo, ir pradės ieškoti tik toje temoje kuri bus rodoma ekrane", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Po parašyto atsakymo, rodyti naują pranešimą", + "follow_topics_you_reply_to": "Peržiūrėti temas, kuriose Jūs atsakėte", + "follow_topics_you_create": "Peržiūrėti temas, kurias Jūs sukūrėte", + "grouptitle": "Grupės pavadinimas", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Nėra grupės pavadinimo", + "select-skin": "Pasirinkite išvaizdą", + "select-homepage": "Pasirinkite pagrindinį puslapį", + "homepage": "Pagrindinis puslapis", + "homepage_description": "Pasirinkite puslapį kaip savo pagrindinį, arba pasirinkite \"Joks\" norėdami naudoti standartinį pagrindinį puslapį.", + "custom_route": "Pagrindinio puslapio vieta", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "Nerasta pažymėtų pranešimų", + "info.ban-history": "Blokavimų istorija", + "info.no-ban-history": "Šis narys nebuvo užblokuotas.", + "info.banned-until": "Užblokuotas iki %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Užblokuotas visam laikui", + "info.banned-reason-label": "Priežastis", + "info.banned-no-reason": "Be priežasties", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "El. pašto istorija", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Pridėti pastabą", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "Šis bendruomenės forumas renka ir apdoroja jūsų asmeninę informaciją.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Duoti sutikimą", + "consent.right_of_access": "Jūs turite prieigos teisę", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "Turite teisę į duomenų perkėlimą", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/lt/users.json b/public/language/lt/users.json new file mode 100644 index 0000000000..ed03628baf --- /dev/null +++ b/public/language/lt/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Paskutiniai vartotojai", + "top_posters": "Geriausi autoriai", + "most_reputation": "Didžiausia reputacija", + "most_flags": "Most Flags", + "search": "Ieškoti", + "enter_username": "Įrašykite vartotojo vardą paieškai", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Įkelti daugiau", + "users-found-search-took": "Rasta %1 vartotojas(-ai)! Paieška užtruko %2 sekundes.", + "filter-by": "Filtruoti pagal", + "online-only": "Tik prisijunge", + "invite": "Pakviesti", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Pakvietimas el.paštu buvo išsiųstas į %1!", + "user_list": "Vartotojų sąrašas", + "recent_topics": "Paskutinės temos", + "popular_topics": "Populiarios temos", + "unread_topics": "Neperskaitytos temos", + "categories": "Kategorijos", + "tags": "Žymos", + "no-users-found": "Nerasta vartotojų." +} \ No newline at end of file diff --git a/public/language/lv/_DO_NOT_EDIT_FILES_HERE.md b/public/language/lv/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/lv/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/lv/admin/admin.json b/public/language/lv/admin/admin.json new file mode 100644 index 0000000000..d79f1dcb61 --- /dev/null +++ b/public/language/lv/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Vai tiešām vēlies pārkompilēt un pārstartēt NodeBB?", + "alert.confirm-restart": "Vai tiešām vēlies pārstartēt NodeBB?", + + "acp-title": "%1 | NodeBB administrācijas vadības panelis", + "settings-header-contents": "Saturs", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/lv/admin/advanced/cache.json b/public/language/lv/admin/advanced/cache.json new file mode 100644 index 0000000000..2e083938b5 --- /dev/null +++ b/public/language/lv/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Rakstu kešatmiņa", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Aizņemts", + "post-cache-size": "Rakstu kešatmiņas lielums", + "items-in-cache": "Rakstu skaits kešatmiņā" +} \ No newline at end of file diff --git a/public/language/lv/admin/advanced/database.json b/public/language/lv/admin/advanced/database.json new file mode 100644 index 0000000000..ea18ef8ac6 --- /dev/null +++ b/public/language/lv/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 B", + "x-mb": "%1 MB", + "x-gb": "%1 GB", + "uptime-seconds": "Darbspējas laiks sekundēs", + "uptime-days": "Darbspējas laiks dienās", + + "mongo": "MongoDB", + "mongo.version": "MongoDB versija", + "mongo.storage-engine": "Krātuves dzinējs", + "mongo.collections": "Kolekcijas", + "mongo.objects": "Objekti", + "mongo.avg-object-size": "Objekta vidējais lielums", + "mongo.data-size": "Datu lielums", + "mongo.storage-size": "Krātuves lielums", + "mongo.index-size": "Indeksa lielums", + "mongo.file-size": "Faila lielums", + "mongo.resident-memory": "Aizņemtā atmiņa", + "mongo.virtual-memory": "Virtuālā atmiņa", + "mongo.mapped-memory": "Saistītā atmiņa", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB info kods", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis versija", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Savienotie klienti", + "redis.connected-slaves": "Savienotās kopijas", + "redis.blocked-clients": "Bloķētie klienti", + "redis.used-memory": "Aizņemtā atmiņa", + "redis.memory-frag-ratio": "Sadrumstalotības proporcija", + "redis.total-connections-recieved": "Kopēji saņemtie savienojumi", + "redis.total-commands-processed": "Kopēji apstrādātas operācijas", + "redis.iops": "Momentānais operāciju skaits sekundē", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Atrasto atslēgu skaits", + "redis.keyspace-misses": "Neatrasto atslēgu skaits", + "redis.raw-info": "Redis info kods", + + "postgres": "PostgreSQL", + "postgres.version": "PostgreSQL versija", + "postgres.raw-info": "PostgreSQL info kods" +} diff --git a/public/language/lv/admin/advanced/errors.json b/public/language/lv/admin/advanced/errors.json new file mode 100644 index 0000000000..3a02a71dbc --- /dev/null +++ b/public/language/lv/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Attēls %1", + "error-events-per-day": "%1 kļūdas dienā", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Pārvaldīt kļūdu žurnālu", + "export-error-log": "Eksportēt žurnālu (.csv)", + "clear-error-log": "Notīrīt žurnālu", + "route": "Ceļš", + "count": "Skaits", + "no-routes-not-found": "Labi! Nav \"404 Not Found\" kļūdu!", + "clear404-confirm": "Vai tiešām vēlies notīrīt \"404 Not Found\" kļūdu žurnālu?", + "clear404-success": "\"404 Not Found\" kļūdas notīrītas" +} \ No newline at end of file diff --git a/public/language/lv/admin/advanced/events.json b/public/language/lv/admin/advanced/events.json new file mode 100644 index 0000000000..2836e7f88e --- /dev/null +++ b/public/language/lv/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Notikumi", + "no-events": "Nav notikumu", + "control-panel": "Notikumu vadības panelis", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/lv/admin/advanced/logs.json b/public/language/lv/admin/advanced/logs.json new file mode 100644 index 0000000000..48ea007adf --- /dev/null +++ b/public/language/lv/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Žurnāls", + "control-panel": "Žurnāla vadības panelis", + "reload": "Pārlādēt žurnālu", + "clear": "Notīrīt žurnālu", + "clear-success": "Zurnāls notīrīts!" +} \ No newline at end of file diff --git a/public/language/lv/admin/appearance/customise.json b/public/language/lv/admin/appearance/customise.json new file mode 100644 index 0000000000..e6471aff70 --- /dev/null +++ b/public/language/lv/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Pielāgotais CSS/LESS", + "custom-css.description": "Šeit ievadi savas CSS / LESS deklarācijas, kuras piemēros pēc visiem citiem stiliem.", + "custom-css.enable": "Iespējot pielāgotu CSS/LESS", + + "custom-js": "Pielāgotais Javascript", + "custom-js.description": "Šeit ievadi savu javascript. Tas tiks palaists pēc lapas pilnīgas ielādes.", + "custom-js.enable": "Iespējot pielāgotu Javascript", + + "custom-header": "Pielāgotā galvene", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Iespējot pielāgotu galveni", + + "custom-css.livereload": "Iespējot dzīvo pārlādēšanu", + "custom-css.livereload.description": "Piespiest atsvaidzināt visas aktīvās sesijas ikvienā no Tava konta esošajām ierīcēm katru reizi, kad noklikšķini uz \"Saglabāt\"" +} \ No newline at end of file diff --git a/public/language/lv/admin/appearance/skins.json b/public/language/lv/admin/appearance/skins.json new file mode 100644 index 0000000000..f0967bc226 --- /dev/null +++ b/public/language/lv/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Ielādē ādiņas...", + "homepage": "Sākumlapa", + "select-skin": "Izvēlēties ādiņu", + "current-skin": "Pašreizējā ādiņa", + "skin-updated": "Ādiņa atjaunināta", + "applied-success": "%1 ādiņa veiksmīgi iespējota", + "revert-success": "Ādiņa atgriezta pamata krāsās" +} \ No newline at end of file diff --git a/public/language/lv/admin/appearance/themes.json b/public/language/lv/admin/appearance/themes.json new file mode 100644 index 0000000000..4789bcec3a --- /dev/null +++ b/public/language/lv/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Pārbauda instalētās tēmas...", + "homepage": "Sākumlapa", + "select-theme": "Atlasīt tēmu", + "current-theme": "Pašreizējā tēma", + "no-themes": "Nav instalēto tēmu", + "revert-confirm": "Vai tiešām vēlies atjaunot noklusējamo NodeBB tēmu?", + "theme-changed": "Tēma ir mainīta", + "revert-success": "NodeBB veiksmīgi atgriezts atpakaļ uz tā noklusējuma tēmu.", + "restart-to-activate": "Lūdzu, pārkompilēt un pārstartēt NodeBB, lai pilnībā aktivizētu tēmu." +} \ No newline at end of file diff --git a/public/language/lv/admin/dashboard.json b/public/language/lv/admin/dashboard.json new file mode 100644 index 0000000000..076694b4ca --- /dev/null +++ b/public/language/lv/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Foruma datplūsma", + "page-views": "Lapu skatījumi", + "unique-visitors": "Unikālie apmeklētāji", + "logins": "Logins", + "new-users": "New Users", + "posts": "Raksti", + "topics": "Temati", + "page-views-seven": "Pēdējās 7 dienās", + "page-views-thirty": "Pēdējās 30 dienās", + "page-views-last-day": "Pēdējās 24 stundās", + "page-views-custom": "Pielāgotais datumu diapazons", + "page-views-custom-start": "No", + "page-views-custom-end": "Līdz", + "page-views-custom-help": "Ievadīt datumu diapazonu, kā lapu skatījumu skaitu vēlies redzēt. Ja datumu atlasītājs nav pieejams, lietot formātu YYYY-MM-DD", + "page-views-custom-error": "Lūdzu, ievadīt derīgu datumu diapazonu formatā YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "Visu laiku", + + "updates": "Atjauninājumi", + "running-version": "Ir palaists NodeBB v%1.", + "keep-updated": "Lūdzu, vienmēr pārliecināties, ka NodeBB ir atjaunināts ar jaunākajiem drošības ielāpiem un kļūdu labojumiem.", + "up-to-date": "

Šobrīd nav atjauninājumu

", + "upgrade-available": "

Ir izlaista jauna versija (v%1). Apsvērt NodeBB atjaunināšanu.

", + "prerelease-upgrade-available": "

Šī ir novecojusies pirmizlaides NodeBB versija. Jauna versija (v%1) ir bijusi izlaista. Apsvērt NodeBB atjaunināšanu.

", + "prerelease-warning": "

Ši ir pirmizlaides NodeBB versija. Neparedzētas kļūdas var rasties.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "NodeBB darbojas attīstītāju režīmā. NodeBB var būt neaizsargāts pret iespējamiem uzbrukumiem; lūdzu, sazināties ar sistēmas administratoru.", + "latest-lookup-failed": "

Neizdevās atrast jaunāko pieejamo NodeBB versiju

", + + "notices": "Paziņojumi", + "restart-not-required": "Nav nepieciešama pārstartēšana", + "restart-required": "Nepieciešama pārstartēšana", + "search-plugin-installed": "Meklēšanas spraudnis instalēts", + "search-plugin-not-installed": "Meklēšanas spraudnis nav instalēts", + "search-plugin-tooltip": "Instalēt meklēšanas spraudni no spraudņu lapas, lai aktivizētu meklēšanu", + + "control-panel": "Sistēmas kontrole", + "rebuild-and-restart": "Pārkompilēt & pārstartēt", + "restart": "Pārstartēt", + "restart-warning": "NodeBB pārkompilēšana vai pārstartēšana pārtrauks visus esošos savienojumus uz dažām sekundēm.", + "restart-disabled": "NodeBB pārkompilēšana un pārstartēšana ir atspējota, jo, šķiet, ka tā nav bijusi palaista ar atbilstošo dēmona procesu.", + "maintenance-mode": "Uzturēšanas režīms", + "maintenance-mode-title": "Klikšķināt, lai pārietu uz NodeBB uzturēšanas režīmu", + "realtime-chart-updates": "Reālā laika grafiku atjauninājumi", + + "active-users": "Aktīvie lietotāji", + "active-users.users": "Lietotāji", + "active-users.guests": "Viesi", + "active-users.total": "Kopēji", + "active-users.connections": "Savienojumi", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Reģistrētie", + + "user-presence": "Lietotāju novietojums", + "on-categories": "Skatās kategorijas", + "reading-posts": "Lasa rakstus", + "browsing-topics": "Pārlūko tematus", + "recent": "Skatās nesenos rakstus", + "unread": "Skatās nelasītos rakstus", + + "high-presence-topics": "Augstās klātesamības temati", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Lapu skatījumi", + "graphs.page-views-registered": "Lapu skatījumi no lietotājiem", + "graphs.page-views-guest": "Lapu skatījumi no viesiem", + "graphs.page-views-bot": "Lapu skatījumi no botiem", + "graphs.unique-visitors": "Unikālie apmeklētāji", + "graphs.registered-users": "Reģistrētie lietotāji", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Pēdējoreiz restartējis", + "no-users-browsing": "Nav pārlūkojošo lietotāju", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/lv/admin/development/info.json b/public/language/lv/admin/development/info.json new file mode 100644 index 0000000000..d07a526789 --- /dev/null +++ b/public/language/lv/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 serveri atbildēja %2ms laikā!", + "host": "serveris", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "tiešsaistē", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "darbspējas laiks", + + "registered": "Reģistrētie", + "sockets": "Tīkla savienojumi", + "guests": "Viesi", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/lv/admin/development/logger.json b/public/language/lv/admin/development/logger.json new file mode 100644 index 0000000000..9a59ec1183 --- /dev/null +++ b/public/language/lv/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Žurnāla iestatījumi", + "description": "Iespējojot izvēles rūtiņas, saņemsi žurnālus savā terminālī. Ja norādi ceļu, tad žurnāli tiks saglabāti failā. HTTP notikumu žurnāls ir noderīgs, lai apkopotu statistiku par to, kas, kad un kam kāds piekļūst forumā. Papildus HTTP pieprasījumu reģistrēšanai mēs varam arī reģistrēt socket.io notikumus. Socket.io notikumu žurnāls, kopā ar redis-cli monitoru, var būt ļoti noderīgs NodeBB iekšējo darbību mācību apguvē.", + "explanation": "Vienkārši atzīmēt vai noņemt atzīmi no žūrnāla iestatījumiem, lai aktivizētu vai atspējotu žurnālu reālā laikā. Nav nepieciešams forumu restartēt.", + "enable-http": "Iespējot HTTP notikumu žurnālu", + "enable-socket": "Iespējot socket.io notikumu žurnālu", + "file-path": "Ceļš uz žurnāla failu", + "file-path-placeholder": "/ceļš/uz/žurnāla/failu.log ::: atstāt tukšu, lai rakstītu terminālī", + + "control-panel": "Žurnāla vadības panelis", + "update-settings": "Saglabāt iestatījumus" +} \ No newline at end of file diff --git a/public/language/lv/admin/extend/plugins.json b/public/language/lv/admin/extend/plugins.json new file mode 100644 index 0000000000..fdf420530b --- /dev/null +++ b/public/language/lv/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Instalētie", + "active": "Aktīvie", + "inactive": "Neaktīvie", + "out-of-date": "Novecojušie", + "none-found": "Nav spraudņu", + "none-active": "Nav aktīvo spraudņu", + "find-plugins": "Meklēt spraudņos", + + "plugin-search": "Meklēt spraudņus", + "plugin-search-placeholder": "Meklēt spraudni...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Pārkārtot spraudņus", + "order-active": "Kārtot aktīvos spraudņus", + "dev-interested": "Vai esi ieinteresēts(-ēta) rakstīt spraudņus NodeBB?", + "docs-info": "Pilna dokumentācija par spraudņu rakstīšanu atrodama NodeBB dokumentu portālā.", + + "order.description": "Atsevišķi spraudņi darbojas labāk, kad tie tiek inicializēti pirms / pēc citiem spraudņiem.", + "order.explanation": "Spraudņi tiek ielādēti šeit norādītajā secībā, no augšas uz leju", + + "plugin-item.themes": "Tēmas", + "plugin-item.deactivate": "Deaktivizēt", + "plugin-item.activate": "Aktivizēt", + "plugin-item.install": "Instalēt", + "plugin-item.uninstall": "Atinstalēt", + "plugin-item.settings": "Iestatījumi", + "plugin-item.installed": "Instalētie", + "plugin-item.latest": "Jaunākie", + "plugin-item.upgrade": "Uzlabot", + "plugin-item.more-info": "Lai iegūtu vairāk informācijas:", + "plugin-item.unknown": "Nezināmie", + "plugin-item.unknown-explanation": "Šī spraudņa stāvokli nevarēja noteikt, iespējams, iestatījumu kļūdas dēļ.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Spraudnis iespējots", + "alert.disabled": "Spraudnis atspējots", + "alert.upgraded": "Spraudnis uzlabots", + "alert.installed": "Spraudnis instalēts", + "alert.uninstalled": "Spraudnis atinstalēts", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Spraudnis veiksmīgi deaktivizēts", + "alert.upgrade-success": "Lūdzu, pārkompilēt un pārstartēt NodeBB, lai pilnībā uzlabot spraudni.", + "alert.install-success": "Spraudnis veiksmīgi instalēts, lūdzu, to aktivizēt.", + "alert.uninstall-success": "Spraudnis veiksmīgi deaktivizēts un atinstalēts.", + "alert.suggest-error": "

NodeBB nevarēja sasniegt pakotņu pārvaldnieku, turpināt instalēt jaunāko versiju?

Serveris atbildēja (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB nevarēja sasniegt pakotņu pārvaldnieku, šobrīd uzlabošana nav ieteikta.

", + "alert.incompatible": "

Šī NodeBB versija (v%1) atļauj spraudni uzlabot tikai uz v%2 versiju. Lūdzu, atjaunināt NodeBB, ja vēlies instalēt šī spraudņa jaunāko versiju.

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Spraudņi pārkārtoti", + "alert.reorder-success": "Lūdzu, pārkompilēt un pārstartēt NodeBB, lai pilnībā pabeigtu procesu.", + + "license.title": "Spraudņa licences informācija", + "license.intro": "Spraudnis %1 ir licencēts pēc %2. Pirms spraudņa aktivizēšanas, lūdzu, izlasīt un izprast licences noteikumus.", + "license.cta": "Vai vēlies turpināt aktivizēt spraudni?" +} diff --git a/public/language/lv/admin/extend/rewards.json b/public/language/lv/admin/extend/rewards.json new file mode 100644 index 0000000000..97cf227477 --- /dev/null +++ b/public/language/lv/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Balvas", + "condition-if-users": "Ja lietotāja", + "condition-is": "Ir:", + "condition-then": "Tad:", + "max-claims": "Cik reižu balva ir pieprasāma", + "zero-infinite": "Ievadīt 0, lai būtu bez ierobežojuma", + "delete": "Izdzēst", + "enable": "Iespējot", + "disable": "Atspējot", + + "alert.delete-success": "Veiksmīgi izdzēsta balva", + "alert.no-inputs-found": "Nederīga balva - nav ievažu!", + "alert.save-success": "Balva veiksmīgi saglabāta" +} \ No newline at end of file diff --git a/public/language/lv/admin/extend/widgets.json b/public/language/lv/admin/extend/widgets.json new file mode 100644 index 0000000000..399d5ec12b --- /dev/null +++ b/public/language/lv/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Pieejamie logrīki", + "explanation": "Atlasīt spraudni no nolaižamās izvēlnes, un to vilkt un nomest uz veidnes spraudņa lauku kreisajā pusē.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Klonēt logrīkus no", + "containers.available": "Pieejamās tvertnes", + "containers.explanation": "Vilkt un nomest uz jebkura aktīvā logrīka", + "containers.none": "Nav", + "container.well": "Labi", + "container.jumbotron": "Jumbotron", + "container.panel": "Panelis", + "container.panel-header": "Paneļa galvene", + "container.panel-body": "Paneļa korpuss", + "container.alert": "Brīdinājums", + + "alert.confirm-delete": "Vai tiešām vēlies izdzēst šo logrīku?", + "alert.updated": "Logrīki atjaunināti", + "alert.update-success": "Veiksmīgi atjaunināti logrīki", + "alert.clone-success": "Veiksmīgi klonēti logrīki", + + "error.select-clone": "Lūdzu, izvēlēties lapu klonēšanai", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/admins-mods.json b/public/language/lv/admin/manage/admins-mods.json new file mode 100644 index 0000000000..4605719a81 --- /dev/null +++ b/public/language/lv/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administratori", + "global-moderators": "Globālie moderatori", + "moderators": "Moderators", + "no-global-moderators": "Nav globālo moderatoru", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Nav moderatoru", + "add-administrator": "Pievienot administratoru", + "add-global-moderator": "Pievienot globālo moderatoru", + "add-moderator": "Pievienot moderatoru" +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/categories.json b/public/language/lv/admin/manage/categories.json new file mode 100644 index 0000000000..050b03d33a --- /dev/null +++ b/public/language/lv/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Kategorijas iestatījumi", + "privileges": "Privilēģijas", + + "name": "Kategorijas nosaukums", + "description": "Kategorijas apraksts", + "bg-color": "Fona krāsa", + "text-color": "Teksta krāsa", + "bg-image-size": "Fona bildes lielums", + "custom-class": "Pielāgotā klase", + "num-recent-replies": "Neseno atbilžu skaits", + "ext-link": "Ārējā saite", + "subcategories-per-page": "Subcategories per page", + "is-section": "Izmantot kategoriju kā sadaļu", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Augšupielādēt bildi", + "delete-image": "Noņemt", + "category-image": "Kategorijas bilde", + "parent-category": "Virskategorija", + "optional-parent-category": "(Neobligāts) virskategorija", + "top-level": "Top Level", + "parent-category-none": "(Nav)", + "copy-parent": "Copy Parent", + "copy-settings": "Kopēt iestatījumus no", + "optional-clone-settings": "(Neobligāts) klonēt iestatījumus no kategorijas", + "clone-children": "Klonēt apakškategorijas un iestatījumus", + "purge": "Iztīrīt kategoriju", + + "enable": "Iespējot", + "disable": "Atspējot", + "edit": "Rediģēt", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Atlasīt kategoriju", + "set-parent-category": "Iestatīt virskategoriju", + + "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.category-selector": "Privileģiju konfigurēšana", + "privileges.warning": "Piezīme: Privilēģiju maiņas stājas spēkā uzreiz. Pēc maiņām nav nepieciešams saglabāt kategoriju.", + "privileges.section-viewing": "Skatīšanas privilēģijas", + "privileges.section-posting": "Publicēšanas privilēģijas", + "privileges.section-moderation": "Moderatora privilēģijas", + "privileges.section-other": "Other", + "privileges.section-user": "Lietotājs", + "privileges.search-user": "Pievienot lietotāju", + "privileges.no-users": "Nav lietotājiem īpašo privilēģiju šinī kategorijā.", + "privileges.section-group": "Grupa", + "privileges.group-private": "Grupa ir privāta", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Pievienot grupu", + "privileges.copy-to-children": "Kopēt uz apakškategorijām", + "privileges.copy-from-category": "Kopēt no kategorijas", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "Ja registered-users grupai tiek piešķirta īpaša privilēģija, visas pārējās grupas saņem netiešu privilēģiju, pat ja tā nav tieši piešķirta. Šī netiešā privilēģija tiek parādīta, jo visi lietotāji ir daļa no registered-users grupas, tādēļ šī privilēģija pārējām grupām nav papildus jāpiešķir.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Atpakaļ uz kategoriju sarakstu", + "analytics.title": "\"%1\" kategorijas analītika", + "analytics.pageviews-hourly": "Attēls 1 – Kategorijas lapu skatījumi stundā", + "analytics.pageviews-daily": "Attēls 2 – Kategorijas lapu skatījumi dienā", + "analytics.topics-daily": "Attēls 3 – Kategorijas jaunie temati dienā", + "analytics.posts-daily": "Attēls 4 – Kategorijas publicētie raksti dienā", + + "alert.created": "Izveidotās", + "alert.create-success": "Kategorija veiksmīgi izveidota", + "alert.none-active": "Nav aktīvo kategoriju", + "alert.create": "Izveidot kategoriju", + "alert.confirm-purge": "

Vai tiešām vēlies iztīrīt šo kategoriju \"%1\"?

Brīdinājums!Visi temati un raksti šajā kategorijā tiks iztīrīti!

Iztukšojot kategoriju, tiks noņemti visi temati un raksti un kategorija tiks izdzēsta no datu bāzes. Ja vēlies īslaicīgi noņemt kategoriju, \"atspējo\" to.

", + "alert.purge-success": "Kategorija iztīrīta!", + "alert.copy-success": "Iestatījumi kopēti!", + "alert.set-parent-category": "Iestatīt virskategoriju", + "alert.updated": "Atjauninātās kategorijas", + "alert.updated-success": "Kategorija ID %1 veiksmīgi atjaunināta.", + "alert.upload-image": "Augšupielādēt kategorijas bildi", + "alert.find-user": "Meklēt lietotājos", + "alert.user-search": "Meklēt lietotājus šeit", + "alert.find-group": "Meklēt grupās", + "alert.group-search": "Meklēt grupas šeit...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Sakļaut visas", + "expand-all": "Izvērst visas", + "disable-on-create": "Atiestatīt pie izveidošanas", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/digest.json b/public/language/lv/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/lv/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/lv/admin/manage/groups.json b/public/language/lv/admin/manage/groups.json new file mode 100644 index 0000000000..c4bc9be2e4 --- /dev/null +++ b/public/language/lv/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Grupas nosaukums", + "badge": "Badge", + "properties": "Properties", + "description": "Grupas apraksts", + "member-count": "Biedru skaits", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Rediģēt", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Meklēt", + "create": "Izveidot grupu", + "description-placeholder": "Īss grupas apraksts", + "create-button": "Izveidot", + + "alerts.create-failure": "Ak, vai

Veidojot grupu, radās problēma. Lūdzu, pamēģini vēlreiz vēlāk!

", + "alerts.confirm-delete": "Vai tiešām vēlies izdzēst šo grupu?", + + "edit.name": "Grupas nosaukums", + "edit.description": "Grupas apraksts", + "edit.user-title": "Grupas etiķete", + "edit.icon": "Grupas ikona", + "edit.label-color": "Grupas etiķetes krāsa", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Rādīt etiķeti", + "edit.private-details": "Pievienoties grupai nepieciešama grupas īpašnieka apstiprināšana.", + "edit.private-override": "Brīdinājums: privātās grupas ir atspējotas sistēmas līmenī, un šo opciju neņems vērā.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Paslēpta", + "edit.hidden-details": "Grupa nav redzama grupu sarakstā un lietotāji būs jāuzaicina pašrocīgi", + "edit.add-user": "Pievienot lietotāju grupai", + "edit.add-user-search": "Meklēt lietotājus", + "edit.members": "Biedri", + "control-panel": "Grupu vadības panelis", + "revert": "Atgriezties", + + "edit.no-users-found": "Nav lietotāju", + "edit.confirm-remove-user": "Vai tiešām vēlies izdzēst šo lietotāju?", + "edit.save-success": "Izmaiņas saglabātas" +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/privileges.json b/public/language/lv/admin/manage/privileges.json new file mode 100644 index 0000000000..60ba5996f2 --- /dev/null +++ b/public/language/lv/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Globālās", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Sarunāties", + "upload-images": "Augšupielādēt bildes", + "upload-files": "Augšupielādēt failus", + "signature": "Parakstīties", + "ban": "Bloķēt", + "mute": "Mute", + "invite": "Invite", + "search-content": "Meklēt saturā", + "search-users": "Meklēt lietotājos", + "search-tags": "Meklēt birkās", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Vietējā ielogošanās", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Meklēt kategorijās", + "access-category": "Piekļūt kategorijām", + "access-topics": "Piekļūt tematiem", + "create-topics": "Izveidot tematus", + "reply-to-topics": "Atbildēt tematos", + "schedule-topics": "Schedule Topics", + "tag-topics": "Pievienot birkas", + "edit-posts": "Rediģēt rakstus", + "view-edit-history": "Skatīt rediģēšanas vēsturi", + "delete-posts": "Izdzēst rakstus", + "view_deleted": "Skatīt izdzēstos rakstus", + "upvote-posts": "Balsot \"par\"", + "downvote-posts": "Balsot \"pret\"", + "delete-topics": "Izdzēst tematus", + "purge": "Iztīrīt", + "moderate": "Moderēt", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/registration.json b/public/language/lv/admin/manage/registration.json new file mode 100644 index 0000000000..bc28270a7a --- /dev/null +++ b/public/language/lv/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Reģistrācijas rinda", + "description": "Reģistrācijas rindā nav lietotāju.
Lai iespējotu šo funkciju, doties uz Iestatījumi → Lietotāji → Reģistrācija un iestatīt Reģistrācijas veidu uz \"Administratora apstiprināts\".", + + "list.name": "Vārds", + "list.email": "E-pasta adrese", + "list.ip": "IP adrese", + "list.time": "Datums", + "list.username-spam": "Biežums: %1 Parādās: %2 Ticamība: %3", + "list.email-spam": "Biežums: %1 Parādās: %2", + "list.ip-spam": "Biežums: %1 Parādās: %2", + + "invitations": "Ielūgumi", + "invitations.description": "Zemāk skatīt visu nosūtīto ielūgumu sarakstu. Izmantot ctrl-f, lai meklētu e-pasta adresi vai lietotājvārdu sarakstā.

Lietotājvārds tiks parādīts pa labi no e-pasta adresēm tiem lietotājiem, kas ir atbildējuši uz saviem ielūgumiem.", + "invitations.inviter-username": "Lietotājs, kurš uzaicināja", + "invitations.invitee-email": "Uzaicinātā lietotāja e-pasta adrese", + "invitations.invitee-username": "Uzaicinātais lietotājvārds (ja ir jau reģistrējies(-jusies))", + + "invitations.confirm-delete": "Vai tiešām vēlies izdzēst šo ielūgumu?" +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/tags.json b/public/language/lv/admin/manage/tags.json new file mode 100644 index 0000000000..7ab7e6507d --- /dev/null +++ b/public/language/lv/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Forumā vēl nav tematu ar birkām.", + "bg-color": "Fona krāsa", + "text-color": "Teksta krāsa", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Izveidot birku", + "modify": "Rediģēt birkas", + "rename": "Pārdēvēt birkas", + "delete": "Izdzēst atlasītās birkas", + "search": "Meklēt birkās...", + "settings": "Tags Settings", + "name": "Birkas nosaukums", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Vai vēlies izdzēst šīs birkas?", + "alerts.update-success": "Birka atjaunināta!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/uploads.json b/public/language/lv/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/lv/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/lv/admin/manage/users.json b/public/language/lv/admin/manage/users.json new file mode 100644 index 0000000000..5165dd6447 --- /dev/null +++ b/public/language/lv/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Lietotāji", + "edit": "Actions", + "make-admin": "Apstiprināt kā administratoru", + "remove-admin": "Noņemt administratora tiesības", + "validate-email": "Apstiprināt e-pasta adresi", + "send-validation-email": "Sūtīt apstiprināšanas e-pastu", + "password-reset-email": "Sūtīt paroles atiestatīšanas e-pastu", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Bloķēt lietotāju(-s)", + "temp-ban": "Bloķēt lietotāju(-s) uz laiku", + "unban": "Atbloķēt lietotāju(-s)", + "reset-lockout": "Atiestatīt bloķēšanu", + "reset-flags": "Atiestatīt atzīmes", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Lejupielādēt .csv", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "Izveidot jaunu lietotāju", + "filter-by": "Filter by", + "pills.unvalidated": "Neapstiprinātie", + "pills.validated": "Validated", + "pills.banned": "Bloķētie", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "Pēc lietotāja ID", + "search.uid-placeholder": "Meklējamais lietotāja ID", + "search.username": "Pēc lietotājvārda", + "search.username-placeholder": "Meklējamais lietotājvārds", + "search.email": "Pēc e-pasta adreses", + "search.email-placeholder": "Meklējamā e-pasta adrese", + "search.ip": "Pēc IP adreses", + "search.ip-placeholder": "Meklējamā IP adrese", + "search.not-found": "Nav atrasts atbilstošs lietotājs!", + + "inactive.3-months": "3 mēnešus", + "inactive.6-months": "6 mēnešus", + "inactive.12-months": "12 mēnešus", + + "users.uid": "uid", + "users.username": "lietotājvārds", + "users.email": "e-pasta adrese", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "raksti", + "users.reputation": "ranga punkti", + "users.flags": "atzīmes", + "users.joined": "reģistrējies(-jusies)", + "users.last-online": "pēdējoreiz redzēts(-ēta)", + "users.banned": "bloķēts?", + + "create.username": "Vārds", + "create.email": "E-pasta adrese", + "create.email-placeholder": "E-pasta adrese", + "create.password": "Parole", + "create.password-confirm": "Atkārtot paroli", + + "temp-ban.length": "Length", + "temp-ban.reason": "Iemesls (neobligāts)", + "temp-ban.hours": "Stundas", + "temp-ban.days": "Dienas", + "temp-ban.explanation": "Ievadīt bloķēšanas termiņu, ņemot vērā, ka 0 tiks uzskatīts par pastāvīgu bloķēšanu.", + + "alerts.confirm-ban": "Vai tiešām vēlies pastāvīgi bloķēt šo lietotāju?", + "alerts.confirm-ban-multi": "Vai tiešām vēlies pastāvīgi bloķēt šos lietotājus?", + "alerts.ban-success": "Lietotājs(-i) bloķēts(-i)!", + "alerts.button-ban-x": "Bloķēt %1 lietotāju(-s)", + "alerts.unban-success": "Lietotājs(-i) atbloķēts(-i)", + "alerts.lockout-reset-success": "Bloķēšana atiestatīta!", + "alerts.flag-reset-success": "Atzīme(-s) atiestīta(-s)!", + "alerts.no-remove-yourself-admin": "Nevar sev noņemt administratora tiesības!", + "alerts.make-admin-success": "Lietotājs tagad ir administrators.", + "alerts.confirm-remove-admin": "Vai tiešām vēlies noņemt šo administratoru?", + "alerts.remove-admin-success": "Lietotājs tagad vairs nav administrators.", + "alerts.make-global-mod-success": "Lietotājs tagad ir globālais moderators.", + "alerts.confirm-remove-global-mod": "Vai tiešām vēlies noņemt šo globālo moderatoru?", + "alerts.remove-global-mod-success": "Lietotājs tagad vairs nav globālais moderators.", + "alerts.make-moderator-success": "Lietotājs tagad ir moderators.", + "alerts.confirm-remove-moderator": "Vai tiešām vēlies noņemt šo moderatoru?", + "alerts.remove-moderator-success": "Lietotājs tagad vairs nav moderators.", + "alerts.confirm-validate-email": "Vai vēlies apstiprināt šī(-o) lietotāja(-u) e-pasta adresi(-es)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "E-pasti ir apstiprināti", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Vai vēlies nosūtīt paroles atiestatīšanas e-pastu(-s) šim(-iem) lietotājam(-iem)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "Lietotājs(-i) izdzēsts(-i)!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Izveidot lietotāju", + "alerts.button-create": "Izveidot", + "alerts.button-cancel": "Atcelt", + "alerts.error-passwords-different": "Parolēm jāsakrīt!", + "alerts.error-x": "Kļūda

%1

", + "alerts.create-success": "Lietotājs izveidots!", + + "alerts.prompt-email": "E-pasta adreses:", + "alerts.email-sent-to": "Ielūguma e-pasts ir nosūtīts %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/lv/admin/menu.json b/public/language/lv/admin/menu.json new file mode 100644 index 0000000000..5a56214054 --- /dev/null +++ b/public/language/lv/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Vispārējie", + + "section-manage": "Pārvaldīt", + "manage/categories": "Kategorijas", + "manage/privileges": "Privilēģijas", + "manage/tags": "Birkas", + "manage/users": "Lietotāji", + "manage/admins-mods": "Administratori & moderatori", + "manage/registration": "Reģistrācijas rinda", + "manage/post-queue": "Rakstu apstiprināšanas rinda", + "manage/groups": "Grupas", + "manage/ip-blacklist": "IP adrešu melnais saraksts", + "manage/uploads": "Augšupielādes", + "manage/digest": "Digests", + + "section-settings": "Iestatījumi", + "settings/general": "Vispārējie", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "E-pasts", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Viesi", + "settings/uploads": "Augšupielādes", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Dalīšana pa lapām", + "settings/tags": "Birkas", + "settings/notifications": "Paziņojumi", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Sīkfaili", + "settings/web-crawler": "Rāpuļprogrammas", + "settings/sockets": "Tīkls", + "settings/advanced": "Paplašīnātie", + + "settings.page-title": "%1 — iestatījumi", + + "section-appearance": "Izskats", + "appearance/themes": "Tēmas", + "appearance/skins": "Ādiņas", + "appearance/customise": "Pielāgotais saturs (HTML/JS/CSS)", + + "section-extend": "Paplašinājumi", + "extend/plugins": "Spraudņi", + "extend/widgets": "Logrīki", + "extend/rewards": "Balvas", + + "section-social-auth": "Sociālo tīklu autentifikācija", + + "section-plugins": "Spraudņi", + "extend/plugins.install": "Instalēt spraudņus", + + "section-advanced": "Paplašinātie", + "advanced/database": "Datu bāze", + "advanced/events": "Notikumu reģistrs", + "advanced/hooks": "Āķi", + "advanced/logs": "Žurnāls", + "advanced/errors": "Kļūdu žurnāls", + "advanced/cache": "Kešatmiņa", + "development/logger": "Atkļūdošanas žurnāls", + "development/info": "Informācija", + + "rebuild-and-restart-forum": "Pārkompilēt & pārstartēt forumu", + "restart-forum": "Pārstartēt forumu", + "logout": "Izlogoties", + "view-forum": "Uz forumu", + + "search.placeholder": "Search settings", + "search.no-results": "Nav rezultātu...", + "search.search-forum": "Forumā meklēt ", + "search.keep-typing": "Rakstīt vairāk, lai redzētu rezultātus...", + "search.start-typing": "Sākt rakstīt, lai redzētu rezultātus...", + + "connection-lost": "Savienojums ar %1 ir pārtraukts, mēģina no jauna savienoties...", + + "alerts.version": "Palaists NodeBB v%1", + "alerts.upgrade": "Uzlabot uz v%1" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/advanced.json b/public/language/lv/admin/settings/advanced.json new file mode 100644 index 0000000000..1899c0dd8f --- /dev/null +++ b/public/language/lv/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Apkopes režīms", + "maintenance-mode.help": "Kad forums atrodas apkopes režīmā, visa piekļuve tiks novirzīta uz statisku lapu. Uz administratoriem neattiecas šī novirzīšana un viņi var piekļūt vietnei kā parasti.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Apkopes paziņojums", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Iezīmes", + "headers.allow-from": "Iestatīt ALLOW-FROM, lai atļautu NodeBB ievietot iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Pielāgot NodeBB sūtīto \"Powered By\" iezīmi", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin regulārā izteiksme", + "headers.acao-help": "Atstāt tukšu, lai aizliegtu piekļuvi visām vietnēm", + "headers.acao-regex-help": "Ievadīt regulāro izteiksmi, lai atlasītu dinamiskās izcelsmju URL. Atstāt tukšu, lai aizliegtu piekļuvi visām vietnēm", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "HTTP Strict Transport Security (HSTS)", + "hsts.enabled": "Iespējots HSTS (ieteicams)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Iekļaut apakšdomēnus HSTS iezīmē", + "hsts.preload": "Atļaut iepriekš ielādēt HSTS iezīmi", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Satiksmes pārvaldīšana", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Iespējot satiksmes pārvaldīšanu", + "traffic.event-lag": "Notikumu cilpas novilcināšanas slieksnis (milisekundēs)", + "traffic.event-lag-help": "Samazinot šo vērtību, tiek samazināti lapas ielādes gaidīšanas laiki, bet arī vairāk lietotājiem parādīsies ziņojums \"pārmērīga slodze\". (Nepieciešams restartēt)", + "traffic.lag-check-interval": "Pārbaudīšanas intervāls (milisekundēs)", + "traffic.lag-check-interval-help": "Samazinot šo vērtību, NodeBB kļūst jutīgāks pret slodzes smailēm, taču tā var arī izraisīt pārāk jutīgu pārbaudi. (Nepieciešams restartēt)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/api.json b/public/language/lv/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/lv/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/chat.json b/public/language/lv/admin/settings/chat.json new file mode 100644 index 0000000000..9a6a3540f1 --- /dev/null +++ b/public/language/lv/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Sarunu iestatījumi", + "disable": "Atspējot sarunāšanos", + "disable-editing": "Atspējot sarunu rediģēšanu/izdzēšanu", + "disable-editing-help": "Administratori un globālie moderatori ir atbrīvoti no šī ierobežojuma", + "max-length": "Sarunu lielākais garums", + "max-room-size": "Maksimālais lietotāju skaits tērzētavā", + "delay": "Laiks starp sarunām milisekundēs", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/cookies.json b/public/language/lv/admin/settings/cookies.json new file mode 100644 index 0000000000..12328e4166 --- /dev/null +++ b/public/language/lv/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "ES piekrišana", + "consent.enabled": "Iespējots", + "consent.message": "Notifikācija", + "consent.acceptance": "Pieņemšanas paziņojums", + "consent.link-text": "Polises saites teksts", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Atstāt tukšu, lai izmantotu noklusējuma NodeBB lokalizāciju", + "settings": "Iestatījumi", + "cookie-domain": "Sesiju sīkfailu domēns", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Atstāt tukšu, lai izvēlētos pēc noklusējuma" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/email.json b/public/language/lv/admin/settings/email.json new file mode 100644 index 0000000000..81815fc6af --- /dev/null +++ b/public/language/lv/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Sūtītājs", + "address": "Sūtītāja e-pasta adrese", + "address-help": "E-pasta adrese, ko saņēmējs redzēs laukos \"No:\" un \"Atbildēt:\".", + "from": "Sūtītāja vārds vai nosaukums", + "from-help": "Vārds vai nosaukums, ko saņēmējs redzēs kā sūtītāju.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP transports", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Izvēlies no labi zināmu pakalpojumu saraksta vai ievadi pielāgotu pakalpojumu.", + "smtp-transport.service": "Atlasīt servisu", + "smtp-transport.service-custom": "Pielāgotais serviss", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP serveris", + "smtp-transport.port": "SMTP ports", + "smtp-transport.security": "Savienojumu drošība", + "smtp-transport.security-encrypted": "Šifrēts", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Nav", + "smtp-transport.username": "Lietotājvārds", + "smtp-transport.username-help": "Gmail pakalpojumam ievadīt šeit visu e-pasta adresi, it īpaši, ja ir izmantots Google Apps pārvaldīts domēns.", + "smtp-transport.password": "Parole", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Rediģēt e-pasta veidni", + "template.select": "Atlasīt e-pasta veidni", + "template.revert": "Atgriezt uz oriģinālo", + "testing": "E-pasta testēšana", + "testing.select": "Atlasīt e-pasta veidni", + "testing.send": "Sūtīt parauga e-pastu", + "testing.send-help": "Testa e-pasts tiks nosūtīts uz pašlaik ielogojušā lietotāja e-pasta adresi.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Kopsavilkumu nosūtīšanas stunda", + "subscriptions.hour-help": "Ievadīt skaitli, kas norāda stundu, kurā nosūtītu e-pasta rakstu apkopojumu (piemēram, 0 nozīmē pusnakts, 17 nozīmē plkst.1700). Paturēt prātā, ka šī ir stunda servera laikā, un tā var neatbilst Tavam pulkstenim. Aptuvens servera laiks ir:
Nākamais ikdienas apkopojums tiks nosūtīts ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/lv/admin/settings/general.json b/public/language/lv/admin/settings/general.json new file mode 100644 index 0000000000..ea143d538f --- /dev/null +++ b/public/language/lv/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Foruma iestatījumi", + "title": "Foruma nosaukums", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "Foruma virsrakta URL", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Foruma nosaukums", + "title.show-in-header": "Rādīt foruma virsrakstu galvenē", + "browser-title": "Virsraksts pārlūkā", + "browser-title-help": "Foruma virsraksts tiks izmantots, ja virsraksts pārlūkā nav iestatīts", + "title-layout": "Virsraksta izkārtojums", + "title-layout-help": "Noteikt, kā virsraksts pārlūkā tiks izkārtots, t.i., {pageTitle} | {browserTitle}", + "description.placeholder": "Īss foruma apraksts", + "description": "Foruma apraksts", + "keywords": "Foruma atslēgvārdi", + "keywords-placeholder": "Atslēgvārdi, kas apraksta forumu, atdalīti ar komatu", + "logo": "Foruma logo", + "logo.image": "Bilde", + "logo.image-placeholder": "Ceļš uz logo, ko parādītu foruma galvenē", + "logo.upload": "Augšupielādēt", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "Foruma logo URL", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alternatīvais teksts", + "log.alt-text-placeholder": "Alternatīvais teksts pieejamībai", + "favicon": "Favorīta ikona", + "favicon.upload": "Augšupielādēt", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Augšupielādēt", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Izejošās saites", + "outgoing-links.warning-page": "Lietot izejošo saišu brīdinājuma lapu", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domēni, kuriem apiet brīdinājuma lapu", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/lv/admin/settings/group.json b/public/language/lv/admin/settings/group.json new file mode 100644 index 0000000000..22c037194b --- /dev/null +++ b/public/language/lv/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Vispārējie", + "private-groups": "Privātās grupas", + "private-groups.help": "Pievienoties grupai nepieciešama grupas īpašnieka apstiprināšana (iespējots pēc noklusējama)", + "private-groups.warning": "Ja šī opcija ir atspējota un privātās grupas ir iespējotas, tās automātiski kļūst par publiskām.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "Atļaut lietotājiem izvēlēties vairākas grupu etiķetas, nepieciešams tēmas atbalsts.", + "max-name-length": "Maksimālais grupas nosaukuma garums", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Galvenes bildes", + "default-cover": "Noklusējama galvenes bildes", + "default-cover-help": "Pievienot ar komatu atdalītu sarakstu no noklusējuma galvenes bildēm tām grupām, kam nav augšupielādēta galvenes bilde" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/guest.json b/public/language/lv/admin/settings/guest.json new file mode 100644 index 0000000000..394c6b8a25 --- /dev/null +++ b/public/language/lv/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Atļaut viesu iesaukas", + "handles.enabled-help": "Parādīt lauku, kas viesiem ļaus izvēlēties savu iesauku, kas saistīts ar katru viņu publicēto rakstu. Ja ir atspējots, viņus vienkārši sauks par \"Viesiem\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/homepage.json b/public/language/lv/admin/settings/homepage.json new file mode 100644 index 0000000000..68249543f4 --- /dev/null +++ b/public/language/lv/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Sākums", + "description": "Izvēlies, kādu lapu rādīt, kad lietotājs izvēlas foruma saknes URL.", + "home-page-route": "Sākumlapas ceļš", + "custom-route": "Pielāgotais ceļš", + "allow-user-home-pages": "Atļaut lietotājiem savas mājaslapas", + "home-page-title": "Sākumlapas titulis (pēc noklusējuma \"Sākums\")" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/languages.json b/public/language/lv/admin/settings/languages.json new file mode 100644 index 0000000000..5e668f9147 --- /dev/null +++ b/public/language/lv/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Valodas iestatījumi", + "description": "Noklusējuma valoda nosaka valodas iestatījumus visiem lietotājiem, kuri apmeklē forumu.
Lietotājs savā konta iestatījumu lapā var ignorēt noklusējuma valodu.", + "default-language": "Noklusējama valoda", + "auto-detect": "Viesiem automātiski izprast valodas iestatījumus" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/navigation.json b/public/language/lv/admin/settings/navigation.json new file mode 100644 index 0000000000..b4327584a4 --- /dev/null +++ b/public/language/lv/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ikona:", + "change-icon": "izmaiņa", + "route": "Ceļš:", + "tooltip": "Paskaidre:", + "text": "Teksts:", + "text-class": "Teksta klase: neobligāta", + "class": "Class: optional", + "id": "ID: neobligāts", + + "properties": "Īpašības:", + "groups": "Grupas:", + "open-new-window": "Rādīt jaunā logā", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Izdzēst", + "btn.disable": "Atspējot", + "btn.enable": "Iespējot", + + "available-menu-items": "Pieejamās izvēlnes iespējas", + "custom-route": "Pielāgotais ceļš", + "core": "kodols", + "plugin": "spraudnis" +} diff --git a/public/language/lv/admin/settings/notifications.json b/public/language/lv/admin/settings/notifications.json new file mode 100644 index 0000000000..d2f44f18d6 --- /dev/null +++ b/public/language/lv/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Paziņojumi", + "welcome-notification": "Sveicināšanas paziņojums", + "welcome-notification-link": "Sveicināšanas paziņojuma saite", + "welcome-notification-uid": "Sveicināšanas paziņojuma lietotājs (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/pagination.json b/public/language/lv/admin/settings/pagination.json new file mode 100644 index 0000000000..644b827fd4 --- /dev/null +++ b/public/language/lv/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Dalīšana pa lapām", + "enable": "Tematus un rakstus dalīt pa vairākām lapām un nelikt visus vienā.", + "posts": "Post Pagination", + "topics": "Tematu dalīšana pa lapām", + "posts-per-page": "Rakstu skaits lapā", + "max-posts-per-page": "Maksimālais rakstu skaits lapā", + "categories": "Kategoriju dalīšana pa lapām", + "topics-per-page": "Tematu skaits lapā", + "max-topics-per-page": "Maksimālais tematu skaits lapā", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/post.json b/public/language/lv/admin/settings/post.json new file mode 100644 index 0000000000..e7529f2a0d --- /dev/null +++ b/public/language/lv/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Rakstu kārtošana", + "sorting.post-default": "Noklusējuma rakstu kārtošana", + "sorting.oldest-to-newest": "No vecākā līdz jaunākam", + "sorting.newest-to-oldest": "No jaunākā līdz vecākam", + "sorting.most-votes": "Visvairāk balsojumu", + "sorting.most-posts": "Visvairāk rakstu", + "sorting.topic-default": "Noklusējuma tematu kārtošana", + "length": "Raksta garums", + "post-queue": "Post Queue", + "restrictions": "Publicēšanas ierobežojumi", + "restrictions-new": "Jauno lietotāju ierobežojumi", + "restrictions.post-queue": "Iespējot rakstu apstiprināšanas rindu", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Iespējot jauno lietotāju ierobežojumus", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Jauniem lietotājiem sekundes starp rakstiem", + "restrictions.rep-threshold": "Reputācijas slieksnis pirms ierobežojumu atcelšanas", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimālais virsraksta garums", + "restrictions.max-title-length": "Maksimālais virsraksta garums", + "restrictions.min-post-length": "Minimālais raksta garums", + "restrictions.max-post-length": "Maksimālais raksta garums", + "restrictions.days-until-stale": "Dienas, līdz temats tiek uzskatīts par novecojušu", + "restrictions.stale-help": "Ja temats tiek uzskatīts par novecojušu, brīdinājums tiks parādīts tiem lietotājiem, kuri mēģina uz tā atbildēt.", + "timestamp": "Datumi", + "timestamp.cut-off": "Datuma formāta maiņas punkts (dienās)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Ķircinājuma raksts", + "teaser.last-post": "Pēdējo – rādīt jaunāko rakstu, ieskaitot sākotnējo rakstu, ja atbildes nav", + "teaser.last-reply": "Pēdējo – rādīt jaunāko atbildi, vai \"Nav atbildes\" tekstu, ja atbildes nav", + "teaser.first": "Pirmais", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Nelasītie raksti", + "unread.cutoff": "Nelasīto rakstu vecumu robeža", + "unread.min-track-last": "Minimālais rakstu skaits tematā pirms izseko pēdējo lasīto", + "recent": "Nesenie raksti", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Atspējot tematu filtrēšanu ignorētās kategorijās /recent lapā", + "signature": "Parakstīšanās", + "signature.disable": "Atspējot parakstus", + "signature.no-links": "Atspējot saites parakstos", + "signature.no-images": "Atspējot bildes parakstos", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maksimālais paraksta garums", + "composer": "Redaktora iestatījumi", + "composer-help": "Tālāk norādītie iestatījumi nosaka funkcionalitāti un izskatu no raksta redaktoru, ko lieto jaunus tematus izveidojot, vai atbildot jau eksistējošiem tematiem.", + "composer.show-help": "Rādīt \"palīdzība\" cilni", + "composer.enable-plugin-help": "Atļaut spraudņiem pievienot saturu palīdzības cilnei", + "composer.custom-help": "Pielāgotais palīdzības teksts", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP adrešu pierakstīšana", + "ip-tracking.each-post": "Pierakstīt katra raksta IP adresi", + "enable-post-history": "Iespējot rakstu vēsturi" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/reputation.json b/public/language/lv/admin/settings/reputation.json new file mode 100644 index 0000000000..a290ac2ab4 --- /dev/null +++ b/public/language/lv/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Ranga punktu sistēma", + "disable": "Atspējot ranga punktu sistēmu", + "disable-down-voting": "Atspējot balsošanu \"pret\"", + "votes-are-public": "Visi balsojumi ir publiski", + "thresholds": "Aktivitātes sliekšņi", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimālie ranga punkti, lai balsotu \"pret\" rakstiem", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimālie ranga punkti, lai atzīmētu rakstus", + "min-rep-website": "Minimālie ranga punkti, lai lietotāja profilam pievienotu mājaslapu", + "min-rep-aboutme": "Minimālie ranga punkti, lai lietotāja profilam pievienotu \"Par mani\"", + "min-rep-signature": "Minimālie ranga punkti, lai lietotāja profilam pievienotu parakstu", + "min-rep-profile-picture": "Minimālie ranga punkti, lai lietotāja profilam pievienotu profila bildi", + "min-rep-cover-picture": "Minimālie ranga punkti, lai lietotāja profilam pievienotu galvenes bildi", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/social.json b/public/language/lv/admin/settings/social.json new file mode 100644 index 0000000000..03a4c2e035 --- /dev/null +++ b/public/language/lv/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Rakstu kopīgošana", + "info-plugins-additional": "Spraudņi var pievienot papildu rakstu kopīgošanas tīklus.", + "save-success": "Rakstu kopīgošanas tīkli veiksmi saglabāti!" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/sockets.json b/public/language/lv/admin/settings/sockets.json new file mode 100644 index 0000000000..ab7a9b8d51 --- /dev/null +++ b/public/language/lv/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Savienoties no jauna iestatījumi", + "max-attempts": "Maksimālais mēģinājumu skaits savienoties no jauna", + "default-placeholder": "Pēc noklusējuma: %1", + "delay": "Atlikšana, pirms savienoties no jauna" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/sounds.json b/public/language/lv/admin/settings/sounds.json new file mode 100644 index 0000000000..421805409d --- /dev/null +++ b/public/language/lv/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Paziņojumi", + "chat-messages": "Sarunas", + "play-sound": "Spēlēt", + "incoming-message": "Ienākošā saruna", + "outgoing-message": "Izejošā saruna", + "upload-new-sound": "Augšupielādēt jaunu skaņu", + "saved": "Iestatījumi saglabāti" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/tags.json b/public/language/lv/admin/settings/tags.json new file mode 100644 index 0000000000..f5fb8cdf3a --- /dev/null +++ b/public/language/lv/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Birku iestatījumi", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimālais birku skaits tematā", + "max-per-topic": "Maksimālais birku skaits tematā", + "min-length": "Minimālais birkas nosaukuma garums", + "max-length": "Maksimālais birkas nosaukuma garums", + "related-topics": "Saistītie temati", + "max-related-topics": "Maksimālais skaits saistīto tematu, ko rādīt (ja tema tos atbalsta)" +} \ No newline at end of file diff --git a/public/language/lv/admin/settings/uploads.json b/public/language/lv/admin/settings/uploads.json new file mode 100644 index 0000000000..ffc99e8b6f --- /dev/null +++ b/public/language/lv/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Raksti", + "orphans": "Orphaned Files", + "private": "Iestatīt augšupielādētos failus kā privātus", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Failu paplašīnājumi, kurus turēt privātus", + "private-uploads-extensions-help": "Ievadīt ar komatu atdalītu failu paplašinājumu sarakstu, kurus turēt privātus (piemērām pdf,xls,doc). Tukšais saraksts nozīmē, ka visi faili ir privāti.", + "resize-image-width-threshold": "Samazināt blides izmērus, ja ir plašāka par noteikto platumu", + "resize-image-width-threshold-help": "(pikseļos, pēc noklusējuma: 1520 pikseļi, iestatīt 0, lai atspējotu)", + "resize-image-width": "Samazināt blides izmērus līdz noteiktajam platumam", + "resize-image-width-help": "(pikseļos, pēc noklusējuma: 760 pikseļi, iestatīt 0, lai atspējotu)", + "resize-image-quality": "Kvalitāte, ko izmantot, pārveidojot bildes", + "resize-image-quality-help": "Izmantot zemākas kvalitātes iestatījumu, lai samazinātu mainīto bildes faila lielumu.", + "max-file-size": "Maksimālais faila lielums (KiB)", + "max-file-size-help": "(kibibaitos, pēc noklusējuma: 2.048 KiB)", + "reject-image-width": "Maksimālais bildes platums (pikseļos)", + "reject-image-width-help": "Bildes, kas ir platākas par šo vērtību, tiks noraidītas.", + "reject-image-height": "Maksimālais bildes augstums (pikseļos)", + "reject-image-height-help": "Bildes, kas ir augstākas par šo vērtību, tiks noraidītas.", + "allow-topic-thumbnails": "Atļaut lietotājiem augšupielādēt tematu sīktēlus", + "topic-thumb-size": "Tematu sīktēlu lielums", + "allowed-file-extensions": "Atļautie failu paplašinājumi", + "allowed-file-extensions-help": "Ievadīt ar komatu atdalītu failu paplašinājumu sarakstu (piemērām pdf,xls,doc). Tukšais saraksts nozīmē, ka visi failu paplašinājumi ir atļauti.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profila avatari", + "allow-profile-image-uploads": "Atļaut lietotājiem augšupielādēt profila bildes", + "convert-profile-image-png": "Pārvērst profila bildes augšupielādi uz PNG", + "default-avatar": "Pielāgotais noklusējuma stilizētais portrets", + "upload": "Augšupielādēt", + "profile-image-dimension": "Profila bildes izmēri", + "profile-image-dimension-help": "(pikseļos, pēc noklusējuma: 128 pikseļi)", + "max-profile-image-size": "Maksimālais profila bildes faila lielums", + "max-profile-image-size-help": "(kibibaitos, pēc noklusējuma: 256 KiB)", + "max-cover-image-size": "Maksimālais galvenes bildes faila lielums", + "max-cover-image-size-help": "(kibibaitos, pēc noklusējuma: 2.048 KiB)", + "keep-all-user-images": "Saglabāt serverī avataru un profila vāku vecās versijas", + "profile-covers": "Profilu vāki", + "default-covers": "Noklusējama galvenes bildes", + "default-covers-help": "Pievienot ar komatu atdalītu sarakstu no noklusējuma galvenes bildēm tiem kontiem, kam nav augšupielādēta galvenes bilde" +} diff --git a/public/language/lv/admin/settings/user.json b/public/language/lv/admin/settings/user.json new file mode 100644 index 0000000000..9a3d3d0c7d --- /dev/null +++ b/public/language/lv/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Autentifikācija", + "email-confirm-interval": "Lietotājs nevar atkārtoti nosūtīt apstiprinājuma e-pastu pirms", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Ielogoties", + "allow-login-with.username-email": "Ar lietotājvārdu vai e-pasta adresi", + "allow-login-with.username": "Tikai ar lietotājvārdu", + "account-settings": "Kontu iestatījumi", + "gdpr_enabled": "Iespējot VDAR piekrišanas vākšanu", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Atspējot lietotājvārda izmaiņas", + "disable-email-changes": "Atspējot e-pasta adreses izmaiņas", + "disable-password-changes": "Atspējot paroles izmaiņas", + "allow-account-deletion": "Atļaut konta izdzēšanu", + "hide-fullname": "Slēpt vārdu un uzvārdu no lietotājiem", + "hide-email": "Slēpt e-pasta adresi no lietotājiem", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Tēmas", + "disable-user-skins": "Neļaut lietotājiem izvēlēties pielāgotu ādiņu", + "account-protection": "Konta aizsardzība", + "admin-relogin-duration": "Administratora atkal ielogošanas ilgums (minūtes)", + "admin-relogin-duration-help": "Pēc noteiktā laika, piekļuvei administratora sadaļai būs nepieciešams atkārtoti ielogoties, iestatīt 0, lai atspējotu", + "login-attempts": "Ielogošanās mēģinājumi stundā", + "login-attempts-help": "Ja ielogošanās mēģinājumi no lietotāja konta pārsniedz šo slieksni, konts tiks bloķēts uz konfigurētā laika", + "lockout-duration": "Konta bloķēšanas ilgums (minūtes)", + "login-days": "Cik dienas, lai atcerētos lietotāju aktīvās sesijas", + "password-expiry-days": "Piespiest paroles atiestatīšanu pēc noteiktā dienu skaita", + "session-time": "Sesijas laiks", + "session-time-days": "Dienas", + "session-time-seconds": "Sekundes", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minūtes, pēc kura lietotājs tiek uzskatīts par neaktīvu", + "online-cutoff-help": "Ja lietotājs šajā laikā neveic nekādas darbības, tas tiek uzskatīts par neaktīvu un nesaņem reāllaika atjauninājumus.", + "registration": "Reģistrācija", + "registration-type": "Reģistrācijas veids", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Parastais", + "registration-type.admin-approval": "Administratora apstiprināts", + "registration-type.admin-approval-ip": "Administratora apstiprināts dublētām IP adresēm", + "registration-type.invite-only": "Tikai ar ielūgumu", + "registration-type.admin-invite-only": "Tikai ar administratora ielūgumu", + "registration-type.disabled": "Nav", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maksimālais uzaicinājumu skaits katram lietotājam", + "max-invites": "Maksimālais uzaicinājumu skaits katram lietotājam", + "max-invites-help": "Ievadīt 0, lai nebūtu ierobežots. Administratori var vienmēr uzaicināt jaunus lietotājus.
Attiecas tikai uz \"Tikai ar ielūgumu\"", + "invite-expiration": "Ielūguma derīguma termiņš", + "invite-expiration-help": "Cik dienās beidzas ielūguma derīguma termiņš.", + "min-username-length": "Minimālais lietotājvārda garums", + "max-username-length": "Maksimālais lietotājvārda garums", + "min-password-length": "Minimālais paroles garums", + "min-password-strength": "Minimālais paroles stiprums", + "max-about-me-length": "Maksimālais \"Par mani\" garums", + "terms-of-use": "Foruma lietošanas noteikumi (funkcija atspējota ja tukšs)", + "user-search": "Meklējot lietotājos", + "user-search-results-per-page": "Redzamo rezultātu skaits", + "default-user-settings": "Noklusējuma lietotāju iestatījumi", + "show-email": "Rādīt e-pasta adresi", + "show-fullname": "Rādīt vārdu un uzvārdu", + "restrict-chat": "Atļaut sarunas tikai no tiem lietotājiem, kurus es sekoju", + "outgoing-new-tab": "Atvērt izejošās saites jaunā cilnē", + "topic-search": "Iespējot meklēšanu tematu saturā", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Sakopojumu abonements", + "digest-freq.off": "Nav", + "digest-freq.daily": "Ik dienu", + "digest-freq.weekly": "Ik nedēļu", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Ik mēnesi", + "email-chat-notifs": "Sūtīt e-pastu, ja ierodas jauna saruna un es neesmu tiešsaistē", + "email-post-notif": "Sūtīt e-pastu, kad kāds raksta tematā, kuru esmu abonējis", + "follow-created-topics": "Sekot tematiem, kurus esi izveidojis(-jusi)", + "follow-replied-topics": "Sekot tematiem, kuros esi rakstījis(-jusi)", + "default-notification-settings": "Noklusējuma ziņojumu iestatījumi", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/lv/admin/settings/web-crawler.json b/public/language/lv/admin/settings/web-crawler.json new file mode 100644 index 0000000000..24dd40c821 --- /dev/null +++ b/public/language/lv/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Rāpuļprogrammas iestatījumi", + "robots-txt": "Pielāgotais Robots.txt Noklusējuma gadījumā atstāt tukšu", + "sitemap-feed-settings": "Vietnes kartes & RSS plūsmas iestatījumi", + "disable-rss-feeds": "Atspējot RSS plūsmu", + "disable-sitemap-xml": "Atspējot sitemap.xml", + "sitemap-topics": "Cik tematus rādīt vietnes kartē", + "clear-sitemap-cache": "Notīrīt vietnes kartes kešatmiņu", + "view-sitemap": "Skatīt vietnes karti" +} \ No newline at end of file diff --git a/public/language/lv/category.json b/public/language/lv/category.json new file mode 100644 index 0000000000..2f7e110c2b --- /dev/null +++ b/public/language/lv/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategorija", + "subcategories": "Apakškategorijas", + "new_topic_button": "Izveidot jaunu tematu", + "guest-login-post": "Ielogojies lai rakstītu", + "no_topics": "Šinī kategorijā nav rakstu.
Vēlies izveidot kādu rakstu?", + "browsing": "pārlūko", + "no_replies": "Nav atbilžu", + "no_new_posts": "Nav jaunu rakstu.", + "watch": "Novērošana", + "ignore": "Ignorēt", + "watching": "Novērots", + "not-watching": "Not Watching", + "ignoring": "Ignorēts", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Novērotās kategorijas", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/lv/email.json b/public/language/lv/email.json new file mode 100644 index 0000000000..8767a5552d --- /dev/null +++ b/public/language/lv/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Sveiks %1", + "invite": "Uzaicinājums no %1", + "greeting_no_name": "Sveiki", + "greeting_with_name": "Sveiks %1", + "email.verify-your-email.subject": "Lūdzu, apstiprini savu e-pastu", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Paldies, ka reģistrējies %1!", + "welcome.text2": "Lai pilnībā aktivizētu Tavu kontu, mums ir jāpārliecinās, ka Tev pieder e-pasta adrese, ar ko reģistrējies.", + "welcome.text3": "Administrators ir apstiprinājis Tavu reģistrācijas pieteikumu. Tu tagad vari ielogoties ar savu lietotājvārdu un paroli.", + "welcome.cta": "Noklikšķini, lai apstiprinātu savu e-pasta adresi", + "invitation.text1": "%1 ir uzaicinājis Tevi pievienoties %2", + "invitation.text2": "Tavs ielūgums beigsies %1 dienu laikā.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Mēs saņēmām pieprasījumu atiestatīt Tavu paroli, iespējams, tāpēc, ka esi to aizmirsis. Ja tas tā nav, lūdzu, ignorē šo e-pastu.", + "reset.text2": "Lai turpinātu paroles atiestatīšanu, lūdzu, noklikšķini uz šīs saites:", + "reset.cta": "Noklikšķini, lai atiestatītu savu paroli", + "reset.notify.subject": "Parole veiksmīgi mainīta", + "reset.notify.text1": "Mēs Tevi informējam, ka %1 Tava parole tika veiksmīgi mainīta.", + "reset.notify.text2": "Ja neesi to pilnvarojis, nekavējoties informē administratoru par to.", + "digest.latest_topics": "Jaunākie temati no %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Noklikšķini, lai apmeklētu %1", + "digest.unsub.info": "Šis kopsavilkums tika nosūtīts Tev Tavu abonēšanas iestatījumu dēļ.", + "digest.day": "diena", + "digest.week": "nedēļa", + "digest.month": "mēness", + "digest.subject": "Kopsavilkums par %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Jauna saruna saņemta no %1", + "notif.chat.cta": "Noklikšķini, lai turpinātu sarunu", + "notif.chat.unsub.info": "Sarunas paziņojums tika Tev nosūtīts Tavu abonēšanas iestatījumu dēļ.", + "notif.post.unsub.info": "Rakstu paziņojums tika Tev nosūtīts Tavu abonēšanas iestatījumu dēļ.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Šis ir e-pasts, lai pārbaudītu, vai e-pasta sūtītājs ir pareizi iestatīts Tavā NodeBB.", + "unsub.cta": "Noklikšķini, lai mainītu šos iestatījumus", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Tu esi bloķēts no %1", + "banned.text1": "Lietotājs %1 ir bloķēts no %2.", + "banned.text2": "Bloķēšana ilgs līdz %1.", + "banned.text3": "Šis ir iemesls, kāpēc Tu esi bloķēts:", + "closing": "Paldies!" +} \ No newline at end of file diff --git a/public/language/lv/error.json b/public/language/lv/error.json new file mode 100644 index 0000000000..392ca6ea12 --- /dev/null +++ b/public/language/lv/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Nederīgi dati", + "invalid-json": "Nederīgs JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Šķiet, ka neesi ielogojies.", + "account-locked": "Tavs konts ir uz laiku bloķēts", + "search-requires-login": "Meklēšanai nepieciešams konts - lūdzu, ielogojies vai reģistrējies.", + "goback": "Nospiedi atpakaļ, lai atgrieztos iepriekšējā lapā", + "invalid-cid": "Nederīgs kategorijas ID", + "invalid-tid": "Nederīgs temata ID", + "invalid-pid": "Nederīgs raksta ID", + "invalid-uid": "Nederīgs lietotāja ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Nederīgs lietotājvārds", + "invalid-email": "Nederīga e-pasta adrese", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Nederīgs virsraksts", + "invalid-user-data": "Nederīgi lietotāja dati", + "invalid-password": "Nederīga parole", + "invalid-login-credentials": "Nederīgi ielogošanās dati", + "invalid-username-or-password": "Lūdzu, norādi gan lietotājvārdu, gan paroli", + "invalid-search-term": "Nederīga meklēšanas frāze", + "invalid-url": "Nederīga saite\n", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Vietējā ielogošanās ir atspējota nepriviliģētiem kontiem.", + "csrf-invalid": "Mēs nevarējām Tevi ielogot, iespējams, beigušās sesijas dēļ. Lūdzu, mēģināt vēlreiz", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Nederīgs vienību skaits, ir jābūt vismaz %1 un ne vairāk kā %2", + "username-taken": "Lietotājvārds jau izmantots", + "email-taken": "E-pasta adrese jau izmantota", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Nevar sarunāties, kamēr Tava e-pasta adrese netiek apstiprināta, lūdzu, noklikšķini, lai apstiprinātu savu e-pasta adresi.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Mēs nevarējām apstiprināt Tavu e-pasta adresi, lūdzu, vēlāk mēģini vēlreiz.", + "confirm-email-already-sent": "Apstiprinājuma e-pasts ir jau nosūtīts, lūdzu, uzgaidi %1 minūti(-es), lai nosūtītu vēl vienu.", + "sendmail-not-found": "Sendmail programmu nevarēja atrast, lūdzu, pārliecinies, ka lietotājs, kas darbojas ar NodeBB, ir to instalējis un izdarījis palaižamu.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Pārāk īss lietotājvārds", + "username-too-long": "Pārāk garš lietotājvārds", + "password-too-long": "Pārāk gara parole", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Lietotājs ir bloķēts", + "user-banned-reason": "Diemžēl šis konts ir bloķēts (Iemesls: %1)", + "user-banned-reason-until": "Diemžēl šis konts ir bloķēts līdz %1 (Iemesls: %2)", + "user-too-new": "Atvaino, pirms pirmā raksta izveides Tev jāgaida %1 sekundes", + "blacklisted-ip": "Diemžēl Tava IP adrese ir bloķēta šajā kopienā. Ja Tev liekas, ka esam kļūdījušies, lūdzu, sazinies ar administratoru.", + "ban-expiry-missing": "Lūdzu, norādi šīs bloķēšanas beigu datumu", + "no-category": "Kategorija nav atrasta", + "no-topic": "Temats nav atrasts", + "no-post": "Raksts nav atrasts", + "no-group": "Grupa nav atrasta", + "no-user": "Lietotājs nav atrasts", + "no-teaser": "Ievadapraksts nav atrasts", + "no-flag": "Flag does not exist", + "no-privileges": "Tev nepietiek tiesības šai darbībai.", + "category-disabled": "Kategorija ir atspējota", + "topic-locked": "Temats ir slēgts", + "post-edit-duration-expired": "Rakstus drīkst rediģēt tikai līdz %1 sekundēm pēc publicēšanas", + "post-edit-duration-expired-minutes": "Rakstus drīkst rediģēt tikai līdz %1 minūtēm pēc publicēšanas", + "post-edit-duration-expired-minutes-seconds": "Rakstus drīkst rediģēt tikai līdz %1 minūtēm un %2 sekundēm pēc publicēšanas", + "post-edit-duration-expired-hours": "Rakstus drīkst rediģēt tikai līdz %1 stundām pēc publicēšanas", + "post-edit-duration-expired-hours-minutes": "Rakstus drīkst rediģēt tikai līdz %1 stundām un %2 minūtēm pēc publicēšanas", + "post-edit-duration-expired-days": "Rakstus drīkst rediģēt tikai līdz %1 dienām pēc publicēšanas", + "post-edit-duration-expired-days-hours": "Rakstus drīkst rediģēt tikai līdz %1 dienām un %2 stundām pēc publicēšanas", + "post-delete-duration-expired": "Rakstus drīkst izdzēst tikai līdz %1 sekundēm pēc publicēšanas", + "post-delete-duration-expired-minutes": "Rakstus drīkst izdzēst tikai līdz %1 minūtēm pēc publicēšanas", + "post-delete-duration-expired-minutes-seconds": "Rakstus drīkst izdzēst tikai līdz %1 minūtēm un %2 sekundēm pēc publicēšanas", + "post-delete-duration-expired-hours": "Rakstus drīkst izdzēst tikai līdz %1 stundām pēc publicēšanas", + "post-delete-duration-expired-hours-minutes": "Rakstus drīkst izdzēst tikai līdz %1 stundām un %2 minūtēm pēc publicēšanas", + "post-delete-duration-expired-days": "Rakstus drīkst izdzēst tikai līdz %1 dienām pēc publicēšanas", + "post-delete-duration-expired-days-hours": "Rakstus drīkst izdzēst tikai līdz %1 dienām un %2 stundām pēc publicēšanas", + "cant-delete-topic-has-reply": "Nevar izdzēst tematu pēc tam, kad tam ir atbilde", + "cant-delete-topic-has-replies": "Nevar izdzēst tematu pēc tam, kad tam ir %1 atbildes", + "content-too-short": "Lūdzu, ievadīt garāku rakstu. Rakstā jāsatur vismaz %1 rakstzīmes.", + "content-too-long": "Lūdzu, ievadi īsāku rakstu. Rakstā nevar būt vairāk kā %1 rakstzīmju.", + "title-too-short": "Lūdzu, ievadīt garāku virsrakstu. Virsrakstā jāsatur vismaz %1 rakstzīmes.", + "title-too-long": "Lūdzu, ievadi īsāku virsrakstu. Virsrakstā nevar būt vairāk kā %1 rakstzīmes.", + "category-not-selected": "Kategorija nav atlasīta.", + "too-many-posts": "Var publicēt tikai vienu rakstu katras %1 sekundes - lūdzu, uzgaidi, pirms publicē vēlreiz", + "too-many-posts-newbie": "Jauni lietotāji var ievietot tikai vienu rakstu katras %1 sekundes, līdz ir nopelnīti %2 ranga punkti - lūdzu, uzgaidi, pirms publicē vēlreiz", + "already-posting": "You are already posting", + "tag-too-short": "Lūdzu, ievadi garāku birku. Birkā jāsatur vismaz %1 rakstzīmes.", + "tag-too-long": "Lūdzu, ievadi īsāku birku. Birkā nevar būt vairāk kā %1 rakstzīmes.", + "not-enough-tags": "Nav pietiekami daudz birku. Tematiem jābūt vismaz %1 birkām", + "too-many-tags": "Pārāk daudz birku. Tematiem nevar būt vairāk kā %1 birkas", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Lūdzu, uzgaidi, līdz augšupielādes beidzas.", + "file-too-big": "Maksimālais atļautais faila lielums ir %1 kB - lūdzu, augšupielādē mazāku failu", + "guest-upload-disabled": "Viesu failu augšupielāde ir atspējota", + "cors-error": "Neizdevās augšupielādēt bildi nepareizo CORS iestatījumu dēļ", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Tu jau esi atzīmējis šo rakstu ar grāmatzīmi", + "already-unbookmarked": "Tu jau esi noņēmis grāmatzīmi no šī raksta", + "cant-ban-other-admins": "Nevar bloķēt citus administratorus!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Tu esi vienīgais administrators. Pievieno vēl vienu lietotāju kā administratoru, pirms noņemi sevi kā administratoru", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Noņemi administratora tiesības no šī konta, pirms mēģināt to izdzēst.", + "already-deleting": "Already deleting", + "invalid-image": "Nederīgs attēls", + "invalid-image-type": "Nederīgs attēla veids. Atļautie veidi ir: %1", + "invalid-image-extension": "Nederīgs attēla paplašinājums", + "invalid-file-type": "Nederīgs faila veids. Atļautie veidi ir: %1", + "invalid-image-dimensions": "Bildes izmēri ir pārāk lieli", + "group-name-too-short": "Grupas nosaukums ir pārāk īss", + "group-name-too-long": "Grupas nosaukums ir pārāk garš", + "group-already-exists": "Grupa jau pastāv", + "group-name-change-not-allowed": "Grupas nosaukuma maiņa nav atļauta", + "group-already-member": "Jau ir šīs grupas biedrs", + "group-not-member": "Nav šīs grupas biedrs", + "group-needs-owner": "Šai grupai ir nepieciešams vismaz viens īpašnieks", + "group-already-invited": "Šis lietotājs jau ir uzaicināts", + "group-already-requested": "Tavs biedru pieteikums jau ir iesniegts", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Raksts jau ir izdzēsts", + "post-already-restored": "Raksts jau ir atjaunots", + "topic-already-deleted": "Temats jau ir izdzēsts", + "topic-already-restored": "Temats jau ir atjaunots", + "cant-purge-main-post": "Nevar iztīrīt galveno rakstu, lūdzu, tā vietā izdzēsi tematu", + "topic-thumbnails-are-disabled": "Tematu sīktēli ir atspējoti.", + "invalid-file": "Nederīgs fails", + "uploads-are-disabled": "Augšupielāde ir atspējota", + "signature-too-long": "Atvaino, Tavā parakstā nevar būt vairāk kā %1 rakstzīme(-s).", + "about-me-too-long": "Atvaino, Tavā \"Par mani\" nevar būt vairāk kā %1 rakstzīmes.", + "cant-chat-with-yourself": "Nevar sarunāties pats ar sevi!", + "chat-restricted": "Šis lietotājs ir ierobežojis savas sarunas. Viņam ir Tev jāseko, pirms vari sarunāties ar viņu", + "chat-disabled": "Sarunu sistēma ir atspējota", + "too-many-messages": "Tu esi publicējis pārāk daudz rakstu, lūdzu, kādu laiku uzgaidi.", + "invalid-chat-message": "Nederīga saruna", + "chat-message-too-long": "Sarunā nevar būt vairāk kā %1 rakstzīmes.", + "cant-edit-chat-message": "Nav atļauts rediģēt šo rakstu", + "cant-delete-chat-message": "Nav atļauts izdzēst šo rakstu", + "chat-edit-duration-expired": "Pēc publicēšanas ir atļauts tikai %1 sekundes laika rediģēt sarunu", + "chat-delete-duration-expired": "Pēc publicēšanas ir atļauts tikai %1 sekundes laika izdzēst sarunu", + "chat-deleted-already": "Saruna jau ir izdzēsta.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Tu jau balsoji par šo rakstu.", + "reputation-system-disabled": "Ranga punktu sistēma ir atspējota.", + "downvoting-disabled": "Balsošana \"pret\" ir atspējota", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Nevar balsot pats par savu rakstu", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB radās problēma pārlādēšanas laikā: \"%1\". NodeBB turpinās apkalpot esošos klienta puses failus, lai gan Tev būtu jāatceļ tas, ko Tu darīji tieši pirms pārlādēšanas.", + "registration-error": "Kļūda pie reģistrācijas", + "parse-error": "Radās kļūda parsējot servera atbildi", + "wrong-login-type-email": "Lūdzu, izmanto savu e-pasta adresi, lai ielogotos", + "wrong-login-type-username": "Lūdzu, izmanto savu lietotājvārdu, lai ielogotos", + "sso-registration-disabled": "Reģistrācija ir atspējota %1 kontiem, lūdzu, reģistrējies vispirms ar e-pasta adresi", + "sso-multiple-association": "Nevar saistīt vairākus kontus no šī pakalpojuma savā NodeBB kontā. Lūdzu, nošķiri savu esošo kontu un mēģini vēlreiz.", + "invite-maximum-met": "Tu esi uzaicinājis maksimālo cilvēku skaitu (%1 no %2).", + "no-session-found": "Aktīvo sesiju nevarēja atrast!", + "not-in-room": "Lietotājs nav tērzētavā", + "cant-kick-self": "Nevar sevi izslēgt no grupas", + "no-users-selected": "Nav atlasīts neviens lietotājs(-i)", + "invalid-home-page-route": "Nederīgs sākumlapas ceļš", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Nav atlasīts neviens temats", + "cant-move-to-same-topic": "Nevar pārnest uz savu pašu tematu!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Nevar pats sevi bloķēt!", + "cannot-block-privileged": "Nevar bloķēt administratorus vai globālos moderatorus", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "Šķiet, ka pastāv problēma ar Tavu interneta savienojumu", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/lv/flags.json b/public/language/lv/flags.json new file mode 100644 index 0000000000..45d04d7dc6 --- /dev/null +++ b/public/language/lv/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Stāvoklis", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Labi! Nav atzīmju.", + "assignee": "Piešķirtais", + "update": "Atjaunot", + "updated": "Atjaunots", + "resolved": "Resolved", + "target-purged": "Saturs, uz kā attiecas atzīme, ir iztīrīts un vairs nav pieejams.", + + "graph-label": "Daily Flags", + "quick-filters": "Ātrie filtri", + "filter-active": "Atzīmju sarakstā ir aktīvs viens vai vairāki filtri", + "filter-reset": "Noņemt filtrus", + "filters": "Filtrēšanas opcijas", + "filter-reporterId": "Ziņotāja UID", + "filter-targetUid": "Atzīmētā raksta UID", + "filter-type": "Atzīmes veids", + "filter-type-all": "Viss saturs", + "filter-type-post": "Raksts", + "filter-type-user": "Lietotājs", + "filter-state": "Stāvoklis", + "filter-assignee": "Piešķirtā UID", + "filter-cid": "Kategorija", + "filter-quick-mine": "Piešķirts man", + "filter-cid-all": "Visas kategorijas", + "apply-filters": "Iespējot filtrus", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Atzīmētais lietotājs", + "view-profile": "Skatīt profilu", + "start-new-chat": "Sākt jaunu sarunu", + "go-to-target": "Skatīt atzīmēto rakstu", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Skatīt profilu", + "user-edit": "Rediģēt profilu", + + "notes": "Atzīmju piezīmes", + "add-note": "Pievienot piezīmi", + "no-notes": "Nav kopīgu piezīmju.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Piezīme pievienota", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Nav atzīmju vēsture.", + + "state-all": "Visi stāvokļi", + "state-open": "Sākt jaunu/atvērt", + "state-wip": "Darbība iesākta", + "state-resolved": "Atrisināts", + "state-rejected": "Noraidīts", + "no-assignee": "Nav piešķirts", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Lūdzu, norādi iemeslu, kāpēc %1 %2 ir atzīmēts pārskatīšanai. Citādi, izmanto vienu no ātrā ziņojuma pogām.", + "modal-reason-spam": "Mēstule", + "modal-reason-offensive": "Aizskarošs", + "modal-reason-other": "Cits (norādīt zemāk)", + "modal-reason-custom": "Iemesls, kāpēc ziņots par saturu...", + "modal-submit": "Iesniegt ziņojumu", + "modal-submit-success": "Saturs ir atzīmēts moderēšanai.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/lv/global.json b/public/language/lv/global.json new file mode 100644 index 0000000000..85848c8caf --- /dev/null +++ b/public/language/lv/global.json @@ -0,0 +1,126 @@ +{ + "home": "Sākums", + "search": "Meklēt", + "buttons.close": "Aizvērt", + "403.title": "Piekļuve liegta", + "403.message": "Šķiet, ka esi uznācis uz lapu, kurai Tev nav piekļuves.", + "403.login": "Varbūt Tev vajadzētu mēģināt ielogoties?", + "404.title": "Nav atrasts", + "404.message": "Šķiet, ka esi uznācis uz lapu, kura neeksistē. Atgriezies sākumlapā.", + "500.title": "Iekšēja kļūda.", + "500.message": "Hmm... Izskatās, ka kaut kas noticis nepareizi!", + "400.title": "Nepareizs pieprasījums.", + "400.message": "Šķiet, ka šī saite ir nepareiza, lūdzu, pārbaudi un mēģini vēlreiz. Pretējā gadījumā atgriezies sākumlapā.", + "register": "Reģistrēties", + "login": "Ielogoties", + "please_log_in": "Lūdzu, ielogoties", + "logout": "Izlogoties", + "posting_restriction_info": "Pašlaik publicēšana pieejama tikai reģistrētiem biedriem, lai ielogotos, noklikšķini šeit.", + "welcome_back": "Sveiks atpakaļ", + "you_have_successfully_logged_in": "Tu esi veiksmīgi ielogojies", + "save_changes": "Saglabāt izmaiņas", + "save": "Saglabāt", + "close": "Aizvērt", + "pagination": "Dalīšana pa lapām", + "pagination.out_of": "%1 no %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Administrācija", + "header.categories": "Kategorijas", + "header.recent": "Nesenie", + "header.unread": "Nelasītie", + "header.tags": "Birkas", + "header.popular": "Populārākie", + "header.top": "Top", + "header.users": "Lietotāji", + "header.groups": "Grupas", + "header.chats": "Sarunas", + "header.notifications": "Paziņojumi", + "header.search": "Meklēt", + "header.profile": "Profils", + "header.navigation": "Navigācija", + "notifications.loading": "Ielādē paziņojumus", + "chats.loading": "Ielādē sarunas", + "motd.welcome": "Sveicināts NodeBB, nākotnes diskusiju platformā.", + "previouspage": "Iepriekšējā lapa", + "nextpage": "Nākamā lapa", + "alert.success": "Veiksme", + "alert.error": "Kļūda", + "alert.banned": "Bloķētie", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Tu vairs neseko %1!", + "alert.follow": "Tu tagad seko %1!", + "users": "Lietotāji", + "topics": "Temati", + "posts": "Raksti", + "x-posts": "%1 posts", + "best": "Labākie", + "controversial": "Controversial", + "votes": "Balsojumi", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Balsojuši \"par\"", + "upvoted": "Balsojis \"par\"", + "downvoters": "Balsojuši \"pret\"", + "downvoted": "Balsojis \"pret\"", + "views": "Skatījumi", + "posters": "Posters", + "reputation": "Ranga punkti", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "lasīt vairāk", + "more": "Vairāk", + "none": "None", + "posted_ago_by_guest": "Viesis publicēja %1", + "posted_ago_by": "%2 publicēja %1", + "posted_ago": "publicēts \"%1\"", + "posted_in": "publicēts kategorijā \"%1\"", + "posted_in_by": "%2 publicēja kategorijā %1", + "posted_in_ago": "publicēts kategorijā %1 %2", + "posted_in_ago_by": "%3 publicēja kategorijā %1 %2", + "user_posted_ago": "%1 publicēja %2", + "guest_posted_ago": "Viesis publicēja %1", + "last_edited_by": "pēdējoreiz rediģējis %1", + "norecentposts": "Nav nesenu rakstu", + "norecenttopics": "Nav neseno tematu", + "recentposts": "Nesenie raksti", + "recentips": "Nesen lietotās IP adreses", + "moderator_tools": "Moderatora rīki", + "online": "Klāt", + "away": "Projām", + "dnd": "Netraucējams", + "invisible": "Neredzams", + "offline": "Bezsaistē", + "email": "E-pasta adrese", + "language": "Valoda", + "guest": "Viesis", + "guests": "Viesi", + "former_user": "Bijušais lietotājs", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forums ir atjaunināts", + "updated.message": "Forums tikko tika atjaunināts līdz jaunākajai versijai. Noklikšķini šeit, lai atsvaidzinātu lapu.", + "privacy": "Privātums", + "follow": "Sekot", + "unfollow": "Nesekot", + "delete_all": "Izdzēst visus", + "map": "Karte", + "sessions": "Aktīvās sesijas", + "ip_address": "IP adrese", + "enter_page_number": "Ievadīt lapas numuru", + "upload_file": "Augšupielādēt failu", + "upload": "Augšupielādēt", + "uploads": "Augšupielādes", + "allowed-file-types": "Atļautie faila veidi ir %1", + "unsaved-changes": "Tev ir nesaglabātas izmaiņas. Vai tiešām vēlies doties projām?", + "reconnecting-message": "Šķiet, ka Tavs savienojums ar %1 tika pazaudēts, lūdzu, uzgaidi, kamēr mēģinām atkal pievienoties.", + "play": "Spēlēt", + "cookies.message": "Šī vietne izmanto sīkfailus, lai nodrošinātu, ka Tu iegūsti vislabāko pieredzi mūsu vietnē.", + "cookies.accept": "Sapratu!", + "cookies.learn_more": "Uzzināt vairāk", + "edited": "Rediģētie", + "disabled": "Atspējotie", + "select": "Atlasīt", + "user-search-prompt": "Ieraksti kaut ko šeit, lai meklētu lietotājus..." +} \ No newline at end of file diff --git a/public/language/lv/groups.json b/public/language/lv/groups.json new file mode 100644 index 0000000000..1cfc699bb1 --- /dev/null +++ b/public/language/lv/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupas", + "view_group": "Skatīt grupas", + "owner": "Grupas īpašnieks", + "new_group": "Izveidot jaunu grupu", + "no_groups_found": "Nav grupu", + "pending.accept": "Pieņemt", + "pending.reject": "Noraidīt", + "pending.accept_all": "Pieņemt visus", + "pending.reject_all": "Atraidīt visus", + "pending.none": "Šobrīd nav neviena neapstiprināta biedra", + "invited.none": "Šobrīd nav neviena uzaicināta biedra", + "invited.uninvite": "Atsaukt ielūgumu", + "invited.search": "Meklēt lietotājus, kurus uzaicināt šinī grupā", + "invited.notification_title": "Tu esi uzaicināts pievienoties %1", + "request.notification_title": "Grupas dalības pieprasījums no %1", + "request.notification_text": "%1 ir pieprasījis kļūt par %2 biedru", + "cover-save": "Saglabāt", + "cover-saving": "Saglabā", + "details.title": "Grupas informācija", + "details.members": "Biedri", + "details.pending": "Neapstiprinātie biedri", + "details.invited": "Uzaicinātie biedri", + "details.has_no_posts": "Šīs grupas biedri nav publicējuši nevienu rakstu.", + "details.latest_posts": "Pēdējie raksti", + "details.private": "Privāta", + "details.disableJoinRequests": "Atspējot biedra pieprasījumus", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Piešķirt/atsaukt īpašumtiesības", + "details.kick": "Izslēgt", + "details.kick_confirm": "Vai tiešām vēlies izslēgt šo biedru no grupas?", + "details.add-member": "Pievienot biedru", + "details.owner_options": "Grupas administrācija", + "details.group_name": "Pēc nosaukuma", + "details.member_count": "Pēc biedru skaita", + "details.creation_date": "Pēc datuma", + "details.description": "Apraksts", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Etiķetes priekšskats", + "details.change_icon": "Mainīt ikonu", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Etiķetes teksts", + "details.userTitleEnabled": "Rādīt etiķeti", + "details.private_help": "Pievienoties grupai nepieciešama grupas īpašnieka apstiprināšana", + "details.hidden": "Paslēpta", + "details.hidden_help": "Grupa nav redzama grupu sarakstā un lietotāji ir jāuzaicina pašrocīgi", + "details.delete_group": "Izdzēst grupu", + "details.private_system_help": "Privātās grupas ir atspējotas sistēmas līmenī, šī opcija nedara neko", + "event.updated": "Grupas informācija ir atjaunināta", + "event.deleted": "Grupa %1 ir izdzēsta", + "membership.accept-invitation": "Pieņemt ielūgumu", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Neapstiprināts ielūgums", + "membership.join-group": "Pievienoties grupai", + "membership.leave-group": "Atstāt grupu", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Noraidīt", + "new-group.group_name": "Grupas nosaukums", + "upload-group-cover": "Augšupielādēt grupas galvenes bildi", + "bulk-invite-instructions": "Ievadi sarakstu ar lietotājvārdiem, atdalītajiem ar komatu, kurus uzaicināt uz šo grupu", + "bulk-invite": "Lielapjoma uzaicinājums", + "remove_group_cover_confirm": "Vai tiešām vēlies noņemt galvenes bildi?" +} \ No newline at end of file diff --git a/public/language/lv/ip-blacklist.json b/public/language/lv/ip-blacklist.json new file mode 100644 index 0000000000..c13c92bba0 --- /dev/null +++ b/public/language/lv/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Iestatīt IP adrešu melno sarakstu šeit.", + "description": "Reizēm lietotāja konta bloķēšana nav pietiekošs preventīvs līdzeklis. Šādā gadījumā labākais veids, kā aizsargāt forumu, ir ierobežot piekļuvi forumam konkrētai IP adresei vai vairākām IP adresēm. Šajos scenārijos var ievietot traucējošās IP adreses vai veselus CIDR blokus šajā melnajā sarakstā, un no viņām neļaus ielogoties vai reģistrēt jaunu kontu.", + "active-rules": "Aktīvās rindiņas", + "validate": "Pārbaudīt melno sarakstu", + "apply": "Saglabāt melno sarakstu", + "hints": "Sintakses padomi", + "hint-1": "Noteikt vienu IP adresi katrā rindiņā. Var arī noteikt IP blokus, ja vien tie ievēro CIDR formātu (piemēram, 192.168.100.0/22).", + "hint-2": "Var pievienot komentārus, sākot rindiņas ar simbolu #.", + + "validate.x-valid": "%1 no %2 rinda(-s) derīga(-s).", + "validate.x-invalid": "Šie %1 noteikumi nav derīgi:", + + "alerts.applied-success": "Melnais saraksts iespējots", + + "analytics.blacklist-hourly": "Attēls 1 – Melnā saraksta trāpījumi stundā", + "analytics.blacklist-daily": "Attēls 2 – Melnā saraksta trāpījumi dienā", + "ip-banned": "IP adrese bloķēta" +} \ No newline at end of file diff --git a/public/language/lv/language.json b/public/language/lv/language.json new file mode 100644 index 0000000000..d09534adb8 --- /dev/null +++ b/public/language/lv/language.json @@ -0,0 +1,5 @@ +{ + "name": "Latviešu", + "code": "lv", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/lv/login.json b/public/language/lv/login.json new file mode 100644 index 0000000000..26b7bc57df --- /dev/null +++ b/public/language/lv/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Lietotājvārds / e-pasta adrese", + "username": "Lietotājvārds", + "remember_me": "Atcerēties mani?", + "forgot_password": "Aizmirsi paroli?", + "alternative_logins": "Alternatīvie lietotājvārdi", + "failed_login_attempt": "Tev ielogoties neveiksmējās", + "login_successful": "Tu esi veiksmīgi ielogojies!", + "dont_have_account": "Tev nav konta?", + "logged-out-due-to-inactivity": "Neaktivitātes dēļ Tu esi bijis izlogots no administrācijas vadības paneļa", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/lv/modules.json b/public/language/lv/modules.json new file mode 100644 index 0000000000..95eeb481cd --- /dev/null +++ b/public/language/lv/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Sarunāties ar", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Nosūtīt", + "chat.no_active": "Nav aktīvo sarunu.", + "chat.user_typing": "%1 raksta...", + "chat.user_has_messaged_you": "%1 ir sācis ar Tevi sarunāties", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Lūdzu, izvēlies adresātu, lai skatītu sarunu vēsturi", + "chat.no-users-in-room": "Šajā tērzētavā nav lietotāju", + "chat.recent-chats": "Nesenās sarunas", + "chat.contacts": "Kontaktpersonas", + "chat.message-history": "Sarunu vēsture", + "chat.message-deleted": "Message Deleted", + "chat.options": "Sarunu iestatījumi", + "chat.pop-out": "Uznirstošā saruna", + "chat.minimize": "Minimizēt", + "chat.maximize": "Maksimizēt", + "chat.seven_days": "7 dienas", + "chat.thirty_days": "30 dienas", + "chat.three_months": "3 mēneši", + "chat.delete_message_confirm": "Vai tiešām vēlies izdzēst šo sarunu?", + "chat.retrieving-users": "Ielādē lietotājus...", + "chat.manage-room": "Pārvaldīt tērzētavu", + "chat.add-user-help": "Meklē lietotājus šeit. Izvēlētais lietotājs tiks pievienots sarunai. Jaunais lietotājs neredzēs sarunas, kas rakstītas pirms viņu pievienoja sarunai. Tikai tērzētavas īpašnieks(-i) var noņemt lietotājus no tērzētavām.", + "chat.confirm-chat-with-dnd-user": "Lietotājs ir iestatījis savu statusu uz DnD (netraucējams). Vai Tu joprojām vēlies sarunāties ar viņu?", + "chat.rename-room": "Pārdēvēt tērzētavu", + "chat.rename-placeholder": "Ievadi savas tērzētavas nosaukumu šeit", + "chat.rename-help": "Šeit norādītais tērzētavas nosaukums būs redzams visiem dalībniekiem.", + "chat.leave": "Pamest sarunu", + "chat.leave-prompt": "Vai tiešām vēlies pamest šo sarunu?", + "chat.leave-help": "Atstājot šo sarunu, Tu tiksi noņemts no turpmākām sarunām. Ja Tu nākotnē atkārtoti pievienojies, Tu neredzēsi nevienu sarunu no pirms tā brīža.", + "chat.in-room": "Šajā tērzētavā", + "chat.kick": "Izslēgt", + "chat.show-ip": "Rādīt IP adresi", + "chat.owner": "Tērzētavas īpašnieks", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Rediģēt", + "composer.show_preview": "Rādīt priekšskatu", + "composer.hide_preview": "Slēpt priekšskatu", + "composer.user_said_in": "%1 sacīja %2:", + "composer.user_said": "%1 sacīja:", + "composer.discard": "Vai tiešām vēlies atmest šo rakstu?", + "composer.submit_and_lock": "Iesniegt un aizslēgt", + "composer.toggle_dropdown": "Pārslēgt izvēlni", + "composer.uploading": "Augšupielādē %1", + "composer.formatting.bold": "Treknrakstā", + "composer.formatting.italic": "Slīprakstā", + "composer.formatting.list": "Saraksts", + "composer.formatting.strikethrough": "Svītrotā rakstā", + "composer.formatting.code": "Koda gabals", + "composer.formatting.link": "Saite", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Augšupielādēt bildi", + "composer.upload-file": "Augšupielādēt failu", + "composer.zen_mode": "Zen režīms", + "composer.select_category": "Izvēlēties kategoriju", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "Labi", + "bootbox.cancel": "Atcelt", + "bootbox.confirm": "Apstiprināt", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Novietot galvenes bildi", + "cover.dragging_message": "Velc galvenes bildi vēlamajā vietā un noklikšķini uz \"Saglabāt\"", + "cover.saved": "Galvenes bilde un novietojums saglabāta", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/lv/notifications.json b/public/language/lv/notifications.json new file mode 100644 index 0000000000..96575343ad --- /dev/null +++ b/public/language/lv/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Paziņojumi", + "no_notifs": "Nav jaunu paziņojumu", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Atpakaļ uz %1", + "outgoing_link": "Izejošā saite", + "outgoing_link_message": "Tu tagad atstāj %1", + "continue_to": "Turpināt uz %1", + "return_to": "Atgriezties pie %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Ir nelasīti paziņojumi.", + "all": "Visi", + "topics": "Par tematiem", + "replies": "Par atbildēm", + "chat": "Par sarunām", + "group-chat": "Group Chats", + "follows": "Par tiem, kurus sekoju", + "upvote": "Par balsojumiem \"par\"", + "new-flags": "Jaunās atzīmes", + "my-flags": "Atzīmes piešķirtas man", + "bans": "Bloķēšanas", + "new_message_from": "Jauns raksts no %1", + "upvoted_your_post_in": "%1 ir balsojis \"par\" Tavu rakstu%2.", + "upvoted_your_post_in_dual": "%1 un %2 ir balsojuši \"par\" Tavu rakstu %3.", + "upvoted_your_post_in_multiple": "%1 un %2 citi ir balsojuši \"par\" Tavu rakstu %3.", + "moved_your_post": "%1 ir pārvietojis Tavu rakstu %2", + "moved_your_topic": "%1 ir pārvietojis %2", + "user_flagged_post_in": "%1 ir atzīmējis rakstu %2", + "user_flagged_post_in_dual": "%1 un %2 ir atzīmējuši rakstu %3", + "user_flagged_post_in_multiple": "%1 un %2 citi ir atzīmējuši rakstu %3", + "user_flagged_user": "%1 ir atzīmējis lietotāja profilu (%2)", + "user_flagged_user_dual": "%1 un %2 ir atzīmējuši lietotāja profilu (%3)", + "user_flagged_user_multiple": "%1 un %2 citi ir atzīmējuši lietotāja profilu (%3)", + "user_posted_to": "%1 ir atbildējis: %2", + "user_posted_to_dual": "%1 un %2 ir atbildējuši %3", + "user_posted_to_multiple": "%1 un %2 citi ir atbildējuši %3", + "user_posted_topic": "%1 ir ievietojis jaunu tematu: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 sāka Tev sekot.", + "user_started_following_you_dual": "%1 un %2 sāka Tev sekot.", + "user_started_following_you_multiple": "%1 un %2 citi sāka Tev sekot.", + "new_register": "%1 sūtīja reģistrācijas pieteikumu.", + "new_register_multiple": "Ir %1 reģistrācijas pietiekumi, kas jāpārskata.", + "flag_assigned_to_you": "Atzīme %1 ir piešķirta Tev", + "post_awaiting_review": "Raksts, kas jāpārskata", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-pasta adrese ir apstiprināta", + "email-confirmed-message": "Paldies, ka apstiprināji e-pasta adresi. Tavs konts tagad ir pilnībā aktivizēts.", + "email-confirm-error-message": "Tavā e-pasta adreses apstiprināšanā radās problēma. Iespējams, kods ir nederīgs vai ir beidzies derīguma termiņš.", + "email-confirm-sent": "Apstiprinājuma e-pasts ir nosūtīts.", + "none": "Neko nedarīt", + "notification_only": "Tikai paziņot", + "email_only": "Sūtīt e-pastu", + "notification_and_email": "Paziņot un sūtīt e-pastu", + "notificationType_upvote": "Kad kāds balso \"par\" Tavu rakstu", + "notificationType_new-topic": "Kad kāds, kuru Tu seko, publicē rakstu", + "notificationType_new-reply": "Kad jauna atbilde tiek pievienota tematam, kuru novēro", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "Kad kāds sāk Tev sekot", + "notificationType_new-chat": "Kad saņemi sarunu", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "Kad saņemi ielūgumu pievienoties grupai", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "Kad kāds tiek ievietots reģistrācijas rindā", + "notificationType_post-queue": "Kad raksts tiek ievietots apstiprināšanas rindā", + "notificationType_new-post-flag": "Kad raksts tiek atzīmēts", + "notificationType_new-user-flag": "Kad lietotājs tiek atzīmēts" +} \ No newline at end of file diff --git a/public/language/lv/pages.json b/public/language/lv/pages.json new file mode 100644 index 0000000000..03d8154b8c --- /dev/null +++ b/public/language/lv/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Sākums", + "unread": "Nelasītie temati", + "popular-day": "Populārākie temati šodien", + "popular-week": "Populārākie temati šonedēļ", + "popular-month": "Populārākie temati šomēnes", + "popular-alltime": "Visu laiku populārākie temati", + "recent": "Nesenie temati", + "top-day": "Visvairāk balsotie temati šodien", + "top-week": "Visvairāk balsotie temati šonedēļ", + "top-month": "Visvairāk balsotie temati šomēnes", + "top-alltime": "Visvairāk balsotie temati", + "moderator-tools": "Moderatora rīki", + "flagged-content": "Atzīmētais saturs", + "ip-blacklist": "IP adrešu melnais saraksts", + "post-queue": "Rakstu apstiprināšanas rinda", + "users/online": "Lietotāji tiešsaistē", + "users/latest": "Jaunākie lietotāji", + "users/sort-posts": "Lietotāji ar visvairāk rakstu", + "users/sort-reputation": "Lietotāji ar visvairāk ranga punktu", + "users/banned": "Bloķētie lietotāji", + "users/most-flags": "Visvairāk atzīmēto lietotāju", + "users/search": "Meklēt lietotājus", + "notifications": "Paziņojumi", + "tags": "Birkas", + "tag": "Temati, kas atzīmēti ar "%1"", + "register": "Reģistrēt kontu", + "registration-complete": "Reģistrācija ir pabeigta", + "login": "Ielogoties savā kontā", + "reset": "Atiestatīt sava konta paroli", + "categories": "Kategorijas", + "groups": "Grupas", + "group": "Grupa %1", + "chats": "Sarunas", + "chat": "Sarunājās ar %1", + "flags": "Atzīmes", + "flag-details": "Atzīmes %1 informācija", + "account/edit": "Rediģēt \"%1\"", + "account/edit/password": "Rediģēt \"%1\" paroli", + "account/edit/username": "Rediģēt \"%1\" lietotājvārdu", + "account/edit/email": "Rediģēt \"%1\" e-pasta adresi", + "account/info": "Konta info", + "account/following": "Tie, kuri %1 seko", + "account/followers": "Tie, kuri seko %1", + "account/posts": "Rakstījis(-jusi) %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Tematus izveidojis(-jusi) %1", + "account/groups": "%1 grupas", + "account/watched_categories": "%1 novērotās kategorijas", + "account/bookmarks": "%1 atzīmētie raksti", + "account/settings": "Lietotāja iestatījumi", + "account/watched": "Temati, kurus %1 novēro", + "account/ignored": "Temati, kurus %1 ignorē", + "account/upvoted": "%1 balsojis \"par\"", + "account/downvoted": "%1 balsojis \"pret\"", + "account/best": "%1 labākie raksti", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "%1 bloķētie lietotāji", + "account/uploads": "%1 augšupielādes", + "account/sessions": "Aktīvās sesijas", + "confirm": "E-pasta adrese apstiprināta", + "maintenance.text": "%1 šobrīd notiek apkope. Lūdzu, atgriezies vēlāk.", + "maintenance.messageIntro": "Turklāt administrators ir atstājis šo paziņojumu:", + "throttled.text": "%1 šobrīd nav pieejams pārmērīgas slodzes dēļ. Lūdzu, atgriezies vēlāk." +} \ No newline at end of file diff --git a/public/language/lv/post-queue.json b/public/language/lv/post-queue.json new file mode 100644 index 0000000000..79979823fb --- /dev/null +++ b/public/language/lv/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Rakstu apstiprināšanas rinda", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "Lietotājs", + "category": "Kategorija", + "title": "Virsraksts", + "content": "Saturs", + "posted": "Datums", + "reply-to": "Atbildēt \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/lv/recent.json b/public/language/lv/recent.json new file mode 100644 index 0000000000..9187e1ca49 --- /dev/null +++ b/public/language/lv/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Nesenie", + "day": "Šodien", + "week": "Šonedēļ", + "month": "Šomēnes", + "year": "Šogad", + "alltime": "Visu laiku", + "no_recent_topics": "Nav neseno tematu.", + "no_popular_topics": "Nav populāro tematu.", + "there-is-a-new-topic": "Ir jauns temats.", + "there-is-a-new-topic-and-a-new-post": "Ir jauns temats un jauns raksts.", + "there-is-a-new-topic-and-new-posts": "Ir jauns temats un %1 jauni raksti.", + "there-are-new-topics": "Ir %1 jauni temati.", + "there-are-new-topics-and-a-new-post": "Ir %1 jauni temati un jauns raksts.", + "there-are-new-topics-and-new-posts": "Ir %1 jauni temati un %2 jauni raksti.", + "there-is-a-new-post": "Ir jauns raksts.", + "there-are-new-posts": "Ir %1 jauni raksti.", + "click-here-to-reload": "Noklikšķini, lai pārlādētu." +} \ No newline at end of file diff --git a/public/language/lv/register.json b/public/language/lv/register.json new file mode 100644 index 0000000000..3a48e2235a --- /dev/null +++ b/public/language/lv/register.json @@ -0,0 +1,32 @@ +{ + "register": "Reģistrēties", + "cancel_registration": "Atcelt reģistrācijas pieteikumu", + "help.email": "Pēc noklusējuma Tava e-pasta adrese nebūs redzama ārpus NodeBB.", + "help.username_restrictions": "Unikāls lietotājvārds starp %1 un %2 rakstzīmēm. Citi var pieminēt Tevi izmantojot @lietotājvārds.", + "help.minimum_password_length": "Tavas paroles garumam jābūt vismaz %1 rakstzīmēm.", + "email_address": "E-pasta adrese", + "email_address_placeholder": "Ievadīt e-pasta adresi", + "username": "Lietotājvārds", + "username_placeholder": "Ievadīt lietotājvārdu", + "password": "Parole", + "password_placeholder": "Ievadīt paroli", + "confirm_password": "Apstiprināt paroli", + "confirm_password_placeholder": "Apstiprināt paroli", + "register_now_button": "Reģistreties tagad", + "alternative_registration": "Alternatīva reģistrācija", + "terms_of_use": "Lietošanas noteikumi", + "agree_to_terms_of_use": "Es piekrītu lietošanas noteikumiem", + "terms_of_use_error": "Tev ir jāpiekrīt lietošanas noteikumiem", + "registration-added-to-queue": "Tavs reģistrācijas pieteikums ir ievietots reģistrācijas rindā. Tu saņemsi e-pastu, kad administrators to apstiprinās.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Es piekrītu manas personas informācijas vākšanai un apstrādei šajā vietnē.", + "gdpr_agree_email": "Es piekrītu saņemt sakopojumu un paziņojumu e-pastus no šīs vietnes.", + "gdpr_consent_denied": "Tev ir jādod piekrišana šai vietnei, lai savāktu / apstrādātu Tavu informāciju un nosūtītu Tev e-pastus.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/lv/reset_password.json b/public/language/lv/reset_password.json new file mode 100644 index 0000000000..afdb91f7ca --- /dev/null +++ b/public/language/lv/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Atiestatīt paroli", + "update_password": "Atjaunināt paroli", + "password_changed.title": "Parole ir mainīta", + "password_changed.message": "

Parole ir veiksmīgi atiestatīta, lūdzuielogojies vēlreiz.", + "wrong_reset_code.title": "Nepareizs atiestatīšanas kods", + "wrong_reset_code.message": "Saņemtais atiestatīšanas kods ir nepareizs. Lūdzu, mēģini vēlreiz vai pieprasi jaunu atiestatīšanas kodu.", + "new_password": "Jaunā parole", + "repeat_password": "Apstiprināt paroli", + "changing_password": "Changing Password", + "enter_email": "Ievadīt savu e-pasta adresi, un mēs Tev nosūtīsim e-pastu ar norādījumiem par to, kā atiestatīt savu kontu.", + "enter_email_address": "Ievadīt e-pasta adresi", + "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.", + "invalid_email": "Nederīga e-pasta adrese / e-pasta adrese neeksistē!", + "password_too_short": "Ievadītā parole ir pārāk īsa, lūdzu, izvēlēties citu paroli.", + "passwords_do_not_match": "Abas ievadītās paroles nesakrīt.", + "password_expired": "Tava parole ir beigusies, lūdzu, izvēlies jaunu paroli" +} \ No newline at end of file diff --git a/public/language/lv/search.json b/public/language/lv/search.json new file mode 100644 index 0000000000..8db217d04b --- /dev/null +++ b/public/language/lv/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 rezultāts(-i), kas atbilst \"%2\", (%3 sekundes)", + "no-matches": "Sakritības nav atrastas", + "advanced-search": "Meklēt izvērsti", + "in": "Kur", + "titles": "Nosaukumos", + "titles-posts": "Nosaukumos un rakstos", + "match-words": "Meklēt vārdus", + "all": "Visus", + "any": "Jebkurus", + "posted-by": "Publicējis", + "in-categories": "Kategorijās", + "search-child-categories": "Meklēt apakškategorijas", + "has-tags": "Ar birkām", + "reply-count": "Atbilžu skaits", + "at-least": "Vismaz", + "at-most": "Ne vairāk kā", + "relevance": "Pēc atbilstības", + "post-time": "Publicēšanas datums", + "votes": "Balsojumi", + "newer-than": "Jaunāks nekā", + "older-than": "Vecāks nekā", + "any-date": "Jebkurš datums", + "yesterday": "Vakar", + "one-week": "Viena nedēļa", + "two-weeks": "Divas nedēļas", + "one-month": "Viens mēnesis", + "three-months": "Trīs mēneši", + "six-months": "Seši mēneši", + "one-year": "Viens gads", + "sort-by": "Kārtošana", + "last-reply-time": "Pēc pēdējās atbildes laika", + "topic-title": "Pēc temata nosaukuma", + "topic-votes": "Tematu balsojumi", + "number-of-replies": "Pēc atbilžu skaita", + "number-of-views": "Pēc skatījumu skaita", + "topic-start-date": "Pēc temata sākuma datuma", + "username": "Pēc lietotājvārda", + "category": "Pēc kategorijas", + "descending": "Dilstošā secībā", + "ascending": "Augošā secībā", + "save-preferences": "Saglabāt izvēles", + "clear-preferences": "Notīrīt izvēles", + "search-preferences-saved": "Meklēšanas izvēles saglabātas", + "search-preferences-cleared": "Meklēšanas izvēles notīrītas", + "show-results-as": "Rādīt rezultātus kā", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/lv/success.json b/public/language/lv/success.json new file mode 100644 index 0000000000..e44895da00 --- /dev/null +++ b/public/language/lv/success.json @@ -0,0 +1,7 @@ +{ + "success": "Veiksme", + "topic-post": "Veiksmīgi publicēts.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Veiksmīgi autentificējies", + "settings-saved": "Iestatījumi saglabāti!" +} \ No newline at end of file diff --git a/public/language/lv/tags.json b/public/language/lv/tags.json new file mode 100644 index 0000000000..d52ec26718 --- /dev/null +++ b/public/language/lv/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nav neviena temata ar šo birku", + "tags": "Birkas", + "enter_tags_here": "Ievadīt birkas, katrai starp %1 un %2 rakstzīmēm", + "enter_tags_here_short": "Ievadīt birkas...", + "no_tags": "Nav birku.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/lv/top.json b/public/language/lv/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/lv/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/lv/topic.json b/public/language/lv/topic.json new file mode 100644 index 0000000000..abe07301fe --- /dev/null +++ b/public/language/lv/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Temats", + "title": "Title", + "no_topics_found": "Nav tematu!", + "no_posts_found": "Nav rakstu!", + "post_is_deleted": "Raksts izdzēsts!", + "topic_is_deleted": "Temats izdzēsts!", + "profile": "Profils", + "posted_by": "Publicēja %1", + "posted_by_guest": "Publicēja viesis", + "chat": "Sarunāties", + "notify_me": "Tiec informēts par jaunām atbildēm šajā tematā", + "quote": "Atbildēt citējot", + "reply": "Atbildēt", + "replies_to_this_post": "%1 atbildes", + "one_reply_to_this_post": "1 atbilde", + "last_reply_time": "Pēdējā atbilde", + "reply-as-topic": "Atbildēt izveidojot jaunu tematu", + "guest-login-reply": "Ielogoties, lai atbildētu", + "login-to-view": "🔒 Log in to view", + "edit": "Rediģēt", + "delete": "Izdzēst", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Iztīrīt", + "restore": "Atjaunot", + "move": "Pārvietot", + "change-owner": "Change Owner", + "fork": "Nozarot", + "link": "Saistīt", + "share": "Kopīgot", + "tools": "Rīki", + "locked": "Slēgtie", + "pinned": "Piespraustie", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Pārvietots", + "moved-from": "Moved from %1", + "copy-ip": "Kopēt IP adresi", + "ban-ip": "Bloķēt IP adresi", + "view-history": "Rediģēšanas vēsture", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Noklikšķināt, lai atgrieztos pēdējā lasītā rakstā šajā pavedienā.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Šis temats ir izdzēsts. To var skatīt tikai lietotāji ar temata pārvaldības privilēģijām.", + "following_topic.message": "Tagad saņemsi paziņojumus, kad kāds šai tematā rakstīs.", + "not_following_topic.message": "Tu redzēsi šo tematu nelasīto tematu sarakstā, taču nesaņemsi paziņojumus, kad kāds viņā rakstīs.", + "ignoring_topic.message": "Šis temats vairs nebūs redzams nelasīto tematu sarakstā. Tev paziņos, kad tiksi pieminēts(-ēta), vai kāds balsos \"par\" Tavu rakstu.", + "login_to_subscribe": "Lūdzu, reģistrēties vai ielogoties, lai abonētu šo tematu.", + "markAsUnreadForAll.success": "Temats atzīmēts kā nelasīts visiem.", + "mark_unread": "Atzīmēt kā nelasītu", + "mark_unread.success": "Temats atzīmēts kā nelasīts.", + "watch": "Novērošana", + "unwatch": "Pārtraukt novērošanu", + "watch.title": "Tiec informēts par jaunām atbildēm šajā tematā", + "unwatch.title": "Pārtraukt temata novērošanu", + "share_this_post": "Kopīgot rakstu", + "watching": "Novērots", + "not-watching": "Nav novērots", + "ignoring": "Ignorēts", + "watching.description": "Paziņot par jaunām atbildēm.
Atzīmēt tematu kā nelasītu.", + "not-watching.description": "Nepaziņot par jaunām atbildēm.
Atzīmēt tematu kā nelasītu, ja kategorija nav ignorēta.", + "ignoring.description": "Nepaziņot par jaunām atbildēm.
Neatzīmēt tematu kā nelasītu.", + "thread_tools.title": "Rīkoties", + "thread_tools.markAsUnreadForAll": "Visiem atzīmēt kā nelasītu", + "thread_tools.pin": "Noenkurot tematu", + "thread_tools.unpin": "Atenkurot tematu", + "thread_tools.lock": "Slēgt tematu", + "thread_tools.unlock": "Atslēgt tematu", + "thread_tools.move": "Pārvietot tematu", + "thread_tools.move-posts": "Pārvietot rakstus", + "thread_tools.move_all": "Pārvietot visus", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Atlasīt kategoriju", + "thread_tools.fork": "Nozarot tematu", + "thread_tools.delete": "Izdzēst tematu", + "thread_tools.delete-posts": "Izdzēst rakstus", + "thread_tools.delete_confirm": "Vai tiešām vēlies izdzēst šo tematu?", + "thread_tools.restore": "Atjaunot tematu", + "thread_tools.restore_confirm": "Vai tiešām vēlies atjaunot šo tematu?", + "thread_tools.purge": "Iztīrīt tematu", + "thread_tools.purge_confirm": "Vai tiešām vēlies iztīrīt šo tematu?", + "thread_tools.merge_topics": "Apvienot tematus", + "thread_tools.merge": "Apvienot tematus", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Vai tiešām vēlies izdzēst šo rakstu?", + "post_restore_confirm": "Vai tiešām vēlies atjaunot šo rakstu?", + "post_purge_confirm": "Vai tiešām vēlies iztīrīt šo rakstu?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Ielādē kategorijas", + "confirm_move": "Pārvietot", + "confirm_fork": "Nozarot", + "bookmark": "Atzīme", + "bookmarks": "Atzīmētie", + "bookmarks.has_no_bookmarks": "Tu vēl neesi nevienu rakstu atzīmējis.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Ielādē vēl rakstus", + "move_topic": "Pārvietot tematu", + "move_topics": "Pārvietot tematus", + "move_post": "Pārvietot rakstu", + "post_moved": "Raksts pārvietots!", + "fork_topic": "Nozarot tematu", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Noklikšķini uz rakstiem, kurus nozarot", + "fork_no_pids": "Nav atlasīto rakstu!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 raksts(-i) atlasīts(-i)", + "fork_success": "Veiksmīgi nozarots temats! Noklikšķini, lai dotos uz nozaroto tematu.", + "delete_posts_instruction": "Noklikšķini uz rakstiem, kurus notīrīt/iztīrīt", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Ievadīt temata virsrakstu...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Atmest", + "composer.submit": "Publicēt", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Atbild %1", + "composer.new_topic": "Izveidot jaunu tematu", + "composer.editing": "Editing", + "composer.uploading": "augšupielādē...", + "composer.thumb_url_label": "Ielīmēt temata sīktēla URL", + "composer.thumb_title": "Pievienot tematam sīktēlu", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Vai augšupielādēt failu", + "composer.thumb_remove": "Notīrīt laukus", + "composer.drag_and_drop_images": "Vilkt un nomest bildes šeit", + "more_users_and_guests": "Vēl %1 lietotājs(-i) un %2 viesi(-s)", + "more_users": "Vēl %1 lietotājs(-i)", + "more_guests": "Vēl %1 viesis(-i)", + "users_and_others": "%1 un %2 citi", + "sort_by": "Kārtot", + "oldest_to_newest": "No vecākā līdz jaunākam", + "newest_to_oldest": "No jaunākā līdz vecākam", + "most_votes": "Pēc visvairāk balsojumu", + "most_posts": "Pēc visvairāk rakstu", + "most_views": "Most Views", + "stale.title": "Tā vietā izveidot jaunu tematu?", + "stale.warning": "Šis temats, uz kuru atbildi, ir diezgan sens. Vai vēlies izveidot jaunu tematu un atsaukties uz šo tematu?", + "stale.create": "Izveidot jaunu tematu", + "stale.reply_anyway": "Atbildēt tematā jebkurā gadījumā", + "link_back": "Re: [%1](%2)", + "diffs.title": "Raksta rediģēšanas vēsture", + "diffs.description": "Šim rakstam ir %1 versijas. Noklikšķināt zemāk uz vienas no versijām, lai redzētu to raksta versiju.", + "diffs.no-revisions-description": "Šim rakstam ir %1 versijas.", + "diffs.current-revision": "pašreizējā versija", + "diffs.original-revision": "sākotnējā versija", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/lv/unread.json b/public/language/lv/unread.json new file mode 100644 index 0000000000..e2d9ec3198 --- /dev/null +++ b/public/language/lv/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Nelasītie", + "no_unread_topics": "Nav nevienu nelasīto tematu.", + "load_more": "Ielādēt vairāk", + "mark_as_read": "Atzīmēt kā lasītu", + "selected": "Atlasītie", + "all": "Visi", + "all_categories": "Visās kategorijās", + "topics_marked_as_read.success": "Temati atzīmēti kā lasīti!", + "all-topics": "Visos tematos", + "new-topics": "Jaunos tematos", + "watched-topics": "Novērotos tematos", + "unreplied-topics": "Tematos, kuriem nav atbildes", + "multiple-categories-selected": "Vairākas atlasītas" +} \ No newline at end of file diff --git a/public/language/lv/uploads.json b/public/language/lv/uploads.json new file mode 100644 index 0000000000..2efd215ad4 --- /dev/null +++ b/public/language/lv/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Faila augšupielāde...", + "select-file-to-upload": "Atlasīt augšupielādējamo failu", + "upload-success": "Fails ir veiksmīgi augšupielādēts!", + "maximum-file-size": "Maksimālais izmērs %1 kb", + "no-uploads-found": "Nav augšupielādes", + "public-uploads-info": "Augšupielādes ir publiskas, visi apmeklētāji tās var redzēt.", + "private-uploads-info": "Augšupielādes ir privātas, tikai ielogojušies lietotāji tās var redzēt." +} \ No newline at end of file diff --git a/public/language/lv/user.json b/public/language/lv/user.json new file mode 100644 index 0000000000..554e5d3737 --- /dev/null +++ b/public/language/lv/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Bloķētie", + "muted": "Muted", + "offline": "Bezsaistē", + "deleted": "Izdzēstie", + "username": "Vārds", + "joindate": "Reģistrācijas datums", + "postcount": "Rakstu skaits", + "email": "E-pasts", + "confirm_email": "Apstiprināt e-pasta adresi", + "account_info": "Konta informācija", + "admin_actions_label": "Administrative Actions", + "ban_account": "Bloķēt kontu", + "ban_account_confirm": "Vai tiešām vēlies bloķēt šo lietotāju?", + "unban_account": "Atbloķēt kontu", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Izdzēst kontu", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Konts izdzēsts", + "account-content-deleted": "Account content deleted", + "fullname": "Vārds un uzvārds", + "website": "Vietne", + "location": "Vieta", + "age": "Vecums", + "joined": "Reģistrējies(-jusies)", + "lastonline": "Pēdējoreiz redzēts(-ēta)", + "profile": "Profils", + "profile_views": "Profila skatījumi", + "reputation": "Ranga punkti", + "bookmarks": "Atzīmētie", + "watched_categories": "Novērotās kategorijas", + "change_all": "Change All", + "watched": "Novērotie", + "ignored": "Ignorētie", + "default-category-watch-state": "Default category watch state", + "followers": "Man seko", + "following": "Es sekoju", + "blocks": "Bloķētie", + "block_toggle": "Pārslēgt bloķēto", + "block_user": "Bloķēt lietotāju", + "unblock_user": "Atbloķēt lietotāju", + "aboutme": "Par mani", + "signature": "Paraksts", + "birthday": "Dzimšanas diena", + "chat": "Sarunāties", + "chat_with": "Turpināt sarunu ar %1", + "new_chat_with": "Sākt jaunu sarunu ar %1", + "flag-profile": "Atzīmēt profilu", + "follow": "Sekot", + "unfollow": "Pārtraukt sekot", + "more": "Vēl", + "profile_update_success": "Profils ir veiksmīgi atjaunināts!", + "change_picture": "Mainīt bildi", + "change_username": "Mainīt lietotājvārdu", + "change_email": "Mainīt e-pasta adresi", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Rediģēt", + "edit-profile": "Rediģēt profilu", + "default_picture": "Noklusējuma ikona", + "uploaded_picture": "Augšupielādētā bilde", + "upload_new_picture": "Augšupielādēt bildi", + "upload_new_picture_from_url": "Augšupielādēt bildi no URL", + "current_password": "Pašreizējā parole", + "change_password": "Mainīt paroli", + "change_password_error": "Nederīga parole!", + "change_password_error_wrong_current": "Pašreizējā parole nav pareiza!", + "change_password_error_match": "Parolēm jāsakrīt!", + "change_password_error_privileges": "Tev nav tiesības mainīt šo paroli.", + "change_password_success": "Parole ir atjaunināta!", + "confirm_password": "Apstiprināt paroli", + "password": "Parole", + "username_taken_workaround": "Pieprasītais lietotājvārds jau eksistē, tāpēc mēs to nedaudz mainījām. Lietotājvārds tagad ir %1", + "password_same_as_username": "Parole ir tāda pati kā lietotājvārds, lūdzu, izvēlies citu paroli.", + "password_same_as_email": "Parole ir tāda pati kā e-pasta adrese, lūdzu, izvēlies citu paroli.", + "weak_password": "Vāja parole.", + "upload_picture": "Augšupielādēt bildi", + "upload_a_picture": "Augšupielādēt bildi", + "remove_uploaded_picture": "Noņemt augšupielādēto bildi", + "upload_cover_picture": "Augšupielādēt galvenes bildi", + "remove_cover_picture_confirm": "Vai tiešām vēlies noņemt galvenes bildi?", + "crop_picture": "Apgriezt bildi", + "upload_cropped_picture": "Apgriezt un augšupielādēt", + "avatar-background-colour": "Avatar background colour", + "settings": "Iestatījumi", + "show_email": "Atklāt savu e-pasta adresi", + "show_fullname": "Atklāt savu vārdu un uzvārdu", + "restrict_chats": "Atļaut sarunas tikai no tiem lietotājiem, kurus es sekoju", + "digest_label": "Sakopojumu abonements", + "digest_description": "Abonēt e-pasta paziņojumus no šī foruma (par jauniem tematiem un rakstiem) uz noteiktu grafiku", + "digest_off": "Izslēgts", + "digest_daily": "Ik dienas", + "digest_weekly": "Ik nedēļas", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Ik mēnesi", + "has_no_follower": "Šim lietotājam nav nevienu sekotāju :(", + "follows_no_one": "Šis lietotājs neseko nevienam :(", + "has_no_posts": "Lietotājs vēl nav neko rakstījis(-jusi).", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Lietotājs vēl nav izveidojis nevienu tematu.", + "has_no_watched_topics": "Lietotājs vēl nav novērojis nevienu tematu.", + "has_no_ignored_topics": "Lietotājs nav vēl ignorējis nevienu tematu.", + "has_no_upvoted_posts": "Lietotājs vēl nav balsojis \"par\" nevienu rakstu.", + "has_no_downvoted_posts": "Lietotājs vēl nav balsojis \"pret\" nevienu rakstu.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Tu neesi bloķējis nevienu lietotāju.", + "email_hidden": "E-pasta adrese paslēpta", + "hidden": "paslēpies", + "paginate_description": "Tematus un rakstus dalīt pa vairākām lapām un nelikt visus vienā", + "topics_per_page": "Tematu skaits lapā", + "posts_per_page": "Rakstu skaits lapā", + "max_items_per_page": "maksimāli %1", + "acp_language": "Administrācijas lapu valoda", + "notifications": "Notifications", + "upvote-notif-freq": "Balsojumu \"par\" paziņojumu biežums", + "upvote-notif-freq.all": "Uz katru balsojumu \"par\"", + "upvote-notif-freq.first": "Uz pirmā raksta balsojuma", + "upvote-notif-freq.everyTen": "Uz katru desmito balsojumu \"par\"", + "upvote-notif-freq.threshold": "Uz 1., 5., 10., 25., 50., 100., 150., 200. ...", + "upvote-notif-freq.logarithmic": "Uz 10., 100., 1000. ...", + "upvote-notif-freq.disabled": "Nekad", + "browsing": "Pārlūkošana", + "open_links_in_new_tab": "Atvērt izejošās saites jaunā cilnē", + "enable_topic_searching": "Iespējot meklēšanu tematu saturā", + "topic_search_help": "Ja ir iespējots, meklēšana tematos ignorē pārlūka noklusējuma lapu meklēšanas uzvedību un meklē visā tematā, ne tikai tā temata daļā, kas tiek rādīta ekrānā", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Rādīt jauno rakstu pēc publicēšanas", + "follow_topics_you_reply_to": "Novērot tematus, kuros esi rakstījis(-jusi)", + "follow_topics_you_create": "Novērot tematus, kurus esi izveidojis(-jusi)", + "grouptitle": "Grupa", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Nav", + "select-skin": "Ādiņa", + "select-homepage": "Sākumlapa", + "homepage": "Sākumlapa", + "homepage_description": "Izvēlies lapu, kuru izmantot kā foruma sākumlapu vai \"Nav\", lai izmantotu noklusējuma sākumlapu.", + "custom_route": "Pielāgotais sākumlapas ceļš", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Vienotās ielogošanās pakalpojumi", + "sso.associated": "Saistīts ar", + "sso.not-associated": "Sasaistīt ar", + "sso.dissociate": "Atsaistīt", + "sso.dissociate-confirm-title": "Apstiprināt atsaistīšanu", + "sso.dissociate-confirm": "Vai tiešām vēlies atsaistīt Tavu kontu no %1?", + "info.latest-flags": "Jaunākās atzīmes", + "info.no-flags": "Nav atzīmēto rakstu", + "info.ban-history": "Nesenā bloķēšanas vēsture", + "info.no-ban-history": "Šis lietotājs nekad nav bijis bloķēts", + "info.banned-until": "Bloķēts līdz %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Bloķēts pastāvīgi", + "info.banned-reason-label": "Iemesls", + "info.banned-no-reason": "Bez iemesla.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Lietotājvārdu vēsture", + "info.email-history": "E-pastu vēsture", + "info.moderation-note": "Moderatora piezīmes", + "info.moderation-note.success": "Moderatora piezīmes saglabātas", + "info.moderation-note.add": "Pievienot piezīmi", + "sessions.description": "Skatīt jebkuras aktīvās sesijas šajā forumā un vajadzības gadījumā kādu atsaukt. Atcelt šo pašu sesiju, izlogojoties no sava konta.", + "consent.title": "Tiesības & piekrišana", + "consent.lead": "Šis forums apkopo un apstrādā Tavu personisko informāciju.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Ja vien Tavos lietotāja iestatījumos tas nav īpaši iestatīts, šī kopiena Tev nosūtīs e-pasta ziņojumus katru %1.", + "consent.digest_off": "Ja vien Tavos lietotāja iestatījumos tas nav īpaši iestatīts, šī kopiena Tev nesūtīs nekādus e-pasta ziņojumus.", + "consent.received": "Tu esi sniedzis šim forumam piekrišanu, lai savāktu un apstrādātu Tavu personisko informāciju. Papildu darbība nav nepieciešama.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Piekrist", + "consent.right_of_access": "Tev ir tiesības piekļūt saviem datiem", + "consent.right_of_access_description": "Tev ir tiesības pieprasīt visus datus, kas savākti šajā vietnē. Tu vari izgūt šo datu kopiju, zemāk atbilstoši noklikšķinot.", + "consent.right_to_rectification": "Tev ir tiesības labot savus datus", + "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's administrative team.", + "consent.right_to_erasure": "Tev ir tiesības izdzēst savus datus", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "Tev ir tiesības pārnest savus datus", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Eksportēt augšupielādēto saturu (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Eksportēt rakstus (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/lv/users.json b/public/language/lv/users.json new file mode 100644 index 0000000000..5dd31dec5a --- /dev/null +++ b/public/language/lv/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Jaunākie lietotāji", + "top_posters": "Visvairāk rakstu", + "most_reputation": "Visvairāk ranga punktu", + "most_flags": "Visvairāk atzīmju", + "search": "Meklēt", + "enter_username": "Meklējamais lietotājvārds", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Ielādēt vairāk", + "users-found-search-took": "Atrasti %1 lietotājs(-i)! Meklēšana ilga %2 sekundes.", + "filter-by": "Filtrēt pēc", + "online-only": "Tikai tiešsaistē", + "invite": "Uzaicināt", + "prompt-email": "E-pasta adreses:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Ielūguma e-pasts ir nosūtīts %1", + "user_list": "Lietotāji", + "recent_topics": "Nesenie temati", + "popular_topics": "Populārie temati", + "unread_topics": "Nelasītie temati", + "categories": "Kategorijas", + "tags": "Birkas", + "no-users-found": "Nav lietotāju!" +} \ No newline at end of file diff --git a/public/language/ms/_DO_NOT_EDIT_FILES_HERE.md b/public/language/ms/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/ms/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/ms/admin/admin.json b/public/language/ms/admin/admin.json new file mode 100644 index 0000000000..8762610c5d --- /dev/null +++ b/public/language/ms/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Adakah anda ingin membina dan memulakan semula NodeBB", + "alert.confirm-restart": "Adakan anda ingin memulakan semula NodeBB", + + "acp-title": "%1 | Panel Kawalan dan Kendalian NodeBB", + "settings-header-contents": "Isi", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/ms/admin/advanced/cache.json b/public/language/ms/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/ms/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/ms/admin/advanced/database.json b/public/language/ms/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/ms/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/ms/admin/advanced/errors.json b/public/language/ms/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/ms/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/ms/admin/advanced/events.json b/public/language/ms/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/ms/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/ms/admin/advanced/logs.json b/public/language/ms/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/ms/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/ms/admin/appearance/customise.json b/public/language/ms/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/ms/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/ms/admin/appearance/skins.json b/public/language/ms/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/ms/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/ms/admin/appearance/themes.json b/public/language/ms/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/ms/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/ms/admin/dashboard.json b/public/language/ms/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/ms/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/ms/admin/development/info.json b/public/language/ms/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/ms/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/ms/admin/development/logger.json b/public/language/ms/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/ms/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/ms/admin/extend/plugins.json b/public/language/ms/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/ms/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/ms/admin/extend/rewards.json b/public/language/ms/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/ms/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/ms/admin/extend/widgets.json b/public/language/ms/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/ms/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/admins-mods.json b/public/language/ms/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/ms/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/categories.json b/public/language/ms/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/ms/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/digest.json b/public/language/ms/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/ms/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/ms/admin/manage/groups.json b/public/language/ms/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/ms/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/privileges.json b/public/language/ms/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/ms/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/registration.json b/public/language/ms/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/ms/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/tags.json b/public/language/ms/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/ms/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/uploads.json b/public/language/ms/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/ms/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/ms/admin/manage/users.json b/public/language/ms/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/ms/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/ms/admin/menu.json b/public/language/ms/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/ms/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/advanced.json b/public/language/ms/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/ms/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/api.json b/public/language/ms/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/ms/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/chat.json b/public/language/ms/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/ms/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/cookies.json b/public/language/ms/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/ms/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/email.json b/public/language/ms/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/ms/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/ms/admin/settings/general.json b/public/language/ms/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/ms/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/ms/admin/settings/group.json b/public/language/ms/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/ms/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/guest.json b/public/language/ms/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/ms/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/homepage.json b/public/language/ms/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/ms/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/languages.json b/public/language/ms/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/ms/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/navigation.json b/public/language/ms/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/ms/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/ms/admin/settings/notifications.json b/public/language/ms/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/ms/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/pagination.json b/public/language/ms/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/ms/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/post.json b/public/language/ms/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/ms/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/reputation.json b/public/language/ms/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/ms/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/social.json b/public/language/ms/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/ms/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/sockets.json b/public/language/ms/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/ms/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/sounds.json b/public/language/ms/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/ms/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/tags.json b/public/language/ms/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/ms/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/ms/admin/settings/uploads.json b/public/language/ms/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/ms/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/ms/admin/settings/user.json b/public/language/ms/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/ms/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/ms/admin/settings/web-crawler.json b/public/language/ms/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/ms/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/ms/category.json b/public/language/ms/category.json new file mode 100644 index 0000000000..aaad47b31a --- /dev/null +++ b/public/language/ms/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategori", + "subcategories": "Subkategori", + "new_topic_button": "Topik Baru", + "guest-login-post": "Log masuk untuk kirim", + "no_topics": "Tiada topik dalam kategori ini.
Cuba hantar topik yang baru?", + "browsing": "melihat", + "no_replies": "Tiada jawapan", + "no_new_posts": "Tiada kiriman baru.", + "watch": "Melihat", + "ignore": "Abai", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Kategori Dilihat", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/ms/email.json b/public/language/ms/email.json new file mode 100644 index 0000000000..ed309b518d --- /dev/null +++ b/public/language/ms/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Selamat datang ke %1", + "invite": "Jemputan daripada %1", + "greeting_no_name": "Salam", + "greeting_with_name": "Salam %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Terima kasih kerana mendaftar dengan %1!", + "welcome.text2": "Untuk mengaktifkan akaun anda sepenuhnya, kami perlu mengesahkan bahawa anda memiliki alamat emel yang didaftarkan.", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "Klik sini untuk sahkan emel anda", + "invitation.text1": "%1 telah menjemput untuk menyertai %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Kami menerima permintaan set semula kata laluan anda, kemungkinan kerana anda terlupa. Sekiranya tidak, sila abaikan emel ini.", + "reset.text2": "Untuk meneruskan dengan set semula kata laluan, sila klik pautan berikut:", + "reset.cta": "Klik sini untuk set semula kata laluan anda", + "reset.notify.subject": "Kata laluan berjaya ditukar", + "reset.notify.text1": "Pada %1, kata laluan anda berjaya ditukar.", + "reset.notify.text2": "Sekiranya anda tidak pernah melakukannya, sila hubungi pendtadbir / admin dengan segera.", + "digest.latest_topics": "Topik terkini dari %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Klik sini untuk melawat %1", + "digest.unsub.info": "Ringkasan ini dihantar berdasarkan tetapan langganan anda.", + "digest.day": "day", + "digest.week": "week", + "digest.month": "month", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Pesanan baru diterima dari %1", + "notif.chat.cta": "Klik sini untuk meneruskan perbualan", + "notif.chat.unsub.info": "Pemberitahuan sembang ini dihantar berdasarkan tetapan langganan anda.", + "notif.post.unsub.info": "Kiriman pemberitahuan ini dihantar berdasarkan tetapan langganan anda.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Ini adalah percubaan email untuk mengesahkan emailer ditetap dengan betul di NodeBB.", + "unsub.cta": "Klik sini untuk mengubah tetapan itu", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Terima Kasih!" +} \ No newline at end of file diff --git a/public/language/ms/error.json b/public/language/ms/error.json new file mode 100644 index 0000000000..dabfbe3476 --- /dev/null +++ b/public/language/ms/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Data Tak Sah", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Anda tidak log masuk.", + "account-locked": "Akaun anda telah dikunci untuk seketika", + "search-requires-login": "Fungsi Carian perlukan akaun - sila log masuk atau daftar.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Kategori ID Tak Sah", + "invalid-tid": "Topik ID Tak Sah", + "invalid-pid": "Kiriman ID Tak Sah", + "invalid-uid": "ID Pengguna Tak Sah", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Nama Pengguna Tak Sah", + "invalid-email": "Emel Tak Sah", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Data Pengguna Tak Sah", + "invalid-password": "Kata laluan salah!", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Sila tentukan kedua-dua nama pengguna dan kata laluan", + "invalid-search-term": "Terma pencarian tak sah", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Nombor halaman tidak sah, mesti tidak kurang dari %1 dan tidak lebih dari %2", + "username-taken": "Nama pengguna telah digunakan", + "email-taken": "Emel telah digunakan", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Anda tidak dibenarkan sembang sehingga emel disahkan, sila sahkan emel anda.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Kami tidak dapat memastikan emel anda, sila cuba lagi nanti", + "confirm-email-already-sent": "Pengesahan emel telah dihantar, sila tunggu %1 minit() untuk menghantar yang baru.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Nama pengunna terlalu pendek", + "username-too-long": "Nama pengunna terlalu panjang", + "password-too-long": "Kata laluan terlalu panjang", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Pengguna diharamkan", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Maaf, anda dikehendaki menunggu %1 saat() sebelum membuat kiriman pertama anda", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Kategori tidak wujud", + "no-topic": "Topik tidak wujud", + "no-post": "Kiriman tidak wujud", + "no-group": "Kumpulan tidak wujud", + "no-user": "Pengguna tidak wujud", + "no-teaser": "Pengusik tidak wujud", + "no-flag": "Flag does not exist", + "no-privileges": "Anda tidak mempunyai cukup keistimewaan untuk perbuatan ini.", + "category-disabled": "Kategori dilumpuhkan", + "topic-locked": "Topik Dikunci", + "post-edit-duration-expired": "Anda hanya dibenarkan menyunting kiriman selepas %1 saat() berlalu", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Sila masukkan kiriman yang lebih panjang. Kiriman mesti mengandungi sekurang-kurangnya %1 aksara().", + "content-too-long": "Sila masukkan kiriman yang lebih ringkas. Kiriman mesti mengandungi tidak lebih %1 aksara().", + "title-too-short": "Sila masukkan tajuk yang lebih panjang. Tajuk mesti mengandungi sekurang-kurangnya %1 aksara().", + "title-too-long": "Sila masukkan tajuk yang lebih ringkas. Tajuk mesti mengandungi tidak lebih %1 aksara().", + "category-not-selected": "Category not selected.", + "too-many-posts": "Anda hanya boleh mengirim sekali setiap %1 saat() - sila tunggu sebelum kiriman seterusnya", + "too-many-posts-newbie": "Sebagai pengguna baru, anda hanya boleh mengirim sekali setiap %1 saat() sehinnga anda mendapat %2 reputasi - sila tunggu sebelum kiriman seterusnya", + "already-posting": "You are already posting", + "tag-too-short": "Sila masukkan tag yang lebih panjang. Tag mesti mengandungi sekurang-kurangnya %1 aksara()", + "tag-too-long": "Sila masukkan tag yang lebih pendek. Tag mesti mengandungi tidak lebih %1 aksara()", + "not-enough-tags": "Tag tidak mencukupi. Topik memerlukan sekurang-kurangnya %1 tag()", + "too-many-tags": "Tag terlalu banyak. Topik tidak boleh lebih %1 tag()", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Sila tunggu muatnaik untuk siap.", + "file-too-big": "Maksimum saiz fail yang dibenarkan ialah %1 kB - sila muatnaik fail yang lebih kecil", + "guest-upload-disabled": "Tetamu tidak dibenarkan memuatnaik fail", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Anda tidak boleh haramkan admin / pentadbir!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Anda satu-satunya pentadbir. Tambah pentadbir lain sebelum membuang diri anda sebagai pentadbir", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Jenis imej tak sah. Jenis yang dibenarkan ialah: %1", + "invalid-image-extension": "Sambungan imej tak sah", + "invalid-file-type": "Jenis fail tak sah. Jenis fail yang dibenarkan ialah: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Nama kumpulan terlalu pendek", + "group-name-too-long": "Group name too long", + "group-already-exists": "Kumpulan telah wujud", + "group-name-change-not-allowed": "Pengubahan nama kumpulan tidak dibenarkan", + "group-already-member": "Sudah pun sebahagian dari kumpulan ini", + "group-not-member": "Bukan ahli kumpulan ini", + "group-needs-owner": "Kumpulan ini memerlukan sekurang-kurangnya seorang pemilik", + "group-already-invited": "Pengguna ini telah pun dijemput", + "group-already-requested": "Permintaan anda untuk menjadi telah pun dihantar", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Kiriman ini telah dipadam", + "post-already-restored": "Kiriman ini telah dipulihkan", + "topic-already-deleted": "Topik ini telah dipadam", + "topic-already-restored": "Kiriman ini telah dipulihkan", + "cant-purge-main-post": "Anda tidak boleh memadam, kiriman utama, sebaliknya sila pada topik", + "topic-thumbnails-are-disabled": "Topik kecil dilumpuhkan.", + "invalid-file": "Fail tak sah", + "uploads-are-disabled": "Muatnaik dilumpuhkan", + "signature-too-long": "Maaf, tandatangan anda tidak boleh lebih %1 aksara().", + "about-me-too-long": "Maaf, penerangan tentang anda tidak boleh lebih %1 aksara().", + "cant-chat-with-yourself": "Anda tidak boleh sembang dengan diri sendiri!", + "chat-restricted": "Pengguna ini menyekat ruangan sembangnya. Dia hendaklah mengikut anda sebelum kalian dapat bersembang", + "chat-disabled": "Sistem borak tidak diaktifkan", + "too-many-messages": "Anda menghantar terlalu banyak pesanan, sila tunggu seketika.", + "invalid-chat-message": "Mesej borak tidak sah", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "Anda tidak dibenarkan menyunting mesej ini", + "cant-delete-chat-message": "Anda tidak dibenarkan memadamkan mesej ini", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Sistem reputasi dilumpuhkan.", + "downvoting-disabled": "Undi turun dilumpuhkan", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB menemui masalah ketika muat semula: \"%1\". NodeBB akan terus melayan aset pelanggan sedia ada, tapi anda seharusnya undur perbuatan yang dilakukan sebelum muat semula.", + "registration-error": "Ralat pendaftaran.", + "parse-error": "Sesuatu tidak kena berlaku ketika menghuraikan repson pelayan (server)", + "wrong-login-type-email": "Sila guna emel anda untuk log masuk", + "wrong-login-type-username": "Sila guna nama pengguna anda untuk log masuk", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "Anda telah menjemput semaksima jumlah orang (%1 daripada %2).", + "no-session-found": "Tiada sesyen log masuk dijumpai", + "not-in-room": "Pengguna tiada dalam bilik", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/ms/flags.json b/public/language/ms/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/ms/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/ms/global.json b/public/language/ms/global.json new file mode 100644 index 0000000000..fb5d0a418c --- /dev/null +++ b/public/language/ms/global.json @@ -0,0 +1,126 @@ +{ + "home": "Laman Utama", + "search": "Cari", + "buttons.close": "Tutup", + "403.title": "Akses dinafikan", + "403.message": "Anda tidak mempunyai kebenaran untuk melihat halaman ini", + "403.login": "Mungkin anda boleh cuba log masuk?", + "404.title": "tidak dijumpai", + "404.message": "Halaman yang diminta tidak wujud. Kembali ke halaman utama.", + "500.title": "Internal Error.", + "500.message": "Oops! Macam ada yang tidak kena", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Daftar", + "login": "Log Masuk", + "please_log_in": "Sila log masuk", + "logout": "Log Keluar", + "posting_restriction_info": "Kiriman terhad kepada pengguna berdaftar sahaja, Sila click disini untuk daftar masuk", + "welcome_back": "Selamat kembali", + "you_have_successfully_logged_in": "Anda telah berjaya log masuk", + "save_changes": "Simpan perubahan", + "save": "Save", + "close": "Tutup", + "pagination": "Mukasurat", + "pagination.out_of": "%1 daripada %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Pentadbir", + "header.categories": "Kategori", + "header.recent": "Terkini", + "header.unread": "Belum dibaca", + "header.tags": "Tag", + "header.popular": "Popular", + "header.top": "Top", + "header.users": "Pengguna", + "header.groups": "Kumpulan", + "header.chats": "Sembang", + "header.notifications": "Pemberitahuan", + "header.search": "Cari", + "header.profile": "Profil", + "header.navigation": "Navigasi", + "notifications.loading": "Pemberitahuan sedang dimuatkan", + "chats.loading": "Sembang sedang dimuatkan", + "motd.welcome": "Selamat datang ke NodeBB, platfom perbincangan masa hadapan", + "previouspage": "Laman sebelum", + "nextpage": "Laman berikut", + "alert.success": "Berjaya", + "alert.error": "Ralat", + "alert.banned": "Diharamkan", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Anda tidak lagi mengikuti %1", + "alert.follow": "Anda sekarang mengikuti %1", + "users": "Pengguna", + "topics": "Topik", + "posts": "Kiriman", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Lihat", + "posters": "Posters", + "reputation": "Reputasi", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "baca lagi", + "more": "Lagi", + "none": "None", + "posted_ago_by_guest": "dikirim %1 oleh pelawat", + "posted_ago_by": "dikirim %1 oleh %2", + "posted_ago": "dikirim %1", + "posted_in": "dikirim pada %1", + "posted_in_by": "dikirim pada %1 oleh %2", + "posted_in_ago": "dikirim pada %1 %2", + "posted_in_ago_by": "dikirim pada %1 %2 oleh %3", + "user_posted_ago": "%1 mengirim %2", + "guest_posted_ago": "Pelawat mengirim %1", + "last_edited_by": "last edited by %1", + "norecentposts": "Tiada kiriman terkini", + "norecenttopics": "Tiada topik terkini", + "recentposts": "Kiriman terkini", + "recentips": "IP berdaftar terkini", + "moderator_tools": "Moderator Tools", + "online": "Dalam talian", + "away": "Jauh", + "dnd": "Jangan ganggu (dnd)", + "invisible": "Halimunan", + "offline": "Luar talian", + "email": "Emel", + "language": "Bahasa", + "guest": "Pelawat", + "guests": "Pelawat", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum Dikemaskini", + "updated.message": "Forum ini baru sahaja dikemaskini ke versi terkini. Klik sini untuk segar semula halaman.", + "privacy": "Privasi", + "follow": "Ikut", + "unfollow": "Nyah-ikut", + "delete_all": "Padam Semua", + "map": "Peta", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/ms/groups.json b/public/language/ms/groups.json new file mode 100644 index 0000000000..62ffd93e7c --- /dev/null +++ b/public/language/ms/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Kumpulan", + "view_group": "Lihat Kumpulan", + "owner": "Pemilik Kumpulan", + "new_group": "Buat Kumpulan Baru", + "no_groups_found": "Tiada kumpulan untuk dilihat", + "pending.accept": "Terima", + "pending.reject": "Tolak", + "pending.accept_all": "Terima Semua", + "pending.reject_all": "Tolak Semua", + "pending.none": "Tiada ahli yang sedang menunggu buat masa ini", + "invited.none": "Tiada ahli yang dijemput buat masa ini", + "invited.uninvite": "Batalkan Jemputan", + "invited.search": "Cari pengguna untuk dijemput ke kumpulan ini", + "invited.notification_title": "Anda telah dijemput untuk menyertai %1", + "request.notification_title": "Jemputan Ahli Kumpulan dari %1", + "request.notification_text": "%1 telah dijemput untuk menjadi ahli %2", + "cover-save": "Simpan", + "cover-saving": "Menyimpan", + "details.title": "Perincian Kumpulan", + "details.members": "Senarai Ahli", + "details.pending": "Ahli Menunggu", + "details.invited": "Ahli yang dijemput", + "details.has_no_posts": "Kumpulan ahli kumpulan ini belum membuat sebarang kiriman.", + "details.latest_posts": "Kiriman Terkini", + "details.private": "Privasi", + "details.disableJoinRequests": "Batalkan permintaan sertai", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Pemberian/Pembatalan pemilikan", + "details.kick": "Tendang", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Pentadbiran Kumpulan", + "details.group_name": "Nama Kumpulan", + "details.member_count": "Kiraan Ahli", + "details.creation_date": "Tarikh Dicipta", + "details.description": "Penerangan", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Pra-lihat Lencana", + "details.change_icon": "Tukar Ikon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Teks Lencana", + "details.userTitleEnabled": "Tunjuk Lencana", + "details.private_help": "Jika dibolehkan, menyertai kumpulan memerlukan kelulusan pemilik kumpulan", + "details.hidden": "Sembunyi", + "details.hidden_help": "Jika dibolehkan, kumpulan ini tidak akan dijumpai di senarai kumpulan, dan pengguna hendaklah di jemput secara manual", + "details.delete_group": "Padam Kumpulan", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Perincian kumpulan telah dikemaskini", + "event.deleted": "Kumpulan \"%1\" telah dipadam", + "membership.accept-invitation": "Terima Jemputan", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Jemputan Menunggu", + "membership.join-group": "Masuk Kumpulan", + "membership.leave-group": "Keluar Kumpulan", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Tolak", + "new-group.group_name": "Nama Kumpulan:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/ms/ip-blacklist.json b/public/language/ms/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/ms/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/ms/language.json b/public/language/ms/language.json new file mode 100644 index 0000000000..b2de6f9811 --- /dev/null +++ b/public/language/ms/language.json @@ -0,0 +1,5 @@ +{ + "name": "Bahasa Melayu", + "code": "ms", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/ms/login.json b/public/language/ms/login.json new file mode 100644 index 0000000000..5e719043e0 --- /dev/null +++ b/public/language/ms/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Nama pengguna / Emel", + "username": "Nama pengguna", + "remember_me": "Ingatkan Saya", + "forgot_password": "Lupa Kata Laluan?", + "alternative_logins": "Log Masuk Alternatif", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "Anda berjaya log masuk!", + "dont_have_account": "Tiada akaun?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/ms/modules.json b/public/language/ms/modules.json new file mode 100644 index 0000000000..d331389ba5 --- /dev/null +++ b/public/language/ms/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "hantar", + "chat.no_active": "Anda tiada pesanan yang aktif", + "chat.user_typing": "%1 menaip", + "chat.user_has_messaged_you": "%1 mesej anda.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Sila pilih penerima untuk lihat sejarah sembang", + "chat.no-users-in-room": "Tiada pengguna dalam bilik ini", + "chat.recent-chats": "Sembang Terbaru", + "chat.contacts": "Hubungi", + "chat.message-history": "Sejarah Pesanan", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop keluar sembang", + "chat.minimize": "Minimize", + "chat.maximize": "Memaksimum", + "chat.seven_days": "7 Hari", + "chat.thirty_days": "30 Hari", + "chat.three_months": "3 Bulan", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Tulis", + "composer.show_preview": "Pra-lihat", + "composer.hide_preview": "Sorok pra-lihat", + "composer.user_said_in": "%1 disebut di %2:", + "composer.user_said": "%1 berkata:", + "composer.discard": "Anda yakin untuk membuang kiriman ini?", + "composer.submit_and_lock": "Hantar dan Kunci", + "composer.toggle_dropdown": "Togol Kebawah", + "composer.uploading": "Memuat naik %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "Ok", + "bootbox.cancel": "Batal", + "bootbox.confirm": "Pasti", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Kedudukan Gambar Muka", + "cover.dragging_message": "Seret gambar muka ke kedudukan yang diingini dan klik \"Simpan\"", + "cover.saved": "Gambar muka dan kedudukan disimpan", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/ms/notifications.json b/public/language/ms/notifications.json new file mode 100644 index 0000000000..f880fe55f8 --- /dev/null +++ b/public/language/ms/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "pemberitahuan", + "no_notifs": "Anda tiada pemberitahuan baru", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Kembali ke %1", + "outgoing_link": "Sambungan luar", + "outgoing_link_message": "Anda sedang meninggalkan %1", + "continue_to": "Sambung ke %1", + "return_to": "Kembali ke %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Ada pemberitahuan yang belum dibaca", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Pesanan baru daripada %1", + "upvoted_your_post_in": "%1 telah mengundi naik kiriman and di %2.", + "upvoted_your_post_in_dual": "%1dan %2 telah menambah undi pada kiriman anda di %3.", + "upvoted_your_post_in_multiple": "%1 dan %2 lagi telah menambah undi pada kiriman anda di %3.", + "moved_your_post": "%1 telah memindahkan kiriman anda ke %2", + "moved_your_topic": "%1 telah memindahkan %2", + "user_flagged_post_in": "%1 menanda kiriman anda di %2", + "user_flagged_post_in_dual": "%1 dan %2 telah menanda kiriman anda pada %3", + "user_flagged_post_in_multiple": "%1 dan %2 lagi telah menanda kiriman anda pada %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 telah membalas kiriman kepada: %2", + "user_posted_to_dual": "%1 dan %2 membalas kiriman : %3", + "user_posted_to_multiple": "%1 dan %2 lagu membalas kiriman: %3", + "user_posted_topic": "%1 membuka topik baru : %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 mula mengikut anda.", + "user_started_following_you_dual": "%1 dan %2 mula mengikuti anda.", + "user_started_following_you_multiple": "%1 dan %2 lagi mula mengikuti anda.", + "new_register": "%1 menghantar jemputan pendaftaran.", + "new_register_multiple": "Ada %1 permohonan ingin daftar yang sedang menunggu pengesahan.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Emel Disahkan", + "email-confirmed-message": "Terima kasih kerana mengesahkan emel anda. Akaun anda telah diaktifkan sepenuhnya.", + "email-confirm-error-message": "Berlaku masalah semasa mengesahkan emel anda. Mungkin kod tidak sah atau tamat tempoh.", + "email-confirm-sent": "Pengesahan emel telah dihantar.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/ms/pages.json b/public/language/ms/pages.json new file mode 100644 index 0000000000..5dd04295a5 --- /dev/null +++ b/public/language/ms/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Laman Utama", + "unread": "Topik Belum Dibaca", + "popular-day": "Topik Popular Hari Ini", + "popular-week": "Topik Popular Minggu Ini", + "popular-month": "Topik Popular Bulan Ini", + "popular-alltime": "Topik Popular Sepanjang Masa", + "recent": "Topik Baru", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Pengguna Atas Talian", + "users/latest": "Pengguna Terkini", + "users/sort-posts": "Pengguna Mengikut Kiriman Terbanyak", + "users/sort-reputation": "Pengguna Mengikut Reputasi Terbanyak", + "users/banned": "Pengguna Diharam", + "users/most-flags": "Most flagged users", + "users/search": "Carian Pengguna", + "notifications": "Makluman", + "tags": "Tag", + "tag": "Topics tagged under "%1"", + "register": "Daftar Akaun", + "registration-complete": "Registration complete", + "login": "Log Masuk Ke Akaun Anda", + "reset": "Set Semula Kata Laluan", + "categories": "Kategori", + "groups": "Kumpulan", + "group": "%1 Kumpulan", + "chats": "Borak", + "chat": "Borak Dengan %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Menyunting \"%1\"", + "account/edit/password": "Mengemaskini kata laluan \"%1\"", + "account/edit/username": "Mengemaskini nama pengguna \"%1\"", + "account/edit/email": "Mengemaskini email \"%1\"", + "account/info": "Account Info", + "account/following": "Mengikut %1 orang", + "account/followers": "Diikuti oleh %1", + "account/posts": "Kiriman oleh %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topik olej %1", + "account/groups": "Kumpulan %1", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Tetapan Pengguna", + "account/watched": "Topik Diperhati Oleh %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Kiriman diundi naik oleh %1", + "account/downvoted": "Kiriman dibuang undi oleh %1", + "account/best": "Kiriman Terbaik Oleh %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Emel Telah Disahkan", + "maintenance.text": "%1 sedang berada dalam mod pembaikpulihan. Sila cuba lagi nanti.", + "maintenance.messageIntro": "Tambahan, admin meninggalkan mesej ini :", + "throttled.text": "%1 tiada buat masa ini kerana permintaan yang berlebihan. Sila datang lagi lain kali." +} \ No newline at end of file diff --git a/public/language/ms/post-queue.json b/public/language/ms/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/ms/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/ms/recent.json b/public/language/ms/recent.json new file mode 100644 index 0000000000..a2fc732a76 --- /dev/null +++ b/public/language/ms/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Terkini", + "day": "Hari", + "week": "Minggu", + "month": "Bulan", + "year": "Tahun", + "alltime": "Selamanya", + "no_recent_topics": "Tiada topik terkini", + "no_popular_topics": "Tiada topik popular.", + "there-is-a-new-topic": "Ada topik baru.", + "there-is-a-new-topic-and-a-new-post": "Ada topik baru dan kiriman baru.", + "there-is-a-new-topic-and-new-posts": "Ada topik baru dan %1 kiriman baru.", + "there-are-new-topics": "Ada %1 topik baru.", + "there-are-new-topics-and-a-new-post": "Ada %1 topik-topik dan kiriman-kiriman baru.", + "there-are-new-topics-and-new-posts": "Ada %1 topik baru dan %2 kiriman baru.", + "there-is-a-new-post": "Ada kiriman baru.", + "there-are-new-posts": "Ada %1 kiriman baru.", + "click-here-to-reload": "Klik sini untuk muat semula." +} \ No newline at end of file diff --git a/public/language/ms/register.json b/public/language/ms/register.json new file mode 100644 index 0000000000..30c6759a2d --- /dev/null +++ b/public/language/ms/register.json @@ -0,0 +1,32 @@ +{ + "register": "Mendaftar", + "cancel_registration": "Cancel Registration", + "help.email": "E-mel akan disembunyikan daripada orang ramai.", + "help.username_restrictions": "Cuba satu nama pengguna yang unik di antara %1 dan %2 aksara. Orang lain boleh menyebut anda dengan @nama pengguna.", + "help.minimum_password_length": "Panjang kata laluan anda hendaklah sekurang-kurangnya %1 aksara.", + "email_address": "Alamat E-mel", + "email_address_placeholder": "Masukkan Alamat E-mel", + "username": "Nama Pengguna", + "username_placeholder": "Masukkan Nama Pengguna", + "password": "Kata Laluan", + "password_placeholder": "Masukkan Nama Pengunna", + "confirm_password": "Sahkan Kata Laluan", + "confirm_password_placeholder": "Sahkan Kata Laluan", + "register_now_button": "Daftar Sekarang", + "alternative_registration": "Pendaftaran Alternatif", + "terms_of_use": "Terma Penggunaan", + "agree_to_terms_of_use": "Saya bersetuju dengan Terma Penggunaan", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Pendaftaran anda sedang dimasukkan ke barisan pengesahan. Anda akan menerima emel setelah diterima oleh pentadbir.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/ms/reset_password.json b/public/language/ms/reset_password.json new file mode 100644 index 0000000000..d813686249 --- /dev/null +++ b/public/language/ms/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Menetapkan semula kata laluan", + "update_password": "Mengemaskini kata laluan", + "password_changed.title": "Kata laluan diubah", + "password_changed.message": "p>Kata laluan telah ditetapkan semula, Sila log masuk semula.", + "wrong_reset_code.title": "Kod penetapan semula yang salah", + "wrong_reset_code.message": "Kod penetapan semula salah. Sila cuba lagi, atau mohon semula kod penetapan .", + "new_password": "Kata laluan baru", + "repeat_password": "Sahkan kata laluan", + "changing_password": "Changing Password", + "enter_email": "Sila masukkan alamat emel dan kami akan menghantar arahan untuk penetapan semula akaun anda", + "enter_email_address": "Masukkan alamat emel", + "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.", + "invalid_email": "Emel yang tidak sah / Emel tidak wujud", + "password_too_short": "Kata lauan terlalu pendek, sila pilih kata laluan yang lain", + "passwords_do_not_match": "Kedua-dua laluan yang dimasukkan tidak sepadan / tidak sama", + "password_expired": "Kata laluan telah tamat tempoh, pilih kata laluan baru" +} \ No newline at end of file diff --git a/public/language/ms/search.json b/public/language/ms/search.json new file mode 100644 index 0000000000..39ca2b3ad8 --- /dev/null +++ b/public/language/ms/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 hasil sepadan \"%2\", (%3 saat)", + "no-matches": "Tiada padanan dijumpai", + "advanced-search": "Pencarian Lebih Mendalam", + "in": "Dalam", + "titles": "Tajuk", + "titles-posts": "Tajuk dan kiriman", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Dikirim oleh", + "in-categories": "Dalam kategori", + "search-child-categories": "Cari anak kategori", + "has-tags": "Has tags", + "reply-count": "Kira Balasan", + "at-least": "Sekurang-kurangnya", + "at-most": "Selebihnya", + "relevance": "Relevance", + "post-time": "Masa kiriman", + "votes": "Votes", + "newer-than": "Baru daripada", + "older-than": "Lama daripada", + "any-date": "Mana-mana masa", + "yesterday": "Semalam", + "one-week": "Seminggu", + "two-weeks": "Dua minggu", + "one-month": "Sebulan", + "three-months": "Tiga bulan", + "six-months": "Enam bulan", + "one-year": "Setahun", + "sort-by": "Susun mengikut", + "last-reply-time": "Masa balasan terakhir", + "topic-title": "Tajuk topik", + "topic-votes": "Topic votes", + "number-of-replies": "Jumlah dibalas", + "number-of-views": "Jumlah dilihat", + "topic-start-date": "Tarikh topik mula", + "username": "Nama pengguna", + "category": "Kategori", + "descending": "Tertib menurun", + "ascending": "Tertib menaik", + "save-preferences": "Simpan butiran", + "clear-preferences": "Bersihkan butiran", + "search-preferences-saved": "Cari butiran yang disimpan", + "search-preferences-cleared": "Cari butiran yang diersihkan", + "show-results-as": "Tunjuk hasil sebagai", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/ms/success.json b/public/language/ms/success.json new file mode 100644 index 0000000000..e58458ce99 --- /dev/null +++ b/public/language/ms/success.json @@ -0,0 +1,7 @@ +{ + "success": "Berjaya", + "topic-post": "Kiriman anda berjaya dihantar.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Pengesahan Berjaya", + "settings-saved": "Tetapan disimpan!" +} \ No newline at end of file diff --git a/public/language/ms/tags.json b/public/language/ms/tags.json new file mode 100644 index 0000000000..57b12eac2a --- /dev/null +++ b/public/language/ms/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Tiada topik untuk tag ini.", + "tags": "Tag", + "enter_tags_here": "Masukkan tag sini, masing-masing antara %1 dan %2 aksara.", + "enter_tags_here_short": "Masukkan tag ...", + "no_tags": "Belum ada tag.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/ms/top.json b/public/language/ms/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/ms/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/ms/topic.json b/public/language/ms/topic.json new file mode 100644 index 0000000000..95ddeb215f --- /dev/null +++ b/public/language/ms/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Topik", + "title": "Title", + "no_topics_found": "Tiada topik yang ditemui", + "no_posts_found": "Tiada kirim yang dijumpai", + "post_is_deleted": "Kiriman ini dipadam!", + "topic_is_deleted": "Topik ini dipadam!", + "profile": "Profil", + "posted_by": "Dikirim oleh %1", + "posted_by_guest": "Dikirim oleh pelawat", + "chat": "Sembang", + "notify_me": "Kekal dimaklumkan berkenaan respon dalam topik ini", + "quote": "Petikan", + "reply": "Balas", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log masuk untuk balas", + "login-to-view": "🔒 Log in to view", + "edit": "Sunting", + "delete": "Padamkan", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Singkirkan", + "restore": "Pulihkan", + "move": "Pindahkan", + "change-owner": "Change Owner", + "fork": "Fork", + "link": "Pautan", + "share": "Kongsi", + "tools": "Perkakas", + "locked": "Kunci", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Topik ini telah dipadam. Hanya pengguna dengan kuasa pengurusan boleh melihatnya.", + "following_topic.message": "Anda akan menerima makluman apabila ada kiriman ke dalam topik ini", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Sila daftar atau log masuk untuk melanggani topik ini", + "markAsUnreadForAll.success": "Topik ditanda sebagai belum dibaca untuk semua", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "Lihat", + "unwatch": "Batal lihat", + "watch.title": "Akan dimaklumkan sekiranya ada balasan dalam topik ini", + "unwatch.title": "Berhenti melihat topik ini", + "share_this_post": "Kongsi kiriman ini", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Perkakas Topik", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Pinkan topik", + "thread_tools.unpin": "Batalkan pin topik", + "thread_tools.lock": "Kunci topik", + "thread_tools.unlock": "Buka kekunci topik", + "thread_tools.move": "Pindahkan topik", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Pindahkan Semua", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Fork topik", + "thread_tools.delete": "Padamkan topik", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Anda yakin untuk padamkan topik ini?", + "thread_tools.restore": "Pulihkan topik", + "thread_tools.restore_confirm": "Anda yakin untuk pulihkan topik ini?", + "thread_tools.purge": "Singkirkan Topik", + "thread_tools.purge_confirm": "Anda yakin untuk singkirkan topik ini?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Adakah anda pasti untuk memadam kiriman ini?", + "post_restore_confirm": "Adakah anda pasti untuk memulihkan kiriman ini?", + "post_purge_confirm": "Adakah anda pasti untuk singkirkan kiriman ini?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Memuatkan kategori", + "confirm_move": "Pindahkan", + "confirm_fork": "Salin", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Memuatkan lagi kiriman", + "move_topic": "Pindahkan topik", + "move_topics": "Pindahkan topik-topik", + "move_post": "Pindahkan kiriman", + "post_moved": "Kiriman dipindahkan", + "fork_topic": "Salin topik", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Klik kiriman yang anda hendak salin", + "fork_no_pids": "Tiada kiriman yang dipilih", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Berjaya menyalin topik. Klik sini untuk ke topik yang disalin.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Masukkan tajuk topik disini", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Abaikan", + "composer.submit": "Hantar", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Balas ke %1", + "composer.new_topic": "Topik baru", + "composer.editing": "Editing", + "composer.uploading": "Memuat naik ...", + "composer.thumb_url_label": "Tampalkan gambaran URL", + "composer.thumb_title": "Letakkan gambaran kepada topik ini", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Atau muat naik fail", + "composer.thumb_remove": "Bersihkan kawasan", + "composer.drag_and_drop_images": "Seret dan lepaskan imej disini", + "more_users_and_guests": "%1 lebih pengguna(-pengguna) dan %2 pelawat(-pelawat)", + "more_users": "%1 lebih pengguna(-pengguna)", + "more_guests": "%1 lebih pelawat(-pelawat)", + "users_and_others": "%1 dan %2 lain-lain", + "sort_by": "Susun ikut", + "oldest_to_newest": "Lama ke Baru", + "newest_to_oldest": "Baru ke Lama", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Bukan topik baru?", + "stale.warning": "Topik yang anda nak balas agak lapuk. Adakah anda ingin buka topik baru dan rujukkan topik ini dalam balasan anda?", + "stale.create": "Buka topik baru", + "stale.reply_anyway": "Tetap balas topik ini", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/ms/unread.json b/public/language/ms/unread.json new file mode 100644 index 0000000000..aa6f5b08d6 --- /dev/null +++ b/public/language/ms/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Belum dibaca", + "no_unread_topics": "Tiada topik yang belum dibaca", + "load_more": "Muatkan lagi", + "mark_as_read": "Tanda sebagai sudah dibaca", + "selected": "Dipilih", + "all": "Semua", + "all_categories": "Semua Kategori", + "topics_marked_as_read.success": "Topik ditandakan sebagai sudah dibaca", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/ms/uploads.json b/public/language/ms/uploads.json new file mode 100644 index 0000000000..6a19289404 --- /dev/null +++ b/public/language/ms/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Sedang memuatnaik fail...", + "select-file-to-upload": "Pilih fail yang hendak dimuatnaik!", + "upload-success": "Muatnaik fail berjaya!", + "maximum-file-size": "Maksima %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/ms/user.json b/public/language/ms/user.json new file mode 100644 index 0000000000..af86ec6ecb --- /dev/null +++ b/public/language/ms/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Diharamkan", + "muted": "Muted", + "offline": "Luar talian", + "deleted": "Deleted", + "username": "Nama pengguna", + "joindate": "Tarikh Daftar", + "postcount": "Jumlah Kiriman", + "email": "Emel", + "confirm_email": "Pastikan Emel", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Haramkan Akaun", + "ban_account_confirm": "Adakah anda pasti ingin menyekat pengguna ini?", + "unban_account": "Buang Sekatan Akaun", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Padam Akaun", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Akaun Dipadam", + "account-content-deleted": "Account content deleted", + "fullname": "Nama Penuh", + "website": "Laman Web", + "location": "Lokasi", + "age": "Umur", + "joined": "Menyertai", + "lastonline": "Kali terakhir ditalian", + "profile": "Profil", + "profile_views": "Paparan Profil", + "reputation": "Reputasi", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Melihat", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Pengikut", + "following": "Mengikuti", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "Tentang saya", + "signature": "Tandatangan", + "birthday": "Tarikh lahir", + "chat": "Bersembang", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Ikuti", + "unfollow": "Henti mengikuti", + "more": "Lagi", + "profile_update_success": "Profil telah dikemaskini", + "change_picture": "Tukar gambar", + "change_username": "Tukar Nama Pengguna", + "change_email": "Tukar Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Kemaskini", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Muatnaik gambak", + "upload_new_picture": "Muatnaik gambar baru", + "upload_new_picture_from_url": "Muatnaik gambar baru dari URL", + "current_password": "Kata laluan sekarang", + "change_password": "Tukar kata laluan", + "change_password_error": "Kata laluan salah!", + "change_password_error_wrong_current": "Kata laluan anda sekarang tidak sah", + "change_password_error_match": "Kata laluan mesti padan", + "change_password_error_privileges": "Anda tidak mempunyai kebenaran untuk mengubah kata laluan ini", + "change_password_success": "Kata laluan dikemaskini", + "confirm_password": "Sahkan kata laluan", + "password": "kata laluan", + "username_taken_workaround": "Nama pengguna yang anda minta telah digunakan oleh orang lain, jadi kami telah mengubahsuaikannya sedikit. Anda kini dikenali sebagai %1", + "password_same_as_username": "Kata laluan anda adalah sama seperti nama pengguna, sila pilih kata laluan yang lain", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "Muatnaik gambar", + "upload_a_picture": "Muatnaik sekeping gambar", + "remove_uploaded_picture": "Buang Gambar Yang Dimuatnaik", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Tetapan", + "show_email": "Tunjukkan emel saya", + "show_fullname": "Tunjukkan Nama Penuh", + "restrict_chats": "Hanya benarkan sembang mesej dari pengguna yang saya ikut sahaja", + "digest_label": "Langgan berita", + "digest_description": "Langgan berita terkini untuk forum ini melalui emel (Makluman dan topik) menurut jadual yang ditetapkan", + "digest_off": "Tutup", + "digest_daily": "Harian", + "digest_weekly": "Mingguan", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Bulanan", + "has_no_follower": "Pengguna ini tiada pengikut :(", + "follows_no_one": "Pengguna ini tidak mengikuti sesiapa :(", + "has_no_posts": "Pengguna ini belum menulis sebarang kiriman lagi.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Pengguna ini belum menulis sebarang topik lagi.", + "has_no_watched_topics": "Pengguna ini belum melanggan sebarang topik lagi.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Emel disembunyikan", + "hidden": "disembunyikan", + "paginate_description": "Gunakan muka surat untuk topik dan kiriman daripada penggunaan skroll infiniti", + "topics_per_page": "Topik setiap muka", + "posts_per_page": "Kiriman setiap muka", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Melihat-lihat Tetapan", + "open_links_in_new_tab": "Buka pautan luar di tab yang baru", + "enable_topic_searching": "Aktifkan Pencarian Dalam-Topik", + "topic_search_help": "Jika diaktifkan, pencarian dalam-topik akan membatalkan fungsi asal pencarian pelayan dan membenarkan anda untuk mencari seluruh topik, daripada menunjukkan apa yang terdapat pada skrin sahaja", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Tiada nama kumpulan", + "select-skin": "Pilih skin", + "select-homepage": "Pilih Laman Utama", + "homepage": "Laman Utama", + "homepage_description": "Pilih satu halaman untuk digunakan sebagai Laman Utama forum atau 'Tiada' untuk guna tetapan lalai", + "custom_route": "Laluan Laman Utama Tersuai", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Servis Satu Log Masuk", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/ms/users.json b/public/language/ms/users.json new file mode 100644 index 0000000000..0e27f7bbce --- /dev/null +++ b/public/language/ms/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Pengguna terkini", + "top_posters": "Pengirim terbanyak", + "most_reputation": "Reputasi terbaik", + "most_flags": "Most Flags", + "search": "Cari", + "enter_username": "Masukkan nama pengguna untuk carian", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Muat lagi", + "users-found-search-took": "%1 pengguna dijumpai! Pencarian ambil masa %2 saat.", + "filter-by": "Saring dengan", + "online-only": "Atas talian sahaja", + "invite": "Jemput", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Emel jemputan telah dihantar ke %1", + "user_list": "Senarai Pengguna", + "recent_topics": "Topik Terkini", + "popular_topics": "Topik Popular", + "unread_topics": "Topik Belum Dibaca", + "categories": "Kategori", + "tags": "Tag", + "no-users-found": "No users found!" +} \ No newline at end of file diff --git a/public/language/nb/_DO_NOT_EDIT_FILES_HERE.md b/public/language/nb/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/nb/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/nb/admin/admin.json b/public/language/nb/admin/admin.json new file mode 100644 index 0000000000..65bbbc4f99 --- /dev/null +++ b/public/language/nb/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Er du sikker på at du vil gjenoppbygge og restarte NodeBB?", + "alert.confirm-restart": "Er du sikker på at du ønsker å restarte NoddeBB?", + + "acp-title": "%1 | NodeBB Admin Kontrollpanel", + "settings-header-contents": "Innhold", + "changes-saved": "Endringer lagret", + "changes-saved-message": "Dine endringer i NodeBB-konfigurasjonen har blitt lagret.", + "changes-not-saved": "Endringer ikke lagret", + "changes-not-saved-message": "NodeBB støtte på et problem ved lagring av endringer. (%1)" +} \ No newline at end of file diff --git a/public/language/nb/admin/advanced/cache.json b/public/language/nb/admin/advanced/cache.json new file mode 100644 index 0000000000..f75eabe4cd --- /dev/null +++ b/public/language/nb/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post-buffer", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1%full ", + "post-cache-size": "Post-buffer størrelse", + "items-in-cache": "Element i buffer" +} \ No newline at end of file diff --git a/public/language/nb/admin/advanced/database.json b/public/language/nb/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/nb/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/nb/admin/advanced/errors.json b/public/language/nb/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/nb/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/nb/admin/advanced/events.json b/public/language/nb/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/nb/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/nb/admin/advanced/logs.json b/public/language/nb/admin/advanced/logs.json new file mode 100644 index 0000000000..ba29274563 --- /dev/null +++ b/public/language/nb/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logger", + "control-panel": "Kontrollpanel for logg", + "reload": "Last inn logg på nytt", + "clear": "Tøm logg", + "clear-success": "Logg er tømt!" +} \ No newline at end of file diff --git a/public/language/nb/admin/appearance/customise.json b/public/language/nb/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/nb/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/nb/admin/appearance/skins.json b/public/language/nb/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/nb/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/nb/admin/appearance/themes.json b/public/language/nb/admin/appearance/themes.json new file mode 100644 index 0000000000..fcb509e365 --- /dev/null +++ b/public/language/nb/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Hjemmeside", + "select-theme": "Velg tema", + "current-theme": "Nåværende tema", + "no-themes": "Ingen installerte temaer funnet", + "revert-confirm": "Er du sikker på at du vil gjenopprette standard NodeBB-tema?", + "theme-changed": "Tema endret", + "revert-success": "Du har tilbakestilt NodeBB til standardtemaet.", + "restart-to-activate": "Vennligst bygg og start NodeBB for å aktivere dette temaet fullt ut." +} \ No newline at end of file diff --git a/public/language/nb/admin/dashboard.json b/public/language/nb/admin/dashboard.json new file mode 100644 index 0000000000..112aa9389e --- /dev/null +++ b/public/language/nb/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Blar i tråder", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "Høyt synlige tråder", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/nb/admin/development/info.json b/public/language/nb/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/nb/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/nb/admin/development/logger.json b/public/language/nb/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/nb/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/nb/admin/extend/plugins.json b/public/language/nb/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/nb/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/nb/admin/extend/rewards.json b/public/language/nb/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/nb/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/nb/admin/extend/widgets.json b/public/language/nb/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/nb/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/admins-mods.json b/public/language/nb/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/nb/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/categories.json b/public/language/nb/admin/manage/categories.json new file mode 100644 index 0000000000..28d2bc5d4d --- /dev/null +++ b/public/language/nb/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figur 3 – Daglige tråder publisert i denne kategorien", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Vil du virkelig renske kategorien \"%1\"?

Advarsel! Alle tråder og innlegg i denne kategorien vil bli rensket!

Rensking av en kategori vil fjerne alle tråder og innlegg, og slette kategorien fra databasen. Hvis du vil fjerne en kategori midlertidig, vil du \"deaktivere\" kategorien i stedet.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/digest.json b/public/language/nb/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/nb/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/nb/admin/manage/groups.json b/public/language/nb/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/nb/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/privileges.json b/public/language/nb/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/nb/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/registration.json b/public/language/nb/admin/manage/registration.json new file mode 100644 index 0000000000..70676dfe9f --- /dev/null +++ b/public/language/nb/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitasjoner", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Er du sikker på at du ønsker å slette denne invitasjonen?" +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/tags.json b/public/language/nb/admin/manage/tags.json new file mode 100644 index 0000000000..6267762c7a --- /dev/null +++ b/public/language/nb/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Forumet ditt har ingen tråder med emneord.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/uploads.json b/public/language/nb/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/nb/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/nb/admin/manage/users.json b/public/language/nb/admin/manage/users.json new file mode 100644 index 0000000000..c63f6819b5 --- /dev/null +++ b/public/language/nb/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Utestengt", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "omdømme", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Advarsel!

Vil du virkelig slette bruker(e)?

Denne handlingen kan ikke angres! Kun brukerkontoen vil bli slettet, deres innlegg og tråder påvirkes ikke.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "En invitasjonse-post har blitt sendt til %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/nb/admin/menu.json b/public/language/nb/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/nb/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/advanced.json b/public/language/nb/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/nb/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/api.json b/public/language/nb/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/nb/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/chat.json b/public/language/nb/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/nb/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/cookies.json b/public/language/nb/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/nb/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/email.json b/public/language/nb/admin/settings/email.json new file mode 100644 index 0000000000..f5aa006956 --- /dev/null +++ b/public/language/nb/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "E-postinnstillinger", + "address": "E-postadresse", + "address-help": "Følgende e-postadresse viser til e-postadressen som mottakeren vil se i \"Fra\" og \"Svar til\"-feltene. ", + "from": "From Name", + "from-help": "Avsendernavnet som skal vises i e-posten.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "Det ser ut som at du konfigurerer en SMTP transport. Vi skrudde på «SMTP Transport»-alternativet for deg.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Brukernavn", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Endre e-postmal", + "template.select": "Velg e-postmal", + "template.revert": "Revert to Original", + "testing": "E-posttesting", + "testing.select": "Velg e-postmal", + "testing.send": "Send test-e-post", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "E-postsammendrag", + "subscriptions.disable": "Deaktiver e-postsammendrag", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Skriv inn et tall som representerer den timen planlagte e-postsammendrag skal sendes ut (f.eks. 0 for midnatt, 17 for 17:00). Husk at denne tiden forholder seg til serverens tid, og kan avvike fra din systemklokke.
Den omtrentlige servertiden er:
Det neste daglige sammendraget er planlagt utsendt ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Krev at nye brukere legger til en e-postadresse", + "require-email-address-warning": "Som standard, kan brukere velge bort å fylle ut e-postadresse ved å la feltet stå tomt. Å skru på dette valget innebærer at de må legge inn en e-postadresse for å kunne fortsette registreringen. Dette sikrer ikke at brukeren vil legge inn en gyldig e-postadresse, heller ikke en e-postadresse de eier.", + "send-validation-email": "Send bekreftelses-e-post når en e-post legges til eller endres", + "include-unverified-emails": "Send e-post til mottakere som ikke eksplisitt har bekreftet e-postadressen sin.", + "include-unverified-warning": "Som standard, vil brukere som har e-postadresse knyttet til deres konto allerede være verifisert, men det er noen situasjoner hvor dette ikke er tilfelle (f.eks. SSO-innlogginger, grandfathered users, etc). Skru på denne innstillingen på egen risiko – å sende e-poster til uverifiserte e-postadresser kan være brudd på regionale anti-spam-regler.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "Hvis en bruker mangler e-postadresse, eller e-postadressen ikke er bekreftet, vil en advarsel vises på skjermen. ", + "sendEmailToBanned": "Send e-post til brukere selv om de har blitt utestengt" +} diff --git a/public/language/nb/admin/settings/general.json b/public/language/nb/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/nb/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/nb/admin/settings/group.json b/public/language/nb/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/nb/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/guest.json b/public/language/nb/admin/settings/guest.json new file mode 100644 index 0000000000..ab416c7889 --- /dev/null +++ b/public/language/nb/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Innstillinger ", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "Dette alternativet viser et nytt felt som lar gjestene velge et navn som kan knyttes til hvert innlegg de lager. Hvis de er deaktivert, vil de bare bli kalt \"Gjest\"", + "topic-views.enabled": "La gjestene øke antall visninger av emner", + "reply-notifications.enabled": "La gjestene generere varsler på svar" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/homepage.json b/public/language/nb/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/nb/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/languages.json b/public/language/nb/admin/settings/languages.json new file mode 100644 index 0000000000..812345597c --- /dev/null +++ b/public/language/nb/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "Det prevalgte språket avgjør språkinnstillingene for alle brukere som besøker forumet ditt.
Indiviuelle brukere kan overstyre det prevalgte språket inne på deres kontoinnstillinger.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/navigation.json b/public/language/nb/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/nb/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/nb/admin/settings/notifications.json b/public/language/nb/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/nb/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/pagination.json b/public/language/nb/admin/settings/pagination.json new file mode 100644 index 0000000000..2fb3343b1d --- /dev/null +++ b/public/language/nb/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginer tråder og innlegg istedet for å bruke uendelig scrolling.", + "posts": "Post Pagination", + "topics": "Trådpaginering", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Tråder per side", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/post.json b/public/language/nb/admin/settings/post.json new file mode 100644 index 0000000000..68f031bacc --- /dev/null +++ b/public/language/nb/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Standard trådsortering", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Omdømme-terskel før disse restriksjonene fjernes", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Antall svar i tråd før bruker ikke lenger får lov til å slette egen tråd (sett til 0 for å deaktivere)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum antall innlegg i tråd før registrering av sist lest", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "composer-help": "Følgende innstillinger styrer funksjonaliteten og/ eller utseendet til skriveverktøyet vist\n\t\t\t\ttil brukere når de lager nye tråder, eller svarer på eksisterende tråder.", + "composer.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/reputation.json b/public/language/nb/admin/settings/reputation.json new file mode 100644 index 0000000000..ddf6fd0f44 --- /dev/null +++ b/public/language/nb/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Omdømmeinnstillinger", + "disable": "Skru av omdømmesystem", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum omdømme for å stemme opp innlegg", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum omdømme for å stemme ned innlegg", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum omdømme for å flagge innlegg", + "min-rep-website": "Minimum omdømme som kreves for å legge \"Nettsted\" til brukerprofil", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/social.json b/public/language/nb/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/nb/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/sockets.json b/public/language/nb/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/nb/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/sounds.json b/public/language/nb/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/nb/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/tags.json b/public/language/nb/admin/settings/tags.json new file mode 100644 index 0000000000..57c22ae822 --- /dev/null +++ b/public/language/nb/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimalt antall emneord per tråd", + "max-per-topic": "Maksimalt antall emneord per tråd", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Relaterte tråder", + "max-related-topics": "Maks antall relaterte tråder å vise (hvis støttet av tema)" +} \ No newline at end of file diff --git a/public/language/nb/admin/settings/uploads.json b/public/language/nb/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/nb/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/nb/admin/settings/user.json b/public/language/nb/admin/settings/user.json new file mode 100644 index 0000000000..7998884043 --- /dev/null +++ b/public/language/nb/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Autentisering ", + "email-confirm-interval": "Brukeren kan ikke sende en bekreftelses-e-post på nytt før", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Tillat innlogging med", + "allow-login-with.username-email": "Brukernavn eller e-post", + "allow-login-with.username": "Kun brukernavn", + "account-settings": "Kontoinnstillinger ", + "gdpr_enabled": "Aktiver innhenting av GDPR-samtykke", + "gdpr_enabled_help": "Når aktivert, vil alle nye registranter være pålagt å eksplisitt gi samtykke til datainnsamling og behandling under Personvernforordningen (GDPR). Merk: Aktivering av GDPR tvinger ikke eksisterende brukere til å gi samtykke. For å gjøre dette, må du installere GDPR-programutvidelse. ", + "disable-username-changes": "Deaktiver endringer for brukernavn", + "disable-email-changes": "Deaktiver endringer for e-post", + "disable-password-changes": "Deaktiver endringer for passord", + "allow-account-deletion": "Tillat kontosletting ", + "hide-fullname": "Skjul fullt navn for andre brukere", + "hide-email": "Skjul e-post for andre brukere", + "show-fullname-as-displayname": "Vis brukerens fulle navn som navn ved visning hvis tilgjengelig", + "themes": "Temaer", + "disable-user-skins": "Forhindre brukere fra å velge en tilpasset skin", + "account-protection": "Kontobeskyttelse", + "admin-relogin-duration": "Innloggingstid for administrator (minutter)", + "admin-relogin-duration-help": "Etter en angitt tid for å få tilgang til administrasjon-delen, vil det kreve pålogging på nytt, sett til 0 for å deaktivere", + "login-attempts": "Innloggingsforsøk per time", + "login-attempts-help": "Hvis påloggingsforsøk til brukerens konto overskrider denne terskelen, vil brukerkontoen bli låst i en forhåndskonfigurert tid", + "lockout-duration": "Varighet på kontosperring (minutter)", + "login-days": "Dager å huske brukerinnloggingsøkter på", + "password-expiry-days": "Tving passordtilbakestillingen etter angitt antall dager", + "session-time": "Tidssesjon ", + "session-time-days": "Dager", + "session-time-seconds": "Sekunder", + "session-time-help": "Disse verdiene brukes for å følge med på hvor lenge en bruker er logget inn når de sjekker "Remember Me" ved pålogging. Merk at kun en av disse verdiene brukes. Hvis det ikke er sekundverdi bruker vi dager. Hvis det ikke er noen verdier for dager faller verdien tilbake til 14 dager.", + "online-cutoff": "Minutter etter at bruker er ansett som inaktiv ", + "online-cutoff-help": "Hvis brukeren ikke utfører noen handlinger for den bestemte varigheten, anses de som inaktive, og de mottar ikke sanntidsoppdateringer.", + "registration": "Brukerregistrering", + "registration-type": "Registreringstype", + "registration-approval-type": "Registrering godkjenningstype", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Administrator-godkjenning", + "registration-type.admin-approval-ip": "Administrator godkjenning for IP-er", + "registration-type.invite-only": "Kun invitasjon", + "registration-type.admin-invite-only": "Kun invitasjon fra administrator ", + "registration-type.disabled": "Ingen registrering ", + "registration-type.help": "Normalt - Brukere kan registrere seg fra / registersiden.
\nKun invitasjon - Brukere kan invitere andre fra brukersiden \nKun admin-invitasjon- Kun administratorer kan invitere andre fra brukere og administrere/brukere sidene .
\n Ingen registrering - Ingen brukerregistrering.
", + "registration-approval-type.help": "Normalt - Brukere registreres umiddelbart.
\nAdministratorgodkjenning - Brukerregistreringer plasseres i en kø for godkjenning for administratorer.
\nAdmin-godkjenning for IP-er - Normalt for nye brukere, Admin-godkjenning for IP-adresser som allerede har en konto.
", + "registration-queue-auto-approve-time": "Automatisk godkjenningstid", + "registration-queue-auto-approve-time-help": "Timer før brukeren godkjennes automatisk. 0 for å deaktivere.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maksimum invitasjoner per bruker", + "max-invites": "Maksimum invitasjoner per bruker", + "max-invites-help": "0 for uten begrensning. Administratorer får uendelige invitasjoner
Gjelder kun for \"Bare inviter\"", + "invite-expiration": "Invitasjon utløpt", + "invite-expiration-help": "Invitasjoner utløper om # dager.", + "min-username-length": "Minimum lengde på brukernavnet ", + "max-username-length": "Maksimum lengde på brukernavn ", + "min-password-length": "Maksimum passordlengde", + "min-password-strength": "Minimum passordstyrke", + "max-about-me-length": "Maksimum lengde på om meg", + "terms-of-use": "Brukervilkår for nettforumet (La stå tomt for å deaktivere)", + "user-search": "Brukersøk", + "user-search-results-per-page": "Antall resultater å vise", + "default-user-settings": "Standard brukerinnstillinger ", + "show-email": "Vis e-post", + "show-fullname": "Vis fullt navn", + "restrict-chat": "Tillat kun chatt-meldinger fra brukere jeg følger", + "outgoing-new-tab": "Åpne utgående lenker i ny fane", + "topic-search": "Aktiver i-tråd-søk", + "update-url-with-post-index": "Oppdater url med postindeks mens du surfer på emner", + "digest-freq": "Abonner på sammendrag", + "digest-freq.off": "Av", + "digest-freq.daily": "Daglig", + "digest-freq.weekly": "Ukentlig", + "digest-freq.biweekly": "Annenhver uke", + "digest-freq.monthly": "Månedlig ", + "email-chat-notifs": "Send en e-post hvis jeg mottar en ny chatt-melding om jeg ikke er online. ", + "email-post-notif": "Send en e-post når det kommer svar på tråder jeg abonnerer på", + "follow-created-topics": "Følg tråder du lager", + "follow-replied-topics": "Følg tråder du svarer på", + "default-notification-settings": "Standard varslingsinnstillinger", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Overvåker", + "categoryWatchState.notwatching": "Overvåker ikke", + "categoryWatchState.ignoring": "Ignorerer" +} diff --git a/public/language/nb/admin/settings/web-crawler.json b/public/language/nb/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/nb/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/nb/category.json b/public/language/nb/category.json new file mode 100644 index 0000000000..b605c65ab1 --- /dev/null +++ b/public/language/nb/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategori", + "subcategories": "Underkategorier", + "new_topic_button": "Nytt emne", + "guest-login-post": "Logg inn for å publisere innlegg", + "no_topics": "Det er ingen emner i denne kategorien
Hvorfor ikke opprette et?", + "browsing": "leser", + "no_replies": "Ingen har svart", + "no_new_posts": "Ingen nye innlegg.", + "watch": "Overvåk", + "ignore": "Ignorer", + "watching": "Følger", + "not-watching": "Følger ikke", + "ignoring": "Ignorerer", + "watching.description": "Vis tråder blandt uleste og nylige", + "not-watching.description": "Ikke vis emner i ulest, vis nylig ", + "ignoring.description": "Ikke vis tråder blandt uleste og nylige", + "watching.message": "Du ser nå på oppdateringer fra denne kategorien og alle underkategorier", + "notwatching.message": "Du ser ikke på oppdateringer fra denne kategorien og alle underkategorier", + "ignoring.message": "Du ignorerer nå oppdateringer fra denne kategorien og alle underkategorier ", + "watched-categories": "Overvåkede kategorier", + "x-more-categories": "%1 flere kategorier" +} \ No newline at end of file diff --git a/public/language/nb/email.json b/public/language/nb/email.json new file mode 100644 index 0000000000..e36f798865 --- /dev/null +++ b/public/language/nb/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test e-post", + "password-reset-requested": "Tilbakestilling av passord er påkrevd!", + "welcome-to": "Velkommen til %1", + "invite": "Invitasjon fra %1", + "greeting_no_name": "Hei", + "greeting_with_name": "Hei, %1", + "email.verify-your-email.subject": "Bekreft e-postadressen din ", + "email.verify.text1": "Du har bedt oss om å endre eller bekrefte e-postadressen din", + "email.verify.text2": "Av sikkerhetshensyn endrer eller bekrefter vi bare e-postadressen som er registrert når eierskapet er bekreftet via e-post. Hvis du ikke har bedt om dette, er det ikke nødvendig å gjøre noe fra din side.", + "email.verify.text3": "Når du bekrefter denne e-postadressen, bytter vi ut din nåværende e-postadresse med denne (%1). ", + "welcome.text1": "Takk for at du registrerte deg hos %1!", + "welcome.text2": "For å aktivere kontoen din må vi verifisere at du eier e-postadressen du registrerte deg med.", + "welcome.text3": "En administrator har akseptert din søknad om registering. Du kan nå logge inn med ditt brukernavn og passord.", + "welcome.cta": "Klikk her for å verifisere e-postadressen din", + "invitation.text1": "%1 har invitert deg til å bli med i %2", + "invitation.text2": "Invitasjonen din utløper om %1 dager.", + "invitation.cta": "Klikk her for å opprette kontoen din.", + "reset.text1": "Vi har blitt bedt om å tilbakestille passordet ditt, muligens fordi du har glemt det. Hvis dette ikke stemmer kan du ignorere denne e-posten.", + "reset.text2": "Vennligst klikk på følgende lenke for å fortsette med tilbakestillingen:", + "reset.cta": "Klikk her for å tilbakestille passordet ditt", + "reset.notify.subject": "Passordet ble endret", + "reset.notify.text1": "Vi gjør deg oppmerksom på at du endret passordet ditt den %1.", + "reset.notify.text2": "Hvis det ikke var deg som autoriserte dette, vennligst gi beskjed til en administrator umiddelbart.", + "digest.latest_topics": "Siste emner fra %1", + "digest.top-topics": "Toppemner fra %1 ", + "digest.popular-topics": "Populære emner fra %1", + "digest.cta": "Klikk her for å besøke %1", + "digest.unsub.info": "Dette sammendraget er sendt til deg basert på dine innstillinger for abonnering.", + "digest.day": "Dag", + "digest.week": "Uke", + "digest.month": "Måned", + "digest.subject": "Sammendrag for %1", + "digest.title.day": "Ditt daglige sammendrag", + "digest.title.week": "Ditt ukentlige sammendrag ", + "digest.title.month": "Ditt månedlige sammendrag", + "notif.chat.subject": "Ny samtalemelding mottatt fra %1", + "notif.chat.cta": "Klikk her for å fortsette samtalen", + "notif.chat.unsub.info": "Denne samtale-varselen ble sendt til deg basert på dine innstillinger for abonnering.", + "notif.post.unsub.info": "Dette innleggsvarselet ble sendt til deg basert på dine innstillinger for abonnering.", + "notif.post.unsub.one-click": "Alternativt kan du avslutte abonnementet på fremtidige e-poster som dette, ved å klikke", + "notif.cta": "Til forum", + "notif.cta-new-reply": "Vis post", + "notif.cta-new-chat": "Vis chatt", + "notif.test.short": "Testing av varsler ", + "notif.test.long": "Dette er en test av e-postmeldingen for varsler. Send hjelp!", + "test.text1": "Dette er en test e-post for å verifisere at e-postsystemet i NodeBB fungerer som det skal.", + "unsub.cta": "Klikk her for å endre disse innstillingene", + "unsubscribe": "Avfølg", + "unsub.success": "Du vil ikke lenger motta e-poster fra %1 utsendelseslisten ", + "unsub.failure.title": "Kan ikke avslutte abonnementet", + "unsub.failure.message": "Dessverre kunne vi ikke melde deg av e-postlisten, da det var et problem med lenken. Du kan imidlertid endre preferansene for e-post ved å gå tilinnstillinger.

(feil:1%):", + "banned.subject": "Du har blitt utestengt fra %1", + "banned.text1": "Brukeren %1 er utestengt fra %2.", + "banned.text2": "Dette forbudet varer til %1.", + "banned.text3": "Dette er grunnen til at du har blitt utestengt:", + "closing": "Takk!" +} \ No newline at end of file diff --git a/public/language/nb/error.json b/public/language/nb/error.json new file mode 100644 index 0000000000..64130de51f --- /dev/null +++ b/public/language/nb/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Ugyldige data", + "invalid-json": "Ugyldig JSON", + "wrong-parameter-type": "En verdi av typen %3 var forventet for egenskapen `%1`, men %2 ble mottatt i stedet", + "required-parameters-missing": "Nødvendige parametere manglet fra dette API-kallet: %1", + "not-logged-in": "Du ser ikke ut til å være logget inn.", + "account-locked": "Kontoen din har blitt midlertidig låst", + "search-requires-login": "Søking krever en konto - vennligst logg inn eller registrer deg.", + "goback": "Trykk på tilbakeknappen for å gå tilbake til forrige side", + "invalid-cid": "Ugyldig kategori-ID", + "invalid-tid": "Ugyldig emne-ID", + "invalid-pid": "Ugyldig innlegg-ID", + "invalid-uid": "Ugyldig bruker-ID", + "invalid-mid": "Ugyldig ID for chattmelding", + "invalid-date": "En gyldig dato må oppgis", + "invalid-username": "Ugyldig brukernavn", + "invalid-email": "Ugyldig e-post", + "invalid-fullname": "Ugyldig fullt navn", + "invalid-location": "Ugyldig plassering", + "invalid-birthday": "Ugyldig bursdag", + "invalid-title": "Ugyldig tittel", + "invalid-user-data": "Ugyldig brukerdata", + "invalid-password": "Ugyldig passord", + "invalid-login-credentials": "Ugyldige innloggingsdata", + "invalid-username-or-password": "Vennligst spesifiser både et brukernavn og passord", + "invalid-search-term": "Ugyldig søkeord", + "invalid-url": "Ugyldig lenkeadresse", + "invalid-event": "Ugyldig hendelse: %1", + "local-login-disabled": "Lokalt innloggingssystem har blitt deaktivert for ikke-privelegerte brukere", + "csrf-invalid": "Vi kunne ikke logge deg inn, sannsynligvis på grunn av en utgått sesjon. Vennligst prøv igjen", + "invalid-path": "ugyldig sti", + "folder-exists": "Mappen eksisterer", + "invalid-pagination-value": "Ugyldig sidetall, må være minst %1 og maks %2", + "username-taken": "Brukernavn opptatt", + "email-taken": "E-post opptatt", + "email-nochange": "E-posten som er angitt er den samme e-posten som allerede er lagret.", + "email-invited": "E-post har allerede fått invitasjon", + "email-not-confirmed": "Posting i enkelte kategorier eller emner blir aktivert når e-posten din er bekreftet. Klikk her for å sende en bekreftelses-e-post. ", + "email-not-confirmed-chat": "Du kan ikke chatte før e-posten din er bekreftet, vennligst klikk her for å bekrefte e-postadressen.", + "email-not-confirmed-email-sent": "E-posten din er ikke bekreftet ennå, sjekk innboksen din for bekreftelses-e-post. Det kan hende du ikke kan legge ut innlegg i enkelte kategorier eller chatte før e-posten din er bekreftet.", + "no-email-to-confirm": "Kontoen din mangler e-postadresse. En e-postadresse er nødvendig for gjenoppretting av konto, og kan være nødvendig for chatting og innlegg i enkelte kategorier. Klikk her for å skrive inn en e-postadresse.", + "user-doesnt-have-email": "Brukeren «%1» har ikke lagt til e-postadresse. ", + "email-confirm-failed": "Vi kunne ikke bekrefte e-posten din, vennligst prøv igjen senere.", + "confirm-email-already-sent": "E-post for bekreftelse er allerede sendt, vennligst vent %1 minutt(er) for å sende en til.", + "sendmail-not-found": "Funksjonaliteten \"sendmail\" ble ikke funnet, vennligst sjekk at den er installert og kjørbar av brukeren som kjører NodeBB.", + "digest-not-enabled": "Denne brukeren har ikke oppsummeringer aktivert, eller systemstandarden er ikke konfigurert til å sende ut oppsummeringer", + "username-too-short": "Brukernavnet er for kort", + "username-too-long": "Brukernavnet er for langt", + "password-too-long": "Passordet er for langt", + "reset-rate-limited": "For mange passord-tilbakestillinger er forespurt (begrenset antall forespørsler)", + "reset-same-password": "Vennligst bruk et passord som er annerledes fra ditt nåværende", + "user-banned": "Bruker utestengt", + "user-banned-reason": "Beklager, denne kontoen har blitt utestengt (Grunn: %1)", + "user-banned-reason-until": "Beklager, denne kontoen har blit utestengt til %1 (Grunn: %2)", + "user-too-new": "Beklager, du må vente %1 sekund(er) før du oppretter ditt første innlegg", + "blacklisted-ip": "Beklager, din IP-adresse har blitt utestengt fra dette forumet. Hvis du mener dette er en feil, vennligst kontakt en sideadministrator.", + "ban-expiry-missing": "Vennligst oppgi et sluttidspunkt for denne utestengingen.", + "no-category": "Kategorien eksisterer ikke", + "no-topic": "Emne eksisterer ikke", + "no-post": "Innlegg eksisterer ikke", + "no-group": "Gruppe eksisterer ikke", + "no-user": "Bruker eksisterer ikke", + "no-teaser": "Teaseren eksisterer ikke", + "no-flag": "Flag does not exist", + "no-privileges": "Du har ikke nok rettigheter til å utføre denne handlingen.", + "category-disabled": "Kategori deaktivert", + "topic-locked": "Emne låst", + "post-edit-duration-expired": "Du har bare lov til å redigere innlegg i %1 sekund(er) etter at det er sendt", + "post-edit-duration-expired-minutes": "Du har bare lov til å redigere innlegg i %1 sekund(er) etter at det er sendt", + "post-edit-duration-expired-minutes-seconds": "Du har bare lov til å redigere innlegg i %1 minutt(er), %2 sekund(er) etter at det er sendt", + "post-edit-duration-expired-hours": "Du har bare lov til å redigere innlegg i %1 time(r) etter at det er sendt", + "post-edit-duration-expired-hours-minutes": "Du har bare lov til å redigere innlegg i %1 time(r), %2 minutt(er) etter at det er sendt", + "post-edit-duration-expired-days": "Du har bare lov til å redigere innlegg i %1 dag(er) etter at det er sendt", + "post-edit-duration-expired-days-hours": "Du har bare lov til å redigere innlegg i %1 dag(er), %2 time(r) etter at det er sendt", + "post-delete-duration-expired": "Du har bare lov til å slette innlegg i %1 sekund(er) etter at det er sendt", + "post-delete-duration-expired-minutes": "Du har bare lov til å slette innlegg i %1 sekund(er) etter at det er sendt", + "post-delete-duration-expired-minutes-seconds": "Du har bare lov til å slette innlegg i %1 minutt(er), %2 sekund(er) etter at det er sendt", + "post-delete-duration-expired-hours": "Du har bare lov til å slette innlegg i %1 time(r) etter at det er sendt", + "post-delete-duration-expired-hours-minutes": "Du har bare lov til å slette innlegg i %1 time(r), %2 minutt(er) etter at det er sendt", + "post-delete-duration-expired-days": "Du har bare lov til å slette innlegg i %1 dag(er) etter at det er sendt", + "post-delete-duration-expired-days-hours": "Du har bare lov til å slette innlegg i %1 dag(er), %2 time(r) etter at det er sendt", + "cant-delete-topic-has-reply": "Du kan ikke slette tråden din etter den har fått et innlegg", + "cant-delete-topic-has-replies": "Du kan ikke slette tråden din etter den har %1 innlegg", + "content-too-short": "Vennligst skriv et lengre innlegg. Innlegg må inneholde minst %1 tegn.", + "content-too-long": "Vennligst skriv et kortere innlegg. Innlegg kan ikke være lengre enn %1 tegn.", + "title-too-short": "Vennligst skriv en lengre tittel. Titler må inneholde minst %1 tegn.", + "title-too-long": "Vennligst skriv en kortere tittel. Tittel kan ikke være lengre enn %1 tegn.", + "category-not-selected": "Kategori ikke valgt", + "too-many-posts": "Du kan bare poste en gang per %1 sekund(er) – vennligst vent før du poster igjen", + "too-many-posts-newbie": "Som ny bruker kan du bare poste en gang per %1. sekund(er), før du har opparbeidet %2 i omdømme – vennligst vent før du poster igjen", + "already-posting": "You are already posting", + "tag-too-short": "Vennligst skriv et lengre emneord. Disse må være på minst %1 tegn", + "tag-too-long": "Vennligst skriv et kortere emneord. Disse kan ikke være lengre enn %1 tegn", + "not-enough-tags": "Ikke nok emneord. Emner må ha minst %1.", + "too-many-tags": "For mange emneord. Emner kan ikke ha flere enn %1.", + "cant-use-system-tag": "Du kan ikke bruke dette emneordet", + "cant-remove-system-tag": "Du kan ikke fjerne denne systemtaggen.", + "still-uploading": "Vennligst vent til opplastingene er fullført.", + "file-too-big": "Største tillatte filstørrelse er %1 kB – vennligst last opp en mindre fil", + "guest-upload-disabled": "Gjester har ikke tilgang til å laste opp filer", + "cors-error": "Kunne ikke laste opp bilde på grunn av feilinstillt CORS", + "upload-ratelimit-reached": "Du har lastet opp for mange filer samtidig. Vennligst prøv igjen senere.", + "scheduling-to-past": "Vennligst velg en dato i fremtiden.", + "invalid-schedule-date": "Vennligst skriv inn en gyldig dato og tidspunkt.", + "cant-pin-scheduled": "Planlagte tråder kan ikke bli (u)festet.", + "cant-merge-scheduled": "Planlagte tråder kan ikke slås sammen.", + "cant-move-posts-to-scheduled": "Kan ikke flytte innlegg til en planlagt tråd.", + "cant-move-from-scheduled-to-existing": "Kan ikke flytte innlegg fra en planlagt tråd til en eksisterende tråd.", + "already-bookmarked": "Du har allerede bokmerket dette innlegget", + "already-unbookmarked": "Du har allerede fjernet bokmerket fra dette innlegget", + "cant-ban-other-admins": "Du kan ikke utestenge andre administratorer!", + "cant-mute-other-admins": "Du kan ikke kneble andre administratorer.", + "user-muted-for-hours": "Du har blitt kneblet, du vil være i stand til å skrive innlegg om %1 time(r). ", + "user-muted-for-minutes": "Du har blitt kneblet, du vil være i stand til å skrive innlegg om %1 minutt(er).", + "cant-make-banned-users-admin": "Du kan ikke gjøre utestengte brukere til administrator. ", + "cant-remove-last-admin": "Du er den eneste administratoren. Legg til en annen bruker som administrator før du fjerner deg selv.", + "account-deletion-disabled": "Kontosletting er deaktivert", + "cant-delete-admin": "Fjern administratorrettigheter fra denne kontoen før du prøver å slette den.", + "already-deleting": "Sletting pågår allerede", + "invalid-image": "Ugyldig bilde", + "invalid-image-type": "Ugyldig bildetype. Tilatte typer er: %1", + "invalid-image-extension": "Ugyldig bildefiltype", + "invalid-file-type": "Ugyldig filtype. Tillatte typer er: %1", + "invalid-image-dimensions": "Bildedimensjoner er for store", + "group-name-too-short": "Gruppenavnet er for kort", + "group-name-too-long": "Gruppenavnet er for kort", + "group-already-exists": "Gruppe eksisterer allerede", + "group-name-change-not-allowed": "Endring av gruppenavn er ikke tillatt", + "group-already-member": "Allerede del av denne gruppen", + "group-not-member": "Ikke medlem av denne gruppen", + "group-needs-owner": "Denne gruppen krever minst en eier", + "group-already-invited": "Denne brukeren har allerede blitt invitert", + "group-already-requested": "Forespørsel om medlemskap er allerede innsendt", + "group-join-disabled": "Du kan ikke bli med i denne gruppen på dette tidspunktet", + "group-leave-disabled": "Du kan ikke forlate denne gruppen på dette tidspunktet", + "post-already-deleted": "Dette innlegget har blitt slettet", + "post-already-restored": "Dette innlegget har allerede blitt gjenopprettet", + "topic-already-deleted": "Dette emnet har allerede blitt slettet", + "topic-already-restored": "Dette emnet har allerede blitt gjenopprettet", + "cant-purge-main-post": "Du kan ikke slette hovedinnlegget. Vennligst slett emnet i stedet.", + "topic-thumbnails-are-disabled": "Emne-minatyrbilder har blitt deaktivert", + "invalid-file": "Ugyldig fil", + "uploads-are-disabled": "Opplastinger er deaktivert", + "signature-too-long": "Beklager, signaturen din kan ikke være lengre enn %1 tegn", + "about-me-too-long": "Beklager, om meg kan ikke være lengre enn %1 tegn.", + "cant-chat-with-yourself": "Du kan ikke chatte med deg selv!", + "chat-restricted": "Denne brukeren har begrenset sine samtalemeldinger. De må følge deg før du kan chatte med dem", + "chat-disabled": "Chattesystem er deaktivert", + "too-many-messages": "Du har sendt for mange meldinger, vennligst vent en stund.", + "invalid-chat-message": "Ugyldig samtalemelding", + "chat-message-too-long": "Chattebeskjeder kan ikke være lengre enn %1 tegn.", + "cant-edit-chat-message": "Du har ikke tilgang til å redigere denne meldingen", + "cant-delete-chat-message": " Du har ikke lov til å slette denne brukeren", + "chat-edit-duration-expired": "Du har kun lov til å redigere meldinger i %1 sekund(er) etter at den er sendt", + "chat-delete-duration-expired": "Du har kun lov til å slette meldinger i %1 sekund(er) etter den er sendt", + "chat-deleted-already": "Denne meldingen har allerede blitt slettet.", + "chat-restored-already": "Denne meldingen har allerede blitt gjenopprettet.", + "chat-room-does-not-exist": "Dette chatterommet finnes ikke.", + "already-voting-for-this-post": "Du har allerede stemt på dette innlegget", + "reputation-system-disabled": "Omdømmesystemet er deaktivert.", + "downvoting-disabled": "Nedstemming er deaktivert", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "Du trenger %1 omdømme for å stemme opp. ", + "not-enough-reputation-to-downvote": "Du trenger %1 omdømme for å stemme ned. ", + "not-enough-reputation-to-flag": "Du trenger %1 omdømme for å flagge dette innlegget.", + "not-enough-reputation-min-rep-website": "Du trenger %1 omdømme for å legge til et nettsted", + "not-enough-reputation-min-rep-aboutme": "Du trenger %1 omdømme for å legge til om-meg", + "not-enough-reputation-min-rep-signature": "Du trenger %1 omdømme for å legge til signatur", + "not-enough-reputation-min-rep-profile-picture": "Du trenger %1 omdømme for å legge til profilbilde", + "not-enough-reputation-min-rep-cover-picture": "Du trenger %1 omdømme for å legge til omslagsbilde", + "post-already-flagged": "Du har allerede flagget dette innlegget", + "user-already-flagged": "Du har allerede flagget denne brukeren", + "post-flagged-too-many-times": "Dette innlegget har allerede blitt flagget av andre", + "user-flagged-too-many-times": "Denne brukeren har allerede blitt flagget av andre", + "cant-flag-privileged": "Du har ikke lov til å flagge profiler eller innhold fra priveligerte burkere (moderatorer/ globale moderatorer/ administratorer)", + "self-vote": "Du kan ikke stemme på ditt eget innlegg", + "too-many-upvotes-today": "Du kan bare gi oppstemme %1 ganger pr. dag", + "too-many-upvotes-today-user": "Du kan bare gi oppstemme til en bruker %1 ganger pr. dag", + "too-many-downvotes-today": "Du kan bare nedstemme %1 gang(er) dagen", + "too-many-downvotes-today-user": "Du kan bare nedstemme en bruker %1 gang(er) dagen", + "reload-failed": "NodeBB støtte på et problem under lasting på nytt: \"%1\". NodeBB vil fortsette å servere eksisterende klientside ressurser, selv om du burde angre endringene du gjorde før du lastet på nytt.", + "registration-error": "Feil under registrering", + "parse-error": "Noe gikk feil under analysering av serversvar", + "wrong-login-type-email": "Vennligst benytt e-posten din for å logge inn", + "wrong-login-type-username": "Vennligst benytt brukernavnet ditt for å logge inn", + "sso-registration-disabled": "Registrering har blitt deaktivert for %1 konto(er), registrer deg med en e-post adresse først", + "sso-multiple-association": "Du kan ikke knytte flere kontoer til din NodeBB konto. Vennligst koble fra din eksisterende konto og prøv igjen.", + "invite-maximum-met": "Du har invitert maks antall personer (%1 av %2).", + "no-session-found": "Ingen innlogget sesjon funnet!", + "not-in-room": "Bruker ikke i rom", + "cant-kick-self": "Du kan ikke utestenge deg selv fra gruppen", + "no-users-selected": "Ingen bruker(e) valgt", + "invalid-home-page-route": "Ugyldig hjemmesidelenke", + "invalid-session": "Ugyldig økt", + "invalid-session-text": "Det ser ut til at din innloggingssesjon ikke lenger er aktiv. Last inn denne siden på nytt.", + "session-mismatch": "Mismatch på sesjon", + "session-mismatch-text": "Det ser ut til at din innloggingssesjon ikke lenger matcher med serveren. Last inn denne siden på nytt.", + "no-topics-selected": "Ingen tråder valgt!", + "cant-move-to-same-topic": "Du kan ikke flytte innlegg til samme tråd!", + "cant-move-topic-to-same-category": "Du kan ikke flytte tråd til samme kategori!", + "cannot-block-self": "Du kan ikke blokkere deg selv!", + "cannot-block-privileged": "Du kan ikke blokkere administratorer eller globale moderatorer", + "cannot-block-guest": "Gjester kan ikke blokkere andre brukere", + "already-blocked": "Denne brukeren har allerede blitt blokkert", + "already-unblocked": "Denne brukeren har allerede blitt ublokkert", + "no-connection": "Det virker å være et problem med internett-tilgangen din", + "socket-reconnect-failed": "Får ikke tilgang til serveren for øyeblikket. Klikk her for å prøve igjen, eller prøv igjen senere", + "plugin-not-whitelisted": "Ute av stand til å installere tillegget – bare tillegg som er hvitelistet av NodeBB sin pakkebehandler kan bli installert via administratorkontrollpanelet", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Trådhendelse '%1' er ukjent", + "cant-set-child-as-parent": "Kan ikke sette underkategori til hovedkategori", + "cant-set-self-as-parent": "Kan ikke sette denne som hovedkategori", + "api.master-token-no-uid": "Et master token ble mottatt uten korresponderende `_uid` i request body", + "api.400": "Noe var galt med nyttelasten i forespørselen du sendte in.", + "api.401": "En gyldig innloggingssesjon ble ikke funnet. Logg inn og prøv igjen.", + "api.403": "Du er ikke autorisert til å gjøre denne forespørselen", + "api.404": "Ugyldig API-kall", + "api.426": "HTTPS er påkrevd for forespørsler til skrive-api. Ver vennlig å sende forespørselen på nytt via HTTPS", + "api.429": "Du har gjort for mange forespørsler. Prøv igjen senere.", + "api.500": "En uventet feil oppstod mens vi prøvde å betjene forespørsel din.", + "api.501": "Ruten du prøver å kalle er ikke implementert enda. Prøv igjen i morgen", + "api.503": "Ruten du prøver å kalle er for øyeblikket ikke tilgjengelig grunnet innstilling på serveren" +} \ No newline at end of file diff --git a/public/language/nb/flags.json b/public/language/nb/flags.json new file mode 100644 index 0000000000..b53aa44aa7 --- /dev/null +++ b/public/language/nb/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Rapporter", + "first-reported": "Først rapportert", + "no-flags": "Hurra! Ingen flagg funnet", + "assignee": "Tildelt", + "update": "Oppdater ", + "updated": "Oppdatert", + "resolved": "Løst", + "target-purged": "Innholdet dette flagget refererte til er renset og er ikke lenger tilgjengelig.", + + "graph-label": "Daglige flagg", + "quick-filters": "Raske filter", + "filter-active": "Det er ett eller flere filtre som er aktive i denne listen over flagg", + "filter-reset": "Fjern filtre ", + "filters": "Filteralternativer", + "filter-reporterId": "Rapporter UID", + "filter-targetUid": "Flaggede UID", + "filter-type": "Flaggtype", + "filter-type-all": "alt innhold", + "filter-type-post": "Innlegg", + "filter-type-user": "Bruker", + "filter-state": "Status", + "filter-assignee": "Rettighetshavers UID", + "filter-cid": "Kategori", + "filter-quick-mine": "Tildelt til meg", + "filter-cid-all": "Alle kategorier", + "apply-filters": "Bruke filtre", + "more-filters": "Flere filtre ", + "fewer-filters": "Færre filtre", + + "quick-actions": "Raske handlinger ", + "flagged-user": "Flagget bruker", + "view-profile": "Vis profil", + "start-new-chat": "Start ny chat", + "go-to-target": "Vis flaggmålet", + "assign-to-me": "Tildel til meg", + "delete-post": "Slett innlegg", + "purge-post": "Rens post", + "restore-post": "Gjenopprett post", + "delete": "Delete Flag", + + "user-view": "Vis profil", + "user-edit": "Rediger profil", + + "notes": "Flaggnotiser", + "add-note": "Legg til flagg", + "no-notes": "Ingen delte notiser ", + "delete-note-confirm": "Er du sikker på at du ønsker å slette flaggnotifikasjonen? ", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Merknad lagt til", + "note-deleted": "Merknad slettet", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Ingen flagghistorikk", + + "state-all": "Alle statuser", + "state-open": "Ny/Åpne", + "state-wip": "Under arbeid ", + "state-resolved": "Løst", + "state-rejected": "Avvist", + "no-assignee": "Ikke tildelt", + + "sort": "Sorter etter", + "sort-newest": "Nyeste først", + "sort-oldest": "Eldste først", + "sort-reports": "Flest rapporter", + "sort-all": "Alle flaggtyper...", + "sort-posts-only": "Kun innlegg", + "sort-downvotes": "Flest nedstemninger ", + "sort-upvotes": "Flest oppstemte", + "sort-replies": "Flest kommentarer", + + "modal-title": "Rapporter innhold", + "modal-body": "Oppgi årsaken til at du rapporterer %1 %2. Alternativt kan du bruke en av hurtigrapportknappene hvis det er aktuelt.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Støtende", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Årsak til rapportering av dette innholdet ...", + "modal-submit": "Send inn rapporten", + "modal-submit-success": "Innholdet er flagget for moderering.", + + "bulk-actions": "Massehandlinger", + "bulk-resolve": "Løse Flagg(ene).", + "bulk-success": "%1 flagg er oppdaterte ", + "flagged-timeago-readable": "Flaggede 11 (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/nb/global.json b/public/language/nb/global.json new file mode 100644 index 0000000000..4ea9222a50 --- /dev/null +++ b/public/language/nb/global.json @@ -0,0 +1,126 @@ +{ + "home": "Hjem", + "search": "Søk", + "buttons.close": "Lukk", + "403.title": "Adgang nektet", + "403.message": "Du har funnet en side du ikke har tilgang til.", + "403.login": "Kanskje du skal prøve å logge inn?", + "404.title": "Ikke funnet", + "404.message": "Du har funnet en side som ikke eksisterer. Returner til startsiden?", + "500.title": "Intern feil.", + "500.message": "Oops! Ser ut som noe gikk galt!", + "400.title": "Ugyldig forespørsel ", + "400.message": "Det ser ut til at denne lenken er ugyldig, vennligst dobbeltsjekk og prøv igjen. Ellers, gå tilbake til hjemmesiden.", + "register": "Registrer", + "login": "Logg inn", + "please_log_in": "Vennligst logg inn", + "logout": "Logg ut", + "posting_restriction_info": "Posting er foreløpig begrenset til registrerte medlemmer, klikk her for å logge inn.", + "welcome_back": "Velkommen tilbake", + "you_have_successfully_logged_in": "Du har blitt logget inn", + "save_changes": "Lagre endringer", + "save": "Lagre", + "close": "Lukk", + "pagination": "Paginering", + "pagination.out_of": "%1 ut av %2", + "pagination.enter_index": "Gå til indeks for innlegg", + "header.admin": "Admin", + "header.categories": "Kategorier", + "header.recent": "Seneste", + "header.unread": "Uleste", + "header.tags": "Emneord", + "header.popular": "Populære", + "header.top": "Topp", + "header.users": "Brukere", + "header.groups": "Grupper", + "header.chats": "Samtaler", + "header.notifications": "Varsler", + "header.search": "Søk", + "header.profile": "Profil", + "header.navigation": "Navigasjon", + "notifications.loading": "Laster varsler", + "chats.loading": "Laster samtaler", + "motd.welcome": "Velkommen til NodeBB, fremtidens diskusjonsplattform.", + "previouspage": "Forrige side", + "nextpage": "Neste side", + "alert.success": "Suksess", + "alert.error": "Feil", + "alert.banned": "Utestengt", + "alert.banned.message": "Du har nettop blitt utestengt, din tilgang er nå begrenset.", + "alert.unbanned": "Utestengelse opphevet", + "alert.unbanned.message": "Utestengelsen er opphevet", + "alert.unfollow": "Du følger ikke lenger %1!", + "alert.follow": "Du følger nå %1!", + "users": "Brukere", + "topics": "Emner", + "posts": "Innlegg", + "x-posts": "%1 post", + "best": "Best", + "controversial": "Kontroversiell ", + "votes": "Stemmer", + "x-votes": "%1 stemmer", + "voters": "Velgere", + "upvoters": "Oppstemmere", + "upvoted": "Oppstemt ", + "downvoters": "Nedstemmer", + "downvoted": "Nedstemte ", + "views": "Visninger", + "posters": "Innlegg ", + "reputation": "Omdømme", + "lastpost": "Seneste innlegg", + "firstpost": "Første innlegg ", + "read_more": "les mer", + "more": "Mer", + "none": "Ingen", + "posted_ago_by_guest": "skrevet %1 av Gjest", + "posted_ago_by": "skrevet %1 av %2", + "posted_ago": "skrevet %1", + "posted_in": "skrevet i %1", + "posted_in_by": "skrevet i %1 %2", + "posted_in_ago": "skrevet i %1 %2", + "posted_in_ago_by": "skrevet i %1 %2 av %3", + "user_posted_ago": "%1 skrev %2", + "guest_posted_ago": "Gjest skrev den %1", + "last_edited_by": "sist endret av %1", + "norecentposts": "Ingen nylige innlegg", + "norecenttopics": "Ingen nye tråder", + "recentposts": "Seneste innlegg", + "recentips": "Seneste innloggede IPer", + "moderator_tools": "Moderatorverktøy", + "online": "Tilkoblet", + "away": "Borte", + "dnd": "Ikke forstyrr", + "invisible": "Usynlig", + "offline": "Frakoblet", + "email": "E-post", + "language": "Språk", + "guest": "Gjest", + "guests": "Gjester", + "former_user": "En tidligere bruker", + "system-user": "System ", + "unknown-user": "Ukjent bruker", + "updated.title": "Forum oppdatert", + "updated.message": "Dette forumet har nettopp blitt oppdatert til den nyeste versjonen. Klikk her for å laste siden på nytt.", + "privacy": "Personvern", + "follow": "Følg", + "unfollow": "Avfølg", + "delete_all": "Slett alle", + "map": "Kart", + "sessions": "Påloggingsøkt ", + "ip_address": "IP-adresse", + "enter_page_number": "Tast inn sidenummer", + "upload_file": "Last opp fil ", + "upload": "Last opp", + "uploads": "Opplastninger", + "allowed-file-types": "Tillatte filtyper er %1", + "unsaved-changes": "Du har endringer som ikke er lagret. Er du sikker på at du ønsker å navigere bort?", + "reconnecting-message": "Ser ut til at forbindelsen med %1 forsvant, vær vennlig å vent mens vi forsøker å gjenopprette forbindelsen. ", + "play": "Start", + "cookies.message": "Dette nettstedet bruker informasjonskapsler for å sikre at du får den beste opplevelsen på nettstedet vårt.", + "cookies.accept": "Forstått!", + "cookies.learn_more": "Lær mer", + "edited": "Redigert", + "disabled": "Deaktivert ", + "select": "Velg", + "user-search-prompt": "Skriv her for å finne andre brukere..." +} \ No newline at end of file diff --git a/public/language/nb/groups.json b/public/language/nb/groups.json new file mode 100644 index 0000000000..f1526365be --- /dev/null +++ b/public/language/nb/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupper", + "view_group": "Vis gruppe", + "owner": "Gruppeeier", + "new_group": "Opprett ny gruppe", + "no_groups_found": "Det er ingen grupper å se", + "pending.accept": "Aksepter", + "pending.reject": "Avslå", + "pending.accept_all": "Aksepter alle", + "pending.reject_all": "Avslå alle", + "pending.none": "Det er ingen ventende medlemmer på dette tidspunktet", + "invited.none": "Det er ingen inviterte medlemmer på dette tidspunktet", + "invited.uninvite": "Trekk tilbake invitasjon", + "invited.search": "Søk etter en bruker å invitere til denne gruppen", + "invited.notification_title": "Du har blitt invitert til %1", + "request.notification_title": "Forespørsel om gruppemedlemskap fra %1", + "request.notification_text": "%1 har forespurt å bli medlem av %2", + "cover-save": "Lagre", + "cover-saving": "Lagrer", + "details.title": "Gruppedetaljer", + "details.members": "Medlemsliste", + "details.pending": "Ventende medlemmer", + "details.invited": "Inviterte medlemmer", + "details.has_no_posts": "Medlemmene i denne gruppen har ikke skrevet noen innlegg.", + "details.latest_posts": "Seneste innlegg", + "details.private": "Privat", + "details.disableJoinRequests": "Deaktiver forespørsler om å bli med", + "details.disableLeave": "Tillat brukere å forlate gruppen", + "details.grant": "Gi/Opphev Eierskap", + "details.kick": "Kast ut", + "details.kick_confirm": "Er du sikker på at du vil fjerne dette medlemmet fra gruppen?", + "details.add-member": "Legg til medlem", + "details.owner_options": "Gruppeadministrasjon", + "details.group_name": "Gruppenavn", + "details.member_count": "Antall medlemmer", + "details.creation_date": "Opprettelsesdato", + "details.description": "Beskrivelse", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Forhåndsvisning av skilt", + "details.change_icon": "Endre ikon", + "details.change_label_colour": "Endre fargen på etiketten", + "details.change_text_colour": "Endre farge på tekst", + "details.badge_text": "Skilt-tekst", + "details.userTitleEnabled": "Vis skilt", + "details.private_help": "Hvis aktivert, vil medlemskap i grupper kreve godkjennelse fra en gruppeeier", + "details.hidden": "Skjult", + "details.hidden_help": "Hvis aktivert, vil ikke denne gruppen bli funnet i gruppelisten, og brukere må inviteres manuelt", + "details.delete_group": "Slett gruppe", + "details.private_system_help": "Private grupper er deaktivert på systemnivå, dette alternativet gjør ikke noe ytterligere ", + "event.updated": "Gruppedetaljer har blitt oppdatert", + "event.deleted": "Gruppen \"%1\" har blitt slettet", + "membership.accept-invitation": "Aksepter invitasjon", + "membership.accept.notification_title": "Du er nå et medlem av %1", + "membership.invitation-pending": "Invitasjon venter", + "membership.join-group": "Bli med i gruppe", + "membership.leave-group": "Forlat gruppe", + "membership.leave.notification_title": "%1 har forlatt gruppen %2", + "membership.reject": "Avslå", + "new-group.group_name": "Gruppenavn:", + "upload-group-cover": "Last opp et deksel for gruppen ", + "bulk-invite-instructions": "Skriv inn en liste over kommaseparerte brukernavn for å invitere til denne gruppen", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Er du sikker på at du vil fjerne omslagsbildet? " +} \ No newline at end of file diff --git a/public/language/nb/ip-blacklist.json b/public/language/nb/ip-blacklist.json new file mode 100644 index 0000000000..6cdeb55590 --- /dev/null +++ b/public/language/nb/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Konfigurer IP-svartelisten din her.", + "description": "Noen ganger er blokkering av brukerkontoer ikke tilstrekkelig avskrekkende. Andre ganger er derfor den beste måten å beskytte et forum på å begrense tilgangen til forumet for en bestemt IP eller en rekke IP-er. I disse scenariene kan du legge til IP-adresser eller hele CIDR-blokker i denne svartelisten, og de vil bli forhindret fra å logge på eller registrere en ny konto.", + "active-rules": "Aktive regler", + "validate": "Valider svartelisten", + "apply": "Bruk svarteliste", + "hints": "Syntaks-hint", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP er utestengt" +} \ No newline at end of file diff --git a/public/language/nb/language.json b/public/language/nb/language.json new file mode 100644 index 0000000000..d0c0561764 --- /dev/null +++ b/public/language/nb/language.json @@ -0,0 +1,5 @@ +{ + "name": "Norwegian Bokmål", + "code": "nb", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/nb/login.json b/public/language/nb/login.json new file mode 100644 index 0000000000..fc6eb92fc8 --- /dev/null +++ b/public/language/nb/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Brukernavn / E-post", + "username": "Brukernavn", + "remember_me": "Husk meg?", + "forgot_password": "Glemt passord?", + "alternative_logins": "Alternativ innlogging", + "failed_login_attempt": "Innlogging mislyktes", + "login_successful": "Du har blitt logget inn!", + "dont_have_account": "Har du ikke en konto?", + "logged-out-due-to-inactivity": "Du har blitt logget ut av administratorsidene fordi du har vært inaktiv for lenge", + "caps-lock-enabled": "Caps Lock er skrudd på" +} \ No newline at end of file diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json new file mode 100644 index 0000000000..a7d97ba9c9 --- /dev/null +++ b/public/language/nb/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat med", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "Du ser på eldre meldinger, klikk her for å gå til siste melding", + "chat.send": "Send", + "chat.no_active": "Du har ingen aktive chatter.", + "chat.user_typing": "%1 skriver ...", + "chat.user_has_messaged_you": "%1 har sendt deg en melding", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Vennligst velg en mottaker for å vise chatte-melding historikk", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Nylige chatter", + "chat.contacts": "Kontakter", + "chat.message-history": "Meldingshistorikk", + "chat.message-deleted": "Melding slettet", + "chat.options": "Alternativer for chatt", + "chat.pop-out": "Pop-out chatt", + "chat.minimize": "Mimimer", + "chat.maximize": "Maksimer", + "chat.seven_days": "7 dager", + "chat.thirty_days": "30 dager", + "chat.three_months": "3 måneder", + "chat.delete_message_confirm": "Er du sikker på at du vil slette denne meldingen?", + "chat.retrieving-users": "Henter brukere ...", + "chat.manage-room": "Manage Chat Room", + "chat.add-user-help": "Søk etter brukere her. Når dette er valgt, blir brukeren lagt til i chatten. Den nye brukeren vil ikke kunne se chatmeldinger skrevet før de ble lagt til i samtalen. Bare romeiere () kan fjerne brukere fra chatterom.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Komponer", + "composer.show_preview": "Vis forhåndsvisning", + "composer.hide_preview": "Skjul forhåndsvisning", + "composer.user_said_in": "%1 sa i %2: ", + "composer.user_said": "%1 sa: ", + "composer.discard": "Er du sikker på at du vil forkaste dette innlegget?", + "composer.submit_and_lock": "Send og lås", + "composer.toggle_dropdown": "Veksle nedtrekksfelt", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Velg en kategori", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Angitt dato", + "bootbox.ok": "OK", + "bootbox.cancel": "Avbryt", + "bootbox.confirm": "Bekreft", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Legg til miniatyrbilde", + "thumbs.modal.remove": "Fjern miniatyrbilde ", + "thumbs.modal.confirm-remove": "Er du sikker på at du vil fjerne dette miniatyrbilde? " +} \ No newline at end of file diff --git a/public/language/nb/notifications.json b/public/language/nb/notifications.json new file mode 100644 index 0000000000..9ae49a8f93 --- /dev/null +++ b/public/language/nb/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Varsler", + "no_notifs": "Du har ingen nye varsler", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Tilbake til %1", + "outgoing_link": "Utgående link", + "outgoing_link_message": "Du forlater nå %1", + "continue_to": "Fortsett til %1", + "return_to": "Gå tilbake til %1", + "new_notification": "Du har en ny varsling", + "you_have_unread_notifications": "Du har uleste varsler.", + "all": "Alle", + "topics": "Emner", + "replies": "Svar", + "chat": "Samtaler", + "group-chat": "Gruppesamtaler", + "follows": "Følger", + "upvote": "Oppstemmer", + "new-flags": "Nye flagg", + "my-flags": "Flagg som er tildelt til meg", + "bans": "Forbud", + "new_message_from": "Ny melding fra %1", + "upvoted_your_post_in": "%1 har stemt opp innlegget ditt i %2.", + "upvoted_your_post_in_dual": "%1 og 2% har stemt opp innlegget ditt i %3.", + "upvoted_your_post_in_multiple": "%1 og 2% har stemt opp innlegget ditt i %3.", + "moved_your_post": "%1 har flyttet innlegget ditt til %2.", + "moved_your_topic": "%1 har flyttet %2", + "user_flagged_post_in": "%1 har flagget et innlegg i %2", + "user_flagged_post_in_dual": "%1 og %2 flagget et innlegg i %3", + "user_flagged_post_in_multiple": "%1 og %2 har flagget et innlegg i %3", + "user_flagged_user": "%1 flagget en brukerprofil (%2)", + "user_flagged_user_dual": "%1 og 2% har flagget en brukerprofil (%3)", + "user_flagged_user_multiple": "%1 og %2 andre flagget en brukerprofil (%3)", + "user_posted_to": "%1 har skrevet et svar til: %2", + "user_posted_to_dual": "%1 og 2% har svart på innlegget ditt i %3.", + "user_posted_to_multiple": "%1 og 2% andre har svart på %3", + "user_posted_topic": "%1 har skrevet en ny tråd: %2", + "user_edited_post": "%1 har redigert ett innlegg i %2", + "user_started_following_you": "%1 begynte å følge deg.", + "user_started_following_you_dual": "%1 og 2% har begynt å følge deg. ", + "user_started_following_you_multiple": "%1 og %2 andre begynte å følge deg.", + "new_register": "%1 sendte en forespørsel om registrering", + "new_register_multiple": " Det er %1 registreringsforespørsler som venter på deg.", + "flag_assigned_to_you": "Flag %1 har blitt tildelt deg", + "post_awaiting_review": "Innlegg avventer anmeldelse", + "profile-exported": "%1 profil eksportert, klikk for å laste ned", + "posts-exported": "%1 innlegg eksportert, klikk for å laste ned", + "uploads-exported": "%1 opplastninger eksportert, klikk for å laste ned", + "users-csv-exported": "Bruker csv eksportert, klikk for å laste ned", + "post-queue-accepted": "Innlegget ditt i køen er godtatt. Klikk her for å se innlegget ditt.", + "post-queue-rejected": "Innlegget dit i køen har blitt avvist", + "post-queue-notify": "Varsel mottatt for innlegg i kø:
\"%1\"", + "email-confirmed": "E-post bekreftet", + "email-confirmed-message": "Takk for at du har validert din e-post. Kontoen din er nå fullstendig aktivert.", + "email-confirm-error-message": "Det oppsto et problem under validering av e-posten din. Koden kan ha vært ugyldig eller ha utløpt.", + "email-confirm-sent": "Bekreftelses-e-post sendt.", + "none": "Ingen", + "notification_only": "Kun notifikasjon ", + "email_only": "Kun e-post", + "notification_and_email": "Notifikasjon og e-post ", + "notificationType_upvote": "Når noen stemmer opp innlegget ditt", + "notificationType_new-topic": "Når noen du følger følger legger ut et emne", + "notificationType_new-reply": "Når et nytt svar er lagt ut i et emne du overvåker", + "notificationType_post-edit": "Når et innlegg er redigert i et emne du overvåker", + "notificationType_follow": "Når noen starter å følge deg", + "notificationType_new-chat": "Når du mottar en melding i chatt", + "notificationType_new-group-chat": "Når du mottar en gruppemelding i chatt", + "notificationType_group-invite": "Når du får tilsendt en gruppeinvitasjon ", + "notificationType_group-leave": "Når en bruker forlater gruppen din", + "notificationType_group-request-membership": "Når noen sender en forespørsel om å bli med i en gruppe du eier", + "notificationType_new-register": "Når noen blir lag til i kø for å registrere", + "notificationType_post-queue": "Når et nytt innlegg er satt i kø ", + "notificationType_new-post-flag": "Når ett nytt innlegg er flagget", + "notificationType_new-user-flag": "Når en bruker er flagget" +} \ No newline at end of file diff --git a/public/language/nb/pages.json b/public/language/nb/pages.json new file mode 100644 index 0000000000..e44c00e42d --- /dev/null +++ b/public/language/nb/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Hjem", + "unread": "Uleste emner", + "popular-day": "Populære emner i dag", + "popular-week": "Populære emner denne uken", + "popular-month": "Populære emner denne måneden", + "popular-alltime": "Mest populære emner for all tid", + "recent": "Nylige emner", + "top-day": "Dagens emne med fleste stemmer", + "top-week": "Emne med flest stemmer denne uken", + "top-month": "Emne med flest stemme denne måneden ", + "top-alltime": "Emner med flest stemmer", + "moderator-tools": "Moderatorverktøy", + "flagged-content": "Flagget innhold", + "ip-blacklist": "IP Svarteliste", + "post-queue": "Innleggskø", + "users/online": "Påloggede Brukere", + "users/latest": "Nyeste Brukere", + "users/sort-posts": "Brukere med flest innlegg", + "users/sort-reputation": "Brukere med best omdømme", + "users/banned": "Utestengte brukere", + "users/most-flags": "Brukere som er mest flagget ", + "users/search": "Brukersøk", + "notifications": "Varsler", + "tags": "Emneord", + "tag": "Tråder tagget under "%1"", + "register": "Registrer en konto", + "registration-complete": "Registrering er fullført", + "login": "Logg inn på kontoen din", + "reset": "Tilbakestill passordet ditt", + "categories": "Kategorier", + "groups": "Grupper", + "group": "%1 gruppe", + "chats": "Samtaler", + "chat": "Samtale med %1", + "flags": "Flagg", + "flag-details": "Flagg %1 Detaljer", + "account/edit": "Endrer \"%1\"", + "account/edit/password": "Redigeringspassord for \"%1\"", + "account/edit/username": "Rediger brukernavnet til \"%1\"", + "account/edit/email": "Rediger e-post for \"%1\"", + "account/info": "Informasjon om brukerkonto ", + "account/following": "Personer %1 følger", + "account/followers": "Personer som følger %1", + "account/posts": "Innlegg opprettet av %1", + "account/latest-posts": "Seneste innlegg skrevet av %1", + "account/topics": "Emner opprettet av %1", + "account/groups": "%1 sine grupper", + "account/watched_categories": "%1's overvåkede kategorier", + "account/bookmarks": "%1's bokmerkede innlegg", + "account/settings": "Brukerinnstillinger", + "account/watched": "Innlegg overvåket av %1", + "account/ignored": "Emner ignorert av %1", + "account/upvoted": "Innlegg stemt opp av %1", + "account/downvoted": "Innlegg nedstemt av %1", + "account/best": "Beste innlegg skrevet av %1", + "account/controversial": "Kontroversielle innlegg skrevet av %1", + "account/blocks": "Blokkerte brukere for %1", + "account/uploads": "Opplastninger av %1", + "account/sessions": "Påloggingsøkter", + "confirm": "E-post bekreftet", + "maintenance.text": "%1 er for tiden under vedlikehold. Kom tilbake en annen gang.", + "maintenance.messageIntro": "I tillegg har administratoren skrevet denne meldingen:", + "throttled.text": "%1 er for øyeblikket ikke tilgjengelig på grunn av overdreven belastning. Kom tilbake en annen gang." +} \ No newline at end of file diff --git a/public/language/nb/post-queue.json b/public/language/nb/post-queue.json new file mode 100644 index 0000000000..ce7088fa41 --- /dev/null +++ b/public/language/nb/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Innleggskø", + "description": "Det er ingen innlegg i innleggskøen.
For å skru på denne funksjonen, gå til Innstillinger → Innlegg → Innleggskø og skru på Innleggskø.", + "user": "Bruker", + "category": "Kategori", + "title": "Tittel", + "content": "Innhold", + "posted": "Postet", + "reply-to": "Svar til \"%1\"", + "content-editable": "Klikk på innhold for å redigere", + "category-editable": "Klikk på kategori for å redigere", + "title-editable": "Klikk på tittel for å redigere ", + "reply": "Svare", + "topic": "Emne", + "accept": "Aksepter ", + "reject": "Avvis", + "remove": "Fjern", + "notify": "Varsle", + "notify-user": "Varsle bruker", + "confirm-reject": "Vil du avvise dette innlegget?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/nb/recent.json b/public/language/nb/recent.json new file mode 100644 index 0000000000..a4a6128247 --- /dev/null +++ b/public/language/nb/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Nylige", + "day": "Dag", + "week": "Uke", + "month": "Måned", + "year": "År", + "alltime": "All tid", + "no_recent_topics": "Det er ingen nye emner.", + "no_popular_topics": "Det er ingen populære emner.", + "there-is-a-new-topic": "Det er et nytt emne.", + "there-is-a-new-topic-and-a-new-post": "Det er et nytt emne og et nytt innlegg.", + "there-is-a-new-topic-and-new-posts": "Det er et nytt emne og %1 nye innlegg.", + "there-are-new-topics": "Det er %1 nye emner.", + "there-are-new-topics-and-a-new-post": "Det er %1 nye emner og et nytt innlegg.", + "there-are-new-topics-and-new-posts": "Det er %1 nye emner og %2 nye innlegg", + "there-is-a-new-post": "Det er et nytt innlegg.", + "there-are-new-posts": "Det er %1 nye innlegg.", + "click-here-to-reload": "Trykk her for å laste på nytt." +} \ No newline at end of file diff --git a/public/language/nb/register.json b/public/language/nb/register.json new file mode 100644 index 0000000000..55ed85a0e9 --- /dev/null +++ b/public/language/nb/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registrer", + "cancel_registration": "Avbryt registrering ", + "help.email": "Som standard, holdes din e-post skjult for offentligheten.", + "help.username_restrictions": "Et unikt brukernavn mellom %1 og %2 tegn. Andre kan nevne deg med @brukernavn.", + "help.minimum_password_length": "Ditt passord må være minst %1 tegn.", + "email_address": "E-postadresse", + "email_address_placeholder": "Skriv e-postadresse", + "username": "Brukernavn", + "username_placeholder": "Skriv brukernavn", + "password": "Passord", + "password_placeholder": "Skriv passord", + "confirm_password": "Bekreft passord", + "confirm_password_placeholder": "Bekreft passord", + "register_now_button": "Registrer nå", + "alternative_registration": "Alternativ registrering", + "terms_of_use": "Vilkårene for bruk", + "agree_to_terms_of_use": "Jeg godtar vilkårene for bruk", + "terms_of_use_error": "Du må godta vilkårene for bruk", + "registration-added-to-queue": "Din registrering har blitt lagt til i godkjenningskøen. Du vil motta en e-post når denne blir akseptert av en administrator.", + "registration-queue-average-time": "Gjennomsnittlig tid for godkjenning av medlemskap er %1 timer %2 minutter.", + "registration-queue-auto-approve-time": "Ditt medlemskap i dette forumet vil være fullt aktivert på opptil %1 timer.", + "interstitial.intro": "Vi ønsker ytterligere informasjon for å oppdatere din brukerkonto…", + "interstitial.intro-new": "Vi ønsker ytterligere informasjon før vi kan opprette din brukerkonto…", + "interstitial.errors-found": "Vennligst gå igjennom oppgitt informasjon:", + "gdpr_agree_data": "Jeg samtykker til innsamling og behandling av min personlige informasjon på dette nettstedet.", + "gdpr_agree_email": "Jeg samtykker i å motta forumsammendrag og varsler på e-postmeldinger fra dette nettstedet. ", + "gdpr_consent_denied": "Du må gi samtykke til at dette nettstedet kan samle inn/behandle informasjonen din, og sende deg e-post.", + "invite.error-admin-only": "Direkte brukerregistrering er deaktivert. Kontakt en administrator for mer informasjon. ", + "invite.error-invite-only": "Direkte brukerregistrering er deaktivert. Du må bli invitert av en eksisterende bruker for å få tilgang til dette forumet. ", + "invite.error-invalid-data": "Registrerte data som mottas samsvarer ikke med registrene våre. Kontakt en administrator for mer informasjon." +} \ No newline at end of file diff --git a/public/language/nb/reset_password.json b/public/language/nb/reset_password.json new file mode 100644 index 0000000000..cbc576f70b --- /dev/null +++ b/public/language/nb/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Tilbakestill passord", + "update_password": "Oppdater passord", + "password_changed.title": "Passordet ble endret", + "password_changed.message": "

Passord ble tilbakestilt, vennligst logg inn igjen", + "wrong_reset_code.title": "Feil tilbakestillingskode", + "wrong_reset_code.message": "Tilbakestillingskoden mottatt var feil. Vennligst prøv igjen, eller be om en ny tilbakestillingskode.", + "new_password": "Nytt passord", + "repeat_password": "Bekreft passord", + "changing_password": "Endrer passord", + "enter_email": "Vennligst skriv inn e-post-adressen din, så sender vi en e-post med instruksjoner om hvordan du tilbakestiller kontoen din.", + "enter_email_address": "Skriv e-postadresse", + "password_reset_sent": "Hvis den spesifiserte e-postadressen hører til en eksisterende konto blir en e-post med instruksjoner for gjenoppretting av passord sendt. Merk at kun en e-post vil bli sendt ut per minutt.", + "invalid_email": "Ugyldig e-post / e-post eksisterer ikke", + "password_too_short": "Passordet du skrev inn er for kort, vennligst velg et lengre passord.", + "passwords_do_not_match": "Passordene du har skrevet inn samsvarer ikke.", + "password_expired": "Passordet ditt har utløpt, vennligst velg et nytt passord" +} \ No newline at end of file diff --git a/public/language/nb/search.json b/public/language/nb/search.json new file mode 100644 index 0000000000..ad688770e3 --- /dev/null +++ b/public/language/nb/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 resultat(er) samsvarer med \"%2\", (%3 sekunder)", + "no-matches": "Ingen matcher funnet", + "advanced-search": "Avansert søk", + "in": "I", + "titles": "Titler", + "titles-posts": "Titler og innlegg", + "match-words": "Match ord", + "all": "Alle", + "any": "Hvilken som helst", + "posted-by": "Skapt av", + "in-categories": "I kategorier", + "search-child-categories": "Søk underkategorier", + "has-tags": "Har emneord", + "reply-count": "Mengde svar", + "at-least": "Minst", + "at-most": "Maks", + "relevance": "Relevanse", + "post-time": "Innlegg-tid", + "votes": "Stemmer", + "newer-than": "Nyere enn", + "older-than": "Eldre en", + "any-date": "Alle datoer", + "yesterday": "I går", + "one-week": "En uke", + "two-weeks": "To uker", + "one-month": "En måned ", + "three-months": "Tre måneder", + "six-months": "Seks måneder", + "one-year": "Ett år", + "sort-by": "Sorter etter", + "last-reply-time": "Sise svartid", + "topic-title": "Tråd-tittel", + "topic-votes": "Stemmer på emne", + "number-of-replies": "Antall svar", + "number-of-views": "Antall visninger", + "topic-start-date": "Starttid for tråd", + "username": "Brukernavn", + "category": "Kategori", + "descending": "I synkende rekkefølge", + "ascending": "I stigende rekkefølge", + "save-preferences": "Lagre innstillinger", + "clear-preferences": "Tøm innstillinnger", + "search-preferences-saved": "Søkeinnstillinger lagret", + "search-preferences-cleared": "Søkeinnstillinger tømt", + "show-results-as": "Vis resultater som", + "see-more-results": "Se flere resultater (%1)", + "search-in-category": "Søk i \"%1\"" +} \ No newline at end of file diff --git a/public/language/nb/success.json b/public/language/nb/success.json new file mode 100644 index 0000000000..13fa3b4f37 --- /dev/null +++ b/public/language/nb/success.json @@ -0,0 +1,7 @@ +{ + "success": "Suksess", + "topic-post": "Du har nå publisert.", + "post-queued": "Innlegget ditt er satt i kø for godkjenning. Du vil få en melding når den har blitt godkjent eller avvist. ", + "authentication-successful": "Innlogging vellykket!", + "settings-saved": "Innstillinger lagret!" +} \ No newline at end of file diff --git a/public/language/nb/tags.json b/public/language/nb/tags.json new file mode 100644 index 0000000000..6d6e768270 --- /dev/null +++ b/public/language/nb/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Det er ingen emner med dette emneordet.", + "tags": "Emneord", + "enter_tags_here": "Skriv emneord her, mellom %1 og %2 tegn hver.", + "enter_tags_here_short": "Skriv emneord...", + "no_tags": "Det finnes ingen emneord enda.", + "select_tags": "Velg kode" +} \ No newline at end of file diff --git a/public/language/nb/top.json b/public/language/nb/top.json new file mode 100644 index 0000000000..bb11307506 --- /dev/null +++ b/public/language/nb/top.json @@ -0,0 +1,4 @@ +{ + "title": "Topp", + "no_top_topics": "Ingen toppemner" +} \ No newline at end of file diff --git a/public/language/nb/topic.json b/public/language/nb/topic.json new file mode 100644 index 0000000000..2332377e51 --- /dev/null +++ b/public/language/nb/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Emne", + "title": "Tittel", + "no_topics_found": "Ingen tråder funnet!", + "no_posts_found": "Ingen innlegg funnet!", + "post_is_deleted": "Dette innlegget er slettet!", + "topic_is_deleted": "Denne tråden er slettet!", + "profile": "Profil", + "posted_by": "Skapt av %1", + "posted_by_guest": "Skapt av Gjest", + "chat": "Chat", + "notify_me": "Bli varslet om nye svar i denne tråden", + "quote": "Siter", + "reply": "Svar", + "replies_to_this_post": "%1 Svar", + "one_reply_to_this_post": "1 Svar", + "last_reply_time": "Siste svar", + "reply-as-topic": "Svar som tråd", + "guest-login-reply": "Logg inn for å besvare", + "login-to-view": "🔒 Logg inn for å se", + "edit": "Endre", + "delete": "Slett", + "delete-event": "Slett hendelse", + "delete-event-confirm": "Er du sikker på at du vil slette denne hendelsen?", + "purge": "Rensk", + "restore": "Gjenopprett", + "move": "Flytt", + "change-owner": "Bytt eier", + "fork": "Forgren", + "link": "Link", + "share": "Del", + "tools": "Verktøy", + "locked": "Låst", + "pinned": "Festet", + "pinned-with-expiry": "Festet til %1", + "scheduled": "Planlagt", + "moved": "Flyttet", + "moved-from": "Flyttet fra %1", + "copy-ip": "Kopier IP", + "ban-ip": "Forby IP", + "view-history": "Redigere historie", + "locked-by": "Låst av", + "unlocked-by": "Låst opp av", + "pinned-by": "Festet av", + "unpinned-by": "Løsnet av", + "deleted-by": "Slettet av", + "restored-by": "Gjenopprettet av", + "moved-from-by": "Flyttet fra %1 av", + "queued-by": "Innlegg i kø for godkjenning & rarr;", + "backlink": "Henvinst til av", + "forked-by": "Forgrenet av", + "bookmark_instructions": "Klikk her for å gå tilbake til det siste innlegget i denne tråden.", + "flag-post": "Flagg denne posten", + "flag-user": "Flagg denne brukeren", + "already-flagged": "Allerede flagget", + "view-flag-report": "Vis flaggrapport ", + "resolve-flag": "Løs flagg", + "merged_message": "Dette emnet er slått sammen med %2", + "deleted_message": "Denne tråden har blitt slettet. Bare brukere med trådhåndterings-privilegier kan se den.", + "following_topic.message": "Du vil nå motta varsler når noen skriver i denne tråden.", + "not_following_topic.message": "Du vil se denne tråden i trådlisten, men du vil ikke motta varslinger når noen skriver i den.", + "ignoring_topic.message": "Du vil ikke lenger se denne tråden blandt de uleste trådene. Du vil få et varsel når du blir nevnt eller din tråd blir oppstemt.", + "login_to_subscribe": "Vennligst registrer deg eller logg inn for å abonnere på denne tråden.", + "markAsUnreadForAll.success": "Tråd markert som ulest for alle.", + "mark_unread": "Merk som ulest", + "mark_unread.success": "Tråd merket som ulest.", + "watch": "Overvåk", + "unwatch": "Ikke overvåk", + "watch.title": "Bli varslet om nye svar i denne tråden", + "unwatch.title": "Slutt å følge denne tråden", + "share_this_post": "Del ditt innlegg", + "watching": "Overvåker", + "not-watching": "Overvåker ikke", + "ignoring": "Ignorerer", + "watching.description": "Varlse meg om nye svar.
Vis tråd i ulest.", + "not-watching.description": "Ikke varsle meg om nye svar.
Vis tråd i ulest hvis ikke kategori er ignorert.", + "ignoring.description": "Ikke varsle meg om nye svar.
Ikke vis tråd i ulest.", + "thread_tools.title": "Trådverktøy", + "thread_tools.markAsUnreadForAll": "Merk som ulest for alle", + "thread_tools.pin": "Fest tråd", + "thread_tools.unpin": "Ufest tråd", + "thread_tools.lock": "Lås tråd", + "thread_tools.unlock": "Lås opp tråd", + "thread_tools.move": "Flytt tråd", + "thread_tools.move-posts": "Flytt innlegg", + "thread_tools.move_all": "Flytt alle", + "thread_tools.change_owner": "Bytt eier", + "thread_tools.select_category": "Velg kategori", + "thread_tools.fork": "Forgren tråd", + "thread_tools.delete": "Slett tråd", + "thread_tools.delete-posts": "Slett innlegg", + "thread_tools.delete_confirm": "Er du sikker på at du vil slette denne tråden?", + "thread_tools.restore": "Gjenopprett tråd", + "thread_tools.restore_confirm": "Er du sikker på at du vil gjenopprette denne tråden?", + "thread_tools.purge": "Rensk tråd", + "thread_tools.purge_confirm": "Er du sikker på at du vil renske denne tråden?", + "thread_tools.merge_topics": "Flett emner", + "thread_tools.merge": "Slå sammen", + "topic_move_success": "Denne tråden vil straks bli flyttet til \"%1\". Klikk her for å angre.", + "topic_move_multiple_success": "Disse emnene vil straks bli flyttet til \"%1\". Klikk her for å angre.", + "topic_move_all_success": "Alle emner vil straks bli flyttet til \"%1\". Klikk her for å angre.", + "topic_move_undone": "Flytting av emne angret", + "topic_move_posts_success": "Innlegg flyttes om kort tid. Klikk her for å angre.", + "topic_move_posts_undone": "Flytting av innlegg angret", + "post_delete_confirm": "Er du sikker på at du vil slette dette innlegget?", + "post_restore_confirm": "Er du sikker på at du vil gjenopprette dette innlegget?", + "post_purge_confirm": "Er du sikker på at du vil renske dette innlegget?", + "pin-modal-expiry": "Utløpsdato", + "pin-modal-help": "Du kan eventuelt angi en utløpsdato for de festede emnene her. Alternativt kan du la dette feltet stå tomt for å holde emnet festet til det manuelt løsnes.", + "load_categories": "Laster kategorier", + "confirm_move": "Flytt", + "confirm_fork": "Forgren", + "bookmark": "Bokmerke", + "bookmarks": "Bokmerker", + "bookmarks.has_no_bookmarks": "Du har ikke bokmerket noen innlegg ennå.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Laster flere innlegg", + "move_topic": "Flytt tård", + "move_topics": "Flytt tråder", + "move_post": "Flytt innlegg", + "post_moved": "Innlegg flyttet!", + "fork_topic": "Forgren tråd", + "enter-new-topic-title": "Tast inn tittel på emne", + "fork_topic_instruction": "Trykk på innleggene du vil forgrene", + "fork_no_pids": "Ingen innlegg valgt!", + "no-posts-selected": "Ingen innlegg valgt.", + "x-posts-selected": "%1 innlegg valgt", + "x-posts-will-be-moved-to-y": "%1 innlegg(ene) vil bli flyttet til \"%2\"", + "fork_pid_count": "%1 innlegg valgt", + "fork_success": "Denne tråden ble forgrenet! Klikk for å gå til forgrenet tråd.", + "delete_posts_instruction": "Klikk på innleggene du ønsker å slette/rense", + "merge_topics_instruction": "Klikk på emnene du du ønsker å slå sammen eller søk på dem", + "merge-topic-list-title": "Liste over emner som skal slås sammen", + "merge-options": "Slå sammen alternativer", + "merge-select-main-topic": "Velg hovedemne", + "merge-new-title-for-topic": "Ny tittel for emne", + "topic-id": "Emne ID", + "move_posts_instruction": "Klikk på innleggene du vil flytte, og skriv deretter inn en emne-ID, eller gå til målemnet", + "change_owner_instruction": "Klikk på innleggene du vil tildele til en annen bruker", + "composer.title_placeholder": "Skriv din tråd-tittel her", + "composer.handle_placeholder": "Skriv inn navnet ditt / signatur her", + "composer.discard": "Forkast", + "composer.submit": "Send", + "composer.additional-options": "Ytterligere alternativer", + "composer.schedule": "Timeplan", + "composer.replying_to": "Svarer i %1", + "composer.new_topic": "Ny tråd", + "composer.editing": "Redigering", + "composer.uploading": "laster opp...", + "composer.thumb_url_label": "Lim inn som tråd-minatyr URL", + "composer.thumb_title": "Legg til minatyr til denne tråden", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Eller last opp en fil", + "composer.thumb_remove": "Tøm felter", + "composer.drag_and_drop_images": "Dra og slipp bilder her", + "more_users_and_guests": "%1 flere bruker(e) og %2 gjest(er)", + "more_users": "%1 flere bruker(e)", + "more_guests": "%1 flere bruker(e)", + "users_and_others": "%1 og %2 andre", + "sort_by": "Sorter etter", + "oldest_to_newest": "Eldste til nyeste", + "newest_to_oldest": "Nyeste til eldste", + "most_votes": "Flest stemmer", + "most_posts": "Flest innlegg", + "most_views": "Flest visninger", + "stale.title": "Lag en ny tråd i stedet?", + "stale.warning": "Tråden du svarer på er ganske gammel. Vil du heller lage en ny tråd og refere til denne i den?", + "stale.create": "Lag en ny tråd", + "stale.reply_anyway": "Svar på denne tråden allikevel", + "link_back": "Sv: [%1](%2)", + "diffs.title": "Redigeringshistorikk for innlegg", + "diffs.description": "Dette innlegget har %1 redigeringer. Klikk på en av revisjonene nedenfor for å se innholdet på innlegget på det tidspunktet.", + "diffs.no-revisions-description": "Denne posten har %1 redigeringer.", + "diffs.current-revision": "Nåværende redigering ", + "diffs.original-revision": "Orginalversjon", + "diffs.restore": "Gjenopprett denne versjonen ", + "diffs.restore-description": "En ny revisjon vil bli lagt til dette innleggets redigeringshistorikk etter gjenoppretting.", + "diffs.post-restored": "Innlegget ble vellykket gjenopprettet til tidligere revisjon", + "diffs.delete": "Slett denne versjonen", + "diffs.deleted": "Versjon slettet", + "timeago_later": "%1 senere", + "timeago_earlier": "%1 tidligere", + "first-post": "Første innlegg", + "last-post": "Seneste innlegg", + "go-to-my-next-post": "Gå til mitt neste innlegg", + "no-more-next-post": "Du har ikke flere innlegg i dette emnet", + "post-quick-reply": "Skriv hurtigsvar" +} \ No newline at end of file diff --git a/public/language/nb/unread.json b/public/language/nb/unread.json new file mode 100644 index 0000000000..7917180862 --- /dev/null +++ b/public/language/nb/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Uleste", + "no_unread_topics": "Det er ingen uleste emner.", + "load_more": "Last inn mer", + "mark_as_read": "Marker som lest", + "selected": "Valgte", + "all": "Alle", + "all_categories": "Alle kategorier", + "topics_marked_as_read.success": "Emner merket som lest!", + "all-topics": "Alle emner", + "new-topics": "Nye emner", + "watched-topics": "Fulgte emner", + "unreplied-topics": "Emner som ikke er svart på", + "multiple-categories-selected": "Flere valg" +} \ No newline at end of file diff --git a/public/language/nb/uploads.json b/public/language/nb/uploads.json new file mode 100644 index 0000000000..283e0120dd --- /dev/null +++ b/public/language/nb/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Laster opp filen...", + "select-file-to-upload": "Velg en fil å laste opp!", + "upload-success": "Filen ble suksessfullt lastet opp!", + "maximum-file-size": "Maksimum %1 kb", + "no-uploads-found": "Ingen opplastninger funnet", + "public-uploads-info": "Opplastninger er offentlige, alle besøkende kan se dem. ", + "private-uploads-info": "Opplastninger er private, kun innloggede brukere kan se dem. " +} \ No newline at end of file diff --git a/public/language/nb/user.json b/public/language/nb/user.json new file mode 100644 index 0000000000..4a2782610f --- /dev/null +++ b/public/language/nb/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Utestengt", + "muted": "Muted", + "offline": "Frakoblet", + "deleted": "Slettet", + "username": "Brukernavn", + "joindate": "Registereringsdato", + "postcount": "Antall innlegg", + "email": "E-post", + "confirm_email": "Bekreft e-post", + "account_info": "Kontoinformasjon", + "admin_actions_label": "Administrative handlinger ", + "ban_account": "Utesteng kont", + "ban_account_confirm": "Vil du virkelig utestenge denne brukeren?", + "unban_account": "Opphev utestenging", + "mute_account": "Kneble konto", + "unmute_account": "Stopp å kneble konto ", + "delete_account": "Slett konto", + "delete_account_as_admin": "Slett Brukerkonto", + "delete_content": "Slett brukerkonto Innhold", + "delete_all": "Slett Brukerkonto og Innhold", + "delete_account_confirm": "Er du sikker på at du vil anonymisere alle innleggene dine og slette brukerkonten?
Denne handlingen er irreversibel, og du vil ikke kunne gjenopprette noen av dataene dine

Skriv inn passordet ditt for å bekrefte at du ønsker å slette denne brukerkontoen.", + "delete_this_account_confirm": "Er du sikker på at du vil slette denne brukerkontoen og la innholdet ligge igjen?
Denne handlingen er irreversibel, innlegg blir anonymisert, og du vil ikke kunne gjenopprette innleggsassosiasjoner med den slettede kontoen

", + "delete_account_content_confirm": "Er du sikker på at du vil slette innholdet på denne brukerkontoen (innlegg / emner / opplastinger)?
Denne handlingen er irreversibel, og du vil ikke kunne gjenopprette data

", + "delete_all_confirm": "
Er du sikker på at du vil slette denne kontoen og alt innholdet (innlegg / emner / opplastinger)?Denne handlingen er irreversibel, og du vil ikke kunne gjenopprette data\n

", + "account-deleted": "Konto slettet", + "account-content-deleted": "Kontoinnhold slettet", + "fullname": "Fullt navn", + "website": "Nettsted", + "location": "Plassering", + "age": "Alder", + "joined": "Ble med", + "lastonline": "Sist tilkoblet", + "profile": "Profil", + "profile_views": "Profilvisninger", + "reputation": "Omdømme", + "bookmarks": "Bokmerker", + "watched_categories": "Overvåkede kategorier", + "change_all": "Endre alt", + "watched": "Overvåkede", + "ignored": "Ignorert", + "default-category-watch-state": "Standard kategori overvåkingstilstand", + "followers": "Følgere", + "following": "Følger", + "blocks": "Blokkeringer", + "block_toggle": "Toggle Block", + "block_user": "Blokker bruker", + "unblock_user": "Opphev blokkering av bruker", + "aboutme": "Om meg", + "signature": "Signatur", + "birthday": "Bursdag", + "chat": "Chat", + "chat_with": "Fortsett å chatte med %1", + "new_chat_with": "Start ny chatt med %1", + "flag-profile": "Flagg profil", + "follow": "Følg", + "unfollow": "Avfølg", + "more": "Mer", + "profile_update_success": "Profilen ble oppdatert!", + "change_picture": "Bytt bilde", + "change_username": "Endre brukernavn", + "change_email": "Endre e-post", + "email_same_as_password": "Skriv inn ditt nåværende passord for å fortsette – du har skrevet inn den nye e-posten din igjen", + "edit": "Endre", + "edit-profile": "Rediger profil", + "default_picture": "Standardikonet", + "uploaded_picture": "Opplastet bilde", + "upload_new_picture": "Last opp nytt bidle", + "upload_new_picture_from_url": "Last opp nytt bilde fra URL", + "current_password": "Gjeldende passord", + "change_password": "Endre passord", + "change_password_error": "Ugyldig passord!", + "change_password_error_wrong_current": "Ditt gjeldende passord er ikke korrekt!", + "change_password_error_match": "Passordene må samsvare!", + "change_password_error_privileges": "Du har ikke rettigheter tli å endre dette passordet.", + "change_password_success": "Passordet ditt ble oppdatert!", + "confirm_password": "Bekreft passord", + "password": "Passord", + "username_taken_workaround": "Brukernavnet du ønsket er opptatt, så vi har endret det litt. Du er nå kjent som %1", + "password_same_as_username": "Passordet ditt er det samme som brukernavnet ditt. Velg et annet passord.", + "password_same_as_email": "Passordet ditt er det samme som e-postadressen din. Velg et annet passord.", + "weak_password": "Svakt passord", + "upload_picture": "Last opp bilde", + "upload_a_picture": "Last opp et bilde", + "remove_uploaded_picture": "Fjern Opplastet Bilde", + "upload_cover_picture": "Last opp bakgrunnsbilde", + "remove_cover_picture_confirm": "Er du sikker på at du vil fjerne bakgrunnsbilde?", + "crop_picture": "Beskjær bilde", + "upload_cropped_picture": "Beskjær og last opp", + "avatar-background-colour": "Avatar bakgrunnsfarge", + "settings": "Innstillinger", + "show_email": "Vis min e-post", + "show_fullname": "Vis mitt fulle navn", + "restrict_chats": "Bare tillat chat-meldinger fra brukere jeg følger", + "digest_label": "Abonner på sammendrag", + "digest_description": "Abonner på e-post-oppdateringer for dette forumet (nye varsler og emner) i samsvar med valgte tidspunkt", + "digest_off": "Av", + "digest_daily": "Daglig", + "digest_weekly": "Ukentlig", + "digest_biweekly": "Annenhver uke", + "digest_monthly": "Månedlig", + "has_no_follower": "Denne brukeren har ingen følgere :(", + "follows_no_one": "Denne brukeren følger ingen :(", + "has_no_posts": "Denne brukeren har ikke skrevet noe enda.", + "has_no_best_posts": "Denne brukeren har ingen opp-stemte innlegg ennå.", + "has_no_topics": "Denne brukeren har ikke skrevet noen tråder enda.", + "has_no_watched_topics": "Denne brukeren har ikke fulgt noen tråder enda.", + "has_no_ignored_topics": "Denne brukeren har ikke ignorert noen emner ennå", + "has_no_upvoted_posts": "Denne brukeren har ikke stemt opp noen innlegg ennå.", + "has_no_downvoted_posts": "Denne brukeren har ikke stemt ned noen innlegg ennå.", + "has_no_controversial_posts": "Denne brukeren har ikke noen nedstemte innlegg ennå.", + "has_no_blocks": "Du har ingen blokkerte brukere.", + "email_hidden": "E-post skjult", + "hidden": "skjult", + "paginate_description": "Bruk sidevelger for tråder og innlegg istedet for uendelig scrolling", + "topics_per_page": "Tråd per side", + "posts_per_page": "Innlegg per side", + "max_items_per_page": "Maksimum %1", + "acp_language": "Administrer sidespråk", + "notifications": "Notifikasjoner", + "upvote-notif-freq": "Varslingsfrekvens for opp-stemmer", + "upvote-notif-freq.all": "Alle oppstemmer", + "upvote-notif-freq.first": "Først per innlegg", + "upvote-notif-freq.everyTen": "Hver tiende oppstemning", + "upvote-notif-freq.threshold": "På 1, 5, 10, 25, 50, 100, 150, 200 ...", + "upvote-notif-freq.logarithmic": "På 10, 100, 1000 ...", + "upvote-notif-freq.disabled": "Noe er galt med funksjonen", + "browsing": "Surfeinnstillinger", + "open_links_in_new_tab": "Åpne utgående lenker i en ny fane", + "enable_topic_searching": "Aktiver søk-i-tråd", + "topic_search_help": "Hvis søk-i-tård er aktivert, overstyres nettleserens standard sidesøk og gir mulighet til å søke gjennom hele tråden, ikke bare det som vises på skjermen", + "update_url_with_post_index": "Oppdater url med postindeks mens du surfer på emner", + "scroll_to_my_post": "Etter å ha postet et svar, vis det nye innlegget", + "follow_topics_you_reply_to": "Følg tråder du vil svare på", + "follow_topics_you_create": "Følg tråder du vil lage", + "grouptitle": "Gruppetittel", + "group-order-help": "Velg en gruppe og bruk pilene for å gi titler ", + "no-group-title": "Ingen gruppetittel", + "select-skin": "Velg et skin", + "select-homepage": "Velg en hjemmeside", + "homepage": "Hjemmeside", + "homepage_description": "Velg en side du vil bruke som forumets hjemmeside, eller 'Ingen' for å bruke standardhjemmesiden.", + "custom_route": "Tilpasset hjemmeside-rute", + "custom_route_help": "Skriv inn et rutenavn her uten noen forrige skråstrek (f.eks. \"Nylig\" eller \"kategori / 2 / generell diskusjon\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Assosiert med", + "sso.not-associated": "Klikk her for å assosiere med", + "sso.dissociate": "Separer", + "sso.dissociate-confirm-title": "Bekreft seperasjon", + "sso.dissociate-confirm": "Er du sikker på at du vil separere kontoen din fra %1?", + "info.latest-flags": "Seneste flagg", + "info.no-flags": "Ingen flaggede innlegg funnet", + "info.ban-history": "Nylig utestengingshistorikk", + "info.no-ban-history": "Denne brukeren har aldri blitt utestengt ", + "info.banned-until": "Utestengt til %1", + "info.banned-expiry": "Utløp", + "info.banned-permanently": "Utestengt permanent", + "info.banned-reason-label": "Årsak", + "info.banned-no-reason": "ingen årsak oppgitt", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "Ingen grunn oppgitt.", + "info.username-history": "Brukernavnhistorikk", + "info.email-history": "E-post-historikk", + "info.moderation-note": "Moderasjonsnotat ", + "info.moderation-note.success": "Moderasjonsnotat ikke lagret ", + "info.moderation-note.add": "Legg til notat", + "sessions.description": "Denne siden lar deg se alle aktivitetsøkter på dette forumet og tilbakekalle dem om nødvendig. Du kan tilbakekalle din egen økt ved å logge av brukerkontoen din.", + "consent.title": "Dine rettigheter & amp; Samtykke", + "consent.lead": "Dette forumet samler inn og behandler dine personopplysninger. ", + "consent.intro": "Vi bruker kun denne informasjonen for å tilpasse brukeropplevelsen din i dette nettforumet, og for å knytte innleggene du lager til brukerkontoen din. Under registreringstrinnet i Felles brukerhåndtering ble du bedt om å oppgi fullt navn og en e-postadresse. Du kan også velge å oppgi tilleggsinformasjon for å fullføre brukerprofilen din på dette nettstedet.

Vi oppbevarer denne informasjonen så lenge brukerprofilen din er aktiv, og du kan når som helst trekke tilbake samtykke ved å slette kontoen din. Du kan når som helst be om en kopi av ditt bidrag til dette nettstedet, via dine rettigheter & Samtykkeside.

Hvis du har spørsmål eller bekymringer, oppfordrer vi deg til å ta kontakt med forumets administrative team.", + "consent.email_intro": "Vi kan fra tid til annen sende deg en e-post til din registrerte e-postadresse for å varsle deg om oppdateringer og/eller informere deg om ny aktivitet som er relevant for deg. Du kan endre hvor ofte vi sender forumsammendrag (eller du kan slå det helt av), og endre hvilke typer oppdateringer du vil motta via brukerinnstillingene dine.", + "consent.digest_frequency": "Med mindre dette er eksplisitt endret i brukerinnstillingene dine, leverer dette fellesskapet e-postsammendrag hver %1.", + "consent.digest_off": "Med mindre dette er eksplisitt endret i brukerinnstillingene dine, sender ikke dette fellesskapet ut e-postsammendrag", + "consent.received": "Du har gitt samtykke til at dette nettstedet samler inn og behandler informasjonen din. Ingen ytterligere tiltak er nødvendig.", + "consent.not_received": "Du har ikke gitt samtykke til datainnsamling og behandling. Nettsidens administrasjon kan derfor velge å slette kontoen din når som helst, for å bli kompatibel med personvernforordningen. ", + "consent.give": "Gi samtykke", + "consent.right_of_access": "Du har rett til innsyn", + "consent.right_of_access_description": "På forespørsel har du har rett til å få tilgang til data som samles inn av dette nettstedet. Du kan hente en kopi av disse dataene ved å klikke på riktig knapp nedenfor.", + "consent.right_to_rectification": "Du har rett til å få rettet uriktige data ", + "consent.right_to_rectification_description": "Du har rett til å endre eller oppdatere uriktige data som er gitt til oss. Brukerprofilen din kan oppdateres ved å redigere profilen din, og innhold på innlegg kan alltid redigeres. Hvis dette ikke er tilfelle, kan du kontakte dette nettstedets administrasjonsteam.", + "consent.right_to_erasure": "Du har rett til sletting", + "consent.right_to_erasure_description": "Du kan når som helst tilbakekalle ditt samtykke til datainnsamling og/eller behandling ved å slette brukerkontoen din. Den individuelle profilen din kan slettes, selv om de publiserte innleggene dine blir værende igjen. Hvis du vil slette begge kontoer og alt innhold, kontakt administrasjonsteamet for dette nettstedet. ", + "consent.right_to_data_portability": "Du har retten til dataportabilitet", + "consent.right_to_data_portability_description": "Du kan be oss om maskinlesbar eksport av innsamlede data om deg og brukerkontoen din. Du kan gjøre det ved å klikke på riktig knapp nedenfor.", + "consent.export_profile": "Eksporter profil (.json)", + "consent.export-profile-success": "Eksporterer profil, du vil få en notifikasjon når eksporten er fullført. ", + "consent.export_uploads": "Eksporter opplastet innhold (.zip)", + "consent.export-uploads-success": "Når du eksporterer opplastinger, får du et varsel når det er fullført.", + "consent.export_posts": "Eksporter innlegg (.csv)", + "consent.export-posts-success": "Eksporterer innlegg, du får en notifikasjon når eksporten er fullført.", + "emailUpdate.intro": "Skriv inn e-postadressen din nedenfor. Dette forumet bruker e-postadressen din til planlagte sammendrag og varsler, og for gjenoppretting av konto ved glemt passord.", + "emailUpdate.optional": "Dette feltet er valgfritt. Du er ikke forpliktet til å oppgi e-postadressen din, men uten en validert e-postadresse vil du ikke kunne gjenopprette kontoen din eller logge på med e-postadressen din.", + "emailUpdate.required": "Dette feltet er obligatorisk", + "emailUpdate.change-instructions": "En bekreftelses-e-post med en unik lenke vil bli sendt til den angitte e-postadressen. Ved å klikke på lenken, vil du bekrefte at du eier e-postadressen, og den blir aktiv på kontoen din. Du kan når som helst oppdatere e-postadressen på brukerprofilen din.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/nb/users.json b/public/language/nb/users.json new file mode 100644 index 0000000000..b1aac09034 --- /dev/null +++ b/public/language/nb/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Seneste brukere", + "top_posters": "Flest innlegg", + "most_reputation": "Best omdømme", + "most_flags": "Flest flagg", + "search": "Søk", + "enter_username": "Skriv inn et brukernavn for å søke", + "search-user-for-chat": "Søk etter en bruker for å starte chat", + "load_more": "Last flere", + "users-found-search-took": "%1 bruker(e) funnet. Søket tok %2 sekunder.", + "filter-by": "Filtrer etter", + "online-only": "Kun tilkoblede", + "invite": "Invitér", + "prompt-email": "E-poster:", + "groups-to-join": "Grupper som en kan bli med i når invitasjonen godtas:", + "invitation-email-sent": "En invitasjons-e-post ble sendt til %1", + "user_list": "Brukerliste", + "recent_topics": "Seneste tråder", + "popular_topics": "Populære tråder", + "unread_topics": "Uleste tråder", + "categories": "Kategorier", + "tags": "Tagger", + "no-users-found": "Ingen brukere funnet" +} \ No newline at end of file diff --git a/public/language/nl/_DO_NOT_EDIT_FILES_HERE.md b/public/language/nl/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/nl/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/nl/admin/admin.json b/public/language/nl/admin/admin.json new file mode 100644 index 0000000000..a0005c8283 --- /dev/null +++ b/public/language/nl/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Weet u zeker dat u de NodeBB bestanden wilt hergenereren en NodeBB opnieuw wilt opstarten?", + "alert.confirm-restart": "Weet u zeker dat u NodeBB opnieuw wilt opstarten?", + + "acp-title": "%1 | NodeBB Administrator Controlepaneel", + "settings-header-contents": "Inhoud", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/nl/admin/advanced/cache.json b/public/language/nl/admin/advanced/cache.json new file mode 100644 index 0000000000..67cef0c6b5 --- /dev/null +++ b/public/language/nl/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Onderwerpcache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1%vol", + "post-cache-size": "Onderwerpcache grootte", + "items-in-cache": "Items in cache" +} \ No newline at end of file diff --git a/public/language/nl/admin/advanced/database.json b/public/language/nl/admin/advanced/database.json new file mode 100644 index 0000000000..4ebec48004 --- /dev/null +++ b/public/language/nl/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in seconden", + "uptime-days": "Uptime in dagen", + + "mongo": "Mongo", + "mongo.version": "MongoDB versie", + "mongo.storage-engine": "Opslag Engine", + "mongo.collections": "Collecties", + "mongo.objects": "Objecten", + "mongo.avg-object-size": "Gem. objectomvang", + "mongo.data-size": "Data omvang", + "mongo.storage-size": "Opslag omvang", + "mongo.index-size": "Index omvang", + "mongo.file-size": "Bestandsomvang", + "mongo.resident-memory": "Resident geheugen", + "mongo.virtual-memory": "Virtueel geheugen", + "mongo.mapped-memory": "Mapped geheugen", + "mongo.bytes-in": "Bytes Inkomend", + "mongo.bytes-out": "Bytes Uitgaand", + "mongo.num-requests": "Aantal requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis versie", + "redis.keys": "Sleutels", + "redis.expires": "Verloopt", + "redis.avg-ttl": "Gemiddelde TTL", + "redis.connected-clients": "Verbonden clients", + "redis.connected-slaves": "Verbonden slaves", + "redis.blocked-clients": "Geblokkeerde clients", + "redis.used-memory": "Gebruikt geheugen", + "redis.memory-frag-ratio": "Geheugenfragmentatie ratio", + "redis.total-connections-recieved": "Totaal inkomende verbindingen", + "redis.total-commands-processed": "Totaal verwerkte commando's", + "redis.iops": "Gelijktijdige operaties per sec.", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Versie", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/nl/admin/advanced/errors.json b/public/language/nl/admin/advanced/errors.json new file mode 100644 index 0000000000..304e44ddc8 --- /dev/null +++ b/public/language/nl/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figuur %1", + "error-events-per-day": "%1 gebeurtenissen per dag", + "error.404": "404 Niet gevonden", + "error.503": "503 Niet beschikbaar", + "manage-error-log": "Beheer foutenlogboek", + "export-error-log": "Exporteer foutenlogboek (CSV)", + "clear-error-log": "Wis foutenlogboek", + "route": "Route", + "count": "Aantal", + "no-routes-not-found": "Hoera! Geen 404 fouten", + "clear404-confirm": "Weet je het zeker dat je de 404 logs wil wissen?", + "clear404-success": "\"404 Niet gevonden\" foutenlogboek gewist" +} \ No newline at end of file diff --git a/public/language/nl/admin/advanced/events.json b/public/language/nl/admin/advanced/events.json new file mode 100644 index 0000000000..79eb67d04a --- /dev/null +++ b/public/language/nl/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "Er zijn geen events", + "control-panel": "Events Controlepaneel", + "delete-events": "Verwijder eventen", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start datum", + "filter-end": "Eind datum", + "filter-perPage": "Per pagina" +} \ No newline at end of file diff --git a/public/language/nl/admin/advanced/logs.json b/public/language/nl/admin/advanced/logs.json new file mode 100644 index 0000000000..08bf046710 --- /dev/null +++ b/public/language/nl/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logboeken", + "control-panel": "Logboeken Controlepaneel", + "reload": "Logboeken herladen", + "clear": "Logboeken wissen", + "clear-success": "Logboeken gewist!" +} \ No newline at end of file diff --git a/public/language/nl/admin/appearance/customise.json b/public/language/nl/admin/appearance/customise.json new file mode 100644 index 0000000000..8d3f759588 --- /dev/null +++ b/public/language/nl/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Aangepaste CSS/LESS", + "custom-css.description": "Voer hier je eigen CSS/LESS regels in. Deze zullen worden toegepast na alle andere stijlen.", + "custom-css.enable": "Activeer aangepaste CSS/LESS", + + "custom-js": "Aangepast Javascript", + "custom-js.description": "Voer hier je eigen javascript code in. Deze zullen worden uitgevoerd als de pagina volledig is geladen.", + "custom-js.enable": "Activeer aangepast javascript", + + "custom-header": "Aangepaste header", + "custom-header.description": "Voer hier je aangepaste HTML in (bv. Meta Tags, etc.). Deze wordt toegevoegd aan de <head> sectie van de markup van je vforum. Script tags zijn toegestaan, maar worden ontmoedigd, aangezien de Custom Javascript tab hiervoor beschikbaar is.", + "custom-header.enable": "Activeer aangepaste header", + + "custom-css.livereload": "Activeer Live Reload", + "custom-css.livereload.description": "Activeer dit om alle sessies op elk apparaat ingelogd onder jouw account te verversen elke keer wanneer je op opslaan klikt." +} \ No newline at end of file diff --git a/public/language/nl/admin/appearance/skins.json b/public/language/nl/admin/appearance/skins.json new file mode 100644 index 0000000000..0d49afe927 --- /dev/null +++ b/public/language/nl/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Style laden...", + "homepage": "Startpagina", + "select-skin": "Kies stijl", + "current-skin": "Huidige stijl", + "skin-updated": "Stijl bijgewerkt", + "applied-success": "%1 stijl was succesvol toegepast", + "revert-success": "Stijl teruggezet naar basis kleuren" +} \ No newline at end of file diff --git a/public/language/nl/admin/appearance/themes.json b/public/language/nl/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/nl/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/nl/admin/dashboard.json b/public/language/nl/admin/dashboard.json new file mode 100644 index 0000000000..2acaa26a6a --- /dev/null +++ b/public/language/nl/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unieke bezoekers", + "logins": "Logins", + "new-users": "Nieuwe Gebruikers", + "posts": "Berichten", + "topics": "Onderwerpen", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connecties", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Laatst herstart door", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/nl/admin/development/info.json b/public/language/nl/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/nl/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/nl/admin/development/logger.json b/public/language/nl/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/nl/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/nl/admin/extend/plugins.json b/public/language/nl/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/nl/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/nl/admin/extend/rewards.json b/public/language/nl/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/nl/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/nl/admin/extend/widgets.json b/public/language/nl/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/nl/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/admins-mods.json b/public/language/nl/admin/manage/admins-mods.json new file mode 100644 index 0000000000..3425370804 --- /dev/null +++ b/public/language/nl/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Globale Moderators", + "moderators": "Moderators", + "no-global-moderators": "Geen Globale Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Geen Moderators", + "add-administrator": "Voeg Administrator toe", + "add-global-moderator": "Voeg Globale Moderator toe", + "add-moderator": "Voeg Moderator toe" +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/categories.json b/public/language/nl/admin/manage/categories.json new file mode 100644 index 0000000000..4380f5c465 --- /dev/null +++ b/public/language/nl/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 zijn succesvol geüpdatet.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/digest.json b/public/language/nl/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/nl/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/nl/admin/manage/groups.json b/public/language/nl/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/nl/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/privileges.json b/public/language/nl/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/nl/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/registration.json b/public/language/nl/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/nl/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/tags.json b/public/language/nl/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/nl/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/uploads.json b/public/language/nl/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/nl/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/nl/admin/manage/users.json b/public/language/nl/admin/manage/users.json new file mode 100644 index 0000000000..3411342b47 --- /dev/null +++ b/public/language/nl/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Gebruikers", + "edit": "Actions", + "make-admin": "Maak administrator", + "remove-admin": "Verwijder administrator", + "validate-email": "Bevestig Email", + "send-validation-email": "Verstuur Email bevestiging", + "password-reset-email": "Verstuur wachtwoord herstel email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Verban gebruiker(s)", + "temp-ban": "Verban gebruiker(s) tijdelijk", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "Nieuwe gebruiker", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/nl/admin/menu.json b/public/language/nl/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/nl/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/advanced.json b/public/language/nl/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/nl/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/api.json b/public/language/nl/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/nl/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/chat.json b/public/language/nl/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/nl/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/cookies.json b/public/language/nl/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/nl/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/email.json b/public/language/nl/admin/settings/email.json new file mode 100644 index 0000000000..afbfe81aa2 --- /dev/null +++ b/public/language/nl/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "E-mailAdres", + "address-help": "Het volgende e-mailadres refereert aan de e-mail die ontvanger ziet in de \"From\" en \"Reply To\" velden.", + "from": "From Naam", + "from-help": "De from naam om te tonen in de e-mail.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Je kunt een bekende dienst uit de lijst selecteren of vul een aangepaste dienst in.", + "smtp-transport.service": "Selecteer een dienst", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Poort", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Versleuteld", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Geen", + "smtp-transport.username": "Gebruikersnaam", + "smtp-transport.username-help": "For the Gmail service, voer het volledige email adres in, in het bijzonder als je gebruik maakt van een Google Apps managed domain.", + "smtp-transport.password": "Wachtwoord", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Aanpassen E-mail Template", + "template.select": "Selecteer E-mail Template", + "template.revert": "Veranderingen ongedaan maken", + "testing": "E-mail Testen", + "testing.select": "Selecteer E-mail Template", + "testing.send": "Verzend Test E-mail", + "testing.send-help": "De test mail zal worden verstuurd naar het email adres van de op dit moment ingelogde gebruiker.", + "subscriptions": "E-mail digests", + "subscriptions.disable": "Schakel e-mail digests uit", + "subscriptions.hour": "Uur van Digest", + "subscriptions.hour-help": "Voer het nummer in dat het uur representeerd waarop scheduled email digests worden verstuurd (bv. 0 voor middernacht, 17 voor 17:00). Neem er s.v.p. notie van dat dit het uur is van de server self, dit hoeft niet exact overeen te komen met de klok van uw systeem.
De tijd op de server is bij benadering:
De volgende dagelijkse digest staat gepland om ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/nl/admin/settings/general.json b/public/language/nl/admin/settings/general.json new file mode 100644 index 0000000000..fedadad45d --- /dev/null +++ b/public/language/nl/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Instellingen", + "title": "Site Titel", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "De URL van de site titel", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Jouw Communiy Naam", + "title.show-in-header": "Toon Site Titel in Header", + "browser-title": "Browser Titel", + "browser-title-help": "Als geen browser titel is gespecificeerd dan word de site titel gebruikt", + "title-layout": "Titel Lay-out", + "title-layout-help": "Defineer hoe de browser titel gestructureerd word. bijv: {paginaTitel} | {browserTitel}", + "description.placeholder": "Een korte beschrijving van uw gemeenschap", + "description": "Site Beschrijving", + "keywords": "Site Trefwoorden", + "keywords-placeholder": "Trefwoorden die uw community beschrijven, kommagescheiden", + "logo": "Site Logo", + "logo.image": "Afbeelding", + "logo.image-placeholder": "Pad naar een logo om te tonen op de forum header", + "logo.upload": "Uploaden", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "De URL van de site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Tekst", + "log.alt-text-placeholder": "Alternatieve tekst voor toegankelijkheid", + "favicon": "Favoicon", + "favicon.upload": "Uploaden", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Uploaden", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Uitgaande links", + "outgoing-links.warning-page": "Gebruik waarschuwingspagina voor uitgaande links", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domeinen op de whitelist voor het omzeilen van de waarschuwingspagina", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/nl/admin/settings/group.json b/public/language/nl/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/nl/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/guest.json b/public/language/nl/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/nl/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/homepage.json b/public/language/nl/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/nl/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/languages.json b/public/language/nl/admin/settings/languages.json new file mode 100644 index 0000000000..66f75f9248 --- /dev/null +++ b/public/language/nl/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Taalinstellingen", + "description": "De standaard taal bepaald de taalinstellingen voor alle gebruikers die uw forum bezoeken.
Individuele gebruikers kunnen deze standaard instellingen overschrijven op hun gebruikersinstellingen pagina.", + "default-language": "Standaard taal", + "auto-detect": "Detecteer de taalinstellingen voor Gasten automatisch" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/navigation.json b/public/language/nl/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/nl/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/nl/admin/settings/notifications.json b/public/language/nl/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/nl/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/pagination.json b/public/language/nl/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/nl/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/post.json b/public/language/nl/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/nl/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/reputation.json b/public/language/nl/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/nl/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/social.json b/public/language/nl/admin/settings/social.json new file mode 100644 index 0000000000..85ed00c695 --- /dev/null +++ b/public/language/nl/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Berichten delen", + "info-plugins-additional": "Plug-ins kunnen extra netwerken toevoegen om berichten mee te delen.", + "save-success": "Netwerken om berichten te delen is succesvol opgeslagen!" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/sockets.json b/public/language/nl/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/nl/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/sounds.json b/public/language/nl/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/nl/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/tags.json b/public/language/nl/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/nl/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/nl/admin/settings/uploads.json b/public/language/nl/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/nl/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/nl/admin/settings/user.json b/public/language/nl/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/nl/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/nl/admin/settings/web-crawler.json b/public/language/nl/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/nl/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/nl/category.json b/public/language/nl/category.json new file mode 100644 index 0000000000..3dd40b6f85 --- /dev/null +++ b/public/language/nl/category.json @@ -0,0 +1,23 @@ +{ + "category": "Categorie", + "subcategories": "Subcategorieën", + "new_topic_button": "Nieuw onderwerp", + "guest-login-post": "Log in om een reactie te plaatsen", + "no_topics": "Er zijn geen onderwerpen in deze categorie.
Waarom maak je er niet een aan?", + "browsing": "browsing", + "no_replies": "Niemand heeft gereageerd", + "no_new_posts": "Geen nieuwe berichten.", + "watch": "Volgen", + "ignore": "Negeren", + "watching": "Volgend", + "not-watching": "Niet gevolgd", + "ignoring": "Negerend", + "watching.description": "Toon ongelezen en recente onderwerpen", + "not-watching.description": "Toon geen ongelezen onderwerpen, toon wel recente onderwerpen", + "ignoring.description": "Toon geen onderwerpen onder ongelezen en recent", + "watching.message": "Van deze categorie en alle sub-categorieën worden nu meldingen ontvangen ", + "notwatching.message": "Deze categorie en alle sub-categorieën worden niet gevolgd", + "ignoring.message": " Er worden geen meldingen van deze categorie en alle sub-categorieën ontvangen ", + "watched-categories": "Categorieën die bekeken zijn.", + "x-more-categories": "%1 meer categorieën" +} \ No newline at end of file diff --git a/public/language/nl/email.json b/public/language/nl/email.json new file mode 100644 index 0000000000..3d82870e7c --- /dev/null +++ b/public/language/nl/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test e-mail", + "password-reset-requested": "Wachtwoord Reset Aangevraagd!", + "welcome-to": "Welkom bij %1", + "invite": "Uitnodiging van %1 ", + "greeting_no_name": "Hallo", + "greeting_with_name": "Hallo %1", + "email.verify-your-email.subject": "Verifieer alstublieft uw e-mail", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Bedank voor het registreren bij %1!", + "welcome.text2": "Om je account volledig te activeren, moet je de instructies uit het bevestigingsbericht opvolgen. Controleer daarom nu eerst je e-mail inbox voor de activeringscode en volg de link in het bericht.", + "welcome.text3": "Een administrator heeft uw registratie geaccepteerd. U kan nu inloggen met uw gebruikersnaam en wachtwoord.", + "welcome.cta": "Klik hier om je e-mailadres te bevestigen", + "invitation.text1": "%1 heeft u uitgenodigd voor %2 ", + "invitation.text2": "Uw uitnodiging vervalt over %1 dagen.", + "invitation.cta": "Klik hier om je account aan te maken.", + "reset.text1": "We hebben een verzoek ontvangen om je wachtwoord te herstellen, wellicht omdat je hem bent vergeten. Indien dit niet het geval is kan je deze e-mail gewoon negeren.", + "reset.text2": "Om je wachtwoord opnieuw in te stellen klik je op deze link:", + "reset.cta": "Klik hier om je wachtwoord te resetten", + "reset.notify.subject": "Wachtwoord succesvol gewijzigd", + "reset.notify.text1": "Op %1 is het wachtwoord van je account succesvol gewijzigd.", + "reset.notify.text2": "Neem onmiddellijk contact met een beheerder op wanneer je hiervoor geen toestemming hebt gegeven.", + "digest.latest_topics": "De meest recente onderwerpen van %1", + "digest.top-topics": "Top onderwerpen van %1", + "digest.popular-topics": "Populaire onderwerpen van %1", + "digest.cta": "Klik hier om %1 te bezoeken ", + "digest.unsub.info": "Deze samenvatting hebben we naar je verzonden omdat je dat hebt ingesteld.", + "digest.day": "dag", + "digest.week": "week", + "digest.month": "maand", + "digest.subject": "Samenvatting voor %1", + "digest.title.day": "Uw dagelijkse samenvatting", + "digest.title.week": "Uw wekelijkse samenvatting", + "digest.title.month": "Uw maandelijkse samenvatting", + "notif.chat.subject": "Nieuw chatbericht van %1", + "notif.chat.cta": "Klik hier om het gesprek te hervatten", + "notif.chat.unsub.info": "Deze notificatie is verzonden vanwege de gebruikersinstellingen voor abonnementen.", + "notif.post.unsub.info": "Deze notificatie is door ons verzonden vanwege gebruikersinstellingen voor abonnementen en berichten.", + "notif.post.unsub.one-click": "Of om uit te schrijven voor toekomstige e-mails zoals deze: klik", + "notif.cta": "Naar het forum", + "notif.cta-new-reply": "Bericht weergeven", + "notif.cta-new-chat": "Chat weergeven", + "notif.test.short": "Testen van notificaties", + "notif.test.long": "Dit is een test van de notificaties e-mail. Stuur hulp!", + "test.text1": "Dit is een testbericht om te verifiëren dat NodeBB de e-mailberichtservice correct heeft opgezet.", + "unsub.cta": "Klik hier om deze instellingen te wijzigen", + "unsubscribe": "uitschrijven", + "unsub.success": "U zult niet langer e-mails ontvangen van de %1 mailing lijst", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "U bent verbannen van %1", + "banned.text1": "De gebruiker %1 is verbannen van %2.", + "banned.text2": "Deze verbanning duurt tot %1.", + "banned.text3": "U bent verbannen om de volgende reden:", + "closing": "Bedankt!" +} \ No newline at end of file diff --git a/public/language/nl/error.json b/public/language/nl/error.json new file mode 100644 index 0000000000..b0f354ed81 --- /dev/null +++ b/public/language/nl/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Ongeldige data", + "invalid-json": "Ongeldige JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Het lijkt erop dat je niet ingelogd bent.", + "account-locked": "Je account is tijdelijk vergrendeld", + "search-requires-login": "Zoeken vereist een account - meld je aan of registreer je om te zoeken.", + "goback": "Klik terug om terug te keren naar de vorige pagina", + "invalid-cid": "Ongeldige categorie ID", + "invalid-tid": "Ongeldig onderwerp ID", + "invalid-pid": "Ongeldig berichtkenmerk", + "invalid-uid": "Ongeldig gebruikerskenmerk", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Ongeldige gebruikersnaam", + "invalid-email": "Ongeldig e-mailadres", + "invalid-fullname": "Ongeldige volledige naam", + "invalid-location": "Ongeldige locatie", + "invalid-birthday": "Ongeldige geboortedag", + "invalid-title": "Ongeldige titel", + "invalid-user-data": "Ongeldige gebruikersgegevens", + "invalid-password": "Ongeldig wachtwoord", + "invalid-login-credentials": "Ongeldige aanmeldingsreferenties", + "invalid-username-or-password": "Geef zowel een gebruikersnaam als wachtwoord op", + "invalid-search-term": "Ongeldig zoekterm", + "invalid-url": "Ongeldig web adres", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Het lokale login systeem is niet toegankelijk voor niet gerechtigde gebruikers.", + "csrf-invalid": "We konden u niet aanmelden, waarschijnlijk door een verlopen sessie. Probeer het a.u.b. nogmaals.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Ongeldig paginering waarde. De waarde moet op z'n minst %1 zijn en niet hoger dan %2 zijn.", + "username-taken": "Gebruikersnaam is al in gebruik ", + "email-taken": "E-mailadres is al in gebruik", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "E-mail was reeds uitgenodigd", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Het gebruik van chatfunctionaliteit is pas toegestaan na validatie van het e-mailadres.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Helaas kon het e-mailadres niet bevestigd worden, probeer het later nog eens.", + "confirm-email-already-sent": "Bevestigingsmail is zojuist al verzonden, wacht alsjeblieft %1 minuut (minuten) voordat je opnieuw een bevestigingsmail aanvraagt.", + "sendmail-not-found": "De sendmail executable kon niet worden gevonden, zorg ervoor dat deze is geïnstalleerd en dat de gebruiker die NodeBB draait deze kan uitvoeren.", + "digest-not-enabled": "De gebruiker heeft samenvatting niet aangezet, of de systeem default is niet geconfigureerd om samenvattingen te versturen", + "username-too-short": "Gebruikersnaam is te kort", + "username-too-long": "Gebruikersnaam is te lang", + "password-too-long": "Wachtwoord is te lang", + "reset-rate-limited": "Te veel verzoeken voor wachtwoordherstel (snelheid beperkt)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Gebruiker verbannen", + "user-banned-reason": "Sorry, dit account is verbannen (Reden: %1)", + "user-banned-reason-until": "Sorry, dit account is verbannen tot %1 (Reden: %2)", + "user-too-new": "Helaas, het is een vereiste om %1 seconde(n) te wachten voordat het eerste bericht geplaatst kan worden.", + "blacklisted-ip": "Sorry, uw IP-adres is verbannen uit deze community. Als u meent dat dit onterecht is, neem dan contact op met een beheerder.", + "ban-expiry-missing": "Geef een einddatum op voor deze ban.", + "no-category": "Categorie bestaat niet", + "no-topic": "Onderwerp bestaat niet", + "no-post": "Bericht bestaat niet", + "no-group": "Groep bestaat niet", + "no-user": "Gebruiker bestaat niet", + "no-teaser": "Dit voorproefje bestaat niet", + "no-flag": "Flag does not exist", + "no-privileges": "Onvoldoende rechten om deze actie uit te voeren", + "category-disabled": "Categorie uitgeschakeld", + "topic-locked": "Onderwerp gesloten", + "post-edit-duration-expired": "Het is slechts toegestaan om binnen %1 seconde(n) na plaatsen van het bericht, deze te bewerken.", + "post-edit-duration-expired-minutes": "Je kunt berichten pas %1 minuten na het plaatsen aanpassen.", + "post-edit-duration-expired-minutes-seconds": "Je kunt berichten pas %1 minuten en %2 seconden na het plaatsen aanpassen.", + "post-edit-duration-expired-hours": "Je kunt berichten pas %1 uur na het plaatsen aanpassen.", + "post-edit-duration-expired-hours-minutes": "Je kunt berichten pas %1 uur en %2 minuten na het plaatsen aanpassen.", + "post-edit-duration-expired-days": "Je kunt berichten pas %1 dagen na het plaatsen aanpassen.", + "post-edit-duration-expired-days-hours": "Je kunt berichten pas %1 dagen en %2 uur na het plaatsen aanpassen.", + "post-delete-duration-expired": "Je kunt berichten pas %1 seconden na het plaatsen verwijderen.", + "post-delete-duration-expired-minutes": "Je kunt berichten pas %1 minuten na het plaatsen verwijderen.", + "post-delete-duration-expired-minutes-seconds": "Je kunt berichten pas %1 minuten %2 seconden na het plaatsen verwijderen.", + "post-delete-duration-expired-hours": "Je kunt berichten pas %1 uur na het plaatsen verwijderen.", + "post-delete-duration-expired-hours-minutes": "Je kunt berichten pas %1 uur %2 minuten na het plaatsen verwijderen.", + "post-delete-duration-expired-days": "Je kunt berichten pas %1 dagen na het plaatsen verwijderen.", + "post-delete-duration-expired-days-hours": "Je kunt berichten pas %1 dag(en) %2 uur na het plaatsen verwijderen.", + "cant-delete-topic-has-reply": "Je kunt je topic niet verwijderen nadat iemand heeft gereageerd", + "cant-delete-topic-has-replies": "Je kunt je topic niet verwijderen als het %1 reacties heeft", + "content-too-short": "Geef wat meer inhoud aan een bericht! Berichten dienen uit minimaal %1 teken(s) te bestaan.", + "content-too-long": "Kort het bericht wat in, het aantal gebruikte tekens overschrijdt het ingestelde limiet want berichten mogen niet meer dan %1 teken(s) bevatten.", + "title-too-short": "Geef een titel op die uit meer tekens bestaat. Titels dienen ten minste uit %1 teken(s) te bestaan.", + "title-too-long": "Geef een kortere titel op. Titels mogen uit niet meer dan %1 teken(s) bestaan.", + "category-not-selected": "Categorie niet geselecteerd ", + "too-many-posts": "Het is slechts toegestaan iedere %1 seconde(n) een bericht te plaatsen - wacht even voordat opnieuw een bericht verzonden wordt", + "too-many-posts-newbie": "Nieuwe gebruikersaccounts zoals deze zijn begrensd en mogen slechts iedere %1 seconde(n) berichten plaatsen, tot het moment dat %2 reputatie verdiend is - wacht daarom even met opnieuw een bericht te plaatsten", + "already-posting": "You are already posting", + "tag-too-short": "Geef een tag op die uit meer tekens bestaat. Tags dienen uit minimaal %1 teken(s) te bestaan.", + "tag-too-long": "Geef een kortere tag op. Tags mogen niet langer dan %1 teken(s) zijn", + "not-enough-tags": "Niet genoeg labels. Onderwerp moeten tenminste %1 label(s) hebben", + "too-many-tags": "Teveel labels. Onderwerpen kunnen niet meer dan %1 label(s) hebben", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Een moment geduld tot alle bestanden overgebracht zijn...", + "file-too-big": "Maximum toegestane bestandsgrootte is %1 kB - probeer een kleiner bestand te verzenden", + "guest-upload-disabled": "Uploads voor gasten zijn uitgeschaleld ", + "cors-error": "Kan plaatje niet uploaden door verkeerd geconfigureerd CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Je hebt dit bericht al als favoriet toegevoegd", + "already-unbookmarked": "Je hebt dit bericht al verwijderd uit je favorieten", + "cant-ban-other-admins": "Het is niet toegestaan andere beheerders te verbannen!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Je bent de enige beheerder. Stel eerst een andere gebruiker als beheerder in voordat je jezelf geen beheerder meer maakt.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Verwijder administratieve rechten van dit account voordat je probeert deze te verwijderen", + "already-deleting": "Already deleting", + "invalid-image": "Ongeldige afbeelding", + "invalid-image-type": "Ongeldig bestandstype afbeelding. Deze afbeelding is van een bestandstype dat niet ondersteund wordt. Toegestane bestandstypes voor afbeeldingsbestanden zijn: %1", + "invalid-image-extension": "Ongeldig bestandstype afbeelding", + "invalid-file-type": "Dit bestandstype wordt niet ondersteund. Toegestane bestandstypen zijn: %1", + "invalid-image-dimensions": "Dimensies van de afbeelding zijn te groot", + "group-name-too-short": "De groepsnaam is te kort", + "group-name-too-long": "Groepsnaam te lang", + "group-already-exists": "Een groep met deze naam bestaat al", + "group-name-change-not-allowed": "Het aanpassen van de groepsnaam is niet toegestaan", + "group-already-member": "Deze gebruiker is al lid van deze groep", + "group-not-member": "Deze gebruiker is geen lid van deze groep", + "group-needs-owner": "De groep vereist ten minste 1 eigenaar", + "group-already-invited": "Deze gebruiker is al uitgenodigt", + "group-already-requested": "Uw lidmaatschap aanvraag is al verstuurd", + "group-join-disabled": "Je kunt op dit moment geen lid worden van deze groep", + "group-leave-disabled": "Je kunt op dit moment de groep niet verlaten", + "post-already-deleted": "Dit bericht is al verwijderd", + "post-already-restored": "Dit bericht is al hersteld", + "topic-already-deleted": "Dit onderwerp is al verwijderd", + "topic-already-restored": "Dit onderwerp is al hersteld", + "cant-purge-main-post": "Het is niet mogelijk het eerste bericht te verwijderen. Hiervoor dient het gehele onderwerp verwijderd te worden.", + "topic-thumbnails-are-disabled": "Miniatuurweergaven bij onderwerpen uitgeschakeld.", + "invalid-file": "Ongeldig bestand", + "uploads-are-disabled": "Uploads zijn uitgeschakeld", + "signature-too-long": "Sorry, je onderschrift kan niet langer zijn dan %1 karakter(s).", + "about-me-too-long": "Sorry, je beschrijving kan niet langer zijn dan %1 karakter(s).", + "cant-chat-with-yourself": "Het is niet mogelijk om met jezelf een chatgesprek te houden.", + "chat-restricted": "Deze gebruiker heeft beperkingen aan de chatfunctie opgelegd waardoor deze eerst iemand moet volgen voordat deze persoon een nieuwe chat mag initiëren.", + "chat-disabled": "Chat systeem uitgeschakeld", + "too-many-messages": "Je hebt in korte tijd veel berichten verstuurd, als je even wacht mag je weer berichten sturen.", + "invalid-chat-message": "Ongeldig bericht", + "chat-message-too-long": "Chat berichten kunnen niet groter zijn dan %1 karakters.", + "cant-edit-chat-message": "Het is niet toegestaan om dit bericht aan te passen", + "cant-delete-chat-message": "Het is niet toegestaan om dit bericht te verwijderen", + "chat-edit-duration-expired": "Het is slechts toegestaan om binnen %1 seconde(n) na plaatsen van het chat bericht, deze te bewerken.", + "chat-delete-duration-expired": "Het is slechts toegestaan om binnen %1 seconde(n) na plaatsen van het chat bericht, deze te verwijderen.", + "chat-deleted-already": "Dit chat bericht is al verwijderd.", + "chat-restored-already": "Dit chat bericht is al hersteld.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Je hebt al gestemd voor deze post.", + "reputation-system-disabled": "Reputatie systeem is uitgeschakeld.", + "downvoting-disabled": "Negatief stemmen is uitgeschakeld", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "Je hebt dit bericht al gerapporteerd", + "user-already-flagged": "Je hebt deze gebruiker al gerapporteerd", + "post-flagged-too-many-times": "Dit bericht is al door anderen gerapporteerd", + "user-flagged-too-many-times": "Deze gebruiker is al door anderen gerapporteerd", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Het is niet mogelijk om op je eigen bericht te stemmen", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "Je kunt slecht %1 keer per dag downvoten", + "too-many-downvotes-today-user": "Je kunt een gebruiker slecht %1 keer per dag downvoten", + "reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegengekomen. NodeBB blijft operationeel. Echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.", + "registration-error": "Fout tijdens registratie", + "parse-error": "Tijdens het verwerken van het antwoord van de server is er iets misgegaan.", + "wrong-login-type-email": "Gebruik je e-mailadres om in te loggen", + "wrong-login-type-username": "Gebruik je gebruikersnaam om in te loggen", + "sso-registration-disabled": "Registratie is uitgeschakeld voor %1 accounts, registreer eerst met een e-mailadres", + "sso-multiple-association": "U kunt niet meerdere accounts van deze service associeren met uw NodeBB account. Verwijder eerst de associatie met uw huidige account en probeer het opnieuw.", + "invite-maximum-met": "Je heb het maximum aantal mensen uitgenodigd (%1 van de %2).", + "no-session-found": "Geen login sessie gevonden!", + "not-in-room": "Gebruiker niet in de chat", + "cant-kick-self": "Je kunt jezelf niet uit een groep schoppen", + "no-users-selected": "Geen gebruiker(s) geselecteerd", + "invalid-home-page-route": "Onbekende homepage route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Geen onderwerpen geselecteerd!", + "cant-move-to-same-topic": "Een bericht kan niet naar hetzelfde onderwerp worden verplaatst!", + "cant-move-topic-to-same-category": "Kan onderwerp niet verplaatsen naar dezelfde categorie", + "cannot-block-self": "Je kan jezelf niet blokkeren!", + "cannot-block-privileged": "Je kan geen administrators of global moderators blokkeren", + "cannot-block-guest": "Gasten kunnen geen andere gebruikers blokkeren", + "already-blocked": "Deze gebruiker is al geblokkeerd", + "already-unblocked": "Deze gebruiker is al gedeblokkeerd", + "no-connection": "Er lijkt een probleem te zijn met je internetverbinding", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Kan plugin niet installeren – alleen plugins toegestaan door de NodeBB Package Manager kunnen via de ACP geinstalleerd worden", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/nl/flags.json b/public/language/nl/flags.json new file mode 100644 index 0000000000..b2510aeaa7 --- /dev/null +++ b/public/language/nl/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Status", + "reports": "Rapportages", + "first-reported": "Eerste rapportage", + "no-flags": "Hoera! Geen markeringen gevonden.", + "assignee": "Toegekend aan", + "update": "Bijwerken", + "updated": "Bijgewerkt", + "resolved": "Opgelost", + "target-purged": "De inhoud waar deze markering naar verwijst is verwijderd en niet meer beschikbaar.", + + "graph-label": "Dagelijkse markeringen", + "quick-filters": "Snelfilters", + "filter-active": "Er zijn een of meer filters actief in deze lijst van markeringen", + "filter-reset": "Filters verwijderen", + "filters": "Filter opties", + "filter-reporterId": "Rapporteur UID", + "filter-targetUid": "Markering UID", + "filter-type": "Markering Type", + "filter-type-all": "Alle inhoud", + "filter-type-post": "Bericht", + "filter-type-user": "Gebruiker", + "filter-state": "Status", + "filter-assignee": "UID van toewijzer", + "filter-cid": "Categorie", + "filter-quick-mine": "Toegewezen aan mij", + "filter-cid-all": "Alle categorieën", + "apply-filters": "Filters toepassen", + "more-filters": "Meer filters", + "fewer-filters": "Minder filters", + + "quick-actions": "Snelle acties", + "flagged-user": "Gemarkeerde gebruiker", + "view-profile": "Profiel bekijken", + "start-new-chat": "Begin een nieuwe chat", + "go-to-target": "Bekijk markering doel", + "assign-to-me": "Wijs aan mij toe", + "delete-post": "Bericht verwijderen", + "purge-post": "Bericht opruimen", + "restore-post": "Bericht herstellen", + "delete": "Delete Flag", + + "user-view": "Profiel bekijken", + "user-edit": "Profiel wijzigen", + + "notes": "Markering notities", + "add-note": "Notitie toevoegen", + "no-notes": "Geen gedeelde notities", + "delete-note-confirm": "Weet je zeker dat je deze markeringsnotitie wilt verwijderen?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Notitie toegevoegd", + "note-deleted": "Notitie verwijderd", + "flag-deleted": "Flag Deleted", + + "history": "Account & markering geschiedenis", + "no-history": "Geen markering geschiedenis", + + "state-all": "Alle statussen", + "state-open": "Nieuw/Open", + "state-wip": "Wordt aan gewerkt", + "state-resolved": "Opgelost", + "state-rejected": "Afgewezen", + "no-assignee": "Niet toegewezen", + + "sort": "Sorteer op", + "sort-newest": "Nieuwste eerst", + "sort-oldest": "Oudste eerst", + "sort-reports": "Meest gerapporteerd", + "sort-all": "All flag types...", + "sort-posts-only": "Alleen berichten...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Meeste antwoorden", + + "modal-title": "Inhoud rapporteren", + "modal-body": "Beschrijf de reden voor het markeren van %1 %2 voor review. Of gebruik een van de snelknoppen indien van toepassing.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Aanstootgevend", + "modal-reason-other": "Anders (specificeer onder)", + "modal-reason-custom": "Reden voor het rapporteren van deze content...", + "modal-submit": "Rapport verzenden", + "modal-submit-success": "Inhoud is gemarkeerd voor moderatie.", + + "bulk-actions": "Bulk acties", + "bulk-resolve": "Los markering(en) op", + "bulk-success": "%1 markeringen aangepast", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/nl/global.json b/public/language/nl/global.json new file mode 100644 index 0000000000..16de8cfe8e --- /dev/null +++ b/public/language/nl/global.json @@ -0,0 +1,126 @@ +{ + "home": "Home", + "search": "Zoeken", + "buttons.close": "Sluiten", + "403.title": "Toegang geweigerd", + "403.message": "Het lijkt erop dat je op een pagina beland bent waar je geen toegang tot hebt.", + "403.login": "Je kan proberen in te loggen?", + "404.title": "Niet gevonden", + "404.message": "Deze pagina bestaat niet. Klik hier om naar de hoofdpagina van deze website te navigeren.", + "500.title": "Interne fout", + "500.message": "Oeps! Ziet er naar uit dat iets fout ging!", + "400.title": "Foutief verzoek", + "400.message": "Het lijkt erop dat de link onjuist is. Kijk het nog eens na en probeer het opnieuw. Of ga terug naar de startpagina.", + "register": "Registeren", + "login": "Login", + "please_log_in": "Aanmelden", + "logout": "Uitloggen", + "posting_restriction_info": "Reageren is momenteel beperkt tot geregistreerde leden, klik hier om in te loggen.", + "welcome_back": "Welkom terug", + "you_have_successfully_logged_in": "Aanmelden succesvol", + "save_changes": "Wijzigingen opslaan", + "save": "Opslaan", + "close": "Sluiten", + "pagination": "Paginering", + "pagination.out_of": "%1 van %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Beheer", + "header.categories": "Categorieën", + "header.recent": "Recent", + "header.unread": "Ongelezen", + "header.tags": "Tags", + "header.popular": "Populair", + "header.top": "Top", + "header.users": "Gebruikers", + "header.groups": "Groepen", + "header.chats": "Chats", + "header.notifications": "Notificaties", + "header.search": "Zoeken", + "header.profile": "Profiel", + "header.navigation": "Navigatie", + "notifications.loading": "Notificaties laden", + "chats.loading": "Chats laden", + "motd.welcome": "Welkom bij NodeBB, het discussie platform van de toekomst.", + "previouspage": "Vorige pagina", + "nextpage": "Volgende pagina", + "alert.success": "Succes", + "alert.error": "Fout", + "alert.banned": "Verbannen", + "alert.banned.message": "Je bent net verbannen en je toegang is nu beperkt.", + "alert.unbanned": "Verbanning opgeheven", + "alert.unbanned.message": "Je verbanning is opgeheven.", + "alert.unfollow": "%1 wordt niet langer gevolgd!", + "alert.follow": "%1 wordt nu gevolgd!", + "users": "Gebruikers", + "topics": "Onderwerpen", + "posts": "Berichten", + "x-posts": "%1 berichten", + "best": "Beste", + "controversial": "Controversial", + "votes": "Stemmen", + "x-votes": "%1 stemmen", + "voters": "Stemmers", + "upvoters": "Positieve stemmers", + "upvoted": "Omhoog gestemd", + "downvoters": "Negatieve stemmers", + "downvoted": "Omlaag gestemd", + "views": "Weergaven", + "posters": "Plaatsers", + "reputation": "Reputatie", + "lastpost": "Laatste bericht", + "firstpost": "Eerste bericht", + "read_more": "Lees meer", + "more": "Meer", + "none": "None", + "posted_ago_by_guest": "geplaatst %1 door gast", + "posted_ago_by": "geplaatst %1 door %2", + "posted_ago": "geplaatst door %1", + "posted_in": "geplaatst in %1", + "posted_in_by": "geplaatst in %1 door %2", + "posted_in_ago": "geplaatst in %1 %2", + "posted_in_ago_by": "geplaatst in %1 %2 door %3", + "user_posted_ago": "%1 plaatste %2", + "guest_posted_ago": "Gast plaatste %1", + "last_edited_by": "voor het laatst aangepast door %1", + "norecentposts": "Geen recente berichten", + "norecenttopics": "Geen recente onderwerpen", + "recentposts": "Recente berichten", + "recentips": "IP-adressen van recente gebruikers", + "moderator_tools": "Moderator gereedschappen", + "online": "Online", + "away": "Afwezig", + "dnd": "Niet storen", + "invisible": "Onzichtbaar", + "offline": "Offline", + "email": "E-mail", + "language": "Taal", + "guest": "Gast", + "guests": "Gasten", + "former_user": "Een ex-gebruiker", + "system-user": "Systeem", + "unknown-user": "Onbekende gebruiker", + "updated.title": "Site update", + "updated.message": "Deze site heeft zojuist een update ontvangen. Klik hier om de pagina te verversen.", + "privacy": "Privé", + "follow": "Volgen", + "unfollow": "Ontvolgen", + "delete_all": "Alles verwijderen", + "map": "Kaart", + "sessions": "Login sessies", + "ip_address": "IP Adres", + "enter_page_number": "Voer paginanummer in", + "upload_file": "Upload bestand", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Toegestane bestandstypen zijn %1", + "unsaved-changes": "Je hebt niet opgeslagen wijzigingen aangebracht. Weet je zeker dat je de pagina wilt verlaten?", + "reconnecting-message": "Het lijkt erop dat je verbinding naar %1 verloren is gegaan, wacht even terwijl we de verbinding proberen te herstellen.", + "play": "Afspelen", + "cookies.message": "Deze website gebruikt cookies om je ervan te verzekeren dat je de beste ervaring krijgt tijdens het gebruik van onze website.", + "cookies.accept": "Begrepen", + "cookies.learn_more": "Meer", + "edited": "Bewerkt", + "disabled": "Uitgeschakeld", + "select": "Selecteer", + "user-search-prompt": "Typ hier om gebruikers te vinden..." +} \ No newline at end of file diff --git a/public/language/nl/groups.json b/public/language/nl/groups.json new file mode 100644 index 0000000000..34dd5ae976 --- /dev/null +++ b/public/language/nl/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Groepen", + "view_group": "Bekijk groep", + "owner": "Groepseigenaar", + "new_group": "Nieuwe groep aanmaken", + "no_groups_found": "Er zijn geen groepen om weer te geven", + "pending.accept": "Accepteer", + "pending.reject": "Afwijzen", + "pending.accept_all": "Iedereen accepteren", + "pending.reject_all": "Iedereen afwijzen", + "pending.none": "Er zijn geen afwachtende leden op het moment", + "invited.none": "Er zijn geen uitgenodigde leden op het moment", + "invited.uninvite": "Uitnodiging intrekken", + "invited.search": "Zoek naar een gebruiker om uit te nodigen voor deze groep", + "invited.notification_title": "Je bent uitgenodigd voor de groep %1", + "request.notification_title": "groepsverzoek gekregen van %1", + "request.notification_text": "%1 heeft een verzoek ingediend om een lid te zijn van de groep %2", + "cover-save": "Opslaan", + "cover-saving": "Bezig met opslaan", + "details.title": "Groepsdetails", + "details.members": "Ledenlijst", + "details.pending": "Nog niet geaccepteerde leden", + "details.invited": "Uitgenodigde leden", + "details.has_no_posts": "Deze groepleden hebben nog geen berichten geplaatst", + "details.latest_posts": "Meest recente berichten", + "details.private": "Prive", + "details.disableJoinRequests": "Groepsverzoeken uitschakelen", + "details.disableLeave": "Sta gebruikers niet toe de groep te verlaten", + "details.grant": "Toekennen/herroepen van eigendom", + "details.kick": "Kick", + "details.kick_confirm": "Weet u zeker dat u de gebruiker wilt verwijderen uit de groep?", + "details.add-member": "Voeg lid toe", + "details.owner_options": "Groepsadministratie", + "details.group_name": "Groepsnaam", + "details.member_count": "Ledentelling", + "details.creation_date": "Aangemaakt op", + "details.description": "Beschrijving", + "details.member-post-cids": "Category IDs om berichten van te tonen", + "details.badge_preview": "Badge Voorbeeld", + "details.change_icon": "Wijzig icoon", + "details.change_label_colour": "Wijzig labelkleur", + "details.change_text_colour": "Wijzig tekstkleur", + "details.badge_text": "Badge Tekst", + "details.userTitleEnabled": "Badge Weergeven", + "details.private_help": "Wanneer ingeschakeld, zal eerst een groepseigenaar goedkeuring moeten verlenen voordat nieuwe leden kunnen toetreden", + "details.hidden": "Niet getoond", + "details.hidden_help": "Indien geactiveerd zal deze groep niet getoond worden in de groepslijst en zullen gebruikers handmatig uitgenodigd moeten worden.", + "details.delete_group": "Groep verwijderen", + "details.private_system_help": "Private groepen zijn op systeemniveau uitgeschakeld, deze optie doet niets.", + "event.updated": "Groepsdetails zijn bijgewerkt", + "event.deleted": "De groep \"%1\" is verwijderd", + "membership.accept-invitation": "Uitnodiging accepteren", + "membership.accept.notification_title": "Je bent nu lid van %1", + "membership.invitation-pending": "Openstaande uitnodiging", + "membership.join-group": "Deelnemen aan groep", + "membership.leave-group": "Verlaat groep", + "membership.leave.notification_title": "%1 heeft groep %2 verlaten", + "membership.reject": "Afwijzen", + "new-group.group_name": "Groepsnaam:", + "upload-group-cover": "Upload groepscover", + "bulk-invite-instructions": "Vul een lijst is met gebruikersnamen gescheiden met komma's om deze uit te nodigen voor deze groep", + "bulk-invite": "Massa uitnodiging", + "remove_group_cover_confirm": "Weet u zeker dat u de cover foto wilt verwijderen?" +} \ No newline at end of file diff --git a/public/language/nl/ip-blacklist.json b/public/language/nl/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/nl/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/nl/language.json b/public/language/nl/language.json new file mode 100644 index 0000000000..5490106fc4 --- /dev/null +++ b/public/language/nl/language.json @@ -0,0 +1,5 @@ +{ + "name": "Nederlands", + "code": "nl", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/nl/login.json b/public/language/nl/login.json new file mode 100644 index 0000000000..931d2e5d7f --- /dev/null +++ b/public/language/nl/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Gebruikersnaam / Email", + "username": "Gebruikersnaam", + "remember_me": "Aangemeld blijven?", + "forgot_password": "Wachtwoord vergeten?", + "alternative_logins": "Andere manieren van aanmelden", + "failed_login_attempt": "Aanmelden mislukt", + "login_successful": "Je bent succesvol ingelogd!", + "dont_have_account": "Geen gebruikersaccount?", + "logged-out-due-to-inactivity": "Je bent uitgelogd van het admin control panel vanwege inactiviteit.", + "caps-lock-enabled": "Caps Lock staat aan" +} \ No newline at end of file diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json new file mode 100644 index 0000000000..f40385ecf3 --- /dev/null +++ b/public/language/nl/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat met", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "Je kijkt nu naar oudere berichten. Klik hier om naar het meest recente bericht te gaan.", + "chat.send": "Verzenden", + "chat.no_active": "Er zijn geen actieve chats.", + "chat.user_typing": "%1 is aan het typen ...", + "chat.user_has_messaged_you": "%1 heeft een bericht gestuurd", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Selecteer een ontvanger om de chatgeschiedenis in te zien", + "chat.no-users-in-room": "Geen gebruikers in deze chat room", + "chat.recent-chats": "Recent gevoerde gesprekken", + "chat.contacts": "Contacten", + "chat.message-history": "Berichtengeschiedenis", + "chat.message-deleted": "Bericht verwijderd", + "chat.options": "Chat opties", + "chat.pop-out": "Chatvenster opbrengen bij chat", + "chat.minimize": "Verkleinen", + "chat.maximize": "Maximaliseren", + "chat.seven_days": "7 dagen", + "chat.thirty_days": "30 dagen", + "chat.three_months": "3 maanden", + "chat.delete_message_confirm": "Weet je zeker dat je dit bericht wilt verwijderen?", + "chat.retrieving-users": "Gebruikers ophalen...", + "chat.manage-room": "Chat Room beheren", + "chat.add-user-help": "Zoek hier naar gebruikers. Indien geselecteerd word de gebruiker toegevoegd aan de chat. De nieuwe gebruiker kan geen chatberichten zien die geschreven zijn voordat de gebruiker was toegevoegd aan de conversatie. Alleen chatroom-eigenaren () kunnen gebruikers verwijderen uit een chatroom.", + "chat.confirm-chat-with-dnd-user": "Deze gebruiker heeft de status op Niet Storen (DnD, Do not disturb) gezet. Wil je nog steeds een chat starten met deze persoon?", + "chat.rename-room": "Hernoem chatroom", + "chat.rename-placeholder": "Voer hier de naam van je chat room in", + "chat.rename-help": "De naam van de chat room die je hier zet is zichtbaar voor alle deelnemers aan de chat.", + "chat.leave": "Verlaat Chat", + "chat.leave-prompt": "Weet je zeker dat je deze chat wilt verlaten?", + "chat.leave-help": "Als je de chat verlaat zul je toekomstige correspondentie in de chat niet meer zien. Als je later weer wordt toegevoegd, dan kun je de chat geschiedenis tot de hertoevoeging niet zien.", + "chat.in-room": "In deze chat room", + "chat.kick": "Schop", + "chat.show-ip": "Geef IP weer", + "chat.owner": "Chatroom-eigenaar", + "chat.system.user-join": "%1 neemt nu deel aan de chatroom", + "chat.system.user-leave": "%1 heeft de chatroom verlaten", + "chat.system.room-rename": "%2 heeft deze chatroom hernoemd: %1", + "composer.compose": "Samenstellen", + "composer.show_preview": "Voorbeeldweergave", + "composer.hide_preview": "Verberg voorbeeld", + "composer.user_said_in": "%1 zegt in %2:", + "composer.user_said": "%1 zegt:", + "composer.discard": "Bericht plaatsen annuleren?", + "composer.submit_and_lock": "Bericht plaatsen en sluiten", + "composer.toggle_dropdown": "Keuzelijst schakelen", + "composer.uploading": "Uploaden van %1", + "composer.formatting.bold": "Vet", + "composer.formatting.italic": "Cursief", + "composer.formatting.list": "Lijst", + "composer.formatting.strikethrough": "Doorhalen", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload afbeelding", + "composer.upload-file": "Upload bestand", + "composer.zen_mode": "Zen-modus", + "composer.select_category": "Selecteer een categorie", + "composer.textarea.placeholder": "Voer de berichtinhoud hier in, of sleep afbeeldingen hier naartoe", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Annuleren", + "bootbox.confirm": "Bevestig", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Omslag Foto Positionering", + "cover.dragging_message": "Sleep de omslag foto voor de gewilde positie en klik \"Opslaan\"", + "cover.saved": "Omslag foto en positie opgeslagen", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json new file mode 100644 index 0000000000..0025eca31b --- /dev/null +++ b/public/language/nl/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notificaties", + "no_notifs": "Je hebt geen nieuwe notificaties", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Terug naar %1", + "outgoing_link": "Uitgaande Link", + "outgoing_link_message": "U verlaat nu %1", + "continue_to": "Door naar %1", + "return_to": "Terug naar %1", + "new_notification": "U heeft een nieuwe notificatie", + "you_have_unread_notifications": "U heeft ongelezen notificaties.", + "all": "Alles", + "topics": "Onderwerpen", + "replies": "Antwoorden", + "chat": "Chats", + "group-chat": "Groepsgesprekken", + "follows": "Volgt", + "upvote": "Upvotes", + "new-flags": "Nieuwe markeringen", + "my-flags": "Markeringen toegewezen aan mij", + "bans": "Bans", + "new_message_from": "Nieuw bericht van %1", + "upvoted_your_post_in": "%1 heeft voor je bericht gestemd in %2.", + "upvoted_your_post_in_dual": "%1 en %2 hebben voor je bericht gestemd in %3.", + "upvoted_your_post_in_multiple": "%1 en %2 anderen hebben voor je bericht gestemd in %3.", + "moved_your_post": "%1 heeft je bericht verplaatst naar %2", + "moved_your_topic": "%1 heeft %2 verplaatst", + "user_flagged_post_in": "%1 rapporteerde een bericht in %2", + "user_flagged_post_in_dual": "%1 en %2 rapporteerden een bericht in %3", + "user_flagged_post_in_multiple": "%1 en %2 anderen rapporteerden een bericht in %3", + "user_flagged_user": "%1 markeerde een gebruikersprofiel (%2)", + "user_flagged_user_dual": "%1 en %2 markeerden een gebruikersprofiel (%3)", + "user_flagged_user_multiple": "%1 en %2 anderen markeerden een gebruikersprofiel (%3)", + "user_posted_to": "%1 heeft een reactie geplaatst in: %2", + "user_posted_to_dual": "%1 en %2 hebben een reactie geplaatst in: %3", + "user_posted_to_multiple": "%1 en %2 hebben een reactie geplaatst in: %3", + "user_posted_topic": "%1 heeft een nieuw onderwerp geplaatst: %2", + "user_edited_post": "%1 heeft een bericht aangepast in %2", + "user_started_following_you": "%1 volgt jou nu.", + "user_started_following_you_dual": "%1 en %2 volgen jou nu.", + "user_started_following_you_multiple": "%1 en %2 anderen volgen jou nu.", + "new_register": "%1 heeft een registratie verzoek aangevraagd.", + "new_register_multiple": "Er is/zijn %1 registratieverzoek(en) die wacht(en) op goedkeuring.", + "flag_assigned_to_you": "Flag %1 is aan u toegewezen", + "post_awaiting_review": "Bericht wachtend op goedkeuring", + "profile-exported": "%1 profiel geëxporteerd, klik om te downloaden", + "posts-exported": "%1 berichten geëxporteerd, klik om te downloaden", + "uploads-exported": "%1 uploads geëxporteerd, klik om te downloaden", + "users-csv-exported": "Csv met gebruikers geëxporteerd, klik om te downloaden", + "post-queue-accepted": "Je bericht in de wachtrij is geaccepteerd. Klik hier om je bericht te bekijken.", + "post-queue-rejected": "Je bericht in de wachtrij is afgekeurd.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-mailadres bevestigd", + "email-confirmed-message": "Bedankt voor het bevestigen van je e-mailadres. Je account is nu volledig geactiveerd.", + "email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.", + "email-confirm-sent": "Bevestigingsmail verstuurd.", + "none": "Geen", + "notification_only": "Alleen notificatie", + "email_only": "Alleen e-mail", + "notification_and_email": "Notificatie & e-mail", + "notificationType_upvote": "Als iemand positief stemt voor je bericht", + "notificationType_new-topic": "Wanneer iemand die jij volgt een onderwerp post", + "notificationType_new-reply": "Als een nieuwe reactie komt op een onderwerp dat je volgt", + "notificationType_post-edit": "Als een bericht wordt aangepast in een onderwerp dat je volgt", + "notificationType_follow": "Als iemand begint met jou te volgen", + "notificationType_new-chat": "Als je een chat-bericht ontvangt", + "notificationType_new-group-chat": "Als je een bericht uit een groepsgesprek ontvangt.", + "notificationType_group-invite": "Als je een uitnodiging voor een groep ontvangt", + "notificationType_group-leave": "Als een gebruiker je groep verlaat", + "notificationType_group-request-membership": "Als iemand vraagt om lid te worden van een groep waarvan je eigenaar bent", + "notificationType_new-register": "Als iemand wordt toegevoegd aan een registratiewachtrij", + "notificationType_post-queue": "Als een bericht aan de wachtrij wordt toegevoegd", + "notificationType_new-post-flag": "Als een bericht wordt gevlagd", + "notificationType_new-user-flag": "Als een gebruiker wordt gevlagd" +} \ No newline at end of file diff --git a/public/language/nl/pages.json b/public/language/nl/pages.json new file mode 100644 index 0000000000..83bebf19f8 --- /dev/null +++ b/public/language/nl/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Home", + "unread": "Ongelezen onderwerpen", + "popular-day": "Populaire onderwerpen vandaag", + "popular-week": "De populaire onderwerpen van deze week", + "popular-month": "De populaire onderwerpen van deze maand", + "popular-alltime": "De populaire onderwerpen", + "recent": "Recente onderwerpen", + "top-day": "Meest gestemde onderwerpen vandaag", + "top-week": "Meest gestemde onderwerpen van deze week", + "top-month": "Meest gestemde onderwerpen van deze maand", + "top-alltime": "Meest gestemde onderwerpen", + "moderator-tools": "Moderator gereedschappen", + "flagged-content": "Gemarkeerde content", + "ip-blacklist": "IP zwarte lijst", + "post-queue": "Berichten wachtrij", + "users/online": "Online Gebruikers", + "users/latest": "Meest recente gebruikers", + "users/sort-posts": "Gebruikers met de meeste berichten", + "users/sort-reputation": "Gebruikers met de meeste reputatie", + "users/banned": "Verbannen Gebruikers", + "users/most-flags": "Meest gemarkeerde gebruikers", + "users/search": "Zoek Gebruiker", + "notifications": "Notificaties", + "tags": "Tags", + "tag": "Onderwerpen getagd onder "%1"", + "register": "Registeer een gebruikersaccount", + "registration-complete": "Registratie compleet", + "login": "Login met uw gebruikersaccount", + "reset": "Gebruikerswachtwoord opnieuw instellen", + "categories": "Categorieën", + "groups": "Groepen", + "group": "%1's groep", + "chats": "Chats", + "chat": "Chatten met %1", + "flags": "Markeringen", + "flag-details": "Markering %1 details", + "account/edit": "\"%1\" aanpassen", + "account/edit/password": "Wachtwoord van \"%1\" aanpassen", + "account/edit/username": "Gebruikersnaam van \"%1\" aanpassen", + "account/edit/email": "Email van \"%1\" aanpassen", + "account/info": "Gebruikersinformatie", + "account/following": "Door %1 gevolgd", + "account/followers": "Die %1 volgen", + "account/posts": "Berichten geplaatst door %1", + "account/latest-posts": "Meest recente berichten door %1", + "account/topics": "Onderwerpen begonnen door %1", + "account/groups": "%1's groepen", + "account/watched_categories": "Gevolgde Categorieën van %1", + "account/bookmarks": "%1's Favoriete Berichten", + "account/settings": "Gebruikersinstellingen", + "account/watched": "Onderwerpen die door %1 bekeken worden", + "account/ignored": "Onderwerpen genegeerd door %1", + "account/upvoted": "Berichten omhoog gestemd door %1", + "account/downvoted": "Berichten omlaag gestemd door %1", + "account/best": "Beste berichten geplaast door %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Gebruikers geblokkeerd door %1", + "account/uploads": "Uploads door %1", + "account/sessions": "Login sessies", + "confirm": "Email Bevestigd", + "maintenance.text": "%1 is momenteel in onderhoud. Excuses voor het ongemak en probeer het later nog eens.", + "maintenance.messageIntro": "Daarnaast heeft de beheerder het volgende bericht achtergelaten:", + "throttled.text": "%1 is momenteel niet beschikbaar door overmatig gebruikt. Excuses voor het ongemak en probeer het later nog eens." +} \ No newline at end of file diff --git a/public/language/nl/post-queue.json b/public/language/nl/post-queue.json new file mode 100644 index 0000000000..67cbe01a67 --- /dev/null +++ b/public/language/nl/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Berichtenwachtrij", + "description": "Er zijn geen berichten in de wachtrij.
Om deze functionaliteit in te schakelen, ga naar Instellingen → Bericht → Berichtenwachtrij en schakel Berichtenwachtrij in.", + "user": "Gebruiker", + "category": "Categorie", + "title": "Titel", + "content": "Inhoud", + "posted": "Geplaatst", + "reply-to": "Antwoord naar \"%1\"", + "content-editable": "Klik op inhoud om aan te passen", + "category-editable": "Klik op categorie om aan te passen", + "title-editable": "Klik op titel om aan te passen", + "reply": "Antwoorden", + "topic": "Onderwerp", + "accept": "Accepteren", + "reject": "Afkeuren", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/nl/recent.json b/public/language/nl/recent.json new file mode 100644 index 0000000000..db7c18badc --- /dev/null +++ b/public/language/nl/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recent", + "day": "Dag", + "week": "Week", + "month": "Maand", + "year": "Jaar", + "alltime": "altijd", + "no_recent_topics": "Er zijn geen recente onderwerpen.", + "no_popular_topics": "Er zijn geen populaire onderwerpen.", + "there-is-a-new-topic": "Er is een nieuw onderwerp", + "there-is-a-new-topic-and-a-new-post": "Er is een nieuw onderwerp en een nieuw bericht.", + "there-is-a-new-topic-and-new-posts": "Er is een nieuwe onderwerp en %1 nieuwe berichten", + "there-are-new-topics": "Er zijn %1 nieuwe onderwerpen", + "there-are-new-topics-and-a-new-post": "Er zijn %1 nieuwe onderwerpen en een nieuw bericht.", + "there-are-new-topics-and-new-posts": "Er zijn %1 nieuwe onderwerpen en %2 nieuwe berichten.", + "there-is-a-new-post": "Er is een nieuw bericht.", + "there-are-new-posts": "Er zijn %1 nieuwe berichten.", + "click-here-to-reload": "Klik hier om te herladen." +} \ No newline at end of file diff --git a/public/language/nl/register.json b/public/language/nl/register.json new file mode 100644 index 0000000000..58ea9503d7 --- /dev/null +++ b/public/language/nl/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registreren", + "cancel_registration": "Annuleer registratie", + "help.email": "E-mailadressen zijn standaard verborgen voor andere gebruikers.", + "help.username_restrictions": "Een unieke gebruikersnaam tussen %1 en %2 karakters. Anderen kunnen je vermelden met @gebruikersnaam.", + "help.minimum_password_length": "Je wachtwoord moet tenminste %1 karakters lang zijn.", + "email_address": "E-mailadres", + "email_address_placeholder": "Vul e-mailadres in", + "username": "Gebruikersnaam", + "username_placeholder": "Vul Gebruikersnaam in", + "password": "Wachtwoord", + "password_placeholder": "Vul Wachtwoord in", + "confirm_password": "Bevestig Wachtwoord", + "confirm_password_placeholder": "Bevestig Wachtwoord", + "register_now_button": "Nu Registreren", + "alternative_registration": "Alternatieve registratie", + "terms_of_use": "Gebruiksvoorwaarden", + "agree_to_terms_of_use": "Ik ga akkoord met de gebruiksvoorwaarden", + "terms_of_use_error": "Je moet akkoord gaan met de service voorwaarden.", + "registration-added-to-queue": "Het registratieverzoek is toegevoegd aan de wachtrij. Een bericht wordt naar het opgegeven emailadres gestuurd wanneer de registratie is goedgekeurd.", + "registration-queue-average-time": "Onze gemiddelde tijd om lidmaatschappen goed te keuren is %1 uur %2 minuten.", + "registration-queue-auto-approve-time": "Je lidmaatschap op dit forum zal volledig worden geactiveerd binnen maximaal %1 uur.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Ik geef toestemming voor het verzamelen en verwerken van mijn persoonlijke informatie op deze website.", + "gdpr_agree_email": "Ik geef toestemming voor het verzenden van samenvattingen en notificaties per e-mail van deze website.", + "gdpr_consent_denied": "Deze website heeft uw toestemming nodig voor het verzamelen en verwerken van uw gegevens, en voor het verzenden van e-mails.", + "invite.error-admin-only": "Directe gebruikersregistratie is uitgeschakeld. Neem contact op met de beheerder voor meer details.", + "invite.error-invite-only": "Directe gebruikersregistratie is uitgeschakeld. Je moet uitgenodigd worden door een bestaande gebruiker om dit forum te kunnen gebruiken.", + "invite.error-invalid-data": "De ontvangen registratiegegevens corresponderen niet met onze gegevens. Neem contact op met de beheerder voor meer details." +} \ No newline at end of file diff --git a/public/language/nl/reset_password.json b/public/language/nl/reset_password.json new file mode 100644 index 0000000000..17cc9661bb --- /dev/null +++ b/public/language/nl/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Wachtwoord opnieuw instellen", + "update_password": "Wachtwoord bijwerken", + "password_changed.title": "Wachtwoord gewijzigd", + "password_changed.message": "

Wachtwoord is met succes hersteld. Opnieuw inloggen.", + "wrong_reset_code.title": "Onjuiste herstelcode", + "wrong_reset_code.message": "Opgegeven code voor wachtwoordherstel is niet juist. Probeer het opnieuw of vraag een andere code aan.", + "new_password": "Nieuw wachtwoord", + "repeat_password": "Bevestiging wachtwoord", + "changing_password": "Wachtwoord wordt gewijzigd", + "enter_email": "Geef het e-mailadres op dat tijdens registratie gebruikt is, en we versturen je een bericht met vervolginstructies voor het ontgrendelen van de account.", + "enter_email_address": "Geef het e-mailadres op", + "password_reset_sent": "Indien het opgegeven adres overeenkomt met een bestaande gebruikersaccount dan is er nu een wachtwoord reset mail verstuurd. Houd er rekening mee dat er slechts één e-mail per minuut zal worden verstuurd.", + "invalid_email": "Onbekend e-mailadres!", + "password_too_short": "Het opgegeven wachtwoord bevat te weinig tekens. Kies een veiliger wachtwoord met meer tekens.", + "passwords_do_not_match": "De twee opgegeven wachtwoorden komen niet overeen", + "password_expired": "Het huidige wachtwoord is verlopen en er dient een nieuwe gekozen te worden" +} \ No newline at end of file diff --git a/public/language/nl/search.json b/public/language/nl/search.json new file mode 100644 index 0000000000..96796a92fe --- /dev/null +++ b/public/language/nl/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 overeenkomstige resultaten \"%2\", (%3 seconds)", + "no-matches": "Geen overeenkomstige resultaten gevonden", + "advanced-search": "Geavanceerd zoeken", + "in": "in", + "titles": "Titels", + "titles-posts": "Titels en berichten", + "match-words": "Match woorden", + "all": "Alle", + "any": "Enkele", + "posted-by": "Geplaatst door", + "in-categories": "In categorieën", + "search-child-categories": "Doorzoek subcategorieën ", + "has-tags": "Is getagged", + "reply-count": "Aantal reacties", + "at-least": "op zijn minst", + "at-most": "op zijn meest", + "relevance": "Relevantie", + "post-time": "Geplaatst op", + "votes": "Stemmen", + "newer-than": "Nieuwer dan", + "older-than": "Ouder dan", + "any-date": "Elke datum", + "yesterday": "Gisteren", + "one-week": "Eén week", + "two-weeks": "Twee weken", + "one-month": "Eén maand", + "three-months": "Drie maanden", + "six-months": "Zes maanden", + "one-year": "Eén jaar", + "sort-by": "Sorteer op", + "last-reply-time": "Laatste keer geantwoord", + "topic-title": "Onderwerp", + "topic-votes": "Stemmen op onderwerp", + "number-of-replies": "Aantal antwoorden", + "number-of-views": "Aantal keer bekeken", + "topic-start-date": "Onderwerp gestart op datum", + "username": "Gebruikersnaam", + "category": "Categorie", + "descending": "In aflopende volgorde", + "ascending": "In oplopende volgorde", + "save-preferences": "Bewaar voorkeuren", + "clear-preferences": "Voorkeuren verwijderen", + "search-preferences-saved": "Zoek voorkeuren opgeslagen", + "search-preferences-cleared": "Zoek voorkeuren verwijderd", + "show-results-as": "Toon resultaten als", + "see-more-results": "Meer resultaten zien (%1)", + "search-in-category": "Zoeken in \"%1\"" +} \ No newline at end of file diff --git a/public/language/nl/success.json b/public/language/nl/success.json new file mode 100644 index 0000000000..3d38977677 --- /dev/null +++ b/public/language/nl/success.json @@ -0,0 +1,7 @@ +{ + "success": "Geslaagd", + "topic-post": "Je bericht is met succes geplaatst.", + "post-queued": "Je bericht staat in de wachtrij voor goedkeuring. Je krijgt een notificatie als deze wordt geaccepteerd of afgewezen.", + "authentication-successful": "Aanmelden geslaagd", + "settings-saved": "Instellingen opgeslagen!" +} \ No newline at end of file diff --git a/public/language/nl/tags.json b/public/language/nl/tags.json new file mode 100644 index 0000000000..d94000276b --- /dev/null +++ b/public/language/nl/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Er zijn geen onderwerpen met deze tag.", + "tags": "Tags", + "enter_tags_here": "Voeg hier tags toe, tussen de %1 en %2 tekens per stuk.", + "enter_tags_here_short": "Voer tags in...", + "no_tags": "Er zijn nog geen tags geplaatst", + "select_tags": "Selecteer tags" +} \ No newline at end of file diff --git a/public/language/nl/top.json b/public/language/nl/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/nl/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json new file mode 100644 index 0000000000..ebf28d38f5 --- /dev/null +++ b/public/language/nl/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Onderwerp", + "title": "Title", + "no_topics_found": "Geen onderwerpen gevonden!", + "no_posts_found": "Geen berichten gevonden!", + "post_is_deleted": "Dit bericht is verwijderd!", + "topic_is_deleted": "Dit onderwerp is verwijderd!", + "profile": "Profiel", + "posted_by": "Geplaatst door %1", + "posted_by_guest": "Geplaatst door gast", + "chat": "Chat", + "notify_me": "Krijg een melding wanneer nieuwe reacties volgen", + "quote": "Citeren", + "reply": "Reageren", + "replies_to_this_post": "%1 Antwoorden", + "one_reply_to_this_post": "1 Antwoord", + "last_reply_time": "Laatste antwoord", + "reply-as-topic": "Reageren als onderwerp", + "guest-login-reply": "Aanmelden om te reageren", + "login-to-view": "🔒 Aanmelden om te zien", + "edit": "Aanpassen", + "delete": "Verwijderen", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Opschonen", + "restore": "Herstellen", + "move": "Verplaatsen", + "change-owner": "Wijzig eigenaar", + "fork": "Afsplitsen", + "link": "Link", + "share": "Delen", + "tools": "Extra", + "locked": "Gesloten", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Verplaatst", + "moved-from": "Moved from %1", + "copy-ip": "Kopieer IP", + "ban-ip": "Verban IP", + "view-history": "Revisie geschiedenis", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Klik hier om terug te keren naar de laatst gelezen post in deze thread.", + "flag-post": "Rapporteer dit bericht", + "flag-user": "Rapporteer deze gebruiker", + "already-flagged": "Al gerapporteerd", + "view-flag-report": "Rapportage inzien", + "resolve-flag": "Resolve Flag", + "merged_message": "Dit onderwerp is samengevoegd met %2", + "deleted_message": "Dit onderwerp is verwijderd. Alleen gebruikers met beheerrechten op onderwerpniveau kunnen dit inzien.", + "following_topic.message": "Vanaf nu worden meldingen ontvangen zodra iemand een reactie op dit onderwerp geeft.", + "not_following_topic.message": "Dit onderwerp zal verschijnen in de lijst van ongelezen onderwerpen, maar er zullen geen meldingen ontvangen zodra iemand een reactie op dit onderwerp geeft.", + "ignoring_topic.message": "Dit onderwerp zal niet meer verschijnen in de lijst van ongelezen berichten. U zult enkel een melding ontvangen wanneer u wordt genoemd, of wanneer er een positieve stem op uw reactie wordt gegeven.", + "login_to_subscribe": "Log in or registreer om dit onderwerp te volgen.", + "markAsUnreadForAll.success": "Onderwerp is voor iedereen als ongelezen gemarkeerd.", + "mark_unread": "Ongelezen markeren", + "mark_unread.success": "Onderwerp is als ongelezen gemarkeerd.", + "watch": "Volgen", + "unwatch": "Niet meer volgen", + "watch.title": "Krijg meldingen van nieuwe reacties op dit onderwerp", + "unwatch.title": "Dit onderwerp niet langer volgen", + "share_this_post": "Deel dit bericht", + "watching": "Gevolgd", + "not-watching": "Niet gevolgd", + "ignoring": "Genegeerd", + "watching.description": "Stuur me een melding bij nieuwe reacties.
Toon onderwerp bij de ongelezen onderwerpen.", + "not-watching.description": "Stuur me geen melding van nieuwe reacties.
Toon onderwerp in ongelezen mits de categorie niet genegeerd wordt.", + "ignoring.description": "Stuur me geen melding van nieuwe reacties.
Toon dit onderwerp niet onder de ongelezen onderwerpen.", + "thread_tools.title": "Acties", + "thread_tools.markAsUnreadForAll": "Alles als ongelezen markeren", + "thread_tools.pin": "Onderwerp vastpinnen", + "thread_tools.unpin": "Onderwerp losmaken", + "thread_tools.lock": "Onderwerp sluiten", + "thread_tools.unlock": "Onderwerp openen", + "thread_tools.move": "Onderwerp verplaatsen", + "thread_tools.move-posts": "Verplaats berichten", + "thread_tools.move_all": "Verplaats alles", + "thread_tools.change_owner": "Wijzig eigenaar", + "thread_tools.select_category": "Selecteer categorie", + "thread_tools.fork": "Onderwerp afsplitsen", + "thread_tools.delete": "Onderwerp verwijderen", + "thread_tools.delete-posts": "Verwijder berichten", + "thread_tools.delete_confirm": "Weet u het zeker dat u dit onderwerp wilt verwijderen?", + "thread_tools.restore": "Onderwerp herstellen", + "thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?", + "thread_tools.purge": "Wis onderwerp ", + "thread_tools.purge_confirm": "Weet je zeker dat je dit onderwerp wil verwijderen?", + "thread_tools.merge_topics": "Onderwerpen samenvoegen", + "thread_tools.merge": "Samenvoegen", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Verplaatsen onderwerp ongedaan gemaakt", + "topic_move_posts_success": "Berichten zullen spoedig verplaatst worden. Klik hier om ongedaan te maken.", + "topic_move_posts_undone": "Verplaatsen bericht ongedaan gemaakt", + "post_delete_confirm": "Is het absoluut de bedoeling dit bericht te verwijderen?", + "post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?", + "post_purge_confirm": "Is het absoluut zeker dat dit bericht volledig verwijderd kan worden?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Categorieën laden", + "confirm_move": "Verplaatsen", + "confirm_fork": "Splits", + "bookmark": "Favoriet", + "bookmarks": "Favorieten", + "bookmarks.has_no_bookmarks": "Je hebt nog geen berichten aan je favorieten toegevoegd.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Meer berichten laden...", + "move_topic": "Onderwerp verplaatsen", + "move_topics": "Verplaats onderwerpen", + "move_post": "Bericht verplaatsen", + "post_moved": "Bericht verplaatst!", + "fork_topic": "Afgesplitst onderwerp ", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Klik op de berichten die afgesplitst moeten worden", + "fork_no_pids": "Geen berichten geselecteerd!", + "no-posts-selected": "Geen berichten geselecteerd!", + "x-posts-selected": "%1 bericht(en) geselecteerd", + "x-posts-will-be-moved-to-y": "%1 bericht(en) zullen verplaatst worden naar \"%2\"", + "fork_pid_count": "%1 bericht(en) geselecteerd", + "fork_success": "Onderwerp is succesvol afgesplitst. Klik hier om het nieuwe onderwerp te zien.", + "delete_posts_instruction": "Klik op de berichten die verwijderd moeten worden", + "merge_topics_instruction": "Klik op de onderwerpen die je samen wilt voegen, of zoek daarnaar", + "merge-topic-list-title": "Lijst van onderwerpen die samengevoegd zullen worden", + "merge-options": "Opties voor samenvoegen", + "merge-select-main-topic": "Selecteer het hoofdonderwerp", + "merge-new-title-for-topic": "Nieuwe titel voor onderwerp", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Klik op de berichten die je wilt toewijzen aan een andere gebruiker", + "composer.title_placeholder": "Voer hier de titel van het onderwerp in...", + "composer.handle_placeholder": "Voer je naam/pseudoniem hier in", + "composer.discard": "Annuleren", + "composer.submit": "Verzenden", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Reactie op %1", + "composer.new_topic": "Nieuw onderwerp", + "composer.editing": "Editing", + "composer.uploading": "uploaden...", + "composer.thumb_url_label": "Plak een URL naar een miniatuurweergave voor dit onderwerp", + "composer.thumb_title": "Voeg een miniatuurweergave toe aan dit onderwerp", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Of upload een bestand", + "composer.thumb_remove": "Velden leegmaken", + "composer.drag_and_drop_images": "Sleep en zet afbeeldingen hier", + "more_users_and_guests": "%1 of meerdere gebruiker(s) en %2 gast(en)", + "more_users": "%1 meer gebruiker(s)", + "more_guests": "%1 of meerdere gast(en)", + "users_and_others": "%1 en %2 anderen", + "sort_by": "Sorteer op", + "oldest_to_newest": "Oudste berichten bovenaan", + "newest_to_oldest": "Meest recente berichten bovenaan", + "most_votes": "Meeste stemmen", + "most_posts": "Meeste berichten", + "most_views": "Most Views", + "stale.title": "Een nieuw onderwerp maken in de plaats?", + "stale.warning": "Het onderwerp waar je op antwoord is vrij oud. Zou je graag een nieuw onderwerp maken met een referentie naar dit onderwerp in je antwoord?", + "stale.create": "Maak een nieuw onderwerp", + "stale.reply_anyway": "Reageer toch op dit onderwerp", + "link_back": "Re: [%1](%2)", + "diffs.title": "Bericht revisie geschiedenis", + "diffs.description": "Dit bericht heeft %1 revisies. Klik op een revisie hieronder om het bericht te zien op dat punt in de tijd.", + "diffs.no-revisions-description": "Dit bericht heeft %1 revisies.", + "diffs.current-revision": "Huidige revisie", + "diffs.original-revision": "Oorspronkelijke revisie", + "diffs.restore": "Herstel deze revisie", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Bericht was succesvol hersteld naar een eerdere revisie", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 eerder", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/nl/unread.json b/public/language/nl/unread.json new file mode 100644 index 0000000000..ae12c4b9e5 --- /dev/null +++ b/public/language/nl/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Ongelezen", + "no_unread_topics": "Er zijn geen ongelezen onderwerpen.", + "load_more": "Meer laden", + "mark_as_read": "Markeer als gelezen", + "selected": "Geselecteerd", + "all": "Alles", + "all_categories": "Alle categorieën", + "topics_marked_as_read.success": "Onderwerp gemarkeerd als gelezen!", + "all-topics": "Alle onderwerpen", + "new-topics": "Nieuwe onderwerpen", + "watched-topics": "Bekeken onderwerpen", + "unreplied-topics": "Onbeantwoorde onderwerpen", + "multiple-categories-selected": "Meerdere geselecteerd" +} \ No newline at end of file diff --git a/public/language/nl/uploads.json b/public/language/nl/uploads.json new file mode 100644 index 0000000000..9c0ec5fd73 --- /dev/null +++ b/public/language/nl/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Bestand word geüpload...", + "select-file-to-upload": "Selecteer een bestand om te uploaden!", + "upload-success": "Bestand succesvol geüpload!", + "maximum-file-size": "Maximaal %1 kb", + "no-uploads-found": "Geen uploads gevonden", + "public-uploads-info": "Uploads zijn publiek, alle bezoekers en gasten kunnen ze zien.", + "private-uploads-info": "Uploads zijn afgeschermd, alleen ingelogde gebruikers kunnen ze zien." +} \ No newline at end of file diff --git a/public/language/nl/user.json b/public/language/nl/user.json new file mode 100644 index 0000000000..8756d8e099 --- /dev/null +++ b/public/language/nl/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Verbannen", + "muted": "Muted", + "offline": "Offline", + "deleted": "Verwijderd", + "username": "Gebruikersnaam", + "joindate": "Datum van registratie", + "postcount": "Aantal geplaatste berichten", + "email": "E-mail", + "confirm_email": "Bevestig e-mail", + "account_info": "Gebruikersinformatie", + "admin_actions_label": "Administratieve acties", + "ban_account": "Verban gebruiker", + "ban_account_confirm": "Weet u zeker dat u deze gebruiker wilt verbannen?", + "unban_account": "Verbanning intrekken", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Account verwijderen", + "delete_account_as_admin": "Account verwijderen", + "delete_content": "Account Content verwijderen", + "delete_all": "Account en Content verwijderen", + "delete_account_confirm": "Weet je zeker dat je jouw berichten wilt anonimiseren en je account wilt verwijderen?
Deze actie is onomkeerbaar en je data kan niet meer hersteld worden

Voer je wachtwoord in om te bevestigen dat je je account wilt vernietigen.", + "delete_this_account_confirm": "Weet je zeker dat je dit account wilt verwijderen met achterlaten van de inhoud?
Deze actie is onomkeerbaar, berichten worden geanonimizeerd en je kunt associates van deze berichten met het verwijderde account niet meer herstellen

", + "delete_account_content_confirm": "Weet je zeker dat je de inhoud (berichten, onderwerpen, uploads) van dit account wilt verwijderen?
Deze actie is onomkeerbaar en je kunt de gegevens niet meer terugkrijgen

", + "delete_all_confirm": "Weet je zeker dat je dit account wilt verwijderen, inclusief alle inhoud (berichten, onderwerpen, uploads) van dit account?
Deze actie is onomkeerbaar en je kunt de gegevens niet meer terugkrijgen

", + "account-deleted": "Account verwijderd", + "account-content-deleted": "Account inhoud verwijderd", + "fullname": "Volledige naam", + "website": "Website", + "location": "Locatie", + "age": "Leeftijd", + "joined": "Geregistreerd", + "lastonline": "Laatste keer online gezien", + "profile": "Profiel", + "profile_views": "Bekeken", + "reputation": "Reputatie", + "bookmarks": "Favorieten", + "watched_categories": "Gevolgde categorieën", + "change_all": "Wijzig alles", + "watched": "Bekeken", + "ignored": "Genegeerd", + "default-category-watch-state": "Standaard bekeken status van categorie", + "followers": "Volgers", + "following": "Volgend", + "blocks": "Blokkeringen", + "block_toggle": "Toggle Blokkering", + "block_user": "Blokkeer gebruiker", + "unblock_user": "Deblokkeer gebruiker", + "aboutme": "Over mij", + "signature": "Handtekening", + "birthday": "Verjaardag", + "chat": "Chat", + "chat_with": "Chat verder met %1", + "new_chat_with": "Begin een chat met %1", + "flag-profile": "Profiel vlaggen", + "follow": "Volgen", + "unfollow": "Ontvolgen", + "more": "Meer", + "profile_update_success": "Het gebruikersprofiel is met succes gewijzigd", + "change_picture": "Bewerk afbeelding", + "change_username": "Wijzig gebruikersnaam", + "change_email": "Wijzig email", + "email_same_as_password": "Voer uw huidige wachtwoord in om door te gaan – u heeft uw nieuwe e-mail weer ingevoered", + "edit": "Bewerken", + "edit-profile": "Profiel wijzigen", + "default_picture": "Standaard icoon", + "uploaded_picture": "Geüploade afbeelding", + "upload_new_picture": "Nieuwe afbeelding opsturen", + "upload_new_picture_from_url": "Nieuwe afbeelding vanaf een URL toevoegen", + "current_password": "Huidige wachtwoord", + "change_password": "Wijzig wachtwoord", + "change_password_error": "Ongeldig wachtwoord!", + "change_password_error_wrong_current": "Het opgegeven huidige wachtwoord is onjuist!", + "change_password_error_match": "Het eerder opgegeven wachtwoord komt niet overeen!", + "change_password_error_privileges": "Niet geautoriseerd om dit wachtwoord te mogen wijzigen.", + "change_password_success": "Het wachtwoord is gewijzigd!", + "confirm_password": "Bevestig wachtwoord", + "password": "Wachtwoord", + "username_taken_workaround": "Helaas, de gewenste gebruikersnaam is al door iemand in gebruik genomen dus vandaar een kleine aanpassing naar %1 doorgevoerd", + "password_same_as_username": "Je wachtwoord is hetzelfde als je gebruikersnaam. Kies een ander wachtwoord.", + "password_same_as_email": "Je wachtwoord is hetzelfde als je email, kies alsjeblieft een ander wachtwoord.", + "weak_password": "Zwak wachtwoord.", + "upload_picture": "Upload afbeelding", + "upload_a_picture": "Upload een afbeelding", + "remove_uploaded_picture": "Verwijder gëuploade foto", + "upload_cover_picture": "Upload je coverafbeelding", + "remove_cover_picture_confirm": "Weet u zeker dat u de cover foto wilt verwijderen?", + "crop_picture": "Foto bijsnijden", + "upload_cropped_picture": "Bijsnijden en uploaden", + "avatar-background-colour": "Achtergrondkeur van avatar", + "settings": "Instellingen", + "show_email": "E-mailadres weergeven", + "show_fullname": "Laat mijn volledige naam zien", + "restrict_chats": "Sta alleen chatsessies toe van gebruikers die ik zelf volg", + "digest_label": "Abonneer op een samenvatting", + "digest_description": "Abonneer op periodieke e-mail updates van onderwerpen in dit forum", + "digest_off": "Uit", + "digest_daily": "Dagelijks", + "digest_weekly": "Wekelijks", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Maandelijks", + "has_no_follower": "Deze gebruiker heeft geen volgers :(", + "follows_no_one": "Deze gebruiker volgt niemand :(", + "has_no_posts": "Deze gebruiker heeft nog geen berichten geplaatst", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Deze gebruiker heeft nog geen onderwerpen gestart.", + "has_no_watched_topics": "Deze gebruiker heeft nog geen onderwerpen gevolgd.", + "has_no_ignored_topics": "Deze gebruiker heeft nog geen berichten genegeerd.", + "has_no_upvoted_posts": "Deze gebruiker heeft nog geen berichten omhoog gestemd.", + "has_no_downvoted_posts": "Deze gebruiker heeft nog geen berichten omlaag gestemd.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "U hebt geen gebruikers geblokkeerd", + "email_hidden": "E-mail niet beschikbaar", + "hidden": "verborgen", + "paginate_description": "Blader door onderwerpen en berichten in plaats van oneindig scrollen.", + "topics_per_page": "Onderwerpen per pagina", + "posts_per_page": "Berichten per pagina", + "max_items_per_page": "Maximaal %1", + "acp_language": "Taal van Admin Pagina", + "notifications": "Notificaties", + "upvote-notif-freq": "Notificatie frequentie voor Upvotes", + "upvote-notif-freq.all": "Alle Upvotes", + "upvote-notif-freq.first": "Eerst per bericht", + "upvote-notif-freq.everyTen": "Elke tien Upvotes", + "upvote-notif-freq.threshold": "Bij 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Bij 10, 100, 1000...", + "upvote-notif-freq.disabled": "Uitgeschakeld", + "browsing": "Instellingen voor bladeren", + "open_links_in_new_tab": "Open uitgaande links naar een externe site in een nieuw tabblad", + "enable_topic_searching": "Inschakelen mogelijkheid op onderwerp te kunnen zoeken", + "topic_search_help": "Wanneer ingeschakeld zal de standaard zoekfunctie, met een aangepaste methode voor onderwerpen, overschreven worden", + "update_url_with_post_index": "Werk url bij met de index van het bericht tijdens het browsen van onderwerpen", + "scroll_to_my_post": "Toon het nieuwe bericht na het plaatsen van een antwoord", + "follow_topics_you_reply_to": "Ontvang meldingen van berichten waar je op hebt gereageerd", + "follow_topics_you_create": "Ontvang meldingen van berichten die je hebt gemaakt", + "grouptitle": "Groepstitel", + "group-order-help": "Selecteer een groep en gebruik de pijltjes om titels te sorteren", + "no-group-title": "Geen groepstitel", + "select-skin": "Selecteer een skin", + "select-homepage": "Selecteer een startpagina", + "homepage": "Startpagina", + "homepage_description": "Selecteer een pagina om te gebruiken als startpagina, of selecteer geen om de standaard pagina te gebruiken.", + "custom_route": "Aangepaste startpagina route", + "custom_route_help": "Geef hier een routenaam op, zonder voorgevoegde slash (b.v. \"recent\" of \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Geassocieerd met", + "sso.not-associated": "Klik hier om geassocieerd te worden met", + "sso.dissociate": "Ontkoppelen", + "sso.dissociate-confirm-title": "Bevestig ontkoppeling", + "sso.dissociate-confirm": "Weet u zeker dat u uw account wilt ontkoppelen van %1?", + "info.latest-flags": "Laatste markeringen", + "info.no-flags": "Geen gemarkeerde berichten gevonden", + "info.ban-history": "Recente verban-geschiedenis", + "info.no-ban-history": "Deze gebruiker is nooit eerder verbannen", + "info.banned-until": "Verbannen tot %1", + "info.banned-expiry": "Verloopt", + "info.banned-permanently": "Voor altijd verbannen", + "info.banned-reason-label": "Reden", + "info.banned-no-reason": "Geen reden opgegeven", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Gebruikersnaam geschiedenis", + "info.email-history": "Email geschiedenis", + "info.moderation-note": "Moderatie notitie", + "info.moderation-note.success": "Moderatie notitie opgeslagen", + "info.moderation-note.add": "Notitie toevoegen", + "sessions.description": "Deze pagina staat je toe om elke actieve sessie op dit forum te zien en om deze af te breken indien nodig. Je kunt je eigen sessie afbreken door uit te loggen uit je account. ", + "consent.title": "Uw Rechten & Toestemmingen", + "consent.lead": "Dit gemeenschapsforum verzamelt en verwerkt uw persoonlijke informatie.", + "consent.intro": "We gebruiken deze informatie strikt om uw ervaring in deze gemeenschap te personaliseren, en om de berichten die u maakt te koppelen aan uw gebruikersaccount. Tijdens de registratiestap is aan u gevraagd een gebruikersnaam en een e-mail adres te geven, u kan bovendien optioneel aanvullende informatie toevoegen om uw gebruikersprofiel op deze website te completeren.

We bewaren deze informatie voor de levensduur van uw gebruikersaccount, u kan uw toestemming op ieder moment intrekken door uw gebruikersaccount te verwijderen. Op ieder moment kan u een kopie opvragen van uw bijdrage aan deze website via uw Rechten & Toestemmingen pagina.

Indien u vragen of zorgen heeft, bent u welkom om vragen te stellen aan de beheerders van het forum.", + "consent.email_intro": "Af en toe kunnen we berichten mailen naar uw geregistreerde e-mail adres om voortgang te melden en/of om te notificeren dat er nieuwe activiteit is die op u van toepassing is. U kan de frequentie van de samenvattingsmail aanpassen (en geheel uitschakelen) en selecteren welke notificatiesoorten u per e-mail wilt ontvangen, via uw gebruikers instellingen pagina.", + "consent.digest_frequency": "Tenzij expliciet aangepast in uw gebruikers instellingen, stuurt het forum u iedere %1 een samenvattingsmail.", + "consent.digest_off": "Tenzij expliciet aangepast in uw gebruikers instellingen, stuurt het forum u geen samenvattingsmail.", + "consent.received": "U hebt toestemming gegeven dat deze website uw informatie verzamelt en verwerkt. Er is geen aanvullende actie vereist.", + "consent.not_received": "U hebt geen toestemming gegeven voor het verzamelen en verwerken van informatie. Op ieder moment kan de website administrator besluiten uw account te verwijderen om te voldoen aan de General Data Protection Regulation. ", + "consent.give": "Geef toestemming", + "consent.right_of_access": "U hebt Recht op Toegang", + "consent.right_of_access_description": "U hebt het recht op toegang tot al uw gegevens die door deze website zijn verzameld. U kan een kopie van deze data krijgen door te klikken op de van toepassing zijnde knop hieronder.", + "consent.right_to_rectification": "U hebt Recht op Correctie", + "consent.right_to_rectification_description": "U hebt het recht om gegevens die u aan ons hebt verstrekt te wijzigen of te vernieuwen. Uw profiel kan u vernieuwen door uw profiel te bewerken, en bericht inhoud kan altijd worden gewijzigd. Als dit niet het geval is neem dan contact op met het administrator team.", + "consent.right_to_erasure": "U hebt Recht op Verwijdering", + "consent.right_to_erasure_description": "Uw toestemming om gegevens te verzamelen en te verwerken kunt u op ieder moment intrekken door uw account te verwijderen. Uw individuele profiel kan worden verwijderd hoewel uw berichten blijven staan. Als u zowel uw account en uw berichten wilt verwijderen, neem dan contact op met het administrator team van deze website.", + "consent.right_to_data_portability": "U hebt Recht op Dataportabiliteit", + "consent.right_to_data_portability_description": "U kan van ons machine-leesbare export opvragen van verzamelde gegevens van u en uw account. U kan dit doen door te klikken op de van toepassing zijnde knop hieronder.", + "consent.export_profile": "Exporteer profiel (.json)", + "consent.export-profile-success": "Profiel wordt geëxporteerd. Je zult een notificatie krijgen als dit is voltooid.", + "consent.export_uploads": "Exporteer geuploade inhoud (.zip)", + "consent.export-uploads-success": "Uploads worden geëxporteerd. Je zult een notificatie krijgen als dit is voltooid.", + "consent.export_posts": "Exporteer berichten (.csv)", + "consent.export-posts-success": "Berichten worden geëxporteerd. Je zult een notificatie krijgen als dit is voltooid.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/nl/users.json b/public/language/nl/users.json new file mode 100644 index 0000000000..0735b49989 --- /dev/null +++ b/public/language/nl/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Laatste gebruikers", + "top_posters": "Meest actieve leden", + "most_reputation": "Meeste reputatie", + "most_flags": "Meeste vlaggen", + "search": "Zoeken", + "enter_username": "Vul een gebruikersnaam in om te zoeken", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Meer laden...", + "users-found-search-took": "%1 gebruiker(s) gevonden! Zoekactie duurde %2 seconden.", + "filter-by": "Filter op", + "online-only": "Online ", + "invite": "Uitnodigen", + "prompt-email": "E-mails:", + "groups-to-join": "Groepen om aan deel te nemen als de uitnodiging wordt geaccepteerd:", + "invitation-email-sent": "Een uitnodiging email is verstuurd naar %1 ", + "user_list": "Ledenlijst", + "recent_topics": "Recente onderwerpen", + "popular_topics": "Populaire onderwerpen", + "unread_topics": "Ongelezen onderwerpen", + "categories": "Categorieën", + "tags": "Tags", + "no-users-found": "Geen gebruikers gevonden!" +} \ No newline at end of file diff --git a/public/language/pl/_DO_NOT_EDIT_FILES_HERE.md b/public/language/pl/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/pl/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/pl/admin/admin.json b/public/language/pl/admin/admin.json new file mode 100644 index 0000000000..cb58fb890a --- /dev/null +++ b/public/language/pl/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Czy na pewno chcesz przebudować oraz zrestartować NodeBB?", + "alert.confirm-restart": "Czy na pewno chcesz zrestartować NodeBB?", + + "acp-title": "%1 | Panel administracyjny NodeBB", + "settings-header-contents": "Zawartość", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/pl/admin/advanced/cache.json b/public/language/pl/admin/advanced/cache.json new file mode 100644 index 0000000000..57dbfddd78 --- /dev/null +++ b/public/language/pl/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Pamięć podręczna postów", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1%", + "post-cache-size": "Rozmiar pamięci podręcznej postów", + "items-in-cache": "Elementów w pamięci podręcznej" +} \ No newline at end of file diff --git a/public/language/pl/admin/advanced/database.json b/public/language/pl/admin/advanced/database.json new file mode 100644 index 0000000000..2602a51f10 --- /dev/null +++ b/public/language/pl/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Czas od restartu w sekundach", + "uptime-days": "Czas od restartu w dniach", + + "mongo": "Mongo", + "mongo.version": "Wersja MongoDB", + "mongo.storage-engine": "Silnik magazynowania", + "mongo.collections": "Kolekcje", + "mongo.objects": "Obiekty", + "mongo.avg-object-size": "Przybliżony rozmiar obiektu", + "mongo.data-size": "Rozmiar danych", + "mongo.storage-size": "Rozmiar pamięci", + "mongo.index-size": "Rozmiar indeksu", + "mongo.file-size": "Rozmiar pliku", + "mongo.resident-memory": "Pamięć przydzielona", + "mongo.virtual-memory": "Pamięc wirtualna", + "mongo.mapped-memory": "Pamięc zmapowana", + "mongo.bytes-in": "Bajtów wejścia", + "mongo.bytes-out": "Bajtów wyjścia", + "mongo.num-requests": "Liczba żądań", + "mongo.raw-info": "Informacje MongoDB", + "mongo.unauthorized": "NodeBB nie był w stanie przetworzyć bazy danych MongoDB dla odpowiednich statystyk. Upewnij się, że użytkownik w użyciu przez NodeBB zawiera "clusterMonitor" dla "admin" bazy danych.", + + "redis": "Redis", + "redis.version": "Wersja Redis", + "redis.keys": "Klucze", + "redis.expires": "Wygasa", + "redis.avg-ttl": "Przeciętny TTL", + "redis.connected-clients": "Połączonych klientów", + "redis.connected-slaves": "Połączonych urządzeń podrzędnych", + "redis.blocked-clients": "Zablokowanych klientów", + "redis.used-memory": "Użyta pamięć", + "redis.memory-frag-ratio": "Proporcja fragmentacji pamięci", + "redis.total-connections-recieved": "Otrzymanych połączeń", + "redis.total-commands-processed": "Przetworzonych komend", + "redis.iops": "Natychmiastowe Operacje Na Sekundę", + "redis.iinput": "Chwilowe wejście na sekundę", + "redis.ioutput": "Natychmiastowe wyjście na sekundę", + "redis.total-input": "Całkowite wejście", + "redis.total-output": "Całkowite wyjście", + + "redis.keyspace-hits": "Trafione Klucze", + "redis.keyspace-misses": "Nietrafione Klucze", + "redis.raw-info": "Informacje Redis", + + "postgres": "Postgres", + "postgres.version": "Wersja PostgreSQL", + "postgres.raw-info": "Informacje PostgreSQL" +} diff --git a/public/language/pl/admin/advanced/errors.json b/public/language/pl/admin/advanced/errors.json new file mode 100644 index 0000000000..5efa0e19d5 --- /dev/null +++ b/public/language/pl/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Błąd %1", + "error-events-per-day": "%1 zdarzeń dziennie", + "error.404": "404 Nie znaleziono", + "error.503": "503 Usługa niedostępna", + "manage-error-log": "Zarządzaj dziennikiem błędów", + "export-error-log": "Eksportuj dziennik (CSV)", + "clear-error-log": "Wyczyść dziennik", + "route": "Scieżka", + "count": "Licznik", + "no-routes-not-found": "Hura! Żadnych błędów 404!", + "clear404-confirm": "Czy chcesz wyczyścić dziennik błędów 404?", + "clear404-success": "Wyczyszczono błędy \"404 Nie znaleziono\"" +} \ No newline at end of file diff --git a/public/language/pl/admin/advanced/events.json b/public/language/pl/admin/advanced/events.json new file mode 100644 index 0000000000..9a61d40c38 --- /dev/null +++ b/public/language/pl/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Zdarzenia", + "no-events": "Brak zdarzeń", + "control-panel": "Panel zdarzeń", + "delete-events": "Usuń zdarzenia", + "confirm-delete-all-events": "Czy na pewno chcesz usunąć wszystkie zapisane zdarzenia?", + "filters": "Filtry", + "filters-apply": "Zatwierdź filtry", + "filter-type": "Typ zdarzenia", + "filter-start": "Data początkowa", + "filter-end": "Data końcowa", + "filter-perPage": "Na stronę" +} \ No newline at end of file diff --git a/public/language/pl/admin/advanced/logs.json b/public/language/pl/admin/advanced/logs.json new file mode 100644 index 0000000000..d09a7de4b2 --- /dev/null +++ b/public/language/pl/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logi", + "control-panel": "Logi Panelu Sterowania", + "reload": "Przeładuj logi", + "clear": "Wyczyść Logi", + "clear-success": "Logi wyczyszczone!" +} \ No newline at end of file diff --git a/public/language/pl/admin/appearance/customise.json b/public/language/pl/admin/appearance/customise.json new file mode 100644 index 0000000000..57e62ece7c --- /dev/null +++ b/public/language/pl/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Własny CSS/LESS", + "custom-css.description": "Wprowadź tutaj własne deklaracje CSS/LESS, które będą użyte po wszystkich pozostałych stylach.", + "custom-css.enable": "Aktywuj własne CSS/LESS", + + "custom-js": "Własny Javascript", + "custom-js.description": "Wprowadź własny kod javascript tutaj. Będzie użyty po pełnym załadowaniu strony.", + "custom-js.enable": "Aktywuj własny Javascript.", + + "custom-header": "Własny nagłówek", + "custom-header.description": "Wpisz tutaj niestandardowy kod HTML (np. Metatagi, itp.), który zostanie dołączony do sekcji <head> Twojego forum. Dozwolone są tagi skryptów, ale są one odradzane, ponieważ dostępna jest niestandardowa karta JavaScript.", + "custom-header.enable": "Włącz własny nagłówek", + + "custom-css.livereload": "Włącz dynamiczne przeładowanie", + "custom-css.livereload.description": "Włącz, jeśli chcesz wymusić odświeżenie wszystkich sesji na każdym urządzeniu na Twoim koncie zawsze, gdy klikniesz „zapisz”." +} \ No newline at end of file diff --git a/public/language/pl/admin/appearance/skins.json b/public/language/pl/admin/appearance/skins.json new file mode 100644 index 0000000000..05c898c195 --- /dev/null +++ b/public/language/pl/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Ładowania skórki...", + "homepage": "Strona startowa", + "select-skin": "Wybierz Skórkę", + "current-skin": "Obecna skórka", + "skin-updated": "Skórka zaktualizowana", + "applied-success": "%1 skórki jest zachowana z powodzeniem", + "revert-success": "Skórka przywrócowana do pierwotnych kolorów" +} \ No newline at end of file diff --git a/public/language/pl/admin/appearance/themes.json b/public/language/pl/admin/appearance/themes.json new file mode 100644 index 0000000000..03c02785af --- /dev/null +++ b/public/language/pl/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Sprawdzanie zainstalowanego stylu...", + "homepage": "Strona startowa", + "select-theme": "Wybierz Styl", + "current-theme": "Aktualny Styl", + "no-themes": "Brak zainstalowanych stylów", + "revert-confirm": "Czy na pewno chcesz przywrócić domyślny styl NodeBB?", + "theme-changed": "Styl Zmieniony", + "revert-success": "Pomyślnie przywrócono domyślny styl NodeBB.", + "restart-to-activate": "Proszę odbudować i zrestartować NodeBB aby w pełni aktywować ten styl." +} \ No newline at end of file diff --git a/public/language/pl/admin/dashboard.json b/public/language/pl/admin/dashboard.json new file mode 100644 index 0000000000..ba2aed1b4d --- /dev/null +++ b/public/language/pl/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Ruch na forum", + "page-views": "Wyświetlenia strony", + "unique-visitors": "Unikalni goście", + "logins": "Loginy", + "new-users": "Nowi użytkownicy", + "posts": "Posty", + "topics": "Tematy", + "page-views-seven": "Ostatnie 7 dni", + "page-views-thirty": "Ostatnie 30 dni", + "page-views-last-day": "Ostatnie 24 godziny", + "page-views-custom": "Własny zakres dat", + "page-views-custom-start": "Początek zakresu", + "page-views-custom-end": "Koniec zakresu", + "page-views-custom-help": "Wprowadź zakres dat dla wyświetleń strony, które chcesz zobaczyć. Jeśli nie ma możliwości wyboru daty, obowiązuje format RRRR-MM-DD", + "page-views-custom-error": "Proszę wprowadzić poprawny zakres dat w formacie YYYY-MM-DD", + + "stats.yesterday": "Wczoraj", + "stats.today": "Dzisiaj", + "stats.last-week": "Poprzedni tydzień", + "stats.this-week": "Obecny tydzień", + "stats.last-month": "Poprzedni miesiąc", + "stats.this-month": "Obecny miesiąc", + "stats.all": "Cały czas", + + "updates": "Aktualizacje", + "running-version": "Forum działa dzięki NodeBB v%1", + "keep-updated": "Aktualizuj NodeBB regularnie, by zwiększać bezpieczeństwa i wprowadzać poprawki.", + "up-to-date": "

NodeBB jest aktualny

", + "upgrade-available": "

Została wydana nowa wersja (v%1). Rozważ aktualizację NodeBB.

", + "prerelease-upgrade-available": "

To jest nieaktualna przedpremierowa wersja NodeBB. Została wydana nowa wersja (v%1). Rozważ aktualizację NodeBB.

", + "prerelease-warning": "

To jest przedpremierowa wersja NodeBB. Mogą występować błędy.

", + "fallback-emailer-not-found": "Nie znaleziono kopii maila!", + "running-in-development": "Forum działa w trybie programistycznym i może być podatne na zagrożenia. Skontaktuj się z administratorem.", + "latest-lookup-failed": "

Nie udało się odnaleźć najnowszej wersji NodeBB

", + + "notices": "Powiadomienia", + "restart-not-required": "Restart nie jest wymagany", + "restart-required": "Wymagany restart", + "search-plugin-installed": "Wyszukiwarka jest zainstalowana", + "search-plugin-not-installed": "Wyszukiwarka nie jest zainstalowana", + "search-plugin-tooltip": "Zainstaluj wyszukiwarkę ze strony wtyczek, by aktywować funkcję wyszukiwania", + + "control-panel": "Zarządzanie systemem", + "rebuild-and-restart": "Przebudowa i restart", + "restart": "Restart", + "restart-warning": "Przebudowa i restart NodeBB zerwie na kilka sekund wszystkie aktywne połączenia. ", + "restart-disabled": "Zablokowano przebudowę i restart, ponieważ wygląda na to, że nie uruchamiasz NodeBB poprzez właściwy serwis.", + "maintenance-mode": "Tryb serwisowy", + "maintenance-mode-title": "Kliknij tutaj, by skonfigurować tryb konserwacji dla NodeBB", + "realtime-chart-updates": "Wykresy aktualizowane na żywo", + + "active-users": "Aktywni użytkownicy", + "active-users.users": "Użytkownicy", + "active-users.guests": "Goście", + "active-users.total": "Łącznie", + "active-users.connections": "Połączenia", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Zarejestrowani", + + "user-presence": "Obecność użytkownika", + "on-categories": "Na liście kategorii", + "reading-posts": "Czytanie postów", + "browsing-topics": "Przeglądanie tematów", + "recent": "Ostatnie", + "unread": "Nieprzeczytane", + + "high-presence-topics": "Popularne tematy", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Wyświetlenia strony", + "graphs.page-views-registered": "Wyświetlenia użytkowników", + "graphs.page-views-guest": "Wyświetlenia gości", + "graphs.page-views-bot": "Wyświetlenia botów", + "graphs.unique-visitors": "Unikalni użytkownicy", + "graphs.registered-users": "Zarejestrowani użytkownicy", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Ostatnio restartowany przez", + "no-users-browsing": "Brak przeglądających", + + "back-to-dashboard": "Powrót do Dashboardu", + "details.no-users": "Żaden użytkownik nie dołączył w wybranym okresie", + "details.no-topics": "Żadne tematy nie zostały opublikowane w wybranym okresie", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "Żadne logi nie zostały zapisane w wybranym okresie", + "details.logins-static": "NodeBB zapisuje dane sesji tylko na %1 dzień, dlatego tabela poniżej zawierać będzie tylko ostatnią aktywną sesję", + "details.logins-login-time": "Czas logowania" +} diff --git a/public/language/pl/admin/development/info.json b/public/language/pl/admin/development/info.json new file mode 100644 index 0000000000..3cfe72b22d --- /dev/null +++ b/public/language/pl/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Jesteś na %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 węzły odpowiedziały w ciągu %2ms!", + "host": "host", + "primary": "główne / uruchomione zadania", + "pid": "pid", + "nodejs": "nodejs", + "online": "dostępny", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "obciążenie systemu", + "cpu-usage": "użycie procesora", + "uptime": "czas działania", + + "registered": "Zarejestrowane", + "sockets": "Sockety", + "guests": "Goście", + + "info": "Informacja" +} \ No newline at end of file diff --git a/public/language/pl/admin/development/logger.json b/public/language/pl/admin/development/logger.json new file mode 100644 index 0000000000..5f76474579 --- /dev/null +++ b/public/language/pl/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Ustawienia dziennika", + "description": "Poprzez zaznaczenie tych pól wyboru otrzymasz na twój terminal. Zamiast tego jeśli podasz ścieżkę, logi zostaną tam zapisane. Logowanie HTTP jest przydatne dla zbierania statystyk o tym kto, kiedy i co czytali na twoim forum. W dodatku do logowania żądań HTTP, możemy też zapisywać zdarzenia socket.io. Logowanie Socket.io, w powiązaniu z monitorowaniem redis-cli, może być bardzo przydatne podczas poznawania mechanizmów wewnętrznych NodeBB.", + "explanation": "Zaznacz/odznacz aby właczyc albo wyłączyć dziennik. Restart nie jest wymagany.", + "enable-http": "Zapisuj wydarzenia HTTP", + "enable-socket": "Zapisuj wydarzenia Socket.io", + "file-path": "Ścieżka dziennika", + "file-path-placeholder": "/sciezka/do/pliku.log ::: pozostaw pusty aby zapisywac do terminala", + + "control-panel": "Panel dziennika", + "update-settings": "Zapisz ustawienia" +} \ No newline at end of file diff --git a/public/language/pl/admin/extend/plugins.json b/public/language/pl/admin/extend/plugins.json new file mode 100644 index 0000000000..fa3c3ebefc --- /dev/null +++ b/public/language/pl/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "O trendzie", + "installed": "Zainstalowane", + "active": "Aktywne", + "inactive": "Nieaktywne", + "out-of-date": "Nieaktualne", + "none-found": "Nie znaleziono wtyczek", + "none-active": "Brak aktywnych wtyczek", + "find-plugins": "Znajdź wtyczkę", + + "plugin-search": "Szukaj wtyczek", + "plugin-search-placeholder": "Szukaj wtyczki...", + "submit-anonymous-usage": "Prześlij anonimowe dane użycia wtyczki.", + "reorder-plugins": "Posortuj wtyczki", + "order-active": "Posortuj aktywne wtyczki", + "dev-interested": "Chcesz pisać wtyczki do NodeBB?", + "docs-info": "Pełna dokumentacja dotycząca tworzenia wtyczek jest dostępna na NodeBB Docs Portal.", + + "order.description": "Niektóre wtyczki działają optymalnie wówczas, gdy są inicjalizowane przed innymi/po innych.", + "order.explanation": "Wtyczki ładują się w określonej tutaj kolejności, od góry do dołu.", + + "plugin-item.themes": "Style", + "plugin-item.deactivate": "Dezaktywuj", + "plugin-item.activate": "Aktywuj", + "plugin-item.install": "Zainstaluj", + "plugin-item.uninstall": "Odinstaluj", + "plugin-item.settings": "Ustawienia", + "plugin-item.installed": "Zainstalowane", + "plugin-item.latest": "Ostatnie", + "plugin-item.upgrade": "Zaktualizuj", + "plugin-item.more-info": "Więcej informacji:", + "plugin-item.unknown": "Nieznane", + "plugin-item.unknown-explanation": "Nie udało się ustalić stanu tej wtyczki, prawdopodobnie z powodu błędu konfiguracji.", + "plugin-item.compatible": "Ten plugin jest zgodny z wersją NodeBB %1", + "plugin-item.not-compatible": "Ta wtyczka nie zawiera danych dotyczących zgodności, upewnij się, że działa w środowisku produkcyjnym.", + + "alert.enabled": "Wtyczka włączona", + "alert.disabled": "Wtyczka wyłączona", + "alert.upgraded": "Wtyczka zaktualizowana", + "alert.installed": "Wtyczka zainstalowana", + "alert.uninstalled": "Wtyczka odinstalowana", + "alert.activate-success": "Proszę odbudować i zrestartować NodeBB aby w pełni aktywować ten plugin", + "alert.deactivate-success": "Wtyczka została dezaktywowana", + "alert.upgrade-success": "Przebuduj i zrestartuj NodeBB, by w pełni zaktualizować tę wtyczkę.", + "alert.install-success": "Wtyczka została zainstalowana, teraz należy ją aktywować.", + "alert.uninstall-success": "Wtyczka została zdezaktywowana i odinstalowana.", + "alert.suggest-error": "

NodeBB nie może dostać się do menedżera pakietów. Czy kontynuować instalację ostatniej wersji?

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

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

", + "alert.incompatible": "

Twoja wersja NodeBB (v%1) umożliwia aktualizację jedynie do v%2 tej wtyczki. Zaktualizuj NodeBB, jeśli chcesz zainstalować nowszą wersję tej wtyczki.

", + "alert.possibly-incompatible": "

Nie znaleziono informacji o kompatybilności

Nie wskazano konkretnej wersji tej wtyczki dla Twojej wersji NodeBB. Pełna zgodność nie jest gwarantowana, a NodeBB może przestać uruchamiać się prawidłowo.

Jeśli NodeBB nie może się poprawnie uruchomić:

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

Czy kontynuować instalację ostatniej wersji wtyczki?

", + "alert.reorder": "Wtyczki zostały posortowane", + "alert.reorder-success": "Przebuduj i zrestartuj NodeBB, by ukończyć proces.", + + "license.title": "Informacje o licencji wtyczki", + "license.intro": "Wtyczka %1 jest licencjonowana według %2. Zapoznaj się z warunkami licencji przed aktywacją tej wtyczki.", + "license.cta": "Czy chcesz kontynuować aktywację tej wtyczki?" +} diff --git a/public/language/pl/admin/extend/rewards.json b/public/language/pl/admin/extend/rewards.json new file mode 100644 index 0000000000..afe04e15af --- /dev/null +++ b/public/language/pl/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Nagrody", + "condition-if-users": "Jeżeli użytkownik", + "condition-is": "Jest:", + "condition-then": "To:", + "max-claims": "Ile razy nagroda może zostać przyznana", + "zero-infinite": "Wpisz 0, aby nieskończona liczbę razy", + "delete": "Usuń", + "enable": "Włącz", + "disable": "Wyłącz", + + "alert.delete-success": "Pomyślnie usunięto nagrodę", + "alert.no-inputs-found": "Niepoprawnie dodana nagroda ", + "alert.save-success": "Pomyślnie zapisano nagrodę" +} \ No newline at end of file diff --git a/public/language/pl/admin/extend/widgets.json b/public/language/pl/admin/extend/widgets.json new file mode 100644 index 0000000000..584cb4a02b --- /dev/null +++ b/public/language/pl/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Dostępne Widgety", + "explanation": "Wybierz widget z menu rozwijalnego i przeciągnij go na wybrane pole z lewej strony.", + "none-installed": "Nie odnaleziono widgetów! Aktywuj wtyczkę „widget essentials w panelu sterowania wtyczek.", + "clone-from": "Sklonuj widget z", + "containers.available": "Dostępne kontenery", + "containers.explanation": "Przeciągnij i upuść na dowolnym, aktywnym widżecie", + "containers.none": "Żadna", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Nagłówek panelu", + "container.panel-body": "Zawartość panelu", + "container.alert": "Alarm", + + "alert.confirm-delete": "Czy na pewno chcesz usunąć ten widget?", + "alert.updated": "Widgety zaktualizowane", + "alert.update-success": "Widgety zostały zaktualizowane", + "alert.clone-success": "Widgety zostały sklonowane", + + "error.select-clone": "Proszę wybrać stronę do sklonowania", + + "title": "Tytuł", + "title.placeholder": "Tytuł (wyświetlany tylko w niektórych kontenerach)", + "container": "Kontener", + "container.placeholder": "Przeciągnij i upuść kontener lub wpisz tutaj HTML.", + "show-to-groups": "Pokaż dla grup", + "hide-from-groups": "Ukryj dla grup", + "hide-on-mobile": "Ukraj na urządzeniach mobilnych" +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/admins-mods.json b/public/language/pl/admin/manage/admins-mods.json new file mode 100644 index 0000000000..79d2be0b62 --- /dev/null +++ b/public/language/pl/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administratorzy", + "global-moderators": "Globalni moderatorzy", + "moderators": "Moderators", + "no-global-moderators": "Brak globalnych moderatorów", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Brak moderatorów", + "add-administrator": "Dodaj administratora", + "add-global-moderator": "Dodaj globalnego moderatora", + "add-moderator": "Dodaj moderatora" +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/categories.json b/public/language/pl/admin/manage/categories.json new file mode 100644 index 0000000000..70dbbeba4b --- /dev/null +++ b/public/language/pl/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Ustawienia kategorii", + "privileges": "Uprawnienia", + + "name": "Nazwa kategorii", + "description": "Opis kategorii", + "bg-color": "Kolor tła", + "text-color": "Kolor tekstu", + "bg-image-size": "Wielkość obrazka tła", + "custom-class": "Niestandardowa klasa", + "num-recent-replies": "# z ostatnich odpowiedzi", + "ext-link": "Zewnętrzny odnośnik", + "subcategories-per-page": "Subkategorie na stronę", + "is-section": "Traktuj tę kategorię jako sekcję", + "post-queue": "Kolejka postów", + "tag-whitelist": "Otaguj białą listę", + "upload-image": "Prześlij obrazek", + "delete-image": "Usuń", + "category-image": "Obrazek kategorii", + "parent-category": "Kategoria nadrzędna", + "optional-parent-category": "(Opcjonalne) Kategoria nadrzędna", + "top-level": "Najwyższy poziom", + "parent-category-none": "(Żadna)", + "copy-parent": "Skopiuj od rodzica", + "copy-settings": "Skopiuj ustawienia z", + "optional-clone-settings": "(Opcjonalnie) Skopiowanie ustawień z kategorii", + "clone-children": "Sklonuj podrzędne kategorie i ustawienia", + "purge": "Usuń kategorię", + + "enable": "Włącz", + "disable": "Wyłącz", + "edit": "Edytuj", + "analytics": "Analityka", + "view-category": "Wyświetl kategorię", + "set-order": "Ustaw kolejnośc", + "set-order-help": "Ustawienie kolejności kategorii przesunie tę kategorię w żądanej kolejności i zaktualizuje kolejność zgodnie z potrzebą. Minimalna kolejność to 1, co umieści daną kategorię na górze.", + + "select-category": "Wybierz kategorię", + "set-parent-category": "Ustaw nadrzędną kategorie", + + "privileges.description": "Możesz skonfigurować uprawnienia kontroli dostępu do części witryny w tej sekcji. Uprawnienia mogą być przyznawane dla poszczególnych użytkowników lub grup. Wybierz domenę efektu z poniższego menu.", + "privileges.category-selector": "Konfigurowanie uprawnień dla", + "privileges.warning": "Uwaga: Uprawnienia zapisują się natychmiastowo. Nie ma potrzeby zapisywania kategorii po zmianie ustawień.", + "privileges.section-viewing": "Lista uprawnień", + "privileges.section-posting": "Uprawnienia pisania", + "privileges.section-moderation": "Uprawnienia moderowania", + "privileges.section-other": "Inne", + "privileges.section-user": "Użytkownik", + "privileges.search-user": "Dodaj użytkownika", + "privileges.no-users": "Brak uprawnień specyficznych dla użytkowników w tej kategorii", + "privileges.section-group": "Grupa", + "privileges.group-private": "Ta grupa jest prywatna", + "privileges.inheritance-exception": "W tej grupie nie obowiązują przywileje z grup dla zarejestrowanych użytkowników", + "privileges.banned-user-inheritance": "Przywileje z grup zablokowanych użytkowników obowiązują zablokowanych użytkowników", + "privileges.search-group": "Dodaj grupę", + "privileges.copy-to-children": "Skopiuj do podrzędnej", + "privileges.copy-from-category": "Skopiuj z kategorii", + "privileges.copy-privileges-to-all-categories": "Skopiuj do wszystkich kategorii", + "privileges.copy-group-privileges-to-children": "Skopiuj uprawnienia tej grupy do dzieci tej kategorii.", + "privileges.copy-group-privileges-to-all-categories": "Skopiuj uprawnienia tej grupy do wszystkich kategorii.", + "privileges.copy-group-privileges-from": "Skopiuj uprawnienia tej grupy z innej kategorii.", + "privileges.inherit": "Gdy grupie registered-users zostaje nadane określone uprawnienie, to samo bezwarunkowe uprawnienie otrzymują też wszystkie inne grupy, nawet jeśli nie zostały zdefiniowane/zaznaczone. Bezwarunkowe uprawnienie jest wyświetlane, ponieważ wszyscy użytkownicy są częścią grupy registered-users, zatem uprawnienia dla dodatkowych grup nie muszą być przyznawane oddzielnie.", + "privileges.copy-success": "Uprawnienia skopiowane!", + + "analytics.back": "Wróć do listy kategorii", + "analytics.title": "Analityka dla \"%1\" kategorii", + "analytics.pageviews-hourly": "Ilustracja 1– Godzinowe wyświetlenia stron dla tej kategorii", + "analytics.pageviews-daily": "Ilustracja – Dzienne wyświetlenie strony dla tej kategorii", + "analytics.topics-daily": "Ilustracja 3– Dzienne tematy tworzone w tej kategorii", + "analytics.posts-daily": "Ilustracja 4 – Dzienne posty pisane w tej kategorii", + + "alert.created": "Utworzony", + "alert.create-success": "Kategoria pomyślnie dodana!", + "alert.none-active": "Nie masz aktywnych kategorii.", + "alert.create": "Utwórz kategorię", + "alert.confirm-purge": "

Czy na pewno chcesz wymazać tą kategorię \"%1\"?

Uwaga! Wszystkie tematy oraz posty z tej kategorii zostaną wymazane!

Wymazanie kategorii skasuje wszystkie tematy, posty oraz skasuję kategorię z bazy danych. Jeśli chcesz tymczasowousunąć kategorię, będziesz musiał \"wyłączyć\" kategorię.

", + "alert.purge-success": "Kategoria usunięta!", + "alert.copy-success": "Ustawienie skopiowane!", + "alert.set-parent-category": "Ustaw nadrzędną kategorie", + "alert.updated": "Zaktualizuj kategorie", + "alert.updated-success": "ID kategorii %1 pomyślnie zaktualizowano.", + "alert.upload-image": "Prześlij obrazek kategorii", + "alert.find-user": "Znajdź użytkownika", + "alert.user-search": "Wyszukaj użytkownika tutaj...", + "alert.find-group": "Znajdź grupę", + "alert.group-search": "Wyszukaj grupę tutaj...", + "alert.not-enough-whitelisted-tags": "Tagów dodanych na białą listę jest mniej, niż minimalna liczba tagów - należy dodać więcej tagów do białej listy!", + "collapse-all": "Zwiń wszystko", + "expand-all": "Rozwiń wszystko", + "disable-on-create": "Wyłącz przy tworzeniu", + "no-matches": "Brak dopasowań" +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/digest.json b/public/language/pl/admin/manage/digest.json new file mode 100644 index 0000000000..7ac8122adc --- /dev/null +++ b/public/language/pl/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Wykaz statystyk i czasów podsumowań jest wyświetlony poniżej", + "disclaimer": "Proszę mieć na uwadze, że z natury tej technologii, dostarczenie wiadomości email nie jest gwarantowane. Wiele czynników ma wpływ na to, czy wiadomość wysłana na dany serwer ostatecznie trafi do skrzynki użytkownika, takich jak reputacja serwera, czarnej liście adresów IP i temu czy DKIM/SPF/DMARC jest skonfigurowane", + "disclaimer-continued": "Udana wysyłka oznacza, że wiadomość została wysłana przez NodeBB i otrzymane zostało potwierdzenie od serwera docelowego. Nie oznacza to jednak, że email dotarł do skrzynki użytkownika. Dla najlepszych rezultatów polecamy używać zewnętrznych usług dostarczania wiadomości email takich jak SendGrid", + + "user": "Użytkownik", + "subscription": "Typ subskrypcji", + "last-delivery": "Ostatnia udana wysyłka", + "default": "Domyślne ustawienie systemowe", + "default-help": "Domyślne ustawienia systemowe oznaczają, że użytkownik korzysta z globalnych ustawień podsumowań, czyli obecnie: "%1"", + "resend": "Wyślij ponownie podsumowanie", + "resend-all-confirm": "Czy na pewno chcesz ręcznie wykonać wysłanie tego podsumowania?", + "resent-single": "Ręczne wysyłanie podsumowania zakończone", + "resent-day": "Codzienne podsumowanie", + "resent-week": "Tygodniowe podsumowanie", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Miesięczne podsumowanie", + "null": "Nigdy", + "manual-run": "Włącz ręcznie podsumowania", + + "no-delivery-data": "Nie znaleziono danych" +} diff --git a/public/language/pl/admin/manage/groups.json b/public/language/pl/admin/manage/groups.json new file mode 100644 index 0000000000..94aa0d69ab --- /dev/null +++ b/public/language/pl/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Nazwa grupy", + "badge": "Plakietka", + "properties": "Właściwości", + "description": "Opis grupy", + "member-count": "Liczba użytkowników", + "system": "System", + "hidden": "Ukryty", + "private": "Prywatny", + "edit": "Edytuj", + "delete": "Usuń", + "privileges": "Uprawnienia", + "download-csv": "CSV", + "search-placeholder": "Szukaj", + "create": "Utwórz grupę", + "description-placeholder": "Krótki opis grupy", + "create-button": "Utwórz", + + "alerts.create-failure": "Uh-Oh

Wystąpił problem podczas tworzenia grupy. Spróbuj ponownie później

", + "alerts.confirm-delete": "Czy na pewno chcesz usunąć tę grupę?", + + "edit.name": "Nazwa", + "edit.description": "Opis", + "edit.user-title": "Tytuł członków ", + "edit.icon": "Ikona grupy", + "edit.label-color": "Kolor etykiety grupy", + "edit.text-color": "Kolor Tekstu Grupy", + "edit.show-badge": "Pokaż etykietę", + "edit.private-details": "Jeśli włączone, przystępowanie do grup wymaga zatwierdzenia przez właściciela grupy", + "edit.private-override": "Ostrzeżenie: Prywatne grupy są wyłączone w ustawieniach, co powoduje przesłonięcia opcji.", + "edit.disable-join": "Wyłącz prośby o dołączenie", + "edit.disable-leave": "Nie pozwól użytkownikom na opuszczenie tej grupy", + "edit.hidden": "Ukryta", + "edit.hidden-details": "Jeśli opcja jest włączona, grupa ta nie będzie widoczna dla użytkowników.", + "edit.add-user": "Dodaj użytkownika do grupy", + "edit.add-user-search": "Szukaj użytkownika", + "edit.members": "Lista członków", + "control-panel": "Panel sterowania", + "revert": "Cofnij", + + "edit.no-users-found": "Nie znaleziono użytkowników", + "edit.confirm-remove-user": "Jesteś pewny, że chcesz usunąć tego użytkownika?", + "edit.save-success": "Zmiany zapisane!" +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/privileges.json b/public/language/pl/admin/manage/privileges.json new file mode 100644 index 0000000000..eed78b6293 --- /dev/null +++ b/public/language/pl/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Globalny", + "admin": "Admin", + "group-privileges": "Uprawnienia grup", + "user-privileges": "Uprawnienia użytkownika", + "edit-privileges": "Edytuj uprawnienia", + "select-clear-all": "Select/Clear All", + "chat": "Dostęp do czatu", + "upload-images": "Przesyłanie zdjęć", + "upload-files": "Przesyłanie plików", + "signature": "Dodanie sygnatury", + "ban": "Banowanie", + "mute": "Mute", + "invite": "Invite", + "search-content": "Szukanie treści", + "search-users": "Szukanie użytkowników", + "search-tags": "Szukanie tagów", + "view-users": "Wyświetlanie użytkowników", + "view-tags": "Wyświetlanie tagów", + "view-groups": "Wyświetlanie grup", + "allow-local-login": "Logowanie lokalne", + "allow-group-creation": "Tworzenie grup", + "view-users-info": "Pokaż dane użytkownika", + "find-category": "Szukanie kategorii", + "access-category": "Dostęp do kategorii", + "access-topics": "Dostęp do tematów", + "create-topics": "Tworzenie tematów", + "reply-to-topics": "Odpowiadanie na tematy", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tagowanie tematów", + "edit-posts": "Edycja postów", + "view-edit-history": "Dostęp do historii edycji", + "delete-posts": "Usuwanie postów", + "view_deleted": "Dostęp do usuniętych postów", + "upvote-posts": "Lajkowanie postów", + "downvote-posts": "Głosowanie przeciw postom", + "delete-topics": "Usuwanie tematów", + "purge": "Czyszczenie", + "moderate": "Moderowanie", + "admin-dashboard": "Dashboard", + "admin-categories": "Kategorie", + "admin-privileges": "Uprawnienia", + "admin-users": "Użytkownicy", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Ustawienia", + + "alert.confirm-moderate": "Czy na pewno chcesz przyznać uprawnienia moderacji dla tej grupy użytkowników? Ta grupa jest publiczna i każdy użytkownik może do niej dołączyć.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Potwierdź zamiar zapisania uprawnień", + "alert.saved": "Zapisano i zastosowano zmiany w uprawnieniach", + "alert.confirm-discard": "Czy na pewno chcesz odrzucić wprowadzone zmiany w uprawnieniach?", + "alert.discarded": "Odrzucono zmiany w uprawnieniach", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "Tej czynności nie można cofnąć.", + "alert.admin-warning": "Administratorzy domyślnie otrzymują wszelkie uprawnienia", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/registration.json b/public/language/pl/admin/manage/registration.json new file mode 100644 index 0000000000..b2c9e682e8 --- /dev/null +++ b/public/language/pl/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Kolejka", + "description": "Brak użytkowników w kolejce rejestracji.
W celu włączenia tej funkcji, przejdź do Ustawienia → Użytkownik → Rejestracja użytkownika i jako Typ rejestracji ustaw „Zatwierdzenie przez administratora”. ", + + "list.name": "Nazwa", + "list.email": "Adres e-mail", + "list.ip": "IP", + "list.time": "Czas", + "list.username-spam": "Częstotliwość: %1 Występowanie: %2 Pewność: %3", + "list.email-spam": "Częstotliwość: %1 Występowanie: %2", + "list.ip-spam": "Częstotliwość: %1 Występowanie: %2", + + "invitations": "Zaproszenia", + "invitations.description": "Poniżej znajduje się pełna lista wysłanych zaproszeń. Użyj CTRL-f , by przeszukać listę po adresie e-mail lub nazwie użytkownika.

Nazwa użytkownika, który skorzystał z zaproszenia, zostanie wyświetlona po prawej stronie jego adresu e-mail.", + "invitations.inviter-username": "Nazwa użytkownika zapraszającego", + "invitations.invitee-email": "Adres e-mail zaproszonego", + "invitations.invitee-username": "Nazwa użytkownika zaproszonego (jeśli zarejestrowany)", + + "invitations.confirm-delete": "Czy na pewno skasować to zaproszenie?" +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/tags.json b/public/language/pl/admin/manage/tags.json new file mode 100644 index 0000000000..f61aecd889 --- /dev/null +++ b/public/language/pl/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Twoje forum nie ma jeszcze żadnych tematów z tagami.", + "bg-color": "Kolor tła", + "text-color": "Kolor tekstu", + "description": "Wybieraj tagi poprzez klikanie lub przeciąganie; użyj przycisku CTRL do zaznaczenia wielu.", + "create": "Utwórz tag", + "modify": "Modyfikuj tagi", + "rename": "Przemianuj tagi", + "delete": "Usuń zaznaczone tagi", + "search": "Szukanie tagów...", + "settings": "Ustawienia tagów", + "name": "Nazwa taga", + + "alerts.editing": "edytowanie tagu/tagów", + "alerts.confirm-delete": "Czy na pewno chcesz usunąć zaznaczone tagi?", + "alerts.update-success": "Zaktualizowano tag-a!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/uploads.json b/public/language/pl/admin/manage/uploads.json new file mode 100644 index 0000000000..e3dd6c198c --- /dev/null +++ b/public/language/pl/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Wyślij plik", + "filename": "Nazwa pliku", + "usage": "Wykorzystany w poście", + "orphaned": "Osierocone", + "size/filecount": "Rozmiar / Liczba plików", + "confirm-delete": "Czy na pewno chcesz usunąć ten plik?", + "filecount": "%1 plików", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/pl/admin/manage/users.json b/public/language/pl/admin/manage/users.json new file mode 100644 index 0000000000..35a3fcf32d --- /dev/null +++ b/public/language/pl/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Użytkownicy", + "edit": "Actions", + "make-admin": "Nadaj uprawnienia administratora", + "remove-admin": "Odbierz uprawnienia administratora", + "validate-email": "Zweryfikuj e-mail", + "send-validation-email": "Wyślij e-mail weryfikacyjny", + "password-reset-email": "Wyślij e-mail do resetu hasła", + "force-password-reset": "Wymuś Zmianę Hasła i Wyloguj Użytkownika", + "ban": "Zbanuj użytkownika(-ów)", + "temp-ban": "Tymczasowo zbanuj użytkownika(-ów)", + "unban": "Odbanuj użytkownika(-ów)", + "reset-lockout": "Zresetuj blokadę", + "reset-flags": "Zresetuj flagi", + "delete": "Usuń Użytkownika(ów)", + "delete-content": "Usuń Treści Użytkownika(ów)", + "purge": "Usuń Użytkownika(ów) i Treści", + "download-csv": "Pobierz CSV", + "manage-groups": "Zarządzaj grupami", + "add-group": "Dodaj grupę", + "create": "Create User", + "invite": "Invite by Email", + "new": "Nowy użytkownik", + "filter-by": "Filtruj po", + "pills.unvalidated": "Niezweryfikowani", + "pills.validated": "Zweryfikowano", + "pills.banned": "Zbanowani", + + "50-per-page": "50 na stronę", + "100-per-page": "100 na stronę", + "250-per-page": "250 na stronę", + "500-per-page": "500 na stronę", + + "search.uid": "Po ID użytkownika", + "search.uid-placeholder": "Wpisz ID użytkownika, by wyszukać", + "search.username": "Po nazwie użytkownika", + "search.username-placeholder": "Wpisz nazwę użytkownika, by wyszukać", + "search.email": "Po adresie e-mail", + "search.email-placeholder": "Wpisz adres e-mail, by wyszukać", + "search.ip": "Po adresie IP", + "search.ip-placeholder": "Wpisz adres IP, by wyszukać", + "search.not-found": "Nie znaleziono użytkownika!", + + "inactive.3-months": "3 miesiące", + "inactive.6-months": "6 miesięcy", + "inactive.12-months": "12 miesięcy", + + "users.uid": "uid", + "users.username": "nazwa użytkownika", + "users.email": "adres e-mail", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "liczba postów", + "users.reputation": "reputacja", + "users.flags": "flagi", + "users.joined": "dołączono", + "users.last-online": "ostatnio online", + "users.banned": "zbanowany", + + "create.username": "Nazwa użytkownika", + "create.email": "Adres e-mail", + "create.email-placeholder": "Adres e-mail tego użytkownika", + "create.password": "Hasło", + "create.password-confirm": "Powtórz hasło", + + "temp-ban.length": "Length", + "temp-ban.reason": "Powód (Opcjonalnie)", + "temp-ban.hours": "Godziny", + "temp-ban.days": "Dni", + "temp-ban.explanation": "Podaj czas trwania bana. Okres równy 0 będzie traktowany jako ban permanentny.", + + "alerts.confirm-ban": "Czy na pewno chcesz zbanować tego użytkownika permanentnie?", + "alerts.confirm-ban-multi": "Czy na pewno chcesz zbanować tych użytkowników permanentnie?", + "alerts.ban-success": "Użytkownik(-cy) zostali zbanowani!", + "alerts.button-ban-x": "Zbanowano %1 użytkownika(-ów)", + "alerts.unban-success": "Użytkownik(-cy) nie są już zbanowani!", + "alerts.lockout-reset-success": "Zresetowano blokadę(-y)!", + "alerts.flag-reset-success": "Zresetowano flagę(-i)!", + "alerts.no-remove-yourself-admin": "Nie możesz odebrać sobie samemu praw administratora.", + "alerts.make-admin-success": "Użytkownik jest teraz administratorem.", + "alerts.confirm-remove-admin": "Na pewno chcesz usunąć tego administratora?", + "alerts.remove-admin-success": "Użytkownik już nie jest już administratorem.", + "alerts.make-global-mod-success": "Użytkownik jest teraz globalnym moderatorem.", + "alerts.confirm-remove-global-mod": "Na pewno chcesz usunąć tego globalnego moderatora?", + "alerts.remove-global-mod-success": "Użytkownik już nie jest już globalnym moderatorem.", + "alerts.make-moderator-success": "Użytkownik jest teraz moderatorem.", + "alerts.confirm-remove-moderator": "Na pewno chcesz usunąć tego moderatora?", + "alerts.remove-moderator-success": "Użytkownik już nie jest już moderatorem.", + "alerts.confirm-validate-email": "Czy chcesz zweryfikować adres e-mail tych użytkowników?", + "alerts.confirm-force-password-reset": "Jesteś pewien, że chcesz zresetować hasła użytkowników i ich wylogować?", + "alerts.validate-email-success": "Zweryfikowano adresy e-mail", + "alerts.validate-force-password-reset-success": "Hasła użytkownika(ów) zostały zresetowane, a ich istniejące sesje zostały odwołane.", + "alerts.password-reset-confirm": "Czy chcesz wysłać e-mail do resetu hasła tym użytkownikom?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Uwaga!

Czy na pewno chcesz usunąć Uzykownika(ów)?

To działanie jest nieodwracalne! Tylko konto użytkownika zostanie usunięte, jego posty i tematy pozostaną.

", + "alerts.delete-success": "Skasowano użytkownika(-ów)!", + "alerts.confirm-delete-content": "Uwaga!

Czy na pewno chcesz usunąć treściużytkownika(ów)?

To działanie jest nieodwracalne! Konto użytkownika pozostanie, jednak jego posty i tematy zostaną usunięte.

", + "alerts.delete-content-success": "Treści Użytkownika(ów) usunięte!", + "alerts.confirm-purge": "Uwaga!

Czy na pewno chcesz usunąc użytkownika(ów) i jego(ich) treści?

To działanie jest nieodwracalne! Wszystkie dane użytkownika i jego treści zostaną usunięte!

", + "alerts.create": "Utwórz użytkownika", + "alerts.button-create": "Utwórz", + "alerts.button-cancel": "Anuluj", + "alerts.error-passwords-different": "Hasła muszą być takie same!", + "alerts.error-x": "Błąd

%1

", + "alerts.create-success": "Utworzono użytkownika!", + + "alerts.prompt-email": "Adresy e-mail:", + "alerts.email-sent-to": "Wysłano zaproszenie do %1", + "alerts.x-users-found": "Znaleziono %1 użytkownika(-ów), (czas wyszukiwania: %2 s)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/pl/admin/menu.json b/public/language/pl/admin/menu.json new file mode 100644 index 0000000000..03219f99fb --- /dev/null +++ b/public/language/pl/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Ogólne", + + "section-manage": "Zarządzanie", + "manage/categories": "Kategorie", + "manage/privileges": "Uprawnienia", + "manage/tags": "Tagi", + "manage/users": "Użytkownicy", + "manage/admins-mods": "Administratorzy i Moderatorzy", + "manage/registration": "Kolejka rejestracji", + "manage/post-queue": "Kolejka postów", + "manage/groups": "Grupy", + "manage/ip-blacklist": "Czarna lista IP", + "manage/uploads": "Przesłane pliki", + "manage/digest": "Podsumownia", + + "section-settings": "Ustawienia", + "settings/general": "Ogólne", + "settings/homepage": "Strona główna", + "settings/navigation": "Nawigacja", + "settings/reputation": "Reputacja i Flagi", + "settings/email": "E-mail", + "settings/user": "Użytkownicy", + "settings/group": "Grupy", + "settings/guest": "Goście", + "settings/uploads": "Przesyłanie plików", + "settings/languages": "Języki", + "settings/post": "Posty", + "settings/chat": "Czaty", + "settings/pagination": "Paginacja", + "settings/tags": "Tagi", + "settings/notifications": "Powiadomienia", + "settings/api": "Dostęp do API", + "settings/sounds": "Dźwięki", + "settings/social": "Społecznościowe", + "settings/cookies": "Ciasteczka", + "settings/web-crawler": "Roboty internetowe", + "settings/sockets": "Sockety", + "settings/advanced": "Zaawansowane", + + "settings.page-title": "Ustawienia %1", + + "section-appearance": "Wygląd", + "appearance/themes": "Motywy", + "appearance/skins": "Skórki", + "appearance/customise": "Niestandardowy HTML & CSS", + + "section-extend": "Rozszerzenia", + "extend/plugins": "Wtyczki", + "extend/widgets": "Widgety", + "extend/rewards": "Nagrody", + + "section-social-auth": "Alternatywne logowanie", + + "section-plugins": "Wtyczki", + "extend/plugins.install": "Zainstalowane wtyczki", + + "section-advanced": "Zaawansowane", + "advanced/database": "Baza danych", + "advanced/events": "Zdarzenia", + "advanced/hooks": "Hooks", + "advanced/logs": "Logi", + "advanced/errors": "Błędy", + "advanced/cache": "Pamięć", + "development/logger": "Loger", + "development/info": "Informacja", + + "rebuild-and-restart-forum": "Odbudowa i restart forum", + "restart-forum": "Restartuj forum", + "logout": "Wyloguj się", + "view-forum": "Zobacz forum", + + "search.placeholder": "Search settings", + "search.no-results": "Brak wyników...", + "search.search-forum": "Szukaj w forum ", + "search.keep-typing": "Wpisz więcej, aby zobaczyć wyniki ...", + "search.start-typing": "Zacznij pisać, aby zobaczyć wyniki ...", + + "connection-lost": "Połączenie z %1 zostało utracone, próba ponownego połączenia...", + + "alerts.version": "Forum działa dzięki NodeBB v%1", + "alerts.upgrade": "Aktualizacja do v%1" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/advanced.json b/public/language/pl/admin/settings/advanced.json new file mode 100644 index 0000000000..619e26b8de --- /dev/null +++ b/public/language/pl/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Tryb konserwacji", + "maintenance-mode.help": "Kiedy forum jest w trybie konserwacji, wszystkie żądania będą przekierowane do statycznej strony oczekiwania. Administratorzy nie są objęci tym przekierowaniem i mogą normalnie korzystać ze strony.", + "maintenance-mode.status": "Kod stanu trybu konserwacji", + "maintenance-mode.message": "Komunikat na ekranie konserwacji", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Nagłówki", + "headers.allow-from": "Ustaw ALLOW-FROM, aby umieścić NodeBB w ramce iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Dopasuj nagłówek \"Powered By\" wysyłany przez NodeBB", + "headers.acao": "Kontrola-Dostępu-Zezwól-Żródło", + "headers.acao-regex": "Kontrola-Dostępu-Zezwól-Źródło Wyrażenie Regularne", + "headers.acao-help": "Aby zablokować dostęp do wszystkich stron, pozostaw puste.", + "headers.acao-regex-help": "Tutaj wprowadź wyrażenia regularne, aby dopasować dynamiczne źródła. Aby zablokować dostęp do wszystkich stron, pozostaw puste.", + "headers.acac": "Kontrola-Dostępu-Zezwól- Dane Logowania", + "headers.acam": "Kontrola-Dostępu-Zezwól-Metody", + "headers.acah": "Kontrola-Dostępu-Zezwól-Nagłówki", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Włączony HSTS (zalecane)", + "hsts.maxAge": "Maksymalny wiek HSTS", + "hsts.subdomains": "Uwzględnij subdomeny w nagłówku HSTS", + "hsts.preload": "Zezwól na wstępne ładowanie nagłówka HSTS", + "hsts.help": "Jeśli ta opcja jest włączona, dla tej witryny zostanie ustawiony nagłówek HSTS. Możesz zdecydować, czy uwzględnić subdomeny i wstępnie ładować flagi w nagłówku. Jeśli masz wątpliwości, możesz zostawić te pola niezaznaczone. Więcej informacji", + "traffic-management": "Zarządzanie ruchem", + "traffic.help": "NodeBB używa modułu, który automatycznie odmawia żądań w przypadku dużego ruchu sieciowego. Możesz regulować te ustawienia tutaj, chociaż te domyślne są dobrym punktem wyjścia.", + "traffic.enable": "Włącz zarządzanie ruchem", + "traffic.event-lag": "Próg opóźnienia pętli zdarzeń (w milisekundach)", + "traffic.event-lag-help": "Obniżenie tej wartości spowoduje krótsze ładowanie stron, ale równocześnie wyświetli komunikat \"excessive load\" dla większej liczby użytkowników (wymagany restart).", + "traffic.lag-check-interval": "Interwał sprawdzenia (w milisekundach)", + "traffic.lag-check-interval-help": "Obniżenie tej wartości sprawi, że NodeBB będzie bardziej czuły na skoki obciążenia, ale może też spowodować, że sprawdzanie będzie za bardzo dokładne (wymagany restart).", + + "sockets.settings": "Ustawienia WebSocket", + "sockets.max-attempts": "Maksymalna liczba prób połączenia", + "sockets.default-placeholder": "Domyślnie: %1", + "sockets.delay": "Opóźnienie ponownego łączenia", + + "analytics.settings": "Ustawienia Analityki", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Ustawienia Kompresji", + "compression.enable": "Włącz Kompresję", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/api.json b/public/language/pl/admin/settings/api.json new file mode 100644 index 0000000000..13742ccb6b --- /dev/null +++ b/public/language/pl/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokeny", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Kliknij tutaj, aby zobaczyć pełną specyfikację API", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "ID Użytkownika", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Opis", + "no-description": "Brak opisu.", + "token-on-save": "Token zostanie wygenerowany po zapisaniu formularza" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/chat.json b/public/language/pl/admin/settings/chat.json new file mode 100644 index 0000000000..151fb08037 --- /dev/null +++ b/public/language/pl/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Ustawienia czatu", + "disable": "Wyłącz czat", + "disable-editing": "Wyłącz edycję/usuwanie wiadomości czat", + "disable-editing-help": "Ograniczenie to nie dotyczy administratorów i moderatorów globalnych", + "max-length": "Maksymalna długość wiadomości czat", + "max-room-size": "Maksymalna liczba użytkowników w pokojach czatu", + "delay": "Czas pomiędzy wiadomościami czat (w milisekundach)", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Liczba sekund, przez które wiadomość czatu pozostanie edytowalna. (0 wyłączony)", + "restrictions.seconds-delete-after": "Liczba sekund, przez które wiadomość czatu pozostanie do usunięcia. (0 wyłączony)" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/cookies.json b/public/language/pl/admin/settings/cookies.json new file mode 100644 index 0000000000..c6c4cf6f85 --- /dev/null +++ b/public/language/pl/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Wymaganie EU", + "consent.enabled": "Włączone", + "consent.message": "Wiadomość powiadomienia", + "consent.acceptance": "Wiadomość o zaakceptowaniu", + "consent.link-text": "Tekst odnośnika do polityki", + "consent.link-url": "Link odnośnika do polityki", + "consent.blank-localised-default": "Pozostaw puste, aby użyć przetłumaczonych informacji domyślnych NodeBB ", + "settings": "Ustawienia", + "cookie-domain": "Domena plików cookie sesji", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Pozostaw puste, aby użyć wartości domyślnej" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/email.json b/public/language/pl/admin/settings/email.json new file mode 100644 index 0000000000..d68a780326 --- /dev/null +++ b/public/language/pl/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Ustawienia poczty", + "address": "Adres e-mail", + "address-help": "Ten adres e-mail odbiorca zobaczy w polach „Od” i „Odpowiedz”.", + "from": "Pole „Od”", + "from-help": "Nazwa „Od” widoczna w e-mailach", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "Transport SMTP", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Możesz wybrać z listy dobrze znanych usług lub wskazać usługę niestandardową.", + "smtp-transport.service": "Wybierz usługę", + "smtp-transport.service-custom": "Usługa niestandardowa", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "Host SMTP", + "smtp-transport.port": "Port SMTP", + "smtp-transport.security": "Bezpieczeństwo połączenia", + "smtp-transport.security-encrypted": "Szyfrowane", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Bez szyfrowania", + "smtp-transport.username": "Nazwa użytkownika", + "smtp-transport.username-help": "Dla usługi Gmail wprowadź pełny adres e-mail tutaj, zwłaszcza jeśli korzystasz z domeny zrządzanej przez G Suite.", + "smtp-transport.password": "Hasło", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edytuj szablon e-maila", + "template.select": "Wybierz szablon e-maila", + "template.revert": "Przywróć oryginalny szablon", + "testing": "Testowanie e-maila", + "testing.select": "Wybierz szablon e-maila", + "testing.send": "Wyślij testowy e-mail", + "testing.send-help": "Testowy e-mail zostanie wysłany na adres aktualnie zalogowanego użytkownika.", + "subscriptions": "Podsumowania e-mail", + "subscriptions.disable": "Wyłącz podsumowania e-maili", + "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. 0 dla północy lub 17 dla 17:00). Pamiętaj, że godzina jest godziną serwera i nie musi zgadzać się z czasem lokalnym administratora. Przybliżony czas serwera to:
Wysłanie kolejnego e-maila z podsumowaniem zaplanowano na ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/pl/admin/settings/general.json b/public/language/pl/admin/settings/general.json new file mode 100644 index 0000000000..04284cbb99 --- /dev/null +++ b/public/language/pl/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Ustawienia strony", + "title": "Tytuł strony", + "title.short": "Krótki tytuł", + "title.short-placeholder": "Jeśli nie wskazano krótkiego tytułu, użyty zostanie tytuł strony", + "title.url": "Title Link URL", + "title.url-placeholder": "Adres URL strony tytułowej", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Nazwa twojej społeczności", + "title.show-in-header": "Pokazuj tytuł strony w nagłówku", + "browser-title": "Tytuł karty przeglądarki", + "browser-title-help": "Jeśli nie wskazano tytułu karty przeglądarki, użyty zostanie tytuł strony", + "title-layout": "Struktura tytułu karty przeglądarki", + "title-layout-help": "Określ strukturę tytułu karty przeglądarki, np. {pageTitle} | {browserTitle}", + "description.placeholder": "Krótki opis twojej społeczności", + "description": "Opis strony", + "keywords": "Słowa kluczowe strony", + "keywords-placeholder": "Słowa kluczowe opisujące społeczność, oddzielone przecinkami", + "logo": "Logo strony", + "logo.image": "Obraz", + "logo.image-placeholder": "Ścieżka do logo, które ma być wyświetlane w nagłówku forum", + "logo.upload": "Prześlij", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "Adres URL logo strony", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alternatywny tekst", + "log.alt-text-placeholder": "Alternatywny tekst dla dostępności", + "favicon": "Favikona", + "favicon.upload": "Prześlij", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Prześlij", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Odnośniki wychodzące", + "outgoing-links.warning-page": "Używaj strony ostrzegawczej o odnośnikach wychodzących", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domeny na białej liście pozwalającej ominąć stronę ostrzegawczą", + "site-colors": "Metadane kolorów strony", + "theme-color": "Kolor przewodni", + "background-color": "Kolor tła", + "background-color-help": "Kolor wykorzystywany jako tło ekranu ładowania gdy strona jest zainstalowana jako PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/pl/admin/settings/group.json b/public/language/pl/admin/settings/group.json new file mode 100644 index 0000000000..9ffcccc941 --- /dev/null +++ b/public/language/pl/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Ogólne", + "private-groups": "Grupy prywatne", + "private-groups.help": "Jeśli ta opcja jest włączona, dołączenie do grupy wymaga zatwierdzenia przez właściciela grupy (domyślnie: włączone)", + "private-groups.warning": "Uwaga! Jeśli ta opcja jest wyłączona i masz prywatne grupy, automatycznie stają się one publiczne.", + "allow-multiple-badges": "Zezwól na korzystanie z wielu etykiet", + "allow-multiple-badges-help": "Dzięki tej fladze użytkownicy mogą wybierać różne etykiety dla grup, w zależności od tematu.", + "max-name-length": "Maksymalna długość nazwy grupy", + "max-title-length": "Maksymalna długość tytułu grupy", + "cover-image": "Obraz profilowy grupy", + "default-cover": "Domyślne obrazy profilowe", + "default-cover-help": "Dodaj rozdzielone przecinkiem domyślne obrazy profilowe dla grup, które nie przesłały własnych" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/guest.json b/public/language/pl/admin/settings/guest.json new file mode 100644 index 0000000000..d69ff83fc4 --- /dev/null +++ b/public/language/pl/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Ustawienia", + "handles.enabled": "Zezwalaj gościom na podpisywanie się", + "handles.enabled-help": "Opcja ta udostępnia gościom nowe pole, w którym mogą wybrać nazwę, pod jaką będą publikować posty. Jeśli opcja jest wyłączona, stosowana będzie po prostu nazwa „Gość”", + "topic-views.enabled": "Zezwalaj gościom na zwiększenie liczbę wyświetleń tematu", + "reply-notifications.enabled": "Zezwalaj gościom na generowanie powiadomień o odpowiedziach" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/homepage.json b/public/language/pl/admin/settings/homepage.json new file mode 100644 index 0000000000..0fc4160302 --- /dev/null +++ b/public/language/pl/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Strona główna", + "description": "Wybierz stronę startową dla forum", + "home-page-route": "Ścieżka strony głównej", + "custom-route": "Niestandardowa Ścieżka", + "allow-user-home-pages": "Zezwalaj na strony startowe użytkowników", + "home-page-title": "Tytuł strony głównej (domyślnie: „Strona Główna”)" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/languages.json b/public/language/pl/admin/settings/languages.json new file mode 100644 index 0000000000..6fa0554e20 --- /dev/null +++ b/public/language/pl/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Ustawienia językowe", + "description": "Domyślny język określa ustawienia języka dla wszystkich użytkowników, którzy odwiedzają forum.
Użytkownicy mogą zmienić domyślny język w ustawieniach swojego konta.", + "default-language": "Domyślny język", + "auto-detect": "Automatycznie wykrywaj język gości" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/navigation.json b/public/language/pl/admin/settings/navigation.json new file mode 100644 index 0000000000..36c88f8ca5 --- /dev/null +++ b/public/language/pl/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ikona:", + "change-icon": "zmień", + "route": "Ścieżka:", + "tooltip": "Tooltip:", + "text": "Tekst:", + "text-class": "Klasa tekstu opcjonalnie", + "class": "Klasa: opcjonalnie", + "id": "ID: opcjonalnie", + + "properties": "Ustawienia:", + "groups": "Grupy:", + "open-new-window": "Otwórz w nowym oknie", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Usuń", + "btn.disable": "Wyłącz", + "btn.enable": "Włącz", + + "available-menu-items": "Dostępne obiekty menu", + "custom-route": "Niestandardowa ścieżka", + "core": "system", + "plugin": "wtyczka" +} diff --git a/public/language/pl/admin/settings/notifications.json b/public/language/pl/admin/settings/notifications.json new file mode 100644 index 0000000000..6a3a4be2b8 --- /dev/null +++ b/public/language/pl/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Powiadomienia", + "welcome-notification": "Powiadomienie powitalne", + "welcome-notification-link": "Łącze do komunikatu powitalnego", + "welcome-notification-uid": "Powiadomienie powitalne użytkownika (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/pagination.json b/public/language/pl/admin/settings/pagination.json new file mode 100644 index 0000000000..7a7b914125 --- /dev/null +++ b/public/language/pl/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Ustawienia paginacji", + "enable": "Paginuj tematy oraz posty zamiast używać nieskończonego przewijania", + "posts": "Post Pagination", + "topics": "Paginacja tematów", + "posts-per-page": "Postów na stronie", + "max-posts-per-page": "Maksymalna liczba postów na stronie", + "categories": "Paginacja kategorii", + "topics-per-page": "Tematów na stronę", + "max-topics-per-page": "Maksymalna liczba tematów na stronie", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/post.json b/public/language/pl/admin/settings/post.json new file mode 100644 index 0000000000..140adf9302 --- /dev/null +++ b/public/language/pl/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Sortowanie postów", + "sorting.post-default": "Domyślne sortowanie postów", + "sorting.oldest-to-newest": "Od najstarszych do najnowszych", + "sorting.newest-to-oldest": "Od najnowszych do najstarszych", + "sorting.most-votes": "Najwięcej głosów", + "sorting.most-posts": "Najwięcej postów", + "sorting.topic-default": "Domyślne sortowanie tematów", + "length": "Długość postu", + "post-queue": "Kolejka postów", + "restrictions": "Restrykcje postowania", + "restrictions-new": "Restrykcje dla nowych użytkowników", + "restrictions.post-queue": "Włącz kolejkę postów", + "restrictions.post-queue-rep-threshold": "Reputacja wymagana do ominięcia kolejki postów", + "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.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.seconds-between": "Liczba sekund pomiędzy wpisami", + "restrictions.seconds-between-new": "Liczba sekund pomiędzy wpisami dla nowych użytkowników", + "restrictions.rep-threshold": "Próg reputacji wymagany do zdjęcia restrykcji", + "restrictions.seconds-before-new": "Sekundy, zanim nowy użytkownik może wykonać swój pierwszy post", + "restrictions.seconds-edit-after": "Liczba sekund, przez które wpisy mogą zostać edytowane. (0 wyłączone)", + "restrictions.seconds-delete-after": "Liczba sekund, przez które wpisy mogą zostać usunięte. (0 wyłączone)", + "restrictions.replies-no-delete": "Liczba odpowiedzi, po których użytkownicy nie mogą edytować własnych tematów (0 wyłącza)", + "restrictions.min-title-length": "Minimalna długość tytułu", + "restrictions.max-title-length": "Maksymalna długość tytułu", + "restrictions.min-post-length": "Minimalna długość postu", + "restrictions.max-post-length": "Maksymalna długość postu", + "restrictions.days-until-stale": "Liczba dni, po których temat będzie uznany za martwy", + "restrictions.stale-help": "Jeśli temat został uznany za „martwy”, użytkownikom próbującym na niego odpowiedzieć wyświetli się stosowany komunikat.", + "timestamp": "Znacznik czasowy", + "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).
(domyślnie: 30, lub jeden miesiąc). Ustaw 0, aby zawsze wyświetlać daty; pozostaw puste, aby korzystać z tylko z relatywnych opisów.", + "timestamp.necro-threshold": "Próg nekro (w dniach)", + "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: 7 lub jeden tydzień). Ustaw na 0, aby wyłączyć.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Zwiastun postu", + "teaser.last-post": "Ostatni – Pokaż ostatni post, włączając pierwszy post, w razie braku odpowiedzi", + "teaser.last-reply": "Ostatni – Pokaż ostatnią odpowiedź lub komunikat „Brak odpowiedzi” w razie ich braku", + "teaser.first": "Pierwszy", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Ustawienia nieprzeczytanych", + "unread.cutoff": "Dni do odcięcia nieprzeczytanych ", + "unread.min-track-last": "Minimalna liczba postów w temacie przed śledzeniem ostatnio przeczytanego", + "recent": "Ustawienia ostatnich", + "recent.max-topics": "Maksymalna liczba postów na stronie /recent", + "recent.categoryFilter.disable": "Wyłącz filtrowanie tematów w ignorowanych kategoriach na stronie /recent", + "signature": "Ustawienia sygnatur", + "signature.disable": "Wyłącz sygnatury", + "signature.no-links": "Wyłącz odnośniki w sygnaturach", + "signature.no-images": "Wyłącz obrazy w sygnaturach", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maksymalna długość sygnatury", + "composer": "Ustawienia okna pisania", + "composer-help": "Następujące ustawienia zarządzają funkcjonalnością oraz/lub wyglądem okna pisania postów wyświetlanego\n\t\t\t\tużytkownikom, gdy tworzą nowe tematy lub odpowiadają w istniejących.", + "composer.show-help": "Pokazuj zakładkę „Pomoc”", + "composer.enable-plugin-help": "Zezwalaj wtyczkom na dodawanie zawartości do zakładki pomocy", + "composer.custom-help": "Własny tekst pomocy", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Śledzenie IP", + "ip-tracking.each-post": "Śledź adres IP dla każdego postu", + "enable-post-history": "Włącz historię wpisu" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/reputation.json b/public/language/pl/admin/settings/reputation.json new file mode 100644 index 0000000000..2b5dfae219 --- /dev/null +++ b/public/language/pl/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Ustawienia reputacji", + "disable": "Wyłącz system reputacji", + "disable-down-voting": "Wyłącz system głosów przeciw", + "votes-are-public": "Wszystkie głosy są publiczne", + "thresholds": "Progi aktywności", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimalna reputacja pozwalająca głosować przeciw", + "downvotes-per-day": "Ilość głosów przeciw na dzień (ustaw na 0 by były nielimitowane)", + "downvotes-per-user-per-day": "Ilość głosów przeciw na użytkownika na dzień (ustaw na 0 by były nielimitowane)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimalna reputacja pozwalająca flagować posty", + "min-rep-website": "Minimalna reputacja pozwalająca wypełnić sekcję „Strona WWW” w profilu użytkownika", + "min-rep-aboutme": "Minimalna reputacja pozwalająca wypełnić sekcję „O mnie” w profilu użytkownika", + "min-rep-signature": "Minimalna reputacja pozwalająca wypełnić sekcję „Sygnatura” w profilu użytkownika", + "min-rep-profile-picture": "Minimalny poziom uprawnień, by dodać \"Zdjęcie profilowe\" w profilu użytkownika", + "min-rep-cover-picture": "Minimalny poziom uprawnień, by dodać \"Zdjęcie w tle\" w profilu użytkownika", + + "flags": "Ustawienia flag", + "flags.limit-per-target": "Maksymalna ilość razy coś może być oflagowane", + "flags.limit-per-target-placeholder": "Domyślnie: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/social.json b/public/language/pl/admin/settings/social.json new file mode 100644 index 0000000000..e75834e540 --- /dev/null +++ b/public/language/pl/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Udostępnianie postów", + "info-plugins-additional": "Wtyczki mogą dodać dodatkowe platformy do udostępniania postów", + "save-success": "Pomyślnie zapisano!" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/sockets.json b/public/language/pl/admin/settings/sockets.json new file mode 100644 index 0000000000..b752fc5720 --- /dev/null +++ b/public/language/pl/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Ustawienia ponownego łączenia", + "max-attempts": "Maksymalna liczba prób połączenia", + "default-placeholder": "Domyślnie: %1", + "delay": "Opóźnienie ponownego łączenia" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/sounds.json b/public/language/pl/admin/settings/sounds.json new file mode 100644 index 0000000000..1ab957ffa3 --- /dev/null +++ b/public/language/pl/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Powiadomienia", + "chat-messages": "Wiadomości czatu", + "play-sound": "Odtwórz", + "incoming-message": "Przychodzące wiadomości", + "outgoing-message": "Wychodzące wiadomości", + "upload-new-sound": "Prześlij nowy dźwięk", + "saved": "Ustawienia zapisane" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/tags.json b/public/language/pl/admin/settings/tags.json new file mode 100644 index 0000000000..528b4f66c7 --- /dev/null +++ b/public/language/pl/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Ustawienia tagów", + "link-to-manage": "Zarządzaj tagami", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimalna liczba tagów na temat", + "max-per-topic": "Maksymalna liczba tagów na temat", + "min-length": "Minimalna długość tagu", + "max-length": "Maksymalna długość tagu", + "related-topics": "Powiązane tematy", + "max-related-topics": "Maksymalna liczba powiązanych tematów do wyświetlenia (jeśli możliwe w ramach tematu)" +} \ No newline at end of file diff --git a/public/language/pl/admin/settings/uploads.json b/public/language/pl/admin/settings/uploads.json new file mode 100644 index 0000000000..5a742ea287 --- /dev/null +++ b/public/language/pl/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posty", + "orphans": "Orphaned Files", + "private": "Oznaczaj wysyłane pliki jako prywatne", + "strip-exif-data": "Usuń dane EXIF", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Rozszerzenia plików, które mają być prywatne", + "private-uploads-extensions-help": "Tutaj wpisz oddzielone przecinkami rozszerzenia plików, które mają być prywatne (np. pdf,xls,doc). Jeśli lista jest pusta, wszystkie pliki są prywatne.", + "resize-image-width-threshold": "Zmień rozmiar obrazów, jeśli są szersze niż określona szerokość", + "resize-image-width-threshold-help": "(w pikselach, domyślnie: 1520 pixeli, ustaw 0, aby wyłączyć)", + "resize-image-width": "Zmień rozmiar obrazów na określoną szerokość", + "resize-image-width-help": "(w pikselach, domyślnie: 760 pixeli, ustaw 0, aby wyłączyć)", + "resize-image-quality": "Poziom jakości użyty przy zmianie rozmiaru", + "resize-image-quality-help": "Użyj niższych ustawień jakości aby zredukować rozmiar pliku zmienionego obrazu.", + "max-file-size": "Maksymalny rozmiar plików (w KiB)", + "max-file-size-help": "(w kilobajtach, domyślnie: 2048 KiB)", + "reject-image-width": "Maksymalna szerokość obrazu (w pikselach)", + "reject-image-width-help": "Obrazy o szerokości przekraczającej tę wartość zostaną odrzucone.", + "reject-image-height": "Maksymalna wysokość obrazu (w pikselach)", + "reject-image-height-help": "Obrazy o wysokości przekraczającej tę wartość zostaną odrzucone.", + "allow-topic-thumbnails": "Zezwalaj użytkownikom na ustawianie miniaturek tematów", + "topic-thumb-size": "Rozmiar miniatury tematu", + "allowed-file-extensions": "Dozwolone typy plików", + "allowed-file-extensions-help": "Wprowadź rozdzielone przecinkami rozszerzenia plików (np. pdf,xls,doc). Pusta lista oznacza, że wszystkie rozszerzenia są dozwolone.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profilowe awatary", + "allow-profile-image-uploads": "Zezwalaj użytkownikom na ładowanie obrazów profilowych", + "convert-profile-image-png": "Konwertuj przesłane obrazy profilowe na PNG", + "default-avatar": "Własny domyślny awatar", + "upload": "Prześlij", + "profile-image-dimension": "Rozmiary obrazka profilowego", + "profile-image-dimension-help": "(w pikselach, domyślnie: 128px)", + "max-profile-image-size": "Maksymalny rozmiar obrazka profilowego", + "max-profile-image-size-help": "(w kilobajtach, domyślnie: 256 KiB)", + "max-cover-image-size": "Maksymalny rozmiar obrazka profilowego", + "max-cover-image-size-help": "(w kilobajtach, domyślnie: 2048 KiB)", + "keep-all-user-images": "Zachowaj stare wersje awatarów oraz okładek profili na serwerze", + "profile-covers": "Okładki profili", + "default-covers": "Domyślne obrazy profilowe", + "default-covers-help": "Dodaj rozdzieloną przecinkami listę domyślnych obrazów dla kont użytkowników, którzy nie wysłali swoich własnych obrazów profilowych." +} diff --git a/public/language/pl/admin/settings/user.json b/public/language/pl/admin/settings/user.json new file mode 100644 index 0000000000..0389a4abaa --- /dev/null +++ b/public/language/pl/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Uwierzytelnianie", + "email-confirm-interval": "Użytkownik nie może ponownie wysłać e-maila z potwierdzeniem, dopóki nie minie", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Zezwalaj na logowanie przy użyciu", + "allow-login-with.username-email": "Nazwy użytkownika lub adresu email", + "allow-login-with.username": "Tylko nazwy użytkownika", + "account-settings": "Ustawienia konta", + "gdpr_enabled": "Włącz gromadzenie danych (RODO)", + "gdpr_enabled_help": "Po włączeniu wszyscy nowi użytkownicy będą musieli jednoznacznie wyrazić zgodę na gromadzenie i wykorzystanie danych na mocy ogólnego rozporządzenia o ochronie danych (RODO). Uwaga: włączenie RODO nie zmusza istniejących użytkowników do wyrażenia zgody. Aby to zrobić, musisz zainstalować wtyczkę GDPR.", + "disable-username-changes": "Wyłącz możliwość zmiany nazwy użytkownika", + "disable-email-changes": "Wyłącz możliwość zmiany adresu e-mail", + "disable-password-changes": "Wyłącz możliwość zmiany hasła", + "allow-account-deletion": "Zezwalaj na usunięcie konta", + "hide-fullname": "Ukrywaj pełne imię i nazwisko przed innymi użytkownikami", + "hide-email": "Ukryj adresy e-mail użytkowników", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Motywy", + "disable-user-skins": "Nie zezwalaj użytkownikom na wybranie niestandardowej skórki", + "account-protection": "Ochrona konta", + "admin-relogin-duration": "Czas do ponownego logowania administratora (minuty)", + "admin-relogin-duration-help": "Po zdefiniowanym czasie dostęp do sekcji administracyjnej będzie wymagał ponownego logowania, ustaw 0 by wyłączyć.", + "login-attempts": "Maksymalna liczba prób logowania na godzinę", + "login-attempts-help": "Jeśli liczba prób logowania na konto użytkownika przekroczy ten próg, to konto zostanie zablokowane na zdefiniowany wcześniej czas", + "lockout-duration": "Czas trwania blokady konta (minuty)", + "login-days": "Liczba dni zapamiętywania sesji logowania użytkownika", + "password-expiry-days": "Wymuś resetowanie hasła po określonej liczbie dni", + "session-time": "Czas sesji", + "session-time-days": "Dni", + "session-time-seconds": "Sekund", + "session-time-help": "Te wartości określają czas, przez jaki użytkownik pozostaje zalogowany, gdy zaznaczy opcję "Zapamiętaj mnie" przy logowaniu. Użyta zostanie tylko jedna z tych wartości. Jeśli nie ma wartości sekundach, dostępne będą dni. W razie braku wartości w dniach domyślną wartością będzie 14 dni.", + "online-cutoff": "Po tylu minutach użytkownik zostaje uznany za nieaktywnego.", + "online-cutoff-help": "Jeśli użytkownik nie wykona żadnych działań w określonym czasie, zostaje on uznany za nieaktywnego i nie otrzyma aktualizacji w czasie rzeczywistym.", + "registration": "Rejestracja użytkownika", + "registration-type": "Typ rejestracji", + "registration-approval-type": "Typ zatwierdzenia rejestracji", + "registration-type.normal": "Standardowa", + "registration-type.admin-approval": "Zatwierdzenie przez administratora", + "registration-type.admin-approval-ip": "Zatwierdzenie administratora dla IP", + "registration-type.invite-only": "Tylko zaproszenia", + "registration-type.admin-invite-only": "Tylko zaproszenia administratora", + "registration-type.disabled": "Brak rejestracji", + "registration-type.help": "Standardowa - Użytkownicy mogą się rejestrować na stronie /register.
\nTylko zaproszenia - Użytkownicy mogą zapraszać innych poprzez stronę users.
\nTylko zaproszenia administratora - Tylko administratorzy mogą zapraszać innych poprzez stronę users oraz admin/manage/users.
\nBrak rejestracji - Brak rejestracji użytkowników.
", + "registration-approval-type.help": "Normalny - użytkownicy są rejestrowani natychmiast.
\nZatwierdzenie administratora - rejestracje użytkowników są umieszczane w kolejce zatwierdzania dla administratorów.
\n Kolejka zatwierdzania dla IPs - Normalne dla nowych użytkowników, kolejka zatwierdzania dla adresów IP, które już mają konto.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maksymalnie liczba zaproszeń na użytkownika", + "max-invites": "Maksymalnie liczba zaproszeń na użytkownika", + "max-invites-help": "0 dla braku ograniczeń. Administratorzy otrzymują nieskończoną liczbę zaproszeń
Aplikowane tylko dla \"Tylko zaproszeni\"", + "invite-expiration": "Wygasanie zaproszeń", + "invite-expiration-help": "Liczba dni, po których wygasają zaproszenia.", + "min-username-length": "Minimalna długość nazwy użytkownika", + "max-username-length": "Maksymalna długość nazwy użytkownika", + "min-password-length": "Minimalna długość hasła", + "min-password-strength": "Minimalna siła hasła", + "max-about-me-length": "Maksymalna długość pola O mnie", + "terms-of-use": "Warunki użytkowania forum (Pozostaw puste, aby wyłączyć)", + "user-search": "Wyszukiwanie użytkownków", + "user-search-results-per-page": "Liczba wyników do wyświetlenia", + "default-user-settings": "Domyślne ustawienia użytkownika", + "show-email": "Pokazuj adres e-mail", + "show-fullname": "Pokazuj pełną nazwę uzytkownika", + "restrict-chat": "Przyjmuj wiadomości na czacie tylko od osób, które obserwuję", + "outgoing-new-tab": "Otwieraj odnośniki wychodzące na nowej karcie", + "topic-search": "Włącz wyszukiwanie wewnątrz tematów", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Podsumowania - tryb", + "digest-freq.off": "Wyłączone", + "digest-freq.daily": "Dzienny ", + "digest-freq.weekly": "Tygodniowy", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Miesięczny", + "email-chat-notifs": "Wyślij powiadomienie email, jeśli dostanę nową wiadomość, a nie jestem on-line", + "email-post-notif": "Wyślij wiadomość email, kiedy w tematach, które subskrybuję, pojawią się odpowiedzi", + "follow-created-topics": "Obserwuj tematy, które stworzyłeś", + "follow-replied-topics": "Obserwuj tematy, w których się wypowiedziałeś ", + "default-notification-settings": "Domyślne ustawienia powiadomień", + "categoryWatchState": "Domyślny stan oglądania kategorii", + "categoryWatchState.watching": "Obserwowane", + "categoryWatchState.notwatching": "Nie obserwowane", + "categoryWatchState.ignoring": "Ignorowane" +} diff --git a/public/language/pl/admin/settings/web-crawler.json b/public/language/pl/admin/settings/web-crawler.json new file mode 100644 index 0000000000..c6b041a4c7 --- /dev/null +++ b/public/language/pl/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Ustawienia robotów sieciowych", + "robots-txt": "Własny Robots.txtPozostaw puste, aby użyć domyślnego", + "sitemap-feed-settings": "Ustawienia mapy strony oraz kanału", + "disable-rss-feeds": "Wyłącz kanały RSS", + "disable-sitemap-xml": "Wyłącz Sitemap.xml", + "sitemap-topics": "Liczba tematów do wyświetlenia w mapie strony", + "clear-sitemap-cache": "Wyczyść pamięć podręczną mapy strony", + "view-sitemap": "Wyświetl mapę strony" +} \ No newline at end of file diff --git a/public/language/pl/category.json b/public/language/pl/category.json new file mode 100644 index 0000000000..4bddf861b4 --- /dev/null +++ b/public/language/pl/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategoria", + "subcategories": "Podkategorie", + "new_topic_button": "Nowy temat", + "guest-login-post": "Zaloguj się, aby napisać post", + "no_topics": "W tej kategorii nie ma jeszcze żadnych tematów.
Może pora na napisanie pierwszego?", + "browsing": "przegląda", + "no_replies": "Nikt jeszcze nie odpowiedział", + "no_new_posts": "Brak nowych postów.", + "watch": "Obserwuj", + "ignore": "Ignoruj", + "watching": "Obserwowane", + "not-watching": "Nie obserwowane", + "ignoring": "Ignorowane", + "watching.description": "Pokaż tematy w nieprzeczytanych i najnowszych", + "not-watching.description": "Nie pokazuj tematów w nieprzeczytanym, pokaż je w ostatnim czasie", + "ignoring.description": "Nie pokazuj tematów w nieprzeczytanych i najnowszych", + "watching.message": "Obserwujesz teraz aktualizacje tej kategorii i wszystkie podkategorie", + "notwatching.message": "Obserwujesz teraz aktualizacje tej kategorii i wszystkie podkategorie", + "ignoring.message": "Obserwujesz teraz aktualizacje tej kategorii i wszystkie podkategorie", + "watched-categories": "Obserwowane kategorie", + "x-more-categories": "$1 więcej kategorii" +} \ No newline at end of file diff --git a/public/language/pl/email.json b/public/language/pl/email.json new file mode 100644 index 0000000000..13dd96a274 --- /dev/null +++ b/public/language/pl/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Testowy email", + "password-reset-requested": "Wymagane zrestartowanie hasła!", + "welcome-to": "Witaj na %1", + "invite": "Zaproszenie od %1", + "greeting_no_name": "Witaj", + "greeting_with_name": "Witaj, %1", + "email.verify-your-email.subject": "Zweryfikuj swój adres e-mail", + "email.verify.text1": "Zażądałeś zmiany albo potwierdzenia swojego adresu email", + "email.verify.text2": "Ze względów bezpieczeństwa, możemy zmienić lub potwierdzić adres email, wtedy gdy został zweryfikowany jako należący do Ciebie. Jeżeli to nie Ty wysłałeś to żądanie, nie musisz robić nic.", + "email.verify.text3": "Jak potwierdzisz swój adres email, zamienimy Twój obecny adres email na ten (%1).", + "welcome.text1": "Dziękujemy za rejestrację na %1", + "welcome.text2": "Aby w pełni aktywować konto, musisz potwierdzić, że podany adres e-mail należy do Ciebie.", + "welcome.text3": "Administrator zaakceptował Twoją prośbę o rejestrację. Możesz się teraz zalogować za pomocą swojej nazwy użytkownika i hasła.", + "welcome.cta": "Kliknij tutaj, aby potwierdzić swój adres e-mail", + "invitation.text1": "%1 zaprasza do dołączenia do %2", + "invitation.text2": "Twoje zaproszenie wygaśnie za %1 dni.", + "invitation.cta": "Kliknij tutaj, aby stworzyć konto.", + "reset.text1": "Otrzymaliśmy prośbę o reset Twojego hasła. Jeśli nie zgłaszałeś takiej prośby, zignoruj ten e-mail.", + "reset.text2": "Aby zresetować hasło, skorzystaj z poniższego odnośnika:", + "reset.cta": "Kliknij tutaj, aby zresetować swoje hasło", + "reset.notify.subject": "Hasło zmienione pomyślnie", + "reset.notify.text1": "Informujemy, że Twoje hasło na %1 zostało zmienione.", + "reset.notify.text2": "Jeśli nie wyraziłeś na to zgody, niezwłocznie poinformuj administratora.", + "digest.latest_topics": "Ostatnie tematy z %1", + "digest.top-topics": "Topowe tematy z %1", + "digest.popular-topics": "Najpopularniejsze tematy z %1", + "digest.cta": "Kliknij tutaj, by przejść do %1", + "digest.unsub.info": "To podsumowanie zostało wysłane zgodnie z Twoimi ustawieniami.", + "digest.day": "dni", + "digest.week": "tygodni", + "digest.month": "miesięcy", + "digest.subject": "Podsumowanie z %1", + "digest.title.day": "Twoje dzienne podsumowanie", + "digest.title.week": "Twoje tygodniowe podsumowanie", + "digest.title.month": "Twoje miesięczne podsumowanie", + "notif.chat.subject": "Nowa wiadomość na czacie od %1", + "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.post.unsub.info": "To powiadomienie o poście zostało wysłane zgodnie z Twoimi ustawieniami.", + "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-new-reply": "Pokaż wpisy", + "notif.cta-new-chat": "Pokaż czat", + "notif.test.short": "Przetestuj powiadomienia", + "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.", + "unsub.cta": "Kliknij tutaj, aby zmienić te ustawienia", + "unsubscribe": "Wypisz się", + "unsub.success": "Nie będziesz już otrzymywać wiadomości e-mail z %1", + "unsub.failure.title": "Nie udało się odsubskrybować", + "unsub.failure.message": "Niestety nie udało nam się wypisać Cię z listy mailingowej, ponieważ wystąpił problem z linkiem. Możesz jednak zmienić swoje preferencje dotyczące poczty e-mail, przechodząc do Twoich ustawień.

(błąd: %1)", + "banned.subject": "Zostałeś zbanowany na %1", + "banned.text1": "Użytkownik %1 został zbanowany na %2.", + "banned.text2": "Ban potrwa do %1", + "banned.text3": "Oto powód, dla którego zostałeś zbanowany:", + "closing": "Dziękujemy!" +} \ No newline at end of file diff --git a/public/language/pl/error.json b/public/language/pl/error.json new file mode 100644 index 0000000000..ba0618f597 --- /dev/null +++ b/public/language/pl/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Nieprawidłowe dane", + "invalid-json": "Niewłaściwy JSON", + "wrong-parameter-type": "Wartość typu %3 była oczekiwania dla właściwości `%1`, ale %2 został dostarczony", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Nie jesteś zalogowany/a.", + "account-locked": "Twoje konto zostało tymczasowo zablokowane", + "search-requires-login": "Wyszukiwanie wymaga konta - zaloguj się lub zarejestruj.", + "goback": "Wciśnij wstecz, aby powrócić do poprzedniej strony", + "invalid-cid": "Nieprawidłowy ID kategorii", + "invalid-tid": "Nieprawidłowy ID tematu", + "invalid-pid": "Nieprawidłowy ID posta", + "invalid-uid": "Nieprawidłowy ID użytkownika", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "Musi być podana prawidłowa data ", + "invalid-username": "Nieprawidłowy login", + "invalid-email": "Nieprawidłowy adres e-mail", + "invalid-fullname": "Nieprawidłowa nazwa", + "invalid-location": "Nieprawidłowa lokalizacja", + "invalid-birthday": "Nieprawidłowa data urodzenia", + "invalid-title": "Błędna nazwa", + "invalid-user-data": "Błędne dane użytkownika", + "invalid-password": "Błędne hasło", + "invalid-login-credentials": "Niewłaściwe dane logowania", + "invalid-username-or-password": "Podaj nazwę użytkownika i hasło", + "invalid-search-term": "Błędne wyszukiwane wyrażenie", + "invalid-url": "Błąd w adresie URL.", + "invalid-event": "Nieprawidłowe zdarzenie: %1", + "local-login-disabled": "System lokalnego logowania został wyłączony dla kont bez uprawnień.", + "csrf-invalid": "Logowanie nie powiodło się, zapewne na skutek wygaśnięcia sesji. Spróbuj ponownie.", + "invalid-path": "Nieprawidłowa ścieżka", + "folder-exists": "Folder istnieje", + "invalid-pagination-value": "Błędna wartość paginacji, zakres od %1 do %2", + "username-taken": "Login zajęty", + "email-taken": "Email zajęty", + "email-nochange": "Podany email jest taki sam jak ten już zapisany.", + "email-invited": "Ten adres email otrzymał już zaproszenie", + "email-not-confirmed": "Pisanie w niektórych kategoriach albo tematach jest dozwolone wtedy gdy Twój adres email został zweryfikowany, proszę kliknij tutaj aby wysłać potwierdzający 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": "Twój email nie został jeszcze zweryfikowany, proszę sprawdź swoją skrzynkę pocztową. Do tego czasu możesz nie móc pisać w niektórych kategoriach albo rozmawiać na czacie.", + "no-email-to-confirm": "Twoje konto nie ma ustawionego adresu email. Adres email jest konieczny w celu odzyskania konta i może być wymagany do pisania na czacie a także pisania w niektórych kategoriach. Proszę kliknij tutaj aby podać adres email.", + "user-doesnt-have-email": "Użytkownik \"%1\" nie ma ustawionego adresu email.", + "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.", + "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": "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-long": "Zbyt długa nazwa użytkownika", + "password-too-long": "Hasło jest za długie", + "reset-rate-limited": "Zbyt wiele żądań resetowania hasła (ograniczona ilość)", + "reset-same-password": "Proszę użyj innego hasła niż Twoje obecne", + "user-banned": "Użytkownik zbanowany", + "user-banned-reason": "Twoje konto zostało zablokowane (Powód: %1)", + "user-banned-reason-until": "Przepraszamy, to konto zostało zbanowane do %1 (Powód: %2)", + "user-too-new": "Przepraszamy, musisz odczekać %1 sekund(y) przed utworzeniem pierwszego posta", + "blacklisted-ip": "Twój adres IP został zablokowany na tej społeczności. Jeśli uważasz to za błąd, zgłoś to administratorowi", + "ban-expiry-missing": "Wprowadź datę końca blokady", + "no-category": "Kategoria nie istnieje", + "no-topic": "Temat nie istnieje", + "no-post": "Post nie istnieje", + "no-group": "Grupa nie istnieje", + "no-user": "Użytkownik nie istnieje", + "no-teaser": "Zwiastun nie istnieje", + "no-flag": "Flag does not exist", + "no-privileges": "Nie masz przywileju wykonywania tej akcji", + "category-disabled": "Kategoria wyłączona.", + "topic-locked": "Temat zablokowany", + "post-edit-duration-expired": "Możesz edytować posty tylko przez %1 sekund(y) po ich napisaniu", + "post-edit-duration-expired-minutes": "Możesz edytować posty tylko przez %1 minut(y) po ich napisaniu", + "post-edit-duration-expired-minutes-seconds": "Możesz edytować posty tylko przez %1 minut(y) i %2 sekund(y) po ich napisaniu", + "post-edit-duration-expired-hours": "Możesz edytować posty tylko przez %1 godzin(y) po ich napisaniu", + "post-edit-duration-expired-hours-minutes": "Możesz edytować posty tylko przez %1 godzin(y) i %2 minut(y) po ich napisaniu", + "post-edit-duration-expired-days": "Możesz edytować posty tylko przez %1 dzień (dni) po ich napisaniu", + "post-edit-duration-expired-days-hours": "Możesz edytować posty tylko przez %1 dzień (dni) i %2 godzin(y) po ich napisaniu", + "post-delete-duration-expired": "Możesz kasować posty przez %1 sekund(-y) po napisaniu", + "post-delete-duration-expired-minutes": "Możesz kasować posty przez %1 minut(-y) po napisaniu", + "post-delete-duration-expired-minutes-seconds": "Możesz kasować posty przez %1 minut(-y) i %2 sekund(-y) po napisaniu", + "post-delete-duration-expired-hours": "Możesz kasować posty przez %1 godzin(-y) po napisaniu", + "post-delete-duration-expired-hours-minutes": "Możesz kasować posty przez %1 godzin(-y) i %2 minut(-y) po napisaniu", + "post-delete-duration-expired-days": "Możesz kasować posty przez %1 dni po napisaniu", + "post-delete-duration-expired-days-hours": "Możesz kasować posty przez %1 dni i %2 godzin(-y) po napisaniu", + "cant-delete-topic-has-reply": "Nie możesz usunąć tematu zawierającego odpowiedź", + "cant-delete-topic-has-replies": "Nie możesz usunąć tematu zawierającego %1 odpowiedzi", + "content-too-short": "Wpisz dłuższy post. Posty powinny zawierać co najmniej %1 znaków.", + "content-too-long": "Wpisz krótszy post. Posty nie mogą zawierać więcej niż %1 znaków.", + "title-too-short": "Wpisz dłuższy tytuł. Tytuły powinny liczyć co najmniej %1 znaków.", + "title-too-long": "Wpisz krótszy tytuł. Tytuły nie mogą zawierać więcej niż %1 znaków.", + "category-not-selected": "Nie wybrano kategorii.", + "too-many-posts": "Możesz publikować posty raz na %1 sekund – poczekaj, zanim dodasz kolejny post", + "too-many-posts-newbie": "Jako nowy użytkownik możesz publikować posty raz na %1 sekund, dopóki nie zdobędziesz reputacji na poziomie %2 – poczekaj, zanim dodasz kolejny post", + "already-posting": "You are already posting", + "tag-too-short": "Wprowadź dłuższy tag. Tagi muszą mieć przynajmniej %1 znak(-ów)", + "tag-too-long": "Wprowadź krótszy tag. Tagi nie mogą mieć więcej niż %1 znak(-ów)", + "not-enough-tags": "Zbyt mało tagów. Tematy muszą posiadać przynajmniej %1 tag(ów)", + "too-many-tags": "Zbyt wiele tagów. Tematy nie mogą posiadać więcej niż %1 tag(ów)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Poczekaj na zakończenie przesyłania", + "file-too-big": "Maksymalny dopuszczalny rozmiar pliku to %1 kB – prześlij mniejszy plik", + "guest-upload-disabled": "Przesyłanie plików przez gości zostało wyłączone", + "cors-error": "Nie można przesłać obrazu z powodu źle skonfigurowanego CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Już dodałeś ten post do zakładek", + "already-unbookmarked": "Już usunąłeś ten post z zakładek", + "cant-ban-other-admins": "Nie możesz zbanować innych adminów!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "Zostałeś wyciszony, będziesz mógł pisać po upływie %1 godziny(godzin)", + "user-muted-for-minutes": "Zostałeś wyciszony, będziesz mógł pisać po upływie %1 minut", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Jesteś jedynym administratorem. Dodaj innego użytkownika jako administratora przed usunięciem siebie z tej grupy", + "account-deletion-disabled": "Usuwanie konta jest wyłączone", + "cant-delete-admin": "Usuń uprawnienia administratora z tego konta przed próbą jego usunięcia.", + "already-deleting": "W trakcie usuwania", + "invalid-image": "Błędny obraz.", + "invalid-image-type": "Błędny typ obrazka. Dozwolone typy to: %1", + "invalid-image-extension": "Błędne rozszerzenie pliku", + "invalid-file-type": "Błędny typ pliku. Dozwolone typy to: %1", + "invalid-image-dimensions": "Rozmiary obrazu są zbyt duże", + "group-name-too-short": "Nazwa grupy jest za krótka", + "group-name-too-long": "Nazwa grupy jest za długa", + "group-already-exists": "Grupa już istnieje", + "group-name-change-not-allowed": "Nie można zmieniać nazwy tej grupy.", + "group-already-member": "Już jesteś członkiem tej grupy", + "group-not-member": "Nie jesteś członkiem tej grupy", + "group-needs-owner": "Ta grupa musi mieć przynajmniej jednego właściciela", + "group-already-invited": "Ten użytkownik został już zaproszony", + "group-already-requested": "Twoje podanie o członkostwo zostało już wysłane", + "group-join-disabled": "Nie możesz teraz dołączyć do tej grupy", + "group-leave-disabled": "Obecnie nie możesz opuścić tej grupy", + "post-already-deleted": "Ten post został już skasowany", + "post-already-restored": "Ten post został już przywrócony", + "topic-already-deleted": "Ten temat został już skasowany", + "topic-already-restored": "Ten temat został już przywrócony", + "cant-purge-main-post": "Nie możesz wymazać głównego posta, zamiast tego usuń temat", + "topic-thumbnails-are-disabled": "Miniatury tematów są wyłączone.", + "invalid-file": "Błędny plik", + "uploads-are-disabled": "Przesyłanie plików jest wyłączone", + "signature-too-long": "Przepraszamy, Twoja sygnatura nie może być dłuższa niż %1 znaków.", + "about-me-too-long": "Przepraszamy, Twój tekst „O mnie” nie może być dłuższy niż %1 znaków.", + "cant-chat-with-yourself": "Nie możesz rozmawiać sam ze sobą!", + "chat-restricted": "Ten użytkownik korzysta z czatu w ograniczonym zakresie. Mogą z nim rozmawiać tylko te osoby, które obserwuje.", + "chat-disabled": "System rozmów jest wyłączony", + "too-many-messages": "Wysłałeś zbyt wiele wiadomości, prosimy chwilę poczekać.", + "invalid-chat-message": "Nieprawidłowa wiadomość", + "chat-message-too-long": "Wiadomości czatu nie mogą być dłuższe niż %1 znaków.", + "cant-edit-chat-message": "Nie jesteś upoważniony do edycji tej wiadomości", + "cant-delete-chat-message": "Nie jesteś upoważniony do usunięcia tej wiadomości", + "chat-edit-duration-expired": "Możesz edytować komunikat czatu tylko przez %1 sekund(y) po napisaniu.", + "chat-delete-duration-expired": "Możesz skasować komunikat czatu tylko przez %1 sekund(y) po napisaniu.", + "chat-deleted-already": "Ten komunikat czatu jest już skasowany", + "chat-restored-already": "Ta wiadomość została już przywrócona", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Już zagłosowałeś na ten post", + "reputation-system-disabled": "System reputacji jest wyłączony.", + "downvoting-disabled": "Negatywna ocena postów jest wyłączona", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "Potrzebujesz %1 reputacji aby głosować pozytywnie", + "not-enough-reputation-to-downvote": "Potrzebujesz %1 reputacji aby głosować negatywnie", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "Potrzebujesz %1 reputacji aby dodać sekcję o mnie", + "not-enough-reputation-min-rep-signature": "Potrzebujesz %1 reputacji aby dodać podpis", + "not-enough-reputation-min-rep-profile-picture": "Potrzebujesz %1 reputacji aby dodać zdjęcie profilowe", + "not-enough-reputation-min-rep-cover-picture": "Potrzebujesz %1 reputacji aby dodać zdjęcie w tle", + "post-already-flagged": "Ten post został już przez Ciebie oflagowany", + "user-already-flagged": "Ten użytkownik został już przez ciebie oflagowany", + "post-flagged-too-many-times": "Ten post został już oflagowany przez innych użytkowników", + "user-flagged-too-many-times": "Ten użytkownik został już oflagowany przez innych użytkowników", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Nie możesz głosować na swój własny wpis.", + "too-many-upvotes-today": "Możesz jedynie oceniać pozytywnie %1 razy dziennie", + "too-many-upvotes-today-user": "Możesz jedynie oceniać danego użytkownika pozytywnie %1 razy dziennie", + "too-many-downvotes-today": "Możesz głosować przeciw postowi tylko %1 razy dziennie", + "too-many-downvotes-today-user": "Możesz głosować przeciwko użytkownikowi tylko %1 razy dziennie", + "reload-failed": "NodeBB napotkało problem w czasie przeładowywania \"%1\". Forum będzie nadal dostarczać istniejące zasoby strony klienta, jednak powinieneś cofnąć ostatnią akcję.", + "registration-error": "Błąd rejestracji", + "parse-error": "Coś poszło nie tak podczas przetwarzania odpowiedzi serwera", + "wrong-login-type-email": "Zaloguj się za pomocą adresu e-mail", + "wrong-login-type-username": "Zaloguj się za pomocą nazwy użytkownika", + "sso-registration-disabled": "Rejestracja dla kont %1 jest zablokowana. Zarejestruj się najpierw za pomocą adresu e-mail.", + "sso-multiple-association": "Nie można dowiązać wielu kont z tego serwisu do twojego konta NodeBB. Proszę odwiązać istniejące konto i spróbować ponownie.", + "invite-maximum-met": "Zaprosiłeś maksymalną liczbę osób (%1 z %2).", + "no-session-found": "Nie znaleziono sesji logowania", + "not-in-room": "Użytkownika nie ma w pokoju", + "cant-kick-self": "Nie możesz wyrzucić samego siebie z grupy", + "no-users-selected": "Nie wybrano żadnych użytkowników", + "invalid-home-page-route": "Niepoprawny odnośnik strony domowej", + "invalid-session": "Nieprawidłowa sesja", + "invalid-session-text": "Wygląda na to, że Twoja sesja wygasła. Proszę odśwież stronę.", + "session-mismatch": "Niezgodność sesji", + "session-mismatch-text": "Wygląda na to, że Twoja sesja nie jest odpowiednia dla serwera. Proszę odśwież tą stronę.", + "no-topics-selected": "Nie wybrano tematów.", + "cant-move-to-same-topic": "Nie można przenieść wpisu do tego samego tematu!", + "cant-move-topic-to-same-category": "Nie można przenieść tematu do tej samej kategorii!", + "cannot-block-self": "Nie możesz zablokować samego siebie!", + "cannot-block-privileged": "Nie możesz blokować administratorów ani globalnych moderatorów", + "cannot-block-guest": "Goście nie mogą blokować innych użytkowników", + "already-blocked": "Ten użytkownik jest już zablokowany", + "already-unblocked": "Ten użytkownik jest już odblokowany", + "no-connection": "Sprawdź swoje połączenie z internetem", + "socket-reconnect-failed": "W tej chwili nie można połączyć się z serwerem. Kliknij tutaj, aby spróbować ponownie, lub spróbuj ponownie później", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Coś było nie tak z przekazaną treścią żądania.", + "api.401": "Poprawna sesja logowanie nie została znaleziona. Proszę zaloguj się i spróbuj ponownie.", + "api.403": "Nie masz uprawnień do wykonania tego żądania", + "api.404": "Invalid API call", + "api.426": "HTTPS jest wymagany dla żądań do API zapisu, wyślij ponownie żądanie przez HTTPS", + "api.429": "Został przekroczony limit żądań, proszę spróbuj ponownie później", + "api.500": "Wystąpił nieoczekiwany błąd podczas próby obsługi Twojego żądania.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "Ścieżka z którą próbujesz się połączyć, jest obecnie niedostępna z powodu konfiguracji serwera" +} \ No newline at end of file diff --git a/public/language/pl/flags.json b/public/language/pl/flags.json new file mode 100644 index 0000000000..37ec744c1e --- /dev/null +++ b/public/language/pl/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Stan", + "reports": "Zgłoszenia", + "first-reported": "Pierwszy zgłoszony", + "no-flags": "Hura! Nie znaleziono flag.", + "assignee": "Oflagowany", + "update": "Zaktualizuj", + "updated": "Zaaktualizowano", + "resolved": "Resolved", + "target-purged": "Treści, do których odnosi się ta flaga, zostały usunięte i nie są już dostępne.", + + "graph-label": "Codzienne flagi", + "quick-filters": "Szybkie filtry", + "filter-active": "Istnieje co najmniej jeden aktywny filtr w tej liście flag", + "filter-reset": "Usuń filtry", + "filters": "Opcje filtrowania", + "filter-reporterId": "UID zgłaszającego", + "filter-targetUid": "Oflagowany UID", + "filter-type": "Typ flagi", + "filter-type-all": "Cała treść", + "filter-type-post": "Post", + "filter-type-user": "Użytkownik", + "filter-state": "Stan", + "filter-assignee": "UID oflagowanego", + "filter-cid": "Kategoria", + "filter-quick-mine": "Przypisane do mnie", + "filter-cid-all": "Wszystkie kategorie", + "apply-filters": "Zastosuj filtry", + "more-filters": "Więcej filtrów", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Szybkie Akcje", + "flagged-user": "Oflagowany użytkownik", + "view-profile": "Zobacz profil", + "start-new-chat": "Rozpocznij nowy czat", + "go-to-target": "Zobacz cel flagowania", + "assign-to-me": "Przypisz do mnie", + "delete-post": "Usuń post", + "purge-post": "Wyczyść post", + "restore-post": "Przywróć post", + "delete": "Delete Flag", + + "user-view": "Zobacz profil", + "user-edit": "Edytuj profil", + + "notes": "Notatki do flagi", + "add-note": "Dodaj notatkę", + "no-notes": "Brak udostępnionych notatek", + "delete-note-confirm": "Czy na pewno chcesz usunąć tę notatkę do flagi?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Dodano notatkę", + "note-deleted": "Notatka usunięta", + "flag-deleted": "Flag Deleted", + + "history": "Konto i historia flag", + "no-history": "Brak historii flag", + + "state-all": "Wszystkie stany", + "state-open": "Nowy/Otwarty", + "state-wip": "W trakcie prac", + "state-resolved": "Rozwiązano", + "state-rejected": "Odrzucono", + "no-assignee": "Nie przypisano", + + "sort": "Sortuj według", + "sort-newest": "Najpierw najnowsze", + "sort-oldest": "Najpierw najstarsze", + "sort-reports": "Najwięcej zgłoszeń", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Wskaż powód oflagowania i zgłoszenia %1 %2 do oceny. Jeśli to możliwe, użyj jednego z przycisków szybkiego zgłoszenia.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Treści obraźliwe", + "modal-reason-other": "Inne (wybierz poniżej)", + "modal-reason-custom": "Powód zgłaszania treści", + "modal-submit": "Wyślij zgłoszenie", + "modal-submit-success": "Treści zostały oflagowane i zgłoszone do moderacji.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Oznacz flagi jako rozwiązane", + "bulk-success": "Zaktualizowano %1 flag", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/pl/global.json b/public/language/pl/global.json new file mode 100644 index 0000000000..7f4600a6cb --- /dev/null +++ b/public/language/pl/global.json @@ -0,0 +1,126 @@ +{ + "home": "Strona startowa", + "search": "Szukaj", + "buttons.close": "Zamknij", + "403.title": "Dostęp zabroniony", + "403.message": "Wygląda na to, że trafiłeś na stronę, do której nie masz dostępu.", + "403.login": "Może powinieneś się zalogować?", + "404.title": "Nie znaleziono", + "404.message": "Wygląda na to, że trafiłeś na stronę, która nie istnieje. Wróć do strony startowej.", + "500.title": "Wewnętrzny błąd.", + "500.message": "Ups! Coś poszło nie tak.", + "400.title": "Złe zapytanie.", + "400.message": "Zdaje się, że ten odnośnik jest nieprawidłowy. Sprawdź odnośnik i spróbuj ponownie albo wróć na stronę startową.", + "register": "Zarejestruj się", + "login": "Zaloguj się", + "please_log_in": "Proszę się zalogować", + "logout": "Wyloguj się", + "posting_restriction_info": "Posty mogą pisać tylko zarejestrowani użytkownicy forum. Kliknij tutaj, aby się zalogować.", + "welcome_back": "Witaj ponownie,", + "you_have_successfully_logged_in": "Logowanie powiodło się.", + "save_changes": "Zapisz zmiany", + "save": "Zapisz", + "close": "Zamknij", + "pagination": "Numerowanie stron", + "pagination.out_of": "%1 z %2", + "pagination.enter_index": "Skocz do postu", + "header.admin": "Administracja", + "header.categories": "Kategorie", + "header.recent": "Ostatnie", + "header.unread": "Nieprzeczytane", + "header.tags": "Tagi", + "header.popular": "Popularne", + "header.top": "Najlepsze", + "header.users": "Użytkownicy", + "header.groups": "Grupy", + "header.chats": "Czaty", + "header.notifications": "Powiadomienia", + "header.search": "Szukaj", + "header.profile": "Profil", + "header.navigation": "Nawigacja", + "notifications.loading": "Ładowanie powiadomień", + "chats.loading": "Ładowanie rozmów", + "motd.welcome": "Witaj w NodeBB, platformie dyskusyjnej przyszłości.", + "previouspage": "Poprzednia strona", + "nextpage": "Następna strona", + "alert.success": "Udało się", + "alert.error": "Błąd", + "alert.banned": "Ban", + "alert.banned.message": "Zostałeś zbanowany i Twoje konto jest teraz w trybie ograniczonych możliwości", + "alert.unbanned": "Odbanowany", + "alert.unbanned.message": "Twój ban został zniesiony", + "alert.unfollow": "Nie obserwujesz już %1.", + "alert.follow": "Obserwujesz %1.", + "users": "Użytkownicy", + "topics": "Tematy", + "posts": "Posty", + "x-posts": "%1 posty", + "best": "Najlepsze", + "controversial": "Kontrowersyjne", + "votes": "Głosy", + "x-votes": "%1 głosy", + "voters": "Głosujący", + "upvoters": "Głosujący za", + "upvoted": "Oddane głosy za", + "downvoters": "Głosujący przeciw", + "downvoted": "Oddane głosy przeciw", + "views": "Wyświetlenia", + "posters": "Posters", + "reputation": "Reputacja", + "lastpost": "Ostatni post", + "firstpost": "Pierwszy post", + "read_more": "czytaj więcej", + "more": "Więcej", + "none": "Żadna", + "posted_ago_by_guest": "wysłany %1 przez Gościa", + "posted_ago_by": "wysłany %1 przez %2", + "posted_ago": "wysłany %1", + "posted_in": "napisane w %1", + "posted_in_by": "napisane w %1 przez %2", + "posted_in_ago": "wysłany w %1 %2", + "posted_in_ago_by": "wysłany w %1 %2 przez %3", + "user_posted_ago": "%1 napisał %2", + "guest_posted_ago": "Gość napisał %1", + "last_edited_by": "ostatnio edytowany przez %1", + "norecentposts": "Brak ostatnich postów", + "norecenttopics": "Brak ostatnich tematów", + "recentposts": "Ostatnie posty", + "recentips": "Adresy IP ostatnich logowań", + "moderator_tools": "Narzędzia dla moderatorów", + "online": "Online", + "away": "Zaraz wracam", + "dnd": "Nie przeszkadzać", + "invisible": "Niewidoczny", + "offline": "Niedostępny", + "email": "Adres e-mail", + "language": "Język", + "guest": "Gość", + "guests": "Goście", + "former_user": "Dawny użytkownik", + "system-user": "System", + "unknown-user": "Nieznany użytkownik", + "updated.title": "Forum zaktualizowane", + "updated.message": "To forum zostało zaktualizowane do najnowszej wersji. Kliknij tutaj, by odświeżyć stronę.", + "privacy": "Prywatność", + "follow": "Obserwuj", + "unfollow": "Przestań obserwować", + "delete_all": "Usuń wszystko", + "map": "Mapa", + "sessions": "Sesje logowania", + "ip_address": "Adres IP", + "enter_page_number": "Wpisz numer strony", + "upload_file": "Prześlij plik", + "upload": "Prześlij", + "uploads": "Przesłane pliki", + "allowed-file-types": "Dozwolone typy plików %1", + "unsaved-changes": "Twoje zmiany nie zostały zapisane. Czy na pewno chcesz opuścić stronę?", + "reconnecting-message": "Zdaje się, że Twoje połączenie z %1 zostało przerwane. Zaczekaj na ponowne nawiązanie połączenia.", + "play": "Odtwórz", + "cookies.message": "Ta strona używa plików cookies, by zapewnić Ci wygodę użytkowania.", + "cookies.accept": "Rozumiem!", + "cookies.learn_more": "Dowiedz się więcej", + "edited": "Edytowany", + "disabled": "Wyłączony", + "select": "Wybierz", + "user-search-prompt": "Aby znaleźć użytkowników, wpisz tutaj..." +} \ No newline at end of file diff --git a/public/language/pl/groups.json b/public/language/pl/groups.json new file mode 100644 index 0000000000..70c903b8a0 --- /dev/null +++ b/public/language/pl/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupy", + "view_group": "Obejrzyj grupę", + "owner": "Właściciel grupy", + "new_group": "Stwórz nową grupę", + "no_groups_found": "Brak grup do wyświetlenia", + "pending.accept": "Przyjmij", + "pending.reject": "Odrzuć", + "pending.accept_all": "Przyjmij wszystkie", + "pending.reject_all": "Odrzuć wszystkie", + "pending.none": "Nie ma w tym momencie żadnych oczekujących członków", + "invited.none": "Nie ma w tym momencie żadnych zaproszonych członków", + "invited.uninvite": "Cofnij zaproszenie", + "invited.search": "Wyszukaj użytkownika, aby zaprosić go do tej grupy", + "invited.notification_title": "Otrzymano zaproszenie do dołączenia do %1", + "request.notification_title": "Podanie o członkostwo w grupie od %1", + "request.notification_text": "%1 chce zostać członkiem %2", + "cover-save": "Zapisz", + "cover-saving": "Zapisuję", + "details.title": "Szczegóły grupy", + "details.members": "Lista członków", + "details.pending": "Członkowie oczekujący", + "details.invited": "Zaproszeni Członkowie", + "details.has_no_posts": "Członkowie tej grupy nie napisali żadnych postów.", + "details.latest_posts": "Ostatnie posty", + "details.private": "Prywatna", + "details.disableJoinRequests": "Wyłączono prośbę o dołączenie", + "details.disableLeave": "Wyłącz możliwość opuszczania użytkowników z grupy", + "details.grant": "Nadaj/Cofnij prawa Właściciela", + "details.kick": "Wykop", + "details.kick_confirm": "Jesteś pewny, że chcesz wyrzucić tego użytkownika z grupy?", + "details.add-member": "Dodaj członka", + "details.owner_options": "Administracja grupy", + "details.group_name": "Nazwa grupy", + "details.member_count": "Liczba Członków", + "details.creation_date": "Data Utworzenia", + "details.description": "Opis", + "details.member-post-cids": "ID kategorii, z której wyświetlone są posty", + "details.badge_preview": "Podgląd etykiety", + "details.change_icon": "Zmień ikonę", + "details.change_label_colour": "Zmień kolor etykiety", + "details.change_text_colour": "Zmień kolor tekstu", + "details.badge_text": "Treść etykiety", + "details.userTitleEnabled": "Pokaż etykietę", + "details.private_help": "Jeśli aktywowane, przystępowanie do grup wymaga zatwierdzenia przez właściciela grupy", + "details.hidden": "Ukryty", + "details.hidden_help": "Jeśli aktywowane, ta grupa nie będzie widoczna w wykazie grup, a użytkownicy będą musieli być zapraszani manualnie.", + "details.delete_group": "Usuń grupę", + "details.private_system_help": "Prywatne grupy zostały zablokowane w systemie, ta opcja nic nie zmienia.", + "event.updated": "Dane grupy zostały zaktualizowane", + "event.deleted": "Grupa \"%1\" została usunięta", + "membership.accept-invitation": "Przyjmij zaproszenie", + "membership.accept.notification_title": "Jesteś teraz członkiem %1", + "membership.invitation-pending": "Oczekujące zaproszenie", + "membership.join-group": "Dołącz do grupy", + "membership.leave-group": "Opuść grupę", + "membership.leave.notification_title": "%1 opuścił grupę %2", + "membership.reject": "Odrzuć", + "new-group.group_name": "Nazwa grupy:", + "upload-group-cover": "Prześlij zdjęcie tła grupy", + "bulk-invite-instructions": "Wprowadź listę oddzielonych przecinkami nazw użytkowników, których chcesz zaprosić do tej grupy", + "bulk-invite": "Masowe zaproszenie", + "remove_group_cover_confirm": "Czy na pewno chcesz usunąć zdjęcie w tle?" +} \ No newline at end of file diff --git a/public/language/pl/ip-blacklist.json b/public/language/pl/ip-blacklist.json new file mode 100644 index 0000000000..6616e9c1af --- /dev/null +++ b/public/language/pl/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Tutaj skonfiguruj czarną listę IP", + "description": "Czasem ban konta użytkownika nie jest wystarczającym zabezpieczeniem. Wówczas najlepszą metodą ochrony forum jest ograniczenie dostępu do forum z konkretnego adresu IP lub zakresu adresów IP. W tym przypadku możesz dodać adresy IP lub całe bloki CIDR do czarnej listy i tym samym uniemożliwić im logowanie się na forum lub zakładanie nowych kont.", + "active-rules": "Aktywne reguły", + "validate": "Sprawdź czarną listę", + "apply": "Zastosuj czarną listę", + "hints": "Podpowiedzi składni", + "hint-1": "W każdej linii zdefiniuj pojedynczy adres IP. Możesz dodać bloki IP pod warunkiem, że spełniają one wymagania formatu CIDR (np. 192.168.100.0/22).", + "hint-2": "Możesz dodawać komentarze poprzez rozpoczęcie linii symbolem #.", + + "validate.x-valid": "%1 z %2 reguł jest poprawnych.", + "validate.x-invalid": "Następujące %1 reguły są niewłaściwe:", + + "alerts.applied-success": "Zastosowano czarną listę", + + "analytics.blacklist-hourly": "Ilustracja 1 – Wpisy z czarnej listy na godzinę", + "analytics.blacklist-daily": "Ilustracja 2 – Wpisy z czarnej listy na dzień", + "ip-banned": "Zbanowany adres IP" +} \ No newline at end of file diff --git a/public/language/pl/language.json b/public/language/pl/language.json new file mode 100644 index 0000000000..e9506feed9 --- /dev/null +++ b/public/language/pl/language.json @@ -0,0 +1,5 @@ +{ + "name": "Polski", + "code": "pl", + "dir": "od lewej do prawej" +} \ No newline at end of file diff --git a/public/language/pl/login.json b/public/language/pl/login.json new file mode 100644 index 0000000000..8266068dc5 --- /dev/null +++ b/public/language/pl/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Nazwa użytkownika lub adres e-mail", + "username": "Nazwa użytkownika", + "remember_me": "Zapamiętaj mnie", + "forgot_password": "Nie pamiętasz hasła?", + "alternative_logins": "Alternatywne logowanie", + "failed_login_attempt": "Logowanie nie powiodło się.", + "login_successful": "Logowanie powiodło się.", + "dont_have_account": "Nie masz konta?", + "logged-out-due-to-inactivity": "Zostałeś wylogowany z Panelu Administratora z powodu braku aktywności.", + "caps-lock-enabled": "Caps Lock jest włączony" +} \ No newline at end of file diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json new file mode 100644 index 0000000000..bf26e04ef3 --- /dev/null +++ b/public/language/pl/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Czatuj z", + "chat.placeholder": "Wpisz tutaj wiadomość, przeciągnij i opuść obrazki, kliknij enter aby wysłać", + "chat.scroll-up-alert": "Przeglądasz starsze wiadomości, naciśnij tutaj by przejść do najnowszych", + "chat.send": "Wyślij", + "chat.no_active": "Brak aktywnych czatów", + "chat.user_typing": "%1 pisze...", + "chat.user_has_messaged_you": "%1 napisał do Ciebie", + "chat.see_all": "Wszystkie rozmowy", + "chat.mark_all_read": "Zaznacz wszystkie jako przeczytane", + "chat.no-messages": "Wybierz adresata, by wyświetlić historię czatów", + "chat.no-users-in-room": "Brak użytkowników w tym pokoju", + "chat.recent-chats": "Ostatnie czaty", + "chat.contacts": "Kontakty", + "chat.message-history": "Historia wiadomości", + "chat.message-deleted": "Wiadomość usunięta", + "chat.options": "Ustawienia czatu", + "chat.pop-out": "Otwórz czat w nowym oknie", + "chat.minimize": "Minimalizuj", + "chat.maximize": "Maksymalizuj", + "chat.seven_days": "7 dni", + "chat.thirty_days": "30 dni", + "chat.three_months": "3 miesiące", + "chat.delete_message_confirm": "Czy na pewno chcesz usunąć tę wiadomość?", + "chat.retrieving-users": "Pobieram użytkowników...", + "chat.manage-room": "Zarządzaj pokojami czatu", + "chat.add-user-help": "Tu można wyszukiwać użytkowników. Wybrany użytkownik zostanie dodany do czatu. Nowy użytkownik nie zobaczy wiadomości sprzed dołączenia do konwersacji. Tylko właściciele pokoi () mogą usuwać użytkowników z pokoi czatu.", + "chat.confirm-chat-with-dnd-user": "Ten użytkownik ustawił status „nie przeszkadzać”. Czy chcesz z nim rozmawiać mimo to?", + "chat.rename-room": "Zmień nazwę pokoju", + "chat.rename-placeholder": "Tu wpisz nazwę pokoju", + "chat.rename-help": "Ustawiona tu nazwa pokoju będzie widoczna dla wszystkich obecnych w nim użytkowników.", + "chat.leave": "Opuść czat", + "chat.leave-prompt": "Czy na pewno chcesz opuścić ten czat?", + "chat.leave-help": "Opuszczając czat, tracisz dostęp do dalszej rozmowy na czacie. Jeśli w przyszłości zostaniesz znów dodany, nie zobaczysz historii czatu sprzed ponownego dołączenia.", + "chat.in-room": "W tym pokoju", + "chat.kick": "Wyrzuć", + "chat.show-ip": "Pokaż IP", + "chat.owner": "Właściciel pokoju", + "chat.system.user-join": "%1 dołączył(a) do pokoju", + "chat.system.user-leave": "%1 opuścił(a) pokój", + "chat.system.room-rename": "%2 zmienił(a) nazwę pokoju: %1", + "composer.compose": "Napisz", + "composer.show_preview": "Pokaż podgląd", + "composer.hide_preview": "Ukryj podgląd", + "composer.user_said_in": "%1 napisał w %2:", + "composer.user_said": "%1 napisał:", + "composer.discard": "Na pewno chcesz porzucić ten post?", + "composer.submit_and_lock": "Prześlij i zablokuj", + "composer.toggle_dropdown": "Przełącz listę rozwijaną", + "composer.uploading": "Wysyłanie %1", + "composer.formatting.bold": "Pogrubienie", + "composer.formatting.italic": "Kursywa", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Przekreślenie", + "composer.formatting.code": "Kod", + "composer.formatting.link": "Odnośnik", + "composer.formatting.picture": "Link do obrazka", + "composer.upload-picture": "Wyślij obraz", + "composer.upload-file": "Wyślij plik", + "composer.zen_mode": "Tryb Zen", + "composer.select_category": "Wybierz kategorię", + "composer.textarea.placeholder": "Wprowadź tutaj zawartość swojego posta, możesz przeciągnąć i upuścić obrazki", + "composer.schedule-for": "Zaplanuj temat na", + "composer.schedule-date": "Data", + "composer.schedule-time": "Czas", + "composer.cancel-scheduling": "Anuluj planowanie", + "composer.set-schedule-date": "Ustaw datę", + "bootbox.ok": "OK", + "bootbox.cancel": "Anuluj", + "bootbox.confirm": "Potwierdź", + "bootbox.submit": "Zatwierdź", + "bootbox.send": "Wyślij", + "cover.dragging_title": "Pozycjonowanie tła", + "cover.dragging_message": "Przeciągnij zdjęcie tła do wybranej pozycji i kliknij „Zapisz”", + "cover.saved": "Tło zapisane", + "thumbs.modal.title": "Zarządzaj miniaturkami tematów", + "thumbs.modal.no-thumbs": "Żadne miniaturki nie zostały znalezione", + "thumbs.modal.resize-note": "Note: To forum jest skonfigurowane tak aby zmienić rozmiar miniaturek tematów do maksymalnej szerokości %1px", + "thumbs.modal.add": "Dodaj miniaturkę", + "thumbs.modal.remove": "Usuń miniaturkę", + "thumbs.modal.confirm-remove": "Czy jesteś pewny że chcesz usunąć tą miniaturkę?" +} \ No newline at end of file diff --git a/public/language/pl/notifications.json b/public/language/pl/notifications.json new file mode 100644 index 0000000000..c146c517d5 --- /dev/null +++ b/public/language/pl/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Powiadomienia", + "no_notifs": "Nie masz nowych powiadomień", + "see_all": "Wszystkie powiadomienia", + "mark_all_read": "Zaznacz wszystkie jako przeczytane", + "back_to_home": "Wróć do %1", + "outgoing_link": "Odnośnik wychodzący", + "outgoing_link_message": "Opuszczasz %1", + "continue_to": "Przejdź do %1", + "return_to": "Wróć do %1", + "new_notification": "Masz nowe powiadomienie", + "you_have_unread_notifications": "Masz nieprzeczytane powiadomienia.", + "all": "Wszystko", + "topics": "Tematy", + "replies": "Odpowiedzi", + "chat": "Czaty", + "group-chat": "Rozmowy grupowe", + "follows": "Obserwuje", + "upvote": "Głosy na tak", + "new-flags": "Nowe flagi", + "my-flags": "Flagi przypisane mnie", + "bans": "Bany", + "new_message_from": "Nowa wiadomość od %1", + "upvoted_your_post_in": "%1 zagłosował na Twój post w %2", + "upvoted_your_post_in_dual": "%1 oraz %2 zagłosowali na Twój post w %3.", + "upvoted_your_post_in_multiple": "%1 oraz %2 innych zagłosowali na Twój post w %3.", + "moved_your_post": "%1 przeniósł Twój post do %2", + "moved_your_topic": "%1 przeniósł %2", + "user_flagged_post_in": "%1 oflagował post w %2", + "user_flagged_post_in_dual": "%1 oraz %2 oflagowali post w %3", + "user_flagged_post_in_multiple": "%1 oraz %2 innych oflagowali post w %3", + "user_flagged_user": "%1 oflagował profil użytkownika (%2)", + "user_flagged_user_dual": "%1 oraz %2 oflagowali profil użytkownika (%3)", + "user_flagged_user_multiple": "%1 and %2 innych oflagowali profil użytkownika (%3)", + "user_posted_to": "%1 dodał odpowiedź do %2", + "user_posted_to_dual": "%1 oraz %2 dodali odpowiedzi do %3", + "user_posted_to_multiple": "%1 oraz %2 innych dodali odpowiedzi do %3", + "user_posted_topic": "%1 stworzył nowy temat: %2", + "user_edited_post": "%1 edytował post w %2", + "user_started_following_you": "%1 zaczął Cię obserwować.", + "user_started_following_you_dual": "%1 oraz %2 zaczęli Cię obserwować.", + "user_started_following_you_multiple": "%1 oraz %2 innych obserwują Cię.", + "new_register": "%1 wysłał(-a) żądanie rejestracji.", + "new_register_multiple": "%1 żądania rejestracji oczekują na sprawdzenie.", + "flag_assigned_to_you": "Flaga %1 została przypisana do ciebie", + "post_awaiting_review": "Posty oczkujące na sprawdzenie", + "profile-exported": "%1profil wyeksportowany, kliknij tutaj by pobrać", + "posts-exported": "%1postów wyeksportowane, kliknij tutaj by pobrać", + "uploads-exported": "%1przesłanych plików wyeksportowane, kliknij tutaj by pobrać", + "users-csv-exported": "Plik csv użytkowników wyeksportowany, kliknij aby pobrać", + "post-queue-accepted": "Twój oczekujący post w kolejce został zaakceptowany. Click tutaj aby go zobaczyć.", + "post-queue-rejected": "Twój post oczekujący w kolejce został odrzucony.", + "post-queue-notify": "Post oczekujący w kolejce otrzymał powiadomienie:
\"%1\"", + "email-confirmed": "E-mail potwierdzony", + "email-confirmed-message": "Dziękujemy za potwierdzenie maila. Twoje konto zostało aktywowane.", + "email-confirm-error-message": "Wystąpił problem przy aktywacji - kod jest błędny lub przestarzały", + "email-confirm-sent": "E-mail potwierdzający wysłany.", + "none": "Żadna z opcji", + "notification_only": "Tylko powiadomienie", + "email_only": "Tylko e-mail", + "notification_and_email": "Powiadomienie oraz e-mail", + "notificationType_upvote": "Kiedy ktoś zagłosuje na Twój post", + "notificationType_new-topic": "Kiedy ktoś, kogo obserwujesz, utworzy temat", + "notificationType_new-reply": "Kiedy ktoś doda nową odpowiedź w temacie, który obserwujesz", + "notificationType_post-edit": "Kiedy post jest edytowany w temacie, który obserwujesz", + "notificationType_follow": "Kiedy ktoś zacznie Cię obserwować", + "notificationType_new-chat": "Kiedy otrzymasz wiadomość na czacie", + "notificationType_new-group-chat": "Gdy otrzymasz wiadomość na czacie grupowym", + "notificationType_group-invite": "Kiedy otrzymasz grupowe zaproszenie", + "notificationType_group-leave": "Kiedy użytkownik opuszcza Twoją grupę", + "notificationType_group-request-membership": "Kiedy ktoś prosi o dołączenie do grupy, którą posiadasz", + "notificationType_new-register": "Kiedy ktoś zostaje dodany do kolejki rejestracyjnej", + "notificationType_post-queue": "Kiedy nowy post jest kolejkowany", + "notificationType_new-post-flag": "Kiedy post zostanie oflagowany", + "notificationType_new-user-flag": "Kiedy użytkownik zostanie oflagowany" +} \ No newline at end of file diff --git a/public/language/pl/pages.json b/public/language/pl/pages.json new file mode 100644 index 0000000000..8204caddb5 --- /dev/null +++ b/public/language/pl/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Strona główna", + "unread": "Nieprzeczytane tematy", + "popular-day": "Tematy popularne dzisiaj", + "popular-week": "Tematy popularne w tym tygodniu", + "popular-month": "Tematy popularne w tym miesiącu", + "popular-alltime": "Wszystkie popularne tematy", + "recent": "Ostatnie tematy", + "top-day": "Tematy z najwyższą liczbą głosów dzisiaj", + "top-week": "Tematy z najwyższą liczbą głosów w tym tygodniu", + "top-month": "Tematy z najwyższą liczbą głosów w tym miesiącu", + "top-alltime": "Tematy z najwyższą liczbą głosów", + "moderator-tools": "Narzędzia dla moderatorów", + "flagged-content": "Flagi", + "ip-blacklist": "Czarna lista adresów IP", + "post-queue": "Kolejka postów", + "users/online": "Dostępni użytkownicy", + "users/latest": "Nowi użytkownicy", + "users/sort-posts": "Użytkownicy z największą liczbą postów", + "users/sort-reputation": "Użytkownicy z najwyższą reputacją", + "users/banned": "Zbanowani użytkownicy", + "users/most-flags": "Użytkownicy z najwyższą liczbą flag", + "users/search": "Wyszukiwanie użytkownków", + "notifications": "Powiadomienia", + "tags": "Tagi", + "tag": "Tematy oznaczone pod " %1 "", + "register": "Utwórz konto", + "registration-complete": "Rejestracja przebiegła pomyślnie", + "login": "Zaloguj się na swoje konto", + "reset": "Zresetuj hasło do swojego konta", + "categories": "Kategorie", + "groups": "Grupy", + "group": "Grupa %1", + "chats": "Czaty", + "chat": "Czat z %1", + "flags": "Flagi", + "flag-details": "Szczegóły flagi %1", + "account/edit": "Edytowanie „%1”", + "account/edit/password": "Edytowanie hasła „%1”", + "account/edit/username": "Edytowanie nazwy użytkownika „%1”", + "account/edit/email": "Edytowanie adresu e-mail „%1”", + "account/info": "Informacje o koncie", + "account/following": "Obserwowani przez %1", + "account/followers": "Obserwujący %1", + "account/posts": "Posty napisane przez %1", + "account/latest-posts": "Najnowszy post utworzony przez %1", + "account/topics": "Tematy utworzone przez %1", + "account/groups": "Grupy %1", + "account/watched_categories": "Kategorie obserwowane przez %1", + "account/bookmarks": "Posty w zakładkach %1", + "account/settings": "Ustawienia użytkownika", + "account/watched": "Tematy obserwowane przez %1", + "account/ignored": "Tematy zignorowane przez %1", + "account/upvoted": "Posty, na które zagłosował %1", + "account/downvoted": "Posty, przeciw którym zagłosował %1", + "account/best": "Najlepsze posty napisane przez %1", + "account/controversial": "Kontrowersyjne posty napisane przez %1", + "account/blocks": "Użytkownicy zablokowani przez %1", + "account/uploads": "Pliki przesłane przez %1", + "account/sessions": "Sesje logowania", + "confirm": "E-mail potwierdzony", + "maintenance.text": "Obecnie trwają prace konserwacyjne nad %1. Wróć później.", + "maintenance.messageIntro": "Dodatkowo administrator zostawił wiadomość:", + "throttled.text": "%1 jest niedostępny z powodu przeciążenia. Wróć później." +} \ No newline at end of file diff --git a/public/language/pl/post-queue.json b/public/language/pl/post-queue.json new file mode 100644 index 0000000000..6059c8b63e --- /dev/null +++ b/public/language/pl/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Kolejka postów", + "description": "Nie ma żadnych postów w kolejce.
Aby włączyć tą funkcję, idź do: Ustawienia → Post → Kolejka postów i włącz kolejkę postów.", + "user": "Użytkownik", + "category": "Kategoria", + "title": "Tytuł", + "content": "Zawartość", + "posted": "Napisano", + "reply-to": "Odpowiedz do \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Przyjmij", + "reject": "Odrzuć", + "remove": "Usuń", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/pl/recent.json b/public/language/pl/recent.json new file mode 100644 index 0000000000..02b291f6db --- /dev/null +++ b/public/language/pl/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Ostatnie", + "day": "Dzień", + "week": "Tydzień", + "month": "Miesiąc", + "year": "Rok", + "alltime": "Od początku", + "no_recent_topics": "Brak ostatnich tematów.", + "no_popular_topics": "Brak popularnych tematów.", + "there-is-a-new-topic": "Masz nowy temat.", + "there-is-a-new-topic-and-a-new-post": "Masz nowy temat i nowy post.", + "there-is-a-new-topic-and-new-posts": "Masz nowy temat i %1 nowych postów.", + "there-are-new-topics": "Masz %1 nowych tematów.", + "there-are-new-topics-and-a-new-post": "Masz %1 nowych tematów i nowy post.", + "there-are-new-topics-and-new-posts": "Masz %1 nowych tematów i %2 nowych postów.", + "there-is-a-new-post": "Masz nowy post.", + "there-are-new-posts": "Masz %1 nowych postów.", + "click-here-to-reload": "Kliknij tutaj, aby przeładować." +} \ No newline at end of file diff --git a/public/language/pl/register.json b/public/language/pl/register.json new file mode 100644 index 0000000000..a4e4bcb46c --- /dev/null +++ b/public/language/pl/register.json @@ -0,0 +1,32 @@ +{ + "register": "Rejestracja", + "cancel_registration": "Anuluj rejestrację", + "help.email": "Domyślnie Twój adres e-mail będzie ukryty.", + "help.username_restrictions": "Unikalna nazwa użytkownika licząca od %1 do %2 znaków. Inni użytkownicy mogą Cię zawołać, pisząc @nazwa użytkownika.", + "help.minimum_password_length": "Hasło musi mieć co najmniej %1 znaków.", + "email_address": "Adres e-mail", + "email_address_placeholder": "Wpisz swój adres e-mail", + "username": "Nazwa użytkownika", + "username_placeholder": "Wpisz nazwę użytkownika", + "password": "Hasło", + "password_placeholder": "Wpisz hasło", + "confirm_password": "Potwierdź hasło", + "confirm_password_placeholder": "Potwierdź hasło", + "register_now_button": "Zarejestruj się", + "alternative_registration": "Alternatywna rejestracja", + "terms_of_use": "Warunki korzystania z serwisu", + "agree_to_terms_of_use": "Zgadzam się na powyższe warunki", + "terms_of_use_error": "Musisz zaakceptować warunki korzystania z serwisu", + "registration-added-to-queue": "Twoja rejestracja została dodana do kolejki oczekujących na akceptację. Otrzymasz e-mail, kiedy zostanie zatwierdzona przez administratora.", + "registration-queue-average-time": "Nasz średni czas zatwierdzania członkostwa wynosi %1 godzin i %2 minut.", + "registration-queue-auto-approve-time": "Twoje członkostwo na tym forum zostanie w pełni aktywowane w ciągu maksymalnie %1 godzin.", + "interstitial.intro": "Do zaktualizowania Twojego konta potrzebne są dodatkowe informacje…", + "interstitial.intro-new": "Do utworzenia Twojego konta potrzebne są dodatkowe informacje.", + "interstitial.errors-found": "Proszę sprawdź wprowadzone informację", + "gdpr_agree_data": "Wyrażam zgodę na zbieranie i przetwarzanie moich danych przez tę stronę.", + "gdpr_agree_email": "Wyrażam zgodę na otrzymywanie e-maili z podsumowaniami i powiadomieniami od tej strony.", + "gdpr_consent_denied": "Musisz wyrazić zgodę na zbieranie/przetwarzanie Twoich danych przez tę stronę oraz na otrzymywanie e-maili.", + "invite.error-admin-only": "Bezpośrednia rejestracja użytkownika została wyłączona. Aby uzyskać więcej informacji, skontaktuj się z administratorem.", + "invite.error-invite-only": "Bezpośrednia rejestracja użytkownika została wyłączona. Aby uzyskać dostęp do tego forum, musisz otrzymać zaproszenie od istniejącego użytkownika.", + "invite.error-invalid-data": "Otrzymane dane rejestracyjne nie odpowiadają naszej bazie danych. Aby uzyskać więcej informacji, skontaktuj się z administratorem" +} \ No newline at end of file diff --git a/public/language/pl/reset_password.json b/public/language/pl/reset_password.json new file mode 100644 index 0000000000..446344767a --- /dev/null +++ b/public/language/pl/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Zresetuj hasło", + "update_password": "Zaktualizuj hasło", + "password_changed.title": "Hasło zmienione", + "password_changed.message": "

Hasło zostało zmienione. Zaloguj się ponownie.", + "wrong_reset_code.title": "Nieprawidłowy kod resetujący", + "wrong_reset_code.message": "Wprowadzony kod resetujący jest nieprawidłowy. Spróbuj ponownie lub uzyskaj nowy kod.", + "new_password": "Nowe hasło", + "repeat_password": "Powtórz hasło", + "changing_password": "Zmienianie hasła", + "enter_email": "Podaj swój adres e-mail, by otrzymać wiadomość z instrukcjami, jak zresetować hasło.", + "enter_email_address": "Wpisz swój adres e-mail", + "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.", + "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", + "password_expired": "Twoje hasło wygasło, wybierz nowe hasło" +} \ No newline at end of file diff --git a/public/language/pl/search.json b/public/language/pl/search.json new file mode 100644 index 0000000000..221e5e23d9 --- /dev/null +++ b/public/language/pl/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 wyników pasujących do „%2” (%3 sekund)", + "no-matches": "Nie znaleziono pasujących wyników", + "advanced-search": "Wyszukiwanie zaawansowane", + "in": "w", + "titles": "Tytuły", + "titles-posts": "Tytuły i posty", + "match-words": "Dopasuj słowa", + "all": "Wszystkie", + "any": "Dowolne", + "posted-by": "Napisane przez", + "in-categories": "W kategoriach", + "search-child-categories": "Przeszukaj podkategorie", + "has-tags": "Ma tagi", + "reply-count": "Liczba odpowiedzi", + "at-least": "Przynajmniej", + "at-most": "Co najwyżej", + "relevance": "Trafność", + "post-time": "Data zamieszczenia", + "votes": "Głosy", + "newer-than": "Nowsze niż", + "older-than": "Starsze niż", + "any-date": "Kiedykolwiek", + "yesterday": "Wczoraj", + "one-week": "Jeden tydzień temu", + "two-weeks": "Dwa tygodnie temu", + "one-month": "Jeden miesiąc temu", + "three-months": "Trzy miesiące temu", + "six-months": "Sześć miesięcy temu", + "one-year": "Jeden rok temu", + "sort-by": "Sortuj po", + "last-reply-time": "Odpowiedziano ostatnio", + "topic-title": "Tytuł tematu", + "topic-votes": "Głosy tematu", + "number-of-replies": "Liczba odpowiedzi", + "number-of-views": "Liczba wyświetleń", + "topic-start-date": "Data utworzenia tematu", + "username": "Nazwa użytkownika", + "category": "Kategoria", + "descending": "W kolejności malejącej", + "ascending": "W kolejności rosnącej", + "save-preferences": "Zapisz ustawienia", + "clear-preferences": "Wyczyść ustawienia", + "search-preferences-saved": "Ustawienia wyszukiwania zapisane", + "search-preferences-cleared": "Ustawienia wyszukiwania wyczyszczone", + "show-results-as": "Pokazuj wyniki jako", + "see-more-results": "Wyświetl więcej wyników (%1)", + "search-in-category": "Szukaj w \"%1\"" +} \ No newline at end of file diff --git a/public/language/pl/success.json b/public/language/pl/success.json new file mode 100644 index 0000000000..dcf69790f4 --- /dev/null +++ b/public/language/pl/success.json @@ -0,0 +1,7 @@ +{ + "success": "Udało się", + "topic-post": "Twój post został wysłany.", + "post-queued": "Twój post oczekuje w kolejce na zatwierdzenie. Otrzymasz powiadomienie jego akceptacji lub odrzucenia.", + "authentication-successful": "Uwierzytelnienie powiodło się.", + "settings-saved": "Ustawienia zostały zapisane." +} \ No newline at end of file diff --git a/public/language/pl/tags.json b/public/language/pl/tags.json new file mode 100644 index 0000000000..df5b2ee6f5 --- /dev/null +++ b/public/language/pl/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nie ma tematów z tym tagiem", + "tags": "Tagi", + "enter_tags_here": "Wpisz tagi tutaj, każdy o długości od %1 do %2 znaków.", + "enter_tags_here_short": "Wpisz tagi...", + "no_tags": "Jeszcze nie ma tagów.", + "select_tags": "Wybierz tagi" +} \ No newline at end of file diff --git a/public/language/pl/top.json b/public/language/pl/top.json new file mode 100644 index 0000000000..6dbf85cad0 --- /dev/null +++ b/public/language/pl/top.json @@ -0,0 +1,4 @@ +{ + "title": "Najlepsze", + "no_top_topics": "Nie ma żadnych tematów w najlepszych" +} \ No newline at end of file diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json new file mode 100644 index 0000000000..6c8cb1d96f --- /dev/null +++ b/public/language/pl/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Temat", + "title": "Tytuł", + "no_topics_found": "Nie znaleziono żadnych tematów.", + "no_posts_found": "Nie znaleziono żadnych postów.", + "post_is_deleted": "Ten post został usunięty!", + "topic_is_deleted": "Ten temat został usunięty!", + "profile": "Profil", + "posted_by": "Napisany przez %1", + "posted_by_guest": "Napisany przez gościa", + "chat": "Czat", + "notify_me": "Powiadamiaj mnie o nowych odpowiedziach w tym temacie", + "quote": "Cytuj", + "reply": "Odpowiedz", + "replies_to_this_post": "%1 odpowiedzi", + "one_reply_to_this_post": "1 odpowiedź", + "last_reply_time": "Ostatnia odpowiedź", + "reply-as-topic": "Odpowiedz, zakładając nowy temat", + "guest-login-reply": "Zaloguj się, aby odpowiedzieć", + "login-to-view": "Zaloguj się by zobaczyć", + "edit": "Edytuj", + "delete": "Usuń", + "delete-event": "Usuń zdarzenie", + "delete-event-confirm": "Czy na pewno chcesz usunąć to zdarzenie?", + "purge": "Wymaż", + "restore": "Przywróć", + "move": "Przenieś", + "change-owner": "Zmień właściciela", + "fork": "Skopiuj", + "link": "Odnośnik", + "share": "Udostępnij", + "tools": "Narzędzia", + "locked": "Zablokowany", + "pinned": "Przypięty", + "pinned-with-expiry": "Przypięte do %1", + "scheduled": "Zaplanowany", + "moved": "Przeniesiony", + "moved-from": "Przeniesiony z %1", + "copy-ip": "Kopiuj IP", + "ban-ip": "Blokuj IP", + "view-history": "Historia edycji", + "locked-by": "Zamknięto przez", + "unlocked-by": "Odblokowano przez", + "pinned-by": "Przypięto przez", + "unpinned-by": "Odpięty przez", + "deleted-by": "Usunięty przez", + "restored-by": "Przywrócony przez", + "moved-from-by": "Przeniesiony z %1 przez", + "queued-by": "Post queued for approval →", + "backlink": "Odwołuje się do", + "forked-by": "Forked by", + "bookmark_instructions": "Kliknij tutaj, by powrócić do ostatniego przeczytanego postu w tym temacie.", + "flag-post": "Zgłoś ten post", + "flag-user": "Zgłoś tego użytkownika", + "already-flagged": "Już zgłoszono", + "view-flag-report": "Zobacz zgłoszenie", + "resolve-flag": "Oznacz flagę jako rozwiązaną", + "merged_message": "Ten temat został połączony z %2", + "deleted_message": "Ten temat został usunięty. Mogą go zobaczyć tylko użytkownicy upoważnieni do zarządzania tematami.", + "following_topic.message": "Będziesz teraz otrzymywać powiadomienia o nowych odpowiedziach w tym temacie.", + "not_following_topic.message": "Zobaczysz ten temat na liście nieprzeczytanych, ale nie będziesz otrzymywać powiadomień o odpowiedziach w tym temacie.", + "ignoring_topic.message": "Nie zobaczysz już tego tematu na liście nieprzeczytanych. Otrzymasz powiadomienie, kiedy ktoś o Tobie wspomni lub zagłosuje na Twój post.", + "login_to_subscribe": "Zarejestruj lub zaloguj się, aby subskrybować ten temat.", + "markAsUnreadForAll.success": "Temat oznaczony jako nieprzeczytany dla wszystkich", + "mark_unread": "Oznacz jako nieprzeczytany", + "mark_unread.success": "Temat oznaczony jako nieprzeczytany", + "watch": "Obserwuj", + "unwatch": "Nie obserwuj", + "watch.title": "Otrzymuj powiadomienia o nowych odpowiedziach w tym temacie", + "unwatch.title": "Przestań obserwować ten temat", + "share_this_post": "Udostępnij", + "watching": "Obserwuj", + "not-watching": "Nie obserwuj", + "ignoring": "Ignoruj", + "watching.description": "Powiadamiaj mnie o nowych odpowiedziach.
Pokazuj temat w nieprzeczytanych.", + "not-watching.description": "Nie powiadamiaj mnie o nowych odpowiedziach.
Pokazuj temat w nieprzeczytanych, jeśli kategoria nie jest ignorowana.", + "ignoring.description": "Nie powiadamiaj mnie o nowych odpowiedziach.
Nie pokazuj tematu w nieprzeczytanych.", + "thread_tools.title": "Narzędzia tematu", + "thread_tools.markAsUnreadForAll": "Oznacz jako nieprzeczytany dla wszystkich", + "thread_tools.pin": "Przypnij temat", + "thread_tools.unpin": "Odepnij temat", + "thread_tools.lock": "Zablokuj temat", + "thread_tools.unlock": "Odblokuj temat", + "thread_tools.move": "Przenieś temat", + "thread_tools.move-posts": "Przenieś posty", + "thread_tools.move_all": "Przenieś wszystko", + "thread_tools.change_owner": "Zmień właściciela", + "thread_tools.select_category": "Wybierz kategorię", + "thread_tools.fork": "Skopiuj temat", + "thread_tools.delete": "Usuń temat", + "thread_tools.delete-posts": "Usuń posty", + "thread_tools.delete_confirm": "Czy na pewno chcesz usunąć ten temat?", + "thread_tools.restore": "Przywróć temat", + "thread_tools.restore_confirm": "Czy na pewno chcesz przywrócić ten temat?", + "thread_tools.purge": "Wymaż temat", + "thread_tools.purge_confirm": "Na pewno chcesz wyczyścić ten temat?", + "thread_tools.merge_topics": "Połącz tematy", + "thread_tools.merge": "Połącz", + "topic_move_success": "Ten temat zostanie wkrótce przeniesiony do \"%1\". Naciśnij tutaj by to cofnąć.", + "topic_move_multiple_success": "Te tematy zostaną wkrótce przeniesione do \"%1\". Naciśnij tutaj by to cofnąć.", + "topic_move_all_success": "Wszystkie tematy zostaną wkrótce przeniesione do \"%1\". Naciśnij tutaj by to cofnąć.", + "topic_move_undone": "Cofnięto przenoszenie tematu", + "topic_move_posts_success": "Posty zostaną wkrótce przeniesione. Naciśnij tutaj by to cofnąć.", + "topic_move_posts_undone": "Cofnięto przenoszenie postów", + "post_delete_confirm": "Czy na pewno chcesz usunąć ten post?", + "post_restore_confirm": "Czy na pewno chcesz przywrócić ten post?", + "post_purge_confirm": "Czy na pewno chcesz wyczyścić ten post?", + "pin-modal-expiry": "Data wygaśnięcia", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Ładowanie kategorii", + "confirm_move": "Przenieś", + "confirm_fork": "Skopiuj", + "bookmark": "Dodaj do zakładek", + "bookmarks": "Zakładki", + "bookmarks.has_no_bookmarks": "Nie masz jeszcze żadnych postów w zakładkach.", + "copy-permalink": "Skopiuj link", + "loading_more_posts": "Załaduj więcej postów", + "move_topic": "Przenieś temat", + "move_topics": "Przenieś tematy", + "move_post": "Przenieś post", + "post_moved": "Post został przeniesiony!", + "fork_topic": "Skopiuj temat", + "enter-new-topic-title": "Wpisz nowy tytuł tematu", + "fork_topic_instruction": "Zaznacz posty, które chcesz skopiować", + "fork_no_pids": "Nie zaznaczono żadnych postów!", + "no-posts-selected": "Nie zaznaczono żadnych postów!", + "x-posts-selected": "Zaznaczono %1 post(-ów)", + "x-posts-will-be-moved-to-y": "%1 post(-ów) zostanie przeniesione do \"%2\"", + "fork_pid_count": "Zaznaczono %1 post(-ów)", + "fork_success": "Temat został skopiowany. Kliknij tutaj, aby do niego przejść.", + "delete_posts_instruction": "Zaznacz posty, które chcesz usunąć/wyczyścić", + "merge_topics_instruction": "Zaznacz tematy, które chcesz połączyć lub je wyszukaj", + "merge-topic-list-title": "Lista tematów do połączenia", + "merge-options": "Opcję łączenia tematów", + "merge-select-main-topic": "Wybierz główny temat", + "merge-new-title-for-topic": "Nowy tytuł tematu", + "topic-id": "Identyfikator tematu", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Kliknij w posty, które chcesz przypisać do innego użytkownika", + "composer.title_placeholder": "Tutaj wpisz tytuł tematu...", + "composer.handle_placeholder": "Tutaj wpisz swoje imię/nazwę", + "composer.discard": "Odrzuć", + "composer.submit": "Utwórz", + "composer.additional-options": "Dodatkowe opcje", + "composer.schedule": "Schedule", + "composer.replying_to": "Odpowiedź na %1", + "composer.new_topic": "Nowy temat", + "composer.editing": "Edytowanie", + "composer.uploading": "wysyłanie...", + "composer.thumb_url_label": "Wklej adres miniaturki tematu", + "composer.thumb_title": "Dodaj miniaturkę do tego tematu", + "composer.thumb_url_placeholder": "http://przykład.pl/thumb.png", + "composer.thumb_file_label": "Lub wyślij plik", + "composer.thumb_remove": "Wyczyść pola", + "composer.drag_and_drop_images": "Przeciągnij i upuść obrazy tutaj", + "more_users_and_guests": "%1 użytkownik(-ów) i %2 gość(-ci) więcej", + "more_users": "%1 użytkownik(-ów) więcej", + "more_guests": "%1 gość(-ci) więcej", + "users_and_others": "%1 i %2 innych", + "sort_by": "Sortuj według", + "oldest_to_newest": "Najpierw najstarsze", + "newest_to_oldest": "Najpierw najnowsze", + "most_votes": "Najwięcej głosów", + "most_posts": "Najwięcej postów", + "most_views": "Najwięcej wyświetleń", + "stale.title": "Stworzyć nowy temat?", + "stale.warning": "Temat, na który chcesz udzielić odpowiedzi, jest dość stary. Czy nie wolisz utworzyć nowego tematu i jedynie odnieść się do tego?", + "stale.create": "Stwórz nowy temat", + "stale.reply_anyway": "Odpowiedz na ten temat", + "link_back": "Re: [%1](%2)", + "diffs.title": "Historia edycji postu", + "diffs.description": "Ten post zawiera %1 zmian. Kliknij w którąś ze zmian poniżej, aby zobaczyć treść postu w momencie jej dokonania.", + "diffs.no-revisions-description": "Ten post zawiera %1 zmian.", + "diffs.current-revision": "wersja obecna", + "diffs.original-revision": "pierwsza zmiana", + "diffs.restore": "Przywróć tę wersję", + "diffs.restore-description": "Nowa wersja zostanie dodana do historii edycji tego postu po przywróceniu.", + "diffs.post-restored": "Post został przywrócony do poprzedniej wersji", + "diffs.delete": "Usuń tę wersję", + "diffs.deleted": "Wersja usunięta", + "timeago_later": "%1 później", + "timeago_earlier": "%1 wcześniej", + "first-post": "Pierwszy post", + "last-post": "Ostatni post", + "go-to-my-next-post": "Idź do następnego posta", + "no-more-next-post": "Nie masz więcej postów w tym temacie", + "post-quick-reply": "Wyślij szybką odpowiedź" +} \ No newline at end of file diff --git a/public/language/pl/unread.json b/public/language/pl/unread.json new file mode 100644 index 0000000000..01888a3b22 --- /dev/null +++ b/public/language/pl/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Nieprzeczytane", + "no_unread_topics": "Nie masz żadnych nieprzeczytanych tematów.", + "load_more": "Więcej", + "mark_as_read": "Oznacz jako przeczytane", + "selected": "Wybrane", + "all": "Wszystkie", + "all_categories": "Wszystkie kategorie", + "topics_marked_as_read.success": "Tematy zostały oznaczone jako przeczytane!", + "all-topics": "Wszystkie tematy", + "new-topics": "Nowe tematy", + "watched-topics": "Obserwowane tematy", + "unreplied-topics": "Tematy bez odpowiedzi", + "multiple-categories-selected": "Kilka zaznaczonych" +} \ No newline at end of file diff --git a/public/language/pl/uploads.json b/public/language/pl/uploads.json new file mode 100644 index 0000000000..fb364a8996 --- /dev/null +++ b/public/language/pl/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Wysyłanie pliku...", + "select-file-to-upload": "Zaznacz plik do wysłania!", + "upload-success": "Plik został wysłany!", + "maximum-file-size": "Maksymalnie %1 kb", + "no-uploads-found": "Nie znaleziono przesłanych plików", + "public-uploads-info": "Przesłane pliki są ogólnodostępne, a zatem widoczne dla wszystkich gości.", + "private-uploads-info": "Przesłane pliki są prywatne, a zatem widoczne tylko dla zalogowanych użytkowników." +} \ No newline at end of file diff --git a/public/language/pl/user.json b/public/language/pl/user.json new file mode 100644 index 0000000000..c0a2e33d0b --- /dev/null +++ b/public/language/pl/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Zbanowany", + "muted": "Wyciszony", + "offline": "Offline", + "deleted": "Usunięty", + "username": "Nazwa użytkownika", + "joindate": "Data rejestracji", + "postcount": "Liczba postów", + "email": "Adres e-mail", + "confirm_email": "Potwierdź adres e-mail", + "account_info": "Informacje o koncie", + "admin_actions_label": "Działania administacyjne", + "ban_account": "Zbanuj konto", + "ban_account_confirm": "Na pewno chcesz zbanować tego użytkownika?", + "unban_account": "Odbanuj konto", + "mute_account": "Wycisz konto", + "unmute_account": "Wyłącz wyciszenie konta", + "delete_account": "Usuń konto", + "delete_account_as_admin": "Usuń Konto", + "delete_content": "Usuń Treści Konta", + "delete_all": "Usuń Konto i Treści", + "delete_account_confirm": "Czy na pewno chcesz zanonimizować swoje posty i usunąć konto?
To działanie jest nieodwracalne i nie będziesz w stanie przywrócić swoich danych

Wpisz hasło w celu potwierdzenia chęci usunięcia konta.", + "delete_this_account_confirm": "Czy na pewno chcesz usunąć konto pozostawiając swoje treści?
To działanie jest nieodwracalne, posty zostaną zanonimizowane i nie będziesz w stanie przywrócić żadnych powiązań z usuniętym kontem

", + "delete_account_content_confirm": "Czy na pewno chcesz usunąć treści tego konta (posty/tematy/pliki)?
To działanie jest nieodwracalne i nie będziesz w stanie przywrócić żadnych danych

", + "delete_all_confirm": "Czy na pewno chcesz usunąć konto i jego treści (posty/tematy/pliki)?
To działanie jest nieodwracalne i nie będziesz w stanie przywrócić żadnych danych

", + "account-deleted": "Konto usunięte", + "account-content-deleted": "Treści konta usunięte", + "fullname": "Pełna nazwa", + "website": "Strona WWW", + "location": "Lokalizacja", + "age": "Wiek", + "joined": "Dołączono", + "lastonline": "Ostatnio online", + "profile": "Profil", + "profile_views": "Wyświetlenia", + "reputation": "Reputacja", + "bookmarks": "Zakładki", + "watched_categories": "Obserwowane kategorie", + "change_all": "Zmień wszystko", + "watched": "Obserwowane", + "ignored": "Zignorowane", + "default-category-watch-state": "Domyślny stan oglądania kategorii", + "followers": "Obserwujący", + "following": "Obserwowani", + "blocks": "Blokady", + "block_toggle": "Przełącz blokadę", + "block_user": "Blokuj użytkownika", + "unblock_user": "Odblokuj użytkownika", + "aboutme": "O mnie", + "signature": "Sygnatura", + "birthday": "Urodziny", + "chat": "Czatuj", + "chat_with": "Kontynuuj czat z %1", + "new_chat_with": "Rozpocznij czat z %1", + "flag-profile": "Zgłoś profil", + "follow": "Obserwuj", + "unfollow": "Przestań obserwować", + "more": "Więcej", + "profile_update_success": "Profil został zaktualizowany!", + "change_picture": "Zmień zdjęcie", + "change_username": "Zmień nazwę użytkownika", + "change_email": "Zmień adres e-mail", + "email_same_as_password": "Wprowadź bieżące hasło, aby kontynuować – ponownie wprowadziłeś nową wiadomość e-mail", + "edit": "Edytuj", + "edit-profile": "Edytuj profil", + "default_picture": "Domyślna ikona", + "uploaded_picture": "Przesłane zdjęcie", + "upload_new_picture": "Prześlij nowe zdjęcie", + "upload_new_picture_from_url": "Prześlij nowe zdjęcie z adresu URL", + "current_password": "Obecne hasło", + "change_password": "Zmień hasło", + "change_password_error": "Błędne hasło!", + "change_password_error_wrong_current": "Twoje aktualne hasło nie jest poprawne!", + "change_password_error_match": "Hasła muszą pasować!", + "change_password_error_privileges": "Nie masz uprawnień do zmiany tego hasła.", + "change_password_success": "Twoje hasło zostało zaktualizowane!", + "confirm_password": "Potwierdź hasło", + "password": "Hasło", + "username_taken_workaround": "Wybrany login jest już zajęty, więc zmieniliśmy go trochę. Proponujemy %1", + "password_same_as_username": "Twoje hasło jest takie samo jak nazwa użytkownika. Wybierz inne hasło.", + "password_same_as_email": "Twoje hasło jest takie samo jak adres e-mail. Wybierz inne hasło.", + "weak_password": "Słabe hasło", + "upload_picture": "Prześlij zdjęcie", + "upload_a_picture": "Prześlij zdjęcie", + "remove_uploaded_picture": "Usuń przesłane zdjęcie", + "upload_cover_picture": "Prześlij zdjęcie tła", + "remove_cover_picture_confirm": "Czy na pewno chcesz usunąć zdjęcie w tle?", + "crop_picture": "Przytnij obrazek", + "upload_cropped_picture": "Przytnij i prześlij", + "avatar-background-colour": "Kolor tła awatara", + "settings": "Ustawienia", + "show_email": "Wyświetlaj mój adres e-mail", + "show_fullname": "Wyświetlaj moją pełną nazwę", + "restrict_chats": "Przyjmuj wiadomości na czacie tylko od osób, które obserwuję", + "digest_label": "Przysyłaj okresowe podsumowanie wiadomości na forum", + "digest_description": "Subskrybuj, aby otrzymywać maile dla tego forum (nowe powiadomienia i tematy) zgodnie z ustalonym harmonogramem", + "digest_off": "Wyłączone", + "digest_daily": "Codziennie", + "digest_weekly": "Co tydzień", + "digest_biweekly": "Co dwa tygodnie", + "digest_monthly": "Co miesiąc", + "has_no_follower": "Ten użytkownik nie ma jeszcze żadnych obserwujących", + "follows_no_one": "Ten użytkownik jeszcze nikogo nie obserwuje", + "has_no_posts": "Ten użytkownik nic jeszcze nie napisał.", + "has_no_best_posts": "Ten użytkownik nie ma jeszcze żadnych postów z dodatnią reputacją.", + "has_no_topics": "Ten użytkownik nie stworzył jeszcze żadnych tematów.", + "has_no_watched_topics": "Ten użytkownik nie obserwuje jeszcze żadnych tematów.", + "has_no_ignored_topics": "Użytkownik nie pominął jeszcze żadnego tematu.", + "has_no_upvoted_posts": "Ten użytkownik jeszcze nie głosował za w żadnym temacie", + "has_no_downvoted_posts": "Ten użytkownik jeszcze nie głosował przeciw w żadnym temacie.", + "has_no_controversial_posts": "Ten użytkownik nie ma jeszcze żadnych postów z ujemną reputacją.", + "has_no_blocks": "Nie zablokowałeś jeszcze żadnych użytkowników", + "email_hidden": "Adres e-mail ukryty", + "hidden": "ukryty", + "paginate_description": "Dziel tematy i posty na strony zamiast używać nieskończonego przewijania", + "topics_per_page": "Tematów na stronę", + "posts_per_page": "Postów na stronę", + "max_items_per_page": "Maksymalnie %1", + "acp_language": "Język Strony Administratora", + "notifications": "Powiadomienia", + "upvote-notif-freq": "Częstotliwość informowania o pozytywnych głosach", + "upvote-notif-freq.all": "Wszystkie głosy", + "upvote-notif-freq.first": "Pierwszy dla postu", + "upvote-notif-freq.everyTen": "Co dziesięć głosów", + "upvote-notif-freq.threshold": "Po 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Po 10, 100, 1000...", + "upvote-notif-freq.disabled": "Wyłączone", + "browsing": "Ustawienia szukania", + "open_links_in_new_tab": "Otwieraj odnośniki wychodzące w nowej karcie", + "enable_topic_searching": "Włącz szukanie w temacie", + "topic_search_help": "Zaznacz, jeśli chcesz, by wyszukiwanie w temacie zastąpiło przeszukiwanie strony poprzez przeglądarkę, a tym samym umożliwiło przeszukiwanie całego tematu, a nie tylko treści aktualnie wyświetlanych na ekranie", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Wyświetl nowy post po zamieszczeniu odpowiedzi", + "follow_topics_you_reply_to": "Obserwuj tematy, w których uczestniczysz", + "follow_topics_you_create": "Obserwuj tematy, które utworzyłeś", + "grouptitle": "Nazwa grupy", + "group-order-help": "Wybierz grupę i użyj strzałek, aby zamówić tytuł", + "no-group-title": "Brak nazwy grupy", + "select-skin": "Wybierz skórkę", + "select-homepage": "Wybierz stronę startową", + "homepage": "Strona startowa", + "homepage_description": "Wybierz preferowaną stronę startową lub „None”, jeśli chcesz używać strony domyślnej. ", + "custom_route": "Niestandardowa strona startowa", + "custom_route_help": "Wprowadź ścieżkę bez poprzedzającego slasha (np: \"recent\" albo \"category/2/general-discussion\")", + "sso.title": "Usługi Pojedynczego Logowania", + "sso.associated": "Powiązane z", + "sso.not-associated": "Kliknij tutaj, aby powiązać z", + "sso.dissociate": "Odwiąż", + "sso.dissociate-confirm-title": "Potwierdź odwiązanie", + "sso.dissociate-confirm": "Czy na pewno odwiązać Twoje konto od %1?", + "info.latest-flags": "Ostatnie flagi", + "info.no-flags": "Brak oflagowanych postów", + "info.ban-history": "Historia ostatnich banów", + "info.no-ban-history": "Ten użytkownik nigdy nie był zbanowany", + "info.banned-until": "Zbanowany do %1", + "info.banned-expiry": "Wygaśnięcie", + "info.banned-permanently": "Zbanowany permanentnie", + "info.banned-reason-label": "Powód", + "info.banned-no-reason": "Nie podano powodu.", + "info.mute-history": "Historia ostatnich wyciszeń", + "info.no-mute-history": "Ten użytkownik nigdy nie był wyciszony", + "info.muted-until": "Wyciszony do %1", + "info.muted-expiry": "Wygaśnięcie", + "info.muted-no-reason": "Nie podano powodu.", + "info.username-history": "Historia nazwy użytkownika", + "info.email-history": "Historia adresu e-mail", + "info.moderation-note": "Notatka moderatora", + "info.moderation-note.success": "Notatka została zapisana", + "info.moderation-note.add": "Dodaj notatkę", + "sessions.description": "Na tej stronie możesz przeglądać wszystkie aktywne sesje na forum i unieważniać je w razie potrzeby. Możesz unieważnić własną sesję poprzez wylogowanie się ze swojego konta.", + "consent.title": "Twoje prawa i zgody", + "consent.lead": "To forum gromadzi i przetwarza twoje dane osobowe.", + "consent.intro": "Wykorzystujemy te informacje wyłącznie w celu dostosowania działania forum do Twoich potrzeb, a także powiązania zamieszczanych przez Ciebie postów z Twoim kontem użytkownika. Na etapie rejestracji poprosiliśmy o podanie nazwy użytkownika i adresu e-mail; możesz również zamieścić dodatkowe informacje, by uzupełnić swój profil użytkownika na tej stronie.

Będziemy przechowywać te informacje tak długo, jak będzie istniało Twoje konto użytkownika. Możesz wycofać zgodę w dowolnym momencie poprzez usunięcie konta. W każdej chwili możesz też poprosić poprzez stronę „Prawa i zgody” o kopię treści zamieszczonych przez Ciebie na tej stronie.

W razie pytań lub wątpliwości zwróć się do administratorów forum.", + "consent.email_intro": "Czasem możemy wysyłać e-maile na podany przez Ciebie adres e-mail, by przekazywać aktualności lub powiadamiać o nowych wydarzeniach, które mogą mieć dla Ciebie znaczenie. Możesz samodzielnie określić częstotliwość przesyłania podsumowań (lub zupełnie je wyłączyć), a także wybrać rodzaje powiadomień, jakie chcesz otrzymywać e-mailem, poprzez stronę ustawień użytkownika.", + "consent.digest_frequency": "Forum przesyła podsumowania e-mailem co %1, chyba że w ustawieniach użytkownika wyraźnie zaznaczono inaczej.", + "consent.digest_off": "Forum nie przesyła podsumowań e-mailem, chyba że w ustawieniach użytkownika wyraźnie zaznaczono inaczej.", + "consent.received": "Wyraziłeś zgodę na gromadzenie i przetwarzanie Twoich danych przez tę stronę. Żadne dodatkowe działania nie są wymagane.", + "consent.not_received": "Nie wyraziłeś zgody na gromadzenie i przetwarzanie danych. Administratorzy tej strony mogą w dowolnym momencie usunąć Twoje konto, by spełnić wymogi Ogólnego Rozporządzenia o Ochronie Danych.", + "consent.give": "Wyrażam zgodę", + "consent.right_of_access": "Masz prawo dostępu", + "consent.right_of_access_description": "Masz prawo na żądanie uzyskać dostęp do wszelkich danych gromadzonych przez tę stronę. Możesz pobrać kopię tych danych poprzez kliknięcie stosownego przycisku poniżej.", + "consent.right_to_rectification": "Masz prawo do sprostowania", + "consent.right_to_rectification_description": "Masz prawo zmienić lub zaktualizować wszelkie nieprawidłowe dane, jakie nam przekazałeś. Możesz zaktualizować swój profil poprzez edycję profilu, a także w dowolnej chwili edytować treść postów. Jeśli jest inaczej, skontaktuj się z administratorami tej strony.", + "consent.right_to_erasure": "Masz prawo do bycia zapomnianym", + "consent.right_to_erasure_description": "Możesz w każdej chwili cofnąć zgodę na gromadzenie lub przetwarzanie danych poprzez usunięcie konta. Twój profil zostanie usunięty, ale zamieszczone przez Ciebie treści pozostaną dostępne. Jeśli chcesz usunąć swoje konto oraz swoje treści, skontaktuj się z administratorami tej strony.", + "consent.right_to_data_portability": "Masz prawo do przenoszenia danych", + "consent.right_to_data_portability_description": "Możesz poprosić nas o eksport wszelkich danych gromadzonych na temat Ciebie i Twojego konta w formie do odczytu elektronicznego. W tym celu kliknij stosowny przycisk poniżej.", + "consent.export_profile": "Eksportuj profil (.json)", + "consent.export-profile-success": "Eksportowanie profilu. Otrzymasz powiadomienie gdy eksport będzie gotowy.", + "consent.export_uploads": "Eksportuj przesłane treści (zip)", + "consent.export-uploads-success": "Eksportowanie przesłanych plików. Otrzymasz powiadomienie gdy będą gotowe.", + "consent.export_posts": "Eksportuj wpisy (csv)", + "consent.export-posts-success": "Eksportowanie postów. Otrzymasz powiadomienie gdy będą gotowe.", + "emailUpdate.intro": "Proszę wprowadź swój adres email poniżej. To forum używa adresu email do notyfikacji a także do odzyskania konta w razie zapomnienia hasła.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "To pole jest wymagane.", + "emailUpdate.change-instructions": "Email z unikalnym linkiem zostanie wysłany na wprowadzony adres email. Otwarcie tego linku potwierdzi, że podany adres email należy do Ciebie. W każdej chwili możesz go zmienić w edycji profilu.", + "emailUpdate.password-challenge": "Proszę wprowadź hasło aby potwierdzić, że to konto należy do Ciebie." +} \ No newline at end of file diff --git a/public/language/pl/users.json b/public/language/pl/users.json new file mode 100644 index 0000000000..8524151a76 --- /dev/null +++ b/public/language/pl/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Nowi użytkownicy", + "top_posters": "Najwięcej postów", + "most_reputation": "Najlepsza reputacja", + "most_flags": "Najwięcej flag", + "search": "Szukaj", + "enter_username": "Wpisz nazwę użytkownika", + "search-user-for-chat": "Wyszukaj użytkownika by rozpocząć rozmowę", + "load_more": "Wczytaj więcej", + "users-found-search-took": "Znaleziono %1 użytkownika(-ów). Szukanie zajęło %2 sek.", + "filter-by": "Filtruj", + "online-only": "Tylko online", + "invite": "Zaproś", + "prompt-email": "Adresy e-mail:", + "groups-to-join": "Przypisane grupy po akceptacji zaproszenia", + "invitation-email-sent": "E-mail z zaproszeniem został wysłany do %1", + "user_list": "Lista użytkowników", + "recent_topics": "Ostatnie tematy", + "popular_topics": "Popularne tematy", + "unread_topics": "Nieprzeczytane tematy", + "categories": "Kategorie", + "tags": "Tagi", + "no-users-found": "Nie znaleziono pasujących użytkowników!" +} \ No newline at end of file diff --git a/public/language/pt-BR/_DO_NOT_EDIT_FILES_HERE.md b/public/language/pt-BR/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/pt-BR/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/pt-BR/admin/admin.json b/public/language/pt-BR/admin/admin.json new file mode 100644 index 0000000000..d5f47bbf50 --- /dev/null +++ b/public/language/pt-BR/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Tem certeza de que deseja recompilar e reiniciar o NodeBB?", + "alert.confirm-restart": "Tem certeza de que você deseja reiniciar o NodeBB?", + + "acp-title": "%1 | Painel de Controle Administrativo do NodeBB", + "settings-header-contents": "Conteúdos", + "changes-saved": "Alterações Salvas", + "changes-saved-message": "Suas alterações na configuração do NodeBB foram salvas.", + "changes-not-saved": "Alterações não foram Salvas", + "changes-not-saved-message": "O NodeBB encontrou um problema ao salvar suas alterações. (%1)" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/advanced/cache.json b/public/language/pt-BR/admin/advanced/cache.json new file mode 100644 index 0000000000..ab005b8a1e --- /dev/null +++ b/public/language/pt-BR/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Cache de Posts", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Cheio", + "post-cache-size": "Tamanho do Cache de Posts", + "items-in-cache": "Itens no Cache" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/advanced/database.json b/public/language/pt-BR/admin/advanced/database.json new file mode 100644 index 0000000000..f96d860b31 --- /dev/null +++ b/public/language/pt-BR/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Tempo de Atividade em Segundos", + "uptime-days": "Tempo de Atividade em Dias", + + "mongo": "Mongo", + "mongo.version": "Versão do MongoDB", + "mongo.storage-engine": "Mecanismo de Armazenamento", + "mongo.collections": "Coleções", + "mongo.objects": "Objetos", + "mongo.avg-object-size": "Tamanho Médio de Objeto", + "mongo.data-size": "Quantidade de Dados", + "mongo.storage-size": "Tamanho do Armazenamento", + "mongo.index-size": "Tamanho do Índice", + "mongo.file-size": "Tamanho do Arquivo", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Memória Virtual", + "mongo.mapped-memory": "Memória Mapeada", + "mongo.bytes-in": "Bytes recebidos", + "mongo.bytes-out": "Bytes enviados", + "mongo.num-requests": "Quantidade de Requisições", + "mongo.raw-info": "Informações Não-Processadas do MongoDB", + "mongo.unauthorized": "O NodeBB não conseguiu consultar o banco de dados MongoDB para gerar estatísticas relevantes. Por favor, certifique-se de que o usuário em uso pelo NodeBB tem a função de "clusterMonitor" para o banco de dados "admin".", + + "redis": "Redis", + "redis.version": "Versão do Redis", + "redis.keys": "Chaves", + "redis.expires": "Expira em", + "redis.avg-ttl": "Tempo médio de TTL", + "redis.connected-clients": "Clientes Conectados", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Clientes Bloqueados", + "redis.used-memory": "Memória Utilizada", + "redis.memory-frag-ratio": "Proporção da Fragmentação da Memória", + "redis.total-connections-recieved": "Total de Conexões Recebidas", + "redis.total-commands-processed": "Total de Comandos Processados", + "redis.iops": "Operações Instantâneas Por Segundo", + "redis.iinput": "Entradas Instantâneas Por Segundo", + "redis.ioutput": "Saídas Instantâneas Por Segundo", + "redis.total-input": "Total Recebido", + "redis.total-output": "Total Enviado", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Informações Não-Processadas do Redis", + + "postgres": "Postgres", + "postgres.version": "Versão do PostgreSQL", + "postgres.raw-info": "Informações Não-Processadas do Postgres" +} diff --git a/public/language/pt-BR/admin/advanced/errors.json b/public/language/pt-BR/admin/advanced/errors.json new file mode 100644 index 0000000000..57d4ce0617 --- /dev/null +++ b/public/language/pt-BR/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figura %1", + "error-events-per-day": "%1 eventos por dia", + "error.404": "404 Não Encontrada", + "error.503": "503 Serviço Indisponível", + "manage-error-log": "Administrar Log de Erros", + "export-error-log": "Exportar Log de Erros (CSV)", + "clear-error-log": "Limpar Log de Erros", + "route": "Rota", + "count": "Contagem", + "no-routes-not-found": "Ihuul! Sem erros 404!", + "clear404-confirm": "Você tem certeza de que deseja limpar todos os logs de erro 404?", + "clear404-success": "Erros de \"404 Não Encontrada\" apagados" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/advanced/events.json b/public/language/pt-BR/admin/advanced/events.json new file mode 100644 index 0000000000..6a6aa9b69d --- /dev/null +++ b/public/language/pt-BR/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Eventos", + "no-events": "Não há eventos", + "control-panel": "Painel de Controle de Eventos", + "delete-events": "Excluir Eventos", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filtros", + "filters-apply": "Aplicar Filtros", + "filter-type": "Tipo de Evento", + "filter-start": "Data de Início", + "filter-end": "Data de Fim", + "filter-perPage": "Por Página" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/advanced/logs.json b/public/language/pt-BR/admin/advanced/logs.json new file mode 100644 index 0000000000..40136281e8 --- /dev/null +++ b/public/language/pt-BR/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Painel de Controle de Logs", + "reload": "Recarregar Logs", + "clear": "Limpar Logs", + "clear-success": "Logs Apagados!" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/appearance/customise.json b/public/language/pt-BR/admin/appearance/customise.json new file mode 100644 index 0000000000..1818cd7d05 --- /dev/null +++ b/public/language/pt-BR/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "CSS/LESS personalizado", + "custom-css.description": "Insira suas próprias declarações CSS/LESS aqui, elas serão aplicadas após todos os outros estilos.", + "custom-css.enable": "Habilitar CSS/LESS personalizado", + + "custom-js": "Javascript personalizado.", + "custom-js.description": "Insira seu javascript aqui. Ele será executado após a pagina ter sido completamente carregada.", + "custom-js.enable": "Habilitar javascript personalizado.", + + "custom-header": "Cabeçalho Personalizado", + "custom-header.description": "Adicione aqui HTML personalizado (ex. Meta Tags, etc.), os quais serão adicionados à seção de <cabeçalho> do código do teu fórum. Tags de script são permitidas, mas são desencorajadas, já que a tab Javascript Personalizado está disponível.", + "custom-header.enable": "Habilitar Cabeçalho Personalizado", + + "custom-css.livereload": "Ativar Recarregamento Automático", + "custom-css.livereload.description": "Ative esta opção para forçar todas as sessões em todos os dispositivos que estão conectados a sua conta a serem atualizados sempre que você clicar em salvar" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/appearance/skins.json b/public/language/pt-BR/admin/appearance/skins.json new file mode 100644 index 0000000000..7b1b6fa8f3 --- /dev/null +++ b/public/language/pt-BR/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Carregando Skins...", + "homepage": "Página Inicial", + "select-skin": "Escolher Skin", + "current-skin": "Skin Atual", + "skin-updated": "Skin Atualizada", + "applied-success": "A skin %1 foi aplicada com sucesso", + "revert-success": "A skin foi restaurada para as cores iniciais" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/appearance/themes.json b/public/language/pt-BR/admin/appearance/themes.json new file mode 100644 index 0000000000..3f2d6d67c1 --- /dev/null +++ b/public/language/pt-BR/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Procurando por temas instalados...", + "homepage": "Página Inicial", + "select-theme": "Escolher Tema", + "current-theme": "Tema Atual", + "no-themes": "Nenhum tema instalado encontrado", + "revert-confirm": "Tem certeza de que você deseja restaurar o tema padrão do NodeBB?", + "theme-changed": "Tema Alterado", + "revert-success": "Você reverteu com sucesso o seu NodeBB para seu tema padrão.", + "restart-to-activate": "Por favor, recompile e reinicie seu NodeBB para ativar este tema." +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/dashboard.json b/public/language/pt-BR/admin/dashboard.json new file mode 100644 index 0000000000..e59eb05ad1 --- /dev/null +++ b/public/language/pt-BR/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Tráfego do Forum", + "page-views": "Visualizações de Página", + "unique-visitors": "Visitantes Únicos", + "logins": "Logins", + "new-users": "Novos Usuários", + "posts": "Posts", + "topics": "Tópicos", + "page-views-seven": "Últimos 7 Dias", + "page-views-thirty": "Últimos 30 Dias", + "page-views-last-day": "Últimas 24 horas", + "page-views-custom": "Intervalo de Data Personalizado", + "page-views-custom-start": "Ínicio do Intervalo", + "page-views-custom-end": "Fim do Intervalo", + "page-views-custom-help": "Entre com um intervalo de data de visualizações de página que gostaria de ver. Se nenhum selecionador de data estiver disponível, o formato aceito é AAAA-MM-DD", + "page-views-custom-error": "Por favor, entre com um intervalo de data válido no formato AAAA-MM-DD", + + "stats.yesterday": "Ontem", + "stats.today": "Hoje", + "stats.last-week": "Semana Passada", + "stats.this-week": "Esta Semana", + "stats.last-month": "Mês Passado", + "stats.this-month": "Este Mês", + "stats.all": "Todos os Tempos", + + "updates": "Atualizações", + "running-version": "Você está usando o NodeBB v%1.", + "keep-updated": "Sempre se certifique de que o seu NodeBB está atualizado com os últimos patches de segurança e de correções de bugs.", + "up-to-date": "

Você está atualizado

", + "upgrade-available": "

Uma nova versão (v%1) foi lançada. Considere atualizar o seu NodeBB.

", + "prerelease-upgrade-available": "

Esta é uma versão de pré-lançamento desatualizada do NodeBB. Uma nova versão (v%1) foi lançada. Considere atualizar o seu NodeBB.

", + "prerelease-warning": "

Esta é uma versão de pré-lançamento do NodeBB. Bugs inesperados podem ocorrer.

", + "fallback-emailer-not-found": "Emailer substituto não encontrado!", + "running-in-development": "O fórum está sendo executado em modo de desenvolvedor. O fórum pode estar sujeito a potenciais vulnerabilidades; por favor, entre em contato com o seu administrador de sistemas.", + "latest-lookup-failed": "

Falha ao procurar a versão mais recente disponível do NodeBB

", + + "notices": "Avisos", + "restart-not-required": "Reiniciar não é necessário", + "restart-required": "É necessário reiniciar", + "search-plugin-installed": "Plugin de Pesquisa instalado", + "search-plugin-not-installed": "Plugin de Pesquisa não instalado", + "search-plugin-tooltip": "Instale um plugin de pesquisa na página de plugins para que a funcionalidade de pesquisa seja ativada", + + "control-panel": "Controle do Sistema", + "rebuild-and-restart": "Recompilar & Reiniciar", + "restart": "Reiniciar", + "restart-warning": "Recompilar ou Reiniciar o seu NodeBB desconectará todas as conexões existentes por alguns segundos.", + "restart-disabled": "Recompilar e Reiniciar o seu NodeBB foi desativado, pois parece que você não está fazendo-o por meios apropriados.", + "maintenance-mode": "Modo de Manutenção", + "maintenance-mode-title": "Clique aqui para ativar o modo de manutenção do NodeBB", + "realtime-chart-updates": "Atualização de Gráfico em Tempo Real", + + "active-users": "Usuários Ativos", + "active-users.users": "Usuários", + "active-users.guests": "Visitantes", + "active-users.total": "Total", + "active-users.connections": "Conexões", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registrado", + + "user-presence": "Presença de Usuário", + "on-categories": "Na lista de categorias", + "reading-posts": "Lendo posts", + "browsing-topics": "Explorando tópicos", + "recent": "Recente", + "unread": "Não-lidos", + + "high-presence-topics": "Tópicos de Alta Participação", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Páginas Visualizadas", + "graphs.page-views-registered": "Páginas Visualizadas por Registrados", + "graphs.page-views-guest": "Páginas Visualizadas por Visitantes", + "graphs.page-views-bot": "Páginas Visualizadas por Bot", + "graphs.unique-visitors": "Visitantes Únicos", + "graphs.registered-users": "Usuários Registrados", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Última vez reiniciado por", + "no-users-browsing": "Nenhum usuário navegando", + + "back-to-dashboard": "De volta ao Painel", + "details.no-users": "Nenhum usuário ingressou dentro do período de tempo selecionado", + "details.no-topics": "Nenhum tópico foi postado dentro do período de tempo selecionado", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "Nenhum login foi registrado dentro do período de tempo selecionado", + "details.logins-static": "O NodeBB só salva os dados da sessão por %1 dias, então esta tabela abaixo mostrará apenas as sessões ativas mais recentemente", + "details.logins-login-time": "Hora de Login" +} diff --git a/public/language/pt-BR/admin/development/info.json b/public/language/pt-BR/admin/development/info.json new file mode 100644 index 0000000000..e1a5b6b618 --- /dev/null +++ b/public/language/pt-BR/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Você está em %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes respondidos dentro de %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "carga do sistema", + "cpu-usage": "uso da cpu", + "uptime": "tempo de atividade", + + "registered": "Registrado", + "sockets": "Sockets", + "guests": "Visitantes", + + "info": "Informação" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/development/logger.json b/public/language/pt-BR/admin/development/logger.json new file mode 100644 index 0000000000..03ee8e289a --- /dev/null +++ b/public/language/pt-BR/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Configurações de Logs", + "description": "Ao habilitar as caixas de checagem, você irá receber os logs no seu terminal. Se você escolher um caminho de arquivo (path), os logs serão salvos em um arquivo ao invés disso. O log de HTTP é útil para coletar estatísticas sobre quem, quando, e o que as pessoas acessam no seu fórum. Além de logar solicitações de HTTP, nós podemos também logar eventos de socket.io Logs de socket.io, em combinação com o monitor redis-cli, podem ser de muito auxílio para se aprender o funcionamento interno do NodeBB.", + "explanation": "Apenas marque/desmarque as configurações de log para ativar ou desativar o log enquanto em tempo real. Reiniciar não é necessário.", + "enable-http": "Ativar o log de HTTP", + "enable-socket": "Ativar o log de eventos do socket.io", + "file-path": "Caminho do arquivo de log", + "file-path-placeholder": "/caminho/para/o/arquivo-de-log.log ::: deixe em branco para que os logs cheguem no seu terminal", + + "control-panel": "Painel de Controle do Logger", + "update-settings": "Atualizar Configurações do Logger" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/extend/plugins.json b/public/language/pt-BR/admin/extend/plugins.json new file mode 100644 index 0000000000..4ed999a60a --- /dev/null +++ b/public/language/pt-BR/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Instalado", + "active": "Ativo", + "inactive": "Inativo", + "out-of-date": "Desatualizado", + "none-found": "Nenhum plugin encontrado.", + "none-active": "Nenhum Plugin Ativo", + "find-plugins": "Encontrar Plugins", + + "plugin-search": "Pesquisar Plugin", + "plugin-search-placeholder": "Pesquisar por plugin...", + "submit-anonymous-usage": "Enviar anonimamente dados de uso deste plugin", + "reorder-plugins": "Re-ordenar Plugins", + "order-active": "Ordenar Plugins Ativos", + "dev-interested": "Interessado em desenvolver plugins para o NodeBB?", + "docs-info": "A documentação completa sobre a criação de plugins pode ser encontrada noPortal de Documentação do NodeBB.", + + "order.description": "Certos plugins funcionam melhor quando eles são inicializados antes ou após outros plugins.", + "order.explanation": "Os plugins são carregados na ordem especificada aqui, de cima para baixo", + + "plugin-item.themes": "Temas", + "plugin-item.deactivate": "Desativar", + "plugin-item.activate": "Ativar", + "plugin-item.install": "Instalar", + "plugin-item.uninstall": "Desinstalar", + "plugin-item.settings": "Configurações", + "plugin-item.installed": "Instalado", + "plugin-item.latest": "Mais Recente", + "plugin-item.upgrade": "Atualizar", + "plugin-item.more-info": "Para mais informação:", + "plugin-item.unknown": "Desconhecido", + "plugin-item.unknown-explanation": "O estado deste plugin não pôde ser determinado, possivelmente devido a um erro de configuração.", + "plugin-item.compatible": "Este plugin funciona no NodeBB %1", + "plugin-item.not-compatible": "Este plugin não apresenta compatibilidade, tenha certeza que ele funcione antes de instalar em seu ambiente de produção", + + "alert.enabled": "Plugin Ativado", + "alert.disabled": "Plugin Desativado", + "alert.upgraded": "Plugin Atualizado", + "alert.installed": "Plugin Instalado", + "alert.uninstalled": "Plugin Desinstalado", + "alert.activate-success": "Por favor, reconstrua e reinicie o seu NodeBB para ativar totalmente este plugin", + "alert.deactivate-success": "Plugin desativado com sucesso", + "alert.upgrade-success": "Por favor, recompile e reinicie seu NodeBB para atualizar totalmente este plugin.", + "alert.install-success": "Plugin instalado com sucesso, por favor ative o plugin.", + "alert.uninstall-success": "O plugin foi desativado e desinstalado com sucesso.", + "alert.suggest-error": "

O NodeBB não pôde encontrar o gerenciador de pacotes, proceder com a instalação da última versão?

O servidor retornou (%1): %2
", + "alert.package-manager-unreachable": "

O NodeBB não pôde encontrar o gerenciador de pacotes, não é recomendado realizar uma atualização agora.

", + "alert.incompatible": "

Na versão atual do seu NodeBB (v%1), só é permitido atualizar até a versão v%2 deste plugin. Por favor, atualize o seu NodeBB se você quiser instalar uma versão mais recente deste plugin.

", + "alert.possibly-incompatible": "

Nenhuma Informação de Compatibilidade Encontrada

Dada a versão do seu NodeBB, este plugin não especificou uma versão específica para instalação. Portanto, não é garantida uma completa compatibilidade. Podendo, assim, causar problemas na hora de iniciar o seu NodeBB.

Caso isto ocorra, tente isso:

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

Deseja continuar com a instalação da última versão deste plugin?

", + "alert.reorder": "Plugins Reorganizados", + "alert.reorder-success": "Por favor, recompile e reinicie o NodeBB para completar o processo.", + + "license.title": "Informação sobre a Licença do Plugin", + "license.intro": "O plugin %1 está licenciado sob a %2. Por gentileza leia e entenda a licença antes de ativar este plugin.", + "license.cta": "Deseja continuar com a ativação deste plugin?" +} diff --git a/public/language/pt-BR/admin/extend/rewards.json b/public/language/pt-BR/admin/extend/rewards.json new file mode 100644 index 0000000000..08b1de8119 --- /dev/null +++ b/public/language/pt-BR/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Prêmios", + "condition-if-users": "Se do Usuário", + "condition-is": "É:", + "condition-then": "Então:", + "max-claims": "Tanto de vezes que a recompensa é reivindicável", + "zero-infinite": "Use 0 para infinito", + "delete": "Deletar", + "enable": "Ativar", + "disable": "Desativar", + + "alert.delete-success": "Recompensa excluída com sucesso", + "alert.no-inputs-found": "Recompensa ilegal - nenhuma entrada encontrada!", + "alert.save-success": "Recompensas salvas com sucesso" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/extend/widgets.json b/public/language/pt-BR/admin/extend/widgets.json new file mode 100644 index 0000000000..f9930d4a39 --- /dev/null +++ b/public/language/pt-BR/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Widgets Disponíveis", + "explanation": "Escolha um widget do menu de dropdown e então arraste e solte numa área de widget do template à esquerda.", + "none-installed": "Nenhum widget encontrado! Ative o plug-in de widgets básicos no painel de controle de plugins.", + "clone-from": "Copiar widgets de", + "containers.available": "Contêineres Disponíveis", + "containers.explanation": "Arrastar e soltar em cima de qualquer widget ativo", + "containers.none": "Nenhum", + "container.well": "Bem", + "container.jumbotron": "Jumbotron", + "container.panel": "Painel", + "container.panel-header": "Cabeçalho do Painel", + "container.panel-body": "Corpo do Painel", + "container.alert": "Alerta", + + "alert.confirm-delete": "Tem certeza de que deseja excluir este widget?", + "alert.updated": "Widgets Atualizados", + "alert.update-success": "Widgets atualizados com sucesso", + "alert.clone-success": "Widgets copiados com sucesso!", + + "error.select-clone": "Por favor, selecione a página a ser copiada", + + "title": "Título", + "title.placeholder": "Título (mostrado apenas em alguns contêineres)", + "container": "Contêiner", + "container.placeholder": "Arraste e solte um contêiner ou insira HTML aqui.", + "show-to-groups": "Mostrar para grupos", + "hide-from-groups": "Esconder dos grupos", + "hide-on-mobile": "Esconder no móvel" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/admins-mods.json b/public/language/pt-BR/admin/manage/admins-mods.json new file mode 100644 index 0000000000..62150d81e3 --- /dev/null +++ b/public/language/pt-BR/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administradores", + "global-moderators": "Moderadores Globais", + "moderators": "Moderators", + "no-global-moderators": "Moderadores não Globais", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Sem moderadores", + "add-administrator": "Adicionar Administrador", + "add-global-moderator": "Adicionar Moderador Global", + "add-moderator": "Adicionar Moderador" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/categories.json b/public/language/pt-BR/admin/manage/categories.json new file mode 100644 index 0000000000..8d8b38f147 --- /dev/null +++ b/public/language/pt-BR/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Configurações de Categorias", + "privileges": "Privilégios", + + "name": "Nome da Categoria", + "description": "Descrição da Categoria", + "bg-color": "Cor de Fundo", + "text-color": "Cor do Texto", + "bg-image-size": "Tamanho da Imagem de Fundo", + "custom-class": "Classe Personalizada", + "num-recent-replies": "# de Respostas Recentes", + "ext-link": "Link Externo", + "subcategories-per-page": "Subcategorias por página", + "is-section": "Trate esta categoria como uma seção", + "post-queue": "Fila de posts", + "tag-whitelist": "Lista Branca de Tags", + "upload-image": "Enviar Imagem", + "delete-image": "Remover", + "category-image": "Imagem da Categoria", + "parent-category": "Categoria-Mãe", + "optional-parent-category": "(Opcional) Categoria-Mãe", + "top-level": "Nível Superior", + "parent-category-none": "(Nenhum)", + "copy-parent": "Copiar Mãe", + "copy-settings": "Copiar Configurações De", + "optional-clone-settings": "(Opcional) Clonar Configurações de Categoria", + "clone-children": "Copiar as categorias-filho e suas configurações", + "purge": "Purgar Categoria", + + "enable": "Ativar", + "disable": "Desativar", + "edit": "Editar", + "analytics": "Analytics", + "view-category": "Ver categoria", + "set-order": "Definir ordem", + "set-order-help": "Definir a ordem da categoria moverá esta categoria para aquela ordem e atualizará a ordem das outras categorias conforme necessário. A ordem mínima é 1, o que coloca a categoria no topo.", + + "select-category": "Selecionar Categoria", + "set-parent-category": "Definir Categoria-Mãe", + + "privileges.description": "Você pode configurar os privilégios de controle de acesso para partes do site nesta seção. Privilégios podem ser concedidos por usuário ou por grupo. Selecione o que você quer alterar no menu dropdown abaixo.", + "privileges.category-selector": "Configurando privilégios para", + "privileges.warning": "Atenção: as alterações nas configurações de privilégios têm efeito imediato. Não é necessário salvar a categoria após ajustar estas configurações.", + "privileges.section-viewing": "Privilégios de Visualização", + "privileges.section-posting": "Privilégios de Postagem", + "privileges.section-moderation": "Privilégios de Moderação", + "privileges.section-other": "Outros", + "privileges.section-user": "Usuário", + "privileges.search-user": "Adicionar Usuário", + "privileges.no-users": "Sem privilégios para usuários específicos nesta categoria.", + "privileges.section-group": "Grupo", + "privileges.group-private": "Este grupo é privado", + "privileges.inheritance-exception": "Este grupo não herda privilégios do grupo registered-users", + "privileges.banned-user-inheritance": "Usuários banidos herdam privilégios do grupo banned-users", + "privileges.search-group": "Adicionar Grupo", + "privileges.copy-to-children": "Copiar para Filhos", + "privileges.copy-from-category": "Copiar da Categoria", + "privileges.copy-privileges-to-all-categories": "Copie para Todas as Categorias", + "privileges.copy-group-privileges-to-children": "Copie os privilégios deste grupo para as categorias filhas desta categoria.", + "privileges.copy-group-privileges-to-all-categories": "Copie os privilégios deste grupo para todas as categorias.", + "privileges.copy-group-privileges-from": "Copie os privilégos deste grupo de outra categoria.", + "privileges.inherit": "Se o grupo registered-users recebe um privilégio específico, todos os outros grupos recebem um privilégio implícito, mesmo que eles não estejam explicitamente definidos/habilitados. Este privilégio implícito é exibido para você, porque todos os usuários são parte do grupo registered-users e, portanto, privilégios para grupos adicionais não precisam ser explicitamente concedidos.", + "privileges.copy-success": "Provilégios copiados!", + + "analytics.back": "De volta para a lista de Categorias", + "analytics.title": "Analítica da categoria \"%1\"", + "analytics.pageviews-hourly": "Figura 1 – Visualizações de página por hora nesta categoria", + "analytics.pageviews-daily": "Figura 2 – Visualizações de páginas desta categoria por dia", + "analytics.topics-daily": "Figura 3 – Tópicos criados nessa categoria por dia", + "analytics.posts-daily": "Figura 4 – Posts feitos nessa categoria por dia", + + "alert.created": "Criado", + "alert.create-success": "Categoria criada com sucesso!", + "alert.none-active": "Você não possui categorias ativas.", + "alert.create": "Criar uma Categoria", + "alert.confirm-purge": "

Você realmente quer purgar esta categoria \"%1\"?

Aviso! Todos os tópicos e posts desta categoria serão purgados!

Purgar uma categoria removerá todos os tópicos e posts, e deletará a categoria do banco de dados. Se você quiser remover uma categoria temporariamente, ao invés de fazer isso nós recomendados que você \"desabilite\" a categoria.

", + "alert.purge-success": "Categoria purgada!", + "alert.copy-success": "Configurações Copiadas!", + "alert.set-parent-category": "Definir Categoria Mãe", + "alert.updated": "Categorias Atualizadas", + "alert.updated-success": "Categoria com IDs %1 foi atualizada.", + "alert.upload-image": "Enviar imagem de categoria", + "alert.find-user": "Encontrar um Usuário", + "alert.user-search": "Procure por um usuário aqui...", + "alert.find-group": "Encontre um Grupo", + "alert.group-search": "Pesquise por um grupo aqui...", + "alert.not-enough-whitelisted-tags": "As tags na lista de permissões são em menor número do que as tags mínimas, você precisa criar mais tags na lista de permissões!", + "collapse-all": "Esconder todos", + "expand-all": "Expandir todos", + "disable-on-create": "Desativar ao criar", + "no-matches": "Nada encontrado" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/digest.json b/public/language/pt-BR/admin/manage/digest.json new file mode 100644 index 0000000000..2ef72c7c9e --- /dev/null +++ b/public/language/pt-BR/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Uma lista de estatísticas e tempos de entrega de resumo é exibida abaixo.", + "disclaimer": "Informamos que a entrega de e-mail não é garantida, devido à natureza da tecnologia de e-mail. Muitos fatores variáveis ​​determinam se um e-mail enviado ao servidor do destinatário é entregue na caixa de entrada do usuário, incluindo reputação do servidor, endereços IP na lista negra e se DKIM/SPF/DMARC está configurado.", + "disclaimer-continued": "Uma entrega bem-sucedida significa que a mensagem foi enviada com sucesso pelo NodeBB e confirmada pelo servidor do destinatário. Isso não significa que o e-mail foi parar na caixa de entrada. Para obter melhores resultados, recomendamos o uso de um serviço de entrega de e-mail de terceiros, como o SendGrid.", + + "user": "Usuário", + "subscription": "Tipo de Inscrição", + "last-delivery": "Última entrega bem sucedida", + "default": "Padrão do sistema", + "default-help": "Padrão do sistema significa que o usuário não substituiu explicitamente a configuração global do fórum para resumos, a qual atualmente é: \"%1\"", + "resend": "Reenviar Resumo", + "resend-all-confirm": "Tem certeza de que deseja executar manualmente esta execução de resumo?", + "resent-single": "Reenvio manual de resumos concluído", + "resent-day": "Resumo diário reenviado", + "resent-week": "Resumo semanal reenviado", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Resumo mensal reenviado", + "null": "Nunca", + "manual-run": "Execução de resumo manual:", + + "no-delivery-data": "Nenhum dado de entrega encontrado" +} diff --git a/public/language/pt-BR/admin/manage/groups.json b/public/language/pt-BR/admin/manage/groups.json new file mode 100644 index 0000000000..07b24ad3a6 --- /dev/null +++ b/public/language/pt-BR/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Nome do Grupo", + "badge": "Insígnia", + "properties": "Propriedades", + "description": "Descrição do Grupo", + "member-count": "Número de Membros", + "system": "Sistema", + "hidden": "Oculto", + "private": "Privado", + "edit": "Editar", + "delete": "Excluir", + "privileges": "Privilégios", + "download-csv": "CSV", + "search-placeholder": "Procurar", + "create": "Criar Grupo", + "description-placeholder": "Uma breve descrição do seu grupo", + "create-button": "Criar", + + "alerts.create-failure": "Uh-Oh

Houve um problema ao criar o seu grupo. Por favor, tente novamente mais tarde!

", + "alerts.confirm-delete": "Você tem certeza de que deseja deletar este grupo?", + + "edit.name": "Nome", + "edit.description": "Descrição", + "edit.user-title": "Título dos Membros", + "edit.icon": "Ícone do Grupo", + "edit.label-color": "Cor do Rótulo do Grupo", + "edit.text-color": "Cor do Texto do Grupo", + "edit.show-badge": "Mostrar Insígnia", + "edit.private-details": "Se ativado, entrar em grupos requer a aprovação do dono do grupo.", + "edit.private-override": "Aviso: grupos privados estão desabilitados no sistema, o que sobrepõe esta opção.", + "edit.disable-join": "Desativar pedidos de adesão", + "edit.disable-leave": "Impedir que usuários saiam do grupo", + "edit.hidden": "Oculto", + "edit.hidden-details": "Se ligado, o grupo não será encontrado nas listagens de grupos, e os usuários terão de ser convidados manualmente", + "edit.add-user": "Adicionar Usuário ao Grupo", + "edit.add-user-search": "Pesquisar Usuários", + "edit.members": "Lista de Membros", + "control-panel": "Painel de Controle dos Grupos", + "revert": "Reverter", + + "edit.no-users-found": "Nenhum Usuário Encontrado", + "edit.confirm-remove-user": "Tem certeza que deseja excluir este usuário?", + "edit.save-success": "Alterações salvas!" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/privileges.json b/public/language/pt-BR/admin/manage/privileges.json new file mode 100644 index 0000000000..6841ea3f91 --- /dev/null +++ b/public/language/pt-BR/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Privilégios do Grupo", + "user-privileges": "Privilégios do Usuário", + "edit-privileges": "Editar Privilégios", + "select-clear-all": "Selecionar/Limpar Tudo", + "chat": "Conversar", + "upload-images": "Enviar Imagens", + "upload-files": "Enviar Arquivos", + "signature": "Assinatura", + "ban": "Banir", + "mute": "Mute", + "invite": "Convidar", + "search-content": "Pesquisar Conteúdo", + "search-users": "Pesquisar Usuários", + "search-tags": "Pesquisar Tags", + "view-users": "Ver Usuários", + "view-tags": "Ver Tags", + "view-groups": "Ver Grupos", + "allow-local-login": "Login Local", + "allow-group-creation": "Criar Grupo", + "view-users-info": "Ver Informações dos Usuários", + "find-category": "Encontrar Categoria", + "access-category": "Acessar Categoria", + "access-topics": "Acessar Tópicos", + "create-topics": "Criar Tópicos", + "reply-to-topics": "Responder aos Tópicos", + "schedule-topics": "Agendar Tópicos", + "tag-topics": "Definir tag em tópicos", + "edit-posts": "Editar Posts", + "view-edit-history": "Ver Histórico de Edição", + "delete-posts": "Deletar Posts", + "view_deleted": "Ver Posts Deletados", + "upvote-posts": "Positivar Posts", + "downvote-posts": "Negativar Posts", + "delete-topics": "Deletar Tópicos", + "purge": "Purgar", + "moderate": "Moderar", + "admin-dashboard": "Painel", + "admin-categories": "Categorias", + "admin-privileges": "Privilégios", + "admin-users": "Usuários", + "admin-admins-mods": "Admins e Moderadores", + "admin-groups": "Grupos", + "admin-tags": "Tags", + "admin-settings": "Configurações", + + "alert.confirm-moderate": "Tem certeza de que deseja conceder o privilégio de moderação a este grupo de usuários? Este grupo é público e qualquer usuário pode entrar à vontade.", + "alert.confirm-admins-mods": "Tem certeza de que deseja conceder o privilégio de 'Administradores e Mods' a este usuário/grupo? Os usuários com este privilégio podem promover e rebaixar outros usuários a posições privilegiadas, incluindo superadministrador", + "alert.confirm-save": "Por favor, confirme a sua intenção de salvar estes privilégios", + "alert.saved": "Alterações de privilégio salvas e aplicadas", + "alert.confirm-discard": "Você tem certeza que quer descartar suas mudanças nos privilégios?", + "alert.discarded": "Mudanças de privilégio descartadas", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "Esta ação não pode ser desfeita.", + "alert.admin-warning": "Os administradores obtêm implicitamente todos os privilégios", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/registration.json b/public/language/pt-BR/admin/manage/registration.json new file mode 100644 index 0000000000..2f1b13b8bf --- /dev/null +++ b/public/language/pt-BR/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Fila", + "description": "Não existem usuários na fila de registro.
Para habilitar esta função, acesse Configurações → Usuário → Registro de Usuário e defina Tipo de Registro para \"Aprovação do Administrador\".", + + "list.name": "Nome", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Tempo", + "list.username-spam": "Frequência: %1 Aparece: %2 Confiança: %3", + "list.email-spam": "Frequência: %1 Aparece: %2", + "list.ip-spam": "Frequência: %1 Aparece: %2", + + "invitations": "Convites", + "invitations.description": "Abaixo está uma lista completa dos convites enviados. Use CTRL + F para procurar na lista por um e-mail ou um nome de usuário.

O nome do usuário será exibido à direita dos e-mails para usuários que aceitaram seus convites.", + "invitations.inviter-username": "Nome de Usuário do Convidante", + "invitations.invitee-email": "E-mail do Convidado", + "invitations.invitee-username": "Nome do Usuário do Convidado (se registrado)", + + "invitations.confirm-delete": "Tem certeza que deseja excluir este convite?" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/tags.json b/public/language/pt-BR/admin/manage/tags.json new file mode 100644 index 0000000000..4b984d6bde --- /dev/null +++ b/public/language/pt-BR/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "O seu fórum ainda não tem tópicos com tags.", + "bg-color": "Cor de Fundo", + "text-color": "Cor do Text", + "description": "Selecione as tags clicando ou arrastando, use CTRL para selecionar várias tags.", + "create": "Criar Tag", + "modify": "Modificar Tags", + "rename": "Renomear Tags", + "delete": "Excluir Tags Selecionadas", + "search": "Pesquisar por tags...", + "settings": "Configurações de Tags", + "name": "Nome da Tag", + + "alerts.editing": "Editando tag(s)", + "alerts.confirm-delete": "Você deseja excluir as tags selecionadas?", + "alerts.update-success": "Tag Atualizada!", + "reset-colors": "Redefinir cores" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/uploads.json b/public/language/pt-BR/admin/manage/uploads.json new file mode 100644 index 0000000000..32fc6a1860 --- /dev/null +++ b/public/language/pt-BR/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Enviar Arquivo", + "filename": "Nome do Arquivo", + "usage": "Uso do Post", + "orphaned": "Orphaned", + "size/filecount": "Tamanho / Quantidade de arquivos", + "confirm-delete": "Você tem certeza de que deseja deletar este arquivo?", + "filecount": "%1 arquivos", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/users.json b/public/language/pt-BR/admin/manage/users.json new file mode 100644 index 0000000000..e46b6c1e41 --- /dev/null +++ b/public/language/pt-BR/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Usuários", + "edit": "Actions", + "make-admin": "Tornar Administrador", + "remove-admin": "Excluir Administrador", + "validate-email": "Validar E-mail", + "send-validation-email": "Enviar E-mail de Validação", + "password-reset-email": "Enviar E-mail de Redefinição de Senha", + "force-password-reset": "Forçar a Redifinição de Senha & Desconectar Usuário", + "ban": "Banir Usuário(s)", + "temp-ban": "Banir Usuário(s) Temporariamente", + "unban": "Desbanir Usuário(s)", + "reset-lockout": "Excluir Bloqueio", + "reset-flags": "Resetar Sinalizações", + "delete": "Excluir Usuário(s)", + "delete-content": "Excluir Conteúdo do(s) Usuário(s)", + "purge": "Excluir Usuário(s) e Conteúdo", + "download-csv": "Baixar CSV", + "manage-groups": "Gerenciar Grupos", + "add-group": "Adicionar Grupo", + "create": "Create User", + "invite": "Invite by Email", + "new": "Novo Usuário", + "filter-by": "Filtrar por", + "pills.unvalidated": "Não Validado", + "pills.validated": "Validado", + "pills.banned": "Banido", + + "50-per-page": "50 por página", + "100-per-page": "100 por página", + "250-per-page": "250 por página", + "500-per-page": "500 por página", + + "search.uid": "Por ID de usuário", + "search.uid-placeholder": "Digite o ID do usuário para pesquisar", + "search.username": "Por Nome de Usuário", + "search.username-placeholder": "Entre com um nome de usuário para pesquisar", + "search.email": "Por E-mail", + "search.email-placeholder": "Digite um e-mail para pesquisar", + "search.ip": "Por Endereço IP", + "search.ip-placeholder": "Digite um endereço IP para pesquisar", + "search.not-found": "Usuário não encontrado!", + + "inactive.3-months": "3 meses", + "inactive.6-months": "6 meses", + "inactive.12-months": "12 meses", + + "users.uid": "uid", + "users.username": "nome de usuário", + "users.email": "e-mail", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "quantidade de posts", + "users.reputation": "reputação", + "users.flags": "sinalizações", + "users.joined": "juntou-se em", + "users.last-online": "última vez online", + "users.banned": "banido", + + "create.username": "Nome de Usuário", + "create.email": "E-mail", + "create.email-placeholder": "E-mail deste usuário", + "create.password": "Senha", + "create.password-confirm": "Confirme a Senha", + + "temp-ban.length": "Length", + "temp-ban.reason": "Motivo (Opcional)", + "temp-ban.hours": "Horas", + "temp-ban.days": "Dias", + "temp-ban.explanation": "Entre com o período de tempo para o banimento. Note que um tempo de 0 será considerado um banimento permanente.", + + "alerts.confirm-ban": "Você realmente deseja banir este usuário permanentemente?", + "alerts.confirm-ban-multi": "Você realmente quer banir estes usuários permanentemente?", + "alerts.ban-success": "Usuário(s) banido(s)!", + "alerts.button-ban-x": "Banir %1 usuário(s)", + "alerts.unban-success": "Usuário(s) desbanidos!", + "alerts.lockout-reset-success": "Bloqueio(s) redefinidos!", + "alerts.flag-reset-success": "Sinalização(ões) excluída(s)!", + "alerts.no-remove-yourself-admin": "Você não pode remover a si mesmo como Administrador!", + "alerts.make-admin-success": "O usuário agora é administrador.", + "alerts.confirm-remove-admin": "Você tem certeza que deseja remover este administrador?", + "alerts.remove-admin-success": "O usuário deixou de ser administrador.", + "alerts.make-global-mod-success": "O usuário agora é moderador global.", + "alerts.confirm-remove-global-mod": "Você tem certeza que deseja remover este moderador global?", + "alerts.remove-global-mod-success": "O usuário deixou de ser moderador global.", + "alerts.make-moderator-success": "O usuário agora é moderador.", + "alerts.confirm-remove-moderator": "Você tem certeza que deseja remover este moderador?", + "alerts.remove-moderator-success": "O usuário deixou de ser moderador.", + "alerts.confirm-validate-email": "Você deseja validar o e-mail deste(s) usuário(s)?", + "alerts.confirm-force-password-reset": "Tem certeza de que deseja forçar a redefinição de senha e desconectar esses usuário(s)?", + "alerts.validate-email-success": "E-mails validados", + "alerts.validate-force-password-reset-success": "A senha do(s) usuário(s) foi redefinida e as suas sessões existentes foram revogadas.", + "alerts.password-reset-confirm": "Você quer enviar e-mail(s) de redefinição de senha para este(s) usuário(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Aviso!

Você realmente quer deletar usuário(s)?

Esta ação não é reversível! Apenas a conta do usuário será excluída, suas postagens e tópicos permanecerão.

", + "alerts.delete-success": "Usuário(s) Deletados!", + "alerts.confirm-delete-content": "Aviso!

Você realmente deseja excluir o conteúdo destes usuários?

Esta ação não é reversível! As contão permanecerão, mas seus posts e tópicos serão excluídos.

", + "alerts.delete-content-success": "Conteúdo do(s) Usuário(s) Excluído!", + "alerts.confirm-purge": "Aviso!

Você realmente quer excluir usuário(s) e seu conteúdo?

Essa ação não é reversível! Todos os dados e conteúdo dos usuários serão apagados!

", + "alerts.create": "Criar Usuário", + "alerts.button-create": "Criar", + "alerts.button-cancel": "Cancelar", + "alerts.error-passwords-different": "As senhas devem combinar!", + "alerts.error-x": "Erro

%1

", + "alerts.create-success": "Usuário criado!", + + "alerts.prompt-email": "E-mails:", + "alerts.email-sent-to": "Um e-mail de convite foi enviado para %1", + "alerts.x-users-found": "%1 usuário(s) encontrados, (%2 segundos)", + "export-users-started": "Exportando usuários como csv, isso pode demorar um pouco. Você receberá uma notificação quando isso for concluído.", + "export-users-completed": "Usuários exportados como csv, clique aqui para fazer o download." +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/menu.json b/public/language/pt-BR/admin/menu.json new file mode 100644 index 0000000000..e146ab8d61 --- /dev/null +++ b/public/language/pt-BR/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Painéis", + "dashboard/overview": "Visão Geral", + "dashboard/logins": "Logins", + "dashboard/users": "Usuários", + "dashboard/topics": "Tópicos", + "dashboard/searches": "Searches", + "section-general": "Geral", + + "section-manage": "Administrar", + "manage/categories": "Categorias", + "manage/privileges": "Privilégios", + "manage/tags": "Tags", + "manage/users": "Usuários", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Fila de Registro", + "manage/post-queue": "Fila de Posts", + "manage/groups": "Grupos", + "manage/ip-blacklist": "Lista Negra de IPs", + "manage/uploads": "Uploads", + "manage/digest": "Resumos", + + "section-settings": "Configurações", + "settings/general": "Geral", + "settings/homepage": "Página Inicial", + "settings/navigation": "Navegação", + "settings/reputation": "Reputação & Sinalizações", + "settings/email": "E-mail", + "settings/user": "Usuários", + "settings/group": "Grupos", + "settings/guest": "Visitantes", + "settings/uploads": "Uploads", + "settings/languages": "Idiomas", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Paginação", + "settings/tags": "Tags", + "settings/notifications": "Notificações", + "settings/api": "Acesso a API", + "settings/sounds": "Sons", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Motores de Busca", + "settings/sockets": "Sockets", + "settings/advanced": "Avançado", + + "settings.page-title": "Configurações %1", + + "section-appearance": "Aparência", + "appearance/themes": "Temas", + "appearance/skins": "Skins", + "appearance/customise": "Conteúdo Personalizado (HTML/JS/CSS)", + + "section-extend": "Personalizar", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Recompensas", + + "section-social-auth": "Autenticação Social", + + "section-plugins": "Plugins", + "extend/plugins.install": "Instalar Plugins", + + "section-advanced": "Avançado", + "advanced/database": "Banco de Dados", + "advanced/events": "Eventos", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Erros", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Informação", + + "rebuild-and-restart-forum": "Recompilar & Reiniciar Fórum", + "restart-forum": "Reiniciar Fórum", + "logout": "Sair da Conta", + "view-forum": "Ver Fórum", + + "search.placeholder": "Search settings", + "search.no-results": "Sem resultados...", + "search.search-forum": "Pesquisar o fórum por ", + "search.keep-typing": "Digite para ver mais resultados...", + "search.start-typing": "Comece a digitar para ver resultados...", + + "connection-lost": "A conexão com o %1 foi perdida, tentando reconectar...", + + "alerts.version": "Usando NodeBB v%1", + "alerts.upgrade": "Atualizar para v%1" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/advanced.json b/public/language/pt-BR/admin/settings/advanced.json new file mode 100644 index 0000000000..84d1f48c6f --- /dev/null +++ b/public/language/pt-BR/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Modo de Manutenção", + "maintenance-mode.help": "Quando o fórum está em modo de manutenção, todas as solicitações serão redirecionadas para uma página estática. Administradores não sofrem este redirecionamento e podem acessar o site normalmente.", + "maintenance-mode.status": "Código de Status de Modo de Mautenção", + "maintenance-mode.message": "Mensagem de Manutenção", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Cabeçalhos", + "headers.allow-from": "Defina ALLOW-FROM para Colocar o NodeBB em um iFrame", + "headers.csp-frame-ancestors": "Define o cabeçalho de Content-Security-Policy frame-ancestors para Colocar o NodeBB em um iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self' (padrão) ou lista de URIs a permitir.", + "headers.powered-by": "Personalizar o cabeçalho de \"Powered By\" enviado pelo NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "Para impedir o acesso a todos os sites, deixe vazio", + "headers.acao-regex-help": "Insira expressões regulares aqui para corresponder às origens dinâmicas. Para impedir o acesso a todos os sites, deixe vazio", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Habilitar HSTS (recomendado)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Incluir subdomínios no cabeçalho do HSTS", + "hsts.preload": "Permitir pré-carregamento do cabeçalho do HSTS", + "hsts.help": "Se habilitado, um cabeçalho de HSTS será enviado para este site. Você pode selecionar tanto quais subdomínios deseja incluir, como quais serão as flags de pré-carregamento no seu cabeçalho. Se estiver em dúvida, você pode deixar esta opção desmarcada. Mais informações", + "traffic-management": "Administração de Tráfego", + "traffic.help": "NodeBB usa um módulo que automaticamente nega requisições em situações de alto tráfego. Você pode ajustar estas configurações aqui, apesar de que os padrões são um bom ponto de partida.", + "traffic.enable": "Ativar a Administração de Tráfego", + "traffic.event-lag": "Limite do Lag do Loop de Eventos (em milisegundos)", + "traffic.event-lag-help": "Abaixar este valor diminui o tempo de espera para o carregamentos de página, mas irá também mostrar a mensagem de \"carga excessiva\" para mais usuários. (É necessário reiniciar)", + "traffic.lag-check-interval": "Intervalo de Checagem (em milisegundos)", + "traffic.lag-check-interval-help": "Diminuir esse valor faz com que o NodeBB fique mais sensível a picos de carga, mas também pode fazer com que a verificação fique muito sensível. (É necessário reiniciar)", + + "sockets.settings": "Configurações de WebSocket", + "sockets.max-attempts": "Máx. Tentativas de Reconexão", + "sockets.default-placeholder": "Padrão: %1", + "sockets.delay": "Espera de Reconexão", + + "analytics.settings": "Configurações de Analytics", + "analytics.max-cache": "Valor Máx. do Cache de Analytics", + "analytics.max-cache-help": "Em instalações de alto tráfego, o cache pode ser exaurido continuamente se houver mais usuários ativos simultâneos do que o valor Max Cache. (É necessário reiniciar)", + "compression.settings": "Configurções de Compressão", + "compression.enable": "Habilitar Compreesão", + "compression.help": "Esta configuração ativa a compactação gzip. Para um site de alto tráfego em produção, a melhor maneira de implementar a compactação é implementá-la em um nível de proxy reverso. Você pode habilitá-lo aqui para fins de teste." +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/api.json b/public/language/pt-BR/admin/settings/api.json new file mode 100644 index 0000000000..a7427d4aeb --- /dev/null +++ b/public/language/pt-BR/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Configurações", + "lead-text": "Nesta página, você pode configurar o acesso à API de Escrita no NodeBB.", + "intro": "Por padrão, a API de Escrita autentica os usuários com base em seu cookie de \nsessão, mas o NodeBB também oferece suporte à autenticação Bearer por\n meio de tokens gerados por meio desta página.", + "docs": "Clique aqui para acessar a especificação completa da API", + + "require-https": "Exigir uso da API apenas via HTTPS", + "require-https-caveat": "Nota: Algumas instalações que envolvem balanceadores de carga podem fazer proxy de suas solicitações para NodeBB usando HTTP, caso em que esta opção deve permanecer desabilitada.", + + "uid": "ID do Usuário", + "uid-help-text": "Especifique um ID de usuário para associar a este token. Se o ID do usuário for0, ele será considerada uma token master, que pode assumir a identidade de outros usuários com base no parâmetro _uid", + "description": "Descrição", + "no-description": "Nenhuma descrição especificada.", + "token-on-save": "O token será gerado assim que o formulário for salvo" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/chat.json b/public/language/pt-BR/admin/settings/chat.json new file mode 100644 index 0000000000..710c1e29c4 --- /dev/null +++ b/public/language/pt-BR/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Configurações de Chat", + "disable": "Desativar o chat", + "disable-editing": "Desabilitar editar/apagar mensagem ", + "disable-editing-help": "Administradores e moderadores globais não sofrem esta restrição", + "max-length": "Tamanho máximo das mensagens de chat", + "max-room-size": "Número máximo de usuários nas salas de chat", + "delay": "Tempo entre mensagens de chat em milisegundos", + "notification-delay": "Tempo de espera para notificação de mensagens de bate-papo. (0 para não esperar)", + "restrictions.seconds-edit-after": "Número de segundos que uma mensagem de chat permanecerá editável. (0 desligado)", + "restrictions.seconds-delete-after": "Número de segundos que uma mensagem de chat permanecerá deletável. (0 desligado)" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/cookies.json b/public/language/pt-BR/admin/settings/cookies.json new file mode 100644 index 0000000000..a59e7b91f0 --- /dev/null +++ b/public/language/pt-BR/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Anuência para Europa", + "consent.enabled": "Ativado", + "consent.message": "Mensagem de notificação", + "consent.acceptance": "Mensagem de aprovação", + "consent.link-text": "Texto do Link da Política", + "consent.link-url": "Link para Política de Privacidade", + "consent.blank-localised-default": "Deixar em branco para utilizar os padrões de localidade do NodeBB", + "settings": "Configurações", + "cookie-domain": "Domínio da sessão de cookie", + "max-user-sessions": "Máximo de sessões ativas por usuário", + "blank-default": "Deixe em branco para o valor padrão" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/email.json b/public/language/pt-BR/admin/settings/email.json new file mode 100644 index 0000000000..1c56b7edcd --- /dev/null +++ b/public/language/pt-BR/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Configurações de E-mail", + "address": "Endereço de E-mail", + "address-help": "O seguinte endereço de e-mail se refere ao e-mail que o destinatário verá nos campos \"De\" e \"Responder Para\".", + "from": "Por Nome (From)", + "from-help": "O nome que será mostrado em \"De\" no e-mail.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "Trasporte por SMTP", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Você pode escolher entre uma lista de serviços conhecidos ou adicionar um personalizado.", + "smtp-transport.service": "Escolha um serviço", + "smtp-transport.service-custom": "Serviço Personalizado", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "Host SMTP", + "smtp-transport.port": "Porta SMTP", + "smtp-transport.security": "Segurança da conexão", + "smtp-transport.security-encrypted": "Encriptada", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Nenhuma", + "smtp-transport.username": "Nome de usuário", + "smtp-transport.username-help": "Para o serviço do Gmail, entre com o endereço de e-mail completo aqui, principalmente se você estiver usando um domínio administrado pelo Google Apps.", + "smtp-transport.password": "Senha", + "smtp-transport.pool": "Habilitar conexões em pool", + "smtp-transport.pool-help": "O pool de conexões evita que o NodeBB crie uma nova conexão para cada e-mail. Esta opção se aplica apenas se o Transporte SMTP estiver habilitado.", + + "template": "Editar Modelo do E-mail", + "template.select": "Escolher Modelo do E-mail", + "template.revert": "Reverter ao Original", + "testing": "Teste de E-mail", + "testing.select": "Escolher Modelo do E-mail", + "testing.send": "Enviar E-mail de Teste", + "testing.send-help": "O e-mail de teste será enviado para o seu endereço de e-mail.", + "subscriptions": "Resumos por Email", + "subscriptions.disable": "Desabilitar resumos por email", + "subscriptions.hour": "Hora de Envio dos Resumos", + "subscriptions.hour-help": "Por favor, entre um número representando a hora para enviar os resumos agendados via e-mail (por exemplo: 0 para meia-noite, 17 para 5:00pm). Tenha em mente que esta é a hora de acordo com o servidor e pode não combinar exatamente com o relógio do seu sistema.
O horário aproximado do servidor é:
O próximo resumo diário está agendado para ser enviado ", + "notifications.remove-images": "Remover imagens de notificações por e-mail", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/pt-BR/admin/settings/general.json b/public/language/pt-BR/admin/settings/general.json new file mode 100644 index 0000000000..9527613308 --- /dev/null +++ b/public/language/pt-BR/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Configurações do Site", + "title": "Título do Site", + "title.short": "Título Curto", + "title.short-placeholder": "Se nenhum título curto for especificado, o título do site será usado", + "title.url": "Title Link URL", + "title.url-placeholder": "A URL do título do site", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Nome da Sua Comunidade", + "title.show-in-header": "Mostrar o Título do Site no Cabeçalho", + "browser-title": "Título do Navegador", + "browser-title-help": "Se nenhum título de navegador for especificado, o título do site será usado", + "title-layout": "Layout do Título", + "title-layout-help": "Defina como o título do navegador será estruturado, por exemplo: {pageTitle} | {browserTitle}", + "description.placeholder": "Uma descrição curta sobre a sua comunidade", + "description": "Descrição do Site", + "keywords": "Palavras-chave do Site", + "keywords-placeholder": "Palavras-chave descrevendo sua comunidade, separadas por vírgula", + "logo": "Logo do Site", + "logo.image": "Imagem", + "logo.image-placeholder": "Caminho de URL do logotipo para mostrar no cabeçalho do fórum", + "logo.upload": "Enviar", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "A URL do logo do site", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Todo o Texto", + "log.alt-text-placeholder": "Texto alternativo para acessibilidade", + "favicon": "Favicon", + "favicon.upload": "Enviar", + "pwa": "Progressive Web App", + "touch-icon": "Ícone para Touch", + "touch-icon.upload": "Enviar", + "touch-icon.help": "Tamanho e formato recomendados: 512x512, somente formato PNG. Se nenhum ícone para touch for especificado, o NodeBB usará o seu próprio favicon.", + "maskable-icon": "Ícone Mascarável (de Tela Inicial)", + "maskable-icon.help": "Tamanho e formato recomendados: 512x512, somente formato PNG. Se nenhum ícone mascarável for especificado, o NodeBB usará o seu próprio Ícone para Touch.", + "outgoing-links": "Links Externos", + "outgoing-links.warning-page": "Habilitar Página de Aviso de Links Externos", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domínios que não receberão o aviso de link externo quando clicados", + "site-colors": "Metadados de Cores do Site", + "theme-color": "Cor do Thema", + "background-color": "Cor de Fundo", + "background-color-help": "Cor usada para o fundo da tela inicial quando o site é instalado como um PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/pt-BR/admin/settings/group.json b/public/language/pt-BR/admin/settings/group.json new file mode 100644 index 0000000000..e5e37f5c0c --- /dev/null +++ b/public/language/pt-BR/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Geral", + "private-groups": "Grupos Privados", + "private-groups.help": "Se habilitado, a entrada nos grupos exigirá a apovação do dono do grupo (Padrão: ligado)", + "private-groups.warning": "Atenção! Se esta opção estiver desabilitada e você tiver grupos privados, eles automaticamente se tornarão públicos.", + "allow-multiple-badges": "Permitir Vários Emblemas", + "allow-multiple-badges-help": "Esta opção pode ser usada para permitir que os usuários selecionem várias insígnias de grupo, requer suporte ao tema.", + "max-name-length": "Tamanho Máximo do Nome do Grupo", + "max-title-length": "Tamanho Máximo do Título do Grupo", + "cover-image": "Imagem de Capa do Grupo", + "default-cover": "Imagens de Capa Padrão", + "default-cover-help": "Adicione uma lista, separada por vírgulas, de imagens de capa padrão para grupos que não tenham enviado uma imagem de capa" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/guest.json b/public/language/pt-BR/admin/settings/guest.json new file mode 100644 index 0000000000..7e80da5987 --- /dev/null +++ b/public/language/pt-BR/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Configurações", + "handles.enabled": "Permitir que visitantes escolham um nome", + "handles.enabled-help": "Esta opção mostra um novo campo que permite visitantes de escolher um nome para associar a cada post que eles fizerem. Se desabilitado, eles serão simplesmente chamados de \"Visitante\".", + "topic-views.enabled": "Permitir que visitantes aumentem a contagem de visualizações do tópico", + "reply-notifications.enabled": "Permitir que convidados gerem notificações de resposta" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/homepage.json b/public/language/pt-BR/admin/settings/homepage.json new file mode 100644 index 0000000000..d2a1bb0f7e --- /dev/null +++ b/public/language/pt-BR/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Página Inicial", + "description": "Escolha qual página será mostrada quando usuários navegarem para a URL raíz do seu fórum.", + "home-page-route": "Rota da Página Inicial", + "custom-route": "Rota Personalizada", + "allow-user-home-pages": "Permitir Páginas Iniciais do Usuário", + "home-page-title": "Título da página inicial (padrão \"Home\")" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/languages.json b/public/language/pt-BR/admin/settings/languages.json new file mode 100644 index 0000000000..53fa515534 --- /dev/null +++ b/public/language/pt-BR/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Configurações de Idioma", + "description": "O idioma padrão determina as configurações de idioma para todos os usuários que estiverem visitando o seu fórum.
Usuários individuais podem sobrepor o idioma padrão em sua página de configurações de conta.", + "default-language": "Idioma Padrão", + "auto-detect": "Auto Detectar Configurações de Idioma para Convidados" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/navigation.json b/public/language/pt-BR/admin/settings/navigation.json new file mode 100644 index 0000000000..87f2bea84e --- /dev/null +++ b/public/language/pt-BR/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ícone:", + "change-icon": "modificar", + "route": "Rota:", + "tooltip": "Tooltip:", + "text": "Texto:", + "text-class": "Classe do Texto: opcional", + "class": "Classe: opcional", + "id": "ID: opcional", + + "properties": "Propriedades:", + "groups": "Grupos:", + "open-new-window": "Abrir em uma nova janela", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Deletar", + "btn.disable": "Desativar", + "btn.enable": "Ativar", + + "available-menu-items": "Itens Disponíveis no Menu", + "custom-route": "Rota Personalizada", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/pt-BR/admin/settings/notifications.json b/public/language/pt-BR/admin/settings/notifications.json new file mode 100644 index 0000000000..9a78c28825 --- /dev/null +++ b/public/language/pt-BR/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notificações", + "welcome-notification": "Notificação de Boas-vindas", + "welcome-notification-link": "Link da Notificação de Boas-vindas", + "welcome-notification-uid": "Usuário de Notificação de Boas-vindas (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/pagination.json b/public/language/pt-BR/admin/settings/pagination.json new file mode 100644 index 0000000000..65535045f7 --- /dev/null +++ b/public/language/pt-BR/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Configurações de Paginação", + "enable": "Paginar posts e tópicos ao invés de usar rolagem infinita.", + "posts": "Paginação do Post", + "topics": "Paginação de Tópico", + "posts-per-page": "Posts por Página", + "max-posts-per-page": "Máximo de posts por página", + "categories": "Paginação de Categorias", + "topics-per-page": "Tópicos por Página", + "max-topics-per-page": "Máximo de tópicos por página", + "categories-per-page": "Categorias por página" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/post.json b/public/language/pt-BR/admin/settings/post.json new file mode 100644 index 0000000000..8ecd1f8e69 --- /dev/null +++ b/public/language/pt-BR/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Ordenação de Posts", + "sorting.post-default": "Ordenação Padrão de Posts", + "sorting.oldest-to-newest": "Do Mais Antigo para o Mais Recente", + "sorting.newest-to-oldest": "Do Mais Recente para o Mais Antigo", + "sorting.most-votes": "Mais Votados", + "sorting.most-posts": "Mais postados", + "sorting.topic-default": "Ordenação Padrão de Tópicos", + "length": "Tamanho do Post", + "post-queue": "Fila de Posts", + "restrictions": "Restições de Postagem", + "restrictions-new": "Restrições a Novos Usuários", + "restrictions.post-queue": "Ativar enfileiramento de posts", + "restrictions.post-queue-rep-threshold": "Reputação exigida para evitar a fila de posts", + "restrictions.groups-exempt-from-post-queue": "Selecionar grupos que devem ficar isentos da fila de postagem", + "restrictions-new.post-queue": "Ativar novas restrições de usuários", + "restrictions.post-queue-help": "Habilitar a fila de postagens colocará as postagens de novos usuários na fila para aprovação", + "restrictions-new.post-queue-help": "Habilitar restrições a novos usuários irá estabelecer restrições em postagens criadas por novos usuários", + "restrictions.seconds-between": "Segundos entre postagens", + "restrictions.seconds-between-new": "Tempo em segundos entre postagens para novos usuários", + "restrictions.rep-threshold": "Reputação mínima para que essas restrições sejam desativadas", + "restrictions.seconds-before-new": "Segundos necessários antes de um novo usuário poder realizar sua primeira postagem", + "restrictions.seconds-edit-after": "Tempo, em segundos, que uma postagem permanece editável, após postada (coloque 0 para desabilitar)", + "restrictions.seconds-delete-after": "Tempo, em segundos, que uma postagem pode ser deletada, após postada (coloque 0 para desabilitar)", + "restrictions.replies-no-delete": "Após este número de respostas em uma postagem, o usuário não poderá deletar sua postagem (coloque 0 para desabilitar)", + "restrictions.min-title-length": "Tamanho Mínimo dos Títulos", + "restrictions.max-title-length": "Tamanho Máximo dos Títulos", + "restrictions.min-post-length": "Tamanho Mínimo dos Posts", + "restrictions.max-post-length": "Tamanho Máximo dos Posts", + "restrictions.days-until-stale": "Dias para que o tópico seja considerado antigo", + "restrictions.stale-help": "Se um tópico é considerado \"antigo\", então um aviso será exibido aos usuários que tentarem responder àquele tópico.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Data de corte (em dias)", + "timestamp.cut-off-help": "Datas & horários serão exibidos de uma forma relativa (por exemplo: \"3 horas atrás\" / \"5 dias atrás\"), e de acordo com os mais diversos\n\t\t\t\t\tidiomas. Após um certo ponto, este texto pode ser trocado para mostrar a própria data local\n\t\t\t\t\t(por exemplo: 5 Nov 2016 15:30).
(Padrão: 30, ou um mês). Defina como 0 para sempre mostrar datas, deixe em branco para sempre mostrar horários relativos.", + "timestamp.necro-threshold": "Limiar para Necro (em dias)", + "timestamp.necro-threshold-help": "Uma mensagem será exibida entre as postagens se o tempo entre elas for maior do que o limite necro. (Padrão: 7, ou uma semana). Defina como 0 para desativar.", + "timestamp.topic-views-interval": "Intervalo de incrementação de visualizações do tópico (em minutos)", + "timestamp.topic-views-interval-help": "As visualizações de tópico serão incrementadas apenas uma vez a cada X minutos, conforme definido por esta configuração.", + "teaser": "Post de Propaganda", + "teaser.last-post": "Último – Exibir o último post, incluindo o post original, se não houver respostas", + "teaser.last-reply": "Último – Exibir a última resposta, ou um marcador \"Sem respostas\" se não houver respostas", + "teaser.first": "Primeiro", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Configurações de Não-Lidos", + "unread.cutoff": "Data de corte de não-lidos", + "unread.min-track-last": "Mínimo de posts no tópico antes de rastrear o último lido", + "recent": "Configurações Recentes", + "recent.max-topics": "Máximo de tópicos em /recent", + "recent.categoryFilter.disable": "Desailitar filtragem de tópicos em categorias ignoradas na página /recente", + "signature": "Configurações de Assinatura", + "signature.disable": "Desabilitar assinaturas", + "signature.no-links": "Desabilitar links em assinaturas", + "signature.no-images": "Desabilitar imagens em assinaturas", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Tamanho Máximo de Assinatura", + "composer": "Configurações do Compositor", + "composer-help": "As seguintes configurações diz respeito à funcionalidade e/ou à aparência do compositor de postagem mostrado\n\t\t\t\taos usuários quando eles criam novos tópicos ou respondem a tópicos existentes.", + "composer.show-help": "Mostrar aba \"Ajuda\"", + "composer.enable-plugin-help": "Permitir plugins de adicionar conteúdo à aba ajuda", + "composer.custom-help": "Texto de Ajuda Personalizado", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Rastreamento de IP", + "ip-tracking.each-post": "Rastrear Endereço IP para cada post", + "enable-post-history": "Ativar o Histórico de Postagem" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/reputation.json b/public/language/pt-BR/admin/settings/reputation.json new file mode 100644 index 0000000000..e24a9f4a00 --- /dev/null +++ b/public/language/pt-BR/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Configurações de Reputação", + "disable": "Desabilitar o Sistema de Reputação", + "disable-down-voting": "Desativar a Negativação", + "votes-are-public": "Todos os Votos São Públicos", + "thresholds": "Limites às atividades", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Reputação mínima para votar negativamente em posts", + "downvotes-per-day": "Votos negativos por dia (definido como 0 para votos negativos ilimitados)", + "downvotes-per-user-per-day": "Votos negativos por usuário por dia (definido como 0 para votos negativos ilimitados)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Reputação mínima para sinalizar posts", + "min-rep-website": "Reputação mínima para adicionar \"Website\" ao perfil do usuário", + "min-rep-aboutme": "Reputação mínima para adicionar \"Sobre mim\" ao perfil do usuário", + "min-rep-signature": "Reputação mínima para adicionar \"Assinatura\" ao perfil do usuário", + "min-rep-profile-picture": "Reputação mínima para adicionar \"Foto do Perfil\" ao perfil do usuário", + "min-rep-cover-picture": "Reputação mínima para adicionar \"Foto de Capa\" ao perfil do usuário", + + "flags": "Configurações de Sinalização", + "flags.limit-per-target": "Número máximo de vezes que algo pode ser sinalizado", + "flags.limit-per-target-placeholder": "Padrão: 0", + "flags.limit-per-target-help": "Quando uma postagem ou usuário é sinalizado várias vezes, cada sinalizador adicional é considerado uma 'reportagem' e adicionado ao sinalizador original. Defina esta opção com um número diferente de zero para limitar o número de relatórios que um item pode receber.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Resolver automaticamente todos os tickets de um usuário quando eles são banidos", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/social.json b/public/language/pt-BR/admin/settings/social.json new file mode 100644 index 0000000000..3c58397604 --- /dev/null +++ b/public/language/pt-BR/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Compartilhamento de Posts", + "info-plugins-additional": "Plugins podem adicionar redes sociais adicionais para compartilhar posts.", + "save-success": "Redes de Compartilhamento de Posts salvas com êxito!" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/sockets.json b/public/language/pt-BR/admin/settings/sockets.json new file mode 100644 index 0000000000..299c8f44d3 --- /dev/null +++ b/public/language/pt-BR/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Configurações de Reconexão", + "max-attempts": "Máximo de Tentativas de Reconexão", + "default-placeholder": "Padrão: %1", + "delay": "Tempo para Reconexão" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/sounds.json b/public/language/pt-BR/admin/settings/sounds.json new file mode 100644 index 0000000000..9c8d09b9bc --- /dev/null +++ b/public/language/pt-BR/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notificações", + "chat-messages": "Mensagens de Chat", + "play-sound": "Tocar", + "incoming-message": "Ao receber mensagem", + "outgoing-message": "Ao enviar mensagem", + "upload-new-sound": "Enviar Novo Som", + "saved": "Configurações Salvas" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/tags.json b/public/language/pt-BR/admin/settings/tags.json new file mode 100644 index 0000000000..f44e8ba6b3 --- /dev/null +++ b/public/language/pt-BR/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Configurações de Tag", + "link-to-manage": "Gerenciar Tags", + "system-tags": "Tags do Sistema", + "system-tags-help": "Apenas usuários privilegiados poderão usar essas tags.", + "min-per-topic": "Mínimo de Tags por Tópico", + "max-per-topic": "Máximo de Tags por Tópico", + "min-length": "Tamanho Mínimo das Tags", + "max-length": "Tamanho Máximo das Tags", + "related-topics": "Tópicos Relacionados", + "max-related-topics": "Máximo de tópicos relacionados para exibir (se suportado pelo tema)" +} \ No newline at end of file diff --git a/public/language/pt-BR/admin/settings/uploads.json b/public/language/pt-BR/admin/settings/uploads.json new file mode 100644 index 0000000000..fd0a574db0 --- /dev/null +++ b/public/language/pt-BR/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Tornar arquivos enviados particulares", + "strip-exif-data": "Retirar Metadata EXIF", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Extensões de arquivo para tornar privado", + "private-uploads-extensions-help": "Digite uma lista, separada por vírgulas, de extensões de arquivos para torná-las privadas aqui (por exemplo: pdf, xls, doc). Uma lista vazia sinigica que todos os arquivos são privado.", + "resize-image-width-threshold": "Redimensionar imagens se a largura dela for maior do que a largura especificada", + "resize-image-width-threshold-help": "(em pixels, padrão: 1520 pixels, defina como 0 para desativar)", + "resize-image-width": "Redimensionar imagens para a largura especificada", + "resize-image-width-help": "(em pixels, padrão: 760 pixels, defina como 0 para desativar)", + "resize-image-quality": "Qualidade para usar ao redimensionar imagens", + "resize-image-quality-help": "Use uma configuração de qualidade mais baixa para reduzir o tamanho do arquivo de imagens redimensionadas.", + "max-file-size": "Tamanho Máximo de Arquivo (em KiB)", + "max-file-size-help": "(em kibibytes, padrão: 2048 KiB)", + "reject-image-width": "Largura Máxima da Imagem (em pixels)", + "reject-image-width-help": "Imagens com uma largura maior que esta serão rejeitadas.", + "reject-image-height": "Altura Máxima das Imagens (em pixels)", + "reject-image-height-help": "Imagens com uma altura maior do que este valor serão rejeitadas.", + "allow-topic-thumbnails": "Permitir usuários de enviar miniaturas de tópico", + "topic-thumb-size": "Tamanho da Miniatura de Tópico", + "allowed-file-extensions": "Extensões de Arquivo Permitidas", + "allowed-file-extensions-help": "Digite uma lista, separada por vírgulas, de extensões de arquivos aqui (por exemplo: pdf,xls,doc). Uma lista vazia significa que todas as extensões são permitidas.", + "upload-limit-threshold": "Limitar a taxa de uploads de usuários para:", + "upload-limit-threshold-per-minute": "A Cada %1 Minuto", + "upload-limit-threshold-per-minutes": "A Cada %1 Minutos", + "profile-avatars": "Avatares de Perfil", + "allow-profile-image-uploads": "Permitir usuários de enviar imagens de perfil", + "convert-profile-image-png": "Converter imagens de perfil enviadas para PNG", + "default-avatar": "Avatar Personalizado Padrão", + "upload": "Enviar", + "profile-image-dimension": "Dimensão da Imagem de Perfil", + "profile-image-dimension-help": "(em pixels, padrão: 128 pixels)", + "max-profile-image-size": "Tamanho Máximo do Arquivo de Imagem de Perfil", + "max-profile-image-size-help": "(em kibibytes, padrão: 256 KiB)", + "max-cover-image-size": "Tamanho Máximo do Arquivo de Imagem de Capa", + "max-cover-image-size-help": "(em kibibytes, padrão: 2,048 KiB)", + "keep-all-user-images": "Manter versões antigas de avatares e capas de perfil no servidor", + "profile-covers": "Capas de Perfil", + "default-covers": "Imagens de Capa Padrão", + "default-covers-help": "Adicione uma lista, separada por vírgulas, de imagens de capa padrão para contas que não tenham enviado uma imagem de capa" +} diff --git a/public/language/pt-BR/admin/settings/user.json b/public/language/pt-BR/admin/settings/user.json new file mode 100644 index 0000000000..069cfb422f --- /dev/null +++ b/public/language/pt-BR/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Autenticação", + "email-confirm-interval": "O usuário não pode reenviar um e-mail de confirmação até", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Permitir login com", + "allow-login-with.username-email": "Nome de Usuário ou E-mail", + "allow-login-with.username": "Apenas Nome de Usuário", + "account-settings": "Configurações de Conta", + "gdpr_enabled": "Ativar coleta de consentimento do GDPR", + "gdpr_enabled_help": "Quando ativado, todos os novos registrantes serão obrigados a dar consentimento explícito para a coleta de dados e uso sob o General Data Protection Regulation (GDPR). Nota: Ativar o GDPR não força usuários pré-existentes a fornecer consentimento. Para fazer isso, você precisará instalar o plug-in GDPR.", + "disable-username-changes": "Desabilitar mudança de nome de usuário", + "disable-email-changes": "Desabilitar mudanças de e-mail", + "disable-password-changes": "Desabilitar mudanças de senha", + "allow-account-deletion": "Permitir exclusão de conta", + "hide-fullname": "Esconder nome completo de outros usuários", + "hide-email": "Esconder e-mail de outros usuários", + "show-fullname-as-displayname": "Mostrar o nome completo do usuário como nome de exibição, se disponível", + "themes": "Temas", + "disable-user-skins": "Impedir usuários de escolherem um tema diferente", + "account-protection": "Proteção de Conta", + "admin-relogin-duration": "Duração para que se exija um novo login para acessar o painel administrativo (em minutos)", + "admin-relogin-duration-help": "Após um determinado período de tempo, o acesso ao painel administrativo exigirá um novo login. Defina como 0 para desabilitar.", + "login-attempts": "Tentativas de login por hora", + "login-attempts-help": "Se as tentativas de login na conta de um usuário ultrapassar este limite, essa conta será bloqueada por um período de tempo pré-determinado.", + "lockout-duration": "Duração do Bloqueio de Conta (em minutos)", + "login-days": "Dias para lembrar as sessões de login dos usuários", + "password-expiry-days": "Forçar a redefinição de senha após um determinado número de dias", + "session-time": "Tempo de Sessão", + "session-time-days": "Dias", + "session-time-seconds": "Segundos", + "session-time-help": "Estes valores são usados para determinar por quanto tempo um usuário fica logado quando eles habilitarem a opção "Lembrar-me" durante o login. Observe que apenas um destes valores será usado. Se não houver um valor para segundos, usamos o valor de dias. Se não houver um valor para dias, usamos o valor padrão, que é 14 dias.", + "online-cutoff": "Minutos para que o usuário seja considerado inativo", + "online-cutoff-help": "Se o usuário não realizar nenhuma ação durante esse período, ele será considerado inativo e não receberá atualizações em tempo real.", + "registration": "Registro de Usuário", + "registration-type": "Tipo de Registro", + "registration-approval-type": "Tipo de Aprovação de Registro", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Aprovação do Administrador", + "registration-type.admin-approval-ip": "Aprovação do Administrador para IPs", + "registration-type.invite-only": "Apenas por Convite", + "registration-type.admin-invite-only": "Apenas por Convite da Administração", + "registration-type.disabled": "Desativar registros", + "registration-type.help": "Normal - Usuários podem se registrar pela página /register.
\nApenas Convite - Usuários podem convidar outros da página usuários.
\nApenas Convite do Administrador - Apenas administradores podem convidar das páginas de usuários e administração/administrar/usuários .
\nSem registro - Sem registro de usuários.
", + "registration-approval-type.help": "Normal - Usuários são registrados imediatamente.
\nAprovação do Admin - Registros de usuários são colocados em uma fila de aprovação por administradores.
\nAprovação do Admin por IPs - Normal para novos usuários, Aprovação do Admin por endereços IPs que já têm uma conta.
", + "registration-queue-auto-approve-time": "Tempo de Aprovação Automática", + "registration-queue-auto-approve-time-help": "Horas antes que o usuário seja aprovado automaticamente. 0 para desativar.", + "registration-queue-show-average-time": "Mostrar aos usuários o tempo médio de demora para aprovar um novo usuário", + "registration.max-invites": "Máximo de Convites por Usuário", + "max-invites": "Máximo de Convites por Usuário", + "max-invites-help": "0 para nenhuma restrição. Administradores têm convites infinitos.
Apenas aplicável para \"Apenas por Convite\".", + "invite-expiration": "O convite expira em", + "invite-expiration-help": "número de dias em que o convite expira.", + "min-username-length": "Tamanho Mínimo do Nome de Usuário", + "max-username-length": "Tamanho Máximo do Nome de Usuário", + "min-password-length": "Tamanho Mínimo da Senha", + "min-password-strength": "Força Mínima da Senha", + "max-about-me-length": "Tamanho Máximo do Sobre Mim", + "terms-of-use": "Termos de Uso do Fórum (Deixar em branco para desabilitar)", + "user-search": "Pesquisa de Usuário", + "user-search-results-per-page": "Número de resultados para exibir", + "default-user-settings": "Configurações Padrão de Usuário", + "show-email": "Exibir e-mail", + "show-fullname": "Exibir nome completo", + "restrict-chat": "Permitir apenas mensagens de chat de usuários que eu sigo", + "outgoing-new-tab": "Abrir links externos em nova aba", + "topic-search": "Permitir Busca dentro do Tópico", + "update-url-with-post-index": "Atualizar url com índice de postagem enquanto navega pelos tópicos", + "digest-freq": "Habilitar o recebimento de Resumos", + "digest-freq.off": "Desligado", + "digest-freq.daily": "Diário", + "digest-freq.weekly": "Semanal", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Mensal", + "email-chat-notifs": "Envie um email se uma nova mensagem de chat chegar e eu não estiver online", + "email-post-notif": "Envie um email quando respostas forem dadas a tópicos que estou inscrito", + "follow-created-topics": "Seguir tópicos que você criar", + "follow-replied-topics": "Seguir os tópicos que você responder", + "default-notification-settings": "Configurações Padrão de Notificações", + "categoryWatchState": "Configuração padrão em relação a acompanhar as novidades das categorias", + "categoryWatchState.watching": "Acompanhando", + "categoryWatchState.notwatching": "Não Acompanhar", + "categoryWatchState.ignoring": "Ignorar" +} diff --git a/public/language/pt-BR/admin/settings/web-crawler.json b/public/language/pt-BR/admin/settings/web-crawler.json new file mode 100644 index 0000000000..ed9a88aebf --- /dev/null +++ b/public/language/pt-BR/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Configurações de Motores de Busca", + "robots-txt": "Personalização de Robots.txt Deixe em branco para o padrão", + "sitemap-feed-settings": "Configurações de Mapa do Site & Feeds", + "disable-rss-feeds": "Desabilitar Feeds RSS", + "disable-sitemap-xml": "Desativar Sitemap.xml", + "sitemap-topics": "Número de Tópicos para mostrar no Mapa do Site", + "clear-sitemap-cache": "Limpar Cache de Mapa do Site", + "view-sitemap": "Ver Mapa do Site" +} \ No newline at end of file diff --git a/public/language/pt-BR/category.json b/public/language/pt-BR/category.json new file mode 100644 index 0000000000..d608582acc --- /dev/null +++ b/public/language/pt-BR/category.json @@ -0,0 +1,23 @@ +{ + "category": "Categoria", + "subcategories": "Subcategorias", + "new_topic_button": "Novo Tópico", + "guest-login-post": "Entre para postar", + "no_topics": "Não há tópicos nesta categoria.
Por que você não tenta postar um?", + "browsing": "navegando", + "no_replies": "Ninguém respondeu", + "no_new_posts": "Não há nenhum post nesta categoria.", + "watch": "Acompanhar", + "ignore": "Ignorar", + "watching": "Acompanhar", + "not-watching": "Não Acompanhar", + "ignoring": "Ignorar", + "watching.description": "Mostrar tópicos em não-lidos e recentes", + "not-watching.description": "Não mostrar tópicos em não-lidos, mostrar em recentes", + "ignoring.description": "Não mostrar tópicos em não-lidos e nem em recentes", + "watching.message": "Agora, você está acompanhando as novidades desta categoria e todas as suas subcategorias", + "notwatching.message": "Agora, você não está acompanhando as novidades nem desta categoria e nem de suas subcategorias", + "ignoring.message": "Agora, você está ignorando as novidades desta categorias e de todas as suas subcategorias", + "watched-categories": "Categorias acompanhadas", + "x-more-categories": "mais %1 categorias" +} \ No newline at end of file diff --git a/public/language/pt-BR/email.json b/public/language/pt-BR/email.json new file mode 100644 index 0000000000..cbbdcb074d --- /dev/null +++ b/public/language/pt-BR/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "E-mail de Teste", + "password-reset-requested": "Redefinição de Senha Solicitada!", + "welcome-to": "Bem vindo a %1", + "invite": "Convite de %1", + "greeting_no_name": "Olá", + "greeting_with_name": "Olà %1", + "email.verify-your-email.subject": "Por favor, confirme o seu e-mail", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Obrigado por se registrar com %1!", + "welcome.text2": "Para terminar de ativar sua conta, nós precisamos verificar que você é o dono do endereço de email com o que você se registrou.", + "welcome.text3": "Um administrador aceitou o seu pedido de registro. Você pode fazer login agora com seu nome de usuário/senha.", + "welcome.cta": "Clique aqui para confirmar seu endereço de email", + "invitation.text1": "%1 convidou você para participar de %2", + "invitation.text2": "O seu convite irá expirar em %1 dias.", + "invitation.cta": "Clique aqui para criar sua conta.", + "reset.text1": "Nós recebemos um pedido para reconfigurar sua senha, possivelmente porque você a esqueceu. Se este não é o caso, por favor ignore este email.", + "reset.text2": "Para continuar com a reconfiguração de senha, por favor clique no seguinte link:", + "reset.cta": "Clique aqui para reconfigurar sua senha", + "reset.notify.subject": "Senha alterada com sucesso", + "reset.notify.text1": "Nós estamos notificando você que em %1, sua senha foi alterada com sucesso.", + "reset.notify.text2": "Se você não autorizou isso, por favor notifique um administrador imediatamente.", + "digest.latest_topics": "Últimos tópicos de %1", + "digest.top-topics": "Tópicos principais de %1", + "digest.popular-topics": "Tópicos populares de %1", + "digest.cta": "Clique aqui para visitar %1", + "digest.unsub.info": "Este resumo foi enviado para você devido às suas configurações de assinatura.", + "digest.day": "dia", + "digest.week": "semana", + "digest.month": "mês", + "digest.subject": "Resumo de %1", + "digest.title.day": "Seu Resumo Diário", + "digest.title.week": "Seu Resumo Semanal", + "digest.title.month": "Seu Resumo Mensal", + "notif.chat.subject": "Nova mensagem de chat recebida de %1", + "notif.chat.cta": "Clique aqui para continuar a conversa", + "notif.chat.unsub.info": "Esta notificação de chat foi enviada a você devido às suas configurações de assinatura.", + "notif.post.unsub.info": "Esta notificação de postagem foi enviada para você devido as suas configurações de assinatura.", + "notif.post.unsub.one-click": "Como alternativa, cancele a inscrição de futuros e-mails como este clicando em", + "notif.cta": "Para o fórum", + "notif.cta-new-reply": "Ver Post", + "notif.cta-new-chat": "Ver Chat", + "notif.test.short": "Testando Notificações", + "notif.test.long": "Este é um teste do email de notificações. Envie ajuda!", + "test.text1": "Este é um e-mail de teste, para verificar que o enviador de emails está corretamente configurado no seu NodeBB.", + "unsub.cta": "Clique aqui para alterar estas configurações", + "unsubscribe": "desinscrever", + "unsub.success": " Você não receberá mais e-mails da lista de emails %1", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Você foi banido de %1", + "banned.text1": "O usuário %1 foi banido de %2.", + "banned.text2": "Este banimento durará até %1.", + "banned.text3": "Este é o motivo pelo qual você foi banido:", + "closing": "Obrigado!" +} \ No newline at end of file diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json new file mode 100644 index 0000000000..960e95cc55 --- /dev/null +++ b/public/language/pt-BR/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Dados Inválidos", + "invalid-json": "JSON Inválido", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Você não parece estar logado.", + "account-locked": "Sua conta foi temporariamente bloqueada ", + "search-requires-login": "É necessário ter uma conta para pesquisar - por favor efetue o login ou cadastre-se.", + "goback": "Pressione voltar para retornar à página anterior", + "invalid-cid": "ID de Categoria Inválido", + "invalid-tid": "ID de Tópico Inválido", + "invalid-pid": "ID de Post Inválido", + "invalid-uid": "ID de Usuário Inválido", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "Uma data válida deve ser fornecida", + "invalid-username": "Nome de Usuário Inválido", + "invalid-email": "Email Inválido", + "invalid-fullname": "Nome Completo Inválido", + "invalid-location": "Localização Inválida", + "invalid-birthday": "Data de Nascimento Inválida", + "invalid-title": "Título inválido", + "invalid-user-data": "Dados de Usuário Inválidos", + "invalid-password": "Senha Inválida", + "invalid-login-credentials": "Credenciais de login inválidas", + "invalid-username-or-password": "Por favor especifique ambos nome de usuário e senha", + "invalid-search-term": "Termo de pesquisa inválido", + "invalid-url": "URL Inválido", + "invalid-event": "Evento inválido: %1", + "local-login-disabled": "O sistema de login local foi desativado para contas sem privilégios.", + "csrf-invalid": "Não foi possível realizar o seu login, provavelmente porque a sua sessão expirou. Por favor, tente novamente.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Valor de paginação inválido, precisa ser no mínimo %1 e no máximo %2", + "username-taken": "Nome de usuário já existe", + "email-taken": "Email já cadastrado", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "O email já foi convidado", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Você não está habilitado a conversar até que seu email seja confirmado, por favor clique aqui para confirmar seu email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Nós não pudemos confirmar seu email, por gentileza tente novamente mais tarde.", + "confirm-email-already-sent": "O email de confirmação já foi enviado, por favor aguarde %1 minuto(s) para enviar outro.", + "sendmail-not-found": "O executável do sendmail não pôde ser encontrado. Por favor, certifique-se de que ele está instalado e que está autorizado a ser executado pelo usuário que roda o NodeBB.", + "digest-not-enabled": "Este usuário não tem resumos habilitados ou o padrão do sistema não está configurado para enviar resumos", + "username-too-short": "Nome de usuário muito curto", + "username-too-long": "Nome de usuário muito longo", + "password-too-long": "A senha é muito grande", + "reset-rate-limited": "Muitas solicitações de redefinição de senha (taxa de ação limitada)", + "reset-same-password": "Use uma senha diferente da atual", + "user-banned": "Usuário banido", + "user-banned-reason": "Desculpa, esta conta foi banida (Motivo: %1)", + "user-banned-reason-until": "Desculpa, esta conta foi banida até %1 (Motivo: %2)", + "user-too-new": "Desculpe, é necessário que você aguarde %1 segundo(s) antes de fazer o seu primeiro post.", + "blacklisted-ip": "Desculpa, o seu endereço IP foi banido desta comunidade. Se você acha que isso é um engano, por favor, contate um administrador.", + "ban-expiry-missing": "Por favor forneça uma data para o fim deste banimento", + "no-category": "A categoria não existe", + "no-topic": "O tópico não existe", + "no-post": "O post não existe", + "no-group": "O grupo não existe", + "no-user": "O usuário não existe", + "no-teaser": "O teaser não existe", + "no-flag": "Flag does not exist", + "no-privileges": "Você não possui privilégios suficientes para esta ação.", + "category-disabled": "Categoria desativada", + "topic-locked": "Tópico Trancado", + "post-edit-duration-expired": "Você só pode editar posts %1 segundo(s) após postar.", + "post-edit-duration-expired-minutes": "Você só pode editar posts %1 minuto(s) após postar", + "post-edit-duration-expired-minutes-seconds": "Você só pode editar posts %1 minuto(s) e %2 segundo(s) após postar", + "post-edit-duration-expired-hours": "Você só pode editar posts %1 hora(s) após postar", + "post-edit-duration-expired-hours-minutes": "Você só pode editar posts %1 hora(s) e %2 minuto(s) após postar", + "post-edit-duration-expired-days": "Você só pode editar posts %1 dia(s) após postar", + "post-edit-duration-expired-days-hours": "Você só pode editar posts %1 dia(s) e %2 hora(s) após postar", + "post-delete-duration-expired": "Você só pode deletar posts %1 segundo(s) após postar", + "post-delete-duration-expired-minutes": "Você só pode deletar post %1 minuto(s) após postar", + "post-delete-duration-expired-minutes-seconds": "Você só pode deletar posts %1 minuto(s) e %2 segundo(s) após postar", + "post-delete-duration-expired-hours": "Você só pode deletar posts %1 hora(s) após postar", + "post-delete-duration-expired-hours-minutes": "Você só pode deletar posts %1 hora(s) e %2 minutos(s) após postar", + "post-delete-duration-expired-days": "Você só pode deletar posts %1 dia(s) após postar", + "post-delete-duration-expired-days-hours": "Você só pode deletar posts %1 dia(s) e %2 hora(s) após postar", + "cant-delete-topic-has-reply": "Você não pode excluir o seu tópico após ele ter uma resposta", + "cant-delete-topic-has-replies": "Você não pode excluir o seu tópico após ele ter %1 respostas", + "content-too-short": "Por favor digite um post maior. Posts precisam conter ao menos %1 caractere(s).", + "content-too-long": "Por favor digite um post mais curto. Posts não podem ser maiores que %1 caractere(s)", + "title-too-short": "Por favor digite um título maior. Títulos devem conter no mínimo %1 caractere(s)", + "title-too-long": "Por favor digite um título menor. Títulos não podem ser maiores que %1 caractere(s).", + "category-not-selected": "Categoria não escolhida.", + "too-many-posts": "Você pode postar uma vez a cada %1 segundo(s) - por favor aguarde antes de postar novamente", + "too-many-posts-newbie": "Como novo usuário, você só pode postar uma vez a cada %1 segundo(s) até que você tenha, pelo menos, %2 de reputação. Por favor, aguarde antes de postar novamente.", + "already-posting": "You are already posting", + "tag-too-short": "Por favor digite uma tag maior. Tags devem conter pelo menos %1 caractere(s)", + "tag-too-long": "Por favor digite uma tag menor. Tags não podem conter mais que %1 caractere(s)", + "not-enough-tags": "Sem tags suficientes. Tópicos devem ter no mínimo %1 tag(s)", + "too-many-tags": "Muitas tags. Tópicos não podem ter mais que %1 tag(s)", + "cant-use-system-tag": "Você não pode usar esta tag de sistema.", + "cant-remove-system-tag": "Você não pode remover esta tag de sistema.", + "still-uploading": "Aguarde a conclusão dos uploads.", + "file-too-big": "O tamanho máximo permitido de arquivo é de %1 kB - por favor faça upload de um arquivo menor", + "guest-upload-disabled": "O upload por visitantes foi desabilitado", + "cors-error": "Não é possível fazer o upload da imagem devido ao CORS mal configurado", + "upload-ratelimit-reached": "Você fez upload de muitos arquivos de uma vez. Por favor, tente novamente mais tarde.", + "scheduling-to-past": "Selecione uma data no futuro.", + "invalid-schedule-date": "Por favor, insira uma data e hora válidas.", + "cant-pin-scheduled": "Tópicos agendados não podem ser (des)fixados.", + "cant-merge-scheduled": "Os tópicos agendados não podem ser mesclados.", + "cant-move-posts-to-scheduled": "Não é possível mover posts para um tópico agendado.", + "cant-move-from-scheduled-to-existing": "Não é possível mover postagens de um tópico agendado para um tópico existente.", + "already-bookmarked": "Você já adicionou este post aos favoritos", + "already-unbookmarked": "Você já removeu este post dos favoritos", + "cant-ban-other-admins": "Você não pode banir outros administradores!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Você é o único administrador. Adicione outro usuário como administrador antes de remover a si mesmo como admin", + "account-deletion-disabled": "A exclusão de conta está desabilitada", + "cant-delete-admin": "Remova os privilégios de administrador dessa conta antes de tentar excluí-la.", + "already-deleting": "Já excluindo", + "invalid-image": "Imagem Inválida", + "invalid-image-type": "Tipo inválido de imagem. Os tipos permitidos são: %1", + "invalid-image-extension": "Extensão de imagem inválida", + "invalid-file-type": "Tipo de arquivo inválido. Os tipos permitidos são: %1", + "invalid-image-dimensions": "As dimensões da imagem são muito grandes", + "group-name-too-short": "Nome do grupo é muito curto", + "group-name-too-long": "O nome do grupo é muito extenso", + "group-already-exists": "O grupo já existe", + "group-name-change-not-allowed": "Sem permissão para alterar nome do grupo", + "group-already-member": "Já faz parte deste grupo", + "group-not-member": "Não é membro deste grupo", + "group-needs-owner": "Este grupo requer ao menos um dono", + "group-already-invited": "Esse usuário já foi convidado", + "group-already-requested": "Seu pedido de filiação já foi enviado", + "group-join-disabled": "Você não pode entrar neste grupo no momento", + "group-leave-disabled": "Você não pode sair deste grupo no momento", + "post-already-deleted": "Este post já foi deletado", + "post-already-restored": "Este post já foi restaurado", + "topic-already-deleted": "Esté tópico já foi deletado", + "topic-already-restored": "Este tópico já foi restaurado", + "cant-purge-main-post": "Você não pode remover o post principal, ao invés disso, apague o tópico por favor.", + "topic-thumbnails-are-disabled": "Thumbnails para tópico estão desativados.", + "invalid-file": "Arquivo Inválido", + "uploads-are-disabled": "Uploads estão desativados", + "signature-too-long": "Desculpe, sua assinatura não pode ser maior que %1 caractere(s).", + "about-me-too-long": "Desculpe, o sobre não pode ser maior que %1 caractere(s).", + "cant-chat-with-yourself": "Você não pode iniciar um chat consigo mesmo!", + "chat-restricted": "Este usuário restringiu suas mensagens de chat. Eles devem seguir você antes que você possa conversar com eles", + "chat-disabled": "O sistema de chat foi desabilitado", + "too-many-messages": "Você enviou muitas mensagens, por favor aguarde um momento.", + "invalid-chat-message": "Mensagem de chat inválida", + "chat-message-too-long": "Mensagens de chat não podem ter mais do que %1 caracteres.", + "cant-edit-chat-message": "Você não tem permissão para editar esta mensagem", + "cant-delete-chat-message": "Você não possui permissão para deletar esta mensagem", + "chat-edit-duration-expired": "Você só pode editar mensagens de chat %1 segundo(s) após postar", + "chat-delete-duration-expired": "Você só pode deletar mensagens de chat %1 segundo(s) após postar", + "chat-deleted-already": "Essa mensagem de chat já foi deletada", + "chat-restored-already": "Essa mensagem de chat já foi restaurada.", + "chat-room-does-not-exist": "A sala de chat não existe.", + "already-voting-for-this-post": "Você já votou neste post.", + "reputation-system-disabled": "O sistema de reputação está desabilitado.", + "downvoting-disabled": "Negativação está desabilitada", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "Você já sinalizou esse post", + "user-already-flagged": "Você já sinalizou esse usuário", + "post-flagged-too-many-times": "Esta postagem já foi sinalizada por outras pessoas", + "user-flagged-too-many-times": "Este usuário já foi sinalizado por outros", + "cant-flag-privileged": "Você não tem permissão para sinalizar os perfis ou o conteúdo de usuários privilegiados (moderadores/moderadores globais/administradores)", + "self-vote": "Você não pode votar no seu próprio post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "Você só pode votar negativamente %1 vezes por dia", + "too-many-downvotes-today-user": "Você só pode votar contra um usuário %1 vezes por dia", + "reload-failed": "O NodeBB encontrou um problema ao recarregar: \"%1\". O NodeBB continuará a servir os assets existentes no lado do cliente, apesar de que você deve desfazer o que você fez antes de recarregar.", + "registration-error": "Erro de Cadastro", + "parse-error": "Algo deu errado ao receber a resposta do servidor", + "wrong-login-type-email": "Por favor use seu email para fazer login", + "wrong-login-type-username": "Por favor use o seu nome de usuário para fazer login", + "sso-registration-disabled": "O cadastro foi desativado para as contas de %1. Por favor, registre-se com um endereço de e-mail primeiro.", + "sso-multiple-association": "Você não pode associar várias contas deste serviço à sua conta do NodeBB. Por favor, desassocie sua conta existente e tente novamente.", + "invite-maximum-met": "Você já convidou o número máximo de pessoas (%1 de %2).", + "no-session-found": "Nenhuma sessão de login encontrada!", + "not-in-room": "O usuário não está na sala", + "cant-kick-self": "Você não pode kickar a si mesmo do grupo", + "no-users-selected": "Nenhuma escolha de usuário(s) foi feita", + "invalid-home-page-route": "Rota de página inicial inválida", + "invalid-session": "Sessão Inválida", + "invalid-session-text": "Parece que a sua sessão de login não está mais ativa. Por favor, atualize esta página.", + "session-mismatch": "Sessão Incompatível", + "session-mismatch-text": "Parece que sua sessão de login não combina mais com a do servidor. Por favor, atualize esta página.", + "no-topics-selected": "Nenhum tópico selecionado!", + "cant-move-to-same-topic": "Não é possível mover um post para o mesmo tópico!", + "cant-move-topic-to-same-category": "Não é possível mover o tópico para a mesma categoria!", + "cannot-block-self": "Você pode bloquear a si mesmo!", + "cannot-block-privileged": "Você não pode bloquear administradores e moderadores globais", + "cannot-block-guest": "Vistantes não podem bloquear outros usuários", + "already-blocked": "Este usuário já foi bloqueado", + "already-unblocked": "Este usuário já foi desbloqueado", + "no-connection": "Parece haver um problema com a sua conexão com a internet", + "socket-reconnect-failed": "Não foi possível acessar o servidor neste momento. Clique aqui para tentar novamente ou tente novamente mais tarde", + "plugin-not-whitelisted": "Não foi possível instalar o plugin - apenas os plug-ins permitidos pelo NodeBB Package Manager podem ser instalados através do ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Evento de tópico '%1' não reconhecido", + "cant-set-child-as-parent": "Não é possível definir filho como categoria pai", + "cant-set-self-as-parent": "Não é possível definir a si mesmo como categoria pai", + "api.master-token-no-uid": "Um token mestre foi recebido sem um `_uid` correspondente no corpo da requisição", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "Não foi encontrada uma sessão válida. Faça login e tente novamente.", + "api.403": "Você não tem autorização para fazer esta chamada", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/pt-BR/flags.json b/public/language/pt-BR/flags.json new file mode 100644 index 0000000000..19b084edc0 --- /dev/null +++ b/public/language/pt-BR/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Estado", + "reports": "Reportagens", + "first-reported": "Primeiro Reportado", + "no-flags": "Ihuul! Nenhuma sinalização encontrada.", + "assignee": "Responsável", + "update": "Atualizar", + "updated": "Atualizado", + "resolved": "Resolvido", + "target-purged": "O conteúdo ao qual essa sinalização se referia foi removido e não está mais disponível.", + + "graph-label": "Sinalizações Diárias", + "quick-filters": "Filtros Rápidos", + "filter-active": "Há um ou mais filtros ativos nesta lista de sinalizações", + "filter-reset": "Remover Filtros", + "filters": "Opções de Filtro", + "filter-reporterId": "UID do Reportador", + "filter-targetUid": "UID Sinalizado", + "filter-type": "Tipo de Sinalização", + "filter-type-all": "Todo o Conteúdo", + "filter-type-post": "Post", + "filter-type-user": "Usuário", + "filter-state": "Estado", + "filter-assignee": "UID do Responsável", + "filter-cid": "Categoria", + "filter-quick-mine": "Atribuído a mim", + "filter-cid-all": "Todas as categorias", + "apply-filters": "Aplicar Filtros", + "more-filters": "Mais Filtros", + "fewer-filters": "Menos Filtros", + + "quick-actions": "Ações Rápidas", + "flagged-user": "Usuário Sinalizado", + "view-profile": "Ver Perfil", + "start-new-chat": "Iniciar Novo Chat", + "go-to-target": "Ver o Sinalizado", + "assign-to-me": "Atribuir À Mim", + "delete-post": "Excluir Post", + "purge-post": "Expurgar Post", + "restore-post": "Restaurar Post", + "delete": "Delete Flag", + + "user-view": "Ver Perfil", + "user-edit": "Editar Perfil", + + "notes": "Notas da Sinalização", + "add-note": "Adicionar Nota", + "no-notes": "Nenhuma nota compartilhada.", + "delete-note-confirm": "Tem certeza de que deseja excluir esta nota de sinalização?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Nota Adicionada", + "note-deleted": "Nota Excluída", + "flag-deleted": "Flag Deleted", + + "history": "Histórico da Conta & Sinalizações", + "no-history": "Sem histórico de sinalizações.", + + "state-all": "Todos os estados", + "state-open": "Novo/Aberto", + "state-wip": "Trabalho em Progresso", + "state-resolved": "Resolvido", + "state-rejected": "Rejeitado", + "no-assignee": "Não Atribuído", + + "sort": "Ordenar por", + "sort-newest": "Mais recentes primeiro", + "sort-oldest": "Mais antigos primeiro", + "sort-reports": "Mais reportagens", + "sort-all": "Todos os tipos de sinalização...", + "sort-posts-only": "Posts apenas...", + "sort-downvotes": "Mais baixovotos", + "sort-upvotes": "Mais cimavotos", + "sort-replies": "Mais réplicas", + + "modal-title": "Reportar Conteúdo", + "modal-body": "Por favor, especifique a razão pela qual você está sinalizando %1 %2 para a revisão. Alternativamente, use um dos botões de reporte rápido se for aplicável.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Ofensivo", + "modal-reason-other": "Outro (especifique abaixo)", + "modal-reason-custom": "Motivo para reportar este conteúdo...", + "modal-submit": "Enviar Denúncia", + "modal-submit-success": "O conteúdo foi sinalizado para moderação.", + + "bulk-actions": "Ações em Massa", + "bulk-resolve": "Resolver Sinalização(ões)", + "bulk-success": "%1 sinalizações atualizadas", + "flagged-timeago-readable": "Sinalizado (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/pt-BR/global.json b/public/language/pt-BR/global.json new file mode 100644 index 0000000000..642255b20d --- /dev/null +++ b/public/language/pt-BR/global.json @@ -0,0 +1,126 @@ +{ + "home": "Home", + "search": "Pesquisar", + "buttons.close": "Fechar", + "403.title": "Acesso Negado", + "403.message": "Parece que você chegou à uma página à qual você não tem acesso.", + "403.login": "Talvez você deveria tentar fazer login?", + "404.title": "Não Encontrado", + "404.message": "Parece que você chegou à uma página que não existe. Voltar para a página inicial.", + "500.title": "Erro Interno.", + "500.message": "Oops! Parece que algo deu errado!", + "400.title": "Solicitação Inválida.", + "400.message": "Parece que este link está incorreto. Por favor, verifique e tente novamente. Caso contrário, volte para a página inicial.", + "register": "Cadastrar", + "login": "Login", + "please_log_in": "Por Favor Efetue o Login", + "logout": "Logout", + "posting_restriction_info": "A postagem está restrita apenas à membros registrados, clique aqui para logar.", + "welcome_back": "Bem-vindo de volta", + "you_have_successfully_logged_in": "Você logou com sucesso", + "save_changes": "Salvar Alterações", + "save": "Salvar", + "close": "Fechar", + "pagination": "Paginação", + "pagination.out_of": "%1 de %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Categorias", + "header.recent": "Recente", + "header.unread": "Não Lido", + "header.tags": "Tags", + "header.popular": "Popular", + "header.top": "Topo", + "header.users": "Usuários", + "header.groups": "Grupos", + "header.chats": "Chats", + "header.notifications": "Notificações", + "header.search": "Pesquisar", + "header.profile": "Perfil", + "header.navigation": "Navegação", + "notifications.loading": "Carregando Notificações", + "chats.loading": "Carregando Chats", + "motd.welcome": "Seja bem-vindo ao NodeBB, a plataforma de discussão do futuro.", + "previouspage": "Página Anterior", + "nextpage": "Próxima Página", + "alert.success": "Sucesso", + "alert.error": "Erro", + "alert.banned": "Banido", + "alert.banned.message": "Você acaba de ser banido, seu acesso agora está restrito.", + "alert.unbanned": "Des-banido", + "alert.unbanned.message": "Seu banimento foi suspenso.", + "alert.unfollow": "Você deixou de seguir %1!", + "alert.follow": "Agora você está seguindo %1!", + "users": "Usuários", + "topics": "Tópicos", + "posts": "Posts", + "x-posts": "%1 posts", + "best": "Melhor", + "controversial": "Controversial", + "votes": "Votos", + "x-votes": "%1 votos", + "voters": "Votantes", + "upvoters": "Votos positivos", + "upvoted": "Votou positivamente", + "downvoters": "Votos negativos", + "downvoted": "Votou negativamente", + "views": "Visualizações", + "posters": "Posters", + "reputation": "Reputação", + "lastpost": "Última postagem", + "firstpost": "Primeira postagem", + "read_more": "ler mais", + "more": "Mais", + "none": "None", + "posted_ago_by_guest": "postado %1 por Visitante", + "posted_ago_by": "postado %1 por %2", + "posted_ago": "postou %1", + "posted_in": "postado em %1", + "posted_in_by": "postado em %1 por %2", + "posted_in_ago": "postado em %1 %2", + "posted_in_ago_by": "postado em %1 %2 por %3", + "user_posted_ago": "%1 postou %2", + "guest_posted_ago": "Visitante postou %1", + "last_edited_by": "última edição por %1", + "norecentposts": "Nenhum Post Recente", + "norecenttopics": "Sem Tópicos Recentes", + "recentposts": "Posts Recentes", + "recentips": "Recentemente Logado nos IPs", + "moderator_tools": "Ferramentas de Moderação", + "online": "Online", + "away": "Ausente", + "dnd": "Não perturbar", + "invisible": "Invisível", + "offline": "Offline", + "email": "Email", + "language": "Idioma", + "guest": "Visitante", + "guests": "Visitantes", + "former_user": "Um ex-usuário", + "system-user": "Sistema", + "unknown-user": "Usuário desconhecido", + "updated.title": "Fórum Atualizado", + "updated.message": "Este fórum foi atualizado para sua última versão. Clique aqui para atualizar a página.", + "privacy": "Privacidade", + "follow": "Seguir", + "unfollow": "Deixar de seguir", + "delete_all": "Deletar Tudo", + "map": "Mapa", + "sessions": "Sessões de Login", + "ip_address": "Endereço IP", + "enter_page_number": "Digite o número da página", + "upload_file": "Fazer upload de arquivo", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Os tipos de arquivo permitidos são %1", + "unsaved-changes": "Você tem alterações não salvas. Tem certeza de que você deseja sair da página?", + "reconnecting-message": "Parece que a sua conexão com o %1 caiu. Por favor, aguarde enquanto tentamos reconectar.", + "play": "Executar", + "cookies.message": "Este site usa cookies para garantir que você obtenha a melhor experiência em nosso site.", + "cookies.accept": "Entendi!", + "cookies.learn_more": "Saber Mais", + "edited": "Editado", + "disabled": "Desativado", + "select": "Escolha", + "user-search-prompt": "Digite alguma coisa aqui para encontrar usuários..." +} \ No newline at end of file diff --git a/public/language/pt-BR/groups.json b/public/language/pt-BR/groups.json new file mode 100644 index 0000000000..c4a056a8dd --- /dev/null +++ b/public/language/pt-BR/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupos", + "view_group": "Ver Grupo", + "owner": "Dono do Grupo", + "new_group": "Criar Novo Grupo", + "no_groups_found": "Não há grupos para ver", + "pending.accept": "Aceitar", + "pending.reject": "Rejeitar", + "pending.accept_all": "Aceitar Todos", + "pending.reject_all": "Rejeitar Todos", + "pending.none": "Não há membros pendentes no momento", + "invited.none": "Não há membros convidados no momento", + "invited.uninvite": "Anular Convite", + "invited.search": "Procure por um usuário para convidar para esse grupo", + "invited.notification_title": "Você foi convidado a participar de %1", + "request.notification_title": "Solicitação de Membro de Grupo de %1", + "request.notification_text": "%1 pediu para se tornar um membro de %2", + "cover-save": "Salvar", + "cover-saving": "Salvando", + "details.title": "Detalhes do Grupo", + "details.members": "Lista de Membros", + "details.pending": "Membros Pendentes", + "details.invited": "Membros Convidados", + "details.has_no_posts": "Os membros deste grupo não fizeram quaisquer posts.", + "details.latest_posts": "Últimos Posts", + "details.private": "Particular", + "details.disableJoinRequests": "Desabilitar pedidos de participação", + "details.disableLeave": "Impedir que usuários saiam do grupo", + "details.grant": "Conceder/Retomar a Posse", + "details.kick": "Chutar", + "details.kick_confirm": "Você tem certeza de que deseja remover este membro do grupo?", + "details.add-member": "Adicionar Membro", + "details.owner_options": "Administração do Grupo", + "details.group_name": "Nome do Grupo", + "details.member_count": "Número de Membros", + "details.creation_date": "Data de Criação", + "details.description": "Descrição", + "details.member-post-cids": "IDs de categoria das quais exibir postagens", + "details.badge_preview": "Visualização do Distintivo", + "details.change_icon": "Mudar Ícone", + "details.change_label_colour": "Alterar Cor do Rótulo", + "details.change_text_colour": "Alterar Cor do Texto", + "details.badge_text": "Texto da Badge", + "details.userTitleEnabled": "Mostrar Badge", + "details.private_help": "Se habilitado, a entrada nos grupos requer aprovação de um dos donos do grupo", + "details.hidden": "Oculto", + "details.hidden_help": "Se habilitado, este grupo não se encontrará na listagem de grupos e os usuários terão de ser convivados manualmente", + "details.delete_group": "Deletar Grupo", + "details.private_system_help": "Grupos particulares estão desabilitados em escala de sistema, esta opção não é válida", + "event.updated": "Os detalhes do grupo foram atualizados", + "event.deleted": "O grupo \"%1\" foi deletado", + "membership.accept-invitation": "Aceitar Convite", + "membership.accept.notification_title": "Você agora é um membro de %1", + "membership.invitation-pending": "Convite Pendente", + "membership.join-group": "Entrar no Grupo", + "membership.leave-group": "Deixar Grupo", + "membership.leave.notification_title": "%1 saiu do grupo %2", + "membership.reject": "Rejeitar", + "new-group.group_name": "Nome do Grupo:", + "upload-group-cover": "Fazer upload de capa do grupo", + "bulk-invite-instructions": "Digite uma lista, separada por vírgulas, de nomes usuários para convidar para este grupo", + "bulk-invite": "Convite em Massa", + "remove_group_cover_confirm": "Tem certeza de que deseja remover a imagem de capa?" +} \ No newline at end of file diff --git a/public/language/pt-BR/ip-blacklist.json b/public/language/pt-BR/ip-blacklist.json new file mode 100644 index 0000000000..2971b69fa8 --- /dev/null +++ b/public/language/pt-BR/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure a sua lista negra de IPs aqui.", + "description": "Ocasionalmente, um banimento de conta de usuário não é suficientemente impeditivo. Outras vezes, restringir acesso ao fórum para um IP específico ou uma faixa de endereços IPs é o melhor jeito de proteger um fórum. Nestes cenários, você pode adicionar endereços IPs problemáticos ou blocos CIDR inteiros a esta lista negra, e eles serão impedidos de logar ou registrar uma nova conta.", + "active-rules": "Regras Ativas", + "validate": "Validar Lista Negra", + "apply": "Aplicar Lista Negra", + "hints": "Dicas de Sintaxe", + "hint-1": "Defina um único endereço IP por linha. Você pode adicionar blocos de IP contanto que eles sigam o formato CIDR (por ex. 192.168.100.0/22).", + "hint-2": "Você pode adicionar comentários começando linhas com o símbolo #.", + + "validate.x-valid": "%1 de %2 regras(s) validá(s).", + "validate.x-invalid": "As seguintes %1 regras são inválidas:", + + "alerts.applied-success": "Lista Negra Aplicada", + + "analytics.blacklist-hourly": "Figura 1 – Acessos na lista de bloqueio por hora", + "analytics.blacklist-daily": "Figura 2 – Acessos na lista de bloqueio por dia", + "ip-banned": "IP banido" +} \ No newline at end of file diff --git a/public/language/pt-BR/language.json b/public/language/pt-BR/language.json new file mode 100644 index 0000000000..a506508cc6 --- /dev/null +++ b/public/language/pt-BR/language.json @@ -0,0 +1,5 @@ +{ + "name": "Português (Brasil)", + "code": "pt-BR", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/pt-BR/login.json b/public/language/pt-BR/login.json new file mode 100644 index 0000000000..d4ad3be6d0 --- /dev/null +++ b/public/language/pt-BR/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Nome de usuário / Email", + "username": "Nome de usuário", + "remember_me": "Lembrar de Mim?", + "forgot_password": "Esqueceu a Senha?", + "alternative_logins": "Logins Alternativos", + "failed_login_attempt": "Falha no Login", + "login_successful": "Você logou com sucesso!", + "dont_have_account": "Não tem uma conta?", + "logged-out-due-to-inactivity": "Você saiu do Painel de Controle de Administração devido à inatividade", + "caps-lock-enabled": "Caps Lock está ligada" +} \ No newline at end of file diff --git a/public/language/pt-BR/modules.json b/public/language/pt-BR/modules.json new file mode 100644 index 0000000000..b90d2a6f92 --- /dev/null +++ b/public/language/pt-BR/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Conversar com", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "Você está vendo mensagens mais antigas, clique aqui para ir para a mensagem mais recente.", + "chat.send": "Enviar", + "chat.no_active": "Você não tem chats ativos.", + "chat.user_typing": "%1 está digitando ...", + "chat.user_has_messaged_you": "%1 te enviou uma mensagem.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Por favor, escolha um destinatário para visualizar o histórico de conversas", + "chat.no-users-in-room": "Nenhum usuário nesta sala", + "chat.recent-chats": "Conversas Recentes", + "chat.contacts": "Contatos", + "chat.message-history": "Histórico de Mensagens", + "chat.message-deleted": "Mensagem Excluída", + "chat.options": "Opções da conversa", + "chat.pop-out": "Pop-out o chat", + "chat.minimize": "Minimizar", + "chat.maximize": "Maximizar", + "chat.seven_days": "7 Dias", + "chat.thirty_days": "30 Dias", + "chat.three_months": "3 Meses", + "chat.delete_message_confirm": "Tem certeza que deseja excluir esta mensagem?", + "chat.retrieving-users": "Carregando usuários", + "chat.manage-room": "Administrar Salas de Conversa", + "chat.add-user-help": "Pesquise usuários aqui. Quando selecionado, o usuário será adicionado ao chat. O novo usuário não poderá ver as mensagens de chat que foram enviadas antes de ele ser adicionado à conversa. Somente os donos de salas () podem remover usuários de salas de conversa.", + "chat.confirm-chat-with-dnd-user": "Este usuário definiu seu estado como DnD(Do not disturb - Não perturbe). Você ainda assim quer conversar com ele?", + "chat.rename-room": "Renomear sala", + "chat.rename-placeholder": "Digite o nome da sala aqui", + "chat.rename-help": "O nome informado será visto por todos os participantes desta sala", + "chat.leave": "Sair da conversa", + "chat.leave-prompt": "Tem certeza que deseja sair da conversa?", + "chat.leave-help": "Ao sair desta conversa você não receberá mais informações à respeito desta. Se você for adicionado novamente no futuro, você não poderá visualizar o histórico da conversa antes de sua re-entrada.", + "chat.in-room": "Nesta sala", + "chat.kick": "Expulsar", + "chat.show-ip": "Mostrar IP", + "chat.owner": "Dono da Sala", + "chat.system.user-join": "%1 entrou na sala", + "chat.system.user-leave": "%1 saiu da sala", + "chat.system.room-rename": "%2 renomeou esta sala: %1", + "composer.compose": "Compor", + "composer.show_preview": "Exibir Pré-visualização", + "composer.hide_preview": "Esconder Pré-visualização", + "composer.user_said_in": "%1 disse em %2:", + "composer.user_said": "%1 disse:", + "composer.discard": "Tem certeza que deseja descartar essa postagem?", + "composer.submit_and_lock": "Enviar e Trancar", + "composer.toggle_dropdown": "Alternar Dropdown", + "composer.uploading": "Enviando %1", + "composer.formatting.bold": "Negrito", + "composer.formatting.italic": "Itálico", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Riscado", + "composer.formatting.code": "Código", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Fazer upload de Imagem", + "composer.upload-file": "Fazer upload de Arquivo", + "composer.zen_mode": "Modo Zen", + "composer.select_category": "Escolha uma categoria", + "composer.textarea.placeholder": "Insira o conteúdo da sua postagem aqui, arraste e solte as imagens", + "composer.schedule-for": "Agendar tópico para", + "composer.schedule-date": "Data", + "composer.schedule-time": "Hora", + "composer.cancel-scheduling": "Cancelar Agendamento", + "composer.set-schedule-date": "Definir Data", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancelar", + "bootbox.confirm": "Confirmar", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Posicionamento da Foto de Capa", + "cover.dragging_message": "Arraste a foto de capa na posição desejada e clique em \"Salvar\"", + "cover.saved": "Imagem de foto da capa e posição foram gravadas", + "thumbs.modal.title": "Gerenciar miniaturas do tópico", + "thumbs.modal.no-thumbs": "Nenhuma miniatura encontrada.", + "thumbs.modal.resize-note": "Nota: Este fórum está configurado para redimensionar as miniaturas dos tópicos para uma largura máxima de %1px", + "thumbs.modal.add": "Adicionar miniatura", + "thumbs.modal.remove": "Remover miniatura", + "thumbs.modal.confirm-remove": "Você tem certeza que você quer remover esta miniatura?" +} \ No newline at end of file diff --git a/public/language/pt-BR/notifications.json b/public/language/pt-BR/notifications.json new file mode 100644 index 0000000000..7c6c5e7989 --- /dev/null +++ b/public/language/pt-BR/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notificações", + "no_notifs": "Você não tem nenhuma notificação nova", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Voltar para %1", + "outgoing_link": "Link Externo", + "outgoing_link_message": "Você está saindo de %1", + "continue_to": "Continuar para %1", + "return_to": "Voltar para %1", + "new_notification": "Você tem uma nova notificação", + "you_have_unread_notifications": "Você possui notificações não lidas.", + "all": "Tudo", + "topics": "Tópicos", + "replies": "Respostas", + "chat": "Conversas", + "group-chat": "Group Chats", + "follows": "Seguindo", + "upvote": "Votos positivos", + "new-flags": "Novas Sinalizações", + "my-flags": "Sinalizações designadas a mim", + "bans": "Banimentos", + "new_message_from": "Nova mensagem de %1", + "upvoted_your_post_in": "%1 deu voto positivo para seu post em %2.", + "upvoted_your_post_in_dual": "%1 e %2 deram voto positivo ao seu post em %3.", + "upvoted_your_post_in_multiple": "%1 e %2 outros deram voto positivo ao seu post em %3.", + "moved_your_post": "%1 moveu seu post para %2", + "moved_your_topic": "%1 se mudou %2", + "user_flagged_post_in": "%1 sinalizou um post em %2", + "user_flagged_post_in_dual": "%1 e %2 sinalizaram um post em %3", + "user_flagged_post_in_multiple": "%1 e %2 outros sinalizaram um post em %3", + "user_flagged_user": "%1 sinalizou um perfil de usuário (%2)", + "user_flagged_user_dual": "%1 e %2 sinalizaram um perfil de usuário (%3)", + "user_flagged_user_multiple": "%1 e %2 outros sinalizaram um perfil de usuário (%3)", + "user_posted_to": "%1 postou uma resposta para: %2", + "user_posted_to_dual": "%1 e %2 postaram respostas para: %3", + "user_posted_to_multiple": "%1 e %2 outros postaram respostas para: %3", + "user_posted_topic": "%1 postou um novo tópico: %2", + "user_edited_post": "%1 editou um post em %2", + "user_started_following_you": "%1 começou a seguir você.", + "user_started_following_you_dual": "%1 e %2 começaram a lhe acompanhar.", + "user_started_following_you_multiple": "%1 e %2 outros começaram a lhe acompanhar.", + "new_register": "%1 lhe enviou um pedido de cadastro.", + "new_register_multiple": "Há %1 pedidos de registro aguardando revisão.", + "flag_assigned_to_you": "A Sinalização %1
foi atribuída a você", + "post_awaiting_review": "Post aguardando revisão", + "profile-exported": "%1 perfil exportado, clique para fazer download", + "posts-exported": "%1 posts exportados, clique para fazer download", + "uploads-exported": "%1 uploads exportados, clique para fazer download", + "users-csv-exported": "Usuários csv exportados, clique para fazer o download", + "post-queue-accepted": "Sua postagem na fila foi aceita. Clique aqui para ver sua postagem.", + "post-queue-rejected": "Sua postagem na fila foi rejeitada.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email Confirmado", + "email-confirmed-message": "Obrigado por validar o seu email. Agora sua conta está plenamente ativada.", + "email-confirm-error-message": "Houve um problema ao validar o seu endereço de email. Talvez o código era invalido ou tenha expirado.", + "email-confirm-sent": "Email de confirmação enviado.", + "none": "Nenhum", + "notification_only": "Apenas Notificações", + "email_only": "Apenas E-mail", + "notification_and_email": "Notificações e E-mail", + "notificationType_upvote": "Quando alguém dá um voto positivo em seu post", + "notificationType_new-topic": "Quando alguém que você segue posta um tópico", + "notificationType_new-reply": "Quando uma nova resposta é postada em um tópico que você está acompanhando", + "notificationType_post-edit": "Quando uma postagem é editada em um tópico que você está assistindo", + "notificationType_follow": "Quando alguém começar a seguir você", + "notificationType_new-chat": "Quando você receber uma mensagem de chat", + "notificationType_new-group-chat": "Quando você recebe uma mensagem de chat em grupo", + "notificationType_group-invite": "Quando você receber um convite para um grupo", + "notificationType_group-leave": "Quando um usuário sai do seu grupo", + "notificationType_group-request-membership": "Quando alguém pede para participar de um grupo que você é dono", + "notificationType_new-register": "Quando alguém for adicionado à fila de registro", + "notificationType_post-queue": "Quando um novo post entrar na fila", + "notificationType_new-post-flag": "Quando um post for marcado", + "notificationType_new-user-flag": "Quando um usuário for marcado" +} \ No newline at end of file diff --git a/public/language/pt-BR/pages.json b/public/language/pt-BR/pages.json new file mode 100644 index 0000000000..d31f713f16 --- /dev/null +++ b/public/language/pt-BR/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Home", + "unread": "Tópicos Não Lidos", + "popular-day": "Tópicos populares de hoje", + "popular-week": "Tópicos populares esta semana", + "popular-month": "Tópicos populares deste mês", + "popular-alltime": "Tópicos populares de todos os tempos", + "recent": "Tópicos Recentes", + "top-day": "Tópicos mais votados de hoje", + "top-week": "Tópicos mais votados nesta semana", + "top-month": "Tópicos mais votados neste mês", + "top-alltime": "Tópicos mais votados", + "moderator-tools": "Ferramentas de Moderação", + "flagged-content": "Conteúdo Sinalizado", + "ip-blacklist": "Lista negra de IPs", + "post-queue": "Fila de Posts", + "users/online": "Usuários Online", + "users/latest": "Últimos Usuários", + "users/sort-posts": "Usuários com mais posts", + "users/sort-reputation": "Usuários com maior reputação", + "users/banned": "Usuários Banidos", + "users/most-flags": "Usuários mais sinalizados", + "users/search": "Pesquisa de Usuários", + "notifications": "Notificações", + "tags": "Tags", + "tag": "Tópicos com a tag "%1"", + "register": "Registrar uma conta", + "registration-complete": "Registro completo", + "login": "Entrar na sua conta", + "reset": "Redefinir a senha da sua conta", + "categories": "Categorias", + "groups": "Grupos", + "group": "%1 grupo", + "chats": "Chats", + "chat": "Conversando com %1", + "flags": "Sinalizações", + "flag-details": "Detalhes da Sinalização %1", + "account/edit": "Editando \"%1\"", + "account/edit/password": "Editando senha de \"%1\"", + "account/edit/username": "Editando nome de usuário de \"%1\"", + "account/edit/email": "Editando email de \"%1\"", + "account/info": "Informação da Conta", + "account/following": "Pessoas que %1 segue", + "account/followers": "Pessoas que seguem %1", + "account/posts": "Posts feitos por %1", + "account/latest-posts": "Última postagem realizada por %1", + "account/topics": "Tópicos criados por %1", + "account/groups": "Grupos de %1", + "account/watched_categories": "Categorias Acompanhadas por %1", + "account/bookmarks": "Posts Favoritos de %1's", + "account/settings": "Configurações de Usuário", + "account/watched": "Tópicos assistidos por %1", + "account/ignored": "Tópicos ignorados por %1", + "account/upvoted": "Posts votados positivamente por %1", + "account/downvoted": "Posts votados negativamente por %1", + "account/best": "Melhores posts de %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Usuários bloqueados para %1", + "account/uploads": "Uploads feitos por %1", + "account/sessions": "Sessões de Login", + "confirm": "E-mail Confirmado", + "maintenance.text": "%1 está atualmente sob manutenção. Por favor retorne em outro momento.", + "maintenance.messageIntro": "Adicionalmente, o administrador deixou esta mensagem:", + "throttled.text": "%1 está atualmente indisponível devido a excesso de contingente. Por favor retorne em outro momento." +} \ No newline at end of file diff --git a/public/language/pt-BR/post-queue.json b/public/language/pt-BR/post-queue.json new file mode 100644 index 0000000000..d3b7c4e810 --- /dev/null +++ b/public/language/pt-BR/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Fila de Posts", + "description": "Não há posts na fila de posts.
Para habilitar essa função, vá até Configurações → Post → Fila de Posts e habilite Fila de Posts.", + "user": "Usuário", + "category": "Categoria", + "title": "Título", + "content": "Conteúdo", + "posted": "Postado", + "reply-to": "Resposta para \"%1\"", + "content-editable": "Clique no conteúdo para editar", + "category-editable": "Clique na categoria para editar", + "title-editable": "Clique no título para editar", + "reply": "Responder", + "topic": "Tópico", + "accept": "Aceitar", + "reject": "Rejeitar", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/pt-BR/recent.json b/public/language/pt-BR/recent.json new file mode 100644 index 0000000000..322ab84c1f --- /dev/null +++ b/public/language/pt-BR/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recente", + "day": "Dia", + "week": "Semana", + "month": "Mês", + "year": "Ano", + "alltime": "Todos os Tempos", + "no_recent_topics": "Não há tópicos recentes.", + "no_popular_topics": "Não há tópicos populares.", + "there-is-a-new-topic": "Há um novo tópico.", + "there-is-a-new-topic-and-a-new-post": "Há um novo tópico e um novo post.", + "there-is-a-new-topic-and-new-posts": "Há um novo tópico e %1 novos posts.", + "there-are-new-topics": "Há %1 novos tópicos.", + "there-are-new-topics-and-a-new-post": "Há %1 novos tópicos e um novo post.", + "there-are-new-topics-and-new-posts": "Há %1 novos tópicos e %2 novos posts.", + "there-is-a-new-post": "Há um novo post.", + "there-are-new-posts": "Há %1 novos posts.", + "click-here-to-reload": "Clique aqui para recarregar." +} \ No newline at end of file diff --git a/public/language/pt-BR/register.json b/public/language/pt-BR/register.json new file mode 100644 index 0000000000..052ea03f97 --- /dev/null +++ b/public/language/pt-BR/register.json @@ -0,0 +1,32 @@ +{ + "register": "Cadastrar", + "cancel_registration": "Cancelar Cadastro", + "help.email": "Por padrão, seu e-mail ficará oculto ao público.", + "help.username_restrictions": "Um nome de usuário único entre %1 e %2 caracteres. Os outros poderão te mencionar digitando @usuário.", + "help.minimum_password_length": "Sua senha tem que ter no mínimo %1 caracteres.", + "email_address": "Endereço de Email", + "email_address_placeholder": "Digite seu Email", + "username": "Nome de Usuário", + "username_placeholder": "Digite seu Nome de Usuário", + "password": "Senha", + "password_placeholder": "Digite sua Senha", + "confirm_password": "Confirmar Senha", + "confirm_password_placeholder": "Confirmar Senha", + "register_now_button": "Registrar Agora", + "alternative_registration": "Cadastro Alternativo", + "terms_of_use": "Termos de Uso", + "agree_to_terms_of_use": "Eu concordo com os Termos de Uso", + "terms_of_use_error": "Você deve concordar com os Termos de Uso", + "registration-added-to-queue": "O seu cadastro foi adicionado à fila de aprovação. Você receberá um email quando ele for aceito por um administrador.", + "registration-queue-average-time": "Nosso tempo médio para aprovação de associações é de %1 horas e %2 minutos.", + "registration-queue-auto-approve-time": "Sua associação a este fórum será totalmente ativada em até %1 horas.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Eu concordo com a coleta e o processamento de minhas informações pessoais neste site.", + "gdpr_agree_email": "Eu concordo em receber e-mails de resumo e notificação deste site.", + "gdpr_consent_denied": "Você deve autorizar não só que este site colete e processe suas informações, como também de permitir que este envie e-mails para você.", + "invite.error-admin-only": "O registro direto do usuário foi desativado. Entre em contato com um administrador para obter mais detalhes.", + "invite.error-invite-only": "O registro direto do usuário foi desativado. Você deve ser convidado por um usuário existente para acessar este fórum.", + "invite.error-invalid-data": "Os dados cadastrais recebidos não correspondem aos nossos registros. Entre em contato com um administrador para obter mais detalhes" +} \ No newline at end of file diff --git a/public/language/pt-BR/reset_password.json b/public/language/pt-BR/reset_password.json new file mode 100644 index 0000000000..a455018695 --- /dev/null +++ b/public/language/pt-BR/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Redefinir Senha", + "update_password": "Alterar Senha", + "password_changed.title": "Senha Alterada", + "password_changed.message": "

Senha redefinida com sucesso, por favor efetue login novamente.", + "wrong_reset_code.title": "Código de Reconfiguração incorreto", + "wrong_reset_code.message": "O código de reconfiguração recebido estava incorreto. Por favor tente novamente, ou peça um novo código de reconfiguração.", + "new_password": "Nova Senha", + "repeat_password": "Confirmar Senha", + "changing_password": "Mudando Senha", + "enter_email": "Por favor digite seu endereço de email e nós iremos lhe enviar em email com instruções de como reconfigurar a sua conta.", + "enter_email_address": "Digite seu Email", + "password_reset_sent": "Se o endereço especificado corresponder a uma conta de usuário existente, um e-mail de redefinição de senha será enviado. Observe que apenas um e-mail será enviado por minuto.", + "invalid_email": "Email Inválido / Email não existe!", + "password_too_short": "A senha entrada é muito curta, por favor escolha uma senha diferente.", + "passwords_do_not_match": "As duas senhas que você digitou não combinam.", + "password_expired": "A sua senha expirou, por favor escolha uma nova senha" +} \ No newline at end of file diff --git a/public/language/pt-BR/search.json b/public/language/pt-BR/search.json new file mode 100644 index 0000000000..7e3682d528 --- /dev/null +++ b/public/language/pt-BR/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 resultado(s) contendo \"%2\", (%3 segundos)", + "no-matches": "Nenhum resultado encontrado", + "advanced-search": "Pesquisa Avançada", + "in": "Em", + "titles": "Títulos", + "titles-posts": "Títulos e Posts", + "match-words": "Palavras correspondentes", + "all": "Todos", + "any": "Qualquer", + "posted-by": "Postado por", + "in-categories": "Nas Categorias", + "search-child-categories": "Pesquisar subcategorias", + "has-tags": "Com as tags", + "reply-count": "Contagem de Respostas", + "at-least": "No mínimo", + "at-most": "No máximo", + "relevance": "Relevância", + "post-time": "Data da postagem", + "votes": "Votos", + "newer-than": "Mais novo que", + "older-than": "Mais antigo que", + "any-date": "Qualquer data", + "yesterday": "Ontem", + "one-week": "Uma semana", + "two-weeks": "Duas semanas", + "one-month": "Um mês", + "three-months": "Três meses", + "six-months": "Seis meses", + "one-year": "Um ano", + "sort-by": "Ordenar por", + "last-reply-time": "Data da última resposta", + "topic-title": "Título do tópico", + "topic-votes": "Votos do Tópico", + "number-of-replies": "Número de respostas", + "number-of-views": "Número de visualizações", + "topic-start-date": "Data do início do tópico", + "username": "Nome de usuário", + "category": "Categoria", + "descending": "Em ordem descendente", + "ascending": "Em ordem ascendente", + "save-preferences": "Salvar preferências", + "clear-preferences": "Limpar preferências", + "search-preferences-saved": "Preferências de pesquisa salvas", + "search-preferences-cleared": "Preferências de pesquisa limpas", + "show-results-as": "Mostrar resultados como", + "see-more-results": "Veja mais resultados (%1)", + "search-in-category": "Pesquisar em '%1'" +} \ No newline at end of file diff --git a/public/language/pt-BR/success.json b/public/language/pt-BR/success.json new file mode 100644 index 0000000000..b9c3e17124 --- /dev/null +++ b/public/language/pt-BR/success.json @@ -0,0 +1,7 @@ +{ + "success": "Sucesso", + "topic-post": "Você postou com sucesso.", + "post-queued": "Sua postagem está na fila para aprovação. Você receberá uma notificação quando for aceito ou rejeitado.", + "authentication-successful": "Autenticação Bem-sucedida", + "settings-saved": "Configurações salvas!" +} \ No newline at end of file diff --git a/public/language/pt-BR/tags.json b/public/language/pt-BR/tags.json new file mode 100644 index 0000000000..36ce0d46df --- /dev/null +++ b/public/language/pt-BR/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Não há tópicos com esta tag.", + "tags": "Tags", + "enter_tags_here": "Digite as tags aqui, entre %1 e %2 caracteres cada.", + "enter_tags_here_short": "Digite tags...", + "no_tags": "Ainda não há tags.", + "select_tags": "Selecionar Tags" +} \ No newline at end of file diff --git a/public/language/pt-BR/top.json b/public/language/pt-BR/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/pt-BR/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/pt-BR/topic.json b/public/language/pt-BR/topic.json new file mode 100644 index 0000000000..b180a14b35 --- /dev/null +++ b/public/language/pt-BR/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tópico", + "title": "Título", + "no_topics_found": "Nenhum tópico encontrado!", + "no_posts_found": "Nenhum post encontrado!", + "post_is_deleted": "Este post está deletado!", + "topic_is_deleted": "Este tópico foi deletado!", + "profile": "Perfil", + "posted_by": "Postado por %1", + "posted_by_guest": "Postado por Visitante", + "chat": "Chat", + "notify_me": "Seja notificado de novas respostas nesse tópico", + "quote": "Citar", + "reply": "Responder", + "replies_to_this_post": "%1 Respostas", + "one_reply_to_this_post": "1 Resposta", + "last_reply_time": "Última resposta", + "reply-as-topic": "Responder como tópico", + "guest-login-reply": "Entre para responder", + "login-to-view": "🔒 Entre para ver", + "edit": "Editar", + "delete": "Deletar", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Expurgar", + "restore": "Restaurar", + "move": "Mover", + "change-owner": "Trocar proprietário", + "fork": "Clonar", + "link": "Link", + "share": "Compartilhar", + "tools": "Ferramentas", + "locked": "Trancado", + "pinned": "Fixado", + "pinned-with-expiry": "Fixado até %1", + "scheduled": "Agendado", + "moved": "Movido", + "moved-from": "Movido de %1", + "copy-ip": "Copiar IP", + "ban-ip": "Banir IP", + "view-history": "Histórico de Edição", + "locked-by": "Bloqueado por", + "unlocked-by": "Desbloqueado por", + "pinned-by": "Fixado por", + "unpinned-by": "Desafixado por", + "deleted-by": "Deletado por", + "restored-by": "Recuperado por", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post aguardando aprovação →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Clique aqui para retornar ao último post lido neste tópico.", + "flag-post": "Marque este post", + "flag-user": "Marque este usuário", + "already-flagged": "Já marcado", + "view-flag-report": "Ver Relatório da Sinalização", + "resolve-flag": "Resolver marcação", + "merged_message": "Este tópico foi fundido com %2", + "deleted_message": "Este tópico foi deletado. Apenas usuários com privilégios de moderação de tópico podem vê-lo.", + "following_topic.message": "Agora você receberá notificações quando alguém responder este tópico.", + "not_following_topic.message": "Você verá este tópico na lista de tópicos não-lidos, mas você não receberá notificações quando alguém postar no tópico.", + "ignoring_topic.message": "Você não verá mais este tópico na lista de tópicos não lidos. Você será notificado quando você for mencionado ou sua postagem for votada positivamente.", + "login_to_subscribe": "Por favor se cadastre ou entre para assinar à este tópico.", + "markAsUnreadForAll.success": "Tópico marcado como não lido para todos.", + "mark_unread": "Marcar como não lido", + "mark_unread.success": "Tópico marcado como não lido.", + "watch": "Acompanhar", + "unwatch": "Desacompanhar", + "watch.title": "Seja notificado sobre novas respostas neste tópico", + "unwatch.title": "Parar de acompanhar este tópico", + "share_this_post": "Compartilhar este Post", + "watching": "Acompanhar", + "not-watching": "Não Acompanhar", + "ignoring": "Ignorando", + "watching.description": "Me notificar de novas respostas.
Mostrar tópico em não-lidos.", + "not-watching.description": "Não me notificar de novas respostas.
Mostrar tópico em não-lido se a categoria não estiver sendo ignorada.", + "ignoring.description": "Não me notificar de novas respostas.
Não mostrar tópico em não-lido.", + "thread_tools.title": "Ferramentas de Tópico", + "thread_tools.markAsUnreadForAll": "Marcar como não lido para todos", + "thread_tools.pin": "Fixar Tópico", + "thread_tools.unpin": "Desfixar Tópico", + "thread_tools.lock": "Trancar Tópico", + "thread_tools.unlock": "Destrancar Tópico", + "thread_tools.move": "Mover Tópico", + "thread_tools.move-posts": "Mover Posts", + "thread_tools.move_all": "Mover Tudo", + "thread_tools.change_owner": "Trocar proprietário", + "thread_tools.select_category": "Escolha a Categoria", + "thread_tools.fork": "Ramificar Tópico", + "thread_tools.delete": "Deletar Tópico", + "thread_tools.delete-posts": "Deletar Posts", + "thread_tools.delete_confirm": "Tem certeza que deseja excluir este tópico?", + "thread_tools.restore": "Restaurar Tópico", + "thread_tools.restore_confirm": "Tem certeza que deseja restaurar este tópico?", + "thread_tools.purge": "Expurgar Tópico", + "thread_tools.purge_confirm": "Tem certeza que deseja expurgar este tópico? ", + "thread_tools.merge_topics": "Mesclar Tópicos", + "thread_tools.merge": "Mesclar", + "topic_move_success": "Este tópico será movido para '% 1' em breve. Clique aqui para desfazer.", + "topic_move_multiple_success": "Esses tópicos serão movidos para '%1' em breve. Clique aqui para desfazer.", + "topic_move_all_success": "Todos os tópicos serão movidos para '%1' em breve. Clique aqui para desfazer.", + "topic_move_undone": "Movimento de tópico desfeito", + "topic_move_posts_success": "As postagens serão movidas em breve. Clique aqui para desfazer.", + "topic_move_posts_undone": "Movimentação de post desfeita", + "post_delete_confirm": "Tem certeza que deseja deletar este post?", + "post_restore_confirm": "Tem certeza que deseja restaurar este post?", + "post_purge_confirm": "Tem certeza que deseja expurgar este post?", + "pin-modal-expiry": "Data de expiração", + "pin-modal-help": "Você pode, opcionalmente, definir uma data de validade para o(s) tópico(s) fixado(s) aqui. Como alternativa, você pode deixar este campo em branco para que o tópico permaneça fixado até que seja liberado manualmente.", + "load_categories": "Carregando Categorias", + "confirm_move": "Mover", + "confirm_fork": "Ramificar", + "bookmark": "Favorito", + "bookmarks": "Favoritos", + "bookmarks.has_no_bookmarks": "Você ainda não adicionou quaisquer posts aos favoritos.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Carregando Mais Posts", + "move_topic": "Mover Tópico", + "move_topics": "Mover Tópicos", + "move_post": "Mover Post", + "post_moved": "Post movido!", + "fork_topic": "Ramificar Tópico", + "enter-new-topic-title": "Insira o novo título do tópico", + "fork_topic_instruction": "Clique nos posts que você quer ramificar", + "fork_no_pids": "Nenhum post selecionado!", + "no-posts-selected": "Nenhum post selecionado!", + "x-posts-selected": "%1 post(s) selecionados", + "x-posts-will-be-moved-to-y": "%1 post(s) serão movidos para \"%2\"", + "fork_pid_count": "%1 post(s) selecionado(s)", + "fork_success": "Tópico ramificado com sucesso! Clique aqui para ir ao tópico ramificado.", + "delete_posts_instruction": "Clique nos posts que você deseja deletar/limpar", + "merge_topics_instruction": "Clique nos tópicos que deseja mesclar ou pesquise por eles", + "merge-topic-list-title": "Lista de tópicos para mesclar", + "merge-options": "Mesclar opções", + "merge-select-main-topic": "Escolher o tópico principal", + "merge-new-title-for-topic": "Novo título para o tópico", + "topic-id": "ID do Tópico", + "move_posts_instruction": "Clique nas postagens que deseja mover e insira um ID de tópico ou vá para o tópico de destino", + "change_owner_instruction": "Clique na postagem que você quer associar a outro usuário", + "composer.title_placeholder": "Digite aqui o título para o seu tópico...", + "composer.handle_placeholder": "Digite seu nome/usuário aqui", + "composer.discard": "Descartar", + "composer.submit": "Enviar", + "composer.additional-options": "Additional Options", + "composer.schedule": "Agendar", + "composer.replying_to": "Respondendo para %1", + "composer.new_topic": "Novo Tópico", + "composer.editing": "Editando", + "composer.uploading": "enviando...", + "composer.thumb_url_label": "Cole o endereço de um thumbnail para o tópico", + "composer.thumb_title": "Adicionar um thumbnail para este tópico", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Ou envie um arquivo", + "composer.thumb_remove": "Limpar campos", + "composer.drag_and_drop_images": "Clique e Arraste Imagens Para Cá", + "more_users_and_guests": "%1 mais usuário(s) e %2 visitante(s)", + "more_users": "%1 mais usuário(s)", + "more_guests": "%1 mais visitante(s)", + "users_and_others": "%1 e %2 outros", + "sort_by": "Ordenar por", + "oldest_to_newest": "Mais Antigo para Mais Recente", + "newest_to_oldest": "Mais Recente para Mais Antigo", + "most_votes": "Mais Votados", + "most_posts": "Mais Postagens", + "most_views": "Most Views", + "stale.title": "Criar um novo tópico ao invés disso?", + "stale.warning": "O tópico que você está respondendo é bem antigo. Você gostaria de criar um novo tópico ao invés disso, e referenciá-lo em sua resposta?", + "stale.create": "Criar um novo tópico", + "stale.reply_anyway": "Responder à este tópico assim mesmo", + "link_back": "Re: [%1](%2)", + "diffs.title": "Histórico de Edição do Post", + "diffs.description": "Este post foi revisado %1 vezes. Clique em uma das revisões abaixo para ver o conteúdo da postagem naquele momento.", + "diffs.no-revisions-description": "Este post foi revisado %1 vezes.", + "diffs.current-revision": "revisão atual", + "diffs.original-revision": "revisão original", + "diffs.restore": "Restaurar esta revisão", + "diffs.restore-description": "Uma nova revisão será anexada ao histórico de edição desta postagem após a restauração.", + "diffs.post-restored": "Postagem restaurada com sucesso para a revisão anterior", + "diffs.delete": "Excluir esta revisão", + "diffs.deleted": "Revisão excluída", + "timeago_later": "%1 depois", + "timeago_earlier": "%1 mais cedo", + "first-post": "Primeiro post", + "last-post": "Último post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/pt-BR/unread.json b/public/language/pt-BR/unread.json new file mode 100644 index 0000000000..a0a998c319 --- /dev/null +++ b/public/language/pt-BR/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Não Lido", + "no_unread_topics": "Não há tópicos não lidos.", + "load_more": "Carregar Mais", + "mark_as_read": "Marcar como Lido", + "selected": "Selecionado", + "all": "Todos", + "all_categories": "Todas as categorias", + "topics_marked_as_read.success": "Tópicos marcados como lidos!", + "all-topics": "Todos os Tópicos", + "new-topics": "Novos Tópicos", + "watched-topics": "Topicos Acompanhados", + "unreplied-topics": "Tópicos Sem Resposta", + "multiple-categories-selected": "Vários Selecionados" +} \ No newline at end of file diff --git a/public/language/pt-BR/uploads.json b/public/language/pt-BR/uploads.json new file mode 100644 index 0000000000..ee71e0fa11 --- /dev/null +++ b/public/language/pt-BR/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Fazendo upload do arquivo...", + "select-file-to-upload": "Escolha um arquivo para fazer upload!", + "upload-success": "Upload realizado com sucesso!", + "maximum-file-size": "No máximo %1 kb", + "no-uploads-found": "Uploads não encontrados", + "public-uploads-info": "Uploads públicos, todos os visitantes poderão vê-los.", + "private-uploads-info": "Uploads privados, somente usuários logados poderão vê-los." +} \ No newline at end of file diff --git a/public/language/pt-BR/user.json b/public/language/pt-BR/user.json new file mode 100644 index 0000000000..e326d02341 --- /dev/null +++ b/public/language/pt-BR/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banido", + "muted": "Muted", + "offline": "Offline", + "deleted": "Deletado", + "username": "Nome de Usuário", + "joindate": "Data de Entrada", + "postcount": "Número de Posts", + "email": "Email", + "confirm_email": "Confirmar Email", + "account_info": "Informações da Conta", + "admin_actions_label": "Ações Administrativas", + "ban_account": "Banir Conta", + "ban_account_confirm": "Você realmente quer banir esse usuario?", + "unban_account": "Desbanir Conta", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Deletar Conta", + "delete_account_as_admin": "Deletar Conta", + "delete_content": "Excluir Conteúdo da Conta", + "delete_all": "Deletar Conta e Conteúdo", + "delete_account_confirm": "Você tem certeza que você quer anonimizar os seus posts e excluir a sua conta?
Esta ação é irreversível e você não poderá recuperar quaisquer dos seus dados

Entre com a sua senha para confirmar que você quer destruir esta conta.", + "delete_this_account_confirm": "Tem certeza de que deseja excluir esta conta, deixando seu conteúdo para trás?
Esta ação é irreversível, as postagens serão feitas anônimas e você não poderá restaurar associações de postagens com a conta excluída

", + "delete_account_content_confirm": "Tem certeza de que deseja excluir o conteúdo desta conta (postagens/tópicos/uploads)?
Esta ação é irreversível e você não poderá recuperar quaisquer dados

", + "delete_all_confirm": "Tem certeza de que deseja excluir esta conta e todo o seu conteúdo (postagens/tópicos/uploads)?
Esta ação é irreversível e você não será capaz de recuperar nenhum dado

", + "account-deleted": "Conta excluída", + "account-content-deleted": "Conteúdo da conta excluído", + "fullname": "Nome Completo", + "website": "Website", + "location": "Local", + "age": "Idade", + "joined": "Cadastrou", + "lastonline": "Última vez Online", + "profile": "Perfil", + "profile_views": "Visualizações de perfil", + "reputation": "Reputação", + "bookmarks": "Favoritos", + "watched_categories": "Categorias acompanhadas", + "change_all": "Mudar Tudo", + "watched": "Acompanhado", + "ignored": "Ignorado", + "default-category-watch-state": "Configuração padrão em relação a acompanhar as novidades das categorias", + "followers": "Seguidores", + "following": "Seguindo", + "blocks": "Bloqueados", + "block_toggle": "Alternar Bloqueio", + "block_user": "Bloquear Usuário", + "unblock_user": "Desbloquear Usuário", + "aboutme": "Sobre mim", + "signature": "Assinatura", + "birthday": "Aniversário", + "chat": "Chat", + "chat_with": "Continuar a conversa com %1", + "new_chat_with": "Iniciar uma nova conversa com %1", + "flag-profile": "Perfil da Sinalização", + "follow": "Seguir", + "unfollow": "Deixar de Seguir", + "more": "Mais", + "profile_update_success": "O Perfil foi atualizado com sucesso!", + "change_picture": "Alterar Foto", + "change_username": "Mudar nome de usuário", + "change_email": "Mudar email", + "email_same_as_password": "Por favor, digite a sua senha atual para continuar – você digitou o seu novo e-mail novamente", + "edit": "Editar", + "edit-profile": "Editar Perfil", + "default_picture": "Ícone Padrão", + "uploaded_picture": "Foto Carregada", + "upload_new_picture": "Carregar Nova Foto", + "upload_new_picture_from_url": "Enviar Nova Foto Por URL", + "current_password": "Senha Atual", + "change_password": "Alterar Senha", + "change_password_error": "Senha Inválida!", + "change_password_error_wrong_current": "Sua senha atual está incorreta!", + "change_password_error_match": "As senhas devem conferir!", + "change_password_error_privileges": "Você não possui permissões para alterar esta senha.", + "change_password_success": "Sua senha foi alterada!", + "confirm_password": "Confirmar Senha", + "password": "Senha", + "username_taken_workaround": "O nome de usuário que você escolheu já existia, então nós o alteramos um pouquinho. Agora você é conhecido como %1", + "password_same_as_username": "A sua senha é igual ao seu nome de usuário, por favor escolha outra senha.", + "password_same_as_email": "A sua senha é igual ao seu e-mail. Por favor, escolha outra senha.", + "weak_password": "Senha fraca.", + "upload_picture": "Carregar Foto", + "upload_a_picture": "Carregue uma Foto", + "remove_uploaded_picture": "Remover Foto Enviada", + "upload_cover_picture": "Fazer upload de imagem de capa ", + "remove_cover_picture_confirm": "Tem certeza que deseja remover a imagem de capa?", + "crop_picture": "Cortar imagem", + "upload_cropped_picture": "Cortar e enviar", + "avatar-background-colour": "Cor de fundo do avatar", + "settings": "Configurações", + "show_email": "Mostrar Meu Email", + "show_fullname": "Mostrar Meu Nome Completo", + "restrict_chats": "Permitir mensagens de chat apenas para usuários que eu sigo", + "digest_label": "Assinar ao Resumo", + "digest_description": "Assinar para receber atualizações por email deste fórum (novas notificações e tópicos) de acordo com um calendário definido", + "digest_off": "Desativado", + "digest_daily": "Diariamente", + "digest_weekly": "Semanalmente", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Mensalmente", + "has_no_follower": "Este usuário não possui seguidores :(", + "follows_no_one": "Este usuário não está seguindo ninguém :(", + "has_no_posts": "Esse usuário ainda não postou nada.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Esse usuário ainda não postou quaisquer tópicos.", + "has_no_watched_topics": "Esse usuário ainda não acompanhou quaisquer tópicos.", + "has_no_ignored_topics": "O usuário ainda não ignorou nenhum tópico.", + "has_no_upvoted_posts": "Este usuário ainda não votou positivamente em quaisquer posts.", + "has_no_downvoted_posts": "Este usuário ainda não votou negativamente em quaisquer posts.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Você não bloqueou nenhum usuário.", + "email_hidden": "E-mail Oculto", + "hidden": "oculto", + "paginate_description": "Paginar tópicos ao invés de utilizar em vez de usar rolagem infinita.", + "topics_per_page": "Tópicos por Página", + "posts_per_page": "Posts por Página", + "max_items_per_page": "No máximo %1", + "acp_language": "Idioma da Página de Administrador", + "notifications": "Notificações", + "upvote-notif-freq": "Frequência de Notificação de Votos Positivos", + "upvote-notif-freq.all": "Todos os Votos Positivos", + "upvote-notif-freq.first": "Primeiro Por Post", + "upvote-notif-freq.everyTen": "A Cada 10 Votos Positivos", + "upvote-notif-freq.threshold": "A cada 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "A cada 10, 100, 1000...", + "upvote-notif-freq.disabled": "Desativado", + "browsing": "Configurações de Navegação", + "open_links_in_new_tab": "Abrir links externos em nova aba", + "enable_topic_searching": "Habilitar Pesquisa dentro de Tópico", + "topic_search_help": "Se habilitado, a pesquisa dentro do tópico irá substituir a pesquisa padrão do seu navegador. Assim, você poderá pesquisar pelo tópico inteiro, e não apenas pelo o que está sendo exibido na tela.", + "update_url_with_post_index": "Atualizar url com índice de postagem enquanto navega pelos tópicos", + "scroll_to_my_post": "Ao responder um tópico, role a página até a minha postagem", + "follow_topics_you_reply_to": "Acompanhar os tópicos que você responde", + "follow_topics_you_create": "Acompanhar os tópicos que você cria", + "grouptitle": "Título do Grupo", + "group-order-help": "Selecione um grupo e use as setas para ordenar os títulos", + "no-group-title": "Sem título de grupo", + "select-skin": "Escolha uma Skin", + "select-homepage": "Selecione uma página inicial", + "homepage": "Página inicial", + "homepage_description": "Selecione uma página para usar como página inicial do fórum ou 'Nenhum' para usar a página inicial padrão.", + "custom_route": "Rota da página inicial personalizada", + "custom_route_help": "Insira um nome de rota aqui, sem nenhuma barra no final (por exemplo, 'recente' ou 'categoria/2/discussao-geral')", + "sso.title": "Logar por outros Serviços", + "sso.associated": "Associado com", + "sso.not-associated": "Clique aqui para associar com", + "sso.dissociate": "Desassociar", + "sso.dissociate-confirm-title": "Confirmar Desassociação", + "sso.dissociate-confirm": "Tem certeza de que deseja desassociar a sua conta de %1?", + "info.latest-flags": "Últimas Sinalizações", + "info.no-flags": "Nenhum Post Sinalizado Encontrado", + "info.ban-history": "Histórico de Banimentos Recentes", + "info.no-ban-history": "Este usuário nunca foi banido", + "info.banned-until": "Banido até %1", + "info.banned-expiry": "Validade", + "info.banned-permanently": "Banido permanentemente", + "info.banned-reason-label": "Motivo", + "info.banned-no-reason": "Sem motivo escolhido.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Histórico do Nome de Usuário", + "info.email-history": "Histórico do Email", + "info.moderation-note": "Nota da Moderação", + "info.moderation-note.success": "Nota da moderação salva", + "info.moderation-note.add": "Adicionar nota", + "sessions.description": "Esta página permite que você veja quaisquer sessões ativas neste fórum e as revogue se necessário. Você pode revogar a sua sessão atual ao desconectar-se.", + "consent.title": "Seus direitos & Consentimento", + "consent.lead": "Este fórum da comunidade coleta e processa suas informações pessoais.", + "consent.intro": "Nós usamos estas informações estritamente para personalizar a sua experiência nesta comunidade, assim como vincular as postagens que você faz à sua conta de usuário. Durante o processo de registro, foi solicitado apenas um usuário e um endereço de e-mail. No entanto, se desejar, você também pode fornecer informações adicionais para completar o seu perfil de usuário.

Enquanto sua conta de usuário existir, nós guardaremos estas informações, porém, você tem a possibilidade de retirar este consentimento a qualquer momento e, para isso, basta excluir a conta. A qualquer momento, você pode requisitar uma cópia da sua contribuição para este site, através da sua página de Direitos & Consentimento.

Se você tem alguma dúvida ou preocupação, nós o aconselhamos a entrar em contato com a equipe administrativa deste fórum.", + "consent.email_intro": "Ocasionalmente, nós poderemos mandar e-mails para o e-mail usado no registro para fornecer atualizações e/ou para notificá-lo sobre novas atividades que são importantes para você. Você tanto pode customizar a frequência destes resumos (inclusive desativá-los imediatamente), bem como selecionar quais tipos de notificações que você deseja receber por e-mail, através da página de configurações de usuário.", + "consent.digest_frequency": "A menos que seja explicitamente alterada nas configurações do usuário, essa comunidade fornece resumos por e-mail a cada %1.", + "consent.digest_off": "A menos que seja explicitamente alterada nas configurações do usuário, essa comunidade não envia resumos por e-mail", + "consent.received": "Você autorizou este site a coletar e a processar os seus dados. Nenhuma ação adicional é necessária.", + "consent.not_received": "Você não autorizou a coleta e o processamento dos seus dados. A qualquer momento, a administração deste site pode optar por excluir sua conta para se tornar compatível com o Regulamento Geral sobre a Proteção de Dados.", + "consent.give": "Dar o consentimento", + "consent.right_of_access": "Você tem o Direito de Acessar", + "consent.right_of_access_description": "Você tem o direito de acessar todos os dados coletados por este site mediante solicitação. Você pode recuperar uma cópia desses dados clicando no botão apropriado abaixo.", + "consent.right_to_rectification": "Você tem o Direito de Retificar", + "consent.right_to_rectification_description": "Você tem o direito de alterar ou atualizar quaisquer dados imprecisos fornecidos a nós. Seu perfil pode ser atualizado editando seu perfil e postar conteúdo sempre pode ser editado. Se esse não for o caso, entre em contato com a equipe administrativa do site.", + "consent.right_to_erasure": "Você tem o Direito de Apagar", + "consent.right_to_erasure_description": "A qualquer momento, você pode revogar o consentimento à coleta e/ou ao processamento de dados excluindo sua conta. Seu perfil individual pode ser excluído, no entanto, as suas postagens serão mantidas. Se você desejar deletar tanto a sua conta e as suas postagens, por favor, entre em contato com a equipe administrativa deste site.", + "consent.right_to_data_portability": "Você tem o Direito de Portabilidade de Dados", + "consent.right_to_data_portability_description": "Você pode solicitar de nós uma exportação legível por máquina de quaisquer dados coletados sobre você e sua conta. Você pode fazer isso clicando no botão apropriado abaixo.", + "consent.export_profile": "Exportar Perfil (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Exportar Arquivos Enviados (.zip)", + "consent.export-uploads-success": "Exportando uploads, você receberá uma notificação quando estiver concluído.", + "consent.export_posts": "Exportar Posts (.csv)", + "consent.export-posts-success": "Exportando postagens, você receberá uma notificação quando estiver concluído.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/pt-BR/users.json b/public/language/pt-BR/users.json new file mode 100644 index 0000000000..e13553d39a --- /dev/null +++ b/public/language/pt-BR/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Últimos Usuários", + "top_posters": "Principais Participantes", + "most_reputation": "Maior Reputação", + "most_flags": "Mais Sinalizações", + "search": "Pesquisar", + "enter_username": "Digite um nome de usuário para pesquisar", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Carregar Mais", + "users-found-search-took": "%1 usuário(s) encontrado(s)! A pesquisa levou %2 segundos.", + "filter-by": "Filtrar Por", + "online-only": "Apenas Online", + "invite": "Convidar", + "prompt-email": "E-mails:", + "groups-to-join": "Grupos a serem inscritos quando o convite é aceito:", + "invitation-email-sent": "Um email de convite foi enviado para %1", + "user_list": "Lista de Usuários", + "recent_topics": "Tópicos Recentes", + "popular_topics": "Tópicos Populares", + "unread_topics": "Topicos Não-Lidos", + "categories": "Categorias", + "tags": "Tags", + "no-users-found": "Nenhum usuário encontrado!" +} \ No newline at end of file diff --git a/public/language/pt-PT/_DO_NOT_EDIT_FILES_HERE.md b/public/language/pt-PT/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/pt-PT/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/pt-PT/admin/admin.json b/public/language/pt-PT/admin/admin.json new file mode 100644 index 0000000000..c471056aa4 --- /dev/null +++ b/public/language/pt-PT/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Tens a certeza que queres reconstruir e reiniciar o NodeBB?", + "alert.confirm-restart": "Tens a certeza que pretendes reiniciar NodeBB?", + + "acp-title": "%1 | Painel de Administração NodeBB", + "settings-header-contents": "Conteúdo", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/advanced/cache.json b/public/language/pt-PT/admin/advanced/cache.json new file mode 100644 index 0000000000..86d9da7df3 --- /dev/null +++ b/public/language/pt-PT/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Cache de Publicações", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Cheio", + "post-cache-size": "Tamanho da Cache de Publicações", + "items-in-cache": "Itens em Cache" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/advanced/database.json b/public/language/pt-PT/admin/advanced/database.json new file mode 100644 index 0000000000..b31d3253ea --- /dev/null +++ b/public/language/pt-PT/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Tempo de Atividade em Segundos", + "uptime-days": "Tempo de Atividade em Dias", + + "mongo": "Mongo", + "mongo.version": "Versão MongoDB", + "mongo.storage-engine": "Mecanismo de Armazenamento", + "mongo.collections": "Coleções", + "mongo.objects": "Objetos", + "mongo.avg-object-size": "Tamanho Médio do Objeto", + "mongo.data-size": "Tamanho dos Dados", + "mongo.storage-size": "Tamanho do Armazenamento", + "mongo.index-size": "Tamanho do Índice", + "mongo.file-size": "Tamanho do Ficheiro", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Memória Virtual", + "mongo.mapped-memory": "Memória Mapeada", + "mongo.bytes-in": "Bytes Recebidos", + "mongo.bytes-out": "Bytes Enviados", + "mongo.num-requests": "Número de Pedidos", + "mongo.raw-info": "Informações Não Processadas do MongoDB", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Versão do Redis", + "redis.keys": "Chaves", + "redis.expires": "Expira em", + "redis.avg-ttl": "Tempo Médio de TTL", + "redis.connected-clients": "Clientes Conectados", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Clientes Bloqueados", + "redis.used-memory": "Memória Usada", + "redis.memory-frag-ratio": "Proporção da Fragmentação da Memória", + "redis.total-connections-recieved": "Total de Conexões Recebidas", + "redis.total-commands-processed": "Total de Comandos Processados", + "redis.iops": "Operações Instantâneas por Segundo", + "redis.iinput": "Entradas Instantâneas por Segundo", + "redis.ioutput": "Saídas Instantâneas por Segundo", + "redis.total-input": "Total Recebido", + "redis.total-output": "Total Enviado", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Informações Não Processadas do Redis", + + "postgres": "Postgres", + "postgres.version": "Versão do PostgreSQL", + "postgres.raw-info": "Informações Não Processadas do Postgres" +} diff --git a/public/language/pt-PT/admin/advanced/errors.json b/public/language/pt-PT/admin/advanced/errors.json new file mode 100644 index 0000000000..c17d2f0ac5 --- /dev/null +++ b/public/language/pt-PT/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figura %1", + "error-events-per-day": "%1 eventos por dia", + "error.404": "404 Não Encontrado", + "error.503": "503 Serviço Indisponível ", + "manage-error-log": "Gerir Registo de Erros", + "export-error-log": "Exportar Registo de Erros (CSV)", + "clear-error-log": "Limpar Registo de Erros", + "route": "Caminho", + "count": "Contagem", + "no-routes-not-found": "Boa! Não existem erros 404!", + "clear404-confirm": "Tens a certeza que pretendes limpar o registo de erros 404?", + "clear404-success": "Erros \"404 Não Encontrado\" limpos" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/advanced/events.json b/public/language/pt-PT/admin/advanced/events.json new file mode 100644 index 0000000000..3063f1dbc2 --- /dev/null +++ b/public/language/pt-PT/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Eventos", + "no-events": "Não existem eventos", + "control-panel": "Painel de Controlo de Eventos", + "delete-events": "Apagar Eventos", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filtros", + "filters-apply": "Aplicar Filtros", + "filter-type": "Tipo de Evento", + "filter-start": "Data de Início", + "filter-end": "Data de Fim", + "filter-perPage": "Por Página" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/advanced/logs.json b/public/language/pt-PT/admin/advanced/logs.json new file mode 100644 index 0000000000..5910befada --- /dev/null +++ b/public/language/pt-PT/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Registos", + "control-panel": "Painel de Controlo de Registos", + "reload": "Recarregar Registos", + "clear": "Limpar Registos", + "clear-success": "Registos Limpos!" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/appearance/customise.json b/public/language/pt-PT/admin/appearance/customise.json new file mode 100644 index 0000000000..620f9ef9ef --- /dev/null +++ b/public/language/pt-PT/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "CSS/LESS Personalizado", + "custom-css.description": "Insere aqui as tuas próprias declarações de CSS/LESS, que serão aplicadas depois de todos os outros estilos.", + "custom-css.enable": "Ativar CSS/LESS Personalizado", + + "custom-js": "Javascript Personalizado", + "custom-js.description": "Insere aqui o teu código Javascript personalizado. Ele será executado logo após a página ser carregada completamente.", + "custom-js.enable": "Ativar Javascript Personalizado", + + "custom-header": "Cabeçalho Personalizado", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Ativar Cabeçalho Personalizado", + + "custom-css.livereload": "Ativar recarregar ao vivo", + "custom-css.livereload.description": "Ativa isto para forçar todas as sessões da tua conta a serem atualizadas em todos os dispositivos sempre que clicares em guardar" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/appearance/skins.json b/public/language/pt-PT/admin/appearance/skins.json new file mode 100644 index 0000000000..8388502a59 --- /dev/null +++ b/public/language/pt-PT/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "A Carregar Máscaras...", + "homepage": "Página principal", + "select-skin": "Escolha uma Máscara", + "current-skin": "Máscara Atual", + "skin-updated": "Máscara Atualizada", + "applied-success": "Máscara %1 aplicada com sucesso", + "revert-success": "Máscara revertida para as cores base" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/appearance/themes.json b/public/language/pt-PT/admin/appearance/themes.json new file mode 100644 index 0000000000..b824283243 --- /dev/null +++ b/public/language/pt-PT/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "A procurar por temas instalados...", + "homepage": "Página principal", + "select-theme": "Selecionar Tema", + "current-theme": "Tema Atual", + "no-themes": "Não foram encontrados temas instalados", + "revert-confirm": "Tens a certeza que desejas restaurar o tema predefinido do NodeBB?", + "theme-changed": "Tema Alterado", + "revert-success": "Tu reverteste com sucesso o teu NodeBB de volta ao seu tema padrão.", + "restart-to-activate": "Por favor reconstrói e reinicia o teu NodeBB para aplicar totalmente este tema." +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/dashboard.json b/public/language/pt-PT/admin/dashboard.json new file mode 100644 index 0000000000..9f590b49c2 --- /dev/null +++ b/public/language/pt-PT/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Tráfego do Fórum", + "page-views": "Visualizações de páginas", + "unique-visitors": "Visitantes únicos", + "logins": "Logins", + "new-users": "Novos Utilizadores", + "posts": "Publicações", + "topics": "Tópicos", + "page-views-seven": "Últimos 7 Dias", + "page-views-thirty": "Últimos 30 Dias", + "page-views-last-day": "Últimas 24 horas", + "page-views-custom": "Intervalo Personalizado", + "page-views-custom-start": "Início do Intervalo", + "page-views-custom-end": "Fim do Intervalo", + "page-views-custom-help": "Insere um intervalo entre datas de visualizações de página que gostarias de visualizar. Se o selecionador de datas não estiver disponível, o formato aceitável é AAAA-MM-DD", + "page-views-custom-error": "Por favor, insere um intervalo entre datas no formato AAAA-MM-DD", + + "stats.yesterday": "Ontem", + "stats.today": "Hoje", + "stats.last-week": "Última Semana", + "stats.this-week": "Esta Semana", + "stats.last-month": "Último Mês", + "stats.this-month": "Este Mês", + "stats.all": "Desde sempre", + + "updates": "Atualizações", + "running-version": "Estás a executar NodeBB v%1.", + "keep-updated": "Cetifica-te que o teu NodeBB está sempre atualizado para teres as mais recentes correções de segurança e correções de bugs.", + "up-to-date": "

O teu NodeBB está atualizado

", + "upgrade-available": "

Uma nova versão (v%1) foi lançada. Considera atualizar o teu NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Ocorreu uma falha a obter a versão mais recente disponível para o NodeBB

", + + "notices": "Avisos", + "restart-not-required": "Não é necessário reiniciar", + "restart-required": "É necessário reiniciar", + "search-plugin-installed": "Plugin de pesquisa instalado", + "search-plugin-not-installed": "Plugin de pesquisa não instalado", + "search-plugin-tooltip": "Instala um plugin de pesquisa a partir da página de Plugins para conseguires ativar a funcionalidade de pesquisa", + + "control-panel": "Controlo do Sistema", + "rebuild-and-restart": "Reconstruir e Reiniciar", + "restart": "Reiniciar", + "restart-warning": "Reconstruir ou Reiniciar o teu NodeBB irá terminar todas as conexões existentes por alguns segundos.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Modo de Manutenção", + "maintenance-mode-title": "Clica aqui para configurar o modo de manutenção para o teu NodeBB", + "realtime-chart-updates": "Actualizar Gráfico em Tempo Real", + + "active-users": "Utilizadores Ativos", + "active-users.users": "Utilizadores", + "active-users.guests": "Convidados", + "active-users.total": "Total", + "active-users.connections": "Conexões", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registados", + + "user-presence": "Presença dos Utilizadores", + "on-categories": "Na lista de categorias", + "reading-posts": "A ler publicações", + "browsing-topics": "A procurar tópicos", + "recent": "Recente", + "unread": "Não lidos", + + "high-presence-topics": " Alta Presença em Tópicos", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Visualizações de páginas", + "graphs.page-views-registered": "Visualizações de páginas por utilizadores registados", + "graphs.page-views-guest": "Visualizações de páginas por convidados", + "graphs.page-views-bot": "Visualizações de páginas por bots", + "graphs.unique-visitors": "Visitantes únicos", + "graphs.registered-users": "Utilizadores Registados", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Última vez reiniciado por", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/pt-PT/admin/development/info.json b/public/language/pt-PT/admin/development/info.json new file mode 100644 index 0000000000..e88ef6e50d --- /dev/null +++ b/public/language/pt-PT/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Tu estás em %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nós responderam dentro de %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "carga do sistema", + "cpu-usage": "uso cpu", + "uptime": "tempo de atividade", + + "registered": "Registados", + "sockets": "Sockets", + "guests": "Convidados", + + "info": "Informação" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/development/logger.json b/public/language/pt-PT/admin/development/logger.json new file mode 100644 index 0000000000..a03cabca38 --- /dev/null +++ b/public/language/pt-PT/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Caminho para o Arquivo de Registos", + "file-path-placeholder": "/caminho/para/ficheiro/registo.log ::: deixa em branco para registar no teu terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/extend/plugins.json b/public/language/pt-PT/admin/extend/plugins.json new file mode 100644 index 0000000000..96b938e856 --- /dev/null +++ b/public/language/pt-PT/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Instalados", + "active": "Ativos", + "inactive": "Inativo", + "out-of-date": "Desatualizados", + "none-found": "Não foram encontrados plugins.", + "none-active": "Sem Plugins Ativos", + "find-plugins": "Procurar Plugins", + + "plugin-search": "Procura de Plugins", + "plugin-search-placeholder": "Procurar um plugin...", + "submit-anonymous-usage": "Enviar dados de uso dos plugins de forma anónima.", + "reorder-plugins": "Reordenar Plugins", + "order-active": "Ordenar Plugins Ativos", + "dev-interested": "Interessado em escrever plugins para o NodeBB?", + "docs-info": "A documentação completa sobre criação de plugins pode ser encontrada na Documentação do NodeBB.", + + "order.description": "Certos plugins funcionam melhor quando são inicializados antes/depois de outros plugins.", + "order.explanation": "Aqui os plugins carregam numa ordem específica, desde o topo até ao fundo", + + "plugin-item.themes": "Temas", + "plugin-item.deactivate": "Desativar", + "plugin-item.activate": "Ativar", + "plugin-item.install": "Instalar", + "plugin-item.uninstall": "Desinstalar", + "plugin-item.settings": "Definições", + "plugin-item.installed": "Versão Instalada", + "plugin-item.latest": "Última Versão", + "plugin-item.upgrade": "Atualizar", + "plugin-item.more-info": "Para mais informações:", + "plugin-item.unknown": "Desconhecido", + "plugin-item.unknown-explanation": "Não foi possível determinar o estado deste plugin, possivelmente devido a um erro de configuração incorreta.", + "plugin-item.compatible": "Este plugin funciona no NodeBB %1", + "plugin-item.not-compatible": "Este plugin não possui dados de compatibilidade, verifica se ele funciona antes de o instalares no teu ambiente de produção.", + + "alert.enabled": "Plugin Ativado", + "alert.disabled": "Plugin Desativado", + "alert.upgraded": "Plugin Atualizado", + "alert.installed": "Plugin Instalado", + "alert.uninstalled": "Plugin Desinstalado", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin desativado com sucesso", + "alert.upgrade-success": "Por favor reconstrói e reinicia o teu NodeBB para atualizar totalmente este plugin.", + "alert.install-success": "Plugin instalado com sucesso, por favor ativa o plugin.", + "alert.uninstall-success": "Este plugin foi desativado e desinstalado com sucesso.", + "alert.suggest-error": "

O NodeBB não conseguiu aceder ao gestor de pacotes, queres prosseguir com a instalação da versão mais recente?

O servidor respondeu (%1): %2
", + "alert.package-manager-unreachable": "

O NodeBB não conseguiu aceder ao gestor de pacotes, uma atualização não é aconselhável de momento.

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins reordenados", + "alert.reorder-success": "Por favor reconstrói e reinicia o teu NodeBB para completar totalmente o processo.", + + "license.title": "Informação sobre a licença do plugin", + "license.intro": "O plug-in %1 está licenciado sob %2. Por favor leia e compreenda os termos da licença antes de ativar este plugin.", + "license.cta": "Tens a certeza que queres ativar este plugin?" +} diff --git a/public/language/pt-PT/admin/extend/rewards.json b/public/language/pt-PT/admin/extend/rewards.json new file mode 100644 index 0000000000..346eb49e2a --- /dev/null +++ b/public/language/pt-PT/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Recompensas", + "condition-if-users": "Se", + "condition-is": "É:", + "condition-then": "Então:", + "max-claims": "Número de vezes que a recompensa pode ser atribuída", + "zero-infinite": "Digite 0 para infinito", + "delete": "Apagar", + "enable": "Ativar", + "disable": "Desativar", + + "alert.delete-success": "Recompensa apagada com sucesso", + "alert.no-inputs-found": "Recompensa ilegal - não foram encontradas entradas!", + "alert.save-success": "Recompensas guardadas com sucesso" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/extend/widgets.json b/public/language/pt-PT/admin/extend/widgets.json new file mode 100644 index 0000000000..b7a32cb195 --- /dev/null +++ b/public/language/pt-PT/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Widgets Disponíveis", + "explanation": "Seleciona um widget no menu suspenso e, em seguida, arrasta-o e solta-o para uma das área de widgets à esquerda.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clonar widgets de", + "containers.available": "Containers Disponíveis", + "containers.explanation": "Arrasta e solta em cima de qualquer widget ativo", + "containers.none": "Nada", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Tens a certeza que desejas eliminar este widget?", + "alert.updated": "Widgets Atualizados", + "alert.update-success": "Widgets atualizados com sucesso", + "alert.clone-success": "Widgets clonados com sucesso", + + "error.select-clone": "Por favor, seleciona uma página para clonar de", + + "title": "Título", + "title.placeholder": "Título (mostrado apenas em alguns containers)", + "container": "Container", + "container.placeholder": "Arrasta e solta um container ou insere HTML aqui.", + "show-to-groups": "Mostrar para os grupos", + "hide-from-groups": "Ocultar dos grupos", + "hide-on-mobile": "Ocultar em telemóvel" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/admins-mods.json b/public/language/pt-PT/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f1cbcb3a4b --- /dev/null +++ b/public/language/pt-PT/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administradores", + "global-moderators": "Moderadores Globais", + "moderators": "Moderators", + "no-global-moderators": "Não existem Moderadores Globais", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Não existem Moderadores", + "add-administrator": "Adicionar Administrador", + "add-global-moderator": "Adicionar Moderador Global", + "add-moderator": "Adicionar Moderador" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/categories.json b/public/language/pt-PT/admin/manage/categories.json new file mode 100644 index 0000000000..d029ce59b9 --- /dev/null +++ b/public/language/pt-PT/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Definições da Categoria", + "privileges": "Privilégios", + + "name": "Nome da Categoria", + "description": "Descrição da Categoria", + "bg-color": "Cor de Fundo", + "text-color": "Cor do Texto", + "bg-image-size": "Tamanho da Imagem de Fundo", + "custom-class": "Classe personalizada", + "num-recent-replies": "# de Respostas Recentes", + "ext-link": "Link Externo", + "subcategories-per-page": "Subcategories per page", + "is-section": "Tratar esta categoria como uma secção", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Enviar Imagem", + "delete-image": "Remover", + "category-image": "Imagem da Categoria", + "parent-category": "Categoria Pai", + "optional-parent-category": "(Opcional) Categoria Pai", + "top-level": "Top Level", + "parent-category-none": "(Nenhuma)", + "copy-parent": "Copiar Pai", + "copy-settings": "Copiar Definições de ", + "optional-clone-settings": "(Opcional) Clonar Definições da Categoria", + "clone-children": "Copiar as categorias-filho e definições", + "purge": "Eliminar Categoria", + + "enable": "Ativar", + "disable": "Desativar", + "edit": "Editar", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Selecionar Categoria", + "set-parent-category": "Definir uma Categoria Pai", + + "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.category-selector": "A configurar privilégios para", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Privilégios de Visualização", + "privileges.section-posting": "Privilégios de Publicação", + "privileges.section-moderation": "Privilégios de Moderação", + "privileges.section-other": "Outra", + "privileges.section-user": "Utilizador", + "privileges.search-user": "Adicionar Utilizador", + "privileges.no-users": "Não existem privilégios específicos para utilizadores nesta categoria.", + "privileges.section-group": "Grupo", + "privileges.group-private": "Este grupo é privado", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Adicionar Grupo", + "privileges.copy-to-children": "Copiar para Filho", + "privileges.copy-from-category": "Copiar da Categoria", + "privileges.copy-privileges-to-all-categories": "Copiar para Todas as Categorias", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privilégios copiados!", + + "analytics.back": "Voltar à Lista de Categorias", + "analytics.title": "Estatísticas para a categoria \"%1\"", + "analytics.pageviews-hourly": "Figura 1 – Visualizações por hora para esta categoria", + "analytics.pageviews-daily": "Figura 2 – Visualizações por dia para esta categoria", + "analytics.topics-daily": "Figura 3 – Tópicos por dia criados nesta categoria", + "analytics.posts-daily": "Figura 4 – Publicações por dia feitas nesta categoria", + + "alert.created": "Criada", + "alert.create-success": "Categoria criada com sucesso!", + "alert.none-active": "Não tens categorias ativas.", + "alert.create": "Criar uma Categoria", + "alert.confirm-purge": "

Tens a certeza que pretendes eliminar definitivamente esta categoria \"%1\"?

\n
Atenção! Todos os tópicos e publicações feitas nesta categoria vão ser eliminados também!

Eliminar uma categoria irá remover todos os tópicos e publicações e eliminar a categoria da base de dados. Se pretendes remover temporariamente uma categoria, em vez disso podes apenas \"desativar\" essa categoria.

", + "alert.purge-success": "Categoria eliminada!", + "alert.copy-success": "Definições Copiadas!", + "alert.set-parent-category": "Definir uma Categoria Pai", + "alert.updated": "Categorias Atualizadas", + "alert.updated-success": "IDs das categorias %1 atualizados com sucesso!", + "alert.upload-image": "Enviar imagem da categoria", + "alert.find-user": "Encontrar um Utilizador", + "alert.user-search": "Procurar por um utilizador aqui...", + "alert.find-group": "Procurar um Grupo", + "alert.group-search": "Procura por um grupo aqui...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Recolher Todas", + "expand-all": "Expandir Todas", + "disable-on-create": "Desativar imediatamente ao criar", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/digest.json b/public/language/pt-PT/admin/manage/digest.json new file mode 100644 index 0000000000..83be67644b --- /dev/null +++ b/public/language/pt-PT/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "Utilizador", + "subscription": "Tipo de Subscrição", + "last-delivery": "Última entrega com sucesso", + "default": "Predefinição do sistema", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Reenviar Resumo", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Reenvio do resumo manual concluído", + "resent-day": "Resumo diário reenviado", + "resent-week": "Resumo semanal reenviado", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Resumo mensal reenviado", + "null": "Nunca", + "manual-run": "Manual digest run:", + + "no-delivery-data": "Nenhum dado de entrega encontrado" +} diff --git a/public/language/pt-PT/admin/manage/groups.json b/public/language/pt-PT/admin/manage/groups.json new file mode 100644 index 0000000000..917757e5bf --- /dev/null +++ b/public/language/pt-PT/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Nome do Grupo", + "badge": "Crachá", + "properties": "Propriedades", + "description": "Descrição do Grupo", + "member-count": "Quantidade de membros", + "system": "Sistema", + "hidden": "Escondido", + "private": "Privado", + "edit": "Editar", + "delete": "Apagar", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Procurar", + "create": "Criar Grupo", + "description-placeholder": "Uma pequena descrição acerca do teu grupo", + "create-button": "Criar", + + "alerts.create-failure": "Ohhh...

Ocorreu um problema a criar o teu grupo. Por favor tenta mais tarde!

", + "alerts.confirm-delete": "Tens a certeza que pretendes apagar este grupo?", + + "edit.name": "Nome", + "edit.description": "Descrição", + "edit.user-title": "Título dos Membros", + "edit.icon": "Ícone do Grupo", + "edit.label-color": "Cor da Etiqueta do Grupo", + "edit.text-color": "Cor do Texto do Grupo", + "edit.show-badge": " Mostrar Crachá", + "edit.private-details": "Se ativada, para aderir ao grupo é necessária aprovação do dono do grupo.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Desativar pedidos de adesão", + "edit.disable-leave": "Proibir os utilizadores de saírem do grupo", + "edit.hidden": "Escondido", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Adicionar um Utilizador ao Grupo", + "edit.add-user-search": "Procurar Utilizadores", + "edit.members": "Lista de Membros", + "control-panel": "Painel de Controlo dos Grupos", + "revert": "Reverter", + + "edit.no-users-found": "Utilizadores Não Encontrados", + "edit.confirm-remove-user": "Tens a certeza que queres remover este utilizador?", + "edit.save-success": "Alterações guardadas!" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/privileges.json b/public/language/pt-PT/admin/manage/privileges.json new file mode 100644 index 0000000000..79b2e087dd --- /dev/null +++ b/public/language/pt-PT/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Administrador", + "group-privileges": "Privilégios de Grupos", + "user-privileges": "Privilégios de Utilizadores", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Conversa", + "upload-images": "Enviar Imagens", + "upload-files": "Enviar Ficheiros", + "signature": "Assinatura", + "ban": "Banir", + "mute": "Mute", + "invite": "Invite", + "search-content": "Procurar Conteúdo", + "search-users": "Procurar Utilizadores", + "search-tags": "Procurar Marcadores", + "view-users": "Ver Utilizadores", + "view-tags": "Ver Etiquetas", + "view-groups": "Ver Grupos", + "allow-local-login": "Início de Sessão Local", + "allow-group-creation": "Criar Grupos", + "view-users-info": "Ver Informação dos Utilizadores", + "find-category": "Encontrar Categoria", + "access-category": "Aceder à Categoria", + "access-topics": "Aceder aos Tópicos", + "create-topics": "Criar Tópicos", + "reply-to-topics": "Responder a Tópicos", + "schedule-topics": "Schedule Topics", + "tag-topics": "Marcar Tópicos", + "edit-posts": "Editar Publicações", + "view-edit-history": "Ver Histórico de Edições", + "delete-posts": "Apagar Publicações", + "view_deleted": "Ver Publicações Eliminadas", + "upvote-posts": "Votar positivamente", + "downvote-posts": "Votar negativamente", + "delete-topics": "Apagar Tópicos", + "purge": "Eliminar", + "moderate": "Moderar", + "admin-dashboard": "Painel de Controlo", + "admin-categories": "Categorias", + "admin-privileges": "Privilégios", + "admin-users": "Utilizadores", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Definições", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/registration.json b/public/language/pt-PT/admin/manage/registration.json new file mode 100644 index 0000000000..361eb16c75 --- /dev/null +++ b/public/language/pt-PT/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Fila de espera", + "description": "Não existem usuários na fila de espera.
Para abilitar esta funcionalidade, vá para Configurações &arr; Usuário &arr; Registro de usuário e configure Tipo de registro como \"Com aprovação de administrador\"", + + "list.name": "Nome", + "list.email": "E-mail", + "list.ip": "IP", + "list.time": "Tempo", + "list.username-spam": "Frequência: %1 Aparência: %2 Confidência: %3", + "list.email-spam": "Frequência: %1 Aparência: %2", + "list.ip-spam": "Frequência: %1 Aparência: %2", + + "invitations": "Convites", + "invitations.description": "Em baixo está uma lista completa de convites enviados. Utiliza CTRL+F para procurar na lista por um e-mail ou um utilizador.

O nome de utilizador será exibido à direita dos e-mails para os utilizadores que aceitaram os seus convites.", + "invitations.inviter-username": "Nome de Utilizador do Convidador", + "invitations.invitee-email": "E-mail do Convidado", + "invitations.invitee-username": "Nome de Utilizador do Convidado (se registado)", + + "invitations.confirm-delete": "Tens a certeza que desejas eliminar este convite?" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/tags.json b/public/language/pt-PT/admin/manage/tags.json new file mode 100644 index 0000000000..a068fe7bd8 --- /dev/null +++ b/public/language/pt-PT/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "O teu fórum ainda não tem nenhum tópico com marcadores.", + "bg-color": "Cor de Fundo", + "text-color": "Cor do Texto", + "description": "Seleciona as etiquetas clicando ou arrastando, usa CTRL para selecionar múltiplas etiquetas.", + "create": "Criar Marcador", + "modify": "Modificar Marcadores", + "rename": "Renomear Marcadores", + "delete": "Apagar Marcadores Selecionados", + "search": "Procurar por marcadores...", + "settings": "Definições das Etiquetas", + "name": "Nome da Etiqueta", + + "alerts.editing": "Editar etiqueta(s)", + "alerts.confirm-delete": "Queres mesmo apagar os marcadores selecionados?", + "alerts.update-success": "Etiqueta Atualizada!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/uploads.json b/public/language/pt-PT/admin/manage/uploads.json new file mode 100644 index 0000000000..86116ed5e4 --- /dev/null +++ b/public/language/pt-PT/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Enviar Ficheiro", + "filename": "Nome do Ficheiro", + "usage": "Uso em Publicações", + "orphaned": "Órfão", + "size/filecount": "Tamanho / Contador de Ficheiros", + "confirm-delete": "Tens a certeza que pretendes apagar este ficheiro?", + "filecount": "%1 ficheiros", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/users.json b/public/language/pt-PT/admin/manage/users.json new file mode 100644 index 0000000000..81f85a3cc4 --- /dev/null +++ b/public/language/pt-PT/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Utilizadores", + "edit": "Actions", + "make-admin": "Tornar Administrador", + "remove-admin": "Remover Administrador", + "validate-email": "Validar E-mail", + "send-validation-email": "Enviar Validação de E-mail", + "password-reset-email": "Enviar E-mail de Reposição de Palavra-passe", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Banir Utilizador(es)", + "temp-ban": "Banir Utilizador(es) Temporariamente", + "unban": "Desbanir Utilizador(es)", + "reset-lockout": "Redefinir Bloqueio", + "reset-flags": "Redefinir Denúncias", + "delete": "Eliminar Utilizador(es)", + "delete-content": "Eliminar Conteúdo do(s) Utilizador(es)", + "purge": "Eliminar Utilizador(es) e os seus Conteúdos", + "download-csv": "Transferir CSV", + "manage-groups": "Gerir Grupos", + "add-group": "Adicionar Grupo", + "create": "Create User", + "invite": "Invite by Email", + "new": "Novo Utilizador", + "filter-by": "Filter by", + "pills.unvalidated": "Não Validados", + "pills.validated": "Validated", + "pills.banned": "Banido", + + "50-per-page": "50 por página", + "100-per-page": "100 por página", + "250-per-page": "250 por página", + "500-per-page": "500 por página", + + "search.uid": "Por ID de Utilizador", + "search.uid-placeholder": "Digita um ID de utilizador para procurar", + "search.username": "Por Nome de Utilizador", + "search.username-placeholder": "Digita um nome de utilizador para procurar", + "search.email": "Por E-mail", + "search.email-placeholder": "Digita um e-mail para procurar", + "search.ip": "Por Endereço IP", + "search.ip-placeholder": "Digita um endereço IP para procurar", + "search.not-found": "Nenhum utilizador encontrado!", + + "inactive.3-months": "3 meses", + "inactive.6-months": "6 meses", + "inactive.12-months": "12 meses", + + "users.uid": "uid", + "users.username": "nome de utilizador", + "users.email": "e-mail", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "publicações", + "users.reputation": "reputação", + "users.flags": "denúncias", + "users.joined": "aderiu", + "users.last-online": "última vez online", + "users.banned": "banido", + + "create.username": "Nome do Utilizador", + "create.email": "E-mail", + "create.email-placeholder": "E-mail deste utilizador", + "create.password": "Palavra-passe", + "create.password-confirm": "Confirmar palavra-passe", + + "temp-ban.length": "Length", + "temp-ban.reason": "Razão (Opcional)", + "temp-ban.hours": "Horas", + "temp-ban.days": "Dias", + "temp-ban.explanation": "Insere o tempo de duração para o banimento. Nota que um tempo de 0 irá ser considerado um banimento permanente.", + + "alerts.confirm-ban": "Tens a certeza que queres banir este utilizador permanentemente?", + "alerts.confirm-ban-multi": "Tens a certeza que queres banir estes utilizadores permanentemente?", + "alerts.ban-success": "Utilizador(es) banido(s)!", + "alerts.button-ban-x": "Banir %1 utilizador(es)", + "alerts.unban-success": "Utilizador(es) desbanido(s)!", + "alerts.lockout-reset-success": "Bloqueio(s) redefinido(s)!", + "alerts.flag-reset-success": "Denúncia(s) redefinida(s)!", + "alerts.no-remove-yourself-admin": "Não podes remover a ti próprio como Administrador!", + "alerts.make-admin-success": "O utilizador é agora um administrador.", + "alerts.confirm-remove-admin": "Tens a certeza que queres remover este administrador?", + "alerts.remove-admin-success": "Este utilizador já não é mais um administrador.", + "alerts.make-global-mod-success": "O utilizador é agora um moderador global.", + "alerts.confirm-remove-global-mod": "Desejas mesmo remover este moderador global?", + "alerts.remove-global-mod-success": "O utilizador não é mais um moderador global.", + "alerts.make-moderator-success": "O utilizador é agora um moderador.", + "alerts.confirm-remove-moderator": "Desejas mesmo remover este moderador?", + "alerts.remove-moderator-success": "O utilizador não é mais um moderador.", + "alerts.confirm-validate-email": "Tens a certeza que queres validar o(s) e-mail(s) deste(s) utilizador(es)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "E-mails validados", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "Utilizador(es) Eliminados!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "Conteúdo do(s) Utilizador(es) Eliminado!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Criar Utilizador", + "alerts.button-create": "Criar", + "alerts.button-cancel": "Cancelar", + "alerts.error-passwords-different": "As palavras-passe têm de coincidir!", + "alerts.error-x": "Erro

%1

", + "alerts.create-success": "Utilizador criado!", + + "alerts.prompt-email": "E-mails:", + "alerts.email-sent-to": "Foi enviado um e-mail de convite para %1", + "alerts.x-users-found": "%1 utilizador(es) encontrado(s), (%2 segundos)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/menu.json b/public/language/pt-PT/admin/menu.json new file mode 100644 index 0000000000..35d3d4af31 --- /dev/null +++ b/public/language/pt-PT/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Geral", + + "section-manage": "Gerir", + "manage/categories": "Categorias", + "manage/privileges": "Privilégios", + "manage/tags": "Marcadores", + "manage/users": "Utilizadores", + "manage/admins-mods": "Administradores e Moderadores", + "manage/registration": "Registos por Aprovar", + "manage/post-queue": "Publicações por Aprovar", + "manage/groups": "Grupos", + "manage/ip-blacklist": "Lista Negra de IPs", + "manage/uploads": "Carregamentos", + "manage/digest": "Resumos", + + "section-settings": "Definições", + "settings/general": "Geral", + "settings/homepage": "Página Inicial", + "settings/navigation": "Navegação", + "settings/reputation": "Reputation & Flags", + "settings/email": "E-mail", + "settings/user": "Utilizadores", + "settings/group": "Grupos", + "settings/guest": "Convidados", + "settings/uploads": "Carregamentos", + "settings/languages": "Idiomas", + "settings/post": "Publicações", + "settings/chat": "Conversas", + "settings/pagination": "Paginação", + "settings/tags": "Marcadores", + "settings/notifications": "Notificações", + "settings/api": "API Access", + "settings/sounds": "Sons", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Avançado", + + "settings.page-title": "%1 Definições", + + "section-appearance": "Aparência", + "appearance/themes": "Temas", + "appearance/skins": "Máscaras", + "appearance/customise": "Conteúdo Personalizado (HTML/JS/CSS)", + + "section-extend": "Extensões", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Recompensas", + + "section-social-auth": "Autenticação Social", + + "section-plugins": "Plugins", + "extend/plugins.install": "Instalar Plugins", + + "section-advanced": "Avançado", + "advanced/database": "Base de Dados", + "advanced/events": "Eventos", + "advanced/hooks": "Hooks", + "advanced/logs": "Eventos", + "advanced/errors": "Erros", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Informação", + + "rebuild-and-restart-forum": "Reconstruir e Reiniciar Fórum", + "restart-forum": "Reiniciar Fórum", + "logout": "Terminar sessão", + "view-forum": "Ver Fórum", + + "search.placeholder": "Search settings", + "search.no-results": "Sem resultados...", + "search.search-forum": "Procurar no fórum por ", + "search.keep-typing": "Digita mais para veres resultados...", + "search.start-typing": "Comece a digitar para ver resultados...", + + "connection-lost": "A conexão a %1 foi perdida, tentando reconectar...", + + "alerts.version": "A executar NodeBB v%1", + "alerts.upgrade": "Atualiza para a v%1" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/advanced.json b/public/language/pt-PT/admin/settings/advanced.json new file mode 100644 index 0000000000..5b01a7fefd --- /dev/null +++ b/public/language/pt-PT/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Modo de Manutenção", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Código de Estado do Modo de Manutenção", + "maintenance-mode.message": "Mensagem de Manutenção", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Personaliza o cabeçalho \"Powered By\" enviado pelo NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Intervalo de Verificação (em milissegundos)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Máximo de Tentativas de Reconexão", + "sockets.default-placeholder": "Predefinição: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/api.json b/public/language/pt-PT/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/pt-PT/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/chat.json b/public/language/pt-PT/admin/settings/chat.json new file mode 100644 index 0000000000..b68f84b072 --- /dev/null +++ b/public/language/pt-PT/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Definições da conversa", + "disable": "Desativar conversas", + "disable-editing": "Desativar edtitar/apagar mensagens das conversas", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Comprimento máximo das mensagens nas conversas", + "max-room-size": "Número máximo de utilizadores nas salas de conversa", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/cookies.json b/public/language/pt-PT/admin/settings/cookies.json new file mode 100644 index 0000000000..18af5122db --- /dev/null +++ b/public/language/pt-PT/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Consentimento da UE", + "consent.enabled": "Ativado", + "consent.message": "Mensagem de notificação", + "consent.acceptance": "Mensagem de aceitação", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Ligação URL da Política", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Definições", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Deixa em branco para a predefinir" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/email.json b/public/language/pt-PT/admin/settings/email.json new file mode 100644 index 0000000000..2c78449689 --- /dev/null +++ b/public/language/pt-PT/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Definições de E-mail", + "address": "Endereço de e-mail", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Selecione um serviço", + "smtp-transport.service-custom": "Serviço Personalizado", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Segurança da ligação", + "smtp-transport.security-encrypted": "Encriptado", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Nada", + "smtp-transport.username": "Nome de utilizador", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Palavra-passe", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Editar Modelo de E-mail", + "template.select": "Escolher Modelo de E-mail", + "template.revert": "Reverter para o Original", + "testing": "Teste de E-mail", + "testing.select": "Escolher Modelo de E-mail", + "testing.send": "Enviar E-mail de Teste", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Resumos por E-mail", + "subscriptions.disable": "Desativar resumos por e-mail", + "subscriptions.hour": "Hora do Resumo", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/pt-PT/admin/settings/general.json b/public/language/pt-PT/admin/settings/general.json new file mode 100644 index 0000000000..f5c13ef595 --- /dev/null +++ b/public/language/pt-PT/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Definições do Site", + "title": "Título do Site", + "title.short": "Título Curto", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "O URL do título do site", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Procurar Título", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Descrição do Site", + "keywords": "Palavras-chave do Site", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Logótipo do Site", + "logo.image": "Imagem", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Enviar", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "O URL do logótipo do site", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Texto Alternativo", + "log.alt-text-placeholder": "Texto alternativo para acessibilidade", + "favicon": "Favicon", + "favicon.upload": "Enviar", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Enviar", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Links Externos", + "outgoing-links.warning-page": "Utilizar a página de aviso para links externos", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domínios para a lista de permissões que ignoram a página de aviso", + "site-colors": "Metadados de Cor do Site", + "theme-color": "Cor do Tema", + "background-color": "Cor de Fundo", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/pt-PT/admin/settings/group.json b/public/language/pt-PT/admin/settings/group.json new file mode 100644 index 0000000000..38826871b3 --- /dev/null +++ b/public/language/pt-PT/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Geral", + "private-groups": "Grupos Privados", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Cuidado! Se esta opção estiver desativada e tu tiveres grupos privados, eles automaticamente vão se tornar públicos.", + "allow-multiple-badges": "Permitir Múltiplos Crachás", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Comprimento Máximo do Nome do Grupo", + "max-title-length": "Comprimento Máximo do Título do Grupo", + "cover-image": "Imagem de Capa do Grupo", + "default-cover": "Imagem de Capa Predefinida", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/guest.json b/public/language/pt-PT/admin/settings/guest.json new file mode 100644 index 0000000000..af3d498e80 --- /dev/null +++ b/public/language/pt-PT/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Permitir nomes para visitantes", + "handles.enabled-help": "Esta opção expôe um novo campo que permite a visitantes escolher um nome para associar a cada publicação que eles criem. Se desabilitada, eles simplesmente se chamarão \"Visitante\" ", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/homepage.json b/public/language/pt-PT/admin/settings/homepage.json new file mode 100644 index 0000000000..e441f4d687 --- /dev/null +++ b/public/language/pt-PT/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Página Principal", + "description": "Escolhe qual página é apresentada quando os utilizadores navegam para o URL raiz do teu fórum.", + "home-page-route": "Caminho da Página Principal", + "custom-route": "Caminho personalizado", + "allow-user-home-pages": "Permitir página principal personalizada para os utilizadores", + "home-page-title": "Título da página inicial (predefinido \"Página inicial\")" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/languages.json b/public/language/pt-PT/admin/settings/languages.json new file mode 100644 index 0000000000..56be63bc12 --- /dev/null +++ b/public/language/pt-PT/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Definições de Idioma", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Idioma Predefinido", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/navigation.json b/public/language/pt-PT/admin/settings/navigation.json new file mode 100644 index 0000000000..ca1e541e23 --- /dev/null +++ b/public/language/pt-PT/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ícone:", + "change-icon": "alterar", + "route": "Caminho:", + "tooltip": "Título:", + "text": "Texto:", + "text-class": "Classe: opcional", + "class": "Classe: opcional", + "id": "ID: opcional", + + "properties": "Propriedades:", + "groups": "Grupos:", + "open-new-window": "Abrir numa nova janela", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Apagar", + "btn.disable": "Desativar", + "btn.enable": "Ativar", + + "available-menu-items": "Itens de menu disponíveis", + "custom-route": "Caminho Personalizado", + "core": "sistema", + "plugin": "plugin" +} diff --git a/public/language/pt-PT/admin/settings/notifications.json b/public/language/pt-PT/admin/settings/notifications.json new file mode 100644 index 0000000000..0ce9737e1f --- /dev/null +++ b/public/language/pt-PT/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notificações", + "welcome-notification": "Notificação de Boas-vindas", + "welcome-notification-link": "Link da Notificação de Boas-vindas", + "welcome-notification-uid": "Notificação de boas-vindas ao utilizador (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/pagination.json b/public/language/pt-PT/admin/settings/pagination.json new file mode 100644 index 0000000000..1d28c8b68d --- /dev/null +++ b/public/language/pt-PT/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Definições de Paginação", + "enable": "Paginar tópicos e publicações em vez de usar scroll infinito.", + "posts": "Post Pagination", + "topics": "Paginação de Tópicos", + "posts-per-page": "Publicações por página", + "max-posts-per-page": "Máximo de publicações por página", + "categories": "Paginação de Categorias", + "topics-per-page": "Tópicos por Página", + "max-topics-per-page": "Máximo de tópicos por página", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/post.json b/public/language/pt-PT/admin/settings/post.json new file mode 100644 index 0000000000..a8a416f651 --- /dev/null +++ b/public/language/pt-PT/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Ordenação das Publicações", + "sorting.post-default": "Ordenação Predefinida das Publicações", + "sorting.oldest-to-newest": "Mais antigo para mais recente", + "sorting.newest-to-oldest": "Mais recente para mais antigo", + "sorting.most-votes": "Mais votos", + "sorting.most-posts": "Mais publicações", + "sorting.topic-default": "Ordenação Predefinida dos Tópicos", + "length": "Comprimento da Publicação", + "post-queue": "Fila de Espera para Publicações", + "restrictions": "Restrições de Publicações", + "restrictions-new": "Restrições para Novos Utilizadores", + "restrictions.post-queue": "Ativar publicações em fila de espera", + "restrictions.post-queue-rep-threshold": "Reputação necessária para ignorar a fila de espera para publicações", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Ativar restrições para novos utilizadores", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Comprimento Mínimo do Título", + "restrictions.max-title-length": "Comprimento Máximo do Título", + "restrictions.min-post-length": "Comprimento Mínimo da Publicação", + "restrictions.max-post-length": "Comprimento Máximo da Publicação", + "restrictions.days-until-stale": "Dias até o tópico ser considerado obsoleto", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Pré-visualização da Publicação", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Definições Recentes", + "recent.max-topics": "Máximo de tópicos em /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Definições da Assinatura", + "signature.disable": "Desativar assinaturas", + "signature.no-links": "Desativar links nas assinaturas", + "signature.no-images": "Desativar imagens nas assinaturas", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Comprimento Máximo da Assinatura", + "composer": "Definições do Editor", + "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.show-help": "Mostrar separador \"Ajuda\"", + "composer.enable-plugin-help": "Permitir aos plugins adicionarem conteúdo ao separador de ajuda", + "composer.custom-help": "Texto de ajuda personalizado", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Ativar histórico de publicações" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/reputation.json b/public/language/pt-PT/admin/settings/reputation.json new file mode 100644 index 0000000000..67cc261e26 --- /dev/null +++ b/public/language/pt-PT/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Definições de Reputação", + "disable": "Desativar Sistema de Reputação", + "disable-down-voting": "Desativar Votos Negativos", + "votes-are-public": "Todos os Votos São Públicos", + "thresholds": "Limites de Atividade", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Reputação mínima para votar negativamente em publicações", + "downvotes-per-day": "Votos negativos por dia (coloca 0 para votos negativos ilimitados)", + "downvotes-per-user-per-day": "Votos negativos por utilizador por dia (coloca 0 para votos negativos ilimitados)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Reputação mínima para denunciar publicações", + "min-rep-website": "Reputação mínima para adicionar \"Website\" ao perfil do utilizador", + "min-rep-aboutme": "Reputação mínima para adicionar \"Sobre mim\" ao perfil do utilizador", + "min-rep-signature": "Reputação mínima para adicionar \"Assinatura\" ao perfil do utilizador", + "min-rep-profile-picture": "Reputação mínima para adicionar \"Fotografia de Perfil\" ao perfil do utilizador", + "min-rep-cover-picture": "Reputação mínima para adicionar \"Fotografia de Capa\" ao perfil do utilizador", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/social.json b/public/language/pt-PT/admin/settings/social.json new file mode 100644 index 0000000000..e856606a7b --- /dev/null +++ b/public/language/pt-PT/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Partilhar Publicações", + "info-plugins-additional": "Os plugins podem adicionar outras redes sociais para partilhar publicações.", + "save-success": "Definições de partilhas de publicações em redes sociais guardadas com sucesso!" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/sockets.json b/public/language/pt-PT/admin/settings/sockets.json new file mode 100644 index 0000000000..b8b9a2bd0d --- /dev/null +++ b/public/language/pt-PT/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Definições de Reconexão", + "max-attempts": "Máximo de Tentativas de Reconexão", + "default-placeholder": "Predefinição: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/sounds.json b/public/language/pt-PT/admin/settings/sounds.json new file mode 100644 index 0000000000..167e6dbed4 --- /dev/null +++ b/public/language/pt-PT/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notificações", + "chat-messages": "Mensagens de conversas", + "play-sound": "Reproduzir", + "incoming-message": "A Receber Mensagem", + "outgoing-message": "A Enviar Mensagem", + "upload-new-sound": "Enviar Novo Som", + "saved": "Definições guardadas" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/tags.json b/public/language/pt-PT/admin/settings/tags.json new file mode 100644 index 0000000000..7cf1dd0d04 --- /dev/null +++ b/public/language/pt-PT/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Definições das Etiquetas", + "link-to-manage": "Gerir Etiquetas", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Mínimo de Etiquetas por Tópico", + "max-per-topic": "Máximo de Etiquetas por Tópico", + "min-length": "Comprimento Mínimo da Etiqueta", + "max-length": "Comprimento Máximo da Etiqueta", + "related-topics": "Tópicos Relacionados", + "max-related-topics": "Máximo de tópicos relacionados a mostrar (se for suportado pelo tema)" +} \ No newline at end of file diff --git a/public/language/pt-PT/admin/settings/uploads.json b/public/language/pt-PT/admin/settings/uploads.json new file mode 100644 index 0000000000..14c302fb20 --- /dev/null +++ b/public/language/pt-PT/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Publicações", + "orphans": "Orphaned Files", + "private": "Tornar os ficheiros enviados privados", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(em pixeis, predefinido: 1520 pixeis, definir 0 para desativar)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(em pixeis, predefinido: 760 pixeis, definir 0 para desativar)", + "resize-image-quality": "Qualidade a utilizar quando redimensionar imagens", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Largura Máxima da Imagem (em píxeis)", + "reject-image-width-help": "Imagens mais largas que este valor vão ser rejeitadas.", + "reject-image-height": "Altura Máxima da Imagem (em píxeis)", + "reject-image-height-help": "Imagens mais altas que este valor vão ser rejeitadas.", + "allow-topic-thumbnails": "Permitir aos utilizadores enviar miniaturas de tópicos", + "topic-thumb-size": "Tamanho da Miniatura do Tópico", + "allowed-file-extensions": "Extensões de Ficheiro Permitidas", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Permitir aos utilizadores enviar fotografias de perfil", + "convert-profile-image-png": "Converter imagens de perfil enviadas em PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Enviar", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(em pixeis, predefinido: 128 pixeis)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Manter versões antigas das fotos de perfil e das fotos de capa no servidor", + "profile-covers": "Imagens de Capa de Perfil", + "default-covers": "Imagem de Capa Predefinida", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/pt-PT/admin/settings/user.json b/public/language/pt-PT/admin/settings/user.json new file mode 100644 index 0000000000..29a086a557 --- /dev/null +++ b/public/language/pt-PT/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Autenticação", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Permitir início de sessão com", + "allow-login-with.username-email": "Nome de Utilizador ou E-mail", + "allow-login-with.username": "Nome de Utilizador Apenas", + "account-settings": "Definições de Conta", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Desativar alterações aos nomes de utilizador", + "disable-email-changes": "Desativar alterações aos e-mails", + "disable-password-changes": "Desativar alterações de palavras-passe", + "allow-account-deletion": "Permitir eliminação da conta", + "hide-fullname": "Esconder o nome completo dos utilizadores", + "hide-email": "Esconder o e-mail dos utilizadores", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Temas", + "disable-user-skins": "Impedir utilizadores de escolherem uma máscara personalizada", + "account-protection": "Proteção de Conta", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Tentativas de início de sessão por hora", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Tempo de Sessão", + "session-time-days": "Dias", + "session-time-seconds": "Segundos", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutos após o utilizador ser considerado inativo", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "Registo de Utilizadores", + "registration-type": "Tipo de Registo", + "registration-approval-type": "Tipo de Aprovação de Registo", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Aprovado por um Administrador", + "registration-type.admin-approval-ip": "Aprovado por um Administrador para IPs", + "registration-type.invite-only": "Apenas por Convite", + "registration-type.admin-invite-only": "Apenas por Convite de um Administrador", + "registration-type.disabled": "Sem registo", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Máximo de Convites por Utilizador", + "max-invites": "Máximo de Convites por Utilizador", + "max-invites-help": "Usa 0 para nenhuma restrição. Administradores têm convites infinitos.
Apenas aplicável quando selecionado \"Apenas por Convite\"", + "invite-expiration": "Data de validade do convite", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Comprimento Mínimo do Nome de Utilizador", + "max-username-length": "Comprimento Máximo do Nome de Utilizador", + "min-password-length": "Comprimento Mínimo da Palavra-Passe", + "min-password-strength": "Força Mínima da Palavra-Passe", + "max-about-me-length": "Comprimento Máximo do \"Sobre Mim\"", + "terms-of-use": "Termos de Uso do Fórum (Deixa em branco para desativar)", + "user-search": "Procura de Utilizadores", + "user-search-results-per-page": "Número de resultados a mostrar", + "default-user-settings": "Definições Predefinidas do Utilizador", + "show-email": "Mostrar e-mail", + "show-fullname": "Mostrar nome completo", + "restrict-chat": "Apenas permitir mensagens de utilizadores que eu sigo", + "outgoing-new-tab": "Abrir links externos num novo separador", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscrever o Resumo", + "digest-freq.off": "Desligado", + "digest-freq.daily": "Diariamente ", + "digest-freq.weekly": "Semanalmente", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Mensalmente", + "email-chat-notifs": "Enviar um e-mail se receber uma nova mensagem e não estiver online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Definições predefinidas das notificações", + "categoryWatchState": "Estado predefinido da subscrição de categorias", + "categoryWatchState.watching": "A seguir", + "categoryWatchState.notwatching": "A não seguir", + "categoryWatchState.ignoring": "A ignorar" +} diff --git a/public/language/pt-PT/admin/settings/web-crawler.json b/public/language/pt-PT/admin/settings/web-crawler.json new file mode 100644 index 0000000000..b8d164ecdd --- /dev/null +++ b/public/language/pt-PT/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Desativar RSS Feeds", + "disable-sitemap-xml": "Desativar Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Limpar Cache do Sitemap", + "view-sitemap": "Ver Sitemap" +} \ No newline at end of file diff --git a/public/language/pt-PT/category.json b/public/language/pt-PT/category.json new file mode 100644 index 0000000000..b1b955f79c --- /dev/null +++ b/public/language/pt-PT/category.json @@ -0,0 +1,23 @@ +{ + "category": "Categoria", + "subcategories": "Subcategorias", + "new_topic_button": "Novo Tópico", + "guest-login-post": "Inicia sessão para publicar algo", + "no_topics": "Não existe nenhum tópico nesta categoria.
Que tal seres o primeiro a publicar aqui?", + "browsing": "navegação", + "no_replies": "Ainda sem respostas", + "no_new_posts": "Não existem publicações novas.", + "watch": "Subscrever", + "ignore": "Ignorar", + "watching": "A seguir", + "not-watching": "Não seguir", + "ignoring": "A ignorar", + "watching.description": "Mostrar tópicos em \"não lidos\" e \"recentes\"", + "not-watching.description": "Não mostrar tópicos em \"não lidos\", mostrar em \"recentes\"", + "ignoring.description": "Não mostrar tópicos em \"não lidos\" e \"recentes\"", + "watching.message": "Estás agora a seguir todas as atualizações desta categoria e de todas as suas subcategorias", + "notwatching.message": "Não estás a seguir atualizações desta categoria e de todas as suas subcategorias", + "ignoring.message": "Estás agora a ignorar todas as atualizações desta categoria e de todas as suas subcategorias", + "watched-categories": "Categorias subscritas", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/pt-PT/email.json b/public/language/pt-PT/email.json new file mode 100644 index 0000000000..6eb33c397a --- /dev/null +++ b/public/language/pt-PT/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Testar E-mail", + "password-reset-requested": "Reposição de palavra-passe solicitada!", + "welcome-to": "Bem-vindo ao %1", + "invite": "Convite enviado por %1", + "greeting_no_name": "Olá", + "greeting_with_name": "Olá %1", + "email.verify-your-email.subject": "Por favor verifica o teu e-mail", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Obrigado por te teres registado no %1!", + "welcome.text2": "De forma a finalizar o processo de activação da tua conta, precisamos de verificar que és o legítimo dono da conta de e-mail registada.", + "welcome.text3": "Um administrador aceitou o teu registo. Podes agora iniciar sessão com o teu nome de utilizador/palavra-passe.", + "welcome.cta": "Clica aqui para confirmares o teu endereço de e-mail", + "invitation.text1": "%1 convidou-te para te juntares a %2", + "invitation.text2": "O teu convite vai expirar em %1 dias.", + "invitation.cta": "Clica aqui para criares a tua conta.", + "reset.text1": "Recebemos um pedido para repôr a tua palavra-passe, possivelmente porque te esqueceste dela. Se este não é o caso, por favor ignora este e-mail.", + "reset.text2": "Para continuares com a reposição da tua palavra-passe, clica no seguinte link:", + "reset.cta": "Clica aqui para reiniciares a tua palavra-passe.", + "reset.notify.subject": "A tua palavra-passe foi alterada com sucesso", + "reset.notify.text1": "Estamos a notificar-te que a %1, a tua palavra-passe foi alterada com sucesso.", + "reset.notify.text2": "Se não autorizaste isto, por favor notifica o administrador imediatamente.", + "digest.latest_topics": "Tópicos recentes de %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Clica aqui para visitares %1", + "digest.unsub.info": "Este resumo foi-te enviado devido às tuas definições de subscrição.", + "digest.day": "dia", + "digest.week": "semana", + "digest.month": "mês", + "digest.subject": "Resumo para %1", + "digest.title.day": "O teu Resumo Diário", + "digest.title.week": "O teu Resumo Semanal", + "digest.title.month": "O teu Resumo Mensal", + "notif.chat.subject": "Nova mensagem de %1", + "notif.chat.cta": "Clique aqui para continuar a conversa", + "notif.chat.unsub.info": "Esta notificação de chat foi enviada devido às suas definições de subscrição", + "notif.post.unsub.info": "Esta notificação foi envidada devido às tuas definições de subscrição.", + "notif.post.unsub.one-click": "Em alternativa, cancela a subscrição de e-mails futuros como este, clicando em", + "notif.cta": "Para o fórum", + "notif.cta-new-reply": "Ver Publicação", + "notif.cta-new-chat": "Ver Conversa", + "notif.test.short": "Testar Notificações", + "notif.test.long": "Este é um teste do e-mail de notificações. Envia ajuda!", + "test.text1": "Este é um e-mail de teste para verificar que o emailer está configurado corretamente para o teu NodeBB.", + "unsub.cta": "Clica aqui para alterares essas definições", + "unsubscribe": "cancelar subscrição", + "unsub.success": "Não receberás mais e-mails da lista de e-mails %1", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Foste banido de %1", + "banned.text1": "O utilizador %1 foi banido de %2.", + "banned.text2": "Este banimento irá durar até %1.", + "banned.text3": "Esta é a razão porque foste banido:", + "closing": "Obrigado!" +} \ No newline at end of file diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json new file mode 100644 index 0000000000..65e772d521 --- /dev/null +++ b/public/language/pt-PT/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Dados inválidos", + "invalid-json": "JSON inválido", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Não tens sessão iniciada.", + "account-locked": "A sua conta foi bloqueada temporariamente", + "search-requires-login": "A pesquisa requer uma conta de utilizador - por favor inicia sessão ou cria uma conta.", + "goback": "Pressione voltar para regressar à página anterior", + "invalid-cid": "ID de Categoria Inválido", + "invalid-tid": "ID de Tópico Inválido", + "invalid-pid": "ID de Publicação Inválido", + "invalid-uid": "ID de Utilizador Inválido", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Utilizador Inválido", + "invalid-email": "E-mail Inválido", + "invalid-fullname": "Nome Completo Inválido", + "invalid-location": "Localização Inválida", + "invalid-birthday": "Aniversário Inválido", + "invalid-title": "Título inválido", + "invalid-user-data": "Dados de Utilizador Inválidos", + "invalid-password": "Palavra-passe Inválida", + "invalid-login-credentials": "Credenciais de início de sessão inválidas", + "invalid-username-or-password": "Por favor especificar um nome de utilizador e uma palavra-passe", + "invalid-search-term": "Termo de pesquisa inválido", + "invalid-url": "URL Inválido", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "Não conseguimos iniciar a tua sessão, provavelmente devido a uma sessão que já expirou. Por favor, tenta novamente", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Valor de paginação errado, deve ser no mínimo %1 e no máximo %2", + "username-taken": "Nome de utilizar já utilizado", + "email-taken": "E-mail já utilizado", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Não podes utilizar o chat enquanto não confirmares o teu e-mail, por favor clica aqui para confirmares o teu e-mail.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Não conseguimos confirmar o teu e-mail, por favor tenta mais tarde.", + "confirm-email-already-sent": "O e-mail de confirmação já foi enviado, por favor espera %1 minuto(s) para enviares outro.", + "sendmail-not-found": "O executável sendmail não foi encontrado, por favor assegura-te que se encontra instalado e executável pelo utilizador a correr o NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Nome de utilizador muito curto", + "username-too-long": "Nome de utilizador muito longo", + "password-too-long": "Palavra-passe muito longa", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Utilizador banido", + "user-banned-reason": "Desculpa, esta conta foi banida (Motivo: %1)", + "user-banned-reason-until": "Desculpa, esta conta foi banida até %1 (Motivo: %2)", + "user-too-new": "Desculpa, é necessário que esperes %1 segundo(s) antes de fazeres a tua primeira publicação", + "blacklisted-ip": "Desculpa, o teu endereço IP foi banido desta comunidade. Se sentes que isto é um erro, por favor contacta o administrador.", + "ban-expiry-missing": "Por favor providencia uma data para o fim deste banimento", + "no-category": "Categoria não existente", + "no-topic": "Tópico não existente", + "no-post": "Publicação não existente", + "no-group": "Grupo não existente", + "no-user": "Utilizador não existente", + "no-teaser": "Não existe pré-visualização", + "no-flag": "Flag does not exist", + "no-privileges": "Não possuis privilégios suficientes para esta ação.", + "category-disabled": "Categoria desativada", + "topic-locked": "Tópico bloqueado", + "post-edit-duration-expired": "Só tens permissão para editar publicações %1 segundo(s) depois da sua publicação", + "post-edit-duration-expired-minutes": "Só tens permissão para editar publicações %1 minuto(s) depois da sua publicação", + "post-edit-duration-expired-minutes-seconds": "Só tens permissão para editar publicações %1 minuto(s) %2segundo(s) depois da sua publicação", + "post-edit-duration-expired-hours": "Só tens permissão para editar publicações %1 hora(s) depois da sua publicação", + "post-edit-duration-expired-hours-minutes": "Só tens permissão para editar publicações %1 hora(s) 2% minuto(s) depois da sua publicação", + "post-edit-duration-expired-days": "Só tens permissão para editar publicações %1 dia(s) depois da sua publicação", + "post-edit-duration-expired-days-hours": "Só tens permissão para editar publicações %1 dia(s) %2 hora(s) depois da sua publicação", + "post-delete-duration-expired": "Só tens permissão para eliminar publicações %1 segundo(s) depois da sua publicação", + "post-delete-duration-expired-minutes": "Só tens permissão para eliminar publicações %1 minuto(s) depois da sua publicação", + "post-delete-duration-expired-minutes-seconds": "Só tens permissão para eliminar publicações %1 minuto(s) %2 segundo(s) depois da sua publicação", + "post-delete-duration-expired-hours": "Só tens permissão para eliminar publicações %1 hora(s) depois da sua publicação", + "post-delete-duration-expired-hours-minutes": "Só tens permissão para eliminar publicações %1 hora(s) %2 minuto(s) depois da sua publicação", + "post-delete-duration-expired-days": "Só tens permissão para eliminar publicações %1 dia(s) depois da sua publicação", + "post-delete-duration-expired-days-hours": "Só tens permissão para eliminar publicações %1 dia(s) %2 hora(s) depois da sua publicação", + "cant-delete-topic-has-reply": "Não podes apagar um tópico após ele ter uma resposta", + "cant-delete-topic-has-replies": "Não podes apagar o tópico após ele ter %1 respostas", + "content-too-short": "Por favor insere uma publicação maior. As publicações devem ter no mínimo %1 caracter(es).", + "content-too-long": "Por favor introduz uma publicação mais curta. As publicações não devem ter mais que %1 caracter(es).", + "title-too-short": "Por favor introduz um título maior. Os títulos devem conter pelo menos %1 caracter(es).", + "title-too-long": "Por favor introduz um título mais curto. Os títulos deve ter no máximo %1 caracter(es).", + "category-not-selected": "Categoria não selecionada.", + "too-many-posts": "Só podes publicar a cada %1 segundo(s) - por favor espera até poderes publicar outra vez", + "too-many-posts-newbie": "Como novo utilizador, só podes publicar a cada %1 segundo(s) até teres conquistado %2 de reputação - por favor espera até poderes publicar outra vez", + "already-posting": "You are already posting", + "tag-too-short": "Por favor introduz um marcador maior. Os marcadores devem ter pelo menos %1 caracter(s)", + "tag-too-long": "Por favor introduz um marcador mais curto. Os marcadores devem ter no máximo %1 caracter(es)", + "not-enough-tags": "Não existem marcadores suficientes. Os tópicos devem ter pelo menos %1 marcador(es)", + "too-many-tags": "Existem marcadores a mais. Os tópicos não podem ter mais do que %1 marcador(es)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Por favor aguarda até todos os carregamentos estarem completos.", + "file-too-big": "O tamanho máximo permitido para um ficheiro é de %1 kB - por favor carrega um ficheiro mais pequeno", + "guest-upload-disabled": "Os carregamentos por parte de convidados foram desativados", + "cors-error": "Impossível carregar a imagem devido ao CORS mal configurado", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Já marcaste esta publicação", + "already-unbookmarked": "Já desmarcaste esta publicação", + "cant-ban-other-admins": "Não podes banir outros administradores!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "És o único administrador. Adicionar outro utilizador como administrador antes de te removeres como administrador.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove os privilégios de administrador desta conta antes de tentares apagá-la.", + "already-deleting": "Already deleting", + "invalid-image": "Imagem inválida", + "invalid-image-type": "Tipo de imagem inválida. Os tipos válidos são: %1", + "invalid-image-extension": "Extensão de imagem inválida", + "invalid-file-type": "Tipo de ficheiro inválido. Os tipos válidos são: %1", + "invalid-image-dimensions": "Dimensões da imagem demasiado grandes", + "group-name-too-short": "Nome de grupo muito curto", + "group-name-too-long": "Nome de grupo muito longo", + "group-already-exists": "Grupo já existente", + "group-name-change-not-allowed": "Alterações ao nome do grupo não são permitidas", + "group-already-member": "Já pertences a este grupo", + "group-not-member": "Não és um membro deste grupo", + "group-needs-owner": "Este grupo requer pelo menos um dono", + "group-already-invited": "Este utilizador já foi convidado", + "group-already-requested": "O teu pedido de adesão já foi submetido", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Esta publicação já foi eliminada", + "post-already-restored": "Esta publicação já foi restaurada", + "topic-already-deleted": "Este tópico já foi eliminado", + "topic-already-restored": "Este tópico já foi restaurado", + "cant-purge-main-post": "Não podes eliminar a publicação principal, em vez disso, por favor apaga o tópico", + "topic-thumbnails-are-disabled": "Miniaturas para os tópicos estão desativadas.", + "invalid-file": "Ficheiro inválido", + "uploads-are-disabled": "Os carregamentos estão desativados", + "signature-too-long": "Desculpa, a tua assinatura não pode ser superior a %1 caracter(es).", + "about-me-too-long": "Desculpa, o teu \"sobre mim\" não pode ser superior a %1 caracter(es).", + "cant-chat-with-yourself": "Não podes conversar contigo mesmo!", + "chat-restricted": "Este utilizador colocou restrições sobre as suas mensagens de chat. Ele deve primeiro seguir-te antes que possas conversar com ele", + "chat-disabled": "Sistema de conversas desativado", + "too-many-messages": "Enviaste demasiadas mensagens, por favor espera um pouco.", + "invalid-chat-message": "Mensagem de chat inválida", + "chat-message-too-long": "As mensagens não podem ter mais de %1 caracteres.", + "cant-edit-chat-message": "Não tens permissão para editar esta mensagem", + "cant-delete-chat-message": "Não tens permissão para eliminar esta mensagem", + "chat-edit-duration-expired": "Só tens permissão para editar mensagens do chat %1 segundo(s) depois de publicares", + "chat-delete-duration-expired": "Só tens permissão para apagar mensagens do chat %1 segundo(s) depois de publicares", + "chat-deleted-already": "Esta mensagem já foi apagada.", + "chat-restored-already": "Esta mensagem já foi restaurada.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Já votaste nesta publicação.", + "reputation-system-disabled": "O sistema de reputação está desativado.", + "downvoting-disabled": "Os votos negativos estão desativados", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Não podes votar na tua própria publicação", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "Tu só podes votar negativamente %1 vezes por dia", + "too-many-downvotes-today-user": "Tu só podes votar negativamente um utilizador %1 vezes por dia", + "reload-failed": "NodeBB encontrou um erro enquanto recarregava: \"%1\". NodeBB irá continuar a servir os ativos existentes do lado do utilizador. No entanto deverias desfazer o que fizeste mesmo antes de teres voltado a recarregar.", + "registration-error": "Erro de registro", + "parse-error": "Ocorreu um erro enquanto analisávamos a resposta do servidor", + "wrong-login-type-email": "Por favor utiliza o teu e-mail para iniciares sessão", + "wrong-login-type-username": "Por favor utiliza o teu nome de utilizador para iniciares sessão", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "Convidaste o máximo de pessoas (%1 em %2).", + "no-session-found": "Não foram encontradas sessões ativas!", + "not-in-room": "Utilizador não se encontra na sala", + "cant-kick-self": "Não te podes expulsar a ti próprio do grupo", + "no-users-selected": "Não existe(m) utilizador(es) selecionado(s)", + "invalid-home-page-route": "Rota para a página principal inválida", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Nenhum tópico selecionado!", + "cant-move-to-same-topic": "Não podes mover publicações para o mesmo tópico!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Não podes bloquear-te a ti próprio!", + "cannot-block-privileged": "Não podes bloquear administradores ou moderadores globais", + "cannot-block-guest": "Convidados não podem bloquear outros utilizadores", + "already-blocked": "Este utilizador já está bloqueado", + "already-unblocked": "Este utilizador já está desbloqueado", + "no-connection": "Parece haver um problema com a tua conexão à Internet", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/pt-PT/flags.json b/public/language/pt-PT/flags.json new file mode 100644 index 0000000000..8a16ba96e9 --- /dev/null +++ b/public/language/pt-PT/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Estado", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Fantástico! Não foram encontradas denúncias.", + "assignee": "Responsável", + "update": "Atualizar", + "updated": "Atualizado", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Denúncias Diárias", + "quick-filters": "Filtros Rápidos", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remover Filtros", + "filters": "Opções dos Filtros", + "filter-reporterId": "UID do autor da denúncia", + "filter-targetUid": "UID do denunciado", + "filter-type": "Tipo de denúncia", + "filter-type-all": "Todo o Conteúdo", + "filter-type-post": "Publicação", + "filter-type-user": "Utilizador", + "filter-state": "Estado", + "filter-assignee": "UID do responsável", + "filter-cid": "Categoria", + "filter-quick-mine": "Atribuído a mim", + "filter-cid-all": "Todas as categorias", + "apply-filters": "Aplicar Filtros", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Ações Rápidas", + "flagged-user": "Utilizador Denunciado", + "view-profile": "Ver Perfil", + "start-new-chat": "Iniciar Nova Conversa", + "go-to-target": "Ver Alvo da Denúncia", + "assign-to-me": "Atribuir a Mim", + "delete-post": "Apagar Publicação", + "purge-post": "Eliminar Publicação", + "restore-post": "Restaurar Publicação", + "delete": "Delete Flag", + + "user-view": "Ver Perfil", + "user-edit": "Editar Perfil", + + "notes": "Notas da Denúncia", + "add-note": "Adicionar Nota", + "no-notes": "Não existem notas partilhadas.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Nota Adicionada.", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Conta & Histórico de Denúncias", + "no-history": "Não existe histórico de denúncias.", + + "state-all": "Todos os estados", + "state-open": "Novo/Abrir", + "state-wip": "Trabalho em Progresso", + "state-resolved": "Resolvido", + "state-rejected": "Rejeitado", + "no-assignee": "Não Atribuído", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Ofensivo", + "modal-reason-other": "Outra (especificar abaixo)", + "modal-reason-custom": "Motivo para denunciar este conteúdo...", + "modal-submit": "Submeter Denúncia", + "modal-submit-success": "Este conteúdo foi denunciado para moderação.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/pt-PT/global.json b/public/language/pt-PT/global.json new file mode 100644 index 0000000000..1fa4b002ae --- /dev/null +++ b/public/language/pt-PT/global.json @@ -0,0 +1,126 @@ +{ + "home": "Página principal", + "search": "Procurar", + "buttons.close": "Fechar", + "403.title": "Acesso negado", + "403.message": "Parece que encontraste uma página à qual não tens acesso.", + "403.login": "Talvez devesses tentar iniciar sessão?", + "404.title": "Não encontrado", + "404.message": "Parece que encontraste uma página que não existe. Regressa à página principal.", + "500.title": "Erro interno.", + "500.message": "Oops! Parece que algo correu mal!", + "400.title": "O pedido não correu bem.", + "400.message": "Parece que este link está mal formado. Por favor volta a confirma-lo e tenta novamente ou então retorna à página principal.", + "register": "Regista-te", + "login": "Iniciar sessão", + "please_log_in": "Por favor inicia sessão", + "logout": "Terminar sessão", + "posting_restriction_info": "Publicar está, neste momento, apenas restrito a membros registados, clica aqui para iniciares sessão.", + "welcome_back": "Bem-vindo de volta", + "you_have_successfully_logged_in": "Iniciaste sessão com sucesso", + "save_changes": "Guardar as alterações", + "save": "Guardar", + "close": "Fechar", + "pagination": "Paginação", + "pagination.out_of": "%1 de %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Administrador", + "header.categories": "Categorias", + "header.recent": "Recentes", + "header.unread": "Por ler", + "header.tags": "Marcadores", + "header.popular": "Popular", + "header.top": "Top", + "header.users": "Utilizadores", + "header.groups": "Grupos", + "header.chats": "Conversas", + "header.notifications": "Notificações", + "header.search": "Procurar", + "header.profile": "Perfil", + "header.navigation": "Navegação", + "notifications.loading": "Carregando as notificações", + "chats.loading": "Carregando as conversas", + "motd.welcome": "Bem-vindo ao NodeBB, a plataforma de discussões do futuro.", + "previouspage": "Página anterior", + "nextpage": "Página seguinte", + "alert.success": "Sucesso", + "alert.error": "Erro", + "alert.banned": "Banido", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Deixaste de seguir %1!", + "alert.follow": "Estás agora a seguir %1!", + "users": "Utilizadores", + "topics": "Tópicos", + "posts": "Publicações", + "x-posts": "%1 posts", + "best": "Melhores", + "controversial": "Controversial", + "votes": "Votos", + "x-votes": "%1 votes", + "voters": "Votantes", + "upvoters": "Votos positivos", + "upvoted": "Votado favoravelmente", + "downvoters": "Votos negativos", + "downvoted": "Votado negativamente", + "views": "Visualizações", + "posters": "Posters", + "reputation": "Reputação", + "lastpost": "Última publicação", + "firstpost": "Primeira publicação", + "read_more": "Ler mais", + "more": "Mais", + "none": "None", + "posted_ago_by_guest": "publicou %1 por Convidado", + "posted_ago_by": "publicou %1 por %2", + "posted_ago": "publicou %1", + "posted_in": "publicado em %1", + "posted_in_by": "publicado em %1 por %2", + "posted_in_ago": "publicado em %1 %2", + "posted_in_ago_by": "publicado em %1 %2 por %3", + "user_posted_ago": "%1 publicou %2", + "guest_posted_ago": "Convidado publicou %1", + "last_edited_by": "última edição por %1", + "norecentposts": "Não existen publicações recentes", + "norecenttopics": "Não existem tópicos recentes", + "recentposts": "Publicações recentes", + "recentips": "Recentemente com sessões iniciadas em IPs", + "moderator_tools": "Ferramentas de moderador", + "online": "Online", + "away": "Ausente", + "dnd": "Não perturbar", + "invisible": "Invisível", + "offline": "Offline", + "email": "E-mail", + "language": "Língua", + "guest": "Convidado", + "guests": "Convidados", + "former_user": "Um Utilizador Antigo", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Fórum atualizado", + "updated.message": "Este fórum acabou de ser atualizado para a versão mais recente. Carrega aqui para atualizares a página.", + "privacy": "Privacidade", + "follow": "Seguir", + "unfollow": "Deixar de seguir", + "delete_all": "Apagar tudo", + "map": "Mapa", + "sessions": "Sessões ativas", + "ip_address": "Endereço IP", + "enter_page_number": "Introduzir número da página", + "upload_file": "Enviar ficheiro", + "upload": "Carregar", + "uploads": "Carregamentos", + "allowed-file-types": "Os tipos de ficheiro permitidos são %1", + "unsaved-changes": "Tens alterações por guardar. Tens a certeza que pretendes mudar de página?", + "reconnecting-message": "Parece que a tua conexão com %1 foi perdida. Por favor, espera enquanto tentamos reconectar-te.", + "play": "Reproduzir", + "cookies.message": "Este website utiliza cookies para assegurar que tens a melhor experiência no nosso website.", + "cookies.accept": "Apontado!", + "cookies.learn_more": "Aprende Mais", + "edited": "Editado", + "disabled": "Desativado", + "select": "Selecionar", + "user-search-prompt": "Digita algo aqui para encontrar utilizadores..." +} \ No newline at end of file diff --git a/public/language/pt-PT/groups.json b/public/language/pt-PT/groups.json new file mode 100644 index 0000000000..60d8665f8a --- /dev/null +++ b/public/language/pt-PT/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupos", + "view_group": "Ver o grupo", + "owner": "Dono do grupo", + "new_group": "Criar novo grupo", + "no_groups_found": "Não existem grupos para ver", + "pending.accept": "Aceitar", + "pending.reject": "Rejeitar", + "pending.accept_all": "Aceitar todos", + "pending.reject_all": "Rejeitar todas", + "pending.none": "Não existem membros pendentes neste momento", + "invited.none": "Não existem membros convidados neste momento", + "invited.uninvite": "Cancelar convite", + "invited.search": "Procura por um utilizador para convidares para este grupo", + "invited.notification_title": "Foste convidado para te juntares a %1", + "request.notification_title": "Pedido de adesão ao grupo por parte de %1", + "request.notification_text": "%1 pediu para se tornar um membro de %2", + "cover-save": "Guardar", + "cover-saving": "Guardando", + "details.title": "Detalhes do grupo", + "details.members": "Lista de membros", + "details.pending": "Membros pendentes", + "details.invited": "Membros convidados", + "details.has_no_posts": "Os membros deste grupo ainda não fizeram nenhuma publicação.", + "details.latest_posts": "Publicações Recentes", + "details.private": "Privado", + "details.disableJoinRequests": "Desativar pedidos de adesão", + "details.disableLeave": "Proibir os utilizadores de saírem do grupo", + "details.grant": "Conceder/rescindir posse", + "details.kick": "Expulsar", + "details.kick_confirm": "Tens a certeza que queres remover este membro do grupo?", + "details.add-member": "Adicionar Membro", + "details.owner_options": "Administração do grupo", + "details.group_name": "Nome do grupo", + "details.member_count": "Quantidade de membros", + "details.creation_date": "Data de criação", + "details.description": "Descrição", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Pré-visualização do crachá", + "details.change_icon": "Alterar o Ícone", + "details.change_label_colour": "Alterar Cor da Etiqueta", + "details.change_text_colour": "Alterar Cor do Texto", + "details.badge_text": "Texto do crachá", + "details.userTitleEnabled": "Mostrar crachá", + "details.private_help": "Se ativado, a adesão ao grupo requer a aprovação de um dos donos do grupo", + "details.hidden": "Escondido", + "details.hidden_help": "Se ativado, este grupo não será encontrado na listagem de grupos e os utilizadores terão de ser convidados manualmente", + "details.delete_group": "Eliminar grupo", + "details.private_system_help": "Tornar grupos privados é desativado ao nível do sistema. Esta opção não executa nada", + "event.updated": "Detalhes do grupo foram atualizados", + "event.deleted": "O grupo \"%1\" foi apagado", + "membership.accept-invitation": "Aceitar convite", + "membership.accept.notification_title": "És agora um membro de %1", + "membership.invitation-pending": "Convite em espera", + "membership.join-group": "Aderir ao grupo", + "membership.leave-group": "Sair do grupo", + "membership.leave.notification_title": "%1 deixou o grupo %2", + "membership.reject": "Rejeitar", + "new-group.group_name": "Nome do grupo:", + "upload-group-cover": "Carregar capa do grupo", + "bulk-invite-instructions": "Introduz uma lista de nomes de utilizadores separados por vírgulas para convidar para este grupo", + "bulk-invite": "Convidar em grupo", + "remove_group_cover_confirm": "Tens a certeza que queres remover a foto de capa?" +} \ No newline at end of file diff --git a/public/language/pt-PT/ip-blacklist.json b/public/language/pt-PT/ip-blacklist.json new file mode 100644 index 0000000000..a61e504775 --- /dev/null +++ b/public/language/pt-PT/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configura a tua lista negra de IPs aqui.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Regras Ativas", + "validate": "Validar Lista Negra", + "apply": "Aplicar Lista Negra", + "hints": "Dicas de Sintaxe", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "Podes adicionar comentários começando linhas com o símbolo #.", + + "validate.x-valid": "%1 de %2 regra(s) válidas.", + "validate.x-invalid": "As seguintes %1 regras são inválidas:", + + "alerts.applied-success": "Lista Negra Aplicada", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banido" +} \ No newline at end of file diff --git a/public/language/pt-PT/language.json b/public/language/pt-PT/language.json new file mode 100644 index 0000000000..2c368a51ae --- /dev/null +++ b/public/language/pt-PT/language.json @@ -0,0 +1,5 @@ +{ + "name": "Português", + "code": "pt-PT", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/pt-PT/login.json b/public/language/pt-PT/login.json new file mode 100644 index 0000000000..7f0f1c509c --- /dev/null +++ b/public/language/pt-PT/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Nome de utilizador / E-mail", + "username": "Nome de utilizador", + "remember_me": "Lembrar-me", + "forgot_password": "Esqueceste-te da palavra-passe?", + "alternative_logins": "Inícios de sessão alternativos", + "failed_login_attempt": "Início de sessão sem sucesso", + "login_successful": "Iniciaste sessão com sucesso!", + "dont_have_account": "Não tens uma conta?", + "logged-out-due-to-inactivity": "A tua sessão no Painel de Controlo foi terminada devido a inatividade", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/pt-PT/modules.json b/public/language/pt-PT/modules.json new file mode 100644 index 0000000000..424003f11f --- /dev/null +++ b/public/language/pt-PT/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Conversar com", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Enviar", + "chat.no_active": "Não tens conversas ativas.", + "chat.user_typing": "%1 está a escrever ...", + "chat.user_has_messaged_you": "%1 enviou-te uma mensagem.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Por favor seleciona um destinatário para veres o histórico de mensagens", + "chat.no-users-in-room": "Não existem utilizadores nesta sala", + "chat.recent-chats": "Conversas recentes", + "chat.contacts": "Contactos", + "chat.message-history": "Histórico de mensagens", + "chat.message-deleted": "Mensagem Apagada", + "chat.options": "Opções de conversa", + "chat.pop-out": "Destacar a janela de conversação", + "chat.minimize": "Minimizar", + "chat.maximize": "Maximizar", + "chat.seven_days": "7 dias", + "chat.thirty_days": "30 dias", + "chat.three_months": "3 meses", + "chat.delete_message_confirm": "Tens a certeza que desejas apagar esta mensagem?", + "chat.retrieving-users": "A recuperar utilizadores...", + "chat.manage-room": "Gerir sala de conversa", + "chat.add-user-help": "Encontra utilizadores aqui. Quando selecionado, o utilizador vai ser adicionado à conversa. O novo utilizador não conseguirá ver mensagens que foram enviadas antes de ele entrar na sala. Apenas os donos das salas () poderão remover participantes das salas de conversa", + "chat.confirm-chat-with-dnd-user": "Este utilizador definiu o seu estado como \"não perturbar\". Pretendes mesmo assim conversar com ele?", + "chat.rename-room": "Renomear esta sala", + "chat.rename-placeholder": "Insere o nome da conversa aqui", + "chat.rename-help": "O nome desta conversa vai ser visualizada por todos os participantes desta sala.", + "chat.leave": "Sair da conversa", + "chat.leave-prompt": "Tens a certeza que pretendes sair desta conversa?", + "chat.leave-help": "Deixar esta sala vai te remover de receber futuras mensagens importantes nesta conversa. Se fores novamente adicionado no futuro, não conseguirás ver nenhum histórico das conversas de antes da tua re-adesão.", + "chat.in-room": "Participantes nesta sala", + "chat.kick": "Expulsar", + "chat.show-ip": "Mostrar IP", + "chat.owner": "Dono da Sala", + "chat.system.user-join": "%1 entrou na sala", + "chat.system.user-leave": "%1 saiu da sala", + "chat.system.room-rename": "%2 renomeou esta sala: %1", + "composer.compose": "Compor", + "composer.show_preview": "Mostrar pré-visualização", + "composer.hide_preview": "Ocultar pré-visualização", + "composer.user_said_in": "%1 disse em %2:", + "composer.user_said": "%1 disse:", + "composer.discard": "Tens a certeza que queres descartar esta publicação?", + "composer.submit_and_lock": "Submeter e bloquear", + "composer.toggle_dropdown": "Alternar entre caixas", + "composer.uploading": "Carregando %1", + "composer.formatting.bold": "Negrito", + "composer.formatting.italic": "Itálico", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Riscado", + "composer.formatting.code": "Código", + "composer.formatting.link": "Hiperligação", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Enviar imagem", + "composer.upload-file": "Enviar um ficheiro", + "composer.zen_mode": "Modo Zen", + "composer.select_category": "Selecionar uma categoria", + "composer.textarea.placeholder": "Escreve aqui o conteúdo da tua publicação, arrasta e solta imagens", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancelar", + "bootbox.confirm": "Confirmar", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Posicionamento da fotografia de capa", + "cover.dragging_message": "Arrasta a fotografia de capa para a posição desejada e carregar \"Guardar\"", + "cover.saved": "Fotografia de capa e posição guardadas", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/pt-PT/notifications.json b/public/language/pt-PT/notifications.json new file mode 100644 index 0000000000..58c1116f83 --- /dev/null +++ b/public/language/pt-PT/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notificações", + "no_notifs": "Não tens notificações novas", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Voltar para %1", + "outgoing_link": "Link Externo", + "outgoing_link_message": "Estás agora a abandonar %1", + "continue_to": "Continuar para %1", + "return_to": "Voltar a %1", + "new_notification": "Tens uma nova notificação", + "you_have_unread_notifications": "Tens notificações por ler.", + "all": "Tudo", + "topics": "Tópicos", + "replies": "Respostas", + "chat": "Chat", + "group-chat": "Group Chats", + "follows": "Novos seguidores", + "upvote": "Votos positivos", + "new-flags": "Novas denúncias", + "my-flags": "Denúncias atribuídas a mim", + "bans": "Banimentos", + "new_message_from": "Nova mensagem de %1", + "upvoted_your_post_in": "%1 votou de forma favorável na tua publicação em %2.", + "upvoted_your_post_in_dual": "%1 e %2 votaram favoravelmente à tua publicação em %3.", + "upvoted_your_post_in_multiple": "%1 e %2 outros utilizadores votaram favoravelmente na tua publicação em %3.", + "moved_your_post": "%1 moveu a tua publicação para %2", + "moved_your_topic": "%1 moveu %2", + "user_flagged_post_in": "%1 denunciou uma publicação em %2", + "user_flagged_post_in_dual": "%1 e %2 denunciaram uma publicação em %3", + "user_flagged_post_in_multiple": "%1 e %2 outros utilizadores denunciaram uma publicação em %3", + "user_flagged_user": "%1 denunciou um perfil de um utilizador (%2)", + "user_flagged_user_dual": "%1 e %2 denunciaram um perfil de um utilizador (%3)", + "user_flagged_user_multiple": "%1 e outros %2 utilizadores denunciaram um perfil de um utilizador (%3)", + "user_posted_to": "%1 publicou uma resposta a: %2", + "user_posted_to_dual": "%1 e %2 publicaram respostas a: %3", + "user_posted_to_multiple": "%1 e %2 outros utilizadores publicaram respostas a: %3", + "user_posted_topic": "%1 publicou um novo tópico: %2", + "user_edited_post": "%1 editou uma publicação em %2", + "user_started_following_you": "%1 começou a seguir-te.", + "user_started_following_you_dual": "%1 e %2 começaram a seguir-te.", + "user_started_following_you_multiple": "%1 e %2 outros utilizadores começaram a seguir-te.", + "new_register": "%1 enviou um pedido de registro.", + "new_register_multiple": "Existem %1 pedidos de registro aguardando pela tua revisão.", + "flag_assigned_to_you": "A denúncia %1 foi atribuída a ti", + "post_awaiting_review": "Publicação a aguardar revisão", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-mail confirmado", + "email-confirmed-message": "Obrigado por validares o teu endereço de e-mail. A tua conta está agora totalmente ativa.", + "email-confirm-error-message": "Ocorreu um problema a validar o teu endereço de e-mail. Talvez o código seja inválido ou já tenha expirado.", + "email-confirm-sent": "E-mail de confirmação enviado.", + "none": "Nada", + "notification_only": "Apenas Notificação", + "email_only": "Apenas E-mail", + "notification_and_email": "Notificação e E-mail", + "notificationType_upvote": "Quando alguém vota positivamente numa publicação tua", + "notificationType_new-topic": "Quando alguém que tu segues publica um tópico", + "notificationType_new-reply": "Quando uma nova resposta é publicada num tópico que tu estás a seguir", + "notificationType_post-edit": "Quando uma publicação é editada num tópico que estás a seguir", + "notificationType_follow": "Quando alguém começa a seguir-te", + "notificationType_new-chat": "Quando recebes uma mensagem numa conversa", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "Quando recebes um convite para um grupo", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "Quando alguém pede para entrar num grupo que é teu", + "notificationType_new-register": "Quando alguém é adicionado à fila de espera de registo", + "notificationType_post-queue": "Quando uma nova publicação está à espera de aprovação", + "notificationType_new-post-flag": "Quando uma publicação é denunciada", + "notificationType_new-user-flag": "Quando um utilizador é denunciado" +} \ No newline at end of file diff --git a/public/language/pt-PT/pages.json b/public/language/pt-PT/pages.json new file mode 100644 index 0000000000..3651a0c886 --- /dev/null +++ b/public/language/pt-PT/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Página inicial", + "unread": "Tópicos por ler", + "popular-day": "Tópicos populares de hoje", + "popular-week": "Tópicos populares esta semana", + "popular-month": "Tópicos populares este mês", + "popular-alltime": "Tópicos populares desde sempre", + "recent": "Tópicos recentes", + "top-day": "Tópicos mais votados de hoje", + "top-week": "Tópicos mais votados desta semana", + "top-month": "Tópicos mais votados deste mês", + "top-alltime": "Tópicos Mais Votados", + "moderator-tools": "Ferramentas de Moderador", + "flagged-content": "Conteúdo denunciado", + "ip-blacklist": "Lista negra de IPs", + "post-queue": "Publicações por Aprovar", + "users/online": "Utilizadores online", + "users/latest": "Utilizadores Recentes", + "users/sort-posts": "Utilizadores com mais publicações", + "users/sort-reputation": "Utilizadores com a reputação mais elevada", + "users/banned": "Utilizadores banidos", + "users/most-flags": "Utilizadores mais denunciados", + "users/search": "Pesquisa por utilizadores", + "notifications": "Notificações", + "tags": "Marcadores", + "tag": "Tópicos marcados sobre "%1"", + "register": "Registar uma conta", + "registration-complete": "Registro completo", + "login": "Inicia sessão na tua conta", + "reset": "Reinicia a tua palavra-passe", + "categories": "Categorias", + "groups": "Grupos", + "group": "%1 group", + "chats": "Conversas", + "chat": "Conversando com %1", + "flags": "Denúncias", + "flag-details": "Detalhes da denúncia %1", + "account/edit": "Editando \"%1\"", + "account/edit/password": "Editando palavra-passe de \"%1\"", + "account/edit/username": "Editando o nome de utilizador de \"%1\"", + "account/edit/email": "Editando o e-mail de \"%1\"", + "account/info": "Informação de conta", + "account/following": "Pessoas %1 que segue", + "account/followers": "Pessoas que seguem %1", + "account/posts": "Publicações feitas por %1", + "account/latest-posts": "Últimas publicações feitas por %1", + "account/topics": "Tópicos criados por %1", + "account/groups": "Grupos de %1", + "account/watched_categories": "Categorias subscritas por %1", + "account/bookmarks": "Publicações marcadas de %1", + "account/settings": "Definições de utilizador", + "account/watched": "Tópicos subscritos por %1", + "account/ignored": "Tópicos ignorados por %1", + "account/upvoted": "Publicações votadas favoravelmente por %1", + "account/downvoted": "Publicações votadas negativamente por %1", + "account/best": "Melhores publicações feitas por %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Utilizadores bloqueados para %1", + "account/uploads": "Carregamentos feitos por %1", + "account/sessions": "Sessões ativas", + "confirm": "E-mail confirmado", + "maintenance.text": "%1 está atualmente sobre manutenção.
Por favor, volta noutra altura.", + "maintenance.messageIntro": "Adicionalmente, o administrador deixou esta mensagem:", + "throttled.text": "%1 não está disponível de momento devido a um carregamento excesso. Por favor, volta mais tarde." +} \ No newline at end of file diff --git a/public/language/pt-PT/post-queue.json b/public/language/pt-PT/post-queue.json new file mode 100644 index 0000000000..386b3a4714 --- /dev/null +++ b/public/language/pt-PT/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Publicações por Aprovar", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "Utilizador", + "category": "Categoria", + "title": "Título", + "content": "Conteúdo", + "posted": "Publicada", + "reply-to": "Responder a \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/pt-PT/recent.json b/public/language/pt-PT/recent.json new file mode 100644 index 0000000000..e580776804 --- /dev/null +++ b/public/language/pt-PT/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recentes", + "day": "Dia", + "week": "Semana", + "month": "Mês", + "year": "Ano", + "alltime": "Desde sempre", + "no_recent_topics": "Não existem tópicos recentes.", + "no_popular_topics": "Não existem tópicos populares.", + "there-is-a-new-topic": "Existe um novo tópico.", + "there-is-a-new-topic-and-a-new-post": "Existe um novo tópico e uma nova publicação.", + "there-is-a-new-topic-and-new-posts": "Existe um tópico novo e %1 publicações novas.", + "there-are-new-topics": "Existem %1 tópicos novos.", + "there-are-new-topics-and-a-new-post": "Existem %1 tópicos novos e uma nova publicação.", + "there-are-new-topics-and-new-posts": "Existem %1 tópicos novos e %2 publicações novas.", + "there-is-a-new-post": "Existe uma publicação nova.", + "there-are-new-posts": "Existem %1 novas publicações.", + "click-here-to-reload": "Carrega aqui para recarregar." +} \ No newline at end of file diff --git a/public/language/pt-PT/register.json b/public/language/pt-PT/register.json new file mode 100644 index 0000000000..ebfa6647d9 --- /dev/null +++ b/public/language/pt-PT/register.json @@ -0,0 +1,32 @@ +{ + "register": "Regista-te", + "cancel_registration": "Cancelar o registro", + "help.email": "Por definição, o teu e-mail será oculto do público.", + "help.username_restrictions": "Um nome de utilizador único entre %1 e %2 caracteres. Outros podem mencionar-te através de @nome de utilizador.", + "help.minimum_password_length": "O comprimento da palavra-passe deve ter no mínimo %1 caracteres.", + "email_address": "Endereço de e-mail", + "email_address_placeholder": "Insere o endereço de e-mail", + "username": "Nome de utilizador", + "username_placeholder": "Inserir nome de utilizador", + "password": "Palavra-passe", + "password_placeholder": "Insere a palavra-passe", + "confirm_password": "Confirmar palavra-passe", + "confirm_password_placeholder": "Confirmar palavra-passe", + "register_now_button": "Regista-te agora", + "alternative_registration": "Registro alternativo", + "terms_of_use": "Termos de utilização", + "agree_to_terms_of_use": "Eu aceito os Termos de Utilização", + "terms_of_use_error": "Deves aceitar os Termos de Utilização", + "registration-added-to-queue": "O teu registo foi adicionado à fila de aprovação. Receberás um e-mail quando fores aceite por um administrador.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Autorizo a recolha e o processamento das minhas informações pessoais neste website.", + "gdpr_agree_email": "Aceito receber e-mails de resumo e de notificações deste website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/pt-PT/reset_password.json b/public/language/pt-PT/reset_password.json new file mode 100644 index 0000000000..0f505e0d4b --- /dev/null +++ b/public/language/pt-PT/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Reinicia a palavra-passe", + "update_password": "Altera a palavra-passe", + "password_changed.title": "Palavra-passe alterada", + "password_changed.message": "

Palavra-passe reposta com sucesso, por favor inicia sessão outra vez.", + "wrong_reset_code.title": "Código de reiniciação incorreto", + "wrong_reset_code.message": "O código de reinício recebido estava incorreto. Por favor, tenta novamente ou pede um novo código.", + "new_password": "Nova palavra-passe", + "repeat_password": "Confirmar palavra-passe", + "changing_password": "Changing Password", + "enter_email": "Por favor, insere o teu endereço de e-mail e nós iremos enviar-te um e-mail com instruções para reiniciares a tua conta.", + "enter_email_address": "Insere o endereço de 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.", + "invalid_email": "E-mail inválido / E-mail não existe!", + "password_too_short": "A palavra-passe inserida é demasiado pequena. Por favor, escolhe uma nova.", + "passwords_do_not_match": "As duas palavras-passe que inseriste não coincidem.", + "password_expired": "A tua palavra-passe expirou, por favor escolher uma nova" +} \ No newline at end of file diff --git a/public/language/pt-PT/search.json b/public/language/pt-PT/search.json new file mode 100644 index 0000000000..ea0eb7fab5 --- /dev/null +++ b/public/language/pt-PT/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 resultado(s) correspondendo \"%2\", (%3 segundos)", + "no-matches": "Não foram encontradas correspondências", + "advanced-search": "Pesquisa avançada", + "in": "Em", + "titles": "Títulos", + "titles-posts": "Títulos e Publicações", + "match-words": "Corresponder palavras", + "all": "Todas", + "any": "Qualquer", + "posted-by": "Publicado por", + "in-categories": "Nas Categorias", + "search-child-categories": "Procurar categorias infantis", + "has-tags": "Tem marcadores", + "reply-count": "Quantidade de respostas", + "at-least": "Pelo menos", + "at-most": "No máximo", + "relevance": "Relevância", + "post-time": "Hora da publicação", + "votes": "Votos", + "newer-than": "Mais recente que", + "older-than": "Mais antigo que", + "any-date": "Qualquer data", + "yesterday": "Ontem", + "one-week": "Uma semana", + "two-weeks": "Duas semanas", + "one-month": "Um mês", + "three-months": "Três meses", + "six-months": "Seis meses", + "one-year": "Um ano", + "sort-by": "Ordenar por", + "last-reply-time": "Tempo da última resposta", + "topic-title": "Título do tópico", + "topic-votes": "Votos dos tópicos", + "number-of-replies": "Número de respostas", + "number-of-views": "Número de visualizações", + "topic-start-date": "Data de início do tópico", + "username": "Nome de utilizador", + "category": "Categoria", + "descending": "Em ordem descendente", + "ascending": "Em ordem ascendente", + "save-preferences": "Guardar preferências", + "clear-preferences": "Limpar preferências", + "search-preferences-saved": "Preferências de pesquisa guardadas", + "search-preferences-cleared": "Preferências de pesquisa gravadas", + "show-results-as": "Mostrar resultados como", + "see-more-results": "Ver mais resultados (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/pt-PT/success.json b/public/language/pt-PT/success.json new file mode 100644 index 0000000000..c1a5e6c17a --- /dev/null +++ b/public/language/pt-PT/success.json @@ -0,0 +1,7 @@ +{ + "success": "Sucesso", + "topic-post": "Publicaste com sucesso.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Autenticação Bem Sucedida", + "settings-saved": "Configurações guardadas!" +} \ No newline at end of file diff --git a/public/language/pt-PT/tags.json b/public/language/pt-PT/tags.json new file mode 100644 index 0000000000..44b1ac7bdd --- /dev/null +++ b/public/language/pt-PT/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Não existem tópicos com estes marcadores.", + "tags": "Marcadores", + "enter_tags_here": "Insere os marcadores aqui, cada um com %1 a %2 caracteres.", + "enter_tags_here_short": "Insere marcadores...", + "no_tags": "Ainda não existem marcadores.", + "select_tags": "Selecionar Marcadores" +} \ No newline at end of file diff --git a/public/language/pt-PT/top.json b/public/language/pt-PT/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/pt-PT/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/pt-PT/topic.json b/public/language/pt-PT/topic.json new file mode 100644 index 0000000000..63a83a2baa --- /dev/null +++ b/public/language/pt-PT/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tópico", + "title": "Title", + "no_topics_found": "Tópicos não encontrados!", + "no_posts_found": "Publicações não encontradas!", + "post_is_deleted": "Esta publicação foi eliminada!", + "topic_is_deleted": "Este tópico foi eliminado!", + "profile": "Perfil", + "posted_by": "Publicado por %1", + "posted_by_guest": "Publicado por Convidado", + "chat": "Conversas", + "notify_me": "Ser notificado de novas respostas neste tópico", + "quote": "Citar", + "reply": "Responder", + "replies_to_this_post": "%1 Respostas", + "one_reply_to_this_post": "1 Resposta", + "last_reply_time": "Última resposta", + "reply-as-topic": "Responder com um tópico", + "guest-login-reply": "Inicia sessão para responder", + "login-to-view": "🔒 Inicia sessão para veres", + "edit": "Editar", + "delete": "Apagar", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Eliminar", + "restore": "Restaurar", + "move": "Mover", + "change-owner": "Alterar Proprietário", + "fork": "Clonar", + "link": "Link", + "share": "Partilhar", + "tools": "Ferramentas", + "locked": "Bloqueado", + "pinned": "Afixado", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Movido", + "moved-from": "Moved from %1", + "copy-ip": "Copiar IP", + "ban-ip": "Banir IP", + "view-history": "Histórico de Edição", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Carrega aqui para voltares à última publicação lide assunto.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "Este tópico foi fundido com %2", + "deleted_message": "Este tópico foi eliminado. Apenas utilizadores com privilégios de moderação do tópico podem vê-lo.", + "following_topic.message": "A partir de agora receberás uma notificação sempre que alguém publicar neste tópico.", + "not_following_topic.message": "Verás este tópico na lista de tópicos por ler mas não irás receber notificações quando alguém publicar neste tópico.", + "ignoring_topic.message": "Não verás mais este tópico na tua lista de tópicos por ler. Serás notificado sempre que fores mencionado ou o teu tópico seja votado favoravelmente.", + "login_to_subscribe": "Por favor regista-te ou inicia sessão para subscreveres este tópico.", + "markAsUnreadForAll.success": "Tópico marcado como \"não lido\" para todos.", + "mark_unread": "Marcar como não lido", + "mark_unread.success": "Tópico marcado como \"não lido\".", + "watch": "Ver", + "unwatch": "Marcar como não visto", + "watch.title": "Ser notificado de novas respostas neste tópicos", + "unwatch.title": "Parar de seguir este tópico", + "share_this_post": "Partilhar esta publicação", + "watching": "Seguir", + "not-watching": "Não seguir", + "ignoring": "Ignorar", + "watching.description": "Notificar-me sobre novas respostas.
Mostrar o tópico em \"não lidos\".", + "not-watching.description": "Não me notificar de novas respostas.
Mostrar tópico em \"não lidos\" caso a categoria não esteja a ser ignorada.", + "ignoring.description": "Não me notificar de novas respostas.
Não mostrar este tópico em \"não lidos\".", + "thread_tools.title": "Ferramentas de tópicos", + "thread_tools.markAsUnreadForAll": "Marcar como não lido para todos", + "thread_tools.pin": "Fixar tópico", + "thread_tools.unpin": "Desafixar tópico", + "thread_tools.lock": "Bloquear tópico", + "thread_tools.unlock": "Desbloquear tópico", + "thread_tools.move": "Mover tópico", + "thread_tools.move-posts": "Mover publicações", + "thread_tools.move_all": "Mover todos", + "thread_tools.change_owner": "Alterar Proprietário", + "thread_tools.select_category": "Selecionar Categoria", + "thread_tools.fork": "Clonar tópico", + "thread_tools.delete": "Apagar Tópico", + "thread_tools.delete-posts": "Apagar publicações", + "thread_tools.delete_confirm": "Tens a certeza que desejas apagar este tópico?", + "thread_tools.restore": "Restaurar tópico", + "thread_tools.restore_confirm": "Tens a certeza que pretendes restaurar este tópico?", + "thread_tools.purge": "Eliminar tópico", + "thread_tools.purge_confirm": "Tens a certeza que queres eliminar este tópico?", + "thread_tools.merge_topics": "Fundir Tópicos", + "thread_tools.merge": "Fundir", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Tens a certeza que desejas eliminar esta publicação?", + "post_restore_confirm": "Tens a certeza que desejas restaurar esta publicação?", + "post_purge_confirm": "Tens a certeza que queres eliminar esta publicação?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Carregando Categorias", + "confirm_move": "Mover", + "confirm_fork": "Clonar", + "bookmark": "Marcador", + "bookmarks": "Marcadores", + "bookmarks.has_no_bookmarks": "Ainda não marcaste nenhuma publicação.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Carregando mais publicações", + "move_topic": "Mover tópico", + "move_topics": "Mover tópicos", + "move_post": "Mover publicação", + "post_moved": "Publicação movida!", + "fork_topic": "Clonar tópico", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Carrega nas publicações que queres clonar", + "fork_no_pids": "Sem publicações selecionadas!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 publicação(ões) selecionada(s)", + "fork_success": "Clonaste um tópico com sucesso! Carrega aqui para ires para o tópico clonado.", + "delete_posts_instruction": "Carrega nas publicações que queres apagar/eliminar", + "merge_topics_instruction": "Clica nos tópicos que queres fundir ou pesquisa por eles", + "merge-topic-list-title": "Lista de tópicos a serem fundidos", + "merge-options": "Opções de fusão", + "merge-select-main-topic": "Seleciona o tópico principal", + "merge-new-title-for-topic": "Novo título para o tópico", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Insere aqui o título do tópico...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Descartar", + "composer.submit": "Publicar", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Respondendo a %1", + "composer.new_topic": "Novo tópico", + "composer.editing": "Editing", + "composer.uploading": "carregando...", + "composer.thumb_url_label": "Cola um URL da miniatura do tópico", + "composer.thumb_title": "Adiciona uma miniatura a este tópico", + "composer.thumb_url_placeholder": "http://exemplo.com/dedo.png", + "composer.thumb_file_label": "Ou carrega um ficheiro", + "composer.thumb_remove": "Limpar os campos", + "composer.drag_and_drop_images": "Arrasta e larga imagens aqui", + "more_users_and_guests": "mais %1 utilizador(es) e %2 convidado(s)", + "more_users": "mais %1 utilizador(es)", + "more_guests": "mais %1 convidado(s)", + "users_and_others": "%1 e mais %2", + "sort_by": "Dispor por", + "oldest_to_newest": "Do mais antigo para o mais recente", + "newest_to_oldest": "Mais recente para mais antigo", + "most_votes": "Mais votos", + "most_posts": "Mais publicações", + "most_views": "Most Views", + "stale.title": "Em vez disso, criar novo tópico?", + "stale.warning": "O tópico ao qual estás a responder é bastante antigo. Gostarias antes de criar um novo tópico e referir este na tua resposta?", + "stale.create": "Criar um novo tópico", + "stale.reply_anyway": "Responder a este tópico à mesma", + "link_back": "Referindo: [%1](%2)", + "diffs.title": "Histórico de Edição da Publicação", + "diffs.description": "Esta publicação tem %1 revisões. Clica numa das revisões abaixo para veres o conteúdo da publicação naquele momento.", + "diffs.no-revisions-description": "Esta publicação tem %1 revisões.", + "diffs.current-revision": "revisão atual", + "diffs.original-revision": "revisão original", + "diffs.restore": "Restaurar esta revisão", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 depois", + "timeago_earlier": "%1 antes", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/pt-PT/unread.json b/public/language/pt-PT/unread.json new file mode 100644 index 0000000000..b70992b682 --- /dev/null +++ b/public/language/pt-PT/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Por ler", + "no_unread_topics": "Não existem tópicos por ler.", + "load_more": "Carregar mais", + "mark_as_read": "Marcar como lido", + "selected": "Selecionados", + "all": "Todos", + "all_categories": "Todas as categorias", + "topics_marked_as_read.success": "Tópicos marcados como lidos!", + "all-topics": "Todos os tópicos", + "new-topics": "Novos tópicos", + "watched-topics": "Tópicos vistos", + "unreplied-topics": "Tópicos sem respostas", + "multiple-categories-selected": "Vários Selecionados" +} \ No newline at end of file diff --git a/public/language/pt-PT/uploads.json b/public/language/pt-PT/uploads.json new file mode 100644 index 0000000000..c06ae45b06 --- /dev/null +++ b/public/language/pt-PT/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Carregando o ficheiro...", + "select-file-to-upload": "Seleciona um ficheiro para carregar!", + "upload-success": "Ficheiro enviado com sucesso!", + "maximum-file-size": "Máximo de %1 kb", + "no-uploads-found": "Não foram encontrados carregamentos", + "public-uploads-info": "Os carregamentos estão públicos, todos os visitantes os podem ver.", + "private-uploads-info": "Os carregamentos estão privados, apenas utilizadores com sessão iniciada os vão conseguir ver." +} \ No newline at end of file diff --git a/public/language/pt-PT/user.json b/public/language/pt-PT/user.json new file mode 100644 index 0000000000..87d078cde9 --- /dev/null +++ b/public/language/pt-PT/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banido", + "muted": "Muted", + "offline": "Offline", + "deleted": "Apagar", + "username": "Nome de utilizador", + "joindate": "Data de Adesão", + "postcount": "Quantidade de publicações", + "email": "E-mail", + "confirm_email": "Confirmar o e-mail", + "account_info": "Informação de conta", + "admin_actions_label": "Ações Administrativas", + "ban_account": "Banir conta", + "ban_account_confirm": "Queres realmente banir este utilizador?", + "unban_account": "Deixar de banir esta conta", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Eliminar conta", + "delete_account_as_admin": "Eliminar Conta", + "delete_content": "Eliminar Conteúdos da Conta", + "delete_all": "Eliminar Conta e respetivos Conteúdos", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Conta eliminada", + "account-content-deleted": "Conteúdos da conta eliminados", + "fullname": "Nome completo", + "website": "Website", + "location": "Localização", + "age": "Idade", + "joined": "Aderiu", + "lastonline": "Última vez online", + "profile": "Perfil", + "profile_views": "Visualizações ao perfil", + "reputation": "Reputação", + "bookmarks": "Marcadores", + "watched_categories": "Categorias subscritas", + "change_all": "Change All", + "watched": "Subscritos", + "ignored": "Ignorados", + "default-category-watch-state": "Estado predefinido da subscrição de categorias", + "followers": "Seguidores", + "following": "Seguindo", + "blocks": "Bloqueados", + "block_toggle": "Ativar Bloqueio", + "block_user": "Bloquear Utilizador", + "unblock_user": "Desbloquear Utilizador", + "aboutme": "Sobre mim", + "signature": "Assinatura", + "birthday": "Data de nascimento", + "chat": "Conversa", + "chat_with": "Continuar a conversa com %1", + "new_chat_with": "Começa nova conversa com %1", + "flag-profile": "Denunciar Perfil", + "follow": "Segue", + "unfollow": "Deixar de seguir", + "more": "Mais", + "profile_update_success": "Perfil foi atualizado com sucesso!", + "change_picture": "Alterar fotografia", + "change_username": "Alterar Nome de Utilizador", + "change_email": "Alterar E-mail", + "email_same_as_password": "Por favor, insere a tua palavra-passe atual para continuar – tu inseriste o teu novo e-mail novamente", + "edit": "Editar", + "edit-profile": "Editar perfil", + "default_picture": "Icon pré-definido", + "uploaded_picture": "Fotografia enviada", + "upload_new_picture": "Enviar uma nova imagem", + "upload_new_picture_from_url": "Enviar uma nova imagem através de um URL", + "current_password": "Palavra-passe atual", + "change_password": "Palavra-passe alterada", + "change_password_error": "Palavra-passe Inválida!", + "change_password_error_wrong_current": "A tua palavra-passe atual não está correta!", + "change_password_error_match": "As palavras-passe devem coincidir!", + "change_password_error_privileges": "Não tens os direitos necessários para alterar esta palavra-passe.", + "change_password_success": "A tua palavra-passe foi atualizada!", + "confirm_password": "Confirmar palavra-passe", + "password": "Palavra-passe", + "username_taken_workaround": "O nome de utilizador que escolheste já está em utilização por isso alteramo-lo ligeiramente. És agora conhecido como %1", + "password_same_as_username": "A tua palavra-passe é igual ao teu nome de utilizador. Por favor, escolhe outra palavra-passe.", + "password_same_as_email": "A tua palavra-passe é a mesma que o teu e-mail. Por favor, escolhe outra palavra-passe.", + "weak_password": "Palavra-passe fraca.", + "upload_picture": "Enviar imagem", + "upload_a_picture": "Enviar uma imagem", + "remove_uploaded_picture": "Remover Imagem Enviada", + "upload_cover_picture": "Enviar imagem de capa", + "remove_cover_picture_confirm": "Tens a certeza que queres remover a imagem de capa?", + "crop_picture": "Cortar imagem", + "upload_cropped_picture": "Cortar e enviar", + "avatar-background-colour": "Avatar background colour", + "settings": "Definições", + "show_email": "Mostrar o meu e-mail", + "show_fullname": "Mostrar o meu nome completo", + "restrict_chats": "Permitir apenas mensagens de utilizadores que eu sigo", + "digest_label": "Subscrever o resumo", + "digest_description": "Subscrever atualizações por e-mail para este fórum (novas notificações e tópicos) de acordo com um horário definido", + "digest_off": "Desligado", + "digest_daily": "Diariamente ", + "digest_weekly": "Semanalmente", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Mensalmente", + "has_no_follower": "Este utilizador não tem nenhum seguidor :(", + "follows_no_one": "Este utilizador não está a seguir ninguém :(", + "has_no_posts": "Este utilizador ainda não publicou nada.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Este utilizar ainda não publicou nenhum tópico.", + "has_no_watched_topics": "Este utilizador ainda não subscreveu nenhum tópico até ao momento.", + "has_no_ignored_topics": "Este utilizador ainda não ignorou nenhum tópico.", + "has_no_upvoted_posts": "Este utilizador ainda não votou favoravelmente em nenhuma publicação.", + "has_no_downvoted_posts": "Este utilizador ainda não votou negativamente em nenhuma publicação.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Não bloqueaste nenhum utilizador.", + "email_hidden": "E-mail escondido", + "hidden": "Escondido", + "paginate_description": "Paginar os tópicos e publicações em vez de usar o scroll infinito", + "topics_per_page": "Tópicos por página", + "posts_per_page": "Publicações por página", + "max_items_per_page": "Máximo %1", + "acp_language": "Idioma da Página de Administração", + "notifications": "Notifications", + "upvote-notif-freq": "Frequência das Notificações de Votos Positivos", + "upvote-notif-freq.all": "Todos os votos positivos", + "upvote-notif-freq.first": "Apenas o primeiro por publicação", + "upvote-notif-freq.everyTen": "A cada 10 votos positivos", + "upvote-notif-freq.threshold": "Aos 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Aos 10, 100, 1000...", + "upvote-notif-freq.disabled": "Desativado", + "browsing": "Definições de navegação", + "open_links_in_new_tab": "Abrir links externos num novo separador", + "enable_topic_searching": "Permitir pesquisa dentro dos tópicos", + "topic_search_help": "Se ativada, a pesquisa dentro de tópicos irá sobrepor-se ao comportamento normal de pesquisa do browser pré-definido e irá permitir-te pesquisar ao longo de todo o tópico, em vez de pesquisar somente no que é mostrado no ecrã", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Depois de publicar uma resposta, mostrar a nova publicação", + "follow_topics_you_reply_to": "Visualizar tópicos aos quais respondeste", + "follow_topics_you_create": "Visualizar tópicos que criaste", + "grouptitle": "Título do grupo", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Sem título de grupo", + "select-skin": "Seleciona uma máscara", + "select-homepage": "Seleciona a página inicial", + "homepage": "Página Inicial", + "homepage_description": "Seleciona a página que irás usar como página inicial do fórum ou \"Nenhuma\" para usar a página inicial por defeito", + "custom_route": "Rota para a página inicial personalizada", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Serviços de login único", + "sso.associated": "Associado a", + "sso.not-associated": "Carrega aqui para associares com", + "sso.dissociate": "Dissociar", + "sso.dissociate-confirm-title": "Confirmar Dissociação", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Denúncias Recentes", + "info.no-flags": "Não foram encontradas publicações denunciadas", + "info.ban-history": "Histórico de expulsões recentes", + "info.no-ban-history": "Este utilizador nunca foi banido", + "info.banned-until": "Banido até %1", + "info.banned-expiry": "Expiração", + "info.banned-permanently": "Banido permanentemente", + "info.banned-reason-label": "Razão", + "info.banned-no-reason": "Sem razão atribuida.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Históricos do nome de utilizador", + "info.email-history": "Histórico de e-mail", + "info.moderation-note": "Nota de moderação", + "info.moderation-note.success": "Nota de moderação guardada", + "info.moderation-note.add": "Adicionar nota", + "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.", + "consent.title": "Direitos e privacidade", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Dar permissão", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Exportar Perfil (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Exportar Arquivos Enviados (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Exportar Publicações (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/pt-PT/users.json b/public/language/pt-PT/users.json new file mode 100644 index 0000000000..d8ee810a8a --- /dev/null +++ b/public/language/pt-PT/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Utilizadores Recentes", + "top_posters": "Top de publicadores", + "most_reputation": "Maior Reputação", + "most_flags": "Mais Denúncias", + "search": "Procurar", + "enter_username": "Insere um nome de utilizador para pesquisar", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Carregar mais", + "users-found-search-took": "%1 utilizador(es) encontrados! A pesquisa demorou %2 segundos.", + "filter-by": "Filtrar por", + "online-only": "Só online", + "invite": "Convidar", + "prompt-email": "E-mails", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Foi enviado um e-mail de convite para %1", + "user_list": "Lista de utilizadores", + "recent_topics": "Tópicos recentes", + "popular_topics": "Tópicos populares", + "unread_topics": "Tópicos por ler", + "categories": "Categorias", + "tags": "Marcadores", + "no-users-found": "Não foram encontrados utilizadores!" +} \ No newline at end of file diff --git a/public/language/ro/_DO_NOT_EDIT_FILES_HERE.md b/public/language/ro/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/ro/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/ro/admin/admin.json b/public/language/ro/admin/admin.json new file mode 100644 index 0000000000..39edffb66f --- /dev/null +++ b/public/language/ro/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/ro/admin/advanced/cache.json b/public/language/ro/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/ro/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/ro/admin/advanced/database.json b/public/language/ro/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/ro/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/ro/admin/advanced/errors.json b/public/language/ro/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/ro/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/ro/admin/advanced/events.json b/public/language/ro/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/ro/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/ro/admin/advanced/logs.json b/public/language/ro/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/ro/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/ro/admin/appearance/customise.json b/public/language/ro/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/ro/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/ro/admin/appearance/skins.json b/public/language/ro/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/ro/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/ro/admin/appearance/themes.json b/public/language/ro/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/ro/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/ro/admin/dashboard.json b/public/language/ro/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/ro/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/ro/admin/development/info.json b/public/language/ro/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/ro/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/ro/admin/development/logger.json b/public/language/ro/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/ro/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/ro/admin/extend/plugins.json b/public/language/ro/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/ro/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/ro/admin/extend/rewards.json b/public/language/ro/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/ro/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/ro/admin/extend/widgets.json b/public/language/ro/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/ro/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/admins-mods.json b/public/language/ro/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/ro/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/categories.json b/public/language/ro/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/ro/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/digest.json b/public/language/ro/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/ro/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/ro/admin/manage/groups.json b/public/language/ro/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/ro/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/privileges.json b/public/language/ro/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/ro/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/registration.json b/public/language/ro/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/ro/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/tags.json b/public/language/ro/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/ro/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/uploads.json b/public/language/ro/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/ro/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/ro/admin/manage/users.json b/public/language/ro/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/ro/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/ro/admin/menu.json b/public/language/ro/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/ro/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/advanced.json b/public/language/ro/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/ro/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/api.json b/public/language/ro/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/ro/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/chat.json b/public/language/ro/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/ro/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/cookies.json b/public/language/ro/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/ro/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/email.json b/public/language/ro/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/ro/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/ro/admin/settings/general.json b/public/language/ro/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/ro/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/ro/admin/settings/group.json b/public/language/ro/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/ro/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/guest.json b/public/language/ro/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/ro/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/homepage.json b/public/language/ro/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/ro/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/languages.json b/public/language/ro/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/ro/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/navigation.json b/public/language/ro/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/ro/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/ro/admin/settings/notifications.json b/public/language/ro/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/ro/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/pagination.json b/public/language/ro/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/ro/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/post.json b/public/language/ro/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/ro/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/reputation.json b/public/language/ro/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/ro/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/social.json b/public/language/ro/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/ro/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/sockets.json b/public/language/ro/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/ro/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/sounds.json b/public/language/ro/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/ro/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/tags.json b/public/language/ro/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/ro/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/ro/admin/settings/uploads.json b/public/language/ro/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/ro/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/ro/admin/settings/user.json b/public/language/ro/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/ro/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/ro/admin/settings/web-crawler.json b/public/language/ro/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/ro/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/ro/category.json b/public/language/ro/category.json new file mode 100644 index 0000000000..1e5659f5e8 --- /dev/null +++ b/public/language/ro/category.json @@ -0,0 +1,23 @@ +{ + "category": "Categorie", + "subcategories": "Subcategorii", + "new_topic_button": "Subiect Nou", + "guest-login-post": "Conecteaza-te pentru a posta", + "no_topics": "Nu există nici un subiect de discuție în această categorie.
De ce nu încerci să postezi tu unul?", + "browsing": "navighează", + "no_replies": "Nu a răspuns nimeni", + "no_new_posts": "Nici o postare nouă", + "watch": "Urmărește", + "ignore": "Ignoră", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Categorii urmărite", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/ro/email.json b/public/language/ro/email.json new file mode 100644 index 0000000000..e805de385d --- /dev/null +++ b/public/language/ro/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Salutare lui %1", + "invite": "Invitație de la %1", + "greeting_no_name": "Salut", + "greeting_with_name": "Salut %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "îți mulțumim că te-ai Înregistrat cu %1!", + "welcome.text2": "Pentru a-ți activa cu success contul trebuie să verificăm adresa de email pe care ai folosit-o la înregistrare.", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "Apasă aici pentru a confirma adresa ta de email", + "invitation.text1": "%1 te-a invitat să te alături %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", + "reset.text2": "Pentru a continua cu resetarea parolei, te rugăm sa apeși pe următorul link:", + "reset.cta": "Apasă aici pentru a-ți reseta parola", + "reset.notify.subject": "Parola a fost schimbată cu succes", + "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", + "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "digest.latest_topics": "Ultimele mesaje de la %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Apasă aici pentru a vizita %1", + "digest.unsub.info": "This digest was sent to you due to your subscription settings.", + "digest.day": "zi", + "digest.week": "saptămână", + "digest.month": "lună", + "digest.subject": "Rezumat pentru %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Ai primit un mesaj de la %1", + "notif.chat.cta": "Apasă aici pentru a continua conversația", + "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Acesta este un email de test pentru a verica dacă mailul este setat corect pentru NodeBB-ul tău.", + "unsub.cta": "Apasă aici pentru a modifica acele setări", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Mulțumesc!" +} \ No newline at end of file diff --git a/public/language/ro/error.json b/public/language/ro/error.json new file mode 100644 index 0000000000..ed49f99971 --- /dev/null +++ b/public/language/ro/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Date invalide", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Se pare ca nu ești logat.", + "account-locked": "Contul tău a fost blocat temporar", + "search-requires-login": "Pentru a cauta ai nevoie de un cont. Logheaza-te sau autentifica-te.", + "goback": "Press back to return to the previous page", + "invalid-cid": "ID Categorie Invalid", + "invalid-tid": "ID Subiect Invalid", + "invalid-pid": "ID Mesaj Invalid", + "invalid-uid": "ID Utilizator Invalid", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Utilizator Invalid", + "invalid-email": "Email Invalid", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Date utilizator invalide", + "invalid-password": "Parolă Invalidă", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Te rugăm să specifici atât un nume de utilizator cât si o parolă", + "invalid-search-term": "Cuvânt de căutare invalid", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "Numele de utilizator este deja folosit", + "email-taken": "Adresa de email este deja folostă", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Nu vei putea trimite mesaje daca email-ul tau nu e confirmat, click aici sa il confirmi.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Mail-ul tau nu a putut fi confirmat, te rog incearca mai tarziu.", + "confirm-email-already-sent": "Email-ul de confirmare ti-a fost trimis, asteapta te rog %1 minut(e) ca sa trimiti inca unul.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Numele de utilizator este prea scurt", + "username-too-long": "Numele de utilizator este prea lung", + "password-too-long": "Parola prea lunga.", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Utilizator banat", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Imi pare rau dar trebuie sa astepti %1 secunda(e) pentru a posta prima oara.", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Categoria nu exista.", + "no-topic": "Topicul nu exista.", + "no-post": "Post-ul nu exista.", + "no-group": "Grupul nu exista.", + "no-user": "Utilizatorul nu exista.", + "no-teaser": "Teaser does not exist", + "no-flag": "Flag does not exist", + "no-privileges": "You do not have enough privileges for this action.", + "category-disabled": "Categorie dezactivată", + "topic-locked": "Subiect Închis", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", + "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Te rugăm să aștepți până se termină uploadul.", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Nu poți bana alți administratori!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Numele grupului este prea scurt", + "group-name-too-long": "Group name too long", + "group-already-exists": "Grupul deja există", + "group-name-change-not-allowed": "Schimbarea numelui grupului este interzisă", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "This post has already been deleted", + "post-already-restored": "This post has already been restored", + "topic-already-deleted": "This topic has already been deleted", + "topic-already-restored": "This topic has already been restored", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "Pictogramele pentru subiect sunt interzise.", + "invalid-file": "Fișier invalid", + "uploads-are-disabled": "Uploadurile sunt dezactivate", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "Nu poți conversa cu tine!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Sistemul de reputație este dezactivat.", + "downvoting-disabled": "Votarea negativă este dezactivată", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB a întâmpinat o problemă la reîncarcare: \"%1\". NodeBB va continua să servească fișierele existente pentru partea-client, dar tu va trebuie să refaci modificările pe care le-ai facut înainte de reîncarcare.", + "registration-error": "Registration Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/ro/flags.json b/public/language/ro/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/ro/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/ro/global.json b/public/language/ro/global.json new file mode 100644 index 0000000000..ebfa75e87f --- /dev/null +++ b/public/language/ro/global.json @@ -0,0 +1,126 @@ +{ + "home": "Acasă", + "search": "Căutare", + "buttons.close": "Închide", + "403.title": "Acces Interzis", + "403.message": "Se pare că ai ajuns pe o pagină la care nu ai acces", + "403.login": "Poate ar trebui să te autentifici?", + "404.title": "Nu a fost găsit", + "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "500.title": "Internal Error.", + "500.message": "Oops! Se pare că ceva a mers greșit!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Înregistrare", + "login": "Autentificare", + "please_log_in": "Autentifică-te", + "logout": "Ieşire", + "posting_restriction_info": "Pentru a posta trebuie să fi înregistrat. Apasă aici pentru a te atentifica.", + "welcome_back": "Bine ai revenit", + "you_have_successfully_logged_in": "Te-ai conectat cu succes", + "save_changes": "Salvează Modificări", + "save": "Save", + "close": "Închide", + "pagination": "Paginație", + "pagination.out_of": "%1 din %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Categorii", + "header.recent": "Recente", + "header.unread": "Necitite", + "header.tags": "Taguri", + "header.popular": "Populare", + "header.top": "Top", + "header.users": "Utilizatori", + "header.groups": "Grupuri", + "header.chats": "Conversații", + "header.notifications": "Notificări", + "header.search": "Căutare", + "header.profile": "Profil", + "header.navigation": "Navigare", + "notifications.loading": "Se încarcă notificările", + "chats.loading": "Se încarcă conversațiile", + "motd.welcome": "Bine ai venit la NodeBB, platforma de discuții a viitorului.", + "previouspage": "Pagina Precedentă", + "nextpage": "Următoarea Pagină", + "alert.success": "Succes", + "alert.error": "Eroare", + "alert.banned": "Banat", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Nu îl mai urmărești pe %1!", + "alert.follow": "Îl urmărești pe %1!", + "users": "Utilizatori", + "topics": "Subiecte", + "posts": "Mesaje", + "x-posts": "%1 posts", + "best": "Cel mai bun", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Vizualizări", + "posters": "Posters", + "reputation": "Reputație", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "citește mai mult", + "more": "Mai multe", + "none": "None", + "posted_ago_by_guest": "postat %1 de Vizitator", + "posted_ago_by": "postat %1 de %2", + "posted_ago": "postat %1", + "posted_in": "postat în %1", + "posted_in_by": "postat în %1 de %2", + "posted_in_ago": "postat în %1 %2", + "posted_in_ago_by": "postat în %1 %2 de %3", + "user_posted_ago": "%1 a postat %2", + "guest_posted_ago": "Vizitator a postat %1", + "last_edited_by": "ultima editare de %1", + "norecentposts": "Nici un mesaj recent", + "norecenttopics": "Nici un subiect recent", + "recentposts": "Mesaje Recente", + "recentips": "Adrese IP autentificate recent", + "moderator_tools": "Moderator Tools", + "online": "Conectat", + "away": "Plecat", + "dnd": "Nu mă deranja", + "invisible": "Invizibil", + "offline": "Deconectat", + "email": "Email", + "language": "Limbă", + "guest": "Vizitator", + "guests": "Vizitatori", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forumul a fost actualizat", + "updated.message": "Acest forum a fost actualizat la ultima version. Apasă aici pentru a reîmprospăta pagina.", + "privacy": "Intimitate", + "follow": "Urmăreşte", + "unfollow": "Nu mai urmări", + "delete_all": "Şterge Tot", + "map": "Hartă", + "sessions": "Ședința de login", + "ip_address": "Adresa IP", + "enter_page_number": "Introdu numărul paginei", + "upload_file": "Încărcați fișierul", + "upload": "Încărcați", + "uploads": "Uploads", + "allowed-file-types": "Tipuri de fișiere permise sunt %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/ro/groups.json b/public/language/ro/groups.json new file mode 100644 index 0000000000..4c944b733c --- /dev/null +++ b/public/language/ro/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupuri", + "view_group": "Vezi Grup", + "owner": "Propietar de group", + "new_group": "Crează un grup nou", + "no_groups_found": "Nu sunt grupuri de văzut", + "pending.accept": "Acceptă", + "pending.reject": "Respinge", + "pending.accept_all": "Acceptă toate", + "pending.reject_all": "Respinge toate", + "pending.none": "Momentan nu există membrii în așteptare", + "invited.none": "Momentan nu există membrii invitați", + "invited.uninvite": "Anulează invitația", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "Ai fost invitat să te alături %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Salvează", + "cover-saving": "Salvez", + "details.title": "Detalii Grup", + "details.members": "Listă Membrii", + "details.pending": "Membrii în așteptare", + "details.invited": "Membrii invitați", + "details.has_no_posts": "Membrii acestui grup nu au facut nici o postare.", + "details.latest_posts": "Ultimele Mesaje", + "details.private": "Privat", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Administrarea grupului", + "details.group_name": "Numele grupului", + "details.member_count": "Număr de membrii", + "details.creation_date": "Data creării", + "details.description": "Descriere", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Previzualizarea insignei", + "details.change_icon": "Schimbă icoana", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Textul insignei", + "details.userTitleEnabled": "Arată insigna", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Ascuns", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Șterge grupul", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Detaliile grupului au fost actualizate", + "event.deleted": "Grupul %1\" a fost șters", + "membership.accept-invitation": "Acceptă invitația", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitație in așteptare", + "membership.join-group": "Alăture-te grupului", + "membership.leave-group": "Părăsește grupul", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Respinge", + "new-group.group_name": "Numele grupului:", + "upload-group-cover": "Încarcă coperta de grup", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/ro/ip-blacklist.json b/public/language/ro/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/ro/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/ro/language.json b/public/language/ro/language.json new file mode 100644 index 0000000000..671c4dc6d6 --- /dev/null +++ b/public/language/ro/language.json @@ -0,0 +1,5 @@ +{ + "name": "Română (România)", + "code": "ro", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/ro/login.json b/public/language/ro/login.json new file mode 100644 index 0000000000..2d71d22e51 --- /dev/null +++ b/public/language/ro/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Utilizator/Email", + "username": "Utilizator", + "remember_me": "Autentifică-mă automat la fiecare vizită", + "forgot_password": "Ai uitat parola?", + "alternative_logins": "Autentificare Alternativă", + "failed_login_attempt": "Login nereușit", + "login_successful": "Te-ai autentificat cu succes!", + "dont_have_account": "Nu ai un cont?", + "logged-out-due-to-inactivity": "Ai fost deconectat din panoul de administrare din cauza inactivității", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/ro/modules.json b/public/language/ro/modules.json new file mode 100644 index 0000000000..6d73b62df0 --- /dev/null +++ b/public/language/ro/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Trimite", + "chat.no_active": "Nu ai nici o conversație activă", + "chat.user_typing": "%1 scrie ...", + "chat.user_has_messaged_you": "%1 ți-a trimis un mesaj.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Selectează un recipient pentru a vedea istoria mesajelor chat", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Conversații Recente", + "chat.contacts": "Contacte", + "chat.message-history": "Istorie Mesaje", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Chat pop-up", + "chat.minimize": "Minimize", + "chat.maximize": "Maximizează", + "chat.seven_days": "7 Zile", + "chat.thirty_days": "30 de zile", + "chat.three_months": "3 Luni", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Scrie", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 a spus în %2:", + "composer.user_said": "%1 a spus:", + "composer.discard": "Ești sigur că vrei să renunți la acest mesaj?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/ro/notifications.json b/public/language/ro/notifications.json new file mode 100644 index 0000000000..69b437bd6e --- /dev/null +++ b/public/language/ro/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notificări", + "no_notifs": "Nu ai nici o notificare recentă", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Înapoi la %1", + "outgoing_link": "Link Extern", + "outgoing_link_message": "Părăsești acuma %1", + "continue_to": "Continuă la %1", + "return_to": "Întoarce-te la %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Ai notificări necitite.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Un mesaj nou de la %1", + "upvoted_your_post_in": "%1 a votat pozitiv mesajul tău în %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 a semnalizat un mesaj în %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 a postat un răspuns la: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 has posted a new topic: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 a început să te urmărească.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email confirmat", + "email-confirmed-message": "Îți mulțumim pentru validarea emailului. Contul tău este acuma activat.", + "email-confirm-error-message": "A fost o problemă cu activarea adresei tale de email. Poate codul de activare a fost invalid sau expirat.", + "email-confirm-sent": "Un email de confirmare a fost trimis.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/ro/pages.json b/public/language/ro/pages.json new file mode 100644 index 0000000000..9b5be8f4db --- /dev/null +++ b/public/language/ro/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Acasă", + "unread": "Subiecte Necitite", + "popular-day": "Subiecte populare azi", + "popular-week": "Subiecte populare în săptămâna asta", + "popular-month": "Subiecte populare în luna asta", + "popular-alltime": "All time popular topics", + "recent": "Subiecte Noi", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Utilizatori online", + "users/latest": "Ultimii membrii", + "users/sort-posts": "Membrii cu cele mai multe postări", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "User Search", + "notifications": "Notificări", + "tags": "Taguri", + "tag": "Topics tagged under "%1"", + "register": "Înregistrează un cont nou", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Resetează parola contului tău", + "categories": "Categorii", + "groups": "Grupuri", + "group": "%1 group", + "chats": "Chats", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "User Settings", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Confirmed", + "maintenance.text": "%1 este momentan în mentenanță. Întoarce-te în curând!", + "maintenance.messageIntro": "Additionally, the administrator has left this message:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/ro/post-queue.json b/public/language/ro/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/ro/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/ro/recent.json b/public/language/ro/recent.json new file mode 100644 index 0000000000..ad7867e068 --- /dev/null +++ b/public/language/ro/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Recente", + "day": "Zi", + "week": "Săptămână", + "month": "Lună", + "year": "An", + "alltime": "Tot Timpul", + "no_recent_topics": "Nu există subiecte recente.", + "no_popular_topics": "Nu sunt subiecte populare.", + "there-is-a-new-topic": "Există un subiect nou.", + "there-is-a-new-topic-and-a-new-post": "Există un subiect nou si o postare nouă.", + "there-is-a-new-topic-and-new-posts": "Există un subiect nou și %1 postări noi.", + "there-are-new-topics": "Există %1 postări noi.", + "there-are-new-topics-and-a-new-post": "Exista %1 subiect nou și o postare nouă", + "there-are-new-topics-and-new-posts": "Exista %1 subiecte noi și %2 postări noi", + "there-is-a-new-post": "Exista o postare nouă", + "there-are-new-posts": "Există %1 postări noi", + "click-here-to-reload": "Apăsaţi aici pentru a reîncărca." +} \ No newline at end of file diff --git a/public/language/ro/register.json b/public/language/ro/register.json new file mode 100644 index 0000000000..98070948b5 --- /dev/null +++ b/public/language/ro/register.json @@ -0,0 +1,32 @@ +{ + "register": "Înregistrare", + "cancel_registration": "Cancel Registration", + "help.email": "Implicit, adresa ta de email va fi ascunsă.", + "help.username_restrictions": "Un nume de utilizator între %1 și %2 caractere. Alți utilizatori te pot menționa cu @utilizator.", + "help.minimum_password_length": "Lungimea parolei trebuie sa fie mai mare de %1 caractere.", + "email_address": "Adresă de email", + "email_address_placeholder": "Introdu adresă de email", + "username": "Utilizator", + "username_placeholder": "Introdu Utilizator", + "password": "Parolă", + "password_placeholder": "Introdu Parolă", + "confirm_password": "Confirmă Parola", + "confirm_password_placeholder": "Confirmă Parola", + "register_now_button": "Înregistrează-te", + "alternative_registration": "Înregistrare Alternativă", + "terms_of_use": "Termeni de utilizare", + "agree_to_terms_of_use": "Sunt de acord cu termenii de utilizare", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/ro/reset_password.json b/public/language/ro/reset_password.json new file mode 100644 index 0000000000..f5cb307424 --- /dev/null +++ b/public/language/ro/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Resetează Parola", + "update_password": "Actualizează Parola", + "password_changed.title": "Parolă Modificată", + "password_changed.message": "

Parola a fost resetată cu succes, te rugăm să te autentifici dinou.", + "wrong_reset_code.title": "Cod de resetare incorect", + "wrong_reset_code.message": "Codul de resetare primit a fost incorect. Te rugăm să încerci dinou sau să ceri un nou cod de resetare.", + "new_password": "Parolă Nouă", + "repeat_password": "Confirmă Parola", + "changing_password": "Changing Password", + "enter_email": "Te rugăm sa introduci adresa ta de email și îți vom trimite un email cu instrucțiuni pentru a îți reseta contul tău de utilizator.", + "enter_email_address": "Introdu adresă de email", + "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.", + "invalid_email": "Adresă de email invalidă / Adresa de email nu există!", + "password_too_short": "The password entered is too short, please pick a different password.", + "passwords_do_not_match": "The two passwords you've entered do not match.", + "password_expired": "Parola ta a expirat, te rugăm alege altă parolă" +} \ No newline at end of file diff --git a/public/language/ro/search.json b/public/language/ro/search.json new file mode 100644 index 0000000000..f6d835d032 --- /dev/null +++ b/public/language/ro/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 rezultat(e) pentru \"%2\", (%3 secunde)", + "no-matches": "Nu a fost găsit nici un rezultat", + "advanced-search": "Căutare avansată", + "in": "În", + "titles": "Titluri", + "titles-posts": "Titluri şi Mesaje", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Postat de", + "in-categories": "În Categorii", + "search-child-categories": "Search child categories", + "has-tags": "Has tags", + "reply-count": "Numărul de răspunsuri", + "at-least": "Cel puţin", + "at-most": "Cel mult", + "relevance": "Relevance", + "post-time": "Ora mesajului", + "votes": "Votes", + "newer-than": "Mai noi decât", + "older-than": "Mai vechi decât", + "any-date": "Orice Dată", + "yesterday": "Ieri", + "one-week": "Acum o săptămână", + "two-weeks": "Două săptămâni", + "one-month": "O lună", + "three-months": "Trei luni", + "six-months": "Şase luni", + "one-year": "Un an", + "sort-by": "Sortează după", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "topic-votes": "Topic votes", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Utilizator", + "category": "Category", + "descending": "In descending order", + "ascending": "In ascending order", + "save-preferences": "Save preferences", + "clear-preferences": "Clear preferences", + "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)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/ro/success.json b/public/language/ro/success.json new file mode 100644 index 0000000000..27c9593b53 --- /dev/null +++ b/public/language/ro/success.json @@ -0,0 +1,7 @@ +{ + "success": "Succes", + "topic-post": "Ai postat cu succes.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Autentificare Reușită", + "settings-saved": "Setări salvate!" +} \ No newline at end of file diff --git a/public/language/ro/tags.json b/public/language/ro/tags.json new file mode 100644 index 0000000000..1af89c4cd1 --- /dev/null +++ b/public/language/ro/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nu există nici un subiect cu acest tag.", + "tags": "Taguri", + "enter_tags_here": "Introduceți tagurile aici, fiecare tag trebuie să conțină între %1 și %2 caractere.", + "enter_tags_here_short": "Introdu taguri...", + "no_tags": "În acest moment nu există nici un tag.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/ro/top.json b/public/language/ro/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/ro/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/ro/topic.json b/public/language/ro/topic.json new file mode 100644 index 0000000000..9fc37572c1 --- /dev/null +++ b/public/language/ro/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Subiect", + "title": "Title", + "no_topics_found": "Nu a fost găsit nici un subiect!", + "no_posts_found": "Nu a fost găsit nici un mesaj!", + "post_is_deleted": "Acest mesaj a fost șters!", + "topic_is_deleted": "Acest subiect este șters!", + "profile": "Profil", + "posted_by": "Postat de %1", + "posted_by_guest": "Postat de Vizitator", + "chat": "Conversație", + "notify_me": "Notică-mă de noi răspunsuri în acest subiect", + "quote": "Citează", + "reply": "Răspunde", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Răspunde ca subiect", + "guest-login-reply": "Login pentru a răspunde", + "login-to-view": "🔒 Log in to view", + "edit": "Editează", + "delete": "Șterge", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Curăță", + "restore": "Restaurează", + "move": "Mută", + "change-owner": "Change Owner", + "fork": "Bifurcă", + "link": "Link", + "share": "Distribuie", + "tools": "Unelte", + "locked": "Închis", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Acest subiect a fost șters. Doar utilizatorii cu privilegii pentru moderarea subiectelor îl poate vedea.", + "following_topic.message": "Vei primi notificări când cineva va posta un nou mesaj in acest subiect.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Te rugăm să te înregistrezi sau să te autentifici ca să te poți abona la acest subiect.", + "markAsUnreadForAll.success": "Subiect marcat ca citit pentru toți.", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "Urmărește", + "unwatch": "Oprire urmărire", + "watch.title": "Abonează-te la notificări legate de acest subiect", + "unwatch.title": "Oprește urmărirea acestui subiect", + "share_this_post": "Distribuie acest mesaj", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Unelte pentru subiecte", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Pin Subiect", + "thread_tools.unpin": "Unpin Subiect", + "thread_tools.lock": "Închide Subiect", + "thread_tools.unlock": "Deschide Subiect", + "thread_tools.move": "Mută Subiect", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Mută-le pe toate", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Bifurcă Subiect", + "thread_tools.delete": "Șterge Subiect", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Ești sigur că vrei să ștergi acest subiect?", + "thread_tools.restore": "Restaurează Subiect", + "thread_tools.restore_confirm": "Esti sigur că vrei să restaurezi acest subiect?", + "thread_tools.purge": "Curăță Subiect", + "thread_tools.purge_confirm": "Ești sigur că vrei sa cureți acest subiect?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Ești sigur că vrei să ștergi acest mesaj?", + "post_restore_confirm": "Esti sigur că vrei să restaurezi acest mesaj?", + "post_purge_confirm": "Ești sigur că vrei să cureți acest mesaj?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Se Încarcă Categoriile", + "confirm_move": "Mută", + "confirm_fork": "Bifurcă", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Se încarcă mai multe mesaje", + "move_topic": "Mută Subiect", + "move_topics": "Mută Subiecte", + "move_post": "Mută Mesaj", + "post_moved": "Mesaj mutat!", + "fork_topic": "Bifurcă Subiect", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Apasă pe mesajele care vrei sa le bifurci", + "fork_no_pids": "Nu a fost selectat nici un mesaj!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Successfully forked topic! Click here to go to the forked topic.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Introdu numele subiectului aici ...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Renunță", + "composer.submit": "Trimite", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Îi raspunde lui %1", + "composer.new_topic": "Subiect Nou", + "composer.editing": "Editing", + "composer.uploading": "se uploadează ...", + "composer.thumb_url_label": "Lipește un link pentru pictogramă subiect", + "composer.thumb_title": "Adaugă o pictogramă la acest subiect", + "composer.thumb_url_placeholder": "http://exemplu.ro/thumb.png", + "composer.thumb_file_label": "Sau uploadează un fisier", + "composer.thumb_remove": "Șterge câmpurile", + "composer.drag_and_drop_images": "Trage și Pune Imagini Aici", + "more_users_and_guests": "%1 utlizator(i) și %2 vizitator(i)", + "more_users": "%1 utilizator(i)", + "more_guests": "%1 vizitator(i)", + "users_and_others": "%1 și alți %2", + "sort_by": "Sortează de la", + "oldest_to_newest": "Vechi la Noi", + "newest_to_oldest": "Noi la Vechi", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/ro/unread.json b/public/language/ro/unread.json new file mode 100644 index 0000000000..43e6a60d67 --- /dev/null +++ b/public/language/ro/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Necitite", + "no_unread_topics": "Nu există nici un subiect necitit.", + "load_more": "Încarcă mai multe", + "mark_as_read": "Marchează ca citit", + "selected": "Selectate", + "all": "Toate", + "all_categories": "Toate categoriile", + "topics_marked_as_read.success": "Subiectele au fost marcate ca citite!", + "all-topics": "Toate subiectele", + "new-topics": "Subiecte noi", + "watched-topics": "Subiecte urmărite", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/ro/uploads.json b/public/language/ro/uploads.json new file mode 100644 index 0000000000..89095c4dc9 --- /dev/null +++ b/public/language/ro/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Uploading the file...", + "select-file-to-upload": "Selectează un fișier pentru încărcare!", + "upload-success": "File uploaded successfully!", + "maximum-file-size": "Maxim %1 kB", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/ro/user.json b/public/language/ro/user.json new file mode 100644 index 0000000000..d0c9f264b1 --- /dev/null +++ b/public/language/ro/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Banat", + "muted": "Muted", + "offline": "Deconectat", + "deleted": "Deleted", + "username": "Nume utilizator", + "joindate": "Join Date", + "postcount": "Post Count", + "email": "Adresă Email", + "confirm_email": "Confirmă Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Account", + "ban_account_confirm": "Do you really want to ban this user?", + "unban_account": "Unban Account", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Șterge Cont", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Cont șters", + "account-content-deleted": "Account content deleted", + "fullname": "Nume Întreg", + "website": "Pagină Web", + "location": "Locație", + "age": "Vîrstă", + "joined": "S-a Înregistrat", + "lastonline": "S-a conectat ultima oară", + "profile": "Profil", + "profile_views": "Vizualizări", + "reputation": "Reputație", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Watched", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Urmărit de", + "following": "Îi urmărește pe", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "About me", + "signature": "Semnătură", + "birthday": "Zi de naștere", + "chat": "Conversație", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Urmărește", + "unfollow": "Oprește urmărirea", + "more": "Mai multe", + "profile_update_success": "Profilul tău a fost actualizat cu succes!", + "change_picture": "Schimbă Poza", + "change_username": "Change Username", + "change_email": "Change Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Editează", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Poză uploadată", + "upload_new_picture": "Uploadează poză nouă", + "upload_new_picture_from_url": "Uploadează poză nouă cu URL", + "current_password": "Parola curentă", + "change_password": "Schimbă Parola", + "change_password_error": "Parola invalidă!", + "change_password_error_wrong_current": "Parola ta curentă nu este corectă!", + "change_password_error_match": "Parolele trebuie să se potrivească!", + "change_password_error_privileges": "Nu ai nici un drept să schimbi această parolă.", + "change_password_success": "Parola ta a fost actualizată!", + "confirm_password": "Confirmă Parola", + "password": "Parolă", + "username_taken_workaround": "Numele de utilizator pe care l-ai cerut este deja luat, așa că l-am modificat puțin. Acum ești cunoscut ca %1", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "Uploadează poză", + "upload_a_picture": "Uploadează o poză", + "remove_uploaded_picture": "Remove Uploaded Picture", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Setări", + "show_email": "Arată adresa mea de email", + "show_fullname": "Show My Full Name", + "restrict_chats": "Only allow chat messages from users I follow", + "digest_label": "Abonează-te la digest", + "digest_description": "Abonează-te la updateuri prin email de la acest forum (notificări noi si subiecte) în concordanță cu un program prestabilit", + "digest_off": "Închis", + "digest_daily": "Zilnic", + "digest_weekly": "Săptămânal", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Lunar", + "has_no_follower": "Pe acest utilizator nu îl urmărește nimeni :(", + "follows_no_one": "Acest utilizator nu urmărește pe nimeni :(", + "has_no_posts": "This user hasn't posted anything yet.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "This user hasn't posted any topics yet.", + "has_no_watched_topics": "This user hasn't watched any topics yet.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Adresă de email ascunsă", + "hidden": "ascuns", + "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "topics_per_page": "Subiecte pe pagină", + "posts_per_page": "Mesaje pe pagină", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Setări navigare", + "open_links_in_new_tab": "Open outgoing links in new tab", + "enable_topic_searching": "Enable In-Topic Searching", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + "select-skin": "Select a Skin", + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/ro/users.json b/public/language/ro/users.json new file mode 100644 index 0000000000..bda605e6a8 --- /dev/null +++ b/public/language/ro/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Ultimii Utilizatori", + "top_posters": "Top Utilizatori", + "most_reputation": "Cei mai apreciați utilizatori", + "most_flags": "Cele mai multe flaguri", + "search": "Căutare", + "enter_username": "Introdu un nume de utilizator pentru a căuta", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Încarcă mai multe", + "users-found-search-took": "%1 utilizator(i) găsiți! Căutarea a durat %2 secunde.", + "filter-by": "Filtrează după", + "online-only": "Numai online", + "invite": "Invită", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Un email de invitație s-a trimis la %1", + "user_list": "Listă utilizatori", + "recent_topics": "Subiecte Noi", + "popular_topics": "Subiecte Populare", + "unread_topics": "Subiecte Necitite", + "categories": "Categorii", + "tags": "Taguri", + "no-users-found": "Niciun utilizator găsit!" +} \ No newline at end of file diff --git a/public/language/ru/_DO_NOT_EDIT_FILES_HERE.md b/public/language/ru/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/ru/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/ru/admin/admin.json b/public/language/ru/admin/admin.json new file mode 100644 index 0000000000..256c3aa5f0 --- /dev/null +++ b/public/language/ru/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Вы уверены, что хотите пересобрать и перезапустить NodeBB?", + "alert.confirm-restart": "Вы уверены, что хотите перезапустить NodeBB?", + + "acp-title": "%1 | Панель администратора NodeBB", + "settings-header-contents": "Содержание", + "changes-saved": "Изменения сохранены", + "changes-saved-message": "Ваши изменения в конфигурации NodeBB были сохранены", + "changes-not-saved": "Изменения не сохранены", + "changes-not-saved-message": "Произошла ошибка NodeBB при сохранении Ваших изменений (%1)" +} \ No newline at end of file diff --git a/public/language/ru/admin/advanced/cache.json b/public/language/ru/admin/advanced/cache.json new file mode 100644 index 0000000000..42f134ea42 --- /dev/null +++ b/public/language/ru/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Кэш сообщений", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "Заполнен на%1%", + "post-cache-size": "Размер кэша сообщений", + "items-in-cache": "Закешировано элементов" +} \ No newline at end of file diff --git a/public/language/ru/admin/advanced/database.json b/public/language/ru/admin/advanced/database.json new file mode 100644 index 0000000000..b570dab82f --- /dev/null +++ b/public/language/ru/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 байт", + "x-mb": "%1 Мб", + "x-gb": "%1 Гб", + "uptime-seconds": "Время работы в секундах", + "uptime-days": "Время работы в днях", + + "mongo": "Mongo", + "mongo.version": "Версия MongoDB", + "mongo.storage-engine": "Система хранения", + "mongo.collections": "Коллекции", + "mongo.objects": "Документы", + "mongo.avg-object-size": "Средний размер документа", + "mongo.data-size": "Размер данных", + "mongo.storage-size": "Размер хранилища", + "mongo.index-size": "Размер индекса", + "mongo.file-size": "Размер файла", + "mongo.resident-memory": "Долгосрочная память", + "mongo.virtual-memory": "Виртуальная память", + "mongo.mapped-memory": "Расширенная память", + "mongo.bytes-in": "Байт входящих", + "mongo.bytes-out": "Байт исходящих", + "mongo.num-requests": "Количество запросов", + "mongo.raw-info": "Сырые данные о MongoDB", + "mongo.unauthorized": "NodeBB не смог получить статистические данные от MongoDB. Пожалуйста, проверьте, что пользователь, от имени которого NodeBB соединяется с БД имеет привилегию «clusterMonitor» в БД «admin». ", + + "redis": "Redis", + "redis.version": "Версия Redis", + "redis.keys": "Ключей", + "redis.expires": "Истекает", + "redis.avg-ttl": "Средний TTL", + "redis.connected-clients": "Подключенные клиенты", + "redis.connected-slaves": "Подключенные устройства", + "redis.blocked-clients": "Заблокированные клиенты", + "redis.used-memory": "Использовано памяти", + "redis.memory-frag-ratio": "Коэффициент фрагментации памяти", + "redis.total-connections-recieved": "Общее число подключений получено", + "redis.total-commands-processed": "Команд обработано в общем", + "redis.iops": "Операций в секунду", + "redis.iinput": "Текущих входящих в секунду", + "redis.ioutput": "Текущих исходящих в секунду", + "redis.total-input": "Всего входящих", + "redis.total-output": "Всего исходящих", + + "redis.keyspace-hits": "Количество ключевых просмотров", + "redis.keyspace-misses": "Количество не ключевых просмотров", + "redis.raw-info": "Сырые данные о Redis", + + "postgres": "Postgres", + "postgres.version": "Версия PostgreSQL", + "postgres.raw-info": "Информация о PostgreSQL" +} diff --git a/public/language/ru/admin/advanced/errors.json b/public/language/ru/admin/advanced/errors.json new file mode 100644 index 0000000000..f4d5688223 --- /dev/null +++ b/public/language/ru/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "График %1", + "error-events-per-day": "событий %1 в день", + "error.404": "404 Не найдено", + "error.503": "503 Сервис недоступен", + "manage-error-log": "Управление журналом ошибок", + "export-error-log": "Экспорт журнала (CSV)", + "clear-error-log": "Очистить журнал", + "route": "Путь", + "count": "Кол-во", + "no-routes-not-found": "Ура! Ошибок 404 нет!", + "clear404-confirm": "Вы уверены, что хотите очистить журнал ошибок 404?", + "clear404-success": "Журнал ошибок 404 очищен" +} \ No newline at end of file diff --git a/public/language/ru/admin/advanced/events.json b/public/language/ru/admin/advanced/events.json new file mode 100644 index 0000000000..f408366f8c --- /dev/null +++ b/public/language/ru/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "События", + "no-events": "Нет событий", + "control-panel": "Панель управления событиями", + "delete-events": "Удалить события", + "confirm-delete-all-events": "Вы уверены, что хотите удалить все записанные события?", + "filters": "Фильтр", + "filters-apply": "Применить фильтр", + "filter-type": "Тип события", + "filter-start": "Дата начала", + "filter-end": "Дата окончания", + "filter-perPage": "Записей на страницу" +} \ No newline at end of file diff --git a/public/language/ru/admin/advanced/logs.json b/public/language/ru/admin/advanced/logs.json new file mode 100644 index 0000000000..74485d3820 --- /dev/null +++ b/public/language/ru/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Журнал событий", + "control-panel": "Управление журналом событий", + "reload": "Перезагрузить", + "clear": "Очистить", + "clear-success": "Журнал событий очищен!" +} \ No newline at end of file diff --git a/public/language/ru/admin/appearance/customise.json b/public/language/ru/admin/appearance/customise.json new file mode 100644 index 0000000000..dfb96c7a8d --- /dev/null +++ b/public/language/ru/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Пользовательский CSS/LESS", + "custom-css.description": "Добавьте собственные стили CSS/LESS. Они будут применяться в последнюю очередь, после всех остальных.", + "custom-css.enable": "Включить пользовательский CSS/LESS", + + "custom-js": "Пользовательский JavaScript", + "custom-js.description": "Добавьте собственный JS-код. Он будет выполнен после полной загрузки страницы.", + "custom-js.enable": "Включить пользовательский JavaScript", + + "custom-header": "Пользовательский заголовок", + "custom-header.description": "Добавьте HTML в секцию <head> шаблонов страниц форума. Тег <script> использовать можно, но не рекомендуется (для этого предусмотрен раздел Пользовательский JavaScript).", + "custom-header.enable": "Включить пользовательский заголовок", + + "custom-css.livereload": "Включить автоматическую перезагрузку страниц", + "custom-css.livereload.description": "Включите эту опцию, чтобы принудительно обновлять все сеансы на каждом устройстве под этой учетной записью при каждом нажатии кнопки Сохранить" +} \ No newline at end of file diff --git a/public/language/ru/admin/appearance/skins.json b/public/language/ru/admin/appearance/skins.json new file mode 100644 index 0000000000..ef3367939e --- /dev/null +++ b/public/language/ru/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Загрузка стилей...", + "homepage": "Домашняя страница", + "select-skin": "Выбрать стиль", + "current-skin": "Текущий стиль", + "skin-updated": "Стиль обновлён", + "applied-success": "%1 тема была успешно применена", + "revert-success": "Тема возвращена к цветам по умолчанию" +} \ No newline at end of file diff --git a/public/language/ru/admin/appearance/themes.json b/public/language/ru/admin/appearance/themes.json new file mode 100644 index 0000000000..08bdac4239 --- /dev/null +++ b/public/language/ru/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Проверка установленных тем...", + "homepage": "Домашняя страница", + "select-theme": "Выбрать тему", + "current-theme": "Текущая тема", + "no-themes": "Не найдено установленных тем", + "revert-confirm": "Вы уверены, что хотите восстановить стандартную тему оформления NodeBB?", + "theme-changed": "Тема оформления изменена", + "revert-success": "Вы успешно вернули стандартную тему оформления NodeBB.", + "restart-to-activate": "Пожалуйста, пересоберите и перезагрузите NodeBB для полной активации этой темы." +} \ No newline at end of file diff --git a/public/language/ru/admin/dashboard.json b/public/language/ru/admin/dashboard.json new file mode 100644 index 0000000000..486da5be1c --- /dev/null +++ b/public/language/ru/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Трафик ", + "page-views": "Просмотров", + "unique-visitors": "Посетителей", + "logins": "Авторизаций", + "new-users": "Новых пользователей", + "posts": "Сообщений", + "topics": "Тем", + "page-views-seven": "За 7 дней", + "page-views-thirty": "За 30 дней", + "page-views-last-day": "За 24 часа", + "page-views-custom": "Другой диапазон дат", + "page-views-custom-start": "Начало", + "page-views-custom-end": "Конец", + "page-views-custom-help": "Укажите начало и конец периода, за который вы хотите получить данные о просмотрах. Если выбор даты не доступен, то вы можете указать дату в формате ГГГГ-ММ-ДД ", + "page-views-custom-error": "Пожалуйста, укажите правильный диапазон дат в формате ГГГГ-ММ-ДД", + + "stats.yesterday": "Вчера", + "stats.today": "Сегодня", + "stats.last-week": "За прошл. неделю", + "stats.this-week": "За эту неделю", + "stats.last-month": "За прошл. месяц", + "stats.this-month": "За этот месяц", + "stats.all": "За всё время", + + "updates": "Обновления", + "running-version": "Вы используете NodeBB версии %1", + "keep-updated": "Пожалуйста, следите за тем, чтобы NodeBB своевременно обновлялся и получал все необходимые исправления ошибок и уязвимостей.", + "up-to-date": "

Вы используете актуальную версию

", + "upgrade-available": "

Вышла новая версия NodeBB (v%1). Хотите установить обновление?

", + "prerelease-upgrade-available": "

Вы используете устаревшую предрелизную версию NodeBB. Вышла новая (v%1). Хотите установить обновление?

", + "prerelease-warning": "

Вы используете предрелизную версию NodeBB. Вы можете столкнуться с разнообразными ошибками в её работе.

", + "fallback-emailer-not-found": "Резервная почта не найдена!", + "running-in-development": "Форум работает в режиме для разработчиков. Это значит, что он может быть более уязвим для внешних угроз; пожалуйста, свяжитесь с вашим сисадмином.", + "latest-lookup-failed": "

Не удалось проверить наличие обновлений NodeBB

", + + "notices": "Примечания", + "restart-not-required": "Перезапуск не требуется", + "restart-required": "Требуется перезапуск", + "search-plugin-installed": "Плагин поиска установлен", + "search-plugin-not-installed": "Плагин поиска не установлен", + "search-plugin-tooltip": "Чтобы включить функцию поиска, установите соответствующий плагин на странице управления плагинами", + + "control-panel": "Управление системой", + "rebuild-and-restart": "Пересобрать и Перезапустить", + "restart": "Перезапустить", + "restart-warning": "Пересборка или перезапуск вашего NodeBB на несколько секунд оборвёт все имеющиеся соединения.", + "restart-disabled": "Пересборка и перезапуск вашего NodeBB была отключена, поскольку вы запустили форум без использования соответствующего демона.", + "maintenance-mode": "Режим техобслуживания", + "maintenance-mode-title": "Нажмите, чтобы включить и настроить режим техобслуживания", + "realtime-chart-updates": "Обновление графиков в реальном времени", + + "active-users": "Активных посетителей", + "active-users.users": "Польз.", + "active-users.guests": "Гостей", + "active-users.total": "Всего", + "active-users.connections": "Соединений", + + "guest-registered-users": "Гостей относительно Пользователей", + "guest": "Гость", + "registered": "Авторизованные", + + "user-presence": "Присутствие", + "on-categories": "В списке категорий", + "reading-posts": "Читают сообщения", + "browsing-topics": "Просматривают темы", + "recent": "Просм. последние темы", + "unread": "Просм. непрочитанные", + + "high-presence-topics": "Популярные темы", + "popular-searches": "Популярные поисковые запросы", + + "graphs.page-views": "Просмотры", + "graphs.page-views-registered": "Просм. авторизованными", + "graphs.page-views-guest": "Просмотров гостями", + "graphs.page-views-bot": "Просмотров ботами", + "graphs.unique-visitors": "Уникальных посетителей", + "graphs.registered-users": "Авторизованных пользователей", + "graphs.guest-users": "Неавторизированных посетителей", + "last-restarted-by": "Последний перезапуск:", + "no-users-browsing": "Просмотров нет", + + "back-to-dashboard": "Вернуться на Панель управления", + "details.no-users": "Никто не присоединился за выбранный отрезок времени", + "details.no-topics": "Сообщений за выбранный отрезок времени не было", + "details.no-searches": "Поисковых запросов еще не было", + "details.no-logins": "Попыток входа за выбранный отрезок времени не было", + "details.logins-static": "NodeBB хранит данные о сессиях за %1 дней, так что таблица ниже покажет только недавние активные сессии", + "details.logins-login-time": "Время входа" +} diff --git a/public/language/ru/admin/development/info.json b/public/language/ru/admin/development/info.json new file mode 100644 index 0000000000..63d6faae67 --- /dev/null +++ b/public/language/ru/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Вы находитесь на %1:%2", + "ip": "IP %1", + "nodes-responded": "Узлов: %1. Время ответа %2мс!", + "host": "хост", + "primary": "первичный", + "pid": "pid", + "nodejs": "nodejs", + "online": "онлайн", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "системная загрузка", + "cpu-usage": "загрузка процессора", + "uptime": "продолжительность работы", + + "registered": "Авторизованных", + "sockets": "Сокеты", + "guests": "Гостей", + + "info": "Сырые данные" +} \ No newline at end of file diff --git a/public/language/ru/admin/development/logger.json b/public/language/ru/admin/development/logger.json new file mode 100644 index 0000000000..420b27b720 --- /dev/null +++ b/public/language/ru/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Настройки журналирования", + "description": "Включите, чтобы выводить в консоль отчёт о событиях. Чтобы сохранять эти данные в файл, укажите путь к файлу. Журналирование запросов HTTP полезно для сбора данных о том, что именно и когда просматривают посетители вашего форума. Кроме того, возможна запись событий Socket.io, что, в сочетании с мониторингом redis-cli, крайне полезно для отслеживания состояния NodeBB.", + "explanation": "Просто включите или выключите соответствующую опцию, чтобы настроить журналирование. Перезапуск NodeBB не потребуется.", + "enable-http": "Отслеживать HTTP-запросы", + "enable-socket": "Отслеживать события Socket.io", + "file-path": "Путь к файлу журнала", + "file-path-placeholder": "/path/to/log/file.log ::: оставьте пустым для вывода сообщений в консоль", + + "control-panel": "Панель управления", + "update-settings": "Обновить настройки" +} \ No newline at end of file diff --git a/public/language/ru/admin/extend/plugins.json b/public/language/ru/admin/extend/plugins.json new file mode 100644 index 0000000000..00068c7e96 --- /dev/null +++ b/public/language/ru/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Популярные", + "installed": "Установлены", + "active": "Включены", + "inactive": "Отключены", + "out-of-date": "Устарели", + "none-found": "Плагины не найдены.", + "none-active": "Нет активных плагинов", + "find-plugins": "Найти плагины", + + "plugin-search": "Поиск плагинов", + "plugin-search-placeholder": "Искать плагин...", + "submit-anonymous-usage": "Отправлять анонимные сведения об используемых плагинах.", + "reorder-plugins": "Порядок загрузки плагинов", + "order-active": "Изменить", + "dev-interested": "Хотите создать свой плагин для NodeBB?", + "docs-info": "Полная документация по разработке для NodeBB опубликована на Портале NodeBB", + + "order.description": "Некоторые плагины работают идеально только когда они инициализируются в определённом порядке, или до, или после других плагинов.", + "order.explanation": "Плагины загружаются в указанном порядке, сверху вниз.", + + "plugin-item.themes": "Темы", + "plugin-item.deactivate": "Отключить", + "plugin-item.activate": "Включить", + "plugin-item.install": "Установить", + "plugin-item.uninstall": "Удалить", + "plugin-item.settings": "Настройки", + "plugin-item.installed": "Установленная версия", + "plugin-item.latest": "Последняя версия", + "plugin-item.upgrade": "Обновить", + "plugin-item.more-info": "Дополнительная информация:", + "plugin-item.unknown": "Неизвестно", + "plugin-item.unknown-explanation": "Состояние плагина не удается определить, возможно это проблема с настройками.", + "plugin-item.compatible": "Этот плагин совместим с NodeBB %1", + "plugin-item.not-compatible": "Для этого плагина нет данных о совместимости. Проверьте, что он корректно работает, прежде чем использовать его.", + + "alert.enabled": "Плагин включен", + "alert.disabled": "Плагин выключен", + "alert.upgraded": "Плагин обновлён", + "alert.installed": "Плагин установлен", + "alert.uninstalled": "Плагин удалён", + "alert.activate-success": "Пожалуйста пересоберите и перезапустите ваш экземпляр NodeBB для полной активации этого плагина ", + "alert.deactivate-success": "Плагин успешно отключен", + "alert.upgrade-success": "Пожалуйста, пересоберите и перезапустите NodeBB чтобы полностью активировать этот плагин", + "alert.install-success": "Плагин успешно установлен. Пожалуйста, активируйте его.", + "alert.uninstall-success": "Плагин успешно отключен и удалён.", + "alert.suggest-error": "

NodeBB не может запустить менеджер пакетов, продолжить установку последней версии?

Ответ сервера (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB не может запустить менеджер пакетов, установка обновления не рекомендуется

", + "alert.incompatible": "

Ваша версия NodeBB (v%1) совместима только с версией v%2 этого плагина. Пожалуйста, обновите NodeBB, если вы хотите пользоваться более свежей версией.

", + "alert.possibly-incompatible": "

Нет данных о совместимости

Этот плагин не содержит сведений о совместимости с вашей версией NodeBB. Полная совместимость не гарантируется, возможно форум даже не запустится.

В случае, если NodeBB не запускается:

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

Продолжить установку последней версии плагина?

", + "alert.reorder": "Порядок плагинов изменён", + "alert.reorder-success": "Пожалуйста, пересоберите и перезапустите NodeBB чтобы полностью завершить процесс.", + + "license.title": "Сведения о лицензии плагина", + "license.intro": "Плагин %1 распространяется под лицензией %2. Пожалуйста, внимательно прочтите условия лицензии перед активацией плагина.", + "license.cta": "Вы хотите продолжить и активировать плагин?" +} diff --git a/public/language/ru/admin/extend/rewards.json b/public/language/ru/admin/extend/rewards.json new file mode 100644 index 0000000000..d6cc9c4623 --- /dev/null +++ b/public/language/ru/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Награды", + "condition-if-users": "Если у пользователя", + "condition-is": "Соответствует", + "condition-then": "Тогда", + "max-claims": "Сколько раз можно наградить", + "zero-infinite": "Введите 0, если не ограничено", + "delete": "Удалить", + "enable": "Включить", + "disable": "Выключить", + + "alert.delete-success": "Награда успешно удалена", + "alert.no-inputs-found": "Некорректная награда!", + "alert.save-success": "Награды успешно сохранены" +} \ No newline at end of file diff --git a/public/language/ru/admin/extend/widgets.json b/public/language/ru/admin/extend/widgets.json new file mode 100644 index 0000000000..7d28c433c4 --- /dev/null +++ b/public/language/ru/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Доступные виджеты", + "explanation": "Выберите виджет из выпадающего меню и перетащите его в подходящую область слева.", + "none-installed": "Виджеты не найдены! Включите плагин с основными виджетами в панели управления плагинами", + "clone-from": "Скопировать виджеты из", + "containers.available": "Доступные контейнеры", + "containers.explanation": "Перетащите поверх любого активного виджета", + "containers.none": "Отсутствует", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Панель", + "container.panel-header": "Заголовок панели", + "container.panel-body": "Тело панели", + "container.alert": "Уведомление", + + "alert.confirm-delete": "Вы уверены, что хотите удалить этот виджет?", + "alert.updated": "Виджеты обновлены", + "alert.update-success": "Виджеты успешно обновлены", + "alert.clone-success": "Виджеты успешно скопированы", + + "error.select-clone": "Пожалуйста, выберите страницу, откуда нужно скопировать виджеты", + + "title": "Заголовок", + "title.placeholder": "Заголовок (используется только в некоторых контейнерах)", + "container": "Контейнер", + "container.placeholder": "Перетащите контейнер сюда или добавьте HTML вручную.", + "show-to-groups": "Показывать группам", + "hide-from-groups": "Скрывать от групп", + "hide-on-mobile": "Скрывать на мобильных устройствах" +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/admins-mods.json b/public/language/ru/admin/manage/admins-mods.json new file mode 100644 index 0000000000..ba8834c9d3 --- /dev/null +++ b/public/language/ru/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Администраторы", + "global-moderators": "Глобальные модераторы", + "moderators": "Moderators", + "no-global-moderators": "Нет глобальных модераторов", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Нет модераторов", + "add-administrator": "Добавить администратора", + "add-global-moderator": "Добавить глобального модератора", + "add-moderator": "Добавить модератора" +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/categories.json b/public/language/ru/admin/manage/categories.json new file mode 100644 index 0000000000..9d504d3893 --- /dev/null +++ b/public/language/ru/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Настройки категории", + "privileges": "Права доступа", + + "name": "Название категории", + "description": "Описание категории", + "bg-color": "Цвет фона", + "text-color": "Цвет текста", + "bg-image-size": "Размер фонового изображения", + "custom-class": "Свой класс", + "num-recent-replies": "# последних ответов", + "ext-link": "Внешняя ссылка", + "subcategories-per-page": "Подкатегории на страницу", + "is-section": "Рассматривать эту категорию как секцию", + "post-queue": "Очередь на публикацию", + "tag-whitelist": "Разрешенный список меток", + "upload-image": "Загрузить изображение", + "delete-image": "Удалить", + "category-image": "Изображение категории", + "parent-category": "Родительская категория", + "optional-parent-category": "(Не обязательно) Родительская категория\n", + "top-level": "Верхний уровень", + "parent-category-none": "(Не указана)", + "copy-parent": "Скопировать из родительской", + "copy-settings": "Копировать настройки из", + "optional-clone-settings": "(Не обязательно) Копировать настройки из", + "clone-children": "Скопировать вложенные категории и их настройки", + "purge": "Очистить категорию", + + "enable": "Включить", + "disable": "Отключить", + "edit": "Редактировать", + "analytics": "Аналитика", + "view-category": "Перейти в категорию", + "set-order": "Установить порядковый номер", + "set-order-help": "Установка порядка категории переместит эту категорию в этот порядок и при необходимости обновит порядок других категорий. Минимальный порядок - 1, что ставит категорию на первое место.", + + "select-category": "Указать категорию", + "set-parent-category": "Указать родительскую категорию", + + "privileges.description": "Здесь вы можете настроить права доступа к разделам форума. Права могут предоставляться как отдельному пользователю, так и группе. Выберите область действия прав доступа в списке ниже.", + "privileges.category-selector": "Настройка прав доступа для", + "privileges.warning": "Примечание: Настройки прав доступа применяются сразу же, как вы их выбираете. Сохранять настройки категории для этого не нужно.", + "privileges.section-viewing": "Права на просмотр", + "privileges.section-posting": "Права на публикацию", + "privileges.section-moderation": "Права модераторов", + "privileges.section-other": "Другое", + "privileges.section-user": "Пользователь", + "privileges.search-user": "Добавить пользователя", + "privileges.no-users": "В этой категории нет специально заданных прав пользователя.", + "privileges.section-group": "Группа", + "privileges.group-private": "Это закрытая группа", + "privileges.inheritance-exception": "Эта группа не наследует привилегии от группы зарегистрированных пользователей", + "privileges.banned-user-inheritance": "Забаненные пользователи наследуют привилегии из группы забаненных пользователей", + "privileges.search-group": "Добавить группу", + "privileges.copy-to-children": "Скопировать в дочерние", + "privileges.copy-from-category": "Скопировать из категории", + "privileges.copy-privileges-to-all-categories": "Скопировать во все категории", + "privileges.copy-group-privileges-to-children": "Скопировать настройки доступа группы и применить ко всем дочерним подкатегориям.", + "privileges.copy-group-privileges-to-all-categories": "Скопировать настройки доступа группы и применить ко всем категориям.", + "privileges.copy-group-privileges-from": "Скопировать настройки доступа группы из другой категории.", + "privileges.inherit": "Если стандартная группа зарегистрированные пользователи получает определённые права, то все остальные группы также получают аналогичные права неявным образом, то есть, даже если они специально не заданы. Это происходит потому, что статус зарегистрированного пользователя распространяется на всех участников.", + "privileges.copy-success": "Настройки прав доступа скопированы!", + + "analytics.back": "Назад к списку категорий", + "analytics.title": "Статистика категории «%1»", + "analytics.pageviews-hourly": "График 1 – просмотров за час", + "analytics.pageviews-daily": "График 2 – просмотров за день", + "analytics.topics-daily": "График 3 – новых тем за день", + "analytics.posts-daily": "График 4 – новых сообщений за день", + + "alert.created": "Создано", + "alert.create-success": "Категория успешно создана!", + "alert.none-active": "У вас нет активных категорий.", + "alert.create": "Создать категорию", + "alert.confirm-purge": "

Вы точно хотите очистить категорию «%1»?

Предупреждение! Все темы и сообщения в этой категории будут удалены

Очистка категории удаляет все темы и сообщения, а также саму категорию из базы данных. Если вы хотите удалить категорию временно, вместо очистки вам нужно выбрать \"отключить\" .

", + "alert.purge-success": "Категория очищена!", + "alert.copy-success": "Настройки скопированы!", + "alert.set-parent-category": "Выбрать родительскую категорию", + "alert.updated": "Обновленные категории", + "alert.updated-success": "Категории с ID %1 успешно обновлены.", + "alert.upload-image": "Загрузить изображение категории", + "alert.find-user": "Найти пользователя", + "alert.user-search": "Искать пользователя...", + "alert.find-group": "Найти группу", + "alert.group-search": "Искать группу...", + "alert.not-enough-whitelisted-tags": "Разрешенный список меток меньше чем минимальные метки, вам необходимо создать больше разрешенных меток.", + "collapse-all": "Свернуть всё", + "expand-all": "Развернуть всё", + "disable-on-create": "Отключить при создании", + "no-matches": "Нет совпадений" +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/digest.json b/public/language/ru/admin/manage/digest.json new file mode 100644 index 0000000000..aa17069fb7 --- /dev/null +++ b/public/language/ru/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Здесь представлен отчёт об отправке регулярных рассылок-дайджестов.", + "disclaimer": "Пожалуйста, имейте в в виду, что в силу специфики работы электронной почты доставку писем гарантировать невозможно. Получит пользователь рассылку или нет, зависит от множества факторов: репутации сервера, наличие или отсутствие вашего IP-адреса в чёрных списках, наличие или отсутствие настроек DKIM/SPF/DMARC и т.п.", + "disclaimer-continued": "Успешная доставка означает, что NodeBB отправил письмо, и оно прибыло на почтовый сервер получателя. Это ещё не значит, что пользователь увидел его во «Входящих». Для более надёжной доставки мы рекомендуем использовать специальные сторонние сервисы, например SendGrid.", + + "user": "Пользователь", + "subscription": "Тип подписки", + "last-delivery": "Последняя доставка", + "default": "Стандартная системная", + "default-help": "Стандартная системная означает, что пользователь не выбирал своих собственных параметров рассылки и пользуется настройками по умолчанию. В данный момент это «%1».", + "resend": "Отправить заново", + "resend-all-confirm": "Вы уверены, что хотите повторить рассылку вручную?", + "resent-single": "Повтор рассылки вручную завершён", + "resent-day": "Ежедневная рассылка отправлена повторно", + "resent-week": "Еженедельная рассылка отправлена повторно", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Ежемесячная рассылка отправлена повторно", + "null": "Никогда", + "manual-run": "Повтор вручную:", + + "no-delivery-data": "Нет данных о доставке" +} diff --git a/public/language/ru/admin/manage/groups.json b/public/language/ru/admin/manage/groups.json new file mode 100644 index 0000000000..420c13e5e5 --- /dev/null +++ b/public/language/ru/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Название группы", + "badge": "Значок", + "properties": "Свойства", + "description": "Описание группы", + "member-count": "Число участников", + "system": "Системная", + "hidden": "Скрытая", + "private": "Закрытая", + "edit": "Редактировать", + "delete": "Удалить", + "privileges": "Права доступа", + "download-csv": "CSV", + "search-placeholder": "Поиск", + "create": "Создать группу", + "description-placeholder": "Краткое описание вашей группы", + "create-button": "Создать", + + "alerts.create-failure": "Ой-ой

Произошла ошибка создания вашей группы. Пожалуйста, попробуйте позже!

", + "alerts.confirm-delete": "Вы действительно хотите удалить эту группу?", + + "edit.name": "Название", + "edit.description": "Описание", + "edit.user-title": "Звание участников", + "edit.icon": "Иконка группы", + "edit.label-color": "Цвет ярлыка группы", + "edit.text-color": "Цвет текста на ярлыке", + "edit.show-badge": "Показывать значок группы", + "edit.private-details": "Если включено, то вступление в группу требует подтверждения от её владельца.", + "edit.private-override": "Внимание: Закрытые группы отключены на системном уровне.", + "edit.disable-join": "Отключить запросы на вступление", + "edit.disable-leave": "Запретить участникам покидать группу", + "edit.hidden": "Скрытая", + "edit.hidden-details": "Если включено, группа будет скрыта в списках, а участников необходимо будет приглашать вручную", + "edit.add-user": "Добавить участника в группу", + "edit.add-user-search": "Поиск участников", + "edit.members": "Список участников", + "control-panel": "Панель управления группой", + "revert": "Отменить", + + "edit.no-users-found": "Участников не найдено", + "edit.confirm-remove-user": "Вы уверены, что хотите удалить этого участника?", + "edit.save-success": "Изменения сохранены!" +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/privileges.json b/public/language/ru/admin/manage/privileges.json new file mode 100644 index 0000000000..87e9ba1af6 --- /dev/null +++ b/public/language/ru/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Глобальные", + "admin": "Админка", + "group-privileges": "Права групп", + "user-privileges": "Права пользователей", + "edit-privileges": "Редактировать права доступа", + "select-clear-all": "Выделить/Очистить всё", + "chat": "Чат", + "upload-images": "Загрузка изображений", + "upload-files": "Загрузка файлов", + "signature": "Подпись", + "ban": "Блокировка пользователей", + "mute": "Mute", + "invite": "Приглашать", + "search-content": "Поиск по содержимому", + "search-users": "Поиск пользователей", + "search-tags": "Поиск меток", + "view-users": "Просмотр пользователей", + "view-tags": "Просмотр меток", + "view-groups": "Просмотр групп", + "allow-local-login": "Локальный вход", + "allow-group-creation": "Создание групп", + "view-users-info": "Просмотр польз. данных", + "find-category": "Найти категорию", + "access-category": "Читать категорию", + "access-topics": "Читать темы", + "create-topics": "Создавать темы", + "reply-to-topics": "Отвечать в темах", + "schedule-topics": "Schedule Topics", + "tag-topics": "Присваивать метки", + "edit-posts": "Редактировать сообщения", + "view-edit-history": "Просм. историю версий", + "delete-posts": "Удалять сообщения", + "view_deleted": "Просм. удалённые сообщения", + "upvote-posts": "Повышать рейтинг", + "downvote-posts": "Понижать рейтинг", + "delete-topics": "Удалять темы", + "purge": "Стирать удалённое", + "moderate": "Модерировать", + "admin-dashboard": "Панель управления", + "admin-categories": "Категории", + "admin-privileges": "Права доступа", + "admin-users": "Пользователи", + "admin-admins-mods": "Администраторы & Моды", + "admin-groups": "Группы", + "admin-tags": "Тэги", + "admin-settings": "Настройки", + + "alert.confirm-moderate": "Вы уверены, что хотите наделить правами модерации эту группу? Эта группа является публичной, и к ней может присоединиться любой пользователь.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Вы действительно хотите сохранить эти права доступа", + "alert.saved": "Изменения прав доступа сохранены и применены", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Изменения прав доступа отменены", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "Это действие не может быть отменено.", + "alert.admin-warning": "Изначально Администраторы получают все привилегии", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/registration.json b/public/language/ru/admin/manage/registration.json new file mode 100644 index 0000000000..28598e917f --- /dev/null +++ b/public/language/ru/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Очередь", + "description": "В очереди регистраций нет пользователей.
Чтобы включить эту возможность перейдите в Настройки &arr; Пользователь &arr; Регистрация пользователя и задайте Тип регистрации как \"Подтверждение администратором\"", + + "list.name": "Имя", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Время", + "list.username-spam": "Частота: %1 Появлений: %2 Доверие: %3", + "list.email-spam": "Частота: %1 Появлений: %2", + "list.ip-spam": "Частота: %1 Появлений: %2", + + "invitations": "Приглашения", + "invitations.description": "Ниже приведен полный список отправленных приглашений. Для поиска по списку по электронной почте или имени пользователя используйте сочетание клавиш CTRL+F . < br > < br > Имена пользователей, которые приняли приглашение, будут отображаться справа от электронной почты.", + "invitations.inviter-username": "Имя пользователя приглашенного", + "invitations.invitee-email": "Email приглашенного", + "invitations.invitee-username": "Имя пользователя приглашенного (если зарегистрирован)", + + "invitations.confirm-delete": "Вы уверены, что хотите удалить это приглашение" +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/tags.json b/public/language/ru/admin/manage/tags.json new file mode 100644 index 0000000000..a1eca80414 --- /dev/null +++ b/public/language/ru/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "На вашем форуме пока нет тем с метками.", + "bg-color": "Цвет фона", + "text-color": "Цвет текста", + "description": "Нажмите на метку, чтобы выбрать её, или просто перетащите. Используйте клавишу Ctrl, чтобы выбрать несколько меток.", + "create": "Создать метку", + "modify": "Изменить метку", + "rename": "Переименовать метку", + "delete": "Удалить выбранные метки", + "search": "Поиск меток...", + "settings": "Настройки меток", + "name": "Название метки", + + "alerts.editing": "Редактирование меток", + "alerts.confirm-delete": "Вы хотите удалить выбранные метки?", + "alerts.update-success": "Метка обновлена!", + "reset-colors": "Сбросить цвета" +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/uploads.json b/public/language/ru/admin/manage/uploads.json new file mode 100644 index 0000000000..2514655a9b --- /dev/null +++ b/public/language/ru/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Загрузить Файл", + "filename": "Название", + "usage": "Использ. в сообщениях", + "orphaned": "Отделенный", + "size/filecount": "Размер / Файлов", + "confirm-delete": "Вы действительно хотите удалить этот файл?", + "filecount": "%1 файлов", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/ru/admin/manage/users.json b/public/language/ru/admin/manage/users.json new file mode 100644 index 0000000000..b675fe328c --- /dev/null +++ b/public/language/ru/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Пользователи", + "edit": "Actions", + "make-admin": "Сделать администратором", + "remove-admin": "Удалить администратора", + "validate-email": "Подтвердить адрес электронной почты", + "send-validation-email": "Отправить письмо с кодом подтверждения", + "password-reset-email": "Отправить письмо для сброса пароля", + "force-password-reset": "Сбросить пароль и завершить сессию пользователя", + "ban": "Заблокировать пользователя(-ей)", + "temp-ban": "Временно заблокировать пользователя(-ей)", + "unban": "Разблокировать пользователя(-ей)", + "reset-lockout": "Снять локаут", + "reset-flags": "Сбросить счётчик жалоб", + "delete": "Удалить пользователя(-ей)", + "delete-content": "Удалить данные пользователя(-ей)", + "purge": "Удалить пользователя(-ей) и данные", + "download-csv": "Скачать CSV", + "manage-groups": "Изменить членство в группах", + "add-group": "Добавить группу", + "create": "Create User", + "invite": "Invite by Email", + "new": "Новый пользователь", + "filter-by": "Фильтровать по", + "pills.unvalidated": "Не подтверждены", + "pills.validated": "Подтверждены", + "pills.banned": "Заблокированные", + + "50-per-page": "50 на страницу", + "100-per-page": "100 на страницу", + "250-per-page": "250 на страницу", + "500-per-page": "500 на страницу", + + "search.uid": "По ID пользователя", + "search.uid-placeholder": "Введите ID пользователя для поиска", + "search.username": "По имени пользователя", + "search.username-placeholder": "Введите имя пользователя для поиска", + "search.email": "По адресу электронной почты", + "search.email-placeholder": "Введите адрес электронной почты для поиска", + "search.ip": "По IP-адресу", + "search.ip-placeholder": "Введите IP-адрес для поиска", + "search.not-found": "Пользователь не найден!", + + "inactive.3-months": "3 месяца", + "inactive.6-months": "6 месяцев", + "inactive.12-months": "12 месяцев", + + "users.uid": "ID", + "users.username": "Логин", + "users.email": "E-mail", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "Сообщения", + "users.reputation": "Репутация", + "users.flags": "Жалобы", + "users.joined": "Регистрация", + "users.last-online": "В сети", + "users.banned": "Блокировка", + + "create.username": "Имя пользователя", + "create.email": "E-mail", + "create.email-placeholder": "Адрес электронной почты пользователя", + "create.password": "Пароль", + "create.password-confirm": "Подтвердите пароль", + + "temp-ban.length": "Length", + "temp-ban.reason": "Причина (Необязательно)", + "temp-ban.hours": "Часов", + "temp-ban.days": "Дней", + "temp-ban.explanation": "Укажите продолжительность блокировки. Имейте в виду, что «0» означает заблокировать навсегда.", + + "alerts.confirm-ban": "Вы действительно хотите заблокировать пользователя навсегда?", + "alerts.confirm-ban-multi": "Вы действительно хотите заблокировать этих пользователей навсегда?", + "alerts.ban-success": "Пользователь(и) заблокирован(ы)!", + "alerts.button-ban-x": "Заблокировать %1 пользователя(-ей)", + "alerts.unban-success": "Пользователь(и) разблокирован(ы)!", + "alerts.lockout-reset-success": "Локаут снят!", + "alerts.flag-reset-success": "Счётчик жалоб сброшен!", + "alerts.no-remove-yourself-admin": "Вы не можете удалить себя из администраторов!", + "alerts.make-admin-success": "Пользователь теперь Администратор.", + "alerts.confirm-remove-admin": "Вы действительно хотите удалить этого администратора?", + "alerts.remove-admin-success": "Пользователь больше не администратор.", + "alerts.make-global-mod-success": "Пользователь теперь Общий модератор .", + "alerts.confirm-remove-global-mod": "Вы действительно хотите удалить этого общего модератора?", + "alerts.remove-global-mod-success": "Пользователь больше не общий модератор.", + "alerts.make-moderator-success": "Пользователь теперь модератор.", + "alerts.confirm-remove-moderator": "Вы действительно хотите удалить этого модератора?", + "alerts.remove-moderator-success": "Пользователь больше не модератор.", + "alerts.confirm-validate-email": "Вы хотите подтвердить e-mail этого пользователя(-ей)?", + "alerts.confirm-force-password-reset": "Вы уверены, что хотите сбросить пароль и завершить сессию этого пользователя(-ей)?", + "alerts.validate-email-success": "Адреса электронной почты подтверждены", + "alerts.validate-force-password-reset-success": "Пароли были сброшены, а сессии — завершены.", + "alerts.password-reset-confirm": "Вы хотите отправить этому пользователю ссылку для сброса пароля?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Внимание!

Вы действительно хотите удалить этого пользователя(ей)?

Это действие необратимо! Будет удалена только учётная запись, темы и сообщения пользователя останутся.

", + "alerts.delete-success": "Пользователь(и) удален(ы)!", + "alerts.confirm-delete-content": "Внимание!

Вы действительно хотите удалить данные этого пользователя(ей)?

Это действие необратимо! Учётная запись пользователя сохранится, но его контент будет удалён!

", + "alerts.delete-content-success": "Данные пользователя(-ей) удалены!", + "alerts.confirm-purge": "Внимание!

Вы действительно хотите удалить этого пользователя(ей) и его данные?

Это действие необратимо! Будут удалены как учётная запись, так и весь контент пользователя!

", + "alerts.create": "Создать пользователя", + "alerts.button-create": "Создать", + "alerts.button-cancel": "Отмена", + "alerts.error-passwords-different": "Пароли должны совпадать!", + "alerts.error-x": "Ошибка

%1

", + "alerts.create-success": "Пользователь создан!", + + "alerts.prompt-email": "Адреса электронной почты:", + "alerts.email-sent-to": "Письмо с приглашением для %1 отправлено", + "alerts.x-users-found": "%1 пользователь(ей) найдено, (%2 секунды)", + "export-users-started": "Экспорт пользователей в формате CSV может занять некоторое время. Вы получите уведомление, по завершению процесса.", + "export-users-completed": "Пользователи, экспортированные в формате csv, нажмите здесь, чтобы загрузить." +} \ No newline at end of file diff --git a/public/language/ru/admin/menu.json b/public/language/ru/admin/menu.json new file mode 100644 index 0000000000..3ec3e5ce5c --- /dev/null +++ b/public/language/ru/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Панели управления", + "dashboard/overview": "Обзор", + "dashboard/logins": "Авторизаций", + "dashboard/users": "Пользователи", + "dashboard/topics": "Темы", + "dashboard/searches": "Searches", + "section-general": "Общие", + + "section-manage": "Управление", + "manage/categories": "Категории", + "manage/privileges": "Права доступа", + "manage/tags": "Метки", + "manage/users": "Пользователи", + "manage/admins-mods": "Администраторы и модераторы", + "manage/registration": "Очередь на регистрацию", + "manage/post-queue": "Очередь на публикацию", + "manage/groups": "Группы", + "manage/ip-blacklist": "Чёрный список IP", + "manage/uploads": "Загрузки", + "manage/digest": "Рассылки", + + "section-settings": "Настройки", + "settings/general": "Основные", + "settings/homepage": "Главная страница", + "settings/navigation": "Навигация", + "settings/reputation": "Репутация & Жалобы", + "settings/email": "Электронная почта", + "settings/user": "Пользователи", + "settings/group": "Группы", + "settings/guest": "Гости", + "settings/uploads": "Загрузки", + "settings/languages": "Языки", + "settings/post": "Сообщения", + "settings/chat": "Чаты", + "settings/pagination": "Пагинация", + "settings/tags": "Метки", + "settings/notifications": "Уведомления", + "settings/api": "Доступ API", + "settings/sounds": "Звуки", + "settings/social": "Шэринг", + "settings/cookies": "Куки", + "settings/web-crawler": "Индексация", + "settings/sockets": "Сокеты", + "settings/advanced": "Расширенные", + + "settings.page-title": "Настройки %1", + + "section-appearance": "Оформление", + "appearance/themes": "Темы", + "appearance/skins": "Стили", + "appearance/customise": "Настройка контента (HTML/JS/CSS)", + + "section-extend": "Расширения", + "extend/plugins": "Плагины", + "extend/widgets": "Виджеты", + "extend/rewards": "Награды", + + "section-social-auth": "Авторизация", + + "section-plugins": "Плагины", + "extend/plugins.install": "Установка плагинов", + + "section-advanced": "Расширенные", + "advanced/database": "База данных", + "advanced/events": "Журнал событий", + "advanced/hooks": "Хуки", + "advanced/logs": "Системный журнал", + "advanced/errors": "Журнал ошибок", + "advanced/cache": "Кэш", + "development/logger": "Отладочный журнал", + "development/info": "Сведения о системе", + + "rebuild-and-restart-forum": "Пересобрать и перезапустить форум", + "restart-forum": "Перезапустить форум", + "logout": "Выйти", + "view-forum": "Перейти на форум", + + "search.placeholder": "Search settings", + "search.no-results": "Нет результата...", + "search.search-forum": "Искать на форуме", + "search.keep-typing": "Наберите ещё что-нибудь, чтобы увидеть результат...", + "search.start-typing": "Наберите что-нибудь, чтобы увидеть результат...", + + "connection-lost": "Соединение с %1 потеряно, пытаюсь переподключиться...", + + "alerts.version": "Вы используете NodeBB версии %1", + "alerts.upgrade": "Обновить до v%1" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/advanced.json b/public/language/ru/admin/settings/advanced.json new file mode 100644 index 0000000000..caf3bd0ef3 --- /dev/null +++ b/public/language/ru/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Режим техобслуживания", + "maintenance-mode.help": "Когда включен режим техобслуживания, все запросы перенаправляются на специальную страницу-заглушку. Только администраторы сохраняют обычный доступ к форуму.", + "maintenance-mode.status": "Код состояния HTTP для страницы-заглушки", + "maintenance-mode.message": "Сообщение для пользователей", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Заголовки", + "headers.allow-from": "Опция ALLOW-FROM для использования NodeBB через iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Настройка заголовка «Powered By», отправляемого NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Регулярное выражение для Access-Control-Allow-Origin", + "headers.acao-help": "Оставьте пустым, чтобы запретить доступ всем сайтам", + "headers.acao-regex-help": "Введите регулярное выражение для проверки прав доступа. Оставьте поле пустым, чтобы запретить доступ всем сайтам", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Строгая политика безопасности транспортного уровня", + "hsts.enabled": "Включить HSTS (рекомендуется)", + "hsts.maxAge": "Срок действия заголовка HSTS", + "hsts.subdomains": "Включать в заголовок HSTS поддомены", + "hsts.preload": "Разрешить предзагрузку заголовка HSTS", + "hsts.help": "Включите, чтобы установить заголовок HSTS для этого сайта, а также настроить его предзагрузку или использование поддоменов. Если вы не уверены, какими должны быть эти параметры, оставьте всё как есть. Дополнительная информация ", + "traffic-management": "Управление трафиком", + "traffic.help": "NodeBB может автоматически блокировать соединения при высокой нагрузке. Настройте параметры блокировки как считаете нужным, хотя настройки по умолчанию и так вполне хороши.", + "traffic.enable": "Включить управление трафиком", + "traffic.event-lag": "Порог лага Event Loop (в миллисекундах)", + "traffic.event-lag-help": "Уменьшение этого значение ускорит загрузку страниц, но также может привести к показу сообщения \"высокая нагрузка\" большому количеству участников. (Необходим перезапуск)", + "traffic.lag-check-interval": "Интервал проверки (в миллисекундах)", + "traffic.lag-check-interval-help": "Снижение значения этого параметра приведет увеличению чувствительности NodeBB к пикам нагрузки, но также может сделать эту проверку слишком чувствительной. (Необходим перезапуск)", + + "sockets.settings": "Настройки протокола WebSocket", + "sockets.max-attempts": "Макс. попыток переподключения", + "sockets.default-placeholder": "По умолчанию: %1", + "sockets.delay": "Задержка", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/api.json b/public/language/ru/admin/settings/api.json new file mode 100644 index 0000000000..6fc2a1e8ac --- /dev/null +++ b/public/language/ru/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Токены", + "settings": "Настройки", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "ID пользователя", + "uid-help-text": "Укажите идентификатор пользователя, который нужно связать с этим токеном. Если идентификатор пользователя равен 0, он будет считаться главным токеном, который может предполагать идентичность других пользователей на основе параметра _uid.", + "description": "Описание", + "no-description": "Описания нет.", + "token-on-save": "Токен будет сгенерирован после сохранения формы" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/chat.json b/public/language/ru/admin/settings/chat.json new file mode 100644 index 0000000000..d34673dfcc --- /dev/null +++ b/public/language/ru/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Настройки чата", + "disable": "Отключить чат", + "disable-editing": "Отключить редактирование и удаление сообщений чата", + "disable-editing-help": "Администраторы и общие модераторы освобождены от этого ограничения.", + "max-length": "Максимальная длина сообщений в чате", + "max-room-size": "Максимальное кол-во пользователей в чат-комнатах", + "delay": "Пауза между сообщениями (в миллисекундах)", + "notification-delay": "Задержка уведомления для сообщений чата. (0 без задержки)", + "restrictions.seconds-edit-after": "Через сколько секунд после отправки сообщение будет нельзя отредактировать (0 — время не ограничено)", + "restrictions.seconds-delete-after": "Через сколько секунд после отправки сообщение будет нельзя удалить (0 — время не ограничено)" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/cookies.json b/public/language/ru/admin/settings/cookies.json new file mode 100644 index 0000000000..8d2ef49848 --- /dev/null +++ b/public/language/ru/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Согласие на использование cookie", + "consent.enabled": "Включено", + "consent.message": "Текст уведомления", + "consent.acceptance": "Текст сообщения о согласии", + "consent.link-text": "Текст ссылки на правила", + "consent.link-url": "URL ссылки на правила", + "consent.blank-localised-default": "Оставьте пустым, чтобы использовать стандартные настройки NodeBB", + "settings": "Настройки", + "cookie-domain": "Домен для cookie сессии", + "max-user-sessions": "Максимальное количество сессий на пользователя", + "blank-default": "Оставьте пустым для настроек по умолчанию" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/email.json b/public/language/ru/admin/settings/email.json new file mode 100644 index 0000000000..8c519bc778 --- /dev/null +++ b/public/language/ru/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Настройки электронной почты", + "address": "Адрес электронной почты", + "address-help": "Этот адрес получатели писем увидят в полях «От кого» и «Ответить».", + "from": "От кого", + "from-help": "Имя отправителя письма (или название форума).", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "Сервис SMTP", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Выберите один из популярных сервисов или укажите свой почтовый сервер.", + "smtp-transport.service": "Выберите сервис", + "smtp-transport.service-custom": "Другой сервис", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "Сервер SMTP", + "smtp-transport.port": "Порт SMTP", + "smtp-transport.security": "Безопасность соединения", + "smtp-transport.security-encrypted": "Зашифрованное", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Отсутствует", + "smtp-transport.username": "Имя пользователя", + "smtp-transport.username-help": "Для Gmail нужно указать полный адрес электронной почты, особенно если вы используете Google Apps.", + "smtp-transport.password": "Пароль", + "smtp-transport.pool": "Включить объединенные подключения", + "smtp-transport.pool-help": "Объединение соединений не позволяет NodeBB создавать новое соединение для каждой электронной почты. Этот параметр применяется только в том случае, если включен транспортный протокол SMTP.", + + "template": "Шаблоны писем", + "template.select": "Выберите шаблон письма", + "template.revert": "Вернуть стандартный", + "testing": "Проверка отправки", + "testing.select": "Выберите шаблон письма", + "testing.send": "Отправить проверочное письмо", + "testing.send-help": "Проверочное письмо будет отправлено на электронную почту пользователя, который сейчас пользуется панелью администратора.", + "subscriptions": "Новостные рассылки", + "subscriptions.disable": "Отключить новостные рассылки", + "subscriptions.hour": "Час отправки", + "subscriptions.hour-help": "Введите число, соответствующее номеру часа (например, 0 для полуночи, 17 для 17:00). Имейте в виду, что время определяется по часовому поясу сервера.
Текущее время сервера:
Следующая рассылка запланирована на ", + "notifications.remove-images": "Удалить изображения из уведомлений по электронной почте", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/ru/admin/settings/general.json b/public/language/ru/admin/settings/general.json new file mode 100644 index 0000000000..06263dbb2d --- /dev/null +++ b/public/language/ru/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Настройки сайта", + "title": "Название сайта", + "title.short": "Краткий заголовок", + "title.short-placeholder": "Если здесь ничего не указано, будет использовано название сайта", + "title.url": "Title Link URL", + "title.url-placeholder": "URL для названия сайта", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Название вашего сообщества", + "title.show-in-header": "Показывать название в шапке сайта", + "browser-title": "Название для браузера", + "browser-title-help": "Если здесь ничего не указано, будет использовано название сайта", + "title-layout": "Макет заголовка", + "title-layout-help": "Укажите, как сформировать заголовок для браузера, напр.\n{название сайта} | {название для браузера}", + "description.placeholder": "Краткое описание вашего сообщества", + "description": "Описание сайта", + "keywords": "Ключевые слова для сайта", + "keywords-placeholder": "Укажите через запятую ключевые слова, описывающие ваше сообщество", + "logo": "Логотип сайта", + "logo.image": "Логотип в шапке сайта", + "logo.image-placeholder": "Путь к файлу логотипа ", + "logo.upload": "Загрузить", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "URL для логотипа", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Замещающий текст", + "log.alt-text-placeholder": "Текст, который появится, если логотип не загрузится или загрузка изображений будет отключена", + "favicon": "Favicon", + "favicon.upload": "Загрузить", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Загрузить", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Внешние ссылки", + "outgoing-links.warning-page": "Предупреждать, когда пользователь переходит по внешним ссылкам", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Список доменов, для которых страница предупреждения отключена", + "site-colors": "Цвета сайта", + "theme-color": "Цвет темы", + "background-color": "Цвет фона", + "background-color-help": "Эти цвета используются на экране-заставке, если сайт установлен как приложение PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/ru/admin/settings/group.json b/public/language/ru/admin/settings/group.json new file mode 100644 index 0000000000..c89d2d163c --- /dev/null +++ b/public/language/ru/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Основные", + "private-groups": "Закрытые группы", + "private-groups.help": "Когда эта настройка включена, присоединиться к группе можно только с разрешения её владельца (Включено по умолчанию)", + "private-groups.warning": "Внимание! Если вы отключите эту опцию, все закрытые группы автоматически станут открытыми.", + "allow-multiple-badges": "Разрешить использовать несколько значков сразу", + "allow-multiple-badges-help": "Разрешить пользователям выбирать несколько значков групп (для полноценной работы этой функции требуется её поддержка в теме оформления форума).", + "max-name-length": "Максимальная длина названия группы", + "max-title-length": "Максимальная длина звания участника группы", + "cover-image": "Обложка группы", + "default-cover": "Стандартные обложки", + "default-cover-help": "Добавьте через запятую пути к изображениям, которые будут использованы, если у группы нет собственной обложки" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/guest.json b/public/language/ru/admin/settings/guest.json new file mode 100644 index 0000000000..f5af0554bd --- /dev/null +++ b/public/language/ru/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Настройки", + "handles.enabled": "Разрешить гостям выбирать имена", + "handles.enabled-help": "Эта настройка добавляет поле, в котором гость сможет указать имя, под которым он хочет оставить сообщение. Когда она выключена, вместо имени будет написано просто «Гость».", + "topic-views.enabled": "Разрешить гостям увеличивать количество просмотров тем", + "reply-notifications.enabled": "Разрешить гостям создавать уведомления об ответах" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/homepage.json b/public/language/ru/admin/settings/homepage.json new file mode 100644 index 0000000000..2b6c14fe4d --- /dev/null +++ b/public/language/ru/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Главная страница", + "description": "Выберите, какую страницу показывать по корневому URL форума.", + "home-page-route": "Маршрут для главной страницы", + "custom-route": "Другой маршрут", + "allow-user-home-pages": "Разрешить пользователям выбирать персональные главные страницы", + "home-page-title": "Заголовок домашней страницы («Главная» по умолчанию)" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/languages.json b/public/language/ru/admin/settings/languages.json new file mode 100644 index 0000000000..f7a6b365f9 --- /dev/null +++ b/public/language/ru/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Языковые настройки", + "description": "Язык по умолчанию определяет языковые настройки для всех посетителей форума.
Зарегистрированные пользователи могут выбрать другой язык в настройках своего профиля.", + "default-language": "Язык по умолчанию", + "auto-detect": "Автоматически определять язык для гостей" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/navigation.json b/public/language/ru/admin/settings/navigation.json new file mode 100644 index 0000000000..d5ad4c9534 --- /dev/null +++ b/public/language/ru/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Иконка:", + "change-icon": "изменить", + "route": "Маршрут:", + "tooltip": "Подсказка:", + "text": "Текст:", + "text-class": "Класс текста: опционально", + "class": "Класс: опционально", + "id": "ID: опционально", + + "properties": "Свойства:", + "groups": "Группы:", + "open-new-window": "Открывать в новом окне", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Удалить", + "btn.disable": "Выключить", + "btn.enable": "Включить", + + "available-menu-items": "Доступные пункты меню", + "custom-route": "Произвольный маршрут", + "core": "ядро", + "plugin": "плагин" +} diff --git a/public/language/ru/admin/settings/notifications.json b/public/language/ru/admin/settings/notifications.json new file mode 100644 index 0000000000..d6a0478bc5 --- /dev/null +++ b/public/language/ru/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Уведомления", + "welcome-notification": "Приветственное уведомление", + "welcome-notification-link": "Ссылка в уведомлении", + "welcome-notification-uid": "UID отправителя", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/pagination.json b/public/language/ru/admin/settings/pagination.json new file mode 100644 index 0000000000..896180fd8d --- /dev/null +++ b/public/language/ru/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Настройка разбивки на страницы", + "enable": "Разбивать темы и сообщения на страницы вместо бесконечной прокрутки.", + "posts": "Пагинация сообщений", + "topics": "Разбивка темы на страницы", + "posts-per-page": "Сообщений на страницу", + "max-posts-per-page": "Максимальное кол-во сообщений на странице", + "categories": "Разбивка категорий на страницы", + "topics-per-page": "Тем на странице", + "max-topics-per-page": "Максимальное количество тем на странице", + "categories-per-page": "Категорий на страницу" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/post.json b/public/language/ru/admin/settings/post.json new file mode 100644 index 0000000000..c2e7fdf0d4 --- /dev/null +++ b/public/language/ru/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Сортировка сообщений", + "sorting.post-default": "Стандартная сортировка сообщений", + "sorting.oldest-to-newest": "Сначала старые", + "sorting.newest-to-oldest": "Сначала новые", + "sorting.most-votes": "По количеству голосов", + "sorting.most-posts": "По количеству сообщений", + "sorting.topic-default": "Стандартная сортировка тем", + "length": "Длина сообщения", + "post-queue": "Очередь на публикацию", + "restrictions": "Ограничения на публикацию", + "restrictions-new": "Ограничения для новых пользователей", + "restrictions.post-queue": "Включить очередь на публикацию", + "restrictions.post-queue-rep-threshold": "Минимум репутации для публикации без проверки", + "restrictions.groups-exempt-from-post-queue": "Выберите группы, участники которых смогут публиковать сообщения без предварительной проверки", + "restrictions-new.post-queue": "Включить ограничения для новых пользователей", + "restrictions.post-queue-help": "Сообщения от новых пользователей будут опубликованы только после проверки модератором", + "restrictions-new.post-queue-help": "Включение ограничений для новых пользователей будет устанавливать ограничения для сообщений, создаваемых новыми пользователями.", + "restrictions.seconds-between": "Пауза между сообщениями (в секундах)", + "restrictions.seconds-between-new": "Пауза между сообщениями новых пользователей (в секундах)", + "restrictions.rep-threshold": "Минимум репутации, чтобы снять это ограничение", + "restrictions.seconds-before-new": "Пауза перед тем, как новый пользователь сможет написать первое сообщение (в секундах)", + "restrictions.seconds-edit-after": "Через сколько секунд после отправки сообщение нельзя будет отредактировать (0 — время не ограничено)", + "restrictions.seconds-delete-after": "Через сколько секунд после отправки сообщение нельзя будет удалить (0 — время не ограничено)", + "restrictions.replies-no-delete": "Кол-во ответов, после которого пользователям будет запрещено удалять тему (0 — ограничения нет)", + "restrictions.min-title-length": "Минимальная длина названия", + "restrictions.max-title-length": "Максимальная длина названия", + "restrictions.min-post-length": "Минимальная длина сообщения", + "restrictions.max-post-length": "Максимальная длина сообщения", + "restrictions.days-until-stale": "Через сколько дней тема будет считаться устаревшей", + "restrictions.stale-help": "Если тема считается устаревшей, пользователь увидит соответствующее предупреждение при попытке ответить в ней.", + "timestamp": "Дата и время", + "timestamp.cut-off": "Порог отсечки (в днях)", + "timestamp.cut-off-help": "Сначала дата и время сообщения будут отображаться в относительном виде: «3 часа назад» / «5 дней назад», локализованным в соответствии с языковыми настройками форума. Затем формат изменится на обычный: 5 Ноября 2016 15:30.
(Стандартная отсечка: 30, или один месяц). Введите 0, чтобы всегда отображать обычные дату и время, или оставьте поле пустым, чтобы всегда использовать относительный вид", + "timestamp.necro-threshold": "Порог устаревания (в днях)", + "timestamp.necro-threshold-help": "Сообщение будет отображаться между сообщениями, если время между ними превышает пороговое значение. (По умолчанию: 7, или одна неделя). Установите 0 чтобы отключить.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Сообщение-анонс", + "teaser.last-post": "Последнее – показать последнее сообщение в теме (первое, если ответов нет).", + "teaser.last-reply": "Последнее – показать последнее сообщение или пометку «Ответов нет»", + "teaser.first": "Первое сообщение", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Настройка списка непрочитанных тем", + "unread.cutoff": "Порог отсечки (в днях)", + "unread.min-track-last": "Минимальное кол-во сообщений в теме, чтобы начать отслеживать непрочитанные ответы", + "recent": "Настройка списка последних тем", + "recent.max-topics": "Макс. кол-во тем на странице /recent", + "recent.categoryFilter.disable": "Отключить фильтрацию тем из игнорируемых категорий для списка последних тем", + "signature": "Настройка подписей", + "signature.disable": "Отключить подписи", + "signature.no-links": "Отключить ссылки в подписях", + "signature.no-images": "Отключить картинки в подписях", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Максимальная длина подписи", + "composer": "Настройки редактора", + "composer-help": "Эти настройки определяют функциональность и/или внешний вид редактора сообщений", + "composer.show-help": "Показывать вкладку с подсказками", + "composer.enable-plugin-help": "Разрешить плагинам добавлять подсказки на вкладку", + "composer.custom-help": "Пользовательский текст для вкладки с подсказками", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Отслеживание IP", + "ip-tracking.each-post": "Отслеживать IP для каждого сообщения", + "enable-post-history": "Включить историю правок" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/reputation.json b/public/language/ru/admin/settings/reputation.json new file mode 100644 index 0000000000..0b78ff8a39 --- /dev/null +++ b/public/language/ru/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Настройка системы репутации", + "disable": "Выключить отслеживание репутации", + "disable-down-voting": "Отключить понижение рейтинга", + "votes-are-public": "Все голоса общедоступны", + "thresholds": "Пороговые значения", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Минимальная репутация для понижения рейтинга сообщения", + "downvotes-per-day": "Количество отрицательных голосов в день (установите 0 для отключения ограничения)", + "downvotes-per-user-per-day": "Количество отрицательных голосов за участника в день (установите 0 для неограниченного количества отрицательных голосов)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Минимальная репутация для отправки жалобы на сообщение", + "min-rep-website": "Минимальная репутация, чтобы заполнить поле «Веб-сайт» в профиле пользователя", + "min-rep-aboutme": "Минимальная репутация, чтобы добавить «Обо мне» в профиль пользователя", + "min-rep-signature": "Минимальная репутация, чтобы заполнить поле «Подпись» в профиле пользователя", + "min-rep-profile-picture": "Минимальная репутация для загрузки аватара пользователя", + "min-rep-cover-picture": "Минимальная репутация для загрузки обложки профиля пользователя", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/social.json b/public/language/ru/admin/settings/social.json new file mode 100644 index 0000000000..7a4239e955 --- /dev/null +++ b/public/language/ru/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Делиться сообщениями в", + "info-plugins-additional": "Плагины могут добавить дополнительные опции для функции «поделиться сообщением»", + "save-success": "Настройки функции «поделиться сообщением» сохранены!" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/sockets.json b/public/language/ru/admin/settings/sockets.json new file mode 100644 index 0000000000..43e0bd1bbb --- /dev/null +++ b/public/language/ru/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Настройки переподключения", + "max-attempts": "Макс. попыток", + "default-placeholder": "По умолчанию: %1", + "delay": "Задержка" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/sounds.json b/public/language/ru/admin/settings/sounds.json new file mode 100644 index 0000000000..f84b71d629 --- /dev/null +++ b/public/language/ru/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Уведомления", + "chat-messages": "Сообщения чата", + "play-sound": "Воспроизвести", + "incoming-message": "Входящие сообщения", + "outgoing-message": "Исходящие сообщения", + "upload-new-sound": "Загрузить новый звук", + "saved": "Настройки сохранены" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/tags.json b/public/language/ru/admin/settings/tags.json new file mode 100644 index 0000000000..8a46a7ea04 --- /dev/null +++ b/public/language/ru/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Настройки меток", + "link-to-manage": "Управление метками", + "system-tags": "Системные метки", + "system-tags-help": "Только привилегированные пользователи могут использовать эти метки.", + "min-per-topic": "Минимальное количество меток в теме", + "max-per-topic": "Максимальное количество меток в теме", + "min-length": "Минимальная длина метки", + "max-length": "Максимальная длина метки", + "related-topics": "Похожие темы", + "max-related-topics": "Максимальное количество похожих тем для отображения (если тема поддерживает эту настройку)" +} \ No newline at end of file diff --git a/public/language/ru/admin/settings/uploads.json b/public/language/ru/admin/settings/uploads.json new file mode 100644 index 0000000000..a53dcde6ce --- /dev/null +++ b/public/language/ru/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Сообщения", + "orphans": "Orphaned Files", + "private": "Не показывать загрузки гостям", + "strip-exif-data": "Удалять метаданные EXIF", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Типы файлов, которые следует скрывать от гостей", + "private-uploads-extensions-help": "Укажите через запятую список расширений файлов, например pdf,xls,doc. Оставьте поле пустым, чтобы все загрузки были недоступны гостям.", + "resize-image-width-threshold": "Уменьшать изображения, когда ширина превышает", + "resize-image-width-threshold-help": "(в пикс., стандартная настройка: 1520, укажите 0, чтобы отключить)", + "resize-image-width": "Уменьшать изображения до", + "resize-image-width-help": "(в пикс., стандартная настройка: 760, укажите 0, чтобы отключить)", + "resize-image-quality": "Качество уменьшаемых изображений", + "resize-image-quality-help": "Чем ниже качество, тем меньше размер файла.", + "max-file-size": "Макс. размер файла (в КиБ)", + "max-file-size-help": "(в кибибайтах, по умолчанию: 2048 КиБ)", + "reject-image-width": "Макс. ширина изображения (в пикселях)", + "reject-image-width-help": "Загрузка изображений шире указанного значения будет отклонена.", + "reject-image-height": "Макс. высота изображения (в пикселях)", + "reject-image-height-help": "Загрузка изображений выше указанного значения будет отклонена.", + "allow-topic-thumbnails": "Разрешить пользователям загружать миниатюры для тем", + "topic-thumb-size": "Размер миниатюр", + "allowed-file-extensions": "Допустимые расширения файлов", + "allowed-file-extensions-help": "Укажите через запятую список расширений файлов, например pdf,xls,doc. Оставьте поле пустым, чтобы разрешить любые загрузки.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Аватарки пользователей", + "allow-profile-image-uploads": "Разрешить пользователям загружать аватарки", + "convert-profile-image-png": "Конвертировать загруженные изображения в PNG", + "default-avatar": "Стандартная аватарка", + "upload": "Загрузить", + "profile-image-dimension": "Размер аватарки", + "profile-image-dimension-help": "(в пикселях, по умолчанию 128 пикселей)", + "max-profile-image-size": "Макс. размер файла аватарки", + "max-profile-image-size-help": "(в кибибайтах, по умолчанию 256 КиБ)", + "max-cover-image-size": "Макс. размер файла обложки", + "max-cover-image-size-help": "(в кибибайтах, по умолчанию: 2048 КиБ)", + "keep-all-user-images": "Сохранять на сервере прошлые версии аватарок и обложек", + "profile-covers": "Обложки профиля", + "default-covers": "Стандартные обложки", + "default-covers-help": "Добавьте через запятую пути к изображениям, которые будут использованы, если пользователь не загрузил собственную обложку" +} diff --git a/public/language/ru/admin/settings/user.json b/public/language/ru/admin/settings/user.json new file mode 100644 index 0000000000..cfd9c08a47 --- /dev/null +++ b/public/language/ru/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Авторизация", + "email-confirm-interval": "Пользователь не сможет снова запросить код подтверждения, пока не пройдёт", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Разрешить вход с помощью", + "allow-login-with.username-email": "Имени пользователя или адреса электронной почты", + "allow-login-with.username": "Только имени пользователя", + "account-settings": "Настройки учётной записи", + "gdpr_enabled": "Запрашивать согласие на сбор и обработку персональных данных", + "gdpr_enabled_help": "Включите, чтобы при регистрации новых пользователей запрашивать согласие на сбор и обработку данных в соответствии с законом о General Data Protection Regulation (GDPR). Примечание: эта настойка не повлияет на уже зарегистрированных пользователей. Чтобы запросить их согласие, установите плагин GDPR.", + "disable-username-changes": "Запретить смену имени пользователя", + "disable-email-changes": "Запретить смену адреса электронной почты", + "disable-password-changes": "Запретить смену пароля", + "allow-account-deletion": "Разрешить удалять учётную запись", + "hide-fullname": "Скрывать полное имя от других пользователей", + "hide-email": "Скрывать e-mail от других пользователей", + "show-fullname-as-displayname": "Показывать полное имя пользователя в качестве отображаемого имени, если доступно", + "themes": "Оформление", + "disable-user-skins": "Запретить пользователям выбирать стиль темы", + "account-protection": "Защита учётных записей", + "admin-relogin-duration": "Период неактивности (в минутах) до повторного входа в панель администратора", + "admin-relogin-duration-help": "Укажите продолжительность периода неактивности, после которого потребуется снова ввести логин и пароль администратора. Введите 0, чтобы отключить эту опцию", + "login-attempts": "Попыток входа в час", + "login-attempts-help": "Если это значение будет превышено, учётная запись пользователя будет заблокирована на указанный промежуток времени", + "lockout-duration": "Длительность блокировки (в минутах)", + "login-days": "На сколько дней сохранять сессию авторизованного пользователя", + "password-expiry-days": "Принудительно сбрасывать пароли через указанное кол-во дней", + "session-time": "Продолжительность сессии", + "session-time-days": "Дни", + "session-time-seconds": "Секунды", + "session-time-help": "Эти значения используются для определения того, как долго участник остается в системе, когда он включает "Запомнить меня" при входе. Обратите внимание на то, что будет использовано только одно из этих значений. Если значение секунды отсутствует, мы возвращаемся к значению дни. Если значение дни отсутствует, по умолчанию используется значение 14 дней.", + "online-cutoff": "Через сколько минут пользователь будет считаться неактивным", + "online-cutoff-help": "Если участник не выполняет никаких действий в течение этого времени, он считается неактивным и не получает обновлений в реальном времени.", + "registration": "Регистрация пользователей", + "registration-type": "Тип регистрации", + "registration-approval-type": "Тип подтверждения регистрации", + "registration-type.normal": "Обычный", + "registration-type.admin-approval": "Подтверждается администратором", + "registration-type.admin-approval-ip": "Подтверждается администратором для известных IP-адресов", + "registration-type.invite-only": "Только по приглашениям", + "registration-type.admin-invite-only": "Только по приглашению администратора", + "registration-type.disabled": "Регистрация отключена", + "registration-type.help": "Обычный – пользователи могут регистрироваться без ограничений.
\nТолько по приглашениям – существующие пользователи могут приглашать других участников
\nТолько по приглашению администратора – только администратор может приглашать пользователей на странице Пользователи или в панели администратора.
\nРегистрация отключена – пользователи не могут регистрироваться.
", + "registration-approval-type.help": "Обычный – регистрация подтверждается автоматически.
\nПодтверждается администратором – заявки на регистрацию помещаются в очередь на одобрение.
\nПодтверждается администратором для известных IP-адресов – новые пользователи регистрируются как обычно, но если заявка поступает с такого же IP, как у существующего пользователя, требуется подтверждение администратора.
", + "registration-queue-auto-approve-time": "Время автоматического утверждения", + "registration-queue-auto-approve-time-help": "За несколько часов до автоматического утверждения пользователя. 0 для отключения.", + "registration-queue-show-average-time": "Показывать пользователям среднее время, необходимое для утверждения нового пользователя", + "registration.max-invites": "Макс. приглашений у пользователя", + "max-invites": "Макс. приглашений у пользователя", + "max-invites-help": "0 – без ограничений. Администраторы в любом случае могут приглашать бесконечно
Эта настройка действует только в режиме регистрации только по приглашениям.", + "invite-expiration": "Срок действия приглашения", + "invite-expiration-help": "Указывается в днях.", + "min-username-length": "Минимальная длина имени пользователя", + "max-username-length": "Максимальная длина имени пользователя", + "min-password-length": "Минимальная длина пароля", + "min-password-strength": "Минимальная сложность пароля", + "max-about-me-length": "Максимальная длина поля «Обо мне»", + "terms-of-use": "Правила использования форума (оставьте пустым, чтобы отключить)", + "user-search": "Поиск пользователей", + "user-search-results-per-page": "Количество отображаемых результатов", + "default-user-settings": "Стандартные настройки профиля пользователя", + "show-email": "Показывать адрес электронной почты", + "show-fullname": "Показывать полное имя", + "restrict-chat": "Разрешить чат только с теми, на кого подписаны", + "outgoing-new-tab": "Открывать внешние ссылки в новой вкладке", + "topic-search": "Включить поиск по сообщениям внутри тем", + "update-url-with-post-index": "Обновлять URL-адрес с индексом публикации при просмотре тем", + "digest-freq": "Подписка на дайджест", + "digest-freq.off": "Отключена", + "digest-freq.daily": "Ежедневная", + "digest-freq.weekly": "Еженедельная", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Ежемесячная", + "email-chat-notifs": "Уведомить по электронной почте, если пришло новое сообщение в чат, а я не в сети", + "email-post-notif": "Уведомить по электронной почте, если в отслеживаемой теме появилось новое сообщение", + "follow-created-topics": "Включать отслеживание всех тем, которые вы создаёте", + "follow-replied-topics": "Включать отслеживание во всех темах, в которых вы отвечаете", + "default-notification-settings": "Стандартные настройки уведомлений", + "categoryWatchState": "Стандартные настройки отслеживания категорий", + "categoryWatchState.watching": "Отслеживается", + "categoryWatchState.notwatching": "Не отслеживается", + "categoryWatchState.ignoring": "Игнорируется" +} diff --git a/public/language/ru/admin/settings/web-crawler.json b/public/language/ru/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2edbd1da0f --- /dev/null +++ b/public/language/ru/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Настройка индексирования", + "robots-txt": "Пользовательский robots.txt Оставьте поле пустым, чтобы использовать стандартный", + "sitemap-feed-settings": "Карта сайта и RSS", + "disable-rss-feeds": "Отключить RSS", + "disable-sitemap-xml": "Отключить Sitemap.xml", + "sitemap-topics": "Сколько тем указывать в карте сайта", + "clear-sitemap-cache": "Очистить кеш карты сайта", + "view-sitemap": "Посмотреть карту сайта" +} \ No newline at end of file diff --git a/public/language/ru/category.json b/public/language/ru/category.json new file mode 100644 index 0000000000..3b5a7f0cfe --- /dev/null +++ b/public/language/ru/category.json @@ -0,0 +1,23 @@ +{ + "category": "Категория", + "subcategories": "Подкатегории", + "new_topic_button": "Создать тему", + "guest-login-post": "Авторизуйтесь, чтобы написать сообщение", + "no_topics": "В этой категории еще нет тем.
Почему бы вам не создать первую?", + "browsing": "просматривают", + "no_replies": "Нет ответов", + "no_new_posts": "Нет новых сообщений", + "watch": "Отслеживать", + "ignore": "Игнорировать", + "watching": "Отслеживается", + "not-watching": "Не отслеживается", + "ignoring": "Игнорируется", + "watching.description": "Показывать темы из этой категории в списках непрочитанных и недавних", + "not-watching.description": "Не показывать темы из этой категории в непрочитанных, но оставить в списке недавних", + "ignoring.description": "Не показывать темы из этой категории ни в списке непрочитанных, ни в недавних", + "watching.message": "Вы отслеживаете обновления этой категории, включая все подкатегории", + "notwatching.message": "Вы более не отслеживаете обновления этой категории, включая все подкатегории", + "ignoring.message": "Вы игнорируете обновления этой категории, включая все подкатегории", + "watched-categories": "Отслеживаемые категории", + "x-more-categories": "Еще %1 категорий" +} \ No newline at end of file diff --git a/public/language/ru/email.json b/public/language/ru/email.json new file mode 100644 index 0000000000..6c010cff83 --- /dev/null +++ b/public/language/ru/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Проверочное письмо", + "password-reset-requested": "Получен запрос на сброс пароля!", + "welcome-to": "Добро пожаловать на форум %1", + "invite": "Приглашение от %1", + "greeting_no_name": "Здравствуйте!", + "greeting_with_name": "Здравствуйте, %1!", + "email.verify-your-email.subject": "Пожалуйста, подтвердите свой адрес электронной почты", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Благодарим за регистрацию на форуме %1!", + "welcome.text2": "Чтобы активировать учётную запись, необходимо подтвердить ваш адрес электронной почты.", + "welcome.text3": "Администратор подтвердил вашу регистрацию. Теперь вы можете использовать вашу учётную запись.", + "welcome.cta": "Нажмите здесь, чтобы подтвердить e-mail", + "invitation.text1": "%1 приглашает вас на форум %2", + "invitation.text2": "Срок действия вашего приглашения истечёт через %1 дней.", + "invitation.cta": "Нажмите здесь, чтобы создать учётную запись.", + "reset.text1": "Мы получили запрос на сброс вашего пароля. Если вы его на самом деле не отправляли, то просто не обращайте внимания на это письмо.", + "reset.text2": "Чтобы сбросить пароль, перейдите по этой ссылке:", + "reset.cta": "Нажмите здесь, чтобы сбросить пароль", + "reset.notify.subject": "Пароль был успешно изменён", + "reset.notify.text1": "Мы уведомляем вас о том, что %1 ваш пароль был успешно изменён.", + "reset.notify.text2": "Если вы не совершали этого действия, пожалуйста, незамедлительно свяжитесь с администратором сайта.", + "digest.latest_topics": "Последние темы на форуме %1", + "digest.top-topics": "Лучшие темы на форуме %1", + "digest.popular-topics": "Популярные темы из %1", + "digest.cta": "Нажмите здесь, чтобы перейти на форум %1", + "digest.unsub.info": "Вы получили эту рассылку согласно вашим настройкам подписки.", + "digest.day": "день", + "digest.week": "неделя", + "digest.month": "месяц", + "digest.subject": "Новостная рассылка за %1", + "digest.title.day": "Ваша ежедневная рассылка", + "digest.title.week": "Ваша еженедельная рассылка", + "digest.title.month": "Ваша ежемесячная рассылка", + "notif.chat.subject": "Новое сообщение от %1", + "notif.chat.cta": "Нажмите, чтобы ответить", + "notif.chat.unsub.info": "Вы получили это уведомление согласно вашим настройкам подписки.", + "notif.post.unsub.info": "Вы получили это уведомление согласно вашим настройкам подписки.", + "notif.post.unsub.one-click": "Вы можете отписаться от подобных уведомлений по этой ссылке:", + "notif.cta": "Перейти на форум", + "notif.cta-new-reply": "Открыть сообщение", + "notif.cta-new-chat": "Открыть чат", + "notif.test.short": "Проверка рассылки уведомлений", + "notif.test.long": "Это проверка уведомлений по электронной почте. Высылайте подмогу!", + "test.text1": "Это проверочное сообщение: отправка электронной почты в NodeBB настроена правильно.", + "unsub.cta": "Нажмите, чтобы изменить настройки", + "unsubscribe": "отписаться", + "unsub.success": "Вы больше не будете получать рассылку от %1", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Вы были заблокированы на форуме %1.", + "banned.text1": "Пользователь %1 был заблокирован на форуме %2.", + "banned.text2": "Блокировка продлится до %1.", + "banned.text3": "Причина вашей блокировки:", + "closing": "Спасибо!" +} \ No newline at end of file diff --git a/public/language/ru/error.json b/public/language/ru/error.json new file mode 100644 index 0000000000..8367b758b0 --- /dev/null +++ b/public/language/ru/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Неверные данные", + "invalid-json": "Некорректный JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Вы не вошли на сайт.", + "account-locked": "Учётная запись временно заблокирована", + "search-requires-login": "Поиск доступен только для зарегистрированных участников. Пожалуйста, войдите или зарегистрируйтесь.", + "goback": "Нажмите \"назад\", чтобы вернуться на предыдущую страницу", + "invalid-cid": "Неправильный ID категории", + "invalid-tid": "Неправильный ID темы", + "invalid-pid": "Неправильный ID сообщения", + "invalid-uid": "Неправильный ID пользователя", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "Должна быть указана действительная дата.", + "invalid-username": "Неправильное имя пользователя", + "invalid-email": "Неправильный адрес электронной почты", + "invalid-fullname": "Некорректное полное имя", + "invalid-location": "Некорректное местонахождение", + "invalid-birthday": "Некорректная дата рождения", + "invalid-title": "Некорректный заголовок", + "invalid-user-data": "Некорректные пользовательские данные", + "invalid-password": "Неправильный пароль", + "invalid-login-credentials": "Неправильный логин или пароль", + "invalid-username-or-password": "Пожалуйста, укажите имя пользователя и пароль", + "invalid-search-term": "Некорректный поисковый запрос", + "invalid-url": "Некорректный URL", + "invalid-event": "Недействительное событие: %1", + "local-login-disabled": "Локальная система входа отключена для не-привилегированных учетных записей.", + "csrf-invalid": "Нам не удалось вас найти из-за просроченной сессии. Попробуйте ещё раз.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Неправильно указан номер страницы. Значение должно быть в диапазоне от %1 до %2", + "username-taken": "Это имя пользователя уже занято", + "email-taken": "Пользователь с таким адресом электронной почты уже зарегистрирован", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Электронная почта уже была приглашена", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "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 may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "По техническим причинам мы не можем подтвердить ваш адрес электронной почты. Приносим вам наши извинения, пожалуйста, попробуйте позже.", + "confirm-email-already-sent": "Сообщение для подтверждения регистрации уже выслано на ваш адрес электронной почты. Повторная отправка возможна через %1 мин.", + "sendmail-not-found": "Не можем найти sendmail, убедитесь что он установлен и управляется NodeBB.", + "digest-not-enabled": "У этого участника не включены дайджесты, или система по умолчанию не настроена на отправку дайджестов", + "username-too-short": "Слишком короткое имя пользователя", + "username-too-long": "Имя пользователя слишком длинное", + "password-too-long": "Пароль слишком длинный", + "reset-rate-limited": "Слишком много запросов на восстановление пароля (установлена блокировка)", + "reset-same-password": "Пожалуйста, используйте пароль, отличный от вашего текущего", + "user-banned": "Пользователь заблокирован", + "user-banned-reason": "Учетная запись заблокирована (Причина: %1)", + "user-banned-reason-until": "Извините, эта учётная запись заблокирована до %1 (Причина: %2)", + "user-too-new": "Вы сможете написать своё первое сообщение через %1 сек.", + "blacklisted-ip": "Извините, ваш IP адрес был заблокирован этим сообществом. Если вы считаете, что это ошибка, пожалуйста, свяжитесь с администратором.", + "ban-expiry-missing": "Пожалуйста, укажите дату окончания этой блокировки", + "no-category": "Такой категории не существует", + "no-topic": "Такой темы не существует", + "no-post": "Такого сообщения не существует", + "no-group": "Такой группы не существует", + "no-user": "Такого пользователя не существует", + "no-teaser": "Такого тизера не существует", + "no-flag": "Flag does not exist", + "no-privileges": "У вас недостаточно прав для этого действия.", + "category-disabled": "Категория отключена", + "topic-locked": "Тема закрыта", + "post-edit-duration-expired": "Сообщения можно редактировать только в течение %1 с после публикации", + "post-edit-duration-expired-minutes": "Сообщения можно редактировать только в течение %1 мин после публикации.", + "post-edit-duration-expired-minutes-seconds": "Сообщения можно редактировать только в течение %1 мин %2 с после публикации.", + "post-edit-duration-expired-hours": "Сообщения можно редактировать в течение %1 ч после публикации.", + "post-edit-duration-expired-hours-minutes": "Сообщения можно редактировать в течение %1 ч %2 мин после публикации.", + "post-edit-duration-expired-days": "Сообщения можно редактировать в течение %1 дн. после публикации.", + "post-edit-duration-expired-days-hours": "Сообщения можно редактировать в течение %1 дн. и %2 ч после публикации.", + "post-delete-duration-expired": "Сообщение можно удалить только в течение %1 с после публикации.", + "post-delete-duration-expired-minutes": "Сообщение можно удалить только в течение %1 мин после публикации.", + "post-delete-duration-expired-minutes-seconds": "Сообщение можно удалить только в течение %1 мин %2 с после публикации.", + "post-delete-duration-expired-hours": "Сообщение можно удалить в течение %1 ч после публикации.", + "post-delete-duration-expired-hours-minutes": "Сообщение можно удалить в течение %1 ч %2 мин после публикации.", + "post-delete-duration-expired-days": "Сообщение можно удалить в течение %1 дн. после публикации.", + "post-delete-duration-expired-days-hours": "Сообщение можно удалить в течение %1 дн. %2 ч после публикации.", + "cant-delete-topic-has-reply": "Нельзя удалить тему после того, как в ней появились ответы", + "cant-delete-topic-has-replies": "Нельзя удалить свою тему после того, как в ней появились ответы (%1 шт.)", + "content-too-short": "Слишком короткое сообщение. Пожалуйста, напишите подробнее (минимум %1 символов).", + "content-too-long": "Слишком длинное сообщение. Пожалуйста, сократите ваше сообщение до %1 символов.", + "title-too-short": "Слишком короткий заголовок. Пожалуйста, напишите подробнее (минимум %1 символов).", + "title-too-long": "Слишком длинный заголовок. Пожалуйста, сократите заголовок до %1 символов.", + "category-not-selected": "Категория не выбрана", + "too-many-posts": "Для того, чтобы разместить новое сообщение, нужно подождать %1 сек.", + "too-many-posts-newbie": "Для того, чтобы разместить новое сообщение, нужно подождать %1 сек. Это время уменьшится, как только ваша репутация вырастет до %2.", + "already-posting": "You are already posting", + "tag-too-short": "Слишком короткая метка. Минимум %1 символов.", + "tag-too-long": "Слишком длинная метка. Максимум %1 символов.", + "not-enough-tags": "Пожалуйста, добавьте метки в ваше сообщение. У темы должно быть минимум %1 меток.", + "too-many-tags": "Пожалуйста, уберите несколько меток из вашего сообщения. У темы должно быть не более %1 меток.", + "cant-use-system-tag": "Вы не можете использовать эту системную метку.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Пожалуйста, подождите завершения загрузки.", + "file-too-big": "Слишком большой файл. Максимальный размер: %1 Кбайт.", + "guest-upload-disabled": "Загрузка файлов для гостей отключена. Чтобы загрузить файл, пожалуйста, войдите или зарегистрируйтесь на сайте.", + "cors-error": "Невозможно загрузить изображение из-за некорректного CORS", + "upload-ratelimit-reached": "Вы загрузили слишком много файлов за один раз. Пожалуйста, повторите попытку позже.", + "scheduling-to-past": "Пожалуйста, выберите дату в будущем", + "invalid-schedule-date": "Пожалуйста, выберите нужную дату и время", + "cant-pin-scheduled": "Запланированные темы нельзя закрепить или открепить.", + "cant-merge-scheduled": "Запланированные темы не могут быть объединены.", + "cant-move-posts-to-scheduled": "Невозможно переместить сообщения в запланированную тему.", + "cant-move-from-scheduled-to-existing": "Невозможно переместить сообщения из запланированной темы в существующую тему.", + "already-bookmarked": "Вы уже добавили это сообщение в закладки", + "already-unbookmarked": "Вы уже удалили это сообщение из закладок", + "cant-ban-other-admins": "Вы не можете заблокировать других администраторов!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Вы единственный администратор. Чтобы отказаться от своих полномочий, пожалуйста, назначьте администратором другого участника.", + "account-deletion-disabled": "Удаление аккаунта отключено", + "cant-delete-admin": "Чтобы удалить эту учётную запись, сначала надо снять с неё полномочия администратора.", + "already-deleting": "В процессе удаления", + "invalid-image": "Некорректное изображение", + "invalid-image-type": "Этот формат изображения не поддерживается. Загрузите изображение в одном из следующих форматов: %1", + "invalid-image-extension": "Недопустимое расширение файла", + "invalid-file-type": "Этот формат файла не поддерживается. Загрузите файл в одном из следующих форматов: %1", + "invalid-image-dimensions": "Размеры изображения слишком велики", + "group-name-too-short": "Название группы слишком короткое, пожалуйста, выберите название подлиннее", + "group-name-too-long": "Название группы слишком длинное, пожалуйста, сократите его", + "group-already-exists": "Такая группа уже существует, пожалуйста, выберите другое название", + "group-name-change-not-allowed": "Название группы изменить нельзя", + "group-already-member": "Участник уже находится в этой группе", + "group-not-member": "В этой группе нет участников", + "group-needs-owner": "У группы должен быть как минимум один владелец", + "group-already-invited": "Этот участник уже был приглашён в группу", + "group-already-requested": "Запрос на вступление в группу уже отправлен", + "group-join-disabled": "Сейчас вы не можете присоединиться к этой группе", + "group-leave-disabled": "Сейчас вы не можете покинуть эту группу", + "post-already-deleted": "Это сообщение уже удалено", + "post-already-restored": "Это сообщение уже восстановлено", + "topic-already-deleted": "Тема уже удалена", + "topic-already-restored": "Тема уже восстановлена", + "cant-purge-main-post": "Вы не можете стереть первое сообщение в теме. Пожалуйста, удалите саму тему.", + "topic-thumbnails-are-disabled": "Иконки тем отключены.", + "invalid-file": "Некорректный файл", + "uploads-are-disabled": "Загрузка отключена", + "signature-too-long": "Ваша подпись не может быть длиннее %1 символов.", + "about-me-too-long": "Пожалуйста, постарайтесь уложиться в поле \"О себе\" в %1 символов.", + "cant-chat-with-yourself": "Вы не можете создать чат с самим собой!", + "chat-restricted": "Пользователь ограничил приём сообщений. Чтобы написать ему личное сообщение, необходимо, чтобы он был подписан на вас.", + "chat-disabled": "Чат выключен", + "too-many-messages": "Вы отправили слишком много сообщений, подождите немного.", + "invalid-chat-message": "Некорректное сообщение чата", + "chat-message-too-long": "Сообщения чата не могут быть длиннее %1 символов", + "cant-edit-chat-message": "У вас нет прав доступа, чтобы отредактировать это сообщение", + "cant-delete-chat-message": "У вас нет прав доступа, чтобы удалить это сообщение", + "chat-edit-duration-expired": "Вам разрешено редактировать сообщения чата за %1 секунд после публикации", + "chat-delete-duration-expired": "Вам разрешено удалять сообщения чата за %1 секунду после публикации", + "chat-deleted-already": "Это сообщение чата уже удалено.", + "chat-restored-already": "Это сообщение чата уже было восстановлено.", + "chat-room-does-not-exist": "Комната чата не существует.", + "already-voting-for-this-post": "Вы уже проголосовали за это сообщение.", + "reputation-system-disabled": "Система репутации отключена.", + "downvoting-disabled": "Понижение рейтинга отключено", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "Вы уже пожаловались на это сообщение", + "user-already-flagged": "Вы уже пожаловались на этого пользователя", + "post-flagged-too-many-times": "На это сообщение уже пожаловались другие пользователи", + "user-flagged-too-many-times": "На этого пользователя уже пожаловались другие пользователи", + "cant-flag-privileged": "Вам не разрешено оставлять жалобы на профили или контент привилегированных пользователей (Модераторов/Глобальных модераторов/Администраторов)", + "self-vote": "Вы не можете голосовать за свои собственные сообщения", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "Вы можете проголосовать против только %1 раз за день", + "too-many-downvotes-today-user": "Вы можете проголосовать против участника только %1 раз за день.", + "reload-failed": "NodeBB обнаружил проблему при перезагрузке: \"%1\". NodeBB продолжит работать с существующими ресурсами клиента, но вы должны отменить то, что сделали перед перезагрузкой.", + "registration-error": "Ошибка при регистрации", + "parse-error": "Похоже, что-то пошло не так в процессе обработки ответа сервера.", + "wrong-login-type-email": "Пожалуйста, для входа используйте адрес своей электронной почты.", + "wrong-login-type-username": "Пожалуйста, для входа используйте имя пользователя.", + "sso-registration-disabled": "Регистрация отключена для %1 учетных записей. пожалуйста, сначала зарегистрируйтесь с адресом электронной почты", + "sso-multiple-association": "Вы не можете связать несколько учетных записей из этой службы с учетной записью на данном форуме. Пожалуйста, отмените существующую учетную запись и повторите попытку.", + "invite-maximum-met": "Вы пригласили %1 людей из %2 возможных.", + "no-session-found": "Сессия входа не найдена!", + "not-in-room": "Пользователь отсутствует в этой комнате", + "cant-kick-self": "Удалить себя из группы невозможно.", + "no-users-selected": "Выберите одного или нескольких пользователей", + "invalid-home-page-route": "Неверная ссылка на домашнюю страницу", + "invalid-session": "Недействительная сессия", + "invalid-session-text": "Похоже, что ваша сессия входа больше не активна. Пожалуйста, обновите эту страницу.", + "session-mismatch": "Несоответствие сессии", + "session-mismatch-text": "Похоже, что ваша сессия входа больше не совпадает с сервером. Пожалуйста, обновите эту страницу.", + "no-topics-selected": "Темы не выбраны!", + "cant-move-to-same-topic": "Невозможно переместить сообщение в эту же тему!", + "cant-move-topic-to-same-category": "Невозможно переместить тему в эту же категорию!", + "cannot-block-self": "Вы не можете заблокировать себя!", + "cannot-block-privileged": "Вы не можете заблокировать администраторов или глобальных модераторов", + "cannot-block-guest": "Гости не могут блокировать пользователей", + "already-blocked": "Этот пользователь уже заблокирован", + "already-unblocked": "Этот пользователь уже разблокирован", + "no-connection": "Похоже, есть проблема с вашим подключением к Интернету", + "socket-reconnect-failed": "В настоящее время невозможно связаться с сервером. Нажмите здесь, чтобы повторить попытку, или сделайте это позднее", + "plugin-not-whitelisted": "Не удалось установить плагин – только плагины, внесенные в белый список диспетчером пакетов NodeBB, могут быть установлены через ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Событие темы \"%1\" нераспознанно", + "cant-set-child-as-parent": "Невозможно установить дочернюю категорию в качестве родительской", + "cant-set-self-as-parent": "Нельзя установить категорию в качестве родительской для самой себя", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/ru/flags.json b/public/language/ru/flags.json new file mode 100644 index 0000000000..e0eef3de53 --- /dev/null +++ b/public/language/ru/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Состояние", + "reports": "Жалобы", + "first-reported": "Первая жалоба", + "no-flags": "Ура! Жалоб нет.", + "assignee": "Исполнитель", + "update": "Обновить", + "updated": "Обновлено", + "resolved": "Решено", + "target-purged": "Сообщение, на которое поступила жалоба, было удалено и больше не доступно.", + + "graph-label": "Жалоб в день", + "quick-filters": "Быстрые фильтры", + "filter-active": "К списку жалоб применяется один или несколько фильтров", + "filter-reset": "Убрать фильтры", + "filters": "Опции фильтра", + "filter-reporterId": "UID сообщившего", + "filter-targetUid": "UID нарушителя", + "filter-type": "Тип жалобы", + "filter-type-all": "Весь контент", + "filter-type-post": "Сообщение", + "filter-type-user": "Пользователь", + "filter-state": "Состояние", + "filter-assignee": "UID исполнителя", + "filter-cid": "Категория", + "filter-quick-mine": "Назначено мне", + "filter-cid-all": "Все категории", + "apply-filters": "Применить фильтры", + "more-filters": "Больше фильтров", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Быстрые действия", + "flagged-user": "Отмеченный пользователь", + "view-profile": "Просмотреть профиль", + "start-new-chat": "Начать новый чат", + "go-to-target": "Показать предмет жалобы", + "assign-to-me": "Назначить мне", + "delete-post": "Удалить сообщение", + "purge-post": "Стереть удалённое сообщение", + "restore-post": "Восстановить сообщение", + "delete": "Delete Flag", + + "user-view": "Открыть профиль", + "user-edit": "Изменить профиль", + + "notes": "Примечания к жалобе", + "add-note": "Добавить примечание", + "no-notes": "Нет примечаний.", + "delete-note-confirm": "Вы уверены, что хотите удалить это примечание к жалобе?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Примечание добавлено", + "note-deleted": "Примечание удалено", + "flag-deleted": "Flag Deleted", + + "history": "История жалоб участника", + "no-history": "Нет истории жалобы.", + + "state-all": "Все состояния", + "state-open": "Новая/Открытая", + "state-wip": "В процессе", + "state-resolved": "Решена", + "state-rejected": "Отклонена", + "no-assignee": "Не назначена", + + "sort": "Сортировано по", + "sort-newest": "Сначала свежие", + "sort-oldest": "Сначала старые", + "sort-reports": "Большинство жалоб", + "sort-all": "Все виды жалоб", + "sort-posts-only": "Только сообщения", + "sort-downvotes": "Большинство голосов против", + "sort-upvotes": "Большинство голосов за", + "sort-replies": "Большинство ответов", + + "modal-title": "Содержание жалобы", + "modal-body": "Укажите причину для жалобы на %1 %2. Вы можете использовать одну из подходящих стандартных причин.", + "modal-reason-spam": "Спам", + "modal-reason-offensive": "Оскорбительное содержимое", + "modal-reason-other": "Другое (укажите ниже)", + "modal-reason-custom": "Причина жалобы на содержимое...", + "modal-submit": "Отправить отчёт", + "modal-submit-success": "Содержимое было помечено для модераторов.", + + "bulk-actions": "Жалоба %1 обновлена", + "bulk-resolve": "Решить жалобы", + "bulk-success": "Жалоба %1 обновлена", + "flagged-timeago-readable": "Получена жалоба (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/ru/global.json b/public/language/ru/global.json new file mode 100644 index 0000000000..7cce07e5fe --- /dev/null +++ b/public/language/ru/global.json @@ -0,0 +1,126 @@ +{ + "home": "Главная", + "search": "Поиск", + "buttons.close": "Закрыть", + "403.title": "Доступ запрещен", + "403.message": "Вы пытаетесь перейти на страницу, к которой у вас нет доступа.", + "403.login": "Возможно вам следует войти под своей учётной записью?", + "404.title": "Страница не найдена", + "404.message": "Вы пытаетесь перейти на страницу, которой не существует. Начните с главной страницы.", + "500.title": "Внутренняя ошибка.", + "500.message": "Упс! Похоже, что-то пошло не так!", + "400.title": "Неверный запрос.", + "400.message": "Похоже, эта ссылка имеет неправильный формат. Пожалуйста, проверьте её и повторите попытку или вернитесь на главную страницу.", + "register": "Зарегистрироваться", + "login": "Войти", + "please_log_in": "Пожалуйста, войдите под своей учётной записью", + "logout": "Выйти", + "posting_restriction_info": "Сообщения могут оставлять только зарегистрированные участники. Нажмите сюда, чтобы войти на сайт", + "welcome_back": "С возвращением!", + "you_have_successfully_logged_in": "Вы успешно вошли на форум", + "save_changes": "Сохранить изменения", + "save": "Сохранить", + "close": "Закрыть", + "pagination": "Разбивка на страницы", + "pagination.out_of": "%1 из %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Админка", + "header.categories": "Категории", + "header.recent": "Последние", + "header.unread": "Непрочитанные", + "header.tags": "Метки", + "header.popular": "Популярные", + "header.top": "Топ", + "header.users": "Пользователи", + "header.groups": "Группы", + "header.chats": "Чаты", + "header.notifications": "Уведомления", + "header.search": "Поиск", + "header.profile": "Профиль", + "header.navigation": "Навигация", + "notifications.loading": "Загружаем уведомления", + "chats.loading": "Загружаем чаты", + "motd.welcome": "Добро пожаловать в NodeBB, платформу будущего для общения.", + "previouspage": "Предыдущая страница", + "nextpage": "Следующая страница", + "alert.success": "Успешно", + "alert.error": "Ошибка", + "alert.banned": "Заблокирован", + "alert.banned.message": "Вас только что заблокировали, теперь ваш доступ ограничен.", + "alert.unbanned": "Разблокирован", + "alert.unbanned.message": "Вы разблокированы.", + "alert.unfollow": "Вы больше не подписаны на %1!", + "alert.follow": "Вы подписались на %1!", + "users": "Пользователи", + "topics": "Темы", + "posts": "Сообщения", + "x-posts": "%1 сообщений", + "best": "Лучшие сообщения", + "controversial": "Controversial", + "votes": "Голоса", + "x-votes": "%1 голосов", + "voters": "Проголосовавшие", + "upvoters": "Кому понравилось", + "upvoted": "Понравилось", + "downvoters": "Кому не понравилось", + "downvoted": "Не понравилось", + "views": "Просмотры", + "posters": "Posters", + "reputation": "Репутация", + "lastpost": "Последнее сообщение", + "firstpost": "Первое сообщение", + "read_more": "Читать далее", + "more": "Подробнее", + "none": "None", + "posted_ago_by_guest": "создано %1 гостем", + "posted_ago_by": "сообщений %1 от %2", + "posted_ago": "написал %1", + "posted_in": "написал в %1", + "posted_in_by": "опубликовано в %1 %2", + "posted_in_ago": "написал в %1 %2", + "posted_in_ago_by": "%3 написал в %1 %2", + "user_posted_ago": "%1 написал %2", + "guest_posted_ago": "Гость написал %1", + "last_edited_by": "отредактировано %1", + "norecentposts": "Нет новых сообщений", + "norecenttopics": "Нет новых тем", + "recentposts": "Последние сообщения", + "recentips": "Последние IP-адреса, с которых был осуществлен вход", + "moderator_tools": "Инструменты модератора", + "online": "В сети", + "away": "Не активен", + "dnd": "Не беспокоить", + "invisible": "Невидимка", + "offline": "Не в сети", + "email": "Электронная почта", + "language": "Язык", + "guest": "Гость", + "guests": "Гостей", + "former_user": "Бывший пользователь", + "system-user": "Система", + "unknown-user": "Неизвестный пользователь", + "updated.title": "Форум обновлён", + "updated.message": "Форум был обновлён до последней версии. Нажмите здесь, чтобы обновить страницу.", + "privacy": "Безопасность", + "follow": "Подписаться", + "unfollow": "Отписаться", + "delete_all": "Удалить всё", + "map": "Карта", + "sessions": "Сессии входа", + "ip_address": "IP адрес", + "enter_page_number": "Введите номер страницы", + "upload_file": "Загрузить файл", + "upload": "Загрузить", + "uploads": "Загрузки", + "allowed-file-types": "Разрешённые форматы файлов: %1", + "unsaved-changes": "У вас есть несохранённые изменения. Вы уверены, что хотите уйти?", + "reconnecting-message": "Похоже, подключение к %1 было разорвано, подождите, пока мы пытаемся восстановить соединение.", + "play": "Воспроизвести", + "cookies.message": "Этот сайт использует cookies для более удобного взаимодействия.", + "cookies.accept": "Понятно!", + "cookies.learn_more": "Подробнее", + "edited": "Отредактированный", + "disabled": "Отключено", + "select": "Выбрать", + "user-search-prompt": "Введите что-нибудь здесь, чтобы найти пользователей..." +} \ No newline at end of file diff --git a/public/language/ru/groups.json b/public/language/ru/groups.json new file mode 100644 index 0000000000..6ae94aa1f2 --- /dev/null +++ b/public/language/ru/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Группы", + "view_group": "Просмотр группы", + "owner": "Администратор группы", + "new_group": "Создать группу", + "no_groups_found": "Нет групп для отображения", + "pending.accept": "Принять", + "pending.reject": "Отклонить", + "pending.accept_all": "Принять всё", + "pending.reject_all": "Отклонить всё", + "pending.none": "На данный момент нет участников, ожидающих утверждения", + "invited.none": "На данный момент нет приглашённых участников", + "invited.uninvite": "Аннулировать приглашение", + "invited.search": "Найти пользователя для приглашения в эту группу", + "invited.notification_title": "Вы были приглашены в группу %1", + "request.notification_title": "Запрос на участие в группе от пользователя %1", + "request.notification_text": "Пользователь %1 хочет присоединиться к группе %2", + "cover-save": "Сохранить", + "cover-saving": "Сохраняем", + "details.title": "Информация о группе", + "details.members": "Список участников", + "details.pending": "Заявки в группу", + "details.invited": "Приглашенные участники", + "details.has_no_posts": "Участники этой группы ещё ничего не написали.", + "details.latest_posts": "Последние сообщения", + "details.private": "Закрытая", + "details.disableJoinRequests": "Отключить запросы на приглашение", + "details.disableLeave": "Запретить участникам покидать группу", + "details.grant": "Выдать/забрать привилегии администратора", + "details.kick": "Исключить", + "details.kick_confirm": "Вы уверены, что хотите удалить этого участника из группы?", + "details.add-member": "Добавить участника", + "details.owner_options": "Управление группой", + "details.group_name": "Название группы", + "details.member_count": "Количество участников", + "details.creation_date": "Дата создания", + "details.description": "Описание", + "details.member-post-cids": "ID категорий для отображения сообщений из", + "details.badge_preview": "Предпросмотр значка", + "details.change_icon": "Сменить иконку", + "details.change_label_colour": "Изменить цвет ярлыка", + "details.change_text_colour": "Изменить цвет текста", + "details.badge_text": "Текст на значке", + "details.userTitleEnabled": "Показывать значок", + "details.private_help": "Если включено, заявку на вступление в группу должен будет подтвердить её владелец", + "details.hidden": "Скрытая", + "details.hidden_help": "Если включено, группа будет скрыта в списках, а участников необходимо будет приглашать вручную", + "details.delete_group": "Удалить группу", + "details.private_system_help": "Закрытые группы отключены на уровне системы, эта опция ничего не даст", + "event.updated": "Настройки группы обновлены", + "event.deleted": "Группа \"%1\" удалена", + "membership.accept-invitation": "Принять приглашение", + "membership.accept.notification_title": "Вы присоединились к группе %1", + "membership.invitation-pending": "Заявка на рассмотрении", + "membership.join-group": "Вступить", + "membership.leave-group": "Покинуть", + "membership.leave.notification_title": "Участник %1 покинул группу %2", + "membership.reject": "Отклонить", + "new-group.group_name": "Название группы:", + "upload-group-cover": "Загрузить обложку группы", + "bulk-invite-instructions": "Введите через запятую имена пользователей, которых хотите пригласить в эту группу", + "bulk-invite": "Массовое приглашение", + "remove_group_cover_confirm": "Вы уверены, что хотите удалить изображение обложки?" +} \ No newline at end of file diff --git a/public/language/ru/ip-blacklist.json b/public/language/ru/ip-blacklist.json new file mode 100644 index 0000000000..faa604a98e --- /dev/null +++ b/public/language/ru/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Настройка черного списка IP", + "description": "Иногда блокировки учётной записи не достаточно. В этом случае ограничение доступа к форуму по IP-адресу или списку IP-адресов — лучший способ защиты. Здесь вы можете добавить нежелательные IP-адреса или целые блоки IP-адресов в формате CIDR в чёрный список, и им будет запрещено входить в систему или регистрировать новую учётную запись.", + "active-rules": "Активные правила", + "validate": "Проверить чёрный список", + "apply": "Применить чёрный список", + "hints": "Подсказки по синтаксису", + "hint-1": "Указывайте по одному IP-адресу на строку. Вы можете добавлять блокировки для подсетей в формате CIDR (например, 192.168.100.0/22)", + "hint-2": "Вы можете добавить комментарий, поставив символ # в начале строки.", + + "validate.x-valid": "%1 из %2 правил некорректны.", + "validate.x-invalid": "Следующие правила %1 некорректны:", + + "alerts.applied-success": "Чёрный список применён", + + "analytics.blacklist-hourly": "График 1 – количество блокировок в час", + "analytics.blacklist-daily": "График 2 – количество блокировок в день", + "ip-banned": "IP заблокирован" +} \ No newline at end of file diff --git a/public/language/ru/language.json b/public/language/ru/language.json new file mode 100644 index 0000000000..247b03efdf --- /dev/null +++ b/public/language/ru/language.json @@ -0,0 +1,5 @@ +{ + "name": "Русский", + "code": "ru", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/ru/login.json b/public/language/ru/login.json new file mode 100644 index 0000000000..e3890407f0 --- /dev/null +++ b/public/language/ru/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Имя пользователя / Email", + "username": "Имя пользователя", + "remember_me": "Запомнить меня", + "forgot_password": "Забыли пароль?", + "alternative_logins": "Войти через", + "failed_login_attempt": "Неправильно указано имя пользователя или электронная почта", + "login_successful": "Вы успешно вошли!", + "dont_have_account": "Нет учётной записи?", + "logged-out-due-to-inactivity": "Вы вышли из панели управления администратора из-за бездействия", + "caps-lock-enabled": "Caps Lock включен" +} \ No newline at end of file diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json new file mode 100644 index 0000000000..53fd5ed2de --- /dev/null +++ b/public/language/ru/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Чат с", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "Вы просматриваете старые сообщения, щелкните здесь, чтобы перейти к последнему сообщению.", + "chat.send": "Отправить", + "chat.no_active": "У вас нет активных чатов.", + "chat.user_typing": "%1 пишет...", + "chat.user_has_messaged_you": "Пользователь %1 отправил вам сообщение.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Пожалуйста, выберите собеседника для просмотра истории сообщений", + "chat.no-users-in-room": "В этой комнате пусто", + "chat.recent-chats": "Последние переписки", + "chat.contacts": "Контакты", + "chat.message-history": "История сообщений", + "chat.message-deleted": "Сообщение удалено", + "chat.options": "Опции чата", + "chat.pop-out": "Покинуть диалог", + "chat.minimize": "Свернуть", + "chat.maximize": "Развернуть", + "chat.seven_days": "7 дней", + "chat.thirty_days": "30 дней", + "chat.three_months": "3 месяца", + "chat.delete_message_confirm": "Вы уверены, что хотите удалить это сообщение?", + "chat.retrieving-users": "Получение списка пользователей...", + "chat.manage-room": "Управлять комнатой чата", + "chat.add-user-help": "Поиск пользователей здесь. Когда выбрали пользователя, он будет добавлен в чат. Новый пользователь не сможет видеть сообщения чата, написанные до его добавления в беседу. Только владельцы комнат () могут удалить пользователей из чатов.", + "chat.confirm-chat-with-dnd-user": "Этот пользователь установил статус \"Не беспокоить\". Вы всё еще хотите написать ему?", + "chat.rename-room": "Переименовать комнату", + "chat.rename-placeholder": "Введите название комнаты здесь", + "chat.rename-help": "Название комнаты, установленное здесь, будет доступно для просмотра всеми участниками комнаты.", + "chat.leave": "Покинуть чат", + "chat.leave-prompt": "Вы действительно хотите покинуть чат?", + "chat.leave-help": "Оставив этот чат, вы удалите себя из будущей переписки в этом чате. Если вы будете повторно добавлены в будущем, вы не увидите истории чата до вашего повторного присоединения.", + "chat.in-room": "В этой комнате", + "chat.kick": "Исключить", + "chat.show-ip": "Показать IP", + "chat.owner": "Владелец комнаты", + "chat.system.user-join": "%1 присоединился к беседе", + "chat.system.user-leave": "%1 покинул беседу", + "chat.system.room-rename": "%2 переименовал беседу: %1", + "composer.compose": "Редактор сообщений", + "composer.show_preview": "Показать предпросмотр сообщения", + "composer.hide_preview": "Скрыть предпросмотр", + "composer.user_said_in": "Пользователь %1 написал в %2:", + "composer.user_said": "Пользователь %1 написал:", + "composer.discard": "Вы уверены, что передумали писать это сообщение?", + "composer.submit_and_lock": "Отправить и закрыть", + "composer.toggle_dropdown": "Показать выпадающий список", + "composer.uploading": "Загрузка %1", + "composer.formatting.bold": "Жирный", + "composer.formatting.italic": "Курсив", + "composer.formatting.list": "Список", + "composer.formatting.strikethrough": "Зачеркнуть", + "composer.formatting.code": "Код", + "composer.formatting.link": "Ссылка", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Загрузить изображение", + "composer.upload-file": "Загрузить файл", + "composer.zen_mode": "Полноэкранный режим", + "composer.select_category": "Выберите категорию", + "composer.textarea.placeholder": "Введите содержание вашего сообщения здесь, перетащите изображения", + "composer.schedule-for": "Установить дату публикации", + "composer.schedule-date": "Дата", + "composer.schedule-time": "Время", + "composer.cancel-scheduling": "Отменить отложенную публикацию", + "composer.set-schedule-date": "Установить дату", + "bootbox.ok": "ОК", + "bootbox.cancel": "Отмена", + "bootbox.confirm": "Подтвердить", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Позиционирование обложки", + "cover.dragging_message": "Перетащите обложку на желаемое место и нажмите \"Сохранить\"", + "cover.saved": "Обложка и её расположение сохранены", + "thumbs.modal.title": "Управление иконкой темы", + "thumbs.modal.no-thumbs": "Иконка не найдена.", + "thumbs.modal.resize-note": "Примечание. Этот форум настроен на уменьшение размеров иконок тем до максимальной ширины в %1p", + "thumbs.modal.add": "Добавить иконку", + "thumbs.modal.remove": "Убрать иконку", + "thumbs.modal.confirm-remove": "Вы уверены, что хотите удалить эту иконку?" +} \ No newline at end of file diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json new file mode 100644 index 0000000000..9331b0defb --- /dev/null +++ b/public/language/ru/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Уведомления", + "no_notifs": "Для вас нет новых уведомлений", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Назад к %1", + "outgoing_link": "Внешняя ссылка", + "outgoing_link_message": "Вы сейчас читаете: %1", + "continue_to": "Перейти на %1", + "return_to": "Вернуться к %1", + "new_notification": "У вас новое уведомление", + "you_have_unread_notifications": "У вас есть непрочитанные уведомления.", + "all": "Все", + "topics": "Темы", + "replies": "Ответы", + "chat": "Чаты", + "group-chat": "Group Chats", + "follows": "Подписки", + "upvote": "Голоса", + "new-flags": "Новые жалобы", + "my-flags": "Назначенные мне жалобы", + "bans": "Блокировки", + "new_message_from": "Новое сообщение от %1", + "upvoted_your_post_in": "Пользователь %1 проголосовал за ваше сообщение в %2.", + "upvoted_your_post_in_dual": "Пользователи %1 и %2 проголосовали за ваше сообщение в %3.", + "upvoted_your_post_in_multiple": "%1 и %2 других пользователя проголосовали за ваше сообщение в %3", + "moved_your_post": "Модератор %1 переместил ваше сообщение в %2", + "moved_your_topic": "Модератор %1 переместил тему %2", + "user_flagged_post_in": "Пользователь %1 пожаловался на сообщение в %2", + "user_flagged_post_in_dual": "Пользователи %1 и %2 пожаловались на сообщение в %3", + "user_flagged_post_in_multiple": "%1 и %2 других пользователя пожаловались на сообщение в %3", + "user_flagged_user": "Пользователь %1 пожаловался на профиль пользователя (%2)", + "user_flagged_user_dual": "Пользователи %1 и %2 пожаловались на профиль пользователя (%3)", + "user_flagged_user_multiple": "%1 и %2 других пользователя пожаловались на профиль пользователя (%3)", + "user_posted_to": "Пользователь %1 ответил на сообщение в %2", + "user_posted_to_dual": "Пользователи %1 и %2 ответили на сообщение в %3", + "user_posted_to_multiple": "%1 и %2 других пользователя ответили на сообщение в %3", + "user_posted_topic": "Пользователь %1 создал новую тему: %2", + "user_edited_post": " %1 отредактировал сообщение в %2 ", + "user_started_following_you": "Пользователь %1 подписался на вас.", + "user_started_following_you_dual": "Пользователи %1 и %2 подписались на вас.", + "user_started_following_you_multiple": "%1 и %2 других пользователя подписались на вас.", + "new_register": "Посетитель %1 отправил запрос на регистрацию.", + "new_register_multiple": "В очереди %1 заявок на регистрацию.", + "flag_assigned_to_you": "Жалоба %1 была назначена вам", + "post_awaiting_review": "Сообщение ожидает проверки", + "profile-exported": "Профиль %1 экспортирован, нажмите для загрузки", + "posts-exported": "Посты %1 экспортированы, нажмите для загрузки", + "uploads-exported": "Вложения %1 экспортированы, нажмите для загрузки", + "users-csv-exported": "CSV пользователей экспортирован, нажмите, чтобы загрузить", + "post-queue-accepted": "Ваше сообщение из очереди было принято. Нажмите здесь, чтобы увидеть ваше сообщение.", + "post-queue-rejected": "Ваше сообщение из очереди было отклонено.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Электронная почта подтверждена", + "email-confirmed-message": "Спасибо за подтверждение адреса электронной почты. Ваша учётная запись активирована.", + "email-confirm-error-message": "Ошибка проверки адреса электронной почты. Возможно, код подтверждения введён неправильно или у него истёк срок действия.", + "email-confirm-sent": "Письмо с проверочным кодом отправлено на ваш электронный адрес", + "none": "Ничего", + "notification_only": "Только уведомление", + "email_only": "Только письмо", + "notification_and_email": "Уведомление и письмо", + "notificationType_upvote": "Когда кто-то проголосовал за ваше сообщение", + "notificationType_new-topic": "Когда кто-то, на кого вы подписаны, создаёт новую тему", + "notificationType_new-reply": "Когда в теме, за которой вы следите, появляется новое сообщение", + "notificationType_post-edit": "Когда сообщение было отредактировано в теме, на которую вы подписаны", + "notificationType_follow": "Когда кто-то подписался на вас", + "notificationType_new-chat": "Когда вы получаете сообщение в чат", + "notificationType_new-group-chat": "Когда вы получаете сообщение группового чата", + "notificationType_group-invite": "Когда вы получаете приглашение в группу", + "notificationType_group-leave": "Когда пользователь покидает вашу группу", + "notificationType_group-request-membership": "Когда кто-то хочет присоединиться к группе, которой вы управляете", + "notificationType_new-register": "Когда в очереди на регистрацию появляется новый пользователь", + "notificationType_post-queue": "Когда в очереди на проверку появляется новое сообщение", + "notificationType_new-post-flag": "Когда поступает жалоба на сообщение", + "notificationType_new-user-flag": "Когда поступает жалоба на пользователя" +} \ No newline at end of file diff --git a/public/language/ru/pages.json b/public/language/ru/pages.json new file mode 100644 index 0000000000..fb4df23a3a --- /dev/null +++ b/public/language/ru/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Главная", + "unread": "Непрочитанные темы", + "popular-day": "Популярные сегодня темы", + "popular-week": "Популярные темы этой недели", + "popular-month": "Популярные темы этого месяца", + "popular-alltime": "Популярные темы за всё время", + "recent": "Последние темы", + "top-day": "Лучшие темы за сегодня", + "top-week": "Лучшие темы за неделю", + "top-month": "Лучшие темы за месяц", + "top-alltime": "Лучшие темы", + "moderator-tools": "Инструменты модератора", + "flagged-content": "Список жалоб", + "ip-blacklist": "Чёрный список IP", + "post-queue": "Очередь на публикацию", + "users/online": "В сети", + "users/latest": "Новые пользователи", + "users/sort-posts": "Пользователи по кол-ву сообщений", + "users/sort-reputation": "Пользователи по уровню репутации", + "users/banned": "Заблокированные пользователи", + "users/most-flags": "Пользователи, на которых больше всего жалуются", + "users/search": "Поиск пользователей", + "notifications": "Уведомления", + "tags": "Метки", + "tag": "Темы с меткой "%1"", + "register": "Зарегистрироваться", + "registration-complete": "Регистрация завершена", + "login": "Войти", + "reset": "Сбросить пароль", + "categories": "Категории", + "groups": "Группы", + "group": "Группа %1", + "chats": "Чаты", + "chat": "Чат с %1", + "flags": "Жалобы", + "flag-details": "Подробности жалобы %1", + "account/edit": "Редактирование \"%1\"", + "account/edit/password": "Сменить пароль \"%1\"", + "account/edit/username": "Изменить имя пользователя \"%1\"", + "account/edit/email": "Изменить электронную почту \"%1\"", + "account/info": "Информация об учётной записи", + "account/following": "Подписки %1", + "account/followers": "Подписчики %1", + "account/posts": "Сообщения %1", + "account/latest-posts": "Недавние сообщения %1", + "account/topics": "Темы, созданные %1", + "account/groups": "Группы %1", + "account/watched_categories": "Категории, которые отслеживает %1", + "account/bookmarks": "Закладки %1", + "account/settings": "Настройки учётной записи", + "account/watched": "Темы, которые %1 отслеживает", + "account/ignored": "Темы, которые %1 игнорирует", + "account/upvoted": "Понравилось пользователю %1", + "account/downvoted": "Не понравилось пользователю %1", + "account/best": "Лучшие сообщения %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Пользователи, заблокированные %1", + "account/uploads": "Загрузки %1", + "account/sessions": "Сессии", + "confirm": "Электронная почта подтверждена", + "maintenance.text": "%1 в настоящее время на обслуживании. Пожалуйста, возвращайтесь позже.", + "maintenance.messageIntro": "Кроме того, администратор оставил это сообщение:", + "throttled.text": "%1 в настоящее время недоступен из-за высокой нагрузки. Пожалуйста, приходите в другой раз." +} \ No newline at end of file diff --git a/public/language/ru/post-queue.json b/public/language/ru/post-queue.json new file mode 100644 index 0000000000..48a5b5a918 --- /dev/null +++ b/public/language/ru/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Очередь на публикацию", + "description": "В очереди на публикацию нет сообщений.
Чтобы включить эту функцию, перейдите в Настройки → Сообщения → Очередь на публикацию и включите Очередь на публикацию.", + "user": "Пользователь", + "category": "Категория", + "title": "Название", + "content": "Содержимое", + "posted": "Время", + "reply-to": "Ответ \"%1\"", + "content-editable": "Нажмите на содержимое для редактирования", + "category-editable": "Нажмите на категорию для редактирования", + "title-editable": "Нажмите на название для редактирования", + "reply": "Ответить", + "topic": "Тема", + "accept": "Подтвердить", + "reject": "Отклонить", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/ru/recent.json b/public/language/ru/recent.json new file mode 100644 index 0000000000..b852766c31 --- /dev/null +++ b/public/language/ru/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Последние", + "day": "За день", + "week": "За неделю", + "month": "За месяц", + "year": "За год", + "alltime": "За всё время", + "no_recent_topics": "Нет свежих тем.", + "no_popular_topics": "Популярные темы отсутствуют.", + "there-is-a-new-topic": "Опубликована новая тема.", + "there-is-a-new-topic-and-a-new-post": "Опубликована новая тема и новое сообщение.", + "there-is-a-new-topic-and-new-posts": "Опубликована новая тема и %1 новых сообщений.", + "there-are-new-topics": "Опубликовано %1 новых тем.", + "there-are-new-topics-and-a-new-post": "Опубликовано %1 новых тем и новое сообщение.", + "there-are-new-topics-and-new-posts": "Опубликовано %1 новых тем и %2 новых сообщений.", + "there-is-a-new-post": "Опубликовано новое сообщение.", + "there-are-new-posts": "Опубликовано %1 новых сообщений.", + "click-here-to-reload": "Нажмите здесь, чтобы обновить список." +} \ No newline at end of file diff --git a/public/language/ru/register.json b/public/language/ru/register.json new file mode 100644 index 0000000000..3a9cca89d6 --- /dev/null +++ b/public/language/ru/register.json @@ -0,0 +1,32 @@ +{ + "register": "Регистрация", + "cancel_registration": "Отменить регистрацию", + "help.email": "Ваш адрес электронной почты будет скрыт от других пользователей.", + "help.username_restrictions": "Другие пользователи смогут упоминать вас в своих сообщениях таким образом: @никнейм. Длина имени пользователя: %1-%2 символов.", + "help.minimum_password_length": "Ваш пароль должен содержать как минимум %1 символов.", + "email_address": "Электронная почта", + "email_address_placeholder": "Введите свой адрес электронной почты", + "username": "Имя пользователя", + "username_placeholder": "Введите имя пользователя", + "password": "Пароль", + "password_placeholder": "Введите пароль", + "confirm_password": "Подтвердите пароль", + "confirm_password_placeholder": "Подтвердите пароль", + "register_now_button": "Зарегистрироваться", + "alternative_registration": "Регистрация через социальную сеть", + "terms_of_use": "Условия использования сайта", + "agree_to_terms_of_use": "Я соглашаюсь с условиями", + "terms_of_use_error": "Для регистрации на нашем сайте необходимо согласиться с условиями", + "registration-added-to-queue": "Ваша регистрация была добавлена в очередь на утверждение. Вы получите уведомление по электронной почте, когда она будет одобрена администратором.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Я соглашаюсь на сбор и обработку моей личной информации на этом веб-сайте.", + "gdpr_agree_email": "Я соглашаюсь получать дайджесты и уведомления с этого сайта на свой адрес электронной почты.", + "gdpr_consent_denied": "Вы должны дать согласие на сбор, обработку вашей информации и отправку вам сообщений по электронной почте.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/ru/reset_password.json b/public/language/ru/reset_password.json new file mode 100644 index 0000000000..23e47875de --- /dev/null +++ b/public/language/ru/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Восстановить пароль", + "update_password": "Изменить пароль", + "password_changed.title": "Пароль изменён", + "password_changed.message": "

Пароль успешно сброшен. Пожалуйста, войдите снова.", + "wrong_reset_code.title": "Неверный код восстановления", + "wrong_reset_code.message": "Неправильный код восстановления пароля. Попробуйте ещё раз, или запросите новый код восстановления.", + "new_password": "Новый пароль", + "repeat_password": "Подтвердите пароль", + "changing_password": "Изменение пароля", + "enter_email": "Пожалуйста введите ваш адрес электронной почты, чтобы получить письмо с инструкцией по восстановлению пароля.", + "enter_email_address": "Введите адрес электронной почты", + "password_reset_sent": "Если указанный адрес соответствует существующей учетной записи, было отправлено письмо для сброса пароля. Обратите внимание, что в минуту будет отправлено только одно письмо.", + "invalid_email": "Адрес электронной почты указан неверно. Пожалуйста, исправьте", + "password_too_short": "Введённый пароль слишком короткий, это небезопасно. Пожалуйста, придумайте более длинный пароль.", + "passwords_do_not_match": "Введённые пароли не совпадают. Пожалуйста, укажите одинаковые пароли.", + "password_expired": "Для повышения безопасности необходимо периодически менять пароль. Сейчас как раз настало время смены пароля. Пожалуйста, укажите новый пароль." +} \ No newline at end of file diff --git a/public/language/ru/search.json b/public/language/ru/search.json new file mode 100644 index 0000000000..34ea66147d --- /dev/null +++ b/public/language/ru/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "Найдено %1 результатов по запросу: \"%2\". Время поиска: %3 с.", + "no-matches": "Совпадений не найдено", + "advanced-search": "Расширенный поиск", + "in": "Где искать", + "titles": "Заголовки", + "titles-posts": "Заголовки и сообщения", + "match-words": "Совпадающие слова", + "all": "Все", + "any": "Любые", + "posted-by": "Автор", + "in-categories": "В категориях", + "search-child-categories": "Искать во вложенных категориях", + "has-tags": "Метки", + "reply-count": "Кол-во ответов", + "at-least": "Как минимум", + "at-most": "Максимум", + "relevance": "Релевантности", + "post-time": "Дата публикации", + "votes": "Кол-ву голосов", + "newer-than": "Не позже чем", + "older-than": "Не раньше чем", + "any-date": "Любая дата", + "yesterday": "Вчера", + "one-week": "Одна неделя", + "two-weeks": "Две недели", + "one-month": "Один месяц", + "three-months": "Три месяца", + "six-months": "Шесть месяцев", + "one-year": "Год", + "sort-by": "Сортировать по", + "last-reply-time": "Времени последнего ответа", + "topic-title": "Названию темы", + "topic-votes": "Кол-ву голосов за тему", + "number-of-replies": "Количеству ответов", + "number-of-views": "Количеству просмотров", + "topic-start-date": "Времени создания темы", + "username": "Имени пользователя", + "category": "Категориям", + "descending": "По возрастанию", + "ascending": "По убыванию", + "save-preferences": "Сохранить настройки", + "clear-preferences": "Очистить настройки", + "search-preferences-saved": "Настройки поиска сохранены", + "search-preferences-cleared": "Настройки поиска очищены", + "show-results-as": "Показать результаты как:", + "see-more-results": "Показать больше результатов (%1)", + "search-in-category": "Искать в \"%1\"" +} \ No newline at end of file diff --git a/public/language/ru/success.json b/public/language/ru/success.json new file mode 100644 index 0000000000..1e3763f2dc --- /dev/null +++ b/public/language/ru/success.json @@ -0,0 +1,7 @@ +{ + "success": "Готово", + "topic-post": "Вы успешно отправили сообщение.", + "post-queued": "Ваше сообщение поставлено в очередь на утверждение. Вы получите уведомление, когда оно будет принято или отклонено.", + "authentication-successful": "Авторизация выполнена успешно", + "settings-saved": "Настройки сохранены!" +} \ No newline at end of file diff --git a/public/language/ru/tags.json b/public/language/ru/tags.json new file mode 100644 index 0000000000..8377ddf504 --- /dev/null +++ b/public/language/ru/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Тем с такой меткой сейчас нет.", + "tags": "Метки", + "enter_tags_here": "Добавьте метки здесь, от %1 до %2 символов каждая.", + "enter_tags_here_short": "Введите метки...", + "no_tags": "Меток пока нет.", + "select_tags": "Выберите метки" +} \ No newline at end of file diff --git a/public/language/ru/top.json b/public/language/ru/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/ru/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json new file mode 100644 index 0000000000..24e41df714 --- /dev/null +++ b/public/language/ru/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Тема", + "title": "Заголовок", + "no_topics_found": "Темы не найдены!", + "no_posts_found": "Сообщения не найдены!", + "post_is_deleted": "Это сообщение удалено!", + "topic_is_deleted": "Эта тема удалена!", + "profile": "Профиль", + "posted_by": "Опубликовано %1", + "posted_by_guest": "Опубликовано гостем", + "chat": "Чат", + "notify_me": "Получать уведомления о новых сообщениях в этой теме", + "quote": "Цитировать", + "reply": "Ответить", + "replies_to_this_post": "%1 ответов", + "one_reply_to_this_post": "1 ответ", + "last_reply_time": "Последний ответ", + "reply-as-topic": "Ответить, создав новую тему", + "guest-login-reply": "Авторизуйтесь, чтобы ответить", + "login-to-view": "Авторизуйтесь, чтобы просмотреть", + "edit": "Изменить", + "delete": "Удалить", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Стереть", + "restore": "Восстановить", + "move": "Перенести", + "change-owner": "Сменить автора", + "fork": "Разделить", + "link": "Ссылка", + "share": "Поделиться", + "tools": "Действия", + "locked": "Закрыта", + "pinned": "Прикреплена", + "pinned-with-expiry": "Закреплен до %1", + "scheduled": "Запланировано", + "moved": "Перенесена", + "moved-from": "Перенесено с %1", + "copy-ip": "Копировать IP", + "ban-ip": "Забанить IP", + "view-history": "История правок", + "locked-by": "Заблокировано", + "unlocked-by": "Разблокировано", + "pinned-by": "Закреплено", + "unpinned-by": "Откреплено", + "deleted-by": "Удалено", + "restored-by": "Восстановлено", + "moved-from-by": "Moved from %1 by", + "queued-by": "Сообщение поставлено в очередь на утверждение;", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Нажмите здесь, чтобы вернуться к последнему прочитанному сообщению в этой теме.", + "flag-post": "Пожаловаться на это сообщение", + "flag-user": "Пожаловаться на этого пользователя", + "already-flagged": "Жалоба на рассмотрении", + "view-flag-report": "Показать содержание жалобы", + "resolve-flag": "Решенная жалоба", + "merged_message": "Эта тема была объединена с %2", + "deleted_message": "Эта тема была удалена. Только пользователи с правом управления темами могут её видеть.", + "following_topic.message": "Теперь вы будете получать уведомления каждый раз, когда кто-нибудь напишет сообщение в эту тему.", + "not_following_topic.message": "Вы увидите эту тему в списке непрочитанных, но не будете получать уведомлений о новых сообщениях в ней.", + "ignoring_topic.message": "Вы больше не будете видеть эту тему в списке непрочитанных, но если кто-то упомянет вас в ней или проголосует за ваше сообщение, вы получите уведомление.", + "login_to_subscribe": "Пожалуйста, зарегистрируйтесь или авторизуйтесь, чтобы подписаться на эту тему.", + "markAsUnreadForAll.success": "Тема помечена как непрочитанная для всех.", + "mark_unread": "Отметить как непрочитанную", + "mark_unread.success": "Тема помечена как непрочитанная.", + "watch": "Отслеживать", + "unwatch": "Не отслеживать", + "watch.title": "Получать уведомления о новых сообщениях в этой теме", + "unwatch.title": "Перестать отслеживать эту тему", + "share_this_post": "Поделиться сообщением", + "watching": "Отслеживается", + "not-watching": "Не отслеживается", + "ignoring": "Игнорируется", + "watching.description": "Уведомлять о новых сообщениях.
Показывать тему в непрочитанных.", + "not-watching.description": "Не уведомлять о новых сообщениях.
Показывать тему в непрочитанных, если эта категория не игнорируется.", + "ignoring.description": "Не уведомлять о новых сообщениях.
Не показывать эту тему в непрочитанных.", + "thread_tools.title": "Управление темой", + "thread_tools.markAsUnreadForAll": "Пометить непрочитанной для всех", + "thread_tools.pin": "Прикрепить тему", + "thread_tools.unpin": "Открепить тему", + "thread_tools.lock": "Закрыть тему", + "thread_tools.unlock": "Открыть тему", + "thread_tools.move": "Перенести тему", + "thread_tools.move-posts": "Перенести сообщения", + "thread_tools.move_all": "Перенести всё", + "thread_tools.change_owner": "Сменить автора", + "thread_tools.select_category": "Выберите категорию", + "thread_tools.fork": "Разделить тему", + "thread_tools.delete": "Удалить тему", + "thread_tools.delete-posts": "Удалить сообщения", + "thread_tools.delete_confirm": "Вы уверены, что хотите удалить эту тему?", + "thread_tools.restore": "Восстановить тему", + "thread_tools.restore_confirm": "Вы уверены, что хотите восстановить эту тему?", + "thread_tools.purge": "Стереть тему", + "thread_tools.purge_confirm": "Вы уверены, что хотите стереть эту тему?", + "thread_tools.merge_topics": "Объединить темы", + "thread_tools.merge": "Объединить", + "topic_move_success": "Эта тема будет перемещена в \"%1\". Нажмите здесь, чтобы отменить.", + "topic_move_multiple_success": "Эти темы будут перемещены в \"%1\". Нажмите здесь, чтобы отменить.", + "topic_move_all_success": "Все темы будут перемещены в \"%1\". Нажмите здесь, чтобы отменить.", + "topic_move_undone": "Перенос темы отменен", + "topic_move_posts_success": "Сообщения скоро будут перемещены. Нажмите здесь, чтобы отменить.", + "topic_move_posts_undone": "Перемещение сообщений отменено", + "post_delete_confirm": "Вы уверены, что хотите удалить это сообщение?", + "post_restore_confirm": "Вы уверены, что хотите восстановить это сообщение?", + "post_purge_confirm": "Вы уверены, что хотите стереть это сообщение?", + "pin-modal-expiry": "Дата окончания срока", + "pin-modal-help": "При желании вы можете установить дату истечения срока для закрепленных тем здесь. Кроме того, вы можете оставить это поле пустым, чтобы тема оставалась закрепленной до тех пор, пока она не будет откреплена вручную.", + "load_categories": "Загружаем категории", + "confirm_move": "Перенести", + "confirm_fork": "Разделить", + "bookmark": "Добавить в закладки", + "bookmarks": "Закладки", + "bookmarks.has_no_bookmarks": "Вы ещё не добавили в закладки ни одного сообщения.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Загружаем больше сообщений", + "move_topic": "Перенести тему", + "move_topics": "Перенести темы", + "move_post": "Перенести сообщение", + "post_moved": "Сообщение перенесено!", + "fork_topic": "Создать дополнительную ветвь дискуссии", + "enter-new-topic-title": "Введите новое название темы", + "fork_topic_instruction": "Отметьте сообщения, которые вы хотите перенести в отдельную тему", + "fork_no_pids": "Не выбрано ни одного сообщения!", + "no-posts-selected": "Не выбрано ни одного сообщения!", + "x-posts-selected": "Выбрано сообщений: %1", + "x-posts-will-be-moved-to-y": "%1 сообщений перемещено в \"%2\"", + "fork_pid_count": "Выбрано сообщений: %1", + "fork_success": "Готово! Нажмите здесь, чтобы перейти к новой теме.", + "delete_posts_instruction": "Отметьте сообщения, которые вы хотите удалить или стереть", + "merge_topics_instruction": "Нажмите на темы, которые вы хотите объединить (или найдите их ниже)", + "merge-topic-list-title": "Список объединяемых тем", + "merge-options": "Параметры объединения", + "merge-select-main-topic": "Выберите основную тему", + "merge-new-title-for-topic": "Новое название темы", + "topic-id": "ID темы", + "move_posts_instruction": "Щелкните сообщения, которые вы хотите переместить, затем введите ID темы или перейдите к целевой теме.", + "change_owner_instruction": "Нажмите на сообщения, которые вы хотите присвоить другому пользователю", + "composer.title_placeholder": "Введите название темы...", + "composer.handle_placeholder": "Введите ваше имя здесь", + "composer.discard": "Отменить", + "composer.submit": "Отправить", + "composer.additional-options": "Additional Options", + "composer.schedule": "Запланировать", + "composer.replying_to": "Ответ %1", + "composer.new_topic": "Создать тему", + "composer.editing": "Редактирование \"%1\"", + "composer.uploading": "загрузка...", + "composer.thumb_url_label": "Вставьте ссылку на картинку с иконкой темы.", + "composer.thumb_title": "Добавить иконку к этой теме", + "composer.thumb_url_placeholder": "http://example.com/pic.jpg", + "composer.thumb_file_label": "Загрузить новое изображение", + "composer.thumb_remove": "Очистить поля", + "composer.drag_and_drop_images": "Перетащите изображения сюда", + "more_users_and_guests": "ещё %1 пользователей и %2 гостей", + "more_users": "ещё %1 пользователей", + "more_guests": "ещё %1 гостей", + "users_and_others": "%1 пользователей и %2 других", + "sort_by": "Сортировка", + "oldest_to_newest": "Сначала старые", + "newest_to_oldest": "Сначала новые", + "most_votes": "По количеству голосов", + "most_posts": "По количеству сообщений", + "most_views": "Most Views", + "stale.title": "Создать новую тему вместо этой?", + "stale.warning": "Тема, в которую вы собираетесь написать, очень старая. Может, стоит создать новую, а про эту просто напомнить к случаю?", + "stale.create": "Создать новую тему", + "stale.reply_anyway": "Всё равно ответить здесь", + "link_back": "Ответ: [%1](%2)", + "diffs.title": "История правок сообщения", + "diffs.description": "У этого сообщения есть %1 версий. Нажмите на любую из них, чтобы увидеть, каким оно было раньше.", + "diffs.no-revisions-description": "У этого сообщения есть %1версий", + "diffs.current-revision": "текущая версия", + "diffs.original-revision": "исходная версия", + "diffs.restore": "Восстановить эту версию", + "diffs.restore-description": "После восстановления новая редакция будет добавлена в историю редактирования этого сообщения.", + "diffs.post-restored": "Сообщение успешно восстановлено до более ранней версии", + "diffs.delete": "Удалить эту версию", + "diffs.deleted": "Версия удалена", + "timeago_later": "через %1", + "timeago_earlier": "на %1 раньше", + "first-post": "Первое сообщение", + "last-post": "Последнее сообщение", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/ru/unread.json b/public/language/ru/unread.json new file mode 100644 index 0000000000..1450d47aff --- /dev/null +++ b/public/language/ru/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Непрочитанные темы", + "no_unread_topics": "Нет непрочитанных тем.", + "load_more": "Загрузить еще", + "mark_as_read": "Пометить как прочитанное", + "selected": "Выбрано", + "all": "Все", + "all_categories": "Все категории", + "topics_marked_as_read.success": "Все темы помечены как прочитанные!", + "all-topics": "Все темы", + "new-topics": "Новые темы", + "watched-topics": "Отслеживаемые темы", + "unreplied-topics": "Неотвеченные темы", + "multiple-categories-selected": "Выбрано несколько категорий" +} \ No newline at end of file diff --git a/public/language/ru/uploads.json b/public/language/ru/uploads.json new file mode 100644 index 0000000000..1943ccb3c5 --- /dev/null +++ b/public/language/ru/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Загрузка файла...", + "select-file-to-upload": "Укажите файл для загрузки!", + "upload-success": "Файл успешно загружен!", + "maximum-file-size": "Максимум %1 Кб", + "no-uploads-found": "Загрузки не найдены", + "public-uploads-info": "Загрузки общедоступны, все посетители могут их видеть.", + "private-uploads-info": "Загрузки скрыты, только зарегистрированные пользователи могут их видеть." +} \ No newline at end of file diff --git a/public/language/ru/user.json b/public/language/ru/user.json new file mode 100644 index 0000000000..c7655bfef3 --- /dev/null +++ b/public/language/ru/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Заблокирован", + "muted": "Muted", + "offline": "Не в сети", + "deleted": "Удалён", + "username": "Имя пользователя", + "joindate": "Дата регистрации", + "postcount": "Сообщений", + "email": "Электронная почта", + "confirm_email": "Подтвердить электронную почту", + "account_info": "Информация об учётной записи", + "admin_actions_label": "Административные действия", + "ban_account": "Заблокировать учётную запись", + "ban_account_confirm": "Вы действительно хотите заблокировать этого пользователя?", + "unban_account": "Разблокировать учётную запись", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Удалить учётную запись", + "delete_account_as_admin": "Удалить учётную запись", + "delete_content": "Удалить контент учетной записи", + "delete_all": "Удалить учётную запись и контент", + "delete_account_confirm": "Вы уверены, что хотите анонимизировать свои сообщения и удалить свою учетную запись?
Это действие необратимо, и вы не сможете восстановить какие-либо свои данные.

Введите свой пароль, чтобы подтвердить, что вы хотите уничтожить эту учетную запись.", + "delete_this_account_confirm": "Вы уверены, что хотите удалить эту учетную запись, оставив её содержимое?
Это действие необратимо, посты будут анонимными и вы не сможете восстановить связи постов с удаленной учетной записью

", + "delete_account_content_confirm": "Вы уверены, что хотите удалить содержимое этой учетной записи (сообщения / темы / загрузки)?
Это действие необратимо, и вы не сможете восстановить какие-либо данные

", + "delete_all_confirm": "Вы уверены, что хотите удалить эту учетную запись и всё её содержимое (сообщения / темы / загрузки)?
Это действие необратимо и вы не сможете восстановить какие-либо данные

", + "account-deleted": "Учётная запись удалена", + "account-content-deleted": "Контент учетной записи удален", + "fullname": "Полное имя", + "website": "Сайт", + "location": "Местонахождение", + "age": "Возраст", + "joined": "Регистрация", + "lastonline": "Последнее посещение", + "profile": "Профиль", + "profile_views": "Просмотры профиля", + "reputation": "Репутация", + "bookmarks": "Закладки", + "watched_categories": "Отслеживаемые категории", + "change_all": "Изменить для всех", + "watched": "Отслеживаемые темы", + "ignored": "Игнорируемые темы", + "default-category-watch-state": "Стандартная настройка отслеживания категорий", + "followers": "Подписчики", + "following": "Подписки", + "blocks": "Чёрный список", + "block_toggle": "Блок./Разблок", + "block_user": "Добавить в Чёрный Список", + "unblock_user": "Убрать из Чёрного Списка", + "aboutme": "Обо мне", + "signature": "Подпись", + "birthday": "День рождения", + "chat": "Чат", + "chat_with": "Продолжить чат с %1", + "new_chat_with": "Начать новый чат с %1", + "flag-profile": "Пожаловаться на профиль", + "follow": "Подписаться", + "unfollow": "Отписаться", + "more": "Больше", + "profile_update_success": "Профиль обновлён!", + "change_picture": "Изменить аватар", + "change_username": "Изменить имя пользователя", + "change_email": "Изменить электронную почту", + "email_same_as_password": "Пожалуйста, введите пароль, чтобы продолжить – вы снова указали свой новый адрес электронной почты", + "edit": "Редактировать", + "edit-profile": "Редактировать профиль", + "default_picture": "Стандартная иконка", + "uploaded_picture": "Загруженный аватар", + "upload_new_picture": "Загрузить новый аватар", + "upload_new_picture_from_url": "Загрузить изображение по ссылке", + "current_password": "Текущий пароль", + "change_password": "Изменить пароль", + "change_password_error": "Неправильный пароль!", + "change_password_error_wrong_current": "Текущий пароль указан неверно!", + "change_password_error_match": "Пароли должны совпадать!", + "change_password_error_privileges": "Вы не можете изменить пароль.", + "change_password_success": "Ваш пароль изменён!", + "confirm_password": "Подтвердите пароль", + "password": "Пароль", + "username_taken_workaround": "Это имя пользователя уже занято, поэтому пришлось его немного изменить. Теперь вы %1", + "password_same_as_username": "Ваш пароль совпадает с вашим именем пользователя. Пожалуйста, укажите другой пароль.", + "password_same_as_email": "Ваш пароль совпадает с вашей электронной почтой. Пожалуйста, укажите другой пароль.", + "weak_password": "Слабый пароль.", + "upload_picture": "Загрузить изображение", + "upload_a_picture": "Загрузить изображение", + "remove_uploaded_picture": "Удалить аватар", + "upload_cover_picture": "Загрузить обложку профиля", + "remove_cover_picture_confirm": "Вы уверены, что хотите удалить обложку профиля?", + "crop_picture": "Обрезать картинку", + "upload_cropped_picture": "Вырезать и загрузить", + "avatar-background-colour": "Цвет фона аватара", + "settings": "Настройки", + "show_email": "Показывать мою электронную почту", + "show_fullname": "Показывать моё полное имя", + "restrict_chats": "Разрешить чат только с теми, на кого я подписан", + "digest_label": "Подписка на дайджест", + "digest_description": "Подписаться на рассылку уведомлений о событиях и новых темах на форуме с указанной периодичностью", + "digest_off": "Отключена", + "digest_daily": "Ежедневная", + "digest_weekly": "Еженедельная", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Ежемесячная", + "has_no_follower": "На этого пользователя никто не подписан :(", + "follows_no_one": "Этот пользователь ни на кого не подписан :(", + "has_no_posts": "Этот пользователь ещё ничего не написал.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Этот пользователь ещё не создал ни одной темы.", + "has_no_watched_topics": "Этот пользователь не отслеживает ни одной темы.", + "has_no_ignored_topics": "Этот пользователь не игнорирует ни одну тему.", + "has_no_upvoted_posts": "Этот пользователь ещё ни одному сообщению не поднимал рейтинг.", + "has_no_downvoted_posts": "Этот пользователь ещё ни одному сообщению не понижал рейтинг.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Вы никого не заблокировали.", + "email_hidden": "Электронная почта скрыта", + "hidden": "скрыто", + "paginate_description": "Разбивать темы и сообщения на страницы, а не выводить бесконечным списком", + "topics_per_page": "Тем на странице", + "posts_per_page": "Сообщений на странице", + "max_items_per_page": "максимум %1", + "acp_language": "Язык панели администратора", + "notifications": "Уведомления", + "upvote-notif-freq": "Частота уведомлений об изменении рейтинга сообщения", + "upvote-notif-freq.all": "Все уведомления", + "upvote-notif-freq.first": "Только первое", + "upvote-notif-freq.everyTen": "Каждые десять голосов", + "upvote-notif-freq.threshold": "На 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "На 10, 100, 1000...", + "upvote-notif-freq.disabled": "Выключено", + "browsing": "Настройки просмотра", + "open_links_in_new_tab": "Открывать внешние ссылки в новом окне", + "enable_topic_searching": "Включить поиск по сообщениям внутри тем", + "topic_search_help": "Когда эта опция включена, вместо стандартного поиска по странице ваш браузер будет использовать соответствующую функцию форума, которая позволит искать и среди сообщений, которые ещё не загружены.", + "update_url_with_post_index": "Обновлять URL-адрес с индексом публикации при просмотре тем", + "scroll_to_my_post": "Пролистывать страницы к вашим новым сообщениям сразу после их отправки", + "follow_topics_you_reply_to": "Включать отслеживание во всех темах, в которых вы отвечаете", + "follow_topics_you_create": "Включать отслеживание всех тем, которые вы создаёте", + "grouptitle": "Значки групп", + "group-order-help": "Выберите группу и укажите порядок значков с помощью стрелок", + "no-group-title": "Не показывать значок группы", + "select-skin": "Стиль", + "select-homepage": "Настройка главной страницы", + "homepage": "Главная страница", + "homepage_description": "Выберите, на какую страницу вы будете попадать после авторизации и использовать как главную, или оставьте стандартную настройку.", + "custom_route": "Ваш маршрут для главной страницы", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Сервисы единого входа", + "sso.associated": "Связан с", + "sso.not-associated": "Нажмите здесь, чтобы связать учётную запись с", + "sso.dissociate": "Открепить", + "sso.dissociate-confirm-title": "Подтверждение открепления", + "sso.dissociate-confirm": "Вы уверены, что хотите открепить свою учётную запись от %1?", + "info.latest-flags": "Последние жалобы", + "info.no-flags": "Жалоб не найдено", + "info.ban-history": "История блокировок", + "info.no-ban-history": "Этот пользователь никогда не был заблокирован", + "info.banned-until": "Заблокирован до %1", + "info.banned-expiry": "Истечение", + "info.banned-permanently": "Заблокирован навсегда", + "info.banned-reason-label": "Причина", + "info.banned-no-reason": "Без объяснения причин.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "История изменения имён", + "info.email-history": "История изменения электронной почты", + "info.moderation-note": "Примечание модератора", + "info.moderation-note.success": "Примечание модератора сохранено", + "info.moderation-note.add": "Добавить примечание", + "sessions.description": "Эта страница позволяет видеть все активные сессии на форуме и отключать их при необходимости. Вы можете закрыть свою сессию, выйдя из учетной записи.", + "consent.title": "Ваши права и согласие", + "consent.lead": "Этот форум собирает и обрабатывает вашу личную информацию.", + "consent.intro": "Мы используем эту информацию исключительно для персонализации вашего опыта в этом сообществе, а также для связывания сообщений, которые вы вносите в свою учетную запись пользователя. Во время этапа регистрации вас попросили указать имя пользователя и адрес электронной почты, вы также можете предоставить дополнительную информацию, чтобы заполнить свой профиль пользователя на этом веб-сайте.

Мы сохраняем эту информацию на всё время существования учетной записи пользователя, и вы можете отозвать согласие в любое время, удалив свою учетную запись. В любое время вы можете запросить копию своего вклада на этот веб-сайт, используя раздел \"ваши права и согласие\"

Если у вас есть какие-либо вопросы или опасения, мы рекомендуем вам обратиться к администрации этого форума.", + "consent.email_intro": "Иногда мы можем отправлять электронные письма на ваш зарегистрированный адрес электронной почты, чтобы предоставлять обновления и / или уведомлять вас о новой деятельности, которая вам подходит. Вы можете настроить частоту дайджестов сообщества (в том числе отключить его напрямую), а также выбрать, какие типы уведомлений получать по электронной почте, на странице настроек пользователя.", + "consent.digest_frequency": "Если в пользовательских настройках ничего не изменено, это сообщество отправляет электронные дайджесты каждые %1.", + "consent.digest_off": "Если в пользовательских настройках ничего не изменено, это сообщество не будет отправлять электронные дайджесты", + "consent.received": "Вы дали согласие на этот сайт для сбора и обработки вашей информации. Никаких дополнительных действий не требуется.", + "consent.not_received": "Вы не дали согласия на сбор и обработку данных. В любое время администрация этого веб-сайта может удалить вашу учетную запись, чтобы она соответствовала Общему правилу защиты данных (GDPR).", + "consent.give": "Дать согласие", + "consent.right_of_access": "У вас есть право на доступ к данным", + "consent.right_of_access_description": "Вы имеете право запросить доступ к любым данным, собранным на этом веб-сайте. Чтобы получить копию этих данных, нажмите на кнопку ниже.", + "consent.right_to_rectification": "У вас есть право на исправление данных", + "consent.right_to_rectification_description": "Вы имеете право изменять или обновлять любые неточные данные, предоставленные нам. Вы всегда можете отредактировать ваш профиль или ваши сообщения. Если это не так, обратитесь к администраторам сайта.", + "consent.right_to_erasure": "У вас есть право на удаление данных", + "consent.right_to_erasure_description": "В любое время вы можете отозвать свое согласие на сбор и/или обработку данных, удалив свою учётную запись. Ваш индивидуальный профиль можно удалить, хотя ваши сообщения останутся. Если вы хотите удалить как свою учётную запись, так и контент, пожалуйста, свяжитесь с администрацией сайта.", + "consent.right_to_data_portability": "У вас есть право на перенос данных", + "consent.right_to_data_portability_description": "Вы можете запросить у нас машиночитаемый экспорт любых собранных данных о вас и вашей учетной записи. Вы можете сделать это, нажав соответствующую кнопку ниже.", + "consent.export_profile": "Экспорт профиля (.json)", + "consent.export-profile-success": "Экспорт учетной записи, вы получите уведомление, когда он будет завершен.", + "consent.export_uploads": "Экспорт загруженного контента (.zip)", + "consent.export-uploads-success": "Экспорт загрузок, вы получите уведомление, когда он будет завершен.", + "consent.export_posts": "Экспорт сообщений (.csv)", + "consent.export-posts-success": "Экспорт постов, вы получите уведомление, когда он будет завершен.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/ru/users.json b/public/language/ru/users.json new file mode 100644 index 0000000000..85d3dd0382 --- /dev/null +++ b/public/language/ru/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Новые пользователи", + "top_posters": "Самые активные", + "most_reputation": "Лучшая репутация", + "most_flags": "Больше всего жалоб", + "search": "Поиск", + "enter_username": "Введите имя пользователя для поиска", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Загрузить еще", + "users-found-search-took": "Найдено пользователей: %1! Поиск занял %2 с.", + "filter-by": "Сортировать по", + "online-only": "Только онлайн", + "invite": "Пригласить", + "prompt-email": "Адреса электронной почты:", + "groups-to-join": "Группы, в которые вы вступите приняв приглашение:", + "invitation-email-sent": "Письмо с приглашением для %1 отправлено", + "user_list": "Список пользователей", + "recent_topics": "Последние темы", + "popular_topics": "Популярные темы", + "unread_topics": "Непрочитанные темы", + "categories": "Категории", + "tags": "Метки", + "no-users-found": "Пользователи не найдены!" +} \ No newline at end of file diff --git a/public/language/rw/_DO_NOT_EDIT_FILES_HERE.md b/public/language/rw/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/rw/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/rw/admin/admin.json b/public/language/rw/admin/admin.json new file mode 100644 index 0000000000..39edffb66f --- /dev/null +++ b/public/language/rw/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/rw/admin/advanced/cache.json b/public/language/rw/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/rw/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/rw/admin/advanced/database.json b/public/language/rw/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/rw/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/rw/admin/advanced/errors.json b/public/language/rw/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/rw/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/rw/admin/advanced/events.json b/public/language/rw/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/rw/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/rw/admin/advanced/logs.json b/public/language/rw/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/rw/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/rw/admin/appearance/customise.json b/public/language/rw/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/rw/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/rw/admin/appearance/skins.json b/public/language/rw/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/rw/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/rw/admin/appearance/themes.json b/public/language/rw/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/rw/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/rw/admin/dashboard.json b/public/language/rw/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/rw/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/rw/admin/development/info.json b/public/language/rw/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/rw/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/rw/admin/development/logger.json b/public/language/rw/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/rw/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/rw/admin/extend/plugins.json b/public/language/rw/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/rw/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/rw/admin/extend/rewards.json b/public/language/rw/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/rw/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/rw/admin/extend/widgets.json b/public/language/rw/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/rw/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/admins-mods.json b/public/language/rw/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/rw/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/categories.json b/public/language/rw/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/rw/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/digest.json b/public/language/rw/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/rw/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/rw/admin/manage/groups.json b/public/language/rw/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/rw/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/privileges.json b/public/language/rw/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/rw/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/registration.json b/public/language/rw/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/rw/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/tags.json b/public/language/rw/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/rw/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/uploads.json b/public/language/rw/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/rw/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/rw/admin/manage/users.json b/public/language/rw/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/rw/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/rw/admin/menu.json b/public/language/rw/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/rw/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/advanced.json b/public/language/rw/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/rw/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/api.json b/public/language/rw/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/rw/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/chat.json b/public/language/rw/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/rw/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/cookies.json b/public/language/rw/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/rw/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/email.json b/public/language/rw/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/rw/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/rw/admin/settings/general.json b/public/language/rw/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/rw/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/rw/admin/settings/group.json b/public/language/rw/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/rw/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/guest.json b/public/language/rw/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/rw/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/homepage.json b/public/language/rw/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/rw/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/languages.json b/public/language/rw/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/rw/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/navigation.json b/public/language/rw/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/rw/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/rw/admin/settings/notifications.json b/public/language/rw/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/rw/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/pagination.json b/public/language/rw/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/rw/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/post.json b/public/language/rw/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/rw/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/reputation.json b/public/language/rw/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/rw/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/social.json b/public/language/rw/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/rw/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/sockets.json b/public/language/rw/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/rw/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/sounds.json b/public/language/rw/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/rw/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/tags.json b/public/language/rw/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/rw/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/rw/admin/settings/uploads.json b/public/language/rw/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/rw/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/rw/admin/settings/user.json b/public/language/rw/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/rw/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/rw/admin/settings/web-crawler.json b/public/language/rw/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/rw/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/rw/category.json b/public/language/rw/category.json new file mode 100644 index 0000000000..f275dc7356 --- /dev/null +++ b/public/language/rw/category.json @@ -0,0 +1,23 @@ +{ + "category": "Icyiciro", + "subcategories": "Icyiciro gito", + "new_topic_button": "Ikiganiro Gishya", + "guest-login-post": "Injiramo wandike", + "no_topics": "Nta biganiro byo muri iki cyiciro bihari
Watangije kimwe hano se?", + "browsing": "abari kureba", + "no_replies": "Nta muntu urasubiza", + "no_new_posts": "Nta bishya.", + "watch": "Kurikirana", + "ignore": "Ihorere", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Ibyiciro Bikurikirwa", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/rw/email.json b/public/language/rw/email.json new file mode 100644 index 0000000000..5c1e305db6 --- /dev/null +++ b/public/language/rw/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Ikaze kuri %1", + "invite": "Ubutumire buvuye kuri %1", + "greeting_no_name": "Mwirwe", + "greeting_with_name": "Mwiriwe %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Urakoze kwiyandika nk'ukoresha %1!", + "welcome.text2": "Kugirango tuguhe uburenganzira busesuye bwo gukoresha konte yawe, tugomba kubanza gusuzuma niba email watanze wiyandikisha ari iyawe. ", + "welcome.text3": "Umuyobozi w'urubuga yemeye ubusabe bwawe bwo kwandikwa nk'ukoresha urubuga. Ushobora noneho kwinjiramo ukoresheje izina n'ijambobanga byawe.", + "welcome.cta": "Kanda hano kugirango wemeze ko email watanze ari iyawe", + "invitation.text1": "%1 yagutumiye kuri %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Twabonye ubusabe bwo gutangiza ijambobanga ryawe bundibushya, wenda bitewe n'uko wibagiwe iryo wari ufite. Niba atari ko bimeze, si ngombwa kwita ku bindi byanditse muri iyi email.", + "reset.text2": "Niba ushaka kujya aho uri butangize ijambobanga ryawe, kanda ku murongo ukurikira:", + "reset.cta": "Kanda hano kugirango utangize bundibushya ijambobanga ryawe", + "reset.notify.subject": "Ijambobanga ryahinduwe nta ngorane", + "reset.notify.text1": "Turakumenyesha ko kuri %1, ijambobanga wakoreshaga ryahinduwe nk'uko byari byasabwe.", + "reset.notify.text2": "Niba atari wowe wari wabisabye ku bushake bwawe, bimenyeshe umuyobozi w'urubuga aka kanya. ", + "digest.latest_topics": "Ibiganiro biheruka bya %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Kanda hano kugirango usure %1", + "digest.unsub.info": "Izi ngingo z'ingenzi zakohererejwe kuko waziyandikishijeho", + "digest.day": "umunsi", + "digest.week": "icyumweru", + "digest.month": "ukwezi", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Ubutumwa bwo mu gikari bwaturutse kuri %1", + "notif.chat.cta": "Kanda hano kugirango ukomeze", + "notif.chat.unsub.info": "Iri tangazo rijyanye n'ubutumwa bwo mu gikari waryohererejwe kubera ko wabihisemo mu byo uzajya umenyeshwa", + "notif.post.unsub.info": "Iri tangazo rijyanye n'ibyashyizwe ku rubuga waryohererejwe kubera ko wabihisemo mu byo uzajya umenyeshwa", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Iyi message ni igerageza kugirango harebwe niba emailer ya NodeBB yarateguwe neza", + "unsub.cta": "Kanda hano kugirango uhindure uko bizajya bigenda", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Murakoze!" +} \ No newline at end of file diff --git a/public/language/rw/error.json b/public/language/rw/error.json new file mode 100644 index 0000000000..2153353025 --- /dev/null +++ b/public/language/rw/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Ibyashyizwemo Ntibyemewe", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Biragaragara ko utinjiyemo.", + "account-locked": "Konte yawe yabaye ifunze", + "search-requires-login": "Gushaka ikintu bisaba kuba ufite konte - Injiramo cyangwa wiyandike.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Nimero y'Icyiciro Ntiyemewe", + "invalid-tid": "Nimero y'Ikiganiro Ntiyemewe", + "invalid-pid": "Nimero y'Icyashyizweho Ntiyemewe", + "invalid-uid": "Nimero y'Umuntu Ntiyemewe", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Izina Ntiryemewe", + "invalid-email": "Email Ntiyemewe", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Ibyatanzwe Ntibyemewe!", + "invalid-password": "Ijambobanga Ntiryemewe", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Tanga izina ukoresha n'ijambobanga", + "invalid-search-term": "Icyashatswe nticyemewe", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "Izina ryarafashwe mbere", + "email-taken": "Email yarafashwe mbere", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Ntabwo uremererwa kuganirira mu gikari kuko email yawe itari yemezwa. Kanda hano kugirango wemeze email yawe. ", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Ntabwo email yawe yabashije kwemezwa. Ongera ugerageze mu bundi buryo. ", + "confirm-email-already-sent": "Email yo kwemeza yamaze koherezwa. Tegereza iminota (umunota) %1 mbere yo kohereza indi. ", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Izina ni rigufi cyane", + "username-too-long": "Izina ni rirerire cyane", + "password-too-long": "Password too long", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Umuntu wirukanwe", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Wihangena kuko usabwa gutegereza amasegonda (isegonda) %1 mbere yo gushyiraho ikintu cyawe cya mbere", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Icyiciro kitabaho", + "no-topic": "Ikiganiro kitabaho", + "no-post": "Icyashyizweho kitabaho", + "no-group": "Itsinda ritabaho", + "no-user": "Umuntu utabaho", + "no-teaser": "Inshamake itabaho", + "no-flag": "Flag does not exist", + "no-privileges": "Ntabwo uragira uburenganzira buhagije ngo wemererwe iki gikorwa", + "category-disabled": "Icyiciro cyabujijwe", + "topic-locked": "Ikiganiro Cyafungiranywe", + "post-edit-duration-expired": "Wemerewe gusa kugira icyo uhindura ku byo washyizeho nyuma y'amasegonda (isegonda) %1 nyuma yo kubishyiraho", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Gerageza ushyireho ikintu kirekireho. Icyo ushyiraho kigomba kuba kigizwe nibura n'inyuguti (cyangwa ibimenyetso) zigera kuri %1.", + "content-too-long": "Gerageza ushyireho ibintu bigufiyaho. Icyo ushyiraho kigomba kuba kigizwe n'inyuguti (cyangwa ibimenyetso) zirenga %1. ", + "title-too-short": "Gerageza ushyireho umutwe muremureho. Umutwe ugomba kuba ugizwe n'inyuguti (cyangwa ibimenyetso) zigera kuri %1. ", + "title-too-long": "Gerageza ushyireho umutwe mugufiyaho. Umutwe ugomba kuba ugizwe n'inyuguti (cyangwa ibimenyetso) zitarenga %1. ", + "category-not-selected": "Category not selected.", + "too-many-posts": "Wemerewe kugira icyo ushyiraho rimwe mu masegonda (isegonda) %1. Ba utegerejeho gato kugirango wongere", + "too-many-posts-newbie": "Nk'umuntu mushya, wemerewe gushyiraho ikintu rimwe mu masegonda (isegonda) %1 kugeza igihe ugize amanota agera kuri %2. Ba utegerejeho gato kugirango wongere", + "already-posting": "You are already posting", + "tag-too-short": "Gerageza ukoreshe akamenyetso kagizwe n'inyuguti (cyangwa ibimenyetso) nibura zigera kuri %1", + "tag-too-long": "Gerageza ukoreshe akamenyetso kagizwe n'inyuguti (cyangwa ibimenyetso) zitarenze %1", + "not-enough-tags": "Nta tumenyetso turiho duhagije. Ibiganiro bigomba kugira utumenyetso (akamenyetso) nibura %1", + "too-many-tags": "Hariho utumenyetso twinshi. Ibiganiro ntibyarenza utumenyetso (akamenyetso) %1", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Tegereza gupakira bibanze birangire.", + "file-too-big": "Ubunini bwemewe bushoboka bw'ifayilo ni kB %1. Gerageza upakire ifayilo ntoyaho", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Ntabwo wakwirukana abandi bayobozi!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Ni wowe muyobozi wenyine. Ongeramo undi muntu nk'umuyobozi mbere y'uko wikura ku buyobozi", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Ubwoko bw'ifoto wahisemo ntibwemewe. Hemewe gusa: %1", + "invalid-image-extension": "Impera itemewe igaragaza foruma y'ifoto", + "invalid-file-type": "Ubwoko bw'ifayilo ntibwemewe. Hemewe gusa: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Izina ry'itsinda ni rigufi cyane", + "group-name-too-long": "Group name too long", + "group-already-exists": "Itsinda ryitwa gutya risanzweho", + "group-name-change-not-allowed": "Guhindura izina ry'itsinda ntibyemewe", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "Iri tsinda risaba kugira nibura umuyobozi umwe", + "group-already-invited": "Uyu muntu yari yaramaze gutumirwa", + "group-already-requested": "Ubusabe bwo kuba mu itsinda bwari bwaramaze koherezwa", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Ibi byari byarakuweho", + "post-already-restored": "Ibi byari byaragaruwe", + "topic-already-deleted": "Iki kiganiro cyari cyarakuweho", + "topic-already-restored": "Iki kiganiro cyari cyaragaruwe", + "cant-purge-main-post": "Ntabwo ushobora gusibanganya icyashyizweho kandi ibindi bigishamikiyeho. Ahubwo wakuraho ikiganiro cyose", + "topic-thumbnails-are-disabled": "Ishushondanga ntiyemerewe. ", + "invalid-file": "Ifayilo Ntiyemewe", + "uploads-are-disabled": "Ipakira Ntiryemerewe", + "signature-too-long": "Intero yawe ntabwo yemerewe kurenza inyuguti (cyangwa ibimenyetso) %1. ", + "about-me-too-long": "Inshamake y'Ubuzima yawe ntiyemerewe kurenza inyuguti (cyangwa ibimenyetso) %1.", + "cant-chat-with-yourself": "Ntabwo wakwiganiriza!", + "chat-restricted": "Uyu muntu yemerera kuganirira mu gikari n'abantu bamwe na bamwe. Agomba kuba yarahisemo kugukurikira kugirango ube wabasha kumuganiriza uciye mu gikari. ", + "chat-disabled": "Chat system disabled", + "too-many-messages": "Wohereje ubutumwa bwinshi cyane. Ba utegerejeho gato. ", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Ibijyanye n'itangwa ry'amanota ntibyemerewe. ", + "downvoting-disabled": "Kwambura amanota ntibyemerewe", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB yahuye n'ingorane mu gihe cy'ipakira: \"%1\". NodeBB irakomeza kuzana ibyo yari ifite ku ruhande rw'imbere nubwo ufite kuba wasubira inyuma ugafata ibyo wari wakoze mbere yo gupakira. ", + "registration-error": "Ukwibeshya mu Iyandika", + "parse-error": "Hari ikibazo cyavutse mu gihe twari kugerageza kuzana igisubizo kivuye kuri server", + "wrong-login-type-email": "Koresha email yawe kugirango winjiremo", + "wrong-login-type-username": "Koresha izina ry'umukoresha ryawe kugirango winjiremo", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/rw/flags.json b/public/language/rw/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/rw/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/rw/global.json b/public/language/rw/global.json new file mode 100644 index 0000000000..8416897c52 --- /dev/null +++ b/public/language/rw/global.json @@ -0,0 +1,126 @@ +{ + "home": "Imbere", + "search": "Shaka", + "buttons.close": "Funga", + "403.title": "Ntibyemewe Kuhagera", + "403.message": "Wageze kuri paji udafitiye uburenganzira bwo kureba", + "403.login": "Wenda ahari ukeneye kugerageza kwinjiramo", + "404.title": "Ntacyabonetse", + "404.message": "Biragaragara ko wageze kuri paji itariho ikintu. Subira Imbere.", + "500.title": "Internal Error.", + "500.message": "Ye baba we! Ntibikunze!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Iyandikishe", + "login": "Injiramo", + "please_log_in": "Injiramo", + "logout": "Sohokamo", + "posting_restriction_info": "Gushyiraho ikintu byemewe ku banyamuryango gusa. Niba uri we, kanda hano winjiremo. ", + "welcome_back": "Urakaza Neza Urisanga", + "you_have_successfully_logged_in": "Winjiyemo nta ngorane", + "save_changes": "Bika ibyamaze gukorwa", + "save": "Save", + "close": "Funga", + "pagination": "Umubare wa Paji", + "pagination.out_of": "%1 muri %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Ubuyobozi", + "header.categories": "Ibyiciro", + "header.recent": "Ibiheruka", + "header.unread": "Ibitarasomwa", + "header.tags": "Utumenyetso", + "header.popular": "Ibikunzwe", + "header.top": "Top", + "header.users": "Abantu", + "header.groups": "Amatsinda", + "header.chats": "Ubutumwa", + "header.notifications": "Amatangazo", + "header.search": "Shaka", + "header.profile": "Ishusho", + "header.navigation": "Ukureba", + "notifications.loading": "Amatangazo Araje", + "chats.loading": "Ubutumwa Buraje", + "motd.welcome": "Urakaza neza kuri NodeBB, urubuga rujyanye n'ibihe bizaza", + "previouspage": "Paji Ibanza", + "nextpage": "Paji Ikurikira", + "alert.success": "Byaciyemo", + "alert.error": "Byanze", + "alert.banned": "Birukanwe", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Ntabwo ukimukurikira %1!", + "alert.follow": "Ubu ngubu ukurikira %1!", + "users": "Abantu", + "topics": "Ibiganiro", + "posts": "Ibyashyizweho", + "x-posts": "%1 posts", + "best": "Byiza", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Byakunzwe", + "downvoters": "Downvoters", + "downvoted": "Byagawe", + "views": "Byarebwe", + "posters": "Posters", + "reputation": "Amanota", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "komeza usome", + "more": "Ibindi", + "none": "None", + "posted_ago_by_guest": "%1 bishyizweho na Umushyitsi", + "posted_ago_by": "%1 bishyizweho na %2", + "posted_ago": "%1 biriho", + "posted_in": "byashyizwe muri %1", + "posted_in_by": "byashyizwe muri %1 na %2", + "posted_in_ago": "%2 bishyizwe muri %1", + "posted_in_ago_by": "%2 bishyizwe muri %1 na %3", + "user_posted_ago": "%2 %1 ashyizeho", + "guest_posted_ago": "%1 Umushyitsi ashyizeho", + "last_edited_by": "biheruka guhindurwaho na %1", + "norecentposts": "Nta Biherutseho", + "norecenttopics": "Nta Biganiro Biherutse", + "recentposts": "Ibiherutseho", + "recentips": "Aderesi za IP Ziheruka Gusura", + "moderator_tools": "Moderator Tools", + "online": "Ku Murongo", + "away": "Ahandi", + "dnd": "Nta Kurogoya", + "invisible": "Nta Kugaragara", + "offline": "Nta Murongo", + "email": "Email", + "language": "Ururimi", + "guest": "Umushyitsi", + "guests": "Abashyitsi", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Urubuga Rushyizwe ku Gihe", + "updated.message": "Uru rubuga rumaze kuvugururwa. Kanda hano kugirango niba hari ibyahindutse kuri iyi paji bikugereho. ", + "privacy": "Umuhezo", + "follow": "Kurikira", + "unfollow": "Reka Gukurikira", + "delete_all": "Siba Byose", + "map": "Ikarita", + "sessions": "Ukwinjiramo", + "ip_address": "Aderesi ya IP", + "enter_page_number": "Shyiramo nimero ya paji", + "upload_file": "Pakira ifayilo", + "upload": "Pakira", + "uploads": "Uploads", + "allowed-file-types": "Ubwoko bw'amafayilo bwemewe ni %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/rw/groups.json b/public/language/rw/groups.json new file mode 100644 index 0000000000..518a523d3a --- /dev/null +++ b/public/language/rw/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Amatsinda", + "view_group": "Reba Itsinda", + "owner": "Nyir'Itsinda ", + "new_group": "Tangiza Itsinda Rishya", + "no_groups_found": "Nta matsinda agaragara", + "pending.accept": "Emera", + "pending.reject": "Hakanira", + "pending.accept_all": "Emererera Bose", + "pending.reject_all": "Hakanira Bose", + "pending.none": "Nta banyamuryango bategereje bahari", + "invited.none": "Nta banyamuryango batumiwe bahari", + "invited.uninvite": "Kuraho Ubutumire", + "invited.search": "Shaka umuntu wo gutumira muri iri tsinda", + "invited.notification_title": "Utumiwe kwinjira muri %1", + "request.notification_title": "Ubusabe bwo Kujya mu Itsinda Buturutse %1", + "request.notification_text": "%1 yasabye kuba umunyamuryango w'itsinda rya %2", + "cover-save": "Bika", + "cover-saving": "Kubika", + "details.title": "Ibijyanye n'Itsinda", + "details.members": "Urutonde rw'Abagize Itsinda", + "details.pending": "Abategereje Kwemererwa", + "details.invited": "Abatumiwe", + "details.has_no_posts": "Uyu munyamuryango ntabwo arashyiraho ikintu na kimwe", + "details.latest_posts": "Ibiheruka Gushyirwaho", + "details.private": "Yigenga", + "details.disableJoinRequests": "Guhagarika ubusabe bwo kwinjira", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Tanga/Ambura Ubuyobozi", + "details.kick": "Tera", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Ubuyobozi bw'Itsinda", + "details.group_name": "Izina ry'Itsinda", + "details.member_count": "Umubare w'Abagize Itsinda", + "details.creation_date": "Igihe Ryaremewe", + "details.description": "Ibiriranga", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Ibisobanuro ku Kirango", + "details.change_icon": "Hindura Akarango", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Amagambo y'Ikirango", + "details.userTitleEnabled": "Erekana Ikirango", + "details.private_help": "Nubyemera, kujya mu itsinda runaka bizajya bisaba guca kwa nyir'itsinda", + "details.hidden": "Ahishe", + "details.hidden_help": "Nubyemera, iri tsinda ntabwo rizajya rigaragara ku rutonde rw'andi matsinda kandi abantu bazajya basabwa kuritumirwamo buri wese ku giti cye mbere yo kurijyamo", + "details.delete_group": "Senya Itsinda", + "details.private_system_help": "Amatsinda aheza ntabwo ari kwemerera aha, hano ntabwo byahahindurirwa", + "event.updated": "Amakuru ku itsinda yahinduweho bijyanye n'igihe", + "event.deleted": "Itsinda rya \"%1\" ryakuweho", + "membership.accept-invitation": "Emera Ubutumire", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Ubutumire Buracyategereje", + "membership.join-group": "Injira mu Itsinda", + "membership.leave-group": "Va mu Itsinda", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Hakanira", + "new-group.group_name": "Izina ry'Itsinda:", + "upload-group-cover": "Shyiraho ifoto yo hejuru iranga itsinda", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/rw/ip-blacklist.json b/public/language/rw/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/rw/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/rw/language.json b/public/language/rw/language.json new file mode 100644 index 0000000000..20306ead7b --- /dev/null +++ b/public/language/rw/language.json @@ -0,0 +1,5 @@ +{ + "name": "Kinyarwanda", + "code": "rw", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/rw/login.json b/public/language/rw/login.json new file mode 100644 index 0000000000..b5856a55d4 --- /dev/null +++ b/public/language/rw/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Izina / Email", + "username": "Izina ", + "remember_me": "Wibukwe?", + "forgot_password": "Wibagiwe ijambobanga?", + "alternative_logins": "Ukundi Wakwinjiramo", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "Winjiyemo nta ngorane!", + "dont_have_account": "Nta konte ufite?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/rw/modules.json b/public/language/rw/modules.json new file mode 100644 index 0000000000..a2a3592357 --- /dev/null +++ b/public/language/rw/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Ohereza", + "chat.no_active": "Nta biganiro byo mu gikari ufite. ", + "chat.user_typing": "%1 ari kwandika ...", + "chat.user_has_messaged_you": "%1 yagusigiye ubutumwa.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Hitamo umuntu ushaka kurebera ibyo mwandikiranye", + "chat.no-users-in-room": "Nta muntu uri muri iki gikari", + "chat.recent-chats": "Ubutumwa Buheruka", + "chat.contacts": "Abo Kuvugisha", + "chat.message-history": "Ubutumwa Bwahise", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Fungura Akadirishya k'Igikari", + "chat.minimize": "Minimize", + "chat.maximize": "Marirayo", + "chat.seven_days": "Iminsi 7", + "chat.thirty_days": "Iminsi 30", + "chat.three_months": "Amezi 3", + "chat.delete_message_confirm": "Wiringiye neza ko ushaka gusiba ubu butumwa?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Andika", + "composer.show_preview": "Bona Uko Biza Gusa", + "composer.hide_preview": "Hisha Uko Biza Gusa", + "composer.user_said_in": "%1 yavuze muri %2:", + "composer.user_said": "%1 yavuze:", + "composer.discard": "Wiringiye neza ko ushaka kureka kubishyiraho?", + "composer.submit_and_lock": "Shyiraho kandi Unafungirane", + "composer.toggle_dropdown": "Hindura Icyerekezo", + "composer.uploading": "Ugupakira %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "Sawa", + "bootbox.cancel": "Isubire", + "bootbox.confirm": "Emeza", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Kuringaniza Ifoto yo Hejuru", + "cover.dragging_message": "Kurura ifoto yo hejuru mu cyerekezo ushaka ubundi ubike ibirangijwe", + "cover.saved": "Ibyatunganyijwe ku ifoto yo hejuru byafashe", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/rw/notifications.json b/public/language/rw/notifications.json new file mode 100644 index 0000000000..02e93e4452 --- /dev/null +++ b/public/language/rw/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Amatangazo", + "no_notifs": "Nta matangazo mashya ufite", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Subira kuri %1", + "outgoing_link": "Umurongo Usohoka", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "Komereza kuri %1", + "return_to": "Subira kuri %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Ufite amatangazo utarasoma. ", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": " %1 yakwandikiye", + "upvoted_your_post_in": "%1 yagushimye aguha inota kuri %2 washyizeho.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 yatambikanye ikintu muri %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 yanditse kuri: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 yatangije ikiganiro gishya: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 yatangiye kugukurikira.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 yasabye kwandikwa.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email Yemejwe", + "email-confirmed-message": "Urakoze kugaragaza ko email yawe ikora. Ubu ngubu konte yawe irakora nta kabuza. ", + "email-confirm-error-message": "Havutse ikibazo mu gushaka kumenya niba email yawe ikora. Ushobora kuba wakoresheje kode itari yo cyangwa se yarengeje igihe. ", + "email-confirm-sent": "Hoherejwe email yo kubyemeza.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/rw/pages.json b/public/language/rw/pages.json new file mode 100644 index 0000000000..b64927cb55 --- /dev/null +++ b/public/language/rw/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Imbere", + "unread": "Ibiganiro Bitarasomwa", + "popular-day": "Ibiganiro bikunzwe uyu munsi", + "popular-week": "Ibiganiro bikunzwe iki cyumweru", + "popular-month": "Ibiganiro bikunzwe uku kwezi", + "popular-alltime": "Ibiganiro byakunzwe ibihe byose", + "recent": "Ibiganiro Biheruka", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Abariho", + "users/latest": "Abashya", + "users/sort-posts": "Abantu bashyizeho byinshi", + "users/sort-reputation": "Abantu bafite amanota menshi", + "users/banned": "Abantu Bakumiriwe", + "users/most-flags": "Most flagged users", + "users/search": "Gushaka Abantu", + "notifications": "Amatangazo", + "tags": "Ibimenyetso", + "tag": "Topics tagged under "%1"", + "register": "Fungura Konte", + "registration-complete": "Registration complete", + "login": "Injira muri konte yawe", + "reset": "Tangiza bundi bushya konte yawe", + "categories": "Ibyiciro", + "groups": "Amatsinda", + "group": "Itsinda %1 ", + "chats": "Mu Gikari", + "chat": "Ukuganira na %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Uguhindura \"%1\"", + "account/edit/password": "Uguhindura ijambobanga rya \"%1\"", + "account/edit/username": "Uguhindura izina rya \"%1\"", + "account/edit/email": "Uguhindura email ya \"%1\"", + "account/info": "Account Info", + "account/following": "Abantu %1 akurikira", + "account/followers": "Abantu bakurikira %1", + "account/posts": "Ibyashyizweho na %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Ibiganiro byatangijwe na %1", + "account/groups": "Amatsinda ya %1", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Itunganya", + "account/watched": "Ibiganiro bikurikirwa na %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Ibiganiro byakunzwe na %1", + "account/downvoted": "Ibiganiro byanzwe na %1", + "account/best": "Ibihebuje byashyizweho na %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Yemejwe", + "maintenance.text": "%1 ntiboneka kuko ubu iri gutunganywa. Muze kongera kugaruka. ", + "maintenance.messageIntro": "Byongeye, kandi, umuyobozi yasize ubu butumwa: ", + "throttled.text": "% ntibonetse kubera ukunanirwa. Uze kugaruka ikindi gihe. " +} \ No newline at end of file diff --git a/public/language/rw/post-queue.json b/public/language/rw/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/rw/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/rw/recent.json b/public/language/rw/recent.json new file mode 100644 index 0000000000..ac9843086b --- /dev/null +++ b/public/language/rw/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Ubuheruka", + "day": "Umunsi", + "week": "Icyumweru", + "month": "Ukwezi", + "year": "Umwaka", + "alltime": "Ibihe Byose", + "no_recent_topics": "Nta biganiro biheruka. ", + "no_popular_topics": "Nta biganiro bikunzwe. ", + "there-is-a-new-topic": "Hari ikiganiro gishya. ", + "there-is-a-new-topic-and-a-new-post": "Hari ikiganiro gishya kimwe n'icyashyizweho gishya kimwe. ", + "there-is-a-new-topic-and-new-posts": "Hari ikiganiro gishya kimwe n'ibyashyizweho bishya %1 .", + "there-are-new-topics": "Hari ibiganiro bishya %1. ", + "there-are-new-topics-and-a-new-post": "Hari ibiganiro bishya %1 n'icyashyizweho gishya kimwe.", + "there-are-new-topics-and-new-posts": "Hari ibiganiro bishya %1 n'ibyashyizweho bishya %2.", + "there-is-a-new-post": "Hari icyashyizweho gishya. ", + "there-are-new-posts": "Hari ibyashyizweho bishya %1.", + "click-here-to-reload": "Kanda hano wongere upakire." +} \ No newline at end of file diff --git a/public/language/rw/register.json b/public/language/rw/register.json new file mode 100644 index 0000000000..22e2e0b554 --- /dev/null +++ b/public/language/rw/register.json @@ -0,0 +1,32 @@ +{ + "register": "Iyandike", + "cancel_registration": "Cancel Registration", + "help.email": "Ubusanzwe, email yawe ntabwo iba ibonwa na bose", + "help.username_restrictions": "Izina rigomba kuba ryihariye kuri uru rubuga kandi rikaba rifite uburebure bw'inyuguti buva kuri %1 kugera kuri %2. Iryo zina ni ryo abantu bazajya bifashisha nka @username mu gihe bakoresheje izina ryawe mu byo banditse. ", + "help.minimum_password_length": "Umubare w'inyuguti n'ibimenyetso bigize ijambobanga ryawe ugomba kuba nibura %1.", + "email_address": "Aderesi ya Email", + "email_address_placeholder": "Shyiramo Aderesi ya Email", + "username": "Izina Ukoresha", + "username_placeholder": "Shyiramo Izina Ukoresha", + "password": "Ijambobanga", + "password_placeholder": "Shyiramo Ijambobanga", + "confirm_password": "Emeza Ijambobanga", + "confirm_password_placeholder": "Emeza Ijambobanga", + "register_now_button": "Iyandike", + "alternative_registration": "Ukundi Wakwiyandika", + "terms_of_use": "Amategeko n'Amabwiriza", + "agree_to_terms_of_use": "Nzakurikiza Amategeko n'Amabwiriza", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Icyifuzo cy'iyandikwa ryawe cyakiriwe ariko gitegereje isuzuma. Uzabimenyeshwa biciye muri email niba ubuyobozi bwakwemereye kwandikwa. ", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/rw/reset_password.json b/public/language/rw/reset_password.json new file mode 100644 index 0000000000..a59fb331bf --- /dev/null +++ b/public/language/rw/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Rema Bundibushya Ijambobanga", + "update_password": "Vugurura Ijambobanga", + "password_changed.title": "Ijambobanga Ryahinduwe", + "password_changed.message": "

Ijambobanga ryaremwe bundi bushya. Urasabwa kongera ukinjiramo.", + "wrong_reset_code.title": "Kode Itari Yo mu Kurema Bundibushya Ijambobanga", + "wrong_reset_code.message": "Kode yakiriwe mu kurema bundibushya ijambobanga si yo. Ongera ugerageze cyangwa se usabe indi kode.", + "new_password": "Ijambobanga Rishya", + "repeat_password": "Emeza Ijambobanga", + "changing_password": "Changing Password", + "enter_email": "Tanga email ukoresha maze tuze kukoherereza ubutumwa bugusobanuria uko uri bureme bundibushya konte yawe.", + "enter_email_address": "Shyiramo Email", + "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.", + "invalid_email": "Email Itemewe / Email Itabaho!", + "password_too_short": "Ijambobanga washyizemo ni rigufi cyane. Gerageza ufate irindi. ", + "passwords_do_not_match": "Ijambobanga waryanditse mu buryo bubiri butandukanye kandi bitemewe. ", + "password_expired": "Ijambobanga ryawe ryarashaje. Shaka irindi. " +} \ No newline at end of file diff --git a/public/language/rw/search.json b/public/language/rw/search.json new file mode 100644 index 0000000000..940fdf2ebe --- /dev/null +++ b/public/language/rw/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "Habonetse ibintu (ikintu) %1 gihura na \"%2\". (Byafashe amasegonda %3)", + "no-matches": "Nta cyabonetse", + "advanced-search": "Gushaka Byisumbuye", + "in": "Muri", + "titles": "Imitwe", + "titles-posts": "Imitwe n'Ibyashyizweho", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Mu Byashyizweho na", + "in-categories": "Mu Byiciro bya", + "search-child-categories": "Shakira no mu byiciro bikomokaho", + "has-tags": "Has tags", + "reply-count": "Umubare w'Ibisubizo", + "at-least": "Ungana Nibura na", + "at-most": "Utarengeje", + "relevance": "Relevance", + "post-time": "Igihe Byashyiriweho", + "votes": "Votes", + "newer-than": "Nyuma ya", + "older-than": "Mbere ya", + "any-date": "Itariki Yose", + "yesterday": "Ejo Hashize", + "one-week": "Icyumweru kimwe", + "two-weeks": "Ibyumweru bibiri", + "one-month": "Ukwezi kumwe", + "three-months": "Amezi atatu", + "six-months": "Amezi atandatu", + "one-year": "Umwaka umwe", + "sort-by": "Bigaragare Ukurikije", + "last-reply-time": "Igihe baherukira gusubiza", + "topic-title": "Umutwe w'ikiganiro", + "topic-votes": "Topic votes", + "number-of-replies": "Umubare w'ibisubizo", + "number-of-views": "Umubare w'ababirebye", + "topic-start-date": "Igihe ikiganiro cyatangijwe", + "username": "Izina ry'umukoresha", + "category": "Icyiciro", + "descending": "Uva ku kinini ujya ku gito", + "ascending": "Uva ku gito ujya ku kinini", + "save-preferences": "Bika ibyo wahisemo", + "clear-preferences": "Hanagura ibyo wahisemo", + "search-preferences-saved": "Ibyo wahisemo mu gihe cy'ishaka byabitswe", + "search-preferences-cleared": "Ibyo wahisemo mu gihe cy'ishaka byahanaguwe", + "show-results-as": "Ibiboneka bigaragazwe nk'", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/rw/success.json b/public/language/rw/success.json new file mode 100644 index 0000000000..1b764a4f09 --- /dev/null +++ b/public/language/rw/success.json @@ -0,0 +1,7 @@ +{ + "success": "Byaciyemo", + "topic-post": "Wabishyizeho nta ngorane. ", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Igenzura Ryaciyemo", + "settings-saved": "Ibyatunganyijwe byakiriwe!" +} \ No newline at end of file diff --git a/public/language/rw/tags.json b/public/language/rw/tags.json new file mode 100644 index 0000000000..eb54809c75 --- /dev/null +++ b/public/language/rw/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nta biganiro bifite aka kamenyetso bihari. ", + "tags": "Utumenyetso", + "enter_tags_here": "Andika akamenyetso bijyanye aha. Buri kamenyetso kagomba kuba kagizwe n'inyuguti hagati ya %1 na %2. ", + "enter_tags_here_short": "Shyiraho utumenyetso...", + "no_tags": "Nta tumenyetso twari twashyirwaho. ", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/rw/top.json b/public/language/rw/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/rw/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/rw/topic.json b/public/language/rw/topic.json new file mode 100644 index 0000000000..414fada4c9 --- /dev/null +++ b/public/language/rw/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Ikiganiro", + "title": "Title", + "no_topics_found": "Nta kiganiro cyabonetse!", + "no_posts_found": "Nta cyashyizweho cyabonetse!", + "post_is_deleted": "Ibyari byanditse byakuweho!", + "topic_is_deleted": "Iki kiganiro cyakuweho!", + "profile": "Ishusho", + "posted_by": "Byashyizweho na %1", + "posted_by_guest": "Byashyizweho na Umushyitsi", + "chat": "Igikari", + "notify_me": "Uzajye umenyeshwa ibisubizo bishya kuri iki kiganiro", + "quote": "Terura", + "reply": "Subiza", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Bishyireho nk'ikiganiro", + "guest-login-reply": "Injiramo maze usubize", + "login-to-view": "🔒 Log in to view", + "edit": "Hinduraho", + "delete": "Siba", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Sibanganya", + "restore": "Garuraho", + "move": "Imura", + "change-owner": "Change Owner", + "fork": "Gabanyamo", + "link": "Shyiraho Umurongo", + "share": "Sangiza", + "tools": "Ibikoresho", + "locked": "Birafungiranye", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Iki kiganiro cyamaze gukurwaho. Abantu babifitiye uburenganzira ni bo bonyine bashobora kukibona. ", + "following_topic.message": "Ntabwo uzongera kubimenyeshwa nihagira umuntu ugira icyo yandika kuri iki kiganiro. ", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Ba umunyamuryango cyangwa winjiremo niba ushaka kwiyandikisha kuri iki kiganiro. ", + "markAsUnreadForAll.success": "Ikiganiro kigizwe nk'icyasomwe na bose", + "mark_unread": "Garagaza nk'ibyasomwe", + "mark_unread.success": "Ikiganiro cyagaragajwe nk'icyasomwe.", + "watch": "Cunga", + "unwatch": "Rekeraho Gucunga", + "watch.title": "Ujye umenyeshwa ibyongerwaho bishya kuri iki kiganiro", + "unwatch.title": "Rekera aho gucunga iki kiganiro", + "share_this_post": "Sangiza Ibi", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Ibikoresho by'Ikiganiro", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Zamura Ikiganiro", + "thread_tools.unpin": "Manura Ikiganiro", + "thread_tools.lock": "Fungirana Ikiganiro", + "thread_tools.unlock": "Fungurira Ikiganiro", + "thread_tools.move": "Imura Ikiganiro", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Byimure Byose", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Gabanyaho ku Kiganiro", + "thread_tools.delete": "Kuraho Ikiganiro", + "thread_tools.delete-posts": "Siba Icyashizweho", + "thread_tools.delete_confirm": "Wiringiye neza ko ushaka gukuraho iki kiganiro?", + "thread_tools.restore": "Subizaho Ikiganiro", + "thread_tools.restore_confirm": "Wiringiye neza ko ushaka kugarura iki kiganiro?", + "thread_tools.purge": "Sibanganya Ikiganiro", + "thread_tools.purge_confirm": "Wiringiye neza ko ushaka gusibanganya iki kiganiro?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Wiringiye neza ko ushaka gukuraho iki kiganiro?", + "post_restore_confirm": "Wiringiye neza ko ushaka kugarura iki kiganiro? ", + "post_purge_confirm": "Wiringiye neza ko ushaka gusibangaya iki kiganiro?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Ibyiciro Biraje", + "confirm_move": "Imura", + "confirm_fork": "Gabanyaho", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Ibindi Biraje", + "move_topic": "Imura Ikiganiro", + "move_topics": "Imura Ibiganiro", + "move_post": "Imura Icyashyizweho", + "post_moved": "Icyashizweho kirimuwe!", + "fork_topic": "Gabanyaho ku Kiganiro", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Kanda ku byashizweho ushaka kugabanyaho", + "fork_no_pids": "Nta kintu wahisemo!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Umaze kugabanyaho ku kiganiro! Kanda hano ugezwe ku kiganiro cyavutse. ", + "delete_posts_instruction": "Kanda ku bintu ushaka guhisha/gusiba", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Shyira umutwe w'ikiganiro cyawe aha...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Byihorere", + "composer.submit": "Shyiraho", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Gusubiza %1", + "composer.new_topic": "Ikiganiro Gishya", + "composer.editing": "Editing", + "composer.uploading": "gupakira...", + "composer.thumb_url_label": "Omekaho thumbnail URL y'ikiganiro", + "composer.thumb_title": "Ongera agafotondanga kuri iki kiganiro", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Cyangwa upakireho ifayilo ", + "composer.thumb_remove": "Hanagura imirongo", + "composer.drag_and_drop_images": "Terura Ubundi Utereke Amafoto Aha", + "more_users_and_guests": "Abantu (umuntu) banditse barenga %1 n'abashyitsi (umushyitsi) %2 ", + "more_users": "Abantu (umuntu) banditse barenga %1 ", + "more_guests": "Abashyitsi (umushyitsi) barenga %1 ", + "users_and_others": "%1 n'abandi %2 ", + "sort_by": "Ubigaragaze Ukurikije", + "oldest_to_newest": "Ibya Kera Ujya ku bya Vuba", + "newest_to_oldest": "Ibya Vuba Ujya ku bya Kera", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Urashaka gutangiza ahubwo ikiganiro gishya?", + "stale.warning": "Ikiganiro ushaka kuvugaho cyarashaje. Wahitamo gutangiza ikiganiro gishya ariko wenda ukagaragaza kino mu gisubizo uza gushyiraho?", + "stale.create": "Tangiza ikiganiro gishya", + "stale.reply_anyway": "Vuga kuri iki kiganiro nubundi", + "link_back": "Igisubizo: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/rw/unread.json b/public/language/rw/unread.json new file mode 100644 index 0000000000..9946c9a5fa --- /dev/null +++ b/public/language/rw/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Ibitarasomwa", + "no_unread_topics": "Nta biganiro bitarasomwa bihari. ", + "load_more": "Zana Ibindi", + "mark_as_read": "Bigire nkaho Byasomwe", + "selected": "Ibyatoranyijwe", + "all": "Byose", + "all_categories": "Ibyiciro Byose", + "topics_marked_as_read.success": "Ibiganiro byamaze kugaragazwa nk'ibyasomwe!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/rw/uploads.json b/public/language/rw/uploads.json new file mode 100644 index 0000000000..651a839876 --- /dev/null +++ b/public/language/rw/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Uploading the file...", + "select-file-to-upload": "Select a file to upload!", + "upload-success": "File uploaded successfully!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/rw/user.json b/public/language/rw/user.json new file mode 100644 index 0000000000..a3b73ebdbe --- /dev/null +++ b/public/language/rw/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Yarirukanwe", + "muted": "Muted", + "offline": "Ntari ku Murongo", + "deleted": "Deleted", + "username": "Izina ry'Umuntu", + "joindate": "Igiye Yaziye", + "postcount": "Ingano y'ibyo Yashyizeho", + "email": "Email", + "confirm_email": "Emeza Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Irukana", + "ban_account_confirm": "Wiringiye neza ko ushaka kwirukana uyu muntu?", + "unban_account": "Garura iyi Konte", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Siba Konte", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Konte yasibwe", + "account-content-deleted": "Account content deleted", + "fullname": "Izina Ryuzuye", + "website": "Urubuga", + "location": "Ahantu", + "age": "Imyaka", + "joined": "Yaje", + "lastonline": "Aheruka ku Murongo", + "profile": "Ishusho", + "profile_views": "Ishusho Yarebwe", + "reputation": "Amanota", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Ibikurikiranwa", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Abamukurikira", + "following": "Akurikira", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "Inshamake y'Ubuzima", + "signature": "Intero", + "birthday": "Itariki y'Amavuko", + "chat": "Mu Gikari", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Kurikira", + "unfollow": "Ntukurikire", + "more": "Ibindi", + "profile_update_success": "Ishusho yashyizwe ku gihe nta ngorane!", + "change_picture": "Hindura Ifoto", + "change_username": "Hindura Izina", + "change_email": "Hindura Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Hinduraho", + "edit-profile": "Hinduraho ku Ishusho", + "default_picture": "Akamenyetso Gasanzwe", + "uploaded_picture": "Ifoto Yapakiwe", + "upload_new_picture": "Pakira Ifoto Nshya", + "upload_new_picture_from_url": "Pakira Ifoto Nshya Ukoresheje URL", + "current_password": "Ijambobanga Risanzweho", + "change_password": "Hindura Ijambobanga", + "change_password_error": "Ijambobanga Ritari Ryo!", + "change_password_error_wrong_current": "Ijambobanga ryawe watanze nk'irisanzweho ntabwo ari ryo!", + "change_password_error_match": "Ijambobanga ugomba kuryandukura mu buryo bumwe inshuro ebyiri!", + "change_password_error_privileges": "Nta burenganzira ufite bwo guhindura iri jambobanga. ", + "change_password_success": "Ijambobanga ryawe ryavuguruwe!", + "confirm_password": "Emeza Ijambobanga", + "password": "Ijambobanga", + "username_taken_workaround": "Izina ushaka kujya ukoresha twasanze ryarafashwe. Ntugire impungenge kuko twakuboneye iryo byenda kumera kimwe. Uzaba uzwi ku izina rya %1", + "password_same_as_username": "Ijambobanga ryawe rirasa neza n'izina ukoresha; hitamo irindi jambobanga.", + "password_same_as_email": "Ijambobanga ryawe rirasa neza na email yawe; hitamo irindi jambobanga.", + "weak_password": "Weak password.", + "upload_picture": "Gushyiraho ifoto", + "upload_a_picture": "Shyiraho ifoto", + "remove_uploaded_picture": "Kuraho Ifoto", + "upload_cover_picture": "Pakira ifoto yo hejuru", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Itunganya", + "show_email": "Hagaragazwe Email Yanjye", + "show_fullname": "Hagaragazwe Izina Ryuzuye Ryanjye", + "restrict_chats": "Emerera ubutumwa buciye mu gikari abantu ukurikira gusa", + "digest_label": "Iyandikishe ku Ngingo z'Ingenzi", + "digest_description": "Iyandikishe ku makuru aciye kuri email ajyanye n'ibivugirwa aha (amatangazo mashya n'ibiganiro) biciye muri gahunda yagenwe", + "digest_off": "Birafunze", + "digest_daily": "Buri Munsi", + "digest_weekly": "Buri Cyumweru", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Buri Kwezi", + "has_no_follower": "Uyu muntu ntabwo afite abamukurikira :(", + "follows_no_one": "Uyu muntu ntabwo akurikira umuntu numwe :(", + "has_no_posts": "Uyu muntu nta kintu arashyiraho. ", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Uyu muntu nta kiganiro aratangiza na kimwe. ", + "has_no_watched_topics": "Uyu muntu ntabwo arakurikira ikiganiro na kimwe.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "Uyu muntu ntabwo arashima icyashyizweho na kimwe.", + "has_no_downvoted_posts": "Uyu muntu ntabwo aragaya icyashizweho na kimwe. ", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Email Yahishwe", + "hidden": "byahishwe", + "paginate_description": "Gabanya ibiganiro n'ibyashyizweho mu ma paji aho kugirango umuntu ajye amanuka ubudahagarara ", + "topics_per_page": "Ibiganiro kuri Buri Paji", + "posts_per_page": "Ibyashyizweho kuri Buri Paji", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Gutunganya Uburyo Usoma", + "open_links_in_new_tab": "Fungurira imirongo ijya hanze mu idirishya rishya", + "enable_topic_searching": "Emerera Ugushakira mu Kiganiro", + "topic_search_help": "Nibyemerwa, ugushakira mu kiganiro bizajya biba ari byo bikorwa maze bitume umuntu abasha gushakira mu kiganiro hose aho gushakira kuri paji igaragarira amaso, imbere yawe gusa", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Nyuma yo gushyiraho igisubizo, hagaragare icyashyizweho gishya", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Nta mutwe w'itsinda", + "select-skin": "Hitamo Uruhu", + "select-homepage": "Hitamo Paji y'Imbere", + "homepage": "Paji y'Imbere", + "homepage_description": "Hitamo paji yo kugaragaza imbere cyangwa ntuyihitemo kugirango hakoreshwe paji uru rubuga rwagennye", + "custom_route": "Umurongo Wundi wa Paji y'Imbere", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Kwinjiramo ukoreshe serivisi za SSO", + "sso.associated": "Bisanishijwe na", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/rw/users.json b/public/language/rw/users.json new file mode 100644 index 0000000000..37dd4db9d6 --- /dev/null +++ b/public/language/rw/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Abantu Bashya", + "top_posters": "Abashyizeho Byinshi", + "most_reputation": "Abafite Amanota Menshi", + "most_flags": "Most Flags", + "search": "Shaka", + "enter_username": "Shyiramo izina ryo gushaka", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Zana Ibindi", + "users-found-search-took": "Habonetse abantu (umuntu) %1! Byatwaye amasegonda %2 gusa.", + "filter-by": "Yungurura Ukurikije", + "online-only": "Abari ku murongo gusa", + "invite": "Tumira", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Ubutumire bwa email bwohererejwe %1", + "user_list": "Urutonde rw'Abantu", + "recent_topics": "Ibiganiro Biheruka", + "popular_topics": "Ibiganiro Bikunzwe", + "unread_topics": "Ibiganiro Bitarasomwa", + "categories": "Ibyiciro", + "tags": "Ibimenyetso", + "no-users-found": "Nta muntu wabonetse" +} \ No newline at end of file diff --git a/public/language/sc/_DO_NOT_EDIT_FILES_HERE.md b/public/language/sc/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/sc/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/sc/admin/admin.json b/public/language/sc/admin/admin.json new file mode 100644 index 0000000000..39edffb66f --- /dev/null +++ b/public/language/sc/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Are you sure you wish to restart NodeBB?", + + "acp-title": "%1 | NodeBB Admin Control Panel", + "settings-header-contents": "Contents", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/sc/admin/advanced/cache.json b/public/language/sc/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/sc/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/sc/admin/advanced/database.json b/public/language/sc/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/sc/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/sc/admin/advanced/errors.json b/public/language/sc/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/sc/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/sc/admin/advanced/events.json b/public/language/sc/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/sc/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/sc/admin/advanced/logs.json b/public/language/sc/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/sc/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/sc/admin/appearance/customise.json b/public/language/sc/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/sc/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/sc/admin/appearance/skins.json b/public/language/sc/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/sc/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/sc/admin/appearance/themes.json b/public/language/sc/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/sc/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/sc/admin/dashboard.json b/public/language/sc/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/sc/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/sc/admin/development/info.json b/public/language/sc/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/sc/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/sc/admin/development/logger.json b/public/language/sc/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/sc/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/sc/admin/extend/plugins.json b/public/language/sc/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/sc/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/sc/admin/extend/rewards.json b/public/language/sc/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/sc/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/sc/admin/extend/widgets.json b/public/language/sc/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/sc/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/admins-mods.json b/public/language/sc/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/sc/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/categories.json b/public/language/sc/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/sc/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/digest.json b/public/language/sc/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/sc/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/sc/admin/manage/groups.json b/public/language/sc/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/sc/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/privileges.json b/public/language/sc/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/sc/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/registration.json b/public/language/sc/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/sc/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/tags.json b/public/language/sc/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/sc/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/uploads.json b/public/language/sc/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/sc/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/sc/admin/manage/users.json b/public/language/sc/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/sc/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/sc/admin/menu.json b/public/language/sc/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/sc/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/advanced.json b/public/language/sc/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/sc/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/api.json b/public/language/sc/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/sc/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/chat.json b/public/language/sc/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/sc/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/cookies.json b/public/language/sc/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/sc/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/email.json b/public/language/sc/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/sc/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/sc/admin/settings/general.json b/public/language/sc/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/sc/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/sc/admin/settings/group.json b/public/language/sc/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/sc/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/guest.json b/public/language/sc/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/sc/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/homepage.json b/public/language/sc/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/sc/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/languages.json b/public/language/sc/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/sc/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/navigation.json b/public/language/sc/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/sc/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/sc/admin/settings/notifications.json b/public/language/sc/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/sc/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/pagination.json b/public/language/sc/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/sc/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/post.json b/public/language/sc/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/sc/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/reputation.json b/public/language/sc/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/sc/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/social.json b/public/language/sc/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/sc/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/sockets.json b/public/language/sc/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/sc/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/sounds.json b/public/language/sc/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/sc/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/tags.json b/public/language/sc/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/sc/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/sc/admin/settings/uploads.json b/public/language/sc/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/sc/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/sc/admin/settings/user.json b/public/language/sc/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/sc/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/sc/admin/settings/web-crawler.json b/public/language/sc/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/sc/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/sc/category.json b/public/language/sc/category.json new file mode 100644 index 0000000000..f535d5d85a --- /dev/null +++ b/public/language/sc/category.json @@ -0,0 +1,23 @@ +{ + "category": "Category", + "subcategories": "Subcategories", + "new_topic_button": "Arresonada Noa", + "guest-login-post": "Log in to post", + "no_topics": "Non bi sunt arresonadas in custa creze.
Pro ite non nde pones una?", + "browsing": "navighende", + "no_replies": "Perunu at rispostu", + "no_new_posts": "No new posts.", + "watch": "Watch", + "ignore": "Ignore", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Watched categories", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/sc/email.json b/public/language/sc/email.json new file mode 100644 index 0000000000..9f748a2e61 --- /dev/null +++ b/public/language/sc/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Welcome to %1", + "invite": "Invitation from %1", + "greeting_no_name": "Hello", + "greeting_with_name": "Hello %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Thank you for registering with %1!", + "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", + "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.cta": "Click here to confirm your email address", + "invitation.text1": "%1 has invited you to join %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", + "reset.text2": "To continue with the password reset, please click on the following link:", + "reset.cta": "Click here to reset your password", + "reset.notify.subject": "Password successfully changed", + "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", + "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "digest.latest_topics": "Latest topics from %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Click here to visit %1", + "digest.unsub.info": "This digest was sent to you due to your subscription settings.", + "digest.day": "day", + "digest.week": "week", + "digest.month": "month", + "digest.subject": "Digest for %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "New chat message received from %1", + "notif.chat.cta": "Click here to continue the conversation", + "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", + "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", + "unsub.cta": "Click here to alter those settings", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Thanks!" +} \ No newline at end of file diff --git a/public/language/sc/error.json b/public/language/sc/error.json new file mode 100644 index 0000000000..4191fad94f --- /dev/null +++ b/public/language/sc/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Invalid Data", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "You don't seem to be logged in.", + "account-locked": "Your account has been locked temporarily", + "search-requires-login": "Searching requires an account - please login or register.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Invalid Category ID", + "invalid-tid": "Invalid Topic ID", + "invalid-pid": "Invalid Post ID", + "invalid-uid": "Invalid User ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Invalid Username", + "invalid-email": "Invalid Email", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "Invalid User Data", + "invalid-password": "Invalid Password", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Please specify both a username and password", + "invalid-search-term": "Invalid search term", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "username-taken": "Username taken", + "email-taken": "Email taken", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "We could not confirm your email, please try again later.", + "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", + "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "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-same-password": "Please use a password that is different from your current one", + "user-banned": "User banned", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", + "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", + "ban-expiry-missing": "Please provide an end date for this ban", + "no-category": "Category does not exist", + "no-topic": "Topic does not exist", + "no-post": "Post does not exist", + "no-group": "Group does not exist", + "no-user": "User does not exist", + "no-teaser": "Teaser does not exist", + "no-flag": "Flag does not exist", + "no-privileges": "You do not have enough privileges for this action.", + "category-disabled": "Category disabled", + "topic-locked": "Topic Locked", + "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", + "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", + "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", + "post-edit-duration-expired-hours": "You are only allowed to edit posts for %1 hour(s) after posting", + "post-edit-duration-expired-hours-minutes": "You are only allowed to edit posts for %1 hour(s) %2 minute(s) after posting", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "You can't delete your topic after it has a reply", + "cant-delete-topic-has-replies": "You can't delete your topic after it has %1 replies", + "content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).", + "content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).", + "title-too-short": "Please enter a longer title. Titles should contain at least %1 character(s).", + "title-too-long": "Please enter a shorter title. Titles can't be longer than %1 character(s).", + "category-not-selected": "Category not selected.", + "too-many-posts": "You can only post once every %1 second(s) - please wait before posting again", + "too-many-posts-newbie": "As a new user, you can only post once every %1 second(s) until you have earned %2 reputation - please wait before posting again", + "already-posting": "You are already posting", + "tag-too-short": "Please enter a longer tag. Tags should contain at least %1 character(s)", + "tag-too-long": "Please enter a shorter tag. Tags can't be longer than %1 character(s)", + "not-enough-tags": "Not enough tags. Topics must have at least %1 tag(s)", + "too-many-tags": "Too many tags. Topics can't have more than %1 tag(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Please wait for uploads to complete.", + "file-too-big": "Maximum allowed file size is %1 kB - please upload a smaller file", + "guest-upload-disabled": "Guest uploading has been disabled", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "You can't ban other admins!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Invalid image type. Allowed types are: %1", + "invalid-image-extension": "Invalid image extension", + "invalid-file-type": "Invalid file type. Allowed types are: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Group name too short", + "group-name-too-long": "Group name too long", + "group-already-exists": "Group already exists", + "group-name-change-not-allowed": "Group name change not allowed", + "group-already-member": "Already part of this group", + "group-not-member": "Not a member of this group", + "group-needs-owner": "This group requires at least one owner", + "group-already-invited": "This user has already been invited", + "group-already-requested": "Your membership request has already been submitted", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "This post has already been deleted", + "post-already-restored": "This post has already been restored", + "topic-already-deleted": "This topic has already been deleted", + "topic-already-restored": "This topic has already been restored", + "cant-purge-main-post": "You can't purge the main post, please delete the topic instead", + "topic-thumbnails-are-disabled": "Topic thumbnails are disabled.", + "invalid-file": "Invalid File", + "uploads-are-disabled": "Uploads are disabled", + "signature-too-long": "Sorry, your signature cannot be longer than %1 character(s).", + "about-me-too-long": "Sorry, your about me cannot be longer than %1 character(s).", + "cant-chat-with-yourself": "You can't chat with yourself!", + "chat-restricted": "This user has restricted their chat messages. They must follow you before you can chat with them", + "chat-disabled": "Chat system disabled", + "too-many-messages": "You have sent too many messages, please wait awhile.", + "invalid-chat-message": "Invalid chat message", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "You are not allowed to edit this message", + "cant-delete-chat-message": "You are not allowed to delete this message", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "You have already voted for this post.", + "reputation-system-disabled": "Reputation system is disabled.", + "downvoting-disabled": "Downvoting is disabled", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", + "registration-error": "Registration Error", + "parse-error": "Something went wrong while parsing server response", + "wrong-login-type-email": "Please use your email to login", + "wrong-login-type-username": "Please use your username to login", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).", + "no-session-found": "No login session found!", + "not-in-room": "User not in room", + "cant-kick-self": "You can't kick yourself from the group", + "no-users-selected": "No user(s) selected", + "invalid-home-page-route": "Invalid home page route", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/sc/flags.json b/public/language/sc/flags.json new file mode 100644 index 0000000000..8156f1b1fd --- /dev/null +++ b/public/language/sc/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Updated", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Filter Options", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Post", + "filter-type-user": "User", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Category", + "filter-quick-mine": "Assigned to me", + "filter-cid-all": "All categories", + "apply-filters": "Apply Filters", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flagged User", + "view-profile": "View Profile", + "start-new-chat": "Start New Chat", + "go-to-target": "View Flag Target", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "View Profile", + "user-edit": "Edit Profile", + + "notes": "Flag Notes", + "add-note": "Add Note", + "no-notes": "No shared notes.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Note Added", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "New/Open", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Rejected", + "no-assignee": "Not Assigned", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/sc/global.json b/public/language/sc/global.json new file mode 100644 index 0000000000..2fe64fec86 --- /dev/null +++ b/public/language/sc/global.json @@ -0,0 +1,126 @@ +{ + "home": "Domo", + "search": "Chirca", + "buttons.close": "Serra", + "403.title": "Intrada Blocada", + "403.message": "You seem to have stumbled upon a page that you do not have access to.", + "403.login": "Perhaps you should try logging in?", + "404.title": "No Agatadu", + "404.message": "You seem to have stumbled upon a page that does not exist. Return to the home page.", + "500.title": "Internal Error.", + "500.message": "Oops! Paret chi carchi cosa est andada male!", + "400.title": "Bad Request.", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Registra·ti", + "login": "Intra", + "please_log_in": "Pro praghere Intra", + "logout": "Essi·nche", + "posting_restriction_info": "Sa publicatzione immoe est limitada isceti a is impitadores registrados, carca inoghe pro intrare.", + "welcome_back": "Welcome Back", + "you_have_successfully_logged_in": "Ses intradu", + "save_changes": "Alloga Acontzos", + "save": "Save", + "close": "Serra", + "pagination": "Paginatzione", + "pagination.out_of": "%1 out of %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Amministradore", + "header.categories": "Categories", + "header.recent": "Ùrtimos", + "header.unread": "De lèghere", + "header.tags": "Tags", + "header.popular": "Populare", + "header.top": "Top", + "header.users": "Impitadores", + "header.groups": "Groups", + "header.chats": "Tzarras", + "header.notifications": "Notìficas", + "header.search": "Chirca", + "header.profile": "Perfilu", + "header.navigation": "Navigation", + "notifications.loading": "Carrighende Notìficas", + "chats.loading": "Carrighende Tzarras", + "motd.welcome": "Benebènnidu in NodeBB, sa prataforma de arresonos de su tempus benidore.", + "previouspage": "Pàgina a in Antis", + "nextpage": "Pàgina chi Sighit", + "alert.success": "Andat Bene", + "alert.error": "Faddina", + "alert.banned": "Blocadu", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Immoe non ses prus sighende %1!", + "alert.follow": "Immoe ses sighende %1!", + "users": "Users", + "topics": "Topics", + "posts": "Arresonos", + "x-posts": "%1 posts", + "best": "Best", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Upvoters", + "upvoted": "Upvoted", + "downvoters": "Downvoters", + "downvoted": "Downvoted", + "views": "Bìsitas", + "posters": "Posters", + "reputation": "Reputation", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "read more", + "more": "More", + "none": "None", + "posted_ago_by_guest": "posted %1 by Guest", + "posted_ago_by": "posted %1 by %2", + "posted_ago": "posted %1", + "posted_in": "posted in %1", + "posted_in_by": "posted in %1 by %2", + "posted_in_ago": "posted in %1 %2", + "posted_in_ago_by": "posted in %1 %2 by %3", + "user_posted_ago": "%1 posted %2", + "guest_posted_ago": "Guest posted %1", + "last_edited_by": "last edited by %1", + "norecentposts": "No Recent Posts", + "norecenttopics": "No Recent Topics", + "recentposts": "Ùrtimos Arresonos", + "recentips": "Ùrtimos IP Intrados", + "moderator_tools": "Moderator Tools", + "online": "In lìnia", + "away": "A tesu", + "dnd": "Do not disturb", + "invisible": "Invisìbile", + "offline": "Non in lìnia", + "email": "Email", + "language": "Language", + "guest": "Guest", + "guests": "Guests", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum Updated", + "updated.message": "This forum has just been updated to the latest version. Click here to refresh the page.", + "privacy": "Privacy", + "follow": "Follow", + "unfollow": "Unfollow", + "delete_all": "Delete All", + "map": "Map", + "sessions": "Login Sessions", + "ip_address": "IP Address", + "enter_page_number": "Enter page number", + "upload_file": "Upload file", + "upload": "Upload", + "uploads": "Uploads", + "allowed-file-types": "Allowed file types are %1", + "unsaved-changes": "You have unsaved changes. Are you sure you wish to navigate away?", + "reconnecting-message": "Looks like your connection to %1 was lost, please wait while we try to reconnect.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/sc/groups.json b/public/language/sc/groups.json new file mode 100644 index 0000000000..2072d52894 --- /dev/null +++ b/public/language/sc/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Groups", + "view_group": "View Group", + "owner": "Group Owner", + "new_group": "Create New Group", + "no_groups_found": "There are no groups to see", + "pending.accept": "Accept", + "pending.reject": "Reject", + "pending.accept_all": "Accept All", + "pending.reject_all": "Reject All", + "pending.none": "There are no pending members at this time", + "invited.none": "There are no invited members at this time", + "invited.uninvite": "Rescind Invitation", + "invited.search": "Search for a user to invite to this group", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Save", + "cover-saving": "Saving", + "details.title": "Group Details", + "details.members": "Member List", + "details.pending": "Pending Members", + "details.invited": "Invited Members", + "details.has_no_posts": "This group's members have not made any posts.", + "details.latest_posts": "Latest Posts", + "details.private": "Private", + "details.disableJoinRequests": "Disable join requests", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Grant/Rescind Ownership", + "details.kick": "Kick", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Group Administration", + "details.group_name": "Group Name", + "details.member_count": "Member Count", + "details.creation_date": "Creation Date", + "details.description": "Description", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Badge Preview", + "details.change_icon": "Change Icon", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Badge Text", + "details.userTitleEnabled": "Show Badge", + "details.private_help": "If enabled, joining of groups requires approval from a group owner", + "details.hidden": "Hidden", + "details.hidden_help": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "details.delete_group": "Delete Group", + "details.private_system_help": "Private groups is disabled at system level, this option does not do anything", + "event.updated": "Group details have been updated", + "event.deleted": "The group \"%1\" has been deleted", + "membership.accept-invitation": "Accept Invitation", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Invitation Pending", + "membership.join-group": "Join Group", + "membership.leave-group": "Leave Group", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Reject", + "new-group.group_name": "Group Name:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/sc/ip-blacklist.json b/public/language/sc/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/sc/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/sc/language.json b/public/language/sc/language.json new file mode 100644 index 0000000000..119262172f --- /dev/null +++ b/public/language/sc/language.json @@ -0,0 +1,5 @@ +{ + "name": "Sardu (Sardigna)", + "code": "sc", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/sc/login.json b/public/language/sc/login.json new file mode 100644 index 0000000000..f58314c709 --- /dev/null +++ b/public/language/sc/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Username / Email", + "username": "Username", + "remember_me": "Regorda·mi?", + "forgot_password": "Password Iscarèssida?", + "alternative_logins": "Intradas Alternativas", + "failed_login_attempt": "Login Unsuccessful", + "login_successful": "Ses intradu!", + "dont_have_account": "Don't have an account?", + "logged-out-due-to-inactivity": "You have been logged out of the Admin Control Panel due to inactivity", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/sc/modules.json b/public/language/sc/modules.json new file mode 100644 index 0000000000..a3aa3efaad --- /dev/null +++ b/public/language/sc/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chat with", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Imbia", + "chat.no_active": "Non tenes tzarras ativas.", + "chat.user_typing": "%1 is typing ...", + "chat.user_has_messaged_you": "%1 has messaged you.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Please select a recipient to view chat message history", + "chat.no-users-in-room": "No users in this room", + "chat.recent-chats": "Recent Chats", + "chat.contacts": "Contacts", + "chat.message-history": "Message History", + "chat.message-deleted": "Message Deleted", + "chat.options": "Chat options", + "chat.pop-out": "Pop out chat", + "chat.minimize": "Minimize", + "chat.maximize": "Maximize", + "chat.seven_days": "7 Days", + "chat.thirty_days": "30 Days", + "chat.three_months": "3 Months", + "chat.delete_message_confirm": "Are you sure you wish to delete this message?", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat 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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Compose", + "composer.show_preview": "Show Preview", + "composer.hide_preview": "Hide Preview", + "composer.user_said_in": "%1 said in %2:", + "composer.user_said": "%1 said:", + "composer.discard": "Are you sure you wish to discard this post?", + "composer.submit_and_lock": "Submit and Lock", + "composer.toggle_dropdown": "Toggle Dropdown", + "composer.uploading": "Uploading %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Strikethrough", + "composer.formatting.code": "Code", + "composer.formatting.link": "Link", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Upload Image", + "composer.upload-file": "Upload File", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Select a category", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Cancel", + "bootbox.confirm": "Confirm", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Cover Photo Positioning", + "cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"", + "cover.saved": "Cover photo image and position saved", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/sc/notifications.json b/public/language/sc/notifications.json new file mode 100644 index 0000000000..a4111545d1 --- /dev/null +++ b/public/language/sc/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notìficas", + "no_notifs": "Non tenes notìficas noas", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Back to %1", + "outgoing_link": "Acàpiu a Foras", + "outgoing_link_message": "You are now leaving %1", + "continue_to": "Continue to %1", + "return_to": "Return to %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "You have unread notifications.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "New message from %1", + "upvoted_your_post_in": "%1 has upvoted your post in %2.", + "upvoted_your_post_in_dual": "%1 and %2 have upvoted your post in %3.", + "upvoted_your_post_in_multiple": "%1 and %2 others have upvoted your post in %3.", + "moved_your_post": "%1 has moved your post to %2", + "moved_your_topic": "%1 has moved %2", + "user_flagged_post_in": "%1 flagged a post in %2", + "user_flagged_post_in_dual": "%1 and %2 flagged a post in %3", + "user_flagged_post_in_multiple": "%1 and %2 others flagged a post in %3", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 has posted a reply to: %2", + "user_posted_to_dual": "%1 and %2 have posted replies to: %3", + "user_posted_to_multiple": "%1 and %2 others have posted replies to: %3", + "user_posted_topic": "%1 has posted a new topic: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 started following you.", + "user_started_following_you_dual": "%1 and %2 started following you.", + "user_started_following_you_multiple": "%1 and %2 others started following you.", + "new_register": "%1 sent a registration request.", + "new_register_multiple": "There are %1 registration requests awaiting review.", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email Confirmed", + "email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.", + "email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.", + "email-confirm-sent": "Confirmation email sent.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/sc/pages.json b/public/language/sc/pages.json new file mode 100644 index 0000000000..1f96716f66 --- /dev/null +++ b/public/language/sc/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Domo", + "unread": "Arresonadas de Lèghere", + "popular-day": "Popular topics today", + "popular-week": "Popular topics this week", + "popular-month": "Popular topics this month", + "popular-alltime": "All time popular topics", + "recent": "Ùrtimas Arresonadas", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Online Users", + "users/latest": "Latest Users", + "users/sort-posts": "Users with the most posts", + "users/sort-reputation": "Users with the most reputation", + "users/banned": "Banned Users", + "users/most-flags": "Most flagged users", + "users/search": "User Search", + "notifications": "Notìficas", + "tags": "Tags", + "tag": "Topics tagged under "%1"", + "register": "Register an account", + "registration-complete": "Registration complete", + "login": "Login to your account", + "reset": "Reset your account password", + "categories": "Categories", + "groups": "Groups", + "group": "%1 group", + "chats": "Chats", + "chat": "Chatting with %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Editing \"%1\"", + "account/edit/password": "Editing password of \"%1\"", + "account/edit/username": "Editing username of \"%1\"", + "account/edit/email": "Editing email of \"%1\"", + "account/info": "Account Info", + "account/following": "People %1 follows", + "account/followers": "People who follow %1", + "account/posts": "Posts made by %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Topics created by %1", + "account/groups": "%1's Groups", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "User Settings", + "account/watched": "Topics watched by %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": "Posts upvoted by %1", + "account/downvoted": "Posts downvoted by %1", + "account/best": "Best posts made by %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "Email Confirmed", + "maintenance.text": "%1 is currently undergoing maintenance. Please come back another time.", + "maintenance.messageIntro": "Additionally, the administrator has left this message:", + "throttled.text": "%1 is currently unavailable due to excessive load. Please come back another time." +} \ No newline at end of file diff --git a/public/language/sc/post-queue.json b/public/language/sc/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/sc/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/sc/recent.json b/public/language/sc/recent.json new file mode 100644 index 0000000000..ea0c3c0752 --- /dev/null +++ b/public/language/sc/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Ùrtimos", + "day": "Die", + "week": "Chida", + "month": "Mese", + "year": "Year", + "alltime": "All Time", + "no_recent_topics": "Non bi sunt ùrtimas arresonadas.", + "no_popular_topics": "There are no popular topics.", + "there-is-a-new-topic": "There is a new topic.", + "there-is-a-new-topic-and-a-new-post": "There is a new topic and a new post.", + "there-is-a-new-topic-and-new-posts": "There is a new topic and %1 new posts.", + "there-are-new-topics": "There are %1 new topics.", + "there-are-new-topics-and-a-new-post": "There are %1 new topics and a new post.", + "there-are-new-topics-and-new-posts": "There are %1 new topics and %2 new posts.", + "there-is-a-new-post": "There is a new post.", + "there-are-new-posts": "There are %1 new posts.", + "click-here-to-reload": "Click here to reload." +} \ No newline at end of file diff --git a/public/language/sc/register.json b/public/language/sc/register.json new file mode 100644 index 0000000000..ff16e87b0c --- /dev/null +++ b/public/language/sc/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registra·ti", + "cancel_registration": "Cancel Registration", + "help.email": "S'email tua est cuada pro su pùblicu in manera predefinida.", + "help.username_restrictions": "Unu nùmene de impitadore ùnicu intre %1 e %2 caràtere. Is àteros t'ant a pòdere mentovare cun @nùmeneimpitadore.", + "help.minimum_password_length": "Sa password depet èssere a su mancu de %1 caràteres.", + "email_address": "Indiritzu Email", + "email_address_placeholder": "Pone s'Indiritzu Email", + "username": "Nùmene de Impitadore", + "username_placeholder": "Pone su Nùmene de Impitadore", + "password": "Password", + "password_placeholder": "Pone sa Password", + "confirm_password": "Cunfirma Password", + "confirm_password_placeholder": "Cunfirma Password", + "register_now_button": "Registra·ti Immoe", + "alternative_registration": "Registratziones Alternativas", + "terms_of_use": "Tèrmines de Impreu", + "agree_to_terms_of_use": "So de acòrdiu cun is Tèrmines de Impreu", + "terms_of_use_error": "You must agree to the Terms of Use", + "registration-added-to-queue": "Your registration has been added to the approval queue. You will receive an email when it is accepted by an administrator.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/sc/reset_password.json b/public/language/sc/reset_password.json new file mode 100644 index 0000000000..605d67cc30 --- /dev/null +++ b/public/language/sc/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Torra a seberare sa Password", + "update_password": "Annoa Password", + "password_changed.title": "Password Mudada", + "password_changed.message": "

Password torrada a assentare, pro praghere torra a intrare.", + "wrong_reset_code.title": "Còdighe de Reset Non Bàlidu", + "wrong_reset_code.message": "Su còdighe pro torrare a assentare sa password chi amus retzidu est isballiadu. Pro praghere torra a provare, o pedi unu còdighe pro torrare a assentare sa password nou.", + "new_password": "Password Noa", + "repeat_password": "Cunfirma Password", + "changing_password": "Changing Password", + "enter_email": "Pro praghere pone s'indiritzu email tuo e t'amus a imbiare un'email cun is istrutziones pro torrare a assentare s'intrada tua.", + "enter_email_address": "Pone s'Indiritzu Email", + "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.", + "invalid_email": "Email Non Bàlida / Email chi no esistit!", + "password_too_short": "The password entered is too short, please pick a different password.", + "passwords_do_not_match": "The two passwords you've entered do not match.", + "password_expired": "Your password has expired, please choose a new password" +} \ No newline at end of file diff --git a/public/language/sc/search.json b/public/language/sc/search.json new file mode 100644 index 0000000000..639cc0b653 --- /dev/null +++ b/public/language/sc/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)", + "no-matches": "No matches found", + "advanced-search": "Advanced Search", + "in": "In", + "titles": "Titles", + "titles-posts": "Titles and Posts", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Posted by", + "in-categories": "In Categories", + "search-child-categories": "Search child categories", + "has-tags": "Has tags", + "reply-count": "Reply Count", + "at-least": "At least", + "at-most": "At most", + "relevance": "Relevance", + "post-time": "Post time", + "votes": "Votes", + "newer-than": "Newer than", + "older-than": "Older than", + "any-date": "Any date", + "yesterday": "Yesterday", + "one-week": "One week", + "two-weeks": "Two weeks", + "one-month": "One month", + "three-months": "Three months", + "six-months": "Six months", + "one-year": "One year", + "sort-by": "Sort by", + "last-reply-time": "Last reply time", + "topic-title": "Topic title", + "topic-votes": "Topic votes", + "number-of-replies": "Number of replies", + "number-of-views": "Number of views", + "topic-start-date": "Topic start date", + "username": "Username", + "category": "Category", + "descending": "In descending order", + "ascending": "In ascending order", + "save-preferences": "Save preferences", + "clear-preferences": "Clear preferences", + "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)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/sc/success.json b/public/language/sc/success.json new file mode 100644 index 0000000000..7fa5550915 --- /dev/null +++ b/public/language/sc/success.json @@ -0,0 +1,7 @@ +{ + "success": "Success", + "topic-post": "You have successfully posted.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Authentication Successful", + "settings-saved": "Settings saved!" +} \ No newline at end of file diff --git a/public/language/sc/tags.json b/public/language/sc/tags.json new file mode 100644 index 0000000000..24ca6f8a39 --- /dev/null +++ b/public/language/sc/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "There are no topics with this tag.", + "tags": "Tags", + "enter_tags_here": "Enter tags here, between %1 and %2 characters each.", + "enter_tags_here_short": "Enter tags...", + "no_tags": "There are no tags yet.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/sc/top.json b/public/language/sc/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/sc/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/sc/topic.json b/public/language/sc/topic.json new file mode 100644 index 0000000000..73a29b2067 --- /dev/null +++ b/public/language/sc/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Arresonada", + "title": "Title", + "no_topics_found": "Peruna arresonada agatada!", + "no_posts_found": "Perunu arresonu agatadu!", + "post_is_deleted": "This post is deleted!", + "topic_is_deleted": "This topic is deleted!", + "profile": "Perfilu", + "posted_by": "Posted by %1", + "posted_by_guest": "Posted by Guest", + "chat": "Tzarra", + "notify_me": "Imbia·mi notìficas pro is rispostas noas a custa arresonada", + "quote": "Mèntova", + "reply": "Risponde", + "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", + "last_reply_time": "Last reply", + "reply-as-topic": "Reply as topic", + "guest-login-reply": "Log in to reply", + "login-to-view": "🔒 Log in to view", + "edit": "Acontza", + "delete": "Contzella", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Purge", + "restore": "Torra", + "move": "Move", + "change-owner": "Change Owner", + "fork": "Partzi", + "link": "Acàpiu", + "share": "Cumpartzi", + "tools": "Ainas", + "locked": "Locked", + "pinned": "Pinned", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Moved", + "moved-from": "Moved from %1", + "copy-ip": "Copy IP", + "ban-ip": "Ban IP", + "view-history": "Edit History", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Click here to return to the last read post in this thread.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "This topic has been deleted. Only users with topic management privileges can see it.", + "following_topic.message": "As a retzire notìficas si calincunu pùblica in custa arresonada.", + "not_following_topic.message": "You will see this topic in the unread topics list, but you will not receive notifications when somebody posts to this topic.", + "ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.", + "login_to_subscribe": "Pro praghere registra·ti o intra pro sutascrìere custa arresonada.", + "markAsUnreadForAll.success": "Arresonada marcada comente de lèghere pro totus.", + "mark_unread": "Mark unread", + "mark_unread.success": "Topic marked as unread.", + "watch": "Càstia", + "unwatch": "Unwatch", + "watch.title": "Be notified of new replies in this topic", + "unwatch.title": "Stop watching this topic", + "share_this_post": "Cumpartzi custu Arresonu", + "watching": "Watching", + "not-watching": "Not Watching", + "ignoring": "Ignoring", + "watching.description": "Notify me of new replies.
Show topic in unread.", + "not-watching.description": "Do not notify me of new replies.
Show topic in unread if category is not ignored.", + "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "thread_tools.title": "Topic Tools", + "thread_tools.markAsUnreadForAll": "Mark Unread For All", + "thread_tools.pin": "Pone in evidèntzia s'Arresonda", + "thread_tools.unpin": "Boga dae s'Evidèntzia s'Arresonasa", + "thread_tools.lock": "Bloca Arresonada", + "thread_tools.unlock": "Isbloca Arresonada", + "thread_tools.move": "Move Arresonada", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "Move All", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Select Category", + "thread_tools.fork": "Partzi Arresonada", + "thread_tools.delete": "Cantzella Arresonada", + "thread_tools.delete-posts": "Delete Posts", + "thread_tools.delete_confirm": "Are you sure you want to delete this topic?", + "thread_tools.restore": "Torra a s'Arresonada Allogada", + "thread_tools.restore_confirm": "Are you sure you want to restore this topic?", + "thread_tools.purge": "Purge Topic", + "thread_tools.purge_confirm": "Are you sure you want to purge this topic?", + "thread_tools.merge_topics": "Merge Topics", + "thread_tools.merge": "Merge", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Are you sure you want to delete this post?", + "post_restore_confirm": "Are you sure you want to restore this post?", + "post_purge_confirm": "Are you sure you want to purge this post?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Carrighende Crezes", + "confirm_move": "Move", + "confirm_fork": "Partzi", + "bookmark": "Bookmark", + "bookmarks": "Bookmarks", + "bookmarks.has_no_bookmarks": "You haven't bookmarked any posts yet.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Càrriga Prus Arresonos", + "move_topic": "Move Arresonada", + "move_topics": "Move Topics", + "move_post": "Move Arresonu", + "post_moved": "Post moved!", + "fork_topic": "Partzi Arresonada", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Sèbera is arresonos chi boles partzire", + "fork_no_pids": "Perunu arresonu seberadu!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 post(s) selected", + "fork_success": "Successfully forked topic! Click here to go to the forked topic.", + "delete_posts_instruction": "Click the posts you want to delete/purge", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Pone su tìtulu de s'arresonada inoghe...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Lassa a Pèrdere", + "composer.submit": "Imbia", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Replying to %1", + "composer.new_topic": "Arresonada Noa", + "composer.editing": "Editing", + "composer.uploading": "carrighende...", + "composer.thumb_url_label": "Apodda unu URL cun un'immàgine pro s'arresonada", + "composer.thumb_title": "Annanghe un'immàgine pitica a custa arresonada", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "O càrriga unu file", + "composer.thumb_remove": "Lìmpia is datos", + "composer.drag_and_drop_images": "Tràsina Immàgines Inoghe", + "more_users_and_guests": "%1 more user(s) and %2 guest(s)", + "more_users": "%1 more user(s)", + "more_guests": "%1 more guest(s)", + "users_and_others": "%1 and %2 others", + "sort_by": "Sort by", + "oldest_to_newest": "Oldest to Newest", + "newest_to_oldest": "Newest to Oldest", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "Create new topic instead?", + "stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?", + "stale.create": "Create a new topic", + "stale.reply_anyway": "Reply to this topic anyway", + "link_back": "Re: [%1](%2)", + "diffs.title": "Post Edit History", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/sc/unread.json b/public/language/sc/unread.json new file mode 100644 index 0000000000..c3a876f0c0 --- /dev/null +++ b/public/language/sc/unread.json @@ -0,0 +1,15 @@ +{ + "title": "De Lèghere", + "no_unread_topics": "Non bi sunt arresonadas de lèghere.", + "load_more": "Càrriga de Prus", + "mark_as_read": "Mark as Read", + "selected": "Selected", + "all": "All", + "all_categories": "All categories", + "topics_marked_as_read.success": "Topics marked as read!", + "all-topics": "All Topics", + "new-topics": "New Topics", + "watched-topics": "Watched Topics", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/sc/uploads.json b/public/language/sc/uploads.json new file mode 100644 index 0000000000..651a839876 --- /dev/null +++ b/public/language/sc/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Uploading the file...", + "select-file-to-upload": "Select a file to upload!", + "upload-success": "File uploaded successfully!", + "maximum-file-size": "Maximum %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/sc/user.json b/public/language/sc/user.json new file mode 100644 index 0000000000..03ffc0d018 --- /dev/null +++ b/public/language/sc/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Blocadu", + "muted": "Muted", + "offline": "Non in lìnia", + "deleted": "Deleted", + "username": "Nùmene de Impitadore", + "joindate": "Join Date", + "postcount": "Post Count", + "email": "Email", + "confirm_email": "Confirm Email", + "account_info": "Account Info", + "admin_actions_label": "Administrative Actions", + "ban_account": "Ban Account", + "ban_account_confirm": "Do you really want to ban this user?", + "unban_account": "Unban Account", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Delete Account", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Account deleted", + "account-content-deleted": "Account content deleted", + "fullname": "Nùmene e Sambenadu", + "website": "Giassu web", + "location": "Logu", + "age": "Edade", + "joined": "intradu", + "lastonline": "Ùrtimu Collegamentu", + "profile": "Perfilu", + "profile_views": "Bìsitas a su perfilu", + "reputation": "Nodidos", + "bookmarks": "Bookmarks", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Watched", + "ignored": "Ignored", + "default-category-watch-state": "Default category watch state", + "followers": "Sighidores", + "following": "Sighende", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "About me", + "signature": "Firma", + "birthday": "Cumpleannu", + "chat": "Tzarra", + "chat_with": "Continue chat with %1", + "new_chat_with": "Start new chat with %1", + "flag-profile": "Flag Profile", + "follow": "Sighi", + "unfollow": "Non sighes prus", + "more": "More", + "profile_update_success": "Profile has been updated successfully!", + "change_picture": "Muda Immàgine", + "change_username": "Change Username", + "change_email": "Change Email", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Acontza", + "edit-profile": "Edit Profile", + "default_picture": "Default Icon", + "uploaded_picture": "Immàgine Carrigada", + "upload_new_picture": "Càrriga Immàgine Noa", + "upload_new_picture_from_url": "Upload New Picture From URL", + "current_password": "Password Presente", + "change_password": "Muda Password", + "change_password_error": "Invalid Password!", + "change_password_error_wrong_current": "Your current password is not correct!", + "change_password_error_match": "Passwords must match!", + "change_password_error_privileges": "You do not have the rights to change this password.", + "change_password_success": "Your password is updated!", + "confirm_password": "Cunfirma Password", + "password": "Password", + "username_taken_workaround": "The username you requested was already taken, so we have altered it slightly. You are now known as %1", + "password_same_as_username": "Your password is the same as your username, please select another password.", + "password_same_as_email": "Your password is the same as your email, please select another password.", + "weak_password": "Weak password.", + "upload_picture": "Càrriga immàgine", + "upload_a_picture": "Càrriga un'immàgine", + "remove_uploaded_picture": "Remove Uploaded Picture", + "upload_cover_picture": "Upload cover picture", + "remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?", + "crop_picture": "Crop picture", + "upload_cropped_picture": "Crop and upload", + "avatar-background-colour": "Avatar background colour", + "settings": "Sèberos", + "show_email": "Ammustra s'Email Mia", + "show_fullname": "Show My Full Name", + "restrict_chats": "Only allow chat messages from users I follow", + "digest_label": "Subscribe to Digest", + "digest_description": "Subscribe to email updates for this forum (new notifications and topics) according to a set schedule", + "digest_off": "Off", + "digest_daily": "Daily", + "digest_weekly": "Weekly", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Monthly", + "has_no_follower": "Custu impitadore non tenet perunu sighidore :(", + "follows_no_one": "Custu impitadore no est sighende nissunu :(", + "has_no_posts": "This user hasn't posted anything yet.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "This user hasn't posted any topics yet.", + "has_no_watched_topics": "This user hasn't watched any topics yet.", + "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_upvoted_posts": "This user hasn't upvoted any posts yet.", + "has_no_downvoted_posts": "This user hasn't downvoted any posts yet.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "Email Cuada", + "hidden": "cuadu", + "paginate_description": "Paginate topics and posts instead of using infinite scroll", + "topics_per_page": "Arresonadas pro Pàgina", + "posts_per_page": "Arresonos pro Pàgina", + "max_items_per_page": "Maximum %1", + "acp_language": "Admin Page Language", + "notifications": "Notifications", + "upvote-notif-freq": "Upvote Notification Frequency", + "upvote-notif-freq.all": "All Upvotes", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "Every Ten Upvotes", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabled", + "browsing": "Browsing Settings", + "open_links_in_new_tab": "Open outgoing links in new tab", + "enable_topic_searching": "Enable In-Topic Searching", + "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "After posting a reply, show the new post", + "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_create": "Watch topics you create", + "grouptitle": "Group Title", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "No group title", + "select-skin": "Select a Skin", + "select-homepage": "Select a Homepage", + "homepage": "Homepage", + "homepage_description": "Select a page to use as the forum homepage or 'None' to use the default homepage.", + "custom_route": "Custom Homepage Route", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on Services", + "sso.associated": "Associated with", + "sso.not-associated": "Click here to associate with", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Reason", + "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Username History", + "info.email-history": "Email History", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Add note", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/sc/users.json b/public/language/sc/users.json new file mode 100644 index 0000000000..f00d892d88 --- /dev/null +++ b/public/language/sc/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Ùrtimos Impitadores", + "top_posters": "Prus Ativos", + "most_reputation": "Prus Famados", + "most_flags": "Most Flags", + "search": "Chirca", + "enter_username": "Pone unu nùmene de impitadore de chircare", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Càrriga de prus", + "users-found-search-took": "%1 user(s) found! Search took %2 seconds.", + "filter-by": "Filter By", + "online-only": "Online only", + "invite": "Invite", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "An invitation email has been sent to %1", + "user_list": "User List", + "recent_topics": "Recent Topics", + "popular_topics": "Popular Topics", + "unread_topics": "Unread Topics", + "categories": "Categories", + "tags": "Tags", + "no-users-found": "No users found!" +} \ No newline at end of file diff --git a/public/language/sk/_DO_NOT_EDIT_FILES_HERE.md b/public/language/sk/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/sk/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/sk/admin/admin.json b/public/language/sk/admin/admin.json new file mode 100644 index 0000000000..558d471507 --- /dev/null +++ b/public/language/sk/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Ste si istý, že chcete znova zostaviť a reštartovať NodeBB?", + "alert.confirm-restart": "Ste si naozaj istý/á, že chcete reštartovať NodeBB?", + + "acp-title": "Ovládací panel administrátora %1 | NodeBB ", + "settings-header-contents": "Obsah", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/sk/admin/advanced/cache.json b/public/language/sk/admin/advanced/cache.json new file mode 100644 index 0000000000..8790e8de60 --- /dev/null +++ b/public/language/sk/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Vyrovnávacia pamäť príspevku", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% plné", + "post-cache-size": "Veľkosť vyrovnávacej pamäti príspevku", + "items-in-cache": "Položky vo vyrovnávacej pamäti" +} \ No newline at end of file diff --git a/public/language/sk/admin/advanced/database.json b/public/language/sk/admin/advanced/database.json new file mode 100644 index 0000000000..6d2b239a61 --- /dev/null +++ b/public/language/sk/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Doba prevádzky v sekundách", + "uptime-days": "Doba prevádzky v dňoch", + + "mongo": "Monho", + "mongo.version": "Verzia MongoDB", + "mongo.storage-engine": "Modul úložiska ", + "mongo.collections": "Fondy", + "mongo.objects": "Objekty", + "mongo.avg-object-size": "Priemerná veľkosť objektu", + "mongo.data-size": "Veľkosť údajov", + "mongo.storage-size": "Veľkosť úložiska", + "mongo.index-size": "Veľkosť indexu", + "mongo.file-size": "Veľkosť súboru", + "mongo.resident-memory": "Rezidentná pamäť", + "mongo.virtual-memory": "Virtuálna pamäť", + "mongo.mapped-memory": "Namapovaná pamäť", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "Raw informácie MongoDB", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Verzia Redis", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Pripojených klientov", + "redis.connected-slaves": "Druhotné pripojenia", + "redis.blocked-clients": "Blokovaných klientov", + "redis.used-memory": "Použitá pamäť", + "redis.memory-frag-ratio": "Pomer fragmentácia pamäte", + "redis.total-connections-recieved": "Súhrnné množstvo pripojení", + "redis.total-commands-processed": "Súhrnne spracované príkazov", + "redis.iops": "Okamžité spracovanie za sekundu", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Spracovaných kľúčov", + "redis.keyspace-misses": "Chyby kľúča", + "redis.raw-info": "Informácie Redis Raw", + + "postgres": "Postgres", + "postgres.version": "Verzia PostgreSQL", + "postgres.raw-info": "Informácie o Postgres" +} diff --git a/public/language/sk/admin/advanced/errors.json b/public/language/sk/admin/advanced/errors.json new file mode 100644 index 0000000000..a1ccae6894 --- /dev/null +++ b/public/language/sk/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Vyriešiť %1", + "error-events-per-day": "%1 udalostí za deň", + "error.404": "Chyba 404 - Nenájdené", + "error.503": "Chyba 503 - Služba nie je k dispozícií", + "manage-error-log": "Spravovať záznamy s chybami", + "export-error-log": "Exportovať záznam s chybami (CSV)", + "clear-error-log": "Zmazať záznam s chybami", + "route": "Cesta", + "count": "Počet", + "no-routes-not-found": "Hurá! Žiadna chyba 404.", + "clear404-confirm": "Ste si istý/á, že si prajete zmazať záznam s chybami 404?", + "clear404-success": "Chyby hlásenia \"404 Nenájdené\" boli vymazané" +} \ No newline at end of file diff --git a/public/language/sk/admin/advanced/events.json b/public/language/sk/admin/advanced/events.json new file mode 100644 index 0000000000..7f68cdf8bf --- /dev/null +++ b/public/language/sk/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Udalosti", + "no-events": "Žiadne nové udalosti", + "control-panel": "Ovládací panel udalostí", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/sk/admin/advanced/logs.json b/public/language/sk/admin/advanced/logs.json new file mode 100644 index 0000000000..1c30f4dca7 --- /dev/null +++ b/public/language/sk/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Záznamy", + "control-panel": "Ovládací panel záznamov", + "reload": "Znovu načítať záznamy", + "clear": "Vyčistiť záznamy", + "clear-success": "Záznamy vyčistené!" +} \ No newline at end of file diff --git a/public/language/sk/admin/appearance/customise.json b/public/language/sk/admin/appearance/customise.json new file mode 100644 index 0000000000..9428f87608 --- /dev/null +++ b/public/language/sk/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Užívateľské CSS/LESS", + "custom-css.description": "Zadajte svoje vlastné definície CSS/LESS, ktoré budú použité nad všetky ostatné štýly.", + "custom-css.enable": "Povoliť užívateľský CSS/LESS", + + "custom-js": "Používateľský Javascript", + "custom-js.description": "Zadajte tu váš javascriptový kód. Bude spustený, akonáhle sa stránka úplne načíta.", + "custom-js.enable": "Povoliť používateľský Javascript", + + "custom-header": "Používateľská hlavička", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Povoliť používateľskú hlavičku", + + "custom-css.livereload": "Povoliť aktuálne znovu načítanie", + "custom-css.livereload.description": "Povolením si vynútite, aby všetky relácie na každom zariadení pod Vaším účtom sa kedykoľvek obnovili pri kliknutí na tlačidlo „Uložiť”." +} \ No newline at end of file diff --git a/public/language/sk/admin/appearance/skins.json b/public/language/sk/admin/appearance/skins.json new file mode 100644 index 0000000000..4e280d9b99 --- /dev/null +++ b/public/language/sk/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Načítať vzhľady...", + "homepage": "Domovska stránka", + "select-skin": "Vybrať vzhľad", + "current-skin": "Aktuálny vzhľad", + "skin-updated": "Vzhľad aktualizovaný", + "applied-success": "%1 vzhľad bol úspešne aplikovaný", + "revert-success": "Farby vzhľadu boli vrátené na základné" +} \ No newline at end of file diff --git a/public/language/sk/admin/appearance/themes.json b/public/language/sk/admin/appearance/themes.json new file mode 100644 index 0000000000..d0554a150e --- /dev/null +++ b/public/language/sk/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Vyhľadávanie nainštalovaných motívov...", + "homepage": "Domovská stránka", + "select-theme": "Vybrať motív", + "current-theme": "Aktuálny motív", + "no-themes": "Žiadne nainštalované motívy neboli nájdené", + "revert-confirm": "Ste si istý/a, že chcete obnoviť predvolený NodeBB motív?", + "theme-changed": "Motív bol zmenený", + "revert-success": "Úspešne sa Vám podarilo obnoviť Váš NodeBB do predvoleného motívu.", + "restart-to-activate": "Pre úplné aktivovanie tejto témy, znovu zostavte a reštartujte NodeBB." +} \ No newline at end of file diff --git a/public/language/sk/admin/dashboard.json b/public/language/sk/admin/dashboard.json new file mode 100644 index 0000000000..b1c6670e81 --- /dev/null +++ b/public/language/sk/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Prevádzka fóra", + "page-views": "Zobrazenia stránok", + "unique-visitors": "Jedineční návštevníci", + "logins": "Logins", + "new-users": "New Users", + "posts": "Príspevky", + "topics": "Témy", + "page-views-seven": "Posledných 7 dní", + "page-views-thirty": "Posledných 30 dní", + "page-views-last-day": "Posledných 24 hodín", + "page-views-custom": "Podľa rozsahu dátumu", + "page-views-custom-start": "Začiatok rozsahu", + "page-views-custom-end": "Koniec rozsahu", + "page-views-custom-help": "Zadajte rozsah obdobia zobrazenia stránok, ktoré chcete vidieť. Ak nie je obdobie nastavené, predvolený formát je YYYY-MM-DD", + "page-views-custom-error": "Zadajte správny rozsah vo formáte YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "Celé obdobie", + + "updates": "Aktualizácie", + "running-version": "Fungujete na NodeBB v%1.", + "keep-updated": "Vždy udržujte NodeBB aktuálne kvôli bezpečnostným záplatám a opravám.", + "up-to-date": "

Máte aktuálnu verziu

", + "upgrade-available": "

Nová verzia (v%1) bola zverejnená. Zvážte aktualizáciu vášho NodeBB.

", + "prerelease-upgrade-available": "

Toto je zastaralá testovacia verzia NodeBB. Nová verzia (v%1) bola zverejnená. Zvážte aktualizáciu vášho NodeBB.

", + "prerelease-warning": "

Toto je skúšobná verzia NodeBB. Môžu sa vyskytnúť rôzne chyby.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Fórum beží vo vývojárskom režime a môže byť potenciálne zraniteľné. Kontaktujte správcu systému.", + "latest-lookup-failed": "

Chyba pri zistení poslednej dostupnej verzie NodeBB

", + + "notices": "Oznámenia", + "restart-not-required": "Reštart nie je potrebný", + "restart-required": "Je potrebný reštart", + "search-plugin-installed": "Vyhľadávací doplnok bol nainštalovaný", + "search-plugin-not-installed": "Vyhľadávací doplnok nebol nainštalovaný", + "search-plugin-tooltip": "Pre aktivácie funkcie vyhľadávania, nainštalujte rozšírenie pre hľadanie zo stránky rozšírení.", + + "control-panel": "Ovládanie systému", + "rebuild-and-restart": "Znovu zostaviť a reštartovať", + "restart": "Reštartovať", + "restart-warning": "Znovu zostavenie alebo reštartovanie NodeBB odpojí všetky existujúce pripojenia na niekoľko sekúnd.", + "restart-disabled": "Znovu zostavenie a reštartovanie vášho NodeBB bolo zablokované, pretože sa nezdá, že ste bol pripojený cez príslušného „daemona”.", + "maintenance-mode": "Režim údržby", + "maintenance-mode-title": "Pre nastavenia režimu údržby NodeBB, kliknite sem", + "realtime-chart-updates": "Aktualizácie grafov v reálnom čase", + + "active-users": "Aktívny užívatelia", + "active-users.users": "Užívatelia", + "active-users.guests": "Hostia", + "active-users.total": "Celkovo", + "active-users.connections": "Pripojenia", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Zaregistrovaný", + + "user-presence": "Výskyt používateľa", + "on-categories": "V zozname kategórií", + "reading-posts": "Čítanie príspevkov", + "browsing-topics": "Prehľadávanie tém", + "recent": "Nedávne", + "unread": "Neprečitané", + + "high-presence-topics": "Témy s vysokou účasťou", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Zobrazenia stránok", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unikátny navštevníci", + "graphs.registered-users": "Zarestrovaný užívatelia", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Posledná obnova od", + "no-users-browsing": "Žiadni používatelia neprehliadajú", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/sk/admin/development/info.json b/public/language/sk/admin/development/info.json new file mode 100644 index 0000000000..62f5cb9863 --- /dev/null +++ b/public/language/sk/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 väzieb odpovedalo počas %2ms.", + "host": "hosť", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "pripojený", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "čas spustenia", + + "registered": "Registrovaný", + "sockets": "Sockety", + "guests": "Hostia", + + "info": "Informácie" +} \ No newline at end of file diff --git a/public/language/sk/admin/development/logger.json b/public/language/sk/admin/development/logger.json new file mode 100644 index 0000000000..a96417643b --- /dev/null +++ b/public/language/sk/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Nastavenia protokolov", + "description": "Povolením zaškrtávacích polí, budete dostávať protokoly na váš terminál. Ak nastavíte cestu, protokoly budú namiesto toho uložené do súboru. Protokolovanie HTTP je vhodné pre vytvorenie štatistiky o tom, kto, kedy a akí ľudia pristupujú k vášmu fóre. Dodatočne k týmto protokolom môžeme zapisovať aj udalosti z socket.io. Protokolovanie socket.io v kombinácii s monitorom redis-cli je vhodné k porozumeniu vnútorným štruktúram NodeBB.", + "explanation": "Jednoducho zaškrtnite/odškrtnite nastavenia protokolu, zmeny sa prejavia okamžite bez reštartovania.", + "enable-http": "Povoliť protokolovanie HTTP", + "enable-socket": "Povoliť protokolovanie socket.io", + "file-path": "Cesta k protokolovému súboru", + "file-path-placeholder": "/path/to/log/file.log ::: zanechajte prázdne pre zaznamenávanie na vašom terminále", + + "control-panel": "Ovládací panel záznamov", + "update-settings": "Aktualizovať nastavenia záznamov" +} \ No newline at end of file diff --git a/public/language/sk/admin/extend/plugins.json b/public/language/sk/admin/extend/plugins.json new file mode 100644 index 0000000000..4546802a4f --- /dev/null +++ b/public/language/sk/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Nainštalované", + "active": "Aktívny", + "inactive": "Nečinný", + "out-of-date": "Zastaralé", + "none-found": "Neboli nájdené žiadne rozšírenia", + "none-active": "Žiadne aktívne rozšírenia", + "find-plugins": "Nájsť rozšírenia", + + "plugin-search": "Hľadať rozšírenia", + "plugin-search-placeholder": "Hľadať rozšírenia...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Roztriediť rozšírenia", + "order-active": "Poradie aktívnych rozšírení", + "dev-interested": "Zaujíma Vás písanie rozšírení pre NodeBB?", + "docs-info": "Plná dokumentácia ohľadom autorizácie rozšírení je k nájdeniu na Portále dokumentov NodeBB.", + + "order.description": "Niektoré rozšírenia fungujú správne až ak sú inicializované pred/po ostatných rozšíreniach.", + "order.explanation": "Rozšírenia sú načítané podľa poradia tu určenom, zhora nadol", + + "plugin-item.themes": "Motívy", + "plugin-item.deactivate": "Deaktivovať", + "plugin-item.activate": "Aktivovať", + "plugin-item.install": "Nainštalovať", + "plugin-item.uninstall": "Odinštalovať", + "plugin-item.settings": "Nastavenia", + "plugin-item.installed": "Nainštalované", + "plugin-item.latest": "Najnovšie", + "plugin-item.upgrade": "Aktualizácia", + "plugin-item.more-info": "Pre viac informácií:", + "plugin-item.unknown": "Neznámi", + "plugin-item.unknown-explanation": "Stav tohto rozšírenia nemohol byť zistený, možno vďaka chybe v konfigurácii.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Doplnky povolené", + "alert.disabled": "Rozšírenia zakázané", + "alert.upgraded": "Rozšírenie bolo aktualizované", + "alert.installed": "Rozšírenie bolo nainštalované", + "alert.uninstalled": "Rozšírenie bolo odinštalované", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Rozšírenie bolo úspešne deaktivované", + "alert.upgrade-success": "Pre úplnú aktualizáciu tohto rozšírenia, znovu zostavte a reštartujte NodeBB.", + "alert.install-success": "Rozšírenie bolo úspešne nainštalované, môžete ho aktivovať.", + "alert.uninstall-success": "Rozšírenie bolo úspešne de-aktivované a odinštalované.", + "alert.suggest-error": "

NodeBB sa nemohol pripojiť k správcovi balíčku, pokračovať v inštalácii poslednej verzie?

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

NodeBB sa nemohol pripojiť k správcovi balíčku, aktualizácia nie je odporúčaná.

", + "alert.incompatible": "

Vaša verzia NodeBB (v%1) umožňuje iba aktualizovať toto rozšírenie na v%2. Aktualizujte prosím NodeBB, ak chcete nainštalovať najnovšiu verziu tohto rozšírenia.

", + "alert.possibly-incompatible": "

Nebola nájdená žiadna informácia o kompatibilite

Toto rozšírenie nemá nastavenú požadovanú verziu NodeBB. Plná kompatibilita nemôže byť garantovaná a môže spôsobiť, že sa Vám už NodeBB nespustí.

Nespustí ak sa správne NodeBB:

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

Pokračovať v inštalácii tejto aktuálnej verzie rozšírenie?

", + "alert.reorder": "Rozšírenia boli zoradené", + "alert.reorder-success": "Pre úplne dokončenie úkonu, prosím znovu zostavte a reštartuje Váš NodeBB.", + + "license.title": "Licenčná informácie o rozšírení", + "license.intro": "Rozšírenie %1 je licencované pod %2. Pre aktivovanie tohto rozšírenia si prečítajte licenčné podmienky.", + "license.cta": "Želáte si pokračovať v aktivovaní tohto rozšírenia?" +} diff --git a/public/language/sk/admin/extend/rewards.json b/public/language/sk/admin/extend/rewards.json new file mode 100644 index 0000000000..87a475ac61 --- /dev/null +++ b/public/language/sk/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Odmeny", + "condition-if-users": "Ak je používateľ", + "condition-is": "Je:", + "condition-then": "Potom:", + "max-claims": "Počet dosiahnuteľnosti odmeny", + "zero-infinite": "Pre neobmedzené zadajte 0", + "delete": "Odstrániť", + "enable": "Povoliť", + "disable": "Zakázať", + + "alert.delete-success": "Odmena bola úspešne vymazaná", + "alert.no-inputs-found": "Nepovolená odmena - nebol nájdený žiadny záznam.", + "alert.save-success": "Odmeny boli úspešne uložené" +} \ No newline at end of file diff --git a/public/language/sk/admin/extend/widgets.json b/public/language/sk/admin/extend/widgets.json new file mode 100644 index 0000000000..f72125f64d --- /dev/null +++ b/public/language/sk/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Dostupné miniaplikácie", + "explanation": "Vyberte si miniaplikáciu z rozbalovacej ponuky a pretiahnite ju do oblasti šablóny miniaplikácie naľavo.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Okopírovať miniaplikácie z", + "containers.available": "Dostupné moduly", + "containers.explanation": "Presuňte na akúkoľvek aktívnu miniaplikáciu", + "containers.none": "Nič", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Hlavička panela", + "container.panel-body": "Telo panela", + "container.alert": "Upozornenie", + + "alert.confirm-delete": "Ste si istý že chcete zmazať túto miniaplikáciu?", + "alert.updated": "Miniaplikácie boli aktualizované", + "alert.update-success": "Miniaplikácie boli úspešne aktualizované", + "alert.clone-success": "Úspešne naklonované miniaplikácie", + + "error.select-clone": "Vyberte prosím stránku, z ktorej chcete klonovať", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/admins-mods.json b/public/language/sk/admin/manage/admins-mods.json new file mode 100644 index 0000000000..889def7458 --- /dev/null +++ b/public/language/sk/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Správcovia", + "global-moderators": "Hlavný moderátori", + "moderators": "Moderators", + "no-global-moderators": "Žiadny hlavný moderátori", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Žiadny moderátori", + "add-administrator": "Pridať správcu", + "add-global-moderator": "Pridať hlavného moderátora", + "add-moderator": "Pridať moderátora" +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/categories.json b/public/language/sk/admin/manage/categories.json new file mode 100644 index 0000000000..907b0962af --- /dev/null +++ b/public/language/sk/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Nastavenia kategórie", + "privileges": "Oprávnenia", + + "name": "Názov kategórie", + "description": "Popis kategórie", + "bg-color": "Farba pozadia", + "text-color": "Farba textu", + "bg-image-size": "Veľkosť obrázku na pozadí", + "custom-class": "Upraviť triedu", + "num-recent-replies": "# posledných odpovedí", + "ext-link": "Externý odkaz", + "subcategories-per-page": "Subcategories per page", + "is-section": "Zaobchádzať s kategóriou ako so sekciou", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Nahrať obrázok", + "delete-image": "Odobrať", + "category-image": "Obrázok kategórie", + "parent-category": "Nadriadená kategória", + "optional-parent-category": "Nadriadená kategória (odporúčané)", + "top-level": "Top Level", + "parent-category-none": "(nič)", + "copy-parent": "Copy Parent", + "copy-settings": "Kopírovať nastavenia z", + "optional-clone-settings": "Klonovať nastavenia z kategórie (odporúčané)", + "clone-children": "Duplikovať podružné kategórie a nastavenia", + "purge": "Vyčistiť kategóriu", + + "enable": "Povoliť", + "disable": "Zakázať", + "edit": "Upraviť", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Vyberte kategóriu", + "set-parent-category": "Nastaviť nadradenú kategóriu", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Poznámka: nastavenie oprávnení má okamžitý vplyv. Nie je teda nutné uložiť kategóriu pre upravenie týchto nastavení", + "privileges.section-viewing": "Oprávnenie prehliadania", + "privileges.section-posting": "Oprávnenie príspevkov", + "privileges.section-moderation": "Oprávnenie moderovania", + "privileges.section-other": "Other", + "privileges.section-user": "Používateľ", + "privileges.search-user": "Pridať používateľa", + "privileges.no-users": "V tejto kategórií nie je nastavené žiadne oprávnenie používateľa.", + "privileges.section-group": "Skupina", + "privileges.group-private": "Táto skupina je súkromná", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Pridať skupinu", + "privileges.copy-to-children": "Kopírovať do podradených", + "privileges.copy-from-category": "Kopírovať z kategórie", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "Ak má skupina registrovaných používateľov určité oprávnenia, ostatné skupiny budú mať rovnaké oprávnenia, aj keď nie sú výslovne definované/zaškrtnuté. Tieto zdedené oprávnenia Vám sú zobrazené, lebo všetci používatelia sú súčasťou skupiny registrovaných používateľov. Takže oprávnenia pre ďalšie skupiny nemusia byť dodatočne nastavované.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Späť do zoznamu kategórií", + "analytics.title": "Analýza pre kategóriu „%1“", + "analytics.pageviews-hourly": "Postava 1 - zobrazenie stránky za hodinu pre túto kategóriu", + "analytics.pageviews-daily": "Postava 2 - zobrazenie stránky za deň pre túto kategóriu", + "analytics.topics-daily": "Postava 3 - vytvorených tém za deň pre túto kategóriu", + "analytics.posts-daily": "Postava 4 – vytvorených príspevkov za deň pre túto kategóriu", + + "alert.created": "Vytvorené", + "alert.create-success": "Kategória bola úspešne vytvorená.", + "alert.none-active": "Nemáte žiadne aktívne kategórie.", + "alert.create": "Vytvoriť kategóriu", + "alert.confirm-purge": "

Naozaj chcete vyčistiť túto kategóriu „%1“?

Upozornenie! Všetky témy a príspevky v tejto kategórií budu odstránené!

Vyčistenie kategórií odstráni všetky témy a príspevky a odstráni kategórie z databázy. Pokiaľ chcete vyčistiť kategórie dočasne. radšej namiesto toho kategóriu „zakážte“.

", + "alert.purge-success": "Kategória bola vyčistená!", + "alert.copy-success": "Nastavenia boli skopírované!", + "alert.set-parent-category": "Nastaviť nadradenú kategóriu", + "alert.updated": "Kategórie boli aktuilizované", + "alert.updated-success": "ID kategórie %1 bolo aktualizované.", + "alert.upload-image": "Nahrať obrázok kategórie", + "alert.find-user": "Nájsť používateľa", + "alert.user-search": "Nájsť používateľa...", + "alert.find-group": "Nájsť skupinu", + "alert.group-search": "Hľadať skupinu...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Zbaliť všetko", + "expand-all": "Rozbaliť všetko", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/digest.json b/public/language/sk/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/sk/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/sk/admin/manage/groups.json b/public/language/sk/admin/manage/groups.json new file mode 100644 index 0000000000..7f36a498b1 --- /dev/null +++ b/public/language/sk/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Názov skupiny", + "badge": "Badge", + "properties": "Properties", + "description": "Popis skupiny", + "member-count": "Počet členov", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Upraviť", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Hľadať", + "create": "Vytvoriť skupinu", + "description-placeholder": "Krátky popis skupiny", + "create-button": "Vytvoriť", + + "alerts.create-failure": "Ale, ale

Objavil sa problém s vytvorením skupiny. Skúste to neskôr.

", + "alerts.confirm-delete": "Ste si istý, že chcete odstrániť túto skupinu?", + + "edit.name": "Meno", + "edit.description": "Popis", + "edit.user-title": "Názov členov", + "edit.icon": "Ikona skupín", + "edit.label-color": "Farba popisu skupiny", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Zobraziť odznak", + "edit.private-details": "Ak je povolené, pripojenie k skupine vyžaduje schválenie od vlastníka skupiny.", + "edit.private-override": "Upozornenie: súkromné ​​skupiny sú zakázané na systémovej úrovni, ktorej táto možnosť zruší platnosť.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Skryť", + "edit.hidden-details": "Ak je povolené, táto skupina nebude zobrazená na zozname skupín a používatelia musia byť manuálne pozývaný", + "edit.add-user": "Pridať používateľa do skupiny", + "edit.add-user-search": "Hľadať používateľov", + "edit.members": "Zoznam členov", + "control-panel": "Ovládací panel skupín", + "revert": "Späť", + + "edit.no-users-found": "Nebol nájdený žiadny používateľ", + "edit.confirm-remove-user": "Ste si istý, že chcete odstrániť tohto používateľa?", + "edit.save-success": "Zmeny boli uložené!" +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/privileges.json b/public/language/sk/admin/manage/privileges.json new file mode 100644 index 0000000000..df6a6ad14e --- /dev/null +++ b/public/language/sk/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Verejný", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Konverzácia", + "upload-images": "Nahrať obrázky", + "upload-files": "Nahrať súbory", + "signature": "Podpis", + "ban": "Zablokovať", + "mute": "Mute", + "invite": "Invite", + "search-content": "Vyhľadať obsah", + "search-users": "Vyhľadať používateľov", + "search-tags": "Vyhľadať značky", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Nájsť kategóriu", + "access-category": "Prístup ku kategórií", + "access-topics": "Prístup k témam", + "create-topics": "Vytvoriť témy", + "reply-to-topics": "Odpovedať na témy", + "schedule-topics": "Schedule Topics", + "tag-topics": "Značka tém", + "edit-posts": "Upraviť príspevky", + "view-edit-history": "Zobraziť históriu úprav", + "delete-posts": "Odstrániť príspevky", + "view_deleted": "Zobraziť odstránené príspevky", + "upvote-posts": "Súhlasné príspevky", + "downvote-posts": "Nesúhlasné príspevky", + "delete-topics": "Odstrániť témy", + "purge": "Vyčistiť", + "moderate": "Moderovať", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/registration.json b/public/language/sk/admin/manage/registration.json new file mode 100644 index 0000000000..006cd44f12 --- /dev/null +++ b/public/language/sk/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Fronta", + "description": "V registračnej fronte nie sú žiadny používatelia.
Pre povolenie tejto funkcie, prejdite do ponuky Nastavení → Používateľ → a nastavte Typ registrácie na \"Schválené správcom\".", + + "list.name": "Meno", + "list.email": "E-mail", + "list.ip": "IP adresa", + "list.time": "Čas", + "list.username-spam": "Frekvencia: %1 Zdá sa: %2 Dôveryhodnosť: %3", + "list.email-spam": "Frekvencia: %1 zdá sa: %2", + "list.ip-spam": "Frekvencia: %1 zdá sa: %2", + + "invitations": "Pozvánky", + "invitations.description": "Nižšie je kompletný zoznam odoslaných pozvánok. Pre hľadanie v zozname pomocou e-mailu alebo mena používateľa, použite kláves Ctrl + F.

Pri používateľov, ktorí využili pozvanie, bude používateľské meno zobrazené napravo od e-mailov.", + "invitations.inviter-username": "Používateľské meno pozvaného", + "invitations.invitee-email": "E-mail pozvaného", + "invitations.invitee-username": "Používateľské meno pozvaného (ak je registrovaný)", + + "invitations.confirm-delete": "Ste si istý, že chcete odstrániť túto pozvánku?" +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/tags.json b/public/language/sk/admin/manage/tags.json new file mode 100644 index 0000000000..2014aeebcd --- /dev/null +++ b/public/language/sk/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Vaše fórum zatiaľ neobsahuje žiadne témy.", + "bg-color": "Farba pozadia", + "text-color": "Farba textu", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Vytvoriť značku", + "modify": "upraviť značky", + "rename": "Premenovať značky", + "delete": "Odstrániť vybraté značky", + "search": "Hľadanie značky...", + "settings": "Tags Settings", + "name": "Názov značky", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Chcete odstrániť vybranú značku?", + "alerts.update-success": "Značka bola aktualizovaná!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/uploads.json b/public/language/sk/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/sk/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/sk/admin/manage/users.json b/public/language/sk/admin/manage/users.json new file mode 100644 index 0000000000..edeef2865a --- /dev/null +++ b/public/language/sk/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Používatelia", + "edit": "Actions", + "make-admin": "Urobiť správcom", + "remove-admin": "Odobrať správcu", + "validate-email": "Overiť e-mail", + "send-validation-email": "Poslať overovací e-mail", + "password-reset-email": "Poslať e-mail k obnove hesla", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Zablokovať používateľa(ov)", + "temp-ban": "Dočasne zablokovať používateľa(ov)", + "unban": "Zrušiť zákaz používateľa", + "reset-lockout": "Obnoviť uzamknutie", + "reset-flags": "Obnoviť označenia", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Stiahnuť ako CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "Nový používateľ", + "filter-by": "Filter by", + "pills.unvalidated": "Neoverené", + "pills.validated": "Validated", + "pills.banned": "Zablokovaný", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "Podľa ID používateľa", + "search.uid-placeholder": "Pre hľadanie, zadajte ID používateľa", + "search.username": "Podľa mena používateľa", + "search.username-placeholder": "Zadajte hľadané používateľské meno", + "search.email": "Podľa e-mailu", + "search.email-placeholder": "Zadajte hľadaný e-mail", + "search.ip": "Podľa IP adresy", + "search.ip-placeholder": "Zadajte hľadanú IP adresu", + "search.not-found": "Užívateľ nebol nájdený!", + + "inactive.3-months": "3 mesiace", + "inactive.6-months": "6 mesiacov", + "inactive.12-months": "12 mesiacov", + + "users.uid": "uid", + "users.username": "používateľské meno", + "users.email": "e-mail", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "počet príspevkov", + "users.reputation": "reputácia", + "users.flags": "príznaky", + "users.joined": "pripojil", + "users.last-online": "posledné prihlásenie", + "users.banned": "zablokovaný", + + "create.username": "Používateľské meno", + "create.email": "E-mail", + "create.email-placeholder": "E-mail od tohto používateľa", + "create.password": "Heslo", + "create.password-confirm": "Potvrdiť heslo", + + "temp-ban.length": "Length", + "temp-ban.reason": "Dôvod (voliteľné)", + "temp-ban.hours": "Hodiny", + "temp-ban.days": "Dni", + "temp-ban.explanation": "Zadajte dĺžku trvania pre zákaz. Nezabudnite, že 0 je považovaná ako trvalý zákaz.", + + "alerts.confirm-ban": "Naozaj chcete trvalo zablokovať tohto používateľa?", + "alerts.confirm-ban-multi": "Naozaj chcete trvalo zablokovať týchto používateľov? ", + "alerts.ban-success": "Používateľ bol zablokovaný!", + "alerts.button-ban-x": "Zakázať %1 používateľa.", + "alerts.unban-success": "Zablokovanie používateľa bolo zrušené!", + "alerts.lockout-reset-success": "Uzamknutie bolo obnovené!", + "alerts.flag-reset-success": "Príznak(y) boli obnovené!", + "alerts.no-remove-yourself-admin": "Seba samého ako správcu nemôžete odstrániť!", + "alerts.make-admin-success": "Používateľ je odteraz správcom", + "alerts.confirm-remove-admin": "Naozaj chcete odstrániť tohto správcu?", + "alerts.remove-admin-success": "Používateľ už nie je správcom", + "alerts.make-global-mod-success": "Používateľ je odteraz hlavným moderátorom", + "alerts.confirm-remove-global-mod": "Naozaj chcete odstrániť tohto hlavného moderátora?", + "alerts.remove-global-mod-success": "Používateľ už nie je hlavným moderátorom.", + "alerts.make-moderator-success": "Užívateľ je odteraz globálnym moderátorom.", + "alerts.confirm-remove-moderator": "Naozaj chcete odstrániť tohto moderátora?", + "alerts.remove-moderator-success": "Používateľ už nie je moderátorom.", + "alerts.confirm-validate-email": "Chcete schváliť e-mailové adresy týchto používateľov?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "E-maily boli overené", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Chcete odoslať týmto používateľom e-mail pre obnovu hesla?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "Používateľ bol odstránený!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Vytvoriť používateľa", + "alerts.button-create": "Vytvoriť", + "alerts.button-cancel": "Zrušiť", + "alerts.error-passwords-different": "Hesla musia byť zhodné!", + "alerts.error-x": "Chyba

%1

", + "alerts.create-success": "Používateľ bol vytvorený!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "E-mail s pozvánkou bol odoslaný na %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/sk/admin/menu.json b/public/language/sk/admin/menu.json new file mode 100644 index 0000000000..7c42194fa7 --- /dev/null +++ b/public/language/sk/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Všeobecné", + + "section-manage": "Spravovať", + "manage/categories": "Kategórie", + "manage/privileges": "Oprávnenia", + "manage/tags": "Značky", + "manage/users": "Používatelia", + "manage/admins-mods": "Správcovia a moderátori", + "manage/registration": "Registračná fronta", + "manage/post-queue": "Fronta príspevkov", + "manage/groups": "Skupiny", + "manage/ip-blacklist": "Čierny zoznam IP adries", + "manage/uploads": "Nahrané", + "manage/digest": "Digests", + + "section-settings": "Nastavenia", + "settings/general": "Všeobecné", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "E-mail", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Hostia", + "settings/uploads": "Nahrané", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Stránkovanie", + "settings/tags": "Značky", + "settings/notifications": "Oznámenia", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Coockie", + "settings/web-crawler": "Webový prehliadač", + "settings/sockets": "Sockety", + "settings/advanced": "Pokročilé", + + "settings.page-title": "Nastavenia %1", + + "section-appearance": "Vzhľad", + "appearance/themes": "Motívy", + "appearance/skins": "Vzhľady", + "appearance/customise": "Používateľský obsah (HTML/JS/CSS)", + + "section-extend": "Rozšíriť", + "extend/plugins": "Rozšírenia", + "extend/widgets": "Miniaplikácie", + "extend/rewards": "Odmeny", + + "section-social-auth": "Sociálna autentifikácia", + + "section-plugins": "Prídavné moduly", + "extend/plugins.install": "Nainštalovať zásuvný modul", + + "section-advanced": "Pokročilé", + "advanced/database": "Databáza", + "advanced/events": "Udalosti", + "advanced/hooks": "Hooks", + "advanced/logs": "Protokoly", + "advanced/errors": "Chyby", + "advanced/cache": "Medzipamäť", + "development/logger": "Protokolár", + "development/info": "Informácie", + + "rebuild-and-restart-forum": "Znovu zostaviť a reštartovať fórum", + "restart-forum": "Reštartovať fórum", + "logout": "Odhlásiť", + "view-forum": "Zobraziť fórum", + + "search.placeholder": "Search settings", + "search.no-results": "Žiadne výsledky...", + "search.search-forum": "Prehľadať fórum pre ", + "search.keep-typing": "Píšte viac pre zobrazenie výsledkov...", + "search.start-typing": "Začnite písať pre zobrazenie výsledkov...", + + "connection-lost": "Pripojenie k %1 bolo stratené, pokus o opätovné pripojenie...", + + "alerts.version": "Spustené NodeBB v%1", + "alerts.upgrade": "Aktualizovať na v%1" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/advanced.json b/public/language/sk/admin/settings/advanced.json new file mode 100644 index 0000000000..a464b384da --- /dev/null +++ b/public/language/sk/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Režim údržby", + "maintenance-mode.help": "Ak je fórum v režime údržby, všetky požiadavky budú presmerované na statickú stránku. Administrátori sú vylúčení z tohto presmerovania a majú prístup na stránku normálne.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Správa údržby", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Hlavičky", + "headers.allow-from": "Nastavte ALLOW-FROM pro umístění NodeBB do iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Upravte hlavičku „Powered by” odosielanou NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "Ak chcete zamietnuť prístup na všetky stránky, nechajte prázdne", + "headers.acao-regex-help": "Sem zadajte regulárne výrazy, ktoré zodpovedajú dynamickým originálom. Pre zakázanie všetkých stránok, ponechajte prázdne.", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Prísne zabezpečenie prenosu", + "hsts.enabled": "Povoliť HSTS (odporúčané)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Zahrnúť pod domény v hlavičke HSTS", + "hsts.preload": "Povoliť pred načítavanie hlavičky HSTS", + "hsts.help": "Ak je povolené, bude nastavená pre tieto stránky hlavička HSTS. V hlavičke si môžete zvoliť aj zahrnutie pod domén a prednastavených príznakov. Ak si nieste istý, nechajte nezaškrtnuté Viac informácií ", + "traffic-management": "Správa prevádzky", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Povoliť správu prevádzky", + "traffic.event-lag": "Hranice oneskorenia slučky udalosti (v milisekundách)", + "traffic.event-lag-help": "Zníženie tejto hodnoty zníži čas pre načítanie stránky, ale taktiež zobrazí viac používateľom správu o „preťažení stránok”. (je vyžadovaný reštart)", + "traffic.lag-check-interval": "Kontrola intervalov (v milisekundách)", + "traffic.lag-check-interval-help": "Zníženie tejto hodnoty spôsobí, že NodeBB bude citlivejšie na zaťaženie načítania stránok a na kontrolu tohto zaťaženia. (je vyžadovaný reštart)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/api.json b/public/language/sk/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/sk/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/chat.json b/public/language/sk/admin/settings/chat.json new file mode 100644 index 0000000000..d6ccdb4f2d --- /dev/null +++ b/public/language/sk/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Nastavenia konverzácie", + "disable": "Zakázať konverzáciu", + "disable-editing": "Zakázať upravenie/odstránenie konverzačnej správy", + "disable-editing-help": "Správcovia a globálny moderátori sú vyňatí z tohto obmedzenia", + "max-length": "Maximálna dĺžka konverzačnej správy", + "max-room-size": "Maximálny počet používateľov v konverzačnej miestnosti", + "delay": "Čas medzi konverzačnými správami v milisekundách", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/cookies.json b/public/language/sk/admin/settings/cookies.json new file mode 100644 index 0000000000..da4d0ad7a3 --- /dev/null +++ b/public/language/sk/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Súhlas EÚ", + "consent.enabled": "Povoliť", + "consent.message": "Správa o oznámení", + "consent.acceptance": "Správa o prijatí", + "consent.link-text": "Odkaz na text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Pre použitie predvoleného textu NodeBB, nechajte prázdne", + "settings": "Nastavenia", + "cookie-domain": "Doména relácie cookie", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Pre predvolené, zanechajte prázdne" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/email.json b/public/language/sk/admin/settings/email.json new file mode 100644 index 0000000000..f8d64a3462 --- /dev/null +++ b/public/language/sk/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Nastavenia e-mailu", + "address": "E-mailové adresy", + "address-help": "Nasledujúce e-mailové adresy budú zobrazené príjemcovi v políčkach 'Od' a 'Odpovedať'.", + "from": "Meno - od", + "from-help": "Zobrazené meno v e-maily v - Od", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "Prenos SMTP", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Zo zoznamu môžete vybrať známe služby alebo zadať vlastné.", + "smtp-transport.service": "Vyberte službu", + "smtp-transport.service-custom": "Používateľská služba", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "Hostiteľ SMTP", + "smtp-transport.port": "Port SMTP", + "smtp-transport.security": "Zabezpečenie pripojenia", + "smtp-transport.security-encrypted": "Šifrované", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Používateľské meno", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Heslo", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Upraviť šablónu e-mailu", + "template.select": "Vybrať šablónu e-mailu", + "template.revert": "Späť k pôvodnému", + "testing": "Skúška e-mailu", + "testing.select": "Vyberte šablónu e-mailu", + "testing.send": "Odoslať skúšobný e-mail", + "testing.send-help": "Skúšobný e-mail bude odoslaný aktuálne prihlásenému používateľovi na jeho e-mailovú adresu z registrácie.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/sk/admin/settings/general.json b/public/language/sk/admin/settings/general.json new file mode 100644 index 0000000000..697f33a976 --- /dev/null +++ b/public/language/sk/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Nastavenia stránky", + "title": "Názov stránky", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "URL názov stránky", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Názov vašej komunity", + "title.show-in-header": "Zobraziť názov stránky v hlavičke", + "browser-title": "Názov prehliadača", + "browser-title-help": "Ak nebude určený názov prehliadača, bude použitý názov stránky", + "title-layout": "Vzhľad názvu", + "title-layout-help": "Určite, ako má byť zostavený názov prehliadača, tj. {pageTitle} | {browserTitle}", + "description.placeholder": "Skrátený popis Vašej komunity", + "description": "Popis stránky", + "keywords": "Kľúčové slová pre stránky", + "keywords-placeholder": "Kľúčové slová popisujúce Vašu komunitu, oddelené čiarkou", + "logo": "Logo stránky", + "logo.image": "Obrázok", + "logo.image-placeholder": "Cesta k logu, aby mohlo byť zobrazené v hlavičke fóra", + "logo.upload": "Nahrať", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "URL logo stránky", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Opisujúci text", + "log.alt-text-placeholder": "Alternatívny text pre prístupnosť", + "favicon": "Ikona (favicon)", + "favicon.upload": "Nahrať", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Nahrať", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Odchádzajúce odkazy", + "outgoing-links.warning-page": "Použiť stránku s upozornením pri odchádzajúcich odkazoch", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domény u ktorých bude preskočená upozorňovacia stránka", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/sk/admin/settings/group.json b/public/language/sk/admin/settings/group.json new file mode 100644 index 0000000000..438d6a73c6 --- /dev/null +++ b/public/language/sk/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Všeobecné", + "private-groups": "Súkromné ​​skupiny", + "private-groups.help": "Ak je povolené, pripojenie k skupine vyžaduje schválenie zakladateľa skupiny (Predvolené: povolené)", + "private-groups.warning": "Ale pozor, ak je táto možnosť zakázaná a vy máte súkromné ​​skupiny, stanú sa automaticky verejnými.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "Toto označenie môže byť použité, aby používatelia mohli vybrať niekoľko skupinových symbolov, vyžaduj podporu motívov.", + "max-name-length": "Maximálna dĺžka názvu skupiny", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Obrázok skupiny", + "default-cover": "Predvolený obrázok", + "default-cover-help": "Pre skupiny, ktoré nemajú nahraný obrázok, pridajte predvolené obrázky oddelené čiarkami" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/guest.json b/public/language/sk/admin/settings/guest.json new file mode 100644 index 0000000000..760cd59c8b --- /dev/null +++ b/public/language/sk/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Povoliť upravovanie zaobchádzania s hosťami", + "handles.enabled-help": "Táto možnosť odkryje nové pole, ktoré umožňuje hosťom vybrať meno, ktoré sa pripojí ku každému príspevku, ktorý vytvorí. Ak bude zakázané, budú jednoducho nazývaní 'Hosť'", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/homepage.json b/public/language/sk/admin/settings/homepage.json new file mode 100644 index 0000000000..08e12e04ca --- /dev/null +++ b/public/language/sk/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Domovská stránka", + "description": "Vyberte, akú stránku sa zobrazí, keď sa používatelia dostanú do koreňovej adresy URL vášho fóra.", + "home-page-route": "Cesta k domovskej stránke", + "custom-route": "Upraviť cestu", + "allow-user-home-pages": "Povoliť používateľom domovské stránky", + "home-page-title": "Titulok domovskej stránky (Predvolený „Domov”)" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/languages.json b/public/language/sk/admin/settings/languages.json new file mode 100644 index 0000000000..96072b9642 --- /dev/null +++ b/public/language/sk/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Jazykové nastavenia", + "description": "Predvolený jazyk určuje nastavenie jazyka pre všetkých používateľov navštevujúcich vaše fórum.
Každý používateľ si môže potom nastaviť predvolený jazyk na stránke nastavenia účtu.", + "default-language": "Predvolený jazyk", + "auto-detect": "Automaticky rozpoznávať nastavenie jazyka pre hostí" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/navigation.json b/public/language/sk/admin/settings/navigation.json new file mode 100644 index 0000000000..59aef4df0b --- /dev/null +++ b/public/language/sk/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ikona:", + "change-icon": "zmeniť", + "route": "Cesta:", + "tooltip": "Tip:", + "text": "Text:", + "text-class": "Textová trieda: doporučené", + "class": "Class: optional", + "id": "ID: doporučené", + + "properties": "Vlastnosti:", + "groups": "Groups:", + "open-new-window": "Otvoriť v novom okne", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Odstrániť", + "btn.disable": "Zakázať", + "btn.enable": "Povoliť", + + "available-menu-items": "Dostupné položky ponuky", + "custom-route": "Upraviť cestu", + "core": "jadro", + "plugin": "zásuvný modul" +} diff --git a/public/language/sk/admin/settings/notifications.json b/public/language/sk/admin/settings/notifications.json new file mode 100644 index 0000000000..a873f10b02 --- /dev/null +++ b/public/language/sk/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Oznámenia", + "welcome-notification": "Uvítacie oznámenie", + "welcome-notification-link": "Odkaz na uvítanie", + "welcome-notification-uid": "Uvítanie používateľa (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/pagination.json b/public/language/sk/admin/settings/pagination.json new file mode 100644 index 0000000000..2acf61b1d7 --- /dev/null +++ b/public/language/sk/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Nastavenia stránkovania", + "enable": "Stránkovať témy a príspevky namiesto použitia nekonečného posúvania.", + "posts": "Post Pagination", + "topics": "Stránkovanie tém", + "posts-per-page": "Príspevkov na stránku", + "max-posts-per-page": "Maximálne množstvo príspevkov na stránku", + "categories": "Stránkovanie kategórií", + "topics-per-page": "Tém na stránku", + "max-topics-per-page": "Maximálne množstvo tém na stránku", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/post.json b/public/language/sk/admin/settings/post.json new file mode 100644 index 0000000000..39d29ac1eb --- /dev/null +++ b/public/language/sk/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Zoraďovanie príspevkov", + "sorting.post-default": "Predvolené triedenie príspevkov", + "sorting.oldest-to-newest": "Od najstarších po najnovšie", + "sorting.newest-to-oldest": "Od najnovších po najstaršie", + "sorting.most-votes": "Podľa počtu hlasov", + "sorting.most-posts": "Podľa počtu príspevkov", + "sorting.topic-default": "Predvolené zoradenie tém", + "length": "Dĺžka príspevku", + "post-queue": "Post Queue", + "restrictions": "Obmedzenie príspevkov", + "restrictions-new": "Obmedzenia nového používateľa", + "restrictions.post-queue": "Povoliť frontu pre príspevky", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Povoliť obmedzenie nových používateľov", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Sekúnd medzi príspevky pre nových používateľov", + "restrictions.rep-threshold": "Ohraničenie reputácie pred zrušením týchto obmedzení", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Maximálna dĺžka názvu", + "restrictions.max-title-length": "Maximálna dĺžka názvu", + "restrictions.min-post-length": "Minimálna dĺžka príspevku", + "restrictions.max-post-length": "Maximálna dĺžka príspevku", + "restrictions.days-until-stale": "Počet dní, než je téma považovaná za neaktuálnu", + "restrictions.stale-help": "Ak je téma považovaná za „staré” používateľovi sa zobrazí oznámenie pri pokuse o pridanie odpovede.", + "timestamp": "Časový odtlačok", + "timestamp.cut-off": "Dátum ukončenia (v dňoch)", + "timestamp.cut-off-help": "Dátum a čas bude zobrazený relatívne (t.j. „pred 3 hodinami“ / „pred 5 dňami“), a podľa tohto lokalizovaný do rôznych\n\t\t\t\t\tjazykov. Za určitých okolností, môže byť tento text prepnutý na lokalizovaný dátum\n\t\t\t\t\t(t.j. 5 Nov 2017 15:30).
(predvolené: 30, alebo mesiac). Nastavte na 0, pre zobrazenie dátumov, ak ponecháte prázdne, bude vždy zobrazený relatívny čas.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Ukážka príspevku", + "teaser.last-post": "Posledný - zobrazenie posledného príspevku, vrátane hlavného príspevku, ak nie sú odpovede", + "teaser.last-reply": "Posledný - zobrazenie poslednej odpovede, alebo ak nie sú žiadne odpovede textu „Bez odpovede”", + "teaser.first": "Prvý", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Nastavenia neprečítaných", + "unread.cutoff": "Dni ukončenia neprečítaných", + "unread.min-track-last": "Minimálny počet príspevkov v téme pred posledným prečítaním", + "recent": "Nastavenia pre posledné", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Zakázať filtrovanie tém v ignorovaných kategóriach na poslednej stránke", + "signature": "Nastavenia podpisu", + "signature.disable": "Zakázať podpisy", + "signature.no-links": "Zakázať odkazy v podpisoch", + "signature.no-images": "Zakázať obrázky v podpisoch", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximálna dĺžka podpisu", + "composer": "Nastavenia kompozície", + "composer-help": "Nasledujúce nastavenia kontroluje funkčnosť a/alebo vzhľad zobrazených príspevkov\n\t\t\t\tpre používateľov, ktorí vytvoria novú tému alebo odpovedajú na existujúcu tému.", + "composer.show-help": "Zobraziť záložku „Nápoveda”", + "composer.enable-plugin-help": "Povoliť zásuvné moduly pre pridanie obsahu do záložky nápovedy", + "composer.custom-help": "Používateľský text nápovedy", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Sledovanie IP adresy", + "ip-tracking.each-post": "Sledovať IP adresu pri každom príspevku", + "enable-post-history": "Povoliť históriu príspevkov" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/reputation.json b/public/language/sk/admin/settings/reputation.json new file mode 100644 index 0000000000..e6e298291b --- /dev/null +++ b/public/language/sk/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Nastavenie reputácie", + "disable": "Zakázať systém reputácie", + "disable-down-voting": "Zakázať hlasovanie", + "votes-are-public": "Všetky hlasovania sú verejné", + "thresholds": "Obmedzenie aktivity", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimálna reputácia k vyjadreniu nesúhlasu s príspevkom ", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimálna reputácia pre označenie príspevku", + "min-rep-website": "Minimálna reputácia pre pridanie „Webovej stránky” do používateľského profilu", + "min-rep-aboutme": "Minimálna reputácia pre pridanie „O mne” do používateľského profilu", + "min-rep-signature": "Minimálna reputácia pre pridanie „Podpisu” do používateľského profilu", + "min-rep-profile-picture": "Minimálna reputácia pre pridanie „Profilového obrázka” do používateľského profilu ", + "min-rep-cover-picture": "Minimálna reputácia pre pridanie „Titulného obrázka”  do používateľského profilu", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/social.json b/public/language/sk/admin/settings/social.json new file mode 100644 index 0000000000..0b19aa798a --- /dev/null +++ b/public/language/sk/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Zdieľanie príspevku", + "info-plugins-additional": "Doplnky môžu pridávať ďalšie siete na zdieľanie príspevkov.", + "save-success": "Úspešne uložené siete zdieľajúce príspevky." +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/sockets.json b/public/language/sk/admin/settings/sockets.json new file mode 100644 index 0000000000..aeb8ef2809 --- /dev/null +++ b/public/language/sk/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Nastavenia opätovného pripojenia", + "max-attempts": "Maximálny počet pokusov o znovu pripojenie", + "default-placeholder": "Predvolené: %1", + "delay": "Časové oneskorenie pre znovu pripojenie" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/sounds.json b/public/language/sk/admin/settings/sounds.json new file mode 100644 index 0000000000..c408efe93f --- /dev/null +++ b/public/language/sk/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Oznámenia", + "chat-messages": "Správy konverzácie", + "play-sound": "Prehrať", + "incoming-message": "Prichádzajúca správa", + "outgoing-message": "Odchádzajúca správa", + "upload-new-sound": "Nahrať novú zvuk", + "saved": "Nastavenie bolo uložené" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/tags.json b/public/language/sk/admin/settings/tags.json new file mode 100644 index 0000000000..77dc6b53a4 --- /dev/null +++ b/public/language/sk/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Nastavenie značky", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimálny počet značiek pre jednotlivé témy", + "max-per-topic": "Maximálny počet značiek na tému", + "min-length": "Minimálna dĺžka značky", + "max-length": "Maximálna dĺžka značky", + "related-topics": "Súvisiace témy", + "max-related-topics": "Maximálny počet zobrazených súvisiacich tém (ak je podporované motívom)" +} \ No newline at end of file diff --git a/public/language/sk/admin/settings/uploads.json b/public/language/sk/admin/settings/uploads.json new file mode 100644 index 0000000000..5a80e5f00d --- /dev/null +++ b/public/language/sk/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Príspevky", + "orphans": "Orphaned Files", + "private": "Nahrané súbory sú súkromné", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "Prípona súborov je súkromná", + "private-uploads-extensions-help": "Pre nastavenie súkromia, zadajte sem zoznam súborov oddelených čiarkou (napr.: pdf,xls,doc). Prázdny zoznam znamená, že všetky súbory sú súkromné.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Kvalita pri zmene veľkosti obrázkov", + "resize-image-quality-help": "Pre zníženie veľkosti zmenšených obrázkov použite nižšie nastavenia kvality.", + "max-file-size": "Maximálna veľkosť súboru (v KiB)", + "max-file-size-help": "(v kilobajtoch, predvolené 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Povoliť používateľom nahrať miniatúry tém", + "topic-thumb-size": "Veľkosť miniatúry témy", + "allowed-file-extensions": "Predvolené prípony súborov", + "allowed-file-extensions-help": "Zadajte zoznam prípon súborov oddelených čiarkou (napr.: pdf, xls, doc). Prázdny zoznam znamená, že všetky prípony sú povolené.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profilové obrázky", + "allow-profile-image-uploads": "Povoliť používateľom nahrať profilové obrázky", + "convert-profile-image-png": "Previesť profilové obrázky do *.png", + "default-avatar": "Predvolený používateľský obrázok", + "upload": "Nahrať", + "profile-image-dimension": "Rozlíšenie profilového obrázka", + "profile-image-dimension-help": "(v pixeloch, predvolené: 128 pixelov)", + "max-profile-image-size": "Maximálna veľkosť profilového obrázka", + "max-profile-image-size-help": "(v kilobajtoch, predvolené: 256 KiB)", + "max-cover-image-size": "Maximálna veľkosť profilového obrázku", + "max-cover-image-size-help": "(v kilobajtoch, predvolené: 2048 KiB)", + "keep-all-user-images": "Ponechať starú verziu obrázkov a profilových obrázkov na serveri", + "profile-covers": "Profilové obrázky", + "default-covers": "Predvolený obrázok", + "default-covers-help": "Pridať predvolené obrázky oddelené čiarkou pre účty, ktoré nemajú nahraný obrázok" +} diff --git a/public/language/sk/admin/settings/user.json b/public/language/sk/admin/settings/user.json new file mode 100644 index 0000000000..18b8367c0b --- /dev/null +++ b/public/language/sk/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Overenie", + "email-confirm-interval": "Používateľ nesmie požiadať o znovu odoslanie potvrdzujúceho e-mailu do", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Povoliť prihlásenie pomocou", + "allow-login-with.username-email": "Používateľské meno alebo e-mail", + "allow-login-with.username": "Iba používateľské meno", + "account-settings": "Nastavenia účtu", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Zakázať zmenu používateľského mena", + "disable-email-changes": "Zakázať zmenu e-mailu", + "disable-password-changes": "Zakázať zmenu hesla", + "allow-account-deletion": "Povoliť zmazanie účtu", + "hide-fullname": "Skryť meno pred používateľom", + "hide-email": "Skryť e-mail pre používateľmi", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Vzhľady", + "disable-user-skins": "Zabrániť používateľovi vo výbere vlastného vzhľadu", + "account-protection": "Ochrana účtu", + "admin-relogin-duration": "Čas pre opätovné prihlásenie správcu (minúty)", + "admin-relogin-duration-help": "Po nastavení počtu prístupu do správcovskej časti, bude vyžadované opätovné prihlásenie. Pre zakázanie, nastavte na 0.", + "login-attempts": "Počet pokusov o prihlásenie za hodinu", + "login-attempts-help": "Ak prekročia pokusy o prihlásenie používateľa/ov túto hranicu, účet bude uzamknutý na určený čas", + "lockout-duration": "Dĺžka blokovania účtu (v minútach)", + "login-days": "Počet dní na zapamätanie relácie prihlásenie používateľa", + "password-expiry-days": "Vynútiť obnovenie hesla po určitom počte dní", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "Registrácia používateľa", + "registration-type": "Typ registrácie", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normálne", + "registration-type.admin-approval": "Povolenia správcu", + "registration-type.admin-approval-ip": "Povolenie správcu podľa IP adries", + "registration-type.invite-only": "Iba na pozvanie", + "registration-type.admin-invite-only": "Iba pozvaný správcom", + "registration-type.disabled": "Bez registrácie", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximálny počet pozvánok na používateľa", + "max-invites": "Maximálny počet pozvánok na používateľa", + "max-invites-help": "0 pre neobmedzené. Správcovia majú neobmedzene pozvánky
Použiteľné iba pre „Iba pozvané“", + "invite-expiration": "Vypršanie pozvánky", + "invite-expiration-help": "pozvanie vyprší za # dní.", + "min-username-length": "Minimálna dĺžka používateľského mena", + "max-username-length": "Maximálna dĺžka používateľského mena", + "min-password-length": "Minimálna dĺžka hesla", + "min-password-strength": "Minimálna sila hesla", + "max-about-me-length": "Maximálna dĺžka informácií „O mne”", + "terms-of-use": "Podmienky používania fóra (pre zakázanie nechajte prázdne)", + "user-search": "Hľadať používateľa", + "user-search-results-per-page": "Počet zobrazených výsledkov", + "default-user-settings": "Predvolené nastavenia používateľa", + "show-email": "Zobraziť e-mail", + "show-fullname": "Zobraziť celé meno", + "restrict-chat": "Povoliť správy konverzácie iba od používateľov, ktorých sledujem", + "outgoing-new-tab": "Otvoriť odchádzajúce odkazy v novom liste", + "topic-search": "Povoliť vyhľadávanie v témach", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Prihlásiť k prehľadu", + "digest-freq.off": "Vypnuté", + "digest-freq.daily": "Denne", + "digest-freq.weekly": "Týždenne", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Mesačne", + "email-chat-notifs": "Poslať mi e-mail, ak nie som online a dorazí mi nová správa z konverzácie", + "email-post-notif": "Poslať e-mail, ak sa objaví odpoveď v téme, ktorú sledujem", + "follow-created-topics": "Sledovať mnou vytvorené témy", + "follow-replied-topics": "Sledovať témy, na ktoré ste odpovedal", + "default-notification-settings": "Predvolené nastavenia oznámení", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/sk/admin/settings/web-crawler.json b/public/language/sk/admin/settings/web-crawler.json new file mode 100644 index 0000000000..3308016107 --- /dev/null +++ b/public/language/sk/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Nastavenia prehľadávania", + "robots-txt": "Upraviť Robots.txt Pre predvolené ponechajte prázdne ", + "sitemap-feed-settings": "Nastaviť zdroj a mapu stránky", + "disable-rss-feeds": "Zakázať zdroje RSS", + "disable-sitemap-xml": "Zakázať Sitemap.xml", + "sitemap-topics": "Počet tém zobrazených na mape stránky", + "clear-sitemap-cache": "Zmazať vyrovnávaciu pamäť mapy stránky", + "view-sitemap": "Zobraziť mapu stránky" +} \ No newline at end of file diff --git a/public/language/sk/category.json b/public/language/sk/category.json new file mode 100644 index 0000000000..c04b887d0c --- /dev/null +++ b/public/language/sk/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategória", + "subcategories": "Podkategórie", + "new_topic_button": "Nová téma", + "guest-login-post": "Prihlásiť sa k pridávaniu príspevkov", + "no_topics": "V tejto kategórií zatiaľ nie sú žiadne témy.
Môžete byť prvý!", + "browsing": "prehliada", + "no_replies": "Nikto ešte neodpovedal", + "no_new_posts": "Žiadne nové príspevky.", + "watch": "Sledovať", + "ignore": "Ignorovať", + "watching": "Sledované", + "not-watching": "Not Watching", + "ignoring": "Ignorovať", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Sledované kategórie", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/sk/email.json b/public/language/sk/email.json new file mode 100644 index 0000000000..daaa6d84fe --- /dev/null +++ b/public/language/sk/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Vitajte v %1", + "invite": "Pozvánka od %1", + "greeting_no_name": "Dobrý deň", + "greeting_with_name": "Dobrý deň %1", + "email.verify-your-email.subject": "Overte si prosím, vašu e-mailovú adresu", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Ďakujeme Vám za registráciu s %1!", + "welcome.text2": "Pre úplne aktivovanie Vášho účtu, musíme overiť e-mailovú adresu, ktorú ste zadali pri registrácií.", + "welcome.text3": "Správca práve potvrdil vašu registráciu. Teraz sa môžete prihlásiť svojím menom a heslom.", + "welcome.cta": "Kliknite sem pre potvrdenie Vašej e-mailovej adresy", + "invitation.text1": "%1 Vás pozval aby ste sa pridali k %2", + "invitation.text2": "Vaše pozvánky uplynú za %1 dní.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Obdržali sme žiadosť o obnovu Vášho hesla. Ak ste o zmenu hesla nežiadali, prosím ignorujte tento e-mail.", + "reset.text2": "Pre pokračovanie v obnove hesla, kliknite na nasledovný odkaz:", + "reset.cta": "Kliknite sem, pre obnovu hesla", + "reset.notify.subject": "Heslo bolo úspešne zmenené", + "reset.notify.text1": "Oznamujeme Vám že %1, bolo Vaše heslo úspešne zmenené.", + "reset.notify.text2": "Ak ste o to nežiadali, kontaktujte čo najskôr správcu.", + "digest.latest_topics": "Najnovšie témy od %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Kliknite sem a navštívite %1", + "digest.unsub.info": "Tento oznam ste prijali na základe Vašich nastavení odoberania.", + "digest.day": "deň", + "digest.week": "týždeň", + "digest.month": "mesiac", + "digest.subject": "Prehľad za %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Máte novú správu od %1", + "notif.chat.cta": "Kliknite sem pre pokračovanie v konverzácii", + "notif.chat.unsub.info": "Túto správu konverzácie ste prijali na základe Vašich nastavení odoberania.", + "notif.post.unsub.info": "Toto oznámenie o príspevkoch ste prijali na základe Vašich nastavení účtu.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "Toto je skúšobný e-mail na overenie funkčnosti e-mailovej aplikácie Vášho NodeBB fóra.", + "unsub.cta": "Kliknite sem pre zmenu týchto nastavení", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Boli ste zablokovaný používateľom %1", + "banned.text1": "Používateľ %1 bol zablokovaný používateľom %2.", + "banned.text2": "Toto zablokovanie bude trvať do %1.", + "banned.text3": "To je dôvod, prečo ste boli zablokovaný:", + "closing": "Ďakujeme!" +} \ No newline at end of file diff --git a/public/language/sk/error.json b/public/language/sk/error.json new file mode 100644 index 0000000000..191c951b3c --- /dev/null +++ b/public/language/sk/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Nesprávne údaje", + "invalid-json": "Neplatné JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Zdá sa že nie ste prihlásený/á.", + "account-locked": "Váš účet bol dočasne uzamknutý", + "search-requires-login": "K vyhľadávaniu je vyžadovaný účet - prosím prihláste sa alebo zaregistrujte.", + "goback": "Pre návrat na predchádzajúcu stránku, stlačte tlačidlo „Späť”", + "invalid-cid": "Neplatné ID kategórie", + "invalid-tid": "Neplatné ID témy", + "invalid-pid": "Neplatné ID príspevku", + "invalid-uid": "Nesprávne ID užívateľa", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Nesprávne používateľské meno", + "invalid-email": "Nesprávny e-mail", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Neplatný názov", + "invalid-user-data": "Neplatné používateľské údaje", + "invalid-password": "Nesprávne heslo", + "invalid-login-credentials": "Neplatné prihlasovacie údaje", + "invalid-username-or-password": "Prosím upresnite používateľské meno a heslo", + "invalid-search-term": "Neplatný výraz pre vyhľadávanie", + "invalid-url": "Neplatná URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Systém prihlásenia pre miestne účty bol zablokovaný pre neoprávnené účty.", + "csrf-invalid": "Nie sme schopný Vás znova prihlásiť, pravdepodobne kvôli uplynutiu relácie. Zopakujte to neskôr prosím.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Neplatná stránkovania hodnota, musí byť najmenej %1 a najviac %2", + "username-taken": "Užívateľské meno je už obsadené", + "email-taken": "Tento e-mail je už obsadený", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Nemôžete vytvoriť konverzáciu pokiaľ Váš e-mail nebude overený. Prosím kliknite sem, pre overenie Vášho e-mailu.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Momentálne nemôžeme overiť Váš e-mail, prosím zopakujte to neskôr.", + "confirm-email-already-sent": "Overovací e-mail už bol odoslaný. Prosím počkajte %1 minút(y) k odoslaniu ďalšieho.", + "sendmail-not-found": "Odoslaný spúšťač nebol nájdený, prosím uistite sa že je nainštalovaný a spustiteľný užívateľom používajúcim NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Užívateľské meno je príliš krátke", + "username-too-long": "Užívateľské meno je príliš dlhé", + "password-too-long": "Heslo je príliš dlhé", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Užívateľ je zablokovaný", + "user-banned-reason": "Prepáčte, tento účet bol zablokovaný (Dôvod: %1)", + "user-banned-reason-until": "Ospravedlňujeme sa, tento účet bol zablokovaný do %1 (Dôvod: %2)", + "user-too-new": "Prepáčte, musíte počkať %1 sekúnd(y) predtým, ako vytvoríte svoj prvý príspevok", + "blacklisted-ip": "Prepáčte, ale vaša IP adresa bola na tejto komunite zablokovaná. Ak sa cítite poškodený, prosím kontaktujte správcu.", + "ban-expiry-missing": "Prosím uveďte dátum ukončenia tohto zablokovania", + "no-category": "Kategória neexistuje", + "no-topic": "Téma neexistuje", + "no-post": "Príspevok už neexistuje", + "no-group": "Skupina neexistuje", + "no-user": "Užívateľ neexistuje", + "no-teaser": "Ukážka neexistuje", + "no-flag": "Flag does not exist", + "no-privileges": "Na túto akciu nemáte dostatočné oprávnenia.", + "category-disabled": "Kategória je zablokovaná", + "topic-locked": "Téma je uzamknutá", + "post-edit-duration-expired": "Upravovať príspevky môžete až za %1 sekúnd(y) po vytvorení", + "post-edit-duration-expired-minutes": "Upravovať príspevky môžete až za %1 minút(y) po umiestnení", + "post-edit-duration-expired-minutes-seconds": "Upravovať príspevky môžete až za %1 minút(y) %2 sekúnd(y) po umiestnení", + "post-edit-duration-expired-hours": "Upravovať príspevky môžete až za %1 hodinu(y) po umiestnení", + "post-edit-duration-expired-hours-minutes": "Upravovať príspevky môžete až za %1 hodinu(y) %2 minút(y) po umiestnení", + "post-edit-duration-expired-days": "Upravovať príspevky môžete až za %1 deň(dni) po umiestnení", + "post-edit-duration-expired-days-hours": "Upravovať príspevky môžete až za %1 deň(dni) %2 hodinu(y) po umiestnení", + "post-delete-duration-expired": "Odstrániť príspevky môžete až za %1 sekúnd(y) po umiestnení", + "post-delete-duration-expired-minutes": "Odstrániť príspevky môžete až za %1 minút(y) po umiestnení", + "post-delete-duration-expired-minutes-seconds": "Odstrániť príspevky môžete až za %1 minút(y) %2 sekúnd(y) po umiestnení", + "post-delete-duration-expired-hours": "Odstrániť príspevky môžete až za %1 hodinu(y) po umiestnení", + "post-delete-duration-expired-hours-minutes": "Odstrániť príspevky môžete až za %1 hodinu(y) %2 minút(y) po umiestnení", + "post-delete-duration-expired-days": "Odstrániť príspevky môžete až za %1 deň(dni) po umiestnení", + "post-delete-duration-expired-days-hours": "Odstrániť príspevky môžete až za %1 deň(dni) %2 hodinu(y) po umiestnení", + "cant-delete-topic-has-reply": "Nemôžete odstrániť svoju tému po tom, ak už obsahuje odpoveď", + "cant-delete-topic-has-replies": "Nemôžete odstrániť túto tému po tom, ak už obsahuje %1 odpovede", + "content-too-short": "Prosím, zadajte dlhší príspevok. Príspevky musia obsahovať najmenej %1 znak(y).", + "content-too-long": "Prosím, zadajte kratší príspevok. Príspevky nemôžu byť dlhšie ako %1 znaky(ov).", + "title-too-short": "Prosím, zadajte dlhší názov. Názvy musia obsahovať najmenej %1 znak(y).", + "title-too-long": "Prosím, zadajte kratší názov. Názvy nemôžu byť dlhšie ako %1 znaky(ov).", + "category-not-selected": "Kategória nebola vybratá.", + "too-many-posts": "Môžete uverejniť príspevok každých %1 sekúnd(y) - prosím počkajte pred opätovným zverejnením", + "too-many-posts-newbie": "Ako nový užívateľ, môžete uverejniť príspevok raz za %1 sekúnd(y) pokiaľ nezískate %2 reputáciu - prosím, počkajte pred ďalším uverejnením", + "already-posting": "You are already posting", + "tag-too-short": "Prosím, zadajte dlhšiu značku. Značky by mali obsahovať najmenej %1 znak(ov)", + "tag-too-long": "Prosím, zadajte kratšiu značku. Značky nemôžu obsahovať viac ako %1 znak(ov)", + "not-enough-tags": "Príliš malo značiek. Témy musia mať minimálne %1 značku(y)", + "too-many-tags": "Príliš veľa značiek. Témy nemôžu mať viac ako %1 značku(y)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Prosím čakajte na dokončenie nahrávania", + "file-too-big": "Najväčšia povolená veľkosť obrázka je %1 kB - prosím nahrajte menší súbor", + "guest-upload-disabled": "Nahrávanie pre hostí bolo zablokované", + "cors-error": "Nieje možné nahrať obrázok kvôli zle nastavenému „Cross-Origin Resource Sharing (CORS)”", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Tento príspevok máte už medzi záložkami", + "already-unbookmarked": "Tento príspevok už nemáte medzi záložkami", + "cant-ban-other-admins": "Nemôžte zablokovať iných správcov.", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Momentálne ste jediný správca. Najskôr pridajte ďalšieho užívateľa za správcu predtým, ako zrušíte svoje výsady správcu", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Odobrať oprávnenie správcu z tohto účtu pred pokusom ho odstrániť.", + "already-deleting": "Already deleting", + "invalid-image": "Neplatný obrázok", + "invalid-image-type": "Neplatný typ obrázku. Povolené typy sú: %1", + "invalid-image-extension": "Neplatná prípona obrázku", + "invalid-file-type": "Neplatný typ súboru. Povolené typy sú: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Názov skupiny je príliš krátky", + "group-name-too-long": "Názov skupiny je príliš dlhý", + "group-already-exists": "Skupina už existuje", + "group-name-change-not-allowed": "Nepovolená zmena mena skupiny", + "group-already-member": "Už ste súčasťou tejto skupiny", + "group-not-member": "Nie ste členom tejto skupiny", + "group-needs-owner": "Táto skupina vyžaduje aspoň jedného vlastníka", + "group-already-invited": "Tento užívateľ už bol pozvaný", + "group-already-requested": "Vaša požiadavka na členstvo už bola predložená", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Tento príspevok bol odstránený", + "post-already-restored": "Tento príspevok bol obnovený", + "topic-already-deleted": "Táto téma bola odstránená", + "topic-already-restored": "Táto téma bola obnovená", + "cant-purge-main-post": "Nemôžete očistiť hlavný príspevok, namiesto toho prosíme odstráňte tému", + "topic-thumbnails-are-disabled": "Náhľady tém sú zablokované.", + "invalid-file": "Neplatný súbor", + "uploads-are-disabled": "Nahrávanie je zablokované", + "signature-too-long": "Prepáčte, ale Váš podpis nemôže byť dlhší ako %1 znak-y(ov).", + "about-me-too-long": "Prepáčte, ale Vaše 'O mne' nemôže byť dlhšie ako %1 znaky(ov).", + "cant-chat-with-yourself": "Nemôžete sa rozprávať so samým sebou!", + "chat-restricted": "Tento používateľ obmedzil svoje správy v konverzácií. Musí Vás sledovať, aby ste sa s ním mohli rozprávať", + "chat-disabled": "Systém konverzácií je zablokovaný", + "too-many-messages": "Odoslali ste príliš veľa správ, počkajte chvíľu prosím.", + "invalid-chat-message": "Neplatná správa konverzácie", + "chat-message-too-long": "Správy v konverzácií nemôžu byť dlhšie ako %1 znakov.", + "cant-edit-chat-message": "Nemáte oprávnenie k úprave tejto správy", + "cant-delete-chat-message": "Nemáte oprávanie k odstráneniu tejto správy", + "chat-edit-duration-expired": "Je Vám umožnené upraviť správy konverzácie po dobu %1 sekúnd po ich odoslaní", + "chat-delete-duration-expired": "Je Vám umožnené odstrániť správy konverzácie po dobu %1 sekúnd po ich odoslaní", + "chat-deleted-already": "Táto správa konverzácie už bola odstránená.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Za tento príspevok ste už hlasovali.", + "reputation-system-disabled": "Systém reputácie je zablokovaný.", + "downvoting-disabled": "Hlasovanie proti je zablokované", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Za svoj vlastný príspevok nemôžete hlasovať", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB narazil na problém pri načítaní: \"%1\". NodeBB bude pokračovať v službe existujúcej aktívnej klientskej strane, aj keď by ste mali vrátiť to, čo ste spravili tesne pred znovu načítaním.", + "registration-error": "Chyba registrácie", + "parse-error": "Niečo sa pokazilo pri analýze odpovede servera", + "wrong-login-type-email": "Prosím použite svoj e-mail, k prihláseniu", + "wrong-login-type-username": "Použite svoje užívateľské meno pre prihlásenie", + "sso-registration-disabled": "Registrácia bola zakázaná pre účty - %1. Najskôr si zaregistrujte e-mailovú adresu", + "sso-multiple-association": "Nie je možné priradiť viacej účtov z tejto služby do Vášho účtu NodeBB. Vylúčte Váš existujúci účet a skúste to znova.", + "invite-maximum-met": "Pozvali ste maximálny počet ľudí (%1 z %2).", + "no-session-found": "Žiadne prihlásenie sa do relácií nenájdené!", + "not-in-room": "Užívateľ nie je v miestnosti", + "cant-kick-self": "Nemôžete vykopnúť samého seba zo skupiny", + "no-users-selected": "Žiadny užívateľ(ia) neboli vybratý", + "invalid-home-page-route": "Neplatná cesta pre domovskú stránku", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Žiadne vybrané témy.", + "cant-move-to-same-topic": "Nie je možné presunúť príspevok do rovnakej témy!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Nemôžete zablokovať seba samého!", + "cannot-block-privileged": "Nemôžete zablokovať správcov alebo hlavných moderátorov", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "Zdá sa, že máte problém s pripojením k internetu", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/sk/flags.json b/public/language/sk/flags.json new file mode 100644 index 0000000000..dc3ccb78f7 --- /dev/null +++ b/public/language/sk/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Stav", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hurá! Neboli nájdené žiadne príznaky.", + "assignee": "Nástupca", + "update": "Aktualizovať", + "updated": "Aktualizované", + "resolved": "Resolved", + "target-purged": "Obsah, na ktorý sa vzťahuje toto označenie, bol odstránený a už nie je k dispozícii.", + + "graph-label": "Daily Flags", + "quick-filters": "Rýchle filtre", + "filter-active": "V tomto zozname označení je aktívny jeden alebo viac filtrov", + "filter-reset": "Odstrániť filtre", + "filters": "Možnosti filtrov", + "filter-reporterId": "UID ohlasovateľa", + "filter-targetUid": "UID označenie", + "filter-type": "Typ označenia", + "filter-type-all": "Všetok obsah", + "filter-type-post": "Príspevok", + "filter-type-user": "Používateľ", + "filter-state": "Stav", + "filter-assignee": "UID nadobúdateľa", + "filter-cid": "Kategória", + "filter-quick-mine": "Priradené mne", + "filter-cid-all": "Všetky kategórie", + "apply-filters": "Použiť filtre", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Označený používateľ", + "view-profile": "Zobraziť profil", + "start-new-chat": "Začať novú konverzáciu", + "go-to-target": "Zobraziť cieľové označenie", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Zobraziť profil", + "user-edit": "Upraviť profil", + + "notes": "Poznámky príznaku", + "add-note": "Pridať poznámku", + "no-notes": "Žiadne zdieľané poznámky.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Poznámka bola pridaná", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Žiadna história príznakov.", + + "state-all": "Všetky stavy", + "state-open": "Nový/Otvoriť", + "state-wip": "Práca prebieha", + "state-resolved": "Vyriešené", + "state-rejected": "Odmietnuté", + "no-assignee": "Priradené k %1", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Zadajte dôvod, pre ktorý chcete označiť %1 %2 na kontrolu. Prípadne použite jedno z tlačidiel rýchleho hlásenia, ak je to vhodné.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Urážlivý", + "modal-reason-other": "Iné (popíšte nižšie)", + "modal-reason-custom": "Dôvod oznamovania tohto obsahu...", + "modal-submit": "Odoslať správu", + "modal-submit-success": "Obsah bol označený na moderovanie.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/sk/global.json b/public/language/sk/global.json new file mode 100644 index 0000000000..06ee020b42 --- /dev/null +++ b/public/language/sk/global.json @@ -0,0 +1,126 @@ +{ + "home": "Domov", + "search": "Hľadať", + "buttons.close": "Zatvoriť", + "403.title": "Prístup zamietnutý", + "403.message": "Zdá sa, že ste narazili/a na stránku, na ktorú nemáte prístup.", + "403.login": "Možno by ste mali skúste sa prihlásiť ?", + "404.title": "Stránka nenájdená", + "404.message": "Zdá sa že ste narazili na stránku, ktorá už neexistuje. Vráťte sa na domovskú stránku.", + "500.title": "Vnútorná chyba.", + "500.message": "Och! Vyzerá to tak, že sa niečo pokazilo!", + "400.title": "Nesprávna požiadavka.", + "400.message": "Vyzerá to tak, že tento odkaz je poškodený, prosím skontrolujte ho a skúste to znova. V opačnom prípade sa vráťte na domovskú stránku.", + "register": "Registrovať", + "login": "Prihlásiť sa", + "please_log_in": "Prosím, prihláste sa", + "logout": "Odhlásiť sa", + "posting_restriction_info": "V súčasnej dobe je zasielanie príspevkov povolené len registrovaným používateľom, kliknite sem a prihláste sa.", + "welcome_back": "Vitajte späť", + "you_have_successfully_logged_in": "Úspešne ste sa prihlásili", + "save_changes": "Uložiť zmeny", + "save": "Uložiť", + "close": "Zatvoriť", + "pagination": "Stránkovanie", + "pagination.out_of": "%1 z %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Správca", + "header.categories": "Kategórie", + "header.recent": "Nedávne", + "header.unread": "Neprečítané", + "header.tags": "Značky", + "header.popular": "Populárne", + "header.top": "Top", + "header.users": "Užívatelia", + "header.groups": "Skupiny", + "header.chats": "Konverzácie", + "header.notifications": "Oznámenia", + "header.search": "Hľadať", + "header.profile": "Profil", + "header.navigation": "Navigácia", + "notifications.loading": "Načítavanie oznámení", + "chats.loading": "Načítanie konverzácií", + "motd.welcome": "Vitajte na NodeBB, diskusná platforma budúcnosti.", + "previouspage": "Predchádzajúca strana", + "nextpage": "Nasledujúca strana", + "alert.success": "Výsledok", + "alert.error": "Chyba", + "alert.banned": "Zablokovaný", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Prestali ste sledovať %1!", + "alert.follow": "Začali ste sledovať %1!", + "users": "Užívatelia", + "topics": "Témy", + "posts": "Príspevky", + "x-posts": "%1 posts", + "best": "Najlepšie", + "controversial": "Controversial", + "votes": "Počet hlasov", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Hlasovali za", + "upvoted": "Pridaný hlas", + "downvoters": "Hlasovali proti", + "downvoted": "Odobratý hlas", + "views": "Zhliadnutí", + "posters": "Posters", + "reputation": "Reputácia", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "čítaj viac", + "more": "Viac", + "none": "None", + "posted_ago_by_guest": "uverejnené %1 od hosťa", + "posted_ago_by": "uverejnené %1 od %2", + "posted_ago": "uverejnené %1", + "posted_in": "uverejnené v %1", + "posted_in_by": "uverejnené v %1 od %2", + "posted_in_ago": "uverejnené v %1 %2", + "posted_in_ago_by": "uverejnené v %1 %2 od %3", + "user_posted_ago": "%1 uverejnil %2", + "guest_posted_ago": "Hosť uverejnil %1", + "last_edited_by": "naposledy zmenené od %1", + "norecentposts": "Žiadne nové príspevky", + "norecenttopics": "Žiadne nové témy", + "recentposts": "Nedávne príspevky", + "recentips": "Nedávne zaznamenané IP adresy", + "moderator_tools": "Nástroje moderátora", + "online": "Online", + "away": "Preč", + "dnd": "Nevyrušovať", + "invisible": "Neviditeľný", + "offline": "Offline", + "email": "E-mail", + "language": "Jazyk", + "guest": "Hosť", + "guests": "Hostia", + "former_user": "Bývalý používateľ", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Fórum bolo aktualizované", + "updated.message": "Toto fórum bolo práve aktualizované na najnovšiu verziu. Pre aktualizovanie tejto stránky kliknite sem.", + "privacy": "Súkromie", + "follow": "Sledovať", + "unfollow": "Prestať sledovať", + "delete_all": "Odstrániť všetko", + "map": "Mapa", + "sessions": "Prihlásiť sa do relácie", + "ip_address": "IP Adresa", + "enter_page_number": "Zadajte číslo stránky", + "upload_file": "Nahrať súbor", + "upload": "Nahrať", + "uploads": "Nahrané", + "allowed-file-types": "Povolené typy súborov sú %1", + "unsaved-changes": "Máte neuložené zmeny. Ste si istý, že chcete opustiť stránku?", + "reconnecting-message": "Vyzerá to tak, že pripojenie k %1 bolo stratené. Prosím chvíľku počkajte, snažíme sa pripojiť znovu.", + "play": "Prehrať", + "cookies.message": "Táto webová stránka používa cookies k tomu, aby bolo zaistené, že dostanete najlepší pôžitok s návštevy na našich webových stránkach.", + "cookies.accept": "Chápem!", + "cookies.learn_more": "Zistit viac", + "edited": "Zmenené", + "disabled": "Zablokovaný", + "select": "Vybrať", + "user-search-prompt": "Pre hľadanie používateľov, píšte sem..." +} \ No newline at end of file diff --git a/public/language/sk/groups.json b/public/language/sk/groups.json new file mode 100644 index 0000000000..69c06f5840 --- /dev/null +++ b/public/language/sk/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Skupiny", + "view_group": "Zobraziť skupinu", + "owner": "Vlastník skupiny", + "new_group": "Vytvoriť novú skupinu", + "no_groups_found": "Neexistujú žiadne skupiny, ktoré by bolo možné vidieť", + "pending.accept": "Prijať", + "pending.reject": "Odmietnuť", + "pending.accept_all": "Prijať všetko", + "pending.reject_all": "Odmietnuť všetko", + "pending.none": "Momentálne neexistujú žiadny čakajúci členovia", + "invited.none": "Momentálne neexistujú žiadny pozvaný členovia", + "invited.uninvite": "Odvolať pozvánku", + "invited.search": "Hľadať používateľa k pozvaniu do tejto skupiny", + "invited.notification_title": "Boli ste pozvaní aby ste sa pripojili k%1", + "request.notification_title": "Žiadosť o členstvo v skupine od %1", + "request.notification_text": "%1 žiada o členstvo v %2", + "cover-save": "Uložiť", + "cover-saving": "Ukladanie", + "details.title": "Detaily skupiny", + "details.members": "Zoznam členov", + "details.pending": "Čakajúci členovia", + "details.invited": "Pozvaný členovia", + "details.has_no_posts": "Používatelia tejto skupiny zatiaľ nepridali žiadne príspevky.", + "details.latest_posts": "Najnovšie príspevky", + "details.private": "Súkromné", + "details.disableJoinRequests": "Vypnúť požiadavky o prijatie", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Pridať/Zrušiť vlastníctvo", + "details.kick": "Vyhodiť", + "details.kick_confirm": "Ste si naozaj istý, že chcete odstrániť tohto člena zo skupiny?", + "details.add-member": "Pridať používateľa", + "details.owner_options": "Správca skupiny", + "details.group_name": "Názov skupiny", + "details.member_count": "Počet členov", + "details.creation_date": "Dátum vytvorenia", + "details.description": "Popis", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Náhľad odznaku", + "details.change_icon": "Zmeniť ikonu", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Popis odznaku", + "details.userTitleEnabled": "Zobraziť odznak", + "details.private_help": "Ak je to povolené, spájanie skupín vyžaduje schválenie od vlastníka skupiny", + "details.hidden": "Skrytý", + "details.hidden_help": "Ak je to povolené, túto skupinu nebude možné nájsť v zozname skupín, a užívatelia môžu byť pozvaný iba manuálne", + "details.delete_group": "Odstrániť skupinu", + "details.private_system_help": "Súkromne skupiny sú zablokované na systémovej úrovni, s touto voľbou nič nespravíte", + "event.updated": "Podrobnosti skupiny boli aktualizované", + "event.deleted": "Skupina \"%1\" bola odstránená", + "membership.accept-invitation": "Prijať pozvanie", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Pozvánka čakajúca na vybavenie", + "membership.join-group": "Pripojiť do skupiny", + "membership.leave-group": "Opustiť skupinu", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Odmietnuť", + "new-group.group_name": "Názov skupiny:", + "upload-group-cover": "Nahrať obrázok skupiny", + "bulk-invite-instructions": "Zadajte zoznam užívateľských mien oddelených čiarkou, k pozvaniu do tejto skupiny", + "bulk-invite": "Hromadné pozvanie", + "remove_group_cover_confirm": "Ste si naozaj istý, že chcete odstrániť titulný obrázok?" +} \ No newline at end of file diff --git a/public/language/sk/ip-blacklist.json b/public/language/sk/ip-blacklist.json new file mode 100644 index 0000000000..0eeaa72f4f --- /dev/null +++ b/public/language/sk/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Nakonfigurujte si čierny zoznam IP adries.", + "description": "Príležitostne zákaz používania používateľského účtu nie je dostatočne odradzujúci. Inokedy je obmedzenie prístupu na fórum k určitej IP alebo celej škále IP serverov najlepší spôsob, ako chrániť fórum. V týchto scenároch môžete do tejto čiernej listy pridať nepríjemné IP adresy alebo celé bloky CIDR a zabrániť ich prihláseniu alebo registrácii nového účtu.", + "active-rules": "Aktívne pravidlá", + "validate": "Potvrdiť čiernu listinu", + "apply": "Použiť čierny zoznam", + "hints": "Syntax rady", + "hint-1": "Určite jednotlivú IP adresu na riadok. Môžete pridať IP bloky ak spĺňajú formát CIDR (tj. 192.168.100.0/22).", + "hint-2": "Môžete pridať aj komentáre, ak bude riadok začítať symbolom #.", + + "validate.x-valid": "%1 z %2 pravidiel je platných.", + "validate.x-invalid": "Nasledujúcich %1 pravidiel nie je platných:", + + "alerts.applied-success": "Čierny zoznam bol použitý", + + "analytics.blacklist-hourly": "Postava 1 - záznamov v čiernom zozname za hodinu", + "analytics.blacklist-daily": "Postava 2– záznamov v čiernom zozname za deň ", + "ip-banned": "Zablokovaná IP adresa" +} \ No newline at end of file diff --git a/public/language/sk/language.json b/public/language/sk/language.json new file mode 100644 index 0000000000..dfa195e891 --- /dev/null +++ b/public/language/sk/language.json @@ -0,0 +1,5 @@ +{ + "name": "Slovenčina (Slovakia)", + "code": "sk", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/sk/login.json b/public/language/sk/login.json new file mode 100644 index 0000000000..dd2521d2d0 --- /dev/null +++ b/public/language/sk/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Uživateľské meno / E-mail", + "username": "Používateľské meno", + "remember_me": "Zapamätať si ma?", + "forgot_password": "Zabudli ste heslo?", + "alternative_logins": "Ďalšie spôsoby prihlásenia", + "failed_login_attempt": "Prihlásenie neúspešné", + "login_successful": "Úspešne ste sa prihlásili!", + "dont_have_account": "Nemáte účet?", + "logged-out-due-to-inactivity": "Z dôvodu nečinnosti ste bol odhlásený z ovládacieho panela správcu", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json new file mode 100644 index 0000000000..e1c1e9a04e --- /dev/null +++ b/public/language/sk/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Konverzácia s", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Odoslať", + "chat.no_active": "Nemáte žiadne aktívne konverzácie.", + "chat.user_typing": "%1 práve píše...", + "chat.user_has_messaged_you": "%1 Vám poslal správu.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Prosím vyberte príjemcu, pre zobrazenie histórie správ v konverzácií", + "chat.no-users-in-room": "Žiadny používatelia v tejto miestnosti", + "chat.recent-chats": "Najnovšie rozhovory", + "chat.contacts": "Kontakty", + "chat.message-history": "História správ", + "chat.message-deleted": "Message Deleted", + "chat.options": "Možnosti konverzácie", + "chat.pop-out": "Vyskakujúce okno konverzácie", + "chat.minimize": "Minimalizovať", + "chat.maximize": "Maximalizovať", + "chat.seven_days": "7 dní", + "chat.thirty_days": "30 dní", + "chat.three_months": "3 mesiace", + "chat.delete_message_confirm": "Ste si istý, že chcete odstrániť túto správu?", + "chat.retrieving-users": "Získavanie zoznamu používateľov...", + "chat.manage-room": "Spravovať konverzačné miestnosti", + "chat.add-user-help": "Tu môžete vyhľadávať používateľov. Akonáhle si ho vyberiete, užívateľ bude pridaný do konverzácie. Nový používateľ nebude mať možnosť čítať správy, ktoré boli napísané skôr, ako bol pridaný do konverzácie. Iba majitelia miestnosti () môžu odobrať používateľov z konverzačných miestností.", + "chat.confirm-chat-with-dnd-user": "Tento používateľ nastavil svoj stav na NERUŠIŤ. Naozaj chcete s ním začať konverzáciu?", + "chat.rename-room": "Premenovať miestnosť", + "chat.rename-placeholder": "Sem zadajte názov miestnosti", + "chat.rename-help": "Názov miestnosti zadaný sem, bude viditeľný pre všetkých účastníkov komunikácie v miestnosti", + "chat.leave": "Opustiť konverzáciu", + "chat.leave-prompt": "Ste si istý, že chcete ukončiť túto konverzáciu?", + "chat.leave-help": "Ukončením tejto konverzácie budete odstránený z budúcej komunikácie v tejto konverzácií. Ak budete následne znovu pridaný, neuvidíte históriu komunikácie od Vášho odchodu.", + "chat.in-room": "V tejto miestnosti", + "chat.kick": "Vykopnúť", + "chat.show-ip": "Zobraziť IP adresu", + "chat.owner": "Majiteľ miestnosti", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Napísať", + "composer.show_preview": "Zobraziť náhľad", + "composer.hide_preview": "Skryť náhľad", + "composer.user_said_in": "%1 povedal v %2:", + "composer.user_said": "%1 povedal:", + "composer.discard": "Ste si istý, že chcete zahodiť tento príspevok?", + "composer.submit_and_lock": "Vložiť a uzamknúť", + "composer.toggle_dropdown": "Prepnúť rozbalovací zoznam", + "composer.uploading": "Nahrávanie %1", + "composer.formatting.bold": "Tučné", + "composer.formatting.italic": "Kurzíva", + "composer.formatting.list": "Zoznam", + "composer.formatting.strikethrough": "Prečiarknuté", + "composer.formatting.code": "Code", + "composer.formatting.link": "Odkaz", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Nahrať obrázok", + "composer.upload-file": "Nahrať súbor", + "composer.zen_mode": "Režim Zen", + "composer.select_category": "Vyberte kategóriu", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Zrušiť", + "bootbox.confirm": "Potvrdiť", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Pozícia profilovej fotografie", + "cover.dragging_message": "Presuňte profilovú fotografiu do požadovanej pozície a kliknite na „Uložiť\"", + "cover.saved": "Profilová fotografia a pozícia boli uložené", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/sk/notifications.json b/public/language/sk/notifications.json new file mode 100644 index 0000000000..593325926a --- /dev/null +++ b/public/language/sk/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Oznámenia", + "no_notifs": "Nemáte žiadne nové oznámenia", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Naspäť na %1", + "outgoing_link": "Odkaz mimo fórum", + "outgoing_link_message": "Práve opúšťate %1", + "continue_to": "Pokračovať k %1", + "return_to": "Vrátiť sa na %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Máte neprečítané oznámenia.", + "all": "Všetko", + "topics": "Témy", + "replies": "Odpovede", + "chat": "Konverzácie", + "group-chat": "Group Chats", + "follows": "Nasledovatelia", + "upvote": "Súhlasy", + "new-flags": "Nové označenia", + "my-flags": "Označenia priradené mne", + "bans": "Zablokované", + "new_message_from": "Nova spáva od %1", + "upvoted_your_post_in": "%1 dal hlas Vášmu príspevku v %2.", + "upvoted_your_post_in_dual": "%1 a %2 dali hlas Vášmu príspevku v %3.", + "upvoted_your_post_in_multiple": "%1 a %2 ďalší dali hlas Vášmu príspevku v %3.", + "moved_your_post": "%1 presunul Váš príspevok do %2", + "moved_your_topic": "%1 presunul %2", + "user_flagged_post_in": "%1 pridal značku na príspevok %2", + "user_flagged_post_in_dual": "%1 a %2 pridali značky na príspevok %3", + "user_flagged_post_in_multiple": "%1 a %2 ďalší pridali značku na príspevok:%3", + "user_flagged_user": "%1 označil profil používateľa (%2)", + "user_flagged_user_dual": "%1 a %2 označil profil používateľa (%3)", + "user_flagged_user_multiple": "%1 a %2 ďalší označili profil používateľa (%3)", + "user_posted_to": "%1 odpovedal: %2", + "user_posted_to_dual": "%1 a %2 uverejnili odpoveď na:%3", + "user_posted_to_multiple": "%1 a %2 ďalší uverejnili odpovede na:%3", + "user_posted_topic": "%1 pridal novú tému: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 Vás začal sledovať.", + "user_started_following_you_dual": "%1 a %2 Vás začali sledovať.", + "user_started_following_you_multiple": "%1 a %2 ďalší Vás začali sledovať.", + "new_register": "%1 odoslal žiadosť o registráciu.", + "new_register_multiple": "Nachádzajú sa %1 registrácie čakajúce na preskúmanie.", + "flag_assigned_to_you": "Príznak %1 vám bol priradený ", + "post_awaiting_review": "Príspevok na schválenie", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-mail bol potvrdený", + "email-confirmed-message": "Ďakujeme za potvrdenie Vášho e-mailu. Váš účet je teraz aktivovaný.", + "email-confirm-error-message": "Vyskytla sa chyba pri overení Vašej e-mailovej adresy. ", + "email-confirm-sent": "Potvrdzovací e-mail bol odoslaný.", + "none": "Nič", + "notification_only": "Iba oznámenia", + "email_only": "Iba e-mail", + "notification_and_email": "Oznámenia a E-mail", + "notificationType_upvote": "Ak niekto vyjadri súhlas s vaším príspevkom", + "notificationType_new-topic": "Ak začne niekto sledovať príspevky a témy", + "notificationType_new-reply": "Ak bude pridaný nový príspevok v téme, ktorú sledujete", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "Ak Vás začne niekto sledovať", + "notificationType_new-chat": "Ak obdržíte novú správu konverzácie", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "Ak obdržíte pozvanie do skupiny", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "Ak bude niekto pridaný do registračnej fronty", + "notificationType_post-queue": "Ak bude pridaný nový príspevok do fronty", + "notificationType_new-post-flag": "Ak bude príspevok označený", + "notificationType_new-user-flag": "Ak bude používateľ označený" +} \ No newline at end of file diff --git a/public/language/sk/pages.json b/public/language/sk/pages.json new file mode 100644 index 0000000000..41d1234a8b --- /dev/null +++ b/public/language/sk/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Domov", + "unread": "Neprečítané témy", + "popular-day": "Populárne témy za dnešok", + "popular-week": "Populárne témy za tento týždeň", + "popular-month": "Populárne témy za tento mesiac", + "popular-alltime": "Populárne témy za celé obdobie", + "recent": "Nedávne témy", + "top-day": "Dnešná téma s najviac súhlasmi", + "top-week": "Týždenná téma s najviac súhlasmi", + "top-month": "Mesačná téma s najviac súhlasmi", + "top-alltime": "Témy s najviac súhlasmi", + "moderator-tools": "Nástroje moderátora", + "flagged-content": "Nahlásený obsah", + "ip-blacklist": "Čierny zoznam IP adries", + "post-queue": "Fronta príspevkov", + "users/online": "Online používatelia", + "users/latest": "Najnovší používatelia", + "users/sort-posts": "Užívatelia s najväčším počtom príspevkov", + "users/sort-reputation": "Používatelia s najväčšou reputáciou", + "users/banned": "Zablokovaný používatelia", + "users/most-flags": "Najviac používateľov s označením", + "users/search": "Hľadanie užívateľov", + "notifications": "Oznámenia", + "tags": "Značky", + "tag": "Témy označené "%1"", + "register": "Zaregistrovať účet", + "registration-complete": "Registrácia úspešná", + "login": "Prihlásiť sa do účtu", + "reset": "Obnoviť heslo pre Váš účet", + "categories": "Kategórie", + "groups": "Skupiny", + "group": "%1 skupina", + "chats": "Konverzácie", + "chat": "Rozprávate sa s %1", + "flags": "Príznaky", + "flag-details": "Detaily príznaku %1", + "account/edit": "Úprava \"%1\"", + "account/edit/password": "Úprava hesla \"%1\"", + "account/edit/username": "Úprava užívateľského mena \"%1\"", + "account/edit/email": "Úprava e-mailu \"%1\"", + "account/info": "Informácie o účte", + "account/following": "Ľudia, ktorých sleduje %1", + "account/followers": "Ľudia, ktorí sledujú %1", + "account/posts": "Príspevky vytvorené užívateľom %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Témy vytvoril %1", + "account/groups": "%1 skupiny", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1 príspevky v záložkach", + "account/settings": "Užívateľské nastavenia", + "account/watched": "Témy sledované používateľom %1", + "account/ignored": "Témy ignorované používateľom %1", + "account/upvoted": "Príspevky, ktorým používateľ %1 dal hlas", + "account/downvoted": "Nesúhlasí s príspevkom %1", + "account/best": "Najlepšie príspevky vytvorené užívateľom %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Zablokovaní používatelia z %1", + "account/uploads": "Nahraté od %1", + "account/sessions": "Login Sessions", + "confirm": "E-mail potvrdený", + "maintenance.text": "%1 v súčasnej dobe prebieha údržba. Prosíme, vráťte sa neskôr.", + "maintenance.messageIntro": "Správca, dodatočne zanechal túto správu:", + "throttled.text": "%1 je v súčasnej dobe nedostupný z dôvodu nadmerného zaťaženia. Prosím, vráťte sa neskôr" +} \ No newline at end of file diff --git a/public/language/sk/post-queue.json b/public/language/sk/post-queue.json new file mode 100644 index 0000000000..b63aec60fe --- /dev/null +++ b/public/language/sk/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Príspevky vo fronte", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "Používateľ", + "category": "Kategórie", + "title": "Názov", + "content": "Obsah", + "posted": "Pridané", + "reply-to": "Odpovedať na \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/sk/recent.json b/public/language/sk/recent.json new file mode 100644 index 0000000000..44bcaa240b --- /dev/null +++ b/public/language/sk/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Nedávne", + "day": "Deň", + "week": "Týždeň", + "month": "Mesiac", + "year": "Rok", + "alltime": "Vždy", + "no_recent_topics": "Neboli nájdené žiadne nové témy.", + "no_popular_topics": "Neexistujú žiadne populárne témy.", + "there-is-a-new-topic": "K dispozícií je nová téma.", + "there-is-a-new-topic-and-a-new-post": "K dispozícií je nová téma a nový príspevok.", + "there-is-a-new-topic-and-new-posts": "K dispozícií je nová téma a %1 nové príspevky.", + "there-are-new-topics": "K dispozícií sú %1 nové témy.", + "there-are-new-topics-and-a-new-post": "K dispozícií sú %1 nové témy a nový príspevok.", + "there-are-new-topics-and-new-posts": "K dispozícií sú %1 nové témy a %2 nové príspevky.", + "there-is-a-new-post": "K dispozícií je nový príspevok", + "there-are-new-posts": "K dispozícií je %1 nových príspevkov.", + "click-here-to-reload": "Kliknutím sem, znova načítate stránku." +} \ No newline at end of file diff --git a/public/language/sk/register.json b/public/language/sk/register.json new file mode 100644 index 0000000000..d0c1f5f469 --- /dev/null +++ b/public/language/sk/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registrácia", + "cancel_registration": "Zrušiť registráciu", + "help.email": "V predvolenom nastavení bude váš e-mail skrytý.", + "help.username_restrictions": "Jedinečné užívateľské meno dlhé %1 až %2 znakov. Ostatní užívatelia Vás môžu spomenúť ako @užívateľské meno.", + "help.minimum_password_length": "Dĺžka Vášho hesla musí byť aspoň %1 znakov.", + "email_address": "E-mail", + "email_address_placeholder": "Zadajte e-mailovú adresu", + "username": "Používateľské meno", + "username_placeholder": "Zadajte používateľské meno", + "password": "Heslo", + "password_placeholder": "Zadajte heslo", + "confirm_password": "Potvrdenie hesla", + "confirm_password_placeholder": "Potvrdťe heslo", + "register_now_button": "Zaregistrovať sa", + "alternative_registration": "Iný spôsob registrácie", + "terms_of_use": "Podmienky používania", + "agree_to_terms_of_use": "Súhlasím s podmienkami používania", + "terms_of_use_error": "Musíte súhlasiť s podmienkami použitia", + "registration-added-to-queue": "Vaša registrácia bola pridaná do fronty na schválenie. Obdržíte e-mail, keď Vaša registrácia bude prijatá správcom.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Dávam súhlas so zberom a spracovaním mojich osobných údajov na tejto webovej stránke.", + "gdpr_agree_email": "Dávam súhlas k prijímaniu e-mailových prehľadov a oznámení týkajúcich sa tejto webovej stránky.", + "gdpr_consent_denied": "Musíte udeliť súhlas tejto stránke k zbieraniu/spracovaniu informácií o vašej činnosti a odosielať Vám e-maily.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/sk/reset_password.json b/public/language/sk/reset_password.json new file mode 100644 index 0000000000..fc05c116e8 --- /dev/null +++ b/public/language/sk/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Obnoviť heslo", + "update_password": "Upraviť heslo", + "password_changed.title": "Heslo bolo zmenené", + "password_changed.message": "

Heslo bolo úspešne zmenené, prihláste sa znovu prosím.", + "wrong_reset_code.title": "Nesprávny kód na obnovenie", + "wrong_reset_code.message": "Bol zadaný nesprávny kód. Zadajte ho prosím znovu, alebo si nechajte poslať nový.", + "new_password": "Nové heslo", + "repeat_password": "Potvrdenie hesla", + "changing_password": "Changing Password", + "enter_email": "Prosím zadajte svoju e-mailovú adresu a my Vám pošleme informácie, ako môžete obnoviť svoje heslo.", + "enter_email_address": "Zadajte e-mailovú adresu", + "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.", + "invalid_email": "Nesprávny e-mail / E-mail neexistuje.", + "password_too_short": "Zadané heslo je príliš krátke, prosím zadajte iné heslo.", + "passwords_do_not_match": "Heslá ktoré ste zadali sa nezhodujú.", + "password_expired": "Platnosť Vášho hesla vypršala, prosím zvoľte si nové heslo" +} \ No newline at end of file diff --git a/public/language/sk/search.json b/public/language/sk/search.json new file mode 100644 index 0000000000..0059ac9406 --- /dev/null +++ b/public/language/sk/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 výsledok(ov) vhodný(ch) pre \"%2\", (%3 sekúnd)", + "no-matches": "Žiadne zhody nenájdené", + "advanced-search": "Rozšírené vyhľadávanie", + "in": "v", + "titles": "Nadpisy", + "titles-posts": "Nadpisy a príspevky", + "match-words": " Zhodné slová", + "all": "Všetko", + "any": "Akékoľvek", + "posted-by": "Napísal", + "in-categories": "V kategóriách", + "search-child-categories": "Hľadať podružné kategórie", + "has-tags": "Obsahuje značky", + "reply-count": "Počet odpovedí", + "at-least": "Najmenej", + "at-most": "Najviac", + "relevance": "Dôležitosť", + "post-time": "Čas publikovania", + "votes": "Votes", + "newer-than": "Novšie ako", + "older-than": "Staršie ako", + "any-date": "Akýkoľvek dátum", + "yesterday": "Včera", + "one-week": "Jeden týždeň", + "two-weeks": "Dva týždne", + "one-month": "Jeden mesiac", + "three-months": "Tri mesiace", + "six-months": "Šesť mesiacov", + "one-year": "Jeden rok", + "sort-by": "Zoradiť podľa", + "last-reply-time": "Čas poslednej odpovede", + "topic-title": "Názov témy", + "topic-votes": "Topic votes", + "number-of-replies": "Počet odpovedí", + "number-of-views": "Počet zobrazení", + "topic-start-date": "Dátumu začatia témy", + "username": "Užívateľské meno", + "category": "Kategórie", + "descending": "V zostupnom poradí", + "ascending": "Vo vzostupnom poradí", + "save-preferences": "Uložené predvoľby", + "clear-preferences": "Vyčistiť predvoľby", + "search-preferences-saved": "Vyhľadávacie predvoľby uložené", + "search-preferences-cleared": "Vyhľadávacie predvoľby vyčistené", + "show-results-as": "Zobraziť výsledky ako", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/sk/success.json b/public/language/sk/success.json new file mode 100644 index 0000000000..bfc1e4b403 --- /dev/null +++ b/public/language/sk/success.json @@ -0,0 +1,7 @@ +{ + "success": "Úspech", + "topic-post": "Úspešne ste pridali nový príspevok.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Overovanie úspešné", + "settings-saved": "Nastavenia boli uložené." +} \ No newline at end of file diff --git a/public/language/sk/tags.json b/public/language/sk/tags.json new file mode 100644 index 0000000000..aa97976505 --- /dev/null +++ b/public/language/sk/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nie sú tu žiadne témy s touto značkou.", + "tags": "Značky", + "enter_tags_here": "Sem vložte označenie, každé o dĺžke %1 až %2 znakov.", + "enter_tags_here_short": "Zadajte značky...", + "no_tags": "Zatiaľ tu nie sú žiadne značky.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/sk/top.json b/public/language/sk/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/sk/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/sk/topic.json b/public/language/sk/topic.json new file mode 100644 index 0000000000..dc511a1182 --- /dev/null +++ b/public/language/sk/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Téma", + "title": "Title", + "no_topics_found": "Neboli nájdené žiadne témy.", + "no_posts_found": "Neboli nájdené žiadne príspevky", + "post_is_deleted": "Tento príspevok bol odstránený!", + "topic_is_deleted": "Táto téma bola odstránená.", + "profile": "Profil", + "posted_by": "Uverejnil %1", + "posted_by_guest": "Pridané hosťom", + "chat": "Konverzácia", + "notify_me": "Dostávať informácie o nových príspevkoch v tejto téme", + "quote": "Citovať", + "reply": "Odpovedať", + "replies_to_this_post": "%1 odpovedí", + "one_reply_to_this_post": "1 odpoveď", + "last_reply_time": "Posledná odpoveď", + "reply-as-topic": "Odpovedať ako téma", + "guest-login-reply": "Pre odpoveď sa najprv prihláste", + "login-to-view": "🔒 Log in to view", + "edit": "Upraviť", + "delete": "Odstrániť", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Vyčistiť", + "restore": "Obnoviť", + "move": "Presunúť", + "change-owner": "Change Owner", + "fork": "Rozdeliť", + "link": "Odkaz", + "share": "Zdieľať", + "tools": "Nástroje", + "locked": "Uzamknuté", + "pinned": "Pripnuté", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Presunuté", + "moved-from": "Moved from %1", + "copy-ip": "Kopírovať IP adresu", + "ban-ip": "Zablokovať IP adresu", + "view-history": "Upraviť históriu", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Kliknite sem pre návrat k poslednému prečítanému príspevku vo vlákne.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Táto téma bola odstránená. Iba užívatelia s výsadami správcu ju môžu vidieť.", + "following_topic.message": "Odteraz budete prijímať oznámenia, keď niekto prispeje do tejto témy.", + "not_following_topic.message": "Uvidíte túto tému v zozname neprečítaných tém, ale nebudete dostávať oznámenia, keď niekto pridá príspevok do tejto témy.", + "ignoring_topic.message": "Už nebudete naďalej vidieť túto tému v zozname neprečítaných. Budete informovaný, keď sa niekto zmieni o Vašom príspevku alebo mu dá hlas.", + "login_to_subscribe": "Prosím Zaregistrujte sa alebo sa Prihláste, aby ste mohli odoberať túto Tému", + "markAsUnreadForAll.success": "Téma označená ako neprečítaná pre všetkých.", + "mark_unread": "Označiť ako neprečítané", + "mark_unread.success": "Téma označená ako neprečítaná.", + "watch": "Sledovať", + "unwatch": "Prestať sledovať", + "watch.title": "Buďte informovaní o nových odpovediach k tejto téme", + "unwatch.title": "Prestať sledovať túto tému", + "share_this_post": "Zdielať tento príspevok", + "watching": "Sledované", + "not-watching": "Nesledované", + "ignoring": "Ignorované", + "watching.description": "Upozorniť ma na nové odpovede.
Zobraziť tému v neprečítaných.", + "not-watching.description": "Vypnúť upozornenia na nové odpovede.
Zobraziť tému v neprečítaných ak kategória nie je ignorovaná.", + "ignoring.description": "Neupozorňovať na nové upozornenia.
Nezobrazovať témy v neprečítaných.", + "thread_tools.title": "Nástroje témy", + "thread_tools.markAsUnreadForAll": "Označiť ako neprečítané pre všetky", + "thread_tools.pin": "Zviditeľniť tému", + "thread_tools.unpin": "Odstrániť zviditeľnenie témy", + "thread_tools.lock": "Uzamknúť tému", + "thread_tools.unlock": "Odomknúť tému", + "thread_tools.move": "Presunúť tému", + "thread_tools.move-posts": "Presunúť príspevky", + "thread_tools.move_all": "Presunúť všetko", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "Vybrať kategóriu", + "thread_tools.fork": "Rozvetviť tému", + "thread_tools.delete": "Odstrániť tému", + "thread_tools.delete-posts": "Odstrániť príspevky", + "thread_tools.delete_confirm": "Ste si istý že chcete odstrániť túto tému?", + "thread_tools.restore": "Obnoviť tému", + "thread_tools.restore_confirm": "Ste si naozaj istý že chcete obnoviť túto tému?", + "thread_tools.purge": "Vyčistiť tému", + "thread_tools.purge_confirm": "Ste si naozaj istý že chcete vyčistiť túto tému?", + "thread_tools.merge_topics": "Zlúčiť témy", + "thread_tools.merge": "Zlúčiť", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Ste si istý, že chcete odstrániť tento príspevok?", + "post_restore_confirm": "Ste si istí, že chcete obnoviť tento príspevok?", + "post_purge_confirm": "Ste si istý že chcete naozaj vyčistiť tento príspevok?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Načítanie kategórií", + "confirm_move": "Presunúť", + "confirm_fork": "Rozdeliť", + "bookmark": "Záložka", + "bookmarks": "Záložky", + "bookmarks.has_no_bookmarks": "Momentálne nemáte žiadne príspevky v záložkách. ", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Načítavanie ďalších príspevkov", + "move_topic": "Presunúť tému", + "move_topics": "Presunúť témy", + "move_post": "Presunúť príspevok", + "post_moved": "Príspevok presunutý!", + "fork_topic": "Rozdeliť príspevok", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Vyber príspevky, ktoré chceš oddeliť", + "fork_no_pids": "Žiadne príspevky neboli vybraté!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 príspevky(ov) vybraté", + "fork_success": "Rozdelenie témy bolo úspešné! Kliknutím sem sa dostanete na rozdelenú tému", + "delete_posts_instruction": "Kliknite na príspevky, ktoré chcete odstrániť/očistiť", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "Sem zadajte názov témy...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Zahodiť", + "composer.submit": "Odoslať", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Odpovedať na %1", + "composer.new_topic": "Nová téma", + "composer.editing": "Editing", + "composer.uploading": "nahrávanie...", + "composer.thumb_url_label": "Prilep URL náhľadu témy", + "composer.thumb_title": "Pridaj náhľad tejto Témy", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Alebo nahrajte súbor", + "composer.thumb_remove": "Vymazať políčka", + "composer.drag_and_drop_images": "Pretiahni a Pusť Obrázky Sem", + "more_users_and_guests": "%1 užívateľ(ov) a %2 hostí.", + "more_users": "%1 a viac host(í)", + "more_guests": "%1 a ďalší hosť(ia)", + "users_and_others": "%1 a %2 iný", + "sort_by": "Zoradiť podľa", + "oldest_to_newest": "Od najstarších po najnovšie", + "newest_to_oldest": "Od najnovších po najstaršie", + "most_votes": "S najviac hlasmi", + "most_posts": "S najviac príspevkami", + "most_views": "Most Views", + "stale.title": "Vytvoriť novú tému namiesto?", + "stale.warning": "Téma na ktorú odpovedáte je pomerne stará. Chceli by ste vytvoriť novú tému namiesto tejto, a odkazovať na ňu vo Vašej odpovedi?", + "stale.create": "Vytvoriť novú tému", + "stale.reply_anyway": "Napriek tomu odpovedať na túto tému", + "link_back": "Re: [%1](%2)", + "diffs.title": "História úpravy príspevku", + "diffs.description": "Tento príspevok má %1 zmien. Pre zobrazenie obsahu príspevku platného v určitý čas, kliknite nižšie na jednu zo zmien.", + "diffs.no-revisions-description": "Tento príspevok má %1 zmien.", + "diffs.current-revision": "aktuálna revízia", + "diffs.original-revision": "originálna revízia", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/sk/unread.json b/public/language/sk/unread.json new file mode 100644 index 0000000000..62b6ef0c4c --- /dev/null +++ b/public/language/sk/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Neprečítané", + "no_unread_topics": "Nie sú tu žiadne neprečítané témy.", + "load_more": "Načítať viac", + "mark_as_read": "Označiť ako prečítané", + "selected": "Vybrané", + "all": "Všetko", + "all_categories": "Všetky kategórie", + "topics_marked_as_read.success": "Témy boli označené ako prečítané.", + "all-topics": "Všetky témy", + "new-topics": "Nové témy", + "watched-topics": "Sledované témy", + "unreplied-topics": "Nezodpovedané témy", + "multiple-categories-selected": "Viacnásobný výber" +} \ No newline at end of file diff --git a/public/language/sk/uploads.json b/public/language/sk/uploads.json new file mode 100644 index 0000000000..6490b36966 --- /dev/null +++ b/public/language/sk/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Nahrávanie súboru...", + "select-file-to-upload": "Vyberte súbor, ktorý chcete nahrať.", + "upload-success": "Súbor bol úspešne nahraný!", + "maximum-file-size": "Maximálne %1 kb", + "no-uploads-found": "Neboli nájdené žiadne nahrávania", + "public-uploads-info": "Nahrávania sú verejné, všetci návštevnici ich môžu vidieť.", + "private-uploads-info": "Nahrávania sú súkromné, iba prihlásený používatelia ich môžu vidieť." +} \ No newline at end of file diff --git a/public/language/sk/user.json b/public/language/sk/user.json new file mode 100644 index 0000000000..f3800c3ef1 --- /dev/null +++ b/public/language/sk/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Zablokovaný", + "muted": "Muted", + "offline": "Nepripojený", + "deleted": "Odstránené", + "username": "Používateľské meno", + "joindate": "Dátum registrácie", + "postcount": "Počet príspevkov", + "email": "E-mail", + "confirm_email": "Potvrdiť e-mail", + "account_info": "Informácie o účte", + "admin_actions_label": "Administrative Actions", + "ban_account": "Zablokovať účet", + "ban_account_confirm": "Naozaj chcete zablokovať tohto používateľa?", + "unban_account": "Odblokovať účet", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Odstrániť účet", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Účet bol odstránený", + "account-content-deleted": "Account content deleted", + "fullname": "Meno a priezvisko", + "website": "Webová stránka", + "location": "Poloha", + "age": "Vek", + "joined": "Registrovaný", + "lastonline": "Naposledy online", + "profile": "Profil", + "profile_views": "Zobrazenia profilu", + "reputation": "Reputácia", + "bookmarks": "Záložky", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "Sledované", + "ignored": "Ignorovaný", + "default-category-watch-state": "Default category watch state", + "followers": "Nasledovatelia", + "following": "Nasleduje", + "blocks": "Zablokovaný", + "block_toggle": "Prepnúť zablokovanie", + "block_user": "Zablokovať používateľa", + "unblock_user": "Odblokovať používateľa", + "aboutme": "O mne", + "signature": "Podpis", + "birthday": "Dátum narodenia", + "chat": "Konverzácia", + "chat_with": "Konverzácia s %1", + "new_chat_with": "Začať novú konverzáciu s %1", + "flag-profile": "Označiť profil", + "follow": "Nasledovať", + "unfollow": "Prestať sledovať", + "more": "Viac", + "profile_update_success": "Profil bol úspešne aktualizovaný!", + "change_picture": "Zmeniť obrázok", + "change_username": "Zmeniť užívateľské meno", + "change_email": "Zmeniť e-mail", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "Upraviť", + "edit-profile": "Upraviť profil", + "default_picture": "Predvolená ikona", + "uploaded_picture": "Nahraný obrázok", + "upload_new_picture": "Nahrať nový obrázok", + "upload_new_picture_from_url": "Nahrať nový obrázok z URL adresy", + "current_password": "Aktuálne heslo", + "change_password": "Zmeniť heslo", + "change_password_error": "Nesprávne heslo!", + "change_password_error_wrong_current": "Vaše súčasné heslo nie je správne", + "change_password_error_match": "Heslá sa musia zhodovať!", + "change_password_error_privileges": "Nemáte práva na zmenu hesla.", + "change_password_success": "Vaše heslo je aktualizované.", + "confirm_password": "Potvrdenie hesla", + "password": "Heslo", + "username_taken_workaround": "Vaše požadované prihlasovacie meno je už obsadené, tak sme si ho dovolili mierne upraviť. Budeme Vás evidovať ako %1", + "password_same_as_username": "Vaše heslo sa zhoduje s Vaším používateľským menom, prosím zvoľte iné heslo.", + "password_same_as_email": "Vaše heslo sa zhoduje s Vaším e-mailom, prosím zvoľte iné heslo.", + "weak_password": "Slabé heslo.", + "upload_picture": "Nahrať obrázok", + "upload_a_picture": "Nahrať obrázok", + "remove_uploaded_picture": "Vymazať nahraný obrázok", + "upload_cover_picture": "Nahrať titulný obrázok", + "remove_cover_picture_confirm": "Ste si naozaj istý, že chcete odstrániť titulný obrázok?", + "crop_picture": "Orezať obrázok", + "upload_cropped_picture": "Orezať a nahrať", + "avatar-background-colour": "Avatar background colour", + "settings": "Nastavenia", + "show_email": "Zobrazovať môj e-mail", + "show_fullname": "Zobrazovať moje skutočné meno", + "restrict_chats": "Prijímať správy s konverzácií iba od užívateľov ktorých sledujete", + "digest_label": "Prihláste sa na odber ", + "digest_description": "Prihláste sa k e-mailovým novinkám tohto fóra (nové oznámenia a témy) podľa nastavení v rozvrhu.", + "digest_off": "Vypnuté", + "digest_daily": "Denne", + "digest_weekly": "Týždenne", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Mesačne", + "has_no_follower": "Tohto užívateľa nikto nesleduje :(", + "follows_no_one": "Tento užívateľ nikoho nesleduje :(", + "has_no_posts": "Tento užívateľ doteraz nič nezverejnil.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Tento užívateľ doteraz nezverejnil žiadne témy.", + "has_no_watched_topics": "Tento užívateľ zatiaľ nesleduje žiadne témy.", + "has_no_ignored_topics": "Tento používateľ neignoruje žiadne témy.", + "has_no_upvoted_posts": "Tento užívateľ doteraz nedal hlas žiadnemu príspevku.", + "has_no_downvoted_posts": "Tento užívateľ doteraz neodobral hlas žiadnemu príspevku.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Nezablokoval ste žiadneho používateľa.", + "email_hidden": "Skrytý e-mail", + "hidden": "skrytý", + "paginate_description": "Očíslovať témy a príspevky namiesto používania nekonečného rolovania", + "topics_per_page": "Témy na stranu", + "posts_per_page": "Príspevkov na stranu", + "max_items_per_page": "Maximum %1", + "acp_language": "Jazyk stránky správcu", + "notifications": "Notifications", + "upvote-notif-freq": "Frekvencia upozornení na súhlasy", + "upvote-notif-freq.all": "Všetky súhlasy", + "upvote-notif-freq.first": "Prvý podľa príspevku", + "upvote-notif-freq.everyTen": "Každý desiaty súhlas", + "upvote-notif-freq.threshold": "Podľa 1, 5, 10, 25, 50, 100, 150, 200, ...", + "upvote-notif-freq.logarithmic": "Podľa 10, 100, 1000...", + "upvote-notif-freq.disabled": "Zakázané", + "browsing": "Nastavenia prehľadávania", + "open_links_in_new_tab": "Otvárať odchádzajúce odkazy v novom liste", + "enable_topic_searching": "Povoliť vyhľadávanie priamo v téme", + "topic_search_help": "Ak je funkcia povolená, predvolené nastavenia vyhľadávania v prehliadači budú pre nastavené, a umožnia Vám prechádzať cez všetky vstupy, nie iba cez tie, ktoré budú zobrazené na obrazovke", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Po odoslaní odpovede, ukázať nový príspevok", + "follow_topics_you_reply_to": "Sledovať témy na ktoré ste odpovedali", + "follow_topics_you_create": "Sledovať témy ktoré ste vytvorili", + "grouptitle": "Názov skupiny", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "Žiadny názov skupiny", + "select-skin": "Vybrať vzhľad", + "select-homepage": "Vybrať domovskú stránku", + "homepage": "Domovská stránka", + "homepage_description": "Vyberte stránku ktorá bude použitá ako domovská stránka fóra. Pri vybratí 'Žiadna' bude nastavená predvolená domovská stránka.", + "custom_route": "Vlastná cesta pre domovskú stránku", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Služby jednotného prihlasovania", + "sso.associated": "Spojené s", + "sso.not-associated": "Kliknite tu pre spojenie s", + "sso.dissociate": "Odlúčiť", + "sso.dissociate-confirm-title": "Potvrdiť odlúčenie", + "sso.dissociate-confirm": "Ste si istý, že chcete odlúčiť Váš účet z %1?", + "info.latest-flags": "Najnovšie príznaky", + "info.no-flags": "Neboli nájdené žiadne označené príspevky", + "info.ban-history": "Nedávna história zablokovania", + "info.no-ban-history": "Tento člen nebol nikdy zablokovaný", + "info.banned-until": "Zablokovaný až do %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Zablokovaný natrvalo", + "info.banned-reason-label": "Dôvod", + "info.banned-no-reason": "Neboli uvedené žiadne dôvody.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "História užívateľského mena", + "info.email-history": "Hitória e-mailu", + "info.moderation-note": "Zmierňujúca poznámka", + "info.moderation-note.success": "Zmierňujúca poznámka nebola uložená", + "info.moderation-note.add": "Pridať poznámku", + "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.", + "consent.title": "Váš právny súhlas", + "consent.lead": "Toto komunitné fórum zbiera a spracováva Vaše osobné údaje.", + "consent.intro": "Tieto informácie používa iba pre úpravu Vašich skúseností v tejto komunite, rovnako tak k rozpoznaniu príspevkov, ktoré ste pod používateľským účtom vytvoril. Behom jednotlivých registračných krokov budete požiadaný o zadanie Vášho používateľského mena a e-mailovej adresy. Môžete taktiež dobrovoľne poskytnúť niektoré dodatočné informácie do Vášho profilu na webovej stránke. Tieto informácie uchovávame po dobu životnosti Vášho používateľského účtu. Kedykoľvek môžete požiadať kópiu svojich príspevkov na tejto webovej stránke pomocou stránky „Práva a súhlas“

Ak máte nejaké otázky alebo obavy, obráťte sa na tím správcov fóra.", + "consent.email_intro": "Občas Vám zašleme správu na Vašu registrovanú e-mailovú schránku za účelom poskytnutia prehľadu noviniek a/alebo Vám oznámime o nových príspevkoch, ktoré sú pre vás relevantné. Časový prehľad noviniek si môžete kedykoľvek upraviť (prípadne ho zakázať), rovnako tak vybrať, ktoré typy oznámenia chcete dostávať na e-mail. Docielite toho v používateľskom nastavení.", + "consent.digest_frequency": "Ak nie je vo Vašom používateľskom nastavení uvedené inak, táto komunita rozosiela e-mailový prehľad každých %1.", + "consent.digest_off": "Ak nie je vo Vašom používateľskom nastavení uvedené inak, táto komunita nerozosiela e-mailové prehľady", + "consent.received": "Súhlasili ste, že táto stránka môže zhromažďovať a spracovávať informácie o Vás. Žiadny dodatočný úkon nie je potrebný.", + "consent.not_received": "Neposkytli ste súhlas so zberom a spracovaním údajov. V túto chvíľu táto webová stránka a jej tím správcov môže zmazať Váš účet za účelom naplnenia zákona „Všeobecné nariadenia o ochrane osobných údajov (GDPR)“.", + "consent.give": "Dať súhlas", + "consent.right_of_access": "Môžete sa k nám pridať", + "consent.right_of_access_description": "Máte právo overiť si údaje zozbierané touto stránkou. Takúto kópiu údajov získate kliknutím na vhodné tlačidlo nižšie.", + "consent.right_to_rectification": "Máte práva zrušiť svoj súhlas", + "consent.right_to_rectification_description": "Máte právo zmeniť alebo aktualizovať nepresné údaje, ktoré ste nám poskytli. Váš profil môže byť aktualizovaný, obyčajnou editáciou a obsah príspevkov môže byť kedykoľvek upravený. Pokiaľ Vám v tejto chvíli ide o niečo iné, kontaktujte tím správcov tejto stránky.", + "consent.right_to_erasure": "Máte právo byť odstránený", + "consent.right_to_erasure_description": "Kedykoľvek môžete zmeniť svoj súhlas so zhromažďovaním údajov a/alebo spracovaním odstránenia Vášho účtu. Váš profil bude odstránený, hoci Vaše príspevky budú zachované. Ak si prajete odstránenie ako účtu tak aj obsahu, prosím kontaktujte správcov tejto stránky.", + "consent.right_to_data_portability": "Máte právo na prenositeľnosť údajov ", + "consent.right_to_data_portability_description": "Môžete od nás požadovať strojovo čitateľné údaje, ktoré boli zozbierané o Vás a Vašom účte. Urobíte tak kliknutím na tlačidlá zobrazené nižšie.", + "consent.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Exportovať nahraný obsah (*.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Exportovať príspevky (*.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/sk/users.json b/public/language/sk/users.json new file mode 100644 index 0000000000..1e4fa02c93 --- /dev/null +++ b/public/language/sk/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Najnovší používatelia", + "top_posters": "Najaktívnejší", + "most_reputation": "Najváženejší", + "most_flags": "Najviac označované", + "search": "Vyhľadať", + "enter_username": "Zadajte užívateľa k hľadaniu", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Načítať viac", + "users-found-search-took": "%1 užívateľ(ia) sa našli! Vyhľadávanie trvalo %2 sekúnd.", + "filter-by": "Filtrovať podľa", + "online-only": "Iba pripojený", + "invite": "Pozvať", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "E-mailová pozvánka bola odoslaná na adresu %1", + "user_list": "Zoznam používateľov", + "recent_topics": "Nedávne témy", + "popular_topics": "Populárne témy", + "unread_topics": "Neprečítané témy", + "categories": "Kategórie", + "tags": "Značky", + "no-users-found": "Neboli nájdený žiadny užívatelia!" +} \ No newline at end of file diff --git a/public/language/sl/_DO_NOT_EDIT_FILES_HERE.md b/public/language/sl/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/sl/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/sl/admin/admin.json b/public/language/sl/admin/admin.json new file mode 100644 index 0000000000..c107b7a872 --- /dev/null +++ b/public/language/sl/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Ste prepričani, da želite obnoviti in ponovno zagnati NodeBB?", + "alert.confirm-restart": "Ste prepričani, da želite znova zagnati NodeBB?", + + "acp-title": "%1 | NodeBB skrbniška nadzorna plošča", + "settings-header-contents": "Vsebine", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/sl/admin/advanced/cache.json b/public/language/sl/admin/advanced/cache.json new file mode 100644 index 0000000000..f286253a5f --- /dev/null +++ b/public/language/sl/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Predpomnilnik objav", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1%Zasedeno", + "post-cache-size": "Velikost predpomnilnika objav", + "items-in-cache": "Elementi v predpomnilniku" +} \ No newline at end of file diff --git a/public/language/sl/admin/advanced/database.json b/public/language/sl/admin/advanced/database.json new file mode 100644 index 0000000000..a116b29172 --- /dev/null +++ b/public/language/sl/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Čas delovanja v sekundah", + "uptime-days": "Čas delovanja v dneh", + + "mongo": "Mongo", + "mongo.version": "MongoDB verzija", + "mongo.storage-engine": "Pogon za shranjevanje", + "mongo.collections": "Zbirke", + "mongo.objects": "Predmeti", + "mongo.avg-object-size": "Povpr. velikost predmeta", + "mongo.data-size": "Velikost podatkov", + "mongo.storage-size": "Velikost shrambe", + "mongo.index-size": "Velikost indeksa", + "mongo.file-size": "Velikost datoteke", + "mongo.resident-memory": "Stalni spomin", + "mongo.virtual-memory": "Navidezni spomin", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Število zahtev", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB ni mogel poizvedovati po zbirki podatkov MongoDB o ustreznih statističnih podatkih. Prepričajte se, da uporabnik, ki ga uporablja NodeBB, ima vlogo "nadzornika gruče" za "administriranje" zbirke podatkov.", + + "redis": "Redis", + "redis.version": "Redis verzija", + "redis.keys": "Ključi", + "redis.expires": "Poteče", + "redis.avg-ttl": "Povprečni TTL", + "redis.connected-clients": "Povezane stranke", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blokirane stranke", + "redis.used-memory": "Uporabljen pomnilnik", + "redis.memory-frag-ratio": "Razmerje razdrobljenosti pomnilnika", + "redis.total-connections-recieved": "Prejete povezave skupaj", + "redis.total-commands-processed": "Obdelani ukazi skupaj", + "redis.iops": "Takojšnje operacije na sekundo", + "redis.iinput": "Takojšnji vnos na sekundo", + "redis.ioutput": "Takojšnji izhod na sekundo", + "redis.total-input": "Vnos skupaj", + "redis.total-output": "Izhod skupaj", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL verzija", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/sl/admin/advanced/errors.json b/public/language/sl/admin/advanced/errors.json new file mode 100644 index 0000000000..4e5a5ae888 --- /dev/null +++ b/public/language/sl/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Slika %1", + "error-events-per-day": "%1 dogodkov na dan", + "error.404": "4040 ni najdeno", + "error.503": "503 storitev ni na voljo", + "manage-error-log": "Upravljaj dnevnik napak", + "export-error-log": "Izvozi dnevnik napak (CSV)", + "clear-error-log": "Počisti dnevnik napak", + "route": "Pot", + "count": "Število", + "no-routes-not-found": "Hura! Ni napak 404! ", + "clear404-confirm": "Ste prepričani, da želite izbrisati dnevnik napak 404?", + "clear404-success": "Napake \"404 ni najdeno\" so počiščene" +} \ No newline at end of file diff --git a/public/language/sl/admin/advanced/events.json b/public/language/sl/admin/advanced/events.json new file mode 100644 index 0000000000..a06766728e --- /dev/null +++ b/public/language/sl/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Dogodki", + "no-events": "Ni dogodkov", + "control-panel": "Nadzorna plošča za dogodke", + "delete-events": "Izbriši dogodke", + "confirm-delete-all-events": "Ali ste prepričani, da želite izbrisati vse zabeležene dogodke?", + "filters": "Filtri", + "filters-apply": "Uveljavi filtre", + "filter-type": "Tip dogodka", + "filter-start": "Začetni datum", + "filter-end": "Končni datum", + "filter-perPage": "Na stran" +} \ No newline at end of file diff --git a/public/language/sl/admin/advanced/logs.json b/public/language/sl/admin/advanced/logs.json new file mode 100644 index 0000000000..14e4592111 --- /dev/null +++ b/public/language/sl/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Dnevniki", + "control-panel": "Nadzorna plošča dnevnikov", + "reload": "Ponovno naloži dnevnike", + "clear": "Počisti dnevnike", + "clear-success": "Dnevniki so počiščeni!" +} \ No newline at end of file diff --git a/public/language/sl/admin/appearance/customise.json b/public/language/sl/admin/appearance/customise.json new file mode 100644 index 0000000000..744b60dc2e --- /dev/null +++ b/public/language/sl/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "CSS/LESS po meri", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Omogoči CSS/LESS po meri", + + "custom-js": "Javascript po meri", + "custom-js.description": "Tukaj vnesite svoj javascript. Izveden bo, ko se stran popolnoma naloži.", + "custom-js.enable": "Omogoči Javascript po meri", + + "custom-header": "Glava po meri", + "custom-header.description": "Tukaj vnesite HTML po meri (npr. meta oznake itd.), ki bo dodan v & lt; head & gt; razdelek oznak vašega foruma. Oznake skript so dovoljene, vendar niso priporočljive, saj je na voljo zavihek Javascript po meri.", + "custom-header.enable": "Omogoči glavo po meri", + + "custom-css.livereload": "Omogoči ponovno nalaganje v živo", + "custom-css.livereload.description": "Omogočite to, da se vse seje na vsaki napravi v vašem računu osvežijo, ko kliknete shrani" +} \ No newline at end of file diff --git a/public/language/sl/admin/appearance/skins.json b/public/language/sl/admin/appearance/skins.json new file mode 100644 index 0000000000..2d29a09f0d --- /dev/null +++ b/public/language/sl/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Nalagam preobleke...", + "homepage": "Domača stran", + "select-skin": "Izberi preobleko", + "current-skin": "Trenutna preobleka", + "skin-updated": "Preobleka je posodobljena", + "applied-success": "%1 preobleke je bilo uspešno uveljavljene", + "revert-success": "Preobleka je povrnjena v osnovne barve" +} \ No newline at end of file diff --git a/public/language/sl/admin/appearance/themes.json b/public/language/sl/admin/appearance/themes.json new file mode 100644 index 0000000000..3942ae0820 --- /dev/null +++ b/public/language/sl/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Iščem nameščene teme...", + "homepage": "Domača stran", + "select-theme": "Izberi temo", + "current-theme": "Trenutna tema", + "no-themes": "Ni najdenih nameščenih tem", + "revert-confirm": "Ste prepričani, da želite obnoviti privzeto NodeBB temo?", + "theme-changed": "Tema je spremenjena", + "revert-success": "Uspešno ste povrnili vaš NodeBB nazaj na privzeto temo.", + "restart-to-activate": "Za popolno aktivacijo te teme obnovite in ponovno zaženete vaš NodeB." +} \ No newline at end of file diff --git a/public/language/sl/admin/dashboard.json b/public/language/sl/admin/dashboard.json new file mode 100644 index 0000000000..7b8d329e5c --- /dev/null +++ b/public/language/sl/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Promet na forumu", + "page-views": "Ogledi strani", + "unique-visitors": "Edinstveni obiskovalci", + "logins": "Prijave", + "new-users": "Novi uporabniki", + "posts": "Objave", + "topics": "Teme", + "page-views-seven": "Zadnjih 7 dni", + "page-views-thirty": "Zadnjih 30 dni", + "page-views-last-day": "Zadnjih 24 ur", + "page-views-custom": "Časovno obdobje po meri", + "page-views-custom-start": "Začetek obdobja", + "page-views-custom-end": "Konec obdobja", + "page-views-custom-help": "Vnesite časovno obdobje ogledov strani, ki bi si jih radi ogledali. Če izbirnik datumov ni na voljo, je sprejeta oblika LLLL-MM-DD", + "page-views-custom-error": "Vnesite veljavno časovno obdobje v obliki LLLL-MM-DD", + + "stats.yesterday": "Včeraj", + "stats.today": "Danes", + "stats.last-week": "Prejšnji teden", + "stats.this-week": "Ta teden", + "stats.last-month": "Zadnji mesec", + "stats.this-month": "Ta mesec", + "stats.all": "Celotni čas", + + "updates": "Posodobitve", + "running-version": " Teče NodeBB v%1.", + "keep-updated": "Vedno se prepričajte, da je vaš NodeBB posodobljen za najnovejše varnostne popravke in popravke napak.", + "up-to-date": "

Ste na tekočem

", + "upgrade-available": "

Izdana je bila nova različica (v%1). Premislite o posodobitvi vašega NodeBB.

", + "prerelease-upgrade-available": "

To je zastarela predizdajna različica NodeBB. Izšla je nova različica (v%1). Premislite o posodobitvi vašega NodeBB.

", + "prerelease-warning": "

To je predizdajna različica NodeBB. Pojavijo se lahko nenameravane napake.

", + "fallback-emailer-not-found": "Povratnega e-poštnega sporočila ni mogoče najti!", + "running-in-development": "Forum teče v razvojnem načinu. Forum je lahko odprt za potencialne ranljivosti; obrnite se na skrbnika sistema.", + "latest-lookup-failed": "

Najnovejše razpoložljive različice NodeBB ni bilo mogoče najti

", + + "notices": "Opombe", + "restart-not-required": "Ponovni zagon ni potreben", + "restart-required": "Potreben je ponovni zagon", + "search-plugin-installed": "Iskalni vtičnik je nameščen", + "search-plugin-not-installed": "Iskalni vtičnik ni nameščen", + "search-plugin-tooltip": "Za aktiviranje iskalne funkcije namestite iskalni vtičnik s strani vtičnika", + + "control-panel": "Nadzor sistema", + "rebuild-and-restart": "Obnovi & ponovno zaženi", + "restart": "Ponovno zaženi", + "restart-warning": "Obnova ali ponovni zagon vašega NodeBB za nekaj sekund prekine vse obstoječe povezave.", + "restart-disabled": "Obnova in ponovni zagon vašega NodeBB sta onemogočena, saj se zdi, da ga ne izvajate prek ustreznega prikritega procesa.", + "maintenance-mode": "Način vzdrževanja", + "maintenance-mode-title": "Za nastavitev načina vzdrževanja za NodeBB kliknite tukaj", + "realtime-chart-updates": "Posodobitev grafikona v realnem času", + + "active-users": "Aktivni uporabniki", + "active-users.users": "Uporabniki", + "active-users.guests": "Gostje", + "active-users.total": "Skupaj", + "active-users.connections": "Povezave", + + "guest-registered-users": "Gostujoči napram registriranim uporabnikom", + "guest": "Gost", + "registered": "Registrirani", + + "user-presence": "Prisotnost uporabnikov", + "on-categories": "Na seznam kategorij", + "reading-posts": "Branje objav", + "browsing-topics": "Brskanje po temah", + "recent": "Nedavno", + "unread": "Neprebrano", + + "high-presence-topics": "Teme z visoko prisotnostjo", + "popular-searches": "Priljubljena iskanja", + + "graphs.page-views": "Ogledov strani", + "graphs.page-views-registered": "Ogledov strani-registrirani", + "graphs.page-views-guest": "Ogledov strani-gosti", + "graphs.page-views-bot": "Ogledov strani-robot", + "graphs.unique-visitors": "Edinstveni obiskovalci", + "graphs.registered-users": "Registrirani uporabniki", + "graphs.guest-users": "Gostujoči uporabniki", + "last-restarted-by": "Nazadnje ponovno zagnal(a)", + "no-users-browsing": "Ne brska noben uporabnik", + + "back-to-dashboard": "Nazaj na nadzorno ploščo", + "details.no-users": "V izbranem časovnem okviru se ni pridružil noben uporabnik", + "details.no-topics": "V izbranem časovnem okviru ni bila objavljena nobena tema", + "details.no-searches": "Iskanja še niso bila izvedena", + "details.no-logins": "V izbranem časovnem okviru ni bila zabeležena nobena prijava", + "details.logins-static": "NodeBB shranjuje samo podatke o sejah za %1 dni, zato bo ta spodnja tabela prikazala samo zadnje aktivne seje", + "details.logins-login-time": "Čas prijave" +} diff --git a/public/language/sl/admin/development/info.json b/public/language/sl/admin/development/info.json new file mode 100644 index 0000000000..4c469cc598 --- /dev/null +++ b/public/language/sl/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Ste na %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 vozlišč se je odzvalo v %2ms!", + "host": "gostitelj", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "na spletu", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "obremenitev sistema", + "cpu-usage": "uporaba procesorja", + "uptime": "čas delovanja", + + "registered": "Registrirani", + "sockets": "Vtičnice", + "guests": "Gostje", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/sl/admin/development/logger.json b/public/language/sl/admin/development/logger.json new file mode 100644 index 0000000000..ce73544633 --- /dev/null +++ b/public/language/sl/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Nastavitve beleženja", + "description": "Če omogočite potrditvena polja, boste prejemali dnevnike na svoj terminal. Če določite pot, se bodo dnevniki namesto tega shranili v datoteko. Zapisovanje HTTP je uporabno za zbiranje statističnih podatkov o tem, kdo, kdaj in do česa ljudje dostopajo na vašem forumu. Poleg beleženja zahtev HTTP lahko beležimo tudi dogodke socket.io. Zapisovanje Socket.io v kombinaciji z monitorjem redis-cli je lahko v veliko pomoč pri učenju notranjosti NodeBB.", + "explanation": "Preprosto preverite/počistite nastavitve beleženja, če želite omogočiti ali onemogočiti sprotno beleženje. Ponovni zagon ni potreben.", + "enable-http": "Omogoči HTTP prijave", + "enable-socket": "Omogoči beleženje dogodkov socket.io", + "file-path": "Pot do datoteke dnevnika", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Nadzorna plošča beleženja", + "update-settings": "Posodobi nastavitve beleženja" +} \ No newline at end of file diff --git a/public/language/sl/admin/extend/plugins.json b/public/language/sl/admin/extend/plugins.json new file mode 100644 index 0000000000..648edefde3 --- /dev/null +++ b/public/language/sl/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Nameščeno", + "active": "Aktivno", + "inactive": "Neaktivno", + "out-of-date": "zastarelo", + "none-found": "Vtičnikov ni bilo mogoče najti.", + "none-active": "Ni aktivnih vtičnikov.", + "find-plugins": "Najdi vtičnike", + + "plugin-search": "Iskanje vtičnikov", + "plugin-search-placeholder": "Iskanje vtičnika...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Vas zanima pisanje vtičnikov za NodeBB?", + "docs-info": "Celotno dokumentacijo o ustvarjanju vtičnikov najdete v NodeBB dokumentnem portalu.", + + "order.description": "Nekateri vtičniki delujejo idealno, če so inicializirani pred/po drugih vtičnikih.", + "order.explanation": "Vtičniki se nalagajo po vrstnem redu, ki je tukaj naveden, od zgoraj navzdol", + + "plugin-item.themes": "Teme", + "plugin-item.deactivate": "Deaktiviraj", + "plugin-item.activate": "Aktiviraj", + "plugin-item.install": "Namesti", + "plugin-item.uninstall": "Odstrani", + "plugin-item.settings": "Nastavitve", + "plugin-item.installed": "Nameščeno", + "plugin-item.latest": "Najnovejše", + "plugin-item.upgrade": "Posodobi", + "plugin-item.more-info": "Za več informacij:", + "plugin-item.unknown": "Neznano", + "plugin-item.unknown-explanation": "Stanja tega vtičnika ni bilo mogoče določiti, morda zaradi napačne konfiguracije.", + "plugin-item.compatible": "Ta vtičnik deluje na NodeBB %1", + "plugin-item.not-compatible": "Ta vtičnik nima podatkov o združljivosti, preden ga namestite v svoje produkcijsko okolje, se prepričajte, da deluje.", + + "alert.enabled": "Vtičnik omogočen", + "alert.disabled": "Vtičnik onemogočen", + "alert.upgraded": "Vtičnik posodobljen", + "alert.installed": "Vtičnik nameščen", + "alert.uninstalled": "Vtičnik odstranjen", + "alert.activate-success": "Za popolno aktivacijo tega vtičnika obnovite in ponovno zaženete vaš NodeB.", + "alert.deactivate-success": "Vtičnik je bil uspešno deaktiviran", + "alert.upgrade-success": "Za popolno nadgradnjo tega vtičnika obnovite in ponovno zaženete vaš NodeB.", + "alert.install-success": "Vtičnik je uspešno nameščen, aktivirajte ga.", + "alert.uninstall-success": "Vtičnik je bil uspešno deaktiviran in odstranjen.", + "alert.suggest-error": "

NodeBB ni mogel doseči upravitelja paketov, naj nadaljuje z namestitvijo najnovejše različice?

Strežnik je našel (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB ni mogel doseči upravitelja paketov, posodobitev v tem trenutku ni priporočena.

", + "alert.incompatible": "Vaša različica NodeBB (v%1) dovoljuje nadgradnjo tega vtičnika samo na različico v%2. Če želite namestiti novejšo različico tega vtičnika, posodobite svoj NodeBB.

", + "alert.possibly-incompatible": "
Podatke o združljivosti ni mogoče najti

Ta vtičnik ni določil posebne različice za namestitev glede na vašo različico NodeBB. Popolne združljivosti ni mogoče zagotoviti, zato se lahko vaš NodeBB ne zažene več pravilno.

V primeru, da se NodeBB ne zažene pravilno:

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

Ali želite nadaljevati z namestitvijo najnovejše različice tega vtičnika?

", + "alert.reorder": "Vtičniki preurejeni", + "alert.reorder-success": "Prosimo, da za dokončanje postopka v celoti obnovite in znova zaženete NodeBB.", + + "license.title": "Informacija o licenci vtičnika", + "license.intro": "Vtičnik %1 je licenciran pod %2. Preden aktivirate ta vtičnik, preberite in razumite licenčne pogoje.", + "license.cta": "Ali želite nadaljevati z aktiviranjem tega vtičnika?" +} diff --git a/public/language/sl/admin/extend/rewards.json b/public/language/sl/admin/extend/rewards.json new file mode 100644 index 0000000000..48c9762b23 --- /dev/null +++ b/public/language/sl/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Nagrade", + "condition-if-users": "If User's", + "condition-is": "Je:", + "condition-then": "Tedaj:", + "max-claims": "Kolikokrat je mogoče zahtevati nagrado", + "zero-infinite": "Vnesite 0 za neskončno", + "delete": "Izbriši", + "enable": "Omogoči", + "disable": "Onemogoči", + + "alert.delete-success": "Nagrada je uspešno izbrisana", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Nagrada je uspešno shranjena" +} \ No newline at end of file diff --git a/public/language/sl/admin/extend/widgets.json b/public/language/sl/admin/extend/widgets.json new file mode 100644 index 0000000000..6af766bf52 --- /dev/null +++ b/public/language/sl/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Razpoložljivi pripomočki", + "explanation": "V spustnem meniju izberite pripomoček in ga povlecite in spustite v območje gradnikov predloge na levi.", + "none-installed": "Pripomočki niso najdeni! Aktivirajte vtičnik za osnove pripomočkov na nadzorni ploščivtičnikov.", + "clone-from": "Klonirajte pripomočke iz", + "containers.available": "Razpoložljivi vsebniki", + "containers.explanation": "Povlecite in spustite na kateri koli aktivni gradnik", + "containers.none": "Brez", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "plošča", + "container.panel-header": "Glava plošče", + "container.panel-body": "Panel Body", + "container.alert": "Opozorilo", + + "alert.confirm-delete": "Ste prepričani, da želite izbrisati ta pripomoček?", + "alert.updated": "Pripomočki so posodobljeni", + "alert.update-success": "Pripomočki so uspešno shranjeni", + "alert.clone-success": "Pripomočki so uspešno klonirani", + + "error.select-clone": "Izberite stran, s katere želite klonirati", + + "title": "Naslov", + "title.placeholder": "Naslov (vidno le v nekaterih vsebnikih)", + "container": "Vsebnik", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Prikaži skupinam", + "hide-from-groups": "Skrij skupinam", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/admins-mods.json b/public/language/sl/admin/manage/admins-mods.json new file mode 100644 index 0000000000..b1040a7d53 --- /dev/null +++ b/public/language/sl/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Skrbnik", + "global-moderators": "Globalni moderatorji", + "moderators": "Moderators", + "no-global-moderators": "Ni globalnih moderatorjev", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Ni moderatorjev", + "add-administrator": "Dodaj skrbnika", + "add-global-moderator": "Dodaj globalnega moderatorja", + "add-moderator": "Dodaj moderatorja" +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/categories.json b/public/language/sl/admin/manage/categories.json new file mode 100644 index 0000000000..d2bba96e4b --- /dev/null +++ b/public/language/sl/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Nastavitve kategorije", + "privileges": "Privilegiji", + + "name": "Ime kategorije", + "description": "Opis kategorije", + "bg-color": "Barva ozadja", + "text-color": "Barva besedila", + "bg-image-size": "Velikost slike ozadja", + "custom-class": "Razred po meri", + "num-recent-replies": "# nedavnih odgovorov", + "ext-link": "Zunanja povezava", + "subcategories-per-page": "Podkategorij na stran", + "is-section": "Obravnavaj to kategorijo kot sekcijo", + "post-queue": "Čakalna vrsta objav", + "tag-whitelist": "Bela lista oznak", + "upload-image": "Naloži sliko", + "delete-image": "Odstrani", + "category-image": "Slika kategorije", + "parent-category": "Nadrejena kategorija", + "optional-parent-category": "(Izbirno) Nadrejena kategorija", + "top-level": "Vrhnja raven", + "parent-category-none": "(Brez)", + "copy-parent": "Copy Parent", + "copy-settings": "Kopiraj nastavitve iz", + "optional-clone-settings": "(Izbirno) Kloniraj nastavitve iz kategorije", + "clone-children": "Clone Children Categories And Settings", + "purge": "Počisti kategorijo", + + "enable": "Omogoči", + "disable": "Onemogoči", + "edit": "Uredi", + "analytics": "Analitika", + "view-category": "Poglej kategorijo", + "set-order": "Nastavi vrstni red", + "set-order-help": "Če nastavite vrstni red kategorije, se bo ta kategorija premaknila in po potrebi posodobila vrstni red drugih kategorij. Najmanjša št. vrstnega reda je 1, kar kategorijo postavlja na vrh.", + + "select-category": "Izberi kategorijo", + "set-parent-category": "Nastavi nadrejeno kategorijo", + + "privileges.description": "V tem razdelku lahko konfigurirate pravice za nadzor dostopa za dele spletnega mesta. Privilegiji se lahko podelijo uporabniku ali skupini. V spodnjem spustnem meniju izberite področje učinka.", + "privileges.category-selector": "Konfiguriranje privilegijev za", + "privileges.warning": "Opomba: Nastavitve privilegijev pričnejo učinkovati takoj. Po prilagoditvi teh nastavitev kategorije ni potrebno shraniti.", + "privileges.section-viewing": "Pravice ogleda", + "privileges.section-posting": "Pravice za objavo", + "privileges.section-moderation": "Pravice spreminjanja", + "privileges.section-other": "Drugo", + "privileges.section-user": "Uporabnik", + "privileges.search-user": "Dodaj uporabnika", + "privileges.no-users": "V tej kategoriji ni uporabniških pravic.", + "privileges.section-group": "Skupina", + "privileges.group-private": "Ta skupina je zasebna", + "privileges.inheritance-exception": "Ta skupina ne deduje pravic od skupine registriranih uporabnikov", + "privileges.banned-user-inheritance": "Prepovedani uporabniki dedujejo pravice od skupine prepovedanih uporabnikov", + "privileges.search-group": "Dodaj skupino", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Kopiraj iz kategorije", + "privileges.copy-privileges-to-all-categories": "Kopiraj v vse kategorije", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Kopiraj pravice te skupine v vse kategorije.", + "privileges.copy-group-privileges-from": "Kopiraj pravice te skupine iz druge kategorije.", + "privileges.inherit": "Če je skupini registeriranih uporabnikov dodeljena posebna pravica, prejmejo vse druge skupine implicitno pravico, čeprav niso eksplicitno navedene/označene. Ta implicitna pravica se vam prikaže, ker so vsi uporabniki del skupine registriranih uporabnikov, zato pravic za dodatne skupine ni treba izrecno podeliti.", + "privileges.copy-success": "Pravice so kopirane!", + + "analytics.back": "Nazaj na seznam kategorij", + "analytics.title": "Analitika za kategorijo \"%1\"", + "analytics.pageviews-hourly": "Slika 1 – Urni ogledi strani za to kategorijo", + "analytics.pageviews-daily": "Slika 2 – Dnevni ogledi strani za to kategorijo", + "analytics.topics-daily": "Slika 3 – Dnevno ustvarjene teme v tej kategoriji", + "analytics.posts-daily": "Slika 4 – Dnevne objave v tej kategoriji", + + "alert.created": "Ustvarjeno", + "alert.create-success": "Kategorija je uspešno ustvarjena!", + "alert.none-active": "Nimate aktivnih kategorij.", + "alert.create": "Ustvari kategorijo", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Kategorija je počiščena!", + "alert.copy-success": "Nastavitve so kopirane!", + "alert.set-parent-category": "Nastavi nadrejeno kategorijo", + "alert.updated": "Posodobljene kategorije", + "alert.updated-success": "ID -ji kategorij %1 so uspešno posodobljeni.", + "alert.upload-image": "Naloži sliko kategorije", + "alert.find-user": "Poišči uporabnika", + "alert.user-search": "Išči uporabnika tukaj...", + "alert.find-group": "Poišči skupino", + "alert.group-search": "Išči skupino tukaj...", + "alert.not-enough-whitelisted-tags": "Oznak na beli listi je manj od dovoljene spodnje meje, na belo listo dodajte več oznak!", + "collapse-all": "Strni vse", + "expand-all": "Razširi vse", + "disable-on-create": "Onemogoči pri ustvarjanju", + "no-matches": "Ni zadetkov" +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/digest.json b/public/language/sl/admin/manage/digest.json new file mode 100644 index 0000000000..441af32d3c --- /dev/null +++ b/public/language/sl/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "disclaimer": "Upoštevajte, da dostava elektronske pošte zaradi narave tehnologije e-pošte ni zagotovljena. Številne spremenljivke vplivajo na to, ali je e-poštno sporočilo, poslano prejemniškemu strežniku, na koncu dostavljeno v mapo »Prejeto«, vključno z ugledom strežnika, naslovi IP na črnem seznamu in ali je konfiguriran DKIM/SPF/DMARC.", + "disclaimer-continued": "Uspešna dostava pomeni, da je NodeBB uspešno poslal sporočilo in ga je strežnik prejemnika potrdil. To ne pomeni, da je e-poštno sporočilo prispelo v mapo »Prejeto«. Za najboljše rezultate priporočamo uporabo storitev dostave e-pošte tretjih oseb, kot je npr SendGrid.", + + "user": "Uporabnik", + "subscription": "Vrsta naročnine", + "last-delivery": "Zadnja uspešna dostava", + "default": "Privzeta nastavitev sistema", + "default-help": "Privzeta nastavitev sistema pomeni, uporabnik ni izrecno preglasil globalne nastavitve foruma za povzetke, ki je trenutno: "%1"", + "resend": "Ponovno pošlji povzetek", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Dnevni povzetek je ponovno poslan", + "resent-week": "Tedenski povzetek je ponovno poslan", + "resent-biweek": "Dvotedenski povzetek je ponovno poslan", + "resent-month": "Mesečni povzetek je ponovno poslan", + "null": "Nikoli", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/sl/admin/manage/groups.json b/public/language/sl/admin/manage/groups.json new file mode 100644 index 0000000000..f24abd0371 --- /dev/null +++ b/public/language/sl/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Ime skupine", + "badge": "Značka", + "properties": "Lastnosti", + "description": "Opis skupine", + "member-count": "Število članov", + "system": "Sistem", + "hidden": "Skrita", + "private": "Zasebna", + "edit": "Uredi", + "delete": "Izbriši", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Ustvari skupino", + "description-placeholder": "Kratki opis vaše skupine", + "create-button": "Ustvari", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Ste prepričani, da želite izbrisati to skupino?", + + "edit.name": "Ime", + "edit.description": "Opis", + "edit.user-title": "Title of Members", + "edit.icon": "Ikona skupine", + "edit.label-color": "Barva oznake skupine", + "edit.text-color": "Barva besedila skupine", + "edit.show-badge": "Prikaži značko", + "edit.private-details": "Če je omogočeno, je za pridružitev skupinam potrebna odobritev lastnika skupine.", + "edit.private-override": "Opozorilo: Zasebne skupine so onemogočene na sistemski ravni, kar preglasi to možnost.", + "edit.disable-join": "Onemogoči povabila za pridružitev", + "edit.disable-leave": "Ne dovoli uporabnikom, da zapustijo skupino", + "edit.hidden": "Skrito", + "edit.hidden-details": "Če je omogočeno, te skupine ne boste našli na seznamu skupin, uporabnike pa boste morali povabiti ročno", + "edit.add-user": "Dodaj uporabnika v skupino", + "edit.add-user-search": "Iskanje uporabnikov", + "edit.members": "Seznam članov", + "control-panel": "Nadzorna plošča skupine", + "revert": "Povrni", + + "edit.no-users-found": "Uporabnikov ni bilo mogoče najti", + "edit.confirm-remove-user": "Ste prepričani, da želite odstraniti tega uporabnika?", + "edit.save-success": "Spremembe so shranjene!" +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/privileges.json b/public/language/sl/admin/manage/privileges.json new file mode 100644 index 0000000000..b404e7b615 --- /dev/null +++ b/public/language/sl/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Administrator", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Izberi/Počisti vse", + "chat": "Klepet", + "upload-images": "Naloži slike", + "upload-files": "Naloži datoteke", + "signature": "Podpis", + "ban": "Ban", + "mute": "Mute", + "invite": "Povabi", + "search-content": "Išči vsebino", + "search-users": "Išči uporabnike", + "search-tags": "Išči oznake", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Ustvari teme", + "reply-to-topics": "Odgovori na teme", + "schedule-topics": "Schedule Topics", + "tag-topics": "Označi teme", + "edit-posts": "Uredi objave", + "view-edit-history": "Poglej zgodovino urejanja", + "delete-posts": "Izbriši objave", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Izbriši teme", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Kategorije", + "admin-privileges": "Privileges", + "admin-users": "Uporabniki", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Skupine", + "admin-tags": "Oznake", + "admin-settings": "Nastavitve", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/registration.json b/public/language/sl/admin/manage/registration.json new file mode 100644 index 0000000000..3cc776fd0c --- /dev/null +++ b/public/language/sl/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Čakalna vrsta", + "description": "V čakalni vrsti registracij ni nobenega uporabnika.
Da omogočite to funkcionalnost, pojdite na Nastavitve → Uporabnik → Registracija Uporabnika in nastavitev Tip registracijev \"Admin Odobravanje\".", + + "list.name": "Ime", + "list.email": "E-pošta", + "list.ip": "IP", + "list.time": "Čas", + "list.username-spam": "Pogostost: %1 Pojavitve: %2 Samozavest: %3", + "list.email-spam": "Pogostost: %1 Pojavitve: %2", + "list.ip-spam": "Pogostost: %1 Pojavitve: %2", + + "invitations": "Povabila", + "invitations.description": "Spodaj je celotna zbirka poslanih povabil. Uporabite ctrl-f, da iščete skozi zbirko glede na e-poštne naslove in uporabniška imena.

Uporabniško ime bo prikazano desno od e-poštnih naslovov za uporabnike, ki so unovčili povabila.", + "invitations.inviter-username": "Povabil je uporabnik z uporabniškim imenom", + "invitations.invitee-email": "Povabljenčev e-poštni naslov", + "invitations.invitee-username": "Povabljenčevo Uporabniško ime (če je registriran)", + + "invitations.confirm-delete": "Ste prepričani, da želite izbrisati to povabilo?" +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/tags.json b/public/language/sl/admin/manage/tags.json new file mode 100644 index 0000000000..c2774f3284 --- /dev/null +++ b/public/language/sl/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Na vašem forumu še ni nobene teme z oznakami.", + "bg-color": "Barva ozadja", + "text-color": "Barva besedila", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Ustvari oznako", + "modify": "Spremeni oznake", + "rename": "Preimenuj oznake", + "delete": "Izbriši izbrane oznake", + "search": "Iskanje oznak...", + "settings": "Nastavitve oznak", + "name": "Ime oznake", + + "alerts.editing": "Urejanje oznak(e)", + "alerts.confirm-delete": "Ali želite izbrisati izbrane oznake?", + "alerts.update-success": "Oznaka je posodobljena!", + "reset-colors": "Ponastavi barve" +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/uploads.json b/public/language/sl/admin/manage/uploads.json new file mode 100644 index 0000000000..3fc7b46495 --- /dev/null +++ b/public/language/sl/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Naloži datoteko", + "filename": "Ime datoteke", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Velikost / Število datotek", + "confirm-delete": "Ste prepričani, da želite izbrisati to datoteko?", + "filecount": "%1 datotek", + "new-folder": "Nova mapa", + "name-new-folder": "Vnesite ime nove mape" +} \ No newline at end of file diff --git a/public/language/sl/admin/manage/users.json b/public/language/sl/admin/manage/users.json new file mode 100644 index 0000000000..f045790859 --- /dev/null +++ b/public/language/sl/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Uporabniki", + "edit": "Dejanja", + "make-admin": "Nastavi kot skrbnika", + "remove-admin": "Odstrani kot skrbnika", + "validate-email": "Potrdite e-poštni naslov", + "send-validation-email": "Pošljite potrditveno e-sporočilo", + "password-reset-email": "Pošljite e-poštno sporočilo za ponastavitev gesla", + "force-password-reset": "Vsilite ponastavitev gesla in odjavo uporabnika", + "ban": "Prepovejte uporabnika(e)", + "temp-ban": "Začasno prepovejte uporabnika(e)", + "unban": "Razveljavi prepoved uporabnika(ov)", + "reset-lockout": "Ponastavitev zaklepanja", + "reset-flags": "Reset Flags", + "delete": "Izbrišiteuporabnika(e)", + "delete-content": "Izbrišite Vsebino uporabnika(ov)", + "purge": "Izbrišiteuporabnika(e) in vsebino", + "download-csv": "Prenesite CSV", + "manage-groups": "Upravljaj skupine", + "add-group": "Dodaj skupino", + "create": "Create User", + "invite": "Invite by Email", + "new": "Nov uporabnik", + "filter-by": "Filtriraj po", + "pills.unvalidated": "Nepotrjeno", + "pills.validated": "Potrjeno", + "pills.banned": "Prepovedano", + + "50-per-page": "50 na stran", + "100-per-page": "100 na stran", + "250-per-page": "250 na stran", + "500-per-page": "500 na stran", + + "search.uid": "Po ID uporabnika", + "search.uid-placeholder": "Za iskanje vnesite ID uporabnika", + "search.username": "Po imenu uporabnika", + "search.username-placeholder": "Za iskanje vnesite uporabniško ime", + "search.email": "Po e-poštnem naslovu", + "search.email-placeholder": "Za iskanje vnesite e-poštni naslov", + "search.ip": "Po IP naslovu", + "search.ip-placeholder": "Za iskanje vnesite IP naslov", + "search.not-found": "Uporabnika ni bilo mogoče najti!", + + "inactive.3-months": "3 mes.", + "inactive.6-months": "6 mes.", + "inactive.12-months": "12 mes.", + + "users.uid": "uid", + "users.username": "uporabniško ime", + "users.email": "e-poštni naslov", + "users.no-email": "(ni e-poštnega naslova)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "ugled", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "Ime uporabnika", + "create.email": "E-poštni naslov", + "create.email-placeholder": "E-poštni naslov tega uporabnika", + "create.password": "Geslo", + "create.password-confirm": "Potrdi geslo", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Ur", + "temp-ban.days": "Dni", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "Sebe kot skrbnika ne morete odstraniti!", + "alerts.make-admin-success": "Uporabnik je sedaj skrbnik.", + "alerts.confirm-remove-admin": "Ste prepričani, da želite odstraniti tega skrbnika?", + "alerts.remove-admin-success": "Uporabnik ni več skrbnik.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Ali želite potrditi e-poštni(e) naslov(e) tega/teh uporabnika(ov)?", + "alerts.confirm-force-password-reset": "Ali ste prepričani, da želite vsiliti ponastavitev gesla in odjaviti te(ga) uporabnika(e)?", + "alerts.validate-email-success": "E-poštni naslovi so potrjeni", + "alerts.validate-force-password-reset-success": "Gesla uporabnikov so bila ponastavljena in obstoječe seje preklicane.", + "alerts.password-reset-confirm": "Ali želite poslati e-poštno sporočilo za obnovitev gesla temu/tem uporabniku(om)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Opozorilo!

Ali res želite izbrisati uporabnika(e)?

Tega dejanja ni mogoče razveljaviti! Izbrisan bo samo uporabniški račun, njegove objave in teme bodo ostale.

\n", + "alerts.delete-success": "Uporabnik(i) je/so izbrisan(i)!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "Vsebina uporabnika(ov) je izbrisana!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Ustvari uporabnika", + "alerts.button-create": "Ustvari", + "alerts.button-cancel": "Prekliči", + "alerts.error-passwords-different": "Gesli se morata ujemati!", + "alerts.error-x": "Napaka

%1

", + "alerts.create-success": "Uporabnik je ustvarjen!", + + "alerts.prompt-email": "E-poštni naslovi:", + "alerts.email-sent-to": "E -poštno sporočilo s povabilom je bilo poslano %1", + "alerts.x-users-found": "%1 najdenih uporabnik(ov), (%2 sekund)", + "export-users-started": "Izvoz uporabnikov kot CSV lahko traja nekaj časa. Ko bo končano, boste prejeli obvestilo.", + "export-users-completed": "Uporabniki, izvoženi kot CSV, kliknite tukaj za prenos." +} \ No newline at end of file diff --git a/public/language/sl/admin/menu.json b/public/language/sl/admin/menu.json new file mode 100644 index 0000000000..49412fc084 --- /dev/null +++ b/public/language/sl/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Pregled", + "dashboard/logins": "Prijave", + "dashboard/users": "Uporabniki", + "dashboard/topics": "Teme", + "dashboard/searches": "Iskanja", + "section-general": "Splošno", + + "section-manage": "Upravljaj", + "manage/categories": "Kategorije", + "manage/privileges": "Privileges", + "manage/tags": "Oznake", + "manage/users": "Uporabniki", + "manage/admins-mods": "Skrbniki in moderatorji", + "manage/registration": "Čakalna vrsta registracij", + "manage/post-queue": "Čakalna vrsta objav", + "manage/groups": "Skupine", + "manage/ip-blacklist": "IP črna lista", + "manage/uploads": "Nalaganja", + "manage/digest": "Povzetki", + + "section-settings": "Nastavitve", + "settings/general": "Splošno", + "settings/homepage": "Domača stran", + "settings/navigation": "Krmarjenje", + "settings/reputation": "Ugled in zastavice", + "settings/email": "E-pošta", + "settings/user": "Uporabniki", + "settings/group": "Skupine", + "settings/guest": "Gostje", + "settings/uploads": "Nalaganja", + "settings/languages": "Jeziki", + "settings/post": "Objave", + "settings/chat": "Klepeti", + "settings/pagination": "Številčenje strani", + "settings/tags": "Oznake", + "settings/notifications": "Obvestila", + "settings/api": "API dostop", + "settings/sounds": "Zvoki", + "settings/social": "Družbeno", + "settings/cookies": "Piškotki", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Vtičnice", + "settings/advanced": "Napredno", + + "settings.page-title": "%1 nastavitve", + + "section-appearance": "Videz", + "appearance/themes": "Teme", + "appearance/skins": "Preobleke", + "appearance/customise": "Vsebina po meri (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Vtičniki", + "extend/widgets": "Pripomočki", + "extend/rewards": "Nagrade", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Vtičniki", + "extend/plugins.install": "Namesti vtičnike", + + "section-advanced": "Napredno", + "advanced/database": "Podatkovna baza", + "advanced/events": "Dogodki", + "advanced/hooks": "Hooks", + "advanced/logs": "Prijave", + "advanced/errors": "Napake", + "advanced/cache": "Predpomnilnik", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Ponovno zaženi forum", + "logout": "Odjavi se", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "Ni rezultatov...", + "search.search-forum": "Na forumu poišči ", + "search.keep-typing": "Vnesite več, da vidite rezultate...", + "search.start-typing": "Začnite tipkati, da vidite rezultate...", + + "connection-lost": "Povezava z %1 je bila izgubljena, poskus ponovne povezave...", + + "alerts.version": "Teče NodeBB v%1", + "alerts.upgrade": "Nadgradi na v%1" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/advanced.json b/public/language/sl/admin/settings/advanced.json new file mode 100644 index 0000000000..ab1242a9b4 --- /dev/null +++ b/public/language/sl/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Način vzdrževanja", + "maintenance-mode.help": "Ko je forum v načinu vzdrževanja, bodo vse zahteve preusmerjene na statično stran za shranjevanje. Skrbniki so izvzeti iz te preusmeritve in lahko normalno dostopajo do spletnega mesta.", + "maintenance-mode.status": "Koda stanja načina vzdrževanja", + "maintenance-mode.message": "Sporočilo o vzdrževanju", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Glave", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Omogočen HSTS (priporočeno)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "V glavo HSTS vključi poddomene", + "hsts.preload": "Dovoli prednalaganje glave HSTS", + "hsts.help": "Če je omogočeno, bo za to spletno mesto nastavljena glava HSTS. Lahko se odločite za vključitev poddomen in zastavic za vnaprejšnje nalaganje v glavo. Če ste v dvomih, jih lahko pustite neoznačene. Več informacij ", + "traffic-management": "Upravljanje prometa", + "traffic.help": "NodeBB uporablja modul, ki v situacijah z velikim prometom samodejno zavrne zahteve. Tu lahko prilagajate te nastavitve, čeprav so privzete nastavitve dobro izhodišče.", + "traffic.enable": "Omogoči upravljanje prometa", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Z znižanjem te vrednosti se skrajšajo čakalne dobe za nalaganje strani, hkrati pa bo več uporabnikom prikazano sporočilo »čezmerna obremenitev«. (Potreben ponovni zagon)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Privzeto: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "Pri namestitvah z velikim prometom bi se lahko predpomnilnik neprestano izčrpal, če je hkrati aktivnih uporabnikov več kot je največja vrednost predpomnilnika. (Potreben ponovni zagon)", + "compression.settings": "Nastavitve stiskanja", + "compression.enable": "Omogoči stiskanje", + "compression.help": "Ta nastavitev omogoča stiskanje GZIP. Za produkcijsko spletno mesto z velikim prometom je najboljši način za uvedbo stiskanja izvajanje na obratni ravni proxyja. Za namene testiranja ga lahko omogočite tukaj." +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/api.json b/public/language/sl/admin/settings/api.json new file mode 100644 index 0000000000..01306f9cf2 --- /dev/null +++ b/public/language/sl/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Žetoni", + "settings": "Nastavitve", + "lead-text": "Na tej strani lahko konfigurirate dostop do API-ja za pisanje v NodeBB.", + "intro": "API za pisanje privzeto preverja uporabnike na podlagi njihovega piškotka seje, vendar NodeBB podpira tudi preverjanje pristnosti nosilca prek žetonov, ustvarjenih na tej strani.", + "docs": "Kliknite tukaj za dostop do celotne specifikacije API-ja", + + "require-https": "Zahtevaj uporabo API samo prek protokola HTTPS", + "require-https-caveat": "Opomba: Nekatere namestitve, ki vključujejo izravnalnike obremenitve, lahko svoje zahteve posredujejo NodeBB prek protokola HTTP, v tem primeru bi morala ta možnost ostati onemogočena.", + + "uid": "ID uporabnika", + "uid-help-text": "Določite ID uporabnika, ki ga želite povezati s tem žetonom. Če je ID uporabnika 0, bo veljal za glavni žeton, ki lahko prevzame identiteto drugih uporabnikov na podlagi parametra _uid", + "description": "Opis", + "no-description": "Opis ni naveden.", + "token-on-save": "Žeton bo ustvarjen, ko bo obrazec shranjen" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/chat.json b/public/language/sl/admin/settings/chat.json new file mode 100644 index 0000000000..95d7342274 --- /dev/null +++ b/public/language/sl/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Nastavitve klepeta", + "disable": "Onemogoči klepet", + "disable-editing": "Onemogoči urejanje/brisanje sporočila klepeta", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Največja dolžina sporočila klepeta", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/cookies.json b/public/language/sl/admin/settings/cookies.json new file mode 100644 index 0000000000..baea71728d --- /dev/null +++ b/public/language/sl/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU soglasje", + "consent.enabled": "Omogočeno", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Nastavitve", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Največ aktivnih sej na uporabnika", + "blank-default": "Za privzeto pusti prazno" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/email.json b/public/language/sl/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/sl/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/sl/admin/settings/general.json b/public/language/sl/admin/settings/general.json new file mode 100644 index 0000000000..0705ada86f --- /dev/null +++ b/public/language/sl/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Nastavitve spletnega mesta", + "title": "Naslov spletnega mesta", + "title.short": "Kratki naslov", + "title.short-placeholder": "Če kratek naslov ni naveden, bo uporabljen naslov spletnega mesta", + "title.url": "Title Link URL", + "title.url-placeholder": "URL naslova spletnega mesta", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Ime vaše skupnosti", + "title.show-in-header": "V glavi pokaži naslov strani", + "browser-title": "Naslov brskalnika", + "browser-title-help": "Če naslov brskalnika ni naveden, bo uporabljen naslov spletnega mesta", + "title-layout": "Postavitev naslova", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "Kratek opis vaše skupnosti", + "description": "Opis spletne strani", + "keywords": "Ključne besede spletnega mesta", + "keywords-placeholder": "Ključne besede, ki opisujejo vašo skupnost, ločene z vejicami", + "logo": "Logotip spletnega mesta", + "logo.image": "Slika", + "logo.image-placeholder": "Pot do logotipa za prikaz v glavi foruma", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "URL logotipa spletnega mesta", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Odhodne povezave", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Išči", + "search-default-in": "Išči v", + "search-default-in-quick": "Hitro išči v", + "search-default-sort-by": "Razvrsti po", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Metapodatki o barvi spletnega mesta", + "theme-color": "Barva teme", + "background-color": "Barva ozadja", + "background-color-help": "Barva, ki se uporablja za ozadje začetnega zaslona, ​​ko je spletno mesto nameščeno kot PWA", + "undo-timeout": "Razveljavi časovno omejitev", + "undo-timeout-help": "Nekatere operacije, kot so premikanje tem, bodo moderatorju omogočile, da v določenem časovnem okviru razveljavi svoje dejanje. Nastavite na 0, da popolnoma onemogočite razveljavitev.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/sl/admin/settings/group.json b/public/language/sl/admin/settings/group.json new file mode 100644 index 0000000000..33c91eb310 --- /dev/null +++ b/public/language/sl/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Splošno", + "private-groups": "Zasebne skupine", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/guest.json b/public/language/sl/admin/settings/guest.json new file mode 100644 index 0000000000..f0a17f7cfa --- /dev/null +++ b/public/language/sl/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Nastavitve", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/homepage.json b/public/language/sl/admin/settings/homepage.json new file mode 100644 index 0000000000..7198443b3b --- /dev/null +++ b/public/language/sl/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Domača stran", + "description": "Izberite, katera stran se prikaže, ko se uporabniki pomaknejo do korenskega URL-ja vašega foruma.", + "home-page-route": "Pot do domače strani", + "custom-route": "Pot po meri", + "allow-user-home-pages": "Dovoli domače strani uporabnikov", + "home-page-title": "Naslov domače strani (privzeto »Domača stran«)" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/languages.json b/public/language/sl/admin/settings/languages.json new file mode 100644 index 0000000000..311b4af2e7 --- /dev/null +++ b/public/language/sl/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Nastavitve jezika", + "description": "Privzeti jezik določa jezikovne nastavitve za vse uporabnike, ki obiščejo vaš forum.
Posamezni uporabniki lahko na strani z nastavitvami računa preglasijo privzeti jezik.", + "default-language": "Privzeti jezik", + "auto-detect": "Samodejna zaznava nastavitev jezika za goste" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/navigation.json b/public/language/sl/admin/settings/navigation.json new file mode 100644 index 0000000000..fdfe4fce31 --- /dev/null +++ b/public/language/sl/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Ikona:", + "change-icon": "change", + "route": "Pot:", + "tooltip": "Tooltip:", + "text": "Besedilo:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: izbirno", + + "properties": "Lastnosti:", + "groups": "Skupine", + "open-new-window": "Odpri v novem oknu", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Izbriši", + "btn.disable": "Onemogoči", + "btn.enable": "Omogoči", + + "available-menu-items": "Razpoložljivi elementi menija", + "custom-route": "Pot po meri", + "core": "jedro", + "plugin": "vtičnik" +} diff --git a/public/language/sl/admin/settings/notifications.json b/public/language/sl/admin/settings/notifications.json new file mode 100644 index 0000000000..15cc81a25c --- /dev/null +++ b/public/language/sl/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Obvestila", + "welcome-notification": "Obvestilo o dobrodošlici", + "welcome-notification-link": "Povezava do obvestila o dobrodošlici", + "welcome-notification-uid": "Obvestilo o dobrodošlici za uporabnika (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/pagination.json b/public/language/sl/admin/settings/pagination.json new file mode 100644 index 0000000000..fee948d2b5 --- /dev/null +++ b/public/language/sl/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Nastavitve številčenja", + "enable": "Namesto neskončnega drsenja uporabite številčenje tem in objav.", + "posts": "Številčenje objav", + "topics": "Številčenje tem", + "posts-per-page": "Objav na stran", + "max-posts-per-page": "Največ objav na stran", + "categories": "Številčenje kategorij", + "topics-per-page": "Tem na stran", + "max-topics-per-page": "Največ tem na stran", + "categories-per-page": "Kategorij na stran" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/post.json b/public/language/sl/admin/settings/post.json new file mode 100644 index 0000000000..dccdf4f9d5 --- /dev/null +++ b/public/language/sl/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Razvrščanje objav", + "sorting.post-default": "Privzeto razvrščanje objav", + "sorting.oldest-to-newest": "Najstarejše do najnovejše", + "sorting.newest-to-oldest": "Najnovejše do najstarejše", + "sorting.most-votes": "Največ glasov", + "sorting.most-posts": "Največ objav", + "sorting.topic-default": "Privzeto razvrščanje tem", + "length": "Dolžina objave", + "post-queue": "Čakalna vrsta objav", + "restrictions": "Omejitve objavljanja", + "restrictions-new": "Omejitve novega uporabnika", + "restrictions.post-queue": "Omogoči čakalno vrsto objav", + "restrictions.post-queue-rep-threshold": "Da se izogne ​​čakalni vrsti objav je potreben ugled", + "restrictions.groups-exempt-from-post-queue": "Izberite skupine, ki bi morale biti izvzete iz čakalne vrste objav", + "restrictions-new.post-queue": "Omogoči omejitve novega uporabnika", + "restrictions.post-queue-help": "Če omogočite čakalno vrsto objav, bodo objave novih uporabnikov postavljene v čakalno vrsto za odobritev", + "restrictions-new.post-queue-help": "Omogočanje omejitev za nove uporabnike bo postavilo omejitve za objave novih uporabnikov", + "restrictions.seconds-between": "Število sekund med objavami", + "restrictions.seconds-between-new": "Sekunde med objavami za novega uporabnika", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Sekunde, preden lahko nov uporabnik objavi svojo prvo objavo", + "restrictions.seconds-edit-after": "Število sekund, ko je objavo še mogoče urejati (nastavite na 0, da onemogočite)", + "restrictions.seconds-delete-after": "Število sekund, ko je objavo še mogoče izbrisati (nastavite na 0, da onemogočite)", + "restrictions.replies-no-delete": "Število odgovorov, ko uporabnikom ni dovoljeno izbrisati lastnih tem (nastavite na 0, da onemogočite)", + "restrictions.min-title-length": "Najmanjša dolžina naslova", + "restrictions.max-title-length": "Največja dolžina naslova", + "restrictions.min-post-length": "Najmanjša dolžina objave", + "restrictions.max-post-length": "Največja dolžina objave", + "restrictions.days-until-stale": "Število ​​dni, dokler se tema ne šteje za zastarelo", + "restrictions.stale-help": "Če se tema šteje za \"zastarelo\", bo uporabnikom, ki poskušajo odgovoriti na to temo, prikazano opozorilo.", + "timestamp": "Časovni žig", + "timestamp.cut-off": "Mejni datum (v dneh)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Povečanje intervala ogledov teme (v minutah)", + "timestamp.topic-views-interval-help": "Ogledi tem se bodo povečali le enkrat na vsakih X minut, kot je določeno s to nastavitvijo.", + "teaser": "Teaser Post", + "teaser.last-post": "Zadnja – Prikaži najnovejšo objavo, vključno z izvirno, če ni odgovorov", + "teaser.last-reply": "Zadnja – Prikaži najnovejši odgovor ali \"Ni odgovorov\", če ni odgovorov", + "teaser.first": "Prvi", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Neprebrane nastavitve", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Najmanjše število objav v temi pred sledenjem zadnjem branju", + "recent": "Nedavne nastavitve", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Nastavitve podpisa", + "signature.disable": "Onemogoči podpise", + "signature.no-links": "Onemogoči povezave v podpisih", + "signature.no-images": "Onemogoči slike v podpisih", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Največja dolžina podpisa", + "composer": "Nastavitve sestavljalnika", + "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.show-help": "Prikaži zavihek \"Pomoč\"", + "composer.enable-plugin-help": "Dovoli vtičnikom dodajanje vsebine na zavihek za pomoč", + "composer.custom-help": "Besedilo pomoči po meri", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP sledenje", + "ip-tracking.each-post": "Sledi IP naslov za vsako objavo", + "enable-post-history": "Omogoči zgodovino objav" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/reputation.json b/public/language/sl/admin/settings/reputation.json new file mode 100644 index 0000000000..5447842c36 --- /dev/null +++ b/public/language/sl/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Nastavitve ugleda", + "disable": "Onemogoči sistem ugleda", + "disable-down-voting": "Onemogoči glasovanje proti", + "votes-are-public": "Vsi glasovi so javni", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Najmanjši ugled za objavo glasov proti", + "downvotes-per-day": "Glasovi proti na dan (nastavljeno na 0 za neomejeno število glasov proti)", + "downvotes-per-user-per-day": "Glasovi proti na uporabnika na dan (nastavljeno na 0 za neomejeno število glasov proti)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Najmanjši ugled za označevanje objav z zastavico", + "min-rep-website": "Najmanjši ugled za dodajanje \"Spletna stran\" v uporabniški profil", + "min-rep-aboutme": "Najmanjši ugled za dodajanje \"O meni\" v uporabniški profil", + "min-rep-signature": "Najmanjši ugled za dodajanje \"Podpis\" v uporabniški profil", + "min-rep-profile-picture": "Najmanjši ugled za dodajanje \"Profilna slika\" v uporabniški profil", + "min-rep-cover-picture": "Najmanjši ugled za dodajanje \"Naslovna slika\" v uporabniški profil", + + "flags": "Flag Settings", + "flags.limit-per-target": "Največkrat, ko je mogoče nekaj označiti z zastavico", + "flags.limit-per-target-placeholder": "Privzeto: 0", + "flags.limit-per-target-help": "Ko je objava ali uporabnik večkrat označen z zastavico, se vsaka dodatna zastavica šteje za & quot;poročilo" in dodana prvotni zastavici. To možnost nastavite na število, različno od nič, da omejite število poročil, ki jih element lahko prejme.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/social.json b/public/language/sl/admin/settings/social.json new file mode 100644 index 0000000000..f01d398aa1 --- /dev/null +++ b/public/language/sl/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Deljenje objav", + "info-plugins-additional": "Vtičniki lahko dodajo dodatna omrežja za deljenje objav.", + "save-success": "Uspešno shranjena omrežja za deljenje objav!" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/sockets.json b/public/language/sl/admin/settings/sockets.json new file mode 100644 index 0000000000..a47145831d --- /dev/null +++ b/public/language/sl/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Nastavitve vzpostavitve ponovne povezave", + "max-attempts": "Največ poskusov vzpostavitve ponovne povezave", + "default-placeholder": "Privzeto: %1", + "delay": "Zamuda pri vzpostavitvi ponovne povezave" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/sounds.json b/public/language/sl/admin/settings/sounds.json new file mode 100644 index 0000000000..0b20dfc83f --- /dev/null +++ b/public/language/sl/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Obvestila", + "chat-messages": "Sporočila klepeta", + "play-sound": "Predvajaj", + "incoming-message": "Dohodno sporočilo", + "outgoing-message": "Odhodno sporočilo", + "upload-new-sound": "Naloži nov zvok", + "saved": "Nastavitve so shranjene" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/tags.json b/public/language/sl/admin/settings/tags.json new file mode 100644 index 0000000000..3134921790 --- /dev/null +++ b/public/language/sl/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Nastavitve oznak", + "link-to-manage": "Upravljaj oznake", + "system-tags": "System Tags", + "system-tags-help": "Ze oznake bodo lahko uporabljali le privilegirani uporabniki.", + "min-per-topic": "Najmanj oznak na temo", + "max-per-topic": "Največ oznak na temo", + "min-length": "Najmanjša dolžina oznake", + "max-length": "Največja dolžina oznake", + "related-topics": "Sorodne teme", + "max-related-topics": "Največ sorodnih tem za prikaz (če jih tema podpira)" +} \ No newline at end of file diff --git a/public/language/sl/admin/settings/uploads.json b/public/language/sl/admin/settings/uploads.json new file mode 100644 index 0000000000..25a0394722 --- /dev/null +++ b/public/language/sl/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Objave", + "orphans": "Orphaned Files", + "private": "Naložene datoteke označi kot zasebne", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Dovoljene pripone datoteke", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Naloži", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/sl/admin/settings/user.json b/public/language/sl/admin/settings/user.json new file mode 100644 index 0000000000..31a7f090c0 --- /dev/null +++ b/public/language/sl/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Preverjanje pristnosti", + "email-confirm-interval": "Uporabnik morda ne bo mogel znova poslati potrditvenega e-poštnega sporočila, dokler", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Dovoli prijavo z", + "allow-login-with.username-email": "Uporabniško ime ali e-poštni naslov", + "allow-login-with.username": "Samo uporabniško ime", + "account-settings": "Nastavitve računa", + "gdpr_enabled": "Omogoči zbiranje GDPR soglasij", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Onemogoči spreminjanje uporabniškega imena", + "disable-email-changes": "Onemogoči spreminjanje e-poštnega naslova", + "disable-password-changes": "Onemogoči spreminjanje gesla", + "allow-account-deletion": "Dovoli brisanje računa", + "hide-fullname": "Skrij polno ime pred uporabniki", + "hide-email": "Skrij e-poštni naslov pred uporabniki", + "show-fullname-as-displayname": "Prikaži uporabnikovo polno ime kot njegovo prikazno ime, če je na voljo", + "themes": "Teme", + "disable-user-skins": "Prepreči uporabnikom izbiro preobleke po meri", + "account-protection": "Zaščita računa", + "admin-relogin-duration": "Trajanje ponovne prijave skrbnika (v minutah)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Število poskusov prijave na uro", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Trajanje zaklepanja računa (v minutah)", + "login-days": "Dnevi za zapomnitev sej za prijavo uporabnikov", + "password-expiry-days": "Vsilite ponastavitev gesla po nastavljenem številu dni", + "session-time": "Čas seje", + "session-time-days": "Dni", + "session-time-seconds": "Sekund", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minut po tem, ko je uporabnik neaktiven", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "Registracija uporabnika", + "registration-type": "Vrsta registracije", + "registration-approval-type": "Vrsta odobritve registracije", + "registration-type.normal": "Običajno", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Samo povabilo", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Ure do samodejne potrditve uporabnika. 0 da onemogočite.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Največje število povabil na uporabnika", + "max-invites": "Največje število povabil na uporabnika", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "Število dni, v katerih poteče povabilo.", + "min-username-length": "Najmanjša dolžina uporabniškega imena", + "max-username-length": "Največja dolžina uporabniškega imena", + "min-password-length": "Najmanjša dolžina gesla", + "min-password-strength": "Najmanjša moč gesla", + "max-about-me-length": "Največja dolžina O meni", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Pokaži e-poštni naslov", + "show-fullname": "Pokaži polno ime", + "restrict-chat": "V klepetu dovoli samo sporočila uporabnikov, ki jih spremljam", + "outgoing-new-tab": "Zunanje povezave odpri na novem zavihku", + "topic-search": "Omogoči iskanje v temi", + "update-url-with-post-index": "Med brskanjem po temah posodobite URL z indeksom objav", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Dnevno", + "digest-freq.weekly": "Tedensko", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Mesečno", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Spremljanj teme, ki si jih ustvaril", + "follow-replied-topics": "Spremljanj teme, na katere si odgovoril", + "default-notification-settings": "Privzete nastavitve obveščanja", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Spremljano", + "categoryWatchState.notwatching": "Ni spremljano", + "categoryWatchState.ignoring": "Prezrto" +} diff --git a/public/language/sl/admin/settings/web-crawler.json b/public/language/sl/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/sl/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/sl/category.json b/public/language/sl/category.json new file mode 100644 index 0000000000..e96e8805f9 --- /dev/null +++ b/public/language/sl/category.json @@ -0,0 +1,23 @@ +{ + "category": "Od sedaj naprej spremljate posodobitve te kategorije in njenih podkategorij.\nOd sedaj naprej ne spremljate posodobitev te kategorije in njenih podkategorij.\n", + "subcategories": "Podkategorije", + "new_topic_button": "Nova tema", + "guest-login-post": "Prijava", + "no_topics": "V tej kategoriji ni tem.", + "browsing": "Brskanje", + "no_replies": "Nihče ni odgovoril.", + "no_new_posts": "Ni novih objav.", + "watch": "Spremljaj.", + "ignore": "Prezri.", + "watching": "Spremljano", + "not-watching": "Ni spremljano", + "ignoring": "Prezrto", + "watching.description": "Prikaži teme v nedavno in nazadnje", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "Spremljane kategorije", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/sl/email.json b/public/language/sl/email.json new file mode 100644 index 0000000000..cd775ca7e0 --- /dev/null +++ b/public/language/sl/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Dobrodošli na forumu %1!", + "invite": "Povabilo uporabnika %1", + "greeting_no_name": "Pozdravljeni!", + "greeting_with_name": "Pozdravljeni, %1!", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Hvala, da ste se registrirali na forumu %1!", + "welcome.text2": "Pred aktiviranjem vašega računa moramo preveriti lastništvo elektronskega naslova, s katerim ste se registrirali.", + "welcome.text3": "Skrbnik je sprejel vašo registracijo. Sedaj se lahko prijavite s svojim uporabniškim imenom in geslom.", + "welcome.cta": "Kliknite tu za potrditev svojega elektronskega naslova.", + "invitation.text1": "%1 te je povabil/-a, da se pridružiš forumu %2.", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Dobili smo zahtevo za ponastavitev vašega gesla. Če niste zahtevali ponastavitve gesla, prosimo, da prezrete to sporočilo.", + "reset.text2": "Za nadaljevanje ponastavitve gesla prosimo, da kliknete na naslednjo povezavo:", + "reset.cta": "Kliknite tu za ponastavitev gesla.", + "reset.notify.subject": "Geslo je bilo uspešno spremenjeno.", + "reset.notify.text1": "Obveščamo vas, da je bilo na forumu %1 uspešno spremenjeno vaše geslo.", + "reset.notify.text2": "Če tega niste zahtevali, prosimo, da nemudoma obvestite skrbnika.", + "digest.latest_topics": "Zadnje teme na forumu %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Kliknite tu za obisk foruma %1.", + "digest.unsub.info": "Ta povzetek vam je bil poslan zaradi nastavitev vaše naročnine.", + "digest.day": "Dan", + "digest.week": "Teden", + "digest.month": "Mesec", + "digest.subject": "Povzetek za %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Uporabnik %1 vam je poslal novo sporočilo za klepet.", + "notif.chat.cta": "Kliknite tu za nadaljevanje pogovora.", + "notif.chat.unsub.info": "Obvestilo o klepetu vam je bilo poslano zaradi nastavitev vaše naročnine.", + "notif.post.unsub.info": "Obvestilo o objavi vam je bilo poslano zaradi nastavitev vaše naročnine.", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "To je testno elektronsko sporočilo za preverjanje pravilnosti nastavitev podsistema za pošiljanje NodeBB poštnih sporočil.", + "unsub.cta": "Kliknite tu za spremembo nastavitev.", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "You have been banned from %1", + "banned.text1": "The user %1 has been banned from %2.", + "banned.text2": "This ban will last until %1.", + "banned.text3": "This is the reason why you have been banned:", + "closing": "Hvala!" +} \ No newline at end of file diff --git a/public/language/sl/error.json b/public/language/sl/error.json new file mode 100644 index 0000000000..20f0be3469 --- /dev/null +++ b/public/language/sl/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Napačni podatki", + "invalid-json": "Invalid JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Niste prijavljeni.", + "account-locked": "Vaš račun je bil začasno zaklenjen.", + "search-requires-login": "Iskanje zahteva uporabniški račun - prosimo, da se prijavite ali registrirate.", + "goback": "Press back to return to the previous page", + "invalid-cid": "Napačen ID kategorije", + "invalid-tid": "Napačen ID teme", + "invalid-pid": "Napačen ID objave", + "invalid-uid": "Napačen ID uporabnika", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Napačno uporabniško ime", + "invalid-email": "Napačen elektronski naslov", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Napačen naslov", + "invalid-user-data": "Napačni podatki o uporabniku", + "invalid-password": "Napačno geslo", + "invalid-login-credentials": "Invalid login credentials", + "invalid-username-or-password": "Prosimo, vpišite uporabniško ime in geslo.", + "invalid-search-term": "Napačen iskalni izraz", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "Prijava ni mogoča, verjetno zaradi potekle seje. Poskusite znova.", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Napačna vrednost za številčenje strani. Vrednost mora biti najmanj %1 in največ %2.", + "username-taken": "Uporabniško ime je že zasedeno.", + "email-taken": "E-poštni naslov je že zaseden.", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Ne morete klepetati, dokler ne potrdite svojega e-poštnega naslova. Prosimo, kliknite tu za potrditev e-poštnega naslova.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Potrditev vašega e-poštnega naslova ni uspela. Prosimo, poskusite ponovno pozneje.", + "confirm-email-already-sent": "Potrditveno e-sporočilo je že bilo poslano. Prosimo, počakajte %1 min za ponovno pošiljanje.", + "sendmail-not-found": "Ne najdem izvršljive datoteke za pošiljanje e-pošte. Prepričajte se, da je ta nameščena in izvršljiva prek uporabnika, ki izvaja NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Uporabniško ime je prekratko.", + "username-too-long": "Uporabniško ime je predolgo.", + "password-too-long": "Geslo je predolgo.", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Uporabnik je izločen.", + "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", + "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-too-new": " Pred svojo prvo objavo počakajte %1 s.", + "blacklisted-ip": "Vaš IP-naslov je izločen. Povprašajte skrbnika za več informacij.", + "ban-expiry-missing": "Vnesite končni datum za to izločitev.", + "no-category": "Kategorija ne obstaja.", + "no-topic": "Tema ne obstaja.", + "no-post": "Objava ne obstaja.", + "no-group": "Skupina ne obstaja.", + "no-user": "Uporabnik ne obstaja.", + "no-teaser": "Predogled ne obstaja.", + "no-flag": "Flag does not exist", + "no-privileges": "Nimate dovolj pravic za to dejanje.", + "category-disabled": "Kategorija je onemogočena.", + "topic-locked": "Tema je zaklenjena.", + "post-edit-duration-expired": "Urejanje objave je dovoljeno le %1 s po objavi.", + "post-edit-duration-expired-minutes": "Urejanje objave je dovoljeno le %1 min po objavi.", + "post-edit-duration-expired-minutes-seconds": "Urejanje objave je dovoljeno le %1 min in %2 s po objavi.", + "post-edit-duration-expired-hours": "Urejanje objave je dovoljeno le %1 h po objavi.", + "post-edit-duration-expired-hours-minutes": "Urejanje objave je dovoljeno le %1 h in %2 min po objavi.", + "post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting", + "post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting", + "post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting", + "post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting", + "post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting", + "post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting", + "post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting", + "post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting", + "post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting", + "cant-delete-topic-has-reply": "Teme, ki ima odgovor, ni mogoče izbrisati.", + "cant-delete-topic-has-replies": "Število odgovorov, ko teme ni mogoče izbrisati: %1", + "content-too-short": "Prosimo, napišite daljšo objavo. Obvezno število znakov: vsaj %1.", + "content-too-long": "Prosimo, napišite krajšo objavo. Največje število znakov: %1.", + "title-too-short": "Prosimo, vnesite daljši naslov. Obvezno število znakov: vsaj %1.", + "title-too-long": "Prosimo, napišite krajši naslov. Največje število znakov: %1.", + "category-not-selected": "Category not selected.", + "too-many-posts": "Objavljate lahko na %1 s - prosimo, počakajte pred novo objavo.", + "too-many-posts-newbie": "Kot nov uporabnik lahko objavljate le na %1 s, dokler ne dosežete ugled vsaj %2 - prosimo, počakajte pred novo objavo.", + "already-posting": "You are already posting", + "tag-too-short": "Prosimo, vnesite daljšo oznako. Obvezno število znakov: vsaj %1.", + "tag-too-long": "Prosimo, vnesite krajšo oznako. Največje število znakov: %1.", + "not-enough-tags": "Ni dovolj oznak. Obvezno število oznak: %1. ", + "too-many-tags": "Preveč oznak. Največje število oznak: %1.", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Prosimo, počakajte, da se prenosi končajo.", + "file-too-big": "Največja dovoljena velikost datoteke je %1 kB - prosimo, naložite manjšo datoteko.", + "guest-upload-disabled": "Gostom je prenašanje onemogočeno.", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "You have already bookmarked this post", + "already-unbookmarked": "You have already unbookmarked this post", + "cant-ban-other-admins": "Ne morete izločati drugih skrbnikov!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Ste edini skrbnik. Preden se odstranite, dodajte novega skrbnika.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Pred brisanjem tega računa morate odstraniti skrbniške pravice.", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "Nedovoljen format slike. Dovoljeni formati: %1.", + "invalid-image-extension": "Nedovoljena pripona slike.", + "invalid-file-type": "Nedovoljena vrsta datoteke. Dovoljene vrste: %1.", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Ime skupine je prekratko.", + "group-name-too-long": "Ime skupine je predolgo.", + "group-already-exists": "Skupina že obstaja.", + "group-name-change-not-allowed": "Sprememba imena skupine ni dovoljena.", + "group-already-member": "Že član te skupine", + "group-not-member": "Ni član te skupine", + "group-needs-owner": "Ta skupina potrebuje vsaj enega skrbnika.", + "group-already-invited": "Ta uporabnik je že bil povabljen.", + "group-already-requested": "Vaša prošnja za članstvo je že bila sprejeta.", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Ta objava je že bila izbrisana.", + "post-already-restored": "Ta objava je že bila obnovljena.", + "topic-already-deleted": "Ta tema je že bila izbrisana.", + "topic-already-restored": "Ta tema je že bila obnovljena.", + "cant-purge-main-post": "Ne morete odstraniti prve objave, prosimo, izbrišite temo.", + "topic-thumbnails-are-disabled": "Sličice teme so onemogočene.", + "invalid-file": "Nedovoljena datoteka", + "uploads-are-disabled": "Prenosi so onemogočeni.", + "signature-too-long": "Vaš podpis je predolg. Največje število znakov: %1.", + "about-me-too-long": "Rubrika O meni je predolga. Največje število znakov: %1.", + "cant-chat-with-yourself": "Ne morete klepetati s seboj!", + "chat-restricted": "Uporabnik je omejil klepetanje. Za možnost klepeta vas mora uporabnik spremljati.", + "chat-disabled": "Klepet je onemogočen.", + "too-many-messages": "Poslali ste preveč sporočil, prosimo, počakajte nekaj časa.", + "invalid-chat-message": "Neveljavno sporočilo klepeta", + "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "cant-edit-chat-message": "Nimate dovoljenja za urejanje tega sporočila.", + "cant-delete-chat-message": "NImate dovoljenja za izbris tega sporočila.", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Za to objavo ste že glasovali.", + "reputation-system-disabled": "Sistem za ugled je onemogočen.", + "downvoting-disabled": "Negativno glasovanje je onemogočeno.", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB je zaznal težavo pri osveževanju: ", + "registration-error": "Napaka pri registraciji", + "parse-error": "Nekaj je šlo narobe pri pridobivanju odgovora s strežnika.", + "wrong-login-type-email": "Uporabite svoj e-poštni naslov za prijavo.", + "wrong-login-type-username": "Uporabite svoje uporabniško ime za prijavo.", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "Povabili ste največje dovoljeno število ljudi (%1 od %2).", + "no-session-found": "Prijavne seje ni mogoče najti!", + "not-in-room": "Uporabnika ni v sobi.", + "cant-kick-self": "Sebe ne morete umakniti iz skupine.", + "no-users-selected": "Ni izbranih uporabnikov.", + "invalid-home-page-route": "Napačna pot do domače strani.", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/sl/flags.json b/public/language/sl/flags.json new file mode 100644 index 0000000000..71e66d821e --- /dev/null +++ b/public/language/sl/flags.json @@ -0,0 +1,89 @@ +{ + "state": "State", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Hooray! No flags found.", + "assignee": "Assignee", + "update": "Update", + "updated": "Posodobljeno", + "resolved": "Resolved", + "target-purged": "The content this flag referred to has been purged and is no longer available.", + + "graph-label": "Daily Flags", + "quick-filters": "Quick Filters", + "filter-active": "There are one or more filters active in this list of flags", + "filter-reset": "Remove Filters", + "filters": "Možnosti filtra", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Flag Type", + "filter-type-all": "All Content", + "filter-type-post": "Objava", + "filter-type-user": "Uporabnik", + "filter-state": "State", + "filter-assignee": "Assignee UID", + "filter-cid": "Kategorija", + "filter-quick-mine": "Dodeljeno meni", + "filter-cid-all": "Vse kategorije", + "apply-filters": "Uveljavi filtre", + "more-filters": "Več filtrov", + "fewer-filters": "Manj filtrov", + + "quick-actions": "Hitra dejanja", + "flagged-user": "Flagged User", + "view-profile": "Poglej profil", + "start-new-chat": "Začni nov klepet", + "go-to-target": "View Flag Target", + "assign-to-me": "Dodeli meni", + "delete-post": "Izbriši objavo", + "purge-post": "Purge Post", + "restore-post": "Obnovi objavo", + "delete": "Delete Flag", + + "user-view": "Poglej profil", + "user-edit": "Uredi profil", + + "notes": "Flag Notes", + "add-note": "Dodaj opombo", + "no-notes": "Ni deljenih opomb.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Opomba dodana", + "note-deleted": "Opomba izbrisana", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "No flag history.", + + "state-all": "All states", + "state-open": "Nov/Odpri", + "state-wip": "Work in Progress", + "state-resolved": "Resolved", + "state-rejected": "Zavrnjeno", + "no-assignee": "Ni dodeljeno", + + "sort": "Razvrsti po", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Največ glasov proti", + "sort-upvotes": "Največ glasov za", + "sort-replies": "Največ odgovorov", + + "modal-title": "Report Content", + "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Offensive", + "modal-reason-other": "Other (specify below)", + "modal-reason-custom": "Reason for reporting this content...", + "modal-submit": "Submit Report", + "modal-submit-success": "Content has been flagged for moderation.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/sl/global.json b/public/language/sl/global.json new file mode 100644 index 0000000000..ebea071723 --- /dev/null +++ b/public/language/sl/global.json @@ -0,0 +1,126 @@ +{ + "home": "Domov", + "search": "Iskanje", + "buttons.close": "Zapri", + "403.title": "Dostop zavrnjen", + "403.message": "Kaže, da ste naleteli na stran, za katero nimate dovoljenja.", + "403.login": "Morda bi se raje prijavili?", + "404.title": "Tega ni bilo mogoče najti.", + "404.message": "Kaže, da ste naleteli na stran, ki ne obstaja. Vrnite se na začetno stran.", + "500.title": "Interna napaka", + "500.message": "Ups! Nekaj je šlo narobe!", + "400.title": "Napačna zahteva", + "400.message": "It looks like this link is malformed, please double-check and try again. Otherwise, return to the home page.", + "register": "Registracija", + "login": "Prijava", + "please_log_in": "Prijavite se.", + "logout": "Odjava", + "posting_restriction_info": "Objavljanje je trenutno omogočeno le registriranim članom, kliknite tu za prijavo.", + "welcome_back": "Dobrodošli nazaj!", + "you_have_successfully_logged_in": "Uspešno ste se prijavili.", + "save_changes": "Shrani spremembe.", + "save": "Shrani", + "close": "Zapri", + "pagination": "Oštevilčenje strani", + "pagination.out_of": "%1 od %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Skrbnik", + "header.categories": "Kategorije", + "header.recent": "Nedavno", + "header.unread": "Neprebrano", + "header.tags": "Oznake", + "header.popular": "Priljubljeno", + "header.top": "Top", + "header.users": "Uporabniki", + "header.groups": "Skupine", + "header.chats": "Klepeti", + "header.notifications": "Obvestila", + "header.search": "Iskanje", + "header.profile": "Profil", + "header.navigation": "Krmarjenje", + "notifications.loading": "Nalaganje obvestil", + "chats.loading": "Nalaganje klepetov", + "motd.welcome": "Pozdravljeni v NodeBB, pogovorno platformo prihodnosti.", + "previouspage": "Prejšnja stran", + "nextpage": "Naslednja stran", + "alert.success": "Uspešno", + "alert.error": "Napaka", + "alert.banned": "Izločen", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Ne spremljate več %1!", + "alert.follow": "Sedaj spremljate %1!", + "users": "Uporabniki", + "topics": "Teme", + "posts": "Objave", + "x-posts": "%1 posts", + "best": "Najboljše", + "controversial": "Controversial", + "votes": "Votes", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Glasovalcev za", + "upvoted": "Glasov za", + "downvoters": "Glasovalcev proti", + "downvoted": "Glasov proti", + "views": "Ogledov", + "posters": "Posters", + "reputation": "Ugled", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "preberi več", + "more": "Več", + "none": "None", + "posted_ago_by_guest": "objavil %1 kot Gost", + "posted_ago_by": "objavljeno %1 od %2", + "posted_ago": "objavil %1", + "posted_in": "objavljeno v %1", + "posted_in_by": "objavljeno v %1 od %2", + "posted_in_ago": "objavljeno v %1 %2", + "posted_in_ago_by": "objavljeno v %1 %2 od %3", + "user_posted_ago": "%1 je objavil %2", + "guest_posted_ago": "Gost je objavil %1.", + "last_edited_by": "Zadnje urejanje: %1", + "norecentposts": "Ni nedavnih objav.", + "norecenttopics": "Ni nedavnih tem.", + "recentposts": "Nedavne objave", + "recentips": "Nedavni prijavljeni IP-ji", + "moderator_tools": "Moderator Tools", + "online": "Dosegljiv", + "away": "Odsoten", + "dnd": "Ne moti", + "invisible": "Neviden", + "offline": "Nedosegljiv", + "email": "E-pošta", + "language": "Jezik", + "guest": "Gost", + "guests": "Gosti", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forum je posodobljen.", + "updated.message": "Forum je bil pravkar posodobljen na zadnjo različico. Kliknite tu za osvežitev strani.", + "privacy": "Zasebnost", + "follow": "Spremljaj", + "unfollow": "Prekliči spremljanje", + "delete_all": "Izbriši vse", + "map": "Zemljevid", + "sessions": "Prijavne seje", + "ip_address": "Naslov IP", + "enter_page_number": "Vnesi številko strani", + "upload_file": "Prenesi datoteko", + "upload": "Prenos", + "uploads": "Uploads", + "allowed-file-types": "Dovoljene vrste datotek: %1", + "unsaved-changes": "Nekatere spremembe niso shranjene. A res želite zapustiti stran?", + "reconnecting-message": "Kaže, da je bila povezava s/z %1 prekinjena. Prosimo, počakajte, ponovno poskušamo vzpostaviti povezavo.", + "play": "Play", + "cookies.message": "This website uses cookies to ensure you get the best experience on our website.", + "cookies.accept": "Got it!", + "cookies.learn_more": "Learn More", + "edited": "Edited", + "disabled": "Disabled", + "select": "Select", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/sl/groups.json b/public/language/sl/groups.json new file mode 100644 index 0000000000..880aa8e302 --- /dev/null +++ b/public/language/sl/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Skupine", + "view_group": "Poglej skupino", + "owner": "Lastnik skupine", + "new_group": "Ustvari novo skupino", + "no_groups_found": "Ni skupin", + "pending.accept": "Sprejmi", + "pending.reject": "Zavrni", + "pending.accept_all": "Sprejmi vse", + "pending.reject_all": "Zavrni vse", + "pending.none": "Ni čakajočih uporabnikov", + "invited.none": "Ni povabljenih uporabnikov", + "invited.uninvite": "Prekliči povabilo", + "invited.search": "Poišči uporabnika za povabilo v skupino", + "invited.notification_title": "You have been invited to join %1", + "request.notification_title": "Group Membership Request from %1", + "request.notification_text": "%1 has requested to become a member of %2", + "cover-save": "Shrani", + "cover-saving": "Shranjevanje", + "details.title": "Podatki o skupini", + "details.members": "Seznam članov", + "details.pending": "Čakajoči člani", + "details.invited": "Povabljeni člani", + "details.has_no_posts": "Člani skupine še niso objavljali.", + "details.latest_posts": "Zadnje objave", + "details.private": "Zasebno", + "details.disableJoinRequests": "Onemogoči zahteve za pridružitev.", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "Dodeli/Prekliči lastništvo", + "details.kick": "Odstrani iz skupine", + "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.add-member": "Add Member", + "details.owner_options": "Administratorji skupine", + "details.group_name": "Ime skupine", + "details.member_count": "Število članov", + "details.creation_date": "Datum nastanka", + "details.description": "Opis skupine", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Predogled značke", + "details.change_icon": "Zamenjaj ikono", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "Besedilo značke", + "details.userTitleEnabled": "Pokaži značko", + "details.private_help": "Če je ta možnost omogočena, bo pridružitev skupini zahtevala odobritev lastnika skupine.", + "details.hidden": "Skrito", + "details.hidden_help": "Če je ta možnost omogočena, je skupina skrita pred uporabniki, zato se ji lahko pridružijo zgolj tisti s povabilom.", + "details.delete_group": "Izbriši skupino", + "details.private_system_help": "Zasebne skupine so onemogočene na sistemskem nivoju, ta možnost tako nima učinka.", + "event.updated": "Podatki o skupini so bili posodobljeni.", + "event.deleted": "Skupina %1 je bila izbrisana.", + "membership.accept-invitation": "Sprejmi povabilo", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "Čakajoče vabilo", + "membership.join-group": "Pridruži se skupini", + "membership.leave-group": "Zapusti skupino", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "Zavrni", + "new-group.group_name": "Ime skupine:", + "upload-group-cover": "Upload group cover", + "bulk-invite-instructions": "Enter a list of comma separated usernames to invite to this group", + "bulk-invite": "Bulk Invite", + "remove_group_cover_confirm": "Are you sure you want to remove the cover picture?" +} \ No newline at end of file diff --git a/public/language/sl/ip-blacklist.json b/public/language/sl/ip-blacklist.json new file mode 100644 index 0000000000..35baff0c52 --- /dev/null +++ b/public/language/sl/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Tu nastavite svoj črni seznam IP.", + "description": "Včasih prepoved uporabniškega računa ni dovolj odvračilna. V drugih primerih je najboljši način za zaščito foruma omejitev dostopa do foruma za določen IP ali vrsto IP-jev. V teh scenarijih lahko na ta črni seznam dodate problematične naslove IP ali celotne bloke CIDR, pri čemer se jim prepreči prijava ali registracija novega računa.", + "active-rules": "Aktivna pravila", + "validate": "Potrdi črno listo", + "apply": "Uveljavi črno listo", + "hints": "namigi za sintakso", + "hint-1": "Določite posamezne naslove IP na vrstico. Bloke IP lahko dodate, dokler sledijo formatu CIDR (e.g. 192.168.100.0/22).", + "hint-2": "Komentarje lahko dodajate tako, da vrstice začnete z znakom #.", + + "validate.x-valid": "%1 od %2 pravil je neveljavnih.", + "validate.x-invalid": "Naslednjih %1 pravil je neveljavnih:", + + "alerts.applied-success": "Črna lista je uveljavljena", + + "analytics.blacklist-hourly": "Slika 1 – Zadetki na črni listi na uro", + "analytics.blacklist-daily": "Slika 2 – Zadetki na črni listi na dan", + "ip-banned": "Prepovedan IP" +} \ No newline at end of file diff --git a/public/language/sl/language.json b/public/language/sl/language.json new file mode 100644 index 0000000000..8d7cd9c09d --- /dev/null +++ b/public/language/sl/language.json @@ -0,0 +1,5 @@ +{ + "name": "Slovenščina", + "code": "sl", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/sl/login.json b/public/language/sl/login.json new file mode 100644 index 0000000000..f8454f27ec --- /dev/null +++ b/public/language/sl/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Uporabniško ime/E-pošta", + "username": "Uporabniško ime", + "remember_me": "Zapomni si me.", + "forgot_password": "Ste pozabili geslo?", + "alternative_logins": "Alternativne prijave", + "failed_login_attempt": "Prijava ni uspela", + "login_successful": "Uspešno ste se prijavili.", + "dont_have_account": "Ali še nimate uporabniškega računa?", + "logged-out-due-to-inactivity": "Zaradi neaktivnosti ste odjavljeni iz skrbniške nadzorne plošče.", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/sl/modules.json b/public/language/sl/modules.json new file mode 100644 index 0000000000..2557296e53 --- /dev/null +++ b/public/language/sl/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Klepetajte z", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Pošlji", + "chat.no_active": "Ni aktivnih klepetov.", + "chat.user_typing": "%1 piše sporočilo ...", + "chat.user_has_messaged_you": "%1 ti je poslal/-a sporočilo.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Za pregled zgodovine klepeta izberi prejemnika.", + "chat.no-users-in-room": "V tej sobi ni uporabnikov.", + "chat.recent-chats": "Zadnji klepeti", + "chat.contacts": "Stiki", + "chat.message-history": "Zgodovina klepeta", + "chat.message-deleted": "Sporočilo izbrisano", + "chat.options": "Možnosti klepeta", + "chat.pop-out": "Klepet v novem oknu", + "chat.minimize": "Minimiziraj", + "chat.maximize": "Maksimiraj", + "chat.seven_days": "7 dni", + "chat.thirty_days": "30 dni", + "chat.three_months": "3 meseci", + "chat.delete_message_confirm": "Ali ste prepričani, da želite izbrisati to sporočilo?", + "chat.retrieving-users": "Pridobivanje uporabnikov...", + "chat.manage-room": "Upravljaj sobo klepeta", + "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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Zapusti klepet", + "chat.leave-prompt": "Ste prepričani, da želite zapustiti ta klepet?", + "chat.leave-help": "Če zapustite ta klepet boste izključeni iz prihodnje korespondence v tem klepetu. Če boste v prihodnosti v klepet znova dodani, ne boste videli zgodovine klepeta pred ponovno pridružitvijo.", + "chat.in-room": "In this room", + "chat.kick": "Kick", + "chat.show-ip": "Pokaži IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "Sestavljanje", + "composer.show_preview": "Pokaži predogled", + "composer.hide_preview": "Skrij predogled", + "composer.user_said_in": "%1 je napisal/-a v %2:", + "composer.user_said": "%1 je napisal/-a:", + "composer.discard": "Ste prepričani, da želite zavreči to objavo?", + "composer.submit_and_lock": "Pošlji in zakleni", + "composer.toggle_dropdown": "Preklopi spustni meni", + "composer.uploading": "Prenašanje %1", + "composer.formatting.bold": "Krepko", + "composer.formatting.italic": "Ležeče", + "composer.formatting.list": "Seznam", + "composer.formatting.strikethrough": "Prečrtano", + "composer.formatting.code": "Code", + "composer.formatting.link": "Povezava", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Prenesi sliko", + "composer.upload-file": "Prenesi datoteko", + "composer.zen_mode": "Zen način", + "composer.select_category": "Izberi kategorijo", + "composer.textarea.placeholder": "Tukaj vnesite vsebino objave, povlecite in spustite slike", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Datum", + "composer.schedule-time": "Čas", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Nastavi datum", + "bootbox.ok": "V redu", + "bootbox.cancel": "Prekliči", + "bootbox.confirm": "Potrdi", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Določanje položaja naslovne fotografije", + "cover.dragging_message": "Povleci sliko na želeni položaj in klikni \"Shrani\". ", + "cover.saved": "Naslovna fotografija in položaj shranjena", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/sl/notifications.json b/public/language/sl/notifications.json new file mode 100644 index 0000000000..2a216af4bc --- /dev/null +++ b/public/language/sl/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Obvestila", + "no_notifs": "Nimate novih obvestil.", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Nazaj na %1", + "outgoing_link": "Odhodna povezava", + "outgoing_link_message": "Sedaj zapuščate %1.", + "continue_to": "Nadaljujte na %1.", + "return_to": "Vrnite se na %1.", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "Imate neprebrana obvestila.", + "all": "All", + "topics": "Topics", + "replies": "Replies", + "chat": "Chats", + "group-chat": "Group Chats", + "follows": "Follows", + "upvote": "Upvotes", + "new-flags": "New Flags", + "my-flags": "Flags assigned to me", + "bans": "Bans", + "new_message_from": "Novo obvestilo od %1", + "upvoted_your_post_in": "%1 je glasoval/-a za vašo objavo v %2.", + "upvoted_your_post_in_dual": "%1 in %2 sta glasovala/-i za vašo objavo v %3.", + "upvoted_your_post_in_multiple": "%1 in %2 drugih je glasovalo za vašo objavo v %3.", + "moved_your_post": "%1 je premaknil/-a vašo objavo v %2.", + "moved_your_topic": "%1 je premaknil/-a %2.", + "user_flagged_post_in": "%1je označil/-a vašo objavo v %2.", + "user_flagged_post_in_dual": "%1 in %2 sta označila/-a vašo objavo v %3.", + "user_flagged_post_in_multiple": "%1 and %2 drugih je označilo vašo objavo v %3.", + "user_flagged_user": "%1 flagged a user profile (%2)", + "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", + "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_posted_to": "%1 je objavil/-a odgovor na: %2.", + "user_posted_to_dual": "%1 in %2 sta objavila/-i odgovor na: %3.", + "user_posted_to_multiple": "%1 in %2 drugih je objavilo odgovor na: %3.", + "user_posted_topic": "%1 je odprl/-a novo temo: %2.", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 te je začel/-a spremljati.", + "user_started_following_you_dual": "%1 in %2 sta te začela/-i spremljati.", + "user_started_following_you_multiple": "%1 in %2 drugih te je začelo spremljati.", + "new_register": "%1 je poslal/-a zahtevo za registracijo.", + "new_register_multiple": "Število registracijskih zahtev, ki čakajo na pregled: %1", + "flag_assigned_to_you": "Flag %1 has been assigned to you", + "post_awaiting_review": "Post awaiting review", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-poštni naslov potrjen", + "email-confirmed-message": "Hvala, da ste potrdili svoj e-naslov. Račun je sedaj aktiviran.", + "email-confirm-error-message": "Prišlo je do napake pri preverjanju vašega e-poštnega naslova. Morda je bila koda napačna ali pa je potekla.", + "email-confirm-sent": "Potrditveno e-sporočilo je poslano.", + "none": "None", + "notification_only": "Notification Only", + "email_only": "Email Only", + "notification_and_email": "Notification & Email", + "notificationType_upvote": "When someone upvotes your post", + "notificationType_new-topic": "When someone you follow posts a topic", + "notificationType_new-reply": "When a new reply is posted in a topic you are watching", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "When someone starts following you", + "notificationType_new-chat": "When you receive a chat message", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "When you receive a group invite", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "When someone gets added to registration queue", + "notificationType_post-queue": "When a new post is queued", + "notificationType_new-post-flag": "When a post is flagged", + "notificationType_new-user-flag": "When a user is flagged" +} \ No newline at end of file diff --git a/public/language/sl/pages.json b/public/language/sl/pages.json new file mode 100644 index 0000000000..0c766965ef --- /dev/null +++ b/public/language/sl/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Domov", + "unread": "Neprebrane teme", + "popular-day": "Priljubljene teme danes", + "popular-week": "Priljubljene teme v tem tednu", + "popular-month": "Priljubljene teme v tem mesecu", + "popular-alltime": "Vse priljubljene teme", + "recent": "Zadnje teme", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "Moderator Tools", + "flagged-content": "Flagged Content", + "ip-blacklist": "IP Blacklist", + "post-queue": "Post Queue", + "users/online": "Dosegljivi uporabniki", + "users/latest": "Zadnji uporabniki", + "users/sort-posts": "Uporabniki z največ objavami", + "users/sort-reputation": "Uporabniki z največjim ugledom", + "users/banned": "Izločeni uporabniki", + "users/most-flags": "Največkrat označeni uporabniki", + "users/search": "Iskanje uporabnikov", + "notifications": "Obvestila", + "tags": "Oznake", + "tag": "Topics tagged under "%1"", + "register": "Registriraj svoj račun.", + "registration-complete": "Registracija končana", + "login": "Prijavi se v svoj račun.", + "reset": "Ponastavi geslo svojega računa.", + "categories": "Kategorije", + "groups": "Skupine", + "group": "Skupina %1", + "chats": "Klepeti", + "chat": "Klepet z osebo %1", + "flags": "Flags", + "flag-details": "Flag %1 Details", + "account/edit": "Urejanje \"%1\"", + "account/edit/password": "Urejanje gesla za \"%1\"", + "account/edit/username": "Urejanje uporabniškega imena za \"%1\"", + "account/edit/email": "Urejanje e-pošte za \"%1\"", + "account/info": "Podatki o računu", + "account/following": "Ljudje, ki jim sledi oseba %1", + "account/followers": "Ljudje, ki sledijo osebi %1", + "account/posts": "Objave uporabnika %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "Teme, ki jih je ustvaril uporabnik %1", + "account/groups": "Skupine uporabnika %1", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "%1's Bookmarked Posts", + "account/settings": "Uporabniške nastavitve", + "account/watched": "Teme, ki jih spremlja %1", + "account/ignored": "Topics ignored by %1", + "account/upvoted": " Objave uporabnika %1 z glasovi za", + "account/downvoted": "Objave uporabnika %1 z glasovi proti", + "account/best": "Najboljše objave uporabnika %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "E-pošta potrjena", + "maintenance.text": "%1 je trenutno v vzdrževanju.", + "maintenance.messageIntro": "Dodatno vam je skrbnik pustil tole sporočilo:", + "throttled.text": "Storitev %1 je trenutno zaradi obremenitve nedosegljiva. Prosimo, vrnite se pozneje." +} \ No newline at end of file diff --git a/public/language/sl/post-queue.json b/public/language/sl/post-queue.json new file mode 100644 index 0000000000..7c4c0175d0 --- /dev/null +++ b/public/language/sl/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Čakalna vrsta objav", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "Uporabnik", + "category": "Kategorija", + "title": "Naslov", + "content": "Vsebina", + "posted": "Posted", + "reply-to": "Odgovor na %1", + "content-editable": "Za urejanje kliknite na vsebino", + "category-editable": "Za urejanje kliknite na kategorijo", + "title-editable": "Za urejanje kliknite na naslov", + "reply": "Odgovori", + "topic": "Tema", + "accept": "Sprejmi", + "reject": "Zavrni", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/sl/recent.json b/public/language/sl/recent.json new file mode 100644 index 0000000000..a4e3328453 --- /dev/null +++ b/public/language/sl/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Nedavno", + "day": "Dan", + "week": "Teden", + "month": "Mesec", + "year": "Leto", + "alltime": "Vse", + "no_recent_topics": "Ni nedavnih tem.", + "no_popular_topics": "Ni priljubljenih tem.", + "there-is-a-new-topic": "Objavljena je nova tema.", + "there-is-a-new-topic-and-a-new-post": "Objavljeni sta nova tema in nova objava.", + "there-is-a-new-topic-and-new-posts": "Objavljene so nova tema in %1 novih objav.", + "there-are-new-topics": "%1 novih tem.", + "there-are-new-topics-and-a-new-post": "%1 novih tem in nova objava.", + "there-are-new-topics-and-new-posts": "Nove teme: %1, nove objave: %2.", + "there-is-a-new-post": "Objavljena je nova objava.", + "there-are-new-posts": "Nove objave: %1.", + "click-here-to-reload": "Kliknite tu za osvežitev." +} \ No newline at end of file diff --git a/public/language/sl/register.json b/public/language/sl/register.json new file mode 100644 index 0000000000..17def24c5c --- /dev/null +++ b/public/language/sl/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registracija", + "cancel_registration": "Preklic registracije", + "help.email": "Vaš e-poštni naslov bo privzeto skrit za javnost.", + "help.username_restrictions": "Enkratno uporabniško ime s številom znakov: med %1 in %2. Drugi vas lahko v objavi omenijo z @uporabnik.", + "help.minimum_password_length": "Vaše geslo mora vsebovati najmanjše število znakov: %1.", + "email_address": "E-poštni naslov", + "email_address_placeholder": "Vnesi e-poštni naslov", + "username": "Uporabniško ime", + "username_placeholder": "Vnesi uporabniško ime", + "password": "Geslo", + "password_placeholder": "Vnesi geslo", + "confirm_password": "Potrdi geslo", + "confirm_password_placeholder": "Potrdi geslo", + "register_now_button": "Registriraj se", + "alternative_registration": "Alternativna registracija", + "terms_of_use": "Pogoji uporabe", + "agree_to_terms_of_use": "Strinjam se s pogoji uporabe.", + "terms_of_use_error": "S pogoji uporabe se morate strinjati.", + "registration-added-to-queue": "Registracija uporabniškega profila poteka. Ob potrditvi skrbnika boste v svoj e-poštni predal prejeli sporočilo.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/sl/reset_password.json b/public/language/sl/reset_password.json new file mode 100644 index 0000000000..f5d3a758a6 --- /dev/null +++ b/public/language/sl/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Ponastavi geslo", + "update_password": "Posodobi geslo", + "password_changed.title": "Geslo spremenjeno", + "password_changed.message": "

Geslo je bilo uspešno ponastavljeno, prosimo, prijavite se ponovno.", + "wrong_reset_code.title": "Nepravilna koda za ponastavitev", + "wrong_reset_code.message": "Koda za ponastavitev je napačna. Prosimo, poskusite ponovno ali zahtevajte novo kodo.", + "new_password": "Novo geslo", + "repeat_password": "Potrdi geslo", + "changing_password": "Changing Password", + "enter_email": "Prosimo, vpišite svoj e-poštni naslov in poslali vam bomo navodila za ponastavitev uporabniškega računa.", + "enter_email_address": "Vpišite svoj e-poštni naslov.", + "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.", + "invalid_email": "Napačen e-poštni naslov./E-poštni naslov ne obstaja!", + "password_too_short": "Geslo, ki ste ga izbrali, je prekratko, prosimo, izberite drugačno geslo.", + "passwords_do_not_match": "Gesli, ki ste ju vpisali, se ne ujemata.", + "password_expired": "Vaše geslo je poteklo, prosimo, izberite novo geslo." +} \ No newline at end of file diff --git a/public/language/sl/search.json b/public/language/sl/search.json new file mode 100644 index 0000000000..1c6196c710 --- /dev/null +++ b/public/language/sl/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 rezultat(ov) ustreza \"%2\", (%3 sekund)", + "no-matches": "Ni najdenih rezultatov", + "advanced-search": "Napredno iskanje", + "in": "V", + "titles": "Naslovi", + "titles-posts": "Naslovi in objave", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "Objavil", + "in-categories": "V kategoriji", + "search-child-categories": "Išči podkategorije", + "has-tags": "Has tags", + "reply-count": "Število odgovorov", + "at-least": "Vsaj", + "at-most": "Največ", + "relevance": "Relevance", + "post-time": "Čas objave", + "votes": "Votes", + "newer-than": "Novejše kot", + "older-than": "Starejše kot", + "any-date": "Katerikoli datum", + "yesterday": "Včeraj", + "one-week": "En teden", + "two-weeks": "Dva tedna", + "one-month": "En mesec", + "three-months": "Tri mesece", + "six-months": "Šest mesecev", + "one-year": "Eno leto", + "sort-by": "Razvrsti po", + "last-reply-time": "Čas zadnjega odgovora", + "topic-title": "Naslov teme", + "topic-votes": "Topic votes", + "number-of-replies": "Število odgovorov", + "number-of-views": "Število ogledov", + "topic-start-date": "Datum odprtja teme", + "username": "Uporabniško ime", + "category": "Kategorija", + "descending": "Padajoče", + "ascending": "Naraščajoče", + "save-preferences": "Shrani nastavitve", + "clear-preferences": "Počisti nastavitve", + "search-preferences-saved": "Poišči shranjene nastavitve", + "search-preferences-cleared": "Poišči počiščene nastavitve", + "show-results-as": "Prikaži rezultate kot", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/sl/success.json b/public/language/sl/success.json new file mode 100644 index 0000000000..4c6a5ddbe6 --- /dev/null +++ b/public/language/sl/success.json @@ -0,0 +1,7 @@ +{ + "success": "Uspešno", + "topic-post": "Uspešno ste objavili.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Avtentikacija uspešna", + "settings-saved": "Nastavitve shranjene!" +} \ No newline at end of file diff --git a/public/language/sl/tags.json b/public/language/sl/tags.json new file mode 100644 index 0000000000..244d7b7a1f --- /dev/null +++ b/public/language/sl/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Ni novih tem s to oznako.", + "tags": "Oznake", + "enter_tags_here": "Tu vpišite oznake. Dovoljeno število znakov: najmanj %1 in največ %2.", + "enter_tags_here_short": "Vpišite oznake...", + "no_tags": "Oznak še ni.", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/sl/top.json b/public/language/sl/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/sl/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/sl/topic.json b/public/language/sl/topic.json new file mode 100644 index 0000000000..4ac16a2f1a --- /dev/null +++ b/public/language/sl/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tema", + "title": "Naslov", + "no_topics_found": "Ni najdenih tem!", + "no_posts_found": "Ni najdenih objav!", + "post_is_deleted": "Ta objava je izbrisana!", + "topic_is_deleted": "Ta tema je izbrisana!", + "profile": "Profil", + "posted_by": "Objavil %1", + "posted_by_guest": "Objavil Gost", + "chat": "Klepet", + "notify_me": "Bodi obveščen o novih odgovorih na to temo", + "quote": "Citiraj", + "reply": "Odgovori", + "replies_to_this_post": "Št. odogvorov: %1", + "one_reply_to_this_post": "1 odgovor", + "last_reply_time": "Zadnji odgovor", + "reply-as-topic": "Odgovori s temo", + "guest-login-reply": "Prijavi se za odgovor", + "login-to-view": "🔒 Log in to view", + "edit": "Uredi", + "delete": "Izbriši", + "delete-event": "Izbriši dogodek", + "delete-event-confirm": "Ste prepričani, da želite izbrisati ta dogodek?", + "purge": "Očisti", + "restore": "Obnovi", + "move": "Premakni", + "change-owner": "Spremeni lastnika", + "fork": "Razcepi", + "link": "Povezava", + "share": "Deli", + "tools": "Orodja", + "locked": "Zaklenjeno", + "pinned": "Pripeto", + "pinned-with-expiry": "Pripeto do %1", + "scheduled": "Scheduled", + "moved": "Premaknjeno", + "moved-from": "Moved from %1", + "copy-ip": "Kopiraj IP", + "ban-ip": "Prepovej IP", + "view-history": "Uredi zgodovino", + "locked-by": "Zaklenil/a", + "unlocked-by": "Odklenil/a", + "pinned-by": "Pripel/a", + "unpinned-by": "Odpel/a", + "deleted-by": "Izbrisal/a", + "restored-by": "Povrnil/a", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Klikni tukaj za vrnitev na zadnje prebrano objavo v tej niti", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Ta tema je bila izbrisana. Le uporabniki s pravicami upravljanja tem jo lahko vidijo.", + "following_topic.message": "Če nekdo objavi v to temo, boste od sedaj dobivali obvestila. ", + "not_following_topic.message": "To temo boste videli v seznamu neprebranih tem. Ne boste pa dobili obvestila, če bo nekdo objavil v tej temi. ", + "ignoring_topic.message": "To temo ne boste videli v seznamu neprebranih tem. Prav tako ne boste dobili obvestila, če bo nekdo objavil v tej temi. ", + "login_to_subscribe": "Če se želite naročiti na to temo se morate najprej prijaviti oziroma opraviti registracijo.", + "markAsUnreadForAll.success": "Tema označena kot neprebrana za vse.", + "mark_unread": "Označi kot neprebrano", + "mark_unread.success": "Tema označena kot neprebrana.", + "watch": "Spremljaj", + "unwatch": "Ne spremljaj", + "watch.title": "Bodi obveščen o novih odgovorih v tej temi", + "unwatch.title": "Prenehaj spremljati to temo", + "share_this_post": "Deli to objavo", + "watching": "Spremljano", + "not-watching": "Ni spremljano", + "ignoring": "Prezri", + "watching.description": "Obvesti me o novih odgovorih.
Teme prikaži v Neprebrano.", + "not-watching.description": "Ne obvesti me o novih odgovorih.
Teme prikaži v Neprebrano le če kategorija ni prezrta.", + "ignoring.description": "Ne obvesti me o novih odgovorih.
Teme ne prikaži v Neprebrano.", + "thread_tools.title": "Orodja teme", + "thread_tools.markAsUnreadForAll": "Označi vse kot neprebrano", + "thread_tools.pin": "Pripni temo", + "thread_tools.unpin": "Odpni temo", + "thread_tools.lock": "Zakleni temo", + "thread_tools.unlock": "Odkleni temo", + "thread_tools.move": "Premakni temo", + "thread_tools.move-posts": "Premakni objave", + "thread_tools.move_all": "Premakni vse", + "thread_tools.change_owner": "Spremeni lastnika", + "thread_tools.select_category": "Izberi kategorijo", + "thread_tools.fork": "Razcepi temo", + "thread_tools.delete": "Izbriši temo", + "thread_tools.delete-posts": "Izbriši objave", + "thread_tools.delete_confirm": "Ste prepričani, da želite izbrisati to temo?", + "thread_tools.restore": "Obnovi temo", + "thread_tools.restore_confirm": "Ste prepričani, da želite obnoviti to temo?", + "thread_tools.purge": "Očisti temo", + "thread_tools.purge_confirm": "Ste prepričani, da želite očistiti to temo?", + "thread_tools.merge_topics": "Združi teme", + "thread_tools.merge": "Združi", + "topic_move_success": "Ta tema bo kmalu premaknjena v \"%1\". Kliknite tukaj, če želite razveljaviti.", + "topic_move_multiple_success": "Te teme bodo kmalu premaknjene v \"%1\". Kliknite tukaj, če želite razveljaviti.", + "topic_move_all_success": "Vse teme bodo kmalu premaknjene v \"%1\". Kliknite tukaj, če želite razveljaviti.", + "topic_move_undone": "Premik teme razveljavljen", + "topic_move_posts_success": "Objave bodo kmalu premaknjene. Kliknite tukaj, če želite razveljaviti.", + "topic_move_posts_undone": "Premik objav razveljavljen", + "post_delete_confirm": "Ste prepričani, da želite izbrisati to objavo?", + "post_restore_confirm": "Ste prepričani, da želite obnoviti to objavo?", + "post_purge_confirm": "Ste prepričani, da želite očistiti to objavo?", + "pin-modal-expiry": "Datum poteka veljavnosti", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Nalagam kategorije", + "confirm_move": "Premakni", + "confirm_fork": "Razcepi", + "bookmark": "Zaznamek", + "bookmarks": "Zaznamki", + "bookmarks.has_no_bookmarks": "Zaznamovali še niste nobenih objav.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Nalagam več objav", + "move_topic": "Premakni temo", + "move_topics": "Premakni teme", + "move_post": "Premakni objavo", + "post_moved": "Objava premaknjena!", + "fork_topic": "Razcepi temo", + "enter-new-topic-title": "Vnesite nov naslov teme", + "fork_topic_instruction": "Klikni na objavo, ki o želiš odcepiti", + "fork_no_pids": "Ni izbranih objav!", + "no-posts-selected": "Ni izbranih objav!", + "x-posts-selected": "Izbranih objav: %1 ", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "Izbranih objav: %1 ", + "fork_success": "Uspešno ste razcepili temo! Klikni tu za ogled te teme.", + "delete_posts_instruction": "Kliknite na teme, ki jih želite izbrisati/očistiti ", + "merge_topics_instruction": "Kliknite teme, ki jih želite združiti, ali jih poiščite", + "merge-topic-list-title": "Seznam tem za združevanje", + "merge-options": "Možnosti združevanja", + "merge-select-main-topic": "Izberi glavno temo", + "merge-new-title-for-topic": "Nov naslov teme", + "topic-id": "ID teme", + "move_posts_instruction": "Kliknite objave, ki jih želite premakniti, nato vnesite ID teme ali pojdite na ciljno temo", + "change_owner_instruction": "Kliknite objave, ki jih želite dodeliti drugemu uporabniku", + "composer.title_placeholder": "Vpiši naslov teme...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Zavrzi", + "composer.submit": "Pošlji", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Odgovor na %1", + "composer.new_topic": "Nova tema", + "composer.editing": "Urejanje", + "composer.uploading": "nalagam...", + "composer.thumb_url_label": "Prilepite URL sličice teme", + "composer.thumb_title": "Dodajte sličico tej temi", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Ali naložite datoteko", + "composer.thumb_remove": "Počisti polja", + "composer.drag_and_drop_images": "Primite in spustite slike tukaj", + "more_users_and_guests": "%1 uporabnik(a/i/ov) in %2 Gost(a/i/ov)", + "more_users": "%1 uporabnik(a/i/ov)", + "more_guests": "%1 Gost(ov)", + "users_and_others": "%1 in %2 drugi(h)", + "sort_by": "Razvrsti po", + "oldest_to_newest": "Od starejšega do novejšega", + "newest_to_oldest": "Od novejšega do starejšega", + "most_votes": "Največ glasov", + "most_posts": "Največ objav", + "most_views": "Največ ogledov", + "stale.title": "Raje ustvari novo temo?", + "stale.warning": "Tema na katero odgovarjaš je precej stara. A ne bi raje ustvaril novo temo namesto te, z sklicem na to v tvojem odgovoru?", + "stale.create": "Ustvari novo temo", + "stale.reply_anyway": "Vseeno odgovori na to temo", + "link_back": "Odg: [%1](%2)", + "diffs.title": "Zgodovina urejanja objav", + "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has %1 revisions.", + "diffs.current-revision": "trenutna različica", + "diffs.original-revision": "izvirna različica", + "diffs.restore": "Obnovi to različico", + "diffs.restore-description": "Po obnovitvi bo v zgodovino urejanj te objave dodana nova različica.", + "diffs.post-restored": "Objava je bila uspešno obnovljena na prejšnjo različico", + "diffs.delete": "Izbriši to različico", + "diffs.deleted": "Različica izbrisana", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "Prva objava", + "last-post": "Zadnja obava", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Objavi hitri odgovor" +} \ No newline at end of file diff --git a/public/language/sl/unread.json b/public/language/sl/unread.json new file mode 100644 index 0000000000..3179be8951 --- /dev/null +++ b/public/language/sl/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Neprebrano", + "no_unread_topics": "Ni neprebranih tem.", + "load_more": "Naloži več", + "mark_as_read": "Označi kot prebrano", + "selected": "Izbrano", + "all": "Vse", + "all_categories": "Vse kategorije", + "topics_marked_as_read.success": "Teme označene kot prebrane!", + "all-topics": "Vse teme", + "new-topics": "Nove teme", + "watched-topics": "Spremljane teme", + "unreplied-topics": "Unreplied Topics", + "multiple-categories-selected": "Multiple Selected" +} \ No newline at end of file diff --git a/public/language/sl/uploads.json b/public/language/sl/uploads.json new file mode 100644 index 0000000000..06b722a95f --- /dev/null +++ b/public/language/sl/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Prenašanje datoteke ...", + "select-file-to-upload": "Izberete datoteko, ki jo želite prenesti!", + "upload-success": "Datoteka je bila uspešno prenesena!", + "maximum-file-size": "Največ %1 kb ", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/sl/user.json b/public/language/sl/user.json new file mode 100644 index 0000000000..47acf554a8 --- /dev/null +++ b/public/language/sl/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Izločen", + "muted": "Muted", + "offline": "Odjavljeni", + "deleted": "Izbrisano", + "username": "Uporabniško ime", + "joindate": "Datum pridružitve", + "postcount": "Število objav", + "email": "E-pošta", + "confirm_email": "Potrdi e-poštni naslov", + "account_info": "Podatki računa", + "admin_actions_label": "Skrbniška dejanja", + "ban_account": "Izločen račun", + "ban_account_confirm": "Ali želiš izločiti uporabnika?", + "unban_account": "Ponovno vključi račun", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Izbriši račun", + "delete_account_as_admin": "Izbriši račun", + "delete_content": "Izbriši vsebino računa", + "delete_all": "Izbriši račun in vsebino", + "delete_account_confirm": "Ste prepričani, da želite anonimizirati vašo objavo in izbrisati vaš račun?
To dejanje je nepovratno in podatkov ne boste mogli obnoviti.

Vnesite svoje geslo za potrditev, da želite uničiti ta račun.", + "delete_this_account_confirm": "Ali ste prepričani, da želite izbrisati ta račun, pri tem pa pustiti vsebino za seboj?
To dejanje je nepovratno, objave bodo anonimizirane in povezav objav z izbrisanim računom ne boste mogli obnoviti.

", + "delete_account_content_confirm": "Ali ste prepričani, da želite izbrisati vsebino tega računa (objave/teme/nalaganja)?
To dejanje je nepovratno in podatkov ne boste mogli obnoviti

", + "delete_all_confirm": "Ali ste prepričani, da želite izbrisati ta račun in vso njegovo vsebino (objave/teme/nalaganja)?
To dejanje je nepovratno in podatkov ne boste mogli obnoviti

", + "account-deleted": "Račun je izbrisan", + "account-content-deleted": "Vsebina računa je izbrisana", + "fullname": "Ime in priimek", + "website": "Spletna stran", + "location": "Lokacija", + "age": "Starost", + "joined": "Pridružil", + "lastonline": "Nazadnje na strani", + "profile": "Profil", + "profile_views": "Ogledi", + "reputation": "Naziv", + "bookmarks": "Zaznamki", + "watched_categories": "Spremljane kategorije", + "change_all": "Spremeni vse", + "watched": "Spremljano", + "ignored": "Prezrto", + "default-category-watch-state": "Privzeto stanje spremljanja kategorij", + "followers": "Spremljevalci", + "following": "Spremljano", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Blokiraj uporabnika", + "unblock_user": "Odblokiraj uporabnika", + "aboutme": "O meni", + "signature": "Podpis", + "birthday": "Rojstni datum", + "chat": "Klepet", + "chat_with": "Nadaljuj klepet z %1", + "new_chat_with": "Prični nov klepet z %1", + "flag-profile": "Označi profil z zastavico", + "follow": "Spremljaj", + "unfollow": "Ne spremljaj", + "more": "Več", + "profile_update_success": "Profil je bil uspešno posodobljen.", + "change_picture": "Spremeni sliko", + "change_username": "Spremeni uporabniško ime", + "change_email": "Spremeni e-poštni naslov", + "email_same_as_password": "Za nadaljevanje vnesite svoje trenutno geslo & ndash; ponovno ste vnesli svoj novi e-poštni naslov", + "edit": "Uredi", + "edit-profile": "Uredi profil", + "default_picture": "Privzeta ikona", + "uploaded_picture": "Naloži fotografijo", + "upload_new_picture": "Naloži novo fotografijo", + "upload_new_picture_from_url": "Naloži novo fotografijo s spletnega naslova", + "current_password": "Trenutno geslo", + "change_password": "Spremeni geslo", + "change_password_error": "Napačno geslo!", + "change_password_error_wrong_current": "Tvoje trenutno geslo je napačno!", + "change_password_error_match": "Gesli se morata ujemati!", + "change_password_error_privileges": "Nimaš pravice do spremembe gesla.", + "change_password_success": "Geslo je bilo posodobljeno!", + "confirm_password": "Potrdi geslo", + "password": "Geslo", + "username_taken_workaround": "Predlagano uporabniško ime je že zasedeno, zato smo ga rahlo spremenili. Sedaj vas poznamo kot %1", + "password_same_as_username": "Vaše geslo je enako kot vaše uporabniško ime, prosim izberite drugačno geslo.", + "password_same_as_email": "Vaše geslo je enako kot vaše e-poštni naslov, prosim izberite drugačno geslo.", + "weak_password": "Šibko geslo.", + "upload_picture": "Naloži fotografijo", + "upload_a_picture": "Naloži fotografijo", + "remove_uploaded_picture": "Odstrani preneseno sliko ", + "upload_cover_picture": "Prenesi fotografijo naslovnice", + "remove_cover_picture_confirm": "Ste prepričani, da želite odstraniti naslovno sliko?", + "crop_picture": "Obreži sliko", + "upload_cropped_picture": "Obreži in naloži", + "avatar-background-colour": "Slika ozadja avatarja", + "settings": "Nastavitve", + "show_email": "Pokaži moj e-poštni naslov.", + "show_fullname": "Pokaži moj ime in priimek.", + "restrict_chats": "Dovoli klepet samo z osebami, ki jim sledim.", + "digest_label": "Prijavi se na izvleček", + "digest_description": "Prijavi se na obveščanje preko e-pošte (nova obvestila ali teme) na podlagi naslednjega urnika", + "digest_off": "Izključi", + "digest_daily": "Dnevno", + "digest_weekly": "Tedensko", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Mesečno", + "has_no_follower": "Uporabniku nihče ne sledi :(", + "follows_no_one": "Uporabnik nikomur ne sledi :(", + "has_no_posts": "Uporabnik še ni nič objavil.", + "has_no_best_posts": "Ta uporabnik še nima nobenih objav z glasovi za.", + "has_no_topics": "Uporabnik še ni objavil nobene teme.", + "has_no_watched_topics": "Uporabnik še ne spremlja nobene teme.", + "has_no_ignored_topics": "Ta uporabnik še nima nobenih prezrtih tem.", + "has_no_upvoted_posts": "Uporabnik še ni glasoval za nobeno objavo.", + "has_no_downvoted_posts": "Uporabnik še ni glasoval proti nobeni objavi.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Nimate blokiranih uporabnikov.", + "email_hidden": "Skrit e-poštni naslov", + "hidden": "skrit", + "paginate_description": "Uporabi oštevilčenje strani namesto neskončnega drsenja", + "topics_per_page": "Število tem na stran", + "posts_per_page": "Število objav na stran", + "max_items_per_page": "Največ %1", + "acp_language": "Jezik skrbniških strani", + "notifications": "Obvestila", + "upvote-notif-freq": "Pogostost obveščanja o glasovih za", + "upvote-notif-freq.all": "Vsi glasovi za", + "upvote-notif-freq.first": "Prvi na objavo", + "upvote-notif-freq.everyTen": "Vsakih 10 glasov za", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", + "upvote-notif-freq.disabled": "Onemogočeno", + "browsing": "Preglej nastavitve", + "open_links_in_new_tab": "Zunanje povezave odpri v novem zavihku", + "enable_topic_searching": "Omogoči iskanje znotraj teme", + "topic_search_help": "Če omogočite, bo iskanje prepisalo brskalnikove prevzete nastavitve in vam omogočilo iskanje skozi celotno temo.", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Po objavi odgovora prikaži novo objavo", + "follow_topics_you_reply_to": "Spremljanj teme, na katere si odgovoril", + "follow_topics_you_create": "Spremljanj teme, ki si jih ustvaril", + "grouptitle": "Naslov skupine", + "group-order-help": "Izberi skupino in uporabi puščice za razvrstitev naslovov", + "no-group-title": "Skupina nima imena", + "select-skin": "Izberi preobleko", + "select-homepage": "Izberi domačo stran", + "homepage": "Domača stran", + "homepage_description": "Izberite stran, ki jo želite uporabiti kot domačo stran foruma, ali 'Brez', če želite uporabiti privzeto domačo stran.", + "custom_route": "Po do domače strani po meri", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Storitev enotne prijave ", + "sso.associated": "Povezan z", + "sso.not-associated": "Kliknite tu da povežete z", + "sso.dissociate": "Dissociate", + "sso.dissociate-confirm-title": "Confirm Dissociation", + "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.latest-flags": "Latest Flags", + "info.no-flags": "No Flagged Posts Found", + "info.ban-history": "Recent Ban History", + "info.no-ban-history": "This user has never been banned", + "info.banned-until": "Banned until %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Banned permanently", + "info.banned-reason-label": "Razlog", + "info.banned-no-reason": "Razlog ni podan.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Zgodovina uporabniškega imena", + "info.email-history": "Zgodovina e-poštnega naslova", + "info.moderation-note": "Moderation Note", + "info.moderation-note.success": "Moderation note saved", + "info.moderation-note.add": "Dodaj opombo", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "Ta forum skupnosti zbira in obdeluje vaše osebne podatke.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "Imate pravico do popravka.", + "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's administrative team.", + "consent.right_to_erasure": "Imate pravico do izbrisa.", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "Imate pravico do prenosa podatkov", + "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.export_profile": "Izvozi profil (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Izvozi naloženo vsebino (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Izvozi objave (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/sl/users.json b/public/language/sl/users.json new file mode 100644 index 0000000000..174b2e1916 --- /dev/null +++ b/public/language/sl/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Zadnji uporabniki", + "top_posters": "Najboljši uporabniki", + "most_reputation": "Največ ugleda", + "most_flags": "Most Flags", + "search": "Išči", + "enter_username": "Za iskanje vpiši uporabniško ime", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Naloži več", + "users-found-search-took": "%1 uporabnik(ov) najdenih! Iskanje je potrebovalo %2 sekunde.", + "filter-by": "Filtriraj po", + "online-only": "Samo dosegljivi", + "invite": "Povabi", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Povabilo je bilo poslano na e-mail naslov %1", + "user_list": "Sezam uporabnikov", + "recent_topics": "Zadnje teme", + "popular_topics": "Priljubljene teme", + "unread_topics": "Neprebrane teme", + "categories": "Kategorije", + "tags": "Oznake", + "no-users-found": "Ni mogoče najti uporabnikov" +} \ No newline at end of file diff --git a/public/language/sq-AL/_DO_NOT_EDIT_FILES_HERE.md b/public/language/sq-AL/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/sq-AL/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/sq-AL/admin/admin.json b/public/language/sq-AL/admin/admin.json new file mode 100644 index 0000000000..f633c82c19 --- /dev/null +++ b/public/language/sq-AL/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Jeni i sigurt që dëshironi të rindërtoni dhe rinisni NodeBB?", + "alert.confirm-restart": "Jeni i sigurt që dëshironi të rinisni NodeBB?", + + "acp-title": "%1 | NodeBB Paneli i Kontrollit të Administratorit ", + "settings-header-contents": "Përmbatja ", + "changes-saved": "Ndryshimet u ruajtën!", + "changes-saved-message": "Ndryshimet në konfigurimin e NodeBB u ruajtën me sukses!", + "changes-not-saved": "Ndryshimet nuk u ruajtën!", + "changes-not-saved-message": "NodeBB gjeti një problem gjatë ruajtjes së ndryshimeve. (%1)" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/advanced/cache.json b/public/language/sq-AL/admin/advanced/cache.json new file mode 100644 index 0000000000..371a397b8b --- /dev/null +++ b/public/language/sq-AL/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Plot ", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/advanced/database.json b/public/language/sq-AL/admin/advanced/database.json new file mode 100644 index 0000000000..9167b381ed --- /dev/null +++ b/public/language/sq-AL/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "Used Memory", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "Total Connections Received", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/sq-AL/admin/advanced/errors.json b/public/language/sq-AL/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/sq-AL/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/advanced/events.json b/public/language/sq-AL/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/sq-AL/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/advanced/logs.json b/public/language/sq-AL/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/sq-AL/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/appearance/customise.json b/public/language/sq-AL/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/sq-AL/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/appearance/skins.json b/public/language/sq-AL/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/sq-AL/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/appearance/themes.json b/public/language/sq-AL/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/sq-AL/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/dashboard.json b/public/language/sq-AL/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/sq-AL/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/sq-AL/admin/development/info.json b/public/language/sq-AL/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/sq-AL/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/development/logger.json b/public/language/sq-AL/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/sq-AL/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/extend/plugins.json b/public/language/sq-AL/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/sq-AL/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/sq-AL/admin/extend/rewards.json b/public/language/sq-AL/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/sq-AL/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/extend/widgets.json b/public/language/sq-AL/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/sq-AL/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/admins-mods.json b/public/language/sq-AL/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/sq-AL/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/categories.json b/public/language/sq-AL/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/sq-AL/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/digest.json b/public/language/sq-AL/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/sq-AL/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/sq-AL/admin/manage/groups.json b/public/language/sq-AL/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/sq-AL/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/privileges.json b/public/language/sq-AL/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/sq-AL/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/registration.json b/public/language/sq-AL/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/sq-AL/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/tags.json b/public/language/sq-AL/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/sq-AL/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/uploads.json b/public/language/sq-AL/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/sq-AL/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/manage/users.json b/public/language/sq-AL/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/sq-AL/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/menu.json b/public/language/sq-AL/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/sq-AL/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/advanced.json b/public/language/sq-AL/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/sq-AL/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/api.json b/public/language/sq-AL/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/sq-AL/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/chat.json b/public/language/sq-AL/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/sq-AL/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/cookies.json b/public/language/sq-AL/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/sq-AL/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/email.json b/public/language/sq-AL/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/sq-AL/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/sq-AL/admin/settings/general.json b/public/language/sq-AL/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/sq-AL/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/sq-AL/admin/settings/group.json b/public/language/sq-AL/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/sq-AL/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/guest.json b/public/language/sq-AL/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/sq-AL/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/homepage.json b/public/language/sq-AL/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/sq-AL/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/languages.json b/public/language/sq-AL/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/sq-AL/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/navigation.json b/public/language/sq-AL/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/sq-AL/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/sq-AL/admin/settings/notifications.json b/public/language/sq-AL/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/sq-AL/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/pagination.json b/public/language/sq-AL/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/sq-AL/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/post.json b/public/language/sq-AL/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/sq-AL/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/reputation.json b/public/language/sq-AL/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/sq-AL/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/social.json b/public/language/sq-AL/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/sq-AL/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/sockets.json b/public/language/sq-AL/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/sq-AL/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/sounds.json b/public/language/sq-AL/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/sq-AL/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/tags.json b/public/language/sq-AL/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/sq-AL/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/sq-AL/admin/settings/uploads.json b/public/language/sq-AL/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/sq-AL/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/sq-AL/admin/settings/user.json b/public/language/sq-AL/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/sq-AL/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/sq-AL/admin/settings/web-crawler.json b/public/language/sq-AL/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/sq-AL/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/sq-AL/category.json b/public/language/sq-AL/category.json new file mode 100644 index 0000000000..a8f0c123d3 --- /dev/null +++ b/public/language/sq-AL/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategoria", + "subcategories": "Nënkategoritë", + "new_topic_button": "Temë e re", + "guest-login-post": "Hyr për të postuar", + "no_topics": "Nuk ka tema në këtë kategori.
Pse nuk provon të postosh diçka?", + "browsing": "Duke Shfletuar", + "no_replies": "Askush nuk ka kthyer përgjigje", + "no_new_posts": "Nuk ka postime të reja", + "watch": "Shiko", + "ignore": "Injoro", + "watching": "Ndiq temën", + "not-watching": "Mos e ndiq temën", + "ignoring": "Injoro", + "watching.description": "Shfaq temat e fundit dhe të palexuara ", + "not-watching.description": "Mos shfaq temat e palexuara, shfaq vetem temat më të fundit", + "ignoring.description": "Mos shfaq temat e fundit dhe të palexuara", + "watching.message": "Tani je duke ndjekur përditësimet nga kjo kategori dhe të gjitha nënkategoritë e saj.", + "notwatching.message": "Tani nuk je duke ndjekur përditësimet nga kjo kategori dhe të gjitha nënkategoritë e saj.", + "ignoring.message": "Tani je duke injoruar përditësimet nga kjo kategori dhe të gjitha nënkategoritë e saj.", + "watched-categories": "Kategoritë që keni ndjekur", + "x-more-categories": "%1 më shumë kategori" +} \ No newline at end of file diff --git a/public/language/sq-AL/email.json b/public/language/sq-AL/email.json new file mode 100644 index 0000000000..efba38a58b --- /dev/null +++ b/public/language/sq-AL/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Testo email-in", + "password-reset-requested": "Rivendosja e fjalëkalimit u dërgua!", + "welcome-to": "Mirë se erdhe në %1", + "invite": "Ju ka ardhur ftesë nga %1", + "greeting_no_name": "Përshëndetje", + "greeting_with_name": "Përshëndetje %1", + "email.verify-your-email.subject": "Ju lutem verifikoni email-in tuaj!", + "email.verify.text1": "Ju keni kërkuar të ndryshojmë ose konfirmojmë adresën e email-it tuaj", + "email.verify.text2": "Për qëllime sigurie, ne ndryshojmë ose konfirmojmë adresën e emailit vetëm pasi të jetë konfirmuar pronësia e tij. Nëse nuk e keni kërkuar këtë ndryshim, nuk nevojitet asnjë veprim nga ana juaj.", + "email.verify.text3": "Sapo të konfirmoni këtë email, ne do të perditësojmë adresën tuaj aktuale të email-it me këtë të fundit. (%1)", + "welcome.text1": "Faleminderit që u regjistruat me %1!", + "welcome.text2": "Për të përfunduar krijimin e llogarisë, duhet të verifikojmë që ju zotëroni adresën e emailit me të cilën jeni regjistruar.", + "welcome.text3": "Një administrator ka pranuar aplikimin tuaj për regjistrim. Ju mund të identifikoheni me emrin e përdoruesit/fjalëkalimin tuaj tani.", + "welcome.cta": "Klikoni këtu për të konfirmuar adresën tuaj të email-it", + "invitation.text1": "%1 ju ka ftuar ti bashkoheni %2", + "invitation.text2": "Ftesa juaj do të skadojë në 1% ditë", + "invitation.cta": "Klikoni këtu për të krijuar llogarinë tuaj.", + "reset.text1": "Ne morëm një kërkesë për të rivendosur fjalëkalimin tuaj, ndoshta sepse e keni harruar atë. Nëse nuk është kështu, ju lutemi injoroni këtë email.", + "reset.text2": "Për të vazhduar me rivendosjen e fjalëkalimit, ju lutemi klikoni në lidhjen e mëposhtme:", + "reset.cta": "Klikoni këtu për të rivendosur fjalëkalimin tuaj", + "reset.notify.subject": "Fjalëkalimi u ndryshua me sukses", + "reset.notify.text1": "Po ju njoftojmë se në %1, fjalëkalimi juaj u ndryshua me sukses.", + "reset.notify.text2": "Nëse nuk e keni autorizuar këtë, ju lutemi njoftoni menjëherë një administrator të VIAL.", + "digest.latest_topics": "Temat e fundit nga %1", + "digest.top-topics": "Temat kryesore nga %1", + "digest.popular-topics": "Tema të njohura nga %1", + "digest.cta": "Klikoni këtu për të vizituar %1", + "digest.unsub.info": "Kjo përmbledhje ju është dërguar për shkak të abonimit tuaj.", + "digest.day": "Ditë", + "digest.week": "Javë", + "digest.month": "Muaj", + "digest.subject": "Përmblidh për %1", + "digest.title.day": "Përmbledhja juaj e përditshme", + "digest.title.week": "Përmbledhja juaj e përjavshme", + "digest.title.month": "Përmbledhja juaj mujore", + "notif.chat.subject": "Një mesazh i ri nga %1", + "notif.chat.cta": "Klikoni këtu për të vazhduar bisedën", + "notif.chat.unsub.info": "Ky njoftim për bisedën ju është dërguar për shkak të abonimit tuaj.", + "notif.post.unsub.info": "Ky njoftim i postimit ju është dërguar për shkak të abonimit tuaj.", + "notif.post.unsub.one-click": "Përndryshe, ndërprit marrjen e njoftimeve nga email-et e tjera si kjo duke klikuar", + "notif.cta": "Tek forumi", + "notif.cta-new-reply": "Shiko postimin", + "notif.cta-new-chat": "Shiko bisedën", + "notif.test.short": "Njoftimet e testimit", + "notif.test.long": "Ky është një test i emailit të njoftimeve. Kërko ndihmë!", + "test.text1": "Ky është një email provë për të verifikuar që dërguesi i emailit është konfiguruar saktë për forumin tuaj.", + "unsub.cta": "Klikoni këtu për të ndryshuar konfigurimet", + "unsubscribe": "Ç'regjistrohu", + "unsub.success": "Nuk do të merrni më emaile nga lista e postimeve %1", + "unsub.failure.title": "E pamundur të ç'regjistroheni", + "unsub.failure.message": "Fatkeqësisht, nuk mundëm t'ju çregjistronim nga lista e postimeve, pasi kishte një problem me linkun. Megjithatë, ju mund të ndryshoni preferencat tuaja të postës elektronike duke shkuar tek konfigurimet e përdoruesit

(gabim: %1)", + "banned.subject": "Ju jeni përjashtuar nga %1", + "banned.text1": "Përdoruesi %1 është përjashtuar nga %2.", + "banned.text2": "Ky përjashtim do të zgjasë deri më %1.", + "banned.text3": "Kjo është arsyeja pse jeni përjashtuar:", + "closing": "Faleminderit!" +} \ No newline at end of file diff --git a/public/language/sq-AL/error.json b/public/language/sq-AL/error.json new file mode 100644 index 0000000000..8c2c0b7d5e --- /dev/null +++ b/public/language/sq-AL/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Të dhëna të pavlefshme", + "invalid-json": "JSON i pavlefshëm", + "wrong-parameter-type": "Pritej një vlerë e tipit %3 për vetinë '%1', por në vend të saj u mor %2", + "required-parameters-missing": "Parametrat e kërkuar mungonin në këtë API: %1", + "not-logged-in": "Mesa duket nuk jeni identifikuar.", + "account-locked": "Llogaria juaj është bllokuar përkohësisht", + "search-requires-login": "Për të kërkuar ju duhet të keni një llogari - ju lutemi identifikohuni ose regjistrohuni.", + "goback": "Shtypni \"prapa\" për t'u kthyer në faqen e mëparshme", + "invalid-cid": "ID e kategorisë e pavlefshme", + "invalid-tid": "ID e temës e pavlefshme", + "invalid-pid": "ID e postimit e pavlefshme", + "invalid-uid": "ID e anëtarit e pavlefshme", + "invalid-mid": "ID e pavlefshme e mesazhit të bisedës", + "invalid-date": "Duhet të vendoset një datë e vlefshme", + "invalid-username": "Username i pasaktë", + "invalid-email": "Email i pasaktë", + "invalid-fullname": "Emri i plotë i pasaktë", + "invalid-location": "Vendndodhja e pasaktë", + "invalid-birthday": "Ditëlindja e pasaktë", + "invalid-title": "Titull i pasaktë", + "invalid-user-data": "Të dhënat e anëtarit janë të pasakta", + "invalid-password": "Fjalëkalim i pasaktë", + "invalid-login-credentials": "Kredencialet e hyrjes të pasakta", + "invalid-username-or-password": "Ju lutemi specifikoni një emër përdoruesi dhe fjalëkalimin.", + "invalid-search-term": "Term kërkimi i pasaktë", + "invalid-url": "URL e pasaktë", + "invalid-event": "Dicka shkoi keq: %1", + "local-login-disabled": "Sistemi lokal i identifikimit është çaktivizuar për llogaritë e thjeshta.", + "csrf-invalid": "Nuk mundëm t'ju identifikonim për shkak të mbarimit të sesionit. Ju lutemi provoni përsëri!", + "invalid-path": "Gabim", + "folder-exists": "Ky dokument ekziston", + "invalid-pagination-value": "Vlera e pasaktë e faqes, duhet të jetë së paku %1 dhe maksimumi %2", + "username-taken": "Username është i zënë", + "email-taken": "Email-i është i zënë", + "email-nochange": "Email-i i futur është i njëjtë me emailin ekzistues në sistem.", + "email-invited": "Email-i është ftuar më herët", + "email-not-confirmed": "Postimi në disa kategori ose tema aktivizohet pasi emaili juaj të konfirmohet, ju lutemi klikoni këtu për të dërguar një email konfirmimi.", + "email-not-confirmed-chat": "Ju nuk jeni në gjendje të bisedoni derisa emaili juaj të konfirmohet, ju lutemi klikoni këtu për të konfirmuar emailin tuaj.", + "email-not-confirmed-email-sent": "Email-i juaj nuk është konfirmuar ende, ju lutemi kontrolloni inboxin për emailin e konfirmimit. Mund të mos jeni në gjendje të postoni në disa kategori ose të bisedoni privatisht derisa emaili juaj të konfirmohet.", + "no-email-to-confirm": "Llogarisë tuaj i mungon një adresë email-i. Një email është i nevojshëm për rikuperimin e llogarisë dhe mund të jetë i nevojshëm për të biseduar dhe postuar në disa kategori. Ju lutemi klikoni këtu për të caktuar një email.", + "user-doesnt-have-email": "Përdoruesi \"%1\" nuk ka një email të regjistruar.", + "email-confirm-failed": "Nuk mund ta konfirmonim emailin tuaj, ju lutemi provoni sërish më vonë.", + "confirm-email-already-sent": "Email konfirmimi është dërguar tashmë, ju lutemi prisni %1 minut(a) për të dërguar një tjetër.", + "sendmail-not-found": "Ekzekutuesi sendmail nuk mund të gjendej, ju lutemi sigurohuni që ai të jetë i instaluar dhe i ekzekutueshëm nga përdoruesi që përdor NodeBB.", + "digest-not-enabled": "Ky përdorues nuk i ka të aktivizuara përmbledhjet ose sistemi nuk është konfiguruar për të dërguar përmbledhje", + "username-too-short": "Emri i përdoruesit është shumë i shkurtër", + "username-too-long": "Emri i përdoruesit është shumë i gjatë", + "password-too-long": "Fjalëkalimi është shumë i gjatë", + "reset-rate-limited": "Shumë kërkesa për rivendosjen e fjalëkalimit (norma është e kufizuar)", + "reset-same-password": "Ju lutemi përdorni një fjalëkalim që është i ndryshëm nga ai aktuali", + "user-banned": "Anëtari është i përjashtuar", + "user-banned-reason": "Na vjen keq, kjo llogari është pezulluar (Arsyeja: %1)", + "user-banned-reason-until": "Na vjen keq, kjo llogari është pezulluar deri më %1 (Arsyeja: %2)", + "user-too-new": "Na vjen keq, ju duhet të prisni %1 sekond(a) përpara se të bëni postimin tuaj të parë", + "blacklisted-ip": "Na vjen keq, por adresa juaj IP është bllokuar nga ky komunitet. Nëse mendoni se ka një gabim, ju lutemi kontaktoni një administrator të VIAL.", + "ban-expiry-missing": "Ju lutemi jepni një datë përfundimi për këtë pezullim", + "no-category": "Kategoria nuk ekziston", + "no-topic": "Tema nuk ekziston", + "no-post": "Postimi nuk ekziston", + "no-group": "Grupi nuk ekziston", + "no-user": "Përdoruesi nuk ekziston", + "no-teaser": "Përmbledhja nuk ekziston", + "no-flag": "Flag does not exist", + "no-privileges": "Nuk keni akses të mjaftueshem për këtë veprim.", + "category-disabled": "Kategori e çaktivizuar", + "topic-locked": "Temë e kyçur", + "post-edit-duration-expired": "Ju lejohet të redaktoni postimet vetëm për %1 sekond(a) pas postimit", + "post-edit-duration-expired-minutes": "Ju lejohet të redaktoni postimet vetëm për %1 minut(a) pas postimit", + "post-edit-duration-expired-minutes-seconds": "Ju lejohet të redaktoni postimet vetëm për %1 minut(a) %2 sekond(a) pas postimit", + "post-edit-duration-expired-hours": "Ju lejohet të redaktoni postimet vetëm për %1 orë() pas postimit", + "post-edit-duration-expired-hours-minutes": "Ju lejohet të redaktoni postimet vetëm për %1 orë() %2 minut(a) pas postimit", + "post-edit-duration-expired-days": "Ju lejohet të redaktoni postimet vetëm për %1 ditë() pas postimit", + "post-edit-duration-expired-days-hours": "Ju lejohet të redaktoni postimet vetëm për %1 ditë() %2 orë() pas postimit", + "post-delete-duration-expired": "Ju lejohet të fshini postimet vetëm për %1 sekond(a) pas postimit", + "post-delete-duration-expired-minutes": "Ju lejohet të fshini postimet vetëm për %1 minut(a) pas postimit", + "post-delete-duration-expired-minutes-seconds": "Ju lejohet të fshini postimet vetëm për %1 minut(a) %2 sekond(a) pas postimit", + "post-delete-duration-expired-hours": "Ju lejohet të fshini postimet vetëm për %1 orë() pas postimit", + "post-delete-duration-expired-hours-minutes": "Ju lejohet të fshini postimet vetëm për %1 orë() %2 minut(a) pas postimit", + "post-delete-duration-expired-days": "Ju lejohet të fshini postimet vetëm për %1 ditë() pas postimit", + "post-delete-duration-expired-days-hours": "Ju lejohet të fshini postimet vetëm për %1 ditë() %2 orë() pas postimit", + "cant-delete-topic-has-reply": "Nuk mund ta fshish temën pasi të ketë një koment", + "cant-delete-topic-has-replies": "Nuk mund ta fshish temën pasi të ketë %1 komente", + "content-too-short": "Ju lutemi shkruani një tekst më të gjatë. Teksti duhet të përmbajë të paktën %1 karakter(e)", + "content-too-long": "Ju lutemi shkruani një tekst më të shkurtër. Tekstet nuk mund të jenë më të gjata se %1 karakter(e).", + "title-too-short": "Ju lutemi shkruani një titull më të gjatë. Titujt duhet të përmbajnë të paktën %1 karakter(e)", + "title-too-long": "Ju lutemi shkruani një titull më të shkurtër. Titujt nuk mund të jenë më të gjatë se %1 karakter(e).", + "category-not-selected": "Kategoria nuk është zgjedhur.", + "too-many-posts": "Mund të postoni vetëm një herë në %1 sekond(a) - ju lutemi prisni përpara se të postoni përsëri", + "too-many-posts-newbie": "Si përdorues i ri, ju mund të postoni vetëm një herë në %1 sekond(a) derisa të keni fituar %2 reputacion - ju lutemi prisni përpara se të postoni përsëri", + "already-posting": "You are already posting", + "tag-too-short": "Ju lutemi vendosni një tag më të gjatë. Tag-et duhet të përmbajnë të paktën %1 karakter(e)", + "tag-too-long": "Ju lutemi vendosni një tag më të shkurtër. Tag-et nuk mund të jenë më të gjata se %1 karakter(e)", + "not-enough-tags": "Numër jo i mjaftueshëm i tag-eve. Temat duhet të kenë të paktën %1 tag(-e)", + "too-many-tags": "Shumë tag-e. Temat nuk mund të kenë më shumë se %1 tag(-e)", + "cant-use-system-tag": "Ju nuk mund ta përdorni këtë tag sistemi", + "cant-remove-system-tag": "Ju nuk mund ta hiqni këtë tag sistemi", + "still-uploading": "Ju lutem prisni derisa ngarkimet të mbarojnë.", + "file-too-big": "Madhësia maksimale e lejuar e materialit është %1 kB - ngarkoni një material më të vogël", + "guest-upload-disabled": "Ngarkimi nga vizitorëve është i çaktivizuar", + "cors-error": "Imazhi nuk mund të ngarkohet për shkak të konfigurimit të gabuar të CORS", + "upload-ratelimit-reached": "Ju keni ngarkuar shumë materiale në të njëjtën kohë. Ju lutemi provoni sërish më vonë.", + "scheduling-to-past": "Ju lutemi zgjidhni një datë në të ardhmen.", + "invalid-schedule-date": "Ju lutemi shkruani një datë dhe orë të vlefshme.", + "cant-pin-scheduled": "Temat e planifikuara nuk mund të (ç)fiksohen.", + "cant-merge-scheduled": "Temat e planifikuara nuk mund të bashkohen.", + "cant-move-posts-to-scheduled": "Postimet nuk mund të zhvendosen në një temë të planifikuar.", + "cant-move-from-scheduled-to-existing": "Postimet nuk mund të zhvendosen nga një temë e planifikuar në një temë ekzistuese.", + "already-bookmarked": "Ju e keni ruajtur tashmë këtë postim", + "already-unbookmarked": "Tashmë nuk e keni më të ruajtur këtë postim", + "cant-ban-other-admins": "Nuk mund të përjashtoni administratorë të tjerë.", + "cant-mute-other-admins": "Ju nuk mund të bëni mute administratorët e tjerë", + "user-muted-for-hours": "Ju jeni bërë mute, dhe do të mundeni të postoni në %1 orë()", + "user-muted-for-minutes": "Ju jeni bërë mute, dhe do të mundeni të postoni në %1 minut(a)", + "cant-make-banned-users-admin": "Ju nuk mund t'i bëni përdoruesit e ndaluar administrator.", + "cant-remove-last-admin": "Ju jeni i vetmi administrator. Shtoni një përdorues tjetër si administrator përpara se të hiqni veten si administrator", + "account-deletion-disabled": "Fshirja e llogarisë është çaktivizuar", + "cant-delete-admin": "Hiqni aksesin e administratorit nga kjo llogari përpara se të përpiqeni ta fshini atë.", + "already-deleting": "Tashmë po fshihet", + "invalid-image": "Imazh jo i duhur.", + "invalid-image-type": "Lloji i imazhit nuk është i duhuri. Llojet e lejuara janë: %1", + "invalid-image-extension": "Shtesa e pasakte e imazhit", + "invalid-file-type": "Lloj i pavlefshëm i skedarit. Llojet e lejuara janë: %1", + "invalid-image-dimensions": "Dimensionet e imazhit janë shumë të mëdha", + "group-name-too-short": "Emri i grupit është shumë i shkurtër", + "group-name-too-long": "Emri i grupit është shumë i gjatë", + "group-already-exists": "Grupi ekziston", + "group-name-change-not-allowed": "Ndryshimi i emrit të grupit nuk lejohet", + "group-already-member": "Jeni pjesë e këtij grupi", + "group-not-member": "Nuk është anëtar i këtij grupi", + "group-needs-owner": "Ky grup kërkon të paktën një administrator", + "group-already-invited": "Ky përdorues është ftuar tashmë", + "group-already-requested": "Kërkesa juaj për anëtarësim është dorëzuar tashmë", + "group-join-disabled": "Nuk mund t'i bashkohesh këtij grupi për momentin", + "group-leave-disabled": "Nuk mund të largohesh nga ky grup në këtë moment", + "post-already-deleted": "Ky postim tashmë është fshirë", + "post-already-restored": "Ky postim tashmë është rikthyer", + "topic-already-deleted": "Kjo temë tashmë është fshirë", + "topic-already-restored": "Kjo temë tashmë është rikthyer", + "cant-purge-main-post": "Ju nuk mund të fshini postimin kryesor, ju lutemi fshini temën në vend të saj", + "topic-thumbnails-are-disabled": "Miniaturat e temës janë çaktivizuar.", + "invalid-file": "Dokument i pavlefshëm", + "uploads-are-disabled": "Ngarkimet janë çaktivizuar", + "signature-too-long": "Na vjen keq, nënshkrimi juaj nuk mund të jetë më i gjatë se %1 karakter(e).", + "about-me-too-long": "Na vjen keq, por përshkrimi nuk mund të jetë më i gjatë se %1 karakter(e).", + "cant-chat-with-yourself": "Nuk mund të bësh bashkëbisedim me veten!", + "chat-restricted": "Ky përdorues ka kufizuar mesazhet e tij. Duhet t'ju ndjekin përpara se të bisedoni të", + "chat-disabled": "Sistemi i bisedës është çaktivizuar", + "too-many-messages": "Ju keni dërguar shumë mesazhe, ju lutemi prisni pak.", + "invalid-chat-message": "Mesazh i pasaktë në bisedë", + "chat-message-too-long": "Mesazhet e bisedës nuk mund të jenë më të gjata se %1 karaktere.", + "cant-edit-chat-message": "Nuk ju lejohet ta modifikoni këtë mesazh", + "cant-delete-chat-message": "Nuk ju lejohet ta fshini këtë mesazh", + "chat-edit-duration-expired": "Ju lejohet të modifikoni mesazhet e bisedës vetëm për %1 sekond(a) pas postimit", + "chat-delete-duration-expired": "Ju lejohet të fshini mesazhet e bisedës vetëm për %1 sekond(a) pas postimit", + "chat-deleted-already": "Ky mesazh është fshirë tashmë.", + "chat-restored-already": "Ky mesazh është rikthyer tashmë.", + "chat-room-does-not-exist": "Kjo dhomë bisede nuk ekziston.", + "already-voting-for-this-post": "Ju keni votuar tashmë për këtë postim.", + "reputation-system-disabled": "Sistemi i reputacionit është i çaktivizuar.", + "downvoting-disabled": "Votimi kundër është i çaktivizuar", + "not-enough-reputation-to-chat": "Ju nevojitet %1 reputacion për të biseduar", + "not-enough-reputation-to-upvote": "Ju nevojitet %1 reputacion për të votuar pro", + "not-enough-reputation-to-downvote": "Ju nevojitet %1 reputacion për të votuar kundër", + "not-enough-reputation-to-flag": "Ju nevojitet %1 reputacion për të raportuar postimin", + "not-enough-reputation-min-rep-website": "Ju nevojitet %1 reputacion për të shtuar një faqe interneti", + "not-enough-reputation-min-rep-aboutme": "Ju nevojitet %1 reputacion për të shtuar një seksion 'Rreth Meje'", + "not-enough-reputation-min-rep-signature": "Ju nevojitet %1 reputacion për të shtuar një firmë", + "not-enough-reputation-min-rep-profile-picture": "Ju nevojitet %1 reputacion për të shtuar një foto profili", + "not-enough-reputation-min-rep-cover-picture": "Ju nevojitet %1 reputacion për të shtuar një foto kopertine", + "post-already-flagged": "Ju tashmë e keni raportuar këtë postim", + "user-already-flagged": "Ju e keni raportuar tashmë këtë përdorues", + "post-flagged-too-many-times": "Ky postim është raportuar tashmë nga të tjerë", + "user-flagged-too-many-times": "Ky përdorues tashmë është raportuar nga të tjerë", + "cant-flag-privileged": "Nuk ju lejohet të raportoni profilet ose përmbajtjen e përdoruesve të privilegjuar (moderatorët/administratorët)", + "self-vote": "Ju nuk mund të votoni për postimin tuaj", + "too-many-upvotes-today": "Ju mund të votoni pro vetëm %1 herë në ditë", + "too-many-upvotes-today-user": "Ju mund të votoni një përdorues %1 herë në ditë", + "too-many-downvotes-today": "Mund të votosh vetëm %1 herë në ditë", + "too-many-downvotes-today-user": "Ju mund të votoni kundër një përdoruesi vetëm %1 herë në ditë", + "reload-failed": "NodeBB hasi në një problem gjatë ringarkimit: \"%1\". NodeBB do të vazhdojë t'i shërbejë aseteve ekzistuese të klientit, megjithëse duhet të zhbëni atë që keni bërë pak para rifreskimit.", + "registration-error": "Gabim në regjistrim", + "parse-error": "Diçka shkoi keq gjatë analizimit të përgjigjes së serverit", + "wrong-login-type-email": "Ju lutemi përdorni emailin tuaj për t'u identifikuar", + "wrong-login-type-username": "Ju lutemi përdorni emrin tuaj të përdoruesit për t'u identifikuar", + "sso-registration-disabled": "Regjistrimi është çaktivizuar për llogaritë %1, ju lutemi regjistrohuni fillimisht me një adresë emaili", + "sso-multiple-association": "Ju nuk mund të lidhni shumë llogari nga ky shërbim me llogarinë tuaj NodeBB. Ju lutemi shkëputni llogarinë tuaj ekzistuese dhe provoni përsëri.", + "invite-maximum-met": "Ju keni ftuar numrin maksimal të njerëzve (%1 nga %2).", + "no-session-found": "Nuk u gjet asnjë seancë identifikimi!", + "not-in-room": "Përdoruesi nuk është në dhomën e bisedës", + "cant-kick-self": "Nuk mund ta largosh veten nga grupi", + "no-users-selected": "Nuk është zgjedhur asnjë përdorues()", + "invalid-home-page-route": "Link i pavlefshëm", + "invalid-session": "Sesion i pavlefshëm", + "invalid-session-text": "Duket sikur sesioni juaj i hyrjes nuk është më aktiv. Ju lutemi rifreskojeni këtë faqe.", + "session-mismatch": "Mospërputhje e sesionit të identifikimit", + "session-mismatch-text": "Duket sikur sesioni juaj i hyrjes nuk përputhet më me serverin. Ju lutemi rifreskojeni këtë faqe.", + "no-topics-selected": "Asnjë temë e zgjedhur!", + "cant-move-to-same-topic": "Postimi nuk mund të zhvendoset në të njëjtën temë!", + "cant-move-topic-to-same-category": "Tema nuk mund të zhvendoset në të njëjtën kategori!", + "cannot-block-self": "Ju nuk mund të bllokoni veten!", + "cannot-block-privileged": "Ju nuk mund të bllokoni administratorët ose moderatorët", + "cannot-block-guest": "Vizitorët nuk mund të bllokojnë përdoruesit e tjerë", + "already-blocked": "Ky përdorues është tashmë i përjashtuar", + "already-unblocked": "Ky përdorues është tashmë i zhbllokuar", + "no-connection": "Duket se ka një problem me lidhjen tuaj të internetit", + "socket-reconnect-failed": "Nuk mund të arrihet serveri në këtë moment. Kliko këtu për të provuar përsëri, ose provo më vonë", + "plugin-not-whitelisted": "Nuk mund të instalohet plugin – vetëm shtojcat e listuara në listën e bardhë nga Menaxheri i Paketave të NodeBB mund të instalohen nëpërmjet ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Ngjarja e temës '%1' nuk njihet", + "cant-set-child-as-parent": "Nuk mund të vendoset si kategori mëmë", + "cant-set-self-as-parent": "Nuk mund të vendosësh veten si kategori mëmë", + "api.master-token-no-uid": "Një token kryesor u mor pa një `_uid` përkatëse në fushën e kërkesës", + "api.400": "Diçka nuk ishte në rregull me ngarkesën që keni kaluar.", + "api.401": "Nuk u gjet një sesion i vlefshëm identifikimi. Ju lutemi identifikohuni dhe provoni përsëri.", + "api.403": "Ju nuk jeni i autorizuar për ta bërë këtë thirrje", + "api.404": "Thirrje e pasakte e API", + "api.426": "Kërkohet HTTPS për kërkesat në api, ju lutemi ridërgojeni kërkesën tuaj nëpërmjet HTTPS", + "api.429": "Ju keni bërë shumë kërkesa, ju lutemi provoni përsëri më vonë", + "api.500": "Një gabim i papritur u ndesh gjatë përpjekjes për të kryer kërkesën tuaj.", + "api.501": "Itinerari që kërkoni nuk është zbatuar ende, ju lutemi provoni sërish nesër", + "api.503": "Itinerari që kërkoni nuk është aktualisht i disponueshëm për shkak të një konfigurimi të serverit" +} \ No newline at end of file diff --git a/public/language/sq-AL/flags.json b/public/language/sq-AL/flags.json new file mode 100644 index 0000000000..0f927e067c --- /dev/null +++ b/public/language/sq-AL/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Gjendja", + "reports": "Raportet", + "first-reported": "Raportuar për herë të parë", + "no-flags": "Juhu! Nuk u gjet asnje gabim.", + "assignee": "Përfituesi", + "update": "Përditëso", + "updated": "I përditësuar", + "resolved": "E zgjidhur", + "target-purged": "Përmbajtja të cilës i referohet ky raportim është fshire dhe nuk disponohet më.", + + "graph-label": "Raportimet ditore", + "quick-filters": "Filtra të shpejtë", + "filter-active": "Ka një ose më shumë filtra aktivë në këtë listë raportimesh", + "filter-reset": "Hiqni filtrat", + "filters": "Opsionet e filtrit", + "filter-reporterId": "UID e reporterit", + "filter-targetUid": "UID e shënuar", + "filter-type": "Lloji i raportimit", + "filter-type-all": "E gjithë Përmbajtja", + "filter-type-post": "Postim", + "filter-type-user": "Përdorues", + "filter-state": "Gjendja", + "filter-assignee": "Përfituesi UID", + "filter-cid": "Kategoria", + "filter-quick-mine": "Më është caktuar mua", + "filter-cid-all": "Të gjitha kategoritë", + "apply-filters": "Apliko filtrin", + "more-filters": "Më shume filtra", + "fewer-filters": "Më pak filtra", + + "quick-actions": "Veprimet e shpejta", + "flagged-user": "Përdorues i raportuar", + "view-profile": "Shiko Profilin", + "start-new-chat": "Fillo një bisedë të re", + "go-to-target": "Shiko objektivin e raportimit", + "assign-to-me": "Ma cakto mua", + "delete-post": "Fshij postimin", + "purge-post": "Pastro postimin", + "restore-post": "Rikthe postimin", + "delete": "Fshi raportimin", + + "user-view": "Shiko Profilin", + "user-edit": "Rregullo Profilin", + + "notes": "Shënime nga raportimet", + "add-note": "Shtoni shënim", + "no-notes": "Nuk ka shënime të përbashkëta.", + "delete-note-confirm": "Je i sigurt që dëshiron ta fshish këtë shënim?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Shënimi u shtua", + "note-deleted": "Shënimi u fshi", + "flag-deleted": "Flag Deleted", + + "history": "Llogaria & Historia e raportimeve", + "no-history": "Nuk ka histori raportuese", + + "state-all": "Të gjitha gjendjet", + "state-open": "E re/e hapur", + "state-wip": "Në progres", + "state-resolved": "E zgjidhur", + "state-rejected": "I refuzuar", + "no-assignee": "Nuk është caktuar", + + "sort": "Ndaj sipas", + "sort-newest": "Më të rejat ne fillim", + "sort-oldest": "Më të vjetrat në filim", + "sort-reports": "Shumica e raporteve", + "sort-all": "Të gjitha llojet e raportimeve...", + "sort-posts-only": "Vetëm postime...", + "sort-downvotes": "Më pak të pëlqyerat ", + "sort-upvotes": "Më të pëlqyerat", + "sort-replies": "Më të komentuarat", + + "modal-title": "Raportoni përmbajtjen", + "modal-body": "Ju lutemi specifikoni arsyen tuaj për raportimin e %1 %2 për shqyrtim. Përndryshe, përdorni një nga butonat e raportimit të shpejtë nëse është e aplikueshme.", + "modal-reason-spam": "Të bllokuara", + "modal-reason-offensive": "Ofenduese", + "modal-reason-other": "Të tjera (specifikoni më poshtë)", + "modal-reason-custom": "Arsyeja e raportimit të kësaj përmbajtjeje...", + "modal-submit": "Dërgo raportin", + "modal-submit-success": "Përmbajtja është raportuar për moderim", + + "bulk-actions": "Veprime në mas", + "bulk-resolve": "Zgjidhja e raportim(eve)", + "bulk-success": "%1 raportime u përditësuan", + "flagged-timeago-readable": "I raportuar (% 2)", + "auto-flagged": "[I vetë Raportuar] Mori %1 vota kundër." +} \ No newline at end of file diff --git a/public/language/sq-AL/global.json b/public/language/sq-AL/global.json new file mode 100644 index 0000000000..ff92679fd4 --- /dev/null +++ b/public/language/sq-AL/global.json @@ -0,0 +1,126 @@ +{ + "home": "Kreu", + "search": "Kërko", + "buttons.close": "Mbyll", + "403.title": "Hyrja u ndalua", + "403.message": "Ju duket se keni arritur në një faqe në të cilën nuk keni akses.", + "403.login": "Ndoshta duhet të provoni të regjistroheni?", + "404.title": "Nuk u gjet", + "404.message": "Ju duket se keni ngelur në një faqe që nuk ekziston. Kthehu në faqen kryesore. ", + "500.title": "Gabim i brendshëm.", + "500.message": "Ups! Diçka nuk shkoi mirë!", + "400.title": "Kërkesë e pasaktë.", + "400.message": "Me sa duket kjo lidhje është jo e sigurt, ju lutemi kontrolloni dhe provoni përsëri. Përndryshe, kthehuni në faqen kryesore.", + "register": "Regjistrohu", + "login": "Hyr", + "please_log_in": "Ju lutemi Identifikohu", + "logout": "Dil", + "posting_restriction_info": "Postimi aktualisht është i kufizuar vetëm për anëtarët e regjistruar, klikoni këtu për t'u identifikuar.", + "welcome_back": "Mirë se u kthyet", + "you_have_successfully_logged_in": "Ju keni hyrë me sukses", + "save_changes": "Ruaj ndryshimet", + "save": "Ruaj", + "close": "Mbyll", + "pagination": "Numërim Faqesh", + "pagination.out_of": "%1 nga %2", + "pagination.enter_index": "Shkoni te indeksi i postimit", + "header.admin": "Administratorët", + "header.categories": "KATEGORITË", + "header.recent": "TË FUNDIT", + "header.unread": "TË PALEXUARA", + "header.tags": "TAGS", + "header.popular": "MË TË NJOHURAT", + "header.top": "KRYESORET", + "header.users": "PËRDORUESIT", + "header.groups": "GRUPET", + "header.chats": "BISEDAT", + "header.notifications": "NJOFTIME", + "header.search": "Kërko", + "header.profile": "Profili", + "header.navigation": "Lundrim", + "notifications.loading": "Njoftimet po ngarkohen", + "chats.loading": "Po ngarkohen bisedat", + "motd.welcome": "Mirë se vini në NodeBB, platformën e diskutimit të së ardhmes.", + "previouspage": "Faqja e mëparshme", + "nextpage": "Faqja tjetër", + "alert.success": "Sukses", + "alert.error": "Gabim", + "alert.banned": "I ndaluar", + "alert.banned.message": "Sapo je ndaluar, aksesi jot tani është i kufizuar.", + "alert.unbanned": "E pandaluar", + "alert.unbanned.message": "Ndalimi juaj është hequr.", + "alert.unfollow": "Nuk po ndiqni më %1!", + "alert.follow": "Tani po ndiqni %1!", + "users": "Përdoruesit", + "topics": "Temat", + "posts": "Postimet", + "x-posts": "%1 postime", + "best": "Më të mirat", + "controversial": "E diskutueshme", + "votes": "Votat", + "x-votes": "%1 vota", + "voters": "Votuesit", + "upvoters": "Votuesit Pro", + "upvoted": "Votoi pro", + "downvoters": "Votuesit Kundër", + "downvoted": "Votoi kundër", + "views": "Shikimet", + "posters": "Banera", + "reputation": "Reputacioni", + "lastpost": "Postimi i fundit", + "firstpost": "Postimi i parë", + "read_more": "Lexo më shumë", + "more": "Më shumë", + "none": "Asnjë", + "posted_ago_by_guest": "postuar %1 nga Vizitori", + "posted_ago_by": "postuar %1 nga %2", + "posted_ago": "postuar %1", + "posted_in": "postuar ne %1", + "posted_in_by": "postuar %1 nga %2", + "posted_in_ago": "postuar ne %1 %2", + "posted_in_ago_by": "postuar %1 %2 nga %3", + "user_posted_ago": "%1 postoi %2", + "guest_posted_ago": "Vizitori postoi %1", + "last_edited_by": "Modifikuar së fundi nga %1", + "norecentposts": "Nuk ka postime të fundit", + "norecenttopics": "Nuk ka tema të fundit", + "recentposts": "Postimet e fundit", + "recentips": "IP-të e regjistruara së fundi", + "moderator_tools": "Mjetet e Moderatorit", + "online": "Online", + "away": "Kam ikur", + "dnd": "Mos më shqetësoni", + "invisible": "E padukshme", + "offline": "Jashtë linje", + "email": "Email", + "language": "Gjuha", + "guest": "I ftuar", + "guests": "Të ftuarit", + "former_user": "Një Ish Përdorues", + "system-user": "Sistemi", + "unknown-user": "Përdorues i panjohur", + "updated.title": "Forumi u përditësua", + "updated.message": "Ky forum sapo është përditësuar në versionin më të fundit. Klikoni këtu për të rifreskuar faqen.", + "privacy": "Privatësia", + "follow": "Ndiqni", + "unfollow": "Hiq", + "delete_all": "Fshiji te gjitha", + "map": "Harta", + "sessions": "Sesionet e hyrjes", + "ip_address": "Adresa IP", + "enter_page_number": "Fut numrin e faqes", + "upload_file": "Ngarko materialin", + "upload": "Ngarko", + "uploads": "Ngarkime", + "allowed-file-types": "Llojet e lejuara të skedarëve janë %1", + "unsaved-changes": "Ju keni ndryshime të paruajtura. Jeni i sigurt që dëshironi të largoheni?", + "reconnecting-message": "Me sa duket lidhja jote me %1 ka humbur, ju lutemi prisni derisa të përpiqemi të rilidhemi.", + "play": "Luaj", + "cookies.message": "Kjo faqe interneti përdor cookie për tu siguruar që ju të keni përvojën më të mirë në VIAL.", + "cookies.accept": "E kuptova!", + "cookies.learn_more": "Mëso më shumë", + "edited": "U rregullua", + "disabled": "Zhblloko", + "select": "Zgjidh", + "user-search-prompt": "Shkruani diçka këtu për të gjetur përdorues..." +} \ No newline at end of file diff --git a/public/language/sq-AL/groups.json b/public/language/sq-AL/groups.json new file mode 100644 index 0000000000..aac022d0fb --- /dev/null +++ b/public/language/sq-AL/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupe", + "view_group": "Shiko grupin", + "owner": "Zotuesi i grupit", + "new_group": "Krijo një grup të ri", + "no_groups_found": "Nuk ka grupe për të parë", + "pending.accept": "Prano", + "pending.reject": "Refuzo", + "pending.accept_all": "Prano te gjitha", + "pending.reject_all": "Refuzo te gjitha", + "pending.none": "Nuk ka anëtarë në pritje për momentin", + "invited.none": "Nuk ka anëtarë të ftuar në këtë moment", + "invited.uninvite": "Hiq ftesën", + "invited.search": "Kërkoni një përdorues për ta ftuar në këtë grup", + "invited.notification_title": "Jeni ftuar të bashkoheni me %1 ", + "request.notification_title": "Kërkesë për anëtarësim në grup nga % 1", + "request.notification_text": "%1 ka kërkuar të bëhet anëtar i %2", + "cover-save": "Ruaj", + "cover-saving": "Duke u ruajtur", + "details.title": "Detajet e grupit", + "details.members": "Lista e Anëtarëve", + "details.pending": "Anëtarët në pritje", + "details.invited": "Anëtarët e ftuar", + "details.has_no_posts": "Anëtarët e këtij grupi nuk kanë bërë asnjë postim.", + "details.latest_posts": "Postimet e fundit", + "details.private": "Private", + "details.disableJoinRequests": "Çaktivizo kërkesat për bashkim", + "details.disableLeave": "Mos lejoni përdoruesit të largohen nga grupi", + "details.grant": "Dhënia/Shfuqizimi i Pronësisë", + "details.kick": "Largo", + "details.kick_confirm": "Jeni i sigurt që dëshironi ta hiqni këtë anëtar nga grupi?", + "details.add-member": "Shto Anëtar", + "details.owner_options": "Administrimi i grupit", + "details.group_name": "Emri i grupit", + "details.member_count": "Numri i anëtarëve", + "details.creation_date": "Data e krijimit", + "details.description": "Përshkrim", + "details.member-post-cids": "ID-të e kategorive për të shfaqur postimet nga", + "details.badge_preview": "Pamja paraprake e medaljes", + "details.change_icon": "Ndrysho ikonën", + "details.change_label_colour": "Ndrysho ngjyrën e kontureve", + "details.change_text_colour": "Ndrysho ngjyrën e tekstit", + "details.badge_text": "Teksti i medaljes", + "details.userTitleEnabled": "Shfaq medaljen", + "details.private_help": "Nëse aktivizohet, bashkimi i grupeve kërkon miratimin nga një pronar grupi", + "details.hidden": "I fshehur", + "details.hidden_help": "Nëse aktivizohet, ky grup nuk do të gjendet në listën e grupeve dhe përdoruesit do të duhet të ftohen manualisht", + "details.delete_group": "Fshij grupin", + "details.private_system_help": "Grupet private janë çaktivizuar në nivel sistemi, ky opsion nuk bën asgjë", + "event.updated": "Detajet e grupit janë përditësuar", + "event.deleted": "Grupi \"%1\" është fshirë", + "membership.accept-invitation": "Prano Ftesën", + "membership.accept.notification_title": "Tani jeni anëtar i %1", + "membership.invitation-pending": "Ftesa në pritje", + "membership.join-group": "Bashkohu në grup", + "membership.leave-group": "Dil nga grupi", + "membership.leave.notification_title": "% 1 ka lënë grupin % 2", + "membership.reject": "Refuzo", + "new-group.group_name": "Emri i grupit:", + "upload-group-cover": "Ngarko foton e coverit për grupin", + "bulk-invite-instructions": "Futni një listë të emrave të përdoruesve të ndarë me presje për t'i ftuar në këtë grup", + "bulk-invite": "Ftesë me shumicë", + "remove_group_cover_confirm": "Jeni i sigurt që dëshironi ta hiqni foton e coverit?" +} \ No newline at end of file diff --git a/public/language/sq-AL/ip-blacklist.json b/public/language/sq-AL/ip-blacklist.json new file mode 100644 index 0000000000..8c76312e2e --- /dev/null +++ b/public/language/sq-AL/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Konfiguro listën e zezë të IP-së këtu.", + "description": "Herë pas here, një bllokim i llogarisë së përdoruesit nuk është një pengesë e mjaftueshme. Nganjëhere, kufizimi i aksesit në forum në një IP të caktuar ose një sërë IP-sh është mënyra më e mirë për të mbrojtur një forum. Në këto skenarë, ju mund të shtoni adresa IP problematike ose blloqe të tëra CIDR në këtë listë të zezë dhe ato do të parandalohen nga hyrja ose regjistrimi i një llogarie të re.", + "active-rules": "Rregullat aktive", + "validate": "Vërteto listën e zezë", + "apply": "Aplikoni listën e zezë", + "hints": "Këshilla sintaksore", + "hint-1": "Përcaktoni një adresë IP të vetme për rresht. Mund të shtoni blloqe IP për sa kohë që ato ndjekin formatin CIDR (p.sh. 192.168.100.0/22).", + "hint-2": "Mund të shtoni në komente duke filluar rreshtat me simbolin #.", + + "validate.x-valid": "% 1 nga %2 rregull(e) të vlefshme.", + "validate.x-invalid": "Rregullat e mëposhtme % 1 janë të pavlefshme:", + + "alerts.applied-success": "Lista e zezë u aplikua", + + "analytics.blacklist-hourly": "Figura 1 – Goditjet në listën e zezë në orë", + "analytics.blacklist-daily": "Figura 2 – Hitet në listën e zezë në ditë", + "ip-banned": "IP e ndaluar" +} \ No newline at end of file diff --git a/public/language/sq-AL/language.json b/public/language/sq-AL/language.json new file mode 100644 index 0000000000..3dc71d240e --- /dev/null +++ b/public/language/sq-AL/language.json @@ -0,0 +1,5 @@ +{ + "name": "Shqip", + "code": "sq-AL", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/sq-AL/login.json b/public/language/sq-AL/login.json new file mode 100644 index 0000000000..68d155724a --- /dev/null +++ b/public/language/sq-AL/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Emri i përdoruesit / Email", + "username": "Emri i përdoruesit", + "remember_me": "Më mban mend?", + "forgot_password": "Harruat fjalëkalimin?", + "alternative_logins": "Hyrjet alternative", + "failed_login_attempt": "Identifikimi i pasuksesshëm", + "login_successful": "Ju keni hyrë me sukses në forum!", + "dont_have_account": "Nuk keni një llogari?", + "logged-out-due-to-inactivity": "Ju keni dalë nga paneli i kontrollit të administratorit për shkak të pasivitetit", + "caps-lock-enabled": "Caps Lock është aktivizuar" +} \ No newline at end of file diff --git a/public/language/sq-AL/modules.json b/public/language/sq-AL/modules.json new file mode 100644 index 0000000000..6c44d4c9b9 --- /dev/null +++ b/public/language/sq-AL/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Bisedo me", + "chat.placeholder": "Shkruani mesazhin e bisedës këtu, tërhiqni dhe lëshoni imazhet, shtypni enter për t'i dërguar", + "chat.scroll-up-alert": "Po shikoni mesazhet e vjetra, klikoni këtu për të shkuar te mesazhet më të fundit.", + "chat.send": "Dërgo", + "chat.no_active": "Ju nuk keni biseda aktive.", + "chat.user_typing": "%1 është duke shkruajtur...", + "chat.user_has_messaged_you": "%1 ju ka dërguar mesazh.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Ju lutemi zgjidhni një person për të parë historikun e mesazheve të bisedës", + "chat.no-users-in-room": "Jo përdorues në këtë hapësirë", + "chat.recent-chats": "Bisedat e fundit", + "chat.contacts": "Kontaktet", + "chat.message-history": "Historia e mesazheve", + "chat.message-deleted": "Mesazh i fshirë", + "chat.options": "Opsionet e bisedës", + "chat.pop-out": "Veco bisedën", + "chat.minimize": "Minimizo", + "chat.maximize": "Maksimizo", + "chat.seven_days": "7 Ditë", + "chat.thirty_days": "30 Ditë", + "chat.three_months": "3 Muaj", + "chat.delete_message_confirm": "A je i sigurt që dëshiron ta fshihni këtë mesazh?", + "chat.retrieving-users": "Duke marrë përdoruesit...", + "chat.manage-room": "Menaxho hapësirën e bisedave", + "chat.add-user-help": "Kërkoni për përdoruesit këtu. Kur zgjidhet, përdoruesi do të shtohet në bisedë. Përdoruesi i ri nuk do të jetë në gjendje të shohë mesazhet e bisedës të shkruara përpara se të shtoheshin në bisedë. Vetëm krijuesit e bisedes () mund të heqin përdoruesit nga hapesirat e bisedës.", + "chat.confirm-chat-with-dnd-user": "Ky përdorues ka vendosur statusin e tij në (Mos shqetëso). Dëshiron ende të bisedosh me ta?", + "chat.rename-room": "Riemërto dhomën", + "chat.rename-placeholder": "Shkruani emrin e dhomës tuaj këtu", + "chat.rename-help": "Emri i dhomës i vendosur këtu do të jetë i dukshëm nga të gjithë pjesëmarrësit në dhomë.", + "chat.leave": "Largohu nga biseda", + "chat.leave-prompt": "Jeni i sigurt që dëshironi të largoheni nga kjo bisedë?", + "chat.leave-help": "Largimi nga kjo bisedë do t'ju heqë nga korrespondenca e ardhshme në këtë bisedë. Nëse do të rishtoheni në të ardhmen, nuk do të shihni asnjë histori bisede nga para ribashkimit.", + "chat.in-room": "Në këtë dhomë", + "chat.kick": "Largo", + "chat.show-ip": "Shfaq IP", + "chat.owner": "Administratori i hapësirës", + "chat.system.user-join": "%1 i është bashkuar hapësirës", + "chat.system.user-leave": "%1 ka dalë nga hapësira", + "chat.system.room-rename": "%2 e ka riemërtuar këtë hapësirë: %1", + "composer.compose": "Harto", + "composer.show_preview": "Shiko rezultatin", + "composer.hide_preview": "Mbulo rezultatin", + "composer.user_said_in": "%1 tha në %2:", + "composer.user_said": "%1 tha:", + "composer.discard": "Jeni i sigurt që dëshironi ta hiqni këtë postim?", + "composer.submit_and_lock": "Dorëzo dhe izolo", + "composer.toggle_dropdown": "Aktivizo Dropdown", + "composer.uploading": "Ngarkimi %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "List", + "composer.formatting.strikethrough": "Kalo nëpërmjet", + "composer.formatting.code": "Kodi", + "composer.formatting.link": "Linku", + "composer.formatting.picture": "Linku i imazhit", + "composer.upload-picture": "Ngarko imazhin", + "composer.upload-file": "Ngarko dokumentin", + "composer.zen_mode": "Modeli Zen ", + "composer.select_category": "Zgjidh nje kategori", + "composer.textarea.placeholder": "Futni përmbajtjen tuaj të postimit këtu, tërhiqni dhe lëshoni imazhet", + "composer.schedule-for": "Programoni temën për", + "composer.schedule-date": "Data", + "composer.schedule-time": "Koha", + "composer.cancel-scheduling": "Anulo planifikimin", + "composer.set-schedule-date": "Cakto datën", + "bootbox.ok": "Në rregull", + "bootbox.cancel": "Anullo", + "bootbox.confirm": "Konfirmo ", + "bootbox.submit": "Paraqes", + "bootbox.send": "Dërgo", + "cover.dragging_title": "Pozicionimi i fotos së kopertinës", + "cover.dragging_message": "Zhvendosni foton e kopertinës në pozicionin e dëshiruar dhe klikoni \"Ruaj\"", + "cover.saved": "Imazhi dhe pozicioni i fotos së kopertinës u ruajtën", + "thumbs.modal.title": "Menaxho fotografitë e temave", + "thumbs.modal.no-thumbs": "Nuk u gjeten informacione.", + "thumbs.modal.resize-note": "Shenim: Ky forum eshte konfiguruar per te ndryshuar permasat e gjeresise te materialit maksimalisht ne 1%1p", + "thumbs.modal.add": "Shto informacion", + "thumbs.modal.remove": "Largo informacionin", + "thumbs.modal.confirm-remove": "Jeni te sigurtë që doni ta fshini këtë informacion?" +} \ No newline at end of file diff --git a/public/language/sq-AL/notifications.json b/public/language/sq-AL/notifications.json new file mode 100644 index 0000000000..86c2433106 --- /dev/null +++ b/public/language/sq-AL/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Njoftimet", + "no_notifs": "Ju nuk keni njoftime te reja", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Shko mbrapa në %1", + "outgoing_link": "Link dalës", + "outgoing_link_message": "Tani po largoheni nga %1", + "continue_to": "Vazhdoni tek %1", + "return_to": "Kthehuni në %1", + "new_notification": "Ju keni një njoftim të ri", + "you_have_unread_notifications": "Ju keni njoftime të palexuara.", + "all": "Të gjitha", + "topics": "Temat", + "replies": "Përgjigjet", + "chat": "Bisedat", + "group-chat": "Bisedat në Grup", + "follows": "Ndjek", + "upvote": "Votat pro", + "new-flags": "Raportim i ri", + "my-flags": "Raportimet u kaluan tek unë", + "bans": "Të bllokuar", + "new_message_from": "Mesazh i ri nga%1", + "upvoted_your_post_in": "%1ka votuar në postin tënd në %2.", + "upvoted_your_post_in_dual": "%1 dhe % 2 kanë votuar për postimin tuaj në %3.", + "upvoted_your_post_in_multiple": "%1 dhe %2 të tjerë kanë votuar për postimin tuaj në %3.", + "moved_your_post": "%1 e ka zhvendosur postimin tuaj në %2", + "moved_your_topic": "%1 1 ka lëvizur %2", + "user_flagged_post_in": "%1 ka raportuar një postim në %2", + "user_flagged_post_in_dual": "%1 dhe %2 raportuam një postim në %3", + "user_flagged_post_in_multiple": "%1 dhe %2 dhe disa të tjerë raportuan një postim në %3", + "user_flagged_user": "%1 ka raportuar një profil përdoruesi (%2)", + "user_flagged_user_dual": "%1 dhe %2 kane raportuar një profil përdoruesi (%3)", + "user_flagged_user_multiple": "%1 dhe %2 dhe disa të tjerë kanë raportuar një profil përdoruesi (%3)", + "user_posted_to": "%1 ka postuar një përgjigje në: %2", + "user_posted_to_dual": "%1 dhe %2 kanë postuar përgjigje në: %3", + "user_posted_to_multiple": "%1 dhe %2 të tjerë kanë postuar përgjigje në: %3", + "user_posted_topic": "%1 ka postuar një temë të re: %2", + "user_edited_post": "%1 ka redaktuar një postim në %2", + "user_started_following_you": "%1 filloi t'ju ndjekë.", + "user_started_following_you_dual": "% 1 dhe %2 filluan t'ju ndjekin.", + "user_started_following_you_multiple": "%1 dhe %2 të tjerë filluan t'ju ndjekin.", + "new_register": "%1 dërgoi një kërkesë për regjistrim.", + "new_register_multiple": "Ka %1 kërkesa regjistrimi në pritje për shqyrtimit.", + "flag_assigned_to_you": " Raportimi %1 ju është ngarkuar juve", + "post_awaiting_review": "Postimi në pritje të rishikimit", + "profile-exported": "%1 profile u eksportuan, kliko për ta shkarkaur", + "posts-exported": "%1 postime u eksportuan, kliko per ta shkarkaur", + "uploads-exported": "%1 ngarkime u eksportuan, kliko per ta shkarkaur", + "users-csv-exported": "Csv e përdoruesve u eksportua, klikoni për ta shkarkuar", + "post-queue-accepted": "Postimi juaj në radhë është pranuar. Klikoni këtu për të parë postimin tuaj.", + "post-queue-rejected": "Postimi juaj në pritje nuk është pranuar.", + "post-queue-notify": "Ka nje njoftim te ri per postimin ne pritje:
''%1''", + "email-confirmed": "Email-i u konfirmua", + "email-confirmed-message": "Faleminderit për vërtetimin e emailit tuaj. Llogaria juaj tani është plotësisht e aktivizuar.", + "email-confirm-error-message": "Pati një problem me vërtetimin e adresës tuaj të emailit. Ndoshta kodi ishte i pavlefshëm ose ka skaduar.", + "email-confirm-sent": "Email i konfirmimit u dërgua.", + "none": "Asnjë", + "notification_only": "Vetëm njoftime", + "email_only": "Vetëm email", + "notification_and_email": "Njoftim & Email", + "notificationType_upvote": "Kur dikush voton pro për postimin tuaj", + "notificationType_new-topic": "Kur dikush që ndiqni poston një temë", + "notificationType_new-reply": "Kur një përgjigje e re postohet në një temë që po shikoni", + "notificationType_post-edit": "Kur një postim redaktohet në një temë që po shikoni", + "notificationType_follow": "Kur dikush fillon të të ndjekë", + "notificationType_new-chat": "Kur merrni një mesazh", + "notificationType_new-group-chat": "Kur merrni një mesazh bisede në grup", + "notificationType_group-invite": "Kur merrni një ftesë në grup", + "notificationType_group-leave": "Kur një përdorues largohet nga grupi juaj", + "notificationType_group-request-membership": "Kur dikush kërkon t'i bashkohet një grupi që ju zotëroni", + "notificationType_new-register": "Kur dikush shtohet në radhën e regjistrimit", + "notificationType_post-queue": "Kur një postim i ri është në radhë", + "notificationType_new-post-flag": "Kur një postim është raportuar", + "notificationType_new-user-flag": "Kur një përdorues është raportuar" +} \ No newline at end of file diff --git a/public/language/sq-AL/pages.json b/public/language/sq-AL/pages.json new file mode 100644 index 0000000000..2b78bb2024 --- /dev/null +++ b/public/language/sq-AL/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Faqja kryesore", + "unread": "Tema të palexuara", + "popular-day": "Temat më të ndjekura sot", + "popular-week": "Temat e njohura këtë javë", + "popular-month": "Temat e njohura këtë muaj", + "popular-alltime": "Tema të njohura gjatë gjithë kohës", + "recent": "Temat e fundit", + "top-day": "Temat kryesore të votuara sot", + "top-week": "Temat kryesore të votuara këtë javë", + "top-month": "Temat kryesore të votuara këtë muaj", + "top-alltime": "Temat kryesore më të votuara", + "moderator-tools": "Mjetet e Moderatorit", + "flagged-content": "Përmbajtja e shënuar", + "ip-blacklist": "Lista e zezë IP", + "post-queue": "Radha e postimit", + "users/online": "Përdoruesit në internet", + "users/latest": "Përdoruesit e fundit", + "users/sort-posts": "Përdoruesit me më shumë postime", + "users/sort-reputation": "Përdoruesit me reputacionin më të madh", + "users/banned": "Përdoruesit e Ndaluar", + "users/most-flags": "Shumica e përdoruesve të raportuar", + "users/search": "Kërkimi i përdoruesit", + "notifications": "Njoftimet", + "tags": "Etiketimet", + "tag": "Temat e etiketuara nën "%1"", + "register": "Regjistroni një llogari", + "registration-complete": "Regjistrimi ka përfunduar", + "login": "Hyni në llogarinë tuaj", + "reset": "Rivendosni fjalëkalimin e llogarisë tuaj", + "categories": "Kategoritë", + "groups": "Grupet", + "group": "%1 grup", + "chats": "Bisedat", + "chat": "Biseda me %1", + "flags": "Raportime", + "flag-details": "Shënoni %1 Detajet", + "account/edit": "Redaktimi \"%1\"", + "account/edit/password": "Redaktimi i fjalëkalimit të \"%1\"", + "account/edit/username": "Redaktimi i emrit të përdoruesit të \"%1\"", + "account/edit/email": "Email-i po modifikohet i \"%1\"", + "account/info": "Informacioni i llogarisë", + "account/following": "Njerëzit % 1 ndjekin", + "account/followers": "Njerëzit që ndjekin %1", + "account/posts": "Postimet e bëra nga %1", + "account/latest-posts": "Postimet e fundit të bëra nga %1", + "account/topics": "Tema krijuar nga %1", + "account/groups": "Grupet e %1", + "account/watched_categories": "Kategoritë e para nga %1", + "account/bookmarks": "Postimet e shënuara nga %1", + "account/settings": "Cilësimet e përdoruesit", + "account/watched": "Tema e para nga %1", + "account/ignored": "Temat e shmangura nga %1", + "account/upvoted": "Postimi u votua lartë nga %1", + "account/downvoted": "Postimi u votua poshtë nga %1", + "account/best": "Postimet më të mira krijuar nga %1", + "account/controversial": "Postet e dikutuara krijuar nga %1", + "account/blocks": "Përdoruesit e bllokuara për %1", + "account/uploads": "Ngarkimet e %1", + "account/sessions": "Seancat e hyrjes", + "confirm": "Email-i u konfirmua", + "maintenance.text": "%1 po i nënshtrohet mirëmbajtjes. Të lutem kthehu një herë tjetër.", + "maintenance.messageIntro": "Për më tepër, administratori ka lënë këtë mesazh:", + "throttled.text": "%1 është aktualisht i padisponueshëm për shkak të ngarkesës së tepërt. Të lutem kthehu një herë tjetër." +} \ No newline at end of file diff --git a/public/language/sq-AL/post-queue.json b/public/language/sq-AL/post-queue.json new file mode 100644 index 0000000000..ce96f53ea1 --- /dev/null +++ b/public/language/sq-AL/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Radha e postimit", + "description": "Nuk ka asnjë postim në radhën e postimeve. 1
Për të aktivizuar këtë veçori, shkoni te Cilësimet → Post → radha e postimeve dhe aktivizoni Radha e Postimeve", + "user": "Përdorues", + "category": "Kategoria", + "title": "Titulli", + "content": "Përmbajtja", + "posted": "U postua", + "reply-to": "Përgjigju \"%1\"", + "content-editable": "Klikoni mbi përmbajtjen për ta modifikuar", + "category-editable": "Klikoni në kategori për ta modifikuar", + "title-editable": "Klikoni mbi titullin për të modifikuar", + "reply": "Komento", + "topic": "Tema", + "accept": "Prano", + "reject": "Refuzo", + "remove": "Hiq", + "notify": "Njofto", + "notify-user": "Njoftoni përdoruesin", + "confirm-reject": "Jeni i sigurt që dëshironi ta anulloni këtë postim?", + "bulk-actions": "Veprime njëkohësisht", + "accept-all": "Prano të gjitha", + "accept-selected": "Accept Selected", + "reject-all": "Refuzo të gjitha", + "reject-all-confirm": "Dëshiron të refuzosh të gjitha postimet?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Dëshiron të refuzosh %1 postimet e selektuara?", + "bulk-accept-success": "%1 postime u pranuan", + "bulk-reject-success": "%1 postime u refuzuan" +} \ No newline at end of file diff --git a/public/language/sq-AL/recent.json b/public/language/sq-AL/recent.json new file mode 100644 index 0000000000..dadb67e0fa --- /dev/null +++ b/public/language/sq-AL/recent.json @@ -0,0 +1,19 @@ +{ + "title": "E fundit", + "day": "Ditë", + "week": "Javë", + "month": "Muaj", + "year": "Vit", + "alltime": "I gjithë kohërave", + "no_recent_topics": "Nuk ka tema të fundit.", + "no_popular_topics": "Nuk ka tema të njohura.", + "there-is-a-new-topic": "Ka një temë të re.", + "there-is-a-new-topic-and-a-new-post": "Ka një temë të re dhe një postim të ri.", + "there-is-a-new-topic-and-new-posts": "Ka një temë të re dhe %1 postime të reja.", + "there-are-new-topics": "Ka %1 tema të reja.", + "there-are-new-topics-and-a-new-post": "Ka %1 tema të reja dhe një postim të ri.", + "there-are-new-topics-and-new-posts": "Ka %1 tema të reja dhe %2 postime të reja.", + "there-is-a-new-post": "Ka një postim të ri", + "there-are-new-posts": "Ka %1 postime të reja.", + "click-here-to-reload": "Kliko këtu për të ringarkuar." +} \ No newline at end of file diff --git a/public/language/sq-AL/register.json b/public/language/sq-AL/register.json new file mode 100644 index 0000000000..3d57b1d510 --- /dev/null +++ b/public/language/sq-AL/register.json @@ -0,0 +1,32 @@ +{ + "register": "Regjistrohu", + "cancel_registration": "Anulo regjistrimin", + "help.email": "Si parazgjedhje, emaili juaj do të mbahet i fshihte nga publiku.", + "help.username_restrictions": "Një emër përdoruesi unik midis %1 dhe %2 karaktereve. Të tjerët mund t'ju përmendin me @username.", + "help.minimum_password_length": "Gjatësia e fjalëkalimit tuaj duhet të jetë të paktën %1 karaktere.", + "email_address": "Adresa e emailit", + "email_address_placeholder": "Fut adresën e emailit", + "username": "Emri i përdoruesit", + "username_placeholder": "Futni emrin e përdoruesit", + "password": "Fjalëkalimi", + "password_placeholder": "Shkruani fjalëkalimin", + "confirm_password": "Konfirmo fjalëkalimin", + "confirm_password_placeholder": "Konfirmo fjalëkalimin", + "register_now_button": "Regjistrohu Tani", + "alternative_registration": "Regjistrim alternativ", + "terms_of_use": "Kushtet e Përdorimit", + "agree_to_terms_of_use": "Pranoj Kushtet e Përdorimit", + "terms_of_use_error": "Ju duhet të pranoni Kushtet e Përdorimit", + "registration-added-to-queue": "Regjistrimi juaj është shtuar në radhën e miratimit. Ju do të merrni një email kur të pranohet nga një administrator.", + "registration-queue-average-time": "Koha jonë mesatare për miratimin e anëtarësimeve është %1 orë %2 minuta.", + "registration-queue-auto-approve-time": "Anëtarësimi juaj në këtë forum do të aktivizohet plotësisht deri në %1 orë.", + "interstitial.intro": "Ne do të donim disa informacione shtesë për të përditësuar llogarinë tuaj…", + "interstitial.intro-new": "Ne do të donim disa informacione shtesë përpara se të krijojmë llogarinë tuaj…", + "interstitial.errors-found": "Ju lutemi rishikoni informacionin e futur:", + "gdpr_agree_data": "Unë pranoj mbledhjen dhe përpunimin e informacionit tim personal në këtë faqe interneti.", + "gdpr_agree_email": "Unë pranoj të marr email-e përmbledhëse dhe njoftimesh nga kjo faqe interneti.", + "gdpr_consent_denied": "Ju duhet të jepni pëlqimin për këtë faqe për të mbledhur/përpunuar informacionin tuaj dhe për t'ju dërguar email.", + "invite.error-admin-only": "Regjistrimi i drejtpërdrejtë i përdoruesit është çaktivizuar. Ju lutemi kontaktoni një administrator për më shumë detaje.", + "invite.error-invite-only": "Regjistrimi i drejtpërdrejtë i përdoruesit është çaktivizuar. Ju duhet të jeni të ftuar nga një përdorues ekzistues për të hyrë në këtë forum.", + "invite.error-invalid-data": "Të dhënat e marra të regjistrimit nuk korrespondojnë me të dhënat tona. Ju lutemi kontaktoni një administrator për më shumë detaje" +} \ No newline at end of file diff --git a/public/language/sq-AL/reset_password.json b/public/language/sq-AL/reset_password.json new file mode 100644 index 0000000000..5186fea3a2 --- /dev/null +++ b/public/language/sq-AL/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Ndrysho fjalëkalimin", + "update_password": "Përditëso fjalëkalimin", + "password_changed.title": "Fjalëkalimi u ndryshua", + "password_changed.message": "

Fjalëkalimi u rivendos me sukses, ju lutemi identifikohuni përsëri.", + "wrong_reset_code.title": "Kodi i rivendosjes i gabuar", + "wrong_reset_code.message": "Kodi i rivendosjes së marrë ishte i pasaktë. Provo sërish ose kërko një kod të ri rivendosjeje.", + "new_password": "Fjalëkalim i ri", + "repeat_password": "Konfirmo fjalëkalimin", + "changing_password": "Ndryshimi i fjalëkalimit", + "enter_email": "Ju lutemi shkruani adresën tuaj të emailit dhe ne do t'ju dërgojmë një email me udhëzime se si të ndryshoni llogarinë tuaj.", + "enter_email_address": "Fut adresën e emailit", + "password_reset_sent": "Nëse adresa e specifikuar korrespondon me një llogari ekzistuese përdoruesi, është dërguar një email për rivendosjen e fjalëkalimit. Ju lutemi vini re se vetëm një email do të dërgohet në minutë.", + "invalid_email": "Email i pavlefshëm / Email nuk ekziston!", + "password_too_short": "Fjalëkalimi i futur është shumë i shkurtër, ju lutemi zgjidhni një fjalëkalim tjetër.", + "passwords_do_not_match": "Dy fjalëkalimet që keni futur nuk përputhen.", + "password_expired": "Fjalëkalimi juaj ka skaduar, ju lutemi zgjidhni një fjalëkalim të ri" +} \ No newline at end of file diff --git a/public/language/sq-AL/search.json b/public/language/sq-AL/search.json new file mode 100644 index 0000000000..170a8302fc --- /dev/null +++ b/public/language/sq-AL/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 rezultat(e) që përputhen me \"%2\", (%3 sekonda)", + "no-matches": "Nuk u gjet asnjë përputhje", + "advanced-search": "Kërkim i avancuar", + "in": "Në", + "titles": "Titujt", + "titles-posts": "Titujt dhe postimet", + "match-words": "Përputhni fjalët", + "all": "Të gjitha", + "any": "Çdo", + "posted-by": "Postuar nga", + "in-categories": "Në kategori", + "search-child-categories": "Kërko kategoritë e fëmijëve", + "has-tags": "Ka etiketa", + "reply-count": "Numri i komenteve", + "at-least": "Të paktën", + "at-most": "Së shumti", + "relevance": "Rëndësia", + "post-time": "Koha e postimit", + "votes": "Votat", + "newer-than": "Më e re se", + "older-than": "Më të vjetër se", + "any-date": "Çdo datë", + "yesterday": "Dje", + "one-week": "Një javë", + "two-weeks": "Dy javë", + "one-month": "Një muaj", + "three-months": "Tre muaj", + "six-months": "Gjashtë muaj", + "one-year": "Një vit", + "sort-by": "Ndaj sipas", + "last-reply-time": "Komenti i fundit", + "topic-title": "Titulli i temës", + "topic-votes": "Votat e temës", + "number-of-replies": "Komente gjithesej", + "number-of-views": "Shikueshmeria", + "topic-start-date": "Data e fillimit të temës", + "username": "Emri i përdoruesit", + "category": "Kategoria", + "descending": "Në rend zbritës", + "ascending": "Në rend rritës", + "save-preferences": "Ruaj preferencat", + "clear-preferences": "Pastro preferencat", + "search-preferences-saved": "Preferencat e kërkimit u ruajtën", + "search-preferences-cleared": "Preferencat e kërkimit u pastruan", + "show-results-as": "Shfaq rezultatet si", + "see-more-results": "Shiko më shumë rezultate (%1)", + "search-in-category": "Kërko në \"%1\"" +} \ No newline at end of file diff --git a/public/language/sq-AL/success.json b/public/language/sq-AL/success.json new file mode 100644 index 0000000000..0b4da22c73 --- /dev/null +++ b/public/language/sq-AL/success.json @@ -0,0 +1,7 @@ +{ + "success": "Sukses", + "topic-post": "Ju keni postuar me sukses.", + "post-queued": "Postimi juaj është në radhë për miratim. Ju do të merrni një njoftim kur të pranohet ose refuzohet.", + "authentication-successful": "Vërtetimi u krye me sukses", + "settings-saved": "Cilësimet u ruajtën!" +} \ No newline at end of file diff --git a/public/language/sq-AL/tags.json b/public/language/sq-AL/tags.json new file mode 100644 index 0000000000..0fb8f6ec66 --- /dev/null +++ b/public/language/sq-AL/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Nuk ka tema me këtë tag.", + "tags": "Tags", + "enter_tags_here": "Futni këtu tags, ndërmjet %1 dhe %2 karaktere secila.", + "enter_tags_here_short": "Vendos tags...", + "no_tags": "Nuk ka ende tags", + "select_tags": "Zgjidhni tags" +} \ No newline at end of file diff --git a/public/language/sq-AL/top.json b/public/language/sq-AL/top.json new file mode 100644 index 0000000000..07efe74dd1 --- /dev/null +++ b/public/language/sq-AL/top.json @@ -0,0 +1,4 @@ +{ + "title": "Kryesore", + "no_top_topics": "Nuk ka tema kryesore" +} \ No newline at end of file diff --git a/public/language/sq-AL/topic.json b/public/language/sq-AL/topic.json new file mode 100644 index 0000000000..3a1ea0de41 --- /dev/null +++ b/public/language/sq-AL/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Tema", + "title": "Titulli", + "no_topics_found": "Nuk u gjet asnje temë! ", + "no_posts_found": "Nuk u gjet asnjë postim!", + "post_is_deleted": "Ky postim është fshirë!", + "topic_is_deleted": "Kjo temë është fshirë!", + "profile": "Profili", + "posted_by": "Postuar nga %1", + "posted_by_guest": "Postuar nga vizitori", + "chat": "Bisedo", + "notify_me": "Njoftohuni për komentet e reja në këtë temë", + "quote": "Cito", + "reply": "Komento", + "replies_to_this_post": "%1 Komento", + "one_reply_to_this_post": "1 Koment", + "last_reply_time": "Komenti i fundit", + "reply-as-topic": "Komentoje si temë", + "guest-login-reply": "Identifikohu për të komentuar", + "login-to-view": "🔒 Identifikohu për ta parë", + "edit": "Edito", + "delete": "Fshij ", + "delete-event": "Fshij eventin", + "delete-event-confirm": "Je i sigurt që dëshiron ta fshish këtë event?", + "purge": "Pastro", + "restore": "Rikthe", + "move": "Zhvendose", + "change-owner": "Ndrysho pronarin", + "fork": "Ndrysho", + "link": "Link", + "share": "Ndaj", + "tools": "Mjete", + "locked": "I bllokuar", + "pinned": "E fiksuar", + "pinned-with-expiry": "Fiksuar deri më %1", + "scheduled": "E planifikuar", + "moved": "E lëvizur", + "moved-from": "E lëvizur nga %1", + "copy-ip": "Kopjoni IP-në", + "ban-ip": "Pezulloni IP-në", + "view-history": "Ndrysho historinë", + "locked-by": "E mbyllur nga", + "unlocked-by": "E shkyçur nga", + "pinned-by": "Fiksuar nga", + "unpinned-by": "E ç'fiksuar nga", + "deleted-by": "Fshirë nga", + "restored-by": "Rivendosur nga ", + "moved-from-by": "Zhvendosur nga %1 nga", + "queued-by": "Postimi në radhë për miratim →", + "backlink": "Referuar nga", + "forked-by": "Ndryshuar nga", + "bookmark_instructions": "Klikoni këtu për tu kthyer në postimin e fundit të lexuar në këtë temë.", + "flag-post": "Raporto këtë postim", + "flag-user": "Raporto këtë përdorues", + "already-flagged": "Raportuar më parë", + "view-flag-report": "Shiko analizën e raportimeve", + "resolve-flag": "Zgjidh raportimin", + "merged_message": "Kjo temë është bashkuar në %2", + "deleted_message": "Kjo temë është fshirë. Vetëm përdoruesit me privilegje mund ta shohin atë.", + "following_topic.message": "Tani do të merrni njoftime kur dikush poston në këtë temë.", + "not_following_topic.message": "Ju do ta shihni këtë temë në listën e temave të palexuara, por nuk do të merrni njoftime kur dikush poston në të.", + "ignoring_topic.message": "Nuk do ta shihni më këtë temë në listën e temave të palexuara. Do të njoftoheni kur të përmendeni ose kur postimi juaj të votohet.", + "login_to_subscribe": "Ju lutemi regjistrohuni për të marrë njoftime në këtë temë.", + "markAsUnreadForAll.success": "Tema u shënua si e palexuar për të gjithë.", + "mark_unread": "Shëno si të pa lexuar", + "mark_unread.success": "Tema u shënua si e palexuar ", + "watch": "Ndiqe", + "unwatch": "Mos e ndiq", + "watch.title": "Njoftohuni për njoftimet e reja në këtë temë", + "unwatch.title": "Ndaloni së ndjekuri këtë temë", + "share_this_post": "Shpërnda këtë postim", + "watching": "Duke e ndjekur", + "not-watching": "Nuk jam duke ndjekur", + "ignoring": "Duke injoruar", + "watching.description": "Më njoftoni për komentet e reja.
Shfaq temën si të palexuar.", + "not-watching.description": "Mos më njofto për komentet e reja.
Shfaq temën e palexuar nëse kategoria nuk shpërfillet.", + "ignoring.description": "Mos më njofto për komentet e reja.
Mos e shfaq temën e palexuar.", + "thread_tools.title": "Mjetet e temave", + "thread_tools.markAsUnreadForAll": "Shënoni si të palexuar për të gjithë", + "thread_tools.pin": "Fikso temën", + "thread_tools.unpin": "Ç'fikso temën", + "thread_tools.lock": "Blloko temën", + "thread_tools.unlock": "Zhblloko temën", + "thread_tools.move": "Zhvendos temën", + "thread_tools.move-posts": "Zhvendos postimin", + "thread_tools.move_all": "Zhvendos të gjitha", + "thread_tools.change_owner": "Ndrysho pronarin", + "thread_tools.select_category": "Zgjidh një kategori", + "thread_tools.fork": "Ndrysho temën", + "thread_tools.delete": "Fshij temën", + "thread_tools.delete-posts": "Fshij postimin", + "thread_tools.delete_confirm": "Jeni i sigurt që dëshironi ta fshini këtë temë?", + "thread_tools.restore": "Rivendos temën", + "thread_tools.restore_confirm": "Jeni i sigurt që dëshironi ta rivendosni këtë temë?", + "thread_tools.purge": "Pastroni temën", + "thread_tools.purge_confirm": "Jeni i sigurt që dëshironi ta pastroni këtë temë?", + "thread_tools.merge_topics": "Bashko temat", + "thread_tools.merge": "Bashko", + "topic_move_success": "Kjo temë do të zhvendoset në \"%1\" së shpejti. Kliko këtu për ta zhbërë.", + "topic_move_multiple_success": "Këto tema do të zhvendosen në \"%1\" së shpejti. Kliko këtu për ta zhbërë.", + "topic_move_all_success": "Të gjitha temat do të zhvendosen në \"%1\" së shpejti. Kliko këtu për ta zhbërë.", + "topic_move_undone": "Zhvendosja e temës u zhbë", + "topic_move_posts_success": "Postimet do të zhvendosen së shpejti. Kliko këtu për ta zhbërë.", + "topic_move_posts_undone": "Zhvendosja e postimit u zhbë", + "post_delete_confirm": "Jeni i sigurt që dëshironi ta fshini këtë postim?", + "post_restore_confirm": "Jeni i sigurt që dëshironi ta riktheni këtë postim?", + "post_purge_confirm": "Jeni i sigurt që dëshironi ta pastroni këtë postim?", + "pin-modal-expiry": "Data e skadencës", + "pin-modal-help": "Mund të caktoni opsionalisht një datë skadimi për temat() e ngjitura këtu. Përndryshe, mund ta lini këtë fushë bosh që tema të qëndrojë e renditur e para derisa të hiqet manualisht.", + "load_categories": "Duke ngarkuar kategoritë", + "confirm_move": "Lëvizni", + "confirm_fork": "Ndrysho", + "bookmark": "Ruaj", + "bookmarks": "Të ruajtura", + "bookmarks.has_no_bookmarks": "Nuk keni ruajtur ende asnjë postim.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Duke ngarkuar më shumë postime", + "move_topic": "Zhvendos Temën", + "move_topics": "Zhvendos Temat", + "move_post": "Zhvendos Postimin", + "post_moved": "Postimi u zhvendos!", + "fork_topic": "Ndrysho temën", + "enter-new-topic-title": "Vendos titullin e temës së re", + "fork_topic_instruction": "Zgjidhni postimet që dëshironi të ndryshoni", + "fork_no_pids": "Asnjë postim i zgjedhur!", + "no-posts-selected": "Asnjë postim i zgjedhur!", + "x-posts-selected": "%1 postim(e) i zgjedhur", + "x-posts-will-be-moved-to-y": "%1 postim(s) do të zhvendoset në \"%2\"", + "fork_pid_count": "%1 postim(e) i zgjedhur", + "fork_success": "Kjo temë u ndryshua me sukses! Kliko këtu që të shkoni tek tema e ndryshuar.", + "delete_posts_instruction": "Klikoni postimet që dëshironi të fshini/pastroni", + "merge_topics_instruction": "Klikoni temat që dëshironi të bashkoni ose kërkoni", + "merge-topic-list-title": "Lista e temave që do të bashkohen", + "merge-options": "Bashko opsionet", + "merge-select-main-topic": "Zgjidhni temën kryesore", + "merge-new-title-for-topic": "Titulli i ri për temën", + "topic-id": "ID e temës", + "move_posts_instruction": "Klikoni postimet që dëshironi të zhvendosni, më pas vendosni një ID teme ose shkoni te tema e synuar", + "change_owner_instruction": "Klikoni postimet që dëshironi t'i caktoni një përdoruesi tjetër", + "composer.title_placeholder": "Shkruani titullin e temës suaj këtu...", + "composer.handle_placeholder": "Shkruani emrin tuaj këtu", + "composer.discard": "Anullo", + "composer.submit": "Posto", + "composer.additional-options": "Opsione shtesë", + "composer.schedule": "Skedulo", + "composer.replying_to": "Duke komentuar \"%1\"", + "composer.new_topic": "Temë e re", + "composer.editing": "Duke edituar", + "composer.uploading": "duke u ngarkuar...", + "composer.thumb_url_label": "Ngjit një URL të fotos së coverit të temës", + "composer.thumb_title": "Shtoni një foto coveri në këtë temë", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Ose ngarko një skedar", + "composer.thumb_remove": "Pastro fushat", + "composer.drag_and_drop_images": "Zvarrit dhe lësho imazhet këtu", + "more_users_and_guests": "%1 përdorues() të tjerë dhe %2 të ftuar()", + "more_users": "%1 përdorues() të tjerë", + "more_guests": "%1 të ftuar më shumë ()", + "users_and_others": "%1 dhe %2 të tjerë", + "sort_by": "Rendit sipas", + "oldest_to_newest": "Nga më e vjetra tek më e reja", + "newest_to_oldest": "Nga më e reja tek më e vjetra", + "most_votes": "Më të votuarat", + "most_posts": "Të gjitha postimet", + "most_views": "Më të shikuarat", + "stale.title": "Krijo një temë të re më mirë?", + "stale.warning": "Tema që po i përgjigjesh është shumë e vjetër. Dëshironi të krijoni një temë të re në vend të saj dhe t'i referoheni në përgjigjen tuaj?", + "stale.create": "Krijo një temë të re", + "stale.reply_anyway": "Komentoi kësaj teme gjithësesi", + "link_back": "Re: [%1](%2)", + "diffs.title": "Historia e redaktimit të postimit", + "diffs.description": "Ky postim ka %1 rishikime. Klikoni një nga rishikimet më poshtë për të parë përmbajtjen e postimit në një moment të caktuar.", + "diffs.no-revisions-description": "Ky postim ka %1 rishikime.", + "diffs.current-revision": "Rishikimi aktual", + "diffs.original-revision": "Rishikim origjinal", + "diffs.restore": "Rivendosni këtë rishikim", + "diffs.restore-description": "Një rishikim i ri do t'i shtohet historikut të redaktimit të këtij postimi pas rivendosjes.", + "diffs.post-restored": "Postimi u rivendos me sukses në rishikimin e mëparshëm", + "diffs.delete": "Fshije këtë rishikim", + "diffs.deleted": "Rishikimi u fshi", + "timeago_later": "%1 më vonë", + "timeago_earlier": "%1 më parë", + "first-post": "Postimi i parë", + "last-post": "Postimi i fundit", + "go-to-my-next-post": "Shkoni te postimi im i radhës", + "no-more-next-post": "Nuk keni postime të tjera në këtë temë", + "post-quick-reply": "Postoni një koment të shpejtë" +} \ No newline at end of file diff --git a/public/language/sq-AL/unread.json b/public/language/sq-AL/unread.json new file mode 100644 index 0000000000..3c74982864 --- /dev/null +++ b/public/language/sq-AL/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Të palexuara", + "no_unread_topics": "Nuk ka tema të palexuara.", + "load_more": "Ngarko më shumë", + "mark_as_read": "Shëno si të lexuar", + "selected": "I/e Zgjedhur", + "all": "Të gjitha", + "all_categories": "Të gjitha kategoritë", + "topics_marked_as_read.success": "Temat e shënuara si të lexuara!", + "all-topics": "Të gjitha temat", + "new-topics": "Tema të reja", + "watched-topics": "Temat e shikuara", + "unreplied-topics": "Tema pa përgjigje", + "multiple-categories-selected": "Disa të zgjedhura njëkohësisht" +} \ No newline at end of file diff --git a/public/language/sq-AL/uploads.json b/public/language/sq-AL/uploads.json new file mode 100644 index 0000000000..46d5c21fc1 --- /dev/null +++ b/public/language/sq-AL/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Po ngarkohet materiali...", + "select-file-to-upload": "Zgjidhni një material për të ngarkuar!", + "upload-success": "Materiali u ngarkua me sukses!", + "maximum-file-size": "Maksimumi %1 kb", + "no-uploads-found": "Nuk u gjet asnjë material i ngarkuar", + "public-uploads-info": "Ngarkimet janë publike, të gjithë vizitorët mund t'i shohin ato.", + "private-uploads-info": "Ngarkimet janë private, vetëm përdoruesit e regjistruar mund t'i shohin ato." +} \ No newline at end of file diff --git a/public/language/sq-AL/user.json b/public/language/sq-AL/user.json new file mode 100644 index 0000000000..2194839a9f --- /dev/null +++ b/public/language/sq-AL/user.json @@ -0,0 +1,199 @@ +{ + "banned": "I ndaluar", + "muted": "Muted", + "offline": "Jashtë linje", + "deleted": "Fshirë", + "username": "Emri i përdoruesit", + "joindate": "Data e anëtarësimit", + "postcount": "Numri i postimeve", + "email": "Email", + "confirm_email": "Konfirmo Email-in", + "account_info": "Informacioni rreth llogarisë", + "admin_actions_label": "Veprimet Administrative", + "ban_account": "Ndalimi i llogarisë", + "ban_account_confirm": "Dëshiron vërtet ta ndalosh këtë përdorues?", + "unban_account": "Zhblloko llogarinë", + "mute_account": "Bëje llogarinë pa zë", + "unmute_account": "Aktivizo llogarinë", + "delete_account": "Fshij llogarinë", + "delete_account_as_admin": "Fshij llogarinë", + "delete_content": "Fshij përmbajtjen e llogarisë", + "delete_all": "Fshij llogarinë dhe përmbajtjen", + "delete_account_confirm": "Jeni i sigurt që dëshironi të beni anonim postimet tuaja dhe të fshini llogarinë tuaj?
Ky veprim është i pakthyeshëm dhe nuk do të mund të rikuperoni asnjë nga të dhënat tuaja

Futni fjalëkalimin tuaj për të konfirmuar që dëshironi të fshini këtë llogari.", + "delete_this_account_confirm": "Jeni i sigurt që dëshironi ta fshini këtë llogari duke lënë pas përmbajtjen e saj?
Ky veprim është i pakthyeshëm, postimet do të jenë anonime dhe nuk do të jeni në gjendje të rivendosni lidhjet e postimeve me llogarinë e fshirë.

", + "delete_account_content_confirm": "Jeni i sigurt që dëshironi të fshini përmbajtjen e kësaj llogarie (postimet/temat/ngarkimet)?
Ky veprim është i pakthyeshëm dhe nuk do të mund të rikuperoni asnjë të dhënë

", + "delete_all_confirm": "Jeni i sigurt që dëshironi të fshini këtë llogari dhe të gjithë përmbajtjen e saj (postimet/temat/ngarkimet)?
Ky veprim është i pakthyeshëm dhe nuk do të mund të rikuperoni asnjë të dhënë

", + "account-deleted": "Llogaria u fshi", + "account-content-deleted": "Përmbajtja e llogarisë u fshi", + "fullname": "Emri i plotë", + "website": "Faqja e internetit", + "location": "Vendndodhja", + "age": "Mosha", + "joined": "U bashkua", + "lastonline": "Aktiviteti online i fundit", + "profile": "Profili", + "profile_views": "Shikime të profilit", + "reputation": "Reputacioni", + "bookmarks": "Faqe të ruajtura", + "watched_categories": "Kategoritë e kërkuara", + "change_all": "Ndrysho të gjitha", + "watched": "Shikuar", + "ignored": "Injoruar", + "default-category-watch-state": "Gjendja e kategorisë së parazgjedhur", + "followers": "Ndjekësit", + "following": "Duke ndjekur", + "blocks": "Blloqe", + "block_toggle": "Ndrysho bllokimin", + "block_user": "Blloko përdoruesin", + "unblock_user": "Zhblloko përdoruesin", + "aboutme": "Rreth meje", + "signature": "Firma", + "birthday": "Ditëlindja", + "chat": "Bisedë", + "chat_with": "Vazhdo bisedën me %1", + "new_chat_with": "Fillo bisedë te re me %1", + "flag-profile": "Profil i raportuar", + "follow": "Ndjek", + "unfollow": "Hiqe", + "more": "Më shumë", + "profile_update_success": "Profili është përditësuar me sukses!", + "change_picture": "Ndrysho foton", + "change_username": "Ndrysho emrin e përdoruesit", + "change_email": "Ndrysho e-mailin", + "email_same_as_password": "Ju lutemi shkruani fjalëkalimin tuaj aktual për të vazhduar – ju keni futur përsëri emailin tuaj të ri", + "edit": "Rregullo", + "edit-profile": "Rregullo Profilin", + "default_picture": "Ikona e parazgjedhur", + "uploaded_picture": "Fotografia e ngarkuar", + "upload_new_picture": "Ngarko foto të re", + "upload_new_picture_from_url": "Ngarko foto të re nga URL-ja", + "current_password": "Fjalëkalimi aktual", + "change_password": "Ndrysho fjalekalimin", + "change_password_error": "Fjalëkalim i pavlefshëm", + "change_password_error_wrong_current": "Fjalëkalimi juaj aktual nuk është i saktë!", + "change_password_error_match": "Fjalekalimet duhet te perputhen!", + "change_password_error_privileges": "Ju nuk keni të drejtë ta ndryshoni këtë fjalëkalim.", + "change_password_success": "Fjalëkalimi juaj është përditësuar!", + "confirm_password": "Konfirmo fjalëkalimin", + "password": "Fjalëkalimi", + "username_taken_workaround": "Emri i përdoruesit që kërkuat është i zënë. Ju sugjerojmë alternativën tjetër. Tani njiheni si %1", + "password_same_as_username": "Fjalëkalimi juaj është i njëjtë me emrin tuaj të përdoruesit, ju lutemi zgjidhni një fjalëkalim tjetër.", + "password_same_as_email": "Fjalëkalimi juaj është i njëjtë me emailin tuaj, ju lutemi zgjidhni një fjalëkalim tjetër.", + "weak_password": "Fjalëkalim i dobët.", + "upload_picture": "Ngarko foto", + "upload_a_picture": "Ngarko një foto", + "remove_uploaded_picture": "Hiq fotografine e ngarkuar", + "upload_cover_picture": "Ngarko fotografinë e kopertinës", + "remove_cover_picture_confirm": "Jeni i sigurt që dëshironi të hiqni foton e kopertines?", + "crop_picture": "Prisni përmasat e fotos", + "upload_cropped_picture": "Prit dhe Ngarko", + "avatar-background-colour": "Ngjyra e sfondit të Avatarit", + "settings": "Preferenca", + "show_email": "Shfaq emailin tim", + "show_fullname": "Shfaq emrin tim të plotë", + "restrict_chats": "Lejo vetëm mesazhet nga përdoruesit që ndjek.", + "digest_label": "Abonohu të informohesh", + "digest_description": "Abonohu ​​për përditësime me email në këtë forum (njoftime dhe tema të reja) në orare të caktuara", + "digest_off": "Fikur", + "digest_daily": "Përditë", + "digest_weekly": "Javore", + "digest_biweekly": "Dy-Javore", + "digest_monthly": "Mujore", + "has_no_follower": "Përdoruesi nuk ka asnjë ndjekës :(", + "follows_no_one": "Ky përdorues nuk ndjek askënd :(", + "has_no_posts": "Ky përdorues nuk ka postuar akoma asgjë. ", + "has_no_best_posts": "Ky përdorues nuk ka ende asnjë postim me votim.", + "has_no_topics": "Ky përdorues nuk ka postuar akoma asnjë temë.", + "has_no_watched_topics": "Ky përdorues nuk ka frekuentuar akoma asnjë temë.", + "has_no_ignored_topics": "Ky përdorues nuk ka injoruar asnjë temë ende.", + "has_no_upvoted_posts": "Ky përdorues nuk ka votuar pro akoma në asnjë postim.", + "has_no_downvoted_posts": "Ky përdorues nuk ka votuar kundër asnjë postimi.", + "has_no_controversial_posts": "Ky përdorues nuk ka ende asnjë postim me votim kundër.", + "has_no_blocks": "Nuk keni përdorues të bllokuar.", + "email_hidden": "Email i fshehur.", + "hidden": "I fshehur", + "paginate_description": "Kategorizoni temat tuaja në vënd që të lundroni pafund.", + "topics_per_page": "Tema për Faqe", + "posts_per_page": "Postime për Faqe", + "max_items_per_page": "Maksimumi %1", + "acp_language": "Gjuha e faqes së administratorit", + "notifications": "Njoftimet", + "upvote-notif-freq": "Frekuenca e njoftimit për votim pro.", + "upvote-notif-freq.all": "Të gjitha votat Pro", + "upvote-notif-freq.first": "I Pari Për Postim", + "upvote-notif-freq.everyTen": "Për cdo dhjetë vota pro", + "upvote-notif-freq.threshold": "Në 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Në 10, 100, 1000...", + "upvote-notif-freq.disabled": "I kufizuar", + "browsing": "Konfigurimet", + "open_links_in_new_tab": "Hapni lidhjet dalëse në skedën e re", + "enable_topic_searching": "Aktivizo kërkimin brenda temës", + "topic_search_help": "Nëse aktivizohet, kërkimi brenda temës do të anashkalojë sjelljen e paracaktuar të kërkimit të faqes së shfletuesit dhe do t'ju lejojë të kërkoni në të gjithë temën, në vend të asaj që shfaqet vetëm në ekran", + "update_url_with_post_index": "Përditësoni URL-në me indeksin e postimeve gjatë shfletimit të temave", + "scroll_to_my_post": "Pasi të keni postuar një përgjigje, shfaqni postimin e ri", + "follow_topics_you_reply_to": "Shiko temat të cilave u përgjigjesh", + "follow_topics_you_create": "Shikoni temat që keni krijuar", + "grouptitle": "Titull Grupi", + "group-order-help": "Zgjidhni një grup dhe përdorni shigjetat për të renditur titujt", + "no-group-title": "Pa titull grupi", + "select-skin": "Zgjidhni nje karakter", + "select-homepage": "Zgjidhni një Faqe kryesore", + "homepage": "Kryefaqe", + "homepage_description": "Zgjidhni një faqe për t'u përdorur si faqen kryesore të forumit ose 'Asnjë' për të përdorur faqen kryesore të paracaktuar.", + "custom_route": "Faqe Kryesore e Personalizuar", + "custom_route_help": "Futni një emër itinerari këtu, pa ndonjë prerje të mëparshme (p.sh. \"i fundit\" ose \"kategoria/2/diskutim i përgjithshëm\")", + "sso.title": "Shërbimet e hyrjes së vetme", + "sso.associated": "I lidhur me", + "sso.not-associated": "Klikoni këtu për t'u lidhur me", + "sso.dissociate": "Shkëputeni", + "sso.dissociate-confirm-title": "Konfirmo shkëputjen", + "sso.dissociate-confirm": "Jeni i sigurt që dëshironi të shkëputni llogarinë tuaj nga %1?", + "info.latest-flags": "Raportimet më të fundit", + "info.no-flags": "Nuk u gjet asnjë postim i shënuar", + "info.ban-history": "Historia e fundit e ndalimit", + "info.no-ban-history": "Ky përdorues nuk është ndaluar kurrë", + "info.banned-until": "Ndaluar deri në %1", + "info.banned-expiry": "Skadimi", + "info.banned-permanently": "Ndaluar përgjithmonë", + "info.banned-reason-label": "Arsye", + "info.banned-no-reason": "Asnjë arsye e dhënë.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "Nuk u dha asnjë arsye", + "info.username-history": "Historia e emrit të përdoruesit", + "info.email-history": "Historia e emailit", + "info.moderation-note": "Shënim i Moderimit", + "info.moderation-note.success": "Shënimi i moderimit u ruajt", + "info.moderation-note.add": "Shtoni shënim", + "sessions.description": "Kjo faqe ju lejon të shikoni çdo sesion aktiv në këtë forum dhe t'i anuloni ato nëse është e nevojshme. Ju mund ta revokoni seancën tuaj duke dalë nga llogaria juaj.", + "consent.title": "Të drejtat tuaja & Pëlqimi", + "consent.lead": "Kushtet e forumit tonë janë si më poshtë:", + "consent.intro": "Ne e përdorim këtë informacion në mënyrë rigoroze për të personalizuar përvojën tuaj në këtë komunitet, si dhe për të lidhur postimet që bëni me llogarinë tuaj të përdoruesit. Gjatë hapit të regjistrimit ju është kërkuar të jepni një emër përdoruesi dhe adresë emaili, gjithashtu mund të jepni opsionalisht informacion shtesë për të plotësuar profilin tuaj të përdoruesit në këtë faqe interneti.

Ne e ruajmë këtë informacion për sa kohë që llogaria juaj është aktive. Ju jeni në gjendje të tërhiqni pëlqimin në çdo kohë duke fshirë llogarinë tuaj. Në çdo kohë ju mund të kërkoni një kopje të kontributit tuaj në këtë faqe interneti, nëpërmjet të drejtave tuaja & Faqja e pëlqimit.

Nëse keni ndonjë pyetje ose shqetësim, ju inkurajojmë të kontaktoni ekipin administrativ të këtij forumi.", + "consent.email_intro": "Herë pas here, ne mund të dërgojmë email në adresën tuaj të email-it të regjistruar në mënyrë që të ofrojmë përditësime dhe/ose t'ju njoftojmë për aktivitetin e ri që ka të bëjë me ju. Mund të personalizoni frekuencën e përmbledhjes së komunitetit (duke përfshirë çaktivizimin e plotë të tij), si dhe të zgjidhni se cilat lloje njoftimesh të merrni me email, nëpërmjet faqes tuaj të cilësimeve të përdoruesit.", + "consent.digest_frequency": "Nëse nuk ndryshohet në mënyrë eksplicite në cilësimet e përdoruesit, ky komunitet jep përmbledhjet e emaileve çdo %1.", + "consent.digest_off": "Nëse nuk ndryshohet në mënyrë të qartë në cilësimet e përdoruesit, ky komunitet nuk dërgon përmbledhje me email", + "consent.received": "Ju keni dhënë pëlqimin që kjo faqe interneti të mbledhë dhe grumbullojë informacionin tuaj. Asnjë veprim shtesë nuk kërkohet.", + "consent.not_received": "Ju nuk keni dhënë pëlqimin për mbledhjen dhe grumbullimin e të dhënave. Në çdo kohë, administrata e kësaj faqe interneti mund të zgjedhë të fshijë llogarinë tuaj në mënyrë që të jetë në përputhje me Rregulloren e Përgjithshme të Mbrojtjes së të Dhënave.", + "consent.give": "Jep pëlqimin", + "consent.right_of_access": "Ju keni të drejtën e aksesit", + "consent.right_of_access_description": "Ju keni të drejtë të aksesoni çdo të dhënë të mbledhur nga kjo faqe interneti sipas kërkesës. Ju mund të merrni një kopje të këtyre të dhënave duke klikuar butonin e duhur më poshtë.", + "consent.right_to_rectification": "Ju keni të drejtën e korrigjimit", + "consent.right_to_rectification_description": "Ju keni të drejtë të ndryshoni ose përditësoni çdo të dhënë të pasaktë që na jepet. Profili juaj mund të përditësohet duke redaktuar profilin tuaj dhe përmbajtja e postimit mund të modifikohet gjithmonë. Nëse nuk është kështu, ju lutemi kontaktoni ekipin administrativ të kësaj faqeje.", + "consent.right_to_erasure": "Ju keni të drejtën e fshirjes", + "consent.right_to_erasure_description": "Në çdo kohë, ju mund të revokoni pëlqimin tuaj për mbledhjen dhe/ose përpunimin e të dhënave duke fshirë llogarinë tuaj. Profili juaj individual mund të fshihet, megjithëse përmbajtja juaj e postuar do të mbetet. Nëse dëshironi të fshini llogarinë tuaj dhe përmbajtjen tuaj, ju lutemi kontaktoni ekipin administrativ për këtë faqe interneti.", + "consent.right_to_data_portability": "Ju keni të drejtën e transportueshmërisë së të dhënave", + "consent.right_to_data_portability_description": "Ju mund të kërkoni nga ne një eksportim të lexueshëm nga makineritë e çdo të dhëne të mbledhur për ju dhe llogarinë tuaj. Ju mund ta bëni këtë duke klikuar butonin më poshtë.", + "consent.export_profile": "Eksporto profilin (.json)", + "consent.export-profile-success": "Duke eksportuar profilin, do të merrni një njoftim kur të përfundojë.", + "consent.export_uploads": "Eksporto përmbajtjen e ngarkuar (.zip)", + "consent.export-uploads-success": "Duke eksportuar ngarkimet, do të merrni një njoftim kur të përfundojë.", + "consent.export_posts": "Eksporto postimet (.csv)", + "consent.export-posts-success": "Duke eksportuar postimet, do të merrni një njoftim në përfundim.", + "emailUpdate.intro": "Ju lutemi shkruani adresën tuaj të emailit më poshtë. Ky forum përdor adresën tuaj të emailit për përmbledhjen dhe njoftimet e planifikuara, si dhe për rikuperimin e llogarisë në rast të një fjalëkalimi të humbur.", + "emailUpdate.optional": "Kjo fushë është fakultative. Ju nuk jeni të detyruar të jepni adresën tuaj të emailit, por pa një email të vërtetuar nuk do të jeni në gjendje të rikuperoni llogarinë tuaj ose të identifikoheni me emailin tuaj.", + "emailUpdate.required": "Kjo fushë është e detyrueshme.", + "emailUpdate.change-instructions": "Një email konfirmimi do të dërgohet në adresën e postës elektronike të dhene me një link unik. Hyrja në atë link do të konfirmojë zotërimin tuaj të adresës së emailit dhe ajo do të bëhet aktive në llogarinë tuaj. Në çdo kohë, ju mund të përditësoni emailin tuaj në dosje nga faqja e llogarisë tuaj.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/sq-AL/users.json b/public/language/sq-AL/users.json new file mode 100644 index 0000000000..98ef270d3f --- /dev/null +++ b/public/language/sq-AL/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Përdoruesit e fundit", + "top_posters": "Postuesit më të mirë", + "most_reputation": "Me Reputacion", + "most_flags": "Më të raportuarit", + "search": "Kërko", + "enter_username": "Kërko një përdorues", + "search-user-for-chat": "Kërkoni një përdorues për të filluar bisedën", + "load_more": "Ngarko më shumë", + "users-found-search-took": "%1 përdorues u gjet (en) ! Kërkimi zgjati %2 sekonda.", + "filter-by": "Filtro sipas", + "online-only": "Online vetëm", + "invite": "Fto", + "prompt-email": "Email-et", + "groups-to-join": "Grupet për t'u bashkuar kur ftesa të pranohet:", + "invitation-email-sent": "Një email ftese i është dërguar %1", + "user_list": "Lista e përdoruesve", + "recent_topics": "Temat e fundit", + "popular_topics": "Temat me te kerkuara", + "unread_topics": "Tema të palexuara", + "categories": "Kategoritë", + "tags": "Tags", + "no-users-found": "Nuk u gjet asnjë përdorues!" +} \ No newline at end of file diff --git a/public/language/sr/_DO_NOT_EDIT_FILES_HERE.md b/public/language/sr/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/sr/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/sr/admin/admin.json b/public/language/sr/admin/admin.json new file mode 100644 index 0000000000..a4c1a1cdb1 --- /dev/null +++ b/public/language/sr/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "Da li želite da restartujete NodeBB?", + + "acp-title": "%1 | NodeBB Administratorski panel", + "settings-header-contents": "Sadržaj", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/sr/admin/advanced/cache.json b/public/language/sr/admin/advanced/cache.json new file mode 100644 index 0000000000..0148c2d889 --- /dev/null +++ b/public/language/sr/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Post Cache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Post Cache Size", + "items-in-cache": "Items in Cache" +} \ No newline at end of file diff --git a/public/language/sr/admin/advanced/database.json b/public/language/sr/admin/advanced/database.json new file mode 100644 index 0000000000..a529f06370 --- /dev/null +++ b/public/language/sr/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 megabajt", + "x-gb": "%1 gigabajt", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB verzija", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Kolekcije", + "mongo.objects": "Objekti", + "mongo.avg-object-size": "Prosečna veličina objekta", + "mongo.data-size": "Veličina podatka", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Veličina Index-a", + "mongo.file-size": "Veličina Fajla", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtuelna memorija", + "mongo.mapped-memory": "Mapirana Memorija", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "Sirove informacije o MongoDB", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis verzija", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Klijenata povezano", + "redis.connected-slaves": "Povezano \"robova\"", + "redis.blocked-clients": "Klijenata blokirano", + "redis.used-memory": "Iskorišćena memorija", + "redis.memory-frag-ratio": "Odnos fragmentisane memorije", + "redis.total-connections-recieved": "Ukupno primljeno konekcija", + "redis.total-commands-processed": "Ukupno komandi procesuirano", + "redis.iops": "Trenutno operacija po sekundi", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Sirove informacije o Redis-u", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/sr/admin/advanced/errors.json b/public/language/sr/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/sr/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/sr/admin/advanced/events.json b/public/language/sr/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/sr/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/sr/admin/advanced/logs.json b/public/language/sr/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/sr/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/sr/admin/appearance/customise.json b/public/language/sr/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/sr/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/sr/admin/appearance/skins.json b/public/language/sr/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/sr/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/sr/admin/appearance/themes.json b/public/language/sr/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/sr/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/sr/admin/dashboard.json b/public/language/sr/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/sr/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/sr/admin/development/info.json b/public/language/sr/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/sr/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/sr/admin/development/logger.json b/public/language/sr/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/sr/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/sr/admin/extend/plugins.json b/public/language/sr/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/sr/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/sr/admin/extend/rewards.json b/public/language/sr/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/sr/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/sr/admin/extend/widgets.json b/public/language/sr/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/sr/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/admins-mods.json b/public/language/sr/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/sr/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/categories.json b/public/language/sr/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/sr/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/digest.json b/public/language/sr/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/sr/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/sr/admin/manage/groups.json b/public/language/sr/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/sr/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/privileges.json b/public/language/sr/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/sr/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/registration.json b/public/language/sr/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/sr/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/tags.json b/public/language/sr/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/sr/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/uploads.json b/public/language/sr/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/sr/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/sr/admin/manage/users.json b/public/language/sr/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/sr/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/sr/admin/menu.json b/public/language/sr/admin/menu.json new file mode 100644 index 0000000000..02074b61c0 --- /dev/null +++ b/public/language/sr/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Uopšteno", + + "section-manage": "Menadžment", + "manage/categories": "Kategorije", + "manage/privileges": "Privileges", + "manage/tags": "Tagovi", + "manage/users": "Korisnici", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Lista Registracija", + "manage/post-queue": "Post Queue", + "manage/groups": "Grupe", + "manage/ip-blacklist": "Crna Lista IP adresa", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Podešavanja", + "settings/general": "Uopšteno", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Gosti", + "settings/uploads": "Otpremljene datoteke", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifikacije", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Napredno", + + "settings.page-title": "%1 Podešavanja", + + "section-appearance": "Izgled", + "appearance/themes": "Teme", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Proširiti", + "extend/plugins": "Plaginovi", + "extend/widgets": "Vidžeti", + "extend/rewards": "Nagrade", + + "section-social-auth": "Auntentifikacija sa društvenih mreža", + + "section-plugins": "Plugins", + "extend/plugins.install": "Instaliraj plaginove", + + "section-advanced": "Napredno", + "advanced/database": "Baza podataka", + "advanced/events": "Događaji", + "advanced/hooks": "Hooks", + "advanced/logs": "Izveštaji", + "advanced/errors": "Greške", + "advanced/cache": "Cache", + "development/logger": "Loger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Ponovo učitaj forum", + "logout": "Izloguj se", + "view-forum": "Pogledaj Forum", + + "search.placeholder": "Search settings", + "search.no-results": "Nema rezultata...", + "search.search-forum": "Pretraži forum za ", + "search.keep-typing": "Ukucaj više da vidiš rezultate", + "search.start-typing": "Počni da kucaš da vidiš rezultate...", + + "connection-lost": "Konekcija ka %1 je izgubljena, pokušavam ponovo da se konektujem...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/advanced.json b/public/language/sr/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/sr/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/api.json b/public/language/sr/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/sr/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/chat.json b/public/language/sr/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/sr/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/cookies.json b/public/language/sr/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/sr/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/email.json b/public/language/sr/admin/settings/email.json new file mode 100644 index 0000000000..7543180f27 --- /dev/null +++ b/public/language/sr/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Podešavanje Email-a", + "address": "Email adresa", + "address-help": "Označena email adresa se odnosi na email koga će primalac videti \"Od\" i \"Odgovori\" poljima.", + "from": "Od koga", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Promeni šablon Email-a", + "template.select": "Izaberi šablon Email-a", + "template.revert": "Vrati na Originalno podešavanje.", + "testing": "Testiranje Email-a", + "testing.select": "Izaberi šablon Email-a", + "testing.send": "Pošalji probni Email", + "testing.send-help": "Probni email će biti poslat na adresu trenutno ulogovanog korisnika", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Molim unesite broj koji označava satnicu kada da pošalje zakazani sažeti email (nrp. 0 za ponoć, 17 za 5:00 pm). Uzmite u obzir da će se slanje događati po satnici samog servara, i da vrlo verovatno se ne poklapa sa satnicom vašeg sistema.
Trenutno vreme servera je:
Sledeći dnevni sažeti email zakazan je za slanje u ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/sr/admin/settings/general.json b/public/language/sr/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/sr/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/sr/admin/settings/group.json b/public/language/sr/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/sr/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/guest.json b/public/language/sr/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/sr/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/homepage.json b/public/language/sr/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/sr/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/languages.json b/public/language/sr/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/sr/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/navigation.json b/public/language/sr/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/sr/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/sr/admin/settings/notifications.json b/public/language/sr/admin/settings/notifications.json new file mode 100644 index 0000000000..c6d8b928ce --- /dev/null +++ b/public/language/sr/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Notifications", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/pagination.json b/public/language/sr/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/sr/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/post.json b/public/language/sr/admin/settings/post.json new file mode 100644 index 0000000000..be3cd4df14 --- /dev/null +++ b/public/language/sr/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Sortiranje postova", + "sorting.post-default": "Uobičajeno sortiranje postova", + "sorting.oldest-to-newest": "Od starijih ka novijim", + "sorting.newest-to-oldest": "Od novijih ka starijim", + "sorting.most-votes": "Najviše glasova", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Uobičajeno sortiranje tema", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Restrikcije postavljanja", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum karaktera za Naslov", + "restrictions.max-title-length": "Maksimum karaktera za Naslov", + "restrictions.min-post-length": "Minimum karaktera za Post", + "restrictions.max-post-length": "Maksimum karaktera za Post", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "Ako se tema smatra \"ustajalom\", onda će upozorenje biti prikazano korisnicima koji su odgovarali na tu temu.", + "timestamp": "Vremenski žig", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Datumi & vreme će biti pokazano na relativan način (npr. \"pre 3 sata\" / \"pre 5 dana\"), i lokalizovano na različite\n\t\t\t\t\tjezike. Posle određenog vremena, ovaj tekst može biti promenjen na lokalizovani datum\n\t\t\t\t\t(npr. 5 Nov 2016 15:30).
(Uobičajeno: 30, ili jedan mesec). Postavi na 0 da uvek prikaže datume, ostavi prazno da uvek prikaže relativno vreme.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Post zadirkivač", + "teaser.last-post": "Poslednji &ndashč Pokazuje poslednji post, uključujući originalni post, ako nema odgovora", + "teaser.last-reply": "Poslednji &ndashč Pokaži najnoviji odgovor, ili ako \"Nema odgovora\" placeholder ako nema odgovora", + "teaser.first": "Prvi", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Nepročitana podešavanja", + "unread.cutoff": "Nepročitano tokom prekinutih dana", + "unread.min-track-last": "Minimum postova u temi, pre praćenja poslednjeg pročitanog", + "recent": "Nedavna Podešavanja", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Onemogući filtriranje tema u ignorisanim kategorijama na /recent stranici", + "signature": "Podešavanja Potpisa", + "signature.disable": "Onemogući potpise", + "signature.no-links": "Onemogući linkove u potpisima", + "signature.no-images": "Onemogući slike u potpisima", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Minimum karaktera u Potpisu", + "composer": "Podešavanje Composer-a", + "composer-help": "Sledeća podešavanja upravljaju funkcionalnošću i/ili izgledom prikazanom kompozera post-a\n\t\t\t\tprema korisnicima kada prave nove teme, ili odgovaraju na postojeće.", + "composer.show-help": "Prikaži tab \"Pomoć\"", + "composer.enable-plugin-help": "Dozvoli plugin-ovima da dodaju sadržaj na tab-u \"pomoć\"", + "composer.custom-help": "Prilagođen tekst za pomoć", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Praćenje IP adrese", + "ip-tracking.each-post": "Prati IP Adresu za svaki post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/reputation.json b/public/language/sr/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/sr/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/social.json b/public/language/sr/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/sr/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/sockets.json b/public/language/sr/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/sr/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/sounds.json b/public/language/sr/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/sr/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/tags.json b/public/language/sr/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/sr/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/sr/admin/settings/uploads.json b/public/language/sr/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/sr/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/sr/admin/settings/user.json b/public/language/sr/admin/settings/user.json new file mode 100644 index 0000000000..041e750fcc --- /dev/null +++ b/public/language/sr/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Auntentifikacija", + "email-confirm-interval": "Korisnik možda neće moći da ponovo pošalje email konfirmaciju sve dok", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Dozvoli login sa", + "allow-login-with.username-email": "Korisničko ime ili Email", + "allow-login-with.username": "Samo korisničko ime", + "account-settings": "Podešavanje naloga", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Onemogući promenu korisničkog imena", + "disable-email-changes": "Onemogući promenu email-a", + "disable-password-changes": "Onemogući promenu šifre", + "allow-account-deletion": "Dozvoli brisanje naloga", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Teme", + "disable-user-skins": "Onemogući korisnike da izaberu određenu temu", + "account-protection": "Začtita naloga", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Dozvoljeno logovanje po satu", + "login-attempts-help": "Ako broj logovanja prema user's predje određenu granicu, taj nalog može biti zaključan na određeno prekonfigurisano vreme", + "lockout-duration": "Trajanje dok se nalog ne otključa (minuta)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Forsiraj resetovanje lozinke nakon odredjenog broja dana", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "Registracija korisnika", + "registration-type": "Tip registracije", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normalno", + "registration-type.admin-approval": "Administratorsko odobravanje", + "registration-type.admin-approval-ip": "Administratosko odobravanje za IP", + "registration-type.invite-only": "Samo pozivnica", + "registration-type.admin-invite-only": "Samo administratorsko pozivanje", + "registration-type.disabled": "Nema registracije", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maksimum poziva po korisniku.", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 za bez restrikcija. Administratori dobijaju bezgranično pozivnica
Samo određeni za \"Samo pozivnica\"", + "invite-expiration": "Isticanje pozivnice", + "invite-expiration-help": "# dana kada ističe pozivnica.", + "min-username-length": "Minimum karaktera u korisničkom imenu", + "max-username-length": "Maksimum karaktera u korisničkom imenu", + "min-password-length": "Minimum karaktera u lozinci", + "min-password-strength": "Minimalna jačina lozinke", + "max-about-me-length": "Maksimum karaktera O Meni", + "terms-of-use": "Uslovi upotrebe foruma (Ostavite prazno da onemogućite)", + "user-search": "Pretraga Korisnika", + "user-search-results-per-page": "Broj rezultata po prikazu", + "default-user-settings": "Uobičajne Postavke Korisnika", + "show-email": "Prikaži email", + "show-fullname": "Prikaži puno ime", + "restrict-chat": "Samo dozvoli chat poruke korisnika koje ja pratim", + "outgoing-new-tab": "Otvori odlazeće linove u novom tabu", + "topic-search": "Omogući pretraživanje u temi", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Pretplatite se na Digest", + "digest-freq.off": "Isključeno", + "digest-freq.daily": "Dnevno", + "digest-freq.weekly": "Nedeljno", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Mesečno", + "email-chat-notifs": "Pošalji email ako nova chat poruka stigne dok nisam online", + "email-post-notif": "Pošalji email kada odgovori su načinjeni u temu u kojoj sam ja pretplaćen", + "follow-created-topics": "Prati teme koje si ti napravio", + "follow-replied-topics": "Prati teme na koje si ti odgovorio", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/sr/admin/settings/web-crawler.json b/public/language/sr/admin/settings/web-crawler.json new file mode 100644 index 0000000000..82e852567c --- /dev/null +++ b/public/language/sr/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Podešsavanje crawl-ovanja", + "robots-txt": "Napredni Robots.txt Ostavite prazno za uobičajena podešavanja", + "sitemap-feed-settings": "Mapa sajta i podešavanje Feed-a", + "disable-rss-feeds": "Onemogući RSS Feed", + "disable-sitemap-xml": "Onemogući Sitemap.xml", + "sitemap-topics": "Broj Tema za prikaz u Mapi sajta", + "clear-sitemap-cache": "Obriši cache Mape sajta", + "view-sitemap": "Pogledaj Mapu sajta" +} \ No newline at end of file diff --git a/public/language/sr/category.json b/public/language/sr/category.json new file mode 100644 index 0000000000..26b049b3a0 --- /dev/null +++ b/public/language/sr/category.json @@ -0,0 +1,23 @@ +{ + "category": "Категорија", + "subcategories": "Поткатегорије", + "new_topic_button": "Нова тема", + "guest-login-post": "Пријавите се да бисте послали поруку", + "no_topics": "Нема тема у овој категорији.
Зашто не бисте поставили једну?", + "browsing": "гледа", + "no_replies": "Још увек нема одговора", + "no_new_posts": "Нема нових порука", + "watch": "Надгледај", + "ignore": "Игнориши", + "watching": "Надгледај", + "not-watching": "Не надгледај", + "ignoring": "Игнориши", + "watching.description": "Прикажи теме у непрочитаним и недавним", + "not-watching.description": "Не приказуј теме у непрочитаним, прикажи у недавним", + "ignoring.description": "Не приказуј теме у непрочитаним и недавним", + "watching.message": "Сада надгледате ажурирања из ове категорије и свих поткатегорија", + "notwatching.message": "Не надгледате ажурирања из ове категорије и свих поткатегорија", + "ignoring.message": "Сада игноришете ажурирања из ове категорије и свих поткатегорија", + "watched-categories": "Надгледане категорије", + "x-more-categories": "Још %1 категорије/а" +} \ No newline at end of file diff --git a/public/language/sr/email.json b/public/language/sr/email.json new file mode 100644 index 0000000000..44c271e691 --- /dev/null +++ b/public/language/sr/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Пробна е-пошта", + "password-reset-requested": "Захтевано је поништавање лозинке!", + "welcome-to": "Добродошли на %1", + "invite": "Позивница од %1", + "greeting_no_name": "Здраво", + "greeting_with_name": "Здраво %1", + "email.verify-your-email.subject": "Потврдите вашу е-пошту", + "email.verify.text1": "Захтевали сте да променимо или потврдимо вашу адресу е-поште", + "email.verify.text2": "Из безбедносних разлога, мењамо или потврђујемо адресу е-поште која се налази у евиденцији само након што је њено власништво потврђено путем е-поште. Ако ово нисте захтевали, не морате ништа да предузимате.", + "email.verify.text3": "Када потврдите ову адресу е-поште, заменићемо вашу тренутну адресу е-поште овом (%1).", + "welcome.text1": "Хвала што сте се регистровали на %1!", + "welcome.text2": "Да бисте у потпуности активирали ваш налог, потребно је да проверимо да ли стварно поседујете адресу е-поште којом сте се регистровали. ", + "welcome.text3": "Администратор је прихватио вашу регистрацију. Можете се пријавити са вашим именом и лозинком.", + "welcome.cta": "Кликните овде за потврду адресе ваше е-поште", + "invitation.text1": "%1 вас је позвао да се придружите %2", + "invitation.text2": "Ваша позивница ће истећи за %1 дана.", + "invitation.cta": "Кликните овде да бисте креирали ваш налог.", + "reset.text1": "Примили смо захтев за ресетовање ваше лозинке, вероватно зато што сте је заборавили. Уколико то није случај, молимо да занемарите ово писмо.", + "reset.text2": "Да би наставили ас ресетовањем лозинке, кликните на следећу везу:", + "reset.cta": "Кликните овде да ресетујете лозинку", + "reset.notify.subject": "Лозика је успешно змењена", + "reset.notify.text1": "Обавештавамо вас да вам је лозинка на %1 успешно ресетована.", + "reset.notify.text2": "Уколико нисте ви ово одобрили, молимо одмах контактирајте администратора.", + "digest.latest_topics": "Недавне теме од %1", + "digest.top-topics": "Најбоље теме од %1", + "digest.popular-topics": "Популарне теме од %1", + "digest.cta": "Кликните овде да посетите %1", + "digest.unsub.info": "Овај сажетак вам је послат услед вашег подешавања претплате.", + "digest.day": "Дан", + "digest.week": "Недеља", + "digest.month": "Месец", + "digest.subject": "Сажетак за %1", + "digest.title.day": "Ваш дневни сажетак", + "digest.title.week": "Ваш седмични сажетак", + "digest.title.month": "Ваш месечни сажетак", + "notif.chat.subject": "Примљена је нова порука ћаскања од %1", + "notif.chat.cta": "Кликните овде да наставите са разговором", + "notif.chat.unsub.info": "Ова обавештење о ћаскању вам је послато услед вашег подешавања претплате.", + "notif.post.unsub.info": "Ово обавештење вам је послато услед вашег подешавања претплате.", + "notif.post.unsub.one-click": "Алтернативно, откажите будуће овакву е-пошту, кликом на", + "notif.cta": "Ка форуму", + "notif.cta-new-reply": "Погледајте поруку", + "notif.cta-new-chat": "Погледајте ћаскање", + "notif.test.short": "Тестирање обавештења", + "notif.test.long": "Ово је тест е-поште са обавештењима. Пошаљите помоћ!", + "test.text1": "Ово је пробно е-писмо за проверу исправности поставки е-поштара у NodeBB.", + "unsub.cta": "Кликните овде да измените та подешавања", + "unsubscribe": "одјава", + "unsub.success": "Више нећете примати е-пошту са листе слања %1", + "unsub.failure.title": "Није могуће одјавити се", + "unsub.failure.message": "Нажалост, нисмо били у могућности да вас одјавимо са листе за слање јер је дошло до проблема са везом. Међутим, можете да промените подешавања е-поште тако што ћете отићи на ваша корисничка подешавања.

(грешка: %1)", + "banned.subject": "Забрањени сте на %1", + "banned.text1": "Корисник %1 је забрањен на %2.", + "banned.text2": "Ова забрана ће трајати до %1.", + "banned.text3": "Ово је разлог зашто сте забрањени:", + "closing": "Хвала!" +} \ No newline at end of file diff --git a/public/language/sr/error.json b/public/language/sr/error.json new file mode 100644 index 0000000000..2816cd21b3 --- /dev/null +++ b/public/language/sr/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Неисправни подаци", + "invalid-json": "Неважећи JSON", + "wrong-parameter-type": "Очекивана је вредност типа %3 за својство %1, али је уместо тога примљен %2", + "required-parameters-missing": "Недостајали су обавезни параметри у овом API позиву: %1", + "not-logged-in": "Изгледа да нисте пријављени.", + "account-locked": "Ваш налог је привремено закључан", + "search-requires-login": "Претраживање захтева налог — пријавите се или се региструјте.", + "goback": "Притисните назад за повратак на претходну страну", + "invalid-cid": "Неисправан ID категорије", + "invalid-tid": "Неисправан ID теме", + "invalid-pid": "Неисправан ID поруке", + "invalid-uid": "Неисправан ID корисника", + "invalid-mid": "Неисправан ID поруке ћаскања", + "invalid-date": "Мора се навести важећи датум", + "invalid-username": "Неисправно корисничко име", + "invalid-email": "Неисправна е-пошта", + "invalid-fullname": "Неисправно пуно име", + "invalid-location": "Неисправна локација", + "invalid-birthday": "Неисправан датум рођења", + "invalid-title": "Неисправан наслов", + "invalid-user-data": "Неисправни кориснички подаци", + "invalid-password": "Неисправна лозинка", + "invalid-login-credentials": "Неважећи акредитиви за пријављивање", + "invalid-username-or-password": "Молимо наведите и корисничко име и лозинку", + "invalid-search-term": "Неисправан упит за претрагу", + "invalid-url": "Неважећа адреса", + "invalid-event": "Неважећи догађај: %1", + "local-login-disabled": "Локални систем за пријављивање је онемогућен за непривилеговане налоге.", + "csrf-invalid": "Нисмо успели да вас пријавимо, вероватно због истека сесије. Молимо покушајте поново", + "invalid-path": "Неважећа путања", + "folder-exists": "Фасцикла постоји", + "invalid-pagination-value": "Неважећа вредност приликом нумерисања страница, мора бити најмање %1 а највише %2 ", + "username-taken": "Корисничко име је заузето", + "email-taken": "Адреса е-поште је заузета", + "email-nochange": "Унета е-пошта је иста као е-пошта која је већ у евиденцији.", + "email-invited": "Е-пошта је већ позвана", + "email-not-confirmed": "Објављивање у неким категоријама или темама је омогућено када потврдите вашу е-пошту, кликните овде да бисте послали е-поруку за потврду.", + "email-not-confirmed-chat": "Није вам дозвољено да ћаскате док не потврдите вашу е-пошту, кликните овде да то учините.", + "email-not-confirmed-email-sent": "Ваша е-пошта још увек није потврђена, проверите да ли у пријемном сандучету има е-поште за потврду. Можда нећете моћи да објављујете у неким категоријама или ћаскате док не потврдите вашу е-пошту.", + "no-email-to-confirm": "Ваш налог нема подешену адресу е-поште. Е-пошта је неопходна за опоравак налога, а може бити неопходна за ћаскање и објављивање у неким категоријама. Кликните овде да унесете е-пошту.", + "user-doesnt-have-email": "Корисник \"%1\" нема подешену е-пошту.", + "email-confirm-failed": "Потврда е-поште није успела, молимо вас да покушате касније.", + "confirm-email-already-sent": "Е-порука за потврду је већ послата, молимо вас да сачекате %1 минут(а) да бисте послали други.", + "sendmail-not-found": "Програм за слање поште није пронађен, проверите да ли је инсталиран и покренут од стране корисника NodeBB.", + "digest-not-enabled": "Овај корисник нема омогућене сажетке или систем није подразумевано конфигурисан за слање сажетака", + "username-too-short": "Корисничко име је прекратко", + "username-too-long": "Корисничко име је предуго", + "password-too-long": "Шифра је предугачка.", + "reset-rate-limited": "Превише захтева за поништавање лозинке (ограничена стопа)", + "reset-same-password": "Користите лозинку која се разликује од ваше тренутне", + "user-banned": "Корисник је забрањен", + "user-banned-reason": "Овај налог је забрањен (Разлог: %1)", + "user-banned-reason-until": "Овај налог је забрањен до %1 (Разлог: %2)", + "user-too-new": "Жао нам је, морате сачекати %1 секунде/и пре него што објавите прву поруку", + "blacklisted-ip": "Жао нам је, ваша IP је забрањена у овој заједници. Ако мислите да је ово грешка, контактирајте администратора.", + "ban-expiry-missing": "Наведите крајњи датум за ову забрану", + "no-category": "Категорија не постоји", + "no-topic": "Тема не постоји", + "no-post": "Порука не постоји", + "no-group": "Група не постоји", + "no-user": "Корисник не постоји", + "no-teaser": "Исечак не постоји", + "no-flag": "Заставица не постоји", + "no-privileges": "Немате довољне привилегије за обављање ове радње.", + "category-disabled": "Категорија је онемогућена", + "topic-locked": "Тема је закључана", + "post-edit-duration-expired": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 сек.", + "post-edit-duration-expired-minutes": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 мин.", + "post-edit-duration-expired-minutes-seconds": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 мин. и %2 сек.", + "post-edit-duration-expired-hours": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 час.", + "post-edit-duration-expired-hours-minutes": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 час. и %2 мин.", + "post-edit-duration-expired-days": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 дан.", + "post-edit-duration-expired-days-hours": "Време у којем вам је дозвољено уређивање порука након објављивања: %1 дан. и %2 час.", + "post-delete-duration-expired": "Време у којем вам је дозвољено брисање порука након објављивања: %1 сек.", + "post-delete-duration-expired-minutes": "Време у којем вам је дозвољено брисање порука након објављивања: %1 мин.", + "post-delete-duration-expired-minutes-seconds": "Време у којем вам је дозвољено брисање порука након објављивања: %1 мин. и %2 сек.", + "post-delete-duration-expired-hours": "Време у којем вам је дозвољено брисање порука након објављивања: %1 час.", + "post-delete-duration-expired-hours-minutes": "Време у којем вам је дозвољено брисање порука након објављивања: %1 час. и %2 мин.", + "post-delete-duration-expired-days": "Време у којем вам је дозвољено брисање порука након објављивања: %1 дан.", + "post-delete-duration-expired-days-hours": "Време у којем вам је дозвољено брисање порука након објављивања: %1 дан. и %2 час.", + "cant-delete-topic-has-reply": "Не можете обрисати вашу тему након што је на њу одговорено", + "cant-delete-topic-has-replies": "Не можете обрисати вашу тему након што добије %1 одговора", + "content-too-short": "Унесите дужу поруку. Порука мора садржати најмање %1 знак(ов)а.", + "content-too-long": "Унесите краћу поруку. Порука не сме бити дужа од %1 знак(ов)а.", + "title-too-short": "Унесите дужи наслов. Наслов мора садржати најмање %1 знак(ов)а.", + "title-too-long": "Унесите краћи наслов. Наслов не сме бити дужи од %1 знак(ов)а.", + "category-not-selected": "Није одабрана категорија", + "too-many-posts": "Можете објављивати поруке само једном у %1 секунди - сачекајте пре него што покушате поново", + "too-many-posts-newbie": "Као нови корисник, можете објављивати поруке само једном у %1 секунди док не достигнете %2 углед - сачекајте пре него што покушате поново", + "already-posting": "You are already posting", + "tag-too-short": "Унесите дужу ознаку. Ознаке морају садржати најмање %1 знак(ов)а.", + "tag-too-long": "Унесите краћу ознаку. Ознаке не смеју бити дуже од %1 знак(ов)а.", + "not-enough-tags": "Нема довољно ознака. Теме морају имати најмање %1 ознаке/а.", + "too-many-tags": "Превише ознака. Теме не смеју имати више од %1 ознаке/а.", + "cant-use-system-tag": "Не можете користити ову системску ознаку.", + "cant-remove-system-tag": "Не можете уклонити ову системску ознаку.", + "still-uploading": "Сачекајте док се отпремања не заврше.", + "file-too-big": "Највећа дозвољена величина датотеке је %1 kB - отпремите мању датотеку.", + "guest-upload-disabled": "Гостима је онемогућено отпремање", + "cors-error": "Није могуће отпремити слику због погрешно конфигурисаног CORS", + "upload-ratelimit-reached": "Отпремили сте превише датотека одједном. Покушајте поново касније.", + "scheduling-to-past": "Изаберите датум у будућности.", + "invalid-schedule-date": "Унесите важећи датум и време.", + "cant-pin-scheduled": "Планиране теме се не могу закачити/откачити.", + "cant-merge-scheduled": "Планиране теме се не могу спојити.", + "cant-move-posts-to-scheduled": "Није могуће преместити поруке у планирану тему.", + "cant-move-from-scheduled-to-existing": "Није могуће преместити поруке из планиране теме у постојећу.", + "already-bookmarked": "Већ сте додали ову поруку у обележиваче", + "already-unbookmarked": "Већ сте одстранили ову поруку из обележивача", + "cant-ban-other-admins": "Не можете забранити друге администраторе!", + "cant-mute-other-admins": "Не можете привремено искључити друге администраторе!", + "user-muted-for-hours": "Привремено сте искључени, моћи ћете да објављујете за %1 час.", + "user-muted-for-minutes": "Привремено сте искључени, моћи ћете да објављујете за %1 минут(а)", + "cant-make-banned-users-admin": "Не можете забрањене кориснике учинити администраторима.", + "cant-remove-last-admin": "Ви сте једини администратор. Додајте другог корисника као администратора пре него што уклоните себе као администратора.", + "account-deletion-disabled": "Брисање налога је онемогућено", + "cant-delete-admin": "Уклоните администраторске привилегије овом налогу пре него што покушате да га избришете.", + "already-deleting": "Већ се брише", + "invalid-image": "Неважећа слика", + "invalid-image-type": "Неважећи тип слике. Дозвољени типови су: %1", + "invalid-image-extension": "Неважећи тип слике", + "invalid-file-type": "Неважећи тип датотеке. Дозвољени типови су: %1", + "invalid-image-dimensions": "Димензије слике су превелике", + "group-name-too-short": "Име групе је прекратко", + "group-name-too-long": "Име групе је предугачко", + "group-already-exists": "Група већ постоји", + "group-name-change-not-allowed": "Мењање имена групе није дозвољено", + "group-already-member": "Већ је део ове групе", + "group-not-member": "Није члан ове групе", + "group-needs-owner": "Неопходан је најмање један власник ове групе", + "group-already-invited": "Овај корисник је већ позван", + "group-already-requested": "Ваш захтев за чланство је већ поднесен", + "group-join-disabled": "Тренутно нисте у могућности да се придружите овој групи", + "group-leave-disabled": "Тренутно нисте у могућности да напустите ову групу", + "post-already-deleted": "Ова порука је већ избрисана", + "post-already-restored": "Ова порука је већ обновљена", + "topic-already-deleted": "Ова тема је већ избрисана", + "topic-already-restored": "Ова тема је већ обновљена", + "cant-purge-main-post": "Не можете очистити насловну поруку, избришите тему уместо тога", + "topic-thumbnails-are-disabled": "Сличице тема су онемогућене.", + "invalid-file": "Неисправна датотека", + "uploads-are-disabled": "Отпремања су онемогућена", + "signature-too-long": "Жао нам је, потпис не сме бити дужи од %1 знак(ов)а.", + "about-me-too-long": "Жао нам је, информације о вама не смеју бити дуже од %1 знак(ов)а.", + "cant-chat-with-yourself": "Не можете ћаскати са самим собом!", + "chat-restricted": "Овај корисник је ограничио њихова ћаскања. Морају вас пратити пре него што можете ћаскати са њима.", + "chat-disabled": "Ћаскања су онемогућена", + "too-many-messages": "Послали сте превише порука, сачекајте мало.", + "invalid-chat-message": "Неважећа порука", + "chat-message-too-long": "Поруке ћаскања не могу бити дуже од %1 знакова.", + "cant-edit-chat-message": "Није вам дозвољено да уређујете ову поруку", + "cant-delete-chat-message": "Није вам дозвољено да избришете ову поруку", + "chat-edit-duration-expired": "Време у којем вам је дозвољено уређивање порука ћаскања након објављивања: %1 сек.", + "chat-delete-duration-expired": "Време у којем вам је дозвољено брисање порука ћаскања након објављивања: %1 сек.", + "chat-deleted-already": "Ова порука ћаскања је већ избрисана.", + "chat-restored-already": "Ова порука ћаскања је већ обновљена.", + "chat-room-does-not-exist": "Соба за ћаскање не постоји.", + "already-voting-for-this-post": "Већ сте гласали за ову поруку.", + "reputation-system-disabled": "Угледи су онемогућени.", + "downvoting-disabled": "Негативно гласање је онемогућено", + "not-enough-reputation-to-chat": "Потребно репутација: %1 за ћаскање", + "not-enough-reputation-to-upvote": "Потребно репутација: %1 за гласање", + "not-enough-reputation-to-downvote": "Потребно репутација: %1 за негативно гласање", + "not-enough-reputation-to-flag": "Потребно репутација: %1 да бисте заставицом означили ову поруку", + "not-enough-reputation-min-rep-website": "Потребно репутација: %1 за додавање веб сајта", + "not-enough-reputation-min-rep-aboutme": "Потребно репутација: %1 за додавање информација о себи", + "not-enough-reputation-min-rep-signature": "Потребно репутација: %1 за додавање потписа", + "not-enough-reputation-min-rep-profile-picture": "Потребно репутација: %1 за додавање профилне слике", + "not-enough-reputation-min-rep-cover-picture": "Потребно репутација: %1 за додавање насловне слике", + "post-already-flagged": "Већ сте означили заставицом ову поруку", + "user-already-flagged": "Већ сте означили заставицом овог корисника", + "post-flagged-too-many-times": "Ову поруку су већ означили заставицом други", + "user-flagged-too-many-times": "Овог корисника су већ означили заставицом други", + "cant-flag-privileged": "Није вам дозвољено да означавате заставицом профиле или садржај привилегованих корисника (модератори/глобални модератори/администратори)", + "self-vote": "Не можете гласати за своју поруку", + "too-many-upvotes-today": "Можете гласати само %1 пута дневно", + "too-many-upvotes-today-user": "Можете гласати за корисника само %1 пута дневно", + "too-many-downvotes-today": "Можете негативно гласати само %1 пута дневно", + "too-many-downvotes-today-user": "Можете негативно гласати за корисника само %1 пута дневно", + "reload-failed": "NodeBB је наишао на проблем док се поново учитавао: \"%1\". NodeBB ће наставити да опслужује постојећа клијентска средства , иако би требало да опозовете оно што сте урадили пре поновног учитавања.", + "registration-error": "Грешка при регистрацији", + "parse-error": "Нешто је кренуло погрешно приликом анализе одговора сервера", + "wrong-login-type-email": "Користите вашу е-пошту за пријављивање", + "wrong-login-type-username": "Користите ваше корисничко име за пријављивање", + "sso-registration-disabled": "Регистрација је онемогућена за %1 налога, региструјте се са адресом е-поште прво", + "sso-multiple-association": "Не можете повезати више налога са овог сервиса на ваш NodeBB налог. Раздвојите ваш постојећи налог и покушајте поново.", + "invite-maximum-met": "Позвали сте максимални број особа (%1 од %2).", + "no-session-found": "Није пронађена сесија пријављивања!", + "not-in-room": "Корисник није у соби", + "cant-kick-self": "Не можете избацити себе из групе", + "no-users-selected": "Није одабран корисник", + "invalid-home-page-route": "Неважећа путања матичне странице", + "invalid-session": "Неважећа сесија", + "invalid-session-text": "Изгледа да ваша сесија пријављивања није више активна. Поново учитајте ову страницу.", + "session-mismatch": "Неподударање сесије", + "session-mismatch-text": "Изгледа да се ваша сесија пријављивања више не подудара са сервером. Поново учитајте ову страницу.", + "no-topics-selected": "Нема одабраних тема!", + "cant-move-to-same-topic": "Није могуће преместити поруку у исту тему!", + "cant-move-topic-to-same-category": "Није могуће преместити тему у исту категорију!", + "cannot-block-self": "Не можете блокирати себе!", + "cannot-block-privileged": "Не можете блокирати администраторе или глобалне модераторе", + "cannot-block-guest": "Гости нису у могућности да блокирају друге кориснике", + "already-blocked": "Овај корисник је већ блокиран", + "already-unblocked": "Овај корисник је већ одблокиран", + "no-connection": "Изгледа да постоји проблем са вашом интернет везом", + "socket-reconnect-failed": "Тренутно није могуће приступити серверу. Кликните овде да бисте покушали поново или покушајте поново касније", + "plugin-not-whitelisted": "Инсталација додатне компоненте &ndash није могућа; преко ACP-а могу се инсталирати само додатне компоненте које је на белој листи ставио NodeBB Package Manager", + "plugins-set-in-configuration": "Није вам дозвољено да мењате стање додатне компоненте онако како је дефинисано у време извршавања (config.json, променљиве окружења или аргументи терминала), уместо тога измените конфигурацију.", + "theme-not-set-in-configuration": "Приликом дефинисања активних додатних компоненти у конфигурацији, промена тема захтева додавање нове теме на листу активних додатних компоненти пре ажурирања у ACP", + "topic-event-unrecognized": "Догађај из теме „%1“ није препознат", + "cant-set-child-as-parent": "Није могуће поставити подређену категорију као надређену", + "cant-set-self-as-parent": "Није могуће поставити себе као надређену категорију", + "api.master-token-no-uid": "Примљен је главни токен без одговарајућег `_uid` у телу захтева", + "api.400": "Нешто није било у реду са товаром захтева који сте проследили.", + "api.401": "Није пронађена важећа сесија за пријављивање. Пријавите се и покушајте поново.", + "api.403": "Нисте овлашћени да обавите овај позив", + "api.404": "Неважећи API позив", + "api.426": "HTTPS је неопходан за захтеве за записан api, молимо вас да поново пошаљете ваш захтев путем HTTPS-а", + "api.429": "Поднели сте превише захтева, покушајте поново касније", + "api.500": "Дошло је до неочекиване грешке приликом покушаја сервисирања вашег захтева.", + "api.501": "Рута коју покушавате да позовете још увек није примењена, покушајте поново сутра", + "api.503": "Рута коју покушавате да позовете тренутно није доступна због конфигурације сервера" +} \ No newline at end of file diff --git a/public/language/sr/flags.json b/public/language/sr/flags.json new file mode 100644 index 0000000000..f8d80dff6e --- /dev/null +++ b/public/language/sr/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Стање", + "reports": "Извештаји", + "first-reported": "Прво пријављено", + "no-flags": "Ура! Нема заставица.", + "assignee": "Заступник", + "update": "Ажурирај", + "updated": "Ажурирано", + "resolved": "Решено", + "target-purged": "Садржај на који се односи ова заставица је очишћен и није више доступан.", + + "graph-label": "Дневне заставице", + "quick-filters": "Брзи филтери", + "filter-active": "Постоји један или више активних филтера на овом списку заставица", + "filter-reset": "Уклони заставице", + "filters": "Опције филтера", + "filter-reporterId": "UID извештача", + "filter-targetUid": "UID означеног заставицом", + "filter-type": "Тип заставице", + "filter-type-all": "Сав садржај", + "filter-type-post": "Порука", + "filter-type-user": "Корисник", + "filter-state": "Стање", + "filter-assignee": "UID заступника", + "filter-cid": "Категорија", + "filter-quick-mine": "Додељено мени", + "filter-cid-all": "Све категорије", + "apply-filters": "Примени филтере", + "more-filters": "Више филтера", + "fewer-filters": "Мање филтера", + + "quick-actions": "Брзе радње", + "flagged-user": "Корисник означен заставицом", + "view-profile": "Погледај профил", + "start-new-chat": "Започни ново ћаскање", + "go-to-target": "Погледај циљ означавања заставицом", + "assign-to-me": "Додели мени", + "delete-post": "Избриши поруку", + "purge-post": "Очисти поруку", + "restore-post": "Врати поруку", + "delete": "Избриши заставицу", + + "user-view": "Погледај профил", + "user-edit": "Уреди профил", + + "notes": "Белешке са заставицама", + "add-note": "Додај белешку", + "no-notes": "Нема дељених бележака.", + "delete-note-confirm": "Да ли сте сигурни да желите избрисати ову белешку са заставицом?", + "delete-flag-confirm": "Да ли сте сигурни да желите да избришете ову заставицу?", + "note-added": "Белешка је додата", + "note-deleted": "Белешка је избрисана", + "flag-deleted": "Заставица је избрисана", + + "history": "Налог и историја заставица", + "no-history": "Нема историје заставица", + + "state-all": "Сва стања", + "state-open": "Ново/Отвори", + "state-wip": "Рад у току", + "state-resolved": "Решено", + "state-rejected": "Одбијено", + "no-assignee": "Недодељено", + + "sort": "Сортирај по", + "sort-newest": "Прво најновије", + "sort-oldest": "Прво најстарије", + "sort-reports": "Највише извештаја", + "sort-all": "Сви типови заставица...", + "sort-posts-only": "Само поруке...", + "sort-downvotes": "Највише негативних гласова", + "sort-upvotes": "Највише гласова", + "sort-replies": "Највише одговора", + + "modal-title": "Извештај о садржају", + "modal-body": "Наведите разлог за означавање заставицом %1 %2. Алтернативно, користите један од тастера за брзу пријаву ако је примењиво.", + "modal-reason-spam": "Непожељно", + "modal-reason-offensive": "Увредљиво", + "modal-reason-other": "Остало (наведите испод)", + "modal-reason-custom": "Разлог за пријаву овог садржаја...", + "modal-submit": "Поднеси извештај", + "modal-submit-success": "Садржај је означен заставицом за модерацију.", + + "bulk-actions": "Масовне радње", + "bulk-resolve": "Реши заставицу/е", + "bulk-success": "Ажурираних заставица: %1", + "flagged-timeago-readable": "Означено заставицом (%2)", + "auto-flagged": "[Аутоматски означено заставицом] Примљено је %1 негативних гласова." +} \ No newline at end of file diff --git a/public/language/sr/global.json b/public/language/sr/global.json new file mode 100644 index 0000000000..7da7113606 --- /dev/null +++ b/public/language/sr/global.json @@ -0,0 +1,126 @@ +{ + "home": "Матична страница", + "search": "Претражи", + "buttons.close": "Затвори", + "403.title": "Приступ одбијен", + "403.message": "Изгледа да сте набасали на страницу на којој немате дозвољен приступ.", + "403.login": "Можда би требало да се пријавите?", + "404.title": "Не постоји", + "404.message": "Изгледа да сте наишли на страницу која не постоји. Вратите се на матичну страницу..", + "500.title": "Унутрашња грешка.", + "500.message": "Упс! Изгледа да нешто није како треба!", + "400.title": "Неисправан захтев.", + "400.message": "Изгледа да је веза погрешно уобличена, проверите и пробајте поново. У супротном, вратите се на матичну страницу.", + "register": "Регистрација", + "login": "Пријави се", + "please_log_in": "Молимо, пријавите се", + "logout": "Одјави се", + "posting_restriction_info": "Слање порука је тренутно ограничено само на пријављене кориснике, кликните овде да се пријавите.", + "welcome_back": "Добродошли поново", + "you_have_successfully_logged_in": "Успешно сте се пријавили", + "save_changes": "Сачувај измене", + "save": "Сачувај", + "close": "Затвори", + "pagination": "Нумерисање страница", + "pagination.out_of": "%1 од %2", + "pagination.enter_index": "Иди на индекс порука", + "header.admin": "Админ", + "header.categories": "Категорије", + "header.recent": "Недавно", + "header.unread": "Непрочитано", + "header.tags": "Ознаке", + "header.popular": "Популарно", + "header.top": "Најбоље", + "header.users": "Корисници", + "header.groups": "Групе", + "header.chats": "Ћаскања", + "header.notifications": "Обавештења", + "header.search": "Претрага", + "header.profile": "Профил", + "header.navigation": "Навигација", + "notifications.loading": "Учитавање обавештења", + "chats.loading": "Учитавање ћаскања", + "motd.welcome": "Добродошли на NodeBB, дискусиону платформу будућности.", + "previouspage": "Претходна страна", + "nextpage": "Следећа страна", + "alert.success": "Успешно", + "alert.error": "Грешка", + "alert.banned": "Забрањен", + "alert.banned.message": "Управо сте добили забрану, ваш приступ је сада ограничен.", + "alert.unbanned": "Укинута забрана", + "alert.unbanned.message": "Ваша забрана је укинута.", + "alert.unfollow": "Не пратите више %1!", + "alert.follow": "Сада пратите %1!", + "users": "Корисници", + "topics": "Теме", + "posts": "Поруке", + "x-posts": "%1 поруке/а", + "best": "Најбоље", + "controversial": "Спорно", + "votes": "Гласови", + "x-votes": "%1 гласа/ова", + "voters": "Гласачи", + "upvoters": "Гласали", + "upvoted": "Гласано", + "downvoters": "Негативно гласали", + "downvoted": "Негативно гласано", + "views": "Прегледи", + "posters": "Аутори порука", + "reputation": "Углед", + "lastpost": "Последња порука", + "firstpost": "Прва порука", + "read_more": "прочитајте више", + "more": "Више", + "none": "Ниједан", + "posted_ago_by_guest": "објављено %1 од стране госта.", + "posted_ago_by": "објављено %1 од стране %2", + "posted_ago": "објављено %1", + "posted_in": "објављено у %1", + "posted_in_by": "објављено у %1 од стране %2", + "posted_in_ago": "објављено у %1 %2", + "posted_in_ago_by": "објављено у %1 %2 од стране %3", + "user_posted_ago": "%1 објавио %2", + "guest_posted_ago": "Гост је објавио %1", + "last_edited_by": "последњи пут уредио %1", + "norecentposts": "Нема недавних порука", + "norecenttopics": "Нема недавних тема", + "recentposts": "Недавне поруке", + "recentips": "Недавно забележене IP адресе", + "moderator_tools": "Алати модератора", + "online": "На мрежи", + "away": "Одсутан", + "dnd": "Не узнемиравај", + "invisible": "Невидљив", + "offline": "Ван мреже", + "email": "Е-пошта", + "language": "Језик", + "guest": "Гост", + "guests": "Гости", + "former_user": "Бивши корисник", + "system-user": "Систем", + "unknown-user": "Непознати корисник", + "updated.title": "Форум је ажуриран", + "updated.message": "Форум је управо ажуриран на најновију верзију. Кликните овде да бисте освежили страницу.", + "privacy": "Приватност", + "follow": "Прати", + "unfollow": "Не прати", + "delete_all": "Избриши све", + "map": "Мапа", + "sessions": "Сесије пријављивања", + "ip_address": "IP адреса", + "enter_page_number": "Унесите број странице", + "upload_file": "Отпреми датотеку", + "upload": "Отпреми", + "uploads": "Отпремања", + "allowed-file-types": "Дозвољени типови датотека су %1", + "unsaved-changes": "Имате несачуване промене. Да ли сте сигурни да желите да наставите?", + "reconnecting-message": "Изгледа да је ваша веза ка %1 изгубљена, сачекајте док поново не успоставимо везу.", + "play": "Репродукуј", + "cookies.message": "Овај веб сајт користи колачиће да би вам обезбедили најбољи доживљај на нашем сајту.", + "cookies.accept": "Схватам!", + "cookies.learn_more": "Сазнајте више", + "edited": "Уређено", + "disabled": "Онемогућено", + "select": "Изабери", + "user-search-prompt": "Uкуцајте нешто овде како бисте пронашли кориснике..." +} \ No newline at end of file diff --git a/public/language/sr/groups.json b/public/language/sr/groups.json new file mode 100644 index 0000000000..5ef1f2fa26 --- /dev/null +++ b/public/language/sr/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Групе", + "view_group": "Преглед групе", + "owner": "Власник групе", + "new_group": "Направи нову групу", + "no_groups_found": "Нема група за преглед", + "pending.accept": "Прихвати", + "pending.reject": "Одбиј", + "pending.accept_all": "Прихвати све", + "pending.reject_all": "Одбиј све", + "pending.none": "Тренутно нема чланова на чекању.", + "invited.none": "Тренутно нема позваних чланова.", + "invited.uninvite": "Поништи позив", + "invited.search": "Потражите корисника којег ћете позвати у ову групу", + "invited.notification_title": "Добили сте позив да се придружите %1", + "request.notification_title": "Чланство групе затражено од стране %1", + "request.notification_text": "%1 је затражио да постане члан %2", + "cover-save": "Сачувај", + "cover-saving": "Чување", + "details.title": "Детаљи о групи", + "details.members": "Списак чланова", + "details.pending": "Чланови на чекању", + "details.invited": "Позвани чланови", + "details.has_no_posts": "Чланови ове групе нису написали ниједну поруку.", + "details.latest_posts": "Последње поруке", + "details.private": "Приватна", + "details.disableJoinRequests": "Искључи захтеве за придруживање", + "details.disableLeave": "Онемогући корисницима да напусте групу", + "details.grant": "Одобри/Поништи власништво", + "details.kick": "Избаци", + "details.kick_confirm": "Да ли сте сигурни да желите да уклоните овог члана из групе?", + "details.add-member": "Додај члана", + "details.owner_options": "Администрација групе", + "details.group_name": "Име групе", + "details.member_count": "Број чланова", + "details.creation_date": "Датум настанка", + "details.description": "Опис", + "details.member-post-cids": "ID-ови категорија за приказ порука", + "details.badge_preview": "Приказ беџа", + "details.change_icon": "Промени икону", + "details.change_label_colour": "Промени боју ознаке", + "details.change_text_colour": "Промени боју текста", + "details.badge_text": "Текст беџа", + "details.userTitleEnabled": "Приказ беџа", + "details.private_help": "Уколико је укључено, приступање групи захтева одобрење власника групе.", + "details.hidden": "Скривена", + "details.hidden_help": "Уколико је укључено, група неће бити видљива на списку група, и корисницима се позивнице морају слати ручно.", + "details.delete_group": "Избриши групу", + "details.private_system_help": "Приватне групе су искључене на системском нивоу, ова опција нема ефекта", + "event.updated": "Детаљи групе су ажурирани", + "event.deleted": "Група „%1“ је избрисана", + "membership.accept-invitation": "Прихватите позив", + "membership.accept.notification_title": "Сада сте члан групе %1", + "membership.invitation-pending": "Позиви на чекању", + "membership.join-group": "Придружите се групи", + "membership.leave-group": "Напусти групу", + "membership.leave.notification_title": "%1 је напустио групу %2", + "membership.reject": "Одбаци", + "new-group.group_name": "Име групе:", + "upload-group-cover": "Отпреми насловницу групе", + "bulk-invite-instructions": "Унесите списак корисничких имена одвојених зарезима да бисте позвали у ову групу ", + "bulk-invite": "Масовни позив", + "remove_group_cover_confirm": "Да ли сте сигурни да желите да уклоните насловну слику?" +} \ No newline at end of file diff --git a/public/language/sr/ip-blacklist.json b/public/language/sr/ip-blacklist.json new file mode 100644 index 0000000000..ad61ee7aa4 --- /dev/null +++ b/public/language/sr/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Овде конфигуришите своју црну листу IP-а.", + "description": "Забрана корисничког налога некад није довољно средство за одвраћање. Понекад је ограничавање приступа форуму одређеној IP адреси или низу IP адреса најбољи начин да се форум заштити. У овим сценаријима можете додати проблематичне IP адресе или читаве CIDR блокове на ову црну листу и биће им онемогућено да се пријаве или региструју нови налог.", + "active-rules": "Активна правила", + "validate": "Потврдите црну листу", + "apply": "Примени црну листу", + "hints": "Савети за синтаксу", + "hint-1": "Дефинишите једну IP адресу по реду. Можете додати IP блокове све док следе формат CIDR (нпр. 192.168.100.0/22).", + "hint-2": "У коментаре можете додавати почетне редове са симболом #.", + + "validate.x-valid": "%1 од %2 правила важеће.", + "validate.x-invalid": "Следећа правила (укупно %1) су неважећа:", + + "alerts.applied-success": "Црна листа је примењена", + + "analytics.blacklist-hourly": "Figure 1 – број посета са црне листе на сат", + "analytics.blacklist-daily": "Figure 1 – број посета са црне листе на дан", + "ip-banned": "IP је забрањен" +} \ No newline at end of file diff --git a/public/language/sr/language.json b/public/language/sr/language.json new file mode 100644 index 0000000000..f1a8e3dfc3 --- /dev/null +++ b/public/language/sr/language.json @@ -0,0 +1,5 @@ +{ + "name": "Српски", + "code": "sr", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/sr/login.json b/public/language/sr/login.json new file mode 100644 index 0000000000..74eda37de6 --- /dev/null +++ b/public/language/sr/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Корисничко име / Е-пошта", + "username": "Корисничко име", + "remember_me": "Запамти ме?", + "forgot_password": "Заборављена лозинка?", + "alternative_logins": "Алтернативна пријављивања", + "failed_login_attempt": "Неуспешно пријављивање", + "login_successful": "Успешно сте се пријавили!", + "dont_have_account": "Немате налог?", + "logged-out-due-to-inactivity": "Одјављени сте са администраторске контролне табле због неактивности", + "caps-lock-enabled": "Тастер Caps Lock је укључен" +} \ No newline at end of file diff --git a/public/language/sr/modules.json b/public/language/sr/modules.json new file mode 100644 index 0000000000..14b2bc8812 --- /dev/null +++ b/public/language/sr/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Ћаскај са", + "chat.placeholder": "Укуцајте поруку ћаскања овде, превуците и отпустите слике, притисните enter за слање", + "chat.scroll-up-alert": "Гледате старије поруке, кликните овде да бисте прешли на најновију поруку.", + "chat.send": "Пошаљи", + "chat.no_active": "Нема активних ћаскања.", + "chat.user_typing": "%1 куца ...", + "chat.user_has_messaged_you": "%1 вам је послао поруку.", + "chat.see_all": "Сва ћаскања", + "chat.mark_all_read": "Означи све као прочитано", + "chat.no-messages": "Изаберите примаоца да бисте видели историју ћаскања", + "chat.no-users-in-room": "Нема корисника у овој соби", + "chat.recent-chats": "Недавна ћаскања", + "chat.contacts": "Контакти", + "chat.message-history": "Историја порука", + "chat.message-deleted": "Порука је избрисана", + "chat.options": "Опције ћаскања", + "chat.pop-out": "Истакни ћаскање", + "chat.minimize": "Умањи", + "chat.maximize": "Увећај", + "chat.seven_days": "7 дана", + "chat.thirty_days": "30 дана", + "chat.three_months": "3 месеца", + "chat.delete_message_confirm": "Да ли сте сигурни да желите да избришете ову поруку?", + "chat.retrieving-users": "Преузимање корисника...", + "chat.manage-room": "Управљај собом за ћаскање", + "chat.add-user-help": "Потражите кориснике овде. Када буде изабран, корисник ће бити додан у ћаскање. Нови корисник неће бити у могућности да види поруке написане пре него што је додан у преписку. Само власници соба () могу уклонити кориснике из соба за ћаскање.", + "chat.confirm-chat-with-dnd-user": "Овај корисник је поставио свој статус на \"Не узнемиравај\". Да ли и даље желите да ћаскате са њим?", + "chat.rename-room": "Преименуј собу", + "chat.rename-placeholder": "Унесите назив собе овде", + "chat.rename-help": "Име собе постављено овде биће видљиво свим учесницима у соби.", + "chat.leave": "Напусти ћаскање", + "chat.leave-prompt": "Да ли сте сигурни да желите да напустите ово ћаскање?", + "chat.leave-help": "Напуштање овог ћаскања ће вас уклонити из будућих преписки у овом ћаскању. Ако будете поново додани у будућности, нећете видети историју ћаскања од пре вашег поновног придруживања.", + "chat.in-room": "У овој соби", + "chat.kick": "Избаци", + "chat.show-ip": "Прикажи IP", + "chat.owner": "Власник собе", + "chat.system.user-join": "%1 се придружио соби", + "chat.system.user-leave": "%1 је напустио собу", + "chat.system.room-rename": "%2 је преименовао собу: %1", + "composer.compose": "Писање поруке", + "composer.show_preview": "Прикажи преглед", + "composer.hide_preview": "Сакриј преглед", + "composer.user_said_in": "%1 је рекао у %2", + "composer.user_said": "%1 је рекао:", + "composer.discard": "Желите ли да одбаците ову поруку?", + "composer.submit_and_lock": "Проследи и закључај", + "composer.toggle_dropdown": "Подесите \"Dropdown\"", + "composer.uploading": "Отпремање %1", + "composer.formatting.bold": "Подебљано", + "composer.formatting.italic": "Курзив", + "composer.formatting.list": "Листа", + "composer.formatting.strikethrough": "Прецртано", + "composer.formatting.code": "Код", + "composer.formatting.link": "Веза", + "composer.formatting.picture": "Веза слике", + "composer.upload-picture": "Отпреми слику", + "composer.upload-file": "Отпреми датотеку", + "composer.zen_mode": "Цео екран", + "composer.select_category": "Изаберите категорију", + "composer.textarea.placeholder": "Овде унесите садржај поруке, превуците и отпустите слике", + "composer.schedule-for": "Испланирајте тему за", + "composer.schedule-date": "Датум", + "composer.schedule-time": "Време", + "composer.cancel-scheduling": "Откажи планирање", + "composer.set-schedule-date": "Подеси датум", + "bootbox.ok": "У реду", + "bootbox.cancel": "Откажи", + "bootbox.confirm": "Потврди", + "bootbox.submit": "Проследи", + "bootbox.send": "Пошаљи", + "cover.dragging_title": "Позиционирање насловне фотографије", + "cover.dragging_message": "Повуците насловну фотографију до жељене локације и кликните на \"Сачувај\"", + "cover.saved": "Насловна фотографија и позиција су сачуване", + "thumbs.modal.title": "Управљање сличицама теме", + "thumbs.modal.no-thumbs": "Нема пронађених сличица.", + "thumbs.modal.resize-note": "Напомена: Овај форум је конфигурисан да смањује величину сличица тема на максималну ширину од %1px", + "thumbs.modal.add": "Додај сличицу", + "thumbs.modal.remove": "Уклони сличицу", + "thumbs.modal.confirm-remove": "Да ли сте сигурни да желите да уклоните ову сличицу?" +} \ No newline at end of file diff --git a/public/language/sr/notifications.json b/public/language/sr/notifications.json new file mode 100644 index 0000000000..fe330c996b --- /dev/null +++ b/public/language/sr/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Обавештења", + "no_notifs": "Нема нових обавештења", + "see_all": "Сва обавештења", + "mark_all_read": "Означи све као прочитано", + "back_to_home": "Назад на %1", + "outgoing_link": "Одлазна веза", + "outgoing_link_message": "Сада напуштате %1", + "continue_to": "Продужи на %1", + "return_to": "Врати се на %1", + "new_notification": "Имате ново обавештење", + "you_have_unread_notifications": "Имате непрочитана обавештења.", + "all": "Све", + "topics": "Теме", + "replies": "Одговори", + "chat": "Ћаскања", + "group-chat": "Групна ћаскања", + "follows": "Праћења", + "upvote": "Гласови", + "new-flags": "Нове заставице", + "my-flags": "Заставице додељене мени", + "bans": "Забране", + "new_message_from": "Нова порука од %1", + "upvoted_your_post_in": "%1 је гласао за вашу поруку у %2", + "upvoted_your_post_in_dual": "%1 и %2 осталих су гласали за вашу поруку у %3.", + "upvoted_your_post_in_multiple": "%1 и %2 осталих су гласали за вашу поруку у %3.", + "moved_your_post": "%1 је преместио вашу поруку у %2", + "moved_your_topic": "%1 је преместио %2", + "user_flagged_post_in": "%1 је означио заставицом поруку у %2", + "user_flagged_post_in_dual": "%1 и %2 су означили заставицом поруку у %3", + "user_flagged_post_in_multiple": "%1 и осталих %2 су означили заставицом поруку у %3", + "user_flagged_user": "%1 је означио заставицом кориснички профил (%2)", + "user_flagged_user_dual": "%1 и %2 су означили заставицом кориснички профил (%3)", + "user_flagged_user_multiple": "%1 и %2 осталих су означили заставицом кориснички профил (%3)", + "user_posted_to": "%1 је послао нови одговор на: %2", + "user_posted_to_dual": "%1 и %2 су одговорили на: %3", + "user_posted_to_multiple": "%1 и %2 других су одговорили на: %3", + "user_posted_topic": "%1 је поставио нову тему: %2", + "user_edited_post": "%1 је уредио поруку у %2", + "user_started_following_you": "%1 је почео да вас прати.", + "user_started_following_you_dual": "%1 и %2 су почели да вас прате.", + "user_started_following_you_multiple": "%1 и %2 других су почели да вас прате.", + "new_register": "%1 вам је послао захтев за регистрацију.", + "new_register_multiple": "Постоје %1 захтева за регистрацију који чекају преглед.", + "flag_assigned_to_you": "Заставица %1 је додељена вама", + "post_awaiting_review": "Порука на чекању за преглед", + "profile-exported": "%1 профила извезено, кликните за преузимање", + "posts-exported": "%1 порука извезено, кликните за преузимање", + "uploads-exported": "%1 отпремања извезено, кликните за преузимање", + "users-csv-exported": "Кориснички csv извезен, кликните за преузимање", + "post-queue-accepted": "Ваша порука на чекању је прихваћена. Кликните овде да бисте видели своју поруку.", + "post-queue-rejected": "Ваша порука на чекању је одбијена.", + "post-queue-notify": "Порука на чекању је примила обавештење:
\"%1\"", + "email-confirmed": "Е-пошта је потврђена.", + "email-confirmed-message": "Хвала на овери ваше е-поште. Ваш налог је сада у потпуности активан.", + "email-confirm-error-message": "Дошло је до проблема са овером ваше е-поште. Можда је код неисправан или је истекао.", + "email-confirm-sent": "Е-пошта за потврду је послата.", + "none": "Ниједно", + "notification_only": "Само обавештење", + "email_only": "Само е-пошта", + "notification_and_email": "Обавештење и е-пошта", + "notificationType_upvote": "Када неко гласа за вашу поруку", + "notificationType_new-topic": "Када неко кога пратите постави тему", + "notificationType_new-reply": "Када је објављен нови одговор у теми коју надгледате", + "notificationType_post-edit": "Када је порука уређена у теми коју надгледате", + "notificationType_follow": "Када неко почне да вас прати", + "notificationType_new-chat": "Када примите поруку ћаскања", + "notificationType_new-group-chat": "Када примите поруку ћаскања у групи", + "notificationType_group-invite": "Када примите позивницу за групу", + "notificationType_group-leave": "Када корисник напусти вашу групу", + "notificationType_group-request-membership": "Када неко затражи да се придружи групи коју поседујете", + "notificationType_new-register": "Када је неко додат на чекање за регистрацију", + "notificationType_post-queue": "Када је нова порука на чекању", + "notificationType_new-post-flag": "Када је порука означена заставицом", + "notificationType_new-user-flag": "Када је корисник означен заставицом" +} \ No newline at end of file diff --git a/public/language/sr/pages.json b/public/language/sr/pages.json new file mode 100644 index 0000000000..74d8adb262 --- /dev/null +++ b/public/language/sr/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Матична страница", + "unread": "Непрочитане теме", + "popular-day": "Популарне теме данас", + "popular-week": "Популарне теме ове седмице", + "popular-month": "Популарне теме овог месеца", + "popular-alltime": "Популарне теме свих времена", + "recent": "Недавне теме", + "top-day": "Најгласаније теме данас", + "top-week": "Најгласаније теме ове седмице", + "top-month": "Најгласаније теме овог месеца", + "top-alltime": "Најгласаније теме", + "moderator-tools": "Алати модератора", + "flagged-content": "Садржај означен заставицом", + "ip-blacklist": "Црна листа IP адреса", + "post-queue": "Порука на чекању", + "users/online": "Корисници на мрежи", + "users/latest": "Најновији корисници", + "users/sort-posts": "Корисници са највише порука", + "users/sort-reputation": "Корисници са највећим угледом", + "users/banned": "Забрањени корисници", + "users/most-flags": "Корисници најчешће означени заставицом", + "users/search": "Претрага корисника", + "notifications": "Обавештења", + "tags": "Ознаке", + "tag": "Теме са ознаком "%1"", + "register": "Региструј налог", + "registration-complete": "Регистрација је комплетирана", + "login": "Пријавите се на ваш налог", + "reset": "Поништите лозинку вашег налога", + "categories": "Категорије", + "groups": "Групе", + "group": "%1 група", + "chats": "Ћаскања", + "chat": "Ћаскање са %1", + "flags": "Заставице", + "flag-details": "Означи заставицом %1 детаље", + "account/edit": "Уређивање \"%1\"", + "account/edit/password": "Уређивање лозинке од \"%1\"", + "account/edit/username": "Уређивање корисничког имена од \"%1\"", + "account/edit/email": "Уређивање е-поште од \"%1\"", + "account/info": "Информације о налогу", + "account/following": "Особе које %1 прати", + "account/followers": "Особе које прате %1", + "account/posts": "Поруке од %1", + "account/latest-posts": "Најновије поруке од %1", + "account/topics": "Теме од %1", + "account/groups": "Групе корисника %1", + "account/watched_categories": "Надгледане категорије корисника %1", + "account/bookmarks": "Омиљене поруке корисника $1", + "account/settings": "Корисничка подешавања", + "account/watched": "Теме које надгледа %1", + "account/ignored": "Теме које игнорише %1", + "account/upvoted": "Поруке које је гласао %1", + "account/downvoted": "Поруке које је негативно гласао %1", + "account/best": "Најбоље поруке од %1", + "account/controversial": "Спорне поруке од %1", + "account/blocks": "Корисници које је блокирао %1", + "account/uploads": "Отпремио %1", + "account/sessions": "Сесије пријављивања", + "confirm": "Е-пошта је потврђена.", + "maintenance.text": "%1 је тренутно у фази одржавања. Молимо, навратите касније.", + "maintenance.messageIntro": "Додатно, администратор је оставио ову поруку:", + "throttled.text": "%1 је тренутно недоступан због прекомерног оптерећења. Молимо, навратите касније." +} \ No newline at end of file diff --git a/public/language/sr/post-queue.json b/public/language/sr/post-queue.json new file mode 100644 index 0000000000..39b0084310 --- /dev/null +++ b/public/language/sr/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Поруке на чекању", + "description": "Ннема порука на чекању.
Да бисте омогућили ову функцију, идите у ПОдешавања → Поруке → Поруке на чекању и омогућите Поруке на чекању.", + "user": "Корисник", + "category": "Категорија", + "title": "Наслов", + "content": "Садржај", + "posted": "Објављено", + "reply-to": "Одговори на \"%1\"", + "content-editable": "Кликните на садржај да бисте уредили", + "category-editable": "Кликните на категорију да бисте уредили", + "title-editable": "Кликните на наслов да бисте уредили", + "reply": "Одговори", + "topic": "Тема", + "accept": "Прихвати", + "reject": "Одбиј", + "remove": "Уклони", + "notify": "Обавештење", + "notify-user": "Обавести корисника", + "confirm-reject": "Да ли желите да одбаците ову поруку?", + "bulk-actions": "Масовне радње", + "accept-all": "Прихвати све", + "accept-selected": "Прихвати изабрано", + "reject-all": "Одбаци све", + "reject-all-confirm": "Да ли желите да одбаците све поруке?", + "reject-selected": "Одбаци изабрано", + "reject-selected-confirm": "Да ли желите да одбаците изабране поруке (укупно %1)?", + "bulk-accept-success": "Прихваћених порука: %1", + "bulk-reject-success": "Одбачених порука: %1" +} \ No newline at end of file diff --git a/public/language/sr/recent.json b/public/language/sr/recent.json new file mode 100644 index 0000000000..3e39d9d0c4 --- /dev/null +++ b/public/language/sr/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Недавно", + "day": "Дан", + "week": "Седмица", + "month": "Месец", + "year": "Година", + "alltime": "Одувек", + "no_recent_topics": "Нема недавних тема.", + "no_popular_topics": "Нема популарних тема.", + "there-is-a-new-topic": "Постоји нова тема.", + "there-is-a-new-topic-and-a-new-post": "Постоје нова тема и нова порука.", + "there-is-a-new-topic-and-new-posts": "Постоји нова тема и укупно нових порука: %1.", + "there-are-new-topics": "Број нових тема: %1.", + "there-are-new-topics-and-a-new-post": "Постоји нова порука и укупно нових тема: %1.", + "there-are-new-topics-and-new-posts": "Број нових тема: %1 и нових порука: %2.", + "there-is-a-new-post": "Постоји нова порука.", + "there-are-new-posts": "Број нових порука: %1.", + "click-here-to-reload": "Кликните овде за поновно учитавање." +} \ No newline at end of file diff --git a/public/language/sr/register.json b/public/language/sr/register.json new file mode 100644 index 0000000000..4a1581b20c --- /dev/null +++ b/public/language/sr/register.json @@ -0,0 +1,32 @@ +{ + "register": "Регистрација", + "cancel_registration": "Откажи регистрацију", + "help.email": "Ваша е-пошта ће подразумевано бити скривена од јавности.", + "help.username_restrictions": "Јединствено корисничко име са %1 до %2 знакова. Остали вас могу спомињати путем @корисничко име.", + "help.minimum_password_length": "Ваша лозинке мора имати најмање %1 знакова.", + "email_address": "Адреса е-поште", + "email_address_placeholder": "Унесите адресу е-поште", + "username": "Корисничко име", + "username_placeholder": "Унесите корисничко име", + "password": "Лозинка", + "password_placeholder": "Унесите лознку", + "confirm_password": "Потврда лозинке", + "confirm_password_placeholder": "Потврдите лозинку", + "register_now_button": "Региструјте се", + "alternative_registration": "Алтернативно регистровање", + "terms_of_use": "Услови коришћења", + "agree_to_terms_of_use": "Слажем се са условима коришћења", + "terms_of_use_error": "Морате се сложити са условима коришћења", + "registration-added-to-queue": "Ваша регистрација је додата у ред одобравања. Добићете е-пошту када администратор прихвати вашу регистрацију.", + "registration-queue-average-time": "Наше просечно време за одобравање чланства је %1 сата/и и %2 минут/а.", + "registration-queue-auto-approve-time": "Ваше чланство на овом форуму биће у потпуности активирано за %1 сата/и.", + "interstitial.intro": "Желели бисмо неке додатне информације како бисмо ажурирали ваш налог…", + "interstitial.intro-new": "Желели бисмо неке додатне информације пре него што отворимо ваш налог…", + "interstitial.errors-found": "Прегледајте унете информације:", + "gdpr_agree_data": "Пристајем на прикупљање и обраду мојих личних података на овој веб страници.", + "gdpr_agree_email": "Пристајем на примање сажетака и обавештења путем е-поште са ове веб странице.", + "gdpr_consent_denied": "Морате дати пристанак овом сајту да прикупља/обрађује ваше информације и да вам шаље е-пошту.", + "invite.error-admin-only": "Директна регистрација корисника је онемогућена. За више детаља контактирајте администратора.", + "invite.error-invite-only": "Директна регистрација корисника је онемогућена. Да бисте приступили овом форуму, мора вас позвати постојећи корисник.", + "invite.error-invalid-data": "Примљени подаци о регистрацији не одговарају нашој евиденцији. За више детаља контактирајте администратора" +} \ No newline at end of file diff --git a/public/language/sr/reset_password.json b/public/language/sr/reset_password.json new file mode 100644 index 0000000000..71aa267e52 --- /dev/null +++ b/public/language/sr/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Ресетовање лозинке", + "update_password": "Ажурурај лозинку", + "password_changed.title": "Лозинка је промењена", + "password_changed.message": "

Лозинка је успешно ресетована, молимо да се поново пријавите.", + "wrong_reset_code.title": "Неисправан код за ресетовање", + "wrong_reset_code.message": "Примљени код за ресетовање је неисправан. Пробајте поново, или затражите нови код за ресетовањe.", + "new_password": "Нова лозинка", + "repeat_password": "Потврда нове лозинке", + "changing_password": "Мењање лозинке", + "enter_email": "Унесите вашу адресу е-поште и послаћемо вам писмо за упутством за ресетовање налога.", + "enter_email_address": "Унесите адресу е-поште", + "password_reset_sent": "Е-пошта за поништавање лозинке је послата ако наведена адреса одговара постојећем корисничком налогу. Имајте на уму да ће само једна е-пошта бити послата по минуту.", + "invalid_email": "Неисправна е-пошта / е-пошта не постоји!", + "password_too_short": "Унета лозинка је прекратка, молимо изаберите другу лозинку.", + "passwords_do_not_match": "Унете лозинке се не подударају.", + "password_expired": "Ваша лозинка је истекла, молимо изаберите нову" +} \ No newline at end of file diff --git a/public/language/sr/search.json b/public/language/sr/search.json new file mode 100644 index 0000000000..16e14485de --- /dev/null +++ b/public/language/sr/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 резултат(а) се подудара са „%2“, (%3 секунди)", + "no-matches": "Нема подударања", + "advanced-search": "Напредна претрага", + "in": "У", + "titles": "Наслови", + "titles-posts": "Наслови и поруке", + "match-words": "Речи које се подударају", + "all": "Све", + "any": "Било која", + "posted-by": "Објавио", + "in-categories": "У категоријама", + "search-child-categories": "Претражи поткатегорије", + "has-tags": "Има ознаке", + "reply-count": "Број одговора", + "at-least": "Најмање", + "at-most": "Највише", + "relevance": "Важности", + "post-time": "Времену објаве", + "votes": "Гласовима", + "newer-than": "Новије од", + "older-than": "Старије од", + "any-date": "Било који датум", + "yesterday": "Јуче", + "one-week": "Једне седмице", + "two-weeks": "Две седмице", + "one-month": "Једног месеца", + "three-months": "Три месеца", + "six-months": "Шест месеци", + "one-year": "Једне године", + "sort-by": "Сортирај по", + "last-reply-time": "Времену последњег одговора", + "topic-title": "Наслову теме", + "topic-votes": "Гласовима теме", + "number-of-replies": "Броју одговора", + "number-of-views": "Броју прегледа", + "topic-start-date": "Датуму настанка теме", + "username": "Корисничком имену", + "category": "Категорији", + "descending": "У опадајућем низу", + "ascending": "У растућем низу", + "save-preferences": "Сачувај поставке", + "clear-preferences": "Обриши поставке", + "search-preferences-saved": "Поставке претраге су сачуване", + "search-preferences-cleared": "Поставке претраге су обрисане", + "show-results-as": "Прикажи резултате као", + "see-more-results": "Прикажи више резултата (%1)", + "search-in-category": "Претражи у „%1\"" +} \ No newline at end of file diff --git a/public/language/sr/success.json b/public/language/sr/success.json new file mode 100644 index 0000000000..d47cdfbaf3 --- /dev/null +++ b/public/language/sr/success.json @@ -0,0 +1,7 @@ +{ + "success": "Успешно", + "topic-post": "Успешно сте послали поруку.", + "post-queued": "Ваша порука је на чекању за одобрење. Добићете обавештење када буде прихваћена или одбијена.", + "authentication-successful": "Успешна аутентификација", + "settings-saved": "Подешавања су сачувана!" +} \ No newline at end of file diff --git a/public/language/sr/tags.json b/public/language/sr/tags.json new file mode 100644 index 0000000000..fb7273af58 --- /dev/null +++ b/public/language/sr/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Нема тема са овом ознаком.", + "tags": "Ознаке", + "enter_tags_here": "Овде унесите ознаке, од %1 до %2 знакова за сваку.", + "enter_tags_here_short": "Унесите ознаке...", + "no_tags": "Још увек нема ознака.", + "select_tags": "Изабери ознаке" +} \ No newline at end of file diff --git a/public/language/sr/top.json b/public/language/sr/top.json new file mode 100644 index 0000000000..eeaf91d919 --- /dev/null +++ b/public/language/sr/top.json @@ -0,0 +1,4 @@ +{ + "title": "Најпопуларније", + "no_top_topics": "Нема популарних тема" +} \ No newline at end of file diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json new file mode 100644 index 0000000000..a8f424d36c --- /dev/null +++ b/public/language/sr/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Тема", + "title": "Наслов", + "no_topics_found": "Нема пронађених тема!", + "no_posts_found": "Нису пронађене поруке!", + "post_is_deleted": "Ова порука је избрисана!", + "topic_is_deleted": "Ова тема је избрисана!", + "profile": "Профил", + "posted_by": "Поставио %1", + "posted_by_guest": "Поставио гост", + "chat": "Ћаскање", + "notify_me": "Будите обавештени о новим порукама у овој теми", + "quote": "Цитирај", + "reply": "Одговори", + "replies_to_this_post": "Одговора: %1", + "one_reply_to_this_post": "1 одговор", + "last_reply_time": "Последњи одговор", + "reply-as-topic": "Објави одговор као тему", + "guest-login-reply": "Пријавите се да бисте одговорили", + "login-to-view": "🔒 Пријавите се да бисте прегледали", + "edit": "Уреди", + "delete": "Избриши", + "delete-event": "Избриши догађај", + "delete-event-confirm": "Да ли сте сигурни да желите да избришете овај догађај?", + "purge": "Очисти", + "restore": "Обнови", + "move": "Премести", + "change-owner": "Промени власника", + "fork": "Рачвање", + "link": "Веза", + "share": "Дели", + "tools": "Алатке", + "locked": "Закључано", + "pinned": "Закачено", + "pinned-with-expiry": "Закачено до %1", + "scheduled": "Планирано", + "moved": "Премештено", + "moved-from": "Премештено из %1", + "copy-ip": "Копирај IP", + "ban-ip": "Бануј IP", + "view-history": "Уреди историју", + "locked-by": "Закључао", + "unlocked-by": "Откључао", + "pinned-by": "Закачио", + "unpinned-by": "Откачио", + "deleted-by": "Избрисао", + "restored-by": "Вратио", + "moved-from-by": "Из %1 преместио", + "queued-by": "Порука је на чекању за одобрење →", + "backlink": "Упутио", + "forked-by": "Рачвао", + "bookmark_instructions": "Кликните овде за повратак на последњу прочитану поруку у овој теми.", + "flag-post": "Означи поруку заставицом", + "flag-user": "Означи корисника заставицом", + "already-flagged": "Већ је означено заставицом", + "view-flag-report": "Погледај извештај о заставици", + "resolve-flag": "Реши заставицу", + "merged_message": "Ова тема је обједињена у %2", + "deleted_message": "Ова тема је избрисана. Само корисници са привилегијама управљања темама је могу видети.", + "following_topic.message": "Од сада ће те примати обавештења када неко одговори у овој теми.", + "not_following_topic.message": "Видећете ову тему у списку непрочитаних тема али нећете примати обавештења када неко одговори у њој.", + "ignoring_topic.message": "Више нећете видети ову тему у списку непрочитаних тема. Бићете обавештени када вас неко спомене или када неко гласа за вашу поруку.", + "login_to_subscribe": "Региструјте се или се пријавите за праћење ове теме.", + "markAsUnreadForAll.success": "Тема је свима означена као непрочитана.", + "mark_unread": "Означи као непрочитано", + "mark_unread.success": "Тема је означена као непрочитана", + "watch": "Надгледај", + "unwatch": "Не надгледај", + "watch.title": "Будите обавештени о новим одговорима у овој теми", + "unwatch.title": "Заустави надгледање ове теме", + "share_this_post": "Дели ову поруку", + "watching": "Надгледај", + "not-watching": "Не надгледај", + "ignoring": "Игнориши", + "watching.description": "Обавести ме о новим одговорима.
Прикажи тему у непрочитаним", + "not-watching.description": "Немој ме обавештавати о новим одговорима.
Прикажи тему у непрочитаним ако категорија није игнорисана.", + "ignoring.description": "Немој ме обавештавати о новим одговорима.
Не приказуј тему у непрочитаним", + "thread_tools.title": "Алати теме", + "thread_tools.markAsUnreadForAll": "Означи као непрочитано за све", + "thread_tools.pin": "Закачи тему", + "thread_tools.unpin": "Откачи тему", + "thread_tools.lock": "Закључај тему", + "thread_tools.unlock": "Откључај тему", + "thread_tools.move": "Премести тему", + "thread_tools.move-posts": "Премести поруке", + "thread_tools.move_all": "Премести све", + "thread_tools.change_owner": "Промени власника", + "thread_tools.select_category": "Изаберите категорију", + "thread_tools.fork": "Рачвај тему", + "thread_tools.delete": "Избриши тему", + "thread_tools.delete-posts": "Избриши поруку", + "thread_tools.delete_confirm": "Да ли сте сигурни да желите да избришете ову тему?", + "thread_tools.restore": "Обнови тему", + "thread_tools.restore_confirm": "Да ли сте сигурни да желите да обновите ову тему?", + "thread_tools.purge": "Очисти тему", + "thread_tools.purge_confirm": "Да ли сте сигурни да желите да очистите ову тему?", + "thread_tools.merge_topics": "Споји теме", + "thread_tools.merge": "Споји", + "topic_move_success": "Ова тема ће ускоро бити премештена у „%1“. Кликните овде да бисте опозвали.", + "topic_move_multiple_success": "Ове теме ће ускоро бити премештене у „%1“. Кликните овде да бисте опозвали.", + "topic_move_all_success": "Све теме ће ускоро бити премештене у „%1“. Кликните овде да бисте опозвали.", + "topic_move_undone": "Премештање теме је опозвано", + "topic_move_posts_success": "Поруке ће бити премештене ускоро. Кликните овде да бисте опозвали.", + "topic_move_posts_undone": "Премештање поруке је опозвано", + "post_delete_confirm": "Да ли сте сигурни да желите да избришете ову поруку?", + "post_restore_confirm": "Да ли сте сигурни да желите да обновите ову поруку?", + "post_purge_confirm": "Да ли сте сигурни да желите да очистите овај пост?", + "pin-modal-expiry": "Датум истека", + "pin-modal-help": "Овде можете по жељи да одредите датум истека закачених тема. Можете и да ово поље оставите празно да би тема остала закачена док се ручно не откачи.", + "load_categories": "Учитавање категорија", + "confirm_move": "Премести", + "confirm_fork": "Рачвај", + "bookmark": "Обележивач", + "bookmarks": "Обележивачи", + "bookmarks.has_no_bookmarks": "Нисте додали ниједну поруку у обележиваче", + "copy-permalink": "Копирај трајну везу", + "loading_more_posts": "Учитавање још порука", + "move_topic": "Премести тему", + "move_topics": "Премести теме", + "move_post": "Премести поруку", + "post_moved": "Порука је премештена!", + "fork_topic": "Рачвај тему", + "enter-new-topic-title": "Унесите нови наслов теме", + "fork_topic_instruction": "Кликните на поруке које желите да рачвате", + "fork_no_pids": "Нема одабраних порука!", + "no-posts-selected": "Нема одабраних порука!", + "x-posts-selected": "Одабрано порука: %1", + "x-posts-will-be-moved-to-y": "%1 поруке/а ће бити премештено у „%2“", + "fork_pid_count": "Одабрано порука: %1", + "fork_success": "Тема је успешно рачвана! Кликните овде за одлазак на рачвану тему.", + "delete_posts_instruction": "Кликните на поруке које желите да избришете/очистите", + "merge_topics_instruction": "Кликните на теме које желите да спојите или претражите", + "merge-topic-list-title": "Списак тема за спајање", + "merge-options": "Опције спајања", + "merge-select-main-topic": "Изаберите главну тему", + "merge-new-title-for-topic": "Нови наслов теме", + "topic-id": "ID теме", + "move_posts_instruction": "Кликните на поруке које желите да преместите, а затим унесите ID теме или идите на циљну тему", + "change_owner_instruction": "Кликните на поруке које желите да доделите другом кориснику", + "composer.title_placeholder": "Овде унесите наслов теме...", + "composer.handle_placeholder": "Унесите ваше име/идентитет овде", + "composer.discard": "Одбаци", + "composer.submit": "Проследи", + "composer.additional-options": "Додатне опције", + "composer.schedule": "Испланирај", + "composer.replying_to": "Писање одговора на %1", + "composer.new_topic": "Нова тема", + "composer.editing": "Уређивање", + "composer.uploading": "отпремање...", + "composer.thumb_url_label": "Налепи адресу сличице теме", + "composer.thumb_title": "Додај сличицу овој теми", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Или отпреми датотеку", + "composer.thumb_remove": "Обриши поља", + "composer.drag_and_drop_images": "Превуците и отпустите слике овде", + "more_users_and_guests": "још %1 корисник/а и %2 гост/а", + "more_users": "још %1 корисник/а", + "more_guests": "још %1 гост/а", + "users_and_others": "%1 и %2 осталих", + "sort_by": "Сортирај", + "oldest_to_newest": "Од старијих ка новијим", + "newest_to_oldest": "Од новијих ка старијим", + "most_votes": "Највише гласова", + "most_posts": "Највише порука", + "most_views": "Највише прегледа", + "stale.title": "Креирати нову тему уместо тога?", + "stale.warning": "Тема у којој желите да одговорите је сувише стара. Да ли желите да уместо тога креирате нову тему и упутите на ову у вашем одговору?", + "stale.create": "Креирај нову тему", + "stale.reply_anyway": "Одговори на ову тему у сваком случају", + "link_back": "Re: [%1](%2)", + "diffs.title": "Историја уређивања поруке", + "diffs.description": "Ова порука има %1 корекција. Кликните на једну од корекција да бисте видели садржај поруке у том тренутку.", + "diffs.no-revisions-description": "Ова порука има %1 корекција.", + "diffs.current-revision": "тренутна корекција", + "diffs.original-revision": "оригинална корекција", + "diffs.restore": "Врати корекцију", + "diffs.restore-description": "Нова корекција ће бити додана у историју уређивања овог поста након враћања.", + "diffs.post-restored": "Порука је успешно враћена на ранију корекцију", + "diffs.delete": "Избриши ову корекцију", + "diffs.deleted": "Корекција је избрисана", + "timeago_later": "%1 касније", + "timeago_earlier": "%1 раније", + "first-post": "Прва порука", + "last-post": "Последња порука", + "go-to-my-next-post": "Иди на моју следећу поруку", + "no-more-next-post": "Немате више порука у овој теми", + "post-quick-reply": "Објави брзи одговор" +} \ No newline at end of file diff --git a/public/language/sr/unread.json b/public/language/sr/unread.json new file mode 100644 index 0000000000..b2788e1191 --- /dev/null +++ b/public/language/sr/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Непрочитано", + "no_unread_topics": "Нема непрочитаних тема.", + "load_more": "Учитај више", + "mark_as_read": "Означи као прочитано", + "selected": "Изабране", + "all": "Све", + "all_categories": "Све категорије", + "topics_marked_as_read.success": "Теме су означене као прочитане!", + "all-topics": "Све теме", + "new-topics": "Нове теме", + "watched-topics": "Надгледане теме", + "unreplied-topics": "Неодговорене теме", + "multiple-categories-selected": "Вишеструко изабране" +} \ No newline at end of file diff --git a/public/language/sr/uploads.json b/public/language/sr/uploads.json new file mode 100644 index 0000000000..5619438c37 --- /dev/null +++ b/public/language/sr/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Отпремање датотеке...", + "select-file-to-upload": "Изаберите датотеку за отпремање!", + "upload-success": "Датотека је успешно отпремљена!", + "maximum-file-size": "Највише %1 kb", + "no-uploads-found": "Нема пронађених отпремања", + "public-uploads-info": "Отпремања су јавна, сви посетиоци их могу видети.", + "private-uploads-info": "Отпремања си приватна, само пријављени корисници их могу видети." +} \ No newline at end of file diff --git a/public/language/sr/user.json b/public/language/sr/user.json new file mode 100644 index 0000000000..6c2bca7c1f --- /dev/null +++ b/public/language/sr/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Забрањен", + "muted": "Привремено искључен", + "offline": "Ван мреже", + "deleted": "Избрисано", + "username": "Корисничко име", + "joindate": "Датум регистрације", + "postcount": "Број порука", + "email": "Е-пошта", + "confirm_email": "Потврда е-поште", + "account_info": "Информације о налогу", + "admin_actions_label": "Административне радње", + "ban_account": "Забрани налог", + "ban_account_confirm": "Да ли заиста желите да забраните овог корисника?", + "unban_account": "Скини забрану налогу", + "mute_account": "Привремено искључи налог", + "unmute_account": "Поново укључи налог", + "delete_account": "Брисање налога", + "delete_account_as_admin": "Избриши налог", + "delete_content": "Избриши садржај налога", + "delete_all": "Избриши налог и садржај", + "delete_account_confirm": "Да ли сте сигурни да желите да анонимизујете ваше поруке и избришете свој налог?
Ова радња је неповратна и нећете моћи да вратите ваше податке

Унесите вашу лозинку да бисте потврдили да желите да уништите овај налог.", + "delete_this_account_confirm": "Да ли сте сигурни да желите да избришете овај налог остављајући његов садржај?
Ова радња је неповратна, поруке ће бити анонимизоване и нећете моћи да вратите везу порука са избрисаним налогом

", + "delete_account_content_confirm": "Да ли заиста желите да избришете садржај овог налога (поруке/теме/отпремања)?
Ова радња је неповратна и нећете моћи да вратите било који податак

", + "delete_all_confirm": "Да ли заиста желите да избришете овај налог и сав његов садржај (поруке/теме/отпремања)?
Ова радња је неповратна и нећете моћи да вратите било који податак

", + "account-deleted": "Налог је избрисан", + "account-content-deleted": "Садржај налога је избрисан", + "fullname": "Пуно име", + "website": "Веб сајт", + "location": "Локација", + "age": "Старост", + "joined": "Придружио се", + "lastonline": "Последњи пут на мрежи", + "profile": "Профил", + "profile_views": "Прегледи профила", + "reputation": "Репутација", + "bookmarks": " Обележивачи", + "watched_categories": "Надгледане категорије", + "change_all": "Промени све", + "watched": "Надгледано", + "ignored": "Игнорисано", + "default-category-watch-state": "Подразумевано стање надгледања категорија", + "followers": "Пратиоци", + "following": "Праћења", + "blocks": "Блокирања", + "block_toggle": "Блокирај/одблокирај", + "block_user": "Блокирај корисника", + "unblock_user": "Одблокирај корисника", + "aboutme": "О мени", + "signature": "Потпис", + "birthday": "Рођендан", + "chat": "Ђаскање", + "chat_with": "Ћаскај са %1", + "new_chat_with": "Започни ново ћаскање са %1", + "flag-profile": "Означи профил заставицом", + "follow": "Прати", + "unfollow": "Не прати", + "more": "Више", + "profile_update_success": "Профил је успешно ажуриран!", + "change_picture": "Промена слике", + "change_username": "Промена корисничког имена", + "change_email": "Промена е-поште", + "email_same_as_password": "Унесите тренутну лозинку за наставак; поново сте унели нову е-пошту", + "edit": "Уреди", + "edit-profile": "Уреди профил", + "default_picture": "Подразумевана икона", + "uploaded_picture": "Отпремљена слика", + "upload_new_picture": "Отпреми нову слику", + "upload_new_picture_from_url": "Отпреми нову слику са адресе", + "current_password": "Тренутна лозинка", + "change_password": "Промена лозинке", + "change_password_error": "Неисправна лозинка", + "change_password_error_wrong_current": "Ваша тренутна лозинка није исправна!", + "change_password_error_match": "Лозинке се морају подударати!", + "change_password_error_privileges": "Немате дозволу за мењање ове лозинке.", + "change_password_success": "Ваша лозинка је ажурирана!", + "confirm_password": "Потврда лозинке", + "password": "Лозинка", + "username_taken_workaround": "Корисничко име које сте захтевали је већ заузето па смо је мало изменили. Сада сте знани као %1", + "password_same_as_username": "Ваша лозинка је иста као ваше име, изаберите другу лозинку", + "password_same_as_email": "Ваша лозинка је иста као ваша е-пошта, изаберите другу лозинку", + "weak_password": "Лозинка је слаба", + "upload_picture": "Отпремање слике", + "upload_a_picture": "Отпреми слику", + "remove_uploaded_picture": "Уклоните отпремљену слику", + "upload_cover_picture": "Отпреми насловну слику", + "remove_cover_picture_confirm": "Да ли сте сигурни да желите да уклоните насловну слику?", + "crop_picture": "Изрежи слику", + "upload_cropped_picture": "Изрежи и отпреми", + "avatar-background-colour": "Боја позадине аватара", + "settings": "Подешавања", + "show_email": "Прикажи моју лозинку", + "show_fullname": "Прикажи моје пуно име", + "restrict_chats": "Дозволи поруке ћаскања само од корисника које пратим", + "digest_label": "Пријава за сажетак", + "digest_description": "Пријавите се за праћење ажурирања форума (нова обавештења и теме) путем е-поште према одређеном распореду", + "digest_off": "Искључено", + "digest_daily": "Дневно", + "digest_weekly": "Седмично", + "digest_biweekly": "Двоседмично", + "digest_monthly": "Месечно", + "has_no_follower": "Овај корисник нема пратиоце :(", + "follows_no_one": "Овај корисник не прати никога :(", + "has_no_posts": "Овај корисник још ништа није објавио. ", + "has_no_best_posts": "Овај корисник још увек нема ниједну поруку за коју се гласало.", + "has_no_topics": "Овај корисник још није покренуо ниједну тему.", + "has_no_watched_topics": "Овај корисник још не надгледа ниједну тему.", + "has_no_ignored_topics": "Овај корисник још није игнорисао ниједну тему.", + "has_no_upvoted_posts": "Овај корисник још увек није гласао за неку поруку.", + "has_no_downvoted_posts": "Овај корисник још увек није негативно гласао за неку поруку.", + "has_no_controversial_posts": "Овај корисник још увек нема ниједну поруку за коју се негативно гласало.", + "has_no_blocks": "Нисте блокирали ниједног корисника", + "email_hidden": "Скривена е-пошта", + "hidden": "скривена", + "paginate_description": "Нумериши теме и странице уместо бесконачног скроловања", + "topics_per_page": "Тема по страници", + "posts_per_page": "Порука по страници", + "max_items_per_page": "Највише %1", + "acp_language": "Језик странице администратора", + "notifications": "Обавештења", + "upvote-notif-freq": "Учесталост обавештења о гласовима", + "upvote-notif-freq.all": "На сваки глас", + "upvote-notif-freq.first": "Прво по поруци", + "upvote-notif-freq.everyTen": "На сваких десет гласова", + "upvote-notif-freq.threshold": "На 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "На 10, 100, 1000...", + "upvote-notif-freq.disabled": "Онемогућено", + "browsing": "Подешавање прегледања", + "open_links_in_new_tab": "Отвори одлазне везе у новој картици", + "enable_topic_searching": "Омогући претрагу унутар тема", + "topic_search_help": "Ако је омогућено, претраживање унутар тема ће прегазити подразумевано понашање претраге страница и омогућити претрагу целе теме уместо само оног што је приказано на екрану", + "update_url_with_post_index": "Ажурирај адресу индексом порука при прегледању тема", + "scroll_to_my_post": "Након објављивања одговора, прикажи нову поруку", + "follow_topics_you_reply_to": "Надгледај теме у којима си одговорио", + "follow_topics_you_create": "Надгледај теме које си креирао", + "grouptitle": "Назив групе", + "group-order-help": "Изаберите групу и користите стрелице за промену редоследа", + "no-group-title": "Без назива групе", + "select-skin": "Изаберите маску", + "select-homepage": "Изаберите матичну страницу", + "homepage": "Матична страница", + "homepage_description": "Изаберите страницу која ће се користити као матична страница форума или „None“ да би се користила подразумевана почетна страница.", + "custom_route": "Прилагођена путања матичне странице", + "custom_route_help": "Овде унесите назив путање, без икакве претходне косе црте (нпр. „недавно\" или „категорија/2/општа-дискусија\")", + "sso.title": "Једноструки Sign-on сервиси", + "sso.associated": "Повезано са", + "sso.not-associated": "Кликните овде за повезивање са", + "sso.dissociate": "Одвоји", + "sso.dissociate-confirm-title": "Потврди одвајање", + "sso.dissociate-confirm": "Да ли сте сигурни да желите да одвојите овај налог од %1?", + "info.latest-flags": "Најновији означени заставицом", + "info.no-flags": "Нема пронађених порука означених заставицом", + "info.ban-history": "Историја недавно забрањених налога", + "info.no-ban-history": "Овај корисник никада није био забрањен", + "info.banned-until": "Забрањен до %1", + "info.banned-expiry": "Истиче", + "info.banned-permanently": "Забрањен трајно", + "info.banned-reason-label": "Разлог", + "info.banned-no-reason": "Није дат разлог.", + "info.mute-history": "Историја недавно искључених налога", + "info.no-mute-history": "Овај корисник никада није био привремено искључен", + "info.muted-until": "Привремено искључен до %1", + "info.muted-expiry": "Истиче", + "info.muted-no-reason": "Није наведен разлог.", + "info.username-history": "Историја корисничког имена", + "info.email-history": "Историја е-поште", + "info.moderation-note": "Белешка модерације", + "info.moderation-note.success": "Белешка модерације је сачувана", + "info.moderation-note.add": "Додај белешку", + "sessions.description": "Ова страница вам омогућује да прегледате активне сесије на овом форуму и опозовете их ако је потребно. Можете опозвати своју сесију тако што ћете се одјавити са вашег налога.", + "consent.title": "Ваша права и сагласност", + "consent.lead": "Овај форум прикупља и обрађује ваше личне информације.", + "consent.intro": "Ове информације користимо стриктно за персонализовње вашег искуства у овој заједници, као и за повезивање порука које уносите са својим корисничким налогом. Током регистрационог корака од вас је тражено да наведете корисничко име и адресу е-поште, такође можете опционо пружити додатне информације да бисте комплетирали свој кориснички профил на овом веб сајту.

Ми задржавамо ове информације током трајања вашег корисничког налога, а ви сте у могућности повући сагласност у било ком тренутку брисањем вашег налога. У било ком тренутку можете затражити копију вашег доприноса овом веб сајту путем странице ваших права и сагласности.

Ако имате било каквих питања или проблема, саветујемо вас да контактирате административни тим овог форума.", + "consent.email_intro": "Повремено вам можемо слати е-пошту на вашу регистровану адресу е-поште како бисмо вам обезбедили ажурирања и/или обавестили о новој активности која је значајна за вас. Можете прилагодити учесталост примања сажетка заједницe (укључујући онемогућавање истог), као и да изаберете које врсте обавештења да добијате путем е-поште, преко странице са корисничким подешавањима.", + "consent.digest_frequency": "Ако није експлицитно промењено у вашим корисничким подешавањима, ова заједница испоручује сажетак е-поштом на сваких %1.", + "consent.digest_off": "Ако није експлицитно промењено у вашим корисничким подешавањима, ова заједница не шаље сажетак е-поштом", + "consent.received": "Дали сте сагласност да овај веб сајт прикупља и обрађује ваше податке. Нису потребне додатне радње.", + "consent.not_received": "Нисте дали сагласност за прикупљање и обраду података. У било које време администрација овог веб сајта може изабрати да избрише ваш налог како би постао усклађен са Уредбом о општој заштити података.", + "consent.give": "Дајте сагласност", + "consent.right_of_access": "Имате право приступа", + "consent.right_of_access_description": "Имате право приступа на захтев свим подацима прикупљеним од стране овог веб сајта. Можете преузети копију ових података кликом на одговарајуће дугме испод.", + "consent.right_to_rectification": "Имате право на исправку", + "consent.right_to_rectification_description": "Имате право на измену или ажурирање свих нетачних података који су нам достављени. Ваш профил се може ажурирати уређивањем вашег профила и објављени садржај се увек можете уредити. Ако то није случај, молимо контактирајте администрацију овог сајта.", + "consent.right_to_erasure": "Имате право на брисање", + "consent.right_to_erasure_description": "У било које време, у могућности сте да опозовете вашу сагласност за прикупљање и/или обраду података брисањем вашег налога. Ваш појединачни профил може бити избрисан, иако ће ваш објављени садржај остати. Ако желите да избришете и свој налог и садржај, молимо контактирајте администрацију овог веб сајта.", + "consent.right_to_data_portability": "Имате право на преносивост података", + "consent.right_to_data_portability_description": "Можете тражити од нас машински читљив извоз прикупљених података о вама и вашем налогу. То можете урадити кликом на одговарајуће дугме испод.", + "consent.export_profile": "Извези профил (.json)", + "consent.export-profile-success": "Извоз профила, добићете обавештење након завршетка.", + "consent.export_uploads": "Извези отпремљени садржај (.zip)", + "consent.export-uploads-success": "Извоз отпремања, добићете обавештење након завршетка.", + "consent.export_posts": "Извези поруке (.csv)", + "consent.export-posts-success": "Извоз порука, добићете обавештење након завршетка.", + "emailUpdate.intro": "Унесите своју адресу е-поште испод. Овај форум користи вашу адресу е-поште за планирано слање сажетка и обавештења, као и за опоравак налога у случају изгубљене лозинке.", + "emailUpdate.optional": "Ово поље је опционо. Нисте обавезни да наведете своју адресу е-поште, али без ваљане е-поште нећете моћи да вратите свој налог или да се пријавите помоћу своје е-поште.", + "emailUpdate.required": "Ово поље је обавезно.", + "emailUpdate.change-instructions": "На унету адресу е-поште биће послата потврдна порука са јединственом везом. Приступ тој вези потврдиће ваше власништво над адресом е-поште и она ће постати активна на вашем налогу. У било ком тренутку можете да ажурирате своју е-пошту на страници налога.", + "emailUpdate.password-challenge": "Унесите лозинку да бисте потврдили власништво над налогом." +} \ No newline at end of file diff --git a/public/language/sr/users.json b/public/language/sr/users.json new file mode 100644 index 0000000000..96f579342d --- /dev/null +++ b/public/language/sr/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Најновији корисници", + "top_posters": "Највише порука", + "most_reputation": "Највећи углед", + "most_flags": "Најчешће означени заставицом", + "search": "Претрага", + "enter_username": "Унесите корисничко име за претрагу", + "search-user-for-chat": "Претражите корисника да бисте започели ћаскање", + "load_more": "Учитај више", + "users-found-search-took": "Нађено је %1 корисника! Претрага је завршена за %2 секунде.", + "filter-by": "Филтрирај према", + "online-only": "Само корисници на мрежи", + "invite": "Позови", + "prompt-email": "Е-поштe:", + "groups-to-join": "Групе којима ће се придружити када се прихвати позив:", + "invitation-email-sent": "Е-пошта са позивом је послата на %1", + "user_list": "Листа корисника", + "recent_topics": "Недавне теме", + "popular_topics": "Популарне теме", + "unread_topics": "Непрочитане теме", + "categories": "Категорије", + "tags": "Ознаке", + "no-users-found": "Нема пронађених корисника!" +} \ No newline at end of file diff --git a/public/language/sv/_DO_NOT_EDIT_FILES_HERE.md b/public/language/sv/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/sv/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/sv/admin/admin.json b/public/language/sv/admin/admin.json new file mode 100644 index 0000000000..08b4f23ba5 --- /dev/null +++ b/public/language/sv/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Är du säker på att du vill ombygga och starta om NodeBB?", + "alert.confirm-restart": "Är du säker på att du vill starta om NodeBB?", + + "acp-title": "%1 | NodeBB Admin Kontrollpanel", + "settings-header-contents": "Innehåll", + "changes-saved": "Ändringar Sparade", + "changes-saved-message": "Dina ändringar av NodeBB-konfigurationen har sparats.", + "changes-not-saved": "Ändringar Sparades Ej", + "changes-not-saved-message": "NodeBB kunde inte spara dina ändringar. (%1)" +} \ No newline at end of file diff --git a/public/language/sv/admin/advanced/cache.json b/public/language/sv/admin/advanced/cache.json new file mode 100644 index 0000000000..ebdd17c2dc --- /dev/null +++ b/public/language/sv/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Inläggscache", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Full", + "post-cache-size": "Storlek på inläggscache", + "items-in-cache": "Föremål i cache" +} \ No newline at end of file diff --git a/public/language/sv/admin/advanced/database.json b/public/language/sv/admin/advanced/database.json new file mode 100644 index 0000000000..0017290727 --- /dev/null +++ b/public/language/sv/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Driftstid i sekunder", + "uptime-days": "Driftstid i dagar", + + "mongo": "Mongo", + "mongo.version": "MongoDB-version", + "mongo.storage-engine": "Lagringsmotor", + "mongo.collections": "Samlingar", + "mongo.objects": "Objekt", + "mongo.avg-object-size": "Genomsnittlig Objektstorlek", + "mongo.data-size": "Datastorlek", + "mongo.storage-size": "Förvaringsstorlek", + "mongo.index-size": "Indexstorlek", + "mongo.file-size": "Filstorlek", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtuellt minne", + "mongo.mapped-memory": "Mappat minne", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Ut", + "mongo.num-requests": "Antal förfrågningar", + "mongo.raw-info": "MongoDB rådata", + "mongo.unauthorized": "NodeBB kunde inte fråga MongoDB-databasen om relevant statistik. Vänligen se till att NodeBBs användare inkluderar "clusterMonitor" rollen för "admin"-databasen.", + + "redis": "Redis", + "redis.version": "Redis-version", + "redis.keys": "Nycklar", + "redis.expires": "Upphör att gälla", + "redis.avg-ttl": "Genomsnittlig TTL", + "redis.connected-clients": "Anslutna klienter", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blockerade Klienter", + "redis.used-memory": "Använt Minne", + "redis.memory-frag-ratio": "Minnesfragmenteringskvot", + "redis.total-connections-recieved": "Totalt Antal Inkommande Anslutningar", + "redis.total-commands-processed": "Total Commands Processed", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Indata", + "redis.total-output": "Total Utdata", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis rådata", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL-version", + "postgres.raw-info": "Postgres rådata" +} diff --git a/public/language/sv/admin/advanced/errors.json b/public/language/sv/admin/advanced/errors.json new file mode 100644 index 0000000000..546f0f1508 --- /dev/null +++ b/public/language/sv/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figure %1", + "error-events-per-day": "%1 events per day", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Manage Error Log", + "export-error-log": "Export Error Log (CSV)", + "clear-error-log": "Clear Error Log", + "route": "Route", + "count": "Count", + "no-routes-not-found": "Hooray! No 404 errors!", + "clear404-confirm": "Are you sure you wish to clear the 404 error logs?", + "clear404-success": "\"404 Not Found\" errors cleared" +} \ No newline at end of file diff --git a/public/language/sv/admin/advanced/events.json b/public/language/sv/admin/advanced/events.json new file mode 100644 index 0000000000..b2c2033fb5 --- /dev/null +++ b/public/language/sv/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Events", + "no-events": "There are no events", + "control-panel": "Events Control Panel", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/sv/admin/advanced/logs.json b/public/language/sv/admin/advanced/logs.json new file mode 100644 index 0000000000..b9de400e1c --- /dev/null +++ b/public/language/sv/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Logs", + "control-panel": "Logs Control Panel", + "reload": "Reload Logs", + "clear": "Clear Logs", + "clear-success": "Logs Cleared!" +} \ No newline at end of file diff --git a/public/language/sv/admin/appearance/customise.json b/public/language/sv/admin/appearance/customise.json new file mode 100644 index 0000000000..97abb5ede5 --- /dev/null +++ b/public/language/sv/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + + "custom-header": "Custom Header", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Enable Custom Header", + + "custom-css.livereload": "Enable Live Reload", + "custom-css.livereload.description": "Enable this to force all sessions on every device under your account to refresh whenever you click save" +} \ No newline at end of file diff --git a/public/language/sv/admin/appearance/skins.json b/public/language/sv/admin/appearance/skins.json new file mode 100644 index 0000000000..4db6fbdd8a --- /dev/null +++ b/public/language/sv/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Loading Skins...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "current-skin": "Current Skin", + "skin-updated": "Skin Updated", + "applied-success": "%1 skin was succesfully applied", + "revert-success": "Skin reverted to base colours" +} \ No newline at end of file diff --git a/public/language/sv/admin/appearance/themes.json b/public/language/sv/admin/appearance/themes.json new file mode 100644 index 0000000000..597830f379 --- /dev/null +++ b/public/language/sv/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-theme": "Select Theme", + "current-theme": "Current Theme", + "no-themes": "No installed themes found", + "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?", + "theme-changed": "Theme Changed", + "revert-success": "You have successfully reverted your NodeBB back to it's default theme.", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/sv/admin/dashboard.json b/public/language/sv/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/sv/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/sv/admin/development/info.json b/public/language/sv/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/sv/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/sv/admin/development/logger.json b/public/language/sv/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/sv/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/sv/admin/extend/plugins.json b/public/language/sv/admin/extend/plugins.json new file mode 100644 index 0000000000..f7e60c4360 --- /dev/null +++ b/public/language/sv/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Installed", + "active": "Active", + "inactive": "Inactive", + "out-of-date": "Out of Date", + "none-found": "No plugins found.", + "none-active": "No Active Plugins", + "find-plugins": "Find Plugins", + + "plugin-search": "Plugin Search", + "plugin-search-placeholder": "Search for plugin...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Re-order Plugins", + "order-active": "Order Active Plugins", + "dev-interested": "Interested in writing plugins for NodeBB?", + "docs-info": "Full documentation regarding plugin authoring can be found in the NodeBB Docs Portal.", + + "order.description": "Certain plugins work ideally when they are initialised before/after other plugins.", + "order.explanation": "Plugins load in the order specified here, from top to bottom", + + "plugin-item.themes": "Themes", + "plugin-item.deactivate": "Deactivate", + "plugin-item.activate": "Activate", + "plugin-item.install": "Install", + "plugin-item.uninstall": "Uninstall", + "plugin-item.settings": "Settings", + "plugin-item.installed": "Installed", + "plugin-item.latest": "Latest", + "plugin-item.upgrade": "Upgrade", + "plugin-item.more-info": "For more information:", + "plugin-item.unknown": "Unknown", + "plugin-item.unknown-explanation": "The state of this plugin could not be determined, possibly due to a misconfiguration error.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Plugin Enabled", + "alert.disabled": "Plugin Disabled", + "alert.upgraded": "Plugin Upgraded", + "alert.installed": "Plugin Installed", + "alert.uninstalled": "Plugin Uninstalled", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Plugin successfully deactivated", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "Plugin successfully installed, please activate the plugin.", + "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", + "alert.suggest-error": "

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

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

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

", + "alert.incompatible": "

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

", + "alert.possibly-incompatible": "

No Compatibility Information Found

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

In the event that NodeBB cannot boot properly:

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

Continue installation of latest version of this plugin?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "Plugin License Information", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "Do you wish to continue with activating this plugin?" +} diff --git a/public/language/sv/admin/extend/rewards.json b/public/language/sv/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/sv/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/sv/admin/extend/widgets.json b/public/language/sv/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/sv/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/admins-mods.json b/public/language/sv/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/sv/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/categories.json b/public/language/sv/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/sv/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/digest.json b/public/language/sv/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/sv/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/sv/admin/manage/groups.json b/public/language/sv/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/sv/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/privileges.json b/public/language/sv/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/sv/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/registration.json b/public/language/sv/admin/manage/registration.json new file mode 100644 index 0000000000..f51b4d56e6 --- /dev/null +++ b/public/language/sv/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Queue", + "description": "There are no users in the registration queue.
To enable this feature, go to Settings → User → User Registration and set Registration Type to \"Admin Approval\".", + + "list.name": "Name", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Time", + "list.username-spam": "Frequency: %1 Appears: %2 Confidence: %3", + "list.email-spam": "Frequency: %1 Appears: %2", + "list.ip-spam": "Frequency: %1 Appears: %2", + + "invitations": "Invitations", + "invitations.description": "Below is a complete list of invitations sent. Use ctrl-f to search through the list by email or username.

The username will be displayed to the right of the emails for users who have redeemed their invitations.", + "invitations.inviter-username": "Inviter Username", + "invitations.invitee-email": "Invitee Email", + "invitations.invitee-username": "Invitee Username (if registered)", + + "invitations.confirm-delete": "Are you sure you wish to delete this invitation?" +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/tags.json b/public/language/sv/admin/manage/tags.json new file mode 100644 index 0000000000..dc027d84b9 --- /dev/null +++ b/public/language/sv/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Ditt forum har inte några ämnen med etiketter ännu.", + "bg-color": "Bakgrundsfärg", + "text-color": "Textfärg", + "description": "Välj etiketter genom att klicka eller dra. Använd CTRL för att välja flera etiketter.", + "create": "Skapa Etikett", + "modify": "Redigera Etikett", + "rename": "Döp om etikett", + "delete": "Radera Vald Etikett", + "search": "Sök efter etiketter...", + "settings": "Etikettinställningar", + "name": "Etikettnamn", + + "alerts.editing": "Redigerar etikett(er)", + "alerts.confirm-delete": "Vill du radera de valda etiketterna?", + "alerts.update-success": "Etikett Uppdaterad!", + "reset-colors": "Återställ färger" +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/uploads.json b/public/language/sv/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/sv/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/sv/admin/manage/users.json b/public/language/sv/admin/manage/users.json new file mode 100644 index 0000000000..9064153de7 --- /dev/null +++ b/public/language/sv/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Users", + "edit": "Actions", + "make-admin": "Make Admin", + "remove-admin": "Remove Admin", + "validate-email": "Validate Email", + "send-validation-email": "Send Validation Email", + "password-reset-email": "Send Password Reset Email", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Ban User(s)", + "temp-ban": "Ban User(s) Temporarily", + "unban": "Unban User(s)", + "reset-lockout": "Reset Lockout", + "reset-flags": "Reset Flags", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Download CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "New User", + "filter-by": "Filter by", + "pills.unvalidated": "Not Validated", + "pills.validated": "Validated", + "pills.banned": "Banned", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "By User ID", + "search.uid-placeholder": "Enter a user ID to search", + "search.username": "By User Name", + "search.username-placeholder": "Enter a username to search", + "search.email": "By Email", + "search.email-placeholder": "Enter a email to search", + "search.ip": "By IP Address", + "search.ip-placeholder": "Enter an IP Address to search", + "search.not-found": "User not found!", + + "inactive.3-months": "3 months", + "inactive.6-months": "6 months", + "inactive.12-months": "12 months", + + "users.uid": "uid", + "users.username": "username", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "postcount", + "users.reputation": "reputation", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "last online", + "users.banned": "banned", + + "create.username": "User Name", + "create.email": "Email", + "create.email-placeholder": "Email of this user", + "create.password": "Password", + "create.password-confirm": "Confirm Password", + + "temp-ban.length": "Length", + "temp-ban.reason": "Reason (Optional)", + "temp-ban.hours": "Hours", + "temp-ban.days": "Days", + "temp-ban.explanation": "Enter the length of time for the ban. Note that a time of 0 will be a considered a permanent ban.", + + "alerts.confirm-ban": "Do you really want to ban this user permanently?", + "alerts.confirm-ban-multi": "Do you really want to ban these users permanently?", + "alerts.ban-success": "User(s) banned!", + "alerts.button-ban-x": "Ban %1 user(s)", + "alerts.unban-success": "User(s) unbanned!", + "alerts.lockout-reset-success": "Lockout(s) reset!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global moderator.", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", + "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Emails validated", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "User(s) Deleted!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Create User", + "alerts.button-create": "Create", + "alerts.button-cancel": "Cancel", + "alerts.error-passwords-different": "Passwords must match!", + "alerts.error-x": "Error

%1

", + "alerts.create-success": "User created!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "An invitation email has been sent to %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/sv/admin/menu.json b/public/language/sv/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/sv/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/advanced.json b/public/language/sv/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/sv/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/api.json b/public/language/sv/admin/settings/api.json new file mode 100644 index 0000000000..9394d032cf --- /dev/null +++ b/public/language/sv/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Inställningar", + "lead-text": "Från den här sidan kan du konfigurera åtkomst till NodeBBs 'Write API'.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Tillåt endast API-användning via HTTPS", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Beskrivning", + "no-description": "Ingen beskrivning finns.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/chat.json b/public/language/sv/admin/settings/chat.json new file mode 100644 index 0000000000..67898611e7 --- /dev/null +++ b/public/language/sv/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Chat Settings", + "disable": "Disable chat", + "disable-editing": "Disable chat message editing/deletion", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "Maximum length of chat messages", + "max-room-size": "Maximum number of users in chat rooms", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/cookies.json b/public/language/sv/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/sv/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/email.json b/public/language/sv/admin/settings/email.json new file mode 100644 index 0000000000..a51b116c25 --- /dev/null +++ b/public/language/sv/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Epostsammandrag", + "subscriptions.disable": "Avaktivera epostsammandrag", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/sv/admin/settings/general.json b/public/language/sv/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/sv/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/sv/admin/settings/group.json b/public/language/sv/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/sv/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/guest.json b/public/language/sv/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/sv/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/homepage.json b/public/language/sv/admin/settings/homepage.json new file mode 100644 index 0000000000..7428d59eeb --- /dev/null +++ b/public/language/sv/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Home Page", + "description": "Choose what page is shown when users navigate to the root URL of your forum.", + "home-page-route": "Home Page Route", + "custom-route": "Custom Route", + "allow-user-home-pages": "Allow User Home Pages", + "home-page-title": "Title of the home page (default \"Home\")" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/languages.json b/public/language/sv/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/sv/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/navigation.json b/public/language/sv/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/sv/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/sv/admin/settings/notifications.json b/public/language/sv/admin/settings/notifications.json new file mode 100644 index 0000000000..e00e779176 --- /dev/null +++ b/public/language/sv/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Aviseringar", + "welcome-notification": "Welcome Notification", + "welcome-notification-link": "Welcome Notification Link", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/pagination.json b/public/language/sv/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/sv/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/post.json b/public/language/sv/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/sv/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/reputation.json b/public/language/sv/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/sv/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/social.json b/public/language/sv/admin/settings/social.json new file mode 100644 index 0000000000..23aedfcfaa --- /dev/null +++ b/public/language/sv/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Post Sharing", + "info-plugins-additional": "Plugins can add additional networks for sharing posts.", + "save-success": "Successfully saved Post Sharing Networks!" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/sockets.json b/public/language/sv/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/sv/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/sounds.json b/public/language/sv/admin/settings/sounds.json new file mode 100644 index 0000000000..95ccbde0f1 --- /dev/null +++ b/public/language/sv/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Notifications", + "chat-messages": "Chat Messages", + "play-sound": "Play", + "incoming-message": "Incoming Message", + "outgoing-message": "Outgoing Message", + "upload-new-sound": "Upload New Sound", + "saved": "Settings Saved" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/tags.json b/public/language/sv/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/sv/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/sv/admin/settings/uploads.json b/public/language/sv/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/sv/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/sv/admin/settings/user.json b/public/language/sv/admin/settings/user.json new file mode 100644 index 0000000000..1d5d460472 --- /dev/null +++ b/public/language/sv/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Themes", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "Daily", + "digest-freq.weekly": "Weekly", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Monthly", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/sv/admin/settings/web-crawler.json b/public/language/sv/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/sv/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/sv/category.json b/public/language/sv/category.json new file mode 100644 index 0000000000..789b67dc0e --- /dev/null +++ b/public/language/sv/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategori", + "subcategories": "Underkategori", + "new_topic_button": "Nytt ämne", + "guest-login-post": "Logga in för att posta", + "no_topics": "Det finns inga ämnen i denna kategori.
Varför skapar inte du ett ämne?", + "browsing": "läser", + "no_replies": "Ingen har svarat", + "no_new_posts": "Inga nya inlägg.", + "watch": "Bevaka", + "ignore": "Ignorera", + "watching": "Bevakar", + "not-watching": "Följer inte", + "ignoring": "Ignorerar", + "watching.description": "Visa ämnen i olästa och senaste", + "not-watching.description": "Visa inte ämnen i olästa, visa i senaste", + "ignoring.description": "Visa inte ämnen i olästa och senaste", + "watching.message": "Nu får du uppdateringar från den här kategorin och alla underkategorier", + "notwatching.message": "Du får inga uppdateringar från den här kategorin eller alla underkategorier", + "ignoring.message": "Nu ignorerar du alla uppdateringar från den här kategorin och alla underkategorier.", + "watched-categories": "Bevakade kategorier", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/sv/email.json b/public/language/sv/email.json new file mode 100644 index 0000000000..4491c449b8 --- /dev/null +++ b/public/language/sv/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "Välkommen till %1", + "invite": "Inbjudan ifrån %1", + "greeting_no_name": "Hej", + "greeting_with_name": "Hej %1", + "email.verify-your-email.subject": "Vänligen bekräfta din e-postadress", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Tack för att du registerar dig på %1!", + "welcome.text2": "För att slutföra aktiveringen av ditt konto, behöver vi verifiera att du har tillgång till den e-postadress du registrerade dig med.", + "welcome.text3": "En administrator har accepterat din registreringsansökan. Du kan logga in med ditt användarnamn och lösenord nu.", + "welcome.cta": "Klicka här för att bekräfta din e-postadress ", + "invitation.text1": "%1 har bjudit in dig till %2", + "invitation.text2": "Din inbjudan går ut om %1 dagar.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "Vi fick en förfrågan om att återställa ditt lösenord, möjligen för att du har glömt det. Om detta inte är fallet, så kan du bortse från det här epostmeddelandet. ", + "reset.text2": "För att fortsätta med återställning av lösenordet så kan du klicka på följande länk:", + "reset.cta": "Klicka här för att återställa ditt lösenord", + "reset.notify.subject": "Lösenordet ändrat", + "reset.notify.text1": "Vi vill uppmärksamma dig på att ditt lösenord ändrades den %1.", + "reset.notify.text2": "Om du inte godkänt det här så vänligen kontakta en administratör snarast. ", + "digest.latest_topics": "Senaste ämnen från %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Klicka här för att besöka %1", + "digest.unsub.info": "Det här meddelandet fick du på grund av dina inställningar för prenumeration. ", + "digest.day": "dag", + "digest.week": "vecka", + "digest.month": "månad", + "digest.subject": "Sammanställt flöde för %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "Nytt meddelande från %1", + "notif.chat.cta": "Klicka här för att fortsätta konversationen", + "notif.chat.unsub.info": "Denna notifikation skickades till dig på grund av dina inställningar för prenumerationer.", + "notif.post.unsub.info": "Det här meddelandet fick du på grund av dina inställningar för prenumeration. ", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "\nDet här är ett testmeddelande som verifierar att e-posten är korrekt installerad för din NodeBB. ", + "unsub.cta": "Klicka här för att ändra inställningarna", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Du har blivit bannlyst från %1", + "banned.text1": "Användaren %1 har blivit bannlyst från %2.", + "banned.text2": "Denna bannlysning gäller t.o.m. %1", + "banned.text3": "Anledningen till din bannlysning är:", + "closing": "Tack!" +} \ No newline at end of file diff --git a/public/language/sv/error.json b/public/language/sv/error.json new file mode 100644 index 0000000000..5951c4e022 --- /dev/null +++ b/public/language/sv/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Ogiltig data", + "invalid-json": "Ogiltig JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Du verkar inte vara inloggad.", + "account-locked": "Ditt konto har tillfälligt blivit låst", + "search-requires-login": "Sökning kräver ett konto, var god logga in eller registrera dig.", + "goback": "Tryck på tillbakaknappen för att återgå till förra sidan", + "invalid-cid": "Ogiltigt id för kategori", + "invalid-tid": "Ogiltigt id för ämne", + "invalid-pid": "Ogiltigt id för inlägg", + "invalid-uid": "Ogiltigt id för användare", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Ogiltigt användarnamn", + "invalid-email": "Ogiltig epostadress", + "invalid-fullname": "Ogiltigt namn", + "invalid-location": "Ogiltig plats", + "invalid-birthday": "Ogiltig födelsedag", + "invalid-title": "Ogiltig titel", + "invalid-user-data": "Ogiltig användardata", + "invalid-password": "Ogiltigt lösenord", + "invalid-login-credentials": "Ogiltig inloggning", + "invalid-username-or-password": "Specificera både användarnamn och lösenord", + "invalid-search-term": "Ogiltig sökterm", + "invalid-url": "Ogiltig URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Lokala inloggningssystem har stängts av för icke-privilegierade konton.", + "csrf-invalid": "Det gick inte att logga in dig, sannolikt på grund av en utgången session. Var god försök igen", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Ogiltigt värde för siduppdelning. Värdet måste vara mellan %1 och %2", + "username-taken": "Användarnamn upptaget", + "email-taken": "Epostadress upptagen", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "email-not-confirmed-chat": "Du kan ej använda chatten förrän din epostadress har blivit bekräftad, var god klicka här för att bekräfta din epostadress.", + "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Vi kunde ej bekräfta din epostadress, var god försök igen senare.", + "confirm-email-already-sent": "Bekräftningsbrev redan skickat, var god vänta %1 minut(er) innan du skickar ett nytt.", + "sendmail-not-found": "Kunde inte hitta Sendmail, vänligen se till att den är installerad och får köras av den användare som kör NodeBB.", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "Användarnamnet är för kort", + "username-too-long": "Användarnamnet är för långt", + "password-too-long": "Lösenordet är för långt", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Användare bannlyst", + "user-banned-reason": "Vi beklagar, men detta konto har blivit bannlyst (Anledning: %1)", + "user-banned-reason-until": "Vi beklagar, men detta konto har blivit bannlyst till %1 (Anledning: %2)", + "user-too-new": "När du är ny medlem måste du vänta %1 sekund(er) innan du gör ditt första inlägg", + "blacklisted-ip": "Din IP-adress har blivit bannlyst från det här forumet. Om du tror att det beror på ett misstag, vad god kontakta en administratör. ", + "ban-expiry-missing": "Ange ett slutdatum för denna banning", + "no-category": "Kategorin finns inte", + "no-topic": "Ämnet finns inte", + "no-post": "Inlägget finns inte", + "no-group": "Gruppen finns inte", + "no-user": "Användaren finns inte", + "no-teaser": "Förhandsvisningen finns inte", + "no-flag": "Flag does not exist", + "no-privileges": "Du har inte tillräckliga rättigheter för den här åtgärden.", + "category-disabled": "Kategorin inaktiverad", + "topic-locked": "Ämnet låst", + "post-edit-duration-expired": "Du kan endast ändra inlägg inom %1 sekund(er) efter att ha skickat det", + "post-edit-duration-expired-minutes": "Du kan endast ändra inlägg inom %1 minut(er) efter att ha skickat det", + "post-edit-duration-expired-minutes-seconds": "Du kan endast ändra inlägg inom %1 minut(er) %2 sekund(er) efter att ha skickat det", + "post-edit-duration-expired-hours": "Du kan endast ändra inlägg inom %1 timm(ar) efter att ha skickat det", + "post-edit-duration-expired-hours-minutes": "Du kan endast ändra inlägg inom %1 timm(ar) %2 minut(er) efter att ha skickat det", + "post-edit-duration-expired-days": "Du kan endast ändra inlägg inom %1 dag(ar) efter att ha skickat det", + "post-edit-duration-expired-days-hours": "Du kan endast ändra inlägg inom %1 dag(ar) %2 timm(ar) efter att ha skickat det", + "post-delete-duration-expired": "Du kan endast radera inlägg inom %1 sekund(er) efter att ha skickat det", + "post-delete-duration-expired-minutes": "Du kan endast radera inlägg inom %1 minut(er) efter att ha skickat det", + "post-delete-duration-expired-minutes-seconds": "Du kan endast radera inlägg inom %1 minut(er) %2 sekund(er) efter att ha skickat det", + "post-delete-duration-expired-hours": "Du kan endast radera inlägg inom %1 timm(ar) efter att ha skickat det", + "post-delete-duration-expired-hours-minutes": "Du kan endast radera inlägg inom %1 timmar(er) %2 minut(er) efter att ha skickat det", + "post-delete-duration-expired-days": "Du kan endast radera inlägg inom %1 dag(ar) efter att ha skickat det", + "post-delete-duration-expired-days-hours": "Du kan endast radera inlägg inom %1 dag(ar) %2 timm(ar) efter att ha skickat det", + "cant-delete-topic-has-reply": "Du kan inte ta bort ditt ämne om någon har svarat", + "cant-delete-topic-has-replies": "Du kan inte ta bort ditt ämne efter att den har %1 svar", + "content-too-short": "Skriv ett längre inlägg. Inlägg måste innehålla minst %1 tecken.", + "content-too-long": "Skriv ett kortare inlägg. Inlägg kan inte innehålla mer än %1 tecken.", + "title-too-short": "Skriv en längre rubrik. Rubriker måste innehålla minst %1 tecken.", + "title-too-long": "Skriv en kortare rubrik. Rubriker kan inte innehålla mer än %1 tecken.", + "category-not-selected": "Kategori Ej vald.", + "too-many-posts": "Du måste vänta minst %1 sekund(er) mellan varje inlägg", + "too-many-posts-newbie": "Som ny användare måste du vänta %1 sekund(er) mellan varje inlägg tills dess du har %2 förtroende", + "already-posting": "You are already posting", + "tag-too-short": "Fyll i en längre tagg. Taggar måste vara minst %1 tecken långa", + "tag-too-long": "Fyll i en kortare tagg. Taggar kan ej vara längre än %1 tecken långa", + "not-enough-tags": "Otillräckligt antal taggar. Ämnen måste ha minst %1 taggar", + "too-many-tags": "För många taggar. Ämnen kan ej har mer än %1 tagg(ar)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Vänta medan uppladdningen slutförs.", + "file-too-big": "Den maximalt tillåtna filstorleken är %1 kB - var god ladda upp en mindre fil", + "guest-upload-disabled": "Uppladdningar av oregistrerade användare har inaktiverats", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Du har redan bokmärkt det här inlägget", + "already-unbookmarked": "Du har redan tagit bort bokmärket för det här inlägget", + "cant-ban-other-admins": "Du kan inte bannlysa andra administratörer!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Du är den enda administratören. Lägg till en annan användare som administratör innan du tar bort dig själv.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Ta bort administratörsbehörighet från detta konto innan du försöker ta bort den.", + "already-deleting": "Already deleting", + "invalid-image": "Ogiltig bild", + "invalid-image-type": "Ogiltig bildtyp. Tillåtna typer är: %1", + "invalid-image-extension": "Ogiltigt bildformat", + "invalid-file-type": "Ogiltig filtyp. Tillåtna typer är: %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "Gruppnamnet är för kort", + "group-name-too-long": "Gruppnamnet är för långt", + "group-already-exists": "Gruppen existerar redan", + "group-name-change-not-allowed": "Gruppnamnet får inte ändras", + "group-already-member": "Redan i denna grupp", + "group-not-member": "Ej medlem av denna grupp", + "group-needs-owner": "Gruppen kräver minst en ägare", + "group-already-invited": "Användaren har redan bjudits in", + "group-already-requested": "Din medlemsskapsförfrågan har redan skickats", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "Inlägget är redan raderat", + "post-already-restored": "Inlägget är redan återställt", + "topic-already-deleted": "Ämnet är redan raderat", + "topic-already-restored": "Ämnet är redan återställt", + "cant-purge-main-post": "Huvudinlägg kan ej rensas bort, ta bort ämnet istället", + "topic-thumbnails-are-disabled": "Miniatyrbilder för ämnen är inaktiverat", + "invalid-file": "Ogiltig fil", + "uploads-are-disabled": "Uppladdningar är inaktiverat", + "signature-too-long": "Din signatur kan inte vara längre än %1 tecken.", + "about-me-too-long": "Din text om dig själv kan inte vara längre än %1 tecken.", + "cant-chat-with-yourself": "Du kan inte chatta med dig själv!", + "chat-restricted": "Denna användaren har begränsat sina meddelanden. Användaren måste följa dig innan ni kan chatta med varandra", + "chat-disabled": "Chatten är inaktiverad", + "too-many-messages": "Du har skickat för många meddelanden, var god vänta", + "invalid-chat-message": "Ogiltigt chattmeddelande", + "chat-message-too-long": "Chattmeddelanden får inte vara längre än %1 tecken.", + "cant-edit-chat-message": "Du har inte rättigheter att redigera det här meddelandet", + "cant-delete-chat-message": "Du har inte rättigheter att radera det här meddelandet", + "chat-edit-duration-expired": "Du kan endast redigera chattmeddelanden %1 sekunder efter att du skrivit dem", + "chat-delete-duration-expired": "Du kan endast radera chattmeddelanden %1 sekunder efter att du skrivit dem", + "chat-deleted-already": "Detta chattmeddelande har redan raderats.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Du har redan röstat på det här inlägget.", + "reputation-system-disabled": "Ryktessystemet är inaktiverat.", + "downvoting-disabled": "Nedröstning är inaktiverat", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Du kan inte rösta på ditt eget inlägg.", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB stötte på problem med att ladda om: \"%1\". NodeBB kommer fortsätta servera befintliga resurser till klienten, men du borde återställa det du gjorde innan du försökte ladda om.", + "registration-error": "Registreringsfel", + "parse-error": "Något gick fel vid tolkning av svar från servern", + "wrong-login-type-email": "Använd din e-postadress för att logga in", + "wrong-login-type-username": "Använd ditt användarnamn för att logga in", + "sso-registration-disabled": "Registrering är inte tillgänglig för %1-konton, vänligen registrera med en e-postadress först.", + "sso-multiple-association": "Du kan inte associera flera konton från denna tjänst till ditt NodeBB-konto. Vänligen kopppla bort ditt existerande konto och försök igen.", + "invite-maximum-met": "Du har bjudit in det maximala antalet användare (%1 av %2)", + "no-session-found": "Ingen login-session hittades!", + "not-in-room": "Användaren finns inte i rummet", + "cant-kick-self": "Du kan inte sparka ut dig själv från gruppen", + "no-users-selected": "Ingen användare vald(a)", + "invalid-home-page-route": "Ogiltig sidsökväg", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Inga ämnen valda!", + "cant-move-to-same-topic": "Kan inte flytta inlägg till samma ämne!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Du kan inte blockera dig själv!", + "cannot-block-privileged": "Du kan inte blockera administratörer eller globala moderatorer", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "Det verkar vara något problem med din internetanslutning", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/sv/flags.json b/public/language/sv/flags.json new file mode 100644 index 0000000000..cc7c5baafe --- /dev/null +++ b/public/language/sv/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Status", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Yippie! Inga flaggor funna.", + "assignee": "Tilldelad", + "update": "Uppdatera", + "updated": "Uppdatering", + "resolved": "Resolved", + "target-purged": "Innehållet denna flagga refererar till har rensats bort och är inte längre tillgängligt.", + + "graph-label": "Dagliga flaggningar", + "quick-filters": "Snabbfilter", + "filter-active": "Ett eller flera filter är aktiva i denna lista med flaggor", + "filter-reset": "Ta bort filter", + "filters": "Filterinställningar", + "filter-reporterId": "Rapporterandes UID", + "filter-targetUid": "Flaggades UID", + "filter-type": "Flaggtyp", + "filter-type-all": "Allt innehåll", + "filter-type-post": "Inlägg", + "filter-type-user": "Användare", + "filter-state": "Status", + "filter-assignee": "Tilldelad persons UID", + "filter-cid": "Kategori", + "filter-quick-mine": "Tilldelade till mig", + "filter-cid-all": "Alla kategorier", + "apply-filters": "Applicera filter", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Flaggad användare", + "view-profile": "Visa profil", + "start-new-chat": "Påbörja ny chatt", + "go-to-target": "Visa flaggans ämne", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Visa profil", + "user-edit": "Redigera profil", + + "notes": "Flaggans anteckningar", + "add-note": "Lägg till anteckning", + "no-notes": "Inga delade anteckningar.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Anteckning tillagd", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Ingen flagghitorik.", + + "state-all": "Alla status", + "state-open": "Nya/Öppna", + "state-wip": "Pågående arbete", + "state-resolved": "Löst", + "state-rejected": "Avvisad", + "no-assignee": "Ej tilldelad", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Vänligen ange anledningen till att du flaggar %1 %2 för granskning. Alternativt, använd en av snabbrapporteringsknapparna.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Kränkande", + "modal-reason-other": "Annat (ange nedan)", + "modal-reason-custom": "Anledning för rapportering av detta innehåll...", + "modal-submit": "Skicka in rapport", + "modal-submit-success": "Innehållet har flaggats för moderering.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/sv/global.json b/public/language/sv/global.json new file mode 100644 index 0000000000..fb3acb257a --- /dev/null +++ b/public/language/sv/global.json @@ -0,0 +1,126 @@ +{ + "home": "Hem", + "search": "Sök", + "buttons.close": "Stäng", + "403.title": "Tillgång nekad", + "403.message": "Du verkar ha ramlat in på en sida du ej har tillgång till.", + "403.login": "Du kanske bör försöka logga in?", + "404.title": "Sidan saknas", + "404.message": "Du verkar ha ramlat in på en sida som inte finns. Återgå till första sidan.", + "500.title": "Internt fel.", + "500.message": "Hoppsan! Något verkar ha gått fel!", + "400.title": "Felaktig förfrågan.", + "400.message": "Det ser ut som länken är felaktig, vänligen dubbelkolla och försök igen. Annars återvänd till hemsidan.", + "register": "Registrera", + "login": "Logga in", + "please_log_in": "Var god logga in", + "logout": "Logga ut", + "posting_restriction_info": "Man måste vara inloggad för att kunna skapa inlägg, klicka här för att logga in.", + "welcome_back": "Välkommen tillbaka ", + "you_have_successfully_logged_in": "Inloggningen lyckades", + "save_changes": "Spara ändringar", + "save": "Spara", + "close": "Stäng", + "pagination": "Siduppdelning", + "pagination.out_of": "%1 av %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Admin", + "header.categories": "Kategorier", + "header.recent": "Senaste", + "header.unread": "Olästa", + "header.tags": "Taggar", + "header.popular": "Populära", + "header.top": "Top", + "header.users": "Användare", + "header.groups": "Grupper", + "header.chats": "Chattar", + "header.notifications": "Notiser", + "header.search": "Sök", + "header.profile": "Profil", + "header.navigation": "Navigering", + "notifications.loading": "Laddar notiser", + "chats.loading": "Laddar chattar", + "motd.welcome": "Välkommen till NodeBB, framtidens diskussionsplattform.", + "previouspage": "Föregående sida", + "nextpage": "Nästa sida", + "alert.success": "Lyckat", + "alert.error": "Fel", + "alert.banned": "Bannlyst", + "alert.banned.message": "Du har precis blivit bannlyst, du har nu begränsad tillgång.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Du följer inte längre %1!", + "alert.follow": "Du följer nu %1!", + "users": "Användare", + "topics": "Ämnen", + "posts": "Inlägg", + "x-posts": "%1 posts", + "best": "Bästa", + "controversial": "Controversial", + "votes": "Röster", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "Uppröstare", + "upvoted": "Uppröstad", + "downvoters": "Nerröstare", + "downvoted": "Nedröstad", + "views": "Visningar", + "posters": "Posters", + "reputation": "Rykte", + "lastpost": "Senaste inlägget", + "firstpost": "Först inlägget", + "read_more": "läs mer", + "more": "Mer", + "none": "None", + "posted_ago_by_guest": "inskickad %1 av anonym", + "posted_ago_by": "inskickad %1 av %2", + "posted_ago": "postat %1", + "posted_in": "postat i %1", + "posted_in_by": "postat i %1 av %2", + "posted_in_ago": "inskickad i %1 %2", + "posted_in_ago_by": "postat i %1 %2 av %3", + "user_posted_ago": "%1 postades %2", + "guest_posted_ago": "Anonym postade %1", + "last_edited_by": "Senaste redigerad av %1", + "norecentposts": "Inga nya inlägg", + "norecenttopics": "Inga nya ämnen", + "recentposts": "Senaste inläggen", + "recentips": "Nyligen inloggade IPn", + "moderator_tools": "Moderator verktyg", + "online": "Online", + "away": "Borta", + "dnd": "Stör inte", + "invisible": "Osynlig", + "offline": "Offline", + "email": "E-post", + "language": "Språk", + "guest": "Anonym", + "guests": "Anonyma", + "former_user": "En före detta användare", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Forumet uppdaterades", + "updated.message": "Det här forumet har nu uppdaterats till senaste versionen. Klicka här för att ladda om sidan.", + "privacy": "Integritet", + "follow": "Följ", + "unfollow": "Sluta följ", + "delete_all": "Ta bort alla", + "map": "Karta", + "sessions": "Login-sessioner", + "ip_address": "IP-adress", + "enter_page_number": "Skriv in sidnummer", + "upload_file": "Ladda upp en fil", + "upload": "Ladda upp", + "uploads": "Uppladdningar", + "allowed-file-types": "Tillåtna filtyper är %1", + "unsaved-changes": "Du har ändringar som inte sparats. Är du säker på att du vill navigera bort?", + "reconnecting-message": "Ser ut som din anslutning till %1 gick förlorad, vänta medan vi försöker att återansluta.", + "play": "Spela", + "cookies.message": "Denna webbsida använder cookies för att säkerställa bästa möjliga upplevelse.", + "cookies.accept": "Jag förstår!", + "cookies.learn_more": "Läs mer", + "edited": "Redigerad", + "disabled": "Avstängd", + "select": "Välj", + "user-search-prompt": "Skriv något för att hitta användare" +} \ No newline at end of file diff --git a/public/language/sv/groups.json b/public/language/sv/groups.json new file mode 100644 index 0000000000..1bdc3736c5 --- /dev/null +++ b/public/language/sv/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Grupper", + "view_group": "Visa grupp ", + "owner": "Gruppägare", + "new_group": "Skapa ny grupp", + "no_groups_found": "Det finns inga grupper att se", + "pending.accept": "Acceptera", + "pending.reject": "Neka", + "pending.accept_all": "Acceptera alla", + "pending.reject_all": "Avvisa alla", + "pending.none": "Det finns inga väntande medlemmar just nu", + "invited.none": "Det finns inga inbjudna medlemmar just nu", + "invited.uninvite": "Dra tillbaka inbjudan", + "invited.search": "Sök efter en användare att lägga till i denna grupp", + "invited.notification_title": "Du har blivit inbjuden att bli medlem i %1", + "request.notification_title": "Förfrågan om gruppmedlemskap från %1", + "request.notification_text": "%1 har skickat en förfrågan om medlemskap i %2", + "cover-save": "Spara", + "cover-saving": "Sparar", + "details.title": "Detaljer för gruppen ", + "details.members": "Medlemslista", + "details.pending": "Väntande medlemmar", + "details.invited": "Inbjudna medlemmar", + "details.has_no_posts": "Den här gruppens medlemmar har inte skrivit några inlägg.", + "details.latest_posts": "Senaste inlägg", + "details.private": "Privat", + "details.disableJoinRequests": "Inaktivera förfrågningar om att gå med", + "details.disableLeave": "Tillåt inte att användare lämnar gruppen", + "details.grant": "Tilldela/Dra tillbaka ägarskap", + "details.kick": "Sparka ut", + "details.kick_confirm": "Vill du verkligen avlägsna denna användare från gruppen?", + "details.add-member": "Lägg till medlem", + "details.owner_options": "Gruppadministration", + "details.group_name": "Gruppnamn", + "details.member_count": "Medlemsantal", + "details.creation_date": "Skapardatum", + "details.description": "Beskrivning", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Förhandsgranskning av märke", + "details.change_icon": "Byt ikon", + "details.change_label_colour": "Ändra etikettfärg", + "details.change_text_colour": "Ändra textfärg", + "details.badge_text": "Märkestext", + "details.userTitleEnabled": "Visa märke", + "details.private_help": "Om aktiverat kommer en gruppägare behöva godkänna nya gruppmedlemmar", + "details.hidden": "Dold", + "details.hidden_help": "Om aktiverat kommer gruppen inte synas i grupplistan och användare måste bli inbjudna manuellt", + "details.delete_group": "Ta bort grupp", + "details.private_system_help": "Privata grupper är ej tillgängligt. Den här inställningen har ingen effekt.", + "event.updated": "Gruppinformationen har uppdaterats", + "event.deleted": "Gruppen \"%1\" har tagits bort", + "membership.accept-invitation": "Acceptera inbjudan", + "membership.accept.notification_title": "Du är nu medlem i %1", + "membership.invitation-pending": "Inbjudan väntar på svar", + "membership.join-group": "Gå med i grupp", + "membership.leave-group": "Lämna grupp", + "membership.leave.notification_title": "%1 har lämnat gruppen %2", + "membership.reject": "Neka", + "new-group.group_name": "Gruppnamn:", + "upload-group-cover": "Ladda upp omslagsbild för grupp", + "bulk-invite-instructions": "Ange en lista med kommaseparerade användarnamn som du vill bjuda in till denna grupp", + "bulk-invite": "Massinbjudning", + "remove_group_cover_confirm": "Vill du verkligen ta bort omslagsbilden?" +} \ No newline at end of file diff --git a/public/language/sv/ip-blacklist.json b/public/language/sv/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/sv/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/sv/language.json b/public/language/sv/language.json new file mode 100644 index 0000000000..e9c5fdac87 --- /dev/null +++ b/public/language/sv/language.json @@ -0,0 +1,5 @@ +{ + "name": "Svenska", + "code": "sv", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/sv/login.json b/public/language/sv/login.json new file mode 100644 index 0000000000..3dc899fded --- /dev/null +++ b/public/language/sv/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Namn / Epost", + "username": "Namn", + "remember_me": "Kom ihåg mig?", + "forgot_password": "Glömt lösenord?", + "alternative_logins": "Alternativa inloggningssätt", + "failed_login_attempt": "Misslyckad inloggning", + "login_successful": "Du är nu inloggad!", + "dont_have_account": "Har du inget konto?", + "logged-out-due-to-inactivity": "Du har loggats ut från Admin Kontrollpanelen på grund av inaktivitet", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/sv/modules.json b/public/language/sv/modules.json new file mode 100644 index 0000000000..79e42b04cd --- /dev/null +++ b/public/language/sv/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Chatta med", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "Du tittar på äldre meddelanden. Klicka här för att gå till det senaste meddelandet.", + "chat.send": "Skicka", + "chat.no_active": "Du har inte några aktiva chattar.", + "chat.user_typing": "%1 skriver ...", + "chat.user_has_messaged_you": "%1 har skickat ett medelande till dig.", + "chat.see_all": "Alla chattar", + "chat.mark_all_read": "Markera alla chattar som lästa", + "chat.no-messages": "Välj mottagare för att visa historik för chattmeddelande", + "chat.no-users-in-room": "Inga användare i detta rum", + "chat.recent-chats": "Senaste chattarna", + "chat.contacts": "Kontakter ", + "chat.message-history": "Historik för meddelande", + "chat.message-deleted": "Meddelande borttaget", + "chat.options": "Chattinställningar", + "chat.pop-out": "Utskjutande chatt", + "chat.minimize": "Minimera", + "chat.maximize": "Maximera", + "chat.seven_days": "7 dagar", + "chat.thirty_days": "30 dagar", + "chat.three_months": "3 månader", + "chat.delete_message_confirm": "Är du säker på att du vill radera det här meddelandet?", + "chat.retrieving-users": "Hämtar användare...", + "chat.manage-room": "Hantera chattrum", + "chat.add-user-help": "Sök efter användare här. Vid markering läggs användaren till i chatten. Den nya användaren kommer inte kunna se chattmeddelanden som skrevs innan de lades till i konversationen. Endast chattrumsägare () kan avlägsna användare från chattrum.", + "chat.confirm-chat-with-dnd-user": "Denna användare har satt sin status till Stör Ej. Vill du fortfarande chatta med dem?", + "chat.rename-room": "Byt namn på rum", + "chat.rename-placeholder": "Skriv in rummets namn här", + "chat.rename-help": "Rummets namn som skrivs in här kommer vara synligt för alla deltagare i rummet.", + "chat.leave": "Lämna chatt", + "chat.leave-prompt": "Är du säker att du vill lämna denna chatt?", + "chat.leave-help": "Om du lämnar denna chatt kommer du inte vara med i framtida korrespondens i denna chatt. Om du läggs till igen i framtiden, kommer du inte se någon chatthistorik från innan du lades till.", + "chat.in-room": "I detta rum", + "chat.kick": "Sparka ut", + "chat.show-ip": "Visa IP", + "chat.owner": "Rummets ägare", + "chat.system.user-join": "%1 har anslutit till rummet", + "chat.system.user-leave": "%1 har lämnat rummet", + "chat.system.room-rename": "%2 har döpt om rummet: %1", + "composer.compose": "Komponera", + "composer.show_preview": "Visa förhandsgranskning", + "composer.hide_preview": "Dölj förhandsgranskning", + "composer.user_said_in": "%1 sa i %2:", + "composer.user_said": "%1 sa:", + "composer.discard": "Är du säker på att du vill ta bort det här inlägget?", + "composer.submit_and_lock": "Skicka och lås", + "composer.toggle_dropdown": "Visa/Dölj dropdown", + "composer.uploading": "Laddar upp %1", + "composer.formatting.bold": "Fet", + "composer.formatting.italic": "Kursiv", + "composer.formatting.list": "Lista", + "composer.formatting.strikethrough": "Genomstrykning", + "composer.formatting.code": "Kod", + "composer.formatting.link": "Länk", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Ladda upp bild", + "composer.upload-file": "Ladda upp fil", + "composer.zen_mode": "Zen Mode", + "composer.select_category": "Välj en kategori", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "OK", + "bootbox.cancel": "Avbryt", + "bootbox.confirm": "Bekräfta", + "bootbox.submit": "Skicka", + "bootbox.send": "Skicka", + "cover.dragging_title": "Positionering av omslagsbild", + "cover.dragging_message": "Dra omslagsbilden till önskad position och tryck \"Spara\"", + "cover.saved": "Omslagsbilden sparad", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/sv/notifications.json b/public/language/sv/notifications.json new file mode 100644 index 0000000000..577a204de9 --- /dev/null +++ b/public/language/sv/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Notiser", + "no_notifs": "Du har inga nya notiser", + "see_all": "All notifications", + "mark_all_read": "Markera alla notiser som lästa", + "back_to_home": "Tillbaka till %1", + "outgoing_link": "Utgående länk", + "outgoing_link_message": "Du lämnar nu %1", + "continue_to": "Fortsätt till %1", + "return_to": "Återgå till %1", + "new_notification": "Du har en ny notis", + "you_have_unread_notifications": "Du har olästa notiser.", + "all": "Alla", + "topics": "Ämnen", + "replies": "Svar", + "chat": "Chattar", + "group-chat": "Group Chats", + "follows": "Följningar", + "upvote": "Uppröster", + "new-flags": "Nya flaggor", + "my-flags": "Mina tilldelade flaggor", + "bans": "Bannlysningar", + "new_message_from": "Nytt medelande från %1", + "upvoted_your_post_in": "%1 har röstat upp ditt inlägg i %2", + "upvoted_your_post_in_dual": "%1 och %2 har röstat upp ditt inlägg i %3.", + "upvoted_your_post_in_multiple": "%1 och %2 andra har röstat upp ditt inlägg i %3.", + "moved_your_post": "%1 har flyttat ditt inlägg till %2", + "moved_your_topic": "%1 har flyttat %2", + "user_flagged_post_in": "%1 flaggade ett inlägg i %2", + "user_flagged_post_in_dual": "%1 och %2 rapporterade ett inlägg i %3", + "user_flagged_post_in_multiple": "%1 och %2 andra rapporterade ett inlägg i %3", + "user_flagged_user": "%1 flaggade en användarprofil (%2)", + "user_flagged_user_dual": "%1 och %2 flaggade en användarprofil (%3)", + "user_flagged_user_multiple": "%1 och %2 andra flaggade en användarprofil (%3)", + "user_posted_to": "%1 har skrivit ett svar på: %2", + "user_posted_to_dual": "%1 och %2 har svarat på: %3", + "user_posted_to_multiple": "%1 och %2 andra har svarat på: %3", + "user_posted_topic": "%1 har skapat ett nytt ämne: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 började följa dig.", + "user_started_following_you_dual": "%1 och %2 började följa dig.", + "user_started_following_you_multiple": "%1 och %2 andra började följa dig.", + "new_register": "%1 skickade en registreringsförfrågan.", + "new_register_multiple": "Det finns %1 förfrågningar om registrering som inväntar granskning.", + "flag_assigned_to_you": "Flaggan %1 har tillskrivits dig", + "post_awaiting_review": "Inlägg väntar på granskning", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "E-post bekräftad", + "email-confirmed-message": "Tack för att du bekräftat din e-postadress. Ditt konto är nu fullt ut aktiverat.", + "email-confirm-error-message": "Det uppstod ett problem med bekräftelsen av din e-postadress. Kanske var koden felaktig eller ogiltig.", + "email-confirm-sent": "Bekräftelsemeddelande skickat.", + "none": "Inga", + "notification_only": "Endast notis", + "email_only": "Endast e-post", + "notification_and_email": "Notis och e-post", + "notificationType_upvote": "När någon röstar upp ditt inlägg", + "notificationType_new-topic": "När någon du följer skapar ett ämne", + "notificationType_new-reply": "När ett nytt svar skrivs inom ett ämne du följer", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "När någon börjar följa dig", + "notificationType_new-chat": "När du får ett chattmeddelande", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "När du får en gruppinbjudan", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "När någon ber om att få gå med i en grupp du äger", + "notificationType_new-register": "När någon läggs till i registreringskön", + "notificationType_post-queue": "När ett nytt inlägg läggs i kön", + "notificationType_new-post-flag": "När ett nytt inlägg flaggas", + "notificationType_new-user-flag": "När en användare flaggas" +} \ No newline at end of file diff --git a/public/language/sv/pages.json b/public/language/sv/pages.json new file mode 100644 index 0000000000..e6145ba0fd --- /dev/null +++ b/public/language/sv/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Hem", + "unread": "Olästa ämnen", + "popular-day": "Populära ämnen idag", + "popular-week": "Populära ämnen den här veckan", + "popular-month": "Populära ämnen denna månad", + "popular-alltime": "Populäraste ämnena genom tiderna", + "recent": "Senaste ämnena", + "top-day": "Toppröstade ämnen idag", + "top-week": "Toppröstade ämnen denna vecka", + "top-month": "Toppröstade ämnen denna månad", + "top-alltime": "Toppröstade ämnen", + "moderator-tools": "Moderatorverktyg", + "flagged-content": "Flaggat innehåll", + "ip-blacklist": "IP Svartlista", + "post-queue": "Inllägskö", + "users/online": "Användare online", + "users/latest": "Senaste Användare", + "users/sort-posts": "Användare med flest inlägg", + "users/sort-reputation": "Användare med bäst rykte", + "users/banned": "Bannlysta användare", + "users/most-flags": "Mest flaggade användare", + "users/search": "Användar Sök", + "notifications": "Notiser", + "tags": "Etiketter", + "tag": "Ämnen taggade med "%1"", + "register": "Registrera ett konto", + "registration-complete": "Registrering färdig", + "login": "Logga in på ditt konto", + "reset": "Återställ lösenord", + "categories": "Kategorier", + "groups": "Grupper", + "group": "%1 grupp", + "chats": "Chattar", + "chat": "Chattar med %1", + "flags": "Flaggor", + "flag-details": "Detaljer för flaggan %1", + "account/edit": "Redigerar \"%1\"", + "account/edit/password": "Redigerar lösenord för \"%1\"", + "account/edit/username": "Redigerar användarnamn för \"%1\"", + "account/edit/email": "Redigerar e-postadress för \"%1\"", + "account/info": "Konto", + "account/following": "Användare som %1 följer", + "account/followers": "Användare som följer %1", + "account/posts": "Inlägg skapade av %1", + "account/latest-posts": "Senaste inläggen av %1", + "account/topics": "Ämnen skapade av %1 ", + "account/groups": "%1's grupper", + "account/watched_categories": "%1's följda kategorier", + "account/bookmarks": "%1'st bokmärkta inlägg", + "account/settings": "Avnändarinställningar", + "account/watched": "Ämnen som bevakas av %1", + "account/ignored": "Ämnen som %1 ignorerar", + "account/upvoted": "Inlägg som röstats upp av %1", + "account/downvoted": "Inlägg som röstats ned av %1", + "account/best": "Bästa inläggen skapade av %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blockerade användare för %1", + "account/uploads": "Uppladdningar av %1", + "account/sessions": "Inloggningssessioner", + "confirm": "E-postadress bekräftad", + "maintenance.text": "%1 genomgår underhåll just nu. Vänligen kom tillbaka lite senare.", + "maintenance.messageIntro": "Utöver det så lämnade administratören följande meddelande:", + "throttled.text": "%1 ligger tillfälligt nere på grund av överbelastning. Var god återkom senare. " +} \ No newline at end of file diff --git a/public/language/sv/post-queue.json b/public/language/sv/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/sv/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/sv/recent.json b/public/language/sv/recent.json new file mode 100644 index 0000000000..0e75be8355 --- /dev/null +++ b/public/language/sv/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Senaste", + "day": "Dag", + "week": "Vecka", + "month": "Månad", + "year": "År", + "alltime": "Alltid", + "no_recent_topics": "Det finns inga olästa ämnen.", + "no_popular_topics": "Det finns inga populära ämnen.", + "there-is-a-new-topic": "Det finns ett nytt ämne.", + "there-is-a-new-topic-and-a-new-post": "Det finns ett nytt ämne och ett nytt inlägg.", + "there-is-a-new-topic-and-new-posts": "Det finns ett nytt ämne och %1 nya inlägg.", + "there-are-new-topics": "Det finns %1 nya ämnen.", + "there-are-new-topics-and-a-new-post": "Det finns %1 nya ämnen och ett nytt inlägg.", + "there-are-new-topics-and-new-posts": "Det finns %1 nya ämnen och %2 nya inlägg.", + "there-is-a-new-post": "Det finns ett nytt inlägg.", + "there-are-new-posts": "Det finns %1 nya inlägg.", + "click-here-to-reload": "Klicka här för att ladda om." +} \ No newline at end of file diff --git a/public/language/sv/register.json b/public/language/sv/register.json new file mode 100644 index 0000000000..f38cd30562 --- /dev/null +++ b/public/language/sv/register.json @@ -0,0 +1,32 @@ +{ + "register": "Registrera", + "cancel_registration": "Avbryt registrering", + "help.email": "Som standard, är din e-postadress dold för allmänheten.", + "help.username_restrictions": "Ett unikt användarnamn mellan %1 och %2 bokstäver. Andra kan nämna dig med @användarnamn.", + "help.minimum_password_length": "Ditt lösenord måste vara minst %1 bokstäver.", + "email_address": "E-postadress", + "email_address_placeholder": "Ange E-postadress", + "username": "Användarnamn", + "username_placeholder": "Ange användarnamn", + "password": "Lösenord", + "password_placeholder": "Ange lösenord", + "confirm_password": "Bekräfta lösenord", + "confirm_password_placeholder": "Bekräfta lösenord", + "register_now_button": "Registrera nu", + "alternative_registration": "Alternativ registrering", + "terms_of_use": "Användarvillkor", + "agree_to_terms_of_use": "Jag godkänner användarvillkoren", + "terms_of_use_error": "Du måste godkänna användarvillkoren", + "registration-added-to-queue": "Din registrering har lagts till i kön. Du kommer att få ett mail när den accepteras av en administratör.", + "registration-queue-average-time": "Snittiden för att godkänna medlemskap är %1 timmar %2 minuter.", + "registration-queue-auto-approve-time": "Ditt medlemskap på forumet kommer godkännas inom %1 timmar.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Vänligen gå igenom den angivna informationen:", + "gdpr_agree_data": "Jag ger webbsidan mitt medgivande att samla in och behandla min personliga information.", + "gdpr_agree_email": "Jag går med på att få sammanfattningar och notiser från detta forum via e-post.", + "gdpr_consent_denied": "Du måste ge ditt medgivande för att detta forum ska kunna samla in och behandla din information, samt skicka dig e-post.", + "invite.error-admin-only": "Direkt användarregistrering har avaktiverats. Vänligen kontakta en administratör för mer information.", + "invite.error-invite-only": "Direkt användarregistrering har avaktiverats. Du måste bli inbjuden av en existerande användare för att få tillgång till forumet.", + "invite.error-invalid-data": "Erhållen registreringsdata stämmer inte överens med våra uppgifter. Vänligen kontakta en administratör för mer information." +} \ No newline at end of file diff --git a/public/language/sv/reset_password.json b/public/language/sv/reset_password.json new file mode 100644 index 0000000000..71c6396013 --- /dev/null +++ b/public/language/sv/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Återställ lösenord", + "update_password": "Uppdatera lösenord", + "password_changed.title": "Lösenordet ändrat", + "password_changed.message": "

Lösenordet återställt, var god logga in igen.", + "wrong_reset_code.title": "Felaktig återställnings-kod", + "wrong_reset_code.message": "Den mottagna återställningskoden var felaktig. Var god försök igen, eller begär en ny återställningskod.", + "new_password": "Nytt lösenord", + "repeat_password": "Bekräfta lösenord", + "changing_password": "Ändrar lösenord", + "enter_email": "Var god fyll i din e-postadress så skickas ett e-postmeddelande med instruktioner hur du återställer ditt konto.", + "enter_email_address": "Skriv in e-postadress", + "password_reset_sent": "Om den angivna adressen motsvarar ett existerande användarkonto så skickas en epost med en lösenordsåterställning. Vänligen notera att endast en epost per minut kan skickas.", + "invalid_email": "Felaktig e-post / E-post finns inte!", + "password_too_short": "Lösenordet är för kort, var god välj ett annat lösenord.", + "passwords_do_not_match": "De två lösenorden du har fyllt i matchar ej varandra.", + "password_expired": "Ditt lösenord har gått ut, var god välj ett nytt lösenord." +} \ No newline at end of file diff --git a/public/language/sv/search.json b/public/language/sv/search.json new file mode 100644 index 0000000000..4927229237 --- /dev/null +++ b/public/language/sv/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 resultat matchar \"%2\", (%3 sekunder)", + "no-matches": "Inga träffar", + "advanced-search": "Avancerad sökning", + "in": "i", + "titles": "Ämnen", + "titles-posts": "Ämnen och inlägg", + "match-words": "Matcha ord", + "all": "Alla", + "any": "Någon", + "posted-by": "Skapad av", + "in-categories": "I kategorier", + "search-child-categories": "Sök i underkategorier", + "has-tags": "Har taggar", + "reply-count": "Svarsantal", + "at-least": "Som minst", + "at-most": "Som mest", + "relevance": "Relevans", + "post-time": "Inläggstid", + "votes": "Röster", + "newer-than": "Yngre än", + "older-than": "Äldre än", + "any-date": "Alla datum", + "yesterday": "Igår", + "one-week": "En vecka", + "two-weeks": "Två veckor", + "one-month": "En månad", + "three-months": "Tre månader", + "six-months": "Sex månader", + "one-year": "Ett år", + "sort-by": "Sortera på", + "last-reply-time": "Senaste svarstiden", + "topic-title": "Ämnestitel", + "topic-votes": "Ämnesröster", + "number-of-replies": "Antal svar", + "number-of-views": "Antal visningar", + "topic-start-date": "Startdatum för ämne", + "username": "Användarnamn", + "category": "Kategori", + "descending": "I fallande ordning", + "ascending": "I stigande ordning", + "save-preferences": "Spara inställningar", + "clear-preferences": "Rensa inställningar", + "search-preferences-saved": "Sökinställningar sparade", + "search-preferences-cleared": "Sökinställningar rensade", + "show-results-as": "Visa resultat som", + "see-more-results": "Se fler resultat (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/sv/success.json b/public/language/sv/success.json new file mode 100644 index 0000000000..d938c2c9f7 --- /dev/null +++ b/public/language/sv/success.json @@ -0,0 +1,7 @@ +{ + "success": "Lyckat", + "topic-post": "Du har nu gjort ett inlägg.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Autentisering lyckades", + "settings-saved": "Inställningarna sparades." +} \ No newline at end of file diff --git a/public/language/sv/tags.json b/public/language/sv/tags.json new file mode 100644 index 0000000000..ea0f405069 --- /dev/null +++ b/public/language/sv/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Det finns inga ämnen med denna tagg.", + "tags": "Taggar", + "enter_tags_here": "Fyll i taggar på %1 till %2 tecken här.", + "enter_tags_here_short": "Ange taggar...", + "no_tags": "Det finns inga taggar ännu.", + "select_tags": "Välj Etiketter" +} \ No newline at end of file diff --git a/public/language/sv/top.json b/public/language/sv/top.json new file mode 100644 index 0000000000..9d7bf37fd3 --- /dev/null +++ b/public/language/sv/top.json @@ -0,0 +1,4 @@ +{ + "title": "Topp", + "no_top_topics": "Inga toppämnen" +} \ No newline at end of file diff --git a/public/language/sv/topic.json b/public/language/sv/topic.json new file mode 100644 index 0000000000..166760d744 --- /dev/null +++ b/public/language/sv/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Ämne", + "title": "Title", + "no_topics_found": "Inga ämnen hittades!", + "no_posts_found": "Inga inlägg hittades!", + "post_is_deleted": "Detta inlägg är raderat!", + "topic_is_deleted": "Detta ämne är raderat!", + "profile": "Profil", + "posted_by": "Skapat av %1", + "posted_by_guest": "Inlägg av anonym", + "chat": "Chatt", + "notify_me": "Få notiser om nya svar i detta ämne", + "quote": "Citera", + "reply": "Svara", + "replies_to_this_post": "%1 svar", + "one_reply_to_this_post": "Ett svar", + "last_reply_time": "Senaste svaret", + "reply-as-topic": "Svara som ämne", + "guest-login-reply": "Logga in för att posta", + "login-to-view": "🔒 Logga in för att visa", + "edit": "Ändra", + "delete": "Ta bort", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Rensa", + "restore": "Återställ", + "move": "Flytta", + "change-owner": "Ändra ägare", + "fork": "Grena", + "link": "Länk", + "share": "Dela", + "tools": "Verktyg", + "locked": "Låst", + "pinned": "Fäst", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Flyttad", + "moved-from": "Moved from %1", + "copy-ip": "Kopiera IP", + "ban-ip": "Banna IP", + "view-history": "Redigera historik", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Klicka här för att återgå till senast lästa inlägg i detta ämne.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Det här ämnet har raderats. Endast användare med ämneshanterings-privilegier kan se det.", + "following_topic.message": "Du kommer nu få notiser när någon gör inlägg i detta ämne.", + "not_following_topic.message": "Du kommer att se det här ämnet i listan olästa ämnen, men du kommer inte att få meddelande när någon gör inlägg till detta ämne.", + "ignoring_topic.message": "Du kommer inte längre se detta ämne i listan olästa ämnen. Du kommer att meddelas när du nämns eller ditt inlägg är upp röstat.", + "login_to_subscribe": "Var god registrera eller logga in för att kunna prenumerera på detta ämne.", + "markAsUnreadForAll.success": "Ämne markerat som oläst av alla.", + "mark_unread": "Markera som oläst", + "mark_unread.success": "Ämne markerat som oläst.", + "watch": "Bevaka", + "unwatch": "Sluta bevaka", + "watch.title": "Få notis om nya svar till det här ämnet", + "unwatch.title": "Sluta bevaka detta ämne", + "share_this_post": "Dela detta inlägg", + "watching": "Bevakar", + "not-watching": "Bevakar inte", + "ignoring": "Ignorerar", + "watching.description": "Meddela mig om nya svar.
Visa ämne i oläst.", + "not-watching.description": "Meddela mig inte om nya svar.
Visa ämne i oläst ifall kategorin är ignorerad.", + "ignoring.description": "Meddela mig inte om nya svar.
Visa inte ämne i oläst.", + "thread_tools.title": "Ämnesverktyg", + "thread_tools.markAsUnreadForAll": "Markera oläst för alla", + "thread_tools.pin": "Nåla fast ämne", + "thread_tools.unpin": "Lösgör ämne", + "thread_tools.lock": "Lås ämne", + "thread_tools.unlock": "Lås upp ämne", + "thread_tools.move": "Flytta ämne", + "thread_tools.move-posts": "Flytta inlägg", + "thread_tools.move_all": "Flytta alla", + "thread_tools.change_owner": "Ändra ägare", + "thread_tools.select_category": "Välj kategori", + "thread_tools.fork": "Grena ämne", + "thread_tools.delete": "Ta bort ämne", + "thread_tools.delete-posts": "Radera inlägg", + "thread_tools.delete_confirm": "Är du säker på att du vill ta bort det här ämnet?", + "thread_tools.restore": "Återställ ämne", + "thread_tools.restore_confirm": "Är du säker på att du vill återställa det här ämnet?", + "thread_tools.purge": "Rensa bort ämne", + "thread_tools.purge_confirm": "Är du säker att du vill rensa bort det här ämnet?", + "thread_tools.merge_topics": "Slå samman ämnen", + "thread_tools.merge": "Slå samman", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Är du säker på att du vill ta bort det här inlägget?", + "post_restore_confirm": "Är du säker på att du vill återställa det här inlägget?", + "post_purge_confirm": "Är du säker att du vill rensa bort det här inlägget?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Laddar kategorier", + "confirm_move": "Flytta", + "confirm_fork": "Grena", + "bookmark": "Bokmärke", + "bookmarks": "Bokmärken", + "bookmarks.has_no_bookmarks": "Du har inte bokmärkt några inlägg ännu.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Laddar fler inlägg", + "move_topic": "Flytta ämne", + "move_topics": "Flytta ämnen", + "move_post": "Flytta inlägg", + "post_moved": "Inlägget flyttades.", + "fork_topic": "Grena ämne", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Klicka på de inlägg du vill grena", + "fork_no_pids": "Inga inlägg valda!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "%1 inlägg vald(a)", + "fork_success": "Ämnet har blivit förgrenat. Klicka här för att gå till det förgrenade ämnet.", + "delete_posts_instruction": "Klicka på inläggen du vill radera/rensa bort", + "merge_topics_instruction": "Klicka på de ämen du vill slå ihop eller sök efter dem", + "merge-topic-list-title": "Lista av ämnen att slå ihop", + "merge-options": "Ihopslagningsverktyg", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "Ny titel för ämne", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Klicka på de inlägg du vill tilldela en annan användare", + "composer.title_placeholder": "Skriv in ämnets titel här...", + "composer.handle_placeholder": "Skriv ditt namn/användarnamn här", + "composer.discard": "Avbryt", + "composer.submit": "Skicka", + "composer.additional-options": "Ytterligare val", + "composer.schedule": "Schemalägg", + "composer.replying_to": "Svarar till %1", + "composer.new_topic": "Nytt ämne", + "composer.editing": "Redigerar", + "composer.uploading": "laddar upp...", + "composer.thumb_url_label": "Klistra in URL till tumnagel för ämnet", + "composer.thumb_title": "Lägg till tumnagel för detta ämne", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Eller ladda upp en fil", + "composer.thumb_remove": "Töm fält", + "composer.drag_and_drop_images": "Dra och släpp bilder här", + "more_users_and_guests": "%1 fler användare och %2 gäst(er)", + "more_users": "%1 fler användare", + "more_guests": "1% fler gäst(er)", + "users_and_others": "%1 och %2 andra", + "sort_by": "Sortera på", + "oldest_to_newest": "Äldst till nyaste", + "newest_to_oldest": "Nyaste till äldst", + "most_votes": "Flest röster", + "most_posts": "Flest inlägg", + "most_views": "Most Views", + "stale.title": "Skapa nytt ämne istället?", + "stale.warning": "Ämnet du svarar på är ganska gammalt. Vill du skapa ett nytt ämne istället och inkludera en referens till det här ämnet i ditt inlägg?", + "stale.create": "Skapa nytt ämne", + "stale.reply_anyway": "Svara på ämnet ändå", + "link_back": "Re: [%1](%2)", + "diffs.title": "Redigeringshistorik för post", + "diffs.description": "Detta inlägg har %1 revisioner. Klicka på en av revisionerna nedan för att se det dåvarande innehållet i inlägget.", + "diffs.no-revisions-description": "Detta inlägg har %1 revisioner.", + "diffs.current-revision": "Nuvarande revision", + "diffs.original-revision": "Ursprunglig revision", + "diffs.restore": "Återskapa den här ändringen", + "diffs.restore-description": "En ny ändring kommer läggas till i det här inläggets redigeringshistorik efter återskapning.", + "diffs.post-restored": "Inlägg lyckades återskapas till tidigare redigering", + "diffs.delete": "Ta bort den här redigeringen", + "diffs.deleted": "Redigering borttagen", + "timeago_later": "%1 senare", + "timeago_earlier": "%1 tidigare", + "first-post": "Första inlägget", + "last-post": "Sista inlägget", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Gör ett snabbsvar" +} \ No newline at end of file diff --git a/public/language/sv/unread.json b/public/language/sv/unread.json new file mode 100644 index 0000000000..9b529c7249 --- /dev/null +++ b/public/language/sv/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Olästa", + "no_unread_topics": "Det finns inga olästa ämnen.", + "load_more": "Ladda fler", + "mark_as_read": "Markera som läst", + "selected": "Vald", + "all": "Alla", + "all_categories": "Alla kategorier", + "topics_marked_as_read.success": "Ämnet markerat som läst.", + "all-topics": "Alla ämnen", + "new-topics": "Nya ämnen", + "watched-topics": "Bevakade ämnen", + "unreplied-topics": "Obesvarade ämnen", + "multiple-categories-selected": "Flera valda" +} \ No newline at end of file diff --git a/public/language/sv/uploads.json b/public/language/sv/uploads.json new file mode 100644 index 0000000000..96a34f1ab8 --- /dev/null +++ b/public/language/sv/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Laddar upp filen...", + "select-file-to-upload": "Välj en fil att ladda upp!", + "upload-success": "Filen laddades upp!", + "maximum-file-size": "Maximalt %1 kb", + "no-uploads-found": "Inga uppladdningar funna", + "public-uploads-info": "Uppladdningar är publikt tillgängliga, alla forumbesökare kan se dem.", + "private-uploads-info": "Uppladdningar är privata, endast inloggade användare kan se dem." +} \ No newline at end of file diff --git a/public/language/sv/user.json b/public/language/sv/user.json new file mode 100644 index 0000000000..9f87b40310 --- /dev/null +++ b/public/language/sv/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Bannlyst", + "muted": "Muted", + "offline": "Offline", + "deleted": "Raderad", + "username": "Användarnamn", + "joindate": "Gick med", + "postcount": "Antal inlägg", + "email": "E-post", + "confirm_email": "Bekräfta e-postadress ", + "account_info": "Konto", + "admin_actions_label": "Administrative Actions", + "ban_account": "Bannlys konto", + "ban_account_confirm": "Vill du verkligen bannlysa den här användaren?", + "unban_account": "Ta bort bannlysning", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Ta bort ämne", + "delete_account_as_admin": "Radera Konto", + "delete_content": "Radera Kontots Innehåll", + "delete_all": "Radera Konto samt Kontots Innehåll ", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Kontot raderat", + "account-content-deleted": "Account content deleted", + "fullname": "Hela namnet", + "website": "Webbsida", + "location": "Plats", + "age": "Ålder", + "joined": "Gick med", + "lastonline": "Senast online", + "profile": "Profil", + "profile_views": "Profil-visningar", + "reputation": "Rykte", + "bookmarks": "Bokmärken", + "watched_categories": "Bevakade kategorier", + "change_all": "Ändra alla", + "watched": "Bevakad", + "ignored": "Ignorerad", + "default-category-watch-state": "Förvalt bevakningsläge för kategori", + "followers": "Följare", + "following": "Följer", + "blocks": "Blockerar", + "block_toggle": "Ändra blockeringsinställning", + "block_user": "Blockera användare", + "unblock_user": "Sluta blockera användare", + "aboutme": "Om mig", + "signature": "Signatur", + "birthday": "Födelsedag", + "chat": "Chatta", + "chat_with": "Fortsätt chatt med %1", + "new_chat_with": "Påbörja ny chatt med %1", + "flag-profile": "Flagga profil", + "follow": "Följ", + "unfollow": "Sluta följ", + "more": "Mer", + "profile_update_success": "Profilen uppdaterades.", + "change_picture": "Ändra bild", + "change_username": "Ändra användarnamn", + "change_email": "Ändra e-postadress", + "email_same_as_password": "Vänligen skriv ditt lösenord flr att fortsätta – du har angett din nya epost igen", + "edit": "Ändra", + "edit-profile": "Redigera profil", + "default_picture": "Standard-ikon", + "uploaded_picture": "Uppladdad bild", + "upload_new_picture": "Ladda upp ny bild", + "upload_new_picture_from_url": "Ladda upp ny bild via länk", + "current_password": "Nuvarande lösenord", + "change_password": "Ändra lösenord", + "change_password_error": "Ogiltigt lösenord.", + "change_password_error_wrong_current": "Ditt nuvarande lösenord är inte korrekt.", + "change_password_error_match": "Lösenorden måste stämma överens.", + "change_password_error_privileges": "Du har inte rättigheter att ändra det här lösenordet.", + "change_password_success": "Ditt lösenord är uppdaterat.", + "confirm_password": "Bekräfta lösenord", + "password": "Lösenord", + "username_taken_workaround": "Användarnamnet är redan upptaget, så vi förändrade det lite. Du kallas nu för %1", + "password_same_as_username": "Ditt lösenord är samma som ditt användarnamn, välj ett annat lösenord.", + "password_same_as_email": "Ditt lösenord är detsamma som din e-postadress. Var god välj ett annat lösenord.", + "weak_password": "Svagt lösenord", + "upload_picture": "Ladda upp bild", + "upload_a_picture": "Ladda upp en bild", + "remove_uploaded_picture": "Ta bort uppladdad bild", + "upload_cover_picture": "Ladda upp omslagsbild", + "remove_cover_picture_confirm": "Är du säker att du vill radera profilbilden?", + "crop_picture": "Beskär bild", + "upload_cropped_picture": "Beskär och ladda upp", + "avatar-background-colour": "Avatar background colour", + "settings": "Inställningar", + "show_email": "Visa min e-postadress", + "show_fullname": "Visa fullständigt namn", + "restrict_chats": "Tillåt endast chatt-meddelanden från användare som jag följer", + "digest_label": "Prenumerera på sammanställt flöde", + "digest_description": "Prenumerera på e-postuppdateringar för det här forumet (notiser och ämnen) med en viss regelbundenhet", + "digest_off": "Avslagen", + "digest_daily": "Dagligen", + "digest_weekly": "Veckovis", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Månadsvis", + "has_no_follower": "Denna användare har inga följare :(", + "follows_no_one": "Denna användare följer ingen :(", + "has_no_posts": "Användaren har inte skrivit några inlägg ännu.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Användaren har inte postat några ämnen ännu.", + "has_no_watched_topics": "Användaren har inte bevakat några ämnen ännu.", + "has_no_ignored_topics": "Denna användare ignorerar inte några ämnen ännu.", + "has_no_upvoted_posts": "Den här användaren har inte röstat upp några inlägg än.", + "has_no_downvoted_posts": "Den här användaren har inte röstat ned några inlägg än.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Du har inte blockerat några användare.", + "email_hidden": "E-post dold", + "hidden": "dold", + "paginate_description": "Gör så att ämnen och inlägg visas som sidor istället för oändlig skroll", + "topics_per_page": "Ämnen per sida", + "posts_per_page": "Inlägg per sida", + "max_items_per_page": "Maximalt %1", + "acp_language": "Språk för administratörssida", + "notifications": "Notifications", + "upvote-notif-freq": "Notisfrekvens för uppröstningar", + "upvote-notif-freq.all": "Alla uppröstningar", + "upvote-notif-freq.first": "Första per post", + "upvote-notif-freq.everyTen": "Var tionde post", + "upvote-notif-freq.threshold": "Varje 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Varje 10, 100, 1000...", + "upvote-notif-freq.disabled": "Avstängd", + "browsing": "Inställning för bläddring", + "open_links_in_new_tab": "Öppna utgående länkar i ny flik", + "enable_topic_searching": "Aktivera sökning inom ämne", + "topic_search_help": "Om aktiverat kommer sökning inom ämne överskrida webbläsarens vanliga funktionen för sökning bland sidor och tillåta dig att söka genom hela ämnet istället för det som endast visas på skärmen.", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Visa det nya inlägget när ett svar har postats", + "follow_topics_you_reply_to": "Bevaka ämnen som du svarat på", + "follow_topics_you_create": "Bevaka ämnen som du skapat", + "grouptitle": "Grupptitel", + "group-order-help": "Välj en grupp och använd piltangenterna för att ordna rubriker", + "no-group-title": "Ingen titel på gruppen", + "select-skin": "Välj ett Skin", + "select-homepage": "Välj en startsida", + "homepage": "Startsida", + "homepage_description": "Välj en sida som ska användas som forumets startsida eller 'Ingen' för att använda standardstartsidan.", + "custom_route": "Sökväg till egen startsida", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Single Sign-on-tjänster", + "sso.associated": "Associerad med", + "sso.not-associated": "Klicka här för att associera med", + "sso.dissociate": "Frånkoppla", + "sso.dissociate-confirm-title": "Bekräfta frånkoppling", + "sso.dissociate-confirm": "Är du säker att du vill koppla bort ditt konto från %1?", + "info.latest-flags": "Senaste flaggade", + "info.no-flags": "Inga flaggade inlägg hittades", + "info.ban-history": "Ban historik", + "info.no-ban-history": "Den här användaren har aldrig varit bannad", + "info.banned-until": "Bannad tills %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Permanent bannad", + "info.banned-reason-label": "Anledning", + "info.banned-no-reason": "Ingen anledning angiven", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Användarnamn historik", + "info.email-history": "Epost historik", + "info.moderation-note": "Moderations anteckning", + "info.moderation-note.success": "Moderations anteckning sparad", + "info.moderation-note.add": "Lägg till anteckning", + "sessions.description": "Denna sida låter dig se det här forumets alla aktiva sessioner och återkalla dem om den behövs. Du kan återkalla din egen session genom att logga ut från ditt eget konto.", + "consent.title": "Dina rättigheter och Medgivande", + "consent.lead": "Detta forum samlar och behandlar din personliga information.", + "consent.intro": "Vi använder denna information endast i syfte att personligt anpassa din upplevelse i denna gemenskap, och för att associera de inlägg du gör till ditt användarkonto. När du registrerade dig blev du ombedd att ange ett användarnamn och en e-postadress, du kan även frivilligt ange ytterligare information för att komplettera din användarprofil på denna webbsida.

Vi sparar denna information så länge som du har ett konto hos oss, och du kan dra tillbaks ditt medgivande när som helst genom att radera ditt konto. Du kan är som helst efterfråga en kopia av din information på denna webbplats, genom sidan Rättigheter och Medgivande.

Om du har några frågor eller funderingar rekommenderar vi att du tar kontakt med detta forums administrativa team.", + "consent.email_intro": "Vi kan då och då skicka e-post till din registrerade e-postadress för att meddela om uppdateringar och/eller informera dig om ny aktivitet som är relevant för dig. Du kan ändra hur ofta vi skickar forumsammanfattningar (eller stänga av det helt), och ändra vilka sorters uppdateringar du vill få genom dina användarinställningar.", + "consent.digest_frequency": "Såvida du inte ändrat detta i dina användarinställningar, skickar detta forum e-postsammanfattningar varje %1", + "consent.digest_off": "Såvida du inte ändrat detta i dina användarinställningar, skickar detta forum inte ut e-postsammanfattningar", + "consent.received": "Du har givit denna webbsida medgivande att samla in och behandla din information. Inga ytterligare handlingar krävs.", + "consent.not_received": "Du har inte givit medgivande för datainsamling och -behandling. Denna webbsidas administration kan närsomhelst besluta att radera ditt konto för att uppfylla GDPR.", + "consent.give": "Ge medgivande", + "consent.right_of_access": "Du har rätten till tillgång", + "consent.right_of_access_description": "Du har rätten att hämta all data denna webbsida samlar om dig när som helst. Du han hämta en kopia av denna data genom att trycka på knappen nedan.", + "consent.right_to_rectification": "Du har rätten till rättelse", + "consent.right_to_rectification_description": "Du har rätt att ändra eller uppdatera all inkorrekt data som du har givit oss. Din profil kan uppdateras genom att ändra din profil, och innehåll i poster kan alltid redigeras. Om detta inte skulle vara fallet, vänligen kontakta detta forums administrativa team.", + "consent.right_to_erasure": "Du har rätt till borttagning", + "consent.right_to_erasure_description": "Du kan när som helst ta tillbaka ditt medgivande till datainsamling och/eller behandling genom att radera ditt konto. Din individuella profil kan raderas, men innehåll du lagt upp kommer bestå. Om du vill radera både din profiloch ditt innehåll, vänligen kontakta detta forums administrativa team.", + "consent.right_to_data_portability": "Du har rätten till dataförflyttbarhet", + "consent.right_to_data_portability_description": "Du kan hämta en maskinläslig export av all insamlad data om dig och ditt konto. Du kan göra det genom att klicka på passande knapp nedan.", + "consent.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Exportera uppladdat innehåll (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Exportera poster (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/sv/users.json b/public/language/sv/users.json new file mode 100644 index 0000000000..c1247acae0 --- /dev/null +++ b/public/language/sv/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Senaste användarna", + "top_posters": "Flest inlägg", + "most_reputation": "Bäst rykte", + "most_flags": "Mest flaggade", + "search": "Sök", + "enter_username": "Ange ett användarnamn för att söka", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Ladda fler", + "users-found-search-took": "%1 användare hittades! Sökningen tog %2 sekunder.", + "filter-by": "Filtrera på", + "online-only": "Endast online", + "invite": "Bjud in", + "prompt-email": "Emails:", + "groups-to-join": "Grupper att ansluta till när inbjudan är accepterad:", + "invitation-email-sent": "En inbjudan har skickats till %1", + "user_list": "Användarlista", + "recent_topics": "Senaste ämnen", + "popular_topics": "Populära ämnen", + "unread_topics": "Olästa ämnen", + "categories": "Kategorier", + "tags": "Taggar", + "no-users-found": "Inga användare hittades!" +} \ No newline at end of file diff --git a/public/language/th/_DO_NOT_EDIT_FILES_HERE.md b/public/language/th/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/th/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/th/admin/admin.json b/public/language/th/admin/admin.json new file mode 100644 index 0000000000..88cd1451ae --- /dev/null +++ b/public/language/th/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Are you sure you wish to rebuild and restart NodeBB?", + "alert.confirm-restart": "คุณต้องการเริ่มการทำงาน NodeBB ใหม่หรือไม่?", + + "acp-title": "%1 | แผงควบคุมของผู้ดูแลระบบ", + "settings-header-contents": "เนื้อหา", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/th/admin/advanced/cache.json b/public/language/th/admin/advanced/cache.json new file mode 100644 index 0000000000..c751c880b0 --- /dev/null +++ b/public/language/th/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "แคชข้อความ", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "เต็ม %1%", + "post-cache-size": "ขนาดแคชของข้อความ", + "items-in-cache": "รายการที่ถูกแคช" +} \ No newline at end of file diff --git a/public/language/th/admin/advanced/database.json b/public/language/th/admin/advanced/database.json new file mode 100644 index 0000000000..9f196b465b --- /dev/null +++ b/public/language/th/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "ระยะเวลาทำงานต่อเนื่องเป็นวินาที", + "uptime-days": "ระยะเวลาทำงานต่อเนื่องเป็นวัน", + + "mongo": "Mongo", + "mongo.version": "MongoDB เวอร์ชั่น", + "mongo.storage-engine": "ระบบการจัดเก็บ", + "mongo.collections": "คอลเลคชัน", + "mongo.objects": "ออพเจ็กท์", + "mongo.avg-object-size": "ขนาดออพเจ็กท์โดยเฉลี่ย", + "mongo.data-size": "ขนาดข้อมูล", + "mongo.storage-size": "ขนาดพื้นที่จัดเก็บ", + "mongo.index-size": "ขนาดดัชนี", + "mongo.file-size": "ขนาดไฟล์", + "mongo.resident-memory": "หน่วยความจำถาวร", + "mongo.virtual-memory": "หน่วยความจำเสมือน", + "mongo.mapped-memory": "เส้นทางหน่วยความจำ", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "ข้อมูลดิบของ MongoDB", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis เวอร์ชั่น", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "ไคลเอ็นท์ที่เชื่อมต่อแล้ว", + "redis.connected-slaves": "Slaves ที่เชื่อมต่อแล้ว", + "redis.blocked-clients": "ไคลเอ็นท์ที่ถูกบล็อค", + "redis.used-memory": "หน่วยความจำที่ถูกใช้", + "redis.memory-frag-ratio": "อัตราการกระจายตัวของหน่วยความจำ", + "redis.total-connections-recieved": "การเชื่อมต่อที่ได้รับทั้งหมด", + "redis.total-commands-processed": "คำสั่งที่ประมวลผลแล้วทั้งหมด", + "redis.iops": "การทำงานพร้อมกันต่อวินาที", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "ข้อมูลดิบของ Redis", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/th/admin/advanced/errors.json b/public/language/th/admin/advanced/errors.json new file mode 100644 index 0000000000..354bdc8fda --- /dev/null +++ b/public/language/th/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "การปรับแต่ง %1", + "error-events-per-day": "%1 อีเวนท์ต่อวัน", + "error.404": "404 ไม่พบเพจ", + "error.503": "503 เซอร์วิสไม่พร้อมใช้งาน", + "manage-error-log": "จัดการผลบันทึกความผิดพลาด", + "export-error-log": "นำออกผลบันทึกความผิดพลาด (CSV)", + "clear-error-log": "ล้างผลบันทึกความผิดพลาด", + "route": "เส้นทาง", + "count": "นับจำนวน", + "no-routes-not-found": "เอาแล้วซิ! พบความผิดพลาดรหัส 404", + "clear404-confirm": "คุณแน่ใจแล้วใช่ไหมว่าต้องการล้างผลบันทึกความผิดพลาดรหัส 404?", + "clear404-success": "บันทึกความผิดพลาด \"404 ไม่พบเพจ\" ถูกล้างเรียบร้อยแล้ว" +} \ No newline at end of file diff --git a/public/language/th/admin/advanced/events.json b/public/language/th/admin/advanced/events.json new file mode 100644 index 0000000000..1cd0c87bb1 --- /dev/null +++ b/public/language/th/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "อีเวนท์", + "no-events": "ไม่มีอีเวนท์", + "control-panel": "แผงควบคุมอีเวนท์", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/th/admin/advanced/logs.json b/public/language/th/admin/advanced/logs.json new file mode 100644 index 0000000000..ba7e879560 --- /dev/null +++ b/public/language/th/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "บันทึกผลกิจกรรม", + "control-panel": "แผงควบคุมบันทึกผลกิจกรรม", + "reload": "โหลดบันทึกผลกิจกรรมอีกครั้ง", + "clear": "ล้างบันทึกผลกิจกรรม", + "clear-success": "ล้างบันทึกผลกิจกรรมแล้ว!" +} \ No newline at end of file diff --git a/public/language/th/admin/appearance/customise.json b/public/language/th/admin/appearance/customise.json new file mode 100644 index 0000000000..bcb4ebc4af --- /dev/null +++ b/public/language/th/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "กำหนด CSS / LESS เอง", + "custom-css.description": "ใส่ CSS/LESS ของคุณที่นี่, มันจะถูกนำไปใช้ต่อจากสไตล์อื่นๆ", + "custom-css.enable": "เปิดการปรับแต่ง CSS/LESS", + + "custom-js": "ปรับแต่งจาวาสคริปต์", + "custom-js.description": "ป้อนจาวาสคริปต์ของคุณเองที่นี่ จะดำเนินการหลังจากโหลดหน้าเว็บเสร็จสมบูรณ์แล้ว", + "custom-js.enable": "เปิดการปรับแต่งจาวาสคริปต์", + + "custom-header": "ปรับแต่งส่วนหัว", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "เปิดการปรับแต่งส่วนหัว", + + "custom-css.livereload": "เปิดการบังคับให้มีผลในทันที", + "custom-css.livereload.description": "การเปิดนี้จะบังคับทุกเซสชั่นบนทุกอุปกรณ์ภายใต้บัญชีของคุณให้ถูกรีเฟรชทันทีที่คุณกดบันทึก" +} \ No newline at end of file diff --git a/public/language/th/admin/appearance/skins.json b/public/language/th/admin/appearance/skins.json new file mode 100644 index 0000000000..7a3c23814a --- /dev/null +++ b/public/language/th/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "กำลังโหลดหน้ากาก", + "homepage": "หน้าแรก", + "select-skin": "เลือกหน้ากาก", + "current-skin": "หน้ากากปัจจุบัน", + "skin-updated": "หน้ากากถูกอัปเดทแล้ว", + "applied-success": "%1 หน้ากากถูกใช้เสร็จสิ้นแล้ว", + "revert-success": "หน้ากากถูกทำให้ย้อนกลับไปใช้สีพื้นฐานแล้ว" +} \ No newline at end of file diff --git a/public/language/th/admin/appearance/themes.json b/public/language/th/admin/appearance/themes.json new file mode 100644 index 0000000000..589f054c4a --- /dev/null +++ b/public/language/th/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "กำลังตรวจสอบธีมที่ถูกติดตั้งแล้ว", + "homepage": "หน้าแรก", + "select-theme": "เลือกธีม", + "current-theme": "ธีมปัจจุบัน", + "no-themes": "ไม่พบธีมที่ถูกติดตั้งแล้ว", + "revert-confirm": "คุณแน่ใจแล้วใช่ไหมที่ต้องการกลับไปใช้ธีมพื้นฐานของ NodeBB?", + "theme-changed": "ธีมถูกเปลี่ยนแล้ว", + "revert-success": "คุณได้ทำการเปลี่ยน NodeBB ของคุณให้กลับไปใช้ธีมพื้นฐานของมันแล้ว", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/th/admin/dashboard.json b/public/language/th/admin/dashboard.json new file mode 100644 index 0000000000..4d39626882 --- /dev/null +++ b/public/language/th/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "Page Views", + "unique-visitors": "Unique Visitors", + "logins": "Logins", + "new-users": "New Users", + "posts": "Posts", + "topics": "Topics", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "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 YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "All Time", + + "updates": "Updates", + "running-version": "You are running NodeBB v%1.", + "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", + "up-to-date": "

You are up-to-date

", + "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", + "latest-lookup-failed": "

Failed to look up latest available version of NodeBB

", + + "notices": "Notices", + "restart-not-required": "Restart not required", + "restart-required": "Restart required", + "search-plugin-installed": "Search Plugin 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", + + "control-panel": "System Control", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "Maintenance Mode", + "maintenance-mode-title": "Click here to set up maintenance mode for NodeBB", + "realtime-chart-updates": "Realtime Chart Updates", + + "active-users": "Active Users", + "active-users.users": "Users", + "active-users.guests": "Guests", + "active-users.total": "Total", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Registered", + + "user-presence": "User Presence", + "on-categories": "On categories list", + "reading-posts": "Reading posts", + "browsing-topics": "Browsing topics", + "recent": "Recent", + "unread": "Unread", + + "high-presence-topics": "High Presence Topics", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Page Views", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Unique Visitors", + "graphs.registered-users": "Registered Users", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/th/admin/development/info.json b/public/language/th/admin/development/info.json new file mode 100644 index 0000000000..504759aa23 --- /dev/null +++ b/public/language/th/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes ตอบสนองแล้วภายใน %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "ระยะเวลาการทำงาน", + + "registered": "ลงทะเบียนแล้ว", + "sockets": "Sockets", + "guests": "ผู้เยี่ยมเยียน", + + "info": "ข้อมูล" +} \ No newline at end of file diff --git a/public/language/th/admin/development/logger.json b/public/language/th/admin/development/logger.json new file mode 100644 index 0000000000..ed412a1609 --- /dev/null +++ b/public/language/th/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "การตั้งค่าการบันทึกผลกิจกรรม", + "description": "ถ้าคุณเลือกช่องนี้, คุณจะได้รับการแสดงผลกิจกรรมทางจอภาพ แต่ถ้าคุณระบุเส้นทางการจัดเก็บผลการบันทึกกิจกรรมจะถูกบันทึกเป็นไฟล์แทน, ผลการบันทึกกิจกรรมของ HTTP มีประโยชน์เพื่อเก็บสถิติเกี่ยวกับ ใคร, เมื่อไหร่ และที่ไหนในฟอรั่มของคุณที่พวกเขาเข้าถึง เช่นเดียวกันกับที่เราสามารถบันทึกผลกิจกรรมอีเวนท์ของ socket.io โดยการบันทึกผลกิจกรรมของ Socket.io นั้นจะบันทึกร่วมกับการจับตาดู redis-cli ซึ่งสามารถช่วยให้เราศึกษา NodeBB จากภายในได้", + "explanation": "ง่ายมากเพียงแค่ เลือกหรือยกเลิก การตั้งค่าการบันทึกผลกิจกรรม เพื่อเปิดและปิดการบันทึกผลกิจกรรมในทันที ไม่จำเป็นต้องรีสตาร์ท", + "enable-http": "เปิดการบันทึกผลกิจกรรมของ HTTP", + "enable-socket": "เปิดการบันทึกผลกิจกรรมอีเวนท์ของ socket.io", + "file-path": "เส้นทางเพื่อบันทึกไฟล์บันทึกผลกิจกรรม", + "file-path-placeholder": "/path/to/log/file.log ::: ปล่อยว่างถ้าคุณต้องการให้แสดงผลการบันทึกกิจกรรมทางจอภาพ", + + "control-panel": "แผงควบคุมระบบการบันทึกผลกิจกรรม", + "update-settings": "บันทึกการตั้งค่าการบันทึกผลกิจกรรม" +} \ No newline at end of file diff --git a/public/language/th/admin/extend/plugins.json b/public/language/th/admin/extend/plugins.json new file mode 100644 index 0000000000..49c9ad44f8 --- /dev/null +++ b/public/language/th/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "ถูกติดตั้งแล้ว", + "active": "ทำงาน", + "inactive": "ไม่ทำงาน", + "out-of-date": "รุ่นเก่า", + "none-found": "ไม่พบปลั๊กอิน", + "none-active": "ไม่มีปลั๊กอินที่ทำงาน", + "find-plugins": "ค้นหาปลั๊กอิน", + + "plugin-search": "การค้นหาปลั๊กอิน", + "plugin-search-placeholder": "ค้นหาปลั๊กอิน...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "เรียงลำดับปลั๊กอินใหม่", + "order-active": "ลำดับการทำงานของปลั๊กอิน", + "dev-interested": "คุณสนใจที่จะสร้างปลั๊กอินสำหรับ NodeBB หรือไม่?", + "docs-info": "เอกสารฉบับเต็มเกี่ยวกับปลั๊กอินสามารถพบได้ที่ คลังเอกสาร NodeBB ", + + "order.description": "ปลั๊กอินบางตัวใช้งานได้ดีเมื่อเริ่มต้นใช้งานก่อน / หลังปลั๊กอินอื่น ๆ", + "order.explanation": "โหลดปลั๊กอินตามลำดับที่ระบุจากบนลงล่าง", + + "plugin-item.themes": "ธีม", + "plugin-item.deactivate": "ปิดการใช้งาน", + "plugin-item.activate": "เปิดการใช้งาน", + "plugin-item.install": "ติดตั้ง", + "plugin-item.uninstall": "ถอนการติดตั้ง", + "plugin-item.settings": "ตั้งค่า", + "plugin-item.installed": "ติดตั้งแล้ว", + "plugin-item.latest": "ล่าสุด", + "plugin-item.upgrade": "อัพเกรด", + "plugin-item.more-info": "ข้อมูลเพิ่มเติม:", + "plugin-item.unknown": "ไม่ทราบ", + "plugin-item.unknown-explanation": "สถานะของปลั๊กอินนี้ไม่สามารถระบุได้ซึ่งอาจเกิดจากข้อผิดพลาดในการกำหนดค่าผิดพลาด", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "เปิดใช้งานปลั๊กอินแล้ว", + "alert.disabled": "ปิดใช้งานปลั๊กอินแล้ว", + "alert.upgraded": "อัพเกรดปลั๊กอินแล้ว", + "alert.installed": "ติดตั้งปลั๊กอินแล้ว", + "alert.uninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "ปิดการใช้งานปลั๊กอินนี้แล้ว", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "ติดตั้งปลั๊กอินแล้ว โปรดเปิดใช้งานปลั๊กอิน", + "alert.uninstall-success": "ปิดใช้งานปลั๊กอินและยกเลิกการติดตั้งแล้ว", + "alert.suggest-error": "

NodeBB ไม่สามารถเข้าถึงตัวจัดการแพคเกจดำเนินการติดตั้งเวอร์ชันล่าสุดได้หรือไม่?

เซิร์ฟเวอร์ตอบกลับ (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB ไม่สามารถติดต่อตัวจัดการแพคเกจได้ในขณะนี้เราไม่แนะนำให้อัปเกรด

", + "alert.incompatible": "เวอร์ชันของ NodeBB (v%1) จะถูกล้างเพื่อให้มีการอัพเกรดไป v%2 ของปลั๊กอินนี้ โปรดอัปเดต NodeBB ของคุณหากคุณต้องการติดตั้งปลั๊กอินเวอร์ชันใหม่นี้

", + "alert.possibly-incompatible": "

ไม่พบข้อมูลความเข้ากันได้

ปลั๊กอินนี้ไม่ได้ระบุเวอร์ชันเฉพาะสำหรับการติดตั้งที่ให้เวอร์ชัน NodeBB ของคุณ ไม่สามารถรับประกันความสามารถในการใช้งานร่วมกันได้เต็มรูปแบบและอาจทำให้ NodeBB ของคุณทำงานไม่ได้อย่างถูกต้องอีกต่อไป

ในกรณีที่ NodeBB ไม่สามารถบูตได้อย่างถูกต้อง:

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

ติดตั้งปลั๊กอินนี้เวอร์ชันล่าสุดต่อหรือไม่?

", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "ข้อมูลลิขสิทธิ์ปลั๊กอิน", + "license.intro": "ปลั๊กอิน %1ได้รับอนุญาตภายใต้ %2 โปรดอ่านและทำความเข้าใจข้อกำหนดสิทธิ์การใช้งานก่อนเปิดใช้งานปลั๊กอินนี้", + "license.cta": "คุณต้องการจะเปิดใช้ปลั๊กอินนี้ต่อหรือไม่?" +} diff --git a/public/language/th/admin/extend/rewards.json b/public/language/th/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/th/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/th/admin/extend/widgets.json b/public/language/th/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/th/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/th/admin/manage/admins-mods.json b/public/language/th/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/th/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/th/admin/manage/categories.json b/public/language/th/admin/manage/categories.json new file mode 100644 index 0000000000..ed5462e9be --- /dev/null +++ b/public/language/th/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Category Settings", + "privileges": "Privileges", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Category purged!", + "alert.copy-success": "Settings Copied!", + "alert.set-parent-category": "Set Parent Category", + "alert.updated": "Updated Categories", + "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.upload-image": "Upload category image", + "alert.find-user": "Find a User", + "alert.user-search": "Search for a user here...", + "alert.find-group": "Find a Group", + "alert.group-search": "Search for a group here...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Collapse All", + "expand-all": "Expand All", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/th/admin/manage/digest.json b/public/language/th/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/th/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/th/admin/manage/groups.json b/public/language/th/admin/manage/groups.json new file mode 100644 index 0000000000..911fcce010 --- /dev/null +++ b/public/language/th/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Group Name", + "badge": "Badge", + "properties": "Properties", + "description": "Group Description", + "member-count": "Member Count", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Edit", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Search", + "create": "Create Group", + "description-placeholder": "A short description about your group", + "create-button": "Create", + + "alerts.create-failure": "Uh-Oh

There was a problem creating your group. Please try again later!

", + "alerts.confirm-delete": "Are you sure you wish to delete this group?", + + "edit.name": "Name", + "edit.description": "Description", + "edit.user-title": "Title of Members", + "edit.icon": "Group Icon", + "edit.label-color": "Group Label Color", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Show Badge", + "edit.private-details": "If enabled, joining of groups requires approval from a group owner.", + "edit.private-override": "Warning: Private groups is disabled at system level, which overrides this option.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Hidden", + "edit.hidden-details": "If enabled, this group will not be found in the groups listing, and users will have to be invited manually", + "edit.add-user": "Add User to Group", + "edit.add-user-search": "Search Users", + "edit.members": "Member List", + "control-panel": "Groups Control Panel", + "revert": "Revert", + + "edit.no-users-found": "No Users Found", + "edit.confirm-remove-user": "Are you sure you want to remove this user?", + "edit.save-success": "Changes saved!" +} \ No newline at end of file diff --git a/public/language/th/admin/manage/privileges.json b/public/language/th/admin/manage/privileges.json new file mode 100644 index 0000000000..13a38819b0 --- /dev/null +++ b/public/language/th/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Global", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Chat", + "upload-images": "Upload Images", + "upload-files": "Upload Files", + "signature": "Signature", + "ban": "Ban", + "mute": "Mute", + "invite": "Invite", + "search-content": "Search Content", + "search-users": "Search Users", + "search-tags": "Search Tags", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Find Category", + "access-category": "Access Category", + "access-topics": "Access Topics", + "create-topics": "Create Topics", + "reply-to-topics": "Reply to Topics", + "schedule-topics": "Schedule Topics", + "tag-topics": "Tag Topics", + "edit-posts": "Edit Posts", + "view-edit-history": "View Edit History", + "delete-posts": "Delete Posts", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Upvote Posts", + "downvote-posts": "Downvote Posts", + "delete-topics": "Delete Topics", + "purge": "Purge", + "moderate": "Moderate", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/th/admin/manage/registration.json b/public/language/th/admin/manage/registration.json new file mode 100644 index 0000000000..72de48167b --- /dev/null +++ b/public/language/th/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "คิว", + "description": "ไม่มีผู้ใช้ในคิวการลงทะเบียน
เมื่อต้องการเปิดใช้คุณลักษณะนี้ ไปที่ การตั้งค่า → ผู้ใช้ → การลงทะเบียนผู้ใช้และตั้งประเภทการลงทะเบียนเป็น \"การอนุมัติโดยผู้ดูแลระบบ\"", + + "list.name": "ชื่อ", + "list.email": "อีเมล", + "list.ip": "IP", + "list.time": "เวลา", + "list.username-spam": "ความถี่: %1  ปรากฎ: %2 ความมั่นใจ: %3", + "list.email-spam": "ความถี่: %1  ปรากฎ: %2", + "list.ip-spam": "ความถี่: %1  ปรากฎ: %2", + + "invitations": "การเชิญ", + "invitations.description": "
ด้านล่างนี้เป็นรายการคำเชิญที่ส่งแล้ว ใช้ ctrl-f เพื่อค้นหาผ่านรายการทางอีเมลหรือชื่อผู้ใช้
ชื่อผู้ใช้จะปรากฏทางด้านขวาของอีเมลสำหรับผู้ใช้ที่รับคำเชิญแล้ว", + "invitations.inviter-username": "ผู้เชิญผู้ใช้งานนี้", + "invitations.invitee-email": "อีเมลที่เชิญ", + "invitations.invitee-username": "ชื่อผู้ได้รับเชิญ (ถ้าลงทะเบียน)", + + "invitations.confirm-delete": "คุณแน่ใจหรือไม่ว่าต้องการลบคำเชิญนี้" +} \ No newline at end of file diff --git a/public/language/th/admin/manage/tags.json b/public/language/th/admin/manage/tags.json new file mode 100644 index 0000000000..01363dfda0 --- /dev/null +++ b/public/language/th/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Your forum does not have any topics with tags yet.", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Create Tag", + "modify": "Modify Tags", + "rename": "Rename Tags", + "delete": "Delete Selected Tags", + "search": "Search for tags...", + "settings": "Tags Settings", + "name": "Tag Name", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Do you want to delete the selected tags?", + "alerts.update-success": "Tag Updated!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/th/admin/manage/uploads.json b/public/language/th/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/th/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/th/admin/manage/users.json b/public/language/th/admin/manage/users.json new file mode 100644 index 0000000000..b479fe8422 --- /dev/null +++ b/public/language/th/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "ผู้ใช้", + "edit": "Actions", + "make-admin": "ทำให้เป็นแอดมิน", + "remove-admin": "ยกเลิกการเป็นแอดมิน", + "validate-email": "ยืนยันอีเมล", + "send-validation-email": "ส่งอีเมลยืนยัน", + "password-reset-email": "ส่งการล้างค่ารหัสผ่านทางอีเมล", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "ผู้ใช้งานที่โดนแบน", + "temp-ban": "ผู้ใช้งานที่โดนแบนชั่วคราว", + "unban": "ยกเลิกการแบนผู้ใช้งาน", + "reset-lockout": "ยกเลิกการกักกัน", + "reset-flags": "ยกเลิกการการเฝ้าระวัง", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "ดาวน์โหลด CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "ผู้ใช้งานใหม่", + "filter-by": "Filter by", + "pills.unvalidated": "ยังไม่ได้ยืนยัน", + "pills.validated": "Validated", + "pills.banned": "แบน", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "ตามรหัสผู้ใช้", + "search.uid-placeholder": "ป้อนหมายเลขผู้ใช้เพื่อค้นหา", + "search.username": "โดยชื่อผู้ใช้งาน", + "search.username-placeholder": "ใส่ชื่อผู้ใช้งานเพื่อทำการค้นหา", + "search.email": "โดยอีเมล", + "search.email-placeholder": "ใส่อีเมลเพื่อทำการค้นหา", + "search.ip": "โดย IP แอดเดรส", + "search.ip-placeholder": "ใส่ IP แอดเดรสเพื่อทำการค้นหา", + "search.not-found": "ไม่พบผู้ใช้งาน!", + + "inactive.3-months": "สามเดือน", + "inactive.6-months": "หกเดือน", + "inactive.12-months": "สิบสองเดือน", + + "users.uid": "uid", + "users.username": "ชื่อผู้ใช้", + "users.email": "อีเมล", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "จำนวนกระทู้", + "users.reputation": "ชื่อเสียง", + "users.flags": "ติดตาม", + "users.joined": "เข้าร่วม", + "users.last-online": "ออนไลน์ครั้งสุดท้าย", + "users.banned": "แบน", + + "create.username": "ชื่อผู้ใช้งาน", + "create.email": "อีเมล", + "create.email-placeholder": "อีเมลของผู้ใช้", + "create.password": "รหัสผ่าน", + "create.password-confirm": "ยืนยันรหัสผ่าน", + + "temp-ban.length": "Length", + "temp-ban.reason": "เหตุผล (ตัวเลือก)", + "temp-ban.hours": "ชั่วโมง", + "temp-ban.days": "วัน", + "temp-ban.explanation": "ระบุระยะเวลาของการแบน ถ้าระยะเวลาเป็น \"0\" คือการแบนถาวร", + + "alerts.confirm-ban": "คุณต้องการที่จะแบนผู้ใช้คนนี้ ถาวร ?", + "alerts.confirm-ban-multi": "คุณต้องการที่จะแบนผู้ใช้กลุ่มนี้ ถาวร ?", + "alerts.ban-success": "ผู้ใช้งานที่โดนแบน", + "alerts.button-ban-x": "แบน %1 ผู้ใช้งาน", + "alerts.unban-success": "ยกเลิกการแบนผู้ใช้งาน", + "alerts.lockout-reset-success": "ยกเลิกการกักกัน", + "alerts.flag-reset-success": "ยกเลิกการติดตาม", + "alerts.no-remove-yourself-admin": "คุณไม่สามารถที่จะยกเลิกตัวเองจากการเป็นผู้ดูแลระบบ", + "alerts.make-admin-success": "ขณะนี้ผู้ใช้เป็นผู้ดูแลระบบแล้ว", + "alerts.confirm-remove-admin": "คุณต้องการลบผู้ดูแลระบบคนนี้หรือไม่?", + "alerts.remove-admin-success": "ผู้ใช้ไม่ได้เป็นผู้ดูแลอีกต่อไป", + "alerts.make-global-mod-success": "ขณะนี้ผู้ใช้เป็นผู้ดูแลระดับโลกแล้ว", + "alerts.confirm-remove-global-mod": "คุณต้องการลบผู้ดูแลทั่วโลกนี้หรือไม่?", + "alerts.remove-global-mod-success": "ผู้ใช้ไม่เป็นผู้ดูแลระดับโลกอีกแล้ว", + "alerts.make-moderator-success": "ขณะนี้ผู้ใช้เป็นผู้ดูแล", + "alerts.confirm-remove-moderator": "คุณต้องการนำผู้ดูแลนี้ออกหรือไม่?", + "alerts.remove-moderator-success": "ผู้ใช้ไม่ได้เป็นผู้ดูแลอีกต่อไป", + "alerts.confirm-validate-email": "คุณต้องการที่ยืนยันอีเมลของผู้ใช้เหล่านี้หรือไม่?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "อีเมลที่ได้รับการยืนยัน", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "คุณต้องการที่จะส่งอีเมลการล้างค่ารหัสผ่านให้กับผู้ใช้เหล่านี้หรือไม่?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "ผู้ใช้งานโดนลบ!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "สร้างผู้ใช้งาน", + "alerts.button-create": "สร้าง", + "alerts.button-cancel": "ยกเลิก", + "alerts.error-passwords-different": "รหัสผ่านจะต้องเหมือนกัน! ", + "alerts.error-x": "ผิดพลาด

%1

", + "alerts.create-success": "ผู้ใช้งานถูกสร้าง!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "อีเมลคำเชิญถูกส่งไปที่ %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/th/admin/menu.json b/public/language/th/admin/menu.json new file mode 100644 index 0000000000..379e0b2687 --- /dev/null +++ b/public/language/th/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "General", + + "section-manage": "Manage", + "manage/categories": "Categories", + "manage/privileges": "Privileges", + "manage/tags": "Tags", + "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", + "manage/registration": "Registration Queue", + "manage/post-queue": "Post Queue", + "manage/groups": "Groups", + "manage/ip-blacklist": "IP Blacklist", + "manage/uploads": "Uploads", + "manage/digest": "Digests", + + "section-settings": "Settings", + "settings/general": "General", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Email", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Guests", + "settings/uploads": "Uploads", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Pagination", + "settings/tags": "Tags", + "settings/notifications": "Notifications", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web Crawler", + "settings/sockets": "Sockets", + "settings/advanced": "Advanced", + + "settings.page-title": "%1 Settings", + + "section-appearance": "Appearance", + "appearance/themes": "Themes", + "appearance/skins": "Skins", + "appearance/customise": "Custom Content (HTML/JS/CSS)", + + "section-extend": "Extend", + "extend/plugins": "Plugins", + "extend/widgets": "Widgets", + "extend/rewards": "Rewards", + + "section-social-auth": "Social Authentication", + + "section-plugins": "Plugins", + "extend/plugins.install": "Install Plugins", + + "section-advanced": "Advanced", + "advanced/database": "Database", + "advanced/events": "Events", + "advanced/hooks": "Hooks", + "advanced/logs": "Logs", + "advanced/errors": "Errors", + "advanced/cache": "Cache", + "development/logger": "Logger", + "development/info": "Info", + + "rebuild-and-restart-forum": "Rebuild & Restart Forum", + "restart-forum": "Restart Forum", + "logout": "Log out", + "view-forum": "View Forum", + + "search.placeholder": "Search settings", + "search.no-results": "No results...", + "search.search-forum": "Search the forum for ", + "search.keep-typing": "Type more to see results...", + "search.start-typing": "Start typing to see results...", + + "connection-lost": "Connection to %1 has been lost, attempting to reconnect...", + + "alerts.version": "Running NodeBB v%1", + "alerts.upgrade": "Upgrade to v%1" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/advanced.json b/public/language/th/admin/settings/advanced.json new file mode 100644 index 0000000000..982eaa2f64 --- /dev/null +++ b/public/language/th/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Maintenance Mode", + "maintenance-mode.help": "When the forum is in maintenance mode, all requests will be redirected to a static holding page. Administrators are exempt from this redirection, and are able to access the site normally.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Maintenance Message", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Headers", + "headers.allow-from": "Set ALLOW-FROM to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Customise the \"Powered By\" header sent by NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Traffic Management", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Enable Traffic Management", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Lowering this value decreases wait times for page loads, but will also show the \"excessive load\" message to more users. (Restart required)", + "traffic.lag-check-interval": "Check Interval (in milliseconds)", + "traffic.lag-check-interval-help": "Lowering this value causes NodeBB to become more sensitive to spikes in load, but may also cause the check to become too sensitive. (Restart required)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/th/admin/settings/api.json b/public/language/th/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/th/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/chat.json b/public/language/th/admin/settings/chat.json new file mode 100644 index 0000000000..e8f7c02f6a --- /dev/null +++ b/public/language/th/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "ตั้งค่าแชท", + "disable": "ปิดการใช้งานแชท", + "disable-editing": "ปิดการแก้ไข และการลบแชท", + "disable-editing-help": "Administrators and global moderators are exempt from this restriction", + "max-length": "จำนวนอักขระมากที่มากที่สุดต่อแชท", + "max-room-size": "จำนวนผู้ใช้ในห้องแชทมากที่สุด", + "delay": "Time between chat messages in milliseconds", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/cookies.json b/public/language/th/admin/settings/cookies.json new file mode 100644 index 0000000000..1ffd2dced4 --- /dev/null +++ b/public/language/th/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "EU Consent", + "consent.enabled": "Enabled", + "consent.message": "Notification message", + "consent.acceptance": "Acceptance message", + "consent.link-text": "Policy Link Text", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Leave blank to use NodeBB localised defaults", + "settings": "Settings", + "cookie-domain": "Session cookie domain", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Leave blank for default" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/email.json b/public/language/th/admin/settings/email.json new file mode 100644 index 0000000000..35e713adc0 --- /dev/null +++ b/public/language/th/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Email Settings", + "address": "Email Address", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Edit Email Template", + "template.select": "Select Email Template", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "Select Email Template", + "testing.send": "Send Test Email", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/th/admin/settings/general.json b/public/language/th/admin/settings/general.json new file mode 100644 index 0000000000..29b939861b --- /dev/null +++ b/public/language/th/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Settings", + "title": "Site Title", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "The URL of the site title", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Your Community Name", + "title.show-in-header": "Show Site Title in Header", + "browser-title": "Browser Title", + "browser-title-help": "If no browser title is specified, the site title will be used", + "title-layout": "Title Layout", + "title-layout-help": "Define how the browser title will be structured ie. {pageTitle} | {browserTitle}", + "description.placeholder": "A short description about your community", + "description": "Site Description", + "keywords": "Site Keywords", + "keywords-placeholder": "Keywords describing your community, comma-separated", + "logo": "Site Logo", + "logo.image": "Image", + "logo.image-placeholder": "Path to a logo to display on forum header", + "logo.upload": "Upload", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "The URL of the site logo", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Alt Text", + "log.alt-text-placeholder": "Alternative text for accessibility", + "favicon": "Favicon", + "favicon.upload": "Upload", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Upload", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Outgoing Links", + "outgoing-links.warning-page": "Use Outgoing Links Warning Page", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Domains to whitelist for bypassing the warning page", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/th/admin/settings/group.json b/public/language/th/admin/settings/group.json new file mode 100644 index 0000000000..f13933ea7e --- /dev/null +++ b/public/language/th/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "General", + "private-groups": "Private Groups", + "private-groups.help": "If enabled, joining of groups requires the approval of the group owner (Default: enabled)", + "private-groups.warning": "Beware! If this option is disabled and you have private groups, they automatically become public.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "This flag can be used to allow users to select multiple group badges, requires theme support.", + "max-name-length": "Maximum Group Name Length", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Group Cover Image", + "default-cover": "Default Cover Images", + "default-cover-help": "Add comma-separated default cover images for groups that don't have an uploaded cover image" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/guest.json b/public/language/th/admin/settings/guest.json new file mode 100644 index 0000000000..75d44f37e4 --- /dev/null +++ b/public/language/th/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Allow guest handles", + "handles.enabled-help": "This option exposes a new field that allows guests to pick a name to associate with each post they make. If disabled, they will simply be called \"Guest\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/homepage.json b/public/language/th/admin/settings/homepage.json new file mode 100644 index 0000000000..48f9ebe23a --- /dev/null +++ b/public/language/th/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "หน้าแรก", + "description": "เลือกหน้าเว็บที่จะแสดงเมื่อผู้ใช้ไปที่ URL หลักของฟอรัม", + "home-page-route": "เส้นทางหน้าแรก", + "custom-route": "เส้นทางที่กำหนดเอง", + "allow-user-home-pages": "อนุญาตหน้าแรกของผู้ใช้", + "home-page-title": "Title ของหน้าแรก (ค่าเริ่มต้น \"Home\")" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/languages.json b/public/language/th/admin/settings/languages.json new file mode 100644 index 0000000000..bdd57849b3 --- /dev/null +++ b/public/language/th/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Language Settings", + "description": "The default language determines the language settings for all users who are visiting your forum.
Individual users can override the default language on their account settings page.", + "default-language": "Default Language", + "auto-detect": "Auto Detect Language Setting for Guests" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/navigation.json b/public/language/th/admin/settings/navigation.json new file mode 100644 index 0000000000..7baca85096 --- /dev/null +++ b/public/language/th/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Icon:", + "change-icon": "change", + "route": "Route:", + "tooltip": "Tooltip:", + "text": "Text:", + "text-class": "Text Class: optional", + "class": "Class: optional", + "id": "ID: optional", + + "properties": "Properties:", + "groups": "Groups:", + "open-new-window": "Open in a new window", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Delete", + "btn.disable": "Disable", + "btn.enable": "Enable", + + "available-menu-items": "Available Menu Items", + "custom-route": "Custom Route", + "core": "core", + "plugin": "plugin" +} diff --git a/public/language/th/admin/settings/notifications.json b/public/language/th/admin/settings/notifications.json new file mode 100644 index 0000000000..0d8e4ec3c1 --- /dev/null +++ b/public/language/th/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "การแจ้งเตือน", + "welcome-notification": "การยินดีต้อนรับแจ้งเตือน", + "welcome-notification-link": "ลิงค์การยินดีต้อนรับแจ้งเตือน", + "welcome-notification-uid": "Welcome Notification User (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/pagination.json b/public/language/th/admin/settings/pagination.json new file mode 100644 index 0000000000..3bf306b2f9 --- /dev/null +++ b/public/language/th/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Pagination Settings", + "enable": "Paginate topics and posts instead of using infinite scroll.", + "posts": "Post Pagination", + "topics": "Topic Pagination", + "posts-per-page": "Posts per Page", + "max-posts-per-page": "Maximum posts per page", + "categories": "Category Pagination", + "topics-per-page": "Topics per Page", + "max-topics-per-page": "Maximum topics per page", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/post.json b/public/language/th/admin/settings/post.json new file mode 100644 index 0000000000..57cc855319 --- /dev/null +++ b/public/language/th/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Post Sorting", + "sorting.post-default": "Default Post Sorting", + "sorting.oldest-to-newest": "Oldest to Newest", + "sorting.newest-to-oldest": "Newest to Oldest", + "sorting.most-votes": "Most Votes", + "sorting.most-posts": "Most Posts", + "sorting.topic-default": "Default Topic Sorting", + "length": "Post Length", + "post-queue": "Post Queue", + "restrictions": "Posting Restrictions", + "restrictions-new": "New User Restrictions", + "restrictions.post-queue": "Enable post queue", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Enable new user restrictions", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Seconds between posts for new users", + "restrictions.rep-threshold": "Reputation threshold before these restrictions are lifted", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Minimum Title Length", + "restrictions.max-title-length": "Maximum Title Length", + "restrictions.min-post-length": "Minimum Post Length", + "restrictions.max-post-length": "Maximum Post Length", + "restrictions.days-until-stale": "Days until topic is considered stale", + "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", + "timestamp": "Timestamp", + "timestamp.cut-off": "Date cut-off (in days)", + "timestamp.cut-off-help": "Dates & 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).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Teaser Post", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "First", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Unread Settings", + "unread.cutoff": "Unread cutoff days", + "unread.min-track-last": "Minimum posts in topic before tracking last read", + "recent": "Recent Settings", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "signature": "Signature Settings", + "signature.disable": "Disable signatures", + "signature.no-links": "Disable links in signatures", + "signature.no-images": "Disable images in signatures", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Maximum Signature Length", + "composer": "Composer Settings", + "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.show-help": "Show \"Help\" tab", + "composer.enable-plugin-help": "Allow plugins to add content to the help tab", + "composer.custom-help": "Custom Help Text", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP Tracking", + "ip-tracking.each-post": "Track IP Address for each post", + "enable-post-history": "Enable Post History" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/reputation.json b/public/language/th/admin/settings/reputation.json new file mode 100644 index 0000000000..e790ec094f --- /dev/null +++ b/public/language/th/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Reputation Settings", + "disable": "Disable Reputation System", + "disable-down-voting": "Disable Down Voting", + "votes-are-public": "All Votes Are Public", + "thresholds": "Activity Thresholds", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Minimum reputation to downvote posts", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Minimum reputation to flag posts", + "min-rep-website": "Minimum reputation to add \"Website\" to user profile", + "min-rep-aboutme": "Minimum reputation to add \"About me\" to user profile", + "min-rep-signature": "Minimum reputation to add \"Signature\" to user profile", + "min-rep-profile-picture": "Minimum reputation to add \"Profile Picture\" to user profile", + "min-rep-cover-picture": "Minimum reputation to add \"Cover Picture\" to user profile", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/social.json b/public/language/th/admin/settings/social.json new file mode 100644 index 0000000000..7d6a9a8c83 --- /dev/null +++ b/public/language/th/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "การแชร์กระทู้", + "info-plugins-additional": "ส่วนเสริมสามารถเพิ่มการเชือมต่อโซเชียลมิเดียเพื่อแชร์กระทู้", + "save-success": "การบันทึกการโพสแชร์เนื้อหาเสร็จสมบูรณ์!" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/sockets.json b/public/language/th/admin/settings/sockets.json new file mode 100644 index 0000000000..d04ee42fcf --- /dev/null +++ b/public/language/th/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Reconnection Settings", + "max-attempts": "Max Reconnection Attempts", + "default-placeholder": "Default: %1", + "delay": "Reconnection Delay" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/sounds.json b/public/language/th/admin/settings/sounds.json new file mode 100644 index 0000000000..2be53c2cf0 --- /dev/null +++ b/public/language/th/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "การแจ้งเตือน", + "chat-messages": "ข้อความแชท", + "play-sound": "เล่น", + "incoming-message": "ข้อความเข้า", + "outgoing-message": "ข้อความออก", + "upload-new-sound": "อัปโหลดเสียงใหม่", + "saved": "การตั้งค่าได้ถูกบันทึกแล้ว" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/tags.json b/public/language/th/admin/settings/tags.json new file mode 100644 index 0000000000..080010f6f0 --- /dev/null +++ b/public/language/th/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Tag Settings", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Minimum Tags per Topic", + "max-per-topic": "Maximum Tags per Topic", + "min-length": "Minimum Tag Length", + "max-length": "Maximum Tag Length", + "related-topics": "Related Topics", + "max-related-topics": "Maximum related topics to display (if supported by theme)" +} \ No newline at end of file diff --git a/public/language/th/admin/settings/uploads.json b/public/language/th/admin/settings/uploads.json new file mode 100644 index 0000000000..078a19ccd2 --- /dev/null +++ b/public/language/th/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Posts", + "orphans": "Orphaned Files", + "private": "Make uploaded files private", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Quality to use when resizing images", + "resize-image-quality-help": "Use a lower quality setting to reduce the file size of resized images.", + "max-file-size": "Maximum File Size (in KiB)", + "max-file-size-help": "(in kibibytes, default: 2048 KiB)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Allow users to upload topic thumbnails", + "topic-thumb-size": "Topic Thumb Size", + "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Profile Avatars", + "allow-profile-image-uploads": "Allow users to upload profile images", + "convert-profile-image-png": "Convert profile image uploads to PNG", + "default-avatar": "Custom Default Avatar", + "upload": "Upload", + "profile-image-dimension": "Profile Image Dimension", + "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "max-profile-image-size": "Maximum Profile Image File Size", + "max-profile-image-size-help": "(in kibibytes, default: 256 KiB)", + "max-cover-image-size": "Maximum Cover Image File Size", + "max-cover-image-size-help": "(in kibibytes, default: 2,048 KiB)", + "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", + "profile-covers": "Profile Covers", + "default-covers": "Default Cover Images", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/th/admin/settings/user.json b/public/language/th/admin/settings/user.json new file mode 100644 index 0000000000..2ed52e4742 --- /dev/null +++ b/public/language/th/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Authentication", + "email-confirm-interval": "User may not resend a confirmation email until", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Allow login with", + "allow-login-with.username-email": "Username or Email", + "allow-login-with.username": "Username Only", + "account-settings": "Account Settings", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Disable username changes", + "disable-email-changes": "Disable email changes", + "disable-password-changes": "Disable password changes", + "allow-account-deletion": "Allow account deletion", + "hide-fullname": "Hide fullname from users", + "hide-email": "Hide email from users", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "ธีม", + "disable-user-skins": "Prevent users from choosing a custom skin", + "account-protection": "Account Protection", + "admin-relogin-duration": "Admin relogin duration (minutes)", + "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "login-attempts": "Login attempts per hour", + "login-attempts-help": "If login attempts to a user's account exceeds this threshold, that account will be locked for a pre-configured amount of time", + "lockout-duration": "Account Lockout Duration (minutes)", + "login-days": "Days to remember user login sessions", + "password-expiry-days": "Force password reset after a set number of days", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "User Registration", + "registration-type": "Registration Type", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Admin Approval", + "registration-type.admin-approval-ip": "Admin Approval for IPs", + "registration-type.invite-only": "Invite Only", + "registration-type.admin-invite-only": "Admin Invite Only", + "registration-type.disabled": "No registration", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Maximum Invitations per User", + "max-invites": "Maximum Invitations per User", + "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "invite-expiration": "Invite expiration", + "invite-expiration-help": "# of days invitations expire in.", + "min-username-length": "Minimum Username Length", + "max-username-length": "Maximum Username Length", + "min-password-length": "Minimum Password Length", + "min-password-strength": "Minimum Password Strength", + "max-about-me-length": "Maximum About Me Length", + "terms-of-use": "Forum Terms of Use (Leave blank to disable)", + "user-search": "User Search", + "user-search-results-per-page": "Number of results to display", + "default-user-settings": "Default User Settings", + "show-email": "Show email", + "show-fullname": "Show fullname", + "restrict-chat": "Only allow chat messages from users I follow", + "outgoing-new-tab": "Open outgoing links in new tab", + "topic-search": "Enable In-Topic Searching", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Subscribe to Digest", + "digest-freq.off": "Off", + "digest-freq.daily": "ทุกวัน", + "digest-freq.weekly": "ทุกอาทิตย์", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "ทุกเดือน", + "email-chat-notifs": "Send an email if a new chat message arrives and I am not online", + "email-post-notif": "Send an email when replies are made to topics I am subscribed to", + "follow-created-topics": "Follow topics you create", + "follow-replied-topics": "Follow topics that you reply to", + "default-notification-settings": "Default notification settings", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/th/admin/settings/web-crawler.json b/public/language/th/admin/settings/web-crawler.json new file mode 100644 index 0000000000..2e0d31d12b --- /dev/null +++ b/public/language/th/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Crawlability Settings", + "robots-txt": "Custom Robots.txt Leave blank for default", + "sitemap-feed-settings": "Sitemap & Feed Settings", + "disable-rss-feeds": "Disable RSS Feeds", + "disable-sitemap-xml": "Disable Sitemap.xml", + "sitemap-topics": "Number of Topics to display in the Sitemap", + "clear-sitemap-cache": "Clear Sitemap Cache", + "view-sitemap": "View Sitemap" +} \ No newline at end of file diff --git a/public/language/th/category.json b/public/language/th/category.json new file mode 100644 index 0000000000..523faf142b --- /dev/null +++ b/public/language/th/category.json @@ -0,0 +1,23 @@ +{ + "category": "หมวดหมู่", + "subcategories": "หมวดหมู่ย่อย", + "new_topic_button": "ตั้งกระทู้", + "guest-login-post": "เข้าสู่ระบบเพื่อโพสต์", + "no_topics": "ยังไม่มีกระทู้ในหมวดนี้
โพสต์กระทู้แรก?", + "browsing": "เรียกดู", + "no_replies": "ยังไม่มีใครตอบ", + "no_new_posts": "ไม่มีกระทู้ใหม่", + "watch": "ตามดู", + "ignore": "ไม่ต้องสนใจอีก", + "watching": "กำลังตามดู", + "not-watching": "Not Watching", + "ignoring": "เมินเฉย", + "watching.description": "Show topics in unread and recent", + "not-watching.description": "Do not show topics in unread, show in recent", + "ignoring.description": "Do not show topics in unread and recent", + "watching.message": "You are now watching updates from this category and all subcategories", + "notwatching.message": "You are not watching updates from this category and all subcategories", + "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watched-categories": "หมวดหมู่ที่ดูแล้ว", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/th/email.json b/public/language/th/email.json new file mode 100644 index 0000000000..cae9571202 --- /dev/null +++ b/public/language/th/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test Email", + "password-reset-requested": "Password Reset Requested!", + "welcome-to": "ยินดีต้อนรับ %1", + "invite": "คำเชิญจาก %1", + "greeting_no_name": "สวัสดี", + "greeting_with_name": "สวัสดี %1", + "email.verify-your-email.subject": "Please verify your email", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "ขอบคุณที่ลงทะเบียนกับ %1", + "welcome.text2": "เพื่อให้การบัญชีของคุณใช้งานได้อย่างเสร็จสมบูรณ์ เราจำเป็นต้องยืนยันว่าคุณเป็นเจ้าของที่แท้จริงของอีเมล์ที่ใช้สมัครสมาชิก", + "welcome.text3": "ผู้ดูแลระบบได้ทำการยอมรับการสมัครสมาชิกของคุณแล้ว คุณสามารถเข้าสู่ระบบด้วย ชื่อผู้ใช้/รหัสผ่าน ได้แล้วตอนนี้", + "welcome.cta": "กดตรงนี้เพื่อยืนยันอีเมลของคุณ", + "invitation.text1": "%1 ได้เชิญคุณให้เข้าร่วม %2", + "invitation.text2": "Your invitation will expire in %1 days.", + "invitation.cta": "Click here to create your account.", + "reset.text1": "เราได้รับคำร้องให้ตั้งค่ารหัสผ่านใหม่ของคุณ อาจจะเป็นเพราะว่าคุณลืมรหัสผ่านและได้ทำการส่งคำขอเข้ามา หากไม่ใช่ กรุณาเพิกเฉยต่ออีเมล์นี้และไม่ต้องดำเนินการใดๆทั้งสิ้น", + "reset.text2": "เพื่อดำเนินการตั้งรหัสผ่านใหม่ต่อไป, โปรดกดที่ลิ้งค์นี้:", + "reset.cta": "กดตรงนี้เพื่อตั้งรหัสผ่านใหม่", + "reset.notify.subject": "ตั้งค่ารหัสผ่านใหม่เรียบร้อยแล้ว", + "reset.notify.text1": "เรากำลังแจ้งคุณว่าตอน %1 รหัสผ่านของคุณถูกเปลี่ยนเรียบร้อยแล้ว", + "reset.notify.text2": "หากคุณไม่ได้เป็นคนอนุญาตสิ่งนี้ กรุณาแจ้งไปยังผู้ดูแลระบบโดยทันที", + "digest.latest_topics": "หัวข้อสนทนาล่าสุดจาก %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "กดตรงนี้เพื่อเข้าดู %1", + "digest.unsub.info": "คำชี้แจงถูกส่งไปให้คุณแล้ว เนื่องมาจากการตั้งค่าสมาชิกของคุณ", + "digest.day": "วัน", + "digest.week": "สัปดาห์", + "digest.month": "เดือน", + "digest.subject": "คำชี้แจงสำหรับ %1", + "digest.title.day": "Your Daily Digest", + "digest.title.week": "Your Weekly Digest", + "digest.title.month": "Your Monthly Digest", + "notif.chat.subject": "ได้รับข้อความแชทใหม่จาก %1", + "notif.chat.cta": "กดตรงนี้เพื่อกลับไปยังบทสนทนา", + "notif.chat.unsub.info": "การแจ้งเตือนแชทนี้ถูกส่งไปหาคุณเนื่องจากการตั้งค่าสมาชิกของคุณ", + "notif.post.unsub.info": "การแจ้งเตือนกระทู้นี้ถูกส่งไปยังคุณเนื่องการตั้งค่าสมาชิกของคุณ", + "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.cta": "To the forum", + "notif.cta-new-reply": "View Post", + "notif.cta-new-chat": "View Chat", + "notif.test.short": "Testing Notifications", + "notif.test.long": "This is a test of the notifications email. Send help!", + "test.text1": "นี่คืออีเมลทดสอบเพื่อยืนยันว่าระบบอีเมลมีการตั้งค่าที่ถูกต้องสำหรับ NodeBB ของคุณ", + "unsub.cta": "กดตรงนี้เพื่อเปลี่ยนแปลงการตั้งค่า", + "unsubscribe": "unsubscribe", + "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "คุณถูกแบนจาก %1 แล้ว", + "banned.text1": "ผู้ใช้ %1 ได้ถูกแบนจาก %2", + "banned.text2": "การแบนนี้จะใช้เวลาจนถึง %1", + "banned.text3": "นี่คือเหตุผลที่ทำไมคุณถึงถูกแบน", + "closing": "ขอบคุณ!" +} \ No newline at end of file diff --git a/public/language/th/error.json b/public/language/th/error.json new file mode 100644 index 0000000000..a92289d675 --- /dev/null +++ b/public/language/th/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "ข้อมูลไม่ถูกต้อง", + "invalid-json": "รูปแบบ JSON ไม่ถูกต้อง", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "คุณยังไม่ได้ลงชื่อเข้าระบบ", + "account-locked": "บัญชีของคุณถูกระงับการใช้งานชั่วคราว", + "search-requires-login": "\"ฟังก์ชั่นการค้นหา\" ต้องการบัญชีผู้ใช้ กรุณาเข้าสู่ระบบหรือสมัครสมาชิก", + "goback": "กดย้อนกลับเพื่อกลับไปยังหน้าที่แล้ว", + "invalid-cid": "Category ID ไม่ถูกต้อง", + "invalid-tid": "Topic ID ไม่ถูกต้อง", + "invalid-pid": "Post ID ไม่ถูกต้อง", + "invalid-uid": "User ID ไม่ถูกต้อง", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "ชื่อผู้ใช้ไม่ถูกต้อง", + "invalid-email": "อีเมลไม่ถูกต้อง", + "invalid-fullname": "Invalid Fullname", + "invalid-location": "Invalid Location", + "invalid-birthday": "Invalid Birthday", + "invalid-title": "Invalid title", + "invalid-user-data": "User Data ไม่ถูกต้อง", + "invalid-password": "รหัสผ่านไม่ถูกต้อง", + "invalid-login-credentials": "session login หมดอายุ", + "invalid-username-or-password": "กรุณาระบุชื่อผู้ใช้และรหัสผ่าน", + "invalid-search-term": "ข้อความค้นหาไม่ถูกต้อง", + "invalid-url": "Invalid URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Local login system has been disabled for non-privileged accounts.", + "csrf-invalid": "เราไม่สามารถนำท่านเข้าสู่ระบบได้ เหมือนกับว่าเซสชั่นหมดอายุแล้ว กรุณาลองใหม่อีกครั้ง", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "หมายเลขหน้าไม่ถูกต้อง จำเป็นต้องเป็นตัวเลขอย่างน้อย %1 และอย่างมาก %2", + "username-taken": "ชื่อผู้ใช้นี้มีการใช้แล้ว", + "email-taken": "อีเมลนี้มีการใช้แล้ว", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "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 may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "เราไม่สามารถยืนยันอีเมลของคุณ ณ ขณะนี้ กรุณาลองใหม่อีกครั้งภายหลัง", + "confirm-email-already-sent": "อีเมล์ยืนยันตัวตนถูกส่งไปยังคุณเรียบร้อยแล้ว กรุณารอ %1 นาที(s) ก่อนการตัดสินใจส่งอีกครั้ง", + "sendmail-not-found": "ไม่พบการประมวลผลสำหรับการส่งอีเมล์ กรุณาตรวจสอบให้แน่ใจว่าได้มีการติดตั้งโปรแกรมการประมวลผลแล้วโดยผู้ใช้ที่กำลังใช้ NodeBB", + "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "username-too-short": "ชื่อบัญชีผู้ใช้ สั้นเกินไป", + "username-too-long": "ชื่อบัญชีผู้ใช้ ยาวเกินไป", + "password-too-long": "รหัสผ่านยาวเกินไป", + "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "ผู้ใช้ได้รับการแบน", + "user-banned-reason": "ขออภัย บัญชีผู้ใช้นี้ได้รับการแบน (เหตุผล : %1)", + "user-banned-reason-until": "ขออภัย บัญชีผู้ใช้นี้ได้รับการแบนจนถึง %1 (เหตุผล : %2)", + "user-too-new": "ขออภัย คุณจำเป็นต้องรอ %1 วินาที(s) ก่อนการสร้างกระทู้แรกของคุณ", + "blacklisted-ip": "ขออภัย IP Address ของคุณถูกแบนจากชุมชนนี้ หากคุณคิดว่านี่เป็นเออเร่อของระบบ กรุณาติดต่อผู้ดูแลระบบ", + "ban-expiry-missing": "กรุณาระบุวันสิ้นสุดสำหรับการแบนในครั้งนี้", + "no-category": "ยังไม่มี Category นี้", + "no-topic": "ยังไม่มี Topic นี้", + "no-post": "ยังไม่มี Post นี้", + "no-group": "ยังไม่มี Group นี้", + "no-user": "ยังไม่มีผู้ใช้งานนี้", + "no-teaser": "ยังไม่มีทีเซอร์นี้", + "no-flag": "Flag does not exist", + "no-privileges": "คุณมีสิทธิ์ไม่เพียงพอที่จะทำรายการนี้", + "category-disabled": "Category นี้ถูกปิดการใช้งานแล้ว", + "topic-locked": "กระทู้ถูกล็อก", + "post-edit-duration-expired": "คุณได้รับอนุญาตให้แก้ไขโพสต์ได้หลังจากโพสต์ไปแล้ว %1  วินาที (s)", + "post-edit-duration-expired-minutes": "คุณได้รับการอนุญาตให้แก้ไขโพสต์ได้หลังจากโพสต์ไปแล้ว %1 นาที (s)", + "post-edit-duration-expired-minutes-seconds": "คุณได้รับการอนุญาตให้แก้ไขโพสต์ได้หลังจากโพสต์ไปแล้ว %1 นาที(s) %2 วินาที(s) ", + "post-edit-duration-expired-hours": "คุณได้รับการอนุญาตให้แก้ไขโพสต์ได้หลังจากโพสต์ไปแล้ว %1 ชั่วโมง(s) ", + "post-edit-duration-expired-hours-minutes": "คุณได้รับการอนุญาตให้แก้ไขโพสต์ได้หลังจากโพสต์ไปแล้ว %1 ชั่วโมง(s) %2 นาที(s)", + "post-edit-duration-expired-days": "คุณได้รับการอนุญาตให้แก้ไขโพสต์ได้หลังจากโพสต์ไปแล้ว %1 วัน(s)", + "post-edit-duration-expired-days-hours": "คุณได้รับการอนุญาตให้แก้ไขโพสต์ได้หลังจากโพสต์ไปแล้ว %1 วัน(s) %2 ชั่วโมง(s) ", + "post-delete-duration-expired": "คุณได้รับการอนุญาตให้ลบโพสต์ได้หลังจากโพสต์ไปแล้ว %1 วินาที(s)", + "post-delete-duration-expired-minutes": "คุณได้รับการอนุญาตให้ลบโพสต์ได้หลังจากโพสต์ไปแล้ว %1 นาที(s)", + "post-delete-duration-expired-minutes-seconds": "คุณได้รับการอนุญาตให้ลบโพสต์ได้หลังจากโพสต์ไปแล้ว %1 นาที(s) %2 วินาที(s)", + "post-delete-duration-expired-hours": "คุณได้รับการอนุญาตให้ลบโพสต์ได้หลังจากโพสต์ไปแล้ว %1 ชั่วโมง(s)", + "post-delete-duration-expired-hours-minutes": "คุณได้รับการอนุญาตให้ลบโพสต์ได้หลังจากโพสต์ไปแล้ว %1 ชั่วโมง(s) %2 นาที(s)", + "post-delete-duration-expired-days": "คุณได้รับการอนุญาตให้ลบโพสต์ได้หลังจากโพสต์ไปแล้ว %1 วัน(s) ", + "post-delete-duration-expired-days-hours": "คุณได้รับการอนุญาตให้ลบโพสต์ได้หลังจากโพสต์ไปแล้ว %1 วัน(s) %2 ชั่วโมง(s) ", + "cant-delete-topic-has-reply": "คุณไม่สามารถลบกระทู้ได้หลังจากกระทู้ของคุณถูกตอบกลับ", + "cant-delete-topic-has-replies": "คุณไม่สามารถลบกระทู้ได้หลังจากกระทู้มีจำนวนตอบกลับ %1 ", + "content-too-short": "กรุณาโพสต์ข้อความให้ยาวขึ้น โพสต์ควรมีข้อความอย่างน้อย %1 ตัวอักษร(s)", + "content-too-long": "กรุณาโพสต์ข้อความให้สั้นลง โพสต์ไม่สามารถยาวกว่า %1 ตัวอักษร(s)", + "title-too-short": "กรุณากรอกชื่อให้ยาวขึ้น ชื่อควรมีข้อความอย่างน้อย %1 ตัวอักษร(s)", + "title-too-long": "กรุณากรอกชื่อให้สั้นลง ชื่อไม่สามารถยาวกว่า %1 ตัวอักษร(s)", + "category-not-selected": "ไม่มีการเลือกหมวดหมู่", + "too-many-posts": "คุณสามารถโพสต์ได้เพียงครั้งเดียวเท่านั้นในทุกๆ %1 วินาที(s) - โปรดรอสักครู่ก่อนการโพสต์อีกครั้ง", + "too-many-posts-newbie": "เนื่องด้วยการเป็นผู้ใช้งานใหม่ คุณสามารถโพสต์ได้เพียงครั้งเดียวเท่านั้นในทุกๆ %1 วินาที(s) จนกว่าคุณจะได้รับ %2 ชื่อเสียง - โปรดรอสักครู่ก่อนการโพสต์อีกครั้ง", + "already-posting": "You are already posting", + "tag-too-short": "กรุณากรอกแท็กให้ยาวขึ้น แท็กควรมีข้อความอย่างน้อย %1 ตัวอักษร(s)", + "tag-too-long": "กรุณากรอกแท็กให้สั้นลง แท็กไม่สามารถยาวกว่า %1 ตัวอักษร(s)", + "not-enough-tags": "จำนวนแท็กไม่พอ กระทู้ต้องมีอย่างน้อย %1 แท็ก(s)", + "too-many-tags": "แท็กเยอะเกินไป กระทู้ไม่สามารถมีแท็กมากกว่า %1 แท็ก(s)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "กรุณารอการอัพโหลดเพื่อเสร็จสิ้น", + "file-too-big": "ขนาดไฟล์ที่ใหญ่ที่สุดที่ได้รับการอนุญาตคือ %1 kB - กรุณาอัพโหลดไฟล์ที่เล็กลง", + "guest-upload-disabled": "การอัพโหลดของ Guest ถูกปิด", + "cors-error": "Unable to upload image due to misconfigured CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "คุณได้ติดบุ๊กมาร์คของโพสต์นี้แล้ว", + "already-unbookmarked": "คุณได้ลบบุ๊กมาร์คของโพสต์นี้แล้ว", + "cant-ban-other-admins": "คุณแบนแอดมินไม่ได้!!!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "คุณเป็นแอดมินเพียงคนเดียว กรุณาเพิ่มผู้ใช้คนอื่นเป็นแอดมิน ก่อนการลบตัวเองออกจากแอดมิน", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "ลบสิทธิพิเศษของแอดมินจากบัญชีผู้ใช้นี้ ก่อนทำการลบ", + "already-deleting": "Already deleting", + "invalid-image": "Invalid image", + "invalid-image-type": "ประเภทรูปภาพไม่ถูกต้อง ประเภทที่ได้รับการอนุญาติคือ : %1", + "invalid-image-extension": "นามสกุลรูปภาพไม่ถูกต้อง", + "invalid-file-type": "ประเภทไฟล์ไม่ถูกต้อง ประเภทที่ได้รับการอนุญาติคือ : %1", + "invalid-image-dimensions": "Image dimensions are too big", + "group-name-too-short": "ชื่อกลุ่มสั้นเกินไป", + "group-name-too-long": "ชื่อกลุ่มยาวเกินไป", + "group-already-exists": "มีกลุ่มนี้อยู่แล้ว", + "group-name-change-not-allowed": "ชื่อกลุ่มที่เปลี่ยน ไม่ได้รับการอนุญาติ", + "group-already-member": "เป็นส่วนหนึ่งของกลุ่มนี้แล้ว", + "group-not-member": "ไม่ได้เป็นสมาชิกในกลุ่มนี้", + "group-needs-owner": "กลุ่มนี้ต้องการเจ้าของอย่างน้อย 1 คน", + "group-already-invited": "ผู้ใช้นี้ถูกเชิญแล้ว", + "group-already-requested": "คำร้องขอเป็นสมาชิกถูกส่งแล้ว", + "group-join-disabled": "You are not able to join this group at this time", + "group-leave-disabled": "You are not able to leave this group at this time", + "post-already-deleted": "โพสต์นี้ได้ถูกลบไปแล้ว", + "post-already-restored": "โพสต์นี้ถูกกู้คืนเรียบร้อยแล้ว", + "topic-already-deleted": "กระทู้นี้ถูกลบไปแล้ว", + "topic-already-restored": "กระทู้นี้ถูกกู้คืนเรียบร้อยแล้ว", + "cant-purge-main-post": "คุณไม่สามารถลบล้างโพสต์หลักได้ กรุณาลบกระทู้แทน ", + "topic-thumbnails-are-disabled": "ภาพตัวอย่างของกระทู้ถูกปิดใช้งาน", + "invalid-file": "ไฟล์ไม่ถูกต้อง", + "uploads-are-disabled": "การอัพโหลดถูกปิดใช้งาน", + "signature-too-long": "ขออภัย ลายเซ็นต์ของคุณไม่สามารถยาวเกิน %1 ตัวอักษร(s)ได้.", + "about-me-too-long": "ขออภัย \"เกี่ยวกับฉัน\" ของคุณไม่สามารถยาวเกิน %1 ตัวอักษร(s) ได้", + "cant-chat-with-yourself": "คุณไม่สามารถแชทกับตัวเองได้นะ!", + "chat-restricted": "ผู้ใช้นี้ถูกจำกัดข้อความแชท เขาต้องติดตามคุณก่อน คุณจึงจะสามารถแชทกับเขาได้", + "chat-disabled": "ระบบแชทถูกปิดใช้งาน", + "too-many-messages": "คุณได้ส่งข้อความมากเกินไป กรุณารอสักครู่", + "invalid-chat-message": "ข้อความแชทไม่ถูกต้อง", + "chat-message-too-long": "ข้อความแชทไม่สามารถยาวเกิน %1 ตัวอักษรได้", + "cant-edit-chat-message": "คุณไม่ได้รับอนุญาติให้แก้ไขข้อความ", + "cant-delete-chat-message": "คุณไม่ได้รับอนุญาตให้ลบข้อความ", + "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", + "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "คุณได้โหวตโพสต์นี้แล้ว", + "reputation-system-disabled": "ระบบชื่อเสียงถูกปิดใช้งาน", + "downvoting-disabled": "\"การโหวตลง\" ถูกปิดใช้งาน", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "You cannot vote on your own post", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "NodeBB encountered a problem while reloading: \"%1\". NodeBB will continue to serve the existing client-side assets, although you should undo what you did just prior to reloading.", + "registration-error": "การสมัครสมาชิกผิดพลาด", + "parse-error": "มีบางอย่างผิดพลาดขณะรอการตอบกลับจากเซิร์ฟเวอร์", + "wrong-login-type-email": "กรุณาใช้อีเมล์ของคุณในการเข้าสู่ระบบ", + "wrong-login-type-username": "กรุณาใช้ชื่อผู้ใช้ของคุณในการเข้าสู่ระบบ", + "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-multiple-association": "You cannot associate multiple accounts from this service to your NodeBB account. Please dissociate your existing account and try again.", + "invite-maximum-met": "คุณได้ทำการเชิญผู้คนจำนวนมากที่สุด (%1 out of %2).", + "no-session-found": "ไม่พบการเข้าสู่ระบบ", + "not-in-room": "ผู้ใช้ไม่อยู่ในห้อง", + "cant-kick-self": "คุณไม่สามารถเตะตัวเองออกจากกลุ่มได้", + "no-users-selected": "ไม่มีผู้ใช้ที่เลือก", + "invalid-home-page-route": "เส้นทางไปหน้าแรกผิดพลาด", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "No topics selected!", + "cant-move-to-same-topic": "Can't move post to same topic!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "You cannot block yourself!", + "cannot-block-privileged": "You cannot block administrators or global moderators", + "cannot-block-guest": "Guest are not able to block other users", + "already-blocked": "This user is already blocked", + "already-unblocked": "This user is already unblocked", + "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/th/flags.json b/public/language/th/flags.json new file mode 100644 index 0000000000..af2385c0ee --- /dev/null +++ b/public/language/th/flags.json @@ -0,0 +1,89 @@ +{ + "state": "สถานะ", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "ไชโย ไม่เจอธงใดๆเลย", + "assignee": "ผู้ได้รับมอบหมาย", + "update": "อัพเดท", + "updated": "ได้รับการอัพเดท", + "resolved": "Resolved", + "target-purged": "เนื้อหาที่ธงนี้อ้างถึงถูกลบออกและไม่มีอยู่ในระบบอีกต่อไป", + + "graph-label": "Daily Flags", + "quick-filters": "ฟิลเตอร์แบบด่วน", + "filter-active": "ไม่มีฟิลเตอร์ใดๆในรายการปักธง", + "filter-reset": "ลบฟิลเตอร์ออก", + "filters": "ตัวเลือกฟิลเตอร์", + "filter-reporterId": "ไอดีผู้รายงาน", + "filter-targetUid": "ไอดีผู้ที่ถูกปักธง", + "filter-type": "ประเภทการปักธง", + "filter-type-all": "เนื้อหาทั้งหมด", + "filter-type-post": "โพสต์", + "filter-type-user": "User", + "filter-state": "สถานะ", + "filter-assignee": "ผู้ได้รับมอบหมาย", + "filter-cid": "หมวดหมู่", + "filter-quick-mine": "ถูกมอบหมายให้ฉัน", + "filter-cid-all": "ทุกหมวดหมู่", + "apply-filters": "ใช้งานฟิลเตอร์", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "ผู้ใช้ที่ถูกปักธง", + "view-profile": "ดูโปรไฟล์", + "start-new-chat": "เริ่มแชทใหม่", + "go-to-target": "ดูเป้าหมายการปักธง", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "ดูโปรไฟล์", + "user-edit": "แก้ไขโปรไฟล์", + + "notes": "โน๊ตปักธง", + "add-note": "เพิ่มโน้ต", + "no-notes": "ไม่มีโน้ตที่แชร์", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "โน้ตถูกเพิ่มแล้ว", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "ไม่มีประวัติปักธง", + + "state-all": "สถานะทั้งหมด", + "state-open": "เพิ่มใหม่/เปิด", + "state-wip": "อยู่ระหว่างการทำงาน", + "state-resolved": "แก้แล้ว", + "state-rejected": "ถูกปฏิเสธ", + "no-assignee": "ไม่ได้รับการมอบหมาย", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "กรุณาระเหตุผลสำหรับการปักธง %1 %2 สำหรับการรีวิว หรือไม่ก็ใช้หนึ่งในปุ่มกดรายงานด่วนถ้าเป็นไปได้", + "modal-reason-spam": "สแปม", + "modal-reason-offensive": "น่ารังเกียจ", + "modal-reason-other": "อื่น ๆ (ระบุด้านล่าง)", + "modal-reason-custom": "เหตุผลว่าทำไมถึงรายงานเนื้อหานี้...", + "modal-submit": "ส่งรายงาน", + "modal-submit-success": "เนื้อหาถูกรายงานตามความเหมาะสมแล้ว", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/th/global.json b/public/language/th/global.json new file mode 100644 index 0000000000..21cad1c9c3 --- /dev/null +++ b/public/language/th/global.json @@ -0,0 +1,126 @@ +{ + "home": "หน้าแรก", + "search": "ค้นหา", + "buttons.close": "ปิด", + "403.title": "คุณถูกปฏิเสธการเข้าใช้", + "403.message": "ดูเหมือนว่าคุณจะได้รับการสกัดกั้นในหน้าเว็บที่คุณไม่สามารถเข้าถึงได้", + "403.login": "บางทีคุณควรจะ พยายามเข้าสู่ระบบภายใน ดูไหม ?", + "404.title": "ไม่พบ", + "404.message": "ดูเหมือนว่าคุณจะได้รับการสกัดกั้นในหน้าเว็บที่ไม่มีอยู่ กลับไปยัง หน้าแรก ", + "500.title": "ระบบภายในเกิดข้อผิดพลาด", + "500.message": "อุ่ย! มีสิ่งที่ไม่ถูกต้องเกิดขึ้น!", + "400.title": "คำร้องขอที่เลวร้าย", + "400.message": "มันเหมือนว่าลิ้งค์นี้ไม่สมประกอบ กรุณาตรวจสอบอีกครั้งและลองอีกครั้ง หรือไม่ก็กลับไปยัง หน้าแรก ", + "register": "ลงทะเบียน", + "login": "เข้าสู่ระบบ", + "please_log_in": "กรุณาเข้าสู่ระบบ", + "logout": "ออกจากระบบ", + "posting_restriction_info": "คุณต้องเป็นสมาชิกเพื่อทำการโพสต์ คลิกที่นี่เพื่อเข้าสู่ระบบ", + "welcome_back": "ยินดีต้อนรับ", + "you_have_successfully_logged_in": "คุณได้เข้าสู่ระบบแล้ว", + "save_changes": "บันทึกการเปลี่ยนแปลง", + "save": "บันทึก", + "close": "ปิด", + "pagination": "การแบ่งหน้า", + "pagination.out_of": "%1 จาก %2", + "pagination.enter_index": "Go to post index", + "header.admin": "ผู้ดูแลระบบ", + "header.categories": "หมวดหมู่", + "header.recent": "ล่าสุด", + "header.unread": "ไม่ได้อ่าน", + "header.tags": "แท็ก", + "header.popular": "ฮิต", + "header.top": "Top", + "header.users": "ผู้ใช้", + "header.groups": "กลุ่ม", + "header.chats": "สนทนา", + "header.notifications": "แจ้งเตือน", + "header.search": "ค้นหา", + "header.profile": "รายละเอียด", + "header.navigation": "เมนูนำทาง", + "notifications.loading": "กำลังโหลดข้อแจ้งเตือน", + "chats.loading": "กำลังโหลดหัวข้อสนทนา", + "motd.welcome": "ยินดีต้อนรับสู่ NodeBB แพลตฟอร์มการสนทนาแห่งอนาคต", + "previouspage": "หน้าก่อนหน้า", + "nextpage": "หน้าถัดไป", + "alert.success": "สำเร็จ", + "alert.error": "ผิดพลาด", + "alert.banned": "เเบน", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "คุณได้ยกเลิกติดตาม %1 !\n", + "alert.follow": "คุณกำลังติดตาม %1 !", + "users": "ผู้ใช้", + "topics": "กระทู้", + "posts": "กระทู้", + "x-posts": "%1 posts", + "best": "ดีที่สุด", + "controversial": "Controversial", + "votes": "โหวต", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "ผู้ที่โหวดขึ้น", + "upvoted": "โหวตแล้ว", + "downvoters": "ผู้ที่โหวตลง", + "downvoted": "โหวตลง", + "views": "ดู", + "posters": "Posters", + "reputation": "ชื่อเสียง", + "lastpost": "Last post", + "firstpost": "First post", + "read_more": "อ่านต่อ", + "more": "เพิ่มเติม", + "none": "None", + "posted_ago_by_guest": "โพสต์ %1 โดย Guest", + "posted_ago_by": "โพสต์ %1 โดย %2 ", + "posted_ago": "โพสต์ %1 ", + "posted_in": "โพสต์ใน %1 ", + "posted_in_by": "โพสต์ใน %1 โดย %2", + "posted_in_ago": "โพสต์ใน %1 %2", + "posted_in_ago_by": "โพสต์ใน %1 %2 โดย %3", + "user_posted_ago": "%1 โพสต์ %2", + "guest_posted_ago": "Guest โพสต์ %1", + "last_edited_by": "แก้ไขล่าสุดโดย %1", + "norecentposts": "ไม่มีกระทู้ล่าสุด", + "norecenttopics": "ไม่มีกระทู้ล่าสุด", + "recentposts": "กระทู้ล่าสุด", + "recentips": "IP ที่ใช้เข้าสู่ระบบล่าสุด", + "moderator_tools": "เครื่องมือผู้ดูแลระบบ", + "online": "ออนไลน์", + "away": "ไม่อยู่", + "dnd": "ห้ามรบกวน", + "invisible": "ไม่ปรากฏตัว", + "offline": "ออฟไลน์", + "email": "อีเมล์", + "language": "ภาษา", + "guest": "แขก", + "guests": "แขก", + "former_user": "A Former User", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "ฟอรั่มที่ถูกอัพเดทแล้ว", + "updated.message": "ฟอรั่มนี้เพิ่งได้รับการอัพเดทให้เป็นเวอร์ชั่นล่าสุด คลิกที่นี่เพื่อรีเฟรชหน้าเพจ", + "privacy": "ความเป็นส่วนตัว", + "follow": "ติดตาม", + "unfollow": "เลิกติดตาม", + "delete_all": "ลบทั้งหมด", + "map": "แผนที่", + "sessions": "ข้อมูลการเข้าสู่ระบบ", + "ip_address": "ไอพีแอดเดรส", + "enter_page_number": "กรอกหมายเลขหน้า", + "upload_file": "อัพโหลดไฟล์", + "upload": "อัพโหลด", + "uploads": "Uploads", + "allowed-file-types": "ประเภทไฟล์ที่ได้รับการอนุญาติคือ %1", + "unsaved-changes": "การเปลี่ยนแปลงของคุณจะไม่ได้รับการบันทึก คุณแน่ใจหรือว่าต้องการออกจากที่นี่?", + "reconnecting-message": "เหมือนกับว่าการเชื่อมต่อของคุณเพื่อไปยัง %1 นั้นขาดหาย กรุณารอสักครู่ เรากำลังพยายามเชื่อมต่อใหม่", + "play": "เล่น", + "cookies.message": "เว็บไวต์นี้ใช้คุกกี้เพื่อที่จะทำให้แน่ใจว่า คุณได้รับประสบการณ์ที่เยี่ยมยอดที่สุดในการเข้าใช้เว็บไซต์ของเรา", + "cookies.accept": "เข้าใจแล้ว!!! ", + "cookies.learn_more": "เรียนรู้เพิ่มเติม", + "edited": "ถูกแก้ไขแล้ว", + "disabled": "ปิด", + "select": "เลือก", + "user-search-prompt": "Type something here to find users..." +} \ No newline at end of file diff --git a/public/language/th/groups.json b/public/language/th/groups.json new file mode 100644 index 0000000000..e863ac408c --- /dev/null +++ b/public/language/th/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "กลุ่ม", + "view_group": "ดูกลุ่ม", + "owner": "เจ้าของกลุ่ม", + "new_group": "สร้างกลุ่มใหม่", + "no_groups_found": "ยังไม่มีกลุ่ม", + "pending.accept": "ยอมรับ", + "pending.reject": "ไม่ยอมรับ", + "pending.accept_all": "ยอมรับทั้งหมด", + "pending.reject_all": "ปฏิเสธทั้งหมด", + "pending.none": "ไม่มีสมาชิกที่รอการอนุมัติอยู่ในขณะนี้", + "invited.none": "ไม่มีสมาชิกที่ได้รับการเชิญในขณะนี้", + "invited.uninvite": "ยกเลิกคำเชิญ", + "invited.search": "ค้นหาสมาชิกเพื่อเชิญเข้ากลุ่ม", + "invited.notification_title": "คุณถูกเชิญเข้ากลุ่ม %1", + "request.notification_title": "คำขอเข้ากลุ่มจาก %1", + "request.notification_text": "%1 ได้รับเชิญให้เข้าเป็นสมาชิกของ %2", + "cover-save": "บันทึก", + "cover-saving": "กำลังบันทึก", + "details.title": "ข้อมูลกลุ่ม", + "details.members": "รายชื่อสมาชิก", + "details.pending": "สมาชิกที่กำลังรอการตอบรับ", + "details.invited": "สมาชิกที่ได้รับเชิญ", + "details.has_no_posts": "กลุ่มนี้ยังไม่มีโพสต์จากสมาชิก", + "details.latest_posts": "โพสล่าสุด", + "details.private": "ส่วนตัว", + "details.disableJoinRequests": "ปิดคำขอ", + "details.disableLeave": "Disallow users from leaving the group", + "details.grant": "ให้ / ยกเลิกการเป็นเจ้าของ", + "details.kick": "เตะออก", + "details.kick_confirm": "คุณแน่ใจใช่ไหมว่าต้องการลบสมาชิกคนนี้ออกจากกลุ่ม?", + "details.add-member": "Add Member", + "details.owner_options": "การจัดการกลุ่ม", + "details.group_name": "ชื่อกลุ่ม", + "details.member_count": "จำนวนสมาชิก", + "details.creation_date": "สร้างวันที่", + "details.description": "คำอธิบาย", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "สัญลักษณ์พรีวิว", + "details.change_icon": "เปลี่ยนไอคอน", + "details.change_label_colour": "Change Label Colour", + "details.change_text_colour": "Change Text Colour", + "details.badge_text": "สัญลักษณ์ข้อความ", + "details.userTitleEnabled": "แสดงสัญลักษณ์", + "details.private_help": "หากเป็นไปได้ การเข้าร่วมกลุ่มต้องได้รับการอนุมัติจากเจ้าของกลุ่ม", + "details.hidden": "ซ่อน", + "details.hidden_help": "หากเป็นไปได้ จะไม่แสดงในรายชื่อกลุ่ม และผู้ใช้จะต้องได้รับการเชิญเท่านั้นจึงจะเข้าเป็นสมาชิกได้", + "details.delete_group": "ลบกลุ่ม", + "details.private_system_help": "กลุ่มส่วนตัวถูกปิดใช้งานโดยเลเวลระบบ ตัวเลือกนี้จะไม่ได้ทำอะไรทั้งสิ้น", + "event.updated": "ข้อมูลกลุ่มได้รับการบันทึกแล้ว", + "event.deleted": "กลุ่ม \"%1\"  ได้ถูกลบไปแล้ว", + "membership.accept-invitation": "ยอมรับคำเชิญ", + "membership.accept.notification_title": "You are now a member of %1", + "membership.invitation-pending": "คำเชิญที่รอการอนุมัติ", + "membership.join-group": "เข้าร่วมกลุ่ม", + "membership.leave-group": "ออกจากกลุ่ม", + "membership.leave.notification_title": "%1 has left group %2", + "membership.reject": "ปฏิเสธ", + "new-group.group_name": "ชื่อกลุ่ม:", + "upload-group-cover": "อัพโหลดหน้าปกกลุ่ม", + "bulk-invite-instructions": "กรอกรายชื่อผู้ใช้ที่ต้องการเชิญเข้ากลุ่ม ถ้ามีจำนวนมากกว่า 1 ให้ใช้เครื่องหมาย คอมม่า , ในการแบ่ง", + "bulk-invite": "เชิญจำนวนมาก", + "remove_group_cover_confirm": "คุณแน่ใจแล้วใช่ไหมว่าต้องการจะลบภาพหน้าปกนี้?" +} \ No newline at end of file diff --git a/public/language/th/ip-blacklist.json b/public/language/th/ip-blacklist.json new file mode 100644 index 0000000000..588fbd62b6 --- /dev/null +++ b/public/language/th/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Configure your IP blacklist here.", + "description": "Occasionally, a user account ban is not enough of a deterrant. Other times, restricting access to the forum to a specific IP or a range of IPs is the best way to protect a forum. In these scenarios, you can add troublesome IP addresses or entire CIDR blocks to this blacklist, and they will be prevented from logging in to or registering a new account.", + "active-rules": "Active Rules", + "validate": "Validate Blacklist", + "apply": "Apply Blacklist", + "hints": "Syntax Hints", + "hint-1": "Define a single IP addresses per line. You can add IP blocks as long as they follow the CIDR format (e.g. 192.168.100.0/22).", + "hint-2": "You can add in comments by starting lines with the # symbol.", + + "validate.x-valid": "%1 out of %2 rule(s) valid.", + "validate.x-invalid": "The following %1 rules are invalid:", + + "alerts.applied-success": "Blacklist Applied", + + "analytics.blacklist-hourly": "Figure 1 – Blacklist hits per hour", + "analytics.blacklist-daily": "Figure 2 – Blacklist hits per day", + "ip-banned": "IP banned" +} \ No newline at end of file diff --git a/public/language/th/language.json b/public/language/th/language.json new file mode 100644 index 0000000000..16f46a1385 --- /dev/null +++ b/public/language/th/language.json @@ -0,0 +1,5 @@ +{ + "name": "ภาษาไทย", + "code": "th", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/th/login.json b/public/language/th/login.json new file mode 100644 index 0000000000..e8c17a34a2 --- /dev/null +++ b/public/language/th/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "ชื่อผู้ใช้ / อีเมล", + "username": "ชื่อผู้ใช้", + "remember_me": "จำไว้ในระบบ?", + "forgot_password": "ลืมรหัสผ่าน?", + "alternative_logins": "เข้าสู่ระบบโดยทางอื่น", + "failed_login_attempt": "เข้าสู่ระบบสำเร็จ", + "login_successful": "คุณเข้าสู่ระบบเรียบร้อยแล้ว", + "dont_have_account": "คุณยังไม่มีบัญชีเข้าระบบ?", + "logged-out-due-to-inactivity": "คุณได้ออกจากระบบ Admin Control Panel แล้ว เนื่องจากว่าไม่มีกิจกรรมใดๆมาสักระยะ", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/th/modules.json b/public/language/th/modules.json new file mode 100644 index 0000000000..5a5c70ae88 --- /dev/null +++ b/public/language/th/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "คุยกับ", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "ส่ง", + "chat.no_active": "คุณไม่มีแชทที่คุยอยู่", + "chat.user_typing": "%1 กำลังพิมพ์อยู่ ...", + "chat.user_has_messaged_you": "%1 ได้ส่งข้อความถึงคุณ", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "กรุณาเลือกผู้รับเพื่อดูประวัติข้อความ", + "chat.no-users-in-room": "ไม่มีผู้ใช้ในห้องนี้", + "chat.recent-chats": "แชทล่าสุด", + "chat.contacts": "ติดต่อ", + "chat.message-history": "ประวัติข้อความ", + "chat.message-deleted": "Message Deleted", + "chat.options": "ตัวเลือกแชท", + "chat.pop-out": "Pop out แชท", + "chat.minimize": "ย่อเล็กสุด", + "chat.maximize": "ขยายใหญ่สุด", + "chat.seven_days": "7 วัน", + "chat.thirty_days": "30 วัน", + "chat.three_months": "3 เดือน", + "chat.delete_message_confirm": "คุณแน่ใจแล้วใช่ไหมว่าต้องการจะลบข้อความนี้?", + "chat.retrieving-users": "กำลังเรียกข้อมูลผู้ใช้", + "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 () may remove users from chat rooms.", + "chat.confirm-chat-with-dnd-user": "ผู้ใช้นี้ได้ตั้งค่าสถานะเป็น (ห้ามรบกวน) คุณยังอยากจะคุยกับเขาอยู่ไหม?", + "chat.rename-room": "Rename Room", + "chat.rename-placeholder": "ใส่ชื่อห้องของคุณที่นี่", + "chat.rename-help": "ชื่อห้องจะสามารถดูได้โดยผู้เข้าร่วมทั้งหมดในห้อง", + "chat.leave": "ออกแชท", + "chat.leave-prompt": "คุณแน่ใจหรือไม่ว่าต้องการออกจากการแชทนี้", + "chat.leave-help": "การออกจากการแชทนี้จะเป็นการลบคุณออกจากการติดต่อในอนาคตในการแชทนี้ หากคุณถูกเพิ่มเข้ามาใหม่ในอนาคตคุณจะไม่เห็นประวัติการแชทก่อนที่จะเข้าร่วมใหม่", + "chat.in-room": "ในห้องนี้", + "chat.kick": "Kick", + "chat.show-ip": "Show IP", + "chat.owner": "Room Owner", + "chat.system.user-join": "%1 has joined the room", + "chat.system.user-leave": "%1 has left the room", + "chat.system.room-rename": "%2 has renamed this room: %1", + "composer.compose": "เขียน", + "composer.show_preview": "แสดงพรีวิว", + "composer.hide_preview": "ซ่อนพรีวิว", + "composer.user_said_in": "%1 พูดใน %2:", + "composer.user_said": "%1 พูด:", + "composer.discard": "คุณแน่ใจแล้วใช่ไหมว่าจะทิ้งโพสต์นี้?", + "composer.submit_and_lock": "ยืนยันและล็อก", + "composer.toggle_dropdown": "ท็อกเกิลดร็อปดาวน์", + "composer.uploading": "กำลังอัพโหลด %1", + "composer.formatting.bold": "ตัวหนา", + "composer.formatting.italic": "ตัวเอียง", + "composer.formatting.list": "รายการ", + "composer.formatting.strikethrough": "ขีดเส้นใต้", + "composer.formatting.code": "Code", + "composer.formatting.link": "ลิ้งค์", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "อัพโหลดรูปภาพ", + "composer.upload-file": "อัพโหลดไฟล์", + "composer.zen_mode": "เซ็นโหมด", + "composer.select_category": "เลือกหมวดหมู่", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "ตกลง", + "bootbox.cancel": "ยกเลิก", + "bootbox.confirm": "ยืนยัน", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "ตำแหน่งภาพหน้าปก", + "cover.dragging_message": "ลากภาพหน้าปกเพื่อเลือกตำแหน่งแล้วกด \"บันทึก\"", + "cover.saved": "ภาพหน้าปกและตำแหน่งได้รับการบันทึกแล้ว", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/th/notifications.json b/public/language/th/notifications.json new file mode 100644 index 0000000000..96809e48cb --- /dev/null +++ b/public/language/th/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "แจ้งเตือน", + "no_notifs": "คุณไม่มีข้อแจ้งเตือนใหม่", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "กลับสู่ %1", + "outgoing_link": "ลิงค์ออก", + "outgoing_link_message": "ตอนนี้คุณกำลังออกจาก %1", + "continue_to": "ดำเนินการต่อไปยัง %1", + "return_to": "กลับสู่ %1", + "new_notification": "You have a new notification", + "you_have_unread_notifications": "คุณมีคำเตือนที่ยังไม่ได้อ่าน", + "all": "ทั้งหมด", + "topics": "กระทู้", + "replies": "คำตอบ", + "chat": "แชท", + "group-chat": "Group Chats", + "follows": "ติดตาม", + "upvote": "โหวตขึ้น", + "new-flags": "ปักธงใหม่", + "my-flags": "ธงที่ถูกปักให้ฉัน", + "bans": "แบน", + "new_message_from": "ข้อความใหม่จาก %1", + "upvoted_your_post_in": "%1 ได้โหวตโพสต์ของคุณขึ้นใน %2", + "upvoted_your_post_in_dual": "%1 และ %2ได้โหวตโพสต์ของคุณขึ้นใน %3 ", + "upvoted_your_post_in_multiple": "%1 และคืนอื่นๆอีก %2 คน ได้โหวตโพสต์ของคุณขึ้นใน %3", + "moved_your_post": "%1 ได้ย้ายโพสต์ของคุณไปยัง %2", + "moved_your_topic": "%1 ได้ย้าย %2", + "user_flagged_post_in": "%1 ได้ปักธงโพสต์ใน %2", + "user_flagged_post_in_dual": "%1และ %2ได้ปักธงโพสต์ใน %3", + "user_flagged_post_in_multiple": "%1 และคนอื่นๆอีก %2 ได้ปักธงโพสต์ใน %3", + "user_flagged_user": "%1 ได้ปักธงโปรไฟล์ผู้ใช้ (%2)", + "user_flagged_user_dual": "%1และ%2ได้ปักธงโปรไฟล์ผู้ใช้ (%3)", + "user_flagged_user_multiple": "%1และคืนอื่นๆอีก %2 ได้ปักธงโปรไฟล์ผู้ใช้ (%3)", + "user_posted_to": "%1 ได้โพสต์คำตอบไปยัง : %2", + "user_posted_to_dual": "%1และ %2ได้โพสต์คำตอบไปยัง : %3 ", + "user_posted_to_multiple": "%1และคนอื่นๆอีก %2 ได้โพสต์คำตอบไปยัง : %3", + "user_posted_topic": "%1ได้โพสต์กระทู้ใหม่ : %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 ได้เริ่มติดตามคุณ", + "user_started_following_you_dual": "%1และ%2ได้เริ่มติดตามคุณ", + "user_started_following_you_multiple": "%1และคืนอื่นๆอีก %2  คนได้เริ่มติดตามคุณ", + "new_register": "%1ได้ส่งคำขอสมัครสมาชิก", + "new_register_multiple": "มี%1คำขอสมัครสมาชิกที่รอการรีวิว", + "flag_assigned_to_you": "ปักธง %1ได้ถูกปักธงให้คุณ", + "post_awaiting_review": "โพสกำลังรอการพิจารณา", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Email ได้รับการยืนยันแล้ว", + "email-confirmed-message": "ขอบคุณที่ยืนยัน Email ของคุณ บัญชีของคุณสามารถใช้งานได้แล้ว", + "email-confirm-error-message": "มีปัญหาในการยืนยัน Email ของคุณ บางทีรหัสไม่ถูกต้องหรือหมดอายุแล้ว", + "email-confirm-sent": "Email เพื่อยืนยันได้ส่งไปแล้ว", + "none": "ไม่มี", + "notification_only": "แจ้งเตือนอย่างเดียว", + "email_only": "อีเมลอย่างเดียว", + "notification_and_email": "การแจ้งเตือนและอีเมล", + "notificationType_upvote": "เมื่อมีคนโหวตอัพให้โพสต์คุณ", + "notificationType_new-topic": "เมื่อมีคนติดตามโพสต์คุณ", + "notificationType_new-reply": "เมื่อมีการตอบกลับในโพสต์ที่คุณกำลังติดตาม", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "เมื่อมีคนติดตามคุณ", + "notificationType_new-chat": "เมื่อคุณได้รับข้อความใหม่", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "เมื่อคุณได้รับเชิญเข้ากลุ่ม", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_new-register": "เมื่อมีคนถูกเพิ่มในคิวลงทะเบียน", + "notificationType_post-queue": "เมื่อมีโพสต์ใหม่อยู่ในคิว", + "notificationType_new-post-flag": "เมื่อโพสต์ถูกตั้งค่าสถานะ", + "notificationType_new-user-flag": "เมื่อผู้ใช้ถูกตั้งค่าสถานะ" +} \ No newline at end of file diff --git a/public/language/th/pages.json b/public/language/th/pages.json new file mode 100644 index 0000000000..df7deb0d8a --- /dev/null +++ b/public/language/th/pages.json @@ -0,0 +1,65 @@ +{ + "home": "หน้าแรก", + "unread": "กระทู้ที่ไม่ได้อ่าน", + "popular-day": "กระทู้ฮิตวันนี้", + "popular-week": "กระทู้ฮิตสัปดาห์นี้", + "popular-month": "กระทู้ฮิตเดือนนี้", + "popular-alltime": "กระทู้ฮิตตลาดกาล", + "recent": "กระทู้ล่าสุด", + "top-day": "Top voted topics today", + "top-week": "Top voted topics this week", + "top-month": "Top voted topics this month", + "top-alltime": "Top Voted Topics", + "moderator-tools": "เครื่องมือผู้ดูแลระบบ", + "flagged-content": "เนื้อหาที่ถูกปักธง", + "ip-blacklist": "ไอดีที่ถูกขึ้นบัญชีดำ", + "post-queue": "คิวโพส", + "users/online": "ผู้ใช้ออนไลน์", + "users/latest": "ผู้ใช้ล่าสุด", + "users/sort-posts": "ผู้ใช้ที่โพสต์เยอะที่สุด", + "users/sort-reputation": "ผู้ใช้ที่มีชื่อเสียงโด่งดังที่สุด", + "users/banned": "ผู้ใช้ที่ถูกแบน", + "users/most-flags": "ผู้ใช้ที่ถูกปักธงมากที่สุด", + "users/search": "ค้นหาผู้ใช้", + "notifications": "การแจ้งเตือน", + "tags": "แท็ก", + "tag": "หัวข้อที่ติดแท็กใต้ "%1"", + "register": "สมัครบัญชีผู้ใช้", + "registration-complete": "สมัครสมาชิกสำเร็จเรียบร้อย", + "login": "เข้าสู่ระบบบัญชีของคุณ", + "reset": "ตั้งค่ารหัสผ่านใหม่ให้บัญชีของคุณ", + "categories": "หมวดหมู่", + "groups": "กลุ่ม", + "group": "%1 กลุ่ม", + "chats": "แชท", + "chat": "กำลังแชทกับ %1 ", + "flags": "ธง", + "flag-details": "ธง %1 รายละเอียด", + "account/edit": "กำลังแก้ไข \"%1\"", + "account/edit/password": "กำลังแก้ไขรหัสผ่านของ \"%1\"", + "account/edit/username": "กำลังแก้ไขชื่อผู้ใช้ \"%1\"", + "account/edit/email": "กำลังแก้ไขอีเมล์ของ \"%1\"", + "account/info": "ข้อมูลบัญชี", + "account/following": "ผู้คน %1 ติดตาม", + "account/followers": "ผู้คนที่ติดตาม %1", + "account/posts": "โพสต์ถูกสร้างโดย %1", + "account/latest-posts": "Latest posts made by %1", + "account/topics": "กระทู้ถูกสร้างโดย %1", + "account/groups": "กลุ่มของ %1", + "account/watched_categories": "%1's Watched Categories", + "account/bookmarks": "บุ๊กมาร์คโพสต์ของ %1", + "account/settings": "การตั้งค่าผู้ใช้", + "account/watched": "กระทู้ที่ถูกดูโดย %1", + "account/ignored": "กระทู้ที่ถูกละเว้นโดย %1", + "account/upvoted": "โพสต์ที่ถูกโหวตขึ้นโดย %1", + "account/downvoted": "โพสต์ที่โหวตลงโดย %1", + "account/best": "โพสต์ดีที่สุดที่ถูกสร้างโดย %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Blocked users for %1", + "account/uploads": "Uploads by %1", + "account/sessions": "Login Sessions", + "confirm": "อีเมล์ได้รับการยืนยันแล้ว", + "maintenance.text": "%1 กำลังอยู่ระหว่างการปิดปรับปรุงชั่วคราว กรุณาลองใหม่อีกครั้งในภายหลัง", + "maintenance.messageIntro": "ผู้ดูแลระบบได้ฝากข้อความต่อไปนี้เอาไว้", + "throttled.text": "%1 ไม่สามารถเข้าถึงได้ในขณะนี้เนื่องจากมีการโหลดที่หนักมากเกินไป กรุณากลับเข้ามาอีกครั้งในภายหลัง" +} \ No newline at end of file diff --git a/public/language/th/post-queue.json b/public/language/th/post-queue.json new file mode 100644 index 0000000000..e80369c2a7 --- /dev/null +++ b/public/language/th/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Post Queue", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "User", + "category": "Category", + "title": "Title", + "content": "Content", + "posted": "Posted", + "reply-to": "Reply to \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/th/recent.json b/public/language/th/recent.json new file mode 100644 index 0000000000..6e4d87131f --- /dev/null +++ b/public/language/th/recent.json @@ -0,0 +1,19 @@ +{ + "title": "ล่าสุด", + "day": "วัน", + "week": "สัปดาห์", + "month": "เดือน", + "year": "ปี", + "alltime": "ตลอดกาล", + "no_recent_topics": "ไม่มีกระทู้ล่าสุด", + "no_popular_topics": "ไม่มีกระทู้ฮิต", + "there-is-a-new-topic": "มีกระทู้ใหม่", + "there-is-a-new-topic-and-a-new-post": "มีกระทู้ใหม่และโพสต์ใหม่", + "there-is-a-new-topic-and-new-posts": "มีกระทู้ใหม่และ %1  โพสต์ใหม่", + "there-are-new-topics": "มี %1  กระทู้ใหม่", + "there-are-new-topics-and-a-new-post": "มี %1  กระทู้ใหม่และโพสต์ใหม่", + "there-are-new-topics-and-new-posts": "มี %1  กระทู้ใหม่และ %2 โพสต์ใหม่", + "there-is-a-new-post": "มีโพสต์ใหม่", + "there-are-new-posts": "มี %1  โพสต์ใหม่", + "click-here-to-reload": "คลิกที่นี่เพื่อโหลดใหม่อีกครั้ง" +} \ No newline at end of file diff --git a/public/language/th/register.json b/public/language/th/register.json new file mode 100644 index 0000000000..ab6b9d4cc3 --- /dev/null +++ b/public/language/th/register.json @@ -0,0 +1,32 @@ +{ + "register": "ลงทะเบียน", + "cancel_registration": "ยกเลิกการสมัคร", + "help.email": "ผู้ใช้อื่น ๆ จะไม่สามารถมองเห็นอีเมลของคุณโดยดีฟอลต์", + "help.username_restrictions": "ชื่อผู้ใช้ที่ไม่ซ้ำกับผู้อื่น จะต้องมีความยาวระหว่าง% %1 และ %2 ตัวอักษร ผู้ใช้อื่นๆ สามารถพูดถึงคุณโดย @ชื่อผู้ใช้", + "help.minimum_password_length": "ความยาวรหัสผ่านของคุณต้องมีอย่างน้อย %1 ตัวอักษร", + "email_address": "อีเมล์", + "email_address_placeholder": "ใส่อีเมล์", + "username": "ชื่อผู้ใช้", + "username_placeholder": "ใส่ชื่อผู้ใช้", + "password": "รหัสผ่าน", + "password_placeholder": "ใส่รหัสผ่าน", + "confirm_password": "ยืนยันรหัสผ่าน", + "confirm_password_placeholder": "ยืนยันรหัสผ่าน", + "register_now_button": "ลงทะเบียน", + "alternative_registration": "เข้าสู่ระบบโดยทางอื่น", + "terms_of_use": "เงื่อนไขการใช้งาน", + "agree_to_terms_of_use": "ยอมรับข้อตกลงในการใช้งาน", + "terms_of_use_error": "คุณต้องยอมรับเงื่อนไขการใช้งานก่อน", + "registration-added-to-queue": "การสมัครใช้งานของถูกเพิ่มเข้าไปยังระบบเพิ่อรอการอนุมัติแล้ว คุณจะได้รับอีเมล์เมื่อการสมัครใช้งานของคุณถูกยอมรับโดยผู้ดูแลระบบหรือแอดมิน", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "I consent to the collection and processing of my personal information on this website.", + "gdpr_agree_email": "I consent to receive digest and notification emails from this website.", + "gdpr_consent_denied": "You must give consent to this site to collect/process your information, and to send you emails.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/th/reset_password.json b/public/language/th/reset_password.json new file mode 100644 index 0000000000..0448d48e55 --- /dev/null +++ b/public/language/th/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "รีเซ็ตรหัสผ่าน", + "update_password": "ปรับปรุงรหัสผ่าน", + "password_changed.title": "รหัสผ่านได้เปลี่ยนแปลงแล้ว", + "password_changed.message": "

ตั้งค่ารหัสผ่านสำเร็จ กรุณาเข้าสู่ระบบอีกครั้ง", + "wrong_reset_code.title": "รหัสรีเซ็ตไม่ถูกต้อง", + "wrong_reset_code.message": "รหัสรีเซ็ตที่ได้รับไม่ถูกต้อง กรุณาลองใหม่อีกครั้งหรือ ขอรหัสรีเซ็ตใหม่", + "new_password": "รหัสผ่านใหม่", + "repeat_password": "ยืนยันรหัสผ่าน", + "changing_password": "Changing Password", + "enter_email": "กรุณาใส่อีเมลของคุณ เราจะส่งอีเมลให้คุณพร้อมคำแนะนำเกี่ยวกับวิธีการรีเซ็ตบัญชีของคุณ", + "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.", + "invalid_email": "อีเมล์ไม่ถูกต้อง / อีเมล์ไม่มีอยู่!", + "password_too_short": "รหัสผ่านที่คุณกำหนดยังสั้นเกินไป กรุณากำหนดรหัสผ่านของคุณใหม่", + "passwords_do_not_match": "รหัสผ่านทั้ง 2 ที่ใส่ไม่ตรงกัน", + "password_expired": "รหัสผ่านของคุณหมดอายุแล้ว กรุณาเลือกรหัสผ่านใหม่" +} \ No newline at end of file diff --git a/public/language/th/search.json b/public/language/th/search.json new file mode 100644 index 0000000000..5cdb898b5d --- /dev/null +++ b/public/language/th/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 ผลลัพธ์ ตรงตามที่ระบุ \"%2\", (%3 วินาที)", + "no-matches": "ไม่พบผลลัพธ์ที่สอดคล้อง", + "advanced-search": "การค้นหาแบบละเอียด", + "in": "ใน", + "titles": "หัวข้อ", + "titles-posts": "หัวข้อ และ ข้อความ", + "match-words": "Match words", + "all": "All", + "any": "Any", + "posted-by": "บันทึกโดย", + "in-categories": "ในหมวดหมู่", + "search-child-categories": "ค้นหาหมวดหมู่ย่อย", + "has-tags": "มีแท็ก", + "reply-count": "จำนวนข้อความตอบกลับ", + "at-least": "อย่างน้อยที่สุด", + "at-most": "อย่างมากที่สุด", + "relevance": "ที่เกี่ยวข้อง", + "post-time": "เวลาโพสต์", + "votes": "Votes", + "newer-than": "ใหม่กว่า", + "older-than": "เก่ากว่า", + "any-date": "วันที่ใดๆ", + "yesterday": "เมื่อวาน", + "one-week": "1 สัปดาห์", + "two-weeks": "2 สัปดาห์", + "one-month": "1 เดือน", + "three-months": "3 เดือน", + "six-months": "6 เดือน", + "one-year": "1 ปี", + "sort-by": "จัดเรียงโดย", + "last-reply-time": "เวลาตอบกลับล่าสุด", + "topic-title": "หัวข้อกระทู้", + "topic-votes": "Topic votes", + "number-of-replies": "จำนวนข้อความตอบกลับ", + "number-of-views": "จำนวนดู", + "topic-start-date": "วันที่เริ่มกระทู้", + "username": "ชื่อผู้ใช้", + "category": "หมวดหมู่", + "descending": "เรียงจากมากไปน้อย", + "ascending": "เรียงจากน้อยไปมาก", + "save-preferences": "บันทึกการตั้งค่า", + "clear-preferences": "ล้างการตั้งค่า", + "search-preferences-saved": "ค้นหาการตั้งค่าที่บันทึกไว้", + "search-preferences-cleared": "ค้นหาการตั้งค่าที่ลบล้างไป", + "show-results-as": "แสดงผลลัพธ์แบบ", + "see-more-results": "See more results (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/th/success.json b/public/language/th/success.json new file mode 100644 index 0000000000..7751c7b027 --- /dev/null +++ b/public/language/th/success.json @@ -0,0 +1,7 @@ +{ + "success": "สำเร็จ", + "topic-post": "คุณลงข้อความสำเร็จแล้ว", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "การระบุตัวตนสำเร็จแล้ว", + "settings-saved": "การตั้งค่าได้ถูกบันทึกแล้ว" +} \ No newline at end of file diff --git a/public/language/th/tags.json b/public/language/th/tags.json new file mode 100644 index 0000000000..5eb9dfdfb5 --- /dev/null +++ b/public/language/th/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "ไม่มีหัวข้อสนทนาที่เกี่ยวข้องกับป้ายคำศัพท์นี้", + "tags": "ป้ายคำศัพท์", + "enter_tags_here": "กรอกแท็กที่นี่ จำนวนอักขระอยู่ระหว่าง %1 และ %2 ตัวอักษรต่อ 1 แท็ก", + "enter_tags_here_short": "ใส่ป้ายคำศัพท์ ...", + "no_tags": "ยังไม่มีป้ายคำศัพท์", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/th/top.json b/public/language/th/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/th/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/th/topic.json b/public/language/th/topic.json new file mode 100644 index 0000000000..f26eba5345 --- /dev/null +++ b/public/language/th/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "กระทู้", + "title": "Title", + "no_topics_found": "ไม่พบกระทู้", + "no_posts_found": "ไม่พบโพส", + "post_is_deleted": "ลบ Post นี้เรียบร้อยแล้ว!", + "topic_is_deleted": "กระทู้นี้ถูกลบไปแล้ว!", + "profile": "รายละเอียด", + "posted_by": "โพสโดย %1", + "posted_by_guest": "โพสโดย Guest", + "chat": "แชท", + "notify_me": "แจ้งเตือนเมื่อการตอบใหม่ในกระทู้นี้", + "quote": "คำอ้างอิง", + "reply": "ตอบ", + "replies_to_this_post": " %1 คำตอบ", + "one_reply_to_this_post": "1 การตอบกลับ", + "last_reply_time": "คำตอบล่าสุด", + "reply-as-topic": "ตอบโดยตั้งกระทู้ใหม่", + "guest-login-reply": "เข้าสู่ระบบเพื่อตอบกลับ", + "login-to-view": "🔒 Log in to view", + "edit": "แก้ไข", + "delete": "ลบ", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "ล้าง", + "restore": "กู้", + "move": "ย้าย", + "change-owner": "Change Owner", + "fork": "แยก", + "link": "ลิงค์", + "share": "แชร์", + "tools": "เครื่องมือ", + "locked": "ถูกล็อก", + "pinned": "ถูกปักหมุด", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "ถูกย้าย", + "moved-from": "Moved from %1", + "copy-ip": "คัดลอก IP", + "ban-ip": "แบน IP", + "view-history": "แก้ไขประวัติ", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "คลิกที่นี่เพื่อกลับไปยังโพสต์ล่าสุดในหัวข้อนี้", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Topic นี้ถูกลบไปแล้ว เฉพาะผู้ใช้งานที่มีสิทธิ์ในการจัดการ Topic เท่านั้นที่จะมีสิทธิ์ในการเข้าชม", + "following_topic.message": "คุณจะได้รับการแจ้งเตือนเมื่อมีคนโพสต์ในกระทู้นี้", + "not_following_topic.message": "คุณจะเห็นกระทู้นี้ในรายการของกระทู้ที่ยังไม่ได้อ่าน แต่คุณจะไม่ได้รับการแจ้งเตือนเมื่่อมีคนตอบกระทู้นี้", + "ignoring_topic.message": "คุณจะไม่เห็นกระทู้นี้ในรายการของกระทู้ที่ยังไม่ได้อ่านอีกต่อไป คุณจะได้รับการแจ้งเตือนเมื่อมีคนกล่าวถึงคุณหรือโพสต์ของคุณถูกโหวตขึ้น", + "login_to_subscribe": "กรุณาลงทะเบียนหรือเข้าสู่ระบบเพื่อที่จะติดตามกระทู้นี้", + "markAsUnreadForAll.success": "ทำเครื่องหมายว่ายังไม่ได้อ่านทั้งหมด", + "mark_unread": "ถูกมาร์คว่ายังไม่ได้อ่าน", + "mark_unread.success": "กระทู้ที่ถูกมาร์คว่ายังไม่ได้อ่าน", + "watch": "ติดตาม", + "unwatch": "ยังไม่ได้ติดตาม", + "watch.title": "ให้แจ้งเตือนเมื่อมีการตอบกลับ Topic นี้", + "unwatch.title": "ยกเลิกการติดตาม Topic นี้", + "share_this_post": "แชร์โพสต์นี้", + "watching": "กำลังดู", + "not-watching": "ไม่ดูแล้ว", + "ignoring": "ความเมินเฉย", + "watching.description": "เตือนฉันเมื่อมีคำตอบใหม่
แสดงกระทู้ในรายการที่ยังไม่ได้อ่าน", + "not-watching.description": "อย่าเตือนฉันเมือมีคำตอบใหม่
แสดงกระทู้ในรายการที่ยังไม่ได้อ่านหากหมวดหมู่นี้ไม่ได้รับการเมินเฉย", + "ignoring.description": "อย่าเตือนฉันเมื่อมีคำตอบใหม่
อย่าแสดงกระทู้ในรายการที่ยังไม่ได้อ่าน", + "thread_tools.title": "เครื่องมือช่วยจัดการ Topic", + "thread_tools.markAsUnreadForAll": "มาร์คว่ายังไม่ยังอ่านทั้งหมด", + "thread_tools.pin": "ปักหมุดกระทู้", + "thread_tools.unpin": "เลิกปักหมุดกระทู้", + "thread_tools.lock": "ล็อคกระทู้", + "thread_tools.unlock": "ปลดล็อคกระทู้", + "thread_tools.move": "ย้ายกระทู้", + "thread_tools.move-posts": "Move Posts", + "thread_tools.move_all": "ย้ายทั้งหมด", + "thread_tools.change_owner": "Change Owner", + "thread_tools.select_category": "เลือกประเภท", + "thread_tools.fork": "แยกกระทู้", + "thread_tools.delete": "ลบกระทู้", + "thread_tools.delete-posts": "ลบโพสต์", + "thread_tools.delete_confirm": "มั่นใจแล้วหรือไม่ที่จะลบ Topic นี้?", + "thread_tools.restore": "กู้กระทู้", + "thread_tools.restore_confirm": "มั่นใจแล้วหรือไม่ที่จะกู้คืน Topic นี้?", + "thread_tools.purge": "ล้างกระทู้", + "thread_tools.purge_confirm": "คุณแน่ใจแล้วใช้ไมว่าต้องการล้างกระทู้นี้?", + "thread_tools.merge_topics": "รวมกระทู้", + "thread_tools.merge": "รวม", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "คุณแน่ใจแล้วใช่ไหมว่าต้องการลบโพสต์นี้", + "post_restore_confirm": "คุณแน่ใจแล้วใช้ไหมว่าต้องการกู้คืนโพสต์นี้", + "post_purge_confirm": "คุณแน่ใจแล้วใช่ไหมว่าต้องการล้างโพสต์นี้", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "กำลังโหลดหมวดหมู่", + "confirm_move": "ย้าย", + "confirm_fork": "แยก", + "bookmark": "บุ๊กมาร์ก", + "bookmarks": "บุ๊กมาร์ก", + "bookmarks.has_no_bookmarks": "คุณยังไม่มีบุ๊กมาร์กใดๆเลย", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "โหลดโพสเพิ่มเติม", + "move_topic": "ย้ายกระทู้", + "move_topics": "ย้ายกระทู้", + "move_post": "ย้ายโพส", + "post_moved": "โพสต์ถูกย้ายแล้ว!", + "fork_topic": "แยกกระทู้", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "คลิกที่โพสที่คุณต้องการที่จะแยก", + "fork_no_pids": "ไม่มีโพสต์ที่เลือก!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": " %1 โพสต์(s) ที่เลือก", + "fork_success": "แตกกระทู้สำเร็จแล้ว! คลิกที่นี่เพื่อไปยั้งกระทู้ที่คุณแตกประเด็น", + "delete_posts_instruction": "คลิกโพสต์ที่คุณต้องการลบ/ล้าง", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Click the posts you want to assign to another user", + "composer.title_placeholder": "ป้อนชื่อกระทู้ของคุณที่นี่ ...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "ยกเลิก", + "composer.submit": "ส่ง", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "ตอบไปยัง %1", + "composer.new_topic": "กระทู้ใหม่", + "composer.editing": "Editing", + "composer.uploading": "กำลังอัพโหลด ...", + "composer.thumb_url_label": "วาง URL ของภาพของกระทู้นี้", + "composer.thumb_title": "เพิ่มภาพให้กับกระทู้นี้", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "หรืออัปโหลดไฟล์", + "composer.thumb_remove": "ล้างฟิลด์", + "composer.drag_and_drop_images": "ลากและวางภาพที่นี่", + "more_users_and_guests": "ผู้ใช้อีก %1 คน (s) และ %2 guest(s)", + "more_users": "อีก %1 ผู้ใช้(s)", + "more_guests": "อีก %1 guest(s)", + "users_and_others": " %1 และคนอื่นๆอีก %2 ", + "sort_by": "เรียงตาม", + "oldest_to_newest": "เก่าสุดไปยังใหม่สุด", + "newest_to_oldest": "ใหม่สุดไปยังเก่าสุด", + "most_votes": "Most Votes", + "most_posts": "Most Posts", + "most_views": "Most Views", + "stale.title": "ตั้งกระทู้ใหม่แทนไหม?", + "stale.warning": "กระทู้ที่คุณกำลังตอบเก่าไปหน่อยนะ อยากจะลองตั้งกระทู้ใหม่แทนไหมล่ะ? แล้วก็อ้างอิงกระทู้นี้ไปยังคำตอบของคุณ", + "stale.create": "ตั้งกระทู้ใหม่", + "stale.reply_anyway": "ตอบกระทู้นี้ไม่ว่ายังไงก็ตาม", + "link_back": "ตอบกลับ: [%1](%2)", + "diffs.title": "แก้ไขประวัติโพสต์", + "diffs.description": "โพสนี้มี %1 การแก้ไข คลิกที่การแก้ไขด้านล่างเพื่อดูเนื้อหาโพสต์ตามเวลาที่เลือก", + "diffs.no-revisions-description": "โพสนี้มี %1การแก้ไข", + "diffs.current-revision": "current revision", + "diffs.original-revision": "original revision", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 later", + "timeago_earlier": "%1 earlier", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/th/unread.json b/public/language/th/unread.json new file mode 100644 index 0000000000..12fc7c31d2 --- /dev/null +++ b/public/language/th/unread.json @@ -0,0 +1,15 @@ +{ + "title": "ไม่ได้อ่าน", + "no_unread_topics": "ไม่มีกระทู้ที่ยังไม่ได้อ่านเป็น", + "load_more": "โหลดเพิ่มเติม", + "mark_as_read": "ทำเครื่องหมายว่าอ่านแล้ว", + "selected": "เลือก", + "all": "ทั้งหมด", + "all_categories": "หมวดหมู่ทั้งหมด", + "topics_marked_as_read.success": "Topic ถูกทำเครื่องหมายว่าอ่านแล้วเรียบร้อย", + "all-topics": "กระทู้ทั้งหมด", + "new-topics": "ตั้งกระทู้ใหม่", + "watched-topics": "กระทู้ที่ดูแล้ว", + "unreplied-topics": "กระทู้ที่ไม่ได้ตอบ", + "multiple-categories-selected": "เลือกหลายรายการ" +} \ No newline at end of file diff --git a/public/language/th/uploads.json b/public/language/th/uploads.json new file mode 100644 index 0000000000..8b189723fb --- /dev/null +++ b/public/language/th/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "กำลังอัพโหลดไฟล์ ...", + "select-file-to-upload": "กรุณาเลือกไฟล์ที่จะอัพโหลด", + "upload-success": "อัพโหลดไฟล์เรียบร้อยแล้ว", + "maximum-file-size": "มากที่สุดได้ %1 kb", + "no-uploads-found": "No uploads found", + "public-uploads-info": "Uploads are public, all visitors can see them.", + "private-uploads-info": "Uploads are private, only logged in users can see them." +} \ No newline at end of file diff --git a/public/language/th/user.json b/public/language/th/user.json new file mode 100644 index 0000000000..56c577d271 --- /dev/null +++ b/public/language/th/user.json @@ -0,0 +1,199 @@ +{ + "banned": "ถูกแบน", + "muted": "Muted", + "offline": "ออฟไลน์", + "deleted": "ลบแล้ว", + "username": "ชื่อผู้ใช้", + "joindate": "วันที่เข้าร่วม", + "postcount": "จำนวนโพสต์", + "email": "อีเมล์", + "confirm_email": "ยืนยันอีเมล์", + "account_info": "ข้อมูลบัญชี", + "admin_actions_label": "Administrative Actions", + "ban_account": "แบนบัญชี", + "ban_account_confirm": "คุณต้องการแบนผู้ใช้นี้หรือไม่?", + "unban_account": "ปลดแบน", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "ลบบัญชี", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "บัญชีถูกลบแล้ว", + "account-content-deleted": "Account content deleted", + "fullname": "ชื่อเต็ม", + "website": "เว็บไซต์", + "location": "สถานที่", + "age": "อายุ", + "joined": "เข้าร่วม", + "lastonline": "ออนไลน์ล่าสุด", + "profile": "รายละเอียด", + "profile_views": "ดูข้อมูลส่วนตัว", + "reputation": "ชื่อเสียง", + "bookmarks": "ที่คั่นหน้า", + "watched_categories": "Watched categories", + "change_all": "Change All", + "watched": "ดูแล้ว", + "ignored": "ยกเว้นแล้ว", + "default-category-watch-state": "Default category watch state", + "followers": "คนติดตาม", + "following": "ติดตาม", + "blocks": "Blocks", + "block_toggle": "Toggle Block", + "block_user": "Block User", + "unblock_user": "Unblock User", + "aboutme": "เกี่ยวกับฉัน", + "signature": "ลายเซ็น", + "birthday": "วันเกิด", + "chat": "แชท", + "chat_with": "สนทนาต่อกับ %1", + "new_chat_with": "เริ่มสนทนากับ %1", + "flag-profile": "รายงานผู้ใช้", + "follow": "ติดตาม", + "unfollow": "เลิกติดตาม", + "more": "เพิ่มเติม", + "profile_update_success": "ข้อมูลประวัติส่วนตัวได้รับการแก้ไขแล้ว", + "change_picture": "เปลี่ยนรูป", + "change_username": "เปลี่ยนชื่อผู้ใช้", + "change_email": "เปลี่ยนอีเมล", + "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "edit": "แก้ไข", + "edit-profile": "แก้ไขข้อมูลส่วนตัว", + "default_picture": "แก้ไขไอคอน", + "uploaded_picture": "อัปโหลดรูป", + "upload_new_picture": "อัพโหลดรูปใหม่", + "upload_new_picture_from_url": "อัปโหลดรูปจาก URL", + "current_password": "รหัสผ่านปัจจุบัน", + "change_password": "เปลี่ยนรหัสผ่าน", + "change_password_error": "รหัสผ่านใช้ไม่ได้", + "change_password_error_wrong_current": "รหัสผ่านปัจจุบันไม่ถูกต้อง", + "change_password_error_match": "รหัสผ่านต้องเหมือนกัน", + "change_password_error_privileges": "คุณไม่มีสิทธิในการเปลี่ยนรหัสผ่าน", + "change_password_success": "รหัสผ่านของคุณได้รับการแก้ไขแล้ว", + "confirm_password": "ยืนยันรหัสผ่าน", + "password": "รหัสผ่าน", + "username_taken_workaround": "ชื้อผู้ใช้นี้ถูกใช้แล้ว เราทำการแก้ไขชื่อผู้ใช้ของคุณเล็กน้อยเป็น %1", + "password_same_as_username": "คุณใช้รหัสผ่านเดียวกับชื่อผู้ใช้ กรุณาเปลี่ยนรหัสผ่านใหม่", + "password_same_as_email": "คุณใช้รหัสผ่านเดียวกับอีเมล กรุณาเปลี่ยนรหัสผ่านใหม่", + "weak_password": "พาสเวิร์ดเดาได้ง่าย", + "upload_picture": "อัพโหลดรูป", + "upload_a_picture": "อัพโหลดรูป", + "remove_uploaded_picture": "ลบภาพที่อัพโหลดไว้", + "upload_cover_picture": "อัพโหลดภาพหน้าปก", + "remove_cover_picture_confirm": "คุณต้องการลบภาพหน้าปกใช่หรือไม่?", + "crop_picture": "ตัดภาพ", + "upload_cropped_picture": "ตัดภาพและอัพโหลด", + "avatar-background-colour": "Avatar background colour", + "settings": "ตั้งค่า", + "show_email": "แสดงอีเมลของฉัน", + "show_fullname": "แสดงชื่อจริงของฉัน", + "restrict_chats": "รับข้อความสนทนาจากคนที่ฉันติดตามเท่านั้น", + "digest_label": "สมัครรับข่าวสารจาก Digest", + "digest_description": "สมัครรับอีเมลอัพเดทข้อมูลของบอร์ดสนทนา (ข้อความแจ้งเตือนและหัวข้อใหม่ๆ) ตามรายการที่ตั้งไว้", + "digest_off": "ปิด", + "digest_daily": "รายวัน", + "digest_weekly": "รายสัปดาห์", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "รายเดือน", + "has_no_follower": "ผู้ใช้รายนี้ไม่มีใครติดตาม :(", + "follows_no_one": "ผู้ใช้รายนี้ไม่ติดตามใคร :(", + "has_no_posts": "ผู้ใช้นี้ไม่ได้โพสต์ข้อความใดๆ", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "ผู้ใช้นี้ยังไม่เคยตั้งกระทู้ใดๆ", + "has_no_watched_topics": "ผู้ใช้นี้ไม่ได้ติดตามกระทู้ใดๆ", + "has_no_ignored_topics": "ผู้ใช้นี้ไม่ได้ละเว้นกระทู้ใดๆ", + "has_no_upvoted_posts": "ผู้ใช้นี้ไม่ได้โหวตขึ้นให้ข้อความใดๆ", + "has_no_downvoted_posts": "ผู้ใช้นี้ไม่ได้โหวตลงให้ข้อความใดๆ", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "You have blocked no users.", + "email_hidden": "ซ่อนอีเมล", + "hidden": "ซ่อน", + "paginate_description": "ใช้การแบ่งหน้ากระทู้และข้อความแทนการเลื่อนต่อเรื่อยๆ", + "topics_per_page": "จำนวนกระทู้ต่อหน้า", + "posts_per_page": "จำนวนข้อความต่อหน้า", + "max_items_per_page": "สูงสุด %1", + "acp_language": "ภาษาในหน้าผู้ดูแลระบบ", + "notifications": "Notifications", + "upvote-notif-freq": "แจ้งเตือนความถี่โหวตขึ้น", + "upvote-notif-freq.all": "โหวตขึ้นทั้งหมด", + "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.everyTen": "ทุกๆ 10 โหวตขึ้น", + "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "ทุกๆ 10, 100, 1000...", + "upvote-notif-freq.disabled": "ปิด", + "browsing": "เปิดดูการตั้งค่า", + "open_links_in_new_tab": "เปิดลิงค์ในแท็บใหม่", + "enable_topic_searching": "เปิดใช้การค้นหาแบบ In-Topic", + "topic_search_help": "หากเปิดใช้งาน, \"การค้นหาภายในกระทู้\" จะแทนที่ระบบ \"การค้นหาจากค่าเริ่มต้นของเบราเซอร์\" และจะทำให้คุณค้นหาข้อมูลต่างๆภายในกระทู้ได้ แทนที่จะเป็นการหาแค่สิ่งที่แสดงบนหน้าจอเท่านั้น", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "หลังจากได้ทำการโพสต์ตอบกลับ ให้แสดงโพสต์ใหม่", + "follow_topics_you_reply_to": "ดูกระทู้ที่คุณตอบ", + "follow_topics_you_create": "ดูกระทู้ที่คุณตั้ง", + "grouptitle": "ชื่อกลุ่ม", + "group-order-help": "Select a group and use the arrows to order titles", + "no-group-title": "ไม่มีชื่อกลุ่ม", + "select-skin": "เลือกสกิน", + "select-homepage": "เลือกหน้าแรก", + "homepage": "หน้าแรก", + "homepage_description": "เลือกหน้าที่จะใช้เป็นหน้าแรกของฟอรั่ม หรือเลือก None เพื่อใช้ค่าเริ่มต้น", + "custom_route": "กำหนดเส้นทางไปหน้าแรกเอง", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "บริการ Single Sign-on ", + "sso.associated": "เกี่ยวข้องกับ", + "sso.not-associated": "คลิกที่นี่เพื่อเชื่อมโยงกับ", + "sso.dissociate": "แยกตัวออก", + "sso.dissociate-confirm-title": "ยืนยันการแยกตัวออก", + "sso.dissociate-confirm": "คุณแน่ใจหรือไม่ว่าต้องการแยกบัญชีออกจาก %1?", + "info.latest-flags": "ปักธงล่าสุด", + "info.no-flags": "ไม่พบโพสต์ที่ถูกปักธง", + "info.ban-history": "ประวัติแบนล่าสุด", + "info.no-ban-history": "ผู้ใช้นี้ถูกแบนแล้ว", + "info.banned-until": "แบนจนกว่า %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "แบนอย่างถาวร", + "info.banned-reason-label": "เหตุผล", + "info.banned-no-reason": "ไม่มีเหตุผล", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "ประวัติผู้ใช้", + "info.email-history": "ประวัติอีเมล์", + "info.moderation-note": "โน๊ตของ Moderation", + "info.moderation-note.success": "โน้ตของ Moderation ถูกบันทึกแล้ว", + "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.", + "consent.title": "Your Rights & Consent", + "consent.lead": "This community forum collects and processes your personal information.", + "consent.intro": "We use this information strictly to personalise your experience in this community, as well as to associate the posts you make to your user account. During the registration step you were asked to provide a username and email address, you can also optionally provide additional information to complete your user profile on this website.

We retain this information for the life of your user account, and you are able to withdraw consent at any time by deleting your account. At any time you may request a copy of your contribution to this website, via your Rights & Consent page.

If you have any questions or concerns, we encourage you to reach out to this forum's administrative team.", + "consent.email_intro": "Occasionally, we may send emails to your registered email address in order to provide updates and/or to notify you of new activity that is pertinent to you. You can customise the frequency of the community digest (including disabling it outright), as well as select which types of notifications to receive via email, via your user settings page.", + "consent.digest_frequency": "Unless explicitly changed in your user settings, this community delivers email digests every %1.", + "consent.digest_off": "Unless explicitly changed in your user settings, this community does not send out email digests", + "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", + "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.give": "Give consent", + "consent.right_of_access": "You have the Right of Access", + "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_to_rectification": "You have the Right to Rectification", + "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's administrative team.", + "consent.right_to_erasure": "You have the Right to Erasure", + "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 and your content, please contact the administrative team for this website.", + "consent.right_to_data_portability": "You have the Right to Data Portability", + "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.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Export Uploaded Content (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Export Posts (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/th/users.json b/public/language/th/users.json new file mode 100644 index 0000000000..4ed0e4c87b --- /dev/null +++ b/public/language/th/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "ผู้ใช้งานล่าสุด", + "top_posters": "ผู้ที่โพสต์มากที่สุด", + "most_reputation": "ผู้ที่มีชื่อเสียงมากที่สุด", + "most_flags": "ผู้ที่ถูกรายงานมากที่สุด", + "search": "ค้นหา", + "enter_username": "กรอกชื่อผู้ใช้เพื่อค้นหา", + "search-user-for-chat": "Search a user to start chat", + "load_more": "โหลดเพิ่มเติม", + "users-found-search-took": "พบ %1 ผู้ใช้(s)! การค้นหาใช้เวลาทั้งหมด %2 วินาที", + "filter-by": "การกรอง", + "online-only": "กำลังออนไลน์เท่านั้น", + "invite": "เชิญ", + "prompt-email": "Emails:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "อีเมลคำเชิญถูกส่งไปยัง %1 เรียบร้อย", + "user_list": "รายการผู้ใช้", + "recent_topics": "กระทู้ล่าสุด", + "popular_topics": "กระทู้ยอดนิยม", + "unread_topics": "กระทู้ที่ยังไม่อ่าน", + "categories": "หมวดหมู่", + "tags": "แท็ก", + "no-users-found": "ไม่พบผู้ใช้ใดๆ!" +} \ No newline at end of file diff --git a/public/language/tr/_DO_NOT_EDIT_FILES_HERE.md b/public/language/tr/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/tr/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/tr/admin/admin.json b/public/language/tr/admin/admin.json new file mode 100644 index 0000000000..5171fb6b8e --- /dev/null +++ b/public/language/tr/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "NodeBB'yi sıfırlamak ve yeniden başlatmak istediğinizden emin misiniz?", + "alert.confirm-restart": "NodeBB'yi yeniden başlatmak istediğinize emin misiniz?", + + "acp-title": "%1 | NodeBB Yönetici Kontrol Paneli", + "settings-header-contents": "İçerikler", + "changes-saved": "Değişiklikler kaydedildi", + "changes-saved-message": "NodeBB konfigürasyon değişiklikleri kaydedildi.", + "changes-not-saved": "Değişiklikler kaydedilmedi", + "changes-not-saved-message": "NodeBB değişiklikleri kaydederken bir hata oluştu (%1)" +} \ No newline at end of file diff --git a/public/language/tr/admin/advanced/cache.json b/public/language/tr/admin/advanced/cache.json new file mode 100644 index 0000000000..a22bd4e060 --- /dev/null +++ b/public/language/tr/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "İleti Önbelleği", + "group-cache": "Grup Önbelleği", + "local-cache": "Yerel Önbellek", + "object-cache": "Öğe Önbelleği", + "percent-full": "%1% Tam", + "post-cache-size": "İleti Önbellek Boyutu", + "items-in-cache": "Önbellekteki Öğeler" +} \ No newline at end of file diff --git a/public/language/tr/admin/advanced/database.json b/public/language/tr/admin/advanced/database.json new file mode 100644 index 0000000000..8a55ee0978 --- /dev/null +++ b/public/language/tr/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Saniyede Bir Çalışma Zamanı", + "uptime-days": "Günde Bir Çalışma Zamanı", + + "mongo": "Mongo", + "mongo.version": "MongoDB Sürümü", + "mongo.storage-engine": "Depolama Motoru", + "mongo.collections": "Koleksiyonlar", + "mongo.objects": "Objeler", + "mongo.avg-object-size": "Ortalama Nesne Boyutu", + "mongo.data-size": "Veri Boyutu", + "mongo.storage-size": "Depolama Boyutu", + "mongo.index-size": "İndex Boyutu", + "mongo.file-size": "Dosya Boyutu", + "mongo.resident-memory": "Yerleşik Bellek", + "mongo.virtual-memory": "Sanal Bellek", + "mongo.mapped-memory": "Planlanan Bellek", + "mongo.bytes-in": "alınan bayt", + "mongo.bytes-out": "gönderilen bayt", + "mongo.num-requests": "İsteklerin Sayısı", + "mongo.raw-info": "İşlenmemiş MongoDB Bilgisi", + "mongo.unauthorized": "NodeBB, ilgili istatistikler için MongoDB'yi sorgulayamadı. Lütfen NodeBB'yi kullanan kişinin "admin" veri bankasında "clusterMonitor" listesinde bulunduğuna emin olunuz.", + + "redis": "Redis", + "redis.version": "Redis Sürümü", + "redis.keys": "Anahtarlar", + "redis.expires": "Sona erenler", + "redis.avg-ttl": "Ortalama TTL", + "redis.connected-clients": "Bağlı İstemciler", + "redis.connected-slaves": "İlişkili Bağımlılar", + "redis.blocked-clients": "Engellenen İstemciler", + "redis.used-memory": "Kullanılan Bellek", + "redis.memory-frag-ratio": "Bellek Parçalanma Oranı", + "redis.total-connections-recieved": "Toplam Alınan Bağlantılar", + "redis.total-commands-processed": "Toplam İşlenen Komutlar", + "redis.iops": "Saniyede işlenen komut sayısı", + "redis.iinput": "Saniyede yapılan giriş", + "redis.ioutput": "Saniyede yapılan çıkış", + "redis.total-input": "Toplam giriş", + "redis.total-output": "Toplam çıkış", + + "redis.keyspace-hits": "Başarılı anahtar arama sayısı: keyspace_hits", + "redis.keyspace-misses": "Başarısız anahtar arama sayısı: keyspace_misses", + "redis.raw-info": "İşlenmemiş Redis Bilgisi", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Sürümü", + "postgres.raw-info": "İşlenmemiş Postgres Bilgisi" +} diff --git a/public/language/tr/admin/advanced/errors.json b/public/language/tr/admin/advanced/errors.json new file mode 100644 index 0000000000..0838d037ba --- /dev/null +++ b/public/language/tr/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figür %1", + "error-events-per-day": "%1 günlük olay", + "error.404": "404 Bulunamadı", + "error.503": "503 Servis Kullanılamıyor", + "manage-error-log": "Hata Kayıtlarını Yönet", + "export-error-log": "Hata Kayıtlarını Dışarı Aktar (CSV)", + "clear-error-log": "Hata Kayıtlarını Temizle", + "route": "Rota", + "count": "Sayı", + "no-routes-not-found": "Hülooğ! Hiç 404 hatası yok!", + "clear404-confirm": "404 hata kayıtlarını temizlemek istediğinizden emin misiniz?", + "clear404-success": "\"404 Bulunamadı\" hataları temizlendi" +} \ No newline at end of file diff --git a/public/language/tr/admin/advanced/events.json b/public/language/tr/admin/advanced/events.json new file mode 100644 index 0000000000..14bdd8788c --- /dev/null +++ b/public/language/tr/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Aktiviteler", + "no-events": "Aktivite yok", + "control-panel": "Aktivite Kontrol Paneli", + "delete-events": "Aktiviteyi Sil", + "confirm-delete-all-events": "Kaydedilen tüm etkinlikleri silmek istediğinizden emin misiniz?", + "filters": "Filtreler", + "filters-apply": "Filtreleri Uygula", + "filter-type": "Aktivite türü", + "filter-start": "Başlangıç zamanı", + "filter-end": "Bitiş zamanı", + "filter-perPage": "Sayfa Başına" +} \ No newline at end of file diff --git a/public/language/tr/admin/advanced/logs.json b/public/language/tr/admin/advanced/logs.json new file mode 100644 index 0000000000..837846df0e --- /dev/null +++ b/public/language/tr/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Kayıtlar", + "control-panel": "Kayıt Kontrol Paneli", + "reload": "Kayıtları Yeniden Yükle", + "clear": "Kayıtları Temizle", + "clear-success": "Kayıtlar Temizlendi!" +} \ No newline at end of file diff --git a/public/language/tr/admin/appearance/customise.json b/public/language/tr/admin/appearance/customise.json new file mode 100644 index 0000000000..327a9054c0 --- /dev/null +++ b/public/language/tr/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Özel CSS/LESS", + "custom-css.description": "Diğer tüm stillerden sonra uygulanacak kendi CSS/LESS kodlarınızı buraya girin.", + "custom-css.enable": "Özelleştirilmiş CSS/LESS kullan", + + "custom-js": "Özel Javascript", + "custom-js.description": "Buraya kendi javascript'inizi girin. Sayfa tamamen yüklendikten sonra çalışır.", + "custom-js.enable": "Özelleştirilmiş Javascript'i etkinleştir", + + "custom-header": "Özel Header", + "custom-header.description": "Size özel HTML kodları buraya girin (Meta Tags vb.). Bu kodlar forumun markup'ının şu bölümüne eklenecek: <head>. Script etiketlerini kullanabilirsiniz, fakat Custom Javascript sekmesi uygun oldugundan bunları kullanmanız tavsiye edilmez. ", + "custom-header.enable": "Özel Header'ı Etkinleştir", + + "custom-css.livereload": "Canlı Yenilemeyi Etkinleştir", + "custom-css.livereload.description": "\"Kayıt Et\"e her bastığınızda size ait tüm cihazlardaki oturumların yenilenmeye zorlanması için bunu etkinleştirin." +} \ No newline at end of file diff --git a/public/language/tr/admin/appearance/skins.json b/public/language/tr/admin/appearance/skins.json new file mode 100644 index 0000000000..9d9dcecb31 --- /dev/null +++ b/public/language/tr/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Görünümler yükleniyor...", + "homepage": "Anasayfa", + "select-skin": "Görünüm Seç", + "current-skin": "Mevcut Görünüm", + "skin-updated": "Görünüm Güncellendi", + "applied-success": "%1 isimli görünüm başarıyla uygulandı", + "revert-success": "Görünüm temel renkleri geri döndürüldü" +} \ No newline at end of file diff --git a/public/language/tr/admin/appearance/themes.json b/public/language/tr/admin/appearance/themes.json new file mode 100644 index 0000000000..fe734609d4 --- /dev/null +++ b/public/language/tr/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Yüklü temalar kontrol ediliyor...", + "homepage": "Anasayfa", + "select-theme": "Tema Seç", + "current-theme": "Mevcut Tema", + "no-themes": "Yüklü tema bulunamadı", + "revert-confirm": "Varsayılan NodeBB temasını geri yüklemek istediğinizden emin misiniz?", + "theme-changed": "Tema Değiştirildi", + "revert-success": "NodeBB'nin varsayılan temasına başarıyla geri dönüş yaptınız.", + "restart-to-activate": "Bu temayı tamamen etkinleştirmek için NodeBB'nizi lütfen sıfırlayıp, yeniden başlatın." +} \ No newline at end of file diff --git a/public/language/tr/admin/dashboard.json b/public/language/tr/admin/dashboard.json new file mode 100644 index 0000000000..f7bdad37b3 --- /dev/null +++ b/public/language/tr/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Trafiği", + "page-views": "Sayfa Gösterim Sayısı", + "unique-visitors": "Tekil Ziyaretçiler", + "logins": "Girişler", + "new-users": "Yeni Kullanıcılar", + "posts": "İletiler", + "topics": "Başlıklar", + "page-views-seven": "Son 7 Gün", + "page-views-thirty": "Son 30 Gün", + "page-views-last-day": "Son 24 saat", + "page-views-custom": "Özel Tarih Aralığı", + "page-views-custom-start": "Başlangıç Tarihi", + "page-views-custom-end": "Bitiş Tarihi", + "page-views-custom-help": "İncelemek istediğiniz sayfa gösterim sayıları için bir tarih aralığı girin. Tarih seçeceğiniz panel görünmezse, kabul edilebilir format YYYY-AA-GG'dir.", + "page-views-custom-error": "Lütfen tarih aralığını geçerli formatta girin YYYY-MM-DD", + + "stats.yesterday": "Dün", + "stats.today": "Bugün", + "stats.last-week": "Geçen Hafta", + "stats.this-week": "Bu Hafta", + "stats.last-month": "Geçen Ay", + "stats.this-month": "Bu Ay", + "stats.all": "Tüm Zamanlar", + + "updates": "Güncellemeler", + "running-version": "NodeBB v%1 çalışıyor.", + "keep-updated": "En son güvenlik değişiklikleri ve hata düzeltmeleri için NodeBB'nin güncel olduğundan emin olun.", + "up-to-date": "

Sürümünüzgüncel

", + "upgrade-available": "

Yeni bir sürüm (v% 1) yayımlandı. NodeBB yükseltmeyi göz önünde bulundurun

", + "prerelease-upgrade-available": "

Bu, NodeBB'nin eski bir sürümü. Yeni bir sürüm (v% 1) yayımlandı. NodeBB’nizi yükseltmeyi düşünün.

", + "prerelease-warning": "

Bu, NodeBB'nin bir önsürüm versiyonudur. İstenmeyen hatalar oluşabilir.

", + "fallback-emailer-not-found": "\"Fallback emailer\" bulunamadı!", + "running-in-development": "Forum, geliştirici modunda çalışıyor. Forum, potansiyel güvenlik açıklarına açık olabilir; lütfen sistem yöneticinize başvurun.", + "latest-lookup-failed": "

En güncel kullanılabilecek NodeBB sürümü görüntülenemedi

", + + "notices": "Bildirimler", + "restart-not-required": "Yeniden başlatma gerekmiyor", + "restart-required": "Yeniden başlatma gerekiyor", + "search-plugin-installed": "Arama Eklentisi yüklendi", + "search-plugin-not-installed": "Arama Eklentisi yüklenmedi", + "search-plugin-tooltip": "Arama işlevselliğini etkinleştirmek için eklenti sayfasından bir arama eklentisi kurun", + + "control-panel": "Sistem Kontrol Paneli", + "rebuild-and-restart": "Yeniden oluştur & Yeniden Başlat", + "restart": "Yeniden Başlat", + "restart-warning": "NodeBB'yi yeniden oluşturmak (yapılandırmak) veya yeniden başlatmak, mevcut tüm bağlantıları birkaç saniye için sonlandırır.", + "restart-disabled": "NodeBB'nizi yeniden oluşturma ve yeniden başlatma devre dışı bırakıldı.", + "maintenance-mode": "Bakım Modu", + "maintenance-mode-title": "NodeBB için bakım modunu ayarlamak için buraya tıklayın", + "realtime-chart-updates": "Gerçek Zamanlı Grafik Güncellemeleri", + + "active-users": "Aktif Kullanıcılar", + "active-users.users": "Kullanıcılar", + "active-users.guests": "Ziyaretçiler", + "active-users.total": "Genel Toplam", + "active-users.connections": "Bağlantılar", + + "guest-registered-users": "Misafir ve Kayıtlı Kullanıcılar", + "guest": "Misafir", + "registered": "Kayıtlı", + + "user-presence": "Kullanıcı Durumları", + "on-categories": "Kategoriler Listesinde", + "reading-posts": "İleti Okuyor", + "browsing-topics": "Konuları İnceliyor", + "recent": "Yeni Konular Sayfasında", + "unread": "Okunmamış Konular Sayfasında", + + "high-presence-topics": "Öne Çıkan Başlıklar", + "popular-searches": "Popüler Aramalar", + + "graphs.page-views": "Sayfa Gösterimi", + "graphs.page-views-registered": "Kayıtlı Kullanıcıların Sayfa Gösterimi", + "graphs.page-views-guest": "Ziyaretçilerin Sayfa Gösterimi", + "graphs.page-views-bot": "Bot Sayfa Gösterimi", + "graphs.unique-visitors": "Benzersiz Ziyaretçiler", + "graphs.registered-users": "Kayıtlı Kullanıcılar", + "graphs.guest-users": "Misafir Kullanıcılar", + "last-restarted-by": "Son yeniden başlatma bilgisi", + "no-users-browsing": "İnceleyen kullanıcı yok", + + "back-to-dashboard": "Yönetim Paneline geri dön", + "details.no-users": "Seçilen zaman aralığında herhangi bir kullanıcı üye olmadı.", + "details.no-topics": "Seçilen zaman aralığında herhangi bir başlık oluşturulmadı. ", + "details.no-searches": "Henüz arama yapılmadı", + "details.no-logins": "Seçilen zaman aralığında herhangi bir giriş yapılmadı.", + "details.logins-static": "NodeBB oturum kayıtlarını sadece %1 gün tutar, o nedenle aşağıdaki tablo sadece en yakın aktif oturumları listeler", + "details.logins-login-time": "Giriş zamanı" +} diff --git a/public/language/tr/admin/development/info.json b/public/language/tr/admin/development/info.json new file mode 100644 index 0000000000..4aa20f221e --- /dev/null +++ b/public/language/tr/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Buradasınız: %1:%2", + "ip": "IP %1", + "nodes-responded": "%2ms içinde %1 düğüm yanıt verdi!", + "host": "sunucu", + "primary": "ana sunucu / işlemleri gerçekleştir", + "pid": "pid", + "nodejs": "nodejs", + "online": "çevrimiçi", + "git": "git", + "process-memory": "işlem belleği", + "system-memory": "sistem hafızası", + "used-memory-process": "İşleme göre kullanılan bellek", + "used-memory-os": "Kullanılan sistem belleği", + "total-memory-os": "Toplam sistem belleği", + "load": "sistem yüklemesi", + "cpu-usage": "cpu kullanımı", + "uptime": "kesintisiz çalışma süresi", + + "registered": "Kayıtlı", + "sockets": "Soketler", + "guests": "Ziyaretçiler", + + "info": "Bilgi" +} \ No newline at end of file diff --git a/public/language/tr/admin/development/logger.json b/public/language/tr/admin/development/logger.json new file mode 100644 index 0000000000..5a49b5be7c --- /dev/null +++ b/public/language/tr/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Günlükçü Ayarları", + "description": "Onay kutularını etkinleştirdiğinizde, günlükler terminalinize gönderilir. Bir dizin belirtirseniz, günlükler bunun yerine bir dosyaya kaydedilir. HTTP günlüğü, forumunuza kimler, ne zaman erişiyor gibi istatistikleri toplamak için kullanışlıdır. HTTP isteklerinin günlüğüne eklenmesine ek olarak, socket.io olaylarını da günlüğe kaydedebilirsiniz. Redis-cli monitörü ile birlikte Socket.io günlüğü, NodeBB'nin iç kısımlarını öğrenmek için çok yardımcı olabilir.", + "explanation": "Basitçe günlüğe kaydetmeyi etkinleştirmek veya devre dışı bırakmak için günlüğe kaydetme ayarlarını kontrol edin. Yeniden başlatmaya gerek yoktur.", + "enable-http": "HTTP günlüğünü etkinleştir", + "enable-socket": "Socket.io olay günlüğünü etkinleştir", + "file-path": "Günlük dosyası dizini", + "file-path-placeholder": "/path/to/log/file.log ::: terminalinize günlük kaydı yapmak için boş bırakın", + + "control-panel": "Günlükçü Kontrol Paneli", + "update-settings": "Günlükçü Ayarlarını Güncelle" +} \ No newline at end of file diff --git a/public/language/tr/admin/extend/plugins.json b/public/language/tr/admin/extend/plugins.json new file mode 100644 index 0000000000..07446de21d --- /dev/null +++ b/public/language/tr/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Popüler", + "installed": "Yüklenmiş", + "active": "Etkin", + "inactive": "Etkin değil", + "out-of-date": "Güncel değil", + "none-found": "Hiç eklenti bulunamadı.", + "none-active": "Etkin eklenti yok", + "find-plugins": "Eklenti bul", + + "plugin-search": "Eklenti Arama", + "plugin-search-placeholder": "Eklenti Ara", + "submit-anonymous-usage": "Eklenti kullanımına dair verileri anonim olarak paylaş", + "reorder-plugins": "Eklentileri yeniden sırala", + "order-active": "Etkin eklentileri sırala", + "dev-interested": "NodeBB için eklenti yazmakla ilgilenir misiniz?", + "docs-info": "Eklenti yazarlığına ilişkin tüm belgeler NodeBB Docs Portal'da bulunabilir.", + + "order.description": "Bazı eklentiler diğer eklentilerden önce ya da sonra başlatıldığında daha ideal bir şekilde çalışırlar.", + "order.explanation": "Eklentiler burada belirtilen sırayla yüklenir: yukarıdan aşağıya", + + "plugin-item.themes": "Temalar", + "plugin-item.deactivate": "Etkinsizleştir", + "plugin-item.activate": "Etkinleştir", + "plugin-item.install": "Yükle", + "plugin-item.uninstall": "Kaldır", + "plugin-item.settings": "Ayarlar", + "plugin-item.installed": "Yüklenen Sürüm", + "plugin-item.latest": "En Güncel Sürüm", + "plugin-item.upgrade": "Güncelle", + "plugin-item.more-info": "Daha fazla bilgi için:", + "plugin-item.unknown": "Bilinmeyen", + "plugin-item.unknown-explanation": "Bu eklentinin durumu muhtemelen yanlış yapılandırma hatası nedeniyle belirlenemedi.", + "plugin-item.compatible": "Bu eklenti şu sürümde çalışıyor: NodeBB %1", + "plugin-item.not-compatible": "Bu eklentinin NodeBB sürümünüzle uyumlu olup olmadığı bilgisi bulunmuyor. Forumunuza yüklemeden önce test ediniz. ", + + "alert.enabled": "Eklenti Aktif", + "alert.disabled": "Eklenti Devre dışı", + "alert.upgraded": "Eklendi Güncellendi", + "alert.installed": "Eklenti Kuruldu", + "alert.uninstalled": "Eklenti Kaldırıldı", + "alert.activate-success": "Lütfen bu eklentiyi tamamen aktifleştirmek için NodeBB'nizi yeniden oluşturun ve yeniden başlatın.", + "alert.deactivate-success": "Eklenti başarıyla etkinsizleştirildi", + "alert.upgrade-success": "Lütfen bu eklentiyi tamamen yükseltmek için NodeBB'nizi yeniden oluşturun ve yeniden başlatın.", + "alert.install-success": "Eklenti başarıyla kuruldu, lütfen eklentiyi etkinleştirin.", + "alert.uninstall-success": "Eklenti başarıyla etkinsizleştirildi ve kaldırıldı.", + "alert.suggest-error": "

NodeBB paket yöneticisine ulaşamadı, en yeni sürüm yüklenmeye devam edilsin mi?

Sunucu iade etti (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB paket yöneticisine ulaşamadı, şu anda bir yükseltme önerilmedi.

", + "alert.incompatible": "

NodeBB sürümünüz (v%1) bu eklentinin sadece v%2 sürümüne yükseltilmesi için izin veriyor. Bu eklentinin yeni versiyonunu yüklemek için lütfen NodeBB'yi güncelleyin.", + "alert.possibly-incompatible": "

Uyumluluk Bilgisi Bulunamadı

Bu eklenti, NodeBB sürümünüze göre kurulum için belirli bir sürümü belirtmedi. Eklentinin forumla tam uyumluluğu garanti edilemez. Eklenti, NodeBB'nizin artık düzgün çalışmamasına neden olabilir.

NodeBB düzgün şekilde önyükleme yapamıyorsa

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

Bu eklentinin en yeni sürümünü yüklemeye devam et?

", + "alert.reorder": "Eklentiler Yeniden Sıralandı", + "alert.reorder-success": "Lütfen işlemi tamamlamak için NodeBB'nizi yeniden oluşturun ve yeniden başlatın.", + + "license.title": "Eklenti Lisans Bilgisi", + "license.intro": "%1 eklentisi, %2 altında lisanslanmıştır. Lütfen bu eklentiyi etkinleştirmeden önce lisans koşullarını okuyun ve anlayın.", + "license.cta": "Bu eklentiyi aktifleştirmeye devam etmek istiyor musunuz?" +} diff --git a/public/language/tr/admin/extend/rewards.json b/public/language/tr/admin/extend/rewards.json new file mode 100644 index 0000000000..282c929757 --- /dev/null +++ b/public/language/tr/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Ödüller", + "condition-if-users": "Eğer bir kullanıcının", + "condition-is": "şu ise:", + "condition-then": "O halde:", + "max-claims": "Ödül kaç kez alınabilir", + "zero-infinite": "Sınırsız için 0 girin", + "delete": "Sil", + "enable": "Etkinleştir", + "disable": "Etkinsizleştir", + + "alert.delete-success": "Ödül başarıyla silindi", + "alert.no-inputs-found": "Usulsüz ödül - girdi bulunamadı!", + "alert.save-success": "Ödüller başarıyla kaydedildi" +} \ No newline at end of file diff --git a/public/language/tr/admin/extend/widgets.json b/public/language/tr/admin/extend/widgets.json new file mode 100644 index 0000000000..2609575438 --- /dev/null +++ b/public/language/tr/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Kullanılabilir Bileşenler", + "explanation": "Açılır menüden bir bileşen seçin ve sol taraftaki bir şablonun bileşen alanına sürükleyip bırakın.", + "none-installed": "Bileşen bulunamadı! Eklentiler kontrol panelinden bileşen eklentisini aktifleştirmelisiniz!", + "clone-from": "Bileşenleri klonla", + "containers.available": "Kullanılabilir Kutucuklar", + "containers.explanation": "Aktif olan herhangi bir bileşenin üzerine sürükleyin ve bırakın", + "containers.none": "Kutucuk Yok", + "container.well": "Çukur", + "container.jumbotron": "Büyük Gösterim", + "container.panel": "Panel", + "container.panel-header": "Panel Başlığı", + "container.panel-body": "Panel Gövdesi", + "container.alert": "Uyarı", + + "alert.confirm-delete": "Bu bileşeni silmek istediğinizden emin misiniz?", + "alert.updated": "Bileşenler Güncellendi", + "alert.update-success": "Bileşenler başarıyla güncellendi", + "alert.clone-success": "Bileşenler başarıyla klonlandı", + + "error.select-clone": "Lütfen klonlanacak bir sayfa seçin", + + "title": "Başlık", + "title.placeholder": "Başlık (Sadece bazı kutucuklarda gösteriliyor)", + "container": "Kutucuk", + "container.placeholder": "Bir kutucuğu sürükle ve buraya bırak veya HTML gir", + "show-to-groups": "Şu gruplara göster", + "hide-from-groups": "Şu gruplara gösterme", + "hide-on-mobile": "Mobilde gösterme" +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/admins-mods.json b/public/language/tr/admin/manage/admins-mods.json new file mode 100644 index 0000000000..9014cdcea7 --- /dev/null +++ b/public/language/tr/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Yöneticiler", + "global-moderators": "Genel Moderatörler", + "moderators": "Moderatörler", + "no-global-moderators": "Genel Moderatör Yok", + "no-sub-categories": "Alt Kategori Yok", + "subcategories": "%1 Alt Kategori", + "no-moderators": "Moderatör Yok", + "add-administrator": "Yönetici Ekle", + "add-global-moderator": "Genel Moderatör Ekle", + "add-moderator": "Moderatör Ekle" +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/categories.json b/public/language/tr/admin/manage/categories.json new file mode 100644 index 0000000000..c15eb8d545 --- /dev/null +++ b/public/language/tr/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Kategori Ayarları", + "privileges": "İzinler", + + "name": "Kategori Adı", + "description": "Kategori Açıklaması", + "bg-color": "Arkaplan Rengi", + "text-color": "Yazı Rengi", + "bg-image-size": "Arkaplan Görseli Boyutu", + "custom-class": "Özel Sınıf", + "num-recent-replies": "Son Yanıtların # Tanesi", + "ext-link": "Harici Bağlantı", + "subcategories-per-page": "Sayfa başına alt-kategoriler", + "is-section": "Bu kategoriyi bir bölüm olarak değerlendir", + "post-queue": "İleti Kuyruğu", + "tag-whitelist": "İzin Verilen Etiketler", + "upload-image": "Görsel Yükle", + "delete-image": "Sil", + "category-image": "Kategori Görseli", + "parent-category": "Üst Kategori", + "optional-parent-category": "(Opsiyonel) Üst Kategori", + "top-level": "En üst seviye", + "parent-category-none": "(Hiçbiri)", + "copy-parent": "Üst Kategoriyi Kopyala", + "copy-settings": "Ayarları Başka Bir Yerden Kopyala", + "optional-clone-settings": "(İsteğe Bağlı) Kategoriden Ayarları Klonla", + "clone-children": "Alt Kategori ve Ayarlarını Çoğalt", + "purge": "Kategoriyi Temizle", + + "enable": "Etkinleştir", + "disable": "Devre dışı", + "edit": "Düzenle", + "analytics": "Analiz", + "view-category": "Kategori Görüntüle", + "set-order": "Bir sıra ayarla", + "set-order-help": "Kategorinin sırasını ayarlamak, bu kategoriyi o sıraya taşıyacak ve diğer kategorilerin sırasını güncelleyecektir. Kategoriyi en üste taşımak için 1 girin.", + + "select-category": "Kategori Seç", + "set-parent-category": "Ana Kategori Ayarla", + + "privileges.description": "Erişim kontrol ayrıcalıklarını bu bölümde ayarlayabilirsiniz. Bu ayrıcalıklar kullanıcılara veya gruplara özel olabilir. Açılır menüden ilgili bölümü seçebilirsiniz.", + "privileges.category-selector": "için yapılandırılan ayrıcalıklar", + "privileges.warning": "Not: Ayrıcalık ayarları hemen yürürlüğe girer. Bu ayarları yaptıktan sonra kategoriyi kaydetmek gerekli değildir.", + "privileges.section-viewing": "Ayrıcalıkları Görüntüle", + "privileges.section-posting": "Gönderme Ayrıcalıkları", + "privileges.section-moderation": "Moderatörlük Ayrıcalıkları", + "privileges.section-other": "Diğer", + "privileges.section-user": "Kullanıcı", + "privileges.search-user": "Kullanıcı Ekle", + "privileges.no-users": "Bu kategoride kullanıcıya-özel ayrıcalıklar yok.", + "privileges.section-group": "Grup", + "privileges.group-private": "Bu grup gizlidir", + "privileges.inheritance-exception": "Bu grup ayrıcalıkları kayıtlı kullanıcılar grubundan devralmamaktadır. ", + "privileges.banned-user-inheritance": "Yasaklanan kullanıcılar ayrıcalıkları yasaklı kullanıcılar grubundan devralmaktadır. ", + "privileges.search-group": "Grup Ekle", + "privileges.copy-to-children": "Alttakilere Kopyala", + "privileges.copy-from-category": "Kategoriden Kopyala", + "privileges.copy-privileges-to-all-categories": "Tüm Kategorilere Kopyala", + "privileges.copy-group-privileges-to-children": "Bu Grubun Ayrıcalıklarını Alt-Kategorilere Kopyala", + "privileges.copy-group-privileges-to-all-categories": "Bu Grubun Ayrıcalıklarını Tüm Kategorilere Kopyala", + "privileges.copy-group-privileges-from": "Bu Grubun Ayrıcalıklarını Başka Bir Kategoriden Kopyala", + "privileges.inherit": "Kayıtlı kullanıcı grubuna belirli bir ayrıcalık tanınması durumunda, diğer tüm gruplar açıkça tanımlanmamış / kontrol edilmemiş olsalar bile örtük bir ayrıcalık alırlar. Bu örtük ayrıcalık size gösterilir. Tüm kullanıcılar kayıtlı kullanıcılar grubunun bir parçasıdır. Bu nedenle ek gruplara yönelik ayrıcalıkların açıkça verilmesine gerek yoktur.", + "privileges.copy-success": "Ayrıcalıklar kopyalandı!", + + "analytics.back": "Kategori listesine geri dön", + "analytics.title": "\"%1\" kategorisi için analiz", + "analytics.pageviews-hourly": "Şekil 1 – Bu kategori için saatlik sayfa görüntüleme sayısı", + "analytics.pageviews-daily": "Şekil 2 – Bu kategori için günlük sayfa görüntüleme sayısı", + "analytics.topics-daily": "Şekil 3 – Bu kategoride oluşturulan günlük konular", + "analytics.posts-daily": "Şekil 4 – Bu kategoride oluşturulan günlük iletiler", + + "alert.created": "Yaratıldı", + "alert.create-success": "Kategori başarıyla yaratıldı!", + "alert.none-active": "Aktif kategoriniz mevcut değil.", + "alert.create": "Bir Kategori Yarat", + "alert.confirm-purge": "

\"% 1\" kategorisini gerçekten temizlemek istiyor musunuz?

Uyarı! Bu kategorideki tüm başlıklar ve iletiler temizlenir!

Bir kategoriyi temizlemek, tüm başlıkları ve iletileri kaldıracak ve kategoriyi veritabanından silecektir. Bir kategoriyi geçici olarak kaldırmak isterseniz, kategoriyi \"devre dışı\" bırakmanız yeterlidir.

", + "alert.purge-success": "Kategori temizlendi!", + "alert.copy-success": "Ayarlar Kopyalandı!", + "alert.set-parent-category": "Ana Kategori Ayarla", + "alert.updated": "Güncellenen Kategoriler", + "alert.updated-success": "Kategori IDleri % 1 başarıyla güncellendi.", + "alert.upload-image": "Kategori görseli yükle", + "alert.find-user": "Bir Kullanıcı Ara", + "alert.user-search": "Burada bir kullanıcı ara...", + "alert.find-group": "Bir Grup Ara", + "alert.group-search": "Burada bir grup ara...", + "alert.not-enough-whitelisted-tags": "Kullanılabilecek etiketlerin sayısı minimum etiket sayısından daha az, daha fazla etiket belirlemelisiniz. ", + "collapse-all": "Hepsini Kapat", + "expand-all": "Hepsini Genişlet", + "disable-on-create": "Oluşturma sırasında devre dışı bırak", + "no-matches": "Eşleşme Bulunamadı" +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/digest.json b/public/language/tr/admin/manage/digest.json new file mode 100644 index 0000000000..5caee118ff --- /dev/null +++ b/public/language/tr/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Özet teslim istatistiklerinin ve saatlerinin bir listesi aşağıda görüntülenmektedir.", + "disclaimer": "E-posta teknolojileri nedeniyle e-posta iletiminin garanti olmadığını hatırlatmak isteriz. Bir e-postanın kullanıcıların e-posta gelen kutusuna ulaşmasını etki eden pek çok faktör mevcut. Bu faktörlerin bazıları şunlar: kullanılan sunucu tanınırlığı, karalisteye alınan IP adresleri, DKIM/SPF/DMARC ayarlarının yapılıp yapılmadığı...", + "disclaimer-continued": "\"Başarılı Gönderim\" e-postanın NodeBB tarafından başarıyla gönderildiği ve alıcı sunucu tarafından gönderimin onaylandığı anlamına gelir. E-postanın \"Gelen Kutusu\"na ulaştığı anlamına gelmez. En iyi sonuçlar için SendGrid gibi üçüncü parti e-posta teslim servislerini kullanmanızı tavsiye ederiz. ", + + "user": "Kullanıcı", + "subscription": "Abonelik Türü", + "last-delivery": "En Son Başarılı Gönderim", + "default": "Sistem varsayılanı", + "default-help": "Sistem varsayılanı , kullanıcının forumun varsayılan ayarlarını değiştirmediği anlamına gelir. Forumun şu anki varsayılan ayarı: "%1"", + "resend": "Özeti yeniden gönder", + "resend-all-confirm": "Bu özet e-postasını elle göndermek istediğinize emin misiniz?", + "resent-single": "El ile özet gönderimi tamamlandı", + "resent-day": "Günlük özet yeniden gönderildi", + "resent-week": "Haftalık özet yeniden gönderildi", + "resent-biweek": "İki Haftalık özeti yeniden gönder", + "resent-month": "Aylık özet yeniden gönderildi", + "null": "Hiçbir zaman", + "manual-run": "El ile özet gönderimi:", + + "no-delivery-data": "Gönderim bilgisi bulunmadı" +} diff --git a/public/language/tr/admin/manage/groups.json b/public/language/tr/admin/manage/groups.json new file mode 100644 index 0000000000..971bf28aa1 --- /dev/null +++ b/public/language/tr/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Grup Adı", + "badge": "Rozet", + "properties": "Özellikler", + "description": "Grup Açıklaması", + "member-count": "Üye Sayısı", + "system": "Sistem", + "hidden": "Gizlenmiş", + "private": "Özel", + "edit": "Düzenle", + "delete": "Sil", + "privileges": "Ayrıcalıklar", + "download-csv": "CSV", + "search-placeholder": "Ara", + "create": "Grup Oluştur", + "description-placeholder": "Grup hakkında kısa bir açıklama yazın", + "create-button": "Oluştur", + + "alerts.create-failure": "Uh-Oh

Grubu oluştururken bir sorun oluştu. Lütfen daha sonra tekrar deneyin!

", + "alerts.confirm-delete": "Bu grubu silmek istediğinizden emin misiniz?", + + "edit.name": "İsim", + "edit.description": "Açıklama", + "edit.user-title": "Kullanıcıların Başlığı", + "edit.icon": "Grup Simgesi", + "edit.label-color": "Grubun Etiket Rengi", + "edit.text-color": "Grup Yazı Rengi", + "edit.show-badge": "Rozeti Göster", + "edit.private-details": "Gruba katılmak için, eğer etkinse grup sahibinin onayı gerekir.", + "edit.private-override": "Uyarı: Sistem düzeyinde özel gruplar devre dışı bırakıldı, bu seçenek onu geçersiz kılar.", + "edit.disable-join": "Katılım isteği gönderilmesini engelle", + "edit.disable-leave": "Kullanıcıların gruptan ayrılmasını engelle", + "edit.hidden": "Gizli", + "edit.hidden-details": "Bu grup eğer etkinse grup listelerinde bulunmaz, ve kullanıcılar bizzat davet eder", + "edit.add-user": "Gruba Kullanıcı Ekle", + "edit.add-user-search": "Kullanıcıları Ara", + "edit.members": "Üye Listesi", + "control-panel": "Grup Kontrol Paneli", + "revert": "Eski Haline Döndür", + + "edit.no-users-found": "Kullanıcı Bulunamadı", + "edit.confirm-remove-user": "Bu kullanıcıyı kaldırmak istediğinizden emin misiniz?", + "edit.save-success": "Değişiklikler kaydedildi!" +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/privileges.json b/public/language/tr/admin/manage/privileges.json new file mode 100644 index 0000000000..0155c573b7 --- /dev/null +++ b/public/language/tr/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Genel", + "admin": "Admin", + "group-privileges": "Grup Ayrıcalıkları", + "user-privileges": "Kullanıcı Ayrıcalıkları", + "edit-privileges": "Ayrıcalıkları Düzenle", + "select-clear-all": "Hepsini Seç/Temizle", + "chat": "Sohbet", + "upload-images": "Resim Yükle", + "upload-files": "Dosya Yükle", + "signature": "İmza", + "ban": "Ban", + "mute": "Sustur", + "invite": "Davet et", + "search-content": "İçerik Arama", + "search-users": "Kullanıcıları Ara", + "search-tags": "Etiketleri Ara", + "view-users": "Kullanıcıları Görüntüle", + "view-tags": "Etiketleri Görüntüle", + "view-groups": "Grupları Görüntüle", + "allow-local-login": "Yerel Giriş", + "allow-group-creation": "Grup Oluştur", + "view-users-info": "Kullanıcı Bilgilerini Görüntüle", + "find-category": "Kategori Bul", + "access-category": "Kategoriye Eriş", + "access-topics": "Başlıklara Eriş", + "create-topics": "Başlık Oluştur", + "reply-to-topics": "Başlığı Cevapla", + "schedule-topics": "Konuları Planla", + "tag-topics": "Başlığı etiketle", + "edit-posts": "İletiyi düzenle", + "view-edit-history": "Düzenleme Geçmişini Görüntüle", + "delete-posts": "İletileri Sil", + "view_deleted": "Silinen İletileri Görüntüle", + "upvote-posts": "İletilere Artı Oy Ver", + "downvote-posts": "İletilere Eksi Oy Ver", + "delete-topics": "Başlıkları Sil", + "purge": "Temizle", + "moderate": "Moderasyon", + "admin-dashboard": "Yönetim Paneli", + "admin-categories": "Kategoriler", + "admin-privileges": "Ayrıcalıklar", + "admin-users": "Kullanıcılar", + "admin-admins-mods": "Yöneticiler & Modlar", + "admin-groups": "Gruplar", + "admin-tags": "Etiketler", + "admin-settings": "Ayarlar", + + "alert.confirm-moderate": "Bu gruba yönetim ayrıcalıkları vermek istediğinize emin misiniz? Bu grup genele açık olduğundan her kullanıcı gruba katılabilir. ", + "alert.confirm-admins-mods": "Bu gruba "Admins & Mods" ayrıcalıkları vermek istediğinize emin misiniz? Bu ayrıcalığa sahip kullanıcılar başka kullanıcıların ayrıcalıklarını belirleyebilirler, örneğin super administrator veya değiştirebilirler.", + "alert.confirm-save": "Lütfen ayrıcalıkları kaydetme isteğinizi onaylayınız", + "alert.saved": "Ayrıcalık değişiklikleri kaydedildi ve uygulandı", + "alert.confirm-discard": "Ayrıcalık değişikliklerini iptal etmek istediğinize emin misiniz?", + "alert.discarded": "Ayrıcalık değişiklikleri iptal edildi", + "alert.confirm-copyToAll": "Bu %1 kategorisini tüm kategorilere uygulamak istediğinizden emin misiniz? ", + "alert.confirm-copyToAllGroup": "Bu grubun %1 kümesini tüm kategorilere uygulamak istediğinizden emin misiniz?", + "alert.confirm-copyToChildren": "Bu %1 kümesini tüm alt (alt) kategorilere uygulamak istediğinizden emin misiniz?", + "alert.confirm-copyToChildrenGroup": "Bu grubun %1 kümesini tüm alt (alt) kategorilere uygulamak istediğinizden emin misiniz?", + "alert.no-undo": "Bu işlem geri alınamaz.", + "alert.admin-warning": "Yöneticiler dolaylı olarak tüm ayrıcalıklara sahiptirler", + "alert.copyPrivilegesFrom-title": "Kopyalamak için bir kategori seçin", + "alert.copyPrivilegesFrom-warning": "Seçilen kategoriden %1 kopyalayacaktır.", + "alert.copyPrivilegesFromGroup-warning": "Bu, seçilen kategoriden bu grubun %1 kümesini kopyalayacaktır." +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/registration.json b/public/language/tr/admin/manage/registration.json new file mode 100644 index 0000000000..0a59cfdf7e --- /dev/null +++ b/public/language/tr/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Kuyruk", + "description": "Kayıt kuyruğunda hiç kullanıcı yok.
Bu özelliği etkinleştirmek için, şuraya gidin: Ayarlar; Kullanıcı; Kullanıcı Kaydı ve Kayıt Tipini \"Yönetici Onayı\" olarak ayarlayın.", + + "list.name": "İsim", + "list.email": "E-posta", + "list.ip": "IP", + "list.time": "Zaman", + "list.username-spam": "Sıklık: %1 Görüntülenme: %2 Güvenirlilik: %3", + "list.email-spam": "Sıklık: %1 Görüntülenme: %2", + "list.ip-spam": "Sıklık: %1 Görüntülenme: %2", + + "invitations": "Davetiyeler", + "invitations.description": "Aşağıda, gönderilen davetiyelerin tam listesi bulunmaktadır. E-posta veya kullanıcı adı ile listede arama yapmak için ctrl-f kısayolunu kullanın.

Davetiyelerini kullanan kullanıcıların e-postalarının sağında kullanıcı adı görüntülenir.", + "invitations.inviter-username": "Davet Edenin Kullanıcı Adı", + "invitations.invitee-email": "Davetli E-postası", + "invitations.invitee-username": "(Eğer kaydolmuşsa) Davetlinin Kullanıcı Adı", + + "invitations.confirm-delete": "Bu daveti silmek istediğinizden emin misiniz?" +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/tags.json b/public/language/tr/admin/manage/tags.json new file mode 100644 index 0000000000..6417b50d7a --- /dev/null +++ b/public/language/tr/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Forumda henüz etiketli herhangi bir başlık yok.", + "bg-color": "Arkaplan Rengi", + "text-color": "Yazı Rengi", + "description": "Etiketleri tıklayarak veya sürükleyerek seçin, birden fazla etiket seçmek için CTRL tuşunu kullanabilirsiniz. ", + "create": "Etiket Oluştur", + "modify": "Etiketleri Düzenle", + "rename": "Etiketleri Yeniden Adlandır", + "delete": "Seçili Etiketleri Sil", + "search": "Etiketleri ara...", + "settings": "Etiket Ayarları", + "name": "Etiket Adı", + + "alerts.editing": "Etiket(ler)i Düzenle", + "alerts.confirm-delete": "Seçilen etiketleri gerçekten silmek istiyor musunuz?", + "alerts.update-success": "Etiket Güncellendi!", + "reset-colors": "Renkleri sıfırla" +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/uploads.json b/public/language/tr/admin/manage/uploads.json new file mode 100644 index 0000000000..c3cd6a4574 --- /dev/null +++ b/public/language/tr/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Dosya yükle", + "filename": "Dosya adı", + "usage": "İleti Kullanımı", + "orphaned": "Sahipsiz", + "size/filecount": "Boyut / Dosya sayısı", + "confirm-delete": "Bu dosyayı silmek istediğinden emin misin?", + "filecount": "%1 dosya", + "new-folder": "Yeni Dosya", + "name-new-folder": "Yeni klasör için bir ad girin" +} \ No newline at end of file diff --git a/public/language/tr/admin/manage/users.json b/public/language/tr/admin/manage/users.json new file mode 100644 index 0000000000..c787e333f3 --- /dev/null +++ b/public/language/tr/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Kullanıcılar", + "edit": "Hareketler", + "make-admin": "Yönetici Yap", + "remove-admin": "Yöneticiliği Sil", + "validate-email": "E-postayı Doğrula", + "send-validation-email": "Onay E-postası Gönder", + "password-reset-email": "E-posta Sıfırlaması için Parola Gönder", + "force-password-reset": "Kullanıcının oturumunu kapat ve şifreyi değiştirmeye zorla", + "ban": "Yasaklı Kullanıcı(lar)", + "temp-ban": "Kullanıcı(ları) Geçici Olarak Yasakla", + "unban": "Kullanıcı(n/lar)ın Yasağını Kaldır", + "reset-lockout": "Kilitlemeyi Sıfırla", + "reset-flags": "Bayrakları Sıfırla", + "delete": " Kullanıcı(ları) Sil", + "delete-content": "Kullanıcı İçeriğini Sil", + "purge": "Kullanıcıyı/ları ve İçeriği Sil", + "download-csv": "CSV İndir", + "manage-groups": "Grupları Düzenle", + "add-group": "Grup ekle", + "create": "Kullanıcı Oluştur", + "invite": "E-posta ile Davet Et", + "new": "Yeni Kullanıcı", + "filter-by": "Filtreleme", + "pills.unvalidated": "Onaylanmamış", + "pills.validated": "Onaylandı", + "pills.banned": "Yasaklandı", + + "50-per-page": "Sayfa başına 50", + "100-per-page": "Sayfa başına 100", + "250-per-page": "Sayfa başına 250", + "500-per-page": "Sayfa başına 500", + + "search.uid": "Kullanıcı Kimliğiyle", + "search.uid-placeholder": "Aramak için bir kullanıcı kimliği girin", + "search.username": "Kullanıcı Adına Göre", + "search.username-placeholder": "Aramak için bir kullanıcı adı girin", + "search.email": "E-posta'ya göre", + "search.email-placeholder": "Aramak için bir e-posta adresi girin", + "search.ip": "IP Adresiyle", + "search.ip-placeholder": "Aramak için bir IP adresi girin", + "search.not-found": "Kullanıcı bulunamadı!", + + "inactive.3-months": "3 ay", + "inactive.6-months": "6 ay", + "inactive.12-months": "12 ay", + + "users.uid": "benzersiz id", + "users.username": "kullanıcı adı", + "users.email": "e-posta", + "users.no-email": "(e-mail yok)", + "users.ip": "IP", + "users.postcount": "ileti sayısı", + "users.reputation": "itibar", + "users.flags": "bayraklar", + "users.joined": "katılım", + "users.last-online": "en son çevrimiçi", + "users.banned": "yasaklı", + + "create.username": "Kullanıcı Adı", + "create.email": "E-posta", + "create.email-placeholder": "Bu kullanıcının e-posta adresi", + "create.password": "Parola", + "create.password-confirm": "Parolayı Onayla", + + "temp-ban.length": "Uzunluk", + "temp-ban.reason": "Sebep(İsteğe Bağlı)", + "temp-ban.hours": "Saat", + "temp-ban.days": "Gün", + "temp-ban.explanation": "Yasağın süresini girin. 0'lık bir zamanın kalıcı bir yasak olarak sayılacağını unutmayın.", + + "alerts.confirm-ban": "Bu kullanıcıyı kalıcı olarak yasaklamak istiyor musunuz?", + "alerts.confirm-ban-multi": "Bu kullanıcıları kalıcı olarak yasaklamak istiyor musunuz?", + "alerts.ban-success": "Kullanıcı(lar) yasaklandı!", + "alerts.button-ban-x": "%1 kullanıcı(ları) yasakla", + "alerts.unban-success": "Kullanıcı(ların) yasağı kaldırıldı!", + "alerts.lockout-reset-success": "Kilitleme(ler) sıfırlandı!", + "alerts.flag-reset-success": "Bayrak(lar) sıfırlandı!", + "alerts.no-remove-yourself-admin": "Kendinizi Yönetici olarak kaldıramazsınız!", + "alerts.make-admin-success": "Kullanıcı şimdi yönetici.", + "alerts.confirm-remove-admin": "Bu yöneticiyi gerçekten kaldırmak istiyor musunuz?", + "alerts.remove-admin-success": "Kullanıcı artık yönetici değil.", + "alerts.make-global-mod-success": "Kullanıcı artık genel moderatör.", + "alerts.confirm-remove-global-mod": "Bu genel moderatörü gerçekten çıkarmak istiyor musunuz?", + "alerts.remove-global-mod-success": "Kullanıcı artık genel moderatör değil.", + "alerts.make-moderator-success": "Kullanıcı artık moderatör.", + "alerts.confirm-remove-moderator": "Bu moderatörü gerçekten çıkarmak istiyor musunuz?", + "alerts.remove-moderator-success": "Kullanıcı artık moderatör değil.", + "alerts.confirm-validate-email": "Bu kullanıcının(ların) e-postasını(larını) doğrulamak istiyor musunuz?", + "alerts.confirm-force-password-reset": "Bu kullanıcıları şifre sıfırlamaya ve oturumlarını kapatmaya zorlamaya emin misiniz?", + "alerts.validate-email-success": "E-postalar doğrulandı", + "alerts.validate-force-password-reset-success": "Kullanıcıların şifreleri sıfırlandı ve mevcut oturumları iptal edildi. ", + "alerts.password-reset-confirm": "Bu kullanıcıya(lara) şifre sıfırlama e-postası(ları) göndermek istiyor musunuz?", + "alerts.password-reset-email-sent": "Şifre yenileme e-postası gönderildi!", + "alerts.confirm-delete": "Uyarı!

Kullanıcı(lar)ı gerçekten silmek istiyor musunuz?

Bu işlem geri alınamaz! Yalnızca kullanıcı hesapları silinecektir, iletiler ve konular kalacaktır.", + "alerts.delete-success": "Kullanıcı(lar) Silindi!", + "alerts.confirm-delete-content": "Uyarı!

Bu kullanıcının(ların) içeriklerinigerçekten silmek istiyor musunuz?

Bu işlem geri alınamaz! Yalnızca kullanıcı hesabı kalacaktır, iletiler ve konular silinecektir.

", + "alerts.delete-content-success": "Kullanıcının(ların) İçerikleri Silindi!", + "alerts.confirm-purge": "Uyarı!

Kullanıcı(ları) ve içeriklerini silmeyi gerçekten istiyor musunuz?

Bu işlem geri alınamaz! Tüm kullanıcı verileri ve içerikleri silinecektir.

", + "alerts.create": "Kullanıcı Oluştur", + "alerts.button-create": "Oluştur", + "alerts.button-cancel": "İptal", + "alerts.error-passwords-different": "Şifreler aynı olmalı!", + "alerts.error-x": "Hata

%1

", + "alerts.create-success": "Kullanıcı oluşturuldu!", + + "alerts.prompt-email": "Eposta:", + "alerts.email-sent-to": "%1'e bir davet e-postası gönderildi", + "alerts.x-users-found": "%1 kullanıcı bulundu, (%2 saniye)", + "export-users-started": "Kullanıcılar csv olarak aktarılmak üzere hazırlanıyor, bu işlem zaman alabilir. İşlem tamamlandığında bildirim alacaksınız!", + "export-users-completed": "Kullanıcılar csv olarak hazırlandı, indirmek için tıklayınız" +} \ No newline at end of file diff --git a/public/language/tr/admin/menu.json b/public/language/tr/admin/menu.json new file mode 100644 index 0000000000..ffac5f26de --- /dev/null +++ b/public/language/tr/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Yönetim Paneli", + "dashboard/overview": "Genel Bakış", + "dashboard/logins": "Girişler", + "dashboard/users": "Kullanıcılar", + "dashboard/topics": "Başlıklar", + "dashboard/searches": "Aramalar", + "section-general": "Genel", + + "section-manage": "Yönet", + "manage/categories": "Kategoriler", + "manage/privileges": "Yetkiler", + "manage/tags": "Etiketler", + "manage/users": "Kullanıcılar", + "manage/admins-mods": "Yöneticiler ve Modlar", + "manage/registration": "Kayıt Kuyruğu", + "manage/post-queue": "İleti Kuyruğu", + "manage/groups": "Gruplar", + "manage/ip-blacklist": "IP Kara Listesi", + "manage/uploads": "Yüklemeler", + "manage/digest": "Özet e-postaları", + + "section-settings": "Ayarlar", + "settings/general": "Genel", + "settings/homepage": "Ana Sayfa", + "settings/navigation": "Navigasyon", + "settings/reputation": "Saygınlık & Şikayetler", + "settings/email": "E-posta", + "settings/user": "Kullanıcılar", + "settings/group": "Gruplar", + "settings/guest": "Ziyaretçiler", + "settings/uploads": "Yüklemeler", + "settings/languages": "Diller", + "settings/post": "İletiler", + "settings/chat": "Sohbetler", + "settings/pagination": "Sayfalama", + "settings/tags": "Etiketler", + "settings/notifications": "Bildirimler", + "settings/api": "API Erişimi", + "settings/sounds": "Sesler", + "settings/social": "Sosyal", + "settings/cookies": "Çerezler", + "settings/web-crawler": "Web Tarayıcısı", + "settings/sockets": "Soketler", + "settings/advanced": "Gelişmiş", + + "settings.page-title": "%1 Ayar", + + "section-appearance": "Görünüm", + "appearance/themes": "Temalar", + "appearance/skins": "Deriler", + "appearance/customise": "Özelleşmiş İçerik (HTML / JS / CSS)", + + "section-extend": "Genişletme", + "extend/plugins": "Eklentiler", + "extend/widgets": "Bileşenler", + "extend/rewards": "Ödüller", + + "section-social-auth": "Sosyal Kimlik Doğrulama", + + "section-plugins": "Eklentiler", + "extend/plugins.install": "Eklenti Yükle", + + "section-advanced": "Gelişmiş", + "advanced/database": "Veritabanı", + "advanced/events": "Olaylar", + "advanced/hooks": "Kancalar", + "advanced/logs": "Kayıtlar", + "advanced/errors": "Hatalar", + "advanced/cache": "Önbellek", + "development/logger": "Kaydedici", + "development/info": "bilgi", + + "rebuild-and-restart-forum": "Forumu Yeniden oluştur & Yeniden Başlat", + "restart-forum": "Forumu Yeniden Başlat", + "logout": "Çıkış", + "view-forum": "Forumu Görüntüle", + + "search.placeholder": "Arama Ayarları", + "search.no-results": "Sonuç yok...", + "search.search-forum": "Forumda ara: ", + "search.keep-typing": "Sonuçları görmek için daha fazla yazın...", + "search.start-typing": "Sonuçları görmek için yazmaya başlayın...", + + "connection-lost": "%1 ile bağlantı kesildi, yeniden bağlanılmaya çalışılıyor...", + + "alerts.version": "NodeBB v%1", + "alerts.upgrade": "Güncelle: v%1" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/advanced.json b/public/language/tr/admin/settings/advanced.json new file mode 100644 index 0000000000..84fc2f5ad5 --- /dev/null +++ b/public/language/tr/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Bakım Modu", + "maintenance-mode.help": "Forum bakım modundayken, tüm istekler statik bir bekletme sayfasına yönlendirilir. Yöneticiler bu yönlendirmeden muaftır ve siteye normal olarak erişebilirler.", + "maintenance-mode.status": "Bakım Modu Durum Kodu", + "maintenance-mode.message": "Bakım Mesajı", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Başlıklar", + "headers.allow-from": "NodeBB'yi bir iFrame'e yerleştirmek için ALLOW-FROM'u ayarla", + "headers.csp-frame-ancestors": "NodeBB'yi bir iFrame'e yerleştirmek için Content-Security-Policy frame-ancestors başlığını ayarla", + "headers.csp-frame-ancestors-help": "\"yok\", \"iç\" (varsayılan) veya izin verilecek URI'lerin listesi.", + "headers.powered-by": "NodeBB tarafından gönderilen \"Powered By\" başlığını özelleştirin", + "headers.acao": "Erişim-Kontrolü-Kaynak-İzni", + "headers.acao-regex": "Erişim-Kontrolü-Kaynak-İzni Düzenli İfade", + "headers.acao-help": "Tüm sitelere erişimi engellemek için boş bırakın", + "headers.acao-regex-help": "Dinamik kökenleri eşleştirmek için buraya düzenli ifadeler girin. Tüm sitelere erişimi reddetmek için boş bırakın", + "headers.acac": "Erişim-Kontrolü-KimlikBilgileri-İzni", + "headers.acam": "Erişim-Kontrolü-Yöntem-İzni", + "headers.acah": "Erişim-Kontrolü-Başlık-İzni", + "headers.coep": "Cross-Origin-Embed Politikası", + "headers.coep-help": "Etkinleştirildiğinde (varsayılan), başlığı require-corp olarak ayarlayacaktır.", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin Kaynak Politikası", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "STS", + "hsts.enabled": "HSTS'yi etkinleştir (önerilir)", + "hsts.maxAge": "HSTS Maksimum Yaş", + "hsts.subdomains": "Alt alanları HSTS üstbilgisine ekle", + "hsts.preload": "HSTS üst bilgisinin ön yüklemesine izin ver", + "hsts.help": "Etkinleştirildiğinde, bu site için bir HSTS başlığı ayarlanır. Alt alanları ve önyükleme bayraklarını dahil etmeyi seçebilirsiniz. Kararsızsanız, bu alanı işaretlenmemiş olarak bırakabilirsiniz. Daha fazla bilgi ", + "traffic-management": "Trafik Yönetimi", + "traffic.help": "NodeBB, yoğun trafik isteklerini otomatik olarak reddeden bir modül ile donatılmıştır. Varsayıla ayarlar başlangıç için yeterli olsa da, bu ayarları buradan düzenleyebilirsiniz.", + "traffic.enable": "Trafik Yönetimini Etkinleştir", + "traffic.event-lag": "Olay Döngüsü Gecikme Eşiği (milisaniye cinsinden)", + "traffic.event-lag-help": "Bu değeri düşürmek, sayfa yüklemeleri için bekleme sürelerini azaltır, ancak daha fazla kullanıcıya \"aşırı yükleme\" mesajını da gösterir. (Yeniden başlatmak gerekir)", + "traffic.lag-check-interval": "Kontrol Aralığı (milisaniye cinsinden)", + "traffic.lag-check-interval-help": "Bu değerin düşürülmesi, NodeBB'nin yükteki ani artışlara daha duyarlı olmasına neden olur, ancak ayrıca kontrolün çok hassas hale gelmesine de neden olabilir. (Yeniden başlatmak gerekir)", + + "sockets.settings": "WebSocket Ayarları", + "sockets.max-attempts": "Maksimum Tekrar Bağlanma Denemesi", + "sockets.default-placeholder": "Varsayılan: %1", + "sockets.delay": "Yeniden Bağlanma Gecikmesi", + + "analytics.settings": "Analitik Ayarlar", + "analytics.max-cache": "Analitik Önbellek Maksimum Değeri", + "analytics.max-cache-help": "Yüksek trafikli zamanlarda, Maksimum Önbellek değerinden daha fazla eşzamanlı etkin kullanıcı varsa önbellek sürekli olarak tüketilebilir. (Yeniden başlatmak gerekir)", + "compression.settings": "Sıkıştırma Ayarları", + "compression.enable": "Sıkıştırmayı Aktifleştir", + "compression.help": "Bu ayar gzip sıkıştırmasını etkinleştirir. Üretimdeki yüksek trafiğe sahip bir web sitesi için sıkıştırmayı uygulamaya koymanın en iyi yolu ters proxy düzeyinden uygulamaktır. Test amacıyla buradan etkinleştirebilirsiniz." +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/api.json b/public/language/tr/admin/settings/api.json new file mode 100644 index 0000000000..1f1f40a6b5 --- /dev/null +++ b/public/language/tr/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Jetonlar (Tokens)", + "settings": "Ayarlar", + "lead-text": "Bu sayfadan NodeBB'deki \"Write API\"e erişimi yapılandırabilirsiniz.", + "intro": "Varsayılan olarak, Yazma API'si kullanıcıların kimliklerini oturum tanımlama bilgileri temelinde doğrular, ancak NodeBB ayrıca bu sayfa aracılığıyla oluşturulan belirteçler aracılığıyla Taşıyıcı kimlik doğrulamasını da destekler.", + "docs": "Tüm API özeliklerine erişmek için buraya tıklayın. ", + + "require-https": "API kullanımı için HTTPS kısıtlaması gerektir", + "require-https-caveat": "Not: Yük dengeleyicilerini içeren bazı kurulumlar, isteklerini HTTP kullanarak NodeBB'ye proxy uygulayabilir, bu durumda bu seçenek devre dışı kalmalıdır.", + + "uid": "Kullanıcı ID", + "uid-help-text": "Bu jetonla ilişkilendirilecek bir Kullanıcı Kimliği belirtin. Kullanıcı kimliği 0 ise, diğer kullanıcıların kimliğini _uid parametresine göre üstlenebilen bir ana simge olarak kabul edilir.", + "description": "Açıklama", + "no-description": "Hiçbir açıklama belirtilmemiş.", + "token-on-save": "Form kaydedildikten sonra bir jeton oluşturulacak" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/chat.json b/public/language/tr/admin/settings/chat.json new file mode 100644 index 0000000000..ac06420c4d --- /dev/null +++ b/public/language/tr/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Sohbet Ayarları", + "disable": "Sohbeti kapat", + "disable-editing": "Sohbet mesajlarını düzenlemeyi/silmeyi kapat", + "disable-editing-help": "Yöneticiler ve global moderatörler bu kısıtlamadan muaftır", + "max-length": "Maksimum sohbet mesajı uzunluğu", + "max-room-size": "Sohbet odalarındaki maksimum kullanıcı sayısı", + "delay": "Sohbet mesajları arasındaki süre (milisaniye)", + "notification-delay": "Sohbet mesajları için bildirim gecikme süresi (gecikme olmaması için 0 yazın)", + "restrictions.seconds-edit-after": "Sohbet mesajları kaç saniye boyunca düzenlenebilir olacak (0 - özellik aktif değil)", + "restrictions.seconds-delete-after": "Sohbet mesajları kaç saniye boyunca silinebilir olacak (0 - özellik aktif değil)" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/cookies.json b/public/language/tr/admin/settings/cookies.json new file mode 100644 index 0000000000..8b9923cc50 --- /dev/null +++ b/public/language/tr/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "AB Onayı", + "consent.enabled": "Aktif", + "consent.message": "Bildirim mesajı", + "consent.acceptance": "Onay mesajı", + "consent.link-text": "Politika Bağlantısı Metni", + "consent.link-url": "Politika Sayfanızın URL adresi", + "consent.blank-localised-default": "NodeBB yerelleştirilmiş varsayılanlarını kullanmak için boş bırakın", + "settings": "Ayarlar", + "cookie-domain": "Oturum çerezi alanı", + "max-user-sessions": "Kullanıcı başı maksimum aktif oturum ", + "blank-default": "Varsayılan için boş bırakın" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/email.json b/public/language/tr/admin/settings/email.json new file mode 100644 index 0000000000..bdebff56d6 --- /dev/null +++ b/public/language/tr/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "E-posta Ayarları", + "address": "E-posta Adresi", + "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", + "from": "From Name", + "from-help": "The from name to display in the email.", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "SMTP Aktarımını Etkinleştir", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Özel Servis", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Kullanıcı Adı", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "smtp-transport.pool": "Toplu bağlantıları aktifleştir", + "smtp-transport.pool-help": "Toplu bağlantılar NodeBB'nin her e-posta için yeni bir bağlantı oluşturmasını engeller. Bu seçenek sadece SMTP Transport aktif ise geçerlidir.", + + "template": "E-posta Kalıbını Düzenle", + "template.select": "E-posta Kalıbını Seç", + "template.revert": "Revert to Original", + "testing": "Email Testing", + "testing.select": "E-posta Kalıbını Seç", + "testing.send": "Test E-postası Gönder", + "testing.send-help": "The test email will be sent to the currently logged in user's email address.", + "subscriptions": "Özet E-postaları", + "subscriptions.disable": "Özet e-postalarını kapat", + "subscriptions.hour": "Digest Hour", + "subscriptions.hour-help": "Please enter a number representing the hour to send scheduled email digests (e.g. 0 for midnight, 17 for 5:00pm). Keep in mind that this is the hour according to the server itself, and may not exactly match your system clock.
The approximate server time is:
The next daily digest is scheduled to be sent ", + "notifications.remove-images": "Görselleri e-posta bildirimlerinden kaldır", + "require-email-address": "Yeni kullanıcıların bir e-posta adresi belirtmesini gerektir", + "require-email-address-warning": "Varsayılan olarak kullanıcıların bir e-posta adresi girmesi devre dışıdır. Bu seçeneğin etkinleştirirseniz kullanıcı kayıt esnasında bir e-posta adresi girmek zorunda kalır. Elbette bu kullanıcının gerçek bir e-posta adresi veya kendine ait olan bir e-posta girdiği anlamını her zaman taşımaz.", + "send-validation-email": "Bir e-posta eklendiğinde veya değiştirildiğinde doğrulama için e-posta gönderilsin", + "include-unverified-emails": "E-postalarını onaylamayan alıcılara onay e-postası gönderin", + "include-unverified-warning": "Varsayılan olarak, hesaplarıyla ilişkili e-postaları olan kullanıcılar (Sosyal Login) zaten doğrulanmıştır, ancak durumun böyle olmadığı durumlar vardır (ör. Riski size ait olmak üzere bu ayarı etkinleştirin – doğrulanmamış adreslere e-posta göndermek, bölgesel istenmeyen posta önleme yasalarının ihlali olabilir.", + "prompt": "Kullanıcılardan e-postalarını girmelerini veya onaylamalarını isteyin", + "prompt-help": "Bir kullanıcının e-posta seti yoksa veya e-postası onaylanmadıysa ekranda bir uyarı gösterilir.", + "sendEmailToBanned": "Yasaklanmış olsalar bile kullanıcılara e-posta gönder" +} diff --git a/public/language/tr/admin/settings/general.json b/public/language/tr/admin/settings/general.json new file mode 100644 index 0000000000..6bf2e17cdf --- /dev/null +++ b/public/language/tr/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Site Ayarları", + "title": "Site Başlığı", + "title.short": "Başlık Kısaltması", + "title.short-placeholder": "Eğer başlık kısaltması girilmediyse \"Site Başlığı\" kullanılacak", + "title.url": "Başlık Bağlantı URL'si", + "title.url-placeholder": "Site başlığının URL adresi", + "title.url-help": "Başlık tıklandığında kullanıcıları bu adrese gönderin. Boş bırakılırsa, kullanıcı forum dizinine gönderilecektir. Not: Bu, e-postalarda vb. kullanılan harici URL değildir. Bu, config.json'daki url özelliği tarafından belirlenir.", + "title.name": "Topluluk İsmi", + "title.show-in-header": "Site Konusunu Başlık'ta Göster", + "browser-title": "Tarayıcı Başlığı", + "browser-title-help": "Hiçbir tarayıcı başlığı belirtilmemişse, site başlığı kullanılır", + "title-layout": "Başlık Düzeni", + "title-layout-help": "Tarayıcı başlığının nasıl yapılandırılacağını tanımlayın örn. {pageTitle} | {browserTitle}", + "description.placeholder": "Topluluk hakkında kısa bir açıklama yazın", + "description": "Site Açıklaması", + "keywords": "Site Anahtar Kelimeler", + "keywords-placeholder": "Topluluğunuzu tanımlayan anahtar kelimeler, virgülle-ayrılmış", + "logo": "Site Logosu", + "logo.image": "Görsel", + "logo.image-placeholder": "Forum başlığında görüntülenecek bir logo yolu", + "logo.upload": "Yükle", + "logo.url": "Logonun Linki", + "logo.url-placeholder": "Site Logo URL'si", + "logo.url-help": "Logoya tıklandığında kullanıcıları bu adrese gönderin. Boş bırakılırsa, kullanıcı forum dizinine gönderilecektir. Not: Bu, e-postalarda vb. kullanılan harici URL değildir. Bu, config.json'daki url özelliği tarafından belirlenir.", + "logo.alt-text": "Alt Yazı", + "log.alt-text-placeholder": "Erişilebilirlik için alternatif metin", + "favicon": "Favicon", + "favicon.upload": "Yükle", + "pwa": "İleri Web Uygulaması", + "touch-icon": "Dokunma Simgesi", + "touch-icon.upload": "Yükle", + "touch-icon.help": "Önerilen Boyut: 512x512. Önerilen format: PNG. Simge belirtilmezse varsayılan olarak favicon kullanılır.", + "maskable-icon": "Maskelenebilir (Ana Ekran) Simgesi", + "maskable-icon.help": "Önerilen boyut ve format: 512x512, PNG formatı. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Harici Bağlantılar", + "outgoing-links.warning-page": "Dışarı giden bağlantılar için uyarı sayfası kullan", + "search": "Arama", + "search-default-in": "Araştır", + "search-default-in-quick": "Hızlı Arama", + "search-default-sort-by": "Göre sırala", + "outgoing-links.whitelist": "Uyarı sayfasını atlamak için beyaz listeye eklenecek alan-adları", + "site-colors": "Site Renk Metaverisi", + "theme-color": "Tema rengi", + "background-color": "Arkaplan rengi", + "background-color-help": "Site PWA olarak kurulduğunda ekran arkaplanı olarak kullanılacak renk", + "undo-timeout": "Zaman Aşımını Geri Al", + "undo-timeout-help": "Konu taşıma gibi bazı işlemler, moderatörün belirli bir zaman dilimi içinde eylemlerini geri almasına olanak tanır. Tamamen geri almayı devre dışı bırakmak için 0'a ayarlayın.", + "topic-tools": "Konu Araçları" +} diff --git a/public/language/tr/admin/settings/group.json b/public/language/tr/admin/settings/group.json new file mode 100644 index 0000000000..5d8ecd164b --- /dev/null +++ b/public/language/tr/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Genel", + "private-groups": "Gizli Grup", + "private-groups.help": "Eğer etkinse, gruba katılmak için grup sahibinin onayı gerekir. (Varsayılan: Etkin)", + "private-groups.warning": "Dikkat! Eğer bu opsiyon devre dışıysa ve özel gruplarınız varsa, onlar otomatik olarak genele dönüşecektir.", + "allow-multiple-badges": "Çoklu rozete izin ver", + "allow-multiple-badges-help": "Bu bayrak, kullanıcıların birden fazla grup rozetini seçmelerine izin vermek için kullanılabilir, tema desteği gerektirir.", + "max-name-length": "Maksimum Grup Adı Uzunluğu", + "max-title-length": "Grup İsmi Azami Uzunluğu", + "cover-image": "Grup Kapak Resmi", + "default-cover": "Varsayılan Kapak Resmi", + "default-cover-help": "Kapak resmi olmayan grupları, varsayılan kapak resimlerini kullanmaları için virgülle ayırarak ekleyin" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/guest.json b/public/language/tr/admin/settings/guest.json new file mode 100644 index 0000000000..1362b3affb --- /dev/null +++ b/public/language/tr/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Ayarlar", + "handles.enabled": "Misafir üyelere izin ver", + "handles.enabled-help": "Bu seçenek, misafirlerin yaptıkları her gönderiyle ilişkilendirebilecekleri bir isim alanı sunar. Devre dışı bırakılırsa, gönderenin ismi basitçe \"Misafir\" olarak adlandırılacaktır.", + "topic-views.enabled": "Ziyaretçilerin konu bakış sayısını arttırmasına izin ver", + "reply-notifications.enabled": "Ziyaretçilerin cevap bildirimleri oluşturmasına izin ver" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/homepage.json b/public/language/tr/admin/settings/homepage.json new file mode 100644 index 0000000000..3c3b08f9bd --- /dev/null +++ b/public/language/tr/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Ana Sayfa", + "description": "Kullanıcıların, forumunuzun kök bağlantısına gittiğinde hangi sayfanın görüntüleneceğini seçin.", + "home-page-route": "Ana Sayfa Yolu", + "custom-route": "Özel Yol", + "allow-user-home-pages": "Kullanıcılara ana sayfalarını özelleştirmeleri için izin ver", + "home-page-title": "Ana sayfanın başlığı (varsayılan \"Ana Sayfa\")" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/languages.json b/public/language/tr/admin/settings/languages.json new file mode 100644 index 0000000000..5ca8e3ec08 --- /dev/null +++ b/public/language/tr/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Dil Ayarları", + "description": "Varsayılan dil, forumunuzu ziyaret eden tüm kullanıcılar için dil ayarlarını belirler.
Kullanıcılar, bireysel olarak hesap ayarları sayfasında varsayılan dili geçersiz kılabilir.", + "default-language": "Varsayılan Dil", + "auto-detect": "Ziyaretçiler için dili otomatik tespit et" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/navigation.json b/public/language/tr/admin/settings/navigation.json new file mode 100644 index 0000000000..fd2d7766d0 --- /dev/null +++ b/public/language/tr/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "İkon:", + "change-icon": "değiştir", + "route": "Yol:", + "tooltip": "Araç ipucu: ", + "text": "Yazı:", + "text-class": "Metin Sınıfı: opsiyonel", + "class": "Sınıf: opsiyonel", + "id": "ID: opsiyonel", + + "properties": "Özellikler:", + "groups": "Gruplar", + "open-new-window": "Yeni pencerede aç", + "dropdown": "Açılır liste", + "dropdown-placeholder": "Açılır liste öğelerini aşağıda sırala, örneğin:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Sil", + "btn.disable": "Etkinsizleştir", + "btn.enable": "Etkinleştir", + + "available-menu-items": "Kullanılabilir Menü Öğeleri", + "custom-route": "Özel Yol", + "core": "çekirdek", + "plugin": "eklenti" +} diff --git a/public/language/tr/admin/settings/notifications.json b/public/language/tr/admin/settings/notifications.json new file mode 100644 index 0000000000..bf9502ff3d --- /dev/null +++ b/public/language/tr/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Bildirimler", + "welcome-notification": "Hoş Geldin Bildirimi", + "welcome-notification-link": "Hoş Geldin Bildirimi Bağlantısı", + "welcome-notification-uid": "Kullanıcı Hoş Geldiniz Bildirimi (UID)", + "post-queue-notification-uid": "İletisi kuyruğa alınan kullanıcı (UID)" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/pagination.json b/public/language/tr/admin/settings/pagination.json new file mode 100644 index 0000000000..c38eb0c0e0 --- /dev/null +++ b/public/language/tr/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Sayfalama Ayarları", + "enable": "Sonsuz kaydırma yerine konu ve gönderileri sayfalandır.", + "posts": "İleti Sayfalama", + "topics": "Başlık Sayfalama", + "posts-per-page": "Sayfa başına İletiler", + "max-posts-per-page": "Sayfa başına azami gönderi sayısı", + "categories": "Kategori Sayfalama", + "topics-per-page": "Sayfa başına Konular", + "max-topics-per-page": "Sayfa başına azami konu", + "categories-per-page": "Sayfa başına Kategoriler" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/post.json b/public/language/tr/admin/settings/post.json new file mode 100644 index 0000000000..8034b6f1d6 --- /dev/null +++ b/public/language/tr/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "İleti Sıralaması", + "sorting.post-default": "Varsayılan İleti Sıralaması", + "sorting.oldest-to-newest": "En Eskiden En Yeniye", + "sorting.newest-to-oldest": "En Yeniden En Eskiye", + "sorting.most-votes": "En Çok Oylanan", + "sorting.most-posts": "En çok yazılanlar", + "sorting.topic-default": "Varsayılan Konu Sıralaması", + "length": "İleti Uzunluğu", + "post-queue": "İleti Kuyruğu", + "restrictions": "İleti Kısıtlamaları", + "restrictions-new": "Yeni Kullanıcı Kısıtlamaları", + "restrictions.post-queue": "İleti kuyruğunu etkinleştir", + "restrictions.post-queue-rep-threshold": "İleti kuyruğuna girmemek için gereken saygınlık sayısı", + "restrictions.groups-exempt-from-post-queue": "İleti kuyruğuna girmeyecek grupları seçiniz", + "restrictions-new.post-queue": "Yeni kullanıcı kısıtlamalarını etkinleştir", + "restrictions.post-queue-help": "İleti kuyruğunu etkinleştirirseniz yeni kullanıcıların iletilerinin foruma aktarılmadan önce onayı gerekecek", + "restrictions-new.post-queue-help": "Yeni kullanıcı kısıtlamalarını etkinleştirmek yeni kullanıcılar tarafından oluşturulan iletilere sınırlama getirecek", + "restrictions.seconds-between": "Her ileti gönderimi arasındaki saniye cinsinden süre", + "restrictions.seconds-between-new": "Yeni kullanıcılar için gönderimler arasındaki saniye cinsinden süre", + "restrictions.rep-threshold": "Bu kısıtlamalardan önceki itibar eşiği kaldırıldı", + "restrictions.seconds-before-new": "Yeni kullanıcılar kaç saniye sonra ilk iletiyi gönderebilir", + "restrictions.seconds-edit-after": "Bir ileti kaç saniye boyunca değiştirilebilir (Etkinsizleştirmek için 0 yazınız)", + "restrictions.seconds-delete-after": "Bir ileti kaç saniye boyunca silinebilir (Etkinsizleştirmek için 0 yazınız)", + "restrictions.replies-no-delete": "Bir başlığa kaç ileti yazıldıktan sonra o başlık silinemez (Etkinsizleştirmek için 0 yazınız)", + "restrictions.min-title-length": "Minimum Başlık Uzunluğu", + "restrictions.max-title-length": "Maksimum Başlık Uzunluğu", + "restrictions.min-post-length": "Minimum İleti Uzunluğu", + "restrictions.max-post-length": "Maksimum İleti Uzunluğu", + "restrictions.days-until-stale": "Konu eskimiş sayılana kadar geçen gün sayısı", + "restrictions.stale-help": "Bir konu \"eskimiş\" olarak kabul edilirse, o konuya cevap vermeye çalışan kullanıcılara bir uyarı gösterilecektir.", + "timestamp": "Zaman Damgası", + "timestamp.cut-off": "Kapanış tarihi (günler)", + "timestamp.cut-off-help": "Tarihler ve & zamanlar göreli bir şekilde (ör. \"3 saat önce\" / \"5 gün önce\") gösterilecek ve çeşitli dillerde yerleştirilecektir.\n\t\t\t\t\tBelirli bir noktadan sonra, bu metin yerelleştirilmiş tarihi göstermek için değiştirilebilir.\n\t\t\t\t\t(ör. 5 Kasım 2016 15:30).
(Varsayılan: 30, veya bir ay). Tarihleri her zaman görüntülemek için 0'a ayarlayın, her zaman göreli zamanları görüntülemek için boş bırakın.", + "timestamp.necro-threshold": "Ardışık iletiler arasında geçen gün sayısı (Necro Sınırı)", + "timestamp.necro-threshold-help": "Eğer iki ardışık ileti arasında geçen süre Necro Sınırı'ndan fazlaysa, geçen süre yazıyla belirtilecek. (Varsayılan: 7, yani bir hafta). Etkinsizleştirmek için 0 yazınız.", + "timestamp.topic-views-interval": "Konu görüntüleme aralığı artışları (dakika)", + "timestamp.topic-views-interval-help": "Başlıkların bakış sayısı bu ayarların belirttiği her X dakikada bir güncellenecek. ", + "teaser": "Teaser İleti", + "teaser.last-post": "Son – cevap yoksa orijinal gönderi de dahil olmak üzere en son gönderiyi gösterir.", + "teaser.last-reply": "Son – cevap yoksa en son yanıtı veya \"Yanıt yok\" yertutucusunu gösterir.", + "teaser.first": "İlk", + "showPostPreviewsOnHover": "İmleç ileti üstüne geldiğinde önizleme göster", + "unread": "Okunmamış Ayarları", + "unread.cutoff": "Okunmamış gün sınırı", + "unread.min-track-last": "Son okumayı takip etmeden önce konuya yapılan asgari gönderim", + "recent": "Güncel Ayarlar", + "recent.max-topics": "Güncel Bölümde Gösterilecek Maksimum Konu Sayısı", + "recent.categoryFilter.disable": "Son sayfada önemsenmeyen kategorilerde konu filtrelemeyi devre dışı bırak", + "signature": "İmza Ayarları", + "signature.disable": "İmzaları devre dışı bırak", + "signature.no-links": "İmzalarda linkleri devre dışı bırak", + "signature.no-images": "İmzalarda resimleri devre dışı bırak", + "signature.hide-duplicates": "Başlıklardaki ikinci kopya imzaları gizle", + "signature.max-length": "Maksimum İmza Uzunluğu", + "composer": "Editör Ayarları", + "composer-help": "Aşağıdaki ayarlar, yeni konular oluşturduklarında veya mevcut konulara cevap verdiklerinde kullanıcıların \n\t\t\t\tyazı alanının işlevselliğini ve / veya görünümünü yönetmelerini sağlar.", + "composer.show-help": "\"Yardım\" sekmesini göster", + "composer.enable-plugin-help": "Eklentilerin yardım sekmesine içerik eklemesine izin ver", + "composer.custom-help": "Özel Yardım Metni", + "backlinks": "Geri bağlantılar", + "backlinks.enabled": "Konu geri bağlantılarını etkinleştir", + "backlinks.help": "Bir gönderi başka bir konuya atıfta bulunuyorsa, o anda referans verilen konuya gönderiye geri bir bağlantı eklenir.", + "ip-tracking": "IP İzleme", + "ip-tracking.each-post": "Her ileti için IP Adresini takip et", + "enable-post-history": "Gönderi Geçmişini Etkinleştir" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/reputation.json b/public/language/tr/admin/settings/reputation.json new file mode 100644 index 0000000000..57d50011b6 --- /dev/null +++ b/public/language/tr/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "İtibar Ayarları", + "disable": "İtibar Sistemini Devre Dışı Bırak", + "disable-down-voting": "Eksi Oyu Devre Dışı Bırak", + "votes-are-public": "Tüm Oylar Herkese Açık", + "thresholds": "Etkinlik Eşikleri", + "min-rep-upvote": "Artılanan iletiler için gereken minimum itibar", + "upvotes-per-day": "Artı oy günlük limiti (sınırsız artı oy için 0 giriniz)", + "upvotes-per-user-per-day": "Aynı kişiye verilecek artı oy günlük limiti (sınırsız artı oy için 0 giriniz)", + "min-rep-downvote": "Eksilenen iletiler için gereken minimum itibar", + "downvotes-per-day": "Eksi oy günlük limiti (sınırsız eksi oy için 0 giriniz)", + "downvotes-per-user-per-day": "Aynı kişiye verilecek eksi oy günlük limiti (sınırsız eksi oy için 0 giriniz)", + "min-rep-chat": "Özel sohbet edebilmek için gerekli minimum itibar", + "min-rep-flag": "İletileri şikayet etmek için gerekli minimum itibar", + "min-rep-website": "Kullanıcı profiline \"Web Sitesi\" eklemek için gerekli minimum itibar", + "min-rep-aboutme": "Kullanıcı profiline \"Hakkımda\" eklemek için gereken minimum itibar", + "min-rep-signature": "Kullanıcı profiline \"İmza\" eklemek için gerekli minimum itibar", + "min-rep-profile-picture": "Kullanıcı profiline \"Profil Resmi\" eklemek için gerekli minimum itibar", + "min-rep-cover-picture": "Kullanıcı profiline \"Kapak Resmi\" eklemek için gerekli minimum itibar", + + "flags": "Şikayet Ayarları", + "flags.limit-per-target": "Maksimum şikayet edilme sayısı", + "flags.limit-per-target-placeholder": "Varsayılan: 0", + "flags.limit-per-target-help": "Bir gönderi veya kullanıcı birden çok kez şikayet edildiğinde, her ek şikayet bir \"rapor\" olarak kabul edilir ve orijinal şikayete eklenir. Bir öğenin alabileceği rapor sayısını sınırlamak için bu seçeneği sıfırdan farklı bir sayıya ayarlayın.", + "flags.auto-flag-on-downvote-threshold": "Bir iletinin otomatik olarak raporlanması için alması gereken eksi oy sayısı ( Otomatik şikayet özelliğini iptal etmek için buraya 0 giriniz, varsayılan: 0)", + "flags.auto-resolve-on-ban": "Bir kullanıcı forumdan yasaklandığında otomatik olarak şikayetlerini çözülmüş say", + "flags.action-on-resolve": "Bir şikayet çözümlendiğinde şunu yap", + "flags.action-on-reject": "Bir şikayet reddedildiğinde şunu yap", + "flags.action.nothing": "Hiçbir şey yapma", + "flags.action.rescind": "Moderatör ve Adminlere gönderilen bildirimleri iptal et" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/social.json b/public/language/tr/admin/settings/social.json new file mode 100644 index 0000000000..5b186a82c4 --- /dev/null +++ b/public/language/tr/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "İleti Paylaşımı", + "info-plugins-additional": "Eklentiler, paylaşımda bulunmak için ek sosyal ağlar ekleyebilir.", + "save-success": "İleti Paylaşım Ağları başarıyla kaydedildi!" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/sockets.json b/public/language/tr/admin/settings/sockets.json new file mode 100644 index 0000000000..f0155deb5c --- /dev/null +++ b/public/language/tr/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Tekrar Bağlantı Ayarları", + "max-attempts": "Maks Tekrar Bağlanma Denemesi", + "default-placeholder": "Varsayılan: %1", + "delay": "Yeniden Bağlanma Gecikmesi" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/sounds.json b/public/language/tr/admin/settings/sounds.json new file mode 100644 index 0000000000..e1d8a3c4a7 --- /dev/null +++ b/public/language/tr/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Bildirimler", + "chat-messages": "Sohbet Mesajları", + "play-sound": "Oynat", + "incoming-message": "Gelen İleti", + "outgoing-message": "Giden İleti", + "upload-new-sound": "Yeni Ses Yükle", + "saved": "Ayarlar Kaydedildi" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/tags.json b/public/language/tr/admin/settings/tags.json new file mode 100644 index 0000000000..256c56e1ee --- /dev/null +++ b/public/language/tr/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Etiket Ayarları", + "link-to-manage": "Etiketleri Düzenle", + "system-tags": "Sistem etiketleri", + "system-tags-help": "Sadece ayrıcalıklı üyeler bu etiketleri görebilir.", + "min-per-topic": "Konu Başına Minimum Etiket Sayısı", + "max-per-topic": "Konu Başına Maksimum Etiket Sayısı", + "min-length": "Minimum Etiket Uzunluğu", + "max-length": "Maksimum Etiket Uzunluğu", + "related-topics": "İlgili Konular", + "max-related-topics": "Görüntülenecek maksimum ilgili konu sayısı (Tema destekliyorsa)" +} \ No newline at end of file diff --git a/public/language/tr/admin/settings/uploads.json b/public/language/tr/admin/settings/uploads.json new file mode 100644 index 0000000000..ec5035f281 --- /dev/null +++ b/public/language/tr/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "İletiler", + "orphans": "Artık Dosyalar", + "private": "Yüklenen dosyaları gizli yap", + "strip-exif-data": "EXIF bilgilerini sil", + "preserve-orphaned-uploads": "Bir ileti kaldırıldıktan sonra yüklenilen dosyaları diskte bırak", + "orphanExpiryDays": "Artık dosyaların tutulacağı gün süresi", + "orphanExpiryDays-help": "Bu kadar günden sonra artık dosyalar sistemden silinecek.
Devredışı bırakmak için 0 olarak girin veya boş bırakın.", + "private-extensions": "Gizli yapılacak dosya uzantıları", + "private-uploads-extensions-help": "Buraya gizli yapılacak dosya uzantıları listesini virgülle ayırarak giriniz. (ör. pdf,xls,doc). Boş bırakmak, tüm dosyaların gizli olacağı anlamına gelir.", + "resize-image-width-threshold": "Belirtilen genişlikten daha genişse görüntüleri yeniden boyutlandırın", + "resize-image-width-threshold-help": "(piksel olarak, varsayılan: 1520 piksel, devre dışı bırakmak için 0 yazın.)", + "resize-image-width": "Görüntüleri belirtilen genişliğe yeniden boyutlandır", + "resize-image-width-help": "(piksel olarak, varsayılan: 760 piksel, devre dışı bırakmak için 0 yazın.)", + "resize-image-quality": "Resimleri yeniden boyutlandırırken kullanılacak kalite", + "resize-image-quality-help": "Yeniden boyutlandırılan görüntülerin dosya boyutunu azaltmak için daha düşük bir kalite ayarı kullan.", + "max-file-size": "Maksimum Dosya Boyutu (KiB)", + "max-file-size-help": "(Kilobayt, varsayılan: 2048 KiB)", + "reject-image-width": "Maksimum Görsel Genişliği (piksel)", + "reject-image-width-help": "Bu değerden daha geniş olan görseller reddedilecektir.", + "reject-image-height": "Maksimum Görsel Yüksekliği (piksel)", + "reject-image-height-help": "Bu değerden daha uzun olan görseller reddedilecektir.", + "allow-topic-thumbnails": "Kullanıcıların konulara küçük resim yüklemesine izin ver", + "topic-thumb-size": "Konu Küçük Resim Boyutu", + "allowed-file-extensions": "İzin Verilen Dosya Uzantıları", + "allowed-file-extensions-help": "Virgül ile ayrılmış dosya uzantıları listesini buraya girin (ör. pdf, xls, doc). Boş bir liste, tüm uzantılara izin verildiği anlamına gelir.", + "upload-limit-threshold": "Kullanıcı yükleme limit hızı:", + "upload-limit-threshold-per-minute": "Her %1 Dakika", + "upload-limit-threshold-per-minutes": "Her %1 Dakika", + "profile-avatars": "Profil Avatarları", + "allow-profile-image-uploads": "Kullanıcıların profil resmi yüklemesine izin ver", + "convert-profile-image-png": "Profil resmi yüklemelerini PNG'ye dönüştür", + "default-avatar": "Özel Varsayılan Avatar", + "upload": "Yükle", + "profile-image-dimension": "Profil Resmi Boyutu", + "profile-image-dimension-help": "(Piksel cinsinden, varsayılan: 128 piksel)", + "max-profile-image-size": "Maksimum Profil Resmi Dosya Boyutu", + "max-profile-image-size-help": "(Kilobayt, varsayılan: 256 KiB)", + "max-cover-image-size": "Maksimum Kapak Görseli Dosya Boyutu", + "max-cover-image-size-help": "(Kilobayt, varsayılan: 2,048 KiB)", + "keep-all-user-images": "Avatarların ve kapak resimlerinin eski sürümlerini sunucuda sakla", + "profile-covers": "Profil Kapakları", + "default-covers": "Varsayılan Kapak Resmi", + "default-covers-help": "Kapak resmi olmayan hesapları, varsayılan kapak resimlerini kullanmaları için virgülle ayırarak ekleyin" +} diff --git a/public/language/tr/admin/settings/user.json b/public/language/tr/admin/settings/user.json new file mode 100644 index 0000000000..4b9c6a1952 --- /dev/null +++ b/public/language/tr/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Kimlik Doğrulama", + "email-confirm-interval": "Kullanıcı onay e-postasını tekrar gönderemez", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Girişe izin ver:", + "allow-login-with.username-email": "Kullanıcı Adı veya E-posta", + "allow-login-with.username": "Sadece kullanıcı adı", + "account-settings": "Hesap Ayarları", + "gdpr_enabled": "GDPR veri toplamayı etkinleştir", + "gdpr_enabled_help": "Etkinleştirilirse, yeni kayıt olan kullanıcılar General Data Protection Regulation (GDPR) düzenlemeleri altında veri toplanılması ve kullanılması için açık bir şekilde izin vermiş olurlar. Not: GDPR özelliğini aktifleştirmek halihazırda kayıtlı olan kişileri etkilemez. Şu anki kayıtlı kullanıcıların bu düzenlemeleri kabul etmesi için GDPR eklentisini yüklemelisiniz.", + "disable-username-changes": "Kullanıcı adı değişikliği kapalı", + "disable-email-changes": "E-posta değişikliklerini devre dışı bırak", + "disable-password-changes": "Parola değişikliği kapalı", + "allow-account-deletion": "Hesap silmeye izin ver", + "hide-fullname": "Kullanıcı adını gizle", + "hide-email": "E-posta adresini kullanıcılardan gizle", + "show-fullname-as-displayname": "Eğer girilmişse kullanıcının tam ismini görüntülenen isim olarak değiştir", + "themes": "Temalar", + "disable-user-skins": "Kullanıcıların özel bir deri seçmesini engelle", + "account-protection": "Hesap Koruma", + "admin-relogin-duration": "Yönetici yeniden giriş süresi (dakika)", + "admin-relogin-duration-help": "Beliril bir süreden sonra yönetici bölümüne erişmek için yeniden giriş yapılması gerekir, devre dışı bırakmak için 0 olarak ayarlayın", + "login-attempts": "Saatlik giriş deneme sayısı", + "login-attempts-help": "Bir kullanıcının hesabına giriş denemesi bu sınırı aşarsa, bu hesap önceden belirlenmiş olan bir süre için kilitlenir.", + "lockout-duration": "Hesap Kilitleme Süresi (dakika)", + "login-days": "Kullanıcı oturumlarının hatırlanacağı gün sayısı", + "password-expiry-days": "Belirli bir süre sonunda parola sıfırlamayı zorla", + "session-time": "Oturum Süresi", + "session-time-days": "Gün", + "session-time-seconds": "Saniye", + "session-time-help": "Bu değerler, kullanıcının \"Beni Hatırla\" seçeneğini işaretlediğinde ne kadar süreyle oturumda kaldığını kontrol etmek için kullanılır. girişte. Bu değerlerden sadece birinin kullanılacağını unutmayın. Saniye değeri yoksa, gün dikkate alınır. Gün değeri yoksa, varsayılan değer 14 gün olur.", + "online-cutoff": "Kullanıcının atıl olarak değerlendirileceği dakika cinsinden geçen süre", + "online-cutoff-help": "Eğer kullanıcı bu süre içinde herhangi bir işlem yapmazsa, etkin olmayan olarak kabul edilir ve gerçek zamanlı güncellemeler almaz.", + "registration": "Kullanıcı Kaydı", + "registration-type": "Kayıt Tipi", + "registration-approval-type": "Kayıt Onay Tipi", + "registration-type.normal": "Normal", + "registration-type.admin-approval": "Yönetici Onayı", + "registration-type.admin-approval-ip": "IP'ler için Yönetici Onayı", + "registration-type.invite-only": "Sadece Davet", + "registration-type.admin-invite-only": "Sadece Yönetici Daveti", + "registration-type.disabled": "Kayıt yok", + "registration-type.help": "Normal - Kullanıcılar, kayıt sayfasından kayıt olabililrler
\nDavetiye İle - Kullanıcılar, başkalarını users sayfasından davet edebilirler.
\nYönetici Davetiyesi İle - Sadece yöneticiler başkalarını users sayfasından ve admin/manage/users sayfasından davet edebilir.
\nKayıt Yok - Yeni üye kaydı kapalı
", + "registration-approval-type.help": "Normal - Kullanıcılar hemen kaydolur.
\nYönetici Onayı - Kullanıcı kayıtları, yöneticiler tarafından onaylansın diyeapproval queue sırasına konulur.
\nIP için Yönetici Onayı - Yeni Kullanıcılar için Normal Üyelik; hali hazırda kayıtlı olan IP adresleri için Yönetici Onayı
", + "registration-queue-auto-approve-time": "Otomatik onaylanma süresi", + "registration-queue-auto-approve-time-help": "Kullanıcıların otomatik onayı için geçecek süre (saat). İptal etmek için 0 yazın.", + "registration-queue-show-average-time": "Kullanıcılara ortalama üyelik onaylanma süresini göster", + "registration.max-invites": "Kullanıcı Başına Maksimum Davetiye", + "max-invites": "Kullanıcı Başına Maksimum Davetiye", + "max-invites-help": "Kısıtlama olmaması için 0. Yöneticiler sınırsız davetiyeye sahiptir
\"Yalnızca Davet\" seçeneği için geçerlidir", + "invite-expiration": "Davet süresi sonu", + "invite-expiration-help": "Davetiyelerin # gün içinde süresi dolacak.", + "min-username-length": "Minimum Kullanıcı Adı Uzunluğu", + "max-username-length": "Maksimum Kullanıcı Adı Uzunluğu", + "min-password-length": "Minimum Parola Uzunluğu", + "min-password-strength": "Minimum Parola Gücü", + "max-about-me-length": "Maksimum Hakkımda Uzunluğu", + "terms-of-use": "Forum Kullanım Şartları (Devre dışı bırakmak için boş bırakın) ", + "user-search": "Kullanıcı Ara", + "user-search-results-per-page": "Görüntülenecek sonuç sayısı", + "default-user-settings": "Varsayılan Kullanıcı Ayarları", + "show-email": "E-posta Göster", + "show-fullname": "Tam adı göster", + "restrict-chat": "Sadece takip ettiğim kişilerden sohbetleri kabul et", + "outgoing-new-tab": "Dışarı giden bağlantıları yeni sekmede aç", + "topic-search": "Konu içi aramayı etkinleştir", + "update-url-with-post-index": "Sayfayı okurken URL bağlantısındaki ileti numarasını güncelle", + "digest-freq": "Özet e-postalarına abone ol", + "digest-freq.off": "Kapalı", + "digest-freq.daily": "Günlük", + "digest-freq.weekly": "Haftalık", + "digest-freq.biweekly": "İki haftada bir", + "digest-freq.monthly": "Aylık", + "email-chat-notifs": "Çevrimiçi değilken gelen mesajları e-posta olarak gönder", + "email-post-notif": "Abone olduğum konulara cevap gelince bana e-posta gönder", + "follow-created-topics": "Kendi konularımı takip et", + "follow-replied-topics": "Cevap verdiğim konuları takip et", + "default-notification-settings": "Varsayılan bildirim ayarları", + "categoryWatchState": "Varsayılan kategori izlenme durumu", + "categoryWatchState.watching": "Takip ediliyor", + "categoryWatchState.notwatching": "Takip edilmiyor", + "categoryWatchState.ignoring": "Yok sayılıyor" +} diff --git a/public/language/tr/admin/settings/web-crawler.json b/public/language/tr/admin/settings/web-crawler.json new file mode 100644 index 0000000000..a3e94189ca --- /dev/null +++ b/public/language/tr/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Taranabilirlik Ayarları", + "robots-txt": "Özel Robots.txtVarsayılan olarak bırakmak için boş bırak", + "sitemap-feed-settings": "Site Haritası & Besleyici Ayarları", + "disable-rss-feeds": "RSS Besleyicilerini Devre dışı bırak", + "disable-sitemap-xml": "sitemap.xml devre dışı bırak", + "sitemap-topics": "Site Haritası'nda görüntülenecek başlıkların sayısı", + "clear-sitemap-cache": "Site haritası çerezlerini temizle", + "view-sitemap": "Site haritasını gör" +} \ No newline at end of file diff --git a/public/language/tr/category.json b/public/language/tr/category.json new file mode 100644 index 0000000000..2ea2bba0b2 --- /dev/null +++ b/public/language/tr/category.json @@ -0,0 +1,23 @@ +{ + "category": "Kategori", + "subcategories": "Alt kategoriler", + "new_topic_button": "Yeni Başlık", + "guest-login-post": "Giriş Yap", + "no_topics": " Bu kategoride hiç konu yok.
Yeni bir konu oluşturmak istemez misiniz?", + "browsing": "gözden geçiriliyor", + "no_replies": "Kimse yanıtlamadı", + "no_new_posts": "Yeni ileti yok", + "watch": "Takip et", + "ignore": "Yok say", + "watching": "Takip ediliyor", + "not-watching": "Takip edilmiyor", + "ignoring": "Yok sayılıyor", + "watching.description": "Bu kategorideki konuları, okunmamış ve güncel konular arasında göster", + "not-watching.description": "Bu kategorideki konuları, okunmamış konular arasında gösterme; ama güncel konular arasında göster", + "ignoring.description": "Bu kategorideki konuları, okunmamış ve güncel konular arasında gösterme", + "watching.message": "Bu kategori ve alt kategorilerindeki güncellemeleri artık takip ediyorsunuz", + "notwatching.message": "Bu kategori ve alt kategorilerindeki güncellemeleri artık takip etmiyorsunuz", + "ignoring.message": "Bu kategori ve alt kategorilerindeki güncellemeleri artık yok sayıyorsunuz", + "watched-categories": "Takip edilen kategoriler", + "x-more-categories": "%1 daha fazla kategori" +} \ No newline at end of file diff --git a/public/language/tr/email.json b/public/language/tr/email.json new file mode 100644 index 0000000000..6765b0b154 --- /dev/null +++ b/public/language/tr/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Test E-posta", + "password-reset-requested": "Şifre Sıfırlama İstediniz!", + "welcome-to": "%1 'a Hoş Geldiniz!", + "invite": "%1 sizi davet etti", + "greeting_no_name": "Merhaba", + "greeting_with_name": "Merhaba %1", + "email.verify-your-email.subject": "Lütfen e-posta adresinizi doğrulayın", + "email.verify.text1": "E-posta adresinizi değiştirmemizi veya onaylamamızı istediniz", + "email.verify.text2": "Güvenlik amacıyla, kayıtlı e-posta adresini yalnızca sahipliği e-posta yoluyla onaylandıktan sonra değiştirir veya onaylarız. Bunu talep etmediyseniz, herhangi bir işlem yapmanız gerekmez.", + "email.verify.text3": "Bu e-posta adresini onayladığınızda, mevcut e-posta adresinizi bununla (%1) değiştireceğiz.", + "welcome.text1": "Kaydolduğunuz için teşekkürler!", + "welcome.text2": "Hesabınızı aktif hale getirmek için, kaydolduğunuz e-posta adresinin size ait olduğunu onaylamamız gerekiyor.", + "welcome.text3": "Yönetici kayıt olma isteğinizi kabul etti. Kullanıcı adı/şifre ile giriş yapabilirsiniz.", + "welcome.cta": "E-posta adresinizi onaylamak için buraya tıklayın", + "invitation.text1": "%1 sizi %2 ye katılmaya davet etti", + "invitation.text2": "Davetiyelerin %1 gün içinde süresi dolacak.", + "invitation.cta": "Hesap oluşturmak için buraya tıklayın.", + "reset.text1": "Şifrenizi değiştirmek istediğinize dair bir ileti aldık. Eğer böyle bir istek göndermediyseniz, lütfen bu e-postayı görmezden gelin.", + "reset.text2": "Şifre değiştirme işlemine devam etmek için aşağıdaki bağlantıya tıklayın:", + "reset.cta": "Şifrenizi değiştirmek için buraya tıklayın", + "reset.notify.subject": "Şifre başarıyla değiştirildi", + "reset.notify.text1": "Şifrenizin %1 zamanında başarı ile değiştirildiğini bildirmek isteriz.", + "reset.notify.text2": "Bunu siz yetkilendirmediyseniz, lütfen hiç vakit kaybetmeden site yöneticisine bu durumu bildiriniz.", + "digest.latest_topics": "'daki En Güncel Konular", + "digest.top-topics": "%1 tarafından zirve başlıklar", + "digest.popular-topics": "%1 tarafından popüler başlıklar", + "digest.cta": "sitesini ziyaret etmek için buraya tıklayın", + "digest.unsub.info": "Bu e-posta seçtiğiniz ayarlar nedeniyle gönderildi.", + "digest.day": "gün", + "digest.week": "hafta", + "digest.month": "ay", + "digest.subject": "%1 için özet", + "digest.title.day": "Günlük Özet", + "digest.title.week": "Haftalık Özet", + "digest.title.month": "Aylık Özet", + "notif.chat.subject": "%1 kullanıcısından yeni bir mesaj aldınız!", + "notif.chat.cta": "Sohbete devam etmek için buraya tıklayın", + "notif.chat.unsub.info": "Bu bildirim, seçtiğiniz ayarlar nedeniyle gönderilmiştir.", + "notif.post.unsub.info": "Bu yazı bildirimi size abonelik ayarlarınız nedeniyle gönderilmiştir.", + "notif.post.unsub.one-click": "Alternatif olarak şu linke tıklayarak aboneliğinizi sonlandırabilirsiniz: ", + "notif.cta": "Foruma", + "notif.cta-new-reply": "İletiyi Görüntüle", + "notif.cta-new-chat": "Sohbeti Görüntüle", + "notif.test.short": "Bildirim Testi", + "notif.test.long": "Bu, bir bildirim epostası testidir. ", + "test.text1": "Bu ileti NodeBB e-posta ayarlarınızın doğru çalışıp çalışmadığını kontrol etmek için gönderildi.", + "unsub.cta": "Buraya tıklayarak ayarlarınızı değiştirebilirsiniz.", + "unsubscribe": "abonelikten çık", + "unsub.success": "Artık %1 eposta listesinden eposta almayacaksınız.", + "unsub.failure.title": "Abonelikten çıkarılamaz", + "unsub.failure.message": "Maalesef bağlantı linkiyle ilgili bir problemden ötürü abonelikten çıkarılamadınız. Fakat, eposta tercihlerinizi şu bölüme giderek değiştirebillirsiniz: Kullanıcı ayarları.

(hata: %1)", + "banned.subject": "%1 sitesinden yasaklandınız!", + "banned.text1": "%1 kullanıcısı %2 sitesinden yasaklandı.", + "banned.text2": "Bu yasak %1 tarihine kadar sürecek.", + "banned.text3": "Yasaklanmanın nedeni:", + "closing": "Teşekkürler!" +} \ No newline at end of file diff --git a/public/language/tr/error.json b/public/language/tr/error.json new file mode 100644 index 0000000000..3b4fb00e45 --- /dev/null +++ b/public/language/tr/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Geçersiz Veri", + "invalid-json": "Geçersiz JSON", + "wrong-parameter-type": "\"%1\" özelliği için %3 türünde bir değer bekleniyordu, ancak bunun yerine %2 alındı", + "required-parameters-missing": "Bu API çağrısında gerekli parametreler eksikti: %1", + "not-logged-in": "Giriş yapmamış görünüyorsunuz.", + "account-locked": "Hesabınız geçici olarak kilitlendi", + "search-requires-login": "Arama yapmak için üyelik hesabı gerekiyor. Lütfen giriş yapın ya da kaydolun.", + "goback": "Bir önceki sayfaya dönmek için geri tuşuna basın", + "invalid-cid": "Geçersiz Kategori ID", + "invalid-tid": "Geçersiz Konu ID", + "invalid-pid": "Geçersiz İleti ID", + "invalid-uid": "Geçersiz Kullanıcı ID", + "invalid-mid": "Geçersiz Sohbet Başlığı", + "invalid-date": "Geçerli bir tarih girmelisiniz.", + "invalid-username": "Geçersiz Kullanıcı İsmi", + "invalid-email": "Geçersiz E-posta", + "invalid-fullname": "Hatalı İsim", + "invalid-location": "Hatalı Konum", + "invalid-birthday": "Hatalı Doğumgünü", + "invalid-title": "Geçersiz Başlık", + "invalid-user-data": "Geçersiz Kullanıcı Verisi", + "invalid-password": "Geçersiz Şifre", + "invalid-login-credentials": "Geçersiz giriş bilgileri", + "invalid-username-or-password": "Lütfen kullanıcı ismi ve şifre girin.", + "invalid-search-term": "Geçersiz arama sözcüğü", + "invalid-url": "Geçersiz bağlantı", + "invalid-event": "Geçersiz Aktivite: %1", + "local-login-disabled": "Ayrıcalıklı-olmayan hesaplar için yerel giriş sistemi devre dışı bırakıldı.", + "csrf-invalid": "Büyük olasılıkla süresi dolmuş oturum nedeniyle girişinizi geçersiz kıldık. Lütfen tekrar deneyiniz.", + "invalid-path": "Geçersiz yol", + "folder-exists": "Dosya mevcut", + "invalid-pagination-value": "Geçersiz sayfa numarası girdiniz, en az %1 ve en fazla %2 olabilir", + "username-taken": "Kullanıcı İsmi Alınmış", + "email-taken": "E-posta Alınmış", + "email-nochange": "Girdiğiniz e-posta var olan e-posta ile aynı", + "email-invited": "E-posta halihazırda davet edilmiş", + "email-not-confirmed": "Ancak e-postanız onaylandıktan sonra bazı kategorilere veya konulara ileti gönderebilirsiniz; lütfen bir onay e-postası almak için buraya tıklayın.", + "email-not-confirmed-chat": "E-postanız onaylanana kadar sohbet edemezsiniz, onaylamak için lütfen buraya tıklayın.", + "email-not-confirmed-email-sent": "E-postanız henüz onaylanmadı, lütfen onay e-postası için gelen kutunuzu kontrol edin. E-postanız onaylanana kadar bazı kategorilerde gönderi paylaşamayabilir veya sohbet edemeyebilirsiniz.", + "no-email-to-confirm": "Hesabınızda bir e-posta grubu yok. Hesap kurtarma için bir e-posta gereklidir ve bazı kategorilerde sohbet etmek ve gönderi paylaşmak için gerekli olabilir. Bir e-posta girmek için lütfen burayı tıklayın.", + "user-doesnt-have-email": "\"%1\" kullanıcısı bir e-posta belirlememiş.", + "email-confirm-failed": "E-posta adresinizi doğrulayamıyoruz. Lütfen daha sonra tekrar deneyin.", + "confirm-email-already-sent": "E-posta onayı zaten gönderilmiş, yeni bir onay göndermek için lütfen %1 dakika bekleyin.", + "sendmail-not-found": "Sendmail yürütülemedi, lüften indirildiğinden ve NodeBB kullanıcısı tarafından uygulanabilir olduğundan emin olun.", + "digest-not-enabled": "Bu kullanıcı özet e-postalarını etkinleştirmemiş veya sistem varsayılanı özet e-postası göndermek için ayarlanmamış", + "username-too-short": "Kullanıcı ismi çok kısa", + "username-too-long": "Kullanıcı ismi çok uzun", + "password-too-long": "Şifre çok uzun", + "reset-rate-limited": "Aşırı fazla sayıda şifre sıfırlama isteği (kısıtlayıcı)", + "reset-same-password": "Lütfen şu ankinden farklı bir şifre kullanın", + "user-banned": "Kullanıcı Yasaklı", + "user-banned-reason": "Maalesef, bu hesap yasaklandı (Sebep: %1)", + "user-banned-reason-until": "Maalesef, bu hesap %1 tarihine kadar yasaklandı (Sebep: %2)", + "user-too-new": "Özür dileriz, ilk iletinizi göndermeden önce %1 saniye beklemeniz gerekiyor!", + "blacklisted-ip": "Üzgünüz, IP adresiniz bu forumda yasaklandı. Bunun bir hata olduğunu düşünüyorsanız bir yönetici ile irtibata geçiniz.", + "ban-expiry-missing": "Bu yasak için bir bitiş tarihi girin", + "no-category": "Kategori Yok", + "no-topic": "Konu Yok", + "no-post": "İleti Yok", + "no-group": "Grup Yok", + "no-user": "Kullanıcı Yok", + "no-teaser": "İleti Yok", + "no-flag": "Şikayet Yok", + "no-privileges": "Bu işlemi yapmak için yeterli yetkiniz yok.", + "category-disabled": "Kategori aktif değil", + "topic-locked": "Başlık Kilitli", + "post-edit-duration-expired": "Gönderilen iletiler %1 saniyeden sonra değiştirilemez", + "post-edit-duration-expired-minutes": "Gönderilen iletiler %1 dakikadan sonra değiştirilemez", + "post-edit-duration-expired-minutes-seconds": "Gönderilen iletiler %1 dakika ve %2 saniyeden sonra değiştirilemez", + "post-edit-duration-expired-hours": "Gönderilen iletiler %1 saatten sonra değiştirilemez", + "post-edit-duration-expired-hours-minutes": "Gönderilen iletiler %1 saat ve %2 dakikadan sonra değiştirilemez", + "post-edit-duration-expired-days": "Gönderilen iletiler %1 günden sonra değiştirilemez", + "post-edit-duration-expired-days-hours": "Gönderilen iletiler %1 gün ve %2 saatten sonra değiştirilemez", + "post-delete-duration-expired": "Gönderilen iletiler %1 saniyeden sonra silinemez", + "post-delete-duration-expired-minutes": "Gönderilen iletiler %1 dakikadan sonra silinemez", + "post-delete-duration-expired-minutes-seconds": "Gönderilen iletiler %1 dakika ve %2 saniyeden sonra silinemez", + "post-delete-duration-expired-hours": "Gönderilen iletiler %1 saatten sonra silinemez", + "post-delete-duration-expired-hours-minutes": "Gönderilen iletiler %1 saat ve %2 dakikadan sonra silinemez", + "post-delete-duration-expired-days": "Gönderilen iletiler %1 günden sonra silinemez", + "post-delete-duration-expired-days-hours": "Gönderilen iletiler %1 gün ve %2 saatten sonra silinemez", + "cant-delete-topic-has-reply": "Bir başlığı 1 ileti girildikten sonra silemezsiniz!", + "cant-delete-topic-has-replies": "Bir başlığı %1 ileti girildikten sonra silemezsiniz!", + "content-too-short": "Lütfen daha uzun bir ileti girin. İletiler en az %1 karakterden oluşmalı.", + "content-too-long": "Lütfen daha kısa bir ileti girin. İletiler %1 karakterden uzun olamaz.", + "title-too-short": "Lütfen daha uzun bir başlık girin. Başlıklar en az %1 karakter içermelidir.", + "title-too-long": "Lütfen daha kısa bir başlık girin. Başlıklar %1 karakterden uzun olamaz.", + "category-not-selected": "Kategori bulunamadı. Lütfen bir kategori seçiniz. ", + "too-many-posts": "%1 saniye içinde yalnızca bir ileti gönderebilirsiniz - lütfen tekrar ileti göndermeden önce bekleyiniz.", + "too-many-posts-newbie": "Yeni bir kullanıcı olarak, %2 saygınlık puanı kazanana kadar %1 saniye içinde bir ileti gönderebilirsiniz - lütfen tekrar ileti göndermeden önce bekleyiniz.", + "already-posting": "You are already posting", + "tag-too-short": "Lütfen daha uzun bir etiket girin. Etiketler en az %1 karakter içermelidir.", + "tag-too-long": "Lütfen daha kısa bir etiket girin. Etiketler %1 karakterden uzun olamaz.", + "not-enough-tags": "Yeterince etiket yok. Başlılar en az %1 etikete sahip olmalıdır", + "too-many-tags": "Etiket sayısı çok fazla. Başlıklar en fazla %1 etikete sahip olabilir", + "cant-use-system-tag": "Bu sistem etiketini kullanamazsınız.", + "cant-remove-system-tag": "Bu sistem etiketini kaldıramazsınız.", + "still-uploading": "Lütfen yüklemelerin bitmesini bekleyin.", + "file-too-big": "İzin verilen en büyük dosya boyutu %1 kb - lütfen daha küçük bir dosya yükleyiniz", + "guest-upload-disabled": "Ziyaretçilerin yükleme yapması devre dışı bırakıldı", + "cors-error": "Yanlış yapılandırılmış CORS nedeniyle resim yüklenemiyor", + "upload-ratelimit-reached": "Tek seferde çok sayıda dosya yüklediniz. Lütfen daha sonra tekrar deneyin.", + "scheduling-to-past": "Lütfen gelecekte bir tarih seçiniz.", + "invalid-schedule-date": "Lütfen geçerli bir tarih ve saat seçiniz.", + "cant-pin-scheduled": "Zamanlanmış konular sabitlenemez veya sabitliği kaldırılamaz. ", + "cant-merge-scheduled": "Zamanlanmış konular birleştirilemez.", + "cant-move-posts-to-scheduled": "İletileri zamanlanmış bir konuya taşıyamazsınız!", + "cant-move-from-scheduled-to-existing": "İletileri zamanlanmış bir konudan aktif olan bir konuya taşıyamazsınız!", + "already-bookmarked": "Bu iletiyi zaten yer imlerinize eklemişsiniz.", + "already-unbookmarked": "Bu iletiyi zaten yer imlerinizden çıkarmışsınız.", + "cant-ban-other-admins": "Başka yöneticileri yasaklayamazsınız!", + "cant-mute-other-admins": "Diğer yöneticileri susturamazsınız!", + "user-muted-for-hours": "Susturuldunuz, %1 saat sonra yeniden ileti gönderebileceksiniz. ", + "user-muted-for-minutes": "Susturuldunuz, %1 dakika sonra yeniden ileti gönderebileceksiniz. ", + "cant-make-banned-users-admin": "Yasaklanmış üyeleri yönetici yapamazsınız.", + "cant-remove-last-admin": "Tek yönetici sizsiniz. Kendinizi adminlikten çıkarmadan önce başka bir kullanıcıyı admin olarak ekleyiniz", + "account-deletion-disabled": "Hesap silme devre dışı bırakılmış", + "cant-delete-admin": "Bu hesabı kaldırmadan önce yönetici izinlerini kaldırmanız gerekiyor.", + "already-deleting": "Halihazırda siliniyor", + "invalid-image": "Geçersiz görsel", + "invalid-image-type": "Geçersiz görsel uzantısı. Izin verilen uzantılar: %1", + "invalid-image-extension": "Geçersiz görsel uzantısı", + "invalid-file-type": "Geçersiz dosya türü. İzin verilen uzantılar: %1", + "invalid-image-dimensions": "Görsel boyutları çok büyük", + "group-name-too-short": "Grup ismi çok kısa", + "group-name-too-long": "Grup adı çok uzun", + "group-already-exists": "Grup zaten var", + "group-name-change-not-allowed": "Grup ismini değiştiremezsiniz", + "group-already-member": "Bu grubun zaten bir parçasısın.", + "group-not-member": "Bu grubun bir üyesi yok", + "group-needs-owner": "Bu grubu en az bir kişi sahiplenmesi gerekiyor", + "group-already-invited": "Bu kullanıcı zaten davet edilmiş", + "group-already-requested": "Üyelik isteğiniz zaten gönderildi", + "group-join-disabled": "Gruba şu anda katılamazsınız!", + "group-leave-disabled": "Grubu şu anda terk edemezsiniz! ", + "post-already-deleted": "İleti zaten silinmiş", + "post-already-restored": "İleti zaten geri getirilmiş", + "topic-already-deleted": "Başlık zaten silinmiş", + "topic-already-restored": "Başlık zaten geri getirilmiş", + "cant-purge-main-post": "İlk iletiyi silemezsiniz, bunun yerine konuyu silin", + "topic-thumbnails-are-disabled": "Başlık resimleri kapalı.", + "invalid-file": "Geçersiz Dosya", + "uploads-are-disabled": "Yüklemeler kapalı", + "signature-too-long": "Maalesef imzanız %1 karakterden uzun olamaz.", + "about-me-too-long": "Hakkınızda yazdıklarınız en fazla %1 karakter olabilir.", + "cant-chat-with-yourself": "Kendinizle sohbet edemezsiniz!", + "chat-restricted": "Bu kullanıcı sohbet ayarlarını kısıtlamış. Bu kişiye mesaj gönderebilmeniz için sizi takip etmeleri gerekiyor", + "chat-disabled": "Sohbet özelliği kapalı", + "too-many-messages": "Ardı ardına çok fazla mesaj yolladınız, lütfen biraz bekleyiniz.", + "invalid-chat-message": "Geçersiz sohbet mesajı", + "chat-message-too-long": "Sohbet mesajı %1 karakterden daha uzun olamaz.", + "cant-edit-chat-message": "Bu mesajı düzenlemek için izin verilmez", + "cant-delete-chat-message": "Bu mesajı silmek için izin verilmez", + "chat-edit-duration-expired": "Gönderildikten sonra yalnızca %1 saniye mesajı(ları) düzenlemene izin verilir", + "chat-delete-duration-expired": "Gönderildikten sonra yalnızca %1 saniye mesajı(ları) silmene izin verilir", + "chat-deleted-already": "Bu sohbet mesajı zaten silinmiş.", + "chat-restored-already": "Bu sohbet mesajı zaten geri yüklendi.", + "chat-room-does-not-exist": "Sohbet Odası Mevcut Değil", + "already-voting-for-this-post": "Bu gönderi için zaten oy verdin.", + "reputation-system-disabled": "İtibar sistemi devre dışı.", + "downvoting-disabled": "Eksi oylama devre dışı bırakılmış. ", + "not-enough-reputation-to-chat": "Özel Sohbet için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-to-upvote": "Artı oy verebilmek için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-to-downvote": "Eksi oy verebilmek için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-to-flag": "Bu iletiyi şikayet etmek için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-min-rep-website": "Websitesi ekleyebilmek için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-min-rep-aboutme": "Hakkınızda bilgi ekleyebilmek için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-min-rep-signature": "İmza ekleyebilmek için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-min-rep-profile-picture": "Profil fotosu ekleyebilmek için en az %1 saygınlık puanına sahip olmalısınız.", + "not-enough-reputation-min-rep-cover-picture": "Kapak görseli ekleyebilmek için en az %1 saygınlık puanına sahip olmalısınız.", + "post-already-flagged": "Bu iletiyi önceden şikayet etmişsiniz.", + "user-already-flagged": "Bu kullanıcıyı önceden şikayet etmişsiniz.", + "post-flagged-too-many-times": "Bu ileti başkaları tarafından halihazırda şikayet edilmiş.", + "user-flagged-too-many-times": "Bu kullanıcı başkaları tarafından halihazırda şikayet edilmiş.", + "cant-flag-privileged": "Yöneticilerin profillerini veya içeriklerini bayraklayamazsınız.", + "self-vote": "Kendi iletinize oy veremezsiniz", + "too-many-upvotes-today": "Bir günde sadece %1 artı oy verebilirsiniz", + "too-many-upvotes-today-user": "Bir kullanıcıya bir günde sadece %1 artı oy verebilirsiniz", + "too-many-downvotes-today": "Bir günde sadece %1 eksi oy verebilirsiniz", + "too-many-downvotes-today-user": "Bir kullanıcıya bir günde sadece %1 eksi oy verebilirsiniz", + "reload-failed": "NodeBB tekrar yüklenirken bir sorunla karşılaştı: “%1“. NodeBB varolan dosyaları servis etmeye devam edecek.", + "registration-error": "Kayıt Hatası", + "parse-error": "Sunucu yanıtı çözümlemesi sırasında bir şeyler ters gitti", + "wrong-login-type-email": "Lütfen giriş için e-posta adresinizi kullanın", + "wrong-login-type-username": "Lütfen giriş için kullanıcı adınızı kullanın", + "sso-registration-disabled": "%1 hesap için kayıt işlemi devre dışı bırakıldı, lütfen öncelikle bir eposta adresi ile kayıt olun", + "sso-multiple-association": "Bu hizmetten birden fazla hesabı, NodeBB hesabınızla ilişkilendiremezsiniz. Lütfen mevcut hesabınızı ayırın ve tekrar deneyin.", + "invite-maximum-met": "Sen maksimum miktarda insanı davet ettin (%2 üzerinden %1).", + "no-session-found": "Giriş yapılmış bir oturum bulunamadı!", + "not-in-room": "Odada kullanıcı yok", + "cant-kick-self": "Kendinizi gruptan atamazsınız.", + "no-users-selected": "Seçili kullanıcı(lar) bulunamadı", + "invalid-home-page-route": "Geçersiz anasayfa yolu", + "invalid-session": "Geçersiz Oturum", + "invalid-session-text": "Giriş oturumunuz aktif görünmüyor. Lütfen sayfayı yenileyiniz.", + "session-mismatch": "Oturum Uyuşmazlığı", + "session-mismatch-text": "Giriş oturumunuz sunucu ile eşleşmiyor. Lütfen sayfayı yenileyiniz.", + "no-topics-selected": "Hiçbir başlık seçilmedi!", + "cant-move-to-same-topic": "İletiyi aynı başlığa taşıyamazsın!", + "cant-move-topic-to-same-category": "Başlığı bulunduğu kategoriye taşıyamazsınız!", + "cannot-block-self": "Kendi kendinizi engelleyemezsiniz!", + "cannot-block-privileged": "Yöneticileri veya genel moderatörleri engelleyemezsiniz", + "cannot-block-guest": "Misafir diğer kullanıcıları engelleyemez", + "already-blocked": "Bu kullanıcı zaten engellendi", + "already-unblocked": "Bu kullanıcı zaten engellenmedi", + "no-connection": "İnternet bağlantınızda sorun var gibi görünüyor", + "socket-reconnect-failed": "Şu anda sunucuya ulaşılamıyor. Tekrar denemek için buraya tıklayın, veya daha sonra tekrar deneyin.", + "plugin-not-whitelisted": "– eklentisi yüklenemedi, sadece NodeBB Paket Yöneticisi tarafından onaylanan eklentiler kontrol panelinden kurulabilir", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Konu aktivitesi '%1' tanımlanamadı", + "cant-set-child-as-parent": "Alt-kategoriyi üst kategori olarak ayarlayamazsınız!", + "cant-set-self-as-parent": "Kendisini üst kategori olarak ayarlayamazsınız!", + "api.master-token-no-uid": "İsteğe karşılık gelen bir \"_uid\" olmadan bir ana belirteç alındı", + "api.400": "İlettiğiniz istekle ilgili bir sorun vardı.", + "api.401": "Geçerli bir giriş oturumu bulunamadı. Lütfen yeniden giriş yapıp tekrar deneyin.", + "api.403": "Bu aramayı yapmak için yetkiniz yok", + "api.404": "Geçersiz API çağrısı", + "api.426": "Yazma API'sine yapılan istekler için HTTPS gereklidir, lütfen isteğinizi HTTPS aracılığıyla yeniden gönderin", + "api.429": "Fazla sayıda istekte bulundunuz, lütfen daha sonra tekrar deneyiniz.", + "api.500": "İsteğinizi gerçekleştirmeye çalışırken beklenmeyen bir hata ile karşılaşıldı.", + "api.501": "Aramaya çalıştığınız rota henüz uygulanmadı, lütfen yarın tekrar deneyin", + "api.503": "Aramaya çalıştığınız rota sunucu yapılandırması nedeniyle şu anda kullanılamıyor" +} \ No newline at end of file diff --git a/public/language/tr/flags.json b/public/language/tr/flags.json new file mode 100644 index 0000000000..8ad12f9c63 --- /dev/null +++ b/public/language/tr/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Durum", + "reports": "Raporlar", + "first-reported": "İlk rapor tarihi", + "no-flags": "Yaşasın! Hiçbir şikayet bulunamadı.", + "assignee": "Vekil", + "update": "Güncelle", + "updated": "Güncellendi", + "resolved": "Çözüldü", + "target-purged": "Şikayet edilen içerik temizlendi ve artık mevcut değil.", + + "graph-label": "Günlük Şikayetler", + "quick-filters": "Hızlı Filtre", + "filter-active": "Şikayetler listesinde etkin olan bir veya daha fazla filtre var", + "filter-reset": "Filtreleri Kaldır", + "filters": "Filtre Ayarları", + "filter-reporterId": "Muhabir UID", + "filter-targetUid": "Şikayet UID", + "filter-type": "Şikayet Tipi", + "filter-type-all": "Bütün İçerik", + "filter-type-post": "İleti", + "filter-type-user": "Kullanıcı", + "filter-state": "Durum", + "filter-assignee": "Vekil UID", + "filter-cid": "Kategori", + "filter-quick-mine": "Kendime atananlar", + "filter-cid-all": "Tüm kategoriler", + "apply-filters": "Filtreleri Uygula", + "more-filters": "Daha Fazla Filtre", + "fewer-filters": "Daha Az Filtre", + + "quick-actions": "Hızlı Eylemler", + "flagged-user": "Şikayet Edilen Kullanıcı", + "view-profile": "Profili Gör", + "start-new-chat": "Yeni Sohbet Başlat", + "go-to-target": "Şikayet Edilen İçeriği Gör", + "assign-to-me": "Kendime ata", + "delete-post": "İletiyi Sil", + "purge-post": "İletiyi Temizle", + "restore-post": "İletiyi Geri Getir", + "delete": "Şikayeti Sil", + + "user-view": "Profili Gör", + "user-edit": "Profili Düzenle", + + "notes": "Şikayet Notları", + "add-note": "Not Ekle", + "no-notes": "Not paylaşılmadı", + "delete-note-confirm": "Bu şikayet notunu silmek istediğinize emin misiniz?", + "delete-flag-confirm": "Bu şikayeti silmek istediğinize emin misiniz?", + "note-added": "Not eklendi", + "note-deleted": "Not silindi", + "flag-deleted": "Şikayet Silindi", + + "history": "Hesap & Şikayet Geçmişi", + "no-history": "Şikayet geçmişi yok", + + "state-all": "Tüm Durumlar", + "state-open": "Yeni/Açık", + "state-wip": "Çözüm Aşamasında", + "state-resolved": "Çözüldü", + "state-rejected": "Reddedildi", + "no-assignee": "Atanmadı", + + "sort": "Sırala", + "sort-newest": "En yenisi önce", + "sort-oldest": "En eskisi önce", + "sort-reports": "En çok raporlanan", + "sort-all": "Tüm şikayet türleri...", + "sort-posts-only": "Sadece iletiler...", + "sort-downvotes": "En çok eksilenen", + "sort-upvotes": "En çok artılanan", + "sort-replies": "En çok cevap verilen", + + "modal-title": "İçeriği Şikayet Et", + "modal-body": "%1 %2 için şikayet nedenini belirtiniz. Alternatif olarak hızlı rapor butonlarından birini kullanabilirsiniz.", + "modal-reason-spam": "Gereksiz", + "modal-reason-offensive": "Saldırgan", + "modal-reason-other": "Diğer (aşağıda belirtin)", + "modal-reason-custom": "Bu içeriği rapor etme nedeni...", + "modal-submit": "Raporu Gönder", + "modal-submit-success": "İçerik, denetlemesi için şikayet edildi.", + + "bulk-actions": "Toplu Aksiyonlar", + "bulk-resolve": "Şikayetleri Çözümle", + "bulk-success": "%1 şikayet güncellendi", + "flagged-timeago-readable": "Şikayet Edilme Zamanı (%2)", + "auto-flagged": "[Otomatik Şikayet] %1 tane eksi oy aldı." +} \ No newline at end of file diff --git a/public/language/tr/global.json b/public/language/tr/global.json new file mode 100644 index 0000000000..ec24809b05 --- /dev/null +++ b/public/language/tr/global.json @@ -0,0 +1,126 @@ +{ + "home": "Ana Sayfa", + "search": "Arama", + "buttons.close": "Kapat", + "403.title": "Erişim Engellendi", + "403.message": "Erişim izniniz olmayan bir sayfaya denk gelmiş gibisiniz.", + "403.login": "Belki tekrar giriş yapmalısınız?", + "404.title": "Bulunamadı", + "404.message": "Erişim izniniz olmayan bir sayfaya denk gelmiş gibisiniz. Anasayfa'ya geri dönün.", + "500.title": "Dahili hata.", + "500.message": "Ups! Bir şeyler ters gitti sanki!", + "400.title": "Geçersiz istek.", + "400.message": "Bu bağlantı bozuk gibi gözüküyor, lütfen bir kez daha kontrol edin. Aksi taktirde anasayfaya geri dönün.", + "register": "Kayıt Ol", + "login": "Giriş", + "please_log_in": "Lütfen Giriş Yapınız", + "logout": "Çıkış", + "posting_restriction_info": "İleti gönderme sadece kayıtlı kullanıcılar içindir, giriş yapmak için buraya tıklayın.", + "welcome_back": "Tekrar Hoş Geldiniz", + "you_have_successfully_logged_in": "Başarıyla giriş yaptınız!", + "save_changes": "Değişiklikleri Kaydet", + "save": "Kaydet", + "close": "Kapat", + "pagination": "Sayfalara numara koyma", + "pagination.out_of": "%1 - %2", + "pagination.enter_index": "İleti dizinine git", + "header.admin": "Yönetim", + "header.categories": "Kategoriler", + "header.recent": "Güncel", + "header.unread": "Okunmamış", + "header.tags": "Etiketler", + "header.popular": "Popüler", + "header.top": "Zirve", + "header.users": "Kullanıcılar", + "header.groups": "Gruplar", + "header.chats": "Sohbetler", + "header.notifications": "Bildirimler", + "header.search": "Arama", + "header.profile": "Profil", + "header.navigation": "Navigasyon", + "notifications.loading": "Bildirimler Yükleniyor", + "chats.loading": "Sohbetler Yükleniyor", + "motd.welcome": "NodeBB, geleceğin tartışma platformuna hoş geldiniz.", + "previouspage": "Önceki Sayfa", + "nextpage": "Sonraki Sayfa", + "alert.success": "Başarılı", + "alert.error": "Hata", + "alert.banned": "Yasaklı", + "alert.banned.message": "Yasaklandınız, erişiminiz kısıtlanmıştır. ", + "alert.unbanned": "Yasak kaldırıldı", + "alert.unbanned.message": "Yasağınız kaldırıldı.", + "alert.unfollow": "Artık %1'i takip etmiyorsunuz!", + "alert.follow": "%1'i takip ediyorsunuz!", + "users": "Kullanıcı", + "topics": "Konu", + "posts": "İleti", + "x-posts": "%1 ileti", + "best": "En İyi", + "controversial": "Tartışmalı", + "votes": "Oy", + "x-votes": "%1 oy", + "voters": "Oy Verenler", + "upvoters": "Artı Oy Verenler", + "upvoted": "Artı Oylar", + "downvoters": "Eksi Oy Verenler", + "downvoted": "Eksi Oylar", + "views": "Bakış", + "posters": "Yayımlayıcılar", + "reputation": "İtibar", + "lastpost": "Son ileti", + "firstpost": "İlk ileti", + "read_more": "daha fazla oku", + "more": "Daha Fazla", + "none": "Hiçbiri", + "posted_ago_by_guest": "Ziyaretçi tarafından %1 yayımlandı", + "posted_ago_by": "%2 tarafından %1 yayımlandı", + "posted_ago": "%1 yayımlandı", + "posted_in": "%1 içinde yayımlandı", + "posted_in_by": "%2 tarafından %1 içinde yayımlandı", + "posted_in_ago": "%1 içinde %2 yayımlandı", + "posted_in_ago_by": "%1 içinde %3 tarafından %2 yayımlandı", + "user_posted_ago": "%1 %2 yayımladı", + "guest_posted_ago": "Ziyaretçi %1 yayımladı", + "last_edited_by": "Son düzenleyen: %1", + "norecentposts": "Güncel İleti Yok", + "norecenttopics": "Güncel Konu Yok", + "recentposts": "Güncel İletiler", + "recentips": "Güncel giriş yapilan IP adresleri", + "moderator_tools": "Moderasyon Araçları", + "online": "Çevrimiçi", + "away": "Dışarıda", + "dnd": "Rahatsız etme", + "invisible": "Görünmez", + "offline": "Çevrimdışı", + "email": "E-posta", + "language": "Dil", + "guest": "Ziyaretçi", + "guests": "Ziyaretçiler", + "former_user": "Eski Kullanıcı", + "system-user": "Sistem", + "unknown-user": "Bilinmeyen kullanıcı", + "updated.title": "Forum Güncellendi", + "updated.message": "Bu forum şu anda güncellendi. Sayfayı tekrar yüklemek için buraya tıklayın.", + "privacy": "Gizlilik", + "follow": "Takip et", + "unfollow": "Takip etmeyi bırak", + "delete_all": "Hepsini Sil", + "map": "Harita", + "sessions": "Giriş Oturumları", + "ip_address": "IP Adresleri", + "enter_page_number": "Sayfa numarasını girin", + "upload_file": "Dosya yükle", + "upload": "Yükle", + "uploads": "Yüklemeler", + "allowed-file-types": "İzin verilen dosya tipleri %1", + "unsaved-changes": "Kaydedilmemiş değişiklikler var. Çıkmak istediğinize emin misiniz?", + "reconnecting-message": "%1 ile bağlantınız koptu, yeniden bağlanmak için lütfen bekleyiniz.", + "play": "Oynat", + "cookies.message": "Bu web sitesi en iyi deneyimi elde etmeniz amacıyla çerezlerden yararlanır.", + "cookies.accept": "Anladım!", + "cookies.learn_more": "Daha Fazla", + "edited": "Düzenlendi", + "disabled": "Devre dışı", + "select": "Seç", + "user-search-prompt": "Kullanıcı bulmak için buraya yazın ..." +} \ No newline at end of file diff --git a/public/language/tr/groups.json b/public/language/tr/groups.json new file mode 100644 index 0000000000..0dd72078ae --- /dev/null +++ b/public/language/tr/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Gruplar", + "view_group": "Grubu Gör", + "owner": "Grup Kurucusu", + "new_group": "Yeni Grup Oluştur", + "no_groups_found": "Henüz hiç grup yok", + "pending.accept": "Onayla", + "pending.reject": "Reddet", + "pending.accept_all": "Hepsini Kabul Et", + "pending.reject_all": "Hepsini Reddet", + "pending.none": "Şu anda bekleyen üye yok", + "invited.none": "Şu anda davet edilmiş üye yok", + "invited.uninvite": "Daveti iptal et", + "invited.search": "Gruba davet etmek için kullanıcı ara", + "invited.notification_title": "%1 grubuna katılmaya davet edildiniz", + "request.notification_title": "%1 grup daveti gönderdi", + "request.notification_text": "%1 , %2 grubuna katılmak istiyor", + "cover-save": "Kaydet", + "cover-saving": "Kaydediliyor", + "details.title": "Grup Detayları", + "details.members": "Üye Listesi", + "details.pending": "Üyeler bekleniyor", + "details.invited": "Davet Edilen Üyeler", + "details.has_no_posts": "Bu grubun üyeleri henüz bir ileti göndermedi.", + "details.latest_posts": "En son iletiler", + "details.private": "Özel", + "details.disableJoinRequests": "Katılma isteklerini devre dışı bırak", + "details.disableLeave": "Üyelerin gruptan ayrılmasını yasakla", + "details.grant": "Grup Sahibi Yap/Kaldır", + "details.kick": "Dışarı at", + "details.kick_confirm": "Bu üyeyi bu gruptan silmek istediğinize emin misiniz?", + "details.add-member": "Üye Ekle", + "details.owner_options": "Grup Yöneticisi", + "details.group_name": "Grup ismi", + "details.member_count": "Üye Sayısı", + "details.creation_date": "Oluşturulma Tarihi", + "details.description": "Tanımlama", + "details.member-post-cids": "İletilerin gösterileceği kategori ID'leri", + "details.badge_preview": "Rozet Önizlemesi", + "details.change_icon": "İkonu Değiştir", + "details.change_label_colour": "Etiket Rengini Değiştir", + "details.change_text_colour": "Yazı Rengini Değiştir", + "details.badge_text": "Rozet Yazısı", + "details.userTitleEnabled": "Rozeti Göster", + "details.private_help": "Gruba katılmak için eğer etkinse grup sahibini onayı gerekir, ", + "details.hidden": "Gizli", + "details.hidden_help": "Bu grup eğer etkinse grup listelerinde bulunmaz, ve kullanıcılar bizzat davet eder", + "details.delete_group": "Grubu Sil", + "details.private_system_help": "Özel gruplar sistem seviyesinde devre dışı bırakıldı. Bu seçenek hiçbir şeyi değiştirmeyecek.", + "event.updated": "Grup detayları güncellenmiştir", + "event.deleted": "\"%1\" grubu silinmiş", + "membership.accept-invitation": "Daveti Kabul Et", + "membership.accept.notification_title": " %1 grubunun üyesi oldunuz!", + "membership.invitation-pending": "Davet beklemede", + "membership.join-group": "Gruba Katıl", + "membership.leave-group": "Gruptan Ayrıl", + "membership.leave.notification_title": "%1 kullanıcısı şu gruptan ayrıldı: %2", + "membership.reject": "Reddet", + "new-group.group_name": "Grup İsmi:", + "upload-group-cover": "Grup kapağı yükle", + "bulk-invite-instructions": "Bu gruba davet etmek için virgülle ayrılmış adlarının bir listesini girin", + "bulk-invite": "Toplu Davet", + "remove_group_cover_confirm": "Kapak görselini silmek istediğinden emin misin?" +} \ No newline at end of file diff --git a/public/language/tr/ip-blacklist.json b/public/language/tr/ip-blacklist.json new file mode 100644 index 0000000000..5f2dd38512 --- /dev/null +++ b/public/language/tr/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "IP kara listenizi buradan yapılandırın.", + "description": "Bazen bir kullanıcı hesabı yasağı caydırıcılık için yeterli değildir. Bazen, bir forumun belirli bir IP'ye veya bir dizi IP'ye kısıtlanması, bir forumu korumanın en iyi yoludur. Bu senaryolarda, zahmetli IP adresleri veya tüm CIDR bloklarını bu kara listeye ekleyebilirsiniz. Bu sayede farklı bir hesapta oturum açılması veya yeni bir hesapla kaydolunması engellenecektir.", + "active-rules": "Aktif Kurallar", + "validate": "Kara Listeyi Onayla", + "apply": "Kara Listeyi Uygula", + "hints": "Syntax İpuçları", + "hint-1": "Her satır için tek bir IP adresi tanımlayın. CIDR formatını izledikleri sürece IP blokları ekleyebilirsiniz (Örn: 192.168.100.0/22).", + "hint-2": "Yorumlara # sembolüyle başlayan satırları ekleyebilirsiniz.", + + "validate.x-valid": "%2 geçerli kuraldan %1 tanesi.", + "validate.x-invalid": "Şu %1 kural geçersiz:", + + "alerts.applied-success": "Kara Liste Uygulandı", + + "analytics.blacklist-hourly": "Şekil 1 Saatlik kara liste isabeti", + "analytics.blacklist-daily": "Şekil 2 Günlük kara liste isabeti", + "ip-banned": "IP yasaklandı" +} \ No newline at end of file diff --git a/public/language/tr/language.json b/public/language/tr/language.json new file mode 100644 index 0000000000..880d9d609f --- /dev/null +++ b/public/language/tr/language.json @@ -0,0 +1,5 @@ +{ + "name": "Türkçe", + "code": "tr", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/tr/login.json b/public/language/tr/login.json new file mode 100644 index 0000000000..40167b5d61 --- /dev/null +++ b/public/language/tr/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Kullanıcı Adı / E-posta Adresi", + "username": "Kullanıcı Adı", + "remember_me": "Beni Hatırla!", + "forgot_password": "Şifrenizi mi unuttunuz?", + "alternative_logins": "Alternatif Girişler", + "failed_login_attempt": "Giriş Başarısız", + "login_successful": "Başarıyla giriş yaptınız!", + "dont_have_account": "Hesabınız yok mu?", + "logged-out-due-to-inactivity": "Hareketsizlik nedeniyle yönetici panelinden çıkış yapıldı", + "caps-lock-enabled": "Caps Lock aktif" +} \ No newline at end of file diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json new file mode 100644 index 0000000000..ab64665303 --- /dev/null +++ b/public/language/tr/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Sohbet", + "chat.placeholder": "Mesajı yazın veya resim sürükleyip bırakın", + "chat.scroll-up-alert": "Eski mesajlara bakıyorsunuz, en yeni mesajları görmek için buraya tıklayınız.", + "chat.send": "Gönder", + "chat.no_active": "Aktif sohbet mevcut değil", + "chat.user_typing": "%1 yazıyor ...", + "chat.user_has_messaged_you": "%1 size bir mesaj gönderdi.", + "chat.see_all": "Bütün Sohbetler", + "chat.mark_all_read": "Hepsini Okundu Olarak İşaretle", + "chat.no-messages": "Lütfen sohbet geçmişini görüntülemek için bir alıcı seçin", + "chat.no-users-in-room": "Bu odada hiç kullanıcı yok", + "chat.recent-chats": "Güncel Sohbetler", + "chat.contacts": "Sohbet Kişileri", + "chat.message-history": "Mesaj Geçmişi", + "chat.message-deleted": "Mesaj Silindi", + "chat.options": "Sohbet Ayarları", + "chat.pop-out": "Sohbeti Pencereye Çevir", + "chat.minimize": "Küçült", + "chat.maximize": "Büyüt", + "chat.seven_days": "7 Gün", + "chat.thirty_days": "30 Gün", + "chat.three_months": "3 Ay", + "chat.delete_message_confirm": "Bu mesajı silmek istediğinizden emin misiniz?", + "chat.retrieving-users": "Kullanıcılar alınıyor ...", + "chat.manage-room": "Sohbet Odasını Yönet", + "chat.add-user-help": "Burada kullanıcılar için arama yapın. Kullanıcı seçildiğinde sohbete eklenecektir. Yeni kullanıcı sohbete eklenmeden önce yazılmış olan sohbet mesajlarını göremeyecektir. Yalnızca oda sahipleri () kullanıcıları sohbet odalarından kaldırabilir.", + "chat.confirm-chat-with-dnd-user": "Bu kullanıcı durumunu rahatsız etmeyin olarak ayarladı. Hala onunla sohbet etmek istiyor musunuz?", + "chat.rename-room": "Odanın ismini değiştir", + "chat.rename-placeholder": "Oda isminizi buraya girin", + "chat.rename-help": "Buradaki oda ismi odadaki tüm katılımcılar tarafından görülebilir.", + "chat.leave": "Sohbetten Ayrıl", + "chat.leave-prompt": "Sohbetten ayrılmak istediğinizden emin misiniz?", + "chat.leave-help": "Bu sohbetten ayrılmak, bu sohbetteki gelecekteki yazışmalardan sizi silecektir. Gelecekte tekrar eklendiyseniz, yeniden katılmadan önce herhangi bir sohbet geçmişi görmezsiniz.", + "chat.in-room": "Bu odada", + "chat.kick": "Dışarı At", + "chat.show-ip": "IP Göster", + "chat.owner": "Oda Sahibi", + "chat.system.user-join": "%1 odaya katıldı", + "chat.system.user-leave": "%1 odadan çıktı", + "chat.system.room-rename": "%2 şu grubun ismini değiştirdi: %1", + "composer.compose": "Yaz", + "composer.show_preview": "Önizleme Göster", + "composer.hide_preview": "Önizleme Sakla", + "composer.user_said_in": "%1, içinde söyledi: %2", + "composer.user_said": "%1 söyledi:", + "composer.discard": "Bu iletiyi iptal etmek istediğinizden emin misiniz?", + "composer.submit_and_lock": "Gönder ve Kilitle", + "composer.toggle_dropdown": "Menü aç", + "composer.uploading": "Yükleniyor %1", + "composer.formatting.bold": "Kalın", + "composer.formatting.italic": "İtalik", + "composer.formatting.list": "Liste", + "composer.formatting.strikethrough": "Üstüçizili", + "composer.formatting.code": "Kod", + "composer.formatting.link": "Bağlantı", + "composer.formatting.picture": "Görsel Linki", + "composer.upload-picture": "Görsel Yükle", + "composer.upload-file": "Dosya Yükle", + "composer.zen_mode": "Tam ekran modu", + "composer.select_category": "Bir kategori seç", + "composer.textarea.placeholder": "iletinizi buraya giriniz, görselleri sürükleyip bırakabilirsiniz...", + "composer.schedule-for": "Konuyu Zamanla", + "composer.schedule-date": "Tarih", + "composer.schedule-time": "Zaman", + "composer.cancel-scheduling": "Zamanlamayı iptal et", + "composer.set-schedule-date": "Tarihi ayarla", + "bootbox.ok": "Kabul", + "bootbox.cancel": "İptal", + "bootbox.confirm": "Onayla", + "bootbox.submit": "Teslim et", + "bootbox.send": "Gönder", + "cover.dragging_title": "Kapak Görseli Konumlandırma", + "cover.dragging_message": "Kapak görselini istediğin pozisyona getir ve kaydet", + "cover.saved": "Kapak görseli ve pozisyonu kaydedildi", + "thumbs.modal.title": "Başlık simgelerini yönet", + "thumbs.modal.no-thumbs": "Başlık simgesi bulunamadı.", + "thumbs.modal.resize-note": "Not: Forum ayarları başlık simgelerini maksimum %1px genişliğe yeniden ölçülendirecektir. ", + "thumbs.modal.add": "Başlığa simge ekle", + "thumbs.modal.remove": "Başlığın simgesini sil", + "thumbs.modal.confirm-remove": "Bu başlık simgesini kaldırmak istediğinizden emin misiniz?" +} \ No newline at end of file diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json new file mode 100644 index 0000000000..5e793dacb0 --- /dev/null +++ b/public/language/tr/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Bildirimler", + "no_notifs": "Yeni bildiriminiz yok", + "see_all": "Bütün Bildirimler", + "mark_all_read": "Hepsini Okundu Olarak İşaretle", + "back_to_home": "%1 'a geri dön", + "outgoing_link": "Harici Link", + "outgoing_link_message": "%1 'dan ayrılıyorsunuz", + "continue_to": "%1 'a devam et", + "return_to": "%1 'a geri dön", + "new_notification": "Yeni bir bildiriminiz var", + "you_have_unread_notifications": "Okunmamış bildirimleriniz var.", + "all": "Hepsi", + "topics": "Konular", + "replies": "Yanıtlar", + "chat": "Sohbetler", + "group-chat": "Grup Sohbetleri", + "follows": "Takip Edilenler", + "upvote": "Artı Oylananlar", + "new-flags": "Yeni Şikayetler", + "my-flags": "Vekil olarak atandığım şikayetler", + "bans": "Yasaklamalar", + "new_message_from": "%1 size bir mesaj gönderdi", + "upvoted_your_post_in": "%1 şu konudaki iletinizi beğendi: %2.", + "upvoted_your_post_in_dual": "%1 ve %2 şu konudaki iletinizi beğendi: %3", + "upvoted_your_post_in_multiple": "%1 ve %2 kişi daha şu konudaki iletinizi beğendi: %3 ", + "moved_your_post": "%1, iletinizi şuraya taşıdı: %2 ", + "moved_your_topic": "%1 şuraya taşındı: %2", + "user_flagged_post_in": "%1 şu konudaki bir iletiyi şikayet etti: %2", + "user_flagged_post_in_dual": " %1 ve %2 şu konudaki bir iletiyi şikayet etti: %3", + "user_flagged_post_in_multiple": "%1 ve %2 kişi daha şu konudaki iletiyi şikayet etti: %3", + "user_flagged_user": "%1 şu kullanıcının profilini şikayet etti: (%2)", + "user_flagged_user_dual": "%1 ve %2 şu kullanıcının profilini şikayet etti: (%3)", + "user_flagged_user_multiple": "%1 ve %2 kişi daha şu kullanıcının profilini şikayet etti: (%3)", + "user_posted_to": "%1 şu konuya bir ileti yazdı: %2 ", + "user_posted_to_dual": "%1 ve %2 şu konuya ileti yazdılar: %3", + "user_posted_to_multiple": "%1 ve %2 kişi daha şu konuya ileti yazdılar: %3", + "user_posted_topic": "%1 şu yeni konuyu oluşturdu: %2", + "user_edited_post": "%1 şu konudaki bir iletiyi değiştirdi: %2", + "user_started_following_you": "%1 sizi takip etmeye başladı.", + "user_started_following_you_dual": "%1 ve %2 sizi takip etmeye başladı.", + "user_started_following_you_multiple": "%1 ve %2 kişi daha sizi takip etmeye başladı.", + "new_register": "%1 kayıt olma isteği gönderdi.", + "new_register_multiple": "Beklemede %1 kayıt olma isteği bulunmaktadır.", + "flag_assigned_to_you": "Şikayet %1 size devredildi", + "post_awaiting_review": "İnceleme bekleyen ileti(ler) var", + "profile-exported": "%1 profili hazırlandı, indirmek için tıklayınız", + "posts-exported": "%1 iletileri hazırlandı, indirmek için tıklayınız", + "uploads-exported": "%1 yüklemeleri hazırlandı, indirmek için tıklayınız", + "users-csv-exported": "Kullanıcılar csv hazırlandı, indirmek için tıklayınız", + "post-queue-accepted": "Sıradaki gönderiniz kabul edildi. Gönderinizi görmek için buraya tıklayın.", + "post-queue-rejected": "Sıraya alınmış gönderiniz reddedildi.", + "post-queue-notify": "Onay sırasındaki ileti için bir bildirim var:
\"%1\"", + "email-confirmed": "E-posta onaylandı", + "email-confirmed-message": "E-postanızı onayladığınız için teşekkürler. Hesabınız tamamen aktif edildi.", + "email-confirm-error-message": "E-posta adresinizi onaylarken bir hata oluştu. Kodunuz geçersiz ya da eski olabilir.", + "email-confirm-sent": "Onay e-postası gönderildi.", + "none": "Hiçbiri", + "notification_only": "Sadece Bildirim", + "email_only": "Sadece E-posta", + "notification_and_email": "Bildirim & E-posta", + "notificationType_upvote": "Biri iletinize artı oy verdiğinde", + "notificationType_new-topic": "Takip ettiğiniz biri yeni bir konu oluşturduğunda", + "notificationType_new-reply": "Takip ettiğiniz bir konuya yeni bir ileti gönderildiğinde", + "notificationType_post-edit": "Takip ettiğiniz bir konudaki bir ileti değiştirildiğinde", + "notificationType_follow": "Biri sizi takip etmeye başlayınca", + "notificationType_new-chat": "Bir sohbet mesajı aldığınızda", + "notificationType_new-group-chat": "Grup sohbet mesajı aldığınızda", + "notificationType_group-invite": "Bir gruba katılım davetiyesi aldığınızda", + "notificationType_group-leave": "Biri grubunuzu terk ettiğinde", + "notificationType_group-request-membership": "Biri size ait bir gruba üye olmak istediğinde", + "notificationType_new-register": "Biri kayıt kuyruğuna eklendiğinde", + "notificationType_post-queue": "Yeni bir ileti sıraya alındığında", + "notificationType_new-post-flag": "Bir ileti şikayet edildiğinde", + "notificationType_new-user-flag": "Bir kullanıcı şikayet edildiğinde" +} \ No newline at end of file diff --git a/public/language/tr/pages.json b/public/language/tr/pages.json new file mode 100644 index 0000000000..373aa898cb --- /dev/null +++ b/public/language/tr/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Anasayfa", + "unread": "Okunmamış Konular", + "popular-day": "Bugünkü popüler konular", + "popular-week": "Bu haftaki popüler konular", + "popular-month": "Bu ayki popüler konular", + "popular-alltime": "En popüler konular", + "recent": "Güncel Konular", + "top-day": "Bugün en çok oylanan konular", + "top-week": "Bu hafta en çok oylanan konular", + "top-month": "Bu ay en çok oylanan konular", + "top-alltime": "En çok oylanan konular", + "moderator-tools": "Moderatör Araçları", + "flagged-content": "Şikayet Edilen İçerik", + "ip-blacklist": "IP Kara Listesi", + "post-queue": "İleti Kuyruğu", + "users/online": "Çevrimiçi Kullanıcılar", + "users/latest": "En yeni kullanıcılar", + "users/sort-posts": "En çok ileti gönderen kullanıcılar", + "users/sort-reputation": "En çok itibarı olan kullanıcılar", + "users/banned": "Yasaklanmış Kullanıcılar", + "users/most-flags": "En Fazla Bayraklanan Kullanıcılar", + "users/search": "Kullanıcı Ara", + "notifications": "Bildirimler", + "tags": "Etiketler", + "tag": ""%1" altında etiketlenen başlıklar", + "register": "Bir hesap aç", + "registration-complete": "Kayıt tamamlandı", + "login": "Hesabına giriş yap", + "reset": "Hesap şifreni yenile", + "categories": "Kategoriler", + "groups": "Gruplar", + "group": "%1 grubu", + "chats": "Sohbetler", + "chat": "%1 ile sohbet", + "flags": "Şikayetler", + "flag-details": "%1 Nolu Şikayet Detayları", + "account/edit": "\"%1\" düzenleniyor", + "account/edit/password": "\"%1\" parolası düzenleniyor", + "account/edit/username": "\"%1\" kullanıcı adı düzenleniyor", + "account/edit/email": "\"%1\" e-posta adresini düzenliyor", + "account/info": "Hesap Hakkında", + "account/following": "%1 tarafından takip edilenler", + "account/followers": "%1 kullanıcısını takip edenler", + "account/posts": "%1 tarafından gönderilen iletiler", + "account/latest-posts": "%1 tarafından gönderilen son iletiler", + "account/topics": "%1 tarafından oluşturulan başlıklar", + "account/groups": "%1 kullanıcısına ait gruplar", + "account/watched_categories": "%1 kullanıcısının takip ettiği kategoriler", + "account/bookmarks": "%1 kullanıcısının yer imlerine eklenmiş iletiler", + "account/settings": "Kullanıcı Ayarları", + "account/watched": "%1 tarafından takip edilen başlıklar", + "account/ignored": "%1 tarafından yok sayılan başlıklar", + "account/upvoted": "%1 tarafından artılanan iletiler", + "account/downvoted": "%1 tarafından eksilenen iletiler", + "account/best": "%1 tarafından gönderilen en iyi iletiler", + "account/controversial": "%1 tarafından gönderilen tartışmalı iletiler", + "account/blocks": "%1 tarafından engellenen kullanıcılar", + "account/uploads": "%1 kullanıcısının yüklediği dosyalar", + "account/sessions": "Giriş Oturumları", + "confirm": "E-posta Onaylandı", + "maintenance.text": "%1 şu anda bakımda. Lütfen bir süre sonra tekrar deneyin.", + "maintenance.messageIntro": "Ayrıca, yönetici şu mesaji bıraktı:", + "throttled.text": "%1 şu anda kullanılamıyor. Lütfen daha sonra tekrar deneyiniz." +} \ No newline at end of file diff --git a/public/language/tr/post-queue.json b/public/language/tr/post-queue.json new file mode 100644 index 0000000000..7ee1002418 --- /dev/null +++ b/public/language/tr/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "İleti Kuyruğu", + "description": "İleti kuyruğunda hiçbir ileti bulunmamaktadır.
Bu özelliği aktifleştirmek için, şuraya gidin Ayarlar → İleti → İleti Kuyruğu ve şu özelliği aktifleştirin: İleti Kuyruğu.", + "user": "Kullanıcı", + "category": "Kategori", + "title": "Başlık", + "content": "İçerik", + "posted": "Gönderildi", + "reply-to": "\"%1\"'e Cevap Ver", + "content-editable": "Düzenlemek için içeriğe tıklayın", + "category-editable": "Düzenlemek için kategoriye tıklayın", + "title-editable": "Düzenlemek için başlığa tıklayın", + "reply": "Yanıtla", + "topic": "Başlık", + "accept": "Onayla", + "reject": "Reddet", + "remove": "Sil", + "notify": "Bildirim yap", + "notify-user": "Kullanıcıyı uyar", + "confirm-reject": "Bu iletiyi reddetmek istediğinize emin misiniz?", + "bulk-actions": "Toplu İşlemler", + "accept-all": "Hepsini Onayla", + "accept-selected": "Seçili Olanları Onayla", + "reject-all": "Hepsini Reddet", + "reject-all-confirm": "Tüm iletileri reddetmek istediğinize emin misiniz?", + "reject-selected": "Seçili Olanları Reddet", + "reject-selected-confirm": "Seçili olan %1 iletiyi reddetmek istediğinize emin misiniz?", + "bulk-accept-success": "%1 ileti onaylandı", + "bulk-reject-success": "%1 ileti reddedildi" +} \ No newline at end of file diff --git a/public/language/tr/recent.json b/public/language/tr/recent.json new file mode 100644 index 0000000000..3a7ad94b41 --- /dev/null +++ b/public/language/tr/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Güncel", + "day": "Gün", + "week": "Hafta", + "month": "Ay", + "year": "Yıl", + "alltime": "Hepsi", + "no_recent_topics": "Güncel konu yok.", + "no_popular_topics": "Popüler konu yok.", + "there-is-a-new-topic": "Yeni bir konu mevcut.", + "there-is-a-new-topic-and-a-new-post": "Yeni bir konu ve ileti mevcut.", + "there-is-a-new-topic-and-new-posts": "Bir adet yeni konu ve %1 adet yeni ileti var.", + "there-are-new-topics": "%1 adet yeni konu mevcut.", + "there-are-new-topics-and-a-new-post": "%1 adet yeni konu ve bir adet yeni ileti mevcut.", + "there-are-new-topics-and-new-posts": "%1 adet yeni konu ve %2 adet yeni ileti mevcut.", + "there-is-a-new-post": "Yeni bir ileti mevcut.", + "there-are-new-posts": "%1 adet yeni ileti mevcut.", + "click-here-to-reload": "Yüklemek için buraya tıklayın." +} \ No newline at end of file diff --git a/public/language/tr/register.json b/public/language/tr/register.json new file mode 100644 index 0000000000..0d81d81ce0 --- /dev/null +++ b/public/language/tr/register.json @@ -0,0 +1,32 @@ +{ + "register": "Kayıt Ol", + "cancel_registration": "Kaydı İptal Et", + "help.email": "E-posta adresiniz varsayılan olarak topluluktan gizlidir.", + "help.username_restrictions": "%1 ve %2 karakter arası bir kullanıcı ismi. Başkaları sizden @isim kullanarak bahsedebilir.", + "help.minimum_password_length": "Şifreniz en az %1 karakter olmalı", + "email_address": "E-posta Adresi", + "email_address_placeholder": "E-posta Adresinizi Girin", + "username": "Kullanıcı Adı", + "username_placeholder": "Kullanıcı Adınızı Girin", + "password": "Şifre", + "password_placeholder": "Şifrenizi Girin", + "confirm_password": "Şifrenizi Onaylayın", + "confirm_password_placeholder": "Şifrenizi Onaylayın", + "register_now_button": "Hemen Kayıt Ol", + "alternative_registration": "Alternatif Kayıt", + "terms_of_use": "Kullanım Şartları", + "agree_to_terms_of_use": "Kullanım Şartlarını Kabul Ediyorum", + "terms_of_use_error": "Kullanım Şartlarını Kabul Etmeniz Gerekiyor", + "registration-added-to-queue": "Kayıt olma isteğiniz kabul listesine eklenmiştir. Yönetici tarafından kabul edildiğinizde e-posta alacaksınız.", + "registration-queue-average-time": "Üyelik onayı için bekleyeceğiniz ortalama süre: %1 saat %2 dakika.", + "registration-queue-auto-approve-time": "Forum üyeliğiniz %1 saat içerisinde tamamen aktifleştirilecektir. ", + "interstitial.intro": "Hesabınızı güncellemek için bazı ek bilgiler istiyoruz…", + "interstitial.intro-new": "Hesabınızı oluşturabilmemiz için önce bazı ek bilgiler istiyoruz…", + "interstitial.errors-found": "Lütfen girilen bilgileri inceleyin:", + "gdpr_agree_data": "Bu web sitesinde kişisel bilgilerimin toplanmasını ve işlenmesini kabul ediyorum.", + "gdpr_agree_email": "Bu web sitesinden özet ve bildirim e-postası almaya izin veriyorum.", + "gdpr_consent_denied": "Bilgilerinizi toplamak/işlemek ve size e-posta göndermek için bu siteye onay vermelisiniz.", + "invite.error-admin-only": "Direkt üye kaydı devre dışı bırakıldı. Lütfen daha fazla bilgi için bir yöneticiye ulaşın.", + "invite.error-invite-only": "Direkt üye kaydı devre dışı bırakıldı. Bu foruma erişebilmek için bir üye tarafından davet edilmelisiniz. ", + "invite.error-invalid-data": "Girilen üyelik bilgileri kayıtlarda bulunamadı. Lütfen daha fazla bilgi için bir yöneticiye ulaşın." +} \ No newline at end of file diff --git a/public/language/tr/reset_password.json b/public/language/tr/reset_password.json new file mode 100644 index 0000000000..2ab3016329 --- /dev/null +++ b/public/language/tr/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Şifreyi Sıfırla", + "update_password": "Şifreyi Güncelle", + "password_changed.title": "Şifreniz Değiştirildi", + "password_changed.message": "

Şifreniz başarıyla değiştirildi, lütfen tekrar giriş yapın.", + "wrong_reset_code.title": "Yanlış Sıfırlama Kodu", + "wrong_reset_code.message": "Şifre sıfırlama kodu yanlış, lütfen tekrar deneyin ya da yeni bir şifre sıfırlama kodu isteyin.", + "new_password": "Yeni Şifre", + "repeat_password": "Şifreyi Onayla", + "changing_password": "Şifre Değiştiriliyor", + "enter_email": "Lütfen e-posta adresinizi girin , size hesabınızı nasıl sıfırlayacağınızı anlatan bir e-posta gönderelim", + "enter_email_address": "E-posta Adresinizi Girin", + "password_reset_sent": "Girilen adres var olan bir kullanıcıya aitse, kendisine şifre yenileme e-postası gönderildi. Dakikada sadece bir e-posta gönderebileceğinizi unutmayın! ", + "invalid_email": "Geçersiz E-posta / E-posta mevcut değil!", + "password_too_short": "Girdiğiniz şifre çok kısa, lütfen farklı bir şifre seçiniz.", + "passwords_do_not_match": "Girdiğiniz iki şifre birbirine uymuyor.", + "password_expired": "Şifrenizin geçerliliği sona erdi, lütfen yeni bir şifre seçin" +} \ No newline at end of file diff --git a/public/language/tr/search.json b/public/language/tr/search.json new file mode 100644 index 0000000000..94fb3947d4 --- /dev/null +++ b/public/language/tr/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 tane “%2“ bulundu (%3 saniye)", + "no-matches": "Hiç eşleşme bulunamadı", + "advanced-search": "Gelişmiş Arama", + "in": "Konum:", + "titles": "Başlıklar", + "titles-posts": "Başlıklar ve İletiler", + "match-words": "Eşleşen Kelimeler", + "all": "Hepsi", + "any": "Herhangi", + "posted-by": "Gönderen", + "in-categories": "Şu kategorilerde ara:", + "search-child-categories": "Alt kategorilerde de arat", + "has-tags": "Etiketler", + "reply-count": "Cevap sayısı", + "at-least": "En az", + "at-most": "En fazla", + "relevance": "İlgi", + "post-time": "Yayımlanma zamanı", + "votes": "Oylar", + "newer-than": "Daha yeni", + "older-than": "Daha eski", + "any-date": "Herhangi bir tarih", + "yesterday": "Dün", + "one-week": "Bir hafta", + "two-weeks": "İki hafta", + "one-month": "Bir ay", + "three-months": "Üç ay", + "six-months": "Altı ay", + "one-year": "Bir yıl", + "sort-by": "Şuna göre filtrele", + "last-reply-time": "En son cevaplama süresi", + "topic-title": "Konu başlığı", + "topic-votes": "Oylanan konular", + "number-of-replies": "Cevap sayısı", + "number-of-views": "Görüntüleme sayısı", + "topic-start-date": "Başlık oluşturulma tarihi", + "username": "Kullanıcı Adı", + "category": "Kategori", + "descending": "Azalan düzene göre", + "ascending": "Artan düzene göre", + "save-preferences": "Tercihleri Kaydet", + "clear-preferences": "Tercihleri Sil", + "search-preferences-saved": "Arama tercihleri kaydedildi", + "search-preferences-cleared": "Arama tercihleri temizlendi", + "show-results-as": "Sonuçları göster : ", + "see-more-results": "Daha fazla sonuç gör (%1)", + "search-in-category": "\"%1\" içinde ara" +} \ No newline at end of file diff --git a/public/language/tr/success.json b/public/language/tr/success.json new file mode 100644 index 0000000000..d474f90a92 --- /dev/null +++ b/public/language/tr/success.json @@ -0,0 +1,7 @@ +{ + "success": "Başarılı", + "topic-post": "Başarıyla gönderim yaptınız.", + "post-queued": "Gönderiniz onay için sıraya alındı. Kabul edildiğinde veya reddedildiğinde bir bildirim alacaksınız.", + "authentication-successful": "Doğrulama Başarılı", + "settings-saved": "Ayarlar kaydedildi!" +} \ No newline at end of file diff --git a/public/language/tr/tags.json b/public/language/tr/tags.json new file mode 100644 index 0000000000..7b5c872862 --- /dev/null +++ b/public/language/tr/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Bu etiketli başlık yok.", + "tags": "Etiketler", + "enter_tags_here": "Etiketleri buraya girin. %1-%2 karakter. Her etiketten sonra enter tuşuna basın.", + "enter_tags_here_short": "Etiketleri gir...", + "no_tags": "Henüz etiket yok.", + "select_tags": "Etiketleri Seç" +} \ No newline at end of file diff --git a/public/language/tr/top.json b/public/language/tr/top.json new file mode 100644 index 0000000000..012cabb42d --- /dev/null +++ b/public/language/tr/top.json @@ -0,0 +1,4 @@ +{ + "title": "Zirve", + "no_top_topics": "Zirve Konu Yok" +} \ No newline at end of file diff --git a/public/language/tr/topic.json b/public/language/tr/topic.json new file mode 100644 index 0000000000..c83a476e00 --- /dev/null +++ b/public/language/tr/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Konu", + "title": "Başlık", + "no_topics_found": "Hiç başlık bulunamadı!", + "no_posts_found": "Hiç ileti bulunamadı!", + "post_is_deleted": "Bu ileti silindi!", + "topic_is_deleted": "Bu başlık silindi!", + "profile": "Profil", + "posted_by": "%1 tarafından gönderildi", + "posted_by_guest": "Ziyaretçi tarafından yayımlandı", + "chat": "Sohbet", + "notify_me": "Bu başlıktaki yeni cevaplardan haberdar ol", + "quote": "Alıntı", + "reply": "Cevap", + "replies_to_this_post": "%1 Cevap", + "one_reply_to_this_post": "1 Cevap", + "last_reply_time": "Son cevap", + "reply-as-topic": "Yeni başlık oluşturarak cevapla", + "guest-login-reply": "Cevaplamak için giriş yapın", + "login-to-view": "🔒 Görüntülemek için giriş yap!", + "edit": "Düzenle", + "delete": "Sil", + "delete-event": "Etkinliği Sil", + "delete-event-confirm": "Bu etkinliği silmek istediğinizden emin misiniz?", + "purge": "Temizle", + "restore": "Geri Getir", + "move": "Taşı", + "change-owner": "Sahibini Değiştir", + "fork": "Ayır", + "link": "Bağlantı", + "share": "Paylaş", + "tools": "Araçlar", + "locked": "Kilitli", + "pinned": "Sabitlendi", + "pinned-with-expiry": "%1 tarihine kadar sabitlendi", + "scheduled": "Konu Zamanlandı", + "moved": "Taşındı", + "moved-from": "Şuradan taşındı: %1", + "copy-ip": "IP Kopyala", + "ban-ip": "IP Yasakla", + "view-history": "Geçmişi Düzenle", + "locked-by": "Kilitlendi", + "unlocked-by": "Kilit kaldırıldı", + "pinned-by": "Sabitlendi", + "unpinned-by": "Sabitlenme kaldırıldı", + "deleted-by": "Silindi", + "restored-by": "Tekrar Yüklendi", + "moved-from-by": "%1 bölümünden taşındı", + "queued-by": "İleti onay için sıraya alındı →", + "backlink": "Bu başlıktan bahsedildi.", + "forked-by": "Başlık ayrıldı", + "bookmark_instructions": "Bu konuda en son kaldığın yere dönmek için tıkla.", + "flag-post": "Bu iletiyi şikayet et", + "flag-user": "Bu kullanıcıyı şikayet et", + "already-flagged": "Halihazırda şikayet edilmiş", + "view-flag-report": "Şikayet raporunu görüntüle", + "resolve-flag": "Şikayeti Çözümle", + "merged_message": "Bu başlık %2 ile birleştirildi", + "deleted_message": "Bu başlık silindi. Sadece başlık düzenleme yetkisi olan kullanıcılar görebilir.", + "following_topic.message": "Artık bir kullanıcı bu konuya yeni ileti gönderdiğinde siz de bildirim alacaksınız.", + "not_following_topic.message": "Bu konuyu \"Okunmamış\" listesinde göreceksiniz, ama bir kullanıcı yeni bir ileti yazdığında bildirim gelmeyecek.", + "ignoring_topic.message": "Bu konuyu artık \"Okunmamış\" listesinde görmeyeceksiniz. Eğer bir iletide bahsedilirseniz veya iletiniz oylanırsa bildirim alacaksınız.", + "login_to_subscribe": "Lütfen bu konuyu takip etmek için üye olun veya giriş yapın.", + "markAsUnreadForAll.success": "Başlık herkes için okunmadı olarak işaretlendi.", + "mark_unread": "Okunmadı olarak işaretle", + "mark_unread.success": "Başlık okunmamış olarak işaretlendi.", + "watch": "Takip", + "unwatch": "Takip etme", + "watch.title": "Bu konuya gelen yeni iletilerden haberdar ol", + "unwatch.title": "Bu başlığı izleme", + "share_this_post": "Bu iletiyi paylaş", + "watching": "Takip ediliyor", + "not-watching": "Takip edilmiyor", + "ignoring": "Susturulmuş", + "watching.description": "Yeni bir ileti geldiğinde bana bildir.
Konuyu okunmamış olarak göster.", + "not-watching.description": "Yeni bir ileti geldiğinde bana bildirme.
Kategori susturulmamışsa okunmamış olarak göster.", + "ignoring.description": "Yeni bir ileti geldiğinde bana bildirme.
Konuyu okunmamış olarak gösterme.", + "thread_tools.title": "Konu Ayarları", + "thread_tools.markAsUnreadForAll": "Okunmamış olarak İşaretle", + "thread_tools.pin": "Başlığı Sabitle", + "thread_tools.unpin": "Başlığı Sabitleme", + "thread_tools.lock": "Konuyu Kilitle", + "thread_tools.unlock": "Konu Kilidini Kaldır", + "thread_tools.move": "Başlığı Taşı", + "thread_tools.move-posts": "İletiyi Taşı", + "thread_tools.move_all": "Hepsini Taşı", + "thread_tools.change_owner": "Sahibini Değiştir", + "thread_tools.select_category": "Kategori Seç", + "thread_tools.fork": "Konuyu Ayır", + "thread_tools.delete": "Konuyu Sil", + "thread_tools.delete-posts": "İletileri Sil", + "thread_tools.delete_confirm": "Bu başlığı gerçekten silmek istediğinize emin misiniz?", + "thread_tools.restore": "Başlığı Geri Getir", + "thread_tools.restore_confirm": "Bu başlığı gerçekten geri getirmek istiyor musunuz?", + "thread_tools.purge": "Başlığı Temizle", + "thread_tools.purge_confirm": "Bu konuyu temizlemek istediğinize emin misiniz?", + "thread_tools.merge_topics": "Başlıkları Birleştir", + "thread_tools.merge": "Birleştir", + "topic_move_success": "Bu başlık şu bölüme taşınıyor: \"%1\" . İşlemi iptal etmek için tıklayınız.", + "topic_move_multiple_success": "Bu başlıklar şu bölüme taşınıyor: \"%1\" . İşlemi iptal etmek için tıklayınız.", + "topic_move_all_success": "Tüm başlıklar şu bölüme taşınıyor: \"%1\" . İşlemi iptal etmek için tıklayınız.", + "topic_move_undone": "Başlık taşıma iptal edildi", + "topic_move_posts_success": "Bu ileti taşınmak üzere. Taşınma işlemini geri almak için tıklayınız! ", + "topic_move_posts_undone": "İleti taşıma iptal edildi", + "post_delete_confirm": "Bu iletiyi gerçekten silmek istediğinize emin misiniz?", + "post_restore_confirm": "Bu iletiyi gerçekten geri getirmek istiyor musunuz?", + "post_purge_confirm": "Bu iletiyi temizlemek istediğinize emin misiniz?", + "pin-modal-expiry": "Sona erme tarihi", + "pin-modal-help": "Sabitlenen konular için bir bitiş tarihi belirleyebilirsiniz. Eğer bu tarihi boş bırakırsanız, konular siz sabitliğini kaldırana kadar sabitlenmiş olarak kalır.", + "load_categories": "Kategoriler Yükleniyor", + "confirm_move": "Taşı", + "confirm_fork": "Ayır", + "bookmark": "Yer imlerine ekle", + "bookmarks": "Yer imleri", + "bookmarks.has_no_bookmarks": "Henüz hiçbir iletiyi yer imlerine eklemediniz!", + "copy-permalink": "Bağlantıyı Kopyala", + "loading_more_posts": "Daha fazla ileti ", + "move_topic": "Başlığı Taşı", + "move_topics": "Başlıkları Taşı", + "move_post": "İletiyi Taşı", + "post_moved": "İleti taşındı!", + "fork_topic": "Başlığı Ayır", + "enter-new-topic-title": "Oluşturulacak Yeni Başlığı Buraya Giriniz", + "fork_topic_instruction": "Ayırmak istediğiniz iletileri seçiniz!", + "fork_no_pids": "Hiçbir ileti seçilmedi!", + "no-posts-selected": "Hiçbir ileti seçili değil!", + "x-posts-selected": "%1 ileti seçildi", + "x-posts-will-be-moved-to-y": "%1 ileti şuraya taşınacak: \"%2\"", + "fork_pid_count": "%1 ileti(ler) seçildi", + "fork_success": "Başlık başarıyla ayrıldı! Yeni başlığı görüntülemek için tıklayınız!", + "delete_posts_instruction": "Silmek/temizlemek istediğiniz iletileri seçiniz!", + "merge_topics_instruction": "Birleştirmek istediğiniz başlıkları seçiniz veya onları arayınız!", + "merge-topic-list-title": "Birleştirilecek başlık listesi", + "merge-options": "Birleştirme Seçenekleri", + "merge-select-main-topic": "Ana başlığı seçiniz", + "merge-new-title-for-topic": "Konu için yeni başlık", + "topic-id": "Başlık ID", + "move_posts_instruction": "Taşımak istediğiniz iletileri seçin, daha sonra bir başlık ID girin veya hedef başlığa gidin", + "change_owner_instruction": "Başka kullanıcıya aktarmak istediğiniz iletileri seçiniz!", + "composer.title_placeholder": "Başlık ismini buraya giriniz...", + "composer.handle_placeholder": "Kullanıcı adınızı buraya girin", + "composer.discard": "Vazgeç", + "composer.submit": "Gönder", + "composer.additional-options": "Ekstra seçenekler", + "composer.schedule": "Konu Zamanla", + "composer.replying_to": "Yanıtlanan Başlık: %1", + "composer.new_topic": "Yeni Başlık", + "composer.editing": "Düzenleme", + "composer.uploading": "yükleniyor...", + "composer.thumb_url_label": "Başlık fotosu URL adresini yapıştır", + "composer.thumb_title": "Bu başlığa bir fotoğraf ekle", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Veya bir dosya yükle", + "composer.thumb_remove": "Alanları temizle", + "composer.drag_and_drop_images": "Fotoğrafları buraya taşıyıp bırakabilirsiniz!", + "more_users_and_guests": "%1 tane daha kullanıcı ve %2 ziyaretçi", + "more_users": "%1 tane daha kullanıcı", + "more_guests": "%1 tane daha ziyaretçi", + "users_and_others": "%1 ve %2 kişi daha", + "sort_by": "Sırala", + "oldest_to_newest": "En eskiden en yeniye", + "newest_to_oldest": "En yeniden en eskiye", + "most_votes": "En çok oylanan", + "most_posts": "En çok ileti yazılan", + "most_views": "Çok Görüntülenen", + "stale.title": "Bunun yerine yeni bir başlık oluşturun?", + "stale.warning": "Yanıtlamak istediğiniz başlık oldukça eski. Bu başlığa referans oluşturacak yeni bir başlık oluşturmak ister misiniz?", + "stale.create": "Yeni bir başlık oluştur", + "stale.reply_anyway": "Bu başlığı cevapla", + "link_back": "Cevap: [%1](%2)", + "diffs.title": "İleti düzenleme geçmişi", + "diffs.description": "Bu iletinin %1 revizyonu var. Zaman içerisinde ileti içeriğinin tamamını görmek için aşağıdaki revizyonlardan birine tıklayın.", + "diffs.no-revisions-description": "Bu iletinin %1 revizyonu var.", + "diffs.current-revision": "mevcut revizyon", + "diffs.original-revision": "orijinal revizyon", + "diffs.restore": "Bu revizyonu geri getir", + "diffs.restore-description": "Tekrar yüklendikten sonra yeni bir versiyon bu iletinin düzenlenme geçmişine eklenecektir.", + "diffs.post-restored": "İleti önceki revizyona başarıyla geri getirildi", + "diffs.delete": "Bu revizyonu sil", + "diffs.deleted": "Revizyon silindi", + "timeago_later": "%1 sonra", + "timeago_earlier": "%1 önce", + "first-post": "İlk ileti", + "last-post": "Son ileti", + "go-to-my-next-post": "Diğer iletime git", + "no-more-next-post": "Bu başlıkta başka bir iletiniz bulunmamaktadır.", + "post-quick-reply": "Hızlı yanıt gönder" +} \ No newline at end of file diff --git a/public/language/tr/unread.json b/public/language/tr/unread.json new file mode 100644 index 0000000000..29dbed8cc7 --- /dev/null +++ b/public/language/tr/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Okunmamış", + "no_unread_topics": "Okunmamış konu mevcut değil.", + "load_more": "Daha Fazla", + "mark_as_read": "Okundu Olarak İşaretle", + "selected": "Seçili", + "all": "Hepsi", + "all_categories": "Tüm kategoriler", + "topics_marked_as_read.success": "Konular okundu olarak işaretlendi!", + "all-topics": "Tüm Konular", + "new-topics": "Yeni Konular", + "watched-topics": "Takipteki Konular", + "unreplied-topics": "Okunmamış Konular", + "multiple-categories-selected": "Çoklu Seçildi" +} \ No newline at end of file diff --git a/public/language/tr/uploads.json b/public/language/tr/uploads.json new file mode 100644 index 0000000000..95232df30e --- /dev/null +++ b/public/language/tr/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Dosya yükleniyor...", + "select-file-to-upload": "Bir dosya seç!", + "upload-success": "Dosya yüklenmesi tamamlandı!", + "maximum-file-size": "Maksimum %1 kb", + "no-uploads-found": "Hiçbir yükleme bulunamadı", + "public-uploads-info": "Yüklemeler herkese açık, tüm ziyaretçiler onları görebilir.", + "private-uploads-info": "Yüklemeler gizlidir, sadece giriş yapan kullanıcılar görebilir." +} \ No newline at end of file diff --git a/public/language/tr/user.json b/public/language/tr/user.json new file mode 100644 index 0000000000..1809554c6d --- /dev/null +++ b/public/language/tr/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Yasaklı", + "muted": "Sessiz", + "offline": "Çevrimdışı", + "deleted": "Silindi", + "username": "Kullanıcı Adı", + "joindate": "Katılım Tarihi", + "postcount": "İleti Sayısı", + "email": "E-posta", + "confirm_email": "E-posta Adresini Onayla", + "account_info": "Hesap Bilgisi", + "admin_actions_label": "Yönetim Aktiviteleri", + "ban_account": "Hesabı Yasakla", + "ban_account_confirm": "Hesabı yasaklamak istediğinizden emin misiniz?", + "unban_account": "Hesabın Yasağını Kaldır", + "mute_account": "Hesabı Sustur", + "unmute_account": "Hesabın Sesini Aç", + "delete_account": "Hesabı Sil", + "delete_account_as_admin": "Hesabı Sil", + "delete_content": "Hesabın İçeriğini Sil", + "delete_all": "Hesabı ve İçeriğini Sil", + "delete_account_confirm": "Hesabınızı silip iletilerinizi anonimleştirmek istediğinize emin misiniz?
Bu işlem geri döndürülemez ve verilerinizin herhangi bir bölümünü kurtaramazsınız

Bu hesabı yoketme isteğinizi onaylamak için şifrenizi girin.", + "delete_this_account_confirm": "Hesabınızı silip, içeriğini geride bırakmak istediğinize emin misiniz?
Bu işlem geri döndürülemez, iletiler anonimleşecek ve verilerinizi geri getiremeyeceksiniz

", + "delete_account_content_confirm": "Hesabınızın içeriğini silmek istediğinize emin misiniz (iletiler/başlıklar/yüklemeler)?
Bu işlem geri döndürülemez, hiçbir veriyi geri getiremeyeceksiniz

", + "delete_all_confirm": "Hesabınızı ve tüm içeriğini silmek istediğinize emin misiniz (iletiler/başlıklar/yüklemeler)?
Bu işlem geri döndürülemez, hiçbir veriyi geri getiremeyeceksiniz

", + "account-deleted": "Hesap silindi", + "account-content-deleted": "Hesaba ait içerik silindi", + "fullname": "İsim Soyisim", + "website": "İnternet Sitesi", + "location": "Konum", + "age": "Yaş:", + "joined": "Katılım Tarihi:", + "lastonline": "Son Çevrimiçi Zamanı:", + "profile": "Profil", + "profile_views": "Profil Görüntülemeleri", + "reputation": "Saygınlık", + "bookmarks": "Yer İmleri", + "watched_categories": "Takip edilen kategoriler", + "change_all": "Hepsini Değiştir", + "watched": "Takip edilen başlıklar", + "ignored": "Susturulan başlıklar", + "default-category-watch-state": "Varsayılan kategori izleme durumu", + "followers": "Takipçiler", + "following": "Takip Edilenler", + "blocks": "Engellenenler", + "block_toggle": "Blokta Geçiş Yap", + "block_user": "Kullanıcıyı Engelle", + "unblock_user": "Kullanıcı Engelini Kaldır", + "aboutme": "Hakkımda", + "signature": "İmza", + "birthday": "Doğum Tarihi", + "chat": "Sohbet", + "chat_with": "%1 ile sohbete devam et", + "new_chat_with": "%1 ile yeni sohbete başla", + "flag-profile": "Profili şikayet et", + "follow": "Takip Et", + "unfollow": "Takip etme", + "more": "Daha Fazla", + "profile_update_success": "Profiliniz başarıyla güncellendi!", + "change_picture": "Fotoğrafı Değiştir", + "change_username": "Kullanıcı Adı Değiştir", + "change_email": "E-posta Değiştir", + "email_same_as_password": "Lütfen devam etmek için şu anki şifrenizi girin – Tekrar e-posta adresinizi girdiniz", + "edit": "Düzenle", + "edit-profile": "Profil Düzenle", + "default_picture": "Varsayılan ikon", + "uploaded_picture": "Yüklenmiş fotoğraflar", + "upload_new_picture": "Yeni bir fotoğraf yükle", + "upload_new_picture_from_url": "İnternetten yeni bir fotoğraf yükle", + "current_password": "Şu anki şifre", + "change_password": "Şifre Değiştir", + "change_password_error": "Geçersiz Şifre", + "change_password_error_wrong_current": "Şu anki şifre doğru değil!", + "change_password_error_match": "Şifreler aynı olmalı!", + "change_password_error_privileges": "Bu şifreyi değiştirme yetkiniz yok.", + "change_password_success": "Şifreniz güncellendi!", + "confirm_password": "Şifreyi Onayla", + "password": "Şifre", + "username_taken_workaround": "İstediğiniz kullanıcı ismi zaten alınmış, bu yüzden biraz degiştirdik. Şimdiki kullanıcı isminiz %1", + "password_same_as_username": "Parolanız kullanıcı adınız ile aynı, lütfen başka bir parola seçiniz.", + "password_same_as_email": "Şifreniz e-posta adresiniz ile aynı, lütfen başka bir şifre seçin.", + "weak_password": "Zayıf şifre.", + "upload_picture": "Fotoğraf Yükle", + "upload_a_picture": "Bir Fotoğraf Yükle", + "remove_uploaded_picture": "Yüklenmiş fotoğrafı kaldır", + "upload_cover_picture": "Kapak görseli yükle", + "remove_cover_picture_confirm": "Kapak görselini silmek istediğinize emin misiniz?", + "crop_picture": "Görsel Kırp", + "upload_cropped_picture": "Kırp ve yükle", + "avatar-background-colour": "Avatar arkaplan rengi", + "settings": "Ayarlar", + "show_email": "E-postamı göster", + "show_fullname": "Tam ismimi göster", + "restrict_chats": "Sadece takip ettiğim kişilerden sohbetleri kabul et", + "digest_label": "Özet e-postalarına kaydol", + "digest_description": "Bu forum için e-posta güncellemelerine kaydol.", + "digest_off": "Kapalı", + "digest_daily": "Günlük", + "digest_weekly": "Haftalık", + "digest_biweekly": "İki haftada bir", + "digest_monthly": "Aylık", + "has_no_follower": "Bu kullanıcının hiç takipçisi yok :(", + "follows_no_one": "Bu kullanıcı hiçkimseyi takip etmiyor :(", + "has_no_posts": "Bu kullanıcı henüz herhangi bir ileti yazmamış :(", + "has_no_best_posts": "Bu kullanıcının herhangi bir gönderisi henüz artı oy almadı.", + "has_no_topics": "Bu kullanıcı henüz hiçbir başlık açmamış :(", + "has_no_watched_topics": "Bu kullanıcı henüz hiçbir başlığı takip etmiyor :(", + "has_no_ignored_topics": "Bu kullanıcı henüz hiçbir başlığı yok saymamış.", + "has_no_upvoted_posts": "Bu kullanıcı henüz hiçbir iletiyi artılamamış.", + "has_no_downvoted_posts": "Bu kullanıcı henüz hiçbir iletiyi eksilememiş.", + "has_no_controversial_posts": "Bu kullanıcının herhangi bir gönderisi eksi oy almadı.", + "has_no_blocks": "Hiçbir kullanıcıyı engellemediniz.", + "email_hidden": "E-posta gizli", + "hidden": "gizli", + "paginate_description": "Sonsuz yükleme yerine konu ve iletileri sayfalara böl", + "topics_per_page": "Sayfa başına başlık sayısı", + "posts_per_page": "Sayfa başına ileti sayısı", + "max_items_per_page": "Maksimum %1", + "acp_language": "Yönetici Sayfası Dili", + "notifications": "Bildirimler", + "upvote-notif-freq": "Artı oy bildiri sıklığı", + "upvote-notif-freq.all": "Bütün artı oylar", + "upvote-notif-freq.first": "Her ileti için ilk oy", + "upvote-notif-freq.everyTen": "Her 10 oyda bir", + "upvote-notif-freq.threshold": "1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "10, 100, 1000...", + "upvote-notif-freq.disabled": "Devre dışı bırak", + "browsing": "Tarayıcı Ayarları", + "open_links_in_new_tab": "Dışarı giden bağlantıları yeni sekmede aç", + "enable_topic_searching": "Konu içi aramayı aktive et", + "topic_search_help": "Aktive edilirse, konu içi arama tarayıcının normal arama davranışını değiştirerek tüm konuyu aramanızı sağlar", + "update_url_with_post_index": "Sayfayı okurken URL bağlantısındaki ileti numarasını güncelle", + "scroll_to_my_post": "Cevap yazdıktan sonra yeni iletiyi göster", + "follow_topics_you_reply_to": "Cevap verdiğim başlıkları takip et", + "follow_topics_you_create": "Oluşturduğum başlıkları takip et", + "grouptitle": "Grup Unvanları", + "group-order-help": "Bir grup seçin ve unvanları sıralamak için yön tuşlarını kullanın", + "no-group-title": "Grup unvanı yok", + "select-skin": "Bir tema seçin", + "select-homepage": "Bir \"Anasayfa\" seçin", + "homepage": "Anasayfa", + "homepage_description": "Anasayfa olarak kullanacağınız sayfayı seçin veya \"Hiçbiri\" diyerek varsayılan sayfayı kullanın.", + "custom_route": "Özel anasayfa yolu", + "custom_route_help": "Herhangi bir eğik çizgi olmadan, burada bir yol adını girin (örneğin \"yeniler\" , \"popüler\" veya \"category/2/general-discussion\")", + "sso.title": "Tek tuşla giriş uygulamaları", + "sso.associated": "Birleştirilmiş", + "sso.not-associated": "Birleştirmek için buraya tıklayın", + "sso.dissociate": "Ayrış", + "sso.dissociate-confirm-title": "Ayrışmayı Onayla", + "sso.dissociate-confirm": "%1 'den ayrışmak istediğinizden emin misiniz?", + "info.latest-flags": "Son Şikayetler", + "info.no-flags": "Şikayet edilen bir ileti bulunamadı", + "info.ban-history": "Yasaklama Geçmişi", + "info.no-ban-history": "Bu kullanıcı daha önce hiç yasaklanmadı", + "info.banned-until": "Yasaklama süresi %1", + "info.banned-expiry": "Bitiş", + "info.banned-permanently": "Kalıcı şekilde yasakla", + "info.banned-reason-label": "Gerekçe", + "info.banned-no-reason": "Gerekçe belirtilmedi.", + "info.mute-history": "Susturulma Geçmişi", + "info.no-mute-history": "Bu kullanıcı daha önce hiç susturulmadı", + "info.muted-until": "Susturulma süresi %1", + "info.muted-expiry": "Bitiş", + "info.muted-no-reason": "Herhangi bir gerekçe belirtilmedi.", + "info.username-history": "Kullanıcı Adı Geçmişi", + "info.email-history": "E-posta Geçmişi", + "info.moderation-note": "Moderasyon Notu", + "info.moderation-note.success": "Moderasyon notu kaydedildi", + "info.moderation-note.add": "Not ekle", + "sessions.description": "Bu sayfa, bu forumdaki tüm aktif oturumları görüntülemenizi ve gerektiğinde iptal etmenizi sağlar. Kendi oturumunuzu hesabınızdan çıkış yaparak iptal edebilirsiniz.", + "consent.title": "Haklarınız & İzinleriniz", + "consent.lead": "Bu topluluk forumu kişisel bilgilerinizi toplar ve işler.", + "consent.intro": "Bu bilgileri kesinlikle bu topluluktaki deneyiminizi kişiselleştirmek ve kullanıcı hesabınıza yaptığınız yayınları ilişkilendirmek için kullanıyoruz. Kayıt adımı sırasında sizden bir kullanıcı adı ve e-posta adresi vermeniz istenmiştir. Ayrıca, kullanıcı profilinizi bu web sitesinde tamamlamak için isteğe bağlı olarak ek bilgi de ekleyebilirsiniz.

Bu bilgileri kullanıcı hesabınızın ömrü boyunca saklayabilir ve izinleri geri alabilirsiniz. Hesabınızı istediğiniz zaman silebilirsiniz. Herhangi bir zamanda, bu web sitesine yaptığınız katkıların bir kopyasını Haklar & Onay sayfasını talep edebilirsiniz.

Sorularınız veya endişeleriniz varsa, bu forumun yönetim ekibine ulaşmanızı öneririz.", + "consent.email_intro": "Zaman zaman, güncellemeler ve / veya size uygun yeni etkinlikleri bildirmek için kayıtlı adresinize e-posta gönderebiliriz. Topluluk özetlemesinin sıklığını (tam olarak devre dışı bırakma dahil) özelleştirebilir, ayrıca kullanıcı ayarları sayfanız aracılığıyla e-posta yoluyla hangi bildirim türlerini alacağınızı seçebilirsiniz.", + "consent.digest_frequency": "Kullanıcı ayarlarınızda açıkça değiştirilmedikçe, bu topluluk her %1 e-posta özetini gönderir.", + "consent.digest_off": "Kullanıcı ayarlarınızda açıkça değiştirilmedikçe, bu topluluk e-posta özetlerini göndermez", + "consent.received": "Bilgilerinizi toplamak ve işlemek için bu web sitesine izin vermiş bulunuyorsunuz. Ek işlem gerekli değildir.", + "consent.not_received": "Veri toplama ve işleme için onay vermediniz. Herhangi bir zamanda, bu web sitesi yönetimi, Genel Veri Koruma Yönetmeliğine uymak için hesabınızı silmeyi seçebilir.", + "consent.give": "İzin ver", + "consent.right_of_access": "Erişim hakkına sahipsiniz", + "consent.right_of_access_description": "Bu web sitesi tarafından toplanan herhangi bir veriye istek üzerine erişim hakkına sahipsiniz. Aşağıdaki uygun düğmeyi tıklayarak bu verilerin bir kopyasını alabilirsiniz.", + "consent.right_to_rectification": "Düzeltme hakkına sahipsiniz", + "consent.right_to_rectification_description": "Bize verilen yanlış verileri değiştirme veya güncelleme hakkına sahipsiniz. Profiliniz, profilinizi düzenleyerek güncellenebilir ve içerik yayınlamak her zaman düzenlenebilir. Durum böyle değilse, lütfen bu sitenin yönetim ekibiyle iletişime geçin.", + "consent.right_to_erasure": "Silme hakkına sahipsiniz", + "consent.right_to_erasure_description": "Herhangi bir zamanda, hesabınızı silerek veri toplama ve / veya işlemeye onayınızı iptal edebilirsiniz. Gönderilen içeriğiniz kalsa da, bireysel profiliniz silinebilir. Hem hesabınızı hem de içeriğinizi silmek isterseniz, lütfen bu web sitesi için yönetim ekibine başvurun.", + "consent.right_to_data_portability": "Veri taşıma hakkına sahipsiniz", + "consent.right_to_data_portability_description": "Sizden ve hesabınız hakkında toplanan verilere makine tarafından okunabilir bir veri talep edebilirsiniz. Aşağıdaki uygun düğmeyi tıklayarak bunu yapabilirsiniz.", + "consent.export_profile": "Profili Dışa Aktar (.json)", + "consent.export-profile-success": "Profil aktarılmak üzere hazırlanıyor, işlem tamamlandığında bildirim alacaksınız!", + "consent.export_uploads": "Karşıya Yüklenmiş İçeriği Dışarı Aktar (.zip)", + "consent.export-uploads-success": "Yüklemeler aktarılmak üzere hazırlanıyor, işlem tamamlandığında bildirim alacaksınız!", + "consent.export_posts": "Gönderileri Dışa Aktar (.csv)", + "consent.export-posts-success": "İletiler aktarılmak üzere hazırlanıyor, işlem tamamlandığında bildirim alacaksınız!", + "emailUpdate.intro": "Lütfen e-posta adresinizi aşağıya girin. Bu forum, e-posta adresinizi planlanmış özet ve bildirimler ile parolanın kaybolması durumunda hesap kurtarma için kullanır.", + "emailUpdate.optional": "Bu bölüm tercihe bağlıdır. Bir e-posta adresi girmek zorunda değilsiniz, fakat onaylanmış bir e-posta adresi olmadan hesabınızı veya girişinizi e-posta adresiniz ile kurtaramazsınız. ", + "emailUpdate.required": "Bu bölüm zorunludur.", + "emailUpdate.change-instructions": "Girilen e-posta adresine kişiye özel bir bağlantı içeren bir onay e-postası gönderilecektir. Bu bağlantıya erişmek, e-posta adresinin sahibi olduğunuzu onaylayacak ve hesabınızda etkin hale gelecektir. İstediğiniz zaman, hesap sayfanızdan kayıtlı e-postanızı güncelleyebilirsiniz.", + "emailUpdate.password-challenge": "Hesabın size ait olduğunu doğrulamak için lütfen şifrenizi giriniz" +} \ No newline at end of file diff --git a/public/language/tr/users.json b/public/language/tr/users.json new file mode 100644 index 0000000000..624ab59523 --- /dev/null +++ b/public/language/tr/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "En Yeni Kullanıcılar", + "top_posters": "En Fazla Gönderim Yapanlar", + "most_reputation": "En Saygınlar", + "most_flags": "En Fazla Şikayet", + "search": "Ara", + "enter_username": "Aramak için bir kullanıcı adı girin", + "search-user-for-chat": "Sohbet için bir kullanıcı arayın", + "load_more": "Daha Fazla Yükle", + "users-found-search-took": "%1 kullanıcı(lar) bulundu! Arama %2 saniye sürdü.", + "filter-by": "Şu şekilde filtrele", + "online-only": "Sadece çevrimiçi", + "invite": "Davet et", + "prompt-email": "Eposta:", + "groups-to-join": "Davet kabul edildiğinde katılacağınız gruplar:", + "invitation-email-sent": "%1'e bir davet e-posta'sı gönderildi", + "user_list": "Kullanıcı Listesi", + "recent_topics": "Güncel Konular", + "popular_topics": "Popüler Konular", + "unread_topics": "Okunmamış Konular", + "categories": "Kategoriler", + "tags": "Etiketler", + "no-users-found": "Kullanıcı bulunamadı!" +} \ No newline at end of file diff --git a/public/language/uk/_DO_NOT_EDIT_FILES_HERE.md b/public/language/uk/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/uk/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/uk/admin/admin.json b/public/language/uk/admin/admin.json new file mode 100644 index 0000000000..092fae5bf6 --- /dev/null +++ b/public/language/uk/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Ви впевнені, що хочете перебудувати та перезавантажити NodeBB?", + "alert.confirm-restart": "Ви впевнені, що бажаєте перезавантажити NodeBB?", + + "acp-title": "%1 | Адмінська Панель Керування NodeBB", + "settings-header-contents": "Зміст", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/uk/admin/advanced/cache.json b/public/language/uk/admin/advanced/cache.json new file mode 100644 index 0000000000..db008a7eab --- /dev/null +++ b/public/language/uk/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Кеш постів", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "Заповнений на %1%", + "post-cache-size": "Розмір кешу постів", + "items-in-cache": "Елементів у кеші" +} \ No newline at end of file diff --git a/public/language/uk/admin/advanced/database.json b/public/language/uk/admin/advanced/database.json new file mode 100644 index 0000000000..a904c115a4 --- /dev/null +++ b/public/language/uk/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 б", + "x-mb": "%1 мб", + "x-gb": "%1 ГБ", + "uptime-seconds": "Uptime в секундах", + "uptime-days": "Uptime в днях", + + "mongo": "Mongo", + "mongo.version": "Версія MongoDB", + "mongo.storage-engine": "Двигун сховища", + "mongo.collections": "Колекцій", + "mongo.objects": "Об'єктів", + "mongo.avg-object-size": "Середній розмір об'єкта", + "mongo.data-size": "Розмір даних", + "mongo.storage-size": "Розмір сховища", + "mongo.index-size": "Розмір індексу", + "mongo.file-size": "Розмір файлів", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "Virtual Memory", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "Сирі дані від MongoDB", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Версія Redis", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Підключені клієнти", + "redis.connected-slaves": "Підключені слуги", + "redis.blocked-clients": "Заблоковані клієнти", + "redis.used-memory": "Використана пам'ять", + "redis.memory-frag-ratio": "Коефіцієнт фрагментації пам'яті", + "redis.total-connections-recieved": "Кількість отриманих підключень", + "redis.total-commands-processed": "Кілкість оброблених команд", + "redis.iops": "Кількість миттєвих операції в секунду", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Попадання в ключах", + "redis.keyspace-misses": "Промахи в ключах", + "redis.raw-info": "Сирі дані від Redis", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/uk/admin/advanced/errors.json b/public/language/uk/admin/advanced/errors.json new file mode 100644 index 0000000000..6d8a56ca30 --- /dev/null +++ b/public/language/uk/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Рисунок %1", + "error-events-per-day": "%1 подій в день", + "error.404": "404 Not Found", + "error.503": "503 Service Unavailable", + "manage-error-log": "Керувати логом помилок", + "export-error-log": "Експортувати лог помилок (CSV)", + "clear-error-log": "Очистити лог помилок", + "route": "Шлях", + "count": "Кількість", + "no-routes-not-found": "Ура! Помилок 404 немає!", + "clear404-confirm": "Ви впевнені, що бажаєте очистити лог помилок 404?", + "clear404-success": "Помилки \"404 Not Found\" очищено" +} \ No newline at end of file diff --git a/public/language/uk/admin/advanced/events.json b/public/language/uk/admin/advanced/events.json new file mode 100644 index 0000000000..997d177a47 --- /dev/null +++ b/public/language/uk/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Події", + "no-events": "Подій немає", + "control-panel": "Панель керування подіями", + "delete-events": "Delete Events", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "Filters", + "filters-apply": "Apply Filters", + "filter-type": "Event Type", + "filter-start": "Start Date", + "filter-end": "End Date", + "filter-perPage": "Per Page" +} \ No newline at end of file diff --git a/public/language/uk/admin/advanced/logs.json b/public/language/uk/admin/advanced/logs.json new file mode 100644 index 0000000000..b9ecc6916c --- /dev/null +++ b/public/language/uk/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Логи", + "control-panel": "Панель керування логами", + "reload": "Перевантажити логи", + "clear": "Очистити логи", + "clear-success": "Логи очищено!" +} \ No newline at end of file diff --git a/public/language/uk/admin/appearance/customise.json b/public/language/uk/admin/appearance/customise.json new file mode 100644 index 0000000000..914aa4772c --- /dev/null +++ b/public/language/uk/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Користувацькі CSS/LESS", + "custom-css.description": "Введіть свої власні CSS/LESS тут, які будуть застосовані після всіх інших стилів.", + "custom-css.enable": "Увімкнути користувацькі CSS/LESS", + + "custom-js": "Користувацький Javascript", + "custom-js.description": "Введіть свій власний код javascript тут. Він буде виконаний після повного завантаження сторінки.", + "custom-js.enable": "Увімкнути користувацький Javascript", + + "custom-header": "Користувацький заголовок", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "Увімкнути користувацький заголовок", + + "custom-css.livereload": "Увімкнути Автоматичне Оновлення", + "custom-css.livereload.description": "Увімкніть цю опцію, щоб примусово оновлювати всі сесії вашого акаунту, коли ви натискаєте Зберегти" +} \ No newline at end of file diff --git a/public/language/uk/admin/appearance/skins.json b/public/language/uk/admin/appearance/skins.json new file mode 100644 index 0000000000..cc37055d8d --- /dev/null +++ b/public/language/uk/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Завантаження стилів...", + "homepage": "Головна", + "select-skin": "Обрати стиль", + "current-skin": "Поточний стиль", + "skin-updated": "Стиль оновлено", + "applied-success": "Стиль %1 було успішно примінено", + "revert-success": "Стиль повернуто до базових кольорів" +} \ No newline at end of file diff --git a/public/language/uk/admin/appearance/themes.json b/public/language/uk/admin/appearance/themes.json new file mode 100644 index 0000000000..effaee2031 --- /dev/null +++ b/public/language/uk/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Перевірка встановлених тем...", + "homepage": "Головна", + "select-theme": "Обрати тему", + "current-theme": "Поточна тема", + "no-themes": "Не знайдено вставлених тем", + "revert-confirm": "Ви впевнені, що бажаєте відновити тему NodeBB по замовчуванню?", + "theme-changed": "Тему змінено", + "revert-success": "Ви успішно повернули NodeBB до теми по замовчуванню.", + "restart-to-activate": "Будь-ласка, перебудуйте та перезавантажте ваш NodeBB, щоб повністю активувати цю тему." +} \ No newline at end of file diff --git a/public/language/uk/admin/dashboard.json b/public/language/uk/admin/dashboard.json new file mode 100644 index 0000000000..62eee08e51 --- /dev/null +++ b/public/language/uk/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Трафік форуму", + "page-views": "Перегляди сторінок", + "unique-visitors": "Унікальні відвідувачі", + "logins": "Logins", + "new-users": "New Users", + "posts": "Пости", + "topics": "Теми", + "page-views-seven": "Останні 7 Днів", + "page-views-thirty": "Останні 30 Днів", + "page-views-last-day": "Останні 24 Години", + "page-views-custom": "Заданий Період", + "page-views-custom-start": "Початок Періоду", + "page-views-custom-end": "Кінець Періоду", + "page-views-custom-help": "Вкажіть календарний період, за який ви хочете побачити переглянуті сторінки. Якщо ви не можете використати селектор дат, допустимий формат дати YYYY-MM-DD", + "page-views-custom-error": "Будь-ласка вкажіть календарний період у форматі YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "Увесь час", + + "updates": "Оновлень", + "running-version": "У вас працює NodeBB v%1.", + "keep-updated": "Регулярно перевіряйте, що ваш NodeBB знаходиться в актуальному стані, щоб мати останні патчі та виправлення.", + "up-to-date": "

Ваша версія актуальна

", + "upgrade-available": "

Було випущено нову версію (v%1). Подумайте про оновлення вашого NodeBB.

", + "prerelease-upgrade-available": "

Це застаріла до-релізна версія NodeBB. Було випущено нову версію (v%1). Подумайте про оновлення вашого NodeBB.

", + "prerelease-warning": "

Це пре-релізна версія NodeBB. Можуть виникати неочікувані помилки.

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "Форум працює в режимі розробки. Форум потенційно може бути незахищеним, будь-ласка повідомте вашого системного адміністратора.", + "latest-lookup-failed": "

Помилка при спробі перевірки останньої версії NodeBB

", + + "notices": "Сповіщення", + "restart-not-required": "Перезавантаження не потрібне", + "restart-required": "Потрібне перезавантаження", + "search-plugin-installed": "Пошуковий плагін встановлено", + "search-plugin-not-installed": "Пошуковий плагін не встановлено", + "search-plugin-tooltip": "Встановіть пошуковий плагін зі сторінки плагінів, що активувати пошуковий функціонал", + + "control-panel": "Керування системою", + "rebuild-and-restart": "Перебудувати & Перезавантажити", + "restart": "Перезавантажити", + "restart-warning": "Перебудування або перезапуск вашого NodeBB призведе до втрати всіх існуючих з'єднань протягом декількох секунд.", + "restart-disabled": "Перебудування та перезапуск вашого NodeBB вимкнено, оскільки ви, здається, не запускаєте його через відповідний демон.", + "maintenance-mode": "Режим обслуговування", + "maintenance-mode-title": "Натисніть тут, щоб налаштувати режим обслуговування NodeBB", + "realtime-chart-updates": "Оновлення графіків в реальному часі", + + "active-users": "Активні користувачі", + "active-users.users": "Користувачі", + "active-users.guests": "Гості", + "active-users.total": "Разом", + "active-users.connections": "З'єднання", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "Зареєстровані", + + "user-presence": "Присутність користувача", + "on-categories": "На списку категорій", + "reading-posts": "Читають пости", + "browsing-topics": "Переглядають теми", + "recent": "Недавні", + "unread": "Непрочитані", + + "high-presence-topics": "Теми з високою присутністю", + "popular-searches": "Popular Searches", + + "graphs.page-views": "Перегляди сторінок", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "Унікальні відвідувачі", + "graphs.registered-users": "Зареєстровані користувачі", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Останнє перезавантаження", + "no-users-browsing": "Немає користувачів онлайн", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/uk/admin/development/info.json b/public/language/uk/admin/development/info.json new file mode 100644 index 0000000000..c28dc7bb38 --- /dev/null +++ b/public/language/uk/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 вузлів відповіли за %2мс!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Зареєстровано", + "sockets": "Сокети", + "guests": "Гостей", + + "info": "Інфо" +} \ No newline at end of file diff --git a/public/language/uk/admin/development/logger.json b/public/language/uk/admin/development/logger.json new file mode 100644 index 0000000000..b4887c2022 --- /dev/null +++ b/public/language/uk/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Налаштування логування", + "description": "Увімкнувши ці налаштування, ви будете отримувати логи в ваш термінал. Якщо ви вкажете шлях, логи натомість буде збережено в файл. HTTP логування корисно для сбору статистики про те хто, коли і до якого вмісту отримують доступ люди на вашому форумі. Додатково до логування HTTP запитів ми також можемо логувати події socket.io. Логування socket.io, в комбінації з redis-cli моніторингом може бути дуже зручним для вивчення внутрішньої роботи NodeBB.", + "explanation": "Просто поставте/зніміть прапорець, щоб вімкнути/вимкнути логування на льоту. Перезавантаження не потрібне. ", + "enable-http": "Увімкнути логування HTTP", + "enable-socket": "Увімкнути логування подій socket.io", + "file-path": "Шлях до лог файлу", + "file-path-placeholder": "/шлях/до/логу/file.log ::: залишити пустим для виводу в термінал", + + "control-panel": "Панель керування логуванням", + "update-settings": "Оновити налаштування логування" +} \ No newline at end of file diff --git a/public/language/uk/admin/extend/plugins.json b/public/language/uk/admin/extend/plugins.json new file mode 100644 index 0000000000..2dada6ef4f --- /dev/null +++ b/public/language/uk/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "Встановлені", + "active": "Активні", + "inactive": "Неактивні", + "out-of-date": "Застарілі", + "none-found": "Плагінів не знайдено.", + "none-active": "Немає активних плагінів", + "find-plugins": "Знайти плагіни", + + "plugin-search": "Пошук плагінів", + "plugin-search-placeholder": "Шукати плагіни...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "Впорядкувати плагіни", + "order-active": "Впорядкувати активні плагіни", + "dev-interested": "Зацікавлені в написанні плагінів для NodeBB?", + "docs-info": "Повну документацію щодо авторингу плагінів можна знайти на Порталі NodeBB Docs.", + + "order.description": "Певні плагіни працють краще будучи ініціалізованими до/після інших плагінів.", + "order.explanation": "Плагіни вантажаться у заданому тут порядку, згори до низу", + + "plugin-item.themes": "Теми", + "plugin-item.deactivate": "Деактивувати", + "plugin-item.activate": "Активувати", + "plugin-item.install": "Встановити", + "plugin-item.uninstall": "Видалити", + "plugin-item.settings": "Налаштування", + "plugin-item.installed": "Встановлено", + "plugin-item.latest": "Остання", + "plugin-item.upgrade": "Оновити", + "plugin-item.more-info": "Більше інформації:", + "plugin-item.unknown": "Невідомо", + "plugin-item.unknown-explanation": "Стан цього плагіну неможливо визначити, можливо, через помилку налаштування.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "Плагін увімкнуто", + "alert.disabled": "Плагін вимкнуто", + "alert.upgraded": "Плагін оновлено", + "alert.installed": "Плагін встановлено", + "alert.uninstalled": "Плагін видалено", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "Плагін успішно деактивовано", + "alert.upgrade-success": "Будь-ласка, перебудуйте та перезавантажте ваш NodeBB, щоб закінчити оновлення цього плагіну.", + "alert.install-success": "Плагін успішно встановлено, будь-ласка активуйте його.", + "alert.uninstall-success": "Плагін успішно деактивовано та видалено.", + "alert.suggest-error": "

NodeBB не вдалося зв'язатися з менеджером пакетів, приступити до установки останньої версії?

Відповідь сервера (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB не вдалося зв'язатися з менеджером пакетів, оновлення наразі не рекомендується.

", + "alert.incompatible": "

Ваша версія NodeBB (v%1) дозволяє оновлення цього плагіну лише до v%2. Будь ласка, оновіть NodeBB, щоб встановити новішу версію цього плагіну.

", + "alert.possibly-incompatible": "

Інформацію про сумісність не знайдено

У цьому плагіні не вказано сумісну версію NodeBB. Коректна робота не гарантується.

Якщо NodeBB перестане коректно стартувати, виконайте:

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

Продовжити встановлення останньої версії цього плагіну?

", + "alert.reorder": "Плагіни Пересортовані", + "alert.reorder-success": "Будь-ласка перебудуйте та перезавантажте ваш NodeBB, щоб закінчити процес.", + + "license.title": "Інформація про ліцензію плагіна", + "license.intro": "Цей плагін %1 ліцензований відповідно до %2. Будь-ласка прочитайте та зрозумійте умови ліцензії перед активацією цього плагіну.", + "license.cta": "Ви бажаєте активувати цей плагін?" +} diff --git a/public/language/uk/admin/extend/rewards.json b/public/language/uk/admin/extend/rewards.json new file mode 100644 index 0000000000..47a0dd459d --- /dev/null +++ b/public/language/uk/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Нагороди", + "condition-if-users": "Якщо у користувача", + "condition-is": "Є:", + "condition-then": "Тоді:", + "max-claims": "Скільки разів можна отримати цю нагороду", + "zero-infinite": "Уведіть 0 для нескінченності", + "delete": "Видалити", + "enable": "Увімкнути", + "disable": "Вимкнути", + + "alert.delete-success": "Нагороду успішно видалено", + "alert.no-inputs-found": "Невірна нагорода — поля пусті!", + "alert.save-success": "Нагороду успішно збережено" +} \ No newline at end of file diff --git a/public/language/uk/admin/extend/widgets.json b/public/language/uk/admin/extend/widgets.json new file mode 100644 index 0000000000..8d06a68e2a --- /dev/null +++ b/public/language/uk/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Доступні віджети", + "explanation": "Оберіть віджет із випадаючого меню і перетягніть його в область зліва.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Клонувати віджет з", + "containers.available": "Доступні контейнери", + "containers.explanation": "Перетягніть поверх будь-якого активного віджету", + "containers.none": "Ніякий", + "container.well": "Криниця", + "container.jumbotron": "Екран", + "container.panel": "Панель", + "container.panel-header": "Заголовок панелі", + "container.panel-body": "Тіло панелі", + "container.alert": "Тривога", + + "alert.confirm-delete": "Ви впевнені, що бажаєте видалити цей віджет?", + "alert.updated": "Віджети оновлено", + "alert.update-success": "Віджети успішно оновлено", + "alert.clone-success": "Віджети успішно клоновано", + + "error.select-clone": "Будь ласка, виберіть сторінку для клонування з", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/admins-mods.json b/public/language/uk/admin/manage/admins-mods.json new file mode 100644 index 0000000000..04984ca7bd --- /dev/null +++ b/public/language/uk/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Адміністратори", + "global-moderators": "Глобальні Модератори", + "moderators": "Moderators", + "no-global-moderators": "Відсутні Глобальні Модератори", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "Відсутні Модератори", + "add-administrator": "Додати Адміністратора", + "add-global-moderator": "Додати Глобального Модератора", + "add-moderator": "Додати Модератора" +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/categories.json b/public/language/uk/admin/manage/categories.json new file mode 100644 index 0000000000..6198af0464 --- /dev/null +++ b/public/language/uk/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Налаштування категорій", + "privileges": "Права", + + "name": "Назва категорії", + "description": "Опис категорії", + "bg-color": "Колір фону", + "text-color": "Колір тексту", + "bg-image-size": "Розмір фонового зображення", + "custom-class": "Користувацький клас", + "num-recent-replies": "Кількість свіжих відповідей", + "ext-link": "Зовнішнє посилання", + "subcategories-per-page": "Subcategories per page", + "is-section": "Вважати цю категорію розділом", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Завантажити зображення", + "delete-image": "Видалити", + "category-image": "Зображення категорії", + "parent-category": "Батьківська категорія", + "optional-parent-category": "(Необов'язково) Батьківська категорія", + "top-level": "Top Level", + "parent-category-none": "(Жодна)", + "copy-parent": "Copy Parent", + "copy-settings": "Взяти налаштування з", + "optional-clone-settings": "(Необов'язково) Взяти налаштування з категорії", + "clone-children": "Clone Children Categories And Settings", + "purge": "Видалити категорію", + + "enable": "Увімкнути", + "disable": "Вимкнути", + "edit": "Редагувати", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Вибрати категорію", + "set-parent-category": "Встановити батьківську категорію", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Увага: Налаштування прав миттєво вступають у дію. Необов'язкового зберігати категорію після зміни цих налаштувань.", + "privileges.section-viewing": "Права перегляду", + "privileges.section-posting": "Права постингу", + "privileges.section-moderation": "Права модерації", + "privileges.section-other": "Other", + "privileges.section-user": "Користувач", + "privileges.search-user": "Додати користувача", + "privileges.no-users": "Для цієї категорії не задано особливих прав.", + "privileges.section-group": "Група", + "privileges.group-private": "Це приватна група", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Додати групу", + "privileges.copy-to-children": "Копіювати під-категоріям", + "privileges.copy-from-category": "Копіювати з категорії", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "Надаючи права групі registered-users ви неявно надаєте ті ж самі права всім іншим групам, навіть якщо вони явно не відмічені. Це трапляється тому, що всі користувачі входять до групи registered-users.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Повернутися до списку категорій", + "analytics.title": "Аналітика по категорії \"%1\"", + "analytics.pageviews-hourly": "Рисунок 1 — Погодинна кількість переглядів категорії", + "analytics.pageviews-daily": "Рисунок 2 — Щоденна кількість переглядів категорії", + "analytics.topics-daily": "Рисунок 3 — Щоденна кількість створених тем у категорії", + "analytics.posts-daily": "Рисунок 4 — Щоденна кількість постів у категорії", + + "alert.created": "Створена", + "alert.create-success": "Категорія успішно створена!", + "alert.none-active": "У вас немає активних категорій.", + "alert.create": "Створити категорію", + "alert.confirm-purge": "

Ви впевнені, що бажаєте стерти категорію \"%1\"?

Увага! Всі теми та пости в цій категорії буде знищено!

Стирання категорії видалить всі теми та пости і видалить категорію з бази данних. Якщо ви хотіли тимчасово видалити категорію, вам, натомість, варто її просто \"вимкнути\".

", + "alert.purge-success": "Категорію стерто!", + "alert.copy-success": "Налаштування скопійовано!", + "alert.set-parent-category": "Встановити батьківську категорію", + "alert.updated": "Категорії оновлено", + "alert.updated-success": "ID категорій %1 успішно оновлено.", + "alert.upload-image": "Завантажити зображення категорії", + "alert.find-user": "Знайти користувача", + "alert.user-search": "Шукайте користувача тут...", + "alert.find-group": "Знайти групу", + "alert.group-search": "Шукайте групу тут...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "Згорнути всі", + "expand-all": "Розгорнути всі", + "disable-on-create": "Disable on create", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/digest.json b/public/language/uk/admin/manage/digest.json new file mode 100644 index 0000000000..38c634d1f6 --- /dev/null +++ b/public/language/uk/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "A listing of digest delivery stats and times is displayed below.", + "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 SendGrid.", + + "user": "User", + "subscription": "Subscription Type", + "last-delivery": "Last successful delivery", + "default": "System default", + "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", + "resend": "Resend Digest", + "resend-all-confirm": "Are you sure you wish to manually execute this digest run?", + "resent-single": "Manual digest resend completed", + "resent-day": "Daily digest resent", + "resent-week": "Weekly digest resent", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "Monthly digest resent", + "null": "Never", + "manual-run": "Manual digest run:", + + "no-delivery-data": "No delivery data found" +} diff --git a/public/language/uk/admin/manage/groups.json b/public/language/uk/admin/manage/groups.json new file mode 100644 index 0000000000..807758b546 --- /dev/null +++ b/public/language/uk/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Назва групи", + "badge": "Badge", + "properties": "Properties", + "description": "Опис групи", + "member-count": "Кількість Учасників", + "system": "System", + "hidden": "Hidden", + "private": "Private", + "edit": "Редагувати", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "Пошук", + "create": "Створити групу", + "description-placeholder": "Короткий опис вашої групи", + "create-button": "Створити", + + "alerts.create-failure": "Ой

Виникла проблема при створені вашої групи. Будь ласка, спробуйте пізніше!

", + "alerts.confirm-delete": "Ви впевнені, що бажаєте видалити цю групу?", + + "edit.name": "Назва", + "edit.description": "Опис", + "edit.user-title": "Назва учасників", + "edit.icon": "Іконка групи", + "edit.label-color": "Колір іконки групи", + "edit.text-color": "Group Text Color", + "edit.show-badge": "Показувати бейдж", + "edit.private-details": "Якщо увімкнено, приєднання до групи вимагає підтвердження власника.", + "edit.private-override": "Увага: Приватні групи вимкнено на системному рівні, ця опція нічого не робить.", + "edit.disable-join": "Disable join requests", + "edit.disable-leave": "Disallow users from leaving the group", + "edit.hidden": "Прихована", + "edit.hidden-details": "Якщо увімкнено, групу не буде видно в загальному списку і запрошення користувачів потрібно буде здійснювати вручну", + "edit.add-user": "Додати користувача до групи", + "edit.add-user-search": "Пошук користувачів", + "edit.members": "Список учасників", + "control-panel": "Панель керування групами", + "revert": "Повернути", + + "edit.no-users-found": "Користувачів не знайдено", + "edit.confirm-remove-user": "Ви точно бажаєте видалити цього користувача?", + "edit.save-success": "Зміни збережено!" +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/privileges.json b/public/language/uk/admin/manage/privileges.json new file mode 100644 index 0000000000..caff1bef31 --- /dev/null +++ b/public/language/uk/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Глобальні", + "admin": "Admin", + "group-privileges": "Group Privileges", + "user-privileges": "User Privileges", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "Чат", + "upload-images": "Завантаження Зображень", + "upload-files": "Завантаження Файлів", + "signature": "Підпис", + "ban": "Бан", + "mute": "Mute", + "invite": "Invite", + "search-content": "Шукати Зміст", + "search-users": "Шукати Користувачів", + "search-tags": "Шукати Теги", + "view-users": "View Users", + "view-tags": "View Tags", + "view-groups": "View Groups", + "allow-local-login": "Local Login", + "allow-group-creation": "Group Create", + "view-users-info": "View Users Info", + "find-category": "Знайти Категорію", + "access-category": "Доступ до Категорії", + "access-topics": "Доступ до Тем", + "create-topics": "Створювати Теми", + "reply-to-topics": "Відповідати на Теми", + "schedule-topics": "Schedule Topics", + "tag-topics": "Тегувати Теми", + "edit-posts": "Редагувати Пости", + "view-edit-history": "Переглядати Історію Редагування", + "delete-posts": "Видаляти Пости", + "view_deleted": "View Deleted Posts", + "upvote-posts": "Голосувати \"За\" Пости", + "downvote-posts": "Голосувати \"Проти\" Постів", + "delete-topics": "Видаляти Теми", + "purge": "Очищувати", + "moderate": "Модерувати", + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "Settings", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/registration.json b/public/language/uk/admin/manage/registration.json new file mode 100644 index 0000000000..4d0a80bb45 --- /dev/null +++ b/public/language/uk/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Черга", + "description": "В черзі на реєстрацію немає користувачів.
Щоб активувати цю функцію перейдіть до Налаштування → користувачів та реєстрації і встановіть Тип реєстрації на \"Підтвердження адміна\".", + + "list.name": "Ім'я", + "list.email": "Електронна пошта", + "list.ip": "IP-адреса", + "list.time": "Час", + "list.username-spam": "Частота: %1 Появи: %2 Впевненість: %3", + "list.email-spam": "Частота: %1 Появи: %2", + "list.ip-spam": "Частота: %1 Появи: %2", + + "invitations": "Запрошення", + "invitations.description": "Нижче наведено список надісланих запрошень. Використовуйте Ctrl+F щоб здійснити пошук за поштою чи іменем користувача.

Ім'я користувача буде показано справа від електронної пошти для тих користувачів, що використали свої запрошення.", + "invitations.inviter-username": "Ім'я запрошувача", + "invitations.invitee-email": "Електронна пошта запрошуваного", + "invitations.invitee-username": "Ім'я запрошуваного (якщо зареєстрований)", + + "invitations.confirm-delete": "Ви впевнені, що бажаєте видалити це запрошення?" +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/tags.json b/public/language/uk/admin/manage/tags.json new file mode 100644 index 0000000000..a596fda200 --- /dev/null +++ b/public/language/uk/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Жодна тема на вашому форумі ще не має тегів.", + "bg-color": "Колір фону", + "text-color": "Колір тексту", + "description": "Select tags by clicking or dragging, use CTRL to select multiple tags.", + "create": "Створити тег", + "modify": "Змінити тег", + "rename": "Перейменувати теги", + "delete": "Видалити вибрані теги", + "search": "Пошук тегів...", + "settings": "Tags Settings", + "name": "Назва тегу", + + "alerts.editing": "Editing tag(s)", + "alerts.confirm-delete": "Бажаєте видалити декілька тегів?", + "alerts.update-success": "Тег оновлено!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/uploads.json b/public/language/uk/admin/manage/uploads.json new file mode 100644 index 0000000000..72a695ccdc --- /dev/null +++ b/public/language/uk/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Upload File", + "filename": "Filename", + "usage": "Post Usage", + "orphaned": "Orphaned", + "size/filecount": "Size / Filecount", + "confirm-delete": "Do you really want to delete this file?", + "filecount": "%1 files", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/uk/admin/manage/users.json b/public/language/uk/admin/manage/users.json new file mode 100644 index 0000000000..79bf9905e8 --- /dev/null +++ b/public/language/uk/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Користувачі", + "edit": "Actions", + "make-admin": "Створити адміна", + "remove-admin": "Видалити адміна", + "validate-email": "Підтвердити електронну адресу", + "send-validation-email": "Надіслати підтверджувального листа", + "password-reset-email": "Надіслати скидання паролю", + "force-password-reset": "Force Password Reset & Log User Out", + "ban": "Забанити", + "temp-ban": "Забанити тимчасово", + "unban": "Розбанити", + "reset-lockout": "Скинути блокування", + "reset-flags": "Скинути скарги", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "Скачати CSV", + "manage-groups": "Manage Groups", + "add-group": "Add Group", + "create": "Create User", + "invite": "Invite by Email", + "new": "Новий користувач", + "filter-by": "Filter by", + "pills.unvalidated": "Не підтверджені", + "pills.validated": "Validated", + "pills.banned": "Забанені", + + "50-per-page": "50 per page", + "100-per-page": "100 per page", + "250-per-page": "250 per page", + "500-per-page": "500 per page", + + "search.uid": "За ID користувача", + "search.uid-placeholder": "Введіть ID користувача для пошуку", + "search.username": "За іменем", + "search.username-placeholder": "Введіть ім'я для пошуку", + "search.email": "За поштою", + "search.email-placeholder": "Введіть пошту для пошуку", + "search.ip": "За IP адресою", + "search.ip-placeholder": "Введіть IP адресу для пошуку", + "search.not-found": "Користувача не знайдено!", + + "inactive.3-months": "3 місяці", + "inactive.6-months": "6 місяців", + "inactive.12-months": "12 місяців", + + "users.uid": "uid", + "users.username": "ім'я", + "users.email": "email", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "постів", + "users.reputation": "репутація", + "users.flags": "скарги", + "users.joined": "приєднався", + "users.last-online": "востаннє в мережі", + "users.banned": "забанений", + + "create.username": "Ім'я користувача", + "create.email": "Електронна пошта", + "create.email-placeholder": "Електронна пошта користувача", + "create.password": "Пароль", + "create.password-confirm": "Пароль ще раз", + + "temp-ban.length": "Length", + "temp-ban.reason": "Причина (необов'язково)", + "temp-ban.hours": "Години", + "temp-ban.days": "Дні", + "temp-ban.explanation": "Уведіть тривалість бану. 0 означатиме постійний бан.", + + "alerts.confirm-ban": "Ви впевнені, що бажаєте забанити цього користувача напостійно?", + "alerts.confirm-ban-multi": "Ви впевнені, що бажаєте забанити цих користувачів напостійно?", + "alerts.ban-success": "Користувачів забанено!", + "alerts.button-ban-x": "Забанити %1 користувачів", + "alerts.unban-success": "Користувачів забанено!", + "alerts.lockout-reset-success": "Блокування скинуто!", + "alerts.flag-reset-success": "Скарги скинуто!", + "alerts.no-remove-yourself-admin": "Ви не можете видалити себе як адміна!", + "alerts.make-admin-success": "Користувач зараз є адміністратором.", + "alerts.confirm-remove-admin": "Ви дійсно хочете видалити цього адміністратора?", + "alerts.remove-admin-success": "Користувач більше не є адміністратором.", + "alerts.make-global-mod-success": "Користувач зараз є глобальним модератором.", + "alerts.confirm-remove-global-mod": "Ви дійсно хочете вилучити цього глобального модератора?", + "alerts.remove-global-mod-success": "Користувач більше не є глобальним модератором.", + "alerts.make-moderator-success": "Користувач зараз є модератором.", + "alerts.confirm-remove-moderator": "Ви дійсно хочете видалити цього модератора?", + "alerts.remove-moderator-success": "Користувач більше не є модератором.", + "alerts.confirm-validate-email": "Ви точно бажаєте підтвердити електронні пошти цих користувачів?", + "alerts.confirm-force-password-reset": "Are you sure you want to force the password reset and log out these user(s)?", + "alerts.validate-email-success": "Електронні пошти підтверджено", + "alerts.validate-force-password-reset-success": "User(s) passwords have been reset and their existing sessions have been revoked.", + "alerts.password-reset-confirm": "Ви точно бажаєте скинути паролі цим користувачам електронною поштою?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "Користувачів видалено!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "Створити користувача", + "alerts.button-create": "Створити", + "alerts.button-cancel": "Скасувати", + "alerts.error-passwords-different": "Паролі мають співпадати!", + "alerts.error-x": "Помилка

%1

", + "alerts.create-success": "Користувача створено!", + + "alerts.prompt-email": "Emails: ", + "alerts.email-sent-to": "Запрошення надіслано за адресою %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/uk/admin/menu.json b/public/language/uk/admin/menu.json new file mode 100644 index 0000000000..8a44556a55 --- /dev/null +++ b/public/language/uk/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "Загальні", + + "section-manage": "Керування", + "manage/categories": "Категорії", + "manage/privileges": "Права", + "manage/tags": "Теги", + "manage/users": "Користувачі", + "manage/admins-mods": "Адміністратори та моди", + "manage/registration": "Черга реєстрації", + "manage/post-queue": "Черга Постів", + "manage/groups": "Групи", + "manage/ip-blacklist": "Чорний список IP-адрес", + "manage/uploads": "Завантаження", + "manage/digest": "Digests", + + "section-settings": "Налаштування", + "settings/general": "Загальні", + "settings/homepage": "Home Page", + "settings/navigation": "Navigation", + "settings/reputation": "Reputation & Flags", + "settings/email": "Електронна пошта", + "settings/user": "Users", + "settings/group": "Groups", + "settings/guest": "Гості", + "settings/uploads": "Завантаження", + "settings/languages": "Languages", + "settings/post": "Posts", + "settings/chat": "Chats", + "settings/pagination": "Пагінація", + "settings/tags": "Теги", + "settings/notifications": "Сповіщення", + "settings/api": "API Access", + "settings/sounds": "Sounds", + "settings/social": "Social", + "settings/cookies": "Куки", + "settings/web-crawler": "Роботи", + "settings/sockets": "Сокети", + "settings/advanced": "Розширені", + + "settings.page-title": "Налаштування %1", + + "section-appearance": "Зовнішній вигляд", + "appearance/themes": "Теми", + "appearance/skins": "Стилі", + "appearance/customise": "Користувацький вміст (HTML/JS/CSS)", + + "section-extend": "Розширити", + "extend/plugins": "Плагіни", + "extend/widgets": "Віджети", + "extend/rewards": "Нагороди", + + "section-social-auth": "Авторизація соцмережами", + + "section-plugins": "Плагіни", + "extend/plugins.install": "Встановити плагіни", + + "section-advanced": "Розширені", + "advanced/database": "База даних", + "advanced/events": "Події", + "advanced/hooks": "Hooks", + "advanced/logs": "Логи", + "advanced/errors": "Помилки", + "advanced/cache": "Кеш", + "development/logger": "Логування", + "development/info": "Інформація", + + "rebuild-and-restart-forum": "Перебудувати & Перезавантажити Форум", + "restart-forum": "Перезавантажити форум", + "logout": "Вийти", + "view-forum": "Переглянути форум", + + "search.placeholder": "Search settings", + "search.no-results": "Без результатів...", + "search.search-forum": "Шукати на форумі ", + "search.keep-typing": "Для результатів, надрукуйте ще...", + "search.start-typing": "Для результатів, почніть друкувати...", + + "connection-lost": "З'єднання з %1 було втрачено, намагаємось під'єднатись знов...", + + "alerts.version": "Працює версія NodeBB v%1", + "alerts.upgrade": "Оновити до v%1" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/advanced.json b/public/language/uk/admin/settings/advanced.json new file mode 100644 index 0000000000..58e45a1bfb --- /dev/null +++ b/public/language/uk/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Режим обслуговування", + "maintenance-mode.help": "Коли форум знаходиться в режимі обслуговування, всі запити перенаправляються на статичну сторінку. Адміністратори, в свою чергу, не перенаправляються і можуть відвідувати сайт у звичному режимі.", + "maintenance-mode.status": "Maintenance Mode Status Code", + "maintenance-mode.message": "Повідомлення обслуговування", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "Заголовки", + "headers.allow-from": "Задати ALLOW-FROM для розміщення NodeBB в iFrame", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "Налаштувати заголовок \"Powered By\", котрий відправляє NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin Regular Expression", + "headers.acao-help": "Щоб заборонити доступ до всіх сайтів, залиште незаповненим", + "headers.acao-regex-help": "Enter regular expressions here to match dynamic origins. To deny access to all sites, leave empty", + "headers.acac": "Доступ-Контроль-Дозвіл-Права", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Strict Transport Security", + "hsts.enabled": "Enabled HSTS (recommended)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "Include subdomains in HSTS header", + "hsts.preload": "Allow preloading of HSTS header", + "hsts.help": "If enabled, an HSTS header will be set for this site. You can elect to include subdomains and preloading flags in your header. If in doubt, you can leave these unchecked. More information ", + "traffic-management": "Керування трафіком", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "Увімкнути керування трафіком", + "traffic.event-lag": "Затримка циклу обробки дій (в мілісекундах)", + "traffic.event-lag-help": "Зменшення цього значення прискорює час завантаження сторінок, але в той же час більше користувачів будуть отримувати повідомлення про \"надмірне навантаження\". (Потребує перезавантаження)", + "traffic.lag-check-interval": "Інтервал перевірки (в мілісекундах)", + "traffic.lag-check-interval-help": "Зменшення цього значення робить NodeBB більш чутливим до піків навантаження, але може зробити перевірку занадто чутливою. (Потребує перезавантаження)", + + "sockets.settings": "WebSocket Settings", + "sockets.max-attempts": "Max Reconnection Attempts", + "sockets.default-placeholder": "Default: %1", + "sockets.delay": "Reconnection Delay", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/api.json b/public/language/uk/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/uk/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/chat.json b/public/language/uk/admin/settings/chat.json new file mode 100644 index 0000000000..af017b5541 --- /dev/null +++ b/public/language/uk/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Налаштування чату", + "disable": "Вимкнути чат", + "disable-editing": "Вимкнути редагування/видалення повідомлень чату", + "disable-editing-help": "Адміністратори на модератори звільнені від цього обмеження", + "max-length": "Максимальна довжина повідомлення", + "max-room-size": "Максимальна кількість людей у кімнаті", + "delay": "Час між повідомленнями в мілісекундах", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", + "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/cookies.json b/public/language/uk/admin/settings/cookies.json new file mode 100644 index 0000000000..01836cb91f --- /dev/null +++ b/public/language/uk/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Згода ЄС", + "consent.enabled": "Увімкнено", + "consent.message": "Текст сповіщення", + "consent.acceptance": "Текст погодження", + "consent.link-text": "Текст посилання на політику", + "consent.link-url": "Policy Link URL", + "consent.blank-localised-default": "Залишити пустими, щоб використати стандартні локалізовані тексти NodeBB", + "settings": "Налаштування", + "cookie-domain": "Домен куки сесії", + "max-user-sessions": "Max active sessions per user", + "blank-default": "Залишити пустим для налаштувань за замовченням" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/email.json b/public/language/uk/admin/settings/email.json new file mode 100644 index 0000000000..30dbc77964 --- /dev/null +++ b/public/language/uk/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Налаштування електронної пошти", + "address": "Електронна пошта", + "address-help": "Отримувачі будуть бачити цю адресу в полях \"From\" та \"Reply To\".", + "from": "Ім'я відправника", + "from-help": "Ім'я відправника, що буде показано в електронних листах", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "Протокол SMTP", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "Ви можете обрати поштовий сервіс зі списку або використати ваш власний сервіс.", + "smtp-transport.service": "Оберіть сервіс", + "smtp-transport.service-custom": "Власний Сервіс", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "Сервер SMTP", + "smtp-transport.port": "Порт SMTP", + "smtp-transport.security": "Connection security", + "smtp-transport.security-encrypted": "Encrypted", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "None", + "smtp-transport.username": "Ім'я користувача", + "smtp-transport.username-help": "Для сервісу Gmail, вкажіть тут повну електронну адресу, особливо якщо ви використовуєте керований домен Google Apps.", + "smtp-transport.password": "Пароль", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "Редагувати шаблон листа", + "template.select": "Обрати шаблон листа", + "template.revert": "Повернути до оригіналу", + "testing": "Тестування листа", + "testing.select": "Оберіть шаблон листа", + "testing.send": "Надіслати тестового листа", + "testing.send-help": "Тестовий лист було направлено на адресу поточного користувача.", + "subscriptions": "Email Digests", + "subscriptions.disable": "Disable email digests", + "subscriptions.hour": "Година дайджесту", + "subscriptions.hour-help": "Вкажіть, будь ласка, годину о котрій кожного дня буде надсилатися дайджест (наприклад 0 — це північ, а 17 — п'ята година вечора). Зверніть увагу, що година визначається згідно налаштувань сервера і може не співпадати з часом вашого комп'ютера.
Приблизний час сервера:
Наступний дайджест заплановано до відправки ", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/uk/admin/settings/general.json b/public/language/uk/admin/settings/general.json new file mode 100644 index 0000000000..964027ac2b --- /dev/null +++ b/public/language/uk/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Налаштування сайту", + "title": "Назва сайту", + "title.short": "Short Title", + "title.short-placeholder": "If no short title is specified, the site title will be used", + "title.url": "Title Link URL", + "title.url-placeholder": "URL заголовку сайту", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "Назва вашої спільноти", + "title.show-in-header": "Показувати заголовок сайту в шапці", + "browser-title": "Заголовок браузера", + "browser-title-help": "Якщо не задано заголовок браузера, буде використано заголовок сайту", + "title-layout": "Структура заголовка", + "title-layout-help": "Визначте як заголовок браузера буде сформовано, наприклад {pageTitle} | {browserTitle}", + "description.placeholder": "Короткий опис вашої спільноти", + "description": "Опис сайту", + "keywords": "Ключові слова сайту", + "keywords-placeholder": "Ключові слова, що описують вашу спільноту, розділені комами", + "logo": "Логотип сайту", + "logo.image": "Зображення", + "logo.image-placeholder": "Шлях до логотипу для відображення в шапці форуму", + "logo.upload": "Завантажити", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "URL логотипу сайту", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "Текст alt", + "log.alt-text-placeholder": "Альтернативний текст для доступності", + "favicon": "Фавіконка", + "favicon.upload": "Завантажити", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "Завантажити", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "Зовнішні посилання", + "outgoing-links.warning-page": "Використовувати сторінку попередження про зовнішній перехід", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "Безпечні домени для пропуску сторінки попередження", + "site-colors": "Site Color Metadata", + "theme-color": "Theme Color", + "background-color": "Background Color", + "background-color-help": "Color used for splash screen background when website is installed as a PWA", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/uk/admin/settings/group.json b/public/language/uk/admin/settings/group.json new file mode 100644 index 0000000000..a634adf133 --- /dev/null +++ b/public/language/uk/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Загальні", + "private-groups": "Приватні групи", + "private-groups.help": "Якщо увімкнено, приєднання до групи вимагає підтвердження власника (По замовчуванню: увімкнено)", + "private-groups.warning": "Увага! Якщо ця опція вимикається і у вас є приватні групи, вони автоматично стають публічними.", + "allow-multiple-badges": "Allow Multiple Badges", + "allow-multiple-badges-help": "Цей прапорець може бути використаний, щоб надати можливість користувачам обирати кілька бейджів груп, необхідна підтримка цієї можливості темою.", + "max-name-length": "Максимальна довжина імені групи", + "max-title-length": "Maximum Group Title Length", + "cover-image": "Зображення обкладинки групи", + "default-cover": "Зображення обкладинки по замовчуванню", + "default-cover-help": "Вкажіть розділені комами зображення обкладинок, що будуть використовуватись по замовчуванню для груп, що не завантажили власних" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/guest.json b/public/language/uk/admin/settings/guest.json new file mode 100644 index 0000000000..1e439e53a6 --- /dev/null +++ b/public/language/uk/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "Дозволити гостьові імена", + "handles.enabled-help": "Ця опція надає додаткове поле, що дозволяє гостям обрати собі ім'я для кожного посту. Якщо вимкнено, вони будуть просто зватися \"Гість\"", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/homepage.json b/public/language/uk/admin/settings/homepage.json new file mode 100644 index 0000000000..f0b146ca8f --- /dev/null +++ b/public/language/uk/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Головна сторінка", + "description": "Вкажіть яку сторінку показувати коли користувач переходить на корньовий URL форуму.", + "home-page-route": "Шлях головної сторінки", + "custom-route": "Користувацький шлях", + "allow-user-home-pages": "Дозволити користувачам власні сторінки", + "home-page-title": "Назва домашньої сторінки (за замовчуванням \"Домашня сторінка\")" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/languages.json b/public/language/uk/admin/settings/languages.json new file mode 100644 index 0000000000..7f2118d887 --- /dev/null +++ b/public/language/uk/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Налаштування мов", + "description": "Мова за замовчуванням задає мову для всіх користувачів, що відвідують форум.
Кожен користувач може перевизначити мову в своїх налаштуваннях акаунта.", + "default-language": "Мова за замовчуванням", + "auto-detect": "Автоматично визначати мову для гостей" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/navigation.json b/public/language/uk/admin/settings/navigation.json new file mode 100644 index 0000000000..8d6f6fad19 --- /dev/null +++ b/public/language/uk/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Іконка:", + "change-icon": "змінити", + "route": "Шлях:", + "tooltip": "Підказка:", + "text": "Текст:", + "text-class": "Класс тексту: необов'язковий", + "class": "Class: optional", + "id": "ID: необов'язковий", + + "properties": "Властивості:", + "groups": "Groups:", + "open-new-window": "Відкривати у новому вікні", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Видалити", + "btn.disable": "Вимкнути", + "btn.enable": "Увімкнути", + + "available-menu-items": "Доступні пункти меню", + "custom-route": "Користувацький шлях", + "core": "ядро", + "plugin": "плагін" +} diff --git a/public/language/uk/admin/settings/notifications.json b/public/language/uk/admin/settings/notifications.json new file mode 100644 index 0000000000..2322bf6836 --- /dev/null +++ b/public/language/uk/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Сповіщення", + "welcome-notification": "Сповіщення \"Ласкаво просимо\"", + "welcome-notification-link": "Посилання для сповіщення \"Ласкаво просимо\"", + "welcome-notification-uid": "Сповіщення \"Ласкаво просимо\" для користувача (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/pagination.json b/public/language/uk/admin/settings/pagination.json new file mode 100644 index 0000000000..559ae24986 --- /dev/null +++ b/public/language/uk/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Налаштування пагінації", + "enable": "Розбивати список тем та постів на сторінки замість нескінченної прокрутки", + "posts": "Post Pagination", + "topics": "Пагінація тем", + "posts-per-page": "Постів на сторінку", + "max-posts-per-page": "Максимум постів на сторінку", + "categories": "Пагінація категорій", + "topics-per-page": "Тем на сторінку", + "max-topics-per-page": "Максимум тем на сторінку", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/post.json b/public/language/uk/admin/settings/post.json new file mode 100644 index 0000000000..1a881baf3a --- /dev/null +++ b/public/language/uk/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Сортування постів", + "sorting.post-default": "Типове сортування постів", + "sorting.oldest-to-newest": "Старі > Нові", + "sorting.newest-to-oldest": "Нові > Старі", + "sorting.most-votes": "Кількість голосів", + "sorting.most-posts": "Кількість постів", + "sorting.topic-default": "Типове сортування тем", + "length": "Довжина посту", + "post-queue": "Post Queue", + "restrictions": "Обмеження постингу", + "restrictions-new": "Нові обмеження користувачів", + "restrictions.post-queue": "Увімкнути чергу постів", + "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", + "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions-new.post-queue": "Увімкнути нові обмеження користувачів", + "restrictions.post-queue-help": "Enabling post queue will put the posts of new users in a queue for approval", + "restrictions-new.post-queue-help": "Enabling new user restrictions will set restrictions on posts created by new users", + "restrictions.seconds-between": "Number of seconds between posts", + "restrictions.seconds-between-new": "Секунд між постами для нових користувачів", + "restrictions.rep-threshold": "Рівень репутації до того, як ці обмеження скасовуються", + "restrictions.seconds-before-new": "Seconds before a new user can make their first post", + "restrictions.seconds-edit-after": "Number of seconds a post remains editable (set to 0 to disable)", + "restrictions.seconds-delete-after": "Number of seconds a post remains deletable (set to 0 to disable)", + "restrictions.replies-no-delete": "Number of replies after users are disallowed to delete their own topics (set to 0 to disable)", + "restrictions.min-title-length": "Мінімальна довжина заголовку", + "restrictions.max-title-length": "Максимальна довжина заголовку", + "restrictions.min-post-length": "Мінімальна довжина посту", + "restrictions.max-post-length": "Максимальна довжина посту", + "restrictions.days-until-stale": "Днів, доки тема не вважатиметься застарілою", + "restrictions.stale-help": "Якщо тема є \"застарілою\", то для користувачів, що бажають відповісти на неї буде показано попередження.", + "timestamp": "Часова мітка", + "timestamp.cut-off": "Обрізка дат (в днях)", + "timestamp.cut-off-help": "Дата і час показуються відносно (тобто \"3 години тому\" / \"5 днів тому\"). Після певного періоду часу, цей текст може бути змінено на звичайну дати (тобто 5 Лис 2016 15:30).
(Типово: 30, або один місяць). Вкажіть 0, щоб завжди показувати дати або залиште пустим, щоб завжди показувати відносний час.", + "timestamp.necro-threshold": "Necro Threshold (in days)", + "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "Пост тизер", + "teaser.last-post": "Останній — показувати останній пост або перший, якщо немає відповідей", + "teaser.last-reply": "Останній — показувати останній пост або \"Немає відповідей\", якщо немає відповідей", + "teaser.first": "Перший", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "Налаштування непрочитаних", + "unread.cutoff": "За скільки днів показувати непрочитані", + "unread.min-track-last": "Мінімальна кількість постів у темі перш ніж відслідковувати останні прочитані", + "recent": "Останні Налаштування", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "Відключити фільтрування тем в ігнорованих категоріях на сторінці /recent", + "signature": "Налаштування підписів", + "signature.disable": "Вимкнути підписи", + "signature.no-links": "Вимкнути посилання в підписах", + "signature.no-images": "Вимкнути зображення в підписах", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "Максимальна довжина підпису", + "composer": "Налаштування редактора", + "composer-help": "Ці налаштування керують функціональністю та виглядом редактору постів для користувачів коли вони створюють нові теми або відповідають на існуючі.", + "composer.show-help": "Показувати вкладку \"Довідка\"", + "composer.enable-plugin-help": "Дозволити плагінам додавати зміст довідки", + "composer.custom-help": "Користувацький текст довідки", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "Відстеження IP", + "ip-tracking.each-post": "Відстежувати IP адреси для кожного посту", + "enable-post-history": "Увімкнути Історію Постів" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/reputation.json b/public/language/uk/admin/settings/reputation.json new file mode 100644 index 0000000000..67de89ab77 --- /dev/null +++ b/public/language/uk/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Налаштування репутації", + "disable": "Вимкнути систему репутації", + "disable-down-voting": "Вимкнути голосування проти", + "votes-are-public": "Всі голоси публічні", + "thresholds": "Допуски активності", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "Мінімальна репутація для голосування проти постів", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "Мінімальна репутація для подання скарг на пости", + "min-rep-website": "Мінімальна репутація для додавання \"Веб-сайту\" до профілю користувача", + "min-rep-aboutme": "Мінімальна репутація для додавання \"Про мене\" до профілю користувача", + "min-rep-signature": "Мінімальна репутація для додавання \"Підпис\" до профілю користувача", + "min-rep-profile-picture": "Мінімальна репутація, щоб додавати \"Зображення Профілю\" до профілю користувача", + "min-rep-cover-picture": "Мінімальна репутація, щоб додавати \"Зображення Обкладинки\" до профілю користувача", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/social.json b/public/language/uk/admin/settings/social.json new file mode 100644 index 0000000000..ce739d8b13 --- /dev/null +++ b/public/language/uk/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Поширення постів", + "info-plugins-additional": "Плагіни можуть доповнювати набір доступних мереж для поширення постів", + "save-success": "Набір мереж для поширення постів успішно збережено!" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/sockets.json b/public/language/uk/admin/settings/sockets.json new file mode 100644 index 0000000000..8ff97b7761 --- /dev/null +++ b/public/language/uk/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Налаштування перепідключення", + "max-attempts": "Максимальна кількість спроб перепідключення", + "default-placeholder": "По замовчуванню: %1", + "delay": "Затримка перепідключення" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/sounds.json b/public/language/uk/admin/settings/sounds.json new file mode 100644 index 0000000000..17214deba0 --- /dev/null +++ b/public/language/uk/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Сповіщення", + "chat-messages": "Повідомлення чату", + "play-sound": "Грати", + "incoming-message": "Вхідне повідомлення", + "outgoing-message": "Вихідне повідомлення", + "upload-new-sound": "Завантажити новий звук", + "saved": "Налаштування зберережні" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/tags.json b/public/language/uk/admin/settings/tags.json new file mode 100644 index 0000000000..0e6538bb5b --- /dev/null +++ b/public/language/uk/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Налаштування тегів", + "link-to-manage": "Manage Tags", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "Мінімальна кількість тегів для теми", + "max-per-topic": "Максимальна кількість тегів для теми", + "min-length": "Мінімальна довжина тега", + "max-length": "Максимальна довжина тега", + "related-topics": "Пов'язані теми", + "max-related-topics": "Максимальна кількість пов'язаних тем до показу (якщо підтримується темою)" +} \ No newline at end of file diff --git a/public/language/uk/admin/settings/uploads.json b/public/language/uk/admin/settings/uploads.json new file mode 100644 index 0000000000..f3192cec5e --- /dev/null +++ b/public/language/uk/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Пости", + "orphans": "Orphaned Files", + "private": "Зробити завантажувані файли приватними", + "strip-exif-data": "Strip EXIF Data", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "File extensions to make private", + "private-uploads-extensions-help": "Enter comma-separated list of file extensions to make private here (e.g. pdf,xls,doc). An empty list means all files are private.", + "resize-image-width-threshold": "Resize images if they are wider than specified width", + "resize-image-width-threshold-help": "(in pixels, default: 1520 pixels, set to 0 to disable)", + "resize-image-width": "Resize images down to specified width", + "resize-image-width-help": "(in pixels, default: 760 pixels, set to 0 to disable)", + "resize-image-quality": "Якість зображення при зміні розміру", + "resize-image-quality-help": "Використовувати нижчу якість зображення для зменшення розміру файла при зміні розміру зображення.", + "max-file-size": "Максимальний розмір файлу (в КіБ)", + "max-file-size-help": "(в кібібайтах, по замовчанню: 2048 КіБ)", + "reject-image-width": "Maximum Image Width (in pixels)", + "reject-image-width-help": "Images wider than this value will be rejected.", + "reject-image-height": "Maximum Image Height (in pixels)", + "reject-image-height-help": "Images taller than this value will be rejected.", + "allow-topic-thumbnails": "Дозволити користувачам завантажувати мініатюри тем", + "topic-thumb-size": "Розмір мініатюри теми", + "allowed-file-extensions": "Допустимі розширення файлів", + "allowed-file-extensions-help": "Вкажіть розширеня файлів розділені комою (наприклад, pdf,xls,doc). Пустий список дає дозвіл на будь-які розширення.", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "Аватарки профілів", + "allow-profile-image-uploads": "Дозволити користувачам завантажувати зображення профілю", + "convert-profile-image-png": "Конвертувати завантажувані зображення профілю в PNG", + "default-avatar": "Аватар за замовчуванням", + "upload": "Завантаження", + "profile-image-dimension": "Розміри зображення профілю", + "profile-image-dimension-help": "(в пікселях, 128 — за замовчуванням)", + "max-profile-image-size": "Максимальний розмір файлу зображення профілю", + "max-profile-image-size-help": "(в кібібайтах, по замовчанню: 256 КіБ)", + "max-cover-image-size": "Максимальний розмір файлу зображення обкладинки", + "max-cover-image-size-help": "(в кібібайтах, по замовчанню: 2,048 КіБ)", + "keep-all-user-images": "Зберігати старі версію зображень аватарки та обкладинки на сервері", + "profile-covers": "Обкладинки профілю", + "default-covers": "Обкладинка за замовчуванням", + "default-covers-help": "Вкажіть розділені комами зображення обкладинок за замовчуванням для акаунтів, що не завантажували власних" +} diff --git a/public/language/uk/admin/settings/user.json b/public/language/uk/admin/settings/user.json new file mode 100644 index 0000000000..cb43123c90 --- /dev/null +++ b/public/language/uk/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Аутентифікація", + "email-confirm-interval": "Користувач не може повторно надіслати підтвердження електронної пошти поки не мине", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Дозволити вхід використовуючи", + "allow-login-with.username-email": "Ім'я користувача або електронну пошту", + "allow-login-with.username": "Тільки ім'я користувача", + "account-settings": "Налаштування акаунту", + "gdpr_enabled": "Enable GDPR consent collection", + "gdpr_enabled_help": "When enabled, all new registrants will be required to explicitly give consent for data collection and usage under the General Data Protection Regulation (GDPR). Note: Enabling GDPR does not force pre-existing users to provide consent. To do so, you will need to install the GDPR plugin.", + "disable-username-changes": "Вимкнути зміну імені користувача", + "disable-email-changes": "Вимкнути зміну електронної пошти", + "disable-password-changes": "Вимкнути зміну пароля", + "allow-account-deletion": "Дозволити видалення акаунту", + "hide-fullname": "Приховати повне ім'я від користувачів", + "hide-email": "Приховати електронну пошту від користувачів", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "Теми", + "disable-user-skins": "Заборонити користувачам обирати стиль сайту", + "account-protection": "Захист акаунту", + "admin-relogin-duration": "Тривалість повторного входу адміністратора (хвилин)", + "admin-relogin-duration-help": "Після встановленої кількості часу для доступу до розділу адміністрування потрібно буде знову ввійти, встановити значення 0 для вимкнення", + "login-attempts": "Кількість спроб входу за годину", + "login-attempts-help": "Якщо кількість спроб входу в акаунт користувача перевищить цей ліміт, акаунт буде заблоковано на задану кількість часу", + "lockout-duration": "Тривалість блокування акаунту (хвилин)", + "login-days": "Скільки днів пам'ятати сесію користувача", + "password-expiry-days": "Скидати пароль користувачам після заданої кількості днів", + "session-time": "Session Time", + "session-time-days": "Days", + "session-time-seconds": "Seconds", + "session-time-help": "These values are used to govern how long a user stays logged in when they check "Remember Me" on login. Note that only one of these values will be used. If there is no seconds value we fall back to days. If there is no days value we default to 14 days.", + "online-cutoff": "Minutes after user is considered inactive", + "online-cutoff-help": "If user performs no actions for this duration, they are considered inactive and they do not receive realtime updates.", + "registration": "Реєстрація користувачів", + "registration-type": "Тип реєстрації", + "registration-approval-type": "Registration Approval Type", + "registration-type.normal": "Стандартна", + "registration-type.admin-approval": "Підтвердження адміна", + "registration-type.admin-approval-ip": "Підтвердження адміна для IP-адрес", + "registration-type.invite-only": "По запрошенню", + "registration-type.admin-invite-only": "По запрошенню адміна", + "registration-type.disabled": "Без реєстрації", + "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "Кількість запрошень на користувача", + "max-invites": "Кількість запрошень на користувача", + "max-invites-help": "0 — без обмежень. Адміни отримуют необмежену кількість.
Працює лише з типом реєстрації \"По запрошенню\".", + "invite-expiration": "Закінчення терміну дії запрошення", + "invite-expiration-help": "Через скільки днів закінчується термін дії запрошень.", + "min-username-length": "Мінімальна довжина імені користувача", + "max-username-length": "Максимальна довжина імені користувача", + "min-password-length": "Мінімальна довжина пароля", + "min-password-strength": "Мінімальна Довжина Паролю", + "max-about-me-length": "Максимальна довжина розділу \"Про мене\"", + "terms-of-use": "Умови користування форумом (Залиште пустим, щоб вимкнути)", + "user-search": "Пошук користувачів", + "user-search-results-per-page": "Кількість результатів до показу", + "default-user-settings": "Налаштування користувача за замовчуванням", + "show-email": "Показувати електронну пошту", + "show-fullname": "Показувати повне ім'я", + "restrict-chat": "Дозволяти чат повідомлення лише від користувачів за якими я стежу", + "outgoing-new-tab": "Відкривати зовнішні посилання у новій вкладці", + "topic-search": "Увімкнути пошук у темах", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "Підписатися на дайджест", + "digest-freq.off": "Ніколи", + "digest-freq.daily": "Щоденно", + "digest-freq.weekly": "Щотижнево", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "Щомісячно", + "email-chat-notifs": "Надсилати листа, коли я не в мережі, якщо приходить чат повідомлення", + "email-post-notif": "Надсилати листа, коли в темах на які я підписаний з'являються відповіді", + "follow-created-topics": "Стежити за темами які ви створюєте", + "follow-replied-topics": "Стежити за темами в котрих ви відповідаєте", + "default-notification-settings": "Стандартні налаштування сповіщень", + "categoryWatchState": "Default category watch state", + "categoryWatchState.watching": "Watching", + "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState.ignoring": "Ignoring" +} diff --git a/public/language/uk/admin/settings/web-crawler.json b/public/language/uk/admin/settings/web-crawler.json new file mode 100644 index 0000000000..f6f3a4b541 --- /dev/null +++ b/public/language/uk/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Налаштування можливості сканування", + "robots-txt": "Користувацький Robots.txt Залишити пустим для налаштувань за замовчуванням", + "sitemap-feed-settings": "Налаштування мапи сайту та стрічки оновлень", + "disable-rss-feeds": "Вимкнути RSS-стрічки", + "disable-sitemap-xml": "Вимкнути Sitemap.xml", + "sitemap-topics": "Кількість тем для показу в мапі сайту", + "clear-sitemap-cache": "Очистити кеш мапи сайту", + "view-sitemap": "Переглянути мапу сайту" +} \ No newline at end of file diff --git a/public/language/uk/category.json b/public/language/uk/category.json new file mode 100644 index 0000000000..5cdbe18dd1 --- /dev/null +++ b/public/language/uk/category.json @@ -0,0 +1,23 @@ +{ + "category": "Категорія", + "subcategories": "Підкатегорія", + "new_topic_button": "Новий запис", + "guest-login-post": "Увійдіть, щоб постити", + "no_topics": " У цій категорії немає жодної теми.
Чому б вам не створити першу?", + "browsing": "переглядають", + "no_replies": "Немає відповідей", + "no_new_posts": "Немає нових постів.", + "watch": "Стежити", + "ignore": "Ігнорувати", + "watching": "Відстежується", + "not-watching": "Не спостерігається", + "ignoring": "Ігнорувати", + "watching.description": "Показати теми в непрочитаних та останніх", + "not-watching.description": "Не показувати теми в непрочитаних, показувати в останніх", + "ignoring.description": "Не показувати теми в непрочитаних і останніх", + "watching.message": "Ви зараз спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій", + "notwatching.message": "Зараз ви не спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій", + "ignoring.message": "Зараз ви ігноруєте оновлення з цієї категорії та всіх її підкатегорій", + "watched-categories": "Переглянуті категорії", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/uk/email.json b/public/language/uk/email.json new file mode 100644 index 0000000000..e5503fab37 --- /dev/null +++ b/public/language/uk/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Тестове поштове повідомлення", + "password-reset-requested": "Отримано запит на скидання пароля!", + "welcome-to": "Ласкаво просимо до %1", + "invite": "Запрошення від %1", + "greeting_no_name": "Привіт", + "greeting_with_name": "Привіт %1", + "email.verify-your-email.subject": "Будь-ласка перевірте вашу електронну адресу", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "Once you confirm this email address, we will replace your current email address with this one (%1).", + "welcome.text1": "Дякуємо за реєстрацію з %1!", + "welcome.text2": "Щоб повністю активувати ваш акаунт, нам потрібно перевірити, що вам належить електронна адреса, яку ви вказали при реєстрації ", + "welcome.text3": "Адміністратор схвалив ваш запит на реєстрацію. Ви можете залогінитись, використовуючи свій пароль та назву акаунту", + "welcome.cta": "Натисніть тут, щоб підтвердити вашу електронну адресу", + "invitation.text1": "%1 запросив вас приєднатися до %2", + "invitation.text2": "Термін дії вашого запрошення закінчиться за %1 днів.", + "invitation.cta": "Натисніть тут щоб створити акаунт.", + "reset.text1": "Ми отримали запит на відновлення вашого паролю, можливо тому, что ви його забули. Якщо вам це не потрібно - проігноруйте цей лист", + "reset.text2": "Щоб продовжити відновлення паролю, будь ласка, перейдіть за посиланням", + "reset.cta": "Натисніть тут щоб скинути Ваш пароль", + "reset.notify.subject": "Пароль змінено", + "reset.notify.text1": "Ми повідомляємо вас, що на %1, ваш пароль було успішно змінено", + "reset.notify.text2": "Якщо ви не авторизували це, повідомте негайно адміністратора", + "digest.latest_topics": "Останні теми від %1", + "digest.top-topics": "Top topics from %1", + "digest.popular-topics": "Popular topics from %1", + "digest.cta": "Натисніть, щоб відвідати %1", + "digest.unsub.info": "Цей дайджест був висланий вам, згідно ваших налаштувань підписки", + "digest.day": "день", + "digest.week": "тиждень", + "digest.month": "місяць", + "digest.subject": "Дайджест для %1", + "digest.title.day": "Ваш щоденний дайджест", + "digest.title.week": "Ваш тижневий дайджест", + "digest.title.month": "Ваш місячний дайджест", + "notif.chat.subject": "Отримане нове повідомлення чату від %1", + "notif.chat.cta": "Натисніть тут, щоб продовжити розмову", + "notif.chat.unsub.info": "Це повідомлення чату було вислано вам, згідно ваших налаштувань підписки", + "notif.post.unsub.info": "Це поштове повідомлення було вислано вам, згідно ваших налаштувань підписки", + "notif.post.unsub.one-click": "Ви також можете відписатись від схожих майбутніх повідомлень, натиснувши тут", + "notif.cta": "На форум", + "notif.cta-new-reply": "Переглянути допис", + "notif.cta-new-chat": "Переглянути чат", + "notif.test.short": "Перевірка сповіщень", + "notif.test.long": "Це перевірка повідомлення про сповіщення.", + "test.text1": "Це пробний лист для верифікації поштової служби. Всі налаштування вірні для NodeBB.", + "unsub.cta": "Натисніть тут, щоб змінити ці налаштування", + "unsubscribe": "відписатись", + "unsub.success": "Ви більше не будете отримувати повідомлення з %1 поштової розсилки", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "Ви були забанені на %1", + "banned.text1": "Користувач %1 був забанений на %2.", + "banned.text2": "Тривалість бану - до %1.", + "banned.text3": "Це причина, чому ви були забанені:", + "closing": "Дякуємо!" +} \ No newline at end of file diff --git a/public/language/uk/error.json b/public/language/uk/error.json new file mode 100644 index 0000000000..9418ca4bf6 --- /dev/null +++ b/public/language/uk/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Невірні дані", + "invalid-json": "Некоректний формат JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "Не схоже, що ви увійшли в систему.", + "account-locked": "Ваш акаунт тимчасово заблоковано", + "search-requires-login": "Для пошуку потрібен акаунт — будь ласка, увійдіть чи зареєструйтесь.", + "goback": "Натисніть Назад, щоб повернутись до попередньої сторінки.", + "invalid-cid": "Невірний ID категорії", + "invalid-tid": "Невірний ID теми", + "invalid-pid": "Невірний ID поста", + "invalid-uid": "Невірний ID користувача", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "Невірне ім'я користувача", + "invalid-email": "Невірна електронна адреса", + "invalid-fullname": "Невірне повне ім'я", + "invalid-location": "Невірне місцезнаходження", + "invalid-birthday": "Невірна дата народження", + "invalid-title": "Невірний заголовок", + "invalid-user-data": "Невірні користувацькі дані", + "invalid-password": "Невірний пароль", + "invalid-login-credentials": "Невірне ім'я користувача або пароль", + "invalid-username-or-password": "Вкажіть, будь ласка, ім'я користувача та пароль", + "invalid-search-term": "Невірний пошуковий запит", + "invalid-url": "Недійсна URL-адреса", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "Локальний логін вимкнений для акаунтів, які не мають відповідних прав.", + "csrf-invalid": "Нам не вдалося вас пустити, ймовірно, через прострочену сесію. Будь ласка, спробуйте ще раз", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "Невірне значення сторінки, має бути щонайменше %1 та щонайбільше %2", + "username-taken": "Це ім'я зайняте", + "email-taken": "Ця електронна пошта зайнята", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "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 may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "Ми не можемо підтвердити вашу електронну пошту, будь ласка, спробуйте пізніше.", + "confirm-email-already-sent": "Підтвердження по електронній пошті вже було надіслано, зачекайте, будь ласка, %1 хвилин(и), щоб відправити ще одне. ", + "sendmail-not-found": "Виконуваний файл sendmail не знайдено, переконайтесь, будь ласка, що його встановлено та що він виконується власником процесу NodeBB.", + "digest-not-enabled": "Цей користувач не має активних дайджестів, або налаштування по замовчанню не включають надсилання дайджестів.", + "username-too-short": "Ім'я користувача закоротке", + "username-too-long": "Ім'я користувача задовге", + "password-too-long": "Пароль задовгий", + "reset-rate-limited": "Занадто багато запитів на скидання паролю (кількість за період часу обмежена)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "Користувача забанено", + "user-banned-reason": "Вибачте, але цей акаунт було забанено (Причина: %1)", + "user-banned-reason-until": "Вибачте, цей акаунт забанений до %1 (Причина: %2)", + "user-too-new": "Вибачте, але вам необхідно зачекати %1 секунд(и), перед першим постом", + "blacklisted-ip": "Вибачте, але ваша IP-адреса була забанена в цій спільноті. Якщо ви гадаєте, що це сталось помилково, зв'яжіться з адміністратором.", + "ban-expiry-missing": "Вкажіть, будь ласка, кінцеву дату бану", + "no-category": "Категорія не існує", + "no-topic": "Тема не існує", + "no-post": "Посту не існує", + "no-group": "Група не існує", + "no-user": "Користувач не існує", + "no-teaser": "Тизер не існує", + "no-flag": "Flag does not exist", + "no-privileges": "У вас недостатньо повноважень для цієї дії. ", + "category-disabled": "Категорію відключено", + "topic-locked": "Тему заблоковано", + "post-edit-duration-expired": "Ви можете редагувати пости лиш на протязі %1 секунд(и) з часу відправки", + "post-edit-duration-expired-minutes": "Ви можете редагувати пости лиш на протязі %1 хвилин(и) з часу відправки", + "post-edit-duration-expired-minutes-seconds": "Ви можете редагувати пости лиш на протязі %1 хвилин(и) та %2 секунд(и) з часу відправки", + "post-edit-duration-expired-hours": "Ви можете редагувати пости лиш на протязі %1 годин(и) з часу відправки", + "post-edit-duration-expired-hours-minutes": "Ви можете редагувати пости лиш на протязі %1 годин(и) та %2 хвилин(и) з часу відправки", + "post-edit-duration-expired-days": "Ви можете редагувати пости лиш на протязі %1 дні з часу відправки", + "post-edit-duration-expired-days-hours": "Ви можете редагувати пости лиш на протязі %1 дні та %2 годин(и) з часу відправки", + "post-delete-duration-expired": "Ви можете видаляти пости лиш на протязі %1 секунд(и) з часу відправки", + "post-delete-duration-expired-minutes": "Ви можете видаляти пости лиш на протязі %1 хвилин(и) з часу відправки", + "post-delete-duration-expired-minutes-seconds": "Ви можете видаляти пости лиш на протязі %1 хвилин(и) та %2 секунд(и) з часу відправки", + "post-delete-duration-expired-hours": "Ви можете видаляти пости лиш на протязі %1 годин(и) з часу відправки", + "post-delete-duration-expired-hours-minutes": "Ви можете видаляти пости лиш на протязі %1 годин(и) та %2 хвилин(и) з часу відправки", + "post-delete-duration-expired-days": "Ви можете видаляти пости лиш на протязі %1 дні з часу відправки", + "post-delete-duration-expired-days-hours": "Ви можете видаляти пости лиш на протязі %1 дні та %2 годин(и) з часу відправки", + "cant-delete-topic-has-reply": "Ви не можете видалити тему з відповідями", + "cant-delete-topic-has-replies": "Ви не можете видалити тему з %1 відповідями", + "content-too-short": "Введіть, будь ласка, довший пост. Він має складати щонайменше %1 символ(ів).", + "content-too-long": "Введіть, будь ласка, коротший пост. Він має складати щонайбільше %1 символ(ів).", + "title-too-short": "Введіть, будь ласка, довший заголовок. Мінімальна довжина %1 символ(ів).", + "title-too-long": "Введіть, будь ласка, коротший заголовок. Максимальна довжина %1 символ(ів).", + "category-not-selected": "Категорію не вибрано.", + "too-many-posts": "Ви не можете постити частіше %1 секунд(и) — зачекайте, будь ласка, перед повторною спробою", + "too-many-posts-newbie": "Як новий користувач, ви не можете публікувати частіше %1 секунд(и) доки не заробите %2 репутації — зачекайте, будь ласка, перед повторною спробою", + "already-posting": "You are already posting", + "tag-too-short": "Введіть, будь ласка, довший тег. Мінімальна довжина тегу %1 символ(ів)", + "tag-too-long": "Введіть, будь ласка, коротший тег. Максимальна довжина тегу %1 символ(ів)", + "not-enough-tags": "Замало тегів. Тема повинна мати щонайменше %1 тег(и)", + "too-many-tags": "Забагато тегів. Тема не може мати більше %1 тег(и)", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "Зачекайте, будь ласка, доки завантаження завершиться.", + "file-too-big": "Максимальний розмір файлу %1 кБ — завантажте менший файл, будь ласка.", + "guest-upload-disabled": "Гостьове завантаження вимкнено.", + "cors-error": "Неможливо завантажити зображення через неправильно налаштований CORS", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "Ви вже додали цей пост собі в закладки", + "already-unbookmarked": "Ви вже видалили цей пост із закладок", + "cant-ban-other-admins": "Ви не можете банити інших адмінів!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "Ви єдиний адміністратор. Додайте іншого користувача в якості адміністратора перш ніж знімати з себе ці обов'язки.", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "Зніміть обов'язки адміністратора з цього акаунту перш ніж видаляти його.", + "already-deleting": "Already deleting", + "invalid-image": "Невірне зображення", + "invalid-image-type": "Невірний тип зображення. Дозволені типи: %1", + "invalid-image-extension": "Невірне розширення зображення", + "invalid-file-type": "Невірний тип файлу. Дозволені типи: %1", + "invalid-image-dimensions": "Зображення занадто велике", + "group-name-too-short": "Ім'я групи занадто коротке", + "group-name-too-long": "Ім'я групи занадто довге", + "group-already-exists": "Група вже існує", + "group-name-change-not-allowed": "Перейменування групи не дозволено", + "group-already-member": "Вже є учасником цієї групи", + "group-not-member": "Не є учасником цієї групи", + "group-needs-owner": "Ця група потребує щонайменше одного власника", + "group-already-invited": "Користувача вже було запрошено", + "group-already-requested": "Ваша заявка на вступ вже подана", + "group-join-disabled": "Ви не можете приєднатись до цієї групи зараз", + "group-leave-disabled": "Ви не можете покинути цю групу зараз", + "post-already-deleted": "Цей пост вже видалено", + "post-already-restored": "Цей пост вже відновлено", + "topic-already-deleted": "Ця тема вже була видалена", + "topic-already-restored": "Ця тема вже була відновлена", + "cant-purge-main-post": "Ви не можете видалити головний пост, натомість видаліть тему.", + "topic-thumbnails-are-disabled": "Мініатюри теми вимкнено.", + "invalid-file": "Невірний файл", + "uploads-are-disabled": "Завантаження вимкнено", + "signature-too-long": "Вибачте, але ваш підпис не може бути довшим за %1 символ(и).", + "about-me-too-long": "Вибачте, але \"Про мене\" не може бути довшим за %1 символ(и).", + "cant-chat-with-yourself": "Ви не можете писати самому собі!", + "chat-restricted": "Цей користувач обмежив повідомлення. Він має стежити за вами, перш ніж ви зможете спілкуватися з ним", + "chat-disabled": "Чат вимкнено", + "too-many-messages": "Ви надіслали забагато повідомлень, зачекайте трішки.", + "invalid-chat-message": "Невірне повідомлення чату", + "chat-message-too-long": "Повідомлення чату не можуть бути довшими за %1 символів.", + "cant-edit-chat-message": "Ви не можете редагувати повідомлення", + "cant-delete-chat-message": "Ви не можете видалити це повідомлення", + "chat-edit-duration-expired": "Ви можете редагувати повідомлення чату лише через %1 секунд після публікації", + "chat-delete-duration-expired": "Ви можете видаляти повідомлення чату лише через %1 секунд після публікації", + "chat-deleted-already": "Це повідомлення чату вже було видалено.", + "chat-restored-already": "Це чат повідомлення вже було відновлене", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "Ви вже проголосували за цей пост.", + "reputation-system-disabled": "Система репутацій вимкнена.", + "downvoting-disabled": "Голосування проти вимкнено", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "Ви не можете проголосувати за власний пост", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "У NodeBB виникла проблема при перевантаженні: \"%1\". NodeBB продовжить надавати існуючі клієнтські ресурси, проте радимо вам скасувати те, що було зроблено до перевантаження.", + "registration-error": "Помилка реєстрації", + "parse-error": "Щось пішло не так при розборі відповіді сервера", + "wrong-login-type-email": "Будь ласка, використайте вашу електронну пошту для входу", + "wrong-login-type-username": "Будь ласка, використайте ваше ім'я для входу", + "sso-registration-disabled": "Реєстрація була відключена для %1 акаунтів, будь ласка, зареєструйтесь спочатку з адресою електронної пошти", + "sso-multiple-association": "Ви не можете пов'язати кілька облікових записів з цього сервісу з обліковим записом NodeBB. Будь ласка, від'єднайте існуючий обліковий запис і повторіть спробу.", + "invite-maximum-met": "Ви запросили максимальну кілкість людей (%1 з %2).", + "no-session-found": "Жодної сесії не знайдено!", + "not-in-room": "Користувача немає в кімнаті", + "cant-kick-self": "Ви не можете вигнати самі себе з групи", + "no-users-selected": "Не вибрано жодного користувача", + "invalid-home-page-route": "Невірний шлях на головну", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "Не вибрано жодної теми!", + "cant-move-to-same-topic": "Ви не можете перемістити пост до тієї ж самої теми!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "Ви не можете заблокувати самого себе!", + "cannot-block-privileged": "Ви не можете заблокувати адміністраторів або глобальних модераторів", + "cannot-block-guest": "Гості не можуть блокувати інших користувачів", + "already-blocked": "Цей користувач вже заблокований", + "already-unblocked": "Цей користувач вже розблокований", + "no-connection": "Схоже, виникла проблема з вашим Інтернет-з'єднанням", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/uk/flags.json b/public/language/uk/flags.json new file mode 100644 index 0000000000..80db1a33e5 --- /dev/null +++ b/public/language/uk/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Стан", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "Ура! Скарг немає.", + "assignee": "Виконавець", + "update": "Оновлення", + "updated": "Оновлено", + "resolved": "Resolved", + "target-purged": "Зміст на який подана ця скарга було стерто і він більше недоступний.", + + "graph-label": "Щоденні прапорці", + "quick-filters": "Швидкі фільтри", + "filter-active": "У цьому списку скарг активовано один або більше фільтрів", + "filter-reset": "Видалити фільтри", + "filters": "Параметри фільтру", + "filter-reporterId": "UID скаржника", + "filter-targetUid": "UID оскаржуваного", + "filter-type": "Тип скарги", + "filter-type-all": "Увесь зміст", + "filter-type-post": "Пост", + "filter-type-user": "Користувач", + "filter-state": "Стан", + "filter-assignee": "UID виконавця", + "filter-cid": "Категорія", + "filter-quick-mine": "Призначені мені", + "filter-cid-all": "Всі категорії", + "apply-filters": "Примінити фільтри", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "Quick Actions", + "flagged-user": "Користувач зі скаргою", + "view-profile": "Переглянути профіль", + "start-new-chat": "Почати новий чат", + "go-to-target": "Переглянути ціль скарги", + "assign-to-me": "Assign To Me", + "delete-post": "Delete Post", + "purge-post": "Purge Post", + "restore-post": "Restore Post", + "delete": "Delete Flag", + + "user-view": "Переглянути профіль", + "user-edit": "Редагувати профіль", + + "notes": "Коментарі до скарги", + "add-note": "Додати коментар", + "no-notes": "Немає загальних коментарів.", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "Коментар додано", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "Account & Flag History", + "no-history": "Немає історії скарг.", + + "state-all": "Всі стани", + "state-open": "Нова/Відкрита", + "state-wip": "У роботі", + "state-resolved": "Вирішена", + "state-rejected": "Відхилена", + "no-assignee": "Не призначена", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "Будь ласка, вкажіть причину скарги на %1 %2 або використайте одну з відповідних швидких кнопок.", + "modal-reason-spam": "Спам", + "modal-reason-offensive": "Образа", + "modal-reason-other": "Інше (зазначте нижче)", + "modal-reason-custom": "Причина скарги на цей вміст...", + "modal-submit": "Надіслати скаргу", + "modal-submit-success": "Скарга на цей зміст надіслана модератору.", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/uk/global.json b/public/language/uk/global.json new file mode 100644 index 0000000000..4bc5e4d55e --- /dev/null +++ b/public/language/uk/global.json @@ -0,0 +1,126 @@ +{ + "home": "Додому", + "search": "Пошук", + "buttons.close": "Закрити", + "403.title": "Доступ заборонено", + "403.message": "Здається ви натрапили на сторінку до якої не маєте доступу.", + "403.login": "Можливо вам потрібно увійти?", + "404.title": "Не знайдено", + "404.message": "Здається ви натрапили на сторінку якої не існує. Поверніться на початкову сторінку.", + "500.title": "Внутрішня помилка.", + "500.message": "Ой! Здається щось пішло не так!", + "400.title": "Помилковий запит.", + "400.message": "Схоже, що посилання сформовано невірно, перевірте його і спробуйте ще раз. Інакше, поверніться на початкову сторінку.", + "register": "Реєстрація", + "login": "Логін", + "please_log_in": "Увійдіть, будь-ласка", + "logout": "Вийти", + "posting_restriction_info": "Наразі постити можуть лише зареєстровані користувачі, натисніть тут щоб увійти.", + "welcome_back": "З поверненням", + "you_have_successfully_logged_in": "Ви успішно увійшли", + "save_changes": "Зберегти зміни", + "save": "Зберегти", + "close": "Закрити", + "pagination": "Розбиття на сторінки", + "pagination.out_of": "%1 із %2", + "pagination.enter_index": "Go to post index", + "header.admin": "Адмін", + "header.categories": "Категорії", + "header.recent": "Недавні", + "header.unread": "Непрочитані", + "header.tags": "Теги", + "header.popular": "Популярні", + "header.top": "Top", + "header.users": "Користувачі", + "header.groups": "Групи", + "header.chats": "Чати", + "header.notifications": "Сповіщення", + "header.search": "Пошук", + "header.profile": "Профіль", + "header.navigation": "Навігація", + "notifications.loading": "Завантаження сповіщень", + "chats.loading": "Завантаження чатів", + "motd.welcome": "Вітаємо у NodeBB, надсучасній платформі для обговорень.", + "previouspage": "Попередня сторінка", + "nextpage": "Наступна сторінка", + "alert.success": "Успіх", + "alert.error": "Помилка", + "alert.banned": "Забанений", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "Ви більше не стежите за %1!", + "alert.follow": "Ви тепер стежите за %1!", + "users": "Користувачі", + "topics": "Теми", + "posts": "Пости", + "x-posts": "%1 posts", + "best": "Найкращі", + "controversial": "Controversial", + "votes": "Голоси", + "x-votes": "%1 votes", + "voters": "Voters", + "upvoters": "За", + "upvoted": "За", + "downvoters": "Проти", + "downvoted": "Проти", + "views": "Перегляди", + "posters": "Posters", + "reputation": "Репутація", + "lastpost": "Останній допис", + "firstpost": "Перший допис", + "read_more": "читати далі", + "more": "Більше", + "none": "None", + "posted_ago_by_guest": "запостив Гість %1", + "posted_ago_by": "запостив %2 %1", + "posted_ago": "запощено %1", + "posted_in": "запощено в %1", + "posted_in_by": "запостив %2 в %1 ", + "posted_in_ago": "запощено в %1 %2", + "posted_in_ago_by": "запостив %3 в %1 %2 ", + "user_posted_ago": "%1 запостив %2", + "guest_posted_ago": "Гість запостив %1", + "last_edited_by": "востаннє редагувалося %1", + "norecentposts": "Немає свіжих постів", + "norecenttopics": "Немає свіжих тем", + "recentposts": "Нещодавні пости", + "recentips": "Нещодавно увійшовші IP-адреси", + "moderator_tools": "Інструменти модератора", + "online": "Онлайн", + "away": "Відсутній", + "dnd": "Не турбувати", + "invisible": "Невидимий", + "offline": "Не в мережі", + "email": "Email", + "language": "Мова", + "guest": "Гість", + "guests": "Гості", + "former_user": "Колишній користувач", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "Форум оновлено", + "updated.message": "Форум було щойно оновлено до останньої версії. Клікніть тут, щоб оновити сторінку.", + "privacy": "Приватність", + "follow": "Стежити", + "unfollow": "Не стежити", + "delete_all": "Видалити все", + "map": "Мапа", + "sessions": "Сесії", + "ip_address": "IP Адреса", + "enter_page_number": "Уведіть номер сторінки", + "upload_file": "Завантажити файл", + "upload": "Завантажити", + "uploads": "Завантаження", + "allowed-file-types": "Дозволені типи файлів %1", + "unsaved-changes": "У вас є незбережені зміни. Ви точно хочете піти звідси?", + "reconnecting-message": "Схоже, що з'єднання з %1 було втрачено, зачекайте поки ми спробуємо приєднатися знов.", + "play": "Грати", + "cookies.message": "Цей сайт використовує куки, щоб ви отримали найкращий досвід при роботі з сайтом.", + "cookies.accept": "Зрозуміло!", + "cookies.learn_more": "Дізнатися більше", + "edited": "Відредаговано", + "disabled": "Вимкнено", + "select": "Обрати", + "user-search-prompt": "Введіть щось тут, щоб знайти користувачів..." +} \ No newline at end of file diff --git a/public/language/uk/groups.json b/public/language/uk/groups.json new file mode 100644 index 0000000000..7ebaee9b4a --- /dev/null +++ b/public/language/uk/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Групи", + "view_group": "Переглянути групу", + "owner": "Власник групи", + "new_group": "Створити нову групу", + "no_groups_found": "Тут немає груп", + "pending.accept": "Прийняти", + "pending.reject": "Відхилити", + "pending.accept_all": "Прийняти всі", + "pending.reject_all": "Відхилити всі", + "pending.none": "На даний момент жоден учасник не чекає розгляду", + "invited.none": "На даний момент немає запрошених учасників", + "invited.uninvite": "Анулювати запрошення", + "invited.search": "Пошук користувача для запрошення у групу", + "invited.notification_title": "Вас запросили проєднатися до %1", + "request.notification_title": "Запит на членство у групі від %1", + "request.notification_text": "%1 було запрошено стати учасником групи %2", + "cover-save": "Зберегти", + "cover-saving": "Збереження", + "details.title": "Деталі групи", + "details.members": "Список учасників", + "details.pending": "Учасники, що очікують", + "details.invited": "Запрошені учасники", + "details.has_no_posts": "Учасники групи не написали жодного посту.", + "details.latest_posts": "Останні пости", + "details.private": "Приватна", + "details.disableJoinRequests": "Вимкнути запити на приєднання", + "details.disableLeave": "Забороніть користувачам покидати групу", + "details.grant": "Надати/забрати права адміністратора", + "details.kick": "Вигнати", + "details.kick_confirm": "Ви впевнені, що бажаєте видалити цього користувача з групи?", + "details.add-member": "Додати члена групи", + "details.owner_options": "Адміністрація групи", + "details.group_name": "Назва групи", + "details.member_count": "Кількість учасників", + "details.creation_date": "Дата створення", + "details.description": "Опис", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "Попередній перегляд бейджа", + "details.change_icon": "Змінити іконку", + "details.change_label_colour": "Змінити колір позначки", + "details.change_text_colour": "Змінити колір тексту", + "details.badge_text": "Текст бейджа", + "details.userTitleEnabled": "Показати бейдж", + "details.private_help": "Якщо увімкнено, приєднання до групи вимагає підтвердження власника.", + "details.hidden": "Прихована", + "details.hidden_help": "Якщо увімкнено, групу не буде видно в загальному списку і запрошення користувачів потрібно буде здійснювати вручну.", + "details.delete_group": "Видалити групу", + "details.private_system_help": "Приватні групи вимкнено на системному рівні, ця опція нічого не робить.", + "event.updated": "Деталі групи оновлено", + "event.deleted": "Група \"%1\" видалена", + "membership.accept-invitation": "Прийняти запрошення", + "membership.accept.notification_title": "Тепер ви є членом %1", + "membership.invitation-pending": "Запрошення в черзі", + "membership.join-group": "Приєднатися до групи", + "membership.leave-group": "Покинути групу", + "membership.leave.notification_title": "%1 покинув групу %2", + "membership.reject": "Відхилити", + "new-group.group_name": "Назва групи:", + "upload-group-cover": "Завантажити обкладинку групи", + "bulk-invite-instructions": "Уведіть список імен користувачів (розділених комами), котрих ви бажаєте запросити до групи", + "bulk-invite": "Масове запрошення", + "remove_group_cover_confirm": "Ви впевнені, що бажаєте видалити обкладинку?" +} \ No newline at end of file diff --git a/public/language/uk/ip-blacklist.json b/public/language/uk/ip-blacklist.json new file mode 100644 index 0000000000..796d4b9847 --- /dev/null +++ b/public/language/uk/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Налаштуйте чорний список IP адрес.", + "description": "Буває так, що бан користувача є недостатною мірою стримування. Іншим разом, заборона доступу до форуму для певної IP адреси або діапазону IP адрес є кращим засобом захисту форуму. У таких випадках ви можете додати проблемні адреси або цілі CIDR блоки до цього чорного списку і їм буде заборонено входити або реєструвати нові акаунти.", + "active-rules": "Активні правила", + "validate": "Перевірити чорний список", + "apply": "Примінити чорний список", + "hints": "Підказки по синтаксису", + "hint-1": "Вкажіть по одній IP адресі на кожен рядок. Ви можете додавати блоки IP адрес, якщо вони відповідні формату CIDR (наприклад, 192.168.100.0/22)", + "hint-2": "Ви можете додавати коментарі вказавши символ # на початку рядку.", + + "validate.x-valid": "%1 із %2 правил вірні.", + "validate.x-invalid": "Наступні %1 правил невірні:", + + "alerts.applied-success": "Чорний список примінено", + + "analytics.blacklist-hourly": "Графік 1 – Внесення до чорного списку за годину", + "analytics.blacklist-daily": "Графік 2 – Внесення до чорного списку за день", + "ip-banned": "IP заблоковано" +} \ No newline at end of file diff --git a/public/language/uk/language.json b/public/language/uk/language.json new file mode 100644 index 0000000000..3a4bc3cd3c --- /dev/null +++ b/public/language/uk/language.json @@ -0,0 +1,5 @@ +{ + "name": "Українська", + "code": "uk", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/uk/login.json b/public/language/uk/login.json new file mode 100644 index 0000000000..8185742cad --- /dev/null +++ b/public/language/uk/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Ім'я / Пошта", + "username": "Ім'я користувача", + "remember_me": "Запам'ятати мене?", + "forgot_password": "Забули пароль?", + "alternative_logins": "Альтернативний вхід", + "failed_login_attempt": "Вхід невдався", + "login_successful": "Ви успішно зайшли!", + "dont_have_account": "Не маєте акаунту?", + "logged-out-due-to-inactivity": "Ви були розлогінені з Адмінської Панелі Керування через неактивність", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/uk/modules.json b/public/language/uk/modules.json new file mode 100644 index 0000000000..67df7003ac --- /dev/null +++ b/public/language/uk/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Чат з", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "Надіслати", + "chat.no_active": "У вас немає активних чатів.", + "chat.user_typing": "%1 друкує...", + "chat.user_has_messaged_you": "%1 написав вам.", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "Будь ласка, оберіть отримувача, щоб переглянути історію повідомлень", + "chat.no-users-in-room": "У цій кімнаті пусто", + "chat.recent-chats": "Нещодавні чати", + "chat.contacts": "Контакти", + "chat.message-history": "Історія повідомлень", + "chat.message-deleted": "Message Deleted", + "chat.options": "Параметри чату", + "chat.pop-out": "Залишити розмову", + "chat.minimize": "Згорнути", + "chat.maximize": "Розгорнути", + "chat.seven_days": "7 днів", + "chat.thirty_days": "30 днів", + "chat.three_months": "3 місяці", + "chat.delete_message_confirm": "Ви впевнені, що хочете видалити це повідомлення?", + "chat.retrieving-users": "Отримання користувачів...", + "chat.manage-room": "Управління чат кімнатами", + "chat.add-user-help": "Шукайте користувачів тут. Користувача можна додати до чату, обравши його. Нові користувачі не можуть бачити повідомлення, написані до того, як їх додали до розмови. Тільки власники кімнат можуть видаляти користувачів з кімнат.", + "chat.confirm-chat-with-dnd-user": "Користувач змінив свій статус на DnD (Не турбувати). Ви дійсно бажаєте надіслати йому повідомлення в чат?", + "chat.rename-room": "Перейменувати Кімнату", + "chat.rename-placeholder": "Введіть назву своєї кімнати тут", + "chat.rename-help": "Назва кімнати, яку буде встановлено тут, буде доступна для перегляду всіма учасниками в кімнаті.", + "chat.leave": "Залишити чат", + "chat.leave-prompt": "Ви впевнені, що хочете залишити цей чат?", + "chat.leave-help": "Залишивши цей чат, ви видалите вас із майбутньої кореспонденції у цьому чаті. Якщо ви знову будете додані в майбутньому, ви не побачите жодної історії чату перед тим, як знову приєднатися.", + "chat.in-room": "У цій кімнаті", + "chat.kick": "Штурхнути", + "chat.show-ip": "Показати IP", + "chat.owner": "Власник кімнати", + "chat.system.user-join": "%1 зайшов в кімнату", + "chat.system.user-leave": "%1 покинув кімнату", + "chat.system.room-rename": "%2 перейменував кімнату на: %1", + "composer.compose": "Редактор повідомлень", + "composer.show_preview": "Показати попередній перегляд", + "composer.hide_preview": "Сховати попередній перегляд", + "composer.user_said_in": "%1 написав в %2:", + "composer.user_said": "%1 написав:", + "composer.discard": "Ви впевнені, що хочете скасувати цей пост?", + "composer.submit_and_lock": "Надіслати і заблокувати", + "composer.toggle_dropdown": "Показати випадаючий список", + "composer.uploading": "Завантаження %1", + "composer.formatting.bold": "Жирний", + "composer.formatting.italic": "Курсив", + "composer.formatting.list": "Список", + "composer.formatting.strikethrough": "Закреслений", + "composer.formatting.code": "Код", + "composer.formatting.link": "Посилання", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "Завантажити зображення", + "composer.upload-file": "Завантажити файл", + "composer.zen_mode": "Режим Дзен", + "composer.select_category": "Обрати категорію", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "ОК", + "bootbox.cancel": "Скасувати", + "bootbox.confirm": "Підтвердити", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "Розташування обкладинки", + "cover.dragging_message": "Перетягніть обкладинку на бажане місце на натисніть \"Зберегти\"", + "cover.saved": "Зображення обкладинки та її позиція збережені", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/uk/notifications.json b/public/language/uk/notifications.json new file mode 100644 index 0000000000..1033805812 --- /dev/null +++ b/public/language/uk/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Сповіщення", + "no_notifs": "У вас немає нових сповіщень", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "Повернутись до %1", + "outgoing_link": "Зовнішнє посилання", + "outgoing_link_message": "Ви залишаєте %1", + "continue_to": "Перейти до %1", + "return_to": "Повернутись до %1", + "new_notification": "У вас нове сповіщення", + "you_have_unread_notifications": "У вас немає непрочитаних сповіщень", + "all": "Всі", + "topics": "Теми", + "replies": "Відповіді", + "chat": "Чати", + "group-chat": "Group Chats", + "follows": "Вістежування", + "upvote": "Схвалення", + "new-flags": "Нові Скарги", + "my-flags": "Скарги, подані на мене", + "bans": "Бани", + "new_message_from": "Нове повідомлення від %1", + "upvoted_your_post_in": "%1 проголосував за ваш пост в %2.", + "upvoted_your_post_in_dual": "%1 та %2 проголосували за ваш пост в %3.", + "upvoted_your_post_in_multiple": "%1 та %2 інших проголосували за ваш пост в %3.", + "moved_your_post": "%1 перемістив ваш пост до %2", + "moved_your_topic": "%1 перемістив %2", + "user_flagged_post_in": "%1 поскаржився на пост в %2", + "user_flagged_post_in_dual": "%1 та %2 поскаржились на пост в %3", + "user_flagged_post_in_multiple": "%1 та %2 інших поскаржились на пост в %3", + "user_flagged_user": "%1 поскаржився на профіль користувача (%2)", + "user_flagged_user_dual": "%1 та %2 поскаржились на профіль користувача (%3)", + "user_flagged_user_multiple": "%1 та %2 інших поскаржились на профіль користувача (%3)", + "user_posted_to": "%1 запостив відповідь на: %2", + "user_posted_to_dual": "%1 та %2 запостили відповіді до: %3", + "user_posted_to_multiple": "%1 та %2 інших запостили відповіді до: %3", + "user_posted_topic": "%1 запостив нову тему: %2", + "user_edited_post": "%1 has edited a post in %2", + "user_started_following_you": "%1 почав стежити за вами.", + "user_started_following_you_dual": "%1 та %2 почали стежити за вами.", + "user_started_following_you_multiple": "%1 та %2 інших почали стежити за вами.", + "new_register": "%1 надіслав запит на реєстрацію.", + "new_register_multiple": "%1 запити на реєстрацію очікують розгляду.", + "flag_assigned_to_you": "На вас була подана скарга %1", + "post_awaiting_review": "Пост очікує на перевірку", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "Електронну пошту підтверджено", + "email-confirmed-message": "Дякуємо за підтвердження електронної пошти. Ваш акаунт тепер повністю активовано.", + "email-confirm-error-message": "При перевірці вашої електронної пошти сталася проблема. Можливо код був недійсним або простроченим.", + "email-confirm-sent": "Підтвердження по електронній пошті було надіслано.", + "none": "Немає", + "notification_only": "Тільки сповіщення", + "email_only": "Тільки електронну пошту ", + "notification_and_email": "Сповіщення та пошта", + "notificationType_upvote": "Коли хтось голосує за ваш пост", + "notificationType_new-topic": "Коли хтось, кого ви читаєте, публікує тему", + "notificationType_new-reply": "Коли з'являється нова відповідь у темі, за якою ви слідкуєте", + "notificationType_post-edit": "When a post is edited in a topic you are watching", + "notificationType_follow": "Коли хтось починає слідкувати за вами", + "notificationType_new-chat": "Коли ви отримуєте повідомлення чату", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "Коли ви отримуєте запрошення до групи", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "Коли хтось подає запит на приєднання до групи, якою ви володієте", + "notificationType_new-register": "Коли когось додано до черги на реєстрацію", + "notificationType_post-queue": "Коли новий пост знаходиться в черзі", + "notificationType_new-post-flag": "Коли повідомлення позначено", + "notificationType_new-user-flag": "Коли користувача позначено" +} \ No newline at end of file diff --git a/public/language/uk/pages.json b/public/language/uk/pages.json new file mode 100644 index 0000000000..d30285edda --- /dev/null +++ b/public/language/uk/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Головна", + "unread": "Непрочитані теми", + "popular-day": "Популярні теми сьогодні", + "popular-week": "Популярні теми цього тижня", + "popular-month": "Популярні теми цього місяця", + "popular-alltime": "Популярні теми за весь час", + "recent": "Свіжі теми", + "top-day": "Найрейтинговіші теми сьогодні", + "top-week": "Найрейтинговіші теми цього тижня", + "top-month": "Найрейтинговіші теми цього місяця", + "top-alltime": "Найрейтинговіші теми", + "moderator-tools": "Інструменти Модератора", + "flagged-content": "Оскаржений вміст", + "ip-blacklist": "Чорний список IP адрес", + "post-queue": "Черга Постів", + "users/online": "Користувачі в мережі", + "users/latest": "Останні користувачі", + "users/sort-posts": "Користувачі з найбільшою кількістю постів", + "users/sort-reputation": "Користувачі з найкращою репутацією", + "users/banned": "Забанені користувачі", + "users/most-flags": "Користувачі з найбільшою кількістю скарг", + "users/search": "Пошук користувача", + "notifications": "Сповіщення", + "tags": "Теги", + "tag": "Теми, позначені нижче "%1"", + "register": "Зареєструвати акаунт", + "registration-complete": "Реєстрацію завершено", + "login": "Увійдіть в свій акаунт", + "reset": "Скинути пароль вашого акаунту", + "categories": "Категорії", + "groups": "Групи", + "group": "Група %1", + "chats": "Чати", + "chat": "Чат з %1", + "flags": "Скарги", + "flag-details": "Деталі по скарзі %1", + "account/edit": "Редагування \"%1\"", + "account/edit/password": "Редагування паролю для \"%1\"", + "account/edit/username": "Редагування імені для \"%1\"", + "account/edit/email": "Редагування електронної пошти для \"%1\"", + "account/info": "Інформація акаунту", + "account/following": "Люди за котрими стежить %1", + "account/followers": "Люди котрі стежать за %1", + "account/posts": "Пости написані %1", + "account/latest-posts": "Останні дописи від %1", + "account/topics": "Теми створені %1", + "account/groups": "Групи %1", + "account/watched_categories": "Категорії, за якими спостерігає %1", + "account/bookmarks": "Закладки %1", + "account/settings": "Налаштування користувача", + "account/watched": "Теми за якими стежить %1", + "account/ignored": "Теми, які ігноруються", + "account/upvoted": "Пости за які проголосував %1", + "account/downvoted": "Пости проти яких проголосував %1", + "account/best": "Найкращі пости %1", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "Заблоковані користувачі для %1", + "account/uploads": "Завантаження від %1", + "account/sessions": "Логін-сесії", + "confirm": "Електронну пошту підтверджено", + "maintenance.text": "%1 в данний час на технічному обслуговувані. Завітайте, будь ласка, пізніше.", + "maintenance.messageIntro": "Крім того, адміністратор залишив це повідомлення:", + "throttled.text": "%1 в даний час недоступний через надмірне навантаження. Завітайте, будь ласка, пізніше." +} \ No newline at end of file diff --git a/public/language/uk/post-queue.json b/public/language/uk/post-queue.json new file mode 100644 index 0000000000..bea08f674f --- /dev/null +++ b/public/language/uk/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Черга Постів", + "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", + "user": "Користувач", + "category": "Категорія", + "title": "Заголовок", + "content": "Зміст", + "posted": "Опубліковано", + "reply-to": "Відповідь для \"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/uk/recent.json b/public/language/uk/recent.json new file mode 100644 index 0000000000..95603beef3 --- /dev/null +++ b/public/language/uk/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Свіжі", + "day": "День", + "week": "Тиждень", + "month": "Місяць", + "year": "Рік", + "alltime": "Весь час", + "no_recent_topics": "Немає свіжих тем.", + "no_popular_topics": "Немає популярних тем.", + "there-is-a-new-topic": "Є нова тема.", + "there-is-a-new-topic-and-a-new-post": "Є нова тема та нова публікація.", + "there-is-a-new-topic-and-new-posts": "Є нова тема та %1 нових публікацій.", + "there-are-new-topics": "Є %1 нових тем.", + "there-are-new-topics-and-a-new-post": "Є %1 нових тем на нова публікація.", + "there-are-new-topics-and-new-posts": "Є %1 нових тем та %2 нові публікації.", + "there-is-a-new-post": "Є нова публікація.", + "there-are-new-posts": "Є %1 нових публікацій.", + "click-here-to-reload": "Натисніть тут, щоб перевантажити." +} \ No newline at end of file diff --git a/public/language/uk/register.json b/public/language/uk/register.json new file mode 100644 index 0000000000..600fd448a9 --- /dev/null +++ b/public/language/uk/register.json @@ -0,0 +1,32 @@ +{ + "register": "Реєстрація", + "cancel_registration": "Скасувати реєстрацію", + "help.email": "За замовчуванням, ваша email-адреса буде прихована від інших. ", + "help.username_restrictions": "Унікальне ім'я довжиною від %1 до %2 символів. Інші можуть вас згадувати за допомогою @ім'я.", + "help.minimum_password_length": "Довжина паролю має бути щонайменше %1 символів.", + "email_address": "Електронна адреса", + "email_address_placeholder": "Уведіть електронну адресу", + "username": "Ім'я користувача", + "username_placeholder": "Уведіть ім'я", + "password": "Пароль", + "password_placeholder": "Уведіть пароль", + "confirm_password": "Підтвердіть пароль", + "confirm_password_placeholder": "Підтвердження пароля", + "register_now_button": "Зареєструватися зараз", + "alternative_registration": "Альтернативна реєстрація", + "terms_of_use": "Умови користування", + "agree_to_terms_of_use": "Я погоджуюсь з Умовами користування", + "terms_of_use_error": "Ви маєте погодитись з Умовами користування", + "registration-added-to-queue": "Ваша реєстрація була додана в чергу затвердження. Ви отримаєте листа на електронну пошту, коли адміністратор її підтвердить.", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "Я надаю згоду на збір та обробку моїх особистих даних на цьому веб-сайті.", + "gdpr_agree_email": "Я надаю згоду на отримання дайджесту та поштових повідомлень з цього веб-сайту.", + "gdpr_consent_denied": "Ви мусите надати цьому веб-сайту свою згоду на збір/обробку ваших даних та на отримання поштових повідомлень.", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/uk/reset_password.json b/public/language/uk/reset_password.json new file mode 100644 index 0000000000..95e8600e96 --- /dev/null +++ b/public/language/uk/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Скинути пароль", + "update_password": "Змінити пароль", + "password_changed.title": "Пароль змінено", + "password_changed.message": "

Пароль успішно скинуто, будь ласка, увійдіть знову.", + "wrong_reset_code.title": "Невірний код скидання", + "wrong_reset_code.message": "Отриманий код скидання невірний. Спробуйте, будь ласка, ще раз або запросіть новий код.", + "new_password": "Новий пароль", + "repeat_password": "Підтвердіть пароль", + "changing_password": "Changing Password", + "enter_email": "Будь ласка, введіть свою електронну пошту і ми надішлемо вам листа с інструкцією як скинути ваш обліковий запис.", + "enter_email_address": "Введіть електронну пошту", + "password_reset_sent": "Якщо зазначена електронна адреса належить існуючому користувачеві, повідомлення для скидання паролю було надіслане на цю адресу. Майте на увазі, що тільки одне повідомлення може бути надіслане за хвилину.", + "invalid_email": "Невірна або неіснуюча електронна пошта!", + "password_too_short": "Уведений пароль закороткий, оберіть, будь ласка, інший.", + "passwords_do_not_match": "Паролі що ви ввели не співпадають.", + "password_expired": "Ваш пароль закінчився, будь ласка, виберіть новий пароль." +} \ No newline at end of file diff --git a/public/language/uk/search.json b/public/language/uk/search.json new file mode 100644 index 0000000000..ce04e19f39 --- /dev/null +++ b/public/language/uk/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 збіг(ів) по запиту \"%2\" (%3 секунд)", + "no-matches": "Збіги не знайдено", + "advanced-search": "Розширений пошук", + "in": "В", + "titles": "Заголовки", + "titles-posts": "Заголовки та Пости", + "match-words": "Які містять слова", + "all": "Всі", + "any": "Будь-які", + "posted-by": "Запощено", + "in-categories": "В Категоріях", + "search-child-categories": "Шукати в дочірніх категоріях", + "has-tags": "Містить теги", + "reply-count": "Лічильник Відповідей", + "at-least": "Щонайменше", + "at-most": "Щонайбільше", + "relevance": "Релевантність", + "post-time": "Час посту", + "votes": "Голоси", + "newer-than": "Новіші за", + "older-than": "Старіші за", + "any-date": "Будь-яка дата", + "yesterday": "Вчора", + "one-week": "Один тиждень", + "two-weeks": "Два тижні", + "one-month": "Один місяць", + "three-months": "Три місяці", + "six-months": "Шість місяців", + "one-year": "Один рік", + "sort-by": "Сортувати за", + "last-reply-time": "Час останньої відповіді", + "topic-title": "Заголовок теми", + "topic-votes": "Голоси за тему", + "number-of-replies": "Кількість відповідей", + "number-of-views": "Кількість переглядів", + "topic-start-date": "Час початку теми", + "username": "Ім'я користувача", + "category": "Категорія", + "descending": "У порядку спадання", + "ascending": "У порядку зростання", + "save-preferences": "Зберегти налаштування", + "clear-preferences": "Очистити налаштування", + "search-preferences-saved": "Налаштування пошуку збережено", + "search-preferences-cleared": "Налаштування пошуку очищені", + "show-results-as": "Показати результати як", + "see-more-results": "Дивитись більше результатів (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/uk/success.json b/public/language/uk/success.json new file mode 100644 index 0000000000..e6f239a285 --- /dev/null +++ b/public/language/uk/success.json @@ -0,0 +1,7 @@ +{ + "success": "Успіх", + "topic-post": "Публікацію успішно створено.", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "Аутентифікація успішна", + "settings-saved": "Налаштування збережені!" +} \ No newline at end of file diff --git a/public/language/uk/tags.json b/public/language/uk/tags.json new file mode 100644 index 0000000000..a15d895df0 --- /dev/null +++ b/public/language/uk/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Немає теми з цим тегом.", + "tags": "Теги", + "enter_tags_here": "Введіть тег сюди, між літерами %1 та %2 кожен", + "enter_tags_here_short": "Введіть тег", + "no_tags": "Ще немає тегів", + "select_tags": "Select Tags" +} \ No newline at end of file diff --git a/public/language/uk/top.json b/public/language/uk/top.json new file mode 100644 index 0000000000..b8a05bfa5f --- /dev/null +++ b/public/language/uk/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "No top topics" +} \ No newline at end of file diff --git a/public/language/uk/topic.json b/public/language/uk/topic.json new file mode 100644 index 0000000000..771bbf852a --- /dev/null +++ b/public/language/uk/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Тема", + "title": "Title", + "no_topics_found": "Тем не знайдено!", + "no_posts_found": "Постів не знайдено!", + "post_is_deleted": "Цей пост був видалений!", + "topic_is_deleted": "Ця тема була видалена!", + "profile": "Профіль", + "posted_by": "Запощено %1", + "posted_by_guest": "Запощено гостем", + "chat": "Чат", + "notify_me": "Отримувати сповіщення про нові відповіді в цій темі", + "quote": "Цитувати", + "reply": "Відповісти", + "replies_to_this_post": "%1 відповідей", + "one_reply_to_this_post": "1 відповідь", + "last_reply_time": "Остання відповідь", + "reply-as-topic": "Відповісти темою", + "guest-login-reply": "Увійти для відповіді", + "login-to-view": "🔒 Увійдіть щоб переглянути", + "edit": "Редагувати", + "delete": "Видалити", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "Стерти", + "restore": "Відновити", + "move": "Перемістити", + "change-owner": "Змінити Власника", + "fork": "Відгалужити", + "link": "Зв'язати", + "share": "Поширити", + "tools": "Інструменти", + "locked": "Заблокована", + "pinned": "Закріплена", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "Переміщена", + "moved-from": "Moved from %1", + "copy-ip": "Копіювати IP", + "ban-ip": "Заблокувати IP", + "view-history": "Редагувати історію", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "Натисніть тут, щоб повернутися до останнього прочитаного посту у цій темі.", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "Цю тему було видалено. Лише користувачі з правом керування темами можуть її бачити.", + "following_topic.message": "Відтепер ви будете отримувати сповіщення коли хтось постить в цю тему.", + "not_following_topic.message": "Ви будете бачити цю тему в списку непрочитаних, але ви не будете отримувати сповіщень, коли хтось постить до неї.", + "ignoring_topic.message": "Ви більше не будете бачити цю тему в списку непрочитаних. Вас буде сповіщено коли хтось вас згадає або за ваш пост буде проголосовано.", + "login_to_subscribe": "Будь ласка, зареєструйтесь або увійдіть щоб підписатися на цю тему.", + "markAsUnreadForAll.success": "Тема відмічена для всіх як непрочитана.", + "mark_unread": "Помітити непрочитаною", + "mark_unread.success": "Тема помічена непрочитаною", + "watch": "Стежити", + "unwatch": "Не стежити", + "watch.title": "Отримуйте сповіщення про відповіді в цій темі", + "unwatch.title": "Перестати стежити за цією темою", + "share_this_post": "Поширити цей пост", + "watching": "Відстежується", + "not-watching": "Не відстежується", + "ignoring": "Ігнорується", + "watching.description": "Сповіщати мене про нові відповіді.
Показувати тему в непрочитаних.", + "not-watching.description": "Не сповіщати мене про нові відповіді.
Показувати тему в непрочитаних якщо категорія не ігнорується.", + "ignoring.description": "Не сповіщати мене про нові відповіді.
Не показувати тему в непрочитаних.", + "thread_tools.title": "Інструменти теми", + "thread_tools.markAsUnreadForAll": "Відмітити для всіх як непрочитана.", + "thread_tools.pin": "Прикріпити тему", + "thread_tools.unpin": "Відкріпити тему", + "thread_tools.lock": "Заблокувати тему", + "thread_tools.unlock": "Розблокувати тему", + "thread_tools.move": "Перемістити тему", + "thread_tools.move-posts": "Перемістити Пости", + "thread_tools.move_all": "Перемістити всі", + "thread_tools.change_owner": "Змінити Власника", + "thread_tools.select_category": "Обрати Категорію", + "thread_tools.fork": "Відгалужити тему", + "thread_tools.delete": "Видалити тему", + "thread_tools.delete-posts": "Видалити пости", + "thread_tools.delete_confirm": "Ви точно бажаєте видалити цю тему?", + "thread_tools.restore": "Відновити тему", + "thread_tools.restore_confirm": "Ви точно бажаєте відновити цю тему?", + "thread_tools.purge": "Стерти тему", + "thread_tools.purge_confirm": "Ви точно бажаєте стерти цю тему?", + "thread_tools.merge_topics": "Об'єднати теми", + "thread_tools.merge": "Об'єднати", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "Ви точно бажаєте видалити цей пост?", + "post_restore_confirm": "Ви точно бажаєте відновити цей пост?", + "post_purge_confirm": "Ви точно бажаєте стерти цей пост?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "Завантаження категорій", + "confirm_move": "Перемістити", + "confirm_fork": "Відгалужити", + "bookmark": "Закладка", + "bookmarks": "Закладки", + "bookmarks.has_no_bookmarks": "Ви ще не додали в закладки жодного поста.", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "Завантажуємо більше постів", + "move_topic": "Перемістити тему", + "move_topics": "Перемістити теми", + "move_post": "Перемістити пост", + "post_moved": "Пост переміщено!", + "fork_topic": "Відгалужити тему", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "Тисніть пости які ви бажаєте відгалужити", + "fork_no_pids": "Не вибрано жодного поста!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "вибрано %1 пост(ів) ", + "fork_success": "Тему успішно відгалужено. Тисніть тут, щоб перейти до відгалуженої теми.", + "delete_posts_instruction": "Тисніть пости які ви бажаєте видалити/стерти", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "Клікніть на дописи які ви хочете призначити іншому користувачу", + "composer.title_placeholder": "Уведіть заголовок теми...", + "composer.handle_placeholder": "Enter your name/handle here", + "composer.discard": "Скасувати", + "composer.submit": "Надіслати", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "Відповідь для %1", + "composer.new_topic": "Cтворити тему", + "composer.editing": "Editing", + "composer.uploading": "завантаження...", + "composer.thumb_url_label": "Вставте URL мініатюри теми", + "composer.thumb_title": "Додати мініатюру цій темі", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Або завантажте файл", + "composer.thumb_remove": "Очистити поля", + "composer.drag_and_drop_images": "Перетягніть сюди зображення", + "more_users_and_guests": "ще %1 користувач(і) та %2 гостей", + "more_users": "ще %1 користувач(і)", + "more_guests": "ще %1 гостей", + "users_and_others": "%1 та %2 інших", + "sort_by": "Сортувати за", + "oldest_to_newest": "Старі > Нові", + "newest_to_oldest": "Нові > Старі", + "most_votes": "Найбільше Голосів", + "most_posts": "Найбільше Постів", + "most_views": "Most Views", + "stale.title": "Створити натомість нову тему?", + "stale.warning": "Тема на котру ви відповідаєте досить стара. Не бажаєте натомість створити новую тему і зіслатися на цю у вашій відповіді?", + "stale.create": "Так, створити нову тему", + "stale.reply_anyway": "Ні, відповісти все ж на існуючу", + "link_back": "Re: [%1](%2)", + "diffs.title": "Історія редагування посту", + "diffs.description": "Цей пост має %1 версій. Натисніть одну з наведених нижче змін, щоб переглянути вміст публікації в той момент часу.", + "diffs.no-revisions-description": "Цей пост має %1 версій.", + "diffs.current-revision": "поточна ревізія", + "diffs.original-revision": "початкова ревізія", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 пізніше", + "timeago_earlier": "%1 раніше", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/uk/unread.json b/public/language/uk/unread.json new file mode 100644 index 0000000000..bd4079565b --- /dev/null +++ b/public/language/uk/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Непрочитані", + "no_unread_topics": "Не залишилось непрочитаних тем.", + "load_more": "Завантажити більше", + "mark_as_read": "Помітити як прочитане", + "selected": "Вибрано", + "all": "Всі", + "all_categories": "Всі категорії", + "topics_marked_as_read.success": "Теми відмічені прочитаними!", + "all-topics": "Всі теми", + "new-topics": "Нові теми", + "watched-topics": "Переглянуті теми", + "unreplied-topics": "Теми без відповіді", + "multiple-categories-selected": "Мультивибір" +} \ No newline at end of file diff --git a/public/language/uk/uploads.json b/public/language/uk/uploads.json new file mode 100644 index 0000000000..e95a92baa4 --- /dev/null +++ b/public/language/uk/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Завантаження файлу...", + "select-file-to-upload": "Оберіть файл для завантаження!", + "upload-success": "Файл успішно завантажено!", + "maximum-file-size": "Максимально %1 кб", + "no-uploads-found": "Не знайдені завантаження", + "public-uploads-info": "Завантаження є публічними, всі користувачі можуть їх бачити.", + "private-uploads-info": "Завантаження є приватними, тільки залогінені користувачі можуть їх бачити." +} \ No newline at end of file diff --git a/public/language/uk/user.json b/public/language/uk/user.json new file mode 100644 index 0000000000..ec842ecdf2 --- /dev/null +++ b/public/language/uk/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Заблокований", + "muted": "Muted", + "offline": "Не в мережі", + "deleted": "Видалено", + "username": "Ім'я користувача", + "joindate": "Дата вступу", + "postcount": "Кількість постів", + "email": "Електронна пошта", + "confirm_email": "Підтвердження пошти", + "account_info": "Акаунт", + "admin_actions_label": "Administrative Actions", + "ban_account": "Заборонити акаунт", + "ban_account_confirm": "Ви точно хочете забанити цього користувача?", + "unban_account": "Розбанити акаунт", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "Видалити акаунт", + "delete_account_as_admin": "Delete Account", + "delete_content": "Delete Account Content", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "Акаунт видалено", + "account-content-deleted": "Account content deleted", + "fullname": "Повне ім'я", + "website": "Сайт", + "location": "Місце", + "age": "Вік", + "joined": "Приєднаний", + "lastonline": "Востаннє в мережі", + "profile": "Профіль", + "profile_views": "Переглядів профілю", + "reputation": "Репутація", + "bookmarks": "Закладки", + "watched_categories": "Категорії, за якими ви спостерігаєте", + "change_all": "Змінити Всі", + "watched": "Переглянуті", + "ignored": "Ігнорується", + "default-category-watch-state": "Спостереження за категоріями за замовчанням", + "followers": "Відстежувачі", + "following": "Відстежувані", + "blocks": "Блокування", + "block_toggle": "Увімкнути Блокування", + "block_user": "Заблокувати Користувача", + "unblock_user": "Розблокувати Користувача", + "aboutme": "Про мене", + "signature": "Підпис", + "birthday": "День народження", + "chat": "Чат", + "chat_with": "Продовжити чат з %1", + "new_chat_with": "Почати новий чат з %1", + "flag-profile": "Поскаржитись на профіль", + "follow": "Стежити", + "unfollow": "Не стежити", + "more": "Більше", + "profile_update_success": "Профіль успішно оновлений!", + "change_picture": "Змінити зображення", + "change_username": "Змінити ім'я користувача", + "change_email": "Змінити електронну пошту", + "email_same_as_password": "Будь-ласка введіть ваш поточний пароль щоб продовжити – ви ввели ваш новий емейл знову", + "edit": "Редагувати", + "edit-profile": "Редагувати профіль", + "default_picture": "Стандартна іконка", + "uploaded_picture": "Завантажене зображення", + "upload_new_picture": "Завантажити нове зображення", + "upload_new_picture_from_url": "Завантажити нове зображення з URL", + "current_password": "Поточний пароль", + "change_password": "Змінити пароль", + "change_password_error": "Невірний пароль!", + "change_password_error_wrong_current": "Ваш поточний пароль не вірний!", + "change_password_error_match": "Паролі мають співпадати!", + "change_password_error_privileges": "У вас немає прав змінювати цей пароль.", + "change_password_success": "Ваш пароль оновлено!", + "confirm_password": "Підтвердіть пароль", + "password": "Пароль", + "username_taken_workaround": "Ім'я користувача, що ви обрали, вже було зайняте, то ж ми його трішки змінили. Ви тепер відомі як %1", + "password_same_as_username": "Ваш пароль співпадає з іменем користувача. Оберіть інший пароль, будь ласка.", + "password_same_as_email": "Ваш пароль співпадає з електронною поштою. Оберіть інший пароль, будь ласка.", + "weak_password": "Слабкий пароль", + "upload_picture": "Завантажити зображення", + "upload_a_picture": "Завантажити зображення", + "remove_uploaded_picture": "Видалити завантажене зображення", + "upload_cover_picture": "Завантажити обкладинку", + "remove_cover_picture_confirm": "Ви точно бажаєте видалити обкладинку?", + "crop_picture": "Обрізати зображення", + "upload_cropped_picture": "Обрізати та завантажити", + "avatar-background-colour": "Avatar background colour", + "settings": "Налаштування", + "show_email": "Показувати мою пошту", + "show_fullname": "Показувати повне ім'я", + "restrict_chats": "Дозволяти чат повідомлення лише від користувачів за якими я стежу", + "digest_label": "Підписатися на дайджест", + "digest_description": "Підписатися на оновлення цього форуму по електронній пошті (нові оповіщення та теми) згідно заданого розкладу", + "digest_off": "Ніколи", + "digest_daily": "Щоденно", + "digest_weekly": "Щотижнево", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "Щомісячно", + "has_no_follower": "Цей користувач не має відстежувачів :(", + "follows_no_one": "Цей користувач нікого не відстежує :(", + "has_no_posts": "Цей користувач ще ніколи нічого не постив.", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "Цей користувач ще ніколи не створював нових тем.", + "has_no_watched_topics": "Цей користувач ще ніколи не переглядав жодної теми.", + "has_no_ignored_topics": "Цей користувач ще не проігнорував будь-які теми.", + "has_no_upvoted_posts": "Цей користувач ще не голосував за жоден з постів.", + "has_no_downvoted_posts": "Цей користувач ще не голосував проти жодного поста.", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "Ви нікого не заблокували.", + "email_hidden": "Електронна адреса прихована", + "hidden": "прихований", + "paginate_description": "Розбивати список тем та постів на сторінки замість нескінченної прокрутки", + "topics_per_page": "Тем на сторінку", + "posts_per_page": "Постів на сторінку", + "max_items_per_page": "Максимум %1", + "acp_language": "Мова сторінки адміністратора", + "notifications": "Notifications", + "upvote-notif-freq": "Частота сповіщень позитивних відгуків", + "upvote-notif-freq.all": "Всі позитивні відгуки", + "upvote-notif-freq.first": "Перше в дописі", + "upvote-notif-freq.everyTen": "Кожні 10 позитивних відгуків", + "upvote-notif-freq.threshold": "На 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "На 10, 100, 1000...", + "upvote-notif-freq.disabled": "Вимкнено", + "browsing": "Налаштування перегляду", + "open_links_in_new_tab": "Відкривати зовнішні посилання у новій вкладці", + "enable_topic_searching": "Увімкнути пошук у темах", + "topic_search_help": "Будучи увімкненою, ця функція перевизначає вбудований пошук браузера і дозволяє шукати по всій темі, а не лише по змісту, що показаний на екрані.", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "Після відправки відповіді, показувати новий пост", + "follow_topics_you_reply_to": "Підписуватися на теми в котрих ви відповідаєте", + "follow_topics_you_create": "Підписуватися на теми які ви створюєте", + "grouptitle": "Заголовок групи", + "group-order-help": "Оберіть групу і використовуйте стрілки для зміни порядку заголовків", + "no-group-title": "Немає заголовка групи", + "select-skin": "Обрати стиль сайту", + "select-homepage": "Обрати домашню сторінку", + "homepage": "Домашня сторінка", + "homepage_description": "Вкажіть сторінку в якості першої сторінки форуму або \"None\", для використання сторінки за замовчуванням.", + "custom_route": "Шлях першої сторінки", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "Сервіси єдиного входу", + "sso.associated": "Зв'язані з", + "sso.not-associated": "Натисніть тут, щоб зв'язати з", + "sso.dissociate": "Від'єднати", + "sso.dissociate-confirm-title": "Підтвердьте від'єднання", + "sso.dissociate-confirm": "Ви впевнені, що хочете від'єднати свій акаунт від %1?", + "info.latest-flags": "Останні скарги", + "info.no-flags": "Не знайдено постів зі скаргами", + "info.ban-history": "Історія банів", + "info.no-ban-history": "Цього користувача ніколи не банили", + "info.banned-until": "Забанений до %1", + "info.banned-expiry": "Expiry", + "info.banned-permanently": "Забанений назавжди", + "info.banned-reason-label": "Причина", + "info.banned-no-reason": "Причина не вказана", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "Історія імені користувача", + "info.email-history": "Історія електронної пошти", + "info.moderation-note": "Коментар модератора", + "info.moderation-note.success": "Коментар модератора збережено", + "info.moderation-note.add": "Додати коментар", + "sessions.description": "Ця сторінка дозволяє вам переглядати будь-які активні сесії на цьому форумі та видаляти їх якщо потрібно. Ви можете видалити вашу власну сесію, якщо вийдете зі свого акаунта.", + "consent.title": "Ваші Права & Згода", + "consent.lead": "Цей форум збирає та обробляє вашу особисту інформацію.", + "consent.intro": "Ми використовуємо цю інформацію виключно з метою персоналізації вашої активності у цій спільноті, а також для з'єднання ваших постів з вашим особистим акаунтом. На етапі реєстрації ми просили вас надати ім'я користувача та електронну пошту, також ви можете (необов'язково) надати нам додаткову інформацію, щоб завершити створення свого користувацького профілю на цьому сайті.

Ми зберігаємо цю інформацію протягом всього періоду життя вашого акаунту, і ви можете відкликати свою згоду у будь-який час, якщо видалите акаунт. У будь-який час ви можете отримати копію ваших особистих даних та внеску на цьому сайті через свою сторінку Права & Згода.

Якщо у вас виникли будь-які питання або зауваження, ми заохочуємо вас звернутись до команди Адміністраторів цього форуму.", + "consent.email_intro": "Інколи ми можемо відправляти поштові повідомлення на вашу зареєстровану електронну скриньку для інформування вас про оновлення на сайті та/або надання вам інформації про активність на сайті, в якій ви можете бути зацікавлені. Ви можете змінювати частоту отримання дайджесту (або повністю вимкнути його), а також обрати, які типи повідомлень ви бажаєте отримувати, через сторінку налаштувань користувача.", + "consent.digest_frequency": "Якщо ви не зміните цього у ваших налаштуваннях користувача, ця спільнота відправлятиме дайджести електронною поштою кожні %1.", + "consent.digest_off": "Якщо ви не зміните цього у ваших налаштуваннях користувача, ця спільнота не відправлятиме дайджести електронною поштою.", + "consent.received": "Ви надали свою згоду цьому веб-сайту на збір та обробку вашої інформації.", + "consent.not_received": "Ви не надали свою згоду на збір та обробку інформації. У будь-який час адміністрація цього веб-сайту може видалити ваш акаунт, дотримуючись правил Загального Регламенту про Захист Даних.", + "consent.give": "Надати згоду", + "consent.right_of_access": "У вас є Право на Доступ", + "consent.right_of_access_description": "У вас є право на доступ до ваших даних, які збираються цим веб-сайтом, за першою вимогою. Ви можете отримати копію цих даних, натиснувши на відповідну кнопку внизу.", + "consent.right_to_rectification": "У вас є Право на Виправлення", + "consent.right_to_rectification_description": "Ви маєте право змінювати або оновлювати будь-які неточні дані, які ви нам надали. Ваш профіль можна оновлювати через редагування профілю, також зміст ваших постів завжди можна відредагувати. Якщо ви не можете цього зробити, будь-ласка зверніться до команди адміністраторів цього сайту.", + "consent.right_to_erasure": "У вас є Право на Стирання", + "consent.right_to_erasure_description": "У будь-який час ви можете відкликати свою згоду на збір та/або обробку інформації шляхом видалення власного акаунту. Ваш особистий профіль можна видалити, але розміщений вами контент залишиться. Якщо ви хочете видалити ваш акаунт разом з контентом, будь-ласка зверніться до команди адміністраторів цього веб-сайту.", + "consent.right_to_data_portability": "У вас є Право на Переносимість Даних", + "consent.right_to_data_portability_description": "Ви можете отримати від нас експортовану копію машинно-читабельних даних, які були зібрані про вас і ваш акаунт. Ви можете це зробити, натиснувши на відповідну кнопку внизу.", + "consent.export_profile": "Export Profile (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "Експортувати Завантажений Контент (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "Експортувати Пости (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/uk/users.json b/public/language/uk/users.json new file mode 100644 index 0000000000..9495f89850 --- /dev/null +++ b/public/language/uk/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Останні користувачі", + "top_posters": "Кращі автори", + "most_reputation": "Найбільша репутація", + "most_flags": "Найбільше скарг", + "search": "Пошук", + "enter_username": "Уведіть ім'я користувача для пошуку", + "search-user-for-chat": "Search a user to start chat", + "load_more": "Завантажити більше", + "users-found-search-took": "%1 користувач(ів) знайдено! Пошук тривав %2 секунди.", + "filter-by": "Фільтрувати за", + "online-only": "Лише в мережі", + "invite": "Запросити", + "prompt-email": "Емейли:", + "groups-to-join": "Groups to be joined when invite is accepted:", + "invitation-email-sent": "Лист із запрошенням відправлено %1", + "user_list": "Список користувачів", + "recent_topics": "Нещодавні теми", + "popular_topics": "Популярні теми", + "unread_topics": "Непрочитані теми", + "categories": "Категорії", + "tags": "Теги", + "no-users-found": "Жодного користувача не знайдено!" +} \ No newline at end of file diff --git a/public/language/vi/_DO_NOT_EDIT_FILES_HERE.md b/public/language/vi/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/vi/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/vi/admin/admin.json b/public/language/vi/admin/admin.json new file mode 100644 index 0000000000..2123023f10 --- /dev/null +++ b/public/language/vi/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "Bạn có chắc chắn muốn xây lại và khởi động lại NodeBB?", + "alert.confirm-restart": "Bạn có thật sự muốn khởi động lại NodeBB", + + "acp-title": "%1 | Bảng Điểu Khiển Quản Trị Viên NodeBB", + "settings-header-contents": "Nội dung", + "changes-saved": "Đã Lưu Thay Đổi", + "changes-saved-message": "Các thay đổi của bạn đối với cấu hình NodeBB đã được lưu.", + "changes-not-saved": "Thay Đổi Chưa Được Lưu", + "changes-not-saved-message": "NodeBB đã gặp sự cố khi lưu các thay đổi của bạn. (%1)" +} \ No newline at end of file diff --git a/public/language/vi/admin/advanced/cache.json b/public/language/vi/admin/advanced/cache.json new file mode 100644 index 0000000000..8a17c99086 --- /dev/null +++ b/public/language/vi/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "Bộ nhớ đệm bài viết", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% Đầy", + "post-cache-size": "Kích thước cache bài viết", + "items-in-cache": "Thành phần trong Cache" +} \ No newline at end of file diff --git a/public/language/vi/admin/advanced/database.json b/public/language/vi/admin/advanced/database.json new file mode 100644 index 0000000000..ca6530dc65 --- /dev/null +++ b/public/language/vi/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Thời gian hoạt động(giây)", + "uptime-days": "Thời gian hoạt động(Ngày)", + + "mongo": "Mongo", + "mongo.version": "Phiên bản MongoDB ", + "mongo.storage-engine": "Phương Thức Lưu Trữ", + "mongo.collections": "Tập dữ liệu", + "mongo.objects": "Đối tượng", + "mongo.avg-object-size": "Kích thước trung bình", + "mongo.data-size": "Kích thước dữ liệu", + "mongo.storage-size": "Kích thước lưu trữ", + "mongo.index-size": "Kích thước chỉ mục", + "mongo.file-size": "Kích cỡ tệp", + "mongo.resident-memory": "Bộ Nhớ Thường Trú", + "mongo.virtual-memory": "Bộ Nhớ Ảo", + "mongo.mapped-memory": "Bộ Nhớ Được Ánh Xạ", + "mongo.bytes-in": "Byte trong", + "mongo.bytes-out": "Byte ngoài", + "mongo.num-requests": "Số lượng yêu cầu", + "mongo.raw-info": "Thông tin MongoDB", + "mongo.unauthorized": "NodeBB không thể truy vấn cơ sở dữ liệu MongoDB để thống kê có liên quan. Vui lòng đảm bảo rằng người dùng đang sử dụng bởi NodeBB chứa vai trò "clusterMonitor" cho cơ sở dữ liệu "quản trị viên".", + + "redis": "Redis", + "redis.version": "Phiên bản Redis", + "redis.keys": "Chìa khóa", + "redis.expires": "Hết hạn", + "redis.avg-ttl": "TTL Trung Bình", + "redis.connected-clients": "Khách Đã Kết Nối", + "redis.connected-slaves": "Nô lệ được kết nối", + "redis.blocked-clients": "Khách Xem Bị Khóa", + "redis.used-memory": "Bộ Nhớ Đã Sử Dụng", + "redis.memory-frag-ratio": "Tỷ lệ phân mảnh bộ nhớ", + "redis.total-connections-recieved": "Tổng Số Kết Nối Nhận Được", + "redis.total-commands-processed": "Tổng Số Kết Nối Được Xử Lý", + "redis.iops": "Hoạt động tức thời. Môi giây", + "redis.iinput": "Đầu Vào Tức Thời Mỗi Giây", + "redis.ioutput": "Đầu Ra Tức Thời Mỗi Giây", + "redis.total-input": "Tổng Đầu Vào", + "redis.total-output": "Tổng Đầu Ra", + + "redis.keyspace-hits": "Truy Cập Keyspace", + "redis.keyspace-misses": "Bỏ Lỡ Keyspace", + "redis.raw-info": "Thông Tin Gốc Của Redis", + + "postgres": "Postgres", + "postgres.version": "Phiên Bản PostgreSQL", + "postgres.raw-info": "Thông tin gốc của Postgres" +} diff --git a/public/language/vi/admin/advanced/errors.json b/public/language/vi/admin/advanced/errors.json new file mode 100644 index 0000000000..70182f48a9 --- /dev/null +++ b/public/language/vi/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Hình %1", + "error-events-per-day": "sự kiện mỗi ngày %1", + "error.404": "404 Không Tìm Thấy", + "error.503": "503 Dịch Vụ Không Sẵn Có", + "manage-error-log": "Quản Lý Nhật Ký Lỗi", + "export-error-log": "Xuất Nhật ký Lỗi (CSV)", + "clear-error-log": "Xóa Nhật Ký Lỗi", + "route": "Liên kết", + "count": "Số lượng", + "no-routes-not-found": "Hoan hô! Không có lỗi 404!", + "clear404-confirm": "Bạn có chắc chắn muốn xóa nhật ký lỗi 404 không?", + "clear404-success": "Đã xóa lỗi \"404 Không Tìm Lấy\"" +} \ No newline at end of file diff --git a/public/language/vi/admin/advanced/events.json b/public/language/vi/admin/advanced/events.json new file mode 100644 index 0000000000..1ac68d471b --- /dev/null +++ b/public/language/vi/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "Sự kiện", + "no-events": "Không có sự kiện", + "control-panel": "Bảng Điều Khiển Sự Kiện", + "delete-events": "Xóa Sự Kiện", + "confirm-delete-all-events": "Bạn có chắc chắn muốn xóa tất cả các sự kiện đã ghi không?", + "filters": "Bộ lọc", + "filters-apply": "Áp Dụng Bộ Lọc", + "filter-type": "Loại Sự Kiện", + "filter-start": "Ngày Bắt Đầu", + "filter-end": "Ngày Kết Thúc", + "filter-perPage": "Mỗi Trang" +} \ No newline at end of file diff --git a/public/language/vi/admin/advanced/logs.json b/public/language/vi/admin/advanced/logs.json new file mode 100644 index 0000000000..ede9d0df44 --- /dev/null +++ b/public/language/vi/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Nhật ký", + "control-panel": "Bảng Điều Khiển Nhật Ký", + "reload": "Tải Lại Nhật Ký", + "clear": "Xóa Nhật Ký", + "clear-success": "Đã Xóa Nhật Ký!" +} \ No newline at end of file diff --git a/public/language/vi/admin/appearance/customise.json b/public/language/vi/admin/appearance/customise.json new file mode 100644 index 0000000000..fd8785bb1f --- /dev/null +++ b/public/language/vi/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Tùy Chỉnh CSS/LESS", + "custom-css.description": "Nhập các khai báo CSS / LESS của bạn tại đây, nó sẽ được áp dụng sau tất cả các kiểu khác.", + "custom-css.enable": "Bật Tùy Chỉnh CSS/LESS", + + "custom-js": "Javascript tùy chỉnh", + "custom-js.description": "Nhập javascript của riêng bạn ở đây. Nó sẽ được thực hiện sau khi trang được tải hoàn toàn.", + "custom-js.enable": "Bật Javascript tùy chỉnh", + + "custom-header": "Tùy Chỉnh Phần Đầu Trang", + "custom-header.description": "Nhập HTML tùy chỉnh tại đây (VD: Thẻ Meta, v.v...), sẽ được thêm vào phần <head>. Thẻ Script được phép, nhưng không được khuyến khích, vì phần Tùy Chỉnh Javascript đã có sẵn.", + "custom-header.enable": "Bật Tùy Chỉnh Phần Đầu Trang", + + "custom-css.livereload": "Bật tải lại trực tiếp", + "custom-css.livereload.description": "Bật điều này để buộc tất cả các phiên trên mọi thiết bị trong tài khoản của bạn phải làm mới bất cứ khi nào bạn nhấp vào lưu" +} \ No newline at end of file diff --git a/public/language/vi/admin/appearance/skins.json b/public/language/vi/admin/appearance/skins.json new file mode 100644 index 0000000000..faf0579331 --- /dev/null +++ b/public/language/vi/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Đang tải giao diện ...", + "homepage": "Trang chủ", + "select-skin": "Chọn giao diện", + "current-skin": "Giao diện hiện tại", + "skin-updated": "Đã cập nhật giao diện", + "applied-success": "%1 giao diện đã được sử dụng thành công", + "revert-success": "Đã trả giao diện về màu cơ bản" +} \ No newline at end of file diff --git a/public/language/vi/admin/appearance/themes.json b/public/language/vi/admin/appearance/themes.json new file mode 100644 index 0000000000..1d326191dd --- /dev/null +++ b/public/language/vi/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Đang kiểm tra các giao diện đã cài đặt...", + "homepage": "Trang chủ", + "select-theme": "Chọn Giao Diện", + "current-theme": "Giao Diện Hiện Tại", + "no-themes": "Không tìm thấy giao diện đã cài đặt", + "revert-confirm": "Bạn có chắc muốn khôi phục giao diện NodeBB mặc định không?", + "theme-changed": "Đã Đổi Giao Diện", + "revert-success": "Bạn đã thành công chuyển lại NodeBB của mình về giao diện mặc định.", + "restart-to-activate": "Vui lòng xây dựng lại và khởi động lại NodeBB của bạn để kích hoạt đầy đủ giao diện này." +} \ No newline at end of file diff --git a/public/language/vi/admin/dashboard.json b/public/language/vi/admin/dashboard.json new file mode 100644 index 0000000000..7ab9f47374 --- /dev/null +++ b/public/language/vi/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Lưu lượng truy cập", + "page-views": "Xem Trang", + "unique-visitors": "Khách Truy Cập Duy Nhất", + "logins": "Đăng nhập", + "new-users": "Người dùng mới", + "posts": "Lượt Đăng", + "topics": "Chủ đề", + "page-views-seven": "7 ngày trước", + "page-views-thirty": "30 ngày trước", + "page-views-last-day": "24 giờ trước", + "page-views-custom": "Tùy chỉnh phạm vi ngày", + "page-views-custom-start": "Phạm vi bắt đầu", + "page-views-custom-end": "Phạm vi kết thúc", + "page-views-custom-help": "Nhập phạm vi ngày mà bạn muốn xem lượt xem trang. Nếu không có bộ chọn ngày, chấp nhận định dạng là YYYY-MM-DD", + "page-views-custom-error": "Vui lòng nhập một phạm vi ngày hợp lệ trong định dạng YYYY-MM-DD", + + "stats.yesterday": "Hôm qua", + "stats.today": "Hôm nay", + "stats.last-week": "Tuần trước", + "stats.this-week": "Tuần này", + "stats.last-month": "Tháng trước", + "stats.this-month": "Tháng này", + "stats.all": "Mọi lúc", + + "updates": "Cập nhật", + "running-version": "Bạn đang chạy NodeBB v%1.", + "keep-updated": "Đảm bảo NodeBB của bạn luôn cập nhật các bản vá bảo mật và sửa lỗi mới nhất.", + "up-to-date": "

Bạn đã cập nhật bản mới nhất

", + "upgrade-available": "

Phiên bản mới (v%1) đã được phát hành. Xem xét nâng cấp NodeBB của bạn.

", + "prerelease-upgrade-available": "

Đây là bản phát hành NodeBB đã lỗi thời. Phiên bản mới (v%1) đã phát hành. Xem xét nâng cấp NodeBB của bạn.

", + "prerelease-warning": "

Đây là phiên bản NodeBB trước phát hành. Lỗi ngoài ý muốn có thể xảy ra.

", + "fallback-emailer-not-found": "Không tìm thấy trình gửi email dự phòng!", + "running-in-development": "Diễn đàn đang chạy ở chế độ phát triển. Diễn đàn có thể mở cho các lỗ hổng tiềm ẩn; Xin vui lòng liên hệ với quản trị hệ thống của bạn.", + "latest-lookup-failed": "

Không tra cứu được phiên bản NodeBB mới nhất

", + + "notices": "Thông báo", + "restart-not-required": "Không cần khởi động lại", + "restart-required": "Yêu cầu khởi động lại", + "search-plugin-installed": "Đã cài đặt plugin tìm kiếm", + "search-plugin-not-installed": "Plugin Tìm Kiếm chưa được cài đặt", + "search-plugin-tooltip": "Cài đặt một plugin tìm kiếm từ trang plugin để kích hoạt chức năng tìm kiếm", + + "control-panel": "Điều khiển hệ thống", + "rebuild-and-restart": "Xây Dựng Lại & Khởi Động Lại", + "restart": "Khởi động lại", + "restart-warning": "Xây dựng lại hoặc Khởi động lại NodeBB của bạn sẽ hủy tất cả các kết nối hiện có trong vài giây.", + "restart-disabled": "Việc xây dựng lại và khởi động lại NodeBB của bạn đã bị vô hiệu hóa vì bạn dường như không chạy nó qua daemon thích hợp.", + "maintenance-mode": "Chế Độ Bảo Trì", + "maintenance-mode-title": "Bấm vào đây để thiết lập chế độ bảo trì cho NodeBB", + "realtime-chart-updates": "Cập Nhật Biểu Đồ Thời Gian Thực", + + "active-users": "Người Dùng Hoạt Động", + "active-users.users": "Người Dùng", + "active-users.guests": "Khách", + "active-users.total": "Tổng", + "active-users.connections": "Kết nối", + + "guest-registered-users": "Khách vs Người dùng đã đăng ký", + "guest": "Khách", + "registered": "Đã đăng ký", + + "user-presence": "Người Dùng Có Mặt", + "on-categories": "Trên Danh Sách Chuyên Mục", + "reading-posts": "Đọc bài viết", + "browsing-topics": "Duyệt qua chủ đề", + "recent": "Gần đây", + "unread": "Chưa đọc", + + "high-presence-topics": "Chủ Đề Hiện Diện Cao", + "popular-searches": "Tìm kiếm Phổ biến", + + "graphs.page-views": "Xem Trang", + "graphs.page-views-registered": "Đã Đăng Ký Xem Trang", + "graphs.page-views-guest": "Khách Xem Trang", + "graphs.page-views-bot": "Bot Xem Trang", + "graphs.unique-visitors": "Khách Truy Cập Duy Nhất", + "graphs.registered-users": "Thành Viên Chính Thức", + "graphs.guest-users": "Người dùng khách", + "last-restarted-by": "Khởi động lại lần cuối bởi", + "no-users-browsing": "Người không xem bài", + + "back-to-dashboard": "Quay lại Bảng điều khiển", + "details.no-users": "Không có người dùng nào tham gia trong khung thời gian đã chọn", + "details.no-topics": "Không có chủ đề nào được đăng trong khung thời gian đã chọn", + "details.no-searches": "Chưa có tìm kiếm nào", + "details.no-logins": "Không có thông tin đăng nhập nào được ghi lại trong khung thời gian đã chọn", + "details.logins-static": "NodeBB chỉ lưu dữ liệu phiên trong %1 ngày và do đó, bảng này bên dưới sẽ chỉ hiển thị các phiên hoạt động gần đây nhất", + "details.logins-login-time": "Thời gian đăng nhập" +} diff --git a/public/language/vi/admin/development/info.json b/public/language/vi/admin/development/info.json new file mode 100644 index 0000000000..4a76411c21 --- /dev/null +++ b/public/language/vi/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "Bạn đang trên %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nút đã phản hồi trong %2ms!", + "host": "máy chủ", + "primary": "công việc chính/điều hành", + "pid": "pid", + "nodejs": "nodejs", + "online": "trực tuyến", + "git": "git", + "process-memory": "xử lý bộ nhớ", + "system-memory": "bộ nhớ hệ thống", + "used-memory-process": "Đã sử dụng bộ nhớ theo quy trình", + "used-memory-os": "Bộ nhớ hệ thống đã sử dụng", + "total-memory-os": "Tổng bộ nhớ hệ thống", + "load": "tải hệ thống", + "cpu-usage": "sử dụng cpu", + "uptime": "thời gian hoạt động", + + "registered": "Đã đăng ký", + "sockets": "Sockets", + "guests": "Khách", + + "info": "Thông tin" +} \ No newline at end of file diff --git a/public/language/vi/admin/development/logger.json b/public/language/vi/admin/development/logger.json new file mode 100644 index 0000000000..b3e3e9bd1d --- /dev/null +++ b/public/language/vi/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Cài Đặt Ghi Nhật Ký", + "description": "Bật cái này, bạn sẽ nhận nhật ký ở công cụ dòng lệnh của bạn. Nếu có đường dẫn cụ thể, nhật ký sẽ được lưu vào một tệp thay thế. Ghi nhật ký HTTP có lợi để thu thập thống kê về ai, khi nào và những gì mọi người truy cập trên diễn đàn. Ngoài ghi nhật ký yêu cầu HTTP, chúng tôi có thể ghi nhật ký sự kiện socket.io. Ghi nhật ký Socket.io, kết hợp với màn hình redis-cli, có thể hữu ích để tìm hiểu nội bộ NodeBB.", + "explanation": "Chỉ cần chọn/bỏ chọn cài đặt ghi nhật ký để bật hoặc tắt ghi nhật ký một cách nhanh chóng. Không cần khởi động lại.", + "enable-http": "Bật ghi nhật ký HTTP", + "enable-socket": "Bật ghi nhật ký sự kiện socket.io", + "file-path": "Đường dẫn đến tệp nhật ký", + "file-path-placeholder": "/path/to/log/file.log ::: để trống để hiện nhật ký trên cửa sổ dòng lệnh", + + "control-panel": "Bảng Điều Khiển Ghi Nhật Ký", + "update-settings": "Cập Nhật Cài Đặt Ghi Nhật Ký" +} \ No newline at end of file diff --git a/public/language/vi/admin/extend/plugins.json b/public/language/vi/admin/extend/plugins.json new file mode 100644 index 0000000000..1074a7fafd --- /dev/null +++ b/public/language/vi/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Nổi bật", + "installed": "Đã cài đặt", + "active": "Kích hoạt", + "inactive": "Chưa kích hoạt", + "out-of-date": "Hết Hạn", + "none-found": "Không tìm thấy plugin nào.", + "none-active": "Không Có Plugin Hoạt Động", + "find-plugins": "Tìm Plugins", + + "plugin-search": "Tìm Plugin", + "plugin-search-placeholder": "Tìm kiếm plugin...", + "submit-anonymous-usage": "Gửi dữ liệu sử dụng plugin ẩn danh.", + "reorder-plugins": "Sắp Xếp Lại Plugin", + "order-active": "Sắp Xếp Plugin Hoạt Động", + "dev-interested": "Bạn quan tâm đến việc viết plugin cho NodeBB?", + "docs-info": "Tài liệu đầy đủ về tác giả plugin có thể được tìm thấy trong Cổng Thông Tin Tài Liệu NodeBB .", + + "order.description": "Một số plugin nhất định hoạt động lý tưởng khi chúng được khởi tạo trước / sau các plugin khác.", + "order.explanation": "Các plugin tải theo thứ tự đã đặt ra ở đây, từ trên xuống dưới", + + "plugin-item.themes": "Giao diện", + "plugin-item.deactivate": "Vô Hiệu", + "plugin-item.activate": "Kích hoạt", + "plugin-item.install": "Cài đặt", + "plugin-item.uninstall": "Gỡ cài đặt", + "plugin-item.settings": "Cài đặt", + "plugin-item.installed": "Đã Cài Đặt", + "plugin-item.latest": "Mới nhất", + "plugin-item.upgrade": "Nâng cấp", + "plugin-item.more-info": "Để biết thêm thông tin:", + "plugin-item.unknown": "Không Xác Định", + "plugin-item.unknown-explanation": "Không thể xác định trạng thái của plugin này, có thể do lỗi định cấu hình sai.", + "plugin-item.compatible": "Plugin này hoạt động trên NodeBB %1", + "plugin-item.not-compatible": "Plugin này không có dữ liệu tương thích, hãy đảm bảo rằng nó hoạt động trước khi cài đặt trên môi trường sản phẩm của bạn.", + + "alert.enabled": "Đã Bật Plugin", + "alert.disabled": "Plugin Đã Tắt", + "alert.upgraded": "Đã Nâng Cấp Plugin", + "alert.installed": "Đã Cài Đặt Plugin", + "alert.uninstalled": "Đã Gỡ Bỏ Plugin", + "alert.activate-success": "Vui lòng xây dựng lại và khởi động lại NodeBB của bạn để kích hoạt hoàn toàn plugin này", + "alert.deactivate-success": "Đã hủy kích hoạt plugin thành công", + "alert.upgrade-success": "Vui lòng xây dựng lại và khởi động lại NodeBB của bạn để nâng cấp đầy đủ plugin này.", + "alert.install-success": "Đã cài đặt thành công plugin, vui lòng kích hoạt plugin.", + "alert.uninstall-success": "Đã hủy kích hoạt và gỡ cài đặt plugin thành công.", + "alert.suggest-error": "

NodeBB không thể tiếp cận trình quản lý gói, hãy tiến hành cài đặt phiên bản mới nhất?

Máy chủ trả về (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB không thể tiếp cận trình quản lý gói, bản nâng cấp không được đề xuất vào lúc này.

", + "alert.incompatible": "

Phiên bản NodeBB (v%1) của bạn chỉ được xóa để nâng cấp lên v%2 của plugin này. Vui lòng cập nhật NodeBB của bạn nếu muốn cài đặt phiên bản mới hơn của plugin này.

", + "alert.possibly-incompatible": "

Không Có Thông Tin Tương Thích

Plugin này không đưa ra một phiên bản cụ thể để cài đặt với phiên bản NodeBB của bạn. Không đảm bảo khả năng tương thích hoàn toàn và có thể khiến NodeBB của bạn không hoạt động bình thường.

Trường hợp NodeBB không thể hoạt động đúng:

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

Tiếp tục cài đặt phiên bản mới nhất của plugin này?

", + "alert.reorder": "Các Plugin Đã Được Sắp Xếp Lại", + "alert.reorder-success": "Vui lòng xây dựng lại và khởi động lại NodeBB của bạn để hoàn tất quá trình.", + + "license.title": "Thông Tin Cấp Phép Plugin", + "license.intro": "Plugin %1 được cấp phép theo %2. Vui lòng đọc và hiểu các điều khoản cấp phép trước khi kích hoạt plugin này.", + "license.cta": "Bạn có muốn tiếp tục kích hoạt plugin này không?" +} diff --git a/public/language/vi/admin/extend/rewards.json b/public/language/vi/admin/extend/rewards.json new file mode 100644 index 0000000000..d7a803ae6a --- /dev/null +++ b/public/language/vi/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Phần thưởng", + "condition-if-users": "Nếu Người Dùng", + "condition-is": "Là:", + "condition-then": "Sau đó:", + "max-claims": "Số lần nhận thưởng có thể nhận được", + "zero-infinite": "Nhập 0 cho vô hạn", + "delete": "Xóa", + "enable": "Bật", + "disable": "Tắt", + + "alert.delete-success": "Đã xóa thành công phần thưởng", + "alert.no-inputs-found": "Phần thưởng không hợp lệ - không tìm thấy đầu vào!", + "alert.save-success": "Đã lưu thành công phần thưởng" +} \ No newline at end of file diff --git a/public/language/vi/admin/extend/widgets.json b/public/language/vi/admin/extend/widgets.json new file mode 100644 index 0000000000..dd74d40700 --- /dev/null +++ b/public/language/vi/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Tiện ích có sẵn", + "explanation": "Chọn một tiện ích từ menu thả xuống, sau đó kéo và thả nó vào khu vực tiện ích của mẫu ở bên trái.", + "none-installed": "Không tìm thấy tiện ích nào! Kích hoạt plugin tiện ích cần thiết trong bảng điều khiểnplugins .", + "clone-from": "Sao chép tiện ích từ", + "containers.available": "Vùng Chứa Có Sẵn", + "containers.explanation": "Kéo và thả lên trên bất kỳ tiện ích đang hoạt động", + "containers.none": "Trống", + "container.well": "Tốt", + "container.jumbotron": "Khung Hiển Thị Lớn", + "container.panel": "Bảng", + "container.panel-header": "Tiêu Đề Bảng", + "container.panel-body": "Thân Bảng", + "container.alert": "Cảnh báo", + + "alert.confirm-delete": "Bạn có chắc chắn muốn xóa tiện ích này không?", + "alert.updated": "Đã Cập Nhật Tiện Ích", + "alert.update-success": "Đã cập nhật thành công các tiện ích", + "alert.clone-success": "Đã nhân bản thành công tiện ích", + + "error.select-clone": "Hãy chọn một trang để sao chép từ đó", + + "title": "Tiêu đề", + "title.placeholder": "Tiêu đề (chỉ hiển thị trên một số vùng chứa)", + "container": "Vùng chứa", + "container.placeholder": "Kéo và thả một vùng chứa hoặc nhập HTML vào đây.", + "show-to-groups": "Hiển thị lên nhóm", + "hide-from-groups": "Ẩn khỏi nhóm", + "hide-on-mobile": "Ẩn trên thiết bị di động" +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/admins-mods.json b/public/language/vi/admin/manage/admins-mods.json new file mode 100644 index 0000000000..924a46396e --- /dev/null +++ b/public/language/vi/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Quản Trị Viên", + "global-moderators": "Người Điều Hành Toàn Quyền", + "moderators": "Người điều hành", + "no-global-moderators": "Không Có Người Điều Hành Toàn Quyền", + "no-sub-categories": "Không có danh mục phụ", + "subcategories": "%1 danh mục phụ", + "no-moderators": "Không Có Người Điều Hành", + "add-administrator": "Thêm Quản Trị Viên", + "add-global-moderator": "Thêm Người Điều Hành Toàn Quyền", + "add-moderator": "Thêm Người Điều Hành" +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/categories.json b/public/language/vi/admin/manage/categories.json new file mode 100644 index 0000000000..c592ee4cda --- /dev/null +++ b/public/language/vi/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "Cài Đặt Chuyên Mục", + "privileges": "Đặc quyền", + + "name": "Tên Chuyên Mục", + "description": "Mô Tả Chuyên Mục", + "bg-color": "Màu Nền", + "text-color": "Màu Chữ", + "bg-image-size": "Kích Thước Hình Nền", + "custom-class": "Lớp Tùy Chỉnh", + "num-recent-replies": "# của Trả Lời Gần Đây", + "ext-link": "Liên Kết Bên Ngoài", + "subcategories-per-page": "Danh mục phụ mỗi trang", + "is-section": "Coi danh mục này như một phần", + "post-queue": "Hàng đợi bài đăng", + "tag-whitelist": "Danh Sách Trắng Gắn Thẻ ", + "upload-image": "Tải Lên Ảnh", + "delete-image": "Xóa", + "category-image": "Ảnh Chuyên Mục", + "parent-category": "Chuyên Mục Chính", + "optional-parent-category": "(Tùy chọn) Danh mục chính", + "top-level": "Cấp Độ Hàng Đầu", + "parent-category-none": "(Trống)", + "copy-parent": "Sao Chép Mục Chính", + "copy-settings": "Sao Chép Cài Đặt Từ", + "optional-clone-settings": "(Tùy chọn) Cài đặt sao chép từ danh mục", + "clone-children": "Sao Chép Cài Đặt Và Chuyên Mục Con", + "purge": "Loại Bỏ Chuyên Mục", + + "enable": "Bật", + "disable": "Tắt", + "edit": "Sửa", + "analytics": "Phân tích", + "view-category": "Xem chuyên mục", + "set-order": "Đặt thứ tự", + "set-order-help": "Đặt thứ tự của danh mục sẽ chuyển danh mục này sang thứ tự đó và cập nhật thứ tự của các danh mục khác khi cần thiết. Thứ tự tối thiểu là 1 đặt danh mục ở trên cùng.", + + "select-category": "Chọn Chuyên Mục", + "set-parent-category": "Đặt Chuyên Mục Chính", + + "privileges.description": "Bạn có thể cấu hình kiểm soát truy cập các phần của trang web ở phần này. Cấp quyền dựa trên cơ sở mỗi người dùng hoặc mỗi nhóm. Chọn tên miền ảnh hưởng từ menu thả xuống bên dưới.", + "privileges.category-selector": "Cấu hình đặc quyền cho", + "privileges.warning": "Ghi chú: Cài đặt đặc quyền có hiệu lực lập tức. Không cần phải lưu danh mục sau khi điều chỉnh cài đặt này.", + "privileges.section-viewing": "Đặc Quyền Xem", + "privileges.section-posting": "Đặc Quyền Bài Đăng", + "privileges.section-moderation": "Đặc Quyền Kiểm Duyệt", + "privileges.section-other": "Khác", + "privileges.section-user": "Người Dùng", + "privileges.search-user": "Thêm Người Dùng", + "privileges.no-users": "Không có đặc quyền riêng cho người dùng trong chuyên mục này.", + "privileges.section-group": "Nhóm", + "privileges.group-private": "Nhóm này là riêng tư", + "privileges.inheritance-exception": "Nhóm này không kế thừa các đặc quyền từ nhóm registered-users", + "privileges.banned-user-inheritance": "Người dùng bị cấm kế thừa các đặc quyền từ nhóm người dùng bị cấm", + "privileges.search-group": "Thêm Nhóm", + "privileges.copy-to-children": "Sao Chép Đến mục Con", + "privileges.copy-from-category": "Sao Chép Từ Chuyên Mục", + "privileges.copy-privileges-to-all-categories": "Sao Chép Vào Tất Cả Chuyên Mục", + "privileges.copy-group-privileges-to-children": "Sao chép các quyền của nhóm này cho chuyên mục con của chuyên mục này.", + "privileges.copy-group-privileges-to-all-categories": "Sao chép các quyền của nhóm này vào tất cả chuyên mục.", + "privileges.copy-group-privileges-from": "Sao chép các quyền của nhóm này từ một chuyên mục khác.", + "privileges.inherit": "Nếu nhóm registered-users được cấp đặc quyền cụ thể, tất cả các nhóm khác nhận được đặc quyền ngầm , ngay cả khi không xác định/kiểm tra. Đặc quyền ngầm này được hiển thị cho bạn bởi vì tất cả người dùng là một phần của nhóm registered-users, vì thế, các nhóm bổ sung không cần cấp quyền rõ ràng.", + "privileges.copy-success": "Đã sao chép các đặc quyền!", + + "analytics.back": "Trờ Về Danh Sách Chuyên Mục", + "analytics.title": "Phân tích chuyên mục \"%1\"", + "analytics.pageviews-hourly": "Hình 1 – Số lượt xem trang hàng giờ cho chuyên mục này", + "analytics.pageviews-daily": "Hình 2 – Số lượt xem trang hàng ngày cho chuyên mục này", + "analytics.topics-daily": "Hình 3 – Chủ đề được tạo hằng ngày trong chuyên mục này", + "analytics.posts-daily": "Hình 4 – Bài viết hàng ngày được thực hiện trong chuyên mục này", + + "alert.created": "Đã tạo", + "alert.create-success": "Đã tạo chuyên mục thành công!", + "alert.none-active": "Bạn không có chuyên mục hoạt động.", + "alert.create": "Tạo Chuyên Mục", + "alert.confirm-purge": "

Bạn có thực sự muốn xóa danh mục \"%1\" này không?

Cảnh báo! Tất cả chủ đề và bài đăng trong danh mục này sẽ bị xóa!

Xóa danh mục sẽ xóa tất cả các chủ đề và bài đăng, đồng thời xóa danh mục khỏi cơ sở dữ liệu. Nếu bạn muốn xóa một danh mụctạm thời, thay vào đó bạn sẽ muốn \"vô hiệu hóa\" danh mục.

", + "alert.purge-success": "Đã loại bỏ chuyên mục!", + "alert.copy-success": "Đã Sao Chép Cài Đặt!", + "alert.set-parent-category": "Đặt Chuyên Mục Chính", + "alert.updated": "Đã Cập Nhật Chuyên Mục", + "alert.updated-success": "IDs Chuyên mục %1 đã cập nhật thành công.", + "alert.upload-image": "Tải lên ảnh chuyên mục", + "alert.find-user": "Tìm Một Người Dùng", + "alert.user-search": "Tìm một người dùng ở đây...", + "alert.find-group": "Tìm Nhóm", + "alert.group-search": "Tìm một nhóm ở đây...", + "alert.not-enough-whitelisted-tags": "Các thẻ trong danh sách trắng ít hơn tối thiểu, bạn cần tạo thêm các thẻ trong danh sách trắng!", + "collapse-all": "Thu Gọn Tất Cả", + "expand-all": "Mở Rộng Tất Cả", + "disable-on-create": "Tắt lúc tạo", + "no-matches": "Không trùng khớp" +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/digest.json b/public/language/vi/admin/manage/digest.json new file mode 100644 index 0000000000..80010b7fb1 --- /dev/null +++ b/public/language/vi/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "Một danh sách các số liệu thống kê và thời gian phân phối được hiển thị dưới đây.", + "disclaimer": "Xin lưu ý rằng việc gửi email không được đảm bảo, do bản chất của công nghệ email. Nhiều yếu tố quyết định đến việc liệu một email được gửi đến máy chủ người nhận cuối cùng có được gửi đến hộp thư đến của người dùng hay không, bao gồm danh tiếng của máy chủ, địa chỉ IP nằm trong danh sách đen và liệu DKIM/SPF/DMARC được cấu hình.", + "disclaimer-continued": "Gửi thành công nghĩa là tin nhắn được NodeBB gửi thành công và máy chủ người nhận nhận được. Nó không có nghĩa là email đã đến hộp thư đến. Để có kết quả tốt nhất, chúng tôi khuyên bạn nên sử dụng dịch vụ gửi email của bên thứ ba, chẳng hạn như SendGrid.", + + "user": "Người dùng", + "subscription": "Loại đăng ký", + "last-delivery": "Gửi thành công lần cuối", + "default": "Mặc định hệ thống", + "default-help": "Mặc định hệ thống nghĩa là người dùng không đè lên toàn bộ cài đặt thông báo diễn đàn, hiện là: "%1"", + "resend": "Gửi Lại Thông Báo", + "resend-all-confirm": "Bạn có muốn thực hiện thủ công lần chạy thông báo này không?", + "resent-single": "Đã hoàn tất gửi lại thông báo thủ công", + "resent-day": "Đã gửi lại thông báo hàng ngày", + "resent-week": "Đã gửi lại thông báo hàng tuần", + "resent-biweek": "Gửi lại thông báo hai tuần một lần", + "resent-month": "Đã gửi lại thông báo hàng tháng", + "null": "Không", + "manual-run": "Chạy thông báo thủ công:", + + "no-delivery-data": "Không tìm thấy dữ liệu để gửi" +} diff --git a/public/language/vi/admin/manage/groups.json b/public/language/vi/admin/manage/groups.json new file mode 100644 index 0000000000..e781d0612b --- /dev/null +++ b/public/language/vi/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "Tên Nhóm", + "badge": "Huy hiệu", + "properties": "Thuộc tính", + "description": "Mô Tả Nhóm", + "member-count": "Số Thành Viên", + "system": "Hệ Thống", + "hidden": "Đã Ẩn", + "private": "Riêng tư", + "edit": "Sửa", + "delete": "Xóa", + "privileges": "Đặc Quyền", + "download-csv": "CSV", + "search-placeholder": "Tìm", + "create": "Tạo Nhóm", + "description-placeholder": "Mô tả ngắn gọn về nhóm của bạn", + "create-button": "Tạo", + + "alerts.create-failure": "Uh-Oh

Đã xảy ra sự cố khi tạo nhóm của bạn. Vui lòng thử lại sau!

", + "alerts.confirm-delete": "Bạn có chắc chắn muốn xóa nhóm này?", + + "edit.name": "Tên", + "edit.description": "Mô tả", + "edit.user-title": "Chức Danh Thành Viên", + "edit.icon": "Biểu Tượng Nhóm", + "edit.label-color": "Màu Nhãn Nhóm", + "edit.text-color": "Màu Chữ Nhóm", + "edit.show-badge": "Hiển Thị Huy Hiệu", + "edit.private-details": "Nếu bật, tham gia nhóm cần được chủ nhóm chấp nhận.", + "edit.private-override": "Cảnh báo: Nhóm riêng tư bị tắt ở cấp độ hệ thống, tùy chọn này sẽ thay thế.", + "edit.disable-join": "Tắt yêu cầu tham gia", + "edit.disable-leave": "Không cho phép người dùng rời khỏi nhóm", + "edit.hidden": "Đã Ẩn", + "edit.hidden-details": "Nếu bật, nhóm này hiển thị trong danh sách nhóm và người dùng phải được mời thủ công", + "edit.add-user": "Thêm Người Dùng Vào Nhóm", + "edit.add-user-search": "Tìm Kiếm Người Dùng", + "edit.members": "Danh Sách Thành Viên", + "control-panel": "Bảng Điều Khiển Nhóm", + "revert": "Hoàn Tác", + + "edit.no-users-found": "Không Tìm Thấy Người Dùng", + "edit.confirm-remove-user": "Bạn có chắc chắn muốn xóa người dùng này?", + "edit.save-success": "Đã lưu thay đổi!" +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/privileges.json b/public/language/vi/admin/manage/privileges.json new file mode 100644 index 0000000000..9aea7f6569 --- /dev/null +++ b/public/language/vi/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "Chung", + "admin": "Quản Trị Viên", + "group-privileges": "Đặc Quyền Nhóm", + "user-privileges": "Đặc Quyền Người Dùng", + "edit-privileges": "Sửa Đặc Quyền", + "select-clear-all": "Chọn/Xóa tất cả", + "chat": "Trò chuyện", + "upload-images": "Tải Lên Ảnh", + "upload-files": "Tải Lên Tệp", + "signature": "Chữ ký", + "ban": "Cấm", + "mute": "Im lặng", + "invite": "Mời", + "search-content": "Tìm Kiếm Nội Dung", + "search-users": "Tìm Kiếm Người Dùng", + "search-tags": "Tìm Thẻ", + "view-users": "Xem Người Dùng", + "view-tags": "Xem Thẻ", + "view-groups": "Xem Nhóm", + "allow-local-login": "Đăng Nhập Cục Bộ", + "allow-group-creation": "Tạo Nhóm", + "view-users-info": "Xem Thông Tin Người Dùng", + "find-category": "Tìm Chuyên Mục", + "access-category": "Truy Cập Chuyên Mục", + "access-topics": "Truy Cập Chủ Đề", + "create-topics": "Tạo Chủ Đề", + "reply-to-topics": "Trả Lời Chủ Đề", + "schedule-topics": "Lên Lịch Chủ Đề", + "tag-topics": "Gắn Thẻ Chủ Đề", + "edit-posts": "Chỉnh Sửa Bài Đăng", + "view-edit-history": "Xem Lịch Sử Chỉnh Sửa", + "delete-posts": "Xóa Bài Đăng", + "view_deleted": "Xem Bài Viết Đã Xóa", + "upvote-posts": "Ủng Hộ Bài Đăng", + "downvote-posts": "Phản Đối Bài Đăng", + "delete-topics": "Xóa Chủ Đề", + "purge": "Loại Bỏ", + "moderate": "Điều hành", + "admin-dashboard": "Bảng Điều Khiển", + "admin-categories": "Chuyên mục", + "admin-privileges": "Đặc Quyền", + "admin-users": "Người dùng", + "admin-admins-mods": "Quản Trị Viên & Người Điều Hành", + "admin-groups": "Nhóm", + "admin-tags": "Thẻ", + "admin-settings": "Cài Đặt", + + "alert.confirm-moderate": "Bạn có chắc muốn cấp quyền xét duyệt cho nhóm người dùng này không? Nhóm này công khai và ai cũng có thể tham gia.", + "alert.confirm-admins-mods": "Bạn có chắc muốn cấp quyền "Quản Trị Viên& Người Điều Hành" cho người dùng/nhóm này? Người dùng này có quyền thăng và hạ cấp người dùng khác ở các vị trí đặc quyền, Bao gồm quản trị viên cấp cao", + "alert.confirm-save": "Vui lòng xác nhận ý định của bạn để lưu các đặc quyền này", + "alert.saved": "Đã lưu và áp dụng các thay đổi đặc quyền ", + "alert.confirm-discard": "Bạn có chắc chắn muốn hủy các thay đổi đặc quyền của mình không?", + "alert.discarded": "Thay đổi đặc quyền bị hủy", + "alert.confirm-copyToAll": "Bạn có chắc muốn áp dụng cài đặt %1 cho tất cả danh mục?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Bạn có chắc muốn áp dụng cài đặt %1 của nhóm này cho tất cả danh mục con?", + "alert.no-undo": "Hành động này không thể hoàn tác.", + "alert.admin-warning": "Quản trị viên ngầm có tất cả các đặc quyền", + "alert.copyPrivilegesFrom-title": "Chọn một danh mục để sao chép từ", + "alert.copyPrivilegesFrom-warning": "Điều này sẽ sao chép %1 từ danh mục đã chọn.", + "alert.copyPrivilegesFromGroup-warning": "Thao tác này sẽ sao chép cài đặt %1 của nhóm này từ danh mục đã chọn." +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/registration.json b/public/language/vi/admin/manage/registration.json new file mode 100644 index 0000000000..c923fd69bd --- /dev/null +++ b/public/language/vi/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Hàng đợi", + "description": "Không có ai xếp hàng đợi đăng ký.
Để bật tính năng này, truy cập Cài đặt → Người dùng → Người dùng đăng ký và đặt Kiểu Đăng Ký là \"Phê Duyệt Của Quản Trị Viên\".", + + "list.name": "Tên", + "list.email": "Thư điện tử", + "list.ip": "IP", + "list.time": "Thời gian", + "list.username-spam": "Tần suất: %1 Xuất hiện: %2 Độ tin cậy: %3", + "list.email-spam": "Tần suất: %1 Xuất hiện: %2", + "list.ip-spam": "Tần suất: %1 Xuất hiện: %2", + + "invitations": "Lời mời", + "invitations.description": "Dưới đây là danh sách hoàn tất các lời mời đã gửi. Bấm ctrl-f để tìm kiếm trong danh sách bằng email hoặc tên đăng nhập.

Tên đăng nhập sẽ được hiển thị bên phải email cho những người dùng đã đổi lời mời của họ.", + "invitations.inviter-username": "Tên Đăng Nhập Người Mời", + "invitations.invitee-email": "Email của người được mời", + "invitations.invitee-username": "Tên Đăng Nhập Người Được Mời (nếu đã đăng ký)", + + "invitations.confirm-delete": "Bạn có chắc chắn muốn xóa lời mời này không?" +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/tags.json b/public/language/vi/admin/manage/tags.json new file mode 100644 index 0000000000..46eefcfe87 --- /dev/null +++ b/public/language/vi/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Diễn đàn của bạn chưa có bất kỳ chủ đề nào gắn thẻ.", + "bg-color": "Màu Nền", + "text-color": "Màu Chữ", + "description": "Chọn các thẻ bằng cách nhấp hoặc kéo, bấm CTRL để chọn nhiều thẻ.", + "create": "Tạo Thẻ", + "modify": "Sửa Đổi Thẻ", + "rename": "Đổi Tên Thẻ", + "delete": "Xóa Các Thẻ Đã Chọn", + "search": "Tìm kiếm thẻ...", + "settings": "Cài Đặt Thẻ", + "name": "Tên Thẻ", + + "alerts.editing": "Sửa Thẻ", + "alerts.confirm-delete": "Bạn có muốn xóa các thẻ đã chọn không?", + "alerts.update-success": "Đã Cập Nhật Thẻ!", + "reset-colors": "Đặt lại màu" +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/uploads.json b/public/language/vi/admin/manage/uploads.json new file mode 100644 index 0000000000..232dff8993 --- /dev/null +++ b/public/language/vi/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "Tải Lên Tệp", + "filename": "Tên Tệp", + "usage": "Đăng sử dụng", + "orphaned": "Đơn độc", + "size/filecount": "Kích cỡ/ Số lượng tệp", + "confirm-delete": "Bạn có chắc muốn xóa tệp này không?", + "filecount": "%1 tệp", + "new-folder": "Thư mục mới", + "name-new-folder": "Nhập tên cho thư mục mới" +} \ No newline at end of file diff --git a/public/language/vi/admin/manage/users.json b/public/language/vi/admin/manage/users.json new file mode 100644 index 0000000000..bffe16566a --- /dev/null +++ b/public/language/vi/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "Người Dùng", + "edit": "Hành động", + "make-admin": "Làm Quản Trị Viên", + "remove-admin": "Xóa Quản Trị Viên", + "validate-email": "Xác Thực Email", + "send-validation-email": "Gửi Email Xác Thực", + "password-reset-email": "Gửi Email Đặt Lại Mật Khẩu", + "force-password-reset": "Buộc đặt lại mật khẩu và đăng xuất người dùng", + "ban": "Cấm Người Dùng", + "temp-ban": "Cấm Người Dùng Tạm Thời", + "unban": "Bỏ Cấm Người Dùng", + "reset-lockout": "Đặt lại khóa", + "reset-flags": "Đặt Lại Gắn Cờ", + "delete": "Xóa Người Dùng", + "delete-content": "Xóa Nội Dung Người Dùng", + "purge": "Xóa Người DùngNội Dung", + "download-csv": "Tải về CSV", + "manage-groups": "Quản Lý Nhóm", + "add-group": "Thêm Nhóm", + "create": "Tạo Người Dùng", + "invite": "Mời qua Email", + "new": "Người Dùng Mới", + "filter-by": "Lọc bởi", + "pills.unvalidated": "Không Hợp Lệ", + "pills.validated": "Đã xác thực", + "pills.banned": "Bị cấm", + + "50-per-page": "50 mỗi trang", + "100-per-page": "100 mỗi trang", + "250-per-page": "250 mỗi trang", + "500-per-page": "500 mỗi trang", + + "search.uid": "Bởi ID Người Dùng", + "search.uid-placeholder": "Nhập ID người dùng để tìm", + "search.username": "Theo Tên Người Dùng", + "search.username-placeholder": "Nhập một tên đăng nhập để tìm", + "search.email": "Bằng Email", + "search.email-placeholder": "Nhập email để tìm kiếm", + "search.ip": "Bởi Địa Chỉ IP", + "search.ip-placeholder": "Nhập Địa Chỉ IP để tìm kiếm", + "search.not-found": "Không tìm thấy người dùng", + + "inactive.3-months": "3 tháng", + "inactive.6-months": "6 tháng", + "inactive.12-months": "12 tháng", + + "users.uid": "uid", + "users.username": "tên đăng nhập", + "users.email": "thư điện tử", + "users.no-email": "(không có email)", + "users.ip": "IP", + "users.postcount": "số lượng bài viết", + "users.reputation": "uy tín", + "users.flags": "gắn cờ", + "users.joined": "đã tham gia", + "users.last-online": "trực tuyến lần cuối", + "users.banned": "bị cấm", + + "create.username": "Tên Người Dùng", + "create.email": "Thư điện tử", + "create.email-placeholder": "Email người dùng này", + "create.password": "Mật khẩu", + "create.password-confirm": "Xác Nhận Mật Khẩu", + + "temp-ban.length": "Length", + "temp-ban.reason": "Lý do (Không bắt buộc)", + "temp-ban.hours": "Giờ", + "temp-ban.days": "Ngày", + "temp-ban.explanation": "Nhập khoảng thời gian cho lệnh cấm. Lưu ý rằng thời gian bằng 0 sẽ là một lệnh cấm vĩnh viễn.", + + "alerts.confirm-ban": "Bạn có chắc muốn cấm người dùng này mãi mãi?", + "alerts.confirm-ban-multi": "Bạn có chắc muốn cấm những người dùng này mãi mãi?", + "alerts.ban-success": "Đã cấm người dùng!", + "alerts.button-ban-x": "Cấm %1 người dùng", + "alerts.unban-success": "Đã bỏ cấm người dùng!", + "alerts.lockout-reset-success": "Đặt lại khóa!", + "alerts.flag-reset-success": "Đặt lại gắn cờ!", + "alerts.no-remove-yourself-admin": "Bạn không thể tự xóa mình với tư cách Quản trị viên!", + "alerts.make-admin-success": "Người dùng hiện là quản trị viên.", + "alerts.confirm-remove-admin": "Bạn có chắc muốn xóa quản trị viên này không?", + "alerts.remove-admin-success": "Người dùng không còn là quản trị viên.", + "alerts.make-global-mod-success": "Người dùng hiện là người điều hành toàn quyền.", + "alerts.confirm-remove-global-mod": "Bạn có thực sự muốn xóa người điều hành toàn quyền này không?", + "alerts.remove-global-mod-success": "Người dùng không còn là người điều hành toàn quyền.", + "alerts.make-moderator-success": "Người dùng hiện là người điều hành.", + "alerts.confirm-remove-moderator": "Bạn có thực sự muốn xóa người kiểm duyệt này không?", + "alerts.remove-moderator-success": "Người dùng không còn là người điều hành.", + "alerts.confirm-validate-email": "Bạn có muốn xác thực email của người dùng này không?", + "alerts.confirm-force-password-reset": "Bạn có chắc muốn đặt lại mật khẩu và đăng xuất người dùng này không?", + "alerts.validate-email-success": "Đã Xác Thực Email", + "alerts.validate-force-password-reset-success": "Đã đặt lại mật khẩu người dùng và phiên hiện có của họ đã bị thu hồi.", + "alerts.password-reset-confirm": "Bạn có muốn gửi email đặt lại mật khẩu cho người dùng này?", + "alerts.password-reset-email-sent": "Đã gửi email đặt lại mật khẩu.", + "alerts.confirm-delete": "Cảnh báo!

Bạn có thực sự muốn xóa người dùng?

Hành động này là không thể đảo ngược! Chỉ tài khoản người dùng sẽ bị xóa, các bài đăng và chủ đề của họ sẽ vẫn còn.

", + "alerts.delete-success": "Đã Xóa Người Dùng!", + "alerts.confirm-delete-content": "Cảnh báo!

Bạn có thực sự muốn xóa nội dung của người dùng này?

Hành động này là không thể hoàn tác! Tài khoản của người dùng sẽ vẫn còn, nhưng các bài đăng và chủ đề của họ sẽ bị xóa.

", + "alerts.delete-content-success": "Đã Xóa Nội Dung Người Dùng!", + "alerts.confirm-purge": "Cảnh báo!

Bạn có chắc chắn xóa người dùng và nội dung của họ?

Hành động này không thể khôi phục! Tất cả dữ liệu người dùng và nội dung sẽ bị xóa!

", + "alerts.create": "Tạo Người Dùng", + "alerts.button-create": "Tạo", + "alerts.button-cancel": "Hủy", + "alerts.error-passwords-different": "Mật khẩu phải khớp!", + "alerts.error-x": "Lỗi

%1

", + "alerts.create-success": "Đã tạo người dùng!", + + "alerts.prompt-email": "Thư điện tử:", + "alerts.email-sent-to": "Email mời đã được gửi đến %1", + "alerts.x-users-found": "Tìm được %1 người dùng (%2 giây)", + "export-users-started": "Xuất người dùng dưới dạng csv, quá trình này có thể mất một lúc. Bạn sẽ nhận được thông báo khi hoàn tất.", + "export-users-completed": "Đã xuất người dùng ra csv, bấm vào đây tải xuống." +} \ No newline at end of file diff --git a/public/language/vi/admin/menu.json b/public/language/vi/admin/menu.json new file mode 100644 index 0000000000..d6d4e72a6d --- /dev/null +++ b/public/language/vi/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Bảng điều khiển", + "dashboard/overview": "Tổng quát", + "dashboard/logins": "Đăng nhập", + "dashboard/users": "Người dùng", + "dashboard/topics": "Chủ đề", + "dashboard/searches": "Tìm kiếm", + "section-general": "Chung", + + "section-manage": "Quản lý", + "manage/categories": "Chuyên mục", + "manage/privileges": "Đặc quyền", + "manage/tags": "Thẻ", + "manage/users": "Người dùng", + "manage/admins-mods": "Quản trị viên & Người điều hành", + "manage/registration": "Hàng Đợi Đăng Ký", + "manage/post-queue": "Hàng Đợi Bài Viết", + "manage/groups": "Nhóm", + "manage/ip-blacklist": "Danh sách đen IP", + "manage/uploads": "Tải lên", + "manage/digest": "Thông báo", + + "section-settings": "Cài đặt", + "settings/general": "Chung", + "settings/homepage": "Trang Chủ", + "settings/navigation": "Điều Hướng", + "settings/reputation": "Uy Tín & Gắn Cờ", + "settings/email": "Thư điện tử", + "settings/user": "Người dùng", + "settings/group": "Nhóm", + "settings/guest": "Khách", + "settings/uploads": "Tải lên", + "settings/languages": "Ngôn ngữ", + "settings/post": "Bài viết", + "settings/chat": "Trò chuyện", + "settings/pagination": "Phân trang", + "settings/tags": "Thẻ", + "settings/notifications": "Thông báo", + "settings/api": "Truy cập API", + "settings/sounds": "Âm thanh", + "settings/social": "Xã hội", + "settings/cookies": "Cookies", + "settings/web-crawler": "Thu thập dữ liệu trên web", + "settings/sockets": "Sockets", + "settings/advanced": "Nâng cao", + + "settings.page-title": "Cài đặt %1", + + "section-appearance": "Trực quan", + "appearance/themes": "Giao diện", + "appearance/skins": "Kiểu dáng", + "appearance/customise": "Nội dung tùy chỉnh (HTML/JS/CSS)", + + "section-extend": "Mở rộng", + "extend/plugins": "Plugins", + "extend/widgets": "Tiện ích", + "extend/rewards": "Phần thưởng", + + "section-social-auth": "Xác thực xã hội", + + "section-plugins": "Plugins", + "extend/plugins.install": "Cài đặt plugin", + + "section-advanced": "Nâng cao", + "advanced/database": "Cơ sở dữ liệu", + "advanced/events": "Sự kiện", + "advanced/hooks": "Móc nối", + "advanced/logs": "Nhật ký", + "advanced/errors": "Lỗi", + "advanced/cache": "Bộ nhớ đệm", + "development/logger": "Ghi nhật ký", + "development/info": "Thông tin", + + "rebuild-and-restart-forum": "Xây dựng lại & Khởi động lại diễn đàn", + "restart-forum": "Khởi Động Lại Diễn Đàn", + "logout": "Đăng xuất", + "view-forum": "Xem diễn đàn", + + "search.placeholder": "Cài đặt tìm kiếm", + "search.no-results": "Ko có kết quả...", + "search.search-forum": "Tìm kiếm diễn đàn cho ", + "search.keep-typing": "Nhập thêm để xem kết quả...", + "search.start-typing": "Bắt đầu nhập để xem kết quả...", + + "connection-lost": "Kết nối với %1 đã bị mất, cố gắng kết nối lại...", + + "alerts.version": "Đang chạy NodeBB v%1", + "alerts.upgrade": "Nâng cấp lên v%1" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/advanced.json b/public/language/vi/admin/settings/advanced.json new file mode 100644 index 0000000000..4f4b14039f --- /dev/null +++ b/public/language/vi/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "Chế Độ Bảo Trì", + "maintenance-mode.help": "Khi diễn đàn ở chế độ bảo trì, tất cả các yêu cầu sẽ được chuyển hướng đến một trang giữ tĩnh. Quản trị viên không bị chuyển hướng này và có thể truy cập trang bình thường.", + "maintenance-mode.status": "Mã Trạng Thái Chế Độ Bảo Trì", + "maintenance-mode.message": "Thông Báo Bảo Trì", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Chọn các nhóm nên được miễn chế độ bảo trì", + "headers": "Headers", + "headers.allow-from": "Đặt ALLOW-FROM để đặt NodeBB trong iFrame", + "headers.csp-frame-ancestors": "Đặt giá trị Content-Security-Policy frame-ancestors ở phần đầu trang để Đặt NodeBB trong iFrame", + "headers.csp-frame-ancestors-help": "'không', 'bản thân' (mặc định) hoặc danh sách URI cho phép.", + "headers.powered-by": "Tùy chỉnh tiêu đề \"Powered By\" được gửi bởi NodeBB", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Biểu Thức Chính Quy Access-Control-Allow-Origin", + "headers.acao-help": "Để từ chối truy cập tất cả các trang, để trống", + "headers.acao-regex-help": "Nhập các biểu thức thông thường ở đây để phù hợp với nguồn gốc động. Để từ chối truy cập vào tất cả các trang web, để trống", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "Khi được bật (mặc định), sẽ đặt tiêu đề thành require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "Bảo Vệ Truyền Tải Nghiêm Ngặt", + "hsts.enabled": "Đã bật HSTS (đề nghị)", + "hsts.maxAge": "HSTS Tuổi Tối Đa", + "hsts.subdomains": "Bao gồm tên miền phụ trong phần đầu HSTS", + "hsts.preload": "Cho phép tải trước phần đầu HSTS", + "hsts.help": "Nếu bật, một phần đầu trang HSTS sẽ được đặt cho trang web này. Bạn có thể chọn bao gồm tên miền phụ và cờ tải trước trong phần đầu. Nếu nghi ngờ, bạn có thể bỏ chọn. Thêm thông tin ", + "traffic-management": "Quản lý lưu lượng", + "traffic.help": "NodeBB dùng mô-đun tự động từ chối yêu cầu trong các tình huống có lưu lượng truy cập cao. Bạn có thể điều chỉnh các cài đặt này ở đây, mặc dù các cài đặt mặc định là một điểm khởi đầu tốt.", + "traffic.enable": "Bật quản lý lưu lượng", + "traffic.event-lag": "Ngưỡng Trễ Vòng Lặp Sự Kiện (mili giây)", + "traffic.event-lag-help": "Giảm giá trị này sẽ giảm thời gian chờ tải trang, nhưng cũng sẽ hiển thị thông báo \"tải quá mức\" cho nhiều người dùng hơn. (Yêu cầu khởi động lại)", + "traffic.lag-check-interval": "Khoảng thời gian kiểm tra (mili giây)", + "traffic.lag-check-interval-help": "Việc hạ thấp giá trị này khiến NodeBB trở nên nhạy cảm hơn với tải đột biến, nhưng cũng có thể khiến kiểm tra trở nên quá nhạy. (Yêu cầu khởi động lại)", + + "sockets.settings": "Cài Đặt WebSocket", + "sockets.max-attempts": "Nỗ Lực Kết Nối Lại Tối Đa", + "sockets.default-placeholder": "Mặc định: %1", + "sockets.delay": "Độ Trễ Kết Nối Lại", + + "analytics.settings": "Cài Đặt Phân Tích", + "analytics.max-cache": "Giá trị tối đa trong bộ nhớ đệm cho phân tích", + "analytics.max-cache-help": "Cài đặt khi có lưu lượng truy cập cao, bộ nhớ đệm có thể bị cạn liên tục nếu có nhiều người dùng hoạt động cùng lúc hơn giá trị Bộ Nhớ Đệm tối đa. (Yêu cầu khởi động lại)", + "compression.settings": "Cài Đặt Nén", + "compression.enable": "Bật Nén", + "compression.help": "Cài đặt này cho phép nén gzip. Đối với trang web có lượng truy cập cao khi vận hành, tốt nhất để đặt tính năng nén là triển khai nó ở cấp độ proxy ngược. Bạn có thể kích hoạt nó ở đây cho việc thử nghiệm." +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/api.json b/public/language/vi/admin/settings/api.json new file mode 100644 index 0000000000..ad59dadf66 --- /dev/null +++ b/public/language/vi/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Mã truy cập", + "settings": "Cài đặt", + "lead-text": "Từ trang này, bạn có thể cấu hình quyền truy cập vào API Viết trong NodeBB.", + "intro": "Mặc định, API Viết xác thực người dùng dựa trên cookie phiên của họ, nhưng NodeBB cũng hỗ trợ xác thực Bearer thông qua mã truy cập được tạo qua trang này.", + "docs": "Nhấp vào đây để truy cập thông số kỹ thuật API đầy đủ", + + "require-https": "Chỉ yêu cầu sử dụng API qua HTTPS", + "require-https-caveat": "Ghi chú: Một số cài đặt liên quan đến bộ cân bằng tải có thể ủy quyền các yêu cầu của họ tới NodeBB bằng HTTP, trong trường hợp đó tùy chọn này vẫn bị vô hiệu hóa.", + + "uid": "ID Người Dùng", + "uid-help-text": "Ghi rõ ID người dùng liên kết với mã truy cập. Nếu ID người dùng là 0, nó sẽ là môt mã truy cập cao cấp, có thể giả định danh tính của những người dùng khác dựa trên tham số _uid", + "description": "Mô tả", + "no-description": "Không có mô tả cụ thể.", + "token-on-save": "Mã truy cập sẽ được tạo sau khi biểu mẫu được lưu" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/chat.json b/public/language/vi/admin/settings/chat.json new file mode 100644 index 0000000000..12397eecf9 --- /dev/null +++ b/public/language/vi/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "Cài Đặt Trò Chuyện", + "disable": "Tắt trò chuyện", + "disable-editing": "Tắt chỉnh sửa / xóa tin nhắn trò chuyện", + "disable-editing-help": "Quản trị viên và người kiểm duyệt toàn quyền được miễn hạn chế này", + "max-length": "Độ dài tối đa của tin nhắn trò chuyện", + "max-room-size": "Số lượng người dùng tối đa trong phòng trò chuyện", + "delay": "Thời gian giữa các tin nhắn trò chuyện tính bằng mili giây", + "notification-delay": "Độ trễ thông báo cho tin nhắn trò chuyện. (0 để không bị chậm trễ)", + "restrictions.seconds-edit-after": "Số giây một tin nhắn trò chuyện sẽ vẫn có thể chỉnh sửa. (0 bị vô hiệu hóa)", + "restrictions.seconds-delete-after": "Số giây một tin nhắn trò chuyện vẫn có thể xóa được. (0 bị vô hiệu hóa)" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/cookies.json b/public/language/vi/admin/settings/cookies.json new file mode 100644 index 0000000000..0253049420 --- /dev/null +++ b/public/language/vi/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "Tuân thủ EU", + "consent.enabled": "Đã bật", + "consent.message": "Tin nhắn thông báo", + "consent.acceptance": "Tin nhắn chấp nhận", + "consent.link-text": "Văn Bản Liên Kết Điều Khoản", + "consent.link-url": "Liên Kết URL Điều Khoản", + "consent.blank-localised-default": "Để trống để sử dụng mặc định bản địa hóa NodeBB", + "settings": "Cài đặt", + "cookie-domain": "Tên miền phiên cookie", + "max-user-sessions": "Số phiên hoạt động tối đa cho mỗi người dùng", + "blank-default": "Để trống mặc định" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/email.json b/public/language/vi/admin/settings/email.json new file mode 100644 index 0000000000..cdd5a01772 --- /dev/null +++ b/public/language/vi/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "Cài Đặt Email", + "address": "Địa Chỉ Email", + "address-help": "Địa chỉ email sau đề cập đến email mà người nhận sẽ thấy trong trường \"Từ\" và \"Trả lời đến\".", + "from": "Tên Người Gửi", + "from-help": "Tên người gửi hiển thị trong email.", + + "confirmation-settings": "Xác nhận", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "Truyền Tải SMTP", + "smtp-transport.enabled": "Bật truyền tải SMTP", + "smtp-transport-help": "Bạn có thể chọn từ danh sách các dịch vụ nổi bật hoặc nhập một dịch vụ tùy chỉnh.", + "smtp-transport.service": "Chọn một dịch vụ", + "smtp-transport.service-custom": "Tùy chỉnh dịch vụ ", + "smtp-transport.service-help": "Chọn tên dịch vụ ở trên để sử dụng thông tin đã biết về nó. Ngoài ra, hãy chọn "Dịch vụ tùy chỉnh" và nhập các chi tiết bên dưới.", + "smtp-transport.gmail-warning1": "Nếu bạn đang sử dụng GMail làm nhà cung cấp email của mình, bạn sẽ phải tạo "Mật Khẩu Ứng Dụng" để NodeBB xác thực thành công. Bạn có thể tạo một cái tại trang Mật Khẩu Ứng Dụng .", + "smtp-transport.gmail-warning2": "Để biết thêm thông tin về giải pháp này, hãy tham khảo bài viết NodeMailer về vấn đề này. Giải pháp thay thế là sử dụng plugin trình gửi email của bên thứ ba như SendGrid, Mailgun, v.v.. Duyệt qua các plugin có sẵn tại đây.", + "smtp-transport.auto-enable-toast": "Có vẻ như bạn're đang cấu hình truyền tải SMTP. Chúng tôi đã bật tùy chọn \"Truyền tải SMTP\" cho bạn.", + "smtp-transport.host": "Máy Chủ SMTP", + "smtp-transport.port": "Cổng SMTP", + "smtp-transport.security": "Bảo mật kết nối", + "smtp-transport.security-encrypted": "Được mã hóa", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "Trống", + "smtp-transport.username": "Tên đăng nhập", + "smtp-transport.username-help": "Đối với dịch vụ Gmail, nhập địa chỉ email đầy đủ tại đây, đặc biệt nếu bạn đang sử dụng tên miền được Google Apps quản lý.", + "smtp-transport.password": "Mật Khẩu", + "smtp-transport.pool": "Bật kết nối tổng hợp", + "smtp-transport.pool-help": "Việc gộp các kết nối ngăn NodeBB tạo kết nối mới cho mọi email. Tùy chọn này chỉ áp dụng nếu Truyền tải SMTP được bật.", + + "template": "Chỉnh Sửa Mẫu Email", + "template.select": "Chọn Mẫu Email", + "template.revert": "Hoàn Nguyên về Bản Gốc", + "testing": "Email Kiểm Tra", + "testing.select": "Chọn Mẫu Email", + "testing.send": "Gửi Email Kiểm Tra", + "testing.send-help": "Email kiểm tra sẽ được gửi đến địa chỉ email của người dùng hiện đang đăng nhập.", + "subscriptions": "Thông Báo Email", + "subscriptions.disable": "Tắt thông báo email", + "subscriptions.hour": "Giờ Thông Báo", + "subscriptions.hour-help": "Vui lòng nhập một số đại diện cho giờ để gửi thông báo email đã lên lịch (VD: 0 cho nửa đêm, 17 cho 5h chiều). Hãy nhớ rằng đây là giờ theo chính máy chủ và có thể không khớp chính xác với đồng hồ hệ thống của bạn.
Thời gian máy chủ gần đúng là:
Thông báo hàng ngày kế tiếp được lên lịch để gửi ", + "notifications.remove-images": "Xóa hình ảnh khỏi thông báo email", + "require-email-address": "Bắt buộc người dùng mới phải điền địa chỉ email", + "require-email-address-warning": "Theo mặc định, người dùng có thể từ chối nhập địa chỉ email bằng cách để trống trường. Bật tùy chọn này có nghĩa là họ phải nhập địa chỉ email để tiến hành đăng ký. Nó không đảm bảo người dùng sẽ nhập địa chỉ email thực, thậm chí cả địa chỉ mà họ sở hữu.", + "send-validation-email": "Gửi email xác thực khi một email được thêm vào hoặc thay đổi", + "include-unverified-emails": "Gửi email đến những người nhận chưa xác nhận rõ ràng email của họ", + "include-unverified-warning": "Theo mặc định, người dùng có email được liên kết với tài khoản của họ đã được xác minh, nhưng có những trường hợp không phải như vậy (ví dụ: đăng nhập SSO, người dùng phổ thông, v.v.). Bạn tự chịu rủi ro khi bật cài đặt này – gửi email đến các địa chỉ chưa được xác minh có thể vi phạm luật chống thư rác trong khu vực.", + "prompt": "Nhắc người dùng nhập hoặc xác nhận email của họ", + "prompt-help": "Nếu người dùng chưa cung cấp email hoặc email của họ chưa được xác nhận, một cảnh báo sẽ được hiển thị trên màn hình.", + "sendEmailToBanned": "Gửi email cho người dùng ngay cả khi họ đã bị cấm" +} diff --git a/public/language/vi/admin/settings/general.json b/public/language/vi/admin/settings/general.json new file mode 100644 index 0000000000..7034716c95 --- /dev/null +++ b/public/language/vi/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "Cài Đặt Trang Web", + "title": "Tiêu Đề Trang Web", + "title.short": "Tiêu Đề Ngắn", + "title.short-placeholder": "Nếu không có tiêu đề ngắn nào được chỉ định, tiêu đề trang web sẽ được sử dụng", + "title.url": "Liên kết URL Tiêu đề", + "title.url-placeholder": "URL của tiêu đề trang web", + "title.url-help": "Khi tiêu đề được nhấp vào, hãy đưa người dùng đến địa chỉ này. Nếu để trống, người dùng sẽ được chuyển đến chỉ mục diễn đàn.
Lưu ý: Đây không phải là URL bên ngoài được sử dụng trong email, v.v. Nó được đặt bởi thuộc tính url trong config.json", + "title.name": "Tên Cộng Đồng Của Bạn", + "title.show-in-header": "Hiển Thị Tiêu Đề Trang Ở Phần Đầu", + "browser-title": "Tiêu Đề Trình Duyệt", + "browser-title-help": "Nếu không có tiêu đề trình duyệt nào được chỉ định, tiêu đề trang web sẽ được sử dụng", + "title-layout": "Bố Cục Tiêu Đề", + "title-layout-help": "Xác định cách tiêu đề trình duyệt sẽ được cấu trúc, tức là {pageTitle} | {browserTitle}", + "description.placeholder": "Mô tả ngắn gọn về cộng đồng của bạn", + "description": "Mô Tả Trang Web", + "keywords": "Từ Khóa Trang Web", + "keywords-placeholder": "Các từ khóa mô tả cộng đồng của bạn, được phân tách bằng dấu phẩy", + "logo": "Biểu Trưng Trang Web", + "logo.image": "Ảnh", + "logo.image-placeholder": "Đường dẫn đến biểu trưng để hiển thị phần đầu diễn đàn", + "logo.upload": "Tải lên", + "logo.url": "Liên kết URL Logo", + "logo.url-placeholder": "URL biểu trưng trang web", + "logo.url-help": "Khi nhấp vào logo, ​​hãy đưa người dùng đến địa chỉ này. Nếu để trống, người dùng sẽ được chuyển đến chỉ mục diễn đàn.
Lưu ý: Đây không phải là URL bên ngoài được sử dụng trong email, v.v. Nó được đặt bởi thuộc tính url trong config.json", + "logo.alt-text": "Văn Bản Thay Thế", + "log.alt-text-placeholder": "Văn bản thay thế cho khả năng tiếp cận", + "favicon": "Biểu tượng ưa thích", + "favicon.upload": "Tải lên", + "pwa": "Ứng Dụng Web Tiến Bộ", + "touch-icon": "Biểu Tượng Cảm Ứng", + "touch-icon.upload": "Tải lên", + "touch-icon.help": "Kích thước và định dạng được đề xuất: 512x512, chỉ định dạng PNG. Nếu không có biểu tượng cảm ứng nào, NodeBB sẽ quay trở lại sử dụng biểu tượng yêu thích.", + "maskable-icon": "Biểu tượng có thể che được (Màn Trang Chủ)", + "maskable-icon.help": "Kích thước và định dạng nên là: 512x512, chỉ định dạng PNG. Nếu không có biểu tượng có thể che được nào được chỉ định, NodeBB sẽ trở lại Biểu tượng cảm ứng.", + "outgoing-links": "Liên Kết Đi", + "outgoing-links.warning-page": "Sử Dụng Trang Cảnh Báo Liên Kết Đi", + "search": "Tìm kiếm", + "search-default-in": "Tìm kiếm trong", + "search-default-in-quick": "Tìm kiếm nhanh trong", + "search-default-sort-by": "Sắp xếp theo", + "outgoing-links.whitelist": "Các tên miền trong danh sách trắng sẽ bỏ qua trang cảnh báo", + "site-colors": "Dữ Liệu Mô Tả Màu Trang", + "theme-color": "Màu Giao Diện", + "background-color": "Màu Nền", + "background-color-help": "Màu được sử dụng cho nền màn hình khởi động khi trang web được cài đặt làm PWA", + "undo-timeout": "Hoàn tác thời gian chờ", + "undo-timeout-help": "Một số thao tác như chuyển chủ đề sẽ cho phép người kiểm duyệt hoàn tác hành động của họ trong một khung thời gian nhất định. Đặt thành 0 để tắt hoàn toàn hoàn tác.", + "topic-tools": "Công cụ chủ đề" +} diff --git a/public/language/vi/admin/settings/group.json b/public/language/vi/admin/settings/group.json new file mode 100644 index 0000000000..e9a887faaf --- /dev/null +++ b/public/language/vi/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "Chung", + "private-groups": "Nhóm Riêng Tư", + "private-groups.help": "Nếu bật, tham gia nhóm cần chủ nhóm chấp nhận (Mặc định: được bật)", + "private-groups.warning": "Coi chừng! Nếu tắt tùy chọn này và bạn có nhóm riêng tư, chúng sẽ tự động trở thành công khai.", + "allow-multiple-badges": "Cho Phép Nhiều Huy Hiệu", + "allow-multiple-badges-help": "Cờ này có thể được dùng để người dùng chọn nhiều huy hiệu nhóm, yêu cầu hỗ trợ giao diện.", + "max-name-length": "Độ Dài Tên Nhóm Tối Đa", + "max-title-length": "Độ Dài Tựa Đề Nhóm Tối Đa", + "cover-image": "Ảnh Bìa Nhóm", + "default-cover": "Ảnh Bìa Mặc Định", + "default-cover-help": "Thêm ảnh bìa mặc định được phân tách bằng dấu phẩy cho các nhóm không tải lên ảnh bìa" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/guest.json b/public/language/vi/admin/settings/guest.json new file mode 100644 index 0000000000..52c4878bc4 --- /dev/null +++ b/public/language/vi/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Cài đặt", + "handles.enabled": "Cho phép xử lý khách", + "handles.enabled-help": "Tùy chọn này hiển thị một trường mới cho phép khách chọn tên để liên kết với mỗi bài đăng mà họ thực hiện. Nếu bị tắt, họ sẽ chỉ được gọi là \"Khách\"", + "topic-views.enabled": "Cho phép khách tăng lượt xem chủ đề", + "reply-notifications.enabled": "Cho phép khách tạo thông báo trả lời" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/homepage.json b/public/language/vi/admin/settings/homepage.json new file mode 100644 index 0000000000..d4a3e81eb4 --- /dev/null +++ b/public/language/vi/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "Trang Chủ", + "description": "Chọn trang hiển thị khi người dùng được chuyển hướng đến URL gốc diễn đàn của bạn.", + "home-page-route": "Liên Kết Trang Chủ", + "custom-route": "Tùy Chỉnh Liên Kết", + "allow-user-home-pages": "Cho Phép Trang Chủ Người Dùng", + "home-page-title": "Tiêu đề của trang chủ (mặc định là \"Trang chủ\")" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/languages.json b/public/language/vi/admin/settings/languages.json new file mode 100644 index 0000000000..dfb1401e51 --- /dev/null +++ b/public/language/vi/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "Cài đặt ngôn ngữ", + "description": "Ngôn ngữ mặc định xác định ngôn ngữ cho tất cả người dùng đang truy cập diễn đàn của bạn.
Người dùng cá nhân có thể thay đổi ngôn ngữ ưa thích riêng trong cài đặt tài khoản", + "default-language": "Ngôn ngữ mặc định", + "auto-detect": "Tự động phát hiện cài đặt ngôn ngữ cho khách" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/navigation.json b/public/language/vi/admin/settings/navigation.json new file mode 100644 index 0000000000..22ce457fb4 --- /dev/null +++ b/public/language/vi/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "Biểu tượng:", + "change-icon": "thay đổi", + "route": "Liên kết:", + "tooltip": "Chú giải công cụ:", + "text": "Chữ:", + "text-class": "Lớp Chữ: không bắt buộc", + "class": "Lớp: không bắt buộc", + "id": "ID: không bắt buộc", + + "properties": "Thuộc tính:", + "groups": "Nhóm:", + "open-new-window": "Mở trong một cửa sổ mới", + "dropdown": "Thả xuống", + "dropdown-placeholder": "Đặt các mục menu thả xuống của bạn bên dưới, tức là:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "Xóa", + "btn.disable": "Tắt", + "btn.enable": "Bật", + + "available-menu-items": "Các Mục Menu Sẵn Có", + "custom-route": "Tùy Chỉnh Liên Kết", + "core": "lõi", + "plugin": "plugin" +} diff --git a/public/language/vi/admin/settings/notifications.json b/public/language/vi/admin/settings/notifications.json new file mode 100644 index 0000000000..c7055b3c21 --- /dev/null +++ b/public/language/vi/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "Thông báo", + "welcome-notification": "Thông Báo Chào Mừng", + "welcome-notification-link": "Liên Kết Thông Báo Chào Mừng", + "welcome-notification-uid": "Thông Báo Chào Mừng Người Dùng (UID)", + "post-queue-notification-uid": "Hàng Đợi Người Dùng Đăng Bài (UID)" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/pagination.json b/public/language/vi/admin/settings/pagination.json new file mode 100644 index 0000000000..64b821f480 --- /dev/null +++ b/public/language/vi/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "Cài Đặt Phân Trang", + "enable": "Phân trang chủ đề và bài đăng thay vì sử dụng cuộn vô hạn.", + "posts": "Phân Trang Bài Đăng", + "topics": "Phân Trang Chủ Đề", + "posts-per-page": "Số Bài Viết Mỗi Trang", + "max-posts-per-page": "Số Bài Viết Tối Đa Mỗi Trang", + "categories": "Phân Trang Chuyên Mục", + "topics-per-page": "Số Chủ Đề Mỗi Trang", + "max-topics-per-page": "Số Chủ Đề Tối Đa Mỗi Trang", + "categories-per-page": "Chuyên mục mỗi trang" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/post.json b/public/language/vi/admin/settings/post.json new file mode 100644 index 0000000000..10cdce3b46 --- /dev/null +++ b/public/language/vi/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "Sắp Xếp Bài Đăng", + "sorting.post-default": "Sắp Xếp Bài Đăng Mặc Định", + "sorting.oldest-to-newest": "Cũ nhất đến Mới nhất", + "sorting.newest-to-oldest": "Mới nhất đến Cũ nhất", + "sorting.most-votes": "Nhiều Bình Chọn", + "sorting.most-posts": "Nhiều Bài Đăng", + "sorting.topic-default": "Sắp Xếp Chủ Đề Mặc Định", + "length": "Độ Dài Bài Đăng", + "post-queue": "Hàng Đợi Bài Đăng", + "restrictions": "Hạn Chế Đăng Bài", + "restrictions-new": "Giới Hạn Người Dùng Mới", + "restrictions.post-queue": "Bật Hàng Đợi Bài Đăng", + "restrictions.post-queue-rep-threshold": "Cần có đủ uy tín để vượt qua hàng đợi bài đăng", + "restrictions.groups-exempt-from-post-queue": "Chọn các nhóm được miễn khỏi hàng đợi bài đăng", + "restrictions-new.post-queue": "Bật hạn chế người dùng mới", + "restrictions.post-queue-help": "Bật hàng đợi bài đăng sẽ đưa các bài đăng của người dùng mới vào hàng đợi phê duyệt", + "restrictions-new.post-queue-help": "Bật hạn chế người dùng mới sẽ đặt hạn chế đối với bài đăng do người dùng mới tạo", + "restrictions.seconds-between": "Số giây giữa các bài đăng", + "restrictions.seconds-between-new": "Số giây giữa các bài đăng cho người dùng mới", + "restrictions.rep-threshold": "Ngưỡng uy tín trước khi những hạn chế này được dỡ bỏ", + "restrictions.seconds-before-new": "Vài giây trước khi người dùng mới có thể đăng bài đầu tiên của họ", + "restrictions.seconds-edit-after": "Số giây bài đăng vẫn có thể chỉnh sửa được (đặt thành 0 để tắt)", + "restrictions.seconds-delete-after": "Số giây một bài đăng vẫn có thể xóa được (đặt thành 0 để tắt)", + "restrictions.replies-no-delete": "Số câu trả lời sau khi người dùng không được phép xóa chủ đề của chính họ (đặt thành 0 để tắt)", + "restrictions.min-title-length": "Độ Dài Tiêu Đề Tối Thiểu", + "restrictions.max-title-length": "Độ Dài Tiêu Đề Tối Đa", + "restrictions.min-post-length": "Độ Dài Bài Viết Tối Thiểu", + "restrictions.max-post-length": "Độ Dài Bài Viết Tối Đa", + "restrictions.days-until-stale": "Số ngày cho đến khi chủ đề được coi là cũ", + "restrictions.stale-help": "Nếu một chủ đề được coi là \"cũ\", thì một cảnh báo sẽ được hiển thị cho những người dùng cố gắng trả lời chủ đề đó.", + "timestamp": "Dấu thời gian", + "timestamp.cut-off": "Giới hạn ngày (tính theo ngày)", + "timestamp.cut-off-help": "Ngày tháng & thời gian sẽ được hiển thị một cách tương đối (VD: \"3 giờ trước\" / \"5 ngày trước\"), và bản địa hóa thành nhiều\n\t\t\t\t\tngôn ngữ. Sau một thời điểm nhất định, dòng chữ này có thể được chuyển sang hiển thị ngày được bản địa hóa\n\t\t\t\t\t(VD: 5 Tháng 11, 2016 15:30).
(Mặc định: 30, hoặc một tháng). Đặt là 0 để luôn hiển thị ngày tháng, để trống để luôn hiển thị thời gian tương đối.", + "timestamp.necro-threshold": "Ngưỡng Necro (ngày)", + "timestamp.necro-threshold-help": "Một thông báo sẽ được hiển thị giữa các bài đăng nếu thời gian giữa chúng dài hơn ngưỡng yêu cầu. (Mặc định: 7, hoặc một tuần). Đặt thành 0 để tắt.", + "timestamp.topic-views-interval": "Khoảng thời gian xem chủ đề tăng dần (phút)", + "timestamp.topic-views-interval-help": "Lượt xem chủ đề sẽ tăng lên một lần sau mỗi X phút được đặt bởi cài đặt này.", + "teaser": "Đoạn Giới Thiệu Bài Viết", + "teaser.last-post": "Gần đây – Hiển thị bài đăng mới nhất, bao gồm cả bài gốc, nếu không có câu trả lời", + "teaser.last-reply": "Cuối cùng - Hiển thị câu trả lời mới nhất hoặc trình giữ chỗ \"Không trả lời\" nếu không có câu trả lời", + "teaser.first": "Đầu tiên", + "showPostPreviewsOnHover": "Hiển thị bản xem trước của các bài đăng khi di chuột qua", + "unread": "Cài Đặt Chưa Đọc", + "unread.cutoff": "Số ngày giới hạn chưa đọc", + "unread.min-track-last": "Số bài viết tối thiểu trong chủ đề trước khi theo dõi lần đọc cuối cùng", + "recent": "Cài Đặt Gần Đây", + "recent.max-topics": "Chủ đề tối đa trên trang / gần đây", + "recent.categoryFilter.disable": "Tắt tính năng lọc chủ đề trong chuyên mục bị bỏ qua trên trang /gần đây", + "signature": "Cài Đặt Chữ Ký", + "signature.disable": "Tắt chữ ký", + "signature.no-links": "Tắt liên kết trong chữ ký", + "signature.no-images": "Tắt ảnh trong chữ ký", + "signature.hide-duplicates": "Ẩn chữ ký trùng lặp trong các chủ đề", + "signature.max-length": "Chữ Ký Dài Tối Đa", + "composer": "Cài Đặt Trình Biên Soạn", + "composer-help": "Các cài đặt sau chi phối chức năng và / hoặc giao diện hiển thị của trình soạn bài\n\t\t\t\tngười dùng khi họ tạo chủ đề mới hoặc trả lời các chủ đề hiện có.", + "composer.show-help": "Hiển thị tab \"Trợ giúp\"", + "composer.enable-plugin-help": "Cho phép các plugin thêm nội dung vào tab trợ giúp", + "composer.custom-help": "Văn Bản Trợ Giúp Tùy Chỉnh", + "backlinks": "Liên kết ngược", + "backlinks.enabled": "Bật liên kết ngược chủ đề", + "backlinks.help": "Nếu một bài đăng tham chiếu đến chủ đề khác, một liên kết quay lại bài đăng sẽ được chèn vào chủ đề được tham chiếu tại thời điểm đó.", + "ip-tracking": "Theo dõi IP", + "ip-tracking.each-post": "Theo dõi Địa chỉ IP mỗi bài đăng", + "enable-post-history": "Bật Lịch Sử Bài Đăng" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/reputation.json b/public/language/vi/admin/settings/reputation.json new file mode 100644 index 0000000000..ba1d868e2b --- /dev/null +++ b/public/language/vi/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "Cài Đặt Đánh Giá Uy Tín", + "disable": "Tắt Hệ Thống Đánh Giá Uy Tín", + "disable-down-voting": "Tắt Phản Đối", + "votes-are-public": "Tất Cả Bình Chọn Là Công Khai", + "thresholds": "Ngưỡng hoạt động", + "min-rep-upvote": "Uy tín tối thiểu để ủng hộ bài đăng", + "upvotes-per-day": "Số phiếu ủng hộ mỗi ngày (đặt thành 0 để có số phiếu ủng hộ không giới hạn)", + "upvotes-per-user-per-day": "Số phiếu ủng hộ cho mỗi người dùng mỗi ngày (đặt thành 0 để có số phiếu ủng hộ không giới hạn)", + "min-rep-downvote": "Uy tín tối thiểu để phản đối bài đăng", + "downvotes-per-day": "Số phản đối mỗi ngày (đặt là 0 không giới hạn phản đối)", + "downvotes-per-user-per-day": "Số phản đối mỗi người dùng mỗi ngày (đặt là 0 không giới hạn số phản đối)", + "min-rep-chat": "Uy tín tối thiểu để gửi tin nhắn trò chuyện", + "min-rep-flag": "Uy tín tối thiểu để gắn cờ bài đăng", + "min-rep-website": "Uy tín tối thiểu để thêm \"Trang web\" vào hồ sơ người dùng", + "min-rep-aboutme": "Uy tín tối thiểu để thêm \"Giới thiệu bản thân\" vào hồ sơ người dùng", + "min-rep-signature": "Uy tín tối thiểu để thêm \"Chữ ký\" vào hồ sơ người dùng", + "min-rep-profile-picture": "Uy tín tối thiểu để thêm \"Ảnh hồ sơ\" vào hồ sơ người dùng", + "min-rep-cover-picture": "Uy tín tối thiểu để thêm \"Ảnh bìa\" vào hồ sơ người dùng", + + "flags": "Cài Đặt Gắn Cờ", + "flags.limit-per-target": "Số lần tối đa nội dung nào đó có thể được gắn cờ", + "flags.limit-per-target-placeholder": "Mặc định: 0", + "flags.limit-per-target-help": "Khi một bài đăng hoặc người dùng bị gắn cờ nhiều lần, mỗi cờ bổ sung được coi là một "báo cáo" và được thêm vào cờ gốc. Đặt tùy chọn này thành một số khác 0 để giới hạn số lượng báo cáo mà một mục có thể nhận được.", + "flags.auto-flag-on-downvote-threshold": "Số phiếu phản đối cho các bài đăng tự động gắn cờ (Đặt thành 0 để tắt, mặc định: 0)", + "flags.auto-resolve-on-ban": "Tự động giải quyết tất cả các vé của người dùng khi họ bị cấm", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/social.json b/public/language/vi/admin/settings/social.json new file mode 100644 index 0000000000..f9dcde47ad --- /dev/null +++ b/public/language/vi/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Chia sẻ bài viết", + "info-plugins-additional": "Plugin có thể thêm các mạng bổ sung để chia sẻ bài viết.", + "save-success": "Mạng chia sẻ bài đã lưu thành công!" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/sockets.json b/public/language/vi/admin/settings/sockets.json new file mode 100644 index 0000000000..b4c28f0db7 --- /dev/null +++ b/public/language/vi/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Cài Đặt Kết Nối Lại", + "max-attempts": "Nỗ Lực Kết Nối Lại Tối Đa", + "default-placeholder": "Mặc định: %1", + "delay": "Độ Trễ Kết Nối Lại" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/sounds.json b/public/language/vi/admin/settings/sounds.json new file mode 100644 index 0000000000..af4e477aa9 --- /dev/null +++ b/public/language/vi/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Thông báo", + "chat-messages": "Tin Nhắn Trò Chuyện", + "play-sound": "Phát", + "incoming-message": "Tin Nhắn Đến", + "outgoing-message": "Tin Nhắn Gửi Đi", + "upload-new-sound": "Tải Lên Âm Thanh Mới", + "saved": "Đã Lưu Cài Đặt" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/tags.json b/public/language/vi/admin/settings/tags.json new file mode 100644 index 0000000000..dcfd4e09b8 --- /dev/null +++ b/public/language/vi/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Cài Đặt Thẻ", + "link-to-manage": "Quản Lý Thẻ", + "system-tags": "Thẻ Hệ Thống", + "system-tags-help": "Chỉ người dùng đặc quyền mới có thể dùng thẻ này.", + "min-per-topic": "Số Thẻ Ít Nhất Mỗi Chủ Đề", + "max-per-topic": "Số Thẻ Tối Đa Mỗi Chủ Đề", + "min-length": "Độ Dài Thẻ Tối Thiểu", + "max-length": "Độ Dài Thẻ Tối Đa", + "related-topics": "Chủ Đề Liên Quan", + "max-related-topics": "Số chủ đề liên quan tối đa để hiển thị (nếu giao diện hỗ trợ)" +} \ No newline at end of file diff --git a/public/language/vi/admin/settings/uploads.json b/public/language/vi/admin/settings/uploads.json new file mode 100644 index 0000000000..deda121e05 --- /dev/null +++ b/public/language/vi/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "Bài Đăng", + "orphans": "Tệp Mồ Côi", + "private": "Đặt tệp tải lên ở chế độ riêng tư", + "strip-exif-data": "Tách Dữ Liệu EXIF", + "preserve-orphaned-uploads": "Giữ các tệp đã tải lên trên đĩa sau khi bài đăng được xóa", + "orphanExpiryDays": "Ngày lưu giữ các tệp mồ côi", + "orphanExpiryDays-help": "Sau nhiều ngày, các tệp tải lên không rõ nguồn gốc sẽ bị xóa khỏi hệ thống tệp.
Đặt 0 hoặc để trống để tắt.", + "private-extensions": "Phần mở rộng tệp để đặt ở chế độ riêng tư", + "private-uploads-extensions-help": "Nhập danh sách phần mở rộng tệp tại đây phân tách bằng dấu phẩy để đặt ở chế độ riêng tư (VD: pdf,xls,doc). Để trống có nghĩa là mọi tệp đều riêng tư.", + "resize-image-width-threshold": "Chỉnh kích cỡ ảnh nếu chúng rộng hơn chiều rộng đã đặt", + "resize-image-width-threshold-help": "(tính bằng pixel, mặc định: 1520 pixel, đặt thành 0 để tắt)", + "resize-image-width": "Giảm kích cỡ ảnh xuống đến chiều rộng đã đặt", + "resize-image-width-help": "(tính bằng pixel, mặc định: 760 pixel, đặt thành 0 để tắt)", + "resize-image-quality": "Chất lượng để sử dụng khi thay đổi kích thước hình ảnh", + "resize-image-quality-help": "Dùng cài đặt chất lượng thấp hơn để giảm kích cỡ tệp ảnh đã thay đổi kích cỡ.", + "max-file-size": "Kích Cỡ Tệp Tối Đa (KiB)", + "max-file-size-help": "(tính bằng kibibyte, mặc định: 2048 KiB)", + "reject-image-width": "Chiều Rộng Ảnh Tối Đa (pixel)", + "reject-image-width-help": "Hình ảnh rộng hơn giá trị này sẽ bị từ chối.", + "reject-image-height": "Chiều Cao Ảnh Tối Đa (pixel)", + "reject-image-height-help": "Hình ảnh cao hơn giá trị này sẽ bị từ chối.", + "allow-topic-thumbnails": "Cho phép người dùng tải lên ảnh mô tả chủ đề", + "topic-thumb-size": "Kích Cỡ Ảnh Mô Tả Chủ Đề", + "allowed-file-extensions": "Cho Phép Phần Mở Rộng Tệp", + "allowed-file-extensions-help": "Nhập danh sách phần mở rộng tệp phân tách bằng dấu phẩy ở đây (VD: pdf,xls,doc). Để trống là cho phép tất cả.", + "upload-limit-threshold": "‎Giới hạn tốc ‎‎độ tải‎‎ người dùng lên:‎", + "upload-limit-threshold-per-minute": "‎Mỗi %1 Phút‎", + "upload-limit-threshold-per-minutes": "‎Mỗi %1 Phút‎", + "profile-avatars": "Ảnh Đại Diện Hồ Sơ", + "allow-profile-image-uploads": "Cho phép người dùng tải lên ảnh hồ sơ", + "convert-profile-image-png": "Chuyển đổi hình ảnh hồ sơ tải lên thành PNG", + "default-avatar": "Ảnh Đại Diện Mặc Định", + "upload": "Tải lên", + "profile-image-dimension": "Kích Thước Ảnh Hồ Sơ", + "profile-image-dimension-help": "(tính bằng pixel, mặc định: 128 pixel)", + "max-profile-image-size": "Kích Cỡ Tệp Ảnh Hồ Sơ Tối Đa", + "max-profile-image-size-help": "(tính bằng kibibyte, mặc định: 256 KiB)", + "max-cover-image-size": "Kích Cỡ Tệp Ảnh Bìa Tối Đa", + "max-cover-image-size-help": "(tính bằng kibibyte, mặc định: 2.048 KiB)", + "keep-all-user-images": "Giữ hình đại diện và bìa hồ sơ phiên bản cũ trên máy chủ", + "profile-covers": "Ảnh Bìa Hồ Sơ", + "default-covers": "Ảnh Bìa Mặc Định", + "default-covers-help": "Thêm ảnh bìa mặc định phân tách bằng dấu phẩy cho tài khoản không tải lên ảnh bìa " +} diff --git a/public/language/vi/admin/settings/user.json b/public/language/vi/admin/settings/user.json new file mode 100644 index 0000000000..0fd9f6f18e --- /dev/null +++ b/public/language/vi/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "Xác thực", + "email-confirm-interval": "Người dùng không thể gửi lại email xác nhận cho đến khi", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "Cho phép đăng nhập với", + "allow-login-with.username-email": "Tên Đăng Nhập hoặc Email", + "allow-login-with.username": "Chỉ Tên Đăng Nhập", + "account-settings": "Cài Đặt Tài Khoản", + "gdpr_enabled": "Bật đồng ý thu thâp GDPR", + "gdpr_enabled_help": "Khi được bật, tất cả những người đăng ký mới sẽ được yêu cầu đồng ý rõ ràng cho việc thu thập và sử dụng dữ liệu theo Quy định chung về bảo vệ dữ liệu (GDPR). Ghi chú: Bật GDPR không buộc người dùng đã có từ trước phải đồng ý. Để làm như vậy, bạn sẽ cần cài đặt plugin GDPR.", + "disable-username-changes": "Tắt thay đổi tên đăng nhập", + "disable-email-changes": "Tắt thay đổi email", + "disable-password-changes": "Tắt thay đổi mật khẩu", + "allow-account-deletion": "Cho phép xóa tài khoản", + "hide-fullname": "Ẩn tên đầy đủ khỏi người dùng", + "hide-email": "Ẩn email khỏi người dùng", + "show-fullname-as-displayname": "Hiển thị tên đầy đủ của người dùng làm tên hiển thị của họ nếu có", + "themes": "Giao diện", + "disable-user-skins": "Ngăn người dùng chọn giao diện tùy chỉnh", + "account-protection": "Bảo Vệ Tài Khoản", + "admin-relogin-duration": "Thời lượng đăng nhập lại của quản trị viên (phút)", + "admin-relogin-duration-help": "Sau một khoảng thời gian truy cập nhất định vào phần quản trị sẽ yêu cầu đăng nhập lại, hãy đặt thành 0 để tắt", + "login-attempts": "Giới hạn đăng nhập mỗi giờ", + "login-attempts-help": "Nếu số lần người dùng đăng nhập vào tài khoản vượt ngưỡng này, tài khoản sẽ bị khóa trong một khoảng thời gian đã được cài đặt", + "lockout-duration": "Thời Gian Khóa Tài Khoản (phút)", + "login-days": "Số ngày ghi nhớ phiên đăng nhập người dùng", + "password-expiry-days": "Buộc đặt lại mật khẩu sau một số ngày đã định", + "session-time": "Thời Gian Phiên", + "session-time-days": "Ngày", + "session-time-seconds": "Giây", + "session-time-help": "Giá trị này dùng để điều chỉnh thời gian người dùng đăng nhập khi họ chọn "Nhớ Tôi" lúc đăng nhập. Lưu ý chỉ một trong những giá trị này sẽ được dùng. Nếu không có giá trị giây chúng tôi sẽ dùng ngày. Nếu không có ngày mặc định là 14 ngày.", + "online-cutoff": "Số phút sau khi người dùng được coi là không hoạt động", + "online-cutoff-help": "Nếu người dùng không thao tác trong khoảng thời gian này, được coi là không hoạt động và không nhận được cập nhật theo thời gian thực.", + "registration": "Đăng Ký Người Dùng", + "registration-type": "Loại Đăng Ký", + "registration-approval-type": "Loại Xét Duyệt Đăng Ký", + "registration-type.normal": "Bình thường", + "registration-type.admin-approval": "Quản Trị Viên Phê Duyệt", + "registration-type.admin-approval-ip": "Quản Trị Viên Phê Duyệt cho IP", + "registration-type.invite-only": "Chỉ Mời", + "registration-type.admin-invite-only": "Chỉ Quản Trị Viên Mời", + "registration-type.disabled": "Không có đăng ký", + "registration-type.help": "Bình thường - Người dùng có thể đăng ký từ trang /register.
\nChỉ mời - Người dùng có thể mời những người khác từ trang người dùng.
\nChỉ Quản Trị Viên mời - Chỉ quản trị viên mới có thể mời người khác từ trang người dùngadmin/manage/users.
\nKhông đăng ký - Không đăng ký người dùng.
", + "registration-approval-type.help": "Bình thường - Người dùng được đăng ký ngay lập tức.
\nPhê duyệt của quản trị viên - Đăng ký người dùng được đặt trong một hàng đợi phê duyệt cho quản trị viên.
\nPhê duyệt của quản trị viên cho các IP - Bình thường cho người dùng mới, Phê duyệt quản trị cho các địa chỉ IP đã có tài khoản.
", + "registration-queue-auto-approve-time": "Thời Gian Xét Duyệt Tự Động", + "registration-queue-auto-approve-time-help": "Giờ trước khi người dùng được xét duyệt tự động. 0 để tắt.", + "registration-queue-show-average-time": "Hiện thời gian xét duyệt cho người dùng mới biết", + "registration.max-invites": "Lời Mời Tối Đa Mỗi Người Dùng", + "max-invites": "Lời Mời Tối Đa Mỗi Người Dùng", + "max-invites-help": "0 cho không hạn chế. Quản trị viên nhận được lời mời vô hạn
Chỉ áp dụng cho \"Chỉ được mời\"", + "invite-expiration": "Lời mời hết hạn", + "invite-expiration-help": "# ngày lời mời hết hạn.", + "min-username-length": "Tên Đăng Nhập Dài Tối Thiểu", + "max-username-length": "Tên Đăng Nhập Dài Tối Đa", + "min-password-length": "Mật Khẩu Dài Tối Thiểu", + "min-password-strength": "Độ Mạnh Mật Khẩu Tối Thiểu", + "max-about-me-length": "Độ Dài Tối Đa Giới Thiệu Bản Thân", + "terms-of-use": "Điều Khoản Sử Dụng Diễn Đàn (Để trống để tắt)", + "user-search": "Tìm Kiếm Người Dùng", + "user-search-results-per-page": "Số lượng kết quả sẽ hiển thị", + "default-user-settings": "Cài Đặt Người Dùng Mặc Định", + "show-email": "Hiển thị email", + "show-fullname": "Hiển thị tên đầy đủ", + "restrict-chat": "Chỉ cho phép tin nhắn trò chuyện từ những người dùng tôi theo dõi", + "outgoing-new-tab": "Mở các liên kết đi trong tab mới", + "topic-search": "Bật Tìm Kiếm Trong Chủ Đề", + "update-url-with-post-index": "Cập nhật url với chỉ mục bài đăng trong khi duyệt các chủ đề", + "digest-freq": "Đăng Ký Thông báo", + "digest-freq.off": "Tắt", + "digest-freq.daily": "Hàng ngày", + "digest-freq.weekly": "Hàng tuần", + "digest-freq.biweekly": "Hai tuần một lần", + "digest-freq.monthly": "Hàng tháng", + "email-chat-notifs": "Gửi email nếu có tin nhắn trò chuyện mới và tôi không trực tuyến", + "email-post-notif": "Gửi email khi có trả lời ở các chủ đề tôi đã đăng ký", + "follow-created-topics": "Theo dõi các chủ đề bạn tạo", + "follow-replied-topics": "Theo dõi các chủ đề mà bạn trả lời", + "default-notification-settings": "Cài đặt thông báo mặc định", + "categoryWatchState": "Trạng thái xem chuyên mục mặc định", + "categoryWatchState.watching": "Đang Xem", + "categoryWatchState.notwatching": "Không Xem", + "categoryWatchState.ignoring": "Bỏ Qua" +} diff --git a/public/language/vi/admin/settings/web-crawler.json b/public/language/vi/admin/settings/web-crawler.json new file mode 100644 index 0000000000..702c90af14 --- /dev/null +++ b/public/language/vi/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Cài Đặt Khả Năng Thu Thập Thông Tin", + "robots-txt": "Tùy chỉnh Robots.txt Để trống mặc định", + "sitemap-feed-settings": "Sơ đồ trang web & Cài đặt Nguồn cấp dữ liệu", + "disable-rss-feeds": "Tắt Nguồn Cấp RSS", + "disable-sitemap-xml": "Tắt Sitemap.xml", + "sitemap-topics": "Số lượng Chủ đề để hiển thị trong Sơ đồ trang web", + "clear-sitemap-cache": "Xóa Bộ Đệm Sơ Đồ Trang Web", + "view-sitemap": "Xem Sơ Đồ Trang Web" +} \ No newline at end of file diff --git a/public/language/vi/category.json b/public/language/vi/category.json new file mode 100644 index 0000000000..93bc07f7aa --- /dev/null +++ b/public/language/vi/category.json @@ -0,0 +1,23 @@ +{ + "category": "Chuyên mục", + "subcategories": "Chuyên mục con", + "new_topic_button": "Chủ Đề Mới", + "guest-login-post": "Đăng nhập để đăng bài", + "no_topics": "Không có chủ đề nào trong chuyên mục này.
Tại sao bạn không thử đăng?", + "browsing": "đang duyệt", + "no_replies": "Không ai trả lời", + "no_new_posts": "Không có bài mới.", + "watch": "Xem", + "ignore": "Bỏ qua", + "watching": "Đang xem", + "not-watching": "Không xem", + "ignoring": "Bỏ qua", + "watching.description": "Hiển thị chủ đề chưa đọc và gần đây", + "not-watching.description": "Không hiển thị chủ đề trong chưa đọc, hiển thị gần đây", + "ignoring.description": "Không hiển thị chủ đề trong chưa đọc và gần đây", + "watching.message": "Bây giờ bạn đang xem cập nhật từ danh mục này và tất cả các danh mục phụ", + "notwatching.message": "Bạn không xem cập nhật từ danh mục này và tất cả các danh mục phụ", + "ignoring.message": "Bây giờ bạn đang bỏ qua các cập nhật từ danh mục này và tất cả các danh mục phụ", + "watched-categories": "Chuyên mục đã xem", + "x-more-categories": "%1 chuyên mục khác" +} \ No newline at end of file diff --git a/public/language/vi/email.json b/public/language/vi/email.json new file mode 100644 index 0000000000..88aeb4bb50 --- /dev/null +++ b/public/language/vi/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "Kiểm Tra Email", + "password-reset-requested": "Yêu cầu đặt lại mật khẩu!", + "welcome-to": "Chào mừng bạn đến với %1", + "invite": "Lời mời từ %1", + "greeting_no_name": "Xin chào", + "greeting_with_name": "Xin chào %1", + "email.verify-your-email.subject": "Vui lòng xác thực tài khoản của bạn", + "email.verify.text1": "Bạn đã yêu cầu chúng tôi thay đổi hoặc xác nhận địa chỉ email của bạn", + "email.verify.text2": "Vì lý do bảo mật, chúng tôi chỉ thay đổi hoặc xác nhận địa chỉ email trong hồ sơ khi quyền sở hữu của nó đã được xác nhận qua email. Nếu bạn không yêu cầu điều này, bạn không cần thực hiện hành động nào.", + "email.verify.text3": "Sau khi bạn xác nhận địa chỉ email này, chúng tôi sẽ đổi email hiện tại của bạn bằng địa chỉ này (%1).", + "welcome.text1": "Cảm ơn bạn đã đăng ký tại %1!", + "welcome.text2": "Để kích hoạt đầy đủ tính năng của tài khoản, chúng tôi cần xác nhận địa chỉ email mà bạn đã đăng ký.", + "welcome.text3": "Quản trị viên đã chấp nhận đơn đăng ký của bạn. Bạn có thể đăng nhập với tên đăng nhập/mật khẩu ngay bây giờ.", + "welcome.cta": "Nhấn vào đây để xác nhận địa chỉ email", + "invitation.text1": "%1 đã mời bạn tham gia %2", + "invitation.text2": "Lời mời của bạn sẽ hết hạn sau %1 ngày.", + "invitation.cta": "Bấm vào đây để tạo tài khoản của bạn.", + "reset.text1": "Chúng tôi nhận được yêu cầu đặt lại mật khẩu của bạn, có thể bởi vì bạn đã quên nó. Nếu không đúng như vậy, vui lòng bỏ qua email này.", + "reset.text2": "Để đặt lại mật khẩu, hãy bấm vào liên kết sau:", + "reset.cta": "Nhấn vào đây để đặt lại mật khẩu của bạn", + "reset.notify.subject": "Thay đổi mật khẩu thành công", + "reset.notify.text1": "Xin thông báo với bạn: mật khẩu của bạn trên %1 đã được thay đổi thành công.", + "reset.notify.text2": "Nếu bạn không cho phép điều này, vui lòng thông báo cho quản trị viên ngay lập tức.", + "digest.latest_topics": "Chủ đề mới nhất từ %1", + "digest.top-topics": "Chủ đề hàng đầu từ %1", + "digest.popular-topics": "Các chủ đề phổ biến từ %1", + "digest.cta": "Bấm vào đây để truy cập %1", + "digest.unsub.info": "Thông báo này đã được gửi cho bạn theo cài đặt đăng ký của bạn.", + "digest.day": "ngày", + "digest.week": "tuần", + "digest.month": "tháng", + "digest.subject": "Thông báo cho %1", + "digest.title.day": "Thông Báo Hàng Ngày Của Bạn", + "digest.title.week": "Thông Báo Hàng Tuần Của Bạn", + "digest.title.month": "Thông Báo Hàng Tháng Của Bạn", + "notif.chat.subject": "Tin nhắn trò chuyện mới nhận được từ %1", + "notif.chat.cta": "Nhấn vào đây để tiếp tục cuộc hội thoại", + "notif.chat.unsub.info": "Thông báo trò chuyện này đã được gửi cho bạn dựa theo cài đặt đăng ký của bạn.", + "notif.post.unsub.info": "Thông báo bài viết này được gửi cho bạn dựa tên thiết lập nhận thông báo của bạn", + "notif.post.unsub.one-click": "Ngoài ra, hãy hủy đăng ký nhận những email tương tự trong tương lai bằng cách nhấp vào", + "notif.cta": "Đến diễn đàn", + "notif.cta-new-reply": "Xem Bài Viết", + "notif.cta-new-chat": "Xem Trò Chuyện", + "notif.test.short": "Kiểm Tra Thông Báo", + "notif.test.long": "Đây là một kiểm tra email thông báo. Gửi giúp đỡ!", + "test.text1": "Đây là email thử nghiệm để kiểm tra trình gửi email NodeBB của bạn đã cài đặt đúng.", + "unsub.cta": "Nhấn vào đây để thay đổi cài đặt.", + "unsubscribe": "hủy đăng ký", + "unsub.success": "Bạn sẽ không còn nhận được email từ danh sách gửi thư %1 ", + "unsub.failure.title": "Không thể hủy đăng ký", + "unsub.failure.message": "Rất tiếc, chúng tôi không thể xóa bạn khỏi danh sách gửi thư, vì có vấn đề với liên kết. Tuy nhiên, bạn có thể thay đổi tùy chọn email của mình bằng cách đi tới cài đặt người dùng của bạn.

(lỗi: %1)", + "banned.subject": "Bạn đã bị cấm khỏi %1", + "banned.text1": "Người dùng %1 đã bị cấm khỏi %2", + "banned.text2": "Lệnh cấm sẽ kéo dài đến %1.", + "banned.text3": "Đây là lý do tại sao bạn bị cấm:", + "closing": "Xin cảm ơn!" +} \ No newline at end of file diff --git a/public/language/vi/error.json b/public/language/vi/error.json new file mode 100644 index 0000000000..0c41fe1498 --- /dev/null +++ b/public/language/vi/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "Dữ liệu không hợp lệ", + "invalid-json": "JSON không hợp lệ", + "wrong-parameter-type": "Giá trị của loại %3 được mong đợi cho thuộc tính `%1`, nhưng thay vào đó, %2 đã được nhận", + "required-parameters-missing": "Các thông số bắt buộc bị thiếu trong lệnh gọi API này: %1", + "not-logged-in": "Có vẻ bạn chưa đăng nhập.", + "account-locked": "Tài khoản của bạn đang tạm thời bị khóa", + "search-requires-login": "Tìm kiếm yêu cầu một tài khoản - vui lòng đăng nhập hoặc đăng ký.", + "goback": "Nhấn back để quay về trang trước", + "invalid-cid": "ID chuyên mục không hợp lệ", + "invalid-tid": "ID chủ đề không hợp lệ", + "invalid-pid": "ID bài viết không hợp lệ", + "invalid-uid": "ID tài khoản không hợp lệ", + "invalid-mid": "ID Tin Nhắn Không Hợp Lệ", + "invalid-date": "Phải cung cấp một ngày hợp lệ", + "invalid-username": "Tên đăng nhập không hợp lệ", + "invalid-email": "Email không hợp lệ", + "invalid-fullname": "Tên Đầy Đủ Không Hợp Lệ", + "invalid-location": "Vị Trí Không Hợp Lệ", + "invalid-birthday": "Sinh Nhật Không Hợp Lệ", + "invalid-title": "Tiêu đề không hợp lệ", + "invalid-user-data": "Dữ liệu tài khoản không hợp lệ", + "invalid-password": "Mật khẩu không hợp lệ", + "invalid-login-credentials": "Thông tin đăng nhập không hợp lệ", + "invalid-username-or-password": "Hãy nhập tên đăng nhập và mật khẩu cụ thể", + "invalid-search-term": "Cụm từ tìm kiếm không hợp lệ", + "invalid-url": "Đường dẫn không chính xác", + "invalid-event": "Sự kiện không hợp lệ: %1", + "local-login-disabled": "Hệ thống đăng nhập nội bộ đã bị vô hiệu hóa với các tài khoản không đủ quyền.", + "csrf-invalid": "Chúng tôi không thể đăng nhập cho bạn, có thể do một phiên đã hết hạn. Vui lòng thử lại", + "invalid-path": "Đường dẫn không hợp lệ", + "folder-exists": "Thư mục tồn tại", + "invalid-pagination-value": "Giá trị trang không hợp lệ, tối thiểu phải là %1 và tối đa là %2", + "username-taken": "Tên đăng nhập đã tồn tại", + "email-taken": "Email đã được đăng kí", + "email-nochange": "Email đã nhập giống với email đã có trong tệp.", + "email-invited": "Email đã được mời", + "email-not-confirmed": "Đăng trong một số danh mục hoặc chủ đề được bật sau khi email của bạn được xác nhận, vui lòng nhấp vào đây để gửi email xác nhận.", + "email-not-confirmed-chat": "Bạn không thể trò chuyện cho đến khi email của bạn được xác nhận, vui lòng nhấp vào đây để xác nhận email của bạn.", + "email-not-confirmed-email-sent": "Email của bạn vẫn chưa được xác nhận, vui lòng kiểm tra hộp thư đến của bạn để biết email xác nhận. Bạn có thể không đăng được trong một số danh mục hoặc trò chuyện cho đến khi email của bạn được xác nhận.", + "no-email-to-confirm": "Tài khoản của bạn chưa có email. Email cần dùng lúc khôi phục tài khoản và có thể cần để trò chuyện và đăng bài trong một số danh mục. Vui lòng bấm vào đây để nhập email.", + "user-doesnt-have-email": "Người dùng \"%1\" chưa đặt email.", + "email-confirm-failed": "Chúng tôi không thể xác nhận email của bạn, vui lòng thử lại sau.", + "confirm-email-already-sent": "Email xác nhận đã được gửi, vui lòng chờ %1 phút để yêu cầu gửi lại.", + "sendmail-not-found": "Không tìm thấy lệnh thực thi \"sendmail\", hãy chắc chắn nó đã được cài đặt và thực thi bởi người quản trị đang vận hành NodeBB", + "digest-not-enabled": "Người dùng này chưa bật thông báo hoặc mặc định hệ thống không được cấu hình để gửi thông báo", + "username-too-short": "Tên đăng nhập quá ngắn", + "username-too-long": "Tên đăng nhập quá dài", + "password-too-long": "Mật khẩu quá dài", + "reset-rate-limited": "Quá nhiều yêu cầu đặt lại mật khẩu (giới hạn tỷ lệ)", + "reset-same-password": "Vui lòng sử dụng mật khẩu khác với mật khẩu hiện tại của bạn", + "user-banned": "Người dùng đã bị cấm", + "user-banned-reason": "Xin lỗi, tài khoản này đã bị cấm (Lí do: %1)", + "user-banned-reason-until": "Xin lỗi, tài khoản này bị cấm cho đến %1 (Lý do: %2)", + "user-too-new": "Xin lỗi, bắt buộc bạn phải đợi %1 giây trước khi đăng bài viết đầu tiên.", + "blacklisted-ip": "Xin lỗi, địa chỉ IP bạn bị cấm khỏi cộng đồng. Nếu bạn cảm thấy dây là do lỗi, hãy liên lạc với quản trị viên.", + "ban-expiry-missing": "Vui lòng cung cấp ngày hết lệnh cấm này", + "no-category": "Chuyên mục không tồn tại", + "no-topic": "Chủ đề không tồn tại", + "no-post": "Bài viết không tồn tại", + "no-group": "Nhóm không tồn tại", + "no-user": "Người dùng không tồn tại", + "no-teaser": "Đoạn giới thiệu không tồn tại", + "no-flag": "Cờ không tồn tại", + "no-privileges": "Bạn không đủ quyền để thực thi hành động này", + "category-disabled": "Chuyên mục bị khóa", + "topic-locked": "Chủ đề bị khóa", + "post-edit-duration-expired": "Bạn chỉ được phép sửa bài viết sau khi đăng %1 giây.", + "post-edit-duration-expired-minutes": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 phút(s)", + "post-edit-duration-expired-minutes-seconds": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 phút(s) %2 giây(s)", + "post-edit-duration-expired-hours": "Bạn chỉ được phép sửa bài viết sau khi đăng %1 giờ(s).", + "post-edit-duration-expired-hours-minutes": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 giờ(s) %2 phút(s)", + "post-edit-duration-expired-days": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 ngày(s)", + "post-edit-duration-expired-days-hours": "Bạn chỉ được phép sửa các bài viết sau khi đăng %1 ngày(s) %2 giờ(s)", + "post-delete-duration-expired": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giây(s)", + "post-delete-duration-expired-minutes": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 phút(s)", + "post-delete-duration-expired-minutes-seconds": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 phút(s) %2 giây(s)", + "post-delete-duration-expired-hours": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giờ(s)", + "post-delete-duration-expired-hours-minutes": "Bạn chỉ được phép xóa bài viết sau khi đăng %1 giờ(s) 2 phút(s)", + "post-delete-duration-expired-days": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s)", + "post-delete-duration-expired-days-hours": "Bạn chỉ được phép xóa các bài viết sau khi đăng %1 ngày(s) %2 giờ(s)", + "cant-delete-topic-has-reply": "Bạn không thể xóa chủ đề vì đã có 1 bình luận", + "cant-delete-topic-has-replies": "Bạn không thể xóa chủ đề này vì đã có %1 bình luận", + "content-too-short": "Vui lòng nhập một bài viết dài hơn. Bài viết phải có tối thiểu %1 ký tự.", + "content-too-long": "Vui lòng nhập một bài viết ngắn hơn. Bài viết chỉ có thể có tối đa %1 ký tự.", + "title-too-short": "Vui lòng nhập tiêu đề dài hơn. Tiêu đề phải có tối thiểu %1 ký tự.", + "title-too-long": "Vui lòng nhập tiêu đề ngắn hơn. Tiêu đề chỉ có thể có tối đa %1 ký tự.", + "category-not-selected": "Chưa chọn category", + "too-many-posts": "Bạn chỉ có đăng bài mới mỗi %1 giây - vui lòng đợi để tiếp tục đăng bài.", + "too-many-posts-newbie": "Là người dùng mới, bạn chỉ có thể đăng %1 giây một lần cho đến khi bạn đạt được %2 danh tiếng - vui lòng đợi trước khi đăng lại", + "already-posting": "You are already posting", + "tag-too-short": "Vui lòng nhập tag dài hơn. Tag phải có tối thiểu %1 ký tự.", + "tag-too-long": "Vui lòng nhập tag ngắn hơn. Tag chỉ có thể có tối đa %1 ký tự.", + "not-enough-tags": "Không đủ thẻ. Chủ đề phải có ít nhất %1 thẻ.", + "too-many-tags": "Quá nhiều tag. Chủ đề chỉ có thể có tối đa %1 tag.", + "cant-use-system-tag": "Bạn không thể dùng thẻ hệ thống này.", + "cant-remove-system-tag": "Bạn không thể xóa thẻ hệ thống này.", + "still-uploading": "Vui lòng đợi quá trình tải lên hoàn tất.", + "file-too-big": "Kích thước tệp cho phép tối đa là %1 kB - vui lòng tải lên một tệp nhỏ hơn", + "guest-upload-disabled": "Tải lên của khách đã bị tắt", + "cors-error": "Không thể tải lên hình ảnh do CORS bị cấu hình sai", + "upload-ratelimit-reached": "‎Bạn‎‎ đã tải lên quá nhiều tệp cùng một lúc. Vui lòng thử lại sau.‎", + "scheduling-to-past": "Vui lòng chọn một ngày trong tương lai.", + "invalid-schedule-date": "Vui lòng nhập ngày và giờ hợp lệ.", + "cant-pin-scheduled": "Không thể ghim (bỏ) các chủ đề đã lên lịch.", + "cant-merge-scheduled": "Các chủ đề đã lên lịch không thể gộp", + "cant-move-posts-to-scheduled": "Không thể chuyển bài đăng sang chủ đề đã lên lịch.", + "cant-move-from-scheduled-to-existing": "Không thể chuyển bài đăng từ một chủ đề đã lên lịch sang một chủ đề hiện có.", + "already-bookmarked": "Bạn đã đánh dấu trang chủ đề này rồi", + "already-unbookmarked": "Bạn đã hủy đánh dấu trang chủ đề này rồi", + "cant-ban-other-admins": "Bạn không thể cấm quản trị viên khác!", + "cant-mute-other-admins": "Bạn không thể buộc quản trị viên khác im lặng!", + "user-muted-for-hours": "Bạn bị buộc giữ im lặng, bạn sẽ có thể đăng sau %1 giờ", + "user-muted-for-minutes": "Bạn bị buộc giữ im lặng, bạn sẽ có thể đăng sau %1 phút", + "cant-make-banned-users-admin": "Bạn không thể đặt người dùng bị cấm làm quản trị viên.", + "cant-remove-last-admin": "Bạn là quản trị viên duy nhất. Hãy cho thành viên khác làm quản trị viên trước khi huỷ bỏ quyền quản trị của bạn.", + "account-deletion-disabled": "Tính năng xóa tài khoản đã bị tắt", + "cant-delete-admin": "Gỡ bỏ đặc quyền quản trị viên khỏi tài khoản này trước khi cố gắng xóa nó.", + "already-deleting": "Đã sẵn sàng xóa", + "invalid-image": "Hình ảnh không hợp lệ", + "invalid-image-type": "Định dạng ảnh không hợp lệ. Những định dạng được cho phép là: %1", + "invalid-image-extension": "Định dạng ảnh không hợp lệ", + "invalid-file-type": "Loại tệp không hợp lệ. Loại cho phép là: %1", + "invalid-image-dimensions": "Độ phân giải của ảnh quá lớn", + "group-name-too-short": "Tên nhóm quá ngắn", + "group-name-too-long": "Tên nhóm quá dài", + "group-already-exists": "Nhóm đã tồn tại", + "group-name-change-not-allowed": "Không cho phép đổi tên nhóm", + "group-already-member": "Đã là thành viên của nhóm.", + "group-not-member": "Không phải thành viên nhóm này.", + "group-needs-owner": "Yêu cầu phải có ít nhất một chủ nhóm", + "group-already-invited": "Thành viên này đã được mời", + "group-already-requested": "Yêu cầu tham gia của bạn đã được gửi.", + "group-join-disabled": "Bạn không thể tham gia nhóm này vào lúc này", + "group-leave-disabled": "Bạn không thể rời khỏi nhóm này vào lúc này", + "post-already-deleted": "Bài viết này đã bị xóa", + "post-already-restored": "Bài viết này đã được phục hồi", + "topic-already-deleted": "Chủ đề này đã bị xóa", + "topic-already-restored": "Chủ đề này đã được phục hồi", + "cant-purge-main-post": "Bạn không thể xoá bài viết chính, thay vào đó, vui lòng xoá chủ đề.", + "topic-thumbnails-are-disabled": "Thumbnails cho chủ đề đã bị tắt", + "invalid-file": "Tệp Không Hợp Lệ", + "uploads-are-disabled": "Tải lên bị tắt", + "signature-too-long": "Xin lỗi, chữ ký của bạn không thể dài hơn %1 ký tự.", + "about-me-too-long": "Xin lỗi, giới thiệu bản thân bạn không thể dài hơn %1 ký tự.", + "cant-chat-with-yourself": "Bạn không thể trò chuyện với chính bạn!", + "chat-restricted": "Người dùng này đã hạn chế tin nhắn trò chuyện của họ. Họ phải theo dõi bạn trước khi bạn có thể trò chuyện với họ", + "chat-disabled": "Hệ thống trò chuyện bị tắt", + "too-many-messages": "Bạn đã gửi quá nhiều tin nhắn, vui lòng đợi trong giây lát.", + "invalid-chat-message": "Tin nhắn trò chuyện không hợp lệ", + "chat-message-too-long": "Tin nhắn trò chuyện không được dài hơn %1 ký tự.", + "cant-edit-chat-message": "Bạn không được phép chỉnh sửa tin nhắn này", + "cant-delete-chat-message": "Bạn không được phép xoá tin nhắn này", + "chat-edit-duration-expired": "Bạn chỉ được phép sửa tin nhắn trò chuyện này trong %1 giây sau khi đăng.", + "chat-delete-duration-expired": "Bạn chỉ được phép xóa tin nhắn trò chuyện này trong %1 giây sau khi đăng.", + "chat-deleted-already": "Cuộc trò chuyện này đã được xóa.", + "chat-restored-already": "Tin nhắn trò chuyện này đã được khôi phục.", + "chat-room-does-not-exist": "Phòng trò chuyện không tồn tại.", + "already-voting-for-this-post": "Bạn đã bỏ phiếu cho bài viết này", + "reputation-system-disabled": "Hệ thống đánh giá uy tính đã bị vô hiệu hóa.", + "downvoting-disabled": "Phản đối đã bị tắt", + "not-enough-reputation-to-chat": "Bạn cần %1 uy tín để trò chuyện", + "not-enough-reputation-to-upvote": "Bạn cần %1 uy tín để ủng hộ", + "not-enough-reputation-to-downvote": "Bạn cần %1 uy tín để phản đối", + "not-enough-reputation-to-flag": "Bạn cần %1 uy tín để gắn cờ bài đăng này", + "not-enough-reputation-min-rep-website": "Bạn cần %1 uy tín để thêm một trang web", + "not-enough-reputation-min-rep-aboutme": "Bạn cần %1 uy tín để thêm thông tin bản thân", + "not-enough-reputation-min-rep-signature": "Bạn cần %1 uy tín để thêm chữ ký", + "not-enough-reputation-min-rep-profile-picture": "Bạn cần %1 uy tín để thêm ảnh hồ sơ", + "not-enough-reputation-min-rep-cover-picture": "Bạn cần %1 uy tín để thêm ảnh bìa", + "post-already-flagged": "Bạn đã gắn cờ bài đăng này", + "user-already-flagged": "Bạn đã gắn cờ người dùng này", + "post-flagged-too-many-times": "Bài đăng này đã bị người khác gắn cờ", + "user-flagged-too-many-times": "Người dùng này đã bị người khác gắn cờ", + "cant-flag-privileged": "Bạn không có quyền gắn cờ hồ sơ / nội dung của người dùng đặc biệt (người kiểm duyệt/ người điều hành toàn quyền/ quản trị viên)", + "self-vote": "Bạn không thể tự bầu cho bài đăng của mình", + "too-many-upvotes-today": "Bạn chỉ có thể ủng hộ %1 lần một ngày", + "too-many-upvotes-today-user": "Bạn chỉ có thể ủng hộ người dùng %1 lần một ngày", + "too-many-downvotes-today": "Bạn chỉ có thể phản đối %1 lần một ngày", + "too-many-downvotes-today-user": "Bạn chỉ có thể phản đối người dùng %1 lần một ngày", + "reload-failed": "NodeBB gặp lỗi trong khi tải lại: \"%1\". NodeBB sẽ tiếp tục hoạt động với dữ liệu trước đó, tuy nhiên bạn nên tháo gỡ những gì bạn vừa thực hiện trước khi tải lại.", + "registration-error": "Lỗi đăng kí", + "parse-error": "Có gì không ổn khi nhận kết quả từ máy chủ", + "wrong-login-type-email": "Hãy đăng nhập bằng email của bạn", + "wrong-login-type-username": "Hãy đăng nhập bằng tên đăng nhập của bạn", + "sso-registration-disabled": "Không thể đăng ký với tài khoản %1, vui lòng đăng ký với địa chỉ email của bạn", + "sso-multiple-association": "Bạn không thể liên kết nhiều tài khoản từ dịch vụ này đến tài khoản NodeBB của bạn. Vui lòng gỡ liên kết trong tài khoản của bạn và thử lại.", + "invite-maximum-met": "Bạn đã sử dụng hết số lượng lời mời bạn có thể gửi (%1 đã gửi trên tổng số %2 được cho phép)", + "no-session-found": "Không tìm thấy phiên đăng nhập!", + "not-in-room": "Thành viên không có trong phòng", + "cant-kick-self": "Bạn không thể loại mình khỏi nhóm", + "no-users-selected": "Chưa có người dùng(s) nào", + "invalid-home-page-route": "Đường dẫn trang chủ không hợp lệ", + "invalid-session": "Phiên Không Hợp Lệ", + "invalid-session-text": "Có vẻ như phiên đăng nhập của bạn không còn hoạt động. Vui lòng làm mới trang này.", + "session-mismatch": "‎Phiên Không Khớp‎", + "session-mismatch-text": "Có vẻ như phiên đăng nhập của bạn không còn khớp với máy chủ. Vui lòng làm mới trang này.", + "no-topics-selected": "Không có chủ đề nào đang được chọn!", + "cant-move-to-same-topic": "Bạn không thể di chuyển bài viết vào cùng chủ đề hiện tại!", + "cant-move-topic-to-same-category": "Không thể di chuyển chủ đề sang cùng chuyên mục!", + "cannot-block-self": "Bạn không thể tự khóa tài khoản của bạn!", + "cannot-block-privileged": "Bạn không thể khóa người quản trị hoặc là người quản lý chung.", + "cannot-block-guest": "Khách không thể chặn người dùng khác", + "already-blocked": "Người dùng này đã bị chặn", + "already-unblocked": "Người dùng này đã được bỏ chặn", + "no-connection": "Kết nối internet của bạn có vấn đề.", + "socket-reconnect-failed": "Không thể truy cập máy chủ vào lúc này. Nhấp vào đây để thử lại hoặc thử lại sau", + "plugin-not-whitelisted": "Không thể cài đặt plugin – chỉ những plugin được Trình quản lý gói NodeBB đưa vào danh sách trắng mới có thể được cài đặt qua ACP", + "plugins-set-in-configuration": "Bạn không được phép thay đổi trạng thái plugin vì chúng được xác định trong thời gian chạy (config.json, biến môi trường hoặc đối số đầu cuối), thay vào đó hãy sửa đổi cấu hình.", + "theme-not-set-in-configuration": "Khi xác định các plugin hoạt động trong cấu hình, việc thay đổi chủ đề yêu cầu thêm chủ đề mới vào danh sách các plugin hoạt động trước khi cập nhật nó trong ACP", + "topic-event-unrecognized": "Sự kiện chủ đề '%1' không được công nhận", + "cant-set-child-as-parent": "Không thể đặt con làm chuyên mục chính", + "cant-set-self-as-parent": "Không thể tự đặt mình là chuyên mục chính", + "api.master-token-no-uid": "Mã thông báo chính đã được nhận mà không có `_uid` tương ứng trong nội dung yêu cầu", + "api.400": "Đã xảy ra lỗi với tải trọng yêu cầu mà bạn đã chuyển vào.", + "api.401": "Một phiên đăng nhập hợp lệ không được tìm thấy. Hãy đăng nhập và thử lại.", + "api.403": "Bạn không được phép thực hiện lệnh gọi này", + "api.404": "Lệnh gọi API không hợp lệ", + "api.426": "HTTPS là bắt buộc đối với các yêu cầu đối với api viết, vui lòng gửi lại yêu cầu của bạn qua HTTPS", + "api.429": "Bạn đã đưa ra quá nhiều yêu cầu, vui lòng thử lại sau", + "api.500": "Đã xảy ra lỗi không mong muốn khi cố gắng thực hiện yêu cầu của bạn.", + "api.501": "Định tuyến bạn đang cố gắng gọi chưa được triển khai, vui lòng thử lại vào ngày mai", + "api.503": "Định tuyến bạn đang cố gọi hiện không khả dụng do cấu hình máy chủ" +} \ No newline at end of file diff --git a/public/language/vi/flags.json b/public/language/vi/flags.json new file mode 100644 index 0000000000..e9ae411735 --- /dev/null +++ b/public/language/vi/flags.json @@ -0,0 +1,89 @@ +{ + "state": "Trạng thái", + "reports": "Báo cáo", + "first-reported": "Được báo cáo đầu tiên", + "no-flags": "Hoan hô! Không tìm thấy cờ.", + "assignee": "Người được ủy nhiệm", + "update": "Cập nhật", + "updated": "Đã cập nhật", + "resolved": "Đã Xử Lý", + "target-purged": "Nội dung mà cờ này đề cập đến đã bị xóa và không còn nữa.", + + "graph-label": "Cờ Hàng Ngày", + "quick-filters": "Bộ Lọc Nhanh", + "filter-active": "Có một hoặc nhiều bộ lọc đang hoạt động trong danh sách cờ này", + "filter-reset": "Xóa Bộ Lọc", + "filters": "Tùy Chỉnh Bộ Lọc", + "filter-reporterId": "UID người báo cáo", + "filter-targetUid": "UID bị gắn cờ", + "filter-type": "Loại Cờ", + "filter-type-all": "Tất Cả Nội Dung", + "filter-type-post": "Bài viết", + "filter-type-user": "Người dùng", + "filter-state": "Trạng thái", + "filter-assignee": "UID Được chỉ định", + "filter-cid": "Chuyên mục", + "filter-quick-mine": "Được giao cho tôi", + "filter-cid-all": "Tất cả chuyên mục", + "apply-filters": "Áp Dụng Bộ Lọc", + "more-filters": "Thêm Nhiều Bộ Lọc", + "fewer-filters": "Ít bộ lọc hơn", + + "quick-actions": "Hành Động Nhanh", + "flagged-user": "Người Dùng Bị Gắn Cờ", + "view-profile": "Xem Hồ Sơ", + "start-new-chat": "Bắt Đầu Trò Chuyện Mới", + "go-to-target": "Xem Mục Tiêu Gắn Cờ", + "assign-to-me": "Giao cho tôi", + "delete-post": "Xóa Bài Viết", + "purge-post": "Thanh Lọc Bài Viết", + "restore-post": "Khôi Phục Bài Viết", + "delete": "Xóa cờ", + + "user-view": "Xem Hồ Sơ", + "user-edit": "Sửa Hồ Sơ", + + "notes": "Ghi Chú Cờ", + "add-note": "Thêm Ghi Chú", + "no-notes": "Không có chia sẻ ghi chú.", + "delete-note-confirm": "Bạn có chắc muốn xóa ghi chú cờ này không?", + "delete-flag-confirm": "Bạn có chắc chắn muốn xóa cờ này không?", + "note-added": "Đã Thêm Ghi Chú", + "note-deleted": "Đã Xóa Ghi Chú", + "flag-deleted": "Đã xóa cờ", + + "history": "Tài Khoản & Lịch Sử Gắn Cờ", + "no-history": "Không có lịch sử gắn cờ", + + "state-all": "Tất cả trạng thái", + "state-open": "Mới/Mở", + "state-wip": "Đang Xử Lý", + "state-resolved": "Đã Giải Quyết", + "state-rejected": "Từ Chối", + "no-assignee": "Không có chỉ định", + + "sort": "Sắp xếp theo", + "sort-newest": "Mới nhất trước", + "sort-oldest": "Cũ nhất trước", + "sort-reports": "Nhiều báo cáo", + "sort-all": "Tất cả các loại cờ", + "sort-posts-only": "Chỉ bài viết...", + "sort-downvotes": "Nhiều phản đối", + "sort-upvotes": "Nhiều ủng hộ", + "sort-replies": "Nhiều lượt trả lời", + + "modal-title": "Báo Cáo Nội Dung", + "modal-body": "Vui lòng nêu rõ lý do bạn gắn cờ %1 %2 để xem xét. Ngoài ra, hãy sử dụng một trong các nút báo cáo nhanh nếu có.", + "modal-reason-spam": "Quấy Rối", + "modal-reason-offensive": "Phản Cảm", + "modal-reason-other": "Khác (Ghi Rõ Bên Dưới)", + "modal-reason-custom": "Lý do báo cáo nội dung này...", + "modal-submit": "Gửi Báo Cáo", + "modal-submit-success": "Nội dung đã được gắn cờ để kiểm duyệt.", + + "bulk-actions": "‎Hành động hàng loạt‎", + "bulk-resolve": "Xử Lý Cờ", + "bulk-success": "%1 cờ đã cập nhật", + "flagged-timeago-readable": "Đã Gắn Cờ (%2)", + "auto-flagged": "[Tự động được gắn cờ] Đã nhận được %1 phiếu phản đối." +} \ No newline at end of file diff --git a/public/language/vi/global.json b/public/language/vi/global.json new file mode 100644 index 0000000000..ddce58282d --- /dev/null +++ b/public/language/vi/global.json @@ -0,0 +1,126 @@ +{ + "home": "Trang chủ", + "search": "Tìm kiếm", + "buttons.close": "Đóng", + "403.title": "Truy Cập Bị Từ Chối", + "403.message": "Dường như bạn đã tình cờ gặp một trang mà bạn không có quyền truy cập.", + "403.login": "Có lẽ bạn nên thử đăng nhập?", + "404.title": "Không Tìm Thấy", + "404.message": "Có vẻ như bạn đã tình cờ gặp một trang không tồn tại. Hãy trở lại trang chủ.", + "500.title": "Lỗi Bên Trong.", + "500.message": "Úi chà! Có vẻ như đã xảy ra sự cố!", + "400.title": "Yêu Cầu Không Hợp Lệ.", + "400.message": "Có vẻ như liên kết này không đúng định dạng, vui lòng kiểm tra kỹ và thử lại. Nếu không, hãy quay về trang chủ.", + "register": "Đăng ký", + "login": "Đăng nhập", + "please_log_in": "Vui Lòng Đăng Nhập", + "logout": "Đăng xuất", + "posting_restriction_info": "Chỉ thành viên chính thức mới được phép đăng bài, nhấn vào đây để đăng nhập.", + "welcome_back": "Chào mừng bạn quay lại", + "you_have_successfully_logged_in": "Bạn đã đăng nhập thành công", + "save_changes": "Lưu thay đổi", + "save": "Lưu", + "close": "Đóng", + "pagination": "Phân trang", + "pagination.out_of": "%1 trong số %2", + "pagination.enter_index": "Đi đến chỉ mục bài đăng", + "header.admin": "Quản trị viên", + "header.categories": "Chuyên mục", + "header.recent": "Gần đây", + "header.unread": "Chưa đọc", + "header.tags": "Thẻ", + "header.popular": "Phổ Biến", + "header.top": "Hàng Đầu", + "header.users": "Người Dùng", + "header.groups": "Nhóm", + "header.chats": "Trò Chuyện", + "header.notifications": "Thông báo", + "header.search": "Tìm kiếm", + "header.profile": "Hồ sơ", + "header.navigation": "Điều hướng", + "notifications.loading": "Đang tải Thông báo", + "chats.loading": "Đang Tải Trò Chuyện", + "motd.welcome": "Chào mừng bạn đến với NodeBB, nền tảng thảo luận của tương lai.", + "previouspage": "Trang trước", + "nextpage": "Trang kế", + "alert.success": "Thành công", + "alert.error": "Lỗi", + "alert.banned": "Bị cấm", + "alert.banned.message": "Bạn vừa bị cấm, quyền truy cập của bạn hiện bị hạn chế.", + "alert.unbanned": "Bỏ cấm", + "alert.unbanned.message": "Lệnh cấm của bạn đã được dỡ bỏ.", + "alert.unfollow": "Bạn không còn theo dõi %1!", + "alert.follow": "Bạn đang theo dõi %1!", + "users": "Người dùng", + "topics": "Chủ Đề", + "posts": "Bài Viết", + "x-posts": "%1 bài viết", + "best": "Tốt", + "controversial": "Gây tranh cãi", + "votes": "Bình chọn", + "x-votes": "%1 bình chọn", + "voters": "Người Bình Chọn", + "upvoters": "Người Ủng Hộ", + "upvoted": "Đã Ủng Hộ", + "downvoters": "Người phản đối", + "downvoted": "Đã phản đối", + "views": "Lượt xem", + "posters": "Người đăng bài", + "reputation": "Uy tín", + "lastpost": "Bài viết cuối cùng", + "firstpost": "Bài viết đầu tiên", + "read_more": "Đọc thêm", + "more": "Xem thêm", + "none": "Trống", + "posted_ago_by_guest": "đã đăng %1 bởi Khách", + "posted_ago_by": "đã đăng %1 bởi %2", + "posted_ago": "đã đăng %1", + "posted_in": "được đăng trong %1", + "posted_in_by": "được đăng trong %1 bởi %2", + "posted_in_ago": "được đăng trong %1 %2", + "posted_in_ago_by": "được đăng trong %1 %2 bởi %3", + "user_posted_ago": "%1 đã đăng %2", + "guest_posted_ago": "Khách đã đăng %1", + "last_edited_by": "chỉnh sửa lần cuối bởi %1", + "norecentposts": "Không có bài viết nào gần đây", + "norecenttopics": "Không có chủ đề gần đây", + "recentposts": "Bài Viết Gần Đây", + "recentips": "Các IP đã Đăng nhập Gần đây", + "moderator_tools": "Công cụ quản lí", + "online": "Trực tuyến", + "away": "Vắng mặt", + "dnd": "Đừng làm phiền", + "invisible": "Ẩn", + "offline": "Ngoại tuyến", + "email": "Thư điện tử", + "language": "Ngôn ngữ", + "guest": "Khách", + "guests": "Số khách", + "former_user": "Một người dùng cũ", + "system-user": "Hệ thống", + "unknown-user": "Người dùng không xác định", + "updated.title": "Đã Cập Nhật Diễn Đàn", + "updated.message": "Diễn đàn đã được cập nhật phiên bản mới nhất. Nhấn vào đây để tải lại trang.", + "privacy": "Quyền riêng tư", + "follow": "Theo dõi", + "unfollow": "Huỷ theo dõi", + "delete_all": "Xóa hết", + "map": "Bản đồ", + "sessions": "Phiên đăng nhập", + "ip_address": "Địa chỉ IP", + "enter_page_number": "Nhập vào số trang", + "upload_file": "Tải lên tệp", + "upload": "Tải lên", + "uploads": "Tải lên", + "allowed-file-types": "Loại cho phép là %1", + "unsaved-changes": "Có một vài thay đổi chưa được lưu. Bạn muốn rời đi ngay?", + "reconnecting-message": "Có vẻ như bạn đã mất kết nối tới %1, vui lòng đợi một lúc để chúng tôi thử kết nối lại.", + "play": "Chơi", + "cookies.message": "Trang web này sử dụng cookie để đảm bảo bạn có được trải nghiệm tốt.", + "cookies.accept": "Đã rõ!", + "cookies.learn_more": "Xem thêm", + "edited": "Đã cập nhật", + "disabled": "Đã tắt", + "select": "Chọn", + "user-search-prompt": "Nhập để tìm kiếm thành viên" +} \ No newline at end of file diff --git a/public/language/vi/groups.json b/public/language/vi/groups.json new file mode 100644 index 0000000000..359333b498 --- /dev/null +++ b/public/language/vi/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "Nhóm", + "view_group": "Xem nhóm", + "owner": "Người Sở Hữu Nhóm", + "new_group": "Tạo nhóm mới", + "no_groups_found": "Không có nhóm nào để xem", + "pending.accept": "Chấp nhận", + "pending.reject": "Từ chối", + "pending.accept_all": "Chấp nhận tất cả", + "pending.reject_all": "Từ chối tất cả", + "pending.none": "Không có ai đang chờ duyệt tham gia nhóm", + "invited.none": "Không có thành viên nào được mời vào lúc này", + "invited.uninvite": "Từ chối lời mời", + "invited.search": "Tìm kiếm thành viên để mời vào nhóm", + "invited.notification_title": "Bạn đã được mời tham gia %1", + "request.notification_title": "Yêu cầu tham gia nhóm từ %1", + "request.notification_text": "%1 yêu cầu chấp nhận để trở thành thành viên của %2", + "cover-save": "Lưu", + "cover-saving": "Đang lưu", + "details.title": "Chi Tiết Nhóm", + "details.members": "Danh Sách Thành Viên", + "details.pending": "Thành viên đang chờ trả lời", + "details.invited": "Thành viên đã được mời", + "details.has_no_posts": "Thành viên nhóm này chưa đăng bài viết nào.", + "details.latest_posts": "Bài viết mới nhất", + "details.private": "Riêng tư", + "details.disableJoinRequests": "Tắt yêu cầu tham gia", + "details.disableLeave": "Không cho phép người dùng rời khỏi nhóm", + "details.grant": "Cấp/Huỷ bỏ quyền sở hữu", + "details.kick": "Đá ra", + "details.kick_confirm": "Bạn có chắc chắn muốn xoá thành viên này khỏi nhóm?", + "details.add-member": "Thêm Thành Viên", + "details.owner_options": "Quản trị nhóm", + "details.group_name": "Tên nhóm", + "details.member_count": "Số thành viên", + "details.creation_date": "Ngày Thành Lập", + "details.description": "Miêu tả", + "details.member-post-cids": "ID chuyên mục để hiển thị bài đăng từ", + "details.badge_preview": "Xem thử huy hiệu", + "details.change_icon": "Đổi Biểu Tượng", + "details.change_label_colour": "Thay đổi màu nhãn", + "details.change_text_colour": "Thay đổi màu chữ", + "details.badge_text": "Chữ Huy hiệu", + "details.userTitleEnabled": "Hiển thị huy hiệu", + "details.private_help": "Nếu bật, tham gia nhóm cần được chủ nhóm chấp nhận", + "details.hidden": "Đã ẩn", + "details.hidden_help": "Nếu bật, nhóm này sẽ không được hiện thị trong danh sách nhóm, và thành viên phải được mời để tham gia", + "details.delete_group": "Xoá nhóm", + "details.private_system_help": "Các nhóm kín được vô hiệu hóa bởi hệ thống, tùy chọn này không thực hiện bất cứ điều gì cả", + "event.updated": "Thông tin nhóm đã được cập nhật", + "event.deleted": "Nhóm \"%1\" đã bị xoá", + "membership.accept-invitation": "Chấp nhận lời mời", + "membership.accept.notification_title": "Bạn hiện là thành viên của %1", + "membership.invitation-pending": "Lời mời đang chờ trả lời", + "membership.join-group": "Tham gia nhóm", + "membership.leave-group": "Rời khỏi nhóm", + "membership.leave.notification_title": "%1 đã rời nhóm %2", + "membership.reject": "Từ chối", + "new-group.group_name": "Tên Nhóm:", + "upload-group-cover": "Tải lên ảnh bìa nhóm", + "bulk-invite-instructions": "Nhập danh sách username, ngăn cách bằng dấu phẩy, để mời vào nhóm", + "bulk-invite": "Mời nhiều người", + "remove_group_cover_confirm": "Bạn có chắc rằng muốn xoá ảnh bìa không?" +} \ No newline at end of file diff --git a/public/language/vi/ip-blacklist.json b/public/language/vi/ip-blacklist.json new file mode 100644 index 0000000000..8a7c649a2f --- /dev/null +++ b/public/language/vi/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "Cấu hình Danh sách đen IP tại đây.", + "description": "Đôi khi, việc cấm người dùng không hiệu quả. Giới hạn IP cụ thể hoặc một loạt các IP truy cập vào diễn đàn là cách khác tốt hơn để bảo vệ diễn đàn. Với cách này, bạn có thể thêm địa chỉ IP rắc rối hoặc toàn bộ khối CIDR vào danh sách đen này và chúng sẽ bị ngăn đăng nhập hoặc đăng ký tài khoản mới.", + "active-rules": "Quy Tắc Hoạt Động", + "validate": "Xác Thực Danh Sách Đen", + "apply": "Áp Sụng Danh Sách Đen", + "hints": "Gợi Ý Cú Pháp", + "hint-1": "Xác định một địa chỉ IP trên mỗi dòng. Bạn có thể thêm các khối IP miễn là chúng tuân theo định dạng CIDR (VD: 192.168.100.0/22).", + "hint-2": "Bạn có thể thêm bình luận bằng các dòng bắt đầu với ký tự #.", + + "validate.x-valid": "%1 trên %2 quy tắc hợp lệ.", + "validate.x-invalid": "%1 quy tắc sau không hợp lệ:", + + "alerts.applied-success": "Đã Áp Dụng Danh Sách Đen", + + "analytics.blacklist-hourly": "Hình 1 – Danh sách đen truy cập mỗi giờ", + "analytics.blacklist-daily": "Hình 2 – Danh sách đen truy cập mỗi ngày", + "ip-banned": "IP bị chặn" +} \ No newline at end of file diff --git a/public/language/vi/language.json b/public/language/vi/language.json new file mode 100644 index 0000000000..f0787f748a --- /dev/null +++ b/public/language/vi/language.json @@ -0,0 +1,5 @@ +{ + "name": "Tiếng Việt", + "code": "vi", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/vi/login.json b/public/language/vi/login.json new file mode 100644 index 0000000000..531fa94abf --- /dev/null +++ b/public/language/vi/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Tên đăng nhập / Email", + "username": "Tên đăng nhập", + "remember_me": "Ghi Nhớ Tôi?", + "forgot_password": "Quên Mật Khẩu?", + "alternative_logins": "Đăng Nhập Thay Thế", + "failed_login_attempt": "Đăng Nhập Thất Bại", + "login_successful": "Bạn đã đăng nhập thành công!", + "dont_have_account": "Chưa có tài khoản?", + "logged-out-due-to-inactivity": "Đã đăng xuất bạn khỏi Bảng Điều Khiển Quản Trị Viên do không hoạt động quá lâu", + "caps-lock-enabled": "Caps Lock được bật" +} \ No newline at end of file diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json new file mode 100644 index 0000000000..f71323813d --- /dev/null +++ b/public/language/vi/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "Trò chuyện với", + "chat.placeholder": "Nhập tin nhắn trò chuyện tại đây, kéo và thả hình ảnh, nhấn enter để gửi", + "chat.scroll-up-alert": "Bạn đang xem các tin nhắn cũ hơn, nhấp vào đây để chuyển đến tin nhắn gần đây nhất.", + "chat.send": "Gửi", + "chat.no_active": "Bạn không có cuộc trò chuyện đang hoạt động nào.", + "chat.user_typing": "%1 đang nhập...", + "chat.user_has_messaged_you": "%1 đã nhắn tin cho bạn.", + "chat.see_all": "Tất cả trò chuyện", + "chat.mark_all_read": "Đánh dấu tất cả đã đọc", + "chat.no-messages": "Vui lòng chọn người nhận để xem lịch sử tin nhắn trò chuyện", + "chat.no-users-in-room": "Không có người nào trong phòng này.", + "chat.recent-chats": "Trò Chuyện Gần Đây", + "chat.contacts": "Liên hệ", + "chat.message-history": "Lịch sử tin nhắn", + "chat.message-deleted": "Đã Xóa Tin Nhắn", + "chat.options": "Tùy chọn trò chuyện", + "chat.pop-out": "Bật cửa sổ chat", + "chat.minimize": "Thu gọn", + "chat.maximize": "Phóng to", + "chat.seven_days": "7 ngày", + "chat.thirty_days": "30 ngày", + "chat.three_months": "3 tháng", + "chat.delete_message_confirm": "Bạn có chắc muốn xoá tin nhắn này không?", + "chat.retrieving-users": "Đang truy xuất người dùng...", + "chat.manage-room": "Quản Lý Phòng Trò Chuyện", + "chat.add-user-help": "Tìm người dùng ở đây. Người dùng được chọn sẽ được thêm vào trò chuyện. Người dùng mới sẽ không thấy tin nhắn trò chuyện được đăng trước khi họ được thêm vào. Chỉ chủ phòng () được xóa người dùng khỏi phòng trò chuyện.", + "chat.confirm-chat-with-dnd-user": "Người dùng này đã đặt trạng thái của họ thành DnD (Không làm phiền). Bạn vẫn muốn trò chuyện với họ?", + "chat.rename-room": "Đổi Tên Phòng", + "chat.rename-placeholder": "Nhập tên phòng của bạn ở đây", + "chat.rename-help": "Đẳt tên phòng ở đây, tất cả những người tham gia phòng này có thể xem.", + "chat.leave": "Rời Khỏi Trò Chuyện", + "chat.leave-prompt": "Bạn có chắc chắn muốn rời khỏi cuộc trò chuyện này không?", + "chat.leave-help": "Rời khỏi cuộc trò chuyện này sẽ xóa các tin nhắn của bạn trong cuộc trò chuyện này. Nếu bạn được thêm lại trong tương lai, bạn sẽ không thấy bất kỳ lịch sử trò chuyện nào từ trước khi bạn tham gia lại.", + "chat.in-room": "Trong phòng này", + "chat.kick": "Loại ra", + "chat.show-ip": "Hiện IP", + "chat.owner": "Chủ Phòng", + "chat.system.user-join": "%1 đã vào phòng", + "chat.system.user-leave": "%1 đã rời phòng", + "chat.system.room-rename": "%2 đã đổi tên phòng: %1", + "composer.compose": "Soạn thảo", + "composer.show_preview": "Hiện Xem trước", + "composer.hide_preview": "Ẩn Xem trước", + "composer.user_said_in": "%1 đã nói trong %2:", + "composer.user_said": "%1 đã nói:", + "composer.discard": "Bạn có chắc chắn hủy bỏ bài viết này?", + "composer.submit_and_lock": "Đăng và Khoá", + "composer.toggle_dropdown": "Đóng/mở dropdown", + "composer.uploading": "Đang tải lên %1", + "composer.formatting.bold": "In đậm", + "composer.formatting.italic": "In nghiêng", + "composer.formatting.list": "Danh sách", + "composer.formatting.strikethrough": "Gạch ngang", + "composer.formatting.code": "Mã", + "composer.formatting.link": "Liên kết", + "composer.formatting.picture": "Liên Kết Ảnh", + "composer.upload-picture": "Tải ảnh lên", + "composer.upload-file": "Tải Lên Tệp", + "composer.zen_mode": "Chế Độ Zen", + "composer.select_category": "Chọn một chuyên mục", + "composer.textarea.placeholder": "Nhập nội dung bài đăng của bạn vào đây, kéo và thả hình ảnh", + "composer.schedule-for": "Lên lịch chủ đề cho", + "composer.schedule-date": "Ngày", + "composer.schedule-time": "Thời gian", + "composer.cancel-scheduling": "Hủy Lập Lịch", + "composer.set-schedule-date": "Đặt Ngày", + "bootbox.ok": "Đồng ý", + "bootbox.cancel": "Huỷ bỏ", + "bootbox.confirm": "Xác nhận", + "bootbox.submit": "Gửi", + "bootbox.send": "Gửi", + "cover.dragging_title": "Điều chỉnh vị trí ảnh cover", + "cover.dragging_message": "Kéo ảnh cover vào vị trí bạn ưng ý và nhấn \"Lưu\"", + "cover.saved": "Ảnh cover và vị trí đã được lưu", + "thumbs.modal.title": "Quản lý ảnh mô tả chủ đề", + "thumbs.modal.no-thumbs": "Không tìm thấy hình mô tả.", + "thumbs.modal.resize-note": "Ghi chú: Diễn đàn này cấu hình thay đổi kích thước hình mô tả chủ đề xuống chiều rộng tối đa là %1px", + "thumbs.modal.add": "Thêm ảnh mô tả", + "thumbs.modal.remove": "Xóa ảnh mô tả", + "thumbs.modal.confirm-remove": "Bạn có chắc chắn muốn xóa hình mô tả này không?" +} \ No newline at end of file diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json new file mode 100644 index 0000000000..89c5b89675 --- /dev/null +++ b/public/language/vi/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "Thông báo", + "no_notifs": "Bạn không có thông báo mới", + "see_all": "Tất cả thông báo", + "mark_all_read": "Đánh dấu tất cả đã đọc", + "back_to_home": "Quay lại %1", + "outgoing_link": "Liên kết ngoài", + "outgoing_link_message": "Bạn đang rời khỏi %1", + "continue_to": "Tiếp tục đến %1", + "return_to": "Quay lại %1", + "new_notification": "Bạn có một thông báo mới", + "you_have_unread_notifications": "Bạn có thông báo chưa đọc", + "all": "Tất cả", + "topics": "Chủ đề", + "replies": "Phản hồi", + "chat": "Trò Chuyện", + "group-chat": "Trò Chuyện Nhóm", + "follows": "Lượt theo dõi", + "upvote": "Ủng hộ", + "new-flags": "Cảnh báo mới", + "my-flags": "Cảnh báo dành cho tôi", + "bans": "Cấm", + "new_message_from": "Tin nhắn mới từ %1", + "upvoted_your_post_in": "%1 đã bình chọn bài của bạn trong %2.", + "upvoted_your_post_in_dual": "%1%2 đã tán thành với bài viết của bạn trong %3.", + "upvoted_your_post_in_multiple": "%1 và %2 others đã tán thành với bài viết của bạn trong %3.", + "moved_your_post": "%1 đã chuyển bài viết của bạn tới %2", + "moved_your_topic": "%1 đã chuyển %2", + "user_flagged_post_in": "%1 gắn cờ 1 bài trong %2", + "user_flagged_post_in_dual": "%1%2 đã gắn cờ một bài viết trong %3", + "user_flagged_post_in_multiple": "%1 và %2 người khác đã gắn cờ bài viết của bạn trong %3", + "user_flagged_user": "%1 đã gắn cờ một hồ sơ người dùng (%2)", + "user_flagged_user_dual": "%1%2 đã gắn cờ một hồ sơ người dùng (%3)", + "user_flagged_user_multiple": "%1 và %2 người khác đã gắn cờ một hồ sơ người dùng (%3)", + "user_posted_to": "%1 đã đăng một trả lời cho: %2", + "user_posted_to_dual": "%1%2 đã đăng trả lời cho: %3", + "user_posted_to_multiple": "%1 và %2 người khác đã đăng trả lời cho: %3", + "user_posted_topic": "%1 đã đăng một chủ đề mới: %2", + "user_edited_post": "%1 đã chỉnh sửa một bài đăng trong %2", + "user_started_following_you": "%1 đã theo dõi bạn.", + "user_started_following_you_dual": "%1%2 đã bắt đầu theo dõi bạn.", + "user_started_following_you_multiple": "%1 và %2 người khác đã bắt đầu theo dõi bạn.", + "new_register": "%1 đã gửi một yêu cầu tham gia.", + "new_register_multiple": "Có %1 đơn đăng ký đang chờ xem xét.", + "flag_assigned_to_you": "Cờ %1 đã được giao cho bạn", + "post_awaiting_review": "Bài đăng đang chờ xét duyệt", + "profile-exported": "%1 đã xuất hồ sơ, nhấn tải xuống", + "posts-exported": "%1 đã xuất bài viết, nhấn tải xuống", + "uploads-exported": "%1 đã xuất tải lên, nhấn tải xuống", + "users-csv-exported": "Đã xuất csv người dùng, nhấp để tải xuống", + "post-queue-accepted": "Bài đăng trong hàng đợi của bạn đã được chấp nhận. Nhấn vào đây để xem bài viết của bạn.", + "post-queue-rejected": "Bài đăng trong hàng đợi của bạn đã bị từ chối", + "post-queue-notify": "Bài đã xếp hàng đã nhận được thông báo:
\"%1\"", + "email-confirmed": "Đã Xác Nhận Email", + "email-confirmed-message": "Cảm ơn bạn đã xác nhận địa chỉ email của bạn. Tài khoản của bạn đã được kích hoạt đầy đủ.", + "email-confirm-error-message": "Đã có lỗi khi xác nhận địa chỉ email. Có thể đoạn mã không đúng hoặc đã hết hạn.", + "email-confirm-sent": "Đã gửi email xác nhận.", + "none": "Trống", + "notification_only": "Chỉ Thông Báo", + "email_only": "Chỉ email", + "notification_and_email": "Thông Báo & Email", + "notificationType_upvote": "Khi ai đó ủng hộ bài viết của bạn", + "notificationType_new-topic": "Khi người bạn theo dõi đăng một chủ đề", + "notificationType_new-reply": "Khi một câu trả lời mới được đăng trong một chủ đề bạn đang xem", + "notificationType_post-edit": "Khi bài viết được chỉnh sửa trong chủ đề bạn đang xem", + "notificationType_follow": "Khi ai đó bắt đầu theo dõi bạn", + "notificationType_new-chat": "Khi bạn nhận được một tin nhắn trò chuyện", + "notificationType_new-group-chat": "Khi bạn nhận được một tin nhắn trò chuyện nhóm", + "notificationType_group-invite": "Khi bạn nhận một lời mời nhóm", + "notificationType_group-leave": "Khi người dùng rời khỏi nhóm của bạn", + "notificationType_group-request-membership": "Khi ai đó yêu cầu tham gia một nhóm bạn sở hữu", + "notificationType_new-register": "Khi ai đó được thêm vào hàng đợi đăng ký", + "notificationType_post-queue": "Khi bài đăng được thêm vào lượt chờ", + "notificationType_new-post-flag": "Khi bài đăng bị gắn cờ cảnh báo", + "notificationType_new-user-flag": "Khi người dùng bị gắn cờ cảnh báo" +} \ No newline at end of file diff --git a/public/language/vi/pages.json b/public/language/vi/pages.json new file mode 100644 index 0000000000..fc1da7afaf --- /dev/null +++ b/public/language/vi/pages.json @@ -0,0 +1,65 @@ +{ + "home": "Trang chủ", + "unread": "Chủ đề chưa đọc", + "popular-day": "Chủ đề nổi bật hôm nay", + "popular-week": "Chủ đề nội bật tuần này", + "popular-month": "Chủ đề nổi bật tháng này", + "popular-alltime": "Chủ đề nổi bật mọi thời đại", + "recent": "Chủ đề gần đây", + "top-day": "Chủ đề được bình chọn nhiều nhất hôm nay", + "top-week": "Chủ đề được bình chọn nhiều nhất tuần này", + "top-month": "Chủ đề được bình chọn nhiều nhất tháng này", + "top-alltime": "Chủ Đề Được Bình Chọn Nhiều Nhất", + "moderator-tools": "Công Cụ Điều Hành", + "flagged-content": "Nội Dung Bị Gắn Cờ", + "ip-blacklist": "Danh sách đen IP", + "post-queue": "Hàng Đợi Bài Viết", + "users/online": "Thành viên trực tuyến", + "users/latest": "Thành viên mới nhất", + "users/sort-posts": "Thành viên có nhiều bài đăng nhất", + "users/sort-reputation": "Thành viên có điểm tín nhiệm cao nhất", + "users/banned": "Người dùng bị cấm", + "users/most-flags": "Người dùng bị gắn cờ nhiều", + "users/search": "Tìm Kiếm Người Dùng", + "notifications": "Thông báo", + "tags": "Thẻ", + "tag": "Các chủ đề được gắn thẻ bên dưới "%1"", + "register": "Đăng ký một tài khoản mới", + "registration-complete": "Đăng ký hoàn tất", + "login": "Đăng nhập vào tài khoản của bạn", + "reset": "Đặt lại mật khẩu tài khoản của bạn", + "categories": "Chuyên mục", + "groups": "Nhóm", + "group": "Nhóm %1", + "chats": "Trò chuyện", + "chat": "Trò chuyện với %1", + "flags": "Gắn Cờ", + "flag-details": "Gắn Cờ %1 Chi Tiết", + "account/edit": "Chỉnh sửa \"%1\"", + "account/edit/password": "Chỉnh sửa mật khẩu của \"%1\"", + "account/edit/username": "Chỉnh sửa tên đăng nhập của \"%1\"", + "account/edit/email": "Chỉnh sửa email của \"%1\"", + "account/info": "Thông tin tài khoản", + "account/following": "Thành viên %1 đang theo dõi", + "account/followers": "Thành viên đang theo dõi %1", + "account/posts": "Bài viết được đăng bởi %1", + "account/latest-posts": "Bài viết mới nhất do %1", + "account/topics": "Chủ đề được tạo bởi %1", + "account/groups": "Nhóm của %1", + "account/watched_categories": "Chuyên Mục Đã Xem Của %1", + "account/bookmarks": "Bài Đăng Được Đánh Dấu Trang Của %1", + "account/settings": "Cài Đặt Người Dùng", + "account/watched": "Chủ đề đã được %1 xem", + "account/ignored": "Các chủ đề bị bỏ qua bởi %1", + "account/upvoted": "Bài đăng được %1 ủng hộ", + "account/downvoted": "Bài viết %1 phản đối", + "account/best": "Bài viết hay nhất của %1", + "account/controversial": "Các bài đăng gây tranh cãi được thực hiện bởi %1", + "account/blocks": "Người dùng bị chặn vì %1", + "account/uploads": "Tải lên bởi %1", + "account/sessions": "Phiên Đăng Nhập", + "confirm": "Đã xác nhận email", + "maintenance.text": "%1 hiện đang được bảo trì. Vui lòng quay lại lúc khác.", + "maintenance.messageIntro": "Ngoài ra, quản trị viên đã để lại thông báo này:", + "throttled.text": "%1 hiện không khả dụng do quá tải. Vui lòng quay lại vào lúc khác." +} \ No newline at end of file diff --git a/public/language/vi/post-queue.json b/public/language/vi/post-queue.json new file mode 100644 index 0000000000..b25209cb00 --- /dev/null +++ b/public/language/vi/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "Hàng Đợi Bài Đăng", + "description": "Không có bài viết nào trong hàng đợi bài viết.
Để bật tính năng này, hãy truy cập Cài Đặt → Bài Đăng → Hàng Đợi Bài Đăng và bật Hàng Đợi Bài Đăng.", + "user": "Người dùng", + "category": "Chuyên mục", + "title": "Tiêu đề", + "content": "Nội dung", + "posted": "Đã đăng", + "reply-to": "Trả lời đến \"%1\"", + "content-editable": "Nhấp vào nội dung để chỉnh sửa", + "category-editable": "Nhấp vào chuyên mục để chỉnh sửa", + "title-editable": "Nhấp vào tiêu đề để chỉnh sửa", + "reply": "Trả lời", + "topic": "Chủ đề", + "accept": "Chấp nhận", + "reject": "Từ chối", + "remove": "Xóa", + "notify": "Thông báo", + "notify-user": "Thông Báo Người Dùng", + "confirm-reject": "Bạn có muốn từ chối bài viết này không?", + "bulk-actions": "Hành động số lượng lớn", + "accept-all": "Chấp nhận tất cả", + "accept-selected": "Chấp nhận đã chọn", + "reject-all": "Từ chối tất cả", + "reject-all-confirm": "Bạn có muốn từ chối tất cả các bài viết?", + "reject-selected": "Từ chối đã chọn", + "reject-selected-confirm": "Bạn có muốn từ chối %1 bài viết đã chọn không?", + "bulk-accept-success": "%1 bài đăng được chấp nhận", + "bulk-reject-success": "%1 bài đăng bị từ chối" +} \ No newline at end of file diff --git a/public/language/vi/recent.json b/public/language/vi/recent.json new file mode 100644 index 0000000000..49c9fb848a --- /dev/null +++ b/public/language/vi/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Gần đây", + "day": "Ngày", + "week": "Tuần", + "month": "Tháng", + "year": "Năm", + "alltime": "Mọi Lúc", + "no_recent_topics": "Không có chủ đề gần đây.", + "no_popular_topics": "Không có chủ đề nào phổ biến.", + "there-is-a-new-topic": "Có chủ đề mới", + "there-is-a-new-topic-and-a-new-post": "Có chủ đề mới và bài viết mới", + "there-is-a-new-topic-and-new-posts": "Có 1 chủ đề mới và %1 bài viết mới.", + "there-are-new-topics": "Có %1 chủ đề mới.", + "there-are-new-topics-and-a-new-post": "Có %1 chủ đề mới và 1 bài viết mới.", + "there-are-new-topics-and-new-posts": "Có %1 chủ đề mới và %2 bài viết mới.", + "there-is-a-new-post": "Có bài viết mới", + "there-are-new-posts": "Có %1 bài viết mới.", + "click-here-to-reload": "Nhấn vào đây để tải lại." +} \ No newline at end of file diff --git a/public/language/vi/register.json b/public/language/vi/register.json new file mode 100644 index 0000000000..d435c5fe67 --- /dev/null +++ b/public/language/vi/register.json @@ -0,0 +1,32 @@ +{ + "register": "Đăng ký", + "cancel_registration": "Hủy đăng ký", + "help.email": "Mặc định, email của bạn sẽ bị ẩn không công khai.", + "help.username_restrictions": "Một tên đăng nhập duy nhất giữa %1 và %2 ký tự. Người khác có thể nhắc đến bạn với @tên đăng nhập.", + "help.minimum_password_length": "Mật khẩu của bạn phải có ít nhất %1 ký tự", + "email_address": "Địa chỉ Email", + "email_address_placeholder": "Nhập địa chỉ Email", + "username": "Tên đăng nhập", + "username_placeholder": "Nhập tên đăng nhập", + "password": "Mật khẩu", + "password_placeholder": "Nhập mật khẩu", + "confirm_password": "Xác Nhận Mật Khẩu", + "confirm_password_placeholder": "Xác Nhận Mật Khẩu", + "register_now_button": "Đăng ký ngay", + "alternative_registration": "Đăng Ký Thay Thế", + "terms_of_use": "Điều khoản sử dụng", + "agree_to_terms_of_use": "Tôi đồng ý với các điều khoản sử dụng", + "terms_of_use_error": "Bạn phải đồng ý với Điều Khoản Sử Dụng", + "registration-added-to-queue": "Đăng ký của bạn đã được thêm vào hàng đợi phê duyệt. Bạn sẽ nhận được email khi quản trị viên chấp nhận yêu cầu.", + "registration-queue-average-time": "Thời gian chúng tôi phê duyệt tư cách thành viên là %1 giờ %2 phút.", + "registration-queue-auto-approve-time": "Tư cách thành viên của bạn sẽ được kích hoạt đầy đủ trong tối đa %1 giờ.", + "interstitial.intro": "Chúng tôi muốn một số thông tin bổ sung để cập nhật tài khoản của bạn…", + "interstitial.intro-new": "Chúng tôi muốn một số thông tin bổ sung trước khi chúng tôi có thể tạo tài khoản của bạn…", + "interstitial.errors-found": "Vui lòng xem lại thông tin đã nhập:", + "gdpr_agree_data": "Tôi đồng ý với việc thu thập và xử lý thông tin cá nhân của tôi trên trang web này.", + "gdpr_agree_email": "Tôi đồng ý nhận email thông báo và thông báo từ trang web này.", + "gdpr_consent_denied": "Bạn phải đồng ý với trang web này để thu thập/xử lý thông tin của bạn và gửi email cho bạn.", + "invite.error-admin-only": "Đăng ký người dùng trực tiếp đã bị tắt. Vui lòng liên hệ với quản trị viên để biết thêm chi tiết.", + "invite.error-invite-only": "Đăng ký người dùng trực tiếp đã tắt. Bạn phải được người dùng đã là thành viên mời để truy cập diễn đàn này.", + "invite.error-invalid-data": "Dữ liệu đăng ký nhận được không tương ứng với hồ sơ của chúng tôi. Vui lòng liên hệ với quản trị viên để biết thêm chi tiết" +} \ No newline at end of file diff --git a/public/language/vi/reset_password.json b/public/language/vi/reset_password.json new file mode 100644 index 0000000000..68be83b98a --- /dev/null +++ b/public/language/vi/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "Đặt Lại Mật Khẩu", + "update_password": "Cập Nhật Mật Khẩu", + "password_changed.title": "Mật Khẩu Đã Được Thay Đổi", + "password_changed.message": "

Đặt lại mật khẩu thành công, vui lòng đăng nhập lại.", + "wrong_reset_code.title": "Mã thiết lập lại không đúng", + "wrong_reset_code.message": "Mã thiết lập lại không đúng. Xin hãy thử lại, hoặc yêu cầu một mã thiết lập lại khác.", + "new_password": "Mật Khẩu Mới", + "repeat_password": "Xác Nhận Mật Khẩu", + "changing_password": "Thay Đổi Mật Khẩu", + "enter_email": "Xin hãy nhập địa chỉ email của bạn và chúng tôi sẽ gửi một email hướng dẫn cách thiết lập lại tài khoản cho bạn", + "enter_email_address": "Nhập địa chỉ Email", + "password_reset_sent": "Nếu có địa chỉ cụ thể ứng với tài khoản người dùng hiện có, một email đặt lại mật khẩu đã được gửi. Xin lưu ý chỉ có một email được gửi mỗi phút.", + "invalid_email": "Email không đúng/không tồn tại!", + "password_too_short": "Mật khẩu bạn nhập quá ngắn, vui lòng chọn một mật khẩu khác.", + "passwords_do_not_match": "Hai mật khẩu bạn nhập không khớp với nhau.", + "password_expired": "Mật khẩu của bạn đã hết hạn, vui lòng chọn một mật khẩu mới." +} \ No newline at end of file diff --git a/public/language/vi/search.json b/public/language/vi/search.json new file mode 100644 index 0000000000..de5d7dd641 --- /dev/null +++ b/public/language/vi/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "%1 kết quả khớp với \"%2\", (%3 giây)", + "no-matches": "Không tìm thấy kết quả phù hợp", + "advanced-search": "Tìm kiếm nâng cao", + "in": "Trong", + "titles": "Tiêu đề", + "titles-posts": "Tiêu Đề và Bài Viết", + "match-words": "Khớp các từ", + "all": "Tất cả", + "any": "Bất kì", + "posted-by": "Đăng bởi", + "in-categories": "Nằm trong chuyên mục", + "search-child-categories": "Tìm kiếm chuyên mục con", + "has-tags": "Có thẻ bên trong", + "reply-count": "Số lượt trả lời", + "at-least": "Tối thiểu", + "at-most": "Tối đa", + "relevance": "Mức độ liên quan", + "post-time": "Thời gian đăng bài", + "votes": "Phiếu bầu", + "newer-than": "Mới hơn", + "older-than": "Cũ hơn", + "any-date": "Bất kì ngày nào", + "yesterday": "Hôm qua", + "one-week": "Một tuần", + "two-weeks": "Hai tuần", + "one-month": "Một tháng", + "three-months": "Ba tháng", + "six-months": "Sáu tháng", + "one-year": "Một năm", + "sort-by": "Sắp xếp theo", + "last-reply-time": "Thời điểm trả lời lần cuối", + "topic-title": "Tiêu đề chủ đề", + "topic-votes": "Phiếu bầu chủ đề", + "number-of-replies": "Số lượt trả lời", + "number-of-views": "Số lượt xem", + "topic-start-date": "Ngày bắt đầu chủ đề", + "username": "Tên đăng nhập", + "category": "Chuyên mục ", + "descending": "Theo thứ tự giảm dần", + "ascending": "Theo thứ tự tăng dần", + "save-preferences": "Lưu tuỳ chọn", + "clear-preferences": "Xoá tuỳ chọn", + "search-preferences-saved": "Tìm kiếm tuỳ chọn đã lưu", + "search-preferences-cleared": "Tìm kiếm tuỳ chọn đã xoá", + "show-results-as": "Hiện thị kết quả theo", + "see-more-results": "Xem thêm kết quả (%1)", + "search-in-category": "Tìm kiếm ở \"%1\"" +} \ No newline at end of file diff --git a/public/language/vi/success.json b/public/language/vi/success.json new file mode 100644 index 0000000000..f4034365f8 --- /dev/null +++ b/public/language/vi/success.json @@ -0,0 +1,7 @@ +{ + "success": "Thành công", + "topic-post": "Bạn đã đăng bài thành công", + "post-queued": "Bài đăng của bạn được xếp hàng để phê duyệt. Bạn sẽ nhận được thông báo khi nó được chấp nhận hoặc bị từ chối.", + "authentication-successful": "Xác thực thành công", + "settings-saved": "Đã lưu thiết lập" +} \ No newline at end of file diff --git a/public/language/vi/tags.json b/public/language/vi/tags.json new file mode 100644 index 0000000000..6d98a76ec4 --- /dev/null +++ b/public/language/vi/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "Không có bài viết nào với thẻ này.", + "tags": "Thẻ", + "enter_tags_here": "Nhập thẻ ở đây, mỗi thẻ phải có từ %1 tới %2 ký tự.", + "enter_tags_here_short": "Nhập thẻ...", + "no_tags": "Chưa có thẻ nào.", + "select_tags": "Chọn Thẻ" +} \ No newline at end of file diff --git a/public/language/vi/top.json b/public/language/vi/top.json new file mode 100644 index 0000000000..18d3e3238b --- /dev/null +++ b/public/language/vi/top.json @@ -0,0 +1,4 @@ +{ + "title": "Hàng Đầu", + "no_top_topics": "Không có chủ đề hàng đầu" +} \ No newline at end of file diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json new file mode 100644 index 0000000000..c4f60fdaec --- /dev/null +++ b/public/language/vi/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "Chủ đề", + "title": "Tiêu đề", + "no_topics_found": "Không tìm thấy chủ đề nào!", + "no_posts_found": "Không tìm thấy bài gửi nào", + "post_is_deleted": "Bài gửi này đã bị xóa!", + "topic_is_deleted": "Chủ đề này đã bị xóa!", + "profile": "Hồ sơ", + "posted_by": "Được đăng bởi %1", + "posted_by_guest": "Đăng bởi khách", + "chat": "Trò Chuyện", + "notify_me": "Được thông báo khi có trả lời mới trong chủ đề này", + "quote": "Trích dẫn", + "reply": "Trả lời", + "replies_to_this_post": "%1 trả lời", + "one_reply_to_this_post": "1 Phản hồi", + "last_reply_time": "Trả lời cuối cùng", + "reply-as-topic": "Trả lời dưới dạng chủ đề", + "guest-login-reply": "Hãy đăng nhập để trả lời", + "login-to-view": "🔒 Đăng nhập để xem", + "edit": "Chỉnh sửa", + "delete": "Xóa", + "delete-event": "Xóa Sự Kiện", + "delete-event-confirm": "Bạn có chắc muốn xóa sự kiện này không?", + "purge": "Xóa hẳn", + "restore": "Khôi phục", + "move": "Chuyển đi", + "change-owner": "Thay đổi chủ sở hữu", + "fork": "Tạo bản sao", + "link": "Đường dẫn", + "share": "Chia sẻ", + "tools": "Công cụ", + "locked": "Đã Khóa", + "pinned": "Đã ghim", + "pinned-with-expiry": "Được ghim cho đến %1", + "scheduled": "Lên kế hoạch", + "moved": "Chuyển đi", + "moved-from": "Đã chuyển từ %1", + "copy-ip": "Sao chép IP", + "ban-ip": "Cấm IP", + "view-history": "Lịch sử chỉnh sửa", + "locked-by": "Bị khóa bởi", + "unlocked-by": "Được mở khóa bởi", + "pinned-by": "Được ghim bởi", + "unpinned-by": "Được bỏ ghim bởi", + "deleted-by": "Đã xóa bởi", + "restored-by": "Được khôi phục bởi", + "moved-from-by": "Đã chuyển từ %1 bởi", + "queued-by": "Bài đăng được xếp hàng chờ duyệt →", + "backlink": "Được giới thiệu bởi", + "forked-by": "Forked by", + "bookmark_instructions": "Nhấn vào đây để trở lại bài viết đã đọc cuối cùng trong chủ đề này.", + "flag-post": "Gắn cờ bài đăng này", + "flag-user": "Gắn cờ người dùng này", + "already-flagged": "Đã Được Gắn Cờ", + "view-flag-report": "Xem Báo Cáo Gắn Cờ", + "resolve-flag": "Xử Lý Cờ", + "merged_message": "Chủ đề này đã được gộp chung thành %2", + "deleted_message": "Chủ đề này đã bị xóa. Chỉ người dùng có quyền quản lý chủ đề mới được xem.", + "following_topic.message": "Bạn sẽ nhận được thông báo khi có ai đó gửi bài viết trong chủ đề này.", + "not_following_topic.message": "Bạn sẽ thấy chủ đề này trong danh sách chủ đề chưa đọc, nhưng bạn sẽ không nhận được thông báo khi ai đó đăng lên chủ đề này.", + "ignoring_topic.message": "Bạn sẽ không thấy chủ đề này trong danh sách chủ đề chưa đọc. Bạn sẽ nhận thông báo khi bạn được đề cập hoặc bài viết của bạn được ủng hộ.", + "login_to_subscribe": "Vui lòng đăng ký hoặc đăng nhập để theo dõi chủ đề này", + "markAsUnreadForAll.success": "Chủ đề đã được đánh dấu là chưa đọc toàn bộ", + "mark_unread": "Đánh dấu chưa đọc", + "mark_unread.success": "Đã đánh dấu chủ đề chưa đọc.", + "watch": "Xem", + "unwatch": "Bỏ xem", + "watch.title": "Được thông báo khi có trả lời mới trong chủ đề này", + "unwatch.title": "Ngừng xem chủ đề này", + "share_this_post": "Chia sẻ bài viết này", + "watching": "Đang xem", + "not-watching": "Không Xem", + "ignoring": "Bỏ qua", + "watching.description": "Thông báo cho tôi về trả lời mới.
Hiển thị chủ đề chưa đọc", + "not-watching.description": "Không thông báo tôi các trả lời mới.
Hiển thị mục chưa đọc nếu chuyên mục bị bỏ qua.", + "ignoring.description": "Không thông báo tôi các trả lời mới.
Không hiển thị các mục chưa đọc.", + "thread_tools.title": "Công cụ chủ đề", + "thread_tools.markAsUnreadForAll": "Đánh Dấu Chưa Đọc Tất Cả", + "thread_tools.pin": "Ghim chủ đề", + "thread_tools.unpin": "Bỏ ghim chủ đề", + "thread_tools.lock": "Khóa chủ đề", + "thread_tools.unlock": "Mở khóa chủ đề", + "thread_tools.move": "Di Chuyển Chủ Đề", + "thread_tools.move-posts": "Di Chuyển Bài Viết", + "thread_tools.move_all": "Di chuyển tất cả", + "thread_tools.change_owner": "Đổi chủ sở hữu", + "thread_tools.select_category": "Chọn chuyện mục", + "thread_tools.fork": "Tạo bản sao chủ đề", + "thread_tools.delete": "Xóa chủ đề", + "thread_tools.delete-posts": "Xoá bài viết", + "thread_tools.delete_confirm": "Bạn có muốn xóa chủ đề này?", + "thread_tools.restore": "Phục hồi chủ đề", + "thread_tools.restore_confirm": "Bạn có muốn phục hồi chủ đề này?", + "thread_tools.purge": "Xóa hẳn chủ đề", + "thread_tools.purge_confirm": "Bạn có muốn xóa hẳn chủ đề này?", + "thread_tools.merge_topics": "Gộp chủ đề", + "thread_tools.merge": "Gộp", + "topic_move_success": "Chủ đề này sẽ sớm được chuyển đến \"%1\". Nhấn vào đây để hoàn tác.", + "topic_move_multiple_success": "Các chủ đề này sẽ sớm được chuyển đến \"%1\". Nhấn vào đây để hoàn tác.", + "topic_move_all_success": "Tất cả các chủ đề sẽ sớm được chuyển đến \"%1\". Nhấn vào đây để hoàn tác.", + "topic_move_undone": "Đã hoàn tác di chuyển chủ đề", + "topic_move_posts_success": "Bài viết sẽ sớm được di chuyển. Nhấn vào đây để hoàn tác.", + "topic_move_posts_undone": "Đã hoàn tác di chuyển bài viết", + "post_delete_confirm": "Bạn có chắc là muốn xóa bài gửi này không?", + "post_restore_confirm": "Bạn có chắc muốn khôi phục bài đăng này không?", + "post_purge_confirm": "Bạn có chắc muốn xóa hẳn bài này?", + "pin-modal-expiry": "Ngày hết hạn", + "pin-modal-help": "Bạn có thể đặt ngày hết hạn chủ đề được ghim tại đây. Ngoài ra, bạn có thể để trống để giữ chủ đề được ghim cho đến khi chủ đề được bỏ ghim theo cách thủ công.", + "load_categories": "Đang Tải Chuyên Mục", + "confirm_move": "Di chuyển", + "confirm_fork": "Tạo bảo sao", + "bookmark": "Đánh dấu trang", + "bookmarks": "Đánh dấu trang", + "bookmarks.has_no_bookmarks": "Bạn chưa đánh dấu trang bài viết nào cả.", + "copy-permalink": "Sao Chép Liên Kết Tĩnh", + "loading_more_posts": "Tải thêm các bài gửi khác", + "move_topic": "Di Chuyển Chủ Đề", + "move_topics": "Di Chuyển Chủ Đề", + "move_post": "Di chuyển bài đăng", + "post_moved": "Đã chuyển bài gửi!", + "fork_topic": "Tạo bản sao chủ đề", + "enter-new-topic-title": "Nhập tiêu đề chủ đề mới", + "fork_topic_instruction": "Nhấp vào bài viết bạn muốn tách", + "fork_no_pids": "Chưa chọn bài gửi nào!", + "no-posts-selected": "Không bài viết nào được chọn!", + "x-posts-selected": "%1 bài viết được chọn", + "x-posts-will-be-moved-to-y": "%1 bài viết sẽ được chuyển đến \"%2\"", + "fork_pid_count": "%1 bài viết(s) đã được gửi", + "fork_success": "Tạo bản sao thành công! Nhấn vào đây để chuyển tới chủ đề vừa tạo.", + "delete_posts_instruction": "Chọn các bài viết bạn muốn xoá/loại bỏ", + "merge_topics_instruction": "Nhấn vào các chủ đề bạn muốn gộp hoặc tìm kiếm chúng", + "merge-topic-list-title": "Danh sách các chủ đề sẽ được gộp", + "merge-options": "Tùy chọn gộp", + "merge-select-main-topic": "Chọn chủ đề chính", + "merge-new-title-for-topic": "Tiêu đề mới cho chủ đề", + "topic-id": "ID Chủ Đề", + "move_posts_instruction": "Chọn bài đăng bạn muốn di chuyển, sau đó nhập ID chủ đề hoặc đi đến chủ đề mong muốn", + "change_owner_instruction": "Bấm vào bài viết bạn muốn chỉ định cho người dùng khác", + "composer.title_placeholder": "Nhập tiêu đề chủ đề của bạn tại đây...", + "composer.handle_placeholder": "Nhập tên/xử lý của bạn ở đây", + "composer.discard": "Huỷ bỏ", + "composer.submit": "Gửi", + "composer.additional-options": "Tùy chọn bổ sung", + "composer.schedule": "Lên lịch", + "composer.replying_to": "Đang trả lời %1", + "composer.new_topic": "Chủ đề mới", + "composer.editing": "Sửa", + "composer.uploading": "đang tải lên...", + "composer.thumb_url_label": "Dán URL hình mô tả chủ đề", + "composer.thumb_title": "Thêm ảnh mô tả cho chủ đề này", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Hoặc tải lên một tệp", + "composer.thumb_remove": "Xóa các trường", + "composer.drag_and_drop_images": "Kéo và thả hình ảnh tại đây", + "more_users_and_guests": "%1 người dùng và %2 khách nữa", + "more_users": "%1 người dùng nữa", + "more_guests": "%1 khách nữa", + "users_and_others": "%1 và%2 khác", + "sort_by": "Sắp xếp theo", + "oldest_to_newest": "Cũ đến mới", + "newest_to_oldest": "Mới đến cũ", + "most_votes": "Nhiều Bình Chọn", + "most_posts": "Nhiều Bài Đăng", + "most_views": "Xem Nhiều", + "stale.title": "Tạo chủ đề mới thay thế?", + "stale.warning": "Chủ đề bạn đang trả lời đã khá cũ. Bạn có muốn tạo chủ đề mới, và liên kết với chủ đề hiện tại trong bài viết trả lời của bạn?", + "stale.create": "Tạo chủ đề mới", + "stale.reply_anyway": "Trả lời chủ đề này", + "link_back": "Re: [%1](%2)", + "diffs.title": "Lịch sử chỉnh sửa bài viết", + "diffs.description": "Bài viết này có %1 sửa đổi. Nhấp vào một trong các bản sửa đổi bên dưới để xem nội dung bài đăng tại thời điểm đó.", + "diffs.no-revisions-description": "Bài viết này có %1 sửa đổi", + "diffs.current-revision": "bản sửa đổi hiện tại", + "diffs.original-revision": "bản sửa đổi gốc", + "diffs.restore": "Khôi phục bản sửa đổi này", + "diffs.restore-description": "Một bản sửa đổi mới sẽ được thêm vào lịch sử chỉnh sửa bài đăng này sau khi khôi phục.", + "diffs.post-restored": "Đã khôi phục thành công bài đăng về bản sửa đổi trước đó", + "diffs.delete": "Xóa bản sửa đổi này", + "diffs.deleted": "Bản sửa đổi đã bị xóa", + "timeago_later": "%1 sau", + "timeago_earlier": "%1 trước đó", + "first-post": "Bài viết đầu tiên", + "last-post": "Bài viết cuối cùng", + "go-to-my-next-post": "Đi tới bài kế tiếp của tôi", + "no-more-next-post": "Bạn không có bài viết nào khác trong chủ đề này", + "post-quick-reply": "Đăng trả lời nhanh" +} \ No newline at end of file diff --git a/public/language/vi/unread.json b/public/language/vi/unread.json new file mode 100644 index 0000000000..7916bae9c4 --- /dev/null +++ b/public/language/vi/unread.json @@ -0,0 +1,15 @@ +{ + "title": "Chưa đọc", + "no_unread_topics": "Không có chủ đề chưa đọc.", + "load_more": "Tải Thêm", + "mark_as_read": "Đánh dấu đã đọc", + "selected": "Đã chọn", + "all": "Tất cả", + "all_categories": "Tất cả chuyên mục", + "topics_marked_as_read.success": "Chủ đề được đánh dấu đã đọc", + "all-topics": "Toàn bộ chủ đề", + "new-topics": "Chủ đề mới", + "watched-topics": "Chủ đề đã xem", + "unreplied-topics": "Chủ Đề Chưa Có Trả Lời ", + "multiple-categories-selected": "Chọn nhiều cùng lúc" +} \ No newline at end of file diff --git a/public/language/vi/uploads.json b/public/language/vi/uploads.json new file mode 100644 index 0000000000..f32f535b40 --- /dev/null +++ b/public/language/vi/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "Đang tải tệp lên...", + "select-file-to-upload": "Chọn một tệp để tải lên!", + "upload-success": "Tải tệp lên thành công!", + "maximum-file-size": "Tối đa %1 kb", + "no-uploads-found": "Không có tải lên được tìm thấy", + "public-uploads-info": "Các file tải lên được xuất bản, mọi người đều có thể xem được.", + "private-uploads-info": "Các file tải lên được để ở chế độ bí mật, chỉ những người dùng đăng nhập mới có thể xem." +} \ No newline at end of file diff --git a/public/language/vi/user.json b/public/language/vi/user.json new file mode 100644 index 0000000000..c7f22833e4 --- /dev/null +++ b/public/language/vi/user.json @@ -0,0 +1,199 @@ +{ + "banned": "Bị cấm", + "muted": "Đã Im Lặng", + "offline": "Ngoại tuyến", + "deleted": "Đã xoá", + "username": "Tên Đăng Nhập", + "joindate": "Ngày Tham Gia", + "postcount": "Số bài viết", + "email": "Thư điện tử", + "confirm_email": "Xác nhận email", + "account_info": "Thông tin tài khoản", + "admin_actions_label": "Hoạt Động Quản Trị", + "ban_account": "Cấm Tài Khoản", + "ban_account_confirm": "Bạn có chắc muốn cấm người dùng này?", + "unban_account": "Bỏ Cấm Tài Khoản", + "mute_account": "Im Lặng Tài Khoản", + "unmute_account": "Bỏ Im Lặng Tài Khoản", + "delete_account": "Xóa Tài Khoản", + "delete_account_as_admin": "Xóa Tài Khoản", + "delete_content": "Xóa Nội Dung Tài Khoản", + "delete_all": "XóaTài KhoảnNội Dung", + "delete_account_confirm": "Bạn có chắc chắn muốn ẩn danh bài đăng và xóa tài khoản của mình không?
Không thể hoàn tác hành động này và bạn sẽ không thể khôi phục dữ liệu của mình

Nhập mật khẩu của bạn để xác nhận rằng bạn muốn hủy tài khoản này.", + "delete_this_account_confirm": "Bạn có chắc muốn xóa tài khoản này trong khi vẫn để lại nội dung của nó?
Hành động này không thể hoàn tác, các bài viết sẽ được ẩn danh và bạn không thể khôi phục các liên kết bài viết với tài khoản đã xóa

", + "delete_account_content_confirm": "Bạn có chắc chắn muốn xóa nội dung của tài khoản này không (bài viết/chủ đề/tải lên)?
Không thể hoàn tác hành động này và bạn sẽ không thể khôi phục bất kỳ dữ liệu nào

", + "delete_all_confirm": "Bạn có chắc muốn xóa tài khoản này và tất cả nội dung của nó (bài viết/chủ đề/tải lên)?
Không thể hoàn tác hành động này và bạn sẽ không thể khôi phục bất kỳ dữ liệu nào

", + "account-deleted": " Tài khoản đã bị xóa", + "account-content-deleted": "Nội dung tài khoản đã bị xóa", + "fullname": "Tên Đầy Đủ", + "website": "Trang Web", + "location": "Nơi ở", + "age": "Tuổi", + "joined": "Đã tham gia", + "lastonline": "Trực tuyến lần cuối", + "profile": "Hồ sơ", + "profile_views": "Xem Hồ Sơ", + "reputation": "Uy tín", + "bookmarks": "Đánh dấu trang", + "watched_categories": "Danh mục đã xem", + "change_all": "Thay Đổi Tất Cả", + "watched": "Đã xem", + "ignored": "Đã Bỏ Qua", + "default-category-watch-state": "Trạng thái xem chuyên mục mặc định", + "followers": "Người theo dõi", + "following": "Đang theo dõi", + "blocks": "Khóa", + "block_toggle": "Chuyển đổi khối", + "block_user": "Khóa Người Dùng", + "unblock_user": "Mở Khóa Người Dùng", + "aboutme": "Giới thiệu bản thân", + "signature": "Chữ ký", + "birthday": "Ngày sinh ", + "chat": "Trò Chuyện", + "chat_with": "Tiếp tục trò chuyện với %1", + "new_chat_with": "Bắt đầu cuộc trò chuyện mới với %1", + "flag-profile": "Gắn Cờ Hồ Sơ", + "follow": "Theo dõi", + "unfollow": "Hủy theo dõi", + "more": "Thêm nữa", + "profile_update_success": "Đã cập nhật hồ sơ thành công!", + "change_picture": "Đổi Hình Ảnh", + "change_username": "Đổi tên đăng nhập", + "change_email": "Đổi email", + "email_same_as_password": "Vui lòng nhập mật khẩu hiện tại của bạn để tiếp tục – bạn đã nhập lại email mới", + "edit": "Chỉnh sửa", + "edit-profile": "Sửa Hồ Sơ", + "default_picture": "Biểu tượng mặc định", + "uploaded_picture": "Ảnh đã tải lên", + "upload_new_picture": "Tải lên ảnh mới", + "upload_new_picture_from_url": "Tải Lên Ảnh Mới Từ URL", + "current_password": "Mật khẩu hiện tại", + "change_password": "Đổi Mật Khẩu", + "change_password_error": "Mật khẩu không hợp lệ!", + "change_password_error_wrong_current": "Mật khẩu hiện tại của bạn không đúng", + "change_password_error_match": "Mật khẩu phải trùng khớp!", + "change_password_error_privileges": "Bạn không có quyền thay đổi mật khẩu này", + "change_password_success": "Đã cập nhật mật khẩu của bạn!", + "confirm_password": "Xác Nhận Mật Khẩu", + "password": "Mật khẩu", + "username_taken_workaround": "Tên truy cập này đã tồn tại, vì vậy chúng tôi đã sửa đổi nó một chút. Tên truy cập của bạn giờ là %1", + "password_same_as_username": "Mật khẩu của bạn trùng với tên đăng nhập, vui lòng chọn một mật khẩu khác.", + "password_same_as_email": "Mật khẩu của bạn trùng với email của bạn, hãy chọn mật khẩu khác.", + "weak_password": "Mật khẩu yếu", + "upload_picture": "Tải lên hình ảnh", + "upload_a_picture": "Tải lên một hình ảnh", + "remove_uploaded_picture": "Xoá ảnh đã tải lên", + "upload_cover_picture": "Tải ảnh bìa lên", + "remove_cover_picture_confirm": "Bạn có thật sự muốn xóa hình ảnh này?", + "crop_picture": "Cắt ảnh", + "upload_cropped_picture": "Cắt và tải lên", + "avatar-background-colour": "Màu nền hình đại diện", + "settings": "Cài đặt", + "show_email": "Hiện Email của tôi", + "show_fullname": "Hiển Thị Tên Đầy Đủ Của Tôi", + "restrict_chats": "Chỉ cho phép tin nhắn trò chuyện từ những người dùng tôi theo dõi", + "digest_label": "Đăng Ký Thông báo", + "digest_description": "Đăng ký nhận các cập nhật qua email cho diễn đàn này (thông báo và chủ đề mới) theo lịch trình đã định", + "digest_off": "Tắt", + "digest_daily": "Hàng ngày", + "digest_weekly": "Hàng tuần", + "digest_biweekly": "Hai tuần một lần", + "digest_monthly": "Hàng tháng", + "has_no_follower": "Người dùng này không có ai theo dõi :(", + "follows_no_one": "Người dùng này không theo dõi ai :(", + "has_no_posts": "Thành viên này chưa đăng bài viết nào cả.", + "has_no_best_posts": "Người dùng này chưa có bất kỳ bài đăng nào được ủng hộ.", + "has_no_topics": "Thành viên này chưa đăng chủ đề nào cả.", + "has_no_watched_topics": "Người dùng này chưa xem bất kỳ chủ đề nào.", + "has_no_ignored_topics": "Người dùng này chưa bỏ qua bất cứ chủ đề nào.", + "has_no_upvoted_posts": "Người dùng này chưa ủng hộ bất kỳ bài đăng nào.", + "has_no_downvoted_posts": "Thành viên này chưa phản đối bài viết nào cả.", + "has_no_controversial_posts": "Người dùng này chưa có bài viết nào bị phản đối.", + "has_no_blocks": "Bạn không khóa người dùng nào.", + "email_hidden": "Ẩn Email", + "hidden": "Đã ẩn", + "paginate_description": "Phân trang chủ đề và bài đăng thay vì sử dụng cuộn vô hạn", + "topics_per_page": "Số Chủ Đề Mỗi Trang", + "posts_per_page": "Số Bài Viết Mỗi Trang", + "max_items_per_page": "Tối đa %1", + "acp_language": "Ngôn ngữ trang quản trị", + "notifications": "Thông báo", + "upvote-notif-freq": "Tần Suất Thông Báo Ủng Hộ", + "upvote-notif-freq.all": "Tất Cả Số Ủng Hộ", + "upvote-notif-freq.first": "Đầu Tiên Mỗi Bài Đăng", + "upvote-notif-freq.everyTen": "Mỗi 10 lượt thích", + "upvote-notif-freq.threshold": "Trên 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.logarithmic": "Cứ mỗi 10, 100, 1000...", + "upvote-notif-freq.disabled": "Đã tắt", + "browsing": "Đang xem cài đặt", + "open_links_in_new_tab": "Mở liên kết trong tab mới.", + "enable_topic_searching": "Bật tìm kiếm trong chủ đề", + "topic_search_help": "Nếu bật, tìm kiếm trong chủ đề sẽ thay thế tìm kiếm của trình duyệt và cho phép bạn tìm kiếm trong toàn bộ chủ đề, thay vì chỉ tìm kiếm nội dung đang hiện thị trên màn hình", + "update_url_with_post_index": "Cập nhật url với chỉ mục bài viết trong khi duyệt các chủ đề", + "scroll_to_my_post": "Sau khi đăng một trả lời thì hiển thị bài viết mới", + "follow_topics_you_reply_to": "Xem các chủ đề mà bạn trả lời", + "follow_topics_you_create": "Xem chủ đề bạn tạo", + "grouptitle": "Tiêu đề nhóm", + "group-order-help": "Chọn một nhóm và sử dụng các phím mũi tên để sắp xếp các tiêu đề", + "no-group-title": "Không có tiêu đề nhóm", + "select-skin": "Chọn một giao diện", + "select-homepage": "Chọn Trang chủ", + "homepage": "Trang chủ", + "homepage_description": "Chọn một trang dùng cho trang chủ diễn đàn hoặc chọn \"Không\" để dùng trang chủ mặc định.", + "custom_route": "Đường dẫn trang chủ tuỳ chọn", + "custom_route_help": "Nhập tên đường dẫn ở đây, không có dấu gạch chéo trước (VD: \"gan-day\" hoặc \"chuyen-muc/2/thao-luan-chung\")", + "sso.title": "Đăng nhập một lần", + "sso.associated": "Đã liên kết với", + "sso.not-associated": "Nhấn vào đây để liên kết với", + "sso.dissociate": "Tách khỏi", + "sso.dissociate-confirm-title": "Xác nhận việc tách khỏi", + "sso.dissociate-confirm": "Bạn có chắc chắn muốn tách tài khoản của mình khỏi %1?", + "info.latest-flags": "Gắn cờ mới nhất", + "info.no-flags": "Không Tìm Thấy Bài Bị Gắn Cờ", + "info.ban-history": "Lịch Sử Cấm Gần Đây", + "info.no-ban-history": "Người dùng này chưa bao giờ bị cấm", + "info.banned-until": "Bị cấm cho đến %1", + "info.banned-expiry": "Hết hạn", + "info.banned-permanently": "Bị cấm vĩnh viễn", + "info.banned-reason-label": "Lý do", + "info.banned-no-reason": "Không có lí do.", + "info.mute-history": "Lịch Sử Tắt Tiếng Gần Đây", + "info.no-mute-history": "Người dùng này chưa bao giờ bị tắt tiếng", + "info.muted-until": "Đã tắt tiếng cho đến %1", + "info.muted-expiry": "Hết hạn", + "info.muted-no-reason": "Không có lý do nào được đưa ra.", + "info.username-history": "Lịch sử tên người d", + "info.email-history": "Lịch sử email", + "info.moderation-note": "Ghi chú quản lí", + "info.moderation-note.success": "Đã lưu ghi chú quản lý", + "info.moderation-note.add": "Thêm ghi chú", + "sessions.description": "Trang này cho phép bạn xem bất kỳ phiên hoạt động nào trên diễn đàn này và thu hồi chúng nếu cần thiết. Bạn có thể thu hồi phiên của riêng mình bằng cách đăng xuất khỏi tài khoản của bạn.", + "consent.title": "Quyền của bạn & Bằng lòng", + "consent.lead": "Diễn đàn cộng đồng này thu thập và xử lý thông tin cá nhân của bạn.", + "consent.intro": "Chúng tôi dùng thông tin này cẩn thận để cá nhân hóa trải nghiệm của bạn, cũng như để liên kết các bài đăng bạn thực hiện với tài khoản người dùng của bạn. Bạn đã nhập tên người dùng và địa chỉ email khi đăng ký và cũng có thể hoàn tất hồ sơ bằng cách cung cấp thêm thông tin.

Chúng tôi giữ thông tin này suốt vòng đời tài khoản của bạn và bạn có thể rút lại sự đồng ý bất cứ lúc nào bằng cách xóa tài khoản. Bạn có thể yêu cầu một bản sao đóng góp của bạn cho trang web này, ở trang Quyền & Đồng ý

Nếu bạn có câu hỏi hoặc thắc mắc, vui lòng liên hệ với nhóm quản trị diễn đàn.", + "consent.email_intro": "Đôi khi, chúng tôi gửi email đến email bạn đã đăng ký để cung cấp thông tin cập nhật và / hoặc thông báo hoạt động mới phù hợp với bạn. Bạn có thể chỉnh tần suất thông báo cộng đồng (bao gồm vô hiệu hóa hoàn toàn), cũng như chọn loại thông báo sẽ nhận qua email, ở trang cài đặt người dùng của bạn.", + "consent.digest_frequency": "Trừ khi thay đổi rõ ràng trong cài đặt người dùng của bạn, cộng đồng này cung cấp thông báo email mỗi %1.", + "consent.digest_off": "Trừ khi thay đổi trong cài đặt người dùng của bạn, cộng đồng này sẽ không gửi thông báo qua email thông báo", + "consent.received": "Bạn đã đồng ý cho trang web này để thu thập và xử lý thông tin của bạn. Không có hành động bổ sung được yêu cầu.", + "consent.not_received": "Bạn đã không đồng ý cung cấp cho thu thập và xử lý dữ liệu. Bất cứ lúc nào quản trị trang web này'có thể chọn xóa tài khoản của bạn để tuân thủ Quy định bảo vệ dữ liệu chung.", + "consent.give": "Cho phép", + "consent.right_of_access": "Bạn có quyền truy cập", + "consent.right_of_access_description": "Bạn có quyền truy cập bất kỳ dữ liệu trang web này thu thập. Bạn có thể lấy một bản sao của dữ liệu này bằng cách nhấp vào nút thích hợp bên dưới.", + "consent.right_to_rectification": "Bạn có quyền chỉnh lý", + "consent.right_to_rectification_description": "Bạn có quyền thay đổi hoặc cập nhật bất kỳ dữ liệu không chính xác nào được cung cấp cho chúng tôi. Hồ sơ của bạn có thể được cập nhật bằng cách chỉnh sửa hồ sơ của bạn và nội dung bài đăng luôn có thể được chỉnh sửa. Nếu không đúng như vậy, hãy liên hệ nhóm quản trị của trang này.", + "consent.right_to_erasure": "Bạn có quyền xóa", + "consent.right_to_erasure_description": "Bất cứ lúc nào, bạn có thể thu hồi sự đồng ý của bạn về thu thập và / hoặc xử lý dữ liệu bằng cách xóa tài khoản của bạn. Bạn có thể xóa được hồ sơ cá nhân, mặc dù nội dung bài đăng của bạn vẫn còn. Nếu bạn muốn xóa cả tài khoản nội dung, hãy liên hệ với nhóm quản trị trang web này.", + "consent.right_to_data_portability": "Bạn có quyền di chuyển dữ liệu", + "consent.right_to_data_portability_description": "Bạn có thể yêu cầu chúng tôi xuất ra một bản máy có thể đọc được về bất kỳ dữ liệu thu thập về bạn và tài khoản của bạn. Bạn có thể làm như vậy bằng cách nhấp vào nút dành riêng bên dưới.", + "consent.export_profile": "Xuất Tệp Hồ Sơ (.json)", + "consent.export-profile-success": "Đang xuất hồ sơ, bạn sẽ nhận được thông báo khi hoàn tất.", + "consent.export_uploads": "Xuất nội dung đã tải lên (.zip)", + "consent.export-uploads-success": "Đang xuất tải lên, bạn sẽ nhận được thông báo khi hoàn tất.", + "consent.export_posts": "Xuất bài viết (.csv)", + "consent.export-posts-success": "Đang xuất bài viết, bạn sẽ nhận được thông báo khi hoàn tất.", + "emailUpdate.intro": "Vui lòng nhập địa chỉ email của bạn dưới đây. Diễn đàn này sử dụng địa chỉ email của bạn để nhận thông báo và thông báo theo lịch trình, cũng như để khôi phục tài khoản trong trường hợp mất mật khẩu.", + "emailUpdate.optional": "Mục này không bắt buộc. Bạn không có nghĩa vụ cung cấp địa chỉ email của mình, nhưng nếu không có email được xác thực, bạn sẽ không thể khôi phục tài khoản hoặc đăng nhập bằng email của mình.", + "emailUpdate.required": "Trường này là bắt buộc.", + "emailUpdate.change-instructions": "Một email xác nhận sẽ được gửi đến địa chỉ email đã nhập với một liên kết duy nhất. Việc truy cập vào liên kết đó sẽ xác nhận quyền sở hữu của bạn đối với địa chỉ email và nó sẽ có hiệu lực trên tài khoản của bạn. Bất cứ lúc nào, bạn có thể cập nhật email của mình trong hồ sơ từ trong trang tài khoản của bạn.", + "emailUpdate.password-challenge": "Nhập mật khẩu của bạn để xác minh quyền sở hữu tài khoản." +} \ No newline at end of file diff --git a/public/language/vi/users.json b/public/language/vi/users.json new file mode 100644 index 0000000000..b5caadd441 --- /dev/null +++ b/public/language/vi/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "Thành viên mới nhất", + "top_posters": "Thành viên đăng bài nhiều nhất", + "most_reputation": "Nhiều Uy Tín", + "most_flags": "Gắn cờ nhiều", + "search": "Tìm kiếm", + "enter_username": "Nhập tên đăng nhập để tìm", + "search-user-for-chat": "Tìm kiếm người dùng để bắt đầu trò chuyện", + "load_more": "Tải thêm", + "users-found-search-took": "Đã tìm thấy %1 thành viên! Tìm kiếm mất %2 giây.", + "filter-by": "Lọc Bởi", + "online-only": "Chỉ trực tuyến", + "invite": "Mời", + "prompt-email": "Thư điện tử:", + "groups-to-join": "Nhóm được tham gia khi lời mời được chấp nhận:", + "invitation-email-sent": "Email mời đã được gửi tới %1", + "user_list": "Danh sách thành viên", + "recent_topics": "Chủ Đề Gần Đây", + "popular_topics": "Chủ để nổi bật", + "unread_topics": "Chủ đề chưa đọc", + "categories": "Chuyên mục", + "tags": "Thẻ", + "no-users-found": "Không tìm thấy thành viên nào!" +} \ No newline at end of file diff --git a/public/language/zh-CN/_DO_NOT_EDIT_FILES_HERE.md b/public/language/zh-CN/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/zh-CN/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/zh-CN/admin/admin.json b/public/language/zh-CN/admin/admin.json new file mode 100644 index 0000000000..57b7a1fc3a --- /dev/null +++ b/public/language/zh-CN/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "您确定想要重新部署并重启 NodeBB 吗?", + "alert.confirm-restart": "您确定要重启 NodeBB 吗?", + + "acp-title": "%1 | NodeBB 管理员控制面板", + "settings-header-contents": "内容", + "changes-saved": "更改已保存", + "changes-saved-message": "您对 NodeBB 配置的更改已保存。", + "changes-not-saved": "更改未保存", + "changes-not-saved-message": "NodeBB 在保存您的更改时遇到了问题。 (%1)" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/advanced/cache.json b/public/language/zh-CN/admin/advanced/cache.json new file mode 100644 index 0000000000..e829207158 --- /dev/null +++ b/public/language/zh-CN/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "帖子缓存", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% 容量", + "post-cache-size": "帖子缓存大小", + "items-in-cache": "缓存中的条目数量" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/advanced/database.json b/public/language/zh-CN/admin/advanced/database.json new file mode 100644 index 0000000000..96c440c8e0 --- /dev/null +++ b/public/language/zh-CN/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "运行秒数", + "uptime-days": "运行天数", + + "mongo": "Mongo", + "mongo.version": "MongoDB 版本", + "mongo.storage-engine": "存储引擎", + "mongo.collections": "集合", + "mongo.objects": "对象", + "mongo.avg-object-size": "平均对象大小", + "mongo.data-size": "数据大小", + "mongo.storage-size": "存储大小", + "mongo.index-size": "索引大小", + "mongo.file-size": "文件大小", + "mongo.resident-memory": "驻留内存", + "mongo.virtual-memory": "虚拟内存", + "mongo.mapped-memory": "已映射内存", + "mongo.bytes-in": "字节输入", + "mongo.bytes-out": "字节输出", + "mongo.num-requests": "请求数量", + "mongo.raw-info": "MongoDB 原始信息", + "mongo.unauthorized": "NodeBB无法从MongoDB数据库请求相应的统计信息。请确保NodeBB使用的用户含有"admin"数据库的"clusterMonitor"角色。", + + "redis": "Redis", + "redis.version": "Redis 版本", + "redis.keys": "键", + "redis.expires": "有效期", + "redis.avg-ttl": "平均生存时间(TTL通常表示包在被丢弃前最多能经过的路由器个数,由发送主机设置)", + "redis.connected-clients": "已连接客户端", + "redis.connected-slaves": "已连接从", + "redis.blocked-clients": "受阻的客户端", + "redis.used-memory": "已使用内存", + "redis.memory-frag-ratio": "内存碎片比率", + "redis.total-connections-recieved": "已接收的连接总数", + "redis.total-commands-processed": "已执行命令总数", + "redis.iops": "每秒实时操作数", + "redis.iinput": "每秒实时输入", + "redis.ioutput": "每秒实时输出", + "redis.total-input": "总输入", + "redis.total-output": "总输出", + + "redis.keyspace-hits": "Keyspace 命中", + "redis.keyspace-misses": "Keyspace 未命中", + "redis.raw-info": "Redis 原始信息", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL 版本", + "postgres.raw-info": "Postgres 原始信息" +} diff --git a/public/language/zh-CN/admin/advanced/errors.json b/public/language/zh-CN/admin/advanced/errors.json new file mode 100644 index 0000000000..6fd8b532b6 --- /dev/null +++ b/public/language/zh-CN/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "数量 %1", + "error-events-per-day": "%1 事件/天", + "error.404": "404 页面不存在", + "error.503": "503 服务不可用", + "manage-error-log": "管理错误日志", + "export-error-log": "导出错误日志 (.csv)", + "clear-error-log": "清空错误日志", + "route": "路由", + "count": "计数", + "no-routes-not-found": "恭喜!没有404错误!", + "clear404-confirm": "确认清除404错误日志?", + "clear404-success": "“404 页面不存在” 错误已被清空" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/advanced/events.json b/public/language/zh-CN/admin/advanced/events.json new file mode 100644 index 0000000000..0ca510437f --- /dev/null +++ b/public/language/zh-CN/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "事件", + "no-events": "暂无事件", + "control-panel": "事件控制面板", + "delete-events": "删除事件", + "confirm-delete-all-events": "您确定要删除所有记录的事件吗?", + "filters": "过滤器", + "filters-apply": "应用过滤器", + "filter-type": "事件类型", + "filter-start": "开始时间", + "filter-end": "结束时间", + "filter-perPage": "每页" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/advanced/logs.json b/public/language/zh-CN/admin/advanced/logs.json new file mode 100644 index 0000000000..a661c9a286 --- /dev/null +++ b/public/language/zh-CN/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "日志", + "control-panel": "日志控制面板", + "reload": "重载日志", + "clear": "清空日志", + "clear-success": "日志已清空!" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/appearance/customise.json b/public/language/zh-CN/admin/appearance/customise.json new file mode 100644 index 0000000000..d4fbc962e8 --- /dev/null +++ b/public/language/zh-CN/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "自定义 CSS/LESS", + "custom-css.description": "在这里输入您想应用于所有其他风格的 CSS/LESS,", + "custom-css.enable": "启用自定义 CSS/LESS", + + "custom-js": "自定义 Javascript", + "custom-js.description": "在这里输入您想在页面加载完成后执行的 Javascript 代码。", + "custom-js.enable": "启用自定义 Javascript", + + "custom-header": "自定义 Header", + "custom-header.description": "在这里输入自定义的 HTML 代码 (如 Meta Tags 等),这些代码会被添加到论坛的 <head>部分。 您可以在这里使用 Script 标签,但我们更鼓励您将您的 JavaScript 写到 自定义 Javascript 中", + "custom-header.enable": "启用自定义 Header", + + "custom-css.livereload": "启用实时重载", + "custom-css.livereload.description": "启用此功能可以在您点击保存时强制您帐户下的每个设备上的所有会话进行刷新" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/appearance/skins.json b/public/language/zh-CN/admin/appearance/skins.json new file mode 100644 index 0000000000..1a075fe9f9 --- /dev/null +++ b/public/language/zh-CN/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "正在加载皮肤...", + "homepage": "主页", + "select-skin": "选择皮肤", + "current-skin": "当前皮肤", + "skin-updated": "皮肤已更新", + "applied-success": "%1 皮肤已成功应用", + "revert-success": "皮肤已恢复到基础颜色" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/appearance/themes.json b/public/language/zh-CN/admin/appearance/themes.json new file mode 100644 index 0000000000..ae8b579b9a --- /dev/null +++ b/public/language/zh-CN/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "正在检查已安装的主题...", + "homepage": "首页", + "select-theme": "选择主题", + "current-theme": "当前主题", + "no-themes": "未发现已安装的主题", + "revert-confirm": "确认恢复到 NodeBB 默认主题?", + "theme-changed": "主题已更改", + "revert-success": "已成功恢复到 NodeBB 默认主题。", + "restart-to-activate": "请部署并重启您的 NodeBB 以完全激活主题。" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/dashboard.json b/public/language/zh-CN/admin/dashboard.json new file mode 100644 index 0000000000..e637d4bd76 --- /dev/null +++ b/public/language/zh-CN/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "论坛流量", + "page-views": "页面浏览量", + "unique-visitors": "单一访客", + "logins": "登录", + "new-users": "新用户", + "posts": "发帖", + "topics": "主题", + "page-views-seven": "最近7天", + "page-views-thirty": "最近30天", + "page-views-last-day": "最近24小时", + "page-views-custom": "自定义日期范围", + "page-views-custom-start": "范围开始", + "page-views-custom-end": "范围结束", + "page-views-custom-help": "输入您要查看的网页浏览日期范围。 如果没有日期选择器可用,则接受的格式是 YYYY-MM-DD", + "page-views-custom-error": "请输入 YYYY-MM-DD格式的有效日期范围 ", + + "stats.yesterday": "昨天", + "stats.today": "今天", + "stats.last-week": "上一周", + "stats.this-week": "本周", + "stats.last-month": "上一月", + "stats.this-month": "本月", + "stats.all": "总计", + + "updates": "更新", + "running-version": "您正在运行 NodeBB v%1 .", + "keep-updated": "请确保您已及时更新 NodeBB 以获得最新的安全补丁与 Bug 修复。", + "up-to-date": "

正在使用 最新版本

", + "upgrade-available": "

新的版本 (v%1) 已经发布。建议您 升级 NodeBB

", + "prerelease-upgrade-available": "

这是一个已经过期的预发布版本的 NodeBB,新的版本 (v%1) 已经发布。建议您 升级 NodeBB

", + "prerelease-warning": "

正在使用测试版 NodeBB。可能会出现意外的 Bug。

", + "fallback-emailer-not-found": "找不到备用邮箱", + "running-in-development": "论坛正处于开发模式,这可能使其暴露于潜在的危险之中;请联系您的系统管理员。", + "latest-lookup-failed": "

无法查找 NodeBB 的最新可用版本

", + + "notices": "提醒", + "restart-not-required": "不需要重启", + "restart-required": "需要重启", + "search-plugin-installed": "已安装搜索插件", + "search-plugin-not-installed": "未安装搜索插件", + "search-plugin-tooltip": "在插件页面安装搜索插件来激活搜索功能", + + "control-panel": "系统控制", + "rebuild-and-restart": "部署 & 重启", + "restart": "重启", + "restart-warning": "重载或重启 NodeBB 会丢失数秒内全部的连接。", + "restart-disabled": "重建和重新启动NodeBB已被禁用,因为您似乎没有通过适当的守护进程运行它。", + "maintenance-mode": "维护模式", + "maintenance-mode-title": "点击此处设置 NodeBB 的维护模式", + "realtime-chart-updates": "实时图表更新", + + "active-users": "活跃用户", + "active-users.users": "用户", + "active-users.guests": "游客", + "active-users.total": "全部", + "active-users.connections": "连接", + + "guest-registered-users": "游客 vs 注册用户", + "guest": "游客", + "registered": "已注册", + + "user-presence": "用户光临", + "on-categories": "在版块列表", + "reading-posts": "读帖子", + "browsing-topics": "浏览话题", + "recent": "最近", + "unread": "未读", + + "high-presence-topics": "热门话题", + "popular-searches": "热门搜索", + + "graphs.page-views": "页面浏览量", + "graphs.page-views-registered": "注册用户页面浏览量", + "graphs.page-views-guest": "游客页面浏览量", + "graphs.page-views-bot": "爬虫页面浏览量", + "graphs.unique-visitors": "单一访客", + "graphs.registered-users": "已注册用户", + "graphs.guest-users": "游客", + "last-restarted-by": "上次重启管理员/时间", + "no-users-browsing": "没有用户正在浏览", + + "back-to-dashboard": "返回控制面板", + "details.no-users": "选定的时间内没有用户加入", + "details.no-topics": "选定的时间内没有发布主题", + "details.no-searches": "目前还没有进行任何搜索", + "details.no-logins": "选定的时间内没有登录记录", + "details.logins-static": "NodeBB只保留%1天登录数据,下列表格显示最近活动的登录。", + "details.logins-login-time": "登录时间" +} diff --git a/public/language/zh-CN/admin/development/info.json b/public/language/zh-CN/admin/development/info.json new file mode 100644 index 0000000000..8d4e8b4678 --- /dev/null +++ b/public/language/zh-CN/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "你正使用 %1:%2", + "ip": "IP %1", + "nodes-responded": "%1个节点在%2ms内响应!", + "host": "主机", + "primary": "主/运行任务", + "pid": "pid", + "nodejs": "nodejs", + "online": "在线", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "系统负载", + "cpu-usage": "CPU 使用情况", + "uptime": "运行时间", + + "registered": "已注册", + "sockets": "接口", + "guests": "游客", + + "info": "信息" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/development/logger.json b/public/language/zh-CN/admin/development/logger.json new file mode 100644 index 0000000000..4d3d8460c2 --- /dev/null +++ b/public/language/zh-CN/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "日志记录器设置", + "description": "启用此选项后,日志会在您的终端里显示。如果您注明了文件路径,日志会被保存到该文件中。HTTP 日志可以帮助您收集论坛被谁,何时,以及什么内容被访问等统计信息。在此基础上,我们还提供 socket.io 事件日志。结合 socket.io 日志和 redis-cli 监控器,学习 NodeBB 的内部构造会更加方便。", + "explanation": "勾选或反勾选日志设置项即可启用或禁用相应设置。无需重启。", + "enable-http": "启用 HTTP 日志", + "enable-socket": "启用 socket.io 事件日志", + "file-path": "日志文件路径", + "file-path-placeholder": "如 /path/to/log/file.log ::: 如想在终端中显示日志请留空此项", + + "control-panel": "日志记录器控制面板", + "update-settings": "更新日志记录器设置" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/extend/plugins.json b/public/language/zh-CN/admin/extend/plugins.json new file mode 100644 index 0000000000..27e29a407f --- /dev/null +++ b/public/language/zh-CN/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "趋势", + "installed": "已安装", + "active": "已启用", + "inactive": "未启用", + "out-of-date": "已过期", + "none-found": "无插件。", + "none-active": "无生效插件", + "find-plugins": "寻找插件", + + "plugin-search": "插件搜索", + "plugin-search-placeholder": "搜索插件...", + "submit-anonymous-usage": "提交匿名插件使用数据。", + "reorder-plugins": "重新排序插件", + "order-active": "排序生效插件", + "dev-interested": "有兴趣为 NodeBB 开发插件?", + "docs-info": "有关插件创作的完整文档可以在 NodeBB 文档中找到。", + + "order.description": "部分插件需要在其它插件启用之后才能完美运作。", + "order.explanation": "插件将按照以下顺序载入,从上至下。", + + "plugin-item.themes": "主题", + "plugin-item.deactivate": "停用", + "plugin-item.activate": "启用", + "plugin-item.install": "安装", + "plugin-item.uninstall": "卸载", + "plugin-item.settings": "设置", + "plugin-item.installed": "已安装", + "plugin-item.latest": "最新", + "plugin-item.upgrade": "升级", + "plugin-item.more-info": "更多信息:", + "plugin-item.unknown": "未知", + "plugin-item.unknown-explanation": "无法确认该插件的状态,可能由于配置错误造成。", + "plugin-item.compatible": "此插件兼容 NodeBB %1", + "plugin-item.not-compatible": "此插件没有兼容性数据,请确保在生产环境中安装之前它可以正常工作。", + + "alert.enabled": "插件已启用", + "alert.disabled": "插件已禁用", + "alert.upgraded": "插件已升级", + "alert.installed": "插件已安装", + "alert.uninstalled": "插件已卸载", + "alert.activate-success": "请重新编译和启动NodeBB以激活此插件", + "alert.deactivate-success": "插件停用成功", + "alert.upgrade-success": "请部署并重启您的 NodeBB 来完成更新此插件。", + "alert.install-success": "插件安装成功,请启用插件。", + "alert.uninstall-success": "插件已成功被停用且卸载。", + "alert.suggest-error": "

NodeBB 联系不到包管理器, 继续安装最新版本?

服务器返回 (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB 联系不到包管理器,暂时不建议升级。

", + "alert.incompatible": "

您目前安装的 NodeBB 版本(v%1) 只支持到此插件的 v%2 版本。如需要此插件更加新的版本请先升级 NodeBB。

", + "alert.possibly-incompatible": "

未找到兼容性信息

此插件未注明对应的 NodeBB 版本。可能会产生兼容问题,导致 NodeBB 无法正常启动。

NodeBB 无法正常启动时请运行以下命令:

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

是否继续安装此插件的最新版本?

", + "alert.reorder": "插件已重新排序", + "alert.reorder-success": "请部署并重启您的 NodeBB 来完成此流程。", + + "license.title": "插件许可证信息", + "license.intro": "插件 %1 在 %2 下获得许可。请在激活此插件之前阅读,确认许可条款。", + "license.cta": "您希望继续使用此插件吗?" +} diff --git a/public/language/zh-CN/admin/extend/rewards.json b/public/language/zh-CN/admin/extend/rewards.json new file mode 100644 index 0000000000..7646b33976 --- /dev/null +++ b/public/language/zh-CN/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "奖励", + "condition-if-users": "如果用户的", + "condition-is": "是:", + "condition-then": "则:", + "max-claims": "可获取奖励的次数", + "zero-infinite": "无限制请输入0", + "delete": "删除", + "enable": "启用", + "disable": "禁用", + + "alert.delete-success": "已成功删除奖励", + "alert.no-inputs-found": "非法奖励 - 输入为空!", + "alert.save-success": "已成功保存奖励" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/extend/widgets.json b/public/language/zh-CN/admin/extend/widgets.json new file mode 100644 index 0000000000..968ad499c4 --- /dev/null +++ b/public/language/zh-CN/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "可用的窗口部件", + "explanation": "从下拉菜单中选择一个窗口部件并拖放到模板左边的窗口部件区域。", + "none-installed": "未发现窗口部件!请在插件控制面板中启用必要的窗口部件插件。", + "clone-from": "从窗口部件克隆", + "containers.available": "可用的容器", + "containers.explanation": "拖放到任意生效中的窗口部件上", + "containers.none": "无", + "container.well": "Well", + "container.jumbotron": "超大屏幕", + "container.panel": "面板", + "container.panel-header": "面板标题", + "container.panel-body": "面板内容", + "container.alert": "警报", + + "alert.confirm-delete": "确认删除此窗口部件?", + "alert.updated": "窗口部件更新", + "alert.update-success": "已成功更新窗口部件", + "alert.clone-success": "成功克隆部件", + + "error.select-clone": "请选择一个页面进行克隆", + + "title": "标题", + "title.placeholder": "标题(仅在部分容器显示)", + "container": "容器", + "container.placeholder": "将容器拖拽至此处或在此处输入HTML", + "show-to-groups": "对群组显示", + "hide-from-groups": "对群组隐藏", + "hide-on-mobile": "在移动端隐藏" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/admins-mods.json b/public/language/zh-CN/admin/manage/admins-mods.json new file mode 100644 index 0000000000..79457a0f07 --- /dev/null +++ b/public/language/zh-CN/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "管理员", + "global-moderators": "全局版主", + "moderators": "版主", + "no-global-moderators": "没有全局版主", + "no-sub-categories": "没有子版块", + "subcategories": "%1 个子版块", + "no-moderators": "没有版主", + "add-administrator": "添加管理员", + "add-global-moderator": "添加全局版主", + "add-moderator": "添加版主" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/categories.json b/public/language/zh-CN/admin/manage/categories.json new file mode 100644 index 0000000000..aefa5a89b2 --- /dev/null +++ b/public/language/zh-CN/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "版块设置", + "privileges": "权限", + + "name": "版块名", + "description": "版块描述", + "bg-color": "背景颜色", + "text-color": "图标颜色", + "bg-image-size": "背景图片大小", + "custom-class": "自定义 Class", + "num-recent-replies": "最近回复数", + "ext-link": "外部链接", + "subcategories-per-page": "每页的子版块数量", + "is-section": "将此版块作为段落", + "post-queue": "发布队列", + "tag-whitelist": "标签白名单", + "upload-image": "上传图片", + "delete-image": "移除", + "category-image": "版块图片", + "parent-category": "父版块", + "optional-parent-category": "(可选)父版块", + "top-level": "Top Level", + "parent-category-none": "(无)", + "copy-parent": "复制 父类", + "copy-settings": "复制设置", + "optional-clone-settings": "(可选) 从版块复制设置", + "clone-children": "克隆子类别并进行设置", + "purge": "删除版块", + + "enable": "启用", + "disable": "禁用", + "edit": "编辑", + "analytics": "分析", + "view-category": "查看版块", + "set-order": "设置顺序", + "set-order-help": "设置版块的顺序会将此版块移动到对应的顺序,并根据需要更新其他版块的顺序。顺序值最小为 1,即将版块置于顶部。", + + "select-category": "选择版块", + "set-parent-category": "设置父版块", + + "privileges.description": "您可以在此面板中为站点的某些部分配置访问控制权。可以分别为每个用户或每个用户组授予权限。从下方的下拉列表中选择作用域名。", + "privileges.category-selector": "为该版块配置权限:", + "privileges.warning": "注意:权限设置会立即生效。 调整这些设置后,无需保存。", + "privileges.section-viewing": "查看权限", + "privileges.section-posting": "发帖权限", + "privileges.section-moderation": "审核权限", + "privileges.section-other": "其他", + "privileges.section-user": "用户", + "privileges.search-user": "添加用户", + "privileges.no-users": "此版块中没有用户特定的权限。", + "privileges.section-group": "群组", + "privileges.group-private": "这个群组是私密的", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "添加群组", + "privileges.copy-to-children": "复制到子版块", + "privileges.copy-from-category": "从版块复制", + "privileges.copy-privileges-to-all-categories": "复制到全部板块", + "privileges.copy-group-privileges-to-children": "复制此用户组的权限到此版块的子类", + "privileges.copy-group-privileges-to-all-categories": "复制此用户组的权限到全部版块", + "privileges.copy-group-privileges-from": "从其他版块复制权限到此用户组", + "privileges.inherit": "如果 registered-users 组被授予特定权限,所有其他组都会收到隐式权限,即使它们未被明确定义/检查。 将显示此隐式权限,因为所有用户都是 registered-users 群组的一部分,因此无需显式授予其他组的权限。", + "privileges.copy-success": "权限已复制", + + "analytics.back": "返回版块列表", + "analytics.title": "“%1”版块的统计", + "analytics.pageviews-hourly": "图1 – 此版块的每小时页面浏览量", + "analytics.pageviews-daily": "图2 – 此版块的每日页面浏览量", + "analytics.topics-daily": "图3 – 每日在此版块中创建的主题", + "analytics.posts-daily": "图4 – 每日在此版块中每日发布的帖子", + + "alert.created": "创建", + "alert.create-success": "版块创建成功!", + "alert.none-active": "您没有有效的版块。", + "alert.create": "创建一个版块", + "alert.confirm-purge": "

您确定要清除此版块“%1”吗?

警告! 版块将被清除!

清除版块将删除所有主题和帖子,并从数据库中删除版块。 如果您想暂时移除版块,请使用停用版块。

", + "alert.purge-success": "版块已删除!", + "alert.copy-success": "设置已复制!", + "alert.set-parent-category": "设置父版块", + "alert.updated": "版块已更新", + "alert.updated-success": "版块 ID %1 成功更新。", + "alert.upload-image": "上传版块图片", + "alert.find-user": "查找用户", + "alert.user-search": "在这里查找用户…", + "alert.find-group": "查找群组", + "alert.group-search": "在此处搜索群组...", + "alert.not-enough-whitelisted-tags": "白名单标签少于最少标签数量,您需要创建更多的白名单标签!", + "collapse-all": "全部折叠", + "expand-all": "全部展开", + "disable-on-create": "禁用创建", + "no-matches": "没有匹配项" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/digest.json b/public/language/zh-CN/admin/manage/digest.json new file mode 100644 index 0000000000..3621e92cff --- /dev/null +++ b/public/language/zh-CN/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "以下是摘要发送状态及时间列表", + "disclaimer": "请注意,由于 Email 技术本身的原因,邮件不一定能保证送达。有很多因素都会导致邮件无法到达用户的收件箱,比如发件服务器的信誉、IP 地址黑名单、DNS 的 DKIM/SPF/DMARC 配置等。", + "disclaimer-continued": "成功发送意味道消息被 NodeBB 成功发送且被接收人服务器收到。但这并不等同于邮件发送到了收件箱中。为了确保消息可以准确送达,我们建议使用第三方的邮件服务,例如SendGrid。", + + "user": "用户", + "subscription": "订阅类型", + "last-delivery": "上次成功通知", + "default": "系统默认", + "default-help": "System default 表示用户尚未明确覆盖摘要的全局论坛设置,该设置当前为: “%1“", + "resend": "重发摘要", + "resend-all-confirm": "你确定您要手动运行此摘要吗?", + "resent-single": "摘要重发操作完成", + "resent-day": "已发送每日摘要", + "resent-week": "已发送每周摘要", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "已发送每月摘要", + "null": "从不", + "manual-run": "手动运行摘要:", + + "no-delivery-data": "找不到发件数据" +} diff --git a/public/language/zh-CN/admin/manage/groups.json b/public/language/zh-CN/admin/manage/groups.json new file mode 100644 index 0000000000..47d484a769 --- /dev/null +++ b/public/language/zh-CN/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "群组名", + "badge": "徽章", + "properties": "属性", + "description": "群组描述", + "member-count": "成员数量", + "system": "系统", + "hidden": "隐藏", + "private": "私有", + "edit": "编辑", + "delete": "删除", + "privileges": "权限", + "download-csv": "CSV", + "search-placeholder": "搜索", + "create": "创建群组", + "description-placeholder": "一个关于你的群组的简短描述", + "create-button": "创建", + + "alerts.create-failure": "哦不!

创建您的群组时出现问题。 请稍后再试!

", + "alerts.confirm-delete": "您确定要删除此群组吗?", + + "edit.name": "名称", + "edit.description": "描述", + "edit.user-title": "成员标题", + "edit.icon": "群组标志", + "edit.label-color": "群组标签颜色", + "edit.text-color": "用户组文本颜色", + "edit.show-badge": "显示徽章", + "edit.private-details": "启用此选项后,加入群组的请求将需要群组所有者审批。", + "edit.private-override": "警告:系统已禁用了私有群组,优先级高于该选项。", + "edit.disable-join": "禁止申请加入群组", + "edit.disable-leave": "禁止用户离开群组", + "edit.hidden": "隐藏", + "edit.hidden-details": "启用此选项后,此群组将不在群组列表展现,并且用户只能被手动邀请加入", + "edit.add-user": "向此群组添加成员", + "edit.add-user-search": "搜索用户", + "edit.members": "成员列表", + "control-panel": "群组控制面板", + "revert": "重置", + + "edit.no-users-found": "没有找到用户", + "edit.confirm-remove-user": "确认删除此用户吗?", + "edit.save-success": "设置已保存!" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/privileges.json b/public/language/zh-CN/admin/manage/privileges.json new file mode 100644 index 0000000000..6d8ecd6549 --- /dev/null +++ b/public/language/zh-CN/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "全局", + "admin": "管理员", + "group-privileges": "群组权限", + "user-privileges": "用户权限", + "edit-privileges": "编辑权限", + "select-clear-all": "选择/清除 全部", + "chat": "对话", + "upload-images": "上传图片", + "upload-files": "上传文件", + "signature": "签名档", + "ban": "封禁", + "mute": "禁言", + "invite": "邀请", + "search-content": "搜索内容", + "search-users": "搜索用户", + "search-tags": "搜索标签", + "view-users": "浏览用户", + "view-tags": "浏览标签", + "view-groups": "浏览群组", + "allow-local-login": "本地登录", + "allow-group-creation": "群组创建", + "view-users-info": "查看用户信息", + "find-category": "查找版块", + "access-category": "访问版块", + "access-topics": "访问主题", + "create-topics": "创建主题", + "reply-to-topics": "回复主题", + "schedule-topics": "定时主题", + "tag-topics": "标签主题", + "edit-posts": "修改回复", + "view-edit-history": "查看变更历史", + "delete-posts": "删除回复", + "view_deleted": "查看已删除回复", + "upvote-posts": "顶帖", + "downvote-posts": "踩帖", + "delete-topics": "删除主题", + "purge": "清除", + "moderate": "版主", + "admin-dashboard": "仪表盘", + "admin-categories": "版块", + "admin-privileges": "权限", + "admin-users": "用户", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "群组", + "admin-tags": "标签", + "admin-settings": "设置", + + "alert.confirm-moderate": "您确定要将审核权限授予此用户组吗?此用户组是公开的,任何用户都可以随意加入。", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "请验证您保存这些权限的目的", + "alert.saved": "权限修改已保存并应用", + "alert.confirm-discard": "您确定要取消权限修改吗?", + "alert.discarded": "权限修改已被丢弃", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "此操作无法撤消。", + "alert.admin-warning": "管理员隐性获得全部权限", + "alert.copyPrivilegesFrom-title": "选择一个用于复制的版块", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/registration.json b/public/language/zh-CN/admin/manage/registration.json new file mode 100644 index 0000000000..fa2e26c5be --- /dev/null +++ b/public/language/zh-CN/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "申请", + "description": "注册申请队列里面还没有用户申请。
要开启这项功能,请去设置 → 用户 → 用户注册 并设置注册类型为“管理员批准”。", + + "list.name": "姓名", + "list.email": "邮件", + "list.ip": "IP", + "list.time": "时间", + "list.username-spam": "频率: %1 显示:%2 信心:%3", + "list.email-spam": "频率:%1 显示: %2", + "list.ip-spam": "频率:%1 显示: %2", + + "invitations": "邀请", + "invitations.description": "下面列出了所有已发送的邀请。您可以使用 Ctrl+F 快捷键搜索列表中的邮箱地址或用户名。

如果用户接受了邀请,他的用户名将会被显示在邮箱右边。", + "invitations.inviter-username": "邀请人用户名", + "invitations.invitee-email": "受邀请的电子邮箱", + "invitations.invitee-username": "受邀请的用户名(如果已经注册)", + + "invitations.confirm-delete": "确认删除这个邀请?" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/tags.json b/public/language/zh-CN/admin/manage/tags.json new file mode 100644 index 0000000000..5a8dd20804 --- /dev/null +++ b/public/language/zh-CN/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "您的论坛目前没有带标签的主题。", + "bg-color": "背景颜色", + "text-color": "文字颜色", + "description": "通过点击或拖动选择标签,按住 CTRL 进行多选。", + "create": "创建标签", + "modify": "修改标签", + "rename": "重命名标签", + "delete": "删除所选标签", + "search": "搜索标签...", + "settings": "标签设置", + "name": "标签名称", + + "alerts.editing": "编辑标签", + "alerts.confirm-delete": "您确定要删除选择的标签吗?", + "alerts.update-success": "标签已更新!", + "reset-colors": "重置颜色" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/uploads.json b/public/language/zh-CN/admin/manage/uploads.json new file mode 100644 index 0000000000..3bdaa05c20 --- /dev/null +++ b/public/language/zh-CN/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "上传文件", + "filename": "文件名", + "usage": "使用的帖子", + "orphaned": "未使用", + "size/filecount": "大小/文件数", + "confirm-delete": "您确定要删除此文件吗?", + "filecount": "%1 文件", + "new-folder": "新建文件夹", + "name-new-folder": "输入新文件夹的名称" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/users.json b/public/language/zh-CN/admin/manage/users.json new file mode 100644 index 0000000000..49b2421d80 --- /dev/null +++ b/public/language/zh-CN/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "用户", + "edit": "操作", + "make-admin": "设为管理", + "remove-admin": "取消管理员", + "validate-email": "验证邮箱", + "send-validation-email": "发送验证邮件", + "password-reset-email": "发送密码重置邮件", + "force-password-reset": "强制密码重置 & 登录用户已退出", + "ban": "封禁用户", + "temp-ban": "暂时封禁用户", + "unban": "解禁用户", + "reset-lockout": "重设闭锁", + "reset-flags": "重设举报", + "delete": "删除用户", + "delete-content": "删除用户内容", + "purge": "删除用户和内容", + "download-csv": "下载CSV", + "manage-groups": "管理用户组", + "add-group": "添加至群组", + "create": "创建用户", + "invite": "通过邮件邀请", + "new": "新建用户", + "filter-by": "过滤选项", + "pills.unvalidated": "未验证", + "pills.validated": "已验证", + "pills.banned": "被封禁", + + "50-per-page": "每页50", + "100-per-page": "每页100", + "250-per-page": "每页250", + "500-per-page": "每页500", + + "search.uid": "通过用户ID", + "search.uid-placeholder": "搜索用户ID", + "search.username": "通过用户名", + "search.username-placeholder": "输入您想查询的用户名", + "search.email": "通过邮箱", + "search.email-placeholder": "输入您想查询的邮箱地址", + "search.ip": "通过IP地址", + "search.ip-placeholder": "输入您想查询的 IP 地址", + "search.not-found": "未找到用户!", + + "inactive.3-months": "3个月", + "inactive.6-months": "6个月", + "inactive.12-months": "12个月", + + "users.uid": "UID", + "users.username": "用户名", + "users.email": "电子邮件", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "发帖数", + "users.reputation": "声望", + "users.flags": "举报", + "users.joined": "注册时间", + "users.last-online": "最后在线", + "users.banned": "封禁", + + "create.username": "用户名", + "create.email": "电子邮件", + "create.email-placeholder": "该用户的邮箱", + "create.password": "密码", + "create.password-confirm": "确认密码", + + "temp-ban.length": "时长", + "temp-ban.reason": "理由(可选)", + "temp-ban.hours": "小时", + "temp-ban.days": "天", + "temp-ban.explanation": "输入封禁时长。提示,时长为0视为永久封禁。", + + "alerts.confirm-ban": "您确定要永久封禁该用户吗?", + "alerts.confirm-ban-multi": "您确定要永久封禁这些用户吗?", + "alerts.ban-success": "用户已封禁!", + "alerts.button-ban-x": "封禁 %1 名用户", + "alerts.unban-success": "用户已解封!", + "alerts.lockout-reset-success": "锁定已重置!", + "alerts.flag-reset-success": "举报已重置!", + "alerts.no-remove-yourself-admin": "您无法撤销自己的管理员身份!", + "alerts.make-admin-success": "该用户已成为管理员", + "alerts.confirm-remove-admin": "您确定要删除该管理员?", + "alerts.remove-admin-success": " 该用户不再是管理员", + "alerts.make-global-mod-success": " 该用户已成为管理员", + "alerts.confirm-remove-global-mod": "您确定要删除该全局版主?", + "alerts.remove-global-mod-success": "该用户已不再是管理员", + "alerts.make-moderator-success": " 该用户已成为管理员", + "alerts.confirm-remove-moderator": "您确定要删除该版主?", + "alerts.remove-moderator-success": "该用户已不再是管理员", + "alerts.confirm-validate-email": "您确定要验证这些用户的邮箱地址吗?", + "alerts.confirm-force-password-reset": "你确定您想要向这个(这些)用户强制密码重置并退出吗?", + "alerts.validate-email-success": "电子邮箱已验证", + "alerts.validate-force-password-reset-success": "用户密码已经被重置,现存的会话已经被移除", + "alerts.password-reset-confirm": "您确定要向这些用户发送密码重置邮件吗?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "警告!

您确定要删除这些用户吗?

该操作不可逆转!该操作只会删除用户账号,他们的帖子与主题仍会保留。

", + "alerts.delete-success": "用户已删除!", + "alerts.confirm-delete-content": "警告!

您确定要删除这些用户内容吗?

该操作不可逆转!用户账号会被保留,但是用户的帖子和主题将会被删除。

", + "alerts.delete-content-success": "用户内容已删除!", + "alerts.confirm-purge": "警告!

您确定要删除这些用户和内容吗?

该操作不可逆转!选中的所有用户数据和内容都将被清除!

", + "alerts.create": "创建用户", + "alerts.button-create": "创建", + "alerts.button-cancel": "取消", + "alerts.error-passwords-different": "两次输入的密码必须相同!", + "alerts.error-x": "错误

%1

", + "alerts.create-success": "用户已创建!", + + "alerts.prompt-email": "邮件:", + "alerts.email-sent-to": "已发送邀请给 %1", + "alerts.x-users-found": "找到 %1 位用户(耗时 %2 秒)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "用户列表已被导出为 CSV 文件,点击以下载。" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/menu.json b/public/language/zh-CN/admin/menu.json new file mode 100644 index 0000000000..2e456615c0 --- /dev/null +++ b/public/language/zh-CN/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "仪表盘", + "dashboard/overview": "概览", + "dashboard/logins": "登录", + "dashboard/users": "用户", + "dashboard/topics": "主题", + "dashboard/searches": "Searches", + "section-general": "基本", + + "section-manage": "管理", + "manage/categories": "版块", + "manage/privileges": "权限", + "manage/tags": "标签", + "manage/users": "用户", + "manage/admins-mods": "权限分配", + "manage/registration": "注册申请", + "manage/post-queue": "提交列表", + "manage/groups": "群组", + "manage/ip-blacklist": "IP 黑名单", + "manage/uploads": "上传", + "manage/digest": "摘要", + + "section-settings": "设置", + "settings/general": "通用", + "settings/homepage": "主页", + "settings/navigation": "导航", + "settings/reputation": "声望 & 举报", + "settings/email": "邮件", + "settings/user": "用户", + "settings/group": "群组", + "settings/guest": "游客", + "settings/uploads": "上传", + "settings/languages": "语言", + "settings/post": "帖子", + "settings/chat": "聊天", + "settings/pagination": "分页", + "settings/tags": "标签", + "settings/notifications": "通知", + "settings/api": "API 访问", + "settings/sounds": "铃声", + "settings/social": "社交", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web 爬虫", + "settings/sockets": "套接字", + "settings/advanced": "高级", + + "settings.page-title": "%1 设置", + + "section-appearance": "界面", + "appearance/themes": "主题", + "appearance/skins": "皮肤", + "appearance/customise": "自定义代码 (HTML/JavaScript/CSS)", + + "section-extend": "扩展", + "extend/plugins": "插件", + "extend/widgets": "窗口部件", + "extend/rewards": "奖励", + + "section-social-auth": "社交认证", + + "section-plugins": "插件", + "extend/plugins.install": "已安装", + + "section-advanced": "高级", + "advanced/database": "数据库", + "advanced/events": "事件", + "advanced/hooks": "插件钩子", + "advanced/logs": "日志", + "advanced/errors": "错误", + "advanced/cache": "缓存", + "development/logger": "记录器", + "development/info": "信息", + + "rebuild-and-restart-forum": "部署并重启论坛", + "restart-forum": "重启论坛", + "logout": "登出", + "view-forum": "查看论坛", + + "search.placeholder": "搜索设置", + "search.no-results": "没有可用结果…", + "search.search-forum": "搜索论坛为", + "search.keep-typing": "输入更多以查看结果...", + "search.start-typing": "开始输入以查看结果...", + + "connection-lost": "与 %1 的连接已丢失,正尝试重新连接...", + + "alerts.version": "正在运行 NodeBB v%1", + "alerts.upgrade": "升级到 v%1" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/advanced.json b/public/language/zh-CN/admin/settings/advanced.json new file mode 100644 index 0000000000..45a12d552a --- /dev/null +++ b/public/language/zh-CN/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "维护模式", + "maintenance-mode.help": "当论坛处在维护模式时,所有请求将被重定向到一个静态页面。管理员不受重定向限制,并可正常访问站点。", + "maintenance-mode.status": "维护模式状态码", + "maintenance-mode.message": "维护消息", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "标题", + "headers.allow-from": "设置 ALLOW-FROM 来放置 NodeBB 于 iFrame 中", + "headers.csp-frame-ancestors": "设置 Content-Security-Policy frame-ancestors 响应头来将 NodeBB 置于 iFrame 中", + "headers.csp-frame-ancestors-help": "在此输入 none 或是 self (默认),或一系列允许的 URI。", + "headers.powered-by": "自定义由 NodeBB 发送的 \"Powered By\" 响应头 ", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin 正则表达式", + "headers.acao-help": "要拒绝所有网站,请留空", + "headers.acao-regex-help": "输入正则表达式以匹配动态来源。要拒绝所有网站,请留空", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "严格安全传输(HSTS)", + "hsts.enabled": "启用HSTS(推荐)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "HSTS头信息包含的域名", + "hsts.preload": "允许在HSTS头信息中预加载(preloading)", + "hsts.help": "如果启用此项,站点将会向浏览器发送HSTS头信息。您可以设置是否为子域名开启HSTS,以及HSTS头信息中是否包含预加载标志(preload参数)如果您不了解HSTS,可以忽略此项设置。了解详情 ", + "traffic-management": "流量管理", + "traffic.help": "NodeBB 使用一个在高流量情况下自动拒绝请求的模块。尽管默认值就很棒,但您可以在这里调整这些设置。", + "traffic.enable": "启用流量管理", + "traffic.event-lag": "事件循环滞后阈值(毫秒)", + "traffic.event-lag-help": "降低此值会减少页面加载的等待时间,但也会向更多用户显示“过载”消息。(需要重新启动)", + "traffic.lag-check-interval": "检查间隔(毫秒)", + "traffic.lag-check-interval-help": "降低此值会造成 NodeBB 的负载峰值变得更加敏感,但也可能导致检查变得过于敏感(需要重新启动)", + + "sockets.settings": "WebSocket 设置", + "sockets.max-attempts": "最大重连尝试次数", + "sockets.default-placeholder": "默认:%1", + "sockets.delay": "重连等待时间", + + "analytics.settings": "分析设置", + "analytics.max-cache": "分析缓存最大值", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "压缩设置", + "compression.enable": "启用压缩", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/api.json b/public/language/zh-CN/admin/settings/api.json new file mode 100644 index 0000000000..0075718d68 --- /dev/null +++ b/public/language/zh-CN/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "令牌", + "settings": "设置", + "lead-text": "从此处,您可以配置对 NodeBB 中 Write API 的访问。", + "intro": "默认情况下,Write API 根据用户的会话cookie对用户进行身份验证,但 NodeBB 也支持通过此页面生成的令牌进行身份验证。", + "docs": "单击此处访问完整的 API 规范", + + "require-https": "要求 API 只能通过 HTTPS 调用", + "require-https-caveat": "注意:一些负载均衡器可能会使用 HTTP 代理对 NodeBB 的请求,在此情况下此选项应保持关闭状态。", + + "uid": "用户ID", + "uid-help-text": "指定要与此令牌关联的用户ID。如果用户ID是 0, 它将被实危 最高 令牌,可以通过 _uid 参数假定其他用户的身份", + "description": "说明", + "no-description": "未指定说明。", + "token-on-save": "保存表单后将生成令牌" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/chat.json b/public/language/zh-CN/admin/settings/chat.json new file mode 100644 index 0000000000..eb38710164 --- /dev/null +++ b/public/language/zh-CN/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "聊天设置", + "disable": "禁用聊天", + "disable-editing": "禁止编辑/删除聊天消息", + "disable-editing-help": "管理员和超级管理员不受此限制", + "max-length": "聊天信息的最大长度", + "max-room-size": "聊天室的最多用户数", + "delay": "聊天信息间的毫秒数", + "notification-delay": "聊天信息的通知延迟(0 为即时)", + "restrictions.seconds-edit-after": "用户在发布聊天消息后允许编辑帖子的秒数(0为禁用)", + "restrictions.seconds-delete-after": "用户在发布聊天消息后允许删除帖子的秒数(0为禁用)" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/cookies.json b/public/language/zh-CN/admin/settings/cookies.json new file mode 100644 index 0000000000..f447487119 --- /dev/null +++ b/public/language/zh-CN/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "欧盟 Cookies 政策", + "consent.enabled": "启用选项", + "consent.message": "通知消息", + "consent.acceptance": "赞成消息", + "consent.link-text": "政策链接文本", + "consent.link-url": "政策地址链接", + "consent.blank-localised-default": "留空以便使用 NodeBB 本地默认值", + "settings": "设置", + "cookie-domain": "Session cookie 域名", + "max-user-sessions": "每个用户的最大活跃会话数", + "blank-default": "留空以保持默认" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/email.json b/public/language/zh-CN/admin/settings/email.json new file mode 100644 index 0000000000..4d6570a579 --- /dev/null +++ b/public/language/zh-CN/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "邮件设置", + "address": "电子邮箱地址", + "address-help": "下面的电子邮件地址代表收件人在“发件人”和“回复”中所看到的地址。", + "from": "发送者", + "from-help": "用于邮件中显示的发送者", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP 通信", + "smtp-transport.enabled": "启用 SMTP 通信", + "smtp-transport-help": "您可以从列表中选取一个已知的服务或自定义。", + "smtp-transport.service": "选择服务", + "smtp-transport.service-custom": "自定义", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP 主机名", + "smtp-transport.port": "SMTP 端口", + "smtp-transport.security": "连接安全设置", + "smtp-transport.security-encrypted": "加密的", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "无", + "smtp-transport.username": "用户名", + "smtp-transport.username-help": "对于Gmail服务,请在这里输入完整的电子邮箱地址,尤其是如果您使用的是 Google Apps 托管的域名。", + "smtp-transport.password": "密码", + "smtp-transport.pool": "启用池式连接", + "smtp-transport.pool-help": "池式连接可防止 NodeBB 为每封邮件创建新的连接。此选项仅适用于启用SMTP传输的情况下。", + + "template": "编辑电子邮件模板", + "template.select": "选择电子邮件模板", + "template.revert": "还原为初始模板", + "testing": "电子邮件测试", + "testing.select": "选择电子邮件模板", + "testing.send": "发送测试电子邮件", + "testing.send-help": "测试电子邮件将被发送到当前已登录的用户的电子邮件地址。", + "subscriptions": "电子邮件摘要", + "subscriptions.disable": "禁用电子邮件摘要", + "subscriptions.hour": "摘要小时", + "subscriptions.hour-help": "请输入一个代表小时的数字来发送计划的电子邮件摘要 (例如,对于午夜,0,对于下午5:00,17)。 请记住,这是根据服务器本身的时间,可能与您的系统时钟不完全匹配。
服务器的大致时间为:
下一个每日摘要被计划在发送", + "notifications.remove-images": "从电子邮件通知中删除图像", + "require-email-address": "要求新用户指定电子邮箱地址", + "require-email-address-warning": "默认情况下,用户可以通过将该字段留空来选择不输入电子邮件地址。启用此选项意味着他们必须输入电子邮件地址才能继续注册。它不能确保用户输入真实的电子邮件地址,甚至也不能确保他们拥有该地址。", + "send-validation-email": "添加或更改电子邮件时发送验证电子邮件", + "include-unverified-emails": "向尚未明验证其电子邮箱的人发送电子邮件", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "提示用户输入或验证他们的电子邮箱地址", + "prompt-help": "如果用户没有设置电子邮箱,或者他们的电子邮件未被验证,页面上将会显示警告。", + "sendEmailToBanned": "即使用户已被封禁也发送电子邮件" +} diff --git a/public/language/zh-CN/admin/settings/general.json b/public/language/zh-CN/admin/settings/general.json new file mode 100644 index 0000000000..0d85bff4bb --- /dev/null +++ b/public/language/zh-CN/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "站点设置", + "title": "站点标题", + "title.short": "短标题", + "title.short-placeholder": "如果没有指定短标题,将会使用站点标题", + "title.url": "标题链接地址", + "title.url-placeholder": "网站标题链接", + "title.url-help": "用户点击标题时将会跳转到此地址。如果留空,用户将会跳转到论坛首页。
注意:此处不是在电子邮件等地方使用的外部 URL,它应该由配置文件 config.json 中的 url 属性指定。", + "title.name": "您的社区名称", + "title.show-in-header": "在顶部显示站点标题", + "browser-title": "浏览器标题", + "browser-title-help": "如果没有指定浏览器标题,将会使用站点标题", + "title-layout": "标题布局", + "title-layout-help": "定义浏览器标题的布局,即{pageTitle} | {browserTitle}", + "description.placeholder": "关于您的社区的简短说明", + "description": "站点描述", + "keywords": "站点关键字", + "keywords-placeholder": "描述您的社区的关键字(以逗号分隔)", + "logo": "站点 Logo", + "logo.image": "图像", + "logo.image-placeholder": "要在论坛标题上显示的 Logo 的路径", + "logo.upload": "上传", + "logo.url": "Logo 链接地址", + "logo.url-placeholder": "站点 Logo 链接", + "logo.url-help": "用户点击 logo 时将会跳转到此地址。如果留空,用户将会跳转到论坛首页。
注意:此处不是在电子邮件等地方使用的外部 URL,它应该由配置文件 config.json 中的 url 属性指定。", + "logo.alt-text": "替代文本", + "log.alt-text-placeholder": "辅助功能的替代文本", + "favicon": "站点图标", + "favicon.upload": "上传", + "pwa": "渐进式网页应用", + "touch-icon": "触摸图标", + "touch-icon.upload": "上传", + "touch-icon.help": "推荐的尺寸和格式:512x512,仅限PNG格式。如果没有指定触摸图标,NodeBB将回退到站点图标。", + "maskable-icon": "可遮蔽(主屏)图标", + "maskable-icon.help": "推荐的尺寸和格式:512x512,仅限PNG格式。如果没有指定可遮蔽图标,NodeBB将回退到触摸图标。", + "outgoing-links": "站外链接", + "outgoing-links.warning-page": "使用站外链接警告页", + "search": "搜索", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "排序", + "outgoing-links.whitelist": "添加域名到白名单以绕过警告页面", + "site-colors": "站点颜色元数据", + "theme-color": "主题色", + "background-color": "背景色", + "background-color-help": "当网站安装为 PWA 时用于启动屏幕背景的颜色", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "部分操作,例如移动主题,将允许版主在一定时间内撤销其操作。设置为 0 可完全禁用撤消。", + "topic-tools": "主题工具" +} diff --git a/public/language/zh-CN/admin/settings/group.json b/public/language/zh-CN/admin/settings/group.json new file mode 100644 index 0000000000..aa4cc8a2d8 --- /dev/null +++ b/public/language/zh-CN/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "通用", + "private-groups": "私有群组", + "private-groups.help": "启用此选项后,加入用户组需要群组所有者审批(默认启用)。", + "private-groups.warning": "注意!如果这个选项未启用并且你有私有群组,那么你的群组将变为公共的。", + "allow-multiple-badges": "允许多种徽章", + "allow-multiple-badges-help": "启用此选项后,用户可以选择显示多个群组徽章,需要主题支持。", + "max-name-length": "群组名字的最大长度", + "max-title-length": "群组标题最大长度", + "cover-image": "群组封面图片", + "default-cover": "默认封面图片", + "default-cover-help": "为没有上传封面图片的群组添加以逗号分隔的默认封面图片" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/guest.json b/public/language/zh-CN/admin/settings/guest.json new file mode 100644 index 0000000000..db8a11858a --- /dev/null +++ b/public/language/zh-CN/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "设置", + "handles.enabled": "允许游客用户名", + "handles.enabled-help": "这个选项将允许游客使用一个额外的输入框来设置发帖时的用户名,如果被禁用,仅会统一显示为“游客”", + "topic-views.enabled": "将来自游客的浏览记入帖子的浏览数", + "reply-notifications.enabled": "允许游客生成回帖通知" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/homepage.json b/public/language/zh-CN/admin/settings/homepage.json new file mode 100644 index 0000000000..8864e4eb34 --- /dev/null +++ b/public/language/zh-CN/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "主页", + "description": "请选择用户到达根 URL 时所显示的页面。", + "home-page-route": "主页路由", + "custom-route": "自定义路由", + "allow-user-home-pages": "允许用户主页", + "home-page-title": "首页标题(默认“Home”)" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/languages.json b/public/language/zh-CN/admin/settings/languages.json new file mode 100644 index 0000000000..b8cb60203e --- /dev/null +++ b/public/language/zh-CN/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "语言设置", + "description": "默认语言会决定所有用户的语言设定。
单一用户可以各自在帐户设置中覆盖此项设定。", + "default-language": "默认语言", + "auto-detect": "自动检测游客的语言设置" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/navigation.json b/public/language/zh-CN/admin/settings/navigation.json new file mode 100644 index 0000000000..64a218391a --- /dev/null +++ b/public/language/zh-CN/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "图标:", + "change-icon": "更改", + "route": "路由:", + "tooltip": "提示:", + "text": "文本:", + "text-class": "文本类:可选", + "class": "类: 可选", + "id": "ID:可选", + + "properties": "属性:", + "groups": "群组:", + "open-new-window": "在新窗口中打开", + "dropdown": "下拉列表", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "删除", + "btn.disable": "禁用", + "btn.enable": "启用", + + "available-menu-items": "可用的菜单项目", + "custom-route": "自定义路由", + "core": "核心", + "plugin": "插件" +} diff --git a/public/language/zh-CN/admin/settings/notifications.json b/public/language/zh-CN/admin/settings/notifications.json new file mode 100644 index 0000000000..147dc97f23 --- /dev/null +++ b/public/language/zh-CN/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "通知", + "welcome-notification": "欢迎通知", + "welcome-notification-link": "欢迎通知链接", + "welcome-notification-uid": "用户欢迎通知 (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/pagination.json b/public/language/zh-CN/admin/settings/pagination.json new file mode 100644 index 0000000000..f3565fb555 --- /dev/null +++ b/public/language/zh-CN/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "分页设置", + "enable": "在主题和帖子使用分页替代无限滚动浏览。", + "posts": "帖子分页", + "topics": "话题分页", + "posts-per-page": "每页帖子数", + "max-posts-per-page": "每页最多帖子数", + "categories": "版块分页", + "topics-per-page": "每页主题数", + "max-topics-per-page": "每页最多主题数", + "categories-per-page": "每页的版块数量" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/post.json b/public/language/zh-CN/admin/settings/post.json new file mode 100644 index 0000000000..261645aec1 --- /dev/null +++ b/public/language/zh-CN/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "帖子排序", + "sorting.post-default": "默认帖子排序", + "sorting.oldest-to-newest": "从旧到新", + "sorting.newest-to-oldest": "从新到旧", + "sorting.most-votes": "最多点赞", + "sorting.most-posts": "最多回复", + "sorting.topic-default": "默认主题排序", + "length": "发帖字数", + "post-queue": "发帖队列", + "restrictions": "发帖限制", + "restrictions-new": "新用户限制", + "restrictions.post-queue": "启用发帖队列", + "restrictions.post-queue-rep-threshold": "忽略发帖队列的威望值", + "restrictions.groups-exempt-from-post-queue": "选择会被从提交队列豁免的分组", + "restrictions-new.post-queue": "启用新用户限制", + "restrictions.post-queue-help": "启用发帖队列会将新用户的发帖放入审核队列", + "restrictions-new.post-queue-help": "启用新用户限制将对新用户创建的帖子设置限制", + "restrictions.seconds-between": "发帖间隔时间(秒)", + "restrictions.seconds-between-new": "新用户发帖间隔时间(秒)", + "restrictions.rep-threshold": "取消发帖限制所需的声望值", + "restrictions.seconds-before-new": "新用户可以在第一次发布之前的秒数", + "restrictions.seconds-edit-after": "帖子保持可编辑的秒数(设置为0表示禁用)", + "restrictions.seconds-delete-after": "帖子保持可删除的秒数(设置为0表示禁用)", + "restrictions.replies-no-delete": "在用户被禁止删除自己的主题后的回复数。 (0为禁用) ", + "restrictions.min-title-length": "标题字数下限", + "restrictions.max-title-length": "标题字数上限", + "restrictions.min-post-length": "帖子字数下限", + "restrictions.max-post-length": "帖子字数上限", + "restrictions.days-until-stale": "主题过时时间(天)", + "restrictions.stale-help": "如果某个主题被视为“过时”,则会向尝试回复该主题的用户显示警告。", + "timestamp": "时间戳", + "timestamp.cut-off": "截止日期(天)", + "timestamp.cut-off-help": "日期&时间将以相对方式 (例如,“3小时前” / “5天前”) 显示,并且会依照访客语言时区转换。在某一时刻之后,可以切换该文本以显示本地化日期本身 (例如2016年11月5日15:30) 。
(默认值: 30 或一个月) 。 设置为0可始终显示日期,留空以始终显示相对时间。", + "timestamp.necro-threshold": "挖坟警告(单位:天)", + "timestamp.necro-threshold-help": "若进行回复的帖子最后回复的时间早于挖坟警告设定的天数,则在尝试回复前显示挖坟警告(默认:7天)。可以设置为 0 来禁用。", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "根据此设置,主题的访问数量每 X 分钟只增加一次。", + "teaser": "预览帖子", + "teaser.last-post": "最后– 显示最新的帖子,包括原帖,如果没有回复", + "teaser.last-reply": "最后– 显示最新回复,如果没有回复,则显示“无回复”占位符", + "teaser.first": "第一", + "showPostPreviewsOnHover": "鼠标悬停时显示帖子预览", + "unread": "未读设置", + "unread.cutoff": "未读截止时间(天)", + "unread.min-track-last": "跟踪最后阅读之前的主题最小帖子", + "recent": "最近设置", + "recent.max-topics": "/recent 页面的最大主题数", + "recent.categoryFilter.disable": "禁用对 /recent 页面上忽略类别中的主题进行过滤", + "signature": "签名设置", + "signature.disable": "禁用签名", + "signature.no-links": "禁用签名中的链接", + "signature.no-images": "禁用签名中的图片", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "签名字数上限", + "composer": "编辑器设置", + "composer-help": "以下设置控制所示后期编辑器的功能和/或外观\n\t\t\t\t当用户创建新主题或回复现有主题时。", + "composer.show-help": "显示“帮助”选项卡", + "composer.enable-plugin-help": "允许插件添加内容到帮助选项卡", + "composer.custom-help": "自定义帮助文本", + "backlinks": "反向链接", + "backlinks.enabled": "启用主题反向链接", + "backlinks.help": "如果一篇帖子引用了另一个主题,那么一个指向该帖子的链接将在该时间点插入到被引用的主题中。", + "ip-tracking": "IP 跟踪", + "ip-tracking.each-post": "跟踪每个帖子的 IP 地址", + "enable-post-history": "启用回复历史" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/reputation.json b/public/language/zh-CN/admin/settings/reputation.json new file mode 100644 index 0000000000..fc8d47bdf0 --- /dev/null +++ b/public/language/zh-CN/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "声望设置", + "disable": "禁用 声望系统", + "disable-down-voting": "禁用 踩", + "votes-are-public": "公开所有投票", + "thresholds": "操作限制", + "min-rep-upvote": "顶帖子 需要的最低声望", + "upvotes-per-day": "每天顶的次数(设置为0则表示无限制)", + "upvotes-per-user-per-day": "每位用户每天顶的次数(设置为0则表示无限制)", + "min-rep-downvote": "踩帖子 需要的最低声望", + "downvotes-per-day": "每天踩的次数(设置为0则表示无限制)", + "downvotes-per-user-per-day": "每位用户每天踩的次数(设置为0则表示无限制)", + "min-rep-chat": "发送聊天消息 需要的最低声望", + "min-rep-flag": "举报帖子 需要的最低声望", + "min-rep-website": "添加 个人网站 需要的最低声望", + "min-rep-aboutme": "添加 个人 “关于我”页 需要的最低声望", + "min-rep-signature": "添加 签名档 需要的最低声望", + "min-rep-profile-picture": "添加 个人资料图片 需要的最低声望", + "min-rep-cover-picture": "添加 个人封面图片 需要的最低声望", + + "flags": "举报设置", + "flags.limit-per-target": "可以举报某事物的最大次数", + "flags.limit-per-target-placeholder": "默认:0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/social.json b/public/language/zh-CN/admin/settings/social.json new file mode 100644 index 0000000000..0882ee95e9 --- /dev/null +++ b/public/language/zh-CN/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "帖子分享", + "info-plugins-additional": "插件可以增加可选的用于分享帖子的网络。", + "save-success": "已成功保存帖子分享网络。" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/sockets.json b/public/language/zh-CN/admin/settings/sockets.json new file mode 100644 index 0000000000..27cd0e4738 --- /dev/null +++ b/public/language/zh-CN/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "重新连接设置", + "max-attempts": "最大重新连接尝试", + "default-placeholder": "默认:%1", + "delay": "重连等待时间" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/sounds.json b/public/language/zh-CN/admin/settings/sounds.json new file mode 100644 index 0000000000..d330e309ac --- /dev/null +++ b/public/language/zh-CN/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "通知", + "chat-messages": "聊天信息", + "play-sound": "播放", + "incoming-message": "收到的消息", + "outgoing-message": "发出的消息", + "upload-new-sound": "上传新的声音", + "saved": "设置已保存" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/tags.json b/public/language/zh-CN/admin/settings/tags.json new file mode 100644 index 0000000000..43ac39d1e6 --- /dev/null +++ b/public/language/zh-CN/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "标签设置", + "link-to-manage": "管理标签", + "system-tags": "系统标签", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "每个主题的最少标签数", + "max-per-topic": "每个主题的最多标签数", + "min-length": "标签名称最小长度", + "max-length": "标签名称最大长度", + "related-topics": "相关主题", + "max-related-topics": "最大相关主题显示量(如果主题支持)" +} \ No newline at end of file diff --git a/public/language/zh-CN/admin/settings/uploads.json b/public/language/zh-CN/admin/settings/uploads.json new file mode 100644 index 0000000000..61f6db6d3b --- /dev/null +++ b/public/language/zh-CN/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "帖子", + "orphans": "Orphaned Files", + "private": "使上传的文件私有化", + "strip-exif-data": "去除 EXIF 数据", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "自定义文件扩展名", + "private-uploads-extensions-help": "在此处输入以逗号分隔的文件扩展名列表 (例如 pdf,xls,doc )并将其用于自定义。为空则表示允许所有扩展名。", + "resize-image-width-threshold": "如果图像宽度超过指定大小,则对图像进行缩放", + "resize-image-width-threshold-help": "(像素单位,默认 1520 px,设置为0以禁用)", + "resize-image-width": "缩小图片到指定宽度", + "resize-image-width-help": "(像素单位,默认 760 px,设置为0以禁用)", + "resize-image-quality": "调整图像大小时使用的质量", + "resize-image-quality-help": "使用较低质量的设置来减小调整过大小的图像的文件大小", + "max-file-size": "最大文件尺寸(单位 KiB)", + "max-file-size-help": "(单位 KiB ,默认 2048KiB)", + "reject-image-width": "图片最大宽度值(单位:像素)", + "reject-image-width-help": "宽于此数值大小的图片将会被拒绝", + "reject-image-height": "图片最大高度值(单位:像素)", + "reject-image-height-help": "高于此数值大小的图片将会被拒绝", + "allow-topic-thumbnails": "允许用户上传主题缩略图", + "topic-thumb-size": "主题缩略图大小", + "allowed-file-extensions": "允许的文件扩展名", + "allowed-file-extensions-help": "在此处输入以逗号分隔的文件扩展名列表 (例如 pdf,xls,doc )。 为空则表示允许所有扩展名。", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "个人头像", + "allow-profile-image-uploads": "允许用户上传个人资料照片", + "convert-profile-image-png": "转换个人资料图片为 PNG", + "default-avatar": "访客默认头像", + "upload": "上传", + "profile-image-dimension": "个人资料相片尺寸", + "profile-image-dimension-help": "(使用 px 作为单位,默认:128px)", + "max-profile-image-size": "个人资料相片最大大小", + "max-profile-image-size-help": "(单位 KiB ,默认 256 KiB)", + "max-cover-image-size": "最大封面图片文件大小", + "max-cover-image-size-help": "(单位 KiB,默认 2048KiB)", + "keep-all-user-images": "在服务器上保留旧头像和旧的资料封面", + "profile-covers": "资料封面", + "default-covers": "默认封面图片", + "default-covers-help": "为没有上传封面图片的帐户添加以逗号分隔的默认封面图片" +} diff --git a/public/language/zh-CN/admin/settings/user.json b/public/language/zh-CN/admin/settings/user.json new file mode 100644 index 0000000000..f78cc552c4 --- /dev/null +++ b/public/language/zh-CN/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "验证", + "email-confirm-interval": "用户无法重新发送电子邮箱激活直到", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "允许使用何种登录名", + "allow-login-with.username-email": "用户名或者邮箱", + "allow-login-with.username": "仅限用户名", + "account-settings": "用户设置", + "gdpr_enabled": "启用通用数据保护条例(GDPR)许可的个人信息收集", + "gdpr_enabled_help": "当启用时,所有的新注册用户需要明确同意允许数据采集和在通用数据保护协议(GDPR)保护下的使用。注意:开启GDPR不一定要之前已经存在的用户同意。在这之前,您需要去安装GDPR插件。", + "disable-username-changes": "禁用修改用户名", + "disable-email-changes": "禁用修改邮箱", + "disable-password-changes": "禁用修改密码", + "allow-account-deletion": "允许消除帐号", + "hide-fullname": "隐藏用户的全名", + "hide-email": "隐藏用户的电子邮箱", + "show-fullname-as-displayname": "如果可以,把用户的全名作为他们的显示名称。", + "themes": "主题", + "disable-user-skins": "阻止用户选择自定义皮肤", + "account-protection": "帐号保护", + "admin-relogin-duration": "管理员无操作自动退出时长 (分钟)", + "admin-relogin-duration-help": "访问管理面板一段时间后需要重新登录以保证管理面板的安全,设置为0以禁用。", + "login-attempts": "每小时尝试登录次数", + "login-attempts-help": "如果用户的尝试登录次数超过此界限,该帐号将会被被锁定预设的时间。", + "lockout-duration": "帐户锁定时间(分钟)", + "login-days": "记录用户会话天数", + "password-expiry-days": "强制重置密码天数", + "session-time": "Session 过期时间", + "session-time-days": "天", + "session-time-seconds": "秒", + "session-time-help": "这些值将用于控制用户在登录时选中"记住我"后能够保持登录的时长。注意以下数值中只有一个将被使用。若值为空我们将回退使用。若值为空我们将使用默认值14天。", + "online-cutoff": "分钟后认为用户已离线", + "online-cutoff-help": "若用户在此时间后未作出任何动作,他们将被视为不活跃状态且不会收到实时更新。", + "registration": "用户注册", + "registration-type": "注册方式", + "registration-approval-type": "注册批准类型", + "registration-type.normal": "常规", + "registration-type.admin-approval": "管理员批准", + "registration-type.admin-approval-ip": "管理员批准 IP 地址", + "registration-type.invite-only": "仅限邀请", + "registration-type.admin-invite-only": "仅限管理员邀请", + "registration-type.disabled": "禁止注册", + "registration-type.help": "常规 - 用户可以在 /register 页面注册。
\n仅限邀请 - 用户可以在 用户 页面邀请其它用户。
\n仅限管理员邀请 - 只有管理员可以在 用户admin/manage/users 页面邀请其它用户。
\n禁止注册 - 不开放用户注册。
", + "registration-approval-type.help": "常规 - 用户可以直接注册。
\n管理员批准 - 用户的注册请求会被放入 请求队列 待管理员批准。
\n管理员批准 IP 地址 - 新用户不受影响,但 IP 地址已存在账号时需要管理员批准。
", + "registration-queue-auto-approve-time": "自动批准时间", + "registration-queue-auto-approve-time-help": "自动批准用户前所需的小时数。设置为 0 以禁用。", + "registration-queue-show-average-time": "向用户显示批准新用户所需的平均时间", + "registration.max-invites": "每个用户最大邀请数", + "max-invites": "每个用户最大邀请数", + "max-invites-help": "无限制填 0 。管理员没有邀请限制
仅在邀请制时可用", + "invite-expiration": "邀请过期", + "invite-expiration-help": "邀请在#日过期。", + "min-username-length": "最小用户名长度", + "max-username-length": "最大用户名长度", + "min-password-length": "最小密码长度", + "min-password-strength": "最小密码强度", + "max-about-me-length": "自我介绍的最大长度", + "terms-of-use": "论坛使用条款 (留空即可禁用)", + "user-search": "用户搜索", + "user-search-results-per-page": "展示的结果数量", + "default-user-settings": "默认用户设置", + "show-email": "显示邮箱", + "show-fullname": "显示全名", + "restrict-chat": "只允许我关注的用户给我发送聊天消息", + "outgoing-new-tab": "在新标签打开外部链接", + "topic-search": "启用主题内搜索", + "update-url-with-post-index": "浏览主题时更新 URL 中的帖子索引值", + "digest-freq": "订阅摘要", + "digest-freq.off": "关闭", + "digest-freq.daily": "每日", + "digest-freq.weekly": "每周", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "每月", + "email-chat-notifs": "当我不在线并收到新的聊天消息时,给我发送邮件通知", + "email-post-notif": "当我订阅的主题有新回复时,给我发送邮件通知", + "follow-created-topics": "关注您创建的主题", + "follow-replied-topics": "关注您回复的主题", + "default-notification-settings": "默认通知设置", + "categoryWatchState": "默认版块关注状态", + "categoryWatchState.watching": "已关注", + "categoryWatchState.notwatching": "未关注", + "categoryWatchState.ignoring": "已忽略" +} diff --git a/public/language/zh-CN/admin/settings/web-crawler.json b/public/language/zh-CN/admin/settings/web-crawler.json new file mode 100644 index 0000000000..aca41db4b6 --- /dev/null +++ b/public/language/zh-CN/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "爬虫抓取设置", + "robots-txt": "自定义 Robots.txt,留空以使用默认设置", + "sitemap-feed-settings": "站点地图与订阅设置", + "disable-rss-feeds": "禁用 RSS 订阅", + "disable-sitemap-xml": "禁用 Sitemap.xml", + "sitemap-topics": "要在 Stemap 中展示的主题数量", + "clear-sitemap-cache": "清除 Sitemap 缓存", + "view-sitemap": "查看 Sitemap" +} \ No newline at end of file diff --git a/public/language/zh-CN/category.json b/public/language/zh-CN/category.json new file mode 100644 index 0000000000..e9c577f68c --- /dev/null +++ b/public/language/zh-CN/category.json @@ -0,0 +1,23 @@ +{ + "category": "版块", + "subcategories": "子版块", + "new_topic_button": "发表主题", + "guest-login-post": "登录以发表", + "no_topics": "此版块还没有任何内容。
赶紧来发帖吧!", + "browsing": "正在浏览", + "no_replies": "尚无回复", + "no_new_posts": "没有新主题", + "watch": "关注", + "ignore": "忽略", + "watching": "已关注", + "not-watching": "未关注", + "ignoring": "已忽略", + "watching.description": "显示未读和最近的主题", + "not-watching.description": "不显示未读主题,显示最近主题", + "ignoring.description": "不显示未读和最近的主题", + "watching.message": "您关注了此版块和全部子版块的动态。", + "notwatching.message": "您未关注了此版块和全部子版块的动态。", + "ignoring.message": "您未关注此版块和全部子版块的动态。", + "watched-categories": "已关注的版块", + "x-more-categories": "%1 more categories" +} \ No newline at end of file diff --git a/public/language/zh-CN/email.json b/public/language/zh-CN/email.json new file mode 100644 index 0000000000..2da71ec95d --- /dev/null +++ b/public/language/zh-CN/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "测试邮件", + "password-reset-requested": "已申请密码重置!", + "welcome-to": "欢迎来到 %1", + "invite": "来自%1的邀请", + "greeting_no_name": "您好", + "greeting_with_name": "%1,您好", + "email.verify-your-email.subject": "请验证你的电子邮箱", + "email.verify.text1": "You've requested that we change or confirm your email address", + "email.verify.text2": "For security purposes, we only change or confirm the email address on file once its ownership has been confirmed via email. If you did not request this, no action is required on your part.", + "email.verify.text3": "一旦您验证了此电子邮箱地址,我们将会把您当前的电子邮箱地址替换为此电子邮箱地址(%1)。", + "welcome.text1": "感谢您注册 %1 帐户!", + "welcome.text2": "在您验证您绑定的邮箱地址之后,您的账户才能激活。", + "welcome.text3": "管理员批准了您的注册申请,您现在可以使用您的用户名和密码进行登录了。", + "welcome.cta": "点击这里确认您的电子邮箱地址", + "invitation.text1": "%1 邀请您加入 %2", + "invitation.text2": "您的邀请将在 %1 天后过期。", + "invitation.cta": "点击这里新建账号", + "reset.text1": "很可能是您忘记了密码,我们收到了重置您帐户密码的申请。 如果您没有申请密码重置,请忽略这封邮件。", + "reset.text2": "如需继续重置密码,请点击下面的链接:", + "reset.cta": "点击这里重置您的密码", + "reset.notify.subject": "更改密码成功", + "reset.notify.text1": "您在 %1 上的密码已经成功修改。", + "reset.notify.text2": "如果您没有授权此操作,请立即联系管理员。", + "digest.latest_topics": "来自 %1 的最新主题", + "digest.top-topics": "来自 %1 的关注主题", + "digest.popular-topics": "来自 %1 的热门主题 ", + "digest.cta": "点击这里访问 %1", + "digest.unsub.info": "根据您的订阅设置,为您发送此摘要。", + "digest.day": "天", + "digest.week": "周", + "digest.month": "月", + "digest.subject": "%1 的摘要", + "digest.title.day": "您的每日摘要", + "digest.title.week": "您的每周摘要", + "digest.title.month": "您的每月摘要", + "notif.chat.subject": "收到来自 %1 的新聊天消息", + "notif.chat.cta": "点击这里继续会话", + "notif.chat.unsub.info": "根据您的订阅设置,为您发送此聊天提醒。", + "notif.post.unsub.info": "根据您的订阅设置,为您发送此回帖提醒。", + "notif.post.unsub.one-click": "或者通过点击来取消订阅邮件", + "notif.cta": "点击这里前往论坛", + "notif.cta-new-reply": "查看帖子", + "notif.cta-new-chat": "查看聊天", + "notif.test.short": "测试通知", + "notif.test.long": "这是一个测试的通知邮件。", + "test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。", + "unsub.cta": "点击这里修改这些设置", + "unsubscribe": "退订", + "unsub.success": "您将不再收到来自%1邮寄名单的邮件", + "unsub.failure.title": "无法取消订阅", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "您在 %1 的账户已被封禁", + "banned.text1": "您在 %2 的账户 %1 已被封禁。", + "banned.text2": "本次封禁将在 %1 结束。", + "banned.text3": "这是您被封禁的原因:", + "closing": "谢谢!" +} \ No newline at end of file diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json new file mode 100644 index 0000000000..4517693259 --- /dev/null +++ b/public/language/zh-CN/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "无效数据", + "invalid-json": "无效 JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "您还没有登录。", + "account-locked": "您的帐号已被临时锁定", + "search-requires-login": "搜索功能仅限会员使用 - 请先登录或者注册。", + "goback": "按返回以后退至前一页", + "invalid-cid": "无效版块 ID", + "invalid-tid": "无效主题 ID", + "invalid-pid": "无效帖子 ID", + "invalid-uid": "无效用户 ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "请指定有效的日期", + "invalid-username": "无效用户名", + "invalid-email": "无效的电子邮箱", + "invalid-fullname": "无效全名", + "invalid-location": "无效位置", + "invalid-birthday": "无效生日", + "invalid-title": "无效的标题", + "invalid-user-data": "无效用户数据", + "invalid-password": "无效密码", + "invalid-login-credentials": "无效登录凭证", + "invalid-username-or-password": "请确认用户名和密码", + "invalid-search-term": "无效的搜索关键字", + "invalid-url": "无效的 URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "已禁用非管理账户的本地登录。", + "csrf-invalid": "可能是由于会话过期,登录失败。请重试。", + "invalid-path": "无效的路径", + "folder-exists": "文件夹已存在", + "invalid-pagination-value": "无效的分页数值,必须介于 %1 和 %2 之间", + "username-taken": "此用户名已被占用", + "email-taken": "此电子邮箱已被占用", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "已通过电子邮件进行邀请", + "email-not-confirmed": "您需要验证您的电子邮箱后才能在版块或主题中发布帖子,请点击此处以发送验证邮件。", + "email-not-confirmed-chat": "您的电子邮箱尚未确认,无法聊天,请点击这里确认您的电子邮箱。", + "email-not-confirmed-email-sent": "您的电子邮件账户尚未验证,请检查您的收件箱。在电子邮件帐户被验证前,您不能发帖和聊天。", + "no-email-to-confirm": "您的账号未设置电子邮箱。对于找回账号、聊天以及在版块中发布帖子这几项操作,电子邮箱是必需的。请点击此处输入电子邮箱。", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "我们无法确认您的电子邮箱,请重试", + "confirm-email-already-sent": "确认邮件已发出,如需重新发送请等待 %1 分钟后再试。", + "sendmail-not-found": "无法找到 sendmail 可执行程序,请确保 sendmail 已经安装并可被运行 NodeBB 的用户执行", + "digest-not-enabled": "此用户未开启摘要通知,或系统配置默认不发送摘要", + "username-too-short": "用户名太短", + "username-too-long": "用户名太长", + "password-too-long": "密码太长", + "reset-rate-limited": "太多密码重置请求(总数有限)", + "reset-same-password": "新的密码不能与当前使用的相同", + "user-banned": "用户已禁止", + "user-banned-reason": "抱歉,此帐号已经被封禁 (原因:%1)", + "user-banned-reason-until": "抱歉,此帐户已被封禁,直到%1(原因:%2)", + "user-too-new": "抱歉,您需要等待 %1 秒后,才可以发帖!", + "blacklisted-ip": "对不起,您的 IP 地址已被社区禁用。如果您认为这是一个错误,请与管理员联系。", + "ban-expiry-missing": "请提供此次禁言结束日期", + "no-category": "版块不存在", + "no-topic": "主题不存在", + "no-post": "帖子不存在", + "no-group": "群组不存在", + "no-user": "用户不存在", + "no-teaser": "主题预览不存在", + "no-flag": "Flag does not exist", + "no-privileges": "您没有权限执行此操作。", + "category-disabled": "版块已禁用", + "topic-locked": "主题已锁定", + "post-edit-duration-expired": "您只能在发表后 %1 秒内修改内容", + "post-edit-duration-expired-minutes": "您只能在发表后 %1 分钟内修改内容", + "post-edit-duration-expired-minutes-seconds": "您只能在发表后 %1 分 %2 秒内修改内容", + "post-edit-duration-expired-hours": "您只能在发表后 %1 小时后内修改内容", + "post-edit-duration-expired-hours-minutes": "您只能在发表后 %1 小时 %2 分钟内修改内容", + "post-edit-duration-expired-days": "您只能在发表后 %1 天内修改内容", + "post-edit-duration-expired-days-hours": "您只能在发表后 %1 天 %2 小时内修改内容", + "post-delete-duration-expired": "您只能在发表后 %1 秒内删除帖子", + "post-delete-duration-expired-minutes": "您只能在发表后 %1 分钟内删除帖子", + "post-delete-duration-expired-minutes-seconds": "您只能在发表发 %1 分 %2 秒内删除帖子", + "post-delete-duration-expired-hours": "您只能在发表后 %1 小时内删除帖子", + "post-delete-duration-expired-hours-minutes": "您只能在发表后 %1 小时 %2 分钟内删除帖子", + "post-delete-duration-expired-days": "您只能在发表后 %1 天内删除帖子", + "post-delete-duration-expired-days-hours": "您只能在发表后 %1 天 %2 小时内删除帖子", + "cant-delete-topic-has-reply": "您不能删除您的主题,因为已有回复。", + "cant-delete-topic-has-replies": "您不能删除您的主题,因为已有 %1 条回复。", + "content-too-short": "请增添发帖内容,不能少于 %1 个字符。", + "content-too-long": "请删减发帖内容,不能超过 %1 个字符。", + "title-too-short": "请增加标题,不能少于 %1 个字符。", + "title-too-long": "请缩减标题,不超过 %1 个字符。", + "category-not-selected": "未选择版块。", + "too-many-posts": "发帖需要间隔 %1 秒以上 - 请稍候再发帖", + "too-many-posts-newbie": "因为您是新用户,所以限制每隔 %1 秒才能发帖一次,直到您有 %2 点声望为止 —— 请稍候再发帖", + "already-posting": "You are already posting", + "tag-too-short": "标签太短,不能少于 %1 个字符", + "tag-too-long": "标签太长,不能超过 %1 个字符", + "not-enough-tags": "没有足够的标签。主题必须至少有 %1 个标签。", + "too-many-tags": "标签过多。主题不能拥有超过 %1 个标签。", + "cant-use-system-tag": "您不能使用此系统标签。", + "cant-remove-system-tag": "您不能移除此系统标签。", + "still-uploading": "请等待上传完成", + "file-too-big": "上传文件的大小限制为 %1 KB - 请缩减文件大小", + "guest-upload-disabled": "未登录用户不允许上传", + "cors-error": "由于CORS配置错误,无法上传图片。", + "upload-ratelimit-reached": "您在短时间内上传了过多的文件,请稍后再试", + "scheduling-to-past": "请选择一个未来的日期。", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "您已将此贴存为了书签", + "already-unbookmarked": "您已取消了此贴的书签", + "cant-ban-other-admins": "您不能封禁其他管理员!", + "cant-mute-other-admins": "您不能禁言其他管理员!", + "user-muted-for-hours": "您已被禁言,您在 %1 小时后才能发布内容", + "user-muted-for-minutes": "您已被禁言,您在 %1 分钟后才能发布内容", + "cant-make-banned-users-admin": "您不能让被禁止的用户成为管理员。", + "cant-remove-last-admin": "您是唯一的管理员。在删除您的管理员权限前,请添加另一个管理员。", + "account-deletion-disabled": "账号删除功能已禁用", + "cant-delete-admin": "在删除该账号之前,请先移除其管理权限。", + "already-deleting": "Already deleting", + "invalid-image": "无效的图片", + "invalid-image-type": "无效的图像类型。允许的类型有:%1", + "invalid-image-extension": "无效的图像扩展", + "invalid-file-type": "无效文件格式,允许的格式有:%1", + "invalid-image-dimensions": "图片尺寸太大", + "group-name-too-short": "群组名太短", + "group-name-too-long": "群组名太长", + "group-already-exists": "群组已存在", + "group-name-change-not-allowed": "不允许更改群组名称", + "group-already-member": "已经是此群组的成员", + "group-not-member": "不是此群组的成员", + "group-needs-owner": "群组需要指定至少一名群组所有者", + "group-already-invited": "您已邀请该用户", + "group-already-requested": "已提交您的请求", + "group-join-disabled": "您目前无法加入此群组", + "group-leave-disabled": "您目前无法离开此群组", + "post-already-deleted": "此帖已被删除", + "post-already-restored": "此帖已经恢复", + "topic-already-deleted": "此主题已被删除", + "topic-already-restored": "此主题已恢复", + "cant-purge-main-post": "无法清除主贴,请直接删除主题", + "topic-thumbnails-are-disabled": "主题缩略图已禁用", + "invalid-file": "无效文件", + "uploads-are-disabled": "上传已禁用", + "signature-too-long": "抱歉,您的签名不能超过 %1 个字符。", + "about-me-too-long": "抱歉,您的关于我不能超过 %1 个字符。", + "cant-chat-with-yourself": "您不能和自己聊天!", + "chat-restricted": "此用户限制了他的聊天消息。必须他先关注您,您才能和他聊天。", + "chat-disabled": "聊天系统已关闭", + "too-many-messages": "您发送了太多消息,请稍等片刻。", + "invalid-chat-message": "无效的聊天信息", + "chat-message-too-long": "聊天消息不能超过 %1  个字符。", + "cant-edit-chat-message": "您不能编辑这条信息", + "cant-delete-chat-message": "您不允许删除这条消息", + "chat-edit-duration-expired": "您只能在发布 %1 秒后修改聊天信息", + "chat-delete-duration-expired": "您只能在发布 %1 秒后删除聊天信息", + "chat-deleted-already": "聊天消息已经被删除", + "chat-restored-already": "此聊天消息已经恢复。\n", + "chat-room-does-not-exist": "聊天室不存在。", + "already-voting-for-this-post": "您已为此帖回复投过票了。", + "reputation-system-disabled": "声望系统已禁用。", + "downvoting-disabled": "踩已被禁用", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "您需要 %1 声望以进行踩操作", + "not-enough-reputation-to-flag": "您需要 %1 声望才能举报此帖子", + "not-enough-reputation-min-rep-website": "您需要 %1 声望以添加网站", + "not-enough-reputation-min-rep-aboutme": "您需要 %1 声望以设置关于我", + "not-enough-reputation-min-rep-signature": "您需要 %1 声望以添加签名档", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "您已举报此帖", + "user-already-flagged": "您已举报此用户", + "post-flagged-too-many-times": "此贴已被其他用户举报", + "user-flagged-too-many-times": "此用户已被其他用户举报", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "您不能对您自己的帖子投票", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "您每天只能踩 %1 次", + "too-many-downvotes-today-user": "您每天只能对一个用户踩 %1 次", + "reload-failed": "刷新 NodeBB 时遇到问题: \"%1\"。NodeBB 保持给已连接的客户端服务,您应该撤销刷新前做的更改。", + "registration-error": "注册错误", + "parse-error": "服务器响应解析出错", + "wrong-login-type-email": "请输入您的电子邮箱地址登录", + "wrong-login-type-username": "请输入您的用户名登录", + "sso-registration-disabled": "已禁用通过 %1 账户的注册, 请使用邮箱地址注册", + "sso-multiple-association": "您无法将此服务中的多个账户关联到您的NodeBB账号。请您分离的现有账号并重试。", + "invite-maximum-met": "您的邀请人数超出了上限 (%1 超过了 %2)。", + "no-session-found": "未登录!", + "not-in-room": "用户已不在聊天室中", + "cant-kick-self": "您不能把自己踢出群组", + "no-users-selected": "尚未选择用户", + "invalid-home-page-route": "无效的首页路径", + "invalid-session": "无效的会话", + "invalid-session-text": "您的登录会话似乎不再处于活动状态。请刷新此页面。", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "没有主题被选中!", + "cant-move-to-same-topic": "无法将帖子移动到相同的主题中!", + "cant-move-topic-to-same-category": "无法将主题移动到相同的版块!", + "cannot-block-self": "您不能把自己屏蔽!", + "cannot-block-privileged": "您不能屏蔽管理员或者全局版主", + "cannot-block-guest": "游客无法屏蔽其他用户", + "already-blocked": "此用户已被屏蔽", + "already-unblocked": "此用户已被取消屏蔽", + "no-connection": "您的网络连接似乎存在问题", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "无法安装插件 – 只有被NodeBB包管理器列入白名单的插件才能通过ACP安装。", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "无法将子版块设置为父版块", + "cant-set-self-as-parent": "无法将自身设置为父版块", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "找不到有效的登录会话。请登录后再试。", + "api.403": "You are not authorised to make this call", + "api.404": "无效 API 调用", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "您在短时间内发出了过多的请求,请稍后再试", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/zh-CN/flags.json b/public/language/zh-CN/flags.json new file mode 100644 index 0000000000..58cbf02a5c --- /dev/null +++ b/public/language/zh-CN/flags.json @@ -0,0 +1,89 @@ +{ + "state": "状态", + "reports": "报告", + "first-reported": "首次报告", + "no-flags": "啊哈!没发现任何的举报。", + "assignee": "受委托人", + "update": "更新", + "updated": "已更新", + "resolved": "Resolved", + "target-purged": "被举报的内容已经被清除,不再可用。", + + "graph-label": "日举报", + "quick-filters": "快速过滤器", + "filter-active": "该列中有一个或更多激活的过滤器", + "filter-reset": "删除过滤器", + "filters": "过滤器选项", + "filter-reporterId": "举报者UID", + "filter-targetUid": "被举报者 UID", + "filter-type": "举报类型", + "filter-type-all": "所有内容", + "filter-type-post": "帖子", + "filter-type-user": "用户", + "filter-state": "状态", + "filter-assignee": "受委托人 UID", + "filter-cid": "版块", + "filter-quick-mine": "委托给我", + "filter-cid-all": "全部版块", + "apply-filters": "应用过滤器", + "more-filters": "更多过滤器", + "fewer-filters": "Fewer Filters", + + "quick-actions": "快速操作", + "flagged-user": "被举报的用户", + "view-profile": "查看个人资料", + "start-new-chat": "开始新会话", + "go-to-target": "查看举报目标", + "assign-to-me": "指派给我", + "delete-post": "删除帖子", + "purge-post": "清除帖子", + "restore-post": "恢复帖子", + "delete": "Delete Flag", + + "user-view": "查看资料", + "user-edit": "编辑资料", + + "notes": "举报备注", + "add-note": "添加备注", + "no-notes": "没有共享的备注内容。", + "delete-note-confirm": "您确定要删除此举报备注吗?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "备注已添加", + "note-deleted": "备注已删除", + "flag-deleted": "Flag Deleted", + + "history": "账号 & 举报历史", + "no-history": "没有举报历史。", + + "state-all": "所有状态", + "state-open": "新建/打开", + "state-wip": "正在处理", + "state-resolved": "已解决", + "state-rejected": "已拒绝", + "no-assignee": "未指派", + + "sort": "排序", + "sort-newest": "最新", + "sort-oldest": "最旧", + "sort-reports": "最多举报", + "sort-all": "全部举报类型", + "sort-posts-only": "仅限帖子", + "sort-downvotes": "最多踩", + "sort-upvotes": "最多顶", + "sort-replies": "最多回复", + + "modal-title": "举报内容", + "modal-body": "请选择或者输入您举报 %1%2 的原因以便版主进行审核。", + "modal-reason-spam": "垃圾信息", + "modal-reason-offensive": "人身攻击", + "modal-reason-other": "其他(请在下方指定)", + "modal-reason-custom": "举报此内容的理由……", + "modal-submit": "提交举报", + "modal-submit-success": "已举报此内容。", + + "bulk-actions": "批量操作", + "bulk-resolve": "处理举报", + "bulk-success": "已更新%1个举报", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/zh-CN/global.json b/public/language/zh-CN/global.json new file mode 100644 index 0000000000..cd05e59b55 --- /dev/null +++ b/public/language/zh-CN/global.json @@ -0,0 +1,126 @@ +{ + "home": "主页", + "search": "搜索", + "buttons.close": "关闭", + "403.title": "禁止访问", + "403.message": "您似乎没有访问此页面的权限。", + "403.login": "或许您应该先 登录试试?", + "404.title": "未找到", + "404.message": "您访问的页面不存在。返回首页。", + "500.title": "内部错误", + "500.message": "哎呀!看来是哪里出错了!", + "400.title": "错误的请求", + "400.message": "看起来这个链接的格式不正确,请再次检查并重试。或者返回主页。", + "register": "注册", + "login": "登录", + "please_log_in": "请登录", + "logout": "退出", + "posting_restriction_info": "仅限于注册会员发帖,点这里登录。", + "welcome_back": "欢迎回来", + "you_have_successfully_logged_in": "您已成功登录", + "save_changes": "保存更改", + "save": "保存", + "close": "关闭", + "pagination": "分页", + "pagination.out_of": "%1 / %2", + "pagination.enter_index": "跳转到帖子", + "header.admin": "管理", + "header.categories": "版块", + "header.recent": "最新", + "header.unread": "未读", + "header.tags": "标签", + "header.popular": "热门", + "header.top": "顶端", + "header.users": "用户", + "header.groups": "群组", + "header.chats": "聊天", + "header.notifications": "通知", + "header.search": "搜索", + "header.profile": "设置", + "header.navigation": "导航", + "notifications.loading": "正在加载通知", + "chats.loading": "正在加载聊天", + "motd.welcome": "欢迎来到 NodeBB,未来的社区论坛平台。", + "previouspage": "上一页", + "nextpage": "下一页", + "alert.success": "成功", + "alert.error": "错误", + "alert.banned": "已封禁", + "alert.banned.message": "您已被禁止,您当前的访问受到限制。", + "alert.unbanned": "已解封", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "您已取消关注 %1!", + "alert.follow": "您已关注 %1!", + "users": "用户", + "topics": "主题", + "posts": "帖子", + "x-posts": "%1 个帖子", + "best": "最佳", + "controversial": "有争议的", + "votes": "赞同", + "x-votes": "%1 个投票", + "voters": "投票人", + "upvoters": "顶的人", + "upvoted": "顶", + "downvoters": "踩的人", + "downvoted": "踩", + "views": "浏览", + "posters": "发布者", + "reputation": "声望", + "lastpost": "上一个帖子", + "firstpost": "第一个帖子", + "read_more": "阅读更多", + "more": "更多", + "none": "无", + "posted_ago_by_guest": "游客发布于 %1", + "posted_ago_by": "%2 发布于 %1", + "posted_ago": "发布于 %1", + "posted_in": "发布在 %1", + "posted_in_by": "%2 发布于 %1", + "posted_in_ago": "于 %2 发布到 %1 版", + "posted_in_ago_by": "%3 于 %1 发布到 %2", + "user_posted_ago": "%1 发布于 %2", + "guest_posted_ago": "游客发布于 %1", + "last_edited_by": "最后由 %1 编辑", + "norecentposts": "暂无新帖", + "norecenttopics": "暂无新主题", + "recentposts": "新帖", + "recentips": "最近登录的 IP", + "moderator_tools": "版主工具", + "online": "在线", + "away": "离开", + "dnd": "请勿打扰", + "invisible": "隐身", + "offline": "离线", + "email": "电子邮箱", + "language": "语言", + "guest": "游客", + "guests": "游客", + "former_user": "老用户", + "system-user": "系统", + "unknown-user": "未知用户", + "updated.title": "论坛已更新", + "updated.message": "论坛已更新。请点这里刷新页面。", + "privacy": "隐私", + "follow": "关注", + "unfollow": "取消关注", + "delete_all": "全部删除", + "map": "地图", + "sessions": "已登录的会话", + "ip_address": "IP 地址", + "enter_page_number": "输入页码", + "upload_file": "上传文件", + "upload": "上传", + "uploads": "上传", + "allowed-file-types": "允许的文件类型有 %1", + "unsaved-changes": "您有未保存的更改,您确定您要离开么?", + "reconnecting-message": "与 %1 的连接断开,我们正在尝试重连,请耐心等待", + "play": "播放", + "cookies.message": "此网站使用 Cookies 以保障您在我们网站的最佳体验。", + "cookies.accept": "知道了!", + "cookies.learn_more": "了解更多", + "edited": "已编辑", + "disabled": "禁用", + "select": "选择", + "user-search-prompt": "输入以查找用户" +} \ No newline at end of file diff --git a/public/language/zh-CN/groups.json b/public/language/zh-CN/groups.json new file mode 100644 index 0000000000..051c9e5c13 --- /dev/null +++ b/public/language/zh-CN/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "群组", + "view_group": "查看群组", + "owner": "群组所有者", + "new_group": "创建群组", + "no_groups_found": "尚无群组信息", + "pending.accept": "接受", + "pending.reject": "拒绝", + "pending.accept_all": "全部同意", + "pending.reject_all": "全部拒绝", + "pending.none": "暂时没有待加入的成员", + "invited.none": "暂时没有接受邀请的成员", + "invited.uninvite": "取消邀请", + "invited.search": "选择用户加入群组", + "invited.notification_title": "您已被邀请加入 %1", + "request.notification_title": "来自 %1 的群组成员请求", + "request.notification_text": "%1 已被邀请加入 %2", + "cover-save": "保存", + "cover-saving": "正在保存", + "details.title": "群组信息", + "details.members": "成员列表", + "details.pending": "待加入成员", + "details.invited": "已邀请成员", + "details.has_no_posts": "此群组的用户尚未发表任何帖子。", + "details.latest_posts": "最新帖子", + "details.private": "私有", + "details.disableJoinRequests": "禁止申请加入群组", + "details.disableLeave": "禁止用户离开群组", + "details.grant": "授予/取消管理权", + "details.kick": "踢出群组", + "details.kick_confirm": "您确定要将此成员从群组中移除吗?", + "details.add-member": "添加成员", + "details.owner_options": "群组管理", + "details.group_name": "群组名", + "details.member_count": "群组成员数", + "details.creation_date": "创建时间", + "details.description": "描述", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "徽章预览", + "details.change_icon": "更改图标", + "details.change_label_colour": "更改标签颜色", + "details.change_text_colour": "更改文本颜色", + "details.badge_text": "徽章文本", + "details.userTitleEnabled": "显示徽章", + "details.private_help": "启用此选项后,加入群组需要组长审批。", + "details.hidden": "隐藏", + "details.hidden_help": "启用此选项后,群组将不在群组列表中展现,成员只能通过邀请加入。", + "details.delete_group": "删除群组", + "details.private_system_help": "系统禁用了私有群组,这个选项不起任何作用", + "event.updated": "群组信息已更新", + "event.deleted": "群组 \"%1\" 已被删除", + "membership.accept-invitation": "接受邀请", + "membership.accept.notification_title": "你现在是 %1的成员了", + "membership.invitation-pending": "邀请中", + "membership.join-group": "加入群组", + "membership.leave-group": "退出群组", + "membership.leave.notification_title": "%1 退出了群组:%2", + "membership.reject": "拒绝", + "new-group.group_name": "群组名: ", + "upload-group-cover": "上传群组封面", + "bulk-invite-instructions": "输入您要邀请加入此群组的用户名,多个用户以逗号分隔", + "bulk-invite": "批量邀请", + "remove_group_cover_confirm": "确定要移除封面图片吗?" +} \ No newline at end of file diff --git a/public/language/zh-CN/ip-blacklist.json b/public/language/zh-CN/ip-blacklist.json new file mode 100644 index 0000000000..6193d4b4ac --- /dev/null +++ b/public/language/zh-CN/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "在此配置 IP 黑名单", + "description": "有时,一份账号封禁并不足以作为威慑。更多的时候,限制有权浏览论坛的具体 IP 或者一个 IP 范围这一行为可以更好地保护论坛。在以上情况下,您可以添加一些令人厌恶者的 IP 地址或者 CIDR 地址块到此黑名单,此后他们(被加入黑名单者)将被阻止进行登录或者注册新账号的行为。", + "active-rules": "生效规则", + "validate": "验证黑名单", + "apply": "应用黑名单", + "hints": "格式建议", + "hint-1": "每行定义一个独立 IP 地址。您可以添加 IP 块,只要它们满足 CIDR 格式(e.g. 192.168.100.0/22)。", + "hint-2": "您可以通过以#标志开头的行来添加注释。", + + "validate.x-valid": "%1 / %2的规则有效。", + "validate.x-invalid": "下列 %0 个规则无效:", + + "alerts.applied-success": "黑名单生效", + + "analytics.blacklist-hourly": "图 1 – 每小时触发黑名单数", + "analytics.blacklist-daily": "图 2– 每日触发黑名单数", + "ip-banned": "已封禁IP" +} \ No newline at end of file diff --git a/public/language/zh-CN/language.json b/public/language/zh-CN/language.json new file mode 100644 index 0000000000..251bc4ef91 --- /dev/null +++ b/public/language/zh-CN/language.json @@ -0,0 +1,5 @@ +{ + "name": "简体中文", + "code": "zh-CN", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/zh-CN/login.json b/public/language/zh-CN/login.json new file mode 100644 index 0000000000..108eda5b41 --- /dev/null +++ b/public/language/zh-CN/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "用户名 / 邮箱", + "username": "用户名", + "remember_me": "保持登录信息", + "forgot_password": "忘记密码?", + "alternative_logins": "使用合作网站帐号登录", + "failed_login_attempt": "登录失败", + "login_successful": "您已成功登录!", + "dont_have_account": "没有帐号?", + "logged-out-due-to-inactivity": "由于长时间不活动,您的账号已被管理员从控制面板中注销", + "caps-lock-enabled": "大写锁定已启用" +} \ No newline at end of file diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json new file mode 100644 index 0000000000..3dff397d9d --- /dev/null +++ b/public/language/zh-CN/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "与聊天", + "chat.placeholder": "在这里输入聊天消息,或者拖入图片,按下回车键以发送", + "chat.scroll-up-alert": "您正在查看较旧的消息,点击此处转到最新消息。", + "chat.send": "发送", + "chat.no_active": "暂无聊天", + "chat.user_typing": "%1 正在输入……", + "chat.user_has_messaged_you": "%1 向您发送了消息。", + "chat.see_all": "全部对话", + "chat.mark_all_read": "标记全部已读", + "chat.no-messages": "请选择接收人,以查看聊天消息历史", + "chat.no-users-in-room": "此聊天室中没有用户", + "chat.recent-chats": "最近聊天", + "chat.contacts": "联系人", + "chat.message-history": "消息历史", + "chat.message-deleted": "消息已删除", + "chat.options": "聊天配置", + "chat.pop-out": "弹出聊天窗口", + "chat.minimize": "最小化", + "chat.maximize": "最大化", + "chat.seven_days": "7天", + "chat.thirty_days": "30天", + "chat.three_months": "3个月", + "chat.delete_message_confirm": "您确定删除此消息吗?", + "chat.retrieving-users": "查找用户", + "chat.manage-room": "管理聊天室", + "chat.add-user-help": "在这里查找更多用户。选中之后添加到聊天中,新用户在加入聊天之前看不到聊天消息。只有聊天室所有者()可以从聊天室中移除用户。", + "chat.confirm-chat-with-dnd-user": "该用户已将其状态设置为 DnD(请勿打扰)。 您仍希望与其聊天吗?", + "chat.rename-room": "重命名房间", + "chat.rename-placeholder": "在这里输入房间名字", + "chat.rename-help": "这里设置的房间名字能够被房间内所有人都看到。", + "chat.leave": "离开聊天室", + "chat.leave-prompt": "您确定要离开聊天室?", + "chat.leave-help": "离开此聊天会将您在聊天中的未接收的消息移除。您在重新加入之后不会看到任何聊天记录", + "chat.in-room": "在此房间", + "chat.kick": "踢出", + "chat.show-ip": "显示 IP", + "chat.owner": "房间所有者", + "chat.system.user-join": "%1 加入了房间", + "chat.system.user-leave": "%1 离开了房间", + "chat.system.room-rename": "%2 更改房间名为:%1", + "composer.compose": "编写帮助", + "composer.show_preview": "显示预览", + "composer.hide_preview": "隐藏预览", + "composer.user_said_in": "%1 在 %2 中说:", + "composer.user_said": "%1 说:", + "composer.discard": "确定想要取消此帖?", + "composer.submit_and_lock": "提交并锁定", + "composer.toggle_dropdown": "标为 Dropdown", + "composer.uploading": "正在上传 %1", + "composer.formatting.bold": "加粗", + "composer.formatting.italic": "倾斜", + "composer.formatting.list": "列表", + "composer.formatting.strikethrough": "删除线", + "composer.formatting.code": "代码", + "composer.formatting.link": "链接", + "composer.formatting.picture": "图片链接", + "composer.upload-picture": "上传图片", + "composer.upload-file": "上传文件", + "composer.zen_mode": "无干扰模式", + "composer.select_category": "选择一个版块", + "composer.textarea.placeholder": "在此处输入您的帖子内容,拖放图像", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "取消定时", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "确认", + "bootbox.cancel": "取消", + "bootbox.confirm": "确认", + "bootbox.submit": "提交", + "bootbox.send": "发送", + "cover.dragging_title": "设置封面照片位置", + "cover.dragging_message": "拖拽封面照片到期望的位置,然后点击“保存”", + "cover.saved": "封面照片和位置已保存", + "thumbs.modal.title": "管理主题缩略图", + "thumbs.modal.no-thumbs": "没有找到缩略图。", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "添加缩略图", + "thumbs.modal.remove": "移除缩略图", + "thumbs.modal.confirm-remove": "您确定要移除此缩略图吗?" +} \ No newline at end of file diff --git a/public/language/zh-CN/notifications.json b/public/language/zh-CN/notifications.json new file mode 100644 index 0000000000..cfd4b7fd3d --- /dev/null +++ b/public/language/zh-CN/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "通知", + "no_notifs": "您没有新的通知", + "see_all": "全部通知", + "mark_all_read": "标记全部已读", + "back_to_home": "返回 %1", + "outgoing_link": "站外链接", + "outgoing_link_message": "您正在离开 %1", + "continue_to": "继续前往 %1", + "return_to": "返回 %1", + "new_notification": "您有一个新的通知", + "you_have_unread_notifications": "您有未读的通知。", + "all": "所有", + "topics": "主题", + "replies": "回复", + "chat": "聊天", + "group-chat": "Group Chats", + "follows": "关注", + "upvote": "顶", + "new-flags": "新举报", + "my-flags": "指派举报给我", + "bans": "封禁", + "new_message_from": "来自 %1 的新消息", + "upvoted_your_post_in": "%1%2 点赞了您的帖子。", + "upvoted_your_post_in_dual": "%1%2%3 赞了您的帖子。", + "upvoted_your_post_in_multiple": "%1 和 %2 个其他人在 %3 赞了您的帖子。", + "moved_your_post": "您的帖子已被 %1 移动到了 %2", + "moved_your_topic": "%1 移动了 %2", + "user_flagged_post_in": "%1%2 标记了一个帖子", + "user_flagged_post_in_dual": "%1%2%3 举报了一个帖子", + "user_flagged_post_in_multiple": "%1 和 %2 个其他人在 %3 举报了一个帖子", + "user_flagged_user": "%1 举报了 (%2) 的用户资料", + "user_flagged_user_dual": "%1%2 举报了 (%3) 的用户资料", + "user_flagged_user_multiple": "%1 和其他 %2 人举报了 (%3) 的用户资料", + "user_posted_to": "%1 回复了:%2", + "user_posted_to_dual": "%1%2 回复了: %3", + "user_posted_to_multiple": "%1 和 %2 个其他人回复了: %3", + "user_posted_topic": "%1 发表了新主题:%2", + "user_edited_post": "%1%2 编辑了一个帖子", + "user_started_following_you": "%1关注了您。", + "user_started_following_you_dual": "%1%2 关注了您。", + "user_started_following_you_multiple": "%1 和 %2 个其他人关注了您。", + "new_register": "%1 发出了注册请求", + "new_register_multiple": "有 %1 条注册申请等待批准。", + "flag_assigned_to_you": "举报 %1 已经被指派给您", + "post_awaiting_review": "请求查验帖子", + "profile-exported": "%1资料已导出,点击下载", + "posts-exported": "%1帖子已导出,点击下载", + "uploads-exported": "%1上传已导出,点击下载", + "users-csv-exported": "用户列表 CSV 已导出,点击以下载", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "电子邮箱已确认", + "email-confirmed-message": "感谢您验证您的电子邮箱。您的帐户现已完全激活。", + "email-confirm-error-message": "验证的您电子邮箱地址时出现了问题。可能是因为验证码无效或已过期。", + "email-confirm-sent": "确认邮件已发送。", + "none": "无", + "notification_only": "用通知提醒我", + "email_only": "用邮件提醒我", + "notification_and_email": "同时使用 通知 和 邮件 提醒我", + "notificationType_upvote": "当有人顶了我的帖子时", + "notificationType_new-topic": "当有人回复我的帖子时", + "notificationType_new-reply": "当您正在查看的主题中有新回复时", + "notificationType_post-edit": "当您关注的主题有帖子被编辑时", + "notificationType_follow": "当有人关注您时", + "notificationType_new-chat": "当您收到聊天消息时", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "当您收到群组邀请时", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "当有人请求加入您拥有的用户组时", + "notificationType_new-register": "当有人被添加到申请队列时", + "notificationType_post-queue": "当有新帖子等待审核时", + "notificationType_new-post-flag": "当有新的帖子举报时", + "notificationType_new-user-flag": "当有新的用户信息举报时" +} \ No newline at end of file diff --git a/public/language/zh-CN/pages.json b/public/language/zh-CN/pages.json new file mode 100644 index 0000000000..7799ed07a4 --- /dev/null +++ b/public/language/zh-CN/pages.json @@ -0,0 +1,65 @@ +{ + "home": "主页", + "unread": "未读", + "popular-day": "今日热门话题", + "popular-week": "本周热门话题", + "popular-month": "当月热门话题", + "popular-alltime": "热门主题", + "recent": "最新主题", + "top-day": "今日得票数最高的主题", + "top-week": "本周得票数最高的主题", + "top-month": "本月票数最高的主题", + "top-alltime": "票数最高的主题", + "moderator-tools": "版主工具", + "flagged-content": "举报管理", + "ip-blacklist": "IP 黑名单", + "post-queue": "提交列表", + "users/online": "在线用户", + "users/latest": "最新用户", + "users/sort-posts": "发帖最多的用户", + "users/sort-reputation": "积分最多的用户", + "users/banned": "被封禁的用户", + "users/most-flags": "被举报次数最多的用户", + "users/search": "用户搜索", + "notifications": "提醒", + "tags": "标签", + "tag": "标签为“%1”的主题", + "register": "注册帐号", + "registration-complete": "注册完成", + "login": "登录帐号", + "reset": "重置帐户密码", + "categories": "版块", + "groups": "群组", + "group": "%1 的群组", + "chats": "聊天", + "chat": "与 %1 聊天", + "flags": "举报", + "flag-details": "举报 %1 详情", + "account/edit": "正在编辑 \"%1\"", + "account/edit/password": "正在编辑 \"%1\" 的密码", + "account/edit/username": "正在编辑 \"%1\" 的用户名", + "account/edit/email": "正在编辑 \"%1\" 的电子邮箱", + "account/info": "账户信息", + "account/following": "%1 关注", + "account/followers": "关注 %1 的人", + "account/posts": "%1 发布的帖子", + "account/latest-posts": "%1 发布的最新帖子", + "account/topics": "%1 创建的主题", + "account/groups": "%1 的群组", + "account/watched_categories": "%1 关注的版块", + "account/bookmarks": "%1 收藏的帖子", + "account/settings": "用户设置", + "account/watched": "主题已被 %1 关注", + "account/ignored": "主题已被 %1 忽略", + "account/upvoted": "帖子被 %1 顶过", + "account/downvoted": "帖子被 %1 踩过", + "account/best": "%1 发布的最佳帖子", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "%1 屏蔽的用户", + "account/uploads": "%1 上传的文件", + "account/sessions": "已登录的会话", + "confirm": "电子邮箱已确认", + "maintenance.text": "%1 正在进行维护。请稍后再来。", + "maintenance.messageIntro": "此外,管理员留下的消息:", + "throttled.text": "%1 因负荷超载暂不可用。请稍后再来。" +} \ No newline at end of file diff --git a/public/language/zh-CN/post-queue.json b/public/language/zh-CN/post-queue.json new file mode 100644 index 0000000000..bfcebbcd04 --- /dev/null +++ b/public/language/zh-CN/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "发布队列", + "description": "发布队列中暂无新帖。启用此特性,请前往设置→发布→发布队列,然后启用发布队列。", + "user": "用户", + "category": "版块", + "title": "标题", + "content": "内容", + "posted": "发布", + "reply-to": "回复\"%1\"", + "content-editable": "点击内容开始编辑", + "category-editable": "点击版块开始编辑", + "title-editable": "点击标题开始编辑", + "reply": "回复", + "topic": "主题", + "accept": "接受", + "reject": "拒绝", + "remove": "移除", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "你想要拒绝这个帖子吗?", + "bulk-actions": "批量操作", + "accept-all": "全部同意", + "accept-selected": "同意选中项", + "reject-all": "全部拒绝", + "reject-all-confirm": "你想要拒绝所有帖子吗?", + "reject-selected": "拒绝选中项", + "reject-selected-confirm": "您确定要拒绝%1个选择的帖子吗?", + "bulk-accept-success": "%1个帖子已接受", + "bulk-reject-success": "%1个帖子已拒绝" +} \ No newline at end of file diff --git a/public/language/zh-CN/recent.json b/public/language/zh-CN/recent.json new file mode 100644 index 0000000000..ce54404934 --- /dev/null +++ b/public/language/zh-CN/recent.json @@ -0,0 +1,19 @@ +{ + "title": "最新", + "day": "日", + "week": "周", + "month": "月", + "year": "年", + "alltime": "总计", + "no_recent_topics": "暂无主题。", + "no_popular_topics": "暂无热门主题。", + "there-is-a-new-topic": "共计 1 个新主题。", + "there-is-a-new-topic-and-a-new-post": "共计 1 个新主题和 1 个新回复。", + "there-is-a-new-topic-and-new-posts": "共计 1 个新主题和 %1 个新回复。", + "there-are-new-topics": "共计 %1 个新主题。", + "there-are-new-topics-and-a-new-post": "共计 %1 个新主题和 1 个新回复。", + "there-are-new-topics-and-new-posts": "共计 %1 个新主题和 %2 个新回复。", + "there-is-a-new-post": "共计 1 个新回复。", + "there-are-new-posts": "共计 %1 个新回复。", + "click-here-to-reload": "点击这里重新加载。" +} \ No newline at end of file diff --git a/public/language/zh-CN/register.json b/public/language/zh-CN/register.json new file mode 100644 index 0000000000..e24060429c --- /dev/null +++ b/public/language/zh-CN/register.json @@ -0,0 +1,32 @@ +{ + "register": "注册", + "cancel_registration": "取消注册", + "help.email": "默认情况下,您的电子邮箱不会公开。", + "help.username_restrictions": "全局唯一的用户名,长度 %1 到 %2 个字。其他人可以使用 @用户名 提及您。", + "help.minimum_password_length": "您的密码长度必须不少于 %1 个字。", + "email_address": "电子邮箱地址", + "email_address_placeholder": "输入电子邮箱地址", + "username": "用户名", + "username_placeholder": "输入用户名", + "password": "密码", + "password_placeholder": "输入密码", + "confirm_password": "确认密码", + "confirm_password_placeholder": "再次输入密码", + "register_now_button": "立即注册", + "alternative_registration": "其他方式注册", + "terms_of_use": "使用条款", + "agree_to_terms_of_use": "我同意使用条款", + "terms_of_use_error": "您必须同意使用条款", + "registration-added-to-queue": "您的注册正在等待批准。一旦通过,管理员会发送邮件通知您。", + "registration-queue-average-time": "我们通常的注册批准时间为 %1 小时 %2 分钟。", + "registration-queue-auto-approve-time": "您在此论坛的帐号将会在最迟 %1  小时后被完全激活。", + "interstitial.intro": "我们需要一些额外信息以更新您的账号。", + "interstitial.intro-new": "我们需要一些额外信息以创建您的账号。", + "interstitial.errors-found": "请检查输入的信息:", + "gdpr_agree_data": "我同意此网站对我个人信息的收集与处理。", + "gdpr_agree_email": "我同意此网站向我发送摘要和通知邮件。", + "gdpr_consent_denied": "您需要同意此网站收集与处理您的个人信息,以及向您发送电子邮件。", + "invite.error-admin-only": "开放的用户注册已被禁用,详情请联系管理员。", + "invite.error-invite-only": "开放的用户注册已被禁用,您必须被其他用户邀请才能访问此论坛。", + "invite.error-invalid-data": "您的注册资料与我们的记录不符,详情请联系管理员。" +} \ No newline at end of file diff --git a/public/language/zh-CN/reset_password.json b/public/language/zh-CN/reset_password.json new file mode 100644 index 0000000000..db11fed65c --- /dev/null +++ b/public/language/zh-CN/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "重置密码", + "update_password": "更新密码", + "password_changed.title": "密码已更改", + "password_changed.message": "

密码重置成功,请重新登录。", + "wrong_reset_code.title": "重置验证码不正确", + "wrong_reset_code.message": "您输入的重置验证码有误,请重新输入,或者申请新的重置验证码。", + "new_password": "新密码", + "repeat_password": "验证密码", + "changing_password": "正在更改密码", + "enter_email": "请输入您的电子邮箱地址,我们将会发送一份邮件协助您重置账号密码。", + "enter_email_address": "输入邮箱地址", + "password_reset_sent": "如果指定的邮件地址关联到已存在的用户账号,该账号将收到一条密码重置邮件,请注意该邮件一分钟内只发送一次", + "invalid_email": "无效的电子邮箱/电子邮箱不存在!", + "password_too_short": "密码太短,请选择其他密码。", + "passwords_do_not_match": "您输入两个密码不一致。", + "password_expired": "您的密码已过期,请选择新密码" +} \ No newline at end of file diff --git a/public/language/zh-CN/search.json b/public/language/zh-CN/search.json new file mode 100644 index 0000000000..b2926eaa69 --- /dev/null +++ b/public/language/zh-CN/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "共 %1 条结果匹配 \"%2\",(耗时 %3 秒)", + "no-matches": "无匹配结果", + "advanced-search": "高级搜索", + "in": "在", + "titles": "标题", + "titles-posts": "标题和回帖", + "match-words": "匹配字符", + "all": "所有", + "any": "任何", + "posted-by": "发表", + "in-categories": "在版块", + "search-child-categories": "搜索子版块", + "has-tags": "有标签", + "reply-count": "回复数", + "at-least": "至少", + "at-most": "至多", + "relevance": "关联", + "post-time": "发帖时间", + "votes": "赞同数", + "newer-than": "晚于", + "older-than": "早于", + "any-date": "任何日期", + "yesterday": "昨天", + "one-week": "一周", + "two-weeks": "两周", + "one-month": "一个月", + "three-months": "三个月", + "six-months": "六个月", + "one-year": "一年", + "sort-by": "排序", + "last-reply-time": "最后回复时间", + "topic-title": "主题标题", + "topic-votes": "主题赞同数", + "number-of-replies": "回帖数", + "number-of-views": "查看数", + "topic-start-date": "主题开始日期", + "username": "用户名", + "category": "版块", + "descending": "倒序", + "ascending": "顺序", + "save-preferences": "保存设置", + "clear-preferences": "清除设置", + "search-preferences-saved": "搜索设置已保存", + "search-preferences-cleared": "搜索设置已清除", + "show-results-as": "结果显示为", + "see-more-results": "查看更多结果 (%1)", + "search-in-category": "在\"%1\"中搜索" +} \ No newline at end of file diff --git a/public/language/zh-CN/success.json b/public/language/zh-CN/success.json new file mode 100644 index 0000000000..951863f0a6 --- /dev/null +++ b/public/language/zh-CN/success.json @@ -0,0 +1,7 @@ +{ + "success": "成功", + "topic-post": "您已成功发布。", + "post-queued": "你的帖子正在等待批准。帖子被批准或者拒绝的时候,你会收到一个通知。", + "authentication-successful": "验证成功", + "settings-saved": "设置已保存!" +} \ No newline at end of file diff --git a/public/language/zh-CN/tags.json b/public/language/zh-CN/tags.json new file mode 100644 index 0000000000..11f336cb03 --- /dev/null +++ b/public/language/zh-CN/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "此标签还没有主题帖。", + "tags": "标签", + "enter_tags_here": "在这里输入标签,每个标签 %1 到 %2 个字符。", + "enter_tags_here_short": "输入标签...", + "no_tags": "尚无标签。", + "select_tags": "选择标签" +} \ No newline at end of file diff --git a/public/language/zh-CN/top.json b/public/language/zh-CN/top.json new file mode 100644 index 0000000000..fb0e71e2e6 --- /dev/null +++ b/public/language/zh-CN/top.json @@ -0,0 +1,4 @@ +{ + "title": "置顶", + "no_top_topics": "没有置顶主题" +} \ No newline at end of file diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json new file mode 100644 index 0000000000..f146040c10 --- /dev/null +++ b/public/language/zh-CN/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "主题", + "title": "标题", + "no_topics_found": "没有找到主题!", + "no_posts_found": "没有找到回复!", + "post_is_deleted": "此回复已被删除!", + "topic_is_deleted": "此主题已被删除!", + "profile": "资料", + "posted_by": "%1 发布", + "posted_by_guest": "未登录用户发布", + "chat": "聊天", + "notify_me": "此主题有新回复时通知我", + "quote": "引用", + "reply": "回复", + "replies_to_this_post": "%1 条回复", + "one_reply_to_this_post": "1 条回复", + "last_reply_time": "最后回复", + "reply-as-topic": "在新帖中回复", + "guest-login-reply": "登录后回复", + "login-to-view": "🔒登录查看", + "edit": "编辑", + "delete": "删除", + "delete-event": "删除元素", + "delete-event-confirm": "您确定要删除此元素吗?", + "purge": "清除", + "restore": "恢复", + "move": "移动", + "change-owner": "更改所有者", + "fork": "分割", + "link": "链接", + "share": "分享", + "tools": "工具", + "locked": "已锁定", + "pinned": "已固定", + "pinned-with-expiry": "置顶直到 %1", + "scheduled": "已定时", + "moved": "已移动", + "moved-from": "移自%1版 ", + "copy-ip": "复制IP", + "ban-ip": "封禁IP", + "view-history": "编辑历史", + "locked-by": "锁定自", + "unlocked-by": "解锁自", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "点击阅读本主题帖中的最新回复", + "flag-post": "举报此帖", + "flag-user": "举报此用户", + "already-flagged": "已举报", + "view-flag-report": "查看举报报告", + "resolve-flag": "Resolve Flag", + "merged_message": "此主题已并入%2", + "deleted_message": "此主题已被删除。只有拥有主题管理权限的用户可以查看。", + "following_topic.message": "当有人回复此主题时,您会收到通知。", + "not_following_topic.message": "您将在未读主题列表中看到这个主题,但您不会在帖子被回复时收到通知。", + "ignoring_topic.message": "您将不会在未读主题列表里看到这个主题,但在被提到以及帖子被顶时仍将收到通知。", + "login_to_subscribe": "请注册或登录后,再订阅此主题。", + "markAsUnreadForAll.success": "将全部主题标为未读。", + "mark_unread": "标记为未读", + "mark_unread.success": "主题已被标记为未读。", + "watch": "关注", + "unwatch": "取消关注", + "watch.title": "当此主题有新回复时,通知我", + "unwatch.title": "取消关注此主题", + "share_this_post": "分享此帖", + "watching": "关注中", + "not-watching": "未关注", + "ignoring": "忽略中", + "watching.description": "有新回复时通知我。
在未读主题中显示。", + "not-watching.description": "不要在有新回复时通知我。
如果这个版块未被忽略则在未读主题中显示。", + "ignoring.description": "不要在有新回复时通知我。
不要在未读主题中显示该主题。", + "thread_tools.title": "主题工具", + "thread_tools.markAsUnreadForAll": "全部标记为未读", + "thread_tools.pin": "置顶主题", + "thread_tools.unpin": "取消置顶主题", + "thread_tools.lock": "锁定主题", + "thread_tools.unlock": "解锁主题", + "thread_tools.move": "移动主题", + "thread_tools.move-posts": "移动帖子", + "thread_tools.move_all": "移动全部", + "thread_tools.change_owner": "更改所有者", + "thread_tools.select_category": "选择版块", + "thread_tools.fork": "分割主题", + "thread_tools.delete": "删除主题", + "thread_tools.delete-posts": "删除回复", + "thread_tools.delete_confirm": "确定要删除此主题吗?", + "thread_tools.restore": "恢复主题", + "thread_tools.restore_confirm": "确定要恢复此主题吗?", + "thread_tools.purge": "清除主题", + "thread_tools.purge_confirm": "确认清除此主题吗?", + "thread_tools.merge_topics": "合并主题", + "thread_tools.merge": "合并", + "topic_move_success": "注意:此主题将会被移动到“1%”。点击此处可取消。", + "topic_move_multiple_success": "注意:以下主题将会被移动到“%1”,点击此处可取消。", + "topic_move_all_success": "注意 :全部主题都将被移动到“1%”,点击此处可取消。", + "topic_move_undone": "撤销主题移动", + "topic_move_posts_success": "此帖子将马上移动。点击此处撤销", + "topic_move_posts_undone": "撤销帖子移动", + "post_delete_confirm": "您确定要删除此回复吗?", + "post_restore_confirm": "您确定要恢复此回复吗?", + "post_purge_confirm": "您确定要清除此回复吗?", + "pin-modal-expiry": "失效日期", + "pin-modal-help": "您可以在此处选择为置顶主题设置一个失效日期。或者您也可以选择不设置,则该主题将会一直被置顶,直到管理员取消置顶。", + "load_categories": "正在载入版块", + "confirm_move": "移动", + "confirm_fork": "分割", + "bookmark": "书签", + "bookmarks": "书签", + "bookmarks.has_no_bookmarks": "您还没有添加任何书签", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "正在加载更多帖子", + "move_topic": "移动主题", + "move_topics": "移动主题", + "move_post": "移动帖子", + "post_moved": "回复已移动!", + "fork_topic": "分割主题", + "enter-new-topic-title": "输入新的主题标题", + "fork_topic_instruction": "点击将分割的帖子", + "fork_no_pids": "未选中帖子!", + "no-posts-selected": "未选中帖子!", + "x-posts-selected": "已选中%1个帖子", + "x-posts-will-be-moved-to-y": "%1个帖子将被移动到”%2“", + "fork_pid_count": "选择了 %1 个帖子", + "fork_success": "成功分割主题! 点这里跳转到分割后的主题。", + "delete_posts_instruction": "点击想要删除/永久删除的帖子", + "merge_topics_instruction": "点击您想合并或搜索的主题", + "merge-topic-list-title": "要合并的主题列表", + "merge-options": "合并选项", + "merge-select-main-topic": "选择首要主题", + "merge-new-title-for-topic": "主题的新标题", + "topic-id": "主题 ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "点击您想转移给其他用户的帖子", + "composer.title_placeholder": "在此输入您主题的标题...", + "composer.handle_placeholder": "在这里输入您的姓名/昵称", + "composer.discard": "撤销", + "composer.submit": "提交", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "正在回复 %1", + "composer.new_topic": "新主题", + "composer.editing": "Editing", + "composer.uploading": "正在上传...", + "composer.thumb_url_label": "粘贴主题缩略图网址", + "composer.thumb_title": "给此主题添加缩略图", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "或上传文件", + "composer.thumb_remove": "清除字段", + "composer.drag_and_drop_images": "拖拽图片到此处", + "more_users_and_guests": "%1 名用户和 %2 名游客", + "more_users": "%1 名用户", + "more_guests": "%1 名游客", + "users_and_others": "%1 和 %2 其他人", + "sort_by": "排序", + "oldest_to_newest": "从旧到新", + "newest_to_oldest": "从新到旧", + "most_votes": "最多赞同", + "most_posts": "回复最多", + "most_views": "Most Views", + "stale.title": "接受建议并创建新主题?", + "stale.warning": "您回复的主题已经很古老了,是否发布新主题并引用此主题的内容?", + "stale.create": "创建新主题", + "stale.reply_anyway": "仍然回复此帖", + "link_back": "回复: [%1](%2)", + "diffs.title": "历史发布记录", + "diffs.description": "此主题已经重新发布并修订。点击某个时间点查看修订的内容。", + "diffs.no-revisions-description": "该贴已重新修改", + "diffs.current-revision": "当前版本", + "diffs.original-revision": "原始版本", + "diffs.restore": "恢复到此修订", + "diffs.restore-description": "恢复后,新的修订版本将会被添加到此帖子的编辑历史记录中。", + "diffs.post-restored": "帖子成功恢复到更早的修订版本", + "diffs.delete": "删除此修订", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 后", + "timeago_earlier": "%1 前", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/zh-CN/unread.json b/public/language/zh-CN/unread.json new file mode 100644 index 0000000000..a1e5751a7c --- /dev/null +++ b/public/language/zh-CN/unread.json @@ -0,0 +1,15 @@ +{ + "title": "未读", + "no_unread_topics": "没有未读主题。", + "load_more": "载入更多", + "mark_as_read": "标为已读", + "selected": "已选", + "all": "全部", + "all_categories": "全部板块", + "topics_marked_as_read.success": "主题被标为已读!", + "all-topics": "全部主题", + "new-topics": "新建主题", + "watched-topics": "关注主题", + "unreplied-topics": "未回复主题", + "multiple-categories-selected": "多选" +} \ No newline at end of file diff --git a/public/language/zh-CN/uploads.json b/public/language/zh-CN/uploads.json new file mode 100644 index 0000000000..568ce71767 --- /dev/null +++ b/public/language/zh-CN/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "正在上传文件...", + "select-file-to-upload": "请选择需要上传的文件!", + "upload-success": "文件上传成功!", + "maximum-file-size": "最大 %1 kb", + "no-uploads-found": "没有找到上传文件", + "public-uploads-info": "上传公开的文件,所有访客均可查看。", + "private-uploads-info": "上传私有的文件,仅登陆用户可见。" +} \ No newline at end of file diff --git a/public/language/zh-CN/user.json b/public/language/zh-CN/user.json new file mode 100644 index 0000000000..904ac4a7c4 --- /dev/null +++ b/public/language/zh-CN/user.json @@ -0,0 +1,199 @@ +{ + "banned": "已封禁", + "muted": "禁言", + "offline": "离线", + "deleted": "已删除", + "username": "用户名", + "joindate": "注册日期", + "postcount": "发帖数", + "email": "电子邮件", + "confirm_email": "确认电子邮箱", + "account_info": "账户信息", + "admin_actions_label": "管理行为", + "ban_account": "封禁账户", + "ban_account_confirm": "您确定要封禁这位用户吗?", + "unban_account": "解禁账户", + "mute_account": "禁言账号", + "unmute_account": "解除账号禁言", + "delete_account": "删除帐号", + "delete_account_as_admin": "删除账号", + "delete_content": "删除账号内容", + "delete_all": "删除账号和内容", + "delete_account_confirm": "您确定要匿名化您的所有帖子,并删除您的账户吗?
此操作不可撤销,您将无法恢复您的任何数据

请输入您的密码,以确认删除此帐户", + "delete_this_account_confirm": "您确定要删除账户同时保留其发布的内容吗?
此操作不可逆,帖子将被匿名化,而且您无法恢复被删除账户的任何帖子

", + "delete_account_content_confirm": "您确定要删除账户内容(帖子/主题/上传)吗?
此操作不可逆,而且您无法恢复任何数据

", + "delete_all_confirm": "您确定要删除此账号和它的所有内容(帖子/主题/上传)吗?
此操作不可逆,而且您无法恢复任何数据

", + "account-deleted": "帐号已删除", + "account-content-deleted": "账号内容已删除", + "fullname": "全名", + "website": "网站", + "location": "位置", + "age": "年龄", + "joined": "注册时间", + "lastonline": "最后登录", + "profile": "资料", + "profile_views": "资料浏览", + "reputation": "声望", + "bookmarks": "书签", + "watched_categories": "已关注的版块", + "change_all": "更改全部", + "watched": "已关注", + "ignored": "忽略", + "default-category-watch-state": "默认版块关注状态", + "followers": "粉丝", + "following": "关注", + "blocks": "屏蔽", + "block_toggle": "屏蔽该用户", + "block_user": "屏蔽用户", + "unblock_user": "解除屏蔽用户", + "aboutme": "关于我", + "signature": "签名档", + "birthday": "生日", + "chat": "聊天", + "chat_with": "继续与 %1 聊天", + "new_chat_with": "开始与 %1 的新会话", + "flag-profile": "举报资料", + "follow": "关注", + "unfollow": "取消关注", + "more": "更多", + "profile_update_success": "资料已经成功更新。", + "change_picture": "更改头像", + "change_username": "更改用户名", + "change_email": "更改电子邮箱", + "email_same_as_password": "请输入您当前的密码以继续 –您已经再次输入了您的新电子邮箱", + "edit": "编辑", + "edit-profile": "编辑资料", + "default_picture": "默认图标", + "uploaded_picture": "已有头像", + "upload_new_picture": "上传新头像", + "upload_new_picture_from_url": "上传来自URL的新头像", + "current_password": "当前密码", + "change_password": "更改密码", + "change_password_error": "无效的密码!", + "change_password_error_wrong_current": "您的当前密码不正确!", + "change_password_error_match": "两次输入的密码必须相同!", + "change_password_error_privileges": "您无权更改此密码。", + "change_password_success": "您的密码已更新!", + "confirm_password": "确认密码", + "password": "密码", + "username_taken_workaround": "您申请的用户名已被占用,所以我们稍作更改。您现在的用户名是 %1", + "password_same_as_username": "您的密码与用户名相同,请选择另外的密码。", + "password_same_as_email": "您的密码与邮箱相同,请选择另外的密码。", + "weak_password": "密码强度低。", + "upload_picture": "上传头像", + "upload_a_picture": "上传头像", + "remove_uploaded_picture": "删除已上传的头像", + "upload_cover_picture": "上传封面图片", + "remove_cover_picture_confirm": "您确定要移除封面图片吗?", + "crop_picture": "剪裁图片", + "upload_cropped_picture": "剪裁并上传", + "avatar-background-colour": "头像背景颜色", + "settings": "设置", + "show_email": "显示我的电子邮箱", + "show_fullname": "显示我的全名", + "restrict_chats": "只允许我关注的用户给我发送聊天消息", + "digest_label": "订阅摘要", + "digest_description": "订阅此论坛的定期电子邮件更新 (新通知和主题)", + "digest_off": "关闭", + "digest_daily": "每天", + "digest_weekly": "每周", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "每月", + "has_no_follower": "此用户还没有粉丝 :(", + "follows_no_one": "此用户尚未关注任何人 :(", + "has_no_posts": "此用户从未发言。", + "has_no_best_posts": "此用户没有任何顶过的帖子。", + "has_no_topics": "此用户还未发布任何主题。", + "has_no_watched_topics": "此用户还未关注任何主题。", + "has_no_ignored_topics": "此用户尚未忽略任何主题。", + "has_no_upvoted_posts": "此用户还未顶过任何帖子。", + "has_no_downvoted_posts": "此用户还未踩过任何帖子。", + "has_no_controversial_posts": "此用户没有任何踩过的帖子。", + "has_no_blocks": "您没有屏蔽其他用户。", + "email_hidden": "电子邮箱已隐藏", + "hidden": "隐藏", + "paginate_description": "使用分页式版块浏览", + "topics_per_page": "每页主题数", + "posts_per_page": "每页帖子数", + "max_items_per_page": "最大值 %1", + "acp_language": "管理员页面语言", + "notifications": "通知", + "upvote-notif-freq": "帖子被顶的通知频率", + "upvote-notif-freq.all": "每一次被顶都通知我", + "upvote-notif-freq.first": "首次顶贴", + "upvote-notif-freq.everyTen": "每10次被顶通知我一次", + "upvote-notif-freq.threshold": "当被顶的数目为1, 5, 10, 25, 50, 100, 150, 200...时通知我", + "upvote-notif-freq.logarithmic": "当被顶的数目为10, 100, 1000...时通知我", + "upvote-notif-freq.disabled": "任何时候都不要通知我", + "browsing": "浏览设置", + "open_links_in_new_tab": "在新标签打开外部链接", + "enable_topic_searching": "启用主题内搜索", + "topic_search_help": "如果启用此项,主题内搜索会替代浏览器默认的页面搜索,您将可以在整个主题内搜索,而不仅仅只搜索页面上展现的内容。", + "update_url_with_post_index": "浏览主题是更新链接和索引", + "scroll_to_my_post": "在提交回复之后显示新回复", + "follow_topics_you_reply_to": "关注您回复过的主题", + "follow_topics_you_create": "关注您创建的主题", + "grouptitle": "群组称号", + "group-order-help": "选择群组然后使用箭头排列称号", + "no-group-title": "不展示群组称号", + "select-skin": "选择皮肤", + "select-homepage": "选择首页", + "homepage": "首页", + "homepage_description": "选择一个页面作为论坛的首页,否则设置为 ‘空’ 使用默认首页。", + "custom_route": "自定义首页路由", + "custom_route_help": "输入路由名称,前面不需要斜杠 ( 例如, \"recent\" 或 \"category/2/general-discussion\" )", + "sso.title": "单点登录服务", + "sso.associated": "已关联到", + "sso.not-associated": "点击这里来关联", + "sso.dissociate": "解除关联", + "sso.dissociate-confirm-title": "确认解除关联", + "sso.dissociate-confirm": "您确定要将您的账户与 %1 解除关联吗?", + "info.latest-flags": "最新举报", + "info.no-flags": "没有找到被举报的帖子", + "info.ban-history": "最近封禁历史", + "info.no-ban-history": "该用户从未被封禁", + "info.banned-until": "封禁到 %1", + "info.banned-expiry": "到期", + "info.banned-permanently": "永久封禁", + "info.banned-reason-label": "原因", + "info.banned-no-reason": "没有原因", + "info.mute-history": "最近禁言历史", + "info.no-mute-history": "该用户从未被禁言", + "info.muted-until": "禁言到 %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "没有原因", + "info.username-history": "历史用户名", + "info.email-history": "历史邮箱", + "info.moderation-note": "版主备注", + "info.moderation-note.success": "版主备注已保存", + "info.moderation-note.add": "添加备注", + "sessions.description": "此页面允许您查看当前论坛的所有激活的会话(active session),并在需要的时候关闭它们.您可以通过注销您的账户来关闭自己的会话(session)", + "consent.title": "您的权利与许可", + "consent.lead": "本论坛将会收集与处理您的个人信息。", + "consent.intro": "我们收集这些信息将仅用于个性化您于本社区的体验,和关联您的账户与您所发表的帖子。在注册过程中您需要提供一个用户名和邮箱地址,您也可以选择是否提供额外的个人信息,以完善您的用户资料。

在您的用户账户有效期内,我们将保留您的信息。您可以在任何时候通过删除您的账号,以撤回您的许可。您可以在任何时候通过您的权力与许可页面,获取一份您对本论坛的贡献的副本。

如果您有任何疑问,我们鼓励您与本论坛管理团队联系。", + "consent.email_intro": "我们有时可能会向您的注册邮箱发送电子邮件,以向您提供有关于您的新动态和/或新活动。您可以通过您的用户设置页面自定义(包括直接禁用)社区摘要的发送频率,以及选择性地接收哪些类型的通知。", + "consent.digest_frequency": "本社区默认每 %1 发送一封摘要邮件,除非您在用户设置中明确更改了此项。", + "consent.digest_off": "本社区默认不发送摘要邮件,除非您在用户设置中明确更改了此项。", + "consent.received": "您已许可本网站收集与处理您的个人数据。无需其他额外操作。", + "consent.not_received": "您未许可本网站收集与处理您的个人数据。本网站的管理团队可能于任何时候删除您的账户,以符合通用数据保护条例的要求。", + "consent.give": "授予许可", + "consent.right_of_access": "您拥有数据访问权", + "consent.right_of_access_description": "您有权访问本网站根据需求收集的您的任何数据。您可以点击下方相应按钮,获取这些数据的副本。", + "consent.right_to_rectification": "您拥有纠正权", + "consent.right_to_rectification_description": "您拥有修改或更新提供给我们的任何不准确的个人数据的权力。您可以通过编辑以更新个人资料,并可以修改您发表的内容。若非如此,请联系本网站的管理团队。", + "consent.right_to_erasure": "您拥有被遗忘权", + "consent.right_to_erasure_description": "您随时都可以通过删除帐号,来撤销数据收集和处理的许可。您的个人资料可以被删除,但是您发表的内容仍然会保留。如果您想要同时删除您的帐号内容,请联系此网站的管理团队。", + "consent.right_to_data_portability": "您拥有数据转移权", + "consent.right_to_data_portability_description": "您也许想导出有关您和您的账号的机器可读副本。您可以点击下方的按钮来获取它们。", + "consent.export_profile": "导出个人资料 (.json)", + "consent.export-profile-success": "资料导出完成后,您将会收到一条通知。", + "consent.export_uploads": "导出上传文件 (.zip)", + "consent.export-uploads-success": "上传导出完成后,您将会收到一条通知。", + "consent.export_posts": "导出帖子 (.csv)", + "consent.export-posts-success": "帖子导出完成后,您将会收到一条通知。", + "emailUpdate.intro": "请在下方输入您的电子邮箱地址。此论坛使用您的电子邮箱地址用于定时发送摘要和通知,以及用于忘记密码时找回账号。", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "此字段为必填。", + "emailUpdate.change-instructions": "将向输入的电子邮箱地址发送一封带有唯一链接的确认电子邮件。访问该链接将验证您对该电子邮箱的所有权,它将在您的账号上处于活动状态。在任何时候,您都可以在您的账号页面更新存档的电子邮箱地址。", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/zh-CN/users.json b/public/language/zh-CN/users.json new file mode 100644 index 0000000000..055a7028e1 --- /dev/null +++ b/public/language/zh-CN/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "最新用户", + "top_posters": "发帖排行", + "most_reputation": "声望排行", + "most_flags": "举报最多", + "search": "搜索", + "enter_username": "输入用户名搜索", + "search-user-for-chat": "搜索用户以开始聊天", + "load_more": "加载更多", + "users-found-search-took": "找到 %1 位用户!耗时 %2 秒。", + "filter-by": "过滤选项", + "online-only": "只看在线", + "invite": "邀请注册", + "prompt-email": "邮件:", + "groups-to-join": "邀请接受时要加入的群组:", + "invitation-email-sent": "已发送邀请给 %1", + "user_list": "用户列表", + "recent_topics": "最新主题", + "popular_topics": "热门主题", + "unread_topics": "未读主题", + "categories": "版块", + "tags": "标签", + "no-users-found": "未找到匹配的用户!" +} \ No newline at end of file diff --git a/public/language/zh-TW/_DO_NOT_EDIT_FILES_HERE.md b/public/language/zh-TW/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/zh-TW/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/zh-TW/admin/admin.json b/public/language/zh-TW/admin/admin.json new file mode 100644 index 0000000000..4d707e36bf --- /dev/null +++ b/public/language/zh-TW/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "您確定要部署並重啟 NodeBB 嗎?", + "alert.confirm-restart": "您確定要重啟 NodeBB 嗎?", + + "acp-title": "%1 | NodeBB 管理控制台", + "settings-header-contents": "内容", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/advanced/cache.json b/public/language/zh-TW/admin/advanced/cache.json new file mode 100644 index 0000000000..02f2586f57 --- /dev/null +++ b/public/language/zh-TW/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "貼文快取", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "%1% 容量", + "post-cache-size": "貼文快取大小", + "items-in-cache": "快取中的項目數量" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/advanced/database.json b/public/language/zh-TW/admin/advanced/database.json new file mode 100644 index 0000000000..03bf08fc14 --- /dev/null +++ b/public/language/zh-TW/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "運行秒數", + "uptime-days": "運行天數", + + "mongo": "Mongo", + "mongo.version": "MongoDB 版本", + "mongo.storage-engine": "儲存引擎", + "mongo.collections": "集合", + "mongo.objects": "物件數量", + "mongo.avg-object-size": "平均物件大小", + "mongo.data-size": "資酪大小", + "mongo.storage-size": "儲存大小", + "mongo.index-size": "索引大小", + "mongo.file-size": "檔案大小", + "mongo.resident-memory": "常駐記憶體", + "mongo.virtual-memory": "虛擬記憶體", + "mongo.mapped-memory": "已映射記憶體", + "mongo.bytes-in": "位元組輸入", + "mongo.bytes-out": "位元組輸出", + "mongo.num-requests": "請求數量", + "mongo.raw-info": "MongoDB 原始資訊", + "mongo.unauthorized": "NodeBB 無法從 MongoDB 資料庫請求相應的統計資料。請確保 NodeBB 使用的連線帳戶具有"admin"資料庫的"clusterMonitor"角色。", + + "redis": "Redis", + "redis.version": "Redis 版本", + "redis.keys": "鍵數量", + "redis.expires": "有效期", + "redis.avg-ttl": "平均生存時間", + "redis.connected-clients": "已連接客戶端", + "redis.connected-slaves": "已連接從", + "redis.blocked-clients": "封鎖的客戶端", + "redis.used-memory": "已使用記憶體", + "redis.memory-frag-ratio": "記憶體碎片比率", + "redis.total-connections-recieved": "已接收的連線總數", + "redis.total-commands-processed": "已執行命令總數", + "redis.iops": "每秒即時操作數", + "redis.iinput": "每秒即時輸入", + "redis.ioutput": "每秒即時輸出", + "redis.total-input": "總輸入", + "redis.total-output": "總輸出", + + "redis.keyspace-hits": "Keyspace 命中", + "redis.keyspace-misses": "Keyspace 未命中", + "redis.raw-info": "Redis 原始資訊", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL 版本", + "postgres.raw-info": "Postgres 原始資訊" +} diff --git a/public/language/zh-TW/admin/advanced/errors.json b/public/language/zh-TW/admin/advanced/errors.json new file mode 100644 index 0000000000..547effa250 --- /dev/null +++ b/public/language/zh-TW/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "數量 %1", + "error-events-per-day": "%1 事件/天", + "error.404": "404 頁面不存在", + "error.503": "503 服務不可用", + "manage-error-log": "管理錯誤日誌", + "export-error-log": "導出錯誤日誌 (.csv)", + "clear-error-log": "清空錯誤日誌", + "route": "路徑", + "count": "次數", + "no-routes-not-found": "恭喜!沒有404錯誤!", + "clear404-confirm": "確認清除404錯誤日誌?", + "clear404-success": "“404 頁面不存在” 錯誤已被清空" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/advanced/events.json b/public/language/zh-TW/admin/advanced/events.json new file mode 100644 index 0000000000..5ecc44a954 --- /dev/null +++ b/public/language/zh-TW/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "事件", + "no-events": "暫無事件", + "control-panel": "事件控制面板", + "delete-events": "刪除事件", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "過濾器", + "filters-apply": "應用過濾器", + "filter-type": "事件類型", + "filter-start": "開始時間", + "filter-end": "結束時間", + "filter-perPage": "每頁" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/advanced/logs.json b/public/language/zh-TW/admin/advanced/logs.json new file mode 100644 index 0000000000..76d874ac36 --- /dev/null +++ b/public/language/zh-TW/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "日誌", + "control-panel": "日誌控制面板", + "reload": "重載日誌", + "clear": "清空日誌", + "clear-success": "日誌已清空!" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/appearance/customise.json b/public/language/zh-TW/admin/appearance/customise.json new file mode 100644 index 0000000000..9797ee69c7 --- /dev/null +++ b/public/language/zh-TW/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "自訂 CSS/LESS", + "custom-css.description": "在這裡輸入您想應用於所有其它風格的 CSS/LESS,", + "custom-css.enable": "啟用自訂 CSS/LESS", + + "custom-js": "自訂 Javascript", + "custom-js.description": "在這裡輸入您想在頁面加載完成後執行的 Javascript 程式碼。", + "custom-js.enable": "啟用自訂 Javascript", + + "custom-header": "自訂 Header", + "custom-header.description": "在這裡輸入自訂的 HTML 程式碼 (如 Meta Tags 等),這些程式碼會被添加到論壇的 <head>部分。 您可以在這裡使用 Script 標籤,但我們更鼓勵您將您的 JavaScript 寫到 自訂 Javascript 中", + "custom-header.enable": "啟用自訂 Header", + + "custom-css.livereload": "啟用動態重載", + "custom-css.livereload.description": "啟用此功能可以在您點擊儲存時強制您帳戶下的每個設備上的所有會話進行重載" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/appearance/skins.json b/public/language/zh-TW/admin/appearance/skins.json new file mode 100644 index 0000000000..ba0bf3b7cf --- /dev/null +++ b/public/language/zh-TW/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "正在加載配色...", + "homepage": "首頁", + "select-skin": "選擇配色", + "current-skin": "當前配色", + "skin-updated": "配色已更新", + "applied-success": "%1 配色已成功套用", + "revert-success": "配色已恢復到基礎顏色" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/appearance/themes.json b/public/language/zh-TW/admin/appearance/themes.json new file mode 100644 index 0000000000..23b7c948db --- /dev/null +++ b/public/language/zh-TW/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "正在檢查已安裝的佈景主題...", + "homepage": "首頁", + "select-theme": "選擇佈景主題", + "current-theme": "當前佈景主題", + "no-themes": "未發現已安裝的佈景主題", + "revert-confirm": "確認恢復到 NodeBB 預設佈景主題?", + "theme-changed": "佈景主題已更改", + "revert-success": "已成功恢復到 NodeBB 預設佈景主題。", + "restart-to-activate": "請部署並重啟您的 NodeBB 以完成啟用佈景主題。" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/dashboard.json b/public/language/zh-TW/admin/dashboard.json new file mode 100644 index 0000000000..4b9601493e --- /dev/null +++ b/public/language/zh-TW/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "論壇流量", + "page-views": "頁面瀏覽量", + "unique-visitors": "不重複訪客", + "logins": "登入", + "new-users": "新使用者", + "posts": "貼文", + "topics": "主題", + "page-views-seven": "最近7天", + "page-views-thirty": "最近30天", + "page-views-last-day": "最近24小時", + "page-views-custom": "自定義日期範圍", + "page-views-custom-start": "範圍開始", + "page-views-custom-end": "範圍結束", + "page-views-custom-help": "輸入您要查看的網頁瀏覽日期範圍。 如果沒有日期選擇器可用,則接受的格式是 YYYY-MM-DD", + "page-views-custom-error": "請輸入 YYYY-MM-DD格式的有效日期範圍 ", + + "stats.yesterday": "昨天", + "stats.today": "今天", + "stats.last-week": "上一週", + "stats.this-week": "本週", + "stats.last-month": "上一月", + "stats.this-month": "本月", + "stats.all": "總計", + + "updates": "更新", + "running-version": "您正在運行 NodeBB v%1 .", + "keep-updated": "請確保您已及時更新 NodeBB 以獲得最新的安全修補程式與 Bug 修復。", + "up-to-date": "

正在使用 最新版本

", + "upgrade-available": "

新的版本 (v%1) 已經發布。建議您 升級 NodeBB

", + "prerelease-upgrade-available": "

這是一個已經過期的預發佈版本的 NodeBB,新的版本 (v%1) 已經發布。建議您 升級 NodeBB

", + "prerelease-warning": "

正在使用測試版 NodeBB。可能會出現意外的 Bug。

", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "論壇正處於開發模式,這可能使其暴露於潛在的危險之中;請聯繫您的系統管理員。", + "latest-lookup-failed": "

無法找到 NodeBB 的最新可用版本

", + + "notices": "提醒", + "restart-not-required": "不需要重啟", + "restart-required": "需要重啟", + "search-plugin-installed": "已安裝搜尋外掛", + "search-plugin-not-installed": "未安裝搜尋外掛", + "search-plugin-tooltip": "在外掛頁面安裝搜尋外掛來啟用搜尋功能", + + "control-panel": "系統控制", + "rebuild-and-restart": "重建 & 重啟", + "restart": "重啟", + "restart-warning": "重載或重啟 NodeBB 會丟失數秒內全部的連接。", + "restart-disabled": "重建和重新啟動NodeBB已被禁用,因為您似乎沒有通過適當的守護進程運行它。", + "maintenance-mode": "維護模式", + "maintenance-mode-title": "點擊此處設置 NodeBB 的維護模式", + "realtime-chart-updates": "即時圖表更新", + + "active-users": "活躍使用者", + "active-users.users": "使用者", + "active-users.guests": "訪客", + "active-users.total": "全部", + "active-users.connections": "連線", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "已註冊", + + "user-presence": "使用者光臨", + "on-categories": "在版面列表", + "reading-posts": "閱讀貼文", + "browsing-topics": "瀏覽主題", + "recent": "最近", + "unread": "未讀", + + "high-presence-topics": "熱門主題", + "popular-searches": "Popular Searches", + + "graphs.page-views": "頁面瀏覽量", + "graphs.page-views-registered": "註冊使用者頁面瀏覽量", + "graphs.page-views-guest": "訪客頁面瀏覽量", + "graphs.page-views-bot": "爬蟲頁面瀏覽量", + "graphs.unique-visitors": "不重複訪客", + "graphs.registered-users": "已註冊使用者", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "上次重啟管理員/時間", + "no-users-browsing": "沒有使用者正在瀏覽", + + "back-to-dashboard": "回到儀表板", + "details.no-users": "沒有使用者有在選定的時間內註冊", + "details.no-topics": "沒有新增的主題在選定的時間內", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "登入時間" +} diff --git a/public/language/zh-TW/admin/development/info.json b/public/language/zh-TW/admin/development/info.json new file mode 100644 index 0000000000..9b2d42dcfd --- /dev/null +++ b/public/language/zh-TW/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "您的位址 %1:%2", + "ip": "IP %1", + "nodes-responded": "%1個節點在%2ms內響應!", + "host": "主機", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "在線", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "系統負載", + "cpu-usage": "CPU 使用情況", + "uptime": "運行時間", + + "registered": "已註冊", + "sockets": "網路接口", + "guests": "訪客", + + "info": "資訊" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/development/logger.json b/public/language/zh-TW/admin/development/logger.json new file mode 100644 index 0000000000..e7f398272b --- /dev/null +++ b/public/language/zh-TW/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "日誌記錄器設定", + "description": "啟用此選項後,日誌會在您的終端裡顯示。如果您註明了檔案路徑,日誌會被保存到該檔案中。HTTP 日誌可以幫助您收集論壇被誰,何時,以及什麼內容被訪問等統計資訊。在此基礎上,我們還提供 socket.io 事件日誌。結合 socket.io 日誌和 redis-cli 監控器,學習 NodeBB 的內部構造會更加方便。", + "explanation": "勾選或反勾選日誌設定項即可啟用或禁用相應設定。無需重啟。", + "enable-http": "啟用 HTTP 日誌", + "enable-socket": "啟用 socket.io 事件日誌", + "file-path": "日誌檔案路徑", + "file-path-placeholder": "如 /path/to/log/file.log ::: 如想在終端中顯示日誌請留空此項", + + "control-panel": "日誌記錄器控制面板", + "update-settings": "更新日誌記錄器設定" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/extend/plugins.json b/public/language/zh-TW/admin/extend/plugins.json new file mode 100644 index 0000000000..546f286f9a --- /dev/null +++ b/public/language/zh-TW/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "已安裝", + "active": "已啟用", + "inactive": "未啟用", + "out-of-date": "已過期", + "none-found": "無外掛。", + "none-active": "無生效外掛", + "find-plugins": "尋找外掛", + + "plugin-search": "外掛搜索", + "plugin-search-placeholder": "搜索外掛...", + "submit-anonymous-usage": "提交匿名外掛使用資料。", + "reorder-plugins": "重新排序外掛", + "order-active": "排序生效外掛", + "dev-interested": "有興趣為 NodeBB 開發外掛?", + "docs-info": "有關外掛創作的完整文件可以在 NodeBB 文件中找到。", + + "order.description": "部分外掛需要在其它外掛啟用之後才能完整運作。", + "order.explanation": "外掛將按照以下順序載入,從上至下。", + + "plugin-item.themes": "佈景主題", + "plugin-item.deactivate": "停用", + "plugin-item.activate": "啟用", + "plugin-item.install": "安裝", + "plugin-item.uninstall": "卸載", + "plugin-item.settings": "設定", + "plugin-item.installed": "已安裝", + "plugin-item.latest": "最新", + "plugin-item.upgrade": "升級", + "plugin-item.more-info": "更多資訊:", + "plugin-item.unknown": "未知", + "plugin-item.unknown-explanation": "無法確認該外掛的狀態,可能由於設定錯誤造成。", + "plugin-item.compatible": "此外掛適合 NodeBB %1", + "plugin-item.not-compatible": "此外掛沒有相容資料,請確保在生產環境中安裝之前它可以正常工作。", + + "alert.enabled": "外掛已啟用", + "alert.disabled": "外掛已禁用", + "alert.upgraded": "外掛已升級", + "alert.installed": "外掛已安裝", + "alert.uninstalled": "外掛已卸載", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "外掛停用成功", + "alert.upgrade-success": "請部署並重啟您的 NodeBB 來完成更新此外掛。", + "alert.install-success": "外掛安裝成功,請啟用外掛。", + "alert.uninstall-success": "外掛已成功被停用且卸載。", + "alert.suggest-error": "

NodeBB 聯繫不到套件管理器, 繼續安裝最新版本?

伺服器返回 (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB 聯繫不到套件管理器,暫時不建議升級。

", + "alert.incompatible": "

您目前安裝的 NodeBB 版本(v%1) 只支持到此外掛的 v%2 版本。如需要此外掛更加新的版本請先升級 NodeBB。

", + "alert.possibly-incompatible": "

未找到相容性資料

此外掛未註明對應的 NodeBB 版本。可能會產生相容性問題,導致 NodeBB 無法正常啟動。

NodeBB 無法正常啟動時請運行以下命令:

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

是否繼續安裝此插件的最新版本?

", + "alert.reorder": "插件已重新排序", + "alert.reorder-success": "請部署並重啟您的 NodeBB 來完成此流程。", + + "license.title": "外掛授權資料", + "license.intro": "外掛 %1 在 %2 下獲得許可。請在啟用此插件之前閱讀,確認授權條款。", + "license.cta": "您希望繼續使用此外掛嗎?" +} diff --git a/public/language/zh-TW/admin/extend/rewards.json b/public/language/zh-TW/admin/extend/rewards.json new file mode 100644 index 0000000000..924bc85f71 --- /dev/null +++ b/public/language/zh-TW/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "獎勵", + "condition-if-users": "如果使用者的", + "condition-is": "是:", + "condition-then": "則:", + "max-claims": "可獲取獎勵的次數", + "zero-infinite": "無限制請輸入0", + "delete": "刪除", + "enable": "啟用", + "disable": "禁用", + + "alert.delete-success": "已成功刪除獎勵", + "alert.no-inputs-found": "非法獎勵 - 輸入為空!", + "alert.save-success": "已成功儲存獎勵" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/extend/widgets.json b/public/language/zh-TW/admin/extend/widgets.json new file mode 100644 index 0000000000..b2814cdff5 --- /dev/null +++ b/public/language/zh-TW/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "可用的小工具", + "explanation": "從下拉選單中選擇一個小工具並拖放到樣板左邊的小工具區域。", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "從小工具複製", + "containers.available": "可用的容器", + "containers.explanation": "拖放到任意生效中的小工具上", + "containers.none": "無", + "container.well": "Well", + "container.jumbotron": "超大屏幕", + "container.panel": "面板", + "container.panel-header": "面板標題", + "container.panel-body": "面板內容", + "container.alert": "警示", + + "alert.confirm-delete": "確認刪除此小工具?", + "alert.updated": "小工具更新", + "alert.update-success": "已成功更新小工具", + "alert.clone-success": "成功複製小工具", + + "error.select-clone": "請選擇一個頁面進行複製", + + "title": "標題", + "title.placeholder": "標題(僅在部分容器顯示)", + "container": "容器", + "container.placeholder": "將容器拖拽至此處或在此處輸入HTML", + "show-to-groups": "對群組顯示", + "hide-from-groups": "對群組隱藏", + "hide-on-mobile": "在移動端隱藏" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/admins-mods.json b/public/language/zh-TW/admin/manage/admins-mods.json new file mode 100644 index 0000000000..d251180dc4 --- /dev/null +++ b/public/language/zh-TW/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "管理員", + "global-moderators": "超級版主", + "moderators": "Moderators", + "no-global-moderators": "沒有超級版主", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "沒有版主", + "add-administrator": "添加管理員", + "add-global-moderator": "新增超級版主", + "add-moderator": "新增版主" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/categories.json b/public/language/zh-TW/admin/manage/categories.json new file mode 100644 index 0000000000..6f79081a24 --- /dev/null +++ b/public/language/zh-TW/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "版面設定", + "privileges": "權限", + + "name": "版面名稱", + "description": "版面描述", + "bg-color": "背景顏色", + "text-color": "圖示顏色", + "bg-image-size": "背景圖片大小", + "custom-class": "自訂 Class", + "num-recent-replies": "最近回覆數", + "ext-link": "外部連結", + "subcategories-per-page": "Subcategories per page", + "is-section": "將該版面作為分段", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "上傳圖片", + "delete-image": "移除", + "category-image": "版面圖片", + "parent-category": "上層版面", + "optional-parent-category": "(可選)上層版面", + "top-level": "Top Level", + "parent-category-none": "(無)", + "copy-parent": "複製 上層版面", + "copy-settings": "複製設定", + "optional-clone-settings": "(可選) 從版面複製設定", + "clone-children": "複製子版面並進行設定", + "purge": "刪除版面", + + "enable": "啟用", + "disable": "禁用", + "edit": "編輯", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "選擇版面", + "set-parent-category": "設置上層版面", + + "privileges.description": "您可以在此面板中為網站的某些部分設定訪問控制權。可以分別為每個使用者或每個使用者群組授予權限。從下方的下拉列表中選擇作用的網域。", + "privileges.category-selector": "為該版面設定權限:", + "privileges.warning": "注意:權限設定會立即生效。 調整這些設定後,無需保存。", + "privileges.section-viewing": "查看權限", + "privileges.section-posting": "發文權限", + "privileges.section-moderation": "審核權限", + "privileges.section-other": "其它", + "privileges.section-user": "使用者", + "privileges.search-user": "新增使用者", + "privileges.no-users": "此版面中沒有使用者特定的權限。", + "privileges.section-group": "群組", + "privileges.group-private": "這個群組是私密的", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "新增群組", + "privileges.copy-to-children": "複製到子版面", + "privileges.copy-from-category": "從版面複製", + "privileges.copy-privileges-to-all-categories": "複製到全部版面", + "privileges.copy-group-privileges-to-children": "複製此使用者群組的權限到此版面的下層", + "privileges.copy-group-privileges-to-all-categories": "複製此使用者群組的權限到全部版面", + "privileges.copy-group-privileges-from": "從其它版面複製權限到此使用者群組", + "privileges.inherit": "如果 registered-users 群組被授予特定權限,所有其他群組都會收到隱式權限,即使它們未被明確定義/勾選。 將顯示此隱式權限,因為所有使用者都是 registered-users 群組的一部分,因此無需顯式授予其它群組的權限。", + "privileges.copy-success": "權限已複製", + + "analytics.back": "返回版面列表", + "analytics.title": "“%1”版面的統計", + "analytics.pageviews-hourly": "圖1 – 此版面的每小時頁面瀏覽量", + "analytics.pageviews-daily": "圖2 – 此版面的每日頁面瀏覽量", + "analytics.topics-daily": "圖3 – 每日在此版面中建立的主題", + "analytics.posts-daily": "圖4 – 每日在此版面中每日發佈的貼文", + + "alert.created": "建立", + "alert.create-success": "版面建立成功!", + "alert.none-active": "您沒有有效的版面。", + "alert.create": "建立一個版面", + "alert.confirm-purge": "

您確定要清除 “%1” 版面嗎?

警告! 版面將被清除!

清除版塊將刪除所有主題和帖子,並從數據庫中刪除版塊。 如果您想暫時移除版塊,請使用停用版塊。

", + "alert.purge-success": "版面已刪除!", + "alert.copy-success": "設定已複製!", + "alert.set-parent-category": "設定上層版面", + "alert.updated": "版面已更新", + "alert.updated-success": "版面 ID %1 成功更新。", + "alert.upload-image": "上傳版面圖片", + "alert.find-user": "尋找使用者", + "alert.user-search": "在這裡尋找使用者…", + "alert.find-group": "尋找群組", + "alert.group-search": "在此處搜尋群組...", + "alert.not-enough-whitelisted-tags": "Whitelisted tags are less than minimum tags, you need to create more whitelisted tags!", + "collapse-all": "全部摺疊", + "expand-all": "全部展開", + "disable-on-create": "建立後禁用", + "no-matches": "No matches" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/digest.json b/public/language/zh-TW/admin/manage/digest.json new file mode 100644 index 0000000000..368cab446c --- /dev/null +++ b/public/language/zh-TW/admin/manage/digest.json @@ -0,0 +1,22 @@ +{ + "lead": "以下是摘要發送狀態及時間列表", + "disclaimer": "請注意,由於 Email 技術本身的原因,郵件不一定能保證送達。有很多因素都會導致郵件無法到達使用者的信箱,比如郵件伺服器的信譽、IP 地址黑名單、DNS 的 DKIM/SPF/DMARC 設定等。", + "disclaimer-continued": "成功發送意味訊息被 NodeBB 成功發送且被接收人伺服器收到。但這並不等同於郵件發送到了信箱箱中。為了確保信息可以準確送達,我們建議使用第三方的郵件服務,例如SendGrid。", + + "user": "使用者", + "subscription": "訂閱類型", + "last-delivery": "上次成功通知", + "default": "系統預設", + "default-help": "System default 表示使用者尚未明確覆寫摘要的全域論壇設置,該設定當前為: “%1“", + "resend": "重發摘要", + "resend-all-confirm": "您確認要手動執行這個摘要嗎?", + "resent-single": "摘要重發操作完成", + "resent-day": "已發送每日摘要", + "resent-week": "已發送每週摘要", + "resent-biweek": "Bi-Weekly digest resent", + "resent-month": "已發送每月摘要", + "null": "從不", + "manual-run": "手動執行摘要:", + + "no-delivery-data": "找不到發送資料" +} diff --git a/public/language/zh-TW/admin/manage/groups.json b/public/language/zh-TW/admin/manage/groups.json new file mode 100644 index 0000000000..99f07175bf --- /dev/null +++ b/public/language/zh-TW/admin/manage/groups.json @@ -0,0 +1,44 @@ +{ + "name": "群組名", + "badge": "獎章", + "properties": "屬性", + "description": "群組描述", + "member-count": "成員數量", + "system": "系統", + "hidden": "隱藏", + "private": "私有", + "edit": "編輯", + "delete": "Delete", + "privileges": "Privileges", + "download-csv": "CSV", + "search-placeholder": "搜尋", + "create": "建立群組", + "description-placeholder": "一個關於你的群組的簡短描述", + "create-button": "建立", + + "alerts.create-failure": "哦不!

建立您的群組時出現問題。 請稍後再試!

", + "alerts.confirm-delete": "您確定要刪除此群組嗎?", + + "edit.name": "名稱", + "edit.description": "描述", + "edit.user-title": "成員標題", + "edit.icon": "群組圖示", + "edit.label-color": "群組標籤顏色", + "edit.text-color": "群組文字顏色", + "edit.show-badge": "顯示徽章", + "edit.private-details": "啟用此選項後,加入群組的請求將需要群組所有者審核。", + "edit.private-override": "警告:系統已禁用了私有群組,優先性高於該選項。", + "edit.disable-join": "禁止申請加入群組", + "edit.disable-leave": "禁止使用者離開群組", + "edit.hidden": "隱藏", + "edit.hidden-details": "啟用此選項後,此群組將不在群組列表呈現,並且使用者只能被手動邀請加入", + "edit.add-user": "向此群組新增成員", + "edit.add-user-search": "搜尋使用者", + "edit.members": "成員列表", + "control-panel": "群組控制面板", + "revert": "重置", + + "edit.no-users-found": "沒有找到使用者", + "edit.confirm-remove-user": "確認刪除此使用者嗎?", + "edit.save-success": "設定已儲存!" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/privileges.json b/public/language/zh-TW/admin/manage/privileges.json new file mode 100644 index 0000000000..b4e1a30bda --- /dev/null +++ b/public/language/zh-TW/admin/manage/privileges.json @@ -0,0 +1,64 @@ +{ + "global": "全域", + "admin": "管理員", + "group-privileges": "群組權限", + "user-privileges": "使用者權限", + "edit-privileges": "Edit Privileges", + "select-clear-all": "Select/Clear All", + "chat": "聊天", + "upload-images": "上傳圖片", + "upload-files": "上傳檔案", + "signature": "簽名檔", + "ban": "禁用", + "mute": "Mute", + "invite": "Invite", + "search-content": "搜尋內容", + "search-users": "搜尋使用者", + "search-tags": "搜尋標籤", + "view-users": "瀏覽使用者", + "view-tags": "瀏覽標籤", + "view-groups": "瀏覽群組", + "allow-local-login": "本地登入", + "allow-group-creation": "群組建立", + "view-users-info": "檢視使用者資訊", + "find-category": "尋找版面", + "access-category": "存取版面", + "access-topics": "存取主題", + "create-topics": "建立主題", + "reply-to-topics": "回覆主題", + "schedule-topics": "Schedule Topics", + "tag-topics": "新增標籤", + "edit-posts": "修改回覆", + "view-edit-history": "查看變更歷史", + "delete-posts": "刪除回覆", + "view_deleted": "查看已刪除回覆", + "upvote-posts": "點贊", + "downvote-posts": "倒讚", + "delete-topics": "刪除主題", + "purge": "清除", + "moderate": "編審", + "admin-dashboard": "儀表板", + "admin-categories": "版面", + "admin-privileges": "權限", + "admin-users": "使用者", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", + "admin-settings": "設定", + + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", + "alert.confirm-save": "Please confirm your intention to save these privileges", + "alert.saved": "Privilege changes saved and applied", + "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", + "alert.discarded": "Privilege changes discarded", + "alert.confirm-copyToAll": "Are you sure you wish to apply this set of %1 to all categories?", + "alert.confirm-copyToAllGroup": "Are you sure you wish to apply this group's set of %1 to all categories?", + "alert.confirm-copyToChildren": "Are you sure you wish to apply this set of %1 to all descendant (child) categories?", + "alert.confirm-copyToChildrenGroup": "Are you sure you wish to apply this group's set of %1 to all descendant (child) categories?", + "alert.no-undo": "This action cannot be undone.", + "alert.admin-warning": "Administrators implicitly get all privileges", + "alert.copyPrivilegesFrom-title": "Select a category to copy from", + "alert.copyPrivilegesFrom-warning": "This will copy %1 from the selected category.", + "alert.copyPrivilegesFromGroup-warning": "This will copy this group's set of %1 from the selected category." +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/registration.json b/public/language/zh-TW/admin/manage/registration.json new file mode 100644 index 0000000000..1ef5a5f23e --- /dev/null +++ b/public/language/zh-TW/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "申請", + "description": "註冊申請佇列裡面還沒有使用者申請。
要開啟這項功能,請去設定 → 使用者 → 使用者註冊 並設定註冊類型為“管理員批准”。", + + "list.name": "姓名", + "list.email": "郵件", + "list.ip": "IP", + "list.time": "時間", + "list.username-spam": "頻率: %1 顯示:%2 信心:%3", + "list.email-spam": "頻率:%1 顯示: %2", + "list.ip-spam": "頻率:%1 顯示: %2", + + "invitations": "邀請", + "invitations.description": "下面列出了所有已發送的邀請。您可以使用 Ctrl+F 快捷鍵搜索列表中的郵箱地址或帳戶。

如果使用者接受了邀請,他的帳戶將會被顯示在郵箱右邊。", + "invitations.inviter-username": "邀請人帳戶", + "invitations.invitee-email": "受邀請的電子信箱", + "invitations.invitee-username": "受邀請的帳戶(如果已經註冊)", + + "invitations.confirm-delete": "確認刪除這個邀請?" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/tags.json b/public/language/zh-TW/admin/manage/tags.json new file mode 100644 index 0000000000..aba257aa7b --- /dev/null +++ b/public/language/zh-TW/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "您的論壇目前沒有帶有標籤的主題。", + "bg-color": "背景顏色", + "text-color": "文字顏色", + "description": "透過點擊或者拖拉方式選擇標籤, 可同時使用 CTRL 來選擇多個標籤。", + "create": "建立標籤", + "modify": "修改標籤", + "rename": "重命名標籤", + "delete": "刪除所選標籤", + "search": "搜尋標籤...", + "settings": "標籤設定", + "name": "標籤名", + + "alerts.editing": "編輯(多個)標籤", + "alerts.confirm-delete": "是否要刪除所選標籤?", + "alerts.update-success": "標籤已更新!", + "reset-colors": "Reset colors" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/uploads.json b/public/language/zh-TW/admin/manage/uploads.json new file mode 100644 index 0000000000..e8a69ba6bd --- /dev/null +++ b/public/language/zh-TW/admin/manage/uploads.json @@ -0,0 +1,11 @@ +{ + "upload-file": "上傳檔案", + "filename": "檔案名", + "usage": "使用的貼文", + "orphaned": "未使用", + "size/filecount": "大小/檔案數", + "confirm-delete": "您確定要刪除此檔案嗎?", + "filecount": "%1 個檔案", + "new-folder": "New Folder", + "name-new-folder": "Enter a name for new the folder" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/users.json b/public/language/zh-TW/admin/manage/users.json new file mode 100644 index 0000000000..29427c692e --- /dev/null +++ b/public/language/zh-TW/admin/manage/users.json @@ -0,0 +1,112 @@ +{ + "users": "使用者", + "edit": "Actions", + "make-admin": "設為管理員", + "remove-admin": "撤銷管理員", + "validate-email": "驗證電郵地址", + "send-validation-email": "發送驗證郵件", + "password-reset-email": "發送密碼重設郵件", + "force-password-reset": "強制密碼重設 & 登入使用者已退出", + "ban": "封鎖使用者", + "temp-ban": "暫時封鎖使用者", + "unban": "解封使用者", + "reset-lockout": "重設封鎖", + "reset-flags": "重設舉報", + "delete": "Delete User(s)", + "delete-content": "Delete User(s) Content", + "purge": "Delete User(s) and Content", + "download-csv": "下載CSV", + "manage-groups": "管理群組", + "add-group": "新增至群組", + "create": "Create User", + "invite": "Invite by Email", + "new": "新建使用者", + "filter-by": "Filter by", + "pills.unvalidated": "未驗證", + "pills.validated": "Validated", + "pills.banned": "被封鎖", + + "50-per-page": "每頁50", + "100-per-page": "每頁100", + "250-per-page": "每頁250", + "500-per-page": "每頁500", + + "search.uid": "通過使用者ID", + "search.uid-placeholder": "搜尋使用者ID", + "search.username": "通過使用者名", + "search.username-placeholder": "輸入您想查詢的使用者名", + "search.email": "通過電郵地址", + "search.email-placeholder": "輸入您想查詢的電郵地址", + "search.ip": "通過IP地址", + "search.ip-placeholder": "輸入您想查詢的 IP 地址", + "search.not-found": "未找到使用者!", + + "inactive.3-months": "3個月", + "inactive.6-months": "6個月", + "inactive.12-months": "12個月", + + "users.uid": "UID", + "users.username": "使用者名", + "users.email": "電子郵件", + "users.no-email": "(no email)", + "users.ip": "IP", + "users.postcount": "發文數", + "users.reputation": "聲望", + "users.flags": "舉報", + "users.joined": "註冊時間", + "users.last-online": "最後在線", + "users.banned": "封鎖", + + "create.username": "使用者名", + "create.email": "電郵地址", + "create.email-placeholder": "該使用者的郵箱", + "create.password": "密碼", + "create.password-confirm": "確認密碼", + + "temp-ban.length": "Length", + "temp-ban.reason": "理由(可選)", + "temp-ban.hours": "小時", + "temp-ban.days": "天", + "temp-ban.explanation": "輸入停權持續時間。提示,時長為0視為永久停權。", + + "alerts.confirm-ban": "您確定要永久停權該用戶嗎?", + "alerts.confirm-ban-multi": "您確定要永久停權這些用戶嗎?", + "alerts.ban-success": "使用者已停權!", + "alerts.button-ban-x": "停權 %1 名使用者", + "alerts.unban-success": "使用者已復權!", + "alerts.lockout-reset-success": "鎖定已重設!", + "alerts.flag-reset-success": "舉報已重設!", + "alerts.no-remove-yourself-admin": "您無法撤銷自己的管理員身份!", + "alerts.make-admin-success": "該使用者已成為管理員", + "alerts.confirm-remove-admin": "您確定要刪除該管理員?", + "alerts.remove-admin-success": " 該使用者不再是管理員", + "alerts.make-global-mod-success": " 該使用者已成為管理員", + "alerts.confirm-remove-global-mod": "您確定要刪除該超級版主?", + "alerts.remove-global-mod-success": "該使用者已不再是管理員", + "alerts.make-moderator-success": " 該使用者已成為管理員", + "alerts.confirm-remove-moderator": "您確定要刪除該版主?", + "alerts.remove-moderator-success": "該使用者已不再是管理員", + "alerts.confirm-validate-email": "您確定要驗證這些使用者的電郵地址嗎?", + "alerts.confirm-force-password-reset": "你確定您想要向這個(這些)使用者強制密碼重設並退出嗎?", + "alerts.validate-email-success": "電郵地址已驗證", + "alerts.validate-force-password-reset-success": "用戶密碼已經被重設,現存的會話已經被移除", + "alerts.password-reset-confirm": "您確定要向這些使用者發送密碼重設郵件嗎?", + "alerts.password-reset-email-sent": "Password reset email sent.", + "alerts.confirm-delete": "Warning!

Do you really want to delete user(s)?

This action is not reversible! Only the user account will be deleted, their posts and topics will remain.

", + "alerts.delete-success": "使用者已刪除!", + "alerts.confirm-delete-content": "Warning!

Do you really want to delete these user(s) content?

This action is not reversible! The users' accounts will remain, but their posts and topics will be deleted.

", + "alerts.delete-content-success": "User(s) Content Deleted!", + "alerts.confirm-purge": "Warning!

Do you really want to delete user(s) and their content?

This action is not reversible! All user data and content will be erased!

", + "alerts.create": "建立使用者", + "alerts.button-create": "建立", + "alerts.button-cancel": "取消", + "alerts.error-passwords-different": "兩次輸入的密碼必須相同!", + "alerts.error-x": "錯誤

%1

", + "alerts.create-success": "使用者已建立!", + + "alerts.prompt-email": "電郵地址:", + "alerts.email-sent-to": "已發送邀請給 %1", + "alerts.x-users-found": "%1 user(s) found, (%2 seconds)", + "export-users-started": "Exporting users as csv, this might take a while. You will receive a notification when it is complete.", + "export-users-completed": "Users exported as csv, click here to download." +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/menu.json b/public/language/zh-TW/admin/menu.json new file mode 100644 index 0000000000..d8804bbfd0 --- /dev/null +++ b/public/language/zh-TW/admin/menu.json @@ -0,0 +1,89 @@ +{ + "section-dashboard": "Dashboards", + "dashboard/overview": "Overview", + "dashboard/logins": "Logins", + "dashboard/users": "Users", + "dashboard/topics": "Topics", + "dashboard/searches": "Searches", + "section-general": "基本", + + "section-manage": "管理", + "manage/categories": "版面", + "manage/privileges": "權限", + "manage/tags": "標籤", + "manage/users": "使用者", + "manage/admins-mods": "權限分配", + "manage/registration": "註冊申請", + "manage/post-queue": "貼文隊列", + "manage/groups": "群組", + "manage/ip-blacklist": "IP 黑名單", + "manage/uploads": "上傳", + "manage/digest": "摘要", + + "section-settings": "設定", + "settings/general": "基本", + "settings/homepage": "首頁", + "settings/navigation": "導航", + "settings/reputation": "Reputation & Flags", + "settings/email": "郵件", + "settings/user": "使用者", + "settings/group": "群組", + "settings/guest": "訪客", + "settings/uploads": "上傳", + "settings/languages": "語言", + "settings/post": "貼文", + "settings/chat": "聊天", + "settings/pagination": "分頁", + "settings/tags": "標籤", + "settings/notifications": "通知", + "settings/api": "API Access", + "settings/sounds": "聲音", + "settings/social": "社交", + "settings/cookies": "Cookies", + "settings/web-crawler": "Web 爬蟲", + "settings/sockets": "網路接口", + "settings/advanced": "進階", + + "settings.page-title": "%1 設置", + + "section-appearance": "外觀", + "appearance/themes": "佈景主題", + "appearance/skins": "配色", + "appearance/customise": "自訂程式碼 (HTML/JavaScript/CSS)", + + "section-extend": "擴展", + "extend/plugins": "外掛", + "extend/widgets": "小工具", + "extend/rewards": "獎勵", + + "section-social-auth": "社交認證", + + "section-plugins": "外掛", + "extend/plugins.install": "已安裝", + + "section-advanced": "進階", + "advanced/database": "資料庫", + "advanced/events": "事件", + "advanced/hooks": "掛鉤", + "advanced/logs": "日誌", + "advanced/errors": "錯誤", + "advanced/cache": "快取", + "development/logger": "記錄器", + "development/info": "資訊", + + "rebuild-and-restart-forum": "部署並重啟論壇", + "restart-forum": "重啟論壇", + "logout": "登出", + "view-forum": "檢視論壇", + + "search.placeholder": "Search settings", + "search.no-results": "沒有可用結果…", + "search.search-forum": "搜索論壇為", + "search.keep-typing": "輸入更多以查看結果...", + "search.start-typing": "開始輸入以查看結果...", + + "connection-lost": "與 %1 的連線已中斷,正嘗試重新連接...", + + "alerts.version": "正在運行 NodeBB v%1", + "alerts.upgrade": "升級到 v%1" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/advanced.json b/public/language/zh-TW/admin/settings/advanced.json new file mode 100644 index 0000000000..454475cb71 --- /dev/null +++ b/public/language/zh-TW/admin/settings/advanced.json @@ -0,0 +1,50 @@ +{ + "maintenance-mode": "維護模式", + "maintenance-mode.help": "當論壇處在維護模式時,所有請求將被重導向到一個靜態頁面。管理員不受重導向限制,並可正常訪問網站。", + "maintenance-mode.status": "維護模式狀態碼", + "maintenance-mode.message": "維護訊息", + "maintenance-mode.groups-exempt-from-maintenance-mode": "Select groups that should be exempt from maintenance mode", + "headers": "標題", + "headers.allow-from": "設定 ALLOW-FROM 來放置 NodeBB 於 iFrame 中", + "headers.csp-frame-ancestors": "Set Content-Security-Policy frame-ancestors header to Place NodeBB in an iFrame", + "headers.csp-frame-ancestors-help": "'none', 'self'(default) or list of URIs to allow.", + "headers.powered-by": "自訂由 NodeBB 發送的 \"Powered By\" 標頭 ", + "headers.acao": "Access-Control-Allow-Origin", + "headers.acao-regex": "Access-Control-Allow-Origin 正規表示法", + "headers.acao-help": "要拒絕所有網站,請留空", + "headers.acao-regex-help": "輸入正規表示法以匹配動態來源。要拒絕所有網站,請留空", + "headers.acac": "Access-Control-Allow-Credentials", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "headers.coep": "Cross-Origin-Embedder-Policy", + "headers.coep-help": "When enabled (default), will set the header to require-corp", + "headers.coop": "Cross-Origin-Opener-Policy", + "headers.corp": "Cross-Origin-Resource-Policy", + "headers.permissions-policy": "Permissions-Policy", + "headers.permissions-policy-help": "Allows setting permissions policy header, for example \"geolocation=*, camera=()\", see this for more info.", + "hsts": "嚴格安全傳輸", + "hsts.enabled": "啟用HSTS(推薦)", + "hsts.maxAge": "HSTS Max Age", + "hsts.subdomains": "HSTS標頭訊息包含的域名", + "hsts.preload": "允許在HSTS標頭中預加載", + "hsts.help": "如果啟用此項,網站將會向瀏覽器發送HSTS標頭訊息。您可以設定是否為子域名開啟HSTS,以及HSTS標頭訊息中是否包含預加載標誌。如果您不瞭解HSTS,可以忽略此項設定。瞭解詳情 ", + "traffic-management": "流量管理", + "traffic.help": "NodeBB uses a module that automatically denies requests in high-traffic situations. You can tune these settings here, although the defaults are a good starting point.", + "traffic.enable": "啟用流量管理", + "traffic.event-lag": "事件循環滯後門檻值(毫秒)", + "traffic.event-lag-help": "降低此值會減少頁面加載的等待時間,但也會向更多使用者顯示“過載”訊息。(需要重新啟動)", + "traffic.lag-check-interval": "檢查間隔(毫秒)", + "traffic.lag-check-interval-help": "降低此值會造成 NodeBB 的負載峰值變得更加敏感,但也可能導致檢查變得過於敏感(需要重新啟動)", + + "sockets.settings": "WebSocket 設定", + "sockets.max-attempts": "最大重連次數", + "sockets.default-placeholder": "預設: %1", + "sockets.delay": "重新連線延遲時間", + + "analytics.settings": "Analytics Settings", + "analytics.max-cache": "Analytics Cache Max Value", + "analytics.max-cache-help": "On high-traffic installs, the cache could be exhausted continuously if there are more concurrent active users than the Max Cache value. (Restart required)", + "compression.settings": "Compression Settings", + "compression.enable": "Enable Compression", + "compression.help": "This setting enables gzip compression. For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level. You can enable it here for testing purposes." +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/api.json b/public/language/zh-TW/admin/settings/api.json new file mode 100644 index 0000000000..50892925f3 --- /dev/null +++ b/public/language/zh-TW/admin/settings/api.json @@ -0,0 +1,16 @@ +{ + "tokens": "Tokens", + "settings": "Settings", + "lead-text": "From this page you can configure access to the Write API in NodeBB.", + "intro": "By default, the Write API authenticates users based on their session cookie, but NodeBB also supports Bearer authentication via tokens generated via this page.", + "docs": "Click here to access the full API specification", + + "require-https": "Require API usage via HTTPS only", + "require-https-caveat": "Note: Some installations involving load balancers may proxy their requests to NodeBB using HTTP, in which case this option should remain disabled.", + + "uid": "User ID", + "uid-help-text": "Specify a User ID to associate with this token. If the user ID is 0, it will be considered a master token, which can assume the identity of other users based on the _uid parameter", + "description": "Description", + "no-description": "No description specified.", + "token-on-save": "Token will be generated once form is saved" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/chat.json b/public/language/zh-TW/admin/settings/chat.json new file mode 100644 index 0000000000..a98f078d7d --- /dev/null +++ b/public/language/zh-TW/admin/settings/chat.json @@ -0,0 +1,12 @@ +{ + "chat-settings": "聊天設定", + "disable": "禁用聊天", + "disable-editing": "禁止編輯/刪除聊天消息", + "disable-editing-help": "管理員和超級版主不受此限制", + "max-length": "聊天訊息的最大長度", + "max-room-size": "聊天室的最多使用者數", + "delay": "聊天訊息間的毫秒數", + "notification-delay": "Notification delay for chat messages. (0 for no delay)", + "restrictions.seconds-edit-after": "使用者在發佈聊天訊息後允許編輯貼文的秒數(0為禁用)", + "restrictions.seconds-delete-after": "使用者在發佈聊天訊息後允許刪除貼文的秒數(0為禁用)" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/cookies.json b/public/language/zh-TW/admin/settings/cookies.json new file mode 100644 index 0000000000..57645bb7ee --- /dev/null +++ b/public/language/zh-TW/admin/settings/cookies.json @@ -0,0 +1,13 @@ +{ + "eu-consent": "歐盟 Cookies 政策", + "consent.enabled": "啟用選項", + "consent.message": "通知訊息", + "consent.acceptance": "接受訊息", + "consent.link-text": "政策連結文字", + "consent.link-url": "政策地址連結", + "consent.blank-localised-default": "留白以便使用 NodeBB 本地預設值", + "settings": "設定", + "cookie-domain": "Session cookie 域名", + "max-user-sessions": "Max active sessions per user", + "blank-default": "留白以保持預設" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/email.json b/public/language/zh-TW/admin/settings/email.json new file mode 100644 index 0000000000..3289f089ee --- /dev/null +++ b/public/language/zh-TW/admin/settings/email.json @@ -0,0 +1,52 @@ +{ + "email-settings": "郵件設定", + "address": "電子郵件地址", + "address-help": "下面的電子郵件地址代表收件人在“發送人”和“回覆”中所看到的地址。", + "from": "發送人", + "from-help": "用於郵件中顯示的發送人", + + "confirmation-settings": "Confirmation", + "confirmation.expiry": "Hours to keep email confirmation link valid", + + "smtp-transport": "SMTP 通信", + "smtp-transport.enabled": "Enable SMTP Transport", + "smtp-transport-help": "您可以從列表中選取一個已知的服務或自訂。", + "smtp-transport.service": "選擇服務", + "smtp-transport.service-custom": "自訂", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select "Custom Service" and enter the details below.", + "smtp-transport.gmail-warning1": "If you are using GMail as your email provider, you will have to generate an "App Password" in order for NodeBB to authenticate successfully. You can generate one at the App Passwords page.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.auto-enable-toast": "It looks like you're configuring an SMTP transport. We enabled the \"SMTP Transport\" option for you.", + "smtp-transport.host": "SMTP 主機名", + "smtp-transport.port": "SMTP 通訊埠", + "smtp-transport.security": "連線安全設置", + "smtp-transport.security-encrypted": "加密的", + "smtp-transport.security-starttls": "StartTLS", + "smtp-transport.security-none": "無", + "smtp-transport.username": "使用者名", + "smtp-transport.username-help": "對於Gmail服務,請在這裡輸入完整的電子信箱地址,尤其是如果您使用的是 Google Apps 託管的域名。", + "smtp-transport.password": "密碼", + "smtp-transport.pool": "Enable pooled connections", + "smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.", + + "template": "編輯電子郵件樣板", + "template.select": "選擇電子郵件樣板", + "template.revert": "還原為初始樣板", + "testing": "電子郵件測試", + "testing.select": "選擇電子郵件樣板", + "testing.send": "發送測試電子郵件", + "testing.send-help": "測試電子郵件將被發送到當前已登入的使用者的電郵地址。", + "subscriptions": "電子郵件摘要", + "subscriptions.disable": "禁用電子郵件摘要", + "subscriptions.hour": "摘要小時", + "subscriptions.hour-help": "請輸入一個代表小時的數字來發送排程的電子郵件摘要 (例如,對於午夜,0,對於下午5:00,17)。 請記住,這是根據伺服器本身的時間,可能與您的系統時鐘不完全符合。
伺服器的大致時間為:
下一個每日摘要被排程在發送", + "notifications.remove-images": "Remove images from email notifications", + "require-email-address": "Require new users to specify an email address", + "require-email-address-warning": "By default, users can opt-out of entering an email address by leaving the field blank. Enabling this option means they have to enter an email address in order to proceed with registration. It does not ensure user will enter a real email address, nor even an address they own.", + "send-validation-email": "Send validation emails when an email is added or changed", + "include-unverified-emails": "Send emails to recipients who have not explicitly confirmed their emails", + "include-unverified-warning": "By default, users with emails associated with their account have already been verified, but there are situations where this is not the case (e.g. SSO logins, grandfathered users, etc). Enable this setting at your own risk – sending emails to unverified addresses may be a violation of regional anti-spam laws.", + "prompt": "Prompt users to enter or confirm their emails", + "prompt-help": "If a user does not have an email set, or their email is not confirmed, a warning will be shown on screen.", + "sendEmailToBanned": "Send emails to users even if they have been banned" +} diff --git a/public/language/zh-TW/admin/settings/general.json b/public/language/zh-TW/admin/settings/general.json new file mode 100644 index 0000000000..28f7187fee --- /dev/null +++ b/public/language/zh-TW/admin/settings/general.json @@ -0,0 +1,50 @@ +{ + "site-settings": "網站設定", + "title": "網站標題", + "title.short": "短標題", + "title.short-placeholder": "如短標題為指定則會使用網站標題", + "title.url": "Title Link URL", + "title.url-placeholder": "網站標題連結", + "title.url-help": "When the title is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "title.name": "您的社區名稱", + "title.show-in-header": "在頂部顯示網站標題", + "browser-title": "瀏覽器標題", + "browser-title-help": "如果沒有指定瀏覽器標題,將會使用網站標題", + "title-layout": "標題佈局", + "title-layout-help": "定義瀏覽器標題的佈局,即{pageTitle} | {browserTitle}", + "description.placeholder": "關於您的社區的簡短說明", + "description": "網站描述", + "keywords": "網站關鍵字", + "keywords-placeholder": "描述您的社區的關鍵字,以逗號分隔", + "logo": "網站 Logo", + "logo.image": "圖檔", + "logo.image-placeholder": "要在論壇標題上顯示的 Logo 的路徑", + "logo.upload": "上傳", + "logo.url": "Logo Link URL", + "logo.url-placeholder": "網站 Logo 連結", + "logo.url-help": "When the logo is clicked, send users to this address. If left blank, user will be sent to the forum index.
Note: This is not the external URL used in emails, etc. That is set by the url property in config.json", + "logo.alt-text": "替代文字", + "log.alt-text-placeholder": "輔助功能的替代文字", + "favicon": "網站圖示", + "favicon.upload": "上傳", + "pwa": "Progressive Web App", + "touch-icon": "Touch Icon", + "touch-icon.upload": "上傳", + "touch-icon.help": "Recommended size and format: 512x512, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "maskable-icon": "Maskable (Homescreen) Icon", + "maskable-icon.help": "Recommended size and format: 512x512, PNG format only. If no maskable icon is specified, NodeBB will fall back to the Touch Icon.", + "outgoing-links": "站外連結", + "outgoing-links.warning-page": "使用站外連結警告頁", + "search": "Search", + "search-default-in": "Search In", + "search-default-in-quick": "Quick Search In", + "search-default-sort-by": "Sort by", + "outgoing-links.whitelist": "新增域名到白名單以繞過警告頁面", + "site-colors": "網站顏色仲介資料", + "theme-color": "主題顏色", + "background-color": "背景顏色", + "background-color-help": "當網站以 PWA 方式安裝時起始視窗的背景顏色", + "undo-timeout": "Undo Timeout", + "undo-timeout-help": "Some operations such as moving topics will allow for the moderator to undo their action within a certain timeframe. Set to 0 to disable undo completely.", + "topic-tools": "Topic Tools" +} diff --git a/public/language/zh-TW/admin/settings/group.json b/public/language/zh-TW/admin/settings/group.json new file mode 100644 index 0000000000..e4995dd56a --- /dev/null +++ b/public/language/zh-TW/admin/settings/group.json @@ -0,0 +1,13 @@ +{ + "general": "基本", + "private-groups": "私有群組", + "private-groups.help": "啟用此選項後,加入群組需要群組所有者核可(預設啟用)。", + "private-groups.warning": "注意!如果這個選項未啟用並且你有私有群組,那麼你的群組將變為公共的。", + "allow-multiple-badges": "允許多個徽章", + "allow-multiple-badges-help": "啟用此選項後,使用者可以選擇顯示多個群組徽章,需要主題支持。", + "max-name-length": "群組名字的最大長度", + "max-title-length": "群組標題最大長度", + "cover-image": "群組封面圖片", + "default-cover": "預設封面圖片", + "default-cover-help": "為沒有上傳封面圖片的群組增加以逗號分隔的預設封面圖片" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/guest.json b/public/language/zh-TW/admin/settings/guest.json new file mode 100644 index 0000000000..a46b34a3f9 --- /dev/null +++ b/public/language/zh-TW/admin/settings/guest.json @@ -0,0 +1,7 @@ +{ + "settings": "Settings", + "handles.enabled": "允許訪客使用者名", + "handles.enabled-help": "這個選項將允許訪客使用一個額外的輸入框來設置發文時的使用者名,如果被禁用,僅會統一顯示為“訪客”", + "topic-views.enabled": "Allow guests to increase topic view counts", + "reply-notifications.enabled": "Allow guests to generate reply notifications" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/homepage.json b/public/language/zh-TW/admin/settings/homepage.json new file mode 100644 index 0000000000..28e579ad56 --- /dev/null +++ b/public/language/zh-TW/admin/settings/homepage.json @@ -0,0 +1,8 @@ +{ + "home-page": "首頁", + "description": "請選擇使用者到達根 URL 時所顯示的頁面。", + "home-page-route": "首頁路徑", + "custom-route": "自訂路徑", + "allow-user-home-pages": "允許使用者自訂首頁", + "home-page-title": "首頁標題(預設為“Home”)" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/languages.json b/public/language/zh-TW/admin/settings/languages.json new file mode 100644 index 0000000000..c8f7db09de --- /dev/null +++ b/public/language/zh-TW/admin/settings/languages.json @@ -0,0 +1,6 @@ +{ + "language-settings": "語言設定", + "description": "預設語言會決定所有使用者的語言設定。
單一使用者可以各自在帳戶設定中覆蓋此項設定。", + "default-language": "預設語言", + "auto-detect": "自動檢測訪客的語言設定" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/navigation.json b/public/language/zh-TW/admin/settings/navigation.json new file mode 100644 index 0000000000..de20547ab9 --- /dev/null +++ b/public/language/zh-TW/admin/settings/navigation.json @@ -0,0 +1,25 @@ +{ + "icon": "圖示:", + "change-icon": "更改", + "route": "路徑:", + "tooltip": "提示:", + "text": "文字:", + "text-class": "文字類別:可選", + "class": "類: 可選", + "id": "ID:可選", + + "properties": "屬性:", + "groups": "群組:", + "open-new-window": "在新窗口中打開", + "dropdown": "Dropdown", + "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a href="https://myforum.com">Link 1</a></li>", + + "btn.delete": "刪除", + "btn.disable": "禁用", + "btn.enable": "啟用", + + "available-menu-items": "可用的選單項目", + "custom-route": "自訂路徑", + "core": "核心", + "plugin": "外掛" +} diff --git a/public/language/zh-TW/admin/settings/notifications.json b/public/language/zh-TW/admin/settings/notifications.json new file mode 100644 index 0000000000..991f4e19d6 --- /dev/null +++ b/public/language/zh-TW/admin/settings/notifications.json @@ -0,0 +1,7 @@ +{ + "notifications": "通知", + "welcome-notification": "歡迎通知", + "welcome-notification-link": "歡迎通知連結", + "welcome-notification-uid": "歡迎通知使用者 (UID)", + "post-queue-notification-uid": "Post Queue User (UID)" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/pagination.json b/public/language/zh-TW/admin/settings/pagination.json new file mode 100644 index 0000000000..ffe8b6a4ee --- /dev/null +++ b/public/language/zh-TW/admin/settings/pagination.json @@ -0,0 +1,12 @@ +{ + "pagination": "分頁設定", + "enable": "在主題和文章使用分頁替代無限滾動瀏覽。", + "posts": "Post Pagination", + "topics": "主題分頁", + "posts-per-page": "每頁文章數", + "max-posts-per-page": "每頁最多文章數", + "categories": "版面分頁", + "topics-per-page": "每頁主題數", + "max-topics-per-page": "每頁最多主題數", + "categories-per-page": "Categories per page" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/post.json b/public/language/zh-TW/admin/settings/post.json new file mode 100644 index 0000000000..3b3bfd7f10 --- /dev/null +++ b/public/language/zh-TW/admin/settings/post.json @@ -0,0 +1,67 @@ +{ + "sorting": "貼文排序", + "sorting.post-default": "預設貼文排序", + "sorting.oldest-to-newest": "從舊到新", + "sorting.newest-to-oldest": "從新到舊", + "sorting.most-votes": "最多點贊", + "sorting.most-posts": "最多回覆", + "sorting.topic-default": "預設主題排序", + "length": "貼文字數", + "post-queue": "貼文隊列", + "restrictions": "貼文限制", + "restrictions-new": "新使用者限制", + "restrictions.post-queue": "啟用貼文隊列", + "restrictions.post-queue-rep-threshold": "忽略貼文隊列的聲望值", + "restrictions.groups-exempt-from-post-queue": "選擇豁免貼文隊列的群組", + "restrictions-new.post-queue": "啟用新使用者限制", + "restrictions.post-queue-help": "啟用貼文審查會將新使用者的貼文放入審查佇列", + "restrictions-new.post-queue-help": "啟用新使用者限制將對新使用者張貼的文章設定限制", + "restrictions.seconds-between": "貼文間隔時間(秒)", + "restrictions.seconds-between-new": "新使用者貼文間隔時間(秒)", + "restrictions.rep-threshold": "取消貼文限制所需的聲望值", + "restrictions.seconds-before-new": "新使用者可以在第一次發佈之前的秒數", + "restrictions.seconds-edit-after": "貼文保持可編輯的秒數(設定為0表示禁用)", + "restrictions.seconds-delete-after": "貼文保持可刪除的秒數(設定為0表示禁用)", + "restrictions.replies-no-delete": "在使用者被禁止刪除自己的主題後的回覆數。 (0為禁用) ", + "restrictions.min-title-length": "標題字數下限", + "restrictions.max-title-length": "標題字數上限", + "restrictions.min-post-length": "貼文字數下限", + "restrictions.max-post-length": "貼文字數上限", + "restrictions.days-until-stale": "主題過時時間(天)", + "restrictions.stale-help": "如果某個主題被視為“過時”,則會向嘗試回覆該主題的使用者顯示警告。", + "timestamp": "時間郵戳", + "timestamp.cut-off": "截止日期(天)", + "timestamp.cut-off-help": "日期&時間將以相對方式 (例如,“3小時前” / “5天前”) 顯示,並且會依照訪客語言時區轉換。在某一時刻之後,可以切換該文字以顯示本地化日期本身 (例如2016年11月5日15:30) 。
(預設值: 30 或一個月) 。 設定為0可總是顯示日期,留白以總是顯示相對時間。", + "timestamp.necro-threshold": "挖墳警告(單位:天)", + "timestamp.necro-threshold-help": "若進行回覆的貼文最後回覆的時間早於挖墳警告設定的天數,則在嘗試回覆前顯示挖墳警告(預設:7天)。可以設定為 0 來禁用。", + "timestamp.topic-views-interval": "Increment topic views interval (in minutes)", + "timestamp.topic-views-interval-help": "Topic views will only increment once every X minutes as defined by this setting.", + "teaser": "貼文預覽", + "teaser.last-post": "最後– 顯示最新的貼文,包括原帖,如果沒有回覆", + "teaser.last-reply": "最後– 顯示最新回覆,如果沒有回覆,則顯示“無回覆”佔位符", + "teaser.first": "第一", + "showPostPreviewsOnHover": "Show a preview of posts when mouse overed", + "unread": "未讀設定", + "unread.cutoff": "未讀截止時間(天)", + "unread.min-track-last": "跟蹤最後閱讀之前的主題最小貼文", + "recent": "最近設定", + "recent.max-topics": "Maximum topics on /recent", + "recent.categoryFilter.disable": "禁用對 /recent 頁面上忽略版面中的主題進行過濾", + "signature": "簽名設定", + "signature.disable": "禁用簽名", + "signature.no-links": "禁用簽名中的連結", + "signature.no-images": "禁用簽名中的圖片", + "signature.hide-duplicates": "Hide duplicate signatures in topics", + "signature.max-length": "簽名字數上限", + "composer": "編輯器設定", + "composer-help": "以下設定控制所示後期編輯器的功能和/或外觀\n\t\t\t\t當使用者建立新主題或回覆現有主題時。", + "composer.show-help": "顯示“幫助”選項卡", + "composer.enable-plugin-help": "允許外掛添加內容到幫助選項卡", + "composer.custom-help": "自訂幫助文字", + "backlinks": "Backlinks", + "backlinks.enabled": "Enable topic backlinks", + "backlinks.help": "If a post references another topic, a link back to the post will be inserted into the referenced topic at that point in time.", + "ip-tracking": "IP 跟蹤", + "ip-tracking.each-post": "跟蹤每個貼文的 IP 地址", + "enable-post-history": "啟用回覆歷史" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/reputation.json b/public/language/zh-TW/admin/settings/reputation.json new file mode 100644 index 0000000000..6be88d9e23 --- /dev/null +++ b/public/language/zh-TW/admin/settings/reputation.json @@ -0,0 +1,31 @@ +{ + "reputation": "聲望設定", + "disable": "停用 聲望系統", + "disable-down-voting": "停用 倒讚", + "votes-are-public": "公開所有讚", + "thresholds": "操作限制", + "min-rep-upvote": "Minimum reputation to upvote posts", + "upvotes-per-day": "Upvotes per day (set to 0 for unlimited upvotes)", + "upvotes-per-user-per-day": "Upvotes per user per day (set to 0 for unlimited upvotes)", + "min-rep-downvote": "倒讚貼文 需要的最低聲望", + "downvotes-per-day": "Downvotes per day (set to 0 for unlimited downvotes)", + "downvotes-per-user-per-day": "Downvotes per user per day (set to 0 for unlimited downvotes)", + "min-rep-chat": "Minimum reputation to send chat messages", + "min-rep-flag": "舉報貼文 需要的最低聲望", + "min-rep-website": "加入 個人網站 需要的最低聲望", + "min-rep-aboutme": "加入 個人 “關於我”頁 需要的最低聲望", + "min-rep-signature": "加入 簽名檔 需要的最低聲望", + "min-rep-profile-picture": "加入 個人頭像 需要的最低聲望", + "min-rep-cover-picture": "加入 個人封面圖片 需要的最低聲望", + + "flags": "Flag Settings", + "flags.limit-per-target": "Maximum number of times something can be flagged", + "flags.limit-per-target-placeholder": "Default: 0", + "flags.limit-per-target-help": "When a post or user is flagged multiple times, each additional flag is considered a "report" and added to the original flag. Set this option to a number other than zero to limit the number of reports an item can receive.", + "flags.auto-flag-on-downvote-threshold": "Number of downvotes to auto flag posts (Set to 0 to disable, default: 0)", + "flags.auto-resolve-on-ban": "Automatically resolve all of a user's tickets when they are banned", + "flags.action-on-resolve": "Do the following when a flag is resolved", + "flags.action-on-reject": "Do the following when a flag is rejected", + "flags.action.nothing": "Do nothing", + "flags.action.rescind": "Rescind the notification send to moderators/administrators" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/social.json b/public/language/zh-TW/admin/settings/social.json new file mode 100644 index 0000000000..aa16e3b8f5 --- /dev/null +++ b/public/language/zh-TW/admin/settings/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "貼文分享", + "info-plugins-additional": "外掛可以增加額外用於分享貼文的社群媒體。", + "save-success": "已成功儲存貼文分享社群媒體。" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/sockets.json b/public/language/zh-TW/admin/settings/sockets.json new file mode 100644 index 0000000000..37e7ce858f --- /dev/null +++ b/public/language/zh-TW/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "重新連接設定", + "max-attempts": "最大重試次數", + "default-placeholder": "預設: %1", + "delay": "重新連接延遲時間" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/sounds.json b/public/language/zh-TW/admin/settings/sounds.json new file mode 100644 index 0000000000..e30202df06 --- /dev/null +++ b/public/language/zh-TW/admin/settings/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "通知", + "chat-messages": "聊天訊息", + "play-sound": "播放", + "incoming-message": "收到的訊息", + "outgoing-message": "發出的訊息", + "upload-new-sound": "上傳新的音檔", + "saved": "設定已儲存" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/tags.json b/public/language/zh-TW/admin/settings/tags.json new file mode 100644 index 0000000000..7a2c2737a2 --- /dev/null +++ b/public/language/zh-TW/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "標籤設定", + "link-to-manage": "管理標籤", + "system-tags": "System Tags", + "system-tags-help": "Only privileged users will be able to use these tags.", + "min-per-topic": "每個主題的最少標籤數", + "max-per-topic": "每話題的最大標籤數", + "min-length": "最短標籤長度", + "max-length": "最大標籤長度", + "related-topics": "相關主題", + "max-related-topics": "最大相關主題顯示量(如果主題支持)" +} \ No newline at end of file diff --git a/public/language/zh-TW/admin/settings/uploads.json b/public/language/zh-TW/admin/settings/uploads.json new file mode 100644 index 0000000000..a69d6b8abf --- /dev/null +++ b/public/language/zh-TW/admin/settings/uploads.json @@ -0,0 +1,45 @@ +{ + "posts": "貼文", + "orphans": "Orphaned Files", + "private": "使上傳的檔案私有化", + "strip-exif-data": "去除 EXIF 資料", + "preserve-orphaned-uploads": "Keep uploaded files on disk after a post is purged", + "orphanExpiryDays": "Days to keep orphaned files", + "orphanExpiryDays-help": "After this many days, orphaned uploads will be deleted from the file system.
Set 0 or leave blank to disable.", + "private-extensions": "自訂檔案附檔名", + "private-uploads-extensions-help": "在此處輸入以逗號分隔的副檔名列表 (例如 pdf,xls,doc )並將其用於自訂。為空則表示允許所有副檔名。", + "resize-image-width-threshold": "如果圖片寬度超過指定大小,則對圖片進行縮放", + "resize-image-width-threshold-help": "(像素單位,預設 1520 px,設定為0以停用)", + "resize-image-width": "縮小圖片到指定寬度", + "resize-image-width-help": "(像素單位,預設 760 px,設定為0以停用)", + "resize-image-quality": "調整圖片大小時使用的品質", + "resize-image-quality-help": "使用較低品質的設定來減小調整過大小的圖片的檔案大小", + "max-file-size": "最大檔案大小(單位 KiB)", + "max-file-size-help": "(單位 KiB ,預設 2048KiB)", + "reject-image-width": "圖片最大寬度值(單位:像素)", + "reject-image-width-help": "寬於此數值大小的圖片將會被拒絕", + "reject-image-height": "圖片最大高度值(單位:像素)", + "reject-image-height-help": "高於此數值大小的圖片將會被拒絕", + "allow-topic-thumbnails": "允許使用者上傳主題縮圖", + "topic-thumb-size": "主題縮圖大小", + "allowed-file-extensions": "允許的副檔名", + "allowed-file-extensions-help": "在此處輸入以逗號分隔的副檔名列表 (例如 pdf,xls,doc )。 為空則表示允許所有副檔名。", + "upload-limit-threshold": "Rate limit user uploads to:", + "upload-limit-threshold-per-minute": "Per %1 Minute", + "upload-limit-threshold-per-minutes": "Per %1 Minutes", + "profile-avatars": "個人頭像", + "allow-profile-image-uploads": "允許使用者上傳個人頭像", + "convert-profile-image-png": "轉換個人頭像為 PNG 格式", + "default-avatar": "訪客預設頭像", + "upload": "上傳", + "profile-image-dimension": "個人頭像尺寸", + "profile-image-dimension-help": "(使用 px 作為單位,預設:128px)", + "max-profile-image-size": "個人頭像最大大小", + "max-profile-image-size-help": "(單位 KiB ,預設 256 KiB)", + "max-cover-image-size": "最大封面圖片檔案大小", + "max-cover-image-size-help": "(單位 KiB,預設 2048KiB)", + "keep-all-user-images": "在伺服器上保留舊頭像和舊的個人封面", + "profile-covers": "個人封面", + "default-covers": "預設封面圖片", + "default-covers-help": "為沒有上傳封面圖片的帳戶添加以逗號分隔的預設封面圖片" +} diff --git a/public/language/zh-TW/admin/settings/user.json b/public/language/zh-TW/admin/settings/user.json new file mode 100644 index 0000000000..6848e90747 --- /dev/null +++ b/public/language/zh-TW/admin/settings/user.json @@ -0,0 +1,83 @@ +{ + "authentication": "驗證", + "email-confirm-interval": "使用者無法重新發送電子信箱確認信直到", + "email-confirm-interval2": "minutes have elapsed", + "allow-login-with": "允許使用何種登入名", + "allow-login-with.username-email": "使用者名或者電子信箱", + "allow-login-with.username": "僅限使用者名", + "account-settings": "使用者設定", + "gdpr_enabled": "啟用通用資料保護條例許可的個人資料收集", + "gdpr_enabled_help": "當啟用時,所有新註冊使用者需要明確同意允許資料收集和在 通用資料保護法 (GDPR)範圍內的使用。 注意: 開啟GDPR不一定要之前已經存在的用戶同意。在這之前,你需要去安裝GDPR插件。", + "disable-username-changes": "停用修改使用者名", + "disable-email-changes": "停用修改電子信箱", + "disable-password-changes": "停用修改密碼", + "allow-account-deletion": "允許刪除帳戶", + "hide-fullname": "隱藏使用者的全名", + "hide-email": "隱藏使用者的電子信箱", + "show-fullname-as-displayname": "Show user's full name as their display name if available", + "themes": "佈景主題", + "disable-user-skins": "阻止使用者選擇自訂配色", + "account-protection": "帳戶保護", + "admin-relogin-duration": "管理員無操作自動退出持續時間 (分鐘)", + "admin-relogin-duration-help": "訪問控制面板一段時間後需要重新登入以保證控制面板的安全,設定為0以停用。", + "login-attempts": "每小時嘗試登入次數", + "login-attempts-help": "如果使用者的嘗試登入次數超過此界限,該帳戶將會被被鎖定預設的時間。", + "lockout-duration": "帳戶鎖定時間(分鐘)", + "login-days": "記錄使用者會話天數", + "password-expiry-days": "強制重設密碼天數", + "session-time": "Session 過期時間", + "session-time-days": "天", + "session-time-seconds": "秒", + "session-time-help": "這些值將用於控制使用者在登入時選中"記住我"後能夠保持登入的持續時間。注意以下數字中只有一個將被使用。若值為空我們將改為使用。若值為空我們將使用預設值14天。", + "online-cutoff": "分鐘後認定使用者已離線", + "online-cutoff-help": "若使用者在此時間後未作出任何動作,他們將被視為不活躍狀態且不會收到即時更新。", + "registration": "使用者註冊", + "registration-type": "註冊方式", + "registration-approval-type": "註冊批准類型", + "registration-type.normal": "一般", + "registration-type.admin-approval": "管理員批准", + "registration-type.admin-approval-ip": "管理員批准 IP地址", + "registration-type.invite-only": "僅限邀請", + "registration-type.admin-invite-only": "僅限管理員邀請", + "registration-type.disabled": "停用註冊", + "registration-type.help": "一般 - 使用者可以通過 /register頁面註冊
\n管理員批准 - 使用者註冊請求會被放入 請求佇列待管理員批准。
\n邀請制 - 使用者可以通過 使用者 頁面邀請其他使用者。\n管理員邀請制 - 只有管理員可以通過 使用者admin/manage/users 頁面邀請其他使用者。
\n停用註冊 - 不開放用戶註冊。
", + "registration-approval-type.help": "通常 - 用戶可以通過/register頁面註冊
\n管理員批准 - 用戶註冊請求會被放入 請求隊列 待管理員批准。
\n管理員批准 IP地址 - 新用戶不受影響,已存在賬號的IP地址註冊需要管理員批准。
", + "registration-queue-auto-approve-time": "Automatic Approval Time", + "registration-queue-auto-approve-time-help": "Hours before user is approved automatically. 0 to disable.", + "registration-queue-show-average-time": "Show users average time it takes to approve a new user", + "registration.max-invites": "每個使用者最大邀請數", + "max-invites": "每個使用者最大邀請數", + "max-invites-help": "無限制填 0 。管理員沒有邀請限制
僅在邀請制時可用", + "invite-expiration": "邀請過期", + "invite-expiration-help": "邀請在#日過期。", + "min-username-length": "最小使用者名長度", + "max-username-length": "最大使用者名長度", + "min-password-length": "最小密碼長度", + "min-password-strength": "最小密碼強度", + "max-about-me-length": "自我介紹的最大長度", + "terms-of-use": "論壇使用條款 (留空即可禁用)", + "user-search": "用戶搜尋", + "user-search-results-per-page": "顯示的結果數量", + "default-user-settings": "預設使用者設定", + "show-email": "顯示郵箱", + "show-fullname": "顯示全名", + "restrict-chat": "只允許我追隨的使用者給我發送聊天訊息", + "outgoing-new-tab": "在新頁籤打開外部連結", + "topic-search": "啟用主題內搜尋", + "update-url-with-post-index": "Update url with post index while browsing topics", + "digest-freq": "訂閱摘要", + "digest-freq.off": "關閉", + "digest-freq.daily": "每日", + "digest-freq.weekly": "每週", + "digest-freq.biweekly": "Bi-Weekly", + "digest-freq.monthly": "每月", + "email-chat-notifs": "當我不在線並收到新的聊天訊息時,給我發送電郵通知", + "email-post-notif": "當我訂閱的主題有新回覆時,給我發送電郵通知", + "follow-created-topics": "關注您建立的主題", + "follow-replied-topics": "關注您回覆的主題", + "default-notification-settings": "預設通知設定", + "categoryWatchState": "預設版面關注狀態", + "categoryWatchState.watching": "已關注", + "categoryWatchState.notwatching": "未關注", + "categoryWatchState.ignoring": "已忽略" +} diff --git a/public/language/zh-TW/admin/settings/web-crawler.json b/public/language/zh-TW/admin/settings/web-crawler.json new file mode 100644 index 0000000000..04033ccc7c --- /dev/null +++ b/public/language/zh-TW/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "爬蟲抓取設定", + "robots-txt": "自訂 Robots.txt,留白以使用預設設定", + "sitemap-feed-settings": "網站地圖與訂閱設定", + "disable-rss-feeds": "停用 RSS 訂閱", + "disable-sitemap-xml": "停用 Sitemap.xml", + "sitemap-topics": "要在 Sitemap 中展現的主題數量", + "clear-sitemap-cache": "清除 Sitemap 快取", + "view-sitemap": "檢視 Sitemap" +} \ No newline at end of file diff --git a/public/language/zh-TW/category.json b/public/language/zh-TW/category.json new file mode 100644 index 0000000000..1d5770ad7f --- /dev/null +++ b/public/language/zh-TW/category.json @@ -0,0 +1,23 @@ +{ + "category": "版面", + "subcategories": "子版面", + "new_topic_button": "發表主題", + "guest-login-post": "登入以發表", + "no_topics": "此版面還沒有任何內容。
趕緊來貼文吧!", + "browsing": "正在瀏覽", + "no_replies": "尚無回覆", + "no_new_posts": "沒有新主題", + "watch": "關注", + "ignore": "忽略", + "watching": "已關注", + "not-watching": "未關注", + "ignoring": "已忽略", + "watching.description": "顯示未讀和最近的主題", + "not-watching.description": "不顯示未讀主題,顯示最近主題", + "ignoring.description": "不顯示未讀和最近的主題", + "watching.message": "您關注了此版面和全部子版面的動態。", + "notwatching.message": "您未關注了此版面和全部子版面的動態。", + "ignoring.message": "您忽略了此版面和全部子版面的動態。", + "watched-categories": "已關注的版面", + "x-more-categories": "還有 %1 個版面" +} \ No newline at end of file diff --git a/public/language/zh-TW/email.json b/public/language/zh-TW/email.json new file mode 100644 index 0000000000..66eb2196e4 --- /dev/null +++ b/public/language/zh-TW/email.json @@ -0,0 +1,58 @@ +{ + "test-email.subject": "測試郵件", + "password-reset-requested": "已申請密碼重設!", + "welcome-to": "歡迎來到 %1", + "invite": "來自%1的邀請", + "greeting_no_name": "您好", + "greeting_with_name": "%1,您好", + "email.verify-your-email.subject": "請驗證你的電子信箱", + "email.verify.text1": "您要求我們更改或者驗證您的電子信箱地址", + "email.verify.text2": "為了安全起見,我們只會在透過電子郵件確認過電子信箱所有權後才會更改登錄的信箱地址。 假如您沒有提出過這個要求, 您不用進行任何動作。", + "email.verify.text3": "一旦您確認此信箱地址,我們將使用此地址取代您目前的信箱地址(%1)。", + "welcome.text1": "感謝您註冊 %1 帳戶!", + "welcome.text2": "在您驗證您綁定的郵件地址之後,您的帳戶才能啟用。", + "welcome.text3": "管理員批准了您的註冊申請,現在您可以登入您的帳戶了。", + "welcome.cta": "點擊這裡確認您的電子郵件地址", + "invitation.text1": "%1 邀請您加入 %2", + "invitation.text2": "您的邀請將在 %1 天後過期。", + "invitation.cta": "點擊這裡新建帳戶", + "reset.text1": "很可能是您忘記了密碼,我們收到了重設您帳戶密碼的申請。 如果您沒有申請密碼重設,請忽略這封郵件。", + "reset.text2": "如需繼續重設密碼,請點擊下面的連結:", + "reset.cta": "點擊這裡重設您的密碼", + "reset.notify.subject": "更改密碼成功", + "reset.notify.text1": "您在 %1 上的密碼已經成功修改。", + "reset.notify.text2": "如果您沒有授權此操作,請立即聯繫管理員。", + "digest.latest_topics": "來自 %1 的最新主題", + "digest.top-topics": "來自 %1 的置頂主題", + "digest.popular-topics": "來自 %1 的熱門主題", + "digest.cta": "點擊這裡訪問 %1", + "digest.unsub.info": "根據您的訂閱設定,為您發送此摘要。", + "digest.day": "天", + "digest.week": "周", + "digest.month": "月", + "digest.subject": "%1 的摘要", + "digest.title.day": "您的每日摘要", + "digest.title.week": "您的每週摘要", + "digest.title.month": "您的每月摘要", + "notif.chat.subject": "收到來自 %1 的新聊天訊息", + "notif.chat.cta": "點擊這裡繼續聊天", + "notif.chat.unsub.info": "根據您的訂閱設定,為您發送此聊天提醒。", + "notif.post.unsub.info": "根據您的訂閱設定,為您發送此回覆提醒。", + "notif.post.unsub.one-click": "或者通過點擊來取消訂閱郵件", + "notif.cta": "點擊這裡前往論壇", + "notif.cta-new-reply": "查看貼文", + "notif.cta-new-chat": "查看聊天", + "notif.test.short": "測試通知", + "notif.test.long": "這是一個測試的通知郵件。", + "test.text1": "這是一封測試郵件,用來驗證 NodeBB 的郵件設定是否正確。", + "unsub.cta": "點擊這裡修改這些設定", + "unsubscribe": "退訂", + "unsub.success": "您將不再收到來自%1郵寄名單的郵件", + "unsub.failure.title": "Unable to unsubscribe", + "unsub.failure.message": "Unfortunately, we were not able to unsubscribe you from the mailing list, as there was an issue with the link. However, you can alter your email preferences by going to your user settings.

(error: %1)", + "banned.subject": "您在 %1 的帳戶已被停權", + "banned.text1": "您在 %2 的帳戶 %1 已被停權。", + "banned.text2": "本次停權將在 %1 結束。", + "banned.text3": "這是您被停權的原因:", + "closing": "謝謝!" +} \ No newline at end of file diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json new file mode 100644 index 0000000000..57bb0fa200 --- /dev/null +++ b/public/language/zh-TW/error.json @@ -0,0 +1,224 @@ +{ + "invalid-data": "無效資料", + "invalid-json": "無效 JSON", + "wrong-parameter-type": "A value of type %3 was expected for property `%1`, but %2 was received instead", + "required-parameters-missing": "Required parameters were missing from this API call: %1", + "not-logged-in": "您還沒有登入。", + "account-locked": "您的帳戶已被暫時鎖定", + "search-requires-login": "搜尋功能僅限成員使用 - 請先登入或者註冊。", + "goback": "按返回以退至前一頁", + "invalid-cid": "無效版面 ID", + "invalid-tid": "無效主題 ID", + "invalid-pid": "無效貼文 ID", + "invalid-uid": "無效使用者 ID", + "invalid-mid": "Invalid Chat Message ID", + "invalid-date": "A valid date must be provided", + "invalid-username": "無效使用者名", + "invalid-email": "無效的電子信箱", + "invalid-fullname": "無效全名", + "invalid-location": "無效位置", + "invalid-birthday": "無效生日", + "invalid-title": "無效的標題", + "invalid-user-data": "無效使用者資料", + "invalid-password": "無效密碼", + "invalid-login-credentials": "無效登入憑證", + "invalid-username-or-password": "請確認使用者名稱和密碼", + "invalid-search-term": "無效的搜尋關鍵字", + "invalid-url": "無效的 URL", + "invalid-event": "Invalid event: %1", + "local-login-disabled": "已停用非管理帳戶的本地登入。", + "csrf-invalid": "可能是由於會話過期,登入失敗。請重試。", + "invalid-path": "Invalid path", + "folder-exists": "Folder exists", + "invalid-pagination-value": "無效的分頁數,必須介於 %1 和 %2 之間", + "username-taken": "此使用者名已被使用", + "email-taken": "此電子信箱已被使用", + "email-nochange": "The email entered is the same as the email already on file.", + "email-invited": "Email was already invited", + "email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.", + "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 may not be able to post in some categories or chat until your email is confirmed.", + "no-email-to-confirm": "Your account does not have an email set. An email is necessary for account recovery, and may be necessary for chatting and posting in some categories. Please click here to enter an email.", + "user-doesnt-have-email": "User \"%1\" does not have an email set.", + "email-confirm-failed": "我們無法確認您的電子信箱,請重試", + "confirm-email-already-sent": "確認郵件已發出,如需重新發送請等待 %1 分鐘後再試。", + "sendmail-not-found": "無法找到 sendmail 可執行檔,請確保 sendmail 已經安裝並可被運行 NodeBB 的系統帳戶執行", + "digest-not-enabled": "此使用者未開啟摘要通知,或系統設定預設不發送摘要", + "username-too-short": "使用者名太短", + "username-too-long": "使用者名太長", + "password-too-long": "密碼太長", + "reset-rate-limited": "太多密碼重設請求(有頻率限制)", + "reset-same-password": "Please use a password that is different from your current one", + "user-banned": "使用者已停權", + "user-banned-reason": "抱歉,此帳戶已經被停權 (原因:%1)", + "user-banned-reason-until": "抱歉,此帳戶已被停權,直到%1(原因:%2)", + "user-too-new": "抱歉,您需要等待 %1 秒後,才可以發文!", + "blacklisted-ip": "對不起,您的 IP 地址已被社區封鎖。如果您認為這是一個錯誤,請與管理員聯繫。", + "ban-expiry-missing": "請提供此次停權結束日期", + "no-category": "版面不存在", + "no-topic": "主題不存在", + "no-post": "貼文不存在", + "no-group": "群組不存在", + "no-user": "使用者不存在", + "no-teaser": "主題預覽不存在", + "no-flag": "Flag does not exist", + "no-privileges": "您的權限不足以執行此操作。", + "category-disabled": "版面已停用", + "topic-locked": "主題已鎖定", + "post-edit-duration-expired": "您只能在發表後 %1 秒內修改內容", + "post-edit-duration-expired-minutes": "您只能在發表後 %1 分鐘內修改內容", + "post-edit-duration-expired-minutes-seconds": "您只能在發表後 %1 分 %2 秒內修改內容", + "post-edit-duration-expired-hours": "您只能在發表後 %1 小時後內修改內容", + "post-edit-duration-expired-hours-minutes": "您只能在發表後 %1 小時 %2 分鐘內修改內容", + "post-edit-duration-expired-days": "您只能在發表後 %1 天內修改內容", + "post-edit-duration-expired-days-hours": "您只能在發表後 %1 天 %2 小時內修改內容", + "post-delete-duration-expired": "您只能在發表後 %1 秒內刪除貼文", + "post-delete-duration-expired-minutes": "您只能在發表後 %1 分鐘內刪除貼文", + "post-delete-duration-expired-minutes-seconds": "您只能在發表發 %1 分 %2 秒內刪除貼文", + "post-delete-duration-expired-hours": "您只能在發表後 %1 小時內刪除貼文", + "post-delete-duration-expired-hours-minutes": "您只能在發表後 %1 小時 %2 分鐘內刪除貼文", + "post-delete-duration-expired-days": "您只能在發表後 %1 天內刪除貼文", + "post-delete-duration-expired-days-hours": "您只能在發表後 %1 天 %2 小時內刪除貼文", + "cant-delete-topic-has-reply": "您不能刪除您的主題,因為已有回覆。", + "cant-delete-topic-has-replies": "您不能刪除您的主題,因為已有 %1 條回覆。", + "content-too-short": "請增加貼文內容,不能少於 %1 個字符。", + "content-too-long": "請刪減貼文內容,不能超過 %1 個字符。", + "title-too-short": "請增加標題,不能少於 %1 個字符。", + "title-too-long": "請刪減標題,不超過 %1 個字符。", + "category-not-selected": "未選擇版面。", + "too-many-posts": "貼文需要間隔 %1 秒以上 - 請稍候再發文", + "too-many-posts-newbie": "因為您是新使用者,所以限制每隔 %1 秒才能發文一次,直到您有 %2 點聲望為止 —— 請稍候再發文", + "already-posting": "You are already posting", + "tag-too-short": "標籤太短,不能少於 %1 個字元", + "tag-too-long": "標籤太長,不能超過 %1 個字元", + "not-enough-tags": "沒有足夠的主題標籤。主題必須至少有 %1 個標籤", + "too-many-tags": "過多主題標籤。主題不能超過 %1 個標籤", + "cant-use-system-tag": "You can not use this system tag.", + "cant-remove-system-tag": "You can not remove this system tag.", + "still-uploading": "請等待上傳完成", + "file-too-big": "上傳檔案的大小限制為 %1 KB - 請縮減檔案大小", + "guest-upload-disabled": "訪客不允許上傳", + "cors-error": "由於CORS設定錯誤,無法上傳圖片。", + "upload-ratelimit-reached": "You have uploaded too many files at one time. Please try again later.", + "scheduling-to-past": "Please select a date in the future.", + "invalid-schedule-date": "Please enter a valid date and time.", + "cant-pin-scheduled": "Scheduled topics cannot be (un)pinned.", + "cant-merge-scheduled": "Scheduled topics cannot be merged.", + "cant-move-posts-to-scheduled": "Can't move posts to a scheduled topic.", + "cant-move-from-scheduled-to-existing": "Can't move posts from a scheduled topic to an existing topic.", + "already-bookmarked": "您已將此貼文存為了書籤", + "already-unbookmarked": "您已移除了此貼文的書籤", + "cant-ban-other-admins": "您不能封鎖其他管理員!", + "cant-mute-other-admins": "You can't mute other admins!", + "user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)", + "user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)", + "cant-make-banned-users-admin": "You can't make banned users admin.", + "cant-remove-last-admin": "您是唯一的管理員。在刪除您的管理員權限前,請增加另一個管理員。", + "account-deletion-disabled": "Account deletion is disabled", + "cant-delete-admin": "在刪除該帳戶之前,請先移除其管理權限。", + "already-deleting": "Already deleting", + "invalid-image": "無效的圖檔", + "invalid-image-type": "無效的圖檔類型。允許的類型有:%1", + "invalid-image-extension": "無效的圖檔副檔名", + "invalid-file-type": "無效檔案格式,允許的格式有:%1", + "invalid-image-dimensions": "圖片尺寸太大", + "group-name-too-short": "群組名太短", + "group-name-too-long": "群組名太長", + "group-already-exists": "群組已存在", + "group-name-change-not-allowed": "不允許更改群組名稱", + "group-already-member": "已經是此群組的成員", + "group-not-member": "不是此群組的成員", + "group-needs-owner": "群組需要指定至少一名群組所有者", + "group-already-invited": "您已邀請該使用者", + "group-already-requested": "已提交您的請求", + "group-join-disabled": "您目前無法加入此群組", + "group-leave-disabled": "您目前無法離開此群組", + "post-already-deleted": "此貼文已被刪除", + "post-already-restored": "此貼文已經恢復", + "topic-already-deleted": "此主題已被刪除", + "topic-already-restored": "此主題已恢復", + "cant-purge-main-post": "無法清除主貼文,請直接刪除主題", + "topic-thumbnails-are-disabled": "主題縮圖已停用", + "invalid-file": "無效檔案", + "uploads-are-disabled": "上傳已停用", + "signature-too-long": "抱歉,您的簽名不能超過 %1 個字元。", + "about-me-too-long": "抱歉,您的關於我不能超過 %1 個字元。", + "cant-chat-with-yourself": "您不能和自己聊天!", + "chat-restricted": "此使用者限制了他的聊天訊息。必須他先追隨您,您才能和他聊天。", + "chat-disabled": "聊天系統已關閉", + "too-many-messages": "您發送了太多訊息,請稍等片刻。", + "invalid-chat-message": "無效的聊天訊息", + "chat-message-too-long": "聊天訊息不能超過 %1  個字元。", + "cant-edit-chat-message": "您不能編輯這條訊息", + "cant-delete-chat-message": "您不允許刪除這條訊息", + "chat-edit-duration-expired": "您只能在發佈 %1 秒後修改聊天訊息", + "chat-delete-duration-expired": "您只能在發佈 %1 秒後刪除聊天訊息", + "chat-deleted-already": "聊天訊息已經被刪除", + "chat-restored-already": "此聊天訊息已經恢復。", + "chat-room-does-not-exist": "Chat room does not exist.", + "already-voting-for-this-post": "您已讚過此貼文回覆了。", + "reputation-system-disabled": "聲望系統已停用。", + "downvoting-disabled": "倒讚已被停用", + "not-enough-reputation-to-chat": "You need %1 reputation to chat", + "not-enough-reputation-to-upvote": "You need %1 reputation to upvote", + "not-enough-reputation-to-downvote": "You need %1 reputation to downvote", + "not-enough-reputation-to-flag": "You need %1 reputation to flag this post", + "not-enough-reputation-min-rep-website": "You need %1 reputation to add a website", + "not-enough-reputation-min-rep-aboutme": "You need %1 reputation to add an about me", + "not-enough-reputation-min-rep-signature": "You need %1 reputation to add a signature", + "not-enough-reputation-min-rep-profile-picture": "You need %1 reputation to add a profile picture", + "not-enough-reputation-min-rep-cover-picture": "You need %1 reputation to add a cover picture", + "post-already-flagged": "You have already flagged this post", + "user-already-flagged": "You have already flagged this user", + "post-flagged-too-many-times": "This post has been flagged by others already", + "user-flagged-too-many-times": "This user has been flagged by others already", + "cant-flag-privileged": "You are not allowed to flag the profiles or content of privileged users (moderators/global moderators/admins)", + "self-vote": "您不能讚您自己的貼文", + "too-many-upvotes-today": "You can only upvote %1 times a day", + "too-many-upvotes-today-user": "You can only upvote a user %1 times a day", + "too-many-downvotes-today": "You can only downvote %1 times a day", + "too-many-downvotes-today-user": "You can only downvote a user %1 times a day", + "reload-failed": "重載 NodeBB 時遇到問題: \"%1\"。NodeBB 維持給已連線的客戶端服務,您應該取消重載前做的動作。", + "registration-error": "註冊錯誤", + "parse-error": "伺服器回應解析出錯", + "wrong-login-type-email": "請輸入您的電子信箱登入", + "wrong-login-type-username": "請輸入您的使用者名登入", + "sso-registration-disabled": "已停用通過 %1 帳戶的註冊, 請使用電子信箱地址註冊", + "sso-multiple-association": "您無法將此服務中的多個帳戶關聯到您的NodeBB賬號。請您移除連結現有帳戶並重試。", + "invite-maximum-met": "您的邀請人數超出了上限 (%1 超過了 %2)。", + "no-session-found": "未登入!", + "not-in-room": "使用者已不在聊天室中", + "cant-kick-self": "您不能把自己踢出群組", + "no-users-selected": "尚未選擇使用者", + "invalid-home-page-route": "無效的首頁路徑", + "invalid-session": "Invalid Session", + "invalid-session-text": "It looks like your login session is no longer active. Please refresh this page.", + "session-mismatch": "Session Mismatch", + "session-mismatch-text": "It looks like your login session no longer matches with the server. Please refresh this page.", + "no-topics-selected": "沒有主題被選中!", + "cant-move-to-same-topic": "無法將貼文移動到相同的主題中!", + "cant-move-topic-to-same-category": "Can't move topic to the same category!", + "cannot-block-self": "您不能把自己封鎖!", + "cannot-block-privileged": "您不能封鎖管理員或者超級版主", + "cannot-block-guest": "訪客無法封鎖其他使用者", + "already-blocked": "此使用者已被封鎖", + "already-unblocked": "此使用者已被取消封鎖", + "no-connection": "您的網路連線似乎有問題", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", + "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", + "topic-event-unrecognized": "Topic event '%1' unrecognized", + "cant-set-child-as-parent": "Can't set child as parent category", + "cant-set-self-as-parent": "Can't set self as parent category", + "api.master-token-no-uid": "A master token was received without a corresponding `_uid` in the request body", + "api.400": "Something was wrong with the request payload you passed in.", + "api.401": "A valid login session was not found. Please log in and try again.", + "api.403": "You are not authorised to make this call", + "api.404": "Invalid API call", + "api.426": "HTTPS is required for requests to the write api, please re-send your request via HTTPS", + "api.429": "You have made too many requests, please try again later", + "api.500": "An unexpected error was encountered while attempting to service your request.", + "api.501": "The route you are trying to call is not implemented yet, please try again tomorrow", + "api.503": "The route you are trying to call is not currently available due to a server configuration" +} \ No newline at end of file diff --git a/public/language/zh-TW/flags.json b/public/language/zh-TW/flags.json new file mode 100644 index 0000000000..bfb22a77a3 --- /dev/null +++ b/public/language/zh-TW/flags.json @@ -0,0 +1,89 @@ +{ + "state": "狀態", + "reports": "Reports", + "first-reported": "First Reported", + "no-flags": "帥!沒發現任何的舉報。", + "assignee": "受指派人", + "update": "更新", + "updated": "已更新", + "resolved": "Resolved", + "target-purged": "被舉報的內容已經被清除,不再可用。", + + "graph-label": "日舉報", + "quick-filters": "快速過濾器", + "filter-active": "該列中有一個或更多啟用的過濾器", + "filter-reset": "刪除過濾器", + "filters": "過濾器選項", + "filter-reporterId": "舉報者UID", + "filter-targetUid": "被舉報者 UID", + "filter-type": "舉報類型", + "filter-type-all": "所有內容", + "filter-type-post": "貼文", + "filter-type-user": "使用者", + "filter-state": "狀態", + "filter-assignee": "受指派人 UID", + "filter-cid": "版面", + "filter-quick-mine": "指派給我", + "filter-cid-all": "全部版面", + "apply-filters": "應用過濾器", + "more-filters": "More Filters", + "fewer-filters": "Fewer Filters", + + "quick-actions": "快速操作", + "flagged-user": "被舉報的使用者", + "view-profile": "查看個人資料", + "start-new-chat": "開始新聊天對話", + "go-to-target": "查看舉報目標", + "assign-to-me": "Assign To Me", + "delete-post": "刪除貼文", + "purge-post": "清除貼文", + "restore-post": "恢復貼文", + "delete": "Delete Flag", + + "user-view": "查看資料", + "user-edit": "編輯資料", + + "notes": "舉報備註", + "add-note": "新增備註", + "no-notes": "沒有共享的備註內容。", + "delete-note-confirm": "Are you sure you want to delete this flag note?", + "delete-flag-confirm": "Are you sure you want to delete this flag?", + "note-added": "備註已添加", + "note-deleted": "Note Deleted", + "flag-deleted": "Flag Deleted", + + "history": "帳戶 & 舉報紀錄", + "no-history": "沒有舉報歷史。", + + "state-all": "所有狀態", + "state-open": "新增/打開", + "state-wip": "正在處理", + "state-resolved": "已解決", + "state-rejected": "已拒絕", + "no-assignee": "未指派", + + "sort": "Sort by", + "sort-newest": "Newest first", + "sort-oldest": "Oldest first", + "sort-reports": "Most reports", + "sort-all": "All flag types...", + "sort-posts-only": "Posts only...", + "sort-downvotes": "Most downvotes", + "sort-upvotes": "Most upvotes", + "sort-replies": "Most replies", + + "modal-title": "Report Content", + "modal-body": "請選擇或者輸入您舉報 %1%2 的原因以便版主進行審核。", + "modal-reason-spam": "垃圾訊息", + "modal-reason-offensive": "人身攻擊", + "modal-reason-other": "其它(請在下方指定)", + "modal-reason-custom": "舉報此內容的理由……", + "modal-submit": "提交舉報", + "modal-submit-success": "已舉報此內容。", + + "bulk-actions": "Bulk Actions", + "bulk-resolve": "Resolve Flag(s)", + "bulk-success": "%1 flags updated", + "flagged-timeago-readable": "Flagged (%2)", + "auto-flagged": "[Auto Flagged] Received %1 downvotes." +} \ No newline at end of file diff --git a/public/language/zh-TW/global.json b/public/language/zh-TW/global.json new file mode 100644 index 0000000000..bc492eda30 --- /dev/null +++ b/public/language/zh-TW/global.json @@ -0,0 +1,126 @@ +{ + "home": "首頁", + "search": "搜尋", + "buttons.close": "關閉", + "403.title": "禁止訪問", + "403.message": "您似乎沒有訪問此頁面的權限。", + "403.login": "或許您應該先 登入試試?", + "404.title": "未找到", + "404.message": "您訪問的頁面不存在。返回首頁。", + "500.title": "內部錯誤", + "500.message": "哎呀!看來是哪裡出錯了!", + "400.title": "錯誤的請求", + "400.message": "看起來這個連結的格式不正確,請再次檢查並重試。或者返回首頁。", + "register": "註冊", + "login": "登入", + "please_log_in": "請登入", + "logout": "登出", + "posting_restriction_info": "僅限已註冊成員發文,點這裡登入。", + "welcome_back": "歡迎回來", + "you_have_successfully_logged_in": "您已成功登入", + "save_changes": "儲存更改", + "save": "儲存", + "close": "關閉", + "pagination": "分頁", + "pagination.out_of": "%1 / %2", + "pagination.enter_index": "Go to post index", + "header.admin": "管理", + "header.categories": "版面", + "header.recent": "最新", + "header.unread": "未讀", + "header.tags": "標籤", + "header.popular": "熱門", + "header.top": "Top", + "header.users": "使用者", + "header.groups": "群組", + "header.chats": "聊天", + "header.notifications": "通知", + "header.search": "搜尋", + "header.profile": "個人檔案", + "header.navigation": "導航", + "notifications.loading": "正在載入通知", + "chats.loading": "正在載入聊天", + "motd.welcome": "歡迎來到 NodeBB,未來的社區論壇平臺。", + "previouspage": "上一頁", + "nextpage": "下一頁", + "alert.success": "成功", + "alert.error": "錯誤", + "alert.banned": "已停權", + "alert.banned.message": "You have just been banned, your access is now restricted.", + "alert.unbanned": "Unbanned", + "alert.unbanned.message": "Your ban has been lifted.", + "alert.unfollow": "您已取消追隨 %1!", + "alert.follow": "您已追隨 %1!", + "users": "使用者", + "topics": "主題", + "posts": "貼文", + "x-posts": "%1 posts", + "best": "最佳", + "controversial": "Controversial", + "votes": "評價", + "x-votes": "%1 votes", + "voters": "評價人數", + "upvoters": "點贊人數", + "upvoted": "已點讚", + "downvoters": "倒讚人數", + "downvoted": "倒讚", + "views": "瀏覽", + "posters": "Posters", + "reputation": "聲望", + "lastpost": "上一個貼文", + "firstpost": "第一個貼文", + "read_more": "閱讀更多", + "more": "更多", + "none": "None", + "posted_ago_by_guest": "訪客發佈於 %1", + "posted_ago_by": "%2 發佈於 %1", + "posted_ago": "發佈於 %1", + "posted_in": "發佈在 %1", + "posted_in_by": "%2 發佈於 %1", + "posted_in_ago": "於 %2 發佈到 %1 版", + "posted_in_ago_by": "%3 於 %1 發佈到 %2", + "user_posted_ago": "%1 發佈於 %2", + "guest_posted_ago": "訪客發佈於 %1", + "last_edited_by": "最後由 %1 編輯", + "norecentposts": "暫無新貼文", + "norecenttopics": "暫無新主題", + "recentposts": "新貼文", + "recentips": "最近登入的 IP", + "moderator_tools": "版主工具", + "online": "線上", + "away": "離開", + "dnd": "請勿打擾", + "invisible": "隱身", + "offline": "離線", + "email": "電子信箱", + "language": "語言", + "guest": "訪客", + "guests": "訪客", + "former_user": "舊使用者", + "system-user": "System", + "unknown-user": "Unknown user", + "updated.title": "論壇已更新", + "updated.message": "論壇已更新。請點這裡重載頁面。", + "privacy": "隱私", + "follow": "追隨", + "unfollow": "取消追隨", + "delete_all": "全部刪除", + "map": "地圖", + "sessions": "已登入的會話", + "ip_address": "IP 地址", + "enter_page_number": "輸入頁號", + "upload_file": "上傳檔案", + "upload": "上傳", + "uploads": "上傳", + "allowed-file-types": "允許的檔案類型有 %1", + "unsaved-changes": "您有未儲存的更改,您確定您要離開嗎?", + "reconnecting-message": "與 %1 的連線中斷,我們正在嘗試重連,請耐心等待", + "play": "播放", + "cookies.message": "此網站使用 Cookies 以確保您在我們網站的最佳體驗。", + "cookies.accept": "知道了!", + "cookies.learn_more": "瞭解更多", + "edited": "已編輯", + "disabled": "停用", + "select": "選擇", + "user-search-prompt": "輸入以搜尋使用者" +} \ No newline at end of file diff --git a/public/language/zh-TW/groups.json b/public/language/zh-TW/groups.json new file mode 100644 index 0000000000..26bfb74072 --- /dev/null +++ b/public/language/zh-TW/groups.json @@ -0,0 +1,64 @@ +{ + "groups": "群組", + "view_group": "檢視群組", + "owner": "群組所有者", + "new_group": "新增群組", + "no_groups_found": "尚無群組訊息", + "pending.accept": "同意", + "pending.reject": "拒絕", + "pending.accept_all": "全部同意", + "pending.reject_all": "全部拒絕", + "pending.none": "暫時沒有待加入的成員", + "invited.none": "暫時沒有接受邀請的成員", + "invited.uninvite": "取消邀請", + "invited.search": "選擇使用者加入群組", + "invited.notification_title": "您已被邀請加入 %1", + "request.notification_title": "來自 %1 的群組成員請求", + "request.notification_text": "%1 已被邀請加入 %2", + "cover-save": "儲存", + "cover-saving": "正在儲存", + "details.title": "群組訊息", + "details.members": "成員列表", + "details.pending": "待加入成員", + "details.invited": "已邀請成員", + "details.has_no_posts": "此群組的成員尚未發表任何貼文。", + "details.latest_posts": "最新貼文", + "details.private": "私有", + "details.disableJoinRequests": "禁用申請加入群組", + "details.disableLeave": "禁用使用者離開群組", + "details.grant": "准許/撤銷管理權", + "details.kick": "踢出群組", + "details.kick_confirm": "您確定要將此成員從群組中移除嗎?", + "details.add-member": "新增成員", + "details.owner_options": "群組管理", + "details.group_name": "群組名", + "details.member_count": "群組成員數", + "details.creation_date": "建立時間", + "details.description": "描述", + "details.member-post-cids": "Category IDs to display posts from", + "details.badge_preview": "徽章預覽", + "details.change_icon": "更改圖示", + "details.change_label_colour": "更改標籤顏色", + "details.change_text_colour": "更改文字顏色", + "details.badge_text": "徽章文字", + "details.userTitleEnabled": "顯示組內稱號", + "details.private_help": "啟用此選項後,加入群組需要組長審核。", + "details.hidden": "隱藏", + "details.hidden_help": "啟用此選項後,群組將不在群組列表中展現,成員只能通過邀請加入。", + "details.delete_group": "刪除群組", + "details.private_system_help": "系統禁用了私有群組,這個選項不起任何作用", + "event.updated": "群組訊息已更新", + "event.deleted": "群組 \"%1\" 已被刪除", + "membership.accept-invitation": "接受邀請", + "membership.accept.notification_title": "你現在是 %1的成員了", + "membership.invitation-pending": "邀請中", + "membership.join-group": "加入群組", + "membership.leave-group": "退出群組", + "membership.leave.notification_title": "%1 退出了群組:%2", + "membership.reject": "拒絕", + "new-group.group_name": "群組名:", + "upload-group-cover": "上傳群組封面", + "bulk-invite-instructions": "輸入您要邀請加入此群組的使用者名,多個使用者以逗號分隔", + "bulk-invite": "批次邀請", + "remove_group_cover_confirm": "確定要移除封面圖片嗎?" +} \ No newline at end of file diff --git a/public/language/zh-TW/ip-blacklist.json b/public/language/zh-TW/ip-blacklist.json new file mode 100644 index 0000000000..b98811a04c --- /dev/null +++ b/public/language/zh-TW/ip-blacklist.json @@ -0,0 +1,19 @@ +{ + "lead": "在此設定 IP 黑名單", + "description": "有時,一份帳戶封鎖並不足以作為威懾。更多的時候,限制有權瀏覽論壇的具體 IP 或者一個 IP 範圍這一行為可以更有效地保護論壇。在以上情況下,您可以新增一些令人厭惡者的 IP 地址或者 CIDR 地址塊到此黑名單,此後他們(被加入黑名單者)將被阻止進行登入或者註冊新帳戶的行為。", + "active-rules": "生效規則", + "validate": "驗證黑名單", + "apply": "應用黑名單", + "hints": "格式建議", + "hint-1": "每行定義一個獨立 IP 地址。您可以添加 IP 範圍,只要它們滿足 CIDR 格式(e.g. 192.168.100.0/22)。", + "hint-2": "您可以通過以#標誌開頭的行來添加註解。", + + "validate.x-valid": "%1 / %2的規則有效。", + "validate.x-invalid": "下列 %0 個規則無效:", + + "alerts.applied-success": "黑名單生效", + + "analytics.blacklist-hourly": "圖 1 – 每小時觸發黑名單數", + "analytics.blacklist-daily": "圖 2– 每日觸發黑名單數", + "ip-banned": "已封鎖IP" +} \ No newline at end of file diff --git a/public/language/zh-TW/language.json b/public/language/zh-TW/language.json new file mode 100644 index 0000000000..de9713d95a --- /dev/null +++ b/public/language/zh-TW/language.json @@ -0,0 +1,5 @@ +{ + "name": "繁體中文", + "code": "zh-TW", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/zh-TW/login.json b/public/language/zh-TW/login.json new file mode 100644 index 0000000000..9763c5e1b0 --- /dev/null +++ b/public/language/zh-TW/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "使用者名 / 電子信箱", + "username": "使用者名", + "remember_me": "保持登入?", + "forgot_password": "忘記密碼?", + "alternative_logins": "使用合作網站帳戶登錄", + "failed_login_attempt": "登入失敗", + "login_successful": "您已成功登入!", + "dont_have_account": "沒有帳戶?", + "logged-out-due-to-inactivity": "由於長時間沒有活動,您的帳戶已被管理員從後台登出", + "caps-lock-enabled": "Caps Lock is enabled" +} \ No newline at end of file diff --git a/public/language/zh-TW/modules.json b/public/language/zh-TW/modules.json new file mode 100644 index 0000000000..4f0edc5ac1 --- /dev/null +++ b/public/language/zh-TW/modules.json @@ -0,0 +1,82 @@ +{ + "chat.chatting_with": "與聊天", + "chat.placeholder": "Type chat message here, drag & drop images, press enter to send", + "chat.scroll-up-alert": "You are looking at older messages, click here to go to most recent message.", + "chat.send": "發送", + "chat.no_active": "暫無聊天", + "chat.user_typing": "%1 正在輸入……", + "chat.user_has_messaged_you": "%1 向您發送了訊息。", + "chat.see_all": "All chats", + "chat.mark_all_read": "Mark all read", + "chat.no-messages": "請選擇接收人,以查看聊天訊息紀錄", + "chat.no-users-in-room": "此聊天室中沒有使用者", + "chat.recent-chats": "最近聊天", + "chat.contacts": "聯絡人", + "chat.message-history": "訊息紀錄", + "chat.message-deleted": "訊息已刪除", + "chat.options": "聊天設定", + "chat.pop-out": "彈出聊天視窗", + "chat.minimize": "最小化", + "chat.maximize": "最大化", + "chat.seven_days": "7天", + "chat.thirty_days": "30天", + "chat.three_months": "3個月", + "chat.delete_message_confirm": "您確定刪除此訊息嗎?", + "chat.retrieving-users": "搜尋使用者", + "chat.manage-room": "管理聊天室", + "chat.add-user-help": "在這裡搜尋更多使用者。選中之後加入到聊天中,新使用者在加入聊天之前看不到聊天訊息。只有聊天室所有者()可以從聊天室中移除使用者。", + "chat.confirm-chat-with-dnd-user": "該使用者已將其狀態設置為 DnD(請勿打擾)。 您仍希望與其聊天嗎?", + "chat.rename-room": "重新命名房間", + "chat.rename-placeholder": "在這裡輸入房間名字", + "chat.rename-help": "這裡設定的房間名字能夠被房間內所有人都看到。", + "chat.leave": "離開聊天室", + "chat.leave-prompt": "您確定要離開聊天室?", + "chat.leave-help": "離開此聊天會將您在聊天中的未接收的訊息移除。您在重新加入之後不會看到任何聊天記錄", + "chat.in-room": "在此房間", + "chat.kick": "踢出", + "chat.show-ip": "顯示 IP", + "chat.owner": "房間所有者", + "chat.system.user-join": "%1 加入了房間", + "chat.system.user-leave": "%1 離開了房間", + "chat.system.room-rename": "%2 更改房間名為:%1", + "composer.compose": "撰寫", + "composer.show_preview": "顯示預覽", + "composer.hide_preview": "隱藏預覽", + "composer.user_said_in": "%1 在 %2 中說:", + "composer.user_said": "%1 說:", + "composer.discard": "確定想要取消此貼文?", + "composer.submit_and_lock": "提交並鎖定", + "composer.toggle_dropdown": "標為 Dropdown", + "composer.uploading": "正在上傳 %1", + "composer.formatting.bold": "粗體", + "composer.formatting.italic": "斜體", + "composer.formatting.list": "清單", + "composer.formatting.strikethrough": "刪除線", + "composer.formatting.code": "程式碼", + "composer.formatting.link": "連結", + "composer.formatting.picture": "Image Link", + "composer.upload-picture": "上傳圖片", + "composer.upload-file": "上傳檔案", + "composer.zen_mode": "無干擾模式", + "composer.select_category": "選擇一個版面", + "composer.textarea.placeholder": "Enter your post content here, drag and drop images", + "composer.schedule-for": "Schedule topic for", + "composer.schedule-date": "Date", + "composer.schedule-time": "Time", + "composer.cancel-scheduling": "Cancel Scheduling", + "composer.set-schedule-date": "Set Date", + "bootbox.ok": "確認", + "bootbox.cancel": "取消", + "bootbox.confirm": "確認", + "bootbox.submit": "Submit", + "bootbox.send": "Send", + "cover.dragging_title": "設定封面照片位置", + "cover.dragging_message": "拖拽封面照片到期望的位置,然後點擊“儲存”", + "cover.saved": "封面照片和位置已儲存", + "thumbs.modal.title": "Manage topic thumbnails", + "thumbs.modal.no-thumbs": "No thumbnails found.", + "thumbs.modal.resize-note": "Note: This forum is configured to resize topic thumbnails down to a maximum width of %1px", + "thumbs.modal.add": "Add thumbnail", + "thumbs.modal.remove": "Remove thumbnail", + "thumbs.modal.confirm-remove": "Are you sure you want to remove this thumbnail?" +} \ No newline at end of file diff --git a/public/language/zh-TW/notifications.json b/public/language/zh-TW/notifications.json new file mode 100644 index 0000000000..e4274312ae --- /dev/null +++ b/public/language/zh-TW/notifications.json @@ -0,0 +1,76 @@ +{ + "title": "通知", + "no_notifs": "您沒有新的通知", + "see_all": "All notifications", + "mark_all_read": "Mark all read", + "back_to_home": "返回 %1", + "outgoing_link": "站外連結", + "outgoing_link_message": "您正在離開 %1", + "continue_to": "繼續前往 %1", + "return_to": "返回 %1", + "new_notification": "您有一個新的通知", + "you_have_unread_notifications": "您有未讀的通知。", + "all": "所有", + "topics": "主題", + "replies": "回覆", + "chat": "聊天", + "group-chat": "Group Chats", + "follows": "關注", + "upvote": "點讚", + "new-flags": "新舉報", + "my-flags": "指派舉報給我", + "bans": "停權", + "new_message_from": "來自 %1 的新訊息", + "upvoted_your_post_in": "%1%2 點讚了您的貼文。", + "upvoted_your_post_in_dual": "%1%2%3 點讚了您的貼文。", + "upvoted_your_post_in_multiple": "%1 和 %2 個其他人在 %3 點讚了您的貼文。", + "moved_your_post": "您的貼文已被 %1 移動到了 %2", + "moved_your_topic": "%1 移動了 %2", + "user_flagged_post_in": "%1%2 舉報了一個貼文", + "user_flagged_post_in_dual": "%1%2%3 舉報了一個貼文", + "user_flagged_post_in_multiple": "%1 和 %2 個其他人在 %3 舉報了一個貼文", + "user_flagged_user": "%1 舉報了 (%2) 的使用者資料", + "user_flagged_user_dual": "%1%2 舉報了 (%3) 的使用者資料", + "user_flagged_user_multiple": "%1 和其他 %2 人舉報了 (%3) 的使用者資料", + "user_posted_to": "%1 回覆了:%2", + "user_posted_to_dual": "%1%2 回覆了: %3", + "user_posted_to_multiple": "%1 和 %2 個其他人回覆了: %3", + "user_posted_topic": "%1 發表了新主題:%2", + "user_edited_post": "%1%2編輯了一則貼文", + "user_started_following_you": "%1追隨了您。", + "user_started_following_you_dual": "%1%2 追隨了您。", + "user_started_following_you_multiple": "%1 和 %2 個其他人追隨了您。", + "new_register": "%1 發出了註冊請求", + "new_register_multiple": "有 %1 個註冊申請等待批准。", + "flag_assigned_to_you": "舉報 %1 已經被指派給您", + "post_awaiting_review": "請求查驗貼文", + "profile-exported": "%1 profile exported, click to download", + "posts-exported": "%1 posts exported, click to download", + "uploads-exported": "%1 uploads exported, click to download", + "users-csv-exported": "Users csv exported, click to download", + "post-queue-accepted": "Your queued post has been accepted. Click here to see your post.", + "post-queue-rejected": "Your queued post has been rejected.", + "post-queue-notify": "Queued post received a notification:
\"%1\"", + "email-confirmed": "電子信箱已確認", + "email-confirmed-message": "感謝您驗證您的電子信箱。您的帳戶現已完全啟用。", + "email-confirm-error-message": "驗證的您電子信箱地址時出現了問題。可能是因為驗證碼無效或已過期。", + "email-confirm-sent": "確認郵件已發送。", + "none": "不通知", + "notification_only": "頁面提醒", + "email_only": "電子郵件", + "notification_and_email": "頁面以及電郵", + "notificationType_upvote": "當有人點贊了我的貼文時", + "notificationType_new-topic": "當有人回覆我的貼文時", + "notificationType_new-reply": "當您正在查看的主題中有新回覆時", + "notificationType_post-edit": "當您關注中的主題有貼文被編輯時", + "notificationType_follow": "當有人追隨您時", + "notificationType_new-chat": "當您收到聊天訊息時", + "notificationType_new-group-chat": "When you receive a group chat message", + "notificationType_group-invite": "當您收到群組邀請時", + "notificationType_group-leave": "When a user leaves your group", + "notificationType_group-request-membership": "當有人請求加入您擁有的群組時", + "notificationType_new-register": "當有註冊申請待審核時", + "notificationType_post-queue": "當有新貼文等待審核時", + "notificationType_new-post-flag": "當有新的貼文舉報時", + "notificationType_new-user-flag": "當有新的使用者資料舉報時" +} \ No newline at end of file diff --git a/public/language/zh-TW/pages.json b/public/language/zh-TW/pages.json new file mode 100644 index 0000000000..9516c568cb --- /dev/null +++ b/public/language/zh-TW/pages.json @@ -0,0 +1,65 @@ +{ + "home": "首頁", + "unread": "未讀", + "popular-day": "今日熱門主題", + "popular-week": "本週熱門主題", + "popular-month": "當月熱門主題", + "popular-alltime": "熱門主題", + "recent": "最新主題", + "top-day": "今日點贊最高的主題", + "top-week": "本週點贊最高的主題", + "top-month": "本月點贊最高的主題", + "top-alltime": "點贊最高的主題", + "moderator-tools": "版主工具", + "flagged-content": "舉報管理", + "ip-blacklist": "IP 黑名單", + "post-queue": "貼文隊列", + "users/online": "線上使用者", + "users/latest": "最新使用者", + "users/sort-posts": "發文最多的使用者", + "users/sort-reputation": "聲望最高的使用者", + "users/banned": "被停權的使用者", + "users/most-flags": "被舉報次數最多的使用者", + "users/search": "使用者搜尋", + "notifications": "通知", + "tags": "標籤", + "tag": "標籤為\"%1\"的主題", + "register": "註冊帳戶", + "registration-complete": "註冊完成", + "login": "登入帳戶", + "reset": "重設帳戶密碼", + "categories": "版面", + "groups": "群組", + "group": "%1 的群組", + "chats": "聊天", + "chat": "與 %1 聊天", + "flags": "舉報", + "flag-details": "舉報 %1 詳情", + "account/edit": "正在編輯 \"%1\"", + "account/edit/password": "正在編輯 \"%1\" 的密碼", + "account/edit/username": "正在編輯 \"%1\" 的使用者名", + "account/edit/email": "正在編輯 \"%1\" 的電子信箱", + "account/info": "帳戶資訊", + "account/following": "%1 關注", + "account/followers": "關注 %1 的人", + "account/posts": "%1 發佈的貼文", + "account/latest-posts": "%1 發佈的最新貼文", + "account/topics": "%1 建立的主題", + "account/groups": "%1 的群組", + "account/watched_categories": "%1 關注的版面", + "account/bookmarks": "%1 收藏的貼文", + "account/settings": "使用者設定", + "account/watched": "主題已被 %1 關注", + "account/ignored": "主題已被 %1 忽略", + "account/upvoted": "貼文被 %1 點贊", + "account/downvoted": "貼文被 %1 倒讚", + "account/best": "%1 發佈的最佳貼文", + "account/controversial": "Controversial posts made by %1", + "account/blocks": "%1 封鎖的使用者", + "account/uploads": "%1 上傳的檔案", + "account/sessions": "已登入的會話", + "confirm": "電子信箱已確認", + "maintenance.text": "%1 正在進行維護。請稍後再來。", + "maintenance.messageIntro": "此外,管理員留下的訊息:", + "throttled.text": "%1 因負荷超載暫不可用。請稍後再來。" +} \ No newline at end of file diff --git a/public/language/zh-TW/post-queue.json b/public/language/zh-TW/post-queue.json new file mode 100644 index 0000000000..a2723b14db --- /dev/null +++ b/public/language/zh-TW/post-queue.json @@ -0,0 +1,31 @@ + +{ + "post-queue": "貼文隊列", + "description": "貼文隊列中暫無新貼文。啟用此功能,請前往設定→貼文→貼文隊列,然後啟用貼文隊列。", + "user": "使用者", + "category": "版面", + "title": "標題", + "content": "內容", + "posted": "發佈", + "reply-to": "回覆\"%1\"", + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject", + "remove": "Remove", + "notify": "Notify", + "notify-user": "Notify User", + "confirm-reject": "Do you want to reject this post?", + "bulk-actions": "Bulk Actions", + "accept-all": "Accept All", + "accept-selected": "Accept Selected", + "reject-all": "Reject All", + "reject-all-confirm": "Do you want to reject all posts?", + "reject-selected": "Reject Selected", + "reject-selected-confirm": "Do you want to reject %1 selected posts?", + "bulk-accept-success": "%1 posts accepted", + "bulk-reject-success": "%1 posts rejected" +} \ No newline at end of file diff --git a/public/language/zh-TW/recent.json b/public/language/zh-TW/recent.json new file mode 100644 index 0000000000..7045c6752d --- /dev/null +++ b/public/language/zh-TW/recent.json @@ -0,0 +1,19 @@ +{ + "title": "最新", + "day": "日", + "week": "周", + "month": "月", + "year": "年", + "alltime": "總計", + "no_recent_topics": "暫無主題。", + "no_popular_topics": "暫無熱門主題。", + "there-is-a-new-topic": "共計 1 個新主題。", + "there-is-a-new-topic-and-a-new-post": "共計 1 個新主題和 1 個新回覆。", + "there-is-a-new-topic-and-new-posts": "共計 1 個新主題和 %1 個新回覆。", + "there-are-new-topics": "共計 %1 個新主題。", + "there-are-new-topics-and-a-new-post": "共計 %1 個新主題和 1 個新回覆。", + "there-are-new-topics-and-new-posts": "共計 %1 個新主題和 %2 個新回覆。", + "there-is-a-new-post": "共計 1 個新回覆。", + "there-are-new-posts": "共計 %1 個新回覆。", + "click-here-to-reload": "點擊這裡重新載入。" +} \ No newline at end of file diff --git a/public/language/zh-TW/register.json b/public/language/zh-TW/register.json new file mode 100644 index 0000000000..9258b76735 --- /dev/null +++ b/public/language/zh-TW/register.json @@ -0,0 +1,32 @@ +{ + "register": "註冊", + "cancel_registration": "取消註冊", + "help.email": "預設情況下,您的電子信箱不會公開。", + "help.username_restrictions": "全站唯一的登入名稱,長度 %1 到 %2 個字元。其他人可以使用 @使用者名 提及您。", + "help.minimum_password_length": "您的密碼長度必須不少於 %1 個字元。", + "email_address": "電子信箱地址", + "email_address_placeholder": "輸入電子信箱地址", + "username": "使用者名", + "username_placeholder": "輸入使用者名", + "password": "密碼", + "password_placeholder": "輸入密碼", + "confirm_password": "確認密碼", + "confirm_password_placeholder": "再次輸入密碼", + "register_now_button": "立即註冊", + "alternative_registration": "其它方式註冊", + "terms_of_use": "使用條款", + "agree_to_terms_of_use": "我同意使用條款", + "terms_of_use_error": "您必須同意使用條款", + "registration-added-to-queue": "您的註冊正在等待批准。一旦通過,管理員會發送郵件通知您。", + "registration-queue-average-time": "Our average time for approving memberships is %1 hours %2 minutes.", + "registration-queue-auto-approve-time": "Your membership to this forum will be fully activated in up to %1 hours.", + "interstitial.intro": "We'd like some additional information in order to update your account…", + "interstitial.intro-new": "We'd like some additional information before we can create your account…", + "interstitial.errors-found": "Please review the entered information:", + "gdpr_agree_data": "我同意此網站對我個人資料的收集與處理。", + "gdpr_agree_email": "我同意此網站向我發送摘要和通知郵件。", + "gdpr_consent_denied": "您需要同意此網站收集與處理您的個人資料,以及向您發送電子郵件。", + "invite.error-admin-only": "Direct user registration has been disabled. Please contact an administrator for more details.", + "invite.error-invite-only": "Direct user registration has been disabled. You must be invited by an existing user in order to access this forum.", + "invite.error-invalid-data": "The registration data received does not correspond to our records. Please contact an administrator for more details" +} \ No newline at end of file diff --git a/public/language/zh-TW/reset_password.json b/public/language/zh-TW/reset_password.json new file mode 100644 index 0000000000..74d95580c4 --- /dev/null +++ b/public/language/zh-TW/reset_password.json @@ -0,0 +1,18 @@ +{ + "reset_password": "重設密碼", + "update_password": "更改密碼", + "password_changed.title": "密碼已更改", + "password_changed.message": "

密碼重設成功,請重新登入。", + "wrong_reset_code.title": "重設驗證碼不正確", + "wrong_reset_code.message": "您輸入的重設驗證碼有誤,請重新輸入,或者申請新的重設驗證碼。", + "new_password": "新密碼", + "repeat_password": "驗證密碼", + "changing_password": "Changing Password", + "enter_email": "請輸入您的電子信箱地址,我們將會發送一份郵件協助您重設帳戶密碼。", + "enter_email_address": "輸入電子信箱地址", + "password_reset_sent": "如果指定的信箱地址關聯到已存在的帳戶,該帳戶將收到一條密碼重置郵件,請注意該郵件一分鐘內只發送一次", + "invalid_email": "無效的電子信箱/電子信箱不存在!", + "password_too_short": "密碼太短,請選擇其他密碼。", + "passwords_do_not_match": "您輸入兩個密碼不一致。", + "password_expired": "您的密碼已過期,請選擇新密碼" +} \ No newline at end of file diff --git a/public/language/zh-TW/search.json b/public/language/zh-TW/search.json new file mode 100644 index 0000000000..c42a6f599e --- /dev/null +++ b/public/language/zh-TW/search.json @@ -0,0 +1,49 @@ +{ + "results_matching": "共 %1 條結果符合 \"%2\",(耗時 %3 秒)", + "no-matches": "無符合的結果", + "advanced-search": "進階搜尋", + "in": "在", + "titles": "標題", + "titles-posts": "標題和貼文", + "match-words": "關鍵字匹配", + "all": "所有", + "any": "任何", + "posted-by": "發表", + "in-categories": "在版面", + "search-child-categories": "搜索子版面", + "has-tags": "有標籤", + "reply-count": "回覆數", + "at-least": "至少", + "at-most": "至多", + "relevance": "關聯性", + "post-time": "貼文時間", + "votes": "點贊數", + "newer-than": "晚於", + "older-than": "早於", + "any-date": "任何日期", + "yesterday": "昨天", + "one-week": "一週", + "two-weeks": "兩週", + "one-month": "一個月", + "three-months": "三個月", + "six-months": "六個月", + "one-year": "一年", + "sort-by": "排序", + "last-reply-time": "最後回覆時間", + "topic-title": "主題標題", + "topic-votes": "主題點贊數", + "number-of-replies": "回文數", + "number-of-views": "查看數", + "topic-start-date": "主題開始日期", + "username": "帳戶", + "category": "版面", + "descending": "降冪排序", + "ascending": "升冪排序", + "save-preferences": "存為偏好設定", + "clear-preferences": "清除偏好設定", + "search-preferences-saved": "搜尋偏好設定已儲存", + "search-preferences-cleared": "搜尋偏好設定已清除", + "show-results-as": "結果顯示為", + "see-more-results": "查看更多結果 (%1)", + "search-in-category": "Search in \"%1\"" +} \ No newline at end of file diff --git a/public/language/zh-TW/success.json b/public/language/zh-TW/success.json new file mode 100644 index 0000000000..759e67120d --- /dev/null +++ b/public/language/zh-TW/success.json @@ -0,0 +1,7 @@ +{ + "success": "成功", + "topic-post": "您已成功發佈。", + "post-queued": "Your post is queued for approval. You will get a notification when it is accepted or rejected.", + "authentication-successful": "驗證成功", + "settings-saved": "設定已儲存!" +} \ No newline at end of file diff --git a/public/language/zh-TW/tags.json b/public/language/zh-TW/tags.json new file mode 100644 index 0000000000..08d3c49cae --- /dev/null +++ b/public/language/zh-TW/tags.json @@ -0,0 +1,8 @@ +{ + "no_tag_topics": "此標籤還沒有主題貼文。", + "tags": "標籤", + "enter_tags_here": "在這裡輸入標籤,每個標籤 %1 到 %2 個字元。", + "enter_tags_here_short": "輸入標籤...", + "no_tags": "尚無標籤。", + "select_tags": "選擇標籤" +} \ No newline at end of file diff --git a/public/language/zh-TW/top.json b/public/language/zh-TW/top.json new file mode 100644 index 0000000000..62fae776f8 --- /dev/null +++ b/public/language/zh-TW/top.json @@ -0,0 +1,4 @@ +{ + "title": "Top", + "no_top_topics": "無置頂主題" +} \ No newline at end of file diff --git a/public/language/zh-TW/topic.json b/public/language/zh-TW/topic.json new file mode 100644 index 0000000000..b519872d63 --- /dev/null +++ b/public/language/zh-TW/topic.json @@ -0,0 +1,188 @@ +{ + "topic": "主題", + "title": "標題", + "no_topics_found": "沒有找到主題!", + "no_posts_found": "沒有找到回覆!", + "post_is_deleted": "此回覆已被刪除!", + "topic_is_deleted": "此主題已被刪除!", + "profile": "個人資料", + "posted_by": "%1 發佈", + "posted_by_guest": "訪客發佈", + "chat": "聊天", + "notify_me": "此主題有新回覆時通知我", + "quote": "引用", + "reply": "回覆", + "replies_to_this_post": "%1 條回覆", + "one_reply_to_this_post": "1 條回覆", + "last_reply_time": "最後回覆", + "reply-as-topic": "在新貼文中回覆", + "guest-login-reply": "登入後回覆", + "login-to-view": "🔒登入查看", + "edit": "編輯", + "delete": "刪除", + "delete-event": "Delete Event", + "delete-event-confirm": "Are you sure you want to delete this event?", + "purge": "清除", + "restore": "恢復", + "move": "移動", + "change-owner": "更改所有者", + "fork": "分割", + "link": "連結", + "share": "分享", + "tools": "工具", + "locked": "已鎖定", + "pinned": "已置頂", + "pinned-with-expiry": "Pinned until %1", + "scheduled": "Scheduled", + "moved": "已移動", + "moved-from": "Moved from %1", + "copy-ip": "複製IP", + "ban-ip": "禁用IP", + "view-history": "編輯歷史", + "locked-by": "Locked by", + "unlocked-by": "Unlocked by", + "pinned-by": "Pinned by", + "unpinned-by": "Unpinned by", + "deleted-by": "Deleted by", + "restored-by": "Restored by", + "moved-from-by": "Moved from %1 by", + "queued-by": "Post queued for approval →", + "backlink": "Referenced by", + "forked-by": "Forked by", + "bookmark_instructions": "點擊閱讀本主題貼文中的最新回覆", + "flag-post": "Flag this post", + "flag-user": "Flag this user", + "already-flagged": "Already Flagged", + "view-flag-report": "View Flag Report", + "resolve-flag": "Resolve Flag", + "merged_message": "This topic has been merged into %2", + "deleted_message": "此主題已被刪除。只有擁有主題管理權限的使用者可以查看。", + "following_topic.message": "當有人回覆此主題時,您會收到通知。", + "not_following_topic.message": "您將在未讀主題列表中看到這個主題,但您不會在貼文被回覆時收到通知。", + "ignoring_topic.message": "您將不會在未讀主題列表裡看到這個主題,但在被提及以及貼文被點贊時仍將收到通知。", + "login_to_subscribe": "請註冊或登入後,再訂閱此主題。", + "markAsUnreadForAll.success": "將全部主題標為未讀。", + "mark_unread": "標記為未讀", + "mark_unread.success": "主題已被標記為未讀。", + "watch": "關注", + "unwatch": "取消關注", + "watch.title": "當此主題有新回覆時,通知我", + "unwatch.title": "取消關注此主題", + "share_this_post": "分享此貼文", + "watching": "關注中", + "not-watching": "未關注", + "ignoring": "忽略中", + "watching.description": "有新回覆時通知我。
在未讀主題中顯示。", + "not-watching.description": "不要在有新回覆時通知我。
如果這個版面未被忽略則在未讀主題中顯示。", + "ignoring.description": "不要在有新回覆時通知我。
不要在未讀主題中顯示該主題。", + "thread_tools.title": "主題工具", + "thread_tools.markAsUnreadForAll": "全部標記為未讀", + "thread_tools.pin": "置頂主題", + "thread_tools.unpin": "取消置頂主題", + "thread_tools.lock": "鎖定主題", + "thread_tools.unlock": "解鎖主題", + "thread_tools.move": "移動主題", + "thread_tools.move-posts": "移動貼文", + "thread_tools.move_all": "移動全部", + "thread_tools.change_owner": "更改所有者", + "thread_tools.select_category": "選擇版面", + "thread_tools.fork": "分割主題", + "thread_tools.delete": "刪除主題", + "thread_tools.delete-posts": "刪除回覆", + "thread_tools.delete_confirm": "確定要刪除此主題嗎?", + "thread_tools.restore": "恢復主題", + "thread_tools.restore_confirm": "確定要恢復此主題嗎?", + "thread_tools.purge": "清除主題", + "thread_tools.purge_confirm": "確認清除此主題嗎?", + "thread_tools.merge_topics": "合併主題", + "thread_tools.merge": "合併", + "topic_move_success": "This topic will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_multiple_success": "These topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_all_success": "All topics will be moved to \"%1\" shortly. Click here to undo.", + "topic_move_undone": "Topic move undone", + "topic_move_posts_success": "Posts will be moved shortly. Click here to undo.", + "topic_move_posts_undone": "Post move undone", + "post_delete_confirm": "您確定要刪除此回覆嗎?", + "post_restore_confirm": "您確定要恢復此回覆嗎?", + "post_purge_confirm": "您確定要清除此回覆嗎?", + "pin-modal-expiry": "Expiration Date", + "pin-modal-help": "You can optionally set an expiration date for the pinned topic(s) here. Alternatively, you can leave this field blank to have the topic stay pinned until it is manually unpinned.", + "load_categories": "正在載入版面", + "confirm_move": "移動", + "confirm_fork": "分割", + "bookmark": "書籤", + "bookmarks": "書籤", + "bookmarks.has_no_bookmarks": "您還沒有加入任何書籤", + "copy-permalink": "Copy Permalink", + "loading_more_posts": "正在載入更多貼文", + "move_topic": "移動主題", + "move_topics": "移動主題", + "move_post": "移動貼文", + "post_moved": "回覆已移動!", + "fork_topic": "分割主題", + "enter-new-topic-title": "Enter new topic title", + "fork_topic_instruction": "點擊將分割的貼文", + "fork_no_pids": "未選中貼文!", + "no-posts-selected": "No posts selected!", + "x-posts-selected": "%1 post(s) selected", + "x-posts-will-be-moved-to-y": "%1 post(s) will be moved to \"%2\"", + "fork_pid_count": "選擇了 %1 則貼文", + "fork_success": "成功分割主題! 點這裡跳轉到分割後的主題。", + "delete_posts_instruction": "點擊想要刪除/永久刪除的貼文", + "merge_topics_instruction": "Click the topics you want to merge or search for them", + "merge-topic-list-title": "List of topics to be merged", + "merge-options": "Merge options", + "merge-select-main-topic": "Select the main topic", + "merge-new-title-for-topic": "New title for topic", + "topic-id": "Topic ID", + "move_posts_instruction": "Click the posts you want to move then enter a topic ID or go to the target topic", + "change_owner_instruction": "點擊您想轉移給其他使用者的貼文", + "composer.title_placeholder": "在此輸入您主題的標題...", + "composer.handle_placeholder": "在此輸入您的名稱/代稱", + "composer.discard": "撤銷", + "composer.submit": "提交", + "composer.additional-options": "Additional Options", + "composer.schedule": "Schedule", + "composer.replying_to": "正在回覆 %1", + "composer.new_topic": "新主題", + "composer.editing": "Editing", + "composer.uploading": "正在上傳...", + "composer.thumb_url_label": "添加主題縮圖網址", + "composer.thumb_title": "給此主題添加縮圖", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "或上傳檔案", + "composer.thumb_remove": "清除欄位", + "composer.drag_and_drop_images": "拖曳圖片到此處", + "more_users_and_guests": "%1 名使用者和 %2 名訪客", + "more_users": "%1 名使用者", + "more_guests": "%1 名訪客", + "users_and_others": "%1 和 %2 其他人", + "sort_by": "排序", + "oldest_to_newest": "從舊到新", + "newest_to_oldest": "從新到舊", + "most_votes": "最多點贊", + "most_posts": "回覆最多", + "most_views": "Most Views", + "stale.title": "接受建議並建立新主題?", + "stale.warning": "您回覆的主題已經很古老了,是否發佈新主題並引用此主題的內容?", + "stale.create": "建立新主題", + "stale.reply_anyway": "仍然回覆此貼文", + "link_back": "回覆: [%1](%2)", + "diffs.title": "歷史發佈記錄", + "diffs.description": "此主題已經重新發布並修訂。點擊某個時間點查看修訂的內容。", + "diffs.no-revisions-description": "該貼文已重新修改", + "diffs.current-revision": "當前版本", + "diffs.original-revision": "原始版本", + "diffs.restore": "Restore this revision", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", + "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", + "timeago_later": "%1 後", + "timeago_earlier": "%1 前", + "first-post": "First post", + "last-post": "Last post", + "go-to-my-next-post": "Go to my next post", + "no-more-next-post": "You don't have more posts in this topic", + "post-quick-reply": "Post quick reply" +} \ No newline at end of file diff --git a/public/language/zh-TW/unread.json b/public/language/zh-TW/unread.json new file mode 100644 index 0000000000..f20b451ede --- /dev/null +++ b/public/language/zh-TW/unread.json @@ -0,0 +1,15 @@ +{ + "title": "未讀", + "no_unread_topics": "沒有未讀主題。", + "load_more": "載入更多", + "mark_as_read": "標為已讀", + "selected": "已選", + "all": "全部", + "all_categories": "全部版面", + "topics_marked_as_read.success": "主題被標為已讀!", + "all-topics": "全部主題", + "new-topics": "新建主題", + "watched-topics": "關注主題", + "unreplied-topics": "未回覆主題", + "multiple-categories-selected": "多選" +} \ No newline at end of file diff --git a/public/language/zh-TW/uploads.json b/public/language/zh-TW/uploads.json new file mode 100644 index 0000000000..a0b1451682 --- /dev/null +++ b/public/language/zh-TW/uploads.json @@ -0,0 +1,9 @@ +{ + "uploading-file": "正在上傳檔案...", + "select-file-to-upload": "請選擇需要上傳的檔案!", + "upload-success": "檔案上傳成功!", + "maximum-file-size": "最大 %1 kb", + "no-uploads-found": "沒有找到上傳檔案", + "public-uploads-info": "上傳公開的檔案,所有訪客均可查看。", + "private-uploads-info": "上傳私有的檔案,僅登入使用者可見。" +} \ No newline at end of file diff --git a/public/language/zh-TW/user.json b/public/language/zh-TW/user.json new file mode 100644 index 0000000000..7f4e9db7f1 --- /dev/null +++ b/public/language/zh-TW/user.json @@ -0,0 +1,199 @@ +{ + "banned": "已停權", + "muted": "Muted", + "offline": "離線", + "deleted": "已刪除", + "username": "使用者名", + "joindate": "註冊日期", + "postcount": "貼文數量", + "email": "電子信箱", + "confirm_email": "確認電子信箱", + "account_info": "帳戶訊息", + "admin_actions_label": "管理行動", + "ban_account": "禁用帳戶", + "ban_account_confirm": "您確定要禁用此帳戶、嗎?", + "unban_account": "解禁帳戶", + "mute_account": "Mute Account", + "unmute_account": "Unmute Account", + "delete_account": "刪除帳戶", + "delete_account_as_admin": "刪除 帳戶", + "delete_content": "刪除帳戶 內容", + "delete_all": "Delete Account and Content", + "delete_account_confirm": "Are you sure you want to anonymize your posts and delete your account?
This action is irreversible and you will not be able to recover any of your data

Enter your password to confirm that you wish to destroy this account.", + "delete_this_account_confirm": "Are you sure you want to delete this account while leaving its contents behind?
This action is irreversible, posts will be anonymized, and you will not be able to restore post associations with the deleted account

", + "delete_account_content_confirm": "Are you sure you want to delete this account's content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "delete_all_confirm": "Are you sure you want to delete this account and all of its content (posts/topics/uploads)?
This action is irreversible and you will not be able to recover any data

", + "account-deleted": "帳戶已刪除", + "account-content-deleted": "帳戶內容已刪除", + "fullname": "姓名", + "website": "網站", + "location": "位置", + "age": "年齡", + "joined": "註冊時間", + "lastonline": "最後登入", + "profile": "個人資料", + "profile_views": "個人資料瀏覽", + "reputation": "聲望", + "bookmarks": "書籤", + "watched_categories": "已關注的版面", + "change_all": "全部更改", + "watched": "已關注", + "ignored": "已忽略", + "default-category-watch-state": "預設版面關注狀態", + "followers": "追隨者", + "following": "追隨", + "blocks": "屏蔽", + "block_toggle": "封鎖該使用者", + "block_user": "封鎖使用者", + "unblock_user": "解封使用者", + "aboutme": "關於我", + "signature": "簽名檔", + "birthday": "生日", + "chat": "聊天", + "chat_with": "繼續與 %1 聊天", + "new_chat_with": "開始與 %1 的新對話", + "flag-profile": "舉報個人檔案", + "follow": "追隨", + "unfollow": "取消追隨", + "more": "更多", + "profile_update_success": "資料已經成功更新。", + "change_picture": "更改頭像", + "change_username": "更改帳戶", + "change_email": "更改電子信箱", + "email_same_as_password": "請輸入您當前的密碼以繼續 –您已經再次輸入了您的新電子信箱", + "edit": "編輯", + "edit-profile": "編輯個人檔案", + "default_picture": "預設頭像", + "uploaded_picture": "已有頭像", + "upload_new_picture": "上傳新頭像", + "upload_new_picture_from_url": "上傳來自URL的新頭像", + "current_password": "當前密碼", + "change_password": "更改密碼", + "change_password_error": "無效的密碼!", + "change_password_error_wrong_current": "您的當前密碼不正確!", + "change_password_error_match": "兩次輸入的密碼必須相同!", + "change_password_error_privileges": "您無權更改此密碼。", + "change_password_success": "您的密碼已更新!", + "confirm_password": "確認密碼", + "password": "密碼", + "username_taken_workaround": "您申請的帳戶已被佔用,所以我們稍作更改。您現在的帳戶是 %1", + "password_same_as_username": "您的密碼與帳戶相同,請選擇另外的密碼。", + "password_same_as_email": "您的密碼與郵箱相同,請選擇另外的密碼。", + "weak_password": "密碼強度低。", + "upload_picture": "上傳頭像", + "upload_a_picture": "上傳頭像", + "remove_uploaded_picture": "刪除已上傳的頭像", + "upload_cover_picture": "上傳封面圖片", + "remove_cover_picture_confirm": "您確定要移除封面圖片嗎?", + "crop_picture": "剪裁圖片", + "upload_cropped_picture": "剪裁併上傳", + "avatar-background-colour": "Avatar background colour", + "settings": "設定", + "show_email": "顯示我的電子信箱", + "show_fullname": "顯示我的全名", + "restrict_chats": "只允許我追隨的使用者給我發送聊天訊息", + "digest_label": "訂閱摘要", + "digest_description": "訂閱此論壇的定期電子郵件更新 (新通知和主題)", + "digest_off": "關閉", + "digest_daily": "每天", + "digest_weekly": "每週", + "digest_biweekly": "Bi-Weekly", + "digest_monthly": "每月", + "has_no_follower": "此使用者還沒有追隨者 :(", + "follows_no_one": "此使用者尚未追隨任何人 :(", + "has_no_posts": "此使用者從未發文。", + "has_no_best_posts": "This user does not have any upvoted posts yet.", + "has_no_topics": "此使用者還未發佈任何主題。", + "has_no_watched_topics": "此使用者還未關注任何主題。", + "has_no_ignored_topics": "此使用者尚未忽略任何主題。", + "has_no_upvoted_posts": "此使用者還未點贊過任何貼文。", + "has_no_downvoted_posts": "此使用者還未倒讚過任何貼文。", + "has_no_controversial_posts": "This user does not have any downvoted posts yet.", + "has_no_blocks": "您沒有封鎖其他使用者。", + "email_hidden": "電子信箱已隱藏", + "hidden": "隱藏", + "paginate_description": "使用分頁式版面瀏覽", + "topics_per_page": "每頁主題數", + "posts_per_page": "每頁貼文數", + "max_items_per_page": "最大值 %1", + "acp_language": "控制台頁面語言", + "notifications": "Notifications", + "upvote-notif-freq": "貼文被讚的通知頻率", + "upvote-notif-freq.all": "每一次被讚都通知我", + "upvote-notif-freq.first": "首次點贊貼文", + "upvote-notif-freq.everyTen": "每10次被點贊通知我一次", + "upvote-notif-freq.threshold": "當被點贊的數目為1, 5, 10, 25, 50, 100, 150, 200...時通知我", + "upvote-notif-freq.logarithmic": "當被點讚的數目為10, 100, 1000...時通知我", + "upvote-notif-freq.disabled": "任何時候都不要通知我", + "browsing": "瀏覽設定", + "open_links_in_new_tab": "在新頁籤打開外部連結", + "enable_topic_searching": "啟用主題內搜索", + "topic_search_help": "如果啟用此項,主題內搜索會替代瀏覽器預設的頁面搜索,您將可以在整個主題內搜索,而不僅僅只搜索頁面上展現的內容。", + "update_url_with_post_index": "Update url with post index while browsing topics", + "scroll_to_my_post": "在提交回覆之後顯示新回覆", + "follow_topics_you_reply_to": "關注您回覆過的主題", + "follow_topics_you_create": "關注您建立的主題", + "grouptitle": "群組稱號", + "group-order-help": "選擇群組然後使用箭頭排列稱號", + "no-group-title": "不顯示群組稱號", + "select-skin": "選擇配色", + "select-homepage": "選擇首頁", + "homepage": "首頁", + "homepage_description": "選擇一個頁面作為論壇的首頁,否則設置為 ‘空’ 使用預設首頁。", + "custom_route": "自訂首頁路徑", + "custom_route_help": "Enter a route name here, without any preceding slash (e.g. \"recent\" or \"category/2/general-discussion\")", + "sso.title": "單一簽入服務", + "sso.associated": "已關聯到", + "sso.not-associated": "點擊這裡來關聯", + "sso.dissociate": "解除關聯", + "sso.dissociate-confirm-title": "確認解除關聯", + "sso.dissociate-confirm": "您確定要將您的帳戶與 %1 解除關聯嗎?", + "info.latest-flags": "最新舉報", + "info.no-flags": "沒有找到被舉報的貼文", + "info.ban-history": "最近停權紀錄", + "info.no-ban-history": "該使用者從未被停權", + "info.banned-until": "停權直到 %1", + "info.banned-expiry": "過期", + "info.banned-permanently": "永久禁用", + "info.banned-reason-label": "原因", + "info.banned-no-reason": "沒有原因", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", + "info.muted-no-reason": "No reason given.", + "info.username-history": "用過的使用者名", + "info.email-history": "用過的電子信箱", + "info.moderation-note": "版主備註", + "info.moderation-note.success": "版主備註已儲存", + "info.moderation-note.add": "新增備註", + "sessions.description": "此頁面允許您查看當前論壇的所有當前的會話(active session),並在需要的時候關閉它們.您可以通過登出您的帳戶來關閉自己的會話(session)", + "consent.title": "您的權利與許可", + "consent.lead": "本論壇將會收集與處理您的個人資料。", + "consent.intro": "我們收集這些資料將僅用於個人化您於本社區的體驗,和關聯您的帳戶與您所發表的貼文。在註冊過程中您需要提供一個使用者名和信箱地址,您也可以選擇是否提供額外的個人資料,以完善您的使用者檔案。

在您的帳戶有效期內,我們將保留您的資料。您可以在任何時候通過刪除您的帳戶,以撤回您的許可。您可以在任何時候通過您的權力與許可頁面,獲取一份您對本論壇的貢獻的副本。

如果您有任何疑問,我們鼓勵您與本論壇管理團隊聯繫。", + "consent.email_intro": "我們有時可能會向您的註冊信箱發送電子郵件,以向您提供有關於您的新動態和/或新活動。您可以通過您的使用者設定頁面自訂(包括直接禁用)社區摘要的發送頻率,以及選擇性地接收哪些類型的通知。", + "consent.digest_frequency": "本社區預設每 %1 發送一封摘要郵件,除非您在使用者設定中明確更改了此設定。", + "consent.digest_off": "本社區預設不發送摘要郵件,除非您在使用者設置中明確更改了此設定。", + "consent.received": "您已許可本網站收集與處理您的個人資料。無需其它額外操作。", + "consent.not_received": "您未許可本網站收集與處理您的個人資料。本網站的管理團隊可能於任何時候刪除您的帳戶,以符合通用資料保護條例的要求。", + "consent.give": "授予許可", + "consent.right_of_access": "您擁有資料訪問權", + "consent.right_of_access_description": "您有權訪問本網站根據需求收集的您的任何資料。您可以點擊下方相應按鈕,獲取這些數據的副本。", + "consent.right_to_rectification": "您擁有資料更正權", + "consent.right_to_rectification_description": "您擁有修改或更新提供給我們的任何不準確的個人資料的權力。您可以通過編輯以更新個人資料,並可以修改您發表的內容。若非如此,請聯繫本網站的管理團隊。", + "consent.right_to_erasure": "您擁有被遺忘權", + "consent.right_to_erasure_description": "您隨時都可以通過刪除帳戶,來撤銷資料收集和處理的許可。您的個人檔案可以被刪除,但是您發表的內容仍然會保留。如果您想要同時刪除您的帳戶內容,請聯繫此網站的管理團隊。", + "consent.right_to_data_portability": "您擁有資料轉移權", + "consent.right_to_data_portability_description": "您也許想導出有關您和您的帳戶的機器可讀副本。您可以點擊下方的按鈕來獲取它們。", + "consent.export_profile": "輸出個人資料 (.json)", + "consent.export-profile-success": "Exporting profile, you will get a notification when it is complete.", + "consent.export_uploads": "導出上傳檔案 (.zip)", + "consent.export-uploads-success": "Exporting uploads, you will get a notification when it is complete.", + "consent.export_posts": "導出貼文 (.csv)", + "consent.export-posts-success": "Exporting posts, you will get a notification when it is complete.", + "emailUpdate.intro": "Please enter your email address below. This forum uses your email address for scheduled digest and notifications, as well as for account recovery in the event of a lost password.", + "emailUpdate.optional": "This field is optional. You are not obligated to provide your email address, but without a validated email you will not be able to recover your account or login with your email.", + "emailUpdate.required": "This field is required.", + "emailUpdate.change-instructions": "A confirmation email will be sent to the entered email address with a unique link. Accessing that link will confirm your ownership of the email address and it will become active on your account. At any time, you are able to update your email on file from within your account page.", + "emailUpdate.password-challenge": "Please enter your password in order to verify account ownership." +} \ No newline at end of file diff --git a/public/language/zh-TW/users.json b/public/language/zh-TW/users.json new file mode 100644 index 0000000000..2e67d9c826 --- /dev/null +++ b/public/language/zh-TW/users.json @@ -0,0 +1,24 @@ +{ + "latest_users": "最新使用者", + "top_posters": "發文排行", + "most_reputation": "聲望排行", + "most_flags": "舉報最多", + "search": "搜尋", + "enter_username": "輸入使用者名搜索", + "search-user-for-chat": "Search a user to start chat", + "load_more": "載入更多", + "users-found-search-took": "找到 %1 位使用者!耗時 %2 秒。", + "filter-by": "過濾選項", + "online-only": "只看在線", + "invite": "邀請註冊", + "prompt-email": "郵件:", + "groups-to-join": "同意邀請後要加入的群組:", + "invitation-email-sent": "已發送邀請給 %1", + "user_list": "使用者列表", + "recent_topics": "最新主題", + "popular_topics": "熱門主題", + "unread_topics": "未讀主題", + "categories": "版面", + "tags": "標籤", + "no-users-found": "未找到符合的使用者!" +} \ No newline at end of file diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less new file mode 100644 index 0000000000..26432e1226 --- /dev/null +++ b/public/less/admin/admin.less @@ -0,0 +1,293 @@ +@import "bootstrap/less/bootstrap"; +@import "./paper/variables"; +@import "./paper/bootswatch"; +@import "./mixins"; +@import "./vars"; + +@import "./header"; +@import "./mobile"; + +@import "./general/dashboard"; +@import "./general/navigation"; +@import "./manage/categories"; +@import "./manage/privileges"; +@import "./manage/tags"; +@import "./manage/groups"; +@import "./manage/registration"; +@import "./manage/users"; +@import "./manage/admins-mods"; +@import "./appearance/customise"; +@import "./appearance/themes"; +@import "./extend/plugins"; +@import "./extend/rewards"; +@import "./extend/widgets"; +@import "./advanced/database"; +@import "./advanced/events"; +@import "./advanced/logs"; +@import "./advanced/errors"; +@import "./advanced/hooks"; +@import "./development/info"; +@import "./settings"; + +@import "../flags"; + +@import "./modules/alerts"; +@import "./modules/selectable"; +@import "./modules/nprogress"; +@import "./modules/search"; + +body { + overflow-y: scroll; +} + +.admin { + background: #fff; + font-size: 14px; + + h1 { + font-size: 35px; + margin-bottom: 50px; + } + + label { + font-weight: 700; + height: auto; + } + + + .btn { + border-radius: 0; + } + + .btn-link { + color: @link-color; + } + + // .floating-button can either be a container or the button itself + .floating-button { + position: fixed; + right: 30px; + bottom: 30px; + z-index: 1; + max-width: 56px; + + button { + &.primary { + background: @brand-primary !important; + } + + &.success { + background: @brand-success !important; + } + + &:not(:last-child) { + margin-bottom: 2rem; + } + } + } + button.floating-button { + background: @brand-primary !important; + } + + .user-img { + width:24px; + height:24px; + } + + .nodebb-logo { + img { + height: 31px; + margin-top: -8px; + margin-left: -7px; + vertical-align: -43%; + } + + .box-header-font; + color: #fff; + } + + #breadcrumbs { + cursor: default; + } + + .acp-panel-heading { + padding: 7px 14px; + border: 0; + .box-header-font; + } + + .panel:not([data-container-html]) { + background-color: #FFF; + box-sizing: border-box; + border-radius: 3px; + box-shadow: 0px 1px 3px 0px rgba(165, 165, 165, 0.75); + margin-bottom: 20px; + + &.panel-default >.panel-heading { + .acp-panel-heading; + background: #fefefe; + color: #333; + } + + &.panel-danger >.panel-heading { + .acp-panel-heading; + } + } + + .nav-header { + .box-header-font + } + + .icon-container { + .row { + margin: 0; + i { + width:20px; + height:20px; + margin: 1px; + .pointer; + line-height: 20px; + text-align: center; + color: @gray-dark; + + &:hover, &.selected { + background: @brand-primary; + color: white; + } + } + } + } + + .navbar-static-top, .navbar-fixed-top { + box-shadow: 0px -3px 12px rgba(0, 0, 0, 0.5); + } + + .navbar-header > .navbar-toggle { + margin-right: 8px; + } + + .navbar-nav { + margin-top: 0; + margin-bottom: 0; + + >li { + >a { + padding-top: 15px; + padding-bottom: 15px; + } + + >a:hover, >a:focus { + color: @gray-dark; + background-color: @gray-light; + } + + >#reconnect { + color: @gray-light; + } + + >#reconnect:focus, >#reconnect:hover { + color: @gray-light; + background-color: transparent; + } + } + } + + #taskbar { + display: none; /* not sure why I have to do this, but it only seems to show up on prod */ + } + + /* Allows the autocomplete dropbox to appear on top of a modal's backdrop */ + .ui-autocomplete { + z-index: @zindex-popover; + } +} + +// Allowing text to the right of an image-type brand +// See: https://github.com/twbs/bootstrap/commit/8e2348e9eda51296eb680192379ab37f10355ca3 +.navbar-brand > img { + display: inline-block; +} + +.category-settings-form { + h3 { + margin-top: 0; + .pointer; + } + + h4 { + .pointer; + } +} + +.category-preview { + .pointer; + width: 100%; + height: 100px; + text-align: center; + color: white; + margin-top: 0; + + .icon { + width: 30px; + height: 30px; + line-height: 40px; + display: inline-block; + margin: 35px 5px 0 5px; + } +} +[component="category-selector"] { + .fa-stack { + border-radius: 50%; + } + .category-dropdown-menu { + max-height: 400px; + overflow-y: auto; + } +} + +.table-reordering { + tr:hover { + cursor: move; + } +} + +.privilege-table { + th { + font-size: 10px; + } + + img { + max-width: 24px; + max-height: 24px; + } +} + +.mdl-switch.is-checked .mdl-switch__ripple-container { + cursor: pointer !important; +} + +.mdl-switch.is-checked .mdl-switch__thumb { + background: @brand-primary !important; +} + +.mdl-switch.is-checked .mdl-switch__track { + background: lighten(@brand-primary, 20%) !important; +} + +* > .checkbox:first-child { + margin-top: 0px; +} + +[class^="col-"] .mdl-switch__label { + padding-right: 15px; +} + +.ui-selectable-helper { + border: 1px dashed @brand-success; + background: lighten(@brand-success, 10%); + opacity: 0.5; +} + +form small { + color: @gray-light; +} \ No newline at end of file diff --git a/public/less/admin/advanced/database.less b/public/less/admin/advanced/database.less new file mode 100644 index 0000000000..3799ced20a --- /dev/null +++ b/public/less/admin/advanced/database.less @@ -0,0 +1,23 @@ +.database-info { + span { + display:inline-block; + width:220px; + } +} + + + + + + + + + + + + + + + + + diff --git a/public/less/admin/advanced/errors.less b/public/less/admin/advanced/errors.less new file mode 100644 index 0000000000..89b9dda540 --- /dev/null +++ b/public/less/admin/advanced/errors.less @@ -0,0 +1,26 @@ + +.page-advanced-errors { + .table { + table-layout: fixed; + + th { + &:first-child { + width: 90%; + } + + &:last-child { + text-align: center; + } + } + + td { + &:first-child { + word-wrap: break-word; + } + + &:last-child { + text-align: center; + } + } + } +} \ No newline at end of file diff --git a/public/less/admin/advanced/events.less b/public/less/admin/advanced/events.less new file mode 100644 index 0000000000..4f4c95afbb --- /dev/null +++ b/public/less/admin/advanced/events.less @@ -0,0 +1,8 @@ +.events-list { + .delete-event { + i { + cursor: pointer; + margin-left: 10px; + } + } +} diff --git a/public/less/admin/advanced/hooks.less b/public/less/admin/advanced/hooks.less new file mode 100644 index 0000000000..ef8ca579f4 --- /dev/null +++ b/public/less/admin/advanced/hooks.less @@ -0,0 +1,3 @@ +.admin .hooks-list .panel.panel-default .panel-heading .panel-title a { + text-transform: none; +} diff --git a/public/less/admin/advanced/logs.less b/public/less/admin/advanced/logs.less new file mode 100644 index 0000000000..3a1023130b --- /dev/null +++ b/public/less/admin/advanced/logs.less @@ -0,0 +1,8 @@ + +.logs { + .panel-body { + pre { + height: 600px; + } + } +} \ No newline at end of file diff --git a/public/less/admin/appearance/customise.less b/public/less/admin/appearance/customise.less new file mode 100644 index 0000000000..3bef7fa560 --- /dev/null +++ b/public/less/admin/appearance/customise.less @@ -0,0 +1,9 @@ +#customCSS, #customJS, #customHTML, #email-editor { + width: 100%; + height: 450px; + display: block; +} +// ACP text colour when searching through custom CSS or JS. +.ace_search_field { + color: #000 !important; +} diff --git a/public/less/admin/appearance/themes.less b/public/less/admin/appearance/themes.less new file mode 100644 index 0000000000..f69222999b --- /dev/null +++ b/public/less/admin/appearance/themes.less @@ -0,0 +1,77 @@ +.themes, .skins { + ul.directory { + margin: 0; + padding: 0; + + li { + padding: 10px 16px; + margin: 0.25em 1em; + list-style-type: none; + .pointer; + + img { + max-width: 150px; + float: left; + } + + h4, p { + margin-left: 170px; + } + + p { + font-size: 0.9em; + } + + &.no-themes { + font-style: italic; + } + } + } + + .theme-card { + margin-bottom: 30px; + margin-left: auto; + margin-right: auto; + + .mdl-card__title { + height: 223px; + background-size: contain; + } + + .mdl-card__supporting-text { + font-size: 1.5rem; + margin: 0 auto; + + .mdl-card__title-text { + display: inline-block; + margin-bottom: 15px; + } + } + } + + [data-theme].selected .mdl-button { + color: black; + } + + [data-type="bootswatch"] { + .mdl-card__title { + height: 198px; + } + + .mdl-card__title-text { + display: none; + } + } + + [data-type="local"] { + .mdl-card__supporting-text { + height: 150px; + } + } + + textarea[data-field] { + min-height: 450px; + width: 100%; + resize: vertical; + } +} \ No newline at end of file diff --git a/public/less/admin/development/info.less b/public/less/admin/development/info.less new file mode 100644 index 0000000000..188d0a1118 --- /dev/null +++ b/public/less/admin/development/info.less @@ -0,0 +1,3 @@ +.page-admin-info #content { + width: auto; +} \ No newline at end of file diff --git a/public/less/admin/extend/plugins.less b/public/less/admin/extend/plugins.less new file mode 100644 index 0000000000..d7e4584f02 --- /dev/null +++ b/public/less/admin/extend/plugins.less @@ -0,0 +1,58 @@ +.plugins { + padding-left: 0px; + + li { + list-style-type: none; + background: rgba(64, 64, 64, 0.05); + padding: 1em; + margin-bottom: 5px; + border-left: 5px solid #08c; + margin-left: -40px; + + h2 { + font-size: 16px; + margin: 0; + } + + p { + font-size: 12px; + } + } + + .plugin-list.ui-sortable { + li { + .pointer; + + .fa-chevron-up { + margin-right: 10px; + } + + .fa-chevron-up, .fa-chevron-down { + border: 1px solid; + border-radius: 50%; + padding: 3px; + vertical-align: 1px; + background-color: white; + } + + &:first-child .fa-chevron-up, &:last-child .fa-chevron-down { + pointer-events: none; + color: @gray-light; + } + } + } + .controls .btn { + display: list-item; + width: 120px; + margin-bottom: 3px; + margin-left: 10px; + } + + .acp-sidebar { + .mdl-switch__label { + margin-left: 24px; + display: block; + left: 0; + } + } +} \ No newline at end of file diff --git a/public/less/admin/extend/rewards.less b/public/less/admin/extend/rewards.less new file mode 100644 index 0000000000..e90eb3e717 --- /dev/null +++ b/public/less/admin/extend/rewards.less @@ -0,0 +1,54 @@ +#rewards { + .well, .panel-body { + vertical-align: top; + min-height: 100px; + + &.pull-right { + min-height: 0px; + } + } + + ul { + list-style-type: none; + padding: 0px; + margin: 0px; + + > li { + border-bottom: 1px solid #ddd; + margin-bottom: 20px; + &:last-child { + border-bottom: 0; + } + } + } + + .rewards { width: 100%; } + + .well { + border-radius: 2px; + border-width: 2px; + color: #333; + + &.if-block { + border-color: @brand-primary; + max-width: 33%; + } + &.this-block { + border-color: @brand-warning; + max-width: 33%; + } + &.then-block { + border-color: @brand-success; + max-width: 33%; + } + &.reward-block { + border-color: @brand-success; + background-color: lighten(@brand-success, 15%); + color: #fff; + a, select, input { color: #fff; } + select > option { color: #333; } + width: 100%; + min-height: 110px; + } + } +} \ No newline at end of file diff --git a/public/less/admin/extend/widgets.less b/public/less/admin/extend/widgets.less new file mode 100644 index 0000000000..8316030446 --- /dev/null +++ b/public/less/admin/extend/widgets.less @@ -0,0 +1,19 @@ +.page-extend-widgets { + [component="clone"] { + display: flex; + align-items: stretch; + align-content: stretch; + + [component="clone/button"] { + flex-grow: 1; + text-align: left; + } + + .dropdown-menu { + max-height: 300px; + overflow-y: scroll; + min-width: 250px; + border-radius: 0; + } + } +} \ No newline at end of file diff --git a/public/less/admin/general/dashboard.less b/public/less/admin/general/dashboard.less new file mode 100644 index 0000000000..f7e3f8ff72 --- /dev/null +++ b/public/less/admin/general/dashboard.less @@ -0,0 +1,204 @@ +.dashboard { + max-width: 1680px; + + .panel { + max-width: 100% !important; + } + + #analytics-panel .panel-heading > div { + &.fa-expand { + display: none; + } + + font-family: @font-family-sans-serif; + font-weight: 600; + color: @gray-dark; + padding-left: .5em; + + padding: .75em; + background-color: @gray-lighter; + color: @gray-base; + cursor: pointer; + .transition(all .4s); + + &.active { + display: inline; + } + } + + .graph-container { + padding-right: 50px; + position: relative; + background: @body-bg; + + &:hover { + .fa-expand { + color: @gray-lighter; + background-color: @gray-base; + } + } + + &.fullscreen { + width: 100%; + padding: 40px; + + .fa-expand { + top: 20px; + } + + .graph-legend { + top: 7rem; + left: 12rem; + } + } + + &.pie-chart { + padding-right: 0px; + padding-left: 50px; + min-height: 180px; + + .graph-legend { + top: -10px; + left: 0px; + } + + &.compact { + padding-left: 0px; + padding-top: 60px; + } + + &.legend-down { + padding-left: 0px; + padding-top: 0px; + + canvas { + margin-bottom: 25px; + } + + .graph-legend { + position: relative; + + li { + float: left; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &:nth-child(odd) { + margin-right: 2%; + } + } + } + } + } + + .graph-legend { + .box-header-font; + display: inline-block; + max-width: 100%; + position: absolute; + top: 2rem; + left: 7rem; + list-style-type: none; + padding: 0.5rem 1rem; + margin: 0; + background: rgba(255, 255, 255, 0.66); + border: 1px solid #ddd; + + li { + div { + border: 1px solid; + width: 12px; + height: 12px; + vertical-align: -41%; + margin-bottom: 5px; + display: inline-block; + margin-right: 5px; + + &.page-views { + border-color: rgba(220,220,220,1); + background-color: rgba(220,220,220,0.2); + } + &.unique-visitors { + border-color: rgba(151,187,205,1); + background-color: rgba(151,187,205,0.2); + } + &.guest { + border-color: #46BFBD; + background-color: #5AD3D1; + } + &.registered { + border-color: #F7464A; + background-color: #FF5A5E; + } + &.reading-posts { + border-color: #46BFBD; + background-color: #5AD3D1; + } + &.on-categories { + border-color: #F7464A; + background-color: #FF5A5E; + } + &.browsing-topics { + border-color: #FDB45C; + background-color: #FFC870; + } + &.recent { + border-color: #949FB1; + background-color: #A8B3C5; + } + &.unread { + border-color: #949FB1; + background-color: #9FB194; + } + } + } + } + + + } + + .version-check { + -webkit-transition: background, color 500ms ease-in; + -moz-transition: background, color 500ms ease-in; + -ms-transition: background, color 500ms ease-in; + -o-transition: background, color 500ms ease-in; + transition: background, color 500ms ease-in; + } + + #unique-visitors, #active-users { + margin-left: -15px; + > div { + width: 25%; + font-size: 13px; + } + } + + .pageview-stats { + strong { + font-size: 22px; + } + } + + .motd textarea { + width: 100%; + } + + .stats { + .formatted-number { + font-size: 22px; + } + + .stat { + text-transform: uppercase; + font-weight: 600; + font-size: 10px; + color: #999; + } + } + + .updatePageviewsGraph.active { + font-weight: bold; + } +} diff --git a/public/less/admin/general/navigation.less b/public/less/admin/general/navigation.less new file mode 100644 index 0000000000..9d0316ee3a --- /dev/null +++ b/public/less/admin/general/navigation.less @@ -0,0 +1,57 @@ +#navigation { + .fa-nbb-none { + display: inline-block; + width: 16px; + height: 16px; + border: 2px dashed #aaa; + position: relative; + top: 0.2em; + } + + #active-navigation { + float: none; + min-height: 50px; + border: 1px solid #eee; + overflow: auto; + + .active { + background-color: #eee; + } + + li a { + cursor: move; + } + + li { + display: inline-block; + } + } + + #available { + .drag-item { + cursor: move; + margin-right: 10px; + padding: 8px 10px; + margin-bottom: 5px; + } + + p { + line-height: 20px; + min-height: 40px; + } + } + + #enabled { + .iconPicker i { + cursor: pointer; + } + .form-group { + min-height: 80px; + } + } + + ul { + list-style-type: none; + padding: 0; + } +} \ No newline at end of file diff --git a/public/less/admin/header.less b/public/less/admin/header.less new file mode 100644 index 0000000000..63d8e8bcdb --- /dev/null +++ b/public/less/admin/header.less @@ -0,0 +1,163 @@ +.header { + .no-select; + position: relative; + background: #333; + width: 100%; + height: 200px; + margin-bottom: 50px; + font-size: 16px; + + #main-page-title { + position: absolute; + left: 48px; + bottom: 17px; + color: #aaa; + font-size: 47px; + font-weight: 300; + } + + .quick-actions { + position: static; + padding: 15px; + display: flex; + flex-direction: row-reverse; + margin: 0; + + li { + align-self: end; + } + + > * { + margin-right: 20px; + } + + > .menu-button { + margin-right: 0; + padding: 0 5px; + } + + .alert { + font-size: 14px; + margin-bottom: 5px; + + &.alert-info { + background-color: #eee; + color: #333; + } + } + + .dropdown { + margin-right: 0px; + + .dropdown-toggle i { + padding: 0 1rem; + } + } + + .fa { + line-height: 44px; + font-size: 25px; + } + + #user_dropdown { + font-size: 25px; + color: #eee; + + i { + margin-top: 12px; + display: block; + } + } + } + + #acp-search { + input { + padding: 10px 20px; + width: 250px; + height: 44px; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 3px; + box-shadow: none; + .transition(.4s ease background-color); + + &:focus { + background-color: #eee; + color: #333; + } + } + + .dropdown:not(.open) { + &:before { + content: '/'; + border: 1px solid @gray; + border-radius: 5px; + padding: 0px 6px; + font-size: 12px; + font-weight: 600; + pointer-events: none; + + position: absolute; + top: 10px; + left: 1em; + } + + &:after { + content: attr(data-text); + position: absolute; + top: 10px; + left: 3em; + font-size: small; + font-weight: 600; + pointer-events: none; + } + + input { + color: transparent; + } + } + + .search-match { + font-weight: 700; + color: black; + } + } + + #main-menu > li { + padding-bottom: 10px; + } + + > ul { + list-style-type: none; + padding: 0px; + position: absolute; + bottom: -11px; + left: 50px; + + > li { + float: left; + margin-right: 30px; + border-bottom: 4px solid transparent; + transition: border-color 150ms linear; + + &:hover { + border-color: darken(@brand-primary, 20%); + } + + &.active { + border-color: @brand-primary; + } + + > a { + color: white; + text-transform: uppercase; + text-decoration: none; + outline: none; + } + } + } + + .plugins-menu { + max-height: 50vh; + overflow-y: auto; + } +} diff --git a/public/less/admin/manage/admins-mods.less b/public/less/admin/manage/admins-mods.less new file mode 100644 index 0000000000..a8efbf00a1 --- /dev/null +++ b/public/less/admin/manage/admins-mods.less @@ -0,0 +1,30 @@ +.admins-mods { + .user-card { + background: #eeeeee; + border-radius: 4px; + margin: 5px; + padding: 5px; + height: 35px; + } + + .remove-user-icon { + margin-right: 5px; + margin-left: 5px; + } + + .category-depth-1 { + margin-left: 30px; + } + .category-depth-2 { + margin-left: 60px; + } + .category-depth-3 { + margin-left: 90px; + } + .category-depth-4 { + margin-left: 120px; + } + .category-depth-5 { + margin-left: 150px; + } +} \ No newline at end of file diff --git a/public/less/admin/manage/categories.less b/public/less/admin/manage/categories.less new file mode 100644 index 0000000000..8350b574c8 --- /dev/null +++ b/public/less/admin/manage/categories.less @@ -0,0 +1,135 @@ +div.categories { + ul[data-cid] { + .no-select; + list-style-type: none; + margin: 0; + padding: 0; + + > li > ul > li { + margin-left: 4.5rem; + } + > li > a { + margin-left: 4.5rem; + } + .row { + margin-left: -15px; + margin-right: -15px; + } + + > li li:last-child { + .row { + border-bottom: 0px; + } + } + > li { + margin: 16px 0 24px 0; + + &.placeholder { + border: 1px dashed #2196F3; + background-color: #E1F5FE; + } + } + } + + .stats { + display: inline-block; + + li { + min-height: 0; + display: inline; + margin: 0 16px 0 0; + left: 0; + } + } + + + .disabled > .category-row { + + .icon, .category-header, .description { + opacity: 0.5; + } + + .stats { + opacity: 0.3; + } + } + + .toggle { + width: 24px; + height: 24px; + border-radius: 50%; + line-height: 24px; + text-align: center; + vertical-align: bottom; + background-size: cover; + float: left; + margin-right: 0px; + cursor: pointer; + .fa { + font-size: 85%; + } + } + + .information { + cursor: move; + padding-left: 3rem; + + .icon { + width: 24px; + height: 24px; + border-radius: 50%; + line-height: 24px; + text-align: center; + vertical-align: bottom; + background-size: cover; + float: left; + margin-right: 1rem; + + .fa { + font-size: 85%; + } + } + } + + .category-header { + margin-top: 0; + margin-bottom: 8px; + } + + .description { + margin: 0; + } + + .children-placeholder{ + min-height: 20px; + height: 20px; + } +} + +.category { + .privilege-table { + tr > th:first-child { + min-width: 150px; + } + + .privilege-table-header { + background: white; + + th { + text-align: center; + border-top: 0; + text-transform: uppercase; + font-size: 9px; + vertical-align: bottom; + } + + .arrowed:after { + border-bottom: 1px dashed #ccc; + content: ""; + width: 100%; + display: block; + padding-top: 5px; + } + } + } +} \ No newline at end of file diff --git a/public/less/admin/manage/groups.less b/public/less/admin/manage/groups.less new file mode 100644 index 0000000000..2183cc748c --- /dev/null +++ b/public/less/admin/manage/groups.less @@ -0,0 +1,52 @@ +.group { + [component="groups/members"] { + padding: 0; + tbody { + max-height: 500px; + display: block; + overflow-y: auto; + .member-name { + width: 100%; + } + } + } + + #group-icon { + cursor: pointer; + } +} + +.groups { + #group-search { + margin-bottom: 10px; + } + + .groups-list { + p { + margin: 0; + } + td { + max-width: 350px; + } + } +} + +.page-admin-groups { + [component="category/list"] li { + cursor: pointer; + } + + .fa-nbb-none { + border: 1px dotted black; + height: 35px; + width: 35px; + } + + .fa-icons .fa-nbb-none { + vertical-align: -6px; + } + + #group-icon-preview.fa-nbb-none { + display: none; + } +} \ No newline at end of file diff --git a/public/less/admin/manage/privileges.less b/public/less/admin/manage/privileges.less new file mode 100644 index 0000000000..8614373b0b --- /dev/null +++ b/public/less/admin/manage/privileges.less @@ -0,0 +1,36 @@ +.page-admin-privileges { + @keyframes fadeOut { + 0% {background-color: @brand-primary;} + 100% {background-color: white;} + } + + [data-group-name].selected, [data-uid].selected { + animation-name: fadeOut; + animation-duration: 5s; + animation-fill-mode: both; + animation-timing-function: ease-out; + } + + .privilege-table { + td:first-child { + white-space: nowrap; + } + + td[data-delta="true"] > input { + &:after { + border-color: @state-success-text; + background-color: @state-success-text; + } + } + + td[data-delta="false"] > input { + &:after { + border-color: @state-danger-bg; + } + + &:indeterminate:after { + background-color: @state-danger-bg; + } + } + } +} \ No newline at end of file diff --git a/public/less/admin/manage/registration.less b/public/less/admin/manage/registration.less new file mode 100644 index 0000000000..ea483e727d --- /dev/null +++ b/public/less/admin/manage/registration.less @@ -0,0 +1,7 @@ +@media screen and (max-width: @screen-sm-max) { + .page-manage-registration { + .users-list { + font-size: 10px; + } + } +} \ No newline at end of file diff --git a/public/less/admin/manage/tags.less b/public/less/admin/manage/tags.less new file mode 100644 index 0000000000..fc4ea9f2e1 --- /dev/null +++ b/public/less/admin/manage/tags.less @@ -0,0 +1,31 @@ +.tags { + .tag-list { + h3 { + min-width: 225px; + } + + .tag-row { + padding: 0.5rem; + float: left; + margin-left: 0.5rem; + + .tag-item { + cursor: pointer; + display: inline-block; + font-size: 11px; + } + + &.ui-selected { + background: lighten(@brand-success, 25%); + } + + &.ui-selecting { + background: lighten(@brand-success, 40%); + } + } + } + + .tag-topic-count { + font-size: 14px; + } +} \ No newline at end of file diff --git a/public/less/admin/manage/users.less b/public/less/admin/manage/users.less new file mode 100644 index 0000000000..097c13d4ac --- /dev/null +++ b/public/less/admin/manage/users.less @@ -0,0 +1,19 @@ +.manage-users { + min-height: 500px; + .search { + .form-control { + width: 100%; + } + } +} + +.page-admin-users { + .group-card { + margin: 2px; + padding: 2px; + } + + .remove-group-icon { + margin-left: 5px; + } +} \ No newline at end of file diff --git a/public/less/admin/mixins.less b/public/less/admin/mixins.less new file mode 100644 index 0000000000..36315ce5f2 --- /dev/null +++ b/public/less/admin/mixins.less @@ -0,0 +1,7 @@ +@import "../mixins"; + +.box-header-font { + font-size: 11px; + text-transform: uppercase; + font-weight: 700; +} \ No newline at end of file diff --git a/public/less/admin/mobile.less b/public/less/admin/mobile.less new file mode 100644 index 0000000000..021145b1ae --- /dev/null +++ b/public/less/admin/mobile.less @@ -0,0 +1,197 @@ +#mobile-menu { + display: none; +} + +@media (max-width: 991px) { + body { + height: 100%; + } + + #panel { + background-color: inherit; + min-height: 100%; + } + + body, #panel, .slideout-menu { + -webkit-overflow-scrolling: touch; + } + + .header { + height: 58px; + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.26); + position: fixed; + top: 0px; + z-index: 5; + + #main-page-title { + bottom: -31px; + font-size: 20px; + color: #FFF; + left: 52px; + font-weight: 400; + } + + #user_label { + right: 0px; + bottom: 7px; + } + + #main-menu { + display: none; + } + } + + #mobile-menu { + width: 22px; + background: none; + border: none; + margin-right: 10px; + margin-left: -5px; + outline: none !important; + display: block; + + position: absolute; + top: 22px; + left: 22px; + + .bar { + width: 100%; + height: 2px; + background: #fff; + margin-bottom: 3px; + border-radius: 10px; + } + } + + #menu { + background-color: #1D1F20; + background-image: linear-gradient(145deg, #1D1F20, #404348); + + a { + color: #fff; + text-decoration: none; + } + + a:hover { + text-decoration: underline; + } + } + + .menu-header-title { + font-weight: 400; + letter-spacing: 0.5px; + margin: 0; + } + + .menu-section { + margin: 25px 0; + + &.quick-actions { + margin: 0; + + .button-group { + display: flex; + justify-content: center; + } + + .alert { + border-radius: 0; + + .span { + display: block; + } + } + } + } + + .menu-section-title { + text-transform: uppercase; + color: #85888d; + font-weight: 200; + font-size: 13px; + letter-spacing: 1px; + padding: 0 20px; + margin:0; + } + + .menu-section-list { + padding: 0; + margin: 10px 0; + list-style: none; + + a { + display: block; + padding: 10px 20px; + } + + a:hover { + background-color: rgba(255, 255, 255, 0.1); + text-decoration: none; + } + } + + #panel { + background: white; + min-height: 100%; + padding-top: 80px; + } + + .slideout-menu { + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 0; + width: 256px; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + display: none; + } + + + .slideout-panel { + position: relative; + z-index: 1; + } + + .slideout-open, + .slideout-open body, + .slideout-open .slideout-panel { + overflow: hidden; + overflow-y: hidden !important; + } + + .slideout-open .slideout-menu { + display: block; + } + + html { + height: 100%; + overflow-y: hidden; + } + + .slideout-open { + overflow-y: hidden; + height: 100%; + } + + body { + overflow-y: scroll; + overflow-x: hidden; + } +} + +@media (max-width: 768px) { + .content-header, .settings-header { + font-size: 200%; + margin-bottom: 20px; + margin-left: -2px; + } + + + .dropdown-menu { + margin-top: -35px; + margin-right: -2px; + } +} \ No newline at end of file diff --git a/public/less/admin/modules/alerts.less b/public/less/admin/modules/alerts.less new file mode 100644 index 0000000000..6463576efc --- /dev/null +++ b/public/less/admin/modules/alerts.less @@ -0,0 +1,95 @@ +.alert-window { + position: fixed; + width: 300px; + z-index: 10002; + + right: 20px; + bottom: 0px; + + .alert { + .close { + color: inherit; + } + + &::before { + position: relative; + top: -15px; + left: -15px; + display: block; + height: 2px; + width: 0; + transition: inherit; + } + + &.alert-info::before { + background-color: @brand-info; + } + + &.alert-warning::before { + background-color: @brand-warning; + } + + &.alert-success::before { + background-color: @brand-success; + } + + &.alert-danger::before { + background-color: @brand-danger; + } + + &.animate { + &.alert-info::before { + background-color: lighten(@brand-info, 25%); + } + + &.alert-warning::before { + background-color: lighten(@brand-warning, 25%); + } + + &.alert-success::before { + background-color: lighten(@brand-success, 25%); + } + + &.alert-danger::before { + background-color: lighten(@brand-danger, 25%); + } + + &::before { + width: ~"calc(100% + 50px)"; + } + } + + background-color: white; + border: 0; + border-left: 5px solid !important; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.25), 0px 2px 10px 0px rgba(0, 0, 0, 0.25); + + strong { + text-transform: uppercase; + } + + p { + padding: 10px 0px 0px; + } + + &.alert-info { + color: @brand-info; + border-color: @brand-info; + } + + &.alert-warning { + color: @brand-warning; + border-color: @brand-warning; + } + + &.alert-success { + color: @brand-success; + border-color: @brand-success; + } + + &.alert-danger { + color: @brand-danger; + border-color: @brand-danger; + } + } +} diff --git a/public/less/admin/modules/nprogress.less b/public/less/admin/modules/nprogress.less new file mode 100644 index 0000000000..c1b6effe02 --- /dev/null +++ b/public/less/admin/modules/nprogress.less @@ -0,0 +1,80 @@ +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: #29d; + + position: fixed; + z-index: 1031; + top: 0; + left: 0; + + width: 100%; + height: 2px; +} + +#nprogress .peg { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #29d, 0 0 5px #29d; + opacity: 1.0; + + -webkit-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +#nprogress .spinner { + display: block; + position: fixed; + z-index: 1031; + top: 165px; + right: 35px; +} + +@media (max-width: @screen-xs-max) { + #nprogress .spinner { + bottom: 15px; + right: 15px; + top: initial; + } +} + + +#nprogress .spinner-icon { + width: 18px; + height: 18px; + box-sizing: border-box; + + border: solid 2px transparent; + border-top-color: #29d; + border-left-color: #29d; + border-radius: 50%; + + -webkit-animation: nprogress-spinner 400ms linear infinite; + animation: nprogress-spinner 400ms linear infinite; +} + +.nprogress-custom-parent { + overflow: hidden; + position: relative; +} + +.nprogress-custom-parent #nprogress .spinner, +.nprogress-custom-parent #nprogress .bar { + position: absolute; +} + +@-webkit-keyframes nprogress-spinner { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); } +} +@keyframes nprogress-spinner { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + diff --git a/public/less/admin/modules/search.less b/public/less/admin/modules/search.less new file mode 100644 index 0000000000..d2286005bf --- /dev/null +++ b/public/less/admin/modules/search.less @@ -0,0 +1,45 @@ +#acp-search { + .dropdown-menu { + max-height: 75vh; + overflow-y: auto; + + > li > a { + &.focus { + &:extend(.dropdown-menu>li>a:focus); + } + &:focus { + outline: none; + } + } + } + + .state-start-typing { + .keep-typing, .search-forum, .no-results { + display: none; + } + } + + .state-keep-typing { + .start-typing, .search-forum, .no-results { + display: none; + } + } + + .state-no-results { + .keep-typing, .start-typing { + display: none; + } + } + + .state-yes-results { + .keep-typing, .start-typing, .no-results { + display: none; + } + } + + .search-disabled { + .search-forum { + display: none; + } + } +} \ No newline at end of file diff --git a/public/less/admin/modules/selectable.less b/public/less/admin/modules/selectable.less new file mode 100644 index 0000000000..7a1721fe23 --- /dev/null +++ b/public/less/admin/modules/selectable.less @@ -0,0 +1,23 @@ +.selectable { + .user-select(none); + position: relative; + + .selector { + position: absolute; + border: 1px solid #89B; + background: #BCE; + background-color: #BEC; + border-color: #8B9; + z-index: 999; + } + + .selection { + border: 1px solid transparent; + margin: 2px; + + &.selected, &.active { + background-color: #ECF1DB; + border: 1px dashed #9B8; + } + } +} \ No newline at end of file diff --git a/public/less/admin/paper/bootswatch.less b/public/less/admin/paper/bootswatch.less new file mode 100644 index 0000000000..024e48970b --- /dev/null +++ b/public/less/admin/paper/bootswatch.less @@ -0,0 +1,621 @@ +// Paper 3.3.5 +// Bootswatch +// ----------------------------------------------------- + +// Navbar ===================================================================== + +.navbar { + border: none; + .box-shadow(0 1px 2px rgba(0,0,0,.3)); + + &-brand { + font-size: 24px; + } + + &-inverse { + .form-control { + color: #fff; + .placeholder(@navbar-inverse-link-color); + + &[type=text], + &[type=password] { + .box-shadow(inset 0 -1px 0 @navbar-inverse-link-color); + + &:focus { + .box-shadow(inset 0 -2px 0 #fff); + } + } + } + } +} + +// Buttons ==================================================================== + +#btn(@class,@bg) { + .btn-@{class} { + background-size: 200%; + background-position: 50%; + + &:focus { + background-color: @bg; + } + + &:hover, + &:active:hover { + background-color: darken(@bg, 6%); + } + + &:active { + background-color: darken(@bg, 12%); + #gradient > .radial(darken(@bg, 12%) 10%, @bg 11%); + background-size: 1000%; + .box-shadow(2px 2px 4px rgba(0,0,0,.4)); + } + } +} + +#btn(default,@btn-default-bg); +#btn(primary,@btn-primary-bg); +#btn(success,@btn-success-bg); +#btn(info,@btn-info-bg); +#btn(warning,@btn-warning-bg); +#btn(danger,@btn-danger-bg); +#btn(link,#fff); + +.btn { + text-transform: uppercase; + border: none; + .box-shadow(1px 1px 4px rgba(0,0,0,.4)); + .transition(all 0.4s); + + &-link { + border-radius: @btn-border-radius-base; + .box-shadow(none); + color: @btn-default-color; + + &:hover, + &:focus { + .box-shadow(none); + color: @btn-default-color; + text-decoration: none; + } + } + + &-default { + + &.disabled { + background-color: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.4); + opacity: 1; + } + } +} + +.btn-group { + .btn + .btn, + .btn + .btn-group, + .btn-group + .btn, + .btn-group + .btn-group { + margin-left: 0; + } + + &-vertical { + > .btn + .btn, + > .btn + .btn-group, + > .btn-group + .btn, + > .btn-group + .btn-group { + margin-top: 0; + } + } +} + +// Typography ================================================================= + +body { + -webkit-font-smoothing: antialiased; + letter-spacing: .1px; +} + +p { + margin: 0 0 1em; +} + +input, +button { + -webkit-font-smoothing: antialiased; + letter-spacing: .1px; +} + +a { + .transition(all 0.2s); +} + +// Tables ===================================================================== + +.table-hover { + > tbody > tr, + > tbody > tr > th, + > tbody > tr > td { + .transition(all 0.2s); + } +} + +// Forms ====================================================================== + +label { + font-weight: normal; +} + +textarea, +textarea.form-control, +input.form-control, +input[type=text], +input[type=password], +input[type=email], +input[type=number], +[type=text].form-control, +[type=password].form-control, +[type=email].form-control, +[type=tel].form-control, +[contenteditable].form-control { + padding: 0; + border: none; + border-radius: 0; + -webkit-appearance: none; + .box-shadow(inset 0 -1px 0 #ddd); + font-size: 16px; + + &:focus { + .box-shadow(inset 0 -2px 0 @brand-primary); + } + + &[disabled], + &[readonly] { + .box-shadow(none); + border-bottom: 1px dotted #ddd; + } + + &.input { + &-sm { + font-size: @font-size-small; + } + + &-lg { + font-size: @font-size-large; + } + } +} + +select, +select.form-control { + border: 0; + border-radius: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + padding-left: 0; + padding-right: 0\9; // remove padding for < ie9 since default arrow can't be removed + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEVmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmaP/QSjAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=); + background-size: 13px; + background-repeat: no-repeat; + background-position: right center; + .box-shadow(inset 0 -1px 0 #ddd); + font-size: 16px; + line-height: 1.5; + + &::-ms-expand { + display: none; + } + + &.input { + &-sm { + font-size: @font-size-small; + } + + &-lg { + font-size: @font-size-large; + } + } + + &:focus { + .box-shadow(inset 0 -2px 0 @brand-primary); + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEUhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISF8S9ewAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=); + } + + &[multiple] { + background: none; + } +} + +.radio, +.radio-inline, +.checkbox, +.checkbox-inline { + label { + padding-left: 25px; + } + + input[type="radio"], + input[type="checkbox"] { + margin-left: -25px; + } +} + +input[type="radio"], +.radio input[type="radio"], +.radio-inline input[type="radio"] { + position: relative; + margin-top: 6px; + margin-right: 4px; + vertical-align: top; + border: none; + background-color: transparent; + -webkit-appearance: none; + appearance: none; + cursor: pointer; + + &:focus { + outline: none; + } + + &:before, + &:after { + content: ""; + display: block; + width: 18px; + height: 18px; + border-radius: 50%; + .transition(240ms); + } + + &:before { + position: absolute; + left: 0; + top: -3px; + background-color: @brand-primary; + .scale(0); + } + + &:after { + position: relative; + top: -3px; + border: 2px solid @gray; + } + + &:checked:before { + .scale(0.5); + } + + &:disabled:checked:before { + background-color: @gray-light; + } + + &:checked:after { + border-color: @brand-primary; + } + + &:disabled:after, + &:disabled:checked:after { + border-color: @gray-light; + } +} + +input[type="checkbox"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: relative; + border: none; + margin-bottom: -4px; + -webkit-appearance: none; + appearance: none; + cursor: pointer; + + &:focus { + outline: none; + } + + &:after { + content: ""; + display: block; + width: 18px; + height: 18px; + margin-top: -2px; + margin-right: 5px; + border: 2px solid @gray; + border-radius: 2px; + .transition(240ms); + } + + &:indeterminate:before { + content: ""; + position: absolute; + top: 6px; + left: 6px; + display: table; + width: 6px; + height: 12px; + border-top: 2px solid #fff; + } + + &:indeterminate:after { + background-color: @brand-primary; + border-color: @brand-primary; + } + + &:checked:before { + content: ""; + position: absolute; + top: 0; + left: 6px; + display: table; + width: 6px; + height: 12px; + border: 2px solid #fff; + border-top-width: 0; + border-left-width: 0; + .rotate(45deg); + } + + &:checked:after { + background-color: @brand-primary; + border-color: @brand-primary; + } + + &:disabled:after { + border-color: @gray-light; + } + + &:disabled:checked:after { + background-color: @gray-light; + border-color: transparent; + } +} + +.has-warning { + input:not([type=checkbox]), + .form-control, + input.form-control[readonly], + input[type=text][readonly], + [type=text].form-control[readonly], + input:not([type=checkbox]):focus, + .form-control:focus { + border-bottom: none; + .box-shadow(inset 0 -2px 0 @brand-warning); + } +} + +.has-error { + input:not([type=checkbox]), + .form-control, + input.form-control[readonly], + input[type=text][readonly], + [type=text].form-control[readonly], + input:not([type=checkbox]):focus, + .form-control:focus { + border-bottom: none; + .box-shadow(inset 0 -2px 0 @brand-danger); + } +} + +.has-success { + input:not([type=checkbox]), + .form-control, + input.form-control[readonly], + input[type=text][readonly], + [type=text].form-control[readonly], + input:not([type=checkbox]):focus, + .form-control:focus { + border-bottom: none; + .box-shadow(inset 0 -2px 0 @brand-success); + } +} + +// Remove the Bootstrap feedback styles for input addons +.input-group-addon { + .has-warning &, .has-error &, .has-success & { + color: @input-color; + border-color: @input-group-addon-border-color; + background-color: @input-group-addon-bg; + } +} + +// Navs ======================================================================= + +.nav-tabs { + > li > a, + > li > a:focus { + margin-right: 0; + background-color: transparent; + border: none; + color: @navbar-default-link-color; + .box-shadow(inset 0 -1px 0 #ddd); + .transition(all 0.2s); + + &:hover { + background-color: transparent; + .box-shadow(inset 0 -2px 0 @brand-primary); + color: @brand-primary; + } + } + + & > li.active > a, + & > li.active > a:focus { + border: none; + .box-shadow(inset 0 -2px 0 @brand-primary); + color: @brand-primary; + + &:hover { + border: none; + color: @brand-primary; + } + } + + & > li.disabled > a { + .box-shadow(inset 0 -1px 0 #ddd); + } + + &.nav-justified { + + & > li > a, + & > li > a:hover, + & > li > a:focus, + & > .active > a, + & > .active > a:hover, + & > .active > a:focus { + border: none; + } + } + + .dropdown-menu { + margin-top: 0; + } +} + +.dropdown-menu { + margin-top: 0; + border: none; + .box-shadow(0 1px 4px rgba(0,0,0,.3)); +} + +// Indicators ================================================================= + +.alert { + border: none; + color: #fff; + + &-success { + background-color: @brand-success; + } + + &-info { + background-color: @brand-info; + } + + &-warning { + background-color: @brand-warning; + } + + &-danger { + background-color: @brand-danger; + } + + a:not(.close), + .alert-link { + color: #fff; + font-weight: bold; + } + + .close { + color: #fff; + } +} + +.badge { + padding: 3px 6px 5px; +} + +.progress { + position: relative; + z-index: 1; + height: 6px; + border-radius: 0; + + .box-shadow(none); + + &-bar { + .box-shadow(none); + + &:last-child { + border-radius: 0 3px 3px 0; + } + + &:last-child { + &:before { + display: block; + content: ""; + position: absolute; + width: 100%; + height: 100%; + left: 0; + right: 0; + z-index: -1; + background-color: lighten(@progress-bar-bg, 35%); + } + } + + &-success:last-child.progress-bar:before { + background-color: lighten(@brand-success, 35%); + } + + &-info:last-child.progress-bar:before { + background-color: lighten(@brand-info, 45%); + } + &-warning:last-child.progress-bar:before { + background-color: lighten(@brand-warning, 35%); + } + + &-danger:last-child.progress-bar:before { + background-color: lighten(@brand-danger, 25%); + } + } +} + +// Progress bars ============================================================== + +// Containers ================================================================= + +.close { + font-size: 34px; + font-weight: 300; + line-height: 24px; + opacity: 0.6; + .transition(all 0.2s); + + &:hover { + opacity: 1; + } +} + +.list-group { + + &-item { + padding: 15px; + } + + &-item-text { + color: @gray-light; + } +} + +.well { + border-radius: 0; + .box-shadow(none); +} + +.panel { + border: none; + border-radius: 2px; + .box-shadow(0 1px 4px rgba(0,0,0,.3)); + + &-heading { + border-bottom: none; + } + + &-footer { + border-top: none; + } +} + +.popover { + border: none; + .box-shadow(0 1px 4px rgba(0,0,0,.3)); +} + +.carousel { + &-caption { + h1, h2, h3, h4, h5, h6 { + color: inherit; + } + } +} + diff --git a/public/less/admin/paper/variables.less b/public/less/admin/paper/variables.less new file mode 100644 index 0000000000..eed5b3cfae --- /dev/null +++ b/public/less/admin/paper/variables.less @@ -0,0 +1,869 @@ +// Paper 3.3.7 +// Variables +// -------------------------------------------------- + + +//== Colors +// +//## Gray and brand colors for use across Bootstrap. + +@gray-base: #000; +@gray-darker: lighten(@gray-base, 13.5%); // #222 +@gray-dark: #212121; +@gray: #666; +@gray-light: #bbb; +@gray-lighter: lighten(@gray-base, 93.5%); // #eee + +@brand-primary: #2196F3; +@brand-success: #4CAF50; +@brand-info: #9C27B0; +@brand-warning: #ff9800; +@brand-danger: #e51c23; + + +//== Scaffolding +// +//## Settings for some of the most global styles. + +//** Background color for ``. +@body-bg: #fff; +//** Global text color on ``. +@text-color: @gray; + +//** Global textual link color. +@link-color: @brand-primary; +//** Link hover color set via `darken()` function. +@link-hover-color: darken(@link-color, 15%); +//** Link hover decoration. +@link-hover-decoration: underline; + + +//== Typography +// +//## Font, line-height, and color for body text, headings, and more. + +@font-family-sans-serif: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-serif: Georgia, "Times New Roman", Times, serif; +//** Default monospace fonts for ``, ``, and `

`.
+@font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base:        @font-family-sans-serif;
+
+@font-size-base:          13px;
+@font-size-large:         ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small:         ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1:            56px;
+@font-size-h2:            45px;
+@font-size-h3:            34px;
+@font-size-h4:            24px;
+@font-size-h5:            20px;
+@font-size-h6:            14px;
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base:        1.846; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed:    floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the ``.
+@headings-font-family:    inherit;
+@headings-font-weight:    400;
+@headings-line-height:    1.1;
+@headings-color:          #444;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+@icon-font-path:          "../fonts/";
+//** File name for all font files.
+@icon-font-name:          "glyphicons-halflings-regular";
+//** Element ID within SVG icon file.
+@icon-font-svg-id:        "glyphicons_halflingsregular";
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical:     6px;
+@padding-base-horizontal:   16px;
+
+@padding-large-vertical:    10px;
+@padding-large-horizontal:  16px;
+
+@padding-small-vertical:    5px;
+@padding-small-horizontal:  10px;
+
+@padding-xs-vertical:       1px;
+@padding-xs-horizontal:     5px;
+
+@line-height-large:         1.3333333; // extra decimals for Win 8.1 Chrome
+@line-height-small:         1.5;
+
+@border-radius-base:        3px;
+@border-radius-large:       3px;
+@border-radius-small:       3px;
+
+//** Global color for active items (e.g., navs or dropdowns).
+@component-active-color:    #fff;
+//** Global background color for active items (e.g., navs or dropdowns).
+@component-active-bg:       @brand-primary;
+
+//** Width of the `border` for generating carets that indicate dropdowns.
+@caret-width-base:          4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large:         5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ``s.
+@table-cell-padding:            8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding:  5px;
+
+//** Default background color used for all tables.
+@table-bg:                      transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent:               #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover:                #f5f5f5;
+@table-bg-active:               @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color:            #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight:                normal;
+
+@btn-default-color:              #444;
+@btn-default-bg:                 #fff;
+@btn-default-border:             transparent;
+
+@btn-primary-color:              #fff;
+@btn-primary-bg:                 @brand-primary;
+@btn-primary-border:             transparent;
+
+@btn-success-color:              #fff;
+@btn-success-bg:                 @brand-success;
+@btn-success-border:             transparent;
+
+@btn-info-color:                 #fff;
+@btn-info-bg:                    @brand-info;
+@btn-info-border:                transparent;
+
+@btn-warning-color:              #fff;
+@btn-warning-bg:                 @brand-warning;
+@btn-warning-border:             transparent;
+
+@btn-danger-color:               #fff;
+@btn-danger-bg:                  @brand-danger;
+@btn-danger-border:              transparent;
+
+@btn-link-disabled-color:        @gray-light;
+
+// Allows for customizing button radius independently from global border radius
+@btn-border-radius-base:         @border-radius-base;
+@btn-border-radius-large:        @border-radius-large;
+@btn-border-radius-small:        @border-radius-small;
+
+
+//== Forms
+//
+//##
+
+//** `` background color
+@input-bg:                       transparent;
+//** `` background color
+@input-bg-disabled:              transparent;
+
+//** Text color for ``s
+@input-color:                    @gray;
+//** `` border color
+@input-border:                   transparent;
+
+// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on ``s in CSS.
+@input-border-radius:            @border-radius-base;
+//** Large `.form-control` border radius
+@input-border-radius-large:      @border-radius-large;
+//** Small `.form-control` border radius
+@input-border-radius-small:      @border-radius-small;
+
+//** Border color for inputs on focus
+@input-border-focus:             #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder:        @gray-light;
+
+//** Default `.form-control` height
+@input-height-base:              (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large:             (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small:             (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+//** `.form-group` margin
+@form-group-margin-bottom:       15px;
+
+@legend-color:                   @gray-dark;
+@legend-border-color:            #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg:           transparent;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+//** Disabled cursor for form controls and buttons.
+@cursor-disabled:                not-allowed;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg:                    #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border:                rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border:       #ccc;
+//** Divider color for between dropdown items.
+@dropdown-divider-bg:            #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color:            @text-color;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color:      darken(@gray-dark, 5%);
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg:         @gray-lighter;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color:     @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg:        @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color:   @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color:          @gray-light;
+
+//** Deprecated `@dropdown-caret-color` as of v3.1.0
+@dropdown-caret-color:           @gray-light;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar:            1000;
+@zindex-dropdown:          1000;
+@zindex-popover:           1060;
+@zindex-tooltip:           1070;
+@zindex-navbar-fixed:      1030;
+@zindex-modal-background:  1040;
+@zindex-modal:             1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `@screen-xs` as of v3.0.1
+@screen-xs:                  480px;
+//** Deprecated `@screen-xs-min` as of v3.2.0
+@screen-xs-min:              @screen-xs;
+//** Deprecated `@screen-phone` as of v3.0.1
+@screen-phone:               @screen-xs-min;
+
+// Small screen / tablet
+//** Deprecated `@screen-sm` as of v3.0.1
+@screen-sm:                  768px;
+@screen-sm-min:              @screen-sm;
+//** Deprecated `@screen-tablet` as of v3.0.1
+@screen-tablet:              @screen-sm-min;
+
+// Medium screen / desktop
+//** Deprecated `@screen-md` as of v3.0.1
+@screen-md:                  992px;
+@screen-md-min:              @screen-md;
+//** Deprecated `@screen-desktop` as of v3.0.1
+@screen-desktop:             @screen-md-min;
+
+// Large screen / wide desktop
+//** Deprecated `@screen-lg` as of v3.0.1
+@screen-lg:                  1200px;
+@screen-lg-min:              @screen-lg;
+//** Deprecated `@screen-lg-desktop` as of v3.0.1
+@screen-lg-desktop:          @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max:              (@screen-sm-min - 1);
+@screen-sm-max:              (@screen-md-min - 1);
+@screen-md-max:              (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns:              12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width:         30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint:     @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet:             (720px + @grid-gutter-width);
+//** For `@screen-sm-min` and up.
+@container-sm:                 @container-tablet;
+
+// Medium screen / desktop
+@container-desktop:            (940px + @grid-gutter-width);
+//** For `@screen-md-min` and up.
+@container-md:                 @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop:      (1140px + @grid-gutter-width);
+//** For `@screen-lg-min` and up.
+@container-lg:                 @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height:                    64px;
+@navbar-margin-bottom:             @line-height-computed;
+@navbar-border-radius:             @border-radius-base;
+@navbar-padding-horizontal:        floor((@grid-gutter-width / 2));
+@navbar-padding-vertical:          ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height:       340px;
+
+@navbar-default-color:             @gray-light;
+@navbar-default-bg:                #fff;
+@navbar-default-border:            transparent;
+
+// Navbar links
+@navbar-default-link-color:                @gray;
+@navbar-default-link-hover-color:          @gray-dark;
+@navbar-default-link-hover-bg:             transparent;
+@navbar-default-link-active-color:         @gray-dark;
+@navbar-default-link-active-bg:            darken(@navbar-default-bg, 6.5%);
+@navbar-default-link-disabled-color:       #ccc;
+@navbar-default-link-disabled-bg:          transparent;
+
+// Navbar brand label
+@navbar-default-brand-color:               @navbar-default-link-color;
+@navbar-default-brand-hover-color:         @navbar-default-link-hover-color;
+@navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg:           transparent;
+@navbar-default-toggle-icon-bar-bg:        rgba(0,0,0,0.5);
+@navbar-default-toggle-border-color:       transparent;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+@navbar-inverse-color:                      @gray-light;
+@navbar-inverse-bg:                         @brand-primary;
+@navbar-inverse-border:                     transparent;
+
+// Inverted navbar links
+@navbar-inverse-link-color:                 lighten(@brand-primary, 30%);
+@navbar-inverse-link-hover-color:           #fff;
+@navbar-inverse-link-hover-bg:              transparent;
+@navbar-inverse-link-active-color:          @navbar-inverse-link-hover-color;
+@navbar-inverse-link-active-bg:             darken(@navbar-inverse-bg, 10%);
+@navbar-inverse-link-disabled-color:        #444;
+@navbar-inverse-link-disabled-bg:           transparent;
+
+// Inverted navbar brand label
+@navbar-inverse-brand-color:                @navbar-inverse-link-color;
+@navbar-inverse-brand-hover-color:          #fff;
+@navbar-inverse-brand-hover-bg:             transparent;
+
+// Inverted navbar toggle\
+@navbar-inverse-toggle-hover-bg:            transparent;
+@navbar-inverse-toggle-icon-bar-bg:         rgba(0,0,0,0.5);
+@navbar-inverse-toggle-border-color:        transparent;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding:                          10px 15px;
+@nav-link-hover-bg:                         @gray-lighter;
+
+@nav-disabled-link-color:                   @gray-light;
+@nav-disabled-link-hover-color:             @gray-light;
+
+//== Tabs
+@nav-tabs-border-color:                     transparent;
+
+@nav-tabs-link-hover-border-color:          @gray-lighter;
+
+@nav-tabs-active-link-hover-bg:             transparent;
+@nav-tabs-active-link-hover-color:          @gray;
+@nav-tabs-active-link-hover-border-color:   transparent;
+
+@nav-tabs-justified-link-border-color:            @nav-tabs-border-color;
+@nav-tabs-justified-active-link-border-color:     @body-bg;
+
+//== Pills
+@nav-pills-border-radius:                   @border-radius-base;
+@nav-pills-active-link-hover-bg:            @component-active-bg;
+@nav-pills-active-link-hover-color:         @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color:                     @link-color;
+@pagination-bg:                        #fff;
+@pagination-border:                    #ddd;
+
+@pagination-hover-color:               @link-hover-color;
+@pagination-hover-bg:                  @gray-lighter;
+@pagination-hover-border:              #ddd;
+
+@pagination-active-color:              #fff;
+@pagination-active-bg:                 @brand-primary;
+@pagination-active-border:             @brand-primary;
+
+@pagination-disabled-color:            @gray-light;
+@pagination-disabled-bg:               #fff;
+@pagination-disabled-border:           #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg:                             @pagination-bg;
+@pager-border:                         @pagination-border;
+@pager-border-radius:                  15px;
+
+@pager-hover-bg:                       @pagination-hover-bg;
+
+@pager-active-bg:                      @pagination-active-bg;
+@pager-active-color:                   @pagination-active-color;
+
+@pager-disabled-color:                 @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding:              30px;
+@jumbotron-color:                inherit;
+@jumbotron-bg:                   #f5f5f5;
+@jumbotron-heading-color:        @headings-color;
+@jumbotron-font-size:            ceil((@font-size-base * 1.5));
+@jumbotron-heading-font-size:    ceil((@font-size-base * 4.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text:             @brand-success;
+@state-success-bg:               #dff0d8;
+@state-success-border:           darken(spin(@state-success-bg, -10), 5%);
+
+@state-info-text:                @brand-info;
+@state-info-bg:                  #e1bee7;
+@state-info-border:              darken(spin(@state-info-bg, -10), 7%);
+
+@state-warning-text:             @brand-warning;
+@state-warning-bg:               #ffe0b2;
+@state-warning-border:           darken(spin(@state-warning-bg, -10), 5%);
+
+@state-danger-text:              @brand-danger;
+@state-danger-bg:                #f9bdbb;
+@state-danger-border:            darken(spin(@state-danger-bg, -10), 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width:           200px;
+//** Tooltip text color
+@tooltip-color:               #fff;
+//** Tooltip background color
+@tooltip-bg:                  #727272;
+@tooltip-opacity:             .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width:         5px;
+//** Tooltip arrow color
+@tooltip-arrow-color:         @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg:                          #fff;
+//** Popover maximum width
+@popover-max-width:                   276px;
+//** Popover border color
+@popover-border-color:                transparent;
+//** Popover fallback border color
+@popover-fallback-border-color:       transparent;
+
+//** Popover title background color
+@popover-title-bg:                    darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width:                 10px;
+//** Popover arrow color
+@popover-arrow-color:                 @popover-bg;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width:           (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color:           fadein(@popover-border-color, 12%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color:  darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg:            @gray-light;
+//** Primary label background color
+@label-primary-bg:            @brand-primary;
+//** Success label background color
+@label-success-bg:            @brand-success;
+//** Info label background color
+@label-info-bg:               @brand-info;
+//** Warning label background color
+@label-warning-bg:            @brand-warning;
+//** Danger label background color
+@label-danger-bg:             @brand-danger;
+
+//** Default label text color
+@label-color:                 #fff;
+//** Default text color of a linked label
+@label-link-hover-color:      #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding:         15px;
+
+//** Padding applied to the modal title
+@modal-title-padding:         15px;
+//** Modal title line-height
+@modal-title-line-height:     @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg:                             #fff;
+//** Modal content border color
+@modal-content-border-color:                   transparent;
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color:          #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg:           #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity:      .5;
+//** Modal header border color
+@modal-header-border-color:   transparent;
+//** Modal footer border color
+@modal-footer-border-color:   @modal-header-border-color;
+
+@modal-lg:                    900px;
+@modal-md:                    600px;
+@modal-sm:                    300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding:               15px;
+@alert-border-radius:         @border-radius-base;
+@alert-link-font-weight:      bold;
+
+@alert-success-bg:            @state-success-bg;
+@alert-success-text:          @state-success-text;
+@alert-success-border:        @state-success-border;
+
+@alert-info-bg:               @state-info-bg;
+@alert-info-text:             @state-info-text;
+@alert-info-border:           @state-info-border;
+
+@alert-warning-bg:            @state-warning-bg;
+@alert-warning-text:          @state-warning-text;
+@alert-warning-border:        @state-warning-border;
+
+@alert-danger-bg:             @state-danger-bg;
+@alert-danger-text:           @state-danger-text;
+@alert-danger-border:         @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg:                 #f5f5f5;
+//** Progress bar text color
+@progress-bar-color:          #fff;
+//** Variable for setting rounded corners on progress bar.
+@progress-border-radius:      @border-radius-base;
+
+//** Default progress bar color
+@progress-bar-bg:             @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg:     @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg:     @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg:      @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg:        @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg:                 #fff;
+//** `.list-group-item` border color
+@list-group-border:             #ddd;
+//** List group border radius
+@list-group-border-radius:      @border-radius-base;
+
+//** Background color of single list items on hover
+@list-group-hover-bg:           #f5f5f5;
+//** Text color of active list items
+@list-group-active-color:       @component-active-color;
+//** Background color of active list items
+@list-group-active-bg:          @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border:      @list-group-active-bg;
+//** Text color for content within active list items
+@list-group-active-text-color:  lighten(@list-group-active-bg, 40%);
+
+//** Text color of disabled list items
+@list-group-disabled-color:      @gray-light;
+//** Background color of disabled list items
+@list-group-disabled-bg:         @gray-lighter;
+//** Text color for content within disabled list items
+@list-group-disabled-text-color: @list-group-disabled-color;
+
+@list-group-link-color:         #555;
+@list-group-link-hover-color:   @list-group-link-color;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg:                    #fff;
+@panel-body-padding:          15px;
+@panel-heading-padding:       10px 15px;
+@panel-footer-padding:        @panel-heading-padding;
+@panel-border-radius:         @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border:          #ddd;
+@panel-footer-bg:             #f5f5f5;
+
+@panel-default-text:          @gray-dark;
+@panel-default-border:        #ddd;
+@panel-default-heading-bg:    #f5f5f5;
+
+@panel-primary-text:          #fff;
+@panel-primary-border:        @brand-primary;
+@panel-primary-heading-bg:    @brand-primary;
+
+@panel-success-text:          #fff;
+@panel-success-border:        @state-success-border;
+@panel-success-heading-bg:    @brand-success;
+
+@panel-info-text:             #fff;
+@panel-info-border:           @state-info-border;
+@panel-info-heading-bg:       @brand-info;
+
+@panel-warning-text:          #fff;
+@panel-warning-border:        @state-warning-border;
+@panel-warning-heading-bg:    @brand-warning;
+
+@panel-danger-text:           #fff;
+@panel-danger-border:         @state-danger-border;
+@panel-danger-heading-bg:     @brand-danger;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding:           4px;
+//** Thumbnail background color
+@thumbnail-bg:                @body-bg;
+//** Thumbnail border color
+@thumbnail-border:            #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius:     @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color:     @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding:   9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg:                     #f5f5f5;
+@well-border:                 transparent;
+
+
+//== Badges
+//
+//##
+
+@badge-color:                 #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color:      #fff;
+@badge-bg:                    @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color:          @link-color;
+//** Badge background color in active nav link
+@badge-active-bg:             #fff;
+
+@badge-font-weight:           normal;
+@badge-line-height:           1;
+@badge-border-radius:         10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical:   8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg:                 #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color:              #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color:       @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator:          "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color:                      #fff;
+@carousel-control-width:                      15%;
+@carousel-control-opacity:                    .5;
+@carousel-control-font-size:                  20px;
+
+@carousel-indicator-active-bg:                #fff;
+@carousel-indicator-border-color:             #fff;
+
+@carousel-caption-color:                      #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight:           normal;
+@close-color:                 #000;
+@close-text-shadow:           none;
+
+
+//== Code
+//
+//##
+
+@code-color:                  #c7254e;
+@code-bg:                     #f9f2f4;
+
+@kbd-color:                   #fff;
+@kbd-bg:                      #333;
+
+@pre-bg:                      #f5f5f5;
+@pre-color:                   @gray-dark;
+@pre-border-color:            #ccc;
+@pre-scrollable-max-height:   340px;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+//** Text muted color
+@text-muted:                  @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color:           @gray-light;
+//** Headings small color
+@headings-small-color:        @gray-light;
+//** Blockquote small color
+@blockquote-small-color:      @gray-light;
+//** Blockquote font size
+@blockquote-font-size:        (@font-size-base * 1.25);
+//** Blockquote border color
+@blockquote-border-color:     @gray-lighter;
+//** Page header border color
+@page-header-border-color:    @gray-lighter;
+//** Width of horizontal description list titles
+@dl-horizontal-offset:        @component-offset-horizontal;
+//** Point at which .dl-horizontal becomes horizontal
+@dl-horizontal-breakpoint:    @grid-float-breakpoint;
+//** Horizontal line color.
+@hr-border:                   @gray-lighter;
\ No newline at end of file
diff --git a/public/less/admin/settings.less b/public/less/admin/settings.less
new file mode 100644
index 0000000000..b4fe92432e
--- /dev/null
+++ b/public/less/admin/settings.less
@@ -0,0 +1,35 @@
+.settings {
+	> .row {
+		margin-bottom: 30px;
+	}
+
+	.section-content {
+		border-left: 3px solid @brand-primary;
+
+		ul {
+			list-style-type: none;
+			font-size: 16px;
+			padding-left: 20px;
+		}
+	}
+
+	[data-action="upload"][type="text"] {
+		width: 95%;
+	}
+
+	.bootstrap-tagsinput {
+		width: 100%;
+		border: 0;
+		box-shadow: none;
+		padding-left: 0;
+
+		input {
+			width: 100%;
+			margin-left: 1px;
+			margin-top: 9px;
+			border-bottom: 1px dotted #ccc !important;
+			padding-bottom: 5px;
+			padding-left: 0;
+		}
+	}
+}
\ No newline at end of file
diff --git a/public/less/admin/vars.less b/public/less/admin/vars.less
new file mode 100644
index 0000000000..dda7ed1fe8
--- /dev/null
+++ b/public/less/admin/vars.less
@@ -0,0 +1,20 @@
+// system font family
+// based on those in [bootstrap@5.0.0-alpha1](https://github.com/twbs/bootstrap/blob/b531bda07cbea2e124194aefe3b8597b3ac2578e/scss/_variables.scss#L386)
+// and [wordpress admin](https://core.trac.wordpress.org/browser/trunk/src/wp-admin/css/common.css?rev=47835#L220)
+//   system-ui                           : supported by the latest browsers for this very purpose
+//   apple-system, BlinkMacSystemFont    : iOS and MacOS
+//   "Segoe UI"                          : Windows Vista, 7, 8, 10
+//   Roboto                              : Android 4.0+
+//   Oxygen-Sans                         : KDE
+//   Ubuntu                              : Ubuntu
+//   Cantarell                           : GNOME
+//   "Helvetica Neue"                    : Mac OS X
+//   Helvetica                           : backup, better looking than Arial
+//   Arial                               : backup
+//   "Noto Sans"                         : broader language support on Android
+//   sans-serif                          : whatever the browser can give us
+//   "Apple Color Emoji"                 : Emoji on iOS and MacOS
+//   "Segoe UI Emoji", "Segoe UI Symbol" : Emoji on Windows
+//   "Noto Color Emoji"                  : Emoji on Android
+@font-family-system: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+@font-family-sans-serif: @font-family-system;
diff --git a/public/less/flags.less b/public/less/flags.less
new file mode 100644
index 0000000000..0edccd7819
--- /dev/null
+++ b/public/less/flags.less
@@ -0,0 +1,45 @@
+/*
+	Flags page CSS
+	  - Originally in ACP
+	  - Now available in front-end for global mods as well
+*/
+
+.page-flags {
+	// hide the all categories li element
+	[component="flags/filters"] [component="category/dropdown"] [data-all="all"] {
+		display: none;
+	}
+}
+
+.page-manage-flags, .page-posts-flags {
+	.post-container > .row {
+		margin-bottom: 2rem;
+	}
+
+	.flag-reporters {
+		font-size: 1.2rem;
+
+		ul {
+			padding-left: 0;
+
+			li {
+				list-style-type: none;
+
+				img, .user-icon {
+					.user-icon-style(18px, 1rem);
+					margin-right: 1rem;
+				}
+			}
+		}
+	}
+
+	.flag-post-body {
+		img, .user-icon {
+			.user-icon-style(24px, 1.5rem);
+		}
+	}
+
+	[component="posts/flag/history"] .avatar {
+		margin-right: 1rem;
+	}
+}
\ No newline at end of file
diff --git a/public/less/generics.less b/public/less/generics.less
new file mode 100644
index 0000000000..ec8b0c1adf
--- /dev/null
+++ b/public/less/generics.less
@@ -0,0 +1,184 @@
+.define-if-not-set() {
+	@gray-base:              #000;
+	@gray-darker:            lighten(@gray-base, 13.5%); // #222
+	@gray-dark:              lighten(@gray-base, 20%);   // #333
+	@gray:                   lighten(@gray-base, 33.5%); // #555
+	@gray-light:             lighten(@gray-base, 46.7%); // #777
+	@gray-lighter:           lighten(@gray-base, 93.5%); // #eee
+
+	@brand-primary:         darken(#428bca, 6.5%); // #337ab7
+	@brand-success:         #5cb85c;
+	@brand-info:            #5bc0de;
+	@brand-warning:         #f0ad4e;
+	@brand-danger:          #d9534f;
+}
+
+.define-if-not-set();
+
+#move_thread_modal .category-list {
+	height: 500px;
+	overflow-y: auto;
+	overflow-x: hidden;
+}
+
+.topic-watch-dropdown {
+	.help-text {
+		margin-left: 20px;
+	}
+}
+
+.category-list {
+	padding: 0;
+
+	li {
+		.inline-block;
+		.pointer;
+		padding: 0.5em;
+		margin: 0.25em;
+		.border-radius(3px);
+
+		&.disabled {
+			background-color: #888!important;
+			opacity: 0.5;
+		}
+	}
+}
+
+.user-list {
+	padding-left: 2rem;
+	padding-top: 1rem;
+
+	li {
+		.pointer;
+		display: inline-block;
+		list-style-type: none;
+		padding: 0.5rem 1rem;
+
+		&:hover {
+			background: #eee;
+		}
+
+		.avatar  {
+			float: left;
+			margin-right: 1rem;
+		}
+
+		span {
+			vertical-align: middle;
+			display: inline-block;
+		}
+	}
+}
+
+.user-icon {
+	display: inline-block;
+	text-align: center;
+	color: @gray-lighter;
+	font-weight: normal;
+	vertical-align: middle;
+	overflow: hidden;	/* stops alt text from overflowing past boundaries if image does not load */
+	white-space: nowrap;
+
+	&:before {
+		content: '';
+		display: inline-block;
+		height: 100%;
+		vertical-align: middle;
+	}
+}
+
+.avatar {
+	/* Contains the user icon class as a mixin, so there's no need to include that in the template */
+	.user-icon;
+
+	&.avatar-xs {
+		width: 16px;
+		height: 16px;
+		.user-icon-style(16px, 1rem);
+	}
+
+	&.avatar-sm {
+		width: 24px;
+		height: 24px;
+		.user-icon-style(24px, 1.5rem);
+	}
+
+	&.avatar-sm2x {
+		width: 48px;
+		height: 48px;
+		.user-icon-style(48px, 1.5rem);
+	}
+
+	&.avatar-md {
+		width: 32px;
+		height: 32px;
+		.user-icon-style(32px, 1.5rem);
+	}
+
+	&.avatar-lg {
+		width: 64px;
+		height: 64px;
+		.user-icon-style(64px, 4rem);
+	}
+
+	&.avatar-xl {
+		width: 128px;
+		height: 128px;
+		.user-icon-style(128px, 7.5rem);
+	}
+
+	&.avatar-rounded {
+		border-radius: 50%;
+	}
+}
+
+.ban-modal {
+	.form-inline, .form-group {
+		width: 100%;
+	}
+
+	.units {
+		line-height: 5rem;
+	}
+}
+
+.admin .ban-modal .units {
+	line-height: 1.846;
+}
+
+#crop-picture-modal {
+	#cropped-image {
+		max-width: 100%;
+	}
+
+	.cropper-container.cropper-bg {
+		max-width: 100%;
+	}
+}
+
+.necro-post {
+	color: rgba(127,127,127,.5);
+	font-size: 1.5em;
+	margin-bottom: 20px;
+	text-align: center;
+	text-transform: uppercase;
+}
+
+.timeline-event {
+	display: flex;
+	align-items: center;
+	justify-content: center;
+
+	.timeline-badge {
+		padding: 1rem;
+	}
+}
+
+.imagedrop {
+	position: absolute;
+	text-align: center;
+	font-size: 24px;
+	color: @gray-light;
+	width: 100%;
+	display: none;
+}
\ No newline at end of file
diff --git a/public/less/global.less b/public/less/global.less
new file mode 100644
index 0000000000..7fb8234708
--- /dev/null
+++ b/public/less/global.less
@@ -0,0 +1,8 @@
+/*
+	This stylesheet is applied to all themes and all pages.
+	They can be overridden by themes, though their presence (or initial settings) may be depended upon by
+	client-side logic in core.
+
+	==========
+*/
+
diff --git a/public/less/install.less b/public/less/install.less
new file mode 100644
index 0000000000..ffd98e9f30
--- /dev/null
+++ b/public/less/install.less
@@ -0,0 +1,101 @@
+@import "./admin/vars";
+
+.working {
+  width: 24px;
+  height: 24px;
+
+  position: relative;
+	display: inline-block;
+	vertical-align: bottom;
+	
+	&::before, &::after {
+		content: ' ';
+
+		width: 100%;
+		height: 100%;
+		border-radius: 50%;
+		background-color: #fff;
+		opacity: 0.6;
+		position: absolute;
+		top: 0;
+		left: 0;
+		
+		-webkit-animation: sk-bounce 2.0s infinite ease-in-out;
+		animation: sk-bounce 2.0s infinite ease-in-out;
+	}
+	
+	&::after {
+		-webkit-animation-delay: -1.0s;
+		animation-delay: -1.0s;
+	}
+}
+
+@-webkit-keyframes sk-bounce {
+  0%, 100% { -webkit-transform: scale(0.0) }
+  50% { -webkit-transform: scale(1.0) }
+}
+
+@keyframes sk-bounce {
+  0%, 100% { 
+    transform: scale(0.0);
+    -webkit-transform: scale(0.0);
+  } 50% { 
+    transform: scale(1.0);
+    -webkit-transform: scale(1.0);
+  }
+}
+
+.btn, .form-control, .navbar {
+	border-radius: 0;
+}
+
+.container {
+	font-size: 18px;
+	margin-bottom: 100px;
+}
+
+body, small, p, div {
+	font-family: @font-family-sans-serif;
+}
+
+.input-row {
+	margin-bottom: 20px;
+
+	.form-control {
+		margin-bottom: 5px;
+	}
+
+	.help-text {
+		pointer-events: none;
+		line-height: 20px;
+		color: #888;
+		font-size: 85%;
+		display: none;
+	}
+
+	.input-field {
+		border-right: 5px solid #FFF;
+	}
+
+	&.active {
+		.input-field {
+			border-right-color: #38B44A;
+			padding-right: 20px;
+		}
+
+		.help-text {
+			display: block;
+		}
+	}
+
+	&.error {
+		.input-field {
+			border-right-color: #BF3E11;
+			padding-right: 20px;
+		}
+
+		.help-text {
+			display: block;
+		}
+	}
+}
\ No newline at end of file
diff --git a/public/less/jquery-ui.less b/public/less/jquery-ui.less
new file mode 100644
index 0000000000..1f6e7ae03c
--- /dev/null
+++ b/public/less/jquery-ui.less
@@ -0,0 +1,10 @@
+@import (inline) 'jquery-ui/themes/base/core.css';
+@import (inline) 'jquery-ui/themes/base/menu.css';
+@import (inline) 'jquery-ui/themes/base/button.css';
+@import (inline) 'jquery-ui/themes/base/datepicker.css';
+@import (inline) 'jquery-ui/themes/base/autocomplete.css';
+@import (inline) 'jquery-ui/themes/base/resizable.css';
+@import (inline) 'jquery-ui/themes/base/selectable.css';
+@import (inline) 'jquery-ui/themes/base/draggable.css';
+@import (inline) 'jquery-ui/themes/base/sortable.css';
+@import (inline) 'jquery-ui/themes/base/theme.css';
diff --git a/public/less/mixins.less b/public/less/mixins.less
new file mode 100644
index 0000000000..04259bb089
--- /dev/null
+++ b/public/less/mixins.less
@@ -0,0 +1,80 @@
+.no-select {
+	-webkit-touch-callout: none;
+	-webkit-user-select: none;
+	-khtml-user-select: none;
+	-moz-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+}
+
+.pointer {
+	cursor: pointer;
+	*cursor: hand;
+}
+
+.inline-block {
+	display: inline-block;
+	*display: inline;
+	zoom: 1;
+}
+
+.clear {
+	clear: both;
+}
+
+.zebra {
+	&:nth-child(even) {
+		background: rgba(191,191,191,0.2);
+	}
+
+	&:nth-child(odd) {
+		background: rgba(223,223,223,0.2);
+	}
+}
+
+.border-radius (@radius: 5px) {
+	-webkit-border-radius: @radius;
+	-moz-border-radius: @radius;
+	-ms-border-radius: @radius;
+	-o-border-radius: @radius;
+	border-radius: @radius;
+}
+
+.text-ellipsis {
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+.fix-lists {
+	ul {
+		> li {
+			list-style-type: disc;
+
+			ul > li {
+				list-style-type: circle;
+
+				ul > li {
+					list-style-type: square;
+				}
+			}
+		}
+	}
+
+	> ul, > ol {
+		margin-bottom: 10px;
+	}
+}
+
+.user-icon-style(@size: 32px, @font-size: 1.5rem, @border-radius: inherit) {
+	border-radius: @border-radius;
+	width: @size;
+	height: @size;
+	line-height: @size;
+	font-size: @font-size;
+}
+
+.box-shadow(@shadow) {
+	-webkit-box-shadow: @shadow;
+	box-shadow: @shadow;
+}
\ No newline at end of file
diff --git a/public/less/modals.less b/public/less/modals.less
new file mode 100644
index 0000000000..8bf0da402e
--- /dev/null
+++ b/public/less/modals.less
@@ -0,0 +1,18 @@
+.tool-modal {
+	position: fixed;
+	bottom: 10%;
+	right: 2rem;
+	z-index: 1;
+}
+
+@media screen and (min-width: @screen-sm-min) {
+	.tool-modal {
+		max-width: 33%;
+	}
+}
+
+.topic-thumbs-modal {
+	img.media-object {
+		max-width: 20rem;
+	}
+}
\ No newline at end of file
diff --git a/public/openapi/components/responses/400.yaml b/public/openapi/components/responses/400.yaml
new file mode 100644
index 0000000000..19ad24825f
--- /dev/null
+++ b/public/openapi/components/responses/400.yaml
@@ -0,0 +1,6 @@
+'400':
+  description: Bad Request
+  content:
+    application/json:
+      schema:
+        $ref: ../../components/schemas/Error.yaml#/Error
\ No newline at end of file
diff --git a/public/openapi/components/responses/401.yaml b/public/openapi/components/responses/401.yaml
new file mode 100644
index 0000000000..982e0b5ce3
--- /dev/null
+++ b/public/openapi/components/responses/401.yaml
@@ -0,0 +1,6 @@
+'401':
+  description: Not Authorized
+  content:
+    application/json:
+      schema:
+        $ref: ../../components/schemas/Error.yaml#/Error
\ No newline at end of file
diff --git a/public/openapi/components/responses/403.yaml b/public/openapi/components/responses/403.yaml
new file mode 100644
index 0000000000..3fdf549726
--- /dev/null
+++ b/public/openapi/components/responses/403.yaml
@@ -0,0 +1,6 @@
+'403':
+  description: Forbidden
+  content:
+    application/json:
+      schema:
+        $ref: ../../components/schemas/Error.yaml#/Error
\ No newline at end of file
diff --git a/public/openapi/components/responses/404.yaml b/public/openapi/components/responses/404.yaml
new file mode 100644
index 0000000000..f5a8a84ede
--- /dev/null
+++ b/public/openapi/components/responses/404.yaml
@@ -0,0 +1,6 @@
+'404':
+  description: Not Found
+  content:
+    application/json:
+      schema:
+        $ref: ../../components/schemas/Error.yaml#/Error
\ No newline at end of file
diff --git a/public/openapi/components/responses/426.yaml b/public/openapi/components/responses/426.yaml
new file mode 100644
index 0000000000..534da859c3
--- /dev/null
+++ b/public/openapi/components/responses/426.yaml
@@ -0,0 +1,6 @@
+'426':
+  description: Upgrade Required
+  content:
+    application/json:
+      schema:
+        $ref: ../../components/schemas/Error.yaml#/Error
\ No newline at end of file
diff --git a/public/openapi/components/responses/500.yaml b/public/openapi/components/responses/500.yaml
new file mode 100644
index 0000000000..950a79f707
--- /dev/null
+++ b/public/openapi/components/responses/500.yaml
@@ -0,0 +1,6 @@
+'500':
+  description: Internal Server Error
+  content:
+    application/json:
+      schema:
+        $ref: ../../components/schemas/Error.yaml#/Error
\ No newline at end of file
diff --git a/public/openapi/components/schemas/Breadcrumbs.yaml b/public/openapi/components/schemas/Breadcrumbs.yaml
new file mode 100644
index 0000000000..986e3544cb
--- /dev/null
+++ b/public/openapi/components/schemas/Breadcrumbs.yaml
@@ -0,0 +1,16 @@
+Breadcrumbs:
+  type: object
+  properties:
+    breadcrumbs:
+      type: array
+      items:
+        type: object
+        properties:
+          text:
+            type: string
+          url:
+            type: string
+          cid:
+            type: number
+        required:
+          - text
\ No newline at end of file
diff --git a/public/openapi/components/schemas/CategoryObject.yaml b/public/openapi/components/schemas/CategoryObject.yaml
new file mode 100644
index 0000000000..4d6cb0ca4e
--- /dev/null
+++ b/public/openapi/components/schemas/CategoryObject.yaml
@@ -0,0 +1,88 @@
+CategoryObject:
+  allOf:
+    - type: object
+      properties:
+        cid:
+          type: number
+          description: A category identifier assigned upon category creation (this value cannot be changed)
+        name:
+          type: string
+          description: The category's name/title
+        description:
+          type: string
+          description: A variable-length description of the category (usually displayed underneath the category name)
+        descriptionParsed:
+          type: string
+          description: A variable-length description of the category (usually displayed underneath the category name). Unlike `description`, this value here will have been run through any parsers installed on the forum (e.g. Markdown)
+        icon:
+          type: string
+          description: A FontAwesome icon string
+          example: fa-comments-o
+        bgColor:
+          type: string
+          description: Theme-related, a six-character hexadecimal string representing the background colour of the category
+        color:
+          type: string
+          description: Theme-related, a six-character hexadecimal string representing the foreground/text colour of the category
+        slug:
+          type: string
+          description: An URL-safe variant of the category title. This value is automatically generated.
+          readOnly: true
+        parentCid:
+          type: number
+          description: The category identifier for the category that is the immediate ancestor of the current category
+        topic_count:
+          type: number
+          description: The number of topics in the category
+        post_count:
+          type: number
+          description: The number of posts in the category
+        disabled:
+          type: number
+          description: Whether or not this category is disabled.
+        order:
+          type: number
+          description: A number representing the category's place in the hierarchy
+        link:
+          type: string
+          description: If set, attempting to access the forum will go to this external link instead (theme-specific)
+        numRecentReplies:
+          type: number
+          description: The number of posts to render in the API response (this is mostly used at the theme level)
+        class:
+          type: string
+          description: Values that are appended to the `class` attribute of the category's parent/root element
+        imageClass:
+          type: string
+          enum: [auto, cover, contain]
+          description: The `background-position` of the category background image, if one is set
+        isSection:
+          type: number
+        minTags:
+          type: number
+          description: Minimum tags per topic in this category
+        maxTags:
+          type: number
+          description: Maximum tags per topic in this category
+        postQueue:
+          type: number
+        totalPostCount:
+          type: number
+          description: The number of posts in the category
+        totalTopicCount:
+          type: number
+          description: The number of topics in the category
+        subCategoriesPerPage:
+          type: number
+          description: The number of subcategories to display on the categories and category page
+    - type: object
+      description: Optional properties that may or may not be present (except for `cid`, which is always present, and is only here as a hack to pass validation)
+      properties:
+        cid:
+          type: number
+          description: A category identifier
+        backgroundImage:
+          type: string
+          description: Relative URL to the category's background image
+      required:
+        - cid
\ No newline at end of file
diff --git a/public/openapi/components/schemas/Chats.yaml b/public/openapi/components/schemas/Chats.yaml
new file mode 100644
index 0000000000..91c41f777b
--- /dev/null
+++ b/public/openapi/components/schemas/Chats.yaml
@@ -0,0 +1,191 @@
+RoomObject:
+  type: object
+  properties:
+    owner:
+      type: number
+      description: the uid of the chat room owner (usually the user who created the room initially)
+    roomId:
+      type: number
+      description: unique identifier for the chat room
+    roomName:
+      type: string
+    groupChat:
+      type: boolean
+      description: whether the chat room is a group chat or not
+MessageObject:
+  type: object
+  properties:
+    content:
+      type: string
+      description: A chat message's content, parsed like a post (so probably outputs html)
+    timestamp:
+      type: number
+    fromuid:
+      type: number
+    roomId:
+      type: number
+    deleted:
+      type: boolean
+    system:
+      type: boolean
+    edited:
+      type: number
+    timestampISO:
+      type: string
+    editedISO:
+      type: string
+    messageId:
+      type: number
+    fromUser:
+      type: object
+      properties:
+        uid:
+          type: number
+          description: A user identifier
+        username:
+          type: string
+          description: A friendly name for a given user account
+          example: Dragon Fruit
+        userslug:
+          type: string
+          description: An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.)
+          example: dragon-fruit
+        picture:
+          type: string
+          nullable: true
+          description: A URL pointing to a picture to be used as the user's avatar
+          example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
+        status:
+          type: string
+          enum:
+            - online
+            - offline
+            - dnd
+            - away
+        banned:
+          type: boolean
+          description: Whether a user is banned or not
+          example: false
+        displayname:
+          type: string
+          description: This is either username or fullname depending on forum and user settings
+          example: Dragon Fruit
+        icon:text:
+          type: string
+          description: A single-letter representation of a username. This is used in the
+            auto-generated icon given to users
+            without an avatar
+        icon:bgColor:
+          type: string
+          description: A six-character hexadecimal colour code assigned to the user. This
+            value is used in conjunction with
+            `icon:text` for the user's
+            auto-generated icon
+          example: "#f44336"
+        banned_until_readable:
+          type: string
+          description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned"
+          example: Not Banned
+        deleted:
+          type: boolean
+    self:
+      type: number
+    newSet:
+      type: boolean
+    cleanedContent:
+      type: string
+RoomUserList:
+  type: object
+  properties:
+    users:
+      type: array
+      items:
+        type: object
+        properties:
+          uid:
+            type: number
+            description: A user identifier
+          username:
+            type: string
+            description: A friendly name for a given user account
+          picture:
+            nullable: true
+            type: string
+          status:
+            type: string
+          displayname:
+            type: string
+            description: This is either username or fullname depending on forum and user settings
+          icon:text:
+            type: string
+            description: A single-letter representation of a username. This is used in the
+              auto-generated icon given to users
+              without an avatar
+          icon:bgColor:
+            type: string
+            description: A six-character hexadecimal colour code assigned to the user. This
+              value is used in conjunction with
+              `icon:text` for the user's
+              auto-generated icon
+            example: "#f44336"
+          isOwner:
+            type: boolean
+          canKick:
+            type: boolean
+RoomObjectFull:
+  # Messaging.loadRoom
+  allOf:
+    - $ref: '#/RoomObject'
+    - $ref: '#/MessageObject'
+    - type: object
+      properties:
+        isOwner:
+          type: boolean
+        users:
+          type: array
+          items:
+            type: object
+            properties:
+              uid:
+                type: number
+                description: A user identifier
+              username:
+                type: string
+                description: A friendly name for a given user account
+              picture:
+                nullable: true
+                type: string
+              status:
+                type: string
+              displayname:
+                type: string
+                description: This is either username or fullname depending on forum and user settings
+              icon:text:
+                type: string
+                description: A single-letter representation of a username. This is used in the
+                  auto-generated icon given to users
+                  without an avatar
+              icon:bgColor:
+                type: string
+                description: A six-character hexadecimal colour code assigned to the user. This
+                  value is used in conjunction with
+                  `icon:text` for the user's
+                  auto-generated icon
+                example: "#f44336"
+              isOwner:
+                type: boolean
+        canReply:
+          type: boolean
+        groupChat:
+          type: boolean
+        usernames:
+          type: string
+          description: User-friendly depiction of the users within the chat room
+        maximumUsersInChatRoom:
+          type: number
+        maximumChatMessageLength:
+          type: number
+        showUserInput:
+          type: boolean
+        isAdminOrGlobalMod:
+          type: boolean
\ No newline at end of file
diff --git a/public/openapi/components/schemas/CommonProps.yaml b/public/openapi/components/schemas/CommonProps.yaml
new file mode 100644
index 0000000000..8428791791
--- /dev/null
+++ b/public/openapi/components/schemas/CommonProps.yaml
@@ -0,0 +1,81 @@
+CommonProps:
+  type: object
+  properties:
+    loggedIn:
+      type: boolean
+      description: True if user is logged in, false otherwise
+    relative_path:
+      type: string
+      description: |
+        If NodeBB is installed in a subfolder this becomes the path to the forum. For example if your forum url is
+        `example.org/community` then relative_path will be `/community`. If your forum url is `example.com` then relative path will be an empty string.
+    template:
+      type: object
+      properties:
+        name:
+          type: string
+          description: The path to the template, which acts as a unique name
+          example: admin/settings/general
+      additionalProperties:
+        description: There will be one additional property added to all routes here. It is a boolean value whose key is the path to the current template. It is used on the client-side to verify the current page inside of a conditional (e.g. `if (ajaxify.data.template.topic)` to ensure a script is run only on the topic page)
+        type: boolean
+        enum: [true]
+    url:
+      type: string
+      description: Base url of the current page, does not include query params
+    bodyClass:
+      type: string
+      description: The css class string that is appended to the body element
+    _header:
+      type: object
+      description: List of meta and link tags that are added to the head element
+      properties:
+        tags:
+          type: object
+          properties:
+            meta:
+              type: array
+              items:
+                type: object
+                properties:
+                  name:
+                    type: string
+                  content:
+                    type: string
+                  noEscape:
+                    type: boolean
+                  property:
+                    type: string
+                required:
+                  - content
+            link:
+              type: array
+              items:
+                type: object
+                properties:
+                  rel:
+                    type: string
+                  type:
+                    type: string
+                  href:
+                    type: string
+                  title:
+                    type: string
+                  sizes:
+                    type: string
+                  as:
+                    type: string
+                required:
+                  - rel
+                  - href
+    widgets:
+      type: object
+      description: Each widget area will have its own property in this object
+      additionalProperties:
+        type: array
+        description: A collection of HTML snippets that are appended to each widget area
+        items:
+          type: object
+          properties:
+            html:
+              type: string
\ No newline at end of file
diff --git a/public/openapi/components/schemas/Error.yaml b/public/openapi/components/schemas/Error.yaml
new file mode 100644
index 0000000000..f6b3a543e6
--- /dev/null
+++ b/public/openapi/components/schemas/Error.yaml
@@ -0,0 +1,12 @@
+Error:
+  type: object
+  properties:
+    status:
+      type: object
+      properties:
+        code:
+          type: string
+        message:
+          type: string
+    response:
+      type: object
\ No newline at end of file
diff --git a/public/openapi/components/schemas/FlagObject.yaml b/public/openapi/components/schemas/FlagObject.yaml
new file mode 100644
index 0000000000..388ae7f2a9
--- /dev/null
+++ b/public/openapi/components/schemas/FlagObject.yaml
@@ -0,0 +1,184 @@
+FlagObject:
+  description: The resulting object of a call to `Flags.get()`
+  allOf:
+    - type: object
+      properties:
+        state:
+          type: string
+        flagId:
+          type: number
+        type:
+          type: string
+        targetId:
+          type: number
+        targetUid:
+          type: number
+        datetime:
+          type: number
+        datetimeISO:
+          type: string
+        target_readable:
+          type: string
+        target:
+          type: object
+          properties: {}
+          additionalProperties:
+            description: Properties change depending on the target type (user, post, etc.)
+        assignee:
+          type: number
+          nullable: true
+        reports:
+          type: array
+          items:
+            type: object
+            properties:
+              value:
+                type: string
+              timestamp:
+                type: number
+              timestampISO:
+                type: string
+              reporter:
+                type: object
+                properties:
+                  username:
+                    type: string
+                    description: A friendly name for a given user account
+                  displayname:
+                    type: string
+                    description: This is either username or fullname depending on forum and user settings
+                  userslug:
+                    type: string
+                    description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                      removed, etc.)
+                  picture:
+                    nullable: true
+                  reputation:
+                    type: number
+                  uid:
+                    type: number
+                    description: A user identifier
+                  icon:text:
+                    type: string
+                    description: A single-letter representation of a username. This is used in the
+                      auto-generated icon given to users without an
+                      avatar
+                  icon:bgColor:
+                    type: string
+                    description: A six-character hexadecimal colour code assigned to the user. This
+                      value is used in conjunction with `icon:text` for
+                      the user's auto-generated icon
+                    example: "#f44336"
+    - $ref: '#/FlagHistoryObject'
+    - $ref: '#/FlagNotesObject'
+FlagHistoryObject:
+  type: object
+  properties:
+    history:
+      type: array
+      nullable: true
+      items:
+        type: object
+        properties:
+          uid:
+            type: number
+            description: A user identifier
+          fields:
+            type: object
+            additionalProperties: {}
+          meta:
+            type: array
+            items:
+              type: object
+              properties:
+                key:
+                  type: string
+                value:
+                  type: string
+                labelClass:
+                  type: string
+                  enum: ['default', 'primary', 'success', 'info', 'danger']
+              required:
+                - key
+          datetime:
+            type: number
+          datetimeISO:
+            type: string
+          user:
+            type: object
+            properties:
+              username:
+                type: string
+                description: A friendly name for a given user account
+              displayname:
+                type: string
+                description: This is either username or fullname depending on forum and user settings
+              userslug:
+                type: string
+                description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                  removed, etc.)
+              picture:
+                nullable: true
+              uid:
+                type: number
+                description: A user identifier
+              icon:text:
+                type: string
+                description: A single-letter representation of a username. This is used in the
+                  auto-generated icon given to users without
+                  an avatar
+              icon:bgColor:
+                type: string
+                description: A six-character hexadecimal colour code assigned to the user. This
+                  value is used in conjunction with
+                  `icon:text` for the user's auto-generated
+                  icon
+                example: "#f44336"
+        required:
+          - uid
+          - datetime
+          - datetimeISO
+          - user
+FlagNotesObject:
+  type: object
+  properties:
+    notes:
+      type: array
+      items:
+        type: object
+        properties:
+          uid:
+            type: number
+          content:
+            type: string
+          datetime:
+            type: number
+          datetimeISO:
+            type: string
+          user:
+            type: object
+            properties:
+              username:
+                type: string
+                description: A friendly name for a given user account
+              userslug:
+                type: string
+                description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                  removed, etc.)
+              picture:
+                type: string
+              uid:
+                type: number
+                description: A user identifier
+              icon:text:
+                type: string
+                description: A single-letter representation of a username. This is used in the
+                  auto-generated icon given to users without
+                  an avatar
+              icon:bgColor:
+                type: string
+                description: A six-character hexadecimal colour code assigned to the user. This
+                  value is used in conjunction with
+                  `icon:text` for the user's auto-generated
+                  icon
+                example: "#f44336"
\ No newline at end of file
diff --git a/public/openapi/components/schemas/GroupObject.yaml b/public/openapi/components/schemas/GroupObject.yaml
new file mode 100644
index 0000000000..c1b28479d5
--- /dev/null
+++ b/public/openapi/components/schemas/GroupObject.yaml
@@ -0,0 +1,153 @@
+GroupFullObject:
+  type: object
+  description: The response from an internal call to `Groups.get()`
+  properties:
+    name:
+      type: string
+      description: The group name
+    slug:
+      type: string
+      description: URL-safe slug of the group name
+    createtime:
+      type: number
+      description: UNIX timestamp of the group's creation
+    userTitle:
+      type: number
+      description: Label text for the user badge
+    userTitleEscaped:
+      type: number
+      description: Same as userTitle but with translation tokens escaped, used to display raw userTitle in group management
+    userTitleEnabled:
+      type: number
+    description:
+      type: string
+      description: The group description
+    memberCount:
+      type: number
+    hidden:
+      type: number
+    system:
+      type: number
+    private:
+      type: number
+    disableJoinRequests:
+      type: number
+    disableLeave:
+      type: number
+    nameEncoded:
+      type: string
+    displayName:
+      type: string
+      description: A custom override of the group's name, a friendly name
+    labelColor:
+      type: string
+      description: A six-character hexadecimal colour code
+    textColor:
+      type: string
+      description: A six-character hexadecimal colour code
+    memberPostCids:
+      type: string
+    memberPostCidsArray:
+      type: array
+      items:
+        type: number
+        example: [1, 2, 3]
+    icon:
+      type: string
+      description: A FontAwesome icon string
+    createtimeISO:
+      type: string
+      description: "`createtime` rendered as an ISO 8601 format"
+    cover:thumb:url:
+      type: string
+    cover:url:
+      type: string
+    cover:position:
+      type: string
+    descriptionParsed:
+      type: string
+    members:
+      type: array
+      items:
+        $ref: UserObject.yaml#/UserObjectSlim
+    membersNextStart:
+      type: number
+    pending:
+      type: array
+    invited:
+      type: array
+    isMember:
+      type: boolean
+    isPending:
+      type: boolean
+    isInvited:
+      type: boolean
+    isOwner:
+      type: boolean
+  nullable: true
+GroupDataObject:
+  type: object
+  description: The response from an internal call to `Groups.getGroupsFields(, [])` with **explicitly** no fields passed in
+  properties:
+    name:
+      type: string
+      description: The group name
+    slug:
+      type: string
+      description: URL-safe slug of the group name
+    createtime:
+      type: number
+      description: UNIX timestamp of the group's creation
+    userTitle:
+      type: number
+      description: Label text for the user badge
+    userTitleEscaped:
+      type: number
+      description: Same as userTitle but with translation tokens escaped, used to display raw userTitle in group management
+    userTitleEnabled:
+      type: number
+    description:
+      type: string
+      description: The group description
+    memberCount:
+      type: number
+    hidden:
+      type: number
+    system:
+      type: number
+    private:
+      type: number
+    disableJoinRequests:
+      type: number
+    disableLeave:
+      type: number
+    cover:url:
+      type: string
+    cover:thumb:url:
+      type: string
+    nameEncoded:
+      type: string
+    displayName:
+      type: string
+      description: A custom override of the group's name, a friendly name
+    labelColor:
+      type: string
+      description: A six-character hexadecimal colour code
+    textColor:
+      type: string
+      description: A six-character hexadecimal colour code
+    icon:
+      type: string
+      description: A FontAwesome icon string
+    createtimeISO:
+      type: string
+      description: "`createtime` rendered as an ISO 8601 format"
+    cover:position:
+      type: string
+    memberPostCids:
+      type: string
+    memberPostCidsArray:
+      type: array
+      items:
+        type: number
+        example: [1, 2, 3]
\ No newline at end of file
diff --git a/public/openapi/components/schemas/Pagination.yaml b/public/openapi/components/schemas/Pagination.yaml
new file mode 100644
index 0000000000..290eb95971
--- /dev/null
+++ b/public/openapi/components/schemas/Pagination.yaml
@@ -0,0 +1,64 @@
+Pagination:
+  type: object
+  properties:
+    pagination:
+      type: object
+      properties:
+        prev:
+          type: object
+          properties:
+            page:
+              type: number
+            active:
+              type: boolean
+        next:
+          type: object
+          properties:
+            page:
+              type: number
+            active:
+              type: boolean
+        first:
+          type: object
+          properties:
+            page:
+              type: number
+            active:
+              type: boolean
+        last:
+          type: object
+          properties:
+            page:
+              type: number
+            active:
+              type: boolean
+        rel:
+          type: array
+          description: A collection of objects used to build the link tags pointing to adjacent pages, if any.
+          items:
+            type: object
+            properties:
+              rel:
+                type: string
+                enum: [prev, next]
+              href:
+                type: string
+                description: A query string that points to the previous or next page
+        pages:
+          type: array
+          items:
+            type: object
+            properties:
+              page:
+                type: number
+                description: The current page
+              active:
+                type: boolean
+                description: If the page noted in this array is the current page
+              qs:
+                type: string
+                description: A query string that points to the page noted in this array
+        currentPage:
+          type: number
+        pageCount:
+          type: number
\ No newline at end of file
diff --git a/public/openapi/components/schemas/PostObject.yaml b/public/openapi/components/schemas/PostObject.yaml
new file mode 100644
index 0000000000..ea91579cc6
--- /dev/null
+++ b/public/openapi/components/schemas/PostObject.yaml
@@ -0,0 +1,142 @@
+PostObject:
+  description: A single post in the array returned from `Posts.getPostSummaryByPids`
+  type: object
+  properties:
+    pid:
+      type: number
+    tid:
+      type: number
+      description: A topic identifier
+    content:
+      type: string
+    uid:
+      type: number
+      description: A user identifier
+    timestamp:
+      type: number
+    deleted:
+      type: boolean
+    upvotes:
+      type: number
+    downvotes:
+      type: number
+    votes:
+      type: number
+    timestampISO:
+      type: string
+      description: An ISO 8601 formatted date string (complementing `timestamp`)
+    user:
+      type: object
+      properties:
+        uid:
+          type: number
+          description: A user identifier
+        username:
+          type: string
+          description: A friendly name for a given user account
+        displayname:
+          type: string
+          description: This is either username or fullname depending on forum and user settings
+        userslug:
+          type: string
+          description: An URL-safe variant of the username (i.e. lower-cased, spaces
+            removed, etc.)
+        picture:
+          type: string
+          nullable: true
+        status:
+          type: string
+        icon:text:
+          type: string
+          description: A single-letter representation of a username. This is used in the
+            auto-generated icon given to users without
+            an avatar
+        icon:bgColor:
+          type: string
+          description: A six-character hexadecimal colour code assigned to the user. This
+            value is used in conjunction with
+            `icon:text` for the user's auto-generated
+            icon
+          example: "#f44336"
+    topic:
+      type: object
+      properties:
+        uid:
+          type: number
+          description: A user identifier
+        tid:
+          type: number
+          description: A topic identifier
+        title:
+          type: string
+        cid:
+          type: number
+          description: A category identifier
+        slug:
+          type: string
+        deleted:
+          type: number
+        scheduled:
+          type: number
+        timestamp:
+          type: number
+        timestampISO:
+          type: string
+          description: An ISO 8601 formatted date string (complementing `timestamp`)
+        postcount:
+          type: number
+        mainPid:
+          type: number
+          description: The post id of the first post in this topic (also called the
+            "original post")
+        teaserPid:
+          type: number
+          description: The post id of the teaser (the most recent post, depending on settings)
+          nullable: true
+        titleRaw:
+          type: string
+        oldTitle:
+          type: string
+        isMainPost:
+          type: boolean
+        renamed:
+          type: boolean
+        tags:
+          type: array
+          items:
+            $ref: ../../components/schemas/TagObject.yaml#/TagObject
+      required:
+        - uid
+        - tid
+        - cid
+        - title
+        - slug
+    category:
+      type: object
+      properties:
+        cid:
+          type: number
+          description: A category identifier
+        name:
+          type: string
+        icon:
+          type: string
+        slug:
+          type: string
+        parentCid:
+          type: number
+          description: The category identifier for the category that is the immediate
+            ancestor of the current category
+        bgColor:
+          type: string
+        color:
+          type: string
+        backgroundImage:
+          nullable: true
+        imageClass:
+          nullable: true
+          type: string
+    isMainPost:
+      type: boolean
+    replies:
+      type: number
\ No newline at end of file
diff --git a/public/openapi/components/schemas/PostsObject.yaml b/public/openapi/components/schemas/PostsObject.yaml
new file mode 100644
index 0000000000..b43d965888
--- /dev/null
+++ b/public/openapi/components/schemas/PostsObject.yaml
@@ -0,0 +1,5 @@
+PostsObject:
+  description: One of the objects in the array returned from `Posts.getPostSummaryByPids`
+  type: array
+  items:
+    $ref: ./PostObject.yaml#/PostObject
\ No newline at end of file
diff --git a/public/openapi/components/schemas/SettingsObj.yaml b/public/openapi/components/schemas/SettingsObj.yaml
new file mode 100644
index 0000000000..eb2362bd63
--- /dev/null
+++ b/public/openapi/components/schemas/SettingsObj.yaml
@@ -0,0 +1,152 @@
+Settings:
+  type: object
+  properties:
+    showemail:
+      type: boolean
+      description: Show user email in profile page
+    usePagination:
+      type: boolean
+      description: Toggles between pagination (when enabled), or infinite scrolling (when disabled)
+    topicsPerPage:
+      type: number
+      description: Number of topics displayed on a category page
+    postsPerPage:
+      type: number
+      description: Number of posts displayed on a topic page
+    topicPostSort:
+      type: string
+      description: Default sorting strategy of the posts in of a topic
+    openOutgoingLinksInNewTab:
+      type: boolean
+      description: Whether to automatically open all external links in a new tab
+    dailyDigestFreq:
+      type: string
+      description: How often to receive the scheduled digest from this forum
+    showfullname:
+      type: boolean
+      description: Show user full name in profile page
+    followTopicsOnCreate:
+      type: boolean
+      description: Automatically be notified of new posts in a topic, when you create a topic
+    followTopicsOnReply:
+      type: boolean
+      description: Automatically be notified of new posts in a topic, when you reply to that topic
+    restrictChat:
+      type: boolean
+      description: Do not allow other users to start chats with you (or add you to other chat rooms)
+    topicSearchEnabled:
+      type: boolean
+      description: Enable keyword searching within topics
+    updateUrlWithPostIndex:
+      type: boolean
+      description: Update url with post index while browsing topics
+    categoryTopicSort:
+      type: string
+      description: Default sorting strategy of the topics in a category
+    userLang:
+      type: string
+      description: Override the system localised language in favour of the language defined here
+    bootswatchSkin:
+      type: string
+      description: Set a custom bootswatch skin
+    homePageRoute:
+      type: string
+      description: Override the behaviour of the home page route (`/`) to go to a specific page
+    scrollToMyPost:
+      type: boolean
+      description: Automatically center the viewport to you new post after posting
+    notificationType_new-chat:
+      type: string
+      description: Notification type for new chat messages
+    notificationType_new-group-chat:
+      type: string
+      description: Notification type for new group chat messages
+    notificationType_new-reply:
+      type: string
+      description: Notification type for new topic replies
+    notificationType_post-edit:
+      type: string
+      description: Notification type for post edits
+    sendChatNotifications:
+      nullable: true
+    sendPostNotifications:
+      nullable: true
+    notificationType_upvote:
+      type: string
+      description: Notification type for upvotes
+    notificationType_new-topic:
+      type: string
+      description: Notification type for new topics
+    notificationType_follow:
+      type: string
+      description: Notification type for another user following you
+    notificationType_group-invite:
+      type: string
+      description: Notification type for group invitations
+    notificationType_group-leave:
+      type: string
+      description: Notification type for when users leave your group
+    upvoteNotifFreq:
+      type: string
+      description: How often to notify you when your posts are upvoted
+    notificationType_mention:
+      type: string
+      description: Notification type for mentions in a post
+    acpLang:
+      type: string
+      description: Language localisation for the admin control panel
+    notificationType_new-register:
+      type: string
+      description: Notification type for new registration in queue
+    notificationType_post-queue:
+      type: string
+      description: Notification type for new post in post queue
+    notificationType_new-post-flag:
+      type: string
+      description: Notification type for post flagged
+    notificationType_new-user-flag:
+      type: string
+      description: Notification type for user flagged
+    categoryWatchState:
+      type: string
+      description: Default watch state for categories
+    notificationType_group-request-membership:
+      type: string
+      description: Notification type for group membership requests
+    uid:
+      type: number
+      description: A user identifier
+  required:
+    - showemail
+    - usePagination
+    - topicsPerPage
+    - postsPerPage
+    - topicPostSort
+    - openOutgoingLinksInNewTab
+    - dailyDigestFreq
+    - showfullname
+    - followTopicsOnCreate
+    - followTopicsOnReply
+    - restrictChat
+    - topicSearchEnabled
+    - categoryTopicSort
+    - userLang
+    - bootswatchSkin
+    - homePageRoute
+    - scrollToMyPost
+    - notificationType_new-chat
+    - notificationType_new-reply
+    - notificationType_upvote
+    - notificationType_new-topic
+    - notificationType_follow
+    - notificationType_group-invite
+    - notificationType_group-leave
+    - upvoteNotifFreq
+    - acpLang
+    - notificationType_new-register
+    - notificationType_post-queue
+    - notificationType_new-post-flag
+    - notificationType_new-user-flag
+    - categoryWatchState
+    - notificationType_group-request-membership
+    - uid
\ No newline at end of file
diff --git a/public/openapi/components/schemas/Status.yaml b/public/openapi/components/schemas/Status.yaml
new file mode 100644
index 0000000000..07d32b21de
--- /dev/null
+++ b/public/openapi/components/schemas/Status.yaml
@@ -0,0 +1,9 @@
+Status:
+  type: object
+  properties:
+    code:
+      type: string
+      example: ok
+    message:
+      type: string
+      example: OK
\ No newline at end of file
diff --git a/public/openapi/components/schemas/TagObject.yaml b/public/openapi/components/schemas/TagObject.yaml
new file mode 100644
index 0000000000..2b72c5a1c9
--- /dev/null
+++ b/public/openapi/components/schemas/TagObject.yaml
@@ -0,0 +1,19 @@
+TagObject:
+  type: object
+  properties:
+    value:
+      type: string
+      description: The tag name
+    score:
+      type: number
+      description: The number of topics containing this tag
+    valueEscaped:
+      type: string
+    color:
+      type: string
+      description: Six-character hexadecimal string (with `#` prepended)
+      example: "#ff0000"
+    bgColor:
+      type: string
+      description: Six-character hexadecimal string (with `#` prepended)
+      example: "#ff0000"
\ No newline at end of file
diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml
new file mode 100644
index 0000000000..64de19aeb8
--- /dev/null
+++ b/public/openapi/components/schemas/TopicObject.yaml
@@ -0,0 +1,281 @@
+TopicObject:
+  allOf:
+    - $ref: '#/TopicObjectSlim'
+    - type: object
+      properties:
+        lastposttime:
+          type: number
+        category:
+          type: object
+          properties:
+            cid:
+              type: number
+              description: A category identifier
+            name:
+              type: string
+            slug:
+              type: string
+            icon:
+              type: string
+            backgroundImage:
+              nullable: true
+              type: string
+            imageClass:
+              nullable: true
+              type: string
+            bgColor:
+              type: string
+            color:
+              type: string
+            disabled:
+              type: number
+        user:
+          type: object
+          properties:
+            uid:
+              type: number
+              description: A user identifier
+            username:
+              type: string
+              description: A friendly name for a given user account
+            displayname:
+              type: string
+              description: This is either username or fullname depending on forum and user settings
+            fullname:
+              type: string
+            userslug:
+              type: string
+              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                removed, etc.)
+            reputation:
+              type: number
+            postcount:
+              type: number
+            picture:
+              type: string
+              nullable: true
+            signature:
+              type: string
+              nullable: true
+            banned:
+              type: number
+            status:
+              type: string
+            icon:text:
+              type: string
+              description: A single-letter representation of a username. This is used in the
+                auto-generated icon given to users without
+                an avatar
+            icon:bgColor:
+              type: string
+              description: A six-character hexadecimal colour code assigned to the user. This
+                value is used in conjunction with
+                `icon:text` for the user's auto-generated
+                icon
+              example: "#f44336"
+            banned_until_readable:
+              type: string
+          required:
+            - uid
+            - username
+            - userslug
+            - reputation
+            - postcount
+            - picture
+            - signature
+            - banned
+            - status
+            - icon:text
+            - icon:bgColor
+            - banned_until_readable
+        teaser:
+          type: object
+          properties:
+            pid:
+              type: number
+            uid:
+              type: number
+              description: A user identifier
+            timestamp:
+              type: number
+            tid:
+              type: number
+              description: A topic identifier
+            content:
+              type: string
+            timestampISO:
+              type: string
+              description: An ISO 8601 formatted date string (complementing `timestamp`)
+            user:
+              type: object
+              properties:
+                uid:
+                  type: number
+                  description: A user identifier
+                username:
+                  type: string
+                  description: A friendly name for a given user account
+                userslug:
+                  type: string
+                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                    removed, etc.)
+                picture:
+                  nullable: true
+                  type: string
+                icon:text:
+                  type: string
+                  description: A single-letter representation of a username. This is used in the
+                    auto-generated icon given to users
+                    without an avatar
+                icon:bgColor:
+                  type: string
+                  description: A six-character hexadecimal colour code assigned to the user. This
+                    value is used in conjunction with
+                    `icon:text` for the user's
+                    auto-generated icon
+                  example: "#f44336"
+            index:
+              type: number
+          nullable: true
+        tags:
+          type: array
+          items:
+            type: object
+            properties:
+              value:
+                type: string
+              valueEscaped:
+                type: string
+              color:
+                type: string
+              bgColor:
+                type: string
+              score:
+                type: number
+        isOwner:
+          type: boolean
+        ignored:
+          type: boolean
+        unread:
+          type: boolean
+        bookmark:
+          nullable: true
+          type: number
+        unreplied:
+          type: boolean
+        icons:
+          type: array
+          items:
+            type: string
+            description: HTML injected into the theme
+    - type: object
+      description: Optional properties that may or may not be present (except for `tid`, which is always present, and is only here as a hack to pass validation)
+      properties:
+        tid:
+          type: number
+          description: A topic identifier
+        thumb:
+          type: string
+        pinExpiry:
+          type: number
+          description: A UNIX timestamp indicating when a pinned topic will no longer be pinned (i.e. the pin has expired)
+        pinExpiryISO:
+          type: string
+          description: "`pinExpiry` rendered as an ISO 8601 format"
+        index:
+          type: number
+      required:
+        - tid
+TopicObjectSlim:
+  description: The output of a call to `Topics.getTopicField`, these properties are always present no matter the fields passed in
+  allOf:
+    - type: object
+      properties:
+        tid:
+          type: number
+          description: A topic identifier
+        uid:
+          type: number
+          description: A user identifier
+        cid:
+          type: number
+          description: A category identifier
+        title:
+          type: string
+        slug:
+          type: string
+        mainPid:
+          type: number
+          description: The post id of the first post in this topic (also called the "original post")
+        postcount:
+          type: number
+        viewcount:
+          type: number
+        postercount:
+          type: number
+        scheduled:
+          type: number
+        deleted:
+          type: number
+        deleterUid:
+          type: number
+        titleRaw:
+          type: string
+        locked:
+          type: number
+        pinned:
+          type: number
+          description: Whether or not this particular topic is pinned to the top of the
+            category
+        timestamp:
+          type: number
+        timestampISO:
+          type: string
+          description: An ISO 8601 formatted date string (complementing `timestamp`)
+        lastposttime:
+          type: number
+        lastposttimeISO:
+          type: string
+          description: An ISO 8601 formatted date string (complementing `lastposttime`)
+        pinExpiry:
+          type: number
+          description: A UNIX timestamp indicating when a pinned topic will no longer be pinned (i.e. the pin has expired)
+        pinExpiryISO:
+          type: string
+          description: "`pinExpiry` rendered as an ISO 8601 format"
+        upvotes:
+          type: number
+        downvotes:
+          type: number
+        votes:
+          type: number
+        teaserPid:
+          oneOf:
+            - type: number
+            - type: string
+          nullable: true
+        thumbs:
+          type: array
+          items:
+            type: object
+            properties:
+              id:
+                type: number
+                description: The topic id
+              name:
+                type: string
+                description: The topic thumbnail filename
+              url:
+                type: string
+                description: Relative path to the topic thumbnail
+    - type: object
+      description: Optional properties that may or may not be present (except for `tid`, which is always present, and is only here as a hack to pass validation)
+      properties:
+        tid:
+          type: number
+          description: A topic identifier
+        numThumbs:
+          type: number
+          description: The number of thumbnails associated with this topic
+      required:
+        - tid
\ No newline at end of file
diff --git a/public/openapi/components/schemas/UserObj.yaml b/public/openapi/components/schemas/UserObj.yaml
new file mode 100644
index 0000000000..fc2837b2cb
--- /dev/null
+++ b/public/openapi/components/schemas/UserObj.yaml
@@ -0,0 +1,123 @@
+UserObj:
+  properties:
+    uid:
+      type: number
+      example: 1
+    username:
+      type: string
+      example: Dragon Fruit
+    userslug:
+      type: string
+      example: dragon-fruit
+    email:
+      type: string
+      example: dragonfruit@example.org
+    'email:confirmed':
+      type: number
+      example: 1
+    joindate:
+      type: number
+      example: 1585337827953
+    lastonline:
+      type: number
+      example: 1585337827953
+    picture:
+      type: string
+      example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
+    fullname:
+      type: string
+      example: Mr. Dragon Fruit Jr.
+    displayname:
+      type: string
+      description: This is either username or fullname depending on forum and user settings
+      example: Dragon Fruit
+    location:
+      type: string
+      example: 'Toronto, Canada'
+    birthday:
+      type: string
+      description: A birthdate given in an ISO format parseable by the Date object
+      example: 03/27/2020
+    website:
+      type: string
+      example: 'https://example.org'
+    aboutme:
+      type: string
+      example: |
+        This is a paragraph all about how my life got twist-turned upside-down
+        and I'd like to take a minute and sit right here,
+        to tell you all about how I because the administrator of NodeBB
+    signature:
+      type: string
+      example: |
+        This is an example signature
+        It can span multiple lines.
+    uploadedpicture:
+      type: string
+      example: /assets/profile/1-profileimg.png
+      description: 'In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.'
+    profileviews:
+      type: number
+      example: 1000
+    reputation:
+      type: number
+      example: 100
+    postcount:
+      type: number
+      example: 1000
+    topiccount:
+      type: number
+      example: 50
+    lastposttime:
+      type: number
+      example: 1585337827953
+    banned:
+      type: number
+      example: 0
+    'banned:expire':
+      type: number
+      example: 1585337827953
+    status:
+      type: string
+      example: online
+    flags:
+      type: number
+      example: 0
+    followercount:
+      type: number
+      example: 2
+    followingcount:
+      type: number
+      example: 5
+    'cover:url':
+      type: string
+      example: /assets/profile/1-cover.png
+    'cover:position':
+      type: string
+      example: 50.0301% 19.2464%
+    groupTitle:
+      type: string
+      example: '["administrators","Staff"]'
+    groupTitleArray:
+      type: array
+      example:
+        - administrators
+        - Staff
+    'icon:text':
+      type: string
+      example: D
+    'icon:bgColor':
+      type: string
+      example: '#9c27b0'
+    joindateISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    lastonlineISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    banned_until:
+      type: number
+      example: 0
+    banned_until_readable:
+      type: string
+      example: Not Banned
\ No newline at end of file
diff --git a/public/openapi/components/schemas/UserObject.yaml b/public/openapi/components/schemas/UserObject.yaml
new file mode 100644
index 0000000000..979e5db13c
--- /dev/null
+++ b/public/openapi/components/schemas/UserObject.yaml
@@ -0,0 +1,677 @@
+UserObject:
+  type: object
+  properties:
+    uid:
+      type: number
+      description: A user identifier
+      example: 1
+    username:
+      type: string
+      description: A friendly name for a given user account
+      example: Dragon Fruit
+    userslug:
+      type: string
+      description: An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.)
+      example: dragon-fruit
+    email:
+      type: string
+      description: Email address associated with the user account
+      example: dragonfruit@example.org
+    'email:confirmed':
+      type: number
+      description: Whether the user has confirmed their email address or not
+      example: 1
+    joindate:
+      type: number
+      description: A UNIX timestamp representing the moment the user's account was created
+      example: 1585337827953
+    accounttype:
+      type: string
+      description: Type of the user account
+      example: student
+    lastonline:
+      type: number
+      description: A UNIX timestamp representing the moment the user was last recorded online on this site
+      example: 1585337827953
+    picture:
+      type: string
+      description: A URL pointing to a picture to be used as the user's avatar
+      example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
+      nullable: true
+    fullname:
+      type: string
+      example: Mr. Dragon Fruit Jr.
+    displayname:
+      type: string
+      description: This is either username or fullname depending on forum and user settings
+      example: Dragon Fruit
+    location:
+      type: string
+      example: 'Toronto, Canada'
+      nullable: true
+    birthday:
+      type: string
+      description: A birthdate given in an ISO format parseable by the Date object
+      example: 03/27/2020
+      nullable: true
+    website:
+      type: string
+      example: 'https://example.org'
+      nullable: true
+    aboutme:
+      type: string
+      example: |
+        This is a paragraph all about how my life got twist-turned upside-down
+        and I'd like to take a minute and sit right here,
+        to tell you all about how I became the administrator of NodeBB
+      nullable: true
+    signature:
+      type: string
+      example: |
+        This is an example signature
+        It can span multiple lines.
+      nullable: true
+    uploadedpicture:
+      type: string
+      example: /assets/profile/1-profileimg.png
+      description: 'In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.'
+      nullable: true
+    profileviews:
+      type: number
+      description: The number of times this user's profile has been viewed
+      example: 1000
+    reputation:
+      type: number
+      description: The user's reputation score on the forum. Out-of-the-box, users gain/lose reputation points based on upvotes/downvotes, though plugins can alter the logic and criterion for awarding reputation points
+      example: 100
+    postcount:
+      type: number
+      example: 1000
+    topiccount:
+      type: number
+      example: 50
+    lastposttime:
+      type: number
+      description: A UNIX timestamp representing the moment the user posted last
+      example: 1585337827953
+    banned:
+      type: number
+      description: A Boolean representing whether a user is banned or not
+      example: 0
+    'banned:expire':
+      type: number
+      description: A UNIX timestamp representing the moment the ban will be lifted
+      example: 1585337827953
+    status:
+      type: string
+      enum:
+        - online
+        - offline
+        - dnd
+        - away
+      example: online
+    flags:
+      type: number
+      example: 0
+      nullable: true
+    followerCount:
+      type: number
+      example: 2
+    followingCount:
+      type: number
+      example: 5
+    'cover:url':
+      type: string
+      example: /assets/profile/1-cover.png
+      nullable: true
+    'cover:position':
+      type: string
+      example: 50.0301% 19.2464%
+      nullable: true
+    groupTitle:
+      type: string
+      example: '["administrators","Staff"]'
+      nullable: true
+    groupTitleArray:
+      type: array
+      example:
+        - administrators
+        - Staff
+    'icon:text':
+      type: string
+      description: A single-letter representation of a username. This is used in the auto-generated icon given to users without an avatar
+      example: D
+    'icon:bgColor':
+      type: string
+      description: A six-character hexadecimal colour code assigned to the user. This value is used in conjunction with `icon:text` for the user's auto-generated icon
+      example: '#9c27b0'
+    joindateISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    lastonlineISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    banned_until:
+      type: number
+      description: A UNIX timestamp representing the moment a ban will be lifted
+      example: 0
+    banned_until_readable:
+      type: string
+      description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned"
+      example: Not Banned
+  required:
+    - uid
+    - username
+    - userslug
+    - 'email:confirmed'
+    - joindate
+    - lastonline
+    - picture
+    - location
+    - birthday
+    - website
+    - aboutme
+    - signature
+    - uploadedpicture
+    - profileviews
+    - reputation
+    - postcount
+    - topiccount
+    - lastposttime
+    - banned
+    - 'banned:expire'
+    - status
+    - enum
+    - flags
+    - followerCount
+    - followingCount
+    - 'cover:url'
+    - 'cover:position'
+    - groupTitle
+    - groupTitleArray
+    - example
+    - 'icon:text'
+    - 'icon:bgColor'
+    - joindateISO
+    - lastonlineISO
+    - banned_until
+    - banned_until_readable
+UserObjectFull:
+  # accountHelpers.getUserDataByUserSlug
+  type: object
+  properties:
+    uid:
+      type: number
+      description: A user identifier
+      example: 1
+    username:
+      type: string
+      description: A friendly name for a given user account
+      example: Dragon Fruit
+    userslug:
+      type: string
+      description: An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.)
+      example: dragon-fruit
+    email:
+      type: string
+      description: Email address associated with the user account
+      example: dragonfruit@example.org
+    'email:confirmed':
+      type: number
+      description: Whether the user has confirmed their email address or not
+      example: 1
+    joindate:
+      type: number
+      description: A UNIX timestamp representing the moment the user's account was created
+      example: 1585337827953
+    accounttype:
+      type: string
+      description: Type of the user account
+      example: student
+    lastonline:
+      type: number
+      description: A UNIX timestamp representing the moment the user was last recorded online on this site
+      example: 1585337827953
+    picture:
+      type: string
+      description: A URL pointing to a picture to be used as the user's avatar
+      example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
+      nullable: true
+    fullname:
+      type: string
+      example: Mr. Dragon Fruit Jr.
+    displayname:
+      type: string
+      description: This is either username or fullname depending on forum and user settings
+      example: Dragon Fruit
+    location:
+      type: string
+      example: 'Toronto, Canada'
+    birthday:
+      type: string
+      description: A birthdate given in an ISO format parseable by the Date object
+      example: 03/27/2020
+    website:
+      type: string
+      example: 'https://example.org'
+    aboutme:
+      type: string
+      example: |
+        This is a paragraph all about how my life got twist-turned upside-down
+        and I'd like to take a minute and sit right here,
+        to tell you all about how I became the administrator of NodeBB
+    signature:
+      type: string
+      example: |
+        This is an example signature
+        It can span multiple lines.
+    uploadedpicture:
+      type: string
+      example: /assets/profile/1-profileimg.png
+      description: 'In almost all cases, defer to "picture" instead. Use this if you need to specifically reference the picture uploaded to the forum.'
+      nullable: true
+    profileviews:
+      type: number
+      description: The number of times this user's profile has been viewed
+      example: 1000
+    reputation:
+      type: number
+      description: The user's reputation score on the forum. Out-of-the-box, users gain/lose reputation points based on upvotes/downvotes, though plugins can alter the logic and criterion for awarding reputation points
+      example: 100
+    postcount:
+      type: number
+      example: 1000
+    topiccount:
+      type: number
+      example: 50
+    lastposttime:
+      type: number
+      description: A UNIX timestamp representing the moment the user posted last
+      example: 1585337827953
+    banned:
+      type: number
+      description: A Boolean representing whether a user is banned or not
+      example: 0
+    'banned:expire':
+      type: number
+      description: A UNIX timestamp representing the moment the ban will be lifted
+      example: 1585337827953
+    status:
+      type: string
+      enum:
+        - online
+        - offline
+        - dnd
+        - away
+      example: online
+    flags:
+      type: number
+      example: 0
+      nullable: true
+    followerCount:
+      type: number
+      example: 2
+    followingCount:
+      type: number
+      example: 5
+    'cover:url':
+      type: string
+      example: /assets/profile/1-cover.png
+      nullable: true
+    'cover:position':
+      type: string
+      example: 50.0301% 19.2464%
+      nullable: true
+    groupTitle:
+      type: string
+      example: '["administrators","Staff"]'
+    groupTitleArray:
+      type: array
+      example:
+        - administrators
+        - Staff
+    'icon:text':
+      type: string
+      description: A single-letter representation of a username. This is used in the auto-generated icon given to users without an avatar
+      example: D
+    'icon:bgColor':
+      type: string
+      description: A six-character hexadecimal colour code assigned to the user. This value is used in conjunction with `icon:text` for the user's auto-generated icon
+      example: '#9c27b0'
+    joindateISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    lastonlineISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    banned_until:
+      type: number
+      description: A UNIX timestamp representing the moment a ban will be lifted
+      example: 0
+    banned_until_readable:
+      type: string
+      description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned"
+      example: Not Banned
+    aboutmeParsed:
+      type: string
+    age:
+      type: number
+    emailClass:
+      type: string
+    ips:
+      type: array
+      items:
+        type: string
+    moderationNote:
+      type: string
+    counts:
+      type: object
+      properties:
+        best:
+          type: number
+        blocks:
+          type: number
+        bookmarks:
+          type: number
+        categoriesWatched:
+          type: number
+        downvoted:
+          type: number
+        followers:
+          type: number
+        following:
+          type: number
+        groups:
+          type: number
+        ignored:
+          type: number
+        posts:
+          type: number
+        topics:
+          type: number
+        uploaded:
+          type: number
+        upvoted:
+          type: number
+        watched:
+          type: number
+    isBlocked:
+      type: boolean
+    blocksCount:
+      type: number
+    yourid:
+      type: number
+    theirid:
+      type: number
+    isTargetAdmin:
+      type: boolean
+    isAdmin:
+      type: boolean
+    isGlobalModerator:
+      type: boolean
+    isModerator:
+      type: boolean
+    isAdminOrGlobalModerator:
+      type: boolean
+    isAdminOrGlobalModeratorOrModerator:
+      type: boolean
+    isSelfOrAdminOrGlobalModerator:
+      type: boolean
+    canEdit:
+      type: boolean
+    canBan:
+      type: boolean
+    canFlag:
+      type: boolean
+    canChangePassword:
+      type: boolean
+    isSelf:
+      type: boolean
+    isFollowing:
+      type: boolean
+    hasPrivateChat:
+      type: number
+    showHidden:
+      type: boolean
+    groups:
+      type: array
+      items:
+        $ref: ./GroupObject.yaml#/GroupFullObject
+    disableSignatures:
+      type: boolean
+    reputation:disabled:
+      type: boolean
+    downvote:disabled:
+      type: boolean
+    profile_links:
+      type: array
+      items:
+        type: object
+        properties:
+          id:
+            type: string
+          route:
+            type: string
+          name:
+            type: string
+          visibility:
+            type: object
+            properties:
+              self:
+                type: boolean
+              other:
+                type: boolean
+              moderator:
+                type: boolean
+              globalMod:
+                type: boolean
+              admin:
+                type: boolean
+              canViewInfo:
+                type: boolean
+          public:
+            type: boolean
+          icon:
+            type: string
+        required:
+          - id
+          - route
+          - name
+          - visibility
+          - public
+    sso:
+      type: array
+      items:
+        type: object
+        properties:
+          associated:
+            type: boolean
+          url:
+            type: string
+          name:
+            type: string
+          icon:
+            type: string
+          deauthUrl:
+            type: string
+    websiteLink:
+      type: string
+    websiteName:
+      type: string
+    username:disableEdit:
+      type: number
+    email:disableEdit:
+      type: number
+UserObjectSlim:
+  type: object
+  properties:
+    uid:
+      type: number
+      description: A user identifier
+      example: 1
+    username:
+      type: string
+      description: A friendly name for a given user account
+      example: Dragon Fruit
+    displayname:
+      type: string
+      description: This is either username or fullname depending on forum and user settings
+      example: Dragon Fruit
+    userslug:
+      type: string
+      description: An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.)
+      example: dragon-fruit
+    picture:
+      type: string
+      description: A URL pointing to a picture to be used as the user's avatar
+      example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
+      nullable: true
+    status:
+      type: string
+      enum:
+        - online
+        - offline
+        - dnd
+        - away
+      example: online
+    postcount:
+      type: number
+      example: 1000
+    reputation:
+      type: number
+      description: The user's reputation score on the forum. Out-of-the-box, users gain/lose reputation points based on upvotes/downvotes, though plugins can alter the logic and criterion for awarding reputation points
+      example: 100
+    'email:confirmed':
+      type: number
+      description: Whether the user has confirmed their email address or not
+      example: 1
+    lastonline:
+      type: number
+      description: A UNIX timestamp representing the moment the user was last recorded online on this site
+      example: 1585337827953
+    flags:
+      type: number
+      example: 0
+      nullable: true
+    banned:
+      type: number
+      description: A Boolean representing whether a user is banned or not
+      example: 0
+    'banned:expire':
+      type: number
+      description: A UNIX timestamp representing the moment the ban will be lifted
+      example: 1585337827953
+    joindate:
+      type: number
+      description: A UNIX timestamp representing the moment the user's account was created
+      example: 1585337827953
+    accounttype:
+      type: string
+      description: Type of the user account
+      example: student
+    'icon:text':
+      type: string
+      description: A single-letter representation of a username. This is used in the auto-generated icon given to users without an avatar
+      example: D
+    'icon:bgColor':
+      type: string
+      description: A six-character hexadecimal colour code assigned to the user. This value is used in conjunction with `icon:text` for the user's auto-generated icon
+      example: '#9c27b0'
+    joindateISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    lastonlineISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    banned_until:
+      type: number
+      description: A UNIX timestamp representing the moment a ban will be lifted
+      example: 0
+    banned_until_readable:
+      type: string
+      description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned"
+      example: Not Banned
+UserObjectACP:
+  type: object
+  properties:
+    uid:
+      type: number
+      description: A user identifier
+      example: 1
+    username:
+      type: string
+      description: A friendly name for a given user account
+      example: Dragon Fruit
+    displayname:
+      type: string
+      description: This is either username or fullname depending on forum and user settings
+      example: Dragon Fruit
+    userslug:
+      type: string
+      description: An URL-safe variant of the username (i.e. lower-cased, spaces removed, etc.)
+      example: dragon-fruit
+    email:
+      type: string
+      description: Email address associated with the user account
+      example: dragonfruit@example.org
+    postcount:
+      type: number
+      example: 1000
+    joindate:
+      type: number
+      description: A UNIX timestamp representing the moment the user's account was created
+      example: 1585337827953
+    accounttype:
+      type: string
+      description: Type of the user account
+      example: student
+    banned:
+      type: number
+      description: A Boolean representing whether a user is banned or not
+      example: 0
+    reputation:
+      type: number
+      description: The user's reputation score on the forum. Out-of-the-box, users gain/lose reputation points based on upvotes/downvotes, though plugins can alter the logic and criterion for awarding reputation points
+      example: 100
+    picture:
+      type: string
+      description: A URL pointing to a picture to be used as the user's avatar
+      example: 'https://images.unsplash.com/photo-1560070094-e1f2ddec4337?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=256&h=256&q=80'
+      nullable: true
+    flags:
+      type: number
+      example: 0
+      nullable: true
+    lastonline:
+      type: number
+      description: A UNIX timestamp representing the moment the user was last recorded online on this site
+      example: 1585337827953
+    'email:confirmed':
+      type: number
+      description: Whether the user has confirmed their email address or not
+      example: 1
+    'icon:text':
+      type: string
+      description: A single-letter representation of a username. This is used in the auto-generated icon given to users without an avatar
+      example: D
+    'icon:bgColor':
+      type: string
+      description: A six-character hexadecimal colour code assigned to the user. This value is used in conjunction with `icon:text` for the user's auto-generated icon
+      example: '#9c27b0'
+    joindateISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    lastonlineISO:
+      type: string
+      example: '2020-03-27T20:30:36.590Z'
+    banned_until_readable:
+      type: string
+      description: An ISO 8601 formatted date string representing the moment a ban will be lifted, or the words "Not Banned"
+      example: Not Banned
+    administrator:
+      type: boolean
+    ip:
+      type: string
+      nullable: true
+    ips:
+      type: array
diff --git a/public/openapi/components/schemas/admin/dashboard.yaml b/public/openapi/components/schemas/admin/dashboard.yaml
new file mode 100644
index 0000000000..54a2b51a93
--- /dev/null
+++ b/public/openapi/components/schemas/admin/dashboard.yaml
@@ -0,0 +1,47 @@
+Stats:
+  type: object
+  properties:
+    stats:
+      type: array
+      items:
+        allOf:
+          - type: object
+            properties:
+              yesterday:
+                type: number
+              today:
+                type: number
+              lastweek:
+                type: number
+              thisweek:
+                type: number
+              lastmonth:
+                type: number
+              thismonth:
+                type: number
+              alltime:
+                type: number
+              dayIncrease:
+                type: string
+              dayTextClass:
+                type: string
+              weekIncrease:
+                type: string
+              weekTextClass:
+                type: string
+              monthIncrease:
+                type: string
+              monthTextClass:
+                type: string
+              name:
+                type: string
+          - type: object
+            description: Optional properties that may or may not be present (except for `cid`, which is always present, and is only here as a hack to pass validation)
+            properties:
+              name:
+                type: string
+              href:
+                type: string
+                description: Relative path to dashboard analytics sub-page, if applicable.
+            required:
+              - name
\ No newline at end of file
diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml
new file mode 100644
index 0000000000..20f835b904
--- /dev/null
+++ b/public/openapi/read.yaml
@@ -0,0 +1,334 @@
+openapi: 3.0.0
+info:
+  title: NodeBB Read API
+  version: 1.15.0
+  contact:
+    email: support@nodebb.org
+  license:
+    name: GPL-3.0
+  description: >-
+    # Overview
+
+    The following document outlines every Read API route available via NodeBB. Unlike the write API, the v1.x API was coded organically, and is **not** strictly RESTful. These shortcomings will be addressed in time as the APIs mature.
+
+    ## Shortcomings
+
+    The Read API is named because its primary use is by NodeBB itself when navigating between pages. Therefore, the routes almost universally always follow the same path as actual pages on NodeBB itself. There are also a small number of non-`GET` routes, which do not make sense in a Read API. These will be merged into the Write API in time.
+
+    ## Authentication
+
+    There are a multitude of ways to authenticate with the Read API.
+
+    ### Cookie Authentication
+
+    By default, the API will attempt to find a valid session in the browser's cookies. A valid login session is required for API calls that pertain to operations involving a logged-in user. For example, `/api/unread` is a route showing unread topics, and is not accessible by guest users.
+
+    Most data transfer utilities like cURL will allow you to construct something like a cookie, to be sent alongside the request, to function much like a browser cookie. This should work with the API.
+
+    ### Bearer Authentication
+
+    Both the Read API and Write API offer bearer authentication, as administered through the administration panel.
+
+      * Up until v1.14.3, this is provided by [`nodebb-plugin-write-api`](https://github.com/NodeBB/nodebb-plugin-write-api). The Write API plugin needs to be installed before authentication via bearer token is enabled on routes provided by the Read API.
+      * From NodeBB v1.15.0 onwards, the Write API is available in core, and bearer authentication is available out-of-the-box
+
+    In both cases, a bearer token is issued in the NodeBB admin panel in order to grant access to the API.
+
+    There are two types of tokens:
+      * A *user token* is associated with a specific uid, and all calls made are made in the name of that user
+      * A *master token* is not associated with any specific uid, though a `_uid` parameter is required in the request, and then all calls are made in the name of *that* user.
+        This is the only difference between the two tokens. A master token with `_uid` set to a non-administrator will not allow you to make administrative calls.
+tags:
+  - name: home
+    description: Routes used at the forum index only
+  - name: categories
+    description: Category hierarchy and navigation
+  - name: topics
+  - name: posts
+  - name: users
+  - name: authentication
+    description: User authentication (e.g. login/registration)
+  - name: groups
+    description: User groups
+  - name: admin
+    description: Administrative Control Panel (ACP) routing
+  - name: emails
+    description: Email utilities
+  - name: flags
+    description: Reporting of content by users
+  - name: notifications
+    description: Real-time notifications
+  - name: search
+  - name: tags
+    description: Disparate method of categorizing topics
+  - name: shorthand
+    description: Convenience and utility routes for accessing other part of the API
+  - name: other
+    description: Other one-off routes that do not fit in a section of their own
+paths:
+  /api/:
+    $ref: 'read/index.yaml'
+  /api/admin:
+    $ref: 'read/admin.yaml'
+  /api/admin/dashboard:
+    $ref: 'read/admin/dashboard.yaml'
+  /api/admin/dashboard/logins:
+    $ref: 'read/admin/dashboard/logins.yaml'
+  /api/admin/dashboard/users:
+    $ref: 'read/admin/dashboard/users.yaml'
+  /api/admin/dashboard/topics:
+    $ref: 'read/admin/dashboard/topics.yaml'
+  /api/admin/dashboard/searches:
+    $ref: 'read/admin/dashboard/searches.yaml'
+  "/api/admin/settings/{term}":
+    $ref: 'read/admin/settings/term.yaml'
+  /api/admin/settings/languages:
+    $ref: 'read/admin/settings/languages.yaml'
+  /api/admin/settings/navigation:
+    $ref: 'read/admin/settings/navigation.yaml'
+  /api/admin/settings/homepage:
+    $ref: 'read/admin/settings/homepage.yaml'
+  /api/admin/settings/social:
+    $ref: 'read/admin/settings/social.yaml'
+  /api/admin/settings/email:
+    $ref: 'read/admin/settings/email.yaml'
+  /api/admin/settings/user:
+    $ref: 'read/admin/settings/user.yaml'
+  /api/admin/settings/post:
+    $ref: 'read/admin/settings/post.yaml'
+  /api/admin/settings/advanced:
+    $ref: 'read/admin/settings/advanced.yaml'
+  /api/admin/manage/categories:
+    $ref: 'read/admin/manage/categories.yaml'
+  "/api/admin/manage/categories/{category_id}":
+    $ref: 'read/admin/manage/categories/category_id.yaml'
+  "/api/admin/manage/categories/{category_id}/analytics":
+    $ref: 'read/admin/manage/categories/category_id/analytics.yaml'
+  "/api/admin/manage/privileges/{cid}":
+    $ref: 'read/admin/manage/privileges/cid.yaml'
+  /api/admin/manage/tags:
+    $ref: 'read/admin/manage/tags.yaml'
+  /api/admin/manage/users:
+    $ref: 'read/admin/manage/users.yaml'
+  /api/admin/manage/registration:
+    $ref: 'read/admin/manage/registration.yaml'
+  /api/admin/manage/admins-mods:
+    $ref: 'read/admin/manage/admins-mods.yaml'
+  /api/admin/manage/groups:
+    $ref: 'read/admin/manage/groups.yaml'
+  "/api/admin/manage/groups/{name}":
+    $ref: 'read/admin/manage/groups/name.yaml'
+  /api/admin/manage/uploads:
+    $ref: 'read/admin/manage/uploads.yaml'
+  /api/admin/manage/digest:
+    $ref: 'read/admin/manage/digest.yaml'
+  "/api/admin/appearance/{term}":
+    $ref: 'read/admin/appearance/term.yaml'
+  /api/admin/extend/plugins:
+    $ref: 'read/admin/extend/plugins.yaml'
+  /api/admin/extend/widgets:
+    $ref: 'read/admin/extend/widgets.yaml'
+  /api/admin/extend/rewards:
+    $ref: 'read/admin/extend/rewards.yaml'
+  /api/admin/advanced/database:
+    $ref: 'read/admin/advanced/database.yaml'
+  /api/admin/advanced/events:
+    $ref: 'read/admin/advanced/events.yaml'
+  /api/admin/advanced/hooks:
+    $ref: 'read/admin/advanced/hooks.yaml'
+  /api/admin/advanced/logs:
+    $ref: 'read/admin/advanced/logs.yaml'
+  /api/admin/advanced/errors:
+    $ref: 'read/admin/advanced/errors.yaml'
+  /api/admin/advanced/errors/export:
+    $ref: 'read/admin/advanced/errors/export.yaml'
+  /api/admin/advanced/cache:
+    $ref: 'read/admin/advanced/cache.yaml'
+  /api/admin/advanced/cache/dump:
+    $ref: 'read/admin/advanced/cache/dump.yaml'
+  /api/admin/development/logger:
+    $ref: 'read/admin/development/logger.yaml'
+  /api/admin/development/info:
+    $ref: 'read/admin/development/info.yaml'
+  /api/admin/users/csv:
+    $ref: 'read/admin/users/csv.yaml'
+  /api/admin/groups/{groupname}/csv:
+    $ref: 'read/admin/groups/groupname/csv.yaml'
+  /api/admin/analytics:
+    $ref: 'read/admin/analytics.yaml'
+  /api/admin/category/uploadpicture:
+    $ref: 'read/admin/category/uploadpicture.yaml'
+  /api/admin/uploadfavicon:
+    $ref: 'read/admin/uploadfavicon.yaml'
+  /api/admin/uploadTouchIcon:
+    $ref: 'read/admin/uploadTouchIcon.yaml'
+  /api/admin/uploadMaskableIcon:
+    $ref: 'read/admin/uploadMaskableIcon.yaml'
+  /api/admin/uploadlogo:
+    $ref: 'read/admin/uploadlogo.yaml'
+  /api/admin/uploadOgImage:
+    $ref: 'read/admin/uploadOgImage.yaml'
+  /api/admin/upload/file:
+    $ref: 'read/admin/upload/file.yaml'
+  /api/admin/uploadDefaultAvatar:
+    $ref: 'read/admin/uploadDefaultAvatar.yaml'
+  /api/config:
+    $ref: 'read/config.yaml'
+  /api/users:
+    $ref: 'read/users.yaml'
+  "/api/user/uid/{uid}":
+    $ref: 'read/user/uid/uid.yaml'
+  "/api/user/username/{username}":
+    $ref: 'read/user/username/username.yaml'
+  "/api/user/email/{email}":
+    $ref: 'read/user/email/email.yaml'
+  "/api/user/{userslug}/export/posts":
+    $ref: 'read/user/userslug/export/posts.yaml'
+  "/api/user/{userslug}/export/uploads":
+    $ref: 'read/user/userslug/export/uploads.yaml'
+  "/api/user/{userslug}/export/profile":
+    $ref: 'read/user/userslug/export/profile.yaml'
+  "/api/user/uid/{userslug}/export/{type}":
+    $ref: 'read/user/uid/userslug/export/type.yaml'
+  /api/categories:
+    $ref: 'read/categories.yaml'
+  "/api/categories/{cid}/moderators":
+    $ref: 'read/categories/cid/moderators.yaml'
+  "/api/topic/{topic_id}/{slug}":
+    $ref: 'read/topic/topic_id.yaml'
+  "/api/topic/{topic_id}/{slug}/{post_index}":
+    $ref: 'read/topic/topic_id.yaml'
+  /api/recent:
+    $ref: 'read/recent.yaml'
+  "/api/recent/posts/{term}":
+    $ref: 'read/recent/posts/term.yaml'
+  /api/unread:
+    $ref: 'read/unread.yaml'
+  /api/unread/total:
+    $ref: 'read/unread/total.yaml'
+  "/api/topic/teaser/{topic_id}":
+    $ref: 'read/topic/teaser/topic_id.yaml'
+  "/api/topic/pagination/{topic_id}":
+    $ref: 'read/topic/pagination/topic_id.yaml'
+  /api/post/upload:
+    $ref: 'read/post/upload.yaml'
+  /api/topic/thumb/upload:
+    $ref: 'read/topic/thumb/upload.yaml'
+  /api/login:
+    $ref: 'read/login.yaml'
+  /api/register:
+    $ref: 'read/register.yaml'
+  /api/register/complete:
+    $ref: 'read/register/complete.yaml'
+  "/api/confirm/{code}":
+    $ref: 'read/confirm/code.yaml'
+  /api/tos:
+    $ref: 'read/tos.yaml'
+  /api/search:
+    $ref: 'read/search.yaml'
+  "/api/reset":
+    $ref: 'read/reset.yaml'
+  "/api/reset/{code}":
+    $ref: 'read/reset/code.yaml'
+  "/api/email/unsubscribe/{token}":
+    $ref: 'read/email/unsubscribe/token.yaml'
+  "/api/post/{pid}":
+    $ref: 'read/post/pid.yaml'
+  /api/flags:
+    $ref: 'read/flags.yaml'
+  "/api/flags/{flagId}":
+    $ref: 'read/flags/flagId.yaml'
+  /api/post-queue:
+    $ref: 'read/post-queue.yaml'
+  "/api/post-queue/{id}":
+    $ref: 'read/post-queue.yaml'
+  /api/ip-blacklist:
+    $ref: 'read/ip-blacklist.yaml'
+  /api/registration-queue:
+    $ref: 'read/registration-queue.yaml'
+  /api/tags:
+    $ref: 'read/tags.yaml'
+  "/api/tags/{tag}":
+    $ref: 'read/tags/tag.yaml'
+  /api/popular:
+    $ref: 'read/popular.yaml'
+  /api/top:
+    $ref: 'read/top.yaml'
+  "/api/category/{category_id}/{slug}":
+    $ref: 'read/category/category_id.yaml'
+  "/api/category/{category_id}/{slug}/{topic_index}":
+    $ref: 'read/category/category_id.yaml'
+  /api/career:
+    $ref: 'read/career.yaml'
+  /api/self:
+    $ref: 'read/self.yaml'
+  /api/me:
+    $ref: 'read/me.yaml'
+  /api/me/*:
+    $ref: 'read/me.yaml'
+  "/api/uid/{uid*}":
+    $ref: 'read/uid/uid.yaml'
+  "/api/user/{userslug}":
+    $ref: 'read/user/userslug.yaml'
+  "/api/user/{userslug}/following":
+    $ref: 'read/user/userslug/following.yaml'
+  "/api/user/{userslug}/followers":
+    $ref: 'read/user/userslug/followers.yaml'
+  "/api/user/{userslug}/categories":
+    $ref: 'read/user/userslug/categories.yaml'
+  "/api/user/{userslug}/posts":
+    $ref: 'read/user/userslug/posts.yaml'
+  "/api/user/{userslug}/topics":
+    $ref: 'read/user/userslug/topics.yaml'
+  "/api/user/{userslug}/best":
+    $ref: 'read/user/userslug/best.yaml'
+  "/api/user/{userslug}/controversial":
+    $ref: 'read/user/userslug/controversial.yaml'
+  "/api/user/{userslug}/groups":
+    $ref: 'read/user/userslug/groups.yaml'
+  "/api/user/{userslug}/bookmarks":
+    $ref: 'read/user/userslug/bookmarks.yaml'
+  "/api/user/{userslug}/watched":
+    $ref: 'read/user/userslug/watched.yaml'
+  "/api/user/{userslug}/ignored":
+    $ref: 'read/user/userslug/ignored.yaml'
+  "/api/user/{userslug}/upvoted":
+    $ref: 'read/user/userslug/upvoted.yaml'
+  "/api/user/{userslug}/downvoted":
+    $ref: 'read/user/userslug/downvoted.yaml'
+  "/api/user/{userslug}/edit":
+    $ref: 'read/user/userslug/edit.yaml'
+  "/api/user/{userslug}/edit/username":
+    $ref: 'read/user/userslug/edit/username.yaml'
+  "/api/user/{userslug}/edit/email":
+    $ref: 'read/user/userslug/edit/email.yaml'
+  "/api/user/{userslug}/edit/password":
+    $ref: 'read/user/userslug/edit/password.yaml'
+  "/api/user/{userslug}/info":
+    $ref: 'read/user/userslug/info.yaml'
+  "/api/user/{userslug}/settings":
+    $ref: 'read/user/userslug/settings.yaml'
+  "/api/user/{userslug}/uploads":
+    $ref: 'read/user/userslug/uploads.yaml'
+  "/api/user/{userslug}/consent":
+    $ref: 'read/user/userslug/consent.yaml'
+  "/api/user/{userslug}/blocks":
+    $ref: 'read/user/userslug/blocks.yaml'
+  "/api/user/{userslug}/sessions":
+    $ref: 'read/user/userslug/sessions.yaml'
+  "/api/user/{userslug}/session/{uuid}":
+    $ref: 'read/user/userslug/session/uuid.yaml'
+  /api/notifications:
+    $ref: 'read/notifications.yaml'
+  "/api/user/{userslug}/chats/{roomid}":
+    $ref: 'read/user/userslug/chats/roomid.yaml'
+  "/api/chats/{roomid}":
+    $ref: 'read/chats/roomid.yaml'
+  /api/groups:
+    $ref: 'read/groups.yaml'
+  "/api/groups/{slug}":
+    $ref: 'read/groups/slug.yaml'
+  "/api/groups/{slug}/members":
+    $ref: 'read/groups/slug/members.yaml'
+  /api/outgoing:
+    $ref: 'read/outgoing.yaml'
\ No newline at end of file
diff --git a/public/openapi/read/admin.yaml b/public/openapi/read/admin.yaml
new file mode 100644
index 0000000000..27ec1b9d93
--- /dev/null
+++ b/public/openapi/read/admin.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - admin
+  summary: Get administrative index
+  description: |
+    Internally, NodeBB will redirect you to a different page based on your privilege levels.
+
+    The default is "dashboard" for superadmins and those with the "dashboard" privilege. If the requesting user is neither, then they will be redirected to a page that they have privileges to view (e.g. `/categories`, `/privileges`, `/users`, or `/settings/general`).
+
+    Failing that, the request will be denied.
+  responses:
+    "200":
+      description: |
+        A JSON object containing data for the default admin index.
+      content:
+        application/json:
+          schema:
+            properties: {}
+            additionalProperties: {}
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/cache.yaml b/public/openapi/read/admin/advanced/cache.yaml
new file mode 100644
index 0000000000..c6daf4fbea
--- /dev/null
+++ b/public/openapi/read/admin/advanced/cache.yaml
@@ -0,0 +1,104 @@
+get:
+  tags:
+    - admin
+  summary: Get system cache info
+  parameters:
+    - in: query
+      name: name
+      schema:
+        type: string
+        enum: ['post', 'object', 'group', 'local']
+      required: false
+      description: Specify cache to dump if calling `/dump`
+      example: 'post'
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  postCache:
+                    type: object
+                    properties:
+                      length:
+                        type: number
+                      max:
+                        type: number
+                        nullable: true
+                      itemCount:
+                        type: number
+                      percentFull:
+                        type: number
+                      hits:
+                        type: string
+                      misses:
+                        type: string
+                      hitRatio:
+                        type: string
+                      enabled:
+                        type: boolean
+                  groupCache:
+                    type: object
+                    properties:
+                      length:
+                        type: number
+                      max:
+                        type: number
+                      itemCount:
+                        type: number
+                      percentFull:
+                        type: number
+                      hits:
+                        type: string
+                      misses:
+                        type: string
+                      hitRatio:
+                        type: string
+                      enabled:
+                        type: boolean
+                  localCache:
+                    type: object
+                    properties:
+                      length:
+                        type: number
+                      max:
+                        type: number
+                      itemCount:
+                        type: number
+                      percentFull:
+                        type: number
+                      hits:
+                        type: string
+                      misses:
+                        type: string
+                      hitRatio:
+                        type: string
+                      enabled:
+                        type: boolean
+                  objectCache:
+                    type: object
+                    properties:
+                      length:
+                        type: number
+                      max:
+                        type: number
+                      itemCount:
+                        type: number
+                      percentFull:
+                        type: number
+                      hits:
+                        type: string
+                      misses:
+                        type: string
+                      hitRatio:
+                        type: string
+                      enabled:
+                        type: boolean
+                required:
+                  - postCache
+                  - groupCache
+                  - localCache
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/cache/dump.yaml b/public/openapi/read/admin/advanced/cache/dump.yaml
new file mode 100644
index 0000000000..2be4e38f9b
--- /dev/null
+++ b/public/openapi/read/admin/advanced/cache/dump.yaml
@@ -0,0 +1,23 @@
+get:
+  tags:
+    - admin
+  summary: Get system cache info
+  parameters:
+    - in: query
+      name: name
+      schema:
+        type: string
+        enum: ['post', 'object', 'group', 'local']
+      required: false
+      description: Specify cache to dump if calling `/dump`
+      example: 'post'
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            type: object
+            properties: {}
+            additionalProperties:
+              description: The type of response is dependent on the database used. Please examine the output.
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/database.yaml b/public/openapi/read/admin/advanced/database.yaml
new file mode 100644
index 0000000000..ae2676b369
--- /dev/null
+++ b/public/openapi/read/admin/advanced/database.yaml
@@ -0,0 +1,14 @@
+get:
+  tags:
+    - admin
+  summary: Get database information
+  responses:
+    "200":
+      description: "A JSON object with database status information"
+      content:
+        application/json:
+          schema:
+            properties: {}
+            additionalProperties:
+              type: object
+              description: Each database configured will have an entry here with information about its runtime status
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/errors.yaml b/public/openapi/read/admin/advanced/errors.yaml
new file mode 100644
index 0000000000..a409e47723
--- /dev/null
+++ b/public/openapi/read/admin/advanced/errors.yaml
@@ -0,0 +1,38 @@
+get:
+  tags:
+    - admin
+  summary: Get server-side errors
+  responses:
+    "200":
+      description: "A JSON object containing server-side errors"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  not-found:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        value:
+                          type: string
+                          description: Path to the requested URL that returned a 404
+                        score:
+                          type: number
+                          description: The number of times that URL was requested
+                  analytics:
+                    type: object
+                    properties:
+                      not-found:
+                        type: array
+                        description: 404 responses groups by day, from 6 days ago, to present day
+                        items:
+                          type: number
+                      toobusy:
+                        type: array
+                        description: 503 responses groups by day, from 6 days ago, to present day
+                        items:
+                          type: number
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/errors/export.yaml b/public/openapi/read/admin/advanced/errors/export.yaml
new file mode 100644
index 0000000000..4cb6a3febd
--- /dev/null
+++ b/public/openapi/read/admin/advanced/errors/export.yaml
@@ -0,0 +1,12 @@
+get:
+  tags:
+    - admin
+  summary: Export errors (.csv)
+  responses:
+    "200":
+      description: "A CSV file containing server-side errors"
+      content:
+        text/csv:
+          schema:
+            type: string
+            format: binary
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/events.yaml b/public/openapi/read/admin/advanced/events.yaml
new file mode 100644
index 0000000000..72ce4be7e2
--- /dev/null
+++ b/public/openapi/read/admin/advanced/events.yaml
@@ -0,0 +1,65 @@
+get:
+  tags:
+    - admin
+  summary: Get event log
+  parameters:
+    - in: query
+      name: type
+      schema:
+        type: string
+      description: Event name to filter by
+      example: config-change
+    - in: query
+      name: start
+      schema:
+        type: string
+      description: Start date to filter by
+      example: ''
+    - in: query
+      name: end
+      schema:
+        type: string
+      description: End date to filter by
+      example: ''
+    - in: query
+      name: perPage
+      schema:
+        type: string
+      description: Limit the number of events returned per page
+      example: 20
+  responses:
+    "200":
+      description: "A JSON object containing "
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  events:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        type:
+                          type: string
+                      additionalProperties:
+                        description: Each individual event as added by core/plugins can append their own metadata related to the event
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - type: object
+                properties:
+                  types:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        value:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+                  query:
+                    additionalProperties:
+                      description: An object containing the query string parameters, if any
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/hooks.yaml b/public/openapi/read/admin/advanced/hooks.yaml
new file mode 100644
index 0000000000..d43da54603
--- /dev/null
+++ b/public/openapi/read/admin/advanced/hooks.yaml
@@ -0,0 +1,45 @@
+get:
+  tags:
+    - admin
+  summary: Get active plugin hooks
+  responses:
+    "200":
+      description: "A JSON object containing all hooks with active listeners"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  hooks:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        hookName:
+                          type: string
+                          description: The name of the hook (also the name used in code)
+                        methods:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              id:
+                                type: string
+                                description: Plugin listening to this hook
+                              priority:
+                                type: number
+                                description: Priority level, lower priorities are executed earlier
+                              method:
+                                type: string
+                                description: Stringified method for examination
+                              index:
+                                type: string
+                                description: Internal counter used for DOM element ids
+                        index:
+                          type: string
+                          description: Internal counter used for DOM element ids
+                        count:
+                          type: number
+                          description: The number of listeners subscribed to this hook
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/advanced/logs.yaml b/public/openapi/read/admin/advanced/logs.yaml
new file mode 100644
index 0000000000..57c1245a34
--- /dev/null
+++ b/public/openapi/read/admin/advanced/logs.yaml
@@ -0,0 +1,17 @@
+get:
+  tags:
+    - admin
+  summary: Get server-side log output
+  responses:
+    "200":
+      description: "A JSON object containing the server-side log"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  data:
+                    type: string
+                    description: Output of the server-side log file
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/analytics.yaml b/public/openapi/read/admin/analytics.yaml
new file mode 100644
index 0000000000..508325aace
--- /dev/null
+++ b/public/openapi/read/admin/analytics.yaml
@@ -0,0 +1,58 @@
+get:
+  tags:
+    - admin
+  summary: Get site analytics
+  parameters:
+    - in: query
+      name: units
+      schema:
+        type: string
+        enum: [hours, days]
+      description: Whether to display dashboard data segmented daily or hourly
+      example: days
+    - in: query
+      name: until
+      schema:
+        type: number
+      description: A UNIX timestamp denoting the end of the analytics reporting period
+      example: ''
+    - in: query
+      name: count
+      schema:
+        type: number
+      description: The number of entries to return (e.g. if `units` is `hourly`, and `count` is `24`, the result set will contain 24 hours' worth of analytics)
+      example: 20
+  responses:
+    "200":
+      description: "A JSON object containing analytics data"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              query:
+                additionalProperties:
+                  description: The query string passed in
+              result:
+                type: object
+                properties:
+                  uniquevisitors:
+                    type: array
+                    items:
+                      type: number
+                  pageviews:
+                    type: array
+                    items:
+                      type: number
+                  pageviews:registered:
+                    type: array
+                    items:
+                      type: number
+                  pageviews:bot:
+                    type: array
+                    items:
+                      type: number
+                  pageviews:guest:
+                    type: array
+                    items:
+                      type: number
\ No newline at end of file
diff --git a/public/openapi/read/admin/appearance/term.yaml b/public/openapi/read/admin/appearance/term.yaml
new file mode 100644
index 0000000000..5c8364fa31
--- /dev/null
+++ b/public/openapi/read/admin/appearance/term.yaml
@@ -0,0 +1,18 @@
+get:
+  tags:
+    - admin
+  summary: Get appearance settings
+  parameters:
+    - name: term
+      in: path
+      required: true
+      schema:
+        type: string
+      example: themes
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/category/uploadpicture.yaml b/public/openapi/read/admin/category/uploadpicture.yaml
new file mode 100644
index 0000000000..76ca5c1a71
--- /dev/null
+++ b/public/openapi/read/admin/category/uploadpicture.yaml
@@ -0,0 +1,37 @@
+post:
+  tags:
+    - admin
+  summary: Update category picture (via image upload)
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            cid:
+              type: number
+              description: Category identifier whose picture will be set after successful upload
+              example: 1
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - cid
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded image for use client-side
\ No newline at end of file
diff --git a/public/openapi/read/admin/dashboard.yaml b/public/openapi/read/admin/dashboard.yaml
new file mode 100644
index 0000000000..a281150473
--- /dev/null
+++ b/public/openapi/read/admin/dashboard.yaml
@@ -0,0 +1,64 @@
+get:
+  tags:
+    - admin
+  summary: Get administrative dashboard
+  responses:
+    "200":
+      description: A JSON object containing dashboard data
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  version:
+                    type: string
+                  lookupFailed:
+                    type: boolean
+                  latestVersion:
+                    type: string
+                    nullable: true
+                  upgradeAvailable:
+                    type: boolean
+                    nullable: true
+                  currentPrerelease:
+                    type: boolean
+                  notices:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        done:
+                          type: boolean
+                        doneText:
+                          type: string
+                        notDoneText:
+                          type: string
+                        tooltip:
+                          type: string
+                        link:
+                          type: string
+                      required:
+                        - done
+                  canRestart:
+                    type: boolean
+                  lastrestart:
+                    nullable: true
+                    type: object
+                    properties:
+                      uid:
+                        type: number
+                        description: A user identifier
+                      ip:
+                        type: string
+                      timestamp:
+                        type: number
+                      user:
+                        $ref: ../../components/schemas/UserObject.yaml#/UserObject
+                      timestampISO:
+                        type: string
+                        description: An ISO 8601 formatted date string (complementing `timestamp`)
+                  showSystemControls:
+                    type: boolean
+              - $ref: ../../components/schemas/admin/dashboard.yaml#/Stats
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/dashboard/logins.yaml b/public/openapi/read/admin/dashboard/logins.yaml
new file mode 100644
index 0000000000..2b3280eed3
--- /dev/null
+++ b/public/openapi/read/admin/dashboard/logins.yaml
@@ -0,0 +1,55 @@
+get:
+  tags:
+    - admin
+  summary: Get detailed login analytics
+  responses:
+    "200":
+      description: A JSON object containing more detailed analytics related to user login sessions.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  set:
+                    type: string
+                    description: The analytics set that is being queried
+                  query:
+                    additionalProperties:
+                      description: An object containing the query string parameters, if any
+                  summary:
+                    type: object
+                    properties:
+                      day:
+                        type: number
+                      week:
+                        type: number
+                      month:
+                        type: number
+                  sessions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        ip:
+                          type: string
+                        uuid:
+                          type: string
+                        datetime:
+                          type: number
+                        platform:
+                          type: string
+                        browser:
+                          type: string
+                        version:
+                          type: string
+                        current:
+                          type: boolean
+                        datetimeISO:
+                          type: string
+                        user:
+                          $ref: ../../../components/schemas/UserObj.yaml#/UserObj
+                  loginDays:
+                    type: number
+              - $ref: ../../../components/schemas/admin/dashboard.yaml#/Stats
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/dashboard/searches.yaml b/public/openapi/read/admin/dashboard/searches.yaml
new file mode 100644
index 0000000000..7097c8bcb8
--- /dev/null
+++ b/public/openapi/read/admin/dashboard/searches.yaml
@@ -0,0 +1,25 @@
+get:
+  tags:
+    - admin
+  summary: Get detailed user registration analytics
+  responses:
+    "200":
+      description: A JSON object containing popular searches.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  searches:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        value:
+                          type: string
+                          description: The string that was searched
+                        score:
+                          type: number
+                          description: Number of times this string has been searched
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/dashboard/topics.yaml b/public/openapi/read/admin/dashboard/topics.yaml
new file mode 100644
index 0000000000..2a787893c1
--- /dev/null
+++ b/public/openapi/read/admin/dashboard/topics.yaml
@@ -0,0 +1,34 @@
+get:
+  tags:
+    - admin
+  summary: Get detailed user registration analytics
+  responses:
+    "200":
+      description: A JSON object containing more detailed analytics related to user registrations.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  set:
+                    type: string
+                    description: The analytics set that is being queried
+                  query:
+                    additionalProperties:
+                      description: An object containing the query string parameters, if any
+                  summary:
+                    type: object
+                    properties:
+                      day:
+                        type: number
+                      week:
+                        type: number
+                      month:
+                        type: number
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/TopicObject.yaml#/TopicObject
+              - $ref: ../../../components/schemas/admin/dashboard.yaml#/Stats
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/dashboard/users.yaml b/public/openapi/read/admin/dashboard/users.yaml
new file mode 100644
index 0000000000..67c101d943
--- /dev/null
+++ b/public/openapi/read/admin/dashboard/users.yaml
@@ -0,0 +1,34 @@
+get:
+  tags:
+    - admin
+  summary: Get detailed user registration analytics
+  responses:
+    "200":
+      description: A JSON object containing more detailed analytics related to user registrations.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  set:
+                    type: string
+                    description: The analytics set that is being queried
+                  query:
+                    additionalProperties:
+                      description: An object containing the query string parameters, if any
+                  summary:
+                    type: object
+                    properties:
+                      day:
+                        type: number
+                      week:
+                        type: number
+                      month:
+                        type: number
+                  users:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/UserObject.yaml#/UserObject
+              - $ref: ../../../components/schemas/admin/dashboard.yaml#/Stats
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/development/info.yaml b/public/openapi/read/admin/development/info.yaml
new file mode 100644
index 0000000000..23d277fc3f
--- /dev/null
+++ b/public/openapi/read/admin/development/info.yaml
@@ -0,0 +1,166 @@
+get:
+  tags:
+    - admin
+  summary: Get process/system information
+  responses:
+    "200":
+      description: "A JSON object containing process and system information"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  info:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        process:
+                          type: object
+                          properties:
+                            port:
+                              description: An array containing the port numbers configured to be used by NodeBB processes
+                              oneOf:
+                                - type: array
+                                  items:
+                                    oneOf:
+                                      - type: string
+                                      - type: number
+                                - type: string
+                                - type: number
+                            pid:
+                              type: number
+                              description: Process id
+                            title:
+                              type: number
+                              description: Executable
+                            version:
+                              type: number
+                              description: NodeBB version
+                            memoryUsage:
+                              type: object
+                              properties:
+                                rss:
+                                  type: number
+                                heapTotal:
+                                  type: number
+                                heapUsed:
+                                  type: number
+                                external:
+                                  type: number
+                                arrayBuffers:
+                                  type: number
+                                humanReadable:
+                                  type: number
+                              required:
+                                - rss
+                                - heapTotal
+                                - heapUsed
+                                - external
+                                - humanReadable
+                            uptime:
+                              type: number
+                            uptimeHumanReadable:
+                              type: string
+                            cpuUsage:
+                              type: object
+                              properties:
+                                user:
+                                  type: string
+                                system:
+                                  type: string
+                        os:
+                          type: object
+                          properties:
+                            hostname:
+                              type: string
+                            type:
+                              type: string
+                            platform:
+                              type: string
+                            arch:
+                              type: string
+                            release:
+                              type: string
+                            load:
+                              type: string
+                              description: CPU load
+                            freemem:
+                              type: string
+                            totalmem:
+                              type: string
+                        nodebb:
+                          type: object
+                          properties:
+                            isPrimary:
+                              type: boolean
+                            isCluster:
+                              type: boolean
+                            runJobs:
+                              type: boolean
+                            jobsDisabled:
+                              type: boolean
+                        git:
+                          type: object
+                          properties:
+                            hash:
+                              type: string
+                            hashShort:
+                              type: string
+                            branch:
+                              type: string
+                        stats:
+                          type: object
+                          properties:
+                            onlineGuestCount:
+                              type: number
+                            onlineRegisteredCount:
+                              type: number
+                            socketCount:
+                              type: number
+                            users:
+                              type: object
+                              properties:
+                                categories:
+                                  type: number
+                                recent:
+                                  type: number
+                                unread:
+                                  type: number
+                                topics:
+                                  type: number
+                                category:
+                                  type: number
+                            topics:
+                              type: array
+                        id:
+                          type: string
+                  infoJSON:
+                    type: string
+                    description: "`info`, but stringified"
+                  host:
+                    type: string
+                    description: Server hostname
+                  port:
+                    description: An array containing the port numbers configured to be used by NodeBB processes
+                    oneOf:
+                      - type: array
+                        items:
+                          oneOf:
+                            - type: string
+                            - type: number
+                      - type: string
+                      - type: number
+                  nodeCount:
+                    type: number
+                    description: The number of NodeBB application processes currently running
+                  timeout:
+                    type: number
+                  ip:
+                    type: string
+                  loggedIn:
+                    type: boolean
+                  relative_path:
+                    type: string
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/development/logger.yaml b/public/openapi/read/admin/development/logger.yaml
new file mode 100644
index 0000000000..bc02dc6faf
--- /dev/null
+++ b/public/openapi/read/admin/development/logger.yaml
@@ -0,0 +1,11 @@
+get:
+  tags:
+    - admin
+  summary: Get system logger settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/extend/plugins.yaml b/public/openapi/read/admin/extend/plugins.yaml
new file mode 100644
index 0000000000..8613d267fb
--- /dev/null
+++ b/public/openapi/read/admin/extend/plugins.yaml
@@ -0,0 +1,206 @@
+get:
+  tags:
+    - admin
+  summary: Get system plugin settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  installed:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        latest:
+                          type: string
+                        description:
+                          type: string
+                        name:
+                          type: string
+                        updated:
+                          type: string
+                        url:
+                          type: string
+                        numInstalls:
+                          type: number
+                        isCompatible:
+                          type: boolean
+                        id:
+                          type: string
+                        installed:
+                          type: boolean
+                        active:
+                          type: boolean
+                        isTheme:
+                          type: boolean
+                        error:
+                          type: boolean
+                        version:
+                          type: string
+                        license:
+                          type: object
+                          properties:
+                            name:
+                              type: string
+                            text:
+                              type: string
+                          nullable: true
+                        outdated:
+                          type: boolean
+                        settingsRoute:
+                          type: string
+                      required:
+                        - latest
+                        - description
+                        - name
+                        - id
+                        - installed
+                        - active
+                        - isTheme
+                        - error
+                        - version
+                        - license
+                        - outdated
+                  installedCount:
+                    type: number
+                  activeCount:
+                    type: number
+                  inactiveCount:
+                    type: number
+                  upgradeCount:
+                    type: number
+                  download:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        updated:
+                          type: string
+                        description:
+                          type: string
+                        latest:
+                          type: string
+                        url:
+                          type: string
+                        numInstalls:
+                          type: number
+                        isCompatible:
+                          type: boolean
+                        id:
+                          type: string
+                        installed:
+                          type: boolean
+                        active:
+                          type: boolean
+                      required:
+                        - name
+                        - updated
+                        - latest
+                        - url
+                        - numInstalls
+                        - isCompatible
+                        - id
+                        - installed
+                        - active
+                  incompatible:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        latest:
+                          type: string
+                        description:
+                          type: string
+                        name:
+                          type: string
+                        updated:
+                          type: string
+                        url:
+                          type: string
+                        numInstalls:
+                          type: number
+                        isCompatible:
+                          type: boolean
+                        id:
+                          type: string
+                        installed:
+                          type: boolean
+                        active:
+                          type: boolean
+                        downloads:
+                          type: number
+                      required:
+                        - name
+                        - updated
+                        - latest
+                        - url
+                        - numInstalls
+                        - isCompatible
+                        - id
+                        - installed
+                        - active
+                  trending:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        latest:
+                          type: string
+                        description:
+                          type: string
+                        name:
+                          type: string
+                        updated:
+                          type: string
+                        url:
+                          type: string
+                        numInstalls:
+                          type: number
+                        isCompatible:
+                          type: boolean
+                        id:
+                          type: string
+                        installed:
+                          type: boolean
+                        active:
+                          type: boolean
+                        isTheme:
+                          type: boolean
+                        error:
+                          type: boolean
+                        version:
+                          type: string
+                        license:
+                          type: object
+                          properties:
+                            name:
+                              type: string
+                            text:
+                              type: string
+                          nullable: true
+                        outdated:
+                          type: boolean
+                        settingsRoute:
+                          type: string
+                        downloads:
+                          type: number
+                      required:
+                        - latest
+                        - description
+                        - name
+                        - id
+                        - installed
+                        - active
+                        - downloads
+                  submitPluginUsage:
+                    type: number
+                  version:
+                    type: string
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/extend/rewards.yaml b/public/openapi/read/admin/extend/rewards.yaml
new file mode 100644
index 0000000000..fcd9b4b04f
--- /dev/null
+++ b/public/openapi/read/admin/extend/rewards.yaml
@@ -0,0 +1,85 @@
+get:
+  tags:
+    - admin
+  summary: Get rewards settings
+  responses:
+    "200":
+      description: "A JSON object containing rewards and their settings"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  active:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        condition:
+                          type: string
+                        conditional:
+                          type: string
+                        value:
+                          type: number
+                        rid:
+                          type: string
+                        claimable:
+                          type: string
+                        id:
+                          type: string
+                        disabled:
+                          type: boolean
+                        rewards:
+                          type: array
+                          items:
+                            additionalProperties: {}
+                            description: Reward-specific properties
+                  conditions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        condition:
+                          type: string
+                  conditionals:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        conditional:
+                          type: string
+                  rewards:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        rid:
+                          type: string
+                        name:
+                          type: string
+                        inputs:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              type:
+                                type: string
+                              name:
+                                type: string
+                              label:
+                                type: string
+                              values:
+                                type: array
+                                items:
+                                  type: object
+                                  properties:
+                                    name:
+                                      type: string
+                                    value:
+                                      type: string
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/extend/widgets.yaml b/public/openapi/read/admin/extend/widgets.yaml
new file mode 100644
index 0000000000..90527bf8f0
--- /dev/null
+++ b/public/openapi/read/admin/extend/widgets.yaml
@@ -0,0 +1,90 @@
+get:
+  tags:
+    - admin
+  summary: Get widget settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  templates:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        template:
+                          type: string
+                        areas:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              name:
+                                type: string
+                              location:
+                                type: string
+                  areas:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        template:
+                          type: string
+                        location:
+                          type: string
+                        data:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              widget:
+                                type: string
+                              data:
+                                type: object
+                                properties:
+                                  html:
+                                    type: string
+                                  cid:
+                                    type: string
+                                  title:
+                                    type: string
+                                  container:
+                                    type: string
+                                  groups:
+                                    type: array
+                                    items: {}
+                                  groupsHideFrom:
+                                    type: array
+                                    items: {}
+                                  hide-mobile:
+                                    type: string
+                                  numTags:
+                                    type: string
+                                  numUsers:
+                                    type: string
+                                  text:
+                                    type: string
+                                  parseAsPost:
+                                    type: string
+                                  numTopics:
+                                    type: string
+                  availableWidgets:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        widget:
+                          type: string
+                        name:
+                          type: string
+                        description:
+                          type: string
+                        content:
+                          type: string
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/groups/groupname/csv.yaml b/public/openapi/read/admin/groups/groupname/csv.yaml
new file mode 100644
index 0000000000..e774a1b4a1
--- /dev/null
+++ b/public/openapi/read/admin/groups/groupname/csv.yaml
@@ -0,0 +1,25 @@
+get:
+  tags:
+    - admin
+  summary: Get members of a group (.csv)
+  parameters:
+    - in: header
+      name: referer
+      schema:
+        type: string
+      required: true
+      example: /admin/manage/groups
+    - in: path
+      name: groupname
+      schema:
+        type: string
+      required: true
+      example: registered-users
+  responses:
+    "200":
+      description: "A CSV file containing all users in the group"
+      content:
+        text/csv:
+          schema:
+            type: string
+            format: binary
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/admins-mods.yaml b/public/openapi/read/admin/manage/admins-mods.yaml
new file mode 100644
index 0000000000..ba922850e9
--- /dev/null
+++ b/public/openapi/read/admin/manage/admins-mods.yaml
@@ -0,0 +1,107 @@
+get:
+  tags:
+    - admin
+  summary: Get administrators and moderators
+  responses:
+    "200":
+      description: "A JSON object containing administrators and moderators globally and per-category"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  admins:
+                    $ref: ../../../components/schemas/GroupObject.yaml#/GroupFullObject
+                  globalMods:
+                    $ref: ../../../components/schemas/GroupObject.yaml#/GroupFullObject
+                  categoryMods:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        cid:
+                          type: number
+                          description: A category identifier assigned upon category creation (this value cannot be changed)
+                        name:
+                          type: string
+                          description: The category's name/title
+                        description:
+                          type: string
+                          description: A variable-length description of the category (usually displayed underneath the category name)
+                        descriptionParsed:
+                          type: string
+                          description: A variable-length description of the category (usually displayed underneath the category name). Unlike `description`, this value here will have been run through any parsers installed on the forum (e.g. Markdown)
+                        icon:
+                          type: string
+                          description: A FontAwesome icon string
+                          example: fa-comments-o
+                        bgColor:
+                          type: string
+                          description: Theme-related, a six-character hexadecimal string representing the background colour of the category
+                        color:
+                          type: string
+                          description: Theme-related, a six-character hexadecimal string representing the foreground/text colour of the category
+                        slug:
+                          type: string
+                          description: An URL-safe variant of the category title. This value is automatically generated.
+                          readOnly: true
+                        parentCid:
+                          type: number
+                          description: The category identifier for the category that is the immediate ancestor of the current category
+                        topic_count:
+                          type: number
+                          description: The number of topics in the category
+                        post_count:
+                          type: number
+                          description: The number of posts in the category
+                        disabled:
+                          type: number
+                          description: Whether or not this category is disabled.
+                        order:
+                          type: number
+                          description: A number representing the category's place in the hierarchy
+                        link:
+                          type: string
+                          description: If set, attempting to access the forum will go to this external link instead (theme-specific)
+                        numRecentReplies:
+                          type: number
+                          description: The number of posts to render in the API response (this is mostly used at the theme level)
+                        class:
+                          type: string
+                          description: Values that are appended to the `class` attribute of the category's parent/root element
+                        imageClass:
+                          type: string
+                          enum: [auto, cover, contain]
+                          description: The `background-position` of the category background image, if one is set
+                        isSection:
+                          type: number
+                        minTags:
+                          type: number
+                          description: Minimum tags per topic in this category
+                        maxTags:
+                          type: number
+                          description: Maximum tags per topic in this category
+                        postQueue:
+                          type: number
+                        totalPostCount:
+                          type: number
+                          description: The number of posts in the category
+                        totalTopicCount:
+                          type: number
+                          description: The number of topics in the category
+                        subCategoriesPerPage:
+                          type: number
+                          description: The number of subcategories to display on the categories and category page
+                        moderators:
+                          type: array
+                          items:
+                            $ref: ../../../components/schemas/UserObject.yaml#/UserObjectSlim
+                  selectedCategory:
+                    $ref: ../../../components/schemas/CategoryObject.yaml#/CategoryObject
+                  allPrivileges:
+                    type: array
+                    items:
+                      type: string
+                      description: A simple array containing user privilege names (used client-side when giving mod privilege)
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/categories.yaml b/public/openapi/read/admin/manage/categories.yaml
new file mode 100644
index 0000000000..7cfc8753ff
--- /dev/null
+++ b/public/openapi/read/admin/manage/categories.yaml
@@ -0,0 +1,53 @@
+get:
+  tags:
+    - admin
+  summary: Get category management settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type : object
+                properties:
+                  categoriesPerPage:
+                    type: number
+                  categoriesTree:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        cid:
+                          type: number
+                          description: A category identifier
+                        name:
+                          type: string
+                        disabled:
+                          type: number
+                        icon:
+                          type: string
+                        link:
+                          type: string
+                        parentCid:
+                          type: number
+                          description: The category identifier for the category that is the immediate
+                            ancestor of the current category
+                        color:
+                          type: string
+                        bgColor:
+                          type: string
+                        backgroundImage:
+                          type: string
+                          nullable: true
+                        imageClass:
+                          type: string
+                        order:
+                          type: number
+                        subCategoriesPerPage:
+                          type: number
+                        children:
+                          type: array
+                          description: Array of children categories
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/categories/category_id.yaml b/public/openapi/read/admin/manage/categories/category_id.yaml
new file mode 100644
index 0000000000..37794b3915
--- /dev/null
+++ b/public/openapi/read/admin/manage/categories/category_id.yaml
@@ -0,0 +1,42 @@
+get:
+  tags:
+    - admin
+  summary: Get category settings
+  parameters:
+    - name: category_id
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  category:
+                    allOf:
+                      - $ref: ../../../../components/schemas/CategoryObject.yaml#/CategoryObject
+                      - type: object
+                        properties:
+                          tagWhitelist:
+                            type: array
+                            items:
+                              type: string
+                          unread-class:
+                            type: string
+                          parent:
+                            $ref: ../../../../components/schemas/CategoryObject.yaml#/CategoryObject
+                  selectedCategory:
+                    $ref: ../../../../components/schemas/CategoryObject.yaml#/CategoryObject
+                  customClasses:
+                    type: array
+                    items:
+                      type: string
+                  postQueueEnabled:
+                    type: boolean
+              - $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/categories/category_id/analytics.yaml b/public/openapi/read/admin/manage/categories/category_id/analytics.yaml
new file mode 100644
index 0000000000..4805c4729b
--- /dev/null
+++ b/public/openapi/read/admin/manage/categories/category_id/analytics.yaml
@@ -0,0 +1,42 @@
+get:
+  tags:
+    - admin
+  summary: Get category anayltics
+  parameters:
+    - name: category_id
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  name:
+                    type: string
+                  analytics:
+                    type: object
+                    properties:
+                      pageviews:hourly:
+                        type: array
+                        items:
+                          type: number
+                      pageviews:daily:
+                        type: array
+                        items:
+                          type: number
+                      topics:daily:
+                        type: array
+                        items:
+                          type: number
+                      posts:daily:
+                        type: array
+                        items:
+                          type: number
+              - $ref: ../../../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/digest.yaml b/public/openapi/read/admin/manage/digest.yaml
new file mode 100644
index 0000000000..f094a3f7b6
--- /dev/null
+++ b/public/openapi/read/admin/manage/digest.yaml
@@ -0,0 +1,54 @@
+get:
+  tags:
+    - admin
+  summary: Get system digest info/settings
+  responses:
+    "200":
+      description: "A JSON object containing recent digest sends and settings"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  delivery:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        username:
+                          type: string
+                          description: A friendly name for a given user account
+                        displayname:
+                          type: string
+                          description: This is either username or fullname depending on forum and user settings
+                        picture:
+                          nullable: true
+                          type: string
+                        uid:
+                          type: number
+                          description: A user identifier
+                        icon:text:
+                          type: string
+                          description: A single-letter representation of a username. This is used in the
+                            auto-generated icon given to users without an
+                            avatar
+                        icon:bgColor:
+                          type: string
+                          description: A six-character hexadecimal colour code assigned to the user. This
+                            value is used in conjunction with `icon:text`
+                            for the user's auto-generated icon
+                          example: "#f44336"
+                        lastDelivery:
+                          type: string
+                        setting:
+                          type: boolean
+                  default:
+                    type: string
+                required:
+                  - title
+                  - delivery
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/groups.yaml b/public/openapi/read/admin/manage/groups.yaml
new file mode 100644
index 0000000000..12b224b07d
--- /dev/null
+++ b/public/openapi/read/admin/manage/groups.yaml
@@ -0,0 +1,101 @@
+get:
+  tags:
+    - admin
+  summary: Get user groups
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  groups:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        description:
+                          type: string
+                        deleted:
+                          oneOf:
+                            - type: number
+                            - type: string
+                        hidden:
+                          type: number
+                        system:
+                          type: number
+                        userTitle:
+                          type: string
+                        userTitleEscaped:
+                          type: string
+                        icon:
+                          type: string
+                        labelColor:
+                          type: string
+                        slug:
+                          type: string
+                        createtime:
+                          type: number
+                        memberCount:
+                          type: number
+                        private:
+                          type: number
+                        cover:url:
+                          type: string
+                        cover:position:
+                          type: string
+                        userTitleEnabled:
+                          type: number
+                        disableJoinRequests:
+                          type: number
+                        disableLeave:
+                          type: number
+                        nameEncoded:
+                          type: string
+                        displayName:
+                          type: string
+                        textColor:
+                          type: string
+                        createtimeISO:
+                          type: string
+                        cover:thumb:url:
+                          type: string
+                        ownerUid:
+                          type: number
+                        memberPostCids:
+                          type: string
+                        memberPostCidsArray:
+                          type: array
+                          items:
+                            type: number
+                            example: [1, 2, 3]
+                      required:
+                        - name
+                        - description
+                        - hidden
+                        - system
+                        - userTitle
+                        - icon
+                        - labelColor
+                        - slug
+                        - createtime
+                        - memberCount
+                        - private
+                        - cover:url
+                        - cover:position
+                        - userTitleEnabled
+                        - disableJoinRequests
+                        - disableLeave
+                        - nameEncoded
+                        - displayName
+                        - textColor
+                        - createtimeISO
+                        - cover:thumb:url
+                  yourid:
+                    type: number
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/groups/name.yaml b/public/openapi/read/admin/manage/groups/name.yaml
new file mode 100644
index 0000000000..f04987a1f6
--- /dev/null
+++ b/public/openapi/read/admin/manage/groups/name.yaml
@@ -0,0 +1,40 @@
+get:
+  tags:
+    - admin
+  summary: Get user group details
+  parameters:
+    - name: name
+      in: path
+      required: true
+      schema:
+        type: string
+      example: administrators
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  group:
+                    $ref: ../../../../components/schemas/GroupObject.yaml#/GroupFullObject
+                  groupNames:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        encodedName:
+                          type: string
+                        displayName:
+                          type: string
+                        selected:
+                          type: boolean
+                  allowPrivateGroups:
+                    type: number
+                  maximumGroupNameLength:
+                    type: number
+                  maximumGroupTitleLength:
+                    type: number
+              - $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/privileges/cid.yaml b/public/openapi/read/admin/manage/privileges/cid.yaml
new file mode 100644
index 0000000000..2ef7c8d891
--- /dev/null
+++ b/public/openapi/read/admin/manage/privileges/cid.yaml
@@ -0,0 +1,122 @@
+get:
+  tags:
+    - admin
+  summary: Get category privileges
+  parameters:
+    - name: cid
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  privileges:
+                    type: object
+                    properties:
+                      labels:
+                        type: object
+                        properties:
+                          users:
+                            type: array
+                            items:
+                              type: string
+                              description: Language key of the privilege name's user-friendly name
+                          groups:
+                            type: array
+                            items:
+                              type: string
+                              description: Language key of the privilege name's user-friendly name
+                      keys:
+                        type: object
+                        properties:
+                          users:
+                            type: array
+                            items:
+                              type: string
+                          groups:
+                            type: array
+                            items:
+                              type: string
+                      users:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            name:
+                              type: string
+                            nameEscaped:
+                              type: string
+                            privileges:
+                              type: object
+                              additionalProperties:
+                                type: boolean
+                                description: Each privilege will have a key in this object
+                      groups:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            name:
+                              type: string
+                            nameEscaped:
+                              type: string
+                            privileges:
+                              type: object
+                              additionalProperties:
+                                type: boolean
+                                description: Each privilege will have a key in this object
+                            isPrivate:
+                              type: boolean
+                            isSystem:
+                              type: boolean
+                      columnCountUserOther:
+                        type: number
+                      columnCountGroupOther:
+                        type: number
+                  categories:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        cid:
+                          type: number
+                          description: A category identifier
+                        name:
+                          type: string
+                        icon:
+                          type: string
+                        selected:
+                          type: boolean
+                        level:
+                          type: string
+                        parentCid:
+                          type: number
+                          description: The category identifier for the category that is the immediate
+                            ancestor of the current category
+                        color:
+                          type: string
+                        bgColor:
+                          type: string
+                        imageClass:
+                          type: string
+                      required:
+                        - cid
+                        - name
+                        - icon
+                        - selected
+                  selectedCategory:
+                    $ref: ../../../../components/schemas/CategoryObject.yaml#/CategoryObject
+                  cid:
+                    type: number
+                    description: A category identifier
+                  group:
+                    type: string
+              - $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/registration.yaml b/public/openapi/read/admin/manage/registration.yaml
new file mode 100644
index 0000000000..3d7fad4487
--- /dev/null
+++ b/public/openapi/read/admin/manage/registration.yaml
@@ -0,0 +1,90 @@
+get:
+  tags:
+    - admin
+  summary: Get registration queue/invites
+  responses:
+    "200":
+      description: "A JSON object containing the registration queue and invites"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  registrationQueueCount:
+                    type: number
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        username:
+                          type: string
+                        email:
+                          type: string
+                        ip:
+                          type: string
+                        timestampISO:
+                          type: string
+                        usernameEscaped:
+                          type: string
+                        ipMatch:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              username:
+                                type: string
+                                description: A friendly name for a given user account
+                              userslug:
+                                type: string
+                                description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                  removed, etc.)
+                              picture:
+                                type: string
+                              uid:
+                                type: number
+                                description: A user identifier
+                              icon:text:
+                                type: string
+                                description: A single-letter representation of a username. This is used in the
+                                  auto-generated icon given to users without
+                                  an avatar
+                              icon:bgColor:
+                                type: string
+                                description: A six-character hexadecimal colour code assigned to the user. This
+                                  value is used in conjunction with
+                                  `icon:text` for the user's auto-generated
+                                  icon
+                                example: "#f44336"
+                        customActions:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              title:
+                                type: string
+                              id:
+                                type: string
+                              class:
+                                type: string
+                              icon:
+                                type: string
+                  customHeaders:
+                    type: array
+                  invites:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        uid:
+                          type: number
+                        invitations:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              email:
+                                type: string
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/tags.yaml b/public/openapi/read/admin/manage/tags.yaml
new file mode 100644
index 0000000000..f42d4df021
--- /dev/null
+++ b/public/openapi/read/admin/manage/tags.yaml
@@ -0,0 +1,18 @@
+get:
+  tags:
+    - admin
+  summary: Get tag settings
+  responses:
+    "200":
+      description: "A JSON object containing tag settings"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  tags:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/TagObject.yaml#/TagObject
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/uploads.yaml b/public/openapi/read/admin/manage/uploads.yaml
new file mode 100644
index 0000000000..85949a2ddd
--- /dev/null
+++ b/public/openapi/read/admin/manage/uploads.yaml
@@ -0,0 +1,57 @@
+get:
+  tags:
+    - admin
+  summary: Get uploaded files
+  parameters:
+    - in: query
+      name: dir
+      schema:
+        type: string
+      description: Path of the folder, relative to `public/uploads/`
+      example: /
+  responses:
+    "200":
+      description: "A JSON object containing uploaded files"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  currentFolder:
+                    type: string
+                    description: Path of the folder, relative to `public/uploads/`
+                  showPids:
+                    type: boolean
+                    description: Whether or not the post identifiers should be shown (this is `true` only for `public/uploads/files/`, as that is where post uploads go)
+                  files:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        path:
+                          type: string
+                          description: Path relative to `currentFolder`
+                        url:
+                          type: string
+                          description: Relative URL ready to be combined with `config.relative_path` on the client-side or templates
+                        fileCount:
+                          type: number
+                          description: For directories, the number of files inside
+                        size:
+                          type: number
+                          description: The size of the file/directory
+                        sizeHumanReadable:
+                          type: string
+                        isDirectory:
+                          type: boolean
+                        isFile:
+                          type: boolean
+                        mtime:
+                          type: number
+                          description: Last modified time of the file, down to the microsecond (expressed as a UNIX timestamp)
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/manage/users.yaml b/public/openapi/read/admin/manage/users.yaml
new file mode 100644
index 0000000000..80dbed1728
--- /dev/null
+++ b/public/openapi/read/admin/manage/users.yaml
@@ -0,0 +1,39 @@
+get:
+  tags:
+    - admin
+  summary: Get users
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  users:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/UserObject.yaml#/UserObjectACP
+                  page:
+                    type: number
+                  pageCount:
+                    type: number
+                  resultsPerPage:
+                    type: number
+                  reverse:
+                    type: boolean
+                  sortBy:
+                    type: string
+                  sort_lastonline:
+                    type: boolean
+                  userCount:
+                    type: number
+                  showInviteButton:
+                    type: boolean
+                  inviteOnly:
+                    type: boolean
+                  adminInviteOnly:
+                    type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/advanced.yaml b/public/openapi/read/admin/settings/advanced.yaml
new file mode 100644
index 0000000000..2cebeeb7e9
--- /dev/null
+++ b/public/openapi/read/admin/settings/advanced.yaml
@@ -0,0 +1,18 @@
+get:
+  tags:
+    - admin
+  summary: Get advanced settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  groupsExemptFromMaintenanceMode:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/GroupObject.yaml#/GroupDataObject
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/email.yaml b/public/openapi/read/admin/settings/email.yaml
new file mode 100644
index 0000000000..e2d9b76257
--- /dev/null
+++ b/public/openapi/read/admin/settings/email.yaml
@@ -0,0 +1,43 @@
+get:
+  tags:
+    - admin
+  summary: Get emailer settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  emails:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        path:
+                          type: string
+                          description: The name of the email template
+                        fullpath:
+                          type: string
+                          description: Full system path to the email template
+                        text:
+                          type: string
+                          description: Customized email template text, if applicable, otherwise identical to `original`
+                        original:
+                          type: string
+                          description: The email template text as provided by NodeBB core
+                        isCustom:
+                          type: boolean
+                  sendable:
+                    type: array
+                    items:
+                      type: string
+                      description: The name of the email template
+                  services:
+                    type: array
+                    items:
+                      type: string
+                      description: A list of email services which can be used to send emails on behalf of NodeBB
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/homepage.yaml b/public/openapi/read/admin/settings/homepage.yaml
new file mode 100644
index 0000000000..3225629c37
--- /dev/null
+++ b/public/openapi/read/admin/settings/homepage.yaml
@@ -0,0 +1,23 @@
+get:
+  tags:
+    - admin
+  summary: Get homepage settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  routes:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        route:
+                          type: string
+                        name:
+                          type: string
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/languages.yaml b/public/openapi/read/admin/settings/languages.yaml
new file mode 100644
index 0000000000..64b1e6b54e
--- /dev/null
+++ b/public/openapi/read/admin/settings/languages.yaml
@@ -0,0 +1,35 @@
+get:
+  tags:
+    - admin
+  summary: Get language settings
+  responses:
+    "200":
+      description: A JSON object containing available languages and settings
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  languages:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                          description: Localised name of the language
+                        code:
+                          type: string
+                          description: A language code (similar to ISO-639)
+                        dir:
+                          type: string
+                          description: Directionality of the language
+                          enum: [ltr, rtl]
+                        selected:
+                          type: boolean
+                          description: Denotes the currently selected default system language on the forum
+                  autoDetectLang:
+                    type: integer
+                    description: Whether the forum will attempt to guess language based on browser's `Accept-Language` header
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/navigation.yaml b/public/openapi/read/admin/settings/navigation.yaml
new file mode 100644
index 0000000000..9f75f9aa28
--- /dev/null
+++ b/public/openapi/read/admin/settings/navigation.yaml
@@ -0,0 +1,107 @@
+get:
+  tags:
+    - admin
+  summary: Get navigation bar settings
+  responses:
+    "200":
+      description: A JSON object containing navigation settings
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  enabled:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        route:
+                          type: string
+                          description: Relative URL to the page the navigation item goes to
+                        title:
+                          type: string
+                          description: Tooltip text
+                        enabled:
+                          type: boolean
+                        iconClass:
+                          type: string
+                          description: A FontAwesome icon string
+                        textClass:
+                          type: string
+                          description: HTML class applied to the text label for this navigation item
+                        text:
+                          type: string
+                          description: Label text for this navigation item
+                        order:
+                          type: integer
+                          description: Ordinality of this item, lower value appears earlier
+                        groups:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              displayName:
+                                type: string
+                              selected:
+                                type: boolean
+                        index:
+                          type: integer
+                          description: Seemingly identical to order, but an integer instead of a string
+                        selected:
+                          type: boolean
+                  available:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        id:
+                          type: string
+                          description: Unique ID that will be added to the navigation element's `id` property in the DOM
+                        route:
+                          type: string
+                          description: Relative URL to the page the navigation item goes to
+                        title:
+                          type: string
+                          description: Tooltip text
+                        enabled:
+                          type: boolean
+                        iconClass:
+                          type: string
+                          description: A FontAwesome icon string
+                        textClass:
+                          type: string
+                          description: HTML class applied to the text label for this navigation item
+                        text:
+                          type: string
+                          description: Label text for this navigation item
+                        core:
+                          type: boolean
+                          description: Whether the navigation item is provided by core or not (a plugin)
+                        groups:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              name:
+                                type: string
+                              displayName:
+                                type: string
+                        properties:
+                          type: object
+                          properties:
+                            targetBlank:
+                              type: boolean
+                  groups:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        displayName:
+                          type: string
+                  navigation:
+                    type: array
+                    description: A clone of `enabled`
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/post.yaml b/public/openapi/read/admin/settings/post.yaml
new file mode 100644
index 0000000000..f8273c8b8c
--- /dev/null
+++ b/public/openapi/read/admin/settings/post.yaml
@@ -0,0 +1,18 @@
+get:
+  tags:
+    - admin
+  summary: Get post settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  groupsExemptFromPostQueue:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/GroupObject.yaml#/GroupDataObject
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/social.yaml b/public/openapi/read/admin/settings/social.yaml
new file mode 100644
index 0000000000..7d32a17064
--- /dev/null
+++ b/public/openapi/read/admin/settings/social.yaml
@@ -0,0 +1,28 @@
+get:
+  tags:
+    - admin
+  summary: Get post social sharing settings
+  responses:
+    "200":
+      description: "A JSON object containing post social sharing settings"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  posts:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        id:
+                          type: string
+                        name:
+                          type: string
+                        class:
+                          type: string
+                          description: A FontAwesome icon string
+                        activated:
+                          type: boolean
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/term.yaml b/public/openapi/read/admin/settings/term.yaml
new file mode 100644
index 0000000000..1801041776
--- /dev/null
+++ b/public/openapi/read/admin/settings/term.yaml
@@ -0,0 +1,24 @@
+get:
+  tags:
+    - admin
+  summary: Get system settings
+  parameters:
+    - name: term
+      in: path
+      required: true
+      schema:
+        type: string
+      example: general
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties: {}
+                additionalProperties:
+                  type: object
+                  description: Most of the settings pages have their values loaded on the client-side, so the settings are not exposed server-side.
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/settings/user.yaml b/public/openapi/read/admin/settings/user.yaml
new file mode 100644
index 0000000000..6dccccedee
--- /dev/null
+++ b/public/openapi/read/admin/settings/user.yaml
@@ -0,0 +1,25 @@
+get:
+  tags:
+    - admin
+  summary: Get user settings
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  notificationSettings:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                          description: The notification type
+                        label:
+                          type: string
+                          description: The language key for the notification type (for localisation client-side)
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/admin/upload/file.yaml b/public/openapi/read/admin/upload/file.yaml
new file mode 100644
index 0000000000..7d600b76c4
--- /dev/null
+++ b/public/openapi/read/admin/upload/file.yaml
@@ -0,0 +1,35 @@
+post:
+  tags:
+    - admin
+  summary: Upload a file
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            folder:
+              type: string
+              description: The folder to upload the files to (relative to `public/uploads/`)
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "File uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded file for use client-side
\ No newline at end of file
diff --git a/public/openapi/read/admin/uploadDefaultAvatar.yaml b/public/openapi/read/admin/uploadDefaultAvatar.yaml
new file mode 100644
index 0000000000..1f725c8c0f
--- /dev/null
+++ b/public/openapi/read/admin/uploadDefaultAvatar.yaml
@@ -0,0 +1,32 @@
+post:
+  tags:
+    - admin
+  summary: Upload default avatar
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded image for use client-side
\ No newline at end of file
diff --git a/public/openapi/read/admin/uploadMaskableIcon.yaml b/public/openapi/read/admin/uploadMaskableIcon.yaml
new file mode 100644
index 0000000000..94640a74fa
--- /dev/null
+++ b/public/openapi/read/admin/uploadMaskableIcon.yaml
@@ -0,0 +1,32 @@
+post:
+  tags:
+    - admin
+  summary: Upload Maskable Icon
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded logo for the Maskable Icon entry for PWA / A2HS
\ No newline at end of file
diff --git a/public/openapi/read/admin/uploadOgImage.yaml b/public/openapi/read/admin/uploadOgImage.yaml
new file mode 100644
index 0000000000..9561a14b52
--- /dev/null
+++ b/public/openapi/read/admin/uploadOgImage.yaml
@@ -0,0 +1,32 @@
+post:
+  tags:
+    - admin
+  summary: Upload site-wide Open Graph Image
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded image for use client-side
\ No newline at end of file
diff --git a/public/openapi/read/admin/uploadTouchIcon.yaml b/public/openapi/read/admin/uploadTouchIcon.yaml
new file mode 100644
index 0000000000..d0c63ff107
--- /dev/null
+++ b/public/openapi/read/admin/uploadTouchIcon.yaml
@@ -0,0 +1,32 @@
+post:
+  tags:
+    - admin
+  summary: Upload Touch Icon
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded logo for the Homescreen/Touch Icon to enable PWA
\ No newline at end of file
diff --git a/public/openapi/read/admin/uploadfavicon.yaml b/public/openapi/read/admin/uploadfavicon.yaml
new file mode 100644
index 0000000000..d53b593fa4
--- /dev/null
+++ b/public/openapi/read/admin/uploadfavicon.yaml
@@ -0,0 +1,32 @@
+post:
+  tags:
+    - admin
+  summary: Upload favicon
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded image for use client-side
\ No newline at end of file
diff --git a/public/openapi/read/admin/uploadlogo.yaml b/public/openapi/read/admin/uploadlogo.yaml
new file mode 100644
index 0000000000..ef1ddd8f71
--- /dev/null
+++ b/public/openapi/read/admin/uploadlogo.yaml
@@ -0,0 +1,32 @@
+post:
+  tags:
+    - admin
+  summary: Upload site logo
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded image for use client-side
\ No newline at end of file
diff --git a/public/openapi/read/admin/users/csv.yaml b/public/openapi/read/admin/users/csv.yaml
new file mode 100644
index 0000000000..244a88beea
--- /dev/null
+++ b/public/openapi/read/admin/users/csv.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - admin
+  summary: Get users export (.csv)
+  parameters:
+    - in: header
+      name: referer
+      schema:
+        type: string
+      required: true
+      example: /admin/manage/users
+  responses:
+    "200":
+      description: "A CSV file containing all registered users"
+      content:
+        text/csv:
+          schema:
+            type: string
+            format: binary
\ No newline at end of file
diff --git a/public/openapi/read/career.yaml b/public/openapi/read/career.yaml
new file mode 100644
index 0000000000..fb222b0c0e
--- /dev/null
+++ b/public/openapi/read/career.yaml
@@ -0,0 +1,8 @@
+get:
+  tags:
+    - career
+  summary: Get career information
+  description: Returns career-related information for the career page
+  responses:
+    "200":
+      description: ""
diff --git a/public/openapi/read/categories.yaml b/public/openapi/read/categories.yaml
new file mode 100644
index 0000000000..74e2a9d486
--- /dev/null
+++ b/public/openapi/read/categories.yaml
@@ -0,0 +1,207 @@
+get:
+  tags:
+    - categories
+  summary: Get a list of categories
+  description: >
+    This route retrieve the list of categories currently available to the
+    accessing user. It doesn't necessarily mean that the user can *enter*
+    the category, as that is a separate privilege. Specifically, this route
+    will return all categories that grant the calling user the "Find
+    Category" privilege.
+
+
+    Subcategories are also returned, nested under a category's `children` property.
+  responses:
+    "200":
+      description: A list of category objects currently available to the accessing user
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  title:
+                    description: The page title
+                    type: string
+                  selectCategoryLabel:
+                    type: string
+                    description: Label to use for the category selector
+                  categories:
+                    description: A collection of category data objects
+                    type: array
+                    items:
+                      allOf:
+                        - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                        - type: object
+                          properties:
+                            tagWhitelist:
+                              type: array
+                              items:
+                                type: string
+                            unread-class:
+                              type: string
+                            children:
+                              type: array
+                              items:
+                                allOf:
+                                  - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                                  - type: object
+                                    properties:
+                                      tagWhitelist:
+                                        type: array
+                                        items:
+                                          type: string
+                                      unread-class:
+                                        type: string
+                                      children:
+                                        type: array
+                                        items:
+                                          $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                                      parent:
+                                        allOf:
+                                          - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                                          - type: object
+                                            properties:
+                                              tagWhitelist:
+                                                type: array
+                                                items:
+                                                  type: string
+                                              unread-class:
+                                                type: string
+                                      posts:
+                                        type: array
+                                        items:
+                                          type: object
+                                          properties:
+                                            pid:
+                                              type: number
+                                            timestamp:
+                                              type: number
+                                            content:
+                                              type: string
+                                            timestampISO:
+                                              type: string
+                                              description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                            user:
+                                              type: object
+                                              properties:
+                                                uid:
+                                                  type: number
+                                                  description: A user identifier
+                                                username:
+                                                  type: string
+                                                  description: A friendly name for a given user account
+                                                userslug:
+                                                  type: string
+                                                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                                    removed, etc.)
+                                                picture:
+                                                  nullable: true
+                                                  type: string
+                                                icon:text:
+                                                  type: string
+                                                  description: A single-letter representation of a username. This is used in the
+                                                    auto-generated icon given to
+                                                    users without an avatar
+                                                icon:bgColor:
+                                                  type: string
+                                                  description: A six-character hexadecimal colour code assigned to the user. This
+                                                    value is used in conjunction
+                                                    with `icon:text` for the user's
+                                                    auto-generated icon
+                                                  example: "#f44336"
+                                            index:
+                                              type: number
+                                            cid:
+                                              type: number
+                                              description: A category identifier
+                                            topic:
+                                              type: object
+                                              properties:
+                                                slug:
+                                                  type: string
+                                                title:
+                                                  type: string
+                                      imageClass:
+                                        type: string
+                                      timesClicked:
+                                        type: number
+                            posts:
+                              type: array
+                              items:
+                                type: object
+                                properties:
+                                  pid:
+                                    type: number
+                                  timestamp:
+                                    type: number
+                                  content:
+                                    type: string
+                                  timestampISO:
+                                    type: string
+                                    description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                  user:
+                                    type: object
+                                    properties:
+                                      uid:
+                                        type: number
+                                        description: A user identifier
+                                      username:
+                                        type: string
+                                        description: A friendly name for a given user account
+                                      displayname:
+                                        type: string
+                                        description: This is either username or fullname depending on forum and user settings
+                                      userslug:
+                                        type: string
+                                        description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                          removed, etc.)
+                                      picture:
+                                        nullable: true
+                                        type: string
+                                      icon:text:
+                                        type: string
+                                        description: A single-letter representation of a username. This is used in the
+                                          auto-generated icon given to users
+                                          without an avatar
+                                      icon:bgColor:
+                                        type: string
+                                        description: A six-character hexadecimal colour code assigned to the user. This
+                                          value is used in conjunction with
+                                          `icon:text` for the user's
+                                          auto-generated icon
+                                        example: "#f44336"
+                                  index:
+                                    type: number
+                                  cid:
+                                    type: number
+                                    description: A category identifier
+                                  topic:
+                                    type: object
+                                    properties:
+                                      slug:
+                                        type: string
+                                      title:
+                                        type: string
+                            teaser:
+                              type: object
+                              properties:
+                                url:
+                                  type: string
+                                timestampISO:
+                                  type: string
+                                  description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                pid:
+                                  type: number
+                                topic:
+                                  type: object
+                                  properties:
+                                    slug:
+                                      type: string
+                                    title:
+                                      type: string
+                            imageClass:
+                              type: string
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/categories/cid/moderators.yaml b/public/openapi/read/categories/cid/moderators.yaml
new file mode 100644
index 0000000000..b1116115f0
--- /dev/null
+++ b/public/openapi/read/categories/cid/moderators.yaml
@@ -0,0 +1,30 @@
+get:
+  tags:
+    - categories
+  summary: Get mods for a category
+  description: >
+    This route returns an array of uids that correspond to the moderators
+    for the category in question.
+  parameters:
+    - name: cid
+      description: The category identifier for the category you wish to look up
+      in: path
+      required: true
+      schema:
+        type: number
+      example: 1
+  responses:
+    "200":
+      description: An array of moderators for the requested category
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              moderators:
+                type: array
+          example:
+            moderators:
+              - 1
+              - 2
+              - 3
\ No newline at end of file
diff --git a/public/openapi/read/category/category_id.yaml b/public/openapi/read/category/category_id.yaml
new file mode 100644
index 0000000000..10a8523774
--- /dev/null
+++ b/public/openapi/read/category/category_id.yaml
@@ -0,0 +1,104 @@
+get:
+  tags:
+    - categories
+  summary: Get a single category
+  description: This route retrieves a single category's data, along with its children and the topics created inside of the category.
+  parameters:
+    - name: category_id
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+    - name: slug
+      description: This parameter is not required. If omitted, the request will be automatically redirected with the proper category slug.
+      in: path
+      required: true
+      schema:
+        type: string
+      example: test
+    - name: topic_index
+      description: This parameter is not required. If omitted, the request will presume that you want the first post. The API response is largely unaffected by this parameter, it is used client-side (to send the user to the requested post), and changes the meta/link tags in the server-side generated HTML.
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject
+              - type: object
+                properties:
+                  tagWhitelist:
+                    type: array
+                    items:
+                      type: string
+                  unread-class:
+                    type: string
+                  children:
+                    type: array
+                    items:
+                      $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../../components/schemas/TopicObject.yaml#/TopicObject
+                  nextStart:
+                    type: number
+                  isWatched:
+                    type: boolean
+                  isNotWatched:
+                    type: boolean
+                  isIgnored:
+                    type: boolean
+                  title:
+                    type: string
+                  selectCategoryLabel:
+                    type: string
+                    description: Label to use for the category selector
+                  privileges:
+                    type: object
+                    properties:
+                      topics:create:
+                        type: boolean
+                      topics:read:
+                        type: boolean
+                      topics:tag:
+                        type: boolean
+                      topics:schedule:
+                        type: boolean
+                      read:
+                        type: boolean
+                      posts:view_deleted:
+                        type: boolean
+                      cid:
+                        type: string
+                      uid:
+                        type: number
+                        description: A user identifier
+                      editable:
+                        type: boolean
+                      view_deleted:
+                        type: boolean
+                      isAdminOrMod:
+                        type: boolean
+                  showSelect:
+                    type: boolean
+                  showTopicTools:
+                    type: boolean
+                  topicIndex:
+                    type: number
+                  rssFeedUrl:
+                    type: string
+                  feeds:disableRSS:
+                    type: number
+                  reputation:disabled:
+                    type: number
+              - $ref: ../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/chats/roomid.yaml b/public/openapi/read/chats/roomid.yaml
new file mode 100644
index 0000000000..0780b774cb
--- /dev/null
+++ b/public/openapi/read/chats/roomid.yaml
@@ -0,0 +1,20 @@
+get:
+  tags:
+    - shorthand
+  summary: Access a chat room
+  description: Redirects a request to the proper chat page URL
+  parameters:
+    - name: roomid
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: "Chat identifier resolved"
+      content:
+        text/plain:
+          schema:
+            type: string
+            description: A relative path to the canonical URL for that chat page
\ No newline at end of file
diff --git a/public/openapi/read/config.yaml b/public/openapi/read/config.yaml
new file mode 100644
index 0000000000..2d8d2ff07b
--- /dev/null
+++ b/public/openapi/read/config.yaml
@@ -0,0 +1,148 @@
+get:
+  tags:
+    - home
+  summary: Get forum settings
+  description: This route retrieves forum settings and user-specific settings for client-side options on the forum.
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              relative_path:
+                type: string
+              upload_url:
+                type: string
+              assetBaseUrl:
+                type: string
+              siteTitle:
+                type: string
+              browserTitle:
+                type: string
+              titleLayout:
+                type: string
+              showSiteTitle:
+                type: boolean
+              maintenanceMode:
+                type: boolean
+              minimumTitleLength:
+                type: number
+              maximumTitleLength:
+                type: number
+              minimumPostLength:
+                type: number
+              maximumPostLength:
+                type: number
+              minimumTagsPerTopic:
+                type: number
+              maximumTagsPerTopic:
+                type: number
+              minimumTagLength:
+                type: number
+              undoTimeout:
+                type: number
+              maximumTagLength:
+                type: number
+              useOutgoingLinksPage:
+                type: boolean
+              allowGuestHandles:
+                type: boolean
+              allowTopicsThumbnail:
+                type: boolean
+              usePagination:
+                type: boolean
+              disableChat:
+                type: boolean
+              disableChatMessageEditing:
+                type: boolean
+              maximumChatMessageLength:
+                type: number
+              socketioTransports:
+                type: array
+                items:
+                  type: string
+              socketioOrigins:
+                type: string
+              websocketAddress:
+                type: string
+              maxReconnectionAttempts:
+                type: number
+              reconnectionDelay:
+                type: number
+              topicsPerPage:
+                type: number
+              postsPerPage:
+                type: number
+              maximumFileSize:
+                type: number
+              theme:id:
+                type: string
+              theme:src:
+                type: string
+              defaultLang:
+                type: string
+              userLang:
+                type: string
+              loggedIn:
+                type: boolean
+              uid:
+                type: number
+                description: A user identifier
+              cache-buster:
+                type: string
+              topicPostSort:
+                type: string
+              categoryTopicSort:
+                type: string
+              csrf_token:
+                type: string
+              searchEnabled:
+                type: boolean
+              bootswatchSkin:
+                type: string
+              enablePostHistory:
+                type: boolean
+              timeagoCutoff:
+                type: number
+              timeagoCodes:
+                type: array
+                items:
+                  type: string
+              cookies:
+                type: object
+                properties:
+                  enabled:
+                    type: boolean
+                  message:
+                    type: string
+                  dismiss:
+                    type: string
+                  link:
+                    type: string
+                  link_url:
+                    type: string
+              thumbs:
+                type: object
+                properties:
+                  size:
+                    type: number
+              acpLang:
+                type: string
+              openOutgoingLinksInNewTab:
+                type: boolean
+              topicSearchEnabled:
+                type: boolean
+              hideSubCategories:
+                type: boolean
+              hideCategoryLastPost:
+                type: boolean
+              enableQuickReply:
+                type: boolean
+              iconBackgrounds:
+                type: array
+                items:
+                  type: string
+                  description: A valid CSS colour code
+                  example: '#fff'
\ No newline at end of file
diff --git a/public/openapi/read/confirm/code.yaml b/public/openapi/read/confirm/code.yaml
new file mode 100644
index 0000000000..c3e75649b8
--- /dev/null
+++ b/public/openapi/read/confirm/code.yaml
@@ -0,0 +1,21 @@
+get:
+  tags:
+    - authentication
+  summary: Verify an email address
+  responses:
+    "200":
+      description: Email address verified, or confirmation code was incorrect
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  error:
+                    type: string
+                    description: Translation key for client-side localisation
+                required:
+                  - title
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/email/unsubscribe/token.yaml b/public/openapi/read/email/unsubscribe/token.yaml
new file mode 100644
index 0000000000..3b1c9deaf8
--- /dev/null
+++ b/public/openapi/read/email/unsubscribe/token.yaml
@@ -0,0 +1,60 @@
+get:
+  tags:
+    - emails
+  summary: Unsubscribe user from email type (user variant)
+  parameters:
+    - name: token
+      in: path
+      required: true
+      schema:
+        type: string
+      example: testToken
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  payload:
+                    type: object
+                    properties:
+                      uid:
+                        type: number
+                      template:
+                        type: string
+                        description: The type of email template to unsubscribe from.
+                        enum:
+                          - digest
+                          - notification
+                      type:
+                        type: string
+                        description: Only used if `template` is `notification`, signifies the type of notification to unsubscribe from.
+                        nullable: true
+                      iat:
+                        type: number
+                        description: Reflection of the token's "issued at" claim
+                    required:
+                      - uid
+                      - template
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
+    "500":
+      description: "Server-side error (likely token verification failure)"
+post:
+  tags:
+    - emails
+  summary: Unsubscribe user from email type (auto variant)
+  parameters:
+    - name: token
+      in: path
+      required: true
+      schema:
+        type: string
+      example: testToken
+  responses:
+    "200":
+      description: "Successfully unsubscribed"
+    "500":
+      description: "Server-side error (likely token verification failure)"
\ No newline at end of file
diff --git a/public/openapi/read/flags.yaml b/public/openapi/read/flags.yaml
new file mode 100644
index 0000000000..696a61afab
--- /dev/null
+++ b/public/openapi/read/flags.yaml
@@ -0,0 +1,76 @@
+get:
+  tags:
+    - flags
+  summary: Get flags list
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  flags:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        state:
+                          type: string
+                        heat:
+                          type: number
+                          description: The number of reports that make up this flag
+                        flagId:
+                          type: number
+                        type:
+                          type: string
+                        targetId:
+                          oneOf:
+                            - type: string
+                            - type: number
+                        targetUid:
+                          type: number
+                        datetime:
+                          type: number
+                        labelClass:
+                          type: string
+                        target_readable:
+                          type: string
+                        datetimeISO:
+                          type: string
+                        assignee:
+                          type: string
+                          nullable: true
+                  analytics:
+                    type: array
+                    items:
+                      type: number
+                  hasFilter:
+                    type: boolean
+                  filters:
+                    type: object
+                    properties:
+                      page:
+                        type: number
+                      perPage:
+                        type: number
+                  expanded:
+                    type: boolean
+                  sort:
+                    type: string
+                  title:
+                    type: string
+                  selectedCategory:
+                    type: object
+                    properties:
+                      icon:
+                        type: string
+                      name:
+                        type: string
+                      bgColor:
+                        type: string
+                    nullable: true
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/flags/flagId.yaml b/public/openapi/read/flags/flagId.yaml
new file mode 100644
index 0000000000..0939124937
--- /dev/null
+++ b/public/openapi/read/flags/flagId.yaml
@@ -0,0 +1,46 @@
+get:
+  tags:
+    - flags
+  summary: /api/flags/{flagId}
+  parameters:
+    - name: flagId
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../components/schemas/FlagObject.yaml#/FlagObject
+              - type: object
+                properties:
+                  type_path:
+                    type: string
+                  assignees:
+                    type: array
+                    items:
+                      $ref: ../../components/schemas/UserObject.yaml#/UserObject
+                  type_bool:
+                    type: object
+                    properties:
+                      post:
+                        type: boolean
+                      user:
+                        type: boolean
+                      empty:
+                        type: boolean
+                  title:
+                    type: string
+                  privileges:
+                    type: object
+                    properties: {}
+                    additionalProperties:
+                      description: "A list of global and admin privileges, and whether the calling user has (or has inherited) them"
+                      type: boolean
+              - $ref: ../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/groups.yaml b/public/openapi/read/groups.yaml
new file mode 100644
index 0000000000..8fc121708b
--- /dev/null
+++ b/public/openapi/read/groups.yaml
@@ -0,0 +1,109 @@
+get:
+  tags:
+    - groups
+  summary: Get user groups
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  groups:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        description:
+                          type: string
+                        hidden:
+                          type: number
+                        system:
+                          type: number
+                        userTitle:
+                          type: string
+                        userTitleEscaped:
+                          type: string
+                        icon:
+                          type: string
+                        labelColor:
+                          type: string
+                        createtime:
+                          type: number
+                        slug:
+                          type: string
+                        memberCount:
+                          type: number
+                        private:
+                          type: number
+                        userTitleEnabled:
+                          type: number
+                        disableJoinRequests:
+                          type: number
+                        disableLeave:
+                          type: number
+                        nameEncoded:
+                          type: string
+                        displayName:
+                          type: string
+                        textColor:
+                          type: string
+                        createtimeISO:
+                          type: string
+                        cover:thumb:url:
+                          type: string
+                        cover:url:
+                          type: string
+                        cover:position:
+                          type: string
+                        memberPostCids:
+                          type: string
+                        memberPostCidsArray:
+                          type: array
+                          items:
+                            type: number
+                            example: [1, 2, 3]
+                        members:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              uid:
+                                type: number
+                                description: A user identifier
+                              username:
+                                type: string
+                                description: A friendly name for a given user account
+                              picture:
+                                nullable: true
+                                type: string
+                              userslug:
+                                type: string
+                                description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                  removed, etc.)
+                              icon:text:
+                                type: string
+                                description: A single-letter representation of a username. This is used in the
+                                  auto-generated icon given to users without
+                                  an avatar
+                              icon:bgColor:
+                                type: string
+                                description: A six-character hexadecimal colour code assigned to the user. This
+                                  value is used in conjunction with
+                                  `icon:text` for the user's auto-generated
+                                  icon
+                                example: "#f44336"
+                        truncated:
+                          type: boolean
+                  allowGroupCreation:
+                    type: boolean
+                  nextStart:
+                    type: number
+                  title:
+                    type: string
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/groups/slug.yaml b/public/openapi/read/groups/slug.yaml
new file mode 100644
index 0000000000..d2ef318d8e
--- /dev/null
+++ b/public/openapi/read/groups/slug.yaml
@@ -0,0 +1,34 @@
+get:
+  tags:
+    - groups
+  summary: Get user group details
+  parameters:
+    - name: slug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: administrators
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  group:
+                    $ref: ../../components/schemas/GroupObject.yaml#/GroupFullObject
+                  posts:
+                    $ref: ../../components/schemas/PostsObject.yaml#/PostsObject
+                  isAdmin:
+                    type: boolean
+                  isGlobalMod:
+                    type: boolean
+                  allowPrivateGroups:
+                    type: number
+              - $ref: ../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/groups/slug/members.yaml b/public/openapi/read/groups/slug/members.yaml
new file mode 100644
index 0000000000..6f2f3d64a3
--- /dev/null
+++ b/public/openapi/read/groups/slug/members.yaml
@@ -0,0 +1,25 @@
+get:
+  tags:
+    - groups
+  summary: Get user group members
+  parameters:
+    - name: slug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: administrators
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  users:
+                    type: array
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
diff --git a/public/openapi/read/index.yaml b/public/openapi/read/index.yaml
new file mode 100644
index 0000000000..4e6c6e446b
--- /dev/null
+++ b/public/openapi/read/index.yaml
@@ -0,0 +1,208 @@
+get:
+  tags:
+    - home
+  description: >
+    This route is used to populate the homepage of NodeBB. It is the main
+    access point of the forum, and shows a list of categories for navigation
+    purposes.
+  summary: Get forum index data
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  title:
+                    type: string
+                    description: The page title
+                  selectCategoryLabel:
+                    type: string
+                    description: Label to use for the category selector
+                  categories:
+                    description: A collection of category data objects
+                    type: array
+                    items:
+                      allOf:
+                        - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                        - type: object
+                          properties:
+                            tagWhitelist:
+                              type: array
+                              items:
+                                type: string
+                            unread-class:
+                              type: string
+                            children:
+                              type: array
+                              items:
+                                allOf:
+                                  - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                                  - type: object
+                                    properties:
+                                      tagWhitelist:
+                                        type: array
+                                        items:
+                                          type: string
+                                      unread-class:
+                                        type: string
+                                      children:
+                                        type: array
+                                        items:
+                                          $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                                      parent:
+                                        allOf:
+                                          - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                                          - type: object
+                                            properties:
+                                              tagWhitelist:
+                                                type: array
+                                                items:
+                                                  type: string
+                                              unread-class:
+                                                type: string
+                                      posts:
+                                        type: array
+                                        items:
+                                          type: object
+                                          properties:
+                                            pid:
+                                              type: number
+                                            timestamp:
+                                              type: number
+                                            content:
+                                              type: string
+                                            timestampISO:
+                                              type: string
+                                              description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                            user:
+                                              type: object
+                                              properties:
+                                                uid:
+                                                  type: number
+                                                  description: A user identifier
+                                                username:
+                                                  type: string
+                                                  description: A friendly name for a given user account
+                                                displayname:
+                                                  type: string
+                                                  description: This is either username or fullname depending on forum and user settings
+                                                userslug:
+                                                  type: string
+                                                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                                    removed, etc.)
+                                                picture:
+                                                  nullable: true
+                                                  type: string
+                                                icon:text:
+                                                  type: string
+                                                  description: A single-letter representation of a username. This is used in the
+                                                    auto-generated icon given to
+                                                    users without an avatar
+                                                icon:bgColor:
+                                                  type: string
+                                                  description: A six-character hexadecimal colour code assigned to the user. This
+                                                    value is used in conjunction
+                                                    with `icon:text` for the user's
+                                                    auto-generated icon
+                                                  example: "#f44336"
+                                            index:
+                                              type: number
+                                            cid:
+                                              type: number
+                                              description: A category identifier
+                                            parentCid:
+                                              type: number
+                                              description: The category identifier for the category that is the immediate
+                                                ancestor of the current category
+                                            topic:
+                                              type: object
+                                              properties:
+                                                slug:
+                                                  type: string
+                                                title:
+                                                  type: string
+                                      imageClass:
+                                        type: string
+                                      timesClicked:
+                                        type: number
+                            posts:
+                              type: array
+                              items:
+                                type: object
+                                properties:
+                                  pid:
+                                    type: number
+                                  timestamp:
+                                    type: number
+                                  content:
+                                    type: string
+                                  timestampISO:
+                                    type: string
+                                    description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                  user:
+                                    type: object
+                                    properties:
+                                      uid:
+                                        type: number
+                                        description: A user identifier
+                                      username:
+                                        type: string
+                                        description: A friendly name for a given user account
+                                      displayname:
+                                        type: string
+                                        description: This is either username or fullname depending on forum and user settings
+                                      userslug:
+                                        type: string
+                                        description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                          removed, etc.)
+                                      picture:
+                                        nullable: true
+                                        type: string
+                                      icon:text:
+                                        type: string
+                                        description: A single-letter representation of a username. This is used in the
+                                          auto-generated icon given to users
+                                          without an avatar
+                                      icon:bgColor:
+                                        type: string
+                                        description: A six-character hexadecimal colour code assigned to the user. This
+                                          value is used in conjunction with
+                                          `icon:text` for the user's
+                                          auto-generated icon
+                                        example: "#f44336"
+                                  index:
+                                    type: number
+                                  cid:
+                                    type: number
+                                    description: A category identifier
+                                  topic:
+                                    type: object
+                                    properties:
+                                      slug:
+                                        type: string
+                                      title:
+                                        type: string
+                            teaser:
+                              type: object
+                              properties:
+                                url:
+                                  type: string
+                                timestampISO:
+                                  type: string
+                                  description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                pid:
+                                  type: number
+                                topic:
+                                  type: object
+                                  properties:
+                                    slug:
+                                      type: string
+                                    title:
+                                      type: string
+                            imageClass:
+                              type: string
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/ip-blacklist.yaml b/public/openapi/read/ip-blacklist.yaml
new file mode 100644
index 0000000000..332d2fc1d6
--- /dev/null
+++ b/public/openapi/read/ip-blacklist.yaml
@@ -0,0 +1,7 @@
+get:
+  tags:
+    - admin
+  summary: Get IP blacklist settings
+  responses:
+    "418":
+      description: "TODO: A proper response needs to be added. It is not really a teapot | Copy response from corresponding admin route"
\ No newline at end of file
diff --git a/public/openapi/read/login.yaml b/public/openapi/read/login.yaml
new file mode 100644
index 0000000000..4e5b3944a9
--- /dev/null
+++ b/public/openapi/read/login.yaml
@@ -0,0 +1,58 @@
+get:
+  tags:
+    - authentication
+  summary: Log in a user
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  loginFormEntry:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        label:
+                          type: string
+                          description: A label for the added block
+                        html:
+                          type: string
+                          description: HTML to render on the login page
+                        styleName:
+                          type: string
+                          description: Custom identifier (value is added to `input[id]` and `label[for]`)
+                  alternate_logins:
+                    type: boolean
+                  authentication:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        callbackURL:
+                          type: string
+                        icon:
+                          type: string
+                        scope:
+                          type: string
+                        prompt:
+                          type: string
+                  allowRegistration:
+                    type: boolean
+                  allowLoginWith:
+                    type: string
+                  title:
+                    type: string
+                  allowPasswordReset:
+                    type: boolean
+                  allowLocalLogin:
+                    type: boolean
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/me.yaml b/public/openapi/read/me.yaml
new file mode 100644
index 0000000000..6c3febadfd
--- /dev/null
+++ b/public/openapi/read/me.yaml
@@ -0,0 +1,10 @@
+get:
+  tags:
+    - shorthand
+  summary: Access your own profile's sub-pages
+  description: >-
+    This shorthand is useful if you want to link to pages in your own account profile, but do not want (or have) the `userslug`. It is also especially useful as a
+    means to instruct users on how to do things, as you can easily redirect them to their own profile pages.
+  responses:
+    "200":
+      description: "Canonical URL to your requested profile page"
\ No newline at end of file
diff --git a/public/openapi/read/notifications.yaml b/public/openapi/read/notifications.yaml
new file mode 100644
index 0000000000..59acc0ad62
--- /dev/null
+++ b/public/openapi/read/notifications.yaml
@@ -0,0 +1,112 @@
+get:
+  tags:
+    - notifications
+  summary: Get notifications
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  notifications:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        bodyShort:
+                          type: string
+                        path:
+                          type: string
+                        nid:
+                          type: string
+                        from:
+                          type: number
+                        importance:
+                          type: number
+                        datetime:
+                          type: number
+                        datetimeISO:
+                          type: string
+                        user:
+                          type: object
+                          properties:
+                            username:
+                              type: string
+                              description: A friendly name for a given user account
+                            displayname:
+                              type: string
+                              description: This is either username or fullname depending on forum and user settings
+                            userslug:
+                              type: string
+                              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                removed, etc.)
+                            picture:
+                              type: string
+                              nullable: true
+                            uid:
+                              type: number
+                              description: A user identifier
+                            icon:text:
+                              type: string
+                              description: A single-letter representation of a username. This is used in the
+                                auto-generated icon given to users without
+                                an avatar
+                            icon:bgColor:
+                              type: string
+                              description: A six-character hexadecimal colour code assigned to the user. This
+                                value is used in conjunction with
+                                `icon:text` for the user's auto-generated
+                                icon
+                              example: "#f44336"
+                        image:
+                          type: string
+                          nullable: true
+                        read:
+                          type: boolean
+                        readClass:
+                          type: string
+                  filters:
+                    type: array
+                    items:
+                      type: object
+                      additionalProperties: {}
+                  regularFilters:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        filter:
+                          type: string
+                        selected:
+                          type: boolean
+                      required:
+                        - name
+                        - filter
+                  moderatorFilters:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        filter:
+                          type: string
+                  selectedFilter:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      filter:
+                        type: string
+                      selected:
+                        type: boolean
+                  title:
+                    type: string
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/outgoing.yaml b/public/openapi/read/outgoing.yaml
new file mode 100644
index 0000000000..ffde8242bf
--- /dev/null
+++ b/public/openapi/read/outgoing.yaml
@@ -0,0 +1,29 @@
+get:
+  tags:
+    - other
+  summary: Warn before navigating externally
+  parameters:
+    - in: query
+      name: url
+      schema:
+        type: string
+      description: URL of the page to warn the user about
+      example: https://example.org
+  description: This route presents a warning to a user notifying them that the page they are about to view is hosted externally. They then have the option of continuing onwards or going back to where they came from.
+  responses:
+    "200":
+      description: Warning page presented
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  outgoing:
+                    type: string
+                    description: Escaped URL of the page to navigate to
+                  title:
+                    description: The page title
+                    type: string
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/popular.yaml b/public/openapi/read/popular.yaml
new file mode 100644
index 0000000000..4d870249f6
--- /dev/null
+++ b/public/openapi/read/popular.yaml
@@ -0,0 +1,111 @@
+get:
+  tags:
+    - topics
+  summary: Get popular topics
+  description: Returns a list of topics sorted by most replies. In an event of a
+    tie breaker, the topic with the most views. Can be filtered by All Time,
+    Day, Week, or Month.
+  responses:
+    "200":
+      description: An array of topic objects sorted by most replies and views.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  nextStart:
+                    type: number
+                  topicCount:
+                    type: number
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../components/schemas/TopicObject.yaml#/TopicObject
+                  tids:
+                    type: array
+                    items:
+                      type: number
+                  canPost:
+                    type: boolean
+                  showSelect:
+                    type: boolean
+                  showTopicTools:
+                    type: boolean
+                  allCategoriesUrl:
+                    type: string
+                  selectedCategory:
+                    type: object
+                    properties:
+                      icon:
+                        type: string
+                      name:
+                        type: string
+                      bgColor:
+                        type: string
+                    nullable: true
+                  selectedCids:
+                    type: array
+                    items:
+                      type: number
+                  feeds:disableRSS:
+                    type: number
+                  rssFeedUrl:
+                    type: string
+                  title:
+                    type: string
+                  filters:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        selected:
+                          type: boolean
+                        filter:
+                          type: string
+                        icon:
+                          type: string
+                  selectedFilter:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      url:
+                        type: string
+                      selected:
+                        type: boolean
+                      filter:
+                        type: string
+                      icon:
+                        type: string
+                  terms:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        selected:
+                          type: boolean
+                        term:
+                          type: string
+                  selectedTerm:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      url:
+                        type: string
+                      selected:
+                        type: boolean
+                      term:
+                        type: string
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/post-queue.yaml b/public/openapi/read/post-queue.yaml
new file mode 100644
index 0000000000..2edffc67d5
--- /dev/null
+++ b/public/openapi/read/post-queue.yaml
@@ -0,0 +1,167 @@
+get:
+  tags:
+    - admin
+  summary: Get flag data
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  allCategoriesUrl:
+                    type: string
+                  selectedCategory:
+                    type: object
+                    properties:
+                      icon:
+                        type: string
+                      name:
+                        type: string
+                      bgColor:
+                        type: string
+                    nullable: true
+                  selectedCids:
+                    type: array
+                    items:
+                      type: number
+                  posts:
+                    type: array
+                    items:
+                      allOf:
+                        - type: object
+                          properties:
+                            id:
+                              type: string
+                            uid:
+                              type: number
+                              description: A user identifier
+                            type:
+                              type: string
+                            data:
+                              type: object
+                              properties:
+                                title:
+                                  type: string
+                                content:
+                                  type: string
+                                thumb:
+                                  type: string
+                                cid:
+                                  oneOf:
+                                    - type: number
+                                    - type: string
+                                tags:
+                                  type: array
+                                  items: {}
+                                uid:
+                                  type: number
+                                  description: A user identifier
+                                req:
+                                  type: object
+                                  properties:
+                                    uid:
+                                      type: number
+                                      description: A user identifier
+                                    ip:
+                                      type: string
+                                    host:
+                                      type: string
+                                    protocol:
+                                      type: string
+                                    secure:
+                                      type: boolean
+                                    url:
+                                      type: string
+                                    path:
+                                      type: string
+                                    headers:
+                                      type: object
+                                      properties:
+                                        x-real-ip:
+                                          type: string
+                                        x-forwarded-for:
+                                          type: string
+                                        x-forwarded-proto:
+                                          type: string
+                                        host:
+                                          type: string
+                                        x-nginx-proxy:
+                                          type: string
+                                        connection:
+                                          type: string
+                                        accept:
+                                          type: string
+                                        user-agent:
+                                          type: string
+                                        sec-fetch-site:
+                                          type: string
+                                        sec-fetch-mode:
+                                          type: string
+                                        referer:
+                                          type: string
+                                        accept-encoding:
+                                          type: string
+                                        accept-language:
+                                          type: string
+                                        cookie:
+                                          type: string
+                                timestamp:
+                                  type: number
+                                fromQueue:
+                                  type: boolean
+                                timestampISO:
+                                  type: string
+                                  description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                rawContent:
+                                  type: string
+                                tid:
+                                  type: number
+                                  description: A topic identifier
+                                toPid:
+                                  nullable: true
+                            user:
+                              type: object
+                              properties:
+                                username:
+                                  type: string
+                                  description: A friendly name for a given user account
+                                userslug:
+                                  type: string
+                                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                    removed, etc.)
+                                picture:
+                                  nullable: true
+                                  type: string
+                                uid:
+                                  type: number
+                                  description: A user identifier
+                                icon:text:
+                                  type: string
+                                  description: A single-letter representation of a username. This is used in the
+                                    auto-generated icon given to users without
+                                    an avatar
+                                icon:bgColor:
+                                  type: string
+                                  description: A six-character hexadecimal colour code assigned to the user. This
+                                    value is used in conjunction with
+                                    `icon:text` for the user's auto-generated
+                                    icon
+                                  example: "#f44336"
+                            topic:
+                              type: object
+                              properties:
+                                cid:
+                                  type: number
+                                title:
+                                  type: string
+                                titleRaw:
+                                  type: string
+                        - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/post/pid.yaml b/public/openapi/read/post/pid.yaml
new file mode 100644
index 0000000000..6d294895d5
--- /dev/null
+++ b/public/openapi/read/post/pid.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - shorthand
+  summary: Access a specific post
+  description: This route comes in handy when all you have is the `pid`, and you want to redirect users to the canonical URL for the topic, with the appropriate topic slug and post index.
+  parameters:
+    - name: pid
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: "Canonical URL of topic"
+      content:
+        text/plain:
+          schema:
+            type: string
\ No newline at end of file
diff --git a/public/openapi/read/post/upload.yaml b/public/openapi/read/post/upload.yaml
new file mode 100644
index 0000000000..6b4c6438dc
--- /dev/null
+++ b/public/openapi/read/post/upload.yaml
@@ -0,0 +1,25 @@
+post:
+  tags:
+    - posts
+  summary: Upload a file to a specific post
+  description: Provided by NodeBB core and used mainly by the composer, this route allows you to upload an image or file to a post.
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  images:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
\ No newline at end of file
diff --git a/public/openapi/read/recent.yaml b/public/openapi/read/recent.yaml
new file mode 100644
index 0000000000..4e019cf157
--- /dev/null
+++ b/public/openapi/read/recent.yaml
@@ -0,0 +1,109 @@
+get:
+  tags:
+    - topics
+  summary: Get recent topics
+  description: Returns a list of topics sorted by timestamp.
+  responses:
+    "200":
+      description: An array of topic objects sorted by timestamp.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  nextStart:
+                    type: number
+                  topicCount:
+                    type: number
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../components/schemas/TopicObject.yaml#/TopicObject
+                  tids:
+                    type: array
+                    items:
+                      type: number
+                  canPost:
+                    type: boolean
+                  showSelect:
+                    type: boolean
+                  showTopicTools:
+                    type: boolean
+                  allCategoriesUrl:
+                    type: string
+                  selectedCategory:
+                    type: object
+                    properties:
+                      icon:
+                        type: string
+                      name:
+                        type: string
+                      bgColor:
+                        type: string
+                    nullable: true
+                  selectedCids:
+                    type: array
+                    items:
+                      type: number
+                  feeds:disableRSS:
+                    type: number
+                  rssFeedUrl:
+                    type: string
+                  title:
+                    type: string
+                  filters:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        selected:
+                          type: boolean
+                        filter:
+                          type: string
+                        icon:
+                          type: string
+                  selectedFilter:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      url:
+                        type: string
+                      selected:
+                        type: boolean
+                      filter:
+                        type: string
+                      icon:
+                        type: string
+                  terms:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        selected:
+                          type: boolean
+                        term:
+                          type: string
+                  selectedTerm:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      url:
+                        type: string
+                      selected:
+                        type: boolean
+                      term:
+                        type: string
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/recent/posts/term.yaml b/public/openapi/read/recent/posts/term.yaml
new file mode 100644
index 0000000000..ae5fdec7a6
--- /dev/null
+++ b/public/openapi/read/recent/posts/term.yaml
@@ -0,0 +1,25 @@
+get:
+  tags:
+    - posts
+  summary: Get recent posts
+  parameters:
+    - name: term
+      in: path
+      required: true
+      schema:
+        type: string
+      example: day
+      description: term is used to limit the returned posts to a specific term. Valid values are day, week, month. If you don't pass it in all posts will be returned.
+    - name: page
+      in: query
+      required: false
+      schema:
+        type: number
+      example: page=1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
\ No newline at end of file
diff --git a/public/openapi/read/register.yaml b/public/openapi/read/register.yaml
new file mode 100644
index 0000000000..b4ec6d4fbc
--- /dev/null
+++ b/public/openapi/read/register.yaml
@@ -0,0 +1,57 @@
+get:
+  tags:
+    - authentication
+  summary: Register a new user
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  register_window:spansize:
+                    type: string
+                  alternate_logins:
+                    type: boolean
+                  authentication:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        callbackURL:
+                          type: string
+                        icon:
+                          type: string
+                        scope:
+                          type: string
+                        prompt:
+                          type: string
+                  minimumUsernameLength:
+                    type: number
+                  maximumUsernameLength:
+                    type: number
+                  minimumPasswordLength:
+                    type: number
+                  minimumPasswordStrength:
+                    type: number
+                  regFormEntry:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        label:
+                          type: string
+                        html:
+                          type: string
+                        styleName:
+                          type: string
+                  title:
+                    type: string
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/register/complete.yaml b/public/openapi/read/register/complete.yaml
new file mode 100644
index 0000000000..d6f8e48a35
--- /dev/null
+++ b/public/openapi/read/register/complete.yaml
@@ -0,0 +1,30 @@
+get:
+  tags:
+    - authentication
+  summary: Complete a user's registration
+  responses:
+    "302":
+      description: If there are no additional registration steps to complete, then the user is redirected back to the registration page (`/register`)
+      headers:
+        Location:
+          schema:
+            type: string
+            example: /register
+    "200":
+      description: ''
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  errors:
+                    type: array
+                    items: {}
+                  sections:
+                    type: array
+                    items:
+                      type: string
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/registration-queue.yaml b/public/openapi/read/registration-queue.yaml
new file mode 100644
index 0000000000..dc44e9d353
--- /dev/null
+++ b/public/openapi/read/registration-queue.yaml
@@ -0,0 +1,7 @@
+get:
+  tags:
+    - admin
+  summary: Get registration queue
+  responses:
+    "418":
+      description: "TODO: A proper response needs to be added. It is not really a teapot | Copy response from corresponding admin route"
\ No newline at end of file
diff --git a/public/openapi/read/reset.yaml b/public/openapi/read/reset.yaml
new file mode 100644
index 0000000000..d8883c1682
--- /dev/null
+++ b/public/openapi/read/reset.yaml
@@ -0,0 +1,20 @@
+get:
+  tags:
+    - authentication
+  summary: Get user password reset (step 1)
+  responses:
+    "200":
+      description: "A JSON object containing the 1st step of the user password reset flow"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  code:
+                    type: string
+                    nullable: true
+                  title:
+                    type: string
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/reset/code.yaml b/public/openapi/read/reset/code.yaml
new file mode 100644
index 0000000000..7c0670bf1e
--- /dev/null
+++ b/public/openapi/read/reset/code.yaml
@@ -0,0 +1,32 @@
+get:
+  tags:
+    - authentication
+  summary: Get user password reset (step 2)
+  parameters:
+    - name: code
+      in: path
+      required: true
+      schema:
+        type: string
+      example: testCode
+  responses:
+    "200":
+      description: "A JSON object containing the 2nd step of the user password reset flow"
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  valid:
+                    type: boolean
+                  code:
+                    type: string
+                  minimumPasswordLength:
+                    type: number
+                  minimumPasswordStrength:
+                    type: number
+                  title:
+                    type: string
+              - $ref: ../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/search.yaml b/public/openapi/read/search.yaml
new file mode 100644
index 0000000000..9ada567f59
--- /dev/null
+++ b/public/openapi/read/search.yaml
@@ -0,0 +1,77 @@
+get:
+  tags:
+    - search
+  summary: Get search results
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../components/schemas/PostsObject.yaml#/PostsObject
+                  matchCount:
+                    type: number
+                  pageCount:
+                    type: number
+                  time:
+                    type: string
+                  multiplePages:
+                    type: boolean
+                  search_query:
+                    type: string
+                  term:
+                    type: string
+                  allCategories:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        value:
+                          oneOf:
+                            - type: string
+                            - type: number
+                        text:
+                          type: string
+                  allCategoriesCount:
+                    type: number
+                  expandSearch:
+                    type: boolean
+                  showAsPosts:
+                    type: boolean
+                  showAsTopics:
+                    type: boolean
+                  title:
+                    type: string
+                  searchDefaultSortBy:
+                    type: string
+                  privileges:
+                    type: object
+                    properties:
+                      search:users:
+                        type: boolean
+                      search:content:
+                        type: boolean
+                      search:tags:
+                        type: boolean
+                required:
+                  - posts
+                  - matchCount
+                  - pageCount
+                  - time
+                  - multiplePages
+                  - search_query
+                  - allCategories
+                  - allCategoriesCount
+                  - expandSearch
+                  - showAsPosts
+                  - showAsTopics
+                  - title
+                  - searchDefaultSortBy
+                  - permissions
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/self.yaml b/public/openapi/read/self.yaml
new file mode 100644
index 0000000000..e3979709cf
--- /dev/null
+++ b/public/openapi/read/self.yaml
@@ -0,0 +1,12 @@
+get:
+  tags:
+    - shorthand
+  summary: Access your profile data
+  description: This shorthand returns the data for the logged in user, identical to the data returned at this route /user/
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../components/schemas/UserObject.yaml#/UserObjectFull
\ No newline at end of file
diff --git a/public/openapi/read/tags.yaml b/public/openapi/read/tags.yaml
new file mode 100644
index 0000000000..05f407ef18
--- /dev/null
+++ b/public/openapi/read/tags.yaml
@@ -0,0 +1,27 @@
+get:
+  tags:
+    - tags
+  summary: Get tags
+  description: Returns a list of tags sorted by the most topics
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  tags:
+                    type: array
+                    description: An array of tags sorted by the most topics
+                    items:
+                      $ref: ../components/schemas/TagObject.yaml#/TagObject
+                  displayTagSearch:
+                    type: boolean
+                  nextStart:
+                    type: number
+                  title:
+                    type: string
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/tags/tag.yaml b/public/openapi/read/tags/tag.yaml
new file mode 100644
index 0000000000..41557d7c61
--- /dev/null
+++ b/public/openapi/read/tags/tag.yaml
@@ -0,0 +1,261 @@
+get:
+  tags:
+    - tags
+  summary: Get tag data
+  description: Returns a list of topics that are tagged with {tag}
+  parameters:
+    - name: tag
+      description: The tag used to retrieve the topics
+      in: path
+      required: true
+      schema:
+        type: string
+      example: test
+    - name: page
+      description: Page number used in pagination
+      in: query
+      required: false
+      schema:
+        type: number
+      example: ''
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  topics:
+                    type: array
+                    description: An array of topics that are all tagged with {tag}
+                    items:
+                      type: object
+                      properties:
+                        tid:
+                          type: number
+                          description: A topic identifier
+                        uid:
+                          type: number
+                          description: A user identifier
+                        cid:
+                          type: number
+                          description: A category identifier
+                        mainPid:
+                          type: number
+                          description: The post id of the first post in this topic (also called the
+                            "original post")
+                        title:
+                          type: string
+                        slug:
+                          type: string
+                        timestamp:
+                          type: number
+                        lastposttime:
+                          type: number
+                        postcount:
+                          type: number
+                        viewcount:
+                          type: number
+                        teaserPid:
+                          oneOf:
+                            - type: number
+                            - type: string
+                        deleted:
+                          type: number
+                        locked:
+                          type: number
+                        pinned:
+                          type: number
+                          description: Whether or not this particular topic is pinned to the top of the
+                            category
+                        upvotes:
+                          type: number
+                        downvotes:
+                          type: number
+                        titleRaw:
+                          type: string
+                        timestampISO:
+                          type: string
+                          description: An ISO 8601 formatted date string (complementing `timestamp`)
+                        lastposttimeISO:
+                          type: string
+                        votes:
+                          type: number
+                        category:
+                          type: object
+                          properties:
+                            cid:
+                              type: number
+                              description: A category identifier
+                            name:
+                              type: string
+                            slug:
+                              type: string
+                            icon:
+                              type: string
+                            image:
+                              nullable: true
+                            imageClass:
+                              nullable: true
+                              type: string
+                            bgColor:
+                              type: string
+                            color:
+                              type: string
+                            disabled:
+                              type: number
+                        user:
+                          type: object
+                          properties:
+                            uid:
+                              type: number
+                              description: A user identifier
+                            username:
+                              type: string
+                              description: A friendly name for a given user account
+                            userslug:
+                              type: string
+                              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                removed, etc.)
+                            reputation:
+                              type: number
+                            postcount:
+                              type: number
+                            picture:
+                              nullable: true
+                              type: string
+                            signature:
+                              nullable: true
+                              type: string
+                            banned:
+                              type: number
+                            status:
+                              type: string
+                            icon:text:
+                              type: string
+                              description: A single-letter representation of a username. This is used in the
+                                auto-generated icon given to users without
+                                an avatar
+                            icon:bgColor:
+                              type: string
+                              description: A six-character hexadecimal colour code assigned to the user. This
+                                value is used in conjunction with
+                                `icon:text` for the user's auto-generated
+                                icon
+                              example: "#f44336"
+                            banned_until_readable:
+                              type: string
+                            fullname:
+                              type: string
+                        teaser:
+                          type: object
+                          properties:
+                            pid:
+                              type: number
+                            uid:
+                              type: number
+                              description: A user identifier
+                            timestamp:
+                              type: number
+                            tid:
+                              type: number
+                              description: A topic identifier
+                            content:
+                              type: string
+                            timestampISO:
+                              type: string
+                              description: An ISO 8601 formatted date string (complementing `timestamp`)
+                            user:
+                              type: object
+                              properties:
+                                uid:
+                                  type: number
+                                  description: A user identifier
+                                username:
+                                  type: string
+                                  description: A friendly name for a given user account
+                                userslug:
+                                  type: string
+                                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                    removed, etc.)
+                                picture:
+                                  nullable: true
+                                  type: string
+                                icon:text:
+                                  type: string
+                                  description: A single-letter representation of a username. This is used in the
+                                    auto-generated icon given to users
+                                    without an avatar
+                                icon:bgColor:
+                                  type: string
+                                  description: A six-character hexadecimal colour code assigned to the user. This
+                                    value is used in conjunction with
+                                    `icon:text` for the user's
+                                    auto-generated icon
+                                  example: "#f44336"
+                            index:
+                              type: number
+                        tags:
+                          type: array
+                          items:
+                            $ref: ../../components/schemas/TagObject.yaml#/TagObject
+                        isOwner:
+                          type: boolean
+                        ignored:
+                          type: boolean
+                        unread:
+                          type: boolean
+                        bookmark:
+                          nullable: true
+                        unreplied:
+                          type: boolean
+                        icons:
+                          type: array
+                          items: {}
+                        index:
+                          type: number
+                        thumb:
+                          type: string
+                        isQuestion:
+                          nullable: true
+                          type: number
+                        isSolved:
+                          type: number
+                  tag:
+                    type: string
+                  title:
+                    type: string
+                  showSelect:
+                    type: boolean
+                  showTopicTools:
+                    type: boolean
+                  allCategoriesUrl:
+                    type: string
+                  selectedCategory:
+                    type: object
+                    properties:
+                      icon:
+                        type: string
+                      name:
+                        type: string
+                      bgColor:
+                        type: string
+                    nullable: true
+                  selectedCids:
+                    type: array
+                    items:
+                      type: number
+                  rssFeedUrl:
+                    type: string
+                  feeds:disableRSS:
+                    type: boolean
+                required:
+                  - topics
+                  - tag
+                  - title
+                  - categories
+              - $ref: ../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/top.yaml b/public/openapi/read/top.yaml
new file mode 100644
index 0000000000..e9b552ffcf
--- /dev/null
+++ b/public/openapi/read/top.yaml
@@ -0,0 +1,122 @@
+get:
+  tags:
+    - topics
+  summary: Get top topics
+  description: Returns a list of topics sorted by most upvotes.
+  responses:
+    "200":
+      description: An array of topic objects sorted by most upvotes.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  nextStart:
+                    type: number
+                  topicCount:
+                    type: number
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../components/schemas/TopicObject.yaml#/TopicObject
+                  tids:
+                    type: array
+                    items:
+                      type: number
+                  canPost:
+                    type: boolean
+                  showSelect:
+                    type: boolean
+                  showTopicTools:
+                    type: boolean
+                  allCategoriesUrl:
+                    type: string
+                  selectedCategory:
+                    type: object
+                    properties:
+                      cid:
+                        type: number
+                        description: A category identifier
+                      name:
+                        type: string
+                      level:
+                        type: string
+                      icon:
+                        type: string
+                      parentCid:
+                        type: number
+                        description: The category identifier for the category that is the immediate
+                          ancestor of the current category
+                      color:
+                        type: string
+                      bgColor:
+                        type: string
+                      selected:
+                        type: boolean
+                    nullable: true
+                  selectedCids:
+                    type: array
+                    items:
+                      type: number
+                  feeds:disableRSS:
+                    type: number
+                  rssFeedUrl:
+                    type: string
+                  title:
+                    type: string
+                  filters:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        selected:
+                          type: boolean
+                        filter:
+                          type: string
+                        icon:
+                          type: string
+                  selectedFilter:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      url:
+                        type: string
+                      selected:
+                        type: boolean
+                      filter:
+                        type: string
+                      icon:
+                        type: string
+                  terms:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        selected:
+                          type: boolean
+                        term:
+                          type: string
+                  selectedTerm:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      url:
+                        type: string
+                      selected:
+                        type: boolean
+                      term:
+                        type: string
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/topic/pagination/topic_id.yaml b/public/openapi/read/topic/pagination/topic_id.yaml
new file mode 100644
index 0000000000..4d91144cc4
--- /dev/null
+++ b/public/openapi/read/topic/pagination/topic_id.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - topics
+  summary: Get topic pagination data
+  description: This route retrieves pagination data for a given topic. It is used mainly client-side, as it return data necessary to update a pagination block client-side.
+  parameters:
+    - name: topic_id
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/Pagination.yaml#/Pagination
\ No newline at end of file
diff --git a/public/openapi/read/topic/teaser/topic_id.yaml b/public/openapi/read/topic/teaser/topic_id.yaml
new file mode 100644
index 0000000000..5b0450afa5
--- /dev/null
+++ b/public/openapi/read/topic/teaser/topic_id.yaml
@@ -0,0 +1,18 @@
+get:
+  tags:
+    - topics
+  summary: Get a topic's teaser post
+  parameters:
+    - name: topic_id
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: "A JSON object containing the teaser post for a topic"
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
\ No newline at end of file
diff --git a/public/openapi/read/topic/thumb/upload.yaml b/public/openapi/read/topic/thumb/upload.yaml
new file mode 100644
index 0000000000..74b096681e
--- /dev/null
+++ b/public/openapi/read/topic/thumb/upload.yaml
@@ -0,0 +1,35 @@
+post:
+  tags:
+    - topics
+  summary: Upload topic thumb
+  requestBody:
+    required: true
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+          required:
+            - files
+  responses:
+    "200":
+      description: "Image uploaded"
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              name:
+                type: string
+                description: The filename
+              url:
+                type: string
+                description: URL of the uploaded image for use client-side
+              path:
+                type: string
+                description: Path to the file in the local file system
\ No newline at end of file
diff --git a/public/openapi/read/topic/topic_id.yaml b/public/openapi/read/topic/topic_id.yaml
new file mode 100644
index 0000000000..4b890143f7
--- /dev/null
+++ b/public/openapi/read/topic/topic_id.yaml
@@ -0,0 +1,392 @@
+get:
+  tags:
+    - topics
+  summary: Get topic data
+  parameters:
+    - name: topic_id
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+    - name: slug
+      description: This parameter is not required. If omitted, the request will be automatically redirected with the proper topic slug.
+      in: path
+      required: true
+      schema:
+        type: string
+      example: test-topic
+    - name: post_index
+      description: This parameter is not required. If omitted, the request will presume that you want the first post. The API response is largely unaffected by this parameter, it is used client-side (to send the user to the requested post), and changes the meta/link tags in the server-side generated HTML.
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../components/schemas/TopicObject.yaml#/TopicObjectSlim
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  slug:
+                    type: string
+                  teaserPid:
+                    type: number
+                    nullable: true
+                  titleRaw:
+                    type: string
+                  tags:
+                    type: array
+                    items:
+                      $ref: ../../components/schemas/TagObject.yaml#/TagObject
+                  posts:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        pid:
+                          type: number
+                        uid:
+                          type: number
+                          description: A user identifier
+                        tid:
+                          type: number
+                          description: A topic identifier
+                        content:
+                          type: string
+                        timestamp:
+                          type: number
+                        votes:
+                          type: number
+                        deleted:
+                          type: number
+                        upvotes:
+                          type: number
+                        downvotes:
+                          type: number
+                        bookmarks:
+                          type: number
+                        deleterUid:
+                          type: number
+                        edited:
+                          type: number
+                        timestampISO:
+                          type: string
+                          description: An ISO 8601 formatted date string (complementing `timestamp`)
+                        editedISO:
+                          type: string
+                        index:
+                          type: number
+                        user:
+                          type: object
+                          properties:
+                            uid:
+                              type: number
+                              description: A user identifier
+                            username:
+                              type: string
+                              description: A friendly name for a given user account
+                            displayname:
+                              type: string
+                              description: This is either username or fullname depending on forum and user settings
+                            userslug:
+                              type: string
+                              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                removed, etc.)
+                            reputation:
+                              type: number
+                            postcount:
+                              type: number
+                            topiccount:
+                              type: number
+                            picture:
+                              type: string
+                              nullable: true
+                            signature:
+                              type: string
+                            banned:
+                              type: number
+                            banned:expire:
+                              type: number
+                            status:
+                              type: string
+                            lastonline:
+                              type: number
+                            groupTitle:
+                              nullable: true
+                              type: string
+                            groupTitleArray:
+                              type: array
+                              items:
+                                type: string
+                            icon:text:
+                              type: string
+                              description: A single-letter representation of a username. This is used in the
+                                auto-generated icon given to users without
+                                an avatar
+                            icon:bgColor:
+                              type: string
+                              description: A six-character hexadecimal colour code assigned to the user. This
+                                value is used in conjunction with
+                                `icon:text` for the user's auto-generated
+                                icon
+                              example: "#f44336"
+                            lastonlineISO:
+                              type: string
+                            banned_until:
+                              type: number
+                            banned_until_readable:
+                              type: string
+                            selectedGroups:
+                              type: array
+                              items:
+                                type: object
+                                properties:
+                                  name:
+                                    type: string
+                                  slug:
+                                    type: string
+                                  labelColor:
+                                    type: string
+                                  textColor:
+                                    type: string
+                                  icon:
+                                    type: string
+                                  userTitle:
+                                    type: string
+                            custom_profile_info:
+                              type: array
+                              items:
+                                type: object
+                                properties:
+                                  content:
+                                    type: string
+                                    description: HTML that is injected into `topic.tpl` of themes that support custom profile info
+                        editor:
+                          nullable: true
+                        bookmarked:
+                          type: boolean
+                        upvoted:
+                          type: boolean
+                        downvoted:
+                          type: boolean
+                        replies:
+                          type: object
+                          properties:
+                            hasMore:
+                              type: boolean
+                            users:
+                              type: array
+                              items:
+                                type: object
+                                properties:
+                                  username:
+                                    type: string
+                                    description: A friendly name for a given user account
+                                  userslug:
+                                    type: string
+                                    description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                      removed, etc.)
+                                  picture:
+                                    type: string
+                                  uid:
+                                    type: number
+                                    description: A user identifier
+                                  icon:text:
+                                    type: string
+                                    description: A single-letter representation of a username. This is used in the
+                                      auto-generated icon given to users without
+                                      an avatar
+                                  icon:bgColor:
+                                    type: string
+                                    description: A six-character hexadecimal colour code assigned to the user. This
+                                      value is used in conjunction with
+                                      `icon:text` for the user's auto-generated
+                                      icon
+                                    example: "#f44336"
+                                  administrator:
+                                    type: boolean
+                            text:
+                              type: string
+                            count:
+                              type: number
+                        selfPost:
+                          type: boolean
+                        topicOwnerPost:
+                          type: boolean
+                        display_edit_tools:
+                          type: boolean
+                        display_delete_tools:
+                          type: boolean
+                        display_moderator_tools:
+                          type: boolean
+                        display_move_tools:
+                          type: boolean
+                        display_post_menu:
+                          type: boolean
+                        flagId:
+                          type: number
+                          description: The flag identifier, if this particular post has been flagged before
+                  events:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        type:
+                          type: string
+                        id:
+                          type: number
+                        timestamp:
+                          type: number
+                        timestampISO:
+                          type: string
+                  category:
+                    $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject
+                  tagWhitelist:
+                    type: array
+                    items:
+                      type: string
+                  minTags:
+                    type: number
+                  maxTags:
+                    type: number
+                  thread_tools:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        class:
+                          type: string
+                        title:
+                          type: string
+                        icon:
+                          type: string
+                  isFollowing:
+                    type: boolean
+                  isNotFollowing:
+                    type: boolean
+                  isIgnoring:
+                    type: boolean
+                  bookmark:
+                    nullable: true
+                  postSharing:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        id:
+                          type: string
+                        name:
+                          type: string
+                        class:
+                          type: string
+                        activated:
+                          type: boolean
+                  deleter:
+                    nullable: true
+                  merger:
+                    nullable: true
+                  related:
+                    type: array
+                    items:
+                      $ref: ../../components/schemas/TopicObject.yaml#/TopicObject
+                  unreplied:
+                    type: boolean
+                  icons:
+                    type: array
+                    items:
+                      type: string
+                      description: HTML that is rendered by the theme
+                  privileges:
+                    type: object
+                    properties:
+                      topics:reply:
+                        type: boolean
+                      topics:read:
+                        type: boolean
+                      topics:tag:
+                        type: boolean
+                      topics:delete:
+                        type: boolean
+                      posts:edit:
+                        type: boolean
+                      posts:history:
+                        type: boolean
+                      posts:delete:
+                        type: boolean
+                      posts:view_deleted:
+                        type: boolean
+                      read:
+                        type: boolean
+                      purge:
+                        type: boolean
+                      view_thread_tools:
+                        type: boolean
+                      editable:
+                        type: boolean
+                      deletable:
+                        type: boolean
+                      view_deleted:
+                        type: boolean
+                      view_scheduled:
+                        type: boolean
+                      isAdminOrMod:
+                        type: boolean
+                      disabled:
+                        type: number
+                      tid:
+                        type: string
+                      uid:
+                        type: number
+                        description: A user identifier
+                  topicStaleDays:
+                    type: number
+                  reputation:disabled:
+                    type: number
+                  downvote:disabled:
+                    type: number
+                  feeds:disableRSS:
+                    type: number
+                  bookmarkThreshold:
+                    type: number
+                  necroThreshold:
+                    type: number
+                  postEditDuration:
+                    type: number
+                  postDeleteDuration:
+                    type: number
+                  scrollToMyPost:
+                    type: boolean
+                  updateUrlWithPostIndex:
+                    type: boolean
+                  allowMultipleBadges:
+                    type: boolean
+                  privateUploads:
+                    type: boolean
+                  rssFeedUrl:
+                    type: string
+                  postIndex:
+                    type: number
+                  loggedInUser:
+                    $ref: ../../components/schemas/UserObject.yaml#/UserObject
+              - type: object
+                description: Optional properties that may or may not be present (except for `tid`, which is always present, and is only here as a hack to pass validation)
+                properties:
+                  tid:
+                    type: number
+                    description: A topic identifier
+                  thumb:
+                    type: string
+                    description: An uploaded topic thumbnail
+                required:
+                  - tid
+              - $ref: ../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/tos.yaml b/public/openapi/read/tos.yaml
new file mode 100644
index 0000000000..1c66f4bdc9
--- /dev/null
+++ b/public/openapi/read/tos.yaml
@@ -0,0 +1,18 @@
+get:
+  tags:
+    - authentication
+  summary: Get forum terms of service
+  description: This route allows you to view the forum terms of service.
+  responses:
+    "200":
+      description: Terms of service retrieved.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  termsOfUse:
+                    type: string
+                    description: Full text of the configured terms of service/terms of use.
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/uid/uid.yaml b/public/openapi/read/uid/uid.yaml
new file mode 100644
index 0000000000..dccb23e01a
--- /dev/null
+++ b/public/openapi/read/uid/uid.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - shorthand
+  summary: Access a user's profile pages
+  description: >-
+    This particular shorthand is useful if you are looking to redirect to a user's profile (or other associated pages), but do not know or want to retrieve their userslug,
+    which is part of the canonical url.
+
+    For example, to go to `uid` 15's list of topics made, you can navigate to `/api/uid/15/topics`, which will send you to the appropriate canonical URL for that user's topics.
+  parameters:
+    - name: uid*
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: "Canonical URL of user profile page"
\ No newline at end of file
diff --git a/public/openapi/read/unread.yaml b/public/openapi/read/unread.yaml
new file mode 100644
index 0000000000..4d6ad5d902
--- /dev/null
+++ b/public/openapi/read/unread.yaml
@@ -0,0 +1,243 @@
+get:
+  tags:
+    - topics
+  summary: Get unread topics
+  description: Returns a list of the current user's unread topics, sorted by the
+    last post's timestamp.
+  responses:
+    "200":
+      description: An array of unread topic objects sorted by the last post's timestamp.
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  showSelect:
+                    type: boolean
+                  showTopicTools:
+                    type: boolean
+                  nextStart:
+                    type: number
+                  topics:
+                    type: array
+                    items:
+                      allOf:
+                        - $ref: ../components/schemas/TopicObject.yaml#/TopicObjectSlim
+                        - type: object
+                          properties:
+                            title:
+                              type: string
+                            slug:
+                              type: string
+                            teaserPid:
+                              type: number
+                              nullable: true
+                            titleRaw:
+                              type: string
+                            category:
+                              type: object
+                              properties:
+                                cid:
+                                  type: number
+                                  description: A category identifier
+                                name:
+                                  type: string
+                                slug:
+                                  type: string
+                                icon:
+                                  type: string
+                                backgroundImage:
+                                  nullable: true
+                                imageClass:
+                                  nullable: true
+                                  type: string
+                                bgColor:
+                                  type: string
+                                color:
+                                  type: string
+                                disabled:
+                                  type: number
+                            user:
+                              type: object
+                              properties:
+                                uid:
+                                  type: number
+                                  description: A user identifier
+                                username:
+                                  type: string
+                                  description: A friendly name for a given user account
+                                displayname:
+                                  type: string
+                                  description: This is either username or fullname depending on forum and user settings
+                                fullname:
+                                  type: string
+                                userslug:
+                                  type: string
+                                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                    removed, etc.)
+                                reputation:
+                                  type: number
+                                postcount:
+                                  type: number
+                                picture:
+                                  nullable: true
+                                  type: string
+                                signature:
+                                  nullable: true
+                                  type: string
+                                banned:
+                                  type: number
+                                status:
+                                  type: string
+                                icon:text:
+                                  type: string
+                                  description: A single-letter representation of a username. This is used in the
+                                    auto-generated icon given to users without
+                                    an avatar
+                                icon:bgColor:
+                                  type: string
+                                  description: A six-character hexadecimal colour code assigned to the user. This
+                                    value is used in conjunction with
+                                    `icon:text` for the user's auto-generated
+                                    icon
+                                  example: "#f44336"
+                                banned_until_readable:
+                                  type: string
+                              required:
+                                - uid
+                                - username
+                                - userslug
+                                - reputation
+                                - postcount
+                                - picture
+                                - signature
+                                - banned
+                                - status
+                                - icon:text
+                                - icon:bgColor
+                                - banned_until_readable
+                            teaser:
+                              type: object
+                              nullable: true
+                              properties:
+                                pid:
+                                  type: number
+                                uid:
+                                  type: number
+                                  description: A user identifier
+                                timestamp:
+                                  type: number
+                                tid:
+                                  type: number
+                                  description: A topic identifier
+                                content:
+                                  type: string
+                                timestampISO:
+                                  type: string
+                                  description: An ISO 8601 formatted date string (complementing `timestamp`)
+                                user:
+                                  type: object
+                                  properties:
+                                    uid:
+                                      type: number
+                                      description: A user identifier
+                                    username:
+                                      type: string
+                                      description: A friendly name for a given user account
+                                    userslug:
+                                      type: string
+                                      description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                        removed, etc.)
+                                    picture:
+                                      nullable: true
+                                      type: string
+                                    icon:text:
+                                      type: string
+                                      description: A single-letter representation of a username. This is used in the
+                                        auto-generated icon given to users
+                                        without an avatar
+                                    icon:bgColor:
+                                      type: string
+                                      description: A six-character hexadecimal colour code assigned to the user. This
+                                        value is used in conjunction with
+                                        `icon:text` for the user's
+                                        auto-generated icon
+                                      example: "#f44336"
+                                index:
+                                  type: number
+                            tags:
+                              type: array
+                              items:
+                                $ref: ../components/schemas/TagObject.yaml#/TagObject
+                            isOwner:
+                              type: boolean
+                            ignored:
+                              type: boolean
+                            unread:
+                              type: boolean
+                            bookmark:
+                              nullable: true
+                            unreplied:
+                              type: boolean
+                            icons:
+                              type: array
+                              items:
+                                type: string
+                            index:
+                              type: number
+                  topicCount:
+                    type: number
+                  title:
+                    type: string
+                  pageCount:
+                    type: number
+                  allCategoriesUrl:
+                    type: string
+                  selectedCategory:
+                    type: object
+                    properties:
+                      icon:
+                        type: string
+                      name:
+                        type: string
+                      bgColor:
+                        type: string
+                    nullable: true
+                  selectCategoryLabel:
+                    type: string
+                  selectedCids:
+                    type: array
+                    items:
+                      type: number
+                  filters:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                        selected:
+                          type: boolean
+                        filter:
+                          type: string
+                        icon:
+                          type: string
+                  selectedFilter:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      url:
+                        type: string
+                      selected:
+                        type: boolean
+                      filter:
+                        type: string
+                      icon:
+                        type: string
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/unread/total.yaml b/public/openapi/read/unread/total.yaml
new file mode 100644
index 0000000000..60a5a1871f
--- /dev/null
+++ b/public/openapi/read/unread/total.yaml
@@ -0,0 +1,11 @@
+get:
+  tags:
+    - topics
+  summary: Get number of unread topics
+  responses:
+    "200":
+      description: "Success"
+      content:
+        text/plain:
+          schema:
+            type: number
\ No newline at end of file
diff --git a/public/openapi/read/user/email/email.yaml b/public/openapi/read/user/email/email.yaml
new file mode 100644
index 0000000000..131a37fb20
--- /dev/null
+++ b/public/openapi/read/user/email/email.yaml
@@ -0,0 +1,21 @@
+get:
+  tags:
+    - users
+  summary: Get user by email
+  description: |
+    This route retrieves a user's public profile data. If the calling user is the same as the profile, then it will also return data the user elected to hide (e.g. email/fullname).
+    Additionally, this route will only return data if the calling user is an admin or global moderator, or if the end user has elected to make their email public. Otherwise, it will simply return a `404 Not Found`.
+  parameters:
+    - name: email
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 'test@example.org'
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/UserObject.yaml#/UserObject
\ No newline at end of file
diff --git a/public/openapi/read/user/uid/uid.yaml b/public/openapi/read/user/uid/uid.yaml
new file mode 100644
index 0000000000..c17bb997a5
--- /dev/null
+++ b/public/openapi/read/user/uid/uid.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - users
+  summary: Get user by uid
+  description: This route retrieves a user's public profile data. If the calling user is the same as the profile, then it will also return data the user elected to hide (e.g. email/fullname)
+  parameters:
+    - name: uid
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/UserObject.yaml#/UserObject
\ No newline at end of file
diff --git a/public/openapi/read/user/uid/userslug/export/type.yaml b/public/openapi/read/user/uid/userslug/export/type.yaml
new file mode 100644
index 0000000000..e0ea7d93d1
--- /dev/null
+++ b/public/openapi/read/user/uid/userslug/export/type.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - deprecated
+  summary: Export a user's posts/profile/uploads (.csv)
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "302":
+      description: A redirect to the new URL format (without the `/uid` prefix)
+      headers:
+        Location:
+          schema:
+            type: string
+            example: /api/user/admin/export/posts
\ No newline at end of file
diff --git a/public/openapi/read/user/username/username.yaml b/public/openapi/read/user/username/username.yaml
new file mode 100644
index 0000000000..7ef85ee379
--- /dev/null
+++ b/public/openapi/read/user/username/username.yaml
@@ -0,0 +1,19 @@
+get:
+  tags:
+    - users
+  summary: Get user by username
+  description: This route retrieves a user's public profile data. If the calling user is the same as the profile, then it will also return data the user elected to hide (e.g. email/fullname)
+  parameters:
+    - name: username
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            $ref: ../../../components/schemas/UserObject.yaml#/UserObject
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug.yaml b/public/openapi/read/user/userslug.yaml
new file mode 100644
index 0000000000..4858383cd3
--- /dev/null
+++ b/public/openapi/read/user/userslug.yaml
@@ -0,0 +1,86 @@
+get:
+  tags:
+    - users
+  summary: Get user profile
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../../components/schemas/PostsObject.yaml#/PostsObject
+                  latestPosts:
+                    $ref: ../../components/schemas/PostsObject.yaml#/PostsObject
+                  bestPosts:
+                    $ref: ../../components/schemas/PostsObject.yaml#/PostsObject
+                  hasPrivateChat:
+                    type: number
+                  title:
+                    type: string
+                  allowCoverPicture:
+                    type: boolean
+                  selectedGroup:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        slug:
+                          type: string
+                        createtime:
+                          type: number
+                        userTitle:
+                          type: string
+                        description:
+                          type: string
+                        memberCount:
+                          type: number
+                        deleted:
+                          type: string
+                        hidden:
+                          type: number
+                        system:
+                          type: number
+                        private:
+                          type: number
+                        ownerUid:
+                          type: number
+                        icon:
+                          type: string
+                        labelColor:
+                          type: string
+                        cover:url:
+                          type: string
+                        cover:position:
+                          type: string
+                        userTitleEnabled:
+                          type: number
+                        disableJoinRequests:
+                          type: number
+                        disableLeave:
+                          type: number
+                        nameEncoded:
+                          type: string
+                        displayName:
+                          type: string
+                        textColor:
+                          type: string
+                        createtimeISO:
+                          type: string
+                        cover:thumb:url:
+                          type: string
+              - $ref: ../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/best.yaml b/public/openapi/read/user/userslug/best.yaml
new file mode 100644
index 0000000000..657e54fdeb
--- /dev/null
+++ b/public/openapi/read/user/userslug/best.yaml
@@ -0,0 +1,45 @@
+get:
+  tags:
+    - users
+  summary: Get a user's best performing posts
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/blocks.yaml b/public/openapi/read/user/userslug/blocks.yaml
new file mode 100644
index 0000000000..d8147d9d64
--- /dev/null
+++ b/public/openapi/read/user/userslug/blocks.yaml
@@ -0,0 +1,30 @@
+get:
+  tags:
+    - users
+  summary: Get user's blocks
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  users:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/UserObject.yaml#/UserObjectSlim
+                  title:
+                    type: string
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/bookmarks.yaml b/public/openapi/read/user/userslug/bookmarks.yaml
new file mode 100644
index 0000000000..b820b15978
--- /dev/null
+++ b/public/openapi/read/user/userslug/bookmarks.yaml
@@ -0,0 +1,45 @@
+get:
+  tags:
+    - users
+  summary: Get user's bookmarks
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/categories.yaml b/public/openapi/read/user/userslug/categories.yaml
new file mode 100644
index 0000000000..20cc798400
--- /dev/null
+++ b/public/openapi/read/user/userslug/categories.yaml
@@ -0,0 +1,63 @@
+get:
+  tags:
+    - users
+  summary: Get user's watched categories
+  description: This route retrieves the list of categories and their watch states
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  categories:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        cid:
+                          type: number
+                          description: A category identifier
+                        name:
+                          type: string
+                        level:
+                          type: string
+                        icon:
+                          type: string
+                        parentCid:
+                          type: number
+                          description: The category identifier for the category that is the immediate
+                            ancestor of the current category
+                        color:
+                          type: string
+                        bgColor:
+                          type: string
+                        descriptionParsed:
+                          type: string
+                        depth:
+                          type: number
+                        slug:
+                          type: string
+                        isIgnored:
+                          type: boolean
+                        isWatched:
+                          type: boolean
+                        isNotWatched:
+                          type: boolean
+                        imageClass:
+                          type: string
+                  title:
+                    type: string
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/chats/roomid.yaml b/public/openapi/read/user/userslug/chats/roomid.yaml
new file mode 100644
index 0000000000..72fc62a125
--- /dev/null
+++ b/public/openapi/read/user/userslug/chats/roomid.yaml
@@ -0,0 +1,314 @@
+get:
+  tags:
+    - users
+  summary: Get chat room
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+    - name: roomid
+      in: path
+      required: true
+      schema:
+        type: string
+      example: 1
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  owner:
+                    type: number
+                  roomId:
+                    type: number
+                  roomName:
+                    type: string
+                  messages:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        content:
+                          type: string
+                        timestamp:
+                          type: number
+                        fromuid:
+                          type: number
+                        roomId:
+                          type: string
+                        deleted:
+                          type: boolean
+                        system:
+                          type: boolean
+                        edited:
+                          type: number
+                        timestampISO:
+                          type: string
+                          description: An ISO 8601 formatted date string (complementing `timestamp`)
+                        editedISO:
+                          type: string
+                        messageId:
+                          type: number
+                        fromUser:
+                          type: object
+                          properties:
+                            uid:
+                              type: number
+                              description: A user identifier
+                            username:
+                              type: string
+                              description: A friendly name for a given user account
+                            displayname:
+                              type: string
+                              description: This is either username or fullname depending on forum and user settings
+                            userslug:
+                              type: string
+                              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                removed, etc.)
+                            picture:
+                              type: string
+                              nullable: true
+                            status:
+                              type: string
+                            banned:
+                              type: boolean
+                            icon:text:
+                              type: string
+                              description: A single-letter representation of a username. This is used in the
+                                auto-generated icon given to users without
+                                an avatar
+                            icon:bgColor:
+                              type: string
+                              description: A six-character hexadecimal colour code assigned to the user. This
+                                value is used in conjunction with
+                                `icon:text` for the user's auto-generated
+                                icon
+                              example: "#f44336"
+                            banned_until_readable:
+                              type: string
+                            deleted:
+                              type: boolean
+                        self:
+                          type: number
+                        newSet:
+                          type: boolean
+                        index:
+                          type: number
+                        cleanedContent:
+                          type: string
+                        isOwner:
+                          type: boolean
+                  isOwner:
+                    type: boolean
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        uid:
+                          type: number
+                          description: A user identifier
+                        username:
+                          type: string
+                          description: A friendly name for a given user account
+                        displayname:
+                          type: string
+                          description: This is either username or fullname depending on forum and user settings
+                        picture:
+                          type: string
+                          nullable: true
+                        status:
+                          type: string
+                        icon:text:
+                          type: string
+                          description: A single-letter representation of a username. This is used in the
+                            auto-generated icon given to users without an
+                            avatar
+                        icon:bgColor:
+                          type: string
+                          description: A six-character hexadecimal colour code assigned to the user. This
+                            value is used in conjunction with `icon:text`
+                            for the user's auto-generated icon
+                          example: "#f44336"
+                        isOwner:
+                          type: boolean
+                  canReply:
+                    type: boolean
+                  groupChat:
+                    type: boolean
+                  usernames:
+                    type: string
+                  maximumUsersInChatRoom:
+                    type: number
+                  maximumChatMessageLength:
+                    type: number
+                  showUserInput:
+                    type: boolean
+                  isAdminOrGlobalMod:
+                    type: boolean
+                  rooms:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        owner:
+                          oneOf:
+                            - type: number
+                            - type: string
+                        roomId:
+                          type: number
+                        roomName:
+                          type: string
+                        users:
+                          type: array
+                          items:
+                            type: object
+                            properties:
+                              uid:
+                                type: number
+                                description: A user identifier
+                              username:
+                                type: string
+                                description: A friendly name for a given user account
+                              displayname:
+                                type: string
+                                description: This is either username or fullname depending on forum and user settings
+                              userslug:
+                                type: string
+                                description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                  removed, etc.)
+                              picture:
+                                nullable: true
+                                type: string
+                              status:
+                                type: string
+                              lastonline:
+                                type: number
+                              icon:text:
+                                type: string
+                                description: A single-letter representation of a username. This is used in the
+                                  auto-generated icon given to users without
+                                  an avatar
+                              icon:bgColor:
+                                type: string
+                                description: A six-character hexadecimal colour code assigned to the user. This
+                                  value is used in conjunction with
+                                  `icon:text` for the user's auto-generated
+                                  icon
+                                example: "#f44336"
+                              lastonlineISO:
+                                type: string
+                        groupChat:
+                          type: boolean
+                        unread:
+                          type: boolean
+                        teaser:
+                          type: object
+                          properties:
+                            fromuid:
+                              type: number
+                            content:
+                              type: string
+                            timestamp:
+                              type: number
+                            timestampISO:
+                              type: string
+                              description: An ISO 8601 formatted date string (complementing `timestamp`)
+                            user:
+                              type: object
+                              properties:
+                                uid:
+                                  type: number
+                                  description: A user identifier
+                                username:
+                                  type: string
+                                  description: A friendly name for a given user account
+                                displayname:
+                                  type: string
+                                  description: This is either username or fullname depending on forum and user settings
+                                userslug:
+                                  type: string
+                                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                    removed, etc.)
+                                picture:
+                                  nullable: true
+                                  type: string
+                                status:
+                                  type: string
+                                lastonline:
+                                  type: number
+                                icon:text:
+                                  type: string
+                                  description: A single-letter representation of a username. This is used in the
+                                    auto-generated icon given to users
+                                    without an avatar
+                                icon:bgColor:
+                                  type: string
+                                  description: A six-character hexadecimal colour code assigned to the user. This
+                                    value is used in conjunction with
+                                    `icon:text` for the user's
+                                    auto-generated icon
+                                  example: "#f44336"
+                                lastonlineISO:
+                                  type: string
+                          nullable: true
+                        lastUser:
+                          type: object
+                          properties:
+                            uid:
+                              type: number
+                              description: A user identifier
+                            username:
+                              type: string
+                              description: A friendly name for a given user account
+                            displayname:
+                              type: string
+                              description: This is either username or fullname depending on forum and user settings
+                            userslug:
+                              type: string
+                              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                removed, etc.)
+                            picture:
+                              nullable: true
+                              type: string
+                            status:
+                              type: string
+                            lastonline:
+                              type: number
+                            icon:text:
+                              type: string
+                              description: A single-letter representation of a username. This is used in the
+                                auto-generated icon given to users without
+                                an avatar
+                            icon:bgColor:
+                              type: string
+                              description: A six-character hexadecimal colour code assigned to the user. This
+                                value is used in conjunction with
+                                `icon:text` for the user's auto-generated
+                                icon
+                              example: "#f44336"
+                            lastonlineISO:
+                              type: string
+                        usernames:
+                          type: string
+                  nextStart:
+                    type: number
+                  title:
+                    type: string
+                  uid:
+                    type: number
+                    description: A user identifier
+                  userslug:
+                    type: string
+                    description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                      removed, etc.)
+                  canViewInfo:
+                    type: boolean
+              - $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/consent.yaml b/public/openapi/read/user/userslug/consent.yaml
new file mode 100644
index 0000000000..2f0502aab3
--- /dev/null
+++ b/public/openapi/read/user/userslug/consent.yaml
@@ -0,0 +1,34 @@
+get:
+  tags:
+    - users
+  summary: Get user's GDPR consent settings
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  gdpr_consent:
+                    type: boolean
+                  digest:
+                    type: object
+                    properties:
+                      frequency:
+                        type: string
+                      enabled:
+                        type: boolean
+                  title:
+                    type: string
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/controversial.yaml b/public/openapi/read/user/userslug/controversial.yaml
new file mode 100644
index 0000000000..c5a7fa2791
--- /dev/null
+++ b/public/openapi/read/user/userslug/controversial.yaml
@@ -0,0 +1,45 @@
+get:
+  tags:
+    - users
+  summary: Get a user's worse performing posts ("controversial")
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/downvoted.yaml b/public/openapi/read/user/userslug/downvoted.yaml
new file mode 100644
index 0000000000..f920269e2f
--- /dev/null
+++ b/public/openapi/read/user/userslug/downvoted.yaml
@@ -0,0 +1,51 @@
+get:
+  tags:
+    - users
+  summary: Get user's downvoted posts
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                    description: Translation key for message notifying user that there were no posts found
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+                required:
+                  - posts
+                  - nextStart
+                  - noItemsFoundKey
+                  - title
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/edit.yaml b/public/openapi/read/user/userslug/edit.yaml
new file mode 100644
index 0000000000..734ea32abf
--- /dev/null
+++ b/public/openapi/read/user/userslug/edit.yaml
@@ -0,0 +1,66 @@
+get:
+  tags:
+    - users
+  summary: Get user profile for editing
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  maximumSignatureLength:
+                    type: number
+                  maximumAboutMeLength:
+                    type: number
+                  maximumProfileImageSize:
+                    type: number
+                  allowProfilePicture:
+                    type: boolean
+                  allowCoverPicture:
+                    type: boolean
+                  allowProfileImageUploads:
+                    type: number
+                  allowedProfileImageExtensions:
+                    type: string
+                  allowMultipleBadges:
+                    type: boolean
+                  allowAccountDelete:
+                    type: boolean
+                  allowWebsite:
+                    type: boolean
+                  allowAboutMe:
+                    type: boolean
+                  allowSignature:
+                    type: boolean
+                  profileImageDimension:
+                    type: number
+                  defaultAvatar:
+                    type: string
+                  groupSelectSize:
+                    type: number
+                  title:
+                    type: string
+                  editButtons:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        link:
+                          type: string
+                          description: A relative path to the page linked to
+                        text:
+                          type: string
+                          description: Button label
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/edit/email.yaml b/public/openapi/read/user/userslug/edit/email.yaml
new file mode 100644
index 0000000000..b8fd327d5e
--- /dev/null
+++ b/public/openapi/read/user/userslug/edit/email.yaml
@@ -0,0 +1,27 @@
+get:
+  tags:
+    - users
+  summary: Get configs for email editing
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        text/plain:
+          schema:
+              type: string
+              description: A relative path to the registration interstitial page so they can add or update an email for their account
+              example: /register/complete
+    "302":
+      description: Redirects the user to a registration interstitial page so they can add or update an email for their account
+      headers:
+        Location:
+          schema:
+            type: string
+            example: /register/complete
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/edit/password.yaml b/public/openapi/read/user/userslug/edit/password.yaml
new file mode 100644
index 0000000000..17cf092c9b
--- /dev/null
+++ b/public/openapi/read/user/userslug/edit/password.yaml
@@ -0,0 +1,31 @@
+get:
+  tags:
+    - users
+  summary: Get configs for password editing
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  hasPassword:
+                    type: boolean
+                  minimumPasswordLength:
+                    type: number
+                  minimumPasswordStrength:
+                    type: number
+                  title:
+                    type: string
+              - $ref: ../../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/edit/username.yaml b/public/openapi/read/user/userslug/edit/username.yaml
new file mode 100644
index 0000000000..17761324db
--- /dev/null
+++ b/public/openapi/read/user/userslug/edit/username.yaml
@@ -0,0 +1,27 @@
+get:
+  tags:
+    - users
+  summary: Get configs for username editing
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  hasPassword:
+                    type: boolean
+                  title:
+                    type: string
+              - $ref: ../../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/export/posts.yaml b/public/openapi/read/user/userslug/export/posts.yaml
new file mode 100644
index 0000000000..80132ab8ad
--- /dev/null
+++ b/public/openapi/read/user/userslug/export/posts.yaml
@@ -0,0 +1,20 @@
+get:
+  tags:
+    - users
+  summary: Export a user's posts (.csv)
+  description: This route retrieves an existing export of user's posts. To create one go to `/user/{userslug}/consent`
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: "A CSV file containing a user's posts"
+      content:
+        text/csv:
+          schema:
+            type: string
+            format: binary
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/export/profile.yaml b/public/openapi/read/user/userslug/export/profile.yaml
new file mode 100644
index 0000000000..da0b6bec45
--- /dev/null
+++ b/public/openapi/read/user/userslug/export/profile.yaml
@@ -0,0 +1,20 @@
+get:
+  tags:
+    - users
+  summary: Export a user's profile data (.json)
+  description: This route retrieves an existing export of user's profile data. To create one go to `/user/{userslug}/consent`
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: "A JSON file containing the user profile"
+      content:
+        text/json:
+          schema:
+            type: string
+            format: binary
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/export/uploads.yaml b/public/openapi/read/user/userslug/export/uploads.yaml
new file mode 100644
index 0000000000..cbe7a9aefa
--- /dev/null
+++ b/public/openapi/read/user/userslug/export/uploads.yaml
@@ -0,0 +1,20 @@
+get:
+  tags:
+    - users
+  summary: Export a user's uploads (.zip)
+  description: This route retrieves an existing export of user's profile data. To create one go to `/user/{userslug}/consent`
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: Successful export of user uploads
+      content:
+        application/zip:
+          schema:
+            type: string
+            format: binary
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/followers.yaml b/public/openapi/read/user/userslug/followers.yaml
new file mode 100644
index 0000000000..a4abeaa4ed
--- /dev/null
+++ b/public/openapi/read/user/userslug/followers.yaml
@@ -0,0 +1,91 @@
+get:
+  tags:
+    - users
+  summary: Get followers
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+    - name: page
+      in: query
+      schema:
+        type: number
+      example: ''
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        uid:
+                          type: number
+                          description: A user identifier
+                        username:
+                          type: string
+                          description: A friendly name for a given user account
+                        userslug:
+                          type: string
+                          description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                            removed, etc.)
+                        picture:
+                          nullable: true
+                          type: string
+                        status:
+                          type: string
+                        postcount:
+                          type: number
+                        reputation:
+                          type: number
+                        email:confirmed:
+                          type: number
+                          description: Whether the user has confirmed their email address or not
+                        lastonline:
+                          type: number
+                        flags:
+                          nullable: true
+                        banned:
+                          type: number
+                        banned:expire:
+                          type: number
+                        joindate:
+                          type: number
+                          description: A UNIX timestamp representing the moment the user's account was
+                            created
+                        icon:text:
+                          type: string
+                          description: A single-letter representation of a username. This is used in the
+                            auto-generated icon given to users without an
+                            avatar
+                        icon:bgColor:
+                          type: string
+                          description: A six-character hexadecimal colour code assigned to the user. This
+                            value is used in conjunction with `icon:text`
+                            for the user's auto-generated icon
+                          example: "#f44336"
+                        joindateISO:
+                          type: string
+                        lastonlineISO:
+                          type: string
+                        banned_until:
+                          type: number
+                        banned_until_readable:
+                          type: string
+                        administrator:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/following.yaml b/public/openapi/read/user/userslug/following.yaml
new file mode 100644
index 0000000000..3329b8a543
--- /dev/null
+++ b/public/openapi/read/user/userslug/following.yaml
@@ -0,0 +1,91 @@
+get:
+  tags:
+    - users
+  summary: Get followed users
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+    - name: page
+      in: query
+      schema:
+        type: number
+      example: ''
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        uid:
+                          type: number
+                          description: A user identifier
+                        username:
+                          type: string
+                          description: A friendly name for a given user account
+                        userslug:
+                          type: string
+                          description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                            removed, etc.)
+                        picture:
+                          nullable: true
+                          type: string
+                        status:
+                          type: string
+                        postcount:
+                          type: number
+                        reputation:
+                          type: number
+                        email:confirmed:
+                          type: number
+                          description: Whether the user has confirmed their email address or not
+                        lastonline:
+                          type: number
+                        flags:
+                          nullable: true
+                        banned:
+                          type: number
+                        banned:expire:
+                          type: number
+                        joindate:
+                          type: number
+                          description: A UNIX timestamp representing the moment the user's account was
+                            created
+                        icon:text:
+                          type: string
+                          description: A single-letter representation of a username. This is used in the
+                            auto-generated icon given to users without an
+                            avatar
+                        icon:bgColor:
+                          type: string
+                          description: A six-character hexadecimal colour code assigned to the user. This
+                            value is used in conjunction with `icon:text`
+                            for the user's auto-generated icon
+                          example: "#f44336"
+                        joindateISO:
+                          type: string
+                        lastonlineISO:
+                          type: string
+                        banned_until:
+                          type: number
+                        banned_until_readable:
+                          type: string
+                        administrator:
+                          type: boolean
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/groups.yaml b/public/openapi/read/user/userslug/groups.yaml
new file mode 100644
index 0000000000..ba1cd9d243
--- /dev/null
+++ b/public/openapi/read/user/userslug/groups.yaml
@@ -0,0 +1,32 @@
+get:
+  tags:
+    - users
+  summary: Get user's groups
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  title:
+                    type: string
+                  template:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                      account/groups:
+                        type: boolean
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/ignored.yaml b/public/openapi/read/user/userslug/ignored.yaml
new file mode 100644
index 0000000000..0ab5e8bb0f
--- /dev/null
+++ b/public/openapi/read/user/userslug/ignored.yaml
@@ -0,0 +1,47 @@
+get:
+  tags:
+    - users
+  summary: Get user's ignored topics
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/TopicObject.yaml#/TopicObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/info.yaml b/public/openapi/read/user/userslug/info.yaml
new file mode 100644
index 0000000000..e56f76ec84
--- /dev/null
+++ b/public/openapi/read/user/userslug/info.yaml
@@ -0,0 +1,181 @@
+get:
+  tags:
+    - users
+  summary: Get user moderation info
+  description: Administrators and Global Moderators get access to the `/info` page, which shows some backend data that is useful from a moderation point-of-view (such as IP addresses, recent bans, moderation history, etc).
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  history:
+                    type: object
+                    properties:
+                      flags:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            pid:
+                              type: number
+                            timestamp:
+                              type: number
+                            timestampISO:
+                              type: string
+                              description: An ISO 8601 formatted date string (complementing `timestamp`)
+                            timestampReadable:
+                              type: string
+                          additionalProperties:
+                            description: Contextual data is added to this object (such as topic data, etc.)
+                      bans:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            uid:
+                              type: number
+                            timestamp:
+                              type: number
+                            expire:
+                              type: number
+                            fromUid:
+                              type: number
+                            user:
+                              type: object
+                              properties:
+                                username:
+                                  type: string
+                                  description: A friendly name for a given user account
+                                userslug:
+                                  type: string
+                                  description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                    removed, etc.)
+                                picture:
+                                  type: string
+                                uid:
+                                  type: number
+                                  description: A user identifier
+                                icon:text:
+                                  type: string
+                                  description: A single-letter representation of a username. This is used in the
+                                    auto-generated icon given to users without
+                                    an avatar
+                                icon:bgColor:
+                                  type: string
+                                  description: A six-character hexadecimal colour code assigned to the user. This
+                                    value is used in conjunction with
+                                    `icon:text` for the user's auto-generated
+                                    icon
+                                  example: "#f44336"
+                            until:
+                              type: number
+                            untilReadable:
+                              type: string
+                            timestampReadable:
+                              type: string
+                            timestampISO:
+                              type: string
+                            reason:
+                              type: string
+                  sessions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        ip:
+                          type: string
+                        uuid:
+                          type: string
+                        datetime:
+                          type: number
+                        platform:
+                          type: string
+                        browser:
+                          type: string
+                        version:
+                          type: string
+                        current:
+                          type: boolean
+                        datetimeISO:
+                          type: string
+                  usernames:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        value:
+                          type: string
+                        timestamp:
+                          type: number
+                        timestampISO:
+                          type: string
+                          description: An ISO 8601 formatted date string (complementing `timestamp`)
+                  emails:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        value:
+                          type: string
+                        timestamp:
+                          type: number
+                        timestampISO:
+                          type: string
+                          description: An ISO 8601 formatted date string (complementing `timestamp`)
+                  moderationNotes:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        uid:
+                          type: number
+                        note:
+                          type: string
+                        timestamp:
+                          type: number
+                        timestampISO:
+                          type: string
+                        user:
+                          type: object
+                          properties:
+                            username:
+                              type: string
+                              description: A friendly name for a given user account
+                            userslug:
+                              type: string
+                              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                removed, etc.)
+                            picture:
+                              type: string
+                            uid:
+                              type: number
+                              description: A user identifier
+                            icon:text:
+                              type: string
+                              description: A single-letter representation of a username. This is used in the
+                                auto-generated icon given to users without
+                                an avatar
+                            icon:bgColor:
+                              type: string
+                              description: A six-character hexadecimal colour code assigned to the user. This
+                                value is used in conjunction with
+                                `icon:text` for the user's auto-generated
+                                icon
+                              example: "#f44336"
+                  title:
+                    type: string
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/posts.yaml b/public/openapi/read/user/userslug/posts.yaml
new file mode 100644
index 0000000000..a2b6c630fa
--- /dev/null
+++ b/public/openapi/read/user/userslug/posts.yaml
@@ -0,0 +1,45 @@
+get:
+  tags:
+    - users
+  summary: Get a user's posts
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/session/uuid.yaml b/public/openapi/read/user/userslug/session/uuid.yaml
new file mode 100644
index 0000000000..e75daf6bb5
--- /dev/null
+++ b/public/openapi/read/user/userslug/session/uuid.yaml
@@ -0,0 +1,20 @@
+delete:
+  tags:
+    - users
+  summary: Revoke a user session
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+    - name: uuid
+      in: path
+      required: true
+      schema:
+        type: string
+      example: testuuid
+  responses:
+    "200":
+      description: User session revoked
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/sessions.yaml b/public/openapi/read/user/userslug/sessions.yaml
new file mode 100644
index 0000000000..2c9a439c76
--- /dev/null
+++ b/public/openapi/read/user/userslug/sessions.yaml
@@ -0,0 +1,46 @@
+get:
+  tags:
+    - users
+  summary: Get user's active sessions
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  sessions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        ip:
+                          type: string
+                        uuid:
+                          type: string
+                        datetime:
+                          type: number
+                        platform:
+                          type: string
+                        browser:
+                          type: string
+                        version:
+                          type: string
+                        current:
+                          type: boolean
+                        datetimeISO:
+                          type: string
+                  title:
+                    type: string
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/settings.yaml b/public/openapi/read/user/userslug/settings.yaml
new file mode 100644
index 0000000000..e74a44fb2f
--- /dev/null
+++ b/public/openapi/read/user/userslug/settings.yaml
@@ -0,0 +1,139 @@
+get:
+  tags:
+    - users
+  summary: Get user's settings
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  settings:
+                    $ref: ../../../components/schemas/SettingsObj.yaml#/Settings
+                  languages:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        code:
+                          type: string
+                        dir:
+                          type: string
+                        selected:
+                          type: boolean
+                  acpLanguages:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        code:
+                          type: string
+                        dir:
+                          type: string
+                        selected:
+                          type: boolean
+                  customSettings:
+                    type: array
+                    items:
+                      type: object
+                      properties: {}
+                      additionalProperties: {}
+                  homePageRoutes:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        route:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+                  notificationSettings:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        label:
+                          type: string
+                        none:
+                          type: boolean
+                        notification:
+                          type: boolean
+                        email:
+                          type: boolean
+                        notificationemail:
+                          type: boolean
+                  disableEmailSubscriptions:
+                    type: number
+                  dailyDigestFreqOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        value:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+                  bootswatchSkinOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        value:
+                          type: string
+                        selected:
+                          type: boolean
+                  upvoteNotifFreq:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+                  categoryWatchState:
+                    type: object
+                    properties:
+                      watching:
+                        type: boolean
+                  disableCustomUserSkins:
+                    type: number
+                  allowUserHomePage:
+                    type: number
+                  hideFullname:
+                    type: number
+                  hideEmail:
+                    type: number
+                  inTopicSearchAvailable:
+                    type: boolean
+                  maxTopicsPerPage:
+                    type: number
+                  maxPostsPerPage:
+                    type: number
+                  title:
+                    type: string
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/topics.yaml b/public/openapi/read/user/userslug/topics.yaml
new file mode 100644
index 0000000000..64b9efd739
--- /dev/null
+++ b/public/openapi/read/user/userslug/topics.yaml
@@ -0,0 +1,47 @@
+get:
+  tags:
+    - users
+  summary: Get a user's topics
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/TopicObject.yaml#/TopicObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/uploads.yaml b/public/openapi/read/user/userslug/uploads.yaml
new file mode 100644
index 0000000000..f8b74cc521
--- /dev/null
+++ b/public/openapi/read/user/userslug/uploads.yaml
@@ -0,0 +1,37 @@
+get:
+  tags:
+    - users
+  summary: Get user's uploads
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  uploads:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        url:
+                          type: string
+                  privateUploads:
+                    type: boolean
+                  title:
+                    type: string
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/upvoted.yaml b/public/openapi/read/user/userslug/upvoted.yaml
new file mode 100644
index 0000000000..81623294a6
--- /dev/null
+++ b/public/openapi/read/user/userslug/upvoted.yaml
@@ -0,0 +1,51 @@
+get:
+  tags:
+    - users
+  summary: Get user's upvoted posts
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  posts:
+                    $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                    description: Translation key for message notifying user that there were no posts found
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+                required:
+                  - posts
+                  - nextStart
+                  - noItemsFoundKey
+                  - title
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/user/userslug/watched.yaml b/public/openapi/read/user/userslug/watched.yaml
new file mode 100644
index 0000000000..11e6cf9266
--- /dev/null
+++ b/public/openapi/read/user/userslug/watched.yaml
@@ -0,0 +1,49 @@
+get:
+  tags:
+    - users
+  summary: Get user's watched topics
+  parameters:
+    - name: userslug
+      in: path
+      required: true
+      schema:
+        type: string
+      example: admin
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull
+              - type: object
+                properties:
+                  moderationNote:
+                    type: string
+                  topics:
+                    type: array
+                    items:
+                      $ref: ../../../components/schemas/TopicObject.yaml#/TopicObject
+                  nextStart:
+                    type: number
+                  noItemsFoundKey:
+                    type: string
+                  title:
+                    type: string
+                  showSort:
+                    type: boolean
+                  sortOptions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        url:
+                          type: string
+                        name:
+                          type: string
+                        selected:
+                          type: boolean
+              - $ref: ../../../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/read/users.yaml b/public/openapi/read/users.yaml
new file mode 100644
index 0000000000..160aa927d0
--- /dev/null
+++ b/public/openapi/read/users.yaml
@@ -0,0 +1,119 @@
+get:
+  tags:
+    - users
+  summary: Get users
+  parameters:
+    - in: query
+      name: section
+      schema:
+        type: string
+        enum: ['joindate', 'online', 'sort-posts', 'sort-reputation', 'banned', 'flagged']
+      required: false
+      description: Allows filtering of the user list via pre-defined sections
+      example: 'joindate'
+    - in: query
+      name: term
+      schema:
+        type: string
+      required: false
+      description: Allows for searching of user list
+      example: ''
+  responses:
+    "200":
+      description: ""
+      content:
+        application/json:
+          schema:
+            allOf:
+              - type: object
+                properties:
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        uid:
+                          type: number
+                          description: A user identifier
+                        username:
+                          type: string
+                          description: A friendly name for a given user account
+                        displayname:
+                          type: string
+                          description: This is either username or fullname depending on forum and user settings
+                        userslug:
+                          type: string
+                          description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                            removed, etc.)
+                        picture:
+                          nullable: true
+                          type: string
+                        status:
+                          type: string
+                        postcount:
+                          type: number
+                        reputation:
+                          type: number
+                        email:confirmed:
+                          type: number
+                          description: Whether the user has confirmed their email address or not
+                        lastonline:
+                          type: number
+                        flags:
+                          nullable: true
+                        banned:
+                          type: number
+                        banned:expire:
+                          type: number
+                        joindate:
+                          type: number
+                          description: A UNIX timestamp representing the moment the user's account was
+                            created
+                        icon:text:
+                          type: string
+                          description: A single-letter representation of a username. This is used in the
+                            auto-generated icon given to users without an
+                            avatar
+                        icon:bgColor:
+                          type: string
+                          description: A six-character hexadecimal colour code assigned to the user. This
+                            value is used in conjunction with `icon:text`
+                            for the user's auto-generated icon
+                          example: "#f44336"
+                        joindateISO:
+                          type: string
+                        lastonlineISO:
+                          type: string
+                        banned_until:
+                          type: number
+                        banned_until_readable:
+                          type: string
+                  userCount:
+                    type: number
+                  title:
+                    type: string
+                  isAdminOrGlobalMod:
+                    type: boolean
+                  isAdmin:
+                    type: boolean
+                  isGlobalMod:
+                    type: boolean
+                  displayUserSearch:
+                    type: boolean
+                  section_joindate:
+                    type: boolean
+                  maximumInvites:
+                    type: number
+                  inviteOnly:
+                    type: boolean
+                  adminInviteOnly:
+                    type: boolean
+                  invites:
+                    type: number
+                  showInviteButton:
+                    type: boolean
+                  reputation:disabled:
+                    type: number
+              - $ref: ../components/schemas/Pagination.yaml#/Pagination
+              - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
+              - $ref: ../components/schemas/CommonProps.yaml#/CommonProps
\ No newline at end of file
diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml
new file mode 100644
index 0000000000..6f55dbcfc0
--- /dev/null
+++ b/public/openapi/write.yaml
@@ -0,0 +1,178 @@
+openapi: 3.0.0
+info:
+  title: NodeBB Write API
+  description: >-
+    # Overview
+
+    The following document outlines every route exposed by the NodeBB Write API. As of NodeBB v1.15.0, NodeBB will use these routes to make changes to the database (e.g. creating new posts, editing user profiles, etc.)
+
+    We invite you to build external integrations with NodeBB using this document as a guide.
+
+    # History
+
+    Up until v1.15.0, NodeBB utilised the [WebSocket](https://en.wikipedia.org/wiki/WebSocket) protocol to communicate with the backend. However, it was decided in early 2020 that this usage of WebSocket – while functional – led to occasional wheel reinvention and disregarded an otherwise fully-featured technology (that is, REST).
+
+    Years prior to this determination, many users of NodeBB had asked for a RESTful API to call against NodeBB, which led to the creation of [`nodebb-plugin-write-api`](https://github.com/NodeBB/nodebb-plugin-write-api). In tandem with the above decision, the Write API was merged into NodeBB core in late 2020.
+
+    v3 of the Write API (this document) achieves rough feature parity with v2 of the Write API plugin.
+
+    # Authentication
+
+    Please see the ["Authentication" section under the Read API](../read/#section/Overview/Authentication) for more information on how to authenticate against this API in order to make calls.
+  version: 1.19.0
+  contact:
+    email: support@nodebb.org
+  license:
+    name: GPL-3.0
+servers:
+  - url: /api/v3
+tags:
+  - name: utilities
+    description: Utility calls to test Write API functionality
+  - name: users
+    description: Account related calls (create, modify, delete, etc.)
+  - name: groups
+    description: Calls related to user groups
+  - name: categories
+    description: Administrative calls to manage categories
+  - name: topics
+    description: Topic-based calls (create, modify, delete, etc.)
+  - name: posts
+    description: Individual post-related calls (create, modify, delete, etc.)
+  - name: chats
+    description: Calls related to the user private messaging system
+  - name: admin
+    description: Administrative calls
+  - name: files
+    description: File upload routes
+paths:
+  /ping:
+    $ref: 'write/ping.yaml'
+  /utilities/login:
+    $ref: 'write/login.yaml'
+  /users/:
+    $ref: 'write/users.yaml'
+  /users/{uid}:
+    $ref: 'write/users/uid.yaml'
+  /users/{uid}/picture:
+    $ref: 'write/users/uid/picture.yaml'
+  /users/{uid}/content:
+    $ref: 'write/users/uid/content.yaml'
+  /users/{uid}/account:
+    $ref: 'write/users/uid/account.yaml'
+  /users/{uid}/settings:
+    $ref: 'write/users/uid/settings.yaml'
+  /users/{uid}/password:
+    $ref: 'write/users/uid/password.yaml'
+  /users/{uid}/follow:
+    $ref: 'write/users/uid/follow.yaml'
+  /users/{uid}/ban:
+    $ref: 'write/users/uid/ban.yaml'
+  /users/{uid}/mute:
+    $ref: 'write/users/uid/mute.yaml'
+  /users/{uid}/tokens:
+    $ref: 'write/users/uid/tokens.yaml'
+  /users/{uid}/tokens/{token}:
+    $ref: 'write/users/uid/tokens/token.yaml'
+  /users/{uid}/sessions/{uuid}:
+    $ref: 'write/users/uid/sessions/uuid.yaml'
+  /users/{uid}/invites:
+    $ref: 'write/users/uid/invites.yaml'
+  /users/{uid}/invites/groups:
+    $ref: 'write/users/uid/invites/groups.yaml'
+  /users/{uid}/emails:
+    $ref: 'write/users/uid/emails.yaml'
+  /users/{uid}/emails/{email}:
+    $ref: 'write/users/uid/emails/email.yaml'
+  /users/{uid}/emails/{email}/confirm:
+    $ref: 'write/users/uid/emails/email/confirm.yaml'
+  /users/{uid}/exports/{type}:
+    $ref: 'write/users/uid/exports/type.yaml'
+  /groups/:
+    $ref: 'write/groups.yaml'
+  /groups/{slug}:
+    $ref: 'write/groups/slug.yaml'
+  /groups/{slug}/membership/{uid}:
+    $ref: 'write/groups/slug/membership/uid.yaml'
+  /groups/{slug}/ownership/{uid}:
+    $ref: 'write/groups/slug/ownership/uid.yaml'
+  /categories/:
+    $ref: 'write/categories.yaml'
+  /categories/{cid}:
+    $ref: 'write/categories/cid.yaml'
+  /categories/{cid}/privileges:
+    $ref: 'write/categories/cid/privileges.yaml'
+  /categories/{cid}/privileges/{privilege}:
+    $ref: 'write/categories/cid/privileges/privilege.yaml'
+  /categories/{cid}/moderator/{uid}:
+    $ref: 'write/categories/cid/moderator/uid.yaml'
+  /topics/:
+    $ref: 'write/topics.yaml'
+  /topics/{tid}:
+    $ref: 'write/topics/tid.yaml'
+  /topics/{tid}/state:
+    $ref: 'write/topics/tid/state.yaml'
+  /topics/{tid}/lock:
+    $ref: 'write/topics/tid/lock.yaml'
+  /topics/{tid}/pin:
+    $ref: 'write/topics/tid/pin.yaml'
+  /topics/{tid}/follow:
+    $ref: 'write/topics/tid/follow.yaml'
+  /topics/{tid}/ignore:
+    $ref: 'write/topics/tid/ignore.yaml'
+  /topics/{tid}/tags:
+    $ref: 'write/topics/tid/tags.yaml'
+  /topics/{tid}/thumbs:
+    $ref: 'write/topics/tid/thumbs.yaml'
+  /topics/{tid}/thumbs/order:
+    $ref: 'write/topics/tid/thumbs/order.yaml'
+  /topics/{tid}/events:
+    $ref: 'write/topics/tid/events.yaml'
+  /topics/{tid}/events/{eventId}:
+    $ref: 'write/topics/tid/events/eventId.yaml'
+  /posts/{pid}:
+    $ref: 'write/posts/pid.yaml'
+  /posts/{pid}/state:
+    $ref: 'write/posts/pid/state.yaml'
+  /posts/{pid}/move:
+    $ref: 'write/posts/pid/move.yaml'
+  /posts/{pid}/vote:
+    $ref: 'write/posts/pid/vote.yaml'
+  /posts/{pid}/bookmark:
+    $ref: 'write/posts/pid/bookmark.yaml'
+  /posts/{pid}/diffs:
+    $ref: 'write/posts/pid/diffs.yaml'
+  /posts/{pid}/diffs/{since}:
+    $ref: 'write/posts/pid/diffs/since.yaml'
+  /posts/{pid}/diffs/{timestamp}:
+    $ref: 'write/posts/pid/diffs/timestamp.yaml'
+  /chats/:
+    $ref: 'write/chats.yaml'
+  /chats/{roomId}:
+    $ref: 'write/chats/roomId.yaml'
+  /chats/{roomId}/users:
+    $ref: 'write/chats/roomId/users.yaml'
+  /chats/{roomId}/users/{uid}:
+    $ref: 'write/chats/roomId/users/uid.yaml'
+  /chats/{roomId}/messages:
+    $ref: 'write/chats/roomId/messages.yaml'
+  /chats/{roomId}/messages/{mid}:
+    $ref: 'write/chats/roomId/messages/mid.yaml'
+  /flags/:
+    $ref: 'write/flags.yaml'
+  /flags/{flagId}:
+    $ref: 'write/flags/flagId.yaml'
+  /flags/{flagId}/notes:
+    $ref: 'write/flags/flagId/notes.yaml'
+  /flags/{flagId}/notes/{datetime}:
+    $ref: 'write/flags/flagId/notes/datetime.yaml'
+  /admin/settings/{setting}:
+    $ref: 'write/admin/settings/setting.yaml'
+  /admin/analytics:
+    $ref: 'write/admin/analytics.yaml'
+  /admin/analytics/{set}:
+    $ref: 'write/admin/analytics/set.yaml'
+  /files/:
+    $ref: 'write/files.yaml'
+  /files/folder:
+    $ref: 'write/files/folder.yaml'
\ No newline at end of file
diff --git a/public/openapi/write/admin/analytics.yaml b/public/openapi/write/admin/analytics.yaml
new file mode 100644
index 0000000000..06f68a3778
--- /dev/null
+++ b/public/openapi/write/admin/analytics.yaml
@@ -0,0 +1,20 @@
+get:
+  tags:
+    - admin
+  summary: get analytics keys
+  description: This operation returns the list metrics tracked by NodeBB. It is only accessible to administrators.
+  responses:
+    '200':
+      description: Analytics keys retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  keys:
+                    type: array
\ No newline at end of file
diff --git a/public/openapi/write/admin/analytics/set.yaml b/public/openapi/write/admin/analytics/set.yaml
new file mode 100644
index 0000000000..31f63467f1
--- /dev/null
+++ b/public/openapi/write/admin/analytics/set.yaml
@@ -0,0 +1,46 @@
+get:
+  tags:
+    - admin
+  summary: get analytics data
+  description: This operation retrieves analytics data from NodeBB. It is only accessible to administrators.
+  parameters:
+    - in: path
+      name: set
+      schema:
+        type: string
+      required: true
+      description: analytics set to retrieve
+      example: topics
+    - in: query
+      name: units
+      schema:
+        type: string
+        enum: [hours, days]
+      description: Whether to display dashboard data segmented daily or hourly
+      example: days
+    - in: query
+      name: until
+      schema:
+        type: number
+      description: A UNIX timestamp denoting the end of the analytics reporting period
+      example: ''
+    - in: query
+      name: count
+      schema:
+        type: number
+      description: The number of entries to return (e.g. if `units` is `hourly`, and `count` is `24`, the result set will contain 24 hours' worth of analytics)
+      example: 20
+  responses:
+    '200':
+      description: Analytics set retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: array
+                items:
+                  type: number
\ No newline at end of file
diff --git a/public/openapi/write/admin/settings/setting.yaml b/public/openapi/write/admin/settings/setting.yaml
new file mode 100644
index 0000000000..009f478b81
--- /dev/null
+++ b/public/openapi/write/admin/settings/setting.yaml
@@ -0,0 +1,37 @@
+put:
+  tags:
+    - admin
+  summary: update configuration setting
+  description: This operation updates a configuration setting in the backend. The calling user must have the `admin:settings` privilege (or be a superadmin) in order for this call to proceed.
+  parameters:
+    - in: path
+      name: setting
+      schema:
+        type: string
+      required: true
+      description: backend id of the setting to update
+      example: maximumRelatedTopics
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            value:
+              type: string
+              description: the value of the new setting
+              example: 2
+  responses:
+    '200':
+      description: Admin setting updated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/categories.yaml b/public/openapi/write/categories.yaml
new file mode 100644
index 0000000000..5c26b53633
--- /dev/null
+++ b/public/openapi/write/categories.yaml
@@ -0,0 +1,66 @@
+post:
+  tags:
+    - categories
+  summary: create a category
+  description: This operation creates a new category
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            name:
+              type: string
+              example: My New Category
+            description:
+              type: string
+              example: Lorem ipsum, dolor sit amet
+            parentCid:
+              type: number
+              example: 0
+            cloneFromCid:
+              type: number
+              example: 0
+            icon:
+              type: string
+              example: bullhorn
+              description: A ForkAwesome icon without the `fa-` prefix
+            bgColor:
+              type: string
+              example: '#ffffff'
+            color:
+              type: string
+              example: '#000000'
+            link:
+              type: string
+              example: 'https://example.org'
+            class:
+              type: string
+              example: 'col-md-3 col-xs-6'
+            backgroundImage:
+              type: string
+              example: '/assets/relative/path/to/image'
+          required:
+            - name
+  responses:
+    '200':
+      description: category successfully created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
+                  - type: object
+                    properties:
+                      tagWhitelist:
+                        type: array
+                        items:
+                          type: string
+                      unread-class:
+                        type: string
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid.yaml b/public/openapi/write/categories/cid.yaml
new file mode 100644
index 0000000000..bcb50225a5
--- /dev/null
+++ b/public/openapi/write/categories/cid.yaml
@@ -0,0 +1,93 @@
+get:
+  tags:
+    - categories
+  summary: get a category
+  description: This operation retrieves a category's data
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: string
+      required: true
+      description: a valid category id
+      example: 2
+  responses:
+    '200':
+      description: Category successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject
+put:
+  tags:
+    - categories
+  summary: update a category
+  description: This operation updates an existing category.
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: number
+      required: true
+      description: a valid category id
+      example: 2
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties: {}
+          additionalProperties: {}
+  responses:
+    '200':
+      description: category successfully updated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject
+                  - type: object
+                    properties:
+                      tagWhitelist:
+                        type: array
+                        items:
+                          type: string
+                      unread-class:
+                        type: string
+delete:
+  tags:
+    - categories
+  summary: delete a category
+  description: This operation deletes and purges a category and all of its topics and posts (careful, there is no confirmation!)
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: number
+      required: true
+      description: a valid category id
+      example: 2
+  responses:
+    '200':
+      description: Category successfully deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid/moderator/uid.yaml b/public/openapi/write/categories/cid/moderator/uid.yaml
new file mode 100644
index 0000000000..4dc5e576ae
--- /dev/null
+++ b/public/openapi/write/categories/cid/moderator/uid.yaml
@@ -0,0 +1,64 @@
+put:
+  tags:
+    - categories
+  summary: Make a user moderator of category
+  description: This operation makes a user the moderator of a specific category
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: string
+      required: true
+      description: a valid category id
+      example: 1
+    - in: path
+      name: uid
+      schema:
+        type: string
+      required: true
+      description: The uid of the user that will be the moderator
+      example: 2
+  responses:
+    '200':
+      description: User successfully made moderator
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+delete:
+  tags:
+    - categories
+  summary: Remove a category moderator
+  description: This operation removes a user from category moderators
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: string
+      required: true
+      description: a valid category id
+      example: 1
+    - in: path
+      name: uid
+      schema:
+        type: string
+      required: true
+      description: The uid of the user that will be removed from moderators
+      example: 2
+  responses:
+    '200':
+      description: User successfully made moderator
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid/privileges.yaml b/public/openapi/write/categories/cid/privileges.yaml
new file mode 100644
index 0000000000..209b0ad051
--- /dev/null
+++ b/public/openapi/write/categories/cid/privileges.yaml
@@ -0,0 +1,94 @@
+get:
+  tags:
+    - categories
+  summary: get a category's privilege set
+  description: This operation retrieves a category's privilege set.
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: string
+      required: true
+      description: a valid category id, `0` for global privileges, `admin` for admin privileges
+      example: 1
+  responses:
+    '200':
+      description: Category privileges successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  labels:
+                    type: object
+                    properties:
+                      users:
+                        type: array
+                        items:
+                          type: string
+                          description: Language key of the privilege name's user-friendly name
+                      groups:
+                        type: array
+                        items:
+                          type: string
+                          description: Language key of the privilege name's user-friendly name
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        nameEscaped:
+                          type: string
+                        privileges:
+                          type: object
+                          additionalProperties:
+                            type: boolean
+                            description: A set of privileges with either true or false
+                        isPrivate:
+                          type: boolean
+                        isSystem:
+                          type: boolean
+                  groups:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        nameEscaped:
+                          type: string
+                        privileges:
+                          type: object
+                          additionalProperties:
+                            type: boolean
+                            description: A set of privileges with either true or false
+                        isPrivate:
+                          type: boolean
+                        isSystem:
+                          type: boolean
+                  keys:
+                    type: object
+                    properties:
+                      users:
+                        type: array
+                        items:
+                          type: string
+                          description: "Privilege name"
+                      groups:
+                        type: array
+                        items:
+                          type: string
+                          description: "Privilege name"
+                  columnCountUserOther:
+                    type: number
+                    description: "The number of additional user privileges added by plugins"
+                  columnCountGroupOther:
+                    type: number
+                    description: "The number of additional group privileges added by plugins"
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid/privileges/privilege.yaml b/public/openapi/write/categories/cid/privileges/privilege.yaml
new file mode 100644
index 0000000000..1303640b19
--- /dev/null
+++ b/public/openapi/write/categories/cid/privileges/privilege.yaml
@@ -0,0 +1,222 @@
+put:
+  tags:
+    - categories
+  summary: Grant category privilege for user/group
+  description: This operation grants a category privilege for a specific user or group
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: string
+      required: true
+      description: a valid category id, `0` for global privileges, `admin` for admin privileges
+      example: 1
+    - in: path
+      name: privilege
+      schema:
+        type: string
+      required: true
+      description: The specific privilege you would like to grant. Privileges for groups must be prefixed `group:`
+      example: 'groups:ban'
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            member:
+              type: string
+              description: A valid user id or group name
+              example: 'guests'
+  responses:
+    '200':
+      description: Privilege successfully granted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  labels:
+                    type: object
+                    properties:
+                      users:
+                        type: array
+                        items:
+                          type: string
+                          description: Language key of the privilege name's user-friendly name
+                      groups:
+                        type: array
+                        items:
+                          type: string
+                          description: Language key of the privilege name's user-friendly name
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        nameEscaped:
+                          type: string
+                        privileges:
+                          type: object
+                          additionalProperties:
+                            type: boolean
+                            description: A set of privileges with either true or false
+                        isPrivate:
+                          type: boolean
+                        isSystem:
+                          type: boolean
+                  groups:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        nameEscaped:
+                          type: string
+                        privileges:
+                          type: object
+                          additionalProperties:
+                            type: boolean
+                            description: A set of privileges with either true or false
+                        isPrivate:
+                          type: boolean
+                        isSystem:
+                          type: boolean
+                  keys:
+                    type: object
+                    properties:
+                      users:
+                        type: array
+                        items:
+                          type: string
+                          description: "Privilege name"
+                      groups:
+                        type: array
+                        items:
+                          type: string
+                          description: "Privilege name"
+                  columnCountUserOther:
+                    type: number
+                    description: "The number of additional user privileges added by plugins"
+                  columnCountGroupOther:
+                    type: number
+                    description: "The number of additional user privileges added by plugins"
+delete:
+  tags:
+    - categories
+  summary: Rescinds category privilege for user/group
+  description: This operation rescinds a category privilege for a specific user or group
+  parameters:
+    - in: path
+      name: cid
+      schema:
+        type: string
+      required: true
+      description: a valid category id, `0` for global privileges, `admin` for admin privileges
+      example: 1
+    - in: path
+      name: privilege
+      schema:
+        type: string
+      required: true
+      description: The specific privilege you would like to rescind. Privileges for groups must be prefixed `group:`
+      example: 'groups:ban'
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            member:
+              type: string
+              description: A valid user id or group name
+              example: 'guests'
+  responses:
+    '200':
+      description: Privilege successfully rescinded
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  labels:
+                    type: object
+                    properties:
+                      users:
+                        type: array
+                        items:
+                          type: string
+                          description: Language key of the privilege name's user-friendly name
+                      groups:
+                        type: array
+                        items:
+                          type: string
+                          description: Language key of the privilege name's user-friendly name
+                  users:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        nameEscaped:
+                          type: string
+                        privileges:
+                          type: object
+                          additionalProperties:
+                            type: boolean
+                            description: A set of privileges with either true or false
+                        isPrivate:
+                          type: boolean
+                        isSystem:
+                          type: boolean
+                  groups:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        name:
+                          type: string
+                        nameEscaped:
+                          type: string
+                        privileges:
+                          type: object
+                          additionalProperties:
+                            type: boolean
+                            description: A set of privileges with either true or false
+                        isPrivate:
+                          type: boolean
+                        isSystem:
+                          type: boolean
+                  keys:
+                    type: object
+                    properties:
+                      users:
+                        type: array
+                        items:
+                          type: string
+                          description: "Privilege name"
+                      groups:
+                        type: array
+                        items:
+                          type: string
+                          description: "Privilege name"
+                  columnCountUserOther:
+                    type: number
+                    description: "The number of additional user privileges added by plugins"
+                  columnCountGroupOther:
+                    type: number
+                    description: "The number of additional user privileges added by plugins"
\ No newline at end of file
diff --git a/public/openapi/write/chats.yaml b/public/openapi/write/chats.yaml
new file mode 100644
index 0000000000..519b9f6556
--- /dev/null
+++ b/public/openapi/write/chats.yaml
@@ -0,0 +1,192 @@
+get:
+  tags:
+    - chats
+  summary: list recent chat rooms
+  description: This operation lists recently used chat rooms that the calling user is a part of
+  parameters:
+    - in: query
+      name: perPage
+      schema:
+        type: number
+      description: The number of chat rooms displayed per page
+      example: 20
+    - in: query
+      name: page
+      schema:
+        type: number
+      description: The page number
+      example: 1
+  responses:
+    '200':
+      description: chat rooms successfully listed
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../components/schemas/Chats.yaml#/RoomObject
+                  - type: object
+                    properties:
+                      unread:
+                        type: boolean
+                        description: Whether or not the chat has unread messages within
+                      teaser:
+                        type: object
+                        nullable: true
+                        properties:
+                          fromuid:
+                            type: number
+                          content:
+                            type: string
+                          timestamp:
+                            type: number
+                          timestampISO:
+                            type: string
+                          user:
+                            type: object
+                            properties:
+                              uid:
+                                type: number
+                                description: A user identifier
+                              username:
+                                type: string
+                                description: A friendly name for a given user account
+                              displayname:
+                                type: string
+                                description: This is either username or fullname depending on forum and user settings
+                              userslug:
+                                type: string
+                                description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                  removed, etc.)
+                              picture:
+                                nullable: true
+                                type: string
+                              status:
+                                type: string
+                              lastonline:
+                                type: number
+                              icon:text:
+                                type: string
+                                description: A single-letter representation of a username. This is used in the
+                                  auto-generated icon given to users
+                                  without an avatar
+                              icon:bgColor:
+                                type: string
+                                description: A six-character hexadecimal colour code assigned to the user. This
+                                  value is used in conjunction with
+                                  `icon:text` for the user's
+                                  auto-generated icon
+                                example: "#f44336"
+                              lastonlineISO:
+                                type: string
+                      users:
+                        type: array
+                        items:
+                          type: object
+                          properties:
+                            uid:
+                              type: number
+                              description: A user identifier
+                            username:
+                              type: string
+                              description: A friendly name for a given user account
+                            displayname:
+                              type: string
+                              description: This is either username or fullname depending on forum and user settings
+                            userslug:
+                              type: string
+                              description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                                removed, etc.)
+                            picture:
+                              nullable: true
+                              type: string
+                            status:
+                              type: string
+                            lastonline:
+                              type: number
+                            icon:text:
+                              type: string
+                              description: A single-letter representation of a username. This is used in the
+                                auto-generated icon given to users
+                                without an avatar
+                            icon:bgColor:
+                              type: string
+                              description: A six-character hexadecimal colour code assigned to the user. This
+                                value is used in conjunction with
+                                `icon:text` for the user's
+                                auto-generated icon
+                              example: "#f44336"
+                            lastonlineISO:
+                              type: string
+                      lastUser:
+                        type: object
+                        properties:
+                          uid:
+                            type: number
+                            description: A user identifier
+                          username:
+                            type: string
+                            description: A friendly name for a given user account
+                          displayname:
+                            type: string
+                            description: This is either username or fullname depending on forum and user settings
+                          userslug:
+                            type: string
+                            description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                              removed, etc.)
+                          picture:
+                            nullable: true
+                            type: string
+                          status:
+                            type: string
+                          lastonline:
+                            type: number
+                          icon:text:
+                            type: string
+                            description: A single-letter representation of a username. This is used in the
+                              auto-generated icon given to users
+                              without an avatar
+                          icon:bgColor:
+                            type: string
+                            description: A six-character hexadecimal colour code assigned to the user. This
+                              value is used in conjunction with
+                              `icon:text` for the user's
+                              auto-generated icon
+                            example: "#f44336"
+                          lastonlineISO:
+                            type: string
+                      usernames:
+                        type: string
+post:
+  tags:
+    - chats
+  summary: create a chat room
+  description: This operation creates a new chat room and adds users to the room, if provided.
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            uids:
+              type: array
+              example: [2]
+          required:
+            - uids
+  responses:
+    '200':
+      description: chat room successfully created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../components/schemas/Chats.yaml#/RoomObject
\ No newline at end of file
diff --git a/public/openapi/write/chats/roomId.yaml b/public/openapi/write/chats/roomId.yaml
new file mode 100644
index 0000000000..3473c92e1b
--- /dev/null
+++ b/public/openapi/write/chats/roomId.yaml
@@ -0,0 +1,128 @@
+head:
+  tags:
+    - chats
+  summary: check if a chat room exists
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: room ID to check
+      example: 1
+  responses:
+    '200':
+      description: chat room found
+    '404':
+      description: chat room not found
+get:
+  tags:
+    - chats
+  summary: get a chat room
+  description: This operation retrieves a chat room's data including users and messages
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+  responses:
+    '200':
+      description: Chat room successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/Chats.yaml#/RoomObjectFull
+post:
+  tags:
+    - chats
+  summary: send a chat message
+  description: This operation sends a chat message to a chat room
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            message:
+              type: string
+              example: This is a test message
+          required:
+            - message
+  responses:
+    '200':
+      description: message successfully sent
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../../components/schemas/Chats.yaml#/MessageObject
+                  - type: object
+                    properties:
+                      self:
+                        type: number
+                        description: Whether or not the message was sent by the calling user (which if you're using this route, will always be 1)
+                      newSet:
+                        type: boolean
+                        description: Whether the message is considered part of a new "set" of messages. It is used in the frontend UI for explicitly denoting that a time gap existed between messages.
+                      cleanedContent:
+                        type: string
+                      mid:
+                        type: number
+put:
+  tags:
+    - chats
+  summary: rename a chat room
+  description: This operation renames a chat room.
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid room id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            name:
+              type: string
+              description: the new name of the room
+              example: 'casper the friendly room'
+  responses:
+    '200':
+      description: Chat room renamed
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/Chats.yaml#/RoomObjectFull
\ No newline at end of file
diff --git a/public/openapi/write/chats/roomId/messages.yaml b/public/openapi/write/chats/roomId/messages.yaml
new file mode 100644
index 0000000000..ee981a871f
--- /dev/null
+++ b/public/openapi/write/chats/roomId/messages.yaml
@@ -0,0 +1,54 @@
+get:
+  tags:
+    - chats
+  summary: get chat room messages
+  description: This operation retrieves the messages in a chat room, with pagination options accepted
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+    - in: query
+      name: uid
+      schema:
+        type: number
+      description: a valid user id
+      example: 1
+    - in: query
+      name: start
+      schema:
+        type: number
+      description: At which chat message index to start returning messages from
+      example: 0
+  responses:
+    '200':
+      description: Messages successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  messages:
+                    type: array
+                    items:
+                      allOf:
+                        - $ref: ../../../components/schemas/Chats.yaml#/MessageObject
+                        - type: object
+                          description: Optional properties that may or may not be present (except for `messageId`, which is always present, and is only here as a hack to pass validation)
+                          properties:
+                            messageId:
+                              type: number
+                            index:
+                              type: number
+                            isOwner:
+                              type: boolean
+                          required:
+                            - messageId
\ No newline at end of file
diff --git a/public/openapi/write/chats/roomId/messages/mid.yaml b/public/openapi/write/chats/roomId/messages/mid.yaml
new file mode 100644
index 0000000000..5053f1546d
--- /dev/null
+++ b/public/openapi/write/chats/roomId/messages/mid.yaml
@@ -0,0 +1,141 @@
+get:
+  tags:
+    - chats
+  summary: get a chat message
+  description: This operation retrieves a single chat room message, by its id
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+    - in: path
+      name: mid
+      schema:
+        type: number
+      required: true
+      description: a valid message id
+      example: 1
+  responses:
+    '200':
+      description: Message successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../../components/schemas/Chats.yaml#/MessageObject
+put:
+  tags:
+    - chats
+  summary: edit a chat message
+  description: This operation edits a chat message.
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+    - in: path
+      name: mid
+      schema:
+        type: number
+      required: true
+      description: a valid message id
+      example: 5
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            message:
+              type: string
+              description: message content
+              example: 'edited message'
+  responses:
+    '200':
+      description: Message successfully edited
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../../components/schemas/Chats.yaml#/MessageObject
+delete:
+  tags:
+    - chats
+  summary: delete a chat message
+  description: This operation deletes a chat message
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+    - in: path
+      name: mid
+      schema:
+        type: number
+      required: true
+      description: a valid message id
+      example: 5
+  responses:
+    '200':
+      description: Message successfully deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+post:
+  tags:
+    - chats
+  summary: restore a chat message
+  description: This operation restores a delete chat message
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+    - in: path
+      name: mid
+      schema:
+        type: number
+      required: true
+      description: a valid message id
+      example: 5
+  responses:
+    '200':
+      description: message successfully restored
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/chats/roomId/users.yaml b/public/openapi/write/chats/roomId/users.yaml
new file mode 100644
index 0000000000..c96366509f
--- /dev/null
+++ b/public/openapi/write/chats/roomId/users.yaml
@@ -0,0 +1,103 @@
+get:
+  tags:
+    - chats
+  summary: get chat room users
+  description: This operation retrieves the users in a chat room message
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+  responses:
+    '200':
+      description: Users successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../components/schemas/Chats.yaml#/RoomUserList
+post:
+  tags:
+    - chats
+  summary: add users to chat room
+  description: This operation invites users to a chat room
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            uids:
+              type: array
+              description: A list of valid uids
+              example: [2, 4]
+              items:
+                type: number
+                description: A valid uid
+  responses:
+    '200':
+      description: users successfully invited to chat room
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../components/schemas/Chats.yaml#/RoomUserList
+delete:
+  tags:
+    - chats
+  summary: leave/remove users from chat room
+  description: This operation removes (kicks) multiple user from a chat room, or leaves the chat room if the requested user is the same as the calling user.
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            uids:
+              type: array
+              description: A list of valid uids
+              example: [2]
+              items:
+                type: number
+                description: A valid uid
+  responses:
+    '200':
+      description: users successfully removed from chat room
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../components/schemas/Chats.yaml#/RoomUserList
\ No newline at end of file
diff --git a/public/openapi/write/chats/roomId/users/uid.yaml b/public/openapi/write/chats/roomId/users/uid.yaml
new file mode 100644
index 0000000000..5bd028effa
--- /dev/null
+++ b/public/openapi/write/chats/roomId/users/uid.yaml
@@ -0,0 +1,32 @@
+delete:
+  tags:
+    - chats
+  summary: leave/remove one user from chat room
+  description: This operation removes (kicks) a single user from a chat room, or leaves the chat room if the requested user is the same as the calling user.
+  parameters:
+    - in: path
+      name: roomId
+      schema:
+        type: number
+      required: true
+      description: a valid chat room id
+      example: 1
+    - in: path
+      name: uid
+      schema:
+        type: number
+      required: true
+      description: a valid user id
+      example: 4
+  responses:
+    '200':
+      description: user successfully removed from chat room
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../../components/schemas/Chats.yaml#/RoomUserList
\ No newline at end of file
diff --git a/public/openapi/write/files.yaml b/public/openapi/write/files.yaml
new file mode 100644
index 0000000000..23c497db38
--- /dev/null
+++ b/public/openapi/write/files.yaml
@@ -0,0 +1,31 @@
+delete:
+  tags:
+    - files
+  summary: delete uploaded file
+  description: This operation deletes a file uploaded to NodeBB
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            path:
+              type: string
+              description: Path to the file (relative to the configured `upload_path`)
+              example: files/test.txt
+          required:
+            - path
+  responses:
+    '200':
+      description: File deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/files/folder.yaml b/public/openapi/write/files/folder.yaml
new file mode 100644
index 0000000000..84295a2917
--- /dev/null
+++ b/public/openapi/write/files/folder.yaml
@@ -0,0 +1,36 @@
+put:
+  tags:
+    - files
+  summary: create a new folder
+  description: This operation creates a new folder inside upload path
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            path:
+              type: string
+              description: Path to the file (relative to the configured `upload_path`)
+              example: /files
+            folderName:
+              type: string
+              description: New folder name
+              example: myfiles
+          required:
+            - path
+            - folderName
+  responses:
+    '200':
+      description: Folder created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/flags.yaml b/public/openapi/write/flags.yaml
new file mode 100644
index 0000000000..88d63dcc70
--- /dev/null
+++ b/public/openapi/write/flags.yaml
@@ -0,0 +1,38 @@
+post:
+  tags:
+    - flags
+  summary: create a flag
+  description: This operation creates a new flag (with a report). If a flag already exists for a given user or post, a report will be appended to the existing flag. The response will change depending on the privilege level of the calling uid. Privileged users (moderators and up) will see the full flag details, whereas regular users will see an empty (but successful) response.
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            type:
+              type: string
+              enum: ['post', 'user']
+              example: 'post'
+            id:
+              type: number
+              example: 2
+            reason:
+              type: string
+              example: 'Spam'
+          required:
+            - type
+            - id
+            - reason
+  responses:
+    '200':
+      description: flag successfully created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../components/schemas/FlagObject.yaml#/FlagObject
\ No newline at end of file
diff --git a/public/openapi/write/flags/flagId.yaml b/public/openapi/write/flags/flagId.yaml
new file mode 100644
index 0000000000..7f30e92835
--- /dev/null
+++ b/public/openapi/write/flags/flagId.yaml
@@ -0,0 +1,95 @@
+get:
+  tags:
+    - flags
+  summary: get a flag
+  description: This operation retrieve a flag's details. It is only available to privileged users (that is, moderators, global moderators, and administrators).
+  parameters:
+    - in: path
+      name: flagId
+      schema:
+        type: number
+      required: true
+      description: a valid flag id
+      example: 1
+  responses:
+    '200':
+      description: flag successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/FlagObject.yaml#/FlagObject
+put:
+  tags:
+    - flags
+  summary: update a flag
+  description: This operation updates a flag's details. It is only available to privileged users (that is, moderators, global moderators, and administrators).
+  parameters:
+    - in: path
+      name: flagId
+      schema:
+        type: number
+      required: true
+      description: a valid flag id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            datetime:
+              type: number
+              example: 1625859990035
+            state:
+              type: string
+              enum: ['open', 'wip', 'resolved', 'rejected']
+              example: 'wip'
+            assignee:
+              type: number
+              example: 1
+  responses:
+    '200':
+      description: flag successfully updated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/FlagObject.yaml#/FlagHistoryObject
+delete:
+  tags:
+    - flags
+  summary: delete a flag
+  description: |
+    This operation deletes a flag. Unlike posts and topics, flags are not marked as deleted.
+    This deletion endpoint will purge the flag and all of its associated content from the database.
+  parameters:
+    - in: path
+      name: flagId
+      schema:
+        type: number
+      required: true
+      description: a valid flag id
+      example: 1
+  responses:
+    '200':
+      description: Flag successfully deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/flags/flagId/notes.yaml b/public/openapi/write/flags/flagId/notes.yaml
new file mode 100644
index 0000000000..1a52881d71
--- /dev/null
+++ b/public/openapi/write/flags/flagId/notes.yaml
@@ -0,0 +1,42 @@
+post:
+  tags:
+    - flags
+  summary: append a flag note
+  description: This operation append a shared note for a given flag. It is only available to privileged users (that is, moderators, global moderators, and administrators).
+  parameters:
+    - in: path
+      name: flagId
+      schema:
+        type: number
+      required: true
+      description: a valid flag id
+      example: 2
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            note:
+              type: string
+              example: 'test note'
+            datetime:
+              type: number
+              example: 1626446956652
+          required:
+            - note
+  responses:
+    '200':
+      description: flag note successfully added or updated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../../../components/schemas/FlagObject.yaml#/FlagNotesObject
+                  - $ref: ../../../components/schemas/FlagObject.yaml#/FlagHistoryObject
\ No newline at end of file
diff --git a/public/openapi/write/flags/flagId/notes/datetime.yaml b/public/openapi/write/flags/flagId/notes/datetime.yaml
new file mode 100644
index 0000000000..bfd296b69b
--- /dev/null
+++ b/public/openapi/write/flags/flagId/notes/datetime.yaml
@@ -0,0 +1,34 @@
+delete:
+  tags:
+    - flags
+  summary: delete a flag note
+  description: This operation deletes a shared note for a given flag. It is only available to privileged users (that is, moderators, global moderators, and administrators).
+  parameters:
+    - in: path
+      name: flagId
+      schema:
+        type: number
+      required: true
+      description: a valid flag id
+      example: 2
+    - in: path
+      name: datetime
+      schema:
+        type: number
+      required: true
+      description: A valid UNIX timestamp
+      example: 1626446956652
+  responses:
+    '200':
+      description: Flag note deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../../../../components/schemas/FlagObject.yaml#/FlagNotesObject
+                  - $ref: ../../../../components/schemas/FlagObject.yaml#/FlagHistoryObject
\ No newline at end of file
diff --git a/public/openapi/write/groups.yaml b/public/openapi/write/groups.yaml
new file mode 100644
index 0000000000..8d325c758e
--- /dev/null
+++ b/public/openapi/write/groups.yaml
@@ -0,0 +1,58 @@
+post:
+  tags:
+    - groups
+  summary: create a new group
+  description: This operation creates a new group
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            name:
+              type: string
+              example: 'My Test Group'
+            timestamp:
+              type: number
+            disableJoinRequests:
+              type: number
+              enum: [0, 1]
+            disableLeave:
+              type: number
+              enum: [0, 1]
+            hidden:
+              type: number
+              enum: [0, 1]
+              example: 1
+            ownerUid:
+              type: number
+            private:
+              type: number
+              enum: [0, 1]
+            description:
+              type: string
+            userTitleEnabled:
+              type: number
+              enum: [0, 1]
+            createtime:
+              type: number
+            memberPostCids:
+              type: array
+              items:
+                type: number
+                example: [1, 2, 3]
+          required:
+            - name
+  responses:
+    '200':
+      description: group successfully created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../components/schemas/GroupObject.yaml#/GroupDataObject
\ No newline at end of file
diff --git a/public/openapi/write/groups/slug.yaml b/public/openapi/write/groups/slug.yaml
new file mode 100644
index 0000000000..4959da9f12
--- /dev/null
+++ b/public/openapi/write/groups/slug.yaml
@@ -0,0 +1,80 @@
+head:
+  tags:
+    - groups
+  summary: check if a group exists
+  parameters:
+    - in: path
+      name: slug
+      schema:
+        type: string
+      required: true
+      description: group slug (that also acts as its identifier) to check
+      example: my-test-group
+  responses:
+    '200':
+      description: group found
+    '404':
+      description: group not found
+put:
+  tags:
+    - groups
+  summary: update group data
+  parameters:
+    - in: path
+      name: slug
+      schema:
+        type: string
+      required: true
+      description: slug of the group you wish to update
+      example: my-test-group
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            icon:
+              type: string
+              example: fa-times
+          additionalProperties:
+            description: An object of group properties you wish to update
+            example:
+  responses:
+    '200':
+      description: group successfully updated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/GroupObject.yaml#/GroupDataObject
+delete:
+  tags:
+    - groups
+  summary: Delete an existing group
+  description: This operation deletes an existing group, all members within this group will cease to be members after the group is deleted.
+  parameters:
+    - in: path
+      name: slug
+      schema:
+        type: string
+      required: true
+      description: slug of the group you wish to delete
+      example: my-test-group
+  responses:
+    '200':
+      description: group successfully deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/groups/slug/membership/uid.yaml b/public/openapi/write/groups/slug/membership/uid.yaml
new file mode 100644
index 0000000000..9a5fd7df47
--- /dev/null
+++ b/public/openapi/write/groups/slug/membership/uid.yaml
@@ -0,0 +1,66 @@
+put:
+  tags:
+    - groups
+  summary: join a group
+  description: This operation joins an existing group, or causes another user to join a group. If the group is private and you are not an administrator, this method will cause that user to request membership, instead. For user _invitations_, you'll want to call `PUT /groups/{slug}/invites/{uid}`.
+  parameters:
+    - in: path
+      name: slug
+      schema:
+        type: string
+      required: true
+      description: slug of the group you would like to join
+      example: test-group
+    - in: path
+      name: uid
+      schema:
+        type: number
+      required: true
+      description: uid of the user to join the group
+      example: 1
+  responses:
+    '200':
+      description: group successfully joined, or membership requested
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - groups
+  summary: leave a group
+  description: This operation leaves a group.
+  parameters:
+    - in: path
+      name: slug
+      schema:
+        type: string
+      required: true
+      description: slug of the group you would like to leave
+      example: test-group
+    - in: path
+      name: uid
+      schema:
+        type: number
+      required: true
+      description: uid of the user to leave the group
+      example: 1
+  responses:
+    '200':
+      description: group successfully left
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/groups/slug/ownership/uid.yaml b/public/openapi/write/groups/slug/ownership/uid.yaml
new file mode 100644
index 0000000000..ba8e16558e
--- /dev/null
+++ b/public/openapi/write/groups/slug/ownership/uid.yaml
@@ -0,0 +1,66 @@
+put:
+  tags:
+    - groups
+  summary: grant group ownership
+  description: This operation grants ownership privilege to a user.
+  parameters:
+    - in: path
+      name: slug
+      schema:
+        type: string
+      required: true
+      description: slug of the group you would like to grant ownership
+      example: test-group
+    - in: path
+      name: uid
+      schema:
+        type: number
+      required: true
+      description: uid of the user to grant ownership
+      example: 1
+  responses:
+    '200':
+      description: ownership successfully granted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - groups
+  summary: rescind group ownership
+  description: 'This operation rescinds ownership privilege from a user. **Note**: Every group needs at least one owner, so if you are attempting to remove the last owner of a group, this call will fail.'
+  parameters:
+    - in: path
+      name: slug
+      schema:
+        type: string
+      required: true
+      description: slug of the group you would like to rescind ownership
+      example: test-group
+    - in: path
+      name: uid
+      schema:
+        type: number
+      required: true
+      description: uid of the user to rescind ownership from
+      example: 2
+  responses:
+    '200':
+      description: ownership successfully rescinded
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/login.yaml b/public/openapi/write/login.yaml
new file mode 100644
index 0000000000..001cd5da01
--- /dev/null
+++ b/public/openapi/write/login.yaml
@@ -0,0 +1,32 @@
+post:
+  tags:
+    - utilities
+  summary: verify login credentials
+  description: |
+    This route accepts a username/password or email/password pair (dependent on forum settings), returning a standard user object if credentials are validated successfully.
+    This route also initializes a standard login session and returns a valid cookie that can be used in subsequent API calls as though it were a browser session.
+    **Note**: Cookie-based sessions require a CSRF token for non-`GET` routes.
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            username:
+              type: string
+              example: admin
+            password:
+              type: string
+              example: '123456'
+  responses:
+    '200':
+      description: credentials successfully validated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../components/schemas/UserObject.yaml#/UserObjectSlim
\ No newline at end of file
diff --git a/public/openapi/write/ping.yaml b/public/openapi/write/ping.yaml
new file mode 100644
index 0000000000..67d3dd62ba
--- /dev/null
+++ b/public/openapi/write/ping.yaml
@@ -0,0 +1,57 @@
+get:
+  tags:
+    - utilities
+  summary: test route
+  description: This route responds with a simple `200 OK` if the Write API is enabled. Since there is no way of disabling the Write API, this will always return a success. However, it is also a good way to ensure the instance you are calling supports v3 of the Write API.
+  responses:
+    '200':
+      description: pingback
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  pong:
+                    type: boolean
+                    example: true
+post:
+  tags:
+    - utilities
+  summary: test route
+  description: |
+    Requires authentication. This route bounces back the data payload sent to it, and the uid the token resolved to.
+
+    It is also a good way to ensure the instance you are calling supports v3 of the Write API. Also, as it requires authentication, it is a good way to check if the passed-in token is a valid token.
+  requestBody:
+    required: false
+    content:
+      application/json:
+        schema:
+          type: object
+          properties: {}
+          additionalProperties: {}
+  responses:
+    '200':
+      description: pingback
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  uid:
+                    type: number
+                    description: The `uid` that the passed-in token resolves to.
+                  received:
+                    type: object
+                    description: A free-form object containing the data that was passed to it. It reflects the data payload as the Write API understands it, and it may be useful to call this route to see how a request body is parsed, if at all.
+                    additionalProperties: {}
\ No newline at end of file
diff --git a/public/openapi/write/posts/pid.yaml b/public/openapi/write/posts/pid.yaml
new file mode 100644
index 0000000000..593a7acd01
--- /dev/null
+++ b/public/openapi/write/posts/pid.yaml
@@ -0,0 +1,141 @@
+get:
+  tags:
+    - posts
+  summary: get a post
+  description: This operation retrieves a post's data
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 1
+  responses:
+    '200':
+      description: Post successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  pid:
+                    type: number
+                  uid:
+                    type: number
+                    description: A user identifier
+                  tid:
+                    type: number
+                    description: A topic identifier
+                  content:
+                    type: string
+                  timestamp:
+                    type: number
+                  flagId:
+                    type: number
+                  deleted:
+                    type: number
+                  upvotes:
+                    type: number
+                  downvotes:
+                    type: number
+                  deleterUid:
+                    type: number
+                  edited:
+                    type: number
+                  replies:
+                    type: number
+                  bookmarks:
+                    type: number
+                  votes:
+                    type: number
+                  timestampISO:
+                    type: string
+                    description: An ISO 8601 formatted date string (complementing `timestamp`)
+                  editedISO:
+                    type: string
+                    description: An ISO 8601 formatted date string (complementing `timestamp`)
+                  upvoted:
+                    type: boolean
+                  downvoted:
+                    type: boolean
+put:
+  tags:
+    - posts
+  summary: edit a post
+  description: This operation edits a post
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            content:
+              type: string
+              description: New post content
+              example: New post content
+            title:
+              type: string
+              description: Topic title, only accepted for main posts
+              example: New title
+          required:
+            - content
+  responses:
+    '200':
+      description: Post successfully edited
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../../components/schemas/PostObject.yaml#/PostObject
+                  - type: object
+                    properties:
+                      edited:
+                        type: boolean
+                      deleterUid:
+                        type: number
+delete:
+  tags:
+    - posts
+  summary: purge a post
+  description: This operation purges a post.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 1
+  responses:
+    '200':
+      description: Post successfully purged
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/posts/pid/bookmark.yaml b/public/openapi/write/posts/pid/bookmark.yaml
new file mode 100644
index 0000000000..2ebce0e2ff
--- /dev/null
+++ b/public/openapi/write/posts/pid/bookmark.yaml
@@ -0,0 +1,52 @@
+put:
+  tags:
+    - posts
+  summary: bookmark a post
+  description: This operation bookmarks a post.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+  responses:
+    '200':
+      description: Post successfully bookmarked
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - posts
+  summary: unbookmark a post
+  description: This operation unbookmarks a post.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+  responses:
+    '200':
+      description: Post successfully unbookmarked
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/posts/pid/diffs.yaml b/public/openapi/write/posts/pid/diffs.yaml
new file mode 100644
index 0000000000..ea76f7ea66
--- /dev/null
+++ b/public/openapi/write/posts/pid/diffs.yaml
@@ -0,0 +1,43 @@
+get:
+  tags:
+    - posts
+  summary: get post edit history
+  description: This operation retrieves a post's edit history
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+  responses:
+    '200':
+      description: Post history successfully retrieved.
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  timestamps:
+                    type: array
+                    items:
+                      type: string
+                  revisions:
+                    type: array
+                    items:
+                      type: object
+                      properties:
+                        timestamp:
+                          type: string
+                        username:
+                          type: string
+                  editable:
+                    type: boolean
+                  deletable:
+                    type: boolean
diff --git a/public/openapi/write/posts/pid/diffs/since.yaml b/public/openapi/write/posts/pid/diffs/since.yaml
new file mode 100644
index 0000000000..8db8c6f4ac
--- /dev/null
+++ b/public/openapi/write/posts/pid/diffs/since.yaml
@@ -0,0 +1,65 @@
+get:
+  tags:
+    - posts
+  summary: get single post edit history
+  description: This operation retrieves a post's edit history
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+    - in: path
+      name: since
+      schema:
+        type: number
+      required: true
+      description: a valid UNIX timestamp
+      example: 0
+  responses:
+    '200':
+      description: Post history successfully retrieved.
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../../components/schemas/PostObject.yaml#/PostObject
+put:
+  tags:
+    - posts
+  summary: revert a post
+  description: This operation reverts a post to an earlier version. The revert process will append a new history item to the post's edit history.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+    - in: path
+      name: since
+      schema:
+        type: number
+      required: true
+      description: a valid UNIX timestamp
+      example: 0
+  responses:
+    '200':
+      description: Post successfully reverted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/posts/pid/diffs/timestamp.yaml b/public/openapi/write/posts/pid/diffs/timestamp.yaml
new file mode 100644
index 0000000000..15d942fc26
--- /dev/null
+++ b/public/openapi/write/posts/pid/diffs/timestamp.yaml
@@ -0,0 +1,25 @@
+delete:
+  tags:
+    - posts
+  summary: delete a post diff
+  description: This operation deletes a post diff from its history.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+    - in: path
+      name: timestamp
+      schema:
+        type: number
+      required: true
+      description: a valid UNIX timestamp
+      example: 1611850000000
+  responses:
+    '200':
+      description: Post diff successfully deleted
+      content:
+        $ref: ../diffs.yaml#/get/responses/200/content
\ No newline at end of file
diff --git a/public/openapi/write/posts/pid/move.yaml b/public/openapi/write/posts/pid/move.yaml
new file mode 100644
index 0000000000..7198554ffb
--- /dev/null
+++ b/public/openapi/write/posts/pid/move.yaml
@@ -0,0 +1,36 @@
+put:
+  tags:
+    - posts
+  summary: move a post
+  description: This operation moves a post to a different topic.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: number
+      required: true
+      description: a valid post id
+      example: 5
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            tid:
+              type: number
+              description: a valid topic id
+              example: 4
+  responses:
+    '200':
+      description: Post successfully moved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/posts/pid/state.yaml b/public/openapi/write/posts/pid/state.yaml
new file mode 100644
index 0000000000..6403b74860
--- /dev/null
+++ b/public/openapi/write/posts/pid/state.yaml
@@ -0,0 +1,52 @@
+delete:
+  tags:
+    - posts
+  summary: deletes a post
+  description: This operation soft deletes a post.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+  responses:
+    '200':
+      description: Post successfully deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+put:
+  tags:
+    - posts
+  summary: restore a post
+  description: This operation restores a post.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+  responses:
+    '200':
+      description: Topic successfully restored
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/posts/pid/vote.yaml b/public/openapi/write/posts/pid/vote.yaml
new file mode 100644
index 0000000000..0cdb895a81
--- /dev/null
+++ b/public/openapi/write/posts/pid/vote.yaml
@@ -0,0 +1,63 @@
+put:
+  tags:
+    - posts
+  summary: vote on a post
+  description: This operation casts a vote on a post.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            delta:
+              type: number
+              description: Positive integer for upvote, negative integer for downvote (0 to unvote.)
+              example: 1
+  responses:
+    '200':
+      description: Post successfully upvoted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - posts
+  summary: unvote a post
+  description: This operation removes a pre-cast vote on a post.
+  parameters:
+    - in: path
+      name: pid
+      schema:
+        type: string
+      required: true
+      description: a valid post id
+      example: 2
+  responses:
+    '200':
+      description: Post successfully unvoted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics.yaml b/public/openapi/write/topics.yaml
new file mode 100644
index 0000000000..ba00cf0024
--- /dev/null
+++ b/public/openapi/write/topics.yaml
@@ -0,0 +1,46 @@
+post:
+  tags:
+    - topics
+  summary: create a new topic
+  description: This operation creates a new topic with a post. Topic creation without a post is not allowed via the Write API as it is an internal-only method.
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            cid:
+              type: number
+              example: 1
+            title:
+              type: string
+              example: Test topic
+            content:
+              type: string
+              example: This is the test topic's content
+            tags:
+              type: array
+              items:
+                type: string
+              example: [test, topic]
+          required:
+            - cid
+            - title
+            - content
+  responses:
+    '200':
+      description: topic successfully created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                allOf:
+                  - $ref: ../components/schemas/TopicObject.yaml#/TopicObject
+                  - type: object
+                    properties:
+                      mainPost: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid.yaml b/public/openapi/write/topics/tid.yaml
new file mode 100644
index 0000000000..8e68efe25a
--- /dev/null
+++ b/public/openapi/write/topics/tid.yaml
@@ -0,0 +1,92 @@
+get:
+  tags:
+    - topics
+  summary: get a topic
+  description: This operation retrieves a topic's data
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/TopicObject.yaml#/TopicObjectSlim
+post:
+  tags:
+    - topics
+  summary: reply to a topic
+  description: This operation creates a new reply to an existing topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 2
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            content:
+              type: string
+              example: This is a test reply
+            timestamp:
+              type: number
+            toPid:
+              type: number
+          required:
+            - content
+  responses:
+    '200':
+      description: post successfully created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/PostObject.yaml#/PostObject
+delete:
+  tags:
+    - topics
+  summary: delete a topic
+  description: This operation purges a topic and all of its posts (careful, there is no confirmation!)
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 3
+  responses:
+    '200':
+      description: Topic successfully purged
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/events.yaml b/public/openapi/write/topics/tid/events.yaml
new file mode 100644
index 0000000000..0bf03cd604
--- /dev/null
+++ b/public/openapi/write/topics/tid/events.yaml
@@ -0,0 +1,85 @@
+get:
+  tags:
+    - topics
+  summary: get topic events
+  description: This operation retrieves a topic's events
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic events successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: array
+                description: A list of the topic events that still remain
+                items:
+                  type: object
+                  properties:
+                    type:
+                      type: string
+                      description: A valid event type
+                    id:
+                      type: number
+                      description: Unique identifier for this topic event
+                    timestamp:
+                      type: number
+                    timestampISO:
+                      type: string
+                      description: An ISO 8601 formatted date string (complementing `timestamp`)
+                    icon:
+                      type: string
+                      description: FontAwesome icon name
+                      example: fa-bullhorn
+                    text:
+                      type: string
+                      description: A language key
+                    uid:
+                      type: number
+                    user:
+                      type: object
+                      properties:
+                        uid:
+                          type: number
+                          description: A user identifier
+                        username:
+                          type: string
+                          description: A friendly name for a given user account
+                        displayname:
+                          type: string
+                          description: This is either username or fullname depending on forum and user settings
+                        userslug:
+                          type: string
+                          description: An URL-safe variant of the username (i.e. lower-cased, spaces
+                            removed, etc.)
+                        picture:
+                          nullable: true
+                          type: string
+                        icon:text:
+                          type: string
+                          description: A single-letter representation of a username. This is used in the
+                            auto-generated icon given to users
+                            without an avatar
+                        icon:bgColor:
+                          type: string
+                          description: A six-character hexadecimal colour code assigned to the user. This
+                            value is used in conjunction with
+                            `icon:text` for the user's
+                            auto-generated icon
+                          example: "#f44336"
+                  required:
+                    - type
+                    - id
+                    - timestamp
+                    - timestampISO
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/events/eventId.yaml b/public/openapi/write/topics/tid/events/eventId.yaml
new file mode 100644
index 0000000000..4b0de4ad53
--- /dev/null
+++ b/public/openapi/write/topics/tid/events/eventId.yaml
@@ -0,0 +1,33 @@
+delete:
+  tags:
+    - topics
+  summary: Delete a topic event
+  description: This operation deletes a single topic event from the topic
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+    - in: path
+      name: eventId
+      schema:
+        type: string
+      required: true
+      description: a valid topic event id
+      example: 1
+  responses:
+    '200':
+      description: Topic event successfully deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/follow.yaml b/public/openapi/write/topics/tid/follow.yaml
new file mode 100644
index 0000000000..eada4d56bc
--- /dev/null
+++ b/public/openapi/write/topics/tid/follow.yaml
@@ -0,0 +1,52 @@
+put:
+  tags:
+    - topics
+  summary: follow a topic
+  description: This operation follows (or watches) a topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully followed
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - topics
+  summary: unfollow a topic
+  description: This operation unfollows (or unwatches) a topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully unwatched
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/ignore.yaml b/public/openapi/write/topics/tid/ignore.yaml
new file mode 100644
index 0000000000..321c995c90
--- /dev/null
+++ b/public/openapi/write/topics/tid/ignore.yaml
@@ -0,0 +1,52 @@
+put:
+  tags:
+    - topics
+  summary: ignore a topic
+  description: This operation ignores (or watches) a topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully ignored
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - topics
+  summary: unignore a topic
+  description: This operation unignores (or unfollows/unwatches) a topic. It is functionally identical to `DEL /topics/{tid}/follow`.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully unignored/unwatched
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/lock.yaml b/public/openapi/write/topics/tid/lock.yaml
new file mode 100644
index 0000000000..224d0b8d50
--- /dev/null
+++ b/public/openapi/write/topics/tid/lock.yaml
@@ -0,0 +1,52 @@
+put:
+  tags:
+    - topics
+  summary: lock a topic
+  description: This operation locks an existing topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully locked
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - topics
+  summary: unlock a topic
+  description: This operation unlocks a topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully unlocked
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/pin.yaml b/public/openapi/write/topics/tid/pin.yaml
new file mode 100644
index 0000000000..b3f2d580ae
--- /dev/null
+++ b/public/openapi/write/topics/tid/pin.yaml
@@ -0,0 +1,52 @@
+put:
+  tags:
+    - topics
+  summary: pin a topic
+  description: This operation pins an existing topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully pinned
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - topics
+  summary: unpin a topic
+  description: This operation unpins a topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully unpinned
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/state.yaml b/public/openapi/write/topics/tid/state.yaml
new file mode 100644
index 0000000000..a82348953c
--- /dev/null
+++ b/public/openapi/write/topics/tid/state.yaml
@@ -0,0 +1,52 @@
+delete:
+  tags:
+    - topics
+  summary: delete a topic
+  description: This operation deletes an existing topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+put:
+  tags:
+    - topics
+  summary: restore a topic
+  description: This operation restores a topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic successfully restored
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/tags.yaml b/public/openapi/write/topics/tid/tags.yaml
new file mode 100644
index 0000000000..9f229d9707
--- /dev/null
+++ b/public/openapi/write/topics/tid/tags.yaml
@@ -0,0 +1,65 @@
+put:
+  tags:
+    - topics
+  summary: adds tags to a topic
+  description: This operation adds tags to a topic
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            tags:
+              type: array
+              description: 'An array of tags'
+              items:
+                type: string
+              example: [test, foobar]
+  responses:
+    '200':
+      description: Topic tags successfully added
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - topics
+  summary: Removes all tags from a topic
+  description: This operation removed all tags associated with a topic.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Topic tags successfully removed.
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/thumbs.yaml b/public/openapi/write/topics/tid/thumbs.yaml
new file mode 100644
index 0000000000..a51858aeb3
--- /dev/null
+++ b/public/openapi/write/topics/tid/thumbs.yaml
@@ -0,0 +1,160 @@
+get:
+  tags:
+    - topics
+  summary: get topic thumbnails
+  description: This operation retrieves a topic's uploaded thumbnails
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  responses:
+    '200':
+      description: Thumbnails successfully retrieved
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: array
+                description: A list of the topic thumbnails that still remain
+                items:
+                  type: object
+                  properties:
+                    id:
+                      type: string
+                    name:
+                      type: string
+                    url:
+                      type: string
+                      description: Path to a topic thumbnail
+post:
+  tags:
+    - topics
+  summary: add topic thumbnail
+  description: This operation adds a thumbnail to an existing topic or a draft (via a composer `uuid`)
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  requestBody:
+    content:
+      multipart/form-data:
+        schema:
+          type: object
+          properties:
+            files:
+              type: array
+              items:
+                type: string
+                format: binary
+  responses:
+    '200':
+      description: Thumbnail successfully added
+      content:
+        application/json:
+          schema:
+            type: array
+            items:
+              type: object
+              properties:
+                url:
+                  type: string
+                path:
+                  type: string
+                name:
+                  type: string
+put:
+  tags:
+    - topics
+  summary: migrate topic thumbnail
+  description: This operation migrates a thumbnails from a topic or draft, to another tid or draft.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id or draft uuid
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            tid:
+              type: string
+              description: a valid topic id or draft uuid
+              example: '1'
+  responses:
+    '200':
+      description: Topic thumbnails migrated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
+delete:
+  tags:
+    - topics
+  summary: remove topic thumbnail
+  description: This operation removes a topic thumbnail.
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            path:
+              type: string
+              description: Relative path to the topic thumbnail
+              example: files/test.png
+  responses:
+    '200':
+      description: Topic thumbnail removed
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: array
+                description: A list of the topic thumbnails that still remain
+                items:
+                  type: object
+                  properties:
+                    id:
+                      type: string
+                    name:
+                      type: string
+                    url:
+                      type: string
+                      description: Path to a topic thumbnail
\ No newline at end of file
diff --git a/public/openapi/write/topics/tid/thumbs/order.yaml b/public/openapi/write/topics/tid/thumbs/order.yaml
new file mode 100644
index 0000000000..8e9a12fef1
--- /dev/null
+++ b/public/openapi/write/topics/tid/thumbs/order.yaml
@@ -0,0 +1,41 @@
+put:
+  tags:
+    - topics
+  summary: reorder topic thumbnail
+  description: This operation sets the order for a topic thumbnail. It can handle either topics (if a valid `tid` is passed in), or drafts. A 404 is returned if the topic or draft does not actually contain that thumbnail path. Paths passed in should **not** contain the path to the uploads folder (`config.upload_url` on client side)
+  parameters:
+    - in: path
+      name: tid
+      schema:
+        type: string
+      required: true
+      description: a valid topic id or draft uuid
+      example: 2
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            path:
+              type: string
+              description: Relative path to the topic thumbnail
+              example: files/test.png
+            order:
+              type: number
+              description: The order of topic thumbnails. Lower numbers are loaded first.
+              example: 0
+  responses:
+    '200':
+      description: Topic thumbnail re-ordered
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties: {}
\ No newline at end of file
diff --git a/public/openapi/write/users.yaml b/public/openapi/write/users.yaml
new file mode 100644
index 0000000000..d5fcdf30e6
--- /dev/null
+++ b/public/openapi/write/users.yaml
@@ -0,0 +1,76 @@
+post:
+  tags:
+    - users
+  summary: create a user
+  description: This operation creates a new user account
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            username:
+              type: string
+              description: 'If the username is taken, a number will be appended'
+              example: Dragon Fruit
+            password:
+              type: string
+              example: s3cre7password
+            email:
+              type: string
+              example: dragonfruit@example.org
+          required:
+            - username
+  responses:
+    '200':
+      description: user successfully created
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../components/schemas/UserObj.yaml#/UserObj
+    '400':
+      $ref: ../components/responses/400.yaml#/400
+    '401':
+      $ref: ../components/responses/401.yaml#/401
+    '403':
+      $ref: ../components/responses/403.yaml#/403
+    '426':
+      $ref: ../components/responses/426.yaml#/426
+    '500':
+      $ref: ../components/responses/500.yaml#/500
+delete:
+  tags:
+    - users
+  summary: delete one or more users
+  description: This operation deletes one or many user accounts, including their contributions (posts, topics, etc.)
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            uids:
+              type: array
+              description: A collection of uids
+              items:
+                type: number
+              example: [5, 6]
+  responses:
+    '200':
+      description: user account(s) deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../components/schemas/Status.yaml#/Status
+              response:
+                type: object
diff --git a/public/openapi/write/users/uid.yaml b/public/openapi/write/users/uid.yaml
new file mode 100644
index 0000000000..c8cbcb61b7
--- /dev/null
+++ b/public/openapi/write/users/uid.yaml
@@ -0,0 +1,131 @@
+head:
+  tags:
+    - users
+  summary: check if a user exists
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to check
+      example: 3
+  responses:
+    '200':
+      description: user found
+    '404':
+      description: user not found
+get:
+  tags:
+    - users
+  summary: get a single user account
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to check
+      example: 3
+  responses:
+    '200':
+      description: successfully retrieved user profile
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/UserObj.yaml#/UserObj
+delete:
+  tags:
+    - users
+  summary: delete a single user account
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to delete
+      example: 3
+  responses:
+    '200':
+      description: user account deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+put:
+  tags:
+    - users
+  summary: update a user account
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to update
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            fullname:
+              type: string
+              example: Mr. Dragon Fruit Jr.
+            website:
+              type: string
+              example: 'https://example.org'
+            location:
+              type: string
+              example: 'Toronto, Canada'
+            groupTitle:
+              type: string
+              example: '["administrators","Staff"]'
+            birthday:
+              type: string
+              description: A birthdate given in an ISO format parseable by the Date object
+              example: 03/27/2020
+            signature:
+              type: string
+              example: |
+                This is an example signature
+                It can span multiple lines.
+            aboutme:
+              type: string
+              example: |
+                This is a paragraph all about how my life got twist-turned upside-down
+                and I'd like to take a minute and sit right here,
+                to tell you all about how I because the administrator of NodeBB
+  responses:
+    '200':
+      description: user profile updated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../components/schemas/UserObj.yaml#/UserObj
+    '401':
+      $ref: ../../components/responses/401.yaml#/401
+    '403':
+      $ref: ../../components/responses/403.yaml#/403
+    '426':
+      $ref: ../../components/responses/426.yaml#/426
+    '500':
+      $ref: ../../components/responses/500.yaml#/500
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/account.yaml b/public/openapi/write/users/uid/account.yaml
new file mode 100644
index 0000000000..51c149e5e6
--- /dev/null
+++ b/public/openapi/write/users/uid/account.yaml
@@ -0,0 +1,25 @@
+delete:
+  tags:
+    - users
+  summary: delete a single user account (preserve content)
+  description: This route deletes a single user's account, but preserves the content (posts, bookmarks, etc.)
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to delete
+      example: 7
+  responses:
+    '200':
+      description: user account deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/ban.yaml b/public/openapi/write/users/uid/ban.yaml
new file mode 100644
index 0000000000..624c6e8eb7
--- /dev/null
+++ b/public/openapi/write/users/uid/ban.yaml
@@ -0,0 +1,61 @@
+put:
+  tags:
+    - users
+  summary: ban a user
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to ban
+      example: 2
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            until:
+              type: number
+              description: UNIX timestamp of the ban expiry
+              example: 1585775608076
+            reason:
+              type: string
+              example: the reason for the ban
+  responses:
+    '200':
+      description: successfully banned user
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+delete:
+  tags:
+    - users
+  summary: unbans a user
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to unban
+      example: 2
+  responses:
+    '200':
+      description: successfully unbanned user
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/content.yaml b/public/openapi/write/users/uid/content.yaml
new file mode 100644
index 0000000000..7b5cd0f641
--- /dev/null
+++ b/public/openapi/write/users/uid/content.yaml
@@ -0,0 +1,25 @@
+delete:
+  tags:
+    - users
+  summary: delete a single user account's content (preserve account)
+  description: This route deletes a single user's account content (posts, bookmarks, etc.) but preserves the account itself
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user's content to delete
+      example: 7
+  responses:
+    '200':
+      description: user account content deleted
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/emails.yaml b/public/openapi/write/users/uid/emails.yaml
new file mode 100644
index 0000000000..8bd00809c0
--- /dev/null
+++ b/public/openapi/write/users/uid/emails.yaml
@@ -0,0 +1,33 @@
+get:
+  tags:
+    - users
+  summary: get user emails
+  description: |
+    This operation lists all emails associated with the user.
+    This route is accessible to all users if the target user has elected to show their email publicly. Otherwise, it is only accessible to privileged users, or if the calling user is the same as the target user.
+  parameters:
+    - in: path
+      required: true
+      name: uid
+      schema:
+        type: number
+      description: A valid user id
+      example: 1
+  responses:
+    '200':
+      description: user emails successfully listed
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+                properties:
+                  emails:
+                    type: array
+                    items:
+                      type: string
+                      description: An email address
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/emails/email.yaml b/public/openapi/write/users/uid/emails/email.yaml
new file mode 100644
index 0000000000..7f0118db5a
--- /dev/null
+++ b/public/openapi/write/users/uid/emails/email.yaml
@@ -0,0 +1,25 @@
+get:
+  tags:
+    - users
+  summary: get user's email data
+  description: |
+    This operation lists the data associated with a single email.
+    This route is accessible to all users if the target user has elected to show their email publicly. Otherwise, it is only accessible to privileged users, or if the calling user is the same as the target user.
+  parameters:
+    - in: path
+      required: true
+      name: uid
+      schema:
+        type: number
+      description: A valid user id
+      example: 1
+    - in: path
+      required: true
+      name: email
+      schema:
+        type: string
+      description: A valid email address
+      example: test@example.org
+  responses:
+    '204':
+      description: user's email data successfully retrieved
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/emails/email/confirm.yaml b/public/openapi/write/users/uid/emails/email/confirm.yaml
new file mode 100644
index 0000000000..6ccd34ff51
--- /dev/null
+++ b/public/openapi/write/users/uid/emails/email/confirm.yaml
@@ -0,0 +1,34 @@
+post:
+  tags:
+    - users
+  summary: validate a user's email address
+  description: |
+    Marks the passed-in user's email as confirmed.
+    This route is only accessible to administrators with the `admin:users` privilege (or superadmins)
+  parameters:
+    - in: path
+      required: true
+      name: uid
+      schema:
+        type: number
+      description: A valid user id
+      example: 1
+    - in: path
+      required: true
+      name: email
+      schema:
+        type: string
+      description: A valid email address
+      example: test@example.org
+  responses:
+    '200':
+      description: successfully confirmed a user email
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/exports/type.yaml b/public/openapi/write/users/uid/exports/type.yaml
new file mode 100644
index 0000000000..928cb9b8f4
--- /dev/null
+++ b/public/openapi/write/users/uid/exports/type.yaml
@@ -0,0 +1,85 @@
+head:
+  tags:
+    - users
+  summary: Check if a user's export exists
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to make the query for
+      example: 1
+    - in: path
+      name: type
+      schema:
+        type: string
+      required: true
+      description: The type of export to query
+      example: posts
+  responses:
+    '204':
+      description: Exported file found.
+    '404':
+      description: Exported file not found — this could be because an export has never been generated for this user.
+get:
+  tags:
+    - users
+  summary: Download a user's exported data
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to make the query for
+      example: 1
+    - in: path
+      name: type
+      schema:
+        type: string
+      required: true
+      description: The type of export to download
+      example: posts
+  responses:
+    '200':
+      description: A download containing the requested exported data
+    '404':
+      description: Exported file not found — this could be because an export has never been generated for this user.
+post:
+  tags:
+    - users
+  summary: Generate a user export
+  description: |
+    This operation generates a user export file for later download.
+    It will return immediately with the `202 Accepted` response code, meaning the request was accepted for processing.
+    The expected behaviour is for the client to then poll the corresponding `HEAD` method until it returns a `204 No Content`
+    (or if awaiting a new export, for the `Last-Modified` or `ETag` header to change)
+    at which point the `GET` method can be called for download.
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to make the query for
+      example: 1
+    - in: path
+      name: type
+      schema:
+        type: string
+      required: true
+      description: The type of export to download
+      example: posts
+  responses:
+    '202':
+      description: Successfully started generating the requested user export
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/follow.yaml b/public/openapi/write/users/uid/follow.yaml
new file mode 100644
index 0000000000..a993985333
--- /dev/null
+++ b/public/openapi/write/users/uid/follow.yaml
@@ -0,0 +1,48 @@
+put:
+  tags:
+    - users
+  summary: follow a user
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to follow
+      example: 2
+  responses:
+    '200':
+      description: successfully followed user
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+delete:
+  tags:
+    - users
+  summary: unfollows a user
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to unfollow
+      example: 2
+  responses:
+    '200':
+      description: successfully unfollowed user
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/invites.yaml b/public/openapi/write/users/uid/invites.yaml
new file mode 100644
index 0000000000..9fd3596296
--- /dev/null
+++ b/public/openapi/write/users/uid/invites.yaml
@@ -0,0 +1,48 @@
+post:
+  tags:
+    - users
+  summary: invite users with email by email
+  description: This operation sends an invitation email to the given addresses, with an option to join selected groups on acceptance
+  parameters:
+  - in: path
+    name: uid
+    schema:
+      type: integer
+    required: true
+    description: uid of the user sending invitations
+    example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            emails:
+              type: string
+              description: A single or list of comma separated email addresses
+              example: friend01@example.com,friend02@example.com
+            groupsToJoin:
+              type: array
+              description: A collection of group names
+              example: ['administrators']
+          required:
+            - emails
+  responses:
+    '200':
+      description: invitation email(s) sent
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+    '400':
+      $ref: ../../../components/responses/400.yaml#/400
+    '401':
+      $ref: ../../../components/responses/401.yaml#/401
+    '403':
+      $ref: ../../../components/responses/403.yaml#/403
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/invites/groups.yaml b/public/openapi/write/users/uid/invites/groups.yaml
new file mode 100644
index 0000000000..5683db658d
--- /dev/null
+++ b/public/openapi/write/users/uid/invites/groups.yaml
@@ -0,0 +1,23 @@
+get:
+  tags:
+    - users
+  summary: Get group names that the user can invite
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to make the query for
+      example: 1
+  responses:
+    '200':
+      description: A collection of group names returned
+      content:
+        application/json:
+          schema:
+            type: array
+            items:
+              type: string
+    '401':
+      $ref: ../../../../components/responses/401.yaml#/401
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/mute.yaml b/public/openapi/write/users/uid/mute.yaml
new file mode 100644
index 0000000000..7fa84c9b22
--- /dev/null
+++ b/public/openapi/write/users/uid/mute.yaml
@@ -0,0 +1,61 @@
+put:
+  tags:
+    - users
+  summary: mute a user
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to mute
+      example: 2
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            until:
+              type: number
+              description: UNIX timestamp of the mute expiry
+              example: 1585775608076
+            reason:
+              type: string
+              example: the reason for the mute
+  responses:
+    '200':
+      description: successfully muted user
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
+delete:
+  tags:
+    - users
+  summary: unmute a user
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to unmute
+      example: 2
+  responses:
+    '200':
+      description: successfully unmuted user
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/password.yaml b/public/openapi/write/users/uid/password.yaml
new file mode 100644
index 0000000000..1a52f85e53
--- /dev/null
+++ b/public/openapi/write/users/uid/password.yaml
@@ -0,0 +1,40 @@
+put:
+  tags:
+    - users
+  summary: change a user's password
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to update
+      example: 1
+  requestBody:
+    required: true
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            currentPassword:
+              type: string
+              description: test
+              example: '123456'
+            newPassword:
+              type: string
+              example: '123456'
+          required:
+            - newPassword
+  responses:
+    '200':
+      description: user profile updated
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/picture.yaml b/public/openapi/write/users/uid/picture.yaml
new file mode 100644
index 0000000000..d6498a0af2
--- /dev/null
+++ b/public/openapi/write/users/uid/picture.yaml
@@ -0,0 +1,43 @@
+put:
+  tags:
+    - users
+  summary: update user picture or icon background colour
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user
+      example: 1
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            type:
+              type: string
+              description: The source of the picture
+              enum: ['default', 'uploaded', 'external']
+              example: default
+            url:
+              type: string
+              description: Only used for `external` type, specifies the source of the external image to use as avatar
+              example: ''
+            bgColor:
+              type: string
+              description: A hexadecimal colour representation
+              example: '#ff0000'
+  responses:
+    '200':
+      description: successfully updated user picture
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/sessions/uuid.yaml b/public/openapi/write/users/uid/sessions/uuid.yaml
new file mode 100644
index 0000000000..4b01dee1e0
--- /dev/null
+++ b/public/openapi/write/users/uid/sessions/uuid.yaml
@@ -0,0 +1,31 @@
+delete:
+  tags:
+    - users
+  summary: revoke a user session
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user's session
+      example: 3
+    - in: path
+      name: uuid
+      schema:
+        type: string
+      required: true
+      description: uuid of the user's session
+      example: 7c1a66b3-90e1-41f4-9f74-2b2edaebf917
+  responses:
+    '200':
+      description: user session revoked
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/settings.yaml b/public/openapi/write/users/uid/settings.yaml
new file mode 100644
index 0000000000..2da70faba6
--- /dev/null
+++ b/public/openapi/write/users/uid/settings.yaml
@@ -0,0 +1,36 @@
+put:
+  tags:
+    - users
+  summary: update user settings
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user
+      example: 1
+  requestBody:
+    content:
+      application/json:
+        schema:
+          type: object
+          properties:
+            settings:
+              type: object
+              description: An object containing key-value pairs of user settings to update
+              example:
+                showemail: '0'
+                showfullname: '1'
+  responses:
+    '200':
+      description: successfully updated user settings
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                $ref: ../../../components/schemas/SettingsObj.yaml#/Settings
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/tokens.yaml b/public/openapi/write/users/uid/tokens.yaml
new file mode 100644
index 0000000000..49b7e39185
--- /dev/null
+++ b/public/openapi/write/users/uid/tokens.yaml
@@ -0,0 +1,25 @@
+post:
+  tags:
+    - users
+  summary: generate a user token
+  description: This route can only be used to generate tokens for the same user. In other words, you cannot use this route to generate a token for a different user than the one you are authenticated as.
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user to generate a token for
+      example: 1
+  responses:
+    '200':
+      description: successfully generated a user token
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/openapi/write/users/uid/tokens/token.yaml b/public/openapi/write/users/uid/tokens/token.yaml
new file mode 100644
index 0000000000..ef17a8e752
--- /dev/null
+++ b/public/openapi/write/users/uid/tokens/token.yaml
@@ -0,0 +1,31 @@
+delete:
+  tags:
+    - users
+  summary: delete user token
+  parameters:
+    - in: path
+      name: uid
+      schema:
+        type: integer
+      required: true
+      description: uid of the user whose token you want to delete
+      example: 1
+    - in: path
+      name: token
+      schema:
+        type: string
+      required: true
+      description: a valid API token
+      example: 6d03a630-86fd-4515-9a35-e957502f4f89
+  responses:
+    '200':
+      description: successfully deleted user token
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              status:
+                $ref: ../../../../components/schemas/Status.yaml#/Status
+              response:
+                type: object
\ No newline at end of file
diff --git a/public/src/admin/.eslintrc b/public/src/admin/.eslintrc
new file mode 100644
index 0000000000..2d4ef534de
--- /dev/null
+++ b/public/src/admin/.eslintrc
@@ -0,0 +1,5 @@
+{
+	"globals": {
+		"Sortable": true
+	}
+}
\ No newline at end of file
diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js
new file mode 100644
index 0000000000..561850827b
--- /dev/null
+++ b/public/src/admin/admin.js
@@ -0,0 +1,244 @@
+'use strict';
+
+require('../app');
+
+// scripts-admin.js contains javascript files
+// from plugins that add files to "acpScripts" block in plugin.json
+// eslint-disable-next-line import/no-unresolved
+require('../../scripts-admin');
+
+app.onDomReady();
+
+(function () {
+    let logoutTimer = 0;
+    let logoutMessage;
+    function startLogoutTimer() {
+        if (app.config.adminReloginDuration <= 0) {
+            return;
+        }
+        if (logoutTimer) {
+            clearTimeout(logoutTimer);
+        }
+        // pre-translate language string gh#9046
+        if (!logoutMessage) {
+            require(['translator'], function (translator) {
+                translator.translate('[[login:logged-out-due-to-inactivity]]', function (translated) {
+                    logoutMessage = translated;
+                });
+            });
+        }
+
+        logoutTimer = setTimeout(function () {
+            require(['bootbox'], function (bootbox) {
+                bootbox.alert({
+                    closeButton: false,
+                    message: logoutMessage,
+                    callback: function () {
+                        window.location.reload();
+                    },
+                });
+            });
+        }, 3600000);
+    }
+
+    require(['hooks'], (hooks) => {
+        hooks.on('action:ajaxify.end', () => {
+            showCorrectNavTab();
+            startLogoutTimer();
+            if ($('.settings').length) {
+                require(['admin/settings'], function (Settings) {
+                    Settings.prepare();
+                    Settings.populateTOC();
+                });
+            }
+        });
+    });
+
+    function showCorrectNavTab() {
+        // show correct tab if url has #
+        if (window.location.hash) {
+            $('.nav-pills a[href="' + window.location.hash + '"]').tab('show');
+        }
+    }
+
+    $(document).ready(function () {
+        if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
+            require(['admin/modules/search'], function (search) {
+                search.init();
+            });
+        }
+
+        $('[component="logout"]').on('click', function () {
+            require(['logout'], function (logout) {
+                logout();
+            });
+            return false;
+        });
+
+        configureSlidemenu();
+        setupNProgress();
+    });
+
+    $(window).on('action:ajaxify.contentLoaded', function (ev, data) {
+        selectMenuItem(data.url);
+        setupRestartLinks();
+        require('material-design-lite');
+        componentHandler.upgradeDom();
+    });
+
+    function setupNProgress() {
+        require(['nprogress', 'hooks'], function (NProgress, hooks) {
+            $(window).on('action:ajaxify.start', function () {
+                NProgress.set(0.7);
+            });
+
+            hooks.on('action:ajaxify.end', function () {
+                NProgress.done();
+            });
+        });
+    }
+
+    function selectMenuItem(url) {
+        require(['translator'], function (translator) {
+            url = url
+                .replace(/\/\d+$/, '')
+                .split('/').slice(0, 3).join('/')
+                .split(/[?#]/)[0].replace(/(\/+$)|(^\/+)/, '');
+
+            // If index is requested, load the dashboard
+            if (url === 'admin') {
+                url = 'admin/dashboard';
+            }
+
+            url = [config.relative_path, url].join('/');
+            let fallback;
+
+            $('#main-menu li').removeClass('active');
+            $('#main-menu a').removeClass('active').filter('[href="' + url + '"]').each(function () {
+                const menu = $(this);
+                if (menu.parent().attr('data-link')) {
+                    return;
+                }
+
+                menu
+                    .parent().addClass('active')
+                    .parents('.menu-item').addClass('active');
+                fallback = menu.text();
+            });
+
+            let mainTitle;
+            let pageTitle;
+            if (/admin\/plugins\//.test(url)) {
+                mainTitle = fallback;
+                pageTitle = '[[admin/menu:section-plugins]] > ' + mainTitle;
+            } else {
+                const matches = url.match(/admin\/(.+?)\/(.+?)$/);
+                if (matches) {
+                    mainTitle = '[[admin/menu:' + matches[1] + '/' + matches[2] + ']]';
+                    pageTitle = '[[admin/menu:section-' +
+                        (matches[1] === 'development' ? 'advanced' : matches[1]) +
+                        ']]' + (matches[2] ? (' > ' + mainTitle) : '');
+                    if (matches[2] === 'settings') {
+                        mainTitle = translator.compile('admin/menu:settings.page-title', mainTitle);
+                    }
+                } else {
+                    mainTitle = '[[admin/menu:section-dashboard]]';
+                    pageTitle = '[[admin/menu:section-dashboard]]';
+                }
+            }
+
+            pageTitle = translator.compile('admin/admin:acp-title', pageTitle);
+
+            translator.translate(pageTitle, function (title) {
+                document.title = title.replace(/>/g, '>');
+            });
+            translator.translate(mainTitle, function (text) {
+                $('#main-page-title').text(text);
+            });
+        });
+    }
+
+    function setupRestartLinks() {
+        require(['benchpress', 'bootbox', 'admin/modules/instance'], function (benchpress, bootbox, instance) {
+            // need to preload the compiled alert template
+            // otherwise it can be unloaded when rebuild & restart is run
+            // the client can't fetch the template file, resulting in an error
+            benchpress.render('alert', {}).then(function () {
+                $('.rebuild-and-restart').off('click').on('click', function () {
+                    bootbox.confirm('[[admin/admin:alert.confirm-rebuild-and-restart]]', function (confirm) {
+                        if (confirm) {
+                            instance.rebuildAndRestart();
+                        }
+                    });
+                });
+
+                $('.restart').off('click').on('click', function () {
+                    bootbox.confirm('[[admin/admin:alert.confirm-restart]]', function (confirm) {
+                        if (confirm) {
+                            instance.restart();
+                        }
+                    });
+                });
+            });
+        });
+    }
+
+    function configureSlidemenu() {
+        require(['slideout'], function (Slideout) {
+            let env = utils.findBootstrapEnvironment();
+
+            const slideout = new Slideout({
+                panel: document.getElementById('panel'),
+                menu: document.getElementById('menu'),
+                padding: 256,
+                tolerance: 70,
+            });
+
+            if (env === 'md' || env === 'lg') {
+                slideout.disableTouch();
+            }
+
+            $('#mobile-menu').on('click', function () {
+                slideout.toggle();
+            });
+
+            $('#menu a').on('click', function () {
+                slideout.close();
+            });
+
+            $(window).on('resize', function () {
+                slideout.close();
+
+                env = utils.findBootstrapEnvironment();
+
+                if (env === 'md' || env === 'lg') {
+                    slideout.disableTouch();
+                    $('#header').css({
+                        position: 'relative',
+                    });
+                } else {
+                    slideout.enableTouch();
+                    $('#header').css({
+                        position: 'fixed',
+                    });
+                }
+            });
+
+            function onOpeningMenu() {
+                $('#header').css({
+                    top: ($('#panel').position().top * -1) + 'px',
+                    position: 'absolute',
+                });
+            }
+
+            slideout.on('open', onOpeningMenu);
+
+            slideout.on('close', function () {
+                $('#header').css({
+                    top: '0px',
+                    position: 'fixed',
+                });
+            });
+        });
+    }
+}());
diff --git a/public/src/admin/advanced/cache.js b/public/src/admin/advanced/cache.js
new file mode 100644
index 0000000000..4d26e42879
--- /dev/null
+++ b/public/src/admin/advanced/cache.js
@@ -0,0 +1,32 @@
+'use strict';
+
+define('admin/advanced/cache', ['alerts'], function (alerts) {
+    const Cache = {};
+    Cache.init = function () {
+        require(['admin/settings'], function (Settings) {
+            Settings.prepare();
+        });
+
+        $('.clear').on('click', function () {
+            const name = $(this).attr('data-name');
+            socket.emit('admin.cache.clear', { name: name }, function (err) {
+                if (err) {
+                    return alerts.error(err);
+                }
+                ajaxify.refresh();
+            });
+        });
+
+        $('.checkbox').on('change', function () {
+            const input = $(this).find('input');
+            const flag = input.is(':checked');
+            const name = $(this).attr('data-name');
+            socket.emit('admin.cache.toggle', { name: name, enabled: flag }, function (err) {
+                if (err) {
+                    return alerts.error(err);
+                }
+            });
+        });
+    };
+    return Cache;
+});
diff --git a/public/src/admin/advanced/errors.js b/public/src/admin/advanced/errors.js
new file mode 100644
index 0000000000..d69eb2681c
--- /dev/null
+++ b/public/src/admin/advanced/errors.js
@@ -0,0 +1,113 @@
+'use strict';
+
+
+define('admin/advanced/errors', ['bootbox', 'alerts', 'Chart'], function (bootbox, alerts, Chart) {
+    const Errors = {};
+
+    Errors.init = function () {
+        Errors.setupCharts();
+
+        $('[data-action="clear"]').on('click', Errors.clear404);
+    };
+
+    Errors.clear404 = function () {
+        bootbox.confirm('[[admin/advanced/errors:clear404-confirm]]', function (ok) {
+            if (ok) {
+                socket.emit('admin.errors.clear', {}, function (err) {
+                    if (err) {
+                        return alerts.error(err);
+                    }
+
+                    ajaxify.refresh();
+                    alerts.success('[[admin/advanced/errors:clear404-success]]');
+                });
+            }
+        });
+    };
+
+    Errors.setupCharts = function () {
+        const notFoundCanvas = document.getElementById('not-found');
+        const tooBusyCanvas = document.getElementById('toobusy');
+        let dailyLabels = utils.getDaysArray();
+
+        dailyLabels = dailyLabels.slice(-7);
+
+        if (utils.isMobile()) {
+            Chart.defaults.global.tooltips.enabled = false;
+        }
+
+        const data = {
+            'not-found': {
+                labels: dailyLabels,
+                datasets: [
+                    {
+                        label: '',
+                        backgroundColor: 'rgba(186,139,175,0.2)',
+                        borderColor: 'rgba(186,139,175,1)',
+                        pointBackgroundColor: 'rgba(186,139,175,1)',
+                        pointHoverBackgroundColor: '#fff',
+                        pointBorderColor: '#fff',
+                        pointHoverBorderColor: 'rgba(186,139,175,1)',
+                        data: ajaxify.data.analytics['not-found'],
+                    },
+                ],
+            },
+            toobusy: {
+                labels: dailyLabels,
+                datasets: [
+                    {
+                        label: '',
+                        backgroundColor: 'rgba(151,187,205,0.2)',
+                        borderColor: 'rgba(151,187,205,1)',
+                        pointBackgroundColor: 'rgba(151,187,205,1)',
+                        pointHoverBackgroundColor: '#fff',
+                        pointBorderColor: '#fff',
+                        pointHoverBorderColor: 'rgba(151,187,205,1)',
+                        data: ajaxify.data.analytics.toobusy,
+                    },
+                ],
+            },
+        };
+
+        notFoundCanvas.width = $(notFoundCanvas).parent().width();
+        tooBusyCanvas.width = $(tooBusyCanvas).parent().width();
+
+        new Chart(notFoundCanvas.getContext('2d'), {
+            type: 'line',
+            data: data['not-found'],
+            options: {
+                responsive: true,
+                legend: {
+                    display: false,
+                },
+                scales: {
+                    yAxes: [{
+                        ticks: {
+                            beginAtZero: true,
+                        },
+                    }],
+                },
+            },
+        });
+
+        new Chart(tooBusyCanvas.getContext('2d'), {
+            type: 'line',
+            data: data.toobusy,
+            options: {
+                responsive: true,
+                legend: {
+                    display: false,
+                },
+                scales: {
+                    yAxes: [{
+                        ticks: {
+                            beginAtZero: true,
+                        },
+                    }],
+                },
+            },
+        });
+    };
+
+    return Errors;
+});
diff --git a/public/src/admin/advanced/events.js b/public/src/admin/advanced/events.js
new file mode 100644
index 0000000000..a2794fee0b
--- /dev/null
+++ b/public/src/admin/advanced/events.js
@@ -0,0 +1,43 @@
+'use strict';
+
+
+define('admin/advanced/events', ['bootbox', 'alerts'], function (bootbox, alerts) {
+    const Events = {};
+
+    Events.init = function () {
+        $('[data-action="clear"]').on('click', function () {
+            bootbox.confirm('[[admin/advanced/events:confirm-delete-all-events]]', (confirm) => {
+                if (confirm) {
+                    socket.emit('admin.deleteAllEvents', function (err) {
+                        if (err) {
+                            return alerts.error(err);
+                        }
+                        $('.events-list').empty();
+                    });
+                }
+            });
+        });
+
+        $('.delete-event').on('click', function () {
+            const $parentEl = $(this).parents('[data-eid]');
+            const eid = $parentEl.attr('data-eid');
+            socket.emit('admin.deleteEvents', [eid], function (err) {
+                if (err) {
+                    return alerts.error(err);
+                }
+                $parentEl.remove();
+            });
+        });
+
+        $('#apply').on('click', Events.refresh);
+    };
+
+    Events.refresh = function (event) {
+        event.preventDefault();
+
+        const $formEl = $('#filters');
+        ajaxify.go('admin/advanced/events?' + $formEl.serialize());
+    };
+
+    return Events;
+});
diff --git a/public/src/admin/advanced/logs.js b/public/src/admin/advanced/logs.js
new file mode 100644
index 0000000000..1c120e3949
--- /dev/null
+++ b/public/src/admin/advanced/logs.js
@@ -0,0 +1,44 @@
+'use strict';
+
+
+define('admin/advanced/logs', ['alerts'], function (alerts) {
+    const Logs = {};
+
+    Logs.init = function () {
+        const logsEl = $('.logs pre');
+        logsEl.scrollTop(logsEl.prop('scrollHeight'));
+        // Affix menu
+        $('.affix').affix();
+
+        $('.logs').find('button[data-action]').on('click', function () {
+            const btnEl = $(this);
+            const action = btnEl.attr('data-action');
+
+            switch (action) {
+            case 'reload':
+                socket.emit('admin.logs.get', function (err, logs) {
+                    if (!err) {
+                        logsEl.text(logs);
+                        logsEl.scrollTop(logsEl.prop('scrollHeight'));
+                    } else {
+                        alerts.error(err);
+                    }
+                });
+                break;
+
+            case 'clear':
+                socket.emit('admin.logs.clear', function (err) {
+                    if (!err) {
+                        alerts.success('[[admin/advanced/logs:clear-success]]');
+                        btnEl.prev().click();
+                    } else {
+                        alerts.error(err);
+                    }
+                });
+                break;
+            }
+        });
+    };
+
+    return Logs;
+});
diff --git a/public/src/admin/appearance/customise.js b/public/src/admin/appearance/customise.js
new file mode 100644
index 0000000000..e3b324e6d8
--- /dev/null
+++ b/public/src/admin/appearance/customise.js
@@ -0,0 +1,40 @@
+'use strict';
+
+define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Settings, ace) {
+    const Customise = {};
+
+    Customise.init = function () {
+        Settings.prepare(function () {
+            $('#customCSS').text($('#customCSS-holder').val());
+            $('#customJS').text($('#customJS-holder').val());
+            $('#customHTML').text($('#customHTML-holder').val());
+
+            initACE('customCSS', 'less', '#customCSS-holder');
+            initACE('customJS', 'javascript', '#customJS-holder');
+            initACE('customHTML', 'html', '#customHTML-holder');
+
+            $('#save').on('click', function () {
+                if ($('#enableLiveReload').is(':checked')) {
+                    socket.emit('admin.reloadAllSessions');
+                }
+            });
+        });
+    };
+
+    function initACE(aceElementId, mode, holder) {
+        const editorEl = ace.edit(aceElementId, {
+            mode: 'ace/mode/' + mode,
+            theme: 'ace/theme/twilight',
+            maxLines: 30,
+            minLines: 30,
+            fontSize: 14,
+        });
+        editorEl.on('change', function () {
+            app.flags = app.flags || {};
+            app.flags._unsaved = true;
+            $(holder).val(editorEl.getValue());
+        });
+    }
+
+    return Customise;
+});
diff --git a/public/src/admin/appearance/skins.js b/public/src/admin/appearance/skins.js
new file mode 100644
index 0000000000..14c3864376
--- /dev/null
+++ b/public/src/admin/appearance/skins.js
@@ -0,0 +1,113 @@
+'use strict';
+
+
+define('admin/appearance/skins', ['translator', 'alerts'], function (translator, alerts) {
+    const Skins = {};
+
+    Skins.init = function () {
+        // Populate skins from Bootswatch API
+        $.ajax({
+            method: 'get',
+            url: 'https://bootswatch.com/api/3.json',
+        }).done(Skins.render);
+
+        $('#skins').on('click', function (e) {
+            let target = $(e.target);
+
+            if (!target.attr('data-action')) {
+                target = target.parents('[data-action]');
+            }
+
+            const action = target.attr('data-action');
+
+            if (action && action === 'use') {
+                const parentEl = target.parents('[data-theme]');
+                const themeType = parentEl.attr('data-type');
+                const cssSrc = parentEl.attr('data-css');
+                const themeId = parentEl.attr('data-theme');
+
+
+                socket.emit('admin.themes.set', {
+                    type: themeType,
+                    id: themeId,
+                    src: cssSrc,
+                }, function (err) {
+                    if (err) {
+                        return alerts.error(err);
+                    }
+                    highlightSelectedTheme(themeId);
+
+                    alerts.alert({
+                        alert_id: 'admin:theme',
+                        type: 'info',
+                        title: '[[admin/appearance/skins:skin-updated]]',
+                        message: themeId ? ('[[admin/appearance/skins:applied-success, ' + themeId + ']]') : '[[admin/appearance/skins:revert-success]]',
+                        timeout: 5000,
+                    });
+                });
+            }
+        });
+    };
+
+    Skins.render = function (bootswatch) {
+        const themeContainer = $('#bootstrap_themes');
+
+        app.parseAndTranslate('admin/partials/theme_list', {
+            themes: bootswatch.themes.map(function (theme) {
+                return {
+                    type: 'bootswatch',
+                    id: theme.name,
+                    name: theme.name,
+                    description: theme.description,
+                    screenshot_url: theme.thumbnail,
+                    url: theme.preview,
+                    css: theme.cssCdn,
+                    skin: true,
+                };
+            }),
+            showRevert: true,
+        }, function (html) {
+            themeContainer.html(html);
+
+            if (config['theme:src']) {
+                const skin = config['theme:src']
+                    .match(/latest\/(\S+)\/bootstrap.min.css/)[1]
+                    .replace(/(^|\s)([a-z])/g, function (m, p1, p2) { return p1 + p2.toUpperCase(); });
+
+                highlightSelectedTheme(skin);
+            }
+        });
+    };
+
+    function highlightSelectedTheme(themeId) {
+        translator.translate('[[admin/appearance/skins:select-skin]]  ||  [[admin/appearance/skins:current-skin]]', function (text) {
+            text = text.split('  ||  ');
+            const select = text[0];
+            const current = text[1];
+
+            $('[data-theme]')
+                .removeClass('selected')
+                .find('[data-action="use"]').each(function () {
+                    if ($(this).parents('[data-theme]').attr('data-theme')) {
+                        $(this)
+                            .html(select)
+                            .removeClass('btn-success')
+                            .addClass('btn-primary');
+                    }
+                });
+
+            if (!themeId) {
+                return;
+            }
+
+            $('[data-theme="' + themeId + '"]')
+                .addClass('selected')
+                .find('[data-action="use"]')
+                .html(current)
+                .removeClass('btn-primary')
+                .addClass('btn-success');
+        });
+    }
+
+    return Skins;
+});
diff --git a/public/src/admin/appearance/themes.js b/public/src/admin/appearance/themes.js
new file mode 100644
index 0000000000..a9d6796465
--- /dev/null
+++ b/public/src/admin/appearance/themes.js
@@ -0,0 +1,118 @@
+'use strict';
+
+
+define('admin/appearance/themes', ['bootbox', 'translator', 'alerts'], function (bootbox, translator, alerts) {
+    const Themes = {};
+
+    Themes.init = function () {
+        $('#installed_themes').on('click', function (e) {
+            const target = $(e.target);
+            const action = target.attr('data-action');
+
+            if (action && action === 'use') {
+                const parentEl = target.parents('[data-theme]');
+                const themeType = parentEl.attr('data-type');
+                const cssSrc = parentEl.attr('data-css');
+                const themeId = parentEl.attr('data-theme');
+
+                if (config['theme:id'] === themeId) {
+                    return;
+                }
+                socket.emit('admin.themes.set', {
+                    type: themeType,
+                    id: themeId,
+                    src: cssSrc,
+                }, function (err) {
+                    if (err) {
+                        return alerts.error(err);
+                    }
+                    config['theme:id'] = themeId;
+                    highlightSelectedTheme(themeId);
+
+                    alerts.alert({
+                        alert_id: 'admin:theme',
+                        type: 'info',
+                        title: '[[admin/appearance/themes:theme-changed]]',
+                        message: '[[admin/appearance/themes:restart-to-activate]]',
+                        timeout: 5000,
+                        clickfn: function () {
+                            require(['admin/modules/instance'], function (instance) {
+                                instance.rebuildAndRestart();
+                            });
+                        },
+                    });
+                });
+            }
+        });
+
+        $('#revert_theme').on('click', function () {
+            if (config['theme:id'] === 'nodebb-theme-persona') {
+                return;
+            }
+            bootbox.confirm('[[admin/appearance/themes:revert-confirm]]', function (confirm) {
+                if (confirm) {
+                    socket.emit('admin.themes.set', {
+                        type: 'local',
+                        id: 'nodebb-theme-persona',
+                    }, function (err) {
+                        if (err) {
+                            return alerts.error(err);
+                        }
+                        config['theme:id'] = 'nodebb-theme-persona';
+                        highlightSelectedTheme('nodebb-theme-persona');
+                        alerts.alert({
+                            alert_id: 'admin:theme',
+                            type: 'success',
+                            title: '[[admin/appearance/themes:theme-changed]]',
+                            message: '[[admin/appearance/themes:revert-success]]',
+                            timeout: 3500,
+                        });
+                    });
+                }
+            });
+        });
+
+        socket.emit('admin.themes.getInstalled', function (err, themes) {
+            if (err) {
+                return alerts.error(err);
+            }
+
+            const instListEl = $('#installed_themes');
+
+            if (!themes.length) {
+                instListEl.append($('
  • ').addClass('no-themes').translateHtml('[[admin/appearance/themes:no-themes]]')); + } else { + app.parseAndTranslate('admin/partials/theme_list', { + themes: themes, + }, function (html) { + instListEl.html(html); + highlightSelectedTheme(config['theme:id']); + }); + } + }); + }; + + function highlightSelectedTheme(themeId) { + translator.translate('[[admin/appearance/themes:select-theme]] || [[admin/appearance/themes:current-theme]]', function (text) { + text = text.split(' || '); + const select = text[0]; + const current = text[1]; + + $('[data-theme]') + .removeClass('selected') + .find('[data-action="use"]') + .html(select) + .removeClass('btn-success') + .addClass('btn-primary'); + + $('[data-theme="' + themeId + '"]') + .addClass('selected') + .find('[data-action="use"]') + .html(current) + .removeClass('btn-primary') + .addClass('btn-success'); + }); + } + + return Themes; +}); diff --git a/public/src/admin/dashboard.js b/public/src/admin/dashboard.js new file mode 100644 index 0000000000..47e793a7ec --- /dev/null +++ b/public/src/admin/dashboard.js @@ -0,0 +1,605 @@ +'use strict'; + + +define('admin/dashboard', [ + 'Chart', 'translator', 'benchpress', 'bootbox', 'alerts', +], function (Chart, translator, Benchpress, bootbox, alerts) { + const Admin = {}; + const intervals = { + rooms: false, + graphs: false, + }; + let isMobile = false; + const graphData = { + rooms: {}, + traffic: {}, + }; + const currentGraph = { + units: 'hours', + until: undefined, + }; + + const DEFAULTS = { + roomInterval: 10000, + graphInterval: 15000, + realtimeInterval: 1500, + }; + + const usedTopicColors = []; + + $(window).on('action:ajaxify.start', function () { + clearInterval(intervals.rooms); + clearInterval(intervals.graphs); + + intervals.rooms = null; + intervals.graphs = null; + graphData.rooms = null; + graphData.traffic = null; + usedTopicColors.length = 0; + }); + + Admin.init = function () { + app.enterRoom('admin'); + + isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + + $('[data-toggle="tooltip"]').tooltip(); + + setupRealtimeButton(); + setupGraphs(function () { + socket.emit('admin.rooms.getAll', Admin.updateRoomUsage); + initiateDashboard(); + }); + setupFullscreen(); + }; + + Admin.updateRoomUsage = function (err, data) { + if (err) { + return alerts.error(err); + } + + if (JSON.stringify(graphData.rooms) === JSON.stringify(data)) { + return; + } + + graphData.rooms = data; + + const html = '
    ' + + '' + data.onlineRegisteredCount + '' + + '
    [[admin/dashboard:active-users.users]]
    ' + + '
    ' + + '
    ' + + '' + data.onlineGuestCount + '' + + '
    [[admin/dashboard:active-users.guests]]
    ' + + '
    ' + + '
    ' + + '' + (data.onlineRegisteredCount + data.onlineGuestCount) + '' + + '
    [[admin/dashboard:active-users.total]]
    ' + + '
    ' + + '
    ' + + '' + data.socketCount + '' + + '
    [[admin/dashboard:active-users.connections]]
    ' + + '
    '; + + updateRegisteredGraph(data.onlineRegisteredCount, data.onlineGuestCount); + updatePresenceGraph(data.users); + updateTopicsGraph(data.topTenTopics); + + $('#active-users').translateHtml(html); + }; + + const graphs = { + traffic: null, + registered: null, + presence: null, + topics: null, + }; + + const topicColors = ['#bf616a', '#5B90BF', '#d08770', '#ebcb8b', '#a3be8c', '#96b5b4', '#8fa1b3', '#b48ead', '#ab7967', '#46BFBD']; + + /* eslint-disable */ + // from chartjs.org + function lighten(col, amt) { + let usePound = false; + + if (col[0] === '#') { + col = col.slice(1); + usePound = true; + } + + const num = parseInt(col, 16); + + let r = (num >> 16) + amt; + + if (r > 255) r = 255; + else if (r < 0) r = 0; + + let b = ((num >> 8) & 0x00FF) + amt; + + if (b > 255) b = 255; + else if (b < 0) b = 0; + + let g = (num & 0x0000FF) + amt; + + if (g > 255) g = 255; + else if (g < 0) g = 0; + + return (usePound ? '#' : '') + (g | (b << 8) | (r << 16)).toString(16); + } + /* eslint-enable */ + + function setupGraphs(callback) { + callback = callback || function () {}; + const trafficCanvas = document.getElementById('analytics-traffic'); + const registeredCanvas = document.getElementById('analytics-registered'); + const presenceCanvas = document.getElementById('analytics-presence'); + const topicsCanvas = document.getElementById('analytics-topics'); + const trafficCtx = trafficCanvas.getContext('2d'); + const registeredCtx = registeredCanvas.getContext('2d'); + const presenceCtx = presenceCanvas.getContext('2d'); + const topicsCtx = topicsCanvas.getContext('2d'); + const trafficLabels = utils.getHoursArray(); + + if (isMobile) { + Chart.defaults.global.tooltips.enabled = false; + } + + const t = translator.Translator.create(); + Promise.all([ + t.translateKey('admin/dashboard:graphs.page-views', []), + t.translateKey('admin/dashboard:graphs.page-views-registered', []), + t.translateKey('admin/dashboard:graphs.page-views-guest', []), + t.translateKey('admin/dashboard:graphs.page-views-bot', []), + t.translateKey('admin/dashboard:graphs.unique-visitors', []), + t.translateKey('admin/dashboard:graphs.registered-users', []), + t.translateKey('admin/dashboard:graphs.guest-users', []), + t.translateKey('admin/dashboard:on-categories', []), + t.translateKey('admin/dashboard:reading-posts', []), + t.translateKey('admin/dashboard:browsing-topics', []), + t.translateKey('admin/dashboard:recent', []), + t.translateKey('admin/dashboard:unread', []), + ]).then(function (translations) { + const data = { + labels: trafficLabels, + datasets: [ + { + label: translations[0], + backgroundColor: 'rgba(220,220,220,0.2)', + borderColor: 'rgba(220,220,220,1)', + pointBackgroundColor: 'rgba(220,220,220,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(220,220,220,1)', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + { + label: translations[1], + backgroundColor: '#ab464233', + borderColor: '#ab4642', + pointBackgroundColor: '#ab4642', + pointHoverBackgroundColor: '#ab4642', + pointBorderColor: '#fff', + pointHoverBorderColor: '#ab4642', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + { + label: translations[2], + backgroundColor: '#ba8baf33', + borderColor: '#ba8baf', + pointBackgroundColor: '#ba8baf', + pointHoverBackgroundColor: '#ba8baf', + pointBorderColor: '#fff', + pointHoverBorderColor: '#ba8baf', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + { + label: translations[3], + backgroundColor: '#f7ca8833', + borderColor: '#f7ca88', + pointBackgroundColor: '#f7ca88', + pointHoverBackgroundColor: '#f7ca88', + pointBorderColor: '#fff', + pointHoverBorderColor: '#f7ca88', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + { + label: translations[4], + backgroundColor: 'rgba(151,187,205,0.2)', + borderColor: 'rgba(151,187,205,1)', + pointBackgroundColor: 'rgba(151,187,205,1)', + pointHoverBackgroundColor: 'rgba(151,187,205,1)', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(151,187,205,1)', + data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + ], + }; + + trafficCanvas.width = $(trafficCanvas).parent().width(); + + data.datasets[0].yAxisID = 'left-y-axis'; + data.datasets[1].yAxisID = 'left-y-axis'; + data.datasets[2].yAxisID = 'left-y-axis'; + data.datasets[3].yAxisID = 'left-y-axis'; + data.datasets[4].yAxisID = 'right-y-axis'; + + graphs.traffic = new Chart(trafficCtx, { + type: 'line', + data: data, + options: { + responsive: true, + legend: { + display: true, + }, + scales: { + yAxes: [{ + id: 'left-y-axis', + ticks: { + beginAtZero: true, + precision: 0, + }, + type: 'linear', + position: 'left', + scaleLabel: { + display: true, + labelString: translations[0], + }, + }, { + id: 'right-y-axis', + ticks: { + beginAtZero: true, + suggestedMax: 10, + precision: 0, + }, + type: 'linear', + position: 'right', + scaleLabel: { + display: true, + labelString: translations[4], + }, + }], + }, + tooltips: { + mode: 'x', + }, + }, + }); + + graphs.registered = new Chart(registeredCtx, { + type: 'doughnut', + data: { + labels: translations.slice(5, 7), + datasets: [{ + data: [1, 1], + backgroundColor: ['#F7464A', '#46BFBD'], + hoverBackgroundColor: ['#FF5A5E', '#5AD3D1'], + }], + }, + options: { + responsive: true, + legend: { + display: false, + }, + }, + }); + + graphs.presence = new Chart(presenceCtx, { + type: 'doughnut', + data: { + labels: translations.slice(7, 12), + datasets: [{ + data: [1, 1, 1, 1, 1], + backgroundColor: ['#F7464A', '#46BFBD', '#FDB45C', '#949FB1', '#9FB194'], + hoverBackgroundColor: ['#FF5A5E', '#5AD3D1', '#FFC870', '#A8B3C5', '#A8B3C5'], + }], + }, + options: { + responsive: true, + legend: { + display: false, + }, + }, + }); + + graphs.topics = new Chart(topicsCtx, { + type: 'doughnut', + data: { + labels: [], + datasets: [{ + data: [], + backgroundColor: [], + hoverBackgroundColor: [], + }], + }, + options: { + responsive: true, + legend: { + display: false, + }, + }, + }); + + updateTrafficGraph(); + + $(window).on('resize', adjustPieCharts); + adjustPieCharts(); + + $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () { + let until = new Date(); + const amount = $(this).attr('data-amount'); + if ($(this).attr('data-units') === 'days') { + until.setHours(0, 0, 0, 0); + } + until = until.getTime(); + updateTrafficGraph($(this).attr('data-units'), until, amount); + + require(['translator'], function (translator) { + translator.translate('[[admin/dashboard:page-views-custom]]', function (translated) { + $('[data-action="updateGraph"][data-units="custom"]').text(translated); + }); + }); + }); + + $('[data-action="updateGraph"][data-units="custom"]').on('click', function () { + const targetEl = $(this); + + Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) { + const modal = bootbox.dialog({ + title: '[[admin/dashboard:page-views-custom]]', + message: html, + buttons: { + submit: { + label: '[[global:search]]', + className: 'btn-primary', + callback: submit, + }, + }, + }).on('shown.bs.modal', function () { + const date = new Date(); + const today = date.toISOString().slice(0, 10); + date.setDate(date.getDate() - 1); + const yesterday = date.toISOString().slice(0, 10); + + modal.find('#startRange').val(targetEl.attr('data-startRange') || yesterday); + modal.find('#endRange').val(targetEl.attr('data-endRange') || today); + }); + + function submit() { + // NEED TO ADD VALIDATION HERE FOR YYYY-MM-DD + const formData = modal.find('form').serializeObject(); + const validRegexp = /\d{4}-\d{2}-\d{2}/; + + // Input validation + if (!formData.startRange && !formData.endRange) { + // No range? Assume last 30 days + updateTrafficGraph('days'); + return; + } else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) { + // Invalid Input + modal.find('.alert-danger').removeClass('hidden'); + return false; + } + + let until = new Date(formData.endRange); + until.setDate(until.getDate() + 1); + until = until.getTime(); + const amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24); + + updateTrafficGraph('days', until, amount); + + // Update "custom range" label + targetEl.attr('data-startRange', formData.startRange); + targetEl.attr('data-endRange', formData.endRange); + targetEl.html(formData.startRange + ' – ' + formData.endRange); + } + }); + }); + + socket.emit('admin.rooms.getAll', Admin.updateRoomUsage); + initiateDashboard(); + callback(); + }); + } + + function adjustPieCharts() { + $('.pie-chart.legend-up').each(function () { + const $this = $(this); + + if ($this.width() < 320) { + $this.addClass('compact'); + } else { + $this.removeClass('compact'); + } + }); + } + + function updateTrafficGraph(units, until, amount) { + // until and amount are optional + + if (!app.isFocused) { + return; + } + + socket.emit('admin.analytics.get', { + graph: 'traffic', + units: units || 'hours', + until: until, + amount: amount, + }, function (err, data) { + if (err) { + return alerts.error(err); + } + if (JSON.stringify(graphData.traffic) === JSON.stringify(data)) { + return; + } + + graphData.traffic = data; + + if (units === 'days') { + graphs.traffic.data.xLabels = utils.getDaysArray(until, amount); + } else { + graphs.traffic.data.xLabels = utils.getHoursArray(); + + $('#pageViewsThirty').html(data.summary.thirty); + $('#pageViewsSeven').html(data.summary.seven); + $('#pageViewsPastDay').html(data.pastDay); + utils.addCommasToNumbers($('#pageViewsThirty')); + utils.addCommasToNumbers($('#pageViewsSeven')); + utils.addCommasToNumbers($('#pageViewsPastDay')); + } + + graphs.traffic.data.datasets[0].data = data.pageviews; + graphs.traffic.data.datasets[1].data = data.pageviewsRegistered; + graphs.traffic.data.datasets[2].data = data.pageviewsGuest; + graphs.traffic.data.datasets[3].data = data.pageviewsBot; + graphs.traffic.data.datasets[4].data = data.uniqueVisitors; + graphs.traffic.data.labels = graphs.traffic.data.xLabels; + + graphs.traffic.update(); + currentGraph.units = units; + currentGraph.until = until; + currentGraph.amount = amount; + + // Update the View as JSON button url + const apiEl = $('#view-as-json'); + const newHref = $.param({ + units: units || 'hours', + until: until, + count: amount, + }); + apiEl.attr('href', config.relative_path + '/api/admin/analytics?' + newHref); + }); + } + + function updateRegisteredGraph(registered, guest) { + $('#analytics-legend .registered').parent().find('.count').text(registered); + $('#analytics-legend .guest').parent().find('.count').text(guest); + graphs.registered.data.datasets[0].data[0] = registered; + graphs.registered.data.datasets[0].data[1] = guest; + graphs.registered.update(); + } + + function updatePresenceGraph(users) { + $('#analytics-presence-legend .on-categories').parent().find('.count').text(users.categories); + $('#analytics-presence-legend .reading-posts').parent().find('.count').text(users.topics); + $('#analytics-presence-legend .browsing-topics').parent().find('.count').text(users.category); + $('#analytics-presence-legend .recent').parent().find('.count').text(users.recent); + $('#analytics-presence-legend .unread').parent().find('.count').text(users.unread); + graphs.presence.data.datasets[0].data[0] = users.categories; + graphs.presence.data.datasets[0].data[1] = users.topics; + graphs.presence.data.datasets[0].data[2] = users.category; + graphs.presence.data.datasets[0].data[3] = users.recent; + graphs.presence.data.datasets[0].data[4] = users.unread; + + graphs.presence.update(); + } + + function updateTopicsGraph(topics) { + if (!topics.length) { + translator.translate('[[admin/dashboard:no-users-browsing]]', function (translated) { + topics = [{ + title: translated, + count: 1, + }]; + updateTopicsGraph(topics); + }); + return; + } + + graphs.topics.data.labels = []; + graphs.topics.data.datasets[0].data = []; + graphs.topics.data.datasets[0].backgroundColor = []; + graphs.topics.data.datasets[0].hoverBackgroundColor = []; + + topics.forEach(function (topic, i) { + graphs.topics.data.labels.push(topic.title); + graphs.topics.data.datasets[0].data.push(topic.count); + graphs.topics.data.datasets[0].backgroundColor.push(topicColors[i]); + graphs.topics.data.datasets[0].hoverBackgroundColor.push(lighten(topicColors[i], 10)); + }); + + function buildTopicsLegend() { + let html = ''; + topics.forEach(function (t, i) { + const link = t.tid ? ' ' + t.title + '' : t.title; + const label = t.count === '0' ? t.title : link; + + html += '
  • ' + + '
    ' + + ' (' + t.count + ') ' + label + '' + + '
  • '; + }); + $('#topics-legend').translateHtml(html); + } + + buildTopicsLegend(); + graphs.topics.update(); + } + + function setupRealtimeButton() { + $('#toggle-realtime .fa').on('click', function () { + const $this = $(this); + if ($this.hasClass('fa-toggle-on')) { + $this.removeClass('fa-toggle-on').addClass('fa-toggle-off'); + $this.parent().find('strong').html('OFF'); + initiateDashboard(false); + } else { + $this.removeClass('fa-toggle-off').addClass('fa-toggle-on'); + $this.parent().find('strong').html('ON'); + initiateDashboard(true); + } + }); + } + + function initiateDashboard(realtime) { + clearInterval(intervals.rooms); + clearInterval(intervals.graphs); + + intervals.rooms = setInterval(function () { + if (app.isFocused && socket.connected) { + socket.emit('admin.rooms.getAll', Admin.updateRoomUsage); + } + }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.roomInterval); + + intervals.graphs = setInterval(function () { + updateTrafficGraph(currentGraph.units, currentGraph.until, currentGraph.amount); + }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.graphInterval); + } + + function setupFullscreen() { + const container = document.getElementById('analytics-panel'); + const $container = $(container); + const btn = $container.find('.fa-expand'); + let fsMethod; + let exitMethod; + + if (container.requestFullscreen) { + fsMethod = 'requestFullscreen'; + exitMethod = 'exitFullscreen'; + } else if (container.mozRequestFullScreen) { + fsMethod = 'mozRequestFullScreen'; + exitMethod = 'mozCancelFullScreen'; + } else if (container.webkitRequestFullscreen) { + fsMethod = 'webkitRequestFullscreen'; + exitMethod = 'webkitCancelFullScreen'; + } else if (container.msRequestFullscreen) { + fsMethod = 'msRequestFullscreen'; + exitMethod = 'msCancelFullScreen'; + } + + if (fsMethod) { + btn.addClass('active'); + btn.on('click', function () { + if ($container.hasClass('fullscreen')) { + document[exitMethod](); + $container.removeClass('fullscreen'); + } else { + container[fsMethod](); + $container.addClass('fullscreen'); + } + }); + } + } + + return Admin; +}); diff --git a/public/src/admin/dashboard/logins.js b/public/src/admin/dashboard/logins.js new file mode 100644 index 0000000000..92cc2f220d --- /dev/null +++ b/public/src/admin/dashboard/logins.js @@ -0,0 +1,14 @@ +'use strict'; + +define('admin/dashboard/logins', ['admin/modules/dashboard-line-graph'], (graph) => { + const ACP = {}; + + ACP.init = () => { + graph.init({ + set: 'logins', + dataset: ajaxify.data.dataset, + }); + }; + + return ACP; +}); diff --git a/public/src/admin/dashboard/topics.js b/public/src/admin/dashboard/topics.js new file mode 100644 index 0000000000..4119c45c4b --- /dev/null +++ b/public/src/admin/dashboard/topics.js @@ -0,0 +1,32 @@ +'use strict'; + +define('admin/dashboard/topics', ['admin/modules/dashboard-line-graph', 'hooks'], (graph, hooks) => { + const ACP = {}; + + ACP.init = () => { + graph.init({ + set: 'topics', + dataset: ajaxify.data.dataset, + }).then(() => { + hooks.onPage('action:admin.dashboard.updateGraph', ACP.updateTable); + }); + }; + + ACP.updateTable = () => { + if (window.fetch) { + fetch(`${config.relative_path}/api${ajaxify.data.url}${window.location.search}`, { credentials: 'include' }).then((response) => { + if (response.ok) { + response.json().then(function (payload) { + app.parseAndTranslate(ajaxify.data.template.name, 'topics', payload, function (html) { + const tbodyEl = document.querySelector('.topics-list tbody'); + tbodyEl.innerHTML = ''; + tbodyEl.append(...html.map((idx, el) => el)); + }); + }); + } + }); + } + }; + + return ACP; +}); diff --git a/public/src/admin/dashboard/users.js b/public/src/admin/dashboard/users.js new file mode 100644 index 0000000000..d1f5d2fa92 --- /dev/null +++ b/public/src/admin/dashboard/users.js @@ -0,0 +1,34 @@ +'use strict'; + +define('admin/dashboard/users', ['admin/modules/dashboard-line-graph', 'hooks'], (graph, hooks) => { + const ACP = {}; + + ACP.init = () => { + graph.init({ + set: 'registrations', + dataset: ajaxify.data.dataset, + }).then(() => { + hooks.onPage('action:admin.dashboard.updateGraph', ACP.updateTable); + }); + }; + + ACP.updateTable = () => { + if (window.fetch) { + fetch(`${config.relative_path}/api${ajaxify.data.url}${window.location.search}`, { credentials: 'include' }).then((response) => { + if (response.ok) { + response.json().then(function (payload) { + app.parseAndTranslate(ajaxify.data.template.name, 'users', payload, function (html) { + const tbodyEl = document.querySelector('.users-list tbody'); + tbodyEl.innerHTML = ''; + tbodyEl.append(...html.map((idx, el) => el)); + + html.find('.timeago').timeago(); + }); + }); + } + }); + } + }; + + return ACP; +}); diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js new file mode 100644 index 0000000000..9da5b475e7 --- /dev/null +++ b/public/src/admin/extend/plugins.js @@ -0,0 +1,348 @@ +'use strict'; + + +define('admin/extend/plugins', [ + 'translator', + 'benchpress', + 'bootbox', + 'alerts', + 'jquery-ui/widgets/sortable', +], function (translator, Benchpress, bootbox, alerts) { + const Plugins = {}; + Plugins.init = function () { + const pluginsList = $('.plugins'); + const numPlugins = pluginsList[0].querySelectorAll('li').length; + let pluginID; + + if (!numPlugins) { + translator.translate('
  • [[admin/extend/plugins:none-found]]

  • ', function (html) { + pluginsList.append(html); + }); + return; + } + + const searchInputEl = document.querySelector('#plugin-search'); + searchInputEl.value = ''; + + pluginsList.on('click', 'button[data-action="toggleActive"]', function () { + const pluginEl = $(this).parents('li'); + pluginID = pluginEl.attr('data-plugin-id'); + const btn = $('[id="' + pluginID + '"] [data-action="toggleActive"]'); + + const pluginData = ajaxify.data.installed[pluginEl.attr('data-plugin-index')]; + + function toggleActivate() { + socket.emit('admin.plugins.toggleActive', pluginID, function (err, status) { + if (err) { + return alerts.error(err); + } + translator.translate(' [[admin/extend/plugins:plugin-item.' + (status.active ? 'deactivate' : 'activate') + ']]', function (buttonText) { + btn.html(buttonText); + btn.toggleClass('btn-warning', status.active).toggleClass('btn-success', !status.active); + + // clone it to active plugins tab + if (status.active && !$('#active [id="' + pluginID + '"]').length) { + $('#active ul').prepend(pluginEl.clone(true)); + } + + // Toggle active state in template data + pluginData.active = !pluginData.active; + + alerts.alert({ + alert_id: 'plugin_toggled', + title: '[[admin/extend/plugins:alert.' + (status.active ? 'enabled' : 'disabled') + ']]', + message: '[[admin/extend/plugins:alert.' + (status.active ? 'activate-success' : 'deactivate-success') + ']]', + type: status.active ? 'warning' : 'success', + timeout: 5000, + clickfn: function () { + require(['admin/modules/instance'], function (instance) { + instance.rebuildAndRestart(); + }); + }, + }); + }); + }); + } + + if (pluginData.license && pluginData.active !== true) { + Benchpress.render('admin/partials/plugins/license', pluginData).then(function (html) { + bootbox.dialog({ + title: '[[admin/extend/plugins:license.title]]', + message: html, + size: 'large', + buttons: { + cancel: { + label: '[[modules:bootbox.cancel]]', + className: 'btn-link', + }, + save: { + label: '[[modules:bootbox.confirm]]', + className: 'btn-primary', + callback: toggleActivate, + }, + }, + onShown: function () { + const saveEl = this.querySelector('button.btn-primary'); + if (saveEl) { + saveEl.focus(); + } + }, + }); + }); + } else { + toggleActivate(pluginID); + } + }); + + pluginsList.on('click', 'button[data-action="toggleInstall"]', function () { + const btn = $(this); + btn.attr('disabled', true); + pluginID = $(this).parents('li').attr('data-plugin-id'); + + if ($(this).attr('data-installed') === '1') { + return Plugins.toggleInstall(pluginID, $(this).parents('li').attr('data-version')); + } + + Plugins.suggest(pluginID, function (err, payload) { + if (err) { + bootbox.confirm(translator.compile('admin/extend/plugins:alert.suggest-error', err.status, err.responseText), function (confirm) { + if (confirm) { + Plugins.toggleInstall(pluginID, 'latest'); + } else { + btn.removeAttr('disabled'); + } + }); + return; + } + + if (payload.version !== 'latest') { + Plugins.toggleInstall(pluginID, payload.version); + } else if (payload.version === 'latest') { + confirmInstall(pluginID, function (confirm) { + if (confirm) { + Plugins.toggleInstall(pluginID, 'latest'); + } else { + btn.removeAttr('disabled'); + } + }); + } else { + btn.removeAttr('disabled'); + } + }); + }); + + pluginsList.on('click', 'button[data-action="upgrade"]', function () { + const btn = $(this); + const parent = btn.parents('li'); + pluginID = parent.attr('data-plugin-id'); + + Plugins.suggest(pluginID, function (err, payload) { + if (err) { + return bootbox.alert('[[admin/extend/plugins:alert.package-manager-unreachable]]'); + } + + require(['compare-versions'], function (compareVersions) { + const currentVersion = parent.find('.currentVersion').text(); + if (payload.version !== 'latest' && compareVersions.compare(payload.version, currentVersion, '>')) { + upgrade(pluginID, btn, payload.version); + } else if (payload.version === 'latest') { + confirmInstall(pluginID, function () { + upgrade(pluginID, btn, payload.version); + }); + } else { + bootbox.alert(translator.compile('admin/extend/plugins:alert.incompatible', app.config.version, payload.version)); + } + }); + }); + }); + + $(searchInputEl).on('input propertychange', function () { + const term = $(this).val(); + $('.plugins li').each(function () { + const pluginId = $(this).attr('data-plugin-id'); + $(this).toggleClass('hide', pluginId && pluginId.indexOf(term) === -1); + }); + + const tabEls = document.querySelectorAll('.plugins .tab-pane'); + tabEls.forEach((tabEl) => { + const remaining = tabEl.querySelectorAll('li:not(.hide)').length; + const noticeEl = tabEl.querySelector('.no-plugins'); + if (noticeEl) { + noticeEl.classList.toggle('hide', remaining !== 0); + } + }); + }); + + $('#plugin-submit-usage').on('click', function () { + socket.emit('admin.config.setMultiple', { + submitPluginUsage: $(this).prop('checked') ? '1' : '0', + }, function (err) { + if (err) { + return alerts.error(err); + } + }); + }); + + $('#plugin-order').on('click', function () { + $('#order-active-plugins-modal').modal('show'); + socket.emit('admin.plugins.getActive', function (err, activePlugins) { + if (err) { + return alerts.error(err); + } + let html = ''; + activePlugins.forEach(function (plugin) { + html += '
  • ' + plugin + '
  • '; + }); + if (!activePlugins.length) { + translator.translate('[[admin/extend/plugins:none-active]]', function (text) { + $('#order-active-plugins-modal .plugin-list').html(text).sortable(); + }); + return; + } + const list = $('#order-active-plugins-modal .plugin-list'); + list.html(html).sortable(); + + list.find('.fa-chevron-up').on('click', function () { + const item = $(this).parents('li'); + item.prev().before(item); + }); + + list.find('.fa-chevron-down').on('click', function () { + const item = $(this).parents('li'); + item.next().after(item); + }); + }); + }); + + $('#save-plugin-order').on('click', function () { + const plugins = $('#order-active-plugins-modal .plugin-list').children(); + const data = []; + plugins.each(function (index, el) { + data.push({ name: $(el).text(), order: index }); + }); + + socket.emit('admin.plugins.orderActivePlugins', data, function (err) { + if (err) { + return alerts.error(err); + } + $('#order-active-plugins-modal').modal('hide'); + + alerts.alert({ + alert_id: 'plugin_reordered', + title: '[[admin/extend/plugins:alert.reorder]]', + message: '[[admin/extend/plugins:alert.reorder-success]]', + type: 'success', + timeout: 5000, + clickfn: function () { + require(['admin/modules/instance'], function (instance) { + instance.rebuildAndRestart(); + }); + }, + }); + }); + }); + + populateUpgradeablePlugins(); + populateActivePlugins(); + searchInputEl.focus(); + }; + + function confirmInstall(pluginID, callback) { + bootbox.confirm(translator.compile('admin/extend/plugins:alert.possibly-incompatible', pluginID), function (confirm) { + callback(confirm); + }); + } + + function upgrade(pluginID, btn, version) { + btn.attr('disabled', true).find('i').attr('class', 'fa fa-refresh fa-spin'); + socket.emit('admin.plugins.upgrade', { + id: pluginID, + version: version, + }, function (err, isActive) { + if (err) { + return alerts.error(err); + } + const parent = btn.parents('li'); + parent.find('.fa-exclamation-triangle').remove(); + parent.find('.currentVersion').text(version); + btn.remove(); + if (isActive) { + alerts.alert({ + alert_id: 'plugin_upgraded', + title: '[[admin/extend/plugins:alert.upgraded]]', + message: '[[admin/extend/plugins:alert.upgrade-success]]', + type: 'warning', + timeout: 5000, + clickfn: function () { + require(['admin/modules/instance'], function (instance) { + instance.rebuildAndRestart(); + }); + }, + }); + } + }); + } + + Plugins.toggleInstall = function (pluginID, version, callback) { + const btn = $('li[data-plugin-id="' + pluginID + '"] button[data-action="toggleInstall"]'); + btn.find('i').attr('class', 'fa fa-refresh fa-spin'); + + socket.emit('admin.plugins.toggleInstall', { + id: pluginID, + version: version, + }, function (err, pluginData) { + if (err) { + btn.removeAttr('disabled'); + return alerts.error(err); + } + + ajaxify.refresh(); + + alerts.alert({ + alert_id: 'plugin_toggled', + title: '[[admin/extend/plugins:alert.' + (pluginData.installed ? 'installed' : 'uninstalled') + ']]', + message: '[[admin/extend/plugins:alert.' + (pluginData.installed ? 'install-success' : 'uninstall-success') + ']]', + type: 'info', + timeout: 5000, + }); + + if (typeof callback === 'function') { + callback.apply(this, arguments); + } + }); + }; + + Plugins.suggest = function (pluginId, callback) { + const nbbVersion = app.config.version.match(/^\d+\.\d+\.\d+/); + $.ajax((app.config.registry || 'https://packages.nodebb.org') + '/api/v1/suggest', { + type: 'GET', + data: { + package: pluginId, + version: nbbVersion[0], + }, + dataType: 'json', + }).done(function (payload) { + callback(undefined, payload); + }).fail(callback); + }; + + function populateUpgradeablePlugins() { + $('#installed ul li').each(function () { + if ($(this).children('[data-action="upgrade"]').length) { + $('#upgrade ul').append($(this).clone(true)); + } + }); + } + + function populateActivePlugins() { + $('#installed ul li').each(function () { + if ($(this).hasClass('active')) { + $('#active ul').append($(this).clone(true)); + } else { + $('#deactive ul').append($(this).clone(true)); + } + }); + } + + return Plugins; +}); diff --git a/public/src/admin/extend/rewards.js b/public/src/admin/extend/rewards.js new file mode 100644 index 0000000000..c7c05d7ae0 --- /dev/null +++ b/public/src/admin/extend/rewards.js @@ -0,0 +1,186 @@ +'use strict'; + + +define('admin/extend/rewards', ['alerts'], function (alerts) { + const rewards = {}; + + + let available; + let active; + let conditions; + let conditionals; + + rewards.init = function () { + available = ajaxify.data.rewards; + active = ajaxify.data.active; + conditions = ajaxify.data.conditions; + conditionals = ajaxify.data.conditionals; + + $('[data-selected]').each(function () { + select($(this)); + }); + + $('#active') + .on('change', '[data-selected]', function () { + update($(this)); + }) + .on('click', '.delete', function () { + const parent = $(this).parents('[data-id]'); + const id = parent.attr('data-id'); + + socket.emit('admin.rewards.delete', { id: id }, function (err) { + if (err) { + alerts.error(err); + } else { + alerts.success('[[admin/extend/rewards:alert.delete-success]]'); + } + }); + + parent.remove(); + return false; + }) + .on('click', '.toggle', function () { + const btn = $(this); + const disabled = btn.hasClass('btn-success'); + btn.toggleClass('btn-warning').toggleClass('btn-success').translateHtml('[[admin/extend/rewards:' + (disabled ? 'disable' : 'enable') + ']]'); + // send disable api call + return false; + }); + + $('#new').on('click', newReward); + $('#save').on('click', saveRewards); + + populateInputs(); + }; + + function select(el) { + el.val(el.attr('data-selected')); + switch (el.attr('name')) { + case 'rid': + selectReward(el); + break; + } + } + + function update(el) { + el.attr('data-selected', el.val()); + switch (el.attr('name')) { + case 'rid': + selectReward(el); + break; + } + } + + function selectReward(el) { + const parent = el.parents('[data-rid]'); + const div = parent.find('.inputs'); + let inputs; + let html = ''; + + for (const reward in available) { + if (available.hasOwnProperty(reward)) { + if (available[reward].rid === el.attr('data-selected')) { + inputs = available[reward].inputs; + parent.attr('data-rid', available[reward].rid); + break; + } + } + } + + if (!inputs) { + return alerts.error('[[admin/extend/rewards:alert.no-inputs-found]] ' + el.attr('data-selected')); + } + + inputs.forEach(function (input) { + html += '
    '; + }); + + div.html(html); + } + + function populateInputs() { + $('[data-rid]').each(function (i) { + const div = $(this).find('.inputs'); + const rewards = active[i].rewards; + + for (const reward in rewards) { + if (rewards.hasOwnProperty(reward)) { + div.find('[name="' + reward + '"]').val(rewards[reward]); + } + } + }); + } + + function newReward() { + const ul = $('#active'); + + const data = { + active: [{ + disabled: true, + value: '', + claimable: 1, + rid: null, + id: null, + }], + conditions: conditions, + conditionals: conditionals, + rewards: available, + }; + + app.parseAndTranslate('admin/extend/rewards', 'active', data, function (li) { + ul.append(li); + li.find('select').val(''); + }); + } + + function saveRewards() { + const activeRewards = []; + + $('#active li').each(function () { + const data = { rewards: {} }; + const main = $(this).find('form.main').serializeArray(); + const rewards = $(this).find('form.rewards').serializeArray(); + + main.forEach(function (obj) { + data[obj.name] = obj.value; + }); + + rewards.forEach(function (obj) { + data.rewards[obj.name] = obj.value; + }); + + data.id = $(this).attr('data-id'); + data.disabled = $(this).find('.toggle').hasClass('btn-success'); + + activeRewards.push(data); + }); + + socket.emit('admin.rewards.save', activeRewards, function (err, result) { + if (err) { + alerts.error(err); + } else { + alerts.success('[[admin/extend/rewards:alert.save-success]]'); + // newly added rewards are missing data-id, update to prevent rewards getting duplicated + $('#active li').each(function (index) { + if (!$(this).attr('data-id')) { + $(this).attr('data-id', result[index].id); + } + }); + } + }); + } + + return rewards; +}); diff --git a/public/src/admin/extend/widgets.js b/public/src/admin/extend/widgets.js new file mode 100644 index 0000000000..d307b2723f --- /dev/null +++ b/public/src/admin/extend/widgets.js @@ -0,0 +1,282 @@ +'use strict'; + + +define('admin/extend/widgets', [ + 'bootbox', + 'alerts', + 'jquery-ui/widgets/sortable', + 'jquery-ui/widgets/draggable', + 'jquery-ui/widgets/droppable', + 'jquery-ui/widgets/datepicker', +], function (bootbox, alerts) { + const Widgets = {}; + + Widgets.init = function () { + $('#widgets .nav-pills .dropdown-menu a').on('click', function (ev) { + const $this = $(this); + $('#widgets .tab-pane').removeClass('active'); + const templateName = $this.attr('data-template'); + $('#widgets .tab-pane[data-template="' + templateName + '"]').addClass('active'); + $('#widgets .selected-template').text(templateName); + $('#widgets .nav-pills .dropdown').trigger('click'); + ev.preventDefault(); + return false; + }); + + $('#widget-selector').on('change', function () { + $('.available-widgets [data-widget]').addClass('hide'); + $('.available-widgets [data-widget="' + $(this).val() + '"]').removeClass('hide'); + }); + + $('#widget-selector').trigger('change'); + + loadWidgetData(); + setupCloneButton(); + }; + + function prepareWidgets() { + $('[data-location="drafts"]').insertAfter($('[data-location="drafts"]').closest('.tab-content')); + + $('#widgets .available-widgets .widget-panel').draggable({ + helper: function (e) { + return $(e.target).parents('.widget-panel').clone(); + }, + distance: 10, + connectToSortable: '.widget-area', + }); + + $('#widgets .available-containers .containers > [data-container-html]') + .draggable({ + helper: function (e) { + let target = $(e.target); + target = target.attr('data-container-html') ? target : target.parents('[data-container-html]'); + + return target.clone().addClass('block').width(target.width()).css('opacity', '0.5'); + }, + distance: 10, + }) + .each(function () { + $(this).attr('data-container-html', $(this).attr('data-container-html').replace(/\\\{([\s\S]*?)\\\}/g, '{$1}')); + }); + + $('#widgets .widget-area').sortable({ + update: function (event, ui) { + createDatePicker(ui.item); + appendToggle(ui.item); + }, + connectWith: 'div', + }).on('click', '.delete-widget', function () { + const panel = $(this).parents('.widget-panel'); + + bootbox.confirm('[[admin/extend/widgets:alert.confirm-delete]]', function (confirm) { + if (confirm) { + panel.remove(); + } + }); + }).on('mouseup', '> .panel > .panel-heading', function (evt) { + if (!($(this).parent().is('.ui-sortable-helper') || $(evt.target).closest('.delete-widget').length)) { + $(this).parent().children('.panel-body').toggleClass('hidden'); + } + }); + + $('#save').on('click', saveWidgets); + + function saveWidgets() { + const saveData = []; + $('#widgets [data-template][data-location]').each(function (i, el) { + el = $(el); + + const template = el.attr('data-template'); + const location = el.attr('data-location'); + const area = el.children('.widget-area'); + const widgets = []; + + area.find('.widget-panel[data-widget]').each(function () { + const widgetData = {}; + const data = $(this).find('form').serializeArray(); + + for (const d in data) { + if (data.hasOwnProperty(d)) { + if (data[d].name) { + if (widgetData[data[d].name]) { + if (!Array.isArray(widgetData[data[d].name])) { + widgetData[data[d].name] = [ + widgetData[data[d].name], + ]; + } + widgetData[data[d].name].push(data[d].value); + } else { + widgetData[data[d].name] = data[d].value; + } + } + } + } + + widgets.push({ + widget: $(this).attr('data-widget'), + data: widgetData, + }); + }); + + saveData.push({ + template: template, + location: location, + widgets: widgets, + }); + }); + + socket.emit('admin.widgets.set', saveData, function (err) { + if (err) { + alerts.error(err); + } + + alerts.alert({ + alert_id: 'admin:widgets', + type: 'success', + title: '[[admin/extend/widgets:alert.updated]]', + message: '[[admin/extend/widgets:alert.update-success]]', + timeout: 2500, + }); + }); + } + + $('.color-selector').on('click', '.btn', function () { + const btn = $(this); + const selector = btn.parents('.color-selector'); + const container = selector.parents('[data-container-html]'); + const classList = []; + + selector.children().each(function () { + classList.push($(this).attr('data-class')); + }); + + container + .removeClass(classList.join(' ')) + .addClass(btn.attr('data-class')); + + container.attr('data-container-html', container.attr('data-container-html') + .replace(/class="[a-zA-Z0-9-\s]+"/, 'class="' + container[0].className.replace(' pointer ui-draggable ui-draggable-handle', '') + '"')); + }); + } + + function createDatePicker(el) { + const currentYear = new Date().getFullYear(); + el.find('.date-selector').datepicker({ + changeMonth: true, + changeYear: true, + yearRange: currentYear + ':' + (currentYear + 100), + }); + } + + function appendToggle(el) { + if (!el.hasClass('block')) { + el.addClass('block').css('width', '').css('height', '') + .droppable({ + accept: '[data-container-html]', + drop: function (event, ui) { + const el = $(this); + + el.find('.panel-body .container-html').val(ui.draggable.attr('data-container-html')); + el.find('.panel-body').removeClass('hidden'); + }, + hoverClass: 'panel-info', + }) + .children('.panel-heading') + .append('
     
    ') + .children('small') + .html(''); + } + } + + function loadWidgetData() { + function populateWidget(widget, data) { + if (data.title) { + const title = widget.find('.panel-heading strong'); + title.text(title.text() + ' - ' + data.title); + } + + widget.find('input, textarea, select').each(function () { + const input = $(this); + const value = data[input.attr('name')]; + + if (input.attr('type') === 'checkbox') { + input.prop('checked', !!value).trigger('change'); + } else { + input.val(value); + } + }); + + return widget; + } + + $.get(config.relative_path + '/api/admin/extend/widgets', function (data) { + const areas = data.areas; + + for (let i = 0; i < areas.length; i += 1) { + const area = areas[i]; + const widgetArea = $('#widgets .area[data-template="' + area.template + '"][data-location="' + area.location + '"]').find('.widget-area'); + + widgetArea.html(''); + + for (let k = 0; k < area.data.length; k += 1) { + const widgetData = area.data[k]; + const widgetEl = $('.available-widgets [data-widget="' + widgetData.widget + '"]').clone(true).removeClass('hide'); + + widgetArea.append(populateWidget(widgetEl, widgetData.data)); + appendToggle(widgetEl); + createDatePicker(widgetEl); + } + } + + prepareWidgets(); + }); + } + + function setupCloneButton() { + const clone = $('[component="clone"]'); + const cloneBtn = $('[component="clone/button"]'); + + clone.find('.dropdown-menu li').on('click', function () { + const template = $(this).find('a').text(); + cloneBtn.translateHtml('[[admin/extend/widgets:clone-from]] ' + template + ''); + cloneBtn.attr('data-template', template); + }); + + cloneBtn.on('click', function () { + const template = cloneBtn.attr('data-template'); + if (!template) { + return alerts.error('[[admin/extend/widgets:error.select-clone]]'); + } + + const currentTemplate = $('#active-widgets .active.tab-pane[data-template] .area'); + const templateToClone = $('#active-widgets .tab-pane[data-template="' + template + '"] .area'); + + const currentAreas = currentTemplate.map(function () { + return $(this).attr('data-location'); + }).get(); + + const areasToClone = templateToClone.map(function () { + const location = $(this).attr('data-location'); + return currentAreas.indexOf(location) !== -1 ? location : undefined; + }).get().filter(function (i) { return i; }); + + function clone(location) { + $('#active-widgets .tab-pane[data-template="' + template + '"] [data-location="' + location + '"]').each(function () { + $(this).find('[data-widget]').each(function () { + const widget = $(this).clone(true); + $('#active-widgets .active.tab-pane[data-template]:not([data-template="global"]) [data-location="' + location + '"] .widget-area').append(widget); + }); + }); + } + + for (let i = 0, ii = areasToClone.length; i < ii; i++) { + const location = areasToClone[i]; + clone(location); + } + + alerts.success('[[admin/extend/widgets:alert.clone-success]]'); + }); + } + + return Widgets; +}); diff --git a/public/src/admin/manage/admins-mods.js b/public/src/admin/manage/admins-mods.js new file mode 100644 index 0000000000..b5acce0cc9 --- /dev/null +++ b/public/src/admin/manage/admins-mods.js @@ -0,0 +1,133 @@ +'use strict'; + +define('admin/manage/admins-mods', [ + 'autocomplete', 'api', 'bootbox', 'alerts', 'categorySelector', +], function (autocomplete, api, bootbox, alerts, categorySelector) { + const AdminsMods = {}; + + AdminsMods.init = function () { + autocomplete.user($('#admin-search'), function (ev, ui) { + socket.emit('admin.user.makeAdmins', [ui.item.user.uid], function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[admin/manage/users:alerts.make-admin-success]]'); + $('#admin-search').val(''); + + if ($('.administrator-area [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'admins.members', { admins: { members: [ui.item.user] } }, function (html) { + $('.administrator-area').prepend(html); + }); + }); + }); + + $('.administrator-area').on('click', '.remove-user-icon', function () { + const userCard = $(this).parents('[data-uid]'); + const uid = userCard.attr('data-uid'); + if (parseInt(uid, 10) === parseInt(app.user.uid, 10)) { + return alerts.error('[[admin/manage/users:alerts.no-remove-yourself-admin]]'); + } + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-admin]]', function (confirm) { + if (confirm) { + socket.emit('admin.user.removeAdmins', [uid], function (err) { + if (err) { + return alerts.error(err.message); + } + alerts.success('[[admin/manage/users:alerts.remove-admin-success]]'); + userCard.remove(); + }); + } + }); + }); + + autocomplete.user($('#global-mod-search'), function (ev, ui) { + api.put('/groups/global-moderators/membership/' + ui.item.user.uid).then(() => { + alerts.success('[[admin/manage/users:alerts.make-global-mod-success]]'); + $('#global-mod-search').val(''); + + if ($('.global-moderator-area [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'globalMods.members', { globalMods: { members: [ui.item.user] } }, function (html) { + $('.global-moderator-area').prepend(html); + $('#no-global-mods-warning').addClass('hidden'); + }); + }).catch(alerts.error); + }); + + $('.global-moderator-area').on('click', '.remove-user-icon', function () { + const userCard = $(this).parents('[data-uid]'); + const uid = userCard.attr('data-uid'); + + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-global-mod]]', function (confirm) { + if (confirm) { + api.del('/groups/global-moderators/membership/' + uid).then(() => { + alerts.success('[[admin/manage/users:alerts.remove-global-mod-success]]'); + userCard.remove(); + if (!$('.global-moderator-area').children().length) { + $('#no-global-mods-warning').removeClass('hidden'); + } + }).catch(alerts.error); + } + }); + }); + + + categorySelector.init($('[component="category-selector"]'), { + parentCid: ajaxify.data.selectedCategory ? ajaxify.data.selectedCategory.cid : 0, + onSelect: function (selectedCategory) { + ajaxify.go('admin/manage/admins-mods' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : '')); + }, + localCategories: [], + }); + + autocomplete.user($('.moderator-search'), function (ev, ui) { + const input = $(ev.target); + const cid = $(ev.target).attr('data-cid'); + api.put(`/categories/${cid}/moderator/${ui.item.user.uid}`, {}, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[admin/manage/users:alerts.make-moderator-success]]'); + input.val(''); + + if ($('.moderator-area[data-cid="' + cid + '"] [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'globalMods.members', { globalMods: { members: [ui.item.user] } }, function (html) { + $('.moderator-area[data-cid="' + cid + '"]').prepend(html); + $('.no-moderator-warning[data-cid="' + cid + '"]').addClass('hidden'); + }); + }); + }); + + $('.moderator-area').on('click', '.remove-user-icon', function () { + const moderatorArea = $(this).parents('[data-cid]'); + const cid = moderatorArea.attr('data-cid'); + const userCard = $(this).parents('[data-uid]'); + const uid = userCard.attr('data-uid'); + + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-moderator]]', function (confirm) { + if (confirm) { + api.delete(`/categories/${cid}/moderator/${uid}`, {}, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[admin/manage/users:alerts.remove-moderator-success]]'); + userCard.remove(); + if (!moderatorArea.children().length) { + $('.no-moderator-warning[data-cid="' + cid + '"]').removeClass('hidden'); + } + }); + } + }); + }); + }; + + return AdminsMods; +}); diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js new file mode 100644 index 0000000000..3c6660a4c0 --- /dev/null +++ b/public/src/admin/manage/categories.js @@ -0,0 +1,304 @@ +'use strict'; + +define('admin/manage/categories', [ + 'translator', + 'benchpress', + 'categorySelector', + 'api', + 'Sortable', + 'bootbox', + 'alerts', +], function (translator, Benchpress, categorySelector, api, Sortable, bootbox, alerts) { + Sortable = Sortable.default; + const Categories = {}; + let newCategoryId = -1; + let sortables; + + Categories.init = function () { + categorySelector.init($('.category [component="category-selector"]'), { + parentCid: ajaxify.data.selectedCategory ? ajaxify.data.selectedCategory.cid : 0, + onSelect: function (selectedCategory) { + ajaxify.go('/admin/manage/categories' + (selectedCategory.cid ? '?cid=' + selectedCategory.cid : '')); + }, + localCategories: [], + }); + Categories.render(ajaxify.data.categoriesTree); + + $('button[data-action="create"]').on('click', Categories.throwCreateModal); + + // Enable/Disable toggle events + $('.categories').on('click', '.category-tools [data-action="toggle"]', function () { + const $this = $(this); + const cid = $this.attr('data-disable-cid'); + const parentEl = $this.parents('li[data-cid="' + cid + '"]'); + const disabled = parentEl.hasClass('disabled'); + const childrenEls = parentEl.find('li[data-cid]'); + const childrenCids = childrenEls.map(function () { + return $(this).attr('data-cid'); + }).get(); + + Categories.toggle([cid].concat(childrenCids), !disabled); + }); + + $('.categories').on('click', '.toggle', function () { + const el = $(this); + el.find('i').toggleClass('fa-chevron-down').toggleClass('fa-chevron-right'); + el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden'); + }); + + $('.categories').on('click', '.set-order', function () { + const cid = $(this).attr('data-cid'); + const order = $(this).attr('data-order'); + const modal = bootbox.dialog({ + title: '[[admin/manage/categories:set-order]]', + message: '

    [[admin/manage/categories:set-order-help]]

    ', + show: true, + buttons: { + save: { + label: '[[modules:bootbox.confirm]]', + className: 'btn-primary', + callback: function () { + const val = modal.find('input').val(); + if (val && cid) { + const modified = {}; + modified[cid] = { order: Math.max(1, parseInt(val, 10)) }; + api.put('/categories/' + cid, modified[cid]).then(function () { + ajaxify.refresh(); + }).catch(alerts.error); + } else { + return false; + } + }, + }, + }, + }); + }); + + $('#collapse-all').on('click', function () { + toggleAll(false); + }); + + $('#expand-all').on('click', function () { + toggleAll(true); + }); + + function toggleAll(expand) { + const el = $('.categories .toggle'); + el.find('i').toggleClass('fa-chevron-down', expand).toggleClass('fa-chevron-right', !expand); + el.closest('[data-cid]').find('> ul[data-cid]').toggleClass('hidden', !expand); + } + }; + + Categories.throwCreateModal = function () { + Benchpress.render('admin/partials/categories/create', {}).then(function (html) { + const modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.create]]', + message: html, + buttons: { + save: { + label: '[[global:save]]', + className: 'btn-primary', + callback: submit, + }, + }, + }); + const options = { + localCategories: [ + { + cid: 0, + name: '[[admin/manage/categories:parent-category-none]]', + icon: 'fa-none', + }, + ], + }; + const parentSelector = categorySelector.init(modal.find('#parentCidGroup [component="category-selector"]'), options); + const cloneFromSelector = categorySelector.init(modal.find('#cloneFromCidGroup [component="category-selector"]'), options); + function submit() { + const formData = modal.find('form').serializeObject(); + formData.description = ''; + formData.icon = 'fa-comments'; + formData.uid = app.user.uid; + formData.parentCid = parentSelector.getSelectedCid(); + formData.cloneFromCid = cloneFromSelector.getSelectedCid(); + + Categories.create(formData); + modal.modal('hide'); + return false; + } + + $('#cloneChildren').on('change', function () { + const check = $(this); + const parentSelect = modal.find('#parentCidGroup [component="category-selector"] .dropdown-toggle'); + + if (check.prop('checked')) { + parentSelect.attr('disabled', 'disabled'); + parentSelector.selectCategory(0); + } else { + parentSelect.removeAttr('disabled'); + } + }); + + modal.find('form').on('submit', submit); + }); + }; + + Categories.create = function (payload) { + api.post('/categories', payload, function (err, data) { + if (err) { + return alerts.error(err); + } + + alerts.alert({ + alert_id: 'category_created', + title: '[[admin/manage/categories:alert.created]]', + message: '[[admin/manage/categories:alert.create-success]]', + type: 'success', + timeout: 2000, + }); + + ajaxify.go('admin/manage/categories/' + data.cid); + }); + }; + + Categories.render = function (categories) { + const container = $('.categories'); + + if (!categories || !categories.length) { + translator.translate('[[admin/manage/categories:alert.none-active]]', function (text) { + $('
    ') + .addClass('alert alert-info text-center') + .text(text) + .appendTo(container); + }); + } else { + sortables = {}; + renderList(categories, container, { cid: 0 }); + } + }; + + Categories.toggle = function (cids, disabled) { + const listEl = document.querySelector('.categories ul'); + Promise.all(cids.map(cid => api.put('/categories/' + cid, { + disabled: disabled ? 1 : 0, + }).then(() => { + const categoryEl = listEl.querySelector(`li[data-cid="${cid}"]`); + categoryEl.classList[disabled ? 'add' : 'remove']('disabled'); + $(categoryEl).find('li a[data-action="toggle"]').first().translateText(disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]'); + }).catch(alerts.error))); + }; + + function itemDidAdd(e) { + newCategoryId = e.to.dataset.cid; + } + + function itemDragDidEnd(e) { + const isCategoryUpdate = parseInt(newCategoryId, 10) !== -1; + + // Update needed? + if ((e.newIndex != null && parseInt(e.oldIndex, 10) !== parseInt(e.newIndex, 10)) || isCategoryUpdate) { + const cid = e.item.dataset.cid; + const modified = {}; + // on page 1 baseIndex is 0, on page n baseIndex is (n - 1) * ajaxify.data.categoriesPerPage + // this makes sure order is correct when drag & drop is used on pages > 1 + const baseIndex = (ajaxify.data.pagination.currentPage - 1) * ajaxify.data.categoriesPerPage; + modified[cid] = { + order: baseIndex + e.newIndex + 1, + }; + + if (isCategoryUpdate) { + modified[cid].parentCid = newCategoryId; + + // Show/hide expand buttons after drag completion + const oldParentCid = parseInt(e.from.getAttribute('data-cid'), 10); + const newParentCid = parseInt(e.to.getAttribute('data-cid'), 10); + if (oldParentCid !== newParentCid) { + const toggle = document.querySelector(`.categories li[data-cid="${newParentCid}"] .toggle`); + if (toggle) { + toggle.classList.toggle('hide', false); + } + + const children = document.querySelectorAll(`.categories li[data-cid="${oldParentCid}"] ul[data-cid] li[data-cid]`); + if (!children.length) { + const toggle = document.querySelector(`.categories li[data-cid="${oldParentCid}"] .toggle`); + if (toggle) { + toggle.classList.toggle('hide', true); + } + } + + e.item.dataset.parentCid = newParentCid; + } + } + + newCategoryId = -1; + api.put('/categories/' + cid, modified[cid]).catch(alerts.error); + } + } + + /** + * Render categories - recursively + * + * @param categories {array} categories tree + * @param level {number} current sub-level of rendering + * @param container {object} parent jquery element for the list + * @param parentId {number} parent category identifier + */ + function renderList(categories, container, parentCategory) { + // Translate category names if needed + let count = 0; + const parentId = parentCategory.cid; + categories.forEach(function (category, idx, parent) { + translator.translate(category.name, function (translated) { + if (category.name !== translated) { + category.name = translated; + } + count += 1; + + if (count === parent.length) { + continueRender(); + } + }); + }); + + if (!categories.length) { + continueRender(); + } + + function continueRender() { + app.parseAndTranslate('admin/partials/categories/category-rows', { + cid: parentCategory.cid, + categories: categories, + parentCategory: parentCategory, + }, function (html) { + if (container.find('.category-row').length) { + container.find('.category-row').after(html); + } else { + container.append(html); + } + + // Disable expand toggle + if (!categories.length) { + const toggleEl = container.get(0).querySelector('.toggle'); + toggleEl.classList.toggle('hide', true); + } + + // Handle and children categories in this level have + for (let x = 0, numCategories = categories.length; x < numCategories; x += 1) { + renderList(categories[x].children, $('li[data-cid="' + categories[x].cid + '"]'), categories[x]); + } + + // Make list sortable + sortables[parentId] = Sortable.create($('ul[data-cid="' + parentId + '"]')[0], { + group: 'cross-categories', + animation: 150, + handle: '.information', + dataIdAttr: 'data-cid', + ghostClass: 'placeholder', + onAdd: itemDidAdd, + onEnd: itemDragDidEnd, + }); + }); + } + } + + return Categories; +}); diff --git a/public/src/admin/manage/category-analytics.js b/public/src/admin/manage/category-analytics.js new file mode 100644 index 0000000000..f9694e0e51 --- /dev/null +++ b/public/src/admin/manage/category-analytics.js @@ -0,0 +1,173 @@ +'use strict'; + + +define('admin/manage/category-analytics', ['Chart'], function (Chart) { + const CategoryAnalytics = {}; + + CategoryAnalytics.init = function () { + const hourlyCanvas = document.getElementById('pageviews:hourly'); + const dailyCanvas = document.getElementById('pageviews:daily'); + const topicsCanvas = document.getElementById('topics:daily'); + const postsCanvas = document.getElementById('posts:daily'); + const hourlyLabels = utils.getHoursArray().map(function (text, idx) { + return idx % 3 ? '' : text; + }); + const dailyLabels = utils.getDaysArray().map(function (text, idx) { + return idx % 3 ? '' : text; + }); + + if (utils.isMobile()) { + Chart.defaults.global.tooltips.enabled = false; + } + + const data = { + 'pageviews:hourly': { + labels: hourlyLabels, + datasets: [ + { + label: '', + backgroundColor: 'rgba(186,139,175,0.2)', + borderColor: 'rgba(186,139,175,1)', + pointBackgroundColor: 'rgba(186,139,175,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(186,139,175,1)', + data: ajaxify.data.analytics['pageviews:hourly'], + }, + ], + }, + 'pageviews:daily': { + labels: dailyLabels, + datasets: [ + { + label: '', + backgroundColor: 'rgba(151,187,205,0.2)', + borderColor: 'rgba(151,187,205,1)', + pointBackgroundColor: 'rgba(151,187,205,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(151,187,205,1)', + data: ajaxify.data.analytics['pageviews:daily'], + }, + ], + }, + 'topics:daily': { + labels: dailyLabels.slice(-7), + datasets: [ + { + label: '', + backgroundColor: 'rgba(171,70,66,0.2)', + borderColor: 'rgba(171,70,66,1)', + pointBackgroundColor: 'rgba(171,70,66,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(171,70,66,1)', + data: ajaxify.data.analytics['topics:daily'], + }, + ], + }, + 'posts:daily': { + labels: dailyLabels.slice(-7), + datasets: [ + { + label: '', + backgroundColor: 'rgba(161,181,108,0.2)', + borderColor: 'rgba(161,181,108,1)', + pointBackgroundColor: 'rgba(161,181,108,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(161,181,108,1)', + data: ajaxify.data.analytics['posts:daily'], + }, + ], + }, + }; + + hourlyCanvas.width = $(hourlyCanvas).parent().width(); + dailyCanvas.width = $(dailyCanvas).parent().width(); + topicsCanvas.width = $(topicsCanvas).parent().width(); + postsCanvas.width = $(postsCanvas).parent().width(); + + new Chart(hourlyCanvas.getContext('2d'), { + type: 'line', + data: data['pageviews:hourly'], + options: { + responsive: true, + animation: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + precision: 0, + }, + }], + }, + }, + }); + + new Chart(dailyCanvas.getContext('2d'), { + type: 'line', + data: data['pageviews:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + precision: 0, + }, + }], + }, + }, + }); + + new Chart(topicsCanvas.getContext('2d'), { + type: 'line', + data: data['topics:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + precision: 0, + }, + }], + }, + }, + }); + + new Chart(postsCanvas.getContext('2d'), { + type: 'line', + data: data['posts:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + precision: 0, + }, + }], + }, + }, + }); + }; + + return CategoryAnalytics; +}); diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js new file mode 100644 index 0000000000..d5b14e8d8c --- /dev/null +++ b/public/src/admin/manage/category.js @@ -0,0 +1,310 @@ +'use strict'; + +define('admin/manage/category', [ + 'uploader', + 'iconSelect', + 'categorySelector', + 'benchpress', + 'api', + 'bootbox', + 'alerts', +], function (uploader, iconSelect, categorySelector, Benchpress, api, bootbox, alerts) { + const Category = {}; + let updateHash = {}; + + Category.init = function () { + $('#category-settings select').each(function () { + const $this = $(this); + $this.val($this.attr('data-value')); + }); + + categorySelector.init($('[component="category-selector"]'), { + onSelect: function (selectedCategory) { + ajaxify.go('admin/manage/categories/' + selectedCategory.cid); + }, + showLinks: true, + }); + + handleTags(); + + $('#category-settings input, #category-settings select, #category-settings textarea').on('change', function (ev) { + modified(ev.target); + }); + + $('[data-name="imageClass"]').on('change', function () { + $('.category-preview').css('background-size', $(this).val()); + }); + + $('[data-name="bgColor"], [data-name="color"]').on('input', function () { + const $inputEl = $(this); + const previewEl = $inputEl.parents('[data-cid]').find('.category-preview'); + if ($inputEl.attr('data-name') === 'bgColor') { + previewEl.css('background-color', $inputEl.val()); + } else if ($inputEl.attr('data-name') === 'color') { + previewEl.css('color', $inputEl.val()); + } + + modified($inputEl[0]); + }); + + $('#save').on('click', function () { + const tags = $('#tag-whitelist').val() ? $('#tag-whitelist').val().split(',') : []; + if (tags.length && tags.length < parseInt($('#cid-min-tags').val(), 10)) { + return alerts.error('[[admin/manage/categories:alert.not-enough-whitelisted-tags]]'); + } + + const cid = ajaxify.data.category.cid; + api.put('/categories/' + cid, updateHash).then((res) => { + app.flags._unsaved = false; + alerts.alert({ + title: 'Updated Categories', + message: 'Category "' + res.name + '" was successfully updated.', + type: 'success', + timeout: 5000, + }); + updateHash = {}; + }).catch(alerts.error); + + return false; + }); + + $('.purge').on('click', function (e) { + e.preventDefault(); + + Benchpress.render('admin/partials/categories/purge', { + name: ajaxify.data.category.name, + topic_count: ajaxify.data.category.topic_count, + }).then(function (html) { + const modal = bootbox.dialog({ + title: '[[admin/manage/categories:purge]]', + message: html, + size: 'large', + buttons: { + save: { + label: '[[modules:bootbox.confirm]]', + className: 'btn-primary', + callback: function () { + modal.find('.modal-footer button').prop('disabled', true); + + const intervalId = setInterval(function () { + socket.emit('categories.getTopicCount', ajaxify.data.category.cid, function (err, count) { + if (err) { + return alerts.error(err); + } + + let percent = 0; + if (ajaxify.data.category.topic_count > 0) { + percent = + Math.max(0, (1 - (count / ajaxify.data.category.topic_count))) * 100; + } + + modal.find('.progress-bar').css({ width: percent + '%' }); + }); + }, 1000); + + api.del('/categories/' + ajaxify.data.category.cid).then(() => { + if (intervalId) { + clearInterval(intervalId); + } + modal.modal('hide'); + alerts.success('[[admin/manage/categories:alert.purge-success]]'); + ajaxify.go('admin/manage/categories'); + }).catch(alerts.error); + + return false; + }, + }, + }, + }); + }); + }); + + $('.copy-settings').on('click', function () { + Benchpress.render('admin/partials/categories/copy-settings', {}).then(function (html) { + let selectedCid; + const modal = bootbox.dialog({ + title: '[[modules:composer.select_category]]', + message: html, + buttons: { + save: { + label: '[[modules:bootbox.confirm]]', + className: 'btn-primary', + callback: function () { + if (!selectedCid || + parseInt(selectedCid, 10) === parseInt(ajaxify.data.category.cid, 10)) { + return; + } + + socket.emit('admin.categories.copySettingsFrom', { + fromCid: selectedCid, + toCid: ajaxify.data.category.cid, + copyParent: modal.find('#copyParent').prop('checked'), + }, function (err) { + if (err) { + return alerts.error(err); + } + + modal.modal('hide'); + alert.success('[[admin/manage/categories:alert.copy-success]]'); + ajaxify.refresh(); + }); + return false; + }, + }, + }, + }); + modal.find('.modal-footer button').prop('disabled', true); + categorySelector.init(modal.find('[component="category-selector"]'), { + onSelect: function (selectedCategory) { + selectedCid = selectedCategory && selectedCategory.cid; + if (selectedCid) { + modal.find('.modal-footer button').prop('disabled', false); + } + }, + showLinks: true, + }); + }); + return false; + }); + + $('.upload-button').on('click', function () { + const inputEl = $(this); + const cid = inputEl.attr('data-cid'); + + uploader.show({ + title: '[[admin/manage/categories:alert.upload-image]]', + route: config.relative_path + '/api/admin/category/uploadpicture', + params: { cid: cid }, + }, function (imageUrlOnServer) { + $('#category-image').val(imageUrlOnServer); + const previewBox = inputEl.parent().parent().siblings('.category-preview'); + previewBox.css('background', 'url(' + imageUrlOnServer + '?' + new Date().getTime() + ')'); + + modified($('#category-image')); + }); + }); + + $('#category-image').on('change', function () { + $('.category-preview').css('background-image', $(this).val() ? ('url("' + $(this).val() + '")') : ''); + modified($('#category-image')); + }); + + $('.delete-image').on('click', function (e) { + e.preventDefault(); + + const inputEl = $('#category-image'); + const previewBox = $('.category-preview'); + + inputEl.val(''); + previewBox.css('background-image', ''); + modified(inputEl[0]); + $(this).parent().addClass('hide').hide(); + }); + + $('.category-preview').on('click', function () { + iconSelect.init($(this).find('i'), modified); + }); + + $('[type="checkbox"]').on('change', function () { + modified($(this)); + }); + + $('button[data-action="setParent"], button[data-action="changeParent"]').on('click', Category.launchParentSelector); + $('button[data-action="removeParent"]').on('click', function () { + api.put('/categories/' + ajaxify.data.category.cid, { + parentCid: 0, + }).then(() => { + $('button[data-action="removeParent"]').parent().addClass('hide'); + $('button[data-action="changeParent"]').parent().addClass('hide'); + $('button[data-action="setParent"]').removeClass('hide'); + }).catch(alerts.error); + }); + $('button[data-action="toggle"]').on('click', function () { + const $this = $(this); + const disabled = $this.attr('data-disabled') === '1'; + api.put('/categories/' + ajaxify.data.category.cid, { + disabled: disabled ? 0 : 1, + }).then(() => { + $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); + }).catch(alerts.error); + }); + }; + + function modified(el) { + let value; + if ($(el).is(':checkbox')) { + value = $(el).is(':checked') ? 1 : 0; + } else { + value = $(el).val(); + } + const dataName = $(el).attr('data-name'); + const fields = dataName.match(/[^\][.]+/g); + + function setNestedFields(obj, index) { + if (index === fields.length) { + return; + } + obj[fields[index]] = obj[fields[index]] || {}; + if (index === fields.length - 1) { + obj[fields[index]] = value; + } + setNestedFields(obj[fields[index]], index + 1); + } + + if (fields && fields.length) { + if (fields.length === 1) { // simple field name ie data-name="name" + updateHash[fields[0]] = value; + } else if (fields.length > 1) { // nested field name ie data-name="name[sub1][sub2]" + setNestedFields(updateHash, 0); + } + } + + app.flags = app.flags || {}; + app.flags._unsaved = true; + } + + function handleTags() { + const tagEl = $('#tag-whitelist'); + tagEl.tagsinput({ + confirmKeys: [13, 44], + trimValue: true, + }); + + ajaxify.data.category.tagWhitelist.forEach(function (tag) { + tagEl.tagsinput('add', tag); + }); + + tagEl.on('itemAdded itemRemoved', function () { + modified(tagEl); + }); + } + + Category.launchParentSelector = function () { + categorySelector.modal({ + onSubmit: function (selectedCategory) { + const parentCid = selectedCategory.cid; + if (!parentCid) { + return; + } + api.put('/categories/' + ajaxify.data.category.cid, { + parentCid: parentCid, + }).then(() => { + api.get(`/categories/${parentCid}`, {}).then(function (parent) { + if (parent && parent.icon && parent.name) { + const buttonHtml = ' ' + parent.name; + $('button[data-action="changeParent"]').html(buttonHtml).parent().removeClass('hide'); + } + }); + + $('button[data-action="removeParent"]').parent().removeClass('hide'); + $('button[data-action="setParent"]').addClass('hide'); + }).catch(alerts.error); + }, + showLinks: true, + }); + }; + + return Category; +}); diff --git a/public/src/admin/manage/digest.js b/public/src/admin/manage/digest.js new file mode 100644 index 0000000000..d8e518cf0d --- /dev/null +++ b/public/src/admin/manage/digest.js @@ -0,0 +1,45 @@ +'use strict'; + + +define('admin/manage/digest', ['bootbox', 'alerts'], function (bootbox, alerts) { + const Digest = {}; + + Digest.init = function () { + $('table').on('click', '[data-action]', function () { + const action = this.getAttribute('data-action'); + const uid = this.getAttribute('data-uid'); + + if (action.startsWith('resend-')) { + const interval = action.slice(7); + bootbox.confirm('[[admin/manage/digest:resend-all-confirm]]', function (ok) { + if (ok) { + Digest.send(action, undefined, function (err) { + if (err) { + return alerts.error(err); + } + + alerts.success('[[admin/manage/digest:resent-' + interval + ']]'); + }); + } + }); + } else { + Digest.send(action, uid, function (err) { + if (err) { + return alerts.error(err); + } + + alerts.success('[[admin/manage/digest:resent-single]]'); + }); + } + }); + }; + + Digest.send = function (action, uid, callback) { + socket.emit('admin.digest.resend', { + action: action, + uid: uid, + }, callback); + }; + + return Digest; +}); diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js new file mode 100644 index 0000000000..610477817d --- /dev/null +++ b/public/src/admin/manage/group.js @@ -0,0 +1,165 @@ +'use strict'; + +define('admin/manage/group', [ + 'forum/groups/memberlist', + 'iconSelect', + 'translator', + 'categorySelector', + 'groupSearch', + 'slugify', + 'api', + 'bootbox', + 'alerts', +], function (memberList, iconSelect, translator, categorySelector, groupSearch, slugify, api, bootbox, alerts) { + const Groups = {}; + + Groups.init = function () { + const groupIcon = $('#group-icon'); + const changeGroupUserTitle = $('#change-group-user-title'); + const changeGroupLabelColor = $('#change-group-label-color'); + const changeGroupTextColor = $('#change-group-text-color'); + const groupLabelPreview = $('#group-label-preview'); + const groupLabelPreviewText = $('#group-label-preview-text'); + + const groupName = ajaxify.data.group.name; + + $('#group-selector').on('change', function () { + ajaxify.go('admin/manage/groups/' + $(this).val() + window.location.hash); + }); + + memberList.init('admin/manage/group'); + + changeGroupUserTitle.on('keyup', function () { + groupLabelPreviewText.translateText(changeGroupUserTitle.val()); + }); + + changeGroupLabelColor.on('keyup input', function () { + groupLabelPreview.css('background-color', changeGroupLabelColor.val() || '#000000'); + }); + + changeGroupTextColor.on('keyup input', function () { + groupLabelPreview.css('color', changeGroupTextColor.val() || '#ffffff'); + }); + + setupGroupMembersMenu(); + + $('#group-icon, #group-icon-label').on('click', function () { + const currentIcon = groupIcon.attr('value'); + iconSelect.init(groupIcon, function () { + let newIcon = groupIcon.attr('value'); + if (newIcon === currentIcon) { + return; + } + if (newIcon === 'fa-nbb-none') { + newIcon = 'hidden'; + } + $('#group-icon-preview').attr('class', 'fa fa-fw ' + (newIcon || 'hidden')); + app.flags = app.flags || {}; + app.flags._unsaved = true; + }); + }); + + categorySelector.init($('.edit-privileges-selector [component="category-selector"]'), { + onSelect: function (selectedCategory) { + navigateToCategory(selectedCategory.cid); + }, + showLinks: true, + }); + + const cidSelector = categorySelector.init($('.member-post-cids-selector [component="category-selector"]'), { + onSelect: function (selectedCategory) { + let cids = ($('#memberPostCids').val() || '').split(',').map(cid => parseInt(cid, 10)); + cids.push(selectedCategory.cid); + cids = cids.filter((cid, index, array) => array.indexOf(cid) === index); + $('#memberPostCids').val(cids.join(',')); + cidSelector.selectCategory(0); + }, + }); + + groupSearch.init($('[component="group-selector"]')); + + $('form [data-property]').on('change', function () { + app.flags = app.flags || {}; + app.flags._unsaved = true; + }); + + $('#save').on('click', function () { + api.put(`/groups/${slugify(groupName)}`, { + name: $('#change-group-name').val(), + userTitle: changeGroupUserTitle.val(), + description: $('#change-group-desc').val(), + icon: groupIcon.attr('value'), + labelColor: changeGroupLabelColor.val(), + textColor: changeGroupTextColor.val(), + userTitleEnabled: $('#group-userTitleEnabled').is(':checked'), + private: $('#group-private').is(':checked'), + hidden: $('#group-hidden').is(':checked'), + memberPostCids: $('#memberPostCids').val(), + disableJoinRequests: $('#group-disableJoinRequests').is(':checked'), + disableLeave: $('#group-disableLeave').is(':checked'), + }).then(() => { + const newName = $('#change-group-name').val(); + + // If the group name changed, change url + if (groupName !== newName) { + ajaxify.go('admin/manage/groups/' + encodeURIComponent(newName), undefined, true); + } + + alerts.success('[[admin/manage/groups:edit.save-success]]'); + }).catch(alerts.error); + return false; + }); + }; + + function setupGroupMembersMenu() { + $('[component="groups/members"]').on('click', '[data-action]', function () { + const btnEl = $(this); + const userRow = btnEl.parents('[data-uid]'); + const ownerFlagEl = userRow.find('.member-name .user-owner-icon'); + const isOwner = !ownerFlagEl.hasClass('invisible'); + const uid = userRow.attr('data-uid'); + const action = btnEl.attr('data-action'); + + switch (action) { + case 'toggleOwnership': + api[isOwner ? 'del' : 'put'](`/groups/${ajaxify.data.group.slug}/ownership/${uid}`, {}).then(() => { + ownerFlagEl.toggleClass('invisible'); + }).catch(alerts.error); + break; + + case 'kick': + bootbox.confirm('[[admin/manage/groups:edit.confirm-remove-user]]', function (confirm) { + if (!confirm) { + return; + } + api.del('/groups/' + ajaxify.data.group.slug + '/membership/' + uid).then(() => { + userRow.slideUp().remove(); + }).catch(alerts.error); + }); + break; + default: + break; + } + }); + } + + function navigateToCategory(cid) { + if (cid) { + const url = 'admin/manage/privileges/' + cid + '?group=' + ajaxify.data.group.nameEncoded; + if (app.flags && app.flags._unsaved === true) { + translator.translate('[[global:unsaved-changes]]', function (text) { + bootbox.confirm(text, function (navigate) { + if (navigate) { + app.flags._unsaved = false; + ajaxify.go(url); + } + }); + }); + return; + } + ajaxify.go(url); + } + } + + return Groups; +}); diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js new file mode 100644 index 0000000000..5015416626 --- /dev/null +++ b/public/src/admin/manage/groups.js @@ -0,0 +1,122 @@ +'use strict'; + +define('admin/manage/groups', [ + 'categorySelector', + 'slugify', + 'api', + 'bootbox', + 'alerts', +], function (categorySelector, slugify, api, bootbox, alerts) { + const Groups = {}; + + Groups.init = function () { + const createModal = $('#create-modal'); + const createGroupName = $('#create-group-name'); + const createModalGo = $('#create-modal-go'); + const createModalError = $('#create-modal-error'); + + handleSearch(); + + createModal.on('keypress', function (e) { + if (e.keyCode === 13) { + createModalGo.click(); + } + }); + + $('#create').on('click', function () { + createModal.modal('show'); + setTimeout(function () { + createGroupName.focus(); + }, 250); + }); + + createModalGo.on('click', function () { + const submitObj = { + name: createGroupName.val(), + description: $('#create-group-desc').val(), + private: $('#create-group-private').is(':checked') ? 1 : 0, + hidden: $('#create-group-hidden').is(':checked') ? 1 : 0, + }; + + api.post('/groups', submitObj).then((response) => { + createModalError.addClass('hide'); + createGroupName.val(''); + createModal.on('hidden.bs.modal', function () { + ajaxify.go('admin/manage/groups/' + response.name); + }); + createModal.modal('hide'); + }).catch((err) => { + if (!utils.hasLanguageKey(err.status.message)) { + err.status.message = '[[admin/manage/groups:alerts.create-failure]]'; + } + createModalError.translateHtml(err.status.message).removeClass('hide'); + }); + }); + + $('.groups-list').on('click', '[data-action]', function () { + const el = $(this); + const action = el.attr('data-action'); + const groupName = el.parents('tr[data-groupname]').attr('data-groupname'); + + switch (action) { + case 'delete': + bootbox.confirm('[[admin/manage/groups:alerts.confirm-delete]]', function (confirm) { + if (confirm) { + api.del(`/groups/${slugify(groupName)}`, {}).then(ajaxify.refresh).catch(alerts.error); + } + }); + break; + } + }); + + enableCategorySelectors(); + }; + + function enableCategorySelectors() { + $('.groups-list [component="category-selector"]').each(function () { + const nameEncoded = $(this).parents('[data-name-encoded]').attr('data-name-encoded'); + categorySelector.init($(this), { + onSelect: function (selectedCategory) { + ajaxify.go('admin/manage/privileges/' + selectedCategory.cid + '?group=' + nameEncoded); + }, + showLinks: true, + }); + }); + } + + function handleSearch() { + const queryEl = $('#group-search'); + + function doSearch() { + if (!queryEl.val()) { + return ajaxify.refresh(); + } + $('.pagination').addClass('hide'); + const groupsEl = $('.groups-list'); + socket.emit('groups.search', { + query: queryEl.val(), + options: { + sort: 'date', + }, + }, function (err, groups) { + if (err) { + return alerts.error(err); + } + + app.parseAndTranslate('admin/manage/groups', 'groups', { + groups: groups, + categories: ajaxify.data.categories, + }, function (html) { + groupsEl.find('[data-groupname]').remove(); + groupsEl.find('tbody').append(html); + enableCategorySelectors(); + }); + }); + } + + queryEl.on('keyup', utils.debounce(doSearch, 200)); + } + + + return Groups; +}); diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js new file mode 100644 index 0000000000..38f9792d86 --- /dev/null +++ b/public/src/admin/manage/privileges.js @@ -0,0 +1,501 @@ +'use strict'; + +define('admin/manage/privileges', [ + 'api', + 'autocomplete', + 'bootbox', + 'alerts', + 'translator', + 'categorySelector', + 'mousetrap', + 'admin/modules/checkboxRowSelector', +], function (api, autocomplete, bootbox, alerts, translator, categorySelector, mousetrap, checkboxRowSelector) { + const Privileges = {}; + + let cid; + // number of columns to skip in category privilege tables + const SKIP_PRIV_COLS = 3; + + Privileges.init = function () { + cid = isNaN(parseInt(ajaxify.data.selectedCategory.cid, 10)) ? 'admin' : ajaxify.data.selectedCategory.cid; + + checkboxRowSelector.init('.privilege-table-container'); + + categorySelector.init($('[component="category-selector"]'), { + onSelect: function (category) { + cid = parseInt(category.cid, 10); + cid = isNaN(cid) ? 'admin' : cid; + Privileges.refreshPrivilegeTable(); + ajaxify.updateHistory('admin/manage/privileges/' + (cid || '')); + }, + localCategories: ajaxify.data.categories, + privilege: 'find', + showLinks: true, + }); + + Privileges.setupPrivilegeTable(); + + highlightRow(); + $('.privilege-filters button:last-child').click(); + }; + + Privileges.setupPrivilegeTable = function () { + $('.privilege-table-container').on('change', 'input[type="checkbox"]:not(.checkbox-helper)', function () { + const $checkboxEl = $(this); + const $wrapperEl = $checkboxEl.parent(); + const columnNo = $wrapperEl.index() + 1; + const privilege = $wrapperEl.attr('data-privilege'); + const state = $checkboxEl.prop('checked'); + const $rowEl = $checkboxEl.parents('tr'); + const member = $rowEl.attr('data-group-name') || $rowEl.attr('data-uid'); + const isPrivate = parseInt($rowEl.attr('data-private') || 0, 10); + const isGroup = $rowEl.attr('data-group-name') !== undefined; + const isBanned = (isGroup && $rowEl.attr('data-group-name') === 'banned-users') || $rowEl.attr('data-banned') !== undefined; + const sourceGroupName = isBanned ? 'banned-users' : 'registered-users'; + const delta = $checkboxEl.prop('checked') === ($wrapperEl.attr('data-value') === 'true') ? null : state; + + if (member) { + if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) { + bootbox.confirm('[[admin/manage/privileges:alert.confirm-moderate]]', function (confirm) { + if (confirm) { + $wrapperEl.attr('data-delta', delta); + Privileges.exposeSingleAssumedPriv(columnNo, sourceGroupName); + } else { + $checkboxEl.prop('checked', !$checkboxEl.prop('checked')); + } + }); + } else if (privilege.endsWith('admin:admins-mods') && state) { + bootbox.confirm('[[admin/manage/privileges:alert.confirm-admins-mods]]', function (confirm) { + if (confirm) { + $wrapperEl.attr('data-delta', delta); + Privileges.exposeSingleAssumedPriv(columnNo, sourceGroupName); + } else { + $checkboxEl.prop('checked', !$checkboxEl.prop('checked')); + } + }); + } else { + $wrapperEl.attr('data-delta', delta); + Privileges.exposeSingleAssumedPriv(columnNo, sourceGroupName); + } + checkboxRowSelector.updateState($checkboxEl); + } else { + alerts.error('[[error:invalid-data]]'); + } + }); + + Privileges.exposeAssumedPrivileges(); + checkboxRowSelector.updateAll(); + Privileges.addEvents(); // events with confirmation modals + }; + + Privileges.addEvents = function () { + document.getElementById('save').addEventListener('click', function () { + throwConfirmModal('save', Privileges.commit); + }); + + document.getElementById('discard').addEventListener('click', function () { + throwConfirmModal('discard', Privileges.discard); + }); + + // Expose discard button as necessary + const containerEl = document.querySelector('.privilege-table-container'); + containerEl.addEventListener('change', (e) => { + const subselector = e.target.closest('td[data-privilege] input'); + if (subselector) { + document.getElementById('discard').style.display = containerEl.querySelectorAll('td[data-delta]').length ? 'unset' : 'none'; + } + }); + + const $privTableCon = $('.privilege-table-container'); + $privTableCon.on('click', '[data-action="search.user"]', Privileges.addUserToPrivilegeTable); + $privTableCon.on('click', '[data-action="search.group"]', Privileges.addGroupToPrivilegeTable); + $privTableCon.on('click', '[data-action="copyToChildren"]', function () { + throwConfirmModal('copyToChildren', Privileges.copyPrivilegesToChildren.bind(null, cid, '')); + }); + $privTableCon.on('click', '[data-action="copyToChildrenGroup"]', function () { + const groupName = $(this).parents('[data-group-name]').attr('data-group-name'); + throwConfirmModal('copyToChildrenGroup', Privileges.copyPrivilegesToChildren.bind(null, cid, groupName)); + }); + + $privTableCon.on('click', '[data-action="copyPrivilegesFrom"]', function () { + Privileges.copyPrivilegesFromCategory(cid, ''); + }); + $privTableCon.on('click', '[data-action="copyPrivilegesFromGroup"]', function () { + const groupName = $(this).parents('[data-group-name]').attr('data-group-name'); + Privileges.copyPrivilegesFromCategory(cid, groupName); + }); + + $privTableCon.on('click', '[data-action="copyToAll"]', function () { + throwConfirmModal('copyToAll', Privileges.copyPrivilegesToAllCategories.bind(null, cid, '')); + }); + $privTableCon.on('click', '[data-action="copyToAllGroup"]', function () { + const groupName = $(this).parents('[data-group-name]').attr('data-group-name'); + throwConfirmModal('copyToAllGroup', Privileges.copyPrivilegesToAllCategories.bind(null, cid, groupName)); + }); + + $privTableCon.on('click', '.privilege-filters > button', filterPrivileges); + + mousetrap.bind('ctrl+s', function (ev) { + throwConfirmModal('save', Privileges.commit); + ev.preventDefault(); + }); + + function throwConfirmModal(method, onConfirm) { + const privilegeSubset = getPrivilegeSubset(); + bootbox.confirm(`[[admin/manage/privileges:alert.confirm-${method}, ${privilegeSubset}]]

    [[admin/manage/privileges:alert.no-undo]]`, function (ok) { + if (ok) { + onConfirm.call(); + } + }); + } + }; + + Privileges.commit = function () { + const tableEl = document.querySelector('.privilege-table-container'); + const requests = $.map(tableEl.querySelectorAll('td[data-delta]'), function (el) { + const privilege = el.getAttribute('data-privilege'); + const rowEl = el.parentNode; + const member = rowEl.getAttribute('data-group-name') || rowEl.getAttribute('data-uid'); + const state = el.getAttribute('data-delta') === 'true' ? 1 : 0; + + return Privileges.setPrivilege(member, privilege, state); + }); + + Promise.allSettled(requests).then((results) => { + Privileges.refreshPrivilegeTable(); + + const rejects = results.filter(r => r.status === 'rejected'); + if (rejects.length) { + rejects.forEach((result) => { + alerts.error(result.reason); + }); + } else { + alerts.success('[[admin/manage/privileges:alert.saved]]'); + } + }); + }; + + Privileges.discard = function () { + Privileges.refreshPrivilegeTable(); + alerts.success('[[admin/manage/privileges:alert.discarded]]'); + }; + + Privileges.refreshPrivilegeTable = function (groupToHighlight) { + api.get(`/categories/${cid}/privileges`, {}).then((privileges) => { + ajaxify.data.privileges = { ...ajaxify.data.privileges, ...privileges }; + const tpl = parseInt(cid, 10) ? 'admin/partials/privileges/category' : 'admin/partials/privileges/global'; + const isAdminPriv = ajaxify.currentPage.endsWith('admin/manage/privileges/admin'); + app.parseAndTranslate(tpl, { privileges, isAdminPriv }).then((html) => { + // Get currently selected filters + const btnIndices = $('.privilege-filters button.btn-warning').map((idx, el) => $(el).index()).get(); + $('.privilege-table-container').html(html); + Privileges.exposeAssumedPrivileges(); + document.querySelectorAll('.privilege-filters').forEach((con, i) => { + // Three buttons, placed in reverse order + const lastIdx = $('.privilege-filters').first().find('button').length - 1; + const idx = btnIndices[i] === undefined ? lastIdx : btnIndices[i]; + con.querySelectorAll('button')[idx].click(); + }); + + hightlightRowByDataAttr('data-group-name', groupToHighlight); + }); + }).catch(alert.error); + }; + + Privileges.exposeAssumedPrivileges = function () { + /* + If registered-users has a privilege enabled, then all users and groups of that privilege + should be assumed to have that privilege as well, even if not set in the db, so reflect + this arrangement in the table + */ + + // As such, individual banned users inherits privileges from banned-users group + const getBannedUsersInputSelector = (privs, i) => `.privilege-table tr[data-banned] td[data-privilege="${privs[i]}"] input`; + const bannedUsersPrivs = getPrivilegesFromRow('banned-users'); + applyPrivileges(bannedUsersPrivs, getBannedUsersInputSelector); + + // For rest that inherits from registered-users + const getRegisteredUsersInputSelector = (privs, i) => `.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="banned-users"],[data-group-name="guests"],[data-group-name="spiders"]) td[data-privilege="${privs[i]}"] input, .privilege-table tr[data-uid]:not([data-banned]) td[data-privilege="${privs[i]}"] input`; + const registeredUsersPrivs = getPrivilegesFromRow('registered-users'); + applyPrivileges(registeredUsersPrivs, getRegisteredUsersInputSelector); + }; + + Privileges.exposeSingleAssumedPriv = function (columnNo, sourceGroupName) { + let inputSelectorFn; + switch (sourceGroupName) { + case 'banned-users': + inputSelectorFn = () => `.privilege-table tr[data-banned] td[data-privilege]:nth-child(${columnNo}) input`; + break; + default: + inputSelectorFn = () => `.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="banned-users"],[data-group-name="guests"],[data-group-name="spiders"]) td[data-privilege]:nth-child(${columnNo}) input, .privilege-table tr[data-uid]:not([data-banned]) td[data-privilege]:nth-child(${columnNo}) input`; + } + + const sourceChecked = getPrivilegeFromColumn(sourceGroupName, columnNo); + applyPrivilegesToColumn(inputSelectorFn, sourceChecked); + }; + + Privileges.setPrivilege = (member, privilege, state) => api[state ? 'put' : 'delete'](`/categories/${isNaN(cid) ? 0 : cid}/privileges/${encodeURIComponent(privilege)}`, { member }); + + Privileges.addUserToPrivilegeTable = function () { + const modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.find-user]]', + message: '', + show: true, + }); + + modal.on('shown.bs.modal', function () { + const inputEl = modal.find('input'); + inputEl.focus(); + + autocomplete.user(inputEl, function (ev, ui) { + addUserToCategory(ui.item.user, function () { + modal.modal('hide'); + }); + }); + }); + }; + + Privileges.addGroupToPrivilegeTable = function () { + const modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.find-group]]', + message: '', + show: true, + }); + + modal.on('shown.bs.modal', function () { + const inputEl = modal.find('input'); + inputEl.focus(); + + autocomplete.group(inputEl, function (ev, ui) { + if (ui.item.group.name === 'administrators') { + return alerts.alert({ + type: 'warning', + message: '[[admin/manage/privileges:alert.admin-warning]]', + }); + } + addGroupToCategory(ui.item.group.name, function () { + modal.modal('hide'); + }); + }); + }); + }; + + Privileges.copyPrivilegesToChildren = function (cid, group) { + const filter = getPrivilegeFilter(); + socket.emit('admin.categories.copyPrivilegesToChildren', { cid, group, filter }, function (err) { + if (err) { + return alerts.error(err.message); + } + alerts.success('[[admin/manage/categories:privileges.copy-success]]'); + }); + }; + + Privileges.copyPrivilegesFromCategory = function (cid, group) { + const privilegeSubset = getPrivilegeSubset(); + const message = '
    ' + + (group ? `[[admin/manage/privileges:alert.copyPrivilegesFromGroup-warning, ${privilegeSubset}]]` : + `[[admin/manage/privileges:alert.copyPrivilegesFrom-warning, ${privilegeSubset}]]`) + + '

    [[admin/manage/privileges:alert.no-undo]]'; + categorySelector.modal({ + title: '[[admin/manage/privileges:alert.copyPrivilegesFrom-title]]', + message, + localCategories: [], + showLinks: true, + onSubmit: function (selectedCategory) { + socket.emit('admin.categories.copyPrivilegesFrom', { + toCid: cid, + filter: getPrivilegeFilter(), + fromCid: selectedCategory.cid, + group: group, + }, function (err) { + if (err) { + return alerts.error(err); + } + ajaxify.refresh(); + }); + }, + }); + }; + + Privileges.copyPrivilegesToAllCategories = function (cid, group) { + const filter = getPrivilegeFilter(); + socket.emit('admin.categories.copyPrivilegesToAllCategories', { cid, group, filter }, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[admin/manage/categories:privileges.copy-success]]'); + }); + }; + + function getPrivilegesFromRow(sourceGroupName) { + const privs = []; + $(`.privilege-table tr[data-group-name="${sourceGroupName}"] td input[type="checkbox"]:not(.checkbox-helper)`) + .parent() + .each(function (idx, el) { + if ($(el).find('input').prop('checked')) { + privs.push(el.getAttribute('data-privilege')); + } + }); + + // Also apply to non-group privileges + return privs.concat(privs.map(function (priv) { + if (priv.startsWith('groups:')) { + return priv.slice(7); + } + + return false; + })).filter(Boolean); + } + + function getPrivilegeFromColumn(sourceGroupName, columnNo) { + return $(`.privilege-table tr[data-group-name="${sourceGroupName}"] td:nth-child(${columnNo}) input[type="checkbox"]`)[0].checked; + } + + function applyPrivileges(privs, inputSelectorFn) { + for (let x = 0, numPrivs = privs.length; x < numPrivs; x += 1) { + const inputs = $(inputSelectorFn(privs, x)); + inputs.each(function (idx, el) { + if (!el.checked) { + el.indeterminate = true; + } + }); + } + } + + function applyPrivilegesToColumn(inputSelectorFn, sourceChecked) { + const $inputs = $(inputSelectorFn()); + $inputs.each((idx, el) => { + el.indeterminate = el.checked ? false : sourceChecked; + }); + } + + function hightlightRowByDataAttr(attrName, attrValue) { + if (attrValue) { + const $el = $('[' + attrName + ']').filter(function () { + return $(this).attr(attrName) === String(attrValue); + }); + + if ($el.length) { + $el.addClass('selected'); + return true; + } + } + return false; + } + + function highlightRow() { + if (ajaxify.data.group) { + if (hightlightRowByDataAttr('data-group-name', ajaxify.data.group)) { + return; + } + addGroupToCategory(ajaxify.data.group); + } + } + + function addGroupToCategory(group, cb) { + cb = cb || function () {}; + const groupRow = document.querySelector('.privilege-table [data-group-name="' + group + '"]'); + if (groupRow) { + hightlightRowByDataAttr('data-group-name', group); + return cb(); + } + // Generate data for new row + const privilegeSet = ajaxify.data.privileges.keys.groups.reduce(function (memo, cur) { + memo[cur] = false; + return memo; + }, {}); + + app.parseAndTranslate('admin/partials/privileges/' + ((isNaN(cid) || cid === 0) ? 'global' : 'category'), 'privileges.groups', { + privileges: { + groups: [ + { + name: group, + nameEscaped: translator.escape(group), + privileges: privilegeSet, + }, + ], + }, + }, function (html) { + const tbodyEl = document.querySelector('.privilege-table tbody'); + const btnIdx = $('.privilege-filters').first().find('button.btn-warning').index(); + tbodyEl.append(html.get(0)); + Privileges.exposeAssumedPrivileges(); + hightlightRowByDataAttr('data-group-name', group); + document.querySelector('.privilege-filters').querySelectorAll('button')[btnIdx].click(); + cb(); + }); + } + + async function addUserToCategory(user, cb) { + cb = cb || function () {}; + const userRow = document.querySelector('.privilege-table [data-uid="' + user.uid + '"]'); + if (userRow) { + hightlightRowByDataAttr('data-uid', user.uid); + return cb(); + } + // Generate data for new row + const privilegeSet = ajaxify.data.privileges.keys.users.reduce(function (memo, cur) { + memo[cur] = false; + return memo; + }, {}); + + const html = await app.parseAndTranslate('admin/partials/privileges/' + (isNaN(cid) ? 'global' : 'category'), 'privileges.users', { + privileges: { + users: [ + { + picture: user.picture, + username: user.username, + banned: user.banned, + uid: user.uid, + 'icon:text': user['icon:text'], + 'icon:bgColor': user['icon:bgColor'], + privileges: privilegeSet, + }, + ], + }, + }); + + const tbodyEl = document.querySelectorAll('.privilege-table tbody'); + const btnIdx = $('.privilege-filters').last().find('button.btn-warning').index(); + tbodyEl[1].append(html.get(0)); + Privileges.exposeAssumedPrivileges(); + hightlightRowByDataAttr('data-uid', user.uid); + document.querySelectorAll('.privilege-filters')[1].querySelectorAll('button')[btnIdx].click(); + cb(); + } + + function filterPrivileges(ev) { + const [startIdx, endIdx] = ev.target.getAttribute('data-filter').split(',').map(i => parseInt(i, 10)); + const rows = $(ev.target).closest('table')[0].querySelectorAll('thead tr:last-child, tbody tr '); + rows.forEach((tr) => { + tr.querySelectorAll('td, th').forEach((el, idx) => { + const offset = el.tagName.toUpperCase() === 'TH' ? 1 : 0; + if (idx < (SKIP_PRIV_COLS - offset)) { + return; + } + el.classList.toggle('hidden', !(idx >= (startIdx - offset) && idx <= (endIdx - offset))); + }); + }); + checkboxRowSelector.updateAll(); + $(ev.target).siblings('button').toArray().forEach(btn => btn.classList.remove('btn-warning')); + ev.target.classList.add('btn-warning'); + } + + function getPrivilegeFilter() { + const indices = document.querySelector('.privilege-filters .btn-warning') + .getAttribute('data-filter') + .split(',') + .map(i => parseInt(i, 10)); + indices[0] -= SKIP_PRIV_COLS; + indices[1] = indices[1] - SKIP_PRIV_COLS + 1; + return indices; + } + + function getPrivilegeSubset() { + const currentPrivFilter = document.querySelector('.privilege-filters .btn-warning'); + const filterText = currentPrivFilter ? currentPrivFilter.textContent.toLocaleLowerCase() : ''; + return filterText.indexOf('privileges') > -1 ? filterText : `${filterText} privileges`.trim(); + } + + return Privileges; +}); diff --git a/public/src/admin/manage/registration.js b/public/src/admin/manage/registration.js new file mode 100644 index 0000000000..4b63647b05 --- /dev/null +++ b/public/src/admin/manage/registration.js @@ -0,0 +1,56 @@ +'use strict'; + + +define('admin/manage/registration', ['bootbox', 'alerts'], function (bootbox, alerts) { + const Registration = {}; + + Registration.init = function () { + $('.users-list').on('click', '[data-action]', function () { + const parent = $(this).parents('[data-username]'); + const action = $(this).attr('data-action'); + const username = parent.attr('data-username'); + const method = action === 'accept' ? 'user.acceptRegistration' : 'user.rejectRegistration'; + + socket.emit(method, { username: username }, function (err) { + if (err) { + return alerts.error(err); + } + parent.remove(); + }); + return false; + }); + + $('.invites-list').on('click', '[data-action]', function () { + const parent = $(this).parents('[data-invitation-mail][data-invited-by]'); + const email = parent.attr('data-invitation-mail'); + const invitedBy = parent.attr('data-invited-by'); + const action = $(this).attr('data-action'); + const method = 'user.deleteInvitation'; + + const removeRow = function () { + const nextRow = parent.next(); + const thisRowinvitedBy = parent.find('.invited-by'); + const nextRowInvitedBy = nextRow.find('.invited-by'); + if (nextRowInvitedBy.html() !== undefined && nextRowInvitedBy.html().length < 2) { + nextRowInvitedBy.html(thisRowinvitedBy.html()); + } + parent.remove(); + }; + if (action === 'delete') { + bootbox.confirm('[[admin/manage/registration:invitations.confirm-delete]]', function (confirm) { + if (confirm) { + socket.emit(method, { email: email, invitedBy: invitedBy }, function (err) { + if (err) { + return alerts.error(err); + } + removeRow(); + }); + } + }); + } + return false; + }); + }; + + return Registration; +}); diff --git a/public/src/admin/manage/tags.js b/public/src/admin/manage/tags.js new file mode 100644 index 0000000000..08668ac187 --- /dev/null +++ b/public/src/admin/manage/tags.js @@ -0,0 +1,141 @@ +'use strict'; + + +define('admin/manage/tags', [ + 'bootbox', + 'alerts', + 'admin/modules/selectable', +], function (bootbox, alerts, selectable) { + const Tags = {}; + + Tags.init = function () { + selectable.enable('.tag-management', '.tag-row'); + + handleCreate(); + handleSearch(); + handleRename(); + handleDeleteSelected(); + }; + + function handleCreate() { + const createModal = $('#create-modal'); + const createTagName = $('#create-tag-name'); + const createModalGo = $('#create-modal-go'); + + createModal.on('keypress', function (e) { + if (e.keyCode === 13) { + createModalGo.click(); + } + }); + + $('#create').on('click', function () { + createModal.modal('show'); + setTimeout(function () { + createTagName.focus(); + }, 250); + }); + + createModalGo.on('click', function () { + socket.emit('admin.tags.create', { + tag: createTagName.val(), + }, function (err) { + if (err) { + return alerts.error(err); + } + + createTagName.val(''); + createModal.on('hidden.bs.modal', function () { + ajaxify.refresh(); + }); + createModal.modal('hide'); + }); + }); + } + + function handleSearch() { + $('#tag-search').on('input propertychange', utils.debounce(function () { + socket.emit('topics.searchAndLoadTags', { + query: $('#tag-search').val(), + }, function (err, result) { + if (err) { + return alerts.error(err); + } + + app.parseAndTranslate('admin/manage/tags', 'tags', { + tags: result.tags, + }, function (html) { + $('.tag-list').html(html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + selectable.enable('.tag-management', '.tag-row'); + }); + }); + }, 250)); + } + + function handleRename() { + $('#rename').on('click', function () { + const tagsToModify = $('.tag-row.ui-selected'); + if (!tagsToModify.length) { + return; + } + + const modal = bootbox.dialog({ + title: '[[admin/manage/tags:alerts.editing]]', + message: $('.rename-modal').html(), + buttons: { + success: { + label: 'Save', + className: 'btn-primary save', + callback: function () { + const data = []; + tagsToModify.each(function (idx, tag) { + tag = $(tag); + data.push({ + value: tag.attr('data-tag'), + newName: modal.find('[data-name="value"]').val(), + }); + }); + + socket.emit('admin.tags.rename', data, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[admin/manage/tags:alerts.update-success]]'); + ajaxify.refresh(); + }); + }, + }, + }, + }); + }); + } + + function handleDeleteSelected() { + $('#deleteSelected').on('click', function () { + const tagsToDelete = $('.tag-row.ui-selected'); + if (!tagsToDelete.length) { + return; + } + + bootbox.confirm('[[admin/manage/tags:alerts.confirm-delete]]', function (confirm) { + if (!confirm) { + return; + } + const tags = []; + tagsToDelete.each(function (index, el) { + tags.push($(el).attr('data-tag')); + }); + socket.emit('admin.tags.deleteTags', { + tags: tags, + }, function (err) { + if (err) { + return alerts.error(err); + } + tagsToDelete.remove(); + }); + }); + }); + } + + return Tags; +}); diff --git a/public/src/admin/manage/uploads.js b/public/src/admin/manage/uploads.js new file mode 100644 index 0000000000..5985647254 --- /dev/null +++ b/public/src/admin/manage/uploads.js @@ -0,0 +1,49 @@ +'use strict'; + +define('admin/manage/uploads', ['api', 'bootbox', 'alerts', 'uploader'], function (api, bootbox, alerts, uploader) { + const Uploads = {}; + + Uploads.init = function () { + $('#upload').on('click', function () { + uploader.show({ + title: '[[admin/manage/uploads:upload-file]]', + route: config.relative_path + '/api/admin/upload/file', + params: { folder: ajaxify.data.currentFolder }, + }, function () { + ajaxify.refresh(); + }); + }); + + $('.delete').on('click', function () { + const file = $(this).parents('[data-path]'); + bootbox.confirm('[[admin/manage/uploads:confirm-delete]]', function (ok) { + if (!ok) { + return; + } + + api.del('/files', { + path: file.attr('data-path'), + }).then(() => { + file.remove(); + }).catch(alerts.error); + }); + }); + + $('#new-folder').on('click', async function () { + bootbox.prompt('[[admin/manage/uploads:name-new-folder]]', (newFolderName) => { + if (!newFolderName || !newFolderName.trim()) { + return; + } + + api.put('/files/folder', { + path: ajaxify.data.currentFolder, + folderName: newFolderName, + }).then(() => { + ajaxify.refresh(); + }).catch(alerts.error); + }); + }); + }; + + return Uploads; +}); diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js new file mode 100644 index 0000000000..aef2a57498 --- /dev/null +++ b/public/src/admin/manage/users.js @@ -0,0 +1,549 @@ +'use strict'; + +define('admin/manage/users', [ + 'translator', 'benchpress', 'autocomplete', 'api', 'slugify', 'bootbox', 'alerts', 'accounts/invite', +], function (translator, Benchpress, autocomplete, api, slugify, bootbox, alerts, AccountInvite) { + const Users = {}; + + Users.init = function () { + $('#results-per-page').val(ajaxify.data.resultsPerPage).on('change', function () { + const query = utils.params(); + query.resultsPerPage = $('#results-per-page').val(); + const qs = buildSearchQuery(query); + ajaxify.go(window.location.pathname + '?' + qs); + }); + + $('.export-csv').on('click', function () { + socket.once('event:export-users-csv', function () { + alerts.remove('export-users-start'); + alerts.alert({ + alert_id: 'export-users', + type: 'success', + title: '[[global:alert.success]]', + message: '[[admin/manage/users:export-users-completed]]', + clickfn: function () { + window.location.href = config.relative_path + '/api/admin/users/csv'; + }, + timeout: 0, + }); + }); + socket.emit('admin.user.exportUsersCSV', {}, function (err) { + if (err) { + return alerts.error(err); + } + alerts.alert({ + alert_id: 'export-users-start', + message: '[[admin/manage/users:export-users-started]]', + timeout: (ajaxify.data.userCount / 5000) * 500, + }); + }); + + return false; + }); + + function getSelectedUids() { + const uids = []; + + $('.users-table [component="user/select/single"]').each(function () { + if ($(this).is(':checked')) { + uids.push($(this).attr('data-uid')); + } + }); + + return uids; + } + + function update(className, state) { + $('.users-table [component="user/select/single"]:checked').parents('.user-row').find(className).each(function () { + $(this).toggleClass('hidden', !state); + }); + } + + function unselectAll() { + $('.users-table [component="user/select/single"]').prop('checked', false); + $('.users-table [component="user/select/all"]').prop('checked', false); + } + + function removeRow(uid) { + const checkboxEl = document.querySelector(`.users-table [component="user/select/single"][data-uid="${uid}"]`); + if (checkboxEl) { + const rowEl = checkboxEl.closest('.user-row'); + rowEl.parentNode.removeChild(rowEl); + } + } + + // use onSuccess instead + function done(successMessage, className, flag) { + return function (err) { + if (err) { + return alerts.error(err); + } + alerts.success(successMessage); + if (className) { + update(className, flag); + } + unselectAll(); + }; + } + + function onSuccess(successMessage, className, flag) { + alerts.success(successMessage); + if (className) { + update(className, flag); + } + unselectAll(); + } + + $('[component="user/select/all"]').on('click', function () { + $('.users-table [component="user/select/single"]').prop('checked', $(this).is(':checked')); + }); + + $('.manage-groups').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + alerts.error('[[error:no-users-selected]]'); + return false; + } + socket.emit('admin.user.loadGroups', uids, function (err, data) { + if (err) { + return alerts.error(err); + } + Benchpress.render('admin/partials/manage_user_groups', data).then(function (html) { + const modal = bootbox.dialog({ + message: html, + title: '[[admin/manage/users:manage-groups]]', + onEscape: true, + }); + modal.on('shown.bs.modal', function () { + autocomplete.group(modal.find('.group-search'), function (ev, ui) { + const uid = $(ev.target).attr('data-uid'); + api.put('/groups/' + ui.item.group.slug + '/membership/' + uid, undefined).then(() => { + ui.item.group.nameEscaped = translator.escape(ui.item.group.displayName); + app.parseAndTranslate('admin/partials/manage_user_groups', { users: [{ groups: [ui.item.group] }] }, function (html) { + $('[data-uid=' + uid + '] .group-area').append(html.find('.group-area').html()); + }); + }).catch(alerts.error); + }); + }); + modal.on('click', '.group-area a', function () { + modal.modal('hide'); + }); + modal.on('click', '.remove-group-icon', function () { + const groupCard = $(this).parents('[data-group-name]'); + const groupName = groupCard.attr('data-group-name'); + const uid = $(this).parents('[data-uid]').attr('data-uid'); + api.del('/groups/' + slugify(groupName) + '/membership/' + uid).then(() => { + groupCard.remove(); + }).catch(alerts.error); + return false; + }); + }); + }); + }); + + $('.ban-user').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + alerts.error('[[error:no-users-selected]]'); + return false; // specifically to keep the menu open + } + + bootbox.confirm((uids.length > 1 ? '[[admin/manage/users:alerts.confirm-ban-multi]]' : '[[admin/manage/users:alerts.confirm-ban]]'), function (confirm) { + if (confirm) { + Promise.all(uids.map(function (uid) { + return api.put('/users/' + uid + '/ban'); + })).then(() => { + onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true); + }).catch(alerts.error); + } + }); + }); + + $('.ban-user-temporary').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + alerts.error('[[error:no-users-selected]]'); + return false; // specifically to keep the menu open + } + + Benchpress.render('admin/partials/temporary-ban', {}).then(function (html) { + bootbox.dialog({ + className: 'ban-modal', + title: '[[user:ban_account]]', + message: html, + show: true, + buttons: { + close: { + label: '[[global:close]]', + className: 'btn-link', + }, + submit: { + label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]', + callback: function () { + const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) { + data[cur.name] = cur.value; + return data; + }, {}); + const until = formData.length > 0 ? ( + Date.now() + + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) + ) : 0; + + Promise.all(uids.map(function (uid) { + return api.put('/users/' + uid + '/ban', { + until: until, + reason: formData.reason, + }); + })).then(() => { + onSuccess('[[admin/manage/users:alerts.ban-success]]', '.ban', true); + }).catch(alerts.error); + }, + }, + }, + }); + }); + }); + + $('.unban-user').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + alerts.error('[[error:no-users-selected]]'); + return false; // specifically to keep the menu open + } + + Promise.all(uids.map(function (uid) { + return api.del('/users/' + uid + '/ban'); + })).then(() => { + onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false); + }); + }); + + $('.reset-lockout').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + return; + } + + socket.emit('admin.user.resetLockouts', uids, done('[[admin/manage/users:alerts.lockout-reset-success]]')); + }); + + $('.validate-email').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + return; + } + + bootbox.confirm('[[admin/manage/users:alerts.confirm-validate-email]]', function (confirm) { + if (!confirm) { + return; + } + socket.emit('admin.user.validateEmail', uids, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[admin/manage/users:alerts.validate-email-success]]'); + update('.notvalidated', false); + update('.validated', true); + unselectAll(); + }); + }); + }); + + $('.send-validation-email').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + return; + } + socket.emit('admin.user.sendValidationEmail', uids, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[notifications:email-confirm-sent]]'); + }); + }); + + $('.password-reset-email').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + return; + } + + bootbox.confirm('[[admin/manage/users:alerts.password-reset-confirm]]', function (confirm) { + if (confirm) { + socket.emit('admin.user.sendPasswordResetEmail', uids, done('[[admin/manage/users:alerts.password-reset-email-sent]]')); + } + }); + }); + + $('.force-password-reset').on('click', function () { + const uids = getSelectedUids(); + if (!uids.length) { + return; + } + + bootbox.confirm('[[admin/manage/users:alerts.confirm-force-password-reset]]', function (confirm) { + if (confirm) { + socket.emit('admin.user.forcePasswordReset', uids, done('[[admin/manage/users:alerts.validate-force-password-reset-success]]')); + } + }); + }); + + $('.delete-user').on('click', () => { + handleDelete('[[admin/manage/users:alerts.confirm-delete]]', '/account'); + }); + + $('.delete-user-content').on('click', () => { + handleDelete('[[admin/manage/users:alerts.confirm-delete-content]]', '/content'); + }); + + $('.delete-user-and-content').on('click', () => { + handleDelete('[[admin/manage/users:alerts.confirm-purge]]', ''); + }); + + const tableEl = document.querySelector('.users-table'); + const actionBtn = document.getElementById('action-dropdown'); + tableEl.addEventListener('change', (e) => { + const subselector = e.target.closest('[component="user/select/single"]') || e.target.closest('[component="user/select/all"]'); + if (subselector) { + const uids = getSelectedUids(); + if (uids.length) { + actionBtn.removeAttribute('disabled'); + } else { + actionBtn.setAttribute('disabled', 'disabled'); + } + } + }); + + function handleDelete(confirmMsg, path) { + const uids = getSelectedUids(); + if (!uids.length) { + return; + } + + bootbox.confirm(confirmMsg, function (confirm) { + if (confirm) { + Promise.all( + uids.map( + uid => api.del(`/users/${uid}${path}`, {}).then(() => { + if (path !== '/content') { + removeRow(uid); + } + }) + ) + ).then(() => { + if (path !== '/content') { + alerts.success('[[admin/manage/users:alerts.delete-success]]'); + } else { + alerts.success('[[admin/manage/users:alerts.delete-content-success]]'); + } + unselectAll(); + if (!$('.users-table [component="user/select/single"]').length) { + ajaxify.refresh(); + } + }).catch(alerts.error); + } + }); + } + + function handleUserCreate() { + $('[data-action="create"]').on('click', function () { + Benchpress.render('admin/partials/create_user_modal', {}).then(function (html) { + const modal = bootbox.dialog({ + message: html, + title: '[[admin/manage/users:alerts.create]]', + onEscape: true, + buttons: { + cancel: { + label: '[[admin/manage/users:alerts.button-cancel]]', + className: 'btn-link', + }, + create: { + label: '[[admin/manage/users:alerts.button-create]]', + className: 'btn-primary', + callback: function () { + createUser.call(this); + return false; + }, + }, + }, + }); + modal.on('shown.bs.modal', function () { + modal.find('#create-user-name').focus(); + }); + }); + return false; + }); + } + + function createUser() { + const modal = this; + const username = document.getElementById('create-user-name').value; + const email = document.getElementById('create-user-email').value; + const password = document.getElementById('create-user-password').value; + const passwordAgain = document.getElementById('create-user-password-again').value; + + const errorEl = $('#create-modal-error'); + + if (password !== passwordAgain) { + return errorEl.translateHtml('[[admin/manage/users:alerts.error-x, [[admin/manage/users:alerts.error-passwords-different]]]]').removeClass('hide'); + } + + const user = { + username: username, + email: email, + password: password, + }; + + api.post('/users', user) + .then(() => { + modal.modal('hide'); + modal.on('hidden.bs.modal', function () { + ajaxify.refresh(); + }); + alerts.success('[[admin/manage/users:alerts.create-success]]'); + }) + .catch(err => errorEl.translateHtml('[[admin/manage/users:alerts.error-x, ' + err.message + ']]').removeClass('hidden')); + } + + handleSearch(); + handleUserCreate(); + handleSort(); + handleFilter(); + AccountInvite.handle(); + }; + + function handleSearch() { + function doSearch() { + $('.fa-spinner').removeClass('hidden'); + loadSearchPage({ + searchBy: $('#user-search-by').val(), + query: $('#user-search').val(), + page: 1, + }); + } + $('#user-search').on('keyup', utils.debounce(doSearch, 250)); + $('#user-search-by').on('change', doSearch); + } + + function loadSearchPage(query) { + const params = utils.params(); + params.searchBy = query.searchBy; + params.query = query.query; + params.page = query.page; + params.sortBy = params.sortBy || 'lastonline'; + const qs = decodeURIComponent($.param(params)); + $.get(config.relative_path + '/api/admin/manage/users?' + qs, function (data) { + renderSearchResults(data); + const url = config.relative_path + '/admin/manage/users?' + qs; + if (history.pushState) { + history.pushState({ + url: url, + }, null, window.location.protocol + '//' + window.location.host + url); + } + }).fail(function (xhrErr) { + if (xhrErr && xhrErr.responseJSON && xhrErr.responseJSON.error) { + alerts.error(xhrErr.responseJSON.error); + } + }); + } + + function renderSearchResults(data) { + Benchpress.render('partials/paginator', { pagination: data.pagination }).then(function (html) { + $('.pagination-container').replaceWith(html); + }); + + app.parseAndTranslate('admin/manage/users', 'users', data, function (html) { + $('.users-table tbody tr').remove(); + $('.users-table tbody').append(html); + html.find('.timeago').timeago(); + $('.fa-spinner').addClass('hidden'); + if (!$('#user-search').val()) { + $('#user-found-notify').addClass('hidden'); + $('#user-notfound-notify').addClass('hidden'); + return; + } + if (data && data.users.length === 0) { + $('#user-notfound-notify').translateHtml('[[admin/manage/users:search.not-found]]') + .removeClass('hidden'); + $('#user-found-notify').addClass('hidden'); + } else { + $('#user-found-notify').translateHtml( + translator.compile('admin/manage/users:alerts.x-users-found', data.matchCount, data.timing) + ).removeClass('hidden'); + $('#user-notfound-notify').addClass('hidden'); + } + }); + } + + function buildSearchQuery(params) { + if ($('#user-search').val()) { + params.query = $('#user-search').val(); + params.searchBy = $('#user-search-by').val(); + } else { + delete params.query; + delete params.searchBy; + } + + return decodeURIComponent($.param(params)); + } + + function handleSort() { + $('.users-table thead th').on('click', function () { + const $this = $(this); + const sortBy = $this.attr('data-sort'); + if (!sortBy) { + return; + } + const params = utils.params(); + params.sortBy = sortBy; + if (ajaxify.data.sortBy === sortBy) { + params.sortDirection = ajaxify.data.reverse ? 'asc' : 'desc'; + } else { + params.sortDirection = 'desc'; + } + + const qs = buildSearchQuery(params); + ajaxify.go('admin/manage/users?' + qs); + }); + } + + function getFilters() { + const filters = []; + $('#filter-by').find('[data-filter-by]').each(function () { + if ($(this).find('.fa-check').length) { + filters.push($(this).attr('data-filter-by')); + } + }); + return filters; + } + + function handleFilter() { + let currentFilters = getFilters(); + $('#filter-by').on('click', 'li', function () { + const $this = $(this); + $this.find('i').toggleClass('fa-check', !$this.find('i').hasClass('fa-check')); + return false; + }); + + $('#filter-by').on('hidden.bs.dropdown', function () { + const filters = getFilters(); + let changed = filters.length !== currentFilters.length; + if (filters.length === currentFilters.length) { + filters.forEach(function (filter, i) { + if (filter !== currentFilters[i]) { + changed = true; + } + }); + } + currentFilters = getFilters(); + if (changed) { + const params = utils.params(); + params.filters = filters; + const qs = buildSearchQuery(params); + ajaxify.go('admin/manage/users?' + qs); + } + }); + } + + return Users; +}); diff --git a/public/src/admin/modules/checkboxRowSelector.js b/public/src/admin/modules/checkboxRowSelector.js new file mode 100644 index 0000000000..af22331dfb --- /dev/null +++ b/public/src/admin/modules/checkboxRowSelector.js @@ -0,0 +1,49 @@ +'use strict'; + +define('admin/modules/checkboxRowSelector', function () { + const self = {}; + let $tableContainer; + + self.toggling = false; + + self.init = function (tableCssSelector) { + $tableContainer = $(tableCssSelector); + $tableContainer.on('change', 'input.checkbox-helper', handleChange); + }; + + self.updateAll = function () { + $tableContainer.find('input.checkbox-helper').each((idx, el) => { + self.updateState($(el)); + }); + }; + + self.updateState = function ($checkboxEl) { + if (self.toggling) { + return; + } + const checkboxes = $checkboxEl.closest('tr').find('input:not([disabled]):visible').toArray(); + const $toggler = $(checkboxes.shift()); + const rowState = checkboxes.length && checkboxes.every(el => el.checked); + $toggler.prop('checked', rowState); + }; + + function handleChange(ev) { + const $checkboxEl = $(ev.target); + toggleAll($checkboxEl); + } + + function toggleAll($checkboxEl) { + self.toggling = true; + const state = $checkboxEl.prop('checked'); + $checkboxEl.closest('tr').find('input:not(.checkbox-helper):visible').each((idx, el) => { + const $checkbox = $(el); + if ($checkbox.prop('checked') === state) { + return; + } + $checkbox.click(); + }); + self.toggling = false; + } + + return self; +}); diff --git a/public/src/admin/modules/colorpicker.js b/public/src/admin/modules/colorpicker.js new file mode 100644 index 0000000000..ff42b0f233 --- /dev/null +++ b/public/src/admin/modules/colorpicker.js @@ -0,0 +1,31 @@ +'use strict'; + +// TODO: no longer used remove in 1.19.0 +define('admin/modules/colorpicker', function () { + const colorpicker = {}; + + colorpicker.enable = function (inputEl, callback) { + (inputEl instanceof jQuery ? inputEl : $(inputEl)).each(function () { + const $this = $(this); + + $this.ColorPicker({ + color: $this.val() || '#000', + onChange: function (hsb, hex) { + $this.val('#' + hex); + if (typeof callback === 'function') { + callback(hsb, hex); + } + }, + onShow: function (colpkr) { + $(colpkr).css('z-index', 1051); + }, + }); + + $(window).one('action:ajaxify.start', function () { + $this.ColorPickerHide(); + }); + }); + }; + + return colorpicker; +}); diff --git a/public/src/admin/modules/dashboard-line-graph.js b/public/src/admin/modules/dashboard-line-graph.js new file mode 100644 index 0000000000..4de21546d1 --- /dev/null +++ b/public/src/admin/modules/dashboard-line-graph.js @@ -0,0 +1,196 @@ +'use strict'; + +define('admin/modules/dashboard-line-graph', ['Chart', 'translator', 'benchpress', 'api', 'hooks', 'bootbox'], function (Chart, translator, Benchpress, api, hooks, bootbox) { + const Graph = { + _current: null, + }; + let isMobile = false; + + Graph.init = ({ set, dataset }) => { + const canvas = document.getElementById('analytics-traffic'); + const canvasCtx = canvas.getContext('2d'); + const trafficLabels = utils.getHoursArray(); + + isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + if (isMobile) { + Chart.defaults.global.tooltips.enabled = false; + } + + Graph.handleUpdateControls({ set }); + + const t = translator.Translator.create(); + return new Promise((resolve) => { + t.translateKey(`admin/menu:${ajaxify.data.template.name.replace('admin/', '')}`, []).then((key) => { + const data = { + labels: trafficLabels, + datasets: [ + { + label: key, + backgroundColor: 'rgba(151,187,205,0.2)', + borderColor: 'rgba(151,187,205,1)', + pointBackgroundColor: 'rgba(151,187,205,1)', + pointHoverBackgroundColor: 'rgba(151,187,205,1)', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(151,187,205,1)', + data: dataset || [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, + ], + }; + + canvas.width = $(canvas).parent().width(); + + data.datasets[0].yAxisID = 'left-y-axis'; + + Graph._current = new Chart(canvasCtx, { + type: 'line', + data: data, + options: { + responsive: true, + legend: { + display: true, + }, + scales: { + yAxes: [{ + id: 'left-y-axis', + ticks: { + beginAtZero: true, + precision: 0, + }, + type: 'linear', + position: 'left', + scaleLabel: { + display: true, + labelString: key, + }, + }], + }, + tooltips: { + mode: 'x', + }, + }, + }); + + if (!dataset) { + Graph.update(set).then(resolve); + } else { + resolve(Graph._current); + } + }); + }); + }; + + Graph.handleUpdateControls = ({ set }) => { + $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () { + let until = new Date(); + const amount = $(this).attr('data-amount'); + if ($(this).attr('data-units') === 'days') { + until.setHours(0, 0, 0, 0); + } + until = until.getTime(); + Graph.update(set, $(this).attr('data-units'), until, amount); + + require(['translator'], function (translator) { + translator.translate('[[admin/dashboard:page-views-custom]]', function (translated) { + $('[data-action="updateGraph"][data-units="custom"]').text(translated); + }); + }); + }); + + $('[data-action="updateGraph"][data-units="custom"]').on('click', function () { + const targetEl = $(this); + + Benchpress.render('admin/partials/pageviews-range-select', {}).then(function (html) { + const modal = bootbox.dialog({ + title: '[[admin/dashboard:page-views-custom]]', + message: html, + buttons: { + submit: { + label: '[[global:search]]', + className: 'btn-primary', + callback: submit, + }, + }, + }).on('shown.bs.modal', function () { + const date = new Date(); + const today = date.toISOString().slice(0, 10); + date.setDate(date.getDate() - 1); + const yesterday = date.toISOString().slice(0, 10); + + modal.find('#startRange').val(targetEl.attr('data-startRange') || yesterday); + modal.find('#endRange').val(targetEl.attr('data-endRange') || today); + }); + + function submit() { + // NEED TO ADD VALIDATION HERE FOR YYYY-MM-DD + const formData = modal.find('form').serializeObject(); + const validRegexp = /\d{4}-\d{2}-\d{2}/; + + // Input validation + if (!formData.startRange && !formData.endRange) { + // No range? Assume last 30 days + Graph.update(set, 'days'); + return; + } else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) { + // Invalid Input + modal.find('.alert-danger').removeClass('hidden'); + return false; + } + + let until = new Date(formData.endRange); + until.setDate(until.getDate() + 1); + until = until.getTime(); + const amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24); + + Graph.update(set, 'days', until, amount); + + // Update "custom range" label + targetEl.attr('data-startRange', formData.startRange); + targetEl.attr('data-endRange', formData.endRange); + targetEl.html(formData.startRange + ' – ' + formData.endRange); + } + }); + }); + }; + + Graph.update = ( + set, + units = ajaxify.data.query.units || 'hours', + until = ajaxify.data.query.until, + amount = ajaxify.data.query.count + ) => { + if (!Graph._current) { + return Promise.reject(new Error('[[error:invalid-data]]')); + } + + return new Promise((resolve) => { + api.get(`/admin/analytics/${set}`, { units, until, amount }).then((dataset) => { + if (units === 'days') { + Graph._current.data.xLabels = utils.getDaysArray(until, amount); + } else { + Graph._current.data.xLabels = utils.getHoursArray(); + } + + Graph._current.data.datasets[0].data = dataset; + Graph._current.data.labels = Graph._current.data.xLabels; + Graph._current.update(); + + // Update address bar and "View as JSON" button url + const apiEl = $('#view-as-json'); + const newHref = $.param({ + units: units || 'hours', + until: until, + count: amount, + }); + apiEl.attr('href', `${config.relative_path}/api/v3/admin/analytics/${ajaxify.data.set}?${newHref}`); + const url = ajaxify.removeRelativePath(ajaxify.data.url.slice(1)); + ajaxify.updateHistory(`${url}?${newHref}`, true); + hooks.fire('action:admin.dashboard.updateGraph', { + graph: Graph._current, + }); + resolve(Graph._current); + }); + }); + }; + + return Graph; +}); diff --git a/public/src/admin/modules/instance.js b/public/src/admin/modules/instance.js new file mode 100644 index 0000000000..af5eaba016 --- /dev/null +++ b/public/src/admin/modules/instance.js @@ -0,0 +1,66 @@ +'use strict'; + +define('admin/modules/instance', [ + 'alerts', +], function (alerts) { + const instance = {}; + + instance.rebuildAndRestart = function (callback) { + alerts.alert({ + alert_id: 'instance_rebuild_and_restart', + type: 'info', + title: 'Rebuilding... ', + message: 'NodeBB is rebuilding front-end assets (css, javascript, etc).', + }); + + $(window).one('action:reconnected', function () { + alerts.alert({ + alert_id: 'instance_rebuild_and_restart', + type: 'success', + title: ' Success', + message: 'NodeBB has rebuilt and restarted successfully.', + timeout: 5000, + }); + + if (typeof callback === 'function') { + callback(); + } + }); + + socket.emit('admin.reload', function () { + alerts.alert({ + alert_id: 'instance_rebuild_and_restart', + type: 'info', + title: 'Build Complete!... ', + message: 'NodeBB is restarting.', + }); + }); + }; + + instance.restart = function (callback) { + alerts.alert({ + alert_id: 'instance_restart', + type: 'info', + title: 'Restarting... ', + message: 'NodeBB is restarting.', + }); + + $(window).one('action:reconnected', function () { + alerts.alert({ + alert_id: 'instance_restart', + type: 'success', + title: ' Success', + message: 'NodeBB has restarted successfully.', + timeout: 5000, + }); + + if (typeof callback === 'function') { + callback(); + } + }); + + socket.emit('admin.restart'); + }; + + return instance; +}); diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js new file mode 100644 index 0000000000..8588ee8e4f --- /dev/null +++ b/public/src/admin/modules/search.js @@ -0,0 +1,164 @@ +'use strict'; + +define('admin/modules/search', ['mousetrap', 'alerts'], function (mousetrap, alerts) { + const search = {}; + + function find(dict, term) { + const html = dict.filter(function (elem) { + return elem.translations.toLowerCase().includes(term); + }).map(function (params) { + const namespace = params.namespace; + const translations = params.translations; + let title = params.title; + const escaped = utils.escapeRegexChars(term); + + const results = translations + // remove all lines without a match + .replace(new RegExp('^(?:(?!' + escaped + ').)*$', 'gmi'), '') + // remove lines that only match the title + .replace(new RegExp('(^|\\n).*?' + title + '.*?(\\n|$)', 'g'), '') + // get up to 25 characters of context on both sides of the match + // and wrap the match in a `.search-match` element + .replace( + new RegExp('^[\\s\\S]*?(.{0,25})(' + escaped + ')(.{0,25})[\\s\\S]*?$', 'gmi'), + '...$1$2$3...
    ' + ) + // collapse whitespace + .replace(/(?:\n ?)+/g, '\n') + .trim(); + + title = title.replace( + new RegExp('(^.*?)(' + escaped + ')(.*?$)', 'gi'), + '$1$2$3' + ); + + return ''; + }).join(''); + return html; + } + + search.init = function () { + if (!app.user.privileges['admin:settings']) { + return; + } + + socket.emit('admin.getSearchDict', {}, function (err, dict) { + if (err) { + alerts.error(err); + throw err; + } + setupACPSearch(dict); + }); + }; + + function setupACPSearch(dict) { + const dropdown = $('#acp-search .dropdown'); + const menu = $('#acp-search .dropdown-menu'); + const input = $('#acp-search input'); + const placeholderText = dropdown.attr('data-text'); + + if (!config.searchEnabled) { + menu.addClass('search-disabled'); + } + + input.on('keyup', function () { + dropdown.addClass('open'); + }); + + $('#acp-search').parents('form').on('submit', function (ev) { + const query = input.val(); + const selected = menu.get(0).querySelector('li.result > a.focus') || menu.get(0).querySelector('li.result > a'); + const href = selected ? selected.getAttribute('href') : config.relative_path + '/search?in=titlesposts&term=' + escape(query); + + ajaxify.go(href.replace(/^\//, '')); + + setTimeout(function () { + dropdown.removeClass('open'); + input.blur(); + dropdown.attr('data-text', query || placeholderText); + }, 150); + + ev.preventDefault(); + return false; + }); + + mousetrap.bind('/', function (ev) { + input.select(); + ev.preventDefault(); + }); + + mousetrap(input[0]).bind(['up', 'down'], function (ev, key) { + let next; + if (key === 'up') { + next = menu.find('li.result > a.focus').removeClass('focus').parent().prev('.result') + .children(); + if (!next.length) { + next = menu.find('li.result > a').last(); + } + next.addClass('focus'); + if (menu[0].getBoundingClientRect().top > next[0].getBoundingClientRect().top) { + next[0].scrollIntoView(true); + } + } else if (key === 'down') { + next = menu.find('li.result > a.focus').removeClass('focus').parent().next('.result') + .children(); + if (!next.length) { + next = menu.find('li.result > a').first(); + } + next.addClass('focus'); + if (menu[0].getBoundingClientRect().bottom < next[0].getBoundingClientRect().bottom) { + next[0].scrollIntoView(false); + } + } + + ev.preventDefault(); + }); + + let prevValue; + + input.on('keyup focus', function () { + const value = input.val().toLowerCase(); + + if (value === prevValue) { + return; + } + prevValue = value; + + menu.children('.result').remove(); + + const len = /\W/.test(value) ? 3 : value.length; + let results; + + menu.toggleClass('state-start-typing', len === 0); + menu.toggleClass('state-keep-typing', len > 0 && len < 3); + + if (len >= 3) { + menu.prepend(find(dict, value)); + + results = menu.children('.result').length; + + menu.toggleClass('state-no-results', !results); + menu.toggleClass('state-yes-results', !!results); + + menu.find('.search-forum') + .not('.divider') + .find('a') + .attr('href', config.relative_path + '/search?in=titlesposts&term=' + escape(value)) + .find('strong') + .text(value); + } else { + menu.removeClass('state-no-results state-yes-results'); + } + }); + } + + return search; +}); diff --git a/public/src/admin/modules/selectable.js b/public/src/admin/modules/selectable.js new file mode 100644 index 0000000000..064633beeb --- /dev/null +++ b/public/src/admin/modules/selectable.js @@ -0,0 +1,16 @@ +'use strict'; + + +define('admin/modules/selectable', [ + 'jquery-ui/widgets/selectable', +], function () { + const selectable = {}; + + selectable.enable = function (containerEl, targets) { + $(containerEl).selectable({ + filter: targets, + }); + }; + + return selectable; +}); diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js new file mode 100644 index 0000000000..531f141d25 --- /dev/null +++ b/public/src/admin/settings.js @@ -0,0 +1,200 @@ +'use strict'; + + +define('admin/settings', ['uploader', 'mousetrap', 'hooks', 'alerts', 'settings'], function (uploader, mousetrap, hooks, alerts, settings) { + const Settings = {}; + + Settings.populateTOC = function () { + const headers = $('.settings-header'); + + if (headers.length > 1) { + headers.each(function () { + const header = $(this).text(); + const anchor = header.toLowerCase().replace(/ /g, '-').trim(); + + $(this).prepend(''); + $('.section-content ul').append('
  • ' + header + '
  • '); + }); + + const scrollTo = $('a[name="' + window.location.hash.replace('#', '') + '"]'); + if (scrollTo.length) { + $('html, body').animate({ + scrollTop: (scrollTo.offset().top) + 'px', + }, 400); + } + } else { + $('.content-header').parents('.row').remove(); + } + }; + + Settings.prepare = function (callback) { + // Populate the fields on the page from the config + const fields = $('#content [data-field]'); + const numFields = fields.length; + const saveBtn = $('#save'); + const revertBtn = $('#revert'); + let x; + let key; + let inputType; + let field; + + // Handle unsaved changes + fields.on('change', function () { + app.flags = app.flags || {}; + app.flags._unsaved = true; + }); + const defaultInputs = ['text', 'hidden', 'password', 'textarea', 'number']; + for (x = 0; x < numFields; x += 1) { + field = fields.eq(x); + key = field.attr('data-field'); + inputType = field.attr('type'); + if (app.config.hasOwnProperty(key)) { + if (field.is('input') && inputType === 'checkbox') { + const checked = parseInt(app.config[key], 10) === 1; + field.prop('checked', checked); + field.parents('.mdl-switch').toggleClass('is-checked', checked); + } else if (field.is('textarea') || field.is('select') || (field.is('input') && defaultInputs.indexOf(inputType) !== -1)) { + field.val(app.config[key]); + } + } + } + + revertBtn.off('click').on('click', function () { + ajaxify.refresh(); + }); + + saveBtn.off('click').on('click', function (e) { + e.preventDefault(); + + const ok = settings.check(document.querySelectorAll('#content [data-field]')); + if (!ok) { + return; + } + + saveFields(fields, function onFieldsSaved(err) { + if (err) { + return alerts.alert({ + alert_id: 'config_status', + timeout: 2500, + title: '[[admin/admin:changes-not-saved]]', + message: `[[admin/admin:changes-not-saved-message, ${err.message}]]`, + type: 'danger', + }); + } + + app.flags._unsaved = false; + + alerts.alert({ + alert_id: 'config_status', + timeout: 2500, + title: '[[admin/admin:changes-saved]]', + message: '[[admin/admin:changes-saved-message]]', + type: 'success', + }); + + hooks.fire('action:admin.settingsSaved'); + }); + }); + + mousetrap.bind('ctrl+s', function (ev) { + saveBtn.click(); + ev.preventDefault(); + }); + + handleUploads(); + setupTagsInput(); + + $('#clear-sitemap-cache').off('click').on('click', function () { + socket.emit('admin.settings.clearSitemapCache', function () { + alerts.success('Sitemap Cache Cleared!'); + }); + return false; + }); + + if (typeof callback === 'function') { + callback(); + } + + setTimeout(function () { + hooks.fire('action:admin.settingsLoaded'); + }, 0); + }; + + function handleUploads() { + $('#content input[data-action="upload"]').each(function () { + const uploadBtn = $(this); + uploadBtn.on('click', function () { + uploader.show({ + title: uploadBtn.attr('data-title'), + description: uploadBtn.attr('data-description'), + route: uploadBtn.attr('data-route'), + params: {}, + showHelp: uploadBtn.attr('data-help') ? uploadBtn.attr('data-help') === 1 : undefined, + accept: uploadBtn.attr('data-accept'), + }, function (image) { + $('#' + uploadBtn.attr('data-target')).val(image); + }); + }); + }); + } + + function setupTagsInput() { + $('[data-field-type="tagsinput"]').tagsinput({ + confirmKeys: [13, 44], + trimValue: true, + }); + app.flags._unsaved = false; + } + + Settings.remove = function (key) { + socket.emit('admin.config.remove', key); + }; + + function saveFields(fields, callback) { + const data = {}; + + fields.each(function () { + const field = $(this); + const key = field.attr('data-field'); + let value; + let inputType; + + if (field.is('input')) { + inputType = field.attr('type'); + switch (inputType) { + case 'text': + case 'password': + case 'hidden': + case 'textarea': + case 'number': + value = field.val(); + break; + + case 'checkbox': + value = field.prop('checked') ? '1' : '0'; + break; + } + } else if (field.is('textarea') || field.is('select')) { + value = field.val(); + } + + data[key] = value; + }); + + socket.emit('admin.config.setMultiple', data, function (err) { + if (err) { + return callback(err); + } + + for (const field in data) { + if (data.hasOwnProperty(field)) { + app.config[field] = data[field]; + } + } + + callback(); + }); + } + + return Settings; +}); diff --git a/public/src/admin/settings/api.js b/public/src/admin/settings/api.js new file mode 100644 index 0000000000..b7aa81603a --- /dev/null +++ b/public/src/admin/settings/api.js @@ -0,0 +1,34 @@ +'use strict'; + +define('admin/settings/api', ['settings', 'alerts', 'hooks'], function (settings, alerts, hooks) { + const ACP = {}; + + ACP.init = function () { + settings.load('core.api', $('.core-api-settings')); + $('#save').on('click', saveSettings); + + hooks.on('action:settings.sorted-list.itemLoaded', ({ element }) => { + element.addEventListener('click', (ev) => { + if (ev.target.closest('input[readonly]')) { + // Select entire input text + ev.target.selectionStart = 0; + ev.target.selectionEnd = ev.target.value.length; + } + }); + }); + }; + + function saveSettings() { + settings.save('core.api', $('.core-api-settings'), function () { + alerts.alert({ + type: 'success', + alert_id: 'core.api-saved', + title: 'Settings Saved', + timeout: 5000, + }); + ajaxify.refresh(); + }); + } + + return ACP; +}); diff --git a/public/src/admin/settings/cookies.js b/public/src/admin/settings/cookies.js new file mode 100644 index 0000000000..7e22dfde00 --- /dev/null +++ b/public/src/admin/settings/cookies.js @@ -0,0 +1,19 @@ +'use strict'; + +define('admin/settings/cookies', ['alerts'], function (alerts) { + const Module = {}; + + Module.init = function () { + $('#delete-all-sessions').on('click', function () { + socket.emit('admin.deleteAllSessions', function (err) { + if (err) { + return alerts.error(err); + } + window.location.href = config.relative_path + '/login'; + }); + return false; + }); + }; + + return Module; +}); diff --git a/public/src/admin/settings/email.js b/public/src/admin/settings/email.js new file mode 100644 index 0000000000..b928e23fd6 --- /dev/null +++ b/public/src/admin/settings/email.js @@ -0,0 +1,126 @@ +'use strict'; + + +define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function (ace, alerts) { + const module = {}; + let emailEditor; + + module.init = function () { + configureEmailTester(); + configureEmailEditor(); + handleDigestHourChange(); + handleSmtpServiceChange(); + + $(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange); + $(window).on('action:admin.settingsSaved', function () { + socket.emit('admin.user.restartJobs'); + }); + $('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange); + }; + + function configureEmailTester() { + $('button[data-action="email.test"]').off('click').on('click', function () { + socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) { + if (err) { + console.error(err.message); + return alerts.error(err); + } + alerts.success('Test Email Sent'); + }); + return false; + }); + } + + function configureEmailEditor() { + $('#email-editor-selector').on('change', updateEmailEditor); + + emailEditor = ace.edit('email-editor'); + emailEditor.$blockScrolling = Infinity; + emailEditor.setTheme('ace/theme/twilight'); + emailEditor.getSession().setMode('ace/mode/html'); + + emailEditor.on('change', function () { + const emailPath = $('#email-editor-selector').val(); + let original; + ajaxify.data.emails.forEach(function (email) { + if (email.path === emailPath) { + original = email.original; + } + }); + const newEmail = emailEditor.getValue(); + $('#email-editor-holder').val(newEmail !== original ? newEmail : ''); + }); + + $('button[data-action="email.revert"]').off('click').on('click', function () { + ajaxify.data.emails.forEach(function (email) { + if (email.path === $('#email-editor-selector').val()) { + emailEditor.getSession().setValue(email.original); + $('#email-editor-holder').val(''); + } + }); + }); + + updateEmailEditor(); + } + + function updateEmailEditor() { + ajaxify.data.emails.forEach(function (email) { + if (email.path === $('#email-editor-selector').val()) { + emailEditor.getSession().setValue(email.text); + $('#email-editor-holder') + .val(email.text !== email.original ? email.text : '') + .attr('data-field', 'email:custom:' + email.path); + } + }); + } + + function handleDigestHourChange() { + let hour = parseInt($('#digestHour').val(), 10); + + if (isNaN(hour)) { + hour = 17; + } else if (hour > 23 || hour < 0) { + hour = 0; + } + + socket.emit('admin.getServerTime', {}, function (err, now) { + if (err) { + return alerts.error(err); + } + + const date = new Date(now.timestamp); + const offset = (new Date().getTimezoneOffset() - now.offset) / 60; + date.setHours(date.getHours() + offset); + + $('#serverTime').text(date.toLocaleTimeString()); + + date.setHours(parseInt(hour, 10) - offset, 0, 0, 0); + + // If adjusted time is in the past, move to next day + if (date.getTime() < Date.now()) { + date.setDate(date.getDate() + 1); + } + + $('#nextDigestTime').text(date.toLocaleString()); + }); + } + + function handleSmtpServiceChange() { + const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp'; + $('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom); + + const enabledEl = document.getElementById('email:smtpTransport:enabled'); + if (enabledEl) { + if (!enabledEl.checked) { + enabledEl.closest('label').classList.toggle('is-checked', true); + enabledEl.checked = true; + alerts.alert({ + message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]', + timeout: 5000, + }); + } + } + } + + return module; +}); diff --git a/public/src/admin/settings/general.js b/public/src/admin/settings/general.js new file mode 100644 index 0000000000..1c6aff4d4a --- /dev/null +++ b/public/src/admin/settings/general.js @@ -0,0 +1,26 @@ +'use strict'; + + +define('admin/settings/general', ['admin/settings'], function () { + const Module = {}; + + Module.init = function () { + $('button[data-action="removeLogo"]').on('click', function () { + $('input[data-field="brand:logo"]').val(''); + }); + $('button[data-action="removeFavicon"]').on('click', function () { + $('input[data-field="brand:favicon"]').val(''); + }); + $('button[data-action="removeTouchIcon"]').on('click', function () { + $('input[data-field="brand:touchIcon"]').val(''); + }); + $('button[data-action="removeMaskableIcon"]').on('click', function () { + $('input[data-field="brand:maskableIcon"]').val(''); + }); + $('button[data-action="removeOgImage"]').on('click', function () { + $('input[data-field="removeOgImage"]').val(''); + }); + }; + + return Module; +}); diff --git a/public/src/admin/settings/homepage.js b/public/src/admin/settings/homepage.js new file mode 100644 index 0000000000..acd41044eb --- /dev/null +++ b/public/src/admin/settings/homepage.js @@ -0,0 +1,22 @@ +'use strict'; + + +define('admin/settings/homepage', ['admin/settings'], function () { + function toggleCustomRoute() { + if ($('[data-field="homePageRoute"]').val() === 'custom') { + $('#homePageCustom').show(); + } else { + $('#homePageCustom').hide(); + } + } + + const Homepage = {}; + + Homepage.init = function () { + $('[data-field="homePageRoute"]').on('change', toggleCustomRoute); + + toggleCustomRoute(); + }; + + return Homepage; +}); diff --git a/public/src/admin/settings/navigation.js b/public/src/admin/settings/navigation.js new file mode 100644 index 0000000000..ad0f16aca8 --- /dev/null +++ b/public/src/admin/settings/navigation.js @@ -0,0 +1,157 @@ +'use strict'; + + +define('admin/settings/navigation', [ + 'translator', + 'iconSelect', + 'benchpress', + 'alerts', + 'jquery-ui/widgets/draggable', + 'jquery-ui/widgets/droppable', + 'jquery-ui/widgets/sortable', +], function (translator, iconSelect, Benchpress, alerts) { + const navigation = {}; + let available; + + navigation.init = function () { + available = ajaxify.data.available; + + $('#available').find('li .drag-item').draggable({ + connectToSortable: '#active-navigation', + helper: 'clone', + distance: 10, + stop: drop, + }); + + $('#active-navigation').sortable().droppable({ + accept: $('#available li .drag-item'), + }); + + $('#enabled').on('click', '.iconPicker', function () { + const iconEl = $(this).find('i'); + iconSelect.init(iconEl, function (el) { + const newIconClass = el.attr('value'); + const index = iconEl.parents('[data-index]').attr('data-index'); + $('#active-navigation [data-index="' + index + '"] i.nav-icon').attr('class', 'fa fa-fw ' + newIconClass); + iconEl.siblings('[name="iconClass"]').val(newIconClass); + iconEl.siblings('.change-icon-link').toggleClass('hidden', !!newIconClass); + }); + }); + + $('#enabled').on('click', '[name="dropdown"]', function () { + const el = $(this); + const index = el.parents('[data-index]').attr('data-index'); + $('#active-navigation [data-index="' + index + '"] i.dropdown-icon').toggleClass('hidden', !el.is(':checked')); + }); + + $('#active-navigation').on('click', 'li', onSelect); + + $('#enabled') + .on('click', '.delete', remove) + .on('click', '.toggle', toggle); + + $('#save').on('click', save); + }; + + function onSelect() { + const clickedIndex = $(this).attr('data-index'); + $('#active-navigation li').removeClass('active'); + $(this).addClass('active'); + + const detailsForm = $('#enabled').children('[data-index="' + clickedIndex + '"]'); + $('#enabled li').addClass('hidden'); + + if (detailsForm.length) { + detailsForm.removeClass('hidden'); + } + return false; + } + + function drop(ev, ui) { + const id = ui.helper.attr('data-id'); + const el = $('#active-navigation [data-id="' + id + '"]'); + const data = id === 'custom' ? { + iconClass: 'fa-navicon', + groups: available[0].groups, + enabled: true, + } : available[id]; + + data.index = (parseInt($('#enabled').children().last().attr('data-index'), 10) || 0) + 1; + data.title = translator.escape(data.title); + data.text = translator.escape(data.text); + data.groups = ajaxify.data.groups; + Benchpress.parse('admin/settings/navigation', 'navigation', { navigation: [data] }, function (li) { + translator.translate(li, function (li) { + li = $(translator.unescape(li)); + el.after(li); + el.remove(); + }); + }); + Benchpress.parse('admin/settings/navigation', 'enabled', { enabled: [data] }, function (li) { + translator.translate(li, function (li) { + li = $(translator.unescape(li)); + $('#enabled').append(li); + componentHandler.upgradeDom(); + }); + }); + } + + function save() { + const nav = []; + + const indices = []; + $('#active-navigation li').each(function () { + indices.push($(this).attr('data-index')); + }); + + indices.forEach(function (index) { + const el = $('#enabled').children('[data-index="' + index + '"]'); + const form = el.find('form').serializeArray(); + const data = {}; + + form.forEach(function (input) { + if (data[input.name]) { + if (!Array.isArray(data[input.name])) { + data[input.name] = [ + data[input.name], + ]; + } + data[input.name].push(input.value); + } else { + data[input.name] = input.value; + } + }); + + nav.push(data); + }); + + socket.emit('admin.navigation.save', nav, function (err) { + if (err) { + alerts.error(err); + } else { + alerts.success('Successfully saved navigation'); + } + }); + } + + function remove() { + const index = $(this).parents('[data-index]').attr('data-index'); + $('#active-navigation [data-index="' + index + '"]').remove(); + $('#enabled [data-index="' + index + '"]').remove(); + return false; + } + + function toggle() { + const btn = $(this); + const disabled = btn.hasClass('btn-success'); + const index = btn.parents('[data-index]').attr('data-index'); + translator.translate(disabled ? '[[admin/settings/navigation:btn.disable]]' : '[[admin/settings/navigation:btn.enable]]', function (html) { + btn.toggleClass('btn-warning').toggleClass('btn-success').html(html); + btn.parents('li').find('[name="enabled"]').val(disabled ? 'on' : ''); + $('#active-navigation [data-index="' + index + '"] a').toggleClass('text-muted', !disabled); + }); + return false; + } + + return navigation; +}); diff --git a/public/src/admin/settings/notifications.js b/public/src/admin/settings/notifications.js new file mode 100644 index 0000000000..424fbc31db --- /dev/null +++ b/public/src/admin/settings/notifications.js @@ -0,0 +1,18 @@ +'use strict'; + +define('admin/settings/notifications', [ + 'autocomplete', +], function (autocomplete) { + const Notifications = {}; + + Notifications.init = function () { + const searchInput = $('[data-field="welcomeUid"]'); + autocomplete.user(searchInput, function (event, selected) { + setTimeout(function () { + searchInput.val(selected.item.user.uid); + }); + }); + }; + + return Notifications; +}); diff --git a/public/src/admin/settings/social.js b/public/src/admin/settings/social.js new file mode 100644 index 0000000000..46e44369a7 --- /dev/null +++ b/public/src/admin/settings/social.js @@ -0,0 +1,27 @@ +'use strict'; + + +define('admin/settings/social', ['alerts'], function (alerts) { + const social = {}; + + social.init = function () { + $('#save').on('click', function () { + const networks = []; + $('#postSharingNetworks input[type="checkbox"]').each(function () { + if ($(this).prop('checked')) { + networks.push($(this).attr('id')); + } + }); + + socket.emit('admin.social.savePostSharingNetworks', networks, function (err) { + if (err) { + return alerts.error(err); + } + + alerts.success('[[admin/settings/social:save-success]]'); + }); + }); + }; + + return social; +}); diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js new file mode 100644 index 0000000000..a6266902eb --- /dev/null +++ b/public/src/ajaxify.js @@ -0,0 +1,594 @@ +'use strict'; + +const hooks = require('./modules/hooks'); +const { render } = require('./widgets'); + +window.ajaxify = window.ajaxify || {}; +ajaxify.widgets = { render: render }; +(function () { + let apiXHR = null; + let ajaxifyTimer; + + let retry = true; + let previousBodyClass = ''; + + ajaxify.count = 0; + ajaxify.currentPage = null; + + ajaxify.go = function (url, callback, quiet) { + // Automatically reconnect to socket and re-ajaxify on success + if (!socket.connected) { + app.reconnect(); + + if (ajaxify.reconnectAction) { + $(window).off('action:reconnected', ajaxify.reconnectAction); + } + ajaxify.reconnectAction = function (e) { + ajaxify.go(url, callback, quiet); + $(window).off(e); + }; + $(window).on('action:reconnected', ajaxify.reconnectAction); + } + + // Abort subsequent requests if clicked multiple times within a short window of time + if (ajaxifyTimer && (Date.now() - ajaxifyTimer) < 500) { + return true; + } + ajaxifyTimer = Date.now(); + + if (ajaxify.handleRedirects(url)) { + return true; + } + + if (!quiet && url === ajaxify.currentPage + window.location.search + window.location.hash) { + quiet = true; + } + + ajaxify.cleanup(url, ajaxify.data.template.name); + + if ($('#content').hasClass('ajaxifying') && apiXHR) { + apiXHR.abort(); + } + + app.previousUrl = !['reset'].includes(ajaxify.currentPage) ? + window.location.pathname.slice(config.relative_path.length) + window.location.search : + app.previousUrl; + + url = ajaxify.start(url); + + // If any listeners alter url and set it to an empty string, abort the ajaxification + if (url === null) { + hooks.fire('action:ajaxify.end', { url: url, tpl_url: ajaxify.data.template.name, title: ajaxify.data.title }); + return false; + } + + previousBodyClass = ajaxify.data.bodyClass; + $('#footer, #content').removeClass('hide').addClass('ajaxifying'); + + ajaxify.loadData(url, function (err, data) { + if (!err || ( + err && + err.data && + (parseInt(err.data.status, 10) !== 302 && parseInt(err.data.status, 10) !== 308) + )) { + ajaxify.updateHistory(url, quiet); + } + + if (err) { + return onAjaxError(err, url, callback, quiet); + } + + retry = true; + + renderTemplate(url, data.templateToRender || data.template.name, data, callback); + }); + + return true; + }; + + // this function is called just once from footer on page load + ajaxify.coldLoad = function () { + const url = ajaxify.start(window.location.pathname.slice(1) + window.location.search + window.location.hash); + ajaxify.updateHistory(url, true); + ajaxify.end(url, ajaxify.data.template.name); + hooks.fire('action:ajaxify.coldLoad'); + }; + + ajaxify.isCold = function () { + return ajaxify.count <= 1; + }; + + ajaxify.handleRedirects = function (url) { + url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, '')).toLowerCase(); + const isClientToAdmin = url.startsWith('admin') && window.location.pathname.indexOf(config.relative_path + '/admin') !== 0; + const isAdminToClient = !url.startsWith('admin') && window.location.pathname.indexOf(config.relative_path + '/admin') === 0; + + if (isClientToAdmin || isAdminToClient) { + window.open(config.relative_path + '/' + url, '_top'); + return true; + } + return false; + }; + + ajaxify.start = function (url) { + url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, '')); + + const payload = { + url: url, + }; + + hooks.logs.collect(); + hooks.fire('action:ajaxify.start', payload); + + ajaxify.count += 1; + + return payload.url; + }; + + ajaxify.updateHistory = function (url, quiet) { + ajaxify.currentPage = url.split(/[?#]/)[0]; + if (window.history && window.history.pushState) { + window.history[!quiet ? 'pushState' : 'replaceState']({ + url: url, + }, url, config.relative_path + '/' + url); + } + }; + + function onAjaxError(err, url, callback, quiet) { + const data = err.data; + const textStatus = err.textStatus; + + if (data) { + let status = parseInt(data.status, 10); + if ([400, 403, 404, 500, 502, 504].includes(status)) { + if (status === 502 && retry) { + retry = false; + ajaxifyTimer = undefined; + return ajaxify.go(url, callback, quiet); + } + if (status === 502) { + status = 500; + } + if (data.responseJSON) { + data.responseJSON.config = config; + } + + $('#footer, #content').removeClass('hide').addClass('ajaxifying'); + return renderTemplate(url, status.toString(), data.responseJSON || {}, callback); + } else if (status === 401) { + require(['alerts'], function (alerts) { + alerts.error('[[global:please_log_in]]'); + }); + app.previousUrl = url; + window.location.href = config.relative_path + '/login'; + } else if (status === 302 || status === 308) { + if (data.responseJSON && data.responseJSON.external) { + // this is used by sso plugins to redirect to the auth route + // cant use ajaxify.go for /auth/sso routes + window.location.href = data.responseJSON.external; + } else if (typeof data.responseJSON === 'string') { + ajaxifyTimer = undefined; + if (data.responseJSON.startsWith('http://') || data.responseJSON.startsWith('https://')) { + window.location.href = data.responseJSON; + } else { + ajaxify.go(data.responseJSON.slice(1), callback, quiet); + } + } + } + } else if (textStatus !== 'abort') { + require(['alerts'], function (alerts) { + alerts.error(data.responseJSON.error); + }); + } + } + + function renderTemplate(url, tpl_url, data, callback) { + hooks.fire('action:ajaxify.loadingTemplates', {}); + require(['translator', 'benchpress'], function (translator, Benchpress) { + Benchpress.render(tpl_url, data) + .then(rendered => translator.translate(rendered)) + .then(function (translated) { + translated = translator.unescape(translated); + $('body').removeClass(previousBodyClass).addClass(data.bodyClass); + $('#content').html(translated); + + ajaxify.end(url, tpl_url); + + if (typeof callback === 'function') { + callback(); + } + + $('#content, #footer').removeClass('ajaxifying'); + + // Only executed on ajaxify. Otherwise these'd be in ajaxify.end() + updateTitle(data.title); + updateTags(); + }); + }); + } + + function updateTitle(title) { + if (!title) { + return; + } + require(['translator'], function (translator) { + title = config.titleLayout.replace(/{/g, '{').replace(/}/g, '}') + .replace('{pageTitle}', function () { return title; }) + .replace('{browserTitle}', function () { return config.browserTitle; }); + + // Allow translation strings in title on ajaxify (#5927) + title = translator.unescape(title); + const data = { title: title }; + hooks.fire('action:ajaxify.updateTitle', data); + translator.translate(data.title, function (translated) { + window.document.title = $('
    ').html(translated).text(); + }); + }); + } + + function updateTags() { + const metaWhitelist = ['title', 'description', /og:.+/, /article:.+/, 'robots'].map(function (val) { + return new RegExp(val); + }); + const linkWhitelist = ['canonical', 'alternate', 'up']; + + // Delete the old meta tags + Array.prototype.slice + .call(document.querySelectorAll('head meta')) + .filter(function (el) { + const name = el.getAttribute('property') || el.getAttribute('name'); + return metaWhitelist.some(function (exp) { + return !!exp.test(name); + }); + }) + .forEach(function (el) { + document.head.removeChild(el); + }); + require(['translator'], function (translator) { + // Add new meta tags + ajaxify.data._header.tags.meta + .filter(function (tagObj) { + const name = tagObj.name || tagObj.property; + return metaWhitelist.some(function (exp) { + return !!exp.test(name); + }); + }).forEach(async function (tagObj) { + if (tagObj.content) { + tagObj.content = await translator.translate(tagObj.content); + } + const metaEl = document.createElement('meta'); + Object.keys(tagObj).forEach(function (prop) { + metaEl.setAttribute(prop, tagObj[prop]); + }); + document.head.appendChild(metaEl); + }); + }); + + // Delete the old link tags + Array.prototype.slice + .call(document.querySelectorAll('head link')) + .filter(function (el) { + const name = el.getAttribute('rel'); + return linkWhitelist.some(function (item) { + return item === name; + }); + }) + .forEach(function (el) { + document.head.removeChild(el); + }); + + // Add new link tags + ajaxify.data._header.tags.link + .filter(function (tagObj) { + return linkWhitelist.some(function (item) { + return item === tagObj.rel; + }); + }) + .forEach(function (tagObj) { + const linkEl = document.createElement('link'); + Object.keys(tagObj).forEach(function (prop) { + linkEl.setAttribute(prop, tagObj[prop]); + }); + document.head.appendChild(linkEl); + }); + } + + ajaxify.end = function (url, tpl_url) { + // Scroll back to top of page + if (!ajaxify.isCold()) { + window.scrollTo(0, 0); + } + ajaxify.loadScript(tpl_url, function done() { + hooks.fire('action:ajaxify.end', { url: url, tpl_url: tpl_url, title: ajaxify.data.title }); + hooks.logs.flush(); + }); + ajaxify.widgets.render(tpl_url); + + hooks.fire('action:ajaxify.contentLoaded', { url: url, tpl: tpl_url }); + + app.processPage(); + }; + + ajaxify.parseData = () => { + const dataEl = document.getElementById('ajaxify-data'); + if (dataEl) { + try { + ajaxify.data = JSON.parse(dataEl.textContent); + } catch (e) { + console.error(e); + ajaxify.data = {}; + } finally { + dataEl.remove(); + } + } + }; + + ajaxify.removeRelativePath = function (url) { + if (url.startsWith(config.relative_path.slice(1))) { + url = url.slice(config.relative_path.length); + } + return url; + }; + + ajaxify.refresh = function (callback) { + ajaxify.go(ajaxify.currentPage + window.location.search + window.location.hash, callback, true); + }; + + ajaxify.loadScript = function (tpl_url, callback) { + let location = !app.inAdmin ? 'forum/' : ''; + + if (tpl_url.startsWith('admin')) { + location = ''; + } + const data = { + tpl_url: tpl_url, + scripts: [location + tpl_url], + }; + + // Hint: useful if you want to load a module on a specific page (append module name to `scripts`) + hooks.fire('action:script.load', data); + hooks.fire('filter:script.load', data).then((data) => { + // Require and parse modules + let outstanding = data.scripts.length; + + const scripts = data.scripts.map(function (script) { + if (typeof script === 'function') { + return function (next) { + script(); + next(); + }; + } + if (typeof script === 'string') { + return async function (next) { + const module = await app.require(script); + // Hint: useful if you want to override a loaded library (e.g. replace core client-side logic), + // or call a method other than .init() + hooks.fire('static:script.init', { tpl_url, name: script, module }).then(() => { + if (module && module.init) { + module.init(); + } + next(); + }); + }; + } + return null; + }).filter(Boolean); + + if (scripts.length) { + scripts.forEach(function (fn) { + fn(function () { + outstanding -= 1; + if (outstanding === 0) { + callback(); + } + }); + }); + } else { + callback(); + } + }); + }; + + ajaxify.loadData = function (url, callback) { + url = ajaxify.removeRelativePath(url); + + hooks.fire('action:ajaxify.loadingData', { url: url }); + + apiXHR = $.ajax({ + url: config.relative_path + '/api/' + url, + cache: false, + headers: { + 'X-Return-To': app.previousUrl, + }, + success: function (data, textStatus, xhr) { + if (!data) { + return; + } + + if (xhr.getResponseHeader('X-Redirect')) { + return callback({ + data: { + status: 302, + responseJSON: data, + }, + textStatus: 'error', + }); + } + + ajaxify.data = data; + data.config = config; + + hooks.fire('action:ajaxify.dataLoaded', { url: url, data: data }); + + callback(null, data); + }, + error: function (data, textStatus) { + if (data.status === 0 && textStatus === 'error') { + data.status = 500; + data.responseJSON = data.responseJSON || {}; + data.responseJSON.error = '[[error:no-connection]]'; + } + callback({ + data: data, + textStatus: textStatus, + }); + }, + }); + }; + + ajaxify.loadTemplate = function (template, callback) { + $.ajax({ + url: `${config.asset_base_url}/templates/${template}.js`, + cache: false, + dataType: 'text', + success: function (script) { + // eslint-disable-next-line no-new-func + const renderFunction = new Function('module', script); + const moduleObj = { exports: {} }; + renderFunction(moduleObj); + callback(moduleObj.exports); + }, + }).fail(function () { + console.error('Unable to load template: ' + template); + callback(new Error('[[error:unable-to-load-template]]')); + }); + }; + + ajaxify.cleanup = (url, tpl_url) => { + app.leaveCurrentRoom(); + $(window).off('scroll'); + hooks.fire('action:ajaxify.cleanup', { url, tpl_url }); + }; + + require(['translator', 'benchpress'], function (translator, Benchpress) { + translator.translate('[[error:no-connection]]'); + translator.translate('[[error:socket-reconnect-failed]]'); + translator.translate(`[[global:reconnecting-message, ${config.siteTitle}]]`); + Benchpress.registerLoader(ajaxify.loadTemplate); + Benchpress.setGlobal('config', config); + Benchpress.render('500', {}); // loads and caches the 500.tpl + }); +}()); + +$(document).ready(function () { + $(window).on('popstate', function (ev) { + ev = ev.originalEvent; + + if (ev !== null && ev.state) { + if (ev.state.url === null && ev.state.returnPath !== undefined) { + window.history.replaceState({ + url: ev.state.returnPath, + }, ev.state.returnPath, config.relative_path + '/' + ev.state.returnPath); + } else if (ev.state.url !== undefined) { + ajaxify.go(ev.state.url, function () { + hooks.fire('action:popstate', { url: ev.state.url }); + }, true); + } + } + }); + + function ajaxifyAnchors() { + function hrefEmpty(href) { + // eslint-disable-next-line no-script-url + return href === undefined || href === '' || href === 'javascript:;'; + } + const location = document.location || window.location; + const rootUrl = location.protocol + '//' + (location.hostname || location.host) + (location.port ? ':' + location.port : ''); + const contentEl = document.getElementById('content'); + + // Enhancing all anchors to ajaxify... + $(document.body).on('click', 'a', function (e) { + const _self = this; + if (this.target !== '' || (this.protocol !== 'http:' && this.protocol !== 'https:')) { + return; + } + + const $this = $(this); + const href = $this.attr('href'); + const internalLink = utils.isInternalURI(this, window.location, config.relative_path); + + const rootAndPath = new RegExp(`^${rootUrl}${config.relative_path}/?`); + const process = function () { + if (!e.ctrlKey && !e.shiftKey && !e.metaKey && e.which === 1) { + if (internalLink) { + const pathname = this.href.replace(rootAndPath, ''); + + // Special handling for urls with hashes + if (window.location.pathname === this.pathname && this.hash.length) { + window.location.hash = this.hash; + } else if (ajaxify.go(pathname)) { + e.preventDefault(); + } + } else if (window.location.pathname !== config.relative_path + '/outgoing') { + if (config.openOutgoingLinksInNewTab && $.contains(contentEl, this)) { + const externalTab = window.open(); + externalTab.opener = null; + externalTab.location = this.href; + e.preventDefault(); + } else if (config.useOutgoingLinksPage) { + const safeUrls = config.outgoingLinksWhitelist.trim().split(/[\s,]+/g).filter(Boolean); + const href = this.href; + if (!safeUrls.length || + !safeUrls.some(function (url) { return href.indexOf(url) !== -1; })) { + ajaxify.go('outgoing?url=' + encodeURIComponent(href)); + e.preventDefault(); + } + } + } + } + }; + + if ($this.attr('data-ajaxify') === 'false') { + if (!internalLink) { + return; + } + return e.preventDefault(); + } + + // Default behaviour for rss feeds + if (internalLink && href && href.endsWith('.rss')) { + return; + } + + // Default behaviour for sitemap + if (internalLink && href && String(_self.pathname).startsWith(config.relative_path + '/sitemap') && href.endsWith('.xml')) { + return; + } + + // Default behaviour for uploads and direct links to API urls + if (internalLink && ['/uploads', '/assets/', '/api/'].some(function (prefix) { + return String(_self.pathname).startsWith(config.relative_path + prefix); + })) { + return; + } + + // eslint-disable-next-line no-script-url + if (hrefEmpty(this.href) || this.protocol === 'javascript:' || href === '#' || href === '') { + return e.preventDefault(); + } + + if (app.flags && app.flags.hasOwnProperty('_unsaved') && app.flags._unsaved === true) { + if (e.ctrlKey) { + return; + } + + require(['bootbox'], function (bootbox) { + bootbox.confirm('[[global:unsaved-changes]]', function (navigate) { + if (navigate) { + app.flags._unsaved = false; + process.call(_self); + } + }); + }); + return e.preventDefault(); + } + + process.call(_self); + }); + } + + if (window.history && window.history.pushState) { + // Progressive Enhancement, ajaxify available only to modern browsers + ajaxifyAnchors(); + } +}); diff --git a/public/src/app.js b/public/src/app.js new file mode 100644 index 0000000000..66076473b5 --- /dev/null +++ b/public/src/app.js @@ -0,0 +1,394 @@ +'use strict'; + +window.$ = require('jquery'); + +window.jQuery = window.$; +require('bootstrap'); +window.bootbox = require('bootbox'); +require('jquery-form'); +window.utils = require('./utils'); +require('timeago'); + +const Benchpress = require('benchpressjs'); +Benchpress.setGlobal('config', config); + +require('./sockets'); +require('./overrides'); +require('./ajaxify'); + +app = window.app || {}; + +Object.defineProperty(app, 'isFocused', { + get() { + return document.visibilityState === 'visible'; + } +}); +app.currentRoom = null; +app.widgets = {}; +app.flags = {}; +app.onDomReady = function () { + $(document).ready(async function () { + if (app.user.timeagoCode && app.user.timeagoCode !== 'en') { + await import(/* webpackChunkName: "timeago/[request]" */ 'timeago/locales/jquery.timeago.' + app.user.timeagoCode); + } + app.load(); + }); +}; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', ajaxify.parseData); +} else { + ajaxify.parseData(); +} + +(function () { + let appLoaded = false; + const isTouchDevice = utils.isTouchDevice(); + + app.cacheBuster = config['cache-buster']; + + app.coldLoad = function () { + if (appLoaded) { + ajaxify.coldLoad(); + } else { + $(window).one('action:app.load', ajaxify.coldLoad); + } + }; + + app.handleEarlyClicks = function () { + /** + * Occasionally, a button or anchor (not meant to be ajaxified) is clicked before + * ajaxify is ready. Capture that event and re-click it once NodeBB is ready. + * + * e.g. New Topic/Reply, post tools + */ + if (document.body) { + let earlyQueue = []; // once we can ES6, use Set instead + const earlyClick = function (ev) { + let btnEl = ev.target.closest('button'); + const anchorEl = ev.target.closest('a'); + if (!btnEl && anchorEl && (anchorEl.getAttribute('data-ajaxify') === 'false' || anchorEl.href === '#')) { + btnEl = anchorEl; + } + if (btnEl && !earlyQueue.includes(btnEl)) { + earlyQueue.push(btnEl); + ev.stopImmediatePropagation(); + ev.preventDefault(); + } + }; + document.body.addEventListener('click', earlyClick); + require(['hooks'], function (hooks) { + hooks.on('action:ajaxify.end', function () { + document.body.removeEventListener('click', earlyClick); + earlyQueue.forEach(function (el) { + el.click(); + }); + earlyQueue = []; + }); + }); + } else { + setTimeout(app.handleEarlyClicks, 50); + } + }; + app.handleEarlyClicks(); + + app.load = function () { + $('body').on('click', '#new_topic', function (e) { + e.preventDefault(); + app.newTopic(); + }); + + registerServiceWorker(); + + require([ + 'taskbar', + 'helpers', + 'forum/pagination', + 'messages', + 'search', + 'forum/header', + 'hooks', + ], function (taskbar, helpers, pagination, messages, search, header, hooks) { + header.prepareDOM(); + taskbar.init(); + helpers.register(); + pagination.init(); + search.init(); + overrides.overrideTimeago(); + hooks.fire('action:app.load'); + messages.show(); + appLoaded = true; + }); + }; + + app.require = async function (modules) { + const single = !Array.isArray(modules); + if (single) { + modules = [modules]; + } + async function requireModule(moduleName) { + let _module; + try { + switch (moduleName) { + case 'bootbox': return require('bootbox'); + case 'benchpressjs': return require('benchpressjs'); + case 'clipboard': return require('clipboard'); + } + if (moduleName.startsWith('admin')) { + _module = await import(/* webpackChunkName: "admin/[request]" */ 'admin/' + moduleName.replace(/^admin\//, '')); + } else if (moduleName.startsWith('forum')) { + _module = await import(/* webpackChunkName: "forum/[request]" */ 'forum/' + moduleName.replace(/^forum\//, '')); + } else { + _module = await import(/* webpackChunkName: "modules/[request]" */ 'modules/' + moduleName); + } + } catch (err) { + console.warn(`error loading ${moduleName}\n${err.stack}`); + } + return _module && _module.default ? _module.default : _module; + } + const result = await Promise.all(modules.map(requireModule)); + return single ? result.pop() : result; + } + + app.logout = function (redirect) { + console.warn('[deprecated] app.logout is deprecated, please use logout module directly'); + require(['logout'], function (logout) { + logout(redirect); + }); + }; + + app.alert = function (params) { + console.warn('[deprecated] app.alert is deprecated, please use alerts.alert'); + require(['alerts'], function (alerts) { + alerts.alert(params); + }); + }; + + app.removeAlert = function (id) { + console.warn('[deprecated] app.removeAlert is deprecated, please use alerts.remove'); + require(['alerts'], function (alerts) { + alerts.remove(id); + }); + }; + + app.alertSuccess = function (message, timeout) { + console.warn('[deprecated] app.alertSuccess is deprecated, please use alerts.success'); + require(['alerts'], function (alerts) { + alerts.success(message, timeout); + }); + }; + + app.alertError = function (message, timeout) { + console.warn('[deprecated] app.alertError is deprecated, please use alerts.error'); + require(['alerts'], function (alerts) { + alerts.error(message, timeout); + }); + }; + + app.enterRoom = function (room, callback) { + callback = callback || function () { }; + if (socket && app.user.uid && app.currentRoom !== room) { + const previousRoom = app.currentRoom; + app.currentRoom = room; + socket.emit('meta.rooms.enter', { + enter: room, + }, function (err) { + if (err) { + app.currentRoom = previousRoom; + require(['alerts'], function (alerts) { + alerts.error(err); + }); + return; + } + + callback(); + }); + } + }; + + app.leaveCurrentRoom = function () { + if (!socket || config.maintenanceMode) { + return; + } + const previousRoom = app.currentRoom; + app.currentRoom = ''; + socket.emit('meta.rooms.leaveCurrent', function (err) { + if (err) { + app.currentRoom = previousRoom; + require(['alerts'], function (alerts) { + alerts.error(err); + }); + } + }); + }; + + function highlightNavigationLink() { + $('#main-nav li') + .removeClass('active') + .find('a') + .filter(function (i, a) { + return $(a).attr('href') !== '#' && window.location.hostname === a.hostname && + ( + window.location.pathname === a.pathname || + window.location.pathname.startsWith(a.pathname + '/') + ); + }) + .parent() + .addClass('active'); + } + + app.createUserTooltips = function (els, placement) { + if (isTouchDevice) { + return; + } + els = els || $('body'); + els.find('.avatar,img[title].teaser-pic,img[title].user-img,div.user-icon,span.user-icon').one('mouseenter', function (ev) { + const $this = $(this); + // perf: create tooltips on demand + $this.tooltip({ + placement: placement || $this.attr('title-placement') || 'top', + title: $this.attr('title'), + container: '#content', + }); + // this will cause the tooltip to show up + $this.trigger(ev); + }); + }; + + app.createStatusTooltips = function () { + if (!isTouchDevice) { + $('body').tooltip({ + selector: '.fa-circle.status', + placement: 'top', + }); + } + }; + + app.processPage = function () { + highlightNavigationLink(); + overrides.overrideTimeagoCutoff(); + $('.timeago').timeago(); + utils.makeNumbersHumanReadable($('.human-readable-number')); + utils.addCommasToNumbers($('.formatted-number')); + app.createUserTooltips($('#content')); + app.createStatusTooltips(); + }; + + app.openChat = function (roomId, uid) { + console.warn('[deprecated] app.openChat is deprecated, please use chat.openChat'); + require(['chat'], function (chat) { + chat.openChat(roomId, uid); + }); + }; + + app.newChat = function (touid, callback) { + console.warn('[deprecated] app.newChat is deprecated, please use chat.newChat'); + require(['chat'], function (chat) { + chat.newChat(touid, callback); + }); + }; + + app.toggleNavbar = function (state) { + require(['components'], (components) => { + const navbarEl = components.get('navbar'); + navbarEl[state ? 'show' : 'hide'](); + }); + }; + + app.enableTopicSearch = function (options) { + console.warn('[deprecated] app.enableTopicSearch is deprecated, please use search.enableQuickSearch(options)'); + require(['search'], function (search) { + search.enableQuickSearch(options); + }); + }; + + app.handleSearch = function (searchOptions) { + console.warn('[deprecated] app.handleSearch is deprecated, please use search.init(options)'); + require(['search'], function (search) { + search.init(searchOptions); + }); + }; + + app.prepareSearch = function () { + console.warn('[deprecated] app.prepareSearch is deprecated, please use search.showAndFocusInput()'); + require(['search'], function (search) { + search.showAndFocusInput(); + }); + }; + + + app.updateUserStatus = function (el, status) { + if (!el.length) { + return; + } + + require(['translator'], function (translator) { + translator.translate('[[global:' + status + ']]', function (translated) { + el.removeClass('online offline dnd away') + .addClass(status) + .attr('title', translated) + .attr('data-original-title', translated); + }); + }); + }; + + app.newTopic = function (cid, tags) { + require(['hooks'], function (hooks) { + hooks.fire('action:composer.topic.new', { + cid: cid || ajaxify.data.cid || 0, + tags: tags || (ajaxify.data.tag ? [ajaxify.data.tag] : []), + }); + }); + }; + + app.loadJQueryUI = function (callback) { + if (typeof $().autocomplete === 'function') { + return callback(); + } + require([ + 'jquery-ui/widgets/datepicker', + 'jquery-ui/widgets/autocomplete', + 'jquery-ui/widgets/sortable', + 'jquery-ui/widgets/resizable', + 'jquery-ui/widgets/draggable', + ], function () { + callback(); + }); + }; + + app.parseAndTranslate = function (template, blockName, data, callback) { + if (typeof blockName !== 'string') { + callback = data; + data = blockName; + blockName = undefined; + } + + return new Promise((resolve, reject) => { + require(['translator', 'benchpress'], function (translator, Benchpress) { + Benchpress.render(template, data, blockName) + .then(rendered => translator.translate(rendered)) + .then(translated => translator.unescape(translated)) + .then(resolve, reject); + }); + }).then((html) => { + html = $(html); + if (callback && typeof callback === 'function') { + setTimeout(callback, 0, html); + } + + return html; + }); + }; + + function registerServiceWorker() { + // Do not register for Safari browsers + if (!config.useragent.isSafari && 'serviceWorker' in navigator) { + navigator.serviceWorker.register(config.relative_path + '/service-worker.js', { scope: config.relative_path + '/' }) + .then(function () { + console.info('ServiceWorker registration succeeded.'); + }).catch(function (err) { + console.info('ServiceWorker registration failed: ', err); + }); + } + } +}()); diff --git a/public/src/client.js b/public/src/client.js new file mode 100644 index 0000000000..67a641e4e4 --- /dev/null +++ b/public/src/client.js @@ -0,0 +1,10 @@ +'use strict'; + +require('./app'); + +// scripts-client.js contains javascript files +// from plugins that add files to "scripts" block in plugin.json +// eslint-disable-next-line import/no-unresolved +require('../scripts-client'); + +app.onDomReady(); diff --git a/public/src/client/account/best.js b/public/src/client/account/best.js new file mode 100644 index 0000000000..3702c33fd6 --- /dev/null +++ b/public/src/client/account/best.js @@ -0,0 +1,16 @@ +'use strict'; + + +define('forum/account/best', ['forum/account/header', 'forum/account/posts'], function (header, posts) { + const Best = {}; + + Best.init = function () { + header.init(); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + + posts.handleInfiniteScroll('account/best'); + }; + + return Best; +}); diff --git a/public/src/client/account/blocks.js b/public/src/client/account/blocks.js new file mode 100644 index 0000000000..6938969050 --- /dev/null +++ b/public/src/client/account/blocks.js @@ -0,0 +1,67 @@ +'use strict'; + +define('forum/account/blocks', [ + 'forum/account/header', + 'api', + 'hooks', + 'alerts', +], function (header, api, hooks, alerts) { + const Blocks = {}; + + Blocks.init = function () { + header.init(); + + $('#user-search').on('keyup', function () { + const username = this.value; + + api.get('/api/users', { + query: username, + searchBy: 'username', + paginate: false, + }, function (err, data) { + if (err) { + return alerts.error(err); + } + + // Only show first 10 matches + if (data.matchCount > 10) { + data.users.length = 10; + } + + app.parseAndTranslate('account/blocks', 'edit', { + edit: data.users, + }, function (html) { + $('.block-edit').html(html); + }); + }); + }); + + $('.block-edit').on('click', '[data-action="toggle"]', function () { + const uid = parseInt(this.getAttribute('data-uid'), 10); + socket.emit('user.toggleBlock', { + blockeeUid: uid, + blockerUid: ajaxify.data.uid, + }, Blocks.refreshList); + }); + }; + + Blocks.refreshList = function (err) { + if (err) { + return alerts.error(err); + } + + $.get(config.relative_path + '/api/' + ajaxify.currentPage) + .done(function (payload) { + app.parseAndTranslate('account/blocks', 'users', payload, function (html) { + $('#users-container').html(html); + $('#users-container').siblings('div.alert')[html.length ? 'hide' : 'show'](); + }); + hooks.fire('action:user.blocks.toggle', { data: payload }); + }) + .fail(function () { + ajaxify.go(ajaxify.currentPage); + }); + }; + + return Blocks; +}); diff --git a/public/src/client/account/bookmarks.js b/public/src/client/account/bookmarks.js new file mode 100644 index 0000000000..1d1d39044c --- /dev/null +++ b/public/src/client/account/bookmarks.js @@ -0,0 +1,16 @@ +'use strict'; + + +define('forum/account/bookmarks', ['forum/account/header', 'forum/account/posts'], function (header, posts) { + const Bookmarks = {}; + + Bookmarks.init = function () { + header.init(); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + + posts.handleInfiniteScroll('account/bookmarks'); + }; + + return Bookmarks; +}); diff --git a/public/src/client/account/categories.js b/public/src/client/account/categories.js new file mode 100644 index 0000000000..022185907b --- /dev/null +++ b/public/src/client/account/categories.js @@ -0,0 +1,62 @@ +'use strict'; + + +define('forum/account/categories', ['forum/account/header', 'alerts'], function (header, alerts) { + const Categories = {}; + + Categories.init = function () { + header.init(); + + ajaxify.data.categories.forEach(function (category) { + handleIgnoreWatch(category.cid); + }); + + $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { + const cids = []; + const state = $(this).attr('data-state'); + $('[data-parent-cid="0"]').each(function (index, el) { + cids.push($(el).attr('data-cid')); + }); + + socket.emit('categories.setWatchState', { cid: cids, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) { + if (err) { + return alerts.error(err); + } + updateDropdowns(modified_cids, state); + }); + }); + }; + + function handleIgnoreWatch(cid) { + const category = $('[data-cid="' + cid + '"]'); + category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { + const $this = $(this); + const state = $this.attr('data-state'); + + socket.emit('categories.setWatchState', { cid: cid, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) { + if (err) { + return alerts.error(err); + } + updateDropdowns(modified_cids, state); + + alerts.success('[[category:' + state + '.message]]'); + }); + }); + } + + function updateDropdowns(modified_cids, state) { + modified_cids.forEach(function (cid) { + const category = $('[data-cid="' + cid + '"]'); + category.find('[component="category/watching/menu"]').toggleClass('hidden', state !== 'watching'); + category.find('[component="category/watching/check"]').toggleClass('fa-check', state === 'watching'); + + category.find('[component="category/notwatching/menu"]').toggleClass('hidden', state !== 'notwatching'); + category.find('[component="category/notwatching/check"]').toggleClass('fa-check', state === 'notwatching'); + + category.find('[component="category/ignoring/menu"]').toggleClass('hidden', state !== 'ignoring'); + category.find('[component="category/ignoring/check"]').toggleClass('fa-check', state === 'ignoring'); + }); + } + + return Categories; +}); diff --git a/public/src/client/account/consent.js b/public/src/client/account/consent.js new file mode 100644 index 0000000000..b3f1606a82 --- /dev/null +++ b/public/src/client/account/consent.js @@ -0,0 +1,34 @@ +'use strict'; + + +define('forum/account/consent', ['forum/account/header', 'alerts', 'api'], function (header, alerts, api) { + const Consent = {}; + + Consent.init = function () { + header.init(); + + $('[data-action="consent"]').on('click', function () { + socket.emit('user.gdpr.consent', {}, function (err) { + if (err) { + return alerts.error(err); + } + + ajaxify.refresh(); + }); + }); + + handleExport($('[data-action="export-profile"]'), 'profile', '[[user:consent.export-profile-success]]'); + handleExport($('[data-action="export-posts"]'), 'posts', '[[user:consent.export-posts-success]]'); + handleExport($('[data-action="export-uploads"]'), 'uploads', '[[user:consent.export-uploads-success]]'); + + function handleExport(el, type, success) { + el.on('click', function () { + api.post(`/users/${ajaxify.data.uid}/exports/${type}`).then(() => { + alerts.success(success); + }).catch(alerts.error); + }); + } + }; + + return Consent; +}); diff --git a/public/src/client/account/downvoted.js b/public/src/client/account/downvoted.js new file mode 100644 index 0000000000..ac18aaf68f --- /dev/null +++ b/public/src/client/account/downvoted.js @@ -0,0 +1,16 @@ +'use strict'; + + +define('forum/account/downvoted', ['forum/account/header', 'forum/account/posts'], function (header, posts) { + const Downvoted = {}; + + Downvoted.init = function () { + header.init(); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + + posts.handleInfiniteScroll('account/downvoted'); + }; + + return Downvoted; +}); diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js new file mode 100644 index 0000000000..2ca8cf7384 --- /dev/null +++ b/public/src/client/account/edit.js @@ -0,0 +1,161 @@ +'use strict'; + +define('forum/account/edit', [ + 'forum/account/header', + 'accounts/picture', + 'translator', + 'api', + 'hooks', + 'bootbox', + 'alerts', +], function (header, picture, translator, api, hooks, bootbox, alerts) { + const AccountEdit = {}; + + AccountEdit.init = function () { + header.init(); + + $('#submitBtn').on('click', updateProfile); + + if (ajaxify.data.groupTitleArray.length === 1 && ajaxify.data.groupTitleArray[0] === '') { + $('#groupTitle option[value=""]').attr('selected', true); + } + + handleImageChange(); + handleAccountDelete(); + handleEmailConfirm(); + updateSignature(); + updateAboutMe(); + handleGroupSort(); + }; + + function updateProfile() { + const userData = $('form[component="profile/edit/form"]').serializeObject(); + userData.uid = ajaxify.data.uid; + userData.groupTitle = userData.groupTitle || ''; + userData.groupTitle = JSON.stringify( + Array.isArray(userData.groupTitle) ? userData.groupTitle : [userData.groupTitle] + ); + + hooks.fire('action:profile.update', userData); + + api.put('/users/' + userData.uid, userData).then((res) => { + alerts.success('[[user:profile_update_success]]'); + + if (res.picture) { + $('#user-current-picture').attr('src', res.picture); + } + + picture.updateHeader(res.picture); + }).catch(alerts.error); + + return false; + } + + function handleImageChange() { + $('#changePictureBtn').on('click', function () { + picture.openChangeModal(); + return false; + }); + } + + function handleAccountDelete() { + $('#deleteAccountBtn').on('click', function () { + translator.translate('[[user:delete_account_confirm]]', function (translated) { + const modal = bootbox.confirm(translated + '

    ', function (confirm) { + if (!confirm) { + return; + } + + const confirmBtn = modal.find('.btn-primary'); + confirmBtn.html(''); + confirmBtn.prop('disabled', true); + api.del(`/users/${ajaxify.data.uid}/account`, { + password: $('#confirm-password').val(), + }, function (err) { + function restoreButton() { + translator.translate('[[modules:bootbox.confirm]]', function (confirmText) { + confirmBtn.text(confirmText); + confirmBtn.prop('disabled', false); + }); + } + + if (err) { + restoreButton(); + return alerts.error(err); + } + + confirmBtn.html(''); + window.location.href = `${config.relative_path}/`; + }); + + return false; + }); + + modal.on('shown.bs.modal', function () { + modal.find('input').focus(); + }); + }); + return false; + }); + } + + function handleEmailConfirm() { + $('#confirm-email').on('click', function () { + const btn = $(this).attr('disabled', true); + socket.emit('user.emailConfirm', {}, function (err) { + btn.removeAttr('disabled'); + if (err) { + return alerts.error(err); + } + alerts.success('[[notifications:email-confirm-sent]]'); + }); + }); + } + + function getCharsLeft(el, max) { + return el.length ? '(' + el.val().length + '/' + max + ')' : ''; + } + + function updateSignature() { + const el = $('#signature'); + $('#signatureCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumSignatureLength)); + + el.on('keyup change', function () { + $('#signatureCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumSignatureLength)); + }); + } + + function updateAboutMe() { + const el = $('#aboutme'); + $('#aboutMeCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumAboutMeLength)); + + el.on('keyup change', function () { + $('#aboutMeCharCountLeft').html(getCharsLeft(el, ajaxify.data.maximumAboutMeLength)); + }); + } + + function handleGroupSort() { + function move(direction) { + const selected = $('#groupTitle').val(); + if (!ajaxify.data.allowMultipleBadges || (Array.isArray(selected) && selected.length > 1)) { + return; + } + const el = $('#groupTitle').find(':selected'); + if (el.length && el.val()) { + if (direction > 0) { + el.insertAfter(el.next()); + } else if (el.prev().val()) { + el.insertBefore(el.prev()); + } + } + } + $('[component="group/order/up"]').on('click', function () { + move(-1); + }); + $('[component="group/order/down"]').on('click', function () { + move(1); + }); + } + + return AccountEdit; +}); diff --git a/public/src/client/account/edit/password.js b/public/src/client/account/edit/password.js new file mode 100644 index 0000000000..5b6213d8ec --- /dev/null +++ b/public/src/client/account/edit/password.js @@ -0,0 +1,121 @@ +'use strict'; + +define('forum/account/edit/password', [ + 'forum/account/header', 'translator', 'zxcvbn', 'api', 'alerts', +], function (header, translator, zxcvbn, api, alerts) { + const AccountEditPassword = {}; + + AccountEditPassword.init = function () { + header.init(); + + handlePasswordChange(); + }; + + function handlePasswordChange() { + const currentPassword = $('#inputCurrentPassword'); + const password_notify = $('#password-notify'); + const password_confirm_notify = $('#password-confirm-notify'); + const password = $('#inputNewPassword'); + const password_confirm = $('#inputNewPasswordAgain'); + let passwordvalid = false; + let passwordsmatch = false; + + function onPasswordChanged() { + passwordvalid = false; + + try { + utils.assertPasswordValidity(password.val(), zxcvbn); + + if (password.val() === ajaxify.data.username) { + throw new Error('[[user:password_same_as_username]]'); + } else if (password.val() === ajaxify.data.email) { + throw new Error('[[user:password_same_as_email]]'); + } + + showSuccess(password_notify); + passwordvalid = true; + } catch (err) { + showError(password_notify, err.message); + } + } + + function onPasswordConfirmChanged() { + if (password.val() !== password_confirm.val()) { + showError(password_confirm_notify, '[[user:change_password_error_match]]'); + passwordsmatch = false; + } else { + if (password.val()) { + showSuccess(password_confirm_notify); + } else { + password_confirm_notify.parent().removeClass('alert-success alert-danger'); + password_confirm_notify.children().show(); + password_confirm_notify.find('.msg').html(''); + } + + passwordsmatch = true; + } + } + + password.on('blur', onPasswordChanged); + password_confirm.on('blur', onPasswordConfirmChanged); + + $('#changePasswordBtn').on('click', function () { + onPasswordChanged(); + onPasswordConfirmChanged(); + + const btn = $(this); + if (passwordvalid && passwordsmatch) { + btn.addClass('disabled').find('i').removeClass('hide'); + api.put('/users/' + ajaxify.data.theirid + '/password', { + currentPassword: currentPassword.val(), + newPassword: password.val(), + }) + .then(() => { + if (parseInt(app.user.uid, 10) === parseInt(ajaxify.data.uid, 10)) { + window.location.href = config.relative_path + '/login'; + } else { + ajaxify.go('user/' + ajaxify.data.userslug + '/edit'); + } + }) + .finally(() => { + btn.removeClass('disabled').find('i').addClass('hide'); + currentPassword.val(''); + password.val(''); + password_confirm.val(''); + password_notify.parent().removeClass('show-success show-danger'); + password_confirm_notify.parent().removeClass('show-success show-danger'); + passwordsmatch = false; + passwordvalid = false; + }); + } else { + if (!passwordsmatch) { + alerts.error('[[user:change_password_error_match]]'); + } + + if (!passwordvalid) { + alerts.error('[[user:change_password_error]]'); + } + } + return false; + }); + } + + function showError(element, msg) { + translator.translate(msg, function (msg) { + element.html(msg); + + element.parent() + .removeClass('show-success') + .addClass('show-danger'); + }); + } + + function showSuccess(element) { + element.html(''); + element.parent() + .removeClass('show-danger') + .addClass('show-success'); + } + + return AccountEditPassword; +}); diff --git a/public/src/client/account/edit/username.js b/public/src/client/account/edit/username.js new file mode 100644 index 0000000000..887ae67f80 --- /dev/null +++ b/public/src/client/account/edit/username.js @@ -0,0 +1,51 @@ +'use strict'; + +define('forum/account/edit/username', [ + 'forum/account/header', 'api', 'slugify', 'alerts', +], function (header, api, slugify, alerts) { + const AccountEditUsername = {}; + + AccountEditUsername.init = function () { + header.init(); + + $('#submitBtn').on('click', function updateUsername() { + const userData = { + uid: $('#inputUID').val(), + username: $('#inputNewUsername').val(), + password: $('#inputCurrentPassword').val(), + }; + + if (!userData.username) { + return; + } + + if (userData.username === userData.password) { + return alerts.error('[[user:username_same_as_password]]'); + } + + const btn = $(this); + btn.addClass('disabled').find('i').removeClass('hide'); + + api.put('/users/' + userData.uid, userData).then((response) => { + const userslug = slugify(userData.username); + if (userData.username && userslug && parseInt(userData.uid, 10) === parseInt(app.user.uid, 10)) { + $('[component="header/profilelink"]').attr('href', config.relative_path + '/user/' + userslug); + $('[component="header/profilelink/edit"]').attr('href', config.relative_path + '/user/' + userslug + '/edit'); + $('[component="header/profilelink/settings"]').attr('href', config.relative_path + '/user/' + userslug + '/settings'); + $('[component="header/username"]').text(userData.username); + $('[component="header/usericon"]').css('background-color', response['icon:bgColor']).text(response['icon:text']); + $('[component="avatar/icon"]').css('background-color', response['icon:bgColor']).text(response['icon:text']); + } + + ajaxify.go('user/' + userslug + '/edit'); + }).catch(alerts.error) + .finally(() => { + btn.removeClass('disabled').find('i').addClass('hide'); + }); + + return false; + }); + }; + + return AccountEditUsername; +}); diff --git a/public/src/client/account/followers.js b/public/src/client/account/followers.js new file mode 100644 index 0000000000..1ee6acd6c1 --- /dev/null +++ b/public/src/client/account/followers.js @@ -0,0 +1,12 @@ +'use strict'; + + +define('forum/account/followers', ['forum/account/header'], function (header) { + const Followers = {}; + + Followers.init = function () { + header.init(); + }; + + return Followers; +}); diff --git a/public/src/client/account/following.js b/public/src/client/account/following.js new file mode 100644 index 0000000000..e6e5b270fe --- /dev/null +++ b/public/src/client/account/following.js @@ -0,0 +1,12 @@ +'use strict'; + + +define('forum/account/following', ['forum/account/header'], function (header) { + const Following = {}; + + Following.init = function () { + header.init(); + }; + + return Following; +}); diff --git a/public/src/client/account/groups.js b/public/src/client/account/groups.js new file mode 100644 index 0000000000..71666f2eda --- /dev/null +++ b/public/src/client/account/groups.js @@ -0,0 +1,20 @@ +'use strict'; + + +define('forum/account/groups', ['forum/account/header'], function (header) { + const AccountTopics = {}; + + AccountTopics.init = function () { + header.init(); + + const groupsEl = $('#groups-list'); + + groupsEl.on('click', '.list-cover', function () { + const groupSlug = $(this).parents('[data-slug]').attr('data-slug'); + + ajaxify.go('groups/' + groupSlug); + }); + }; + + return AccountTopics; +}); diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js new file mode 100644 index 0000000000..21802b643d --- /dev/null +++ b/public/src/client/account/header.js @@ -0,0 +1,287 @@ +'use strict'; + + +define('forum/account/header', [ + 'coverPhoto', + 'pictureCropper', + 'components', + 'translator', + 'benchpress', + 'accounts/delete', + 'api', + 'bootbox', + 'alerts', +], function (coverPhoto, pictureCropper, components, translator, Benchpress, AccountsDelete, api, bootbox, alerts) { + const AccountHeader = {}; + let isAdminOrSelfOrGlobalMod; + + AccountHeader.init = function () { + isAdminOrSelfOrGlobalMod = ajaxify.data.isAdmin || ajaxify.data.isSelf || ajaxify.data.isGlobalModerator; + + hidePrivateLinks(); + selectActivePill(); + + if (isAdminOrSelfOrGlobalMod) { + setupCoverPhoto(); + } + + components.get('account/follow').on('click', function () { + toggleFollow('follow'); + }); + + components.get('account/unfollow').on('click', function () { + toggleFollow('unfollow'); + }); + + components.get('account/chat').on('click', async function () { + const roomId = await socket.emit('modules.chats.hasPrivateChat', ajaxify.data.uid); + const chat = await app.require('chat'); + if (roomId) { + chat.openChat(roomId); + } else { + chat.newChat(ajaxify.data.uid); + } + }); + + components.get('account/new-chat').on('click', async function () { + const chat = await app.require('chat'); + chat.newChat(ajaxify.data.uid, function () { + components.get('account/chat').parent().removeClass('hidden'); + }); + }); + + + components.get('account/ban').on('click', function () { + banAccount(ajaxify.data.theirid); + }); + components.get('account/mute').on('click', function () { + muteAccount(ajaxify.data.theirid); + }); + components.get('account/unban').on('click', function () { + unbanAccount(ajaxify.data.theirid); + }); + components.get('account/unmute').on('click', function () { + unmuteAccount(ajaxify.data.theirid); + }); + components.get('account/delete-account').on('click', handleDeleteEvent.bind(null, 'account')); + components.get('account/delete-content').on('click', handleDeleteEvent.bind(null, 'content')); + components.get('account/delete-all').on('click', handleDeleteEvent.bind(null, 'purge')); + components.get('account/flag').on('click', flagAccount); + components.get('account/block').on('click', toggleBlockAccount); + }; + + function handleDeleteEvent(type) { + AccountsDelete[type](ajaxify.data.theirid); + } + + // TODO: This exported method is used in forum/flags/detail -- refactor?? + AccountHeader.banAccount = banAccount; + AccountHeader.muteAccount = muteAccount; + AccountHeader.unbanAccount = unbanAccount; + AccountHeader.unmuteAccount = unmuteAccount; + + function hidePrivateLinks() { + if (!app.user.uid || app.user.uid !== parseInt(ajaxify.data.theirid, 10)) { + $('.account-sub-links .plugin-link.private').addClass('hide'); + } + } + + function selectActivePill() { + $('.account-sub-links li').removeClass('active').each(function () { + const href = $(this).find('a').attr('href'); + + if (decodeURIComponent(href) === decodeURIComponent(window.location.pathname)) { + $(this).addClass('active'); + return false; + } + }); + } + + function setupCoverPhoto() { + coverPhoto.init( + components.get('account/cover'), + function (imageData, position, callback) { + socket.emit('user.updateCover', { + uid: ajaxify.data.uid, + imageData: imageData, + position: position, + }, callback); + }, + function () { + pictureCropper.show({ + title: '[[user:upload_cover_picture]]', + socketMethod: 'user.updateCover', + aspectRatio: NaN, + allowSkippingCrop: true, + restrictImageDimension: false, + paramName: 'uid', + paramValue: ajaxify.data.theirid, + accept: '.png,.jpg,.bmp', + }, function (imageUrlOnServer) { + imageUrlOnServer = (!imageUrlOnServer.startsWith('http') ? config.relative_path : '') + imageUrlOnServer + '?' + Date.now(); + components.get('account/cover').css('background-image', 'url(' + imageUrlOnServer + ')'); + }); + }, + removeCover + ); + } + + function toggleFollow(type) { + api[type === 'follow' ? 'put' : 'del']('/users/' + ajaxify.data.uid + '/follow', undefined, function (err) { + if (err) { + return alerts.error(err); + } + components.get('account/follow').toggleClass('hide', type === 'follow'); + components.get('account/unfollow').toggleClass('hide', type === 'unfollow'); + alerts.success('[[global:alert.' + type + ', ' + ajaxify.data.username + ']]'); + }); + + return false; + } + + function banAccount(theirid, onSuccess) { + theirid = theirid || ajaxify.data.theirid; + + Benchpress.render('admin/partials/temporary-ban', {}).then(function (html) { + bootbox.dialog({ + className: 'ban-modal', + title: '[[user:ban_account]]', + message: html, + show: true, + buttons: { + close: { + label: '[[global:close]]', + className: 'btn-link', + }, + submit: { + label: '[[user:ban_account]]', + callback: function () { + const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) { + data[cur.name] = cur.value; + return data; + }, {}); + + const until = formData.length > 0 ? ( + Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) + ) : 0; + + api.put('/users/' + theirid + '/ban', { + until: until, + reason: formData.reason || '', + }).then(() => { + if (typeof onSuccess === 'function') { + return onSuccess(); + } + + ajaxify.refresh(); + }).catch(alerts.error); + }, + }, + }, + }); + }); + } + + function unbanAccount(theirid) { + api.del('/users/' + theirid + '/ban').then(() => { + ajaxify.refresh(); + }).catch(alerts.error); + } + + function muteAccount(theirid, onSuccess) { + theirid = theirid || ajaxify.data.theirid; + Benchpress.render('admin/partials/temporary-mute', {}).then(function (html) { + bootbox.dialog({ + className: 'mute-modal', + title: '[[user:mute_account]]', + message: html, + show: true, + buttons: { + close: { + label: '[[global:close]]', + className: 'btn-link', + }, + submit: { + label: '[[user:mute_account]]', + callback: function () { + const formData = $('.mute-modal form').serializeArray().reduce(function (data, cur) { + data[cur.name] = cur.value; + return data; + }, {}); + + const until = formData.length > 0 ? ( + Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1)) + ) : 0; + + api.put('/users/' + theirid + '/mute', { + until: until, + reason: formData.reason || '', + }).then(() => { + if (typeof onSuccess === 'function') { + return onSuccess(); + } + ajaxify.refresh(); + }).catch(alerts.error); + }, + }, + }, + }); + }); + } + + function unmuteAccount(theirid) { + api.del('/users/' + theirid + '/mute').then(() => { + ajaxify.refresh(); + }).catch(alerts.error); + } + + function flagAccount() { + require(['flags'], function (flags) { + flags.showFlagModal({ + type: 'user', + id: ajaxify.data.uid, + }); + }); + } + + function toggleBlockAccount() { + const targetEl = this; + socket.emit('user.toggleBlock', { + blockeeUid: ajaxify.data.uid, + blockerUid: app.user.uid, + }, function (err, blocked) { + if (err) { + return alerts.error(err); + } + + translator.translate('[[user:' + (blocked ? 'unblock' : 'block') + '_user]]', function (label) { + $(targetEl).text(label); + }); + }); + + // Keep dropdown open + return false; + } + + function removeCover() { + translator.translate('[[user:remove_cover_picture_confirm]]', function (translated) { + bootbox.confirm(translated, function (confirm) { + if (!confirm) { + return; + } + + socket.emit('user.removeCover', { + uid: ajaxify.data.uid, + }, function (err) { + if (!err) { + ajaxify.refresh(); + } else { + alerts.error(err); + } + }); + }); + }); + } + + return AccountHeader; +}); diff --git a/public/src/client/account/ignored.js b/public/src/client/account/ignored.js new file mode 100644 index 0000000000..4d2aa93f00 --- /dev/null +++ b/public/src/client/account/ignored.js @@ -0,0 +1,13 @@ +'use strict'; + +define('forum/account/ignored', ['forum/account/header', 'forum/account/topics'], function (header, topics) { + const AccountIgnored = {}; + + AccountIgnored.init = function () { + header.init(); + + topics.handleInfiniteScroll('account/ignored'); + }; + + return AccountIgnored; +}); diff --git a/public/src/client/account/info.js b/public/src/client/account/info.js new file mode 100644 index 0000000000..74c5b41475 --- /dev/null +++ b/public/src/client/account/info.js @@ -0,0 +1,38 @@ +'use strict'; + + +define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/sessions'], function (header, alerts, sessions) { + const Info = {}; + + Info.init = function () { + header.init(); + handleModerationNote(); + sessions.prepareSessionRevocation(); + }; + + function handleModerationNote() { + $('[component="account/save-moderation-note"]').on('click', function () { + const note = $('[component="account/moderation-note"]').val(); + socket.emit('user.setModerationNote', { uid: ajaxify.data.uid, note: note }, function (err) { + if (err) { + return alerts.error(err); + } + $('[component="account/moderation-note"]').val(''); + alerts.success('[[user:info.moderation-note.success]]'); + const timestamp = Date.now(); + const data = [{ + note: utils.escapeHTML(note), + user: app.user, + timestamp: timestamp, + timestampISO: utils.toISOString(timestamp), + }]; + app.parseAndTranslate('account/info', 'moderationNotes', { moderationNotes: data }, function (html) { + $('[component="account/moderation-note/list"]').prepend(html); + html.find('.timeago').timeago(); + }); + }); + }); + } + + return Info; +}); diff --git a/public/src/client/account/posts.js b/public/src/client/account/posts.js new file mode 100644 index 0000000000..90d59df5d8 --- /dev/null +++ b/public/src/client/account/posts.js @@ -0,0 +1,56 @@ +'use strict'; + + +define('forum/account/posts', ['forum/account/header', 'forum/infinitescroll', 'hooks'], function (header, infinitescroll, hooks) { + const AccountPosts = {}; + + let template; + let page = 1; + + AccountPosts.init = function () { + header.init(); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + + AccountPosts.handleInfiniteScroll('account/posts'); + }; + + AccountPosts.handleInfiniteScroll = function (_template) { + template = _template; + page = ajaxify.data.pagination.currentPage; + if (!config.usePagination) { + infinitescroll.init(loadMore); + } + }; + + function loadMore(direction) { + if (direction < 0) { + return; + } + const params = utils.params(); + page += 1; + params.page = page; + + infinitescroll.loadMoreXhr(params, function (data, done) { + if (data.posts && data.posts.length) { + onPostsLoaded(data.posts, done); + } else { + done(); + } + }); + } + + function onPostsLoaded(posts, callback) { + app.parseAndTranslate(template, 'posts', { posts: posts }, function (html) { + $('[component="posts"]').append(html); + html.find('img:not(.not-responsive)').addClass('img-responsive'); + html.find('.timeago').timeago(); + app.createUserTooltips(html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + hooks.fire('action:posts.loaded', { posts: posts }); + callback(); + }); + } + + return AccountPosts; +}); diff --git a/public/src/client/account/profile.js b/public/src/client/account/profile.js new file mode 100644 index 0000000000..2e0aa14765 --- /dev/null +++ b/public/src/client/account/profile.js @@ -0,0 +1,38 @@ +'use strict'; + + +define('forum/account/profile', [ + 'forum/account/header', + 'bootbox', +], function (header, bootbox) { + const Account = {}; + + Account.init = function () { + header.init(); + + app.enterRoom('user/' + ajaxify.data.theirid); + + processPage(); + + if (parseInt(ajaxify.data.emailChanged, 10) === 1) { + bootbox.alert('[[user:emailUpdate.change-instructions]]'); + } + + socket.removeListener('event:user_status_change', onUserStatusChange); + socket.on('event:user_status_change', onUserStatusChange); + }; + + function processPage() { + $('[component="posts"] [component="post/content"] img:not(.not-responsive), [component="aboutme"] img:not(.not-responsive)').addClass('img-responsive'); + } + + function onUserStatusChange(data) { + if (parseInt(ajaxify.data.theirid, 10) !== parseInt(data.uid, 10)) { + return; + } + + app.updateUserStatus($('.account [data-uid="' + data.uid + '"] [component="user/status"]'), data.status); + } + + return Account; +}); diff --git a/public/src/client/account/sessions.js b/public/src/client/account/sessions.js new file mode 100644 index 0000000000..387c2f21b9 --- /dev/null +++ b/public/src/client/account/sessions.js @@ -0,0 +1,38 @@ +'use strict'; + + +define('forum/account/sessions', ['forum/account/header', 'components', 'api', 'alerts'], function (header, components, api, alerts) { + const Sessions = {}; + + Sessions.init = function () { + header.init(); + Sessions.prepareSessionRevocation(); + }; + + Sessions.prepareSessionRevocation = function () { + components.get('user/sessions').on('click', '[data-action]', function () { + const parentEl = $(this).parents('[data-uuid]'); + const uuid = parentEl.attr('data-uuid'); + + if (uuid) { + // This is done via DELETE because a user shouldn't be able to + // revoke his own session! This is what logout is for + api.del(`/users/${ajaxify.data.uid}/sessions/${uuid}`, {}).then(() => { + parentEl.remove(); + }).catch((err) => { + try { + const errorObj = JSON.parse(err.responseText); + if (errorObj.loggedIn === false) { + window.location.href = config.relative_path + '/login?error=' + errorObj.title; + } + alerts.error(errorObj.title); + } catch (e) { + alerts.error('[[error:invalid-data]]'); + } + }); + } + }); + }; + + return Sessions; +}); diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js new file mode 100644 index 0000000000..35065111b5 --- /dev/null +++ b/public/src/client/account/settings.js @@ -0,0 +1,147 @@ +'use strict'; + + +define('forum/account/settings', [ + 'forum/account/header', 'components', 'translator', 'api', 'alerts', +], function (header, components, translator, api, alerts) { + const AccountSettings = {}; + + // If page skin is changed but not saved, switch the skin back + $(window).on('action:ajaxify.start', function () { + if (ajaxify.data.template.name === 'account/settings' && $('#bootswatchSkin').length && $('#bootswatchSkin').val() !== config.bootswatchSkin) { + reskin(config.bootswatchSkin); + } + }); + + AccountSettings.init = function () { + header.init(); + + $('#submitBtn').on('click', function () { + const settings = loadSettings(); + + if (settings.homePageRoute === 'custom' && settings.homePageCustom) { + $.get(config.relative_path + '/' + settings.homePageCustom, function () { + saveSettings(settings); + }).fail(function () { + alerts.error('[[error:invalid-home-page-route]]'); + }); + } else { + saveSettings(settings); + } + + return false; + }); + + $('#bootswatchSkin').on('change', function () { + reskin($(this).val()); + }); + + $('[data-property="homePageRoute"]').on('change', toggleCustomRoute); + + toggleCustomRoute(); + + components.get('user/sessions').find('.timeago').timeago(); + }; + + function loadSettings() { + const settings = {}; + + $('.account').find('input, textarea, select').each(function (id, input) { + input = $(input); + const setting = input.attr('data-property'); + if (input.is('select')) { + settings[setting] = input.val(); + return; + } + + switch (input.attr('type')) { + case 'checkbox': + settings[setting] = input.is(':checked') ? 1 : 0; + break; + default: + settings[setting] = input.val(); + break; + } + }); + + return settings; + } + + function saveSettings(settings) { + api.put(`/users/${ajaxify.data.uid}/settings`, { settings }).then((newSettings) => { + alerts.success('[[success:settings-saved]]'); + let languageChanged = false; + for (const key in newSettings) { + if (newSettings.hasOwnProperty(key)) { + if (key === 'userLang' && config.userLang !== newSettings.userLang) { + languageChanged = true; + } + if (config.hasOwnProperty(key)) { + config[key] = newSettings[key]; + } + } + } + + if (languageChanged && parseInt(app.user.uid, 10) === parseInt(ajaxify.data.theirid, 10)) { + translator.translate('[[language:dir]]', config.userLang, function (translated) { + const htmlEl = $('html'); + htmlEl.attr('data-dir', translated); + htmlEl.css('direction', translated); + }); + + translator.switchTimeagoLanguage(utils.userLangToTimeagoCode(config.userLang), function () { + overrides.overrideTimeago(); + ajaxify.refresh(); + }); + } + }); + } + + function toggleCustomRoute() { + if ($('[data-property="homePageRoute"]').val() === 'custom') { + $('#homePageCustom').show(); + } else { + $('#homePageCustom').hide(); + $('[data-property="homePageCustom"]').val(''); + } + } + + function reskin(skinName) { + const clientEl = Array.prototype.filter.call(document.querySelectorAll('link[rel="stylesheet"]'), function (el) { + return el.href.indexOf(config.relative_path + '/assets/client') !== -1; + })[0] || null; + if (!clientEl) { + return; + } + + const currentSkinClassName = $('body').attr('class').split(/\s+/).filter(function (className) { + return className.startsWith('skin-'); + }); + if (!currentSkinClassName[0]) { + return; + } + let currentSkin = currentSkinClassName[0].slice(5); + currentSkin = currentSkin !== 'noskin' ? currentSkin : ''; + + // Stop execution if skin didn't change + if (skinName === currentSkin) { + return; + } + + const linkEl = document.createElement('link'); + linkEl.rel = 'stylesheet'; + linkEl.type = 'text/css'; + linkEl.href = config.relative_path + '/assets/client' + (skinName ? '-' + skinName : '') + '.css'; + linkEl.onload = function () { + clientEl.parentNode.removeChild(clientEl); + + // Update body class with proper skin name + $('body').removeClass(currentSkinClassName.join(' ')); + $('body').addClass('skin-' + (skinName || 'noskin')); + }; + + document.head.appendChild(linkEl); + } + + return AccountSettings; +}); diff --git a/public/src/client/account/topics.js b/public/src/client/account/topics.js new file mode 100644 index 0000000000..30d601a0da --- /dev/null +++ b/public/src/client/account/topics.js @@ -0,0 +1,57 @@ +'use strict'; + + +define('forum/account/topics', [ + 'forum/account/header', + 'forum/infinitescroll', + 'hooks', +], function (header, infinitescroll, hooks) { + const AccountTopics = {}; + + let template; + let page = 1; + + AccountTopics.init = function () { + header.init(); + + AccountTopics.handleInfiniteScroll('account/topics'); + }; + + AccountTopics.handleInfiniteScroll = function (_template) { + template = _template; + page = ajaxify.data.pagination.currentPage; + if (!config.usePagination) { + infinitescroll.init(loadMore); + } + }; + + function loadMore(direction) { + if (direction < 0) { + return; + } + const params = utils.params(); + page += 1; + params.page = page; + + infinitescroll.loadMoreXhr(params, function (data, done) { + if (data.topics && data.topics.length) { + onTopicsLoaded(data.topics, done); + } else { + done(); + } + }); + } + + function onTopicsLoaded(topics, callback) { + app.parseAndTranslate(template, 'topics', { topics: topics }, function (html) { + $('[component="category"]').append(html); + html.find('.timeago').timeago(); + app.createUserTooltips(html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + hooks.fire('action:topics.loaded', { topics: topics }); + callback(); + }); + } + + return AccountTopics; +}); diff --git a/public/src/client/account/uploads.js b/public/src/client/account/uploads.js new file mode 100644 index 0000000000..cb8fdadadd --- /dev/null +++ b/public/src/client/account/uploads.js @@ -0,0 +1,24 @@ +'use strict'; + +define('forum/account/uploads', ['forum/account/header', 'alerts'], function (header, alerts) { + const AccountUploads = {}; + + AccountUploads.init = function () { + header.init(); + + $('[data-action="delete"]').on('click', function () { + const el = $(this).parents('[data-name]'); + const name = el.attr('data-name'); + + socket.emit('user.deleteUpload', { name: name, uid: ajaxify.data.uid }, function (err) { + if (err) { + return alerts.error(err); + } + el.remove(); + }); + return false; + }); + }; + + return AccountUploads; +}); diff --git a/public/src/client/account/upvoted.js b/public/src/client/account/upvoted.js new file mode 100644 index 0000000000..0caf3419ac --- /dev/null +++ b/public/src/client/account/upvoted.js @@ -0,0 +1,16 @@ +'use strict'; + + +define('forum/account/upvoted', ['forum/account/header', 'forum/account/posts'], function (header, posts) { + const Upvoted = {}; + + Upvoted.init = function () { + header.init(); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + + posts.handleInfiniteScroll('account/upvoted'); + }; + + return Upvoted; +}); diff --git a/public/src/client/account/watched.js b/public/src/client/account/watched.js new file mode 100644 index 0000000000..8469e723fe --- /dev/null +++ b/public/src/client/account/watched.js @@ -0,0 +1,14 @@ +'use strict'; + + +define('forum/account/watched', ['forum/account/header', 'forum/account/topics'], function (header, topics) { + const AccountWatched = {}; + + AccountWatched.init = function () { + header.init(); + + topics.handleInfiniteScroll('account/watched'); + }; + + return AccountWatched; +}); diff --git a/public/src/client/categories.js b/public/src/client/categories.js new file mode 100644 index 0000000000..e71d18048b --- /dev/null +++ b/public/src/client/categories.js @@ -0,0 +1,71 @@ +'use strict'; + + +define('forum/categories', ['components', 'categorySelector', 'hooks'], function (components, categorySelector, hooks) { + const categories = {}; + + $(window).on('action:ajaxify.start', function (ev, data) { + if (ajaxify.currentPage !== data.url) { + socket.removeListener('event:new_post', categories.onNewPost); + } + }); + + categories.init = function () { + app.enterRoom('categories'); + + socket.removeListener('event:new_post', categories.onNewPost); + socket.on('event:new_post', categories.onNewPost); + categorySelector.init($('[component="category-selector"]'), { + privilege: 'find', + onSelect: function (category) { + ajaxify.go('/category/' + category.cid); + }, + }); + + $('.category-header').tooltip({ + placement: 'bottom', + }); + }; + + categories.onNewPost = function (data) { + if (data && data.posts && data.posts.length && data.posts[0].topic) { + renderNewPost(data.posts[0].topic.cid, data.posts[0]); + } + }; + + function renderNewPost(cid, post) { + const category = components.get('categories/category', 'cid', cid); + const numRecentReplies = category.attr('data-numRecentReplies'); + if (!numRecentReplies || !parseInt(numRecentReplies, 10)) { + return; + } + if (!category.find('[component="topic/teaser"]').length) { + return; + } + + const recentPosts = category.find('[component="category/posts"]'); + + app.parseAndTranslate('partials/categories/lastpost', 'posts', { posts: [post] }, function (html) { + html.find('.post-content img:not(.not-responsive)').addClass('img-responsive'); + html.hide(); + if (recentPosts.length === 0) { + html.appendTo(category); + } else { + html.insertBefore(recentPosts.first()); + } + + html.fadeIn(); + + app.createUserTooltips(html); + html.find('.timeago').timeago(); + + if (category.find('[component="category/posts"]').length > parseInt(numRecentReplies, 10)) { + recentPosts.last().remove(); + } + + hooks.fire('action:posts.loaded', { posts: [post] }); + }); + } + + return categories; +}); diff --git a/public/src/client/category.js b/public/src/client/category.js new file mode 100644 index 0000000000..a8a4887118 --- /dev/null +++ b/public/src/client/category.js @@ -0,0 +1,155 @@ +'use strict'; + +define('forum/category', [ + 'forum/infinitescroll', + 'share', + 'navigator', + 'topicList', + 'sort', + 'categorySelector', + 'hooks', + 'alerts', +], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts) { + const Category = {}; + + $(window).on('action:ajaxify.start', function (ev, data) { + if (!String(data.url).startsWith('category/')) { + navigator.disable(); + } + }); + + Category.init = function () { + const cid = ajaxify.data.cid; + + app.enterRoom('category_' + cid); + + share.addShareHandlers(ajaxify.data.name); + + topicList.init('category', loadTopicsAfter); + + sort.handleSort('categoryTopicSort', 'category/' + ajaxify.data.slug); + + if (!config.usePagination) { + navigator.init('[component="category/topic"]', ajaxify.data.topic_count, Category.toTop, Category.toBottom, Category.navigatorCallback); + } else { + navigator.disable(); + } + + handleScrollToTopicIndex(); + + handleIgnoreWatch(cid); + + handleLoadMoreSubcategories(); + + categorySelector.init($('[component="category-selector"]'), { + privilege: 'find', + parentCid: ajaxify.data.cid, + onSelect: function (category) { + ajaxify.go('/category/' + category.cid); + }, + }); + + hooks.fire('action:topics.loaded', { topics: ajaxify.data.topics }); + hooks.fire('action:category.loaded', { cid: ajaxify.data.cid }); + }; + + function handleScrollToTopicIndex() { + let topicIndex = ajaxify.data.topicIndex; + if (topicIndex && utils.isNumber(topicIndex)) { + topicIndex = Math.max(0, parseInt(topicIndex, 10)); + if (topicIndex && window.location.search.indexOf('page=') === -1) { + navigator.scrollToElement($('[component="category/topic"][data-index="' + topicIndex + '"]'), true, 0); + } + } + } + + function handleIgnoreWatch(cid) { + $('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () { + const $this = $(this); + const state = $this.attr('data-state'); + + socket.emit('categories.setWatchState', { cid: cid, state: state }, function (err) { + if (err) { + return alerts.error(err); + } + + $('[component="category/watching/menu"]').toggleClass('hidden', state !== 'watching'); + $('[component="category/watching/check"]').toggleClass('fa-check', state === 'watching'); + + $('[component="category/notwatching/menu"]').toggleClass('hidden', state !== 'notwatching'); + $('[component="category/notwatching/check"]').toggleClass('fa-check', state === 'notwatching'); + + $('[component="category/ignoring/menu"]').toggleClass('hidden', state !== 'ignoring'); + $('[component="category/ignoring/check"]').toggleClass('fa-check', state === 'ignoring'); + + alerts.success('[[category:' + state + '.message]]'); + }); + }); + } + + function handleLoadMoreSubcategories() { + $('[component="category/load-more-subcategories"]').on('click', function () { + const btn = $(this); + socket.emit('categories.loadMoreSubCategories', { + cid: ajaxify.data.cid, + start: ajaxify.data.nextSubCategoryStart, + }, function (err, data) { + if (err) { + return alerts.error(err); + } + btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage); + if (!data.length) { + return; + } + app.parseAndTranslate('category', 'children', { children: data }, function (html) { + html.find('.timeago').timeago(); + $('[component="category/subcategory/container"]').append(html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + app.createUserTooltips(html); + ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage; + ajaxify.data.subCategoriesLeft -= data.length; + btn.toggleClass('hidden', ajaxify.data.subCategoriesLeft <= 0) + .translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]'); + }); + }); + return false; + }); + } + + Category.toTop = function () { + navigator.scrollTop(0); + }; + + Category.toBottom = function () { + socket.emit('categories.getTopicCount', ajaxify.data.cid, function (err, count) { + if (err) { + return alerts.error(err); + } + + navigator.scrollBottom(count - 1); + }); + }; + + Category.navigatorCallback = function (topIndex, bottomIndex) { + return bottomIndex; + }; + + function loadTopicsAfter(after, direction, callback) { + callback = callback || function () {}; + + hooks.fire('action:topics.loading'); + const params = utils.params(); + infinitescroll.loadMore('categories.loadMore', { + cid: ajaxify.data.cid, + after: after, + direction: direction, + query: params, + categoryTopicSort: config.categoryTopicSort, + }, function (data, done) { + hooks.fire('action:topics.loaded', { topics: data.topics }); + callback(data, done); + }); + } + + return Category; +}); diff --git a/public/src/client/category/tools.js b/public/src/client/category/tools.js new file mode 100644 index 0000000000..211012dbd4 --- /dev/null +++ b/public/src/client/category/tools.js @@ -0,0 +1,312 @@ + +'use strict'; + + +define('forum/category/tools', [ + 'topicSelect', + 'forum/topic/threadTools', + 'components', + 'api', + 'bootbox', + 'alerts', +], function (topicSelect, threadTools, components, api, bootbox, alerts) { + const CategoryTools = {}; + + CategoryTools.init = function () { + topicSelect.init(updateDropdownOptions); + + handlePinnedTopicSort(); + + components.get('topic/delete').on('click', function () { + categoryCommand('del', '/state', 'delete', onDeletePurgeComplete); + return false; + }); + + components.get('topic/restore').on('click', function () { + categoryCommand('put', '/state', 'restore', onDeletePurgeComplete); + return false; + }); + + components.get('topic/purge').on('click', function () { + categoryCommand('del', '', 'purge', onDeletePurgeComplete); + return false; + }); + + components.get('topic/lock').on('click', function () { + categoryCommand('put', '/lock', 'lock', onCommandComplete); + return false; + }); + + components.get('topic/unlock').on('click', function () { + categoryCommand('del', '/lock', 'unlock', onCommandComplete); + return false; + }); + + components.get('topic/pin').on('click', function () { + categoryCommand('put', '/pin', 'pin', onCommandComplete); + return false; + }); + + components.get('topic/unpin').on('click', function () { + categoryCommand('del', '/pin', 'unpin', onCommandComplete); + return false; + }); + + // todo: should also use categoryCommand, but no write api call exists for this yet + components.get('topic/mark-unread-for-all').on('click', function () { + const tids = topicSelect.getSelectedTids(); + if (!tids.length) { + return alerts.error('[[error:no-topics-selected]]'); + } + socket.emit('topics.markAsUnreadForAll', tids, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[topic:markAsUnreadForAll.success]]'); + tids.forEach(function (tid) { + $('[component="category/topic"][data-tid="' + tid + '"]').addClass('unread'); + }); + onCommandComplete(); + }); + return false; + }); + + components.get('topic/move').on('click', function () { + require(['forum/topic/move'], function (move) { + const tids = topicSelect.getSelectedTids(); + + if (!tids.length) { + return alerts.error('[[error:no-topics-selected]]'); + } + move.init(tids, null, onCommandComplete); + }); + + return false; + }); + + components.get('topic/move-all').on('click', function () { + const cid = ajaxify.data.cid; + if (!ajaxify.data.template.category) { + return alerts.error('[[error:invalid-data]]'); + } + require(['forum/topic/move'], function (move) { + move.init(null, cid, function (err) { + if (err) { + return alerts.error(err); + } + + ajaxify.refresh(); + }); + }); + }); + + components.get('topic/merge').on('click', function () { + const tids = topicSelect.getSelectedTids(); + require(['forum/topic/merge'], function (merge) { + merge.init(function () { + if (tids.length) { + tids.forEach(function (tid) { + merge.addTopic(tid); + }); + } + }); + }); + }); + + CategoryTools.removeListeners(); + socket.on('event:topic_deleted', setDeleteState); + socket.on('event:topic_restored', setDeleteState); + socket.on('event:topic_purged', onTopicPurged); + socket.on('event:topic_locked', setLockedState); + socket.on('event:topic_unlocked', setLockedState); + socket.on('event:topic_pinned', setPinnedState); + socket.on('event:topic_unpinned', setPinnedState); + socket.on('event:topic_moved', onTopicMoved); + }; + + function categoryCommand(method, path, command, onComplete) { + if (!onComplete) { + onComplete = function () {}; + } + const tids = topicSelect.getSelectedTids(); + const body = {}; + const execute = function (ok) { + if (ok) { + Promise.all(tids.map(tid => api[method](`/topics/${tid}${path}`, body))) + .then(onComplete) + .catch(alerts.error); + } + }; + + if (!tids.length) { + return alerts.error('[[error:no-topics-selected]]'); + } + + switch (command) { + case 'delete': + case 'restore': + case 'purge': + bootbox.confirm(`[[topic:thread_tools.${command}_confirm]]`, execute); + break; + + case 'pin': + threadTools.requestPinExpiry(body, execute.bind(null, true)); + break; + + default: + execute(true); + break; + } + } + + CategoryTools.removeListeners = function () { + socket.removeListener('event:topic_deleted', setDeleteState); + socket.removeListener('event:topic_restored', setDeleteState); + socket.removeListener('event:topic_purged', onTopicPurged); + socket.removeListener('event:topic_locked', setLockedState); + socket.removeListener('event:topic_unlocked', setLockedState); + socket.removeListener('event:topic_pinned', setPinnedState); + socket.removeListener('event:topic_unpinned', setPinnedState); + socket.removeListener('event:topic_moved', onTopicMoved); + }; + + function closeDropDown() { + $('.thread-tools.open').find('.dropdown-toggle').trigger('click'); + } + + function onCommandComplete() { + closeDropDown(); + topicSelect.unselectAll(); + } + + function onDeletePurgeComplete() { + closeDropDown(); + updateDropdownOptions(); + } + + function updateDropdownOptions() { + const tids = topicSelect.getSelectedTids(); + const isAnyDeleted = isAny(isTopicDeleted, tids); + const areAllDeleted = areAll(isTopicDeleted, tids); + const isAnyPinned = isAny(isTopicPinned, tids); + const isAnyLocked = isAny(isTopicLocked, tids); + const isAnyScheduled = isAny(isTopicScheduled, tids); + const areAllScheduled = areAll(isTopicScheduled, tids); + + components.get('topic/delete').toggleClass('hidden', isAnyDeleted); + components.get('topic/restore').toggleClass('hidden', isAnyScheduled || !isAnyDeleted); + components.get('topic/purge').toggleClass('hidden', !areAllDeleted); + + components.get('topic/lock').toggleClass('hidden', isAnyLocked); + components.get('topic/unlock').toggleClass('hidden', !isAnyLocked); + + components.get('topic/pin').toggleClass('hidden', areAllScheduled || isAnyPinned); + components.get('topic/unpin').toggleClass('hidden', areAllScheduled || !isAnyPinned); + + components.get('topic/merge').toggleClass('hidden', isAnyScheduled); + } + + function isAny(method, tids) { + for (let i = 0; i < tids.length; i += 1) { + if (method(tids[i])) { + return true; + } + } + return false; + } + + function areAll(method, tids) { + for (let i = 0; i < tids.length; i += 1) { + if (!method(tids[i])) { + return false; + } + } + return true; + } + + function isTopicDeleted(tid) { + return getTopicEl(tid).hasClass('deleted'); + } + + function isTopicLocked(tid) { + return getTopicEl(tid).hasClass('locked'); + } + + function isTopicPinned(tid) { + return getTopicEl(tid).hasClass('pinned'); + } + + function isTopicScheduled(tid) { + return getTopicEl(tid).hasClass('scheduled'); + } + + function getTopicEl(tid) { + return components.get('category/topic', 'tid', tid); + } + + function setDeleteState(data) { + const topic = getTopicEl(data.tid); + topic.toggleClass('deleted', data.isDeleted); + topic.find('[component="topic/locked"]').toggleClass('hide', !data.isDeleted); + } + + function setPinnedState(data) { + const topic = getTopicEl(data.tid); + topic.toggleClass('pinned', data.isPinned); + topic.find('[component="topic/pinned"]').toggleClass('hide', !data.isPinned); + ajaxify.refresh(); + } + + function setLockedState(data) { + const topic = getTopicEl(data.tid); + topic.toggleClass('locked', data.isLocked); + topic.find('[component="topic/locked"]').toggleClass('hide', !data.isLocked); + } + + function onTopicMoved(data) { + getTopicEl(data.tid).remove(); + } + + function onTopicPurged(data) { + getTopicEl(data.tid).remove(); + } + + function handlePinnedTopicSort() { + if (!ajaxify.data.topics || !ajaxify.data.template.category) { + return; + } + const numPinned = ajaxify.data.topics.filter(topic => topic.pinned).length; + if ((!app.user.isAdmin && !app.user.isMod) || numPinned < 2) { + return; + } + + app.loadJQueryUI(function () { + const topicListEl = $('[component="category"]').filter(function (i, e) { + return !$(e).parents('[widget-area],[data-widget-area]').length; + }); + let baseIndex = 0; + topicListEl.sortable({ + handle: '[component="topic/pinned"]', + items: '[component="category/topic"].pinned', + start: function () { + baseIndex = parseInt(topicListEl.find('[component="category/topic"].pinned').first().attr('data-index'), 10); + }, + update: function (ev, ui) { + socket.emit('topics.orderPinnedTopics', { + tid: ui.item.attr('data-tid'), + order: baseIndex + ui.item.index(), + }, function (err) { + if (err) { + return alerts.error(err); + } + topicListEl.find('[component="category/topic"].pinned').each((index, el) => { + $(el).attr('data-index', baseIndex + index); + }); + }); + }, + }); + }); + } + + return CategoryTools; +}); diff --git a/public/src/client/chats.js b/public/src/client/chats.js new file mode 100644 index 0000000000..88dbbbfa54 --- /dev/null +++ b/public/src/client/chats.js @@ -0,0 +1,521 @@ +'use strict'; + + +define('forum/chats', [ + 'components', + 'translator', + 'mousetrap', + 'forum/chats/recent', + 'forum/chats/search', + 'forum/chats/messages', + 'composer/autocomplete', + 'hooks', + 'bootbox', + 'alerts', + 'chat', + 'api', + 'uploadHelpers', +], function ( + components, translator, mousetrap, + recentChats, search, messages, + autocomplete, hooks, bootbox, alerts, chatModule, + api, uploadHelpers +) { + const Chats = { + initialised: false, + }; + + let newMessage = false; + + Chats.init = function () { + const env = utils.findBootstrapEnvironment(); + + if (!Chats.initialised) { + Chats.addSocketListeners(); + Chats.addGlobalEventListeners(); + } + + recentChats.init(); + + Chats.addEventListeners(); + Chats.setActive(); + + if (env === 'md' || env === 'lg') { + Chats.addHotkeys(); + } + + $(document).ready(function () { + hooks.fire('action:chat.loaded', $('.chats-full')); + }); + + Chats.initialised = true; + messages.scrollToBottom($('.expanded-chat ul.chat-content')); + + search.init(); + + if (ajaxify.data.hasOwnProperty('roomId')) { + components.get('chat/input').focus(); + } + }; + + Chats.addEventListeners = function () { + Chats.addSendHandlers(ajaxify.data.roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]')); + Chats.addPopoutHandler(); + Chats.addActionHandlers(components.get('chat/messages'), ajaxify.data.roomId); + Chats.addMemberHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="members"]')); + Chats.addRenameHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="rename"]')); + Chats.addLeaveHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="leave"]')); + Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); + Chats.addScrollBottomHandler($('.chat-content')); + Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]')); + Chats.addIPHandler($('[component="chat/main-wrapper"]')); + Chats.createAutoComplete($('[component="chat/input"]')); + Chats.addUploadHandler({ + dragDropAreaEl: $('.chats-full'), + pasteEl: $('[component="chat/input"]'), + uploadFormEl: $('[component="chat/upload"]'), + inputEl: $('[component="chat/input"]'), + }); + + $('[data-action="close"]').on('click', function () { + Chats.switchChat(); + }); + }; + + Chats.addUploadHandler = function (options) { + uploadHelpers.init({ + dragDropAreaEl: options.dragDropAreaEl, + pasteEl: options.pasteEl, + uploadFormEl: options.uploadFormEl, + route: '/api/post/upload', // using same route as post uploads + callback: function (uploads) { + const inputEl = options.inputEl; + let text = inputEl.val(); + uploads.forEach((upload) => { + text = text + (text ? '\n' : '') + (upload.isImage ? '!' : '') + `[${upload.filename}](${upload.url})`; + }); + inputEl.val(text); + }, + }); + }; + + Chats.addIPHandler = function (container) { + container.on('click', '.chat-ip-button', function () { + const ipEl = $(this).parent(); + const mid = ipEl.parents('[data-mid]').attr('data-mid'); + socket.emit('modules.chats.getIP', mid, function (err, ip) { + if (err) { + return alerts.error(err); + } + ipEl.html(ip); + }); + }); + }; + + Chats.addPopoutHandler = function () { + $('[data-action="pop-out"]').on('click', function () { + const text = components.get('chat/input').val(); + const roomId = ajaxify.data.roomId; + + if (app.previousUrl && app.previousUrl.match(/chats/)) { + ajaxify.go('user/' + ajaxify.data.userslug + '/chats', function () { + chatModule.openChat(roomId, ajaxify.data.uid); + }, true); + } else { + window.history.go(-1); + chatModule.openChat(roomId, ajaxify.data.uid); + } + + $(window).one('action:chat.loaded', function () { + components.get('chat/input').val(text); + }); + }); + }; + + Chats.addScrollHandler = function (roomId, uid, el) { + let loading = false; + el.off('scroll').on('scroll', function () { + messages.toggleScrollUpAlert(el); + if (loading) { + return; + } + + const top = (el[0].scrollHeight - el.height()) * 0.1; + if (el.scrollTop() >= top) { + return; + } + loading = true; + const start = parseInt(el.children('[data-mid]').length, 10); + api.get(`/chats/${roomId}/messages`, { uid, start }).then((data) => { + data = data.messages; + + if (!data) { + loading = false; + return; + } + data = data.filter(function (chatMsg) { + return !$('[component="chat/message"][data-mid="' + chatMsg.messageId + '"]').length; + }); + if (!data.length) { + loading = false; + return; + } + messages.parseMessage(data, function (html) { + const currentScrollTop = el.scrollTop(); + const previousHeight = el[0].scrollHeight; + html = $(html); + el.prepend(html); + html.find('.timeago').timeago(); + html.find('img:not(.not-responsive)').addClass('img-responsive'); + el.scrollTop((el[0].scrollHeight - previousHeight) + currentScrollTop); + loading = false; + }); + }).catch(alerts.error); + }); + }; + + Chats.addScrollBottomHandler = function (chatContent) { + chatContent.parent() + .find('[component="chat/messages/scroll-up-alert"]') + .off('click').on('click', function () { + messages.scrollToBottom(chatContent); + }); + }; + + Chats.addCharactersLeftHandler = function (parent) { + const element = parent.find('[component="chat/input"]'); + element.on('change keyup paste', function () { + messages.updateRemainingLength(parent); + }); + }; + + Chats.addActionHandlers = function (element, roomId) { + element.on('click', '[data-action]', function () { + const messageId = $(this).parents('[data-mid]').attr('data-mid'); + const action = this.getAttribute('data-action'); + + switch (action) { + case 'edit': { + const inputEl = $('[data-roomid="' + roomId + '"] [component="chat/input"]'); + messages.prepEdit(inputEl, messageId, roomId); + break; + } + case 'delete': + messages.delete(messageId, roomId); + break; + + case 'restore': + messages.restore(messageId, roomId); + break; + } + }); + }; + + Chats.addHotkeys = function () { + mousetrap.bind('ctrl+up', function () { + const activeContact = $('.chats-list .bg-info'); + const prev = activeContact.prev(); + + if (prev.length) { + Chats.switchChat(prev.attr('data-roomid')); + } + }); + mousetrap.bind('ctrl+down', function () { + const activeContact = $('.chats-list .bg-info'); + const next = activeContact.next(); + + if (next.length) { + Chats.switchChat(next.attr('data-roomid')); + } + }); + mousetrap.bind('up', function (e) { + if (e.target === components.get('chat/input').get(0)) { + // Retrieve message id from messages list + const message = components.get('chat/messages').find('.chat-message[data-self="1"]').last(); + if (!message.length) { + return; + } + const lastMid = message.attr('data-mid'); + const inputEl = components.get('chat/input'); + + messages.prepEdit(inputEl, lastMid, ajaxify.data.roomId); + } + }); + }; + + Chats.addMemberHandler = function (roomId, buttonEl) { + let modal; + + buttonEl.on('click', function () { + app.parseAndTranslate('partials/modals/manage_room', {}, function (html) { + modal = bootbox.dialog({ + title: '[[modules:chat.manage-room]]', + message: html, + }); + + modal.attr('component', 'chat/manage-modal'); + + Chats.refreshParticipantsList(roomId, modal); + Chats.addKickHandler(roomId, modal); + + const searchInput = modal.find('input'); + const errorEl = modal.find('.text-danger'); + require(['autocomplete', 'translator'], function (autocomplete, translator) { + autocomplete.user(searchInput, function (event, selected) { + errorEl.text(''); + api.post(`/chats/${roomId}/users`, { + uids: [selected.item.user.uid], + }).then((body) => { + Chats.refreshParticipantsList(roomId, modal, body); + searchInput.val(''); + }).catch((err) => { + translator.translate(err.message, function (translated) { + errorEl.text(translated); + }); + }); + }); + }); + }); + }); + }; + + Chats.addKickHandler = function (roomId, modal) { + modal.on('click', '[data-action="kick"]', function () { + const uid = parseInt(this.getAttribute('data-uid'), 10); + + api.delete(`/chats/${roomId}/users/${uid}`, {}).then((body) => { + Chats.refreshParticipantsList(roomId, modal, body); + }).catch(alerts.error); + }); + }; + + Chats.addLeaveHandler = function (roomId, buttonEl) { + buttonEl.on('click', function () { + bootbox.confirm({ + size: 'small', + title: '[[modules:chat.leave]]', + message: '

    [[modules:chat.leave-prompt]]

    [[modules:chat.leave-help]]

    ', + callback: function (ok) { + if (ok) { + api.delete(`/chats/${roomId}/users/${app.user.uid}`, {}).then(() => { + // Return user to chats page. If modal, close modal. + const modal = buttonEl.parents('.chat-modal'); + if (modal.length) { + chatModule.close(modal); + } else { + ajaxify.go('chats'); + } + }).catch(alerts.error); + } + }, + }); + }); + }; + + Chats.refreshParticipantsList = async (roomId, modal, data) => { + const listEl = modal.find('.list-group'); + + if (!data) { + try { + data = await api.get(`/chats/${roomId}/users`, {}); + } catch (err) { + translator.translate('[[error:invalid-data]]', function (translated) { + listEl.find('li').text(translated); + }); + } + } + + app.parseAndTranslate('partials/modals/manage_room_users', data, function (html) { + listEl.html(html); + }); + }; + + Chats.addRenameHandler = function (roomId, buttonEl, roomName) { + let modal; + + buttonEl.on('click', function () { + app.parseAndTranslate('partials/modals/rename_room', { + name: roomName || ajaxify.data.roomName, + }, function (html) { + modal = bootbox.dialog({ + title: '[[modules:chat.rename-room]]', + message: html, + buttons: { + save: { + label: '[[global:save]]', + className: 'btn-primary', + callback: submit, + }, + }, + }); + }); + }); + + function submit() { + api.put(`/chats/${roomId}`, { + name: modal.find('#roomName').val(), + }).catch(alerts.error); + } + }; + + Chats.addSendHandlers = function (roomId, inputEl, sendEl) { + inputEl.off('keypress').on('keypress', function (e) { + if (e.which === 13 && !e.shiftKey) { + messages.sendMessage(roomId, inputEl); + return false; + } + }); + + sendEl.off('click').on('click', function () { + messages.sendMessage(roomId, inputEl); + inputEl.focus(); + return false; + }); + }; + + Chats.createAutoComplete = function (element) { + if (!element.length) { + return; + } + + const data = { + element: element, + strategies: [], + options: { + style: { + 'z-index': 20000, + flex: 0, + top: 'inherit', + }, + placement: 'top', + }, + }; + + $(window).trigger('chat:autocomplete:init', data); + if (data.strategies.length) { + autocomplete.setup(data); + } + }; + + Chats.leave = function (el) { + const roomId = el.attr('data-roomid'); + api.delete(`/chats/${roomId}/users/${app.user.uid}`, {}).then(() => { + if (parseInt(roomId, 10) === parseInt(ajaxify.data.roomId, 10)) { + ajaxify.go('user/' + ajaxify.data.userslug + '/chats'); + } else { + el.remove(); + } + + const modal = chatModule.getModal(roomId); + if (modal.length) { + chatModule.close(modal); + } + }).catch(alerts.error); + }; + + Chats.switchChat = function (roomid) { + // Allow empty arg for return to chat list/close chat + if (!roomid) { + roomid = ''; + } + + const url = 'user/' + ajaxify.data.userslug + '/chats/' + roomid + window.location.search; + if (self.fetch) { + fetch(config.relative_path + '/api/' + url, { credentials: 'include' }) + .then(function (response) { + if (response.ok) { + response.json().then(function (payload) { + app.parseAndTranslate('partials/chats/message-window', payload, function (html) { + components.get('chat/main-wrapper').html(html); + html.find('.timeago').timeago(); + ajaxify.data = payload; + Chats.setActive(); + Chats.addEventListeners(); + hooks.fire('action:chat.loaded', $('.chats-full')); + messages.scrollToBottom($('.expanded-chat ul.chat-content')); + if (history.pushState) { + history.pushState({ + url: url, + }, null, window.location.protocol + '//' + window.location.host + config.relative_path + '/' + url); + } + }); + }); + } else { + console.warn('[search] Received ' + response.status); + } + }) + .catch(function (error) { + console.warn('[search] ' + error.message); + }); + } else { + ajaxify.go(url); + } + }; + + Chats.addGlobalEventListeners = function () { + $(window).on('mousemove keypress click', function () { + if (newMessage && ajaxify.data.roomId) { + socket.emit('modules.chats.markRead', ajaxify.data.roomId); + newMessage = false; + } + }); + }; + + Chats.addSocketListeners = function () { + socket.on('event:chats.receive', function (data) { + if (parseInt(data.roomId, 10) === parseInt(ajaxify.data.roomId, 10)) { + newMessage = data.self === 0; + data.message.self = data.self; + + messages.appendChatMessage($('.expanded-chat .chat-content'), data.message); + } else if (ajaxify.data.template.chats) { + const roomEl = $('[data-roomid=' + data.roomId + ']'); + + if (roomEl.length > 0) { + roomEl.addClass('unread'); + } else { + const recentEl = components.get('chat/recent'); + app.parseAndTranslate('partials/chats/recent_room', { + rooms: { + roomId: data.roomId, + lastUser: data.message.fromUser, + usernames: data.message.fromUser.username, + unread: true, + }, + }, function (html) { + recentEl.prepend(html); + }); + } + } + }); + + socket.on('event:user_status_change', function (data) { + app.updateUserStatus($('.chats-list [data-uid="' + data.uid + '"] [component="user/status"]'), data.status); + }); + + messages.addSocketListeners(); + + socket.on('event:chats.roomRename', function (data) { + const roomEl = components.get('chat/recent/room', data.roomId); + const titleEl = roomEl.find('[component="chat/title"]'); + ajaxify.data.roomName = data.newName; + + titleEl.text(data.newName); + }); + }; + + Chats.setActive = function () { + if (ajaxify.data.roomId) { + socket.emit('modules.chats.markRead', ajaxify.data.roomId); + $('[data-roomid="' + ajaxify.data.roomId + '"]').toggleClass('unread', false); + $('.expanded-chat [component="chat/input"]').focus(); + } + $('.chats-list li').removeClass('bg-info'); + $('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-info'); + + components.get('chat/nav-wrapper').attr('data-loaded', ajaxify.data.roomId ? '1' : '0'); + }; + + + return Chats; +}); diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js new file mode 100644 index 0000000000..b330f72b00 --- /dev/null +++ b/public/src/client/chats/messages.js @@ -0,0 +1,210 @@ +'use strict'; + + +define('forum/chats/messages', [ + 'components', 'translator', 'benchpress', 'hooks', + 'bootbox', 'alerts', 'messages', 'api', +], function (components, translator, Benchpress, hooks, bootbox, alerts, messagesModule, api) { + const messages = {}; + + messages.sendMessage = async function (roomId, inputEl) { + let message = inputEl.val(); + let mid = inputEl.attr('data-mid'); + + if (!message.trim().length) { + return; + } + + inputEl.val(''); + inputEl.removeAttr('data-mid'); + messages.updateRemainingLength(inputEl.parent()); + const payload = { roomId, message, mid }; + // TODO: move this to success callback of api.post/put call? + hooks.fire('action:chat.sent', payload); + ({ roomId, message, mid } = await hooks.fire('filter:chat.send', payload)); + + if (!mid) { + api.post(`/chats/${roomId}`, { message }).catch((err) => { + inputEl.val(message); + messages.updateRemainingLength(inputEl.parent()); + if (err.message === '[[error:email-not-confirmed-chat]]') { + return messagesModule.showEmailConfirmWarning(err.message); + } + + return alerts.alert({ + alert_id: 'chat_spam_error', + title: '[[global:alert.error]]', + message: err.message, + type: 'danger', + timeout: 10000, + }); + }); + } else { + api.put(`/chats/${roomId}/messages/${mid}`, { message }).catch((err) => { + inputEl.val(message); + inputEl.attr('data-mid', mid); + messages.updateRemainingLength(inputEl.parent()); + return alerts.error(err); + }); + } + }; + + messages.updateRemainingLength = function (parent) { + const element = parent.find('[component="chat/input"]'); + parent.find('[component="chat/message/length"]').text(element.val().length); + parent.find('[component="chat/message/remaining"]').text(config.maximumChatMessageLength - element.val().length); + hooks.fire('action:chat.updateRemainingLength', { + parent: parent, + }); + }; + + messages.appendChatMessage = function (chatContentEl, data) { + const lastSpeaker = parseInt(chatContentEl.find('.chat-message').last().attr('data-uid'), 10); + const lasttimestamp = parseInt(chatContentEl.find('.chat-message').last().attr('data-timestamp'), 10); + if (!Array.isArray(data)) { + data.newSet = lastSpeaker !== parseInt(data.fromuid, 10) || + parseInt(data.timestamp, 10) > parseInt(lasttimestamp, 10) + (1000 * 60 * 3); + } + + messages.parseMessage(data, function (html) { + onMessagesParsed(chatContentEl, html); + }); + }; + + function onMessagesParsed(chatContentEl, html) { + const newMessage = $(html); + const isAtBottom = messages.isAtBottom(chatContentEl); + newMessage.appendTo(chatContentEl); + newMessage.find('.timeago').timeago(); + newMessage.find('img:not(.not-responsive)').addClass('img-responsive'); + if (isAtBottom) { + messages.scrollToBottom(chatContentEl); + } + + hooks.fire('action:chat.received', { + messageEl: newMessage, + }); + } + + + messages.parseMessage = function (data, callback) { + function done(html) { + translator.translate(html, callback); + } + + if (Array.isArray(data)) { + Benchpress.render('partials/chats/message' + (Array.isArray(data) ? 's' : ''), { + messages: data, + }).then(done); + } else { + Benchpress.render('partials/chats/' + (data.system ? 'system-message' : 'message'), { + messages: data, + }).then(done); + } + }; + + messages.isAtBottom = function (containerEl, threshold) { + if (containerEl.length) { + const distanceToBottom = containerEl[0].scrollHeight - ( + containerEl.outerHeight() + containerEl.scrollTop() + ); + return distanceToBottom < (threshold || 100); + } + }; + + messages.scrollToBottom = function (containerEl) { + if (containerEl && containerEl.length) { + containerEl.scrollTop(containerEl[0].scrollHeight - containerEl.height()); + containerEl.parent() + .find('[component="chat/messages/scroll-up-alert"]') + .addClass('hidden'); + } + }; + + messages.toggleScrollUpAlert = function (containerEl) { + const isAtBottom = messages.isAtBottom(containerEl, 300); + containerEl.parent() + .find('[component="chat/messages/scroll-up-alert"]') + .toggleClass('hidden', isAtBottom); + }; + + messages.prepEdit = function (inputEl, messageId, roomId) { + socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function (err, raw) { + if (err) { + return alerts.error(err); + } + // Populate the input field with the raw message content + if (inputEl.val().length === 0) { + // By setting the `data-mid` attribute, I tell the chat code that I am editing a + // message, instead of posting a new one. + inputEl.attr('data-mid', messageId).addClass('editing'); + inputEl.val(raw).focus(); + + hooks.fire('action:chat.prepEdit', { + inputEl: inputEl, + messageId: messageId, + roomId: roomId, + }); + } + }); + }; + + messages.addSocketListeners = function () { + socket.removeListener('event:chats.edit', onChatMessageEdited); + socket.on('event:chats.edit', onChatMessageEdited); + + socket.removeListener('event:chats.delete', onChatMessageDeleted); + socket.on('event:chats.delete', onChatMessageDeleted); + + socket.removeListener('event:chats.restore', onChatMessageRestored); + socket.on('event:chats.restore', onChatMessageRestored); + }; + + function onChatMessageEdited(data) { + data.messages.forEach(function (message) { + const self = parseInt(message.fromuid, 10) === parseInt(app.user.uid, 10); + message.self = self ? 1 : 0; + messages.parseMessage(message, function (html) { + const body = components.get('chat/message', message.messageId); + if (body.length) { + body.replaceWith(html); + components.get('chat/message', message.messageId).find('.timeago').timeago(); + } + }); + }); + } + + function onChatMessageDeleted(messageId) { + components.get('chat/message', messageId) + .toggleClass('deleted', true) + .find('[component="chat/message/body"]').translateHtml('[[modules:chat.message-deleted]]'); + } + + function onChatMessageRestored(message) { + components.get('chat/message', message.messageId) + .toggleClass('deleted', false) + .find('[component="chat/message/body"]').html(message.content); + } + + messages.delete = function (messageId, roomId) { + translator.translate('[[modules:chat.delete_message_confirm]]', function (translated) { + bootbox.confirm(translated, function (ok) { + if (!ok) { + return; + } + + api.delete(`/chats/${roomId}/messages/${messageId}`, {}).then(() => { + components.get('chat/message', messageId).toggleClass('deleted', true); + }).catch(alerts.error); + }); + }); + }; + + messages.restore = function (messageId, roomId) { + api.post(`/chats/${roomId}/messages/${messageId}`, {}).then(() => { + components.get('chat/message', messageId).toggleClass('deleted', false); + }).catch(alerts.error); + }; + + return messages; +}); diff --git a/public/src/client/chats/recent.js b/public/src/client/chats/recent.js new file mode 100644 index 0000000000..18f17e6b8a --- /dev/null +++ b/public/src/client/chats/recent.js @@ -0,0 +1,62 @@ +'use strict'; + + +define('forum/chats/recent', ['alerts'], function (alerts) { + const recent = {}; + + recent.init = function () { + require(['forum/chats'], function (Chats) { + $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function () { + Chats.switchChat($(this).attr('data-roomid')); + }); + + $('[component="chat/recent"]').on('scroll', function () { + const $this = $(this); + const bottom = ($this[0].scrollHeight - $this.height()) * 0.9; + if ($this.scrollTop() > bottom) { + loadMoreRecentChats(); + } + }); + }); + }; + + function loadMoreRecentChats() { + const recentChats = $('[component="chat/recent"]'); + if (recentChats.attr('loading')) { + return; + } + recentChats.attr('loading', 1); + socket.emit('modules.chats.getRecentChats', { + uid: ajaxify.data.uid, + after: recentChats.attr('data-nextstart'), + }, function (err, data) { + if (err) { + return alerts.error(err); + } + + if (data && data.rooms.length) { + onRecentChatsLoaded(data, function () { + recentChats.removeAttr('loading'); + recentChats.attr('data-nextstart', data.nextStart); + }); + } else { + recentChats.removeAttr('loading'); + } + }); + } + + function onRecentChatsLoaded(data, callback) { + if (!data.rooms.length) { + return callback(); + } + + app.parseAndTranslate('chats', 'rooms', data, function (html) { + $('[component="chat/recent"]').append(html); + html.find('.timeago').timeago(); + callback(); + }); + } + + + return recent; +}); diff --git a/public/src/client/chats/search.js b/public/src/client/chats/search.js new file mode 100644 index 0000000000..7adee5c816 --- /dev/null +++ b/public/src/client/chats/search.js @@ -0,0 +1,81 @@ +'use strict'; + + +define('forum/chats/search', ['components', 'api', 'alerts'], function (components, api, alerts) { + const search = {}; + + search.init = function () { + components.get('chat/search').on('keyup', utils.debounce(doSearch, 250)); + }; + + function doSearch() { + const username = components.get('chat/search').val(); + if (!username) { + return $('[component="chat/search/list"]').empty(); + } + + api.get('/api/users', { + query: username, + searchBy: 'username', + paginate: false, + }).then(displayResults) + .catch(alerts.error); + } + + function displayResults(data) { + const chatsListEl = $('[component="chat/search/list"]'); + chatsListEl.empty(); + + data.users = data.users.filter(function (user) { + return parseInt(user.uid, 10) !== parseInt(app.user.uid, 10); + }); + + if (!data.users.length) { + return chatsListEl.translateHtml('
  • [[users:no-users-found]]
  • '); + } + + data.users.forEach(function (userObj) { + const chatEl = displayUser(chatsListEl, userObj); + onUserClick(chatEl, userObj); + }); + + chatsListEl.parent().toggleClass('open', true); + } + + function displayUser(chatsListEl, userObj) { + function createUserImage() { + return (userObj.picture ? + '' : + '
    ' + userObj['icon:text'] + '
    ') + + ' ' + userObj.username; + } + + const chatEl = $('
  • ') + .attr('data-uid', userObj.uid) + .appendTo(chatsListEl); + + chatEl.append(createUserImage()); + return chatEl; + } + + function onUserClick(chatEl, userObj) { + chatEl.on('click', function () { + socket.emit('modules.chats.hasPrivateChat', userObj.uid, function (err, roomId) { + if (err) { + return alerts.error(err); + } + if (roomId) { + require(['forum/chats'], function (chats) { + chats.switchChat(roomId); + }); + } else { + require(['chat'], function (chat) { + chat.newChat(userObj.uid); + }); + } + }); + }); + } + + return search; +}); diff --git a/public/src/client/compose.js b/public/src/client/compose.js new file mode 100644 index 0000000000..5ac2dd096f --- /dev/null +++ b/public/src/client/compose.js @@ -0,0 +1,18 @@ +'use strict'; + + +define('forum/compose', ['hooks'], function (hooks) { + const Compose = {}; + + Compose.init = function () { + const container = $('.composer'); + + if (container.length) { + hooks.fire('action:composer.enhance', { + container: container, + }); + } + }; + + return Compose; +}); diff --git a/public/src/client/flags/detail.js b/public/src/client/flags/detail.js new file mode 100644 index 0000000000..a4125142ea --- /dev/null +++ b/public/src/client/flags/detail.js @@ -0,0 +1,178 @@ +'use strict'; + +define('forum/flags/detail', [ + 'components', 'translator', 'benchpress', 'forum/account/header', 'accounts/delete', 'api', 'bootbox', 'alerts', +], function (components, translator, Benchpress, AccountHeader, AccountsDelete, api, bootbox, alerts) { + const Detail = {}; + + Detail.init = function () { + // Update attributes + $('#state').val(ajaxify.data.state).removeAttr('disabled'); + $('#assignee').val(ajaxify.data.assignee).removeAttr('disabled'); + + $('#content > div').on('click', '[data-action]', function () { + const action = this.getAttribute('data-action'); + const uid = $(this).parents('[data-uid]').attr('data-uid'); + const noteEl = document.getElementById('note'); + + switch (action) { + case 'assign': + $('#assignee').val(app.user.uid); + // falls through + + case 'update': { + const data = $('#attributes').serializeArray().reduce((memo, cur) => { + memo[cur.name] = cur.value; + return memo; + }, {}); + + api.put(`/flags/${ajaxify.data.flagId}`, data).then(({ history }) => { + alerts.success('[[flags:updated]]'); + Detail.reloadHistory(history); + }).catch(alerts.error); + break; + } + + case 'appendNote': + api.post(`/flags/${ajaxify.data.flagId}/notes`, { + note: noteEl.value, + datetime: parseInt(noteEl.getAttribute('data-datetime'), 10), + }).then((payload) => { + alerts.success('[[flags:note-added]]'); + Detail.reloadNotes(payload.notes); + Detail.reloadHistory(payload.history); + + noteEl.removeAttribute('data-datetime'); + }).catch(alerts.error); + break; + + case 'delete-note': { + const datetime = parseInt(this.closest('[data-datetime]').getAttribute('data-datetime'), 10); + bootbox.confirm('[[flags:delete-note-confirm]]', function (ok) { + if (ok) { + api.delete(`/flags/${ajaxify.data.flagId}/notes/${datetime}`, {}).then((payload) => { + alerts.success('[[flags:note-deleted]]'); + Detail.reloadNotes(payload.notes); + Detail.reloadHistory(payload.history); + }).catch(alerts.error); + } + }); + break; + } + case 'chat': + require(['chat'], function (chat) { + chat.newChat(uid); + }); + break; + + case 'ban': + AccountHeader.banAccount(uid, ajaxify.refresh); + break; + + case 'unban': + AccountHeader.unbanAccount(uid); + break; + + case 'mute': + AccountHeader.muteAccount(uid, ajaxify.refresh); + break; + + case 'unmute': + AccountHeader.unmuteAccount(uid); + break; + + case 'delete-account': + AccountsDelete.account(uid, ajaxify.refresh); + break; + + case 'delete-content': + AccountsDelete.content(uid, ajaxify.refresh); + break; + + case 'delete-all': + AccountsDelete.purge(uid, ajaxify.refresh); + break; + + case 'delete-post': + postAction('delete', api.del, `/posts/${ajaxify.data.target.pid}/state`); + break; + + case 'purge-post': + postAction('purge', api.del, `/posts/${ajaxify.data.target.pid}`); + break; + + case 'restore-post': + postAction('restore', api.put, `/posts/${ajaxify.data.target.pid}/state`); + break; + + case 'prepare-edit': { + const selectedNoteEl = this.closest('[data-index]'); + const index = selectedNoteEl.getAttribute('data-index'); + const textareaEl = document.getElementById('note'); + textareaEl.value = ajaxify.data.notes[index].content; + textareaEl.setAttribute('data-datetime', ajaxify.data.notes[index].datetime); + + const siblings = selectedNoteEl.parentElement.children; + for (const el in siblings) { + if (siblings.hasOwnProperty(el)) { + siblings[el].classList.remove('editing'); + } + } + selectedNoteEl.classList.add('editing'); + textareaEl.focus(); + break; + } + + case 'delete-flag': { + bootbox.confirm('[[flags:delete-flag-confirm]]', function (ok) { + if (ok) { + api.delete(`/flags/${ajaxify.data.flagId}`, {}).then(() => { + alerts.success('[[flags:flag-deleted]]'); + ajaxify.go('flags'); + }).catch(alerts.error); + } + }); + break; + } + } + }); + }; + + function postAction(action, method, path) { + translator.translate('[[topic:post_' + action + '_confirm]]', function (msg) { + bootbox.confirm(msg, function (confirm) { + if (!confirm) { + return; + } + + method(path).then(ajaxify.refresh).catch(alerts.error); + }); + }); + } + + Detail.reloadNotes = function (notes) { + ajaxify.data.notes = notes; + Benchpress.render('flags/detail', { + notes: notes, + }, 'notes').then(function (html) { + const wrapperEl = components.get('flag/notes'); + wrapperEl.empty(); + wrapperEl.html(html); + wrapperEl.find('span.timeago').timeago(); + document.getElementById('note').value = ''; + }); + }; + + Detail.reloadHistory = function (history) { + app.parseAndTranslate('flags/detail', 'history', { + history: history, + }, function (html) { + const wrapperEl = components.get('flag/history'); + wrapperEl.empty(); + wrapperEl.html(html); + wrapperEl.find('span.timeago').timeago(); + }); + }; + + return Detail; +}); diff --git a/public/src/client/flags/list.js b/public/src/client/flags/list.js new file mode 100644 index 0000000000..33ef934c7b --- /dev/null +++ b/public/src/client/flags/list.js @@ -0,0 +1,231 @@ +'use strict'; + +define('forum/flags/list', [ + 'components', 'Chart', 'categoryFilter', 'autocomplete', 'api', 'alerts', +], function (components, Chart, categoryFilter, autocomplete, api, alerts) { + const Flags = {}; + + let selectedCids; + + Flags.init = function () { + Flags.enableFilterForm(); + Flags.enableCheckboxes(); + Flags.handleBulkActions(); + + selectedCids = []; + if (ajaxify.data.filters.hasOwnProperty('cid')) { + selectedCids = Array.isArray(ajaxify.data.filters.cid) ? + ajaxify.data.filters.cid : [ajaxify.data.filters.cid]; + } + + categoryFilter.init($('[component="category/dropdown"]'), { + privilege: 'moderate', + selectedCids: selectedCids, + onHidden: function (data) { + selectedCids = data.selectedCids; + }, + }); + + components.get('flags/list') + .on('click', '[data-flag-id]', function (e) { + if (['BUTTON', 'A'].includes(e.target.nodeName)) { + return; + } + + const flagId = this.getAttribute('data-flag-id'); + ajaxify.go('flags/' + flagId); + }); + + $('#flags-daily-wrapper').one('shown.bs.collapse', function () { + Flags.handleGraphs(); + }); + + autocomplete.user($('#filter-assignee, #filter-targetUid, #filter-reporterId'), (ev, ui) => { + setTimeout(() => { ev.target.value = ui.item.user.uid; }); + }); + }; + + Flags.enableFilterForm = function () { + const $filtersEl = components.get('flags/filters'); + + // Parse ajaxify data to set form values to reflect current filters + for (const filter in ajaxify.data.filters) { + if (ajaxify.data.filters.hasOwnProperty(filter)) { + $filtersEl.find('[name="' + filter + '"]').val(ajaxify.data.filters[filter]); + } + } + $filtersEl.find('[name="sort"]').val(ajaxify.data.sort); + + document.getElementById('apply-filters').addEventListener('click', function () { + const payload = $filtersEl.serializeArray(); + // cid is special comes from categoryFilter module + selectedCids.forEach(function (cid) { + payload.push({ name: 'cid', value: cid }); + }); + + ajaxify.go('flags?' + (payload.length ? $.param(payload) : 'reset=1')); + }); + + $filtersEl.find('button[data-target="#more-filters"]').click((ev) => { + const textVariant = ev.target.getAttribute('data-text-variant'); + if (!textVariant) { + return; + } + ev.target.setAttribute('data-text-variant', ev.target.textContent); + ev.target.firstChild.textContent = textVariant; + }); + }; + + Flags.enableCheckboxes = function () { + const flagsList = document.querySelector('[component="flags/list"]'); + const checkboxes = flagsList.querySelectorAll('[data-flag-id] input[type="checkbox"]'); + const bulkEl = document.querySelector('[component="flags/bulk-actions"] button'); + let lastClicked; + + document.querySelector('[data-action="toggle-all"]').addEventListener('click', function () { + const state = this.checked; + + checkboxes.forEach(function (el) { + el.checked = state; + }); + bulkEl.disabled = !state; + }); + + flagsList.addEventListener('click', function (e) { + const subselector = e.target.closest('input[type="checkbox"]'); + if (subselector) { + // Stop checkbox clicks from going into the flag details + e.stopImmediatePropagation(); + + if (lastClicked && e.shiftKey && lastClicked !== subselector) { + // Select all the checkboxes in between + const state = subselector.checked; + let started = false; + + checkboxes.forEach(function (el) { + if ([subselector, lastClicked].some(function (ref) { + return ref === el; + })) { + started = !started; + } + + if (started) { + el.checked = state; + } + }); + } + + // (De)activate bulk actions button based on checkboxes' state + bulkEl.disabled = !Array.prototype.some.call(checkboxes, function (el) { + return el.checked; + }); + + lastClicked = subselector; + } + + // If you miss the checkbox, don't descend into the flag details, either + if (e.target.querySelector('input[type="checkbox"]')) { + e.stopImmediatePropagation(); + } + }); + }; + + Flags.handleBulkActions = function () { + document.querySelector('[component="flags/bulk-actions"]').addEventListener('click', function (e) { + const subselector = e.target.closest('[data-action]'); + if (subselector) { + const action = subselector.getAttribute('data-action'); + const flagIds = Flags.getSelected(); + const promises = flagIds.map((flagId) => { + const data = {}; + if (action === 'bulk-assign') { + data.assignee = app.user.uid; + } else if (action === 'bulk-mark-resolved') { + data.state = 'resolved'; + } + return api.put(`/flags/${flagId}`, data); + }); + + Promise.allSettled(promises).then(function (results) { + const fulfilled = results.filter(function (res) { + return res.status === 'fulfilled'; + }).length; + const errors = results.filter(function (res) { + return res.status === 'rejected'; + }); + if (fulfilled) { + alerts.success('[[flags:bulk-success, ' + fulfilled + ']]'); + ajaxify.refresh(); + } + + errors.forEach(function (res) { + alerts.error(res.reason); + }); + }); + } + }); + }; + + Flags.getSelected = function () { + const checkboxes = document.querySelectorAll('[component="flags/list"] [data-flag-id] input[type="checkbox"]'); + const payload = []; + checkboxes.forEach(function (el) { + if (el.checked) { + payload.push(el.closest('[data-flag-id]').getAttribute('data-flag-id')); + } + }); + + return payload; + }; + + Flags.handleGraphs = function () { + const dailyCanvas = document.getElementById('flags:daily'); + const dailyLabels = utils.getDaysArray().map(function (text, idx) { + return idx % 3 ? '' : text; + }); + + if (utils.isMobile()) { + Chart.defaults.global.tooltips.enabled = false; + } + const data = { + 'flags:daily': { + labels: dailyLabels, + datasets: [ + { + label: '', + backgroundColor: 'rgba(151,187,205,0.2)', + borderColor: 'rgba(151,187,205,1)', + pointBackgroundColor: 'rgba(151,187,205,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(151,187,205,1)', + data: ajaxify.data.analytics, + }, + ], + }, + }; + + dailyCanvas.width = $(dailyCanvas).parent().width(); + new Chart(dailyCanvas.getContext('2d'), { + type: 'line', + data: data['flags:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + precision: 0, + }, + }], + }, + }, + }); + }; + + return Flags; +}); diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js new file mode 100644 index 0000000000..8053644583 --- /dev/null +++ b/public/src/client/groups/details.js @@ -0,0 +1,303 @@ +'use strict'; + +define('forum/groups/details', [ + 'forum/groups/memberlist', + 'iconSelect', + 'components', + 'coverPhoto', + 'pictureCropper', + 'translator', + 'api', + 'slugify', + 'categorySelector', + 'bootbox', + 'alerts', +], function ( + memberList, + iconSelect, + components, + coverPhoto, + pictureCropper, + translator, + api, + slugify, + categorySelector, + bootbox, + alerts +) { + const Details = {}; + let groupName; + + Details.init = function () { + const detailsPage = components.get('groups/container'); + + groupName = ajaxify.data.group.name; + + if (ajaxify.data.group.isOwner) { + Details.prepareSettings(); + + coverPhoto.init( + components.get('groups/cover'), + function (imageData, position, callback) { + socket.emit('groups.cover.update', { + groupName: groupName, + imageData: imageData, + position: position, + }, callback); + }, + function () { + pictureCropper.show({ + title: '[[groups:upload-group-cover]]', + socketMethod: 'groups.cover.update', + aspectRatio: NaN, + allowSkippingCrop: true, + restrictImageDimension: false, + paramName: 'groupName', + paramValue: groupName, + }, function (imageUrlOnServer) { + imageUrlOnServer = (!imageUrlOnServer.startsWith('http') ? config.relative_path : '') + imageUrlOnServer + '?' + Date.now(); + components.get('groups/cover').css('background-image', 'url(' + imageUrlOnServer + ')'); + }); + }, + removeCover + ); + } + + memberList.init(); + + handleMemberInvitations(); + + components.get('groups/activity').find('.content img:not(.not-responsive)').addClass('img-responsive'); + + detailsPage.on('click', '[data-action]', function () { + const btnEl = $(this); + const userRow = btnEl.parents('[data-uid]'); + const ownerFlagEl = userRow.find('.member-name > i'); + const isOwner = !ownerFlagEl.hasClass('invisible'); + const uid = userRow.attr('data-uid'); + const action = btnEl.attr('data-action'); + + switch (action) { + case 'toggleOwnership': + api[isOwner ? 'del' : 'put'](`/groups/${ajaxify.data.group.slug}/ownership/${uid}`, {}).then(() => { + ownerFlagEl.toggleClass('invisible'); + }).catch(alerts.error); + break; + + case 'kick': + translator.translate('[[groups:details.kick_confirm]]', function (translated) { + bootbox.confirm(translated, function (confirm) { + if (!confirm) { + return; + } + + api.del(`/groups/${ajaxify.data.group.slug}/membership/${uid}`, undefined).then(() => userRow.slideUp().remove()).catch(alerts.error); + }); + }); + break; + + case 'update': + Details.update(); + break; + + case 'delete': + Details.deleteGroup(); + break; + + case 'join': + api.put('/groups/' + ajaxify.data.group.slug + '/membership/' + (uid || app.user.uid), undefined).then(() => ajaxify.refresh()).catch(alerts.error); + break; + + case 'leave': + api.del('/groups/' + ajaxify.data.group.slug + '/membership/' + (uid || app.user.uid), undefined).then(() => ajaxify.refresh()).catch(alerts.error); + break; + + // TODO (14/10/2020): rewrite these to use api module and merge with above 2 case blocks + case 'accept': // intentional fall-throughs! + case 'reject': + case 'issueInvite': + case 'rescindInvite': + case 'acceptInvite': + case 'rejectInvite': + case 'acceptAll': + case 'rejectAll': + socket.emit('groups.' + action, { + toUid: uid, + groupName: groupName, + }, function (err) { + if (!err) { + ajaxify.refresh(); + } else { + alerts.error(err); + } + }); + break; + } + }); + }; + + Details.prepareSettings = function () { + const settingsFormEl = components.get('groups/settings'); + const labelColorValueEl = settingsFormEl.find('[name="labelColor"]'); + const textColorValueEl = settingsFormEl.find('[name="textColor"]'); + const iconBtn = settingsFormEl.find('[data-action="icon-select"]'); + const previewEl = settingsFormEl.find('.label'); + const previewElText = settingsFormEl.find('.label-text'); + const previewIcon = previewEl.find('i'); + const userTitleEl = settingsFormEl.find('[name="userTitle"]'); + const userTitleEnabledEl = settingsFormEl.find('[name="userTitleEnabled"]'); + const iconValueEl = settingsFormEl.find('[name="icon"]'); + + labelColorValueEl.on('input', function () { + previewEl.css('background-color', labelColorValueEl.val()); + }); + + textColorValueEl.on('input', function () { + previewEl.css('color', textColorValueEl.val()); + }); + + // Add icon selection interface + iconBtn.on('click', function () { + iconSelect.init(previewIcon, function () { + iconValueEl.val(previewIcon.val()); + }); + }); + + // If the user title changes, update that too + userTitleEl.on('keyup', function () { + previewElText.translateText((this.value || settingsFormEl.find('#name').val())); + }); + + // Disable user title customisation options if the the user title itself is disabled + userTitleEnabledEl.on('change', function () { + const customOpts = components.get('groups/userTitleOption'); + + if (this.checked) { + customOpts.removeAttr('disabled'); + previewEl.removeClass('hide'); + } else { + customOpts.attr('disabled', 'disabled'); + previewEl.addClass('hide'); + } + }); + + const cidSelector = categorySelector.init($('.member-post-cids-selector [component="category-selector"]'), { + onSelect: function (selectedCategory) { + let cids = ($('#memberPostCids').val() || '').split(',').map(cid => parseInt(cid, 10)); + cids.push(selectedCategory.cid); + cids = cids.filter((cid, index, array) => array.indexOf(cid) === index); + $('#memberPostCids').val(cids.join(',')); + cidSelector.selectCategory(0); + }, + }); + }; + + Details.update = function () { + const settingsFormEl = components.get('groups/settings'); + const checkboxes = settingsFormEl.find('input[type="checkbox"][name]'); + + if (settingsFormEl.length) { + const settings = settingsFormEl.serializeObject(); + + // serializeObject doesnt return array for multi selects if only one item is selected + if (!Array.isArray(settings.memberPostCids)) { + settings.memberPostCids = $('#memberPostCids').val(); + } + + // Fix checkbox values + checkboxes.each(function (idx, inputEl) { + inputEl = $(inputEl); + if (inputEl.length) { + settings[inputEl.attr('name')] = inputEl.prop('checked'); + } + }); + + api.put(`/groups/${ajaxify.data.group.slug}`, settings).then(() => { + if (settings.name) { + let pathname = window.location.pathname; + pathname = pathname.slice(1, pathname.lastIndexOf('/') + 1); + ajaxify.go(pathname + slugify(settings.name)); + } else { + ajaxify.refresh(); + } + + alerts.success('[[groups:event.updated]]'); + }).catch(alerts.error); + } + }; + + Details.deleteGroup = function () { + bootbox.confirm('Are you sure you want to delete the group: ' + utils.escapeHTML(groupName), function (confirm) { + if (confirm) { + bootbox.prompt('Please enter the name of this group in order to delete it:', function (response) { + if (response === groupName) { + api.del(`/groups/${ajaxify.data.group.slug}`, {}).then(() => { + alerts.success('[[groups:event.deleted, ' + utils.escapeHTML(groupName) + ']]'); + ajaxify.go('groups'); + }).catch(alerts.error); + } + }); + } + }); + }; + + function handleMemberInvitations() { + if (!ajaxify.data.group.isOwner) { + return; + } + + const searchInput = $('[component="groups/members/invite"]'); + require(['autocomplete'], function (autocomplete) { + autocomplete.user(searchInput, function (event, selected) { + socket.emit('groups.issueInvite', { + toUid: selected.item.user.uid, + groupName: ajaxify.data.group.name, + }, function (err) { + if (err) { + return alerts.error(err); + } + ajaxify.refresh(); + }); + }); + }); + + $('[component="groups/members/bulk-invite-button"]').on('click', function () { + const usernames = $('[component="groups/members/bulk-invite"]').val(); + if (!usernames) { + return false; + } + socket.emit('groups.issueMassInvite', { + usernames: usernames, + groupName: ajaxify.data.group.name, + }, function (err) { + if (err) { + return alerts.error(err); + } + ajaxify.refresh(); + }); + return false; + }); + } + + function removeCover() { + translator.translate('[[groups:remove_group_cover_confirm]]', function (translated) { + bootbox.confirm(translated, function (confirm) { + if (!confirm) { + return; + } + + socket.emit('groups.cover.remove', { + groupName: ajaxify.data.group.name, + }, function (err) { + if (!err) { + ajaxify.refresh(); + } else { + alerts.error(err); + } + }); + }); + }); + } + + return Details; +}); diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js new file mode 100644 index 0000000000..871905e454 --- /dev/null +++ b/public/src/client/groups/list.js @@ -0,0 +1,90 @@ +'use strict'; + +define('forum/groups/list', [ + 'forum/infinitescroll', 'benchpress', 'api', 'bootbox', 'alerts', +], function (infinitescroll, Benchpress, api, bootbox, alerts) { + const Groups = {}; + + Groups.init = function () { + infinitescroll.init(Groups.loadMoreGroups); + + // Group creation + $('button[data-action="new"]').on('click', function () { + bootbox.prompt('[[groups:new-group.group_name]]', function (name) { + if (name && name.length) { + api.post('/groups', { + name: name, + }).then((res) => { + ajaxify.go('groups/' + res.slug); + }).catch(alerts.error); + } + }); + }); + const params = utils.params(); + $('#search-sort').val(params.sort || 'alpha'); + + // Group searching + $('#search-text').on('keyup', Groups.search); + $('#search-button').on('click', Groups.search); + $('#search-sort').on('change', function () { + ajaxify.go('groups?sort=' + $('#search-sort').val()); + }); + }; + + Groups.loadMoreGroups = function (direction) { + if (direction < 0) { + return; + } + + infinitescroll.loadMore('groups.loadMore', { + sort: $('#search-sort').val(), + after: $('[component="groups/container"]').attr('data-nextstart'), + }, function (data, done) { + if (data && data.groups.length) { + Benchpress.render('partials/groups/list', { + groups: data.groups, + }).then(function (html) { + $('#groups-list').append(html); + done(); + }); + } else { + done(); + } + + if (data && data.nextStart) { + $('[component="groups/container"]').attr('data-nextstart', data.nextStart); + } + }); + }; + + Groups.search = function () { + const groupsEl = $('#groups-list'); + const queryEl = $('#search-text'); + const sortEl = $('#search-sort'); + + socket.emit('groups.search', { + query: queryEl.val(), + options: { + sort: sortEl.val(), + filterHidden: true, + showMembers: true, + hideEphemeralGroups: true, + }, + }, function (err, groups) { + if (err) { + return alerts.error(err); + } + groups = groups.filter(function (group) { + return group.name !== 'registered-users' && group.name !== 'guests'; + }); + Benchpress.render('partials/groups/list', { + groups: groups, + }).then(function (html) { + groupsEl.empty().append(html); + }); + }); + return false; + }; + + return Groups; +}); diff --git a/public/src/client/groups/memberlist.js b/public/src/client/groups/memberlist.js new file mode 100644 index 0000000000..e716306f56 --- /dev/null +++ b/public/src/client/groups/memberlist.js @@ -0,0 +1,167 @@ +'use strict'; + +define('forum/groups/memberlist', ['api', 'bootbox', 'alerts'], function (api, bootbox, alerts) { + const MemberList = {}; + let groupName; + let templateName; + + MemberList.init = function (_templateName) { + templateName = _templateName || 'groups/details'; + groupName = ajaxify.data.group.name; + + handleMemberAdd(); + handleMemberSearch(); + handleMemberInfiniteScroll(); + }; + + function handleMemberAdd() { + $('[component="groups/members/add"]').on('click', function () { + app.parseAndTranslate('admin/partials/groups/add-members', {}, function (html) { + const foundUsers = []; + const modal = bootbox.dialog({ + title: '[[groups:details.add-member]]', + message: html, + buttons: { + ok: { + callback: function () { + const users = []; + modal.find('[data-uid][data-selected]').each(function (index, el) { + users.push(foundUsers[$(el).attr('data-uid')]); + }); + addUserToGroup(users, function () { + modal.modal('hide'); + }); + }, + }, + }, + }); + modal.on('click', '[data-username]', function () { + const isSelected = $(this).attr('data-selected') === '1'; + if (isSelected) { + $(this).removeAttr('data-selected'); + } else { + $(this).attr('data-selected', 1); + } + $(this).find('i').toggleClass('invisible'); + }); + modal.find('input').on('keyup', function () { + api.get('/api/users', { + query: $(this).val(), + paginate: false, + }, function (err, result) { + if (err) { + return alerts.error(err); + } + result.users.forEach(function (user) { + foundUsers[user.uid] = user; + }); + app.parseAndTranslate('admin/partials/groups/add-members', 'users', { users: result.users }, function (html) { + modal.find('#search-result').html(html); + }); + }); + }); + }); + }); + } + + function addUserToGroup(users, callback) { + function done() { + users = users.filter(function (user) { + return !$('[component="groups/members"] [data-uid="' + user.uid + '"]').length; + }); + parseAndTranslate(users, function (html) { + $('[component="groups/members"] tbody').prepend(html); + }); + callback(); + } + const uids = users.map(function (user) { return user.uid; }); + if (groupName === 'administrators') { + socket.emit('admin.user.makeAdmins', uids, function (err) { + if (err) { + return alerts.error(err); + } + done(); + }); + } else { + Promise.all(uids.map(uid => api.put('/groups/' + ajaxify.data.group.slug + '/membership/' + uid))).then(done).catch(alerts.error); + } + } + + function handleMemberSearch() { + const searchEl = $('[component="groups/members/search"]'); + searchEl.on('keyup', utils.debounce(function () { + const query = searchEl.val(); + socket.emit('groups.searchMembers', { + groupName: groupName, + query: query, + }, function (err, results) { + if (err) { + return alerts.error(err); + } + parseAndTranslate(results.users, function (html) { + $('[component="groups/members"] tbody').html(html); + $('[component="groups/members"]').attr('data-nextstart', 20); + }); + }); + }, 250)); + } + + function handleMemberInfiniteScroll() { + $('[component="groups/members"] tbody').on('scroll', function () { + const $this = $(this); + const bottom = ($this[0].scrollHeight - $this.innerHeight()) * 0.9; + + if ($this.scrollTop() > bottom && !$('[component="groups/members/search"]').val()) { + loadMoreMembers(); + } + }); + } + + function loadMoreMembers() { + const members = $('[component="groups/members"]'); + if (members.attr('loading')) { + return; + } + + members.attr('loading', 1); + socket.emit('groups.loadMoreMembers', { + groupName: groupName, + after: members.attr('data-nextstart'), + }, function (err, data) { + if (err) { + return alerts.error(err); + } + + if (data && data.users.length) { + onMembersLoaded(data.users, function () { + members.removeAttr('loading'); + members.attr('data-nextstart', data.nextStart); + }); + } else { + members.removeAttr('loading'); + } + }); + } + + function onMembersLoaded(users, callback) { + users = users.filter(function (user) { + return !$('[component="groups/members"] [data-uid="' + user.uid + '"]').length; + }); + + parseAndTranslate(users, function (html) { + $('[component="groups/members"] tbody').append(html); + callback(); + }); + } + + function parseAndTranslate(users, callback) { + app.parseAndTranslate(templateName, 'group.members', { + group: { + members: users, + isOwner: ajaxify.data.group.isOwner, + }, + }, callback); + } + + return MemberList; +}); diff --git a/public/src/client/header.js b/public/src/client/header.js new file mode 100644 index 0000000000..fdfc969cd2 --- /dev/null +++ b/public/src/client/header.js @@ -0,0 +1,79 @@ +'use strict'; + +define('forum/header', [ + 'forum/header/unread', + 'forum/header/notifications', + 'forum/header/chat', + 'alerts', +], function (unread, notifications, chat, alerts) { + const module = {}; + + module.prepareDOM = function () { + if (app.user.uid > 0) { + unread.initUnreadTopics(); + } + notifications.prepareDOM(); + chat.prepareDOM(); + handleStatusChange(); + createHeaderTooltips(); + handleLogout(); + }; + + function handleStatusChange() { + $('[component="header/usercontrol"] [data-status]').off('click').on('click', function (e) { + const status = $(this).attr('data-status'); + socket.emit('user.setStatus', status, function (err) { + if (err) { + return alerts.error(err); + } + $('[data-uid="' + app.user.uid + '"] [component="user/status"], [component="header/profilelink"] [component="user/status"]') + .removeClass('away online dnd offline') + .addClass(status); + $('[component="header/usercontrol"] [data-status]').each(function () { + $(this).find('span').toggleClass('bold', $(this).attr('data-status') === status); + }); + app.user.status = status; + }); + e.preventDefault(); + }); + } + + function createHeaderTooltips() { + const env = utils.findBootstrapEnvironment(); + if (env === 'xs' || env === 'sm' || utils.isTouchDevice()) { + return; + } + $('#header-menu li a[title]').each(function () { + $(this).tooltip({ + placement: 'bottom', + trigger: 'hover', + title: $(this).attr('title'), + }); + }); + + + $('#search-form').tooltip({ + placement: 'bottom', + trigger: 'hover', + title: $('#search-button i').attr('title'), + }); + + + $('#user_dropdown').tooltip({ + placement: 'bottom', + trigger: 'hover', + title: $('#user_dropdown').attr('title'), + }); + } + + function handleLogout() { + $('#header-menu .container').on('click', '[component="user/logout"]', function () { + require(['logout'], function (logout) { + logout(); + }); + return false; + }); + } + + return module; +}); diff --git a/public/src/client/header/chat.js b/public/src/client/header/chat.js new file mode 100644 index 0000000000..3eb2856cc5 --- /dev/null +++ b/public/src/client/header/chat.js @@ -0,0 +1,56 @@ +'use strict'; + +define('forum/header/chat', ['components'], function (components) { + const chat = {}; + + chat.prepareDOM = function () { + const chatsToggleEl = components.get('chat/dropdown'); + const chatsListEl = components.get('chat/list'); + + chatsToggleEl.on('click', function () { + if (chatsToggleEl.parent().hasClass('open')) { + return; + } + requireAndCall('loadChatsDropdown', chatsListEl); + }); + + if (chatsToggleEl.parents('.dropdown').hasClass('open')) { + requireAndCall('loadChatsDropdown', chatsListEl); + } + + socket.removeListener('event:chats.receive', onChatMessageReceived); + socket.on('event:chats.receive', onChatMessageReceived); + + socket.removeListener('event:user_status_change', onUserStatusChange); + socket.on('event:user_status_change', onUserStatusChange); + + socket.removeListener('event:chats.roomRename', onRoomRename); + socket.on('event:chats.roomRename', onRoomRename); + + socket.on('event:unread.updateChatCount', function (count) { + components.get('chat/icon') + .toggleClass('unread-count', count > 0) + .attr('data-content', count > 99 ? '99+' : count); + }); + }; + + function onChatMessageReceived(data) { + requireAndCall('onChatMessageReceived', data); + } + + function onUserStatusChange(data) { + requireAndCall('onUserStatusChange', data); + } + + function onRoomRename(data) { + requireAndCall('onRoomRename', data); + } + + function requireAndCall(method, param) { + require(['chat'], function (chat) { + chat[method](param); + }); + } + + return chat; +}); diff --git a/public/src/client/header/notifications.js b/public/src/client/header/notifications.js new file mode 100644 index 0000000000..dbccf83ac7 --- /dev/null +++ b/public/src/client/header/notifications.js @@ -0,0 +1,46 @@ +'use strict'; + +define('forum/header/notifications', ['components'], function (components) { + const notifications = {}; + + notifications.prepareDOM = function () { + const notifContainer = components.get('notifications'); + const notifTrigger = notifContainer.children('a'); + const notifList = components.get('notifications/list'); + + notifTrigger.on('click', function (e) { + e.preventDefault(); + if (notifContainer.hasClass('open')) { + return; + } + + requireAndCall('loadNotifications', notifList); + }); + + if (notifTrigger.parents('.dropdown').hasClass('open')) { + requireAndCall('loadNotifications', notifList); + } + + socket.removeListener('event:new_notification', onNewNotification); + socket.on('event:new_notification', onNewNotification); + + socket.removeListener('event:notifications.updateCount', onUpdateCount); + socket.on('event:notifications.updateCount', onUpdateCount); + }; + + function onNewNotification(data) { + requireAndCall('onNewNotification', data); + } + + function onUpdateCount(data) { + requireAndCall('updateNotifCount', data); + } + + function requireAndCall(method, param) { + require(['notifications'], function (notifications) { + notifications[method](param); + }); + } + + return notifications; +}); diff --git a/public/src/client/header/unread.js b/public/src/client/header/unread.js new file mode 100644 index 0000000000..f464df7182 --- /dev/null +++ b/public/src/client/header/unread.js @@ -0,0 +1,96 @@ +'use strict'; + +define('forum/header/unread', function () { + const unread = {}; + const watchStates = { + ignoring: 1, + notwatching: 2, + watching: 3, + }; + + unread.initUnreadTopics = function () { + const unreadTopics = app.user.unreadData; + + function onNewPost(data) { + if (data && data.posts && data.posts.length && unreadTopics) { + const post = data.posts[0]; + if (parseInt(post.uid, 10) === parseInt(app.user.uid, 10) || + (!post.topic.isFollowing && post.categoryWatchState !== watchStates.watching) + ) { + return; + } + + const tid = post.topic.tid; + if (!unreadTopics[''][tid] || !unreadTopics.new[tid] || + !unreadTopics.watched[tid] || !unreadTopics.unreplied[tid]) { + markTopicsUnread(tid); + } + + if (!unreadTopics[''][tid]) { + increaseUnreadCount(''); + unreadTopics[''][tid] = true; + } + const isNewTopic = post.isMain && parseInt(post.uid, 10) !== parseInt(app.user.uid, 10); + if (isNewTopic && !unreadTopics.new[tid]) { + increaseUnreadCount('new'); + unreadTopics.new[tid] = true; + } + const isUnreplied = parseInt(post.topic.postcount, 10) <= 1; + if (isUnreplied && !unreadTopics.unreplied[tid]) { + increaseUnreadCount('unreplied'); + unreadTopics.unreplied[tid] = true; + } + + if (post.topic.isFollowing && !unreadTopics.watched[tid]) { + increaseUnreadCount('watched'); + unreadTopics.watched[tid] = true; + } + } + } + + function increaseUnreadCount(filter) { + const unreadUrl = '/unread' + (filter ? '?filter=' + filter : ''); + const newCount = 1 + parseInt($('a[href="' + config.relative_path + unreadUrl + '"].navigation-link i').attr('data-content'), 10); + updateUnreadTopicCount(unreadUrl, newCount); + } + + function markTopicsUnread(tid) { + $('[data-tid="' + tid + '"]').addClass('unread'); + } + + $(window).on('action:ajaxify.end', function () { + if (ajaxify.data.template.topic) { + ['', 'new', 'watched', 'unreplied'].forEach(function (filter) { + delete unreadTopics[filter][ajaxify.data.tid]; + }); + } + }); + socket.removeListener('event:new_post', onNewPost); + socket.on('event:new_post', onNewPost); + + socket.removeListener('event:unread.updateCount', updateUnreadCounters); + socket.on('event:unread.updateCount', updateUnreadCounters); + }; + + function updateUnreadCounters(data) { + updateUnreadTopicCount('/unread', data.unreadTopicCount); + updateUnreadTopicCount('/unread?filter=new', data.unreadNewTopicCount); + updateUnreadTopicCount('/unread?filter=watched', data.unreadWatchedTopicCount); + updateUnreadTopicCount('/unread?filter=unreplied', data.unreadUnrepliedTopicCount); + } + + function updateUnreadTopicCount(url, count) { + if (!utils.isNumber(count)) { + return; + } + + $('a[href="' + config.relative_path + url + '"].navigation-link i') + .toggleClass('unread-count', count > 0) + .attr('data-content', count > 99 ? '99+' : count); + + $('#mobile-menu [data-unread-url="' + url + '"]').attr('data-content', count > 99 ? '99+' : count); + } + unread.updateUnreadTopicCount = updateUnreadTopicCount; + + return unread; +}); diff --git a/public/src/client/infinitescroll.js b/public/src/client/infinitescroll.js new file mode 100644 index 0000000000..ab5c64a493 --- /dev/null +++ b/public/src/client/infinitescroll.js @@ -0,0 +1,124 @@ +'use strict'; + + +define('forum/infinitescroll', ['hooks', 'alerts'], function (hooks, alerts) { + const scroll = {}; + let callback; + let previousScrollTop = 0; + let loadingMore = false; + let container; + let scrollTimeout = 0; + + scroll.init = function (el, cb) { + const $body = $('body'); + if (typeof el === 'function') { + callback = el; + container = $body; + } else { + callback = cb; + container = el || $body; + } + previousScrollTop = $(window).scrollTop(); + $(window).off('scroll', startScrollTimeout).on('scroll', startScrollTimeout); + + if ($body.height() <= $(window).height()) { + callback(1); + } + }; + + function startScrollTimeout() { + if (scrollTimeout) { + clearTimeout(scrollTimeout); + } + scrollTimeout = setTimeout(function () { + scrollTimeout = 0; + onScroll(); + }, 60); + } + + function onScroll() { + const bsEnv = utils.findBootstrapEnvironment(); + const mobileComposerOpen = (bsEnv === 'xs' || bsEnv === 'sm') && $('html').hasClass('composing'); + if (loadingMore || mobileComposerOpen) { + return; + } + const currentScrollTop = $(window).scrollTop(); + const wh = $(window).height(); + const viewportHeight = container.height() - wh; + const offsetTop = container.offset() ? container.offset().top : 0; + const scrollPercent = 100 * (currentScrollTop - offsetTop) / (viewportHeight <= 0 ? wh : viewportHeight); + + const top = 15; + const bottom = 85; + const direction = currentScrollTop > previousScrollTop ? 1 : -1; + + if (scrollPercent < top && currentScrollTop < previousScrollTop) { + callback(direction); + } else if (scrollPercent > bottom && currentScrollTop > previousScrollTop) { + callback(direction); + } else if (scrollPercent < 0 && direction > 0 && viewportHeight < 0) { + callback(direction); + } + + previousScrollTop = currentScrollTop; + } + + scroll.loadMore = function (method, data, callback) { + if (loadingMore) { + return; + } + loadingMore = true; + + const hookData = { method: method, data: data }; + hooks.fire('action:infinitescroll.loadmore', hookData); + + socket.emit(hookData.method, hookData.data, function (err, data) { + if (err) { + loadingMore = false; + return alerts.error(err); + } + callback(data, function () { + loadingMore = false; + }); + }); + }; + + scroll.loadMoreXhr = function (data, callback) { + if (loadingMore) { + return; + } + loadingMore = true; + const url = config.relative_path + '/api' + location.pathname.replace(new RegExp('^' + config.relative_path), ''); + const hookData = { url: url, data: data }; + hooks.fire('action:infinitescroll.loadmore.xhr', hookData); + + $.get(url, data, function (data) { + callback(data, function () { + loadingMore = false; + }); + }).fail(function (jqXHR) { + loadingMore = false; + alerts.error(String(jqXHR.responseJSON || jqXHR.statusText)); + }); + }; + + scroll.removeExtra = function (els, direction, count) { + let removedEls = $(); + if (els.length <= count) { + return removedEls; + } + + const removeCount = els.length - count; + if (direction > 0) { + const height = $(document).height(); + const scrollTop = $(window).scrollTop(); + removedEls = els.slice(0, removeCount).remove(); + $(window).scrollTop(scrollTop + ($(document).height() - height)); + } else { + removedEls = els.slice(els.length - removeCount).remove(); + } + return removedEls; + }; + + return scroll; +}); diff --git a/public/src/client/ip-blacklist.js b/public/src/client/ip-blacklist.js new file mode 100644 index 0000000000..7649bdf3ff --- /dev/null +++ b/public/src/client/ip-blacklist.js @@ -0,0 +1,134 @@ +'use strict'; + + +define('forum/ip-blacklist', ['Chart', 'benchpress', 'bootbox', 'alerts'], function (Chart, Benchpress, bootbox, alerts) { + const Blacklist = {}; + + Blacklist.init = function () { + const blacklist = $('#blacklist-rules'); + + blacklist.on('keyup', function () { + $('#blacklist-rules-holder').val(blacklist.val()); + }); + + $('[data-action="apply"]').on('click', function () { + socket.emit('blacklist.save', blacklist.val(), function (err) { + if (err) { + return alerts.error(err); + } + alerts.alert({ + type: 'success', + alert_id: 'blacklist-saved', + title: '[[ip-blacklist:alerts.applied-success]]', + }); + }); + }); + + $('[data-action="test"]').on('click', function () { + socket.emit('blacklist.validate', { + rules: blacklist.val(), + }, function (err, data) { + if (err) { + return alerts.error(err); + } + + Benchpress.render('admin/partials/blacklist-validate', data).then(function (html) { + bootbox.alert(html); + }); + }); + }); + + Blacklist.setupAnalytics(); + }; + + Blacklist.setupAnalytics = function () { + const hourlyCanvas = document.getElementById('blacklist:hourly'); + const dailyCanvas = document.getElementById('blacklist:daily'); + const hourlyLabels = utils.getHoursArray().map(function (text, idx) { + return idx % 3 ? '' : text; + }); + const dailyLabels = utils.getDaysArray().slice(-7).map(function (text, idx) { + return idx % 3 ? '' : text; + }); + + if (utils.isMobile()) { + Chart.defaults.global.tooltips.enabled = false; + } + + const data = { + 'blacklist:hourly': { + labels: hourlyLabels, + datasets: [ + { + label: '', + backgroundColor: 'rgba(186,139,175,0.2)', + borderColor: 'rgba(186,139,175,1)', + pointBackgroundColor: 'rgba(186,139,175,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(186,139,175,1)', + data: ajaxify.data.analytics.hourly, + }, + ], + }, + 'blacklist:daily': { + labels: dailyLabels, + datasets: [ + { + label: '', + backgroundColor: 'rgba(151,187,205,0.2)', + borderColor: 'rgba(151,187,205,1)', + pointBackgroundColor: 'rgba(151,187,205,1)', + pointHoverBackgroundColor: '#fff', + pointBorderColor: '#fff', + pointHoverBorderColor: 'rgba(151,187,205,1)', + data: ajaxify.data.analytics.daily, + }, + ], + }, + }; + + hourlyCanvas.width = $(hourlyCanvas).parent().width(); + dailyCanvas.width = $(dailyCanvas).parent().width(); + + new Chart(hourlyCanvas.getContext('2d'), { + type: 'line', + data: data['blacklist:hourly'], + options: { + responsive: true, + animation: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + }, + }], + }, + }, + }); + + new Chart(dailyCanvas.getContext('2d'), { + type: 'line', + data: data['blacklist:daily'], + options: { + responsive: true, + animation: false, + legend: { + display: false, + }, + scales: { + yAxes: [{ + ticks: { + beginAtZero: true, + }, + }], + }, + }, + }); + }; + + return Blacklist; +}); diff --git a/public/src/client/login.js b/public/src/client/login.js new file mode 100644 index 0000000000..d2924cfa39 --- /dev/null +++ b/public/src/client/login.js @@ -0,0 +1,111 @@ +'use strict'; + + +define('forum/login', ['hooks', 'translator', 'jquery-form'], function (hooks, translator) { + const Login = { + _capsState: false, + }; + + Login.init = function () { + const errorEl = $('#login-error-notify'); + const submitEl = $('#login'); + const formEl = $('#login-form'); + + submitEl.on('click', function (e) { + e.preventDefault(); + + if (!$('#username').val() || !$('#password').val()) { + errorEl.find('p').translateText('[[error:invalid-username-or-password]]'); + errorEl.show(); + } else { + errorEl.hide(); + + if (submitEl.hasClass('disabled')) { + return; + } + + submitEl.addClass('disabled'); + + hooks.fire('action:app.login'); + formEl.ajaxSubmit({ + headers: { + 'x-csrf-token': config.csrf_token, + }, + beforeSend: function () { + app.flags._login = true; + }, + success: function (data) { + hooks.fire('action:app.loggedIn', data); + const pathname = utils.urlToLocation(data.next).pathname; + const params = utils.params({ url: data.next }); + params.loggedin = true; + delete params.register; // clear register message incase it exists + const qs = decodeURIComponent($.param(params)); + + window.location.href = pathname + '?' + qs; + }, + error: function (data) { + let message = data.responseText; + const errInfo = data.responseJSON; + if (data.status === 403 && data.responseText === 'Forbidden') { + window.location.href = config.relative_path + '/login?error=csrf-invalid'; + } else if (errInfo && errInfo.hasOwnProperty('banned_until')) { + message = errInfo.banned_until ? + translator.compile('error:user-banned-reason-until', (new Date(errInfo.banned_until).toLocaleString()), errInfo.reason) : + '[[error:user-banned-reason, ' + errInfo.reason + ']]'; + } + errorEl.find('p').translateText(message); + errorEl.show(); + submitEl.removeClass('disabled'); + + // Select the entire password if that field has focus + if ($('#password:focus').length) { + $('#password').select(); + } + }, + }); + } + }); + + // Guard against caps lock + Login.capsLockCheck(document.querySelector('#password'), document.querySelector('#caps-lock-warning')); + + $('#login-error-notify button').on('click', function (e) { + e.preventDefault(); + errorEl.hide(); + return false; + }); + + if ($('#content #username').val()) { + $('#content #password').val('').focus(); + } else { + $('#content #username').focus(); + } + $('#content #noscript').val('false'); + }; + + Login.capsLockCheck = (inputEl, warningEl) => { + const toggle = (state) => { + warningEl.classList[state ? 'remove' : 'add']('hidden'); + warningEl.parentNode.classList[state ? 'add' : 'remove']('has-warning'); + }; + if (!inputEl) { + return; + } + inputEl.addEventListener('keyup', function (e) { + if (Login._capsState && e.key === 'CapsLock') { + toggle(false); + Login._capsState = !Login._capsState; + return; + } + Login._capsState = e.getModifierState && e.getModifierState('CapsLock'); + toggle(Login._capsState); + }); + + if (Login._capsState) { + toggle(true); + } + }; + + return Login; +}); diff --git a/public/src/client/notifications.js b/public/src/client/notifications.js new file mode 100644 index 0000000000..9f99a80b10 --- /dev/null +++ b/public/src/client/notifications.js @@ -0,0 +1,30 @@ +'use strict'; + + +define('forum/notifications', ['components', 'alerts'], function (components, alerts) { + const Notifications = {}; + + Notifications.init = function () { + const listEl = $('.notifications-list'); + listEl.on('click', '[component="notifications/item/link"]', function () { + const nid = $(this).parents('[data-nid]').attr('data-nid'); + socket.emit('notifications.markRead', nid, function (err) { + if (err) { + return alerts.error(err); + } + }); + }); + + components.get('notifications/mark_all').on('click', function () { + socket.emit('notifications.markAllRead', function (err) { + if (err) { + return alerts.error(err); + } + + components.get('notifications/item').removeClass('unread'); + }); + }); + }; + + return Notifications; +}); diff --git a/public/src/client/pagination.js b/public/src/client/pagination.js new file mode 100644 index 0000000000..665f3ea20a --- /dev/null +++ b/public/src/client/pagination.js @@ -0,0 +1,39 @@ +'use strict'; + + +define('forum/pagination', ['bootbox'], function (bootbox) { + const pagination = {}; + + pagination.init = function () { + $('body').on('click', '[component="pagination/select-page"]', function () { + bootbox.prompt('[[global:enter_page_number]]', function (pageNum) { + pagination.loadPage(pageNum); + }); + return false; + }); + }; + + pagination.loadPage = function (page, callback) { + callback = callback || function () {}; + page = parseInt(page, 10); + if (!utils.isNumber(page) || page < 1 || page > ajaxify.data.pagination.pageCount) { + return; + } + + const query = utils.params(); + query.page = page; + + const url = window.location.pathname + '?' + $.param(query); + ajaxify.go(url, callback); + }; + + pagination.nextPage = function (callback) { + pagination.loadPage(ajaxify.data.pagination.currentPage + 1, callback); + }; + + pagination.previousPage = function (callback) { + pagination.loadPage(ajaxify.data.pagination.currentPage - 1, callback); + }; + + return pagination; +}); diff --git a/public/src/client/popular.js b/public/src/client/popular.js new file mode 100644 index 0000000000..a34c1d19f4 --- /dev/null +++ b/public/src/client/popular.js @@ -0,0 +1,14 @@ +'use strict'; + + +define('forum/popular', ['topicList'], function (topicList) { + const Popular = {}; + + Popular.init = function () { + app.enterRoom('popular_topics'); + + topicList.init('popular'); + }; + + return Popular; +}); diff --git a/public/src/client/post-queue.js b/public/src/client/post-queue.js new file mode 100644 index 0000000000..30bc1b7da4 --- /dev/null +++ b/public/src/client/post-queue.js @@ -0,0 +1,185 @@ +'use strict'; + + +define('forum/post-queue', [ + 'categoryFilter', 'categorySelector', 'api', 'alerts', 'bootbox', +], function (categoryFilter, categorySelector, api, alerts, bootbox) { + const PostQueue = {}; + + PostQueue.init = function () { + $('[data-toggle="tooltip"]').tooltip(); + + categoryFilter.init($('[component="category/dropdown"]'), { + privilege: 'moderate', + }); + + handleBulkActions(); + + $('.posts-list').on('click', '[data-action]', async function () { + function getMessage() { + return new Promise((resolve) => { + const modal = bootbox.dialog({ + title: '[[post-queue:notify-user]]', + message: '', + buttons: { + OK: { + label: '[[modules:bootbox.send]]', + callback: function () { + const val = modal.find('textarea').val(); + if (val) { + resolve(val); + } + }, + }, + }, + }); + }); + } + + const parent = $(this).parents('[data-id]'); + const action = $(this).attr('data-action'); + const id = parent.attr('data-id'); + const listContainer = parent.get(0).parentNode; + + if ((!['accept', 'reject', 'notify'].includes(action)) || (action === 'reject' && !await confirmReject('[[post-queue:confirm-reject]]'))) { + return; + } + + socket.emit('posts.' + action, { + id: id, + message: action === 'notify' ? await getMessage() : undefined, + }, function (err) { + if (err) { + return alerts.error(err); + } + if (action === 'accept' || action === 'reject') { + parent.remove(); + } + + if (listContainer.childElementCount === 0) { + if (ajaxify.data.singlePost) { + ajaxify.go('/post-queue' + window.location.search); + } else { + ajaxify.refresh(); + } + } + }); + return false; + }); + + handleContentEdit('.post-content', '.post-content-editable', 'textarea'); + handleContentEdit('.topic-title', '.topic-title-editable', 'input'); + + $('.posts-list').on('click', '.topic-category[data-editable]', function () { + const $this = $(this); + const id = $this.parents('[data-id]').attr('data-id'); + categorySelector.modal({ + onSubmit: function (selectedCategory) { + Promise.all([ + api.get(`/categories/${selectedCategory.cid}`, {}), + socket.emit('posts.editQueuedContent', { + id: id, + cid: selectedCategory.cid, + }), + ]).then(function (result) { + const category = result[0]; + app.parseAndTranslate('post-queue', 'posts', { + posts: [{ + category: category, + }], + }, function (html) { + if ($this.find('.category-text').length) { + $this.find('.category-text').text(html.find('.topic-category .category-text').text()); + } else { + // for backwards compatibility, remove in 1.16.0 + $this.replaceWith(html.find('.topic-category')); + } + }); + }).catch(alerts.error); + }, + }); + return false; + }); + + $('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + }; + + function confirmReject(msg) { + return new Promise((resolve) => { + bootbox.confirm(msg, resolve); + }); + } + + function handleContentEdit(displayClass, editableClass, inputSelector) { + $('.posts-list').on('click', displayClass, function () { + const el = $(this); + const inputEl = el.parent().find(editableClass); + if (inputEl.length) { + el.addClass('hidden'); + inputEl.removeClass('hidden').find(inputSelector).focus(); + } + }); + + $('.posts-list').on('blur', editableClass + ' ' + inputSelector, function () { + const textarea = $(this); + const preview = textarea.parent().parent().find(displayClass); + const id = textarea.parents('[data-id]').attr('data-id'); + const titleEdit = displayClass === '.topic-title'; + + socket.emit('posts.editQueuedContent', { + id: id, + title: titleEdit ? textarea.val() : undefined, + content: titleEdit ? undefined : textarea.val(), + }, function (err, data) { + if (err) { + return alerts.error(err); + } + if (titleEdit) { + if (preview.find('.title-text').length) { + preview.find('.title-text').text(data.postData.title); + } else { + // for backwards compatibility, remove in 1.16.0 + preview.html(data.postData.title); + } + } else { + preview.html(data.postData.content); + } + + textarea.parent().addClass('hidden'); + preview.removeClass('hidden'); + }); + }); + } + + function handleBulkActions() { + $('[component="post-queue/bulk-actions"]').on('click', '[data-action]', async function () { + const bulkAction = $(this).attr('data-action'); + let queueEls = $('.posts-list [data-id]'); + if (bulkAction === 'accept-selected' || bulkAction === 'reject-selected') { + queueEls = queueEls.filter( + (i, el) => $(el).find('input[type="checkbox"]').is(':checked') + ); + } + const ids = queueEls.map((i, el) => $(el).attr('data-id')).get(); + const showConfirm = bulkAction === 'reject-all' || bulkAction === 'reject-selected'; + if (!ids.length || (showConfirm && !(await confirmReject(`[[post-queue:${bulkAction}-confirm, ${ids.length}]]`)))) { + return; + } + const action = bulkAction.split('-')[0]; + const promises = ids.map(id => socket.emit('posts.' + action, { id: id })); + + Promise.allSettled(promises).then(function (results) { + const fulfilled = results.filter(res => res.status === 'fulfilled').length; + const errors = results.filter(res => res.status === 'rejected'); + if (fulfilled) { + alerts.success(`[[post-queue:bulk-${action}-success, ${fulfilled}]]`); + ajaxify.refresh(); + } + + errors.forEach(res => alerts.error(res.reason)); + }); + }); + } + + return PostQueue; +}); diff --git a/public/src/client/recent.js b/public/src/client/recent.js new file mode 100644 index 0000000000..6257bf95ee --- /dev/null +++ b/public/src/client/recent.js @@ -0,0 +1,13 @@ +'use strict'; + +define('forum/recent', ['topicList'], function (topicList) { + const Recent = {}; + + Recent.init = function () { + app.enterRoom('recent_topics'); + + topicList.init('recent'); + }; + + return Recent; +}); diff --git a/public/src/client/register.js b/public/src/client/register.js new file mode 100644 index 0000000000..62dbc41f68 --- /dev/null +++ b/public/src/client/register.js @@ -0,0 +1,209 @@ +'use strict'; + + +define('forum/register', [ + 'translator', 'slugify', 'api', 'bootbox', 'forum/login', 'zxcvbn', 'jquery-form', +], function (translator, slugify, api, bootbox, Login, zxcvbn) { + const Register = {}; + let validationError = false; + const successIcon = ''; + + Register.init = function () { + const username = $('#username'); + const password = $('#password'); + const password_confirm = $('#password-confirm'); + const register = $('#register'); + + handleLanguageOverride(); + + $('#content #noscript').val('false'); + + const query = utils.params(); + if (query.token) { + $('#token').val(query.token); + } + + // Update the "others can mention you via" text + username.on('keyup', function () { + $('#yourUsername').text(this.value.length > 0 ? slugify(this.value) : 'username'); + }); + + username.on('blur', function () { + if (username.val().length) { + validateUsername(username.val()); + } + }); + + password.on('blur', function () { + if (password.val().length) { + validatePassword(password.val(), password_confirm.val()); + } + }); + + password_confirm.on('blur', function () { + if (password_confirm.val().length) { + validatePasswordConfirm(password.val(), password_confirm.val()); + } + }); + + function validateForm(callback) { + validationError = false; + validatePassword(password.val(), password_confirm.val()); + validatePasswordConfirm(password.val(), password_confirm.val()); + validateUsername(username.val(), callback); + } + + // Guard against caps lock + Login.capsLockCheck(document.querySelector('#password'), document.querySelector('#caps-lock-warning')); + + register.on('click', function (e) { + const registerBtn = $(this); + const errorEl = $('#register-error-notify'); + errorEl.addClass('hidden'); + e.preventDefault(); + validateForm(function () { + if (validationError) { + return; + } + + registerBtn.addClass('disabled'); + + registerBtn.parents('form').ajaxSubmit({ + headers: { + 'x-csrf-token': config.csrf_token, + }, + success: function (data) { + registerBtn.removeClass('disabled'); + if (!data) { + return; + } + if (data.next) { + const pathname = utils.urlToLocation(data.next).pathname; + + const params = utils.params({ url: data.next }); + params.registered = true; + const qs = decodeURIComponent($.param(params)); + + window.location.href = pathname + '?' + qs; + } else if (data.message) { + translator.translate(data.message, function (msg) { + bootbox.alert(msg); + ajaxify.go('/'); + }); + } + }, + error: function (data) { + translator.translate(data.responseText, config.defaultLang, function (translated) { + if (data.status === 403 && data.responseText === 'Forbidden') { + window.location.href = config.relative_path + '/register?error=csrf-invalid'; + } else { + errorEl.find('p').text(translated); + errorEl.removeClass('hidden'); + registerBtn.removeClass('disabled'); + } + }); + }, + }); + }); + }); + + // Set initial focus + $('#username').focus(); + }; + + function validateUsername(username, callback) { + callback = callback || function () {}; + + const username_notify = $('#username-notify'); + const userslug = slugify(username); + if (username.length < ajaxify.data.minimumUsernameLength || + userslug.length < ajaxify.data.minimumUsernameLength) { + showError(username_notify, '[[error:username-too-short]]'); + } else if (username.length > ajaxify.data.maximumUsernameLength) { + showError(username_notify, '[[error:username-too-long]]'); + } else if (!utils.isUserNameValid(username) || !userslug) { + showError(username_notify, '[[error:invalid-username]]'); + } else { + Promise.allSettled([ + api.head(`/users/bySlug/${username}`, {}), + api.head(`/groups/${username}`, {}), + ]).then((results) => { + if (results.every(obj => obj.status === 'rejected')) { + showSuccess(username_notify, successIcon); + } else { + showError(username_notify, '[[error:username-taken]]'); + } + + callback(); + }); + } + } + + function validatePassword(password, password_confirm) { + const password_notify = $('#password-notify'); + const password_confirm_notify = $('#password-confirm-notify'); + + try { + utils.assertPasswordValidity(password, zxcvbn); + + if (password === $('#username').val()) { + throw new Error('[[user:password_same_as_username]]'); + } + + showSuccess(password_notify, successIcon); + } catch (err) { + showError(password_notify, err.message); + } + + if (password !== password_confirm && password_confirm !== '') { + showError(password_confirm_notify, '[[user:change_password_error_match]]'); + } + } + + function validatePasswordConfirm(password, password_confirm) { + const password_notify = $('#password-notify'); + const password_confirm_notify = $('#password-confirm-notify'); + + if (!password || password_notify.hasClass('alert-error')) { + return; + } + + if (password !== password_confirm) { + showError(password_confirm_notify, '[[user:change_password_error_match]]'); + } else { + showSuccess(password_confirm_notify, successIcon); + } + } + + function showError(element, msg) { + translator.translate(msg, function (msg) { + element.html(msg); + element.parent() + .removeClass('register-success') + .addClass('register-danger'); + element.show(); + }); + validationError = true; + } + + function showSuccess(element, msg) { + translator.translate(msg, function (msg) { + element.html(msg); + element.parent() + .removeClass('register-danger') + .addClass('register-success'); + element.show(); + }); + } + + function handleLanguageOverride() { + if (!app.user.uid && config.defaultLang !== config.userLang) { + const formEl = $('[component="register/local"]'); + const langEl = $(''); + + formEl.append(langEl); + } + } + + return Register; +}); diff --git a/public/src/client/reset.js b/public/src/client/reset.js new file mode 100644 index 0000000000..16158ade9e --- /dev/null +++ b/public/src/client/reset.js @@ -0,0 +1,32 @@ +'use strict'; + + +define('forum/reset', ['alerts'], function (alerts) { + const ResetPassword = {}; + + ResetPassword.init = function () { + const inputEl = $('#email'); + const errorEl = $('#error'); + const successEl = $('#success'); + + $('#reset').on('click', function () { + if (inputEl.val() && inputEl.val().indexOf('@') !== -1) { + socket.emit('user.reset.send', inputEl.val(), function (err) { + if (err) { + return alerts.error(err); + } + + errorEl.addClass('hide'); + successEl.removeClass('hide'); + inputEl.val(''); + }); + } else { + successEl.addClass('hide'); + errorEl.removeClass('hide'); + } + return false; + }); + }; + + return ResetPassword; +}); diff --git a/public/src/client/reset_code.js b/public/src/client/reset_code.js new file mode 100644 index 0000000000..260204d34c --- /dev/null +++ b/public/src/client/reset_code.js @@ -0,0 +1,44 @@ +'use strict'; + + +define('forum/reset_code', ['alerts', 'zxcvbn'], function (alerts, zxcvbn) { + const ResetCode = {}; + + ResetCode.init = function () { + const reset_code = ajaxify.data.code; + + const resetEl = $('#reset'); + const password = $('#password'); + const repeat = $('#repeat'); + + resetEl.on('click', function () { + try { + utils.assertPasswordValidity(password.val(), zxcvbn); + + if (password.val() !== repeat.val()) { + throw new Error('[[reset_password:passwords_do_not_match]]'); + } + + resetEl.prop('disabled', true).translateHtml(' [[reset_password:changing_password]]'); + socket.emit('user.reset.commit', { + code: reset_code, + password: password.val(), + }, function (err) { + if (err) { + ajaxify.refresh(); + return alerts.error(err); + } + + window.location.href = config.relative_path + '/login'; + }); + } catch (err) { + $('#notice').removeClass('hidden'); + $('#notice strong').translateText(err.message); + } + + return false; + }); + }; + + return ResetCode; +}); diff --git a/public/src/client/search.js b/public/src/client/search.js new file mode 100644 index 0000000000..caec7b208e --- /dev/null +++ b/public/src/client/search.js @@ -0,0 +1,181 @@ +'use strict'; + + +define('forum/search', [ + 'search', + 'autocomplete', + 'storage', + 'hooks', + 'alerts', +], function (searchModule, autocomplete, storage, hooks, alerts) { + const Search = {}; + + Search.init = function () { + const searchQuery = $('#results').attr('data-search-query'); + + const searchIn = $('#search-in'); + + searchIn.on('change', function () { + updateFormItemVisiblity(searchIn.val()); + }); + + searchModule.highlightMatches(searchQuery, $('.search-result-text p, .search-result-text.search-result-title a')); + + $('#advanced-search').off('submit').on('submit', function (e) { + e.preventDefault(); + searchModule.query(getSearchDataFromDOM(), function () { + $('#search-input').val(''); + }); + return false; + }); + + handleSavePreferences(); + + enableAutoComplete(); + + fillOutForm(); + }; + + function getSearchDataFromDOM() { + const form = $('#advanced-search'); + const searchData = { + in: $('#search-in').val(), + }; + searchData.term = $('#search-input').val(); + if (searchData.in === 'posts' || searchData.in === 'titlesposts' || searchData.in === 'titles') { + searchData.matchWords = form.find('#match-words-filter').val(); + searchData.by = form.find('#posted-by-user').tagsinput('items'); + searchData.categories = form.find('#posted-in-categories').val(); + searchData.searchChildren = form.find('#search-children').is(':checked'); + searchData.hasTags = form.find('#has-tags').tagsinput('items'); + searchData.replies = form.find('#reply-count').val(); + searchData.repliesFilter = form.find('#reply-count-filter').val(); + searchData.timeFilter = form.find('#post-time-filter').val(); + searchData.timeRange = form.find('#post-time-range').val(); + searchData.sortBy = form.find('#post-sort-by').val(); + searchData.sortDirection = form.find('#post-sort-direction').val(); + searchData.showAs = form.find('#show-as-topics').is(':checked') ? 'topics' : 'posts'; + } + + hooks.fire('action:search.getSearchDataFromDOM', { + form: form, + data: searchData, + }); + + return searchData; + } + + function updateFormItemVisiblity(searchIn) { + const hide = searchIn.indexOf('posts') === -1 && searchIn.indexOf('titles') === -1; + $('.post-search-item').toggleClass('hide', hide); + } + + function fillOutForm() { + const params = utils.params({ + disableToType: true, + }); + + const searchData = searchModule.getSearchPreferences(); + const formData = utils.merge(searchData, params); + + if (formData) { + if (ajaxify.data.term) { + $('#search-input').val(ajaxify.data.term); + } + formData.in = formData.in || ajaxify.data.searchDefaultIn; + $('#search-in').val(formData.in); + updateFormItemVisiblity(formData.in); + + if (formData.matchWords) { + $('#match-words-filter').val(formData.matchWords); + } + + if (formData.by) { + formData.by = Array.isArray(formData.by) ? formData.by : [formData.by]; + formData.by.forEach(function (by) { + $('#posted-by-user').tagsinput('add', by); + }); + } + + if (formData.categories) { + $('#posted-in-categories').val(formData.categories); + } + + if (formData.searchChildren) { + $('#search-children').prop('checked', true); + } + + if (formData.hasTags) { + formData.hasTags = Array.isArray(formData.hasTags) ? formData.hasTags : [formData.hasTags]; + formData.hasTags.forEach(function (tag) { + $('#has-tags').tagsinput('add', tag); + }); + } + + if (formData.replies) { + $('#reply-count').val(formData.replies); + $('#reply-count-filter').val(formData.repliesFilter); + } + + if (formData.timeRange) { + $('#post-time-range').val(formData.timeRange); + $('#post-time-filter').val(formData.timeFilter); + } + + if (formData.sortBy || ajaxify.data.searchDefaultSortBy) { + $('#post-sort-by').val(formData.sortBy || ajaxify.data.searchDefaultSortBy); + } + $('#post-sort-direction').val(formData.sortDirection || 'desc'); + + if (formData.showAs) { + const isTopic = formData.showAs === 'topics'; + const isPost = formData.showAs === 'posts'; + $('#show-as-topics').prop('checked', isTopic).parent().toggleClass('active', isTopic); + $('#show-as-posts').prop('checked', isPost).parent().toggleClass('active', isPost); + } + + hooks.fire('action:search.fillOutForm', { + form: formData, + }); + } + } + + function handleSavePreferences() { + $('#save-preferences').on('click', function () { + storage.setItem('search-preferences', JSON.stringify(getSearchDataFromDOM())); + alerts.success('[[search:search-preferences-saved]]'); + return false; + }); + + $('#clear-preferences').on('click', function () { + storage.removeItem('search-preferences'); + const query = $('#search-input').val(); + $('#advanced-search')[0].reset(); + $('#search-input').val(query); + alerts.success('[[search:search-preferences-cleared]]'); + return false; + }); + } + + function enableAutoComplete() { + const userEl = $('#posted-by-user'); + userEl.tagsinput({ + confirmKeys: [13, 44], + trimValue: true, + }); + if (app.user.privileges['search:users']) { + autocomplete.user(userEl.siblings('.bootstrap-tagsinput').find('input')); + } + + const tagEl = $('#has-tags'); + tagEl.tagsinput({ + confirmKeys: [13, 44], + trimValue: true, + }); + if (app.user.privileges['search:tags']) { + autocomplete.tag(tagEl.siblings('.bootstrap-tagsinput').find('input')); + } + } + + return Search; +}); diff --git a/public/src/client/tag.js b/public/src/client/tag.js new file mode 100644 index 0000000000..81be8f8680 --- /dev/null +++ b/public/src/client/tag.js @@ -0,0 +1,13 @@ +'use strict'; + +define('forum/tag', ['topicList', 'forum/infinitescroll'], function (topicList) { + const Tag = {}; + + Tag.init = function () { + app.enterRoom('tags'); + + topicList.init('tag'); + }; + + return Tag; +}); diff --git a/public/src/client/tags.js b/public/src/client/tags.js new file mode 100644 index 0000000000..79e253f750 --- /dev/null +++ b/public/src/client/tags.js @@ -0,0 +1,64 @@ +'use strict'; + + +define('forum/tags', ['forum/infinitescroll', 'alerts'], function (infinitescroll, alerts) { + const Tags = {}; + + Tags.init = function () { + app.enterRoom('tags'); + $('#tag-search').focus(); + $('#tag-search').on('input propertychange', utils.debounce(function () { + if (!$('#tag-search').val().length) { + return resetSearch(); + } + + socket.emit('topics.searchAndLoadTags', { query: $('#tag-search').val() }, function (err, results) { + if (err) { + return alerts.error(err); + } + onTagsLoaded(results.tags, true); + }); + }, 250)); + + infinitescroll.init(Tags.loadMoreTags); + }; + + Tags.loadMoreTags = function (direction) { + if (direction < 0 || !$('.tag-list').length || $('#tag-search').val()) { + return; + } + + infinitescroll.loadMore('topics.loadMoreTags', { + after: $('.tag-list').attr('data-nextstart'), + }, function (data, done) { + if (data && data.tags && data.tags.length) { + onTagsLoaded(data.tags, false, done); + $('.tag-list').attr('data-nextstart', data.nextStart); + } else { + done(); + } + }); + }; + + function resetSearch() { + socket.emit('topics.loadMoreTags', { + after: 0, + }, function (err, data) { + if (err) { + return alerts.error(err); + } + onTagsLoaded(data.tags, true); + }); + } + + function onTagsLoaded(tags, replace, callback) { + callback = callback || function () {}; + app.parseAndTranslate('tags', 'tags', { tags: tags }, function (html) { + $('.tag-list')[replace ? 'html' : 'append'](html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + callback(); + }); + } + + return Tags; +}); diff --git a/public/src/client/top.js b/public/src/client/top.js new file mode 100644 index 0000000000..46810ef631 --- /dev/null +++ b/public/src/client/top.js @@ -0,0 +1,13 @@ +'use strict'; + +define('forum/top', ['topicList'], function (topicList) { + const Top = {}; + + Top.init = function () { + app.enterRoom('top_topics'); + + topicList.init('top'); + }; + + return Top; +}); diff --git a/public/src/client/topic.js b/public/src/client/topic.js new file mode 100644 index 0000000000..969b3da92a --- /dev/null +++ b/public/src/client/topic.js @@ -0,0 +1,364 @@ +'use strict'; + + +define('forum/topic', [ + 'forum/infinitescroll', + 'forum/topic/threadTools', + 'forum/topic/postTools', + 'forum/topic/events', + 'forum/topic/posts', + 'navigator', + 'sort', + 'components', + 'storage', + 'hooks', + 'api', + 'alerts', +], function ( + infinitescroll, threadTools, postTools, + events, posts, navigator, sort, + components, storage, hooks, api, alerts +) { + const Topic = {}; + let tid = 0; + let currentUrl = ''; + + $(window).on('action:ajaxify.start', function (ev, data) { + events.removeListeners(); + + if (!String(data.url).startsWith('topic/')) { + navigator.disable(); + components.get('navbar/title').find('span').text('').hide(); + alerts.remove('bookmark'); + } + }); + + Topic.init = function () { + const tidChanged = !tid || parseInt(tid, 10) !== parseInt(ajaxify.data.tid, 10); + tid = ajaxify.data.tid; + currentUrl = ajaxify.currentPage; + hooks.fire('action:topic.loading'); + + app.enterRoom('topic_' + tid); + + if (tidChanged) { + posts.signaturesShown = {}; + } + posts.onTopicPageLoad(components.get('post')); + navigator.init('[component="post"]', ajaxify.data.postcount, Topic.toTop, Topic.toBottom, utils.debounce(Topic.navigatorCallback, 500)); + + postTools.init(tid); + threadTools.init(tid, $('.topic')); + events.init(); + + sort.handleSort('topicPostSort', 'topic/' + ajaxify.data.slug); + + if (!config.usePagination) { + infinitescroll.init($('[component="topic"]'), posts.loadMorePosts); + } + + addBlockQuoteHandler(); + addParentHandler(); + addDropupHandler(); + addRepliesHandler(); + addPostsPreviewHandler(); + + handleBookmark(tid); + + $(window).on('scroll', utils.debounce(updateTopicTitle, 250)); + + handleTopicSearch(); + + hooks.fire('action:topic.loaded', ajaxify.data); + }; + + function handleTopicSearch() { + require(['mousetrap'], (mousetrap) => { + if (config.topicSearchEnabled) { + require(['search'], function (search) { + mousetrap.bind(['command+f', 'ctrl+f'], function (e) { + e.preventDefault(); + $('#search-fields input').val('in:topic-' + ajaxify.data.tid + ' '); + search.showAndFocusInput(); + }); + + hooks.onPage('action:ajaxify.cleanup', () => { + mousetrap.unbind(['command+f', 'ctrl+f']); + }); + }); + } + + mousetrap.bind('j', () => { + const index = navigator.getIndex(); + const count = navigator.getCount(); + if (index === count) { + return; + } + + navigator.scrollToIndex(index, true, 0); + }); + + mousetrap.bind('k', () => { + const index = navigator.getIndex(); + if (index === 1) { + return; + } + navigator.scrollToIndex(index - 2, true, 0); + }); + }); + } + + Topic.toTop = function () { + navigator.scrollTop(0); + }; + + Topic.toBottom = function () { + socket.emit('topics.postcount', ajaxify.data.tid, function (err, postCount) { + if (err) { + return alerts.error(err); + } + + navigator.scrollBottom(postCount - 1); + }); + }; + + function handleBookmark(tid) { + if (window.location.hash) { + const el = $(utils.escapeHTML(window.location.hash)); + if (el.length) { + return navigator.scrollToElement(el, true, 0); + } + } + const bookmark = ajaxify.data.bookmark || storage.getItem('topic:' + tid + ':bookmark'); + const postIndex = ajaxify.data.postIndex; + + if (postIndex > 1) { + if (components.get('post/anchor', postIndex - 1).length) { + return navigator.scrollToPostIndex(postIndex - 1, true, 0); + } + } else if (bookmark && ( + !config.usePagination || + (config.usePagination && ajaxify.data.pagination.currentPage === 1) + ) && ajaxify.data.postcount > ajaxify.data.bookmarkThreshold) { + alerts.alert({ + alert_id: 'bookmark', + message: '[[topic:bookmark_instructions]]', + timeout: 0, + type: 'info', + clickfn: function () { + navigator.scrollToIndex(parseInt(bookmark, 10), true); + }, + closefn: function () { + storage.removeItem('topic:' + tid + ':bookmark'); + }, + }); + setTimeout(function () { + alerts.remove('bookmark'); + }, 10000); + } + } + + function addBlockQuoteHandler() { + components.get('topic').on('click', 'blockquote .toggle', function () { + const blockQuote = $(this).parent('blockquote'); + const toggle = $(this); + blockQuote.toggleClass('uncollapsed'); + const collapsed = !blockQuote.hasClass('uncollapsed'); + toggle.toggleClass('fa-angle-down', collapsed).toggleClass('fa-angle-up', !collapsed); + }); + } + + function addParentHandler() { + components.get('topic').on('click', '[component="post/parent"]', function (e) { + const toPid = $(this).attr('data-topid'); + + const toPost = $('[component="topic"]>[component="post"][data-pid="' + toPid + '"]'); + if (toPost.length) { + e.preventDefault(); + navigator.scrollToIndex(toPost.attr('data-index'), true); + return false; + } + }); + } + + Topic.applyDropup = function () { + const containerRect = this.getBoundingClientRect(); + const dropdownEl = this.querySelector('.dropdown-menu'); + const dropdownStyle = window.getComputedStyle(dropdownEl); + const dropdownHeight = dropdownStyle.getPropertyValue('height').slice(0, -2); + const offset = 60; + + // Toggler position (including its height, since the menu spawns above it), + // minus the dropdown's height and navbar offset + const dropUp = (containerRect.top + containerRect.height - dropdownHeight - offset) > 0; + this.classList.toggle('dropup', dropUp); + }; + + function addDropupHandler() { + // Locate all dropdowns + const target = $('#content .dropdown-menu').parent(); + $(target).on('shown.bs.dropdown', function () { + const dropdownEl = this.querySelector('.dropdown-menu'); + if (dropdownEl.innerHTML) { + Topic.applyDropup.call(this); + } + }); + hooks.onPage('action:topic.tools.load', ({ element }) => { + Topic.applyDropup.call(element.get(0).parentNode); + }); + hooks.onPage('action:post.tools.load', ({ element }) => { + Topic.applyDropup.call(element.get(0).parentNode); + }); + } + + function addRepliesHandler() { + $('[component="topic"]').on('click', '[component="post/reply-count"]', function () { + const btn = $(this); + require(['forum/topic/replies'], function (replies) { + replies.init(btn); + }); + }); + } + + function addPostsPreviewHandler() { + if (!ajaxify.data.showPostPreviewsOnHover || utils.isMobile()) { + return; + } + let timeoutId = 0; + const postCache = {}; + $(window).one('action:ajaxify.start', function () { + clearTimeout(timeoutId); + $('#post-tooltip').remove(); + }); + $('[component="topic"]').on('mouseenter', '[component="post"] a, [component="topic/event"] a', async function () { + const link = $(this); + + async function renderPost(pid) { + const postData = postCache[pid] || await socket.emit('posts.getPostSummaryByPid', { pid: pid }); + $('#post-tooltip').remove(); + if (postData && ajaxify.data.template.topic) { + postCache[pid] = postData; + const tooltip = await app.parseAndTranslate('partials/topic/post-preview', { post: postData }); + tooltip.hide().find('.timeago').timeago(); + tooltip.appendTo($('body')).fadeIn(300); + const postContent = link.parents('[component="topic"]').find('[component="post/content"]').first(); + const postRect = postContent.offset(); + const postWidth = postContent.width(); + const linkRect = link.offset(); + tooltip.css({ + top: linkRect.top + 30, + left: postRect.left, + width: postWidth, + }); + } + } + + const href = link.attr('href'); + const location = utils.urlToLocation(href); + const pathname = location.pathname; + const validHref = href && href !== '#' && window.location.hostname === location.hostname; + $('#post-tooltip').remove(); + const postMatch = validHref && pathname && pathname.match(/\/post\/([\d]+)/); + const topicMatch = validHref && pathname && pathname.match(/\/topic\/([\d]+)/); + if (postMatch) { + const pid = postMatch[1]; + if (parseInt(link.parents('[component="post"]').attr('data-pid'), 10) === parseInt(pid, 10)) { + return; // dont render self post + } + + timeoutId = setTimeout(async () => { + renderPost(pid); + }, 300); + } else if (topicMatch) { + timeoutId = setTimeout(async () => { + const tid = topicMatch[1]; + const topicData = await api.get('/topics/' + tid, {}); + renderPost(topicData.mainPid); + }, 300); + } + }).on('mouseleave', '[component="post"] a, [component="topic/event"] a', function () { + clearTimeout(timeoutId); + $('#post-tooltip').remove(); + }); + } + + function updateTopicTitle() { + const span = components.get('navbar/title').find('span'); + if ($(window).scrollTop() > 50 && span.hasClass('hidden')) { + span.html(ajaxify.data.title).removeClass('hidden'); + } else if ($(window).scrollTop() <= 50 && !span.hasClass('hidden')) { + span.html('').addClass('hidden'); + } + if ($(window).scrollTop() > 300) { + alerts.remove('bookmark'); + } + } + + Topic.navigatorCallback = function (index, elementCount) { + if (!ajaxify.data.template.topic || navigator.scrollActive) { + return; + } + + const newUrl = 'topic/' + ajaxify.data.slug + (index > 1 ? ('/' + index) : ''); + if (newUrl !== currentUrl) { + currentUrl = newUrl; + + if (index >= elementCount && app.user.uid) { + socket.emit('topics.markAsRead', [ajaxify.data.tid]); + } + + updateUserBookmark(index); + + Topic.replaceURLTimeout = 0; + if (ajaxify.data.updateUrlWithPostIndex && history.replaceState) { + let search = window.location.search || ''; + if (!config.usePagination) { + search = (search && !/^\?page=\d+$/.test(search) ? search : ''); + } + + history.replaceState({ + url: newUrl + search, + }, null, window.location.protocol + '//' + window.location.host + config.relative_path + '/' + newUrl + search); + } + } + }; + + function updateUserBookmark(index) { + const bookmarkKey = 'topic:' + ajaxify.data.tid + ':bookmark'; + const currentBookmark = ajaxify.data.bookmark || storage.getItem(bookmarkKey); + if (config.topicPostSort === 'newest_to_oldest') { + index = Math.max(1, ajaxify.data.postcount - index + 2); + } + + if ( + ajaxify.data.postcount > ajaxify.data.bookmarkThreshold && + ( + !currentBookmark || + parseInt(index, 10) > parseInt(currentBookmark, 10) || + ajaxify.data.postcount < parseInt(currentBookmark, 10) + ) + ) { + if (app.user.uid) { + socket.emit('topics.bookmark', { + tid: ajaxify.data.tid, + index: index, + }, function (err) { + if (err) { + return alerts.error(err); + } + ajaxify.data.bookmark = index + 1; + }); + } else { + storage.setItem(bookmarkKey, index); + } + } + + // removes the bookmark alert when we get to / past the bookmark + if (!currentBookmark || parseInt(index, 10) >= parseInt(currentBookmark, 10)) { + alerts.remove('bookmark'); + } + } + + + return Topic; +}); diff --git a/public/src/client/topic/change-owner.js b/public/src/client/topic/change-owner.js new file mode 100644 index 0000000000..65f878318c --- /dev/null +++ b/public/src/client/topic/change-owner.js @@ -0,0 +1,91 @@ +'use strict'; + + +define('forum/topic/change-owner', [ + 'postSelect', + 'autocomplete', + 'alerts', +], function (postSelect, autocomplete, alerts) { + const ChangeOwner = {}; + + let modal; + let commit; + let toUid = 0; + ChangeOwner.init = function (postEl) { + if (modal) { + return; + } + app.parseAndTranslate('partials/change_owner_modal', {}, function (html) { + modal = html; + + commit = modal.find('#change_owner_commit'); + + $('body').append(modal); + + modal.find('.close,#change_owner_cancel').on('click', closeModal); + modal.find('#username').on('keyup', checkButtonEnable); + postSelect.init(onPostToggled, { + allowMainPostSelect: true, + }); + showPostsSelected(); + + if (postEl) { + postSelect.togglePostSelection(postEl, postEl.attr('data-pid')); + } + + commit.on('click', function () { + changeOwner(); + }); + + autocomplete.user(modal.find('#username'), { filters: ['notbanned'] }, function (ev, ui) { + toUid = ui.item.user.uid; + checkButtonEnable(); + }); + }); + }; + + function showPostsSelected() { + if (postSelect.pids.length) { + modal.find('#pids').translateHtml('[[topic:fork_pid_count, ' + postSelect.pids.length + ']]'); + } else { + modal.find('#pids').translateHtml('[[topic:fork_no_pids]]'); + } + } + + function checkButtonEnable() { + if (toUid && modal.find('#username').length && modal.find('#username').val().length && postSelect.pids.length) { + commit.removeAttr('disabled'); + } else { + commit.attr('disabled', true); + } + } + + function onPostToggled() { + checkButtonEnable(); + showPostsSelected(); + } + + function changeOwner() { + if (!toUid) { + return; + } + socket.emit('posts.changeOwner', { pids: postSelect.pids, toUid: toUid }, function (err) { + if (err) { + return alerts.error(err); + } + ajaxify.refresh(); + + closeModal(); + }); + } + + function closeModal() { + if (modal) { + modal.remove(); + modal = null; + postSelect.disable(); + } + } + + return ChangeOwner; +}); diff --git a/public/src/client/topic/delete-posts.js b/public/src/client/topic/delete-posts.js new file mode 100644 index 0000000000..bb01220cb9 --- /dev/null +++ b/public/src/client/topic/delete-posts.js @@ -0,0 +1,90 @@ +'use strict'; + +define('forum/topic/delete-posts', [ + 'postSelect', 'alerts', 'api', +], function (postSelect, alerts, api) { + const DeletePosts = {}; + let modal; + let deleteBtn; + let purgeBtn; + let tid; + + DeletePosts.init = function () { + tid = ajaxify.data.tid; + + $(window).off('action:ajaxify.end', onAjaxifyEnd).on('action:ajaxify.end', onAjaxifyEnd); + + if (modal) { + return; + } + + app.parseAndTranslate('partials/delete_posts_modal', {}, function (html) { + modal = html; + + $('body').append(modal); + + deleteBtn = modal.find('#delete_posts_confirm'); + purgeBtn = modal.find('#purge_posts_confirm'); + + modal.find('.close,#delete_posts_cancel').on('click', closeModal); + + postSelect.init(function () { + checkButtonEnable(); + showPostsSelected(); + }); + showPostsSelected(); + + deleteBtn.on('click', function () { + deletePosts(deleteBtn, pid => `/posts/${pid}/state`); + }); + purgeBtn.on('click', function () { + deletePosts(purgeBtn, pid => `/posts/${pid}`); + }); + }); + }; + + function onAjaxifyEnd() { + if (ajaxify.data.template.name !== 'topic' || ajaxify.data.tid !== tid) { + closeModal(); + $(window).off('action:ajaxify.end', onAjaxifyEnd); + } + } + + function deletePosts(btn, route) { + btn.attr('disabled', true); + Promise.all(postSelect.pids.map(pid => api.delete(route(pid), {}))) + .then(closeModal) + .catch(alerts.error) + .finally(() => { + btn.removeAttr('disabled'); + }); + } + + function showPostsSelected() { + if (postSelect.pids.length) { + modal.find('#pids').translateHtml('[[topic:fork_pid_count, ' + postSelect.pids.length + ']]'); + } else { + modal.find('#pids').translateHtml('[[topic:fork_no_pids]]'); + } + } + + function checkButtonEnable() { + if (postSelect.pids.length) { + deleteBtn.removeAttr('disabled'); + purgeBtn.removeAttr('disabled'); + } else { + deleteBtn.attr('disabled', true); + purgeBtn.attr('disabled', true); + } + } + + function closeModal() { + if (modal) { + modal.remove(); + modal = null; + postSelect.disable(); + } + } + + return DeletePosts; +}); diff --git a/public/src/client/topic/diffs.js b/public/src/client/topic/diffs.js new file mode 100644 index 0000000000..0e0b8b539b --- /dev/null +++ b/public/src/client/topic/diffs.js @@ -0,0 +1,117 @@ +'use strict'; + +define('forum/topic/diffs', ['api', 'bootbox', 'alerts', 'forum/topic/images'], function (api, bootbox, alerts) { + const Diffs = {}; + const localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; + + Diffs.open = function (pid) { + if (!config.enablePostHistory) { + return; + } + + api.get(`/posts/${pid}/diffs`, {}).then((data) => { + parsePostHistory(data).then(($html) => { + const $modal = bootbox.dialog({ title: '[[topic:diffs.title]]', message: $html, size: 'large' }); + + if (!data.timestamps.length) { + return; + } + + const $selectEl = $modal.find('select'); + const $revertEl = $modal.find('button[data-action="restore"]'); + const $deleteEl = $modal.find('button[data-action="delete"]'); + const $postContainer = $modal.find('ul.posts-list'); + const $numberOfDiffCon = $modal.find('.number-of-diffs strong'); + + $selectEl.on('change', function () { + Diffs.load(pid, this.value, $postContainer); + $revertEl.prop('disabled', data.timestamps.indexOf(this.value) === 0); + $deleteEl.prop('disabled', data.timestamps.indexOf(this.value) === 0); + }); + + $revertEl.on('click', function () { + Diffs.restore(pid, $selectEl.val(), $modal); + }); + + $deleteEl.on('click', function () { + Diffs.delete(pid, $selectEl.val(), $selectEl, $numberOfDiffCon); + }); + + $modal.on('shown.bs.modal', function () { + Diffs.load(pid, $selectEl.val(), $postContainer); + $revertEl.prop('disabled', true); + $deleteEl.prop('disabled', true); + }); + }); + }).catch(alerts.error); + }; + + Diffs.load = function (pid, since, $postContainer) { + if (!config.enablePostHistory) { + return; + } + + api.get(`/posts/${pid}/diffs/${since}`, {}).then((data) => { + data.deleted = !!parseInt(data.deleted, 10); + + app.parseAndTranslate('partials/posts_list', 'posts', { + posts: [data], + }, function ($html) { + $postContainer.empty().append($html); + $postContainer.find('.timeago').timeago(); + }); + }).catch(alerts.error); + }; + + Diffs.restore = function (pid, since, $modal) { + if (!config.enablePostHistory) { + return; + } + + api.put(`/posts/${pid}/diffs/${since}`, {}).then(() => { + $modal.modal('hide'); + alerts.success('[[topic:diffs.post-restored]]'); + }).catch(alerts.error); + }; + + Diffs.delete = function (pid, timestamp, $selectEl, $numberOfDiffCon) { + api.del(`/posts/${pid}/diffs/${timestamp}`).then((data) => { + parsePostHistory(data, 'diffs').then(($html) => { + $selectEl.empty().append($html); + $selectEl.trigger('change'); + const numberOfDiffs = $selectEl.find('option').length; + $numberOfDiffCon.text(numberOfDiffs); + alerts.success('[[topic:diffs.deleted]]'); + }); + }).catch(alerts.error); + }; + + function parsePostHistory(data, blockName) { + return new Promise((resolve) => { + const params = [{ + diffs: data.revisions.map(function (revision) { + const timestamp = parseInt(revision.timestamp, 10); + + return { + username: revision.username, + timestamp: timestamp, + pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts), + }; + }), + numDiffs: data.timestamps.length, + editable: data.editable, + deletable: data.deletable, + }, function ($html) { + resolve($html); + }]; + + if (blockName) { + params.unshift(blockName); + } + + app.parseAndTranslate('partials/modals/post_history', ...params); + }); + } + + return Diffs; +}); diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js new file mode 100644 index 0000000000..2b65912405 --- /dev/null +++ b/public/src/client/topic/events.js @@ -0,0 +1,242 @@ + +'use strict'; + + +define('forum/topic/events', [ + 'forum/topic/postTools', + 'forum/topic/threadTools', + 'forum/topic/posts', + 'forum/topic/images', + 'components', + 'translator', + 'benchpress', + 'hooks', +], function (postTools, threadTools, posts, images, components, translator, Benchpress, hooks) { + const Events = {}; + + const events = { + 'event:user_status_change': onUserStatusChange, + 'event:voted': updatePostVotesAndUserReputation, + 'event:bookmarked': updateBookmarkCount, + + 'event:topic_deleted': threadTools.setDeleteState, + 'event:topic_restored': threadTools.setDeleteState, + 'event:topic_purged': onTopicPurged, + + 'event:topic_locked': threadTools.setLockedState, + 'event:topic_unlocked': threadTools.setLockedState, + + 'event:topic_pinned': threadTools.setPinnedState, + 'event:topic_unpinned': threadTools.setPinnedState, + + 'event:topic_moved': onTopicMoved, + + 'event:post_edited': onPostEdited, + 'event:post_purged': onPostPurged, + + 'event:post_deleted': togglePostDeleteState, + 'event:post_restored': togglePostDeleteState, + + 'posts.bookmark': togglePostBookmark, + 'posts.unbookmark': togglePostBookmark, + + 'posts.upvote': togglePostVote, + 'posts.downvote': togglePostVote, + 'posts.unvote': togglePostVote, + + 'event:new_notification': onNewNotification, + 'event:new_post': posts.onNewPost, + }; + + Events.init = function () { + Events.removeListeners(); + for (const eventName in events) { + if (events.hasOwnProperty(eventName)) { + socket.on(eventName, events[eventName]); + } + } + }; + + Events.removeListeners = function () { + for (const eventName in events) { + if (events.hasOwnProperty(eventName)) { + socket.removeListener(eventName, events[eventName]); + } + } + }; + + function onUserStatusChange(data) { + app.updateUserStatus($('[data-uid="' + data.uid + '"] [component="user/status"]'), data.status); + } + + function updatePostVotesAndUserReputation(data) { + const votes = $('[data-pid="' + data.post.pid + '"] [component="post/vote-count"]').filter(function (index, el) { + return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); + }); + const reputationElements = $('.reputation[data-uid="' + data.post.uid + '"]'); + votes.html(data.post.votes).attr('data-votes', data.post.votes); + reputationElements.html(data.user.reputation).attr('data-reputation', data.user.reputation); + } + + function updateBookmarkCount(data) { + $('[data-pid="' + data.post.pid + '"] .bookmarkCount').filter(function (index, el) { + return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); + }).html(data.post.bookmarks).attr('data-bookmarks', data.post.bookmarks); + } + + function onTopicPurged(data) { + if ( + ajaxify.data.category && + ajaxify.data.category.slug && + parseInt(data.tid, 10) === parseInt(ajaxify.data.tid, 10) + ) { + ajaxify.go('category/' + ajaxify.data.category.slug, null, true); + } + } + + function onTopicMoved(data) { + if (data && data.slug && parseInt(data.tid, 10) === parseInt(ajaxify.data.tid, 10)) { + ajaxify.go('topic/' + data.slug, null, true); + } + } + + function onPostEdited(data) { + if (!data || !data.post || parseInt(data.post.tid, 10) !== parseInt(ajaxify.data.tid, 10)) { + return; + } + const editedPostEl = components.get('post/content', data.post.pid).filter(function (index, el) { + return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); + }); + + const editorEl = $('[data-pid="' + data.post.pid + '"] [component="post/editor"]').filter(function (index, el) { + return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); + }); + const topicTitle = components.get('topic/title'); + const navbarTitle = components.get('navbar/title').find('span'); + const breadCrumb = components.get('breadcrumb/current'); + + if (data.topic.rescheduled) { + return ajaxify.go('topic/' + data.topic.slug, null, true); + } + + if (topicTitle.length && data.topic.title && data.topic.renamed) { + ajaxify.data.title = data.topic.title; + const newUrl = 'topic/' + data.topic.slug + (window.location.search ? window.location.search : ''); + history.replaceState({ url: newUrl }, null, window.location.protocol + '//' + window.location.host + config.relative_path + '/' + newUrl); + + topicTitle.fadeOut(250, function () { + topicTitle.html(data.topic.title).fadeIn(250); + }); + breadCrumb.fadeOut(250, function () { + breadCrumb.html(data.topic.title).fadeIn(250); + }); + navbarTitle.fadeOut(250, function () { + navbarTitle.html(data.topic.title).fadeIn(250); + }); + } + + if (data.post.changed) { + editedPostEl.fadeOut(250, function () { + editedPostEl.html(translator.unescape(data.post.content)); + editedPostEl.find('img:not(.not-responsive)').addClass('img-responsive'); + images.wrapImagesInLinks(editedPostEl.parent()); + posts.addBlockquoteEllipses(editedPostEl.parent()); + editedPostEl.fadeIn(250); + + const editData = { + editor: data.editor, + editedISO: utils.toISOString(data.post.edited), + }; + + app.parseAndTranslate('partials/topic/post-editor', editData, function (html) { + editorEl.replaceWith(html); + $('[data-pid="' + data.post.pid + '"] [component="post/editor"] .timeago').timeago(); + hooks.fire('action:posts.edited', data); + }); + }); + } else { + hooks.fire('action:posts.edited', data); + } + + if (data.topic.tags && data.topic.tagsupdated) { + Benchpress.render('partials/topic/tags', { tags: data.topic.tags }).then(function (html) { + const tags = $('.tags'); + + tags.fadeOut(250, function () { + tags.html(html).fadeIn(250); + }); + }); + } + + postTools.removeMenu(components.get('post', 'pid', data.post.pid)); + } + + function onPostPurged(postData) { + if (!postData || parseInt(postData.tid, 10) !== parseInt(ajaxify.data.tid, 10)) { + return; + } + components.get('post', 'pid', postData.pid).fadeOut(500, function () { + $(this).remove(); + posts.showBottomPostBar(); + }); + ajaxify.data.postcount -= 1; + postTools.updatePostCount(ajaxify.data.postcount); + require(['forum/topic/replies'], function (replies) { + replies.onPostPurged(postData); + }); + } + + function togglePostDeleteState(data) { + const postEl = components.get('post', 'pid', data.pid); + + if (!postEl.length) { + return; + } + + postEl.toggleClass('deleted'); + const isDeleted = postEl.hasClass('deleted'); + postTools.toggle(data.pid, isDeleted); + + if (!ajaxify.data.privileges.isAdminOrMod && parseInt(data.uid, 10) !== parseInt(app.user.uid, 10)) { + postEl.find('[component="post/tools"]').toggleClass('hidden', isDeleted); + if (isDeleted) { + postEl.find('[component="post/content"]').translateHtml('[[topic:post_is_deleted]]'); + } else { + postEl.find('[component="post/content"]').html(translator.unescape(data.content)); + } + } + } + + function togglePostBookmark(data) { + const el = $('[data-pid="' + data.post.pid + '"] [component="post/bookmark"]').filter(function (index, el) { + return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); + }); + if (!el.length) { + return; + } + + el.attr('data-bookmarked', data.isBookmarked); + + el.find('[component="post/bookmark/on"]').toggleClass('hidden', !data.isBookmarked); + el.find('[component="post/bookmark/off"]').toggleClass('hidden', data.isBookmarked); + } + + function togglePostVote(data) { + const post = $('[data-pid="' + data.post.pid + '"]'); + post.find('[component="post/upvote"]').filter(function (index, el) { + return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); + }).toggleClass('upvoted', data.upvote); + post.find('[component="post/downvote"]').filter(function (index, el) { + return parseInt($(el).closest('[data-pid]').attr('data-pid'), 10) === parseInt(data.post.pid, 10); + }).toggleClass('downvoted', data.downvote); + } + + function onNewNotification(data) { + const tid = ajaxify.data.tid; + if (data && data.tid && parseInt(data.tid, 10) === parseInt(tid, 10)) { + socket.emit('topics.markTopicNotificationsRead', [tid]); + } + } + + return Events; +}); diff --git a/public/src/client/topic/fork.js b/public/src/client/topic/fork.js new file mode 100644 index 0000000000..119e17157c --- /dev/null +++ b/public/src/client/topic/fork.js @@ -0,0 +1,106 @@ +'use strict'; + + +define('forum/topic/fork', ['components', 'postSelect', 'alerts'], function (components, postSelect, alerts) { + const Fork = {}; + let forkModal; + let forkCommit; + let fromTid; + + Fork.init = function () { + fromTid = ajaxify.data.tid; + + $(window).off('action:ajaxify.end', onAjaxifyEnd).on('action:ajaxify.end', onAjaxifyEnd); + + if (forkModal) { + return; + } + + app.parseAndTranslate('partials/fork_thread_modal', {}, function (html) { + forkModal = html; + + forkCommit = forkModal.find('#fork_thread_commit'); + + $('body').append(forkModal); + + forkModal.find('.close,#fork_thread_cancel').on('click', closeForkModal); + forkModal.find('#fork-title').on('keyup', checkForkButtonEnable); + + postSelect.init(function () { + checkForkButtonEnable(); + showPostsSelected(); + }); + showPostsSelected(); + + forkCommit.on('click', createTopicFromPosts); + }); + }; + + function onAjaxifyEnd() { + if (ajaxify.data.template.name !== 'topic' || ajaxify.data.tid !== fromTid) { + closeForkModal(); + $(window).off('action:ajaxify.end', onAjaxifyEnd); + } + } + + function createTopicFromPosts() { + forkCommit.attr('disabled', true); + socket.emit('topics.createTopicFromPosts', { + title: forkModal.find('#fork-title').val(), + pids: postSelect.pids, + fromTid: fromTid, + }, function (err, newTopic) { + function fadeOutAndRemove(pid) { + components.get('post', 'pid', pid).fadeOut(500, function () { + $(this).remove(); + }); + } + forkCommit.removeAttr('disabled'); + if (err) { + return alerts.error(err.message); + } + + alerts.alert({ + timeout: 5000, + title: '[[global:alert.success]]', + message: '[[topic:fork_success]]', + type: 'success', + clickfn: function () { + ajaxify.go('topic/' + newTopic.slug); + }, + }); + + postSelect.pids.forEach(function (pid) { + fadeOutAndRemove(pid); + }); + + closeForkModal(); + }); + } + + function showPostsSelected() { + if (postSelect.pids.length) { + forkModal.find('#fork-pids').translateHtml('[[topic:fork_pid_count, ' + postSelect.pids.length + ']]'); + } else { + forkModal.find('#fork-pids').translateHtml('[[topic:fork_no_pids]]'); + } + } + + function checkForkButtonEnable() { + if (forkModal.find('#fork-title').val().length && postSelect.pids.length) { + forkCommit.removeAttr('disabled'); + } else { + forkCommit.attr('disabled', true); + } + } + + function closeForkModal() { + if (forkModal) { + forkModal.remove(); + forkModal = null; + postSelect.disable(); + } + } + + return Fork; +}); diff --git a/public/src/client/topic/images.js b/public/src/client/topic/images.js new file mode 100644 index 0000000000..38ae16745f --- /dev/null +++ b/public/src/client/topic/images.js @@ -0,0 +1,34 @@ +'use strict'; + + +define('forum/topic/images', [], function () { + const Images = {}; + + Images.wrapImagesInLinks = function (posts) { + posts.find('[component="post/content"] img:not(.emoji)').each(function () { + const $this = $(this); + let src = $this.attr('src') || ''; + const alt = $this.attr('alt') || ''; + const suffixRegex = /-resized(\.[\w]+)?$/; + + if (src === 'about:blank') { + return; + } + + if (utils.isRelativeUrl(src) && suffixRegex.test(src)) { + src = src.replace(suffixRegex, '$1'); + } + const srcExt = src.split('.').slice(1).pop(); + const altFilename = alt.split('/').pop(); + const altExt = altFilename.split('.').slice(1).pop(); + + if (!$this.parent().is('a')) { + $this.wrap(''); + } + }); + }; + + return Images; +}); diff --git a/public/src/client/topic/merge.js b/public/src/client/topic/merge.js new file mode 100644 index 0000000000..4f074a1cf6 --- /dev/null +++ b/public/src/client/topic/merge.js @@ -0,0 +1,144 @@ +'use strict'; + + +define('forum/topic/merge', ['search', 'alerts', 'api'], function (search, alerts, api) { + const Merge = {}; + let modal; + let mergeBtn; + + let selectedTids = {}; + + Merge.init = function (callback) { + callback = callback || function () {}; + if (modal) { + return; + } + app.parseAndTranslate('partials/merge_topics_modal', {}, function (html) { + modal = html; + + $('body').append(modal); + + mergeBtn = modal.find('#merge_topics_confirm'); + + modal.find('.close,#merge_topics_cancel').on('click', closeModal); + + $('#content').on('click', '[component="topic/select"]', onTopicClicked); + + showTopicsSelected(); + + mergeBtn.on('click', function () { + mergeTopics(mergeBtn); + }); + + search.enableQuickSearch({ + searchElements: { + inputEl: modal.find('.topic-search-input'), + resultEl: modal.find('.quick-search-container'), + }, + searchOptions: { + in: 'titles', + }, + }); + modal.on('click', '[data-tid]', function () { + if ($(this).attr('data-tid')) { + Merge.addTopic($(this).attr('data-tid')); + } + return false; + }); + + callback(); + }); + }; + + Merge.addTopic = function (tid, callback) { + callback = callback || function () {}; + api.get(`/topics/${tid}`, {}).then(function (topicData) { + const title = topicData ? topicData.title : 'No title'; + if (selectedTids[tid]) { + delete selectedTids[tid]; + } else { + selectedTids[tid] = title; + } + checkButtonEnable(); + showTopicsSelected(); + callback(); + }).catch(alerts.error); + }; + + function onTopicClicked(ev) { + if (!modal) { + return; + } + const tid = $(this).parents('[component="category/topic"]').attr('data-tid'); + Merge.addTopic(tid); + + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + + function mergeTopics(btn) { + btn.attr('disabled', true); + const tids = Object.keys(selectedTids); + const options = {}; + if (modal.find('.merge-main-topic-radio').is(':checked')) { + options.mainTid = modal.find('.merge-main-topic-select').val(); + } else if (modal.find('.merge-new-title-radio').is(':checked')) { + options.newTopicTitle = modal.find('.merge-new-title-input').val(); + } + + socket.emit('topics.merge', { tids: tids, options: options }, function (err, tid) { + btn.removeAttr('disabled'); + if (err) { + return alerts.error(err); + } + ajaxify.go('/topic/' + tid); + closeModal(); + }); + } + + function showTopicsSelected() { + if (!modal) { + return; + } + const tids = Object.keys(selectedTids); + tids.sort(function (a, b) { + return a - b; + }); + + const topics = tids.map(function (tid) { + return { tid: tid, title: selectedTids[tid] }; + }); + + if (tids.length) { + app.parseAndTranslate('partials/merge_topics_modal', { + config: config, + topics: topics, + }, function (html) { + modal.find('.topics-section').html(html.find('.topics-section').html()); + modal.find('.merge-main-topic-select').html(html.find('.merge-main-topic-select').html()); + }); + } else { + modal.find('.topics-section').translateHtml('[[error:no-topics-selected]]'); + } + } + + function checkButtonEnable() { + if (Object.keys(selectedTids).length) { + mergeBtn.removeAttr('disabled'); + } else { + mergeBtn.attr('disabled', true); + } + } + + function closeModal() { + if (modal) { + modal.remove(); + modal = null; + } + selectedTids = {}; + $('#content').off('click', '[component="topic/select"]', onTopicClicked); + } + + return Merge; +}); diff --git a/public/src/client/topic/move-post.js b/public/src/client/topic/move-post.js new file mode 100644 index 0000000000..181bb33c1a --- /dev/null +++ b/public/src/client/topic/move-post.js @@ -0,0 +1,167 @@ +'use strict'; + + +define('forum/topic/move-post', [ + 'components', 'postSelect', 'translator', 'alerts', 'api', +], function (components, postSelect, translator, alerts, api) { + const MovePost = {}; + + let moveModal; + let moveCommit; + let fromTid; + + MovePost.init = function (postEl) { + if (moveModal) { + return; + } + fromTid = ajaxify.data.tid; + app.parseAndTranslate('modals/move-post', {}, function (html) { + moveModal = html; + + moveCommit = moveModal.find('#move_posts_confirm'); + + $('body').append(moveModal); + + moveModal.find('.close,#move_posts_cancel').on('click', closeMoveModal); + moveModal.find('#topicId').on('keyup', utils.debounce(checkMoveButtonEnable, 200)); + postSelect.init(onPostToggled); + showPostsSelected(); + + if (postEl) { + postSelect.togglePostSelection(postEl, postEl.attr('data-pid')); + } + + $(window).off('action:ajaxify.end', onAjaxifyEnd) + .on('action:ajaxify.end', onAjaxifyEnd); + + moveCommit.on('click', function () { + const targetTid = getTargetTid(); + if (!targetTid) { + return; + } + moveCommit.attr('disabled', true); + const data = { + pids: postSelect.pids.slice(), + tid: targetTid, + }; + if (config.undoTimeout > 0) { + return alerts.alert({ + alert_id: 'pids_move_' + postSelect.pids.join('-'), + title: '[[topic:thread_tools.move-posts]]', + message: '[[topic:topic_move_posts_success]]', + type: 'success', + timeout: 10000, + timeoutfn: function () { + movePosts(data); + }, + clickfn: function (alert, params) { + delete params.timeoutfn; + alerts.success('[[topic:topic_move_posts_undone]]'); + moveCommit.removeAttr('disabled'); + }, + }); + } + + movePosts(data); + }); + }); + }; + + function onAjaxifyEnd() { + if (!moveModal) { + return; + } + const tidInput = moveModal.find('#topicId'); + let targetTid = null; + if (ajaxify.data.template.topic && ajaxify.data.tid && + parseInt(ajaxify.data.tid, 10) !== fromTid + ) { + targetTid = ajaxify.data.tid; + } + if (targetTid && !tidInput.val()) { + tidInput.val(targetTid); + } + checkMoveButtonEnable(); + } + + function getTargetTid() { + const tidInput = moveModal.find('#topicId'); + if (tidInput.length && tidInput.val()) { + return tidInput.val(); + } + return ajaxify.data.template.topic && ajaxify.data.tid; + } + + function showPostsSelected() { + if (!moveModal) { + return; + } + const targetTid = getTargetTid(); + if (postSelect.pids.length) { + if (targetTid && parseInt(targetTid, 10) !== parseInt(fromTid, 10)) { + api.get('/topics/' + targetTid, {}).then(function (data) { + if (!data || !data.tid) { + return alerts.error('[[error:no-topic]]'); + } + if (data.scheduled) { + return alerts.error('[[error:cant-move-posts-to-scheduled]]'); + } + const translateStr = translator.compile('topic:x-posts-will-be-moved-to-y', postSelect.pids.length, data.title); + moveModal.find('#pids').translateHtml(translateStr); + }); + } else { + moveModal.find('#pids').translateHtml('[[topic:x-posts-selected, ' + postSelect.pids.length + ']]'); + } + } else { + moveModal.find('#pids').translateHtml('[[topic:no-posts-selected]]'); + } + } + + function checkMoveButtonEnable() { + if (!moveModal) { + return; + } + const targetTid = getTargetTid(); + if (postSelect.pids.length && targetTid && + parseInt(targetTid, 10) !== parseInt(fromTid, 10) + ) { + moveCommit.removeAttr('disabled'); + } else { + moveCommit.attr('disabled', true); + } + showPostsSelected(); + } + + function onPostToggled() { + checkMoveButtonEnable(); + } + + function movePosts(data) { + if (!data.tid) { + return; + } + + Promise.all(data.pids.map(pid => api.put(`/posts/${pid}/move`, { + tid: data.tid, + }))).then(() => { + data.pids.forEach(function (pid) { + components.get('post', 'pid', pid).fadeOut(500, function () { + $(this).remove(); + }); + }); + + closeMoveModal(); + }).catch(alerts.error); + } + + function closeMoveModal() { + if (moveModal) { + moveModal.remove(); + moveModal = null; + postSelect.disable(); + $(window).off('action:ajaxify.end', onAjaxifyEnd); + } + } + + return MovePost; +}); diff --git a/public/src/client/topic/move.js b/public/src/client/topic/move.js new file mode 100644 index 0000000000..a8e47ec7df --- /dev/null +++ b/public/src/client/topic/move.js @@ -0,0 +1,102 @@ +'use strict'; + + +define('forum/topic/move', ['categorySelector', 'alerts', 'hooks'], function (categorySelector, alerts, hooks) { + const Move = {}; + let modal; + let selectedCategory; + + Move.init = function (tids, currentCid, onComplete) { + Move.tids = tids; + Move.currentCid = currentCid; + Move.onComplete = onComplete; + Move.moveAll = !tids; + + showModal(); + }; + + function showModal() { + app.parseAndTranslate('partials/move_thread_modal', {}, function (html) { + modal = html; + modal.on('hidden.bs.modal', function () { + modal.remove(); + }); + + modal.find('#move-confirm').addClass('hide'); + + if (Move.moveAll || (Move.tids && Move.tids.length > 1)) { + modal.find('.modal-header h3').translateText('[[topic:move_topics]]'); + } + + categorySelector.init(modal.find('[component="category-selector"]'), { + onSelect: onCategorySelected, + privilege: 'moderate', + }); + + modal.find('#move_thread_commit').on('click', onCommitClicked); + + modal.modal('show'); + }); + } + + function onCategorySelected(category) { + selectedCategory = category; + modal.find('#move_thread_commit').prop('disabled', false); + } + + function onCommitClicked() { + const commitEl = modal.find('#move_thread_commit'); + + if (!commitEl.prop('disabled') && selectedCategory && selectedCategory.cid) { + commitEl.prop('disabled', true); + + modal.modal('hide'); + let message = '[[topic:topic_move_success, ' + selectedCategory.name + ']]'; + if (Move.tids && Move.tids.length > 1) { + message = '[[topic:topic_move_multiple_success, ' + selectedCategory.name + ']]'; + } else if (!Move.tids) { + message = '[[topic:topic_move_all_success, ' + selectedCategory.name + ']]'; + } + const data = { + tids: Move.tids ? Move.tids.slice() : null, + cid: selectedCategory.cid, + currentCid: Move.currentCid, + onComplete: Move.onComplete, + }; + if (config.undoTimeout > 0) { + return alerts.alert({ + alert_id: 'tids_move_' + (Move.tids ? Move.tids.join('-') : 'all'), + title: '[[topic:thread_tools.move]]', + message: message, + type: 'success', + timeout: config.undoTimeout, + timeoutfn: function () { + moveTopics(data); + }, + clickfn: function (alert, params) { + delete params.timeoutfn; + alerts.success('[[topic:topic_move_undone]]'); + }, + }); + } + + moveTopics(data); + } + } + + function moveTopics(data) { + hooks.fire('action:topic.move', data); + + socket.emit(!data.tids ? 'topics.moveAll' : 'topics.move', data, function (err) { + if (err) { + return alerts.error(err); + } + + if (typeof data.onComplete === 'function') { + data.onComplete(); + } + }); + } + + return Move; +}); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js new file mode 100644 index 0000000000..2bbc86dafd --- /dev/null +++ b/public/src/client/topic/postTools.js @@ -0,0 +1,545 @@ +'use strict'; + + +define('forum/topic/postTools', [ + 'share', + 'navigator', + 'components', + 'translator', + 'forum/topic/votes', + 'api', + 'bootbox', + 'alerts', + 'hooks', +], function (share, navigator, components, translator, votes, api, bootbox, alerts, hooks) { + const PostTools = {}; + + let staleReplyAnyway = false; + + PostTools.init = function (tid) { + staleReplyAnyway = false; + + renderMenu(); + + addPostHandlers(tid); + + share.addShareHandlers(ajaxify.data.titleRaw); + + votes.addVoteHandler(); + + PostTools.updatePostCount(ajaxify.data.postcount); + }; + + function renderMenu() { + $('[component="topic"]').on('show.bs.dropdown', '.moderator-tools', function () { + const $this = $(this); + const dropdownMenu = $this.find('.dropdown-menu'); + if (dropdownMenu.html()) { + return; + } + const postEl = $this.parents('[data-pid]'); + const pid = postEl.attr('data-pid'); + const index = parseInt(postEl.attr('data-index'), 10); + + socket.emit('posts.loadPostTools', { pid: pid, cid: ajaxify.data.cid }, async (err, data) => { + if (err) { + return alerts.error(err); + } + data.posts.display_move_tools = data.posts.display_move_tools && index !== 0; + + const html = await app.parseAndTranslate('partials/topic/post-menu-list', data); + const clipboard = require('clipboard'); + + dropdownMenu.html(html); + dropdownMenu.get(0).classList.toggle('hidden', false); + new clipboard('[data-clipboard-text]'); + + hooks.fire('action:post.tools.load', { + element: dropdownMenu, + }); + }); + }); + } + + PostTools.toggle = function (pid, isDeleted) { + const postEl = components.get('post', 'pid', pid); + + postEl.find('[component="post/quote"], [component="post/bookmark"], [component="post/reply"], [component="post/flag"], [component="user/chat"]') + .toggleClass('hidden', isDeleted); + + postEl.find('[component="post/delete"]').toggleClass('hidden', isDeleted).parent().attr('hidden', isDeleted ? '' : null); + postEl.find('[component="post/restore"]').toggleClass('hidden', !isDeleted).parent().attr('hidden', !isDeleted ? '' : null); + postEl.find('[component="post/purge"]').toggleClass('hidden', !isDeleted).parent().attr('hidden', !isDeleted ? '' : null); + + PostTools.removeMenu(postEl); + }; + + PostTools.removeMenu = function (postEl) { + postEl.find('[component="post/tools"] .dropdown-menu').html(''); + }; + + PostTools.updatePostCount = function (postCount) { + const postCountEl = components.get('topic/post-count'); + postCountEl.html(postCount).attr('title', postCount); + utils.makeNumbersHumanReadable(postCountEl); + navigator.setCount(postCount); + }; + + function addPostHandlers(tid) { + const postContainer = components.get('topic'); + + handleSelectionTooltip(); + + postContainer.on('click', '[component="post/quote"]', function () { + onQuoteClicked($(this), tid); + }); + + postContainer.on('click', '[component="post/reply"]', function () { + onReplyClicked($(this), tid); + }); + + $('.topic').on('click', '[component="topic/reply"]', function (e) { + e.preventDefault(); + onReplyClicked($(this), tid); + }); + + $('.topic').on('click', '[component="topic/reply-as-topic"]', function () { + translator.translate('[[topic:link_back, ' + ajaxify.data.titleRaw + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function (body) { + hooks.fire('action:composer.topic.new', { + cid: ajaxify.data.cid, + body: body, + }); + }); + }); + + postContainer.on('click', '[component="post/bookmark"]', function () { + return bookmarkPost($(this), getData($(this), 'data-pid')); + }); + + postContainer.on('click', '[component="post/upvote"]', function () { + return votes.toggleVote($(this), '.upvoted', 1); + }); + + postContainer.on('click', '[component="post/downvote"]', function () { + return votes.toggleVote($(this), '.downvoted', -1); + }); + + postContainer.on('click', '[component="post/vote-count"]', function () { + votes.showVotes(getData($(this), 'data-pid')); + }); + + postContainer.on('click', '[component="post/flag"]', function () { + const pid = getData($(this), 'data-pid'); + require(['flags'], function (flags) { + flags.showFlagModal({ + type: 'post', + id: pid, + }); + }); + }); + + postContainer.on('click', '[component="post/flagUser"]', function () { + const uid = getData($(this), 'data-uid'); + require(['flags'], function (flags) { + flags.showFlagModal({ + type: 'user', + id: uid, + }); + }); + }); + + postContainer.on('click', '[component="post/flagResolve"]', function () { + const flagId = $(this).attr('data-flagId'); + require(['flags'], function (flags) { + flags.resolve(flagId); + }); + }); + + postContainer.on('click', '[component="post/edit"]', function () { + const btn = $(this); + + const timestamp = parseInt(getData(btn, 'data-timestamp'), 10); + const postEditDuration = parseInt(ajaxify.data.postEditDuration, 10); + + if (checkDuration(postEditDuration, timestamp, 'post-edit-duration-expired')) { + hooks.fire('action:composer.post.edit', { + pid: getData(btn, 'data-pid'), + }); + } + }); + + if (config.enablePostHistory && ajaxify.data.privileges['posts:history']) { + postContainer.on('click', '[component="post/view-history"], [component="post/edit-indicator"]', function () { + const btn = $(this); + require(['forum/topic/diffs'], function (diffs) { + diffs.open(getData(btn, 'data-pid')); + }); + }); + } + + postContainer.on('click', '[component="post/delete"]', function () { + const btn = $(this); + const timestamp = parseInt(getData(btn, 'data-timestamp'), 10); + const postDeleteDuration = parseInt(ajaxify.data.postDeleteDuration, 10); + if (checkDuration(postDeleteDuration, timestamp, 'post-delete-duration-expired')) { + togglePostDelete($(this)); + } + }); + + function checkDuration(duration, postTimestamp, languageKey) { + if (!ajaxify.data.privileges.isAdminOrMod && duration && Date.now() - postTimestamp > duration * 1000) { + const numDays = Math.floor(duration / 86400); + const numHours = Math.floor((duration % 86400) / 3600); + const numMinutes = Math.floor(((duration % 86400) % 3600) / 60); + const numSeconds = ((duration % 86400) % 3600) % 60; + let msg = '[[error:' + languageKey + ', ' + duration + ']]'; + if (numDays) { + if (numHours) { + msg = '[[error:' + languageKey + '-days-hours, ' + numDays + ', ' + numHours + ']]'; + } else { + msg = '[[error:' + languageKey + '-days, ' + numDays + ']]'; + } + } else if (numHours) { + if (numMinutes) { + msg = '[[error:' + languageKey + '-hours-minutes, ' + numHours + ', ' + numMinutes + ']]'; + } else { + msg = '[[error:' + languageKey + '-hours, ' + numHours + ']]'; + } + } else if (numMinutes) { + if (numSeconds) { + msg = '[[error:' + languageKey + '-minutes-seconds, ' + numMinutes + ', ' + numSeconds + ']]'; + } else { + msg = '[[error:' + languageKey + '-minutes, ' + numMinutes + ']]'; + } + } + alerts.error(msg); + return false; + } + return true; + } + + postContainer.on('click', '[component="post/restore"]', function () { + togglePostDelete($(this)); + }); + + postContainer.on('click', '[component="post/purge"]', function () { + purgePost($(this)); + }); + + postContainer.on('click', '[component="post/move"]', function () { + const btn = $(this); + require(['forum/topic/move-post'], function (movePost) { + movePost.init(btn.parents('[data-pid]')); + }); + }); + + postContainer.on('click', '[component="post/change-owner"]', function () { + const btn = $(this); + require(['forum/topic/change-owner'], function (changeOwner) { + changeOwner.init(btn.parents('[data-pid]')); + }); + }); + + postContainer.on('click', '[component="post/ban-ip"]', function () { + const ip = $(this).attr('data-ip'); + socket.emit('blacklist.addRule', ip, function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[admin/manage/blacklist:ban-ip]]'); + }); + }); + + postContainer.on('click', '[component="post/chat"]', function () { + openChat($(this)); + }); + } + + async function onReplyClicked(button, tid) { + const selectedNode = await getSelectedNode(); + + showStaleWarning(async function () { + let username = await getUserSlug(button); + if (getData(button, 'data-uid') === '0' || !getData(button, 'data-userslug')) { + username = ''; + } + + const toPid = button.is('[component="post/reply"]') ? getData(button, 'data-pid') : null; + const isQuoteToPid = !toPid || !selectedNode.pid || toPid === selectedNode.pid; + + if (selectedNode.text && isQuoteToPid) { + username = username || selectedNode.username; + hooks.fire('action:composer.addQuote', { + tid: tid, + pid: toPid, + topicName: ajaxify.data.titleRaw, + username: username, + text: selectedNode.text, + selectedPid: selectedNode.pid, + }); + } else { + hooks.fire('action:composer.post.new', { + tid: tid, + pid: toPid, + topicName: ajaxify.data.titleRaw, + text: username ? username + ' ' : ($('[component="topic/quickreply/text"]').val() || ''), + }); + } + }); + } + + async function onQuoteClicked(button, tid) { + const selectedNode = await getSelectedNode(); + + showStaleWarning(async function () { + const username = await getUserSlug(button); + const toPid = getData(button, 'data-pid'); + + function quote(text) { + hooks.fire('action:composer.addQuote', { + tid: tid, + pid: toPid, + username: username, + topicName: ajaxify.data.titleRaw, + text: text, + }); + } + + if (selectedNode.text && toPid && toPid === selectedNode.pid) { + return quote(selectedNode.text); + } + socket.emit('posts.getRawPost', toPid, function (err, post) { + if (err) { + return alerts.error(err); + } + + quote(post); + }); + }); + } + + async function getSelectedNode() { + let selectedText = ''; + let selectedPid; + let username = ''; + const selection = window.getSelection ? window.getSelection() : document.selection.createRange(); + const postContents = $('[component="post"] [component="post/content"]'); + let content; + postContents.each(function (index, el) { + if (selection && selection.containsNode && el && selection.containsNode(el, true)) { + content = el; + } + }); + + if (content) { + const bounds = document.createRange(); + bounds.selectNodeContents(content); + const range = selection.getRangeAt(0).cloneRange(); + if (range.compareBoundaryPoints(Range.START_TO_START, bounds) < 0) { + range.setStart(bounds.startContainer, bounds.startOffset); + } + if (range.compareBoundaryPoints(Range.END_TO_END, bounds) > 0) { + range.setEnd(bounds.endContainer, bounds.endOffset); + } + bounds.detach(); + selectedText = range.toString(); + const postEl = $(content).parents('[component="post"]'); + selectedPid = postEl.attr('data-pid'); + username = await getUserSlug($(content)); + range.detach(); + } + return { text: selectedText, pid: selectedPid, username: username }; + } + + function bookmarkPost(button, pid) { + const method = button.attr('data-bookmarked') === 'false' ? 'put' : 'del'; + + api[method](`/posts/${pid}/bookmark`, undefined, function (err) { + if (err) { + return alerts.error(err); + } + const type = method === 'put' ? 'bookmark' : 'unbookmark'; + hooks.fire(`action:post.${type}`, { pid: pid }); + }); + return false; + } + + function getData(button, data) { + return button.parents('[data-pid]').attr(data); + } + + function getUserSlug(button) { + return new Promise((resolve) => { + let slug = ''; + if (button.attr('component') === 'topic/reply') { + resolve(slug); + return; + } + const post = button.parents('[data-pid]'); + if (post.length) { + require(['slugify'], function (slugify) { + slug = slugify(post.attr('data-username'), true); + if (!slug) { + if (post.attr('data-uid') !== '0') { + slug = '[[global:former_user]]'; + } else { + slug = '[[global:guest]]'; + } + } + if (slug && slug !== '[[global:former_user]]' && slug !== '[[global:guest]]') { + slug = '@' + slug; + } + resolve(slug); + }); + return; + } + + resolve(slug); + }); + } + + function togglePostDelete(button) { + const pid = getData(button, 'data-pid'); + const postEl = components.get('post', 'pid', pid); + const action = !postEl.hasClass('deleted') ? 'delete' : 'restore'; + + postAction(action, pid); + } + + function purgePost(button) { + postAction('purge', getData(button, 'data-pid')); + } + + async function postAction(action, pid) { + ({ action } = await hooks.fire(`static:post.${action}`, { action, pid })); + if (!action) { + return; + } + + bootbox.confirm('[[topic:post_' + action + '_confirm]]', function (confirm) { + if (!confirm) { + return; + } + + const route = action === 'purge' ? '' : '/state'; + const method = action === 'restore' ? 'put' : 'del'; + api[method](`/posts/${pid}${route}`).catch(alerts.error); + }); + } + + function openChat(button) { + const post = button.parents('[data-pid]'); + require(['chat'], function (chat) { + chat.newChat(post.attr('data-uid')); + }); + button.parents('.btn-group').find('.dropdown-toggle').click(); + return false; + } + + function showStaleWarning(callback) { + const staleThreshold = + Math.min(Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays), 8640000000000000); + if (staleReplyAnyway || ajaxify.data.lastposttime >= staleThreshold) { + return callback(); + } + + const warning = bootbox.dialog({ + title: '[[topic:stale.title]]', + message: '[[topic:stale.warning]]', + buttons: { + reply: { + label: '[[topic:stale.reply_anyway]]', + className: 'btn-link', + callback: function () { + staleReplyAnyway = true; + callback(); + }, + }, + create: { + label: '[[topic:stale.create]]', + className: 'btn-primary', + callback: function () { + translator.translate('[[topic:link_back, ' + ajaxify.data.title + ', ' + config.relative_path + '/topic/' + ajaxify.data.slug + ']]', function (body) { + hooks.fire('action:composer.topic.new', { + cid: ajaxify.data.cid, + body: body, + fromStaleTopic: true, + }); + }); + }, + }, + }, + }); + + warning.modal(); + } + + const selectionChangeFn = utils.debounce(selectionChange, 100); + + function handleSelectionTooltip() { + if (!ajaxify.data.privileges['topics:reply']) { + return; + } + + hooks.onPage('action:posts.loaded', delayedTooltip); + + $(document).off('selectionchange', selectionChangeFn).on('selectionchange', selectionChangeFn); + } + + function selectionChange() { + const selectionEmpty = window.getSelection().toString() === ''; + if (selectionEmpty) { + $('[component="selection/tooltip"]').addClass('hidden'); + } else { + delayedTooltip(); + } + } + + async function delayedTooltip() { + let selectionTooltip = $('[component="selection/tooltip"]'); + selectionTooltip.addClass('hidden'); + if (selectionTooltip.attr('data-ajaxify') === '1') { + selectionTooltip.remove(); + return; + } + + const selection = window.getSelection(); + if (selection.focusNode && selection.type === 'Range' && ajaxify.data.template.topic) { + const focusNode = $(selection.focusNode); + const anchorNode = $(selection.anchorNode); + const firstPid = anchorNode.parents('[data-pid]').attr('data-pid'); + const lastPid = focusNode.parents('[data-pid]').attr('data-pid'); + if (firstPid !== lastPid || !focusNode.parents('[component="post/content"]').length || !anchorNode.parents('[component="post/content"]').length) { + return; + } + const postEl = focusNode.parents('[data-pid]'); + const selectionRange = selection.getRangeAt(0); + if (!postEl.length || selectionRange.collapsed) { + return; + } + const rects = selectionRange.getClientRects(); + const lastRect = rects[rects.length - 1]; + + if (!selectionTooltip.length) { + selectionTooltip = await app.parseAndTranslate('partials/topic/selection-tooltip', ajaxify.data); + selectionTooltip.addClass('hidden').appendTo('body'); + } + selectionTooltip.off('click').on('click', '[component="selection/tooltip/quote"]', function () { + selectionTooltip.addClass('hidden'); + onQuoteClicked(postEl.find('[component="post/quote"]'), ajaxify.data.tid); + }); + selectionTooltip.removeClass('hidden'); + $(window).one('action:ajaxify.start', function () { + selectionTooltip.attr('data-ajaxify', 1).addClass('hidden'); + $(document).off('selectionchange', selectionChangeFn); + }); + const tooltipWidth = selectionTooltip.outerWidth(true); + selectionTooltip.css({ + top: lastRect.bottom + $(window).scrollTop(), + left: tooltipWidth > lastRect.width ? lastRect.left : lastRect.left + lastRect.width - tooltipWidth, + }); + } + } + + return PostTools; +}); diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js new file mode 100644 index 0000000000..41363542ae --- /dev/null +++ b/public/src/client/topic/posts.js @@ -0,0 +1,443 @@ +'use strict'; + + +define('forum/topic/posts', [ + 'forum/pagination', + 'forum/infinitescroll', + 'forum/topic/postTools', + 'forum/topic/images', + 'navigator', + 'components', + 'translator', + 'hooks', + 'helpers', +], function (pagination, infinitescroll, postTools, images, navigator, components, translator, hooks, helpers) { + const Posts = { }; + + Posts.signaturesShown = {}; + + Posts.onNewPost = function (data) { + if ( + !data || + !data.posts || + !data.posts.length || + parseInt(data.posts[0].tid, 10) !== parseInt(ajaxify.data.tid, 10) + ) { + return; + } + + data.loggedIn = !!app.user.uid; + data.privileges = ajaxify.data.privileges; + + // if not a scheduled topic, prevent timeago in future by setting timestamp to 1 sec behind now + data.posts[0].timestamp = data.posts[0].topic.scheduled ? data.posts[0].timestamp : Date.now() - 1000; + data.posts[0].timestampISO = utils.toISOString(data.posts[0].timestamp); + + Posts.modifyPostsByPrivileges(data.posts); + + updatePostCounts(data.posts); + + updatePostIndices(data.posts); + + ajaxify.data.postcount += 1; + postTools.updatePostCount(ajaxify.data.postcount); + + if (config.usePagination) { + onNewPostPagination(data); + } else { + onNewPostInfiniteScroll(data); + } + + require(['forum/topic/replies'], function (replies) { + replies.onNewPost(data); + }); + }; + + Posts.modifyPostsByPrivileges = function (posts) { + posts.forEach(function (post) { + post.selfPost = !!app.user.uid && parseInt(post.uid, 10) === parseInt(app.user.uid, 10); + post.topicOwnerPost = parseInt(post.uid, 10) === parseInt(ajaxify.data.uid, 10); + + post.display_edit_tools = (ajaxify.data.privileges['posts:edit'] && post.selfPost) || ajaxify.data.privileges.isAdminOrMod; + post.display_delete_tools = (ajaxify.data.privileges['posts:delete'] && post.selfPost) || ajaxify.data.privileges.isAdminOrMod; + post.display_moderator_tools = post.display_edit_tools || post.display_delete_tools; + post.display_move_tools = ajaxify.data.privileges.isAdminOrMod; + post.display_post_menu = ajaxify.data.privileges.isAdminOrMod || + (post.selfPost && !ajaxify.data.locked && !post.deleted) || + (post.selfPost && post.deleted && parseInt(post.deleterUid, 10) === parseInt(app.user.uid, 10)) || + ((app.user.uid || ajaxify.data.postSharing.length) && !post.deleted); + }); + }; + + function updatePostCounts(posts) { + for (let i = 0; i < posts.length; i += 1) { + const cmp = components.get('user/postcount', posts[i].uid); + cmp.html(parseInt(cmp.attr('data-postcount'), 10) + 1); + utils.addCommasToNumbers(cmp); + } + } + + function updatePostIndices(posts) { + if (config.topicPostSort === 'newest_to_oldest') { + posts[0].index = 1; + components.get('post').not('[data-index=0]').each(function () { + const newIndex = parseInt($(this).attr('data-index'), 10) + 1; + $(this).attr('data-index', newIndex); + }); + } + } + + function onNewPostPagination(data) { + function scrollToPost() { + scrollToPostIfSelf(data.posts[0]); + } + + const posts = data.posts; + + ajaxify.data.pagination.pageCount = Math.max(1, Math.ceil(posts[0].topic.postcount / config.postsPerPage)); + const direction = config.topicPostSort === 'oldest_to_newest' || config.topicPostSort === 'most_votes' ? 1 : -1; + + const isPostVisible = ( + ajaxify.data.pagination.currentPage === ajaxify.data.pagination.pageCount && + direction === 1 + ) || (ajaxify.data.pagination.currentPage === 1 && direction === -1); + + if (isPostVisible) { + const repliesSelector = $('[component="post"]:not([data-index=0]), [component="topic/event"]'); + createNewPosts(data, repliesSelector, direction, false, scrollToPost); + } else if (ajaxify.data.scrollToMyPost && parseInt(posts[0].uid, 10) === parseInt(app.user.uid, 10)) { + // https://github.com/NodeBB/NodeBB/issues/5004#issuecomment-247157441 + setTimeout(function () { + pagination.loadPage(ajaxify.data.pagination.pageCount, scrollToPost); + }, 250); + } else { + updatePagination(); + } + } + + function updatePagination() { + $.get(config.relative_path + '/api/topic/pagination/' + ajaxify.data.tid, { page: ajaxify.data.pagination.currentPage }, function (paginationData) { + app.parseAndTranslate('partials/paginator', paginationData, function (html) { + $('[component="pagination"]').after(html).remove(); + }); + }); + } + + function onNewPostInfiniteScroll(data) { + const direction = (config.topicPostSort === 'oldest_to_newest' || config.topicPostSort === 'most_votes') ? 1 : -1; + + const isPreviousPostAdded = $('[component="post"][data-index="' + (data.posts[0].index - 1) + '"]').length; + if (!isPreviousPostAdded && (!data.posts[0].selfPost || !ajaxify.data.scrollToMyPost)) { + return; + } + + if (!isPreviousPostAdded && data.posts[0].selfPost) { + return ajaxify.go('post/' + data.posts[0].pid); + } + const repliesSelector = $('[component="topic"]>[component="post"]:not([data-index=0]), [component="topic"]>[component="topic/event"]'); + createNewPosts(data, repliesSelector, direction, false, function (html) { + if (html) { + html.addClass('new'); + } + scrollToPostIfSelf(data.posts[0]); + }); + } + + function scrollToPostIfSelf(post) { + if (post.selfPost && ajaxify.data.scrollToMyPost) { + navigator.scrollBottom(post.index); + } + } + + function createNewPosts(data, repliesSelector, direction, userScrolled, callback) { + callback = callback || function () {}; + if (!data || (data.posts && !data.posts.length)) { + return callback(); + } + + function removeAlreadyAddedPosts() { + const newPosts = $('[component="post"].new'); + + if (newPosts.length === data.posts.length) { + let allSamePids = true; + newPosts.each(function (index, el) { + if (parseInt($(el).attr('data-pid'), 10) !== parseInt(data.posts[index].pid, 10)) { + allSamePids = false; + } + }); + + if (allSamePids) { + newPosts.each(function () { + $(this).removeClass('new'); + }); + data.posts.length = 0; + return; + } + } + + if (newPosts.length && data.posts.length > 1) { + data.posts.forEach(function (post) { + const p = components.get('post', 'pid', post.pid); + if (p.hasClass('new')) { + p.remove(); + } + }); + } + + data.posts = data.posts.filter(function (post) { + return post.index === -1 || $('[component="post"][data-pid="' + post.pid + '"]').length === 0; + }); + } + + removeAlreadyAddedPosts(); + + if (!data.posts.length) { + return callback(); + } + + let after; + let before; + + if (direction > 0 && repliesSelector.length) { + after = repliesSelector.last(); + } else if (direction < 0 && repliesSelector.length) { + before = repliesSelector.first(); + } + + hooks.fire('action:posts.loading', { posts: data.posts, after: after, before: before }); + + app.parseAndTranslate('topic', 'posts', Object.assign({}, ajaxify.data, data), function (html) { + html = html.filter(function () { + const $this = $(this); + const pid = $this.attr('data-pid'); + const index = parseInt($this.attr('data-index'), 10); + const isPost = $this.is('[component="post"]'); + return !isPost || index === -1 || (pid && $('[component="post"][data-pid="' + pid + '"]').length === 0); + }); + + if (after) { + html.insertAfter(after); + } else if (before) { + // Save document height and position for future reference (about 5 lines down) + const height = $(document).height(); + const scrollTop = $(window).scrollTop(); + + html.insertBefore(before); + + // Now restore the relative position the user was on prior to new post insertion + if (userScrolled || scrollTop > 0) { + $(window).scrollTop(scrollTop + ($(document).height() - height)); + } + } else { + components.get('topic').append(html); + } + + const removedEls = infinitescroll.removeExtra($('[component="post"]'), direction, Math.max(20, config.postsPerPage * 2)); + removeNecroPostMessages(removedEls); + + hooks.fire('action:posts.loaded', { posts: data.posts }); + + Posts.onNewPostsAddedToDom(html); + + callback(html); + }); + } + + Posts.loadMorePosts = function (direction) { + if (!components.get('topic').length || navigator.scrollActive) { + return; + } + + const replies = components.get('topic').find(components.get('post').not('[data-index=0]').not('.new')); + const afterEl = direction > 0 ? replies.last() : replies.first(); + const after = parseInt(afterEl.attr('data-index'), 10) || 0; + + const tid = ajaxify.data.tid; + if (!utils.isNumber(tid) || !utils.isNumber(after) || (direction < 0 && components.get('post', 'index', 0).length)) { + return; + } + + const indicatorEl = $('.loading-indicator'); + if (!indicatorEl.is(':animated')) { + indicatorEl.fadeIn(); + } + + infinitescroll.loadMore('topics.loadMore', { + tid: tid, + after: after + (direction > 0 ? 1 : 0), + count: config.postsPerPage, + direction: direction, + topicPostSort: config.topicPostSort, + }, function (data, done) { + indicatorEl.fadeOut(); + + if (data && data.posts && data.posts.length) { + const repliesSelector = $('[component="post"]:not([data-index=0]):not(.new), [component="topic/event"]'); + createNewPosts(data, repliesSelector, direction, true, done); + } else { + navigator.update(); + done(); + } + }); + }; + + Posts.onTopicPageLoad = function (posts) { + handlePrivateUploads(posts); + images.wrapImagesInLinks(posts); + hideDuplicateSignatures(posts); + Posts.showBottomPostBar(); + posts.find('[component="post/content"] img:not(.not-responsive)').addClass('img-responsive'); + Posts.addBlockquoteEllipses(posts); + hidePostToolsForDeletedPosts(posts); + addNecroPostMessage(); + }; + + Posts.addTopicEvents = function (events) { + if (config.topicPostSort === 'most_votes') { + return; + } + const html = helpers.renderEvents.call(ajaxify.data, events); + translator.translate(html, (translated) => { + if (config.topicPostSort === 'oldest_to_newest') { + $('[component="topic"]').append(translated); + } else if (config.topicPostSort === 'newest_to_oldest') { + const mainPost = $('[component="topic"] [component="post"][data-index="0"]'); + if (mainPost.length) { + $(translated).insertAfter(mainPost); + } else { + $('[component="topic"]').prepend(translated); + } + } + + $('[component="topic/event"] .timeago').timeago(); + }); + }; + + function addNecroPostMessage() { + const necroThreshold = ajaxify.data.necroThreshold * 24 * 60 * 60 * 1000; + if (!necroThreshold || (config.topicPostSort !== 'newest_to_oldest' && config.topicPostSort !== 'oldest_to_newest')) { + return; + } + + const postEls = $('[component="post"]').toArray(); + postEls.forEach(function (post) { + post = $(post); + const prev = post.prev('[component="post"]'); + if (post.is(':has(.necro-post)') || !prev.length) { + return; + } + if (config.topicPostSort === 'newest_to_oldest' && parseInt(prev.attr('data-index'), 10) === 0) { + return; + } + + const diff = post.attr('data-timestamp') - prev.attr('data-timestamp'); + if (Math.abs(diff) >= necroThreshold) { + const suffixAgo = $.timeago.settings.strings.suffixAgo; + const prefixAgo = $.timeago.settings.strings.prefixAgo; + const suffixFromNow = $.timeago.settings.strings.suffixFromNow; + const prefixFromNow = $.timeago.settings.strings.prefixFromNow; + + $.timeago.settings.strings.suffixAgo = ''; + $.timeago.settings.strings.prefixAgo = ''; + $.timeago.settings.strings.suffixFromNow = ''; + $.timeago.settings.strings.prefixFromNow = ''; + + const translationText = (diff > 0 ? '[[topic:timeago_later,' : '[[topic:timeago_earlier,') + $.timeago.inWords(diff) + ']]'; + + $.timeago.settings.strings.suffixAgo = suffixAgo; + $.timeago.settings.strings.prefixAgo = prefixAgo; + $.timeago.settings.strings.suffixFromNow = suffixFromNow; + $.timeago.settings.strings.prefixFromNow = prefixFromNow; + app.parseAndTranslate('partials/topic/necro-post', { text: translationText }, function (html) { + html.attr('data-necro-post-index', prev.attr('data-index')); + html.insertBefore(post); + }); + } + }); + } + + function hideDuplicateSignatures(posts) { + if (ajaxify.data['signatures:hideDuplicates']) { + posts.each((index, el) => { + const signatureEl = $(el).find('[component="post/signature"]'); + const uid = signatureEl.attr('data-uid'); + if (Posts.signaturesShown[uid]) { + signatureEl.addClass('hidden'); + } else { + Posts.signaturesShown[uid] = true; + } + }); + } + } + + function removeNecroPostMessages(removedPostEls) { + removedPostEls.each((index, el) => { + $(`[data-necro-post-index="${$(el).attr('data-index')}"]`).remove(); + }); + } + + function handlePrivateUploads(posts) { + if (app.user.uid || !ajaxify.data.privateUploads) { + return; + } + + // Replace all requests for uploaded images/files with a login link + const loginEl = document.createElement('a'); + loginEl.className = 'login-required'; + loginEl.href = config.relative_path + '/login'; + + translator.translate('[[topic:login-to-view]]', function (translated) { + loginEl.appendChild(document.createTextNode(translated)); + posts.each(function (idx, postEl) { + $(postEl).find('[component="post/content"] img').each(function (idx, imgEl) { + imgEl = $(imgEl); + if (imgEl.attr('src').startsWith(config.relative_path + config.upload_url)) { + imgEl.replaceWith(loginEl.cloneNode(true)); + } + }); + }); + }); + } + + Posts.onNewPostsAddedToDom = function (posts) { + Posts.onTopicPageLoad(posts); + + app.createUserTooltips(posts); + + utils.addCommasToNumbers(posts.find('.formatted-number')); + utils.makeNumbersHumanReadable(posts.find('.human-readable-number')); + posts.find('.timeago').timeago(); + }; + + Posts.showBottomPostBar = function () { + const mainPost = components.get('post', 'index', 0); + const placeHolder = $('.post-bar-placeholder'); + const posts = $('[component="post"]'); + if (!!mainPost.length && posts.length > 1 && $('.post-bar').length < 2 && placeHolder.length) { + $('.post-bar').clone().insertAfter(placeHolder); + placeHolder.remove(); + } else if (mainPost.length && posts.length < 2) { + mainPost.find('.post-bar').remove(); + } + }; + + function hidePostToolsForDeletedPosts(posts) { + posts.each(function () { + if ($(this).hasClass('deleted')) { + postTools.toggle($(this).attr('data-pid'), true); + } + }); + } + + Posts.addBlockquoteEllipses = function (posts) { + const blockquotes = posts.find('[component="post/content"] > blockquote > blockquote'); + blockquotes.each(function () { + const $this = $(this); + if ($this.find(':hidden:not(br)').length && !$this.find('.toggle').length) { + $this.append(''); + } + }); + }; + + return Posts; +}); diff --git a/public/src/client/topic/replies.js b/public/src/client/topic/replies.js new file mode 100644 index 0000000000..9862b75abf --- /dev/null +++ b/public/src/client/topic/replies.js @@ -0,0 +1,110 @@ +'use strict'; + + +define('forum/topic/replies', ['forum/topic/posts', 'hooks', 'alerts'], function (posts, hooks, alerts) { + const Replies = {}; + + Replies.init = function (button) { + const post = button.closest('[data-pid]'); + const pid = post.data('pid'); + const open = button.find('[component="post/replies/open"]'); + const loading = button.find('[component="post/replies/loading"]'); + const close = button.find('[component="post/replies/close"]'); + + if (open.is(':not(.hidden)') && loading.is('.hidden')) { + open.addClass('hidden'); + loading.removeClass('hidden'); + + socket.emit('posts.getReplies', pid, function (err, data) { + loading.addClass('hidden'); + if (err) { + open.removeClass('hidden'); + return alerts.error(err); + } + + close.removeClass('hidden'); + + posts.modifyPostsByPrivileges(data); + const tplData = { + posts: data, + privileges: ajaxify.data.privileges, + 'downvote:disabled': ajaxify.data['downvote:disabled'], + 'reputation:disabled': ajaxify.data['reputation:disabled'], + loggedIn: !!app.user.uid, + hideReplies: config.hasOwnProperty('showNestedReplies') ? !config.showNestedReplies : true, + }; + app.parseAndTranslate('topic', 'posts', tplData, function (html) { + const repliesEl = $('
    ', { component: 'post/replies' }).html(html).hide(); + if (button.attr('data-target-component')) { + post.find('[component="' + button.attr('data-target-component') + '"]').html(repliesEl); + } else { + repliesEl.insertAfter(button); + } + + repliesEl.slideDown('fast'); + posts.onNewPostsAddedToDom(html); + hooks.fire('action:posts.loaded', { posts: data }); + }); + }); + } else if (close.is(':not(.hidden)')) { + close.addClass('hidden'); + open.removeClass('hidden'); + loading.addClass('hidden'); + post.find('[component="post/replies"]').slideUp('fast', function () { + $(this).remove(); + }); + } + }; + + Replies.onNewPost = function (data) { + const post = data.posts[0]; + if (!post) { + return; + } + incrementCount(post, 1); + data.hideReplies = config.hasOwnProperty('showNestedReplies') ? !config.showNestedReplies : true; + app.parseAndTranslate('topic', 'posts', data, function (html) { + const replies = $('[component="post"][data-pid="' + post.toPid + '"] [component="post/replies"]').first(); + if (replies.length) { + if (config.topicPostSort === 'newest_to_oldest') { + replies.prepend(html); + } else { + replies.append(html); + } + posts.onNewPostsAddedToDom(html); + } + }); + }; + + Replies.onPostPurged = function (post) { + incrementCount(post, -1); + }; + + function incrementCount(post, inc) { + const replyCount = $('[component="post"][data-pid="' + post.toPid + '"]').find('[component="post/reply-count"]').first(); + const countEl = replyCount.find('[component="post/reply-count/text"]'); + const avatars = replyCount.find('[component="post/reply-count/avatars"]'); + const count = Math.max(0, parseInt(countEl.attr('data-replies'), 10) + inc); + const timestamp = replyCount.find('.timeago').attr('title', post.timestampISO); + + countEl.attr('data-replies', count); + replyCount.toggleClass('hidden', count <= 0); + if (count > 1) { + countEl.translateText('[[topic:replies_to_this_post, ' + count + ']]'); + } else { + countEl.translateText('[[topic:one_reply_to_this_post]]'); + } + + if (!avatars.find('[data-uid="' + post.uid + '"]').length && count < 7) { + app.parseAndTranslate('topic', 'posts', { posts: [{ replies: { users: [post.user] } }] }, function (html) { + avatars.prepend(html.find('[component="post/reply-count/avatars"] [component="avatar/picture"]')); + }); + } + + avatars.addClass('hasMore'); + + timestamp.data('timeago', null).timeago(); + } + + return Replies; +}); diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js new file mode 100644 index 0000000000..804711ab0f --- /dev/null +++ b/public/src/client/topic/threadTools.js @@ -0,0 +1,382 @@ +'use strict'; + + +define('forum/topic/threadTools', [ + 'components', + 'translator', + 'handleBack', + 'forum/topic/posts', + 'api', + 'hooks', + 'bootbox', + 'alerts', +], function (components, translator, handleBack, posts, api, hooks, bootbox, alerts) { + const ThreadTools = {}; + + ThreadTools.init = function (tid, topicContainer) { + renderMenu(topicContainer); + + // function topicCommand(method, path, command, onComplete) { + topicContainer.on('click', '[component="topic/delete"]', function () { + topicCommand('del', '/state', 'delete'); + return false; + }); + + topicContainer.on('click', '[component="topic/restore"]', function () { + topicCommand('put', '/state', 'restore'); + return false; + }); + + topicContainer.on('click', '[component="topic/purge"]', function () { + topicCommand('del', '', 'purge'); + return false; + }); + + topicContainer.on('click', '[component="topic/lock"]', function () { + topicCommand('put', '/lock', 'lock'); + return false; + }); + + topicContainer.on('click', '[component="topic/unlock"]', function () { + topicCommand('del', '/lock', 'unlock'); + return false; + }); + + topicContainer.on('click', '[component="topic/pin"]', function () { + topicCommand('put', '/pin', 'pin'); + return false; + }); + + topicContainer.on('click', '[component="topic/unpin"]', function () { + topicCommand('del', '/pin', 'unpin'); + return false; + }); + + topicContainer.on('click', '[component="topic/event/delete"]', function () { + const eventId = $(this).attr('data-topic-event-id'); + const eventEl = $(this).parents('[component="topic/event"]'); + bootbox.confirm('[[topic:delete-event-confirm]]', (ok) => { + if (ok) { + api.del(`/topics/${tid}/events/${eventId}`, {}) + .then(function () { + eventEl.remove(); + }) + .catch(alerts.error); + } + }); + }); + + // todo: should also use topicCommand, but no write api call exists for this yet + topicContainer.on('click', '[component="topic/mark-unread"]', function () { + socket.emit('topics.markUnread', tid, function (err) { + if (err) { + return alerts.error(err); + } + + if (app.previousUrl && !app.previousUrl.match('^/topic')) { + ajaxify.go(app.previousUrl, function () { + handleBack.onBackClicked(true); + }); + } else if (ajaxify.data.category) { + ajaxify.go('category/' + ajaxify.data.category.slug, handleBack.onBackClicked); + } + + alerts.success('[[topic:mark_unread.success]]'); + }); + return false; + }); + + topicContainer.on('click', '[component="topic/mark-unread-for-all"]', function () { + const btn = $(this); + socket.emit('topics.markAsUnreadForAll', [tid], function (err) { + if (err) { + return alerts.error(err); + } + alerts.success('[[topic:markAsUnreadForAll.success]]'); + btn.parents('.thread-tools.open').find('.dropdown-toggle').trigger('click'); + }); + return false; + }); + + topicContainer.on('click', '[component="topic/move"]', function () { + require(['forum/topic/move'], function (move) { + move.init([tid], ajaxify.data.cid); + }); + return false; + }); + + topicContainer.on('click', '[component="topic/delete/posts"]', function () { + require(['forum/topic/delete-posts'], function (deletePosts) { + deletePosts.init(); + }); + }); + + topicContainer.on('click', '[component="topic/fork"]', function () { + require(['forum/topic/fork'], function (fork) { + fork.init(); + }); + }); + + topicContainer.on('click', '[component="topic/move-posts"]', function () { + require(['forum/topic/move-post'], function (movePosts) { + movePosts.init(); + }); + }); + + topicContainer.on('click', '[component="topic/following"]', function () { + changeWatching('follow'); + }); + topicContainer.on('click', '[component="topic/not-following"]', function () { + changeWatching('follow', 0); + }); + topicContainer.on('click', '[component="topic/ignoring"]', function () { + changeWatching('ignore'); + }); + + function changeWatching(type, state = 1) { + const method = state ? 'put' : 'del'; + api[method](`/topics/${tid}/${type}`, {}, () => { + let message = ''; + if (type === 'follow') { + message = state ? '[[topic:following_topic.message]]' : '[[topic:not_following_topic.message]]'; + } else if (type === 'ignore') { + message = state ? '[[topic:ignoring_topic.message]]' : '[[topic:not_following_topic.message]]'; + } + + // From here on out, type changes to 'unfollow' if state is falsy + if (!state) { + type = 'unfollow'; + } + + setFollowState(type); + + alerts.alert({ + alert_id: 'follow_thread', + message: message, + type: 'success', + timeout: 5000, + }); + + hooks.fire('action:topics.changeWatching', { tid: tid, type: type }); + }, () => { + alerts.alert({ + type: 'danger', + alert_id: 'topic_follow', + title: '[[global:please_log_in]]', + message: '[[topic:login_to_subscribe]]', + timeout: 5000, + }); + }); + + return false; + } + }; + + function renderMenu(container) { + container.on('show.bs.dropdown', '.thread-tools', function () { + const $this = $(this); + const dropdownMenu = $this.find('.dropdown-menu'); + if (dropdownMenu.html()) { + return; + } + + dropdownMenu.toggleClass('hidden', true); + socket.emit('topics.loadTopicTools', { tid: ajaxify.data.tid, cid: ajaxify.data.cid }, function (err, data) { + if (err) { + return alerts.error(err); + } + app.parseAndTranslate('partials/topic/topic-menu-list', data, function (html) { + dropdownMenu.html(html); + dropdownMenu.toggleClass('hidden', false); + + hooks.fire('action:topic.tools.load', { + element: dropdownMenu, + }); + }); + }); + }); + } + + function topicCommand(method, path, command, onComplete) { + if (!onComplete) { + onComplete = function () {}; + } + const tid = ajaxify.data.tid; + const body = {}; + const execute = function (ok) { + if (ok) { + api[method](`/topics/${tid}${path}`, body) + .then(onComplete) + .catch(alerts.error); + } + }; + + switch (command) { + case 'delete': + case 'restore': + case 'purge': + bootbox.confirm(`[[topic:thread_tools.${command}_confirm]]`, execute); + break; + + case 'pin': + ThreadTools.requestPinExpiry(body, execute.bind(null, true)); + break; + + default: + execute(true); + break; + } + } + + ThreadTools.requestPinExpiry = function (body, onSuccess) { + app.parseAndTranslate('modals/set-pin-expiry', {}, function (html) { + const modal = bootbox.dialog({ + title: '[[topic:thread_tools.pin]]', + message: html, + onEscape: true, + size: 'small', + buttons: { + cancel: { + label: '[[modules:bootbox.cancel]]', + className: 'btn-link', + }, + save: { + label: '[[global:save]]', + className: 'btn-primary', + callback: function () { + const expiryEl = modal.get(0).querySelector('#expiry'); + let expiry = expiryEl.value; + + // No expiry set + if (expiry === '') { + return onSuccess(); + } + + // Expiration date set + expiry = new Date(expiry); + + if (expiry && expiry.getTime() > Date.now()) { + body.expiry = expiry.getTime(); + onSuccess(); + } else { + alerts.error('[[error:invalid-date]]'); + } + }, + }, + }, + }); + }); + }; + + ThreadTools.setLockedState = function (data) { + const threadEl = components.get('topic'); + if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { + return; + } + + const isLocked = data.isLocked && !ajaxify.data.privileges.isAdminOrMod; + + components.get('topic/lock').toggleClass('hidden', data.isLocked).parent().attr('hidden', data.isLocked ? '' : null); + components.get('topic/unlock').toggleClass('hidden', !data.isLocked).parent().attr('hidden', !data.isLocked ? '' : null); + + const hideReply = !!((data.isLocked || ajaxify.data.deleted) && !ajaxify.data.privileges.isAdminOrMod); + + components.get('topic/reply/container').toggleClass('hidden', hideReply); + components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !data.isLocked || ajaxify.data.deleted); + + threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply); + threadEl.find('[component="post/edit"], [component="post/delete"]').toggleClass('hidden', isLocked); + + threadEl.find('[component="post"][data-uid="' + app.user.uid + '"].deleted [component="post/tools"]').toggleClass('hidden', isLocked); + + $('[component="topic/labels"] [component="topic/locked"]').toggleClass('hidden', !data.isLocked); + $('[component="post/tools"] .dropdown-menu').html(''); + ajaxify.data.locked = data.isLocked; + + posts.addTopicEvents(data.events); + }; + + ThreadTools.setDeleteState = function (data) { + const threadEl = components.get('topic'); + if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { + return; + } + + components.get('topic/delete').toggleClass('hidden', data.isDelete).parent().attr('hidden', data.isDelete ? '' : null); + components.get('topic/restore').toggleClass('hidden', !data.isDelete).parent().attr('hidden', !data.isDelete ? '' : null); + components.get('topic/purge').toggleClass('hidden', !data.isDelete).parent().attr('hidden', !data.isDelete ? '' : null); + components.get('topic/deleted/message').toggleClass('hidden', !data.isDelete); + + if (data.isDelete) { + app.parseAndTranslate('partials/topic/deleted-message', { + deleter: data.user, + deleted: true, + deletedTimestampISO: utils.toISOString(Date.now()), + }, function (html) { + components.get('topic/deleted/message').replaceWith(html); + html.find('.timeago').timeago(); + }); + } + const hideReply = data.isDelete && !ajaxify.data.privileges.isAdminOrMod; + + components.get('topic/reply/container').toggleClass('hidden', hideReply); + components.get('topic/reply/locked').toggleClass('hidden', ajaxify.data.privileges.isAdminOrMod || !ajaxify.data.locked || data.isDelete); + threadEl.find('[component="post"]:not(.deleted) [component="post/reply"], [component="post"]:not(.deleted) [component="post/quote"]').toggleClass('hidden', hideReply); + + threadEl.toggleClass('deleted', data.isDelete); + ajaxify.data.deleted = data.isDelete ? 1 : 0; + + posts.addTopicEvents(data.events); + }; + + + ThreadTools.setPinnedState = function (data) { + const threadEl = components.get('topic'); + if (parseInt(data.tid, 10) !== parseInt(threadEl.attr('data-tid'), 10)) { + return; + } + + components.get('topic/pin').toggleClass('hidden', data.pinned).parent().attr('hidden', data.pinned ? '' : null); + components.get('topic/unpin').toggleClass('hidden', !data.pinned).parent().attr('hidden', !data.pinned ? '' : null); + const icon = $('[component="topic/labels"] [component="topic/pinned"]'); + icon.toggleClass('hidden', !data.pinned); + if (data.pinned) { + icon.translateAttr('title', ( + data.pinExpiry && data.pinExpiryISO ? + '[[topic:pinned-with-expiry, ' + data.pinExpiryISO + ']]' : + '[[topic:pinned]]' + )); + } + ajaxify.data.pinned = data.pinned; + + posts.addTopicEvents(data.events); + }; + + function setFollowState(state) { + const titles = { + follow: '[[topic:watching]]', + unfollow: '[[topic:not-watching]]', + ignore: '[[topic:ignoring]]', + }; + translator.translate(titles[state], function (translatedTitle) { + $('[component="topic/watch"] button') + .attr('title', translatedTitle) + .tooltip('fixTitle'); + }); + + let menu = components.get('topic/following/menu'); + menu.toggleClass('hidden', state !== 'follow'); + components.get('topic/following/check').toggleClass('fa-check', state === 'follow'); + + menu = components.get('topic/not-following/menu'); + menu.toggleClass('hidden', state !== 'unfollow'); + components.get('topic/not-following/check').toggleClass('fa-check', state === 'unfollow'); + + menu = components.get('topic/ignoring/menu'); + menu.toggleClass('hidden', state !== 'ignore'); + components.get('topic/ignoring/check').toggleClass('fa-check', state === 'ignore'); + } + + + return ThreadTools; +}); diff --git a/public/src/client/topic/votes.js b/public/src/client/topic/votes.js new file mode 100644 index 0000000000..05632f2c04 --- /dev/null +++ b/public/src/client/topic/votes.js @@ -0,0 +1,110 @@ +'use strict'; + + +define('forum/topic/votes', [ + 'components', 'translator', 'api', 'hooks', 'bootbox', 'alerts', +], function (components, translator, api, hooks, bootbox, alerts) { + const Votes = {}; + + Votes.addVoteHandler = function () { + components.get('topic').on('mouseenter', '[data-pid] [component="post/vote-count"]', loadDataAndCreateTooltip); + }; + + function loadDataAndCreateTooltip(e) { + e.stopPropagation(); + + const $this = $(this); + const el = $this.parent(); + el.find('.tooltip').css('display', 'none'); + const pid = el.parents('[data-pid]').attr('data-pid'); + + socket.emit('posts.getUpvoters', [pid], function (err, data) { + if (err) { + return alerts.error(err); + } + + if (data.length) { + createTooltip($this, data[0]); + } + }); + return false; + } + + function createTooltip(el, data) { + function doCreateTooltip(title) { + el.attr('title', title).tooltip('fixTitle').tooltip('show'); + el.parent().find('.tooltip').css('display', ''); + } + let usernames = data.usernames + .filter(name => name !== '[[global:former_user]]'); + if (!usernames.length) { + return; + } + if (usernames.length + data.otherCount > 6) { + usernames = usernames.join(', ').replace(/,/g, '|'); + translator.translate('[[topic:users_and_others, ' + usernames + ', ' + data.otherCount + ']]', function (translated) { + translated = translated.replace(/\|/g, ','); + doCreateTooltip(translated); + }); + } else { + usernames = usernames.join(', '); + doCreateTooltip(usernames); + } + } + + + Votes.toggleVote = function (button, className, delta) { + const post = button.closest('[data-pid]'); + const currentState = post.find(className).length; + + const method = currentState ? 'del' : 'put'; + const pid = post.attr('data-pid'); + api[method](`/posts/${pid}/vote`, { + delta: delta, + }, function (err) { + if (err) { + if (!app.user.uid) { + ajaxify.go('login'); + return; + } + return alerts.error(err); + } + hooks.fire('action:post.toggleVote', { + pid: pid, + delta: delta, + unvote: method === 'del', + }); + }); + + return false; + }; + + Votes.showVotes = function (pid) { + socket.emit('posts.getVoters', { pid: pid, cid: ajaxify.data.cid }, function (err, data) { + if (err) { + if (err.message === '[[error:no-privileges]]') { + return; + } + + // Only show error if it's an unexpected error. + return alerts.error(err); + } + + app.parseAndTranslate('partials/modals/votes_modal', data, function (html) { + const dialog = bootbox.dialog({ + title: '[[global:voters]]', + message: html, + className: 'vote-modal', + show: true, + }); + + dialog.on('click', function () { + dialog.modal('hide'); + }); + }); + }); + }; + + + return Votes; +}); diff --git a/public/src/client/unread.js b/public/src/client/unread.js new file mode 100644 index 0000000000..8d7b5298c7 --- /dev/null +++ b/public/src/client/unread.js @@ -0,0 +1,112 @@ +'use strict'; + + +define('forum/unread', [ + 'forum/header/unread', 'topicSelect', 'components', 'topicList', 'categorySelector', 'alerts', +], function (headerUnread, topicSelect, components, topicList, categorySelector, alerts) { + const Unread = {}; + + Unread.init = function () { + app.enterRoom('unread_topics'); + + handleMarkRead(); + + topicList.init('unread'); + + headerUnread.updateUnreadTopicCount('/' + ajaxify.data.selectedFilter.url, ajaxify.data.topicCount); + }; + + function handleMarkRead() { + function markAllRead() { + socket.emit('topics.markAllRead', function (err) { + if (err) { + return alerts.error(err); + } + + alerts.success('[[unread:topics_marked_as_read.success]]'); + + $('[component="category"]').empty(); + $('[component="pagination"]').addClass('hidden'); + $('#category-no-topics').removeClass('hidden'); + $('.markread').addClass('hidden'); + }); + } + + function markSelectedRead() { + const tids = topicSelect.getSelectedTids(); + if (!tids.length) { + return; + } + socket.emit('topics.markAsRead', tids, function (err) { + if (err) { + return alerts.error(err); + } + + doneRemovingTids(tids); + }); + } + + function markCategoryRead(cid) { + function getCategoryTids(cid) { + const tids = []; + components.get('category/topic', 'cid', cid).each(function () { + tids.push($(this).attr('data-tid')); + }); + return tids; + } + const tids = getCategoryTids(cid); + + socket.emit('topics.markCategoryTopicsRead', cid, function (err) { + if (err) { + return alerts.error(err); + } + + doneRemovingTids(tids); + }); + } + const selector = categorySelector.init($('[component="category-selector"]'), { + onSelect: function (category) { + selector.selectCategory(0); + if (category.cid === 'all') { + markAllRead(); + } else if (category.cid === 'selected') { + markSelectedRead(); + } else if (parseInt(category.cid, 10) > 0) { + markCategoryRead(category.cid); + } + }, + selectCategoryLabel: ajaxify.data.selectCategoryLabel || '[[unread:mark_as_read]]', + localCategories: [ + { + cid: 'selected', + name: '[[unread:selected]]', + icon: '', + }, + { + cid: 'all', + name: '[[unread:all]]', + icon: '', + }, + ], + }); + } + + function doneRemovingTids(tids) { + removeTids(tids); + + alerts.success('[[unread:topics_marked_as_read.success]]'); + + if (!$('[component="category"]').children().length) { + $('#category-no-topics').removeClass('hidden'); + $('.markread').addClass('hidden'); + } + } + + function removeTids(tids) { + for (let i = 0; i < tids.length; i += 1) { + components.get('category/topic', 'tid', tids[i]).remove(); + } + } + + return Unread; +}); diff --git a/public/src/client/users.js b/public/src/client/users.js new file mode 100644 index 0000000000..3e36b1fdac --- /dev/null +++ b/public/src/client/users.js @@ -0,0 +1,122 @@ +'use strict'; + + +define('forum/users', [ + 'translator', 'benchpress', 'api', 'alerts', 'accounts/invite', +], function (translator, Benchpress, api, alerts, AccountInvite) { + const Users = {}; + + let searchResultCount = 0; + + Users.init = function () { + app.enterRoom('user_list'); + + const section = utils.param('section') ? ('?section=' + utils.param('section')) : ''; + $('.nav-pills li').removeClass('active').find('a[href="' + window.location.pathname + section + '"]').parent() + .addClass('active'); + + Users.handleSearch(); + + AccountInvite.handle(); + + socket.removeListener('event:user_status_change', onUserStatusChange); + socket.on('event:user_status_change', onUserStatusChange); + }; + + Users.handleSearch = function (params) { + searchResultCount = params && params.resultCount; + $('#search-user').on('keyup', utils.debounce(doSearch, 250)); + $('.search select, .search input[type="checkbox"]').on('change', doSearch); + }; + + function doSearch() { + if (!ajaxify.data.template.users) { + return; + } + $('[component="user/search/icon"]').removeClass('fa-search').addClass('fa-spinner fa-spin'); + const username = $('#search-user').val(); + const activeSection = getActiveSection(); + + const query = { + section: activeSection, + page: 1, + }; + + if (!username) { + return loadPage(query); + } + + query.query = username; + query.sortBy = getSortBy(); + const filters = []; + if ($('.search .online-only').is(':checked') || (activeSection === 'online')) { + filters.push('online'); + } + if (activeSection === 'banned') { + filters.push('banned'); + } + if (activeSection === 'flagged') { + filters.push('flagged'); + } + if (filters.length) { + query.filters = filters; + } + + loadPage(query); + } + + function getSortBy() { + let sortBy; + const activeSection = getActiveSection(); + if (activeSection === 'sort-posts') { + sortBy = 'postcount'; + } else if (activeSection === 'sort-reputation') { + sortBy = 'reputation'; + } else if (activeSection === 'users') { + sortBy = 'joindate'; + } + return sortBy; + } + + + function loadPage(query) { + api.get('/api/users', query) + .then(renderSearchResults) + .catch(alerts.error); + } + + function renderSearchResults(data) { + Benchpress.render('partials/paginator', { pagination: data.pagination }).then(function (html) { + $('.pagination-container').replaceWith(html); + }); + + if (searchResultCount) { + data.users = data.users.slice(0, searchResultCount); + } + + data.isAdminOrGlobalMod = app.user.isAdmin || app.user.isGlobalMod; + app.parseAndTranslate('users', 'users', data, function (html) { + $('#users-container').html(html); + html.find('span.timeago').timeago(); + $('[component="user/search/icon"]').addClass('fa-search').removeClass('fa-spinner fa-spin'); + }); + } + + function onUserStatusChange(data) { + const section = getActiveSection(); + + if ((section.startsWith('online') || section.startsWith('users'))) { + updateUser(data); + } + } + + function updateUser(data) { + app.updateUserStatus($('#users-container [data-uid="' + data.uid + '"] [component="user/status"]'), data.status); + } + + function getActiveSection() { + return utils.param('section') || ''; + } + + return Users; +}); diff --git a/public/src/installer/install.js b/public/src/installer/install.js new file mode 100644 index 0000000000..bf6b5609e8 --- /dev/null +++ b/public/src/installer/install.js @@ -0,0 +1,144 @@ +/* eslint-disable no-redeclare */ + +'use strict'; + +const $ = require('jquery'); +const zxcvbn = require('zxcvbn'); +const utils = require('../utils'); +const slugify = require('../modules/slugify'); + +$('document').ready(function () { + setupInputs(); + $('[name="username"]').focus(); + + activate('database', $('[name="database"]')); + + if ($('#database-error').length) { + $('[name="database"]').parents('.input-row').addClass('error'); + $('html, body').animate({ + scrollTop: ($('#database-error').offset().top + 100) + 'px', + }, 400); + } + + $('#launch').on('click', launchForum); + + if ($('#installing').length) { + setTimeout(function () { + window.location.reload(true); + }, 5000); + } + + function setupInputs() { + $('form').on('focus', '.form-control', function () { + const parent = $(this).parents('.input-row'); + + $('.input-row.active').removeClass('active'); + parent.addClass('active').removeClass('error'); + + const help = parent.find('.help-text'); + help.html(help.attr('data-help')); + }); + + $('form').on('blur change', '[name]', function () { + activate($(this).attr('name'), $(this)); + }); + + $('form').submit(validateAll); + } + + function validateAll(ev) { + $('form .admin [name]').each(function () { + activate($(this).attr('name'), $(this)); + }); + + if ($('form .admin .error').length) { + ev.preventDefault(); + $('html, body').animate({ scrollTop: '0px' }, 400); + + return false; + } + $('#submit .working').removeClass('hide'); + } + + function activate(type, el) { + const field = el.val(); + const parent = el.parents('.input-row'); + const help = parent.children('.help-text'); + + function validateUsername(field) { + if (!utils.isUserNameValid(field) || !slugify(field)) { + parent.addClass('error'); + help.html('Invalid Username.'); + } else { + parent.removeClass('error'); + } + } + + function validatePassword(field) { + if (!utils.isPasswordValid(field)) { + parent.addClass('error'); + help.html('Invalid Password.'); + } else if (field.length < $('[name="admin:password"]').attr('data-minimum-length')) { + parent.addClass('error'); + help.html('Password is too short.'); + } else if (zxcvbn(field).score < parseInt($('[name="admin:password"]').attr('data-minimum-strength'), 10)) { + parent.addClass('error'); + help.html('Password is too weak.'); + } else { + parent.removeClass('error'); + } + } + + function validateConfirmPassword() { + if ($('[name="admin:password"]').val() !== $('[name="admin:passwordConfirm"]').val()) { + parent.addClass('error'); + help.html('Passwords do not match.'); + } else { + parent.removeClass('error'); + } + } + + function validateEmail(field) { + if (!utils.isEmailValid(field)) { + parent.addClass('error'); + help.html('Invalid Email Address.'); + } else { + parent.removeClass('error'); + } + } + + function switchDatabase(field) { + $('#database-config').html($('[data-database="' + field + '"]').html()); + } + + switch (type) { + case 'admin:username': + return validateUsername(field); + case 'admin:password': + return validatePassword(field); + case 'admin:passwordConfirm': + return validateConfirmPassword(field); + case 'admin:email': + return validateEmail(field); + case 'database': + return switchDatabase(field); + } + } + + function launchForum() { + $('#launch .working').removeClass('hide'); + $.post('/launch', function () { + let successCount = 0; + const url = $('#launch').attr('data-url'); + setInterval(function () { + $.get(url + '/admin').done(function () { + if (successCount >= 5) { + window.location = 'admin'; + } else { + successCount += 1; + } + }); + }, 750); + }); + } +}); diff --git a/public/src/modules/accounts/delete.js b/public/src/modules/accounts/delete.js new file mode 100644 index 0000000000..e8f16deba4 --- /dev/null +++ b/public/src/modules/accounts/delete.js @@ -0,0 +1,53 @@ +'use strict'; + +define('accounts/delete', ['api', 'bootbox', 'alerts'], function (api, bootbox, alerts) { + const Delete = {}; + + Delete.account = function (uid, callback) { + executeAction( + uid, + '[[user:delete_this_account_confirm]]', + '/account', + '[[user:account-deleted]]', + callback + ); + }; + + Delete.content = function (uid, callback) { + executeAction( + uid, + '[[user:delete_account_content_confirm]]', + '/content', + '[[user:account-content-deleted]]', + callback + ); + }; + + Delete.purge = function (uid, callback) { + executeAction( + uid, + '[[user:delete_all_confirm]]', + '', + '[[user:account-deleted]]', + callback + ); + }; + + function executeAction(uid, confirmText, path, successText, callback) { + bootbox.confirm(confirmText, function (confirm) { + if (!confirm) { + return; + } + + api.del(`/users/${uid}${path}`, {}).then(() => { + alerts.success(successText); + + if (typeof callback === 'function') { + return callback(); + } + }).catch(alerts.error); + }); + } + + return Delete; +}); diff --git a/public/src/modules/accounts/invite.js b/public/src/modules/accounts/invite.js new file mode 100644 index 0000000000..95f6177659 --- /dev/null +++ b/public/src/modules/accounts/invite.js @@ -0,0 +1,60 @@ +'use strict'; + +define('accounts/invite', ['api', 'benchpress', 'bootbox', 'alerts'], function (api, Benchpress, bootbox, alerts) { + const Invite = {}; + + function isACP() { + return ajaxify.data.template.name.startsWith('admin/'); + } + + Invite.handle = function () { + $('[component="user/invite"]').on('click', function (e) { + e.preventDefault(); + api.get(`/api/v3/users/${app.user.uid}/invites/groups`, {}).then((groups) => { + Benchpress.parse('modals/invite', { groups: groups }, function (html) { + bootbox.dialog({ + message: html, + title: `[[${isACP() ? 'admin/manage/users:invite' : 'users:invite'}]]`, + onEscape: true, + buttons: { + cancel: { + label: `[[${isACP() ? 'admin/manage/users:alerts.button-cancel' : 'modules:bootbox.cancel'}]]`, + className: 'btn-default', + }, + invite: { + label: `[[${isACP() ? 'admin/manage/users:invite' : 'users:invite'}]]`, + className: 'btn-primary', + callback: Invite.send, + }, + }, + }); + }); + }).catch(alerts.error); + }); + }; + + Invite.send = function () { + const $emails = $('#invite-modal-emails'); + const $groups = $('#invite-modal-groups'); + + const data = { + emails: $emails.val() + .split(',') + .map(m => m.trim()) + .filter(Boolean) + .filter((m, i, arr) => i === arr.indexOf(m)) + .join(','), + groupsToJoin: $groups.val(), + }; + + if (!data.emails) { + return; + } + + api.post(`/users/${app.user.uid}/invites`, data).then(() => { + alerts.success(`[[${isACP() ? 'admin/manage/users:alerts.email-sent-to' : 'users:invitation-email-sent'}, ${data.emails.replace(/,/g, ', ')}]]`); + }).catch(alerts.error); + }; + + return Invite; +}); diff --git a/public/src/modules/accounts/picture.js b/public/src/modules/accounts/picture.js new file mode 100644 index 0000000000..0d905c1882 --- /dev/null +++ b/public/src/modules/accounts/picture.js @@ -0,0 +1,219 @@ +'use strict'; + +define('accounts/picture', [ + 'pictureCropper', + 'api', + 'bootbox', + 'alerts', +], (pictureCropper, api, bootbox, alerts) => { + const Picture = {}; + + Picture.openChangeModal = () => { + socket.emit('user.getProfilePictures', { + uid: ajaxify.data.uid, + }, function (err, pictures) { + if (err) { + return alerts.error(err); + } + + // boolean to signify whether an uploaded picture is present in the pictures list + const uploaded = pictures.reduce(function (memo, cur) { + return memo || cur.type === 'uploaded'; + }, false); + + app.parseAndTranslate('partials/modals/change_picture_modal', { + pictures: pictures, + uploaded: uploaded, + icon: { text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor'] }, + defaultAvatar: ajaxify.data.defaultAvatar, + allowProfileImageUploads: ajaxify.data.allowProfileImageUploads, + iconBackgrounds: config.iconBackgrounds, + user: { + uid: ajaxify.data.uid, + username: ajaxify.data.username, + picture: ajaxify.data.picture, + 'icon:text': ajaxify.data['icon:text'], + 'icon:bgColor': ajaxify.data['icon:bgColor'], + }, + }, function (html) { + const modal = bootbox.dialog({ + className: 'picture-switcher', + title: '[[user:change_picture]]', + message: html, + show: true, + buttons: { + close: { + label: '[[global:close]]', + callback: onCloseModal, + className: 'btn-link', + }, + update: { + label: '[[global:save_changes]]', + callback: saveSelection, + }, + }, + }); + + modal.on('shown.bs.modal', updateImages); + modal.on('click', '.list-group-item', function selectImageType() { + modal.find('.list-group-item').removeClass('active'); + $(this).addClass('active'); + }); + modal.on('change', 'input[type="radio"][name="icon:bgColor"]', (e) => { + const value = e.target.value; + modal.find('.user-icon').css('background-color', value); + }); + + handleImageUpload(modal); + + function updateImages() { + // Check to see which one is the active picture + if (!ajaxify.data.picture) { + modal.find('.list-group-item .user-icon').parents('.list-group-item').addClass('active'); + } else { + modal.find('.list-group-item img').each(function () { + if (this.getAttribute('src') === ajaxify.data.picture) { + $(this).parents('.list-group-item').addClass('active'); + } + }); + } + + // Update avatar background colour + const radioEl = document.querySelector(`.modal input[type="radio"][value="${ajaxify.data['icon:bgColor']}"]`); + if (radioEl) { + radioEl.checked = true; + } else { + // Check the first one + document.querySelector('.modal input[type="radio"]').checked = true; + } + } + + function saveSelection() { + const type = modal.find('.list-group-item.active').attr('data-type'); + const iconBgColor = document.querySelector('.modal.picture-switcher input[type="radio"]:checked').value || 'transparent'; + + changeUserPicture(type, iconBgColor).then(() => { + Picture.updateHeader(type === 'default' ? '' : modal.find('.list-group-item.active img').attr('src'), iconBgColor); + ajaxify.refresh(); + }).catch(alerts.error); + } + + function onCloseModal() { + modal.modal('hide'); + } + }); + }); + }; + + Picture.updateHeader = (picture, iconBgColor) => { + if (parseInt(ajaxify.data.theirid, 10) !== parseInt(ajaxify.data.yourid, 10)) { + return; + } + if (!picture && ajaxify.data.defaultAvatar) { + picture = ajaxify.data.defaultAvatar; + } + $('#header [component="avatar/picture"]')[picture ? 'show' : 'hide'](); + $('#header [component="avatar/icon"]')[!picture ? 'show' : 'hide'](); + if (picture) { + $('#header [component="avatar/picture"]').attr('src', picture); + } + + if (iconBgColor) { + document.querySelectorAll('[component="navbar"] [component="avatar/icon"]').forEach((el) => { + el.style['background-color'] = iconBgColor; + }); + } + }; + + function handleImageUpload(modal) { + function onUploadComplete(urlOnServer) { + urlOnServer = (!urlOnServer.startsWith('http') ? config.relative_path : '') + urlOnServer + '?' + Date.now(); + + Picture.updateHeader(urlOnServer); + + if (ajaxify.data.picture && ajaxify.data.picture.length) { + $('#user-current-picture, img.avatar').attr('src', urlOnServer); + ajaxify.data.uploadedpicture = urlOnServer; + } else { + ajaxify.refresh(function () { + $('#user-current-picture, img.avatar').attr('src', urlOnServer); + }); + } + } + + function onRemoveComplete() { + if (ajaxify.data.uploadedpicture === ajaxify.data.picture) { + ajaxify.refresh(); + Picture.updateHeader(); + } + } + + modal.find('[data-action="upload"]').on('click', function () { + modal.modal('hide'); + + pictureCropper.show({ + socketMethod: 'user.uploadCroppedPicture', + route: config.relative_path + '/api/user/' + ajaxify.data.userslug + '/uploadpicture', + aspectRatio: 1 / 1, + paramName: 'uid', + paramValue: ajaxify.data.theirid, + fileSize: ajaxify.data.maximumProfileImageSize, + allowSkippingCrop: false, + title: '[[user:upload_picture]]', + description: '[[user:upload_a_picture]]', + accept: ajaxify.data.allowedProfileImageExtensions, + }, function (url) { + onUploadComplete(url); + }); + + return false; + }); + + modal.find('[data-action="upload-url"]').on('click', function () { + modal.modal('hide'); + app.parseAndTranslate('partials/modals/upload_picture_from_url_modal', {}, function (uploadModal) { + uploadModal.modal('show'); + + uploadModal.find('.upload-btn').on('click', function () { + const url = uploadModal.find('#uploadFromUrl').val(); + if (!url) { + return false; + } + + uploadModal.modal('hide'); + + pictureCropper.handleImageCrop({ + url: url, + socketMethod: 'user.uploadCroppedPicture', + aspectRatio: 1, + allowSkippingCrop: false, + paramName: 'uid', + paramValue: ajaxify.data.theirid, + }, onUploadComplete); + + return false; + }); + }); + + return false; + }); + + modal.find('[data-action="remove-uploaded"]').on('click', function () { + socket.emit('user.removeUploadedPicture', { + uid: ajaxify.data.theirid, + }, function (err) { + modal.modal('hide'); + if (err) { + return alerts.error(err); + } + onRemoveComplete(); + }); + }); + } + + function changeUserPicture(type, bgColor) { + return api.put(`/users/${ajaxify.data.theirid}/picture`, { type, bgColor }); + } + + return Picture; +}); diff --git a/public/src/modules/ace-editor.js b/public/src/modules/ace-editor.js new file mode 100644 index 0000000000..7c88771a9f --- /dev/null +++ b/public/src/modules/ace-editor.js @@ -0,0 +1,20 @@ +export * from 'ace-builds'; + +// only import the modes and theme we use +import 'ace-builds/src-noconflict/mode-javascript'; +import 'ace-builds/src-noconflict/mode-less'; +import 'ace-builds/src-noconflict/mode-html'; +import 'ace-builds/src-noconflict/ext-searchbox'; +import 'ace-builds/src-noconflict/theme-twilight'; + +/* eslint-disable import/no-webpack-loader-syntax */ +/* eslint-disable import/no-unresolved */ +import htmlWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-html'; +import javascriptWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-javascript'; +import cssWorkerUrl from 'file-loader!ace-builds/src-noconflict/worker-css'; + +ace.config.setModuleUrl('ace/mode/html_worker', htmlWorkerUrl); +ace.config.setModuleUrl('ace/mode/javascript_worker', javascriptWorkerUrl); +ace.config.setModuleUrl('ace/mode/css_worker', cssWorkerUrl); + + diff --git a/public/src/modules/alerts.js b/public/src/modules/alerts.js new file mode 100644 index 0000000000..682101fbdd --- /dev/null +++ b/public/src/modules/alerts.js @@ -0,0 +1,155 @@ +'use strict'; + + +define('alerts', ['translator', 'components', 'hooks'], function (translator, components, hooks) { + const module = {}; + + module.alert = function (params) { + params.alert_id = 'alert_button_' + (params.alert_id ? params.alert_id : new Date().getTime()); + params.title = params.title ? params.title.trim() || '' : ''; + params.message = params.message ? params.message.trim() : ''; + params.type = params.type || 'info'; + + const alert = $('#' + params.alert_id); + if (alert.length) { + updateAlert(alert, params); + } else { + createNew(params); + } + }; + + module.success = function (message, timeout) { + module.alert({ + alert_id: utils.generateUUID(), + title: '[[global:alert.success]]', + message: message, + type: 'success', + timeout: timeout || 5000, + }); + }; + + module.error = function (message, timeout) { + message = (message && message.message) || message; + + if (message === '[[error:revalidate-failure]]') { + socket.disconnect(); + app.reconnect(); + return; + } + + module.alert({ + alert_id: utils.generateUUID(), + title: '[[global:alert.error]]', + message: message, + type: 'danger', + timeout: timeout || 10000, + }); + }; + + module.remove = function (id) { + $('#alert_button_' + id).remove(); + }; + + function createNew(params) { + app.parseAndTranslate('alert', params, function (html) { + let alert = $('#' + params.alert_id); + if (alert.length) { + return updateAlert(alert, params); + } + alert = html; + alert.fadeIn(200); + + components.get('toaster/tray').prepend(alert); + + if (typeof params.closefn === 'function') { + alert.find('button').on('click', function () { + params.closefn(); + fadeOut(alert); + return false; + }); + } + + if (params.timeout) { + startTimeout(alert, params); + } + + if (typeof params.clickfn === 'function') { + alert + .addClass('pointer') + .on('click', function (e) { + if (!$(e.target).is('.close')) { + params.clickfn(alert, params); + } + fadeOut(alert); + }); + } + + hooks.fire('action:alert.new', { alert, params }); + }); + } + + function updateAlert(alert, params) { + alert.find('strong').translateHtml(params.title); + alert.find('p').translateHtml(params.message); + alert.attr('class', 'alert alert-dismissable alert-' + params.type + ' clearfix'); + + clearTimeout(parseInt(alert.attr('timeoutId'), 10)); + if (params.timeout) { + startTimeout(alert, params); + } + + hooks.fire('action:alert.update', { alert, params }); + + // Handle changes in the clickfn + alert.off('click').removeClass('pointer'); + if (typeof params.clickfn === 'function') { + alert + .addClass('pointer') + .on('click', function (e) { + if (!$(e.target).is('.close')) { + params.clickfn(); + } + fadeOut(alert); + }); + } + } + + function fadeOut(alert) { + alert.fadeOut(500, function () { + $(this).remove(); + }); + } + + function startTimeout(alert, params) { + const timeout = params.timeout; + + const timeoutId = setTimeout(function () { + fadeOut(alert); + + if (typeof params.timeoutfn === 'function') { + params.timeoutfn(alert, params); + } + }, timeout); + + alert.attr('timeoutId', timeoutId); + + // Reset and start animation + alert.css('transition-property', 'none'); + alert.removeClass('animate'); + + setTimeout(function () { + alert.css('transition-property', ''); + alert.css('transition', 'width ' + (timeout + 450) + 'ms linear, background-color ' + (timeout + 450) + 'ms ease-in'); + alert.addClass('animate'); + hooks.fire('action:alert.animate', { alert, params }); + }, 50); + + // Handle mouseenter/mouseleave + alert + .on('mouseenter', function () { + $(this).css('transition-duration', 0); + }); + } + + return module; +}); diff --git a/public/src/modules/api.js b/public/src/modules/api.js new file mode 100644 index 0000000000..32b1e8e8e4 --- /dev/null +++ b/public/src/modules/api.js @@ -0,0 +1,100 @@ +'use strict'; + +define('api', ['hooks'], (hooks) => { + const api = {}; + const baseUrl = config.relative_path + '/api/v3'; + + function call(options, callback) { + options.url = options.url.startsWith('/api') ? + config.relative_path + options.url : + baseUrl + options.url; + + async function doAjax(cb) { + // Allow options to be modified by plugins, etc. + ({ options } = await hooks.fire('filter:api.options', { options })); + + $.ajax(options) + .done((res) => { + cb(null, ( + res && + res.hasOwnProperty('status') && + res.hasOwnProperty('response') ? res.response : (res || {}) + )); + }) + .fail((ev) => { + let errMessage; + if (ev.responseJSON) { + errMessage = ev.responseJSON.status && ev.responseJSON.status.message ? + ev.responseJSON.status.message : + ev.responseJSON.error; + } + + cb(new Error(errMessage || ev.statusText)); + }); + } + + if (typeof callback === 'function') { + doAjax(callback); + return; + } + + return new Promise((resolve, reject) => { + doAjax(function (err, data) { + if (err) reject(err); + else resolve(data); + }); + }); + } + + api.get = (route, payload, onSuccess) => call({ + url: route + (payload && Object.keys(payload).length ? ('?' + $.param(payload)) : ''), + }, onSuccess); + + api.head = (route, payload, onSuccess) => call({ + url: route + (payload && Object.keys(payload).length ? ('?' + $.param(payload)) : ''), + method: 'head', + }, onSuccess); + + api.post = (route, payload, onSuccess) => call({ + url: route, + method: 'post', + data: JSON.stringify(payload || {}), + contentType: 'application/json; charset=utf-8', + headers: { + 'x-csrf-token': config.csrf_token, + }, + }, onSuccess); + + api.patch = (route, payload, onSuccess) => call({ + url: route, + method: 'patch', + data: JSON.stringify(payload || {}), + contentType: 'application/json; charset=utf-8', + headers: { + 'x-csrf-token': config.csrf_token, + }, + }, onSuccess); + + api.put = (route, payload, onSuccess) => call({ + url: route, + method: 'put', + data: JSON.stringify(payload || {}), + contentType: 'application/json; charset=utf-8', + headers: { + 'x-csrf-token': config.csrf_token, + }, + }, onSuccess); + + api.del = (route, payload, onSuccess) => call({ + url: route, + method: 'delete', + data: JSON.stringify(payload), + contentType: 'application/json; charset=utf-8', + headers: { + 'x-csrf-token': config.csrf_token, + }, + }, onSuccess); + api.delete = api.del; + + return api; +}); diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js new file mode 100644 index 0000000000..77aaeceb69 --- /dev/null +++ b/public/src/modules/autocomplete.js @@ -0,0 +1,133 @@ +'use strict'; + +define('autocomplete', ['api', 'alerts'], function (api, alerts) { + const module = {}; + const _default = { + delay: 200, + }; + + module.init = (params) => { + const { input, source, onSelect, delay } = { ..._default, ...params }; + + app.loadJQueryUI(function () { + input.autocomplete({ + delay, + open: function () { + $(this).autocomplete('widget').css('z-index', 100005); + }, + select: function (event, ui) { + handleOnSelect(input, onSelect, event, ui); + }, + source, + }); + }); + }; + + module.user = function (input, params, onSelect) { + if (typeof params === 'function') { + onSelect = params; + params = {}; + } + params = params || {}; + + module.init({ + input, + onSelect, + source: (request, response) => { + params.query = request.term; + + api.get('/api/users', params, function (err, result) { + if (err) { + return alerts.error(err); + } + + if (result && result.users) { + const names = result.users.map(function (user) { + const username = $('
    ').html(user.username).text(); + return user && { + label: username, + value: username, + user: { + uid: user.uid, + name: user.username, + slug: user.userslug, + username: user.username, + userslug: user.userslug, + picture: user.picture, + banned: user.banned, + 'icon:text': user['icon:text'], + 'icon:bgColor': user['icon:bgColor'], + }, + }; + }); + response(names); + } + + $('.ui-autocomplete a').attr('data-ajaxify', 'false'); + }); + }, + }); + }; + + module.group = function (input, onSelect) { + module.init({ + input, + onSelect, + source: (request, response) => { + socket.emit('groups.search', { + query: request.term, + }, function (err, results) { + if (err) { + return alerts.error(err); + } + if (results && results.length) { + const names = results.map(function (group) { + return group && { + label: group.name, + value: group.name, + group: group, + }; + }); + response(names); + } + $('.ui-autocomplete a').attr('data-ajaxify', 'false'); + }); + }, + }); + }; + + module.tag = function (input, onSelect) { + module.init({ + input, + onSelect, + delay: 100, + source: (request, response) => { + socket.emit('topics.autocompleteTags', { + query: request.term, + cid: ajaxify.data.cid || 0, + }, function (err, tags) { + if (err) { + return alerts.error(err); + } + if (tags) { + response(tags); + } + $('.ui-autocomplete a').attr('data-ajaxify', 'false'); + }); + }, + }); + }; + + function handleOnSelect(input, onselect, event, ui) { + onselect = onselect || function () { }; + const e = jQuery.Event('keypress'); + e.which = 13; + e.keyCode = 13; + setTimeout(function () { + input.trigger(e); + }, 100); + onselect(event, ui); + } + + return module; +}); diff --git a/public/src/modules/categoryFilter.js b/public/src/modules/categoryFilter.js new file mode 100644 index 0000000000..fef03a8b40 --- /dev/null +++ b/public/src/modules/categoryFilter.js @@ -0,0 +1,103 @@ +'use strict'; + +define('categoryFilter', ['categorySearch', 'api', 'hooks'], function (categorySearch, api, hooks) { + const categoryFilter = {}; + + categoryFilter.init = function (el, options) { + if (!el || !el.length) { + return; + } + options = options || {}; + options.states = options.states || ['watching', 'notwatching', 'ignoring']; + options.template = 'partials/category-filter'; + + hooks.fire('action:category.filter.options', { el: el, options: options }); + + categorySearch.init(el, options); + + let selectedCids = []; + let initialCids = []; + if (Array.isArray(options.selectedCids)) { + selectedCids = options.selectedCids.map(cid => parseInt(cid, 10)); + } else if (Array.isArray(ajaxify.data.selectedCids)) { + selectedCids = ajaxify.data.selectedCids.map(cid => parseInt(cid, 10)); + } + initialCids = selectedCids.slice(); + + el.on('hidden.bs.dropdown', function () { + let changed = initialCids.length !== selectedCids.length; + initialCids.forEach(function (cid, index) { + if (cid !== selectedCids[index]) { + changed = true; + } + }); + if (changed) { + updateFilterButton(el, selectedCids); + } + if (options.onHidden) { + options.onHidden({ changed: changed, selectedCids: selectedCids.slice() }); + return; + } + if (changed) { + let url = window.location.pathname; + const currentParams = utils.params(); + if (selectedCids.length) { + currentParams.cid = selectedCids; + url += '?' + decodeURIComponent($.param(currentParams)); + } + ajaxify.go(url); + } + }); + + el.on('click', '[component="category/list"] [data-cid]', function () { + const listEl = el.find('[component="category/list"]'); + const categoryEl = $(this); + const link = categoryEl.find('a').attr('href'); + if (link && link !== '#' && link.length) { + return; + } + const cid = parseInt(categoryEl.attr('data-cid'), 10); + const icon = categoryEl.find('[component="category/select/icon"]'); + + if (selectedCids.includes(cid)) { + selectedCids.splice(selectedCids.indexOf(cid), 1); + } else { + selectedCids.push(cid); + } + selectedCids.sort(function (a, b) { + return a - b; + }); + options.selectedCids = selectedCids; + + icon.toggleClass('invisible'); + listEl.find('li[data-all="all"] i').toggleClass('invisible', !!selectedCids.length); + if (options.onSelect) { + options.onSelect({ cid: cid, selectedCids: selectedCids.slice() }); + } + return false; + }); + }; + + function updateFilterButton(el, selectedCids) { + if (selectedCids.length > 1) { + renderButton({ + icon: 'fa-plus', + name: '[[unread:multiple-categories-selected]]', + bgColor: '#ddd', + }); + } else if (selectedCids.length === 1) { + api.get(`/categories/${selectedCids[0]}`, {}).then(renderButton); + } else { + renderButton(); + } + function renderButton(category) { + app.parseAndTranslate('partials/category-filter-content', { + selectedCategory: category, + }, function (html) { + el.find('button').replaceWith($('
    ').html(html).find('button')); + }); + } + } + + return categoryFilter; +}); diff --git a/public/src/modules/categorySearch.js b/public/src/modules/categorySearch.js new file mode 100644 index 0000000000..3db9432924 --- /dev/null +++ b/public/src/modules/categorySearch.js @@ -0,0 +1,101 @@ +'use strict'; + +define('categorySearch', ['alerts'], function (alerts) { + const categorySearch = {}; + + categorySearch.init = function (el, options) { + let categoriesList = null; + options = options || {}; + options.privilege = options.privilege || 'topics:read'; + options.states = options.states || ['watching', 'notwatching', 'ignoring']; + + let localCategories = []; + if (Array.isArray(options.localCategories)) { + localCategories = options.localCategories.map(c => ({ ...c })); + } + options.selectedCids = options.selectedCids || ajaxify.data.selectedCids || []; + + const searchEl = el.find('[component="category-selector-search"]'); + if (!searchEl.length) { + return; + } + + const toggleVisibility = searchEl.parent('[component="category/dropdown"]').length > 0 || + searchEl.parent('[component="category-selector"]').length > 0; + + el.on('show.bs.dropdown', function () { + if (toggleVisibility) { + el.find('.dropdown-toggle').addClass('hidden'); + searchEl.removeClass('hidden'); + } + + function doSearch() { + const val = searchEl.find('input').val(); + if (val.length > 1 || (!val && !categoriesList)) { + loadList(val, function (categories) { + categoriesList = categoriesList || categories; + renderList(categories); + }); + } else if (!val && categoriesList) { + categoriesList.forEach(function (c) { + c.selected = options.selectedCids.includes(c.cid); + }); + renderList(categoriesList); + } + } + + searchEl.on('click', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + }); + searchEl.find('input').val('').on('keyup', utils.debounce(doSearch, 300)); + doSearch(); + }); + + el.on('shown.bs.dropdown', function () { + searchEl.find('input').focus(); + }); + + el.on('hide.bs.dropdown', function () { + if (toggleVisibility) { + el.find('.dropdown-toggle').removeClass('hidden'); + searchEl.addClass('hidden'); + } + + searchEl.off('click'); + searchEl.find('input').off('keyup'); + }); + + function loadList(search, callback) { + socket.emit('categories.categorySearch', { + search: search, + query: utils.params(), + parentCid: options.parentCid || 0, + selectedCids: options.selectedCids, + privilege: options.privilege, + states: options.states, + showLinks: options.showLinks, + }, function (err, categories) { + if (err) { + return alerts.error(err); + } + callback(localCategories.concat(categories)); + }); + } + + function renderList(categories) { + app.parseAndTranslate(options.template, { + categoryItems: categories.slice(0, 200), + selectedCategory: ajaxify.data.selectedCategory, + allCategoriesUrl: ajaxify.data.allCategoriesUrl, + }, function (html) { + el.find('[component="category/list"]') + .replaceWith(html.find('[component="category/list"]')); + el.find('[component="category/list"] [component="category/no-matches"]') + .toggleClass('hidden', !!categories.length); + }); + } + }; + + return categorySearch; +}); diff --git a/public/src/modules/categorySelector.js b/public/src/modules/categorySelector.js new file mode 100644 index 0000000000..8ac329519f --- /dev/null +++ b/public/src/modules/categorySelector.js @@ -0,0 +1,96 @@ +'use strict'; + +define('categorySelector', [ + 'categorySearch', 'bootbox', 'hooks', +], function (categorySearch, bootbox, hooks) { + const categorySelector = {}; + + categorySelector.init = function (el, options) { + if (!el || !el.length) { + return; + } + options = options || {}; + const onSelect = options.onSelect || function () {}; + + options.states = options.states || ['watching', 'notwatching', 'ignoring']; + options.template = 'partials/category-selector'; + hooks.fire('action:category.selector.options', { el: el, options: options }); + + categorySearch.init(el, options); + + const selector = { + el: el, + selectedCategory: null, + }; + el.on('click', '[data-cid]', function () { + const categoryEl = $(this); + if (categoryEl.hasClass('disabled')) { + return false; + } + selector.selectCategory(categoryEl.attr('data-cid')); + onSelect(selector.selectedCategory); + }); + const defaultSelectHtml = selector.el.find('[component="category-selector-selected"]').html(); + selector.selectCategory = function (cid) { + const categoryEl = selector.el.find('[data-cid="' + cid + '"]'); + selector.selectedCategory = { + cid: cid, + name: categoryEl.attr('data-name'), + }; + + if (categoryEl.length) { + selector.el.find('[component="category-selector-selected"]').html( + categoryEl.find('[component="category-markup"]').html() + ); + } else { + selector.el.find('[component="category-selector-selected"]').html( + defaultSelectHtml + ); + } + }; + selector.getSelectedCategory = function () { + return selector.selectedCategory; + }; + selector.getSelectedCid = function () { + return selector.selectedCategory ? selector.selectedCategory.cid : 0; + }; + return selector; + }; + + categorySelector.modal = function (options) { + options = options || {}; + options.onSelect = options.onSelect || function () {}; + options.onSubmit = options.onSubmit || function () {}; + app.parseAndTranslate('admin/partials/categories/select-category', { message: options.message }, function (html) { + const modal = bootbox.dialog({ + title: options.title || '[[modules:composer.select_category]]', + message: html, + buttons: { + save: { + label: '[[global:select]]', + className: 'btn-primary', + callback: submit, + }, + }, + }); + + const selector = categorySelector.init(modal.find('[component="category-selector"]'), options); + function submit(ev) { + ev.preventDefault(); + if (selector.selectedCategory) { + options.onSubmit(selector.selectedCategory); + modal.modal('hide'); + } + return false; + } + if (options.openOnLoad) { + modal.on('shown.bs.modal', function () { + modal.find('.dropdown-toggle').dropdown('toggle'); + }); + } + modal.find('form').on('submit', submit); + }); + }; + + return categorySelector; +}); diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js new file mode 100644 index 0000000000..0657e6b5aa --- /dev/null +++ b/public/src/modules/chat.js @@ -0,0 +1,434 @@ +'use strict'; + +define('chat', [ + 'components', 'taskbar', 'translator', 'hooks', 'bootbox', 'alerts', 'api', +], function (components, taskbar, translator, hooks, bootbox, alerts, api) { + const module = {}; + let newMessage = false; + + module.openChat = function (roomId, uid) { + if (!app.user.uid) { + return alerts.error('[[error:not-logged-in]]'); + } + + function loadAndCenter(chatModal) { + module.load(chatModal.attr('data-uuid')); + module.center(chatModal); + module.focusInput(chatModal); + } + + if (module.modalExists(roomId)) { + loadAndCenter(module.getModal(roomId)); + } else { + api.get(`/chats/${roomId}`, { + uid: uid || app.user.uid, + }).then((roomData) => { + roomData.users = roomData.users.filter(function (user) { + return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10); + }); + roomData.uid = uid || app.user.uid; + roomData.isSelf = true; + module.createModal(roomData, loadAndCenter); + }).catch(alerts.error); + } + }; + + module.newChat = function (touid, callback) { + function createChat() { + api.post(`/chats`, { + uids: [touid], + }).then(({ roomId }) => { + if (!ajaxify.data.template.chats) { + module.openChat(roomId); + } else { + ajaxify.go('chats/' + roomId); + } + + callback(null, roomId); + }).catch(alerts.error); + } + + callback = callback || function () { }; + if (!app.user.uid) { + return alerts.error('[[error:not-logged-in]]'); + } + + if (parseInt(touid, 10) === parseInt(app.user.uid, 10)) { + return alerts.error('[[error:cant-chat-with-yourself]]'); + } + socket.emit('modules.chats.isDnD', touid, function (err, isDnD) { + if (err) { + return alerts.error(err); + } + if (!isDnD) { + return createChat(); + } + + bootbox.confirm('[[modules:chat.confirm-chat-with-dnd-user]]', function (ok) { + if (ok) { + createChat(); + } + }); + }); + }; + + module.loadChatsDropdown = function (chatsListEl) { + socket.emit('modules.chats.getRecentChats', { + uid: app.user.uid, + after: 0, + }, function (err, data) { + if (err) { + return alerts.error(err); + } + + const rooms = data.rooms.filter(function (room) { + return room.teaser; + }); + + translator.toggleTimeagoShorthand(function () { + for (let i = 0; i < rooms.length; i += 1) { + rooms[i].teaser.timeago = $.timeago(new Date(parseInt(rooms[i].teaser.timestamp, 10))); + } + translator.toggleTimeagoShorthand(); + app.parseAndTranslate('partials/chats/dropdown', { rooms: rooms }, function (html) { + chatsListEl.find('*').not('.navigation-link').remove(); + chatsListEl.prepend(html); + app.createUserTooltips(chatsListEl, 'right'); + chatsListEl.off('click').on('click', '[data-roomid]', function (ev) { + if ($(ev.target).parents('.user-link').length) { + return; + } + const roomId = $(this).attr('data-roomid'); + if (!ajaxify.currentPage.match(/^chats\//)) { + module.openChat(roomId); + } else { + ajaxify.go('user/' + app.user.userslug + '/chats/' + roomId); + } + }); + + $('[component="chats/mark-all-read"]').off('click').on('click', function () { + socket.emit('modules.chats.markAllRead', function (err) { + if (err) { + return alerts.error(err); + } + }); + }); + }); + }); + }); + }; + + + module.onChatMessageReceived = function (data) { + const isSelf = data.self === 1; + data.message.self = data.self; + + newMessage = data.self === 0; + if (module.modalExists(data.roomId)) { + addMessageToModal(data); + } else if (!ajaxify.data.template.chats) { + api.get(`/chats/${data.roomId}`, {}).then((roomData) => { + roomData.users = roomData.users.filter(function (user) { + return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10); + }); + roomData.silent = true; + roomData.uid = app.user.uid; + roomData.isSelf = isSelf; + module.createModal(roomData); + }).catch(alerts.error); + } + }; + + function addMessageToModal(data) { + const modal = module.getModal(data.roomId); + const username = data.message.fromUser.username; + const isSelf = data.self === 1; + 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); + } + + if (modal.is(':visible')) { + taskbar.updateActive(modal.attr('data-uuid')); + if (ChatsMessages.isAtBottom(modal.find('.chat-content'))) { + ChatsMessages.scrollToBottom(modal.find('.chat-content')); + } + } else if (!ajaxify.data.template.chats) { + module.toggleNew(modal.attr('data-uuid'), true, true); + } + + if (!isSelf && (!modal.is(':visible') || !app.isFocused)) { + taskbar.push('chat', modal.attr('data-uuid'), { + title: '[[modules:chat.chatting_with]] ' + (data.roomName || username), + touid: data.message.fromUser.uid, + roomId: data.roomId, + isSelf: false, + }); + } + }); + } + + module.onUserStatusChange = function (data) { + const modal = module.getModal(data.uid); + app.updateUserStatus(modal.find('[component="user/status"]'), data.status); + }; + + module.onRoomRename = function (data) { + const newTitle = $('
    ').html(data.newName).text(); + const modal = module.getModal(data.roomId); + modal.find('[component="chat/room/name"]').text(newTitle); + taskbar.update('chat', modal.attr('data-uuid'), { + title: newTitle, + }); + hooks.fire('action:chat.renamed', Object.assign(data, { + modal: modal, + })); + }; + + module.getModal = function (roomId) { + return $('#chat-modal-' + roomId); + }; + + module.modalExists = function (roomId) { + return $('#chat-modal-' + roomId).length !== 0; + }; + + module.createModal = function (data, callback) { + callback = callback || function () {}; + require([ + 'scrollStop', 'forum/chats', 'forum/chats/messages', + ], function (scrollStop, Chats, ChatsMessages) { + app.parseAndTranslate('chat', data, function (chatModal) { + if (module.modalExists(data.roomId)) { + return callback(module.getModal(data.roomId)); + } + const uuid = utils.generateUUID(); + let dragged = false; + + chatModal.attr('id', 'chat-modal-' + data.roomId); + chatModal.attr('data-roomid', data.roomId); + chatModal.attr('intervalId', 0); + chatModal.attr('data-uuid', uuid); + chatModal.css('position', 'fixed'); + chatModal.appendTo($('body')); + chatModal.find('.timeago').timeago(); + module.center(chatModal); + + app.loadJQueryUI(function () { + chatModal.find('.modal-content').resizable({ + handles: 'n, e, s, w, se', + minHeight: 250, + minWidth: 400, + }); + + chatModal.find('.modal-content').on('resize', function (event, ui) { + if (ui.originalSize.height === ui.size.height) { + return; + } + + chatModal.find('.modal-body').css('height', module.calculateChatListHeight(chatModal)); + }); + + chatModal.draggable({ + start: function () { + taskbar.updateActive(uuid); + }, + stop: function () { + module.focusInput(chatModal); + }, + distance: 10, + handle: '.modal-header', + }); + }); + + scrollStop.apply(chatModal.find('[component="chat/messages"]')); + + chatModal.find('#chat-close-btn').on('click', function () { + module.close(chatModal); + }); + + function gotoChats() { + const text = components.get('chat/input').val(); + $(window).one('action:ajaxify.end', function () { + components.get('chat/input').val(text); + }); + + ajaxify.go('user/' + app.user.userslug + '/chats/' + chatModal.attr('data-roomid')); + module.close(chatModal); + } + + chatModal.find('.modal-header').on('dblclick', gotoChats); + chatModal.find('button[data-action="maximize"]').on('click', gotoChats); + chatModal.find('button[data-action="minimize"]').on('click', function () { + const uuid = chatModal.attr('data-uuid'); + module.minimize(uuid); + }); + + chatModal.on('mouseup', function () { + taskbar.updateActive(chatModal.attr('data-uuid')); + + if (dragged) { + dragged = false; + } + }); + + chatModal.on('mousemove', function (e) { + if (e.which === 1) { + dragged = true; + } + }); + + chatModal.on('mousemove keypress click', function () { + if (newMessage) { + socket.emit('modules.chats.markRead', data.roomId); + newMessage = false; + } + }); + + Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), data.roomId); + Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="rename"]'), data.roomName); + Chats.addLeaveHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="leave"]')); + Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('.chat-input'), chatModal.find('[data-action="send"]')); + Chats.addMemberHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="members"]')); + + Chats.createAutoComplete(chatModal.find('[component="chat/input"]')); + + Chats.addScrollHandler(chatModal.attr('data-roomid'), data.uid, chatModal.find('.chat-content')); + Chats.addScrollBottomHandler(chatModal.find('.chat-content')); + + Chats.addCharactersLeftHandler(chatModal); + Chats.addIPHandler(chatModal); + + Chats.addUploadHandler({ + dragDropAreaEl: chatModal.find('.modal-content'), + pasteEl: chatModal, + uploadFormEl: chatModal.find('[component="chat/upload"]'), + inputEl: chatModal.find('[component="chat/input"]'), + }); + + ChatsMessages.addSocketListeners(); + + taskbar.push('chat', chatModal.attr('data-uuid'), { + title: '[[modules:chat.chatting_with]] ' + (data.roomName || (data.users.length ? data.users[0].username : '')), + roomId: data.roomId, + icon: 'fa-comment', + state: '', + isSelf: data.isSelf, + }, function () { + taskbar.toggleNew(chatModal.attr('data-uuid'), !data.isSelf); + hooks.fire('action:chat.loaded', chatModal); + + if (typeof callback === 'function') { + callback(chatModal); + } + }); + }); + }); + }; + + module.focusInput = function (chatModal) { + setTimeout(function () { + chatModal.find('[component="chat/input"]').focus(); + }, 20); + }; + + module.close = function (chatModal) { + const uuid = chatModal.attr('data-uuid'); + clearInterval(chatModal.attr('intervalId')); + chatModal.attr('intervalId', 0); + chatModal.remove(); + chatModal.data('modal', null); + taskbar.discard('chat', uuid); + + if (chatModal.attr('data-mobile')) { + module.disableMobileBehaviour(chatModal); + } + + hooks.fire('action:chat.closed', { + uuid: uuid, + modal: chatModal, + }); + }; + + // TODO: see taskbar.js:44 + module.closeByUUID = function (uuid) { + const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]'); + module.close(chatModal); + }; + + module.center = function (chatModal) { + let hideAfter = false; + if (chatModal.hasClass('hide')) { + chatModal.removeClass('hide'); + hideAfter = true; + } + chatModal.css('left', Math.max(0, (($(window).width() - $(chatModal).outerWidth()) / 2) + $(window).scrollLeft()) + 'px'); + chatModal.css('top', Math.max(0, ($(window).height() / 2) - ($(chatModal).outerHeight() / 2)) + 'px'); + + if (hideAfter) { + chatModal.addClass('hide'); + } + return chatModal; + }; + + module.load = function (uuid) { + require(['forum/chats/messages'], function (ChatsMessages) { + const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]'); + chatModal.removeClass('hide'); + taskbar.updateActive(uuid); + ChatsMessages.scrollToBottom(chatModal.find('.chat-content')); + module.focusInput(chatModal); + socket.emit('modules.chats.markRead', chatModal.attr('data-roomid')); + + const env = utils.findBootstrapEnvironment(); + if (env === 'xs' || env === 'sm') { + module.enableMobileBehaviour(chatModal); + } + }); + }; + + module.enableMobileBehaviour = function (modalEl) { + app.toggleNavbar(false); + modalEl.attr('data-mobile', '1'); + const messagesEl = modalEl.find('.modal-body'); + messagesEl.css('height', module.calculateChatListHeight(modalEl)); + function resize() { + messagesEl.css('height', module.calculateChatListHeight(modalEl)); + require(['forum/chats/messages'], function (ChatsMessages) { + ChatsMessages.scrollToBottom(modalEl.find('.chat-content')); + }); + } + + $(window).on('resize', resize); + $(window).one('action:ajaxify.start', function () { + module.close(modalEl); + $(window).off('resize', resize); + }); + }; + + module.disableMobileBehaviour = function () { + app.toggleNavbar(true); + }; + + module.calculateChatListHeight = function (modalEl) { + // Formula: modal height minus header height. Simple(tm). + return modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight(); + }; + + module.minimize = function (uuid) { + const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]'); + chatModal.addClass('hide'); + taskbar.minimize('chat', uuid); + clearInterval(chatModal.attr('intervalId')); + chatModal.attr('intervalId', 0); + hooks.fire('action:chat.minimized', { + uuid: uuid, + modal: chatModal, + }); + }; + + module.toggleNew = taskbar.toggleNew; + + return module; +}); diff --git a/public/src/modules/components.js b/public/src/modules/components.js new file mode 100644 index 0000000000..305aa3c56c --- /dev/null +++ b/public/src/modules/components.js @@ -0,0 +1,73 @@ +'use strict'; + +define('components', function () { + const components = {}; + + components.core = { + 'topic/teaser': function (tid) { + if (tid) { + return $('[component="category/topic"][data-tid="' + tid + '"] [component="topic/teaser"]'); + } + return $('[component="topic/teaser"]'); + }, + topic: function (name, value) { + return $('[component="topic"][data-' + name + '="' + value + '"]'); + }, + post: function (name, value) { + return $('[component="post"][data-' + name + '="' + value + '"]'); + }, + 'post/content': function (pid) { + return $('[component="post"][data-pid="' + pid + '"] [component="post/content"]'); + }, + 'post/header': function (pid) { + return $('[component="post"][data-pid="' + pid + '"] [component="post/header"]'); + }, + 'post/anchor': function (index) { + return $('[component="post"][data-index="' + index + '"] [component="post/anchor"]'); + }, + 'post/vote-count': function (pid) { + return $('[component="post"][data-pid="' + pid + '"] [component="post/vote-count"]'); + }, + 'post/bookmark-count': function (pid) { + return $('[component="post"][data-pid="' + pid + '"] [component="post/bookmark-count"]'); + }, + + 'user/postcount': function (uid) { + return $('[component="user/postcount"][data-uid="' + uid + '"]'); + }, + 'user/reputation': function (uid) { + return $('[component="user/reputation"][data-uid="' + uid + '"]'); + }, + + 'category/topic': function (name, value) { + return $('[component="category/topic"][data-' + name + '="' + value + '"]'); + }, + + 'categories/category': function (name, value) { + return $('[component="categories/category"][data-' + name + '="' + value + '"]'); + }, + + 'chat/message': function (messageId) { + return $('[component="chat/message"][data-mid="' + messageId + '"]'); + }, + + 'chat/message/body': function (messageId) { + return $('[component="chat/message"][data-mid="' + messageId + '"] [component="chat/message/body"]'); + }, + + 'chat/recent/room': function (roomid) { + return $('[component="chat/recent/room"][data-roomid="' + roomid + '"]'); + }, + }; + + components.get = function () { + const args = Array.prototype.slice.call(arguments, 1); + + if (components.core[arguments[0]] && args.length) { + return components.core[arguments[0]].apply(this, args); + } + return $('[component="' + arguments[0] + '"]'); + }; + + return components; +}); diff --git a/public/src/modules/coverPhoto.js b/public/src/modules/coverPhoto.js new file mode 100644 index 0000000000..aa6277949a --- /dev/null +++ b/public/src/modules/coverPhoto.js @@ -0,0 +1,89 @@ +'use strict'; + + +define('coverPhoto', [ + 'alerts', + 'vendor/jquery/draggable-background/backgroundDraggable', +], function (alerts) { + const coverPhoto = { + coverEl: null, + saveFn: null, + }; + + coverPhoto.init = function (coverEl, saveFn, uploadFn, removeFn) { + coverPhoto.coverEl = coverEl; + coverPhoto.saveFn = saveFn; + + coverEl.find('.upload').on('click', uploadFn); + coverEl.find('.resize').on('click', function () { + enableDragging(coverEl); + }); + coverEl.find('.remove').on('click', removeFn); + + coverEl + .on('dragover', coverPhoto.onDragOver) + .on('drop', coverPhoto.onDrop); + + coverEl.find('.save').on('click', coverPhoto.save); + coverEl.addClass('initialised'); + }; + + coverPhoto.onDragOver = function (e) { + e.stopPropagation(); + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = 'copy'; + }; + + coverPhoto.onDrop = function (e) { + e.stopPropagation(); + e.preventDefault(); + + const files = e.originalEvent.dataTransfer.files; + const reader = new FileReader(); + + if (files.length && files[0].type.match('image.*')) { + reader.onload = function (e) { + coverPhoto.coverEl.css('background-image', 'url(' + e.target.result + ')'); + coverPhoto.newCover = e.target.result; + }; + + reader.readAsDataURL(files[0]); + enableDragging(coverPhoto.coverEl); + } + }; + + function enableDragging(coverEl) { + coverEl.toggleClass('active', 1) + .backgroundDraggable({ + axis: 'y', + units: 'percent', + }); + + alerts.alert({ + alert_id: 'drag_start', + title: '[[modules:cover.dragging_title]]', + message: '[[modules:cover.dragging_message]]', + timeout: 5000, + }); + } + + coverPhoto.save = function () { + coverPhoto.coverEl.addClass('saving'); + + coverPhoto.saveFn(coverPhoto.newCover || undefined, coverPhoto.coverEl.css('background-position'), function (err) { + if (!err) { + coverPhoto.coverEl.toggleClass('active', 0); + coverPhoto.coverEl.backgroundDraggable('disable'); + coverPhoto.coverEl.off('dragover', coverPhoto.onDragOver); + coverPhoto.coverEl.off('drop', coverPhoto.onDrop); + alerts.success('[[modules:cover.saved]]'); + } else { + alerts.error(err); + } + + coverPhoto.coverEl.removeClass('saving'); + }); + }; + + return coverPhoto; +}); diff --git a/public/src/modules/flags.js b/public/src/modules/flags.js new file mode 100644 index 0000000000..a1473cc026 --- /dev/null +++ b/public/src/modules/flags.js @@ -0,0 +1,95 @@ +'use strict'; + + +define('flags', ['hooks', 'components', 'api', 'alerts'], function (hooks, components, api, alerts) { + const Flag = {}; + let flagModal; + let flagCommit; + let flagReason; + + Flag.showFlagModal = function (data) { + app.parseAndTranslate('partials/modals/flag_modal', data, function (html) { + flagModal = html; + flagModal.on('hidden.bs.modal', function () { + flagModal.remove(); + }); + + flagCommit = flagModal.find('#flag-post-commit'); + flagReason = flagModal.find('#flag-reason-custom'); + + flagModal.on('click', 'input[name="flag-reason"]', function () { + if ($(this).attr('id') === 'flag-reason-other') { + flagReason.removeAttr('disabled'); + if (!flagReason.val().length) { + flagCommit.attr('disabled', true); + } + } else { + flagReason.attr('disabled', true); + flagCommit.removeAttr('disabled'); + } + }); + + flagCommit.on('click', function () { + const selected = $('input[name="flag-reason"]:checked'); + let reason = selected.val(); + if (selected.attr('id') === 'flag-reason-other') { + reason = flagReason.val(); + } + createFlag(data.type, data.id, reason); + }); + + flagModal.on('click', '#flag-reason-other', function () { + flagReason.focus(); + }); + + flagModal.modal('show'); + hooks.fire('action:flag.showModal', { + modalEl: flagModal, + type: data.type, + id: data.id, + }); + + flagModal.find('#flag-reason-custom').on('keyup blur change', checkFlagButtonEnable); + }); + }; + + Flag.resolve = function (flagId) { + api.put(`/flags/${flagId}`, { + state: 'resolved', + }).then(() => { + alerts.success('[[flags:resolved]]'); + hooks.fire('action:flag.resolved', { flagId: flagId }); + }).catch(alerts.error); + }; + + function createFlag(type, id, reason) { + if (!type || !id || !reason) { + return; + } + const data = { type: type, id: id, reason: reason }; + api.post('/flags', data, function (err, flagId) { + if (err) { + return alerts.error(err); + } + + flagModal.modal('hide'); + alerts.success('[[flags:modal-submit-success]]'); + if (type === 'post') { + const postEl = components.get('post', 'pid', id); + postEl.find('[component="post/flag"]').addClass('hidden').parent().attr('hidden', ''); + postEl.find('[component="post/already-flagged"]').removeClass('hidden').parent().attr('hidden', null); + } + hooks.fire('action:flag.create', { flagId: flagId, data: data }); + }); + } + + function checkFlagButtonEnable() { + if (flagModal.find('#flag-reason-custom').val()) { + flagCommit.removeAttr('disabled'); + } else { + flagCommit.attr('disabled', true); + } + } + + return Flag; +}); diff --git a/public/src/modules/groupSearch.js b/public/src/modules/groupSearch.js new file mode 100644 index 0000000000..016173e0bc --- /dev/null +++ b/public/src/modules/groupSearch.js @@ -0,0 +1,60 @@ +'use strict'; + +define('groupSearch', function () { + const groupSearch = {}; + + groupSearch.init = function (el) { + if (utils.isTouchDevice()) { + return; + } + const searchEl = el.find('[component="group-selector-search"]'); + if (!searchEl.length) { + return; + } + const toggleVisibility = searchEl.parent('[component="group-selector"]').length > 0; + + const groupEls = el.find('[component="group-list"] [data-name]'); + el.on('show.bs.dropdown', function () { + function updateList() { + const val = searchEl.find('input').val().toLowerCase(); + let noMatch = true; + groupEls.each(function () { + const liEl = $(this); + const isMatch = liEl.attr('data-name').toLowerCase().indexOf(val) !== -1; + if (noMatch && isMatch) { + noMatch = false; + } + + liEl.toggleClass('hidden', !isMatch); + }); + + el.find('[component="group-list"] [component="group-no-matches"]').toggleClass('hidden', !noMatch); + } + if (toggleVisibility) { + el.find('.dropdown-toggle').addClass('hidden'); + searchEl.removeClass('hidden'); + } + + searchEl.on('click', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + }); + searchEl.find('input').val('').on('keyup', updateList); + updateList(); + }); + + el.on('shown.bs.dropdown', function () { + searchEl.find('input').focus(); + }); + + el.on('hide.bs.dropdown', function () { + if (toggleVisibility) { + el.find('.dropdown-toggle').removeClass('hidden'); + searchEl.addClass('hidden'); + } + searchEl.off('click').find('input').off('keyup'); + }); + }; + + return groupSearch; +}); diff --git a/public/src/modules/handleBack.js b/public/src/modules/handleBack.js new file mode 100644 index 0000000000..389add5a75 --- /dev/null +++ b/public/src/modules/handleBack.js @@ -0,0 +1,106 @@ +'use strict'; + +define('handleBack', [ + 'components', + 'storage', + 'navigator', + 'forum/pagination', +], function (components, storage, navigator, pagination) { + const handleBack = {}; + let loadTopicsMethod; + + handleBack.init = function (_loadTopicsMethod) { + loadTopicsMethod = _loadTopicsMethod; + saveClickedIndex(); + $(window).off('action:popstate', onBackClicked).on('action:popstate', onBackClicked); + }; + + handleBack.onBackClicked = onBackClicked; + + function saveClickedIndex() { + $('[component="category"]').on('click', '[component="topic/header"]', function () { + const clickedIndex = $(this).parents('[data-index]').attr('data-index'); + const windowScrollTop = $(window).scrollTop(); + $('[component="category/topic"]').each(function (index, el) { + if ($(el).offset().top - windowScrollTop > 0) { + storage.setItem('category:bookmark', $(el).attr('data-index')); + storage.setItem('category:bookmark:clicked', clickedIndex); + storage.setItem('category:bookmark:offset', $(el).offset().top - windowScrollTop); + return false; + } + }); + }); + } + + function onBackClicked(isMarkedUnread) { + const highlightUnread = isMarkedUnread && ajaxify.data.template.unread; + if ( + ajaxify.data.template.category || + ajaxify.data.template.recent || + ajaxify.data.template.popular || + highlightUnread + ) { + let bookmarkIndex = storage.getItem('category:bookmark'); + let clickedIndex = storage.getItem('category:bookmark:clicked'); + + storage.removeItem('category:bookmark'); + storage.removeItem('category:bookmark:clicked'); + if (!utils.isNumber(bookmarkIndex)) { + return; + } + + bookmarkIndex = Math.max(0, parseInt(bookmarkIndex, 10) || 0); + clickedIndex = Math.max(0, parseInt(clickedIndex, 10) || 0); + + if (config.usePagination) { + const page = Math.ceil((parseInt(bookmarkIndex, 10) + 1) / config.topicsPerPage); + if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) { + pagination.loadPage(page, function () { + handleBack.scrollToTopic(bookmarkIndex, clickedIndex); + }); + } else { + handleBack.scrollToTopic(bookmarkIndex, clickedIndex); + } + } else { + if (bookmarkIndex === 0) { + handleBack.scrollToTopic(bookmarkIndex, clickedIndex); + return; + } + + $('[component="category"]').empty(); + loadTopicsMethod(Math.max(0, bookmarkIndex - 1) + 1, function () { + handleBack.scrollToTopic(bookmarkIndex, clickedIndex); + }); + } + } + } + + handleBack.highlightTopic = function (topicIndex) { + const highlight = components.get('category/topic', 'index', topicIndex); + + if (highlight.length && !highlight.hasClass('highlight')) { + highlight.addClass('highlight'); + setTimeout(function () { + highlight.removeClass('highlight'); + }, 5000); + } + }; + + handleBack.scrollToTopic = function (bookmarkIndex, clickedIndex) { + if (!utils.isNumber(bookmarkIndex)) { + return; + } + + const scrollTo = components.get('category/topic', 'index', bookmarkIndex); + + if (scrollTo.length) { + const offset = storage.getItem('category:bookmark:offset'); + storage.removeItem('category:bookmark:offset'); + $(window).scrollTop(scrollTo.offset().top - offset); + handleBack.highlightTopic(clickedIndex); + navigator.update(); + } + }; + + return handleBack; +}); diff --git a/public/src/modules/helpers.common.js b/public/src/modules/helpers.common.js new file mode 100644 index 0000000000..b85fb5f665 --- /dev/null +++ b/public/src/modules/helpers.common.js @@ -0,0 +1,347 @@ +'use strict'; + +module.exports = function (utils, Benchpress, relative_path) { + Benchpress.setGlobal('true', true); + Benchpress.setGlobal('false', false); + + const helpers = { + displayMenuItem, + buildMetaTag, + buildLinkTag, + stringify, + escape, + stripTags, + generateCategoryBackground, + generateChildrenCategories, + generateTopicClass, + membershipBtn, + spawnPrivilegeStates, + localeToHTML, + renderTopicImage, + renderTopicEvents, + renderEvents, + renderDigestAvatar, + userAgentIcons, + buildAvatar, + register, + __escape: identity, + }; + + function identity(str) { + return str; + } + + function displayMenuItem(data, index) { + const item = data.navigation[index]; + if (!item) { + return false; + } + + if (item.route.match('/users') && data.user && !data.user.privileges['view:users']) { + return false; + } + + if (item.route.match('/tags') && data.user && !data.user.privileges['view:tags']) { + return false; + } + + if (item.route.match('/groups') && data.user && !data.user.privileges['view:groups']) { + return false; + } + + return true; + } + + function buildMetaTag(tag) { + const name = tag.name ? 'name="' + tag.name + '" ' : ''; + const property = tag.property ? 'property="' + tag.property + '" ' : ''; + const content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : ''; + + return '\n\t'; + } + + function buildLinkTag(tag) { + const attributes = ['link', 'rel', 'as', 'type', 'href', 'sizes', 'title', 'crossorigin']; + const [link, rel, as, type, href, sizes, title, crossorigin] = attributes.map(attr => (tag[attr] ? `${attr}="${tag[attr]}" ` : '')); + + return '\n\t'; + } + + function stringify(obj) { + // Turns the incoming object into a JSON string + return JSON.stringify(obj).replace(/&/gm, '&').replace(//gm, '>') + .replace(/"/g, '"'); + } + + function escape(str) { + return utils.escapeHTML(str); + } + + function stripTags(str) { + return utils.stripHTMLTags(str); + } + + function generateCategoryBackground(category) { + if (!category) { + return ''; + } + const style = []; + + if (category.bgColor) { + style.push('background-color: ' + category.bgColor); + } + + if (category.color) { + style.push('color: ' + category.color); + } + + if (category.backgroundImage) { + style.push('background-image: url(' + category.backgroundImage + ')'); + if (category.imageClass) { + style.push('background-size: ' + category.imageClass); + } + } + + return style.join('; ') + ';'; + } + + function generateChildrenCategories(category) { + let html = ''; + if (!category || !category.children || !category.children.length) { + return html; + } + category.children.forEach(function (child) { + if (child && !child.isSection) { + const link = child.link ? child.link : (relative_path + '/category/' + child.slug); + html += '' + + '' + + '
    ' + child.name + ''; + } + }); + html = html ? ('' + html + '') : html; + return html; + } + + function generateTopicClass(topic) { + const fields = ['locked', 'pinned', 'deleted', 'unread', 'scheduled']; + return fields.filter(field => !!topic[field]).join(' '); + } + + // Groups helpers + function membershipBtn(groupObj) { + if (groupObj.isMember && groupObj.name !== 'administrators') { + return ''; + } + + if (groupObj.isPending && groupObj.name !== 'administrators') { + return ''; + } else if (groupObj.isInvited) { + return ''; + } else if (!groupObj.disableJoinRequests && groupObj.name !== 'administrators') { + return ''; + } + return ''; + } + + function spawnPrivilegeStates(member, privileges) { + const states = []; + for (const priv in privileges) { + if (privileges.hasOwnProperty(priv)) { + states.push({ + name: priv, + state: privileges[priv], + }); + } + } + return states.map(function (priv) { + const guestDisabled = ['groups:moderate', 'groups:posts:upvote', 'groups:posts:downvote', 'groups:local:login', 'groups:group:create']; + const spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups']; + const globalModDisabled = ['groups:moderate']; + const disabled = + (member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) || + (member === 'spiders' && !spidersEnabled.includes(priv.name)) || + (member === 'Global Moderators' && globalModDisabled.includes(priv.name)); + + return ''; + }).join(''); + } + + function localeToHTML(locale, fallback) { + locale = locale || fallback || 'en-GB'; + return locale.replace('_', '-'); + } + + function renderTopicImage(topicObj) { + if (topicObj.thumb) { + return ''; + } + return ''; + } + + function renderTopicEvents(index, sort) { + if (sort === 'most_votes') { + return ''; + } + const start = this.posts[index].eventStart; + const end = this.posts[index].eventEnd; + const events = this.events.filter(event => event.timestamp >= start && event.timestamp < end); + if (!events.length) { + return ''; + } + + return renderEvents.call(this, events); + } + + function renderEvents(events) { + return events.reduce((html, event) => { + html += `
  • +
    + +
    + + ${event.href ? `${event.text}` : event.text}  + + `; + + if (event.user) { + if (!event.user.system) { + html += `${buildAvatar(event.user, 'xs', true)} ${event.user.username} `; + } else { + html += `[[global:system-user]] `; + } + } + + html += ``; + + if (this.privileges.isAdminOrMod) { + html += ` `; + } + + return html; + }, ''); + } + + function renderDigestAvatar(block) { + if (block.teaser) { + if (block.teaser.user.picture) { + return ''; + } + return '
    ' + block.teaser.user['icon:text'] + '
    '; + } + if (block.user.picture) { + return ''; + } + return '
    ' + block.user['icon:text'] + '
    '; + } + + function userAgentIcons(data) { + let icons = ''; + + switch (data.platform) { + case 'Linux': + icons += ''; + break; + case 'Microsoft Windows': + icons += ''; + break; + case 'Apple Mac': + icons += ''; + break; + case 'Android': + icons += ''; + break; + case 'iPad': + icons += ''; + break; + case 'iPod': // intentional fall-through + case 'iPhone': + icons += ''; + break; + default: + icons += ''; + break; + } + + switch (data.browser) { + case 'Chrome': + icons += ''; + break; + case 'Firefox': + icons += ''; + break; + case 'Safari': + icons += ''; + break; + case 'IE': + icons += ''; + break; + case 'Edge': + icons += ''; + break; + default: + icons += ''; + break; + } + + return icons; + } + + function buildAvatar(userObj, size, rounded, classNames, component) { + /** + * userObj requires: + * - uid, picture, icon:bgColor, icon:text (getUserField w/ "picture" should return all 4), username + * size: one of "xs", "sm", "md", "lg", or "xl" (required), or an integer + * rounded: true or false (optional, default false) + * classNames: additional class names to prepend (optional, default none) + * component: overrides the default component (optional, default none) + */ + + // Try to use root context if passed-in userObj is undefined + if (!userObj) { + userObj = this; + } + + const attributes = [ + 'alt="' + userObj.username + '"', + 'title="' + userObj.username + '"', + 'data-uid="' + userObj.uid + '"', + 'loading="lazy"', + ]; + const styles = []; + classNames = classNames || ''; + + // Validate sizes, handle integers, otherwise fall back to `avatar-sm` + if (['xs', 'sm', 'sm2x', 'md', 'lg', 'xl'].includes(size)) { + classNames += ' avatar-' + size; + } else if (!isNaN(parseInt(size, 10))) { + styles.push('width: ' + size + 'px;', 'height: ' + size + 'px;', 'line-height: ' + size + 'px;', 'font-size: ' + (parseInt(size, 10) / 16) + 'rem;'); + } else { + classNames += ' avatar-sm'; + } + attributes.unshift('class="avatar ' + classNames + (rounded ? ' avatar-rounded' : '') + '"'); + + // Component override + if (component) { + attributes.push('component="' + component + '"'); + } else { + attributes.push('component="avatar/' + (userObj.picture ? 'picture' : 'icon') + '"'); + } + + if (userObj.picture) { + return ''; + } + + styles.push('background-color: ' + userObj['icon:bgColor'] + ';'); + return '' + userObj['icon:text'] + ''; + } + + function register() { + Object.keys(helpers).forEach(function (helperName) { + Benchpress.registerHelper(helperName, helpers[helperName]); + }); + } + + return helpers; +}; diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js new file mode 100644 index 0000000000..b5651c7d03 --- /dev/null +++ b/public/src/modules/helpers.js @@ -0,0 +1,7 @@ +'use strict'; + +const factory = require('./helpers.common'); + +define('helpers', ['utils', 'benchpressjs'], function (utils, Benchpressjs) { + return factory(utils, Benchpressjs, config.relative_path); +}); diff --git a/public/src/modules/hooks.js b/public/src/modules/hooks.js new file mode 100644 index 0000000000..0f84c9643d --- /dev/null +++ b/public/src/modules/hooks.js @@ -0,0 +1,173 @@ +'use strict'; + +define('hooks', [], () => { + const Hooks = { + loaded: {}, + temporary: new Set(), + runOnce: new Set(), + deprecated: { + + }, + logs: { + _collection: new Set(), + }, + }; + + Hooks.logs.collect = () => { + if (Hooks.logs._collection) { + return; + } + + Hooks.logs._collection = new Set(); + }; + + Hooks.logs.log = (...args) => { + if (Hooks.logs._collection) { + Hooks.logs._collection.add(args); + } else { + console.log.apply(console, args); + } + }; + + Hooks.logs.flush = () => { + if (Hooks.logs._collection && Hooks.logs._collection.size) { + console.groupCollapsed('[hooks] Changes to hooks on this page …'); + Hooks.logs._collection.forEach((args) => { + console.log.apply(console, args); + }); + console.groupEnd(); + } + + delete Hooks.logs._collection; + }; + + Hooks.register = (hookName, method) => { + Hooks.loaded[hookName] = Hooks.loaded[hookName] || new Set(); + Hooks.loaded[hookName].add(method); + + if (Hooks.deprecated.hasOwnProperty(hookName)) { + const deprecated = Hooks.deprecated[hookName]; + + if (deprecated) { + console.groupCollapsed(`[hooks] Hook "${hookName}" is deprecated, please use "${deprecated}" instead.`); + } else { + console.groupCollapsed(`[hooks] Hook "${hookName}" is deprecated, there is no alternative.`); + } + + console.info(method); + console.groupEnd(); + } + + Hooks.logs.log(`[hooks] Registered ${hookName}`, method); + return Hooks; + }; + Hooks.on = Hooks.register; + Hooks.one = (hookName, method) => { + Hooks.runOnce.add({ hookName, method }); + return Hooks.register(hookName, method); + }; + + // registerPage/onPage takes care of unregistering the listener on ajaxify + Hooks.registerPage = (hookName, method) => { + Hooks.temporary.add({ hookName, method }); + return Hooks.register(hookName, method); + }; + Hooks.onPage = Hooks.registerPage; + Hooks.register('action:ajaxify.start', () => { + Hooks.temporary.forEach((pair) => { + Hooks.unregister(pair.hookName, pair.method); + Hooks.temporary.delete(pair); + }); + }); + + Hooks.unregister = (hookName, method) => { + if (Hooks.loaded[hookName] && Hooks.loaded[hookName].has(method)) { + Hooks.loaded[hookName].delete(method); + Hooks.logs.log(`[hooks] Unregistered ${hookName}`, method); + } else { + Hooks.logs.log(`[hooks] Unregistration of ${hookName} failed, passed-in method is not a registered listener or the hook itself has no listeners, currently.`); + } + + return Hooks; + }; + Hooks.off = Hooks.unregister; + + Hooks.hasListeners = hookName => Hooks.loaded[hookName] && Hooks.loaded[hookName].size > 0; + + const _onHookError = (e, listener, data) => { + console.warn(`[hooks] Exception encountered in ${listener.name ? listener.name : 'anonymous function'}, stack trace follows.`); + console.error(e); + return Promise.resolve(data); + }; + + const _fireFilterHook = (hookName, data) => { + if (!Hooks.hasListeners(hookName)) { + return Promise.resolve(data); + } + + const listeners = Array.from(Hooks.loaded[hookName]); + return listeners.reduce((promise, listener) => promise.then((data) => { + try { + const result = listener(data); + return utils.isPromise(result) ? + result.then(data => Promise.resolve(data)).catch(e => _onHookError(e, listener, data)) : + result; + } catch (e) { + return _onHookError(e, listener, data); + } + }), Promise.resolve(data)); + }; + + const _fireActionHook = (hookName, data) => { + if (Hooks.hasListeners(hookName)) { + Hooks.loaded[hookName].forEach(listener => listener(data)); + } + + // Backwards compatibility (remove this when we eventually remove jQuery from NodeBB core) + $(window).trigger(hookName, data); + }; + + const _fireStaticHook = async (hookName, data) => { + if (!Hooks.hasListeners(hookName)) { + return Promise.resolve(data); + } + + const listeners = Array.from(Hooks.loaded[hookName]); + await Promise.allSettled(listeners.map((listener) => { + try { + return listener(data); + } catch (e) { + return _onHookError(e, listener); + } + })); + + return await Promise.resolve(data); + }; + + Hooks.fire = (hookName, data) => { + const type = hookName.split(':').shift(); + let result; + switch (type) { + case 'filter': + result = _fireFilterHook(hookName, data); + break; + + case 'action': + result = _fireActionHook(hookName, data); + break; + + case 'static': + result = _fireStaticHook(hookName, data); + break; + } + Hooks.runOnce.forEach((pair) => { + if (pair.hookName === hookName) { + Hooks.unregister(hookName, pair.method); + Hooks.runOnce.delete(pair); + } + }); + return result; + }; + + return Hooks; +}); diff --git a/public/src/modules/iconSelect.js b/public/src/modules/iconSelect.js new file mode 100644 index 0000000000..b1cb4431c6 --- /dev/null +++ b/public/src/modules/iconSelect.js @@ -0,0 +1,125 @@ +'use strict'; + + +define('iconSelect', ['benchpress', 'bootbox'], function (Benchpress, bootbox) { + const iconSelect = {}; + + iconSelect.init = function (el, onModified) { + onModified = onModified || function () {}; + const doubleSize = el.hasClass('fa-2x'); + let selected = el.attr('class').replace('fa-2x', '').replace('fa', '').replace(/\s+/g, ''); + + $('#icons .selected').removeClass('selected'); + + if (selected) { + try { + $('#icons .fa-icons .fa.' + selected).addClass('selected'); + } catch (err) { + selected = ''; + } + } + + Benchpress.render('partials/fontawesome', {}).then(function (html) { + html = $(html); + html.find('.fa-icons').prepend($('')); + + const picker = bootbox.dialog({ + onEscape: true, + backdrop: true, + show: false, + message: html, + title: 'Select an Icon', + buttons: { + noIcon: { + label: 'No Icon', + className: 'btn-default', + callback: function () { + el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '')); + el.val(''); + el.attr('value', ''); + + onModified(el); + }, + }, + success: { + label: 'Select', + className: 'btn-primary', + callback: function () { + const iconClass = $('.bootbox .selected').attr('class') || `fa fa-${$('.bootbox #fa-filter').val()}`; + const categoryIconClass = $('
    ').addClass(iconClass).removeClass('fa').removeClass('selected') + .attr('class'); + const searchElVal = picker.find('input').val(); + + if (categoryIconClass) { + el.attr('class', 'fa ' + (doubleSize ? 'fa-2x ' : '') + categoryIconClass); + el.val(categoryIconClass); + el.attr('value', categoryIconClass); + } else if (searchElVal) { + el.attr('class', searchElVal); + el.val(searchElVal); + el.attr('value', searchElVal); + } + + onModified(el); + }, + }, + }, + }); + + picker.on('show.bs.modal', function () { + const modalEl = $(this); + const searchEl = modalEl.find('input'); + + if (selected) { + modalEl.find('.' + selected).addClass('selected'); + searchEl.val(selected.replace('fa-', '')); + } + }).modal('show'); + + picker.on('shown.bs.modal', function () { + const modalEl = $(this); + const searchEl = modalEl.find('input'); + const icons = modalEl.find('.fa-icons i'); + const submitEl = modalEl.find('button.btn-primary'); + + function changeSelection(newSelection) { + modalEl.find('i.selected').removeClass('selected'); + if (newSelection) { + newSelection.addClass('selected'); + } else if (searchEl.val().length === 0) { + if (selected) { + modalEl.find('.' + selected).addClass('selected'); + } + } else { + modalEl.find('i:visible').first().addClass('selected'); + } + } + + // Focus on the input box + searchEl.selectRange(0, searchEl.val().length); + + modalEl.find('.icon-container').on('click', 'i', function () { + searchEl.val($(this).attr('class').replace('fa fa-', '').replace('selected', '')); + changeSelection($(this)); + }); + + searchEl.on('keyup', function (e) { + if (e.keyCode !== 13) { + // Filter + icons.show(); + icons.each(function (idx, el) { + if (!el.className.match(new RegExp('^fa fa-.*' + searchEl.val() + '.*$'))) { + $(el).hide(); + } + }); + changeSelection(); + } else { + submitEl.click(); + } + }); + }); + }); + }; + + return iconSelect; +}); diff --git a/public/src/modules/logout.js b/public/src/modules/logout.js new file mode 100644 index 0000000000..70bfcd9ed9 --- /dev/null +++ b/public/src/modules/logout.js @@ -0,0 +1,28 @@ +'use strict'; + +define('logout', ['hooks'], function (hooks) { + return function logout(redirect) { + redirect = redirect === undefined ? true : redirect; + hooks.fire('action:app.logout'); + + $.ajax(config.relative_path + '/logout', { + type: 'POST', + headers: { + 'x-csrf-token': config.csrf_token, + }, + beforeSend: function () { + app.flags._logout = true; + }, + success: function (data) { + hooks.fire('action:app.loggedOut', data); + if (redirect) { + if (data.next) { + window.location.href = data.next; + } else { + window.location.reload(); + } + } + }, + }); + }; +}); diff --git a/public/src/modules/messages.js b/public/src/modules/messages.js new file mode 100644 index 0000000000..e30d2a4b4f --- /dev/null +++ b/public/src/modules/messages.js @@ -0,0 +1,131 @@ +'use strict'; + +define('messages', ['bootbox', 'translator', 'storage', 'alerts', 'hooks'], function (bootbox, translator, storage, alerts, hooks) { + const messages = {}; + + let showWelcomeMessage; + let registerMessage; + + messages.show = function () { + hooks.one('action:ajaxify.end', () => { + showQueryStringMessages(); + showCookieWarning(); + messages.showEmailConfirmWarning(); + }); + }; + + messages.showEmailConfirmWarning = function (message) { + if (!config.emailPrompt || !app.user.uid || parseInt(storage.getItem('email-confirm-dismiss'), 10) === 1) { + return; + } + const msg = { + alert_id: 'email_confirm', + type: 'warning', + timeout: 0, + closefn: () => { + storage.setItem('email-confirm-dismiss', 1); + }, + }; + + if (!app.user.email) { + msg.message = '[[error:no-email-to-confirm]]'; + msg.clickfn = function () { + alerts.remove('email_confirm'); + ajaxify.go('user/' + app.user.userslug + '/edit/email'); + }; + alerts.alert(msg); + } else if (!app.user['email:confirmed'] && !app.user.isEmailConfirmSent) { + msg.message = message || '[[error:email-not-confirmed]]'; + msg.clickfn = function () { + alerts.remove('email_confirm'); + ajaxify.go('/me/edit/email'); + }; + alerts.alert(msg); + } else if (!app.user['email:confirmed'] && app.user.isEmailConfirmSent) { + msg.message = '[[error:email-not-confirmed-email-sent]]'; + alerts.alert(msg); + } + }; + + function showCookieWarning() { + if (!config.cookies.enabled || !navigator.cookieEnabled || app.inAdmin || storage.getItem('cookieconsent') === '1') { + return; + } + + config.cookies.message = translator.unescape(config.cookies.message); + config.cookies.dismiss = translator.unescape(config.cookies.dismiss); + config.cookies.link = translator.unescape(config.cookies.link); + config.cookies.link_url = translator.unescape(config.cookies.link_url); + + app.parseAndTranslate('partials/cookie-consent', config.cookies, function (html) { + $(document.body).append(html); + $(document.body).addClass('cookie-consent-open'); + + const warningEl = $('.cookie-consent'); + const dismissEl = warningEl.find('button'); + dismissEl.on('click', function () { + // Save consent cookie and remove warning element + storage.setItem('cookieconsent', '1'); + warningEl.remove(); + $(document.body).removeClass('cookie-consent-open'); + }); + }); + } + + function showQueryStringMessages() { + const params = utils.params({ full: true }); + showWelcomeMessage = params.has('loggedin'); + registerMessage = params.get('register'); + + if (showWelcomeMessage) { + alerts.alert({ + type: 'success', + title: '[[global:welcome_back]] ' + app.user.username + '!', + message: '[[global:you_have_successfully_logged_in]]', + timeout: 5000, + }); + + params.delete('loggedin'); + } + + if (registerMessage) { + bootbox.alert({ + message: utils.escapeHTML(decodeURIComponent(registerMessage)), + }); + + params.delete('register'); + } + + if (params.has('lang') && params.get('lang') === config.defaultLang) { + console.info(`The "lang" parameter was passed in to set the language to "${params.get('lang')}", but that is already the forum default language.`); + params.delete('lang'); + } + + const qs = params.toString(); + ajaxify.updateHistory(ajaxify.currentPage + (qs ? `?${qs}` : '') + document.location.hash, true); + } + + messages.showInvalidSession = function () { + bootbox.alert({ + title: '[[error:invalid-session]]', + message: '[[error:invalid-session-text]]', + closeButton: false, + callback: function () { + window.location.reload(); + }, + }); + }; + + messages.showSessionMismatch = function () { + bootbox.alert({ + title: '[[error:session-mismatch]]', + message: '[[error:session-mismatch-text]]', + closeButton: false, + callback: function () { + window.location.reload(); + }, + }); + }; + + return messages; +}); diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js new file mode 100644 index 0000000000..1a84c4cfdf --- /dev/null +++ b/public/src/modules/navigator.js @@ -0,0 +1,646 @@ +'use strict'; + +define('navigator', ['forum/pagination', 'components', 'hooks', 'alerts'], function (pagination, components, hooks, alerts) { + const navigator = {}; + let index = 0; + let count = 0; + let navigatorUpdateTimeoutId; + + let renderPostIntervalId; + let touchX; + let touchY; + let renderPostIndex; + let isNavigating = false; + let firstMove = true; + + navigator.scrollActive = false; + + let paginationBlockEl = $('.pagination-block'); + let paginationTextEl = paginationBlockEl.find('.pagination-text'); + let paginationBlockMeterEl = paginationBlockEl.find('meter'); + let paginationBlockProgressEl = paginationBlockEl.find('.progress-bar'); + let thumb; + let thumbText; + let thumbIcon; + let thumbIconHeight; + let thumbIconHalfHeight; + + $(window).on('action:ajaxify.start', function () { + $(window).off('keydown', onKeyDown); + }); + + navigator.init = function (selector, count, toTop, toBottom, callback) { + index = 0; + navigator.selector = selector; + navigator.callback = callback; + navigator.toTop = toTop || function () {}; + navigator.toBottom = toBottom || function () {}; + + paginationBlockEl = $('.pagination-block'); + paginationTextEl = paginationBlockEl.find('.pagination-text'); + paginationBlockMeterEl = paginationBlockEl.find('meter'); + paginationBlockProgressEl = paginationBlockEl.find('.progress-bar'); + + thumbIcon = $('.scroller-thumb-icon'); + thumbIconHeight = thumbIcon.height(); + thumbIconHalfHeight = thumbIconHeight / 2; + thumb = $('.scroller-thumb'); + thumbText = thumb.find('.thumb-text'); + + $(window).off('scroll', navigator.delayedUpdate).on('scroll', navigator.delayedUpdate); + + paginationBlockEl.find('.dropdown-menu').off('click').on('click', function (e) { + e.stopPropagation(); + }); + + paginationBlockEl.off('shown.bs.dropdown', '.wrapper').on('shown.bs.dropdown', '.wrapper', function () { + setTimeout(async function () { + if (utils.findBootstrapEnvironment() === 'lg') { + $('.pagination-block input').focus(); + } + const postCountInTopic = await socket.emit('topics.getPostCountInTopic', ajaxify.data.tid); + if (postCountInTopic > 0) { + paginationBlockEl.find('#myNextPostBtn').removeAttr('disabled'); + } + }, 100); + }); + paginationBlockEl.find('.pageup').off('click').on('click', navigator.scrollUp); + paginationBlockEl.find('.pagedown').off('click').on('click', navigator.scrollDown); + paginationBlockEl.find('.pagetop').off('click').on('click', navigator.toTop); + paginationBlockEl.find('.pagebottom').off('click').on('click', navigator.toBottom); + paginationBlockEl.find('#myNextPostBtn').off('click').on('click', gotoMyNextPost); + + paginationBlockEl.find('input').on('keydown', function (e) { + if (e.which === 13) { + const input = $(this); + if (!utils.isNumber(input.val())) { + input.val(''); + return; + } + + const index = parseInt(input.val(), 10); + const url = generateUrl(index); + input.val(''); + $('.pagination-block .dropdown-toggle').trigger('click'); + ajaxify.go(url); + } + }); + + if (ajaxify.data.template.topic) { + handleScrollNav(); + } + + handleKeys(); + + navigator.setCount(count); + navigator.update(0); + }; + + let lastNextIndex = 0; + async function gotoMyNextPost() { + async function getNext(startIndex) { + return await socket.emit('topics.getMyNextPostIndex', { + tid: ajaxify.data.tid, + index: Math.max(1, startIndex), + sort: config.topicPostSort, + }); + } + if (ajaxify.data.template.topic) { + let nextIndex = await getNext(index); + if (lastNextIndex === nextIndex) { // handles last post in pagination + nextIndex = await getNext(nextIndex); + } + if (nextIndex && index !== nextIndex + 1) { + lastNextIndex = nextIndex; + $(window).one('action:ajaxify.end', function () { + if (paginationBlockEl.find('.dropdown-menu').is(':hidden')) { + paginationBlockEl.find('.dropdown-toggle').dropdown('toggle'); + } + }); + navigator.scrollToIndex(nextIndex, true, 0); + } else { + alerts.alert({ + message: '[[topic:no-more-next-post]]', + type: 'info', + }); + + lastNextIndex = 1; + } + } + } + + function clampTop(newTop) { + const parent = thumb.parent(); + const parentOffset = parent.offset(); + if (newTop < parentOffset.top) { + newTop = parentOffset.top; + } else if (newTop > parentOffset.top + parent.height() - thumbIconHeight) { + newTop = parentOffset.top + parent.height() - thumbIconHeight; + } + return newTop; + } + + function setThumbToIndex(index) { + if (!thumb.length || thumb.is(':hidden')) { + return; + } + const parent = thumb.parent(); + const parentOffset = parent.offset(); + let percent = (index - 1) / ajaxify.data.postcount; + if (index === count) { + percent = 1; + } + const newTop = clampTop(parentOffset.top + ((parent.height() - thumbIconHeight) * percent)); + + const offset = { top: newTop, left: thumb.offset().left }; + thumb.offset(offset); + thumbText.text(index + '/' + ajaxify.data.postcount); + renderPost(index); + } + + function handleScrollNav() { + if (!thumb.length) { + return; + } + + const parent = thumb.parent(); + parent.on('click', function (ev) { + if ($(ev.target).hasClass('scroller-container')) { + const index = calculateIndexFromY(ev.pageY); + navigator.scrollToIndex(index - 1, true, 0); + return false; + } + }); + + function calculateIndexFromY(y) { + const newTop = clampTop(y - thumbIconHalfHeight); + const parentOffset = parent.offset(); + const percent = (newTop - parentOffset.top) / (parent.height() - thumbIconHeight); + index = Math.max(1, Math.ceil(ajaxify.data.postcount * percent)); + return index > ajaxify.data.postcount ? ajaxify.data.count : index; + } + + let mouseDragging = false; + hooks.on('action:ajaxify.end', function () { + renderPostIndex = null; + }); + $('.pagination-block .dropdown-menu').parent().on('shown.bs.dropdown', function () { + setThumbToIndex(index); + }); + + thumb.on('mousedown', function () { + mouseDragging = true; + $(window).on('mousemove', mousemove); + firstMove = true; + }); + + function mouseup() { + $(window).off('mousemove', mousemove); + if (mouseDragging) { + navigator.scrollToIndex(index - 1, true, 0); + paginationBlockEl.find('[data-toggle="dropdown"]').trigger('click'); + } + clearRenderInterval(); + mouseDragging = false; + firstMove = false; + } + + function mousemove(ev) { + const newTop = clampTop(ev.pageY - thumbIconHalfHeight); + thumb.offset({ top: newTop, left: thumb.offset().left }); + const index = calculateIndexFromY(ev.pageY); + navigator.updateTextAndProgressBar(); + thumbText.text(index + '/' + ajaxify.data.postcount); + if (firstMove) { + delayedRenderPost(); + } + firstMove = false; + ev.stopPropagation(); + return false; + } + + function delayedRenderPost() { + clearRenderInterval(); + renderPostIntervalId = setInterval(function () { + renderPost(index); + }, 250); + } + + $(window).off('mousemove', mousemove); + $(window).off('mouseup', mouseup).on('mouseup', mouseup); + + thumb.on('touchstart', function (ev) { + isNavigating = true; + touchX = Math.min($(window).width(), Math.max(0, ev.touches[0].clientX)); + touchY = Math.min($(window).height(), Math.max(0, ev.touches[0].clientY)); + firstMove = true; + }); + + thumb.on('touchmove', function (ev) { + const windowWidth = $(window).width(); + const windowHeight = $(window).height(); + const deltaX = Math.abs(touchX - Math.min(windowWidth, Math.max(0, ev.touches[0].clientX))); + const deltaY = Math.abs(touchY - Math.min(windowHeight, Math.max(0, ev.touches[0].clientY))); + touchX = Math.min(windowWidth, Math.max(0, ev.touches[0].clientX)); + touchY = Math.min(windowHeight, Math.max(0, ev.touches[0].clientY)); + + if (deltaY >= deltaX && firstMove) { + isNavigating = true; + delayedRenderPost(); + } + + if (isNavigating && ev.cancelable) { + ev.preventDefault(); + ev.stopPropagation(); + const newTop = clampTop(touchY + $(window).scrollTop() - thumbIconHalfHeight); + thumb.offset({ top: newTop, left: thumb.offset().left }); + const index = calculateIndexFromY(touchY + $(window).scrollTop()); + navigator.updateTextAndProgressBar(); + thumbText.text(index + '/' + ajaxify.data.postcount); + if (firstMove) { + renderPost(index); + } + } + firstMove = false; + }); + + thumb.on('touchend', function () { + clearRenderInterval(); + if (isNavigating) { + navigator.scrollToIndex(index - 1, true, 0); + isNavigating = false; + paginationBlockEl.find('[data-toggle="dropdown"]').trigger('click'); + } + }); + } + + function clearRenderInterval() { + if (renderPostIntervalId) { + clearInterval(renderPostIntervalId); + renderPostIntervalId = 0; + } + } + + function renderPost(index, callback) { + callback = callback || function () {}; + if (renderPostIndex === index || paginationBlockEl.find('.post-content').is(':hidden')) { + return; + } + renderPostIndex = index; + + socket.emit('posts.getPostSummaryByIndex', { tid: ajaxify.data.tid, index: index - 1 }, function (err, postData) { + if (err) { + return alerts.error(err); + } + app.parseAndTranslate('partials/topic/navigation-post', { post: postData }, function (html) { + paginationBlockEl + .find('.post-content') + .html(html) + .find('.timeago').timeago(); + }); + + callback(); + }); + } + + function handleKeys() { + if (!config.usePagination) { + $(window).off('keydown', onKeyDown).on('keydown', onKeyDown); + } + } + + function onKeyDown(ev) { + if (ev.target.nodeName === 'BODY') { + if (ev.shiftKey || ev.ctrlKey || ev.altKey) { + return; + } + if (ev.which === 36 && navigator.toTop) { // home key + navigator.toTop(); + return false; + } else if (ev.which === 35 && navigator.toBottom) { // end key + navigator.toBottom(); + return false; + } + } + } + + function generateUrl(index) { + const pathname = window.location.pathname.replace(config.relative_path, ''); + const parts = pathname.split('/'); + return parts[1] + '/' + parts[2] + '/' + parts[3] + (index ? '/' + index : ''); + } + + navigator.getCount = () => count; + + navigator.setCount = function (value) { + value = parseInt(value, 10); + if (value === count) { + return; + } + count = value; + navigator.updateTextAndProgressBar(); + }; + + navigator.show = function () { + toggle(true); + }; + + navigator.disable = function () { + count = 0; + index = 1; + navigator.callback = null; + navigator.selector = null; + $(window).off('scroll', navigator.delayedUpdate); + + toggle(false); + }; + + function toggle(flag) { + const path = ajaxify.removeRelativePath(window.location.pathname.slice(1)); + if (flag && (!path.startsWith('topic') && !path.startsWith('category'))) { + return; + } + + paginationBlockEl.toggleClass('ready', flag); + } + + navigator.delayedUpdate = function () { + if (!navigatorUpdateTimeoutId) { + navigatorUpdateTimeoutId = setTimeout(function () { + navigator.update(); + navigatorUpdateTimeoutId = undefined; + }, 100); + } + }; + + navigator.update = function (threshold) { + /* + The "threshold" is defined as the distance from the top of the page to + a spot where a user is expecting to begin reading. + */ + threshold = typeof threshold === 'number' ? threshold : undefined; + let newIndex = index; + const els = $(navigator.selector); + if (els.length) { + newIndex = parseInt(els.first().attr('data-index'), 10) + 1; + } + + const scrollTop = $(window).scrollTop(); + const windowHeight = $(window).height(); + const documentHeight = $(document).height(); + const middleOfViewport = scrollTop + (windowHeight / 2); + let previousDistance = Number.MAX_VALUE; + els.each(function () { + const $this = $(this); + const elIndex = parseInt($this.attr('data-index'), 10); + if (elIndex >= 0) { + const distanceToMiddle = + Math.abs(middleOfViewport - ($this.offset().top + ($this.outerHeight(true) / 2))); + + if (distanceToMiddle > previousDistance) { + return false; + } + + if (distanceToMiddle < previousDistance) { + newIndex = elIndex + 1; + previousDistance = distanceToMiddle; + } + } + }); + + const atTop = scrollTop === 0 && parseInt(els.first().attr('data-index'), 10) === 0; + const nearBottom = scrollTop + windowHeight > documentHeight - 100 && parseInt(els.last().attr('data-index'), 10) === count - 1; + + if (atTop) { + newIndex = 1; + } else if (nearBottom) { + newIndex = count; + } + + // If a threshold is undefined, try to determine one based on new index + if (threshold === undefined && ajaxify.data.template.topic) { + if (atTop) { + threshold = 0; + } else { + const anchorEl = components.get('post/anchor', index - 1); + if (anchorEl.length) { + const anchorRect = anchorEl.get(0).getBoundingClientRect(); + threshold = anchorRect.top; + } + } + } + + if (typeof navigator.callback === 'function') { + navigator.callback(newIndex, count, threshold); + } + + if (newIndex !== index) { + index = newIndex; + navigator.updateTextAndProgressBar(); + setThumbToIndex(index); + } + + toggle(!!count); + }; + + navigator.getIndex = () => index; + + navigator.setIndex = (newIndex) => { + index = newIndex + 1; + navigator.updateTextAndProgressBar(); + setThumbToIndex(index); + }; + + navigator.updateTextAndProgressBar = function () { + if (!utils.isNumber(index)) { + return; + } + index = index > count ? count : index; + paginationTextEl.translateHtml('[[global:pagination.out_of, ' + index + ', ' + count + ']]'); + const fraction = (index - 1) / (count - 1 || 1); + paginationBlockMeterEl.val(fraction); + paginationBlockProgressEl.width((fraction * 100) + '%'); + }; + + navigator.scrollUp = function () { + const $window = $(window); + + if (config.usePagination) { + const atTop = $window.scrollTop() <= 0; + if (atTop) { + return pagination.previousPage(function () { + $('body,html').scrollTop($(document).height() - $window.height()); + }); + } + } + $('body,html').animate({ + scrollTop: $window.scrollTop() - $window.height(), + }); + }; + + navigator.scrollDown = function () { + const $window = $(window); + + if (config.usePagination) { + const atBottom = $window.scrollTop() >= $(document).height() - $window.height(); + if (atBottom) { + return pagination.nextPage(); + } + } + $('body,html').animate({ + scrollTop: $window.scrollTop() + $window.height(), + }); + }; + + navigator.scrollTop = function (index) { + if ($(navigator.selector + '[data-index="' + index + '"]').length) { + navigator.scrollToIndex(index, true); + } else { + ajaxify.go(generateUrl()); + } + }; + + navigator.scrollBottom = function (index) { + if (parseInt(index, 10) < 0) { + return; + } + + if ($(navigator.selector + '[data-index="' + index + '"]').length) { + navigator.scrollToIndex(index, true); + } else { + index = parseInt(index, 10) + 1; + ajaxify.go(generateUrl(index)); + } + }; + + navigator.scrollToIndex = function (index, highlight, duration) { + const inTopic = !!components.get('topic').length; + const inCategory = !!components.get('category').length; + + if (!utils.isNumber(index) || (!inTopic && !inCategory)) { + return; + } + + duration = duration !== undefined ? duration : 400; + navigator.scrollActive = true; + + // if in topic and item already on page + if (inTopic && components.get('post/anchor', index).length) { + return navigator.scrollToPostIndex(index, highlight, duration); + } + + // if in category and item alreay on page + if (inCategory && $('[component="category/topic"][data-index="' + index + '"]').length) { + return navigator.scrollToTopicIndex(index, highlight, duration); + } + + if (!config.usePagination) { + navigator.scrollActive = false; + index = parseInt(index, 10) + 1; + ajaxify.go(generateUrl(index)); + return; + } + + const scrollMethod = inTopic ? navigator.scrollToPostIndex : navigator.scrollToTopicIndex; + + const page = 1 + Math.floor(index / config.postsPerPage); + if (parseInt(page, 10) !== ajaxify.data.pagination.currentPage) { + pagination.loadPage(page, function () { + scrollMethod(index, highlight, duration); + }); + } else { + scrollMethod(index, highlight, duration); + } + }; + + navigator.scrollToPostIndex = function (postIndex, highlight, duration) { + const scrollTo = components.get('post', 'index', postIndex); + navigator.scrollToElement(scrollTo, highlight, duration, postIndex); + }; + + navigator.scrollToTopicIndex = function (topicIndex, highlight, duration) { + const scrollTo = $('[component="category/topic"][data-index="' + topicIndex + '"]'); + navigator.scrollToElement(scrollTo, highlight, duration, topicIndex); + }; + + navigator.scrollToElement = async (scrollTo, highlight, duration, newIndex = null) => { + if (!scrollTo.length) { + navigator.scrollActive = false; + return; + } + + await hooks.fire('filter:navigator.scroll', { scrollTo, highlight, duration, newIndex }); + + const postHeight = scrollTo.outerHeight(true); + const navbarHeight = components.get('navbar').outerHeight(true) || 0; + const topicHeaderHeight = $('.topic-header').outerHeight(true) || 0; + const viewportHeight = $(window).height(); + + // Temporarily disable navigator update on scroll + $(window).off('scroll', navigator.delayedUpdate); + + duration = duration !== undefined ? duration : 400; + navigator.scrollActive = true; + let done = false; + + function animateScroll() { + function reenableScroll() { + // Re-enable onScroll behaviour + setTimeout(() => { // fixes race condition from jQuery — onAnimateComplete called too quickly + $(window).on('scroll', navigator.delayedUpdate); + + hooks.fire('action:navigator.scrolled', { scrollTo, highlight, duration, newIndex }); + }, 50); + } + function onAnimateComplete() { + if (done) { + reenableScroll(); + return; + } + done = true; + + navigator.scrollActive = false; + highlightPost(); + + const scrollToRect = scrollTo.get(0).getBoundingClientRect(); + if (!newIndex) { + navigator.update(scrollToRect.top); + } else { + navigator.setIndex(newIndex); + } + } + + let scrollTop = 0; + if (postHeight < viewportHeight - navbarHeight - topicHeaderHeight) { + scrollTop = scrollTo.offset().top - (viewportHeight / 2) + (postHeight / 2); + } else { + scrollTop = scrollTo.offset().top - navbarHeight - topicHeaderHeight; + } + + if (duration === 0) { + $(window).scrollTop(scrollTop); + onAnimateComplete(); + reenableScroll(); + return; + } + $('html, body').animate({ + scrollTop: scrollTop + 'px', + }, duration, onAnimateComplete); + } + + function highlightPost() { + if (highlight) { + $('[component="post"],[component="category/topic"]').removeClass('highlight'); + scrollTo.addClass('highlight'); + setTimeout(function () { + scrollTo.removeClass('highlight'); + }, 10000); + } + } + + animateScroll(); + }; + + return navigator; +}); + diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js new file mode 100644 index 0000000000..da512fd99c --- /dev/null +++ b/public/src/modules/notifications.js @@ -0,0 +1,160 @@ +'use strict'; + + +define('notifications', [ + 'translator', + 'components', + 'navigator', + 'tinycon', + 'hooks', + 'alerts', +], function (translator, components, navigator, Tinycon, hooks, alerts) { + const Notifications = {}; + + let unreadNotifs = {}; + + const _addShortTimeagoString = ({ notifications: notifs }) => new Promise((resolve) => { + translator.toggleTimeagoShorthand(function () { + for (let i = 0; i < notifs.length; i += 1) { + notifs[i].timeago = $.timeago(new Date(parseInt(notifs[i].datetime, 10))); + } + translator.toggleTimeagoShorthand(); + resolve({ notifications: notifs }); + }); + }); + hooks.on('filter:notifications.load', _addShortTimeagoString); + + Notifications.loadNotifications = function (notifList, callback) { + callback = callback || function () {}; + socket.emit('notifications.get', null, function (err, data) { + if (err) { + return alerts.error(err); + } + + const notifs = data.unread.concat(data.read).sort(function (a, b) { + return parseInt(a.datetime, 10) > parseInt(b.datetime, 10) ? -1 : 1; + }); + + hooks.fire('filter:notifications.load', { notifications: notifs }).then(({ notifications }) => { + app.parseAndTranslate('partials/notifications_list', { notifications }, function (html) { + notifList.html(html); + notifList.off('click').on('click', '[data-nid]', function (ev) { + const notifEl = $(this); + if (scrollToPostIndexIfOnPage(notifEl)) { + ev.stopPropagation(); + ev.preventDefault(); + components.get('notifications/list').dropdown('toggle'); + } + + const unread = notifEl.hasClass('unread'); + if (!unread) { + return; + } + const nid = notifEl.attr('data-nid'); + markNotification(nid, true); + }); + components.get('notifications').on('click', '.mark-all-read', Notifications.markAllRead); + + notifList.on('click', '.mark-read', function () { + const liEl = $(this).parents('li'); + const unread = liEl.hasClass('unread'); + const nid = liEl.attr('data-nid'); + markNotification(nid, unread, function () { + liEl.toggleClass('unread'); + }); + return false; + }); + + hooks.fire('action:notifications.loaded', { + notifications: notifs, + list: notifList, + }); + callback(); + }); + }); + }); + }; + + Notifications.onNewNotification = function (notifData) { + if (ajaxify.currentPage === 'notifications') { + ajaxify.refresh(); + } + + socket.emit('notifications.getCount', function (err, count) { + if (err) { + return alerts.error(err); + } + + Notifications.updateNotifCount(count); + }); + + if (!unreadNotifs[notifData.nid]) { + unreadNotifs[notifData.nid] = true; + } + }; + + function markNotification(nid, read, callback) { + socket.emit('notifications.mark' + (read ? 'Read' : 'Unread'), nid, function (err) { + if (err) { + return alerts.error(err); + } + + if (read && unreadNotifs[nid]) { + delete unreadNotifs[nid]; + } + if (callback) { + callback(); + } + }); + } + + function scrollToPostIndexIfOnPage(notifEl) { + // Scroll to index if already in topic (gh#5873) + const pid = notifEl.attr('data-pid'); + const path = notifEl.attr('data-path'); + const postEl = components.get('post', 'pid', pid); + if (path.startsWith(config.relative_path + '/post/') && pid && postEl.length && ajaxify.data.template.topic) { + navigator.scrollToIndex(postEl.attr('data-index'), true); + return true; + } + return false; + } + + Notifications.updateNotifCount = function (count) { + const notifIcon = components.get('notifications/icon'); + count = Math.max(0, count); + if (count > 0) { + notifIcon.removeClass('fa-bell-o').addClass('fa-bell'); + } else { + notifIcon.removeClass('fa-bell').addClass('fa-bell-o'); + } + + notifIcon.toggleClass('unread-count', count > 0); + notifIcon.attr('data-content', count > 99 ? '99+' : count); + + const payload = { + count: count, + updateFavicon: true, + }; + hooks.fire('action:notification.updateCount', payload); + + if (payload.updateFavicon) { + Tinycon.setBubble(count > 99 ? '99+' : count); + } + + if (navigator.setAppBadge) { // feature detection + navigator.setAppBadge(count); + } + }; + + Notifications.markAllRead = function () { + socket.emit('notifications.markAllRead', function (err) { + if (err) { + alerts.error(err); + } + unreadNotifs = {}; + }); + }; + + return Notifications; +}); diff --git a/public/src/modules/pictureCropper.js b/public/src/modules/pictureCropper.js new file mode 100644 index 0000000000..716addd1d4 --- /dev/null +++ b/public/src/modules/pictureCropper.js @@ -0,0 +1,249 @@ +'use strict'; + +define('pictureCropper', ['alerts'], function (alerts) { + const module = {}; + + module.show = function (data, callback) { + const fileSize = data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? parseInt(data.fileSize, 10) : false; + app.parseAndTranslate('partials/modals/upload_file_modal', { + showHelp: data.hasOwnProperty('showHelp') && data.showHelp !== undefined ? data.showHelp : true, + fileSize: fileSize, + title: data.title || '[[global:upload_file]]', + description: data.description || '', + button: data.button || '[[global:upload]]', + accept: data.accept ? data.accept.replace(/,/g, ', ') : '', + }, function (uploadModal) { + uploadModal.modal('show'); + uploadModal.on('hidden.bs.modal', function () { + uploadModal.remove(); + }); + + const uploadForm = uploadModal.find('#uploadForm'); + if (data.route) { + uploadForm.attr('action', data.route); + } + + uploadModal.find('#fileUploadSubmitBtn').on('click', function () { + $(this).addClass('disabled'); + data.uploadModal = uploadModal; + onSubmit(data, callback); + return false; + }); + }); + }; + + module.handleImageCrop = function (data, callback) { + $('#crop-picture-modal').remove(); + app.parseAndTranslate('modals/crop_picture', { + url: utils.escapeHTML(data.url), + }, async function (cropperModal) { + cropperModal.modal({ + backdrop: 'static', + }).modal('show'); + + // Set cropper image max-height based on viewport + const cropBoxHeight = parseInt($(window).height() / 2, 10); + const img = document.getElementById('cropped-image'); + $(img).css('max-height', cropBoxHeight); + const Cropper = (await import(/* webpackChunkName: "cropperjs" */ 'cropperjs')).default; + + let cropperTool = new Cropper(img, { + aspectRatio: data.aspectRatio, + autoCropArea: 1, + viewMode: 1, + checkCrossOrigin: true, + cropmove: function () { + if (data.restrictImageDimension) { + if (cropperTool.cropBoxData.width > data.imageDimension) { + cropperTool.setCropBoxData({ + width: data.imageDimension, + }); + } + if (cropperTool.cropBoxData.height > data.imageDimension) { + cropperTool.setCropBoxData({ + height: data.imageDimension, + }); + } + } + }, + ready: function () { + if (!checkCORS(cropperTool, data)) { + return cropperModal.modal('hide'); + } + + if (data.restrictImageDimension) { + const origDimension = (img.width < img.height) ? img.width : img.height; + const dimension = (origDimension > data.imageDimension) ? data.imageDimension : origDimension; + cropperTool.setCropBoxData({ + width: dimension, + height: dimension, + }); + } + + cropperModal.find('.rotate').on('click', function () { + const degrees = this.getAttribute('data-degrees'); + cropperTool.rotate(degrees); + }); + + cropperModal.find('.flip').on('click', function () { + const option = this.getAttribute('data-option'); + const method = this.getAttribute('data-method'); + if (method === 'scaleX') { + cropperTool.scaleX(option); + } else { + cropperTool.scaleY(option); + } + this.setAttribute('data-option', option * -1); + }); + + cropperModal.find('.reset').on('click', function () { + cropperTool.reset(); + }); + + cropperModal.find('.crop-btn').on('click', function () { + $(this).addClass('disabled'); + const imageData = checkCORS(cropperTool, data); + if (!imageData) { + return; + } + + cropperModal.find('#upload-progress-bar').css('width', '0%'); + cropperModal.find('#upload-progress-box').show().removeClass('hide'); + + socketUpload({ + data: data, + imageData: imageData, + progressBarEl: cropperModal.find('#upload-progress-bar'), + }, function (err, result) { + if (err) { + cropperModal.find('#upload-progress-box').hide(); + cropperModal.find('.upload-btn').removeClass('disabled'); + cropperModal.find('.crop-btn').removeClass('disabled'); + return alerts.error(err); + } + + callback(result.url); + cropperModal.modal('hide'); + }); + }); + + + cropperModal.find('.upload-btn').on('click', async function () { + $(this).addClass('disabled'); + cropperTool.destroy(); + const Cropper = (await import(/* webpackChunkName: "cropperjs" */ 'cropperjs')).default; + cropperTool = new Cropper(img, { + viewMode: 1, + autoCropArea: 1, + ready: function () { + cropperModal.find('.crop-btn').trigger('click'); + }, + }); + }); + }, + }); + }); + }; + + function socketUpload(params, callback) { + const socketData = {}; + socketData[params.data.paramName] = params.data.paramValue; + socketData.method = params.data.socketMethod; + socketData.size = params.imageData.length; + socketData.progress = 0; + + const chunkSize = 100000; + function doUpload() { + const chunk = params.imageData.slice(socketData.progress, socketData.progress + chunkSize); + socket.emit('uploads.upload', { + chunk: chunk, + params: socketData, + }, function (err, result) { + if (err) { + return alerts.error(err); + } + + if (socketData.progress + chunkSize < socketData.size) { + socketData.progress += chunk.length; + params.progressBarEl.css('width', (socketData.progress / socketData.size * 100).toFixed(2) + '%'); + return setTimeout(doUpload, 100); + } + params.progressBarEl.css('width', '100%'); + callback(null, result); + }); + } + doUpload(); + } + + function checkCORS(cropperTool, data) { + let imageData; + try { + imageData = data.imageType ? + cropperTool.getCroppedCanvas().toDataURL(data.imageType) : + cropperTool.getCroppedCanvas().toDataURL(); + } catch (err) { + const corsErrors = [ + 'The operation is insecure.', + 'Failed to execute \'toDataURL\' on \'HTMLCanvasElement\': Tainted canvases may not be exported.', + ]; + if (corsErrors.indexOf(err.message) !== -1) { + alerts.error('[[error:cors-error]]'); + } else { + alerts.error(err.message); + } + return; + } + return imageData; + } + + function onSubmit(data, callback) { + function showAlert(type, message) { + if (type === 'error') { + data.uploadModal.find('#fileUploadSubmitBtn').removeClass('disabled'); + } + data.uploadModal.find('#alert-' + type).translateText(message).removeClass('hide'); + } + const fileInput = data.uploadModal.find('#fileInput'); + if (!fileInput.val()) { + return showAlert('error', '[[uploads:select-file-to-upload]]'); + } + + const file = fileInput[0].files[0]; + const fileSize = data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? parseInt(data.fileSize, 10) : false; + if (fileSize && file.size > fileSize * 1024) { + return showAlert('error', '[[error:file-too-big, ' + fileSize + ']]'); + } + + if (file.name.endsWith('.gif')) { + require(['uploader'], function (uploader) { + uploader.ajaxSubmit(data.uploadModal, callback); + }); + return; + } + + const reader = new FileReader(); + reader.addEventListener('load', function () { + const imageUrl = reader.result; + + data.uploadModal.modal('hide'); + + module.handleImageCrop({ + url: imageUrl, + imageType: file.type, + socketMethod: data.socketMethod, + aspectRatio: data.aspectRatio, + allowSkippingCrop: data.allowSkippingCrop, + restrictImageDimension: data.restrictImageDimension, + imageDimension: data.imageDimension, + paramName: data.paramName, + paramValue: data.paramValue, + }, callback); + }, false); + + if (file) { + reader.readAsDataURL(file); + } + } + + return module; +}); diff --git a/public/src/modules/postSelect.js b/public/src/modules/postSelect.js new file mode 100644 index 0000000000..e1f266d56a --- /dev/null +++ b/public/src/modules/postSelect.js @@ -0,0 +1,73 @@ +'use strict'; + + +define('postSelect', ['components'], function (components) { + const PostSelect = {}; + let onSelect; + + PostSelect.pids = []; + + let allowMainPostSelect = false; + + PostSelect.init = function (_onSelect, options) { + PostSelect.pids.length = 0; + onSelect = _onSelect; + options = options || {}; + allowMainPostSelect = options.allowMainPostSelect || false; + $('#content').on('click', '[component="topic"] [component="post"]', onPostClicked); + disableClicksOnPosts(); + }; + + function onPostClicked(ev) { + ev.stopPropagation(); + const pidClicked = $(this).attr('data-pid'); + const postEls = $('[component="topic"] [data-pid="' + pidClicked + '"]'); + if (!allowMainPostSelect && parseInt($(this).attr('data-index'), 10) === 0) { + return; + } + PostSelect.togglePostSelection(postEls, pidClicked); + } + + PostSelect.disable = function () { + PostSelect.pids.forEach(function (pid) { + components.get('post', 'pid', pid).toggleClass('bg-success', false); + }); + + $('#content').off('click', '[component="topic"] [component="post"]', onPostClicked); + enableClicksOnPosts(); + }; + + PostSelect.togglePostSelection = function (postEls, pid) { + if (pid) { + const index = PostSelect.pids.indexOf(pid); + if (index === -1) { + PostSelect.pids.push(pid); + postEls.toggleClass('bg-success', true); + } else { + PostSelect.pids.splice(index, 1); + postEls.toggleClass('bg-success', false); + } + + if (PostSelect.pids.length) { + PostSelect.pids.sort(function (a, b) { return a - b; }); + } + if (typeof onSelect === 'function') { + onSelect(); + } + } + }; + + function disableClicks() { + return false; + } + + function disableClicksOnPosts() { + $('#content').on('click', '[component="post"] button, [component="post"] a', disableClicks); + } + + function enableClicksOnPosts() { + $('#content').off('click', '[component="post"] button, [component="post"] a', disableClicks); + } + + return PostSelect; +}); diff --git a/public/src/modules/scrollStop.js b/public/src/modules/scrollStop.js new file mode 100644 index 0000000000..12f2562b56 --- /dev/null +++ b/public/src/modules/scrollStop.js @@ -0,0 +1,31 @@ +'use strict'; + + +/* + The point of this library is to enhance(tm) a textarea so that if scrolled, + you can only scroll to the top of it and the event doesn't bubble up to + the document... because it does... and it's annoying at times. + + While I'm here, might I say this is a solved issue on Linux? +*/ + +define('scrollStop', function () { + const Module = {}; + + Module.apply = function (element) { + $(element).on('mousewheel', function (e) { + const scrollTop = this.scrollTop; + const scrollHeight = this.scrollHeight; + const elementHeight = Math.round(this.getBoundingClientRect().height); + + if ( + (e.originalEvent.deltaY < 0 && scrollTop === 0) || // scroll up + (e.originalEvent.deltaY > 0 && (elementHeight + scrollTop) >= scrollHeight) // scroll down + ) { + return false; + } + }); + }; + + return Module; +}); diff --git a/public/src/modules/search.js b/public/src/modules/search.js new file mode 100644 index 0000000000..674ce5c5bf --- /dev/null +++ b/public/src/modules/search.js @@ -0,0 +1,341 @@ +'use strict'; + +define('search', ['translator', 'storage', 'hooks', 'alerts'], function (translator, storage, hooks, alerts) { + const Search = { + current: {}, + }; + + Search.init = function (searchOptions) { + if (!config.searchEnabled) { + return; + } + + searchOptions = searchOptions || { in: config.searchDefaultInQuick || 'titles' }; + const searchButton = $('#search-button'); + const searchFields = $('#search-fields'); + const searchInput = $('#search-fields input'); + const quickSearchContainer = $('#quick-search-container'); + + $('#search-form .advanced-search-link').off('mousedown').on('mousedown', function () { + ajaxify.go('/search'); + }); + + $('#search-form').off('submit').on('submit', function () { + searchInput.blur(); + }); + searchInput.off('blur').on('blur', function dismissSearch() { + setTimeout(function () { + if (!searchInput.is(':focus')) { + searchFields.addClass('hidden'); + searchButton.removeClass('hidden'); + } + }, 200); + }); + searchInput.off('focus'); + + const searchElements = { + inputEl: searchInput, + resultEl: quickSearchContainer, + }; + + Search.enableQuickSearch({ + searchOptions: searchOptions, + searchElements: searchElements, + }); + + searchButton.off('click').on('click', function (e) { + if (!config.loggedIn && !app.user.privileges['search:content']) { + alerts.alert({ + message: '[[error:search-requires-login]]', + timeout: 3000, + }); + ajaxify.go('login'); + return false; + } + e.stopPropagation(); + + Search.showAndFocusInput(); + return false; + }); + + $('#search-form').off('submit').on('submit', function () { + const input = $(this).find('input'); + const data = Search.getSearchPreferences(); + data.term = input.val(); + data.in = searchOptions.in; + hooks.fire('action:search.submit', { + searchOptions: data, + searchElements: searchElements, + }); + Search.query(data, function () { + input.val(''); + }); + + return false; + }); + }; + + Search.enableQuickSearch = function (options) { + if (!config.searchEnabled || !app.user.privileges['search:content']) { + return; + } + + const searchOptions = Object.assign({ in: config.searchDefaultInQuick || 'titles' }, options.searchOptions); + const quickSearchResults = options.searchElements.resultEl; + const inputEl = options.searchElements.inputEl; + let oldValue = inputEl.val(); + const filterCategoryEl = quickSearchResults.find('.filter-category'); + + function updateCategoryFilterName() { + if (ajaxify.data.template.category && ajaxify.data.cid) { + translator.translate('[[search:search-in-category, ' + ajaxify.data.name + ']]', function (translated) { + const name = $('
    ').html(translated).text(); + filterCategoryEl.find('.name').text(name); + }); + } + filterCategoryEl.toggleClass('hidden', !(ajaxify.data.template.category && ajaxify.data.cid)); + } + + function doSearch() { + options.searchOptions = Object.assign({}, searchOptions); + options.searchOptions.term = inputEl.val(); + updateCategoryFilterName(); + + if (ajaxify.data.template.category && ajaxify.data.cid) { + if (filterCategoryEl.find('input[type="checkbox"]').is(':checked')) { + options.searchOptions.categories = [ajaxify.data.cid]; + options.searchOptions.searchChildren = true; + } + } + + quickSearchResults.removeClass('hidden').find('.quick-search-results-container').html(''); + quickSearchResults.find('.loading-indicator').removeClass('hidden'); + hooks.fire('action:search.quick.start', options); + options.searchOptions.searchOnly = 1; + Search.api(options.searchOptions, function (data) { + quickSearchResults.find('.loading-indicator').addClass('hidden'); + if (!data.posts || (options.hideOnNoMatches && !data.posts.length)) { + return quickSearchResults.addClass('hidden').find('.quick-search-results-container').html(''); + } + data.posts.forEach(function (p) { + const text = $('
    ' + p.content + '
    ').text(); + const query = inputEl.val().toLowerCase().replace(/^in:topic-\d+/, ''); + const start = Math.max(0, text.toLowerCase().indexOf(query) - 40); + p.snippet = utils.escapeHTML((start > 0 ? '...' : '') + + text.slice(start, start + 80) + + (text.length - start > 80 ? '...' : '')); + }); + app.parseAndTranslate('partials/quick-search-results', data, function (html) { + if (html.length) { + html.find('.timeago').timeago(); + } + quickSearchResults.toggleClass('hidden', !html.length || !inputEl.is(':focus')) + .find('.quick-search-results-container') + .html(html.length ? html : ''); + const highlightEls = quickSearchResults.find( + '.quick-search-results .quick-search-title, .quick-search-results .snippet' + ); + Search.highlightMatches(options.searchOptions.term, highlightEls); + hooks.fire('action:search.quick.complete', { + data: data, + options: options, + }); + }); + }); + } + + quickSearchResults.find('.filter-category input[type="checkbox"]').on('change', function () { + inputEl.focus(); + doSearch(); + }); + + inputEl.off('keyup').on('keyup', utils.debounce(function () { + if (inputEl.val().length < 3) { + quickSearchResults.addClass('hidden'); + oldValue = inputEl.val(); + return; + } + if (inputEl.val() === oldValue) { + return; + } + oldValue = inputEl.val(); + if (!inputEl.is(':focus')) { + return quickSearchResults.addClass('hidden'); + } + doSearch(); + }, 500)); + + let mousedownOnResults = false; + quickSearchResults.on('mousedown', function () { + $(window).one('mouseup', function () { + quickSearchResults.addClass('hidden'); + }); + mousedownOnResults = true; + }); + inputEl.on('blur', function () { + if (!inputEl.is(':focus') && !mousedownOnResults && !quickSearchResults.hasClass('hidden')) { + quickSearchResults.addClass('hidden'); + } + }); + + let ajaxified = false; + hooks.on('action:ajaxify.end', function () { + if (!ajaxify.isCold()) { + ajaxified = true; + } + }); + + inputEl.on('focus', function () { + mousedownOnResults = false; + const query = inputEl.val(); + oldValue = query; + if (query && quickSearchResults.find('#quick-search-results').children().length) { + updateCategoryFilterName(); + if (ajaxified) { + doSearch(); + ajaxified = false; + } else { + quickSearchResults.removeClass('hidden'); + } + inputEl[0].setSelectionRange( + query.startsWith('in:topic') ? query.indexOf(' ') + 1 : 0, + query.length + ); + } + }); + + inputEl.off('refresh').on('refresh', function () { + doSearch(); + }); + }; + + Search.showAndFocusInput = function () { + $('#search-fields').removeClass('hidden'); + $('#search-button').addClass('hidden'); + $('#search-fields input').focus(); + }; + + Search.query = function (data, callback) { + callback = callback || function () {}; + ajaxify.go('search?' + createQueryString(data)); + callback(); + }; + + Search.api = function (data, callback) { + const apiURL = config.relative_path + '/api/search?' + createQueryString(data); + data.searchOnly = undefined; + const searchURL = config.relative_path + '/search?' + createQueryString(data); + $.get(apiURL, function (result) { + result.url = searchURL; + callback(result); + }); + }; + + function createQueryString(data) { + const searchIn = data.in || 'titles'; + const postedBy = data.by || ''; + let term = data.term.replace(/^[ ?#]*/, ''); + try { + term = encodeURIComponent(term); + } catch (e) { + return alerts.error('[[error:invalid-search-term]]'); + } + + const query = { + term: term, + in: searchIn, + }; + + if (data.matchWords) { + query.matchWords = data.matchWords; + } + + if (postedBy && postedBy.length && (searchIn === 'posts' || searchIn === 'titles' || searchIn === 'titlesposts')) { + query.by = postedBy; + } + + if (data.categories && data.categories.length) { + query.categories = data.categories; + if (data.searchChildren) { + query.searchChildren = data.searchChildren; + } + } + + if (data.hasTags && data.hasTags.length) { + query.hasTags = data.hasTags; + } + + if (parseInt(data.replies, 10) > 0) { + query.replies = data.replies; + query.repliesFilter = data.repliesFilter || 'atleast'; + } + + if (data.timeRange) { + query.timeRange = data.timeRange; + query.timeFilter = data.timeFilter || 'newer'; + } + + if (data.sortBy) { + query.sortBy = data.sortBy; + query.sortDirection = data.sortDirection; + } + + if (data.showAs) { + query.showAs = data.showAs; + } + + if (data.searchOnly) { + query.searchOnly = data.searchOnly; + } + + hooks.fire('action:search.createQueryString', { + query: query, + data: data, + }); + + return decodeURIComponent($.param(query)); + } + + Search.getSearchPreferences = function () { + try { + return JSON.parse(storage.getItem('search-preferences') || '{}'); + } catch (e) { + return {}; + } + }; + + Search.highlightMatches = function (searchQuery, els) { + if (!searchQuery || !els.length) { + return; + } + searchQuery = utils.escapeHTML(searchQuery.replace(/^"/, '').replace(/"$/, '').trim()); + const regexStr = searchQuery.split(' ') + .map(function (word) { return utils.escapeRegexChars(word); }) + .join('|'); + const regex = new RegExp('(' + regexStr + ')', 'gi'); + + els.each(function () { + const result = $(this); + const nested = []; + + result.find('*').each(function () { + $(this).after(''); + nested.push($('
    ').append($(this))); + }); + + result.html(result.html().replace(regex, function (match, p1) { + return '' + p1 + ''; + })); + + nested.forEach(function (nestedEl, i) { + result.html(result.html().replace('', function () { + return nestedEl.html(); + })); + }); + }); + + $('.search-result-text').find('img:not(.not-responsive)').addClass('img-responsive'); + }; + + return Search; +}); diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js new file mode 100644 index 0000000000..166ab5cf09 --- /dev/null +++ b/public/src/modules/settings.js @@ -0,0 +1,609 @@ +'use strict'; + + +define('settings', ['hooks', 'alerts'], function (hooks, alerts) { + // eslint-disable-next-line prefer-const + let Settings; + let onReady = []; + let waitingJobs = 0; + // eslint-disable-next-line prefer-const + let helper; + + /** + Returns the hook of given name that matches the given type or element. + @param type The type of the element to get the matching hook for, or the element itself. + @param name The name of the hook. + */ + function getHook(type, name) { + if (typeof type !== 'string') { + type = $(type); + type = type.data('type') || type.attr('type') || type.prop('tagName'); + } + const plugin = Settings.plugins[type.toLowerCase()]; + if (plugin == null) { + return; + } + const hook = plugin[name]; + if (typeof hook === 'function') { + return hook; + } + return null; + } + + // eslint-disable-next-line prefer-const + helper = { + /** + @returns Object A deep clone of the given object. + */ + deepClone: function (obj) { + if (typeof obj === 'object') { + return JSON.parse(JSON.stringify(obj)); + } + return obj; + }, + /** + Creates a new Element with given data. + @param tagName The tag-name of the element to create. + @param data The attributes to set. + @param text The text to add into the element. + @returns HTMLElement The created element. + */ + createElement: function (tagName, data, text) { + const element = document.createElement(tagName); + for (const k in data) { + if (data.hasOwnProperty(k)) { + element.setAttribute(k, data[k]); + } + } + if (text) { + element.appendChild(document.createTextNode(text)); + } + return element; + }, + /** + Calls the init-hook of the given element. + @param element The element to initialize. + */ + initElement: function (element) { + const hook = getHook(element, 'init'); + if (hook != null) { + hook.call(Settings, $(element)); + } + }, + /** + Calls the destruct-hook of the given element. + @param element The element to destruct. + */ + destructElement: function (element) { + const hook = getHook(element, 'destruct'); + if (hook != null) { + hook.call(Settings, $(element)); + } + }, + /** + Creates and initializes a new element. + @param type The type of the new element. + @param tagName The tag-name of the new element. + @param data The data to forward to create-hook or use as attributes. + @returns JQuery The created element. + */ + createElementOfType: function (type, tagName, data) { + let element; + const hook = getHook(type, 'create'); + if (hook != null) { + element = $(hook.call(Settings, type, tagName, data)); + } else { + if (data == null) { + data = {}; + } + if (type != null) { + data.type = type; + } + element = $(helper.createElement(tagName || 'input', data)); + } + element.data('type', type); + helper.initElement(element); + return element; + }, + /** + Creates a new Array that contains values of given Array depending on trim and empty. + @param array The array to clean. + @param trim Whether to trim each value if it has a trim-function. + @param empty Whether empty values should get added. + @returns Array The filtered and/or modified Array. + */ + cleanArray: function (array, trim, empty) { + const cleaned = []; + if (!trim && empty) { + return array; + } + for (let i = 0; i < array.length; i += 1) { + let value = array[i]; + if (trim) { + if (value === !!value) { + value = +value; + } else if (value && typeof value.trim === 'function') { + value = value.trim(); + } + } + if (empty || (value != null && value.length)) { + cleaned.push(value); + } + } + return cleaned; + }, + isTrue: function (value) { + return value === 'true' || +value === 1; + }, + isFalse: function (value) { + return value === 'false' || +value === 0; + }, + /** + Calls the get-hook of the given element and returns its result. + If no hook is specified it gets treated as input-field. + @param element The element of that the value should get read. + @returns Object The value of the element. + */ + readValue: function (element) { + let empty = !helper.isFalse(element.data('empty')); + const trim = !helper.isFalse(element.data('trim')); + const split = element.data('split'); + const hook = getHook(element, 'get'); + let value; + if (hook != null) { + return hook.call(Settings, element, trim, empty); + } + if (split != null) { + empty = helper.isTrue(element.data('empty')); // default empty-value is false for arrays + value = element.val(); + const array = (value != null && value.split(split || ',')) || []; + return helper.cleanArray(array, trim, empty); + } + value = element.val(); + if (trim && value != null && typeof value.trim === 'function') { + value = value.trim(); + } + if (empty || (value !== undefined && (value == null || value.length !== 0))) { + return value; + } + }, + /** + Calls the set-hook of the given element. + If no hook is specified it gets treated as input-field. + @param element The JQuery-Object of the element to fill. + @param value The value to set. + */ + fillField: function (element, value) { + const hook = getHook(element, 'set'); + let trim = element.data('trim'); + trim = trim !== 'false' && +trim !== 0; + if (hook != null) { + return hook.call(Settings, element, value, trim); + } + if (value instanceof Array) { + value = value.join(element.data('split') || (trim ? ', ' : ',')); + } + if (trim && value && typeof value.trim === 'function') { + value = value.trim(); + if (typeof value.toString === 'function') { + value = value.toString(); + } + } else if (value != null) { + if (typeof value.toString === 'function') { + value = value.toString(); + } + if (trim) { + value = value.trim(); + } + } else { + value = ''; + } + if (value !== undefined) { + element.val(value); + } + }, + /** + Calls the init-hook and {@link helper.fillField} on each field within wrapper-object. + @param wrapper The wrapper-element to set settings within. + */ + initFields: function (wrapper) { + $('[data-key]', wrapper).each(function (ignored, field) { + field = $(field); + const hook = getHook(field, 'init'); + const keyParts = field.data('key').split('.'); + let value = Settings.get(); + if (hook != null) { + hook.call(Settings, field); + } + for (let i = 0; i < keyParts.length; i += 1) { + const part = keyParts[i]; + if (part && value != null) { + value = value[part]; + } + } + helper.fillField(field, value); + }); + }, + /** + Increases the amount of jobs before settings are ready by given amount. + @param amount The amount of jobs to register. + */ + registerReadyJobs: function (amount) { + waitingJobs += amount; + return waitingJobs; + }, + /** + Decreases the amount of jobs before settings are ready by given amount or 1. + If the amount is less or equal 0 all callbacks registered by {@link helper.whenReady} get called. + @param amount The amount of jobs that finished. + */ + beforeReadyJobsDecreased: function (amount) { + if (amount == null) { + amount = 1; + } + if (waitingJobs > 0) { + waitingJobs -= amount; + if (waitingJobs <= 0) { + for (let i = 0; i < onReady.length; i += 1) { + onReady[i](); + } + onReady = []; + } + } + }, + /** + Calls the given callback when the settings are ready. + @param callback The callback. + */ + whenReady: function (callback) { + if (waitingJobs <= 0) { + callback(); + } else { + onReady.push(callback); + } + }, + serializeForm: function (formEl) { + const values = formEl.serializeObject(); + + // "Fix" checkbox values, so that unchecked options are not omitted + formEl.find('input[type="checkbox"]').each(function (idx, inputEl) { + inputEl = $(inputEl); + if (!inputEl.is(':checked')) { + values[inputEl.attr('name')] = 'off'; + } + }); + + // save multiple selects as json arrays + formEl.find('select[multiple]').each(function (idx, selectEl) { + selectEl = $(selectEl); + values[selectEl.attr('name')] = JSON.stringify(selectEl.val()); + }); + + return values; + }, + /** + Persists the given settings with given hash. + @param hash The hash to use as settings-id. + @param settings The settings-object to persist. + @param notify Whether to send notification when settings got saved. + @param callback The callback to call when done. + */ + persistSettings: function (hash, settings, notify, callback) { + if (settings != null && settings._ != null && typeof settings._ !== 'string') { + settings = helper.deepClone(settings); + settings._ = JSON.stringify(settings._); + } + socket.emit('admin.settings.set', { + hash: hash, + values: settings, + }, function (err) { + if (notify) { + if (err) { + alerts.alert({ + title: '[[admin/admin:changes-not-saved]]', + type: 'danger', + message: `[[admin/admin/changes-not-saved-message, ${err.message}]]`, + timeout: 5000, + }); + } else { + alerts.alert({ + title: '[[admin/admin:changes-saved]]', + type: 'success', + message: '[[admin/admin:changes-saved-message]]', + timeout: 2500, + }); + } + } + if (typeof callback === 'function') { + callback(err); + } + }); + }, + /** + Sets the settings to use to given settings. + @param settings The settings to use. + */ + use: function (settings) { + try { + settings._ = JSON.parse(settings._); + } catch (_error) {} + Settings.cfg = settings; + }, + }; + + // eslint-disable-next-line prefer-const + Settings = { + helper: helper, + plugins: {}, + cfg: {}, + + /** + Returns the saved settings. + @returns Object The settings. + */ + get: function () { + if (Settings.cfg != null && Settings.cfg._ !== undefined) { + return Settings.cfg._; + } + return Settings.cfg; + }, + /** + Registers a new plugin and calls its use-hook. + @param service The plugin to register. + @param types The types to bind the plugin to. + */ + registerPlugin: function (service, types) { + if (types == null) { + types = service.types; + } else { + service.types = types; + } + if (typeof service.use === 'function') { + service.use.call(Settings); + } + for (let i = 0; i < types.length; i += 1) { + const type = types[i].toLowerCase(); + if (Settings.plugins[type] == null) { + Settings.plugins[type] = service; + } + } + }, + /** + Sets the settings to given ones, resets the fields within given wrapper and saves the settings server-side. + @param hash The hash to use as settings-id. + @param settings The settings to set. + @param wrapper The wrapper-element to find settings within. + @param callback The callback to call when done. + @param notify Whether to send notification when settings got saved. + */ + set: function (hash, settings, wrapper, callback, notify) { + if (notify == null) { + notify = true; + } + helper.whenReady(function () { + helper.use(settings); + helper.initFields(wrapper || 'form'); + helper.persistSettings(hash, settings, notify, callback); + }); + }, + /** + Fetches the settings from server and calls {@link Settings.helper.initFields} once the settings are ready. + @param hash The hash to use as settings-id. + @param wrapper The wrapper-element to set settings within. + @param callback The callback to call when done. + */ + sync: function (hash, wrapper, callback) { + socket.emit('admin.settings.get', { + hash: hash, + }, function (err, values) { + if (err) { + if (typeof callback === 'function') { + callback(err); + } + } else { + helper.whenReady(function () { + helper.use(values); + helper.initFields(wrapper || 'form'); + if (typeof callback === 'function') { + callback(); + } + }); + } + }); + }, + /** + Reads the settings from fields and saves them server-side. + @param hash The hash to use as settings-id. + @param wrapper The wrapper-element to find settings within. + @param callback The callback to call when done. + @param notify Whether to send notification when settings got saved. + */ + persist: function (hash, wrapper, callback, notify) { + const notSaved = []; + const fields = $('[data-key]', wrapper || 'form').toArray(); + if (notify == null) { + notify = true; + } + for (let i = 0; i < fields.length; i += 1) { + const field = $(fields[i]); + const value = helper.readValue(field); + let parentCfg = Settings.get(); + const keyParts = field.data('key').split('.'); + const lastKey = keyParts[keyParts.length - 1]; + if (keyParts.length > 1) { + for (let j = 0; j < keyParts.length - 1; j += 1) { + const part = keyParts[j]; + if (part && parentCfg != null) { + parentCfg = parentCfg[part]; + } + } + } + if (parentCfg != null) { + if (value != null) { + parentCfg[lastKey] = value; + } else { + delete parentCfg[lastKey]; + } + } else { + notSaved.push(field.data('key')); + } + } + if (notSaved.length) { + alerts.alert({ + title: 'Attributes Not Saved', + message: "'" + (notSaved.join(', ')) + "' could not be saved. Please contact the plugin-author!", + type: 'danger', + timeout: 5000, + }); + } + helper.persistSettings(hash, Settings.cfg, notify, callback); + }, + load: function (hash, formEl, callback) { + callback = callback || function () {}; + const call = formEl.attr('data-socket-get'); + + socket.emit(call || 'admin.settings.get', { + hash: hash, + }, function (err, values) { + if (err) { + return callback(err); + } + // multipe selects are saved as json arrays, parse them here + $(formEl).find('select[multiple]').each(function (idx, selectEl) { + const key = $(selectEl).attr('name'); + if (key && values.hasOwnProperty(key)) { + try { + values[key] = JSON.parse(values[key]); + } catch (e) { + // Leave the value as is + } + } + }); + + // Save loaded settings into ajaxify.data for use client-side + ajaxify.data[call ? hash : 'settings'] = values; + + helper.whenReady(function () { + $(formEl).find('[data-sorted-list]').each(function (idx, el) { + getHook(el, 'get').call(Settings, $(el), hash); + }); + }); + + $(formEl).deserialize(values); + $(formEl).find('input[type="checkbox"]').each(function () { + $(this).parents('.mdl-switch').toggleClass('is-checked', $(this).is(':checked')); + }); + hooks.fire('action:admin.settingsLoaded'); + + // Handle unsaved changes + $(formEl).on('change', 'input, select, textarea', function () { + app.flags = app.flags || {}; + app.flags._unsaved = true; + }); + + const saveEl = document.getElementById('save'); + if (saveEl) { + require(['mousetrap'], function (mousetrap) { + mousetrap.bind('ctrl+s', function (ev) { + saveEl.click(); + ev.preventDefault(); + }); + }); + } + + callback(null, values); + }); + }, + save: function (hash, formEl, callback) { + formEl = $(formEl); + + const controls = formEl.get(0).elements; + const ok = Settings.check(controls); + if (!ok) { + return; + } + + if (formEl.length) { + const values = helper.serializeForm(formEl); + + helper.whenReady(function () { + const list = formEl.find('[data-sorted-list]'); + if (list.length) { + list.each((idx, item) => { + getHook(item, 'set').call(Settings, $(item), values); + }); + } + }); + + const call = formEl.attr('data-socket-set'); + socket.emit(call || 'admin.settings.set', { + hash: hash, + values: values, + }, function (err) { + // Remove unsaved flag to re-enable ajaxify + app.flags._unsaved = false; + + // Also save to local ajaxify.data + ajaxify.data[call ? hash : 'settings'] = values; + + if (typeof callback === 'function') { + callback(err); + } else if (err) { + alerts.alert({ + title: '[[admin/admin:changes-not-saved]]', + message: `[[admin/admin:changes-not-saved-message, ${err.message}]]`, + type: 'error', + timeout: 2500, + }); + } else { + alerts.alert({ + title: '[[admin/admin:changes-saved]]', + type: 'success', + timeout: 2500, + }); + } + }); + } + }, + check: function (controls) { + const onTrigger = (e) => { + const wrapper = e.target.closest('.form-group'); + if (wrapper) { + wrapper.classList.add('has-error'); + } + + e.target.removeEventListener('invalid', onTrigger); + }; + + return Array.prototype.map.call(controls, (controlEl) => { + const wrapper = controlEl.closest('.form-group'); + if (wrapper) { + wrapper.classList.remove('has-error'); + } + + controlEl.addEventListener('invalid', onTrigger); + return controlEl.reportValidity(); + }).every(Boolean); + }, + }; + + + helper.registerReadyJobs(1); + require([ + 'settings/checkbox', + 'settings/number', + 'settings/textarea', + 'settings/select', + 'settings/array', + 'settings/key', + 'settings/object', + 'settings/sorted-list', + ], function () { + for (let i = 0; i < arguments.length; i += 1) { + Settings.registerPlugin(arguments[i]); + } + helper.beforeReadyJobsDecreased(); + }); + + return Settings; +}); diff --git a/public/src/modules/settings/array.js b/public/src/modules/settings/array.js new file mode 100644 index 0000000000..64fa0cedd2 --- /dev/null +++ b/public/src/modules/settings/array.js @@ -0,0 +1,145 @@ +'use strict'; + +define('settings/array', function () { + let helper = null; + + /** + Creates a new button that removes itself and the given elements on click. + Calls {@link Settings.helper.destructElement} for each given field. + @param elements The elements to remove on click. + @returns JQuery The created remove-button. + */ + function createRemoveButton(elements) { + const rm = $(helper.createElement('button', { + class: 'btn btn-xs btn-primary remove', + title: 'Remove Item', + }, '-')); + rm.click(function (event) { + event.preventDefault(); + elements.remove(); + rm.remove(); + elements.each(function (i, element) { + element = $(element); + if (element.is('[data-key]')) { + helper.destructElement(element); + } + }); + }); + return rm; + } + + /** + Creates a new child-element of given field with given data and calls given callback with elements to add. + @param field Any wrapper that contains all fields of the array. + @param key The key of the array. + @param attributes The attributes to call {@link Settings.helper.createElementOfType} with or to add as + element-attributes. + @param value The value to call {@link Settings.helper.fillField} with. + @param separator The separator to use. + @param insertCb The callback to insert the elements. + */ + function addArrayChildElement(field, key, attributes, value, separator, insertCb) { + attributes = helper.deepClone(attributes); + const type = attributes['data-type'] || attributes.type || 'text'; + const element = $(helper.createElementOfType(type, attributes.tagName, attributes)); + element.attr('data-parent', '_' + key); + delete attributes['data-type']; + delete attributes.tagName; + for (const name in attributes) { + if (attributes.hasOwnProperty(name)) { + const val = attributes[name]; + if (name.search('data-') === 0) { + element.data(name.substring(5), val); + } else if (name.search('prop-') === 0) { + element.prop(name.substring(5), val); + } else { + element.attr(name, val); + } + } + } + helper.fillField(element, value); + if ($('[data-parent="_' + key + '"]', field).length) { + insertCb(separator); + } + insertCb(element); + insertCb(createRemoveButton(element.add(separator))); + } + + /** + Adds a new button that adds a new child-element to given element on click. + @param element The element to insert the button. + @param key The key to forward to {@link addArrayChildElement}. + @param attributes The attributes to forward to {@link addArrayChildElement}. + @param separator The separator to forward to {@link addArrayChildElement}. + */ + function addAddButton(element, key, attributes, separator) { + const addSpace = $(document.createTextNode(' ')); + const newValue = element.data('new') || ''; + const add = $(helper.createElement('button', { + class: 'btn btn-sm btn-primary add', + title: 'Expand Array', + }, '+')); + add.click(function (event) { + event.preventDefault(); + addArrayChildElement(element, key, attributes, newValue, separator.clone(), function (el) { + addSpace.before(el); + }); + }); + element.append(addSpace); + element.append(add); + } + + + const SettingsArray = { + types: ['array', 'div'], + use: function () { + helper = this.helper; + }, + create: function (ignored, tagName) { + return helper.createElement(tagName || 'div'); + }, + set: function (element, value) { + let attributes = element.data('attributes'); + const key = element.data('key') || element.data('parent'); + let separator = element.data('split') || ', '; + separator = (function () { + try { + return $(separator); + } catch (_error) { + return $(document.createTextNode(separator)); + } + }()); + if (typeof attributes !== 'object') { + attributes = {}; + } + element.empty(); + if (!(value instanceof Array)) { + value = []; + } + for (let i = 0; i < value.length; i += 1) { + addArrayChildElement(element, key, attributes, value[i], separator.clone(), function (el) { + element.append(el); + }); + } + addAddButton(element, key, attributes, separator); + }, + get: function (element, trim, empty) { + const key = element.data('key') || element.data('parent'); + const children = $('[data-parent="_' + key + '"]', element); + const values = []; + children.each(function (i, child) { + child = $(child); + const val = helper.readValue(child); + const empty = helper.isTrue(child.data('empty')); + if (empty || (val !== undefined && (val == null || val.length !== 0))) { + return values.push(val); + } + }); + if (empty || values.length) { + return values; + } + }, + }; + + return SettingsArray; +}); diff --git a/public/src/modules/settings/checkbox.js b/public/src/modules/settings/checkbox.js new file mode 100644 index 0000000000..c21b3de1ba --- /dev/null +++ b/public/src/modules/settings/checkbox.js @@ -0,0 +1,39 @@ +'use strict'; + +define('settings/checkbox', function () { + let Settings = null; + + const SettingsCheckbox = { + types: ['checkbox'], + use: function () { + Settings = this; + }, + create: function () { + return Settings.helper.createElement('input', { + type: 'checkbox', + }); + }, + set: function (element, value) { + element.prop('checked', value); + element.closest('.mdl-switch').toggleClass('is-checked', element.is(':checked')); + }, + get: function (element, trim, empty) { + const value = element.prop('checked'); + if (value == null) { + return; + } + if (!empty) { + if (value) { + return value; + } + return; + } + if (trim) { + return value ? 1 : 0; + } + return value; + }, + }; + + return SettingsCheckbox; +}); diff --git a/public/src/modules/settings/key.js b/public/src/modules/settings/key.js new file mode 100644 index 0000000000..5d25a1b416 --- /dev/null +++ b/public/src/modules/settings/key.js @@ -0,0 +1,237 @@ +'use strict'; + +define('settings/key', function () { + let helper = null; + let lastKey = null; + let oldKey = null; + const keyMap = Object.freeze({ + 0: '', + 8: 'Backspace', + 9: 'Tab', + 13: 'Enter', + 27: 'Escape', + 32: 'Space', + 37: 'Left', + 38: 'Up', + 39: 'Right', + 40: 'Down', + 45: 'Insert', + 46: 'Delete', + 187: '=', + 189: '-', + 190: '.', + 191: '/', + 219: '[', + 220: '\\', + 221: ']', + }); + + function Key() { + this.c = false; + this.a = false; + this.s = false; + this.m = false; + this.code = 0; + this.char = ''; + } + + /** + Returns either a Key-Object representing the given event or null if only modification-keys got released. + @param event The event to inspect. + @returns Key | null The Key-Object the focused element should be set to. + */ + function getKey(event) { + const anyModChange = ( + event.ctrlKey !== lastKey.c || + event.altKey !== lastKey.a || + event.shiftKey !== lastKey.s || + event.metaKey !== lastKey.m + ); + const modChange = ( + event.ctrlKey + + event.altKey + + event.shiftKey + + event.metaKey - + lastKey.c - + lastKey.a - + lastKey.s - + lastKey.m + ); + const key = new Key(); + key.c = event.ctrlKey; + key.a = event.altKey; + key.s = event.shiftKey; + key.m = event.metaKey; + lastKey = key; + if (anyModChange) { + if (modChange < 0) { + return null; + } + key.code = oldKey.code; + key.char = oldKey.char; + } else { + key.code = event.which; + key.char = convertKeyCodeToChar(key.code); + } + oldKey = key; + return key; + } + + /** + Returns the string that represents the given key-code. + @param code The key-code. + @returns String Representation of the given key-code. + */ + function convertKeyCodeToChar(code) { + code = +code; + if (code === 0) { + return ''; + } else if (code >= 48 && code <= 90) { + return String.fromCharCode(code).toUpperCase(); + } else if (code >= 112 && code <= 123) { + return 'F' + (code - 111); + } + return keyMap[code] || ('#' + code); + } + + /** + Returns a string to identify a Key-Object. + @param key The Key-Object that should get identified. + @param human Whether to show 'Enter a key' when key-char is empty. + @param short Whether to shorten modification-names to first character. + @param separator The separator between modification-names and key-char. + @returns String The string to identify the given key-object the given way. + */ + function getKeyString(key, human, short, separator) { + let str = ''; + if (!(key instanceof Key)) { + return str; + } + if (!key.char) { + if (human) { + return 'Enter a key'; + } + return ''; + } + if (!separator || /CtrlAShifMea#/.test(separator)) { + separator = human ? ' + ' : '+'; + } + if (key.c) { + str += (short ? 'C' : 'Ctrl') + separator; + } + if (key.a) { + str += (short ? 'A' : 'Alt') + separator; + } + if (key.s) { + str += (short ? 'S' : 'Shift') + separator; + } + if (key.m) { + str += (short ? 'M' : 'Meta') + separator; + } + + let out; + if (human) { + out = key.char; + } else if (key.code) { + out = '#' + key.code || ''; + } + + return str + out; + } + + /** + Parses the given string into a Key-Object. + @param str The string to parse. + @returns Key The Key-Object that got identified by the given string. + */ + function getKeyFromString(str) { + if (str instanceof Key) { + return str; + } + const key = new Key(); + const sep = /([^CtrlAShifMea#\d]+)(?:#|\d)/.exec(str); + const parts = sep != null ? str.split(sep[1]) : [str]; + for (let i = 0; i < parts.length; i += 1) { + const part = parts[i]; + switch (part) { + case 'C': + case 'Ctrl': + key.c = true; + break; + case 'A': + case 'Alt': + key.a = true; + break; + case 'S': + case 'Shift': + key.s = true; + break; + case 'M': + case 'Meta': + key.m = true; + break; + default: { + const num = /\d+/.exec(part); + if (num != null) { + key.code = num[0]; + } + key.char = convertKeyCodeToChar(key.code); + } + } + } + return key; + } + + + const SettingsKey = { + types: ['key'], + use: function () { + helper = this.helper; + }, + init: function (element) { + element.focus(function () { + oldKey = element.data('keyData') || new Key(); + lastKey = new Key(); + }).keydown(function (event) { + event.preventDefault(); + handleEvent(element, event); + }).keyup(function (event) { + handleEvent(element, event); + }); + return element; + }, + set: function (element, value) { + const key = getKeyFromString(value || ''); + element.data('keyData', key); + if (key.code) { + element.removeClass('alert-danger'); + } else { + element.addClass('alert-danger'); + } + element.val(getKeyString(key, true, false, ' + ')); + }, + get: function (element, trim, empty) { + const key = element.data('keyData'); + const separator = element.data('split') || element.data('separator') || '+'; + const short = !helper.isFalse(element.data('short')); + if (trim) { + if (empty || (key != null && key.char)) { + return getKeyString(key, false, short, separator); + } + } else if (empty || (key != null && key.code)) { + return key; + } + }, + }; + + function handleEvent(element, event) { + event = event || window.event; + event.which = event.which || event.keyCode || event.key; + const key = getKey(event); + if (key != null) { + SettingsKey.set(element, key); + } + } + + return SettingsKey; +}); diff --git a/public/src/modules/settings/number.js b/public/src/modules/settings/number.js new file mode 100644 index 0000000000..4cf2884c76 --- /dev/null +++ b/public/src/modules/settings/number.js @@ -0,0 +1,17 @@ +'use strict'; + +define('settings/number', function () { + return { + types: ['number'], + get: function (element, trim, empty) { + const value = element.val(); + if (!empty) { + if (value) { + return +value; + } + return; + } + return value ? +value : 0; + }, + }; +}); diff --git a/public/src/modules/settings/object.js b/public/src/modules/settings/object.js new file mode 100644 index 0000000000..1c4bd821ed --- /dev/null +++ b/public/src/modules/settings/object.js @@ -0,0 +1,124 @@ +'use strict'; + +define('settings/object', function () { + let helper = null; + + /** + Creates a new child-element of given property with given data and calls given callback with elements to add. + @param field Any wrapper that contains all properties of the object. + @param key The key of the object. + @param attributes The attributes to call {@link Settings.helper.createElementOfType} with or to add as + element-attributes. + @param value The value to call {@link Settings.helper.fillField} with. + @param separator The separator to use. + @param insertCb The callback to insert the elements. + */ + function addObjectPropertyElement(field, key, attributes, prop, value, separator, insertCb) { + const prepend = attributes['data-prepend']; + const append = attributes['data-append']; + delete attributes['data-prepend']; + delete attributes['data-append']; + attributes = helper.deepClone(attributes); + const type = attributes['data-type'] || attributes.type || 'text'; + const element = $(helper.createElementOfType(type, attributes.tagName, attributes)); + element.attr('data-parent', '_' + key); + element.attr('data-prop', prop); + delete attributes['data-type']; + delete attributes.tagName; + for (const name in attributes) { + if (attributes.hasOwnProperty(name)) { + const val = attributes[name]; + if (name.search('data-') === 0) { + element.data(name.substring(5), val); + } else if (name.search('prop-') === 0) { + element.prop(name.substring(5), val); + } else { + element.attr(name, val); + } + } + } + helper.fillField(element, value); + if ($('[data-parent="_' + key + '"]', field).length) { + insertCb(separator); + } + if (prepend) { + insertCb(prepend); + } + insertCb(element); + if (append) { + insertCb(append); + } + } + + const SettingsObject = { + types: ['object'], + use: function () { + helper = this.helper; + }, + create: function (ignored, tagName) { + return helper.createElement(tagName || 'div'); + }, + set: function (element, value) { + const properties = element.data('attributes') || element.data('properties'); + const key = element.data('key') || element.data('parent'); + let separator = element.data('split') || ', '; + let propertyIndex; + let propertyName; + let attributes; + separator = (function () { + try { + return $(separator); + } catch (_error) { + return $(document.createTextNode(separator)); + } + }()); + element.empty(); + if (typeof value !== 'object') { + value = {}; + } + if (Array.isArray(properties)) { + for (propertyIndex in properties) { + if (properties.hasOwnProperty(propertyIndex)) { + attributes = properties[propertyIndex]; + if (typeof attributes !== 'object') { + attributes = {}; + } + propertyName = attributes['data-prop'] || attributes['data-property'] || propertyIndex; + if (value[propertyName] === undefined && attributes['data-new'] !== undefined) { + value[propertyName] = attributes['data-new']; + } + addObjectPropertyElement( + element, + key, + attributes, + propertyName, + value[propertyName], + separator.clone(), + function (el) { element.append(el); } + ); + } + } + } + }, + get: function (element, trim, empty) { + const key = element.data('key') || element.data('parent'); + const properties = $('[data-parent="_' + key + '"]', element); + const value = {}; + properties.each(function (i, property) { + property = $(property); + const val = helper.readValue(property); + const prop = property.data('prop'); + const empty = helper.isTrue(property.data('empty')); + if (empty || (val !== undefined && (val == null || val.length !== 0))) { + value[prop] = val; + return val; + } + }); + if (empty || Object.keys(value).length) { + return value; + } + }, + }; + + return SettingsObject; +}); diff --git a/public/src/modules/settings/select.js b/public/src/modules/settings/select.js new file mode 100644 index 0000000000..c149235023 --- /dev/null +++ b/public/src/modules/settings/select.js @@ -0,0 +1,46 @@ +'use strict'; + +define('settings/select', function () { + let Settings = null; + + function addOptions(element, options) { + for (let i = 0; i < options.length; i += 1) { + const optionData = options[i]; + const value = optionData.text || optionData.value; + delete optionData.text; + element.append($(Settings.helper.createElement('option', optionData)).text(value)); + } + } + + + const SettingsSelect = { + types: ['select'], + use: function () { + Settings = this; + }, + create: function (ignore, ignored, data) { + const element = $(Settings.helper.createElement('select')); + // prevent data-options from being attached to DOM + addOptions(element, data['data-options']); + delete data['data-options']; + return element; + }, + init: function (element) { + const options = element.data('options'); + if (options != null) { + addOptions(element, options); + } + }, + set: function (element, value) { + element.val(value || ''); + }, + get: function (element, ignored, empty) { + const value = element.val(); + if (empty || value) { + return value; + } + }, + }; + + return SettingsSelect; +}); diff --git a/public/src/modules/settings/sorted-list.js b/public/src/modules/settings/sorted-list.js new file mode 100644 index 0000000000..d565e4e9ab --- /dev/null +++ b/public/src/modules/settings/sorted-list.js @@ -0,0 +1,172 @@ +'use strict'; + +define('settings/sorted-list', [ + 'benchpress', + 'bootbox', + 'hooks', + 'jquery-ui/widgets/sortable', +], function (benchpress, bootbox, hooks) { + let Settings; + + + const SortedList = { + types: ['sorted-list'], + use: function () { + Settings = this; + }, + set: function ($container, values) { + const key = $container.attr('data-sorted-list'); + + values[key] = []; + $container.find('[data-type="item"]').each(function (idx, item) { + const itemUUID = $(item).attr('data-sorted-list-uuid'); + + const formData = Settings.helper.serializeForm($('[data-sorted-list-object="' + key + '"][data-sorted-list-uuid="' + itemUUID + '"]')); + stripTags(formData); + values[key].push(formData); + }); + }, + get: async ($container, hash) => { + const { listEl, key, formTpl, formValues } = await hooks.fire('filter:settings.sorted-list.load', { + listEl: $container.find('[data-type="list"]'), + key: $container.attr('data-sorted-list'), + formTpl: $container.attr('data-form-template'), + formValues: {}, + }); + + const formHtml = await benchpress.render(formTpl, formValues); + + const addBtn = $('[data-sorted-list="' + key + '"] [data-type="add"]'); + + addBtn.on('click', function () { + const modal = bootbox.confirm(formHtml, function (save) { + if (save) { + SortedList.addItem(modal.find('form').children(), $container); + } + }); + hooks.fire('action:settings.sorted-list.modal', { modal }); + }); + + const call = $container.parents('form').attr('data-socket-get'); + const list = ajaxify.data[call ? hash : 'settings'][key]; + + if (Array.isArray(list) && typeof list[0] !== 'string') { + const items = await Promise.all(list.map(async (item) => { + ({ item } = await hooks.fire('filter:settings.sorted-list.loadItem', { item })); + + const itemUUID = utils.generateUUID(); + const form = $(formHtml).deserialize(item); + form.attr('data-sorted-list-uuid', itemUUID); + form.attr('data-sorted-list-object', key); + $('#content').append(form.hide()); + + return { itemUUID, item }; + })); + + // todo: parse() needs to be refactored to return the html, so multiple calls can be parallelized + // eslint-disable-next-line no-restricted-syntax + for (const { itemUUID, item } of items) { + // eslint-disable-next-line no-await-in-loop + await parse($container, itemUUID, item); + hooks.fire('action:settings.sorted-list.itemLoaded', { element: listEl.get(0) }); + } + + hooks.fire('action:settings.sorted-list.loaded', { + containerEl: $container.get(0), + listEl: listEl.get(0), + hash, + key, + }); + } + + listEl.sortable().addClass('pointer'); + }, + addItem: async ($formElements, $target) => { + const key = $target.attr('data-sorted-list'); + const itemUUID = utils.generateUUID(); + const form = $('
    '); + form.append($formElements); + + $('#content').append(form.hide()); + + let data = Settings.helper.serializeForm(form); + ({ item: data } = await hooks.fire('filter:settings.sorted-list.loadItem', { item: data })); + parse($target, itemUUID, data); + }, + }; + + function setupRemoveButton($container, itemUUID) { + const removeBtn = $container.find('[data-sorted-list-uuid="' + itemUUID + '"] [data-type="remove"]'); + removeBtn.on('click', function () { + $('[data-sorted-list-uuid="' + itemUUID + '"]').remove(); + }); + } + + function setupEditButton($container, itemUUID) { + const $list = $container.find('[data-type="list"]'); + const key = $container.attr('data-sorted-list'); + const editBtn = $('[data-sorted-list-uuid="' + itemUUID + '"] [data-type="edit"]'); + + editBtn.on('click', function () { + const form = $('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]'); + const clone = form.clone(true).show(); + + // .clone() doesn't preserve the state of `select` elements, fixing after the fact + clone.find('select').each((idx, el) => { + el.value = form.find(`select#${el.id}`).val(); + }); + + const modal = bootbox.confirm(clone, async (save) => { + if (save) { + const form = $('
    '); + form.append(modal.find('form').children()); + + $('#content').find('[data-sorted-list-uuid="' + itemUUID + '"][data-sorted-list-object="' + key + '"]').remove(); + $('#content').append(form.hide()); + + + let data = Settings.helper.serializeForm(form); + ({ item: data } = await hooks.fire('filter:settings.sorted-list.loadItem', { item: data })); + stripTags(data); + + const oldItem = $list.find('[data-sorted-list-uuid="' + itemUUID + '"]'); + parse($container, itemUUID, data, oldItem); + } + }); + hooks.fire('action:settings.sorted-list.modal', { modal }); + }); + } + + function parse($container, itemUUID, data, replaceEl) { + // replaceEl is optional + const $list = $container.find('[data-type="list"]'); + const itemTpl = $container.attr('data-item-template'); + + stripTags(data); + + return new Promise((resolve) => { + app.parseAndTranslate(itemTpl, data, function (itemHtml) { + itemHtml = $(itemHtml); + if (replaceEl) { + replaceEl.replaceWith(itemHtml); + } else { + $list.append(itemHtml); + } + itemHtml.attr('data-sorted-list-uuid', itemUUID); + + setupRemoveButton($container, itemUUID); + setupEditButton($container, itemUUID); + hooks.fire('action:settings.sorted-list.parse', { itemHtml }); + resolve(); + }); + }); + } + + function stripTags(data) { + return Object.entries(data || {}).forEach(([field, value]) => { + data[field] = typeof value === 'string' ? utils.stripHTMLTags(value, utils.stripTags) : value; + }); + } + + return SortedList; +}); diff --git a/public/src/modules/settings/textarea.js b/public/src/modules/settings/textarea.js new file mode 100644 index 0000000000..1ef951398c --- /dev/null +++ b/public/src/modules/settings/textarea.js @@ -0,0 +1,36 @@ +'use strict'; + +define('settings/textarea', function () { + let Settings = null; + + const SettingsArea = { + types: ['textarea'], + use: function () { + Settings = this; + }, + create: function () { + return Settings.helper.createElement('textarea'); + }, + set: function (element, value, trim) { + if (trim && value != null && typeof value.trim === 'function') { + value = value.trim(); + } + element.val(value || ''); + }, + get: function (element, trim, empty) { + let value = element.val(); + if (trim) { + if (value == null) { + value = undefined; + } else { + value = value.trim(); + } + } + if (empty || value) { + return value; + } + }, + }; + + return SettingsArea; +}); diff --git a/public/src/modules/share.js b/public/src/modules/share.js new file mode 100644 index 0000000000..ea2f1315c5 --- /dev/null +++ b/public/src/modules/share.js @@ -0,0 +1,55 @@ +'use strict'; + + +define('share', ['hooks'], function (hooks) { + const module = {}; + + module.addShareHandlers = function (name) { + const baseUrl = window.location.protocol + '//' + window.location.host; + + function openShare(url, urlToPost, width, height) { + window.open(url + encodeURIComponent(baseUrl + config.relative_path + urlToPost), '_blank', 'width=' + width + ',height=' + height + ',scrollbars=no,status=no'); + hooks.fire('action:share.open', { + url: url, + urlToPost: urlToPost, + }); + return false; + } + + $('#content').off('shown.bs.dropdown', '.share-dropdown').on('shown.bs.dropdown', '.share-dropdown', function () { + const postLink = $(this).find('.post-link'); + postLink.val(baseUrl + getPostUrl($(this))); + + // without the setTimeout can't select the text in the input + setTimeout(function () { + postLink.putCursorAtEnd().select(); + }, 50); + }); + + addHandler('.post-link', function (e) { + e.preventDefault(); + return false; + }); + + addHandler('[component="share/twitter"]', function () { + return openShare('https://twitter.com/intent/tweet?text=' + encodeURIComponent(name) + '&url=', getPostUrl($(this)), 550, 420); + }); + + addHandler('[component="share/facebook"]', function () { + return openShare('https://www.facebook.com/sharer/sharer.php?u=', getPostUrl($(this)), 626, 436); + }); + + hooks.fire('action:share.addHandlers', { openShare: openShare }); + }; + + function addHandler(selector, callback) { + $('#content').off('click', selector).on('click', selector, callback); + } + + function getPostUrl(clickedElement) { + const pid = parseInt(clickedElement.parents('[data-pid]').attr('data-pid'), 10); + return '/post' + (pid ? '/' + (pid) : ''); + } + + return module; +}); diff --git a/public/src/modules/slugify.js b/public/src/modules/slugify.js new file mode 100644 index 0000000000..b189b2c729 --- /dev/null +++ b/public/src/modules/slugify.js @@ -0,0 +1,40 @@ +'use strict'; + +/* global XRegExp */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + define('slugify', ['xregexp'], factory); + } else if (typeof exports === 'object') { + module.exports = factory(require('xregexp')); + } else { + window.slugify = factory(XRegExp); + } +}(function (XRegExp) { + const invalidUnicodeChars = XRegExp('[^\\p{L}\\s\\d\\-_]', 'g'); + const invalidLatinChars = /[^\w\s\d\-_]/g; + const trimRegex = /^\s+|\s+$/g; + const collapseWhitespace = /\s+/g; + const collapseDash = /-+/g; + const trimTrailingDash = /-$/g; + const trimLeadingDash = /^-/g; + const isLatin = /^[\w\d\s.,\-@]+$/; + + // http://dense13.com/blog/2009/05/03/converting-string-to-slug-javascript/ + return function slugify(str, preserveCase) { + if (!str) { + return ''; + } + str = String(str).replace(trimRegex, ''); + if (isLatin.test(str)) { + str = str.replace(invalidLatinChars, '-'); + } else { + str = XRegExp.replace(str, invalidUnicodeChars, '-'); + } + str = !preserveCase ? str.toLocaleLowerCase() : str; + str = str.replace(collapseWhitespace, '-'); + str = str.replace(collapseDash, '-'); + str = str.replace(trimTrailingDash, ''); + str = str.replace(trimLeadingDash, ''); + return str; + }; +})); diff --git a/public/src/modules/sort.js b/public/src/modules/sort.js new file mode 100644 index 0000000000..85c4dfd95f --- /dev/null +++ b/public/src/modules/sort.js @@ -0,0 +1,39 @@ +'use strict'; + + +define('sort', ['components', 'api'], function (components, api) { + const module = {}; + + module.handleSort = function (field, gotoOnSave) { + const threadSort = components.get('thread/sort'); + threadSort.find('i').removeClass('fa-check'); + const currentSetting = threadSort.find('a[data-sort="' + config[field] + '"]'); + currentSetting.find('i').addClass('fa-check'); + + $('body') + .off('click', '[component="thread/sort"] a') + .on('click', '[component="thread/sort"] a', function () { + function refresh(newSetting, params) { + config[field] = newSetting; + const qs = decodeURIComponent($.param(params)); + ajaxify.go(gotoOnSave + (qs ? '?' + qs : '')); + } + const newSetting = $(this).attr('data-sort'); + if (app.user.uid) { + const payload = { settings: {} }; + payload.settings[field] = newSetting; + api.put(`/users/${app.user.uid}/settings`, payload).then(() => { + // Yes, this is normal. If you are logged in, sort is not + // added to qs since it's saved to user settings + refresh(newSetting, utils.params()); + }); + } else { + const urlParams = utils.params(); + urlParams.sort = newSetting; + refresh(newSetting, urlParams); + } + }); + }; + + return module; +}); diff --git a/public/src/modules/storage.js b/public/src/modules/storage.js new file mode 100644 index 0000000000..b7d4c9a852 --- /dev/null +++ b/public/src/modules/storage.js @@ -0,0 +1,84 @@ +'use strict'; + +/** + * Checks localStorage and provides a fallback if it doesn't exist or is disabled + */ +define('storage', function () { + function Storage() { + this._store = {}; + this._keys = []; + } + Storage.prototype.isMock = true; + Storage.prototype.setItem = function (key, val) { + key = String(key); + if (this._keys.indexOf(key) === -1) { + this._keys.push(key); + } + this._store[key] = val; + }; + Storage.prototype.getItem = function (key) { + key = String(key); + if (this._keys.indexOf(key) === -1) { + return null; + } + + return this._store[key]; + }; + Storage.prototype.removeItem = function (key) { + key = String(key); + this._keys = this._keys.filter(function (x) { + return x !== key; + }); + this._store[key] = null; + }; + Storage.prototype.clear = function () { + this._keys = []; + this._store = {}; + }; + Storage.prototype.key = function (n) { + n = parseInt(n, 10) || 0; + return this._keys[n]; + }; + if (Object.defineProperty) { + Object.defineProperty(Storage.prototype, 'length', { + get: function () { + return this._keys.length; + }, + }); + } + + let storage; + const item = Date.now().toString(); + + try { + storage = window.localStorage; + storage.setItem(item, item); + if (storage.getItem(item) !== item) { + throw Error('localStorage behaved unexpectedly'); + } + storage.removeItem(item); + + return storage; + } catch (e) { + console.warn(e); + console.warn('localStorage failed, falling back on sessionStorage'); + + // see if sessionStorage works, and if so, return that + try { + storage = window.sessionStorage; + storage.setItem(item, item); + if (storage.getItem(item) !== item) { + throw Error('sessionStorage behaved unexpectedly'); + } + storage.removeItem(item); + + return storage; + } catch (e) { + console.warn(e); + console.warn('sessionStorage failed, falling back on memory storage'); + + // return an object implementing mock methods + return new Storage(); + } + } +}); diff --git a/public/src/modules/taskbar.js b/public/src/modules/taskbar.js new file mode 100644 index 0000000000..4d1ddcb1d1 --- /dev/null +++ b/public/src/modules/taskbar.js @@ -0,0 +1,213 @@ +'use strict'; + + +define('taskbar', ['benchpress', 'translator', 'hooks'], function (Benchpress, translator, hooks) { + const taskbar = {}; + + taskbar.init = function () { + const self = this; + + Benchpress.render('modules/taskbar', {}).then(function (html) { + self.taskbar = $(html); + self.tasklist = self.taskbar.find('ul'); + $(document.body).append(self.taskbar); + + self.taskbar.on('click', 'li', async function () { + const $btn = $(this); + const moduleName = $btn.attr('data-module'); + const uuid = $btn.attr('data-uuid'); + + const module = await app.require(moduleName); + if (!$btn.hasClass('active')) { + minimizeAll(); + module.load(uuid); + taskbar.toggleNew(uuid, false); + + taskbar.tasklist.removeClass('active'); + $btn.addClass('active'); + } else { + module.minimize(uuid); + } + return false; + }); + }); + + $(window).on('action:app.loggedOut', function () { + taskbar.closeAll(); + }); + }; + + taskbar.close = async function (moduleName, uuid) { + // Sends signal to the appropriate module's .close() fn (if present) + const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]'); + let fnName = 'close'; + + // TODO: Refactor chat module to not take uuid in close instead of by jQuery element + if (moduleName === 'chat') { + fnName = 'closeByUUID'; + } + + if (btnEl.length) { + const module = await app.require(moduleName); + if (module && typeof module[fnName] === 'function') { + module[fnName](uuid); + } + } + }; + + taskbar.closeAll = function (module) { + // module is optional + let selector = '[data-uuid]'; + + if (module) { + selector = '[data-module="' + module + '"]' + selector; + } + + taskbar.tasklist.find(selector).each(function (idx, el) { + taskbar.close(module || el.getAttribute('data-module'), el.getAttribute('data-uuid')); + }); + }; + + taskbar.discard = function (module, uuid) { + const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]'); + btnEl.remove(); + + update(); + }; + + taskbar.push = function (module, uuid, options, callback) { + callback = callback || function () {}; + const element = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]'); + + const data = { + module: module, + uuid: uuid, + options: options, + element: element, + }; + + hooks.fire('filter:taskbar.push', data); + + if (!element.length && data.module) { + createTaskbarItem(data, callback); + } else { + callback(element); + } + }; + + taskbar.get = function (module) { + const items = $('[data-module="' + module + '"]').map(function (idx, el) { + return $(el).data(); + }); + + return items; + }; + + taskbar.minimize = function (module, uuid) { + const btnEl = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]'); + btnEl.toggleClass('active', false); + }; + + taskbar.toggleNew = function (uuid, state, silent) { + const btnEl = taskbar.tasklist.find('[data-uuid="' + uuid + '"]'); + btnEl.toggleClass('new', state); + + if (!silent) { + hooks.fire('action:taskbar.toggleNew', uuid); + } + }; + + taskbar.updateActive = function (uuid) { + const tasks = taskbar.tasklist.find('li'); + tasks.removeClass('active'); + tasks.filter('[data-uuid="' + uuid + '"]').addClass('active'); + + $('[data-uuid]:not([data-module])').toggleClass('modal-unfocused', true); + $('[data-uuid="' + uuid + '"]:not([data-module])').toggleClass('modal-unfocused', false); + }; + + taskbar.isActive = function (uuid) { + const taskBtn = taskbar.tasklist.find('li[data-uuid="' + uuid + '"]'); + return taskBtn.hasClass('active'); + }; + + function update() { + const tasks = taskbar.tasklist.find('li'); + + if (tasks.length > 0) { + taskbar.taskbar.attr('data-active', '1'); + } else { + taskbar.taskbar.removeAttr('data-active'); + } + } + + function minimizeAll() { + taskbar.tasklist.find('.active').removeClass('active'); + } + + function createTaskbarItem(data, callback) { + translator.translate(data.options.title, function (taskTitle) { + const title = $('
    ').text(taskTitle || 'NodeBB Task').html(); + + const taskbarEl = $('
  • ') + .addClass(data.options.className) + .html('' + + (data.options.icon ? ' ' : '') + + '' + title + '' + + '') + .attr({ + title: title, + 'data-module': data.module, + 'data-uuid': data.uuid, + }) + .addClass(data.options.state !== undefined ? data.options.state : 'active'); + + if (!data.options.state || data.options.state === 'active') { + minimizeAll(); + } + + taskbar.tasklist.append(taskbarEl); + update(); + + data.element = taskbarEl; + + taskbarEl.data(data); + hooks.fire('action:taskbar.pushed', data); + callback(taskbarEl); + }); + } + + const processUpdate = function (element, key, value) { + switch (key) { + case 'title': + element.find('[component="taskbar/title"]').text(value); + break; + case 'icon': + element.find('i').attr('class', 'fa fa-' + value); + break; + case 'image': + element.find('a').css('background-image', value ? 'url("' + value.replace(///g, '/') + '")' : ''); + break; + case 'background-color': + element.find('a').css('background-color', value); + break; + } + }; + + taskbar.update = function (module, uuid, options) { + const element = taskbar.tasklist.find('[data-module="' + module + '"][data-uuid="' + uuid + '"]'); + if (!element.length) { + return; + } + const data = element.data(); + + Object.keys(options).forEach(function (key) { + data[key] = options[key]; + processUpdate(element, key, options[key]); + }); + + element.data(data); + }; + + return taskbar; +}); diff --git a/public/src/modules/topicList.js b/public/src/modules/topicList.js new file mode 100644 index 0000000000..6342969a4a --- /dev/null +++ b/public/src/modules/topicList.js @@ -0,0 +1,279 @@ +'use strict'; + +define('topicList', [ + 'forum/infinitescroll', + 'handleBack', + 'topicSelect', + 'categoryFilter', + 'forum/category/tools', + 'hooks', +], function (infinitescroll, handleBack, topicSelect, categoryFilter, categoryTools, hooks) { + const TopicList = {}; + let templateName = ''; + + let newTopicCount = 0; + let newPostCount = 0; + + let loadTopicsCallback; + let topicListEl; + + const scheduledTopics = []; + + $(window).on('action:ajaxify.start', function () { + TopicList.removeListeners(); + categoryTools.removeListeners(); + }); + + TopicList.init = function (template, cb) { + topicListEl = findTopicListElement(); + + templateName = template; + loadTopicsCallback = cb || loadTopicsAfter; + + categoryTools.init(); + + TopicList.watchForNewPosts(); + const states = ['watching']; + if (ajaxify.data.selectedFilter && ajaxify.data.selectedFilter.filter === 'watched') { + states.push('notwatching', 'ignoring'); + } else if (template !== 'unread') { + states.push('notwatching'); + } + + categoryFilter.init($('[component="category/dropdown"]'), { + states: states, + }); + + if (!config.usePagination) { + infinitescroll.init(TopicList.loadMoreTopics); + } + + handleBack.init(function (after, handleBackCallback) { + loadTopicsCallback(after, 1, function (data, loadCallback) { + onTopicsLoaded(templateName, data.topics, ajaxify.data.showSelect, 1, function () { + handleBackCallback(); + loadCallback(); + }); + }); + }); + + if ($('body').height() <= $(window).height() && topicListEl.children().length >= 20) { + $('#load-more-btn').show(); + } + + $('#load-more-btn').on('click', function () { + TopicList.loadMoreTopics(1); + }); + + hooks.fire('action:topics.loaded', { topics: ajaxify.data.topics }); + }; + + function findTopicListElement() { + return $('[component="category"]').filter(function (i, e) { + return !$(e).parents('[widget-area],[data-widget-area]').length; + }); + } + + TopicList.watchForNewPosts = function () { + $('#new-topics-alert').on('click', function () { + $(this).addClass('hide'); + }); + newPostCount = 0; + newTopicCount = 0; + TopicList.removeListeners(); + socket.on('event:new_topic', onNewTopic); + socket.on('event:new_post', onNewPost); + }; + + TopicList.removeListeners = function () { + socket.removeListener('event:new_topic', onNewTopic); + socket.removeListener('event:new_post', onNewPost); + }; + + function onNewTopic(data) { + const d = ajaxify.data; + + const categories = d.selectedCids && + d.selectedCids.length && + d.selectedCids.indexOf(parseInt(data.cid, 10)) === -1; + const filterWatched = d.selectedFilter && + d.selectedFilter.filter === 'watched'; + const category = d.template.category && + parseInt(d.cid, 10) !== parseInt(data.cid, 10); + + const preventAlert = !!(categories || filterWatched || category || scheduledTopics.includes(data.tid)); + hooks.fire('filter:topicList.onNewTopic', { topic: data, preventAlert }).then((result) => { + if (result.preventAlert) { + return; + } + + if (data.scheduled && data.tid) { + scheduledTopics.push(data.tid); + } + newTopicCount += 1; + updateAlertText(); + }); + } + + function onNewPost(data) { + const post = data.posts[0]; + if (!post || !post.topic || post.topic.isFollowing) { + return; + } + + const d = ajaxify.data; + + const isMain = parseInt(post.topic.mainPid, 10) === parseInt(post.pid, 10); + const categories = d.selectedCids && + d.selectedCids.length && + d.selectedCids.indexOf(parseInt(post.topic.cid, 10)) === -1; + const filterNew = d.selectedFilter && + d.selectedFilter.filter === 'new'; + const filterWatched = d.selectedFilter && + d.selectedFilter.filter === 'watched' && + !post.topic.isFollowing; + const category = d.template.category && + parseInt(d.cid, 10) !== parseInt(post.topic.cid, 10); + + const preventAlert = !!(isMain || categories || filterNew || filterWatched || category); + hooks.fire('filter:topicList.onNewPost', { post, preventAlert }).then((result) => { + if (result.preventAlert) { + return; + } + + newPostCount += 1; + updateAlertText(); + }); + } + + function updateAlertText() { + let text = ''; + + if (newTopicCount === 0) { + if (newPostCount === 1) { + text = '[[recent:there-is-a-new-post]]'; + } else if (newPostCount > 1) { + text = '[[recent:there-are-new-posts, ' + newPostCount + ']]'; + } + } else if (newTopicCount === 1) { + if (newPostCount === 0) { + text = '[[recent:there-is-a-new-topic]]'; + } else if (newPostCount === 1) { + text = '[[recent:there-is-a-new-topic-and-a-new-post]]'; + } else if (newPostCount > 1) { + text = '[[recent:there-is-a-new-topic-and-new-posts, ' + newPostCount + ']]'; + } + } else if (newTopicCount > 1) { + if (newPostCount === 0) { + text = '[[recent:there-are-new-topics, ' + newTopicCount + ']]'; + } else if (newPostCount === 1) { + text = '[[recent:there-are-new-topics-and-a-new-post, ' + newTopicCount + ']]'; + } else if (newPostCount > 1) { + text = '[[recent:there-are-new-topics-and-new-posts, ' + newTopicCount + ', ' + newPostCount + ']]'; + } + } + + text += ' [[recent:click-here-to-reload]]'; + + $('#new-topics-alert').translateText(text).removeClass('hide').fadeIn('slow'); + $('#category-no-topics').addClass('hide'); + } + + TopicList.loadMoreTopics = function (direction) { + if (!topicListEl.length || !topicListEl.children().length) { + return; + } + const topics = topicListEl.find('[component="category/topic"]'); + const afterEl = direction > 0 ? topics.last() : topics.first(); + const after = (parseInt(afterEl.attr('data-index'), 10) || 0) + (direction > 0 ? 1 : 0); + + if (!utils.isNumber(after) || (after === 0 && topicListEl.find('[component="category/topic"][data-index="0"]').length)) { + return; + } + + loadTopicsCallback(after, direction, function (data, done) { + onTopicsLoaded(templateName, data.topics, ajaxify.data.showSelect, direction, done); + }); + }; + + function calculateNextPage(after, direction) { + return Math.floor(after / config.topicsPerPage) + (direction > 0 ? 1 : 0); + } + + function loadTopicsAfter(after, direction, callback) { + callback = callback || function () {}; + const query = utils.params(); + query.page = calculateNextPage(after, direction); + infinitescroll.loadMoreXhr(query, callback); + } + + function filterTopicsOnDom(topics) { + return topics.filter(function (topic) { + return !topicListEl.find('[component="category/topic"][data-tid="' + topic.tid + '"]').length; + }); + } + + function onTopicsLoaded(templateName, topics, showSelect, direction, callback) { + if (!topics || !topics.length) { + $('#load-more-btn').hide(); + return callback(); + } + topics = filterTopicsOnDom(topics); + + if (!topics.length) { + $('#load-more-btn').hide(); + return callback(); + } + + let after; + let before; + const topicEls = topicListEl.find('[component="category/topic"]'); + + if (direction > 0 && topics.length) { + after = topicEls.last(); + } else if (direction < 0 && topics.length) { + before = topicEls.first(); + } + + const tplData = { + topics: topics, + showSelect: showSelect, + template: { + name: templateName, + }, + }; + tplData.template[templateName] = true; + + hooks.fire('action:topics.loading', { topics: topics, after: after, before: before }); + + app.parseAndTranslate(templateName, 'topics', tplData, function (html) { + topicListEl.removeClass('hidden'); + $('#category-no-topics').remove(); + + if (after && after.length) { + html.insertAfter(after); + } else if (before && before.length) { + const height = $(document).height(); + const scrollTop = $(window).scrollTop(); + + html.insertBefore(before); + + $(window).scrollTop(scrollTop + ($(document).height() - height)); + } else { + topicListEl.append(html); + } + + if (!topicSelect.getSelectedTids().length) { + infinitescroll.removeExtra(topicListEl.find('[component="category/topic"]'), direction, Math.max(60, config.topicsPerPage * 3)); + } + + html.find('.timeago').timeago(); + app.createUserTooltips(html); + utils.makeNumbersHumanReadable(html.find('.human-readable-number')); + hooks.fire('action:topics.loaded', { topics: topics, template: templateName }); + callback(); + }); + } + + return TopicList; +}); diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js new file mode 100644 index 0000000000..2767fea668 --- /dev/null +++ b/public/src/modules/topicSelect.js @@ -0,0 +1,88 @@ +'use strict'; + + +define('topicSelect', ['components'], function (components) { + const TopicSelect = {}; + let lastSelected; + + let topicsContainer; + + TopicSelect.init = function (onSelect) { + topicsContainer = $('[component="category"]'); + topicsContainer.on('selectstart', '[component="topic/select"]', function (ev) { + ev.preventDefault(); + }); + + topicsContainer.on('click', '[component="topic/select"]', function (ev) { + const select = $(this); + + if (ev.shiftKey) { + selectRange($(this).parents('[component="category/topic"]').attr('data-tid')); + lastSelected = select; + return false; + } + + const isSelected = select.parents('[data-tid]').hasClass('selected'); + toggleSelect(select, !isSelected); + lastSelected = select; + if (typeof onSelect === 'function') { + onSelect(); + } + }); + }; + + function toggleSelect(select, isSelected) { + select.toggleClass('fa-check-square-o', isSelected); + select.toggleClass('fa-square-o', !isSelected); + select.parents('[component="category/topic"]').toggleClass('selected', isSelected); + } + + TopicSelect.getSelectedTids = function () { + const tids = []; + if (!topicsContainer) { + return tids; + } + topicsContainer.find('[component="category/topic"].selected').each(function () { + tids.push($(this).attr('data-tid')); + }); + return tids; + }; + + TopicSelect.unselectAll = function () { + if (topicsContainer) { + topicsContainer.find('[component="category/topic"].selected').removeClass('selected'); + topicsContainer.find('[component="topic/select"]').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true); + } + }; + + function selectRange(clickedTid) { + if (!lastSelected) { + lastSelected = $('[component="category/topic"]').first().find('[component="topic/select"]'); + } + + const isClickedSelected = components.get('category/topic', 'tid', clickedTid).hasClass('selected'); + + const clickedIndex = getIndex(clickedTid); + const lastIndex = getIndex(lastSelected.parents('[component="category/topic"]').attr('data-tid')); + selectIndexRange(clickedIndex, lastIndex, !isClickedSelected); + } + + function selectIndexRange(start, end, isSelected) { + if (start > end) { + const tmp = start; + start = end; + end = tmp; + } + + for (let i = start; i <= end; i += 1) { + const topic = $('[component="category/topic"]').eq(i); + toggleSelect(topic.find('[component="topic/select"]'), isSelected); + } + } + + function getIndex(tid) { + return components.get('category/topic', 'tid', tid).index('[component="category/topic"]'); + } + + return TopicSelect; +}); diff --git a/public/src/modules/topicThumbs.js b/public/src/modules/topicThumbs.js new file mode 100644 index 0000000000..761bf5d3a1 --- /dev/null +++ b/public/src/modules/topicThumbs.js @@ -0,0 +1,130 @@ +'use strict'; + +define('topicThumbs', [ + 'api', 'bootbox', 'alerts', 'uploader', 'benchpress', 'translator', 'jquery-ui/widgets/sortable', +], function (api, bootbox, alerts, uploader, Benchpress, translator) { + const Thumbs = {}; + + Thumbs.get = id => api.get(`/topics/${id}/thumbs`, {}); + + Thumbs.getByPid = pid => api.get(`/posts/${pid}`, {}).then(post => Thumbs.get(post.tid)); + + Thumbs.delete = (id, path) => api.del(`/topics/${id}/thumbs`, { + path: path, + }); + + Thumbs.deleteAll = (id) => { + Thumbs.get(id).then((thumbs) => { + Promise.all(thumbs.map(thumb => Thumbs.delete(id, thumb.url))); + }); + }; + + Thumbs.upload = id => new Promise((resolve) => { + uploader.show({ + title: '[[topic:composer.thumb_title]]', + method: 'put', + route: config.relative_path + `/api/v3/topics/${id}/thumbs`, + }, function (url) { + resolve(url); + }); + }); + + Thumbs.modal = {}; + + Thumbs.modal.open = function (payload) { + const { id, pid } = payload; + let { modal } = payload; + let numThumbs; + + return new Promise((resolve) => { + Promise.all([ + Thumbs.get(id), + pid ? Thumbs.getByPid(pid) : [], + ]).then(results => new Promise((resolve) => { + const thumbs = results.reduce((memo, cur) => memo.concat(cur)); + numThumbs = thumbs.length; + + resolve(thumbs); + })).then(thumbs => Benchpress.render('modals/topic-thumbs', { thumbs })).then((html) => { + if (modal) { + translator.translate(html, function (translated) { + modal.find('.bootbox-body').html(translated); + Thumbs.modal.handleSort({ modal, numThumbs }); + }); + } else { + modal = bootbox.dialog({ + title: '[[modules:thumbs.modal.title]]', + message: html, + buttons: { + add: { + label: ' [[modules:thumbs.modal.add]]', + className: 'btn-success', + callback: () => { + Thumbs.upload(id).then(() => { + Thumbs.modal.open({ ...payload, modal }); + require(['composer'], (composer) => { + composer.updateThumbCount(id, $(`[component="composer"][data-uuid="${id}"]`)); + resolve(); + }); + }); + return false; + }, + }, + close: { + label: '[[global:close]]', + className: 'btn-primary', + }, + }, + }); + Thumbs.modal.handleDelete({ ...payload, modal }); + Thumbs.modal.handleSort({ modal, numThumbs }); + } + }); + }); + }; + + Thumbs.modal.handleDelete = (payload) => { + const modalEl = payload.modal.get(0); + + modalEl.addEventListener('click', (ev) => { + if (ev.target.closest('button[data-action="remove"]')) { + bootbox.confirm('[[modules:thumbs.modal.confirm-remove]]', (ok) => { + if (!ok) { + return; + } + + const id = ev.target.closest('.media[data-id]').getAttribute('data-id'); + const path = ev.target.closest('.media[data-path]').getAttribute('data-path'); + api.del(`/topics/${id}/thumbs`, { + path: path, + }).then(() => { + Thumbs.modal.open(payload); + }).catch(alerts.error); + }); + } + }); + }; + + Thumbs.modal.handleSort = ({ modal, numThumbs }) => { + if (numThumbs > 1) { + const selectorEl = modal.find('.topic-thumbs-modal'); + selectorEl.sortable({ + items: '[data-id]', + }); + selectorEl.on('sortupdate', Thumbs.modal.handleSortChange); + } + }; + + Thumbs.modal.handleSortChange = (ev, ui) => { + const items = ui.item.get(0).parentNode.querySelectorAll('[data-id]'); + Array.from(items).forEach((el, order) => { + const id = el.getAttribute('data-id'); + let path = el.getAttribute('data-path'); + path = path.replace(new RegExp(`^${config.upload_url}`), ''); + + api.put(`/topics/${id}/thumbs/order`, { path, order }).catch(alerts.error); + }); + }; + + return Thumbs; +}); diff --git a/public/src/modules/translator.common.js b/public/src/modules/translator.common.js new file mode 100644 index 0000000000..f934861211 --- /dev/null +++ b/public/src/modules/translator.common.js @@ -0,0 +1,633 @@ +'use strict'; + +module.exports = function (utils, load, warn) { + const assign = Object.assign || jQuery.extend; + + function escapeHTML(str) { + return utils.escapeHTML(utils.decodeHTMLEntities( + String(str) + .replace(/[\s\xa0]+/g, ' ') + .replace(/^\s+|\s+$/g, '') + )); + } + + const Translator = (function () { + /** + * Construct a new Translator object + * @param {string} language - Language code for this translator instance + * @exports translator.Translator + */ + function Translator(language) { + const self = this; + + if (!language) { + throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : '')); + } + + self.modules = Object.keys(Translator.moduleFactories).map(function (namespace) { + const factory = Translator.moduleFactories[namespace]; + return [namespace, factory(language)]; + }).reduce(function (prev, elem) { + const namespace = elem[0]; + const module = elem[1]; + prev[namespace] = module; + + return prev; + }, {}); + + self.lang = language; + self.translations = {}; + } + + Translator.prototype.load = load; + + /** + * Parse the translation instructions into the language of the Translator instance + * @param {string} str - Source string + * @returns {Promise} + */ + Translator.prototype.translate = function translate(str) { + // regex for valid text in namespace / key + const validText = 'a-zA-Z0-9\\-_.\\/'; + const validTextRegex = new RegExp('[' + validText + ']'); + const invalidTextRegex = new RegExp('[^' + validText + '\\]]'); + + // current cursor position + let cursor = 0; + // last break of the input string + let lastBreak = 0; + // length of the input string + const len = str.length; + // array to hold the promises for the translations + // and the strings of untranslated text in between + const toTranslate = []; + + // to store the state of if we're currently in a top-level token for later + let inToken = false; + + // split a translator string into an array of tokens + // but don't split by commas inside other translator strings + function split(text) { + const len = text.length; + const arr = []; + let i = 0; + let brk = 0; + let level = 0; + + while (i + 2 <= len) { + if (text[i] === '[' && text[i + 1] === '[') { + level += 1; + i += 1; + } else if (text[i] === ']' && text[i + 1] === ']') { + level -= 1; + i += 1; + } else if (level === 0 && text[i] === ',' && text[i - 1] !== '\\') { + arr.push(text.slice(brk, i).trim()); + i += 1; + brk = i; + } + i += 1; + } + arr.push(text.slice(brk, i + 1).trim()); + return arr; + } + + // move to the first [[ + cursor = str.indexOf('[[', cursor); + + // the loooop, we'll go to where the cursor + // is equal to the length of the string since + // slice doesn't include the ending index + while (cursor + 2 <= len && cursor !== -1) { + // split the string from the last break + // to the character before the cursor + // add that to the result array + toTranslate.push(str.slice(lastBreak, cursor)); + // set the cursor position past the beginning + // brackets of the translation string + cursor += 2; + // set the last break to our current + // spot since we just broke the string + lastBreak = cursor; + // we're in a token now + inToken = true; + + // the current level of nesting of the translation strings + let level = 0; + let char0; + let char1; + // validating the current string is actually a translation + let textBeforeColonFound = false; + let colonFound = false; + let textAfterColonFound = false; + let commaAfterNameFound = false; + + while (cursor + 2 <= len) { + char0 = str[cursor]; + char1 = str[cursor + 1]; + // found some text after the double bracket, + // so this is probably a translation string + if (!textBeforeColonFound && validTextRegex.test(char0)) { + textBeforeColonFound = true; + cursor += 1; + // found a colon, so this is probably a translation string + } else if (textBeforeColonFound && !colonFound && char0 === ':') { + colonFound = true; + cursor += 1; + // found some text after the colon, + // so this is probably a translation string + } else if (colonFound && !textAfterColonFound && validTextRegex.test(char0)) { + textAfterColonFound = true; + cursor += 1; + } else if (textAfterColonFound && !commaAfterNameFound && char0 === ',') { + commaAfterNameFound = true; + cursor += 1; + // a space or comma was found before the name + // this isn't a translation string, so back out + } else if (!(textBeforeColonFound && colonFound && textAfterColonFound && commaAfterNameFound) && + invalidTextRegex.test(char0)) { + cursor += 1; + lastBreak -= 2; + // no longer in a token + inToken = false; + if (level > 0) { + level -= 1; + } else { + break; + } + // if we're at the beginning of another translation string, + // we're nested, so add to our level + } else if (char0 === '[' && char1 === '[') { + level += 1; + cursor += 2; + // if we're at the end of a translation string + } else if (char0 === ']' && char1 === ']') { + // if we're at the base level, then this is the end + if (level === 0) { + // so grab the name and args + const currentSlice = str.slice(lastBreak, cursor); + const result = split(currentSlice); + const name = result[0]; + const args = result.slice(1); + + // make a backup based on the raw string of the token + // if there are arguments to the token + let backup = ''; + if (args && args.length) { + backup = this.translate(currentSlice); + } + // add the translation promise to the array + toTranslate.push(this.translateKey(name, args, backup)); + // skip past the ending brackets + cursor += 2; + // set this as our last break + lastBreak = cursor; + // and we're no longer in a translation string, + // so continue with the main loop + inToken = false; + break; + } + // otherwise we lower the level + level -= 1; + // and skip past the ending brackets + cursor += 2; + } else { + // otherwise just move to the next character + cursor += 1; + } + } + + // skip to the next [[ + cursor = str.indexOf('[[', cursor); + } + + // ending string of source + let last = str.slice(lastBreak); + + // if we were mid-token, treat it as invalid + if (inToken) { + last = this.translate(last); + } + + // add the remaining text after the last translation string + toTranslate.push(last); + + // and return a promise for the concatenated translated string + return Promise.all(toTranslate).then(function (translated) { + return translated.join(''); + }); + }; + + /** + * Translates a specific key and array of arguments + * @param {string} name - Translation key (ex. 'global:home') + * @param {string[]} args - Arguments for `%1`, `%2`, etc + * @param {string|Promise} backup - Text to use in case the key can't be found + * @returns {Promise} + */ + Translator.prototype.translateKey = function translateKey(name, args, backup) { + const self = this; + + const result = name.split(':', 2); + const namespace = result[0]; + const key = result[1]; + + if (self.modules[namespace]) { + return Promise.resolve(self.modules[namespace](key, args)); + } + + if (namespace && result.length === 1) { + return Promise.resolve('[[' + namespace + ']]'); + } + + if (namespace && !key) { + warn('Missing key in translation token "' + name + '" for language "' + self.lang + '"'); + return Promise.resolve('[[' + namespace + ']]'); + } + + const translation = this.getTranslation(namespace, key); + return translation.then(function (translated) { + // check if the translation is missing first + if (!translated) { + warn('Missing translation "' + name + '" for language "' + self.lang + '"'); + return backup || key; + } + + const argsToTranslate = args.map(function (arg) { + return self.translate(escapeHTML(arg)); + }); + + return Promise.all(argsToTranslate).then(function (translatedArgs) { + let out = translated; + translatedArgs.forEach(function (arg, i) { + let escaped = arg.replace(/%(?=\d)/g, '%').replace(/\\,/g, ','); + // fix double escaped translation keys, see https://github.com/NodeBB/NodeBB/issues/9206 + escaped = escaped.replace(/&lsqb;/g, '[') + .replace(/&rsqb;/g, ']'); + out = out.replace(new RegExp('%' + (i + 1), 'g'), escaped); + }); + return out; + }); + }); + }; + + /** + * Load translation file (or use a cached version), and optionally return the translation of a certain key + * @param {string} namespace - The file name of the translation namespace + * @param {string} [key] - The key of the specific translation to getJSON + * @returns {Promise<{ [key: string]: string } | string>} + */ + Translator.prototype.getTranslation = function getTranslation(namespace, key) { + let translation; + if (!namespace) { + warn('[translator] Parameter `namespace` is ' + namespace + (namespace === '' ? '(empty string)' : '')); + translation = Promise.resolve({}); + } else { + this.translations[namespace] = this.translations[namespace] || + this.load(this.lang, namespace).catch(function () { return {}; }); + translation = this.translations[namespace]; + } + + if (key) { + return translation.then(function (x) { + if (typeof x[key] === 'string') return x[key]; + const keyParts = key.split('.'); + for (let i = 0; i <= keyParts.length; i++) { + if (i === keyParts.length) { + // default to trying to find key with the same name as parent or equal to empty string + return x[keyParts[i - 1]] !== undefined ? x[keyParts[i - 1]] : x['']; + } + switch (typeof x[keyParts[i]]) { + case 'object': + x = x[keyParts[i]]; + break; + case 'string': + if (i === keyParts.length - 1) { + return x[keyParts[i]]; + } + + return false; + + default: + return false; + } + } + }); + } + return translation; + }; + + /** + * @param {Node} node + * @returns {Node[]} + */ + function descendantTextNodes(node) { + const textNodes = []; + + function helper(node) { + if (node.nodeType === 3) { + textNodes.push(node); + } else { + for (let i = 0, c = node.childNodes, l = c.length; i < l; i += 1) { + helper(c[i]); + } + } + } + + helper(node); + return textNodes; + } + + /** + * Recursively translate a DOM element in place + * @param {Element} element - Root element to translate + * @param {string[]} [attributes] - Array of node attributes to translate + * @returns {Promise} + */ + Translator.prototype.translateInPlace = function translateInPlace(element, attributes) { + attributes = attributes || ['placeholder', 'title']; + + const nodes = descendantTextNodes(element); + const text = nodes.map(function (node) { + return utils.escapeHTML(node.nodeValue); + }).join(' || '); + + const attrNodes = attributes.reduce(function (prev, attr) { + const tuples = Array.prototype.map.call(element.querySelectorAll('[' + attr + '*="[["]'), function (el) { + return [attr, el]; + }); + return prev.concat(tuples); + }, []); + const attrText = attrNodes.map(function (node) { + return node[1].getAttribute(node[0]); + }).join(' || '); + + return Promise.all([ + this.translate(text), + this.translate(attrText), + ]).then(function (ref) { + const translated = ref[0]; + const translatedAttrs = ref[1]; + if (translated) { + translated.split(' || ').forEach(function (html, i) { + $(nodes[i]).replaceWith(html); + }); + } + if (translatedAttrs) { + translatedAttrs.split(' || ').forEach(function (text, i) { + attrNodes[i][1].setAttribute(attrNodes[i][0], text); + }); + } + }); + }; + + /** + * Get the language of the current environment, falling back to defaults + * @returns {string} + */ + Translator.getLanguage = function getLanguage() { + return utils.getLanguage(); + }; + + /** + * Create and cache a new Translator instance, or return a cached one + * @param {string} [language] - ('en-GB') Language string + * @returns {Translator} + */ + Translator.create = function create(language) { + if (!language) { + language = Translator.getLanguage(); + } + + Translator.cache[language] = Translator.cache[language] || new Translator(language); + + return Translator.cache[language]; + }; + + Translator.cache = {}; + + /** + * Register a custom module to handle translations + * @param {string} namespace - Namespace to handle translation for + * @param {Function} factory - Function to return the translation function for this namespace + */ + Translator.registerModule = function registerModule(namespace, factory) { + Translator.moduleFactories[namespace] = factory; + + Object.keys(Translator.cache).forEach(function (key) { + const translator = Translator.cache[key]; + translator.modules[namespace] = factory(translator.lang); + }); + }; + + Translator.moduleFactories = {}; + + /** + * Remove the translator patterns from text + * @param {string} text + * @returns {string} + */ + Translator.removePatterns = function removePatterns(text) { + const len = text.length; + let cursor = 0; + let lastBreak = 0; + let level = 0; + let out = ''; + let sub; + + while (cursor < len) { + sub = text.slice(cursor, cursor + 2); + if (sub === '[[') { + if (level === 0) { + out += text.slice(lastBreak, cursor); + } + level += 1; + cursor += 2; + } else if (sub === ']]') { + level -= 1; + cursor += 2; + if (level === 0) { + lastBreak = cursor; + } + } else { + cursor += 1; + } + } + out += text.slice(lastBreak, cursor); + return out; + }; + + /** + * Escape translator patterns in text + * @param {string} text + * @returns {string} + */ + Translator.escape = function escape(text) { + return typeof text === 'string' ? text.replace(/\[\[/g, '[[').replace(/\]\]/g, ']]') : text; + }; + + /** + * Unescape escaped translator patterns in text + * @param {string} text + * @returns {string} + */ + Translator.unescape = function unescape(text) { + return typeof text === 'string' ? + text.replace(/[/g, '[').replace(/\\\[/g, '[') + .replace(/]/g, ']').replace(/\\\]/g, ']') : + text; + }; + + /** + * Construct a translator pattern + * @param {string} name - Translation name + * @param {...string} arg - Optional argument for the pattern + */ + Translator.compile = function compile() { + const args = Array.prototype.slice.call(arguments, 0).map(function (text) { + // escape commas and percent signs in arguments + return String(text).replace(/%/g, '%').replace(/,/g, ','); + }); + + return '[[' + args.join(', ') + ']]'; + }; + + return Translator; + }()); + + /** + * @exports translator + */ + const adaptor = { + /** + * The Translator class + */ + Translator: Translator, + + compile: Translator.compile, + escape: Translator.escape, + unescape: Translator.unescape, + getLanguage: Translator.getLanguage, + + flush: function () { + Object.keys(Translator.cache).forEach(function (code) { + Translator.cache[code].translations = {}; + }); + }, + + flushNamespace: function (namespace) { + Object.keys(Translator.cache).forEach(function (code) { + if (Translator.cache[code] && + Translator.cache[code].translations && + Translator.cache[code].translations[namespace] + ) { + Translator.cache[code].translations[namespace] = null; + } + }); + }, + + + /** + * Legacy translator function for backwards compatibility + */ + translate: function translate(text, language, callback) { + // TODO: deprecate? + + let cb = callback; + let lang = language; + if (typeof language === 'function') { + cb = language; + lang = null; + } + + if (!(typeof text === 'string' || text instanceof String) || text === '') { + if (cb) { + return setTimeout(cb, 0, ''); + } + return ''; + } + + return Translator.create(lang).translate(text).then(function (output) { + if (cb) { + setTimeout(cb, 0, output); + } + return output; + }, function (err) { + warn('Translation failed: ' + err.stack); + }); + }, + translateKeys: async function (keys, language, callback) { + let cb = callback; + let lang = language; + if (typeof language === 'function') { + cb = language; + lang = null; + } + const translations = await Promise.all(keys.map(key => adaptor.translate(key, lang))); + if (typeof cb === 'function') { + return setTimeout(cb, 0, translations); + } + return translations; + }, + + /** + * Add translations to the cache + */ + addTranslation: function addTranslation(language, namespace, translation) { + Translator.create(language).getTranslation(namespace).then(function (translations) { + assign(translations, translation); + }); + }, + + /** + * Get the translations object + */ + getTranslations: function getTranslations(language, namespace, callback) { + callback = callback || function () {}; + Translator.create(language).getTranslation(namespace).then(callback); + }, + + /** + * Alias of getTranslations + */ + load: function load(language, namespace, callback) { + adaptor.getTranslations(language, namespace, callback); + }, + + toggleTimeagoShorthand: function toggleTimeagoShorthand(callback) { + /* eslint "prefer-object-spread": "off" */ + function toggle() { + const tmp = assign({}, jQuery.timeago.settings.strings); + jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort); + adaptor.timeagoShort = assign({}, tmp); + if (typeof callback === 'function') { + callback(); + } + } + + if (!adaptor.timeagoShort) { + let languageCode = utils.userLangToTimeagoCode(config.userLang); + if (!config.timeagoCodes.includes(languageCode + '-short')) { + languageCode = 'en'; + } + + const originalSettings = assign({}, jQuery.timeago.settings.strings); + adaptor.switchTimeagoLanguage(languageCode + '-short', function () { + adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings); + jQuery.timeago.settings.strings = assign({}, originalSettings); + toggle(); + }); + } else { + toggle(); + } + }, + + switchTimeagoLanguage: function switchTimeagoLanguage(langCode, callback) { + // Delete the cached shorthand strings if present + delete adaptor.timeagoShort; + import(/* webpackChunkName: "timeago/[request]" */ 'timeago/locales/jquery.timeago.' + langCode).then(callback); + }, + }; + + return adaptor; +}; diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js new file mode 100644 index 0000000000..c5e48c84cc --- /dev/null +++ b/public/src/modules/translator.js @@ -0,0 +1,25 @@ +'use strict'; + +const factory = require('./translator.common'); + +define('translator', ['jquery', 'utils'], function (jQuery, utils) { + function loadClient(language, namespace) { + return new Promise(function (resolve, reject) { + jQuery.getJSON([config.asset_base_url, 'language', language, namespace].join('/') + '.json?' + config['cache-buster'], function (data) { + const payload = { + language: language, + namespace: namespace, + data: data, + }; + require(['hooks'], function (hooks) { + hooks.fire('action:translator.loadClient', payload); + resolve(payload.promise ? Promise.resolve(payload.promise) : data); + }); + }).fail(function (jqxhr, textStatus, error) { + reject(new Error(textStatus + ', ' + error)); + }); + }); + } + const warn = function () { console.warn.apply(console, arguments); }; + return factory(utils, loadClient, warn); +}); diff --git a/public/src/modules/uploadHelpers.js b/public/src/modules/uploadHelpers.js new file mode 100644 index 0000000000..e56f589768 --- /dev/null +++ b/public/src/modules/uploadHelpers.js @@ -0,0 +1,199 @@ +'use strict'; + + +define('uploadHelpers', ['alerts'], function (alerts) { + const uploadHelpers = {}; + + uploadHelpers.init = function (options) { + const formEl = options.uploadFormEl; + if (!formEl.length) { + return; + } + formEl.attr('action', config.relative_path + options.route); + + if (options.dragDropAreaEl) { + uploadHelpers.handleDragDrop({ + container: options.dragDropAreaEl, + callback: function (upload) { + uploadHelpers.ajaxSubmit({ + uploadForm: formEl, + upload: upload, + callback: options.callback, + }); + }, + }); + } + + if (options.pasteEl) { + uploadHelpers.handlePaste({ + container: options.pasteEl, + callback: function (upload) { + uploadHelpers.ajaxSubmit({ + uploadForm: formEl, + upload: upload, + callback: options.callback, + }); + }, + }); + } + }; + + uploadHelpers.handleDragDrop = function (options) { + let draggingDocument = false; + const postContainer = options.container; + const drop = options.container.find('.imagedrop'); + + postContainer.on('dragenter', function onDragEnter() { + if (draggingDocument) { + return; + } + drop.css('top', '0px'); + drop.css('height', postContainer.height() + 'px'); + drop.css('line-height', postContainer.height() + 'px'); + drop.show(); + + drop.on('dragleave', function () { + drop.hide(); + drop.off('dragleave'); + }); + }); + + drop.on('drop', function onDragDrop(e) { + e.preventDefault(); + const files = e.originalEvent.dataTransfer.files; + + if (files.length) { + let formData; + if (window.FormData) { + formData = new FormData(); + for (var i = 0; i < files.length; ++i) { + formData.append('files[]', files[i], files[i].name); + } + } + options.callback({ + files: files, + formData: formData, + }); + } + + drop.hide(); + return false; + }); + + function cancel(e) { + e.preventDefault(); + return false; + } + + $(document) + .off('dragstart') + .on('dragstart', function () { + draggingDocument = true; + }) + .off('dragend') + .on('dragend, mouseup', function () { + draggingDocument = false; + }); + + drop.on('dragover', cancel); + drop.on('dragenter', cancel); + }; + + uploadHelpers.handlePaste = function (options) { + const container = options.container; + container.on('paste', function (event) { + const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items; + const files = []; + const fileNames = []; + let formData = null; + if (window.FormData) { + formData = new FormData(); + } + [].forEach.call(items, function (item) { + const file = item.getAsFile(); + if (file) { + const fileName = utils.generateUUID() + '-' + file.name; + if (formData) { + formData.append('files[]', file, fileName); + } + files.push(file); + fileNames.push(fileName); + } + }); + + if (files.length) { + options.callback({ + files: files, + fileNames: fileNames, + formData: formData, + }); + } + }); + }; + + uploadHelpers.ajaxSubmit = function (options) { + const files = [...options.upload.files]; + + for (let i = 0; i < files.length; ++i) { + const isImage = files[i].type.match(/image./); + if ((isImage && !app.user.privileges['upload:post:image']) || (!isImage && !app.user.privileges['upload:post:file'])) { + return alerts.error('[[error:no-privileges]]'); + } + if (files[i].size > parseInt(config.maximumFileSize, 10) * 1024) { + options.uploadForm[0].reset(); + return alerts.error('[[error:file-too-big, ' + config.maximumFileSize + ']]'); + } + } + const alert_id = Date.now(); + options.uploadForm.off('submit').on('submit', function () { + $(this).ajaxSubmit({ + headers: { + 'x-csrf-token': config.csrf_token, + }, + resetForm: true, + clearForm: true, + formData: options.upload.formData, + error: function (xhr) { + let errorMsg = (xhr.responseJSON && + (xhr.responseJSON.error || (xhr.responseJSON.status && xhr.responseJSON.status.message))) || + '[[error:parse-error]]'; + + if (xhr && xhr.status === 413) { + errorMsg = xhr.statusText || 'Request Entity Too Large'; + } + alerts.error(errorMsg); + alerts.remove(alert_id); + }, + + uploadProgress: function (event, position, total, percent) { + alerts.alert({ + alert_id: alert_id, + message: '[[modules:composer.uploading, ' + percent + '%]]', + }); + }, + + success: function (res) { + const uploads = res.response.images; + if (uploads && uploads.length) { + for (var i = 0; i < uploads.length; ++i) { + uploads[i].filename = files[i].name; + uploads[i].isImage = /image./.test(files[i].type); + } + } + options.callback(uploads); + }, + + complete: function () { + options.uploadForm[0].reset(); + setTimeout(alerts.remove, 100, alert_id); + }, + }); + + return false; + }); + + options.uploadForm.submit(); + }; + + return uploadHelpers; +}); diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js new file mode 100644 index 0000000000..62210a2d1b --- /dev/null +++ b/public/src/modules/uploader.js @@ -0,0 +1,118 @@ +'use strict'; + + +define('uploader', ['jquery-form'], function () { + const module = {}; + + module.show = function (data, callback) { + const fileSize = data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? parseInt(data.fileSize, 10) : false; + app.parseAndTranslate('partials/modals/upload_file_modal', { + showHelp: data.hasOwnProperty('showHelp') && data.showHelp !== undefined ? data.showHelp : true, + fileSize: fileSize, + title: data.title || '[[global:upload_file]]', + description: data.description || '', + button: data.button || '[[global:upload]]', + accept: data.accept ? data.accept.replace(/,/g, ', ') : '', + }, function (uploadModal) { + uploadModal.modal('show'); + uploadModal.on('hidden.bs.modal', function () { + uploadModal.remove(); + }); + + const uploadForm = uploadModal.find('#uploadForm'); + uploadForm.attr('action', data.route); + uploadForm.find('#params').val(JSON.stringify(data.params)); + + uploadModal.find('#fileUploadSubmitBtn').on('click', function () { + $(this).addClass('disabled'); + uploadForm.submit(); + }); + + uploadForm.submit(function () { + onSubmit(uploadModal, fileSize, callback); + return false; + }); + }); + }; + + module.hideAlerts = function (modal) { + $(modal).find('#alert-status, #alert-success, #alert-error, #upload-progress-box').addClass('hide'); + }; + + function onSubmit(uploadModal, fileSize, callback) { + showAlert(uploadModal, 'status', '[[uploads:uploading-file]]'); + + uploadModal.find('#upload-progress-bar').css('width', '0%'); + uploadModal.find('#upload-progress-box').show().removeClass('hide'); + + const fileInput = uploadModal.find('#fileInput'); + if (!fileInput.val()) { + return showAlert(uploadModal, 'error', '[[uploads:select-file-to-upload]]'); + } + if (!hasValidFileSize(fileInput[0], fileSize)) { + return showAlert(uploadModal, 'error', '[[error:file-too-big, ' + fileSize + ']]'); + } + + module.ajaxSubmit(uploadModal, callback); + } + + function showAlert(uploadModal, type, message) { + module.hideAlerts(uploadModal); + if (type === 'error') { + uploadModal.find('#fileUploadSubmitBtn').removeClass('disabled'); + } + uploadModal.find('#alert-' + type).translateText(message).removeClass('hide'); + } + + module.ajaxSubmit = function (uploadModal, callback) { + const uploadForm = uploadModal.find('#uploadForm'); + uploadForm.ajaxSubmit({ + headers: { + 'x-csrf-token': config.csrf_token, + }, + error: function (xhr) { + xhr = maybeParse(xhr); + showAlert(uploadModal, 'error', xhr.responseJSON ? (xhr.responseJSON.error || xhr.statusText) : 'error uploading, code : ' + xhr.status); + }, + uploadProgress: function (event, position, total, percent) { + uploadModal.find('#upload-progress-bar').css('width', percent + '%'); + }, + success: function (response) { + let images = maybeParse(response); + + // Appropriately handle v3 API responses + if (response.hasOwnProperty('response') && response.hasOwnProperty('status') && response.status.code === 'ok') { + images = response.response.images; + } + + callback(images[0].url); + + showAlert(uploadModal, 'success', '[[uploads:upload-success]]'); + setTimeout(function () { + module.hideAlerts(uploadModal); + uploadModal.modal('hide'); + }, 750); + }, + }); + }; + + function maybeParse(response) { + if (typeof response === 'string') { + try { + return $.parseJSON(response); + } catch (e) { + return { error: '[[error:parse-error]]' }; + } + } + return response; + } + + function hasValidFileSize(fileElement, maxSize) { + if (window.FileReader && maxSize) { + return fileElement.files[0].size <= maxSize * 1000; + } + return true; + } + + return module; +}); diff --git a/public/src/overrides.js b/public/src/overrides.js new file mode 100644 index 0000000000..3cae763921 --- /dev/null +++ b/public/src/overrides.js @@ -0,0 +1,162 @@ +'use strict'; + +const translator = require('./modules/translator'); + +window.overrides = window.overrides || {}; + +function translate(elements, type, str) { + return elements.each(function () { + var el = $(this); + translator.translate(str, function (translated) { + el[type](translated); + }); + }); +} + +if (typeof window !== 'undefined') { + (function ($) { + $.fn.getCursorPosition = function () { + const el = $(this).get(0); + let pos = 0; + if ('selectionStart' in el) { + pos = el.selectionStart; + } else if ('selection' in document) { + el.focus(); + const Sel = document.selection.createRange(); + const SelLength = document.selection.createRange().text.length; + Sel.moveStart('character', -el.value.length); + pos = Sel.text.length - SelLength; + } + return pos; + }; + + $.fn.selectRange = function (start, end) { + if (!end) { + end = start; + } + return this.each(function () { + if (this.setSelectionRange) { + this.focus(); + this.setSelectionRange(start, end); + } else if (this.createTextRange) { + const range = this.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', start); + range.select(); + } + }); + }; + + // http://stackoverflow.com/questions/511088/use-javascript-to-place-cursor-at-end-of-text-in-text-input-element + $.fn.putCursorAtEnd = function () { + return this.each(function () { + $(this).focus(); + + if (this.setSelectionRange) { + const len = $(this).val().length * 2; + this.setSelectionRange(len, len); + } else { + $(this).val($(this).val()); + } + this.scrollTop = 999999; + }); + }; + + $.fn.translateHtml = function (str) { + return translate(this, 'html', str); + }; + + $.fn.translateText = function (str) { + return translate(this, 'text', str); + }; + + $.fn.translateVal = function (str) { + return translate(this, 'val', str); + }; + + $.fn.translateAttr = function (attr, str) { + return this.each(function () { + const el = $(this); + translator.translate(str, function (translated) { + el.attr(attr, translated); + }); + }); + }; + }(jQuery || { fn: {} })); + + (function () { + // FIX FOR #1245 - https://github.com/NodeBB/NodeBB/issues/1245 + // from http://stackoverflow.com/questions/15931962/bootstrap-dropdown-disappear-with-right-click-on-firefox + // obtain a reference to the original handler + let _clearMenus = $._data(document, 'events').click.filter(function (el) { + return el.namespace === 'bs.data-api.dropdown' && el.selector === undefined; + }); + + if (_clearMenus.length) { + _clearMenus = _clearMenus[0].handler; + } + + // disable the old listener + $(document) + .off('click.data-api.dropdown', _clearMenus) + .on('click.data-api.dropdown', function (e) { + // call the handler only when not right-click + if (e.button !== 2) { + _clearMenus(); + } + }); + }()); + let timeagoFn; + overrides.overrideTimeagoCutoff = function () { + const cutoff = parseInt(ajaxify.data.timeagoCutoff || config.timeagoCutoff, 10); + if (cutoff === 0) { + $.timeago.settings.cutoff = 1; + } else if (cutoff > 0) { + $.timeago.settings.cutoff = 1000 * 60 * 60 * 24 * cutoff; + } + }; + + overrides.overrideTimeago = function () { + if (!timeagoFn) { + timeagoFn = $.fn.timeago; + } + + overrides.overrideTimeagoCutoff(); + + $.timeago.settings.allowFuture = true; + const userLang = config.userLang.replace('_', '-'); + const options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; + let formatFn = function (date) { + return date.toLocaleString(userLang, options); + }; + try { + if (typeof Intl !== 'undefined') { + const dtFormat = new Intl.DateTimeFormat(userLang, options); + formatFn = dtFormat.format; + } + } catch (err) { + console.error(err); + } + + let iso; + let date; + $.fn.timeago = function () { + const els = $(this); + // Convert "old" format to new format (#5108) + els.each(function () { + iso = this.getAttribute('title'); + if (!iso) { + return; + } + this.setAttribute('datetime', iso); + date = new Date(iso); + if (!isNaN(date)) { + this.textContent = formatFn(date); + } + }); + + timeagoFn.apply(this, arguments); + }; + }; +} diff --git a/public/src/service-worker.js b/public/src/service-worker.js new file mode 100644 index 0000000000..883a7dcd4f --- /dev/null +++ b/public/src/service-worker.js @@ -0,0 +1,19 @@ +'use strict'; + +self.addEventListener('fetch', function (event) { + // This is the code that ignores post requests + // https://github.com/NodeBB/NodeBB/issues/9151 + // https://github.com/w3c/ServiceWorker/issues/1141 + // https://stackoverflow.com/questions/54448367/ajax-xmlhttprequest-progress-monitoring-doesnt-work-with-service-workers + if (event.request.method === 'POST') { + return; + } + + event.respondWith(caches.match(event.request).then(function (response) { + if (!response) { + return fetch(event.request); + } + + return response; + })); +}); diff --git a/public/src/sockets.js b/public/src/sockets.js new file mode 100644 index 0000000000..16657265fa --- /dev/null +++ b/public/src/sockets.js @@ -0,0 +1,257 @@ +'use strict'; + +// eslint-disable-next-line no-redeclare +const io = require('socket.io-client'); +// eslint-disable-next-line no-redeclare +const $ = require('jquery'); + +app = window.app || {}; + +(function () { + let reconnecting = false; + + const ioParams = { + reconnectionAttempts: config.maxReconnectionAttempts, + reconnectionDelay: config.reconnectionDelay, + transports: config.socketioTransports, + path: config.relative_path + '/socket.io', + }; + + window.socket = io(config.websocketAddress, ioParams); + + const oEmit = socket.emit; + socket.emit = function (event, data, callback) { + if (typeof data === 'function') { + callback = data; + data = null; + } + if (typeof callback === 'function') { + oEmit.apply(socket, [event, data, callback]); + return; + } + + return new Promise(function (resolve, reject) { + oEmit.apply(socket, [event, data, function (err, result) { + if (err) reject(err); + else resolve(result); + }]); + }); + }; + + let hooks; + require(['hooks'], function (_hooks) { + hooks = _hooks; + if (parseInt(app.user.uid, 10) >= 0) { + addHandlers(); + } + }); + + window.app.reconnect = () => { + if (socket.connected) { + return; + } + + const reconnectEl = $('#reconnect'); + $('#reconnect-alert') + .removeClass('alert-danger pointer') + .addClass('alert-warning') + .find('p') + .translateText(`[[global:reconnecting-message, ${config.siteTitle}]]`); + + reconnectEl.html(''); + socket.connect(); + }; + + function addHandlers() { + socket.on('connect', onConnect); + + socket.on('disconnect', onDisconnect); + + socket.io.on('reconnect_failed', function () { + const reconnectEl = $('#reconnect'); + reconnectEl.html(''); + + $('#reconnect-alert') + .removeClass('alert-warning') + .addClass('alert-danger pointer') + .find('p') + .translateText('[[error:socket-reconnect-failed]]') + .one('click', app.reconnect); + + $(window).one('focus', app.reconnect); + }); + + socket.on('checkSession', function (uid) { + if (parseInt(uid, 10) !== parseInt(app.user.uid, 10)) { + handleSessionMismatch(); + } + }); + socket.on('event:invalid_session', () => { + handleInvalidSession(); + }); + + socket.on('setHostname', function (hostname) { + app.upstreamHost = hostname; + }); + + socket.on('event:banned', onEventBanned); + socket.on('event:unbanned', onEventUnbanned); + socket.on('event:logout', function () { + require(['logout'], function (logout) { + logout(); + }); + }); + socket.on('event:alert', function (params) { + require(['alerts'], function (alerts) { + alerts.alert(params); + }); + }); + socket.on('event:deprecated_call', function (data) { + console.warn('[socket.io] ', data.eventName, 'is now deprecated in favour of', data.replacement); + }); + + socket.removeAllListeners('event:nodebb.ready'); + socket.on('event:nodebb.ready', function (data) { + if ((data.hostname === app.upstreamHost) && (!app.cacheBuster || app.cacheBuster !== data['cache-buster'])) { + app.cacheBuster = data['cache-buster']; + require(['alerts'], function (alerts) { + alerts.alert({ + alert_id: 'forum_updated', + title: '[[global:updated.title]]', + message: '[[global:updated.message]]', + clickfn: function () { + window.location.reload(); + }, + type: 'warning', + }); + }); + } + }); + socket.on('event:livereload', function () { + if (app.user.isAdmin && !ajaxify.currentPage.match(/admin/)) { + window.location.reload(); + } + }); + } + + function handleInvalidSession() { + socket.disconnect(); + require(['messages', 'logout'], function (messages, logout) { + logout(false); + messages.showInvalidSession(); + }); + } + + function handleSessionMismatch() { + if (app.flags._login || app.flags._logout) { + return; + } + + socket.disconnect(); + require(['messages'], function (messages) { + messages.showSessionMismatch(); + }); + } + + function onConnect() { + if (!reconnecting) { + hooks.fire('action:connected'); + } + + if (reconnecting) { + const reconnectEl = $('#reconnect'); + const reconnectAlert = $('#reconnect-alert'); + + reconnectEl.tooltip('destroy'); + reconnectEl.html(''); + reconnectAlert.addClass('hide'); + reconnecting = false; + + reJoinCurrentRoom(); + + socket.emit('meta.reconnected'); + + hooks.fire('action:reconnected'); + + setTimeout(function () { + reconnectEl.removeClass('active').addClass('hide'); + }, 3000); + } + } + + function reJoinCurrentRoom() { + if (app.currentRoom) { + const current = app.currentRoom; + app.currentRoom = ''; + app.enterRoom(current); + } + } + + function onReconnecting() { + reconnecting = true; + const reconnectEl = $('#reconnect'); + const reconnectAlert = $('#reconnect-alert'); + + if (!reconnectEl.hasClass('active')) { + reconnectEl.html(''); + reconnectAlert.removeClass('hide'); + } + + reconnectEl.addClass('active').removeClass('hide').tooltip({ + placement: 'bottom', + }); + } + + function onDisconnect() { + setTimeout(function () { + if (socket.disconnected) { + onReconnecting(); + } + }, 2000); + + hooks.fire('action:disconnected'); + } + + function onEventBanned(data) { + require(['bootbox', 'translator'], function (bootbox, translator) { + const message = data.until ? + translator.compile('error:user-banned-reason-until', (new Date(data.until).toLocaleString()), data.reason) : + '[[error:user-banned-reason, ' + data.reason + ']]'; + translator.translate(message, function (message) { + bootbox.alert({ + title: '[[error:user-banned]]', + message: message, + closeButton: false, + callback: function () { + window.location.href = config.relative_path + '/'; + }, + }); + }); + }); + } + + function onEventUnbanned() { + require(['bootbox'], function (bootbox) { + bootbox.alert({ + title: '[[global:alert.unbanned]]', + message: '[[global:alert.unbanned.message]]', + closeButton: false, + callback: function () { + window.location.href = config.relative_path + '/'; + }, + }); + }); + } + + if ( + config.socketioOrigins && + config.socketioOrigins !== '*:*' && + config.socketioOrigins.indexOf(location.hostname) === -1 + ) { + console.error( + 'You are accessing the forum from an unknown origin. This will likely result in websockets failing to connect. \n' + + 'To fix this, set the `"url"` value in `config.json` to the URL at which you access the site. \n' + + 'For more information, see this FAQ topic: https://community.nodebb.org/topic/13388' + ); + } +}()); diff --git a/public/src/utils.common.js b/public/src/utils.common.js new file mode 100644 index 0000000000..67af6c651d --- /dev/null +++ b/public/src/utils.common.js @@ -0,0 +1,750 @@ +'use strict'; + + +// add default escape function for escaping HTML entities +const escapeCharMap = Object.freeze({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`', + '=': '=', +}); +function replaceChar(c) { + return escapeCharMap[c]; +} +const escapeChars = /[&<>"'`=]/g; + +const HTMLEntities = Object.freeze({ + amp: '&', + gt: '>', + lt: '<', + quot: '"', + apos: "'", + AElig: 198, + Aacute: 193, + Acirc: 194, + Agrave: 192, + Aring: 197, + Atilde: 195, + Auml: 196, + Ccedil: 199, + ETH: 208, + Eacute: 201, + Ecirc: 202, + Egrave: 200, + Euml: 203, + Iacute: 205, + Icirc: 206, + Igrave: 204, + Iuml: 207, + Ntilde: 209, + Oacute: 211, + Ocirc: 212, + Ograve: 210, + Oslash: 216, + Otilde: 213, + Ouml: 214, + THORN: 222, + Uacute: 218, + Ucirc: 219, + Ugrave: 217, + Uuml: 220, + Yacute: 221, + aacute: 225, + acirc: 226, + aelig: 230, + agrave: 224, + aring: 229, + atilde: 227, + auml: 228, + ccedil: 231, + eacute: 233, + ecirc: 234, + egrave: 232, + eth: 240, + euml: 235, + iacute: 237, + icirc: 238, + igrave: 236, + iuml: 239, + ntilde: 241, + oacute: 243, + ocirc: 244, + ograve: 242, + oslash: 248, + otilde: 245, + ouml: 246, + szlig: 223, + thorn: 254, + uacute: 250, + ucirc: 251, + ugrave: 249, + uuml: 252, + yacute: 253, + yuml: 255, + copy: 169, + reg: 174, + nbsp: 160, + iexcl: 161, + cent: 162, + pound: 163, + curren: 164, + yen: 165, + brvbar: 166, + sect: 167, + uml: 168, + ordf: 170, + laquo: 171, + not: 172, + shy: 173, + macr: 175, + deg: 176, + plusmn: 177, + sup1: 185, + sup2: 178, + sup3: 179, + acute: 180, + micro: 181, + para: 182, + middot: 183, + cedil: 184, + ordm: 186, + raquo: 187, + frac14: 188, + frac12: 189, + frac34: 190, + iquest: 191, + times: 215, + divide: 247, + 'OElig;': 338, + 'oelig;': 339, + 'Scaron;': 352, + 'scaron;': 353, + 'Yuml;': 376, + 'fnof;': 402, + 'circ;': 710, + 'tilde;': 732, + 'Alpha;': 913, + 'Beta;': 914, + 'Gamma;': 915, + 'Delta;': 916, + 'Epsilon;': 917, + 'Zeta;': 918, + 'Eta;': 919, + 'Theta;': 920, + 'Iota;': 921, + 'Kappa;': 922, + 'Lambda;': 923, + 'Mu;': 924, + 'Nu;': 925, + 'Xi;': 926, + 'Omicron;': 927, + 'Pi;': 928, + 'Rho;': 929, + 'Sigma;': 931, + 'Tau;': 932, + 'Upsilon;': 933, + 'Phi;': 934, + 'Chi;': 935, + 'Psi;': 936, + 'Omega;': 937, + 'alpha;': 945, + 'beta;': 946, + 'gamma;': 947, + 'delta;': 948, + 'epsilon;': 949, + 'zeta;': 950, + 'eta;': 951, + 'theta;': 952, + 'iota;': 953, + 'kappa;': 954, + 'lambda;': 955, + 'mu;': 956, + 'nu;': 957, + 'xi;': 958, + 'omicron;': 959, + 'pi;': 960, + 'rho;': 961, + 'sigmaf;': 962, + 'sigma;': 963, + 'tau;': 964, + 'upsilon;': 965, + 'phi;': 966, + 'chi;': 967, + 'psi;': 968, + 'omega;': 969, + 'thetasym;': 977, + 'upsih;': 978, + 'piv;': 982, + 'ensp;': 8194, + 'emsp;': 8195, + 'thinsp;': 8201, + 'zwnj;': 8204, + 'zwj;': 8205, + 'lrm;': 8206, + 'rlm;': 8207, + 'ndash;': 8211, + 'mdash;': 8212, + 'lsquo;': 8216, + 'rsquo;': 8217, + 'sbquo;': 8218, + 'ldquo;': 8220, + 'rdquo;': 8221, + 'bdquo;': 8222, + 'dagger;': 8224, + 'Dagger;': 8225, + 'bull;': 8226, + 'hellip;': 8230, + 'permil;': 8240, + 'prime;': 8242, + 'Prime;': 8243, + 'lsaquo;': 8249, + 'rsaquo;': 8250, + 'oline;': 8254, + 'frasl;': 8260, + 'euro;': 8364, + 'image;': 8465, + 'weierp;': 8472, + 'real;': 8476, + 'trade;': 8482, + 'alefsym;': 8501, + 'larr;': 8592, + 'uarr;': 8593, + 'rarr;': 8594, + 'darr;': 8595, + 'harr;': 8596, + 'crarr;': 8629, + 'lArr;': 8656, + 'uArr;': 8657, + 'rArr;': 8658, + 'dArr;': 8659, + 'hArr;': 8660, + 'forall;': 8704, + 'part;': 8706, + 'exist;': 8707, + 'empty;': 8709, + 'nabla;': 8711, + 'isin;': 8712, + 'notin;': 8713, + 'ni;': 8715, + 'prod;': 8719, + 'sum;': 8721, + 'minus;': 8722, + 'lowast;': 8727, + 'radic;': 8730, + 'prop;': 8733, + 'infin;': 8734, + 'ang;': 8736, + 'and;': 8743, + 'or;': 8744, + 'cap;': 8745, + 'cup;': 8746, + 'int;': 8747, + 'there4;': 8756, + 'sim;': 8764, + 'cong;': 8773, + 'asymp;': 8776, + 'ne;': 8800, + 'equiv;': 8801, + 'le;': 8804, + 'ge;': 8805, + 'sub;': 8834, + 'sup;': 8835, + 'nsub;': 8836, + 'sube;': 8838, + 'supe;': 8839, + 'oplus;': 8853, + 'otimes;': 8855, + 'perp;': 8869, + 'sdot;': 8901, + 'lceil;': 8968, + 'rceil;': 8969, + 'lfloor;': 8970, + 'rfloor;': 8971, + 'lang;': 9001, + 'rang;': 9002, + 'loz;': 9674, + 'spades;': 9824, + 'clubs;': 9827, + 'hearts;': 9829, + 'diams;': 9830, +}); + +/* eslint-disable no-redeclare */ +const utils = { + // https://github.com/substack/node-ent/blob/master/index.js + decodeHTMLEntities: function (html) { + return String(html) + .replace(/&#(\d+);?/g, function (_, code) { + return String.fromCharCode(code); + }) + .replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) { + return String.fromCharCode(parseInt(hex, 16)); + }) + .replace(/&([^;\W]+;?)/g, function (m, e) { + const ee = e.replace(/;$/, ''); + const target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]); + + if (typeof target === 'number') { + return String.fromCharCode(target); + } else if (typeof target === 'string') { + return target; + } + + return m; + }); + }, + // https://github.com/jprichardson/string.js/blob/master/lib/string.js + stripHTMLTags: function (str, tags) { + const pattern = (tags || ['']).join('|'); + return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), ''); + }, + + cleanUpTag: function (tag, maxLength) { + if (typeof tag !== 'string' || !tag.length) { + return ''; + } + + tag = tag.trim().toLowerCase(); + // see https://github.com/NodeBB/NodeBB/issues/4378 + tag = tag.replace(/\u202E/gi, ''); + tag = tag.replace(/[,/#!$^*;:{}=_`<>'"~()?|]/g, ''); + tag = tag.slice(0, maxLength || 15).trim(); + const matches = tag.match(/^[.-]*(.+?)[.-]*$/); + if (matches && matches.length > 1) { + tag = matches[1]; + } + return tag; + }, + + removePunctuation: function (str) { + return str.replace(/[.,-/#!$%^&*;:{}=\-_`<>'"~()?]/g, ''); + }, + + isEmailValid: function (email) { + return typeof email === 'string' && email.length && email.indexOf('@') !== -1 && email.indexOf(',') === -1 && email.indexOf(';') === -1; + }, + + isUserNameValid: function (name) { + return (name && name !== '' && (/^['" \-+.*[\]0-9\u00BF-\u1FFF\u2C00-\uD7FF\w]+$/.test(name))); + }, + + isPasswordValid: function (password) { + return typeof password === 'string' && password.length; + }, + + isNumber: function (n) { + // `isFinite('') === true` so isNan parseFloat check is necessary + return !isNaN(parseFloat(n)) && isFinite(n); + }, + + languageKeyRegex: /\[\[[\w]+:.+\]\]/, + hasLanguageKey: function (input) { + return utils.languageKeyRegex.test(input); + }, + userLangToTimeagoCode: function (userLang) { + const mapping = { + 'en-GB': 'en', + 'en-US': 'en', + 'fa-IR': 'fa', + 'pt-BR': 'pt-br', + nb: 'no', + }; + return mapping.hasOwnProperty(userLang) ? mapping[userLang] : userLang; + }, + // shallow objects merge + merge: function () { + const result = {}; + let obj; + let keys; + for (let i = 0; i < arguments.length; i += 1) { + obj = arguments[i] || {}; + keys = Object.keys(obj); + for (let j = 0; j < keys.length; j += 1) { + result[keys[j]] = obj[keys[j]]; + } + } + return result; + }, + + fileExtension: function (path) { + return ('' + path).split('.').pop(); + }, + + extensionMimeTypeMap: { + bmp: 'image/bmp', + cmx: 'image/x-cmx', + cod: 'image/cis-cod', + gif: 'image/gif', + ico: 'image/x-icon', + ief: 'image/ief', + jfif: 'image/pipeg', + jpe: 'image/jpeg', + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + png: 'image/png', + pbm: 'image/x-portable-bitmap', + pgm: 'image/x-portable-graymap', + pnm: 'image/x-portable-anymap', + ppm: 'image/x-portable-pixmap', + ras: 'image/x-cmu-raster', + rgb: 'image/x-rgb', + svg: 'image/svg+xml', + tif: 'image/tiff', + tiff: 'image/tiff', + xbm: 'image/x-xbitmap', + xpm: 'image/x-xpixmap', + xwd: 'image/x-xwindowdump', + }, + + fileMimeType: function (path) { + return utils.extensionToMimeType(utils.fileExtension(path)); + }, + + extensionToMimeType: function (extension) { + return utils.extensionMimeTypeMap.hasOwnProperty(extension) ? utils.extensionMimeTypeMap[extension] : '*'; + }, + + isPromise: function (object) { + // https://stackoverflow.com/questions/27746304/how-do-i-tell-if-an-object-is-a-promise#comment97339131_27746324 + return object && typeof object.then === 'function'; + }, + + promiseParallel: function (obj) { + const keys = Object.keys(obj); + return Promise.all( + keys.map(function (k) { return obj[k]; }) + ).then(function (results) { + const data = {}; + keys.forEach(function (k, i) { + data[k] = results[i]; + }); + return data; + }); + }, + + // https://github.com/sindresorhus/is-absolute-url + isAbsoluteUrlRE: /^[a-zA-Z][a-zA-Z\d+\-.]*:/, + isWinPathRE: /^[a-zA-Z]:\\/, + isAbsoluteUrl: function (url) { + if (utils.isWinPathRE.test(url)) { + return false; + } + return utils.isAbsoluteUrlRE.test(url); + }, + + isRelativeUrl: function (url) { + return !utils.isAbsoluteUrl(url); + }, + + makeNumberHumanReadable: function (num) { + const n = parseInt(num, 10); + if (!n) { + return num; + } + if (n > 999999) { + return (n / 1000000).toFixed(1) + 'm'; + } else if (n > 999) { + return (n / 1000).toFixed(1) + 'k'; + } + return n; + }, + + // takes a string like 1000 and returns 1,000 + addCommas: function (text) { + return String(text).replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,'); + }, + + toISOString: function (timestamp) { + if (!timestamp || !Date.prototype.toISOString) { + return ''; + } + + // Prevent too-high values to be passed to Date object + timestamp = Math.min(timestamp, 8640000000000000); + + try { + return new Date(parseInt(timestamp, 10)).toISOString(); + } catch (e) { + return timestamp; + } + }, + + tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont', + 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed', + 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', + 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', + 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', + 'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', + 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'], + + stripTags: ['abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'base', 'basefont', + 'bdi', 'bdo', 'big', 'blink', 'body', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed', + 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'head', 'header', 'hr', 'html', 'iframe', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', + 'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', + 'output', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', + 'source', 'span', 'strike', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', + 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'const', 'video', 'wbr'], + + escapeRegexChars: function (text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + }, + + escapeHTML: function (str) { + if (str == null) { + return ''; + } + if (!str) { + return String(str); + } + + return str.toString().replace(escapeChars, replaceChar); + }, + + isAndroidBrowser: function () { + // http://stackoverflow.com/questions/9286355/how-to-detect-only-the-native-android-browser + const nua = navigator.userAgent; + return ((nua.indexOf('Mozilla/5.0') > -1 && nua.indexOf('Android ') > -1 && nua.indexOf('AppleWebKit') > -1) && !(nua.indexOf('Chrome') > -1)); + }, + + isTouchDevice: function () { + return 'ontouchstart' in document.documentElement; + }, + + findBootstrapEnvironment: function () { + // http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api + const envs = ['xs', 'sm', 'md', 'lg']; + const $el = $('
    '); + + $el.appendTo($('body')); + + for (let i = envs.length - 1; i >= 0; i -= 1) { + const env = envs[i]; + + $el.addClass('hidden-' + env); + if ($el.is(':hidden')) { + $el.remove(); + return env; + } + } + }, + + isMobile: function () { + const env = utils.findBootstrapEnvironment(); + return ['xs', 'sm'].some(function (targetEnv) { + return targetEnv === env; + }); + }, + + getHoursArray: function () { + const currentHour = new Date().getHours(); + const labels = []; + + for (let i = currentHour, ii = currentHour - 24; i > ii; i -= 1) { + const hour = i < 0 ? 24 + i : i; + labels.push(hour + ':00'); + } + + return labels.reverse(); + }, + + getDaysArray: function (from, amount) { + const currentDay = new Date(parseInt(from, 10) || Date.now()).getTime(); + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const labels = []; + let tmpDate; + + for (let x = (amount || 30) - 1; x >= 0; x -= 1) { + tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x)); + labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate()); + } + + return labels; + }, + + /* Retrieved from http://stackoverflow.com/a/7557433 @ 27 Mar 2016 */ + isElementInViewport: function (el) { + // special bonus for those using jQuery + if (typeof jQuery === 'function' && el instanceof jQuery) { + el = el[0]; + } + + const rect = el.getBoundingClientRect(); + + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && /* or $(window).height() */ + rect.right <= (window.innerWidth || document.documentElement.clientWidth) /* or $(window).width() */ + ); + }, + + // get all the url params in a single key/value hash + params: function (options = {}) { + let url; + if (options.url && !options.url.startsWith('http')) { + // relative path passed in + options.url = options.url.replace(new RegExp(`/?${config.relative_path.slice(1)}/`, 'g'), ''); + url = new URL(document.location); + url.pathname = options.url; + } else { + url = new URL(options.url || document.location); + } + let params = url.searchParams; + + if (options.full) { // return URLSearchParams object + return params; + } + + // Handle arrays passed in query string (Object.fromEntries does not) + const arrays = {}; + params.forEach((value, key) => { + if (!key.endsWith('[]')) { + return; + } + + key = key.slice(0, -2); + arrays[key] = arrays[key] || []; + arrays[key].push(utils.toType(value)); + }); + Object.keys(arrays).forEach((key) => { + params.delete(`${key}[]`); + }); + + // Backwards compatibility with v1.x -- all values passed through utils.toType() + params = Object.fromEntries(params); + Object.keys(params).forEach((key) => { + params[key] = utils.toType(params[key]); + }); + + return { ...params, ...arrays }; + }, + + param: function (key) { + return this.params()[key]; + }, + + urlToLocation: function (url) { + const a = document.createElement('a'); + a.href = url; + return a; + }, + + // return boolean if string 'true' or string 'false', or if a parsable string which is a number + // also supports JSON object and/or arrays parsing + toType: function (str) { + const type = typeof str; + if (type !== 'string') { + return str; + } + const nb = parseFloat(str); + if (!isNaN(nb) && isFinite(str)) { + return nb; + } + if (str === 'false') { + return false; + } + if (str === 'true') { + return true; + } + + try { + str = JSON.parse(str); + } catch (e) {} + + return str; + }, + + // Safely get/set chained properties on an object + // set example: utils.props(A, 'a.b.c.d', 10) // sets A to {a: {b: {c: {d: 10}}}}, and returns 10 + // get example: utils.props(A, 'a.b.c') // returns {d: 10} + // get example: utils.props(A, 'a.b.c.foo.bar') // returns undefined without throwing a TypeError + // credits to github.com/gkindel + props: function (obj, props, value) { + if (obj === undefined) { + obj = window; + } + if (props == null) { + return undefined; + } + const i = props.indexOf('.'); + if (i === -1) { + if (value !== undefined) { + obj[props] = value; + } + return obj[props]; + } + const prop = props.slice(0, i); + const newProps = props.slice(i + 1); + + if (props !== undefined && !(obj[prop] instanceof Object)) { + obj[prop] = {}; + } + + return utils.props(obj[prop], newProps, value); + }, + + isInternalURI: function (targetLocation, referenceLocation, relative_path) { + return targetLocation.host === '' || // Relative paths are always internal links + ( + targetLocation.host === referenceLocation.host && + // Otherwise need to check if protocol and host match + targetLocation.protocol === referenceLocation.protocol && + // Subfolder installs need this additional check + (relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true) + ); + }, + + rtrim: function (str) { + return str.replace(/\s+$/g, ''); + }, + + debounce: function (func, wait, immediate) { + // modified from https://davidwalsh.name/javascript-debounce-function + let timeout; + return function () { + const context = this; + const args = arguments; + const later = function () { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + func.apply(context, args); + } + }; + }, + throttle: function (func, wait, immediate) { + let timeout; + return function () { + const context = this; + const args = arguments; + const later = function () { + timeout = null; + if (!immediate) { + func.apply(context, args); + } + }; + const callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + func.apply(context, args); + } + }; + }, +}; + +module.exports = utils; diff --git a/public/src/utils.js b/public/src/utils.js new file mode 100644 index 0000000000..fbb1695b8a --- /dev/null +++ b/public/src/utils.js @@ -0,0 +1,83 @@ +/* eslint-disable no-redeclare */ + +'use strict'; + +const $ = require('jquery'); + +const utils = { ...require('./utils.common') }; + +utils.getLanguage = function () { + let lang = 'en-GB'; + if (typeof window === 'object' && window.config && window.utils) { + lang = utils.params().lang || config.userLang || config.defaultLang || 'en-GB'; + } + return lang; +}; + + +utils.makeNumbersHumanReadable = function (elements) { + elements.each(function () { + $(this) + .html(utils.makeNumberHumanReadable($(this).attr('title'))) + .removeClass('hidden'); + }); +}; + +utils.addCommasToNumbers = function (elements) { + elements.each(function (index, element) { + $(element) + .html(utils.addCommas($(element).html())) + .removeClass('hidden'); + }); +}; + +utils.findBootstrapEnvironment = function () { + // http://stackoverflow.com/questions/14441456/how-to-detect-which-device-view-youre-on-using-twitter-bootstrap-api + const envs = ['xs', 'sm', 'md', 'lg']; + const $el = $('
    '); + + $el.appendTo($('body')); + + for (let i = envs.length - 1; i >= 0; i -= 1) { + const env = envs[i]; + + $el.addClass('hidden-' + env); + if ($el.is(':hidden')) { + $el.remove(); + return env; + } + } +}; + +utils.isMobile = function () { + const env = utils.findBootstrapEnvironment(); + return ['xs', 'sm'].some(function (targetEnv) { + return targetEnv === env; + }); +}; + +utils.assertPasswordValidity = (password, zxcvbn) => { + // More checks on top of basic utils.isPasswordValid() + if (!utils.isPasswordValid(password)) { + throw new Error('[[user:change_password_error]]'); + } else if (password.length < ajaxify.data.minimumPasswordLength) { + throw new Error('[[reset_password:password_too_short]]'); + } else if (password.length > 512) { + throw new Error('[[error:password-too-long]]'); + } + + const passwordStrength = zxcvbn(password); + if (passwordStrength.score < ajaxify.data.minimumPasswordStrength) { + throw new Error('[[user:weak_password]]'); + } +}; + +utils.generateUUID = function () { + // from https://github.com/tracker1/node-uuid4/blob/master/browser.js + const temp_url = URL.createObjectURL(new Blob()); + const uuid = temp_url.toString(); + URL.revokeObjectURL(temp_url); + return uuid.split(/[:/]/g).pop().toLowerCase(); // remove prefixes +}; + +module.exports = utils; diff --git a/public/src/widgets.js b/public/src/widgets.js new file mode 100644 index 0000000000..7ad65229b3 --- /dev/null +++ b/public/src/widgets.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports.render = function (template) { + if (template.match(/^admin/)) { + return; + } + + const locations = Object.keys(ajaxify.data.widgets); + + locations.forEach(function (location) { + let area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0); + if (area.length) { + return; + } + + const widgetsAtLocation = ajaxify.data.widgets[location] || []; + let html = ''; + + widgetsAtLocation.forEach(function (widget) { + html += widget.html; + }); + + if (location === 'footer' && !$('#content [widget-area="footer"],#content [data-widget-area="footer"]').length) { + $('#content').append($('
    ')); + } else if (location === 'sidebar' && !$('#content [widget-area="sidebar"],#content [data-widget-area="sidebar"]').length) { + if ($('[component="account/cover"]').length) { + $('[component="account/cover"]').nextAll().wrapAll($('
    ')); + } else if ($('[component="groups/cover"]').length) { + $('[component="groups/cover"]').nextAll().wrapAll($('
    ')); + } else { + $('#content > *').wrapAll($('
    ')); + } + } else if (location === 'header' && !$('#content [widget-area="header"],#content [data-widget-area="header"]').length) { + $('#content').prepend($('
    ')); + } + + area = $('#content [widget-area="' + location + '"],#content [data-widget-area="' + location + '"]').eq(0); + if (html && area.length) { + area.html(html); + area.find('img:not(.not-responsive)').addClass('img-responsive'); + } + + if (widgetsAtLocation.length) { + area.removeClass('hidden'); + } + }); + + require(['hooks'], function (hooks) { + hooks.fire('action:widgets.loaded', {}); + }); +}; + diff --git a/public/vendor/bootbox/wrapper.js b/public/vendor/bootbox/wrapper.js new file mode 100644 index 0000000000..2b57e8b759 --- /dev/null +++ b/public/vendor/bootbox/wrapper.js @@ -0,0 +1,62 @@ +/* global bootbox */ + +require(['translator', 'bootbox'], function (shim, bootbox) { + 'use strict'; + + // expose as global with a warning + if (Object.defineProperty) { + Object.defineProperty(window, 'bootbox', { + configurable: true, + enumerable: true, + get: function () { + console.warn('[deprecated] Accessing bootbox globally is deprecated. Use `require(["bootbox"], function (bootbox) { ... })` instead'); + return bootbox; + }, + }); + } else { + window.bootbox = bootbox; + } + + bootbox.setDefaults({ + locale: config.userLang, + }); + + var translator = shim.Translator.create(); + var dialog = bootbox.dialog; + var attrsToTranslate = ['placeholder', 'title', 'value', 'label']; + bootbox.dialog = function (options) { + var show = options.show !== false; + options.show = false; + + var $elem = dialog.call(bootbox, options); + var element = $elem[0]; + + if (/\[\[.+\]\]/.test(element.outerHTML)) { + translator.translateInPlace(element, attrsToTranslate).then(function () { + if (show) { + $elem.modal('show'); + } + }); + } else if (show) { + $elem.modal('show'); + } + + return $elem; + }; + + Promise.all([ + translator.translateKey('modules:bootbox.ok', []), + translator.translateKey('modules:bootbox.cancel', []), + translator.translateKey('modules:bootbox.confirm', []), + ]).then(function (translations) { + var lang = shim.getLanguage(); + bootbox.addLocale(lang, { + OK: translations[0], + CANCEL: translations[1], + CONFIRM: translations[2], + }); + + bootbox.setLocale(lang); + }); +}); + diff --git a/public/vendor/fontawesome/.gitignore b/public/vendor/fontawesome/.gitignore new file mode 100644 index 0000000000..aaad45f3c8 --- /dev/null +++ b/public/vendor/fontawesome/.gitignore @@ -0,0 +1,29 @@ +*.pyc +*.egg-info +*.db +*.db.old +*.swp +*.db-journal + +.coverage +.DS_Store +.installed.cfg + +.idea/* +.svn/* +src/website/static/* +src/website/media/* + +bin +build +cfcache +develop-eggs +dist +downloads +eggs +parts +tmp +.sass-cache + +src/website/settingslocal.py +stunnel.log \ No newline at end of file diff --git a/public/vendor/fontawesome/LICENSE.txt b/public/vendor/fontawesome/LICENSE.txt new file mode 100644 index 0000000000..f31bef92b6 --- /dev/null +++ b/public/vendor/fontawesome/LICENSE.txt @@ -0,0 +1,34 @@ +Font Awesome Free License +------------------------- + +Font Awesome Free is free, open source, and GPL friendly. You can use it for +commercial projects, open source projects, or really almost whatever you want. +Full Font Awesome Free license: https://fontawesome.com/license/free. + +# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) +In the Font Awesome Free download, the CC BY 4.0 license applies to all icons +packaged as SVG and JS file types. + +# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) +In the Font Awesome Free download, the SIL OFL license applies to all icons +packaged as web and desktop font files. + +# Code: MIT License (https://opensource.org/licenses/MIT) +In the Font Awesome Free download, the MIT license applies to all non-font and +non-icon files. + +# Attribution +Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font +Awesome Free files already contain embedded comments with sufficient +attribution, so you shouldn't need to do anything additional when using these +files normally. + +We've kept attribution comments terse, so we ask that you do not actively work +to remove them from files, especially code. They're a great way for folks to +learn about Font Awesome. + +# Brand Icons +All brand icons are trademarks of their respective owners. The use of these +trademarks does not indicate endorsement of the trademark holder by Font +Awesome, nor vice versa. **Please do not use brand logos for any purpose except +to represent the company, product, or service to which they refer.** diff --git a/public/vendor/fontawesome/attribution.js b/public/vendor/fontawesome/attribution.js new file mode 100644 index 0000000000..2d28cc90d6 --- /dev/null +++ b/public/vendor/fontawesome/attribution.js @@ -0,0 +1,3 @@ +console.log(`Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com +License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) +`) \ No newline at end of file diff --git a/public/vendor/fontawesome/less/_animated.less b/public/vendor/fontawesome/less/_animated.less new file mode 100644 index 0000000000..704ec95103 --- /dev/null +++ b/public/vendor/fontawesome/less/_animated.less @@ -0,0 +1,19 @@ +// Animated Icons +// -------------------------- + +.@{fa-css-prefix}-spin { + animation: fa-spin 2s infinite linear; +} + +.@{fa-css-prefix}-pulse { + animation: fa-spin 1s infinite steps(8); +} + +@keyframes fa-spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/public/vendor/fontawesome/less/_bordered-pulled.less b/public/vendor/fontawesome/less/_bordered-pulled.less new file mode 100644 index 0000000000..29a356b423 --- /dev/null +++ b/public/vendor/fontawesome/less/_bordered-pulled.less @@ -0,0 +1,16 @@ +// Bordered & Pulled +// ------------------------- + +.@{fa-css-prefix}-border { + border-radius: .1em; + border: solid .08em @fa-border-color; + padding: .2em .25em .15em; +} + +.@{fa-css-prefix}-pull-left { float: left; } +.@{fa-css-prefix}-pull-right { float: right; } + +.@{fa-css-prefix}, .fas, .far, .fal, .fab { + &.@{fa-css-prefix}-pull-left { margin-right: .3em; } + &.@{fa-css-prefix}-pull-right { margin-left: .3em; } +} diff --git a/public/vendor/fontawesome/less/_core.less b/public/vendor/fontawesome/less/_core.less new file mode 100644 index 0000000000..e8c2ff3898 --- /dev/null +++ b/public/vendor/fontawesome/less/_core.less @@ -0,0 +1,12 @@ +// Base Class Definition +// ------------------------- + +.@{fa-css-prefix}, .fas, .far, .fal, .fad, .fab { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; +} diff --git a/public/vendor/fontawesome/less/_fixed-width.less b/public/vendor/fontawesome/less/_fixed-width.less new file mode 100644 index 0000000000..be817c6375 --- /dev/null +++ b/public/vendor/fontawesome/less/_fixed-width.less @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.@{fa-css-prefix}-fw { + text-align: center; + width: (20em / 16); +} diff --git a/public/vendor/fontawesome/less/_icons.less b/public/vendor/fontawesome/less/_icons.less new file mode 100644 index 0000000000..5dc7df5ebb --- /dev/null +++ b/public/vendor/fontawesome/less/_icons.less @@ -0,0 +1,1462 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ + +.@{fa-css-prefix}-500px:before { content: @fa-var-500px; } +.@{fa-css-prefix}-accessible-icon:before { content: @fa-var-accessible-icon; } +.@{fa-css-prefix}-accusoft:before { content: @fa-var-accusoft; } +.@{fa-css-prefix}-acquisitions-incorporated:before { content: @fa-var-acquisitions-incorporated; } +.@{fa-css-prefix}-ad:before { content: @fa-var-ad; } +.@{fa-css-prefix}-address-book:before { content: @fa-var-address-book; } +.@{fa-css-prefix}-address-card:before { content: @fa-var-address-card; } +.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; } +.@{fa-css-prefix}-adn:before { content: @fa-var-adn; } +.@{fa-css-prefix}-adversal:before { content: @fa-var-adversal; } +.@{fa-css-prefix}-affiliatetheme:before { content: @fa-var-affiliatetheme; } +.@{fa-css-prefix}-air-freshener:before { content: @fa-var-air-freshener; } +.@{fa-css-prefix}-airbnb:before { content: @fa-var-airbnb; } +.@{fa-css-prefix}-algolia:before { content: @fa-var-algolia; } +.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; } +.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; } +.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; } +.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; } +.@{fa-css-prefix}-alipay:before { content: @fa-var-alipay; } +.@{fa-css-prefix}-allergies:before { content: @fa-var-allergies; } +.@{fa-css-prefix}-amazon:before { content: @fa-var-amazon; } +.@{fa-css-prefix}-amazon-pay:before { content: @fa-var-amazon-pay; } +.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; } +.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; } +.@{fa-css-prefix}-amilia:before { content: @fa-var-amilia; } +.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; } +.@{fa-css-prefix}-android:before { content: @fa-var-android; } +.@{fa-css-prefix}-angellist:before { content: @fa-var-angellist; } +.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; } +.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; } +.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; } +.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; } +.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; } +.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; } +.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; } +.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; } +.@{fa-css-prefix}-angry:before { content: @fa-var-angry; } +.@{fa-css-prefix}-angrycreative:before { content: @fa-var-angrycreative; } +.@{fa-css-prefix}-angular:before { content: @fa-var-angular; } +.@{fa-css-prefix}-ankh:before { content: @fa-var-ankh; } +.@{fa-css-prefix}-app-store:before { content: @fa-var-app-store; } +.@{fa-css-prefix}-app-store-ios:before { content: @fa-var-app-store-ios; } +.@{fa-css-prefix}-apper:before { content: @fa-var-apper; } +.@{fa-css-prefix}-apple:before { content: @fa-var-apple; } +.@{fa-css-prefix}-apple-alt:before { content: @fa-var-apple-alt; } +.@{fa-css-prefix}-apple-pay:before { content: @fa-var-apple-pay; } +.@{fa-css-prefix}-archive:before { content: @fa-var-archive; } +.@{fa-css-prefix}-archway:before { content: @fa-var-archway; } +.@{fa-css-prefix}-arrow-alt-circle-down:before { content: @fa-var-arrow-alt-circle-down; } +.@{fa-css-prefix}-arrow-alt-circle-left:before { content: @fa-var-arrow-alt-circle-left; } +.@{fa-css-prefix}-arrow-alt-circle-right:before { content: @fa-var-arrow-alt-circle-right; } +.@{fa-css-prefix}-arrow-alt-circle-up:before { content: @fa-var-arrow-alt-circle-up; } +.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; } +.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; } +.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; } +.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; } +.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; } +.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; } +.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; } +.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; } +.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; } +.@{fa-css-prefix}-arrows-alt-h:before { content: @fa-var-arrows-alt-h; } +.@{fa-css-prefix}-arrows-alt-v:before { content: @fa-var-arrows-alt-v; } +.@{fa-css-prefix}-artstation:before { content: @fa-var-artstation; } +.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; } +.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; } +.@{fa-css-prefix}-asymmetrik:before { content: @fa-var-asymmetrik; } +.@{fa-css-prefix}-at:before { content: @fa-var-at; } +.@{fa-css-prefix}-atlas:before { content: @fa-var-atlas; } +.@{fa-css-prefix}-atlassian:before { content: @fa-var-atlassian; } +.@{fa-css-prefix}-atom:before { content: @fa-var-atom; } +.@{fa-css-prefix}-audible:before { content: @fa-var-audible; } +.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; } +.@{fa-css-prefix}-autoprefixer:before { content: @fa-var-autoprefixer; } +.@{fa-css-prefix}-avianex:before { content: @fa-var-avianex; } +.@{fa-css-prefix}-aviato:before { content: @fa-var-aviato; } +.@{fa-css-prefix}-award:before { content: @fa-var-award; } +.@{fa-css-prefix}-aws:before { content: @fa-var-aws; } +.@{fa-css-prefix}-baby:before { content: @fa-var-baby; } +.@{fa-css-prefix}-baby-carriage:before { content: @fa-var-baby-carriage; } +.@{fa-css-prefix}-backspace:before { content: @fa-var-backspace; } +.@{fa-css-prefix}-backward:before { content: @fa-var-backward; } +.@{fa-css-prefix}-bacon:before { content: @fa-var-bacon; } +.@{fa-css-prefix}-bacteria:before { content: @fa-var-bacteria; } +.@{fa-css-prefix}-bacterium:before { content: @fa-var-bacterium; } +.@{fa-css-prefix}-bahai:before { content: @fa-var-bahai; } +.@{fa-css-prefix}-balance-scale:before { content: @fa-var-balance-scale; } +.@{fa-css-prefix}-balance-scale-left:before { content: @fa-var-balance-scale-left; } +.@{fa-css-prefix}-balance-scale-right:before { content: @fa-var-balance-scale-right; } +.@{fa-css-prefix}-ban:before { content: @fa-var-ban; } +.@{fa-css-prefix}-band-aid:before { content: @fa-var-band-aid; } +.@{fa-css-prefix}-bandcamp:before { content: @fa-var-bandcamp; } +.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; } +.@{fa-css-prefix}-bars:before { content: @fa-var-bars; } +.@{fa-css-prefix}-baseball-ball:before { content: @fa-var-baseball-ball; } +.@{fa-css-prefix}-basketball-ball:before { content: @fa-var-basketball-ball; } +.@{fa-css-prefix}-bath:before { content: @fa-var-bath; } +.@{fa-css-prefix}-battery-empty:before { content: @fa-var-battery-empty; } +.@{fa-css-prefix}-battery-full:before { content: @fa-var-battery-full; } +.@{fa-css-prefix}-battery-half:before { content: @fa-var-battery-half; } +.@{fa-css-prefix}-battery-quarter:before { content: @fa-var-battery-quarter; } +.@{fa-css-prefix}-battery-three-quarters:before { content: @fa-var-battery-three-quarters; } +.@{fa-css-prefix}-battle-net:before { content: @fa-var-battle-net; } +.@{fa-css-prefix}-bed:before { content: @fa-var-bed; } +.@{fa-css-prefix}-beer:before { content: @fa-var-beer; } +.@{fa-css-prefix}-behance:before { content: @fa-var-behance; } +.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; } +.@{fa-css-prefix}-bell:before { content: @fa-var-bell; } +.@{fa-css-prefix}-bell-slash:before { content: @fa-var-bell-slash; } +.@{fa-css-prefix}-bezier-curve:before { content: @fa-var-bezier-curve; } +.@{fa-css-prefix}-bible:before { content: @fa-var-bible; } +.@{fa-css-prefix}-bicycle:before { content: @fa-var-bicycle; } +.@{fa-css-prefix}-biking:before { content: @fa-var-biking; } +.@{fa-css-prefix}-bimobject:before { content: @fa-var-bimobject; } +.@{fa-css-prefix}-binoculars:before { content: @fa-var-binoculars; } +.@{fa-css-prefix}-biohazard:before { content: @fa-var-biohazard; } +.@{fa-css-prefix}-birthday-cake:before { content: @fa-var-birthday-cake; } +.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; } +.@{fa-css-prefix}-bitcoin:before { content: @fa-var-bitcoin; } +.@{fa-css-prefix}-bity:before { content: @fa-var-bity; } +.@{fa-css-prefix}-black-tie:before { content: @fa-var-black-tie; } +.@{fa-css-prefix}-blackberry:before { content: @fa-var-blackberry; } +.@{fa-css-prefix}-blender:before { content: @fa-var-blender; } +.@{fa-css-prefix}-blender-phone:before { content: @fa-var-blender-phone; } +.@{fa-css-prefix}-blind:before { content: @fa-var-blind; } +.@{fa-css-prefix}-blog:before { content: @fa-var-blog; } +.@{fa-css-prefix}-blogger:before { content: @fa-var-blogger; } +.@{fa-css-prefix}-blogger-b:before { content: @fa-var-blogger-b; } +.@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; } +.@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; } +.@{fa-css-prefix}-bold:before { content: @fa-var-bold; } +.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; } +.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; } +.@{fa-css-prefix}-bone:before { content: @fa-var-bone; } +.@{fa-css-prefix}-bong:before { content: @fa-var-bong; } +.@{fa-css-prefix}-book:before { content: @fa-var-book; } +.@{fa-css-prefix}-book-dead:before { content: @fa-var-book-dead; } +.@{fa-css-prefix}-book-medical:before { content: @fa-var-book-medical; } +.@{fa-css-prefix}-book-open:before { content: @fa-var-book-open; } +.@{fa-css-prefix}-book-reader:before { content: @fa-var-book-reader; } +.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; } +.@{fa-css-prefix}-bootstrap:before { content: @fa-var-bootstrap; } +.@{fa-css-prefix}-border-all:before { content: @fa-var-border-all; } +.@{fa-css-prefix}-border-none:before { content: @fa-var-border-none; } +.@{fa-css-prefix}-border-style:before { content: @fa-var-border-style; } +.@{fa-css-prefix}-bowling-ball:before { content: @fa-var-bowling-ball; } +.@{fa-css-prefix}-box:before { content: @fa-var-box; } +.@{fa-css-prefix}-box-open:before { content: @fa-var-box-open; } +.@{fa-css-prefix}-box-tissue:before { content: @fa-var-box-tissue; } +.@{fa-css-prefix}-boxes:before { content: @fa-var-boxes; } +.@{fa-css-prefix}-braille:before { content: @fa-var-braille; } +.@{fa-css-prefix}-brain:before { content: @fa-var-brain; } +.@{fa-css-prefix}-bread-slice:before { content: @fa-var-bread-slice; } +.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; } +.@{fa-css-prefix}-briefcase-medical:before { content: @fa-var-briefcase-medical; } +.@{fa-css-prefix}-broadcast-tower:before { content: @fa-var-broadcast-tower; } +.@{fa-css-prefix}-broom:before { content: @fa-var-broom; } +.@{fa-css-prefix}-brush:before { content: @fa-var-brush; } +.@{fa-css-prefix}-btc:before { content: @fa-var-btc; } +.@{fa-css-prefix}-buffer:before { content: @fa-var-buffer; } +.@{fa-css-prefix}-bug:before { content: @fa-var-bug; } +.@{fa-css-prefix}-building:before { content: @fa-var-building; } +.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } +.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; } +.@{fa-css-prefix}-burn:before { content: @fa-var-burn; } +.@{fa-css-prefix}-buromobelexperte:before { content: @fa-var-buromobelexperte; } +.@{fa-css-prefix}-bus:before { content: @fa-var-bus; } +.@{fa-css-prefix}-bus-alt:before { content: @fa-var-bus-alt; } +.@{fa-css-prefix}-business-time:before { content: @fa-var-business-time; } +.@{fa-css-prefix}-buy-n-large:before { content: @fa-var-buy-n-large; } +.@{fa-css-prefix}-buysellads:before { content: @fa-var-buysellads; } +.@{fa-css-prefix}-calculator:before { content: @fa-var-calculator; } +.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; } +.@{fa-css-prefix}-calendar-alt:before { content: @fa-var-calendar-alt; } +.@{fa-css-prefix}-calendar-check:before { content: @fa-var-calendar-check; } +.@{fa-css-prefix}-calendar-day:before { content: @fa-var-calendar-day; } +.@{fa-css-prefix}-calendar-minus:before { content: @fa-var-calendar-minus; } +.@{fa-css-prefix}-calendar-plus:before { content: @fa-var-calendar-plus; } +.@{fa-css-prefix}-calendar-times:before { content: @fa-var-calendar-times; } +.@{fa-css-prefix}-calendar-week:before { content: @fa-var-calendar-week; } +.@{fa-css-prefix}-camera:before { content: @fa-var-camera; } +.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; } +.@{fa-css-prefix}-campground:before { content: @fa-var-campground; } +.@{fa-css-prefix}-canadian-maple-leaf:before { content: @fa-var-canadian-maple-leaf; } +.@{fa-css-prefix}-candy-cane:before { content: @fa-var-candy-cane; } +.@{fa-css-prefix}-cannabis:before { content: @fa-var-cannabis; } +.@{fa-css-prefix}-capsules:before { content: @fa-var-capsules; } +.@{fa-css-prefix}-car:before { content: @fa-var-car; } +.@{fa-css-prefix}-car-alt:before { content: @fa-var-car-alt; } +.@{fa-css-prefix}-car-battery:before { content: @fa-var-car-battery; } +.@{fa-css-prefix}-car-crash:before { content: @fa-var-car-crash; } +.@{fa-css-prefix}-car-side:before { content: @fa-var-car-side; } +.@{fa-css-prefix}-caravan:before { content: @fa-var-caravan; } +.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; } +.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; } +.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; } +.@{fa-css-prefix}-caret-square-down:before { content: @fa-var-caret-square-down; } +.@{fa-css-prefix}-caret-square-left:before { content: @fa-var-caret-square-left; } +.@{fa-css-prefix}-caret-square-right:before { content: @fa-var-caret-square-right; } +.@{fa-css-prefix}-caret-square-up:before { content: @fa-var-caret-square-up; } +.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; } +.@{fa-css-prefix}-carrot:before { content: @fa-var-carrot; } +.@{fa-css-prefix}-cart-arrow-down:before { content: @fa-var-cart-arrow-down; } +.@{fa-css-prefix}-cart-plus:before { content: @fa-var-cart-plus; } +.@{fa-css-prefix}-cash-register:before { content: @fa-var-cash-register; } +.@{fa-css-prefix}-cat:before { content: @fa-var-cat; } +.@{fa-css-prefix}-cc-amazon-pay:before { content: @fa-var-cc-amazon-pay; } +.@{fa-css-prefix}-cc-amex:before { content: @fa-var-cc-amex; } +.@{fa-css-prefix}-cc-apple-pay:before { content: @fa-var-cc-apple-pay; } +.@{fa-css-prefix}-cc-diners-club:before { content: @fa-var-cc-diners-club; } +.@{fa-css-prefix}-cc-discover:before { content: @fa-var-cc-discover; } +.@{fa-css-prefix}-cc-jcb:before { content: @fa-var-cc-jcb; } +.@{fa-css-prefix}-cc-mastercard:before { content: @fa-var-cc-mastercard; } +.@{fa-css-prefix}-cc-paypal:before { content: @fa-var-cc-paypal; } +.@{fa-css-prefix}-cc-stripe:before { content: @fa-var-cc-stripe; } +.@{fa-css-prefix}-cc-visa:before { content: @fa-var-cc-visa; } +.@{fa-css-prefix}-centercode:before { content: @fa-var-centercode; } +.@{fa-css-prefix}-centos:before { content: @fa-var-centos; } +.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; } +.@{fa-css-prefix}-chair:before { content: @fa-var-chair; } +.@{fa-css-prefix}-chalkboard:before { content: @fa-var-chalkboard; } +.@{fa-css-prefix}-chalkboard-teacher:before { content: @fa-var-chalkboard-teacher; } +.@{fa-css-prefix}-charging-station:before { content: @fa-var-charging-station; } +.@{fa-css-prefix}-chart-area:before { content: @fa-var-chart-area; } +.@{fa-css-prefix}-chart-bar:before { content: @fa-var-chart-bar; } +.@{fa-css-prefix}-chart-line:before { content: @fa-var-chart-line; } +.@{fa-css-prefix}-chart-pie:before { content: @fa-var-chart-pie; } +.@{fa-css-prefix}-check:before { content: @fa-var-check; } +.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; } +.@{fa-css-prefix}-check-double:before { content: @fa-var-check-double; } +.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; } +.@{fa-css-prefix}-cheese:before { content: @fa-var-cheese; } +.@{fa-css-prefix}-chess:before { content: @fa-var-chess; } +.@{fa-css-prefix}-chess-bishop:before { content: @fa-var-chess-bishop; } +.@{fa-css-prefix}-chess-board:before { content: @fa-var-chess-board; } +.@{fa-css-prefix}-chess-king:before { content: @fa-var-chess-king; } +.@{fa-css-prefix}-chess-knight:before { content: @fa-var-chess-knight; } +.@{fa-css-prefix}-chess-pawn:before { content: @fa-var-chess-pawn; } +.@{fa-css-prefix}-chess-queen:before { content: @fa-var-chess-queen; } +.@{fa-css-prefix}-chess-rook:before { content: @fa-var-chess-rook; } +.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; } +.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; } +.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; } +.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; } +.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; } +.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; } +.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; } +.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; } +.@{fa-css-prefix}-child:before { content: @fa-var-child; } +.@{fa-css-prefix}-chrome:before { content: @fa-var-chrome; } +.@{fa-css-prefix}-chromecast:before { content: @fa-var-chromecast; } +.@{fa-css-prefix}-church:before { content: @fa-var-church; } +.@{fa-css-prefix}-circle:before { content: @fa-var-circle; } +.@{fa-css-prefix}-circle-notch:before { content: @fa-var-circle-notch; } +.@{fa-css-prefix}-city:before { content: @fa-var-city; } +.@{fa-css-prefix}-clinic-medical:before { content: @fa-var-clinic-medical; } +.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; } +.@{fa-css-prefix}-clipboard-check:before { content: @fa-var-clipboard-check; } +.@{fa-css-prefix}-clipboard-list:before { content: @fa-var-clipboard-list; } +.@{fa-css-prefix}-clock:before { content: @fa-var-clock; } +.@{fa-css-prefix}-clone:before { content: @fa-var-clone; } +.@{fa-css-prefix}-closed-captioning:before { content: @fa-var-closed-captioning; } +.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; } +.@{fa-css-prefix}-cloud-download-alt:before { content: @fa-var-cloud-download-alt; } +.@{fa-css-prefix}-cloud-meatball:before { content: @fa-var-cloud-meatball; } +.@{fa-css-prefix}-cloud-moon:before { content: @fa-var-cloud-moon; } +.@{fa-css-prefix}-cloud-moon-rain:before { content: @fa-var-cloud-moon-rain; } +.@{fa-css-prefix}-cloud-rain:before { content: @fa-var-cloud-rain; } +.@{fa-css-prefix}-cloud-showers-heavy:before { content: @fa-var-cloud-showers-heavy; } +.@{fa-css-prefix}-cloud-sun:before { content: @fa-var-cloud-sun; } +.@{fa-css-prefix}-cloud-sun-rain:before { content: @fa-var-cloud-sun-rain; } +.@{fa-css-prefix}-cloud-upload-alt:before { content: @fa-var-cloud-upload-alt; } +.@{fa-css-prefix}-cloudflare:before { content: @fa-var-cloudflare; } +.@{fa-css-prefix}-cloudscale:before { content: @fa-var-cloudscale; } +.@{fa-css-prefix}-cloudsmith:before { content: @fa-var-cloudsmith; } +.@{fa-css-prefix}-cloudversify:before { content: @fa-var-cloudversify; } +.@{fa-css-prefix}-cocktail:before { content: @fa-var-cocktail; } +.@{fa-css-prefix}-code:before { content: @fa-var-code; } +.@{fa-css-prefix}-code-branch:before { content: @fa-var-code-branch; } +.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; } +.@{fa-css-prefix}-codiepie:before { content: @fa-var-codiepie; } +.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; } +.@{fa-css-prefix}-cog:before { content: @fa-var-cog; } +.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; } +.@{fa-css-prefix}-coins:before { content: @fa-var-coins; } +.@{fa-css-prefix}-columns:before { content: @fa-var-columns; } +.@{fa-css-prefix}-comment:before { content: @fa-var-comment; } +.@{fa-css-prefix}-comment-alt:before { content: @fa-var-comment-alt; } +.@{fa-css-prefix}-comment-dollar:before { content: @fa-var-comment-dollar; } +.@{fa-css-prefix}-comment-dots:before { content: @fa-var-comment-dots; } +.@{fa-css-prefix}-comment-medical:before { content: @fa-var-comment-medical; } +.@{fa-css-prefix}-comment-slash:before { content: @fa-var-comment-slash; } +.@{fa-css-prefix}-comments:before { content: @fa-var-comments; } +.@{fa-css-prefix}-comments-dollar:before { content: @fa-var-comments-dollar; } +.@{fa-css-prefix}-compact-disc:before { content: @fa-var-compact-disc; } +.@{fa-css-prefix}-compass:before { content: @fa-var-compass; } +.@{fa-css-prefix}-compress:before { content: @fa-var-compress; } +.@{fa-css-prefix}-compress-alt:before { content: @fa-var-compress-alt; } +.@{fa-css-prefix}-compress-arrows-alt:before { content: @fa-var-compress-arrows-alt; } +.@{fa-css-prefix}-concierge-bell:before { content: @fa-var-concierge-bell; } +.@{fa-css-prefix}-confluence:before { content: @fa-var-confluence; } +.@{fa-css-prefix}-connectdevelop:before { content: @fa-var-connectdevelop; } +.@{fa-css-prefix}-contao:before { content: @fa-var-contao; } +.@{fa-css-prefix}-cookie:before { content: @fa-var-cookie; } +.@{fa-css-prefix}-cookie-bite:before { content: @fa-var-cookie-bite; } +.@{fa-css-prefix}-copy:before { content: @fa-var-copy; } +.@{fa-css-prefix}-copyright:before { content: @fa-var-copyright; } +.@{fa-css-prefix}-cotton-bureau:before { content: @fa-var-cotton-bureau; } +.@{fa-css-prefix}-couch:before { content: @fa-var-couch; } +.@{fa-css-prefix}-cpanel:before { content: @fa-var-cpanel; } +.@{fa-css-prefix}-creative-commons:before { content: @fa-var-creative-commons; } +.@{fa-css-prefix}-creative-commons-by:before { content: @fa-var-creative-commons-by; } +.@{fa-css-prefix}-creative-commons-nc:before { content: @fa-var-creative-commons-nc; } +.@{fa-css-prefix}-creative-commons-nc-eu:before { content: @fa-var-creative-commons-nc-eu; } +.@{fa-css-prefix}-creative-commons-nc-jp:before { content: @fa-var-creative-commons-nc-jp; } +.@{fa-css-prefix}-creative-commons-nd:before { content: @fa-var-creative-commons-nd; } +.@{fa-css-prefix}-creative-commons-pd:before { content: @fa-var-creative-commons-pd; } +.@{fa-css-prefix}-creative-commons-pd-alt:before { content: @fa-var-creative-commons-pd-alt; } +.@{fa-css-prefix}-creative-commons-remix:before { content: @fa-var-creative-commons-remix; } +.@{fa-css-prefix}-creative-commons-sa:before { content: @fa-var-creative-commons-sa; } +.@{fa-css-prefix}-creative-commons-sampling:before { content: @fa-var-creative-commons-sampling; } +.@{fa-css-prefix}-creative-commons-sampling-plus:before { content: @fa-var-creative-commons-sampling-plus; } +.@{fa-css-prefix}-creative-commons-share:before { content: @fa-var-creative-commons-share; } +.@{fa-css-prefix}-creative-commons-zero:before { content: @fa-var-creative-commons-zero; } +.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } +.@{fa-css-prefix}-critical-role:before { content: @fa-var-critical-role; } +.@{fa-css-prefix}-crop:before { content: @fa-var-crop; } +.@{fa-css-prefix}-crop-alt:before { content: @fa-var-crop-alt; } +.@{fa-css-prefix}-cross:before { content: @fa-var-cross; } +.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; } +.@{fa-css-prefix}-crow:before { content: @fa-var-crow; } +.@{fa-css-prefix}-crown:before { content: @fa-var-crown; } +.@{fa-css-prefix}-crutch:before { content: @fa-var-crutch; } +.@{fa-css-prefix}-css3:before { content: @fa-var-css3; } +.@{fa-css-prefix}-css3-alt:before { content: @fa-var-css3-alt; } +.@{fa-css-prefix}-cube:before { content: @fa-var-cube; } +.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; } +.@{fa-css-prefix}-cut:before { content: @fa-var-cut; } +.@{fa-css-prefix}-cuttlefish:before { content: @fa-var-cuttlefish; } +.@{fa-css-prefix}-d-and-d:before { content: @fa-var-d-and-d; } +.@{fa-css-prefix}-d-and-d-beyond:before { content: @fa-var-d-and-d-beyond; } +.@{fa-css-prefix}-dailymotion:before { content: @fa-var-dailymotion; } +.@{fa-css-prefix}-dashcube:before { content: @fa-var-dashcube; } +.@{fa-css-prefix}-database:before { content: @fa-var-database; } +.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; } +.@{fa-css-prefix}-deezer:before { content: @fa-var-deezer; } +.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } +.@{fa-css-prefix}-democrat:before { content: @fa-var-democrat; } +.@{fa-css-prefix}-deploydog:before { content: @fa-var-deploydog; } +.@{fa-css-prefix}-deskpro:before { content: @fa-var-deskpro; } +.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; } +.@{fa-css-prefix}-dev:before { content: @fa-var-dev; } +.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; } +.@{fa-css-prefix}-dharmachakra:before { content: @fa-var-dharmachakra; } +.@{fa-css-prefix}-dhl:before { content: @fa-var-dhl; } +.@{fa-css-prefix}-diagnoses:before { content: @fa-var-diagnoses; } +.@{fa-css-prefix}-diaspora:before { content: @fa-var-diaspora; } +.@{fa-css-prefix}-dice:before { content: @fa-var-dice; } +.@{fa-css-prefix}-dice-d20:before { content: @fa-var-dice-d20; } +.@{fa-css-prefix}-dice-d6:before { content: @fa-var-dice-d6; } +.@{fa-css-prefix}-dice-five:before { content: @fa-var-dice-five; } +.@{fa-css-prefix}-dice-four:before { content: @fa-var-dice-four; } +.@{fa-css-prefix}-dice-one:before { content: @fa-var-dice-one; } +.@{fa-css-prefix}-dice-six:before { content: @fa-var-dice-six; } +.@{fa-css-prefix}-dice-three:before { content: @fa-var-dice-three; } +.@{fa-css-prefix}-dice-two:before { content: @fa-var-dice-two; } +.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } +.@{fa-css-prefix}-digital-ocean:before { content: @fa-var-digital-ocean; } +.@{fa-css-prefix}-digital-tachograph:before { content: @fa-var-digital-tachograph; } +.@{fa-css-prefix}-directions:before { content: @fa-var-directions; } +.@{fa-css-prefix}-discord:before { content: @fa-var-discord; } +.@{fa-css-prefix}-discourse:before { content: @fa-var-discourse; } +.@{fa-css-prefix}-disease:before { content: @fa-var-disease; } +.@{fa-css-prefix}-divide:before { content: @fa-var-divide; } +.@{fa-css-prefix}-dizzy:before { content: @fa-var-dizzy; } +.@{fa-css-prefix}-dna:before { content: @fa-var-dna; } +.@{fa-css-prefix}-dochub:before { content: @fa-var-dochub; } +.@{fa-css-prefix}-docker:before { content: @fa-var-docker; } +.@{fa-css-prefix}-dog:before { content: @fa-var-dog; } +.@{fa-css-prefix}-dollar-sign:before { content: @fa-var-dollar-sign; } +.@{fa-css-prefix}-dolly:before { content: @fa-var-dolly; } +.@{fa-css-prefix}-dolly-flatbed:before { content: @fa-var-dolly-flatbed; } +.@{fa-css-prefix}-donate:before { content: @fa-var-donate; } +.@{fa-css-prefix}-door-closed:before { content: @fa-var-door-closed; } +.@{fa-css-prefix}-door-open:before { content: @fa-var-door-open; } +.@{fa-css-prefix}-dot-circle:before { content: @fa-var-dot-circle; } +.@{fa-css-prefix}-dove:before { content: @fa-var-dove; } +.@{fa-css-prefix}-download:before { content: @fa-var-download; } +.@{fa-css-prefix}-draft2digital:before { content: @fa-var-draft2digital; } +.@{fa-css-prefix}-drafting-compass:before { content: @fa-var-drafting-compass; } +.@{fa-css-prefix}-dragon:before { content: @fa-var-dragon; } +.@{fa-css-prefix}-draw-polygon:before { content: @fa-var-draw-polygon; } +.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; } +.@{fa-css-prefix}-dribbble-square:before { content: @fa-var-dribbble-square; } +.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; } +.@{fa-css-prefix}-drum:before { content: @fa-var-drum; } +.@{fa-css-prefix}-drum-steelpan:before { content: @fa-var-drum-steelpan; } +.@{fa-css-prefix}-drumstick-bite:before { content: @fa-var-drumstick-bite; } +.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } +.@{fa-css-prefix}-dumbbell:before { content: @fa-var-dumbbell; } +.@{fa-css-prefix}-dumpster:before { content: @fa-var-dumpster; } +.@{fa-css-prefix}-dumpster-fire:before { content: @fa-var-dumpster-fire; } +.@{fa-css-prefix}-dungeon:before { content: @fa-var-dungeon; } +.@{fa-css-prefix}-dyalog:before { content: @fa-var-dyalog; } +.@{fa-css-prefix}-earlybirds:before { content: @fa-var-earlybirds; } +.@{fa-css-prefix}-ebay:before { content: @fa-var-ebay; } +.@{fa-css-prefix}-edge:before { content: @fa-var-edge; } +.@{fa-css-prefix}-edge-legacy:before { content: @fa-var-edge-legacy; } +.@{fa-css-prefix}-edit:before { content: @fa-var-edit; } +.@{fa-css-prefix}-egg:before { content: @fa-var-egg; } +.@{fa-css-prefix}-eject:before { content: @fa-var-eject; } +.@{fa-css-prefix}-elementor:before { content: @fa-var-elementor; } +.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; } +.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; } +.@{fa-css-prefix}-ello:before { content: @fa-var-ello; } +.@{fa-css-prefix}-ember:before { content: @fa-var-ember; } +.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } +.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; } +.@{fa-css-prefix}-envelope-open:before { content: @fa-var-envelope-open; } +.@{fa-css-prefix}-envelope-open-text:before { content: @fa-var-envelope-open-text; } +.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; } +.@{fa-css-prefix}-envira:before { content: @fa-var-envira; } +.@{fa-css-prefix}-equals:before { content: @fa-var-equals; } +.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; } +.@{fa-css-prefix}-erlang:before { content: @fa-var-erlang; } +.@{fa-css-prefix}-ethereum:before { content: @fa-var-ethereum; } +.@{fa-css-prefix}-ethernet:before { content: @fa-var-ethernet; } +.@{fa-css-prefix}-etsy:before { content: @fa-var-etsy; } +.@{fa-css-prefix}-euro-sign:before { content: @fa-var-euro-sign; } +.@{fa-css-prefix}-evernote:before { content: @fa-var-evernote; } +.@{fa-css-prefix}-exchange-alt:before { content: @fa-var-exchange-alt; } +.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; } +.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; } +.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; } +.@{fa-css-prefix}-expand:before { content: @fa-var-expand; } +.@{fa-css-prefix}-expand-alt:before { content: @fa-var-expand-alt; } +.@{fa-css-prefix}-expand-arrows-alt:before { content: @fa-var-expand-arrows-alt; } +.@{fa-css-prefix}-expeditedssl:before { content: @fa-var-expeditedssl; } +.@{fa-css-prefix}-external-link-alt:before { content: @fa-var-external-link-alt; } +.@{fa-css-prefix}-external-link-square-alt:before { content: @fa-var-external-link-square-alt; } +.@{fa-css-prefix}-eye:before { content: @fa-var-eye; } +.@{fa-css-prefix}-eye-dropper:before { content: @fa-var-eye-dropper; } +.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; } +.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; } +.@{fa-css-prefix}-facebook-f:before { content: @fa-var-facebook-f; } +.@{fa-css-prefix}-facebook-messenger:before { content: @fa-var-facebook-messenger; } +.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; } +.@{fa-css-prefix}-fan:before { content: @fa-var-fan; } +.@{fa-css-prefix}-fantasy-flight-games:before { content: @fa-var-fantasy-flight-games; } +.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; } +.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; } +.@{fa-css-prefix}-faucet:before { content: @fa-var-faucet; } +.@{fa-css-prefix}-fax:before { content: @fa-var-fax; } +.@{fa-css-prefix}-feather:before { content: @fa-var-feather; } +.@{fa-css-prefix}-feather-alt:before { content: @fa-var-feather-alt; } +.@{fa-css-prefix}-fedex:before { content: @fa-var-fedex; } +.@{fa-css-prefix}-fedora:before { content: @fa-var-fedora; } +.@{fa-css-prefix}-female:before { content: @fa-var-female; } +.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; } +.@{fa-css-prefix}-figma:before { content: @fa-var-figma; } +.@{fa-css-prefix}-file:before { content: @fa-var-file; } +.@{fa-css-prefix}-file-alt:before { content: @fa-var-file-alt; } +.@{fa-css-prefix}-file-archive:before { content: @fa-var-file-archive; } +.@{fa-css-prefix}-file-audio:before { content: @fa-var-file-audio; } +.@{fa-css-prefix}-file-code:before { content: @fa-var-file-code; } +.@{fa-css-prefix}-file-contract:before { content: @fa-var-file-contract; } +.@{fa-css-prefix}-file-csv:before { content: @fa-var-file-csv; } +.@{fa-css-prefix}-file-download:before { content: @fa-var-file-download; } +.@{fa-css-prefix}-file-excel:before { content: @fa-var-file-excel; } +.@{fa-css-prefix}-file-export:before { content: @fa-var-file-export; } +.@{fa-css-prefix}-file-image:before { content: @fa-var-file-image; } +.@{fa-css-prefix}-file-import:before { content: @fa-var-file-import; } +.@{fa-css-prefix}-file-invoice:before { content: @fa-var-file-invoice; } +.@{fa-css-prefix}-file-invoice-dollar:before { content: @fa-var-file-invoice-dollar; } +.@{fa-css-prefix}-file-medical:before { content: @fa-var-file-medical; } +.@{fa-css-prefix}-file-medical-alt:before { content: @fa-var-file-medical-alt; } +.@{fa-css-prefix}-file-pdf:before { content: @fa-var-file-pdf; } +.@{fa-css-prefix}-file-powerpoint:before { content: @fa-var-file-powerpoint; } +.@{fa-css-prefix}-file-prescription:before { content: @fa-var-file-prescription; } +.@{fa-css-prefix}-file-signature:before { content: @fa-var-file-signature; } +.@{fa-css-prefix}-file-upload:before { content: @fa-var-file-upload; } +.@{fa-css-prefix}-file-video:before { content: @fa-var-file-video; } +.@{fa-css-prefix}-file-word:before { content: @fa-var-file-word; } +.@{fa-css-prefix}-fill:before { content: @fa-var-fill; } +.@{fa-css-prefix}-fill-drip:before { content: @fa-var-fill-drip; } +.@{fa-css-prefix}-film:before { content: @fa-var-film; } +.@{fa-css-prefix}-filter:before { content: @fa-var-filter; } +.@{fa-css-prefix}-fingerprint:before { content: @fa-var-fingerprint; } +.@{fa-css-prefix}-fire:before { content: @fa-var-fire; } +.@{fa-css-prefix}-fire-alt:before { content: @fa-var-fire-alt; } +.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; } +.@{fa-css-prefix}-firefox:before { content: @fa-var-firefox; } +.@{fa-css-prefix}-firefox-browser:before { content: @fa-var-firefox-browser; } +.@{fa-css-prefix}-first-aid:before { content: @fa-var-first-aid; } +.@{fa-css-prefix}-first-order:before { content: @fa-var-first-order; } +.@{fa-css-prefix}-first-order-alt:before { content: @fa-var-first-order-alt; } +.@{fa-css-prefix}-firstdraft:before { content: @fa-var-firstdraft; } +.@{fa-css-prefix}-fish:before { content: @fa-var-fish; } +.@{fa-css-prefix}-fist-raised:before { content: @fa-var-fist-raised; } +.@{fa-css-prefix}-flag:before { content: @fa-var-flag; } +.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; } +.@{fa-css-prefix}-flag-usa:before { content: @fa-var-flag-usa; } +.@{fa-css-prefix}-flask:before { content: @fa-var-flask; } +.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; } +.@{fa-css-prefix}-flipboard:before { content: @fa-var-flipboard; } +.@{fa-css-prefix}-flushed:before { content: @fa-var-flushed; } +.@{fa-css-prefix}-fly:before { content: @fa-var-fly; } +.@{fa-css-prefix}-folder:before { content: @fa-var-folder; } +.@{fa-css-prefix}-folder-minus:before { content: @fa-var-folder-minus; } +.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; } +.@{fa-css-prefix}-folder-plus:before { content: @fa-var-folder-plus; } +.@{fa-css-prefix}-font:before { content: @fa-var-font; } +.@{fa-css-prefix}-font-awesome:before { content: @fa-var-font-awesome; } +.@{fa-css-prefix}-font-awesome-alt:before { content: @fa-var-font-awesome-alt; } +.@{fa-css-prefix}-font-awesome-flag:before { content: @fa-var-font-awesome-flag; } +.@{fa-css-prefix}-font-awesome-logo-full:before { content: @fa-var-font-awesome-logo-full; } +.@{fa-css-prefix}-fonticons:before { content: @fa-var-fonticons; } +.@{fa-css-prefix}-fonticons-fi:before { content: @fa-var-fonticons-fi; } +.@{fa-css-prefix}-football-ball:before { content: @fa-var-football-ball; } +.@{fa-css-prefix}-fort-awesome:before { content: @fa-var-fort-awesome; } +.@{fa-css-prefix}-fort-awesome-alt:before { content: @fa-var-fort-awesome-alt; } +.@{fa-css-prefix}-forumbee:before { content: @fa-var-forumbee; } +.@{fa-css-prefix}-forward:before { content: @fa-var-forward; } +.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; } +.@{fa-css-prefix}-free-code-camp:before { content: @fa-var-free-code-camp; } +.@{fa-css-prefix}-freebsd:before { content: @fa-var-freebsd; } +.@{fa-css-prefix}-frog:before { content: @fa-var-frog; } +.@{fa-css-prefix}-frown:before { content: @fa-var-frown; } +.@{fa-css-prefix}-frown-open:before { content: @fa-var-frown-open; } +.@{fa-css-prefix}-fulcrum:before { content: @fa-var-fulcrum; } +.@{fa-css-prefix}-funnel-dollar:before { content: @fa-var-funnel-dollar; } +.@{fa-css-prefix}-futbol:before { content: @fa-var-futbol; } +.@{fa-css-prefix}-galactic-republic:before { content: @fa-var-galactic-republic; } +.@{fa-css-prefix}-galactic-senate:before { content: @fa-var-galactic-senate; } +.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; } +.@{fa-css-prefix}-gas-pump:before { content: @fa-var-gas-pump; } +.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; } +.@{fa-css-prefix}-gem:before { content: @fa-var-gem; } +.@{fa-css-prefix}-genderless:before { content: @fa-var-genderless; } +.@{fa-css-prefix}-get-pocket:before { content: @fa-var-get-pocket; } +.@{fa-css-prefix}-gg:before { content: @fa-var-gg; } +.@{fa-css-prefix}-gg-circle:before { content: @fa-var-gg-circle; } +.@{fa-css-prefix}-ghost:before { content: @fa-var-ghost; } +.@{fa-css-prefix}-gift:before { content: @fa-var-gift; } +.@{fa-css-prefix}-gifts:before { content: @fa-var-gifts; } +.@{fa-css-prefix}-git:before { content: @fa-var-git; } +.@{fa-css-prefix}-git-alt:before { content: @fa-var-git-alt; } +.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } +.@{fa-css-prefix}-github:before { content: @fa-var-github; } +.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; } +.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; } +.@{fa-css-prefix}-gitkraken:before { content: @fa-var-gitkraken; } +.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; } +.@{fa-css-prefix}-gitter:before { content: @fa-var-gitter; } +.@{fa-css-prefix}-glass-cheers:before { content: @fa-var-glass-cheers; } +.@{fa-css-prefix}-glass-martini:before { content: @fa-var-glass-martini; } +.@{fa-css-prefix}-glass-martini-alt:before { content: @fa-var-glass-martini-alt; } +.@{fa-css-prefix}-glass-whiskey:before { content: @fa-var-glass-whiskey; } +.@{fa-css-prefix}-glasses:before { content: @fa-var-glasses; } +.@{fa-css-prefix}-glide:before { content: @fa-var-glide; } +.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; } +.@{fa-css-prefix}-globe:before { content: @fa-var-globe; } +.@{fa-css-prefix}-globe-africa:before { content: @fa-var-globe-africa; } +.@{fa-css-prefix}-globe-americas:before { content: @fa-var-globe-americas; } +.@{fa-css-prefix}-globe-asia:before { content: @fa-var-globe-asia; } +.@{fa-css-prefix}-globe-europe:before { content: @fa-var-globe-europe; } +.@{fa-css-prefix}-gofore:before { content: @fa-var-gofore; } +.@{fa-css-prefix}-golf-ball:before { content: @fa-var-golf-ball; } +.@{fa-css-prefix}-goodreads:before { content: @fa-var-goodreads; } +.@{fa-css-prefix}-goodreads-g:before { content: @fa-var-goodreads-g; } +.@{fa-css-prefix}-google:before { content: @fa-var-google; } +.@{fa-css-prefix}-google-drive:before { content: @fa-var-google-drive; } +.@{fa-css-prefix}-google-pay:before { content: @fa-var-google-pay; } +.@{fa-css-prefix}-google-play:before { content: @fa-var-google-play; } +.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; } +.@{fa-css-prefix}-google-plus-g:before { content: @fa-var-google-plus-g; } +.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; } +.@{fa-css-prefix}-google-wallet:before { content: @fa-var-google-wallet; } +.@{fa-css-prefix}-gopuram:before { content: @fa-var-gopuram; } +.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; } +.@{fa-css-prefix}-gratipay:before { content: @fa-var-gratipay; } +.@{fa-css-prefix}-grav:before { content: @fa-var-grav; } +.@{fa-css-prefix}-greater-than:before { content: @fa-var-greater-than; } +.@{fa-css-prefix}-greater-than-equal:before { content: @fa-var-greater-than-equal; } +.@{fa-css-prefix}-grimace:before { content: @fa-var-grimace; } +.@{fa-css-prefix}-grin:before { content: @fa-var-grin; } +.@{fa-css-prefix}-grin-alt:before { content: @fa-var-grin-alt; } +.@{fa-css-prefix}-grin-beam:before { content: @fa-var-grin-beam; } +.@{fa-css-prefix}-grin-beam-sweat:before { content: @fa-var-grin-beam-sweat; } +.@{fa-css-prefix}-grin-hearts:before { content: @fa-var-grin-hearts; } +.@{fa-css-prefix}-grin-squint:before { content: @fa-var-grin-squint; } +.@{fa-css-prefix}-grin-squint-tears:before { content: @fa-var-grin-squint-tears; } +.@{fa-css-prefix}-grin-stars:before { content: @fa-var-grin-stars; } +.@{fa-css-prefix}-grin-tears:before { content: @fa-var-grin-tears; } +.@{fa-css-prefix}-grin-tongue:before { content: @fa-var-grin-tongue; } +.@{fa-css-prefix}-grin-tongue-squint:before { content: @fa-var-grin-tongue-squint; } +.@{fa-css-prefix}-grin-tongue-wink:before { content: @fa-var-grin-tongue-wink; } +.@{fa-css-prefix}-grin-wink:before { content: @fa-var-grin-wink; } +.@{fa-css-prefix}-grip-horizontal:before { content: @fa-var-grip-horizontal; } +.@{fa-css-prefix}-grip-lines:before { content: @fa-var-grip-lines; } +.@{fa-css-prefix}-grip-lines-vertical:before { content: @fa-var-grip-lines-vertical; } +.@{fa-css-prefix}-grip-vertical:before { content: @fa-var-grip-vertical; } +.@{fa-css-prefix}-gripfire:before { content: @fa-var-gripfire; } +.@{fa-css-prefix}-grunt:before { content: @fa-var-grunt; } +.@{fa-css-prefix}-guilded:before { content: @fa-var-guilded; } +.@{fa-css-prefix}-guitar:before { content: @fa-var-guitar; } +.@{fa-css-prefix}-gulp:before { content: @fa-var-gulp; } +.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; } +.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; } +.@{fa-css-prefix}-hacker-news-square:before { content: @fa-var-hacker-news-square; } +.@{fa-css-prefix}-hackerrank:before { content: @fa-var-hackerrank; } +.@{fa-css-prefix}-hamburger:before { content: @fa-var-hamburger; } +.@{fa-css-prefix}-hammer:before { content: @fa-var-hammer; } +.@{fa-css-prefix}-hamsa:before { content: @fa-var-hamsa; } +.@{fa-css-prefix}-hand-holding:before { content: @fa-var-hand-holding; } +.@{fa-css-prefix}-hand-holding-heart:before { content: @fa-var-hand-holding-heart; } +.@{fa-css-prefix}-hand-holding-medical:before { content: @fa-var-hand-holding-medical; } +.@{fa-css-prefix}-hand-holding-usd:before { content: @fa-var-hand-holding-usd; } +.@{fa-css-prefix}-hand-holding-water:before { content: @fa-var-hand-holding-water; } +.@{fa-css-prefix}-hand-lizard:before { content: @fa-var-hand-lizard; } +.@{fa-css-prefix}-hand-middle-finger:before { content: @fa-var-hand-middle-finger; } +.@{fa-css-prefix}-hand-paper:before { content: @fa-var-hand-paper; } +.@{fa-css-prefix}-hand-peace:before { content: @fa-var-hand-peace; } +.@{fa-css-prefix}-hand-point-down:before { content: @fa-var-hand-point-down; } +.@{fa-css-prefix}-hand-point-left:before { content: @fa-var-hand-point-left; } +.@{fa-css-prefix}-hand-point-right:before { content: @fa-var-hand-point-right; } +.@{fa-css-prefix}-hand-point-up:before { content: @fa-var-hand-point-up; } +.@{fa-css-prefix}-hand-pointer:before { content: @fa-var-hand-pointer; } +.@{fa-css-prefix}-hand-rock:before { content: @fa-var-hand-rock; } +.@{fa-css-prefix}-hand-scissors:before { content: @fa-var-hand-scissors; } +.@{fa-css-prefix}-hand-sparkles:before { content: @fa-var-hand-sparkles; } +.@{fa-css-prefix}-hand-spock:before { content: @fa-var-hand-spock; } +.@{fa-css-prefix}-hands:before { content: @fa-var-hands; } +.@{fa-css-prefix}-hands-helping:before { content: @fa-var-hands-helping; } +.@{fa-css-prefix}-hands-wash:before { content: @fa-var-hands-wash; } +.@{fa-css-prefix}-handshake:before { content: @fa-var-handshake; } +.@{fa-css-prefix}-handshake-alt-slash:before { content: @fa-var-handshake-alt-slash; } +.@{fa-css-prefix}-handshake-slash:before { content: @fa-var-handshake-slash; } +.@{fa-css-prefix}-hanukiah:before { content: @fa-var-hanukiah; } +.@{fa-css-prefix}-hard-hat:before { content: @fa-var-hard-hat; } +.@{fa-css-prefix}-hashtag:before { content: @fa-var-hashtag; } +.@{fa-css-prefix}-hat-cowboy:before { content: @fa-var-hat-cowboy; } +.@{fa-css-prefix}-hat-cowboy-side:before { content: @fa-var-hat-cowboy-side; } +.@{fa-css-prefix}-hat-wizard:before { content: @fa-var-hat-wizard; } +.@{fa-css-prefix}-hdd:before { content: @fa-var-hdd; } +.@{fa-css-prefix}-head-side-cough:before { content: @fa-var-head-side-cough; } +.@{fa-css-prefix}-head-side-cough-slash:before { content: @fa-var-head-side-cough-slash; } +.@{fa-css-prefix}-head-side-mask:before { content: @fa-var-head-side-mask; } +.@{fa-css-prefix}-head-side-virus:before { content: @fa-var-head-side-virus; } +.@{fa-css-prefix}-heading:before { content: @fa-var-heading; } +.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; } +.@{fa-css-prefix}-headphones-alt:before { content: @fa-var-headphones-alt; } +.@{fa-css-prefix}-headset:before { content: @fa-var-headset; } +.@{fa-css-prefix}-heart:before { content: @fa-var-heart; } +.@{fa-css-prefix}-heart-broken:before { content: @fa-var-heart-broken; } +.@{fa-css-prefix}-heartbeat:before { content: @fa-var-heartbeat; } +.@{fa-css-prefix}-helicopter:before { content: @fa-var-helicopter; } +.@{fa-css-prefix}-highlighter:before { content: @fa-var-highlighter; } +.@{fa-css-prefix}-hiking:before { content: @fa-var-hiking; } +.@{fa-css-prefix}-hippo:before { content: @fa-var-hippo; } +.@{fa-css-prefix}-hips:before { content: @fa-var-hips; } +.@{fa-css-prefix}-hire-a-helper:before { content: @fa-var-hire-a-helper; } +.@{fa-css-prefix}-history:before { content: @fa-var-history; } +.@{fa-css-prefix}-hive:before { content: @fa-var-hive; } +.@{fa-css-prefix}-hockey-puck:before { content: @fa-var-hockey-puck; } +.@{fa-css-prefix}-holly-berry:before { content: @fa-var-holly-berry; } +.@{fa-css-prefix}-home:before { content: @fa-var-home; } +.@{fa-css-prefix}-hooli:before { content: @fa-var-hooli; } +.@{fa-css-prefix}-hornbill:before { content: @fa-var-hornbill; } +.@{fa-css-prefix}-horse:before { content: @fa-var-horse; } +.@{fa-css-prefix}-horse-head:before { content: @fa-var-horse-head; } +.@{fa-css-prefix}-hospital:before { content: @fa-var-hospital; } +.@{fa-css-prefix}-hospital-alt:before { content: @fa-var-hospital-alt; } +.@{fa-css-prefix}-hospital-symbol:before { content: @fa-var-hospital-symbol; } +.@{fa-css-prefix}-hospital-user:before { content: @fa-var-hospital-user; } +.@{fa-css-prefix}-hot-tub:before { content: @fa-var-hot-tub; } +.@{fa-css-prefix}-hotdog:before { content: @fa-var-hotdog; } +.@{fa-css-prefix}-hotel:before { content: @fa-var-hotel; } +.@{fa-css-prefix}-hotjar:before { content: @fa-var-hotjar; } +.@{fa-css-prefix}-hourglass:before { content: @fa-var-hourglass; } +.@{fa-css-prefix}-hourglass-end:before { content: @fa-var-hourglass-end; } +.@{fa-css-prefix}-hourglass-half:before { content: @fa-var-hourglass-half; } +.@{fa-css-prefix}-hourglass-start:before { content: @fa-var-hourglass-start; } +.@{fa-css-prefix}-house-damage:before { content: @fa-var-house-damage; } +.@{fa-css-prefix}-house-user:before { content: @fa-var-house-user; } +.@{fa-css-prefix}-houzz:before { content: @fa-var-houzz; } +.@{fa-css-prefix}-hryvnia:before { content: @fa-var-hryvnia; } +.@{fa-css-prefix}-html5:before { content: @fa-var-html5; } +.@{fa-css-prefix}-hubspot:before { content: @fa-var-hubspot; } +.@{fa-css-prefix}-i-cursor:before { content: @fa-var-i-cursor; } +.@{fa-css-prefix}-ice-cream:before { content: @fa-var-ice-cream; } +.@{fa-css-prefix}-icicles:before { content: @fa-var-icicles; } +.@{fa-css-prefix}-icons:before { content: @fa-var-icons; } +.@{fa-css-prefix}-id-badge:before { content: @fa-var-id-badge; } +.@{fa-css-prefix}-id-card:before { content: @fa-var-id-card; } +.@{fa-css-prefix}-id-card-alt:before { content: @fa-var-id-card-alt; } +.@{fa-css-prefix}-ideal:before { content: @fa-var-ideal; } +.@{fa-css-prefix}-igloo:before { content: @fa-var-igloo; } +.@{fa-css-prefix}-image:before { content: @fa-var-image; } +.@{fa-css-prefix}-images:before { content: @fa-var-images; } +.@{fa-css-prefix}-imdb:before { content: @fa-var-imdb; } +.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; } +.@{fa-css-prefix}-indent:before { content: @fa-var-indent; } +.@{fa-css-prefix}-industry:before { content: @fa-var-industry; } +.@{fa-css-prefix}-infinity:before { content: @fa-var-infinity; } +.@{fa-css-prefix}-info:before { content: @fa-var-info; } +.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; } +.@{fa-css-prefix}-innosoft:before { content: @fa-var-innosoft; } +.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; } +.@{fa-css-prefix}-instagram-square:before { content: @fa-var-instagram-square; } +.@{fa-css-prefix}-instalod:before { content: @fa-var-instalod; } +.@{fa-css-prefix}-intercom:before { content: @fa-var-intercom; } +.@{fa-css-prefix}-internet-explorer:before { content: @fa-var-internet-explorer; } +.@{fa-css-prefix}-invision:before { content: @fa-var-invision; } +.@{fa-css-prefix}-ioxhost:before { content: @fa-var-ioxhost; } +.@{fa-css-prefix}-italic:before { content: @fa-var-italic; } +.@{fa-css-prefix}-itch-io:before { content: @fa-var-itch-io; } +.@{fa-css-prefix}-itunes:before { content: @fa-var-itunes; } +.@{fa-css-prefix}-itunes-note:before { content: @fa-var-itunes-note; } +.@{fa-css-prefix}-java:before { content: @fa-var-java; } +.@{fa-css-prefix}-jedi:before { content: @fa-var-jedi; } +.@{fa-css-prefix}-jedi-order:before { content: @fa-var-jedi-order; } +.@{fa-css-prefix}-jenkins:before { content: @fa-var-jenkins; } +.@{fa-css-prefix}-jira:before { content: @fa-var-jira; } +.@{fa-css-prefix}-joget:before { content: @fa-var-joget; } +.@{fa-css-prefix}-joint:before { content: @fa-var-joint; } +.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } +.@{fa-css-prefix}-journal-whills:before { content: @fa-var-journal-whills; } +.@{fa-css-prefix}-js:before { content: @fa-var-js; } +.@{fa-css-prefix}-js-square:before { content: @fa-var-js-square; } +.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; } +.@{fa-css-prefix}-kaaba:before { content: @fa-var-kaaba; } +.@{fa-css-prefix}-kaggle:before { content: @fa-var-kaggle; } +.@{fa-css-prefix}-key:before { content: @fa-var-key; } +.@{fa-css-prefix}-keybase:before { content: @fa-var-keybase; } +.@{fa-css-prefix}-keyboard:before { content: @fa-var-keyboard; } +.@{fa-css-prefix}-keycdn:before { content: @fa-var-keycdn; } +.@{fa-css-prefix}-khanda:before { content: @fa-var-khanda; } +.@{fa-css-prefix}-kickstarter:before { content: @fa-var-kickstarter; } +.@{fa-css-prefix}-kickstarter-k:before { content: @fa-var-kickstarter-k; } +.@{fa-css-prefix}-kiss:before { content: @fa-var-kiss; } +.@{fa-css-prefix}-kiss-beam:before { content: @fa-var-kiss-beam; } +.@{fa-css-prefix}-kiss-wink-heart:before { content: @fa-var-kiss-wink-heart; } +.@{fa-css-prefix}-kiwi-bird:before { content: @fa-var-kiwi-bird; } +.@{fa-css-prefix}-korvue:before { content: @fa-var-korvue; } +.@{fa-css-prefix}-landmark:before { content: @fa-var-landmark; } +.@{fa-css-prefix}-language:before { content: @fa-var-language; } +.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; } +.@{fa-css-prefix}-laptop-code:before { content: @fa-var-laptop-code; } +.@{fa-css-prefix}-laptop-house:before { content: @fa-var-laptop-house; } +.@{fa-css-prefix}-laptop-medical:before { content: @fa-var-laptop-medical; } +.@{fa-css-prefix}-laravel:before { content: @fa-var-laravel; } +.@{fa-css-prefix}-lastfm:before { content: @fa-var-lastfm; } +.@{fa-css-prefix}-lastfm-square:before { content: @fa-var-lastfm-square; } +.@{fa-css-prefix}-laugh:before { content: @fa-var-laugh; } +.@{fa-css-prefix}-laugh-beam:before { content: @fa-var-laugh-beam; } +.@{fa-css-prefix}-laugh-squint:before { content: @fa-var-laugh-squint; } +.@{fa-css-prefix}-laugh-wink:before { content: @fa-var-laugh-wink; } +.@{fa-css-prefix}-layer-group:before { content: @fa-var-layer-group; } +.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; } +.@{fa-css-prefix}-leanpub:before { content: @fa-var-leanpub; } +.@{fa-css-prefix}-lemon:before { content: @fa-var-lemon; } +.@{fa-css-prefix}-less:before { content: @fa-var-less; } +.@{fa-css-prefix}-less-than:before { content: @fa-var-less-than; } +.@{fa-css-prefix}-less-than-equal:before { content: @fa-var-less-than-equal; } +.@{fa-css-prefix}-level-down-alt:before { content: @fa-var-level-down-alt; } +.@{fa-css-prefix}-level-up-alt:before { content: @fa-var-level-up-alt; } +.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } +.@{fa-css-prefix}-lightbulb:before { content: @fa-var-lightbulb; } +.@{fa-css-prefix}-line:before { content: @fa-var-line; } +.@{fa-css-prefix}-link:before { content: @fa-var-link; } +.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; } +.@{fa-css-prefix}-linkedin-in:before { content: @fa-var-linkedin-in; } +.@{fa-css-prefix}-linode:before { content: @fa-var-linode; } +.@{fa-css-prefix}-linux:before { content: @fa-var-linux; } +.@{fa-css-prefix}-lira-sign:before { content: @fa-var-lira-sign; } +.@{fa-css-prefix}-list:before { content: @fa-var-list; } +.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; } +.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; } +.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; } +.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; } +.@{fa-css-prefix}-lock:before { content: @fa-var-lock; } +.@{fa-css-prefix}-lock-open:before { content: @fa-var-lock-open; } +.@{fa-css-prefix}-long-arrow-alt-down:before { content: @fa-var-long-arrow-alt-down; } +.@{fa-css-prefix}-long-arrow-alt-left:before { content: @fa-var-long-arrow-alt-left; } +.@{fa-css-prefix}-long-arrow-alt-right:before { content: @fa-var-long-arrow-alt-right; } +.@{fa-css-prefix}-long-arrow-alt-up:before { content: @fa-var-long-arrow-alt-up; } +.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; } +.@{fa-css-prefix}-luggage-cart:before { content: @fa-var-luggage-cart; } +.@{fa-css-prefix}-lungs:before { content: @fa-var-lungs; } +.@{fa-css-prefix}-lungs-virus:before { content: @fa-var-lungs-virus; } +.@{fa-css-prefix}-lyft:before { content: @fa-var-lyft; } +.@{fa-css-prefix}-magento:before { content: @fa-var-magento; } +.@{fa-css-prefix}-magic:before { content: @fa-var-magic; } +.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; } +.@{fa-css-prefix}-mail-bulk:before { content: @fa-var-mail-bulk; } +.@{fa-css-prefix}-mailchimp:before { content: @fa-var-mailchimp; } +.@{fa-css-prefix}-male:before { content: @fa-var-male; } +.@{fa-css-prefix}-mandalorian:before { content: @fa-var-mandalorian; } +.@{fa-css-prefix}-map:before { content: @fa-var-map; } +.@{fa-css-prefix}-map-marked:before { content: @fa-var-map-marked; } +.@{fa-css-prefix}-map-marked-alt:before { content: @fa-var-map-marked-alt; } +.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; } +.@{fa-css-prefix}-map-marker-alt:before { content: @fa-var-map-marker-alt; } +.@{fa-css-prefix}-map-pin:before { content: @fa-var-map-pin; } +.@{fa-css-prefix}-map-signs:before { content: @fa-var-map-signs; } +.@{fa-css-prefix}-markdown:before { content: @fa-var-markdown; } +.@{fa-css-prefix}-marker:before { content: @fa-var-marker; } +.@{fa-css-prefix}-mars:before { content: @fa-var-mars; } +.@{fa-css-prefix}-mars-double:before { content: @fa-var-mars-double; } +.@{fa-css-prefix}-mars-stroke:before { content: @fa-var-mars-stroke; } +.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; } +.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; } +.@{fa-css-prefix}-mask:before { content: @fa-var-mask; } +.@{fa-css-prefix}-mastodon:before { content: @fa-var-mastodon; } +.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; } +.@{fa-css-prefix}-mdb:before { content: @fa-var-mdb; } +.@{fa-css-prefix}-medal:before { content: @fa-var-medal; } +.@{fa-css-prefix}-medapps:before { content: @fa-var-medapps; } +.@{fa-css-prefix}-medium:before { content: @fa-var-medium; } +.@{fa-css-prefix}-medium-m:before { content: @fa-var-medium-m; } +.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; } +.@{fa-css-prefix}-medrt:before { content: @fa-var-medrt; } +.@{fa-css-prefix}-meetup:before { content: @fa-var-meetup; } +.@{fa-css-prefix}-megaport:before { content: @fa-var-megaport; } +.@{fa-css-prefix}-meh:before { content: @fa-var-meh; } +.@{fa-css-prefix}-meh-blank:before { content: @fa-var-meh-blank; } +.@{fa-css-prefix}-meh-rolling-eyes:before { content: @fa-var-meh-rolling-eyes; } +.@{fa-css-prefix}-memory:before { content: @fa-var-memory; } +.@{fa-css-prefix}-mendeley:before { content: @fa-var-mendeley; } +.@{fa-css-prefix}-menorah:before { content: @fa-var-menorah; } +.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; } +.@{fa-css-prefix}-meteor:before { content: @fa-var-meteor; } +.@{fa-css-prefix}-microblog:before { content: @fa-var-microblog; } +.@{fa-css-prefix}-microchip:before { content: @fa-var-microchip; } +.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; } +.@{fa-css-prefix}-microphone-alt:before { content: @fa-var-microphone-alt; } +.@{fa-css-prefix}-microphone-alt-slash:before { content: @fa-var-microphone-alt-slash; } +.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; } +.@{fa-css-prefix}-microscope:before { content: @fa-var-microscope; } +.@{fa-css-prefix}-microsoft:before { content: @fa-var-microsoft; } +.@{fa-css-prefix}-minus:before { content: @fa-var-minus; } +.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; } +.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; } +.@{fa-css-prefix}-mitten:before { content: @fa-var-mitten; } +.@{fa-css-prefix}-mix:before { content: @fa-var-mix; } +.@{fa-css-prefix}-mixcloud:before { content: @fa-var-mixcloud; } +.@{fa-css-prefix}-mixer:before { content: @fa-var-mixer; } +.@{fa-css-prefix}-mizuni:before { content: @fa-var-mizuni; } +.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; } +.@{fa-css-prefix}-mobile-alt:before { content: @fa-var-mobile-alt; } +.@{fa-css-prefix}-modx:before { content: @fa-var-modx; } +.@{fa-css-prefix}-monero:before { content: @fa-var-monero; } +.@{fa-css-prefix}-money-bill:before { content: @fa-var-money-bill; } +.@{fa-css-prefix}-money-bill-alt:before { content: @fa-var-money-bill-alt; } +.@{fa-css-prefix}-money-bill-wave:before { content: @fa-var-money-bill-wave; } +.@{fa-css-prefix}-money-bill-wave-alt:before { content: @fa-var-money-bill-wave-alt; } +.@{fa-css-prefix}-money-check:before { content: @fa-var-money-check; } +.@{fa-css-prefix}-money-check-alt:before { content: @fa-var-money-check-alt; } +.@{fa-css-prefix}-monument:before { content: @fa-var-monument; } +.@{fa-css-prefix}-moon:before { content: @fa-var-moon; } +.@{fa-css-prefix}-mortar-pestle:before { content: @fa-var-mortar-pestle; } +.@{fa-css-prefix}-mosque:before { content: @fa-var-mosque; } +.@{fa-css-prefix}-motorcycle:before { content: @fa-var-motorcycle; } +.@{fa-css-prefix}-mountain:before { content: @fa-var-mountain; } +.@{fa-css-prefix}-mouse:before { content: @fa-var-mouse; } +.@{fa-css-prefix}-mouse-pointer:before { content: @fa-var-mouse-pointer; } +.@{fa-css-prefix}-mug-hot:before { content: @fa-var-mug-hot; } +.@{fa-css-prefix}-music:before { content: @fa-var-music; } +.@{fa-css-prefix}-napster:before { content: @fa-var-napster; } +.@{fa-css-prefix}-neos:before { content: @fa-var-neos; } +.@{fa-css-prefix}-network-wired:before { content: @fa-var-network-wired; } +.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; } +.@{fa-css-prefix}-newspaper:before { content: @fa-var-newspaper; } +.@{fa-css-prefix}-nimblr:before { content: @fa-var-nimblr; } +.@{fa-css-prefix}-node:before { content: @fa-var-node; } +.@{fa-css-prefix}-node-js:before { content: @fa-var-node-js; } +.@{fa-css-prefix}-not-equal:before { content: @fa-var-not-equal; } +.@{fa-css-prefix}-notes-medical:before { content: @fa-var-notes-medical; } +.@{fa-css-prefix}-npm:before { content: @fa-var-npm; } +.@{fa-css-prefix}-ns8:before { content: @fa-var-ns8; } +.@{fa-css-prefix}-nutritionix:before { content: @fa-var-nutritionix; } +.@{fa-css-prefix}-object-group:before { content: @fa-var-object-group; } +.@{fa-css-prefix}-object-ungroup:before { content: @fa-var-object-ungroup; } +.@{fa-css-prefix}-octopus-deploy:before { content: @fa-var-octopus-deploy; } +.@{fa-css-prefix}-odnoklassniki:before { content: @fa-var-odnoklassniki; } +.@{fa-css-prefix}-odnoklassniki-square:before { content: @fa-var-odnoklassniki-square; } +.@{fa-css-prefix}-oil-can:before { content: @fa-var-oil-can; } +.@{fa-css-prefix}-old-republic:before { content: @fa-var-old-republic; } +.@{fa-css-prefix}-om:before { content: @fa-var-om; } +.@{fa-css-prefix}-opencart:before { content: @fa-var-opencart; } +.@{fa-css-prefix}-openid:before { content: @fa-var-openid; } +.@{fa-css-prefix}-opera:before { content: @fa-var-opera; } +.@{fa-css-prefix}-optin-monster:before { content: @fa-var-optin-monster; } +.@{fa-css-prefix}-orcid:before { content: @fa-var-orcid; } +.@{fa-css-prefix}-osi:before { content: @fa-var-osi; } +.@{fa-css-prefix}-otter:before { content: @fa-var-otter; } +.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; } +.@{fa-css-prefix}-page4:before { content: @fa-var-page4; } +.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; } +.@{fa-css-prefix}-pager:before { content: @fa-var-pager; } +.@{fa-css-prefix}-paint-brush:before { content: @fa-var-paint-brush; } +.@{fa-css-prefix}-paint-roller:before { content: @fa-var-paint-roller; } +.@{fa-css-prefix}-palette:before { content: @fa-var-palette; } +.@{fa-css-prefix}-palfed:before { content: @fa-var-palfed; } +.@{fa-css-prefix}-pallet:before { content: @fa-var-pallet; } +.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; } +.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; } +.@{fa-css-prefix}-parachute-box:before { content: @fa-var-parachute-box; } +.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } +.@{fa-css-prefix}-parking:before { content: @fa-var-parking; } +.@{fa-css-prefix}-passport:before { content: @fa-var-passport; } +.@{fa-css-prefix}-pastafarianism:before { content: @fa-var-pastafarianism; } +.@{fa-css-prefix}-paste:before { content: @fa-var-paste; } +.@{fa-css-prefix}-patreon:before { content: @fa-var-patreon; } +.@{fa-css-prefix}-pause:before { content: @fa-var-pause; } +.@{fa-css-prefix}-pause-circle:before { content: @fa-var-pause-circle; } +.@{fa-css-prefix}-paw:before { content: @fa-var-paw; } +.@{fa-css-prefix}-paypal:before { content: @fa-var-paypal; } +.@{fa-css-prefix}-peace:before { content: @fa-var-peace; } +.@{fa-css-prefix}-pen:before { content: @fa-var-pen; } +.@{fa-css-prefix}-pen-alt:before { content: @fa-var-pen-alt; } +.@{fa-css-prefix}-pen-fancy:before { content: @fa-var-pen-fancy; } +.@{fa-css-prefix}-pen-nib:before { content: @fa-var-pen-nib; } +.@{fa-css-prefix}-pen-square:before { content: @fa-var-pen-square; } +.@{fa-css-prefix}-pencil-alt:before { content: @fa-var-pencil-alt; } +.@{fa-css-prefix}-pencil-ruler:before { content: @fa-var-pencil-ruler; } +.@{fa-css-prefix}-penny-arcade:before { content: @fa-var-penny-arcade; } +.@{fa-css-prefix}-people-arrows:before { content: @fa-var-people-arrows; } +.@{fa-css-prefix}-people-carry:before { content: @fa-var-people-carry; } +.@{fa-css-prefix}-pepper-hot:before { content: @fa-var-pepper-hot; } +.@{fa-css-prefix}-perbyte:before { content: @fa-var-perbyte; } +.@{fa-css-prefix}-percent:before { content: @fa-var-percent; } +.@{fa-css-prefix}-percentage:before { content: @fa-var-percentage; } +.@{fa-css-prefix}-periscope:before { content: @fa-var-periscope; } +.@{fa-css-prefix}-person-booth:before { content: @fa-var-person-booth; } +.@{fa-css-prefix}-phabricator:before { content: @fa-var-phabricator; } +.@{fa-css-prefix}-phoenix-framework:before { content: @fa-var-phoenix-framework; } +.@{fa-css-prefix}-phoenix-squadron:before { content: @fa-var-phoenix-squadron; } +.@{fa-css-prefix}-phone:before { content: @fa-var-phone; } +.@{fa-css-prefix}-phone-alt:before { content: @fa-var-phone-alt; } +.@{fa-css-prefix}-phone-slash:before { content: @fa-var-phone-slash; } +.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; } +.@{fa-css-prefix}-phone-square-alt:before { content: @fa-var-phone-square-alt; } +.@{fa-css-prefix}-phone-volume:before { content: @fa-var-phone-volume; } +.@{fa-css-prefix}-photo-video:before { content: @fa-var-photo-video; } +.@{fa-css-prefix}-php:before { content: @fa-var-php; } +.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } +.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } +.@{fa-css-prefix}-pied-piper-hat:before { content: @fa-var-pied-piper-hat; } +.@{fa-css-prefix}-pied-piper-pp:before { content: @fa-var-pied-piper-pp; } +.@{fa-css-prefix}-pied-piper-square:before { content: @fa-var-pied-piper-square; } +.@{fa-css-prefix}-piggy-bank:before { content: @fa-var-piggy-bank; } +.@{fa-css-prefix}-pills:before { content: @fa-var-pills; } +.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; } +.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; } +.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; } +.@{fa-css-prefix}-pizza-slice:before { content: @fa-var-pizza-slice; } +.@{fa-css-prefix}-place-of-worship:before { content: @fa-var-place-of-worship; } +.@{fa-css-prefix}-plane:before { content: @fa-var-plane; } +.@{fa-css-prefix}-plane-arrival:before { content: @fa-var-plane-arrival; } +.@{fa-css-prefix}-plane-departure:before { content: @fa-var-plane-departure; } +.@{fa-css-prefix}-plane-slash:before { content: @fa-var-plane-slash; } +.@{fa-css-prefix}-play:before { content: @fa-var-play; } +.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; } +.@{fa-css-prefix}-playstation:before { content: @fa-var-playstation; } +.@{fa-css-prefix}-plug:before { content: @fa-var-plug; } +.@{fa-css-prefix}-plus:before { content: @fa-var-plus; } +.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; } +.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; } +.@{fa-css-prefix}-podcast:before { content: @fa-var-podcast; } +.@{fa-css-prefix}-poll:before { content: @fa-var-poll; } +.@{fa-css-prefix}-poll-h:before { content: @fa-var-poll-h; } +.@{fa-css-prefix}-poo:before { content: @fa-var-poo; } +.@{fa-css-prefix}-poo-storm:before { content: @fa-var-poo-storm; } +.@{fa-css-prefix}-poop:before { content: @fa-var-poop; } +.@{fa-css-prefix}-portrait:before { content: @fa-var-portrait; } +.@{fa-css-prefix}-pound-sign:before { content: @fa-var-pound-sign; } +.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; } +.@{fa-css-prefix}-pray:before { content: @fa-var-pray; } +.@{fa-css-prefix}-praying-hands:before { content: @fa-var-praying-hands; } +.@{fa-css-prefix}-prescription:before { content: @fa-var-prescription; } +.@{fa-css-prefix}-prescription-bottle:before { content: @fa-var-prescription-bottle; } +.@{fa-css-prefix}-prescription-bottle-alt:before { content: @fa-var-prescription-bottle-alt; } +.@{fa-css-prefix}-print:before { content: @fa-var-print; } +.@{fa-css-prefix}-procedures:before { content: @fa-var-procedures; } +.@{fa-css-prefix}-product-hunt:before { content: @fa-var-product-hunt; } +.@{fa-css-prefix}-project-diagram:before { content: @fa-var-project-diagram; } +.@{fa-css-prefix}-pump-medical:before { content: @fa-var-pump-medical; } +.@{fa-css-prefix}-pump-soap:before { content: @fa-var-pump-soap; } +.@{fa-css-prefix}-pushed:before { content: @fa-var-pushed; } +.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; } +.@{fa-css-prefix}-python:before { content: @fa-var-python; } +.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } +.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; } +.@{fa-css-prefix}-question:before { content: @fa-var-question; } +.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; } +.@{fa-css-prefix}-quidditch:before { content: @fa-var-quidditch; } +.@{fa-css-prefix}-quinscape:before { content: @fa-var-quinscape; } +.@{fa-css-prefix}-quora:before { content: @fa-var-quora; } +.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; } +.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; } +.@{fa-css-prefix}-quran:before { content: @fa-var-quran; } +.@{fa-css-prefix}-r-project:before { content: @fa-var-r-project; } +.@{fa-css-prefix}-radiation:before { content: @fa-var-radiation; } +.@{fa-css-prefix}-radiation-alt:before { content: @fa-var-radiation-alt; } +.@{fa-css-prefix}-rainbow:before { content: @fa-var-rainbow; } +.@{fa-css-prefix}-random:before { content: @fa-var-random; } +.@{fa-css-prefix}-raspberry-pi:before { content: @fa-var-raspberry-pi; } +.@{fa-css-prefix}-ravelry:before { content: @fa-var-ravelry; } +.@{fa-css-prefix}-react:before { content: @fa-var-react; } +.@{fa-css-prefix}-reacteurope:before { content: @fa-var-reacteurope; } +.@{fa-css-prefix}-readme:before { content: @fa-var-readme; } +.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } +.@{fa-css-prefix}-receipt:before { content: @fa-var-receipt; } +.@{fa-css-prefix}-record-vinyl:before { content: @fa-var-record-vinyl; } +.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; } +.@{fa-css-prefix}-red-river:before { content: @fa-var-red-river; } +.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; } +.@{fa-css-prefix}-reddit-alien:before { content: @fa-var-reddit-alien; } +.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; } +.@{fa-css-prefix}-redhat:before { content: @fa-var-redhat; } +.@{fa-css-prefix}-redo:before { content: @fa-var-redo; } +.@{fa-css-prefix}-redo-alt:before { content: @fa-var-redo-alt; } +.@{fa-css-prefix}-registered:before { content: @fa-var-registered; } +.@{fa-css-prefix}-remove-format:before { content: @fa-var-remove-format; } +.@{fa-css-prefix}-renren:before { content: @fa-var-renren; } +.@{fa-css-prefix}-reply:before { content: @fa-var-reply; } +.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; } +.@{fa-css-prefix}-replyd:before { content: @fa-var-replyd; } +.@{fa-css-prefix}-republican:before { content: @fa-var-republican; } +.@{fa-css-prefix}-researchgate:before { content: @fa-var-researchgate; } +.@{fa-css-prefix}-resolving:before { content: @fa-var-resolving; } +.@{fa-css-prefix}-restroom:before { content: @fa-var-restroom; } +.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; } +.@{fa-css-prefix}-rev:before { content: @fa-var-rev; } +.@{fa-css-prefix}-ribbon:before { content: @fa-var-ribbon; } +.@{fa-css-prefix}-ring:before { content: @fa-var-ring; } +.@{fa-css-prefix}-road:before { content: @fa-var-road; } +.@{fa-css-prefix}-robot:before { content: @fa-var-robot; } +.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; } +.@{fa-css-prefix}-rocketchat:before { content: @fa-var-rocketchat; } +.@{fa-css-prefix}-rockrms:before { content: @fa-var-rockrms; } +.@{fa-css-prefix}-route:before { content: @fa-var-route; } +.@{fa-css-prefix}-rss:before { content: @fa-var-rss; } +.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; } +.@{fa-css-prefix}-ruble-sign:before { content: @fa-var-ruble-sign; } +.@{fa-css-prefix}-ruler:before { content: @fa-var-ruler; } +.@{fa-css-prefix}-ruler-combined:before { content: @fa-var-ruler-combined; } +.@{fa-css-prefix}-ruler-horizontal:before { content: @fa-var-ruler-horizontal; } +.@{fa-css-prefix}-ruler-vertical:before { content: @fa-var-ruler-vertical; } +.@{fa-css-prefix}-running:before { content: @fa-var-running; } +.@{fa-css-prefix}-rupee-sign:before { content: @fa-var-rupee-sign; } +.@{fa-css-prefix}-rust:before { content: @fa-var-rust; } +.@{fa-css-prefix}-sad-cry:before { content: @fa-var-sad-cry; } +.@{fa-css-prefix}-sad-tear:before { content: @fa-var-sad-tear; } +.@{fa-css-prefix}-safari:before { content: @fa-var-safari; } +.@{fa-css-prefix}-salesforce:before { content: @fa-var-salesforce; } +.@{fa-css-prefix}-sass:before { content: @fa-var-sass; } +.@{fa-css-prefix}-satellite:before { content: @fa-var-satellite; } +.@{fa-css-prefix}-satellite-dish:before { content: @fa-var-satellite-dish; } +.@{fa-css-prefix}-save:before { content: @fa-var-save; } +.@{fa-css-prefix}-schlix:before { content: @fa-var-schlix; } +.@{fa-css-prefix}-school:before { content: @fa-var-school; } +.@{fa-css-prefix}-screwdriver:before { content: @fa-var-screwdriver; } +.@{fa-css-prefix}-scribd:before { content: @fa-var-scribd; } +.@{fa-css-prefix}-scroll:before { content: @fa-var-scroll; } +.@{fa-css-prefix}-sd-card:before { content: @fa-var-sd-card; } +.@{fa-css-prefix}-search:before { content: @fa-var-search; } +.@{fa-css-prefix}-search-dollar:before { content: @fa-var-search-dollar; } +.@{fa-css-prefix}-search-location:before { content: @fa-var-search-location; } +.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; } +.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; } +.@{fa-css-prefix}-searchengin:before { content: @fa-var-searchengin; } +.@{fa-css-prefix}-seedling:before { content: @fa-var-seedling; } +.@{fa-css-prefix}-sellcast:before { content: @fa-var-sellcast; } +.@{fa-css-prefix}-sellsy:before { content: @fa-var-sellsy; } +.@{fa-css-prefix}-server:before { content: @fa-var-server; } +.@{fa-css-prefix}-servicestack:before { content: @fa-var-servicestack; } +.@{fa-css-prefix}-shapes:before { content: @fa-var-shapes; } +.@{fa-css-prefix}-share:before { content: @fa-var-share; } +.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; } +.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; } +.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; } +.@{fa-css-prefix}-shekel-sign:before { content: @fa-var-shekel-sign; } +.@{fa-css-prefix}-shield-alt:before { content: @fa-var-shield-alt; } +.@{fa-css-prefix}-shield-virus:before { content: @fa-var-shield-virus; } +.@{fa-css-prefix}-ship:before { content: @fa-var-ship; } +.@{fa-css-prefix}-shipping-fast:before { content: @fa-var-shipping-fast; } +.@{fa-css-prefix}-shirtsinbulk:before { content: @fa-var-shirtsinbulk; } +.@{fa-css-prefix}-shoe-prints:before { content: @fa-var-shoe-prints; } +.@{fa-css-prefix}-shopify:before { content: @fa-var-shopify; } +.@{fa-css-prefix}-shopping-bag:before { content: @fa-var-shopping-bag; } +.@{fa-css-prefix}-shopping-basket:before { content: @fa-var-shopping-basket; } +.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; } +.@{fa-css-prefix}-shopware:before { content: @fa-var-shopware; } +.@{fa-css-prefix}-shower:before { content: @fa-var-shower; } +.@{fa-css-prefix}-shuttle-van:before { content: @fa-var-shuttle-van; } +.@{fa-css-prefix}-sign:before { content: @fa-var-sign; } +.@{fa-css-prefix}-sign-in-alt:before { content: @fa-var-sign-in-alt; } +.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; } +.@{fa-css-prefix}-sign-out-alt:before { content: @fa-var-sign-out-alt; } +.@{fa-css-prefix}-signal:before { content: @fa-var-signal; } +.@{fa-css-prefix}-signature:before { content: @fa-var-signature; } +.@{fa-css-prefix}-sim-card:before { content: @fa-var-sim-card; } +.@{fa-css-prefix}-simplybuilt:before { content: @fa-var-simplybuilt; } +.@{fa-css-prefix}-sink:before { content: @fa-var-sink; } +.@{fa-css-prefix}-sistrix:before { content: @fa-var-sistrix; } +.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; } +.@{fa-css-prefix}-sith:before { content: @fa-var-sith; } +.@{fa-css-prefix}-skating:before { content: @fa-var-skating; } +.@{fa-css-prefix}-sketch:before { content: @fa-var-sketch; } +.@{fa-css-prefix}-skiing:before { content: @fa-var-skiing; } +.@{fa-css-prefix}-skiing-nordic:before { content: @fa-var-skiing-nordic; } +.@{fa-css-prefix}-skull:before { content: @fa-var-skull; } +.@{fa-css-prefix}-skull-crossbones:before { content: @fa-var-skull-crossbones; } +.@{fa-css-prefix}-skyatlas:before { content: @fa-var-skyatlas; } +.@{fa-css-prefix}-skype:before { content: @fa-var-skype; } +.@{fa-css-prefix}-slack:before { content: @fa-var-slack; } +.@{fa-css-prefix}-slack-hash:before { content: @fa-var-slack-hash; } +.@{fa-css-prefix}-slash:before { content: @fa-var-slash; } +.@{fa-css-prefix}-sleigh:before { content: @fa-var-sleigh; } +.@{fa-css-prefix}-sliders-h:before { content: @fa-var-sliders-h; } +.@{fa-css-prefix}-slideshare:before { content: @fa-var-slideshare; } +.@{fa-css-prefix}-smile:before { content: @fa-var-smile; } +.@{fa-css-prefix}-smile-beam:before { content: @fa-var-smile-beam; } +.@{fa-css-prefix}-smile-wink:before { content: @fa-var-smile-wink; } +.@{fa-css-prefix}-smog:before { content: @fa-var-smog; } +.@{fa-css-prefix}-smoking:before { content: @fa-var-smoking; } +.@{fa-css-prefix}-smoking-ban:before { content: @fa-var-smoking-ban; } +.@{fa-css-prefix}-sms:before { content: @fa-var-sms; } +.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; } +.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; } +.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; } +.@{fa-css-prefix}-snowboarding:before { content: @fa-var-snowboarding; } +.@{fa-css-prefix}-snowflake:before { content: @fa-var-snowflake; } +.@{fa-css-prefix}-snowman:before { content: @fa-var-snowman; } +.@{fa-css-prefix}-snowplow:before { content: @fa-var-snowplow; } +.@{fa-css-prefix}-soap:before { content: @fa-var-soap; } +.@{fa-css-prefix}-socks:before { content: @fa-var-socks; } +.@{fa-css-prefix}-solar-panel:before { content: @fa-var-solar-panel; } +.@{fa-css-prefix}-sort:before { content: @fa-var-sort; } +.@{fa-css-prefix}-sort-alpha-down:before { content: @fa-var-sort-alpha-down; } +.@{fa-css-prefix}-sort-alpha-down-alt:before { content: @fa-var-sort-alpha-down-alt; } +.@{fa-css-prefix}-sort-alpha-up:before { content: @fa-var-sort-alpha-up; } +.@{fa-css-prefix}-sort-alpha-up-alt:before { content: @fa-var-sort-alpha-up-alt; } +.@{fa-css-prefix}-sort-amount-down:before { content: @fa-var-sort-amount-down; } +.@{fa-css-prefix}-sort-amount-down-alt:before { content: @fa-var-sort-amount-down-alt; } +.@{fa-css-prefix}-sort-amount-up:before { content: @fa-var-sort-amount-up; } +.@{fa-css-prefix}-sort-amount-up-alt:before { content: @fa-var-sort-amount-up-alt; } +.@{fa-css-prefix}-sort-down:before { content: @fa-var-sort-down; } +.@{fa-css-prefix}-sort-numeric-down:before { content: @fa-var-sort-numeric-down; } +.@{fa-css-prefix}-sort-numeric-down-alt:before { content: @fa-var-sort-numeric-down-alt; } +.@{fa-css-prefix}-sort-numeric-up:before { content: @fa-var-sort-numeric-up; } +.@{fa-css-prefix}-sort-numeric-up-alt:before { content: @fa-var-sort-numeric-up-alt; } +.@{fa-css-prefix}-sort-up:before { content: @fa-var-sort-up; } +.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; } +.@{fa-css-prefix}-sourcetree:before { content: @fa-var-sourcetree; } +.@{fa-css-prefix}-spa:before { content: @fa-var-spa; } +.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; } +.@{fa-css-prefix}-speakap:before { content: @fa-var-speakap; } +.@{fa-css-prefix}-speaker-deck:before { content: @fa-var-speaker-deck; } +.@{fa-css-prefix}-spell-check:before { content: @fa-var-spell-check; } +.@{fa-css-prefix}-spider:before { content: @fa-var-spider; } +.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; } +.@{fa-css-prefix}-splotch:before { content: @fa-var-splotch; } +.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; } +.@{fa-css-prefix}-spray-can:before { content: @fa-var-spray-can; } +.@{fa-css-prefix}-square:before { content: @fa-var-square; } +.@{fa-css-prefix}-square-full:before { content: @fa-var-square-full; } +.@{fa-css-prefix}-square-root-alt:before { content: @fa-var-square-root-alt; } +.@{fa-css-prefix}-squarespace:before { content: @fa-var-squarespace; } +.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; } +.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; } +.@{fa-css-prefix}-stackpath:before { content: @fa-var-stackpath; } +.@{fa-css-prefix}-stamp:before { content: @fa-var-stamp; } +.@{fa-css-prefix}-star:before { content: @fa-var-star; } +.@{fa-css-prefix}-star-and-crescent:before { content: @fa-var-star-and-crescent; } +.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; } +.@{fa-css-prefix}-star-half-alt:before { content: @fa-var-star-half-alt; } +.@{fa-css-prefix}-star-of-david:before { content: @fa-var-star-of-david; } +.@{fa-css-prefix}-star-of-life:before { content: @fa-var-star-of-life; } +.@{fa-css-prefix}-staylinked:before { content: @fa-var-staylinked; } +.@{fa-css-prefix}-steam:before { content: @fa-var-steam; } +.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; } +.@{fa-css-prefix}-steam-symbol:before { content: @fa-var-steam-symbol; } +.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; } +.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; } +.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; } +.@{fa-css-prefix}-sticker-mule:before { content: @fa-var-sticker-mule; } +.@{fa-css-prefix}-sticky-note:before { content: @fa-var-sticky-note; } +.@{fa-css-prefix}-stop:before { content: @fa-var-stop; } +.@{fa-css-prefix}-stop-circle:before { content: @fa-var-stop-circle; } +.@{fa-css-prefix}-stopwatch:before { content: @fa-var-stopwatch; } +.@{fa-css-prefix}-stopwatch-20:before { content: @fa-var-stopwatch-20; } +.@{fa-css-prefix}-store:before { content: @fa-var-store; } +.@{fa-css-prefix}-store-alt:before { content: @fa-var-store-alt; } +.@{fa-css-prefix}-store-alt-slash:before { content: @fa-var-store-alt-slash; } +.@{fa-css-prefix}-store-slash:before { content: @fa-var-store-slash; } +.@{fa-css-prefix}-strava:before { content: @fa-var-strava; } +.@{fa-css-prefix}-stream:before { content: @fa-var-stream; } +.@{fa-css-prefix}-street-view:before { content: @fa-var-street-view; } +.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; } +.@{fa-css-prefix}-stripe:before { content: @fa-var-stripe; } +.@{fa-css-prefix}-stripe-s:before { content: @fa-var-stripe-s; } +.@{fa-css-prefix}-stroopwafel:before { content: @fa-var-stroopwafel; } +.@{fa-css-prefix}-studiovinari:before { content: @fa-var-studiovinari; } +.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } +.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; } +.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; } +.@{fa-css-prefix}-subway:before { content: @fa-var-subway; } +.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; } +.@{fa-css-prefix}-suitcase-rolling:before { content: @fa-var-suitcase-rolling; } +.@{fa-css-prefix}-sun:before { content: @fa-var-sun; } +.@{fa-css-prefix}-superpowers:before { content: @fa-var-superpowers; } +.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; } +.@{fa-css-prefix}-supple:before { content: @fa-var-supple; } +.@{fa-css-prefix}-surprise:before { content: @fa-var-surprise; } +.@{fa-css-prefix}-suse:before { content: @fa-var-suse; } +.@{fa-css-prefix}-swatchbook:before { content: @fa-var-swatchbook; } +.@{fa-css-prefix}-swift:before { content: @fa-var-swift; } +.@{fa-css-prefix}-swimmer:before { content: @fa-var-swimmer; } +.@{fa-css-prefix}-swimming-pool:before { content: @fa-var-swimming-pool; } +.@{fa-css-prefix}-symfony:before { content: @fa-var-symfony; } +.@{fa-css-prefix}-synagogue:before { content: @fa-var-synagogue; } +.@{fa-css-prefix}-sync:before { content: @fa-var-sync; } +.@{fa-css-prefix}-sync-alt:before { content: @fa-var-sync-alt; } +.@{fa-css-prefix}-syringe:before { content: @fa-var-syringe; } +.@{fa-css-prefix}-table:before { content: @fa-var-table; } +.@{fa-css-prefix}-table-tennis:before { content: @fa-var-table-tennis; } +.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; } +.@{fa-css-prefix}-tablet-alt:before { content: @fa-var-tablet-alt; } +.@{fa-css-prefix}-tablets:before { content: @fa-var-tablets; } +.@{fa-css-prefix}-tachometer-alt:before { content: @fa-var-tachometer-alt; } +.@{fa-css-prefix}-tag:before { content: @fa-var-tag; } +.@{fa-css-prefix}-tags:before { content: @fa-var-tags; } +.@{fa-css-prefix}-tape:before { content: @fa-var-tape; } +.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; } +.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; } +.@{fa-css-prefix}-teamspeak:before { content: @fa-var-teamspeak; } +.@{fa-css-prefix}-teeth:before { content: @fa-var-teeth; } +.@{fa-css-prefix}-teeth-open:before { content: @fa-var-teeth-open; } +.@{fa-css-prefix}-telegram:before { content: @fa-var-telegram; } +.@{fa-css-prefix}-telegram-plane:before { content: @fa-var-telegram-plane; } +.@{fa-css-prefix}-temperature-high:before { content: @fa-var-temperature-high; } +.@{fa-css-prefix}-temperature-low:before { content: @fa-var-temperature-low; } +.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } +.@{fa-css-prefix}-tenge:before { content: @fa-var-tenge; } +.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; } +.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; } +.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; } +.@{fa-css-prefix}-th:before { content: @fa-var-th; } +.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; } +.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; } +.@{fa-css-prefix}-the-red-yeti:before { content: @fa-var-the-red-yeti; } +.@{fa-css-prefix}-theater-masks:before { content: @fa-var-theater-masks; } +.@{fa-css-prefix}-themeco:before { content: @fa-var-themeco; } +.@{fa-css-prefix}-themeisle:before { content: @fa-var-themeisle; } +.@{fa-css-prefix}-thermometer:before { content: @fa-var-thermometer; } +.@{fa-css-prefix}-thermometer-empty:before { content: @fa-var-thermometer-empty; } +.@{fa-css-prefix}-thermometer-full:before { content: @fa-var-thermometer-full; } +.@{fa-css-prefix}-thermometer-half:before { content: @fa-var-thermometer-half; } +.@{fa-css-prefix}-thermometer-quarter:before { content: @fa-var-thermometer-quarter; } +.@{fa-css-prefix}-thermometer-three-quarters:before { content: @fa-var-thermometer-three-quarters; } +.@{fa-css-prefix}-think-peaks:before { content: @fa-var-think-peaks; } +.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; } +.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; } +.@{fa-css-prefix}-thumbtack:before { content: @fa-var-thumbtack; } +.@{fa-css-prefix}-ticket-alt:before { content: @fa-var-ticket-alt; } +.@{fa-css-prefix}-tiktok:before { content: @fa-var-tiktok; } +.@{fa-css-prefix}-times:before { content: @fa-var-times; } +.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; } +.@{fa-css-prefix}-tint:before { content: @fa-var-tint; } +.@{fa-css-prefix}-tint-slash:before { content: @fa-var-tint-slash; } +.@{fa-css-prefix}-tired:before { content: @fa-var-tired; } +.@{fa-css-prefix}-toggle-off:before { content: @fa-var-toggle-off; } +.@{fa-css-prefix}-toggle-on:before { content: @fa-var-toggle-on; } +.@{fa-css-prefix}-toilet:before { content: @fa-var-toilet; } +.@{fa-css-prefix}-toilet-paper:before { content: @fa-var-toilet-paper; } +.@{fa-css-prefix}-toilet-paper-slash:before { content: @fa-var-toilet-paper-slash; } +.@{fa-css-prefix}-toolbox:before { content: @fa-var-toolbox; } +.@{fa-css-prefix}-tools:before { content: @fa-var-tools; } +.@{fa-css-prefix}-tooth:before { content: @fa-var-tooth; } +.@{fa-css-prefix}-torah:before { content: @fa-var-torah; } +.@{fa-css-prefix}-torii-gate:before { content: @fa-var-torii-gate; } +.@{fa-css-prefix}-tractor:before { content: @fa-var-tractor; } +.@{fa-css-prefix}-trade-federation:before { content: @fa-var-trade-federation; } +.@{fa-css-prefix}-trademark:before { content: @fa-var-trademark; } +.@{fa-css-prefix}-traffic-light:before { content: @fa-var-traffic-light; } +.@{fa-css-prefix}-trailer:before { content: @fa-var-trailer; } +.@{fa-css-prefix}-train:before { content: @fa-var-train; } +.@{fa-css-prefix}-tram:before { content: @fa-var-tram; } +.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; } +.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; } +.@{fa-css-prefix}-trash:before { content: @fa-var-trash; } +.@{fa-css-prefix}-trash-alt:before { content: @fa-var-trash-alt; } +.@{fa-css-prefix}-trash-restore:before { content: @fa-var-trash-restore; } +.@{fa-css-prefix}-trash-restore-alt:before { content: @fa-var-trash-restore-alt; } +.@{fa-css-prefix}-tree:before { content: @fa-var-tree; } +.@{fa-css-prefix}-trello:before { content: @fa-var-trello; } +.@{fa-css-prefix}-tripadvisor:before { content: @fa-var-tripadvisor; } +.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; } +.@{fa-css-prefix}-truck:before { content: @fa-var-truck; } +.@{fa-css-prefix}-truck-loading:before { content: @fa-var-truck-loading; } +.@{fa-css-prefix}-truck-monster:before { content: @fa-var-truck-monster; } +.@{fa-css-prefix}-truck-moving:before { content: @fa-var-truck-moving; } +.@{fa-css-prefix}-truck-pickup:before { content: @fa-var-truck-pickup; } +.@{fa-css-prefix}-tshirt:before { content: @fa-var-tshirt; } +.@{fa-css-prefix}-tty:before { content: @fa-var-tty; } +.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; } +.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; } +.@{fa-css-prefix}-tv:before { content: @fa-var-tv; } +.@{fa-css-prefix}-twitch:before { content: @fa-var-twitch; } +.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; } +.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; } +.@{fa-css-prefix}-typo3:before { content: @fa-var-typo3; } +.@{fa-css-prefix}-uber:before { content: @fa-var-uber; } +.@{fa-css-prefix}-ubuntu:before { content: @fa-var-ubuntu; } +.@{fa-css-prefix}-uikit:before { content: @fa-var-uikit; } +.@{fa-css-prefix}-umbraco:before { content: @fa-var-umbraco; } +.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; } +.@{fa-css-prefix}-umbrella-beach:before { content: @fa-var-umbrella-beach; } +.@{fa-css-prefix}-uncharted:before { content: @fa-var-uncharted; } +.@{fa-css-prefix}-underline:before { content: @fa-var-underline; } +.@{fa-css-prefix}-undo:before { content: @fa-var-undo; } +.@{fa-css-prefix}-undo-alt:before { content: @fa-var-undo-alt; } +.@{fa-css-prefix}-uniregistry:before { content: @fa-var-uniregistry; } +.@{fa-css-prefix}-unity:before { content: @fa-var-unity; } +.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; } +.@{fa-css-prefix}-university:before { content: @fa-var-university; } +.@{fa-css-prefix}-unlink:before { content: @fa-var-unlink; } +.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } +.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; } +.@{fa-css-prefix}-unsplash:before { content: @fa-var-unsplash; } +.@{fa-css-prefix}-untappd:before { content: @fa-var-untappd; } +.@{fa-css-prefix}-upload:before { content: @fa-var-upload; } +.@{fa-css-prefix}-ups:before { content: @fa-var-ups; } +.@{fa-css-prefix}-usb:before { content: @fa-var-usb; } +.@{fa-css-prefix}-user:before { content: @fa-var-user; } +.@{fa-css-prefix}-user-alt:before { content: @fa-var-user-alt; } +.@{fa-css-prefix}-user-alt-slash:before { content: @fa-var-user-alt-slash; } +.@{fa-css-prefix}-user-astronaut:before { content: @fa-var-user-astronaut; } +.@{fa-css-prefix}-user-check:before { content: @fa-var-user-check; } +.@{fa-css-prefix}-user-circle:before { content: @fa-var-user-circle; } +.@{fa-css-prefix}-user-clock:before { content: @fa-var-user-clock; } +.@{fa-css-prefix}-user-cog:before { content: @fa-var-user-cog; } +.@{fa-css-prefix}-user-edit:before { content: @fa-var-user-edit; } +.@{fa-css-prefix}-user-friends:before { content: @fa-var-user-friends; } +.@{fa-css-prefix}-user-graduate:before { content: @fa-var-user-graduate; } +.@{fa-css-prefix}-user-injured:before { content: @fa-var-user-injured; } +.@{fa-css-prefix}-user-lock:before { content: @fa-var-user-lock; } +.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; } +.@{fa-css-prefix}-user-minus:before { content: @fa-var-user-minus; } +.@{fa-css-prefix}-user-ninja:before { content: @fa-var-user-ninja; } +.@{fa-css-prefix}-user-nurse:before { content: @fa-var-user-nurse; } +.@{fa-css-prefix}-user-plus:before { content: @fa-var-user-plus; } +.@{fa-css-prefix}-user-secret:before { content: @fa-var-user-secret; } +.@{fa-css-prefix}-user-shield:before { content: @fa-var-user-shield; } +.@{fa-css-prefix}-user-slash:before { content: @fa-var-user-slash; } +.@{fa-css-prefix}-user-tag:before { content: @fa-var-user-tag; } +.@{fa-css-prefix}-user-tie:before { content: @fa-var-user-tie; } +.@{fa-css-prefix}-user-times:before { content: @fa-var-user-times; } +.@{fa-css-prefix}-users:before { content: @fa-var-users; } +.@{fa-css-prefix}-users-cog:before { content: @fa-var-users-cog; } +.@{fa-css-prefix}-users-slash:before { content: @fa-var-users-slash; } +.@{fa-css-prefix}-usps:before { content: @fa-var-usps; } +.@{fa-css-prefix}-ussunnah:before { content: @fa-var-ussunnah; } +.@{fa-css-prefix}-utensil-spoon:before { content: @fa-var-utensil-spoon; } +.@{fa-css-prefix}-utensils:before { content: @fa-var-utensils; } +.@{fa-css-prefix}-vaadin:before { content: @fa-var-vaadin; } +.@{fa-css-prefix}-vector-square:before { content: @fa-var-vector-square; } +.@{fa-css-prefix}-venus:before { content: @fa-var-venus; } +.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; } +.@{fa-css-prefix}-venus-mars:before { content: @fa-var-venus-mars; } +.@{fa-css-prefix}-vest:before { content: @fa-var-vest; } +.@{fa-css-prefix}-vest-patches:before { content: @fa-var-vest-patches; } +.@{fa-css-prefix}-viacoin:before { content: @fa-var-viacoin; } +.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; } +.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; } +.@{fa-css-prefix}-vial:before { content: @fa-var-vial; } +.@{fa-css-prefix}-vials:before { content: @fa-var-vials; } +.@{fa-css-prefix}-viber:before { content: @fa-var-viber; } +.@{fa-css-prefix}-video:before { content: @fa-var-video; } +.@{fa-css-prefix}-video-slash:before { content: @fa-var-video-slash; } +.@{fa-css-prefix}-vihara:before { content: @fa-var-vihara; } +.@{fa-css-prefix}-vimeo:before { content: @fa-var-vimeo; } +.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; } +.@{fa-css-prefix}-vimeo-v:before { content: @fa-var-vimeo-v; } +.@{fa-css-prefix}-vine:before { content: @fa-var-vine; } +.@{fa-css-prefix}-virus:before { content: @fa-var-virus; } +.@{fa-css-prefix}-virus-slash:before { content: @fa-var-virus-slash; } +.@{fa-css-prefix}-viruses:before { content: @fa-var-viruses; } +.@{fa-css-prefix}-vk:before { content: @fa-var-vk; } +.@{fa-css-prefix}-vnv:before { content: @fa-var-vnv; } +.@{fa-css-prefix}-voicemail:before { content: @fa-var-voicemail; } +.@{fa-css-prefix}-volleyball-ball:before { content: @fa-var-volleyball-ball; } +.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; } +.@{fa-css-prefix}-volume-mute:before { content: @fa-var-volume-mute; } +.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; } +.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; } +.@{fa-css-prefix}-vote-yea:before { content: @fa-var-vote-yea; } +.@{fa-css-prefix}-vr-cardboard:before { content: @fa-var-vr-cardboard; } +.@{fa-css-prefix}-vuejs:before { content: @fa-var-vuejs; } +.@{fa-css-prefix}-walking:before { content: @fa-var-walking; } +.@{fa-css-prefix}-wallet:before { content: @fa-var-wallet; } +.@{fa-css-prefix}-warehouse:before { content: @fa-var-warehouse; } +.@{fa-css-prefix}-watchman-monitoring:before { content: @fa-var-watchman-monitoring; } +.@{fa-css-prefix}-water:before { content: @fa-var-water; } +.@{fa-css-prefix}-wave-square:before { content: @fa-var-wave-square; } +.@{fa-css-prefix}-waze:before { content: @fa-var-waze; } +.@{fa-css-prefix}-weebly:before { content: @fa-var-weebly; } +.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; } +.@{fa-css-prefix}-weight:before { content: @fa-var-weight; } +.@{fa-css-prefix}-weight-hanging:before { content: @fa-var-weight-hanging; } +.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; } +.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; } +.@{fa-css-prefix}-whatsapp-square:before { content: @fa-var-whatsapp-square; } +.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; } +.@{fa-css-prefix}-whmcs:before { content: @fa-var-whmcs; } +.@{fa-css-prefix}-wifi:before { content: @fa-var-wifi; } +.@{fa-css-prefix}-wikipedia-w:before { content: @fa-var-wikipedia-w; } +.@{fa-css-prefix}-wind:before { content: @fa-var-wind; } +.@{fa-css-prefix}-window-close:before { content: @fa-var-window-close; } +.@{fa-css-prefix}-window-maximize:before { content: @fa-var-window-maximize; } +.@{fa-css-prefix}-window-minimize:before { content: @fa-var-window-minimize; } +.@{fa-css-prefix}-window-restore:before { content: @fa-var-window-restore; } +.@{fa-css-prefix}-windows:before { content: @fa-var-windows; } +.@{fa-css-prefix}-wine-bottle:before { content: @fa-var-wine-bottle; } +.@{fa-css-prefix}-wine-glass:before { content: @fa-var-wine-glass; } +.@{fa-css-prefix}-wine-glass-alt:before { content: @fa-var-wine-glass-alt; } +.@{fa-css-prefix}-wix:before { content: @fa-var-wix; } +.@{fa-css-prefix}-wizards-of-the-coast:before { content: @fa-var-wizards-of-the-coast; } +.@{fa-css-prefix}-wodu:before { content: @fa-var-wodu; } +.@{fa-css-prefix}-wolf-pack-battalion:before { content: @fa-var-wolf-pack-battalion; } +.@{fa-css-prefix}-won-sign:before { content: @fa-var-won-sign; } +.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; } +.@{fa-css-prefix}-wordpress-simple:before { content: @fa-var-wordpress-simple; } +.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; } +.@{fa-css-prefix}-wpexplorer:before { content: @fa-var-wpexplorer; } +.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; } +.@{fa-css-prefix}-wpressr:before { content: @fa-var-wpressr; } +.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; } +.@{fa-css-prefix}-x-ray:before { content: @fa-var-x-ray; } +.@{fa-css-prefix}-xbox:before { content: @fa-var-xbox; } +.@{fa-css-prefix}-xing:before { content: @fa-var-xing; } +.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; } +.@{fa-css-prefix}-y-combinator:before { content: @fa-var-y-combinator; } +.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; } +.@{fa-css-prefix}-yammer:before { content: @fa-var-yammer; } +.@{fa-css-prefix}-yandex:before { content: @fa-var-yandex; } +.@{fa-css-prefix}-yandex-international:before { content: @fa-var-yandex-international; } +.@{fa-css-prefix}-yarn:before { content: @fa-var-yarn; } +.@{fa-css-prefix}-yelp:before { content: @fa-var-yelp; } +.@{fa-css-prefix}-yen-sign:before { content: @fa-var-yen-sign; } +.@{fa-css-prefix}-yin-yang:before { content: @fa-var-yin-yang; } +.@{fa-css-prefix}-yoast:before { content: @fa-var-yoast; } +.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; } +.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; } +.@{fa-css-prefix}-zhihu:before { content: @fa-var-zhihu; } diff --git a/public/vendor/fontawesome/less/_larger.less b/public/vendor/fontawesome/less/_larger.less new file mode 100644 index 0000000000..6cbb1ec6ec --- /dev/null +++ b/public/vendor/fontawesome/less/_larger.less @@ -0,0 +1,27 @@ +// Icon Sizes +// ------------------------- + +.larger(@factor) when (@factor > 0) { + .larger((@factor - 1)); + + .@{fa-css-prefix}-@{factor}x { + font-size: (@factor * 1em); + } +} + +/* makes the font 33% larger relative to the icon container */ +.@{fa-css-prefix}-lg { + font-size: (4em / 3); + line-height: (3em / 4); + vertical-align: -.0667em; +} + +.@{fa-css-prefix}-xs { + font-size: .75em; +} + +.@{fa-css-prefix}-sm { + font-size: .875em; +} + +.larger(10); diff --git a/public/vendor/fontawesome/less/_list.less b/public/vendor/fontawesome/less/_list.less new file mode 100644 index 0000000000..318aaa96ac --- /dev/null +++ b/public/vendor/fontawesome/less/_list.less @@ -0,0 +1,18 @@ +// List Icons +// ------------------------- + +.@{fa-css-prefix}-ul { + list-style-type: none; + margin-left: (@fa-li-width * 5/4); + padding-left: 0; + + > li { position: relative; } +} + +.@{fa-css-prefix}-li { + left: -@fa-li-width; + position: absolute; + text-align: center; + width: @fa-li-width; + line-height: inherit; +} diff --git a/public/vendor/fontawesome/less/_mixins.less b/public/vendor/fontawesome/less/_mixins.less new file mode 100644 index 0000000000..be561347ee --- /dev/null +++ b/public/vendor/fontawesome/less/_mixins.less @@ -0,0 +1,56 @@ +// Mixins +// -------------------------- + +.fa-icon() { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + font-weight: normal; + line-height: 1; +} + +.fa-icon-rotate(@degrees, @rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; + transform: rotate(@degrees); +} + +.fa-icon-flip(@horiz, @vert, @rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; + transform: scale(@horiz, @vert); +} + + +// Only display content to screen readers. A la Bootstrap 4. +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +.sr-only() { + border: 0; + clip: rect(0,0,0,0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// +// Credit: HTML5 Boilerplate + +.sr-only-focusable() { + &:active, + &:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; + } +} diff --git a/public/vendor/fontawesome/less/_rotated-flipped.less b/public/vendor/fontawesome/less/_rotated-flipped.less new file mode 100644 index 0000000000..d0c63ff80a --- /dev/null +++ b/public/vendor/fontawesome/less/_rotated-flipped.less @@ -0,0 +1,24 @@ +// Rotated & Flipped Icons +// ------------------------- + +.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } +.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } +.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } + +.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } +.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } +.@{fa-css-prefix}-flip-both, .@{fa-css-prefix}-flip-horizontal.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(-1, -1, 2); } + +// Hook for IE8-9 +// ------------------------- + +:root { + .@{fa-css-prefix}-rotate-90, + .@{fa-css-prefix}-rotate-180, + .@{fa-css-prefix}-rotate-270, + .@{fa-css-prefix}-flip-horizontal, + .@{fa-css-prefix}-flip-vertical, + .@{fa-css-prefix}-flip-both { + filter: none; + } +} diff --git a/public/vendor/fontawesome/less/_screen-reader.less b/public/vendor/fontawesome/less/_screen-reader.less new file mode 100644 index 0000000000..11c188196d --- /dev/null +++ b/public/vendor/fontawesome/less/_screen-reader.less @@ -0,0 +1,5 @@ +// Screen Readers +// ------------------------- + +.sr-only { .sr-only(); } +.sr-only-focusable { .sr-only-focusable(); } diff --git a/public/vendor/fontawesome/less/_shims.less b/public/vendor/fontawesome/less/_shims.less new file mode 100644 index 0000000000..3c8d86d744 --- /dev/null +++ b/public/vendor/fontawesome/less/_shims.less @@ -0,0 +1,2066 @@ +.@{fa-css-prefix}.@{fa-css-prefix}-glass:before { content: @fa-var-glass-martini; } + +.@{fa-css-prefix}.@{fa-css-prefix}-meetup { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-star-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-star-o:before { content: @fa-var-star; } + +.@{fa-css-prefix}.@{fa-css-prefix}-remove:before { content: @fa-var-times; } + +.@{fa-css-prefix}.@{fa-css-prefix}-close:before { content: @fa-var-times; } + +.@{fa-css-prefix}.@{fa-css-prefix}-gear:before { content: @fa-var-cog; } + +.@{fa-css-prefix}.@{fa-css-prefix}-trash-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-o:before { content: @fa-var-file; } + +.@{fa-css-prefix}.@{fa-css-prefix}-clock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock; } + +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-alt-circle-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-alt-circle-up; } + +.@{fa-css-prefix}.@{fa-css-prefix}-play-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-repeat:before { content: @fa-var-redo; } + +.@{fa-css-prefix}.@{fa-css-prefix}-rotate-right:before { content: @fa-var-redo; } + +.@{fa-css-prefix}.@{fa-css-prefix}-refresh:before { content: @fa-var-sync; } + +.@{fa-css-prefix}.@{fa-css-prefix}-list-alt { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-dedent:before { content: @fa-var-outdent; } + +.@{fa-css-prefix}.@{fa-css-prefix}-video-camera:before { content: @fa-var-video; } + +.@{fa-css-prefix}.@{fa-css-prefix}-picture-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-picture-o:before { content: @fa-var-image; } + +.@{fa-css-prefix}.@{fa-css-prefix}-photo { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-photo:before { content: @fa-var-image; } + +.@{fa-css-prefix}.@{fa-css-prefix}-image { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-image:before { content: @fa-var-image; } + +.@{fa-css-prefix}.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-pencil-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-edit; } + +.@{fa-css-prefix}.@{fa-css-prefix}-share-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square; } + +.@{fa-css-prefix}.@{fa-css-prefix}-check-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square; } + +.@{fa-css-prefix}.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-times-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-check-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-mail-forward:before { content: @fa-var-share; } + +.@{fa-css-prefix}.@{fa-css-prefix}-expand:before { content: @fa-var-expand-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-compress:before { content: @fa-var-compress-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-eye { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-eye-slash { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-warning:before { content: @fa-var-exclamation-triangle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-alt-v; } + +.@{fa-css-prefix}.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-alt-h; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bar-chart { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-bar-chart:before { content: @fa-var-chart-bar; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bar-chart-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-bar-chart-o:before { content: @fa-var-chart-bar; } + +.@{fa-css-prefix}.@{fa-css-prefix}-twitter-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-facebook-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-gears:before { content: @fa-var-cogs; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thumbs-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-up; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thumbs-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-heart-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-linkedin-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumbtack; } + +.@{fa-css-prefix}.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-github-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-lemon-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon; } + +.@{fa-css-prefix}.@{fa-css-prefix}-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-square-o:before { content: @fa-var-square; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bookmark-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark; } + +.@{fa-css-prefix}.@{fa-css-prefix}-twitter { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-facebook { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook-f; } + +.@{fa-css-prefix}.@{fa-css-prefix}-facebook-f { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-facebook-f:before { content: @fa-var-facebook-f; } + +.@{fa-css-prefix}.@{fa-css-prefix}-github { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-credit-card { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-feed:before { content: @fa-var-rss; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hdd-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-point-right; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-point-left; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-point-up; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-point-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-expand-arrows-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-group:before { content: @fa-var-users; } + +.@{fa-css-prefix}.@{fa-css-prefix}-chain:before { content: @fa-var-link; } + +.@{fa-css-prefix}.@{fa-css-prefix}-scissors:before { content: @fa-var-cut; } + +.@{fa-css-prefix}.@{fa-css-prefix}-files-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-files-o:before { content: @fa-var-copy; } + +.@{fa-css-prefix}.@{fa-css-prefix}-floppy-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-floppy-o:before { content: @fa-var-save; } + +.@{fa-css-prefix}.@{fa-css-prefix}-navicon:before { content: @fa-var-bars; } + +.@{fa-css-prefix}.@{fa-css-prefix}-reorder:before { content: @fa-var-bars; } + +.@{fa-css-prefix}.@{fa-css-prefix}-pinterest { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-pinterest-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-google-plus-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-google-plus { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus-g; } + +.@{fa-css-prefix}.@{fa-css-prefix}-money { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-money:before { content: @fa-var-money-bill-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-unsorted:before { content: @fa-var-sort; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-up; } + +.@{fa-css-prefix}.@{fa-css-prefix}-linkedin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin-in; } + +.@{fa-css-prefix}.@{fa-css-prefix}-rotate-left:before { content: @fa-var-undo; } + +.@{fa-css-prefix}.@{fa-css-prefix}-legal:before { content: @fa-var-gavel; } + +.@{fa-css-prefix}.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-dashboard:before { content: @fa-var-tachometer-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-comment-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment; } + +.@{fa-css-prefix}.@{fa-css-prefix}-comments-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments; } + +.@{fa-css-prefix}.@{fa-css-prefix}-flash:before { content: @fa-var-bolt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-clipboard { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-paste { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-paste:before { content: @fa-var-clipboard; } + +.@{fa-css-prefix}.@{fa-css-prefix}-lightbulb-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb; } + +.@{fa-css-prefix}.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bell-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell; } + +.@{fa-css-prefix}.@{fa-css-prefix}-cutlery:before { content: @fa-var-utensils; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-text-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-building-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-building-o:before { content: @fa-var-building; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hospital-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital; } + +.@{fa-css-prefix}.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-mobile-phone:before { content: @fa-var-mobile-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-mail-reply:before { content: @fa-var-reply; } + +.@{fa-css-prefix}.@{fa-css-prefix}-github-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-folder-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder; } + +.@{fa-css-prefix}.@{fa-css-prefix}-folder-open-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open; } + +.@{fa-css-prefix}.@{fa-css-prefix}-smile-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile; } + +.@{fa-css-prefix}.@{fa-css-prefix}-frown-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown; } + +.@{fa-css-prefix}.@{fa-css-prefix}-meh-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh; } + +.@{fa-css-prefix}.@{fa-css-prefix}-keyboard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard; } + +.@{fa-css-prefix}.@{fa-css-prefix}-flag-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag; } + +.@{fa-css-prefix}.@{fa-css-prefix}-mail-reply-all:before { content: @fa-var-reply-all; } + +.@{fa-css-prefix}.@{fa-css-prefix}-star-half-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half; } + +.@{fa-css-prefix}.@{fa-css-prefix}-star-half-empty { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-star-half-empty:before { content: @fa-var-star-half; } + +.@{fa-css-prefix}.@{fa-css-prefix}-star-half-full { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-star-half-full:before { content: @fa-var-star-half; } + +.@{fa-css-prefix}.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-branch; } + +.@{fa-css-prefix}.@{fa-css-prefix}-chain-broken:before { content: @fa-var-unlink; } + +.@{fa-css-prefix}.@{fa-css-prefix}-shield:before { content: @fa-var-shield-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar; } + +.@{fa-css-prefix}.@{fa-css-prefix}-maxcdn { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-html5 { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-css3 { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-minus-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square; } + +.@{fa-css-prefix}.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pen-square; } + +.@{fa-css-prefix}.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-compass { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-down { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-down:before { content: @fa-var-caret-square-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-up; } + +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-up { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-up:before { content: @fa-var-caret-square-up; } + +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-right; } + +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-right:before { content: @fa-var-caret-square-right; } + +.@{fa-css-prefix}.@{fa-css-prefix}-eur:before { content: @fa-var-euro-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-euro:before { content: @fa-var-euro-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-gbp:before { content: @fa-var-pound-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-usd:before { content: @fa-var-dollar-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-dollar:before { content: @fa-var-dollar-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-inr:before { content: @fa-var-rupee-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-rupee:before { content: @fa-var-rupee-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-jpy:before { content: @fa-var-yen-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-cny:before { content: @fa-var-yen-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-rmb:before { content: @fa-var-yen-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-yen:before { content: @fa-var-yen-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-rub:before { content: @fa-var-ruble-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-ruble:before { content: @fa-var-ruble-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-rouble:before { content: @fa-var-ruble-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-krw:before { content: @fa-var-won-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-won:before { content: @fa-var-won-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-btc { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-bitcoin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-bitcoin:before { content: @fa-var-btc; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-text:before { content: @fa-var-file-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-down-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-down-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-down-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-youtube-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-youtube { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-xing { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-xing-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-youtube-play { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube; } + +.@{fa-css-prefix}.@{fa-css-prefix}-dropbox { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-stack-overflow { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-instagram { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-flickr { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-adn { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-bitbucket { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-bitbucket-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket; } + +.@{fa-css-prefix}.@{fa-css-prefix}-tumblr { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-tumblr-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-alt-down; } + +.@{fa-css-prefix}.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-alt-up; } + +.@{fa-css-prefix}.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-alt-left; } + +.@{fa-css-prefix}.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-alt-right; } + +.@{fa-css-prefix}.@{fa-css-prefix}-apple { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-windows { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-android { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-linux { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-dribbble { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-skype { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-foursquare { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-trello { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-gratipay { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-gittip { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-gittip:before { content: @fa-var-gratipay; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sun-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun; } + +.@{fa-css-prefix}.@{fa-css-prefix}-moon-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon; } + +.@{fa-css-prefix}.@{fa-css-prefix}-vk { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-weibo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-renren { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-pagelines { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-stack-exchange { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-right { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-alt-circle-right; } + +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-alt-circle-left; } + +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-left; } + +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-left { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-toggle-left:before { content: @fa-var-caret-square-left; } + +.@{fa-css-prefix}.@{fa-css-prefix}-dot-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-vimeo-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-try:before { content: @fa-var-lira-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-turkish-lira:before { content: @fa-var-lira-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-plus-square-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square; } + +.@{fa-css-prefix}.@{fa-css-prefix}-slack { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-wordpress { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-openid { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-institution:before { content: @fa-var-university; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bank:before { content: @fa-var-university; } + +.@{fa-css-prefix}.@{fa-css-prefix}-mortar-board:before { content: @fa-var-graduation-cap; } + +.@{fa-css-prefix}.@{fa-css-prefix}-yahoo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-google { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-reddit { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-reddit-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-stumbleupon-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-stumbleupon { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-delicious { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-digg { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-pied-piper-pp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-pied-piper-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-drupal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-joomla { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-spoon:before { content: @fa-var-utensil-spoon; } + +.@{fa-css-prefix}.@{fa-css-prefix}-behance { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-behance-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-steam { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-steam-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-automobile:before { content: @fa-var-car; } + +.@{fa-css-prefix}.@{fa-css-prefix}-envelope-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope; } + +.@{fa-css-prefix}.@{fa-css-prefix}-spotify { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-deviantart { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-soundcloud { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-file-pdf-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-word-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-excel-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-powerpoint-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-image-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-photo-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-photo-o:before { content: @fa-var-file-image; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-picture-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-picture-o:before { content: @fa-var-file-image; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-archive-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-zip-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-zip-o:before { content: @fa-var-file-archive; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-audio-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-sound-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-sound-o:before { content: @fa-var-file-audio; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-video-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-movie-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-movie-o:before { content: @fa-var-file-video; } + +.@{fa-css-prefix}.@{fa-css-prefix}-file-code-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code; } + +.@{fa-css-prefix}.@{fa-css-prefix}-vine { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-codepen { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-jsfiddle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-life-ring { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-life-bouy { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-life-bouy:before { content: @fa-var-life-ring; } + +.@{fa-css-prefix}.@{fa-css-prefix}-life-buoy { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-life-buoy:before { content: @fa-var-life-ring; } + +.@{fa-css-prefix}.@{fa-css-prefix}-life-saver { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-life-saver:before { content: @fa-var-life-ring; } + +.@{fa-css-prefix}.@{fa-css-prefix}-support { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-support:before { content: @fa-var-life-ring; } + +.@{fa-css-prefix}.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-notch; } + +.@{fa-css-prefix}.@{fa-css-prefix}-rebel { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-ra { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-ra:before { content: @fa-var-rebel; } + +.@{fa-css-prefix}.@{fa-css-prefix}-resistance { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-resistance:before { content: @fa-var-rebel; } + +.@{fa-css-prefix}.@{fa-css-prefix}-empire { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-ge { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-ge:before { content: @fa-var-empire; } + +.@{fa-css-prefix}.@{fa-css-prefix}-git-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-git { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-hacker-news { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-y-combinator-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-y-combinator-square:before { content: @fa-var-hacker-news; } + +.@{fa-css-prefix}.@{fa-css-prefix}-yc-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-yc-square:before { content: @fa-var-hacker-news; } + +.@{fa-css-prefix}.@{fa-css-prefix}-tencent-weibo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-qq { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-weixin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-wechat { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-wechat:before { content: @fa-var-weixin; } + +.@{fa-css-prefix}.@{fa-css-prefix}-send:before { content: @fa-var-paper-plane; } + +.@{fa-css-prefix}.@{fa-css-prefix}-paper-plane-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane; } + +.@{fa-css-prefix}.@{fa-css-prefix}-send-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-send-o:before { content: @fa-var-paper-plane; } + +.@{fa-css-prefix}.@{fa-css-prefix}-circle-thin { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-header:before { content: @fa-var-heading; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders-h; } + +.@{fa-css-prefix}.@{fa-css-prefix}-futbol-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-futbol-o:before { content: @fa-var-futbol; } + +.@{fa-css-prefix}.@{fa-css-prefix}-soccer-ball-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-soccer-ball-o:before { content: @fa-var-futbol; } + +.@{fa-css-prefix}.@{fa-css-prefix}-slideshare { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-twitch { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-yelp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-newspaper-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-newspaper-o:before { content: @fa-var-newspaper; } + +.@{fa-css-prefix}.@{fa-css-prefix}-paypal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-google-wallet { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-visa { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-mastercard { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-discover { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-amex { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-paypal { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-stripe { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-bell-slash-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-bell-slash-o:before { content: @fa-var-bell-slash; } + +.@{fa-css-prefix}.@{fa-css-prefix}-trash:before { content: @fa-var-trash-alt; } + +.@{fa-css-prefix}.@{fa-css-prefix}-copyright { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-eyedropper:before { content: @fa-var-eye-dropper; } + +.@{fa-css-prefix}.@{fa-css-prefix}-area-chart:before { content: @fa-var-chart-area; } + +.@{fa-css-prefix}.@{fa-css-prefix}-pie-chart:before { content: @fa-var-chart-pie; } + +.@{fa-css-prefix}.@{fa-css-prefix}-line-chart:before { content: @fa-var-chart-line; } + +.@{fa-css-prefix}.@{fa-css-prefix}-lastfm { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-lastfm-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-ioxhost { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-angellist { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-cc:before { content: @fa-var-closed-captioning; } + +.@{fa-css-prefix}.@{fa-css-prefix}-ils:before { content: @fa-var-shekel-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-shekel:before { content: @fa-var-shekel-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-sheqel:before { content: @fa-var-shekel-sign; } + +.@{fa-css-prefix}.@{fa-css-prefix}-meanpath { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-meanpath:before { content: @fa-var-font-awesome; } + +.@{fa-css-prefix}.@{fa-css-prefix}-buysellads { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-connectdevelop { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-dashcube { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-forumbee { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-leanpub { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-sellsy { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-shirtsinbulk { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-simplybuilt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-skyatlas { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-diamond { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-diamond:before { content: @fa-var-gem; } + +.@{fa-css-prefix}.@{fa-css-prefix}-intersex:before { content: @fa-var-transgender; } + +.@{fa-css-prefix}.@{fa-css-prefix}-facebook-official { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook; } + +.@{fa-css-prefix}.@{fa-css-prefix}-pinterest-p { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-whatsapp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-hotel:before { content: @fa-var-bed; } + +.@{fa-css-prefix}.@{fa-css-prefix}-viacoin { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-medium { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-y-combinator { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-yc { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-yc:before { content: @fa-var-y-combinator; } + +.@{fa-css-prefix}.@{fa-css-prefix}-optin-monster { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-opencart { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-expeditedssl { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-battery-4:before { content: @fa-var-battery-full; } + +.@{fa-css-prefix}.@{fa-css-prefix}-battery:before { content: @fa-var-battery-full; } + +.@{fa-css-prefix}.@{fa-css-prefix}-battery-3:before { content: @fa-var-battery-three-quarters; } + +.@{fa-css-prefix}.@{fa-css-prefix}-battery-2:before { content: @fa-var-battery-half; } + +.@{fa-css-prefix}.@{fa-css-prefix}-battery-1:before { content: @fa-var-battery-quarter; } + +.@{fa-css-prefix}.@{fa-css-prefix}-battery-0:before { content: @fa-var-battery-empty; } + +.@{fa-css-prefix}.@{fa-css-prefix}-object-group { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-object-ungroup { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-sticky-note-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-sticky-note-o:before { content: @fa-var-sticky-note; } + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-jcb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cc-diners-club { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-clone { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-hourglass-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hourglass-o:before { content: @fa-var-hourglass; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hourglass-1:before { content: @fa-var-hourglass-start; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hourglass-2:before { content: @fa-var-hourglass-half; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hourglass-3:before { content: @fa-var-hourglass-end; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-rock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-rock-o:before { content: @fa-var-hand-rock; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-grab-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-grab-o:before { content: @fa-var-hand-rock; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-paper-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-paper-o:before { content: @fa-var-hand-paper; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-stop-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-stop-o:before { content: @fa-var-hand-paper; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-scissors-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-scissors-o:before { content: @fa-var-hand-scissors; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-lizard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-lizard-o:before { content: @fa-var-hand-lizard; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-spock-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-spock-o:before { content: @fa-var-hand-spock; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-pointer-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-pointer-o:before { content: @fa-var-hand-pointer; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hand-peace-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-hand-peace-o:before { content: @fa-var-hand-peace; } + +.@{fa-css-prefix}.@{fa-css-prefix}-registered { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-creative-commons { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-gg { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-gg-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-tripadvisor { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-odnoklassniki { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-odnoklassniki-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-get-pocket { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-wikipedia-w { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-safari { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-chrome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-firefox { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-opera { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-internet-explorer { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-television:before { content: @fa-var-tv; } + +.@{fa-css-prefix}.@{fa-css-prefix}-contao { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-500px { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-amazon { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-plus-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-plus-o:before { content: @fa-var-calendar-plus; } + +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-minus-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-minus-o:before { content: @fa-var-calendar-minus; } + +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-times-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-times-o:before { content: @fa-var-calendar-times; } + +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-check-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-calendar-check-o:before { content: @fa-var-calendar-check; } + +.@{fa-css-prefix}.@{fa-css-prefix}-map-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-map-o:before { content: @fa-var-map; } + +.@{fa-css-prefix}.@{fa-css-prefix}-commenting:before { content: @fa-var-comment-dots; } + +.@{fa-css-prefix}.@{fa-css-prefix}-commenting-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-commenting-o:before { content: @fa-var-comment-dots; } + +.@{fa-css-prefix}.@{fa-css-prefix}-houzz { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-vimeo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-vimeo:before { content: @fa-var-vimeo-v; } + +.@{fa-css-prefix}.@{fa-css-prefix}-black-tie { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-fonticons { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-reddit-alien { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-edge { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-credit-card-alt:before { content: @fa-var-credit-card; } + +.@{fa-css-prefix}.@{fa-css-prefix}-codiepie { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-modx { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-fort-awesome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-usb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-product-hunt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-mixcloud { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-scribd { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-pause-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-pause-circle-o:before { content: @fa-var-pause-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-stop-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-stop-circle-o:before { content: @fa-var-stop-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bluetooth { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-bluetooth-b { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-gitlab { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-wpbeginner { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-wpforms { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-envira { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-wheelchair-alt { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-accessible-icon; } + +.@{fa-css-prefix}.@{fa-css-prefix}-question-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-phone-volume; } + +.@{fa-css-prefix}.@{fa-css-prefix}-asl-interpreting:before { content: @fa-var-american-sign-language-interpreting; } + +.@{fa-css-prefix}.@{fa-css-prefix}-deafness:before { content: @fa-var-deaf; } + +.@{fa-css-prefix}.@{fa-css-prefix}-hard-of-hearing:before { content: @fa-var-deaf; } + +.@{fa-css-prefix}.@{fa-css-prefix}-glide { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-glide-g { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-signing:before { content: @fa-var-sign-language; } + +.@{fa-css-prefix}.@{fa-css-prefix}-viadeo { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-viadeo-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-snapchat { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-snapchat-ghost { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-snapchat-square { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-pied-piper { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-first-order { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-yoast { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-themeisle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-google-plus-official { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-google-plus-official:before { content: @fa-var-google-plus; } + +.@{fa-css-prefix}.@{fa-css-prefix}-google-plus-circle { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-google-plus-circle:before { content: @fa-var-google-plus; } + +.@{fa-css-prefix}.@{fa-css-prefix}-font-awesome { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-fa { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-fa:before { content: @fa-var-font-awesome; } + +.@{fa-css-prefix}.@{fa-css-prefix}-handshake-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-handshake-o:before { content: @fa-var-handshake; } + +.@{fa-css-prefix}.@{fa-css-prefix}-envelope-open-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-envelope-open-o:before { content: @fa-var-envelope-open; } + +.@{fa-css-prefix}.@{fa-css-prefix}-linode { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-address-book-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-address-book-o:before { content: @fa-var-address-book; } + +.@{fa-css-prefix}.@{fa-css-prefix}-vcard:before { content: @fa-var-address-card; } + +.@{fa-css-prefix}.@{fa-css-prefix}-address-card-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-address-card-o:before { content: @fa-var-address-card; } + +.@{fa-css-prefix}.@{fa-css-prefix}-vcard-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-vcard-o:before { content: @fa-var-address-card; } + +.@{fa-css-prefix}.@{fa-css-prefix}-user-circle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-user-circle-o:before { content: @fa-var-user-circle; } + +.@{fa-css-prefix}.@{fa-css-prefix}-user-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-user-o:before { content: @fa-var-user; } + +.@{fa-css-prefix}.@{fa-css-prefix}-id-badge { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-drivers-license:before { content: @fa-var-id-card; } + +.@{fa-css-prefix}.@{fa-css-prefix}-id-card-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-id-card-o:before { content: @fa-var-id-card; } + +.@{fa-css-prefix}.@{fa-css-prefix}-drivers-license-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-drivers-license-o:before { content: @fa-var-id-card; } + +.@{fa-css-prefix}.@{fa-css-prefix}-quora { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-free-code-camp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-telegram { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-thermometer-4:before { content: @fa-var-thermometer-full; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thermometer:before { content: @fa-var-thermometer-full; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thermometer-3:before { content: @fa-var-thermometer-three-quarters; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thermometer-2:before { content: @fa-var-thermometer-half; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thermometer-1:before { content: @fa-var-thermometer-quarter; } + +.@{fa-css-prefix}.@{fa-css-prefix}-thermometer-0:before { content: @fa-var-thermometer-empty; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bathtub:before { content: @fa-var-bath; } + +.@{fa-css-prefix}.@{fa-css-prefix}-s15:before { content: @fa-var-bath; } + +.@{fa-css-prefix}.@{fa-css-prefix}-window-maximize { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-window-restore { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-times-rectangle:before { content: @fa-var-window-close; } + +.@{fa-css-prefix}.@{fa-css-prefix}-window-close-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-window-close-o:before { content: @fa-var-window-close; } + +.@{fa-css-prefix}.@{fa-css-prefix}-times-rectangle-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-times-rectangle-o:before { content: @fa-var-window-close; } + +.@{fa-css-prefix}.@{fa-css-prefix}-bandcamp { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-grav { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-etsy { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-imdb { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-ravelry { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-eercast { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-eercast:before { content: @fa-var-sellcast; } + +.@{fa-css-prefix}.@{fa-css-prefix}-snowflake-o { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} +.@{fa-css-prefix}.@{fa-css-prefix}-snowflake-o:before { content: @fa-var-snowflake; } + +.@{fa-css-prefix}.@{fa-css-prefix}-superpowers { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-wpexplorer { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} + +.@{fa-css-prefix}.@{fa-css-prefix}-cab:before { content: @fa-var-taxi; } + diff --git a/public/vendor/fontawesome/less/_stacked.less b/public/vendor/fontawesome/less/_stacked.less new file mode 100644 index 0000000000..263b5c44fc --- /dev/null +++ b/public/vendor/fontawesome/less/_stacked.less @@ -0,0 +1,22 @@ +// Stacked Icons +// ------------------------- + +.@{fa-css-prefix}-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2em; +} + +.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; +} + +.@{fa-css-prefix}-stack-1x { line-height: inherit; } +.@{fa-css-prefix}-stack-2x { font-size: 2em; } +.@{fa-css-prefix}-inverse { color: @fa-inverse; } diff --git a/public/vendor/fontawesome/less/_variables.less b/public/vendor/fontawesome/less/_variables.less new file mode 100644 index 0000000000..f4fafb7551 --- /dev/null +++ b/public/vendor/fontawesome/less/_variables.less @@ -0,0 +1,1474 @@ +// Variables +// -------------------------- + +@fa-font-path: "vendor/fontawesome/webfonts"; +@fa-font-size-base: 16px; +@fa-font-display: block; +@fa-line-height-base: 1; +@fa-css-prefix: fa; +@fa-version: "5.15.3"; +@fa-border-color: #eee; +@fa-inverse: #fff; +@fa-li-width: 2em; +@fa-primary-opacity: 1; +@fa-secondary-opacity: .4; + +@fa-var-500px: "\f26e"; +@fa-var-accessible-icon: "\f368"; +@fa-var-accusoft: "\f369"; +@fa-var-acquisitions-incorporated: "\f6af"; +@fa-var-ad: "\f641"; +@fa-var-address-book: "\f2b9"; +@fa-var-address-card: "\f2bb"; +@fa-var-adjust: "\f042"; +@fa-var-adn: "\f170"; +@fa-var-adversal: "\f36a"; +@fa-var-affiliatetheme: "\f36b"; +@fa-var-air-freshener: "\f5d0"; +@fa-var-airbnb: "\f834"; +@fa-var-algolia: "\f36c"; +@fa-var-align-center: "\f037"; +@fa-var-align-justify: "\f039"; +@fa-var-align-left: "\f036"; +@fa-var-align-right: "\f038"; +@fa-var-alipay: "\f642"; +@fa-var-allergies: "\f461"; +@fa-var-amazon: "\f270"; +@fa-var-amazon-pay: "\f42c"; +@fa-var-ambulance: "\f0f9"; +@fa-var-american-sign-language-interpreting: "\f2a3"; +@fa-var-amilia: "\f36d"; +@fa-var-anchor: "\f13d"; +@fa-var-android: "\f17b"; +@fa-var-angellist: "\f209"; +@fa-var-angle-double-down: "\f103"; +@fa-var-angle-double-left: "\f100"; +@fa-var-angle-double-right: "\f101"; +@fa-var-angle-double-up: "\f102"; +@fa-var-angle-down: "\f107"; +@fa-var-angle-left: "\f104"; +@fa-var-angle-right: "\f105"; +@fa-var-angle-up: "\f106"; +@fa-var-angry: "\f556"; +@fa-var-angrycreative: "\f36e"; +@fa-var-angular: "\f420"; +@fa-var-ankh: "\f644"; +@fa-var-app-store: "\f36f"; +@fa-var-app-store-ios: "\f370"; +@fa-var-apper: "\f371"; +@fa-var-apple: "\f179"; +@fa-var-apple-alt: "\f5d1"; +@fa-var-apple-pay: "\f415"; +@fa-var-archive: "\f187"; +@fa-var-archway: "\f557"; +@fa-var-arrow-alt-circle-down: "\f358"; +@fa-var-arrow-alt-circle-left: "\f359"; +@fa-var-arrow-alt-circle-right: "\f35a"; +@fa-var-arrow-alt-circle-up: "\f35b"; +@fa-var-arrow-circle-down: "\f0ab"; +@fa-var-arrow-circle-left: "\f0a8"; +@fa-var-arrow-circle-right: "\f0a9"; +@fa-var-arrow-circle-up: "\f0aa"; +@fa-var-arrow-down: "\f063"; +@fa-var-arrow-left: "\f060"; +@fa-var-arrow-right: "\f061"; +@fa-var-arrow-up: "\f062"; +@fa-var-arrows-alt: "\f0b2"; +@fa-var-arrows-alt-h: "\f337"; +@fa-var-arrows-alt-v: "\f338"; +@fa-var-artstation: "\f77a"; +@fa-var-assistive-listening-systems: "\f2a2"; +@fa-var-asterisk: "\f069"; +@fa-var-asymmetrik: "\f372"; +@fa-var-at: "\f1fa"; +@fa-var-atlas: "\f558"; +@fa-var-atlassian: "\f77b"; +@fa-var-atom: "\f5d2"; +@fa-var-audible: "\f373"; +@fa-var-audio-description: "\f29e"; +@fa-var-autoprefixer: "\f41c"; +@fa-var-avianex: "\f374"; +@fa-var-aviato: "\f421"; +@fa-var-award: "\f559"; +@fa-var-aws: "\f375"; +@fa-var-baby: "\f77c"; +@fa-var-baby-carriage: "\f77d"; +@fa-var-backspace: "\f55a"; +@fa-var-backward: "\f04a"; +@fa-var-bacon: "\f7e5"; +@fa-var-bacteria: "\e059"; +@fa-var-bacterium: "\e05a"; +@fa-var-bahai: "\f666"; +@fa-var-balance-scale: "\f24e"; +@fa-var-balance-scale-left: "\f515"; +@fa-var-balance-scale-right: "\f516"; +@fa-var-ban: "\f05e"; +@fa-var-band-aid: "\f462"; +@fa-var-bandcamp: "\f2d5"; +@fa-var-barcode: "\f02a"; +@fa-var-bars: "\f0c9"; +@fa-var-baseball-ball: "\f433"; +@fa-var-basketball-ball: "\f434"; +@fa-var-bath: "\f2cd"; +@fa-var-battery-empty: "\f244"; +@fa-var-battery-full: "\f240"; +@fa-var-battery-half: "\f242"; +@fa-var-battery-quarter: "\f243"; +@fa-var-battery-three-quarters: "\f241"; +@fa-var-battle-net: "\f835"; +@fa-var-bed: "\f236"; +@fa-var-beer: "\f0fc"; +@fa-var-behance: "\f1b4"; +@fa-var-behance-square: "\f1b5"; +@fa-var-bell: "\f0f3"; +@fa-var-bell-slash: "\f1f6"; +@fa-var-bezier-curve: "\f55b"; +@fa-var-bible: "\f647"; +@fa-var-bicycle: "\f206"; +@fa-var-biking: "\f84a"; +@fa-var-bimobject: "\f378"; +@fa-var-binoculars: "\f1e5"; +@fa-var-biohazard: "\f780"; +@fa-var-birthday-cake: "\f1fd"; +@fa-var-bitbucket: "\f171"; +@fa-var-bitcoin: "\f379"; +@fa-var-bity: "\f37a"; +@fa-var-black-tie: "\f27e"; +@fa-var-blackberry: "\f37b"; +@fa-var-blender: "\f517"; +@fa-var-blender-phone: "\f6b6"; +@fa-var-blind: "\f29d"; +@fa-var-blog: "\f781"; +@fa-var-blogger: "\f37c"; +@fa-var-blogger-b: "\f37d"; +@fa-var-bluetooth: "\f293"; +@fa-var-bluetooth-b: "\f294"; +@fa-var-bold: "\f032"; +@fa-var-bolt: "\f0e7"; +@fa-var-bomb: "\f1e2"; +@fa-var-bone: "\f5d7"; +@fa-var-bong: "\f55c"; +@fa-var-book: "\f02d"; +@fa-var-book-dead: "\f6b7"; +@fa-var-book-medical: "\f7e6"; +@fa-var-book-open: "\f518"; +@fa-var-book-reader: "\f5da"; +@fa-var-bookmark: "\f02e"; +@fa-var-bootstrap: "\f836"; +@fa-var-border-all: "\f84c"; +@fa-var-border-none: "\f850"; +@fa-var-border-style: "\f853"; +@fa-var-bowling-ball: "\f436"; +@fa-var-box: "\f466"; +@fa-var-box-open: "\f49e"; +@fa-var-box-tissue: "\e05b"; +@fa-var-boxes: "\f468"; +@fa-var-braille: "\f2a1"; +@fa-var-brain: "\f5dc"; +@fa-var-bread-slice: "\f7ec"; +@fa-var-briefcase: "\f0b1"; +@fa-var-briefcase-medical: "\f469"; +@fa-var-broadcast-tower: "\f519"; +@fa-var-broom: "\f51a"; +@fa-var-brush: "\f55d"; +@fa-var-btc: "\f15a"; +@fa-var-buffer: "\f837"; +@fa-var-bug: "\f188"; +@fa-var-building: "\f1ad"; +@fa-var-bullhorn: "\f0a1"; +@fa-var-bullseye: "\f140"; +@fa-var-burn: "\f46a"; +@fa-var-buromobelexperte: "\f37f"; +@fa-var-bus: "\f207"; +@fa-var-bus-alt: "\f55e"; +@fa-var-business-time: "\f64a"; +@fa-var-buy-n-large: "\f8a6"; +@fa-var-buysellads: "\f20d"; +@fa-var-calculator: "\f1ec"; +@fa-var-calendar: "\f133"; +@fa-var-calendar-alt: "\f073"; +@fa-var-calendar-check: "\f274"; +@fa-var-calendar-day: "\f783"; +@fa-var-calendar-minus: "\f272"; +@fa-var-calendar-plus: "\f271"; +@fa-var-calendar-times: "\f273"; +@fa-var-calendar-week: "\f784"; +@fa-var-camera: "\f030"; +@fa-var-camera-retro: "\f083"; +@fa-var-campground: "\f6bb"; +@fa-var-canadian-maple-leaf: "\f785"; +@fa-var-candy-cane: "\f786"; +@fa-var-cannabis: "\f55f"; +@fa-var-capsules: "\f46b"; +@fa-var-car: "\f1b9"; +@fa-var-car-alt: "\f5de"; +@fa-var-car-battery: "\f5df"; +@fa-var-car-crash: "\f5e1"; +@fa-var-car-side: "\f5e4"; +@fa-var-caravan: "\f8ff"; +@fa-var-caret-down: "\f0d7"; +@fa-var-caret-left: "\f0d9"; +@fa-var-caret-right: "\f0da"; +@fa-var-caret-square-down: "\f150"; +@fa-var-caret-square-left: "\f191"; +@fa-var-caret-square-right: "\f152"; +@fa-var-caret-square-up: "\f151"; +@fa-var-caret-up: "\f0d8"; +@fa-var-carrot: "\f787"; +@fa-var-cart-arrow-down: "\f218"; +@fa-var-cart-plus: "\f217"; +@fa-var-cash-register: "\f788"; +@fa-var-cat: "\f6be"; +@fa-var-cc-amazon-pay: "\f42d"; +@fa-var-cc-amex: "\f1f3"; +@fa-var-cc-apple-pay: "\f416"; +@fa-var-cc-diners-club: "\f24c"; +@fa-var-cc-discover: "\f1f2"; +@fa-var-cc-jcb: "\f24b"; +@fa-var-cc-mastercard: "\f1f1"; +@fa-var-cc-paypal: "\f1f4"; +@fa-var-cc-stripe: "\f1f5"; +@fa-var-cc-visa: "\f1f0"; +@fa-var-centercode: "\f380"; +@fa-var-centos: "\f789"; +@fa-var-certificate: "\f0a3"; +@fa-var-chair: "\f6c0"; +@fa-var-chalkboard: "\f51b"; +@fa-var-chalkboard-teacher: "\f51c"; +@fa-var-charging-station: "\f5e7"; +@fa-var-chart-area: "\f1fe"; +@fa-var-chart-bar: "\f080"; +@fa-var-chart-line: "\f201"; +@fa-var-chart-pie: "\f200"; +@fa-var-check: "\f00c"; +@fa-var-check-circle: "\f058"; +@fa-var-check-double: "\f560"; +@fa-var-check-square: "\f14a"; +@fa-var-cheese: "\f7ef"; +@fa-var-chess: "\f439"; +@fa-var-chess-bishop: "\f43a"; +@fa-var-chess-board: "\f43c"; +@fa-var-chess-king: "\f43f"; +@fa-var-chess-knight: "\f441"; +@fa-var-chess-pawn: "\f443"; +@fa-var-chess-queen: "\f445"; +@fa-var-chess-rook: "\f447"; +@fa-var-chevron-circle-down: "\f13a"; +@fa-var-chevron-circle-left: "\f137"; +@fa-var-chevron-circle-right: "\f138"; +@fa-var-chevron-circle-up: "\f139"; +@fa-var-chevron-down: "\f078"; +@fa-var-chevron-left: "\f053"; +@fa-var-chevron-right: "\f054"; +@fa-var-chevron-up: "\f077"; +@fa-var-child: "\f1ae"; +@fa-var-chrome: "\f268"; +@fa-var-chromecast: "\f838"; +@fa-var-church: "\f51d"; +@fa-var-circle: "\f111"; +@fa-var-circle-notch: "\f1ce"; +@fa-var-city: "\f64f"; +@fa-var-clinic-medical: "\f7f2"; +@fa-var-clipboard: "\f328"; +@fa-var-clipboard-check: "\f46c"; +@fa-var-clipboard-list: "\f46d"; +@fa-var-clock: "\f017"; +@fa-var-clone: "\f24d"; +@fa-var-closed-captioning: "\f20a"; +@fa-var-cloud: "\f0c2"; +@fa-var-cloud-download-alt: "\f381"; +@fa-var-cloud-meatball: "\f73b"; +@fa-var-cloud-moon: "\f6c3"; +@fa-var-cloud-moon-rain: "\f73c"; +@fa-var-cloud-rain: "\f73d"; +@fa-var-cloud-showers-heavy: "\f740"; +@fa-var-cloud-sun: "\f6c4"; +@fa-var-cloud-sun-rain: "\f743"; +@fa-var-cloud-upload-alt: "\f382"; +@fa-var-cloudflare: "\e07d"; +@fa-var-cloudscale: "\f383"; +@fa-var-cloudsmith: "\f384"; +@fa-var-cloudversify: "\f385"; +@fa-var-cocktail: "\f561"; +@fa-var-code: "\f121"; +@fa-var-code-branch: "\f126"; +@fa-var-codepen: "\f1cb"; +@fa-var-codiepie: "\f284"; +@fa-var-coffee: "\f0f4"; +@fa-var-cog: "\f013"; +@fa-var-cogs: "\f085"; +@fa-var-coins: "\f51e"; +@fa-var-columns: "\f0db"; +@fa-var-comment: "\f075"; +@fa-var-comment-alt: "\f27a"; +@fa-var-comment-dollar: "\f651"; +@fa-var-comment-dots: "\f4ad"; +@fa-var-comment-medical: "\f7f5"; +@fa-var-comment-slash: "\f4b3"; +@fa-var-comments: "\f086"; +@fa-var-comments-dollar: "\f653"; +@fa-var-compact-disc: "\f51f"; +@fa-var-compass: "\f14e"; +@fa-var-compress: "\f066"; +@fa-var-compress-alt: "\f422"; +@fa-var-compress-arrows-alt: "\f78c"; +@fa-var-concierge-bell: "\f562"; +@fa-var-confluence: "\f78d"; +@fa-var-connectdevelop: "\f20e"; +@fa-var-contao: "\f26d"; +@fa-var-cookie: "\f563"; +@fa-var-cookie-bite: "\f564"; +@fa-var-copy: "\f0c5"; +@fa-var-copyright: "\f1f9"; +@fa-var-cotton-bureau: "\f89e"; +@fa-var-couch: "\f4b8"; +@fa-var-cpanel: "\f388"; +@fa-var-creative-commons: "\f25e"; +@fa-var-creative-commons-by: "\f4e7"; +@fa-var-creative-commons-nc: "\f4e8"; +@fa-var-creative-commons-nc-eu: "\f4e9"; +@fa-var-creative-commons-nc-jp: "\f4ea"; +@fa-var-creative-commons-nd: "\f4eb"; +@fa-var-creative-commons-pd: "\f4ec"; +@fa-var-creative-commons-pd-alt: "\f4ed"; +@fa-var-creative-commons-remix: "\f4ee"; +@fa-var-creative-commons-sa: "\f4ef"; +@fa-var-creative-commons-sampling: "\f4f0"; +@fa-var-creative-commons-sampling-plus: "\f4f1"; +@fa-var-creative-commons-share: "\f4f2"; +@fa-var-creative-commons-zero: "\f4f3"; +@fa-var-credit-card: "\f09d"; +@fa-var-critical-role: "\f6c9"; +@fa-var-crop: "\f125"; +@fa-var-crop-alt: "\f565"; +@fa-var-cross: "\f654"; +@fa-var-crosshairs: "\f05b"; +@fa-var-crow: "\f520"; +@fa-var-crown: "\f521"; +@fa-var-crutch: "\f7f7"; +@fa-var-css3: "\f13c"; +@fa-var-css3-alt: "\f38b"; +@fa-var-cube: "\f1b2"; +@fa-var-cubes: "\f1b3"; +@fa-var-cut: "\f0c4"; +@fa-var-cuttlefish: "\f38c"; +@fa-var-d-and-d: "\f38d"; +@fa-var-d-and-d-beyond: "\f6ca"; +@fa-var-dailymotion: "\e052"; +@fa-var-dashcube: "\f210"; +@fa-var-database: "\f1c0"; +@fa-var-deaf: "\f2a4"; +@fa-var-deezer: "\e077"; +@fa-var-delicious: "\f1a5"; +@fa-var-democrat: "\f747"; +@fa-var-deploydog: "\f38e"; +@fa-var-deskpro: "\f38f"; +@fa-var-desktop: "\f108"; +@fa-var-dev: "\f6cc"; +@fa-var-deviantart: "\f1bd"; +@fa-var-dharmachakra: "\f655"; +@fa-var-dhl: "\f790"; +@fa-var-diagnoses: "\f470"; +@fa-var-diaspora: "\f791"; +@fa-var-dice: "\f522"; +@fa-var-dice-d20: "\f6cf"; +@fa-var-dice-d6: "\f6d1"; +@fa-var-dice-five: "\f523"; +@fa-var-dice-four: "\f524"; +@fa-var-dice-one: "\f525"; +@fa-var-dice-six: "\f526"; +@fa-var-dice-three: "\f527"; +@fa-var-dice-two: "\f528"; +@fa-var-digg: "\f1a6"; +@fa-var-digital-ocean: "\f391"; +@fa-var-digital-tachograph: "\f566"; +@fa-var-directions: "\f5eb"; +@fa-var-discord: "\f392"; +@fa-var-discourse: "\f393"; +@fa-var-disease: "\f7fa"; +@fa-var-divide: "\f529"; +@fa-var-dizzy: "\f567"; +@fa-var-dna: "\f471"; +@fa-var-dochub: "\f394"; +@fa-var-docker: "\f395"; +@fa-var-dog: "\f6d3"; +@fa-var-dollar-sign: "\f155"; +@fa-var-dolly: "\f472"; +@fa-var-dolly-flatbed: "\f474"; +@fa-var-donate: "\f4b9"; +@fa-var-door-closed: "\f52a"; +@fa-var-door-open: "\f52b"; +@fa-var-dot-circle: "\f192"; +@fa-var-dove: "\f4ba"; +@fa-var-download: "\f019"; +@fa-var-draft2digital: "\f396"; +@fa-var-drafting-compass: "\f568"; +@fa-var-dragon: "\f6d5"; +@fa-var-draw-polygon: "\f5ee"; +@fa-var-dribbble: "\f17d"; +@fa-var-dribbble-square: "\f397"; +@fa-var-dropbox: "\f16b"; +@fa-var-drum: "\f569"; +@fa-var-drum-steelpan: "\f56a"; +@fa-var-drumstick-bite: "\f6d7"; +@fa-var-drupal: "\f1a9"; +@fa-var-dumbbell: "\f44b"; +@fa-var-dumpster: "\f793"; +@fa-var-dumpster-fire: "\f794"; +@fa-var-dungeon: "\f6d9"; +@fa-var-dyalog: "\f399"; +@fa-var-earlybirds: "\f39a"; +@fa-var-ebay: "\f4f4"; +@fa-var-edge: "\f282"; +@fa-var-edge-legacy: "\e078"; +@fa-var-edit: "\f044"; +@fa-var-egg: "\f7fb"; +@fa-var-eject: "\f052"; +@fa-var-elementor: "\f430"; +@fa-var-ellipsis-h: "\f141"; +@fa-var-ellipsis-v: "\f142"; +@fa-var-ello: "\f5f1"; +@fa-var-ember: "\f423"; +@fa-var-empire: "\f1d1"; +@fa-var-envelope: "\f0e0"; +@fa-var-envelope-open: "\f2b6"; +@fa-var-envelope-open-text: "\f658"; +@fa-var-envelope-square: "\f199"; +@fa-var-envira: "\f299"; +@fa-var-equals: "\f52c"; +@fa-var-eraser: "\f12d"; +@fa-var-erlang: "\f39d"; +@fa-var-ethereum: "\f42e"; +@fa-var-ethernet: "\f796"; +@fa-var-etsy: "\f2d7"; +@fa-var-euro-sign: "\f153"; +@fa-var-evernote: "\f839"; +@fa-var-exchange-alt: "\f362"; +@fa-var-exclamation: "\f12a"; +@fa-var-exclamation-circle: "\f06a"; +@fa-var-exclamation-triangle: "\f071"; +@fa-var-expand: "\f065"; +@fa-var-expand-alt: "\f424"; +@fa-var-expand-arrows-alt: "\f31e"; +@fa-var-expeditedssl: "\f23e"; +@fa-var-external-link-alt: "\f35d"; +@fa-var-external-link-square-alt: "\f360"; +@fa-var-eye: "\f06e"; +@fa-var-eye-dropper: "\f1fb"; +@fa-var-eye-slash: "\f070"; +@fa-var-facebook: "\f09a"; +@fa-var-facebook-f: "\f39e"; +@fa-var-facebook-messenger: "\f39f"; +@fa-var-facebook-square: "\f082"; +@fa-var-fan: "\f863"; +@fa-var-fantasy-flight-games: "\f6dc"; +@fa-var-fast-backward: "\f049"; +@fa-var-fast-forward: "\f050"; +@fa-var-faucet: "\e005"; +@fa-var-fax: "\f1ac"; +@fa-var-feather: "\f52d"; +@fa-var-feather-alt: "\f56b"; +@fa-var-fedex: "\f797"; +@fa-var-fedora: "\f798"; +@fa-var-female: "\f182"; +@fa-var-fighter-jet: "\f0fb"; +@fa-var-figma: "\f799"; +@fa-var-file: "\f15b"; +@fa-var-file-alt: "\f15c"; +@fa-var-file-archive: "\f1c6"; +@fa-var-file-audio: "\f1c7"; +@fa-var-file-code: "\f1c9"; +@fa-var-file-contract: "\f56c"; +@fa-var-file-csv: "\f6dd"; +@fa-var-file-download: "\f56d"; +@fa-var-file-excel: "\f1c3"; +@fa-var-file-export: "\f56e"; +@fa-var-file-image: "\f1c5"; +@fa-var-file-import: "\f56f"; +@fa-var-file-invoice: "\f570"; +@fa-var-file-invoice-dollar: "\f571"; +@fa-var-file-medical: "\f477"; +@fa-var-file-medical-alt: "\f478"; +@fa-var-file-pdf: "\f1c1"; +@fa-var-file-powerpoint: "\f1c4"; +@fa-var-file-prescription: "\f572"; +@fa-var-file-signature: "\f573"; +@fa-var-file-upload: "\f574"; +@fa-var-file-video: "\f1c8"; +@fa-var-file-word: "\f1c2"; +@fa-var-fill: "\f575"; +@fa-var-fill-drip: "\f576"; +@fa-var-film: "\f008"; +@fa-var-filter: "\f0b0"; +@fa-var-fingerprint: "\f577"; +@fa-var-fire: "\f06d"; +@fa-var-fire-alt: "\f7e4"; +@fa-var-fire-extinguisher: "\f134"; +@fa-var-firefox: "\f269"; +@fa-var-firefox-browser: "\e007"; +@fa-var-first-aid: "\f479"; +@fa-var-first-order: "\f2b0"; +@fa-var-first-order-alt: "\f50a"; +@fa-var-firstdraft: "\f3a1"; +@fa-var-fish: "\f578"; +@fa-var-fist-raised: "\f6de"; +@fa-var-flag: "\f024"; +@fa-var-flag-checkered: "\f11e"; +@fa-var-flag-usa: "\f74d"; +@fa-var-flask: "\f0c3"; +@fa-var-flickr: "\f16e"; +@fa-var-flipboard: "\f44d"; +@fa-var-flushed: "\f579"; +@fa-var-fly: "\f417"; +@fa-var-folder: "\f07b"; +@fa-var-folder-minus: "\f65d"; +@fa-var-folder-open: "\f07c"; +@fa-var-folder-plus: "\f65e"; +@fa-var-font: "\f031"; +@fa-var-font-awesome: "\f2b4"; +@fa-var-font-awesome-alt: "\f35c"; +@fa-var-font-awesome-flag: "\f425"; +@fa-var-font-awesome-logo-full: "\f4e6"; +@fa-var-fonticons: "\f280"; +@fa-var-fonticons-fi: "\f3a2"; +@fa-var-football-ball: "\f44e"; +@fa-var-fort-awesome: "\f286"; +@fa-var-fort-awesome-alt: "\f3a3"; +@fa-var-forumbee: "\f211"; +@fa-var-forward: "\f04e"; +@fa-var-foursquare: "\f180"; +@fa-var-free-code-camp: "\f2c5"; +@fa-var-freebsd: "\f3a4"; +@fa-var-frog: "\f52e"; +@fa-var-frown: "\f119"; +@fa-var-frown-open: "\f57a"; +@fa-var-fulcrum: "\f50b"; +@fa-var-funnel-dollar: "\f662"; +@fa-var-futbol: "\f1e3"; +@fa-var-galactic-republic: "\f50c"; +@fa-var-galactic-senate: "\f50d"; +@fa-var-gamepad: "\f11b"; +@fa-var-gas-pump: "\f52f"; +@fa-var-gavel: "\f0e3"; +@fa-var-gem: "\f3a5"; +@fa-var-genderless: "\f22d"; +@fa-var-get-pocket: "\f265"; +@fa-var-gg: "\f260"; +@fa-var-gg-circle: "\f261"; +@fa-var-ghost: "\f6e2"; +@fa-var-gift: "\f06b"; +@fa-var-gifts: "\f79c"; +@fa-var-git: "\f1d3"; +@fa-var-git-alt: "\f841"; +@fa-var-git-square: "\f1d2"; +@fa-var-github: "\f09b"; +@fa-var-github-alt: "\f113"; +@fa-var-github-square: "\f092"; +@fa-var-gitkraken: "\f3a6"; +@fa-var-gitlab: "\f296"; +@fa-var-gitter: "\f426"; +@fa-var-glass-cheers: "\f79f"; +@fa-var-glass-martini: "\f000"; +@fa-var-glass-martini-alt: "\f57b"; +@fa-var-glass-whiskey: "\f7a0"; +@fa-var-glasses: "\f530"; +@fa-var-glide: "\f2a5"; +@fa-var-glide-g: "\f2a6"; +@fa-var-globe: "\f0ac"; +@fa-var-globe-africa: "\f57c"; +@fa-var-globe-americas: "\f57d"; +@fa-var-globe-asia: "\f57e"; +@fa-var-globe-europe: "\f7a2"; +@fa-var-gofore: "\f3a7"; +@fa-var-golf-ball: "\f450"; +@fa-var-goodreads: "\f3a8"; +@fa-var-goodreads-g: "\f3a9"; +@fa-var-google: "\f1a0"; +@fa-var-google-drive: "\f3aa"; +@fa-var-google-pay: "\e079"; +@fa-var-google-play: "\f3ab"; +@fa-var-google-plus: "\f2b3"; +@fa-var-google-plus-g: "\f0d5"; +@fa-var-google-plus-square: "\f0d4"; +@fa-var-google-wallet: "\f1ee"; +@fa-var-gopuram: "\f664"; +@fa-var-graduation-cap: "\f19d"; +@fa-var-gratipay: "\f184"; +@fa-var-grav: "\f2d6"; +@fa-var-greater-than: "\f531"; +@fa-var-greater-than-equal: "\f532"; +@fa-var-grimace: "\f57f"; +@fa-var-grin: "\f580"; +@fa-var-grin-alt: "\f581"; +@fa-var-grin-beam: "\f582"; +@fa-var-grin-beam-sweat: "\f583"; +@fa-var-grin-hearts: "\f584"; +@fa-var-grin-squint: "\f585"; +@fa-var-grin-squint-tears: "\f586"; +@fa-var-grin-stars: "\f587"; +@fa-var-grin-tears: "\f588"; +@fa-var-grin-tongue: "\f589"; +@fa-var-grin-tongue-squint: "\f58a"; +@fa-var-grin-tongue-wink: "\f58b"; +@fa-var-grin-wink: "\f58c"; +@fa-var-grip-horizontal: "\f58d"; +@fa-var-grip-lines: "\f7a4"; +@fa-var-grip-lines-vertical: "\f7a5"; +@fa-var-grip-vertical: "\f58e"; +@fa-var-gripfire: "\f3ac"; +@fa-var-grunt: "\f3ad"; +@fa-var-guilded: "\e07e"; +@fa-var-guitar: "\f7a6"; +@fa-var-gulp: "\f3ae"; +@fa-var-h-square: "\f0fd"; +@fa-var-hacker-news: "\f1d4"; +@fa-var-hacker-news-square: "\f3af"; +@fa-var-hackerrank: "\f5f7"; +@fa-var-hamburger: "\f805"; +@fa-var-hammer: "\f6e3"; +@fa-var-hamsa: "\f665"; +@fa-var-hand-holding: "\f4bd"; +@fa-var-hand-holding-heart: "\f4be"; +@fa-var-hand-holding-medical: "\e05c"; +@fa-var-hand-holding-usd: "\f4c0"; +@fa-var-hand-holding-water: "\f4c1"; +@fa-var-hand-lizard: "\f258"; +@fa-var-hand-middle-finger: "\f806"; +@fa-var-hand-paper: "\f256"; +@fa-var-hand-peace: "\f25b"; +@fa-var-hand-point-down: "\f0a7"; +@fa-var-hand-point-left: "\f0a5"; +@fa-var-hand-point-right: "\f0a4"; +@fa-var-hand-point-up: "\f0a6"; +@fa-var-hand-pointer: "\f25a"; +@fa-var-hand-rock: "\f255"; +@fa-var-hand-scissors: "\f257"; +@fa-var-hand-sparkles: "\e05d"; +@fa-var-hand-spock: "\f259"; +@fa-var-hands: "\f4c2"; +@fa-var-hands-helping: "\f4c4"; +@fa-var-hands-wash: "\e05e"; +@fa-var-handshake: "\f2b5"; +@fa-var-handshake-alt-slash: "\e05f"; +@fa-var-handshake-slash: "\e060"; +@fa-var-hanukiah: "\f6e6"; +@fa-var-hard-hat: "\f807"; +@fa-var-hashtag: "\f292"; +@fa-var-hat-cowboy: "\f8c0"; +@fa-var-hat-cowboy-side: "\f8c1"; +@fa-var-hat-wizard: "\f6e8"; +@fa-var-hdd: "\f0a0"; +@fa-var-head-side-cough: "\e061"; +@fa-var-head-side-cough-slash: "\e062"; +@fa-var-head-side-mask: "\e063"; +@fa-var-head-side-virus: "\e064"; +@fa-var-heading: "\f1dc"; +@fa-var-headphones: "\f025"; +@fa-var-headphones-alt: "\f58f"; +@fa-var-headset: "\f590"; +@fa-var-heart: "\f004"; +@fa-var-heart-broken: "\f7a9"; +@fa-var-heartbeat: "\f21e"; +@fa-var-helicopter: "\f533"; +@fa-var-highlighter: "\f591"; +@fa-var-hiking: "\f6ec"; +@fa-var-hippo: "\f6ed"; +@fa-var-hips: "\f452"; +@fa-var-hire-a-helper: "\f3b0"; +@fa-var-history: "\f1da"; +@fa-var-hive: "\e07f"; +@fa-var-hockey-puck: "\f453"; +@fa-var-holly-berry: "\f7aa"; +@fa-var-home: "\f015"; +@fa-var-hooli: "\f427"; +@fa-var-hornbill: "\f592"; +@fa-var-horse: "\f6f0"; +@fa-var-horse-head: "\f7ab"; +@fa-var-hospital: "\f0f8"; +@fa-var-hospital-alt: "\f47d"; +@fa-var-hospital-symbol: "\f47e"; +@fa-var-hospital-user: "\f80d"; +@fa-var-hot-tub: "\f593"; +@fa-var-hotdog: "\f80f"; +@fa-var-hotel: "\f594"; +@fa-var-hotjar: "\f3b1"; +@fa-var-hourglass: "\f254"; +@fa-var-hourglass-end: "\f253"; +@fa-var-hourglass-half: "\f252"; +@fa-var-hourglass-start: "\f251"; +@fa-var-house-damage: "\f6f1"; +@fa-var-house-user: "\e065"; +@fa-var-houzz: "\f27c"; +@fa-var-hryvnia: "\f6f2"; +@fa-var-html5: "\f13b"; +@fa-var-hubspot: "\f3b2"; +@fa-var-i-cursor: "\f246"; +@fa-var-ice-cream: "\f810"; +@fa-var-icicles: "\f7ad"; +@fa-var-icons: "\f86d"; +@fa-var-id-badge: "\f2c1"; +@fa-var-id-card: "\f2c2"; +@fa-var-id-card-alt: "\f47f"; +@fa-var-ideal: "\e013"; +@fa-var-igloo: "\f7ae"; +@fa-var-image: "\f03e"; +@fa-var-images: "\f302"; +@fa-var-imdb: "\f2d8"; +@fa-var-inbox: "\f01c"; +@fa-var-indent: "\f03c"; +@fa-var-industry: "\f275"; +@fa-var-infinity: "\f534"; +@fa-var-info: "\f129"; +@fa-var-info-circle: "\f05a"; +@fa-var-innosoft: "\e080"; +@fa-var-instagram: "\f16d"; +@fa-var-instagram-square: "\e055"; +@fa-var-instalod: "\e081"; +@fa-var-intercom: "\f7af"; +@fa-var-internet-explorer: "\f26b"; +@fa-var-invision: "\f7b0"; +@fa-var-ioxhost: "\f208"; +@fa-var-italic: "\f033"; +@fa-var-itch-io: "\f83a"; +@fa-var-itunes: "\f3b4"; +@fa-var-itunes-note: "\f3b5"; +@fa-var-java: "\f4e4"; +@fa-var-jedi: "\f669"; +@fa-var-jedi-order: "\f50e"; +@fa-var-jenkins: "\f3b6"; +@fa-var-jira: "\f7b1"; +@fa-var-joget: "\f3b7"; +@fa-var-joint: "\f595"; +@fa-var-joomla: "\f1aa"; +@fa-var-journal-whills: "\f66a"; +@fa-var-js: "\f3b8"; +@fa-var-js-square: "\f3b9"; +@fa-var-jsfiddle: "\f1cc"; +@fa-var-kaaba: "\f66b"; +@fa-var-kaggle: "\f5fa"; +@fa-var-key: "\f084"; +@fa-var-keybase: "\f4f5"; +@fa-var-keyboard: "\f11c"; +@fa-var-keycdn: "\f3ba"; +@fa-var-khanda: "\f66d"; +@fa-var-kickstarter: "\f3bb"; +@fa-var-kickstarter-k: "\f3bc"; +@fa-var-kiss: "\f596"; +@fa-var-kiss-beam: "\f597"; +@fa-var-kiss-wink-heart: "\f598"; +@fa-var-kiwi-bird: "\f535"; +@fa-var-korvue: "\f42f"; +@fa-var-landmark: "\f66f"; +@fa-var-language: "\f1ab"; +@fa-var-laptop: "\f109"; +@fa-var-laptop-code: "\f5fc"; +@fa-var-laptop-house: "\e066"; +@fa-var-laptop-medical: "\f812"; +@fa-var-laravel: "\f3bd"; +@fa-var-lastfm: "\f202"; +@fa-var-lastfm-square: "\f203"; +@fa-var-laugh: "\f599"; +@fa-var-laugh-beam: "\f59a"; +@fa-var-laugh-squint: "\f59b"; +@fa-var-laugh-wink: "\f59c"; +@fa-var-layer-group: "\f5fd"; +@fa-var-leaf: "\f06c"; +@fa-var-leanpub: "\f212"; +@fa-var-lemon: "\f094"; +@fa-var-less: "\f41d"; +@fa-var-less-than: "\f536"; +@fa-var-less-than-equal: "\f537"; +@fa-var-level-down-alt: "\f3be"; +@fa-var-level-up-alt: "\f3bf"; +@fa-var-life-ring: "\f1cd"; +@fa-var-lightbulb: "\f0eb"; +@fa-var-line: "\f3c0"; +@fa-var-link: "\f0c1"; +@fa-var-linkedin: "\f08c"; +@fa-var-linkedin-in: "\f0e1"; +@fa-var-linode: "\f2b8"; +@fa-var-linux: "\f17c"; +@fa-var-lira-sign: "\f195"; +@fa-var-list: "\f03a"; +@fa-var-list-alt: "\f022"; +@fa-var-list-ol: "\f0cb"; +@fa-var-list-ul: "\f0ca"; +@fa-var-location-arrow: "\f124"; +@fa-var-lock: "\f023"; +@fa-var-lock-open: "\f3c1"; +@fa-var-long-arrow-alt-down: "\f309"; +@fa-var-long-arrow-alt-left: "\f30a"; +@fa-var-long-arrow-alt-right: "\f30b"; +@fa-var-long-arrow-alt-up: "\f30c"; +@fa-var-low-vision: "\f2a8"; +@fa-var-luggage-cart: "\f59d"; +@fa-var-lungs: "\f604"; +@fa-var-lungs-virus: "\e067"; +@fa-var-lyft: "\f3c3"; +@fa-var-magento: "\f3c4"; +@fa-var-magic: "\f0d0"; +@fa-var-magnet: "\f076"; +@fa-var-mail-bulk: "\f674"; +@fa-var-mailchimp: "\f59e"; +@fa-var-male: "\f183"; +@fa-var-mandalorian: "\f50f"; +@fa-var-map: "\f279"; +@fa-var-map-marked: "\f59f"; +@fa-var-map-marked-alt: "\f5a0"; +@fa-var-map-marker: "\f041"; +@fa-var-map-marker-alt: "\f3c5"; +@fa-var-map-pin: "\f276"; +@fa-var-map-signs: "\f277"; +@fa-var-markdown: "\f60f"; +@fa-var-marker: "\f5a1"; +@fa-var-mars: "\f222"; +@fa-var-mars-double: "\f227"; +@fa-var-mars-stroke: "\f229"; +@fa-var-mars-stroke-h: "\f22b"; +@fa-var-mars-stroke-v: "\f22a"; +@fa-var-mask: "\f6fa"; +@fa-var-mastodon: "\f4f6"; +@fa-var-maxcdn: "\f136"; +@fa-var-mdb: "\f8ca"; +@fa-var-medal: "\f5a2"; +@fa-var-medapps: "\f3c6"; +@fa-var-medium: "\f23a"; +@fa-var-medium-m: "\f3c7"; +@fa-var-medkit: "\f0fa"; +@fa-var-medrt: "\f3c8"; +@fa-var-meetup: "\f2e0"; +@fa-var-megaport: "\f5a3"; +@fa-var-meh: "\f11a"; +@fa-var-meh-blank: "\f5a4"; +@fa-var-meh-rolling-eyes: "\f5a5"; +@fa-var-memory: "\f538"; +@fa-var-mendeley: "\f7b3"; +@fa-var-menorah: "\f676"; +@fa-var-mercury: "\f223"; +@fa-var-meteor: "\f753"; +@fa-var-microblog: "\e01a"; +@fa-var-microchip: "\f2db"; +@fa-var-microphone: "\f130"; +@fa-var-microphone-alt: "\f3c9"; +@fa-var-microphone-alt-slash: "\f539"; +@fa-var-microphone-slash: "\f131"; +@fa-var-microscope: "\f610"; +@fa-var-microsoft: "\f3ca"; +@fa-var-minus: "\f068"; +@fa-var-minus-circle: "\f056"; +@fa-var-minus-square: "\f146"; +@fa-var-mitten: "\f7b5"; +@fa-var-mix: "\f3cb"; +@fa-var-mixcloud: "\f289"; +@fa-var-mixer: "\e056"; +@fa-var-mizuni: "\f3cc"; +@fa-var-mobile: "\f10b"; +@fa-var-mobile-alt: "\f3cd"; +@fa-var-modx: "\f285"; +@fa-var-monero: "\f3d0"; +@fa-var-money-bill: "\f0d6"; +@fa-var-money-bill-alt: "\f3d1"; +@fa-var-money-bill-wave: "\f53a"; +@fa-var-money-bill-wave-alt: "\f53b"; +@fa-var-money-check: "\f53c"; +@fa-var-money-check-alt: "\f53d"; +@fa-var-monument: "\f5a6"; +@fa-var-moon: "\f186"; +@fa-var-mortar-pestle: "\f5a7"; +@fa-var-mosque: "\f678"; +@fa-var-motorcycle: "\f21c"; +@fa-var-mountain: "\f6fc"; +@fa-var-mouse: "\f8cc"; +@fa-var-mouse-pointer: "\f245"; +@fa-var-mug-hot: "\f7b6"; +@fa-var-music: "\f001"; +@fa-var-napster: "\f3d2"; +@fa-var-neos: "\f612"; +@fa-var-network-wired: "\f6ff"; +@fa-var-neuter: "\f22c"; +@fa-var-newspaper: "\f1ea"; +@fa-var-nimblr: "\f5a8"; +@fa-var-node: "\f419"; +@fa-var-node-js: "\f3d3"; +@fa-var-not-equal: "\f53e"; +@fa-var-notes-medical: "\f481"; +@fa-var-npm: "\f3d4"; +@fa-var-ns8: "\f3d5"; +@fa-var-nutritionix: "\f3d6"; +@fa-var-object-group: "\f247"; +@fa-var-object-ungroup: "\f248"; +@fa-var-octopus-deploy: "\e082"; +@fa-var-odnoklassniki: "\f263"; +@fa-var-odnoklassniki-square: "\f264"; +@fa-var-oil-can: "\f613"; +@fa-var-old-republic: "\f510"; +@fa-var-om: "\f679"; +@fa-var-opencart: "\f23d"; +@fa-var-openid: "\f19b"; +@fa-var-opera: "\f26a"; +@fa-var-optin-monster: "\f23c"; +@fa-var-orcid: "\f8d2"; +@fa-var-osi: "\f41a"; +@fa-var-otter: "\f700"; +@fa-var-outdent: "\f03b"; +@fa-var-page4: "\f3d7"; +@fa-var-pagelines: "\f18c"; +@fa-var-pager: "\f815"; +@fa-var-paint-brush: "\f1fc"; +@fa-var-paint-roller: "\f5aa"; +@fa-var-palette: "\f53f"; +@fa-var-palfed: "\f3d8"; +@fa-var-pallet: "\f482"; +@fa-var-paper-plane: "\f1d8"; +@fa-var-paperclip: "\f0c6"; +@fa-var-parachute-box: "\f4cd"; +@fa-var-paragraph: "\f1dd"; +@fa-var-parking: "\f540"; +@fa-var-passport: "\f5ab"; +@fa-var-pastafarianism: "\f67b"; +@fa-var-paste: "\f0ea"; +@fa-var-patreon: "\f3d9"; +@fa-var-pause: "\f04c"; +@fa-var-pause-circle: "\f28b"; +@fa-var-paw: "\f1b0"; +@fa-var-paypal: "\f1ed"; +@fa-var-peace: "\f67c"; +@fa-var-pen: "\f304"; +@fa-var-pen-alt: "\f305"; +@fa-var-pen-fancy: "\f5ac"; +@fa-var-pen-nib: "\f5ad"; +@fa-var-pen-square: "\f14b"; +@fa-var-pencil-alt: "\f303"; +@fa-var-pencil-ruler: "\f5ae"; +@fa-var-penny-arcade: "\f704"; +@fa-var-people-arrows: "\e068"; +@fa-var-people-carry: "\f4ce"; +@fa-var-pepper-hot: "\f816"; +@fa-var-perbyte: "\e083"; +@fa-var-percent: "\f295"; +@fa-var-percentage: "\f541"; +@fa-var-periscope: "\f3da"; +@fa-var-person-booth: "\f756"; +@fa-var-phabricator: "\f3db"; +@fa-var-phoenix-framework: "\f3dc"; +@fa-var-phoenix-squadron: "\f511"; +@fa-var-phone: "\f095"; +@fa-var-phone-alt: "\f879"; +@fa-var-phone-slash: "\f3dd"; +@fa-var-phone-square: "\f098"; +@fa-var-phone-square-alt: "\f87b"; +@fa-var-phone-volume: "\f2a0"; +@fa-var-photo-video: "\f87c"; +@fa-var-php: "\f457"; +@fa-var-pied-piper: "\f2ae"; +@fa-var-pied-piper-alt: "\f1a8"; +@fa-var-pied-piper-hat: "\f4e5"; +@fa-var-pied-piper-pp: "\f1a7"; +@fa-var-pied-piper-square: "\e01e"; +@fa-var-piggy-bank: "\f4d3"; +@fa-var-pills: "\f484"; +@fa-var-pinterest: "\f0d2"; +@fa-var-pinterest-p: "\f231"; +@fa-var-pinterest-square: "\f0d3"; +@fa-var-pizza-slice: "\f818"; +@fa-var-place-of-worship: "\f67f"; +@fa-var-plane: "\f072"; +@fa-var-plane-arrival: "\f5af"; +@fa-var-plane-departure: "\f5b0"; +@fa-var-plane-slash: "\e069"; +@fa-var-play: "\f04b"; +@fa-var-play-circle: "\f144"; +@fa-var-playstation: "\f3df"; +@fa-var-plug: "\f1e6"; +@fa-var-plus: "\f067"; +@fa-var-plus-circle: "\f055"; +@fa-var-plus-square: "\f0fe"; +@fa-var-podcast: "\f2ce"; +@fa-var-poll: "\f681"; +@fa-var-poll-h: "\f682"; +@fa-var-poo: "\f2fe"; +@fa-var-poo-storm: "\f75a"; +@fa-var-poop: "\f619"; +@fa-var-portrait: "\f3e0"; +@fa-var-pound-sign: "\f154"; +@fa-var-power-off: "\f011"; +@fa-var-pray: "\f683"; +@fa-var-praying-hands: "\f684"; +@fa-var-prescription: "\f5b1"; +@fa-var-prescription-bottle: "\f485"; +@fa-var-prescription-bottle-alt: "\f486"; +@fa-var-print: "\f02f"; +@fa-var-procedures: "\f487"; +@fa-var-product-hunt: "\f288"; +@fa-var-project-diagram: "\f542"; +@fa-var-pump-medical: "\e06a"; +@fa-var-pump-soap: "\e06b"; +@fa-var-pushed: "\f3e1"; +@fa-var-puzzle-piece: "\f12e"; +@fa-var-python: "\f3e2"; +@fa-var-qq: "\f1d6"; +@fa-var-qrcode: "\f029"; +@fa-var-question: "\f128"; +@fa-var-question-circle: "\f059"; +@fa-var-quidditch: "\f458"; +@fa-var-quinscape: "\f459"; +@fa-var-quora: "\f2c4"; +@fa-var-quote-left: "\f10d"; +@fa-var-quote-right: "\f10e"; +@fa-var-quran: "\f687"; +@fa-var-r-project: "\f4f7"; +@fa-var-radiation: "\f7b9"; +@fa-var-radiation-alt: "\f7ba"; +@fa-var-rainbow: "\f75b"; +@fa-var-random: "\f074"; +@fa-var-raspberry-pi: "\f7bb"; +@fa-var-ravelry: "\f2d9"; +@fa-var-react: "\f41b"; +@fa-var-reacteurope: "\f75d"; +@fa-var-readme: "\f4d5"; +@fa-var-rebel: "\f1d0"; +@fa-var-receipt: "\f543"; +@fa-var-record-vinyl: "\f8d9"; +@fa-var-recycle: "\f1b8"; +@fa-var-red-river: "\f3e3"; +@fa-var-reddit: "\f1a1"; +@fa-var-reddit-alien: "\f281"; +@fa-var-reddit-square: "\f1a2"; +@fa-var-redhat: "\f7bc"; +@fa-var-redo: "\f01e"; +@fa-var-redo-alt: "\f2f9"; +@fa-var-registered: "\f25d"; +@fa-var-remove-format: "\f87d"; +@fa-var-renren: "\f18b"; +@fa-var-reply: "\f3e5"; +@fa-var-reply-all: "\f122"; +@fa-var-replyd: "\f3e6"; +@fa-var-republican: "\f75e"; +@fa-var-researchgate: "\f4f8"; +@fa-var-resolving: "\f3e7"; +@fa-var-restroom: "\f7bd"; +@fa-var-retweet: "\f079"; +@fa-var-rev: "\f5b2"; +@fa-var-ribbon: "\f4d6"; +@fa-var-ring: "\f70b"; +@fa-var-road: "\f018"; +@fa-var-robot: "\f544"; +@fa-var-rocket: "\f135"; +@fa-var-rocketchat: "\f3e8"; +@fa-var-rockrms: "\f3e9"; +@fa-var-route: "\f4d7"; +@fa-var-rss: "\f09e"; +@fa-var-rss-square: "\f143"; +@fa-var-ruble-sign: "\f158"; +@fa-var-ruler: "\f545"; +@fa-var-ruler-combined: "\f546"; +@fa-var-ruler-horizontal: "\f547"; +@fa-var-ruler-vertical: "\f548"; +@fa-var-running: "\f70c"; +@fa-var-rupee-sign: "\f156"; +@fa-var-rust: "\e07a"; +@fa-var-sad-cry: "\f5b3"; +@fa-var-sad-tear: "\f5b4"; +@fa-var-safari: "\f267"; +@fa-var-salesforce: "\f83b"; +@fa-var-sass: "\f41e"; +@fa-var-satellite: "\f7bf"; +@fa-var-satellite-dish: "\f7c0"; +@fa-var-save: "\f0c7"; +@fa-var-schlix: "\f3ea"; +@fa-var-school: "\f549"; +@fa-var-screwdriver: "\f54a"; +@fa-var-scribd: "\f28a"; +@fa-var-scroll: "\f70e"; +@fa-var-sd-card: "\f7c2"; +@fa-var-search: "\f002"; +@fa-var-search-dollar: "\f688"; +@fa-var-search-location: "\f689"; +@fa-var-search-minus: "\f010"; +@fa-var-search-plus: "\f00e"; +@fa-var-searchengin: "\f3eb"; +@fa-var-seedling: "\f4d8"; +@fa-var-sellcast: "\f2da"; +@fa-var-sellsy: "\f213"; +@fa-var-server: "\f233"; +@fa-var-servicestack: "\f3ec"; +@fa-var-shapes: "\f61f"; +@fa-var-share: "\f064"; +@fa-var-share-alt: "\f1e0"; +@fa-var-share-alt-square: "\f1e1"; +@fa-var-share-square: "\f14d"; +@fa-var-shekel-sign: "\f20b"; +@fa-var-shield-alt: "\f3ed"; +@fa-var-shield-virus: "\e06c"; +@fa-var-ship: "\f21a"; +@fa-var-shipping-fast: "\f48b"; +@fa-var-shirtsinbulk: "\f214"; +@fa-var-shoe-prints: "\f54b"; +@fa-var-shopify: "\e057"; +@fa-var-shopping-bag: "\f290"; +@fa-var-shopping-basket: "\f291"; +@fa-var-shopping-cart: "\f07a"; +@fa-var-shopware: "\f5b5"; +@fa-var-shower: "\f2cc"; +@fa-var-shuttle-van: "\f5b6"; +@fa-var-sign: "\f4d9"; +@fa-var-sign-in-alt: "\f2f6"; +@fa-var-sign-language: "\f2a7"; +@fa-var-sign-out-alt: "\f2f5"; +@fa-var-signal: "\f012"; +@fa-var-signature: "\f5b7"; +@fa-var-sim-card: "\f7c4"; +@fa-var-simplybuilt: "\f215"; +@fa-var-sink: "\e06d"; +@fa-var-sistrix: "\f3ee"; +@fa-var-sitemap: "\f0e8"; +@fa-var-sith: "\f512"; +@fa-var-skating: "\f7c5"; +@fa-var-sketch: "\f7c6"; +@fa-var-skiing: "\f7c9"; +@fa-var-skiing-nordic: "\f7ca"; +@fa-var-skull: "\f54c"; +@fa-var-skull-crossbones: "\f714"; +@fa-var-skyatlas: "\f216"; +@fa-var-skype: "\f17e"; +@fa-var-slack: "\f198"; +@fa-var-slack-hash: "\f3ef"; +@fa-var-slash: "\f715"; +@fa-var-sleigh: "\f7cc"; +@fa-var-sliders-h: "\f1de"; +@fa-var-slideshare: "\f1e7"; +@fa-var-smile: "\f118"; +@fa-var-smile-beam: "\f5b8"; +@fa-var-smile-wink: "\f4da"; +@fa-var-smog: "\f75f"; +@fa-var-smoking: "\f48d"; +@fa-var-smoking-ban: "\f54d"; +@fa-var-sms: "\f7cd"; +@fa-var-snapchat: "\f2ab"; +@fa-var-snapchat-ghost: "\f2ac"; +@fa-var-snapchat-square: "\f2ad"; +@fa-var-snowboarding: "\f7ce"; +@fa-var-snowflake: "\f2dc"; +@fa-var-snowman: "\f7d0"; +@fa-var-snowplow: "\f7d2"; +@fa-var-soap: "\e06e"; +@fa-var-socks: "\f696"; +@fa-var-solar-panel: "\f5ba"; +@fa-var-sort: "\f0dc"; +@fa-var-sort-alpha-down: "\f15d"; +@fa-var-sort-alpha-down-alt: "\f881"; +@fa-var-sort-alpha-up: "\f15e"; +@fa-var-sort-alpha-up-alt: "\f882"; +@fa-var-sort-amount-down: "\f160"; +@fa-var-sort-amount-down-alt: "\f884"; +@fa-var-sort-amount-up: "\f161"; +@fa-var-sort-amount-up-alt: "\f885"; +@fa-var-sort-down: "\f0dd"; +@fa-var-sort-numeric-down: "\f162"; +@fa-var-sort-numeric-down-alt: "\f886"; +@fa-var-sort-numeric-up: "\f163"; +@fa-var-sort-numeric-up-alt: "\f887"; +@fa-var-sort-up: "\f0de"; +@fa-var-soundcloud: "\f1be"; +@fa-var-sourcetree: "\f7d3"; +@fa-var-spa: "\f5bb"; +@fa-var-space-shuttle: "\f197"; +@fa-var-speakap: "\f3f3"; +@fa-var-speaker-deck: "\f83c"; +@fa-var-spell-check: "\f891"; +@fa-var-spider: "\f717"; +@fa-var-spinner: "\f110"; +@fa-var-splotch: "\f5bc"; +@fa-var-spotify: "\f1bc"; +@fa-var-spray-can: "\f5bd"; +@fa-var-square: "\f0c8"; +@fa-var-square-full: "\f45c"; +@fa-var-square-root-alt: "\f698"; +@fa-var-squarespace: "\f5be"; +@fa-var-stack-exchange: "\f18d"; +@fa-var-stack-overflow: "\f16c"; +@fa-var-stackpath: "\f842"; +@fa-var-stamp: "\f5bf"; +@fa-var-star: "\f005"; +@fa-var-star-and-crescent: "\f699"; +@fa-var-star-half: "\f089"; +@fa-var-star-half-alt: "\f5c0"; +@fa-var-star-of-david: "\f69a"; +@fa-var-star-of-life: "\f621"; +@fa-var-staylinked: "\f3f5"; +@fa-var-steam: "\f1b6"; +@fa-var-steam-square: "\f1b7"; +@fa-var-steam-symbol: "\f3f6"; +@fa-var-step-backward: "\f048"; +@fa-var-step-forward: "\f051"; +@fa-var-stethoscope: "\f0f1"; +@fa-var-sticker-mule: "\f3f7"; +@fa-var-sticky-note: "\f249"; +@fa-var-stop: "\f04d"; +@fa-var-stop-circle: "\f28d"; +@fa-var-stopwatch: "\f2f2"; +@fa-var-stopwatch-20: "\e06f"; +@fa-var-store: "\f54e"; +@fa-var-store-alt: "\f54f"; +@fa-var-store-alt-slash: "\e070"; +@fa-var-store-slash: "\e071"; +@fa-var-strava: "\f428"; +@fa-var-stream: "\f550"; +@fa-var-street-view: "\f21d"; +@fa-var-strikethrough: "\f0cc"; +@fa-var-stripe: "\f429"; +@fa-var-stripe-s: "\f42a"; +@fa-var-stroopwafel: "\f551"; +@fa-var-studiovinari: "\f3f8"; +@fa-var-stumbleupon: "\f1a4"; +@fa-var-stumbleupon-circle: "\f1a3"; +@fa-var-subscript: "\f12c"; +@fa-var-subway: "\f239"; +@fa-var-suitcase: "\f0f2"; +@fa-var-suitcase-rolling: "\f5c1"; +@fa-var-sun: "\f185"; +@fa-var-superpowers: "\f2dd"; +@fa-var-superscript: "\f12b"; +@fa-var-supple: "\f3f9"; +@fa-var-surprise: "\f5c2"; +@fa-var-suse: "\f7d6"; +@fa-var-swatchbook: "\f5c3"; +@fa-var-swift: "\f8e1"; +@fa-var-swimmer: "\f5c4"; +@fa-var-swimming-pool: "\f5c5"; +@fa-var-symfony: "\f83d"; +@fa-var-synagogue: "\f69b"; +@fa-var-sync: "\f021"; +@fa-var-sync-alt: "\f2f1"; +@fa-var-syringe: "\f48e"; +@fa-var-table: "\f0ce"; +@fa-var-table-tennis: "\f45d"; +@fa-var-tablet: "\f10a"; +@fa-var-tablet-alt: "\f3fa"; +@fa-var-tablets: "\f490"; +@fa-var-tachometer-alt: "\f3fd"; +@fa-var-tag: "\f02b"; +@fa-var-tags: "\f02c"; +@fa-var-tape: "\f4db"; +@fa-var-tasks: "\f0ae"; +@fa-var-taxi: "\f1ba"; +@fa-var-teamspeak: "\f4f9"; +@fa-var-teeth: "\f62e"; +@fa-var-teeth-open: "\f62f"; +@fa-var-telegram: "\f2c6"; +@fa-var-telegram-plane: "\f3fe"; +@fa-var-temperature-high: "\f769"; +@fa-var-temperature-low: "\f76b"; +@fa-var-tencent-weibo: "\f1d5"; +@fa-var-tenge: "\f7d7"; +@fa-var-terminal: "\f120"; +@fa-var-text-height: "\f034"; +@fa-var-text-width: "\f035"; +@fa-var-th: "\f00a"; +@fa-var-th-large: "\f009"; +@fa-var-th-list: "\f00b"; +@fa-var-the-red-yeti: "\f69d"; +@fa-var-theater-masks: "\f630"; +@fa-var-themeco: "\f5c6"; +@fa-var-themeisle: "\f2b2"; +@fa-var-thermometer: "\f491"; +@fa-var-thermometer-empty: "\f2cb"; +@fa-var-thermometer-full: "\f2c7"; +@fa-var-thermometer-half: "\f2c9"; +@fa-var-thermometer-quarter: "\f2ca"; +@fa-var-thermometer-three-quarters: "\f2c8"; +@fa-var-think-peaks: "\f731"; +@fa-var-thumbs-down: "\f165"; +@fa-var-thumbs-up: "\f164"; +@fa-var-thumbtack: "\f08d"; +@fa-var-ticket-alt: "\f3ff"; +@fa-var-tiktok: "\e07b"; +@fa-var-times: "\f00d"; +@fa-var-times-circle: "\f057"; +@fa-var-tint: "\f043"; +@fa-var-tint-slash: "\f5c7"; +@fa-var-tired: "\f5c8"; +@fa-var-toggle-off: "\f204"; +@fa-var-toggle-on: "\f205"; +@fa-var-toilet: "\f7d8"; +@fa-var-toilet-paper: "\f71e"; +@fa-var-toilet-paper-slash: "\e072"; +@fa-var-toolbox: "\f552"; +@fa-var-tools: "\f7d9"; +@fa-var-tooth: "\f5c9"; +@fa-var-torah: "\f6a0"; +@fa-var-torii-gate: "\f6a1"; +@fa-var-tractor: "\f722"; +@fa-var-trade-federation: "\f513"; +@fa-var-trademark: "\f25c"; +@fa-var-traffic-light: "\f637"; +@fa-var-trailer: "\e041"; +@fa-var-train: "\f238"; +@fa-var-tram: "\f7da"; +@fa-var-transgender: "\f224"; +@fa-var-transgender-alt: "\f225"; +@fa-var-trash: "\f1f8"; +@fa-var-trash-alt: "\f2ed"; +@fa-var-trash-restore: "\f829"; +@fa-var-trash-restore-alt: "\f82a"; +@fa-var-tree: "\f1bb"; +@fa-var-trello: "\f181"; +@fa-var-tripadvisor: "\f262"; +@fa-var-trophy: "\f091"; +@fa-var-truck: "\f0d1"; +@fa-var-truck-loading: "\f4de"; +@fa-var-truck-monster: "\f63b"; +@fa-var-truck-moving: "\f4df"; +@fa-var-truck-pickup: "\f63c"; +@fa-var-tshirt: "\f553"; +@fa-var-tty: "\f1e4"; +@fa-var-tumblr: "\f173"; +@fa-var-tumblr-square: "\f174"; +@fa-var-tv: "\f26c"; +@fa-var-twitch: "\f1e8"; +@fa-var-twitter: "\f099"; +@fa-var-twitter-square: "\f081"; +@fa-var-typo3: "\f42b"; +@fa-var-uber: "\f402"; +@fa-var-ubuntu: "\f7df"; +@fa-var-uikit: "\f403"; +@fa-var-umbraco: "\f8e8"; +@fa-var-umbrella: "\f0e9"; +@fa-var-umbrella-beach: "\f5ca"; +@fa-var-uncharted: "\e084"; +@fa-var-underline: "\f0cd"; +@fa-var-undo: "\f0e2"; +@fa-var-undo-alt: "\f2ea"; +@fa-var-uniregistry: "\f404"; +@fa-var-unity: "\e049"; +@fa-var-universal-access: "\f29a"; +@fa-var-university: "\f19c"; +@fa-var-unlink: "\f127"; +@fa-var-unlock: "\f09c"; +@fa-var-unlock-alt: "\f13e"; +@fa-var-unsplash: "\e07c"; +@fa-var-untappd: "\f405"; +@fa-var-upload: "\f093"; +@fa-var-ups: "\f7e0"; +@fa-var-usb: "\f287"; +@fa-var-user: "\f007"; +@fa-var-user-alt: "\f406"; +@fa-var-user-alt-slash: "\f4fa"; +@fa-var-user-astronaut: "\f4fb"; +@fa-var-user-check: "\f4fc"; +@fa-var-user-circle: "\f2bd"; +@fa-var-user-clock: "\f4fd"; +@fa-var-user-cog: "\f4fe"; +@fa-var-user-edit: "\f4ff"; +@fa-var-user-friends: "\f500"; +@fa-var-user-graduate: "\f501"; +@fa-var-user-injured: "\f728"; +@fa-var-user-lock: "\f502"; +@fa-var-user-md: "\f0f0"; +@fa-var-user-minus: "\f503"; +@fa-var-user-ninja: "\f504"; +@fa-var-user-nurse: "\f82f"; +@fa-var-user-plus: "\f234"; +@fa-var-user-secret: "\f21b"; +@fa-var-user-shield: "\f505"; +@fa-var-user-slash: "\f506"; +@fa-var-user-tag: "\f507"; +@fa-var-user-tie: "\f508"; +@fa-var-user-times: "\f235"; +@fa-var-users: "\f0c0"; +@fa-var-users-cog: "\f509"; +@fa-var-users-slash: "\e073"; +@fa-var-usps: "\f7e1"; +@fa-var-ussunnah: "\f407"; +@fa-var-utensil-spoon: "\f2e5"; +@fa-var-utensils: "\f2e7"; +@fa-var-vaadin: "\f408"; +@fa-var-vector-square: "\f5cb"; +@fa-var-venus: "\f221"; +@fa-var-venus-double: "\f226"; +@fa-var-venus-mars: "\f228"; +@fa-var-vest: "\e085"; +@fa-var-vest-patches: "\e086"; +@fa-var-viacoin: "\f237"; +@fa-var-viadeo: "\f2a9"; +@fa-var-viadeo-square: "\f2aa"; +@fa-var-vial: "\f492"; +@fa-var-vials: "\f493"; +@fa-var-viber: "\f409"; +@fa-var-video: "\f03d"; +@fa-var-video-slash: "\f4e2"; +@fa-var-vihara: "\f6a7"; +@fa-var-vimeo: "\f40a"; +@fa-var-vimeo-square: "\f194"; +@fa-var-vimeo-v: "\f27d"; +@fa-var-vine: "\f1ca"; +@fa-var-virus: "\e074"; +@fa-var-virus-slash: "\e075"; +@fa-var-viruses: "\e076"; +@fa-var-vk: "\f189"; +@fa-var-vnv: "\f40b"; +@fa-var-voicemail: "\f897"; +@fa-var-volleyball-ball: "\f45f"; +@fa-var-volume-down: "\f027"; +@fa-var-volume-mute: "\f6a9"; +@fa-var-volume-off: "\f026"; +@fa-var-volume-up: "\f028"; +@fa-var-vote-yea: "\f772"; +@fa-var-vr-cardboard: "\f729"; +@fa-var-vuejs: "\f41f"; +@fa-var-walking: "\f554"; +@fa-var-wallet: "\f555"; +@fa-var-warehouse: "\f494"; +@fa-var-watchman-monitoring: "\e087"; +@fa-var-water: "\f773"; +@fa-var-wave-square: "\f83e"; +@fa-var-waze: "\f83f"; +@fa-var-weebly: "\f5cc"; +@fa-var-weibo: "\f18a"; +@fa-var-weight: "\f496"; +@fa-var-weight-hanging: "\f5cd"; +@fa-var-weixin: "\f1d7"; +@fa-var-whatsapp: "\f232"; +@fa-var-whatsapp-square: "\f40c"; +@fa-var-wheelchair: "\f193"; +@fa-var-whmcs: "\f40d"; +@fa-var-wifi: "\f1eb"; +@fa-var-wikipedia-w: "\f266"; +@fa-var-wind: "\f72e"; +@fa-var-window-close: "\f410"; +@fa-var-window-maximize: "\f2d0"; +@fa-var-window-minimize: "\f2d1"; +@fa-var-window-restore: "\f2d2"; +@fa-var-windows: "\f17a"; +@fa-var-wine-bottle: "\f72f"; +@fa-var-wine-glass: "\f4e3"; +@fa-var-wine-glass-alt: "\f5ce"; +@fa-var-wix: "\f5cf"; +@fa-var-wizards-of-the-coast: "\f730"; +@fa-var-wodu: "\e088"; +@fa-var-wolf-pack-battalion: "\f514"; +@fa-var-won-sign: "\f159"; +@fa-var-wordpress: "\f19a"; +@fa-var-wordpress-simple: "\f411"; +@fa-var-wpbeginner: "\f297"; +@fa-var-wpexplorer: "\f2de"; +@fa-var-wpforms: "\f298"; +@fa-var-wpressr: "\f3e4"; +@fa-var-wrench: "\f0ad"; +@fa-var-x-ray: "\f497"; +@fa-var-xbox: "\f412"; +@fa-var-xing: "\f168"; +@fa-var-xing-square: "\f169"; +@fa-var-y-combinator: "\f23b"; +@fa-var-yahoo: "\f19e"; +@fa-var-yammer: "\f840"; +@fa-var-yandex: "\f413"; +@fa-var-yandex-international: "\f414"; +@fa-var-yarn: "\f7e3"; +@fa-var-yelp: "\f1e9"; +@fa-var-yen-sign: "\f157"; +@fa-var-yin-yang: "\f6ad"; +@fa-var-yoast: "\f2b1"; +@fa-var-youtube: "\f167"; +@fa-var-youtube-square: "\f431"; +@fa-var-zhihu: "\f63f"; diff --git a/public/vendor/fontawesome/less/brands.less b/public/vendor/fontawesome/less/brands.less new file mode 100644 index 0000000000..030b7ae909 --- /dev/null +++ b/public/vendor/fontawesome/less/brands.less @@ -0,0 +1,23 @@ +/*! + * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import "_variables.less"; + +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-style: normal; + font-weight: 400; + font-display: @fa-font-display; + src: url('@{fa-font-path}/fa-brands-400.eot'); + src: url('@{fa-font-path}/fa-brands-400.eot?#iefix') format('embedded-opentype'), + url('@{fa-font-path}/fa-brands-400.woff2') format('woff2'), + url('@{fa-font-path}/fa-brands-400.woff') format('woff'), + url('@{fa-font-path}/fa-brands-400.ttf') format('truetype'), + url('@{fa-font-path}/fa-brands-400.svg#fontawesome') format('svg'); +} + +.fab { + font-family: 'Font Awesome 5 Brands'; + font-weight: 400; +} diff --git a/public/vendor/fontawesome/less/fontawesome.less b/public/vendor/fontawesome/less/fontawesome.less new file mode 100644 index 0000000000..826afc5e5e --- /dev/null +++ b/public/vendor/fontawesome/less/fontawesome.less @@ -0,0 +1,16 @@ +/*! + * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import "_variables.less"; +@import "_mixins.less"; +@import "_core.less"; +@import "_larger.less"; +@import "_fixed-width.less"; +@import "_list.less"; +@import "_bordered-pulled.less"; +@import "_animated.less"; +@import "_rotated-flipped.less"; +@import "_stacked.less"; +@import "_icons.less"; +@import "_screen-reader.less"; diff --git a/public/vendor/fontawesome/less/nodebb-shims.less b/public/vendor/fontawesome/less/nodebb-shims.less new file mode 100644 index 0000000000..a2621fb4ef --- /dev/null +++ b/public/vendor/fontawesome/less/nodebb-shims.less @@ -0,0 +1,321 @@ +@import "_variables.less"; + + +@font-face { + font-family: 'FontAwesome'; + font-style: normal; + font-weight: 400; + font-display: @fa-font-display; + src: url('@{fa-font-path}/fa-solid-900.eot'); + src: url('@{fa-font-path}/fa-solid-900.eot?#iefix') format('embedded-opentype'), + url('@{fa-font-path}/fa-solid-900.woff2') format('woff2'), + url('@{fa-font-path}/fa-solid-900.woff') format('woff'), + url('@{fa-font-path}/fa-solid-900.ttf') format('truetype'), + url('@{fa-font-path}/fa-solid-900.svg#fontawesome') format('svg'); +} +@font-face { + font-family: 'FontAwesome'; + font-style: normal; + font-weight: 400; + font-display: @fa-font-display; + src: url('@{fa-font-path}/fa-brands-400.eot'); + src: url('@{fa-font-path}/fa-brands-400.eot?#iefix') format('embedded-opentype'), + url('@{fa-font-path}/fa-brands-400.woff2') format('woff2'), + url('@{fa-font-path}/fa-brands-400.woff') format('woff'), + url('@{fa-font-path}/fa-brands-400.ttf') format('truetype'), + url('@{fa-font-path}/fa-brands-400.svg#fontawesome') format('svg'); +} +@font-face { + font-family: 'FontAwesome'; + font-style: normal; + font-weight: 400; + font-display: @fa-font-display; + src: url('@{fa-font-path}/fa-regular-400.eot'); + src: url('@{fa-font-path}/fa-regular-400.eot?#iefix') format('embedded-opentype'), + url('@{fa-font-path}/fa-regular-400.woff2') format('woff2'), + url('@{fa-font-path}/fa-regular-400.woff') format('woff'), + url('@{fa-font-path}/fa-regular-400.ttf') format('truetype'), + url('@{fa-font-path}/fa-regular-400.svg#fontawesome') format('svg'); +} + + +@fa-var-address-book-o: @fa-var-address-book; +@fa-var-address-card-o: @fa-var-address-card; +@fa-var-area-chart: @fa-var-chart-area; +@fa-var-arrow-circle-o-down: @fa-var-arrow-alt-circle-down; +@fa-var-arrow-circle-o-left: @fa-var-arrow-alt-circle-left; +@fa-var-arrow-circle-o-right: @fa-var-arrow-alt-circle-right; +@fa-var-arrow-circle-o-up: @fa-var-arrow-alt-circle-up; +@fa-var-arrows: @fa-var-arrows-alt; +@fa-var-arrows-alt: @fa-var-expand-arrows-alt; +@fa-var-arrows-h: @fa-var-arrows-alt-h; +@fa-var-arrows-v: @fa-var-arrows-alt-v; +@fa-var-asl-interpreting: @fa-var-american-sign-language-interpreting; +@fa-var-automobile: @fa-var-car; +@fa-var-bank: @fa-var-university; +@fa-var-bar-chart: @fa-var-chart-bar; +@fa-var-bar-chart-o: @fa-var-chart-bar; +@fa-var-bathtub: @fa-var-bath; +@fa-var-battery: @fa-var-battery-full; +@fa-var-battery-0: @fa-var-battery-empty; +@fa-var-battery-1: @fa-var-battery-quarter; +@fa-var-battery-2: @fa-var-battery-half; +@fa-var-battery-3: @fa-var-battery-three-quarters; +@fa-var-battery-4: @fa-var-battery-full; +@fa-var-bell-o: @fa-var-bell; +@fa-var-bell-slash-o: @fa-var-bell-slash; +@fa-var-bitbucket-square: @fa-var-bitbucket; +@fa-var-bitcoin: @fa-var-btc; +@fa-var-bookmark-o: @fa-var-bookmark; +@fa-var-building-o: @fa-var-building; +@fa-var-cab: @fa-var-taxi; +@fa-var-calendar: @fa-var-calendar-alt; +@fa-var-calendar-check-o: @fa-var-calendar-check; +@fa-var-calendar-minus-o: @fa-var-calendar-minus; +@fa-var-calendar-o: @fa-var-calendar; +@fa-var-calendar-plus-o: @fa-var-calendar-plus; +@fa-var-calendar-times-o: @fa-var-calendar-times; +@fa-var-caret-square-o-down: @fa-var-caret-square-down; +@fa-var-caret-square-o-left: @fa-var-caret-square-left; +@fa-var-caret-square-o-right: @fa-var-caret-square-right; +@fa-var-caret-square-o-up: @fa-var-caret-square-up; +@fa-var-cc: @fa-var-closed-captioning; +@fa-var-chain: @fa-var-link; +@fa-var-chain-broken: @fa-var-unlink; +@fa-var-check-circle-o: @fa-var-check-circle; +@fa-var-check-square-o: @fa-var-check-square; +@fa-var-circle-o: @fa-var-circle; +@fa-var-circle-o-notch: @fa-var-circle-notch; +@fa-var-circle-thin: @fa-var-circle; +@fa-var-clock-o: @fa-var-clock; +@fa-var-close: @fa-var-times; +@fa-var-cloud-download: @fa-var-cloud-download-alt; +@fa-var-cloud-upload: @fa-var-cloud-upload-alt; +@fa-var-cny: @fa-var-yen-sign; +@fa-var-code-fork: @fa-var-code-branch; +@fa-var-comment-o: @fa-var-comment; +@fa-var-commenting: @fa-var-comment-dots; +@fa-var-commenting-o: @fa-var-comment-dots; +@fa-var-comments-o: @fa-var-comments; +@fa-var-credit-card-alt: @fa-var-credit-card; +@fa-var-cutlery: @fa-var-utensils; +@fa-var-dashboard: @fa-var-tachometer-alt; +@fa-var-deafness: @fa-var-deaf; +@fa-var-dedent: @fa-var-outdent; +@fa-var-diamond: @fa-var-gem; +@fa-var-dollar: @fa-var-dollar-sign; +@fa-var-dot-circle-o: @fa-var-dot-circle; +@fa-var-drivers-license: @fa-var-id-card; +@fa-var-drivers-license-o: @fa-var-id-card; +@fa-var-eercast: @fa-var-sellcast; +@fa-var-envelope-o: @fa-var-envelope; +@fa-var-envelope-open-o: @fa-var-envelope-open; +@fa-var-eur: @fa-var-euro-sign; +@fa-var-euro: @fa-var-euro-sign; +@fa-var-exchange: @fa-var-exchange-alt; +@fa-var-external-link: @fa-var-external-link-alt; +@fa-var-external-link-square: @fa-var-external-link-square-alt; +@fa-var-eyedropper: @fa-var-eye-dropper; +@fa-var-fa: @fa-var-font-awesome; +@fa-var-facebook: @fa-var-facebook-f; +@fa-var-facebook-official: @fa-var-facebook; +@fa-var-feed: @fa-var-rss; +@fa-var-file-archive-o: @fa-var-file-archive; +@fa-var-file-audio-o: @fa-var-file-audio; +@fa-var-file-code-o: @fa-var-file-code; +@fa-var-file-excel-o: @fa-var-file-excel; +@fa-var-file-image-o: @fa-var-file-image; +@fa-var-file-movie-o: @fa-var-file-video; +@fa-var-file-o: @fa-var-file; +@fa-var-file-pdf-o: @fa-var-file-pdf; +@fa-var-file-photo-o: @fa-var-file-image; +@fa-var-file-picture-o: @fa-var-file-image; +@fa-var-file-powerpoint-o: @fa-var-file-powerpoint; +@fa-var-file-sound-o: @fa-var-file-audio; +@fa-var-file-text: @fa-var-file-alt; +@fa-var-file-text-o: @fa-var-file-alt; +@fa-var-file-video-o: @fa-var-file-video; +@fa-var-file-word-o: @fa-var-file-word; +@fa-var-file-zip-o: @fa-var-file-archive; +@fa-var-files-o: @fa-var-copy; +@fa-var-flag-o: @fa-var-flag; +@fa-var-flash: @fa-var-bolt; +@fa-var-floppy-o: @fa-var-save; +@fa-var-folder-o: @fa-var-folder; +@fa-var-folder-open-o: @fa-var-folder-open; +@fa-var-frown-o: @fa-var-frown; +@fa-var-futbol-o: @fa-var-futbol; +@fa-var-gbp: @fa-var-pound-sign; +@fa-var-ge: @fa-var-empire; +@fa-var-gear: @fa-var-cog; +@fa-var-gears: @fa-var-cogs; +@fa-var-gittip: @fa-var-gratipay; +@fa-var-glass: @fa-var-glass-martini; +@fa-var-google-plus: @fa-var-google-plus-g; +@fa-var-google-plus-circle: @fa-var-google-plus; +@fa-var-google-plus-official: @fa-var-google-plus; +@fa-var-group: @fa-var-users; +@fa-var-hand-grab-o: @fa-var-hand-rock; +@fa-var-hand-lizard-o: @fa-var-hand-lizard; +@fa-var-hand-o-down: @fa-var-hand-point-down; +@fa-var-hand-o-left: @fa-var-hand-point-left; +@fa-var-hand-o-right: @fa-var-hand-point-right; +@fa-var-hand-o-up: @fa-var-hand-point-up; +@fa-var-hand-paper-o: @fa-var-hand-paper; +@fa-var-hand-peace-o: @fa-var-hand-peace; +@fa-var-hand-pointer-o: @fa-var-hand-pointer; +@fa-var-hand-rock-o: @fa-var-hand-rock; +@fa-var-hand-scissors-o: @fa-var-hand-scissors; +@fa-var-hand-spock-o: @fa-var-hand-spock; +@fa-var-hand-stop-o: @fa-var-hand-paper; +@fa-var-handshake-o: @fa-var-handshake; +@fa-var-hard-of-hearing: @fa-var-deaf; +@fa-var-hdd-o: @fa-var-hdd; +@fa-var-header: @fa-var-heading; +@fa-var-heart-o: @fa-var-heart; +@fa-var-hospital-o: @fa-var-hospital; +@fa-var-hotel: @fa-var-bed; +@fa-var-hourglass-1: @fa-var-hourglass-start; +@fa-var-hourglass-2: @fa-var-hourglass-half; +@fa-var-hourglass-3: @fa-var-hourglass-end; +@fa-var-hourglass-o: @fa-var-hourglass; +@fa-var-id-card-o: @fa-var-id-card; +@fa-var-ils: @fa-var-shekel-sign; +@fa-var-inr: @fa-var-rupee-sign; +@fa-var-institution: @fa-var-university; +@fa-var-intersex: @fa-var-transgender; +@fa-var-jpy: @fa-var-yen-sign; +@fa-var-keyboard-o: @fa-var-keyboard; +@fa-var-krw: @fa-var-won-sign; +@fa-var-legal: @fa-var-gavel; +@fa-var-lemon-o: @fa-var-lemon; +@fa-var-level-down: @fa-var-level-down-alt; +@fa-var-level-up: @fa-var-level-up-alt; +@fa-var-life-bouy: @fa-var-life-ring; +@fa-var-life-buoy: @fa-var-life-ring; +@fa-var-life-saver: @fa-var-life-ring; +@fa-var-lightbulb-o: @fa-var-lightbulb; +@fa-var-line-chart: @fa-var-chart-line; +@fa-var-linkedin: @fa-var-linkedin-in; +@fa-var-linkedin-square: @fa-var-linkedin; +@fa-var-long-arrow-down: @fa-var-long-arrow-alt-down; +@fa-var-long-arrow-left: @fa-var-long-arrow-alt-left; +@fa-var-long-arrow-right: @fa-var-long-arrow-alt-right; +@fa-var-long-arrow-up: @fa-var-long-arrow-alt-up; +@fa-var-mail-forward: @fa-var-share; +@fa-var-mail-reply: @fa-var-reply; +@fa-var-mail-reply-all: @fa-var-reply-all; +@fa-var-map-marker: @fa-var-map-marker-alt; +@fa-var-map-o: @fa-var-map; +@fa-var-meanpath: @fa-var-font-awesome; +@fa-var-meh-o: @fa-var-meh; +@fa-var-minus-square-o: @fa-var-minus-square; +@fa-var-mobile: @fa-var-mobile-alt; +@fa-var-mobile-phone: @fa-var-mobile-alt; +@fa-var-money: @fa-var-money-bill-alt; +@fa-var-moon-o: @fa-var-moon; +@fa-var-mortar-board: @fa-var-graduation-cap; +@fa-var-navicon: @fa-var-bars; +@fa-var-newspaper-o: @fa-var-newspaper; +@fa-var-paper-plane-o: @fa-var-paper-plane; +@fa-var-paste: @fa-var-clipboard; +@fa-var-pause-circle-o: @fa-var-pause-circle; +@fa-var-pencil: @fa-var-pencil-alt; +@fa-var-pencil-square: @fa-var-pen-square; +@fa-var-pencil-square-o: @fa-var-edit; +@fa-var-photo: @fa-var-image; +@fa-var-picture-o: @fa-var-image; +@fa-var-pie-chart: @fa-var-chart-pie; +@fa-var-play-circle-o: @fa-var-play-circle; +@fa-var-plus-square-o: @fa-var-plus-square; +@fa-var-question-circle-o: @fa-var-question-circle; +@fa-var-ra: @fa-var-rebel; +@fa-var-refresh: @fa-var-sync; +@fa-var-remove: @fa-var-times; +@fa-var-reorder: @fa-var-bars; +@fa-var-repeat: @fa-var-redo; +@fa-var-resistance: @fa-var-rebel; +@fa-var-rmb: @fa-var-yen-sign; +@fa-var-rotate-left: @fa-var-undo; +@fa-var-rotate-right: @fa-var-redo; +@fa-var-rouble: @fa-var-ruble-sign; +@fa-var-rub: @fa-var-ruble-sign; +@fa-var-ruble: @fa-var-ruble-sign; +@fa-var-rupee: @fa-var-rupee-sign; +@fa-var-s15: @fa-var-bath; +@fa-var-scissors: @fa-var-cut; +@fa-var-send: @fa-var-paper-plane; +@fa-var-send-o: @fa-var-paper-plane; +@fa-var-share-square-o: @fa-var-share-square; +@fa-var-shekel: @fa-var-shekel-sign; +@fa-var-sheqel: @fa-var-shekel-sign; +@fa-var-shield: @fa-var-shield-alt; +@fa-var-sign-in: @fa-var-sign-in-alt; +@fa-var-sign-out: @fa-var-sign-out-alt; +@fa-var-signing: @fa-var-sign-language; +@fa-var-sliders: @fa-var-sliders-h; +@fa-var-smile-o: @fa-var-smile; +@fa-var-snowflake-o: @fa-var-snowflake; +@fa-var-soccer-ball-o: @fa-var-futbol; +@fa-var-sort-alpha-asc: @fa-var-sort-alpha-down; +@fa-var-sort-alpha-desc: @fa-var-sort-alpha-up; +@fa-var-sort-amount-asc: @fa-var-sort-amount-down; +@fa-var-sort-amount-desc: @fa-var-sort-amount-up; +@fa-var-sort-asc: @fa-var-sort-up; +@fa-var-sort-desc: @fa-var-sort-down; +@fa-var-sort-numeric-asc: @fa-var-sort-numeric-down; +@fa-var-sort-numeric-desc: @fa-var-sort-numeric-up; +@fa-var-spoon: @fa-var-utensil-spoon; +@fa-var-square-o: @fa-var-square; +@fa-var-star-half-empty: @fa-var-star-half; +@fa-var-star-half-full: @fa-var-star-half; +@fa-var-star-half-o: @fa-var-star-half; +@fa-var-star-o: @fa-var-star; +@fa-var-sticky-note-o: @fa-var-sticky-note; +@fa-var-stop-circle-o: @fa-var-stop-circle; +@fa-var-sun-o: @fa-var-sun; +@fa-var-support: @fa-var-life-ring; +@fa-var-tablet: @fa-var-tablet-alt; +@fa-var-tachometer: @fa-var-tachometer-alt; +@fa-var-television: @fa-var-tv; +@fa-var-thermometer: @fa-var-thermometer-full; +@fa-var-thermometer-0: @fa-var-thermometer-empty; +@fa-var-thermometer-1: @fa-var-thermometer-quarter; +@fa-var-thermometer-2: @fa-var-thermometer-half; +@fa-var-thermometer-3: @fa-var-thermometer-three-quarters; +@fa-var-thermometer-4: @fa-var-thermometer-full; +@fa-var-thumb-tack: @fa-var-thumbtack; +@fa-var-thumbs-o-down: @fa-var-thumbs-down; +@fa-var-thumbs-o-up: @fa-var-thumbs-up; +@fa-var-ticket: @fa-var-ticket-alt; +@fa-var-times-circle-o: @fa-var-times-circle; +@fa-var-times-rectangle: @fa-var-window-close; +@fa-var-times-rectangle-o: @fa-var-window-close; +@fa-var-toggle-down: @fa-var-caret-square-down; +@fa-var-toggle-left: @fa-var-caret-square-left; +@fa-var-toggle-right: @fa-var-caret-square-right; +@fa-var-toggle-up: @fa-var-caret-square-up; +@fa-var-trash: @fa-var-trash-alt; +@fa-var-trash-o: @fa-var-trash-alt; +@fa-var-try: @fa-var-lira-sign; +@fa-var-turkish-lira: @fa-var-lira-sign; +@fa-var-unsorted: @fa-var-sort; +@fa-var-usd: @fa-var-dollar-sign; +@fa-var-user-circle-o: @fa-var-user-circle; +@fa-var-user-o: @fa-var-user; +@fa-var-vcard: @fa-var-address-card; +@fa-var-vcard-o: @fa-var-address-card; +@fa-var-video-camera: @fa-var-video; +@fa-var-vimeo: @fa-var-vimeo-v; +@fa-var-volume-control-phone: @fa-var-phone-volume; +@fa-var-warning: @fa-var-exclamation-triangle; +@fa-var-wechat: @fa-var-weixin; +@fa-var-wheelchair-alt: @fa-var-accessible-icon; +@fa-var-window-close-o: @fa-var-window-close; +@fa-var-won: @fa-var-won-sign; +@fa-var-y-combinator-square: @fa-var-hacker-news; +@fa-var-yc: @fa-var-y-combinator; +@fa-var-yc-square: @fa-var-hacker-news; +@fa-var-yen: @fa-var-yen-sign; +@fa-var-youtube-play: @fa-var-youtube; + diff --git a/public/vendor/fontawesome/less/regular.less b/public/vendor/fontawesome/less/regular.less new file mode 100644 index 0000000000..8057ddd92f --- /dev/null +++ b/public/vendor/fontawesome/less/regular.less @@ -0,0 +1,23 @@ +/*! + * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import "_variables.less"; + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 400; + font-display: @fa-font-display; + src: url('@{fa-font-path}/fa-regular-400.eot'); + src: url('@{fa-font-path}/fa-regular-400.eot?#iefix') format('embedded-opentype'), + url('@{fa-font-path}/fa-regular-400.woff2') format('woff2'), + url('@{fa-font-path}/fa-regular-400.woff') format('woff'), + url('@{fa-font-path}/fa-regular-400.ttf') format('truetype'), + url('@{fa-font-path}/fa-regular-400.svg#fontawesome') format('svg'); +} + +.far { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} diff --git a/public/vendor/fontawesome/less/solid.less b/public/vendor/fontawesome/less/solid.less new file mode 100644 index 0000000000..ea03f05c07 --- /dev/null +++ b/public/vendor/fontawesome/less/solid.less @@ -0,0 +1,24 @@ +/*! + * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import "_variables.less"; + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 900; + font-display: @fa-font-display; + src: url('@{fa-font-path}/fa-solid-900.eot'); + src: url('@{fa-font-path}/fa-solid-900.eot?#iefix') format('embedded-opentype'), + url('@{fa-font-path}/fa-solid-900.woff2') format('woff2'), + url('@{fa-font-path}/fa-solid-900.woff') format('woff'), + url('@{fa-font-path}/fa-solid-900.ttf') format('truetype'), + url('@{fa-font-path}/fa-solid-900.svg#fontawesome') format('svg'); +} + +.fa, +.fas { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; +} diff --git a/public/vendor/fontawesome/less/v4-shims.less b/public/vendor/fontawesome/less/v4-shims.less new file mode 100644 index 0000000000..e7e0e7e9ce --- /dev/null +++ b/public/vendor/fontawesome/less/v4-shims.less @@ -0,0 +1,6 @@ +/*! + * Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com + * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import '_variables.less'; +@import '_shims.less'; diff --git a/public/vendor/fontawesome/webfonts/fa-brands-400.eot b/public/vendor/fontawesome/webfonts/fa-brands-400.eot new file mode 100644 index 0000000000000000000000000000000000000000..d05ea581fbbf17eb0d3139f9937ac6a8fde98685 GIT binary patch literal 134346 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 + + + +Created by FontForge 20201107 at Tue Mar 16 10:15:04 2021 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vendor/fontawesome/webfonts/fa-brands-400.ttf b/public/vendor/fontawesome/webfonts/fa-brands-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fc567cd2f11d83683d9eb4ca1a5fdc912f7d417c GIT binary patch literal 134040 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

    >*Mp)}2DJkS!E5#pYJXmI{5?Wiwex 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 PAApscxF+9%?|HK{Ep3zbfwb*orEJ71 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

    |#S?oSB>4 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 literal 0 HcmV?d00001 diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.eot b/public/vendor/fontawesome/webfonts/fa-regular-400.eot new file mode 100644 index 0000000000000000000000000000000000000000..fae180dacc52937c1d6a24636431663d6754fef5 GIT binary patch literal 34034 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 + + + +Created by FontForge 20201107 at Tue Mar 16 10:15:04 2021 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.ttf b/public/vendor/fontawesome/webfonts/fa-regular-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d1ac9ba1169e4076832034c5585e1c5bf9d6f83c GIT binary patch literal 33736 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 {0*j9;c)hv7CKtT<=$) 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 literal 0 HcmV?d00001 diff --git a/public/vendor/fontawesome/webfonts/fa-regular-400.woff2 b/public/vendor/fontawesome/webfonts/fa-regular-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9df490e8cfdd75704d31f518caf76ab34494b124 GIT binary patch literal 13276 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 aY@s`2`P2|tR<;SDtTi=oII|b5WimRFpv7+h literal 0 HcmV?d00001 diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.eot b/public/vendor/fontawesome/webfonts/fa-solid-900.eot new file mode 100644 index 0000000000000000000000000000000000000000..afe315244f6dae3beda0159693d25a6e0466dd90 GIT binary patch literal 203030 zcmeEvdth8uwfEj<&Y5#&<~(LHuRNy7MzVw_2g{z*)6i2zczh|r2oJ3jv8PO$EY>n}-+mSQf}xpe8OQy%!?`Oh#{^e|Fm zCoW#HludIDApRucrKg_W(A0d#nO&%~8u6}+u2|FEe_5u5vA}x9^q1DHxwd<3447Qe zeaP3=ZMgoDRX={<3C3m|VbXUttiO28+LB*By%KqZzimAdo2)%q(oZaVN^ zE)U}S8MF3oxay)citDn)Xu1ROeOIj6)Ge>%=OG_)7Qb@M6&F{3duAzAq z4H^wp!e4qU4PHr&9l`T6PP)jXhwBKXxQ_6%SzII%VlGN0MVv92|J}8PDM((yFG1Rg z&U=UzF;F+og--e0tMig2YgwvS866We)boRE6*2{7d^Z0w!g0p!o*?`+!bOxiKw*NU z6Hl^uE-#fWGxouRGL)XQ^Eko;$hSlphIpD87>U16z=L$8y+>)t7a%c|p>zRb87avu z${SA`5A&|^F!EL4Da6~4I#gbhl4I}V&78O@!iYQgG>X%Z2wIc5bi1vT9e4U6(gcku zR=`nK0%U2@Scjf9ODLuceLsK>_iFq4EJ0%1tN3u6m^kr<#38T;*YTp zva})J9(TMcjJyoBA=*hH58%+3>nHEC^X+oZTv8potaxLbhO%u;emnI9pN{vjGG@m`?- zCobWgt3zQn0W=zqEZDS&d`*PKJW$*or!3#%UC7cvl*YQq$Cm`IE|hm@#yE;ON15FC zi*UBT_FQPdLwz9H^7)QQNryJ%<@!u%1aa^Iy;CeG=(59N&e3j$z~%=#Y{x|#G)4;1 zkM;;?Er@WI28s)uDZJAn&o3b?fbx(g@||(9>kHnrc|0}tezpvi`$vGfG&Wf|LSaAZ z#tEmufwm|Npgfz-6y8Yy-c%+#-cFhEa+Jpa7%TBkB218S(wS4A(g;#He9pXK{l<|- z>DhPhcwH%(mxpv7w?VX(vhihc+H{Vm2^vtw;aQu`B;siYKjlpV!ke4Nq+QOL>kphh z3BJvaTlOvL#i=~@1*aa;Xq*JUQJP&=jFU#~iaM#WAqOvwG4>F9Ua?;PmQ5G*Qhi!S zRFA^!SRi}?XWD*e<93?8Kk*)sXVYZUmcPDi9!XLeT30rpEhF2{TsY;lg*@OF{mgq$ z7-P{7;N&E~0TP8#pLbCj@E|Psi{4BC@h6j#4lp|%^C09@R+iA4$_pIyP6JYTafAuC zDBok3&5o(vjwP?c_UY$Iyqmq8{=kC!C8@8Ugl|Z2P9;3L$+nhi8%m1DIZc)-r0xfjWNa8WaXeeCNws7Ia~gdyo^J}(mS2aoA}O89}kb~ zIXQX>F9Ree*3ac5Ps~kfJfHA=7-Z{-Hc?kf0-QHT|7=>&zm%OP<~W_j?Ti)WIbh>~ z9)dF2Imo_q+1R-R%5`vP|%DCYzU|odHMM!HKZo?-b!iU+~VBx91YPn&so9C_9uJ*Fwli zr@TlLI0PP12kT*2;9#QO`-1loMmlu6vA^WU#>KgNpE1b*~Y^d(7WDrc`}c6(wTQg;1ZUCgHlQ~f07 zk@8bx10u~1zbD%5aqy?9JnfwVMBUujF_g(dG8;yD3g?d-;tq|1<}59uZVF{7pX4Q@ zFzVx-9XE`Tqsuw?DWCd4Wm5zq-yUzO$4GyowN2yVlq30`B%EU-0(W+8rfgjcW&f5P zE0jV1Q4i&9IthmZDWcN`(Y|N{<&uQkev5R(?R3 zw0hRllqiCvoF{e?knnx^_BL`?_1orvhSR}^ZTys+uWDxyS?wuzPtPG z?fXpMj=o3wzR>qr-?#f->ic=$8-2Zfzwi5V-(X*+-`8*U2l`|ErTrEC_5CyZTl;7C z&+lK{zqJ44{+0b_^`GB=asO@oTl??t|4jcw{h#fBxc{;Kz5U;XvuYjDfa+Sp&-lRt}suuwmfi12+uZHE{R9eFL8v_>X}{ z26heX9(a7s_z)J%!4;&u&!@xTOe;WA9z`(%pz~2TkN0p=c(bA)}M>~$5 zbM(BUT}LlGdh5|IAN|46XO8~x=#P)Sc=YE-e{;0==pT;$<>-4yKR7xz=pOVBRt#1R zwhXon&K{gQxL|PM;0c3E22UJ3d2r=m=iupss|U{;ykPLc!Al0O7`$e1{$cJRR9OM|}{e0}h@gTEj04Bb2Q@X!~B9v%AD z(7vJlLr)F;aOkH)FAcps^vj`N4;>!*?a=Rr-Wqym=+8s{J@mn_JnSD14i^oV4VMo$ z4z~}_9bP!Rc=*KOlZHMt58phzW%$nF z2Zuj5{Dt91hrc@fjp6?s{{HaO!|CAz!#^AT`S34>|7-Y{!@nASWBB*OZx6pa{FmW@ z;rE8$A7LZk()+t z8QC(jb!6Mf10xTP>>TMC`Qpe|M!r7s*vR7}-y3;qBt7!%$PY(;GV;>Mp^=wIem(N` z$dQr1jtqf-z zKlpTKUr*nc`u6sv`wsQJ3O;?a@14HBzR`ZxZ}j{73&E%H{;K}Q{^tJn{(1dL@aakY zr}TIBpVQyfe{26I`|ox5bZ7t9!KdHu-`}6^e^K!1;r`zKKY>q22iSl-;2nq#6c3aS zG!JwPBnM6zI1PMy^}zLlPqzy`eR!Z};L(BaI(+)f!1Du#242tcY5%~G;8W?SNAPL; z(bW#0-U2>-`k(S?Gx&7Yhw$mS;L|nW)AfVh;M0!}-ZXg2;1=-d-Gkc)A9VP1|KL-D zY4GWb;L}$J-xz#z@V%k%(EUT7AKEqawW03}JwEikp&txAKlI|zA%{=j$m7%ZC-7-K z_;k($K0O_LdLH<6?eL{JKHW5YBlz@|;XA;mJBE84K7HKb)8{7e>2HSL6nuJQxR3aB zcx=Q6J`Fj1T02rdGG_vxo;`BD;L~eHHjku6ZX3BBe0sm&)6auXca3~?24zf7l=N7x)8y)35uze$_AgU4Cw5tPiaBt-o9UZT-di zqxG)!j`fz+YyHMLY`t#%+Iq$Mh4r%abL)`xGwUVmp!HMhC)V@UkE|bB2dw9;N3E|| zk5~^|JFU-IpS8AGcUhmZZnti=ZnmzoK4xvSuC=*X6!M(ZhY1Fit%M* zm+=Lo$N0SQh_S=?l<`U97UO2)6UI%($Be6utBfm*%Z)RPlZ@rYGUG&JiLux?!B}K0 zG?K;wW4L1Zp>L=(6^#%G| zym_{DU_r2%)oA0Qv-}hJFfBXL8JL3DJ z?_J+JzPEjE`hMkm#rLxBknbhmi@u-wp7lNDd(!uP-xI$5zTLiEz6X7G`99^l!}m$w zb-pWn7y8!t&i9??Tje{|SL2)F3wXs{0%-*Q|KI-~f&ZsRz%6-+J}9k1gUzFrLs7Q> zW#4(36M~a#PRRWKk^eVF!1d?<1s!|A7CT__Z-klM57>$0%v#1guQ29&0QbY)Fq3X( z%-o2pMcfzrHQXEHX0sp!z%6SSO-5pXR~U=#!u85x#)`HvR*bvd5~Rm!0k1JO1!tvF z;3@;2_;$w15wAqMRb7l#rvL{TOH2nm065H8O$PwwYI_-*3VhSvXRHn|eHq|M+)bn2 zjJFu8N8JXr)rh){@8bR#?av&=%{pjlW&q%8LA$MU0cfvH2LNx|LB`sXjCC{u_A)kW zGvE+ovsVH77@HFVpq;s>H#g1LJm8(Thq3vfe*tJ)fVPsrw-EF!M0<+>C%nMeVx%ob zecT4KB`Ck7m$9WNvvd%b$sGXDb0X?41O8>30s9zRt^w8qK*vd-@1)fL(0{TFK)WZS z{S}1(;5!BNPI;fPm7M_8Jr(JvZUP);tP}m|1n$$e13<$^Y5}h>c6tlo2xF^2)2bI3 zI|Fsk1fDauGWJolu{s1m8)q@VZonX8X9L$c$UA2n0Cmo-W$Zk}&wGur^HIO+5Myib zUW5D#H2}(71UlBn0eD}GvKMy)(C#IOuLJIN4=}cVHDj0RfHY&5ZDj0n;Jth&V;lT{ zEr3zRt~kKhm5Uj>steG=Sa%@+<*!Ct*8tZw2N}Dz1JKLZMwIy&18fHDW9+)QjC~yR zYyz(9S2K14@@`nq*o`t^FJqfgZ}SE~58xHXZbJG^D0kByz+uKdp#xCv=IMaf7)#-O z3(DMr{@n^Zw+=FP+q;Zy+0WP~s{p`rdyKI=Y8l&#x?6$s&Q1WzeJTaO`>suZ_Zi!U z_}##D_X@xRfL_M#LA`r+F!pKm|I=Lnw7q>X0JPk@6|je~`%vb-U5wqY0d_O?8N?qX z*vZ&K9e@Lj0R`KE$7eS)_8-dtuQB#H;Q8Ek#&&LC>|ul-?qTc^wDEbA{d}6So({mf zjC~;m=wlp!^<;!8bNC_84e+>@CK=xs|cKpy^wkjD5RLOV~P{trTorCS(#rV7x* z*t3fPdl-AJi?IWM9|C^V2mrq4V~o9kc3%LlA8%poC+NdZ_A&O;6k`Wh0gf>C;wWP; zA@8L=#(uU8fV4xq8T+}$*vn|^W#s)L&e$uUI`@Cv|_jE#VvksW~d85>2~-xvUO-a|X@p^d*M8GC;*;1FXU zbOS~i%iulc2keBVLH4q(jPq51QFy*=V%#+su%B^xI-r+vWh3M6#ehD>RlL<*jBCpP zsIR@nxMwv0Y2G;Cea3w+Fs>ubXaOK@rWv;Y{=(Hi4qWBi7_Zn4=w-aJ5byxwRiGttknx%r0Por) z<5M-jUdE?&08p=PFi zNd_R?w1@F#;A%#l7L;#!f$>)Ksr3NkZH0ghfY%sr?_j(G_&YW-J`3e%p^w)S zN4u*YVEhcwdd4e^pVv-0A2eNna$PaN zF2>h@t~GlYzwlfD+Fc7gYtxKh4Ez`4eF@UnZDf4CACO}FQsBR=6VS`}<*2t|Edced zoXhxCRe*hrccXmw0miTH0KCBXHI0CSj9-l8KX?z)v48L{%mo}~{EM3y|I#wRTa52I7l8Mp!1XBdcCTjq zt0?zX(DSv;jDKAQpzb$T0gf>K7{cG&!T4UJe@kQh+i36GTN(e34%ou@KD4#3m+|ke zXZ-OEjQ=Na?ML2z;C=%5o0~iKLB`tw2krS4>JD3DC0jq#Q0BE0QwmJ=}yKEf`%6t1Kwi%XByxL<3EoB z@O~M1fAJdQuWVrazkvVMa{=!&{>y!gzZPTsS7`g!FEIW(+B`g$@!z1mH);VrjQ@4_kafIX8dR&Z!tcyh4E3;89l`K-#Qq74{iM&bicoh@efev1GJOb!1&k-oFn0f37g zXbY3Vs26#SNznsLDn$9BB$JAPrv&-2768(x>}OIb%9QP7QhXzm$`P*Iz@#dKs}W9g zGpS}Vlct6Ms9V>`q#1JoyO~r!9RR!ypr_FfK>kd`n^3QL6O&p{r{y4%S`lum1q?E& z18E)mm^2IY%`OC>?wk!wn!6c*`t$ZOY5p?6D3cZ(VN$Y}NefpqX%Pdw!lV;cF=+|f zUy8I7QD+(2UABWsCjsZlNMBI}c#TP?Aiff1PX(@15${C%r)hx0O!^4Qovs5w!>YA_ zolH721VDSMk+*t~NoQ?h(%BmU?=tBe&~RQ1fcy&znbd{y7oyxn+nKbslSvn^X3{0o znY0dh>$fuL(iKd)3~gO@fJv8cVA6)Sm~_Q%CSAFnNmuP>Qa9SY8u+f>$E0g^FzH&< z-S`@lJ_fiBxId0D*wn(L>o+s$hE;%FOxj!pK)uaxG3lmlfOna6GtzE8h>JYbPoeHD zNhaNjak>>aZtG&w7SQ&|IN%7AZb#njD04@eNsx^aWTW&cv~d^8Z3EnWh)MUn&!kVI zy-yD^Y5M^t-HZ0`MSGBe(*4~4&~-oZ9@q~j;kf|dd+-4O%0850(vCvF7AAcbxc&oa zpNj(!-nkag$E1h1GwG2=0Mb5>@}D1NQqK!a`U28+ZD7)u)3_E~!K6pK0cdk~7n8nv zE|b3I2lO%N>qy&!w!hKCq{p@~>6@FFw09%mEhc>n?{5L$w-*EUkpDH-7XHD>ImIg3 ze4LbQLY%E~Do zwM+Gcv__Y!Q44i(uU4vgxgMkHG2Mw;AXS3~*ej(#Z!svI!P-FeQnr$vDQG7uD$DD6 zgjT6v+mforHK(B;-c_BTa2Bi3)c}>LwyGkGbcs zn(uC+u*6hPCaL)~>uNl7Z3CWxtMEtxGCy0?(6FfCgktJhO7wPWd;3(6%F%C++P7kZ zrls_fvo!52UYpWTdGUTt+fRUM4T~0{TCkWY=;sg6&u3T<+s*d!)A(8Fp}TTsMFaY% z+W&>zGov(Ll}MzNC{d1JWn)F9iqu4F%L1OLQ7fEaOKVGeTYC+X+nSroFd7tW|B$eg zj8`)iK|2m zJ!ok|-{`-VwwaTPw6sxgn;R$hWM)&AVD5LxK1FtMMR6;#EP1?MO?G))+~x5oKGUs* zizM!HnW~Sw3Jsr2(!7R`OKzX;Q(Z2v*U&Us@wgSuA9TyA;`S(NI4X0WYRImLPgS`z zsj|*>kK3!Niq~J@R%L0o;Fn8HkH_@zAMnY@z1sf_x7UqEv$%c#O5bE7*Ei!`LChPH z&%-%)$!@o-a7k8tnpcuFEns>S%d2>BcjQwg$)^{}Ue$ETE|*)?BC@Pw<;u~pO5>r* z?x5d8@)8loyR3ADnai8JUvULp%xGNg+D4Ob1JU$nfJT6(U5DVcHjEBc;b4kZ- zK=U4hU-M4FFDBu54aom`|9)#PO2{7<)@2&<@);O99%bKTI1JLlAQl(wn_GzvvqKWu z|Cv0T&)?&vE2nY6>cq2RwPtz!xU2PFUaVmiixs8Wq$SD6c))KNni*8hP?4r7lIgYF z3U_hU?NJp>v~HSSMbR;2hT%3tsxMZ~B`(WBm#cuwala+Iyk@}SvfBulURgGb0-qPN zA4CiBeOpe9zk0o0fw=7J-GJ-;3n z#Qwsn%U#V)rP3VETgz!q%PSIM&np+ZU$hcCL6ygOQLR!6SPBqbI! zGT01xuMx~3Jkzvj3*gx4JgOP);qj7bC7D!`_DgOAja14ACOrnEi6;qGW0IgH30|vY zCo>kzZp68L7owPUygClTXwROdq2IQ1MG0(%jmtA7~jWv!s5P0FTYt*xZNCA>~=fz(K-!ayxlAg9xeyps4T)h z!X6T`id8|W!7zqu)b{y2S_-DIc%rgGt>^amhNI)@kY~*px?H(QamnOUcw+^$#VB%U z-a@1a9ye0c*4mP&!E7$T7Hq2#c;$*V7dT`wNAHqkAs_ze54#{#bZ&XP+MBToD`ZM@ zC5uvOG`G~?vNVsur)zFrVVA2osbQ|fzv?j|(0VlOW)Xmbb%6*RDxc!6iZxbma6tqF z8lKPQE)iQ@W~XSgS4e^aWf68TZg^dCqw8gVNBPxW!}K;i?LngHk>wjW*W3>|G1sRM zQ!b|p$a8Io`cG>vZt`g^36f^QtHAV6nXVL1O&aBpR|QJI$>n~rxn%AZjnjI8PLBDz z1?#1q!8%&qP9s5CDmI;+!&sm>D)^|4L==Q+v{lI?zPdR;8tiOtYvk4cd>q1w1`LcCHnngW zjwJ0uvr6(vURU@%T@bxfMNa0g|7<{H`93jrGEa8wCP7X0qM!by)4$#^NO+O9?ZZdq zS;ci7#?I}9NVLx~Y3TU&SVUPFt6>eSlfkNzEEH+4LGxuCWh@ z?P!j@CsekQC>86nlqVcDK)OckyBCUQlX%`Eo?`nm?UZI$(1>@D2Sti|2LX*(RQI+^P|bgRzSf@o&vOqqDnVYUs1og3uZNKk%B#N?0unE&|K3@w}I!|aGb^@5jW2)ubIy$O;yz1u5pqT+qT%jTgz4M zI^1)D#9W!bz8P6Je^K7VG}1Qx8Sh>6N^!M3d>@Cmyk?qTc6-nmN2Um0b@WoWx^9C-Ingl;<;7jjjGvBp{M-X2u+nu)*QkwhGWGx{OC? zn-D!|il79RIZBiMg7r?K2_=9A`+r+A7wgDluUS5ixDk8^)vCMmB+=hHsEc~UZ+R52 zp=eeuD%4tPDEgrrD-j&bm|m)1EHt?vwIm=Bzy%$($=VK&`U)hCQb-lemuWG-Y`Qp?3l+Dj zyy^9x-9n?ZRYR3T%bz@IZAV8fN$4#7B$t}mDeN@R54j>prk%vzdGblN*DV&NLom&0 zvtsL}c4tWp<$74zS{dg4y+#n!yOjc2dBt1eT`epna}?QqXGwwR4HV@sEAFC#%&P@O zZlyW^H^4w+x+fJhb_HFssQZ17XJrlbqN|0D?OM2+BG6BJ+={oLsHniJxS_%Zjh^k> z(J$J29vAY~$7ZoR*}d$u?0KBv5=6O>GhtVxxv8DziYBJ5eZD3(RBXP)!P$lA4z-b) zB_Y^b?5ohwNEm^kX~rOxu$0?S0^$j5E!bNOM+7CXs!$pdi69@VdU9xy_(9E#6vSSgn;;kx9LeVWfur%ctY zP|VWwP%IYGHFM~Hv0U<4{64W)E*DQeU0&HRH%`rtH(*T(TKd!}s)5#JpQQ68e5$I7 z<(gS#@nVl)v*)FnIx-{(xeMDV{}cFQ2)tPSGH4;?nHN`}5P)-0NQCQ7*lvbkU;H zS$sN~5B~l!4R%*)&urKzBx%D>Chx}g-zrI}N84Z1R0H`%=Vnf}(;zfxd|M>g6e%MU zO^#m74a~lYItV2^XrIE`Cb2aK^G!<=OBPG6-JU;a&)BCO3DMjXJyKXLg%ic;lIpNj zRkTx6YBhJUTdP%IP00oCQj~~dD0L@bE--F;-C&TO!L#ONw~>So&mUVZ8gq8Y;u*XmE91;OGw3 z&>2h#XYZPN7e)}=Plo6J%j!af)7Y%GgCHJsjuhj@mI++rbafaAe|Q>%?~HQ9Gdo)m3=B zq&6TKih43=bW%<-9*;vc*e%km9es=LT1`0 z`MbqQWsIe1wb6q!lonNLJE}`MI#H{%$s3CZ;dp~MdduQ?a#W-wXx+)o7<-z33w@Z1 z^U3+NGmy2{T~p6tDQRz=FbQIW$>d5qe#nw4p*={gEtR-+BHIWqp3nzWh8nLz6h)dF zk&1~z2DWq*YHmt2;Z<2NkMnyfHh=WY>m}*p4jK(QCTi|ZlQR-_}5LxM1~3!2+K&`xwz*>GK5mg8|Q(ur*|! zo!Xiroj+)dF>oheytu-j>7OxK@|XBcGv%*{PrYbcPf62qVndEQGg}Am!rU3Kfi1;q zYL24$dAPzN6%V(XG>cL9ad)m9{bYlvA&9n8t+w{?4R1b@S)&)I8V>(v3_5*XRrK~4 zf6eU*%4AIWxf7`Hpew0>XI@d&47WNh7Mo`1cbbqRnwrFp@I%tXQT?piD@u;p%wm4a zT3>s%o0pf%-JPVr-|F;->?R>Lhn;}&$6kz^1u>>A?emq^C(-cdc=Ww>w~bL?K+R$R$rv3OGf7EE1vxmZ-3S%IOlwulF2bWcJrGD6<- zCr`?>8B;Evzr3b-UT1fA=aTj2jn!?{)!4Z5&;BZ`!Bf}*b`se;+A+waa!0W`#+Ox2 z$2u;TVH3Bz1Y4$p+rOwi7>=;4gAP{H*8IspsH9rHv$R&hT{HBnVmKDjDe3Z0PS!hi zx#!o_FRF>R$7W<6nmc_+b!U1*F%IDoG!`kfrNQDOdUc7j(^(l8O7|C@O zCSqO?b!un+q)0M<-m4eup5>lA95gMeidOu^G%c~vC%pRmP}4&(2s8VfLOS57;2OGl z{LhJeW1lbnts`l3_BOmm97_K$U?k-O{G63JSw6H~D*o5@n|p;Ex_C+?gdgJ-xb;x0 zUZA%kQh<0UGR3bMhUOR9Mj$UcV2o$KCU9IT6uYi}PkAFVXRvt3@d=nfUp@ho=nD%Z z&9)*4Ice+sY0P&$c3bRU)j|ctiG@UEO;ihg->-1aj+$|%fhTC zOc)XVUEG7FPpYh`shnj5r}TxZ-SWOVq&Kh1NmyztGoA+OlMvrTu;!s^z?J zup%0*i1G%n#o_CK%OK9ZmikXeq5Bv_Hq!3{S+rt}MQC&dNKId(O8X%fdX*dV97RM8 z3*il^CN%ff8W=;%n<9Mp)s8?^>%+}t$E*H8ls_2_@Y`QaM+5N2h^AllPn#xi30npO zu2SH_)({B?NNmzBfeRJvEKWGk(#~I16eB3~V84HiIe`d&T7r48rv$rowfw}%f3gEy zDI{X)Ug`(%eMsYbmE|;#WXPhu3dUt@P_~AlqOI(D=YLbxm>N>osvXPDyy1pDH%#|? z)%Pwt`Q*z^R*)Q1k>mAGN5&0jF6&TLq@R2ljmZb_@pAE9tN;-qyQvlU_TU%~&6~f} z;I4bdTkL(N&fPHe@%sJqzy(>E>lcu6W?K+X8V{(PgF{x{p6rP_QK;CCqjA1*4wD zi@k-x`_`EWDMDi{?&t(xtE^=Fz9wr85~?O0zOejwSoAp2erha~=?TS7Z4Xqgs7#B_ zUVdu(G&0RiYd_WRC%nP{EBx^qpy!>4bH@eja-67dW1mJJ>=`Aq1z2PXFL&Og9l+O( zj2GfiQSQ96AS2SFxLE?t@sqS&E4i7$NhcVYt0zDWvq)%1QYS zXJ+d(tCPxCPwJ&QDGqgM0{+jK-ixUlcsy=;FQFhF@$pmyv#BpR1x*^JY%P9UE;XBk zxqorI7T=1gH@)FQzGMAP&a(TgQp(hQ86Z(VKEF8~xH`^og zYm_KreD`=qGs*EV-<>P6e>@fa=LF!ln{lROaH|x?X>uqKCJYs_{joO4F(k%>0_1d^o=UV(~TnYFN=(I~-B%t%1C5gw^{P>_^vnJP~iP zXYE=~u{YwO_k?)*L(;S5&?f0f_I^}?&tA_15#bQsPlCr`aR;=}trNHwCbA~sadga9 z1*Iv3W1LVWc%$B-#{tgk$H#14Yt`MI-JPA?oaGhMJ2JnVypyd}LoZRm&O91@teX7E zvh)#M{}N5Rb$rg^AJYP={}B!FR~~zw(|7->SPLj1*B__?P%*@g8X=n$S)^>1d^f57H4SU#&Gr(u`v+9bRD# zY;TJ~c91183R@L?uGQ?ZjKAY?1rlzpuB5`#-sY_+ndWgP0xr)qmus5H@l2zfcF%Z@ z+x>=cN8ciQTc@gWQB^?FP%abC~?#tmz4=jhd6%PTMSwLAp?XhgFFI`e|5|YWli4iQM+mAlJr1`AC_H3 zGQ2|njLbj7|5z?^$sd>%#@VXU0e4W`2<_MfFHmSz}>i@&3RB|HVKSZEm4hm zGRaF}4m1p*7vTZoguX5A>`7LPUp(Lpwt(~}w<9y@EJnw$*RTVvHoTr4Zohkn$7`%M zc@ws;Mq1?e&$OmlGw=7BoACu5-M>OSdD`^uE<$+}FTw_u+1gEI@tA2@GtJAWr1OWl zqX|r6e1oKG+b3a%CLDONu|^fOIojObTqAX8P_pBpn0A?2%=Kb3^U4`F#=GM;o^epO zZXr$k7EAwpI9vy>+Z3VYkc}@$9OKZ6$RxSb*c`35Z~e*mD@<*)e8pJ@gAlE0nMuZ{ zL1MBO=)o^8JY~l6b?cTdYLaC|u5igqyVl)%-z6OvR^nFyJna-~fa^xpv~~Bbo9;px z#qCN$wAN4k2v=01Nt`Xj+|a!Pex{37#4a{2lO4ixH?YDM8g=2yQfO(y-xp-oL_;%0 zCDL<&!d6OZN;!W*RT^}MoJ&CT{0mG71w{?qc{YfBYF?Ke<$e(d@%g3scR;BwdTe)P z#T>YfbN!)VEnHQlR<(Feved9Wp?eW$Oo&02M1f(jFFo)_y!kjtBd-tR_BqUZ0 zlq+;6{2^g}DAPrA&2id_WxgSf^5OtaXL|gN=8hn_Kpdq-9Xg@$^`hMJyyxfRI(FGr zc~57a#2Ju!LRQ?Y&Swv|u({)E8RCT`Bi~Lm5&l@H@T^aq)j4hMwBA@KO>sPyEja6} z1%n5_2;J5I?cp$Pk~6Ie`#wLvafwxG&RI~k zzIc8M{O_SNRutmUESI0So}U+23kr?~JX0&9=hIr_w+5yZmUCVjmL#uNuOxlY(T5v^ zghY!lRFL1eI45DiaVQjB$?+292?IdOgReYTU`h*T2qm?!q5SGct}bt|BQqAl-=6n8 zcv)izB5Hx@-Rm_gq*JfH`c$bxL>A1zZ@(IG@~3{|>!JAHW^7>rNTfEw3OVo9IsBKt z(o)}9_PK&z@6-L6y?)(ymg(K@HO0DlkKZEdLJK{%Hin9RI|bV@j7YfunLh#FE8(J| zu!f(Zc*0>kLVVLi{6T$c<5XR72ZD-TuPea-&JV)A^^%mtPl^N3?`D&~?y>DwkGDG? zCT!6%?4mG2zytwe&Tg${eaRYPUU?_%2EfVoMB(71wkM@};h5d|hPMv6_6ZoT9brP;T z6*aL0x z1MwdZ#nMTBNC@f1?rcBAK1{qte#+w-LQX5fE=v=YYy^_6m-} zTL@LUzgEb_kzA2VUNXJbL^Y;B|FVD1Pg8X!u=|zuh}->CW&Du>Y}c$*4>5H zC#*uZzB&}!x^(T*rE3!{%UW8N@kYA;w8y2~3qE3J;uX)J7~zH^SbJE8(p=>TRH*l%wWs&c#%3- z891iJ0686I2cj9>&q@{O^&keCGqEd5gzd91S|$%*kJ>{I#}f$dc5$Dkb;_pPscF8( z#{3BHagN6E%%MDz8-Yn=WcG&CPDSZdL%q3x(~sYXJ>)q3pc@bp6IsHE*a`ixZA;W; zB-_R%QjTZ=+AM|EHx&{EDS8xcawy~HOeO{Tp<@y7caM|b?Wby;n1;U}zt4bLk}X?t}*| z98e@4`DF2=s?<_RJjU2~_+J{|@IRq{a(~3U+Cw-#jQmTJ$J-wJ-f@D{<3XMpAL+5Y zQ9mxdlg6{=hV?WyMtPe+jk3h#1@}uPWV!NIkx@v zJhfWWwsvURGFwHmo8-%oS6=^4cQ%unBmLk9Lg&OCf3)5$>;mwMCIx&?;k@DspW*86 z>2?{uisE^NYH?fT>Z;aAMNxpf=K@6)k=Cl!l~7mnZI}jQZFJ!deXd<-G@39@u3NET z!-}(n`7*g-2Y%pS-&1$t4m`lhaW*Y=Vb>(v|9lDeJTMrM3lyR_*5eA`MSt7$a@eKH zXSDj&8Bv<)@&@>%EaG2lxu9)%;-vNKPf9H7IKL&5q2E6?lv9cNMd%~xqG`-m8fU^H z@=?b0(E zDlqnaQ@7_7|ECguJ6V3v3#gaQcX09$WwGhC`W~wqf7R9={2{9I^}4>}VW8YXk4zeP zciu*ihjcM#Hf|qzg|v%{IYC`bei+{(PZP?d^M2G@>g~xQ>&cIZKC`j?{2BfN=7&5t z@Qo6|_EEmOurSjTo!K1zquFL2GD~8-r?kjK6+y@Egnmyt4kL$OnlI9%X{f4gB`ue< z9b+5|(E~oC;!TgTe8XB}b}nMLadh-%TPHbYh38w#cu#KTfpPNq*nTK>Z|q;(I6WJj zG(f*{#)Ky^(AfbJkxBE3a};=hexnCX4G-#~_=z4p(0z#=JEMGKW=H;%3cR@~MPKuH zE)RbeHL&tjKGX3BGOHb~<2kOwoQ#cN-}>ngnwy*(oUQt~xXe9QcV5m7&TV-~`neP3DjamP_@m`yn84@)wT*8h z*C1C$bqCL?$^AZG)JN&` zS`Yc=%IniK{I8^Qm{t732>pO@0S7g3pK3~C=xMCz2M>v?!QN@I>!dojrxL#ki53K= zSb+k3TcblSE-PLZPn47-N*oG40!6JDzdH#8@l_!hqf3J_WTdCSpD3O-cNFDH5))~% zV3EnqRFz{V6Fy7hcLMm;s!f_R+MbG{qKYCDauXtx{;kf=WF^vzD*jNYuhsQJn8gZeigEKKd*gEbu}=z{TA0Goast*qZ>%@hW0YI!ZWYQLEXmj}oG6xW$<* z4oOw&yLc(2%F>H{rTLM^r!VkM)%B^~1=Eo`AtF3RdyiAcLGaz<)UgpF zS?zV7^(%?EEe;WdA%w_+DV|GF5+Fiis8Pt1O}vcmB{IL;gde%vl7zBJ%v>vc3$7KS zMeG}Yz+NWW2xfnA<6K%U;MwzaIqd9DPi`XfJ8|_w;|F8`Oa`yYMZJ{6IzA?fy ziyIGYe{>RP4Zzk^`5|Z?$!j~Jp?i64qYuj1Z-Jlg(&@_wS(C5Oj>>p$nppzZO-R75tKd4RmJZ`#Rh^5~!K?ih!Y1^WFRMYnAv6+jTqJ|vyRW_D3 zt>|pQXZIwG{SL2Tt>(U>X~{a>)QZlSK7G;DA`kDh3}~eJ=UzYb%O>&*2mq&0hpx(?B+EIifo0(p$U{g$#E#QDT{=ocEWi7_`WQ4ncf`Df?A?1jgXzn zSuPiMVs-)A->Hit$3~N{6JaOGojDj6vnF4hN0xaN;(C+sA`_lN0Pb zT03kzBw8QNRayKw1;WNfUl#}(HUfc##*ecge&hKU)WkhGY^-!K*g(FtMArgRlCagm zofdpW?n7m?)6=Y5a3_Nan$|vl!=orcC8XWug8OsAXL8qFS_mnM=bn2$JnI@I=&8Ub zJ>S9VvFoP7e<;-Iay-S*IMHmX%(KRR235~it2 z5`HGm=J=}Ee$rcgzO8Om1vX(j5O#MreqC0N`=*7AHREy%v!mbz`R3S7vLic(zM4P| z*->ZPRLh%lo(19vhTS2a##C|!tqoIYyAuVC>E4#A()yB;`qHWvZ|1EtE6WQ4(dcgh z0X+~eO7#STJ(w?ed)PnM?8MxKt6Jh!Rq>XpP^h)UzVTM8s!29~F!q#`#96Qg5wUsW z29mtL=m(9mZ5gjniBt>^?ns-YiqeNZ)^ScrYfvdF#JBn4$5Cfp4ltb!ao##0PN1RO5!Alaw*AiP zWwaN0t#anfO57Z``%0^eOA^82DM5F**jvGO$C{=$ozg&e#w%y(df<$bM0H6ZUL32K zHm}-(BytgxqHngn0C$me#u48yg?rU}Frq5F+lT>Rsj-j1?RL{OZPGk=Q0;hp2RTyV zb#(5kRhw3=nu{B!Q)kJ#KNR!mKf^~BI@he}v|oQ+HCL4BLxEW}hVF@l{JKmefDW94 zQ&=Z{+8RnB1;3_I^ko_N#Ru&G)&ux85l6$DsVRE=wIt0pN@AIK zti>F|Q~*EBipM$W(@NdCnUY7NZwYXQF9Yn?3>9Axzz175Xu5m$9@WtH z)11Ma5B|h|@V*bDpEK}vkR|x;$yIbiSdOipI*pSS+Qvozfrq^!7lwM53;!$WVkayY zODreD$A72I)AmVMc+g{Su>U4_(ocwx^(&K%g&KwN%r>FTI;Q_%0H?qJ9H7IM9x=kym8~HNK@3NxlQn5RFzIE9JV^)B85K@ z-mEIqJ(@Ajve^UYQHI@|dHH4l}Cc zmxGzt-S}XyDY@e?{svwS;2B%Gl+GFJG(+<6J9X1_e=uI)E3zu1<>k>rzrT>)@c{p* zaMFUczW`=A&_Tus>|yYI5@TkI9^1kebzBpKm8~a9E4Npe)8e6664j{gV$?O9lNSjs zSQ_a1vAP24mpD>H!M;@9a56yROlZeus6*0h1ajrEr1)G&~Kalxe zE;yd}8xmR>@y!X`?BG`6b}A$u<98BEDNQ_nL?Jw2YCv&V#l&S)=Fo)Sfy5yF>9+)@ zV3lFy@-H3mJ+_>HuT*F&NMJ9pAQX|@jPI`qUs&5FFFqd*;o#_0u&p-=XGF*80xr87 z@jbRATy{TjNu{51yLSuMidDk3qTB>^)VJcqM9XK~qk74urOSPi>T%6*SGhBz!eir8 z!egVwgKwEZ@k$h{#1G(U`j$vJc>&RxyP}3RRB@EZohKkU9L+TQV@ipwTORNis=jK| zTPqxL)h~M1EnmKFuW_ zAbz#>Q}zqApL2GnFE7B`ojxa@^LmfweBHsH@PY%G^fhqDWxggdAEyZ@$hj19fCNJN zaA^Xaa@^?0@zJRkC`<9P@q9n}`OtGo+b!j7O0L%bo*tO}@Rc!BQ#7yJ@52X@xzCSJ zXDFJ9?~ZAH|3AzvE=YI`z`BZn>M1t@rG=%smPmMfWrb4$M%=3g%H8hr0?ixO3#Js6 z`aFq*rlXLZSK+=fAV(@2BjO2V=ieuUzF+E6@gW>jw|oUbuZ0izy6L;gmdj;%@U3UJ z2T5LhxWmGU6*=(Aa?LmO$`IhOfHzoNYk31S2{62pohWxVDNMmJ035e%cCedM-_?DkLM+Z@8Wa&W81UeuBg+8zd zc9j6}Jx+d(J^bc>}xy^O;_y~O1O5Ct#&!2WC-y`?rDAOC6TLJv=8mM!)~L-*nrrLOQ6C@)l(?g?)}T{^#rU+K_#b&T(% zeNhjf58Glj>sV69`Lsl%>D|f;C9hP-w|dRSgg#B!x!?_FdZ+!VU{1ByTrWwDvfL<1 z>rHRf+=A;&ZzJX6fwt{)OA>9I{E0>Yp)HGm33H6v;dK zJwpRdW)UR1{yY>r8}NWwP*xp`TOif{K#Kc9Hg`EKa}@%wZrLQLp%7}7GB zc_4BTit@h%GkxhGJ}nvS_Ukj}oKrH_R2#J*e^d^pGkwAJL9J0W=a!s1yU7>mE+Ack z_NQK)xyT<0S{Jx2f2(Z{(rLYc`Im$Bo4%$m6B?{Xi=n=;c-dw?6Vet#i8SsPOB?jhi=<`SYY=a=d6j9 zK983>JPF>+oh7ly<-$Bn2S!@&5|lCec}rT~s^c%EFVpcSu@BHJ?+wNLd-tO8)#9{g zpB=fM&UZ0to7VVmVK@H*qu;DI!h^uMe{g#Te60QFw zoFLsFU*-0A)s4FzvZFuUPSI-*Y@9=7s;^SL9`~w`L&3qJQr)$YqT7FJN9B(_N8|8+ zczY8#InMGoN#?u!SvbV+L%%1`J~o zf&d3(AY38w5D4qdI*VWl1}DI9BzQ@}ig12`%?|s8{Ji<)+n9tT&L*_K|MR?6-93kd zo!#%VHC1oDRbBO-&wXGKG(oS1(H4rlxK5W@EtYgQ5>s2L1TzKVv?jfAkiD+Akd@%2 z(&6W&;R)7m+{?#~PI^b+k!13bz&vYBC^rj|tWXn}PHF;f43lr;>o-2 zI{6PrVjr?n`o5`1WOFG}j!tchLc16^@IWl7A)yS?;!XMimG^{Oa z?^?O8yUQb;%aBeF>mlz|IcsF~-O~I%K&KVj$>bHuuIyg2DdBK`E&N17%|{7Tz4pMq^ooL+~2?9qu36OtGZcw#Xgl zIJIRumF9m*WJVwrE!jh3v}qlaS1FFt*{?I!DR*OCd;uc{oA!UhoNUnM^c4BRV2{yK zAZ`%@E}LEr3Hrx-vJywTJzkPvlVrG3BU0bSi! zX?u3Hm)>;EL0CVwp0+04i6@OeRZ`qSNo`|yt&#qP@EO`13v?QU=5Ud95!nu z;Gi*3jynN>%cL;~Og1JUk}nUIE8q|Yky30I3-9$Ze6w_fu7pH?B9NXSEkk;`f%D)* z3J#-U?y_CqR{ve~NmYGP%-$U?Puw(bSsT;*zruk}B{ki&TI@kEQ_wk+9qx_sG z>^ph#YfbWlS~@o#Ir`%9a}!5@vw;7KyNb=e5{|_;K z{Gr*Q9S^=3YZUA~IhbKTg{6egO9v$W)lB62<|^NfPYuSu8;teljtuCkcEsLyR13Ct zNKcd-2M*_ZW5Msn2dCoS8$8jQOPESfJG#$4qN%NSwPUsg@O`o+{wQc~@+IC2uhrXJ zDW%daw9b>d|6rV~S5a997lB2qWzu^>Z}obQ6)CqpJVVn!0l& zoahxlW_i&H9m#(mvZM9ok3{^Y|4;;J=r~BUKegNcu_%goCB9<%aR*hOVp)jOu~@yT zp|N=GUqI)>^_M)?&>zU**lS@YDM%;8(~xF=KI_a`pNEW7Ece-;Z~ZSMXPPmbZ+_nH z!`u?hwt}%;!Wv~CyFsa7WDW+ZM&6Wcd%Q-cz){wl%f0c|e7tv=-7zl3dt1KV_*`#% zDc;*$T&Sa>H-3qv{@+LST)ekV74<@aXb@UcBtIhgzZBI1__8kswjY}*kkiF#_KGwE zp5&h#G=Lbz{BeM^)v`VF$lf&y^%=J3nq!FcK0!j z`5;-NyMBOjtYs{SkOzH<`<^1+D>W_kEK&wWf-;7b^mr1BEr91}uEfFN;eEqzx@owg zBcl@pJ;5Zh80|>zO(u~?KsYHgb?3EiTJ&?FkGu(|o8E-C7*;MzNvyUfQ+UaaB(iTA zF(+w7E?@h$$*(}?cwN)HkrgA$I%u0?{WM6af`yZ&Lo%UchqoP_YF*lKa(d|I+2JF} zOmA)GznTZOOdUPBWAsQ^Q)h1;9*E4;Vi>=lffo7`?6n+5(9>2TK3-=&T{;bF9Fkr@ zR-udEOVx&2-yEu?USklJzCs7%vyayU&A@Q^@Zs_oKEU{y4F~eF~vJAN|#UG;m zl(VIQuaR^T3JqN<3#bd}Mis1t7Z)uJ=!a)Khd@tWEK;8^&CG$!eLXq{VbcS}B7%X> z;6m@Y4>wG+^#SH(BKyf#82zVgH zS%4Jh2kD|Xi}SNy&0&bD=XD83z;k|?YzWh2Awr|TszCriTmf8QnX*wNg)b*G&%~3g zFKyl|hQC$1xmYYdk!q%rBK2;EOcXCc;GsVe4u@*tKg&)W>Uo@NA0K&BY~;-&-!GL) zw-kRrjm@$<^#!dFL!h*dl`0+wsU#JoyTR&Bl*jc?s>H!0ryT({tMxP5=oD8V)Or_(z!gYA zhunr295OKEJ%G6(8OS;bNVkY%wJ3wz{IB@kFXML<)6B~Z=LrPMZp5;yUPl-OE-?z6 z-?%Jxzy*+Q;jpw}QNmv7LqxDQjh)aS*z3`$DQRah>78FFc4r(*sRG;o#53IFWo~lw zD&=3;8kN95XyU+JNSpG@K1PK6^Dn*WF>r$)(TzV#+FlHkY zjkVodd%c#Jsk+v0u!kcE4dNR3lnmp_I^Ed??J`0=fbEC2gdrgilf1tx6v||h5fjN^ zY-H7pAn>$MaS=!x##UPsAHB-Rbd4Aiq(uxv7>b5)vxc$H>B*Xqu>-8@p=>mt?m?0@ zW7HpaVu11{WH+io5=KLZLq!CsJ-~VVMu+;r0x8fpap?A4UBRPG#Fa(^kyy}Jvi-^4 zux9v;0K(kH6NyMVq@o^tjz{B+lHP62z`=Hk`Bvim02I6wQM41uWu;#M(M>RyI6@4T zn-?w3gwfQbyJWa|x5pK6Xma1+&InN8W+a01hx%t@*=XoY>SjdQ-r&|sr(?O^Kw#o< zwSau6mOmwod^Q$J`*ZV-v#y@(uvxCeaF-(jt?{KKNi-V_tjA0wl0yXeHIy&6{}PWe zf-!Yn-Tccpiv^zVc{+;4PK%d{qoc*Hsacx6`R3WBR@W0~r`8^O8OebnDXfD^8UHsSq441?DJp+B!2Nx7z_4;HIZisX5BJK zGilC?K&8MJdZVhZL<*3y6$(qasi_?P#D2GMD-h)S5JT>iX^OjX-hO7{w3z2-BawN2 zHkbQ7_xw+YIESarQ#j*k^DCz(&iMRu;pdoLIXvNTBur_Sgko6kDy% zkLFkZhVaTVvR~78B=5w`$Fld7*wg)E`5hmA^Y7(vXv#d6#QVwa?lyMwSaJ`nAI)eP zc3|b`>UED*zjn@dia6KYpigiN>P)jooi)|B%Kz=vxt!+y>T$wZ}W{eFmRh z@xDYbiyDr#)yuIiI&OL-jRi~5=O2g3__SWyqCh6X;&7M7fbg~yQp7*1I)DJSzpfS^ zP|_j5EYyZqhhjQZelQ5JdI|y^^L6Hlk5q#J0M<5Zz8}X}3&simA1_Obh6;)2X z%r=uZ)pRFLcaihAg3OZWHPcK_iGKzh*^Due;FD1&3~T#HEZjlWY*))dWfCHM2jQw< z+FO3<^c|PPBX`_#&@xMgIX+9#x;7u_xfRw{D~N~XILE4+#Up?ENE7NY!x)A$WVNVk zb&AYI^qS;}iQZ=-*0o@&!ZyLv)6(k1ss_mF7Jbld<^aeZ?u|wq4iAXob|PS!_nJ9G z_uy0D1pMHUNLtq)#3=>)uUFCaKjZH~9o6vKqo3sPFn+=(`~=uEt;dZwx=-LI)K;>m z6#FnvSbcyFx=O4F>yF7jn~*6ivTHB_x~zv_pS0z5u*WXI2H+Adh6F5tT2~NN{VNgs z8Pkc|Z=t2pfs(q2g zwB*_Llf29NU34Nwox1wt-Myw~X6fZ^fv)G3)jBheB^Dj`n7P8PjnPC{WG~(;Jjk zgJbT*lP<=BtQm#_Cdl*VD( z*lk4=Rk6+ft+nb*zhxsocEs9k*oPYk47X(#vE~MliVu?^<_2?vu~+TC?UsG^O%}wqS{VVDq@f!i8q!vMa5ye8NRp3YWd|wD9cGH-kK-Pc%oi#dF=(USSZXPRzP0N z=UaaORD~jq9qZV>4EUgtO;HMaW+KrboE0A5h2;9dCbL^r3a$;d~UYt65bgF>VSIF*Asia!IqJzO! z6^OI7tkImQJ#B@K-gy)$!3v9zSGE=l86;xwHR;o!@I2X34qqm_Ac{x-N-zy%j{unw zspHURLMFt!f&`ryhlwn<1*!nq!LQ%>di2iyjxjx=qPhkT?r*vsdtofq zq*(B0*_;Mqri3HS+CRgF4lr(SPIAe$jkmHelFSD1k50pgBrgDr4iR8F1PRs$;Z(+t zH|1&3Ie8L6H)edPJ`6F=(unKAPW^BBxsP#@)<3Sza94D~SWDiZ3uQyjh8&eBK&3g=(U zHB^&r{Ujg>ehF}h1v)if;O$pfq5T&DThwYYZxo^EK+=JsPB6M%EC_dlww5;9nP7)k zKhHnKToIin_Y9yt(P;_=B1@ow&{yc?Doztq)hRGT_#b(}Pz%I|`{{|aQkb7#tY=u5gz;u4J_)w;54M>x}OSH^u-+D+lKiA{mZ-T1Dn06l+7p!m_!`RiUqQ+6X zh~yzk&6~~k9JXh3vf}xvNa^MR1QK0h1-B>`WUNz;tGW*%wdUF_X%|wsbn-F)}sjseFlw|q* zy!1-GsDED9*VKLMqNKy|3{&#BjnFxGis^T+7X4n;hDMuNXa~wq0mXFj6mEU``LyAa zYrb~1HUO_ZEB1gtFdMXX=hqD7|8i`-Q|6{tTaIbQKVIGpAyI

    Icgd~U|XBin{ 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 literal 0 HcmV?d00001 diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.svg b/public/vendor/fontawesome/webfonts/fa-solid-900.svg new file mode 100644 index 0000000000..dce459d0e5 --- /dev/null +++ b/public/vendor/fontawesome/webfonts/fa-solid-900.svg @@ -0,0 +1,5034 @@ + + + + +Created by FontForge 20201107 at Tue Mar 16 10:15:04 2021 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.ttf b/public/vendor/fontawesome/webfonts/fa-solid-900.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f33e8162997aaa9da582aa81428ee87aa48953a6 GIT binary patch literal 202744 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­4q>?ySYZo|x^QGEv^3%G3o>h>p_!r(>A65* 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 z

    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 u|2o=E5jXSlujAO1trGHRNosweEgi(VA4 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 V-}}z&H&EyL{>eN4)A{}F{{tWHyO#g} literal 0 HcmV?d00001 diff --git a/public/vendor/fontawesome/webfonts/fa-solid-900.woff b/public/vendor/fontawesome/webfonts/fa-solid-900.woff new file mode 100644 index 0000000000000000000000000000000000000000..73c1a4d5d156b6ddc62a7e3eba1c206bd6ad19c8 GIT binary patch literal 101652 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

    )n+sYj_ttH-Fb)nnB;>Rff6dYpQ^dV+eQdXhR{ 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 EKZs0^EC2ui literal 0 HcmV?d00001 diff --git a/public/vendor/jquery/draggable-background/backgroundDraggable.js b/public/vendor/jquery/draggable-background/backgroundDraggable.js new file mode 100644 index 0000000000..8453e5aeb0 --- /dev/null +++ b/public/vendor/jquery/draggable-background/backgroundDraggable.js @@ -0,0 +1,174 @@ +/** + * Draggable Background plugin for jQuery + * + * v1.2.4 + * + * Copyright (c) 2014 Kenneth Chung + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + */ +;(function($) { + var $window = $(window); + + // Helper function to guarantee a value between low and hi unless bool is false + var limit = function(low, hi, value, bool) { + if (arguments.length === 3 || bool) { + if (value < low) return low; + if (value > hi) return hi; + } + return value; + }; + + // Adds clientX and clientY properties to the jQuery's event object from touch + var modifyEventForTouch = function(e) { + e.clientX = e.originalEvent.touches[0].clientX; + e.clientY = e.originalEvent.touches[0].clientY; + }; + + var getBackgroundImageDimensions = function($el) { + var bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1]; + if (!bgSrc) return; + + var imageDimensions = { width: 0, height: 0 }, + image = new Image(); + + image.onload = function() { + if ($el.css('background-size') == "cover") { + var elementWidth = $el.innerWidth(), + elementHeight = $el.innerHeight(), + elementAspectRatio = elementWidth / elementHeight; + imageAspectRatio = image.width / image.height, + scale = 1; + + if (imageAspectRatio >= elementAspectRatio) { + scale = elementHeight / image.height; + } else { + scale = elementWidth / image.width; + } + + imageDimensions.width = image.width * scale; + imageDimensions.height = image.height * scale; + } else { + imageDimensions.width = image.width; + imageDimensions.height = image.height; + } + }; + + image.src = bgSrc; + + return imageDimensions; + }; + + function Plugin(element, options) { + this.element = element; + this.options = options; + this.init(); + } + + Plugin.prototype.init = function() { + var $el = $(this.element), + bgSrc = ($el.css('background-image').match(/^url\(['"]?(.*?)['"]?\)$/i) || [])[1], + options = this.options; + + if (!bgSrc) return; + + // Get the image's width and height if bound + var imageDimensions = { width: 0, height: 0 }; + if (options.bound || options.units == 'percent') { + imageDimensions = getBackgroundImageDimensions($el); + } + + $el.on('mousedown.dbg touchstart.dbg', function(e) { + if (e.target !== $el[0]) { + return; + } + e.preventDefault(); + + if (e.originalEvent.touches) { + modifyEventForTouch(e); + } else if (e.which !== 1) { + return; + } + + var x0 = e.clientX, + y0 = e.clientY, + pos = $el.css('background-position').match(/(-?\d+).*?\s(-?\d+)/) || [], + xPos = parseInt(pos[1]) || 0, + yPos = parseInt(pos[2]) || 0; + + // We must convert percentage back to pixels + if (options.units == 'percent') { + xPos = Math.round(xPos / -200 * imageDimensions.width); + yPos = Math.round(yPos / -200 * imageDimensions.height); + } + + $window.on('mousemove.dbg touchmove.dbg', function(e) { + e.preventDefault(); + + if (e.originalEvent.touches) { + modifyEventForTouch(e); + } + + var x = e.clientX, + y = e.clientY; + + if (options.units == 'percent') { + xPos = options.axis === 'y' ? xPos : limit(-imageDimensions.width/2, 0, xPos+x-x0, options.bound); + yPos = options.axis === 'x' ? yPos : limit(-imageDimensions.height/2, 0, yPos+y-y0, options.bound); + + // Convert pixels to percentage + $el.css('background-position', xPos / imageDimensions.width * -200 + '% ' + yPos / imageDimensions.height * -200 + '%'); + } else { + xPos = options.axis === 'y' ? xPos : limit($el.innerWidth()-imageDimensions.width, 0, xPos+x-x0, options.bound); + yPos = options.axis === 'x' ? yPos : limit($el.innerHeight()-imageDimensions.height, 0, yPos+y-y0, options.bound); + + $el.css('background-position', xPos + 'px ' + yPos + 'px'); + } + + x0 = x; + y0 = y; + + }); + + $window.on('mouseup.dbg touchend.dbg mouseleave.dbg', function() { + if (options.done) { + options.done(); + } + + $window.off('mousemove.dbg touchmove.dbg'); + $window.off('mouseup.dbg touchend.dbg mouseleave.dbg'); + }); + }); + }; + + Plugin.prototype.disable = function() { + var $el = $(this.element); + $el.off('mousedown.dbg touchstart.dbg'); + $window.off('mousemove.dbg touchmove.dbg mouseup.dbg touchend.dbg mouseleave.dbg'); + } + + $.fn.backgroundDraggable = function(options) { + var options = options; + var args = Array.prototype.slice.call(arguments, 1); + + return this.each(function() { + var $this = $(this); + + if (typeof options == 'undefined' || typeof options == 'object') { + options = $.extend({}, $.fn.backgroundDraggable.defaults, options); + var plugin = new Plugin(this, options); + $this.data('dbg', plugin); + } else if (typeof options == 'string' && $this.data('dbg')) { + var plugin = $this.data('dbg'); + Plugin.prototype[options].apply(plugin, args); + } + }); + }; + + $.fn.backgroundDraggable.defaults = { + bound: true, + axis: undefined, + units: 'pixels' + }; +}(jQuery)); \ No newline at end of file diff --git a/public/vendor/mdl/material.css b/public/vendor/mdl/material.css new file mode 100644 index 0000000000..74b6b7b71f --- /dev/null +++ b/public/vendor/mdl/material.css @@ -0,0 +1,11476 @@ +/** + * material-design-lite - Material Design Components in CSS, JS and HTML + * @version v1.3.0 + * @license Apache-2.0 + * @copyright 2015 Google, Inc. + * @link https://github.com/google/material-design-lite + */ +@charset "UTF-8"; +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Material Design Lite */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/* + * What follows is the result of much research on cross-browser styling. + * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, + * Kroc Camen, and the H5BP dev community and team. + */ +/* ========================================================================== + Base styles: opinionated defaults + ========================================================================== */ +/*html { + color: rgba(0,0,0, 0.87); + font-size: 1em; + line-height: 1.4; }*/ + +/* + * Remove text-shadow in selection highlight: + * https://twitter.com/miketaylr/status/12228805301 + * + * These selection rule sets have to be separate. + * Customize the background color to match your design. + */ +::-moz-selection { + background: #b3d4fc; + text-shadow: none; } +::selection { + background: #b3d4fc; + text-shadow: none; } + +/* + * A better looking default horizontal rule + */ +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; } + +/* + * Remove the gap between audio, canvas, iframes, + * images, videos and the bottom of their containers: + * https://github.com/h5bp/html5-boilerplate/issues/440 + */ +audio, +canvas, +iframe, +img, +svg, +video { + vertical-align: middle; } + +/* + * Remove default fieldset styles. + */ +fieldset { + border: 0; + margin: 0; + padding: 0; } + +/* + * Allow only vertical resizing of textareas. + */ +textarea { + resize: vertical; } + +/* ========================================================================== + Browser Upgrade Prompt + ========================================================================== */ +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; } + +/* ========================================================================== + Author's custom styles + ========================================================================== */ +/* ========================================================================== + Helper classes + ========================================================================== */ +/* + * Hide visually and from screen readers: + */ +.hidden { + display: none !important; } + +/* + * Hide only visually, but have it available for screen readers: + * http://snook.ca/archives/html_and_css/hiding-content-for-accessibility + */ +.visuallyhidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; } + +/* + * Extends the .visuallyhidden class to allow the element + * to be focusable when navigated to via the keyboard: + * https://www.drupal.org/node/897638 + */ +.visuallyhidden.focusable:active, +.visuallyhidden.focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; } + +/* + * Hide visually and from screen readers, but maintain layout + */ +.invisible { + visibility: hidden; } + +/* + * Clearfix: contain floats + * + * For modern browsers + * 1. The space content is one way to avoid an Opera bug when the + * `contenteditable` attribute is included anywhere else in the document. + * Otherwise it causes space to appear at the top and bottom of elements + * that receive the `clearfix` class. + * 2. The use of `table` rather than `block` is only necessary if using + * `:before` to contain the top-margins of child elements. + */ +.clearfix:before, +.clearfix:after { + content: " "; + /* 1 */ + display: table; + /* 2 */ } + +.clearfix:after { + clear: both; } + +/* ========================================================================== + EXAMPLE Media Queries for Responsive Design. + These examples override the primary ('mobile first') styles. + Modify as content requires. + ========================================================================== */ +@media only screen and (min-width: 35em) { + /* Style adjustments for viewports that meet the condition */ } + +@media print, (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 1.25dppx), (min-resolution: 120dpi) { + /* Style adjustments for high resolution devices */ } + +/* ========================================================================== + Print styles. + Inlined to avoid the additional HTTP request: + http://www.phpied.com/delay-loading-your-print-css/ + ========================================================================== */ +@media print { + *, + *:before, + *:after, + *:first-letter { + background: transparent !important; + color: #000 !important; + /* Black prints faster: http://www.sanbeiji.com/archives/953 */ + box-shadow: none !important; } + a, + a:visited { + text-decoration: underline; } + a[href]:after { + content: " (" attr(href) ")"; } + abbr[title]:after { + content: " (" attr(title) ")"; } + /* + * Don't show links that are fragment identifiers, + * or use the `javascript:` pseudo protocol + */ + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; } + /* + * Printing Tables: + * http://css-discuss.incutio.com/wiki/Printing_Tables + */ + thead { + display: table-header-group; } + tr, + img { + page-break-inside: avoid; } + img { + max-width: 100% !important; } + p, + h2, + h3 { + orphans: 3; + widows: 3; } + h2, + h3 { + page-break-after: avoid; } } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Remove the unwanted box around FAB buttons */ +/* More info: http://goo.gl/IPwKi */ +a, .mdl-accordion, .mdl-button, .mdl-card, .mdl-checkbox, .mdl-dropdown-menu, +.mdl-icon-toggle, .mdl-item, .mdl-radio, .mdl-slider, .mdl-switch, .mdl-tabs__tab { + -webkit-tap-highlight-color: transparent; + -webkit-tap-highlight-color: rgba(255, 255, 255, 0); } + +/* + * Make html take up the entire screen + * Then set touch-action to avoid touch delay on mobile IE + */ +html { + width: 100%; + height: 100%; + -ms-touch-action: manipulation; + touch-action: manipulation; } + +/* +* Make body take up the entire screen +* Remove body margin so layout containers don't cause extra overflow. +*/ +body { + width: 100%; + min-height: 100%; + margin: 0; } + +/* + * Main display reset for IE support. + * Source: http://weblog.west-wind.com/posts/2015/Jan/12/main-HTML5-Tag-not-working-in-Internet-Explorer-91011 + */ +main { + display: block; } + +/* +* Apply no display to elements with the hidden attribute. +* IE 9 and 10 support. +*/ +*[hidden] { + display: none !important; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +/*html, body { + font-family: "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 20px; } + +h1, h2, h3, h4, h5, h6, p { + margin: 0; + padding: 0; }*/ + +/** + * Styles for HTML elements + */ +/*h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 56px; + font-weight: 400; + line-height: 1.35; + letter-spacing: -0.02em; + opacity: 0.54; + font-size: 0.6em; } + +h1 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 56px; + font-weight: 400; + line-height: 1.35; + letter-spacing: -0.02em; + margin-top: 24px; + margin-bottom: 24px; } + +h2 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 45px; + font-weight: 400; + line-height: 48px; + margin-top: 24px; + margin-bottom: 24px; } + +h3 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 34px; + font-weight: 400; + line-height: 40px; + margin-top: 24px; + margin-bottom: 24px; } + +h4 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 24px; + font-weight: 400; + line-height: 32px; + -moz-osx-font-smoothing: grayscale; + margin-top: 24px; + margin-bottom: 16px; } + +h5 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 20px; + font-weight: 500; + line-height: 1; + letter-spacing: 0.02em; + margin-top: 24px; + margin-bottom: 16px; } + +h6 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.04em; + margin-top: 24px; + margin-bottom: 16px; } + +p { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + margin-bottom: 16px; } + +a { + color: rgb(255,64,129); + font-weight: 500; } + +blockquote { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + position: relative; + font-size: 24px; + font-weight: 300; + font-style: italic; + line-height: 1.35; + letter-spacing: 0.08em; } + blockquote:before { + position: absolute; + left: -0.5em; + content: '“'; } + blockquote:after { + content: '”'; + margin-left: -0.05em; } + +mark { + background-color: #f4ff81; } + +dt { + font-weight: 700; } + +address { + font-size: 12px; + font-weight: 400; + line-height: 1; + letter-spacing: 0; + font-style: normal; } + +ul, ol { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; }*/ + +/** + * Class Name Styles + */ +.mdl-typography--display-4 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 112px; + font-weight: 300; + line-height: 1; + letter-spacing: -0.04em; } + +.mdl-typography--display-4-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 112px; + font-weight: 300; + line-height: 1; + letter-spacing: -0.04em; + opacity: 0.54; } + +.mdl-typography--display-3 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 56px; + font-weight: 400; + line-height: 1.35; + letter-spacing: -0.02em; } + +.mdl-typography--display-3-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 56px; + font-weight: 400; + line-height: 1.35; + letter-spacing: -0.02em; + opacity: 0.54; } + +.mdl-typography--display-2 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 45px; + font-weight: 400; + line-height: 48px; } + +.mdl-typography--display-2-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 45px; + font-weight: 400; + line-height: 48px; + opacity: 0.54; } + +.mdl-typography--display-1 { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 34px; + font-weight: 400; + line-height: 40px; } + +.mdl-typography--display-1-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 34px; + font-weight: 400; + line-height: 40px; + opacity: 0.54; } + +.mdl-typography--headline { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 24px; + font-weight: 400; + line-height: 32px; + -moz-osx-font-smoothing: grayscale; } + +.mdl-typography--headline-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 24px; + font-weight: 400; + line-height: 32px; + -moz-osx-font-smoothing: grayscale; + opacity: 0.87; } + +.mdl-typography--title { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 20px; + font-weight: 500; + line-height: 1; + letter-spacing: 0.02em; } + +.mdl-typography--title-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 20px; + font-weight: 500; + line-height: 1; + letter-spacing: 0.02em; + opacity: 0.87; } + +.mdl-typography--subhead { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.04em; } + +.mdl-typography--subhead-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.04em; + opacity: 0.87; } + +.mdl-typography--body-2 { + font-size: 14px; + font-weight: bold; + line-height: 24px; + letter-spacing: 0; } + +.mdl-typography--body-2-color-contrast { + font-size: 14px; + font-weight: bold; + line-height: 24px; + letter-spacing: 0; + opacity: 0.87; } + +.mdl-typography--body-1 { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; } + +.mdl-typography--body-1-color-contrast { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + opacity: 0.87; } + +.mdl-typography--body-2-force-preferred-font { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0; } + +.mdl-typography--body-2-force-preferred-font-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + line-height: 24px; + letter-spacing: 0; + opacity: 0.87; } + +.mdl-typography--body-1-force-preferred-font { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; } + +.mdl-typography--body-1-force-preferred-font-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + opacity: 0.87; } + +.mdl-typography--caption { + font-size: 12px; + font-weight: 400; + line-height: 1; + letter-spacing: 0; } + +.mdl-typography--caption-force-preferred-font { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 12px; + font-weight: 400; + line-height: 1; + letter-spacing: 0; } + +.mdl-typography--caption-color-contrast { + font-size: 12px; + font-weight: 400; + line-height: 1; + letter-spacing: 0; + opacity: 0.54; } + +.mdl-typography--caption-force-preferred-font-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 12px; + font-weight: 400; + line-height: 1; + letter-spacing: 0; + opacity: 0.54; } + +.mdl-typography--menu { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + line-height: 1; + letter-spacing: 0; } + +.mdl-typography--menu-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + line-height: 1; + letter-spacing: 0; + opacity: 0.87; } + +.mdl-typography--button { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + text-transform: uppercase; + line-height: 1; + letter-spacing: 0; } + +.mdl-typography--button-color-contrast { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + text-transform: uppercase; + line-height: 1; + letter-spacing: 0; + opacity: 0.87; } + +.mdl-typography--text-left { + text-align: left; } + +.mdl-typography--text-right { + text-align: right; } + +.mdl-typography--text-center { + text-align: center; } + +.mdl-typography--text-justify { + text-align: justify; } + +.mdl-typography--text-nowrap { + white-space: nowrap; } + +.mdl-typography--text-lowercase { + text-transform: lowercase; } + +.mdl-typography--text-uppercase { + text-transform: uppercase; } + +.mdl-typography--text-capitalize { + text-transform: capitalize; } + +.mdl-typography--font-thin { + font-weight: 200 !important; } + +.mdl-typography--font-light { + font-weight: 300 !important; } + +.mdl-typography--font-regular { + font-weight: 400 !important; } + +.mdl-typography--font-medium { + font-weight: 500 !important; } + +.mdl-typography--font-bold { + font-weight: 700 !important; } + +.mdl-typography--font-black { + font-weight: 900 !important; } + +.material-icons { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + word-wrap: normal; + -moz-font-feature-settings: 'liga'; + font-feature-settings: 'liga'; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-color-text--red { + color: rgb(244,67,54) !important; } + +.mdl-color--red { + background-color: rgb(244,67,54) !important; } + +.mdl-color-text--red-50 { + color: rgb(255,235,238) !important; } + +.mdl-color--red-50 { + background-color: rgb(255,235,238) !important; } + +.mdl-color-text--red-100 { + color: rgb(255,205,210) !important; } + +.mdl-color--red-100 { + background-color: rgb(255,205,210) !important; } + +.mdl-color-text--red-200 { + color: rgb(239,154,154) !important; } + +.mdl-color--red-200 { + background-color: rgb(239,154,154) !important; } + +.mdl-color-text--red-300 { + color: rgb(229,115,115) !important; } + +.mdl-color--red-300 { + background-color: rgb(229,115,115) !important; } + +.mdl-color-text--red-400 { + color: rgb(239,83,80) !important; } + +.mdl-color--red-400 { + background-color: rgb(239,83,80) !important; } + +.mdl-color-text--red-500 { + color: rgb(244,67,54) !important; } + +.mdl-color--red-500 { + background-color: rgb(244,67,54) !important; } + +.mdl-color-text--red-600 { + color: rgb(229,57,53) !important; } + +.mdl-color--red-600 { + background-color: rgb(229,57,53) !important; } + +.mdl-color-text--red-700 { + color: rgb(211,47,47) !important; } + +.mdl-color--red-700 { + background-color: rgb(211,47,47) !important; } + +.mdl-color-text--red-800 { + color: rgb(198,40,40) !important; } + +.mdl-color--red-800 { + background-color: rgb(198,40,40) !important; } + +.mdl-color-text--red-900 { + color: rgb(183,28,28) !important; } + +.mdl-color--red-900 { + background-color: rgb(183,28,28) !important; } + +.mdl-color-text--red-A100 { + color: rgb(255,138,128) !important; } + +.mdl-color--red-A100 { + background-color: rgb(255,138,128) !important; } + +.mdl-color-text--red-A200 { + color: rgb(255,82,82) !important; } + +.mdl-color--red-A200 { + background-color: rgb(255,82,82) !important; } + +.mdl-color-text--red-A400 { + color: rgb(255,23,68) !important; } + +.mdl-color--red-A400 { + background-color: rgb(255,23,68) !important; } + +.mdl-color-text--red-A700 { + color: rgb(213,0,0) !important; } + +.mdl-color--red-A700 { + background-color: rgb(213,0,0) !important; } + +.mdl-color-text--pink { + color: rgb(233,30,99) !important; } + +.mdl-color--pink { + background-color: rgb(233,30,99) !important; } + +.mdl-color-text--pink-50 { + color: rgb(252,228,236) !important; } + +.mdl-color--pink-50 { + background-color: rgb(252,228,236) !important; } + +.mdl-color-text--pink-100 { + color: rgb(248,187,208) !important; } + +.mdl-color--pink-100 { + background-color: rgb(248,187,208) !important; } + +.mdl-color-text--pink-200 { + color: rgb(244,143,177) !important; } + +.mdl-color--pink-200 { + background-color: rgb(244,143,177) !important; } + +.mdl-color-text--pink-300 { + color: rgb(240,98,146) !important; } + +.mdl-color--pink-300 { + background-color: rgb(240,98,146) !important; } + +.mdl-color-text--pink-400 { + color: rgb(236,64,122) !important; } + +.mdl-color--pink-400 { + background-color: rgb(236,64,122) !important; } + +.mdl-color-text--pink-500 { + color: rgb(233,30,99) !important; } + +.mdl-color--pink-500 { + background-color: rgb(233,30,99) !important; } + +.mdl-color-text--pink-600 { + color: rgb(216,27,96) !important; } + +.mdl-color--pink-600 { + background-color: rgb(216,27,96) !important; } + +.mdl-color-text--pink-700 { + color: rgb(194,24,91) !important; } + +.mdl-color--pink-700 { + background-color: rgb(194,24,91) !important; } + +.mdl-color-text--pink-800 { + color: rgb(173,20,87) !important; } + +.mdl-color--pink-800 { + background-color: rgb(173,20,87) !important; } + +.mdl-color-text--pink-900 { + color: rgb(136,14,79) !important; } + +.mdl-color--pink-900 { + background-color: rgb(136,14,79) !important; } + +.mdl-color-text--pink-A100 { + color: rgb(255,128,171) !important; } + +.mdl-color--pink-A100 { + background-color: rgb(255,128,171) !important; } + +.mdl-color-text--pink-A200 { + color: rgb(255,64,129) !important; } + +.mdl-color--pink-A200 { + background-color: rgb(255,64,129) !important; } + +.mdl-color-text--pink-A400 { + color: rgb(245,0,87) !important; } + +.mdl-color--pink-A400 { + background-color: rgb(245,0,87) !important; } + +.mdl-color-text--pink-A700 { + color: rgb(197,17,98) !important; } + +.mdl-color--pink-A700 { + background-color: rgb(197,17,98) !important; } + +.mdl-color-text--purple { + color: rgb(156,39,176) !important; } + +.mdl-color--purple { + background-color: rgb(156,39,176) !important; } + +.mdl-color-text--purple-50 { + color: rgb(243,229,245) !important; } + +.mdl-color--purple-50 { + background-color: rgb(243,229,245) !important; } + +.mdl-color-text--purple-100 { + color: rgb(225,190,231) !important; } + +.mdl-color--purple-100 { + background-color: rgb(225,190,231) !important; } + +.mdl-color-text--purple-200 { + color: rgb(206,147,216) !important; } + +.mdl-color--purple-200 { + background-color: rgb(206,147,216) !important; } + +.mdl-color-text--purple-300 { + color: rgb(186,104,200) !important; } + +.mdl-color--purple-300 { + background-color: rgb(186,104,200) !important; } + +.mdl-color-text--purple-400 { + color: rgb(171,71,188) !important; } + +.mdl-color--purple-400 { + background-color: rgb(171,71,188) !important; } + +.mdl-color-text--purple-500 { + color: rgb(156,39,176) !important; } + +.mdl-color--purple-500 { + background-color: rgb(156,39,176) !important; } + +.mdl-color-text--purple-600 { + color: rgb(142,36,170) !important; } + +.mdl-color--purple-600 { + background-color: rgb(142,36,170) !important; } + +.mdl-color-text--purple-700 { + color: rgb(123,31,162) !important; } + +.mdl-color--purple-700 { + background-color: rgb(123,31,162) !important; } + +.mdl-color-text--purple-800 { + color: rgb(106,27,154) !important; } + +.mdl-color--purple-800 { + background-color: rgb(106,27,154) !important; } + +.mdl-color-text--purple-900 { + color: rgb(74,20,140) !important; } + +.mdl-color--purple-900 { + background-color: rgb(74,20,140) !important; } + +.mdl-color-text--purple-A100 { + color: rgb(234,128,252) !important; } + +.mdl-color--purple-A100 { + background-color: rgb(234,128,252) !important; } + +.mdl-color-text--purple-A200 { + color: rgb(224,64,251) !important; } + +.mdl-color--purple-A200 { + background-color: rgb(224,64,251) !important; } + +.mdl-color-text--purple-A400 { + color: rgb(213,0,249) !important; } + +.mdl-color--purple-A400 { + background-color: rgb(213,0,249) !important; } + +.mdl-color-text--purple-A700 { + color: rgb(170,0,255) !important; } + +.mdl-color--purple-A700 { + background-color: rgb(170,0,255) !important; } + +.mdl-color-text--deep-purple { + color: rgb(103,58,183) !important; } + +.mdl-color--deep-purple { + background-color: rgb(103,58,183) !important; } + +.mdl-color-text--deep-purple-50 { + color: rgb(237,231,246) !important; } + +.mdl-color--deep-purple-50 { + background-color: rgb(237,231,246) !important; } + +.mdl-color-text--deep-purple-100 { + color: rgb(209,196,233) !important; } + +.mdl-color--deep-purple-100 { + background-color: rgb(209,196,233) !important; } + +.mdl-color-text--deep-purple-200 { + color: rgb(179,157,219) !important; } + +.mdl-color--deep-purple-200 { + background-color: rgb(179,157,219) !important; } + +.mdl-color-text--deep-purple-300 { + color: rgb(149,117,205) !important; } + +.mdl-color--deep-purple-300 { + background-color: rgb(149,117,205) !important; } + +.mdl-color-text--deep-purple-400 { + color: rgb(126,87,194) !important; } + +.mdl-color--deep-purple-400 { + background-color: rgb(126,87,194) !important; } + +.mdl-color-text--deep-purple-500 { + color: rgb(103,58,183) !important; } + +.mdl-color--deep-purple-500 { + background-color: rgb(103,58,183) !important; } + +.mdl-color-text--deep-purple-600 { + color: rgb(94,53,177) !important; } + +.mdl-color--deep-purple-600 { + background-color: rgb(94,53,177) !important; } + +.mdl-color-text--deep-purple-700 { + color: rgb(81,45,168) !important; } + +.mdl-color--deep-purple-700 { + background-color: rgb(81,45,168) !important; } + +.mdl-color-text--deep-purple-800 { + color: rgb(69,39,160) !important; } + +.mdl-color--deep-purple-800 { + background-color: rgb(69,39,160) !important; } + +.mdl-color-text--deep-purple-900 { + color: rgb(49,27,146) !important; } + +.mdl-color--deep-purple-900 { + background-color: rgb(49,27,146) !important; } + +.mdl-color-text--deep-purple-A100 { + color: rgb(179,136,255) !important; } + +.mdl-color--deep-purple-A100 { + background-color: rgb(179,136,255) !important; } + +.mdl-color-text--deep-purple-A200 { + color: rgb(124,77,255) !important; } + +.mdl-color--deep-purple-A200 { + background-color: rgb(124,77,255) !important; } + +.mdl-color-text--deep-purple-A400 { + color: rgb(101,31,255) !important; } + +.mdl-color--deep-purple-A400 { + background-color: rgb(101,31,255) !important; } + +.mdl-color-text--deep-purple-A700 { + color: rgb(98,0,234) !important; } + +.mdl-color--deep-purple-A700 { + background-color: rgb(98,0,234) !important; } + +.mdl-color-text--indigo { + color: rgb(63,81,181) !important; } + +.mdl-color--indigo { + background-color: rgb(63,81,181) !important; } + +.mdl-color-text--indigo-50 { + color: rgb(232,234,246) !important; } + +.mdl-color--indigo-50 { + background-color: rgb(232,234,246) !important; } + +.mdl-color-text--indigo-100 { + color: rgb(197,202,233) !important; } + +.mdl-color--indigo-100 { + background-color: rgb(197,202,233) !important; } + +.mdl-color-text--indigo-200 { + color: rgb(159,168,218) !important; } + +.mdl-color--indigo-200 { + background-color: rgb(159,168,218) !important; } + +.mdl-color-text--indigo-300 { + color: rgb(121,134,203) !important; } + +.mdl-color--indigo-300 { + background-color: rgb(121,134,203) !important; } + +.mdl-color-text--indigo-400 { + color: rgb(92,107,192) !important; } + +.mdl-color--indigo-400 { + background-color: rgb(92,107,192) !important; } + +.mdl-color-text--indigo-500 { + color: rgb(63,81,181) !important; } + +.mdl-color--indigo-500 { + background-color: rgb(63,81,181) !important; } + +.mdl-color-text--indigo-600 { + color: rgb(57,73,171) !important; } + +.mdl-color--indigo-600 { + background-color: rgb(57,73,171) !important; } + +.mdl-color-text--indigo-700 { + color: rgb(48,63,159) !important; } + +.mdl-color--indigo-700 { + background-color: rgb(48,63,159) !important; } + +.mdl-color-text--indigo-800 { + color: rgb(40,53,147) !important; } + +.mdl-color--indigo-800 { + background-color: rgb(40,53,147) !important; } + +.mdl-color-text--indigo-900 { + color: rgb(26,35,126) !important; } + +.mdl-color--indigo-900 { + background-color: rgb(26,35,126) !important; } + +.mdl-color-text--indigo-A100 { + color: rgb(140,158,255) !important; } + +.mdl-color--indigo-A100 { + background-color: rgb(140,158,255) !important; } + +.mdl-color-text--indigo-A200 { + color: rgb(83,109,254) !important; } + +.mdl-color--indigo-A200 { + background-color: rgb(83,109,254) !important; } + +.mdl-color-text--indigo-A400 { + color: rgb(61,90,254) !important; } + +.mdl-color--indigo-A400 { + background-color: rgb(61,90,254) !important; } + +.mdl-color-text--indigo-A700 { + color: rgb(48,79,254) !important; } + +.mdl-color--indigo-A700 { + background-color: rgb(48,79,254) !important; } + +.mdl-color-text--blue { + color: rgb(33,150,243) !important; } + +.mdl-color--blue { + background-color: rgb(33,150,243) !important; } + +.mdl-color-text--blue-50 { + color: rgb(227,242,253) !important; } + +.mdl-color--blue-50 { + background-color: rgb(227,242,253) !important; } + +.mdl-color-text--blue-100 { + color: rgb(187,222,251) !important; } + +.mdl-color--blue-100 { + background-color: rgb(187,222,251) !important; } + +.mdl-color-text--blue-200 { + color: rgb(144,202,249) !important; } + +.mdl-color--blue-200 { + background-color: rgb(144,202,249) !important; } + +.mdl-color-text--blue-300 { + color: rgb(100,181,246) !important; } + +.mdl-color--blue-300 { + background-color: rgb(100,181,246) !important; } + +.mdl-color-text--blue-400 { + color: rgb(66,165,245) !important; } + +.mdl-color--blue-400 { + background-color: rgb(66,165,245) !important; } + +.mdl-color-text--blue-500 { + color: rgb(33,150,243) !important; } + +.mdl-color--blue-500 { + background-color: rgb(33,150,243) !important; } + +.mdl-color-text--blue-600 { + color: rgb(30,136,229) !important; } + +.mdl-color--blue-600 { + background-color: rgb(30,136,229) !important; } + +.mdl-color-text--blue-700 { + color: rgb(25,118,210) !important; } + +.mdl-color--blue-700 { + background-color: rgb(25,118,210) !important; } + +.mdl-color-text--blue-800 { + color: rgb(21,101,192) !important; } + +.mdl-color--blue-800 { + background-color: rgb(21,101,192) !important; } + +.mdl-color-text--blue-900 { + color: rgb(13,71,161) !important; } + +.mdl-color--blue-900 { + background-color: rgb(13,71,161) !important; } + +.mdl-color-text--blue-A100 { + color: rgb(130,177,255) !important; } + +.mdl-color--blue-A100 { + background-color: rgb(130,177,255) !important; } + +.mdl-color-text--blue-A200 { + color: rgb(68,138,255) !important; } + +.mdl-color--blue-A200 { + background-color: rgb(68,138,255) !important; } + +.mdl-color-text--blue-A400 { + color: rgb(41,121,255) !important; } + +.mdl-color--blue-A400 { + background-color: rgb(41,121,255) !important; } + +.mdl-color-text--blue-A700 { + color: rgb(41,98,255) !important; } + +.mdl-color--blue-A700 { + background-color: rgb(41,98,255) !important; } + +.mdl-color-text--light-blue { + color: rgb(3,169,244) !important; } + +.mdl-color--light-blue { + background-color: rgb(3,169,244) !important; } + +.mdl-color-text--light-blue-50 { + color: rgb(225,245,254) !important; } + +.mdl-color--light-blue-50 { + background-color: rgb(225,245,254) !important; } + +.mdl-color-text--light-blue-100 { + color: rgb(179,229,252) !important; } + +.mdl-color--light-blue-100 { + background-color: rgb(179,229,252) !important; } + +.mdl-color-text--light-blue-200 { + color: rgb(129,212,250) !important; } + +.mdl-color--light-blue-200 { + background-color: rgb(129,212,250) !important; } + +.mdl-color-text--light-blue-300 { + color: rgb(79,195,247) !important; } + +.mdl-color--light-blue-300 { + background-color: rgb(79,195,247) !important; } + +.mdl-color-text--light-blue-400 { + color: rgb(41,182,246) !important; } + +.mdl-color--light-blue-400 { + background-color: rgb(41,182,246) !important; } + +.mdl-color-text--light-blue-500 { + color: rgb(3,169,244) !important; } + +.mdl-color--light-blue-500 { + background-color: rgb(3,169,244) !important; } + +.mdl-color-text--light-blue-600 { + color: rgb(3,155,229) !important; } + +.mdl-color--light-blue-600 { + background-color: rgb(3,155,229) !important; } + +.mdl-color-text--light-blue-700 { + color: rgb(2,136,209) !important; } + +.mdl-color--light-blue-700 { + background-color: rgb(2,136,209) !important; } + +.mdl-color-text--light-blue-800 { + color: rgb(2,119,189) !important; } + +.mdl-color--light-blue-800 { + background-color: rgb(2,119,189) !important; } + +.mdl-color-text--light-blue-900 { + color: rgb(1,87,155) !important; } + +.mdl-color--light-blue-900 { + background-color: rgb(1,87,155) !important; } + +.mdl-color-text--light-blue-A100 { + color: rgb(128,216,255) !important; } + +.mdl-color--light-blue-A100 { + background-color: rgb(128,216,255) !important; } + +.mdl-color-text--light-blue-A200 { + color: rgb(64,196,255) !important; } + +.mdl-color--light-blue-A200 { + background-color: rgb(64,196,255) !important; } + +.mdl-color-text--light-blue-A400 { + color: rgb(0,176,255) !important; } + +.mdl-color--light-blue-A400 { + background-color: rgb(0,176,255) !important; } + +.mdl-color-text--light-blue-A700 { + color: rgb(0,145,234) !important; } + +.mdl-color--light-blue-A700 { + background-color: rgb(0,145,234) !important; } + +.mdl-color-text--cyan { + color: rgb(0,188,212) !important; } + +.mdl-color--cyan { + background-color: rgb(0,188,212) !important; } + +.mdl-color-text--cyan-50 { + color: rgb(224,247,250) !important; } + +.mdl-color--cyan-50 { + background-color: rgb(224,247,250) !important; } + +.mdl-color-text--cyan-100 { + color: rgb(178,235,242) !important; } + +.mdl-color--cyan-100 { + background-color: rgb(178,235,242) !important; } + +.mdl-color-text--cyan-200 { + color: rgb(128,222,234) !important; } + +.mdl-color--cyan-200 { + background-color: rgb(128,222,234) !important; } + +.mdl-color-text--cyan-300 { + color: rgb(77,208,225) !important; } + +.mdl-color--cyan-300 { + background-color: rgb(77,208,225) !important; } + +.mdl-color-text--cyan-400 { + color: rgb(38,198,218) !important; } + +.mdl-color--cyan-400 { + background-color: rgb(38,198,218) !important; } + +.mdl-color-text--cyan-500 { + color: rgb(0,188,212) !important; } + +.mdl-color--cyan-500 { + background-color: rgb(0,188,212) !important; } + +.mdl-color-text--cyan-600 { + color: rgb(0,172,193) !important; } + +.mdl-color--cyan-600 { + background-color: rgb(0,172,193) !important; } + +.mdl-color-text--cyan-700 { + color: rgb(0,151,167) !important; } + +.mdl-color--cyan-700 { + background-color: rgb(0,151,167) !important; } + +.mdl-color-text--cyan-800 { + color: rgb(0,131,143) !important; } + +.mdl-color--cyan-800 { + background-color: rgb(0,131,143) !important; } + +.mdl-color-text--cyan-900 { + color: rgb(0,96,100) !important; } + +.mdl-color--cyan-900 { + background-color: rgb(0,96,100) !important; } + +.mdl-color-text--cyan-A100 { + color: rgb(132,255,255) !important; } + +.mdl-color--cyan-A100 { + background-color: rgb(132,255,255) !important; } + +.mdl-color-text--cyan-A200 { + color: rgb(24,255,255) !important; } + +.mdl-color--cyan-A200 { + background-color: rgb(24,255,255) !important; } + +.mdl-color-text--cyan-A400 { + color: rgb(0,229,255) !important; } + +.mdl-color--cyan-A400 { + background-color: rgb(0,229,255) !important; } + +.mdl-color-text--cyan-A700 { + color: rgb(0,184,212) !important; } + +.mdl-color--cyan-A700 { + background-color: rgb(0,184,212) !important; } + +.mdl-color-text--teal { + color: rgb(0,150,136) !important; } + +.mdl-color--teal { + background-color: rgb(0,150,136) !important; } + +.mdl-color-text--teal-50 { + color: rgb(224,242,241) !important; } + +.mdl-color--teal-50 { + background-color: rgb(224,242,241) !important; } + +.mdl-color-text--teal-100 { + color: rgb(178,223,219) !important; } + +.mdl-color--teal-100 { + background-color: rgb(178,223,219) !important; } + +.mdl-color-text--teal-200 { + color: rgb(128,203,196) !important; } + +.mdl-color--teal-200 { + background-color: rgb(128,203,196) !important; } + +.mdl-color-text--teal-300 { + color: rgb(77,182,172) !important; } + +.mdl-color--teal-300 { + background-color: rgb(77,182,172) !important; } + +.mdl-color-text--teal-400 { + color: rgb(38,166,154) !important; } + +.mdl-color--teal-400 { + background-color: rgb(38,166,154) !important; } + +.mdl-color-text--teal-500 { + color: rgb(0,150,136) !important; } + +.mdl-color--teal-500 { + background-color: rgb(0,150,136) !important; } + +.mdl-color-text--teal-600 { + color: rgb(0,137,123) !important; } + +.mdl-color--teal-600 { + background-color: rgb(0,137,123) !important; } + +.mdl-color-text--teal-700 { + color: rgb(0,121,107) !important; } + +.mdl-color--teal-700 { + background-color: rgb(0,121,107) !important; } + +.mdl-color-text--teal-800 { + color: rgb(0,105,92) !important; } + +.mdl-color--teal-800 { + background-color: rgb(0,105,92) !important; } + +.mdl-color-text--teal-900 { + color: rgb(0,77,64) !important; } + +.mdl-color--teal-900 { + background-color: rgb(0,77,64) !important; } + +.mdl-color-text--teal-A100 { + color: rgb(167,255,235) !important; } + +.mdl-color--teal-A100 { + background-color: rgb(167,255,235) !important; } + +.mdl-color-text--teal-A200 { + color: rgb(100,255,218) !important; } + +.mdl-color--teal-A200 { + background-color: rgb(100,255,218) !important; } + +.mdl-color-text--teal-A400 { + color: rgb(29,233,182) !important; } + +.mdl-color--teal-A400 { + background-color: rgb(29,233,182) !important; } + +.mdl-color-text--teal-A700 { + color: rgb(0,191,165) !important; } + +.mdl-color--teal-A700 { + background-color: rgb(0,191,165) !important; } + +.mdl-color-text--green { + color: rgb(76,175,80) !important; } + +.mdl-color--green { + background-color: rgb(76,175,80) !important; } + +.mdl-color-text--green-50 { + color: rgb(232,245,233) !important; } + +.mdl-color--green-50 { + background-color: rgb(232,245,233) !important; } + +.mdl-color-text--green-100 { + color: rgb(200,230,201) !important; } + +.mdl-color--green-100 { + background-color: rgb(200,230,201) !important; } + +.mdl-color-text--green-200 { + color: rgb(165,214,167) !important; } + +.mdl-color--green-200 { + background-color: rgb(165,214,167) !important; } + +.mdl-color-text--green-300 { + color: rgb(129,199,132) !important; } + +.mdl-color--green-300 { + background-color: rgb(129,199,132) !important; } + +.mdl-color-text--green-400 { + color: rgb(102,187,106) !important; } + +.mdl-color--green-400 { + background-color: rgb(102,187,106) !important; } + +.mdl-color-text--green-500 { + color: rgb(76,175,80) !important; } + +.mdl-color--green-500 { + background-color: rgb(76,175,80) !important; } + +.mdl-color-text--green-600 { + color: rgb(67,160,71) !important; } + +.mdl-color--green-600 { + background-color: rgb(67,160,71) !important; } + +.mdl-color-text--green-700 { + color: rgb(56,142,60) !important; } + +.mdl-color--green-700 { + background-color: rgb(56,142,60) !important; } + +.mdl-color-text--green-800 { + color: rgb(46,125,50) !important; } + +.mdl-color--green-800 { + background-color: rgb(46,125,50) !important; } + +.mdl-color-text--green-900 { + color: rgb(27,94,32) !important; } + +.mdl-color--green-900 { + background-color: rgb(27,94,32) !important; } + +.mdl-color-text--green-A100 { + color: rgb(185,246,202) !important; } + +.mdl-color--green-A100 { + background-color: rgb(185,246,202) !important; } + +.mdl-color-text--green-A200 { + color: rgb(105,240,174) !important; } + +.mdl-color--green-A200 { + background-color: rgb(105,240,174) !important; } + +.mdl-color-text--green-A400 { + color: rgb(0,230,118) !important; } + +.mdl-color--green-A400 { + background-color: rgb(0,230,118) !important; } + +.mdl-color-text--green-A700 { + color: rgb(0,200,83) !important; } + +.mdl-color--green-A700 { + background-color: rgb(0,200,83) !important; } + +.mdl-color-text--light-green { + color: rgb(139,195,74) !important; } + +.mdl-color--light-green { + background-color: rgb(139,195,74) !important; } + +.mdl-color-text--light-green-50 { + color: rgb(241,248,233) !important; } + +.mdl-color--light-green-50 { + background-color: rgb(241,248,233) !important; } + +.mdl-color-text--light-green-100 { + color: rgb(220,237,200) !important; } + +.mdl-color--light-green-100 { + background-color: rgb(220,237,200) !important; } + +.mdl-color-text--light-green-200 { + color: rgb(197,225,165) !important; } + +.mdl-color--light-green-200 { + background-color: rgb(197,225,165) !important; } + +.mdl-color-text--light-green-300 { + color: rgb(174,213,129) !important; } + +.mdl-color--light-green-300 { + background-color: rgb(174,213,129) !important; } + +.mdl-color-text--light-green-400 { + color: rgb(156,204,101) !important; } + +.mdl-color--light-green-400 { + background-color: rgb(156,204,101) !important; } + +.mdl-color-text--light-green-500 { + color: rgb(139,195,74) !important; } + +.mdl-color--light-green-500 { + background-color: rgb(139,195,74) !important; } + +.mdl-color-text--light-green-600 { + color: rgb(124,179,66) !important; } + +.mdl-color--light-green-600 { + background-color: rgb(124,179,66) !important; } + +.mdl-color-text--light-green-700 { + color: rgb(104,159,56) !important; } + +.mdl-color--light-green-700 { + background-color: rgb(104,159,56) !important; } + +.mdl-color-text--light-green-800 { + color: rgb(85,139,47) !important; } + +.mdl-color--light-green-800 { + background-color: rgb(85,139,47) !important; } + +.mdl-color-text--light-green-900 { + color: rgb(51,105,30) !important; } + +.mdl-color--light-green-900 { + background-color: rgb(51,105,30) !important; } + +.mdl-color-text--light-green-A100 { + color: rgb(204,255,144) !important; } + +.mdl-color--light-green-A100 { + background-color: rgb(204,255,144) !important; } + +.mdl-color-text--light-green-A200 { + color: rgb(178,255,89) !important; } + +.mdl-color--light-green-A200 { + background-color: rgb(178,255,89) !important; } + +.mdl-color-text--light-green-A400 { + color: rgb(118,255,3) !important; } + +.mdl-color--light-green-A400 { + background-color: rgb(118,255,3) !important; } + +.mdl-color-text--light-green-A700 { + color: rgb(100,221,23) !important; } + +.mdl-color--light-green-A700 { + background-color: rgb(100,221,23) !important; } + +.mdl-color-text--lime { + color: rgb(205,220,57) !important; } + +.mdl-color--lime { + background-color: rgb(205,220,57) !important; } + +.mdl-color-text--lime-50 { + color: rgb(249,251,231) !important; } + +.mdl-color--lime-50 { + background-color: rgb(249,251,231) !important; } + +.mdl-color-text--lime-100 { + color: rgb(240,244,195) !important; } + +.mdl-color--lime-100 { + background-color: rgb(240,244,195) !important; } + +.mdl-color-text--lime-200 { + color: rgb(230,238,156) !important; } + +.mdl-color--lime-200 { + background-color: rgb(230,238,156) !important; } + +.mdl-color-text--lime-300 { + color: rgb(220,231,117) !important; } + +.mdl-color--lime-300 { + background-color: rgb(220,231,117) !important; } + +.mdl-color-text--lime-400 { + color: rgb(212,225,87) !important; } + +.mdl-color--lime-400 { + background-color: rgb(212,225,87) !important; } + +.mdl-color-text--lime-500 { + color: rgb(205,220,57) !important; } + +.mdl-color--lime-500 { + background-color: rgb(205,220,57) !important; } + +.mdl-color-text--lime-600 { + color: rgb(192,202,51) !important; } + +.mdl-color--lime-600 { + background-color: rgb(192,202,51) !important; } + +.mdl-color-text--lime-700 { + color: rgb(175,180,43) !important; } + +.mdl-color--lime-700 { + background-color: rgb(175,180,43) !important; } + +.mdl-color-text--lime-800 { + color: rgb(158,157,36) !important; } + +.mdl-color--lime-800 { + background-color: rgb(158,157,36) !important; } + +.mdl-color-text--lime-900 { + color: rgb(130,119,23) !important; } + +.mdl-color--lime-900 { + background-color: rgb(130,119,23) !important; } + +.mdl-color-text--lime-A100 { + color: rgb(244,255,129) !important; } + +.mdl-color--lime-A100 { + background-color: rgb(244,255,129) !important; } + +.mdl-color-text--lime-A200 { + color: rgb(238,255,65) !important; } + +.mdl-color--lime-A200 { + background-color: rgb(238,255,65) !important; } + +.mdl-color-text--lime-A400 { + color: rgb(198,255,0) !important; } + +.mdl-color--lime-A400 { + background-color: rgb(198,255,0) !important; } + +.mdl-color-text--lime-A700 { + color: rgb(174,234,0) !important; } + +.mdl-color--lime-A700 { + background-color: rgb(174,234,0) !important; } + +.mdl-color-text--yellow { + color: rgb(255,235,59) !important; } + +.mdl-color--yellow { + background-color: rgb(255,235,59) !important; } + +.mdl-color-text--yellow-50 { + color: rgb(255,253,231) !important; } + +.mdl-color--yellow-50 { + background-color: rgb(255,253,231) !important; } + +.mdl-color-text--yellow-100 { + color: rgb(255,249,196) !important; } + +.mdl-color--yellow-100 { + background-color: rgb(255,249,196) !important; } + +.mdl-color-text--yellow-200 { + color: rgb(255,245,157) !important; } + +.mdl-color--yellow-200 { + background-color: rgb(255,245,157) !important; } + +.mdl-color-text--yellow-300 { + color: rgb(255,241,118) !important; } + +.mdl-color--yellow-300 { + background-color: rgb(255,241,118) !important; } + +.mdl-color-text--yellow-400 { + color: rgb(255,238,88) !important; } + +.mdl-color--yellow-400 { + background-color: rgb(255,238,88) !important; } + +.mdl-color-text--yellow-500 { + color: rgb(255,235,59) !important; } + +.mdl-color--yellow-500 { + background-color: rgb(255,235,59) !important; } + +.mdl-color-text--yellow-600 { + color: rgb(253,216,53) !important; } + +.mdl-color--yellow-600 { + background-color: rgb(253,216,53) !important; } + +.mdl-color-text--yellow-700 { + color: rgb(251,192,45) !important; } + +.mdl-color--yellow-700 { + background-color: rgb(251,192,45) !important; } + +.mdl-color-text--yellow-800 { + color: rgb(249,168,37) !important; } + +.mdl-color--yellow-800 { + background-color: rgb(249,168,37) !important; } + +.mdl-color-text--yellow-900 { + color: rgb(245,127,23) !important; } + +.mdl-color--yellow-900 { + background-color: rgb(245,127,23) !important; } + +.mdl-color-text--yellow-A100 { + color: rgb(255,255,141) !important; } + +.mdl-color--yellow-A100 { + background-color: rgb(255,255,141) !important; } + +.mdl-color-text--yellow-A200 { + color: rgb(255,255,0) !important; } + +.mdl-color--yellow-A200 { + background-color: rgb(255,255,0) !important; } + +.mdl-color-text--yellow-A400 { + color: rgb(255,234,0) !important; } + +.mdl-color--yellow-A400 { + background-color: rgb(255,234,0) !important; } + +.mdl-color-text--yellow-A700 { + color: rgb(255,214,0) !important; } + +.mdl-color--yellow-A700 { + background-color: rgb(255,214,0) !important; } + +.mdl-color-text--amber { + color: rgb(255,193,7) !important; } + +.mdl-color--amber { + background-color: rgb(255,193,7) !important; } + +.mdl-color-text--amber-50 { + color: rgb(255,248,225) !important; } + +.mdl-color--amber-50 { + background-color: rgb(255,248,225) !important; } + +.mdl-color-text--amber-100 { + color: rgb(255,236,179) !important; } + +.mdl-color--amber-100 { + background-color: rgb(255,236,179) !important; } + +.mdl-color-text--amber-200 { + color: rgb(255,224,130) !important; } + +.mdl-color--amber-200 { + background-color: rgb(255,224,130) !important; } + +.mdl-color-text--amber-300 { + color: rgb(255,213,79) !important; } + +.mdl-color--amber-300 { + background-color: rgb(255,213,79) !important; } + +.mdl-color-text--amber-400 { + color: rgb(255,202,40) !important; } + +.mdl-color--amber-400 { + background-color: rgb(255,202,40) !important; } + +.mdl-color-text--amber-500 { + color: rgb(255,193,7) !important; } + +.mdl-color--amber-500 { + background-color: rgb(255,193,7) !important; } + +.mdl-color-text--amber-600 { + color: rgb(255,179,0) !important; } + +.mdl-color--amber-600 { + background-color: rgb(255,179,0) !important; } + +.mdl-color-text--amber-700 { + color: rgb(255,160,0) !important; } + +.mdl-color--amber-700 { + background-color: rgb(255,160,0) !important; } + +.mdl-color-text--amber-800 { + color: rgb(255,143,0) !important; } + +.mdl-color--amber-800 { + background-color: rgb(255,143,0) !important; } + +.mdl-color-text--amber-900 { + color: rgb(255,111,0) !important; } + +.mdl-color--amber-900 { + background-color: rgb(255,111,0) !important; } + +.mdl-color-text--amber-A100 { + color: rgb(255,229,127) !important; } + +.mdl-color--amber-A100 { + background-color: rgb(255,229,127) !important; } + +.mdl-color-text--amber-A200 { + color: rgb(255,215,64) !important; } + +.mdl-color--amber-A200 { + background-color: rgb(255,215,64) !important; } + +.mdl-color-text--amber-A400 { + color: rgb(255,196,0) !important; } + +.mdl-color--amber-A400 { + background-color: rgb(255,196,0) !important; } + +.mdl-color-text--amber-A700 { + color: rgb(255,171,0) !important; } + +.mdl-color--amber-A700 { + background-color: rgb(255,171,0) !important; } + +.mdl-color-text--orange { + color: rgb(255,152,0) !important; } + +.mdl-color--orange { + background-color: rgb(255,152,0) !important; } + +.mdl-color-text--orange-50 { + color: rgb(255,243,224) !important; } + +.mdl-color--orange-50 { + background-color: rgb(255,243,224) !important; } + +.mdl-color-text--orange-100 { + color: rgb(255,224,178) !important; } + +.mdl-color--orange-100 { + background-color: rgb(255,224,178) !important; } + +.mdl-color-text--orange-200 { + color: rgb(255,204,128) !important; } + +.mdl-color--orange-200 { + background-color: rgb(255,204,128) !important; } + +.mdl-color-text--orange-300 { + color: rgb(255,183,77) !important; } + +.mdl-color--orange-300 { + background-color: rgb(255,183,77) !important; } + +.mdl-color-text--orange-400 { + color: rgb(255,167,38) !important; } + +.mdl-color--orange-400 { + background-color: rgb(255,167,38) !important; } + +.mdl-color-text--orange-500 { + color: rgb(255,152,0) !important; } + +.mdl-color--orange-500 { + background-color: rgb(255,152,0) !important; } + +.mdl-color-text--orange-600 { + color: rgb(251,140,0) !important; } + +.mdl-color--orange-600 { + background-color: rgb(251,140,0) !important; } + +.mdl-color-text--orange-700 { + color: rgb(245,124,0) !important; } + +.mdl-color--orange-700 { + background-color: rgb(245,124,0) !important; } + +.mdl-color-text--orange-800 { + color: rgb(239,108,0) !important; } + +.mdl-color--orange-800 { + background-color: rgb(239,108,0) !important; } + +.mdl-color-text--orange-900 { + color: rgb(230,81,0) !important; } + +.mdl-color--orange-900 { + background-color: rgb(230,81,0) !important; } + +.mdl-color-text--orange-A100 { + color: rgb(255,209,128) !important; } + +.mdl-color--orange-A100 { + background-color: rgb(255,209,128) !important; } + +.mdl-color-text--orange-A200 { + color: rgb(255,171,64) !important; } + +.mdl-color--orange-A200 { + background-color: rgb(255,171,64) !important; } + +.mdl-color-text--orange-A400 { + color: rgb(255,145,0) !important; } + +.mdl-color--orange-A400 { + background-color: rgb(255,145,0) !important; } + +.mdl-color-text--orange-A700 { + color: rgb(255,109,0) !important; } + +.mdl-color--orange-A700 { + background-color: rgb(255,109,0) !important; } + +.mdl-color-text--deep-orange { + color: rgb(255,87,34) !important; } + +.mdl-color--deep-orange { + background-color: rgb(255,87,34) !important; } + +.mdl-color-text--deep-orange-50 { + color: rgb(251,233,231) !important; } + +.mdl-color--deep-orange-50 { + background-color: rgb(251,233,231) !important; } + +.mdl-color-text--deep-orange-100 { + color: rgb(255,204,188) !important; } + +.mdl-color--deep-orange-100 { + background-color: rgb(255,204,188) !important; } + +.mdl-color-text--deep-orange-200 { + color: rgb(255,171,145) !important; } + +.mdl-color--deep-orange-200 { + background-color: rgb(255,171,145) !important; } + +.mdl-color-text--deep-orange-300 { + color: rgb(255,138,101) !important; } + +.mdl-color--deep-orange-300 { + background-color: rgb(255,138,101) !important; } + +.mdl-color-text--deep-orange-400 { + color: rgb(255,112,67) !important; } + +.mdl-color--deep-orange-400 { + background-color: rgb(255,112,67) !important; } + +.mdl-color-text--deep-orange-500 { + color: rgb(255,87,34) !important; } + +.mdl-color--deep-orange-500 { + background-color: rgb(255,87,34) !important; } + +.mdl-color-text--deep-orange-600 { + color: rgb(244,81,30) !important; } + +.mdl-color--deep-orange-600 { + background-color: rgb(244,81,30) !important; } + +.mdl-color-text--deep-orange-700 { + color: rgb(230,74,25) !important; } + +.mdl-color--deep-orange-700 { + background-color: rgb(230,74,25) !important; } + +.mdl-color-text--deep-orange-800 { + color: rgb(216,67,21) !important; } + +.mdl-color--deep-orange-800 { + background-color: rgb(216,67,21) !important; } + +.mdl-color-text--deep-orange-900 { + color: rgb(191,54,12) !important; } + +.mdl-color--deep-orange-900 { + background-color: rgb(191,54,12) !important; } + +.mdl-color-text--deep-orange-A100 { + color: rgb(255,158,128) !important; } + +.mdl-color--deep-orange-A100 { + background-color: rgb(255,158,128) !important; } + +.mdl-color-text--deep-orange-A200 { + color: rgb(255,110,64) !important; } + +.mdl-color--deep-orange-A200 { + background-color: rgb(255,110,64) !important; } + +.mdl-color-text--deep-orange-A400 { + color: rgb(255,61,0) !important; } + +.mdl-color--deep-orange-A400 { + background-color: rgb(255,61,0) !important; } + +.mdl-color-text--deep-orange-A700 { + color: rgb(221,44,0) !important; } + +.mdl-color--deep-orange-A700 { + background-color: rgb(221,44,0) !important; } + +.mdl-color-text--brown { + color: rgb(121,85,72) !important; } + +.mdl-color--brown { + background-color: rgb(121,85,72) !important; } + +.mdl-color-text--brown-50 { + color: rgb(239,235,233) !important; } + +.mdl-color--brown-50 { + background-color: rgb(239,235,233) !important; } + +.mdl-color-text--brown-100 { + color: rgb(215,204,200) !important; } + +.mdl-color--brown-100 { + background-color: rgb(215,204,200) !important; } + +.mdl-color-text--brown-200 { + color: rgb(188,170,164) !important; } + +.mdl-color--brown-200 { + background-color: rgb(188,170,164) !important; } + +.mdl-color-text--brown-300 { + color: rgb(161,136,127) !important; } + +.mdl-color--brown-300 { + background-color: rgb(161,136,127) !important; } + +.mdl-color-text--brown-400 { + color: rgb(141,110,99) !important; } + +.mdl-color--brown-400 { + background-color: rgb(141,110,99) !important; } + +.mdl-color-text--brown-500 { + color: rgb(121,85,72) !important; } + +.mdl-color--brown-500 { + background-color: rgb(121,85,72) !important; } + +.mdl-color-text--brown-600 { + color: rgb(109,76,65) !important; } + +.mdl-color--brown-600 { + background-color: rgb(109,76,65) !important; } + +.mdl-color-text--brown-700 { + color: rgb(93,64,55) !important; } + +.mdl-color--brown-700 { + background-color: rgb(93,64,55) !important; } + +.mdl-color-text--brown-800 { + color: rgb(78,52,46) !important; } + +.mdl-color--brown-800 { + background-color: rgb(78,52,46) !important; } + +.mdl-color-text--brown-900 { + color: rgb(62,39,35) !important; } + +.mdl-color--brown-900 { + background-color: rgb(62,39,35) !important; } + +.mdl-color-text--grey { + color: rgb(158,158,158) !important; } + +.mdl-color--grey { + background-color: rgb(158,158,158) !important; } + +.mdl-color-text--grey-50 { + color: rgb(250,250,250) !important; } + +.mdl-color--grey-50 { + background-color: rgb(250,250,250) !important; } + +.mdl-color-text--grey-100 { + color: rgb(245,245,245) !important; } + +.mdl-color--grey-100 { + background-color: rgb(245,245,245) !important; } + +.mdl-color-text--grey-200 { + color: rgb(238,238,238) !important; } + +.mdl-color--grey-200 { + background-color: rgb(238,238,238) !important; } + +.mdl-color-text--grey-300 { + color: rgb(224,224,224) !important; } + +.mdl-color--grey-300 { + background-color: rgb(224,224,224) !important; } + +.mdl-color-text--grey-400 { + color: rgb(189,189,189) !important; } + +.mdl-color--grey-400 { + background-color: rgb(189,189,189) !important; } + +.mdl-color-text--grey-500 { + color: rgb(158,158,158) !important; } + +.mdl-color--grey-500 { + background-color: rgb(158,158,158) !important; } + +.mdl-color-text--grey-600 { + color: rgb(117,117,117) !important; } + +.mdl-color--grey-600 { + background-color: rgb(117,117,117) !important; } + +.mdl-color-text--grey-700 { + color: rgb(97,97,97) !important; } + +.mdl-color--grey-700 { + background-color: rgb(97,97,97) !important; } + +.mdl-color-text--grey-800 { + color: rgb(66,66,66) !important; } + +.mdl-color--grey-800 { + background-color: rgb(66,66,66) !important; } + +.mdl-color-text--grey-900 { + color: rgb(33,33,33) !important; } + +.mdl-color--grey-900 { + background-color: rgb(33,33,33) !important; } + +.mdl-color-text--blue-grey { + color: rgb(96,125,139) !important; } + +.mdl-color--blue-grey { + background-color: rgb(96,125,139) !important; } + +.mdl-color-text--blue-grey-50 { + color: rgb(236,239,241) !important; } + +.mdl-color--blue-grey-50 { + background-color: rgb(236,239,241) !important; } + +.mdl-color-text--blue-grey-100 { + color: rgb(207,216,220) !important; } + +.mdl-color--blue-grey-100 { + background-color: rgb(207,216,220) !important; } + +.mdl-color-text--blue-grey-200 { + color: rgb(176,190,197) !important; } + +.mdl-color--blue-grey-200 { + background-color: rgb(176,190,197) !important; } + +.mdl-color-text--blue-grey-300 { + color: rgb(144,164,174) !important; } + +.mdl-color--blue-grey-300 { + background-color: rgb(144,164,174) !important; } + +.mdl-color-text--blue-grey-400 { + color: rgb(120,144,156) !important; } + +.mdl-color--blue-grey-400 { + background-color: rgb(120,144,156) !important; } + +.mdl-color-text--blue-grey-500 { + color: rgb(96,125,139) !important; } + +.mdl-color--blue-grey-500 { + background-color: rgb(96,125,139) !important; } + +.mdl-color-text--blue-grey-600 { + color: rgb(84,110,122) !important; } + +.mdl-color--blue-grey-600 { + background-color: rgb(84,110,122) !important; } + +.mdl-color-text--blue-grey-700 { + color: rgb(69,90,100) !important; } + +.mdl-color--blue-grey-700 { + background-color: rgb(69,90,100) !important; } + +.mdl-color-text--blue-grey-800 { + color: rgb(55,71,79) !important; } + +.mdl-color--blue-grey-800 { + background-color: rgb(55,71,79) !important; } + +.mdl-color-text--blue-grey-900 { + color: rgb(38,50,56) !important; } + +.mdl-color--blue-grey-900 { + background-color: rgb(38,50,56) !important; } + +.mdl-color--black { + background-color: rgb(0,0,0) !important; } + +.mdl-color-text--black { + color: rgb(0,0,0) !important; } + +.mdl-color--white { + background-color: rgb(255,255,255) !important; } + +.mdl-color-text--white { + color: rgb(255,255,255) !important; } + +.mdl-color--primary { + background-color: rgb(63,81,181) !important; } + +.mdl-color--primary-contrast { + background-color: rgb(255,255,255) !important; } + +.mdl-color--primary-dark { + background-color: rgb(48,63,159) !important; } + +.mdl-color--accent { + background-color: rgb(255,64,129) !important; } + +.mdl-color--accent-contrast { + background-color: rgb(255,255,255) !important; } + +.mdl-color-text--primary { + color: rgb(63,81,181) !important; } + +.mdl-color-text--primary-contrast { + color: rgb(255,255,255) !important; } + +.mdl-color-text--primary-dark { + color: rgb(48,63,159) !important; } + +.mdl-color-text--accent { + color: rgb(255,64,129) !important; } + +.mdl-color-text--accent-contrast { + color: rgb(255,255,255) !important; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-ripple { + background: rgb(0,0,0); + border-radius: 50%; + height: 50px; + left: 0; + opacity: 0; + pointer-events: none; + position: absolute; + top: 0; + -webkit-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); + width: 50px; + overflow: hidden; } + .mdl-ripple.is-animating { + transition: width 0.3s cubic-bezier(0, 0, 0.2, 1), height 0.3s cubic-bezier(0, 0, 0.2, 1), opacity 0.6s cubic-bezier(0, 0, 0.2, 1), -webkit-transform 0.3s cubic-bezier(0, 0, 0.2, 1); + transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1), width 0.3s cubic-bezier(0, 0, 0.2, 1), height 0.3s cubic-bezier(0, 0, 0.2, 1), opacity 0.6s cubic-bezier(0, 0, 0.2, 1); + transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1), width 0.3s cubic-bezier(0, 0, 0.2, 1), height 0.3s cubic-bezier(0, 0, 0.2, 1), opacity 0.6s cubic-bezier(0, 0, 0.2, 1), -webkit-transform 0.3s cubic-bezier(0, 0, 0.2, 1); } + .mdl-ripple.is-visible { + opacity: 0.3; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-animation--default { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } + +.mdl-animation--fast-out-slow-in { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } + +.mdl-animation--linear-out-slow-in { + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } + +.mdl-animation--fast-out-linear-in { + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-badge { + position: relative; + white-space: nowrap; + margin-right: 24px; } + .mdl-badge:not([data-badge]) { + margin-right: auto; } + .mdl-badge[data-badge]:after { + content: attr(data-badge); + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-content: center; + -ms-flex-line-pack: center; + align-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + position: absolute; + top: -11px; + right: -24px; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-weight: 600; + font-size: 12px; + width: 22px; + height: 22px; + border-radius: 50%; + background: rgb(255,64,129); + color: rgb(255,255,255); } + .mdl-button .mdl-badge[data-badge]:after { + top: -10px; + right: -5px; } + .mdl-badge.mdl-badge--no-background[data-badge]:after { + color: rgb(255,64,129); + background: rgba(255,255,255,0.2); + box-shadow: 0 0 1px gray; } + .mdl-badge.mdl-badge--overlap { + margin-right: 10px; } + .mdl-badge.mdl-badge--overlap:after { + right: -10px; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-button { + background: transparent; + border: none; + border-radius: 2px; + color: rgb(0,0,0); + position: relative; + height: 36px; + margin: 0; + min-width: 64px; + padding: 0 16px; + display: inline-block; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + text-transform: uppercase; + line-height: 1; + letter-spacing: 0; + overflow: hidden; + will-change: box-shadow; + transition: box-shadow 0.2s cubic-bezier(0.4, 0, 1, 1), background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), color 0.2s cubic-bezier(0.4, 0, 0.2, 1); + outline: none; + cursor: pointer; + text-decoration: none; + text-align: center; + line-height: 36px; + vertical-align: middle; } + .mdl-button::-moz-focus-inner { + border: 0; } + .mdl-button:hover { + background-color: rgba(158,158,158, 0.20); } + .mdl-button:focus:not(:active) { + background-color: rgba(0,0,0, 0.12); } + .mdl-button:active { + background-color: rgba(158,158,158, 0.40); } + .mdl-button.mdl-button--colored { + color: rgb(63,81,181); } + .mdl-button.mdl-button--colored:focus:not(:active) { + background-color: rgba(0,0,0, 0.12); } + +input.mdl-button[type="submit"] { + -webkit-appearance: none; } + +.mdl-button--raised { + background: rgba(158,158,158, 0.20); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); } + .mdl-button--raised:active { + box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.2); + background-color: rgba(158,158,158, 0.40); } + .mdl-button--raised:focus:not(:active) { + box-shadow: 0 0 8px rgba(0, 0, 0, 0.18), 0 8px 16px rgba(0, 0, 0, 0.36); + background-color: rgba(158,158,158, 0.40); } + .mdl-button--raised.mdl-button--colored { + background: rgb(63,81,181); + color: rgb(255,255,255); } + .mdl-button--raised.mdl-button--colored:hover { + background-color: rgb(63,81,181); } + .mdl-button--raised.mdl-button--colored:active { + background-color: rgb(63,81,181); } + .mdl-button--raised.mdl-button--colored:focus:not(:active) { + background-color: rgb(63,81,181); } + .mdl-button--raised.mdl-button--colored .mdl-ripple { + background: rgb(255,255,255); } + +.mdl-button--fab { + border-radius: 50%; + font-size: 24px; + height: 56px; + margin: auto; + min-width: 56px; + width: 56px; + padding: 0; + overflow: hidden; + background: rgba(158,158,158, 0.20); + box-shadow: 0 1px 1.5px 0 rgba(0, 0, 0, 0.12), 0 1px 1px 0 rgba(0, 0, 0, 0.24); + position: relative; + line-height: normal; } + .mdl-button--fab .material-icons { + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-12px, -12px); + transform: translate(-12px, -12px); + line-height: 24px; + width: 24px; } + .mdl-button--fab.mdl-button--mini-fab { + height: 40px; + min-width: 40px; + width: 40px; } + .mdl-button--fab .mdl-button__ripple-container { + border-radius: 50%; + -webkit-mask-image: -webkit-radial-gradient(circle, white, black); } + .mdl-button--fab:active { + box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.2); + background-color: rgba(158,158,158, 0.40); } + .mdl-button--fab:focus:not(:active) { + box-shadow: 0 0 8px rgba(0, 0, 0, 0.18), 0 8px 16px rgba(0, 0, 0, 0.36); + background-color: rgba(158,158,158, 0.40); } + .mdl-button--fab.mdl-button--colored { + background: rgb(255,64,129); + color: rgb(255,255,255); } + .mdl-button--fab.mdl-button--colored:hover { + background-color: rgb(255,64,129); } + .mdl-button--fab.mdl-button--colored:focus:not(:active) { + background-color: rgb(255,64,129); } + .mdl-button--fab.mdl-button--colored:active { + background-color: rgb(255,64,129); } + .mdl-button--fab.mdl-button--colored .mdl-ripple { + background: rgb(255,255,255); } + +.mdl-button--icon { + border-radius: 50%; + font-size: 24px; + height: 32px; + margin-left: 0; + margin-right: 0; + min-width: 32px; + width: 32px; + padding: 0; + overflow: hidden; + color: inherit; + line-height: normal; } + .mdl-button--icon .material-icons { + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-12px, -12px); + transform: translate(-12px, -12px); + line-height: 24px; + width: 24px; } + .mdl-button--icon.mdl-button--mini-icon { + height: 24px; + min-width: 24px; + width: 24px; } + .mdl-button--icon.mdl-button--mini-icon .material-icons { + top: 0px; + left: 0px; } + .mdl-button--icon .mdl-button__ripple-container { + border-radius: 50%; + -webkit-mask-image: -webkit-radial-gradient(circle, white, black); } + +.mdl-button__ripple-container { + display: block; + height: 100%; + left: 0px; + position: absolute; + top: 0px; + width: 100%; + z-index: 0; + overflow: hidden; } + .mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple, + .mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple { + background-color: transparent; } + +.mdl-button--primary.mdl-button--primary { + color: rgb(63,81,181); } + .mdl-button--primary.mdl-button--primary .mdl-ripple { + background: rgb(255,255,255); } + .mdl-button--primary.mdl-button--primary.mdl-button--raised, .mdl-button--primary.mdl-button--primary.mdl-button--fab { + color: rgb(255,255,255); + background-color: rgb(63,81,181); } + +.mdl-button--accent.mdl-button--accent { + color: rgb(255,64,129); } + .mdl-button--accent.mdl-button--accent .mdl-ripple { + background: rgb(255,255,255); } + .mdl-button--accent.mdl-button--accent.mdl-button--raised, .mdl-button--accent.mdl-button--accent.mdl-button--fab { + color: rgb(255,255,255); + background-color: rgb(255,64,129); } + +.mdl-button[disabled][disabled], .mdl-button.mdl-button--disabled.mdl-button--disabled { + color: rgba(0,0,0, 0.26); + cursor: default; + background-color: transparent; } + +.mdl-button--fab[disabled][disabled], .mdl-button--fab.mdl-button--disabled.mdl-button--disabled { + background-color: rgba(0,0,0, 0.12); + color: rgba(0,0,0, 0.26); } + +.mdl-button--raised[disabled][disabled], .mdl-button--raised.mdl-button--disabled.mdl-button--disabled { + background-color: rgba(0,0,0, 0.12); + color: rgba(0,0,0, 0.26); + box-shadow: none; } + +.mdl-button--colored[disabled][disabled], .mdl-button--colored.mdl-button--disabled.mdl-button--disabled { + color: rgba(0,0,0, 0.26); } + +.mdl-button .material-icons { + vertical-align: middle; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-card { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + font-size: 16px; + font-weight: 400; + min-height: 200px; + overflow: hidden; + width: 330px; + z-index: 1; + position: relative; + background: rgb(255,255,255); + border-radius: 2px; + box-sizing: border-box; } + +.mdl-card__media { + background-color: rgb(255,64,129); + background-repeat: repeat; + background-position: 50% 50%; + background-size: cover; + background-origin: padding-box; + background-attachment: scroll; + box-sizing: border-box; } + +.mdl-card__title { + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + color: rgb(0,0,0); + display: block; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-justify-content: stretch; + -ms-flex-pack: stretch; + justify-content: stretch; + line-height: normal; + padding: 16px 16px; + -webkit-perspective-origin: 165px 56px; + perspective-origin: 165px 56px; + -webkit-transform-origin: 165px 56px; + transform-origin: 165px 56px; + box-sizing: border-box; } + .mdl-card__title.mdl-card--border { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); } + +.mdl-card__title-text { + -webkit-align-self: flex-end; + -ms-flex-item-align: end; + align-self: flex-end; + color: inherit; + display: block; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + font-size: 24px; + font-weight: 300; + line-height: normal; + overflow: hidden; + -webkit-transform-origin: 149px 48px; + transform-origin: 149px 48px; + margin: 0; } + +.mdl-card__subtitle-text { + font-size: 14px; + color: rgba(0,0,0, 0.54); + margin: 0; } + +.mdl-card__supporting-text { + color: rgba(0,0,0, 0.54); + font-size: 1rem; + line-height: 18px; + overflow: hidden; + padding: 16px 16px; + width: 90%; } + .mdl-card__supporting-text.mdl-card--border { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); } + +.mdl-card__actions { + font-size: 16px; + line-height: normal; + width: 100%; + background-color: transparent; + padding: 8px; + box-sizing: border-box; } + .mdl-card__actions.mdl-card--border { + border-top: 1px solid rgba(0, 0, 0, 0.1); } + +.mdl-card--expand { + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; } + +.mdl-card__menu { + position: absolute; + right: 16px; + top: 16px; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-checkbox { + position: relative; + z-index: 1; + vertical-align: middle; + display: inline-block; + box-sizing: border-box; + width: 100%; + height: 24px; + margin: 0; + padding: 0; } + .mdl-checkbox.is-upgraded { + padding-left: 24px; } + +.mdl-checkbox__input { + line-height: 24px; } + .mdl-checkbox.is-upgraded .mdl-checkbox__input { + position: absolute; + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + -ms-appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + border: none; } + +.mdl-checkbox__box-outline { + position: absolute; + top: 3px; + left: 0; + display: inline-block; + box-sizing: border-box; + width: 16px; + height: 16px; + margin: 0; + cursor: pointer; + overflow: hidden; + border: 2px solid rgba(0,0,0, 0.54); + border-radius: 2px; + z-index: 2; } + .mdl-checkbox.is-checked .mdl-checkbox__box-outline { + border: 2px solid rgb(63,81,181); } + fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline, + .mdl-checkbox.is-disabled .mdl-checkbox__box-outline { + border: 2px solid rgba(0,0,0, 0.26); + cursor: auto; } + +.mdl-checkbox__focus-helper { + position: absolute; + top: 3px; + left: 0; + display: inline-block; + box-sizing: border-box; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: transparent; } + .mdl-checkbox.is-focused .mdl-checkbox__focus-helper { + box-shadow: 0 0 0px 8px rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.1); } + .mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper { + box-shadow: 0 0 0px 8px rgba(63,81,181, 0.26); + background-color: rgba(63,81,181, 0.26); } + +.mdl-checkbox__tick-outline { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + -webkit-mask: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg=="); + mask: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg=="); + background: transparent; + transition-duration: 0.28s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-property: background; } + .mdl-checkbox.is-checked .mdl-checkbox__tick-outline { + background: rgb(63,81,181) url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K"); } + fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline, + .mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline { + background: rgba(0,0,0, 0.26) url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K"); } + +.mdl-checkbox__label { + position: relative; + cursor: pointer; + font-size: 16px; + line-height: 24px; + margin: 0; } + fieldset[disabled] .mdl-checkbox .mdl-checkbox__label, + .mdl-checkbox.is-disabled .mdl-checkbox__label { + color: rgba(0,0,0, 0.26); + cursor: auto; } + +.mdl-checkbox__ripple-container { + position: absolute; + z-index: 2; + top: -6px; + left: -10px; + box-sizing: border-box; + width: 36px; + height: 36px; + border-radius: 50%; + cursor: pointer; + overflow: hidden; + -webkit-mask-image: -webkit-radial-gradient(circle, white, black); } + .mdl-checkbox__ripple-container .mdl-ripple { + background: rgb(63,81,181); } + fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container, + .mdl-checkbox.is-disabled .mdl-checkbox__ripple-container { + cursor: auto; } + fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple, + .mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple { + background: transparent; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-chip { + height: 32px; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + line-height: 32px; + padding: 0 12px; + border: 0; + border-radius: 16px; + background-color: #dedede; + display: inline-block; + color: rgba(0,0,0, 0.87); + margin: 2px 0; + font-size: 0; + white-space: nowrap; } + .mdl-chip__text { + font-size: 13px; + vertical-align: middle; + display: inline-block; } + .mdl-chip__action { + height: 24px; + width: 24px; + background: transparent; + opacity: 0.54; + display: inline-block; + cursor: pointer; + text-align: center; + vertical-align: middle; + padding: 0; + margin: 0 0 0 4px; + font-size: 13px; + text-decoration: none; + color: rgba(0,0,0, 0.87); + border: none; + outline: none; + overflow: hidden; } + .mdl-chip__contact { + height: 32px; + width: 32px; + border-radius: 16px; + display: inline-block; + vertical-align: middle; + margin-right: 8px; + overflow: hidden; + text-align: center; + font-size: 18px; + line-height: 32px; } + .mdl-chip:focus { + outline: 0; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); } + .mdl-chip:active { + background-color: #d6d6d6; } + .mdl-chip--deletable { + padding-right: 4px; } + .mdl-chip--contact { + padding-left: 0; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-data-table { + position: relative; + border: 1px solid rgba(0, 0, 0, 0.12); + border-collapse: collapse; + white-space: nowrap; + font-size: 13px; + background-color: rgb(255,255,255); } + .mdl-data-table thead { + padding-bottom: 3px; } + .mdl-data-table thead .mdl-data-table__select { + margin-top: 0; } + .mdl-data-table tbody tr { + position: relative; + height: 48px; + transition-duration: 0.28s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-property: background-color; } + .mdl-data-table tbody tr.is-selected { + background-color: #e0e0e0; } + .mdl-data-table tbody tr:hover { + background-color: #eeeeee; } + .mdl-data-table td, .mdl-data-table th { + padding: 0 18px 12px 18px; + text-align: right; } + .mdl-data-table td:first-of-type, .mdl-data-table th:first-of-type { + padding-left: 24px; } + .mdl-data-table td:last-of-type, .mdl-data-table th:last-of-type { + padding-right: 24px; } + .mdl-data-table td { + position: relative; + vertical-align: middle; + height: 48px; + border-top: 1px solid rgba(0, 0, 0, 0.12); + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + padding-top: 12px; + box-sizing: border-box; } + .mdl-data-table td .mdl-data-table__select { + vertical-align: middle; } + .mdl-data-table th { + position: relative; + vertical-align: bottom; + text-overflow: ellipsis; + font-size: 14px; + font-weight: bold; + line-height: 24px; + letter-spacing: 0; + height: 48px; + font-size: 12px; + color: rgba(0, 0, 0, 0.54); + padding-bottom: 8px; + box-sizing: border-box; } + .mdl-data-table th.mdl-data-table__header--sorted-ascending, .mdl-data-table th.mdl-data-table__header--sorted-descending { + color: rgba(0, 0, 0, 0.87); } + .mdl-data-table th.mdl-data-table__header--sorted-ascending:before, .mdl-data-table th.mdl-data-table__header--sorted-descending:before { + font-family: 'Material Icons'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + word-wrap: normal; + -moz-font-feature-settings: 'liga'; + font-feature-settings: 'liga'; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; + font-size: 16px; + content: "\e5d8"; + margin-right: 5px; + vertical-align: sub; } + .mdl-data-table th.mdl-data-table__header--sorted-ascending:hover, .mdl-data-table th.mdl-data-table__header--sorted-descending:hover { + cursor: pointer; } + .mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before, .mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before { + color: rgba(0, 0, 0, 0.26); } + .mdl-data-table th.mdl-data-table__header--sorted-descending:before { + content: "\e5db"; } + +.mdl-data-table__select { + width: 16px; } + +.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric { + text-align: left; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-dialog { + border: none; + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); + width: 280px; } + .mdl-dialog__title { + padding: 24px 24px 0; + margin: 0; + font-size: 2.5rem; } + .mdl-dialog__actions { + padding: 8px 8px 8px 24px; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row-reverse; + -ms-flex-direction: row-reverse; + flex-direction: row-reverse; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; } + .mdl-dialog__actions > * { + margin-right: 8px; + height: 36px; } + .mdl-dialog__actions > *:first-child { + margin-right: 0; } + .mdl-dialog__actions--full-width { + padding: 0 0 8px 0; } + .mdl-dialog__actions--full-width > * { + height: 48px; + -webkit-flex: 0 0 100%; + -ms-flex: 0 0 100%; + flex: 0 0 100%; + padding-right: 16px; + margin-right: 0; + text-align: right; } + .mdl-dialog__content { + padding: 20px 24px 24px 24px; + color: rgba(0,0,0, 0.54); } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-mega-footer { + padding: 16px 40px; + color: rgb(158,158,158); + background-color: rgb(66,66,66); } + +.mdl-mega-footer--top-section:after, +.mdl-mega-footer--middle-section:after, +.mdl-mega-footer--bottom-section:after, +.mdl-mega-footer__top-section:after, +.mdl-mega-footer__middle-section:after, +.mdl-mega-footer__bottom-section:after { + content: ''; + display: block; + clear: both; } + +.mdl-mega-footer--left-section, +.mdl-mega-footer__left-section { + margin-bottom: 16px; } + +.mdl-mega-footer--right-section, +.mdl-mega-footer__right-section { + margin-bottom: 16px; } + +.mdl-mega-footer--right-section a, +.mdl-mega-footer__right-section a { + display: block; + margin-bottom: 16px; + color: inherit; + text-decoration: none; } + +@media screen and (min-width: 760px) { + .mdl-mega-footer--left-section, + .mdl-mega-footer__left-section { + float: left; } + .mdl-mega-footer--right-section, + .mdl-mega-footer__right-section { + float: right; } + .mdl-mega-footer--right-section a, + .mdl-mega-footer__right-section a { + display: inline-block; + margin-left: 16px; + line-height: 36px; + vertical-align: middle; } } + +.mdl-mega-footer--social-btn, +.mdl-mega-footer__social-btn { + width: 36px; + height: 36px; + padding: 0; + margin: 0; + background-color: rgb(158,158,158); + border: none; } + +.mdl-mega-footer--drop-down-section, +.mdl-mega-footer__drop-down-section { + display: block; + position: relative; } + +@media screen and (min-width: 760px) { + .mdl-mega-footer--drop-down-section, + .mdl-mega-footer__drop-down-section { + width: 33%; } + .mdl-mega-footer--drop-down-section:nth-child(1), + .mdl-mega-footer--drop-down-section:nth-child(2), + .mdl-mega-footer__drop-down-section:nth-child(1), + .mdl-mega-footer__drop-down-section:nth-child(2) { + float: left; } + .mdl-mega-footer--drop-down-section:nth-child(3), + .mdl-mega-footer__drop-down-section:nth-child(3) { + float: right; } + .mdl-mega-footer--drop-down-section:nth-child(3):after, + .mdl-mega-footer__drop-down-section:nth-child(3):after { + clear: right; } + .mdl-mega-footer--drop-down-section:nth-child(4), + .mdl-mega-footer__drop-down-section:nth-child(4) { + clear: right; + float: right; } + .mdl-mega-footer--middle-section:after, + .mdl-mega-footer__middle-section:after { + content: ''; + display: block; + clear: both; } + .mdl-mega-footer--bottom-section, + .mdl-mega-footer__bottom-section { + padding-top: 0; } } + +@media screen and (min-width: 1024px) { + .mdl-mega-footer--drop-down-section, + .mdl-mega-footer--drop-down-section:nth-child(3), + .mdl-mega-footer--drop-down-section:nth-child(4), + .mdl-mega-footer__drop-down-section, + .mdl-mega-footer__drop-down-section:nth-child(3), + .mdl-mega-footer__drop-down-section:nth-child(4) { + width: 24%; + float: left; } } + +.mdl-mega-footer--heading-checkbox, +.mdl-mega-footer__heading-checkbox { + position: absolute; + width: 100%; + height: 55.8px; + padding: 32px; + margin: 0; + margin-top: -16px; + cursor: pointer; + z-index: 1; + opacity: 0; } + .mdl-mega-footer--heading-checkbox + .mdl-mega-footer--heading:after, + .mdl-mega-footer--heading-checkbox + .mdl-mega-footer__heading:after, + .mdl-mega-footer__heading-checkbox + .mdl-mega-footer--heading:after, + .mdl-mega-footer__heading-checkbox + .mdl-mega-footer__heading:after { + font-family: 'Material Icons'; + content: '\E5CE'; } + +.mdl-mega-footer--heading-checkbox:checked ~ .mdl-mega-footer--link-list, +.mdl-mega-footer--heading-checkbox:checked ~ .mdl-mega-footer__link-list, +.mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer--heading + .mdl-mega-footer--link-list, +.mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer__heading + .mdl-mega-footer__link-list, +.mdl-mega-footer__heading-checkbox:checked ~ .mdl-mega-footer--link-list, +.mdl-mega-footer__heading-checkbox:checked ~ .mdl-mega-footer__link-list, +.mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer--heading + .mdl-mega-footer--link-list, +.mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer__heading + .mdl-mega-footer__link-list { + display: none; } + +.mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer--heading:after, +.mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer__heading:after, +.mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer--heading:after, +.mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer__heading:after { + font-family: 'Material Icons'; + content: '\E5CF'; } + +.mdl-mega-footer--heading, +.mdl-mega-footer__heading { + position: relative; + width: 100%; + padding-right: 39.8px; + margin-bottom: 16px; + box-sizing: border-box; + font-size: 14px; + line-height: 23.8px; + font-weight: 500; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + color: rgb(224,224,224); } + +.mdl-mega-footer--heading:after, +.mdl-mega-footer__heading:after { + content: ''; + position: absolute; + top: 0; + right: 0; + display: block; + width: 23.8px; + height: 23.8px; + background-size: cover; } + +.mdl-mega-footer--link-list, +.mdl-mega-footer__link-list { + list-style: none; + margin: 0; + padding: 0; + margin-bottom: 32px; } + .mdl-mega-footer--link-list:after, + .mdl-mega-footer__link-list:after { + clear: both; + display: block; + content: ''; } + +.mdl-mega-footer--link-list li, +.mdl-mega-footer__link-list li { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + line-height: 20px; } + +.mdl-mega-footer--link-list a, +.mdl-mega-footer__link-list a { + color: inherit; + text-decoration: none; + white-space: nowrap; } + +@media screen and (min-width: 760px) { + .mdl-mega-footer--heading-checkbox, + .mdl-mega-footer__heading-checkbox { + display: none; } + .mdl-mega-footer--heading-checkbox + .mdl-mega-footer--heading:after, + .mdl-mega-footer--heading-checkbox + .mdl-mega-footer__heading:after, + .mdl-mega-footer__heading-checkbox + .mdl-mega-footer--heading:after, + .mdl-mega-footer__heading-checkbox + .mdl-mega-footer__heading:after { + content: ''; } + .mdl-mega-footer--heading-checkbox:checked ~ .mdl-mega-footer--link-list, + .mdl-mega-footer--heading-checkbox:checked ~ .mdl-mega-footer__link-list, + .mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer__heading + .mdl-mega-footer__link-list, + .mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer--heading + .mdl-mega-footer--link-list, + .mdl-mega-footer__heading-checkbox:checked ~ .mdl-mega-footer--link-list, + .mdl-mega-footer__heading-checkbox:checked ~ .mdl-mega-footer__link-list, + .mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer__heading + .mdl-mega-footer__link-list, + .mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer--heading + .mdl-mega-footer--link-list { + display: block; } + .mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer--heading:after, + .mdl-mega-footer--heading-checkbox:checked + .mdl-mega-footer__heading:after, + .mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer--heading:after, + .mdl-mega-footer__heading-checkbox:checked + .mdl-mega-footer__heading:after { + content: ''; } } + +.mdl-mega-footer--bottom-section, +.mdl-mega-footer__bottom-section { + padding-top: 16px; + margin-bottom: 16px; } + +.mdl-logo { + margin-bottom: 16px; + color: white; } + +.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li, +.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li { + float: left; + margin-bottom: 0; + margin-right: 16px; } + +@media screen and (min-width: 760px) { + .mdl-logo { + float: left; + margin-bottom: 0; + margin-right: 16px; } } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-mini-footer { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row wrap; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 32px 16px; + color: rgb(158,158,158); + background-color: rgb(66,66,66); } + .mdl-mini-footer:after { + content: ''; + display: block; } + .mdl-mini-footer .mdl-logo { + line-height: 36px; } + +.mdl-mini-footer--link-list, +.mdl-mini-footer__link-list { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row nowrap; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + list-style: none; + margin: 0; + padding: 0; } + .mdl-mini-footer--link-list li, + .mdl-mini-footer__link-list li { + margin-bottom: 0; + margin-right: 16px; } + @media screen and (min-width: 760px) { + .mdl-mini-footer--link-list li, + .mdl-mini-footer__link-list li { + line-height: 36px; } } + .mdl-mini-footer--link-list a, + .mdl-mini-footer__link-list a { + color: inherit; + text-decoration: none; + white-space: nowrap; } + +.mdl-mini-footer--left-section, +.mdl-mini-footer__left-section { + display: inline-block; + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; } + +.mdl-mini-footer--right-section, +.mdl-mini-footer__right-section { + display: inline-block; + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + +.mdl-mini-footer--social-btn, +.mdl-mini-footer__social-btn { + width: 36px; + height: 36px; + padding: 0; + margin: 0; + background-color: rgb(158,158,158); + border: none; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-icon-toggle { + position: relative; + z-index: 1; + vertical-align: middle; + display: inline-block; + height: 32px; + margin: 0; + padding: 0; } + +.mdl-icon-toggle__input { + line-height: 32px; } + .mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input { + position: absolute; + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + -ms-appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + border: none; } + +.mdl-icon-toggle__label { + display: inline-block; + position: relative; + cursor: pointer; + height: 32px; + width: 32px; + min-width: 32px; + color: rgb(97,97,97); + border-radius: 50%; + padding: 0; + margin-left: 0; + margin-right: 0; + text-align: center; + background-color: transparent; + will-change: background-color; + transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), color 0.2s cubic-bezier(0.4, 0, 0.2, 1); } + .mdl-icon-toggle__label.material-icons { + line-height: 32px; + font-size: 24px; } + .mdl-icon-toggle.is-checked .mdl-icon-toggle__label { + color: rgb(63,81,181); } + .mdl-icon-toggle.is-disabled .mdl-icon-toggle__label { + color: rgba(0,0,0, 0.26); + cursor: auto; + transition: none; } + .mdl-icon-toggle.is-focused .mdl-icon-toggle__label { + background-color: rgba(0,0,0, 0.12); } + .mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label { + background-color: rgba(63,81,181, 0.26); } + +.mdl-icon-toggle__ripple-container { + position: absolute; + z-index: 2; + top: -2px; + left: -2px; + box-sizing: border-box; + width: 36px; + height: 36px; + border-radius: 50%; + cursor: pointer; + overflow: hidden; + -webkit-mask-image: -webkit-radial-gradient(circle, white, black); } + .mdl-icon-toggle__ripple-container .mdl-ripple { + background: rgb(97,97,97); } + .mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container { + cursor: auto; } + .mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple { + background: transparent; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-list { + display: block; + padding: 8px 0; + list-style: none; } + +.mdl-list__item { + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.04em; + line-height: 1; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 48px; + box-sizing: border-box; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + padding: 16px; + cursor: default; + color: rgba(0,0,0, 0.87); + overflow: hidden; } + .mdl-list__item .mdl-list__item-primary-content { + -webkit-order: 0; + -ms-flex-order: 0; + order: 0; + -webkit-flex-grow: 2; + -ms-flex-positive: 2; + flex-grow: 2; + text-decoration: none; + box-sizing: border-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; } + .mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon { + margin-right: 32px; } + .mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar { + margin-right: 16px; } + .mdl-list__item .mdl-list__item-secondary-content { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column; + -ms-flex-flow: column; + flex-flow: column; + -webkit-align-items: flex-end; + -ms-flex-align: end; + align-items: flex-end; + margin-left: 16px; } + .mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label { + display: inline; } + .mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info { + font-size: 12px; + font-weight: 400; + line-height: 1; + letter-spacing: 0; + color: rgba(0,0,0, 0.54); } + .mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header { + padding: 0 0 0 16px; } + +.mdl-list__item-icon, +.mdl-list__item-icon.material-icons { + height: 24px; + width: 24px; + font-size: 24px; + box-sizing: border-box; + color: rgb(117,117,117); } + +.mdl-list__item-avatar, +.mdl-list__item-avatar.material-icons { + height: 40px; + width: 40px; + box-sizing: border-box; + border-radius: 50%; + background-color: rgb(117,117,117); + font-size: 40px; + color: white; } + +.mdl-list__item--two-line { + height: 72px; } + .mdl-list__item--two-line .mdl-list__item-primary-content { + height: 36px; + line-height: 20px; + display: block; } + .mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar { + float: left; } + .mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon { + float: left; + margin-top: 6px; } + .mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content { + height: 36px; } + .mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + line-height: 18px; + color: rgba(0,0,0, 0.54); + display: block; + padding: 0; } + +.mdl-list__item--three-line { + height: 88px; } + .mdl-list__item--three-line .mdl-list__item-primary-content { + height: 52px; + line-height: 20px; + display: block; } + .mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar, + .mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon { + float: left; } + .mdl-list__item--three-line .mdl-list__item-secondary-content { + height: 52px; } + .mdl-list__item--three-line .mdl-list__item-text-body { + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + line-height: 18px; + height: 52px; + color: rgba(0,0,0, 0.54); + display: block; + padding: 0; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-menu__container { + display: block; + margin: 0; + padding: 0; + border: none; + position: absolute; + overflow: visible; + height: 0; + width: 0; + visibility: hidden; + z-index: -1; } + .mdl-menu__container.is-visible, .mdl-menu__container.is-animating { + z-index: 999; + visibility: visible; } + +.mdl-menu__outline { + display: block; + background: rgb(255,255,255); + margin: 0; + padding: 0; + border: none; + border-radius: 2px; + position: absolute; + top: 0; + left: 0; + overflow: hidden; + opacity: 0; + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: 0 0; + transform-origin: 0 0; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); + will-change: transform; + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + z-index: -1; } + .mdl-menu__container.is-visible .mdl-menu__outline { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + z-index: 999; } + .mdl-menu__outline.mdl-menu--bottom-right { + -webkit-transform-origin: 100% 0; + transform-origin: 100% 0; } + .mdl-menu__outline.mdl-menu--top-left { + -webkit-transform-origin: 0 100%; + transform-origin: 0 100%; } + .mdl-menu__outline.mdl-menu--top-right { + -webkit-transform-origin: 100% 100%; + transform-origin: 100% 100%; } + +.mdl-menu { + position: absolute; + list-style: none; + top: 0; + left: 0; + height: auto; + width: auto; + min-width: 124px; + padding: 8px 0; + margin: 0; + opacity: 0; + clip: rect(0 0 0 0); + z-index: -1; } + .mdl-menu__container.is-visible .mdl-menu { + opacity: 1; + z-index: 999; } + .mdl-menu.is-animating { + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1), clip 0.3s cubic-bezier(0.4, 0, 0.2, 1); } + .mdl-menu.mdl-menu--bottom-right { + left: auto; + right: 0; } + .mdl-menu.mdl-menu--top-left { + top: auto; + bottom: 0; } + .mdl-menu.mdl-menu--top-right { + top: auto; + left: auto; + bottom: 0; + right: 0; } + .mdl-menu.mdl-menu--unaligned { + top: auto; + left: auto; } + +.mdl-menu__item { + display: block; + border: none; + color: rgba(0,0,0, 0.87); + background-color: transparent; + text-align: left; + margin: 0; + padding: 0 16px; + outline-color: rgb(189,189,189); + position: relative; + overflow: hidden; + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + text-decoration: none; + cursor: pointer; + height: 48px; + line-height: 48px; + white-space: nowrap; + opacity: 0; + transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .mdl-menu__container.is-visible .mdl-menu__item { + opacity: 1; } + .mdl-menu__item::-moz-focus-inner { + border: 0; } + .mdl-menu__item--full-bleed-divider { + border-bottom: 1px solid rgba(0,0,0, 0.12); } + .mdl-menu__item[disabled], .mdl-menu__item[data-mdl-disabled] { + color: rgb(189,189,189); + background-color: transparent; + cursor: auto; } + .mdl-menu__item[disabled]:hover, .mdl-menu__item[data-mdl-disabled]:hover { + background-color: transparent; } + .mdl-menu__item[disabled]:focus, .mdl-menu__item[data-mdl-disabled]:focus { + background-color: transparent; } + .mdl-menu__item[disabled] .mdl-ripple, .mdl-menu__item[data-mdl-disabled] .mdl-ripple { + background: transparent; } + .mdl-menu__item:hover { + background-color: rgb(238,238,238); } + .mdl-menu__item:focus { + outline: none; + background-color: rgb(238,238,238); } + .mdl-menu__item:active { + background-color: rgb(224,224,224); } + +.mdl-menu__item--ripple-container { + display: block; + height: 100%; + left: 0px; + position: absolute; + top: 0px; + width: 100%; + z-index: 0; + overflow: hidden; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-progress { + display: block; + position: relative; + height: 4px; + width: 500px; + max-width: 100%; } + +.mdl-progress > .bar { + display: block; + position: absolute; + top: 0; + bottom: 0; + width: 0%; + transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1); } + +.mdl-progress > .progressbar { + background-color: rgb(63,81,181); + z-index: 1; + left: 0; } + +.mdl-progress > .bufferbar { + background-image: linear-gradient(to right, rgba(255,255,255, 0.7), rgba(255,255,255, 0.7)), linear-gradient(to right, rgb(63,81,181), rgb(63,81,181)); + z-index: 0; + left: 0; } + +.mdl-progress > .auxbar { + right: 0; } + +@supports (-webkit-appearance: none) { + .mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate) > .auxbar, + .mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate) > .auxbar { + background-image: linear-gradient(to right, rgba(255,255,255, 0.7), rgba(255,255,255, 0.7)), linear-gradient(to right, rgb(63,81,181), rgb(63,81,181)); + -webkit-mask: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo="); + mask: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo="); } } + +.mdl-progress:not(.mdl-progress--indeterminate) > .auxbar, +.mdl-progress:not(.mdl-progress__indeterminate) > .auxbar { + background-image: linear-gradient(to right, rgba(255,255,255, 0.9), rgba(255,255,255, 0.9)), linear-gradient(to right, rgb(63,81,181), rgb(63,81,181)); } + +.mdl-progress.mdl-progress--indeterminate > .bar1, +.mdl-progress.mdl-progress__indeterminate > .bar1 { + background-color: rgb(63,81,181); + -webkit-animation-name: indeterminate1; + animation-name: indeterminate1; + -webkit-animation-duration: 2s; + animation-duration: 2s; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + animation-timing-function: linear; } + +.mdl-progress.mdl-progress--indeterminate > .bar3, +.mdl-progress.mdl-progress__indeterminate > .bar3 { + background-image: none; + background-color: rgb(63,81,181); + -webkit-animation-name: indeterminate2; + animation-name: indeterminate2; + -webkit-animation-duration: 2s; + animation-duration: 2s; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + animation-timing-function: linear; } + +@-webkit-keyframes indeterminate1 { + 0% { + left: 0%; + width: 0%; } + 50% { + left: 25%; + width: 75%; } + 75% { + left: 100%; + width: 0%; } } + +@keyframes indeterminate1 { + 0% { + left: 0%; + width: 0%; } + 50% { + left: 25%; + width: 75%; } + 75% { + left: 100%; + width: 0%; } } + +@-webkit-keyframes indeterminate2 { + 0% { + left: 0%; + width: 0%; } + 50% { + left: 0%; + width: 0%; } + 75% { + left: 0%; + width: 25%; } + 100% { + left: 100%; + width: 0%; } } + +@keyframes indeterminate2 { + 0% { + left: 0%; + width: 0%; } + 50% { + left: 0%; + width: 0%; } + 75% { + left: 0%; + width: 25%; } + 100% { + left: 100%; + width: 0%; } } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-navigation { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + box-sizing: border-box; } + +.mdl-navigation__link { + color: rgb(66,66,66); + text-decoration: none; + margin: 0; + font-size: 14px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0; + opacity: 0.87; } + .mdl-navigation__link .material-icons { + vertical-align: middle; } + +.mdl-layout { + width: 100%; + height: 100%; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + overflow-y: auto; + overflow-x: hidden; + position: relative; + -webkit-overflow-scrolling: touch; } + +.mdl-layout.is-small-screen .mdl-layout--large-screen-only { + display: none; } + +.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only { + display: none; } + +.mdl-layout__container { + position: absolute; + width: 100%; + height: 100%; } + +.mdl-layout__title, +.mdl-layout-title { + display: block; + position: relative; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 20px; + font-weight: 500; + line-height: 1; + letter-spacing: 0.02em; + font-weight: 400; + box-sizing: border-box; } + +.mdl-layout-spacer { + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; } + +.mdl-layout__drawer { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + width: 240px; + height: 100%; + max-height: 100%; + position: absolute; + top: 0; + left: 0; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); + box-sizing: border-box; + border-right: 1px solid rgb(224,224,224); + background: rgb(250,250,250); + -webkit-transform: translateX(-250px); + transform: translateX(-250px); + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + will-change: transform; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-property: -webkit-transform; + transition-property: transform; + transition-property: transform, -webkit-transform; + color: rgb(66,66,66); + overflow: visible; + overflow-y: auto; + z-index: 5; } + .mdl-layout__drawer.is-visible { + -webkit-transform: translateX(0); + transform: translateX(0); } + .mdl-layout__drawer.is-visible ~ .mdl-layout__content.mdl-layout__content { + overflow: hidden; } + .mdl-layout__drawer > * { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; } + .mdl-layout__drawer > .mdl-layout__title, + .mdl-layout__drawer > .mdl-layout-title { + line-height: 64px; + padding-left: 40px; } + @media screen and (max-width: 1024px) { + .mdl-layout__drawer > .mdl-layout__title, + .mdl-layout__drawer > .mdl-layout-title { + line-height: 56px; + padding-left: 16px; } } + .mdl-layout__drawer .mdl-navigation { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: stretch; + -ms-flex-align: stretch; + align-items: stretch; + padding-top: 16px; } + .mdl-layout__drawer .mdl-navigation .mdl-navigation__link { + display: block; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + padding: 16px 40px; + margin: 0; + color: #757575; } + @media screen and (max-width: 1024px) { + .mdl-layout__drawer .mdl-navigation .mdl-navigation__link { + padding: 16px 16px; } } + .mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover { + background-color: rgb(224,224,224); } + .mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current { + background-color: rgb(224,224,224); + color: rgb(0,0,0); } + @media screen and (min-width: 1025px) { + .mdl-layout--fixed-drawer > .mdl-layout__drawer { + -webkit-transform: translateX(0); + transform: translateX(0); } } + +.mdl-layout__drawer-button { + display: block; + position: absolute; + height: 48px; + width: 48px; + border: 0; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + overflow: hidden; + text-align: center; + cursor: pointer; + font-size: 26px; + line-height: 56px; + font-family: Helvetica, Arial, sans-serif; + margin: 8px 12px; + top: 0; + left: 0; + color: rgb(255,255,255); + z-index: 4; } + .mdl-layout__header .mdl-layout__drawer-button { + position: absolute; + color: rgb(255,255,255); + background-color: inherit; } + @media screen and (max-width: 1024px) { + .mdl-layout__header .mdl-layout__drawer-button { + margin: 4px; } } + @media screen and (max-width: 1024px) { + .mdl-layout__drawer-button { + margin: 4px; + color: rgba(0, 0, 0, 0.5); } } + @media screen and (min-width: 1025px) { + .mdl-layout__drawer-button { + line-height: 54px; } + .mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button, + .mdl-layout--fixed-drawer > .mdl-layout__drawer-button, + .mdl-layout--no-drawer-button .mdl-layout__drawer-button { + display: none; } } + +.mdl-layout__header { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + box-sizing: border-box; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: 100%; + margin: 0; + padding: 0; + border: none; + min-height: 64px; + max-height: 1000px; + z-index: 3; + background-color: rgb(63,81,181); + color: rgb(255,255,255); + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-property: max-height, box-shadow; } + @media screen and (max-width: 1024px) { + .mdl-layout__header { + min-height: 56px; } } + .mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen) > .mdl-layout__header { + margin-left: 240px; + width: calc(100% - 240px); } + @media screen and (min-width: 1025px) { + .mdl-layout--fixed-drawer > .mdl-layout__header .mdl-layout__header-row { + padding-left: 40px; } } + .mdl-layout__header > .mdl-layout-icon { + position: absolute; + left: 40px; + top: 16px; + height: 32px; + width: 32px; + overflow: hidden; + z-index: 3; + display: block; } + @media screen and (max-width: 1024px) { + .mdl-layout__header > .mdl-layout-icon { + left: 16px; + top: 12px; } } + .mdl-layout.has-drawer .mdl-layout__header > .mdl-layout-icon { + display: none; } + .mdl-layout__header.is-compact { + max-height: 64px; } + @media screen and (max-width: 1024px) { + .mdl-layout__header.is-compact { + max-height: 56px; } } + .mdl-layout__header.is-compact.has-tabs { + height: 112px; } + @media screen and (max-width: 1024px) { + .mdl-layout__header.is-compact.has-tabs { + min-height: 104px; } } + @media screen and (max-width: 1024px) { + .mdl-layout__header { + display: none; } + .mdl-layout--fixed-header > .mdl-layout__header { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; } } + +.mdl-layout__header--transparent.mdl-layout__header--transparent { + background-color: transparent; + box-shadow: none; } + +.mdl-layout__header--seamed { + box-shadow: none; } + +.mdl-layout__header--scroll { + box-shadow: none; } + +.mdl-layout__header--waterfall { + box-shadow: none; + overflow: hidden; } + .mdl-layout__header--waterfall.is-casting-shadow { + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); } + .mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top { + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; } + +.mdl-layout__header-row { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + box-sizing: border-box; + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + align-self: stretch; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + height: 64px; + margin: 0; + padding: 0 40px 0 80px; } + .mdl-layout--no-drawer-button .mdl-layout__header-row { + padding-left: 40px; } + @media screen and (min-width: 1025px) { + .mdl-layout--no-desktop-drawer-button .mdl-layout__header-row { + padding-left: 40px; } } + @media screen and (max-width: 1024px) { + .mdl-layout__header-row { + height: 56px; + padding: 0 16px 0 72px; } + .mdl-layout--no-drawer-button .mdl-layout__header-row { + padding-left: 16px; } } + .mdl-layout__header-row > * { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; } + .mdl-layout__header--scroll .mdl-layout__header-row { + width: 100%; } + .mdl-layout__header-row .mdl-navigation { + margin: 0; + padding: 0; + height: 64px; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; } + @media screen and (max-width: 1024px) { + .mdl-layout__header-row .mdl-navigation { + height: 56px; } } + .mdl-layout__header-row .mdl-navigation__link { + display: block; + color: rgb(255,255,255); + line-height: 64px; + padding: 0 24px; } + @media screen and (max-width: 1024px) { + .mdl-layout__header-row .mdl-navigation__link { + line-height: 56px; + padding: 0 16px; } } + +.mdl-layout__obfuscator { + background-color: transparent; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 4; + visibility: hidden; + transition-property: background-color; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } + .mdl-layout__obfuscator.is-visible { + background-color: rgba(0, 0, 0, 0.5); + visibility: visible; } + @supports (pointer-events: auto) { + .mdl-layout__obfuscator { + background-color: rgba(0, 0, 0, 0.5); + opacity: 0; + transition-property: opacity; + visibility: visible; + pointer-events: none; } + .mdl-layout__obfuscator.is-visible { + pointer-events: auto; + opacity: 1; } } + +.mdl-layout__content { + -ms-flex: 0 1 auto; + position: relative; + display: inline-block; + overflow-y: auto; + overflow-x: hidden; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + z-index: 1; + -webkit-overflow-scrolling: touch; } + .mdl-layout--fixed-drawer > .mdl-layout__content { + margin-left: 240px; } + .mdl-layout__container.has-scrolling-header .mdl-layout__content { + overflow: visible; } + @media screen and (max-width: 1024px) { + .mdl-layout--fixed-drawer > .mdl-layout__content { + margin-left: 0; } + .mdl-layout__container.has-scrolling-header .mdl-layout__content { + overflow-y: auto; + overflow-x: hidden; } } + +.mdl-layout__tab-bar { + height: 96px; + margin: 0; + width: calc(100% - 112px); + padding: 0 0 0 56px; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + background-color: rgb(63,81,181); + overflow-y: hidden; + overflow-x: scroll; } + .mdl-layout__tab-bar::-webkit-scrollbar { + display: none; } + .mdl-layout--no-drawer-button .mdl-layout__tab-bar { + padding-left: 16px; + width: calc(100% - 32px); } + @media screen and (min-width: 1025px) { + .mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar { + padding-left: 16px; + width: calc(100% - 32px); } } + @media screen and (max-width: 1024px) { + .mdl-layout__tab-bar { + width: calc(100% - 60px); + padding: 0 0 0 60px; } + .mdl-layout--no-drawer-button .mdl-layout__tab-bar { + width: calc(100% - 8px); + padding-left: 4px; } } + .mdl-layout--fixed-tabs .mdl-layout__tab-bar { + padding: 0; + overflow: hidden; + width: 100%; } + +.mdl-layout__tab-bar-container { + position: relative; + height: 48px; + width: 100%; + border: none; + margin: 0; + z-index: 2; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + overflow: hidden; } + .mdl-layout__container > .mdl-layout__tab-bar-container { + position: absolute; + top: 0; + left: 0; } + +.mdl-layout__tab-bar-button { + display: inline-block; + position: absolute; + top: 0; + height: 48px; + width: 56px; + z-index: 4; + text-align: center; + background-color: rgb(63,81,181); + color: transparent; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button, + .mdl-layout--no-drawer-button .mdl-layout__tab-bar-button { + width: 16px; } + .mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons, + .mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons { + position: relative; + left: -4px; } + @media screen and (max-width: 1024px) { + .mdl-layout__tab-bar-button { + width: 60px; } } + .mdl-layout--fixed-tabs .mdl-layout__tab-bar-button { + display: none; } + .mdl-layout__tab-bar-button .material-icons { + line-height: 48px; } + .mdl-layout__tab-bar-button.is-active { + color: rgb(255,255,255); } + +.mdl-layout__tab-bar-left-button { + left: 0; } + +.mdl-layout__tab-bar-right-button { + right: 0; } + +.mdl-layout__tab { + margin: 0; + border: none; + padding: 0 24px 0 24px; + float: left; + position: relative; + display: block; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + text-decoration: none; + height: 48px; + line-height: 48px; + text-align: center; + font-weight: 500; + font-size: 14px; + text-transform: uppercase; + color: rgba(255,255,255, 0.6); + overflow: hidden; } + @media screen and (max-width: 1024px) { + .mdl-layout__tab { + padding: 0 12px 0 12px; } } + .mdl-layout--fixed-tabs .mdl-layout__tab { + float: none; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + padding: 0; } + .mdl-layout.is-upgraded .mdl-layout__tab.is-active { + color: rgb(255,255,255); } + .mdl-layout.is-upgraded .mdl-layout__tab.is-active::after { + height: 2px; + width: 100%; + display: block; + content: " "; + bottom: 0; + left: 0; + position: absolute; + background: rgb(255,64,129); + -webkit-animation: border-expand 0.2s cubic-bezier(0.4, 0, 0.4, 1) 0.01s alternate forwards; + animation: border-expand 0.2s cubic-bezier(0.4, 0, 0.4, 1) 0.01s alternate forwards; + transition: all 1s cubic-bezier(0.4, 0, 1, 1); } + .mdl-layout__tab .mdl-layout__tab-ripple-container { + display: block; + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + z-index: 1; + overflow: hidden; } + .mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple { + background-color: rgb(255,255,255); } + +.mdl-layout__tab-panel { + display: block; } + .mdl-layout.is-upgraded .mdl-layout__tab-panel { + display: none; } + .mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active { + display: block; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-radio { + position: relative; + font-size: 16px; + line-height: 24px; + display: inline-block; + vertical-align: middle; + box-sizing: border-box; + height: 24px; + margin: 0; + padding-left: 0; } + .mdl-radio.is-upgraded { + padding-left: 24px; } + +.mdl-radio__button { + line-height: 24px; } + .mdl-radio.is-upgraded .mdl-radio__button { + position: absolute; + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + -ms-appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + border: none; } + +.mdl-radio__outer-circle { + position: absolute; + top: 4px; + left: 0; + display: inline-block; + box-sizing: border-box; + width: 16px; + height: 16px; + margin: 0; + cursor: pointer; + border: 2px solid rgba(0,0,0, 0.54); + border-radius: 50%; + z-index: 2; } + .mdl-radio.is-checked .mdl-radio__outer-circle { + border: 2px solid rgb(63,81,181); } + .mdl-radio__outer-circle fieldset[disabled] .mdl-radio, + .mdl-radio.is-disabled .mdl-radio__outer-circle { + border: 2px solid rgba(0,0,0, 0.26); + cursor: auto; } + +.mdl-radio__inner-circle { + position: absolute; + z-index: 1; + margin: 0; + top: 8px; + left: 4px; + box-sizing: border-box; + width: 8px; + height: 8px; + cursor: pointer; + transition-duration: 0.28s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-property: -webkit-transform; + transition-property: transform; + transition-property: transform, -webkit-transform; + -webkit-transform: scale(0, 0); + transform: scale(0, 0); + border-radius: 50%; + background: rgb(63,81,181); } + .mdl-radio.is-checked .mdl-radio__inner-circle { + -webkit-transform: scale(1, 1); + transform: scale(1, 1); } + fieldset[disabled] .mdl-radio .mdl-radio__inner-circle, + .mdl-radio.is-disabled .mdl-radio__inner-circle { + background: rgba(0,0,0, 0.26); + cursor: auto; } + .mdl-radio.is-focused .mdl-radio__inner-circle { + box-shadow: 0 0 0px 10px rgba(0, 0, 0, 0.1); } + +.mdl-radio__label { + cursor: pointer; } + fieldset[disabled] .mdl-radio .mdl-radio__label, + .mdl-radio.is-disabled .mdl-radio__label { + color: rgba(0,0,0, 0.26); + cursor: auto; } + +.mdl-radio__ripple-container { + position: absolute; + z-index: 2; + top: -9px; + left: -13px; + box-sizing: border-box; + width: 42px; + height: 42px; + border-radius: 50%; + cursor: pointer; + overflow: hidden; + -webkit-mask-image: -webkit-radial-gradient(circle, white, black); } + .mdl-radio__ripple-container .mdl-ripple { + background: rgb(63,81,181); } + fieldset[disabled] .mdl-radio .mdl-radio__ripple-container, + .mdl-radio.is-disabled .mdl-radio__ripple-container { + cursor: auto; } + fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple, + .mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple { + background: transparent; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +_:-ms-input-placeholder, :root .mdl-slider.mdl-slider.is-upgraded { + -ms-appearance: none; + height: 32px; + margin: 0; } + +.mdl-slider { + width: calc(100% - 40px); + margin: 0 20px; } + .mdl-slider.is-upgraded { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 2px; + background: transparent; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + outline: 0; + padding: 0; + color: rgb(63,81,181); + -webkit-align-self: center; + -ms-flex-item-align: center; + -ms-grid-row-align: center; + align-self: center; + z-index: 1; + cursor: pointer; + /**************************** Tracks ****************************/ + /**************************** Thumbs ****************************/ + /**************************** 0-value ****************************/ + /**************************** Disabled ****************************/ } + .mdl-slider.is-upgraded::-moz-focus-outer { + border: 0; } + .mdl-slider.is-upgraded::-ms-tooltip { + display: none; } + .mdl-slider.is-upgraded::-webkit-slider-runnable-track { + background: transparent; } + .mdl-slider.is-upgraded::-moz-range-track { + background: transparent; + border: none; } + .mdl-slider.is-upgraded::-ms-track { + background: none; + color: transparent; + height: 2px; + width: 100%; + border: none; } + .mdl-slider.is-upgraded::-ms-fill-lower { + padding: 0; + background: linear-gradient(to right, transparent, transparent 16px, rgb(63,81,181) 16px, rgb(63,81,181) 0); } + .mdl-slider.is-upgraded::-ms-fill-upper { + padding: 0; + background: linear-gradient(to left, transparent, transparent 16px, rgba(0,0,0, 0.26) 16px, rgba(0,0,0, 0.26) 0); } + .mdl-slider.is-upgraded::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 12px; + box-sizing: border-box; + border-radius: 50%; + background: rgb(63,81,181); + border: none; + transition: border 0.18s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.18s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 0.18s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), border 0.18s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.18s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), border 0.18s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.18s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 0.18s cubic-bezier(0.4, 0, 0.2, 1); } + .mdl-slider.is-upgraded::-moz-range-thumb { + -moz-appearance: none; + width: 12px; + height: 12px; + box-sizing: border-box; + border-radius: 50%; + background-image: none; + background: rgb(63,81,181); + border: none; } + .mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb { + box-shadow: 0 0 0 10px rgba(63,81,181, 0.26); } + .mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb { + box-shadow: 0 0 0 10px rgba(63,81,181, 0.26); } + .mdl-slider.is-upgraded:active::-webkit-slider-thumb { + background-image: none; + background: rgb(63,81,181); + -webkit-transform: scale(1.5); + transform: scale(1.5); } + .mdl-slider.is-upgraded:active::-moz-range-thumb { + background-image: none; + background: rgb(63,81,181); + transform: scale(1.5); } + .mdl-slider.is-upgraded::-ms-thumb { + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + background: rgb(63,81,181); + transform: scale(0.375); + transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 0.18s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), -webkit-transform 0.18s cubic-bezier(0.4, 0, 0.2, 1); } + .mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb { + background: radial-gradient(circle closest-side, rgb(63,81,181) 0%, rgb(63,81,181) 37.5%, rgba(63,81,181, 0.26) 37.5%, rgba(63,81,181, 0.26) 100%); + transform: scale(1); } + .mdl-slider.is-upgraded:active::-ms-thumb { + background: rgb(63,81,181); + transform: scale(0.5625); } + .mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb { + border: 2px solid rgba(0,0,0, 0.26); + background: transparent; } + .mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb { + border: 2px solid rgba(0,0,0, 0.26); + background: transparent; } + .mdl-slider.is-upgraded.is-lowest-value + +.mdl-slider__background-flex > .mdl-slider__background-upper { + left: 6px; } + .mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb { + box-shadow: 0 0 0 10px rgba(0,0,0, 0.12); + background: rgba(0,0,0, 0.12); } + .mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb { + box-shadow: 0 0 0 10px rgba(0,0,0, 0.12); + background: rgba(0,0,0, 0.12); } + .mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb { + border: 1.6px solid rgba(0,0,0, 0.26); + -webkit-transform: scale(1.5); + transform: scale(1.5); } + .mdl-slider.is-upgraded.is-lowest-value:active + +.mdl-slider__background-flex > .mdl-slider__background-upper { + left: 9px; } + .mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb { + border: 1.5px solid rgba(0,0,0, 0.26); + transform: scale(1.5); } + .mdl-slider.is-upgraded.is-lowest-value::-ms-thumb { + background: radial-gradient(circle closest-side, transparent 0%, transparent 66.67%, rgba(0,0,0, 0.26) 66.67%, rgba(0,0,0, 0.26) 100%); } + .mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb { + background: radial-gradient(circle closest-side, rgba(0,0,0, 0.12) 0%, rgba(0,0,0, 0.12) 25%, rgba(0,0,0, 0.26) 25%, rgba(0,0,0, 0.26) 37.5%, rgba(0,0,0, 0.12) 37.5%, rgba(0,0,0, 0.12) 100%); + transform: scale(1); } + .mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb { + transform: scale(0.5625); + background: radial-gradient(circle closest-side, transparent 0%, transparent 77.78%, rgba(0,0,0, 0.26) 77.78%, rgba(0,0,0, 0.26) 100%); } + .mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower { + background: transparent; } + .mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper { + margin-left: 6px; } + .mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper { + margin-left: 9px; } + .mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb, .mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb, .mdl-slider.is-upgraded:disabled::-webkit-slider-thumb { + -webkit-transform: scale(0.667); + transform: scale(0.667); + background: rgba(0,0,0, 0.26); } + .mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb, .mdl-slider.is-upgraded:disabled:active::-moz-range-thumb, .mdl-slider.is-upgraded:disabled::-moz-range-thumb { + transform: scale(0.667); + background: rgba(0,0,0, 0.26); } + .mdl-slider.is-upgraded:disabled + +.mdl-slider__background-flex > .mdl-slider__background-lower { + background-color: rgba(0,0,0, 0.26); + left: -6px; } + .mdl-slider.is-upgraded:disabled + +.mdl-slider__background-flex > .mdl-slider__background-upper { + left: 6px; } + .mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb, .mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb, .mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb { + border: 3px solid rgba(0,0,0, 0.26); + background: transparent; + -webkit-transform: scale(0.667); + transform: scale(0.667); } + .mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb, .mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb, .mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb { + border: 3px solid rgba(0,0,0, 0.26); + background: transparent; + transform: scale(0.667); } + .mdl-slider.is-upgraded.is-lowest-value:disabled:active + +.mdl-slider__background-flex > .mdl-slider__background-upper { + left: 6px; } + .mdl-slider.is-upgraded:disabled:focus::-ms-thumb, .mdl-slider.is-upgraded:disabled:active::-ms-thumb, .mdl-slider.is-upgraded:disabled::-ms-thumb { + transform: scale(0.25); + background: rgba(0,0,0, 0.26); } + .mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb, .mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb, .mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb { + transform: scale(0.25); + background: radial-gradient(circle closest-side, transparent 0%, transparent 50%, rgba(0,0,0, 0.26) 50%, rgba(0,0,0, 0.26) 100%); } + .mdl-slider.is-upgraded:disabled::-ms-fill-lower { + margin-right: 6px; + background: linear-gradient(to right, transparent, transparent 25px, rgba(0,0,0, 0.26) 25px, rgba(0,0,0, 0.26) 0); } + .mdl-slider.is-upgraded:disabled::-ms-fill-upper { + margin-left: 6px; } + .mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper { + margin-left: 6px; } + +.mdl-slider__ie-container { + height: 18px; + overflow: visible; + border: none; + margin: none; + padding: none; } + +.mdl-slider__container { + height: 18px; + position: relative; + background: none; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; } + +.mdl-slider__background-flex { + background: transparent; + position: absolute; + height: 2px; + width: calc(100% - 52px); + top: 50%; + left: 0; + margin: 0 26px; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + overflow: hidden; + border: 0; + padding: 0; + -webkit-transform: translate(0, -1px); + transform: translate(0, -1px); } + +.mdl-slider__background-lower { + background: rgb(63,81,181); + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + position: relative; + border: 0; + padding: 0; } + +.mdl-slider__background-upper { + background: rgba(0,0,0, 0.26); + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + position: relative; + border: 0; + padding: 0; + transition: left 0.18s cubic-bezier(0.4, 0, 0.2, 1); } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-snackbar { + position: fixed; + bottom: 0; + left: 50%; + cursor: default; + background-color: #323232; + z-index: 3; + display: block; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + will-change: transform; + -webkit-transform: translate(0, 80px); + transform: translate(0, 80px); + transition: -webkit-transform 0.25s cubic-bezier(0.4, 0, 1, 1); + transition: transform 0.25s cubic-bezier(0.4, 0, 1, 1); + transition: transform 0.25s cubic-bezier(0.4, 0, 1, 1), -webkit-transform 0.25s cubic-bezier(0.4, 0, 1, 1); + pointer-events: none; } + @media (max-width: 479px) { + .mdl-snackbar { + width: 100%; + left: 0; + min-height: 48px; + max-height: 80px; } } + @media (min-width: 480px) { + .mdl-snackbar { + min-width: 288px; + max-width: 568px; + border-radius: 2px; + -webkit-transform: translate(-50%, 80px); + transform: translate(-50%, 80px); } } + .mdl-snackbar--active { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + pointer-events: auto; + transition: -webkit-transform 0.25s cubic-bezier(0, 0, 0.2, 1); + transition: transform 0.25s cubic-bezier(0, 0, 0.2, 1); + transition: transform 0.25s cubic-bezier(0, 0, 0.2, 1), -webkit-transform 0.25s cubic-bezier(0, 0, 0.2, 1); } + @media (min-width: 480px) { + .mdl-snackbar--active { + -webkit-transform: translate(-50%, 0); + transform: translate(-50%, 0); } } + .mdl-snackbar__text { + padding: 14px 12px 14px 24px; + vertical-align: middle; + color: white; + float: left; } + .mdl-snackbar__action { + background: transparent; + border: none; + color: rgb(255,64,129); + float: right; + text-transform: uppercase; + padding: 14px 24px 14px 12px; + font-family: "Roboto", "Helvetica", "Arial", sans-serif; + font-size: 14px; + font-weight: 500; + text-transform: uppercase; + line-height: 1; + letter-spacing: 0; + overflow: hidden; + outline: none; + opacity: 0; + pointer-events: none; + cursor: pointer; + text-decoration: none; + text-align: center; + -webkit-align-self: center; + -ms-flex-item-align: center; + -ms-grid-row-align: center; + align-self: center; } + .mdl-snackbar__action::-moz-focus-inner { + border: 0; } + .mdl-snackbar__action:not([aria-hidden]) { + opacity: 1; + pointer-events: auto; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-spinner { + display: inline-block; + position: relative; + width: 28px; + height: 28px; } + .mdl-spinner:not(.is-upgraded).is-active:after { + content: "Loading..."; } + .mdl-spinner.is-upgraded.is-active { + -webkit-animation: mdl-spinner__container-rotate 1568.23529412ms linear infinite; + animation: mdl-spinner__container-rotate 1568.23529412ms linear infinite; } + +@-webkit-keyframes mdl-spinner__container-rotate { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +@keyframes mdl-spinner__container-rotate { + to { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); } } + +.mdl-spinner__layer { + position: absolute; + width: 100%; + height: 100%; + opacity: 0; } + +.mdl-spinner__layer-1 { + border-color: rgb(66,165,245); } + .mdl-spinner--single-color .mdl-spinner__layer-1 { + border-color: rgb(63,81,181); } + .mdl-spinner.is-active .mdl-spinner__layer-1 { + -webkit-animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; } + +.mdl-spinner__layer-2 { + border-color: rgb(244,67,54); } + .mdl-spinner--single-color .mdl-spinner__layer-2 { + border-color: rgb(63,81,181); } + .mdl-spinner.is-active .mdl-spinner__layer-2 { + -webkit-animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; } + +.mdl-spinner__layer-3 { + border-color: rgb(253,216,53); } + .mdl-spinner--single-color .mdl-spinner__layer-3 { + border-color: rgb(63,81,181); } + .mdl-spinner.is-active .mdl-spinner__layer-3 { + -webkit-animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; } + +.mdl-spinner__layer-4 { + border-color: rgb(76,175,80); } + .mdl-spinner--single-color .mdl-spinner__layer-4 { + border-color: rgb(63,81,181); } + .mdl-spinner.is-active .mdl-spinner__layer-4 { + -webkit-animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both, mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; } + +@-webkit-keyframes mdl-spinner__fill-unfill-rotate { + 12.5% { + -webkit-transform: rotate(135deg); + transform: rotate(135deg); } + 25% { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + 37.5% { + -webkit-transform: rotate(405deg); + transform: rotate(405deg); } + 50% { + -webkit-transform: rotate(540deg); + transform: rotate(540deg); } + 62.5% { + -webkit-transform: rotate(675deg); + transform: rotate(675deg); } + 75% { + -webkit-transform: rotate(810deg); + transform: rotate(810deg); } + 87.5% { + -webkit-transform: rotate(945deg); + transform: rotate(945deg); } + to { + -webkit-transform: rotate(1080deg); + transform: rotate(1080deg); } } + +@keyframes mdl-spinner__fill-unfill-rotate { + 12.5% { + -webkit-transform: rotate(135deg); + transform: rotate(135deg); } + 25% { + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + 37.5% { + -webkit-transform: rotate(405deg); + transform: rotate(405deg); } + 50% { + -webkit-transform: rotate(540deg); + transform: rotate(540deg); } + 62.5% { + -webkit-transform: rotate(675deg); + transform: rotate(675deg); } + 75% { + -webkit-transform: rotate(810deg); + transform: rotate(810deg); } + 87.5% { + -webkit-transform: rotate(945deg); + transform: rotate(945deg); } + to { + -webkit-transform: rotate(1080deg); + transform: rotate(1080deg); } } + +/** +* HACK: Even though the intention is to have the current .mdl-spinner__layer-N +* at `opacity: 1`, we set it to `opacity: 0.99` instead since this forces Chrome +* to do proper subpixel rendering for the elements being animated. This is +* especially visible in Chrome 39 on Ubuntu 14.04. See: +* +* - https://github.com/Polymer/paper-spinner/issues/9 +* - https://code.google.com/p/chromium/issues/detail?id=436255 +*/ +@-webkit-keyframes mdl-spinner__layer-1-fade-in-out { + from { + opacity: 0.99; } + 25% { + opacity: 0.99; } + 26% { + opacity: 0; } + 89% { + opacity: 0; } + 90% { + opacity: 0.99; } + 100% { + opacity: 0.99; } } +@keyframes mdl-spinner__layer-1-fade-in-out { + from { + opacity: 0.99; } + 25% { + opacity: 0.99; } + 26% { + opacity: 0; } + 89% { + opacity: 0; } + 90% { + opacity: 0.99; } + 100% { + opacity: 0.99; } } + +@-webkit-keyframes mdl-spinner__layer-2-fade-in-out { + from { + opacity: 0; } + 15% { + opacity: 0; } + 25% { + opacity: 0.99; } + 50% { + opacity: 0.99; } + 51% { + opacity: 0; } } + +@keyframes mdl-spinner__layer-2-fade-in-out { + from { + opacity: 0; } + 15% { + opacity: 0; } + 25% { + opacity: 0.99; } + 50% { + opacity: 0.99; } + 51% { + opacity: 0; } } + +@-webkit-keyframes mdl-spinner__layer-3-fade-in-out { + from { + opacity: 0; } + 40% { + opacity: 0; } + 50% { + opacity: 0.99; } + 75% { + opacity: 0.99; } + 76% { + opacity: 0; } } + +@keyframes mdl-spinner__layer-3-fade-in-out { + from { + opacity: 0; } + 40% { + opacity: 0; } + 50% { + opacity: 0.99; } + 75% { + opacity: 0.99; } + 76% { + opacity: 0; } } + +@-webkit-keyframes mdl-spinner__layer-4-fade-in-out { + from { + opacity: 0; } + 65% { + opacity: 0; } + 75% { + opacity: 0.99; } + 90% { + opacity: 0.99; } + 100% { + opacity: 0; } } + +@keyframes mdl-spinner__layer-4-fade-in-out { + from { + opacity: 0; } + 65% { + opacity: 0; } + 75% { + opacity: 0.99; } + 90% { + opacity: 0.99; } + 100% { + opacity: 0; } } + +/** +* Patch the gap that appear between the two adjacent +* div.mdl-spinner__circle-clipper while the spinner is rotating +* (appears on Chrome 38, Safari 7.1, and IE 11). +* +* Update: the gap no longer appears on Chrome when .mdl-spinner__layer-N's +* opacity is 0.99, but still does on Safari and IE. +*/ +.mdl-spinner__gap-patch { + position: absolute; + box-sizing: border-box; + top: 0; + left: 45%; + width: 10%; + height: 100%; + overflow: hidden; + border-color: inherit; } + .mdl-spinner__gap-patch .mdl-spinner__circle { + width: 1000%; + left: -450%; } + +.mdl-spinner__circle-clipper { + display: inline-block; + position: relative; + width: 50%; + height: 100%; + overflow: hidden; + border-color: inherit; } + .mdl-spinner__circle-clipper.mdl-spinner__left { + float: left; } + .mdl-spinner__circle-clipper.mdl-spinner__right { + float: right; } + .mdl-spinner__circle-clipper .mdl-spinner__circle { + width: 200%; } + +.mdl-spinner__circle { + box-sizing: border-box; + height: 100%; + border-width: 3px; + border-style: solid; + border-color: inherit; + border-bottom-color: transparent !important; + border-radius: 50%; + -webkit-animation: none; + animation: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; } + .mdl-spinner__left .mdl-spinner__circle { + border-right-color: transparent !important; + -webkit-transform: rotate(129deg); + transform: rotate(129deg); } + .mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle { + -webkit-animation: mdl-spinner__left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdl-spinner__left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; } + .mdl-spinner__right .mdl-spinner__circle { + left: -100%; + border-left-color: transparent !important; + -webkit-transform: rotate(-129deg); + transform: rotate(-129deg); } + .mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle { + -webkit-animation: mdl-spinner__right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; + animation: mdl-spinner__right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both; } + +@-webkit-keyframes mdl-spinner__left-spin { + from { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); } + 50% { + -webkit-transform: rotate(-5deg); + transform: rotate(-5deg); } + to { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); } } + +@keyframes mdl-spinner__left-spin { + from { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); } + 50% { + -webkit-transform: rotate(-5deg); + transform: rotate(-5deg); } + to { + -webkit-transform: rotate(130deg); + transform: rotate(130deg); } } + +@-webkit-keyframes mdl-spinner__right-spin { + from { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); } + 50% { + -webkit-transform: rotate(5deg); + transform: rotate(5deg); } + to { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); } } + +@keyframes mdl-spinner__right-spin { + from { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); } + 50% { + -webkit-transform: rotate(5deg); + transform: rotate(5deg); } + to { + -webkit-transform: rotate(-130deg); + transform: rotate(-130deg); } } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-switch { + position: relative; + z-index: 1; + vertical-align: middle; + display: inline-block; + box-sizing: border-box; + width: 100%; + height: 24px; + margin: 0; + padding: 0; + overflow: visible; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .mdl-switch.is-upgraded { + padding-left: 28px; } + +.mdl-switch__input { + line-height: 24px; } + .mdl-switch.is-upgraded .mdl-switch__input { + position: absolute; + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + -ms-appearance: none; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + border: none; } + +.mdl-switch__track { + background: rgba(0,0,0, 0.26); + position: absolute; + left: 0; + top: 5px; + height: 14px; + width: 36px; + border-radius: 14px; + cursor: pointer; } + .mdl-switch.is-checked .mdl-switch__track { + background: rgba(63,81,181, 0.5); } + .mdl-switch__track fieldset[disabled] .mdl-switch, + .mdl-switch.is-disabled .mdl-switch__track { + background: rgba(0,0,0, 0.12); + cursor: auto; } + +.mdl-switch__thumb { + background: rgb(250,250,250); + position: absolute; + left: 0; + top: 2px; + height: 20px; + width: 20px; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); + transition-duration: 0.28s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-property: left; } + .mdl-switch.is-checked .mdl-switch__thumb { + background: rgb(63,81,181); + left: 16px; + box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), 0 3px 3px -2px rgba(0, 0, 0, 0.2), 0 1px 8px 0 rgba(0, 0, 0, 0.12); } + .mdl-switch__thumb fieldset[disabled] .mdl-switch, + .mdl-switch.is-disabled .mdl-switch__thumb { + background: rgb(189,189,189); + cursor: auto; } + +.mdl-switch__focus-helper { + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-4px, -4px); + transform: translate(-4px, -4px); + display: inline-block; + box-sizing: border-box; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: transparent; } + .mdl-switch.is-focused .mdl-switch__focus-helper { + box-shadow: 0 0 0px 20px rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.1); } + .mdl-switch.is-focused.is-checked .mdl-switch__focus-helper { + box-shadow: 0 0 0px 20px rgba(63,81,181, 0.26); + background-color: rgba(63,81,181, 0.26); } + +.mdl-switch__label { + position: relative; + cursor: pointer; + font-size: 16px; + line-height: 24px; + margin: 0; + left: 24px; } + .mdl-switch__label fieldset[disabled] .mdl-switch, + .mdl-switch.is-disabled .mdl-switch__label { + color: rgb(189,189,189); + cursor: auto; } + +.mdl-switch__ripple-container { + position: absolute; + z-index: 2; + top: -12px; + left: -14px; + box-sizing: border-box; + width: 48px; + height: 48px; + border-radius: 50%; + cursor: pointer; + overflow: hidden; + -webkit-mask-image: -webkit-radial-gradient(circle, white, black); + transition-duration: 0.40s; + transition-timing-function: step-end; + transition-property: left; } + .mdl-switch__ripple-container .mdl-ripple { + background: rgb(63,81,181); } + .mdl-switch__ripple-container fieldset[disabled] .mdl-switch, + .mdl-switch.is-disabled .mdl-switch__ripple-container { + cursor: auto; } + fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple, + .mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple { + background: transparent; } + .mdl-switch.is-checked .mdl-switch__ripple-container { + left: 2px; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-tabs { + display: block; + width: 100%; } + +.mdl-tabs__tab-bar { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-content: space-between; + -ms-flex-line-pack: justify; + align-content: space-between; + -webkit-align-items: flex-start; + -ms-flex-align: start; + align-items: flex-start; + height: 48px; + padding: 0 0 0 0; + margin: 0; + border-bottom: 1px solid rgb(224,224,224); } + +.mdl-tabs__tab { + margin: 0; + border: none; + padding: 0 24px 0 24px; + float: left; + position: relative; + display: block; + text-decoration: none; + height: 48px; + line-height: 48px; + text-align: center; + font-weight: 500; + font-size: 14px; + text-transform: uppercase; + color: rgba(0,0,0, 0.54); + overflow: hidden; } + .mdl-tabs.is-upgraded .mdl-tabs__tab.is-active { + color: rgba(0,0,0, 0.87); } + .mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after { + height: 2px; + width: 100%; + display: block; + content: " "; + bottom: 0px; + left: 0px; + position: absolute; + background: rgb(63,81,181); + -webkit-animation: border-expand 0.2s cubic-bezier(0.4, 0, 0.4, 1) 0.01s alternate forwards; + animation: border-expand 0.2s cubic-bezier(0.4, 0, 0.4, 1) 0.01s alternate forwards; + transition: all 1s cubic-bezier(0.4, 0, 1, 1); } + .mdl-tabs__tab .mdl-tabs__ripple-container { + display: block; + position: absolute; + height: 100%; + width: 100%; + left: 0px; + top: 0px; + z-index: 1; + overflow: hidden; } + .mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple { + background: rgb(63,81,181); } + +.mdl-tabs__panel { + display: block; } + .mdl-tabs.is-upgraded .mdl-tabs__panel { + display: none; } + .mdl-tabs.is-upgraded .mdl-tabs__panel.is-active { + display: block; } + +@-webkit-keyframes border-expand { + 0% { + opacity: 0; + width: 0; } + 100% { + opacity: 1; + width: 100%; } } + +@keyframes border-expand { + 0% { + opacity: 0; + width: 0; } + 100% { + opacity: 1; + width: 100%; } } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-textfield { + position: relative; + font-size: 16px; + display: inline-block; + box-sizing: border-box; + width: 300px; + max-width: 100%; + margin: 0; + padding: 20px 0; } + .mdl-textfield .mdl-button { + position: absolute; + bottom: 20px; } + +.mdl-textfield--align-right { + text-align: right; } + +.mdl-textfield--full-width { + width: 100%; } + +.mdl-textfield--expandable { + min-width: 32px; + width: auto; + min-height: 32px; } + .mdl-textfield--expandable .mdl-button--icon { + top: 16px; } + +.mdl-textfield__input { + border: none; + border-bottom: 1px solid rgba(0,0,0, 0.12); + display: block; + font-size: 16px; + font-family: "Helvetica", "Arial", sans-serif; + margin: 0; + padding: 4px 0; + width: 100%; + background: none; + text-align: left; + color: inherit; } + .mdl-textfield__input[type="number"] { + -moz-appearance: textfield; } + .mdl-textfield__input[type="number"]::-webkit-inner-spin-button, .mdl-textfield__input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; } + .mdl-textfield.is-focused .mdl-textfield__input { + outline: none; } + .mdl-textfield.is-invalid .mdl-textfield__input { + border-color: rgb(213,0,0); + box-shadow: none; } + fieldset[disabled] .mdl-textfield .mdl-textfield__input, + .mdl-textfield.is-disabled .mdl-textfield__input { + background-color: transparent; + border-bottom: 1px dotted rgba(0,0,0, 0.12); + color: rgba(0,0,0, 0.26); } + +.mdl-textfield textarea.mdl-textfield__input { + display: block; } + +.mdl-textfield__label { + bottom: 0; + color: rgba(0,0,0, 0.26); + font-size: 16px; + left: 0; + right: 0; + pointer-events: none; + position: absolute; + display: block; + top: 24px; + width: 100%; + overflow: hidden; + white-space: nowrap; + text-align: left; } + .mdl-textfield.is-dirty .mdl-textfield__label, + .mdl-textfield.has-placeholder .mdl-textfield__label { + visibility: hidden; } + .mdl-textfield--floating-label .mdl-textfield__label { + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); } + .mdl-textfield--floating-label.has-placeholder .mdl-textfield__label { + transition: none; } + fieldset[disabled] .mdl-textfield .mdl-textfield__label, + .mdl-textfield.is-disabled.is-disabled .mdl-textfield__label { + color: rgba(0,0,0, 0.26); } + .mdl-textfield--floating-label.is-focused .mdl-textfield__label, + .mdl-textfield--floating-label.is-dirty .mdl-textfield__label, + .mdl-textfield--floating-label.has-placeholder .mdl-textfield__label { + color: rgb(63,81,181); + font-size: 12px; + top: 4px; + visibility: visible; } + .mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label, + .mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label, + .mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label { + top: -16px; } + .mdl-textfield--floating-label.is-invalid .mdl-textfield__label { + color: rgb(213,0,0); + font-size: 12px; } + .mdl-textfield__label:after { + background-color: rgb(63,81,181); + bottom: 20px; + content: ''; + height: 2px; + left: 45%; + position: absolute; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + visibility: hidden; + width: 10px; } + .mdl-textfield.is-focused .mdl-textfield__label:after { + left: 0; + visibility: visible; + width: 100%; } + .mdl-textfield.is-invalid .mdl-textfield__label:after { + background-color: rgb(213,0,0); } + +.mdl-textfield__error { + color: rgb(213,0,0); + position: absolute; + font-size: 12px; + margin-top: 3px; + visibility: hidden; + display: block; } + .mdl-textfield.is-invalid .mdl-textfield__error { + visibility: visible; } + +.mdl-textfield__expandable-holder { + display: inline-block; + position: relative; + margin-left: 32px; + transition-duration: 0.2s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + display: inline-block; + max-width: 0.1px; } + .mdl-textfield.is-focused .mdl-textfield__expandable-holder, .mdl-textfield.is-dirty .mdl-textfield__expandable-holder { + max-width: 600px; } + .mdl-textfield__expandable-holder .mdl-textfield__label:after { + bottom: 0; } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-tooltip { + -webkit-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: top center; + transform-origin: top center; + z-index: 999; + background: rgba(97,97,97, 0.9); + border-radius: 2px; + color: rgb(255,255,255); + display: inline-block; + font-size: 10px; + font-weight: 500; + line-height: 14px; + max-width: 170px; + position: fixed; + top: -500px; + left: -500px; + padding: 8px; + text-align: center; } + +.mdl-tooltip.is-active { + -webkit-animation: pulse 200ms cubic-bezier(0, 0, 0.2, 1) forwards; + animation: pulse 200ms cubic-bezier(0, 0, 0.2, 1) forwards; } + +.mdl-tooltip--large { + line-height: 14px; + font-size: 14px; + padding: 16px; } + +@-webkit-keyframes pulse { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0; } + 50% { + -webkit-transform: scale(0.99); + transform: scale(0.99); } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; + visibility: visible; } } + +@keyframes pulse { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0; } + 50% { + -webkit-transform: scale(0.99); + transform: scale(0.99); } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; + visibility: visible; } } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* Typography */ +/* Shadows */ +/* Animations */ +/* Dialog */ +.mdl-shadow--2dp { + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 1px 5px 0 rgba(0, 0, 0, 0.12); } + +.mdl-shadow--3dp { + box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), 0 3px 3px -2px rgba(0, 0, 0, 0.2), 0 1px 8px 0 rgba(0, 0, 0, 0.12); } + +.mdl-shadow--4dp { + box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.2); } + +.mdl-shadow--6dp { + box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.2); } + +.mdl-shadow--8dp { + box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); } + +.mdl-shadow--16dp { + box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.2); } + +.mdl-shadow--24dp { + box-shadow: 0 9px 46px 8px rgba(0, 0, 0, 0.14), 0 11px 15px -7px rgba(0, 0, 0, 0.12), 0 24px 38px 3px rgba(0, 0, 0, 0.2); } + +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* +* NOTE: Some rules here are applied using duplicate selectors. +* This is on purpose to increase their specificity when applied. +* For example: `.mdl-cell--1-col-phone.mdl-cell--1-col-phone` +*/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/*------------------------------------* $CONTENTS +\*------------------------------------*/ +/** + * STYLE GUIDE VARIABLES------------------Declarations of Sass variables + * -----Typography + * -----Colors + * -----Textfield + * -----Switch + * -----Spinner + * -----Radio + * -----Menu + * -----List + * -----Layout + * -----Icon toggles + * -----Footer + * -----Column + * -----Checkbox + * -----Card + * -----Button + * -----Animation + * -----Progress + * -----Badge + * -----Shadows + * -----Grid + * -----Data table + * -----Dialog + * -----Snackbar + * -----Tooltip + * -----Chip + * + * Even though all variables have the `!default` directive, most of them + * should not be changed as they are dependent one another. This can cause + * visual distortions (like alignment issues) that are hard to track down + * and fix. + */ +/* ========== TYPOGRAPHY ========== */ +/* We're splitting fonts into "preferred" and "performance" in order to optimize + page loading. For important text, such as the body, we want it to load + immediately and not wait for the web font load, whereas for other sections, + such as headers and titles, we're OK with things taking a bit longer to load. + We do have some optional classes and parameters in the mixins, in case you + definitely want to make sure you're using the preferred font and don't mind + the performance hit. + We should be able to improve on this once CSS Font Loading L3 becomes more + widely available. +*/ +/* ========== COLORS ========== */ +/** +* +* Material design color palettes. +* @see http://www.google.com/design/spec/style/color.html +* +**/ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== Color Palettes ========== */ +/* colors.scss */ +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* ========== IMAGES ========== */ +/* ========== Color & Themes ========== */ +/* ========== Typography ========== */ +/* ========== Components ========== */ +/* ========== Standard Buttons ========== */ +/* ========== Icon Toggles ========== */ +/* ========== Radio Buttons ========== */ +/* ========== Ripple effect ========== */ +/* ========== Layout ========== */ +/* ========== Content Tabs ========== */ +/* ========== Checkboxes ========== */ +/* ========== Switches ========== */ +/* ========== Spinner ========== */ +/* ========== Text fields ========== */ +/* ========== Card ========== */ +/* ========== Sliders ========== */ +/* ========== Progress ========== */ +/* ========== List ========== */ +/* ========== Item ========== */ +/* ========== Dropdown menu ========== */ +/* ========== Tooltips ========== */ +/* ========== Footer ========== */ +/* TEXTFIELD */ +/* SWITCH */ +/* SPINNER */ +/* RADIO */ +/* MENU */ +/* LIST */ +/* LAYOUT */ +/* ICON TOGGLE */ +/* FOOTER */ +/*mega-footer*/ +/*mini-footer*/ +/* CHECKBOX */ +/* CARD */ +/* Card dimensions */ +/* Cover image */ +/* BUTTON */ +/** + * + * Dimensions + * + */ +/* ANIMATION */ +/* PROGRESS */ +/* BADGE */ +/* SHADOWS */ +/* GRID */ +/* DATA TABLE */ +/* DIALOG */ +/* SNACKBAR */ +/* TOOLTIP */ +/* CHIP */ +.mdl-grid { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row wrap; + -ms-flex-flow: row wrap; + flex-flow: row wrap; + margin: 0 auto 0 auto; + -webkit-align-items: stretch; + -ms-flex-align: stretch; + align-items: stretch; } + .mdl-grid.mdl-grid--no-spacing { + padding: 0; } + +.mdl-cell { + box-sizing: border-box; } + +.mdl-cell--top { + -webkit-align-self: flex-start; + -ms-flex-item-align: start; + align-self: flex-start; } + +.mdl-cell--middle { + -webkit-align-self: center; + -ms-flex-item-align: center; + -ms-grid-row-align: center; + align-self: center; } + +.mdl-cell--bottom { + -webkit-align-self: flex-end; + -ms-flex-item-align: end; + align-self: flex-end; } + +.mdl-cell--stretch { + -webkit-align-self: stretch; + -ms-flex-item-align: stretch; + -ms-grid-row-align: stretch; + align-self: stretch; } + +.mdl-grid.mdl-grid--no-spacing > .mdl-cell { + margin: 0; } + +.mdl-cell--order-1 { + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + +.mdl-cell--order-2 { + -webkit-order: 2; + -ms-flex-order: 2; + order: 2; } + +.mdl-cell--order-3 { + -webkit-order: 3; + -ms-flex-order: 3; + order: 3; } + +.mdl-cell--order-4 { + -webkit-order: 4; + -ms-flex-order: 4; + order: 4; } + +.mdl-cell--order-5 { + -webkit-order: 5; + -ms-flex-order: 5; + order: 5; } + +.mdl-cell--order-6 { + -webkit-order: 6; + -ms-flex-order: 6; + order: 6; } + +.mdl-cell--order-7 { + -webkit-order: 7; + -ms-flex-order: 7; + order: 7; } + +.mdl-cell--order-8 { + -webkit-order: 8; + -ms-flex-order: 8; + order: 8; } + +.mdl-cell--order-9 { + -webkit-order: 9; + -ms-flex-order: 9; + order: 9; } + +.mdl-cell--order-10 { + -webkit-order: 10; + -ms-flex-order: 10; + order: 10; } + +.mdl-cell--order-11 { + -webkit-order: 11; + -ms-flex-order: 11; + order: 11; } + +.mdl-cell--order-12 { + -webkit-order: 12; + -ms-flex-order: 12; + order: 12; } + +@media (max-width: 479px) { + .mdl-grid { + padding: 8px; } + .mdl-cell { + margin: 8px; + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell { + width: 100%; } + .mdl-cell--hide-phone { + display: none !important; } + .mdl-cell--order-1-phone.mdl-cell--order-1-phone { + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + .mdl-cell--order-2-phone.mdl-cell--order-2-phone { + -webkit-order: 2; + -ms-flex-order: 2; + order: 2; } + .mdl-cell--order-3-phone.mdl-cell--order-3-phone { + -webkit-order: 3; + -ms-flex-order: 3; + order: 3; } + .mdl-cell--order-4-phone.mdl-cell--order-4-phone { + -webkit-order: 4; + -ms-flex-order: 4; + order: 4; } + .mdl-cell--order-5-phone.mdl-cell--order-5-phone { + -webkit-order: 5; + -ms-flex-order: 5; + order: 5; } + .mdl-cell--order-6-phone.mdl-cell--order-6-phone { + -webkit-order: 6; + -ms-flex-order: 6; + order: 6; } + .mdl-cell--order-7-phone.mdl-cell--order-7-phone { + -webkit-order: 7; + -ms-flex-order: 7; + order: 7; } + .mdl-cell--order-8-phone.mdl-cell--order-8-phone { + -webkit-order: 8; + -ms-flex-order: 8; + order: 8; } + .mdl-cell--order-9-phone.mdl-cell--order-9-phone { + -webkit-order: 9; + -ms-flex-order: 9; + order: 9; } + .mdl-cell--order-10-phone.mdl-cell--order-10-phone { + -webkit-order: 10; + -ms-flex-order: 10; + order: 10; } + .mdl-cell--order-11-phone.mdl-cell--order-11-phone { + -webkit-order: 11; + -ms-flex-order: 11; + order: 11; } + .mdl-cell--order-12-phone.mdl-cell--order-12-phone { + -webkit-order: 12; + -ms-flex-order: 12; + order: 12; } + .mdl-cell--1-col, + .mdl-cell--1-col-phone.mdl-cell--1-col-phone { + width: calc(25% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--1-col, .mdl-grid--no-spacing > + .mdl-cell--1-col-phone.mdl-cell--1-col-phone { + width: 25%; } + .mdl-cell--2-col, + .mdl-cell--2-col-phone.mdl-cell--2-col-phone { + width: calc(50% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--2-col, .mdl-grid--no-spacing > + .mdl-cell--2-col-phone.mdl-cell--2-col-phone { + width: 50%; } + .mdl-cell--3-col, + .mdl-cell--3-col-phone.mdl-cell--3-col-phone { + width: calc(75% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--3-col, .mdl-grid--no-spacing > + .mdl-cell--3-col-phone.mdl-cell--3-col-phone { + width: 75%; } + .mdl-cell--4-col, + .mdl-cell--4-col-phone.mdl-cell--4-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--4-col, .mdl-grid--no-spacing > + .mdl-cell--4-col-phone.mdl-cell--4-col-phone { + width: 100%; } + .mdl-cell--5-col, + .mdl-cell--5-col-phone.mdl-cell--5-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--5-col, .mdl-grid--no-spacing > + .mdl-cell--5-col-phone.mdl-cell--5-col-phone { + width: 100%; } + .mdl-cell--6-col, + .mdl-cell--6-col-phone.mdl-cell--6-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--6-col, .mdl-grid--no-spacing > + .mdl-cell--6-col-phone.mdl-cell--6-col-phone { + width: 100%; } + .mdl-cell--7-col, + .mdl-cell--7-col-phone.mdl-cell--7-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--7-col, .mdl-grid--no-spacing > + .mdl-cell--7-col-phone.mdl-cell--7-col-phone { + width: 100%; } + .mdl-cell--8-col, + .mdl-cell--8-col-phone.mdl-cell--8-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--8-col, .mdl-grid--no-spacing > + .mdl-cell--8-col-phone.mdl-cell--8-col-phone { + width: 100%; } + .mdl-cell--9-col, + .mdl-cell--9-col-phone.mdl-cell--9-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--9-col, .mdl-grid--no-spacing > + .mdl-cell--9-col-phone.mdl-cell--9-col-phone { + width: 100%; } + .mdl-cell--10-col, + .mdl-cell--10-col-phone.mdl-cell--10-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--10-col, .mdl-grid--no-spacing > + .mdl-cell--10-col-phone.mdl-cell--10-col-phone { + width: 100%; } + .mdl-cell--11-col, + .mdl-cell--11-col-phone.mdl-cell--11-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--11-col, .mdl-grid--no-spacing > + .mdl-cell--11-col-phone.mdl-cell--11-col-phone { + width: 100%; } + .mdl-cell--12-col, + .mdl-cell--12-col-phone.mdl-cell--12-col-phone { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--12-col, .mdl-grid--no-spacing > + .mdl-cell--12-col-phone.mdl-cell--12-col-phone { + width: 100%; } + .mdl-cell--1-offset, + .mdl-cell--1-offset-phone.mdl-cell--1-offset-phone { + margin-left: calc(25% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--1-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--1-offset-phone.mdl-cell--1-offset-phone { + margin-left: 25%; } + .mdl-cell--2-offset, + .mdl-cell--2-offset-phone.mdl-cell--2-offset-phone { + margin-left: calc(50% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--2-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--2-offset-phone.mdl-cell--2-offset-phone { + margin-left: 50%; } + .mdl-cell--3-offset, + .mdl-cell--3-offset-phone.mdl-cell--3-offset-phone { + margin-left: calc(75% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--3-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--3-offset-phone.mdl-cell--3-offset-phone { + margin-left: 75%; } } + +@media (min-width: 480px) and (max-width: 839px) { + .mdl-grid { + padding: 8px; } + .mdl-cell { + margin: 8px; + width: calc(50% - 16px); } + .mdl-grid--no-spacing > .mdl-cell { + width: 50%; } + .mdl-cell--hide-tablet { + display: none !important; } + .mdl-cell--order-1-tablet.mdl-cell--order-1-tablet { + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + .mdl-cell--order-2-tablet.mdl-cell--order-2-tablet { + -webkit-order: 2; + -ms-flex-order: 2; + order: 2; } + .mdl-cell--order-3-tablet.mdl-cell--order-3-tablet { + -webkit-order: 3; + -ms-flex-order: 3; + order: 3; } + .mdl-cell--order-4-tablet.mdl-cell--order-4-tablet { + -webkit-order: 4; + -ms-flex-order: 4; + order: 4; } + .mdl-cell--order-5-tablet.mdl-cell--order-5-tablet { + -webkit-order: 5; + -ms-flex-order: 5; + order: 5; } + .mdl-cell--order-6-tablet.mdl-cell--order-6-tablet { + -webkit-order: 6; + -ms-flex-order: 6; + order: 6; } + .mdl-cell--order-7-tablet.mdl-cell--order-7-tablet { + -webkit-order: 7; + -ms-flex-order: 7; + order: 7; } + .mdl-cell--order-8-tablet.mdl-cell--order-8-tablet { + -webkit-order: 8; + -ms-flex-order: 8; + order: 8; } + .mdl-cell--order-9-tablet.mdl-cell--order-9-tablet { + -webkit-order: 9; + -ms-flex-order: 9; + order: 9; } + .mdl-cell--order-10-tablet.mdl-cell--order-10-tablet { + -webkit-order: 10; + -ms-flex-order: 10; + order: 10; } + .mdl-cell--order-11-tablet.mdl-cell--order-11-tablet { + -webkit-order: 11; + -ms-flex-order: 11; + order: 11; } + .mdl-cell--order-12-tablet.mdl-cell--order-12-tablet { + -webkit-order: 12; + -ms-flex-order: 12; + order: 12; } + .mdl-cell--1-col, + .mdl-cell--1-col-tablet.mdl-cell--1-col-tablet { + width: calc(12.5% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--1-col, .mdl-grid--no-spacing > + .mdl-cell--1-col-tablet.mdl-cell--1-col-tablet { + width: 12.5%; } + .mdl-cell--2-col, + .mdl-cell--2-col-tablet.mdl-cell--2-col-tablet { + width: calc(25% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--2-col, .mdl-grid--no-spacing > + .mdl-cell--2-col-tablet.mdl-cell--2-col-tablet { + width: 25%; } + .mdl-cell--3-col, + .mdl-cell--3-col-tablet.mdl-cell--3-col-tablet { + width: calc(37.5% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--3-col, .mdl-grid--no-spacing > + .mdl-cell--3-col-tablet.mdl-cell--3-col-tablet { + width: 37.5%; } + .mdl-cell--4-col, + .mdl-cell--4-col-tablet.mdl-cell--4-col-tablet { + width: calc(50% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--4-col, .mdl-grid--no-spacing > + .mdl-cell--4-col-tablet.mdl-cell--4-col-tablet { + width: 50%; } + .mdl-cell--5-col, + .mdl-cell--5-col-tablet.mdl-cell--5-col-tablet { + width: calc(62.5% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--5-col, .mdl-grid--no-spacing > + .mdl-cell--5-col-tablet.mdl-cell--5-col-tablet { + width: 62.5%; } + .mdl-cell--6-col, + .mdl-cell--6-col-tablet.mdl-cell--6-col-tablet { + width: calc(75% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--6-col, .mdl-grid--no-spacing > + .mdl-cell--6-col-tablet.mdl-cell--6-col-tablet { + width: 75%; } + .mdl-cell--7-col, + .mdl-cell--7-col-tablet.mdl-cell--7-col-tablet { + width: calc(87.5% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--7-col, .mdl-grid--no-spacing > + .mdl-cell--7-col-tablet.mdl-cell--7-col-tablet { + width: 87.5%; } + .mdl-cell--8-col, + .mdl-cell--8-col-tablet.mdl-cell--8-col-tablet { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--8-col, .mdl-grid--no-spacing > + .mdl-cell--8-col-tablet.mdl-cell--8-col-tablet { + width: 100%; } + .mdl-cell--9-col, + .mdl-cell--9-col-tablet.mdl-cell--9-col-tablet { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--9-col, .mdl-grid--no-spacing > + .mdl-cell--9-col-tablet.mdl-cell--9-col-tablet { + width: 100%; } + .mdl-cell--10-col, + .mdl-cell--10-col-tablet.mdl-cell--10-col-tablet { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--10-col, .mdl-grid--no-spacing > + .mdl-cell--10-col-tablet.mdl-cell--10-col-tablet { + width: 100%; } + .mdl-cell--11-col, + .mdl-cell--11-col-tablet.mdl-cell--11-col-tablet { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--11-col, .mdl-grid--no-spacing > + .mdl-cell--11-col-tablet.mdl-cell--11-col-tablet { + width: 100%; } + .mdl-cell--12-col, + .mdl-cell--12-col-tablet.mdl-cell--12-col-tablet { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--12-col, .mdl-grid--no-spacing > + .mdl-cell--12-col-tablet.mdl-cell--12-col-tablet { + width: 100%; } + .mdl-cell--1-offset, + .mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet { + margin-left: calc(12.5% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--1-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet { + margin-left: 12.5%; } + .mdl-cell--2-offset, + .mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet { + margin-left: calc(25% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--2-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet { + margin-left: 25%; } + .mdl-cell--3-offset, + .mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet { + margin-left: calc(37.5% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--3-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet { + margin-left: 37.5%; } + .mdl-cell--4-offset, + .mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet { + margin-left: calc(50% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--4-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet { + margin-left: 50%; } + .mdl-cell--5-offset, + .mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet { + margin-left: calc(62.5% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--5-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet { + margin-left: 62.5%; } + .mdl-cell--6-offset, + .mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet { + margin-left: calc(75% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--6-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet { + margin-left: 75%; } + .mdl-cell--7-offset, + .mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet { + margin-left: calc(87.5% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--7-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet { + margin-left: 87.5%; } } + +@media (min-width: 840px) { + .mdl-grid { + padding: 8px; } + .mdl-cell { + margin: 8px; + width: calc(33.3333333333% - 16px); } + .mdl-grid--no-spacing > .mdl-cell { + width: 33.3333333333%; } + .mdl-cell--hide-desktop { + display: none !important; } + .mdl-cell--order-1-desktop.mdl-cell--order-1-desktop { + -webkit-order: 1; + -ms-flex-order: 1; + order: 1; } + .mdl-cell--order-2-desktop.mdl-cell--order-2-desktop { + -webkit-order: 2; + -ms-flex-order: 2; + order: 2; } + .mdl-cell--order-3-desktop.mdl-cell--order-3-desktop { + -webkit-order: 3; + -ms-flex-order: 3; + order: 3; } + .mdl-cell--order-4-desktop.mdl-cell--order-4-desktop { + -webkit-order: 4; + -ms-flex-order: 4; + order: 4; } + .mdl-cell--order-5-desktop.mdl-cell--order-5-desktop { + -webkit-order: 5; + -ms-flex-order: 5; + order: 5; } + .mdl-cell--order-6-desktop.mdl-cell--order-6-desktop { + -webkit-order: 6; + -ms-flex-order: 6; + order: 6; } + .mdl-cell--order-7-desktop.mdl-cell--order-7-desktop { + -webkit-order: 7; + -ms-flex-order: 7; + order: 7; } + .mdl-cell--order-8-desktop.mdl-cell--order-8-desktop { + -webkit-order: 8; + -ms-flex-order: 8; + order: 8; } + .mdl-cell--order-9-desktop.mdl-cell--order-9-desktop { + -webkit-order: 9; + -ms-flex-order: 9; + order: 9; } + .mdl-cell--order-10-desktop.mdl-cell--order-10-desktop { + -webkit-order: 10; + -ms-flex-order: 10; + order: 10; } + .mdl-cell--order-11-desktop.mdl-cell--order-11-desktop { + -webkit-order: 11; + -ms-flex-order: 11; + order: 11; } + .mdl-cell--order-12-desktop.mdl-cell--order-12-desktop { + -webkit-order: 12; + -ms-flex-order: 12; + order: 12; } + .mdl-cell--1-col, + .mdl-cell--1-col-desktop.mdl-cell--1-col-desktop { + width: calc(8.3333333333% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--1-col, .mdl-grid--no-spacing > + .mdl-cell--1-col-desktop.mdl-cell--1-col-desktop { + width: 8.3333333333%; } + .mdl-cell--2-col, + .mdl-cell--2-col-desktop.mdl-cell--2-col-desktop { + width: calc(16.6666666667% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--2-col, .mdl-grid--no-spacing > + .mdl-cell--2-col-desktop.mdl-cell--2-col-desktop { + width: 16.6666666667%; } + .mdl-cell--3-col, + .mdl-cell--3-col-desktop.mdl-cell--3-col-desktop { + width: calc(25% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--3-col, .mdl-grid--no-spacing > + .mdl-cell--3-col-desktop.mdl-cell--3-col-desktop { + width: 25%; } + .mdl-cell--4-col, + .mdl-cell--4-col-desktop.mdl-cell--4-col-desktop { + width: calc(33.3333333333% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--4-col, .mdl-grid--no-spacing > + .mdl-cell--4-col-desktop.mdl-cell--4-col-desktop { + width: 33.3333333333%; } + .mdl-cell--5-col, + .mdl-cell--5-col-desktop.mdl-cell--5-col-desktop { + width: calc(41.6666666667% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--5-col, .mdl-grid--no-spacing > + .mdl-cell--5-col-desktop.mdl-cell--5-col-desktop { + width: 41.6666666667%; } + .mdl-cell--6-col, + .mdl-cell--6-col-desktop.mdl-cell--6-col-desktop { + width: calc(50% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--6-col, .mdl-grid--no-spacing > + .mdl-cell--6-col-desktop.mdl-cell--6-col-desktop { + width: 50%; } + .mdl-cell--7-col, + .mdl-cell--7-col-desktop.mdl-cell--7-col-desktop { + width: calc(58.3333333333% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--7-col, .mdl-grid--no-spacing > + .mdl-cell--7-col-desktop.mdl-cell--7-col-desktop { + width: 58.3333333333%; } + .mdl-cell--8-col, + .mdl-cell--8-col-desktop.mdl-cell--8-col-desktop { + width: calc(66.6666666667% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--8-col, .mdl-grid--no-spacing > + .mdl-cell--8-col-desktop.mdl-cell--8-col-desktop { + width: 66.6666666667%; } + .mdl-cell--9-col, + .mdl-cell--9-col-desktop.mdl-cell--9-col-desktop { + width: calc(75% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--9-col, .mdl-grid--no-spacing > + .mdl-cell--9-col-desktop.mdl-cell--9-col-desktop { + width: 75%; } + .mdl-cell--10-col, + .mdl-cell--10-col-desktop.mdl-cell--10-col-desktop { + width: calc(83.3333333333% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--10-col, .mdl-grid--no-spacing > + .mdl-cell--10-col-desktop.mdl-cell--10-col-desktop { + width: 83.3333333333%; } + .mdl-cell--11-col, + .mdl-cell--11-col-desktop.mdl-cell--11-col-desktop { + width: calc(91.6666666667% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--11-col, .mdl-grid--no-spacing > + .mdl-cell--11-col-desktop.mdl-cell--11-col-desktop { + width: 91.6666666667%; } + .mdl-cell--12-col, + .mdl-cell--12-col-desktop.mdl-cell--12-col-desktop { + width: calc(100% - 16px); } + .mdl-grid--no-spacing > .mdl-cell--12-col, .mdl-grid--no-spacing > + .mdl-cell--12-col-desktop.mdl-cell--12-col-desktop { + width: 100%; } + .mdl-cell--1-offset, + .mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop { + margin-left: calc(8.3333333333% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--1-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop { + margin-left: 8.3333333333%; } + .mdl-cell--2-offset, + .mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop { + margin-left: calc(16.6666666667% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--2-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop { + margin-left: 16.6666666667%; } + .mdl-cell--3-offset, + .mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop { + margin-left: calc(25% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--3-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop { + margin-left: 25%; } + .mdl-cell--4-offset, + .mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop { + margin-left: calc(33.3333333333% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--4-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop { + margin-left: 33.3333333333%; } + .mdl-cell--5-offset, + .mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop { + margin-left: calc(41.6666666667% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--5-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop { + margin-left: 41.6666666667%; } + .mdl-cell--6-offset, + .mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop { + margin-left: calc(50% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--6-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop { + margin-left: 50%; } + .mdl-cell--7-offset, + .mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop { + margin-left: calc(58.3333333333% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--7-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop { + margin-left: 58.3333333333%; } + .mdl-cell--8-offset, + .mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop { + margin-left: calc(66.6666666667% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--8-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop { + margin-left: 66.6666666667%; } + .mdl-cell--9-offset, + .mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop { + margin-left: calc(75% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--9-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop { + margin-left: 75%; } + .mdl-cell--10-offset, + .mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop { + margin-left: calc(83.3333333333% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--10-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop { + margin-left: 83.3333333333%; } + .mdl-cell--11-offset, + .mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop { + margin-left: calc(91.6666666667% + 8px); } + .mdl-grid.mdl-grid--no-spacing > .mdl-cell--11-offset, .mdl-grid.mdl-grid--no-spacing > + .mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop { + margin-left: 91.6666666667%; } } diff --git a/public/vendor/redoc/index.html b/public/vendor/redoc/index.html new file mode 100644 index 0000000000..f3fa0dd0a2 --- /dev/null +++ b/public/vendor/redoc/index.html @@ -0,0 +1,23 @@ + + + + ReDoc + + + + + + + + + + + + \ No newline at end of file diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000000..e85c752352 --- /dev/null +++ b/renovate.json @@ -0,0 +1,23 @@ +{ + "extends": [ + "config:base" + ], + "baseBranches": ["develop"], + "labels": ["dependencies"], + "packageRules": [ + { + "matchDepTypes": ["dependencies"], + "excludePackageNames": ["colors"], + "rangeStrategy": "pin" + }, + { + "matchDepTypes": ["devDependencies"], + "automerge": true, + "rangeStrategy": "pin" + }, + { + "matchDepTypes": ["engines"], + "rangeStrategy": "auto" + } + ] +} diff --git a/require-main.js b/require-main.js new file mode 100644 index 0000000000..b062186b7d --- /dev/null +++ b/require-main.js @@ -0,0 +1,10 @@ +'use strict'; + +// this forces `require.main.require` to always be relative to this directory +// this allows plugins to use `require.main.require` to reference NodeBB modules +// without worrying about multiple parent modules +if (require.main !== module) { + require.main.require = function (path) { + return require(path); + }; +} diff --git a/src/admin/search.js b/src/admin/search.js new file mode 100644 index 0000000000..e15b920bc8 --- /dev/null +++ b/src/admin/search.js @@ -0,0 +1,142 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const sanitizeHTML = require('sanitize-html'); +const nconf = require('nconf'); +const winston = require('winston'); + +const file = require('../file'); +const { Translator } = require('../translator'); + +function filterDirectories(directories) { + return directories.map( + // get the relative path + // convert dir to use forward slashes + dir => dir.replace(/^.*(admin.*?).tpl$/, '$1').split(path.sep).join('/') + ).filter( + // exclude .js files + // exclude partials + // only include subpaths + // exclude category.tpl, group.tpl, category-analytics.tpl + dir => ( + !dir.endsWith('.js') && + !dir.includes('/partials/') && + /\/.*\//.test(dir) && + !/manage\/(category|group|category-analytics)$/.test(dir) + ) + ); +} + +async function getAdminNamespaces() { + const directories = await file.walk(path.resolve(nconf.get('views_dir'), 'admin')); + return filterDirectories(directories); +} + +function sanitize(html) { + // reduce the template to just meaningful text + // remove all tags and strip out scripts, etc completely + return sanitizeHTML(html, { + allowedTags: [], + allowedAttributes: [], + }); +} + +function simplify(translations) { + return translations + // remove all mustaches + .replace(/(?:\{{1,2}[^}]*?\}{1,2})/g, '') + // collapse whitespace + .replace(/(?:[ \t]*[\n\r]+[ \t]*)+/g, '\n') + .replace(/[\t ]+/g, ' '); +} + +function nsToTitle(namespace) { + return namespace.replace('admin/', '').split('/').map(str => str[0].toUpperCase() + str.slice(1)).join(' > ') + .replace(/[^a-zA-Z> ]/g, ' '); +} + +const fallbackCache = {}; + +async function initFallback(namespace) { + const template = await fs.promises.readFile(path.resolve(nconf.get('views_dir'), `${namespace}.tpl`), 'utf8'); + + const title = nsToTitle(namespace); + let translations = sanitize(template); + translations = Translator.removePatterns(translations); + translations = simplify(translations); + translations += `\n${title}`; + + return { + namespace: namespace, + translations: translations, + title: title, + }; +} + +async function fallback(namespace) { + if (fallbackCache[namespace]) { + return fallbackCache[namespace]; + } + + const params = await initFallback(namespace); + fallbackCache[namespace] = params; + return params; +} + +async function initDict(language) { + const namespaces = await getAdminNamespaces(); + return await Promise.all(namespaces.map(ns => buildNamespace(language, ns))); +} + +async function buildNamespace(language, namespace) { + const translator = Translator.create(language); + try { + const translations = await translator.getTranslation(namespace); + if (!translations || !Object.keys(translations).length) { + return await fallback(namespace); + } + // join all translations into one string separated by newlines + let str = Object.keys(translations).map(key => translations[key]).join('\n'); + str = sanitize(str); + + let title = namespace; + title = title.match(/admin\/(.+?)\/(.+?)$/); + title = `[[admin/menu:section-${ + title[1] === 'development' ? 'advanced' : title[1] + }]]${title[2] ? (` > [[admin/menu:${ + title[1]}/${title[2]}]]`) : ''}`; + + title = await translator.translate(title); + return { + namespace: namespace, + translations: `${str}\n${title}`, + title: title, + }; + } catch (err) { + winston.error(err.stack); + return { + namespace: namespace, + translations: '', + }; + } +} + +const cache = {}; + +async function getDictionary(language) { + if (cache[language]) { + return cache[language]; + } + + const params = await initDict(language); + cache[language] = params; + return params; +} + +module.exports.getDictionary = getDictionary; +module.exports.filterDirectories = filterDirectories; +module.exports.simplify = simplify; +module.exports.sanitize = sanitize; + +require('../promisify')(module.exports); diff --git a/src/admin/versions.js b/src/admin/versions.js new file mode 100644 index 0000000000..b906b6e797 --- /dev/null +++ b/src/admin/versions.js @@ -0,0 +1,52 @@ +'use strict'; + +const request = require('request'); + +const meta = require('../meta'); + +let versionCache = ''; +let versionCacheLastModified = ''; + +const isPrerelease = /^v?\d+\.\d+\.\d+-.+$/; + +function getLatestVersion(callback) { + const headers = { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': encodeURIComponent(`NodeBB Admin Control Panel/${meta.config.title}`), + }; + + if (versionCacheLastModified) { + headers['If-Modified-Since'] = versionCacheLastModified; + } + + request('https://api.github.com/repos/NodeBB/NodeBB/releases/latest', { + json: true, + headers: headers, + timeout: 2000, + }, (err, res, latestRelease) => { + if (err) { + return callback(err); + } + + if (res.statusCode === 304) { + return callback(null, versionCache); + } + + if (res.statusCode !== 200) { + return callback(new Error(res.statusMessage)); + } + + if (!latestRelease || !latestRelease.tag_name) { + return callback(new Error('[[error:cant-get-latest-release]]')); + } + const tagName = latestRelease.tag_name.replace(/^v/, ''); + versionCache = tagName; + versionCacheLastModified = res.headers['last-modified']; + callback(null, versionCache); + }); +} + +exports.getLatestVersion = getLatestVersion; +exports.isPrerelease = isPrerelease; + +require('../promisify')(exports); diff --git a/src/als.js b/src/als.js new file mode 100644 index 0000000000..a3aec0220f --- /dev/null +++ b/src/als.js @@ -0,0 +1,7 @@ +'use strict'; + +const { AsyncLocalStorage } = require('async_hooks'); + +const asyncLocalStorage = new AsyncLocalStorage(); + +module.exports = asyncLocalStorage; diff --git a/src/analytics.js b/src/analytics.js new file mode 100644 index 0000000000..6cfe293855 --- /dev/null +++ b/src/analytics.js @@ -0,0 +1,301 @@ +'use strict'; + +const cronJob = require('cron').CronJob; +const winston = require('winston'); +const nconf = require('nconf'); +const crypto = require('crypto'); +const util = require('util'); +const _ = require('lodash'); + +const sleep = util.promisify(setTimeout); + +const db = require('./database'); +const utils = require('./utils'); +const plugins = require('./plugins'); +const meta = require('./meta'); +const pubsub = require('./pubsub'); +const cacheCreate = require('./cache/lru'); + +const Analytics = module.exports; + +const secret = nconf.get('secret'); + +let local = { + counters: {}, + pageViews: 0, + pageViewsRegistered: 0, + pageViewsGuest: 0, + pageViewsBot: 0, + uniqueIPCount: 0, + uniquevisitors: 0, +}; +const empty = _.cloneDeep(local); +const total = _.cloneDeep(local); + +let ipCache; + +const runJobs = nconf.get('runJobs'); + +Analytics.init = async function () { + ipCache = cacheCreate({ + max: parseInt(meta.config['analytics:maxCache'], 10) || 500, + ttl: 0, + }); + + new cronJob('*/10 * * * * *', (async () => { + publishLocalAnalytics(); + if (runJobs) { + await sleep(2000); + await Analytics.writeData(); + } + }), null, true); + + if (runJobs) { + pubsub.on('analytics:publish', (data) => { + incrementProperties(total, data.local); + }); + } +}; + +function publishLocalAnalytics() { + pubsub.publish('analytics:publish', { + local: local, + }); + local = _.cloneDeep(empty); +} + +function incrementProperties(obj1, obj2) { + for (const [key, value] of Object.entries(obj2)) { + if (typeof value === 'object') { + incrementProperties(obj1[key], value); + } else if (utils.isNumber(value)) { + obj1[key] = obj1[key] || 0; + obj1[key] += obj2[key]; + } + } +} + +Analytics.increment = function (keys, callback) { + keys = Array.isArray(keys) ? keys : [keys]; + + plugins.hooks.fire('action:analytics.increment', { keys: keys }); + + keys.forEach((key) => { + local.counters[key] = local.counters[key] || 0; + local.counters[key] += 1; + }); + + if (typeof callback === 'function') { + callback(); + } +}; + +Analytics.getKeys = async () => db.getSortedSetRange('analyticsKeys', 0, -1); + +Analytics.pageView = async function (payload) { + local.pageViews += 1; + + if (payload.uid > 0) { + local.pageViewsRegistered += 1; + } else if (payload.uid < 0) { + local.pageViewsBot += 1; + } else { + local.pageViewsGuest += 1; + } + + if (payload.ip) { + // Retrieve hash or calculate if not present + let hash = ipCache.get(payload.ip + secret); + if (!hash) { + hash = crypto.createHash('sha1').update(payload.ip + secret).digest('hex'); + ipCache.set(payload.ip + secret, hash); + } + + const score = await db.sortedSetScore('ip:recent', hash); + if (!score) { + local.uniqueIPCount += 1; + } + const today = new Date(); + today.setHours(today.getHours(), 0, 0, 0); + if (!score || score < today.getTime()) { + local.uniquevisitors += 1; + await db.sortedSetAdd('ip:recent', Date.now(), hash); + } + } +}; + +Analytics.writeData = async function () { + const today = new Date(); + const month = new Date(); + const dbQueue = []; + const incrByBulk = []; + + // Build list of metrics that were updated + let metrics = [ + 'pageviews', + 'pageviews:month', + ]; + metrics.forEach((metric) => { + const toAdd = ['registered', 'guest', 'bot'].map(type => `${metric}:${type}`); + metrics = [...metrics, ...toAdd]; + }); + metrics.push('uniquevisitors'); + + today.setHours(today.getHours(), 0, 0, 0); + month.setMonth(month.getMonth(), 1); + month.setHours(0, 0, 0, 0); + + if (total.pageViews > 0) { + incrByBulk.push(['analytics:pageviews', total.pageViews, today.getTime()]); + incrByBulk.push(['analytics:pageviews:month', total.pageViews, month.getTime()]); + total.pageViews = 0; + } + + if (total.pageViewsRegistered > 0) { + incrByBulk.push(['analytics:pageviews:registered', total.pageViewsRegistered, today.getTime()]); + incrByBulk.push(['analytics:pageviews:month:registered', total.pageViewsRegistered, month.getTime()]); + total.pageViewsRegistered = 0; + } + + if (total.pageViewsGuest > 0) { + incrByBulk.push(['analytics:pageviews:guest', total.pageViewsGuest, today.getTime()]); + incrByBulk.push(['analytics:pageviews:month:guest', total.pageViewsGuest, month.getTime()]); + total.pageViewsGuest = 0; + } + + if (total.pageViewsBot > 0) { + incrByBulk.push(['analytics:pageviews:bot', total.pageViewsBot, today.getTime()]); + incrByBulk.push(['analytics:pageviews:month:bot', total.pageViewsBot, month.getTime()]); + total.pageViewsBot = 0; + } + + if (total.uniquevisitors > 0) { + incrByBulk.push(['analytics:uniquevisitors', total.uniquevisitors, today.getTime()]); + total.uniquevisitors = 0; + } + + if (total.uniqueIPCount > 0) { + dbQueue.push(db.incrObjectFieldBy('global', 'uniqueIPCount', total.uniqueIPCount)); + total.uniqueIPCount = 0; + } + + for (const [key, value] of Object.entries(total.counters)) { + incrByBulk.push([`analytics:${key}`, value, today.getTime()]); + metrics.push(key); + delete total.counters[key]; + } + + if (incrByBulk.length) { + dbQueue.push(db.sortedSetIncrByBulk(incrByBulk)); + } + + // Update list of tracked metrics + dbQueue.push(db.sortedSetAdd('analyticsKeys', metrics.map(() => +Date.now()), metrics)); + + try { + await Promise.all(dbQueue); + } catch (err) { + winston.error(`[analytics] Encountered error while writing analytics to data store\n${err.stack}`); + } +}; + +Analytics.getHourlyStatsForSet = async function (set, hour, numHours) { + // Guard against accidental ommission of `analytics:` prefix + if (!set.startsWith('analytics:')) { + set = `analytics:${set}`; + } + + const terms = {}; + const hoursArr = []; + + hour = new Date(hour); + hour.setHours(hour.getHours(), 0, 0, 0); + + for (let i = 0, ii = numHours; i < ii; i += 1) { + hoursArr.push(hour.getTime() - (i * 3600 * 1000)); + } + + const counts = await db.sortedSetScores(set, hoursArr); + + hoursArr.forEach((term, index) => { + terms[term] = parseInt(counts[index], 10) || 0; + }); + + const termsArr = []; + + hoursArr.reverse(); + hoursArr.forEach((hour) => { + termsArr.push(terms[hour]); + }); + + return termsArr; +}; + +Analytics.getDailyStatsForSet = async function (set, day, numDays) { + // Guard against accidental ommission of `analytics:` prefix + if (!set.startsWith('analytics:')) { + set = `analytics:${set}`; + } + + const daysArr = []; + day = new Date(day); + // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values + day.setDate(day.getDate() + 1); + day.setHours(0, 0, 0, 0); + + while (numDays > 0) { + /* eslint-disable no-await-in-loop */ + const dayData = await Analytics.getHourlyStatsForSet( + set, + day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1)), + 24 + ); + daysArr.push(dayData.reduce((cur, next) => cur + next)); + numDays -= 1; + } + return daysArr; +}; + +Analytics.getUnwrittenPageviews = function () { + return local.pageViews; +}; + +Analytics.getSummary = async function () { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const [seven, thirty] = await Promise.all([ + Analytics.getDailyStatsForSet('analytics:pageviews', today, 7), + Analytics.getDailyStatsForSet('analytics:pageviews', today, 30), + ]); + + return { + seven: seven.reduce((sum, cur) => sum + cur, 0), + thirty: thirty.reduce((sum, cur) => sum + cur, 0), + }; +}; + +Analytics.getCategoryAnalytics = async function (cid) { + return await utils.promiseParallel({ + 'pageviews:hourly': Analytics.getHourlyStatsForSet(`analytics:pageviews:byCid:${cid}`, Date.now(), 24), + 'pageviews:daily': Analytics.getDailyStatsForSet(`analytics:pageviews:byCid:${cid}`, Date.now(), 30), + 'topics:daily': Analytics.getDailyStatsForSet(`analytics:topics:byCid:${cid}`, Date.now(), 7), + 'posts:daily': Analytics.getDailyStatsForSet(`analytics:posts:byCid:${cid}`, Date.now(), 7), + }); +}; + +Analytics.getErrorAnalytics = async function () { + return await utils.promiseParallel({ + 'not-found': Analytics.getDailyStatsForSet('analytics:errors:404', Date.now(), 7), + toobusy: Analytics.getDailyStatsForSet('analytics:errors:503', Date.now(), 7), + }); +}; + +Analytics.getBlacklistAnalytics = async function () { + return await utils.promiseParallel({ + daily: Analytics.getDailyStatsForSet('analytics:blacklist', Date.now(), 7), + hourly: Analytics.getHourlyStatsForSet('analytics:blacklist', Date.now(), 24), + }); +}; + +require('./promisify')(Analytics); diff --git a/src/api/categories.js b/src/api/categories.js new file mode 100644 index 0000000000..41191bd708 --- /dev/null +++ b/src/api/categories.js @@ -0,0 +1,102 @@ +'use strict'; + +const categories = require('../categories'); +const events = require('../events'); +const user = require('../user'); +const groups = require('../groups'); +const privileges = require('../privileges'); + +const categoriesAPI = module.exports; + +categoriesAPI.get = async function (caller, data) { + const [userPrivileges, category] = await Promise.all([ + privileges.categories.get(data.cid, caller.uid), + categories.getCategoryData(data.cid), + ]); + if (!category || !userPrivileges.read) { + return null; + } + + return category; +}; + +categoriesAPI.create = async function (caller, data) { + const response = await categories.create(data); + const categoryObjs = await categories.getCategories([response.cid], caller.uid); + return categoryObjs[0]; +}; + +categoriesAPI.update = async function (caller, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + await categories.update(data); +}; + +categoriesAPI.delete = async function (caller, data) { + const name = await categories.getCategoryField(data.cid, 'name'); + await categories.purge(data.cid, caller.uid); + await events.log({ + type: 'category-purge', + uid: caller.uid, + ip: caller.ip, + cid: data.cid, + name: name, + }); +}; + +categoriesAPI.getPrivileges = async (caller, cid) => { + let responsePayload; + + if (cid === 'admin') { + responsePayload = await privileges.admin.list(caller.uid); + } else if (!parseInt(cid, 10)) { + responsePayload = await privileges.global.list(); + } else { + responsePayload = await privileges.categories.list(cid); + } + + return responsePayload; +}; + +categoriesAPI.setPrivilege = async (caller, data) => { + const [userExists, groupExists] = await Promise.all([ + user.exists(data.member), + groups.exists(data.member), + ]); + + if (!userExists && !groupExists) { + throw new Error('[[error:no-user-or-group]]'); + } + const privs = Array.isArray(data.privilege) ? data.privilege : [data.privilege]; + const type = data.set ? 'give' : 'rescind'; + if (!privs.length) { + throw new Error('[[error:invalid-data]]'); + } + if (parseInt(data.cid, 10) === 0) { + const adminPrivList = await privileges.admin.getPrivilegeList(); + const adminPrivs = privs.filter(priv => adminPrivList.includes(priv)); + if (adminPrivs.length) { + await privileges.admin[type](adminPrivs, data.member); + } + const globalPrivList = await privileges.global.getPrivilegeList(); + const globalPrivs = privs.filter(priv => globalPrivList.includes(priv)); + if (globalPrivs.length) { + await privileges.global[type](globalPrivs, data.member); + } + } else { + const categoryPrivList = await privileges.categories.getPrivilegeList(); + const categoryPrivs = privs.filter(priv => categoryPrivList.includes(priv)); + await privileges.categories[type](categoryPrivs, data.cid, data.member); + } + + await events.log({ + uid: caller.uid, + type: 'privilege-change', + ip: caller.ip, + privilege: data.privilege.toString(), + cid: data.cid, + action: data.set ? 'grant' : 'rescind', + target: data.member, + }); +}; diff --git a/src/api/chats.js b/src/api/chats.js new file mode 100644 index 0000000000..e6ade229f5 --- /dev/null +++ b/src/api/chats.js @@ -0,0 +1,120 @@ +'use strict'; + +const validator = require('validator'); + +const user = require('../user'); +const meta = require('../meta'); +const messaging = require('../messaging'); +const plugins = require('../plugins'); + +// const websockets = require('../socket.io'); +const socketHelpers = require('../socket.io/helpers'); + +const chatsAPI = module.exports; + +function rateLimitExceeded(caller) { + const session = caller.request ? caller.request.session : caller.session; // socket vs req + const now = Date.now(); + session.lastChatMessageTime = session.lastChatMessageTime || 0; + if (now - session.lastChatMessageTime < meta.config.chatMessageDelay) { + return true; + } + session.lastChatMessageTime = now; + return false; +} + +chatsAPI.create = async function (caller, data) { + if (rateLimitExceeded(caller)) { + throw new Error('[[error:too-many-messages]]'); + } + + if (!data.uids || !Array.isArray(data.uids)) { + throw new Error(`[[error:wrong-parameter-type, uids, ${typeof data.uids}, Array]]`); + } + + await Promise.all(data.uids.map(async uid => messaging.canMessageUser(caller.uid, uid))); + const roomId = await messaging.newRoom(caller.uid, data.uids); + + return await messaging.getRoomData(roomId); +}; + +chatsAPI.post = async (caller, data) => { + if (rateLimitExceeded(caller)) { + throw new Error('[[error:too-many-messages]]'); + } + + ({ data } = await plugins.hooks.fire('filter:messaging.send', { + data, + uid: caller.uid, + })); + + await messaging.canMessageRoom(caller.uid, data.roomId); + const message = await messaging.sendMessage({ + uid: caller.uid, + roomId: data.roomId, + content: data.message, + timestamp: Date.now(), + ip: caller.ip, + }); + messaging.notifyUsersInRoom(caller.uid, data.roomId, message); + user.updateOnlineUsers(caller.uid); + + return message; +}; + +chatsAPI.rename = async (caller, data) => { + await messaging.renameRoom(caller.uid, data.roomId, data.name); + const uids = await messaging.getUidsInRoom(data.roomId, 0, -1); + const eventData = { roomId: data.roomId, newName: validator.escape(String(data.name)) }; + + socketHelpers.emitToUids('event:chats.roomRename', eventData, uids); + return messaging.loadRoom(caller.uid, { + roomId: data.roomId, + }); +}; + +chatsAPI.users = async (caller, data) => { + const [isOwner, users] = await Promise.all([ + messaging.isRoomOwner(caller.uid, data.roomId), + messaging.getUsersInRoom(data.roomId, 0, -1), + ]); + users.forEach((user) => { + user.canKick = (parseInt(user.uid, 10) !== parseInt(caller.uid, 10)) && isOwner; + }); + return { users }; +}; + +chatsAPI.invite = async (caller, data) => { + const userCount = await messaging.getUserCountInRoom(data.roomId); + const maxUsers = meta.config.maximumUsersInChatRoom; + if (maxUsers && userCount >= maxUsers) { + throw new Error('[[error:cant-add-more-users-to-chat-room]]'); + } + + const uidsExist = await user.exists(data.uids); + if (!uidsExist.every(Boolean)) { + throw new Error('[[error:no-user]]'); + } + await Promise.all(data.uids.map(async uid => messaging.canMessageUser(caller.uid, uid))); + await messaging.addUsersToRoom(caller.uid, data.uids, data.roomId); + + delete data.uids; + return chatsAPI.users(caller, data); +}; + +chatsAPI.kick = async (caller, data) => { + const uidsExist = await user.exists(data.uids); + if (!uidsExist.every(Boolean)) { + throw new Error('[[error:no-user]]'); + } + + // Additional checks if kicking vs leaving + if (data.uids.length === 1 && parseInt(data.uids[0], 10) === caller.uid) { + await messaging.leaveRoom([caller.uid], data.roomId); + } else { + await messaging.removeUsersFromRoom(caller.uid, data.uids, data.roomId); + } + + delete data.uids; + return chatsAPI.users(caller, data); +}; diff --git a/src/api/flags.js b/src/api/flags.js new file mode 100644 index 0000000000..8b34f604bb --- /dev/null +++ b/src/api/flags.js @@ -0,0 +1,84 @@ +'use strict'; + +const user = require('../user'); +const flags = require('../flags'); + +const flagsApi = module.exports; + +flagsApi.create = async (caller, data) => { + const required = ['type', 'id', 'reason']; + if (!required.every(prop => !!data[prop])) { + throw new Error('[[error:invalid-data]]'); + } + + const { type, id, reason } = data; + + await flags.validate({ + uid: caller.uid, + type: type, + id: id, + }); + + const flagObj = await flags.create(type, id, caller.uid, reason); + flags.notify(flagObj, caller.uid); + + return flagObj; +}; + +flagsApi.update = async (caller, data) => { + const allowed = await user.isPrivileged(caller.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + + const { flagId } = data; + delete data.flagId; + + await flags.update(flagId, caller.uid, data); + return await flags.getHistory(flagId); +}; + +flagsApi.appendNote = async (caller, data) => { + const allowed = await user.isPrivileged(caller.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + if (data.datetime && data.flagId) { + try { + const note = await flags.getNote(data.flagId, data.datetime); + if (note.uid !== caller.uid) { + throw new Error('[[error:no-privileges]]'); + } + } catch (e) { + // Okay if not does not exist in database + if (e.message !== '[[error:invalid-data]]') { + throw e; + } + } + } + await flags.appendNote(data.flagId, caller.uid, data.note, data.datetime); + const [notes, history] = await Promise.all([ + flags.getNotes(data.flagId), + flags.getHistory(data.flagId), + ]); + return { notes: notes, history: history }; +}; + +flagsApi.deleteNote = async (caller, data) => { + const note = await flags.getNote(data.flagId, data.datetime); + if (note.uid !== caller.uid) { + throw new Error('[[error:no-privileges]]'); + } + + await flags.deleteNote(data.flagId, data.datetime); + await flags.appendHistory(data.flagId, caller.uid, { + notes: '[[flags:note-deleted]]', + datetime: Date.now(), + }); + + const [notes, history] = await Promise.all([ + flags.getNotes(data.flagId), + flags.getHistory(data.flagId), + ]); + return { notes: notes, history: history }; +}; diff --git a/src/api/groups.js b/src/api/groups.js new file mode 100644 index 0000000000..a5c22168de --- /dev/null +++ b/src/api/groups.js @@ -0,0 +1,238 @@ +'use strict'; + +const validator = require('validator'); + +const privileges = require('../privileges'); +const events = require('../events'); +const groups = require('../groups'); +const user = require('../user'); +const meta = require('../meta'); +const notifications = require('../notifications'); +const slugify = require('../slugify'); + +const groupsAPI = module.exports; + +groupsAPI.create = async function (caller, data) { + if (!caller.uid) { + throw new Error('[[error:no-privileges]]'); + } else if (!data) { + throw new Error('[[error:invalid-data]]'); + } else if (typeof data.name !== 'string' || groups.isPrivilegeGroup(data.name)) { + throw new Error('[[error:invalid-group-name]]'); + } + + const canCreate = await privileges.global.can('group:create', caller.uid); + if (!canCreate) { + throw new Error('[[error:no-privileges]]'); + } + data.ownerUid = caller.uid; + data.system = false; + const groupData = await groups.create(data); + logGroupEvent(caller, 'group-create', { + groupName: data.name, + }); + + return groupData; +}; + +groupsAPI.update = async function (caller, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + await isOwner(caller, groupName); + + delete data.slug; + await groups.update(groupName, data); + + return await groups.getGroupData(data.name || groupName); +}; + +groupsAPI.delete = async function (caller, data) { + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + await isOwner(caller, groupName); + if ( + groups.systemGroups.includes(groupName) || + groups.ephemeralGroups.includes(groupName) + ) { + throw new Error('[[error:not-allowed]]'); + } + + await groups.destroy(groupName); + logGroupEvent(caller, 'group-delete', { + groupName: groupName, + }); +}; + +groupsAPI.join = async function (caller, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + if (caller.uid <= 0 || !data.uid) { + throw new Error('[[error:invalid-uid]]'); + } + + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + if (!groupName) { + throw new Error('[[error:no-group]]'); + } + + const isCallerAdmin = await user.isAdministrator(caller.uid); + if (!isCallerAdmin && ( + groups.systemGroups.includes(groupName) || + groups.isPrivilegeGroup(groupName) + )) { + throw new Error('[[error:not-allowed]]'); + } + + const [groupData, isCallerOwner, userExists] = await Promise.all([ + groups.getGroupData(groupName), + groups.ownership.isOwner(caller.uid, groupName), + user.exists(data.uid), + ]); + + if (!userExists) { + throw new Error('[[error:invalid-uid]]'); + } + + const isSelf = parseInt(caller.uid, 10) === parseInt(data.uid, 10); + if (!meta.config.allowPrivateGroups && isSelf) { + // all groups are public! + await groups.join(groupName, data.uid); + logGroupEvent(caller, 'group-join', { + groupName: groupName, + targetUid: data.uid, + }); + return; + } + + if (!isCallerAdmin && isSelf && groupData.private && groupData.disableJoinRequests) { + throw new Error('[[error:group-join-disabled]]'); + } + + if ((!groupData.private && isSelf) || isCallerAdmin || isCallerOwner) { + await groups.join(groupName, data.uid); + logGroupEvent(caller, 'group-join', { + groupName: groupName, + targetUid: data.uid, + }); + } else if (isSelf) { + await groups.requestMembership(groupName, caller.uid); + logGroupEvent(caller, 'group-request-membership', { + groupName: groupName, + targetUid: data.uid, + }); + } +}; + +groupsAPI.leave = async function (caller, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + if (caller.uid <= 0) { + throw new Error('[[error:invalid-uid]]'); + } + const isSelf = parseInt(caller.uid, 10) === parseInt(data.uid, 10); + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + if (!groupName) { + throw new Error('[[error:no-group]]'); + } + + if (typeof groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } + + if (groupName === 'administrators' && isSelf) { + throw new Error('[[error:cant-remove-self-as-admin]]'); + } + + const [groupData, isCallerAdmin, isCallerOwner, userExists, isMember] = await Promise.all([ + groups.getGroupData(groupName), + user.isAdministrator(caller.uid), + groups.ownership.isOwner(caller.uid, groupName), + user.exists(data.uid), + groups.isMember(data.uid, groupName), + ]); + + if (!userExists) { + throw new Error('[[error:invalid-uid]]'); + } + if (!isMember) { + return; + } + + if (groupData.disableLeave && isSelf) { + throw new Error('[[error:group-leave-disabled]]'); + } + + if (isSelf || isCallerAdmin || isCallerOwner) { + await groups.leave(groupName, data.uid); + } else { + throw new Error('[[error:no-privileges]]'); + } + + const { displayname } = await user.getUserFields(data.uid, ['username']); + + const notification = await notifications.create({ + type: 'group-leave', + bodyShort: `[[groups:membership.leave.notification_title, ${displayname}, ${groupName}]]`, + nid: `group:${validator.escape(groupName)}:uid:${data.uid}:group-leave`, + path: `/groups/${slugify(groupName)}`, + from: data.uid, + }); + const uids = await groups.getOwners(groupName); + await notifications.push(notification, uids); + + logGroupEvent(caller, 'group-leave', { + groupName: groupName, + targetUid: data.uid, + }); +}; + +groupsAPI.grant = async (caller, data) => { + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + await isOwner(caller, groupName); + + await groups.ownership.grant(data.uid, groupName); + logGroupEvent(caller, 'group-owner-grant', { + groupName: groupName, + targetUid: data.uid, + }); +}; + +groupsAPI.rescind = async (caller, data) => { + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + await isOwner(caller, groupName); + + await groups.ownership.rescind(data.uid, groupName); + logGroupEvent(caller, 'group-owner-rescind', { + groupName: groupName, + targetUid: data.uid, + }); +}; + +async function isOwner(caller, groupName) { + if (typeof groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } + const [hasAdminPrivilege, isGlobalModerator, isOwner, group] = await Promise.all([ + privileges.admin.can('admin:groups', caller.uid), + user.isGlobalModerator(caller.uid), + groups.ownership.isOwner(caller.uid, groupName), + groups.getGroupData(groupName), + ]); + + const check = isOwner || hasAdminPrivilege || (isGlobalModerator && !group.system); + if (!check) { + throw new Error('[[error:no-privileges]]'); + } +} + +function logGroupEvent(caller, event, additional) { + events.log({ + type: event, + uid: caller.uid, + ip: caller.ip, + ...additional, + }); +} diff --git a/src/api/helpers.js b/src/api/helpers.js new file mode 100644 index 0000000000..0be587293a --- /dev/null +++ b/src/api/helpers.js @@ -0,0 +1,142 @@ +'use strict'; + +const url = require('url'); +const user = require('../user'); +const topics = require('../topics'); +const posts = require('../posts'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const socketHelpers = require('../socket.io/helpers'); +const websockets = require('../socket.io'); +const events = require('../events'); + +exports.setDefaultPostData = function (reqOrSocket, data) { + data.uid = reqOrSocket.uid; + data.req = exports.buildReqObject(reqOrSocket, { ...data }); + data.timestamp = Date.now(); + data.fromQueue = false; +}; + +// creates a slimmed down version of the request object +exports.buildReqObject = (req, payload) => { + req = req || {}; + const headers = req.headers || (req.request && req.request.headers) || {}; + const encrypted = req.connection ? !!req.connection.encrypted : false; + let { host } = headers; + const referer = headers.referer || ''; + + if (!host) { + host = url.parse(referer).host || ''; + } + + return { + uid: req.uid, + params: req.params, + method: req.method, + body: payload || req.body, + session: req.session, + ip: req.ip, + host: host, + protocol: encrypted ? 'https' : 'http', + secure: encrypted, + url: referer, + path: referer.slice(referer.indexOf(host) + host.length), + headers: headers, + }; +}; + +exports.doTopicAction = async function (action, event, caller, { tids }) { + if (!Array.isArray(tids)) { + throw new Error('[[error:invalid-tid]]'); + } + + const exists = await topics.exists(tids); + if (!exists.every(Boolean)) { + throw new Error('[[error:no-topic]]'); + } + + if (typeof topics.tools[action] !== 'function') { + return; + } + + const uids = await user.getUidsFromSet('users:online', 0, -1); + + await Promise.all(tids.map(async (tid) => { + const title = await topics.getTopicField(tid, 'title'); + const data = await topics.tools[action](tid, caller.uid); + const notifyUids = await privileges.categories.filterUids('topics:read', data.cid, uids); + socketHelpers.emitToUids(event, data, notifyUids); + await logTopicAction(action, caller, tid, title); + })); +}; + +async function logTopicAction(action, req, tid, title) { + // Only log certain actions to system event log + const actionsToLog = ['delete', 'restore', 'purge']; + if (!actionsToLog.includes(action)) { + return; + } + await events.log({ + type: `topic-${action}`, + uid: req.uid, + ip: req.ip, + tid: tid, + title: String(title), + }); +} + +exports.postCommand = async function (caller, command, eventName, notification, data) { + if (!caller.uid) { + throw new Error('[[error:not-logged-in]]'); + } + + if (!data || !data.pid) { + throw new Error('[[error:invalid-data]]'); + } + + if (!data.room_id) { + throw new Error(`[[error:invalid-room-id, ${data.room_id} ]]`); + } + const [exists, deleted] = await Promise.all([ + posts.exists(data.pid), + posts.getPostField(data.pid, 'deleted'), + ]); + + if (!exists) { + throw new Error('[[error:invalid-pid]]'); + } + + if (deleted) { + throw new Error('[[error:post-deleted]]'); + } + + /* + hooks: + filter:post.upvote + filter:post.downvote + filter:post.unvote + filter:post.bookmark + filter:post.unbookmark + */ + const filteredData = await plugins.hooks.fire(`filter:post.${command}`, { + data: data, + uid: caller.uid, + }); + return await executeCommand(caller, command, eventName, notification, filteredData.data); +}; + +async function executeCommand(caller, command, eventName, notification, data) { + const result = await posts[command](data.pid, caller.uid); + if (result && eventName) { + websockets.in(`uid_${caller.uid}`).emit(`posts.${command}`, result); + websockets.in(data.room_id).emit(`event:${eventName}`, result); + } + if (result && command === 'upvote') { + socketHelpers.upvote(result, notification); + } else if (result && notification) { + socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, command, notification); + } else if (result && command === 'unvote') { + socketHelpers.rescindUpvoteNotification(data.pid, caller.uid); + } + return result; +} diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000000..3c1187abb9 --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = { + users: require('./users'), + groups: require('./groups'), + topics: require('./topics'), + posts: require('./posts'), + chats: require('./chats'), + categories: require('./categories'), + flags: require('./flags'), +}; diff --git a/src/api/posts.js b/src/api/posts.js new file mode 100644 index 0000000000..3ad970e96a --- /dev/null +++ b/src/api/posts.js @@ -0,0 +1,335 @@ +'use strict'; + +const validator = require('validator'); +const _ = require('lodash'); + +const utils = require('../utils'); +const user = require('../user'); +const posts = require('../posts'); +const topics = require('../topics'); +const groups = require('../groups'); +const meta = require('../meta'); +const events = require('../events'); +const privileges = require('../privileges'); +const apiHelpers = require('./helpers'); +const websockets = require('../socket.io'); +const socketHelpers = require('../socket.io/helpers'); + +const postsAPI = module.exports; + +postsAPI.get = async function (caller, data) { + const [userPrivileges, post, voted] = await Promise.all([ + privileges.posts.get([data.pid], caller.uid), + posts.getPostData(data.pid), + posts.hasVoted(data.pid, caller.uid), + ]); + if (!post) { + return null; + } + Object.assign(post, voted); + + const userPrivilege = userPrivileges[0]; + if (!userPrivilege.read || !userPrivilege['topics:read']) { + return null; + } + + post.ip = userPrivilege.isAdminOrMod ? post.ip : undefined; + const selfPost = caller.uid && caller.uid === parseInt(post.uid, 10); + if (post.deleted && !(userPrivilege.isAdminOrMod || selfPost)) { + post.content = '[[topic:post_is_deleted]]'; + } + + return post; +}; + +postsAPI.edit = async function (caller, data) { + if (!data || !data.pid || (meta.config.minimumPostLength !== 0 && !data.content)) { + throw new Error('[[error:invalid-data]]'); + } + if (!caller.uid) { + throw new Error('[[error:not-logged-in]]'); + } + // Trim and remove HTML (latter for composers that send in HTML, like redactor) + const contentLen = utils.stripHTMLTags(data.content).trim().length; + + if (data.title && data.title.length < meta.config.minimumTitleLength) { + throw new Error(`[[error:title-too-short, ${meta.config.minimumTitleLength}]]`); + } else if (data.title && data.title.length > meta.config.maximumTitleLength) { + throw new Error(`[[error:title-too-long, ${meta.config.maximumTitleLength}]]`); + } else if (meta.config.minimumPostLength !== 0 && contentLen < meta.config.minimumPostLength) { + throw new Error(`[[error:content-too-short, ${meta.config.minimumPostLength}]]`); + } else if (contentLen > meta.config.maximumPostLength) { + throw new Error(`[[error:content-too-long, ${meta.config.maximumPostLength}]]`); + } + + data.uid = caller.uid; + data.req = apiHelpers.buildReqObject(caller); + data.timestamp = parseInt(data.timestamp, 10) || Date.now(); + + const editResult = await posts.edit(data); + if (editResult.topic.isMainPost) { + await topics.thumbs.migrate(data.uuid, editResult.topic.tid); + } + const selfPost = parseInt(caller.uid, 10) === parseInt(editResult.post.uid, 10); + if (!selfPost && editResult.post.changed) { + await events.log({ + type: `post-edit`, + uid: caller.uid, + ip: caller.ip, + pid: editResult.post.pid, + oldContent: editResult.post.oldContent, + newContent: editResult.post.newContent, + }); + } + + if (editResult.topic.renamed) { + await events.log({ + type: 'topic-rename', + uid: caller.uid, + ip: caller.ip, + tid: editResult.topic.tid, + oldTitle: validator.escape(String(editResult.topic.oldTitle)), + newTitle: validator.escape(String(editResult.topic.title)), + }); + } + const postObj = await posts.getPostSummaryByPids([editResult.post.pid], caller.uid, {}); + const returnData = { ...postObj[0], ...editResult.post }; + returnData.topic = { ...postObj[0].topic, ...editResult.post.topic }; + + if (!editResult.post.deleted) { + websockets.in(`topic_${editResult.topic.tid}`).emit('event:post_edited', editResult); + return returnData; + } + + const memberData = await groups.getMembersOfGroups([ + 'administrators', + 'Global Moderators', + `cid:${editResult.topic.cid}:privileges:moderate`, + `cid:${editResult.topic.cid}:privileges:groups:moderate`, + ]); + + const uids = _.uniq(_.flatten(memberData).concat(String(caller.uid))); + uids.forEach(uid => websockets.in(`uid_${uid}`).emit('event:post_edited', editResult)); + return returnData; +}; + +postsAPI.delete = async function (caller, data) { + await deleteOrRestore(caller, data, { + command: 'delete', + event: 'event:post_deleted', + type: 'post-delete', + }); +}; + +postsAPI.restore = async function (caller, data) { + await deleteOrRestore(caller, data, { + command: 'restore', + event: 'event:post_restored', + type: 'post-restore', + }); +}; + +async function deleteOrRestore(caller, data, params) { + if (!data || !data.pid) { + throw new Error('[[error:invalid-data]]'); + } + const postData = await posts.tools[params.command](caller.uid, data.pid); + const results = await isMainAndLastPost(data.pid); + if (results.isMain && results.isLast) { + await deleteOrRestoreTopicOf(params.command, data.pid, caller); + } + + websockets.in(`topic_${postData.tid}`).emit(params.event, postData); + + await events.log({ + type: params.type, + uid: caller.uid, + pid: data.pid, + tid: postData.tid, + ip: caller.ip, + }); +} + +async function deleteOrRestoreTopicOf(command, pid, caller) { + const topic = await posts.getTopicFields(pid, ['tid', 'cid', 'deleted', 'scheduled']); + // exempt scheduled topics from being deleted/restored + if (topic.scheduled) { + return; + } + // command: delete/restore + await apiHelpers.doTopicAction( + command, + topic.deleted ? 'event:topic_restored' : 'event:topic_deleted', + caller, + { tids: [topic.tid], cid: topic.cid } + ); +} + +postsAPI.purge = async function (caller, data) { + if (!data || !parseInt(data.pid, 10)) { + throw new Error('[[error:invalid-data]]'); + } + + const results = await isMainAndLastPost(data.pid); + if (results.isMain && !results.isLast) { + throw new Error('[[error:cant-purge-main-post]]'); + } + + const isMainAndLast = results.isMain && results.isLast; + const postData = await posts.getPostFields(data.pid, ['toPid', 'tid']); + postData.pid = data.pid; + + const canPurge = await privileges.posts.canPurge(data.pid, caller.uid); + if (!canPurge) { + throw new Error('[[error:no-privileges]]'); + } + require('../posts/cache').del(data.pid); + await posts.purge(data.pid, caller.uid); + + websockets.in(`topic_${postData.tid}`).emit('event:post_purged', postData); + const topicData = await topics.getTopicFields(postData.tid, ['title', 'cid']); + + await events.log({ + type: 'post-purge', + pid: data.pid, + uid: caller.uid, + ip: caller.ip, + tid: postData.tid, + title: String(topicData.title), + }); + + if (isMainAndLast) { + await apiHelpers.doTopicAction( + 'purge', + 'event:topic_purged', + caller, + { tids: [postData.tid], cid: topicData.cid } + ); + } +}; + +async function isMainAndLastPost(pid) { + const [isMain, topicData] = await Promise.all([ + posts.isMain(pid), + posts.getTopicFields(pid, ['postcount']), + ]); + return { + isMain: isMain, + isLast: topicData && topicData.postcount === 1, + }; +} + +postsAPI.move = async function (caller, data) { + if (!caller.uid) { + throw new Error('[[error:not-logged-in]]'); + } + if (!data || !data.pid || !data.tid) { + throw new Error('[[error:invalid-data]]'); + } + const canMove = await Promise.all([ + privileges.topics.isAdminOrMod(data.tid, caller.uid), + privileges.posts.canMove(data.pid, caller.uid), + ]); + if (!canMove.every(Boolean)) { + throw new Error('[[error:no-privileges]]'); + } + + await topics.movePostToTopic(caller.uid, data.pid, data.tid); + + const [postDeleted, topicDeleted] = await Promise.all([ + posts.getPostField(data.pid, 'deleted'), + topics.getTopicField(data.tid, 'deleted'), + await events.log({ + type: `post-move`, + uid: caller.uid, + ip: caller.ip, + pid: data.pid, + toTid: data.tid, + }), + ]); + + if (!postDeleted && !topicDeleted) { + socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, 'move', 'notifications:moved_your_post'); + } +}; + +postsAPI.upvote = async function (caller, data) { + return await apiHelpers.postCommand(caller, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data); +}; + +postsAPI.downvote = async function (caller, data) { + return await apiHelpers.postCommand(caller, 'downvote', 'voted', '', data); +}; + +postsAPI.unvote = async function (caller, data) { + return await apiHelpers.postCommand(caller, 'unvote', 'voted', '', data); +}; + +postsAPI.bookmark = async function (caller, data) { + return await apiHelpers.postCommand(caller, 'bookmark', 'bookmarked', '', data); +}; + +postsAPI.unbookmark = async function (caller, data) { + return await apiHelpers.postCommand(caller, 'unbookmark', 'bookmarked', '', data); +}; + +async function diffsPrivilegeCheck(pid, uid) { + const [deleted, privilegesData] = await Promise.all([ + posts.getPostField(pid, 'deleted'), + privileges.posts.get([pid], uid), + ]); + + const allowed = privilegesData[0]['posts:history'] && (deleted ? privilegesData[0]['posts:view_deleted'] : true); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } +} + +postsAPI.getDiffs = async (caller, data) => { + await diffsPrivilegeCheck(data.pid, caller.uid); + const timestamps = await posts.diffs.list(data.pid); + const post = await posts.getPostFields(data.pid, ['timestamp', 'uid']); + + const diffs = await posts.diffs.get(data.pid); + const uids = diffs.map(diff => diff.uid || null); + uids.push(post.uid); + let usernames = await user.getUsersFields(uids, ['username']); + usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null)); + + const cid = await posts.getCidByPid(data.pid); + const [isAdmin, isModerator] = await Promise.all([ + user.isAdministrator(caller.uid), + privileges.users.isModerator(caller.uid, cid), + ]); + + // timestamps returned by posts.diffs.list are strings + timestamps.push(String(post.timestamp)); + + return { + timestamps: timestamps, + revisions: timestamps.map((timestamp, idx) => ({ + timestamp: timestamp, + username: usernames[idx], + })), + // Only admins, global mods and moderator of that cid can delete a diff + deletable: isAdmin || isModerator, + // These and post owners can restore to a different post version + editable: isAdmin || isModerator || parseInt(caller.uid, 10) === parseInt(post.uid, 10), + }; +}; + +postsAPI.loadDiff = async (caller, data) => { + await diffsPrivilegeCheck(data.pid, caller.uid); + return await posts.diffs.load(data.pid, data.since, caller.uid); +}; + +postsAPI.restoreDiff = async (caller, data) => { + const cid = await posts.getCidByPid(data.pid); + const canEdit = await privileges.categories.can('posts:edit', cid, caller.uid); + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + const edit = await posts.diffs.restore(data.pid, data.since, caller.uid, apiHelpers.buildReqObject(caller)); + websockets.in(`topic_${edit.topic.tid}`).emit('event:post_edited', edit); +}; diff --git a/src/api/topics.js b/src/api/topics.js new file mode 100644 index 0000000000..901b8903c7 --- /dev/null +++ b/src/api/topics.js @@ -0,0 +1,154 @@ +'use strict'; + +const user = require('../user'); +const topics = require('../topics'); +const posts = require('../posts'); +const meta = require('../meta'); +const privileges = require('../privileges'); + +const apiHelpers = require('./helpers'); + +const { doTopicAction } = apiHelpers; + +const websockets = require('../socket.io'); +const socketHelpers = require('../socket.io/helpers'); + +const topicsAPI = module.exports; + +topicsAPI.get = async function (caller, data) { + const [userPrivileges, topic] = await Promise.all([ + privileges.topics.get(data.tid, caller.uid), + topics.getTopicData(data.tid), + ]); + if ( + !topic || + !userPrivileges.read || + !userPrivileges['topics:read'] || + !privileges.topics.canViewDeletedScheduled(topic, userPrivileges) + ) { + return null; + } + + return topic; +}; + +topicsAPI.create = async function (caller, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + + const payload = { ...data }; + payload.tags = payload.tags || []; + apiHelpers.setDefaultPostData(caller, payload); + const isScheduling = parseInt(data.timestamp, 10) > payload.timestamp; + if (isScheduling) { + if (await privileges.categories.can('topics:schedule', data.cid, caller.uid)) { + payload.timestamp = parseInt(data.timestamp, 10); + } else { + throw new Error('[[error:no-privileges]]'); + } + } + + await meta.blacklist.test(caller.ip); + const shouldQueue = await posts.shouldQueue(caller.uid, payload); + if (shouldQueue) { + return await posts.addToQueue(payload); + } + + const result = await topics.post(payload); + await topics.thumbs.migrate(data.uuid, result.topicData.tid); + + socketHelpers.emitToUids('event:new_post', { posts: [result.postData] }, [caller.uid]); + socketHelpers.emitToUids('event:new_topic', result.topicData, [caller.uid]); + socketHelpers.notifyNew(caller.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); + + return result.topicData; +}; + +topicsAPI.reply = async function (caller, data) { + if (!data || !data.tid || (meta.config.minimumPostLength !== 0 && !data.content)) { + throw new Error('[[error:invalid-data]]'); + } + const payload = { ...data }; + apiHelpers.setDefaultPostData(caller, payload); + + await meta.blacklist.test(caller.ip); + const shouldQueue = await posts.shouldQueue(caller.uid, payload); + if (shouldQueue) { + return await posts.addToQueue(payload); + } + + const postData = await topics.reply(payload); // postData seems to be a subset of postObj, refactor? + const postObj = await posts.getPostSummaryByPids([postData.pid], caller.uid, {}); + + const result = { + posts: [postData], + 'reputation:disabled': meta.config['reputation:disabled'] === 1, + 'downvote:disabled': meta.config['downvote:disabled'] === 1, + }; + + user.updateOnlineUsers(caller.uid); + if (caller.uid) { + socketHelpers.emitToUids('event:new_post', result, [caller.uid]); + } else if (caller.uid === 0) { + websockets.in('online_guests').emit('event:new_post', result); + } + + socketHelpers.notifyNew(caller.uid, 'newPost', result); + + return postObj[0]; +}; + +topicsAPI.delete = async function (caller, data) { + await doTopicAction('delete', 'event:topic_deleted', caller, { + tids: data.tids, + }); +}; + +topicsAPI.restore = async function (caller, data) { + await doTopicAction('restore', 'event:topic_restored', caller, { + tids: data.tids, + }); +}; + +topicsAPI.purge = async function (caller, data) { + await doTopicAction('purge', 'event:topic_purged', caller, { + tids: data.tids, + }); +}; + +topicsAPI.pin = async function (caller, data) { + await doTopicAction('pin', 'event:topic_pinned', caller, { + tids: data.tids, + }); +}; + +topicsAPI.unpin = async function (caller, data) { + await doTopicAction('unpin', 'event:topic_unpinned', caller, { + tids: data.tids, + }); +}; + +topicsAPI.lock = async function (caller, data) { + await doTopicAction('lock', 'event:topic_locked', caller, { + tids: data.tids, + }); +}; + +topicsAPI.unlock = async function (caller, data) { + await doTopicAction('unlock', 'event:topic_unlocked', caller, { + tids: data.tids, + }); +}; + +topicsAPI.follow = async function (caller, data) { + await topics.follow(data.tid, caller.uid); +}; + +topicsAPI.ignore = async function (caller, data) { + await topics.ignore(data.tid, caller.uid); +}; + +topicsAPI.unfollow = async function (caller, data) { + await topics.unfollow(data.tid, caller.uid); +}; diff --git a/src/api/users.js b/src/api/users.js new file mode 100644 index 0000000000..7e8546b454 --- /dev/null +++ b/src/api/users.js @@ -0,0 +1,478 @@ +'use strict'; + +const validator = require('validator'); +const winston = require('winston'); + +const db = require('../database'); +const user = require('../user'); +const groups = require('../groups'); +const meta = require('../meta'); +const flags = require('../flags'); +const privileges = require('../privileges'); +const notifications = require('../notifications'); +const plugins = require('../plugins'); +const events = require('../events'); +const translator = require('../translator'); +const sockets = require('../socket.io'); + +const usersAPI = module.exports; + +usersAPI.create = async function (caller, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + const uid = await user.create(data); + return await user.getUserData(uid); +}; + +usersAPI.update = async function (caller, data) { + if (!caller.uid) { + throw new Error('[[error:invalid-uid]]'); + } + + if (!data || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + + const oldUserData = await user.getUserFields(data.uid, ['email', 'username']); + if (!oldUserData || !oldUserData.username) { + throw new Error('[[error:invalid-data]]'); + } + + const [isAdminOrGlobalMod, canEdit] = await Promise.all([ + user.isAdminOrGlobalMod(caller.uid), + privileges.users.canEdit(caller.uid, data.uid), + ]); + + // Changing own email/username requires password confirmation + if (data.hasOwnProperty('email') || data.hasOwnProperty('username')) { + await isPrivilegedOrSelfAndPasswordMatch(caller, data); + } + + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + if (!isAdminOrGlobalMod && meta.config['username:disableEdit']) { + data.username = oldUserData.username; + } + + if (!isAdminOrGlobalMod && meta.config['email:disableEdit']) { + data.email = oldUserData.email; + } + + await user.updateProfile(caller.uid, data); + const userData = await user.getUserData(data.uid); + + if (userData.username !== oldUserData.username) { + await events.log({ + type: 'username-change', + uid: caller.uid, + targetUid: data.uid, + ip: caller.ip, + oldUsername: oldUserData.username, + newUsername: userData.username, + }); + } + return userData; +}; + +usersAPI.delete = async function (caller, { uid, password }) { + await processDeletion({ uid: uid, method: 'delete', password, caller }); +}; + +usersAPI.deleteContent = async function (caller, { uid, password }) { + await processDeletion({ uid, method: 'deleteContent', password, caller }); +}; + +usersAPI.deleteAccount = async function (caller, { uid, password }) { + await processDeletion({ uid, method: 'deleteAccount', password, caller }); +}; + +usersAPI.deleteMany = async function (caller, data) { + if (await canDeleteUids(data.uids)) { + await Promise.all(data.uids.map(uid => processDeletion({ uid, method: 'delete', caller }))); + } +}; + +usersAPI.updateSettings = async function (caller, data) { + if (!caller.uid || !data || !data.settings) { + throw new Error('[[error:invalid-data]]'); + } + + const canEdit = await privileges.users.canEdit(caller.uid, data.uid); + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + let defaults = await user.getSettings(0); + defaults = { + postsPerPage: defaults.postsPerPage, + topicsPerPage: defaults.topicsPerPage, + userLang: defaults.userLang, + acpLang: defaults.acpLang, + }; + // load raw settings without parsing values to booleans + const current = await db.getObject(`user:${data.uid}:settings`); + const payload = { ...defaults, ...current, ...data.settings }; + delete payload.uid; + + return await user.saveSettings(data.uid, payload); +}; + +usersAPI.changePassword = async function (caller, data) { + await user.changePassword(caller.uid, Object.assign(data, { ip: caller.ip })); + await events.log({ + type: 'password-change', + uid: caller.uid, + targetUid: data.uid, + ip: caller.ip, + }); +}; + +usersAPI.follow = async function (caller, data) { + await user.follow(caller.uid, data.uid); + plugins.hooks.fire('action:user.follow', { + fromUid: caller.uid, + toUid: data.uid, + }); + + const userData = await user.getUserFields(caller.uid, ['username', 'userslug']); + const { displayname } = userData; + + const notifObj = await notifications.create({ + type: 'follow', + bodyShort: `[[notifications:user_started_following_you, ${displayname}]]`, + nid: `follow:${data.uid}:uid:${caller.uid}`, + from: caller.uid, + path: `/uid/${data.uid}/followers`, + mergeId: 'notifications:user_started_following_you', + }); + if (!notifObj) { + return; + } + notifObj.user = userData; + await notifications.push(notifObj, [data.uid]); +}; + +usersAPI.unfollow = async function (caller, data) { + await user.unfollow(caller.uid, data.uid); + plugins.hooks.fire('action:user.unfollow', { + fromUid: caller.uid, + toUid: data.uid, + }); +}; + +usersAPI.ban = async function (caller, data) { + if (!await privileges.users.hasBanPrivilege(caller.uid)) { + throw new Error('[[error:no-privileges]]'); + } else if (await user.isAdministrator(data.uid)) { + throw new Error('[[error:cant-ban-other-admins]]'); + } + + const banData = await user.bans.ban(data.uid, data.until, data.reason); + await db.setObjectField(`uid:${data.uid}:ban:${banData.timestamp}`, 'fromUid', caller.uid); + + if (!data.reason) { + data.reason = await translator.translate('[[user:info.banned-no-reason]]'); + } + + sockets.in(`uid_${data.uid}`).emit('event:banned', { + until: data.until, + reason: validator.escape(String(data.reason || '')), + }); + + await flags.resolveFlag('user', data.uid, caller.uid); + await flags.resolveUserPostFlags(data.uid, caller.uid); + await events.log({ + type: 'user-ban', + uid: caller.uid, + targetUid: data.uid, + ip: caller.ip, + reason: data.reason || undefined, + }); + plugins.hooks.fire('action:user.banned', { + callerUid: caller.uid, + ip: caller.ip, + uid: data.uid, + until: data.until > 0 ? data.until : undefined, + reason: data.reason || undefined, + }); + const canLoginIfBanned = await user.bans.canLoginIfBanned(data.uid); + if (!canLoginIfBanned) { + await user.auth.revokeAllSessions(data.uid); + } +}; + +usersAPI.unban = async function (caller, data) { + if (!await privileges.users.hasBanPrivilege(caller.uid)) { + throw new Error('[[error:no-privileges]]'); + } + + await user.bans.unban(data.uid); + + sockets.in(`uid_${data.uid}`).emit('event:unbanned'); + + await events.log({ + type: 'user-unban', + uid: caller.uid, + targetUid: data.uid, + ip: caller.ip, + }); + plugins.hooks.fire('action:user.unbanned', { + callerUid: caller.uid, + ip: caller.ip, + uid: data.uid, + }); +}; + +usersAPI.mute = async function (caller, data) { + if (!await privileges.users.hasMutePrivilege(caller.uid)) { + throw new Error('[[error:no-privileges]]'); + } else if (await user.isAdministrator(data.uid)) { + throw new Error('[[error:cant-mute-other-admins]]'); + } + const reason = data.reason || '[[user:info.muted-no-reason]]'; + await db.setObject(`user:${data.uid}`, { + mutedUntil: data.until, + mutedReason: reason, + }); + const now = Date.now(); + const muteKey = `uid:${data.uid}:mute:${now}`; + const muteData = { + fromUid: caller.uid, + uid: data.uid, + timestamp: now, + expire: data.until, + }; + if (data.reason) { + muteData.reason = reason; + } + await db.sortedSetAdd(`uid:${data.uid}:mutes:timestamp`, now, muteKey); + await db.setObject(muteKey, muteData); + await events.log({ + type: 'user-mute', + uid: caller.uid, + targetUid: data.uid, + ip: caller.ip, + reason: data.reason || undefined, + }); + plugins.hooks.fire('action:user.muted', { + callerUid: caller.uid, + ip: caller.ip, + uid: data.uid, + until: data.until > 0 ? data.until : undefined, + reason: data.reason || undefined, + }); +}; + +usersAPI.unmute = async function (caller, data) { + if (!await privileges.users.hasMutePrivilege(caller.uid)) { + throw new Error('[[error:no-privileges]]'); + } + + await db.deleteObjectFields(`user:${data.uid}`, ['mutedUntil', 'mutedReason']); + + await events.log({ + type: 'user-unmute', + uid: caller.uid, + targetUid: data.uid, + ip: caller.ip, + }); + plugins.hooks.fire('action:user.unmuted', { + callerUid: caller.uid, + ip: caller.ip, + uid: data.uid, + }); +}; + +async function isPrivilegedOrSelfAndPasswordMatch(caller, data) { + const { uid } = caller; + const isSelf = parseInt(uid, 10) === parseInt(data.uid, 10); + const canEdit = await privileges.users.canEdit(uid, data.uid); + + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + const [hasPassword, passwordMatch] = await Promise.all([ + user.hasPassword(data.uid), + data.password ? user.isPasswordCorrect(data.uid, data.password, caller.ip) : false, + ]); + + if (isSelf && hasPassword && !passwordMatch) { + throw new Error('[[error:invalid-password]]'); + } +} + +async function processDeletion({ uid, method, password, caller }) { + const isTargetAdmin = await user.isAdministrator(uid); + const isSelf = parseInt(uid, 10) === parseInt(caller.uid, 10); + const isAdmin = await user.isAdministrator(caller.uid); + + if (isSelf && meta.config.allowAccountDelete !== 1) { + throw new Error('[[error:account-deletion-disabled]]'); + } else if (!isSelf && !isAdmin) { + throw new Error('[[error:no-privileges]]'); + } else if (isTargetAdmin) { + throw new Error('[[error:cant-delete-admin]'); + } + + // Privilege checks -- only deleteAccount is available for non-admins + const hasAdminPrivilege = await privileges.admin.can('admin:users', caller.uid); + if (!hasAdminPrivilege && ['delete', 'deleteContent'].includes(method)) { + throw new Error('[[error:no-privileges]]'); + } + + // Self-deletions require a password + const hasPassword = await user.hasPassword(uid); + if (isSelf && hasPassword) { + const ok = await user.isPasswordCorrect(uid, password, caller.ip); + if (!ok) { + throw new Error('[[error:invalid-password]]'); + } + } + + await flags.resolveFlag('user', uid, caller.uid); + + let userData; + if (method === 'deleteAccount') { + userData = await user[method](uid); + } else { + userData = await user[method](caller.uid, uid); + } + userData = userData || {}; + + sockets.server.sockets.emit('event:user_status_change', { uid: caller.uid, status: 'offline' }); + + plugins.hooks.fire('action:user.delete', { + callerUid: caller.uid, + uid: uid, + ip: caller.ip, + user: userData, + }); + + await events.log({ + type: `user-${method}`, + uid: caller.uid, + targetUid: uid, + ip: caller.ip, + username: userData.username, + email: userData.email, + }); +} + +async function canDeleteUids(uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + const isMembers = await groups.isMembers(uids, 'administrators'); + if (isMembers.includes(true)) { + throw new Error('[[error:cant-delete-other-admins]]'); + } + + return true; +} + +usersAPI.search = async function (caller, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + const [allowed, isPrivileged] = await Promise.all([ + privileges.global.can('search:users', caller.uid), + user.isPrivileged(caller.uid), + ]); + let filters = data.filters || []; + filters = Array.isArray(filters) ? filters : [filters]; + if (!allowed || + (( + data.searchBy === 'ip' || + data.searchBy === 'email' || + filters.includes('banned') || + filters.includes('flagged') + ) && !isPrivileged) + ) { + throw new Error('[[error:no-privileges]]'); + } + return await user.search({ + query: data.query, + searchBy: data.searchBy || 'username', + page: data.page || 1, + sortBy: data.sortBy || 'lastonline', + filters: filters, + }); +}; + +usersAPI.changePicture = async (caller, data) => { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + + const { type, url } = data; + let picture = ''; + + await user.checkMinReputation(caller.uid, data.uid, 'min:rep:profile-picture'); + const canEdit = await privileges.users.canEdit(caller.uid, data.uid); + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + if (type === 'default') { + picture = ''; + } else if (type === 'uploaded') { + picture = await user.getUserField(data.uid, 'uploadedpicture'); + } else if (type === 'external' && url) { + picture = validator.escape(url); + } else { + const returnData = await plugins.hooks.fire('filter:user.getPicture', { + uid: caller.uid, + type: type, + picture: undefined, + }); + picture = returnData && returnData.picture; + } + + const validBackgrounds = await user.getIconBackgrounds(caller.uid); + if (!validBackgrounds.includes(data.bgColor)) { + data.bgColor = validBackgrounds[0]; + } + + await user.updateProfile(caller.uid, { + uid: data.uid, + picture: picture, + 'icon:bgColor': data.bgColor, + }, ['picture', 'icon:bgColor']); +}; + +usersAPI.generateExport = async (caller, { uid, type }) => { + const count = await db.incrObjectField('locks', `export:${uid}${type}`); + if (count > 1) { + throw new Error('[[error:already-exporting]]'); + } + + const child = require('child_process').fork(`./src/user/jobs/export-${type}.js`, [], { + env: process.env, + }); + child.send({ uid }); + child.on('error', async (err) => { + winston.error(err.stack); + await db.deleteObjectField('locks', `export:${uid}${type}`); + }); + child.on('exit', async () => { + await db.deleteObjectField('locks', `export:${uid}${type}`); + const userData = await user.getUserFields(uid, ['username', 'userslug']); + const { displayname } = userData; + const n = await notifications.create({ + bodyShort: `[[notifications:${type}-exported, ${displayname}]]`, + path: `/api/user/${userData.userslug}/export/${type}`, + nid: `${type}:export:${uid}`, + from: uid, + }); + await notifications.push(n, [caller.uid]); + await events.log({ + type: `export:${type}`, + uid: caller.uid, + targetUid: uid, + ip: caller.ip, + }); + }); +}; diff --git a/src/batch.js b/src/batch.js new file mode 100644 index 0000000000..c53b00783c --- /dev/null +++ b/src/batch.js @@ -0,0 +1,92 @@ + +'use strict'; + +const util = require('util'); + +const db = require('./database'); +const utils = require('./utils'); + +const DEFAULT_BATCH_SIZE = 100; + +const sleep = util.promisify(setTimeout); + +exports.processSortedSet = async function (setKey, process, options) { + options = options || {}; + + if (typeof process !== 'function') { + throw new Error('[[error:process-not-a-function]]'); + } + + // Progress bar handling (upgrade scripts) + if (options.progress) { + options.progress.total = await db.sortedSetCard(setKey); + } + + options.batch = options.batch || DEFAULT_BATCH_SIZE; + + // use the fast path if possible + if (db.processSortedSet && typeof options.doneIf !== 'function' && !utils.isNumber(options.alwaysStartAt)) { + return await db.processSortedSet(setKey, process, options); + } + + // custom done condition + options.doneIf = typeof options.doneIf === 'function' ? options.doneIf : function () {}; + + let start = 0; + let stop = options.batch - 1; + + if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { + process = util.promisify(process); + } + + while (true) { + /* eslint-disable no-await-in-loop */ + const ids = await db[`getSortedSetRange${options.withScores ? 'WithScores' : ''}`](setKey, start, stop); + if (!ids.length || options.doneIf(start, stop, ids)) { + return; + } + await process(ids); + + start += utils.isNumber(options.alwaysStartAt) ? options.alwaysStartAt : options.batch; + stop = start + options.batch - 1; + + if (options.interval) { + await sleep(options.interval); + } + } +}; + +exports.processArray = async function (array, process, options) { + options = options || {}; + + if (!Array.isArray(array) || !array.length) { + return; + } + if (typeof process !== 'function') { + throw new Error('[[error:process-not-a-function]]'); + } + + const batch = options.batch || DEFAULT_BATCH_SIZE; + let start = 0; + if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { + process = util.promisify(process); + } + + while (true) { + const currentBatch = array.slice(start, start + batch); + + if (!currentBatch.length) { + return; + } + + await process(currentBatch); + + start += batch; + + if (options.interval) { + await sleep(options.interval); + } + } +}; + +require('./promisify')(exports); diff --git a/src/cache.js b/src/cache.js new file mode 100644 index 0000000000..c9c633f3d7 --- /dev/null +++ b/src/cache.js @@ -0,0 +1,9 @@ +'use strict'; + +const cacheCreate = require('./cache/lru'); + +module.exports = cacheCreate({ + name: 'local', + max: 40000, + ttl: 0, +}); diff --git a/src/cache/lru.js b/src/cache/lru.js new file mode 100644 index 0000000000..d4bcdd0234 --- /dev/null +++ b/src/cache/lru.js @@ -0,0 +1,146 @@ +'use strict'; + +module.exports = function (opts) { + const LRU = require('lru-cache'); + const pubsub = require('../pubsub'); + + // lru-cache@7 deprecations + const winston = require('winston'); + const chalk = require('chalk'); + + // sometimes we kept passing in `length` with no corresponding `maxSize`. + // This is now enforced in v7; drop superfluous property + if (opts.hasOwnProperty('length') && !opts.hasOwnProperty('maxSize')) { + winston.warn(`[cache/init(${opts.name})] ${chalk.white.bgRed.bold('DEPRECATION')} ${chalk.yellow('length')} was passed in without a corresponding ${chalk.yellow('maxSize')}. Both are now required as of lru-cache@7.0.0.`); + delete opts.length; + } + + const deprecations = new Map([ + ['stale', 'allowStale'], + ['maxAge', 'ttl'], + ['length', 'sizeCalculation'], + ]); + deprecations.forEach((newProp, oldProp) => { + if (opts.hasOwnProperty(oldProp) && !opts.hasOwnProperty(newProp)) { + winston.warn(`[cache/init(${opts.name})] ${chalk.white.bgRed.bold('DEPRECATION')} The option ${chalk.yellow(oldProp)} has been deprecated as of lru-cache@7.0.0. Please change this to ${chalk.yellow(newProp)} instead.`); + opts[newProp] = opts[oldProp]; + delete opts[oldProp]; + } + }); + + const lruCache = new LRU(opts); + + const cache = {}; + cache.name = opts.name; + cache.hits = 0; + cache.misses = 0; + cache.enabled = opts.hasOwnProperty('enabled') ? opts.enabled : true; + const cacheSet = lruCache.set; + + // expose properties while keeping backwards compatibility + const propertyMap = new Map([ + ['length', 'calculatedSize'], + ['calculatedSize', 'calculatedSize'], + ['max', 'max'], + ['maxSize', 'maxSize'], + ['itemCount', 'size'], + ['size', 'size'], + ['ttl', 'ttl'], + ]); + propertyMap.forEach((lruProp, cacheProp) => { + Object.defineProperty(cache, cacheProp, { + get: function () { + return lruCache[lruProp]; + }, + configurable: true, + enumerable: true, + }); + }); + + cache.set = function (key, value, ttl) { + if (!cache.enabled) { + return; + } + const opts = {}; + if (ttl) { + opts.ttl = ttl; + } + cacheSet.apply(lruCache, [key, value, opts]); + }; + + cache.get = function (key) { + if (!cache.enabled) { + return undefined; + } + const data = lruCache.get(key); + if (data === undefined) { + cache.misses += 1; + } else { + cache.hits += 1; + } + return data; + }; + + cache.del = function (keys) { + if (!Array.isArray(keys)) { + keys = [keys]; + } + pubsub.publish(`${cache.name}:lruCache:del`, keys); + keys.forEach(key => lruCache.delete(key)); + }; + cache.delete = cache.del; + + cache.reset = function () { + pubsub.publish(`${cache.name}:lruCache:reset`); + localReset(); + }; + cache.clear = cache.reset; + + function localReset() { + lruCache.clear(); + cache.hits = 0; + cache.misses = 0; + } + + pubsub.on(`${cache.name}:lruCache:reset`, () => { + localReset(); + }); + + pubsub.on(`${cache.name}:lruCache:del`, (keys) => { + if (Array.isArray(keys)) { + keys.forEach(key => lruCache.delete(key)); + } + }); + + cache.getUnCachedKeys = function (keys, cachedData) { + if (!cache.enabled) { + return keys; + } + let data; + let isCached; + const unCachedKeys = keys.filter((key) => { + data = cache.get(key); + isCached = data !== undefined; + if (isCached) { + cachedData[key] = data; + } + return !isCached; + }); + + const hits = keys.length - unCachedKeys.length; + const misses = keys.length - hits; + cache.hits += hits; + cache.misses += misses; + return unCachedKeys; + }; + + cache.dump = function () { + return lruCache.dump(); + }; + + cache.peek = function (key) { + return lruCache.peek(key); + }; + + return cache; +}; diff --git a/src/cache/ttl.js b/src/cache/ttl.js new file mode 100644 index 0000000000..51dcdab5fd --- /dev/null +++ b/src/cache/ttl.js @@ -0,0 +1,119 @@ +'use strict'; + +module.exports = function (opts) { + const TTLCache = require('@isaacs/ttlcache'); + const pubsub = require('../pubsub'); + + const ttlCache = new TTLCache(opts); + + const cache = {}; + cache.name = opts.name; + cache.hits = 0; + cache.misses = 0; + cache.enabled = opts.hasOwnProperty('enabled') ? opts.enabled : true; + const cacheSet = ttlCache.set; + + // expose properties + const propertyMap = new Map([ + ['max', 'max'], + ['itemCount', 'size'], + ['size', 'size'], + ['ttl', 'ttl'], + ]); + propertyMap.forEach((ttlProp, cacheProp) => { + Object.defineProperty(cache, cacheProp, { + get: function () { + return ttlCache[ttlProp]; + }, + configurable: true, + enumerable: true, + }); + }); + + cache.set = function (key, value, ttl) { + if (!cache.enabled) { + return; + } + const opts = {}; + if (ttl) { + opts.ttl = ttl; + } + cacheSet.apply(ttlCache, [key, value, opts]); + }; + + cache.get = function (key) { + if (!cache.enabled) { + return undefined; + } + const data = ttlCache.get(key); + if (data === undefined) { + cache.misses += 1; + } else { + cache.hits += 1; + } + return data; + }; + + cache.del = function (keys) { + if (!Array.isArray(keys)) { + keys = [keys]; + } + pubsub.publish(`${cache.name}:ttlCache:del`, keys); + keys.forEach(key => ttlCache.delete(key)); + }; + cache.delete = cache.del; + + cache.reset = function () { + pubsub.publish(`${cache.name}:ttlCache:reset`); + localReset(); + }; + cache.clear = cache.reset; + + function localReset() { + ttlCache.clear(); + cache.hits = 0; + cache.misses = 0; + } + + pubsub.on(`${cache.name}:ttlCache:reset`, () => { + localReset(); + }); + + pubsub.on(`${cache.name}:ttlCache:del`, (keys) => { + if (Array.isArray(keys)) { + keys.forEach(key => ttlCache.delete(key)); + } + }); + + cache.getUnCachedKeys = function (keys, cachedData) { + if (!cache.enabled) { + return keys; + } + let data; + let isCached; + const unCachedKeys = keys.filter((key) => { + data = cache.get(key); + isCached = data !== undefined; + if (isCached) { + cachedData[key] = data; + } + return !isCached; + }); + + const hits = keys.length - unCachedKeys.length; + const misses = keys.length - hits; + cache.hits += hits; + cache.misses += misses; + return unCachedKeys; + }; + + cache.dump = function () { + return Array.from(ttlCache.entries()); + }; + + cache.peek = function (key) { + return ttlCache.get(key, { updateAgeOnGet: false }); + }; + + return cache; +}; diff --git a/src/cacheCreate.js b/src/cacheCreate.js new file mode 100644 index 0000000000..14a5a7a79b --- /dev/null +++ b/src/cacheCreate.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./cache/lru'); diff --git a/src/categories/activeusers.js b/src/categories/activeusers.js new file mode 100644 index 0000000000..77f801a63c --- /dev/null +++ b/src/categories/activeusers.js @@ -0,0 +1,17 @@ +'use strict'; + +const _ = require('lodash'); + +const posts = require('../posts'); +const db = require('../database'); + +module.exports = function (Categories) { + Categories.getActiveUsers = async function (cids) { + if (!Array.isArray(cids)) { + cids = [cids]; + } + const pids = await db.getSortedSetRevRange(cids.map(cid => `cid:${cid}:pids`), 0, 24); + const postData = await posts.getPostsFields(pids, ['uid']); + return _.uniq(postData.map(post => post.uid).filter(uid => uid)); + }; +}; diff --git a/src/categories/create.js b/src/categories/create.js new file mode 100644 index 0000000000..01a3c337c2 --- /dev/null +++ b/src/categories/create.js @@ -0,0 +1,250 @@ +'use strict'; + +const async = require('async'); +const _ = require('lodash'); + +const db = require('../database'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); +const utils = require('../utils'); +const slugify = require('../slugify'); +const cache = require('../cache'); + +module.exports = function (Categories) { + Categories.create = async function (data) { + const parentCid = data.parentCid ? data.parentCid : 0; + const [cid, firstChild] = await Promise.all([ + db.incrObjectField('global', 'nextCid'), + db.getSortedSetRangeWithScores(`cid:${parentCid}:children`, 0, 0), + ]); + + data.name = String(data.name || `Category ${cid}`); + const slug = `${cid}/${slugify(data.name)}`; + const smallestOrder = firstChild.length ? firstChild[0].score - 1 : 1; + const order = data.order || smallestOrder; // If no order provided, place it at the top + const colours = Categories.assignColours(); + + let category = { + cid: cid, + name: data.name, + description: data.description ? data.description : '', + descriptionParsed: data.descriptionParsed ? data.descriptionParsed : '', + icon: data.icon ? data.icon : '', + bgColor: data.bgColor || colours[0], + color: data.color || colours[1], + slug: slug, + parentCid: parentCid, + topic_count: 0, + post_count: 0, + disabled: data.disabled ? 1 : 0, + order: order, + link: data.link || '', + numRecentReplies: 1, + class: (data.class ? data.class : 'col-md-3 col-xs-6'), + imageClass: 'cover', + isSection: 0, + subCategoriesPerPage: 10, + }; + + if (data.backgroundImage) { + category.backgroundImage = data.backgroundImage; + } + + const defaultPrivileges = [ + 'groups:find', + 'groups:read', + 'groups:topics:read', + 'groups:topics:create', + 'groups:topics:reply', + 'groups:topics:tag', + 'groups:posts:edit', + 'groups:posts:history', + 'groups:posts:delete', + 'groups:posts:upvote', + 'groups:posts:downvote', + 'groups:topics:delete', + ]; + const modPrivileges = defaultPrivileges.concat([ + 'groups:topics:schedule', + 'groups:posts:view_deleted', + 'groups:purge', + ]); + const guestPrivileges = ['groups:find', 'groups:read', 'groups:topics:read']; + + const result = await plugins.hooks.fire('filter:category.create', { + category: category, + data: data, + defaultPrivileges: defaultPrivileges, + modPrivileges: modPrivileges, + guestPrivileges: guestPrivileges, + }); + category = result.category; + + await db.setObject(`category:${category.cid}`, category); + if (!category.descriptionParsed) { + await Categories.parseDescription(category.cid, category.description); + } + + await db.sortedSetAddBulk([ + ['categories:cid', category.order, category.cid], + [`cid:${parentCid}:children`, category.order, category.cid], + ['categories:name', 0, `${data.name.slice(0, 200).toLowerCase()}:${category.cid}`], + ]); + + await privileges.categories.give(result.defaultPrivileges, category.cid, 'registered-users'); + await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']); + await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']); + + cache.del([ + 'categories:cid', + `cid:${parentCid}:children`, + `cid:${parentCid}:children:all`, + ]); + if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { + category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid); + } + + if (data.cloneChildren) { + await duplicateCategoriesChildren(category.cid, data.cloneFromCid, data.uid); + } + + plugins.hooks.fire('action:category.create', { category: category }); + return category; + }; + + async function duplicateCategoriesChildren(parentCid, cid, uid) { + let children = await Categories.getChildren([cid], uid); + if (!children.length) { + return; + } + + children = children[0]; + + children.forEach((child) => { + child.parentCid = parentCid; + child.cloneFromCid = child.cid; + child.cloneChildren = true; + child.name = utils.decodeHTMLEntities(child.name); + child.description = utils.decodeHTMLEntities(child.description); + child.uid = uid; + }); + + await async.each(children, Categories.create); + } + + Categories.assignColours = function () { + const backgrounds = ['#AB4642', '#DC9656', '#F7CA88', '#A1B56C', '#86C1B9', '#7CAFC2', '#BA8BAF', '#A16946']; + const text = ['#ffffff', '#ffffff', '#333333', '#ffffff', '#333333', '#ffffff', '#ffffff', '#ffffff']; + const index = Math.floor(Math.random() * backgrounds.length); + return [backgrounds[index], text[index]]; + }; + + Categories.copySettingsFrom = async function (fromCid, toCid, copyParent) { + const [source, destination] = await Promise.all([ + db.getObject(`category:${fromCid}`), + db.getObject(`category:${toCid}`), + ]); + if (!source) { + throw new Error('[[error:invalid-cid]]'); + } + + const oldParent = parseInt(destination.parentCid, 10) || 0; + const newParent = parseInt(source.parentCid, 10) || 0; + if (copyParent && newParent !== parseInt(toCid, 10)) { + await db.sortedSetRemove(`cid:${oldParent}:children`, toCid); + await db.sortedSetAdd(`cid:${newParent}:children`, source.order, toCid); + cache.del([ + `cid:${oldParent}:children`, + `cid:${oldParent}:children:all`, + `cid:${newParent}:children`, + `cid:${newParent}:children:all`, + ]); + } + + destination.description = source.description; + destination.descriptionParsed = source.descriptionParsed; + destination.icon = source.icon; + destination.bgColor = source.bgColor; + destination.color = source.color; + destination.link = source.link; + destination.numRecentReplies = source.numRecentReplies; + destination.class = source.class; + destination.image = source.image; + destination.imageClass = source.imageClass; + destination.minTags = source.minTags; + destination.maxTags = source.maxTags; + + if (copyParent) { + destination.parentCid = source.parentCid || 0; + } + await plugins.hooks.fire('filter:categories.copySettingsFrom', { + source: source, + destination: destination, + copyParent: copyParent, + }); + + await db.setObject(`category:${toCid}`, destination); + + await copyTagWhitelist(fromCid, toCid); + + await Categories.copyPrivilegesFrom(fromCid, toCid); + + return destination; + }; + + async function copyTagWhitelist(fromCid, toCid) { + const data = await db.getSortedSetRangeWithScores(`cid:${fromCid}:tag:whitelist`, 0, -1); + await db.delete(`cid:${toCid}:tag:whitelist`); + await db.sortedSetAdd(`cid:${toCid}:tag:whitelist`, data.map(item => item.score), data.map(item => item.value)); + cache.del(`cid:${toCid}:tag:whitelist`); + } + + Categories.copyPrivilegesFrom = async function (fromCid, toCid, group, filter = []) { + group = group || ''; + let privsToCopy; + if (group) { + const groupPrivilegeList = await privileges.categories.getGroupPrivilegeList(); + privsToCopy = groupPrivilegeList.slice(...filter); + } else { + const privs = await privileges.categories.getPrivilegeList(); + const halfIdx = privs.length / 2; + privsToCopy = privs.slice(0, halfIdx).slice(...filter).concat(privs.slice(halfIdx).slice(...filter)); + } + + const data = await plugins.hooks.fire('filter:categories.copyPrivilegesFrom', { + privileges: privsToCopy, + fromCid: fromCid, + toCid: toCid, + group: group, + }); + if (group) { + await copyPrivilegesByGroup(data.privileges, data.fromCid, data.toCid, group); + } else { + await copyPrivileges(data.privileges, data.fromCid, data.toCid); + } + }; + + async function copyPrivileges(privileges, fromCid, toCid) { + const toGroups = privileges.map(privilege => `group:cid:${toCid}:privileges:${privilege}:members`); + const fromGroups = privileges.map(privilege => `group:cid:${fromCid}:privileges:${privilege}:members`); + + const currentMembers = await db.getSortedSetsMembers(toGroups.concat(fromGroups)); + const copyGroups = _.uniq(_.flatten(currentMembers)); + await async.each(copyGroups, async (group) => { + await copyPrivilegesByGroup(privileges, fromCid, toCid, group); + }); + } + + async function copyPrivilegesByGroup(privilegeList, fromCid, toCid, group) { + const fromGroups = privilegeList.map(privilege => `group:cid:${fromCid}:privileges:${privilege}:members`); + const toGroups = privilegeList.map(privilege => `group:cid:${toCid}:privileges:${privilege}:members`); + const [fromChecks, toChecks] = await Promise.all([ + db.isMemberOfSortedSets(fromGroups, group), + db.isMemberOfSortedSets(toGroups, group), + ]); + const givePrivs = privilegeList.filter((priv, index) => fromChecks[index] && !toChecks[index]); + const rescindPrivs = privilegeList.filter((priv, index) => !fromChecks[index] && toChecks[index]); + await privileges.categories.give(givePrivs, toCid, group); + await privileges.categories.rescind(rescindPrivs, toCid, group); + } +}; diff --git a/src/categories/data.js b/src/categories/data.js new file mode 100644 index 0000000000..5e32f7a9a3 --- /dev/null +++ b/src/categories/data.js @@ -0,0 +1,112 @@ +'use strict'; + +const validator = require('validator'); + +const db = require('../database'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const intFields = [ + 'cid', 'parentCid', 'disabled', 'isSection', 'order', + 'topic_count', 'post_count', 'numRecentReplies', + 'minTags', 'maxTags', 'postQueue', 'subCategoriesPerPage', +]; + +module.exports = function (Categories) { + Categories.getCategoriesFields = async function (cids, fields) { + if (!Array.isArray(cids) || !cids.length) { + return []; + } + + const keys = cids.map(cid => `category:${cid}`); + const categories = await db.getObjects(keys, fields); + const result = await plugins.hooks.fire('filter:category.getFields', { + cids: cids, + categories: categories, + fields: fields, + keys: keys, + }); + result.categories.forEach(category => modifyCategory(category, fields)); + return result.categories; + }; + + Categories.getCategoryData = async function (cid) { + const categories = await Categories.getCategoriesFields([cid], []); + return categories && categories.length ? categories[0] : null; + }; + + Categories.getCategoriesData = async function (cids) { + return await Categories.getCategoriesFields(cids, []); + }; + + Categories.getCategoryField = async function (cid, field) { + const category = await Categories.getCategoryFields(cid, [field]); + return category ? category[field] : null; + }; + + Categories.getCategoryFields = async function (cid, fields) { + const categories = await Categories.getCategoriesFields([cid], fields); + return categories ? categories[0] : null; + }; + + Categories.getAllCategoryFields = async function (fields) { + const cids = await Categories.getAllCidsFromSet('categories:cid'); + return await Categories.getCategoriesFields(cids, fields); + }; + + Categories.setCategoryField = async function (cid, field, value) { + await db.setObjectField(`category:${cid}`, field, value); + }; + + Categories.incrementCategoryFieldBy = async function (cid, field, value) { + await db.incrObjectFieldBy(`category:${cid}`, field, value); + }; +}; + +function defaultIntField(category, fields, fieldName, defaultField) { + if (!fields.length || fields.includes(fieldName)) { + const useDefault = !category.hasOwnProperty(fieldName) || + category[fieldName] === null || + category[fieldName] === '' || + !utils.isNumber(category[fieldName]); + + category[fieldName] = useDefault ? meta.config[defaultField] : category[fieldName]; + } +} + +function modifyCategory(category, fields) { + if (!category) { + return; + } + + defaultIntField(category, fields, 'minTags', 'minimumTagsPerTopic'); + defaultIntField(category, fields, 'maxTags', 'maximumTagsPerTopic'); + defaultIntField(category, fields, 'postQueue', 'postQueue'); + + db.parseIntFields(category, intFields, fields); + + const escapeFields = ['name', 'color', 'bgColor', 'backgroundImage', 'imageClass', 'class', 'link']; + escapeFields.forEach((field) => { + if (category.hasOwnProperty(field)) { + category[field] = validator.escape(String(category[field] || '')); + } + }); + + if (category.hasOwnProperty('icon')) { + category.icon = category.icon || 'hidden'; + } + + if (category.hasOwnProperty('post_count')) { + category.totalPostCount = category.post_count; + } + + if (category.hasOwnProperty('topic_count')) { + category.totalTopicCount = category.topic_count; + } + + if (category.description) { + category.description = validator.escape(String(category.description)); + category.descriptionParsed = category.descriptionParsed || category.description; + } +} diff --git a/src/categories/delete.js b/src/categories/delete.js new file mode 100644 index 0000000000..a1b91f4032 --- /dev/null +++ b/src/categories/delete.js @@ -0,0 +1,91 @@ +'use strict'; + +const async = require('async'); +const db = require('../database'); +const batch = require('../batch'); +const plugins = require('../plugins'); +const topics = require('../topics'); +const groups = require('../groups'); +const privileges = require('../privileges'); +const cache = require('../cache'); + +module.exports = function (Categories) { + Categories.purge = async function (cid, uid) { + await batch.processSortedSet(`cid:${cid}:tids`, async (tids) => { + await async.eachLimit(tids, 10, async (tid) => { + await topics.purgePostsAndTopic(tid, uid); + }); + }, { alwaysStartAt: 0 }); + + const pinnedTids = await db.getSortedSetRevRange(`cid:${cid}:tids:pinned`, 0, -1); + await async.eachLimit(pinnedTids, 10, async (tid) => { + await topics.purgePostsAndTopic(tid, uid); + }); + const categoryData = await Categories.getCategoryData(cid); + await purgeCategory(cid, categoryData); + plugins.hooks.fire('action:category.delete', { cid: cid, uid: uid, category: categoryData }); + }; + + async function purgeCategory(cid, categoryData) { + const bulkRemove = [['categories:cid', cid]]; + if (categoryData && categoryData.name) { + bulkRemove.push(['categories:name', `${categoryData.name.slice(0, 200).toLowerCase()}:${cid}`]); + } + await db.sortedSetRemoveBulk(bulkRemove); + + await removeFromParent(cid); + await deleteTags(cid); + await db.deleteAll([ + `cid:${cid}:tids`, + `cid:${cid}:tids:pinned`, + `cid:${cid}:tids:posts`, + `cid:${cid}:tids:votes`, + `cid:${cid}:tids:views`, + `cid:${cid}:tids:lastposttime`, + `cid:${cid}:recent_tids`, + `cid:${cid}:pids`, + `cid:${cid}:read_by_uid`, + `cid:${cid}:uid:watch:state`, + `cid:${cid}:children`, + `cid:${cid}:tag:whitelist`, + `category:${cid}`, + ]); + const privilegeList = await privileges.categories.getPrivilegeList(); + await groups.destroy(privilegeList.map(privilege => `cid:${cid}:privileges:${privilege}`)); + } + + async function removeFromParent(cid) { + const [parentCid, children] = await Promise.all([ + Categories.getCategoryField(cid, 'parentCid'), + db.getSortedSetRange(`cid:${cid}:children`, 0, -1), + ]); + + const bulkAdd = []; + const childrenKeys = children.map((cid) => { + bulkAdd.push(['cid:0:children', cid, cid]); + return `category:${cid}`; + }); + + await Promise.all([ + db.sortedSetRemove(`cid:${parentCid}:children`, cid), + db.setObjectField(childrenKeys, 'parentCid', 0), + db.sortedSetAddBulk(bulkAdd), + ]); + + cache.del([ + 'categories:cid', + 'cid:0:children', + `cid:${parentCid}:children`, + `cid:${parentCid}:children:all`, + `cid:${cid}:children`, + `cid:${cid}:children:all`, + `cid:${cid}:tag:whitelist`, + ]); + } + + async function deleteTags(cid) { + const tags = await db.getSortedSetMembers(`cid:${cid}:tags`); + await db.deleteAll(tags.map(tag => `cid:${cid}:tag:${tag}:topics`)); + await db.delete(`cid:${cid}:tags`); + } +}; diff --git a/src/categories/index.js b/src/categories/index.js new file mode 100644 index 0000000000..7330369385 --- /dev/null +++ b/src/categories/index.js @@ -0,0 +1,409 @@ + +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const user = require('../user'); +const groups = require('../groups'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); +const cache = require('../cache'); +const meta = require('../meta'); + +const Categories = module.exports; + +require('./data')(Categories); +require('./create')(Categories); +require('./delete')(Categories); +require('./topics')(Categories); +require('./unread')(Categories); +require('./activeusers')(Categories); +require('./recentreplies')(Categories); +require('./update')(Categories); +require('./watch')(Categories); +require('./search')(Categories); + +Categories.exists = async function (cids) { + return await db.exists( + Array.isArray(cids) ? cids.map(cid => `category:${cid}`) : `category:${cids}` + ); +}; + +Categories.getCategoryById = async function (data) { + const categories = await Categories.getCategories([data.cid], data.uid); + if (!categories[0]) { + return null; + } + const category = categories[0]; + data.category = category; + + const promises = [ + Categories.getCategoryTopics(data), + Categories.getTopicCount(data), + Categories.getWatchState([data.cid], data.uid), + getChildrenTree(category, data.uid), + ]; + + if (category.parentCid) { + promises.push(Categories.getCategoryData(category.parentCid)); + } + const [topics, topicCount, watchState, , parent] = await Promise.all(promises); + + category.topics = topics.topics; + category.nextStart = topics.nextStart; + category.topic_count = topicCount; + category.isWatched = watchState[0] === Categories.watchStates.watching; + category.isNotWatched = watchState[0] === Categories.watchStates.notwatching; + category.isIgnored = watchState[0] === Categories.watchStates.ignoring; + category.parent = parent; + + calculateTopicPostCount(category); + const result = await plugins.hooks.fire('filter:category.get', { + category: category, + ...data, + }); + return result.category; +}; + +Categories.getAllCidsFromSet = async function (key) { + let cids = cache.get(key); + if (cids) { + return cids.slice(); + } + + cids = await db.getSortedSetRange(key, 0, -1); + cids = cids.map(cid => parseInt(cid, 10)); + cache.set(key, cids); + return cids.slice(); +}; + +Categories.getAllCategories = async function (uid) { + const cids = await Categories.getAllCidsFromSet('categories:cid'); + return await Categories.getCategories(cids, uid); +}; + +Categories.getCidsByPrivilege = async function (set, uid, privilege) { + const cids = await Categories.getAllCidsFromSet(set); + return await privileges.categories.filterCids(privilege, cids, uid); +}; + +Categories.getCategoriesByPrivilege = async function (set, uid, privilege) { + const cids = await Categories.getCidsByPrivilege(set, uid, privilege); + return await Categories.getCategories(cids, uid); +}; + +Categories.getModerators = async function (cid) { + const uids = await Categories.getModeratorUids([cid]); + return await user.getUsersFields(uids[0], ['uid', 'username', 'userslug', 'picture']); +}; + +Categories.getModeratorUids = async function (cids) { + const groupNames = cids.reduce((memo, cid) => { + memo.push(`cid:${cid}:privileges:moderate`); + memo.push(`cid:${cid}:privileges:groups:moderate`); + return memo; + }, []); + + const memberSets = await groups.getMembersOfGroups(groupNames); + // Every other set is actually a list of user groups, not uids, so convert those to members + const sets = memberSets.reduce((memo, set, idx) => { + if (idx % 2) { + memo.groupNames.push(set); + } else { + memo.uids.push(set); + } + + return memo; + }, { groupNames: [], uids: [] }); + + const uniqGroups = _.uniq(_.flatten(sets.groupNames)); + const groupUids = await groups.getMembersOfGroups(uniqGroups); + const map = _.zipObject(uniqGroups, groupUids); + const moderatorUids = cids.map( + (cid, index) => _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g])))) + ); + return moderatorUids; +}; + +Categories.getCategories = async function (cids, uid) { + if (!Array.isArray(cids)) { + throw new Error('[[error:invalid-cid]]'); + } + + if (!cids.length) { + return []; + } + uid = parseInt(uid, 10); + + const [categories, tagWhitelist, hasRead] = await Promise.all([ + Categories.getCategoriesData(cids), + Categories.getTagWhitelist(cids), + Categories.hasReadCategories(cids, uid), + ]); + categories.forEach((category, i) => { + if (category) { + category.tagWhitelist = tagWhitelist[i]; + category['unread-class'] = (category.topic_count === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread'; + } + }); + return categories; +}; + +Categories.getTagWhitelist = async function (cids) { + const cachedData = {}; + + const nonCachedCids = cids.filter((cid) => { + const data = cache.get(`cid:${cid}:tag:whitelist`); + const isInCache = data !== undefined; + if (isInCache) { + cachedData[cid] = data; + } + return !isInCache; + }); + + if (!nonCachedCids.length) { + return cids.map(cid => cachedData[cid]); + } + + const keys = nonCachedCids.map(cid => `cid:${cid}:tag:whitelist`); + const data = await db.getSortedSetsMembers(keys); + + nonCachedCids.forEach((cid, index) => { + cachedData[cid] = data[index]; + cache.set(`cid:${cid}:tag:whitelist`, data[index]); + }); + return cids.map(cid => cachedData[cid]); +}; + +// remove system tags from tag whitelist for non privileged user +Categories.filterTagWhitelist = function (tagWhitelist, isAdminOrMod) { + const systemTags = (meta.config.systemTags || '').split(','); + if (!isAdminOrMod && systemTags.length) { + return tagWhitelist.filter(tag => !systemTags.includes(tag)); + } + return tagWhitelist; +}; + +function calculateTopicPostCount(category) { + if (!category) { + return; + } + + let postCount = category.post_count; + let topicCount = category.topic_count; + if (Array.isArray(category.children)) { + category.children.forEach((child) => { + calculateTopicPostCount(child); + postCount += parseInt(child.totalPostCount, 10) || 0; + topicCount += parseInt(child.totalTopicCount, 10) || 0; + }); + } + + category.totalPostCount = postCount; + category.totalTopicCount = topicCount; +} +Categories.calculateTopicPostCount = calculateTopicPostCount; + +Categories.getParents = async function (cids) { + const categoriesData = await Categories.getCategoriesFields(cids, ['parentCid']); + const parentCids = categoriesData.filter(c => c && c.parentCid).map(c => c.parentCid); + if (!parentCids.length) { + return cids.map(() => null); + } + const parentData = await Categories.getCategoriesData(parentCids); + const cidToParent = _.zipObject(parentCids, parentData); + return categoriesData.map(category => cidToParent[category.parentCid]); +}; + +Categories.getChildren = async function (cids, uid) { + const categoryData = await Categories.getCategoriesFields(cids, ['parentCid']); + const categories = categoryData.map((category, index) => ({ cid: cids[index], parentCid: category.parentCid })); + await Promise.all(categories.map(c => getChildrenTree(c, uid))); + return categories.map(c => c && c.children); +}; + +async function getChildrenTree(category, uid) { + let childrenCids = await Categories.getChildrenCids(category.cid); + childrenCids = await privileges.categories.filterCids('find', childrenCids, uid); + childrenCids = childrenCids.filter(cid => parseInt(category.cid, 10) !== parseInt(cid, 10)); + if (!childrenCids.length) { + category.children = []; + return; + } + let childrenData = await Categories.getCategoriesData(childrenCids); + childrenData = childrenData.filter(Boolean); + childrenCids = childrenData.map(child => child.cid); + const hasRead = await Categories.hasReadCategories(childrenCids, uid); + childrenData.forEach((child, i) => { + child['unread-class'] = (child.topic_count === 0 || (hasRead[i] && uid !== 0)) ? '' : 'unread'; + }); + Categories.getTree([category].concat(childrenData), category.parentCid); +} + +Categories.getChildrenTree = getChildrenTree; + +Categories.getParentCids = async function (currentCid) { + let cid = currentCid; + const parents = []; + while (parseInt(cid, 10)) { + // eslint-disable-next-line + cid = await Categories.getCategoryField(cid, 'parentCid'); + if (cid) { + parents.unshift(cid); + } + } + return parents; +}; + +Categories.getChildrenCids = async function (rootCid) { + let allCids = []; + async function recursive(keys) { + let childrenCids = await db.getSortedSetRange(keys, 0, -1); + + childrenCids = childrenCids.filter(cid => !allCids.includes(parseInt(cid, 10))); + if (!childrenCids.length) { + return; + } + keys = childrenCids.map(cid => `cid:${cid}:children`); + childrenCids.forEach(cid => allCids.push(parseInt(cid, 10))); + await recursive(keys); + } + const key = `cid:${rootCid}:children`; + const cacheKey = `${key}:all`; + const childrenCids = cache.get(cacheKey); + if (childrenCids) { + return childrenCids.slice(); + } + + await recursive(key); + allCids = _.uniq(allCids); + cache.set(cacheKey, allCids); + return allCids.slice(); +}; + +Categories.flattenCategories = function (allCategories, categoryData) { + categoryData.forEach((category) => { + if (category) { + allCategories.push(category); + + if (Array.isArray(category.children) && category.children.length) { + Categories.flattenCategories(allCategories, category.children); + } + } + }); +}; + +/** + * build tree from flat list of categories + * + * @param categories {array} flat list of categories + * @param parentCid {number} start from 0 to build full tree + */ +Categories.getTree = function (categories, parentCid) { + parentCid = parentCid || 0; + const cids = categories.map(category => category && category.cid); + const cidToCategory = {}; + const parents = {}; + cids.forEach((cid, index) => { + if (cid) { + categories[index].children = undefined; + cidToCategory[cid] = categories[index]; + parents[cid] = { ...categories[index] }; + } + }); + + const tree = []; + + categories.forEach((category) => { + if (category) { + category.children = category.children || []; + if (!category.cid) { + return; + } + if (!category.hasOwnProperty('parentCid') || category.parentCid === null) { + category.parentCid = 0; + } + if (category.parentCid === parentCid) { + tree.push(category); + category.parent = parents[parentCid]; + } else { + const parent = cidToCategory[category.parentCid]; + if (parent && parent.cid !== category.cid) { + category.parent = parents[category.parentCid]; + parent.children = parent.children || []; + parent.children.push(category); + } + } + } + }); + function sortTree(tree) { + tree.sort((a, b) => { + if (a.order !== b.order) { + return a.order - b.order; + } + return a.cid - b.cid; + }); + tree.forEach((category) => { + if (category && Array.isArray(category.children)) { + sortTree(category.children); + } + }); + } + sortTree(tree); + + categories.forEach(c => calculateTopicPostCount(c)); + return tree; +}; + +Categories.buildForSelect = async function (uid, privilege, fields) { + const cids = await Categories.getCidsByPrivilege('categories:cid', uid, privilege); + return await getSelectData(cids, fields); +}; + +Categories.buildForSelectAll = async function (fields) { + const cids = await Categories.getAllCidsFromSet('categories:cid'); + return await getSelectData(cids, fields); +}; + +async function getSelectData(cids, fields) { + const categoryData = await Categories.getCategoriesData(cids); + const tree = Categories.getTree(categoryData); + return Categories.buildForSelectCategories(tree, fields); +} + +Categories.buildForSelectCategories = function (categories, fields, parentCid) { + function recursive(category, categoriesData, level, depth) { + const bullet = level ? '• ' : ''; + category.value = category.cid; + category.level = level; + category.text = level + bullet + category.name; + category.depth = depth; + categoriesData.push(category); + if (Array.isArray(category.children)) { + category.children.forEach(child => recursive(child, categoriesData, `    ${level}`, depth + 1)); + } + } + parentCid = parentCid || 0; + const categoriesData = []; + + const rootCategories = categories.filter(category => category && category.parentCid === parentCid); + + rootCategories.forEach(category => recursive(category, categoriesData, '', 0)); + + const pickFields = [ + 'cid', 'name', 'level', 'icon', 'parentCid', + 'color', 'bgColor', 'backgroundImage', 'imageClass', + ]; + fields = fields || []; + if (fields.includes('text') && fields.includes('value')) { + return categoriesData.map(category => _.pick(category, fields)); + } + if (fields.length) { + pickFields.push(...fields); + } + + return categoriesData.map(category => _.pick(category, pickFields)); +}; + +require('../promisify')(Categories); diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js new file mode 100644 index 0000000000..e945217e7d --- /dev/null +++ b/src/categories/recentreplies.js @@ -0,0 +1,212 @@ + +'use strict'; + +const winston = require('winston'); +const _ = require('lodash'); + +const db = require('../database'); +const posts = require('../posts'); +const topics = require('../topics'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const batch = require('../batch'); + +module.exports = function (Categories) { + Categories.getRecentReplies = async function (cid, uid, start, stop) { + // backwards compatibility, treat start as count + if (stop === undefined && start > 0) { + winston.warn('[Categories.getRecentReplies] 3 params deprecated please use Categories.getRecentReplies(cid, uid, start, stop)'); + stop = start - 1; + start = 0; + } + let pids = await db.getSortedSetRevRange(`cid:${cid}:pids`, start, stop); + pids = await privileges.posts.filter('topics:read', pids, uid); + return await posts.getPostSummaryByPids(pids, uid, { stripTags: true }); + }; + + Categories.updateRecentTid = async function (cid, tid) { + const [count, numRecentReplies] = await Promise.all([ + db.sortedSetCard(`cid:${cid}:recent_tids`), + db.getObjectField(`category:${cid}`, 'numRecentReplies'), + ]); + + if (count >= numRecentReplies) { + const data = await db.getSortedSetRangeWithScores(`cid:${cid}:recent_tids`, 0, count - numRecentReplies); + const shouldRemove = !(data.length === 1 && count === 1 && data[0].value === String(tid)); + if (data.length && shouldRemove) { + await db.sortedSetsRemoveRangeByScore([`cid:${cid}:recent_tids`], '-inf', data[data.length - 1].score); + } + } + if (numRecentReplies > 0) { + await db.sortedSetAdd(`cid:${cid}:recent_tids`, Date.now(), tid); + } + await plugins.hooks.fire('action:categories.updateRecentTid', { cid: cid, tid: tid }); + }; + + Categories.updateRecentTidForCid = async function (cid) { + let postData; + let topicData; + let index = 0; + do { + /* eslint-disable no-await-in-loop */ + const pids = await db.getSortedSetRevRange(`cid:${cid}:pids`, index, index); + if (!pids.length) { + return; + } + postData = await posts.getPostFields(pids[0], ['tid', 'deleted']); + + if (postData && postData.tid && !postData.deleted) { + topicData = await topics.getTopicData(postData.tid); + } + index += 1; + } while (!topicData || topicData.deleted || topicData.scheduled); + + if (postData && postData.tid) { + await Categories.updateRecentTid(cid, postData.tid); + } + }; + + Categories.getRecentTopicReplies = async function (categoryData, uid, query) { + if (!Array.isArray(categoryData) || !categoryData.length) { + return; + } + const categoriesToLoad = + categoryData.filter(c => c && c.numRecentReplies && parseInt(c.numRecentReplies, 10) > 0); + let keys = []; + if (plugins.hooks.hasListeners('filter:categories.getRecentTopicReplies')) { + const result = await plugins.hooks.fire('filter:categories.getRecentTopicReplies', { + categories: categoriesToLoad, + uid: uid, + query: query, + keys: [], + }); + keys = result.keys; + } else { + keys = categoriesToLoad.map(c => `cid:${c.cid}:recent_tids`); + } + + const results = await db.getSortedSetsMembers(keys); + let tids = _.uniq(_.flatten(results).filter(Boolean)); + + tids = await privileges.topics.filterTids('topics:read', tids, uid); + const topics = await getTopics(tids, uid); + assignTopicsToCategories(categoryData, topics); + + bubbleUpChildrenPosts(categoryData); + }; + + async function getTopics(tids, uid) { + const topicData = await topics.getTopicsFields( + tids, + ['tid', 'mainPid', 'slug', 'title', 'teaserPid', 'cid', 'postcount'] + ); + topicData.forEach((topic) => { + if (topic) { + topic.teaserPid = topic.teaserPid || topic.mainPid; + } + }); + const cids = _.uniq(topicData.map(t => t && t.cid).filter(cid => parseInt(cid, 10))); + const getToRoot = async () => await Promise.all(cids.map(Categories.getParentCids)); + const [toRoot, teasers] = await Promise.all([ + getToRoot(), + topics.getTeasers(topicData, uid), + ]); + const cidToRoot = _.zipObject(cids, toRoot); + + teasers.forEach((teaser, index) => { + if (teaser) { + teaser.cid = topicData[index].cid; + teaser.parentCids = cidToRoot[teaser.cid]; + teaser.tid = undefined; + teaser.uid = undefined; + teaser.topic = { + slug: topicData[index].slug, + title: topicData[index].title, + }; + } + }); + return teasers.filter(Boolean); + } + + function assignTopicsToCategories(categories, topics) { + categories.forEach((category) => { + if (category) { + category.posts = topics + .filter(t => t.cid && (t.cid === category.cid || t.parentCids.includes(category.cid))) + .sort((a, b) => b.pid - a.pid) + .slice(0, parseInt(category.numRecentReplies, 10)); + } + }); + topics.forEach((t) => { t.parentCids = undefined; }); + } + + function bubbleUpChildrenPosts(categoryData) { + categoryData.forEach((category) => { + if (category) { + if (category.posts.length) { + return; + } + const posts = []; + getPostsRecursive(category, posts); + + posts.sort((a, b) => b.pid - a.pid); + if (posts.length) { + category.posts = [posts[0]]; + } + } + }); + } + + function getPostsRecursive(category, posts) { + if (Array.isArray(category.posts)) { + category.posts.forEach(p => posts.push(p)); + } + + category.children.forEach(child => getPostsRecursive(child, posts)); + } + + // terrible name, should be topics.moveTopicPosts + Categories.moveRecentReplies = async function (tid, oldCid, cid) { + await updatePostCount(tid, oldCid, cid); + const [pids, topicDeleted] = await Promise.all([ + topics.getPids(tid), + topics.getTopicField(tid, 'deleted'), + ]); + + await batch.processArray(pids, async (pids) => { + const postData = await posts.getPostsFields(pids, ['pid', 'deleted', 'uid', 'timestamp', 'upvotes', 'downvotes']); + + const bulkRemove = []; + const bulkAdd = []; + postData.forEach((post) => { + bulkRemove.push([`cid:${oldCid}:uid:${post.uid}:pids`, post.pid]); + bulkRemove.push([`cid:${oldCid}:uid:${post.uid}:pids:votes`, post.pid]); + bulkAdd.push([`cid:${cid}:uid:${post.uid}:pids`, post.timestamp, post.pid]); + if (post.votes > 0 || post.votes < 0) { + bulkAdd.push([`cid:${cid}:uid:${post.uid}:pids:votes`, post.votes, post.pid]); + } + }); + + const postsToReAdd = postData.filter(p => !p.deleted && !topicDeleted); + const timestamps = postsToReAdd.map(p => p && p.timestamp); + await Promise.all([ + db.sortedSetRemove(`cid:${oldCid}:pids`, pids), + db.sortedSetAdd(`cid:${cid}:pids`, timestamps, postsToReAdd.map(p => p.pid)), + db.sortedSetRemoveBulk(bulkRemove), + db.sortedSetAddBulk(bulkAdd), + ]); + }, { batch: 500 }); + }; + + async function updatePostCount(tid, oldCid, newCid) { + const postCount = await topics.getTopicField(tid, 'postcount'); + if (!postCount) { + return; + } + + await Promise.all([ + db.incrObjectFieldBy(`category:${oldCid}`, 'post_count', -postCount), + db.incrObjectFieldBy(`category:${newCid}`, 'post_count', postCount), + ]); + } +}; diff --git a/src/categories/search.js b/src/categories/search.js new file mode 100644 index 0000000000..1568940166 --- /dev/null +++ b/src/categories/search.js @@ -0,0 +1,81 @@ +'use strict'; + +const _ = require('lodash'); + +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const db = require('../database'); + +module.exports = function (Categories) { + Categories.search = async function (data) { + const query = data.query || ''; + const page = data.page || 1; + const uid = data.uid || 0; + const paginate = data.hasOwnProperty('paginate') ? data.paginate : true; + + const startTime = process.hrtime(); + + let cids = await findCids(query, data.hardCap); + + const result = await plugins.hooks.fire('filter:categories.search', { + data: data, + cids: cids, + uid: uid, + }); + cids = await privileges.categories.filterCids('find', result.cids, uid); + + const searchResult = { + matchCount: cids.length, + }; + + if (paginate) { + const resultsPerPage = data.resultsPerPage || 50; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage; + searchResult.pageCount = Math.ceil(cids.length / resultsPerPage); + cids = cids.slice(start, stop); + } + + const childrenCids = await getChildrenCids(cids, uid); + const uniqCids = _.uniq(cids.concat(childrenCids)); + const categoryData = await Categories.getCategories(uniqCids, uid); + + Categories.getTree(categoryData, 0); + await Categories.getRecentTopicReplies(categoryData, uid, data.qs); + categoryData.forEach((category) => { + if (category && Array.isArray(category.children)) { + category.children = category.children.slice(0, category.subCategoriesPerPage); + category.children.forEach((child) => { + child.children = undefined; + }); + } + }); + + categoryData.sort((c1, c2) => { + if (c1.parentCid !== c2.parentCid) { + return c1.parentCid - c2.parentCid; + } + return c1.order - c2.order; + }); + searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2); + searchResult.categories = categoryData.filter(c => cids.includes(c.cid)); + return searchResult; + }; + + async function findCids(query, hardCap) { + if (!query || String(query).length < 2) { + return []; + } + const data = await db.getSortedSetScan({ + key: 'categories:name', + match: `*${String(query).toLowerCase()}*`, + limit: hardCap || 500, + }); + return data.map(data => parseInt(data.split(':').pop(), 10)); + } + + async function getChildrenCids(cids, uid) { + const childrenCids = await Promise.all(cids.map(cid => Categories.getChildrenCids(cid))); + return await privileges.categories.filterCids('find', _.flatten(childrenCids), uid); + } +}; diff --git a/src/categories/topics.js b/src/categories/topics.js new file mode 100644 index 0000000000..00addbe02e --- /dev/null +++ b/src/categories/topics.js @@ -0,0 +1,196 @@ +'use strict'; + +const db = require('../database'); +const topics = require('../topics'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const privileges = require('../privileges'); +const user = require('../user'); + +module.exports = function (Categories) { + Categories.getCategoryTopics = async function (data) { + let results = await plugins.hooks.fire('filter:category.topics.prepare', data); + const tids = await Categories.getTopicIds(results); + let topicsData = await topics.getTopicsByTids(tids, data.uid); + topicsData = await user.blocks.filter(data.uid, topicsData); + + if (!topicsData.length) { + return { topics: [], uid: data.uid }; + } + topics.calculateTopicIndices(topicsData, data.start); + + results = await plugins.hooks.fire('filter:category.topics.get', { cid: data.cid, topics: topicsData, uid: data.uid }); + return { topics: results.topics, nextStart: data.stop + 1 }; + }; + + Categories.getTopicIds = async function (data) { + const dataForPinned = { ...data }; + dataForPinned.start = 0; + dataForPinned.stop = -1; + + const [pinnedTids, set, direction] = await Promise.all([ + Categories.getPinnedTids(dataForPinned), + Categories.buildTopicsSortedSet(data), + Categories.getSortedSetRangeDirection(data.sort), + ]); + + const totalPinnedCount = pinnedTids.length; + const pinnedTidsOnPage = pinnedTids.slice(data.start, data.stop !== -1 ? data.stop + 1 : undefined); + const pinnedCountOnPage = pinnedTidsOnPage.length; + const topicsPerPage = data.stop - data.start + 1; + const normalTidsToGet = Math.max(0, topicsPerPage - pinnedCountOnPage); + + if (!normalTidsToGet && data.stop !== -1) { + return pinnedTidsOnPage; + } + + if (plugins.hooks.hasListeners('filter:categories.getTopicIds')) { + const result = await plugins.hooks.fire('filter:categories.getTopicIds', { + tids: [], + data: data, + pinnedTids: pinnedTidsOnPage, + allPinnedTids: pinnedTids, + totalPinnedCount: totalPinnedCount, + normalTidsToGet: normalTidsToGet, + }); + return result && result.tids; + } + + let { start } = data; + if (start > 0 && totalPinnedCount) { + start -= totalPinnedCount - pinnedCountOnPage; + } + + const stop = data.stop === -1 ? data.stop : start + normalTidsToGet - 1; + let normalTids; + const reverse = direction === 'highest-to-lowest'; + if (Array.isArray(set)) { + const weights = set.map((s, index) => (index ? 0 : 1)); + normalTids = await db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ sets: set, start: start, stop: stop, weights: weights }); + } else { + normalTids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop); + } + normalTids = normalTids.filter(tid => !pinnedTids.includes(tid)); + return pinnedTidsOnPage.concat(normalTids); + }; + + Categories.getTopicCount = async function (data) { + if (plugins.hooks.hasListeners('filter:categories.getTopicCount')) { + const result = await plugins.hooks.fire('filter:categories.getTopicCount', { + topicCount: data.category.topic_count, + data: data, + }); + return result && result.topicCount; + } + const set = await Categories.buildTopicsSortedSet(data); + if (Array.isArray(set)) { + return await db.sortedSetIntersectCard(set); + } else if (data.targetUid && set) { + return await db.sortedSetCard(set); + } + return data.category.topic_count; + }; + + Categories.buildTopicsSortedSet = async function (data) { + const { cid } = data; + let set = `cid:${cid}:tids`; + const sort = data.sort || (data.settings && data.settings.categoryTopicSort) || meta.config.categoryTopicSort || 'newest_to_oldest'; + + if (sort === 'most_posts') { + set = `cid:${cid}:tids:posts`; + } else if (sort === 'most_votes') { + set = `cid:${cid}:tids:votes`; + } else if (sort === 'most_views') { + set = `cid:${cid}:tids:views`; + } + + if (data.tag) { + if (Array.isArray(data.tag)) { + set = [set].concat(data.tag.map(tag => `tag:${tag}:topics`)); + } else { + set = [set, `tag:${data.tag}:topics`]; + } + } + + if (data.targetUid) { + set = (Array.isArray(set) ? set : [set]).concat([`cid:${cid}:uid:${data.targetUid}:tids`]); + } + + const result = await plugins.hooks.fire('filter:categories.buildTopicsSortedSet', { + set: set, + data: data, + }); + return result && result.set; + }; + + Categories.getSortedSetRangeDirection = async function (sort) { + sort = sort || 'newest_to_oldest'; + const direction = ['newest_to_oldest', 'most_posts', 'most_votes', 'most_views'].includes(sort) ? 'highest-to-lowest' : 'lowest-to-highest'; + const result = await plugins.hooks.fire('filter:categories.getSortedSetRangeDirection', { + sort: sort, + direction: direction, + }); + return result && result.direction; + }; + + Categories.getAllTopicIds = async function (cid, start, stop) { + return await db.getSortedSetRange([`cid:${cid}:tids:pinned`, `cid:${cid}:tids`], start, stop); + }; + + Categories.getPinnedTids = async function (data) { + if (plugins.hooks.hasListeners('filter:categories.getPinnedTids')) { + const result = await plugins.hooks.fire('filter:categories.getPinnedTids', { + pinnedTids: [], + data: data, + }); + return result && result.pinnedTids; + } + const [allPinnedTids, canSchedule] = await Promise.all([ + db.getSortedSetRevRange(`cid:${data.cid}:tids:pinned`, data.start, data.stop), + privileges.categories.can('topics:schedule', data.cid, data.uid), + ]); + const pinnedTids = canSchedule ? allPinnedTids : await filterScheduledTids(allPinnedTids); + + return await topics.tools.checkPinExpiry(pinnedTids); + }; + + Categories.modifyTopicsByPrivilege = function (topics, privileges) { + if (!Array.isArray(topics) || !topics.length || privileges.view_deleted) { + return; + } + + topics.forEach((topic) => { + if (!topic.scheduled && topic.deleted && !topic.isOwner) { + topic.title = '[[topic:topic_is_deleted]]'; + if (topic.hasOwnProperty('titleRaw')) { + topic.titleRaw = '[[topic:topic_is_deleted]]'; + } + topic.slug = topic.tid; + topic.teaser = null; + topic.noAnchor = true; + topic.tags = []; + } + }); + }; + + Categories.onNewPostMade = async function (cid, pinned, postData) { + if (!cid || !postData) { + return; + } + const promises = [ + db.sortedSetAdd(`cid:${cid}:pids`, postData.timestamp, postData.pid), + db.incrObjectField(`category:${cid}`, 'post_count'), + ]; + if (!pinned) { + promises.push(db.sortedSetIncrBy(`cid:${cid}:tids:posts`, 1, postData.tid)); + } + await Promise.all(promises); + await Categories.updateRecentTidForCid(cid); + }; + + async function filterScheduledTids(tids) { + const scores = await db.sortedSetScores('topics:scheduled', tids); + const now = Date.now(); + return tids.filter((tid, index) => tid && (!scores[index] || scores[index] <= now)); + } +}; diff --git a/src/categories/unread.js b/src/categories/unread.js new file mode 100644 index 0000000000..8d69e0ec1d --- /dev/null +++ b/src/categories/unread.js @@ -0,0 +1,38 @@ +'use strict'; + +const db = require('../database'); + +module.exports = function (Categories) { + Categories.markAsRead = async function (cids, uid) { + if (!Array.isArray(cids) || !cids.length || parseInt(uid, 10) <= 0) { + return; + } + let keys = cids.map(cid => `cid:${cid}:read_by_uid`); + const hasRead = await db.isMemberOfSets(keys, uid); + keys = keys.filter((key, index) => !hasRead[index]); + await db.setsAdd(keys, uid); + }; + + Categories.markAsUnreadForAll = async function (cid) { + if (!parseInt(cid, 10)) { + return; + } + await db.delete(`cid:${cid}:read_by_uid`); + }; + + Categories.hasReadCategories = async function (cids, uid) { + if (parseInt(uid, 10) <= 0) { + return cids.map(() => false); + } + + const sets = cids.map(cid => `cid:${cid}:read_by_uid`); + return await db.isMemberOfSets(sets, uid); + }; + + Categories.hasReadCategory = async function (cid, uid) { + if (parseInt(uid, 10) <= 0) { + return false; + } + return await db.isSetMember(`cid:${cid}:read_by_uid`, uid); + }; +}; diff --git a/src/categories/update.js b/src/categories/update.js new file mode 100644 index 0000000000..87a39517b0 --- /dev/null +++ b/src/categories/update.js @@ -0,0 +1,145 @@ +'use strict'; + +const db = require('../database'); +const meta = require('../meta'); +const utils = require('../utils'); +const slugify = require('../slugify'); +const translator = require('../translator'); +const plugins = require('../plugins'); +const cache = require('../cache'); + +module.exports = function (Categories) { + Categories.update = async function (modified) { + const cids = Object.keys(modified); + await Promise.all(cids.map(cid => updateCategory(cid, modified[cid]))); + return cids; + }; + + async function updateCategory(cid, modifiedFields) { + const exists = await Categories.exists(cid); + if (!exists) { + return; + } + + if (modifiedFields.hasOwnProperty('name')) { + const translated = await translator.translate(modifiedFields.name); + modifiedFields.slug = `${cid}/${slugify(translated)}`; + } + const result = await plugins.hooks.fire('filter:category.update', { cid: cid, category: modifiedFields }); + + const { category } = result; + const fields = Object.keys(category); + // move parent to front, so its updated first + const parentCidIndex = fields.indexOf('parentCid'); + if (parentCidIndex !== -1 && fields.length > 1) { + fields.splice(0, 0, fields.splice(parentCidIndex, 1)[0]); + } + + for (const key of fields) { + // eslint-disable-next-line no-await-in-loop + await updateCategoryField(cid, key, category[key]); + } + plugins.hooks.fire('action:category.update', { cid: cid, modified: category }); + } + + async function updateCategoryField(cid, key, value) { + if (key === 'parentCid') { + return await updateParent(cid, value); + } else if (key === 'tagWhitelist') { + return await updateTagWhitelist(cid, value); + } else if (key === 'name') { + return await updateName(cid, value); + } else if (key === 'order') { + return await updateOrder(cid, value); + } + + await db.setObjectField(`category:${cid}`, key, value); + if (key === 'description') { + await Categories.parseDescription(cid, value); + } + } + + async function updateParent(cid, newParent) { + newParent = parseInt(newParent, 10) || 0; + if (parseInt(cid, 10) === newParent) { + throw new Error('[[error:cant-set-self-as-parent]]'); + } + const childrenCids = await Categories.getChildrenCids(cid); + if (childrenCids.includes(newParent)) { + throw new Error('[[error:cant-set-child-as-parent]]'); + } + const categoryData = await Categories.getCategoryFields(cid, ['parentCid', 'order']); + const oldParent = categoryData.parentCid; + if (oldParent === newParent) { + return; + } + await Promise.all([ + db.sortedSetRemove(`cid:${oldParent}:children`, cid), + db.sortedSetAdd(`cid:${newParent}:children`, categoryData.order, cid), + db.setObjectField(`category:${cid}`, 'parentCid', newParent), + ]); + + cache.del([ + `cid:${oldParent}:children`, + `cid:${newParent}:children`, + `cid:${oldParent}:children:all`, + `cid:${newParent}:children:all`, + ]); + } + + async function updateTagWhitelist(cid, tags) { + tags = tags.split(',').map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength)) + .filter(Boolean); + await db.delete(`cid:${cid}:tag:whitelist`); + const scores = tags.map((tag, index) => index); + await db.sortedSetAdd(`cid:${cid}:tag:whitelist`, scores, tags); + cache.del(`cid:${cid}:tag:whitelist`); + } + + async function updateOrder(cid, order) { + const parentCid = await Categories.getCategoryField(cid, 'parentCid'); + await db.sortedSetsAdd('categories:cid', order, cid); + + const childrenCids = await db.getSortedSetRange( + `cid:${parentCid}:children`, 0, -1 + ); + + const currentIndex = childrenCids.indexOf(String(cid)); + if (currentIndex === -1) { + throw new Error('[[error:no-category]]'); + } + // moves cid to index order - 1 in the array + if (childrenCids.length > 1) { + childrenCids.splice(Math.max(0, order - 1), 0, childrenCids.splice(currentIndex, 1)[0]); + } + + // recalculate orders from array indices + await db.sortedSetAdd( + `cid:${parentCid}:children`, + childrenCids.map((cid, index) => index + 1), + childrenCids + ); + + await db.setObjectBulk( + childrenCids.map((cid, index) => [`category:${cid}`, { order: index + 1 }]) + ); + + cache.del([ + 'categories:cid', + `cid:${parentCid}:children`, + `cid:${parentCid}:children:all`, + ]); + } + + Categories.parseDescription = async function (cid, description) { + const parsedDescription = await plugins.hooks.fire('filter:parse.raw', description); + await Categories.setCategoryField(cid, 'descriptionParsed', parsedDescription); + }; + + async function updateName(cid, newName) { + const oldName = await Categories.getCategoryField(cid, 'name'); + await db.sortedSetRemove('categories:name', `${oldName.slice(0, 200).toLowerCase()}:${cid}`); + await db.sortedSetAdd('categories:name', 0, `${newName.slice(0, 200).toLowerCase()}:${cid}`); + await db.setObjectField(`category:${cid}`, 'name', newName); + } +}; diff --git a/src/categories/watch.js b/src/categories/watch.js new file mode 100644 index 0000000000..c26149d888 --- /dev/null +++ b/src/categories/watch.js @@ -0,0 +1,54 @@ +'use strict'; + +const db = require('../database'); +const user = require('../user'); + +module.exports = function (Categories) { + Categories.watchStates = { + ignoring: 1, + notwatching: 2, + watching: 3, + }; + + Categories.isIgnored = async function (cids, uid) { + if (!(parseInt(uid, 10) > 0)) { + return cids.map(() => false); + } + const states = await Categories.getWatchState(cids, uid); + return states.map(state => state === Categories.watchStates.ignoring); + }; + + Categories.getWatchState = async function (cids, uid) { + if (!(parseInt(uid, 10) > 0)) { + return cids.map(() => Categories.watchStates.notwatching); + } + if (!Array.isArray(cids) || !cids.length) { + return []; + } + const keys = cids.map(cid => `cid:${cid}:uid:watch:state`); + const [userSettings, states] = await Promise.all([ + user.getSettings(uid), + db.sortedSetsScore(keys, uid), + ]); + return states.map(state => state || Categories.watchStates[userSettings.categoryWatchState]); + }; + + Categories.getIgnorers = async function (cid, start, stop) { + const count = (stop === -1) ? -1 : (stop - start + 1); + return await db.getSortedSetRevRangeByScore(`cid:${cid}:uid:watch:state`, start, count, Categories.watchStates.ignoring, Categories.watchStates.ignoring); + }; + + Categories.filterIgnoringUids = async function (cid, uids) { + const states = await Categories.getUidsWatchStates(cid, uids); + const readingUids = uids.filter((uid, index) => uid && states[index] !== Categories.watchStates.ignoring); + return readingUids; + }; + + Categories.getUidsWatchStates = async function (cid, uids) { + const [userSettings, states] = await Promise.all([ + user.getMultipleUserSettings(uids), + db.sortedSetScores(`cid:${cid}:uid:watch:state`, uids), + ]); + return states.map((state, index) => state || Categories.watchStates[userSettings[index].categoryWatchState]); + }; +}; diff --git a/src/cli/colors.js b/src/cli/colors.js new file mode 100644 index 0000000000..da17202744 --- /dev/null +++ b/src/cli/colors.js @@ -0,0 +1,160 @@ +'use strict'; + +// override commander help formatting functions +// to include color styling in the output +// so the CLI looks nice + +const { Command } = require('commander'); +const chalk = require('chalk'); + +const colors = [ + // depth = 0, top-level command + { command: 'yellow', option: 'cyan', arg: 'magenta' }, + // depth = 1, second-level commands + { command: 'green', option: 'blue', arg: 'red' }, + // depth = 2, third-level commands + { command: 'yellow', option: 'cyan', arg: 'magenta' }, + // depth = 3 fourth-level commands + { command: 'green', option: 'blue', arg: 'red' }, +]; + +function humanReadableArgName(arg) { + const nameOutput = arg.name() + (arg.variadic === true ? '...' : ''); + + return arg.required ? `<${nameOutput}>` : `[${nameOutput}]`; +} + +function getControlCharacterSpaces(term) { + const matches = term.match(/.\[\d+m/g); + return matches ? matches.length * 5 : 0; +} + +// get depth of command +// 0 = top, 1 = subcommand of top, etc +Command.prototype.depth = function () { + if (this._depth === undefined) { + let depth = 0; + let { parent } = this; + while (parent) { depth += 1; parent = parent.parent; } + + this._depth = depth; + } + return this._depth; +}; + +module.exports = { + commandUsage(cmd) { + const depth = cmd.depth(); + + // Usage + let cmdName = cmd._name; + if (cmd._aliases[0]) { + cmdName = `${cmdName}|${cmd._aliases[0]}`; + } + let parentCmdNames = ''; + let parentCmd = cmd.parent; + let parentDepth = depth - 1; + while (parentCmd) { + parentCmdNames = `${chalk[colors[parentDepth].command](parentCmd.name())} ${parentCmdNames}`; + + parentCmd = parentCmd.parent; + parentDepth -= 1; + } + + // from Command.prototype.usage() + const args = cmd._args.map(arg => chalk[colors[depth].arg](humanReadableArgName(arg))); + const cmdUsage = [].concat( + (cmd.options.length || cmd._hasHelpOption ? chalk[colors[depth].option]('[options]') : []), + (cmd.commands.length ? chalk[colors[depth + 1].command]('[command]') : []), + (cmd._args.length ? args : []) + ).join(' '); + + return `${parentCmdNames}${chalk[colors[depth].command](cmdName)} ${cmdUsage}`; + }, + subcommandTerm(cmd) { + const depth = cmd.depth(); + + // Legacy. Ignores custom usage string, and nested commands. + const args = cmd._args.map(arg => humanReadableArgName(arg)).join(' '); + return chalk[colors[depth].command](cmd._name + ( + cmd._aliases[0] ? `|${cmd._aliases[0]}` : '' + )) + + chalk[colors[depth].option](cmd.options.length ? ' [options]' : '') + // simplistic check for non-help option + chalk[colors[depth].arg](args ? ` ${args}` : ''); + }, + longestOptionTermLength(cmd, helper) { + return helper.visibleOptions(cmd).reduce((max, option) => Math.max( + max, + helper.optionTerm(option).length - getControlCharacterSpaces(helper.optionTerm(option)) + ), 0); + }, + longestSubcommandTermLength(cmd, helper) { + return helper.visibleCommands(cmd).reduce((max, command) => Math.max( + max, + helper.subcommandTerm(command).length - getControlCharacterSpaces(helper.subcommandTerm(command)) + ), 0); + }, + longestArgumentTermLength(cmd, helper) { + return helper.visibleArguments(cmd).reduce((max, argument) => Math.max( + max, + helper.argumentTerm(argument).length - getControlCharacterSpaces(helper.argumentTerm(argument)) + ), 0); + }, + formatHelp(cmd, helper) { + const depth = cmd.depth(); + + const termWidth = helper.padWidth(cmd, helper); + const helpWidth = helper.helpWidth || 80; + const itemIndentWidth = 2; + const itemSeparatorWidth = 2; // between term and description + function formatItem(term, description) { + const padding = ' '.repeat((termWidth + itemSeparatorWidth) - (term.length - getControlCharacterSpaces(term))); + if (description) { + const fullText = `${term}${padding}${description}`; + return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth); + } + return term; + } + function formatList(textArray) { + return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth)); + } + + // Usage + let output = [`Usage: ${helper.commandUsage(cmd)}`, '']; + + // Description + const commandDescription = helper.commandDescription(cmd); + if (commandDescription.length > 0) { + output = output.concat([commandDescription, '']); + } + + // Arguments + const argumentList = helper.visibleArguments(cmd).map(argument => formatItem( + chalk[colors[depth].arg](argument.term), + argument.description + )); + if (argumentList.length > 0) { + output = output.concat(['Arguments:', formatList(argumentList), '']); + } + + // Options + const optionList = helper.visibleOptions(cmd).map(option => formatItem( + chalk[colors[depth].option](helper.optionTerm(option)), + helper.optionDescription(option) + )); + if (optionList.length > 0) { + output = output.concat(['Options:', formatList(optionList), '']); + } + + // Commands + const commandList = helper.visibleCommands(cmd).map(cmd => formatItem( + helper.subcommandTerm(cmd), + helper.subcommandDescription(cmd) + )); + if (commandList.length > 0) { + output = output.concat(['Commands:', formatList(commandList), '']); + } + + return output.join('\n'); + }, +}; diff --git a/src/cli/index.js b/src/cli/index.js new file mode 100644 index 0000000000..2d4f7857c4 --- /dev/null +++ b/src/cli/index.js @@ -0,0 +1,322 @@ +/* eslint-disable import/order */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +require('../../require-main'); + +const packageInstall = require('./package-install'); +const { paths } = require('../constants'); + +try { + fs.accessSync(paths.currentPackage, fs.constants.R_OK); // throw on missing package.json + try { // handle missing node_modules/ directory + fs.accessSync(paths.nodeModules, fs.constants.R_OK); + } catch (e) { + if (e.code === 'ENOENT') { + // run package installation just to sync up node_modules/ with existing package.json + packageInstall.installAll(); + } else { + throw e; + } + } + fs.accessSync(path.join(paths.nodeModules, 'semver/package.json'), fs.constants.R_OK); + + const semver = require('semver'); + const defaultPackage = require('../../install/package.json'); + + const checkVersion = function (packageName) { + const { version } = JSON.parse(fs.readFileSync(path.join(paths.nodeModules, packageName, 'package.json'), 'utf8')); + if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) { + const e = new TypeError(`Incorrect dependency version: ${packageName}`); + e.code = 'DEP_WRONG_VERSION'; + throw e; + } + }; + + checkVersion('nconf'); + checkVersion('async'); + checkVersion('commander'); + checkVersion('chalk'); + checkVersion('lodash'); + checkVersion('lru-cache'); +} catch (e) { + if (['ENOENT', 'DEP_WRONG_VERSION', 'MODULE_NOT_FOUND'].includes(e.code)) { + console.warn('Dependencies outdated or not yet installed.'); + console.log('Installing them now...\n'); + + packageInstall.updatePackageFile(); + packageInstall.preserveExtraneousPlugins(); + packageInstall.installAll(); + + const chalk = require('chalk'); + console.log(`${chalk.green('OK')}\n`); + } else { + throw e; + } +} + +const chalk = require('chalk'); +const nconf = require('nconf'); +const { program } = require('commander'); +const yargs = require('yargs'); + +const pkg = require('../../install/package.json'); +const file = require('../file'); +const prestart = require('../prestart'); + +program.configureHelp(require('./colors')); + +program + .name('./nodebb') + .description('Welcome to NodeBB') + .version(pkg.version) + .option('--json-logging', 'Output to logs in JSON format', false) + .option('--log-level ', 'Default logging level to use', 'info') + .option('--config ', 'Specify a config file', 'config.json') + .option('-d, --dev', 'Development mode, including verbose logging', false) + .option('-l, --log', 'Log subprocess output to console', false); + +// provide a yargs object ourselves +// otherwise yargs will consume `--help` or `help` +// and `nconf` will exit with useless usage info +const opts = yargs(process.argv.slice(2)).help(false).exitProcess(false); +nconf.argv(opts).env({ + separator: '__', +}); + +prestart.setupWinston(); + +// Alternate configuration file support +const configFile = path.resolve(paths.baseDir, nconf.get('config') || 'config.json'); +const configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database')); + +prestart.loadConfig(configFile); +prestart.versionCheck(); + +if (!configExists && process.argv[2] !== 'setup') { + require('./setup').webInstall(); + return; +} + +process.env.CONFIG = configFile; + +// running commands +program + .command('start') + .description('Start the NodeBB server') + .action(() => { + require('./running').start(program.opts()); + }); +program + .command('slog', null, { + noHelp: true, + }) + .description('Start the NodeBB server and view the live output log') + .action(() => { + require('./running').start({ ...program.opts(), log: true }); + }); +program + .command('dev', null, { + noHelp: true, + }) + .description('Start NodeBB in verbose development mode') + .action(() => { + process.env.NODE_ENV = 'development'; + global.env = 'development'; + require('./running').start({ ...program.opts(), dev: true }); + }); +program + .command('stop') + .description('Stop the NodeBB server') + .action(() => { + require('./running').stop(program.opts()); + }); +program + .command('restart') + .description('Restart the NodeBB server') + .action(() => { + require('./running').restart(program.opts()); + }); +program + .command('status') + .description('Check the running status of the NodeBB server') + .action(() => { + require('./running').status(program.opts()); + }); +program + .command('log') + .description('Open the output log (useful for debugging)') + .action(() => { + require('./running').log(program.opts()); + }); + +// management commands +program + .command('setup [config]') + .description('Run the NodeBB setup script, or setup with an initial config') + .option('--skip-build', 'Run setup without building assets') + .action((initConfig) => { + if (initConfig) { + try { + initConfig = JSON.parse(initConfig); + } catch (e) { + console.warn(chalk.red('Invalid JSON passed as initial config value.')); + console.log('If you meant to pass in an initial config value, please try again.\n'); + + throw e; + } + } + require('./setup').setup(initConfig); + }); + +program + .command('install [plugin]') + .description('Launch the NodeBB web installer for configuration setup or install a plugin') + .option('-f, --force', 'Force plugin installation even if it may be incompatible with currently installed NodeBB version') + .action((plugin, options) => { + if (plugin) { + require('./manage').install(plugin, options); + } else { + require('./setup').webInstall(); + } + }); + +program + .command('build [targets...]') + .description(`Compile static assets ${chalk.red('(JS, CSS, templates, languages)')}`) + .option('-s, --series', 'Run builds in series without extra processes') + .option('-w, --webpack', 'Bundle assets with webpack', true) + .action((targets, options) => { + if (program.opts().dev) { + process.env.NODE_ENV = 'development'; + global.env = 'development'; + } + require('./manage').build(targets.length ? targets : true, options); + }) + .on('--help', () => { + require('../meta/aliases').buildTargets(); + }); +program + .command('activate [plugin]') + .description('Activate a plugin for the next startup of NodeBB (nodebb-plugin- prefix is optional)') + .action((plugin) => { + require('./manage').activate(plugin); + }); +program + .command('plugins') + .action(() => { + require('./manage').listPlugins(); + }) + .description('List all installed plugins'); +program + .command('events [count]') + .description('Outputs the most recent administrative events recorded by NodeBB') + .action((count) => { + require('./manage').listEvents(count); + }); +program + .command('info') + .description('Outputs various system info') + .action(() => { + require('./manage').info(); + }); + +// reset +const resetCommand = program.command('reset'); + +resetCommand + .description('Reset plugins, themes, settings, etc') + .option('-t, --theme [theme]', 'Reset to [theme] or to the default theme') + .option('-p, --plugin [plugin]', 'Disable [plugin] or all plugins') + .option('-w, --widgets', 'Disable all widgets') + .option('-s, --settings', 'Reset settings to their default values') + .option('-a, --all', 'All of the above') + .action((options) => { + const valid = ['theme', 'plugin', 'widgets', 'settings', 'all'].some(x => options[x]); + if (!valid) { + console.warn(`\n${chalk.red('No valid options passed in, so nothing was reset.')}`); + resetCommand.help(); + } + + require('./reset').reset(options, (err) => { + if (err) { + return process.exit(1); + } + + process.exit(0); + }); + }); + +// user +program + .addCommand(require('./user')()); + +// upgrades +program + .command('upgrade [scripts...]') + .description('Run NodeBB upgrade scripts and ensure packages are up-to-date, or run a particular upgrade script') + .option('-m, --package', 'Update package.json from defaults', false) + .option('-i, --install', 'Bringing base dependencies up to date', false) + .option('-p, --plugins', 'Check installed plugins for updates', false) + .option('-s, --schema', 'Update NodeBB data store schema', false) + .option('-b, --build', 'Rebuild assets', false) + .on('--help', () => { + console.log(`\n${[ + 'When running particular upgrade scripts, options are ignored.', + 'By default all options are enabled. Passing any options disables that default.', + '\nExamples:', + ` Only package and dependency updates: ${chalk.yellow('./nodebb upgrade -mi')}`, + ` Only database update: ${chalk.yellow('./nodebb upgrade -s')}`, + ].join('\n')}`); + }) + .action((scripts, options) => { + if (program.opts().dev) { + process.env.NODE_ENV = 'development'; + global.env = 'development'; + } + require('./upgrade').upgrade(scripts.length ? scripts : true, options); + }); + +program + .command('upgrade-plugins', null, { + noHelp: true, + }) + .alias('upgradePlugins') + .description('Upgrade plugins') + .action(() => { + require('./upgrade-plugins').upgradePlugins((err) => { + if (err) { + throw err; + } + console.log(chalk.green('OK')); + process.exit(); + }); + }); + +program + .command('help [command]') + .description('Display help for [command]') + .action((name) => { + if (!name) { + return program.help(); + } + + const command = program.commands.find(command => command._name === name); + if (command) { + command.help(); + } else { + console.log(`error: unknown command '${command}'.`); + program.help(); + } + }); + +if (process.argv.length === 2) { + program.help(); +} + +program.executables = false; + +program.parse(); diff --git a/src/cli/manage.js b/src/cli/manage.js new file mode 100644 index 0000000000..cfb545b5cb --- /dev/null +++ b/src/cli/manage.js @@ -0,0 +1,209 @@ +'use strict'; + +const winston = require('winston'); +const childProcess = require('child_process'); +const CliGraph = require('cli-graph'); +const chalk = require('chalk'); +const nconf = require('nconf'); + +const build = require('../meta/build'); +const db = require('../database'); +const plugins = require('../plugins'); +const events = require('../events'); +const analytics = require('../analytics'); +const reset = require('./reset'); +const { pluginNamePattern, themeNamePattern, paths } = require('../constants'); + +async function install(plugin, options) { + if (!options) { + options = {}; + } + try { + await db.init(); + if (!pluginNamePattern.test(plugin)) { + // Allow omission of `nodebb-plugin-` + plugin = `nodebb-plugin-${plugin}`; + } + + plugin = await plugins.autocomplete(plugin); + + const isInstalled = await plugins.isInstalled(plugin); + if (isInstalled) { + throw new Error('plugin already installed'); + } + const nbbVersion = require(paths.currentPackage).version; + const suggested = await plugins.suggest(plugin, nbbVersion); + if (!suggested.version) { + if (!options.force) { + throw new Error(suggested.message); + } + winston.warn(`${suggested.message} Proceeding with installation anyway due to force option being provided`); + suggested.version = 'latest'; + } + winston.info('Installing Plugin `%s@%s`', plugin, suggested.version); + await plugins.toggleInstall(plugin, suggested.version); + + process.exit(0); + } catch (err) { + winston.error(`An error occurred during plugin installation\n${err.stack}`); + process.exit(1); + } +} + +async function activate(plugin) { + if (themeNamePattern.test(plugin)) { + await reset.reset({ + theme: plugin, + }); + process.exit(); + } + try { + await db.init(); + if (!pluginNamePattern.test(plugin)) { + // Allow omission of `nodebb-plugin-` + plugin = `nodebb-plugin-${plugin}`; + } + + plugin = await plugins.autocomplete(plugin); + + const isInstalled = await plugins.isInstalled(plugin); + if (!isInstalled) { + throw new Error('plugin not installed'); + } + const isActive = await plugins.isActive(plugin); + if (isActive) { + winston.info('Plugin `%s` already active', plugin); + process.exit(0); + } + if (nconf.get('plugins:active')) { + winston.error('Cannot activate plugins while plugin state configuration is set, please change your active configuration (config.json, environmental variables or terminal arguments) instead'); + process.exit(1); + } + const numPlugins = await db.sortedSetCard('plugins:active'); + winston.info('Activating plugin `%s`', plugin); + await db.sortedSetAdd('plugins:active', numPlugins, plugin); + await events.log({ + type: 'plugin-activate', + text: plugin, + }); + + process.exit(0); + } catch (err) { + winston.error(`An error occurred during plugin activation\n${err.stack}`); + process.exit(1); + } +} + +async function listPlugins() { + await db.init(); + const installed = await plugins.showInstalled(); + const installedList = installed.map(plugin => plugin.name); + const active = await plugins.getActive(); + // Merge the two sets, defer to plugins in `installed` if already present + const combined = installed.concat(active.reduce((memo, cur) => { + if (!installedList.includes(cur)) { + memo.push({ + id: cur, + active: true, + installed: false, + }); + } + + return memo; + }, [])); + + // Alphabetical sort + combined.sort((a, b) => (a.id > b.id ? 1 : -1)); + + // Pretty output + process.stdout.write('Active plugins:\n'); + combined.forEach((plugin) => { + process.stdout.write(`\t* ${plugin.id}${plugin.version ? `@${plugin.version}` : ''} (`); + process.stdout.write(plugin.installed ? chalk.green('installed') : chalk.red('not installed')); + process.stdout.write(', '); + process.stdout.write(plugin.active ? chalk.green('enabled') : chalk.yellow('disabled')); + process.stdout.write(')\n'); + }); + + process.exit(); +} + +async function listEvents(count = 10) { + await db.init(); + const eventData = await events.getEvents('', 0, count - 1); + console.log(chalk.bold(`\nDisplaying last ${count} administrative events...`)); + eventData.forEach((event) => { + console.log(` * ${chalk.green(String(event.timestampISO))} ${chalk.yellow(String(event.type))}${event.text ? ` ${event.text}` : ''} (uid: ${event.uid ? event.uid : 0})`); + }); + process.exit(); +} + +async function info() { + console.log(''); + const { version } = require('../../package.json'); + console.log(` version: ${version}`); + + console.log(` Node ver: ${process.version}`); + + const hash = childProcess.execSync('git rev-parse HEAD'); + console.log(` git hash: ${hash}`); + + console.log(` database: ${nconf.get('database')}`); + + await db.init(); + const info = await db.info(db.client); + + switch (nconf.get('database')) { + case 'redis': + console.log(` version: ${info.redis_version}`); + console.log(` disk sync: ${info.rdb_last_bgsave_status}`); + break; + + case 'mongo': + console.log(` version: ${info.version}`); + console.log(` engine: ${info.storageEngine}`); + break; + case 'postgres': + console.log(` version: ${info.version}`); + console.log(` uptime: ${info.uptime}`); + break; + } + + const analyticsData = await analytics.getHourlyStatsForSet('analytics:pageviews', Date.now(), 24); + const graph = new CliGraph({ + height: 12, + width: 25, + center: { + x: 0, + y: 11, + }, + }); + const min = Math.min(...analyticsData); + const max = Math.max(...analyticsData); + + analyticsData.forEach((point, idx) => { + graph.addPoint(idx + 1, Math.round(point / max * 10)); + }); + + console.log(''); + console.log(graph.toString()); + console.log(`Pageviews, last 24h (min: ${min} max: ${max})`); + process.exit(); +} + +async function buildWrapper(targets, options) { + try { + await build.build(targets, options); + process.exit(0); + } catch (err) { + winston.error(err.stack); + process.exit(1); + } +} + +exports.build = buildWrapper; +exports.install = install; +exports.activate = activate; +exports.listPlugins = listPlugins; +exports.listEvents = listEvents; +exports.info = info; diff --git a/src/cli/package-install.js b/src/cli/package-install.js new file mode 100644 index 0000000000..5d14d8f90d --- /dev/null +++ b/src/cli/package-install.js @@ -0,0 +1,174 @@ +'use strict'; + +const path = require('path'); + +const fs = require('fs'); +const cproc = require('child_process'); + +const { paths, pluginNamePattern } = require('../constants'); + +const pkgInstall = module.exports; + +function sortDependencies(dependencies) { + return Object.entries(dependencies) + .sort((a, b) => (a < b ? -1 : 1)) + .reduce((memo, pkg) => { + memo[pkg[0]] = pkg[1]; + return memo; + }, {}); +} + +pkgInstall.updatePackageFile = () => { + let oldPackageContents; + + try { + oldPackageContents = JSON.parse(fs.readFileSync(paths.currentPackage, 'utf8')); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } else { + // No local package.json, copy from install/package.json + fs.copyFileSync(paths.installPackage, paths.currentPackage); + return; + } + } + + const _ = require('lodash'); + const defaultPackageContents = JSON.parse(fs.readFileSync(paths.installPackage, 'utf8')); + + let dependencies = {}; + Object.entries(oldPackageContents.dependencies || {}).forEach(([dep, version]) => { + if (pluginNamePattern.test(dep)) { + dependencies[dep] = version; + } + }); + + const { devDependencies } = defaultPackageContents; + + // Sort dependencies alphabetically + dependencies = sortDependencies({ ...dependencies, ...defaultPackageContents.dependencies }); + + const packageContents = { ..._.merge(oldPackageContents, defaultPackageContents), dependencies, devDependencies }; + fs.writeFileSync(paths.currentPackage, JSON.stringify(packageContents, null, 4)); +}; + +pkgInstall.supportedPackageManager = [ + 'npm', + 'cnpm', + 'pnpm', + 'yarn', +]; + +pkgInstall.getPackageManager = () => { + try { + const packageContents = require(paths.currentPackage); + // This regex technically allows invalid values: + // cnpm isn't supported by corepack and it doesn't enforce a version string being present + const pmRegex = new RegExp(`^(?${pkgInstall.supportedPackageManager.join('|')})@?[\\d\\w\\.\\-]*$`); + const packageManager = packageContents.packageManager ? packageContents.packageManager.match(pmRegex) : false; + if (packageManager) { + return packageManager.groups.packageManager; + } + fs.accessSync(path.join(paths.nodeModules, 'nconf/package.json'), fs.constants.R_OK); + const nconf = require('nconf'); + if (!Object.keys(nconf.stores).length) { + // Quick & dirty nconf setup for when you cannot rely on nconf having been required already + const configFile = path.resolve(__dirname, '../../', nconf.any(['config', 'CONFIG']) || 'config.json'); + nconf.env().file({ // not sure why adding .argv() causes the process to terminate + file: configFile, + }); + } + if (nconf.get('package_manager') && !pkgInstall.supportedPackageManager.includes(nconf.get('package_manager'))) { + nconf.clear('package_manager'); + } + + if (!nconf.get('package_manager')) { + nconf.set('package_manager', getPackageManagerByLockfile()); + } + + return nconf.get('package_manager') || 'npm'; + } catch (e) { + // nconf not installed or other unexpected error/exception + return getPackageManagerByLockfile() || 'npm'; + } +}; + +function getPackageManagerByLockfile() { + for (const [packageManager, lockfile] of Object.entries({ npm: 'package-lock.json', yarn: 'yarn.lock', pnpm: 'pnpm-lock.yaml' })) { + try { + fs.accessSync(path.resolve(__dirname, `../../${lockfile}`), fs.constants.R_OK); + return packageManager; + } catch (e) {} + } +} + +pkgInstall.installAll = () => { + const prod = process.env.NODE_ENV !== 'development'; + let command = 'npm install'; + + const supportedPackageManagerList = exports.supportedPackageManager; // load config from src/cli/package-install.js + const packageManager = pkgInstall.getPackageManager(); + if (supportedPackageManagerList.indexOf(packageManager) >= 0) { + switch (packageManager) { + case 'yarn': + command = `yarn${prod ? ' --production' : ''}`; + break; + case 'pnpm': + command = 'pnpm install'; // pnpm checks NODE_ENV + break; + case 'cnpm': + command = `cnpm install ${prod ? ' --production' : ''}`; + break; + default: + command += prod ? ' --omit=dev' : ''; + break; + } + } + + try { + cproc.execSync(command, { + cwd: path.join(__dirname, '../../'), + stdio: [0, 1, 2], + }); + } catch (e) { + console.log('Error installing dependencies!'); + console.log(`message: ${e.message}`); + console.log(`stdout: ${e.stdout}`); + console.log(`stderr: ${e.stderr}`); + throw e; + } +}; + +pkgInstall.preserveExtraneousPlugins = () => { + // Skip if `node_modules/` is not found or inaccessible + try { + fs.accessSync(paths.nodeModules, fs.constants.R_OK); + } catch (e) { + return; + } + + const packages = fs.readdirSync(paths.nodeModules) + .filter(pkgName => pluginNamePattern.test(pkgName)); + + const packageContents = JSON.parse(fs.readFileSync(paths.currentPackage, 'utf8')); + + const extraneous = packages + // only extraneous plugins (ones not in package.json) which are not links + .filter((pkgName) => { + const extraneous = !packageContents.dependencies.hasOwnProperty(pkgName); + const isLink = fs.lstatSync(path.join(paths.nodeModules, pkgName)).isSymbolicLink(); + + return extraneous && !isLink; + }) + // reduce to a map of package names to package versions + .reduce((map, pkgName) => { + const pkgConfig = JSON.parse(fs.readFileSync(path.join(paths.nodeModules, pkgName, 'package.json'), 'utf8')); + map[pkgName] = pkgConfig.version; + return map; + }, {}); + + // Add those packages to package.json + packageContents.dependencies = sortDependencies({ ...packageContents.dependencies, ...extraneous }); + + fs.writeFileSync(paths.currentPackage, JSON.stringify(packageContents, null, 4)); +}; diff --git a/src/cli/reset.js b/src/cli/reset.js new file mode 100644 index 0000000000..3c3110b55c --- /dev/null +++ b/src/cli/reset.js @@ -0,0 +1,157 @@ +'use strict'; + +const path = require('path'); +const winston = require('winston'); +const fs = require('fs'); +const chalk = require('chalk'); +const nconf = require('nconf'); + +const db = require('../database'); +const events = require('../events'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const widgets = require('../widgets'); +const privileges = require('../privileges'); +const { paths, pluginNamePattern, themeNamePattern } = require('../constants'); + +exports.reset = async function (options) { + const map = { + theme: async function () { + let themeId = options.theme; + if (themeId === true) { + await resetThemes(); + } else { + if (!themeNamePattern.test(themeId)) { + // Allow omission of `nodebb-theme-` + themeId = `nodebb-theme-${themeId}`; + } + + themeId = await plugins.autocomplete(themeId); + await resetTheme(themeId); + } + }, + plugin: async function () { + let pluginId = options.plugin; + if (pluginId === true) { + await resetPlugins(); + } else { + if (!pluginNamePattern.test(pluginId)) { + // Allow omission of `nodebb-plugin-` + pluginId = `nodebb-plugin-${pluginId}`; + } + + pluginId = await plugins.autocomplete(pluginId); + await resetPlugin(pluginId); + } + }, + widgets: resetWidgets, + settings: resetSettings, + all: async function () { + await resetWidgets(); + await resetThemes(); + await resetPlugin(); + await resetSettings(); + }, + }; + + const tasks = Object.keys(map).filter(x => options[x]).map(x => map[x]); + + if (!tasks.length) { + console.log([ + chalk.yellow('No arguments passed in, so nothing was reset.\n'), + `Use ./nodebb reset ${chalk.red('{-t|-p|-w|-s|-a}')}`, + ' -t\tthemes', + ' -p\tplugins', + ' -w\twidgets', + ' -s\tsettings', + ' -a\tall of the above', + '', + 'Plugin and theme reset flags (-p & -t) can take a single argument', + ' e.g. ./nodebb reset -p nodebb-plugin-mentions, ./nodebb reset -t nodebb-theme-persona', + ' Prefix is optional, e.g. ./nodebb reset -p markdown, ./nodebb reset -t persona', + ].join('\n')); + + process.exit(0); + } + + try { + await db.init(); + for (const task of tasks) { + /* eslint-disable no-await-in-loop */ + await task(); + } + winston.info('[reset] Reset complete. Please run `./nodebb build` to rebuild assets.'); + process.exit(0); + } catch (err) { + winston.error(`[reset] Errors were encountered during reset -- ${err.message}`); + process.exit(1); + } +}; + +async function resetSettings() { + await privileges.global.give(['groups:local:login'], 'registered-users'); + winston.info('[reset] registered-users given login privilege'); + winston.info('[reset] Settings reset to default'); +} + +async function resetTheme(themeId) { + try { + await fs.promises.access(path.join(paths.nodeModules, themeId, 'package.json')); + } catch (err) { + winston.warn('[reset] Theme `%s` is not installed on this forum', themeId); + throw new Error('theme-not-found'); + } + await resetThemeTo(themeId); +} + +async function resetThemes() { + await resetThemeTo('nodebb-theme-persona'); +} + +async function resetThemeTo(themeId) { + await meta.themes.set({ + type: 'local', + id: themeId, + }); + await meta.configs.set('bootswatchSkin', ''); + winston.info(`[reset] Theme reset to ${themeId} and default skin`); +} + +async function resetPlugin(pluginId) { + try { + if (nconf.get('plugins:active')) { + winston.error('Cannot reset plugins while plugin state is set in the configuration (config.json, environmental variables or terminal arguments), please modify the configuration instead'); + process.exit(1); + } + const isActive = await db.isSortedSetMember('plugins:active', pluginId); + if (isActive) { + await db.sortedSetRemove('plugins:active', pluginId); + await events.log({ + type: 'plugin-deactivate', + text: pluginId, + }); + winston.info('[reset] Plugin `%s` disabled', pluginId); + } else { + winston.warn('[reset] Plugin `%s` was not active on this forum', pluginId); + winston.info('[reset] No action taken.'); + } + } catch (err) { + winston.error(`[reset] Could not disable plugin: ${pluginId} encountered error %s\n${err.stack}`); + throw err; + } +} + +async function resetPlugins() { + if (nconf.get('plugins:active')) { + winston.error('Cannot reset plugins while plugin state is set in the configuration (config.json, environmental variables or terminal arguments), please modify the configuration instead'); + process.exit(1); + } + await db.delete('plugins:active'); + winston.info('[reset] All Plugins De-activated'); +} + +async function resetWidgets() { + await plugins.reload(); + await widgets.reset(); + winston.info('[reset] All Widgets moved to Draft Zone'); +} diff --git a/src/cli/running.js b/src/cli/running.js new file mode 100644 index 0000000000..83faa23fba --- /dev/null +++ b/src/cli/running.js @@ -0,0 +1,125 @@ +'use strict'; + +const fs = require('fs'); +const childProcess = require('child_process'); +const chalk = require('chalk'); + +const fork = require('../meta/debugFork'); +const { paths } = require('../constants'); + +const cwd = paths.baseDir; + +function getRunningPid(callback) { + fs.readFile(paths.pidfile, { + encoding: 'utf-8', + }, (err, pid) => { + if (err) { + return callback(err); + } + + pid = parseInt(pid, 10); + + try { + process.kill(pid, 0); + callback(null, pid); + } catch (e) { + callback(e); + } + }); +} + +function start(options) { + if (options.dev) { + process.env.NODE_ENV = 'development'; + fork(paths.loader, ['--no-daemon', '--no-silent'], { + env: process.env, + stdio: 'inherit', + cwd, + }); + return; + } + if (options.log) { + console.log(`\n${[ + chalk.bold('Starting NodeBB with logging output'), + chalk.red('Hit ') + chalk.bold('Ctrl-C ') + chalk.red('to exit'), + 'The NodeBB process will continue to run in the background', + `Use "${chalk.yellow('./nodebb stop')}" to stop the NodeBB server`, + ].join('\n')}`); + } else if (!options.silent) { + console.log(`\n${[ + chalk.bold('Starting NodeBB'), + ` "${chalk.yellow('./nodebb stop')}" to stop the NodeBB server`, + ` "${chalk.yellow('./nodebb log')}" to view server output`, + ` "${chalk.yellow('./nodebb help')}" for more commands\n`, + ].join('\n')}`); + } + + // Spawn a new NodeBB process + const child = fork(paths.loader, process.argv.slice(3), { + env: process.env, + cwd, + }); + if (options.log) { + childProcess.spawn('tail', ['-F', './logs/output.log'], { + stdio: 'inherit', + cwd, + }); + } + + return child; +} + +function stop() { + getRunningPid((err, pid) => { + if (!err) { + process.kill(pid, 'SIGTERM'); + console.log('Stopping NodeBB. Goodbye!'); + } else { + console.log('NodeBB is already stopped.'); + } + }); +} + +function restart(options) { + getRunningPid((err, pid) => { + if (!err) { + console.log(chalk.bold('\nRestarting NodeBB')); + process.kill(pid, 'SIGTERM'); + + options.silent = true; + start(options); + } else { + console.warn('NodeBB could not be restarted, as a running instance could not be found.'); + } + }); +} + +function status() { + getRunningPid((err, pid) => { + if (!err) { + console.log(`\n${[ + chalk.bold('NodeBB Running ') + chalk.cyan(`(pid ${pid.toString()})`), + `\t"${chalk.yellow('./nodebb stop')}" to stop the NodeBB server`, + `\t"${chalk.yellow('./nodebb log')}" to view server output`, + `\t"${chalk.yellow('./nodebb restart')}" to restart NodeBB\n`, + ].join('\n')}`); + } else { + console.log(chalk.bold('\nNodeBB is not running')); + console.log(`\t"${chalk.yellow('./nodebb start')}" to launch the NodeBB server\n`); + } + }); +} + +function log() { + console.log(`${chalk.red('\nHit ') + chalk.bold('Ctrl-C ') + chalk.red('to exit\n')}\n`); + childProcess.spawn('tail', ['-F', './logs/output.log'], { + stdio: 'inherit', + cwd, + }); +} + +exports.start = start; +exports.stop = stop; +exports.restart = restart; +exports.status = status; +exports.log = log; diff --git a/src/cli/setup.js b/src/cli/setup.js new file mode 100644 index 0000000000..360cd2450a --- /dev/null +++ b/src/cli/setup.js @@ -0,0 +1,60 @@ +'use strict'; + +const winston = require('winston'); +const path = require('path'); +const nconf = require('nconf'); + +const { install } = require('../../install/web'); + +async function setup(initConfig) { + const { paths } = require('../constants'); + const install = require('../install'); + const build = require('../meta/build'); + const prestart = require('../prestart'); + const pkg = require('../../package.json'); + + winston.info('NodeBB Setup Triggered via Command Line'); + + console.log(`\nWelcome to NodeBB v${pkg.version}!`); + console.log('\nThis looks like a new installation, so you\'ll have to answer a few questions about your environment before we can proceed.'); + console.log('Press enter to accept the default setting (shown in brackets).'); + + install.values = initConfig; + const data = await install.setup(); + let configFile = paths.config; + if (nconf.get('config')) { + configFile = path.resolve(paths.baseDir, nconf.get('config')); + } + + prestart.loadConfig(configFile); + + if (!nconf.get('skip-build')) { + await build.buildAll(); + } + + let separator = ' '; + if (process.stdout.columns > 10) { + for (let x = 0, cols = process.stdout.columns - 10; x < cols; x += 1) { + separator += '='; + } + } + console.log(`\n${separator}\n`); + + if (data.hasOwnProperty('password')) { + console.log('An administrative user was automatically created for you:'); + console.log(` Username: ${data.username}`); + console.log(` Password: ${data.password}`); + console.log(''); + } + console.log('NodeBB Setup Completed. Run "./nodebb start" to manually start your NodeBB server.'); + + // If I am a child process, notify the parent of the returned data before exiting (useful for notifying + // hosts of auto-generated username/password during headless setups) + if (process.send) { + process.send(data); + } + process.exit(); +} + +exports.setup = setup; +exports.webInstall = install; diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js new file mode 100644 index 0000000000..e83027e177 --- /dev/null +++ b/src/cli/upgrade-plugins.js @@ -0,0 +1,159 @@ +'use strict'; + +const prompt = require('prompt'); +const request = require('request-promise-native'); +const cproc = require('child_process'); +const semver = require('semver'); +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); + +const { paths, pluginNamePattern } = require('../constants'); +const pkgInstall = require('./package-install'); + +const packageManager = pkgInstall.getPackageManager(); +let packageManagerExecutable = packageManager; +const packageManagerInstallArgs = packageManager === 'yarn' ? ['add'] : ['install', '--save']; + +if (process.platform === 'win32') { + packageManagerExecutable += '.cmd'; +} + +async function getModuleVersions(modules) { + const versionHash = {}; + const batch = require('../batch'); + await batch.processArray(modules, async (moduleNames) => { + await Promise.all(moduleNames.map(async (module) => { + let pkg = await fs.promises.readFile( + path.join(paths.nodeModules, module, 'package.json'), { encoding: 'utf-8' } + ); + pkg = JSON.parse(pkg); + versionHash[module] = pkg.version; + })); + }, { + batch: 50, + }); + + return versionHash; +} + +async function getInstalledPlugins() { + let [deps, bundled] = await Promise.all([ + fs.promises.readFile(paths.currentPackage, { encoding: 'utf-8' }), + fs.promises.readFile(paths.installPackage, { encoding: 'utf-8' }), + ]); + + deps = Object.keys(JSON.parse(deps).dependencies) + .filter(pkgName => pluginNamePattern.test(pkgName)); + bundled = Object.keys(JSON.parse(bundled).dependencies) + .filter(pkgName => pluginNamePattern.test(pkgName)); + + + // Whittle down deps to send back only extraneously installed plugins/themes/etc + const checklist = deps.filter((pkgName) => { + if (bundled.includes(pkgName)) { + return false; + } + + // Ignore git repositories + try { + fs.accessSync(path.join(paths.nodeModules, pkgName, '.git')); + return false; + } catch (e) { + return true; + } + }); + + return await getModuleVersions(checklist); +} + +async function getCurrentVersion() { + let pkg = await fs.promises.readFile(paths.installPackage, { encoding: 'utf-8' }); + pkg = JSON.parse(pkg); + return pkg.version; +} + +async function getSuggestedModules(nbbVersion, toCheck) { + let body = await request({ + method: 'GET', + url: `https://packages.nodebb.org/api/v1/suggest?version=${nbbVersion}&package[]=${toCheck.join('&package[]=')}`, + json: true, + }); + if (!Array.isArray(body) && toCheck.length === 1) { + body = [body]; + } + return body; +} + +async function checkPlugins() { + process.stdout.write('Checking installed plugins and themes for updates... '); + const [plugins, nbbVersion] = await Promise.all([ + getInstalledPlugins(), + getCurrentVersion(), + ]); + + const toCheck = Object.keys(plugins); + if (!toCheck.length) { + process.stdout.write(chalk.green(' OK')); + return []; // no extraneous plugins installed + } + const suggestedModules = await getSuggestedModules(nbbVersion, toCheck); + process.stdout.write(chalk.green(' OK')); + + let current; + let suggested; + const upgradable = suggestedModules.map((suggestObj) => { + current = plugins[suggestObj.package]; + suggested = suggestObj.version; + + if (suggestObj.code === 'match-found' && semver.gt(suggested, current)) { + return { + name: suggestObj.package, + current: current, + suggested: suggested, + }; + } + return null; + }).filter(Boolean); + + return upgradable; +} + +async function upgradePlugins() { + try { + const found = await checkPlugins(); + if (found && found.length) { + process.stdout.write(`\n\nA total of ${chalk.bold(String(found.length))} package(s) can be upgraded:\n\n`); + found.forEach((suggestObj) => { + process.stdout.write(`${chalk.yellow(' * ') + suggestObj.name} (${chalk.yellow(suggestObj.current)} -> ${chalk.green(suggestObj.suggested)})\n`); + }); + } else { + console.log(chalk.green('\nAll packages up-to-date!')); + return; + } + + prompt.message = ''; + prompt.delimiter = ''; + + prompt.start(); + const result = await prompt.get({ + name: 'upgrade', + description: '\nProceed with upgrade (y|n)?', + type: 'string', + }); + + if (['y', 'Y', 'yes', 'YES'].includes(result.upgrade)) { + console.log('\nUpgrading packages...'); + const args = packageManagerInstallArgs.concat(found.map(suggestObj => `${suggestObj.name}@${suggestObj.suggested}`)); + + cproc.execFileSync(packageManagerExecutable, args, { stdio: 'ignore' }); + } else { + console.log(`${chalk.yellow('Package upgrades skipped')}. Check for upgrades at any time by running "${chalk.green('./nodebb upgrade -p')}".`); + } + } catch (err) { + console.log(`${chalk.yellow('Warning')}: An unexpected error occured when attempting to verify plugin upgradability`); + throw err; + } +} + +exports.upgradePlugins = upgradePlugins; diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js new file mode 100644 index 0000000000..2ac32ade47 --- /dev/null +++ b/src/cli/upgrade.js @@ -0,0 +1,95 @@ +'use strict'; + +const nconf = require('nconf'); +const chalk = require('chalk'); + +const packageInstall = require('./package-install'); +const { upgradePlugins } = require('./upgrade-plugins'); + +const steps = { + package: { + message: 'Updating package.json file with defaults...', + handler: function () { + packageInstall.updatePackageFile(); + packageInstall.preserveExtraneousPlugins(); + process.stdout.write(chalk.green(' OK\n')); + }, + }, + install: { + message: 'Bringing base dependencies up to date...', + handler: function () { + process.stdout.write(chalk.green(' started\n')); + packageInstall.installAll(); + }, + }, + plugins: { + message: 'Checking installed plugins for updates...', + handler: async function () { + await require('../database').init(); + await upgradePlugins(); + }, + }, + schema: { + message: 'Updating NodeBB data store schema...', + handler: async function () { + await require('../database').init(); + await require('../meta').configs.init(); + await require('../upgrade').run(); + }, + }, + build: { + message: 'Rebuilding assets...', + handler: async function () { + await require('../meta/build').buildAll(); + }, + }, +}; + +async function runSteps(tasks) { + try { + for (let i = 0; i < tasks.length; i++) { + const step = steps[tasks[i]]; + if (step && step.message && step.handler) { + process.stdout.write(`\n${chalk.bold(`${i + 1}. `)}${chalk.yellow(step.message)}`); + /* eslint-disable-next-line */ + await step.handler(); + } + } + const message = 'NodeBB Upgrade Complete!'; + // some consoles will return undefined/zero columns, + // so just use 2 spaces in upgrade script if we can't get our column count + const { columns } = process.stdout; + const spaces = columns ? new Array(Math.floor(columns / 2) - (message.length / 2) + 1).join(' ') : ' '; + + console.log(`\n\n${spaces}${chalk.green.bold(message)}\n`); + + process.exit(); + } catch (err) { + console.error(`Error occurred during upgrade: ${err.stack}`); + throw err; + } +} + +async function runUpgrade(upgrades, options) { + console.log(chalk.cyan('\nUpdating NodeBB...')); + options = options || {}; + // disable mongo timeouts during upgrade + nconf.set('mongo:options:socketTimeoutMS', 0); + + if (upgrades === true) { + let tasks = Object.keys(steps); + if (options.package || options.install || + options.plugins || options.schema || options.build) { + tasks = tasks.filter(key => options[key]); + } + await runSteps(tasks); + return; + } + + await require('../database').init(); + await require('../meta').configs.init(); + await require('../upgrade').runParticular(upgrades); + process.exit(0); +} + +exports.upgrade = runUpgrade; diff --git a/src/cli/user.js b/src/cli/user.js new file mode 100644 index 0000000000..026758eafa --- /dev/null +++ b/src/cli/user.js @@ -0,0 +1,311 @@ +'use strict'; + +const { Command, Option } = require('commander'); + +module.exports = () => { + const userCmd = new Command('user') + .description('Manage users') + .arguments('[command]'); + + userCmd.configureHelp(require('./colors')); + const userCommands = UserCommands(); + + userCmd + .command('info') + .description('Display user info by uid/username/userslug.') + .option('-i, --uid ', 'Retrieve user by uid') + .option('-u, --username ', 'Retrieve user by username') + .option('-s, --userslug ', 'Retrieve user by userslug') + .action((...args) => execute(userCommands.info, args)); + userCmd + .command('create') + .description('Create a new user.') + .arguments('') + .option('-p, --password ', 'Set a new password. (Auto-generates if omitted)') + .option('-e, --email ', 'Associate with an email.') + .action((...args) => execute(userCommands.create, args)); + userCmd + .command('reset') + .description('Reset a user\'s password or send a password reset email.') + .arguments('') + .option('-p, --password ', 'Set a new password. (Auto-generates if passed empty)', false) + .option('-s, --send-reset-email', 'Send a password reset email.', false) + .action((...args) => execute(userCommands.reset, args)); + userCmd + .command('delete') + .description('Delete user(s) and/or their content') + .arguments('') + .addOption( + new Option('-t, --type [operation]', 'Delete user content ([purge]), leave content ([account]), or delete content only ([content])') + .choices(['purge', 'account', 'content']).default('purge') + ) + .action((...args) => execute(userCommands.deleteUser, args)); + + const make = userCmd.command('make') + .description('Make user(s) admin, global mod, moderator or a regular user.') + .arguments('[command]'); + + make.command('admin') + .description('Make user(s) an admin') + .arguments('') + .action((...args) => execute(userCommands.makeAdmin, args)); + make.command('global-mod') + .description('Make user(s) a global moderator') + .arguments('') + .action((...args) => execute(userCommands.makeGlobalMod, args)); + make.command('mod') + .description('Make uid(s) of user(s) moderator of given category IDs (cids)') + .arguments('') + .requiredOption('-c, --cid ', 'ID(s) of categories to make the user a moderator of') + .action((...args) => execute(userCommands.makeMod, args)); + make.command('regular') + .description('Make user(s) a non-privileged user') + .arguments('') + .action((...args) => execute(userCommands.makeRegular, args)); + + return userCmd; +}; + +let db; +let user; +let groups; +let privileges; +let privHelpers; +let utils; +let winston; + +async function init() { + db = require('../database'); + await db.init(); + + user = require('../user'); + groups = require('../groups'); + privileges = require('../privileges'); + privHelpers = require('../privileges/helpers'); + utils = require('../utils'); + winston = require('winston'); +} + +async function execute(cmd, args) { + await init(); + try { + await cmd(...args); + } catch (err) { + const userError = err.name === 'UserError'; + winston.error(`[userCmd/${cmd.name}] ${userError ? `${err.message}` : 'Command failed.'}`, userError ? '' : err); + process.exit(1); + } + + process.exit(); +} + +function UserCmdHelpers() { + async function getAdminUidOrFail() { + const adminUid = await user.getFirstAdminUid(); + if (!adminUid) { + const err = new Error('An admin account does not exists to execute the operation.'); + err.name = 'UserError'; + throw err; + } + return adminUid; + } + + async function setupApp() { + const nconf = require('nconf'); + const Benchpress = require('benchpressjs'); + + const meta = require('../meta'); + await meta.configs.init(); + + const webserver = require('../webserver'); + const viewsDir = nconf.get('views_dir'); + + webserver.app.engine('tpl', (filepath, data, next) => { + filepath = filepath.replace(/\.tpl$/, '.js'); + + Benchpress.__express(filepath, data, next); + }); + webserver.app.set('view engine', 'tpl'); + webserver.app.set('views', viewsDir); + + const emailer = require('../emailer'); + emailer.registerApp(webserver.app); + } + + const argParsers = { + intParse: (value, varName) => { + const parsedValue = parseInt(value, 10); + if (isNaN(parsedValue)) { + const err = new Error(`"${varName}" expected to be a number.`); + err.name = 'UserError'; + throw err; + } + return parsedValue; + }, + intArrayParse: (values, varName) => values.map(value => argParsers.intParse(value, varName)), + }; + + return { + argParsers, + getAdminUidOrFail, + setupApp, + }; +} + +function UserCommands() { + const { argParsers, getAdminUidOrFail, setupApp } = UserCmdHelpers(); + + async function info({ uid, username, userslug }) { + if (!uid && !username && !userslug) { + return winston.error('[userCmd/info] At least one option has to be passed (--uid, --username or --userslug).'); + } + + if (uid) { + uid = argParsers.intParse(uid, 'uid'); + } else if (username) { + uid = await user.getUidByUsername(username); + } else { + uid = await user.getUidByUserslug(userslug); + } + + const userData = await user.getUserData(uid); + winston.info('[userCmd/info] User info retrieved:'); + console.log(userData); + } + + async function create(username, { password, email }) { + let pwGenerated = false; + if (password === undefined) { + password = utils.generateUUID().slice(0, 8); + pwGenerated = true; + } + + const userExists = await user.getUidByUsername(username); + if (userExists) { + return winston.error(`[userCmd/create] A user with username '${username}' already exists`); + } + + const uid = await user.create({ + username, + password, + email, + }); + + winston.info(`[userCmd/create] User '${username}'${password ? '' : ' without a password'} has been created with uid: ${uid}.\ +${pwGenerated ? ` Generated password: ${password}` : ''}`); + } + + async function reset(uid, { password, sendResetEmail }) { + uid = argParsers.intParse(uid, 'uid'); + + if (password === false && sendResetEmail === false) { + return winston.error('[userCmd/reset] At least one option has to be passed (--password or --send-reset-email).'); + } + + const userExists = await user.exists(uid); + if (!userExists) { + return winston.error(`[userCmd/reset] A user with given uid does not exists.`); + } + + let pwGenerated = false; + if (password === '') { + password = utils.generateUUID().slice(0, 8); + pwGenerated = true; + } + + const adminUid = await getAdminUidOrFail(); + + if (password) { + await user.setUserField(uid, 'password', ''); + await user.changePassword(adminUid, { + newPassword: password, + uid, + }); + winston.info(`[userCmd/reset] ${password ? 'User password changed.' : ''}${pwGenerated ? ` Generated password: ${password}` : ''}`); + } + + if (sendResetEmail) { + const userEmail = await user.getUserField(uid, 'email'); + if (!userEmail) { + return winston.error('User doesn\'t have an email address to send reset email.'); + } + await setupApp(); + await user.reset.send(userEmail); + winston.info('[userCmd/reset] Password reset email has been sent.'); + } + } + + async function deleteUser(uids, { type }) { + uids = argParsers.intArrayParse(uids, 'uids'); + + const userExists = await user.exists(uids); + if (!userExists || userExists.some(r => r === false)) { + return winston.error(`[userCmd/reset] A user with given uid does not exists.`); + } + + await db.initSessionStore(); + const adminUid = await getAdminUidOrFail(); + + switch (type) { + case 'purge': + await Promise.all(uids.map(uid => user.delete(adminUid, uid))); + winston.info(`[userCmd/delete] User(s) with their content has been deleted.`); + break; + case 'account': + await Promise.all(uids.map(uid => user.deleteAccount(uid))); + winston.info(`[userCmd/delete] User(s) has been deleted, their content left intact.`); + break; + case 'content': + await Promise.all(uids.map(uid => user.deleteContent(adminUid, uid))); + winston.info(`[userCmd/delete] User(s)' content has been deleted.`); + break; + } + } + + async function makeAdmin(uids) { + uids = argParsers.intArrayParse(uids, 'uids'); + await Promise.all(uids.map(uid => groups.join('administrators', uid))); + + winston.info('[userCmd/make/admin] User(s) added as administrators.'); + } + + async function makeGlobalMod(uids) { + uids = argParsers.intArrayParse(uids, 'uids'); + await Promise.all(uids.map(uid => groups.join('Global Moderators', uid))); + + winston.info('[userCmd/make/globalMod] User(s) added as global moderators.'); + } + + async function makeMod(uids, { cid: cids }) { + uids = argParsers.intArrayParse(uids, 'uids'); + cids = argParsers.intArrayParse(cids, 'cids'); + + const categoryPrivList = await privileges.categories.getPrivilegeList(); + await privHelpers.giveOrRescind(groups.join, categoryPrivList, cids, uids); + + winston.info('[userCmd/make/mod] User(s) added as moderators to given categories.'); + } + + async function makeRegular(uids) { + uids = argParsers.intArrayParse(uids, 'uids'); + + await Promise.all(uids.map(uid => groups.leave(['administrators', 'Global Moderators'], uid))); + + const categoryPrivList = await privileges.categories.getPrivilegeList(); + const cids = await db.getSortedSetRevRange('categories:cid', 0, -1); + await privHelpers.giveOrRescind(groups.leave, categoryPrivList, cids, uids); + + winston.info('[userCmd/make/regular] User(s) made regular/non-privileged.'); + } + + return { + info, + create, + reset, + deleteUser, + makeAdmin, + makeGlobalMod, + makeMod, + makeRegular, + }; +} diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000000..19b584402a --- /dev/null +++ b/src/constants.js @@ -0,0 +1,28 @@ +'use strict'; + +const path = require('path'); + +const baseDir = path.join(__dirname, '../'); +const loader = path.join(baseDir, 'loader.js'); +const app = path.join(baseDir, 'app.js'); +const pidfile = path.join(baseDir, 'pidfile'); +const config = path.join(baseDir, 'config.json'); +const currentPackage = path.join(baseDir, 'package.json'); +const installPackage = path.join(baseDir, 'install/package.json'); +const nodeModules = path.join(baseDir, 'node_modules'); +const themes = path.join(baseDir, 'themes'); + +exports.paths = { + baseDir, + loader, + app, + pidfile, + config, + currentPackage, + installPackage, + nodeModules, + themes, +}; + +exports.pluginNamePattern = /^(@[\w-]+\/)?nodebb-(theme|plugin|widget|rewards)-[\w-]+$/; +exports.themeNamePattern = /^(@[\w-]+\/)?nodebb-theme-[\w-]+$/; diff --git a/src/controllers/404.js b/src/controllers/404.js new file mode 100644 index 0000000000..bb27aa366f --- /dev/null +++ b/src/controllers/404.js @@ -0,0 +1,64 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); +const validator = require('validator'); + +const meta = require('../meta'); +const plugins = require('../plugins'); +const middleware = require('../middleware'); +const helpers = require('../middleware/helpers'); + +exports.handle404 = function handle404(req, res) { + const relativePath = nconf.get('relative_path'); + const isClientScript = new RegExp(`^${relativePath}\\/assets\\/src\\/.+\\.js(\\?v=\\w+)?$`); + + if (plugins.hooks.hasListeners('action:meta.override404')) { + return plugins.hooks.fire('action:meta.override404', { + req: req, + res: res, + error: {}, + }); + } + + if (isClientScript.test(req.url)) { + res.type('text/javascript').status(404).send('Not Found'); + } else if ( + !res.locals.isAPI && ( + req.path.startsWith(`${relativePath}/assets/uploads`) || + (req.get('accept') && !req.get('accept').includes('text/html')) || + req.path === '/favicon.ico' + ) + ) { + meta.errors.log404(req.path || ''); + res.sendStatus(404); + } else if (req.accepts('html')) { + if (process.env.NODE_ENV === 'development') { + winston.warn(`Route requested but not found: ${req.url}`); + } + + meta.errors.log404(req.path.replace(/^\/api/, '') || ''); + exports.send404(req, res); + } else { + res.status(404).type('txt').send('Not found'); + } +}; + +exports.send404 = async function (req, res) { + res.status(404); + const path = String(req.path || ''); + if (res.locals.isAPI) { + return res.json({ + path: validator.escape(path.replace(/^\/api/, '')), + title: '[[global:404.title]]', + bodyClass: helpers.buildBodyClass(req, res), + }); + } + + await middleware.buildHeaderAsync(req, res); + await res.render('404', { + path: validator.escape(path), + title: '[[global:404.title]]', + bodyClass: helpers.buildBodyClass(req, res), + }); +}; diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js new file mode 100644 index 0000000000..af28d9b7ee --- /dev/null +++ b/src/controllers/accounts.js @@ -0,0 +1,20 @@ +'use strict'; + +const accountsController = { + profile: require('./accounts/profile'), + edit: require('./accounts/edit'), + info: require('./accounts/info'), + categories: require('./accounts/categories'), + settings: require('./accounts/settings'), + groups: require('./accounts/groups'), + follow: require('./accounts/follow'), + posts: require('./accounts/posts'), + notifications: require('./accounts/notifications'), + chats: require('./accounts/chats'), + sessions: require('./accounts/sessions'), + blocks: require('./accounts/blocks'), + uploads: require('./accounts/uploads'), + consent: require('./accounts/consent'), +}; + +module.exports = accountsController; diff --git a/src/controllers/accounts/blocks.js b/src/controllers/accounts/blocks.js new file mode 100644 index 0000000000..25e789436f --- /dev/null +++ b/src/controllers/accounts/blocks.js @@ -0,0 +1,39 @@ +'use strict'; + +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); +const pagination = require('../../pagination'); +const user = require('../../user'); +const plugins = require('../../plugins'); + +const blocksController = module.exports; + +blocksController.getBlocks = async function (req, res, next) { + const page = parseInt(req.query.page, 10) || 1; + const resultsPerPage = 50; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + const uids = await user.blocks.list(userData.uid); + const data = await plugins.hooks.fire('filter:user.getBlocks', { + uids: uids, + uid: userData.uid, + start: start, + stop: stop, + }); + + data.uids = data.uids.slice(start, stop + 1); + userData.users = await user.getUsers(data.uids, req.uid); + userData.title = `[[pages:account/blocks, ${userData.username}]]`; + + const pageCount = Math.ceil(userData.counts.blocks / resultsPerPage); + userData.pagination = pagination.create(page, pageCount); + + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[user:blocks]]' }]); + + res.render('account/blocks', userData); +}; diff --git a/src/controllers/accounts/categories.js b/src/controllers/accounts/categories.js new file mode 100644 index 0000000000..a0b7dc2a4d --- /dev/null +++ b/src/controllers/accounts/categories.js @@ -0,0 +1,44 @@ +'use strict'; + +const user = require('../../user'); +const categories = require('../../categories'); +const accountHelpers = require('./helpers'); +const helpers = require('../helpers'); +const pagination = require('../../pagination'); +const meta = require('../../meta'); + +const categoriesController = module.exports; + +categoriesController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + const [states, allCategoriesData] = await Promise.all([ + user.getCategoryWatchState(userData.uid), + categories.buildForSelect(userData.uid, 'find', ['descriptionParsed', 'depth', 'slug']), + ]); + + const pageCount = Math.max(1, Math.ceil(allCategoriesData.length / meta.config.categoriesPerPage)); + const page = Math.min(parseInt(req.query.page, 10) || 1, pageCount); + const start = Math.max(0, (page - 1) * meta.config.categoriesPerPage); + const stop = start + meta.config.categoriesPerPage - 1; + const categoriesData = allCategoriesData.slice(start, stop + 1); + + + categoriesData.forEach((category) => { + if (category) { + category.isIgnored = states[category.cid] === categories.watchStates.ignoring; + category.isWatched = states[category.cid] === categories.watchStates.watching; + category.isNotWatched = states[category.cid] === categories.watchStates.notwatching; + } + }); + userData.categories = categoriesData; + userData.title = `[[pages:account/watched_categories, ${userData.username}]]`; + userData.breadcrumbs = helpers.buildBreadcrumbs([ + { text: userData.username, url: `/user/${userData.userslug}` }, + { text: '[[pages:categories]]' }, + ]); + userData.pagination = pagination.create(page, pageCount, req.query); + res.render('account/categories', userData); +}; diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js new file mode 100644 index 0000000000..f534eaf619 --- /dev/null +++ b/src/controllers/accounts/chats.js @@ -0,0 +1,65 @@ +'use strict'; + +const messaging = require('../../messaging'); +const meta = require('../../meta'); +const user = require('../../user'); +const privileges = require('../../privileges'); +const helpers = require('../helpers'); + +const chatsController = module.exports; + +chatsController.get = async function (req, res, next) { + if (meta.config.disableChat) { + return next(); + } + + const uid = await user.getUidByUserslug(req.params.userslug); + if (!uid) { + return next(); + } + const canChat = await privileges.global.can('chat', req.uid); + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } + const recentChats = await messaging.getRecentChats(req.uid, uid, 0, 19); + if (!recentChats) { + return next(); + } + + if (!req.params.roomid) { + return res.render('chats', { + rooms: recentChats.rooms, + uid: uid, + userslug: req.params.userslug, + nextStart: recentChats.nextStart, + allowed: true, + title: '[[pages:chats]]', + }); + } + const room = await messaging.loadRoom(req.uid, { uid: uid, roomId: req.params.roomid }); + if (!room) { + return next(); + } + + room.rooms = recentChats.rooms; + room.nextStart = recentChats.nextStart; + room.title = room.roomName || room.usernames || '[[pages:chats]]'; + room.uid = uid; + room.userslug = req.params.userslug; + + room.canViewInfo = await privileges.global.can('view:users:info', uid); + + res.render('chats', room); +}; + +chatsController.redirectToChat = async function (req, res, next) { + if (!req.loggedIn) { + return next(); + } + const userslug = await user.getUserField(req.uid, 'userslug'); + if (!userslug) { + return next(); + } + const roomid = parseInt(req.params.roomid, 10); + helpers.redirect(res, `/user/${userslug}/chats${roomid ? `/${roomid}` : ''}`); +}; diff --git a/src/controllers/accounts/consent.js b/src/controllers/accounts/consent.js new file mode 100644 index 0000000000..63ff886f3a --- /dev/null +++ b/src/controllers/accounts/consent.js @@ -0,0 +1,30 @@ +'use strict'; + +const db = require('../../database'); +const meta = require('../../meta'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); + +const consentController = module.exports; + +consentController.get = async function (req, res, next) { + if (!meta.config.gdpr_enabled) { + return next(); + } + + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + const consented = await db.getObjectField(`user:${userData.uid}`, 'gdpr_consent'); + userData.gdpr_consent = parseInt(consented, 10) === 1; + userData.digest = { + frequency: meta.config.dailyDigestFreq || 'off', + enabled: meta.config.dailyDigestFreq !== 'off', + }; + + userData.title = '[[user:consent.title]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[user:consent.title]]' }]); + + res.render('account/consent', userData); +}; diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js new file mode 100644 index 0000000000..da3c1acd84 --- /dev/null +++ b/src/controllers/accounts/edit.js @@ -0,0 +1,169 @@ +'use strict'; + +const user = require('../../user'); +const meta = require('../../meta'); +const helpers = require('../helpers'); +const groups = require('../../groups'); +const accountHelpers = require('./helpers'); +const privileges = require('../../privileges'); +const file = require('../../file'); + +const editController = module.exports; + +editController.get = async function (req, res, next) { + const [userData, canUseSignature] = await Promise.all([ + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query), + privileges.global.can('signature', req.uid), + ]); + if (!userData) { + return next(); + } + userData.maximumSignatureLength = meta.config.maximumSignatureLength; + userData.maximumAboutMeLength = meta.config.maximumAboutMeLength; + userData.maximumProfileImageSize = meta.config.maximumProfileImageSize; + userData.allowProfilePicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:profile-picture']; + userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture']; + userData.allowProfileImageUploads = meta.config.allowProfileImageUploads; + userData.allowedProfileImageExtensions = user.getAllowedProfileImageExtensions().map(ext => `.${ext}`).join(', '); + userData.allowMultipleBadges = meta.config.allowMultipleBadges === 1; + userData.allowAccountDelete = meta.config.allowAccountDelete === 1; + userData.allowWebsite = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:website']; + userData.allowAboutMe = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:aboutme']; + userData.allowSignature = canUseSignature && (!userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:signature']); + userData.profileImageDimension = meta.config.profileImageDimension; + userData.defaultAvatar = user.getDefaultAvatar(); + + userData.groups = userData.groups.filter(g => g && g.userTitleEnabled && !groups.isPrivilegeGroup(g.name) && g.name !== 'registered-users'); + + if (!userData.allowMultipleBadges) { + userData.groupTitle = userData.groupTitleArray[0]; + } + + userData.groups.sort((a, b) => { + const i1 = userData.groupTitleArray.indexOf(a.name); + const i2 = userData.groupTitleArray.indexOf(b.name); + if (i1 === -1) { + return 1; + } else if (i2 === -1) { + return -1; + } + return i1 - i2; + }); + userData.groups.forEach((group) => { + group.userTitle = group.userTitle || group.displayName; + group.selected = userData.groupTitleArray.includes(group.name); + }); + userData.groupSelectSize = Math.min(10, Math.max(5, userData.groups.length + 1)); + + userData.title = `[[pages:account/edit, ${userData.username}]]`; + userData.breadcrumbs = helpers.buildBreadcrumbs([ + { + text: userData.username, + url: `/user/${userData.userslug}`, + }, + { + text: '[[user:edit]]', + }, + ]); + userData.editButtons = []; + res.render('account/edit', userData); +}; + +editController.password = async function (req, res, next) { + await renderRoute('password', req, res, next); +}; + +editController.username = async function (req, res, next) { + await renderRoute('username', req, res, next); +}; + +editController.email = async function (req, res, next) { + const targetUid = await user.getUidByUserslug(req.params.userslug); + if (!targetUid) { + return next(); + } + + const [isAdminOrGlobalMod, canEdit] = await Promise.all([ + user.isAdminOrGlobalMod(req.uid), + privileges.users.canEdit(req.uid, targetUid), + ]); + + if (!isAdminOrGlobalMod && !canEdit) { + return next(); + } + + req.session.returnTo = `/uid/${targetUid}`; + req.session.registration = req.session.registration || {}; + req.session.registration.updateEmail = true; + req.session.registration.uid = targetUid; + helpers.redirect(res, '/register/complete'); +}; + +async function renderRoute(name, req, res, next) { + const userData = await getUserData(req, next); + if (!userData) { + return next(); + } + if (meta.config[`${name}:disableEdit`] && !userData.isAdmin) { + return helpers.notAllowed(req, res); + } + + if (name === 'password') { + userData.minimumPasswordLength = meta.config.minimumPasswordLength; + userData.minimumPasswordStrength = meta.config.minimumPasswordStrength; + } + + userData.title = `[[pages:account/edit/${name}, ${userData.username}]]`; + userData.breadcrumbs = helpers.buildBreadcrumbs([ + { + text: userData.username, + url: `/user/${userData.userslug}`, + }, + { + text: '[[user:edit]]', + url: `/user/${userData.userslug}/edit`, + }, + { + text: `[[user:${name}]]`, + }, + ]); + + res.render(`account/edit/${name}`, userData); +} + +async function getUserData(req) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return null; + } + + userData.hasPassword = await user.hasPassword(userData.uid); + return userData; +} + +editController.uploadPicture = async function (req, res, next) { + const userPhoto = req.files.files[0]; + try { + const updateUid = await user.getUidByUserslug(req.params.userslug); + const isAllowed = await privileges.users.canEdit(req.uid, updateUid); + if (!isAllowed) { + return helpers.notAllowed(req, res); + } + await user.checkMinReputation(req.uid, updateUid, 'min:rep:profile-picture'); + + const image = await user.uploadCroppedPictureFile({ + callerUid: req.uid, + uid: updateUid, + file: userPhoto, + }); + + res.json([{ + name: userPhoto.name, + url: image.url, + }]); + } catch (err) { + next(err); + } finally { + await file.delete(userPhoto.path); + } +}; diff --git a/src/controllers/accounts/follow.js b/src/controllers/accounts/follow.js new file mode 100644 index 0000000000..e54f584dac --- /dev/null +++ b/src/controllers/accounts/follow.js @@ -0,0 +1,41 @@ +'use strict'; + +const user = require('../../user'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); +const pagination = require('../../pagination'); + +const followController = module.exports; + +followController.getFollowing = async function (req, res, next) { + await getFollow('account/following', 'following', req, res, next); +}; + +followController.getFollowers = async function (req, res, next) { + await getFollow('account/followers', 'followers', req, res, next); +}; + +async function getFollow(tpl, name, req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + + const page = parseInt(req.query.page, 10) || 1; + const resultsPerPage = 50; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + + userData.title = `[[pages:${tpl}, ${userData.username}]]`; + + const method = name === 'following' ? 'getFollowing' : 'getFollowers'; + userData.users = await user[method](userData.uid, start, stop); + + const count = name === 'following' ? userData.followingCount : userData.followerCount; + const pageCount = Math.ceil(count / resultsPerPage); + userData.pagination = pagination.create(page, pageCount); + + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: `[[user:${name}]]` }]); + + res.render(tpl, userData); +} diff --git a/src/controllers/accounts/groups.js b/src/controllers/accounts/groups.js new file mode 100644 index 0000000000..287813068b --- /dev/null +++ b/src/controllers/accounts/groups.js @@ -0,0 +1,25 @@ +'use strict'; + +const groups = require('../../groups'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); + +const groupsController = module.exports; + +groupsController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + let groupsData = await groups.getUserGroups([userData.uid]); + groupsData = groupsData[0]; + const groupNames = groupsData.filter(Boolean).map(group => group.name); + const members = await groups.getMemberUsers(groupNames, 0, 3); + groupsData.forEach((group, index) => { + group.members = members[index]; + }); + userData.groups = groupsData; + userData.title = `[[pages:account/groups, ${userData.username}]]`; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[global:header.groups]]' }]); + res.render('account/groups', userData); +}; diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js new file mode 100644 index 0000000000..fee96fd44d --- /dev/null +++ b/src/controllers/accounts/helpers.js @@ -0,0 +1,267 @@ +'use strict'; + +const validator = require('validator'); +const nconf = require('nconf'); + +const db = require('../../database'); +const user = require('../../user'); +const groups = require('../../groups'); +const plugins = require('../../plugins'); +const meta = require('../../meta'); +const utils = require('../../utils'); +const privileges = require('../../privileges'); +const translator = require('../../translator'); +const messaging = require('../../messaging'); +const categories = require('../../categories'); + +const helpers = module.exports; + +helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {}) { + const uid = await user.getUidByUserslug(userslug); + if (!uid) { + return null; + } + + const results = await getAllData(uid, callerUID); + if (!results.userData) { + throw new Error('[[error:invalid-uid]]'); + } + await parseAboutMe(results.userData); + + let { userData } = results; + const { userSettings } = results; + const { isAdmin } = results; + const { isGlobalModerator } = results; + const { isModerator } = results; + const { canViewInfo } = results; + const isSelf = parseInt(callerUID, 10) === parseInt(userData.uid, 10); + + userData.age = Math.max( + 0, + userData.birthday ? Math.floor((new Date().getTime() - new Date(userData.birthday).getTime()) / 31536000000) : 0 + ); + + userData = await user.hidePrivateData(userData, callerUID); + userData.emailClass = userSettings.showemail ? 'hide' : ''; + + // If email unconfirmed, hide from result set + if (!userData['email:confirmed']) { + userData.email = ''; + } + + if (isAdmin || isSelf || (canViewInfo && !results.isTargetAdmin)) { + userData.ips = results.ips; + } + + if (!isAdmin && !isGlobalModerator && !isModerator) { + userData.moderationNote = undefined; + } + + userData.isBlocked = results.isBlocked; + userData.yourid = callerUID; + userData.theirid = userData.uid; + userData.isTargetAdmin = results.isTargetAdmin; + userData.isAdmin = isAdmin; + userData.isGlobalModerator = isGlobalModerator; + userData.isModerator = isModerator; + userData.isAdminOrGlobalModerator = isAdmin || isGlobalModerator; + userData.isAdminOrGlobalModeratorOrModerator = isAdmin || isGlobalModerator || isModerator; + userData.isSelfOrAdminOrGlobalModerator = isSelf || isAdmin || isGlobalModerator; + userData.canEdit = results.canEdit; + userData.canBan = results.canBanUser; + userData.canMute = results.canMuteUser; + userData.canFlag = (await privileges.users.canFlag(callerUID, userData.uid)).flag; + userData.canChangePassword = isAdmin || (isSelf && !meta.config['password:disableEdit']); + userData.isSelf = isSelf; + userData.isFollowing = results.isFollowing; + userData.hasPrivateChat = results.hasPrivateChat; + userData.showHidden = results.canEdit; // remove in v1.19.0 + userData.groups = Array.isArray(results.groups) && results.groups.length ? results.groups[0] : []; + userData.disableSignatures = meta.config.disableSignatures === 1; + userData['reputation:disabled'] = meta.config['reputation:disabled'] === 1; + userData['downvote:disabled'] = meta.config['downvote:disabled'] === 1; + userData['email:confirmed'] = !!userData['email:confirmed']; + userData.profile_links = filterLinks(results.profile_menu.links, { + self: isSelf, + other: !isSelf, + moderator: isModerator, + globalMod: isGlobalModerator, + admin: isAdmin, + canViewInfo: canViewInfo, + }); + + userData.sso = results.sso.associations; + userData.banned = Boolean(userData.banned); + userData.muted = parseInt(userData.mutedUntil, 10) > Date.now(); + userData.website = escape(userData.website); + userData.websiteLink = !userData.website.startsWith('http') ? `http://${userData.website}` : userData.website; + userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), ''); + + userData.fullname = escape(userData.fullname); + userData.location = escape(userData.location); + userData.signature = escape(userData.signature); + userData.birthday = validator.escape(String(userData.birthday || '')); + userData.moderationNote = validator.escape(String(userData.moderationNote || '')); + + if (userData['cover:url']) { + userData['cover:url'] = userData['cover:url'].startsWith('http') ? userData['cover:url'] : (nconf.get('relative_path') + userData['cover:url']); + } else { + userData['cover:url'] = require('../../coverPhoto').getDefaultProfileCover(userData.uid); + } + + userData['cover:position'] = validator.escape(String(userData['cover:position'] || '50% 50%')); + userData['username:disableEdit'] = !userData.isAdmin && meta.config['username:disableEdit']; + userData['email:disableEdit'] = !userData.isAdmin && meta.config['email:disableEdit']; + + await getCounts(userData, callerUID); + + const hookData = await plugins.hooks.fire('filter:helpers.getUserDataByUserSlug', { + userData: userData, + callerUID: callerUID, + query: query, + }); + return hookData.userData; +}; + +function escape(value) { + return translator.escape(validator.escape(String(value || ''))); +} + +async function getAllData(uid, callerUID) { + return await utils.promiseParallel({ + userData: user.getUserData(uid), + isTargetAdmin: user.isAdministrator(uid), + userSettings: user.getSettings(uid), + isAdmin: user.isAdministrator(callerUID), + isGlobalModerator: user.isGlobalModerator(callerUID), + isModerator: user.isModeratorOfAnyCategory(callerUID), + isFollowing: user.isFollowing(callerUID, uid), + ips: user.getIPs(uid, 4), + profile_menu: getProfileMenu(uid, callerUID), + groups: groups.getUserGroups([uid]), + sso: plugins.hooks.fire('filter:auth.list', { uid: uid, associations: [] }), + canEdit: privileges.users.canEdit(callerUID, uid), + canBanUser: privileges.users.canBanUser(callerUID, uid), + canMuteUser: privileges.users.canMuteUser(callerUID, uid), + isBlocked: user.blocks.is(uid, callerUID), + canViewInfo: privileges.global.can('view:users:info', callerUID), + hasPrivateChat: messaging.hasPrivateChat(callerUID, uid), + }); +} + +async function getCounts(userData, callerUID) { + const { uid } = userData; + const cids = await categories.getCidsByPrivilege('categories:cid', callerUID, 'topics:read'); + const promises = { + posts: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:pids`)), + best: Promise.all(cids.map(async c => db.sortedSetCount(`cid:${c}:uid:${uid}:pids:votes`, 1, '+inf'))), + controversial: Promise.all(cids.map(async c => db.sortedSetCount(`cid:${c}:uid:${uid}:pids:votes`, '-inf', -1))), + topics: db.sortedSetsCardSum(cids.map(c => `cid:${c}:uid:${uid}:tids`)), + }; + if (userData.isAdmin || userData.isSelf) { + promises.ignored = db.sortedSetCard(`uid:${uid}:ignored_tids`); + promises.watched = db.sortedSetCard(`uid:${uid}:followed_tids`); + promises.upvoted = db.sortedSetCard(`uid:${uid}:upvote`); + promises.downvoted = db.sortedSetCard(`uid:${uid}:downvote`); + promises.bookmarks = db.sortedSetCard(`uid:${uid}:bookmarks`); + promises.uploaded = db.sortedSetCard(`uid:${uid}:uploads`); + promises.categoriesWatched = user.getWatchedCategories(uid); + promises.blocks = user.getUserField(userData.uid, 'blocksCount'); + } + const counts = await utils.promiseParallel(promises); + counts.best = counts.best.reduce((sum, count) => sum + count, 0); + counts.controversial = counts.controversial.reduce((sum, count) => sum + count, 0); + counts.categoriesWatched = counts.categoriesWatched && counts.categoriesWatched.length; + counts.groups = userData.groups.length; + counts.following = userData.followingCount; + counts.followers = userData.followerCount; + userData.blocksCount = counts.blocks || 0; // for backwards compatibility, remove in 1.16.0 + userData.counts = counts; +} + +async function getProfileMenu(uid, callerUID) { + const links = [{ + id: 'info', + route: 'info', + name: '[[user:account_info]]', + icon: 'fa-info', + visibility: { + self: false, + other: false, + moderator: false, + globalMod: false, + admin: true, + canViewInfo: true, + }, + }, { + id: 'sessions', + route: 'sessions', + name: '[[pages:account/sessions]]', + icon: 'fa-group', + visibility: { + self: true, + other: false, + moderator: false, + globalMod: false, + admin: false, + canViewInfo: false, + }, + }]; + + if (meta.config.gdpr_enabled) { + links.push({ + id: 'consent', + route: 'consent', + name: '[[user:consent.title]]', + icon: 'fa-thumbs-o-up', + visibility: { + self: true, + other: false, + moderator: false, + globalMod: false, + admin: false, + canViewInfo: false, + }, + }); + } + + return await plugins.hooks.fire('filter:user.profileMenu', { + uid: uid, + callerUID: callerUID, + links: links, + }); +} + +async function parseAboutMe(userData) { + if (!userData.aboutme) { + userData.aboutme = ''; + userData.aboutmeParsed = ''; + return; + } + userData.aboutme = validator.escape(String(userData.aboutme || '')); + const parsed = await plugins.hooks.fire('filter:parse.aboutme', userData.aboutme); + userData.aboutme = translator.escape(userData.aboutme); + userData.aboutmeParsed = translator.escape(parsed); +} + +function filterLinks(links, states) { + return links.filter((link, index) => { + // Default visibility + link.visibility = { + self: true, + other: true, + moderator: true, + globalMod: true, + admin: true, + canViewInfo: true, + ...link.visibility, + }; + + const permit = Object.keys(states).some(state => states[state] && link.visibility[state]); + + links[index].public = permit; + return permit; + }); +} + +require('../../promisify')(helpers); diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js new file mode 100644 index 0000000000..295425535d --- /dev/null +++ b/src/controllers/accounts/info.js @@ -0,0 +1,54 @@ +'use strict'; + +const db = require('../../database'); +const user = require('../../user'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); +const pagination = require('../../pagination'); + +const infoController = module.exports; + +infoController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + const page = Math.max(1, req.query.page || 1); + const itemsPerPage = 10; + const start = (page - 1) * itemsPerPage; + const stop = start + itemsPerPage - 1; + + const [history, sessions, usernames, emails, notes] = await Promise.all([ + user.getModerationHistory(userData.uid), + user.auth.getSessions(userData.uid, req.sessionID), + user.getHistory(`user:${userData.uid}:usernames`), + user.getHistory(`user:${userData.uid}:emails`), + getNotes(userData, start, stop), + ]); + + userData.history = history; + userData.sessions = sessions; + userData.usernames = usernames; + userData.emails = emails; + + if (userData.isAdminOrGlobalModeratorOrModerator) { + userData.moderationNotes = notes.notes; + const pageCount = Math.ceil(notes.count / itemsPerPage); + userData.pagination = pagination.create(page, pageCount, req.query); + } + userData.title = '[[pages:account/info]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[user:account_info]]' }]); + + res.render('account/info', userData); +}; + +async function getNotes(userData, start, stop) { + if (!userData.isAdminOrGlobalModeratorOrModerator) { + return; + } + const [notes, count] = await Promise.all([ + user.getModerationNotes(userData.uid, start, stop), + db.sortedSetCard(`uid:${userData.uid}:moderation:notes`), + ]); + return { notes: notes, count: count }; +} diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js new file mode 100644 index 0000000000..02ca307d37 --- /dev/null +++ b/src/controllers/accounts/notifications.js @@ -0,0 +1,72 @@ +'use strict'; + +const user = require('../../user'); +const helpers = require('../helpers'); +const plugins = require('../../plugins'); +const pagination = require('../../pagination'); + +const notificationsController = module.exports; + +notificationsController.get = async function (req, res, next) { + const regularFilters = [ + { name: '[[notifications:all]]', filter: '' }, + { name: '[[global:topics]]', filter: 'new-topic' }, + { name: '[[notifications:replies]]', filter: 'new-reply' }, + { name: '[[notifications:chat]]', filter: 'new-chat' }, + { name: '[[notifications:group-chat]]', filter: 'new-group-chat' }, + { name: '[[notifications:follows]]', filter: 'follow' }, + { name: '[[notifications:upvote]]', filter: 'upvote' }, + ]; + + const moderatorFilters = [ + { name: '[[notifications:new-flags]]', filter: 'new-post-flag' }, + { name: '[[notifications:my-flags]]', filter: 'my-flags' }, + { name: '[[notifications:bans]]', filter: 'ban' }, + ]; + + const filter = req.query.filter || ''; + const page = Math.max(1, req.query.page || 1); + const itemsPerPage = 20; + const start = (page - 1) * itemsPerPage; + const stop = start + itemsPerPage - 1; + + const [filters, isPrivileged] = await Promise.all([ + plugins.hooks.fire('filter:notifications.addFilters', { + regularFilters: regularFilters, + moderatorFilters: moderatorFilters, + uid: req.uid, + }), + user.isPrivileged(req.uid), + ]); + + let allFilters = filters.regularFilters; + if (isPrivileged) { + allFilters = allFilters.concat([ + { separator: true }, + ]).concat(filters.moderatorFilters); + } + const selectedFilter = allFilters.find((filterData) => { + filterData.selected = filterData.filter === filter; + return filterData.selected; + }); + if (!selectedFilter) { + return next(); + } + + const nids = await user.notifications.getAll(req.uid, selectedFilter.filter); + let notifications = await user.notifications.getNotifications(nids, req.uid); + + const pageCount = Math.max(1, Math.ceil(notifications.length / itemsPerPage)); + notifications = notifications.slice(start, stop + 1); + + res.render('notifications', { + notifications: notifications, + pagination: pagination.create(page, pageCount, req.query), + filters: allFilters, + regularFilters: regularFilters, + moderatorFilters: moderatorFilters, + selectedFilter: selectedFilter, + title: '[[pages:notifications]]', + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:notifications]]' }]), + }); +}; diff --git a/src/controllers/accounts/posts.js b/src/controllers/accounts/posts.js new file mode 100644 index 0000000000..27ab282a0b --- /dev/null +++ b/src/controllers/accounts/posts.js @@ -0,0 +1,254 @@ +'use strict'; + +const db = require('../../database'); +const user = require('../../user'); +const posts = require('../../posts'); +const topics = require('../../topics'); +const categories = require('../../categories'); +const privileges = require('../../privileges'); +const pagination = require('../../pagination'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); +const plugins = require('../../plugins'); +const utils = require('../../utils'); + +const postsController = module.exports; + +const templateToData = { + 'account/bookmarks': { + type: 'posts', + noItemsFoundKey: '[[topic:bookmarks.has_no_bookmarks]]', + crumb: '[[user:bookmarks]]', + getSets: function (callerUid, userData) { + return `uid:${userData.uid}:bookmarks`; + }, + }, + 'account/posts': { + type: 'posts', + noItemsFoundKey: '[[user:has_no_posts]]', + crumb: '[[global:posts]]', + getSets: async function (callerUid, userData) { + const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + return cids.map(c => `cid:${c}:uid:${userData.uid}:pids`); + }, + }, + 'account/upvoted': { + type: 'posts', + noItemsFoundKey: '[[user:has_no_upvoted_posts]]', + crumb: '[[global:upvoted]]', + getSets: function (callerUid, userData) { + return `uid:${userData.uid}:upvote`; + }, + }, + 'account/downvoted': { + type: 'posts', + noItemsFoundKey: '[[user:has_no_downvoted_posts]]', + crumb: '[[global:downvoted]]', + getSets: function (callerUid, userData) { + return `uid:${userData.uid}:downvote`; + }, + }, + 'account/best': { + type: 'posts', + noItemsFoundKey: '[[user:has_no_best_posts]]', + crumb: '[[global:best]]', + getSets: async function (callerUid, userData) { + const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + return cids.map(c => `cid:${c}:uid:${userData.uid}:pids:votes`); + }, + getTopics: async (sets, req, start, stop) => { + let pids = await db.getSortedSetRevRangeByScore(sets, start, stop - start + 1, '+inf', 1); + pids = await privileges.posts.filter('topics:read', pids, req.uid); + const postObjs = await posts.getPostSummaryByPids(pids, req.uid, { stripTags: false }); + return { posts: postObjs, nextStart: stop + 1 }; + }, + getItemCount: async (sets) => { + const counts = await Promise.all(sets.map(set => db.sortedSetCount(set, 1, '+inf'))); + return counts.reduce((acc, val) => acc + val, 0); + }, + }, + 'account/controversial': { + type: 'posts', + noItemsFoundKey: '[[user:has_no_controversial_posts]]', + crumb: '[[global:controversial]]', + getSets: async function (callerUid, userData) { + const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + return cids.map(c => `cid:${c}:uid:${userData.uid}:pids:votes`); + }, + getTopics: async (sets, req, start, stop) => { + let pids = await db.getSortedSetRangeByScore(sets, start, stop - start + 1, '-inf', -1); + pids = await privileges.posts.filter('topics:read', pids, req.uid); + const postObjs = await posts.getPostSummaryByPids(pids, req.uid, { stripTags: false }); + return { posts: postObjs, nextStart: stop + 1 }; + }, + getItemCount: async (sets) => { + const counts = await Promise.all(sets.map(set => db.sortedSetCount(set, '-inf', -1))); + return counts.reduce((acc, val) => acc + val, 0); + }, + }, + 'account/watched': { + type: 'topics', + noItemsFoundKey: '[[user:has_no_watched_topics]]', + crumb: '[[user:watched]]', + getSets: function (callerUid, userData) { + return `uid:${userData.uid}:followed_tids`; + }, + getTopics: async function (set, req, start, stop) { + const { sort } = req.query; + const map = { + votes: 'topics:votes', + posts: 'topics:posts', + views: 'topics:views', + lastpost: 'topics:recent', + firstpost: 'topics:tid', + }; + + if (!sort || !map[sort]) { + return await topics.getTopicsFromSet(set, req.uid, start, stop); + } + const sortSet = map[sort]; + let tids = await db.getSortedSetRevRange(set, 0, -1); + const scores = await db.sortedSetScores(sortSet, tids); + tids = tids.map((tid, i) => ({ tid: tid, score: scores[i] })) + .sort((a, b) => b.score - a.score) + .slice(start, stop + 1) + .map(t => t.tid); + + const topicsData = await topics.getTopics(tids, req.uid); + topics.calculateTopicIndices(topicsData, start); + return { topics: topicsData, nextStart: stop + 1 }; + }, + }, + 'account/ignored': { + type: 'topics', + noItemsFoundKey: '[[user:has_no_ignored_topics]]', + crumb: '[[user:ignored]]', + getSets: function (callerUid, userData) { + return `uid:${userData.uid}:ignored_tids`; + }, + }, + 'account/topics': { + type: 'topics', + noItemsFoundKey: '[[user:has_no_topics]]', + crumb: '[[global:topics]]', + getSets: async function (callerUid, userData) { + const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + return cids.map(c => `cid:${c}:uid:${userData.uid}:tids`); + }, + }, +}; + +postsController.getBookmarks = async function (req, res, next) { + await getPostsFromUserSet('account/bookmarks', req, res, next); +}; + +postsController.getPosts = async function (req, res, next) { + await getPostsFromUserSet('account/posts', req, res, next); +}; + +postsController.getUpVotedPosts = async function (req, res, next) { + await getPostsFromUserSet('account/upvoted', req, res, next); +}; + +postsController.getDownVotedPosts = async function (req, res, next) { + await getPostsFromUserSet('account/downvoted', req, res, next); +}; + +postsController.getBestPosts = async function (req, res, next) { + await getPostsFromUserSet('account/best', req, res, next); +}; + +postsController.getControversialPosts = async function (req, res, next) { + await getPostsFromUserSet('account/controversial', req, res, next); +}; + +postsController.getWatchedTopics = async function (req, res, next) { + await getPostsFromUserSet('account/watched', req, res, next); +}; + +postsController.getIgnoredTopics = async function (req, res, next) { + await getPostsFromUserSet('account/ignored', req, res, next); +}; + +postsController.getTopics = async function (req, res, next) { + await getPostsFromUserSet('account/topics', req, res, next); +}; + +async function getPostsFromUserSet(template, req, res, next) { + const data = templateToData[template]; + const page = Math.max(1, parseInt(req.query.page, 10) || 1); + + const [userData, settings] = await Promise.all([ + accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query), + user.getSettings(req.uid), + ]); + + if (!userData) { + return next(); + } + const itemsPerPage = data.type === 'topics' ? settings.topicsPerPage : settings.postsPerPage; + const start = (page - 1) * itemsPerPage; + const stop = start + itemsPerPage - 1; + const sets = await data.getSets(req.uid, userData); + let result; + if (plugins.hooks.hasListeners('filter:account.getPostsFromUserSet')) { + result = await plugins.hooks.fire('filter:account.getPostsFromUserSet', { + req: req, + template: template, + userData: userData, + settings: settings, + data: data, + start: start, + stop: stop, + itemCount: 0, + itemData: [], + }); + } else { + result = await utils.promiseParallel({ + itemCount: getItemCount(sets, data, settings), + itemData: getItemData(sets, data, req, start, stop), + }); + } + const { itemCount, itemData } = result; + userData[data.type] = itemData[data.type]; + userData.nextStart = itemData.nextStart; + + const pageCount = Math.ceil(itemCount / itemsPerPage); + userData.pagination = pagination.create(page, pageCount, req.query); + + userData.noItemsFoundKey = data.noItemsFoundKey; + userData.title = `[[pages:${template}, ${userData.username}]]`; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: data.crumb }]); + userData.showSort = template === 'account/watched'; + const baseUrl = (req.baseUrl + req.path.replace(/^\/api/, '')); + userData.sortOptions = [ + { url: `${baseUrl}?sort=votes`, name: '[[global:votes]]' }, + { url: `${baseUrl}?sort=posts`, name: '[[global:posts]]' }, + { url: `${baseUrl}?sort=views`, name: '[[global:views]]' }, + { url: `${baseUrl}?sort=lastpost`, name: '[[global:lastpost]]' }, + { url: `${baseUrl}?sort=firstpost`, name: '[[global:firstpost]]' }, + ]; + userData.sortOptions.forEach((option) => { + option.selected = option.url.includes(`sort=${req.query.sort}`); + }); + + res.render(template, userData); +} + +async function getItemData(sets, data, req, start, stop) { + if (data.getTopics) { + return await data.getTopics(sets, req, start, stop); + } + const method = data.type === 'topics' ? topics.getTopicsFromSet : posts.getPostSummariesFromSet; + return await method(sets, req.uid, start, stop); +} + +async function getItemCount(sets, data, settings) { + if (!settings.usePagination) { + return 0; + } + if (data.getItemCount) { + return await data.getItemCount(sets); + } + return await db.sortedSetsCardSum(sets); +} diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js new file mode 100644 index 0000000000..8a3ab2dc67 --- /dev/null +++ b/src/controllers/accounts/profile.js @@ -0,0 +1,169 @@ +'use strict'; + +const nconf = require('nconf'); +const _ = require('lodash'); + +const db = require('../../database'); +const user = require('../../user'); +const posts = require('../../posts'); +const categories = require('../../categories'); +const plugins = require('../../plugins'); +const meta = require('../../meta'); +const privileges = require('../../privileges'); +const accountHelpers = require('./helpers'); +const helpers = require('../helpers'); +const utils = require('../../utils'); + +const profileController = module.exports; + +profileController.get = async function (req, res, next) { + const lowercaseSlug = req.params.userslug.toLowerCase(); + + if (req.params.userslug !== lowercaseSlug) { + if (res.locals.isAPI) { + req.params.userslug = lowercaseSlug; + } else { + return res.redirect(`${nconf.get('relative_path')}/user/${lowercaseSlug}`); + } + } + + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + + await incrementProfileViews(req, userData); + + const [latestPosts, bestPosts] = await Promise.all([ + getLatestPosts(req.uid, userData), + getBestPosts(req.uid, userData), + posts.parseSignature(userData, req.uid), + ]); + + if (meta.config['reputation:disabled']) { + delete userData.reputation; + } + + userData.posts = latestPosts; // for backwards compat. + userData.latestPosts = latestPosts; + userData.bestPosts = bestPosts; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username }]); + userData.title = userData.username; + userData.allowCoverPicture = !userData.isSelf || !!meta.config['reputation:disabled'] || userData.reputation >= meta.config['min:rep:cover-picture']; + + // Show email changed modal on first access after said change + userData.emailChanged = req.session.emailChanged; + delete req.session.emailChanged; + + if (!userData.profileviews) { + userData.profileviews = 1; + } + + addMetaTags(res, userData); + + userData.selectedGroup = userData.groups.filter(group => group && userData.groupTitleArray.includes(group.name)) + .sort((a, b) => userData.groupTitleArray.indexOf(a.name) - userData.groupTitleArray.indexOf(b.name)); + + res.render('account/profile', userData); +}; + +async function incrementProfileViews(req, userData) { + if (req.uid >= 1) { + req.session.uids_viewed = req.session.uids_viewed || {}; + + if ( + req.uid !== userData.uid && + (!req.session.uids_viewed[userData.uid] || req.session.uids_viewed[userData.uid] < Date.now() - 3600000) + ) { + await user.incrementUserFieldBy(userData.uid, 'profileviews', 1); + req.session.uids_viewed[userData.uid] = Date.now(); + } + } +} + +async function getLatestPosts(callerUid, userData) { + return await getPosts(callerUid, userData, 'pids'); +} + +async function getBestPosts(callerUid, userData) { + return await getPosts(callerUid, userData, 'pids:votes'); +} + +async function getPosts(callerUid, userData, setSuffix) { + const cids = await categories.getCidsByPrivilege('categories:cid', callerUid, 'topics:read'); + const keys = cids.map(c => `cid:${c}:uid:${userData.uid}:${setSuffix}`); + let hasMorePosts = true; + let start = 0; + const count = 10; + const postData = []; + + const [isAdmin, isModOfCids, canSchedule] = await Promise.all([ + user.isAdministrator(callerUid), + user.isModerator(callerUid, cids), + privileges.categories.isUserAllowedTo('topics:schedule', cids, callerUid), + ]); + const cidToIsMod = _.zipObject(cids, isModOfCids); + const cidToCanSchedule = _.zipObject(cids, canSchedule); + + do { + /* eslint-disable no-await-in-loop */ + let pids = await db.getSortedSetRevRange(keys, start, start + count - 1); + if (!pids.length || pids.length < count) { + hasMorePosts = false; + } + if (pids.length) { + ({ pids } = await plugins.hooks.fire('filter:account.profile.getPids', { + uid: callerUid, + userData, + setSuffix, + pids, + })); + const p = await posts.getPostSummaryByPids(pids, callerUid, { stripTags: false }); + postData.push(...p.filter( + p => p && p.topic && (isAdmin || cidToIsMod[p.topic.cid] || + (p.topic.scheduled && cidToCanSchedule[p.topic.cid]) || (!p.deleted && !p.topic.deleted)) + )); + } + start += count; + } while (postData.length < count && hasMorePosts); + return postData.slice(0, count); +} + +function addMetaTags(res, userData) { + const plainAboutMe = userData.aboutme ? utils.stripHTMLTags(utils.decodeHTMLEntities(userData.aboutme)) : ''; + res.locals.metaTags = [ + { + name: 'title', + content: userData.fullname || userData.username, + noEscape: true, + }, + { + name: 'description', + content: plainAboutMe, + }, + { + property: 'og:title', + content: userData.fullname || userData.username, + noEscape: true, + }, + { + property: 'og:description', + content: plainAboutMe, + }, + ]; + + if (userData.picture) { + res.locals.metaTags.push( + { + property: 'og:image', + content: userData.picture, + noEscape: true, + }, + { + property: 'og:image:url', + content: userData.picture, + noEscape: true, + } + ); + } +} diff --git a/src/controllers/accounts/sessions.js b/src/controllers/accounts/sessions.js new file mode 100644 index 0000000000..88094f5426 --- /dev/null +++ b/src/controllers/accounts/sessions.js @@ -0,0 +1,20 @@ +'use strict'; + +const user = require('../../user'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); + +const sessionController = module.exports; + +sessionController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + + userData.sessions = await user.auth.getSessions(userData.uid, req.sessionID); + userData.title = '[[pages:account/sessions]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[pages:account/sessions]]' }]); + + res.render('account/sessions', userData); +}; diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js new file mode 100644 index 0000000000..4d2e4cf3b4 --- /dev/null +++ b/src/controllers/accounts/settings.js @@ -0,0 +1,243 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); +const _ = require('lodash'); +const jwt = require('jsonwebtoken'); +const util = require('util'); + +const user = require('../../user'); +const languages = require('../../languages'); +const meta = require('../../meta'); +const plugins = require('../../plugins'); +const notifications = require('../../notifications'); +const db = require('../../database'); +const helpers = require('../helpers'); +const accountHelpers = require('./helpers'); + +const settingsController = module.exports; + +settingsController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + const [settings, languagesData] = await Promise.all([ + user.getSettings(userData.uid), + languages.list(), + ]); + + userData.settings = settings; + userData.languages = languagesData; + if (userData.isAdmin && userData.isSelf) { + userData.acpLanguages = _.cloneDeep(languagesData); + } + + const data = await plugins.hooks.fire('filter:user.customSettings', { + settings: settings, + customSettings: [], + uid: req.uid, + }); + + const [notificationSettings, routes] = await Promise.all([ + getNotificationSettings(userData), + getHomePageRoutes(userData), + ]); + + userData.customSettings = data.customSettings; + userData.homePageRoutes = routes; + userData.notificationSettings = notificationSettings; + userData.disableEmailSubscriptions = meta.config.disableEmailSubscriptions; + + userData.dailyDigestFreqOptions = [ + { value: 'off', name: '[[user:digest_off]]', selected: userData.settings.dailyDigestFreq === 'off' }, + { value: 'day', name: '[[user:digest_daily]]', selected: userData.settings.dailyDigestFreq === 'day' }, + { value: 'week', name: '[[user:digest_weekly]]', selected: userData.settings.dailyDigestFreq === 'week' }, + { value: 'biweek', name: '[[user:digest_biweekly]]', selected: userData.settings.dailyDigestFreq === 'biweek' }, + { value: 'month', name: '[[user:digest_monthly]]', selected: userData.settings.dailyDigestFreq === 'month' }, + ]; + + userData.bootswatchSkinOptions = [ + { name: 'Default', value: '' }, + { name: 'Cerulean', value: 'cerulean' }, + { name: 'Cosmo', value: 'cosmo' }, + { name: 'Cyborg', value: 'cyborg' }, + { name: 'Darkly', value: 'darkly' }, + { name: 'Flatly', value: 'flatly' }, + { name: 'Journal', value: 'journal' }, + { name: 'Lumen', value: 'lumen' }, + { name: 'Paper', value: 'paper' }, + { name: 'Readable', value: 'readable' }, + { name: 'Sandstone', value: 'sandstone' }, + { name: 'Simplex', value: 'simplex' }, + { name: 'Slate', value: 'slate' }, + { name: 'Spacelab', value: 'spacelab' }, + { name: 'Superhero', value: 'superhero' }, + { name: 'United', value: 'united' }, + { name: 'Yeti', value: 'yeti' }, + ]; + + userData.bootswatchSkinOptions.forEach((skin) => { + skin.selected = skin.value === userData.settings.bootswatchSkin; + }); + + userData.languages.forEach((language) => { + language.selected = language.code === userData.settings.userLang; + }); + + if (userData.isAdmin && userData.isSelf) { + userData.acpLanguages.forEach((language) => { + language.selected = language.code === userData.settings.acpLang; + }); + } + + const notifFreqOptions = [ + 'all', + 'first', + 'everyTen', + 'threshold', + 'logarithmic', + 'disabled', + ]; + + userData.upvoteNotifFreq = notifFreqOptions.map( + name => ({ name: name, selected: name === userData.settings.upvoteNotifFreq }) + ); + + userData.categoryWatchState = { [userData.settings.categoryWatchState]: true }; + + userData.disableCustomUserSkins = meta.config.disableCustomUserSkins || 0; + + userData.allowUserHomePage = meta.config.allowUserHomePage === 1 ? 1 : 0; + + userData.hideFullname = meta.config.hideFullname || 0; + userData.hideEmail = meta.config.hideEmail || 0; + + userData.inTopicSearchAvailable = plugins.hooks.hasListeners('filter:topic.search'); + + userData.maxTopicsPerPage = meta.config.maxTopicsPerPage; + userData.maxPostsPerPage = meta.config.maxPostsPerPage; + + userData.title = '[[pages:account/settings]]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[user:settings]]' }]); + + res.render('account/settings', userData); +}; + +const unsubscribable = ['digest', 'notification']; +const jwtVerifyAsync = util.promisify((token, callback) => { + jwt.verify(token, nconf.get('secret'), (err, payload) => callback(err, payload)); +}); +const doUnsubscribe = async (payload) => { + if (payload.template === 'digest') { + await Promise.all([ + user.setSetting(payload.uid, 'dailyDigestFreq', 'off'), + user.updateDigestSetting(payload.uid, 'off'), + ]); + } else if (payload.template === 'notification') { + const current = await db.getObjectField(`user:${payload.uid}:settings`, `notificationType_${payload.type}`); + await user.setSetting(payload.uid, `notificationType_${payload.type}`, (current === 'notificationemail' ? 'notification' : 'none')); + } + return true; +}; + +settingsController.unsubscribe = async (req, res) => { + try { + const payload = await jwtVerifyAsync(req.params.token); + if (!payload || !unsubscribable.includes(payload.template)) { + return; + } + await doUnsubscribe(payload); + res.render('unsubscribe', { + payload, + }); + } catch (err) { + res.render('unsubscribe', { + error: err.message, + }); + } +}; + +settingsController.unsubscribePost = async function (req, res) { + let payload; + try { + payload = await jwtVerifyAsync(req.params.token); + if (!payload || !unsubscribable.includes(payload.template)) { + return res.sendStatus(404); + } + } catch (err) { + return res.sendStatus(403); + } + try { + await doUnsubscribe(payload); + res.sendStatus(200); + } catch (err) { + winston.error(`[settings/unsubscribe] One-click unsubscribe failed with error: ${err.message}`); + res.sendStatus(500); + } +}; + +async function getNotificationSettings(userData) { + const privilegedTypes = []; + + const privileges = await user.getPrivileges(userData.uid); + if (privileges.isAdmin) { + privilegedTypes.push('notificationType_new-register'); + } + if (privileges.isAdmin || privileges.isGlobalMod || privileges.isModeratorOfAnyCategory) { + privilegedTypes.push('notificationType_post-queue', 'notificationType_new-post-flag'); + } + if (privileges.isAdmin || privileges.isGlobalMod) { + privilegedTypes.push('notificationType_new-user-flag'); + } + const results = await plugins.hooks.fire('filter:user.notificationTypes', { + types: notifications.baseTypes.slice(), + privilegedTypes: privilegedTypes, + }); + + function modifyType(type) { + const setting = userData.settings[type]; + return { + name: type, + label: `[[notifications:${type}]]`, + none: setting === 'none', + notification: setting === 'notification', + email: setting === 'email', + notificationemail: setting === 'notificationemail', + }; + } + + if (meta.config.disableChat) { + results.types = results.types.filter(type => type !== 'notificationType_new-chat'); + } + + return results.types.map(modifyType).concat(results.privilegedTypes.map(modifyType)); +} + +async function getHomePageRoutes(userData) { + let routes = await helpers.getHomePageRoutes(userData.uid); + + // Set selected for each route + let customIdx; + let hasSelected = false; + routes = routes.map((route, idx) => { + if (route.route === userData.settings.homePageRoute) { + route.selected = true; + hasSelected = true; + } else { + route.selected = false; + } + + if (route.route === 'custom') { + customIdx = idx; + } + + return route; + }); + + if (!hasSelected && customIdx && userData.settings.homePageRoute !== 'none') { + routes[customIdx].selected = true; + } + + return routes; +} diff --git a/src/controllers/accounts/uploads.js b/src/controllers/accounts/uploads.js new file mode 100644 index 0000000000..a5b2917fe1 --- /dev/null +++ b/src/controllers/accounts/uploads.js @@ -0,0 +1,40 @@ +'use strict'; + +const path = require('path'); + +const nconf = require('nconf'); + +const db = require('../../database'); +const helpers = require('../helpers'); +const meta = require('../../meta'); +const pagination = require('../../pagination'); +const accountHelpers = require('./helpers'); + +const uploadsController = module.exports; + +uploadsController.get = async function (req, res, next) { + const userData = await accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, req.query); + if (!userData) { + return next(); + } + + const page = Math.max(1, parseInt(req.query.page, 10) || 1); + const itemsPerPage = 25; + const start = (page - 1) * itemsPerPage; + const stop = start + itemsPerPage - 1; + const [itemCount, uploadNames] = await Promise.all([ + db.sortedSetCard(`uid:${userData.uid}:uploads`), + db.getSortedSetRevRange(`uid:${userData.uid}:uploads`, start, stop), + ]); + + userData.uploads = uploadNames.map(uploadName => ({ + name: uploadName, + url: path.resolve(nconf.get('upload_url'), uploadName), + })); + const pageCount = Math.ceil(itemCount / itemsPerPage); + userData.pagination = pagination.create(page, pageCount, req.query); + userData.privateUploads = meta.config.privateUploads === 1; + userData.title = `[[pages:account/uploads, ${userData.username}]]`; + userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[global:uploads]]' }]); + res.render('account/uploads', userData); +}; diff --git a/src/controllers/admin.js b/src/controllers/admin.js new file mode 100644 index 0000000000..0fa19665e4 --- /dev/null +++ b/src/controllers/admin.js @@ -0,0 +1,58 @@ +'use strict'; + +const privileges = require('../privileges'); +const helpers = require('./helpers'); + +const adminController = { + dashboard: require('./admin/dashboard'), + categories: require('./admin/categories'), + privileges: require('./admin/privileges'), + adminsMods: require('./admin/admins-mods'), + tags: require('./admin/tags'), + groups: require('./admin/groups'), + digest: require('./admin/digest'), + appearance: require('./admin/appearance'), + extend: { + widgets: require('./admin/widgets'), + rewards: require('./admin/rewards'), + }, + events: require('./admin/events'), + hooks: require('./admin/hooks'), + logs: require('./admin/logs'), + errors: require('./admin/errors'), + database: require('./admin/database'), + cache: require('./admin/cache'), + plugins: require('./admin/plugins'), + settings: require('./admin/settings'), + logger: require('./admin/logger'), + themes: require('./admin/themes'), + users: require('./admin/users'), + uploads: require('./admin/uploads'), + info: require('./admin/info'), +}; + +adminController.routeIndex = async (req, res) => { + const privilegeSet = await privileges.admin.get(req.uid); + + if (privilegeSet.superadmin || privilegeSet['admin:dashboard']) { + return adminController.dashboard.get(req, res); + } else if (privilegeSet['admin:categories']) { + return helpers.redirect(res, 'admin/manage/categories'); + } else if (privilegeSet['admin:privileges']) { + return helpers.redirect(res, 'admin/manage/privileges'); + } else if (privilegeSet['admin:users']) { + return helpers.redirect(res, 'admin/manage/users'); + } else if (privilegeSet['admin:groups']) { + return helpers.redirect(res, 'admin/manage/groups'); + } else if (privilegeSet['admin:admins-mods']) { + return helpers.redirect(res, 'admin/manage/admins-mods'); + } else if (privilegeSet['admin:tags']) { + return helpers.redirect(res, 'admin/manage/tags'); + } else if (privilegeSet['admin:settings']) { + return helpers.redirect(res, 'admin/settings/general'); + } + + return helpers.notAllowed(req, res); +}; + +module.exports = adminController; diff --git a/src/controllers/admin/admins-mods.js b/src/controllers/admin/admins-mods.js new file mode 100644 index 0000000000..62b9142052 --- /dev/null +++ b/src/controllers/admin/admins-mods.js @@ -0,0 +1,61 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../../database'); +const groups = require('../../groups'); +const categories = require('../../categories'); +const user = require('../../user'); +const meta = require('../../meta'); +const pagination = require('../../pagination'); +const categoriesController = require('./categories'); + +const AdminsMods = module.exports; + +AdminsMods.get = async function (req, res) { + const rootCid = parseInt(req.query.cid, 10) || 0; + + const cidsCount = await db.sortedSetCard(`cid:${rootCid}:children`); + + const pageCount = Math.max(1, Math.ceil(cidsCount / meta.config.categoriesPerPage)); + const page = Math.min(parseInt(req.query.page, 10) || 1, pageCount); + const start = Math.max(0, (page - 1) * meta.config.categoriesPerPage); + const stop = start + meta.config.categoriesPerPage - 1; + + const cids = await db.getSortedSetRange(`cid:${rootCid}:children`, start, stop); + + const selectedCategory = rootCid ? await categories.getCategoryData(rootCid) : null; + const pageCategories = await categories.getCategoriesData(cids); + + const [admins, globalMods, moderators, crumbs] = await Promise.all([ + groups.get('administrators', { uid: req.uid }), + groups.get('Global Moderators', { uid: req.uid }), + getModeratorsOfCategories(pageCategories), + categoriesController.buildBreadCrumbs(selectedCategory, '/admin/manage/admins-mods'), + ]); + + res.render('admin/manage/admins-mods', { + admins: admins, + globalMods: globalMods, + categoryMods: moderators, + selectedCategory: selectedCategory, + pagination: pagination.create(page, pageCount, req.query), + breadcrumbs: crumbs, + }); +}; + +async function getModeratorsOfCategories(categoryData) { + const [moderatorUids, childrenCounts] = await Promise.all([ + categories.getModeratorUids(categoryData.map(c => c.cid)), + db.sortedSetsCard(categoryData.map(c => `cid:${c.cid}:children`)), + ]); + + const uids = _.uniq(_.flatten(moderatorUids)); + const moderatorData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']); + const moderatorMap = _.zipObject(uids, moderatorData); + categoryData.forEach((c, index) => { + c.moderators = moderatorUids[index].map(uid => moderatorMap[uid]); + c.subCategoryCount = childrenCounts[index]; + }); + return categoryData; +} diff --git a/src/controllers/admin/appearance.js b/src/controllers/admin/appearance.js new file mode 100644 index 0000000000..d77dc75e2d --- /dev/null +++ b/src/controllers/admin/appearance.js @@ -0,0 +1,9 @@ +'use strict'; + +const appearanceController = module.exports; + +appearanceController.get = function (req, res) { + const term = req.params.term ? req.params.term : 'themes'; + + res.render(`admin/appearance/${term}`, {}); +}; diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js new file mode 100644 index 0000000000..6f5775aae5 --- /dev/null +++ b/src/controllers/admin/cache.js @@ -0,0 +1,67 @@ +'use strict'; + +const cacheController = module.exports; + +const utils = require('../../utils'); +const plugins = require('../../plugins'); + +cacheController.get = async function (req, res) { + const postCache = require('../../posts/cache'); + const groupCache = require('../../groups').cache; + const { objectCache } = require('../../database'); + const localCache = require('../../cache'); + + function getInfo(cache) { + return { + length: cache.length, + max: cache.max, + maxSize: cache.maxSize, + itemCount: cache.itemCount, + percentFull: cache.name === 'post' ? + ((cache.length / cache.maxSize) * 100).toFixed(2) : + ((cache.itemCount / cache.max) * 100).toFixed(2), + hits: utils.addCommas(String(cache.hits)), + misses: utils.addCommas(String(cache.misses)), + hitRatio: ((cache.hits / (cache.hits + cache.misses) || 0)).toFixed(4), + enabled: cache.enabled, + ttl: cache.ttl, + }; + } + let caches = { + post: postCache, + group: groupCache, + local: localCache, + }; + if (objectCache) { + caches.object = objectCache; + } + caches = await plugins.hooks.fire('filter:admin.cache.get', caches); + for (const [key, value] of Object.entries(caches)) { + caches[key] = getInfo(value); + } + + res.render('admin/advanced/cache', { caches }); +}; + +cacheController.dump = async function (req, res, next) { + let caches = { + post: require('../../posts/cache'), + object: require('../../database').objectCache, + group: require('../../groups').cache, + local: require('../../cache'), + }; + caches = await plugins.hooks.fire('filter:admin.cache.get', caches); + if (!caches[req.query.name]) { + return next(); + } + + const data = JSON.stringify(caches[req.query.name].dump(), null, 4); + res.setHeader('Content-disposition', `attachment; filename= ${req.query.name}-cache.json`); + res.setHeader('Content-type', 'application/json'); + res.write(data, (err) => { + if (err) { + return next(err); + } + res.end(); + }); +}; diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js new file mode 100644 index 0000000000..d70acbbf91 --- /dev/null +++ b/src/controllers/admin/categories.js @@ -0,0 +1,143 @@ +'use strict'; + +const _ = require('lodash'); +const nconf = require('nconf'); +const categories = require('../../categories'); +const analytics = require('../../analytics'); +const plugins = require('../../plugins'); +const translator = require('../../translator'); +const meta = require('../../meta'); +const helpers = require('../helpers'); +const pagination = require('../../pagination'); + +const categoriesController = module.exports; + +categoriesController.get = async function (req, res, next) { + const [categoryData, parent, selectedData] = await Promise.all([ + categories.getCategories([req.params.category_id], req.uid), + categories.getParents([req.params.category_id]), + helpers.getSelectedCategory(req.params.category_id), + ]); + + const category = categoryData[0]; + if (!category) { + return next(); + } + + category.parent = parent[0]; + + const data = await plugins.hooks.fire('filter:admin.category.get', { + req: req, + res: res, + category: category, + customClasses: [], + }); + data.category.name = translator.escape(String(data.category.name)); + data.category.description = translator.escape(String(data.category.description)); + + res.render('admin/manage/category', { + category: data.category, + selectedCategory: selectedData.selectedCategory, + customClasses: data.customClasses, + postQueueEnabled: !!meta.config.postQueue, + }); +}; + +categoriesController.getAll = async function (req, res) { + const rootCid = parseInt(req.query.cid, 10) || 0; + async function getRootAndChildren() { + const rootChildren = await categories.getAllCidsFromSet(`cid:${rootCid}:children`); + const childCids = _.flatten(await Promise.all(rootChildren.map(cid => categories.getChildrenCids(cid)))); + return [rootCid].concat(rootChildren.concat(childCids)); + } + + // Categories list will be rendered on client side with recursion, etc. + const cids = await (rootCid ? getRootAndChildren() : categories.getAllCidsFromSet('categories:cid')); + + let rootParent = 0; + if (rootCid) { + rootParent = await categories.getCategoryField(rootCid, 'parentCid') || 0; + } + + const fields = [ + 'cid', 'name', 'icon', 'parentCid', 'disabled', 'link', 'order', + 'color', 'bgColor', 'backgroundImage', 'imageClass', 'subCategoriesPerPage', + ]; + const categoriesData = await categories.getCategoriesFields(cids, fields); + const result = await plugins.hooks.fire('filter:admin.categories.get', { categories: categoriesData, fields: fields }); + let tree = categories.getTree(result.categories, rootParent); + const cidsCount = rootCid && tree[0] ? tree[0].children.length : tree.length; + + const pageCount = Math.max(1, Math.ceil(cidsCount / meta.config.categoriesPerPage)); + const page = Math.min(parseInt(req.query.page, 10) || 1, pageCount); + const start = Math.max(0, (page - 1) * meta.config.categoriesPerPage); + const stop = start + meta.config.categoriesPerPage; + + function trim(c) { + if (c.children) { + c.subCategoriesLeft = Math.max(0, c.children.length - c.subCategoriesPerPage); + c.hasMoreSubCategories = c.children.length > c.subCategoriesPerPage; + c.showMorePage = Math.ceil(c.subCategoriesPerPage / meta.config.categoriesPerPage); + c.children = c.children.slice(0, c.subCategoriesPerPage); + c.children.forEach(c => trim(c)); + } + } + if (rootCid && tree[0] && Array.isArray(tree[0].children)) { + tree[0].children = tree[0].children.slice(start, stop); + tree[0].children.forEach(trim); + } else { + tree = tree.slice(start, stop); + tree.forEach(trim); + } + + let selectedCategory; + if (rootCid) { + selectedCategory = await categories.getCategoryData(rootCid); + } + const crumbs = await buildBreadcrumbs(selectedCategory, '/admin/manage/categories'); + res.render('admin/manage/categories', { + categoriesTree: tree, + selectedCategory: selectedCategory, + breadcrumbs: crumbs, + pagination: pagination.create(page, pageCount, req.query), + categoriesPerPage: meta.config.categoriesPerPage, + }); +}; + +async function buildBreadcrumbs(categoryData, url) { + if (!categoryData) { + return; + } + const breadcrumbs = [ + { + text: categoryData.name, + url: `${nconf.get('relative_path')}${url}?cid=${categoryData.cid}`, + cid: categoryData.cid, + }, + ]; + const allCrumbs = await helpers.buildCategoryBreadcrumbs(categoryData.parentCid); + const crumbs = allCrumbs.filter(c => c.cid); + + crumbs.forEach((c) => { + c.url = `${url}?cid=${c.cid}`; + }); + crumbs.unshift({ + text: '[[admin/manage/categories:top-level]]', + url: url, + }); + + return crumbs.concat(breadcrumbs); +} + +categoriesController.buildBreadCrumbs = buildBreadcrumbs; + +categoriesController.getAnalytics = async function (req, res) { + const [name, analyticsData] = await Promise.all([ + categories.getCategoryField(req.params.category_id, 'name'), + analytics.getCategoryAnalytics(req.params.category_id), + ]); + res.render('admin/manage/category-analytics', { + name: name, + analytics: analyticsData, + }); +}; diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js new file mode 100644 index 0000000000..d35063b1e8 --- /dev/null +++ b/src/controllers/admin/dashboard.js @@ -0,0 +1,344 @@ +'use strict'; + +const nconf = require('nconf'); +const semver = require('semver'); +const winston = require('winston'); +const _ = require('lodash'); +const validator = require('validator'); + +const versions = require('../../admin/versions'); +const db = require('../../database'); +const meta = require('../../meta'); +const analytics = require('../../analytics'); +const plugins = require('../../plugins'); +const user = require('../../user'); +const topics = require('../../topics'); +const utils = require('../../utils'); +const emailer = require('../../emailer'); + +const dashboardController = module.exports; + +dashboardController.get = async function (req, res) { + const [stats, notices, latestVersion, lastrestart, isAdmin, popularSearches] = await Promise.all([ + getStats(), + getNotices(), + getLatestVersion(), + getLastRestart(), + user.isAdministrator(req.uid), + getPopularSearches(), + ]); + const version = nconf.get('version'); + + res.render('admin/dashboard', { + version: version, + lookupFailed: latestVersion === null, + latestVersion: latestVersion, + upgradeAvailable: latestVersion && semver.gt(latestVersion, version), + currentPrerelease: versions.isPrerelease.test(version), + notices: notices, + stats: stats, + canRestart: !!process.send, + lastrestart: lastrestart, + showSystemControls: isAdmin, + popularSearches: popularSearches, + }); +}; + +async function getNotices() { + const notices = [ + { + done: !meta.reloadRequired, + doneText: '[[admin/dashboard:restart-not-required]]', + notDoneText: '[[admin/dashboard:restart-required]]', + }, + { + done: plugins.hooks.hasListeners('filter:search.query'), + doneText: '[[admin/dashboard:search-plugin-installed]]', + notDoneText: '[[admin/dashboard:search-plugin-not-installed]]', + tooltip: '[[admin/dashboard:search-plugin-tooltip]]', + link: '/admin/extend/plugins', + }, + ]; + + if (emailer.fallbackNotFound) { + notices.push({ + done: false, + notDoneText: '[[admin/dashboard:fallback-emailer-not-found]]', + }); + } + + if (global.env !== 'production') { + notices.push({ + done: false, + notDoneText: '[[admin/dashboard:running-in-development]]', + }); + } + + return await plugins.hooks.fire('filter:admin.notices', notices); +} + +async function getLatestVersion() { + try { + return await versions.getLatestVersion(); + } catch (err) { + winston.error(`[acp] Failed to fetch latest version\n${err.stack}`); + } + return null; +} + +dashboardController.getAnalytics = async (req, res, next) => { + // Basic validation + const validUnits = ['days', 'hours']; + const validSets = ['uniquevisitors', 'pageviews', 'pageviews:registered', 'pageviews:bot', 'pageviews:guest']; + const until = req.query.until ? new Date(parseInt(req.query.until, 10)) : Date.now(); + const count = req.query.count || (req.query.units === 'hours' ? 24 : 30); + if (isNaN(until) || !validUnits.includes(req.query.units)) { + return next(new Error('[[error:invalid-data]]')); + } + + // Filter out invalid sets, if no sets, assume all sets + let sets; + if (req.query.sets) { + sets = Array.isArray(req.query.sets) ? req.query.sets : [req.query.sets]; + sets = sets.filter(set => validSets.includes(set)); + } else { + sets = validSets; + } + + const method = req.query.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; + let payload = await Promise.all(sets.map(set => method(`analytics:${set}`, until, count))); + payload = _.zipObject(sets, payload); + + res.json({ + query: { + set: req.query.set, + units: req.query.units, + until: until, + count: count, + }, + result: payload, + }); +}; + +async function getStats() { + const cache = require('../../cache'); + const cachedStats = cache.get('admin:stats'); + if (cachedStats !== undefined) { + return cachedStats; + } + + let results = await Promise.all([ + getStatsForSet('ip:recent', 'uniqueIPCount'), + getStatsFromAnalytics('logins', 'loginCount'), + getStatsForSet('users:joindate', 'userCount'), + getStatsForSet('posts:pid', 'postCount'), + getStatsForSet('topics:tid', 'topicCount'), + ]); + results[0].name = '[[admin/dashboard:unique-visitors]]'; + + results[1].name = '[[admin/dashboard:logins]]'; + results[1].href = `${nconf.get('relative_path')}/admin/dashboard/logins`; + + results[2].name = '[[admin/dashboard:new-users]]'; + results[2].href = `${nconf.get('relative_path')}/admin/dashboard/users`; + + results[3].name = '[[admin/dashboard:posts]]'; + + results[4].name = '[[admin/dashboard:topics]]'; + results[4].href = `${nconf.get('relative_path')}/admin/dashboard/topics`; + + ({ results } = await plugins.hooks.fire('filter:admin.getStats', { + results, + helpers: { getStatsForSet, getStatsFromAnalytics }, + })); + + cache.set('admin:stats', results, 600000); + return results; +} + +async function getStatsForSet(set, field) { + const terms = { + day: 86400000, + week: 604800000, + month: 2592000000, + }; + + const now = Date.now(); + const results = await utils.promiseParallel({ + yesterday: db.sortedSetCount(set, now - (terms.day * 2), '+inf'), + today: db.sortedSetCount(set, now - terms.day, '+inf'), + lastweek: db.sortedSetCount(set, now - (terms.week * 2), '+inf'), + thisweek: db.sortedSetCount(set, now - terms.week, '+inf'), + lastmonth: db.sortedSetCount(set, now - (terms.month * 2), '+inf'), + thismonth: db.sortedSetCount(set, now - terms.month, '+inf'), + alltime: getGlobalField(field), + }); + + return calculateDeltas(results); +} + +async function getStatsFromAnalytics(set, field) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const data = await analytics.getDailyStatsForSet(`analytics:${set}`, today, 60); + const sum = arr => arr.reduce((memo, cur) => memo + cur, 0); + const results = { + yesterday: sum(data.slice(-2)), + today: data.slice(-1)[0], + lastweek: sum(data.slice(-14)), + thisweek: sum(data.slice(-7)), + lastmonth: sum(data.slice(0)), // entire set + thismonth: sum(data.slice(-30)), + alltime: await getGlobalField(field), + }; + + return calculateDeltas(results); +} + +function calculateDeltas(results) { + function textClass(num) { + if (num > 0) { + return 'text-success'; + } else if (num < 0) { + return 'text-danger'; + } + return 'text-warning'; + } + + function increasePercent(last, now) { + const percent = last ? (now - last) / last * 100 : 0; + return percent.toFixed(1); + } + results.yesterday -= results.today; + results.dayIncrease = increasePercent(results.yesterday, results.today); + results.dayTextClass = textClass(results.dayIncrease); + + results.lastweek -= results.thisweek; + results.weekIncrease = increasePercent(results.lastweek, results.thisweek); + results.weekTextClass = textClass(results.weekIncrease); + + results.lastmonth -= results.thismonth; + results.monthIncrease = increasePercent(results.lastmonth, results.thismonth); + results.monthTextClass = textClass(results.monthIncrease); + + return results; +} + +async function getGlobalField(field) { + const count = await db.getObjectField('global', field); + return parseInt(count, 10) || 0; +} + +async function getLastRestart() { + const lastrestart = await db.getObject('lastrestart'); + if (!lastrestart) { + return null; + } + const userData = await user.getUserData(lastrestart.uid); + lastrestart.user = userData; + lastrestart.timestampISO = utils.toISOString(lastrestart.timestamp); + return lastrestart; +} + +async function getPopularSearches() { + const searches = await db.getSortedSetRevRangeWithScores('searches:all', 0, 9); + return searches.map(s => ({ value: validator.escape(String(s.value)), score: s.score })); +} + +dashboardController.getLogins = async (req, res) => { + let stats = await getStats(); + stats = stats.filter(stat => stat.name === '[[admin/dashboard:logins]]').map(({ ...stat }) => { + delete stat.href; + return stat; + }); + const summary = { + day: stats[0].today, + week: stats[0].thisweek, + month: stats[0].thismonth, + }; + + // List recent sessions + const start = Date.now() - (1000 * 60 * 60 * 24 * meta.config.loginDays); + const uids = await db.getSortedSetRangeByScore('users:online', 0, 500, start, Date.now()); + const usersData = await user.getUsersData(uids); + let sessions = await Promise.all(uids.map(async (uid) => { + const sessions = await user.auth.getSessions(uid); + sessions.forEach((session) => { + session.user = usersData[uids.indexOf(uid)]; + }); + + return sessions; + })); + sessions = _.flatten(sessions).sort((a, b) => b.datetime - a.datetime); + + res.render('admin/dashboard/logins', { + set: 'logins', + query: req.query, + stats, + summary, + sessions, + loginDays: meta.config.loginDays, + }); +}; + +dashboardController.getUsers = async (req, res) => { + let stats = await getStats(); + stats = stats.filter(stat => stat.name === '[[admin/dashboard:new-users]]').map(({ ...stat }) => { + delete stat.href; + return stat; + }); + const summary = { + day: stats[0].today, + week: stats[0].thisweek, + month: stats[0].thismonth, + }; + + // List of users registered within time frame + const end = parseInt(req.query.until, 10) || Date.now(); + const start = end - (1000 * 60 * 60 * (req.query.units === 'days' ? 24 : 1) * (req.query.count || (req.query.units === 'days' ? 30 : 24))); + const uids = await db.getSortedSetRangeByScore('users:joindate', 0, 500, start, end); + const users = await user.getUsersData(uids); + + res.render('admin/dashboard/users', { + set: 'registrations', + query: req.query, + stats, + summary, + users, + }); +}; + +dashboardController.getTopics = async (req, res) => { + let stats = await getStats(); + stats = stats.filter(stat => stat.name === '[[admin/dashboard:topics]]').map(({ ...stat }) => { + delete stat.href; + return stat; + }); + const summary = { + day: stats[0].today, + week: stats[0].thisweek, + month: stats[0].thismonth, + }; + + // List of topics created within time frame + const end = parseInt(req.query.until, 10) || Date.now(); + const start = end - (1000 * 60 * 60 * (req.query.units === 'days' ? 24 : 1) * (req.query.count || (req.query.units === 'days' ? 30 : 24))); + const tids = await db.getSortedSetRangeByScore('topics:tid', 0, 500, start, end); + const topicData = await topics.getTopicsByTids(tids); + + res.render('admin/dashboard/topics', { + set: 'topics', + query: req.query, + stats, + summary, + topics: topicData, + }); +}; + +dashboardController.getSearches = async (req, res) => { + const searches = await db.getSortedSetRevRangeWithScores('searches:all', 0, 99); + res.render('admin/dashboard/searches', { + searches: searches.map(s => ({ value: validator.escape(String(s.value)), score: s.score })), + }); +}; diff --git a/src/controllers/admin/database.js b/src/controllers/admin/database.js new file mode 100644 index 0000000000..443fdcfcfa --- /dev/null +++ b/src/controllers/admin/database.js @@ -0,0 +1,23 @@ +'use strict'; + +const nconf = require('nconf'); + +const databaseController = module.exports; + +databaseController.get = async function (req, res) { + const results = {}; + if (nconf.get('redis')) { + const rdb = require('../../database/redis'); + results.redis = await rdb.info(rdb.client); + } + if (nconf.get('mongo')) { + const mdb = require('../../database/mongo'); + results.mongo = await mdb.info(mdb.client); + } + if (nconf.get('postgres')) { + const pdb = require('../../database/postgres'); + results.postgres = await pdb.info(pdb.pool); + } + + res.render('admin/advanced/database', results); +}; diff --git a/src/controllers/admin/digest.js b/src/controllers/admin/digest.js new file mode 100644 index 0000000000..30fed9a8f1 --- /dev/null +++ b/src/controllers/admin/digest.js @@ -0,0 +1,23 @@ +'use strict'; + +const meta = require('../../meta'); +const digest = require('../../user/digest'); +const pagination = require('../../pagination'); + +const digestController = module.exports; + +digestController.get = async function (req, res) { + const page = parseInt(req.query.page, 10) || 1; + const resultsPerPage = 50; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + const delivery = await digest.getDeliveryTimes(start, stop); + + const pageCount = Math.ceil(delivery.count / resultsPerPage); + res.render('admin/manage/digest', { + title: '[[admin/menu:manage/digest]]', + delivery: delivery.users, + default: meta.config.dailyDigestFreq, + pagination: pagination.create(page, pageCount), + }); +}; diff --git a/src/controllers/admin/errors.js b/src/controllers/admin/errors.js new file mode 100644 index 0000000000..98bdbe8e48 --- /dev/null +++ b/src/controllers/admin/errors.js @@ -0,0 +1,25 @@ +'use strict'; + +const json2csvAsync = require('json2csv').parseAsync; + +const meta = require('../../meta'); +const analytics = require('../../analytics'); +const utils = require('../../utils'); + +const errorsController = module.exports; + +errorsController.get = async function (req, res) { + const data = await utils.promiseParallel({ + 'not-found': meta.errors.get(true), + analytics: analytics.getErrorAnalytics(), + }); + res.render('admin/advanced/errors', data); +}; + +errorsController.export = async function (req, res) { + const data = await meta.errors.get(false); + const fields = data.length ? Object.keys(data[0]) : []; + const opts = { fields }; + const csv = await json2csvAsync(data, opts); + res.set('Content-Type', 'text/csv').set('Content-Disposition', 'attachment; filename="404.csv"').send(csv); +}; diff --git a/src/controllers/admin/events.js b/src/controllers/admin/events.js new file mode 100644 index 0000000000..3b001013fe --- /dev/null +++ b/src/controllers/admin/events.js @@ -0,0 +1,44 @@ +'use strict'; + +const db = require('../../database'); +const events = require('../../events'); +const pagination = require('../../pagination'); + +const eventsController = module.exports; + +eventsController.get = async function (req, res) { + const page = parseInt(req.query.page, 10) || 1; + const itemsPerPage = parseInt(req.query.perPage, 10) || 20; + const start = (page - 1) * itemsPerPage; + const stop = start + itemsPerPage - 1; + + // Limit by date + let from = req.query.start ? new Date(req.query.start) || undefined : undefined; + let to = req.query.end ? new Date(req.query.end) || undefined : new Date(); + from = from && from.setHours(0, 0, 0, 0); // setHours returns a unix timestamp (Number, not Date) + to = to && to.setHours(23, 59, 59, 999); // setHours returns a unix timestamp (Number, not Date) + + const currentFilter = req.query.type || ''; + + const [eventCount, eventData, counts] = await Promise.all([ + db.sortedSetCount(`events:time${currentFilter ? `:${currentFilter}` : ''}`, from || '-inf', to), + events.getEvents(currentFilter, start, stop, from || '-inf', to), + db.sortedSetsCard([''].concat(events.types).map(type => `events:time${type ? `:${type}` : ''}`)), + ]); + + const types = [''].concat(events.types).map((type, index) => ({ + value: type, + name: type || 'all', + selected: type === currentFilter, + count: counts[index], + })); + + const pageCount = Math.max(1, Math.ceil(eventCount / itemsPerPage)); + + res.render('admin/advanced/events', { + events: eventData, + pagination: pagination.create(page, pageCount, req.query), + types: types, + query: req.query, + }); +}; diff --git a/src/controllers/admin/groups.js b/src/controllers/admin/groups.js new file mode 100644 index 0000000000..169b49a074 --- /dev/null +++ b/src/controllers/admin/groups.js @@ -0,0 +1,98 @@ +'use strict'; + +const nconf = require('nconf'); +const validator = require('validator'); + +const db = require('../../database'); +const user = require('../../user'); +const groups = require('../../groups'); +const meta = require('../../meta'); +const pagination = require('../../pagination'); +const events = require('../../events'); +const slugify = require('../../slugify'); + +const groupsController = module.exports; + +groupsController.list = async function (req, res) { + const page = parseInt(req.query.page, 10) || 1; + const groupsPerPage = 20; + + let groupNames = await getGroupNames(); + const pageCount = Math.ceil(groupNames.length / groupsPerPage); + const start = (page - 1) * groupsPerPage; + const stop = start + groupsPerPage - 1; + groupNames = groupNames.slice(start, stop + 1); + + const groupData = await groups.getGroupsData(groupNames); + res.render('admin/manage/groups', { + groups: groupData, + pagination: pagination.create(page, pageCount), + yourid: req.uid, + }); +}; + +groupsController.get = async function (req, res, next) { + const slug = slugify(req.params.name); + const groupName = await groups.getGroupNameByGroupSlug(slug); + const [groupNames, group] = await Promise.all([ + getGroupNames(), + groups.get(groupName, { uid: req.uid, truncateUserList: true, userListCount: 20 }), + ]); + + if (!group || groupName === groups.BANNED_USERS) { + return next(); + } + group.isOwner = true; + + const groupNameData = groupNames.map(name => ({ + encodedName: encodeURIComponent(name), + displayName: validator.escape(String(name)), + selected: name === groupName, + })); + + res.render('admin/manage/group', { + group: group, + groupNames: groupNameData, + allowPrivateGroups: meta.config.allowPrivateGroups, + maximumGroupNameLength: meta.config.maximumGroupNameLength, + maximumGroupTitleLength: meta.config.maximumGroupTitleLength, + }); +}; + +async function getGroupNames() { + const groupNames = await db.getSortedSetRange('groups:createtime', 0, -1); + return groupNames.filter(name => ( + name !== 'registered-users' && + name !== 'verified-users' && + name !== 'unverified-users' && + name !== groups.BANNED_USERS && + !groups.isPrivilegeGroup(name) + )); +} + +groupsController.getCSV = async function (req, res) { + const { referer } = req.headers; + + if (!referer || !referer.replace(nconf.get('url'), '').startsWith('/admin/manage/groups')) { + return res.status(403).send('[[error:invalid-origin]]'); + } + await events.log({ + type: 'getGroupCSV', + uid: req.uid, + ip: req.ip, + group: req.params.groupname, + }); + const groupName = req.params.groupname; + const members = (await groups.getMembersOfGroups([groupName]))[0]; + const fields = ['email', 'username', 'uid']; + const userData = await user.getUsersFields(members, fields); + let csvContent = `${fields.join(',')}\n`; + csvContent += userData.reduce((memo, user) => { + memo += `${user.email},${user.username},${user.uid}\n`; + return memo; + }, ''); + + res.attachment(`${validator.escape(groupName)}_members.csv`); + res.setHeader('Content-Type', 'text/csv'); + res.end(csvContent); +}; diff --git a/src/controllers/admin/hooks.js b/src/controllers/admin/hooks.js new file mode 100644 index 0000000000..eb3cb6c3f1 --- /dev/null +++ b/src/controllers/admin/hooks.js @@ -0,0 +1,32 @@ +'use strict'; + +const validator = require('validator'); +const plugins = require('../../plugins'); + +const hooksController = module.exports; + +hooksController.get = function (req, res) { + const hooks = []; + Object.keys(plugins.loadedHooks).forEach((key, hookIndex) => { + const current = { + hookName: key, + methods: [], + index: `hook-${hookIndex}`, + count: plugins.loadedHooks[key].length, + }; + + plugins.loadedHooks[key].forEach((hookData, methodIndex) => { + current.methods.push({ + id: hookData.id, + priority: hookData.priority, + method: hookData.method ? validator.escape(hookData.method.toString()) : 'No plugin function!', + index: `${hookIndex}-code-${methodIndex}`, + }); + }); + hooks.push(current); + }); + + hooks.sort((a, b) => b.count - a.count); + + res.render('admin/advanced/hooks', { hooks: hooks }); +}; diff --git a/src/controllers/admin/info.js b/src/controllers/admin/info.js new file mode 100644 index 0000000000..d2cdc240d3 --- /dev/null +++ b/src/controllers/admin/info.js @@ -0,0 +1,144 @@ +'use strict'; + +const os = require('os'); +const winston = require('winston'); +const nconf = require('nconf'); +const { exec } = require('child_process'); + +const pubsub = require('../../pubsub'); +const rooms = require('../../socket.io/admin/rooms'); + +const infoController = module.exports; + +let info = {}; +let previousUsage = process.cpuUsage(); +let usageStartDate = Date.now(); + +infoController.get = function (req, res) { + info = {}; + pubsub.publish('sync:node:info:start'); + const timeoutMS = 1000; + setTimeout(() => { + const data = []; + Object.keys(info).forEach(key => data.push(info[key])); + data.sort((a, b) => { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + }); + + let port = nconf.get('port'); + if (!Array.isArray(port) && !isNaN(parseInt(port, 10))) { + port = [port]; + } + + res.render('admin/development/info', { + info: data, + infoJSON: JSON.stringify(data, null, 4), + host: os.hostname(), + port: port, + nodeCount: data.length, + timeout: timeoutMS, + ip: req.ip, + }); + }, timeoutMS); +}; + +pubsub.on('sync:node:info:start', async () => { + try { + const data = await getNodeInfo(); + data.id = `${os.hostname()}:${nconf.get('port')}`; + pubsub.publish('sync:node:info:end', { data: data, id: data.id }); + } catch (err) { + winston.error(err.stack); + } +}); + +pubsub.on('sync:node:info:end', (data) => { + info[data.id] = data.data; +}); + +async function getNodeInfo() { + const data = { + process: { + port: nconf.get('port'), + pid: process.pid, + title: process.title, + version: process.version, + memoryUsage: process.memoryUsage(), + uptime: process.uptime(), + cpuUsage: getCpuUsage(), + }, + os: { + hostname: os.hostname(), + type: os.type(), + platform: os.platform(), + arch: os.arch(), + release: os.release(), + load: os.loadavg().map(load => load.toFixed(2)).join(', '), + freemem: os.freemem(), + totalmem: os.totalmem(), + }, + nodebb: { + isCluster: nconf.get('isCluster'), + isPrimary: nconf.get('isPrimary'), + runJobs: nconf.get('runJobs'), + jobsDisabled: nconf.get('jobsDisabled'), + }, + }; + + data.process.memoryUsage.humanReadable = (data.process.memoryUsage.rss / (1024 * 1024 * 1024)).toFixed(3); + data.process.uptimeHumanReadable = humanReadableUptime(data.process.uptime); + data.os.freemem = (data.os.freemem / (1024 * 1024 * 1024)).toFixed(2); + data.os.totalmem = (data.os.totalmem / (1024 * 1024 * 1024)).toFixed(2); + data.os.usedmem = (data.os.totalmem - data.os.freemem).toFixed(2); + const [stats, gitInfo] = await Promise.all([ + rooms.getLocalStats(), + getGitInfo(), + ]); + data.git = gitInfo; + data.stats = stats; + return data; +} + +function getCpuUsage() { + const newUsage = process.cpuUsage(); + const diff = (newUsage.user + newUsage.system) - (previousUsage.user + previousUsage.system); + const now = Date.now(); + const result = diff / ((now - usageStartDate) * 1000) * 100; + previousUsage = newUsage; + usageStartDate = now; + return result.toFixed(2); +} + +function humanReadableUptime(seconds) { + if (seconds < 60) { + return `${Math.floor(seconds)}s`; + } else if (seconds < 3600) { + return `${Math.floor(seconds / 60)}m`; + } else if (seconds < 3600 * 24) { + return `${Math.floor(seconds / (60 * 60))}h`; + } + return `${Math.floor(seconds / (60 * 60 * 24))}d`; +} + +async function getGitInfo() { + function get(cmd, callback) { + exec(cmd, (err, stdout) => { + if (err) { + winston.error(err.stack); + } + callback(null, stdout ? stdout.replace(/\n$/, '') : 'no-git-info'); + }); + } + const getAsync = require('util').promisify(get); + const [hash, branch] = await Promise.all([ + getAsync('git rev-parse HEAD'), + getAsync('git rev-parse --abbrev-ref HEAD'), + ]); + return { hash: hash, hashShort: hash.slice(0, 6), branch: branch }; +} diff --git a/src/controllers/admin/logger.js b/src/controllers/admin/logger.js new file mode 100644 index 0000000000..ee6af55364 --- /dev/null +++ b/src/controllers/admin/logger.js @@ -0,0 +1,7 @@ +'use strict'; + +const loggerController = module.exports; + +loggerController.get = function (req, res) { + res.render('admin/development/logger', {}); +}; diff --git a/src/controllers/admin/logs.js b/src/controllers/admin/logs.js new file mode 100644 index 0000000000..104df038e9 --- /dev/null +++ b/src/controllers/admin/logs.js @@ -0,0 +1,20 @@ +'use strict'; + +const validator = require('validator'); +const winston = require('winston'); + +const meta = require('../../meta'); + +const logsController = module.exports; + +logsController.get = async function (req, res) { + let logs = ''; + try { + logs = await meta.logs.get(); + } catch (err) { + winston.error(err.stack); + } + res.render('admin/advanced/logs', { + data: validator.escape(logs), + }); +}; diff --git a/src/controllers/admin/plugins.js b/src/controllers/admin/plugins.js new file mode 100644 index 0000000000..4f2c7bae0a --- /dev/null +++ b/src/controllers/admin/plugins.js @@ -0,0 +1,69 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); +const plugins = require('../../plugins'); +const meta = require('../../meta'); + +const pluginsController = module.exports; + +pluginsController.get = async function (req, res) { + const [compatible, all, trending] = await Promise.all([ + getCompatiblePlugins(), + getAllPlugins(), + plugins.listTrending(), + ]); + + const compatiblePkgNames = compatible.map(pkgData => pkgData.name); + const installedPlugins = compatible.filter(plugin => plugin && plugin.installed); + const activePlugins = all.filter(plugin => plugin && plugin.installed && plugin.active); + + const trendingScores = trending.reduce((memo, cur) => { + memo[cur.label] = cur.value; + return memo; + }, {}); + const trendingPlugins = all + .filter(plugin => plugin && Object.keys(trendingScores).includes(plugin.id)) + .sort((a, b) => trendingScores[b.id] - trendingScores[a.id]) + .map((plugin) => { + plugin.downloads = trendingScores[plugin.id]; + return plugin; + }); + + res.render('admin/extend/plugins', { + installed: installedPlugins, + installedCount: installedPlugins.length, + activeCount: activePlugins.length, + inactiveCount: Math.max(0, installedPlugins.length - activePlugins.length), + canChangeState: !nconf.get('plugins:active'), + upgradeCount: compatible.reduce((count, current) => { + if (current.installed && current.outdated) { + count += 1; + } + return count; + }, 0), + download: compatible.filter(plugin => !plugin.installed), + incompatible: all.filter(plugin => !compatiblePkgNames.includes(plugin.name)), + trending: trendingPlugins, + submitPluginUsage: meta.config.submitPluginUsage, + version: nconf.get('version'), + }); +}; + +async function getCompatiblePlugins() { + return await getPlugins(true); +} + +async function getAllPlugins() { + return await getPlugins(false); +} + +async function getPlugins(matching) { + try { + const pluginsData = await plugins.list(matching); + return pluginsData || []; + } catch (err) { + winston.error(err.stack); + return []; + } +} diff --git a/src/controllers/admin/privileges.js b/src/controllers/admin/privileges.js new file mode 100644 index 0000000000..427f252aad --- /dev/null +++ b/src/controllers/admin/privileges.js @@ -0,0 +1,52 @@ +'use strict'; + +const categories = require('../../categories'); +const privileges = require('../../privileges'); + +const privilegesController = module.exports; + +privilegesController.get = async function (req, res) { + const cid = req.params.cid ? parseInt(req.params.cid, 10) || 0 : 0; + const isAdminPriv = req.params.cid === 'admin'; + + let privilegesData; + if (cid > 0) { + privilegesData = await privileges.categories.list(cid); + } else if (cid === 0) { + privilegesData = await (isAdminPriv ? privileges.admin.list(req.uid) : privileges.global.list()); + } + + const categoriesData = [{ + cid: 0, + name: '[[admin/manage/privileges:global]]', + icon: 'fa-list', + }, { + cid: 'admin', + name: '[[admin/manage/privileges:admin]]', + icon: 'fa-lock', + }]; + + let selectedCategory; + categoriesData.forEach((category) => { + if (category) { + category.selected = category.cid === (!isAdminPriv ? cid : 'admin'); + + if (category.selected) { + selectedCategory = category; + } + } + }); + if (!selectedCategory) { + selectedCategory = await categories.getCategoryFields(cid, ['cid', 'name', 'icon', 'bgColor', 'color']); + } + + const group = req.query.group ? req.query.group : ''; + res.render('admin/manage/privileges', { + privileges: privilegesData, + categories: categoriesData, + selectedCategory, + cid, + group, + isAdminPriv, + }); +}; diff --git a/src/controllers/admin/rewards.js b/src/controllers/admin/rewards.js new file mode 100644 index 0000000000..644bb79d3c --- /dev/null +++ b/src/controllers/admin/rewards.js @@ -0,0 +1,10 @@ +'use strict'; + +const admin = require('../../rewards/admin'); + +const rewardsController = module.exports; + +rewardsController.get = async function (req, res) { + const data = await admin.get(); + res.render('admin/extend/rewards', data); +}; diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js new file mode 100644 index 0000000000..9392290172 --- /dev/null +++ b/src/controllers/admin/settings.js @@ -0,0 +1,110 @@ +'use strict'; + +const validator = require('validator'); + +const meta = require('../../meta'); +const emailer = require('../../emailer'); +const notifications = require('../../notifications'); +const groups = require('../../groups'); +const languages = require('../../languages'); +const navigationAdmin = require('../../navigation/admin'); +const social = require('../../social'); + +const helpers = require('../helpers'); +const translator = require('../../translator'); + +const settingsController = module.exports; + +settingsController.get = async function (req, res) { + const term = req.params.term || 'general'; + res.render(`admin/settings/${term}`); +}; + +settingsController.email = async (req, res) => { + const emails = await emailer.getTemplates(meta.config); + + res.render('admin/settings/email', { + emails: emails, + sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path), + services: emailer.listServices(), + }); +}; + +settingsController.user = async (req, res) => { + const notificationTypes = await notifications.getAllNotificationTypes(); + const notificationSettings = notificationTypes.map(type => ({ + name: type, + label: `[[notifications:${type}]]`, + })); + res.render('admin/settings/user', { + notificationSettings: notificationSettings, + }); +}; + +settingsController.post = async (req, res) => { + const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); + res.render('admin/settings/post', { + groupsExemptFromPostQueue: groupData, + }); +}; + +settingsController.advanced = async (req, res) => { + const groupData = await groups.getNonPrivilegeGroups('groups:createtime', 0, -1); + res.render('admin/settings/advanced', { + groupsExemptFromMaintenanceMode: groupData, + }); +}; + +settingsController.languages = async function (req, res) { + const languageData = await languages.list(); + languageData.forEach((language) => { + language.selected = language.code === meta.config.defaultLang; + }); + + res.render('admin/settings/languages', { + languages: languageData, + autoDetectLang: meta.config.autoDetectLang, + }); +}; + +settingsController.navigation = async function (req, res) { + const [admin, allGroups] = await Promise.all([ + navigationAdmin.getAdmin(), + groups.getNonPrivilegeGroups('groups:createtime', 0, -1), + ]); + + allGroups.sort((a, b) => b.system - a.system); + + admin.groups = allGroups.map(group => ({ name: group.name, displayName: group.displayName })); + admin.enabled.forEach((enabled, index) => { + enabled.index = index; + enabled.selected = index === 0; + enabled.title = translator.escape(enabled.title); + enabled.text = translator.escape(enabled.text); + enabled.dropdownContent = translator.escape(validator.escape(String(enabled.dropdownContent || ''))); + enabled.groups = admin.groups.map(group => ({ + displayName: group.displayName, + selected: enabled.groups.includes(group.name), + })); + }); + + admin.available.forEach((available) => { + available.groups = admin.groups; + }); + + admin.navigation = admin.enabled.slice(); + + res.render('admin/settings/navigation', admin); +}; + +settingsController.homepage = async function (req, res) { + const routes = await helpers.getHomePageRoutes(req.uid); + res.render('admin/settings/homepage', { routes: routes }); +}; + +settingsController.social = async function (req, res) { + const posts = await social.getPostSharing(); + res.render('admin/settings/social', { + posts: posts, + }); +}; diff --git a/src/controllers/admin/tags.js b/src/controllers/admin/tags.js new file mode 100644 index 0000000000..294a8f9d54 --- /dev/null +++ b/src/controllers/admin/tags.js @@ -0,0 +1,10 @@ +'use strict'; + +const topics = require('../../topics'); + +const tagsController = module.exports; + +tagsController.get = async function (req, res) { + const tags = await topics.getTags(0, 199); + res.render('admin/manage/tags', { tags: tags }); +}; diff --git a/src/controllers/admin/themes.js b/src/controllers/admin/themes.js new file mode 100644 index 0000000000..ae546e8075 --- /dev/null +++ b/src/controllers/admin/themes.js @@ -0,0 +1,31 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +const file = require('../../file'); +const { paths } = require('../../constants'); + +const themesController = module.exports; + +const defaultScreenshotPath = path.join(__dirname, '../../../public/images/themes/default.png'); + +themesController.get = async function (req, res, next) { + const themeDir = path.join(paths.themes, req.params.theme); + const themeConfigPath = path.join(themeDir, 'theme.json'); + + let themeConfig; + try { + themeConfig = await fs.promises.readFile(themeConfigPath, 'utf8'); + themeConfig = JSON.parse(themeConfig); + } catch (err) { + if (err.code === 'ENOENT') { + return next(Error('invalid-data')); + } + return next(err); + } + + const screenshotPath = themeConfig.screenshot ? path.join(themeDir, themeConfig.screenshot) : defaultScreenshotPath; + const exists = await file.exists(screenshotPath); + res.sendFile(exists ? screenshotPath : defaultScreenshotPath); +}; diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js new file mode 100644 index 0000000000..2f1e010cad --- /dev/null +++ b/src/controllers/admin/uploads.js @@ -0,0 +1,273 @@ +'use strict'; + +const path = require('path'); +const nconf = require('nconf'); +const fs = require('fs'); + +const meta = require('../../meta'); +const posts = require('../../posts'); +const file = require('../../file'); +const image = require('../../image'); +const plugins = require('../../plugins'); +const pagination = require('../../pagination'); + +const allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; + +const uploadsController = module.exports; + +uploadsController.get = async function (req, res, next) { + const currentFolder = path.join(nconf.get('upload_path'), req.query.dir || ''); + if (!currentFolder.startsWith(nconf.get('upload_path'))) { + return next(new Error('[[error:invalid-path]]')); + } + const itemsPerPage = 20; + const page = parseInt(req.query.page, 10) || 1; + try { + let files = await fs.promises.readdir(currentFolder); + files = files.filter(filename => filename !== '.gitignore'); + const itemCount = files.length; + const start = Math.max(0, (page - 1) * itemsPerPage); + const stop = start + itemsPerPage; + files = files.slice(start, stop); + + files = await filesToData(currentFolder, files); + + // Float directories to the top + files.sort((a, b) => { + if (a.isDirectory && !b.isDirectory) { + return -1; + } else if (!a.isDirectory && b.isDirectory) { + return 1; + } else if (!a.isDirectory && !b.isDirectory) { + return a.mtime < b.mtime ? -1 : 1; + } + + return 0; + }); + + // Add post usage info if in /files + if (['files', '/files', '/files/'].includes(req.query.dir)) { + const usage = await posts.uploads.getUsage(files); + files.forEach((file, idx) => { + file.inPids = usage[idx].map(pid => parseInt(pid, 10)); + }); + } + res.render('admin/manage/uploads', { + currentFolder: currentFolder.replace(nconf.get('upload_path'), ''), + showPids: files.length && files[0].hasOwnProperty('inPids'), + files: files, + breadcrumbs: buildBreadcrumbs(currentFolder), + pagination: pagination.create(page, Math.ceil(itemCount / itemsPerPage), req.query), + }); + } catch (err) { + next(err); + } +}; + +function buildBreadcrumbs(currentFolder) { + const crumbs = []; + const parts = currentFolder.replace(nconf.get('upload_path'), '').split(path.sep); + let currentPath = ''; + parts.forEach((part) => { + const dir = path.join(currentPath, part); + crumbs.push({ + text: part || 'Uploads', + url: part ? + (`${nconf.get('relative_path')}/admin/manage/uploads?dir=${dir}`) : + `${nconf.get('relative_path')}/admin/manage/uploads`, + }); + currentPath = dir; + }); + + return crumbs; +} + +async function filesToData(currentDir, files) { + return await Promise.all(files.map(file => getFileData(currentDir, file))); +} + +async function getFileData(currentDir, file) { + const pathToFile = path.join(currentDir, file); + const stat = await fs.promises.stat(pathToFile); + let filesInDir = []; + if (stat.isDirectory()) { + filesInDir = await fs.promises.readdir(pathToFile); + } + const url = `${nconf.get('upload_url') + currentDir.replace(nconf.get('upload_path'), '')}/${file}`; + return { + name: file, + path: pathToFile.replace(path.join(nconf.get('upload_path'), '/'), ''), + url: url, + fileCount: Math.max(0, filesInDir.length - 1), // ignore .gitignore + size: stat.size, + sizeHumanReadable: `${(stat.size / 1024).toFixed(1)}KiB`, + isDirectory: stat.isDirectory(), + isFile: stat.isFile(), + mtime: stat.mtimeMs, + }; +} + +uploadsController.uploadCategoryPicture = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + let params = null; + + try { + params = JSON.parse(req.body.params); + } catch (e) { + file.delete(uploadedFile.path); + return next(new Error('[[error:invalid-json]]')); + } + + if (validateUpload(res, uploadedFile, allowedImageTypes)) { + const filename = `category-${params.cid}${path.extname(uploadedFile.name)}`; + await uploadImage(filename, 'category', uploadedFile, req, res, next); + } +}; + +uploadsController.uploadFavicon = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + const allowedTypes = ['image/x-icon', 'image/vnd.microsoft.icon']; + + if (validateUpload(res, uploadedFile, allowedTypes)) { + try { + const imageObj = await file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path); + res.json([{ name: uploadedFile.name, url: imageObj.url }]); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } + } +}; + +uploadsController.uploadTouchIcon = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + const allowedTypes = ['image/png']; + const sizes = [36, 48, 72, 96, 144, 192, 512]; + + if (validateUpload(res, uploadedFile, allowedTypes)) { + try { + const imageObj = await file.saveFileToLocal('touchicon-orig.png', 'system', uploadedFile.path); + // Resize the image into squares for use as touch icons at various DPIs + for (const size of sizes) { + /* eslint-disable no-await-in-loop */ + await image.resizeImage({ + path: uploadedFile.path, + target: path.join(nconf.get('upload_path'), 'system', `touchicon-${size}.png`), + width: size, + height: size, + }); + } + res.json([{ name: uploadedFile.name, url: imageObj.url }]); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } + } +}; + + +uploadsController.uploadMaskableIcon = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + const allowedTypes = ['image/png']; + + if (validateUpload(res, uploadedFile, allowedTypes)) { + try { + const imageObj = await file.saveFileToLocal('maskableicon-orig.png', 'system', uploadedFile.path); + res.json([{ name: uploadedFile.name, url: imageObj.url }]); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } + } +}; + +uploadsController.uploadLogo = async function (req, res, next) { + await upload('site-logo', req, res, next); +}; + +uploadsController.uploadFile = async function (req, res, next) { + const uploadedFile = req.files.files[0]; + let params; + try { + params = JSON.parse(req.body.params); + } catch (e) { + file.delete(uploadedFile.path); + return next(new Error('[[error:invalid-json]]')); + } + + try { + const data = await file.saveFileToLocal(uploadedFile.name, params.folder, uploadedFile.path); + res.json([{ url: data.url }]); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } +}; + +uploadsController.uploadDefaultAvatar = async function (req, res, next) { + await upload('avatar-default', req, res, next); +}; + +uploadsController.uploadOgImage = async function (req, res, next) { + await upload('og:image', req, res, next); +}; + +async function upload(name, req, res, next) { + const uploadedFile = req.files.files[0]; + + if (validateUpload(res, uploadedFile, allowedImageTypes)) { + const filename = name + path.extname(uploadedFile.name); + await uploadImage(filename, 'system', uploadedFile, req, res, next); + } +} + +function validateUpload(res, uploadedFile, allowedTypes) { + if (!allowedTypes.includes(uploadedFile.type)) { + file.delete(uploadedFile.path); + res.json({ error: `[[error:invalid-image-type, ${allowedTypes.join(', ')}]]` }); + return false; + } + + return true; +} + +async function uploadImage(filename, folder, uploadedFile, req, res, next) { + let imageData; + try { + if (plugins.hooks.hasListeners('filter:uploadImage')) { + imageData = await plugins.hooks.fire('filter:uploadImage', { image: uploadedFile, uid: req.uid, folder: folder }); + } else { + imageData = await file.saveFileToLocal(filename, folder, uploadedFile.path); + } + + if (path.basename(filename, path.extname(filename)) === 'site-logo' && folder === 'system') { + const uploadPath = path.join(nconf.get('upload_path'), folder, 'site-logo-x50.png'); + await image.resizeImage({ + path: uploadedFile.path, + target: uploadPath, + height: 50, + }); + await meta.configs.set('brand:emailLogo', path.join(nconf.get('upload_url'), 'system/site-logo-x50.png')); + const size = await image.size(uploadedFile.path); + await meta.configs.setMultiple({ + 'brand:logo:width': size.width, + 'brand:logo:height': size.height, + }); + } else if (path.basename(filename, path.extname(filename)) === 'og:image' && folder === 'system') { + const size = await image.size(uploadedFile.path); + await meta.configs.setMultiple({ + 'og:image:width': size.width, + 'og:image:height': size.height, + }); + } + res.json([{ name: uploadedFile.name, url: imageData.url.startsWith('http') ? imageData.url : nconf.get('relative_path') + imageData.url }]); + } catch (err) { + next(err); + } finally { + file.delete(uploadedFile.path); + } +} diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js new file mode 100644 index 0000000000..7afc65c6ab --- /dev/null +++ b/src/controllers/admin/users.js @@ -0,0 +1,280 @@ +'use strict'; + +const validator = require('validator'); + +const user = require('../../user'); +const meta = require('../../meta'); +const db = require('../../database'); +const pagination = require('../../pagination'); +const events = require('../../events'); +const plugins = require('../../plugins'); +const privileges = require('../../privileges'); +const utils = require('../../utils'); + +const usersController = module.exports; + +const userFields = [ + 'uid', 'username', 'userslug', 'email', 'postcount', 'joindate', 'banned', + 'reputation', 'picture', 'flags', 'lastonline', 'email:confirmed', +]; + +usersController.index = async function (req, res) { + if (req.query.query) { + await usersController.search(req, res); + } else { + await getUsers(req, res); + } +}; + +async function getUsers(req, res) { + const sortDirection = req.query.sortDirection || 'desc'; + const reverse = sortDirection === 'desc'; + + const page = parseInt(req.query.page, 10) || 1; + let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50; + if (![50, 100, 250, 500].includes(resultsPerPage)) { + resultsPerPage = 50; + } + let sortBy = validator.escape(req.query.sortBy || ''); + const filterBy = Array.isArray(req.query.filters || []) ? (req.query.filters || []) : [req.query.filters]; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + + function buildSet() { + const sortToSet = { + postcount: 'users:postcount', + reputation: 'users:reputation', + joindate: 'users:joindate', + lastonline: 'users:online', + flags: 'users:flags', + }; + + const set = []; + if (sortBy) { + set.push(sortToSet[sortBy]); + } + if (filterBy.includes('unverified')) { + set.push('group:unverified-users:members'); + } + if (filterBy.includes('verified')) { + set.push('group:verified-users:members'); + } + if (filterBy.includes('banned')) { + set.push('users:banned'); + } + if (!set.length) { + set.push('users:online'); + sortBy = 'lastonline'; + } + return set.length > 1 ? set : set[0]; + } + + async function getCount(set) { + if (Array.isArray(set)) { + return await db.sortedSetIntersectCard(set); + } + return await db.sortedSetCard(set); + } + + async function getUids(set) { + let uids = []; + if (Array.isArray(set)) { + const weights = set.map((s, index) => (index ? 0 : 1)); + uids = await db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ + sets: set, + start: start, + stop: stop, + weights: weights, + }); + } else { + uids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop); + } + return uids; + } + + const set = buildSet(); + const uids = await getUids(set); + const [count, users] = await Promise.all([ + getCount(set), + loadUserInfo(req.uid, uids), + ]); + + await render(req, res, { + users: users.filter(user => user && parseInt(user.uid, 10)), + page: page, + pageCount: Math.max(1, Math.ceil(count / resultsPerPage)), + resultsPerPage: resultsPerPage, + reverse: reverse, + sortBy: sortBy, + }); +} + +usersController.search = async function (req, res) { + const sortDirection = req.query.sortDirection || 'desc'; + const reverse = sortDirection === 'desc'; + const page = parseInt(req.query.page, 10) || 1; + let resultsPerPage = parseInt(req.query.resultsPerPage, 10) || 50; + if (![50, 100, 250, 500].includes(resultsPerPage)) { + resultsPerPage = 50; + } + + const searchData = await user.search({ + uid: req.uid, + query: req.query.query, + searchBy: req.query.searchBy, + sortBy: req.query.sortBy, + sortDirection: sortDirection, + filters: req.query.filters, + page: page, + resultsPerPage: resultsPerPage, + findUids: async function (query, searchBy, hardCap) { + if (!query || query.length < 2) { + return []; + } + query = String(query).toLowerCase(); + if (!query.endsWith('*')) { + query += '*'; + } + + const data = await db.getSortedSetScan({ + key: `${searchBy}:sorted`, + match: query, + limit: hardCap || (resultsPerPage * 10), + }); + return data.map(data => data.split(':').pop()); + }, + }); + + const uids = searchData.users.map(user => user && user.uid); + searchData.users = await loadUserInfo(req.uid, uids); + if (req.query.searchBy === 'ip') { + searchData.users.forEach((user) => { + user.ip = user.ips.find(ip => ip.includes(String(req.query.query))); + }); + } + searchData.query = validator.escape(String(req.query.query || '')); + searchData.page = page; + searchData.resultsPerPage = resultsPerPage; + searchData.sortBy = req.query.sortBy; + searchData.reverse = reverse; + await render(req, res, searchData); +}; + +async function loadUserInfo(callerUid, uids) { + async function getIPs() { + return await Promise.all(uids.map(uid => db.getSortedSetRevRange(`uid:${uid}:ip`, 0, -1))); + } + const [isAdmin, userData, lastonline, ips] = await Promise.all([ + user.isAdministrator(uids), + user.getUsersWithFields(uids, userFields, callerUid), + db.sortedSetScores('users:online', uids), + getIPs(), + ]); + userData.forEach((user, index) => { + if (user) { + user.administrator = isAdmin[index]; + user.flags = userData[index].flags || 0; + const timestamp = lastonline[index] || user.joindate; + user.lastonline = timestamp; + user.lastonlineISO = utils.toISOString(timestamp); + user.ips = ips[index]; + user.ip = ips[index] && ips[index][0] ? ips[index][0] : null; + } + }); + return userData; +} + +usersController.registrationQueue = async function (req, res) { + const page = parseInt(req.query.page, 10) || 1; + const itemsPerPage = 20; + const start = (page - 1) * 20; + const stop = start + itemsPerPage - 1; + + const data = await utils.promiseParallel({ + registrationQueueCount: db.sortedSetCard('registration:queue'), + users: user.getRegistrationQueue(start, stop), + customHeaders: plugins.hooks.fire('filter:admin.registrationQueue.customHeaders', { headers: [] }), + invites: getInvites(), + }); + const pageCount = Math.max(1, Math.ceil(data.registrationQueueCount / itemsPerPage)); + data.pagination = pagination.create(page, pageCount); + data.customHeaders = data.customHeaders.headers; + res.render('admin/manage/registration', data); +}; + +async function getInvites() { + const invitations = await user.getAllInvites(); + const uids = invitations.map(invite => invite.uid); + let usernames = await user.getUsersFields(uids, ['username']); + usernames = usernames.map(user => user.username); + + invitations.forEach((invites, index) => { + invites.username = usernames[index]; + }); + + async function getUsernamesByEmails(emails) { + const uids = await db.sortedSetScores('email:uid', emails.map(email => String(email).toLowerCase())); + const usernames = await user.getUsersFields(uids, ['username']); + return usernames.map(user => user.username); + } + + usernames = await Promise.all(invitations.map(invites => getUsernamesByEmails(invites.invitations))); + + invitations.forEach((invites, index) => { + invites.invitations = invites.invitations.map((email, i) => ({ + email: email, + username: usernames[index][i] === '[[global:guest]]' ? '' : usernames[index][i], + })); + }); + return invitations; +} + +async function render(req, res, data) { + data.pagination = pagination.create(data.page, data.pageCount, req.query); + + const { registrationType } = meta.config; + + data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; + data.adminInviteOnly = registrationType === 'admin-invite-only'; + data[`sort_${data.sortBy}`] = true; + if (req.query.searchBy) { + data[`searchBy_${validator.escape(String(req.query.searchBy))}`] = true; + } + const filterBy = Array.isArray(req.query.filters || []) ? (req.query.filters || []) : [req.query.filters]; + filterBy.forEach((filter) => { + data[`filterBy_${validator.escape(String(filter))}`] = true; + }); + data.userCount = parseInt(await db.getObjectField('global', 'userCount'), 10); + if (data.adminInviteOnly) { + data.showInviteButton = await privileges.users.isAdministrator(req.uid); + } else { + data.showInviteButton = await privileges.users.hasInvitePrivilege(req.uid); + } + + res.render('admin/manage/users', data); +} + +usersController.getCSV = async function (req, res, next) { + await events.log({ + type: 'getUsersCSV', + uid: req.uid, + ip: req.ip, + }); + const path = require('path'); + const { baseDir } = require('../../constants').paths; + res.sendFile('users.csv', { + root: path.join(baseDir, 'build/export'), + headers: { + 'Content-Type': 'text/csv', + 'Content-Disposition': 'attachment; filename=users.csv', + }, + }, (err) => { + if (err) { + if (err.code === 'ENOENT') { + res.locals.isAPI = false; + return next(); + } + return next(err); + } + }); +}; diff --git a/src/controllers/admin/widgets.js b/src/controllers/admin/widgets.js new file mode 100644 index 0000000000..8a4896368f --- /dev/null +++ b/src/controllers/admin/widgets.js @@ -0,0 +1,9 @@ +'use strict'; + +const widgetsController = module.exports; +const admin = require('../../widgets/admin'); + +widgetsController.get = async function (req, res) { + const data = await admin.get(); + res.render('admin/extend/widgets', data); +}; diff --git a/src/controllers/api.js b/src/controllers/api.js new file mode 100644 index 0000000000..1398c45716 --- /dev/null +++ b/src/controllers/api.js @@ -0,0 +1,131 @@ +'use strict'; + +const validator = require('validator'); +const nconf = require('nconf'); + +const meta = require('../meta'); +const user = require('../user'); +const categories = require('../categories'); +const plugins = require('../plugins'); +const translator = require('../translator'); +const languages = require('../languages'); + +const apiController = module.exports; + +const relative_path = nconf.get('relative_path'); +const upload_url = nconf.get('upload_url'); +const asset_base_url = nconf.get('asset_base_url'); +const socketioTransports = nconf.get('socket.io:transports') || ['polling', 'websocket']; +const socketioOrigins = nconf.get('socket.io:origins'); +const websocketAddress = nconf.get('socket.io:address') || ''; + +apiController.loadConfig = async function (req) { + const config = { + relative_path, + upload_url, + asset_base_url, + assetBaseUrl: asset_base_url, // deprecate in 1.20.x + siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')), + browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')), + titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'), + showSiteTitle: meta.config.showSiteTitle === 1, + maintenanceMode: meta.config.maintenanceMode === 1, + minimumTitleLength: meta.config.minimumTitleLength, + maximumTitleLength: meta.config.maximumTitleLength, + minimumPostLength: meta.config.minimumPostLength, + maximumPostLength: meta.config.maximumPostLength, + minimumTagsPerTopic: meta.config.minimumTagsPerTopic || 0, + maximumTagsPerTopic: meta.config.maximumTagsPerTopic || 5, + minimumTagLength: meta.config.minimumTagLength || 3, + maximumTagLength: meta.config.maximumTagLength || 15, + undoTimeout: meta.config.undoTimeout || 0, + useOutgoingLinksPage: meta.config.useOutgoingLinksPage === 1, + outgoingLinksWhitelist: meta.config.useOutgoingLinksPage === 1 ? meta.config['outgoingLinks:whitelist'] : undefined, + allowGuestHandles: meta.config.allowGuestHandles === 1, + allowTopicsThumbnail: meta.config.allowTopicsThumbnail === 1, + usePagination: meta.config.usePagination === 1, + disableChat: meta.config.disableChat === 1, + disableChatMessageEditing: meta.config.disableChatMessageEditing === 1, + maximumChatMessageLength: meta.config.maximumChatMessageLength || 1000, + socketioTransports, + socketioOrigins, + websocketAddress, + maxReconnectionAttempts: meta.config.maxReconnectionAttempts, + reconnectionDelay: meta.config.reconnectionDelay, + topicsPerPage: meta.config.topicsPerPage || 20, + postsPerPage: meta.config.postsPerPage || 20, + maximumFileSize: meta.config.maximumFileSize, + 'theme:id': meta.config['theme:id'], + 'theme:src': meta.config['theme:src'], + defaultLang: meta.config.defaultLang || 'en-GB', + userLang: req.query.lang ? validator.escape(String(req.query.lang)) : (meta.config.defaultLang || 'en-GB'), + loggedIn: !!req.user, + uid: req.uid, + 'cache-buster': meta.config['cache-buster'] || '', + topicPostSort: meta.config.topicPostSort || 'oldest_to_newest', + categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest', + csrf_token: req.uid >= 0 && req.csrfToken && req.csrfToken(), + searchEnabled: plugins.hooks.hasListeners('filter:search.query'), + searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles', + bootswatchSkin: meta.config.bootswatchSkin || '', + enablePostHistory: meta.config.enablePostHistory === 1, + timeagoCutoff: meta.config.timeagoCutoff !== '' ? Math.max(0, parseInt(meta.config.timeagoCutoff, 10)) : meta.config.timeagoCutoff, + timeagoCodes: languages.timeagoCodes, + cookies: { + enabled: meta.config.cookieConsentEnabled === 1, + message: translator.escape(validator.escape(meta.config.cookieConsentMessage || '[[global:cookies.message]]')).replace(/\\/g, '\\\\'), + dismiss: translator.escape(validator.escape(meta.config.cookieConsentDismiss || '[[global:cookies.accept]]')).replace(/\\/g, '\\\\'), + link: translator.escape(validator.escape(meta.config.cookieConsentLink || '[[global:cookies.learn_more]]')).replace(/\\/g, '\\\\'), + link_url: translator.escape(validator.escape(meta.config.cookieConsentLinkUrl || 'https://www.cookiesandyou.com')).replace(/\\/g, '\\\\'), + }, + thumbs: { + size: meta.config.topicThumbSize, + }, + iconBackgrounds: await user.getIconBackgrounds(req.uid), + emailPrompt: meta.config.emailPrompt, + useragent: req.useragent, + }; + + let settings = config; + let isAdminOrGlobalMod; + if (req.loggedIn) { + ([settings, isAdminOrGlobalMod] = await Promise.all([ + user.getSettings(req.uid), + user.isAdminOrGlobalMod(req.uid), + ])); + } + + // Handle old skin configs + const oldSkins = ['noskin', 'default']; + settings.bootswatchSkin = oldSkins.includes(settings.bootswatchSkin) ? '' : settings.bootswatchSkin; + + config.usePagination = settings.usePagination; + config.topicsPerPage = settings.topicsPerPage; + config.postsPerPage = settings.postsPerPage; + config.userLang = validator.escape( + String((req.query.lang ? req.query.lang : null) || settings.userLang || config.defaultLang) + ); + config.acpLang = validator.escape(String((req.query.lang ? req.query.lang : null) || settings.acpLang)); + config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; + config.topicPostSort = settings.topicPostSort || config.topicPostSort; + config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; + config.topicSearchEnabled = settings.topicSearchEnabled || false; + config.bootswatchSkin = (meta.config.disableCustomUserSkins !== 1 && settings.bootswatchSkin && settings.bootswatchSkin !== '') ? settings.bootswatchSkin : ''; + + // Overrides based on privilege + config.disableChatMessageEditing = isAdminOrGlobalMod ? false : config.disableChatMessageEditing; + + return await plugins.hooks.fire('filter:config.get', config); +}; + +apiController.getConfig = async function (req, res) { + const config = await apiController.loadConfig(req); + res.json(config); +}; + +apiController.getModerators = async function (req, res) { + const moderators = await categories.getModerators(req.params.cid); + res.json({ moderators: moderators }); +}; + +require('../promisify')(apiController, ['getConfig', 'getObject', 'getModerators']); diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js new file mode 100644 index 0000000000..ecd8e6a731 --- /dev/null +++ b/src/controllers/authentication.js @@ -0,0 +1,510 @@ +'use strict'; + +const winston = require('winston'); +const passport = require('passport'); +const nconf = require('nconf'); +const validator = require('validator'); +const _ = require('lodash'); +const util = require('util'); + +const db = require('../database'); +const meta = require('../meta'); +const analytics = require('../analytics'); +const user = require('../user'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const slugify = require('../slugify'); +const helpers = require('./helpers'); +const privileges = require('../privileges'); +const sockets = require('../socket.io'); + +const authenticationController = module.exports; + +async function registerAndLoginUser(req, res, userData) { + if (!userData.hasOwnProperty('email')) { + userData.updateEmail = true; + } + + const data = await plugins.hooks.fire('filter:register.interstitial', { + req, + userData, + interstitials: [], + }); + + // If interstitials are found, save registration attempt into session and abort + const deferRegistration = data.interstitials.length; + + if (deferRegistration) { + userData.register = true; + req.session.registration = userData; + + if (req.body.noscript === 'true') { + res.redirect(`${nconf.get('relative_path')}/register/complete`); + return; + } + res.json({ next: `${nconf.get('relative_path')}/register/complete` }); + return; + } + const queue = await user.shouldQueueUser(req.ip); + const result = await plugins.hooks.fire('filter:register.shouldQueue', { req: req, res: res, userData: userData, queue: queue }); + if (result.queue) { + return await addToApprovalQueue(req, userData); + } + + const uid = await user.create(userData); + if (res.locals.processLogin) { + await authenticationController.doLogin(req, uid); + } + + // Distinguish registrations through invites from direct ones + if (userData.token) { + // Token has to be verified at this point + await Promise.all([ + user.confirmIfInviteEmailIsUsed(userData.token, userData.email, uid), + user.joinGroupsFromInvitation(uid, userData.token), + ]); + } + await user.deleteInvitationKey(userData.email, userData.token); + const next = req.session.returnTo || `${nconf.get('relative_path')}/`; + const complete = await plugins.hooks.fire('filter:register.complete', { uid: uid, next: next }); + req.session.returnTo = complete.next; + return complete; +} + +authenticationController.register = async function (req, res) { + const registrationType = meta.config.registrationType || 'normal'; + + if (registrationType === 'disabled') { + return res.sendStatus(403); + } + + const userData = req.body; + try { + if (userData.token || registrationType === 'invite-only' || registrationType === 'admin-invite-only') { + await user.verifyInvitation(userData); + } + + if ( + !userData.username || + userData.username.length < meta.config.minimumUsernameLength || + slugify(userData.username).length < meta.config.minimumUsernameLength + ) { + throw new Error('[[error:username-too-short]]'); + } + + if (userData.username.length > meta.config.maximumUsernameLength) { + throw new Error('[[error:username-too-long]]'); + } + + if (userData.password !== userData['password-confirm']) { + throw new Error('[[user:change_password_error_match]]'); + } + + if (userData.password.length > 512) { + throw new Error('[[error:password-too-long]]'); + } + + if (!userData['account-type'] || + (userData['account-type'] !== 'student' && userData['account-type'] !== 'instructor')) { + throw new Error('Invalid account type'); + } + + user.isPasswordValid(userData.password); + + res.locals.processLogin = true; // set it to false in plugin if you wish to just register only + await plugins.hooks.fire('filter:register.check', { req: req, res: res, userData: userData }); + + const data = await registerAndLoginUser(req, res, userData); + if (data) { + if (data.uid && req.body.userLang) { + await user.setSetting(data.uid, 'userLang', req.body.userLang); + } + res.json(data); + } + } catch (err) { + helpers.noScriptErrors(req, res, err.message, 400); + } +}; + +async function addToApprovalQueue(req, userData) { + userData.ip = req.ip; + await user.addToApprovalQueue(userData); + let message = '[[register:registration-added-to-queue]]'; + if (meta.config.showAverageApprovalTime) { + const average_time = await db.getObjectField('registration:queue:approval:times', 'average'); + if (average_time > 0) { + message += ` [[register:registration-queue-average-time, ${Math.floor(average_time / 60)}, ${Math.floor(average_time % 60)}]]`; + } + } + if (meta.config.autoApproveTime > 0) { + message += ` [[register:registration-queue-auto-approve-time, ${meta.config.autoApproveTime}]]`; + } + return { message: message }; +} + +authenticationController.registerComplete = async function (req, res) { + try { + // For the interstitials that respond, execute the callback with the form body + const data = await plugins.hooks.fire('filter:register.interstitial', { + req, + userData: req.session.registration, + interstitials: [], + }); + + const callbacks = data.interstitials.reduce((memo, cur) => { + if (cur.hasOwnProperty('callback') && typeof cur.callback === 'function') { + req.body.files = req.files; + if ( + (cur.callback.constructor && cur.callback.constructor.name === 'AsyncFunction') || + cur.callback.length === 2 // non-async function w/o callback + ) { + memo.push(cur.callback); + } else { + memo.push(util.promisify(cur.callback)); + } + } + + return memo; + }, []); + + const done = function (data) { + delete req.session.registration; + const relative_path = nconf.get('relative_path'); + if (data && data.message) { + return res.redirect(`${relative_path}/?register=${encodeURIComponent(data.message)}`); + } + + if (req.session.returnTo) { + res.redirect(relative_path + req.session.returnTo.replace(new RegExp(`^${relative_path}`), '')); + } else { + res.redirect(`${relative_path}/`); + } + }; + + const results = await Promise.allSettled(callbacks.map(async (cb) => { + await cb(req.session.registration, req.body); + })); + const errors = results.map(result => result.status === 'rejected' && result.reason && result.reason.message).filter(Boolean); + if (errors.length) { + req.flash('errors', errors); + return req.session.save(() => { + res.redirect(`${nconf.get('relative_path')}/register/complete`); + }); + } + + if (req.session.registration.register === true) { + res.locals.processLogin = true; + req.body.noscript = 'true'; // trigger full page load on error + + const data = await registerAndLoginUser(req, res, req.session.registration); + if (!data) { + return winston.warn('[register] Interstitial callbacks processed with no errors, but one or more interstitials remain. This is likely an issue with one of the interstitials not properly handling a null case or invalid value.'); + } + done(data); + } else { + // Update user hash, clear registration data in session + const payload = req.session.registration; + const { uid } = payload; + delete payload.uid; + delete payload.returnTo; + + Object.keys(payload).forEach((prop) => { + if (typeof payload[prop] === 'boolean') { + payload[prop] = payload[prop] ? 1 : 0; + } + }); + + await user.setUserFields(uid, payload); + done(); + } + } catch (err) { + delete req.session.registration; + res.redirect(`${nconf.get('relative_path')}/?register=${encodeURIComponent(err.message)}`); + } +}; + +authenticationController.registerAbort = function (req, res) { + if (req.uid) { + // Clear interstitial data and continue on... + delete req.session.registration; + res.redirect(nconf.get('relative_path') + (req.session.returnTo || '/')); + } else { + // End the session and redirect to home + req.session.destroy(() => { + res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get()); + res.redirect(`${nconf.get('relative_path')}/`); + }); + } +}; + +authenticationController.login = async (req, res, next) => { + let { strategy } = await plugins.hooks.fire('filter:login.override', { req, strategy: 'local' }); + if (!passport._strategy(strategy)) { + winston.error(`[auth/override] Requested login strategy "${strategy}" not found, reverting back to local login strategy.`); + strategy = 'local'; + } + + if (plugins.hooks.hasListeners('action:auth.overrideLogin')) { + return continueLogin(strategy, req, res, next); + } + + const loginWith = meta.config.allowLoginWith || 'username-email'; + req.body.username = String(req.body.username).trim(); + const errorHandler = res.locals.noScriptErrors || helpers.noScriptErrors; + try { + await plugins.hooks.fire('filter:login.check', { req: req, res: res, userData: req.body }); + } catch (err) { + return errorHandler(req, res, err.message, 403); + } + try { + const isEmailLogin = loginWith.includes('email') && req.body.username && utils.isEmailValid(req.body.username); + const isUsernameLogin = loginWith.includes('username') && !validator.isEmail(req.body.username); + if (isEmailLogin) { + const username = await user.getUsernameByEmail(req.body.username); + if (username !== '[[global:guest]]') { + req.body.username = username; + } + } + if (isEmailLogin || isUsernameLogin) { + continueLogin(strategy, req, res, next); + } else { + errorHandler(req, res, `[[error:wrong-login-type-${loginWith}]]`, 400); + } + } catch (err) { + return errorHandler(req, res, err.message, 500); + } +}; + +function continueLogin(strategy, req, res, next) { + passport.authenticate(strategy, async (err, userData, info) => { + if (err) { + plugins.hooks.fire('action:login.continue', { req, strategy, userData, error: err }); + return helpers.noScriptErrors(req, res, err.data || err.message, 403); + } + + if (!userData) { + if (info instanceof Error) { + info = info.message; + } else if (typeof info === 'object') { + info = '[[error:invalid-username-or-password]]'; + } + + plugins.hooks.fire('action:login.continue', { req, strategy, userData, error: new Error(info) }); + return helpers.noScriptErrors(req, res, info, 403); + } + + // Alter user cookie depending on passed-in option + if (req.body.remember === 'on') { + const duration = meta.getSessionTTLSeconds() * 1000; + req.session.cookie.maxAge = duration; + req.session.cookie.expires = new Date(Date.now() + duration); + } else { + req.session.cookie.maxAge = false; + req.session.cookie.expires = false; + } + + plugins.hooks.fire('action:login.continue', { req, strategy, userData, error: null }); + + if (userData.passwordExpiry && userData.passwordExpiry < Date.now()) { + winston.verbose(`[auth] Triggering password reset for uid ${userData.uid} due to password policy`); + req.session.passwordExpired = true; + + const code = await user.reset.generate(userData.uid); + (res.locals.redirectAfterLogin || redirectAfterLogin)(req, res, `${nconf.get('relative_path')}/reset/${code}`); + } else { + delete req.query.lang; + await authenticationController.doLogin(req, userData.uid); + let destination; + if (req.session.returnTo) { + destination = req.session.returnTo.startsWith('http') ? + req.session.returnTo : + nconf.get('relative_path') + req.session.returnTo; + delete req.session.returnTo; + } else { + destination = `${nconf.get('relative_path')}/`; + } + + (res.locals.redirectAfterLogin || redirectAfterLogin)(req, res, destination); + } + })(req, res, next); +} + +function redirectAfterLogin(req, res, destination) { + if (req.body.noscript === 'true') { + res.redirect(`${destination}?loggedin`); + } else { + res.status(200).send({ + next: destination, + }); + } +} + +authenticationController.doLogin = async function (req, uid) { + if (!uid) { + return; + } + const loginAsync = util.promisify(req.login).bind(req); + await loginAsync({ uid: uid }, { keepSessionInfo: req.res.locals !== false }); + await authenticationController.onSuccessfulLogin(req, uid); +}; + +authenticationController.onSuccessfulLogin = async function (req, uid) { + /* + * 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; + } + + try { + const uuid = utils.generateUUID(); + + req.uid = uid; + req.loggedIn = true; + await meta.blacklist.test(req.ip); + await user.logIP(uid, req.ip); + await user.bans.unbanIfExpired([uid]); + await user.reset.cleanByUid(uid); + + req.session.meta = {}; + + delete req.session.forceLogin; + // Associate IP used during login with user account + req.session.meta.ip = req.ip; + + // Associate metadata retrieved via user-agent + req.session.meta = _.extend(req.session.meta, { + uuid: uuid, + datetime: Date.now(), + platform: req.useragent.platform, + browser: req.useragent.browser, + version: req.useragent.version, + }); + await Promise.all([ + new Promise((resolve) => { + req.session.save(resolve); + }), + user.auth.addSession(uid, req.sessionID), + user.updateLastOnlineTime(uid), + user.updateOnlineUsers(uid), + analytics.increment('logins'), + db.incrObjectFieldBy('global', 'loginCount', 1), + ]); + if (uid > 0) { + await db.setObjectField(`uid:${uid}:sessionUUID:sessionId`, uuid, req.sessionID); + } + + // Force session check for all connected socket.io clients with the same session id + sockets.in(`sess_${req.sessionID}`).emit('checkSession', uid); + + plugins.hooks.fire('action:user.loggedIn', { uid: uid, req: req }); + } catch (err) { + req.session.destroy(); + throw err; + } +}; + +authenticationController.localLogin = async function (req, username, password, next) { + if (!username) { + return next(new Error('[[error:invalid-username]]')); + } + + if (!password || !utils.isPasswordValid(password)) { + return next(new Error('[[error:invalid-password]]')); + } + + if (password.length > 512) { + return next(new Error('[[error:password-too-long]]')); + } + + const userslug = slugify(username); + const uid = await user.getUidByUserslug(userslug); + try { + const [userData, isAdminOrGlobalMod, canLoginIfBanned] = await Promise.all([ + user.getUserFields(uid, ['uid', 'passwordExpiry']), + user.isAdminOrGlobalMod(uid), + user.bans.canLoginIfBanned(uid), + ]); + + userData.isAdminOrGlobalMod = isAdminOrGlobalMod; + + if (!canLoginIfBanned) { + return next(await getBanError(uid)); + } + + // Doing this after the ban check, because user's privileges might change after a ban expires + const hasLoginPrivilege = await privileges.global.can('local:login', uid); + if (parseInt(uid, 10) && !hasLoginPrivilege) { + return next(new Error('[[error:local-login-disabled]]')); + } + + const passwordMatch = await user.isPasswordCorrect(uid, password, req.ip); + if (!passwordMatch) { + return next(new Error('[[error:invalid-login-credentials]]')); + } + + next(null, userData, '[[success:authentication-successful]]'); + } catch (err) { + next(err); + } +}; + +const destroyAsync = util.promisify((req, callback) => req.session.destroy(callback)); +const logoutAsync = util.promisify((req, callback) => req.logout(callback)); + +authenticationController.logout = async function (req, res, next) { + if (!req.loggedIn || !req.sessionID) { + res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get()); + return res.status(200).send('not-logged-in'); + } + const { uid } = req; + const { sessionID } = req; + + try { + await user.auth.revokeSession(sessionID, uid); + await logoutAsync(req); + + await destroyAsync(req); + res.clearCookie(nconf.get('sessionKey'), meta.configs.cookie.get()); + + await user.setUserField(uid, 'lastonline', Date.now() - (meta.config.onlineCutoff * 60000)); + await db.sortedSetAdd('users:online', Date.now() - (meta.config.onlineCutoff * 60000), uid); + await plugins.hooks.fire('static:user.loggedOut', { req: req, res: res, uid: uid, sessionID: sessionID }); + + // Force session check for all connected socket.io clients with the same session id + sockets.in(`sess_${sessionID}`).emit('checkSession', 0); + const payload = { + next: `${nconf.get('relative_path')}/`, + }; + plugins.hooks.fire('filter:user.logout', payload); + + if (req.body.noscript === 'true') { + return res.redirect(payload.next); + } + res.status(200).send(payload); + } catch (err) { + next(err); + } +}; + +async function getBanError(uid) { + try { + const banInfo = await user.getLatestBanInfo(uid); + + if (!banInfo.reason) { + banInfo.reason = '[[user:info.banned-no-reason]]'; + } + const err = new Error(banInfo.reason); + err.data = banInfo; + return err; + } catch (err) { + if (err.message === 'no-ban-info') { + return new Error('[[error:user-banned]]'); + } + throw err; + } +} + +require('../promisify')(authenticationController, ['register', 'registerComplete', 'registerAbort', 'login', 'localLogin', 'logout']); diff --git a/src/controllers/career.js b/src/controllers/career.js new file mode 100644 index 0000000000..beb8b6e83c --- /dev/null +++ b/src/controllers/career.js @@ -0,0 +1,8 @@ +'use strict'; + +const careerController = module.exports; + +careerController.get = async function (req, res) { + const careerData = {}; + res.render('career', careerData); +}; diff --git a/src/controllers/categories.js b/src/controllers/categories.js new file mode 100644 index 0000000000..88925f9935 --- /dev/null +++ b/src/controllers/categories.js @@ -0,0 +1,61 @@ +'use strict'; + +const nconf = require('nconf'); +const _ = require('lodash'); + +const categories = require('../categories'); +const meta = require('../meta'); +const pagination = require('../pagination'); +const helpers = require('./helpers'); +const privileges = require('../privileges'); + +const categoriesController = module.exports; + +categoriesController.list = async function (req, res) { + res.locals.metaTags = [{ + name: 'title', + content: String(meta.config.title || 'NodeBB'), + }, { + property: 'og:type', + content: 'website', + }]; + + const allRootCids = await categories.getAllCidsFromSet('cid:0:children'); + const rootCids = await privileges.categories.filterCids('find', allRootCids, req.uid); + const pageCount = Math.max(1, Math.ceil(rootCids.length / meta.config.categoriesPerPage)); + const page = Math.min(parseInt(req.query.page, 10) || 1, pageCount); + const start = Math.max(0, (page - 1) * meta.config.categoriesPerPage); + const stop = start + meta.config.categoriesPerPage - 1; + const pageCids = rootCids.slice(start, stop + 1); + + const allChildCids = _.flatten(await Promise.all(pageCids.map(categories.getChildrenCids))); + const childCids = await privileges.categories.filterCids('find', allChildCids, req.uid); + const categoryData = await categories.getCategories(pageCids.concat(childCids), req.uid); + const tree = categories.getTree(categoryData, 0); + await categories.getRecentTopicReplies(categoryData, req.uid, req.query); + + const data = { + title: meta.config.homePageTitle || '[[pages:home]]', + selectCategoryLabel: '[[pages:categories]]', + categories: tree, + pagination: pagination.create(page, pageCount, req.query), + }; + + data.categories.forEach((category) => { + if (category) { + helpers.trimChildren(category); + helpers.setCategoryTeaser(category); + } + }); + + if (req.originalUrl.startsWith(`${nconf.get('relative_path')}/api/categories`) || req.originalUrl.startsWith(`${nconf.get('relative_path')}/categories`)) { + data.title = '[[pages:categories]]'; + data.breadcrumbs = helpers.buildBreadcrumbs([{ text: data.title }]); + res.locals.metaTags.push({ + property: 'og:title', + content: '[[pages:categories]]', + }); + } + + res.render('categories', data); +}; diff --git a/src/controllers/category.js b/src/controllers/category.js new file mode 100644 index 0000000000..5d9a59b073 --- /dev/null +++ b/src/controllers/category.js @@ -0,0 +1,206 @@ +'use strict'; + + +const nconf = require('nconf'); +const validator = require('validator'); +const qs = require('querystring'); + +const db = require('../database'); +const privileges = require('../privileges'); +const user = require('../user'); +const categories = require('../categories'); +const meta = require('../meta'); +const pagination = require('../pagination'); +const helpers = require('./helpers'); +const utils = require('../utils'); +const translator = require('../translator'); +const analytics = require('../analytics'); + +const categoryController = module.exports; + +const url = nconf.get('url'); +const relative_path = nconf.get('relative_path'); + +categoryController.get = async function (req, res, next) { + const cid = req.params.category_id; + + let currentPage = parseInt(req.query.page, 10) || 1; + let topicIndex = utils.isNumber(req.params.topic_index) ? parseInt(req.params.topic_index, 10) - 1 : 0; + if ((req.params.topic_index && !utils.isNumber(req.params.topic_index)) || !utils.isNumber(cid)) { + return next(); + } + + const [categoryFields, userPrivileges, userSettings, rssToken] = await Promise.all([ + categories.getCategoryFields(cid, ['slug', 'disabled', 'link']), + privileges.categories.get(cid, req.uid), + user.getSettings(req.uid), + user.auth.getFeedToken(req.uid), + ]); + + if (!categoryFields.slug || + (categoryFields && categoryFields.disabled) || + (userSettings.usePagination && currentPage < 1)) { + return next(); + } + if (topicIndex < 0) { + return helpers.redirect(res, `/category/${categoryFields.slug}?${qs.stringify(req.query)}`); + } + + if (!userPrivileges.read) { + return helpers.notAllowed(req, res); + } + + if (!res.locals.isAPI && !req.params.slug && (categoryFields.slug && categoryFields.slug !== `${cid}/`)) { + return helpers.redirect(res, `/category/${categoryFields.slug}?${qs.stringify(req.query)}`, true); + } + + if (categoryFields.link) { + await db.incrObjectField(`category:${cid}`, 'timesClicked'); + return helpers.redirect(res, validator.unescape(categoryFields.link)); + } + + if (!userSettings.usePagination) { + topicIndex = Math.max(0, topicIndex - (Math.ceil(userSettings.topicsPerPage / 2) - 1)); + } else if (!req.query.page) { + const index = Math.max(parseInt((topicIndex || 0), 10), 0); + currentPage = Math.ceil((index + 1) / userSettings.topicsPerPage); + topicIndex = 0; + } + + const targetUid = await user.getUidByUserslug(req.query.author); + const start = ((currentPage - 1) * userSettings.topicsPerPage) + topicIndex; + const stop = start + userSettings.topicsPerPage - 1; + + const categoryData = await categories.getCategoryById({ + uid: req.uid, + cid: cid, + start: start, + stop: stop, + sort: req.query.sort || userSettings.categoryTopicSort, + settings: userSettings, + query: req.query, + tag: req.query.tag, + targetUid: targetUid, + }); + if (!categoryData) { + return next(); + } + + if (topicIndex > Math.max(categoryData.topic_count - 1, 0)) { + return helpers.redirect(res, `/category/${categoryData.slug}/${categoryData.topic_count}?${qs.stringify(req.query)}`); + } + const pageCount = Math.max(1, Math.ceil(categoryData.topic_count / userSettings.topicsPerPage)); + if (userSettings.usePagination && currentPage > pageCount) { + return next(); + } + + categories.modifyTopicsByPrivilege(categoryData.topics, userPrivileges); + categoryData.tagWhitelist = categories.filterTagWhitelist(categoryData.tagWhitelist, userPrivileges.isAdminOrMod); + + await buildBreadcrumbs(req, categoryData); + if (categoryData.children.length) { + const allCategories = []; + categories.flattenCategories(allCategories, categoryData.children); + await categories.getRecentTopicReplies(allCategories, req.uid, req.query); + categoryData.subCategoriesLeft = Math.max(0, categoryData.children.length - categoryData.subCategoriesPerPage); + categoryData.hasMoreSubCategories = categoryData.children.length > categoryData.subCategoriesPerPage; + categoryData.nextSubCategoryStart = categoryData.subCategoriesPerPage; + categoryData.children = categoryData.children.slice(0, categoryData.subCategoriesPerPage); + categoryData.children.forEach((child) => { + if (child) { + helpers.trimChildren(child); + helpers.setCategoryTeaser(child); + } + }); + } + + categoryData.title = translator.escape(categoryData.name); + categoryData.selectCategoryLabel = '[[category:subcategories]]'; + categoryData.description = translator.escape(categoryData.description); + categoryData.privileges = userPrivileges; + categoryData.showSelect = userPrivileges.editable; + categoryData.showTopicTools = userPrivileges.editable; + categoryData.topicIndex = topicIndex; + categoryData.rssFeedUrl = `${url}/category/${categoryData.cid}.rss`; + if (parseInt(req.uid, 10)) { + categories.markAsRead([cid], req.uid); + categoryData.rssFeedUrl += `?uid=${req.uid}&token=${rssToken}`; + } + + addTags(categoryData, res); + + categoryData['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0; + categoryData['reputation:disabled'] = meta.config['reputation:disabled']; + categoryData.pagination = pagination.create(currentPage, pageCount, req.query); + categoryData.pagination.rel.forEach((rel) => { + rel.href = `${url}/category/${categoryData.slug}${rel.href}`; + res.locals.linkTags.push(rel); + }); + + analytics.increment([`pageviews:byCid:${categoryData.cid}`]); + + res.render('category', categoryData); +}; + +async function buildBreadcrumbs(req, categoryData) { + const breadcrumbs = [ + { + text: categoryData.name, + url: `${relative_path}/category/${categoryData.slug}`, + cid: categoryData.cid, + }, + ]; + const crumbs = await helpers.buildCategoryBreadcrumbs(categoryData.parentCid); + if (req.originalUrl.startsWith(`${relative_path}/api/category`) || req.originalUrl.startsWith(`${relative_path}/category`)) { + categoryData.breadcrumbs = crumbs.concat(breadcrumbs); + } +} + +function addTags(categoryData, res) { + res.locals.metaTags = [ + { + name: 'title', + content: categoryData.name, + noEscape: true, + }, + { + property: 'og:title', + content: categoryData.name, + noEscape: true, + }, + { + name: 'description', + content: categoryData.description, + noEscape: true, + }, + { + property: 'og:type', + content: 'website', + }, + ]; + + if (categoryData.backgroundImage) { + if (!categoryData.backgroundImage.startsWith('http')) { + categoryData.backgroundImage = url + categoryData.backgroundImage; + } + res.locals.metaTags.push({ + property: 'og:image', + content: categoryData.backgroundImage, + }); + } + + res.locals.linkTags = [ + { + rel: 'up', + href: url, + }, + ]; + + if (!categoryData['feeds:disableRSS']) { + res.locals.linkTags.push({ + rel: 'alternate', + type: 'application/rss+xml', + href: categoryData.rssFeedUrl, + }); + } +} diff --git a/src/controllers/composer.js b/src/controllers/composer.js new file mode 100644 index 0000000000..1eca9e1667 --- /dev/null +++ b/src/controllers/composer.js @@ -0,0 +1,111 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.post = exports.get = void 0; +const nconf_1 = __importDefault(require("nconf")); +const user_1 = __importDefault(require("../user")); +const plugins_1 = __importDefault(require("../plugins")); +const topics_1 = __importDefault(require("../topics")); +const posts_1 = __importDefault(require("../posts")); +const helpers_1 = __importDefault(require("./helpers")); +function get(req, res, callback) { + return __awaiter(this, void 0, void 0, function* () { + res.locals.metaTags = Object.assign(Object.assign({}, res.locals.metaTags), { name: 'robots', content: 'noindex' }); + const data = yield plugins_1.default.hooks.fire('filter:composer.build', { + req: req, + res: res, + next: callback, + templateData: {}, + }); + if (res.headersSent) { + return; + } + if (!data || !data.templateData) { + return callback(new Error('[[error:invalid-data]]')); + } + if (data.templateData.disabled) { + res.render('', { + title: '[[modules:composer.compose]]', + }); + } + else { + data.templateData.title = '[[modules:composer.compose]]'; + res.render('compose', data.templateData); + } + }); +} +exports.get = get; +function post(req, res) { + return __awaiter(this, void 0, void 0, function* () { + const { body } = req; + const data = { + uid: req.uid, + req: req, + timestamp: Date.now(), + content: body.content, + fromQueue: false, + }; + req.body.noscript = 'true'; + if (!data.content) { + yield helpers_1.default.noScriptErrors(req, res, '[[error:invalid-data]]', 400); + return; + } + function queueOrPost(postFn, data) { + return __awaiter(this, void 0, void 0, function* () { + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const shouldQueue = yield posts_1.default.shouldQueue(req.uid, data); + if (shouldQueue) { + delete data.req; + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return yield posts_1.default.addToQueue(data); + } + return yield postFn(data); + }); + } + try { + let result; + if (body.tid) { + data.tid = body.tid; + result = yield queueOrPost(topics_1.default.reply, data); + } + else if (body.cid) { + data.cid = body.cid; + data.title = body.title; + data.tags = []; + data.thumb = ''; + result = yield queueOrPost(topics_1.default.post, data); + } + else { + throw new Error('[[error:invalid-data]]'); + } + if (result.queued) { + return res.redirect(`${nconf_1.default.get('relative_path') || '/'}?noScriptMessage=[[success:post-queued]]`); + } + const uid = result.uid ? result.uid : result.topicData.uid; + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + user_1.default.updateOnlineUsers(uid); + const path = result.pid ? `/post/${result.pid}` : `/topic/${result.topicData.slug}`; + res.redirect(nconf_1.default.get('relative_path') + path); + } + catch (err) { + if (err instanceof Error) { + yield helpers_1.default.noScriptErrors(req, res, err.message, 400); + } + } + }); +} +exports.post = post; diff --git a/src/controllers/composer.ts b/src/controllers/composer.ts new file mode 100644 index 0000000000..2000867109 --- /dev/null +++ b/src/controllers/composer.ts @@ -0,0 +1,138 @@ +import nconf from 'nconf'; + +import { Request, Response, NextFunction } from 'express'; +import { TopicObject } from '../types'; + +import user from '../user'; +import plugins from '../plugins'; +import topics from '../topics'; +import posts from '../posts'; +import helpers from './helpers'; + +type ComposerBuildData = { + templateData: TemplateData +} + +type TemplateData = { + title: string, + disabled: boolean +} + +type Locals = { + metaTags: { [key: string]: string }; +} + +export async function get(req: Request, res: Response, callback: NextFunction): Promise { + res.locals.metaTags = { + ...res.locals.metaTags, + name: 'robots', + content: 'noindex', + }; + + const data: ComposerBuildData = await plugins.hooks.fire('filter:composer.build', { + req: req, + res: res, + next: callback, + templateData: {}, + }) as ComposerBuildData; + + if (res.headersSent) { + return; + } + if (!data || !data.templateData) { + return callback(new Error('[[error:invalid-data]]')); + } + + if (data.templateData.disabled) { + res.render('', { + title: '[[modules:composer.compose]]', + }); + } else { + data.templateData.title = '[[modules:composer.compose]]'; + res.render('compose', data.templateData); + } +} + +type ComposerData = { + uid: number, + req: Request, + timestamp: number, + content: string, + fromQueue: boolean, + tid?: number, + cid?: number, + title?: string, + tags?: string[], + thumb?: string, + noscript?: string +} + +type QueueResult = { + uid: number, + queued: boolean, + topicData: TopicObject, + pid: number +} + +type PostFnType = (data: ComposerData) => Promise; + +export async function post(req: Request & { uid: number }, res: Response): Promise { + const { body } = req; + const data: ComposerData = { + uid: req.uid, + req: req, + timestamp: Date.now(), + content: body.content, + fromQueue: false, + }; + req.body.noscript = 'true'; + + if (!data.content) { + await helpers.noScriptErrors(req, res, '[[error:invalid-data]]', 400); + return; + } + async function queueOrPost(postFn: PostFnType, data: ComposerData): Promise { + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const shouldQueue: boolean = await posts.shouldQueue(req.uid, data) as boolean; + if (shouldQueue) { + delete data.req; + + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + return await posts.addToQueue(data) as QueueResult; + } + return await postFn(data); + } + + try { + let result: QueueResult; + if (body.tid) { + data.tid = body.tid; + result = await queueOrPost(topics.reply as PostFnType, data); + } else if (body.cid) { + data.cid = body.cid; + data.title = body.title; + data.tags = []; + data.thumb = ''; + result = await queueOrPost(topics.post as PostFnType, data); + } else { + throw new Error('[[error:invalid-data]]'); + } + if (result.queued) { + return res.redirect(`${nconf.get('relative_path') as string || '/'}?noScriptMessage=[[success:post-queued]]`); + } + const uid: number = result.uid ? result.uid : result.topicData.uid; + + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + user.updateOnlineUsers(uid); + + const path: string = result.pid ? `/post/${result.pid}` : `/topic/${result.topicData.slug}`; + res.redirect((nconf.get('relative_path') as string) + path); + } catch (err: unknown) { + if (err instanceof Error) { + await helpers.noScriptErrors(req, res, err.message, 400); + } + } +} diff --git a/src/controllers/errors.js b/src/controllers/errors.js new file mode 100644 index 0000000000..90df864451 --- /dev/null +++ b/src/controllers/errors.js @@ -0,0 +1,111 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); +const validator = require('validator'); +const translator = require('../translator'); +const plugins = require('../plugins'); +const middleware = require('../middleware'); +const middlewareHelpers = require('../middleware/helpers'); +const helpers = require('./helpers'); + +exports.handleURIErrors = async function handleURIErrors(err, req, res, next) { + // Handle cases where malformed URIs are passed in + if (err instanceof URIError) { + const cleanPath = req.path.replace(new RegExp(`^${nconf.get('relative_path')}`), ''); + const tidMatch = cleanPath.match(/^\/topic\/(\d+)\//); + const cidMatch = cleanPath.match(/^\/category\/(\d+)\//); + + if (tidMatch) { + res.redirect(nconf.get('relative_path') + tidMatch[0]); + } else if (cidMatch) { + res.redirect(nconf.get('relative_path') + cidMatch[0]); + } else { + winston.warn(`[controller] Bad request: ${req.path}`); + if (req.path.startsWith(`${nconf.get('relative_path')}/api`)) { + res.status(400).json({ + error: '[[global:400.title]]', + }); + } else { + await middleware.buildHeaderAsync(req, res); + res.status(400).render('400', { error: validator.escape(String(err.message)) }); + } + } + } else { + next(err); + } +}; + +// this needs to have four arguments or express treats it as `(req, res, next)` +// don't remove `next`! +exports.handleErrors = async function handleErrors(err, req, res, next) { // eslint-disable-line no-unused-vars + const cases = { + EBADCSRFTOKEN: function () { + winston.error(`${req.method} ${req.originalUrl}\n${err.message}`); + res.sendStatus(403); + }, + 'blacklisted-ip': function () { + res.status(403).type('text/plain').send(err.message); + }, + }; + const defaultHandler = async function () { + if (res.headersSent) { + return; + } + // Display NodeBB error page + const status = parseInt(err.status, 10); + if ((status === 302 || status === 308) && err.path) { + return res.locals.isAPI ? res.set('X-Redirect', err.path).status(200).json(err.path) : res.redirect(nconf.get('relative_path') + err.path); + } + + const path = String(req.path || ''); + + if (path.startsWith(`${nconf.get('relative_path')}/api/v3`)) { + let status = 500; + if (err.message.startsWith('[[')) { + status = 400; + err.message = await translator.translate(err.message); + } + return helpers.formatApiResponse(status, res, err); + } + + winston.error(`${req.method} ${req.originalUrl}\n${err.stack}`); + res.status(status || 500); + const data = { + path: validator.escape(path), + error: validator.escape(String(err.message)), + bodyClass: middlewareHelpers.buildBodyClass(req, res), + }; + if (res.locals.isAPI) { + res.json(data); + } else { + await middleware.buildHeaderAsync(req, res); + res.render('500', data); + } + }; + const data = await getErrorHandlers(cases); + try { + if (data.cases.hasOwnProperty(err.code)) { + data.cases[err.code](err, req, res, defaultHandler); + } else { + await defaultHandler(); + } + } catch (_err) { + winston.error(`${req.method} ${req.originalUrl}\n${_err.stack}`); + if (!res.headersSent) { + res.status(500).send(_err.message); + } + } +}; + +async function getErrorHandlers(cases) { + try { + return await plugins.hooks.fire('filter:error.handle', { + cases: cases, + }); + } catch (err) { + // Assume defaults + winston.warn(`[errors/handle] Unable to retrieve plugin handlers for errors: ${err.message}`); + return { cases }; + } +} diff --git a/src/controllers/globalmods.js b/src/controllers/globalmods.js new file mode 100644 index 0000000000..7534b4c16d --- /dev/null +++ b/src/controllers/globalmods.js @@ -0,0 +1,36 @@ +'use strict'; + +const user = require('../user'); +const meta = require('../meta'); +const analytics = require('../analytics'); +const usersController = require('./admin/users'); +const helpers = require('./helpers'); + +const globalModsController = module.exports; + +globalModsController.ipBlacklist = async function (req, res, next) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(req.uid); + if (!isAdminOrGlobalMod) { + return next(); + } + + const [rules, analyticsData] = await Promise.all([ + meta.blacklist.get(), + analytics.getBlacklistAnalytics(), + ]); + res.render('ip-blacklist', { + title: '[[pages:ip-blacklist]]', + rules: rules, + analytics: analyticsData, + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:ip-blacklist]]' }]), + }); +}; + + +globalModsController.registrationQueue = async function (req, res, next) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(req.uid); + if (!isAdminOrGlobalMod) { + return next(); + } + await usersController.registrationQueue(req, res); +}; diff --git a/src/controllers/groups.js b/src/controllers/groups.js new file mode 100644 index 0000000000..fdcb46155b --- /dev/null +++ b/src/controllers/groups.js @@ -0,0 +1,120 @@ +'use strict'; + +const validator = require('validator'); +const nconf = require('nconf'); + +const meta = require('../meta'); +const groups = require('../groups'); +const user = require('../user'); +const helpers = require('./helpers'); +const pagination = require('../pagination'); +const privileges = require('../privileges'); + +const groupsController = module.exports; + +groupsController.list = async function (req, res) { + const sort = req.query.sort || 'alpha'; + + const [groupData, allowGroupCreation] = await Promise.all([ + groups.getGroupsBySort(sort, 0, 14), + privileges.global.can('group:create', req.uid), + ]); + + res.render('groups/list', { + groups: groupData, + allowGroupCreation: allowGroupCreation, + nextStart: 15, + title: '[[pages:groups]]', + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:groups]]' }]), + }); +}; + +groupsController.details = async function (req, res, next) { + const lowercaseSlug = req.params.slug.toLowerCase(); + if (req.params.slug !== lowercaseSlug) { + if (res.locals.isAPI) { + req.params.slug = lowercaseSlug; + } else { + return res.redirect(`${nconf.get('relative_path')}/groups/${lowercaseSlug}`); + } + } + const groupName = await groups.getGroupNameByGroupSlug(req.params.slug); + if (!groupName) { + return next(); + } + const [exists, isHidden, isAdmin, isGlobalMod] = await Promise.all([ + groups.exists(groupName), + groups.isHidden(groupName), + user.isAdministrator(req.uid), + user.isGlobalModerator(req.uid), + ]); + if (!exists) { + return next(); + } + if (isHidden && !isAdmin && !isGlobalMod) { + const [isMember, isInvited] = await Promise.all([ + groups.isMember(req.uid, groupName), + groups.isInvited(req.uid, groupName), + ]); + if (!isMember && !isInvited) { + return next(); + } + } + const [groupData, posts] = await Promise.all([ + groups.get(groupName, { + uid: req.uid, + truncateUserList: true, + userListCount: 20, + }), + groups.getLatestMemberPosts(groupName, 10, req.uid), + ]); + if (!groupData) { + return next(); + } + groupData.isOwner = groupData.isOwner || isAdmin || (isGlobalMod && !groupData.system); + + res.render('groups/details', { + title: `[[pages:group, ${groupData.displayName}]]`, + group: groupData, + posts: posts, + isAdmin: isAdmin, + isGlobalMod: isGlobalMod, + allowPrivateGroups: meta.config.allowPrivateGroups, + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:groups]]', url: '/groups' }, { text: groupData.displayName }]), + }); +}; + +groupsController.members = async function (req, res, next) { + const page = parseInt(req.query.page, 10) || 1; + const usersPerPage = 50; + const start = Math.max(0, (page - 1) * usersPerPage); + const stop = start + usersPerPage - 1; + const groupName = await groups.getGroupNameByGroupSlug(req.params.slug); + if (!groupName) { + return next(); + } + const [groupData, isAdminOrGlobalMod, isMember, isHidden] = await Promise.all([ + groups.getGroupData(groupName), + user.isAdminOrGlobalMod(req.uid), + groups.isMember(req.uid, groupName), + groups.isHidden(groupName), + ]); + + if (isHidden && !isMember && !isAdminOrGlobalMod) { + return next(); + } + const users = await user.getUsersFromSet(`group:${groupName}:members`, req.uid, start, stop); + + const breadcrumbs = helpers.buildBreadcrumbs([ + { text: '[[pages:groups]]', url: '/groups' }, + { text: validator.escape(String(groupName)), url: `/groups/${req.params.slug}` }, + { text: '[[groups:details.members]]' }, + ]); + + const pageCount = Math.max(1, Math.ceil(groupData.memberCount / usersPerPage)); + res.render('groups/members', { + users: users, + pagination: pagination.create(page, pageCount, req.query), + breadcrumbs: breadcrumbs, + }); +}; diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js new file mode 100644 index 0000000000..56430f8c5d --- /dev/null +++ b/src/controllers/helpers.js @@ -0,0 +1,575 @@ +'use strict'; + +const nconf = require('nconf'); +const validator = require('validator'); +const querystring = require('querystring'); +const _ = require('lodash'); +const chalk = require('chalk'); + +const translator = require('../translator'); +const user = require('../user'); +const privileges = require('../privileges'); +const categories = require('../categories'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const middlewareHelpers = require('../middleware/helpers'); +const utils = require('../utils'); + +const helpers = module.exports; + +const relative_path = nconf.get('relative_path'); +const url = nconf.get('url'); + +helpers.noScriptErrors = async function (req, res, error, httpStatus) { + if (req.body.noscript !== 'true') { + if (typeof error === 'string') { + return res.status(httpStatus).send(error); + } + return res.status(httpStatus).json(error); + } + const middleware = require('../middleware'); + const httpStatusString = httpStatus.toString(); + await middleware.buildHeaderAsync(req, res); + res.status(httpStatus).render(httpStatusString, { + path: req.path, + loggedIn: req.loggedIn, + error: error, + returnLink: true, + title: `[[global:${httpStatusString}.title]]`, + }); +}; + +helpers.terms = { + daily: 'day', + weekly: 'week', + monthly: 'month', +}; + +helpers.buildQueryString = function (query, key, value) { + const queryObj = { ...query }; + if (value) { + queryObj[key] = value; + } else { + delete queryObj[key]; + } + delete queryObj._; + return Object.keys(queryObj).length ? `?${querystring.stringify(queryObj)}` : ''; +}; + +helpers.addLinkTags = function (params) { + params.res.locals.linkTags = params.res.locals.linkTags || []; + params.res.locals.linkTags.push({ + rel: 'canonical', + href: `${url}/${params.url}`, + }); + + params.tags.forEach((rel) => { + rel.href = `${url}/${params.url}${rel.href}`; + params.res.locals.linkTags.push(rel); + }); +}; + +helpers.buildFilters = function (url, filter, query) { + return [{ + name: '[[unread:all-topics]]', + url: url + helpers.buildQueryString(query, 'filter', ''), + selected: filter === '', + filter: '', + icon: 'fa-book', + }, { + name: '[[unread:new-topics]]', + url: url + helpers.buildQueryString(query, 'filter', 'new'), + selected: filter === 'new', + filter: 'new', + icon: 'fa-clock-o', + }, { + name: '[[unread:watched-topics]]', + url: url + helpers.buildQueryString(query, 'filter', 'watched'), + selected: filter === 'watched', + filter: 'watched', + icon: 'fa-bell-o', + }, { + name: '[[unread:unreplied-topics]]', + url: url + helpers.buildQueryString(query, 'filter', 'unreplied'), + selected: filter === 'unreplied', + filter: 'unreplied', + icon: 'fa-reply', + }]; +}; + +helpers.buildTerms = function (url, term, query) { + return [{ + name: '[[recent:alltime]]', + url: url + helpers.buildQueryString(query, 'term', ''), + selected: term === 'alltime', + term: 'alltime', + }, { + name: '[[recent:day]]', + url: url + helpers.buildQueryString(query, 'term', 'daily'), + selected: term === 'day', + term: 'day', + }, { + name: '[[recent:week]]', + url: url + helpers.buildQueryString(query, 'term', 'weekly'), + selected: term === 'week', + term: 'week', + }, { + name: '[[recent:month]]', + url: url + helpers.buildQueryString(query, 'term', 'monthly'), + selected: term === 'month', + term: 'month', + }]; +}; + +helpers.notAllowed = async function (req, res, error) { + ({ error } = await plugins.hooks.fire('filter:helpers.notAllowed', { req, res, error })); + + await plugins.hooks.fire('response:helpers.notAllowed', { req, res, error }); + if (res.headersSent) { + return; + } + + if (req.loggedIn || req.uid === -1) { + if (res.locals.isAPI) { + if (req.originalUrl.startsWith(`${relative_path}/api/v3`)) { + helpers.formatApiResponse(403, res, error); + } else { + res.status(403).json({ + path: req.path.replace(/^\/api/, ''), + loggedIn: req.loggedIn, + error: error, + title: '[[global:403.title]]', + bodyClass: middlewareHelpers.buildBodyClass(req, res), + }); + } + } else { + const middleware = require('../middleware'); + await middleware.buildHeaderAsync(req, res); + res.status(403).render('403', { + path: req.path, + loggedIn: req.loggedIn, + error, + title: '[[global:403.title]]', + }); + } + } else if (res.locals.isAPI) { + req.session.returnTo = req.url.replace(/^\/api/, ''); + helpers.formatApiResponse(401, res, error); + } else { + req.session.returnTo = req.url; + res.redirect(`${relative_path}/login${req.path.startsWith('/admin') ? '?local=1' : ''}`); + } +}; + +helpers.redirect = function (res, url, permanent) { + // this is used by sso plugins to redirect to the auth route + // { external: '/auth/sso' } or { external: 'https://domain/auth/sso' } + if (url.hasOwnProperty('external')) { + const redirectUrl = encodeURI(prependRelativePath(url.external)); + if (res.locals.isAPI) { + res.set('X-Redirect', redirectUrl).status(200).json({ external: redirectUrl }); + } else { + res.redirect(permanent ? 308 : 307, redirectUrl); + } + return; + } + + if (res.locals.isAPI) { + url = encodeURI(url); + res.set('X-Redirect', url).status(200).json(url); + } else { + res.redirect(permanent ? 308 : 307, encodeURI(prependRelativePath(url))); + } +}; + +function prependRelativePath(url) { + return url.startsWith('http://') || url.startsWith('https://') ? + url : relative_path + url; +} + +helpers.buildCategoryBreadcrumbs = async function (cid) { + const breadcrumbs = []; + + while (parseInt(cid, 10)) { + /* eslint-disable no-await-in-loop */ + const data = await categories.getCategoryFields(cid, ['name', 'slug', 'parentCid', 'disabled', 'isSection']); + if (!data.disabled && !data.isSection) { + breadcrumbs.unshift({ + text: String(data.name), + url: `${relative_path}/category/${data.slug}`, + cid: cid, + }); + } + cid = data.parentCid; + } + if (meta.config.homePageRoute && meta.config.homePageRoute !== 'categories') { + breadcrumbs.unshift({ + text: '[[global:header.categories]]', + url: `${relative_path}/categories`, + }); + } + + breadcrumbs.unshift({ + text: '[[global:home]]', + url: `${relative_path}/`, + }); + + return breadcrumbs; +}; + +helpers.buildBreadcrumbs = function (crumbs) { + const breadcrumbs = [ + { + text: '[[global:home]]', + url: `${relative_path}/`, + }, + ]; + + crumbs.forEach((crumb) => { + if (crumb) { + if (crumb.url) { + crumb.url = `${utils.isRelativeUrl(crumb.url) ? relative_path : ''}${crumb.url}`; + } + breadcrumbs.push(crumb); + } + }); + + return breadcrumbs; +}; + +helpers.buildTitle = function (pageTitle) { + const titleLayout = meta.config.titleLayout || '{pageTitle} | {browserTitle}'; + + const browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')); + pageTitle = pageTitle || ''; + const title = titleLayout.replace('{pageTitle}', () => pageTitle).replace('{browserTitle}', () => browserTitle); + return title; +}; + +helpers.getCategories = async function (set, uid, privilege, selectedCid) { + const cids = await categories.getCidsByPrivilege(set, uid, privilege); + return await getCategoryData(cids, uid, selectedCid, Object.values(categories.watchStates), privilege); +}; + +helpers.getCategoriesByStates = async function (uid, selectedCid, states, privilege = 'topics:read') { + const cids = await categories.getAllCidsFromSet('categories:cid'); + return await getCategoryData(cids, uid, selectedCid, states, privilege); +}; + +async function getCategoryData(cids, uid, selectedCid, states, privilege) { + const [visibleCategories, selectData] = await Promise.all([ + helpers.getVisibleCategories({ + cids, uid, states, privilege, showLinks: false, + }), + helpers.getSelectedCategory(selectedCid), + ]); + + const categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass']); + + categoriesData.forEach((category) => { + category.selected = selectData.selectedCids.includes(category.cid); + }); + selectData.selectedCids.sort((a, b) => a - b); + return { + categories: categoriesData, + selectedCategory: selectData.selectedCategory, + selectedCids: selectData.selectedCids, + }; +} + +helpers.getVisibleCategories = async function (params) { + const { cids, uid, privilege } = params; + const states = params.states || [categories.watchStates.watching, categories.watchStates.notwatching]; + const showLinks = !!params.showLinks; + + let [allowed, watchState, categoriesData, isAdmin, isModerator] = await Promise.all([ + privileges.categories.isUserAllowedTo(privilege, cids, uid), + categories.getWatchState(cids, uid), + categories.getCategoriesData(cids), + user.isAdministrator(uid), + user.isModerator(uid, cids), + ]); + + const filtered = await plugins.hooks.fire('filter:helpers.getVisibleCategories', { + uid: uid, + allowed: allowed, + watchState: watchState, + categoriesData: categoriesData, + isModerator: isModerator, + isAdmin: isAdmin, + }); + ({ allowed, watchState, categoriesData, isModerator, isAdmin } = filtered); + + categories.getTree(categoriesData, params.parentCid); + + const cidToAllowed = _.zipObject(cids, allowed.map((allowed, i) => isAdmin || isModerator[i] || allowed)); + const cidToCategory = _.zipObject(cids, categoriesData); + const cidToWatchState = _.zipObject(cids, watchState); + + return categoriesData.filter((c) => { + if (!c) { + return false; + } + const hasVisibleChildren = checkVisibleChildren(c, cidToAllowed, cidToWatchState, states); + const isCategoryVisible = ( + cidToAllowed[c.cid] && + (showLinks || !c.link) && + !c.disabled && + states.includes(cidToWatchState[c.cid]) + ); + const shouldBeRemoved = !hasVisibleChildren && !isCategoryVisible; + const shouldBeDisaplayedAsDisabled = hasVisibleChildren && !isCategoryVisible; + + if (shouldBeDisaplayedAsDisabled) { + c.disabledClass = true; + } + + if (shouldBeRemoved && c.parent && c.parent.cid && cidToCategory[c.parent.cid]) { + cidToCategory[c.parent.cid].children = + cidToCategory[c.parent.cid].children.filter(child => child.cid !== c.cid); + } + + return !shouldBeRemoved; + }); +}; + +helpers.getSelectedCategory = async function (cids) { + if (cids && !Array.isArray(cids)) { + cids = [cids]; + } + cids = cids && cids.map(cid => parseInt(cid, 10)); + let selectedCategories = await categories.getCategoriesData(cids); + const selectedCids = selectedCategories.map(c => c && c.cid).filter(Boolean); + if (selectedCategories.length > 1) { + selectedCategories = { + icon: 'fa-plus', + name: '[[unread:multiple-categories-selected]]', + bgColor: '#ddd', + }; + } else if (selectedCategories.length === 1 && selectedCategories[0]) { + selectedCategories = selectedCategories[0]; + } else { + selectedCategories = null; + } + return { + selectedCids: selectedCids, + selectedCategory: selectedCategories, + }; +}; + +helpers.trimChildren = function (category) { + if (category && Array.isArray(category.children)) { + category.children = category.children.slice(0, category.subCategoriesPerPage); + category.children.forEach((child) => { + if (category.isSection) { + helpers.trimChildren(child); + } else { + child.children = undefined; + } + }); + } +}; + +helpers.setCategoryTeaser = function (category) { + if (Array.isArray(category.posts) && category.posts.length && category.posts[0]) { + category.teaser = { + url: `${nconf.get('relative_path')}/post/${category.posts[0].pid}`, + timestampISO: category.posts[0].timestampISO, + pid: category.posts[0].pid, + topic: category.posts[0].topic, + }; + } +}; + +function checkVisibleChildren(c, cidToAllowed, cidToWatchState, states) { + if (!c || !Array.isArray(c.children)) { + return false; + } + return c.children.some(c => !c.disabled && ( + (cidToAllowed[c.cid] && states.includes(cidToWatchState[c.cid])) || + checkVisibleChildren(c, cidToAllowed, cidToWatchState, states) + )); +} + +helpers.getHomePageRoutes = async function (uid) { + const routes = [ + { + route: 'categories', + name: 'Categories', + }, + { + route: 'unread', + name: 'Unread', + }, + { + route: 'recent', + name: 'Recent', + }, + { + route: 'top', + name: 'Top', + }, + { + route: 'popular', + name: 'Popular', + }, + { + route: 'custom', + name: 'Custom', + }, + ]; + const data = await plugins.hooks.fire('filter:homepage.get', { + uid: uid, + routes: routes, + }); + return data.routes; +}; + +helpers.formatApiResponse = async (statusCode, res, payload) => { + if (res.req.method === 'HEAD') { + return res.sendStatus(statusCode); + } + + if (String(statusCode).startsWith('2')) { + if (res.req.loggedIn) { + res.set('cache-control', 'private'); + } + + let code = 'ok'; + let message = 'OK'; + switch (statusCode) { + case 202: + code = 'accepted'; + message = 'Accepted'; + break; + + case 204: + code = 'no-content'; + message = 'No Content'; + break; + } + + res.status(statusCode).json({ + status: { code, message }, + response: payload || {}, + }); + } else if (payload instanceof Error) { + const { message } = payload; + const response = {}; + + // Update status code based on some common error codes + switch (message) { + case '[[error:user-banned]]': + Object.assign(response, await generateBannedResponse(res)); + // intentional fall through + + case '[[error:no-privileges]]': + statusCode = 403; + break; + + case '[[error:invalid-uid]]': + statusCode = 401; + break; + } + + if (message.startsWith('[[error:required-parameters-missing, ')) { + const params = message.slice('[[error:required-parameters-missing, '.length, -2).split(' '); + Object.assign(response, { params }); + } + + const returnPayload = await helpers.generateError(statusCode, message, res); + returnPayload.response = response; + + if (global.env === 'development') { + returnPayload.stack = payload.stack; + process.stdout.write(`[${chalk.yellow('api')}] Exception caught, error with stack trace follows:\n`); + process.stdout.write(payload.stack); + } + res.status(statusCode).json(returnPayload); + } else if (!payload) { + // Non-2xx statusCode, generate predefined error + const returnPayload = await helpers.generateError(statusCode, null, res); + res.status(statusCode).json(returnPayload); + } +}; + +async function generateBannedResponse(res) { + const response = {}; + const [reason, expiry] = await Promise.all([ + user.bans.getReason(res.req.uid), + user.getUserField(res.req.uid, 'banned:expire'), + ]); + + response.reason = reason; + if (expiry) { + Object.assign(response, { + expiry, + expiryISO: new Date(expiry).toISOString(), + expiryLocaleString: new Date(expiry).toLocaleString(), + }); + } + + return response; +} + +helpers.generateError = async (statusCode, message, res) => { + async function translateMessage(message) { + const { req } = res; + const settings = req.query.lang ? null : await user.getSettings(req.uid); + const language = String(req.query.lang || settings.userLang || meta.config.defaultLang); + return await translator.translate(message, language); + } + if (message && message.startsWith('[[')) { + message = await translateMessage(message); + } + + const payload = { + status: { + code: 'internal-server-error', + message: message || await translateMessage(`[[error:api.${statusCode}]]`), + }, + response: {}, + }; + + switch (statusCode) { + case 400: + payload.status.code = 'bad-request'; + break; + + case 401: + payload.status.code = 'not-authorised'; + break; + + case 403: + payload.status.code = 'forbidden'; + break; + + case 404: + payload.status.code = 'not-found'; + break; + + case 426: + payload.status.code = 'upgrade-required'; + break; + + case 429: + payload.status.code = 'too-many-requests'; + break; + + case 500: + payload.status.code = 'internal-server-error'; + break; + + case 501: + payload.status.code = 'not-implemented'; + break; + + case 503: + payload.status.code = 'service-unavailable'; + break; + } + + return payload; +}; + +require('../promisify')(helpers); diff --git a/src/controllers/home.js b/src/controllers/home.js new file mode 100644 index 0000000000..134af52be7 --- /dev/null +++ b/src/controllers/home.js @@ -0,0 +1,64 @@ +'use strict'; + +const url = require('url'); + +const plugins = require('../plugins'); +const meta = require('../meta'); +const user = require('../user'); + +function adminHomePageRoute() { + return ((meta.config.homePageRoute === 'custom' ? meta.config.homePageCustom : meta.config.homePageRoute) || 'categories').replace(/^\//, ''); +} + +async function getUserHomeRoute(uid) { + const settings = await user.getSettings(uid); + let route = adminHomePageRoute(); + + if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') { + route = (settings.homePageRoute || route).replace(/^\/+/, ''); + } + + return route; +} + +async function rewrite(req, res, next) { + if (req.path !== '/' && req.path !== '/api/' && req.path !== '/api') { + return next(); + } + let route = adminHomePageRoute(); + if (meta.config.allowUserHomePage) { + route = await getUserHomeRoute(req.uid, next); + } + + let parsedUrl; + try { + parsedUrl = url.parse(route, true); + } catch (err) { + return next(err); + } + + const { pathname } = parsedUrl; + const hook = `action:homepage.get:${pathname}`; + if (!plugins.hooks.hasListeners(hook)) { + req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + pathname; + } else { + res.locals.homePageRoute = pathname; + } + req.query = Object.assign(parsedUrl.query, req.query); + + next(); +} + +exports.rewrite = rewrite; + +function pluginHook(req, res, next) { + const hook = `action:homepage.get:${res.locals.homePageRoute}`; + + plugins.hooks.fire(hook, { + req: req, + res: res, + next: next, + }); +} + +exports.pluginHook = pluginHook; diff --git a/src/controllers/index.js b/src/controllers/index.js new file mode 100644 index 0000000000..b2f816cd9a --- /dev/null +++ b/src/controllers/index.js @@ -0,0 +1,375 @@ +'use strict'; + +const nconf = require('nconf'); +const validator = require('validator'); + +const meta = require('../meta'); +const user = require('../user'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); +const helpers = require('./helpers'); + +const Controllers = module.exports; + +Controllers.ping = require('./ping'); +Controllers.home = require('./home'); +Controllers.topics = require('./topics'); +Controllers.posts = require('./posts'); +Controllers.career = require('./career'); +Controllers.categories = require('./categories'); +Controllers.category = require('./category'); +Controllers.unread = require('./unread'); +Controllers.recent = require('./recent'); +Controllers.popular = require('./popular'); +Controllers.top = require('./top'); +Controllers.tags = require('./tags'); +Controllers.search = require('./search'); +Controllers.user = require('./user'); +Controllers.users = require('./users'); +Controllers.groups = require('./groups'); +Controllers.accounts = require('./accounts'); +Controllers.authentication = require('./authentication'); +Controllers.api = require('./api'); +Controllers.admin = require('./admin'); +Controllers.globalMods = require('./globalmods'); +Controllers.mods = require('./mods'); +Controllers.sitemap = require('./sitemap'); +Controllers.osd = require('./osd'); +Controllers['404'] = require('./404'); +Controllers.errors = require('./errors'); +Controllers.composer = require('./composer'); + +Controllers.write = require('./write'); + +Controllers.reset = async function (req, res) { + if (meta.config['password:disableEdit']) { + return helpers.notAllowed(req, res); + } + + res.locals.metaTags = { + ...res.locals.metaTags, + name: 'robots', + content: 'noindex', + }; + + const renderReset = function (code, valid) { + res.render('reset_code', { + valid: valid, + displayExpiryNotice: req.session.passwordExpired, + code: code, + minimumPasswordLength: meta.config.minimumPasswordLength, + minimumPasswordStrength: meta.config.minimumPasswordStrength, + breadcrumbs: helpers.buildBreadcrumbs([ + { + text: '[[reset_password:reset_password]]', + url: '/reset', + }, + { + text: '[[reset_password:update_password]]', + }, + ]), + title: '[[pages:reset]]', + }); + delete req.session.passwordExpired; + }; + + if (req.params.code) { + req.session.reset_code = req.params.code; + } + + if (req.session.reset_code) { + // Validate and save to local variable before removing from session + const valid = await user.reset.validate(req.session.reset_code); + renderReset(req.session.reset_code, valid); + delete req.session.reset_code; + } else { + res.render('reset', { + code: null, + breadcrumbs: helpers.buildBreadcrumbs([{ + text: '[[reset_password:reset_password]]', + }]), + title: '[[pages:reset]]', + }); + } +}; + +Controllers.login = async function (req, res) { + const data = { loginFormEntry: [] }; + const loginStrategies = require('../routes/authentication').getLoginStrategies(); + const registrationType = meta.config.registrationType || 'normal'; + const allowLoginWith = (meta.config.allowLoginWith || 'username-email'); + + let errorText; + if (req.query.error === 'csrf-invalid') { + errorText = '[[error:csrf-invalid]]'; + } else if (req.query.error) { + errorText = validator.escape(String(req.query.error)); + } + + if (req.headers['x-return-to']) { + req.session.returnTo = req.headers['x-return-to']; + } + + // Occasionally, x-return-to is passed a full url. + req.session.returnTo = req.session.returnTo && req.session.returnTo.replace(nconf.get('base_url'), '').replace(nconf.get('relative_path'), ''); + + data.alternate_logins = loginStrategies.length > 0; + data.authentication = loginStrategies; + data.allowRegistration = registrationType === 'normal'; + data.allowLoginWith = `[[login:${allowLoginWith}]]`; + data.breadcrumbs = helpers.buildBreadcrumbs([{ + text: '[[global:login]]', + }]); + data.error = req.flash('error')[0] || errorText; + data.title = '[[pages:login]]'; + data.allowPasswordReset = !meta.config['password:disableEdit']; + + const hasLoginPrivilege = await privileges.global.canGroup('local:login', 'registered-users'); + data.allowLocalLogin = hasLoginPrivilege || parseInt(req.query.local, 10) === 1; + + if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) { + return helpers.redirect(res, { external: data.authentication[0].url }); + } + + // Re-auth challenge, pre-fill username + if (req.loggedIn) { + const userData = await user.getUserFields(req.uid, ['username']); + data.username = userData.username; + data.alternate_logins = false; + } + res.render('login', data); +}; + +Controllers.register = async function (req, res, next) { + const registrationType = meta.config.registrationType || 'normal'; + + if (registrationType === 'disabled') { + return setImmediate(next); + } + + let errorText; + const returnTo = (req.headers['x-return-to'] || '').replace(nconf.get('base_url') + nconf.get('relative_path'), ''); + if (req.query.error === 'csrf-invalid') { + errorText = '[[error:csrf-invalid]]'; + } + try { + if (registrationType === 'invite-only' || registrationType === 'admin-invite-only') { + try { + await user.verifyInvitation(req.query); + } catch (e) { + return res.render('400', { + error: e.message, + }); + } + } + + if (returnTo) { + req.session.returnTo = returnTo; + } + + const loginStrategies = require('../routes/authentication').getLoginStrategies(); + res.render('register', { + 'register_window:spansize': loginStrategies.length ? 'col-md-6' : 'col-md-12', + alternate_logins: !!loginStrategies.length, + authentication: loginStrategies, + + minimumUsernameLength: meta.config.minimumUsernameLength, + maximumUsernameLength: meta.config.maximumUsernameLength, + minimumPasswordLength: meta.config.minimumPasswordLength, + minimumPasswordStrength: meta.config.minimumPasswordStrength, + breadcrumbs: helpers.buildBreadcrumbs([{ + text: '[[register:register]]', + }]), + regFormEntry: [ + { + label: 'Account Type', + styleName: 'account-type', + html: ` + + `, + }, + ], + error: req.flash('error')[0] || errorText, + title: '[[pages:register]]', + }); + } catch (err) { + next(err); + } +}; + +Controllers.registerInterstitial = async function (req, res, next) { + if (!req.session.hasOwnProperty('registration')) { + return res.redirect(`${nconf.get('relative_path')}/register`); + } + try { + const data = await plugins.hooks.fire('filter:register.interstitial', { + req, + userData: req.session.registration, + interstitials: [], + }); + + if (!data.interstitials.length) { + // No interstitials, redirect to home + const returnTo = req.session.returnTo || req.session.registration.returnTo; + delete req.session.registration; + return helpers.redirect(res, returnTo || '/'); + } + + const errors = req.flash('errors'); + const renders = data.interstitials.map( + interstitial => req.app.renderAsync(interstitial.template, { ...interstitial.data || {}, errors }) + ); + const sections = await Promise.all(renders); + + res.render('registerComplete', { + title: '[[pages:registration-complete]]', + register: data.userData.register, + sections, + errors, + }); + } catch (err) { + next(err); + } +}; + +Controllers.confirmEmail = async (req, res, next) => { + try { + await user.email.confirmByCode(req.params.code, req.session.id); + } catch (e) { + if (e.message === '[[error:invalid-data]]') { + return next(); + } + + throw e; + } + + res.render('confirm', { + title: '[[pages:confirm]]', + }); +}; + +Controllers.robots = function (req, res) { + res.set('Content-Type', 'text/plain'); + + if (meta.config['robots:txt']) { + res.send(meta.config['robots:txt']); + } else { + res.send(`${'User-agent: *\n' + + 'Disallow: '}${nconf.get('relative_path')}/admin/\n` + + `Disallow: ${nconf.get('relative_path')}/reset/\n` + + `Disallow: ${nconf.get('relative_path')}/compose\n` + + `Sitemap: ${nconf.get('url')}/sitemap.xml`); + } +}; + +Controllers.manifest = async function (req, res) { + const manifest = { + name: meta.config.title || 'NodeBB', + short_name: meta.config['title:short'] || meta.config.title || 'NodeBB', + start_url: nconf.get('url'), + display: 'standalone', + orientation: 'portrait', + theme_color: meta.config.themeColor || '#ffffff', + background_color: meta.config.backgroundColor || '#ffffff', + icons: [], + }; + + if (meta.config['brand:touchIcon']) { + manifest.icons.push({ + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-36.png`, + sizes: '36x36', + type: 'image/png', + density: 0.75, + }, { + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-48.png`, + sizes: '48x48', + type: 'image/png', + density: 1.0, + }, { + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-72.png`, + sizes: '72x72', + type: 'image/png', + density: 1.5, + }, { + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-96.png`, + sizes: '96x96', + type: 'image/png', + density: 2.0, + }, { + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-144.png`, + sizes: '144x144', + type: 'image/png', + density: 3.0, + }, { + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-192.png`, + sizes: '192x192', + type: 'image/png', + density: 4.0, + }, { + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-512.png`, + sizes: '512x512', + type: 'image/png', + density: 10.0, + }); + } + + + if (meta.config['brand:maskableIcon']) { + manifest.icons.push({ + src: `${nconf.get('relative_path')}/assets/uploads/system/maskableicon-orig.png`, + type: 'image/png', + purpose: 'maskable', + }); + } else if (meta.config['brand:touchIcon']) { + manifest.icons.push({ + src: `${nconf.get('relative_path')}/assets/uploads/system/touchicon-orig.png`, + type: 'image/png', + purpose: 'maskable', + }); + } + + const data = await plugins.hooks.fire('filter:manifest.build', { + req: req, + res: res, + manifest: manifest, + }); + res.status(200).json(data.manifest); +}; + +Controllers.outgoing = function (req, res, next) { + const url = req.query.url || ''; + const allowedProtocols = [ + 'http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'gopher', + 'nntp', 'feed', 'telnet', 'mms', 'rtsp', 'svn', 'tel', 'fax', 'xmpp', 'webcal', + ]; + const parsed = require('url').parse(url); + + if (!url || !parsed.protocol || !allowedProtocols.includes(parsed.protocol.slice(0, -1))) { + return next(); + } + + res.render('outgoing', { + outgoing: validator.escape(String(url)), + title: meta.config.title, + breadcrumbs: helpers.buildBreadcrumbs([{ + text: '[[notifications:outgoing_link]]', + }]), + }); +}; + +Controllers.termsOfUse = async function (req, res, next) { + if (!meta.config.termsOfUse) { + return next(); + } + const termsOfUse = await plugins.hooks.fire('filter:parse.post', { + postData: { + content: meta.config.termsOfUse || '', + }, + }); + res.render('tos', { + termsOfUse: termsOfUse.postData.content, + }); +}; diff --git a/src/controllers/mods.js b/src/controllers/mods.js new file mode 100644 index 0000000000..638d1fc7cb --- /dev/null +++ b/src/controllers/mods.js @@ -0,0 +1,200 @@ +'use strict'; + +const user = require('../user'); +const posts = require('../posts'); +const flags = require('../flags'); +const analytics = require('../analytics'); +const plugins = require('../plugins'); +const pagination = require('../pagination'); +const privileges = require('../privileges'); +const utils = require('../utils'); +const helpers = require('./helpers'); + +const modsController = module.exports; +modsController.flags = {}; + +modsController.flags.list = async function (req, res) { + const validFilters = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick', 'page', 'perPage']; + const validSorts = ['newest', 'oldest', 'reports', 'upvotes', 'downvotes', 'replies']; + + const results = await Promise.all([ + user.isAdminOrGlobalMod(req.uid), + user.getModeratedCids(req.uid), + plugins.hooks.fire('filter:flags.validateFilters', { filters: validFilters }), + plugins.hooks.fire('filter:flags.validateSort', { sorts: validSorts }), + ]); + const [isAdminOrGlobalMod, moderatedCids,, { sorts }] = results; + let [,, { filters }] = results; + + if (!(isAdminOrGlobalMod || !!moderatedCids.length)) { + return helpers.notAllowed(req, res); + } + + if (!isAdminOrGlobalMod && moderatedCids.length) { + res.locals.cids = moderatedCids.map(cid => String(cid)); + } + + // Parse query string params for filters, eliminate non-valid filters + filters = filters.reduce((memo, cur) => { + if (req.query.hasOwnProperty(cur)) { + if (typeof req.query[cur] === 'string' && req.query[cur].trim() !== '') { + memo[cur] = req.query[cur].trim(); + } else if (Array.isArray(req.query[cur]) && req.query[cur].length) { + memo[cur] = req.query[cur]; + } + } + + return memo; + }, {}); + + let hasFilter = !!Object.keys(filters).length; + + if (res.locals.cids) { + if (!filters.cid) { + // If mod and no cid filter, add filter for their modded categories + filters.cid = res.locals.cids; + } else if (Array.isArray(filters.cid)) { + // Remove cids they do not moderate + filters.cid = filters.cid.filter(cid => res.locals.cids.includes(String(cid))); + } else if (!res.locals.cids.includes(String(filters.cid))) { + filters.cid = res.locals.cids; + hasFilter = false; + } + } + + // Pagination doesn't count as a filter + if ( + (Object.keys(filters).length === 1 && filters.hasOwnProperty('page')) || + (Object.keys(filters).length === 2 && filters.hasOwnProperty('page') && filters.hasOwnProperty('perPage')) + ) { + hasFilter = false; + } + + // Parse sort from query string + let sort; + if (req.query.sort) { + sort = sorts.includes(req.query.sort) ? req.query.sort : null; + } + if (sort === 'newest') { + sort = undefined; + } + hasFilter = hasFilter || !!sort; + + const [flagsData, analyticsData, selectData] = await Promise.all([ + flags.list({ + filters: filters, + sort: sort, + uid: req.uid, + query: req.query, + }), + analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30), + helpers.getSelectedCategory(filters.cid), + ]); + + res.render('flags/list', { + flags: flagsData.flags, + analytics: analyticsData, + selectedCategory: selectData.selectedCategory, + hasFilter: hasFilter, + filters: filters, + expanded: !!(filters.assignee || filters.reporterId || filters.targetUid), + sort: sort || 'newest', + title: '[[pages:flags]]', + pagination: pagination.create(flagsData.page, flagsData.pageCount, req.query), + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:flags]]' }]), + }); +}; + +modsController.flags.detail = async function (req, res, next) { + const results = await utils.promiseParallel({ + isAdminOrGlobalMod: user.isAdminOrGlobalMod(req.uid), + moderatedCids: user.getModeratedCids(req.uid), + flagData: flags.get(req.params.flagId), + assignees: user.getAdminsandGlobalModsandModerators(), + privileges: Promise.all(['global', 'admin'].map(async type => privileges[type].get(req.uid))), + }); + results.privileges = { ...results.privileges[0], ...results.privileges[1] }; + + if (!results.flagData || (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length))) { + return next(); // 404 + } + + results.flagData.history = results.isAdminOrGlobalMod ? (await flags.getHistory(req.params.flagId)) : null; + + if (results.flagData.type === 'user') { + results.flagData.type_path = 'uid'; + } else if (results.flagData.type === 'post') { + results.flagData.type_path = 'post'; + } + + res.render('flags/detail', Object.assign(results.flagData, { + assignees: results.assignees, + type_bool: ['post', 'user', 'empty'].reduce((memo, cur) => { + if (cur !== 'empty') { + memo[cur] = results.flagData.type === cur && ( + !results.flagData.target || + !!Object.keys(results.flagData.target).length + ); + } else { + memo[cur] = !Object.keys(results.flagData.target).length; + } + + return memo; + }, {}), + states: Object.fromEntries(flags._states), + title: `[[pages:flag-details, ${req.params.flagId}]]`, + privileges: results.privileges, + breadcrumbs: helpers.buildBreadcrumbs([ + { text: '[[pages:flags]]', url: '/flags' }, + { text: `[[pages:flag-details, ${req.params.flagId}]]` }, + ]), + })); +}; + +modsController.postQueue = async function (req, res, next) { + if (!req.loggedIn) { + return next(); + } + const { id } = req.params; + const { cid } = req.query; + const page = parseInt(req.query.page, 10) || 1; + const postsPerPage = 20; + + let postData = await posts.getQueuedPosts({ id: id }); + const [isAdmin, isGlobalMod, moderatedCids, categoriesData] = await Promise.all([ + user.isAdministrator(req.uid), + user.isGlobalModerator(req.uid), + user.getModeratedCids(req.uid), + helpers.getSelectedCategory(cid), + ]); + + postData = postData.filter(p => p && + (!categoriesData.selectedCids.length || categoriesData.selectedCids.includes(p.category.cid)) && + (isAdmin || isGlobalMod || moderatedCids.includes(Number(p.category.cid)) || req.uid === p.user.uid)); + + ({ posts: postData } = await plugins.hooks.fire('filter:post-queue.get', { + posts: postData, + req: req, + })); + + const pageCount = Math.max(1, Math.ceil(postData.length / postsPerPage)); + const start = (page - 1) * postsPerPage; + const stop = start + postsPerPage - 1; + postData = postData.slice(start, stop + 1); + const crumbs = [{ text: '[[pages:post-queue]]', url: id ? '/post-queue' : undefined }]; + if (id && postData.length) { + const text = postData[0].data.tid ? '[[post-queue:reply]]' : '[[post-queue:topic]]'; + crumbs.push({ text: text }); + } + res.render('post-queue', { + title: '[[pages:post-queue]]', + posts: postData, + isAdmin: isAdmin, + canAccept: isAdmin || isGlobalMod || !!moderatedCids.length, + ...categoriesData, + allCategoriesUrl: `post-queue${helpers.buildQueryString(req.query, 'cid', '')}`, + pagination: pagination.create(page, pageCount), + breadcrumbs: helpers.buildBreadcrumbs(crumbs), + singlePost: !!id, + }); +}; diff --git a/src/controllers/osd.js b/src/controllers/osd.js new file mode 100644 index 0000000000..8c06a9357b --- /dev/null +++ b/src/controllers/osd.js @@ -0,0 +1,57 @@ +'use strict'; + +const xml = require('xml'); +const nconf = require('nconf'); + +const plugins = require('../plugins'); +const meta = require('../meta'); + +module.exports.handle = function (req, res, next) { + if (plugins.hooks.hasListeners('filter:search.query')) { + res.type('application/opensearchdescription+xml').send(generateXML()); + } else { + next(); + } +}; + +function generateXML() { + return xml([{ + OpenSearchDescription: [ + { + _attr: { + xmlns: 'http://a9.com/-/spec/opensearch/1.1/', + 'xmlns:moz': 'http://www.mozilla.org/2006/browser/search/', + }, + }, + { ShortName: trimToLength(String(meta.config.title || meta.config.browserTitle || 'NodeBB'), 16) }, + { Description: trimToLength(String(meta.config.description || ''), 1024) }, + { InputEncoding: 'UTF-8' }, + { + Image: [ + { + _attr: { + width: '16', + height: '16', + type: 'image/x-icon', + }, + }, + `${nconf.get('url')}/favicon.ico`, + ], + }, + { + Url: { + _attr: { + type: 'text/html', + method: 'get', + template: `${nconf.get('url')}/search?term={searchTerms}&in=titlesposts`, + }, + }, + }, + { 'moz:SearchForm': `${nconf.get('url')}/search` }, + ], + }], { declaration: true, indent: '\t' }); +} + +function trimToLength(string, length) { + return string.trim().substring(0, length).trim(); +} diff --git a/src/controllers/ping.js b/src/controllers/ping.js new file mode 100644 index 0000000000..68ca1d079d --- /dev/null +++ b/src/controllers/ping.js @@ -0,0 +1,13 @@ +'use strict'; + +const nconf = require('nconf'); +const db = require('../database'); + +module.exports.ping = async function (req, res, next) { + try { + await db.getObject('config'); + res.status(200).send(req.path === `${nconf.get('relative_path')}/sping` ? 'healthy' : '200'); + } catch (err) { + next(err); + } +}; diff --git a/src/controllers/popular.js b/src/controllers/popular.js new file mode 100644 index 0000000000..eac02644ef --- /dev/null +++ b/src/controllers/popular.js @@ -0,0 +1,30 @@ + +'use strict'; + +const nconf = require('nconf'); +const validator = require('validator'); + +const helpers = require('./helpers'); +const recentController = require('./recent'); + +const popularController = module.exports; + +popularController.get = async function (req, res, next) { + const data = await recentController.getData(req, 'popular', 'posts'); + if (!data) { + return next(); + } + const term = helpers.terms[req.query.term] || 'alltime'; + if (req.originalUrl.startsWith(`${nconf.get('relative_path')}/api/popular`) || req.originalUrl.startsWith(`${nconf.get('relative_path')}/popular`)) { + data.title = `[[pages:popular-${term}]]`; + const breadcrumbs = [{ text: '[[global:header.popular]]' }]; + data.breadcrumbs = helpers.buildBreadcrumbs(breadcrumbs); + } + + const feedQs = data.rssFeedUrl.split('?')[1]; + data.rssFeedUrl = `${nconf.get('relative_path')}/popular/${validator.escape(String(req.query.term || 'alltime'))}.rss`; + if (req.loggedIn) { + data.rssFeedUrl += `?${feedQs}`; + } + res.render('popular', data); +}; diff --git a/src/controllers/posts.js b/src/controllers/posts.js new file mode 100644 index 0000000000..4222cc6746 --- /dev/null +++ b/src/controllers/posts.js @@ -0,0 +1,39 @@ +'use strict'; + +const querystring = require('querystring'); + +const posts = require('../posts'); +const privileges = require('../privileges'); +const helpers = require('./helpers'); + +const postsController = module.exports; + +postsController.redirectToPost = async function (req, res, next) { + const pid = parseInt(req.params.pid, 10); + if (!pid) { + return next(); + } + + const [canRead, path] = await Promise.all([ + privileges.posts.can('topics:read', pid, req.uid), + posts.generatePostPath(pid, req.uid), + ]); + if (!path) { + return next(); + } + if (!canRead) { + return helpers.notAllowed(req, res); + } + + const qs = querystring.stringify(req.query); + helpers.redirect(res, qs ? `${path}?${qs}` : path); +}; + +postsController.getRecentPosts = async function (req, res) { + const page = parseInt(req.query.page, 10) || 1; + const postsPerPage = 20; + const start = Math.max(0, (page - 1) * postsPerPage); + const stop = start + postsPerPage - 1; + const data = await posts.getRecentPosts(req.uid, start, stop, req.params.term); + res.json(data); +}; diff --git a/src/controllers/recent.js b/src/controllers/recent.js new file mode 100644 index 0000000000..1a588c2072 --- /dev/null +++ b/src/controllers/recent.js @@ -0,0 +1,99 @@ + +'use strict'; + +const nconf = require('nconf'); + +const user = require('../user'); +const categories = require('../categories'); +const topics = require('../topics'); +const meta = require('../meta'); +const helpers = require('./helpers'); +const pagination = require('../pagination'); +const privileges = require('../privileges'); + +const recentController = module.exports; +const relative_path = nconf.get('relative_path'); + +recentController.get = async function (req, res, next) { + const data = await recentController.getData(req, 'recent', 'recent'); + if (!data) { + return next(); + } + res.render('recent', data); +}; + +recentController.getData = async function (req, url, sort) { + const page = parseInt(req.query.page, 10) || 1; + let term = helpers.terms[req.query.term]; + const { cid, tags } = req.query; + const filter = req.query.filter || ''; + + if (!term && req.query.term) { + return null; + } + term = term || 'alltime'; + + const [settings, categoryData, rssToken, canPost, isPrivileged] = await Promise.all([ + user.getSettings(req.uid), + helpers.getSelectedCategory(cid), + user.auth.getFeedToken(req.uid), + canPostTopic(req.uid), + user.isPrivileged(req.uid), + ]); + + const start = Math.max(0, (page - 1) * settings.topicsPerPage); + const stop = start + settings.topicsPerPage - 1; + + const data = await topics.getSortedTopics({ + cids: cid, + tags: tags, + uid: req.uid, + start: start, + stop: stop, + filter: filter, + term: term, + sort: sort, + floatPinned: req.query.pinned, + query: req.query, + }); + + const isDisplayedAsHome = !(req.originalUrl.startsWith(`${relative_path}/api/${url}`) || req.originalUrl.startsWith(`${relative_path}/${url}`)); + const baseUrl = isDisplayedAsHome ? '' : url; + + if (isDisplayedAsHome) { + data.title = meta.config.homePageTitle || '[[pages:home]]'; + } else { + data.title = `[[pages:${url}]]`; + data.breadcrumbs = helpers.buildBreadcrumbs([{ text: `[[${url}:title]]` }]); + } + + data.canPost = canPost; + data.showSelect = isPrivileged; + data.showTopicTools = isPrivileged; + data.allCategoriesUrl = baseUrl + helpers.buildQueryString(req.query, 'cid', ''); + data.selectedCategory = categoryData.selectedCategory; + data.selectedCids = categoryData.selectedCids; + data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0; + data.rssFeedUrl = `${relative_path}/${url}.rss`; + if (req.loggedIn) { + data.rssFeedUrl += `?uid=${req.uid}&token=${rssToken}`; + } + + data.filters = helpers.buildFilters(baseUrl, filter, req.query); + data.selectedFilter = data.filters.find(filter => filter && filter.selected); + data.terms = helpers.buildTerms(baseUrl, term, req.query); + data.selectedTerm = data.terms.find(term => term && term.selected); + + const pageCount = Math.max(1, Math.ceil(data.topicCount / settings.topicsPerPage)); + data.pagination = pagination.create(page, pageCount, req.query); + helpers.addLinkTags({ url: url, res: req.res, tags: data.pagination.rel }); + return data; +}; + +async function canPostTopic(uid) { + let cids = await categories.getAllCidsFromSet('categories:cid'); + cids = await privileges.categories.filterCids('topics:create', cids, uid); + return cids.length > 0; +} + +require('../promisify')(recentController, ['get']); diff --git a/src/controllers/search.js b/src/controllers/search.js new file mode 100644 index 0000000000..9deed84935 --- /dev/null +++ b/src/controllers/search.js @@ -0,0 +1,145 @@ + +'use strict'; + +const validator = require('validator'); + +const db = require('../database'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const search = require('../search'); +const categories = require('../categories'); +const pagination = require('../pagination'); +const privileges = require('../privileges'); +const utils = require('../utils'); +const helpers = require('./helpers'); + +const searchController = module.exports; + +searchController.search = async function (req, res, next) { + if (!plugins.hooks.hasListeners('filter:search.query')) { + return next(); + } + const page = Math.max(1, parseInt(req.query.page, 10)) || 1; + + const searchOnly = parseInt(req.query.searchOnly, 10) === 1; + + const userPrivileges = await utils.promiseParallel({ + 'search:users': privileges.global.can('search:users', req.uid), + 'search:content': privileges.global.can('search:content', req.uid), + 'search:tags': privileges.global.can('search:tags', req.uid), + }); + req.query.in = req.query.in || meta.config.searchDefaultIn || 'titlesposts'; + let allowed = (req.query.in === 'users' && userPrivileges['search:users']) || + (req.query.in === 'tags' && userPrivileges['search:tags']) || + (req.query.in === 'categories') || + (['titles', 'titlesposts', 'posts'].includes(req.query.in) && userPrivileges['search:content']); + ({ allowed } = await plugins.hooks.fire('filter:search.isAllowed', { + uid: req.uid, + query: req.query, + allowed, + })); + if (!allowed) { + return helpers.notAllowed(req, res); + } + + if (req.query.categories && !Array.isArray(req.query.categories)) { + req.query.categories = [req.query.categories]; + } + if (req.query.hasTags && !Array.isArray(req.query.hasTags)) { + req.query.hasTags = [req.query.hasTags]; + } + + const data = { + query: req.query.term, + searchIn: req.query.in, + matchWords: req.query.matchWords || 'all', + postedBy: req.query.by, + categories: req.query.categories, + searchChildren: req.query.searchChildren, + hasTags: req.query.hasTags, + replies: req.query.replies, + repliesFilter: req.query.repliesFilter, + timeRange: req.query.timeRange, + timeFilter: req.query.timeFilter, + sortBy: req.query.sortBy || meta.config.searchDefaultSortBy || '', + sortDirection: req.query.sortDirection, + page: page, + itemsPerPage: req.query.itemsPerPage, + uid: req.uid, + qs: req.query, + }; + + const [searchData, categoriesData] = await Promise.all([ + search.search(data), + buildCategories(req.uid, searchOnly), + recordSearch(data), + ]); + + searchData.pagination = pagination.create(page, searchData.pageCount, req.query); + searchData.multiplePages = searchData.pageCount > 1; + searchData.search_query = validator.escape(String(req.query.term || '')); + searchData.term = req.query.term; + + if (searchOnly) { + return res.json(searchData); + } + + searchData.allCategories = categoriesData; + searchData.allCategoriesCount = Math.max(10, Math.min(20, categoriesData.length)); + + searchData.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[global:search]]' }]); + searchData.expandSearch = !req.query.term; + + searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts'; + searchData.showAsTopics = req.query.showAs === 'topics'; + searchData.title = '[[global:header.search]]'; + + searchData.searchDefaultSortBy = meta.config.searchDefaultSortBy || ''; + searchData.searchDefaultIn = meta.config.searchDefaultIn || 'titlesposts'; + searchData.privileges = userPrivileges; + + res.render('search', searchData); +}; + +const searches = {}; + +async function recordSearch(data) { + const { query, searchIn } = data; + if (query) { + const cleanedQuery = String(query).trim().toLowerCase().slice(0, 255); + if (['titles', 'titlesposts', 'posts'].includes(searchIn) && cleanedQuery.length > 2) { + searches[data.uid] = searches[data.uid] || { timeoutId: 0, queries: [] }; + searches[data.uid].queries.push(cleanedQuery); + if (searches[data.uid].timeoutId) { + clearTimeout(searches[data.uid].timeoutId); + } + searches[data.uid].timeoutId = setTimeout(async () => { + if (searches[data.uid] && searches[data.uid].queries) { + const copy = searches[data.uid].queries.slice(); + const filtered = searches[data.uid].queries.filter( + q => !copy.find(query => query.startsWith(q) && query.length > q.length) + ); + delete searches[data.uid]; + await Promise.all(filtered.map(query => db.sortedSetIncrBy('searches:all', 1, query))); + } + }, 5000); + } + } +} + +async function buildCategories(uid, searchOnly) { + if (searchOnly) { + return []; + } + + const cids = await categories.getCidsByPrivilege('categories:cid', uid, 'read'); + let categoriesData = await categories.getCategoriesData(cids); + categoriesData = categoriesData.filter(category => category && !category.link); + categoriesData = categories.getTree(categoriesData); + categoriesData = categories.buildForSelectCategories(categoriesData, ['text', 'value']); + + return [ + { value: 'all', text: '[[unread:all_categories]]' }, + { value: 'watched', text: '[[category:watched-categories]]' }, + ].concat(categoriesData); +} diff --git a/src/controllers/sitemap.js b/src/controllers/sitemap.js new file mode 100644 index 0000000000..a3d3878de3 --- /dev/null +++ b/src/controllers/sitemap.js @@ -0,0 +1,40 @@ +'use strict'; + +const sitemap = require('../sitemap'); +const meta = require('../meta'); + +const sitemapController = module.exports; + +sitemapController.render = async function (req, res, next) { + if (meta.config['feeds:disableSitemap']) { + return setImmediate(next); + } + const tplData = await sitemap.render(); + const xml = await req.app.renderAsync('sitemap', tplData); + res.header('Content-Type', 'application/xml'); + res.send(xml); +}; + +sitemapController.getPages = function (req, res, next) { + sendSitemap(sitemap.getPages, res, next); +}; + +sitemapController.getCategories = function (req, res, next) { + sendSitemap(sitemap.getCategories, res, next); +}; + +sitemapController.getTopicPage = function (req, res, next) { + sendSitemap(async () => await sitemap.getTopicPage(parseInt(req.params[0], 10)), res, next); +}; + +async function sendSitemap(method, res, callback) { + if (meta.config['feeds:disableSitemap']) { + return setImmediate(callback); + } + const xml = await method(); + if (!xml) { + return callback(); + } + res.header('Content-Type', 'application/xml'); + res.send(xml); +} diff --git a/src/controllers/tags.js b/src/controllers/tags.js new file mode 100644 index 0000000000..45195ebbd3 --- /dev/null +++ b/src/controllers/tags.js @@ -0,0 +1,83 @@ +'use strict'; + +const validator = require('validator'); +const nconf = require('nconf'); + +const meta = require('../meta'); +const user = require('../user'); +const categories = require('../categories'); +const topics = require('../topics'); +const privileges = require('../privileges'); +const pagination = require('../pagination'); +const utils = require('../utils'); +const helpers = require('./helpers'); + +const tagsController = module.exports; + +tagsController.getTag = async function (req, res) { + const tag = validator.escape(utils.cleanUpTag(req.params.tag, meta.config.maximumTagLength)); + const page = parseInt(req.query.page, 10) || 1; + const cid = Array.isArray(req.query.cid) || !req.query.cid ? req.query.cid : [req.query.cid]; + + const templateData = { + topics: [], + tag: tag, + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[tags:tags]]', url: '/tags' }, { text: tag }]), + title: `[[pages:tag, ${tag}]]`, + }; + const [settings, cids, categoryData, isPrivileged] = await Promise.all([ + user.getSettings(req.uid), + cid || categories.getCidsByPrivilege('categories:cid', req.uid, 'topics:read'), + helpers.getSelectedCategory(cid), + user.isPrivileged(req.uid), + ]); + const start = Math.max(0, (page - 1) * settings.topicsPerPage); + const stop = start + settings.topicsPerPage - 1; + + const [topicCount, tids] = await Promise.all([ + topics.getTagTopicCount(tag, cids), + topics.getTagTidsByCids(tag, cids, start, stop), + ]); + + templateData.topics = await topics.getTopics(tids, req.uid); + templateData.showSelect = isPrivileged; + templateData.showTopicTools = isPrivileged; + templateData.allCategoriesUrl = `tags/${tag}${helpers.buildQueryString(req.query, 'cid', '')}`; + templateData.selectedCategory = categoryData.selectedCategory; + templateData.selectedCids = categoryData.selectedCids; + topics.calculateTopicIndices(templateData.topics, start); + res.locals.metaTags = [ + { + name: 'title', + content: tag, + }, + { + property: 'og:title', + content: tag, + }, + ]; + + const pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); + templateData.pagination = pagination.create(page, pageCount, req.query); + helpers.addLinkTags({ url: `tags/${tag}`, res: req.res, tags: templateData.pagination.rel }); + + templateData['feeds:disableRSS'] = meta.config['feeds:disableRSS']; + templateData.rssFeedUrl = `${nconf.get('relative_path')}/tags/${tag}.rss`; + res.render('tag', templateData); +}; + +tagsController.getTags = async function (req, res) { + const cids = await categories.getCidsByPrivilege('categories:cid', req.uid, 'topics:read'); + const [canSearch, tags] = await Promise.all([ + privileges.global.can('search:tags', req.uid), + topics.getCategoryTagsData(cids, 0, 99), + ]); + + res.render('tags', { + tags: tags.filter(Boolean), + displayTagSearch: canSearch, + nextStart: 100, + breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[tags:tags]]' }]), + title: '[[pages:tags]]', + }); +}; diff --git a/src/controllers/top.js b/src/controllers/top.js new file mode 100644 index 0000000000..799630ec4c --- /dev/null +++ b/src/controllers/top.js @@ -0,0 +1,28 @@ + +'use strict'; + +const nconf = require('nconf'); +const validator = require('validator'); + +const helpers = require('./helpers'); +const recentController = require('./recent'); + +const topController = module.exports; + +topController.get = async function (req, res, next) { + const data = await recentController.getData(req, 'top', 'votes'); + if (!data) { + return next(); + } + const term = helpers.terms[req.query.term] || 'alltime'; + if (req.originalUrl.startsWith(`${nconf.get('relative_path')}/api/top`) || req.originalUrl.startsWith(`${nconf.get('relative_path')}/top`)) { + data.title = `[[pages:top-${term}]]`; + } + + const feedQs = data.rssFeedUrl.split('?')[1]; + data.rssFeedUrl = `${nconf.get('relative_path')}/top/${validator.escape(String(req.query.term || 'alltime'))}.rss`; + if (req.loggedIn) { + data.rssFeedUrl += `?${feedQs}`; + } + res.render('top', data); +}; diff --git a/src/controllers/topics.js b/src/controllers/topics.js new file mode 100644 index 0000000000..eefc268197 --- /dev/null +++ b/src/controllers/topics.js @@ -0,0 +1,374 @@ +'use strict'; + +const nconf = require('nconf'); +const qs = require('querystring'); + +const user = require('../user'); +const meta = require('../meta'); +const topics = require('../topics'); +const categories = require('../categories'); +const posts = require('../posts'); +const privileges = require('../privileges'); +const helpers = require('./helpers'); +const pagination = require('../pagination'); +const utils = require('../utils'); +const analytics = require('../analytics'); + +const topicsController = module.exports; + +const url = nconf.get('url'); +const relative_path = nconf.get('relative_path'); +const upload_url = nconf.get('upload_url'); + +topicsController.get = async function getTopic(req, res, next) { + const tid = req.params.topic_id; + + if ( + (req.params.post_index && !utils.isNumber(req.params.post_index) && req.params.post_index !== 'unread') || + !utils.isNumber(tid) + ) { + return next(); + } + let postIndex = parseInt(req.params.post_index, 10) || 1; + const [ + userPrivileges, + settings, + topicData, + rssToken, + ] = await Promise.all([ + privileges.topics.get(tid, req.uid), + user.getSettings(req.uid), + topics.getTopicData(tid), + user.auth.getFeedToken(req.uid), + ]); + + let currentPage = parseInt(req.query.page, 10) || 1; + const pageCount = Math.max(1, Math.ceil((topicData && topicData.postcount) / settings.postsPerPage)); + const invalidPagination = (settings.usePagination && (currentPage < 1 || currentPage > pageCount)); + if ( + !topicData || + userPrivileges.disabled || + invalidPagination || + (topicData.scheduled && !userPrivileges.view_scheduled) + ) { + return next(); + } + + if (!userPrivileges['topics:read'] || (!topicData.scheduled && topicData.deleted && !userPrivileges.view_deleted)) { + return helpers.notAllowed(req, res); + } + + if (req.params.post_index === 'unread') { + postIndex = await topics.getUserBookmark(tid, req.uid); + } + + if (!res.locals.isAPI && (!req.params.slug || topicData.slug !== `${tid}/${req.params.slug}`) && (topicData.slug && topicData.slug !== `${tid}/`)) { + return helpers.redirect(res, `/topic/${topicData.slug}${postIndex ? `/${postIndex}` : ''}${generateQueryString(req.query)}`, true); + } + + if (utils.isNumber(postIndex) && topicData.postcount > 0 && (postIndex < 1 || postIndex > topicData.postcount)) { + return helpers.redirect(res, `/topic/${tid}/${req.params.slug}${postIndex > topicData.postcount ? `/${topicData.postcount}` : ''}${generateQueryString(req.query)}`); + } + postIndex = Math.max(1, postIndex); + const sort = req.query.sort || settings.topicPostSort; + const set = sort === 'most_votes' ? `tid:${tid}:posts:votes` : `tid:${tid}:posts`; + const reverse = sort === 'newest_to_oldest' || sort === 'most_votes'; + if (settings.usePagination && !req.query.page) { + currentPage = calculatePageFromIndex(postIndex, settings); + } + const { start, stop } = calculateStartStop(currentPage, postIndex, settings); + + await topics.getTopicWithPosts(topicData, set, req.uid, start, stop, reverse); + + topics.modifyPostsByPrivilege(topicData, userPrivileges); + topicData.tagWhitelist = categories.filterTagWhitelist(topicData.tagWhitelist, userPrivileges.isAdminOrMod); + + topicData.privileges = userPrivileges; + topicData.topicStaleDays = meta.config.topicStaleDays; + topicData['reputation:disabled'] = meta.config['reputation:disabled']; + topicData['downvote:disabled'] = meta.config['downvote:disabled']; + topicData['feeds:disableRSS'] = meta.config['feeds:disableRSS'] || 0; + topicData['signatures:hideDuplicates'] = meta.config['signatures:hideDuplicates']; + topicData.bookmarkThreshold = meta.config.bookmarkThreshold; + topicData.necroThreshold = meta.config.necroThreshold; + topicData.postEditDuration = meta.config.postEditDuration; + topicData.postDeleteDuration = meta.config.postDeleteDuration; + topicData.scrollToMyPost = settings.scrollToMyPost; + topicData.updateUrlWithPostIndex = settings.updateUrlWithPostIndex; + topicData.allowMultipleBadges = meta.config.allowMultipleBadges === 1; + topicData.privateUploads = meta.config.privateUploads === 1; + topicData.showPostPreviewsOnHover = meta.config.showPostPreviewsOnHover === 1; + topicData.rssFeedUrl = `${relative_path}/topic/${topicData.tid}.rss`; + if (req.loggedIn) { + topicData.rssFeedUrl += `?uid=${req.uid}&token=${rssToken}`; + } + + topicData.postIndex = postIndex; + + await Promise.all([ + buildBreadcrumbs(topicData), + addOldCategory(topicData, userPrivileges), + addTags(topicData, req, res), + incrementViewCount(req, tid), + markAsRead(req, tid), + analytics.increment([`pageviews:byCid:${topicData.category.cid}`]), + ]); + + topicData.pagination = pagination.create(currentPage, pageCount, req.query); + topicData.pagination.rel.forEach((rel) => { + rel.href = `${url}/topic/${topicData.slug}${rel.href}`; + res.locals.linkTags.push(rel); + }); + + res.render('topic', topicData); +}; + +function generateQueryString(query) { + const qString = qs.stringify(query); + return qString.length ? `?${qString}` : ''; +} + +function calculatePageFromIndex(postIndex, settings) { + return 1 + Math.floor((postIndex - 1) / settings.postsPerPage); +} + +function calculateStartStop(page, postIndex, settings) { + let startSkip = 0; + + if (!settings.usePagination) { + if (postIndex > 1) { + page = 1; + } + startSkip = Math.max(0, postIndex - Math.ceil(settings.postsPerPage / 2)); + } + + const start = ((page - 1) * settings.postsPerPage) + startSkip; + const stop = start + settings.postsPerPage - 1; + return { start: Math.max(0, start), stop: Math.max(0, stop) }; +} + +async function incrementViewCount(req, tid) { + const allow = req.uid > 0 || (meta.config.guestsIncrementTopicViews && req.uid === 0); + if (allow) { + req.session.tids_viewed = req.session.tids_viewed || {}; + const now = Date.now(); + const interval = meta.config.incrementTopicViewsInterval * 60000; + if (!req.session.tids_viewed[tid] || req.session.tids_viewed[tid] < now - interval) { + await topics.increaseViewCount(tid); + req.session.tids_viewed[tid] = now; + } + } +} + +async function markAsRead(req, tid) { + if (req.loggedIn) { + const markedRead = await topics.markAsRead([tid], req.uid); + const promises = [topics.markTopicNotificationsRead([tid], req.uid)]; + if (markedRead) { + promises.push(topics.pushUnreadCount(req.uid)); + } + await Promise.all(promises); + } +} + +async function buildBreadcrumbs(topicData) { + const breadcrumbs = [ + { + text: topicData.category.name, + url: `${relative_path}/category/${topicData.category.slug}`, + cid: topicData.category.cid, + }, + { + text: topicData.title, + }, + ]; + const parentCrumbs = await helpers.buildCategoryBreadcrumbs(topicData.category.parentCid); + topicData.breadcrumbs = parentCrumbs.concat(breadcrumbs); +} + +async function addOldCategory(topicData, userPrivileges) { + if (userPrivileges.isAdminOrMod && topicData.oldCid) { + topicData.oldCategory = await categories.getCategoryFields( + topicData.oldCid, ['cid', 'name', 'icon', 'bgColor', 'color', 'slug'] + ); + } +} + +async function addTags(topicData, req, res) { + 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)); + let description = ''; + if (postAtIndex && postAtIndex.content) { + description = utils.stripHTMLTags(utils.decodeHTMLEntities(postAtIndex.content)); + } + + if (description.length > 255) { + description = `${description.slice(0, 255)}...`; + } + description = description.replace(/\n/g, ' '); + + res.locals.metaTags = [ + { + name: 'title', + content: topicData.titleRaw, + }, + { + name: 'description', + content: description, + }, + { + property: 'og:title', + content: topicData.titleRaw, + }, + { + property: 'og:description', + content: description, + }, + { + property: 'og:type', + content: 'article', + }, + { + property: 'article:published_time', + content: utils.toISOString(topicData.timestamp), + }, + { + property: 'article:modified_time', + content: utils.toISOString(topicData.lastposttime), + }, + { + property: 'article:section', + content: topicData.category ? topicData.category.name : '', + }, + ]; + + await addOGImageTags(res, topicData, postAtIndex); + + res.locals.linkTags = [ + { + rel: 'canonical', + href: `${url}/topic/${topicData.slug}`, + }, + ]; + + if (!topicData['feeds:disableRSS']) { + res.locals.linkTags.push({ + rel: 'alternate', + type: 'application/rss+xml', + href: topicData.rssFeedUrl, + }); + } + + if (topicData.category) { + res.locals.linkTags.push({ + rel: 'up', + href: `${url}/category/${topicData.category.slug}`, + }); + } +} + +async function addOGImageTags(res, topicData, postAtIndex) { + const uploads = postAtIndex ? await posts.uploads.listWithSizes(postAtIndex.pid) : []; + const images = uploads.map((upload) => { + upload.name = `${url + upload_url}/${upload.name}`; + return upload; + }); + if (topicData.thumbs) { + const path = require('path'); + const thumbs = topicData.thumbs.filter( + t => t && images.every(img => path.normalize(img.name) !== path.normalize(url + t.url)) + ); + images.push(...thumbs.map(thumbObj => ({ name: url + thumbObj.url }))); + } + if (topicData.category.backgroundImage && (!postAtIndex || !postAtIndex.index)) { + images.push(topicData.category.backgroundImage); + } + if (postAtIndex && postAtIndex.user && postAtIndex.user.picture) { + images.push(postAtIndex.user.picture); + } + images.forEach(path => addOGImageTag(res, path)); +} + +function addOGImageTag(res, image) { + let imageUrl; + if (typeof image === 'string' && !image.startsWith('http')) { + imageUrl = url + image.replace(new RegExp(`^${relative_path}`), ''); + } else if (typeof image === 'object') { + imageUrl = image.name; + } else { + imageUrl = image; + } + + res.locals.metaTags.push({ + property: 'og:image', + content: imageUrl, + noEscape: true, + }, { + property: 'og:image:url', + content: imageUrl, + noEscape: true, + }); + + if (typeof image === 'object' && image.width && image.height) { + res.locals.metaTags.push({ + property: 'og:image:width', + content: String(image.width), + }, { + property: 'og:image:height', + content: String(image.height), + }); + } +} + +topicsController.teaser = async function (req, res, next) { + const tid = req.params.topic_id; + if (!utils.isNumber(tid)) { + return next(); + } + const canRead = await privileges.topics.can('topics:read', tid, req.uid); + if (!canRead) { + return res.status(403).json('[[error:no-privileges]]'); + } + const pid = await topics.getLatestUndeletedPid(tid); + if (!pid) { + return res.status(404).json('not-found'); + } + const postData = await posts.getPostSummaryByPids([pid], req.uid, { stripTags: false }); + if (!postData.length) { + return res.status(404).json('not-found'); + } + res.json(postData[0]); +}; + +topicsController.pagination = async function (req, res, next) { + const tid = req.params.topic_id; + const currentPage = parseInt(req.query.page, 10) || 1; + + if (!utils.isNumber(tid)) { + return next(); + } + + const [userPrivileges, settings, topic] = await Promise.all([ + privileges.topics.get(tid, req.uid), + user.getSettings(req.uid), + topics.getTopicData(tid), + ]); + + if (!topic) { + return next(); + } + + if (!userPrivileges.read || !privileges.topics.canViewDeletedScheduled(topic, userPrivileges)) { + return helpers.notAllowed(req, res); + } + + const postCount = topic.postcount; + const pageCount = Math.max(1, Math.ceil(postCount / settings.postsPerPage)); + + const paginationData = pagination.create(currentPage, pageCount); + paginationData.rel.forEach((rel) => { + rel.href = `${url}/topic/${topic.slug}${rel.href}`; + }); + + res.json({ pagination: paginationData }); +}; diff --git a/src/controllers/unread.js b/src/controllers/unread.js new file mode 100644 index 0000000000..66601ca9a3 --- /dev/null +++ b/src/controllers/unread.js @@ -0,0 +1,78 @@ + +'use strict'; + +const nconf = require('nconf'); +const querystring = require('querystring'); + +const meta = require('../meta'); +const pagination = require('../pagination'); +const user = require('../user'); +const topics = require('../topics'); +const helpers = require('./helpers'); + +const unreadController = module.exports; +const relative_path = nconf.get('relative_path'); + +unreadController.get = async function (req, res) { + const { cid } = req.query; + const filter = req.query.filter || ''; + + const [categoryData, userSettings, isPrivileged] = await Promise.all([ + helpers.getSelectedCategory(cid), + user.getSettings(req.uid), + user.isPrivileged(req.uid), + ]); + + const page = parseInt(req.query.page, 10) || 1; + const start = Math.max(0, (page - 1) * userSettings.topicsPerPage); + const stop = start + userSettings.topicsPerPage - 1; + const data = await topics.getUnreadTopics({ + cid: cid, + uid: req.uid, + start: start, + stop: stop, + filter: filter, + query: req.query, + }); + + const isDisplayedAsHome = !(req.originalUrl.startsWith(`${relative_path}/api/unread`) || req.originalUrl.startsWith(`${relative_path}/unread`)); + const baseUrl = isDisplayedAsHome ? '' : 'unread'; + + if (isDisplayedAsHome) { + data.title = meta.config.homePageTitle || '[[pages:home]]'; + } else { + data.title = '[[pages:unread]]'; + data.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[unread:title]]' }]); + } + + data.pageCount = Math.max(1, Math.ceil(data.topicCount / userSettings.topicsPerPage)); + data.pagination = pagination.create(page, data.pageCount, req.query); + helpers.addLinkTags({ url: 'unread', res: req.res, tags: data.pagination.rel }); + + if (userSettings.usePagination && (page < 1 || page > data.pageCount)) { + req.query.page = Math.max(1, Math.min(data.pageCount, page)); + return helpers.redirect(res, `/unread?${querystring.stringify(req.query)}`); + } + data.showSelect = true; + data.showTopicTools = isPrivileged; + data.allCategoriesUrl = `${baseUrl}${helpers.buildQueryString(req.query, 'cid', '')}`; + data.selectedCategory = categoryData.selectedCategory; + data.selectedCids = categoryData.selectedCids; + data.selectCategoryLabel = '[[unread:mark_as_read]]'; + data.selectCategoryIcon = 'fa-inbox'; + data.showCategorySelectLabel = true; + data.filters = helpers.buildFilters(baseUrl, filter, req.query); + data.selectedFilter = data.filters.find(filter => filter && filter.selected); + + res.render('unread', data); +}; + +unreadController.unreadTotal = async function (req, res, next) { + const filter = req.query.filter || ''; + try { + const unreadCount = await topics.getTotalUnread(req.uid, filter); + res.json(unreadCount); + } catch (err) { + next(err); + } +}; diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js new file mode 100644 index 0000000000..d1eee4a636 --- /dev/null +++ b/src/controllers/uploads.js @@ -0,0 +1,203 @@ +'use strict'; + +const path = require('path'); +const nconf = require('nconf'); +const validator = require('validator'); + +const user = require('../user'); +const meta = require('../meta'); +const file = require('../file'); +const plugins = require('../plugins'); +const image = require('../image'); +const privileges = require('../privileges'); + +const helpers = require('./helpers'); + +const uploadsController = module.exports; + +uploadsController.upload = async function (req, res, filesIterator) { + let files; + try { + files = req.files.files; + } catch (e) { + return helpers.formatApiResponse(400, res); + } + + // These checks added because of odd behaviour by request: https://github.com/request/request/issues/2445 + if (!Array.isArray(files)) { + return helpers.formatApiResponse(500, res, new Error('[[error:invalid-file]]')); + } + if (Array.isArray(files[0])) { + files = files[0]; + } + + try { + const images = []; + for (const fileObj of files) { + /* eslint-disable no-await-in-loop */ + images.push(await filesIterator(fileObj)); + } + + helpers.formatApiResponse(200, res, { images }); + + return images; + } catch (err) { + return helpers.formatApiResponse(500, res, err); + } finally { + deleteTempFiles(files); + } +}; + +uploadsController.uploadPost = async function (req, res) { + await uploadsController.upload(req, res, async (uploadedFile) => { + const isImage = uploadedFile.type.match(/image./); + if (isImage) { + return await uploadAsImage(req, uploadedFile); + } + return await uploadAsFile(req, uploadedFile); + }); +}; + +async function uploadAsImage(req, uploadedFile) { + const canUpload = await privileges.global.can('upload:post:image', req.uid); + if (!canUpload) { + throw new Error('[[error:no-privileges]]'); + } + await image.checkDimensions(uploadedFile.path); + await image.stripEXIF(uploadedFile.path); + + if (plugins.hooks.hasListeners('filter:uploadImage')) { + return await plugins.hooks.fire('filter:uploadImage', { + image: uploadedFile, + uid: req.uid, + folder: 'files', + }); + } + await image.isFileTypeAllowed(uploadedFile.path); + + let fileObj = await uploadsController.uploadFile(req.uid, uploadedFile); + // sharp can't save svgs skip resize for them + const isSVG = uploadedFile.type === 'image/svg+xml'; + if (isSVG || meta.config.resizeImageWidth === 0 || meta.config.resizeImageWidthThreshold === 0) { + return fileObj; + } + + fileObj = await resizeImage(fileObj); + return { url: fileObj.url }; +} + +async function uploadAsFile(req, uploadedFile) { + const canUpload = await privileges.global.can('upload:post:file', req.uid); + if (!canUpload) { + throw new Error('[[error:no-privileges]]'); + } + + const fileObj = await uploadsController.uploadFile(req.uid, uploadedFile); + return { + url: fileObj.url, + name: fileObj.name, + }; +} + +async function resizeImage(fileObj) { + const imageData = await image.size(fileObj.path); + if ( + imageData.width < meta.config.resizeImageWidthThreshold || + meta.config.resizeImageWidth > meta.config.resizeImageWidthThreshold + ) { + return fileObj; + } + + await image.resizeImage({ + path: fileObj.path, + target: file.appendToFileName(fileObj.path, '-resized'), + width: meta.config.resizeImageWidth, + quality: meta.config.resizeImageQuality, + }); + // Return the resized version to the composer/postData + fileObj.url = file.appendToFileName(fileObj.url, '-resized'); + + return fileObj; +} + +uploadsController.uploadThumb = async function (req, res) { + if (!meta.config.allowTopicsThumbnail) { + deleteTempFiles(req.files.files); + return helpers.formatApiResponse(503, res, new Error('[[error:topic-thumbnails-are-disabled]]')); + } + + return await uploadsController.upload(req, res, async (uploadedFile) => { + if (!uploadedFile.type.match(/image./)) { + throw new Error('[[error:invalid-file]]'); + } + await image.isFileTypeAllowed(uploadedFile.path); + const dimensions = await image.checkDimensions(uploadedFile.path); + + if (dimensions.width > parseInt(meta.config.topicThumbSize, 10)) { + await image.resizeImage({ + path: uploadedFile.path, + width: meta.config.topicThumbSize, + }); + } + if (plugins.hooks.hasListeners('filter:uploadImage')) { + return await plugins.hooks.fire('filter:uploadImage', { + image: uploadedFile, + uid: req.uid, + folder: 'files', + }); + } + + return await uploadsController.uploadFile(req.uid, uploadedFile); + }); +}; + +uploadsController.uploadFile = async function (uid, uploadedFile) { + if (plugins.hooks.hasListeners('filter:uploadFile')) { + return await plugins.hooks.fire('filter:uploadFile', { + file: uploadedFile, + uid: uid, + folder: 'files', + }); + } + + if (!uploadedFile) { + throw new Error('[[error:invalid-file]]'); + } + + if (uploadedFile.size > meta.config.maximumFileSize * 1024) { + throw new Error(`[[error:file-too-big, ${meta.config.maximumFileSize}]]`); + } + + const allowed = file.allowedExtensions(); + + const extension = path.extname(uploadedFile.name).toLowerCase(); + if (allowed.length > 0 && (!extension || extension === '.' || !allowed.includes(extension))) { + throw new Error(`[[error:invalid-file-type, ${allowed.join(', ')}]]`); + } + + return await saveFileToLocal(uid, 'files', uploadedFile); +}; + +async function saveFileToLocal(uid, folder, uploadedFile) { + const name = uploadedFile.name || 'upload'; + const extension = path.extname(name) || ''; + + const filename = `${Date.now()}-${validator.escape(name.slice(0, -extension.length)).slice(0, 255)}${extension}`; + + const upload = await file.saveFileToLocal(filename, folder, uploadedFile.path); + const storedFile = { + url: nconf.get('relative_path') + upload.url, + path: upload.path, + name: uploadedFile.name, + }; + + await user.associateUpload(uid, upload.url.replace(`${nconf.get('upload_url')}/`, '')); + const data = await plugins.hooks.fire('filter:uploadStored', { uid: uid, uploadedFile: uploadedFile, storedFile: storedFile }); + return data.storedFile; +} + +function deleteTempFiles(files) { + files.forEach(fileObj => file.delete(fileObj.path)); +} + +require('../promisify')(uploadsController, ['upload', 'uploadPost', 'uploadThumb']); diff --git a/src/controllers/user.js b/src/controllers/user.js new file mode 100644 index 0000000000..673a8cf75a --- /dev/null +++ b/src/controllers/user.js @@ -0,0 +1,118 @@ +'use strict'; + +const path = require('path'); +const winston = require('winston'); + +const user = require('../user'); +const privileges = require('../privileges'); +const accountHelpers = require('./accounts/helpers'); + +const userController = module.exports; + +userController.getCurrentUser = async function (req, res) { + if (!req.loggedIn) { + return res.status(401).json('not-authorized'); + } + const userslug = await user.getUserField(req.uid, 'userslug'); + const userData = await accountHelpers.getUserDataByUserSlug(userslug, req.uid, req.query); + res.json(userData); +}; + +userController.getUserByUID = async function (req, res, next) { + await byType('uid', req, res, next); +}; + +userController.getUserByUsername = async function (req, res, next) { + await byType('username', req, res, next); +}; + +userController.getUserByEmail = async function (req, res, next) { + await byType('email', req, res, next); +}; + +async function byType(type, req, res, next) { + const userData = await userController.getUserDataByField(req.uid, type, req.params[type]); + if (!userData) { + return next(); + } + res.json(userData); +} + +userController.getUserDataByField = async function (callerUid, field, fieldValue) { + let uid = null; + if (field === 'uid') { + uid = fieldValue; + } else if (field === 'username') { + uid = await user.getUidByUsername(fieldValue); + } else if (field === 'email') { + uid = await user.getUidByEmail(fieldValue); + if (uid) { + const isPrivileged = await user.isAdminOrGlobalMod(callerUid); + const settings = await user.getSettings(uid); + if (!isPrivileged && (settings && !settings.showemail)) { + uid = 0; + } + } + } + if (!uid) { + return null; + } + return await userController.getUserDataByUID(callerUid, uid); +}; + +userController.getUserDataByUID = async function (callerUid, uid) { + if (!parseInt(uid, 10)) { + throw new Error('[[error:no-user]]'); + } + const canView = await privileges.global.can('view:users', callerUid); + if (!canView) { + throw new Error('[[error:no-privileges]]'); + } + + let userData = await user.getUserData(uid); + if (!userData) { + throw new Error('[[error:no-user]]'); + } + + userData = await user.hidePrivateData(userData, callerUid); + + return userData; +}; + +userController.exportPosts = async function (req, res, next) { + sendExport(`${res.locals.uid}_posts.csv`, 'text/csv', res, next); +}; + +userController.exportUploads = function (req, res, next) { + sendExport(`${res.locals.uid}_uploads.zip`, 'application/zip', res, next); +}; + +userController.exportProfile = async function (req, res, next) { + sendExport(`${res.locals.uid}_profile.json`, 'application/json', res, next); +}; + +// DEPRECATED; Remove in NodeBB v3.0.0 +function sendExport(filename, type, res, next) { + winston.warn(`[users/export] Access via page API is deprecated, use GET /api/v3/users/:uid/exports/:type instead.`); + + res.sendFile(filename, { + root: path.join(__dirname, '../../build/export'), + headers: { + 'Content-Type': type, + 'Content-Disposition': `attachment; filename=${filename}`, + }, + }, (err) => { + if (err) { + if (err.code === 'ENOENT') { + res.locals.isAPI = false; + return next(); + } + return next(err); + } + }); +} + +require('../promisify')(userController, [ + 'getCurrentUser', 'getUserByUID', 'getUserByUsername', 'getUserByEmail', + 'exportPosts', 'exportUploads', 'exportProfile', +]); diff --git a/src/controllers/users.js b/src/controllers/users.js new file mode 100644 index 0000000000..acc4e30e9d --- /dev/null +++ b/src/controllers/users.js @@ -0,0 +1,212 @@ +'use strict'; + +const user = require('../user'); +const meta = require('../meta'); + +const db = require('../database'); +const pagination = require('../pagination'); +const privileges = require('../privileges'); +const helpers = require('./helpers'); +const api = require('../api'); +const utils = require('../utils'); + +const usersController = module.exports; + +usersController.index = async function (req, res, next) { + const section = req.query.section || 'joindate'; + const sectionToController = { + joindate: usersController.getUsersSortedByJoinDate, + online: usersController.getOnlineUsers, + 'sort-posts': usersController.getUsersSortedByPosts, + 'sort-reputation': usersController.getUsersSortedByReputation, + banned: usersController.getBannedUsers, + flagged: usersController.getFlaggedUsers, + }; + + if (req.query.query) { + await usersController.search(req, res, next); + } else if (sectionToController[section]) { + await sectionToController[section](req, res, next); + } else { + await usersController.getUsersSortedByJoinDate(req, res, next); + } +}; + +usersController.search = async function (req, res) { + const searchData = await api.users.search(req, req.query); + + const section = req.query.section || 'joindate'; + + searchData.pagination = pagination.create(req.query.page, searchData.pageCount, req.query); + searchData[`section_${section}`] = true; + searchData.displayUserSearch = true; + await render(req, res, searchData); +}; + +usersController.getOnlineUsers = async function (req, res) { + const [userData, guests] = await Promise.all([ + usersController.getUsers('users:online', req.uid, req.query), + require('../socket.io/admin/rooms').getTotalGuestCount(), + ]); + + let hiddenCount = 0; + if (!userData.isAdminOrGlobalMod) { + userData.users = userData.users.filter((user) => { + const showUser = user && (user.uid === req.uid || user.userStatus !== 'offline'); + if (!showUser) { + hiddenCount += 1; + } + return showUser; + }); + } + + userData.anonymousUserCount = guests + hiddenCount; + userData.timeagoCutoff = 1000 * 60 * 60 * 24; + + await render(req, res, userData); +}; + +usersController.getUsersSortedByPosts = async function (req, res) { + await usersController.renderUsersPage('users:postcount', req, res); +}; + +usersController.getUsersSortedByReputation = async function (req, res, next) { + if (meta.config['reputation:disabled']) { + return next(); + } + await usersController.renderUsersPage('users:reputation', req, res); +}; + +usersController.getUsersSortedByJoinDate = async function (req, res) { + await usersController.renderUsersPage('users:joindate', req, res); +}; + +usersController.getBannedUsers = async function (req, res) { + await renderIfAdminOrGlobalMod('users:banned', req, res); +}; + +usersController.getFlaggedUsers = async function (req, res) { + await renderIfAdminOrGlobalMod('users:flags', req, res); +}; + +async function renderIfAdminOrGlobalMod(set, req, res) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(req.uid); + if (!isAdminOrGlobalMod) { + return helpers.notAllowed(req, res); + } + await usersController.renderUsersPage(set, req, res); +} + +usersController.renderUsersPage = async function (set, req, res) { + const userData = await usersController.getUsers(set, req.uid, req.query); + await render(req, res, userData); +}; + +usersController.getUsers = async function (set, uid, query) { + const setToData = { + 'users:postcount': { title: '[[pages:users/sort-posts]]', crumb: '[[users:top_posters]]' }, + 'users:reputation': { title: '[[pages:users/sort-reputation]]', crumb: '[[users:most_reputation]]' }, + 'users:joindate': { title: '[[pages:users/latest]]', crumb: '[[global:users]]' }, + 'users:online': { title: '[[pages:users/online]]', crumb: '[[global:online]]' }, + 'users:banned': { title: '[[pages:users/banned]]', crumb: '[[user:banned]]' }, + 'users:flags': { title: '[[pages:users/most-flags]]', crumb: '[[users:most_flags]]' }, + }; + + if (!setToData[set]) { + setToData[set] = { title: '', crumb: '' }; + } + + const breadcrumbs = [{ text: setToData[set].crumb }]; + + if (set !== 'users:joindate') { + breadcrumbs.unshift({ text: '[[global:users]]', url: '/users' }); + } + + const page = parseInt(query.page, 10) || 1; + const resultsPerPage = meta.config.userSearchResultsPerPage; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage - 1; + + const [isAdmin, isGlobalMod, canSearch, usersData] = await Promise.all([ + user.isAdministrator(uid), + user.isGlobalModerator(uid), + privileges.global.can('search:users', uid), + usersController.getUsersAndCount(set, uid, start, stop), + ]); + const pageCount = Math.ceil(usersData.count / resultsPerPage); + return { + users: usersData.users, + pagination: pagination.create(page, pageCount, query), + userCount: usersData.count, + title: setToData[set].title || '[[pages:users/latest]]', + breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs), + isAdminOrGlobalMod: isAdmin || isGlobalMod, + isAdmin: isAdmin, + isGlobalMod: isGlobalMod, + displayUserSearch: canSearch, + [`section_${query.section || 'joindate'}`]: true, + }; +}; + +usersController.getUsersAndCount = async function (set, uid, start, stop) { + async function getCount() { + if (set === 'users:online') { + return await db.sortedSetCount('users:online', Date.now() - 86400000, '+inf'); + } else if (set === 'users:banned' || set === 'users:flags') { + return await db.sortedSetCard(set); + } + return await db.getObjectField('global', 'userCount'); + } + async function getUsers() { + if (set === 'users:online') { + const count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; + const data = await db.getSortedSetRevRangeByScoreWithScores(set, start, count, '+inf', Date.now() - 86400000); + const uids = data.map(d => d.value); + const scores = data.map(d => d.score); + const [userStatus, userData] = await Promise.all([ + db.getObjectsFields(uids.map(uid => `user:${uid}`), ['status']), + user.getUsers(uids, uid), + ]); + + userData.forEach((user, i) => { + if (user) { + user.lastonline = scores[i]; + user.lastonlineISO = utils.toISOString(user.lastonline); + user.userStatus = userStatus[i].status || 'online'; + } + }); + return userData; + } + return await user.getUsersFromSet(set, uid, start, stop); + } + const [usersData, count] = await Promise.all([ + getUsers(), + getCount(), + ]); + return { + users: usersData.filter(user => user && parseInt(user.uid, 10)), + count: count, + }; +}; + +async function render(req, res, data) { + const { registrationType } = meta.config; + + data.maximumInvites = meta.config.maximumInvites; + data.inviteOnly = registrationType === 'invite-only' || registrationType === 'admin-invite-only'; + data.adminInviteOnly = registrationType === 'admin-invite-only'; + data.invites = await user.getInvitesNumber(req.uid); + + data.showInviteButton = false; + if (data.adminInviteOnly) { + data.showInviteButton = await privileges.users.isAdministrator(req.uid); + } else if (req.loggedIn) { + const canInvite = await privileges.users.hasInvitePrivilege(req.uid); + data.showInviteButton = canInvite && (!data.maximumInvites || data.invites < data.maximumInvites); + } + + data['reputation:disabled'] = meta.config['reputation:disabled']; + + res.append('X-Total-Count', data.userCount); + res.render('users', data); +} diff --git a/src/controllers/write/admin.js b/src/controllers/write/admin.js new file mode 100644 index 0000000000..e5963bab30 --- /dev/null +++ b/src/controllers/write/admin.js @@ -0,0 +1,42 @@ +'use strict'; + +const meta = require('../../meta'); +const privileges = require('../../privileges'); +const analytics = require('../../analytics'); + +const helpers = require('../helpers'); + +const Admin = module.exports; + +Admin.updateSetting = async (req, res) => { + const ok = await privileges.admin.can('admin:settings', req.uid); + + if (!ok) { + return helpers.formatApiResponse(403, res); + } + + await meta.configs.set(req.params.setting, req.body.value); + helpers.formatApiResponse(200, res); +}; + +Admin.getAnalyticsKeys = async (req, res) => { + let keys = await analytics.getKeys(); + + // Sort keys alphabetically + keys = keys.sort((a, b) => (a < b ? -1 : 1)); + + helpers.formatApiResponse(200, res, { keys }); +}; + +Admin.getAnalyticsData = async (req, res) => { + // Default returns views from past 24 hours, by hour + if (!req.query.amount) { + if (req.query.units === 'days') { + req.query.amount = 30; + } else { + req.query.amount = 24; + } + } + const getStats = req.query.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; + helpers.formatApiResponse(200, res, await getStats(`analytics:${req.params.set}`, parseInt(req.query.until, 10) || Date.now(), req.query.amount)); +}; diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js new file mode 100644 index 0000000000..be85a74d0c --- /dev/null +++ b/src/controllers/write/categories.js @@ -0,0 +1,82 @@ +'use strict'; + +const privileges = require('../../privileges'); +const categories = require('../../categories'); +const api = require('../../api'); + +const helpers = require('../helpers'); + +const Categories = module.exports; + +const hasAdminPrivilege = async (uid) => { + const ok = await privileges.admin.can(`admin:categories`, uid); + if (!ok) { + throw new Error('[[error:no-privileges]]'); + } +}; + +Categories.get = async (req, res) => { + helpers.formatApiResponse(200, res, await api.categories.get(req, req.params)); +}; + +Categories.create = async (req, res) => { + await hasAdminPrivilege(req.uid); + + const response = await api.categories.create(req, req.body); + helpers.formatApiResponse(200, res, response); +}; + +Categories.update = async (req, res) => { + await hasAdminPrivilege(req.uid); + + const payload = {}; + payload[req.params.cid] = req.body; + await api.categories.update(req, payload); + const categoryObjs = await categories.getCategories([req.params.cid]); + helpers.formatApiResponse(200, res, categoryObjs[0]); +}; + +Categories.delete = async (req, res) => { + await hasAdminPrivilege(req.uid); + + await api.categories.delete(req, { cid: req.params.cid }); + helpers.formatApiResponse(200, res); +}; + +Categories.getPrivileges = async (req, res) => { + if (!await privileges.admin.can('admin:privileges', req.uid)) { + throw new Error('[[error:no-privileges]]'); + } + + const privilegeSet = await api.categories.getPrivileges(req, req.params.cid); + helpers.formatApiResponse(200, res, privilegeSet); +}; + +Categories.setPrivilege = async (req, res) => { + if (!await privileges.admin.can('admin:privileges', req.uid)) { + throw new Error('[[error:no-privileges]]'); + } + + await api.categories.setPrivilege(req, { + ...req.params, + member: req.body.member, + set: req.method === 'PUT', + }); + + const privilegeSet = await api.categories.getPrivileges(req, req.params.cid); + helpers.formatApiResponse(200, res, privilegeSet); +}; + +Categories.setModerator = async (req, res) => { + if (!await privileges.admin.can('admin:admins-mods', req.uid)) { + throw new Error('[[error:no-privileges]]'); + } + const privilegeList = await privileges.categories.getUserPrivilegeList(); + await api.categories.setPrivilege(req, { + cid: req.params.cid, + privilege: privilegeList, + member: req.params.uid, + set: req.method === 'PUT', + }); + helpers.formatApiResponse(200, res); +}; diff --git a/src/controllers/write/chats.js b/src/controllers/write/chats.js new file mode 100644 index 0000000000..38bec34fc8 --- /dev/null +++ b/src/controllers/write/chats.js @@ -0,0 +1,129 @@ +'use strict'; + +const api = require('../../api'); +const messaging = require('../../messaging'); + +const helpers = require('../helpers'); + +const Chats = module.exports; + +Chats.list = async (req, res) => { + const page = (isFinite(req.query.page) && parseInt(req.query.page, 10)) || 1; + const perPage = (isFinite(req.query.perPage) && parseInt(req.query.perPage, 10)) || 20; + const start = Math.max(0, page - 1) * perPage; + const stop = start + perPage; + const { rooms } = await messaging.getRecentChats(req.uid, req.uid, start, stop); + + helpers.formatApiResponse(200, res, { rooms }); +}; + +Chats.create = async (req, res) => { + const roomObj = await api.chats.create(req, req.body); + helpers.formatApiResponse(200, res, roomObj); +}; + +Chats.exists = async (req, res) => { + helpers.formatApiResponse(200, res); +}; + +Chats.get = async (req, res) => { + const roomObj = await messaging.loadRoom(req.uid, { + uid: req.query.uid || req.uid, + roomId: req.params.roomId, + }); + + helpers.formatApiResponse(200, res, roomObj); +}; + +Chats.post = async (req, res) => { + const messageObj = await api.chats.post(req, { + ...req.body, + roomId: req.params.roomId, + }); + + helpers.formatApiResponse(200, res, messageObj); +}; + +Chats.rename = async (req, res) => { + const roomObj = await api.chats.rename(req, { + ...req.body, + roomId: req.params.roomId, + }); + + helpers.formatApiResponse(200, res, roomObj); +}; + +Chats.users = async (req, res) => { + const users = await api.chats.users(req, { + ...req.params, + }); + helpers.formatApiResponse(200, res, users); +}; + +Chats.invite = async (req, res) => { + const users = await api.chats.invite(req, { + ...req.body, + roomId: req.params.roomId, + }); + + helpers.formatApiResponse(200, res, users); +}; + +Chats.kick = async (req, res) => { + const users = await api.chats.kick(req, { + ...req.body, + roomId: req.params.roomId, + }); + + helpers.formatApiResponse(200, res, users); +}; + +Chats.kickUser = async (req, res) => { + req.body.uids = [req.params.uid]; + const users = await api.chats.kick(req, { + ...req.body, + roomId: req.params.roomId, + }); + + helpers.formatApiResponse(200, res, users); +}; + +Chats.messages = {}; +Chats.messages.list = async (req, res) => { + const messages = await messaging.getMessages({ + callerUid: req.uid, + uid: req.query.uid || req.uid, + roomId: req.params.roomId, + start: parseInt(req.query.start, 10) || 0, + count: 50, + }); + + helpers.formatApiResponse(200, res, { messages }); +}; + +Chats.messages.get = async (req, res) => { + const messages = await messaging.getMessagesData([req.params.mid], req.uid, req.params.roomId, false); + helpers.formatApiResponse(200, res, messages.pop()); +}; + +Chats.messages.edit = async (req, res) => { + await messaging.canEdit(req.params.mid, req.uid); + await messaging.editMessage(req.uid, req.params.mid, req.params.roomId, req.body.message); + + const messages = await messaging.getMessagesData([req.params.mid], req.uid, req.params.roomId, false); + helpers.formatApiResponse(200, res, messages.pop()); +}; + +Chats.messages.delete = async (req, res) => { + await messaging.canDelete(req.params.mid, req.uid); + await messaging.deleteMessage(req.params.mid, req.uid); + + helpers.formatApiResponse(200, res); +}; + +Chats.messages.restore = async (req, res) => { + await messaging.canDelete(req.params.mid, req.uid); + await messaging.restoreMessage(req.params.mid, req.uid); + + helpers.formatApiResponse(200, res); +}; diff --git a/src/controllers/write/files.js b/src/controllers/write/files.js new file mode 100644 index 0000000000..f3f33db533 --- /dev/null +++ b/src/controllers/write/files.js @@ -0,0 +1,16 @@ +'use strict'; + +const fs = require('fs').promises; +const helpers = require('../helpers'); + +const Files = module.exports; + +Files.delete = async (req, res) => { + await fs.unlink(res.locals.cleanedPath); + helpers.formatApiResponse(200, res); +}; + +Files.createFolder = async (req, res) => { + await fs.mkdir(res.locals.folderPath); + helpers.formatApiResponse(200, res); +}; diff --git a/src/controllers/write/flags.js b/src/controllers/write/flags.js new file mode 100644 index 0000000000..e199019308 --- /dev/null +++ b/src/controllers/write/flags.js @@ -0,0 +1,53 @@ +'use strict'; + +const user = require('../../user'); +const flags = require('../../flags'); +const api = require('../../api'); +const helpers = require('../helpers'); + +const Flags = module.exports; + +Flags.create = async (req, res) => { + const flagObj = await api.flags.create(req, { ...req.body }); + helpers.formatApiResponse(200, res, await user.isPrivileged(req.uid) ? flagObj : undefined); +}; + +Flags.get = async (req, res) => { + const isPrivileged = await user.isPrivileged(req.uid); + if (!isPrivileged) { + return helpers.formatApiResponse(403, res); + } + + helpers.formatApiResponse(200, res, await flags.get(req.params.flagId)); +}; + +Flags.update = async (req, res) => { + const history = await api.flags.update(req, { + flagId: req.params.flagId, + ...req.body, + }); + + helpers.formatApiResponse(200, res, { history }); +}; + +Flags.delete = async (req, res) => { + await flags.purge([req.params.flagId]); + helpers.formatApiResponse(200, res); +}; + +Flags.appendNote = async (req, res) => { + const payload = await api.flags.appendNote(req, { + flagId: req.params.flagId, + ...req.body, + }); + + helpers.formatApiResponse(200, res, payload); +}; + +Flags.deleteNote = async (req, res) => { + const payload = await api.flags.deleteNote(req, { + ...req.params, + }); + + helpers.formatApiResponse(200, res, payload); +}; diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js new file mode 100644 index 0000000000..26b5e6ff6f --- /dev/null +++ b/src/controllers/write/groups.js @@ -0,0 +1,49 @@ +'use strict'; + +const api = require('../../api'); + +const helpers = require('../helpers'); + +const Groups = module.exports; + +Groups.exists = async (req, res) => { + helpers.formatApiResponse(200, res); +}; + +Groups.create = async (req, res) => { + const groupObj = await api.groups.create(req, req.body); + helpers.formatApiResponse(200, res, groupObj); +}; + +Groups.update = async (req, res) => { + const groupObj = await api.groups.update(req, { + ...req.body, + slug: req.params.slug, + }); + helpers.formatApiResponse(200, res, groupObj); +}; + +Groups.delete = async (req, res) => { + await api.groups.delete(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Groups.join = async (req, res) => { + await api.groups.join(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Groups.leave = async (req, res) => { + await api.groups.leave(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Groups.grant = async (req, res) => { + await api.groups.grant(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Groups.rescind = async (req, res) => { + await api.groups.rescind(req, req.params); + helpers.formatApiResponse(200, res); +}; diff --git a/src/controllers/write/index.js b/src/controllers/write/index.js new file mode 100644 index 0000000000..ad797c212c --- /dev/null +++ b/src/controllers/write/index.js @@ -0,0 +1,14 @@ +'use strict'; + +const Write = module.exports; + +Write.users = require('./users'); +Write.groups = require('./groups'); +Write.categories = require('./categories'); +Write.topics = require('./topics'); +Write.posts = require('./posts'); +Write.chats = require('./chats'); +Write.flags = require('./flags'); +Write.admin = require('./admin'); +Write.files = require('./files'); +Write.utilities = require('./utilities'); diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js new file mode 100644 index 0000000000..64fd93bd2f --- /dev/null +++ b/src/controllers/write/posts.js @@ -0,0 +1,116 @@ +'use strict'; + +const posts = require('../../posts'); +const privileges = require('../../privileges'); + +const api = require('../../api'); +const helpers = require('../helpers'); +const apiHelpers = require('../../api/helpers'); + +const Posts = module.exports; + +Posts.get = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.get(req, { pid: req.params.pid })); +}; + +Posts.edit = async (req, res) => { + const editResult = await api.posts.edit(req, { + ...req.body, + pid: req.params.pid, + uid: req.uid, + req: apiHelpers.buildReqObject(req), + }); + + helpers.formatApiResponse(200, res, editResult); +}; + +Posts.purge = async (req, res) => { + await api.posts.purge(req, { pid: req.params.pid }); + helpers.formatApiResponse(200, res); +}; + +Posts.restore = async (req, res) => { + await api.posts.restore(req, { pid: req.params.pid }); + helpers.formatApiResponse(200, res); +}; + +Posts.delete = async (req, res) => { + await api.posts.delete(req, { pid: req.params.pid }); + helpers.formatApiResponse(200, res); +}; + +Posts.move = async (req, res) => { + await api.posts.move(req, { + pid: req.params.pid, + tid: req.body.tid, + }); + helpers.formatApiResponse(200, res); +}; + +async function mock(req) { + const tid = await posts.getPostField(req.params.pid, 'tid'); + return { pid: req.params.pid, room_id: `topic_${tid}` }; +} + +Posts.vote = async (req, res) => { + const data = await mock(req); + if (req.body.delta > 0) { + await api.posts.upvote(req, data); + } else if (req.body.delta < 0) { + await api.posts.downvote(req, data); + } else { + await api.posts.unvote(req, data); + } + + helpers.formatApiResponse(200, res); +}; + +Posts.unvote = async (req, res) => { + const data = await mock(req); + await api.posts.unvote(req, data); + helpers.formatApiResponse(200, res); +}; + +Posts.bookmark = async (req, res) => { + const data = await mock(req); + await api.posts.bookmark(req, data); + helpers.formatApiResponse(200, res); +}; + +Posts.unbookmark = async (req, res) => { + const data = await mock(req); + await api.posts.unbookmark(req, data); + helpers.formatApiResponse(200, res); +}; + +Posts.getDiffs = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.getDiffs(req, { ...req.params })); +}; + +Posts.loadDiff = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.loadDiff(req, { ...req.params })); +}; + +Posts.restoreDiff = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.restoreDiff(req, { ...req.params })); +}; + +Posts.deleteDiff = async (req, res) => { + if (!parseInt(req.params.pid, 10)) { + throw new Error('[[error:invalid-data]]'); + } + + const cid = await posts.getCidByPid(req.params.pid); + const [isAdmin, isModerator] = await Promise.all([ + privileges.users.isAdministrator(req.uid), + privileges.users.isModerator(req.uid, cid), + ]); + + if (!(isAdmin || isModerator)) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + await posts.diffs.delete(req.params.pid, req.params.timestamp, req.uid); + + helpers.formatApiResponse(200, res, await api.posts.getDiffs(req, { ...req.params })); +}; diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js new file mode 100644 index 0000000000..6fcb475fb1 --- /dev/null +++ b/src/controllers/write/topics.js @@ -0,0 +1,242 @@ +'use strict'; + +const validator = require('validator'); + +const db = require('../../database'); +const api = require('../../api'); +const topics = require('../../topics'); +const privileges = require('../../privileges'); + +const helpers = require('../helpers'); +const middleware = require('../../middleware'); +const uploadsController = require('../uploads'); + +const Topics = module.exports; + +Topics.get = async (req, res) => { + helpers.formatApiResponse(200, res, await api.topics.get(req, req.params)); +}; + +Topics.create = async (req, res) => { + const id = await lockPosting(req, '[[error:already-posting]]'); + try { + const payload = await api.topics.create(req, req.body); + if (payload.queued) { + helpers.formatApiResponse(202, res, payload); + } else { + helpers.formatApiResponse(200, res, payload); + } + } finally { + await db.deleteObjectField('locks', id); + } +}; + +Topics.reply = async (req, res) => { + const id = await lockPosting(req, '[[error:already-posting]]'); + try { + const payload = await api.topics.reply(req, { ...req.body, tid: req.params.tid }); + helpers.formatApiResponse(200, res, payload); + } finally { + await db.deleteObjectField('locks', id); + } +}; + +async function lockPosting(req, error) { + const id = req.uid > 0 ? req.uid : req.sessionID; + const value = `posting${id}`; + const count = await db.incrObjectField('locks', value); + if (count > 1) { + throw new Error(error); + } + return value; +} + +Topics.delete = async (req, res) => { + await api.topics.delete(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + +Topics.restore = async (req, res) => { + await api.topics.restore(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + +Topics.purge = async (req, res) => { + await api.topics.purge(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + +Topics.pin = async (req, res) => { + // Pin expiry was not available w/ sockets hence not included in api lib method + if (req.body.expiry) { + await topics.tools.setPinExpiry(req.params.tid, req.body.expiry, req.uid); + } + await api.topics.pin(req, { tids: [req.params.tid] }); + + helpers.formatApiResponse(200, res); +}; + +Topics.unpin = async (req, res) => { + await api.topics.unpin(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + +Topics.lock = async (req, res) => { + await api.topics.lock(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + +Topics.unlock = async (req, res) => { + await api.topics.unlock(req, { tids: [req.params.tid] }); + helpers.formatApiResponse(200, res); +}; + +Topics.follow = async (req, res) => { + await api.topics.follow(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Topics.ignore = async (req, res) => { + await api.topics.ignore(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Topics.unfollow = async (req, res) => { + await api.topics.unfollow(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Topics.addTags = async (req, res) => { + if (!await privileges.topics.canEdit(req.params.tid, req.user.uid)) { + return helpers.formatApiResponse(403, res); + } + const cid = await topics.getTopicField(req.params.tid, 'cid'); + await topics.validateTags(req.body.tags, cid, req.user.uid, req.params.tid); + const tags = await topics.filterTags(req.body.tags); + + await topics.addTags(tags, [req.params.tid]); + helpers.formatApiResponse(200, res); +}; + +Topics.deleteTags = async (req, res) => { + if (!await privileges.topics.canEdit(req.params.tid, req.user.uid)) { + return helpers.formatApiResponse(403, res); + } + + await topics.deleteTopicTags(req.params.tid); + helpers.formatApiResponse(200, res); +}; + +Topics.getThumbs = async (req, res) => { + if (isFinite(req.params.tid)) { // post_uuids can be passed in occasionally, in that case no checks are necessary + const [exists, canRead] = await Promise.all([ + topics.exists(req.params.tid), + privileges.topics.can('topics:read', req.params.tid, req.uid), + ]); + if (!exists || !canRead) { + return helpers.formatApiResponse(403, res); + } + } + + helpers.formatApiResponse(200, res, await topics.thumbs.get(req.params.tid)); +}; + +Topics.addThumb = async (req, res) => { + await checkThumbPrivileges({ tid: req.params.tid, uid: req.user.uid, res }); + if (res.headersSent) { + return; + } + + const files = await uploadsController.uploadThumb(req, res); // response is handled here + + // Add uploaded files to topic zset + if (files && files.length) { + await Promise.all(files.map(async (fileObj) => { + await topics.thumbs.associate({ + id: req.params.tid, + path: fileObj.path || fileObj.url, + }); + })); + } +}; + +Topics.migrateThumbs = async (req, res) => { + await Promise.all([ + checkThumbPrivileges({ tid: req.params.tid, uid: req.user.uid, res }), + checkThumbPrivileges({ tid: req.body.tid, uid: req.user.uid, res }), + ]); + if (res.headersSent) { + return; + } + + await topics.thumbs.migrate(req.params.tid, req.body.tid); + helpers.formatApiResponse(200, res); +}; + +Topics.deleteThumb = async (req, res) => { + if (!req.body.path.startsWith('http')) { + await middleware.assert.path(req, res, () => {}); + if (res.headersSent) { + return; + } + } + + await checkThumbPrivileges({ tid: req.params.tid, uid: req.user.uid, res }); + if (res.headersSent) { + return; + } + + await topics.thumbs.delete(req.params.tid, req.body.path); + helpers.formatApiResponse(200, res, await topics.thumbs.get(req.params.tid)); +}; + +Topics.reorderThumbs = async (req, res) => { + await checkThumbPrivileges({ tid: req.params.tid, uid: req.user.uid, res }); + if (res.headersSent) { + return; + } + + const exists = await topics.thumbs.exists(req.params.tid, req.body.path); + if (!exists) { + return helpers.formatApiResponse(404, res); + } + + await topics.thumbs.associate({ + id: req.params.tid, + path: req.body.path, + score: req.body.order, + }); + helpers.formatApiResponse(200, res); +}; + +async function checkThumbPrivileges({ tid, uid, res }) { + // req.params.tid could be either a tid (pushing a new thumb to an existing topic) + // or a post UUID (a new topic being composed) + const isUUID = validator.isUUID(tid); + + // Sanity-check the tid if it's strictly not a uuid + if (!isUUID && (isNaN(parseInt(tid, 10)) || !await topics.exists(tid))) { + return helpers.formatApiResponse(404, res, new Error('[[error:no-topic]]')); + } + + // While drafts are not protected, tids are + if (!isUUID && !await privileges.topics.canEdit(tid, uid)) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } +} + +Topics.getEvents = async (req, res) => { + if (!await privileges.topics.can('topics:read', req.params.tid, req.uid)) { + return helpers.formatApiResponse(403, res); + } + + helpers.formatApiResponse(200, res, await topics.events.get(req.params.tid, req.uid)); +}; + +Topics.deleteEvent = async (req, res) => { + if (!await privileges.topics.isAdminOrMod(req.params.tid, req.uid)) { + return helpers.formatApiResponse(403, res); + } + await topics.events.purge(req.params.tid, [req.params.eventId]); + helpers.formatApiResponse(200, res); +}; diff --git a/src/controllers/write/users.js b/src/controllers/write/users.js new file mode 100644 index 0000000000..84dfc1591c --- /dev/null +++ b/src/controllers/write/users.js @@ -0,0 +1,356 @@ +'use strict'; + +const util = require('util'); +const nconf = require('nconf'); +const path = require('path'); +const crypto = require('crypto'); +const fs = require('fs').promises; + +const db = require('../../database'); +const api = require('../../api'); +const groups = require('../../groups'); +const meta = require('../../meta'); +const privileges = require('../../privileges'); +const user = require('../../user'); +const utils = require('../../utils'); + +const helpers = require('../helpers'); + +const Users = module.exports; + +const exportMetadata = new Map([ + ['posts', ['csv', 'text/csv']], + ['uploads', ['zip', 'application/zip']], + ['profile', ['json', 'application/json']], +]); + +const hasAdminPrivilege = async (uid, privilege) => { + const ok = await privileges.admin.can(`admin:${privilege}`, uid); + if (!ok) { + throw new Error('[[error:no-privileges]]'); + } +}; + +Users.redirectBySlug = async (req, res) => { + const uid = await user.getUidByUserslug(req.params.userslug); + + if (uid) { + const path = req.path.split('/').slice(3).join('/'); + const urlObj = new URL(nconf.get('url') + req.url); + res.redirect(308, nconf.get('relative_path') + encodeURI(`/api/v3/users/${uid}/${path}${urlObj.search}`)); + } else { + helpers.formatApiResponse(404, res); + } +}; + +Users.create = async (req, res) => { + await hasAdminPrivilege(req.uid, 'users'); + const userObj = await api.users.create(req, req.body); + helpers.formatApiResponse(200, res, userObj); +}; + +Users.exists = async (req, res) => { + helpers.formatApiResponse(200, res); +}; + +Users.get = async (req, res) => { + const userData = await user.getUserData(req.params.uid); + const publicUserData = await user.hidePrivateData(userData, req.uid); + helpers.formatApiResponse(200, res, publicUserData); +}; + +Users.update = async (req, res) => { + const userObj = await api.users.update(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res, userObj); +}; + +Users.delete = async (req, res) => { + await api.users.delete(req, { ...req.params, password: req.body.password }); + helpers.formatApiResponse(200, res); +}; + +Users.deleteContent = async (req, res) => { + await api.users.deleteContent(req, { ...req.params, password: req.body.password }); + helpers.formatApiResponse(200, res); +}; + +Users.deleteAccount = async (req, res) => { + await api.users.deleteAccount(req, { ...req.params, password: req.body.password }); + helpers.formatApiResponse(200, res); +}; + +Users.deleteMany = async (req, res) => { + await hasAdminPrivilege(req.uid, 'users'); + await api.users.deleteMany(req, req.body); + helpers.formatApiResponse(200, res); +}; + +Users.changePicture = async (req, res) => { + await api.users.changePicture(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res); +}; + +Users.updateSettings = async (req, res) => { + const settings = await api.users.updateSettings(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res, settings); +}; + +Users.changePassword = async (req, res) => { + await api.users.changePassword(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res); +}; + +Users.follow = async (req, res) => { + await api.users.follow(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Users.unfollow = async (req, res) => { + await api.users.unfollow(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Users.ban = async (req, res) => { + await api.users.ban(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res); +}; + +Users.unban = async (req, res) => { + await api.users.unban(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res); +}; + +Users.mute = async (req, res) => { + await api.users.mute(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res); +}; + +Users.unmute = async (req, res) => { + await api.users.unmute(req, { ...req.body, uid: req.params.uid }); + helpers.formatApiResponse(200, res); +}; + +Users.generateToken = async (req, res) => { + await hasAdminPrivilege(req.uid, 'settings'); + if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10)) { + return helpers.formatApiResponse(401, res); + } + + const settings = await meta.settings.get('core.api'); + settings.tokens = settings.tokens || []; + + const newToken = { + token: utils.generateUUID(), + uid: req.user.uid, + description: req.body.description || '', + timestamp: Date.now(), + }; + settings.tokens.push(newToken); + await meta.settings.set('core.api', settings); + helpers.formatApiResponse(200, res, newToken); +}; + +Users.deleteToken = async (req, res) => { + await hasAdminPrivilege(req.uid, 'settings'); + if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10)) { + return helpers.formatApiResponse(401, res); + } + + const settings = await meta.settings.get('core.api'); + const beforeLen = settings.tokens.length; + settings.tokens = settings.tokens.filter(tokenObj => tokenObj.token !== req.params.token); + if (beforeLen !== settings.tokens.length) { + await meta.settings.set('core.api', settings); + helpers.formatApiResponse(200, res); + } else { + helpers.formatApiResponse(404, res); + } +}; + +const getSessionAsync = util.promisify((sid, callback) => { + db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)); +}); + +Users.revokeSession = async (req, res) => { + // Only admins or global mods (besides the user themselves) can revoke sessions + if (parseInt(req.params.uid, 10) !== req.uid && !await user.isAdminOrGlobalMod(req.uid)) { + return helpers.formatApiResponse(404, res); + } + + const sids = await db.getSortedSetRange(`uid:${req.params.uid}:sessions`, 0, -1); + let _id; + for (const sid of sids) { + /* eslint-disable no-await-in-loop */ + const sessionObj = await getSessionAsync(sid); + if (sessionObj && sessionObj.meta && sessionObj.meta.uuid === req.params.uuid) { + _id = sid; + break; + } + } + + if (!_id) { + throw new Error('[[error:no-session-found]]'); + } + + await user.auth.revokeSession(_id, req.params.uid); + helpers.formatApiResponse(200, res); +}; + +Users.invite = async (req, res) => { + const { emails, groupsToJoin = [] } = req.body; + + if (!emails || !Array.isArray(groupsToJoin)) { + return helpers.formatApiResponse(400, res, new Error('[[error:invalid-data]]')); + } + + // For simplicity, this API route is restricted to self-use only. This can change if needed. + if (parseInt(req.user.uid, 10) !== parseInt(req.params.uid, 10)) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + const canInvite = await privileges.users.hasInvitePrivilege(req.uid); + if (!canInvite) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + const { registrationType } = meta.config; + const isAdmin = await user.isAdministrator(req.uid); + if (registrationType === 'admin-invite-only' && !isAdmin) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + const inviteGroups = (await groups.getUserInviteGroups(req.uid)).map(group => group.name); + const cannotInvite = groupsToJoin.some(group => !inviteGroups.includes(group)); + if (groupsToJoin.length > 0 && cannotInvite) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + const max = meta.config.maximumInvites; + const emailsArr = emails.split(',').map(email => email.trim()).filter(Boolean); + + for (const email of emailsArr) { + /* eslint-disable no-await-in-loop */ + let invites = 0; + if (max) { + invites = await user.getInvitesNumber(req.uid); + } + if (!isAdmin && max && invites >= max) { + return helpers.formatApiResponse(403, res, new Error(`[[error:invite-maximum-met, ${invites}, ${max}]]`)); + } + + await user.sendInvitationEmail(req.uid, email, groupsToJoin); + } + + return helpers.formatApiResponse(200, res); +}; + +Users.getInviteGroups = async function (req, res) { + if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10)) { + return helpers.formatApiResponse(401, res); + } + + const userInviteGroups = await groups.getUserInviteGroups(req.params.uid); + return helpers.formatApiResponse(200, res, userInviteGroups.map(group => group.displayName)); +}; + +Users.listEmails = async (req, res) => { + const [isPrivileged, { showemail }] = await Promise.all([ + user.isPrivileged(req.uid), + user.getSettings(req.params.uid), + ]); + const isSelf = req.uid === parseInt(req.params.uid, 10); + + if (isSelf || isPrivileged || showemail) { + const emails = await db.getSortedSetRangeByScore('email:uid', 0, 500, req.params.uid, req.params.uid); + helpers.formatApiResponse(200, res, { emails }); + } else { + helpers.formatApiResponse(204, res); + } +}; + +Users.getEmail = async (req, res) => { + const [isPrivileged, { showemail }, exists] = await Promise.all([ + user.isPrivileged(req.uid), + user.getSettings(req.params.uid), + db.isSortedSetMember('email:uid', req.params.email.toLowerCase()), + ]); + const isSelf = req.uid === parseInt(req.params.uid, 10); + + if (exists && (isSelf || isPrivileged || showemail)) { + helpers.formatApiResponse(204, res); + } else { + helpers.formatApiResponse(404, res); + } +}; + +Users.confirmEmail = async (req, res) => { + const [pending, current, canManage] = await Promise.all([ + user.email.isValidationPending(req.params.uid, req.params.email), + user.getUserField(req.params.uid, 'email'), + privileges.admin.can('admin:users', req.uid), + ]); + + if (!canManage) { + return helpers.notAllowed(req, res); + } + + if (pending) { // has active confirmation request + const code = await db.get(`confirm:byUid:${req.params.uid}`); + await user.email.confirmByCode(code, req.session.id); + helpers.formatApiResponse(200, res); + } else if (current && current === req.params.email) { // email in user hash (i.e. email passed into user.create) + await user.email.confirmByUid(req.params.uid); + helpers.formatApiResponse(200, res); + } else { + helpers.formatApiResponse(404, res); + } +}; + +const prepareExport = async (req, res) => { + const [extension] = exportMetadata.get(req.params.type); + const filename = `${req.params.uid}_${req.params.type}.${extension}`; + try { + const stat = await fs.stat(path.join(__dirname, '../../../build/export', filename)); + const modified = new Date(stat.mtimeMs); + res.set('Last-Modified', modified.toUTCString()); + res.set('ETag', `"${crypto.createHash('md5').update(String(stat.mtimeMs)).digest('hex')}"`); + res.status(204); + return true; + } catch (e) { + res.status(404); + return false; + } +}; + +Users.checkExportByType = async (req, res) => { + await prepareExport(req, res); + res.end(); +}; + +Users.getExportByType = async (req, res) => { + const [extension, mime] = exportMetadata.get(req.params.type); + const filename = `${req.params.uid}_${req.params.type}.${extension}`; + + const exists = await prepareExport(req, res); + if (!exists) { + return res.end(); + } + + res.status(200); + res.sendFile(filename, { + root: path.join(__dirname, '../../../build/export'), + headers: { + 'Content-Type': mime, + 'Content-Disposition': `attachment; filename=${filename}`, + }, + }, (err) => { + if (err) { + throw err; + } + }); +}; + +Users.generateExportsByType = async (req, res) => { + await api.users.generateExport(req, req.params); + helpers.formatApiResponse(202, res); +}; diff --git a/src/controllers/write/utilities.js b/src/controllers/write/utilities.js new file mode 100644 index 0000000000..432ca56da8 --- /dev/null +++ b/src/controllers/write/utilities.js @@ -0,0 +1,33 @@ +'use strict'; + +const user = require('../../user'); +const authenticationController = require('../authentication'); +const helpers = require('../helpers'); + +const Utilities = module.exports; + +Utilities.ping = {}; +Utilities.ping.get = (req, res) => { + helpers.formatApiResponse(200, res, { + pong: true, + }); +}; + +Utilities.ping.post = (req, res) => { + helpers.formatApiResponse(200, res, { + uid: req.user.uid, + received: req.body, + }); +}; + +Utilities.login = (req, res) => { + res.locals.redirectAfterLogin = async (req, res) => { + const userData = (await user.getUsers([req.uid], req.uid)).pop(); + helpers.formatApiResponse(200, res, userData); + }; + res.locals.noScriptErrors = (req, res, err, statusCode) => { + helpers.formatApiResponse(statusCode, res, new Error(err)); + }; + + authenticationController.login(req, res); +}; diff --git a/src/coverPhoto.js b/src/coverPhoto.js new file mode 100644 index 0000000000..a0e2e9f2c6 --- /dev/null +++ b/src/coverPhoto.js @@ -0,0 +1,40 @@ +'use strict'; + + +const nconf = require('nconf'); +const meta = require('./meta'); + +const relative_path = nconf.get('relative_path'); + +const coverPhoto = module.exports; + +coverPhoto.getDefaultGroupCover = function (groupName) { + return getCover('groups', groupName); +}; + +coverPhoto.getDefaultProfileCover = function (uid) { + return getCover('profile', parseInt(uid, 10)); +}; + +function getCover(type, id) { + const defaultCover = `${relative_path}/assets/images/cover-default.png`; + if (meta.config[`${type}:defaultCovers`]) { + const covers = String(meta.config[`${type}:defaultCovers`]).trim().split(/[\s,]+/g); + let coverPhoto = defaultCover; + if (!covers.length) { + return coverPhoto; + } + + if (typeof id === 'string') { + id = (id.charCodeAt(0) + id.charCodeAt(1)) % covers.length; + } else { + id %= covers.length; + } + if (covers[id]) { + coverPhoto = covers[id].startsWith('http') ? covers[id] : (relative_path + covers[id]); + } + return coverPhoto; + } + + return defaultCover; +} diff --git a/src/database/cache.js b/src/database/cache.js new file mode 100644 index 0000000000..cdd9622d04 --- /dev/null +++ b/src/database/cache.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports.create = function (name) { + const cacheCreate = require('../cache/lru'); + return cacheCreate({ + name: `${name}-object`, + max: 40000, + ttl: 0, + }); +}; diff --git a/src/database/helpers.js b/src/database/helpers.js new file mode 100644 index 0000000000..48e7fa1d9c --- /dev/null +++ b/src/database/helpers.js @@ -0,0 +1,28 @@ +'use strict'; + +const helpers = module.exports; + +helpers.mergeBatch = function (batchData, start, stop, sort) { + function getFirst() { + let selectedArray = batchData[0]; + for (let i = 1; i < batchData.length; i++) { + if (batchData[i].length && ( + !selectedArray.length || + (sort === 1 && batchData[i][0].score < selectedArray[0].score) || + (sort === -1 && batchData[i][0].score > selectedArray[0].score) + )) { + selectedArray = batchData[i]; + } + } + return selectedArray.length ? selectedArray.shift() : null; + } + let item = null; + const result = []; + do { + item = getFirst(batchData); + if (item) { + result.push(item); + } + } while (item && (result.length < (stop - start + 1) || stop === -1)); + return result; +}; diff --git a/src/database/index.js b/src/database/index.js new file mode 100644 index 0000000000..917a15b348 --- /dev/null +++ b/src/database/index.js @@ -0,0 +1,37 @@ +'use strict'; + +const nconf = require('nconf'); + +const databaseName = nconf.get('database'); +const winston = require('winston'); + +if (!databaseName) { + winston.error(new Error('Database type not set! Run ./nodebb setup')); + process.exit(); +} + +const primaryDB = require(`./${databaseName}`); + +primaryDB.parseIntFields = function (data, intFields, requestedFields) { + intFields.forEach((field) => { + if (!requestedFields || !requestedFields.length || requestedFields.includes(field)) { + data[field] = parseInt(data[field], 10) || 0; + } + }); +}; + +primaryDB.initSessionStore = async function () { + const sessionStoreConfig = nconf.get('session_store') || nconf.get('redis') || nconf.get(databaseName); + let sessionStoreDB = primaryDB; + + if (nconf.get('session_store')) { + sessionStoreDB = require(`./${sessionStoreConfig.name}`); + } else if (nconf.get('redis')) { + // if redis is specified, use it as session store over others + sessionStoreDB = require('./redis'); + } + + primaryDB.sessionStore = await sessionStoreDB.createSessionStore(sessionStoreConfig); +}; + +module.exports = primaryDB; diff --git a/src/database/mongo.js b/src/database/mongo.js new file mode 100644 index 0000000000..8cf47148e0 --- /dev/null +++ b/src/database/mongo.js @@ -0,0 +1,188 @@ + +'use strict'; + + +const winston = require('winston'); +const nconf = require('nconf'); +const semver = require('semver'); +const prompt = require('prompt'); +const utils = require('../utils'); + +let client; + +const connection = require('./mongo/connection'); + +const mongoModule = module.exports; + +function isUriNotSpecified() { + return !prompt.history('mongo:uri').value; +} + +mongoModule.questions = [ + { + name: 'mongo:uri', + description: 'MongoDB connection URI: (leave blank if you wish to specify host, port, username/password and database individually)\nFormat: mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]', + default: nconf.get('mongo:uri') || '', + hideOnWebInstall: true, + }, + { + name: 'mongo:host', + description: 'Host IP or address of your MongoDB instance', + default: nconf.get('mongo:host') || '127.0.0.1', + ask: isUriNotSpecified, + }, + { + name: 'mongo:port', + description: 'Host port of your MongoDB instance', + default: nconf.get('mongo:port') || 27017, + ask: isUriNotSpecified, + }, + { + name: 'mongo:username', + description: 'MongoDB username', + default: nconf.get('mongo:username') || '', + ask: isUriNotSpecified, + }, + { + name: 'mongo:password', + description: 'Password of your MongoDB database', + default: nconf.get('mongo:password') || '', + hidden: true, + ask: isUriNotSpecified, + before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; }, + }, + { + name: 'mongo:database', + description: 'MongoDB database name', + default: nconf.get('mongo:database') || 'nodebb', + ask: isUriNotSpecified, + }, +]; + +mongoModule.init = async function () { + client = await connection.connect(nconf.get('mongo')); + mongoModule.client = client.db(); +}; + +mongoModule.createSessionStore = async function (options) { + const MongoStore = require('connect-mongo'); + const meta = require('../meta'); + + const store = MongoStore.create({ + clientPromise: connection.connect(options), + ttl: meta.getSessionTTLSeconds(), + }); + + return store; +}; + +mongoModule.createIndices = async function () { + if (!mongoModule.client) { + winston.warn('[database/createIndices] database not initialized'); + return; + } + + winston.info('[database] Checking database indices.'); + const collection = mongoModule.client.collection('objects'); + await collection.createIndex({ _key: 1, score: -1 }, { background: true }); + await collection.createIndex({ _key: 1, value: -1 }, { background: true, unique: true, sparse: true }); + await collection.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0, background: true }); + winston.info('[database] Checking database indices done!'); +}; + +mongoModule.checkCompatibility = function (callback) { + const mongoPkg = require('mongodb/package.json'); + mongoModule.checkCompatibilityVersion(mongoPkg.version, callback); +}; + +mongoModule.checkCompatibilityVersion = function (version, callback) { + if (semver.lt(version, '2.0.0')) { + return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.')); + } + + callback(); +}; + +mongoModule.info = async function (db) { + if (!db) { + const client = await connection.connect(nconf.get('mongo')); + db = client.db(); + } + mongoModule.client = mongoModule.client || db; + let serverStatusError = ''; + + async function getServerStatus() { + try { + return await db.command({ serverStatus: 1 }); + } catch (err) { + serverStatusError = err.message; + // Override mongo error with more human-readable error + if (err.name === 'MongoError' && err.codeName === 'Unauthorized') { + serverStatusError = '[[admin/advanced/database:mongo.unauthorized]]'; + } + winston.error(err.stack); + } + } + + let [serverStatus, stats, listCollections] = await Promise.all([ + getServerStatus(), + db.command({ dbStats: 1 }), + getCollectionStats(db), + ]); + stats = stats || {}; + serverStatus = serverStatus || {}; + stats.serverStatusError = serverStatusError; + const scale = 1024 * 1024 * 1024; + + listCollections = listCollections.map(collectionInfo => ({ + name: collectionInfo.ns, + count: collectionInfo.count, + size: collectionInfo.size, + avgObjSize: collectionInfo.avgObjSize, + storageSize: collectionInfo.storageSize, + totalIndexSize: collectionInfo.totalIndexSize, + indexSizes: collectionInfo.indexSizes, + })); + + stats.mem = serverStatus.mem || { resident: 0, virtual: 0, mapped: 0 }; + stats.mem.resident = (stats.mem.resident / 1024).toFixed(3); + stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(3); + stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(3); + stats.collectionData = listCollections; + stats.network = serverStatus.network || { bytesIn: 0, bytesOut: 0, numRequests: 0 }; + stats.network.bytesIn = (stats.network.bytesIn / scale).toFixed(3); + stats.network.bytesOut = (stats.network.bytesOut / scale).toFixed(3); + stats.network.numRequests = utils.addCommas(stats.network.numRequests); + stats.raw = JSON.stringify(stats, null, 4); + + stats.avgObjSize = stats.avgObjSize.toFixed(2); + stats.dataSize = (stats.dataSize / scale).toFixed(3); + stats.storageSize = (stats.storageSize / scale).toFixed(3); + stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(3) : 0; + stats.indexSize = (stats.indexSize / scale).toFixed(3); + stats.storageEngine = serverStatus.storageEngine ? serverStatus.storageEngine.name : 'mmapv1'; + stats.host = serverStatus.host; + stats.version = serverStatus.version; + stats.uptime = serverStatus.uptime; + stats.mongo = true; + return stats; +}; + +async function getCollectionStats(db) { + const items = await db.listCollections().toArray(); + return await Promise.all(items.map(collection => db.collection(collection.name).stats())); +} + +mongoModule.close = function (callback) { + callback = callback || function () {}; + client.close(err => callback(err)); +}; + +require('./mongo/main')(mongoModule); +require('./mongo/hash')(mongoModule); +require('./mongo/sets')(mongoModule); +require('./mongo/sorted')(mongoModule); +require('./mongo/list')(mongoModule); +require('./mongo/transaction')(mongoModule); + +require('../promisify')(mongoModule, ['client', 'sessionStore']); diff --git a/src/database/mongo/connection.js b/src/database/mongo/connection.js new file mode 100644 index 0000000000..314677e58b --- /dev/null +++ b/src/database/mongo/connection.js @@ -0,0 +1,62 @@ +'use strict'; + +const nconf = require('nconf'); + +const winston = require('winston'); +const _ = require('lodash'); + +const connection = module.exports; + +connection.getConnectionString = function (mongo) { + mongo = mongo || nconf.get('mongo'); + let usernamePassword = ''; + const uri = mongo.uri || ''; + if (mongo.username && mongo.password) { + usernamePassword = `${mongo.username}:${encodeURIComponent(mongo.password)}@`; + } else if (!uri.includes('@') || !uri.slice(uri.indexOf('://') + 3, uri.indexOf('@'))) { + winston.warn('You have no mongo username/password setup!'); + } + + // Sensible defaults for Mongo, if not set + if (!mongo.host) { + mongo.host = '127.0.0.1'; + } + if (!mongo.port) { + mongo.port = 27017; + } + const dbName = mongo.database; + if (dbName === undefined || dbName === '') { + winston.warn('You have no database name, using "nodebb"'); + mongo.database = 'nodebb'; + } + + const hosts = mongo.host.split(','); + const ports = mongo.port.toString().split(','); + const servers = []; + + for (let i = 0; i < hosts.length; i += 1) { + servers.push(`${hosts[i]}:${ports[i]}`); + } + + return uri || `mongodb://${usernamePassword}${servers.join()}/${mongo.database}`; +}; + +connection.getConnectionOptions = function (mongo) { + mongo = mongo || nconf.get('mongo'); + const connOptions = { + maxPoolSize: 10, + minPoolSize: 3, + connectTimeoutMS: 90000, + }; + + return _.merge(connOptions, mongo.options || {}); +}; + +connection.connect = async function (options) { + const mongoClient = require('mongodb').MongoClient; + + const connString = connection.getConnectionString(options); + const connOptions = connection.getConnectionOptions(options); + + return await mongoClient.connect(connString, connOptions); +}; diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js new file mode 100644 index 0000000000..50bee32c38 --- /dev/null +++ b/src/database/mongo/hash.js @@ -0,0 +1,282 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + const cache = require('../cache').create('mongo'); + + module.objectCache = cache; + + module.setObject = async function (key, data) { + const isArray = Array.isArray(key); + if (!key || !data || (isArray && !key.length)) { + return; + } + + const writeData = helpers.serializeData(data); + if (!Object.keys(writeData).length) { + return; + } + try { + if (isArray) { + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + key.forEach(key => bulk.find({ _key: key }).upsert().updateOne({ $set: writeData })); + await bulk.execute(); + } else { + await module.client.collection('objects').updateOne({ _key: key }, { $set: writeData }, { upsert: true }); + } + } catch (err) { + if (err && err.message.startsWith('E11000 duplicate key error')) { + return await module.setObject(key, data); + } + throw err; + } + + cache.del(key); + }; + + module.setObjectBulk = async function (...args) { + let data = args[0]; + if (!Array.isArray(data) || !data.length) { + return; + } + if (Array.isArray(args[1])) { + console.warn('[deprecated] db.setObjectBulk(keys, data) usage is deprecated, please use db.setObjectBulk(data)'); + // conver old format to new format for backwards compatibility + data = args[0].map((key, i) => [key, args[1][i]]); + } + + try { + let bulk; + data.forEach((item) => { + const writeData = helpers.serializeData(item[1]); + if (Object.keys(writeData).length) { + if (!bulk) { + bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + } + bulk.find({ _key: item[0] }).upsert().updateOne({ $set: writeData }); + } + }); + if (bulk) { + await bulk.execute(); + } + } catch (err) { + if (err && err.message.startsWith('E11000 duplicate key error')) { + return await module.setObjectBulk(data); + } + throw err; + } + + cache.del(data.map(item => item[0])); + }; + + module.setObjectField = async function (key, field, value) { + if (!field) { + return; + } + const data = {}; + data[field] = value; + await module.setObject(key, data); + }; + + module.getObject = async function (key, fields = []) { + if (!key) { + return null; + } + + const data = await module.getObjects([key], fields); + return data && data.length ? data[0] : null; + }; + + module.getObjects = async function (keys, fields = []) { + return await module.getObjectsFields(keys, fields); + }; + + module.getObjectField = async function (key, field) { + if (!key) { + return null; + } + const cachedData = {}; + cache.getUnCachedKeys([key], cachedData); + if (cachedData[key]) { + return cachedData[key].hasOwnProperty(field) ? cachedData[key][field] : null; + } + field = helpers.fieldToString(field); + const item = await module.client.collection('objects').findOne({ _key: key }, { projection: { _id: 0, [field]: 1 } }); + if (!item) { + return null; + } + return item.hasOwnProperty(field) ? item[field] : null; + }; + + module.getObjectFields = async function (key, fields) { + if (!key) { + return null; + } + const data = await module.getObjectsFields([key], fields); + return data ? data[0] : null; + }; + + module.getObjectsFields = async function (keys, fields) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const cachedData = {}; + const unCachedKeys = cache.getUnCachedKeys(keys, cachedData); + let data = []; + if (unCachedKeys.length >= 1) { + data = await module.client.collection('objects').find( + { _key: unCachedKeys.length === 1 ? unCachedKeys[0] : { $in: unCachedKeys } }, + { projection: { _id: 0 } } + ).toArray(); + data = data.map(helpers.deserializeData); + } + + const map = helpers.toMap(data); + unCachedKeys.forEach((key) => { + cachedData[key] = map[key] || null; + cache.set(key, cachedData[key]); + }); + + if (!Array.isArray(fields) || !fields.length) { + return keys.map(key => (cachedData[key] ? { ...cachedData[key] } : null)); + } + return keys.map((key) => { + const item = cachedData[key] || {}; + const result = {}; + fields.forEach((field) => { + result[field] = item[field] !== undefined ? item[field] : null; + }); + return result; + }); + }; + + module.getObjectKeys = async function (key) { + const data = await module.getObject(key); + return data ? Object.keys(data) : []; + }; + + module.getObjectValues = async function (key) { + const data = await module.getObject(key); + return data ? Object.values(data) : []; + }; + + module.isObjectField = async function (key, field) { + const data = await module.isObjectFields(key, [field]); + return Array.isArray(data) && data.length ? data[0] : false; + }; + + module.isObjectFields = async function (key, fields) { + if (!key) { + return; + } + + const data = {}; + fields.forEach((field) => { + field = helpers.fieldToString(field); + if (field) { + data[field] = 1; + } + }); + + const item = await module.client.collection('objects').findOne({ _key: key }, { projection: data }); + const results = fields.map(f => !!item && item[f] !== undefined && item[f] !== null); + return results; + }; + + module.deleteObjectField = async function (key, field) { + await module.deleteObjectFields(key, [field]); + }; + + module.deleteObjectFields = async function (key, fields) { + if (!key || (Array.isArray(key) && !key.length) || !Array.isArray(fields) || !fields.length) { + return; + } + fields = fields.filter(Boolean); + if (!fields.length) { + return; + } + + const data = {}; + fields.forEach((field) => { + field = helpers.fieldToString(field); + data[field] = ''; + }); + if (Array.isArray(key)) { + await module.client.collection('objects').updateMany({ _key: { $in: key } }, { $unset: data }); + } else { + await module.client.collection('objects').updateOne({ _key: key }, { $unset: data }); + } + + cache.del(key); + }; + + module.incrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, 1); + }; + + module.decrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, -1); + }; + + module.incrObjectFieldBy = async function (key, field, value) { + value = parseInt(value, 10); + if (!key || isNaN(value)) { + return null; + } + + const increment = {}; + field = helpers.fieldToString(field); + increment[field] = value; + + if (Array.isArray(key)) { + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + key.forEach((key) => { + bulk.find({ _key: key }).upsert().update({ $inc: increment }); + }); + await bulk.execute(); + cache.del(key); + const result = await module.getObjectsFields(key, [field]); + return result.map(data => data && data[field]); + } + try { + const result = await module.client.collection('objects').findOneAndUpdate({ + _key: key, + }, { + $inc: increment, + }, { + returnDocument: 'after', + upsert: true, + }); + cache.del(key); + return result && result.value ? result.value[field] : null; + } catch (err) { + // if there is duplicate key error retry the upsert + // https://github.com/NodeBB/NodeBB/issues/4467 + // https://jira.mongodb.org/browse/SERVER-14322 + // https://docs.mongodb.org/manual/reference/command/findAndModify/#upsert-and-unique-index + if (err && err.message.startsWith('E11000 duplicate key error')) { + return await module.incrObjectFieldBy(key, field, value); + } + throw err; + } + }; + + module.incrObjectFieldByBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + + data.forEach((item) => { + const increment = {}; + for (const [field, value] of Object.entries(item[1])) { + increment[helpers.fieldToString(field)] = value; + } + bulk.find({ _key: item[0] }).upsert().update({ $inc: increment }); + }); + await bulk.execute(); + cache.del(data.map(item => item[0])); + }; +}; diff --git a/src/database/mongo/helpers.js b/src/database/mongo/helpers.js new file mode 100644 index 0000000000..9edf83a031 --- /dev/null +++ b/src/database/mongo/helpers.js @@ -0,0 +1,67 @@ +'use strict'; + +const helpers = module.exports; +const utils = require('../../utils'); + +helpers.noop = function () {}; + +helpers.toMap = function (data) { + const map = {}; + for (let i = 0; i < data.length; i += 1) { + map[data[i]._key] = data[i]; + delete data[i]._key; + } + return map; +}; + +helpers.fieldToString = function (field) { + if (field === null || field === undefined) { + return field; + } + + if (typeof field !== 'string') { + field = field.toString(); + } + // if there is a '.' in the field name it inserts subdocument in mongo, replace '.'s with \uff0E + return field.replace(/\./g, '\uff0E'); +}; + +helpers.serializeData = function (data) { + const serialized = {}; + for (const [field, value] of Object.entries(data)) { + if (field !== '') { + serialized[helpers.fieldToString(field)] = value; + } + } + return serialized; +}; + +helpers.deserializeData = function (data) { + const deserialized = {}; + for (const [field, value] of Object.entries(data)) { + deserialized[field.replace(/\uff0E/g, '.')] = value; + } + return deserialized; +}; + +helpers.valueToString = function (value) { + return String(value); +}; + +helpers.buildMatchQuery = function (match) { + let _match = match; + if (match.startsWith('*')) { + _match = _match.substring(1); + } + if (match.endsWith('*')) { + _match = _match.substring(0, _match.length - 1); + } + _match = utils.escapeRegexChars(_match); + if (!match.startsWith('*')) { + _match = `^${_match}`; + } + if (!match.endsWith('*')) { + _match += '$'; + } + return _match; +}; diff --git a/src/database/mongo/list.js b/src/database/mongo/list.js new file mode 100644 index 0000000000..59b1678c58 --- /dev/null +++ b/src/database/mongo/list.js @@ -0,0 +1,99 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.listPrepend = async function (key, value) { + if (!key) { + return; + } + value = Array.isArray(value) ? value : [value]; + value.reverse(); + const exists = await module.isObjectField(key, 'array'); + if (exists) { + await listPush(key, value, { $position: 0 }); + } else { + await module.listAppend(key, value); + } + }; + + module.listAppend = async function (key, value) { + if (!key) { + return; + } + value = Array.isArray(value) ? value : [value]; + await listPush(key, value); + }; + + async function listPush(key, values, position) { + values = values.map(helpers.valueToString); + await module.client.collection('objects').updateOne({ + _key: key, + }, { + $push: { + array: { + $each: values, + ...(position || {}), + }, + }, + }, { + upsert: true, + }); + } + + module.listRemoveLast = async function (key) { + if (!key) { + return; + } + const value = await module.getListRange(key, -1, -1); + module.client.collection('objects').updateOne({ _key: key }, { $pop: { array: 1 } }); + return (value && value.length) ? value[0] : null; + }; + + module.listRemoveAll = async function (key, value) { + if (!key) { + return; + } + const isArray = Array.isArray(value); + if (isArray) { + value = value.map(helpers.valueToString); + } else { + value = helpers.valueToString(value); + } + + await module.client.collection('objects').updateOne({ + _key: key, + }, { + $pull: { array: isArray ? { $in: value } : value }, + }); + }; + + module.listTrim = async function (key, start, stop) { + if (!key) { + return; + } + const value = await module.getListRange(key, start, stop); + await module.client.collection('objects').updateOne({ _key: key }, { $set: { array: value } }); + }; + + module.getListRange = async function (key, start, stop) { + if (!key) { + return; + } + + const data = await module.client.collection('objects').findOne({ _key: key }, { array: 1 }); + if (!(data && data.array)) { + return []; + } + + return data.array.slice(start, stop !== -1 ? stop + 1 : undefined); + }; + + module.listLength = async function (key) { + const result = await module.client.collection('objects').aggregate([ + { $match: { _key: key } }, + { $project: { count: { $size: '$array' } } }, + ]).toArray(); + return Array.isArray(result) && result.length && result[0].count; + }; +}; diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js new file mode 100644 index 0000000000..b81fa60d66 --- /dev/null +++ b/src/database/mongo/main.js @@ -0,0 +1,150 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + module.flushdb = async function () { + await module.client.dropDatabase(); + }; + + module.emptydb = async function () { + await module.client.collection('objects').deleteMany({}); + module.objectCache.reset(); + }; + + module.exists = async function (key) { + if (!key) { + return; + } + + if (Array.isArray(key)) { + const data = await module.client.collection('objects').find({ + _key: { $in: key }, + }, { _id: 0, _key: 1 }).toArray(); + + const map = {}; + data.forEach((item) => { + map[item._key] = true; + }); + + return key.map(key => !!map[key]); + } + + const item = await module.client.collection('objects').findOne({ + _key: key, + }, { _id: 0, _key: 1 }); + return item !== undefined && item !== null; + }; + + module.scan = async function (params) { + const match = helpers.buildMatchQuery(params.match); + return await module.client.collection('objects').distinct( + '_key', { _key: { $regex: new RegExp(match) } } + ); + }; + + module.delete = async function (key) { + if (!key) { + return; + } + await module.client.collection('objects').deleteMany({ _key: key }); + module.objectCache.del(key); + }; + + module.deleteAll = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + await module.client.collection('objects').deleteMany({ _key: { $in: keys } }); + module.objectCache.del(keys); + }; + + module.get = async function (key) { + if (!key) { + return; + } + + const objectData = await module.client.collection('objects').findOne({ _key: key }, { projection: { _id: 0 } }); + + // fallback to old field name 'value' for backwards compatibility #6340 + let value = null; + if (objectData) { + if (objectData.hasOwnProperty('data')) { + value = objectData.data; + } else if (objectData.hasOwnProperty('value')) { + value = objectData.value; + } + } + return value; + }; + + module.set = async function (key, value) { + if (!key) { + return; + } + await module.setObject(key, { data: value }); + }; + + module.increment = async function (key) { + if (!key) { + return; + } + const result = await module.client.collection('objects').findOneAndUpdate({ + _key: key, + }, { + $inc: { data: 1 }, + }, { + returnDocument: 'after', + upsert: true, + }); + return result && result.value ? result.value.data : null; + }; + + module.rename = async function (oldKey, newKey) { + await module.client.collection('objects').updateMany({ _key: oldKey }, { $set: { _key: newKey } }); + module.objectCache.del([oldKey, newKey]); + }; + + module.type = async function (key) { + const data = await module.client.collection('objects').findOne({ _key: key }); + if (!data) { + return null; + } + delete data.expireAt; + const keys = Object.keys(data); + if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) { + return 'zset'; + } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('members')) { + return 'set'; + } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('array')) { + return 'list'; + } else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('data')) { + return 'string'; + } + return 'hash'; + }; + + module.expire = async function (key, seconds) { + await module.expireAt(key, Math.round(Date.now() / 1000) + seconds); + }; + + module.expireAt = async function (key, timestamp) { + await module.setObjectField(key, 'expireAt', new Date(timestamp * 1000)); + }; + + module.pexpire = async function (key, ms) { + await module.pexpireAt(key, Date.now() + parseInt(ms, 10)); + }; + + module.pexpireAt = async function (key, timestamp) { + timestamp = Math.min(timestamp, 8640000000000000); + await module.setObjectField(key, 'expireAt', new Date(timestamp)); + }; + + module.ttl = async function (key) { + return Math.round((await module.getObjectField(key, 'expireAt') - Date.now()) / 1000); + }; + + module.pttl = async function (key) { + return await module.getObjectField(key, 'expireAt') - Date.now(); + }; +}; diff --git a/src/database/mongo/sets.js b/src/database/mongo/sets.js new file mode 100644 index 0000000000..8cfdacb61b --- /dev/null +++ b/src/database/mongo/sets.js @@ -0,0 +1,199 @@ +'use strict'; + +module.exports = function (module) { + const _ = require('lodash'); + const helpers = require('./helpers'); + + module.setAdd = async function (key, value) { + if (!Array.isArray(value)) { + value = [value]; + } + if (!value.length) { + return; + } + value = value.map(v => helpers.valueToString(v)); + + await module.client.collection('objects').updateOne({ + _key: key, + }, { + $addToSet: { + members: { + $each: value, + }, + }, + }, { + upsert: true, + }); + }; + + module.setsAdd = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + + if (!Array.isArray(value)) { + value = [value]; + } + + value = value.map(v => helpers.valueToString(v)); + + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + + for (let i = 0; i < keys.length; i += 1) { + bulk.find({ _key: keys[i] }).upsert().updateOne({ + $addToSet: { + members: { + $each: value, + }, + }, + }); + } + try { + await bulk.execute(); + } catch (err) { + if (err && err.message.startsWith('E11000 duplicate key error')) { + return await module.setsAdd(keys, value); + } + throw err; + } + }; + + module.setRemove = async function (key, value) { + if (!Array.isArray(value)) { + value = [value]; + } + + value = value.map(v => helpers.valueToString(v)); + + await module.client.collection('objects').updateMany({ + _key: Array.isArray(key) ? { $in: key } : key, + }, { + $pullAll: { members: value }, + }); + }; + + module.setsRemove = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + value = helpers.valueToString(value); + + await module.client.collection('objects').updateMany({ + _key: { $in: keys }, + }, { + $pull: { members: value }, + }); + }; + + module.isSetMember = async function (key, value) { + if (!key) { + return false; + } + value = helpers.valueToString(value); + + const item = await module.client.collection('objects').findOne({ + _key: key, members: value, + }, { + projection: { _id: 0, members: 0 }, + }); + return item !== null && item !== undefined; + }; + + module.isSetMembers = async function (key, values) { + if (!key || !Array.isArray(values) || !values.length) { + return []; + } + values = values.map(v => helpers.valueToString(v)); + + const result = await module.client.collection('objects').findOne({ + _key: key, + }, { + projection: { _id: 0, _key: 0 }, + }); + const membersSet = new Set(result && Array.isArray(result.members) ? result.members : []); + return values.map(v => membersSet.has(v)); + }; + + module.isMemberOfSets = async function (sets, value) { + if (!Array.isArray(sets) || !sets.length) { + return []; + } + value = helpers.valueToString(value); + + const result = await module.client.collection('objects').find({ + _key: { $in: sets }, members: value, + }, { + projection: { _id: 0, members: 0 }, + }).toArray(); + + const map = {}; + result.forEach((item) => { + map[item._key] = true; + }); + + return sets.map(set => !!map[set]); + }; + + module.getSetMembers = async function (key) { + if (!key) { + return []; + } + + const data = await module.client.collection('objects').findOne({ + _key: key, + }, { + projection: { _id: 0, _key: 0 }, + }); + return data ? data.members : []; + }; + + module.getSetsMembers = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const data = await module.client.collection('objects').find({ + _key: { $in: keys }, + }, { + projection: { _id: 0 }, + }).toArray(); + + const sets = {}; + data.forEach((set) => { + sets[set._key] = set.members || []; + }); + + return keys.map(k => sets[k] || []); + }; + + module.setCount = async function (key) { + if (!key) { + return 0; + } + const data = await module.client.collection('objects').aggregate([ + { $match: { _key: key } }, + { $project: { _id: 0, count: { $size: '$members' } } }, + ]).toArray(); + return Array.isArray(data) && data.length ? data[0].count : 0; + }; + + module.setsCount = async function (keys) { + const data = await module.client.collection('objects').aggregate([ + { $match: { _key: { $in: keys } } }, + { $project: { _id: 0, _key: 1, count: { $size: '$members' } } }, + ]).toArray(); + const map = _.keyBy(data, '_key'); + return keys.map(key => (map.hasOwnProperty(key) ? map[key].count : 0)); + }; + + module.setRemoveRandom = async function (key) { + const data = await module.client.collection('objects').findOne({ _key: key }); + if (!data) { + return; + } + + const randomIndex = Math.floor(Math.random() * data.members.length); + const value = data.members[randomIndex]; + await module.setRemove(data._key, value); + return value; + }; +}; diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js new file mode 100644 index 0000000000..b1a5979c9d --- /dev/null +++ b/src/database/mongo/sorted.js @@ -0,0 +1,569 @@ +'use strict'; + +const _ = require('lodash'); +const utils = require('../../utils'); + +module.exports = function (module) { + const helpers = require('./helpers'); + const dbHelpers = require('../helpers'); + + const util = require('util'); + const sleep = util.promisify(setTimeout); + + require('./sorted/add')(module); + require('./sorted/remove')(module); + require('./sorted/union')(module); + require('./sorted/intersect')(module); + + module.getSortedSetRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', 1, false); + }; + + module.getSortedSetRevRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', -1, false); + }; + + module.getSortedSetRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', 1, true); + }; + + module.getSortedSetRevRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, '-inf', '+inf', -1, true); + }; + + async function getSortedSetRange(key, start, stop, min, max, sort, withScores) { + if (!key) { + return; + } + const isArray = Array.isArray(key); + if ((start < 0 && start > stop) || (isArray && !key.length)) { + return []; + } + const query = { _key: key }; + if (isArray) { + if (key.length > 1) { + query._key = { $in: key }; + } else { + query._key = key[0]; + } + } + + if (min !== '-inf') { + query.score = { $gte: min }; + } + if (max !== '+inf') { + query.score = query.score || {}; + query.score.$lte = max; + } + + if (max === min) { + query.score = max; + } + + const fields = { _id: 0, _key: 0 }; + if (!withScores) { + fields.score = 0; + } + + let reverse = false; + if (start === 0 && stop < -1) { + reverse = true; + sort *= -1; + start = Math.abs(stop + 1); + stop = -1; + } else if (start < 0 && stop > start) { + const tmp1 = Math.abs(stop + 1); + stop = Math.abs(start + 1); + start = tmp1; + } + + let limit = stop - start + 1; + if (limit <= 0) { + limit = 0; + } + + let result = []; + async function doQuery(_key, fields, skip, limit) { + return await module.client.collection('objects').find({ ...query, ...{ _key: _key } }, { projection: fields }) + .sort({ score: sort }) + .skip(skip) + .limit(limit) + .toArray(); + } + + if (isArray && key.length > 100) { + const batches = []; + const batch = require('../../batch'); + const batchSize = Math.ceil(key.length / Math.ceil(key.length / 100)); + await batch.processArray(key, async currentBatch => batches.push(currentBatch), { batch: batchSize }); + const batchData = await Promise.all(batches.map( + batch => doQuery({ $in: batch }, { _id: 0, _key: 0 }, 0, stop + 1) + )); + result = dbHelpers.mergeBatch(batchData, 0, stop, sort); + if (start > 0) { + result = result.slice(start, stop !== -1 ? stop + 1 : undefined); + } + } else { + result = await doQuery(query._key, fields, start, limit); + } + + if (reverse) { + result.reverse(); + } + if (!withScores) { + result = result.map(item => item.value); + } + + return result; + } + + module.getSortedSetRangeByScore = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, false); + }; + + module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, false); + }; + + module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, true); + }; + + module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, true); + }; + + async function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores) { + if (parseInt(count, 10) === 0) { + return []; + } + const stop = (parseInt(count, 10) === -1) ? -1 : (start + count - 1); + return await getSortedSetRange(key, start, stop, min, max, sort, withScores); + } + + module.sortedSetCount = async function (key, min, max) { + if (!key) { + return; + } + + const query = { _key: key }; + if (min !== '-inf') { + query.score = { $gte: min }; + } + if (max !== '+inf') { + query.score = query.score || {}; + query.score.$lte = max; + } + + const count = await module.client.collection('objects').countDocuments(query); + return count || 0; + }; + + module.sortedSetCard = async function (key) { + if (!key) { + return 0; + } + const count = await module.client.collection('objects').countDocuments({ _key: key }); + return parseInt(count, 10) || 0; + }; + + module.sortedSetsCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const promises = keys.map(k => module.sortedSetCard(k)); + return await Promise.all(promises); + }; + + module.sortedSetsCardSum = async function (keys) { + if (!keys || (Array.isArray(keys) && !keys.length)) { + return 0; + } + + const count = await module.client.collection('objects').countDocuments({ _key: Array.isArray(keys) ? { $in: keys } : keys }); + return parseInt(count, 10) || 0; + }; + + module.sortedSetRank = async function (key, value) { + return await getSortedSetRank(false, key, value); + }; + + module.sortedSetRevRank = async function (key, value) { + return await getSortedSetRank(true, key, value); + }; + + async function getSortedSetRank(reverse, key, value) { + if (!key) { + return; + } + value = helpers.valueToString(value); + const score = await module.sortedSetScore(key, value); + if (score === null) { + return null; + } + + return await module.client.collection('objects').countDocuments({ + $or: [ + { + _key: key, + score: reverse ? { $gt: score } : { $lt: score }, + }, + { + _key: key, + score: score, + value: reverse ? { $gt: value } : { $lt: value }, + }, + ], + }); + } + + module.sortedSetsRanks = async function (keys, values) { + return await sortedSetsRanks(module.sortedSetRank, keys, values); + }; + + module.sortedSetsRevRanks = async function (keys, values) { + return await sortedSetsRanks(module.sortedSetRevRank, keys, values); + }; + + async function sortedSetsRanks(method, keys, values) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const data = new Array(values.length); + for (let i = 0; i < values.length; i += 1) { + data[i] = { key: keys[i], value: values[i] }; + } + const promises = data.map(item => method(item.key, item.value)); + return await Promise.all(promises); + } + + module.sortedSetRanks = async function (key, values) { + return await sortedSetRanks(false, key, values); + }; + + module.sortedSetRevRanks = async function (key, values) { + return await sortedSetRanks(true, key, values); + }; + + async function sortedSetRanks(reverse, key, values) { + if (values.length === 1) { + return [await getSortedSetRank(reverse, key, values[0])]; + } + const sortedSet = await module[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](key, 0, -1); + return values.map((value) => { + if (!value) { + return null; + } + const index = sortedSet.indexOf(value.toString()); + return index !== -1 ? index : null; + }); + } + + module.sortedSetScore = async function (key, value) { + if (!key) { + return null; + } + value = helpers.valueToString(value); + const result = await module.client.collection('objects').findOne({ _key: key, value: value }, { projection: { _id: 0, _key: 0, value: 0 } }); + return result ? result.score : null; + }; + + module.sortedSetsScore = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + value = helpers.valueToString(value); + const result = await module.client.collection('objects').find({ _key: { $in: keys }, value: value }, { projection: { _id: 0, value: 0 } }).toArray(); + const map = {}; + result.forEach((item) => { + if (item) { + map[item._key] = item; + } + }); + + return keys.map(key => (map[key] ? map[key].score : null)); + }; + + module.sortedSetScores = async function (key, values) { + if (!key) { + return null; + } + if (!values.length) { + return []; + } + values = values.map(helpers.valueToString); + const result = await module.client.collection('objects').find({ _key: key, value: { $in: values } }, { projection: { _id: 0, _key: 0 } }).toArray(); + + const valueToScore = {}; + result.forEach((item) => { + if (item) { + valueToScore[item.value] = item.score; + } + }); + + return values.map(v => (utils.isNumber(valueToScore[v]) ? valueToScore[v] : null)); + }; + + module.isSortedSetMember = async function (key, value) { + if (!key) { + return; + } + value = helpers.valueToString(value); + const result = await module.client.collection('objects').findOne({ + _key: key, value: value, + }, { + projection: { _id: 0, value: 1 }, + }); + return !!result; + }; + + module.isSortedSetMembers = async function (key, values) { + if (!key) { + return; + } + if (!values.length) { + return []; + } + values = values.map(helpers.valueToString); + const results = await module.client.collection('objects').find({ + _key: key, value: { $in: values }, + }, { + projection: { _id: 0, value: 1 }, + }).toArray(); + + const isMember = {}; + results.forEach((item) => { + if (item) { + isMember[item.value] = true; + } + }); + + return values.map(value => !!isMember[value]); + }; + + module.isMemberOfSortedSets = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + value = helpers.valueToString(value); + const results = await module.client.collection('objects').find({ + _key: { $in: keys }, value: value, + }, { + projection: { _id: 0, _key: 1, value: 1 }, + }).toArray(); + + const isMember = {}; + results.forEach((item) => { + if (item) { + isMember[item._key] = true; + } + }); + + return keys.map(key => !!isMember[key]); + }; + + module.getSortedSetMembers = async function (key) { + const data = await module.getSortedSetsMembers([key]); + return data && data[0]; + }; + + module.getSortedSetsMembers = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const arrayOfKeys = keys.length > 1; + const projection = { _id: 0, value: 1 }; + if (arrayOfKeys) { + projection._key = 1; + } + const data = await module.client.collection('objects').find({ + _key: arrayOfKeys ? { $in: keys } : keys[0], + }, { projection: projection }).toArray(); + + if (!arrayOfKeys) { + return [data.map(item => item.value)]; + } + const sets = {}; + data.forEach((item) => { + sets[item._key] = sets[item._key] || []; + sets[item._key].push(item.value); + }); + + return keys.map(k => sets[k] || []); + }; + + module.sortedSetIncrBy = async function (key, increment, value) { + if (!key) { + return; + } + const data = {}; + value = helpers.valueToString(value); + data.score = parseFloat(increment); + + try { + const result = await module.client.collection('objects').findOneAndUpdate({ + _key: key, + value: value, + }, { + $inc: data, + }, { + returnDocument: 'after', + upsert: true, + }); + return result && result.value ? result.value.score : null; + } catch (err) { + // if there is duplicate key error retry the upsert + // https://github.com/NodeBB/NodeBB/issues/4467 + // https://jira.mongodb.org/browse/SERVER-14322 + // https://docs.mongodb.org/manual/reference/command/findAndModify/#upsert-and-unique-index + if (err && err.message.startsWith('E11000 duplicate key error')) { + return await module.sortedSetIncrBy(key, increment, value); + } + throw err; + } + }; + + module.sortedSetIncrByBulk = async function (data) { + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + data.forEach((item) => { + bulk.find({ _key: item[0], value: helpers.valueToString(item[2]) }) + .upsert() + .update({ $inc: { score: parseFloat(item[1]) } }); + }); + await bulk.execute(); + const result = await module.client.collection('objects').find({ + _key: { $in: _.uniq(data.map(i => i[0])) }, + value: { $in: _.uniq(data.map(i => i[2])) }, + }, { + projection: { _id: 0, _key: 1, value: 1, score: 1 }, + }).toArray(); + + const map = {}; + result.forEach((item) => { + map[`${item._key}:${item.value}`] = item.score; + }); + return data.map(item => map[`${item[0]}:${item[2]}`]); + }; + + module.getSortedSetRangeByLex = async function (key, min, max, start, count) { + return await sortedSetLex(key, min, max, 1, start, count); + }; + + module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) { + return await sortedSetLex(key, min, max, -1, start, count); + }; + + module.sortedSetLexCount = async function (key, min, max) { + const data = await sortedSetLex(key, min, max, 1, 0, 0); + return data ? data.length : null; + }; + + async function sortedSetLex(key, min, max, sort, start, count) { + const query = { _key: key }; + start = start !== undefined ? start : 0; + count = count !== undefined ? count : 0; + buildLexQuery(query, min, max); + + const data = await module.client.collection('objects').find(query, { projection: { _id: 0, value: 1 } }) + .sort({ value: sort }) + .skip(start) + .limit(count === -1 ? 0 : count) + .toArray(); + + return data.map(item => item && item.value); + } + + module.sortedSetRemoveRangeByLex = async function (key, min, max) { + const query = { _key: key }; + buildLexQuery(query, min, max); + + await module.client.collection('objects').deleteMany(query); + }; + + function buildLexQuery(query, min, max) { + if (min !== '-') { + if (min.match(/^\(/)) { + query.value = { $gt: min.slice(1) }; + } else if (min.match(/^\[/)) { + query.value = { $gte: min.slice(1) }; + } else { + query.value = { $gte: min }; + } + } + if (max !== '+') { + query.value = query.value || {}; + if (max.match(/^\(/)) { + query.value.$lt = max.slice(1); + } else if (max.match(/^\[/)) { + query.value.$lte = max.slice(1); + } else { + query.value.$lte = max; + } + } + } + + module.getSortedSetScan = async function (params) { + const project = { _id: 0, value: 1 }; + if (params.withScores) { + project.score = 1; + } + + const match = helpers.buildMatchQuery(params.match); + let regex; + try { + regex = new RegExp(match); + } catch (err) { + return []; + } + + const cursor = module.client.collection('objects').find({ + _key: params.key, value: { $regex: regex }, + }, { projection: project }); + + if (params.limit) { + cursor.limit(params.limit); + } + + const data = await cursor.toArray(); + if (!params.withScores) { + return data.map(d => d.value); + } + return data; + }; + + module.processSortedSet = async function (setKey, processFn, options) { + let done = false; + const ids = []; + const project = { _id: 0, _key: 0 }; + + if (!options.withScores) { + project.score = 0; + } + const cursor = await module.client.collection('objects').find({ _key: setKey }, { projection: project }) + .sort({ score: 1 }) + .batchSize(options.batch); + + if (processFn && processFn.constructor && processFn.constructor.name !== 'AsyncFunction') { + processFn = util.promisify(processFn); + } + + while (!done) { + /* eslint-disable no-await-in-loop */ + const item = await cursor.next(); + if (item === null) { + done = true; + } else { + ids.push(options.withScores ? item : item.value); + } + + if (ids.length >= options.batch || (done && ids.length !== 0)) { + await processFn(ids); + + ids.length = 0; + if (options.interval) { + await sleep(options.interval); + } + } + } + }; +}; diff --git a/src/database/mongo/sorted/add.js b/src/database/mongo/sorted/add.js new file mode 100644 index 0000000000..8e9bd367a4 --- /dev/null +++ b/src/database/mongo/sorted/add.js @@ -0,0 +1,91 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + const utils = require('../../../utils'); + + module.sortedSetAdd = async function (key, score, value) { + if (!key) { + return; + } + if (Array.isArray(score) && Array.isArray(value)) { + return await sortedSetAddBulk(key, score, value); + } + if (!utils.isNumber(score)) { + throw new Error(`[[error:invalid-score, ${score}]]`); + } + value = helpers.valueToString(value); + + try { + await module.client.collection('objects').updateOne({ _key: key, value: value }, { $set: { score: parseFloat(score) } }, { upsert: true }); + } catch (err) { + if (err && err.message.startsWith('E11000 duplicate key error')) { + return await module.sortedSetAdd(key, score, value); + } + throw err; + } + }; + + async function sortedSetAddBulk(key, scores, values) { + if (!scores.length || !values.length) { + return; + } + if (scores.length !== values.length) { + throw new Error('[[error:invalid-data]]'); + } + for (let i = 0; i < scores.length; i += 1) { + if (!utils.isNumber(scores[i])) { + throw new Error(`[[error:invalid-score, ${scores[i]}]]`); + } + } + values = values.map(helpers.valueToString); + + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + for (let i = 0; i < scores.length; i += 1) { + bulk.find({ _key: key, value: values[i] }).upsert().updateOne({ $set: { score: parseFloat(scores[i]) } }); + } + await bulk.execute(); + } + + module.sortedSetsAdd = async function (keys, scores, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + const isArrayOfScores = Array.isArray(scores); + if ((!isArrayOfScores && !utils.isNumber(scores)) || + (isArrayOfScores && scores.map(s => utils.isNumber(s)).includes(false))) { + throw new Error(`[[error:invalid-score, ${scores}]]`); + } + + if (isArrayOfScores && scores.length !== keys.length) { + throw new Error('[[error:invalid-data]]'); + } + + value = helpers.valueToString(value); + + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + for (let i = 0; i < keys.length; i += 1) { + bulk + .find({ _key: keys[i], value: value }) + .upsert() + .updateOne({ $set: { score: parseFloat(isArrayOfScores ? scores[i] : scores) } }); + } + await bulk.execute(); + }; + + module.sortedSetAddBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + data.forEach((item) => { + if (!utils.isNumber(item[1])) { + throw new Error(`[[error:invalid-score, ${item[1]}]]`); + } + bulk.find({ _key: item[0], value: String(item[2]) }) + .upsert() + .updateOne({ $set: { score: parseFloat(item[1]) } }); + }); + await bulk.execute(); + }; +}; diff --git a/src/database/mongo/sorted/intersect.js b/src/database/mongo/sorted/intersect.js new file mode 100644 index 0000000000..b4d9e1972b --- /dev/null +++ b/src/database/mongo/sorted/intersect.js @@ -0,0 +1,219 @@ +'use strict'; + +module.exports = function (module) { + module.sortedSetIntersectCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return 0; + } + const objects = module.client.collection('objects'); + const counts = await countSets(keys, 50000); + if (counts.minCount === 0) { + return 0; + } + let items = await objects.find({ _key: counts.smallestSet }, { + projection: { _id: 0, value: 1 }, + }).batchSize(counts.minCount + 1).toArray(); + + const otherSets = keys.filter(s => s !== counts.smallestSet); + for (let i = 0; i < otherSets.length; i++) { + /* eslint-disable no-await-in-loop */ + const query = { _key: otherSets[i], value: { $in: items.map(i => i.value) } }; + if (i === otherSets.length - 1) { + return await objects.countDocuments(query); + } + items = await objects.find(query, { projection: { _id: 0, value: 1 } }) + .batchSize(items.length + 1).toArray(); + } + }; + + async function countSets(sets, limit) { + const objects = module.client.collection('objects'); + const counts = await Promise.all( + sets.map(s => objects.countDocuments({ _key: s }, { + limit: limit || 25000, + })) + ); + const minCount = Math.min(...counts); + const index = counts.indexOf(minCount); + const smallestSet = sets[index]; + return { + minCount: minCount, + smallestSet: smallestSet, + }; + } + + module.getSortedSetIntersect = async function (params) { + params.sort = 1; + return await getSortedSetRevIntersect(params); + }; + + module.getSortedSetRevIntersect = async function (params) { + params.sort = -1; + return await getSortedSetRevIntersect(params); + }; + + async function getSortedSetRevIntersect(params) { + params.start = params.hasOwnProperty('start') ? params.start : 0; + params.stop = params.hasOwnProperty('stop') ? params.stop : -1; + params.weights = params.weights || []; + + params.limit = params.stop - params.start + 1; + if (params.limit <= 0) { + params.limit = 0; + } + params.counts = await countSets(params.sets); + if (params.counts.minCount === 0) { + return []; + } + + const simple = params.weights.filter(w => w === 1).length === 1 && params.limit !== 0; + if (params.counts.minCount < 25000 && simple) { + return await intersectSingle(params); + } else if (simple) { + return await intersectBatch(params); + } + return await intersectAggregate(params); + } + + async function intersectSingle(params) { + const objects = module.client.collection('objects'); + const sortSet = params.sets[params.weights.indexOf(1)]; + if (sortSet === params.counts.smallestSet) { + return await intersectBatch(params); + } + + const cursorSmall = objects.find({ _key: params.counts.smallestSet }, { + projection: { _id: 0, value: 1 }, + }); + if (params.counts.minCount > 1) { + cursorSmall.batchSize(params.counts.minCount + 1); + } + let items = await cursorSmall.toArray(); + const project = { _id: 0, value: 1 }; + if (params.withScores) { + project.score = 1; + } + const otherSets = params.sets.filter(s => s !== params.counts.smallestSet); + // move sortSet to the end of array + otherSets.push(otherSets.splice(otherSets.indexOf(sortSet), 1)[0]); + for (let i = 0; i < otherSets.length; i++) { + /* eslint-disable no-await-in-loop */ + const cursor = objects.find({ _key: otherSets[i], value: { $in: items.map(i => i.value) } }); + cursor.batchSize(items.length + 1); + // at the last step sort by sortSet + if (i === otherSets.length - 1) { + cursor.project(project).sort({ score: params.sort }).skip(params.start).limit(params.limit); + } else { + cursor.project({ _id: 0, value: 1 }); + } + items = await cursor.toArray(); + } + if (!params.withScores) { + items = items.map(i => i.value); + } + return items; + } + + async function intersectBatch(params) { + const project = { _id: 0, value: 1 }; + if (params.withScores) { + project.score = 1; + } + const sortSet = params.sets[params.weights.indexOf(1)]; + const batchSize = 10000; + const cursor = await module.client.collection('objects') + .find({ _key: sortSet }, { projection: project }) + .sort({ score: params.sort }) + .batchSize(batchSize); + + const otherSets = params.sets.filter(s => s !== sortSet); + let inters = []; + let done = false; + while (!done) { + /* eslint-disable no-await-in-loop */ + const items = []; + while (items.length < batchSize) { + const nextItem = await cursor.next(); + if (!nextItem) { + done = true; + break; + } + items.push(nextItem); + } + + const members = await Promise.all(otherSets.map(async (s) => { + const data = await module.client.collection('objects').find({ + _key: s, value: { $in: items.map(i => i.value) }, + }, { + projection: { _id: 0, value: 1 }, + }).batchSize(items.length + 1).toArray(); + return new Set(data.map(i => i.value)); + })); + inters = inters.concat(items.filter(item => members.every(arr => arr.has(item.value)))); + if (inters.length >= params.stop) { + done = true; + inters = inters.slice(params.start, params.stop + 1); + } + } + if (!params.withScores) { + inters = inters.map(item => item.value); + } + return inters; + } + + async function intersectAggregate(params) { + const aggregate = {}; + + if (params.aggregate) { + aggregate[`$${params.aggregate.toLowerCase()}`] = '$score'; + } else { + aggregate.$sum = '$score'; + } + const pipeline = [{ $match: { _key: { $in: params.sets } } }]; + + params.weights.forEach((weight, index) => { + if (weight !== 1) { + pipeline.push({ + $project: { + value: 1, + score: { + $cond: { + if: { + $eq: ['$_key', params.sets[index]], + }, + then: { + $multiply: ['$score', weight], + }, + else: '$score', + }, + }, + }, + }); + } + }); + + pipeline.push({ $group: { _id: { value: '$value' }, totalScore: aggregate, count: { $sum: 1 } } }); + pipeline.push({ $match: { count: params.sets.length } }); + pipeline.push({ $sort: { totalScore: params.sort } }); + + if (params.start) { + pipeline.push({ $skip: params.start }); + } + + if (params.limit > 0) { + pipeline.push({ $limit: params.limit }); + } + + const project = { _id: 0, value: '$_id.value' }; + if (params.withScores) { + project.score = '$totalScore'; + } + pipeline.push({ $project: project }); + + let data = await module.client.collection('objects').aggregate(pipeline).toArray(); + if (!params.withScores) { + data = data.map(item => item.value); + } + return data; + } +}; diff --git a/src/database/mongo/sorted/remove.js b/src/database/mongo/sorted/remove.js new file mode 100644 index 0000000000..891b74389c --- /dev/null +++ b/src/database/mongo/sorted/remove.js @@ -0,0 +1,63 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + + module.sortedSetRemove = async function (key, value) { + if (!key) { + return; + } + const isValueArray = Array.isArray(value); + if (!value || (isValueArray && !value.length)) { + return; + } + + if (isValueArray) { + value = value.map(helpers.valueToString); + } else { + value = helpers.valueToString(value); + } + + await module.client.collection('objects').deleteMany({ + _key: Array.isArray(key) ? { $in: key } : key, + value: isValueArray ? { $in: value } : value, + }); + }; + + module.sortedSetsRemove = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + value = helpers.valueToString(value); + + await module.client.collection('objects').deleteMany({ _key: { $in: keys }, value: value }); + }; + + module.sortedSetsRemoveRangeByScore = async function (keys, min, max) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + const query = { _key: { $in: keys } }; + if (keys.length === 1) { + query._key = keys[0]; + } + if (min !== '-inf') { + query.score = { $gte: parseFloat(min) }; + } + if (max !== '+inf') { + query.score = query.score || {}; + query.score.$lte = parseFloat(max); + } + + await module.client.collection('objects').deleteMany(query); + }; + + module.sortedSetRemoveBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + const bulk = module.client.collection('objects').initializeUnorderedBulkOp(); + data.forEach(item => bulk.find({ _key: item[0], value: String(item[1]) }).delete()); + await bulk.execute(); + }; +}; diff --git a/src/database/mongo/sorted/union.js b/src/database/mongo/sorted/union.js new file mode 100644 index 0000000000..30cd38d2d9 --- /dev/null +++ b/src/database/mongo/sorted/union.js @@ -0,0 +1,69 @@ +'use strict'; + +module.exports = function (module) { + module.sortedSetUnionCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return 0; + } + + const data = await module.client.collection('objects').aggregate([ + { $match: { _key: { $in: keys } } }, + { $group: { _id: { value: '$value' } } }, + { $group: { _id: null, count: { $sum: 1 } } }, + ]).toArray(); + return Array.isArray(data) && data.length ? data[0].count : 0; + }; + + module.getSortedSetUnion = async function (params) { + params.sort = 1; + return await getSortedSetUnion(params); + }; + + module.getSortedSetRevUnion = async function (params) { + params.sort = -1; + return await getSortedSetUnion(params); + }; + + async function getSortedSetUnion(params) { + if (!Array.isArray(params.sets) || !params.sets.length) { + return; + } + let limit = params.stop - params.start + 1; + if (limit <= 0) { + limit = 0; + } + + const aggregate = {}; + if (params.aggregate) { + aggregate[`$${params.aggregate.toLowerCase()}`] = '$score'; + } else { + aggregate.$sum = '$score'; + } + + const pipeline = [ + { $match: { _key: { $in: params.sets } } }, + { $group: { _id: { value: '$value' }, totalScore: aggregate } }, + { $sort: { totalScore: params.sort } }, + ]; + + if (params.start) { + pipeline.push({ $skip: params.start }); + } + + if (limit > 0) { + pipeline.push({ $limit: limit }); + } + + const project = { _id: 0, value: '$_id.value' }; + if (params.withScores) { + project.score = '$totalScore'; + } + pipeline.push({ $project: project }); + + let data = await module.client.collection('objects').aggregate(pipeline).toArray(); + if (!params.withScores) { + data = data.map(item => item.value); + } + return data; + } +}; diff --git a/src/database/mongo/transaction.js b/src/database/mongo/transaction.js new file mode 100644 index 0000000000..1e98aac8c3 --- /dev/null +++ b/src/database/mongo/transaction.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function (module) { + // TODO + module.transaction = function (perform, callback) { + perform(module.client, callback); + }; +}; diff --git a/src/database/postgres.js b/src/database/postgres.js new file mode 100644 index 0000000000..69f5ab5cfd --- /dev/null +++ b/src/database/postgres.js @@ -0,0 +1,390 @@ +'use strict'; + +const winston = require('winston'); +const async = require('async'); +const nconf = require('nconf'); +const session = require('express-session'); +const semver = require('semver'); + +const connection = require('./postgres/connection'); + +const postgresModule = module.exports; + +postgresModule.questions = [ + { + name: 'postgres:host', + description: 'Host IP or address of your PostgreSQL instance', + default: nconf.get('postgres:host') || '127.0.0.1', + }, + { + name: 'postgres:port', + description: 'Host port of your PostgreSQL instance', + default: nconf.get('postgres:port') || 5432, + }, + { + name: 'postgres:username', + description: 'PostgreSQL username', + default: nconf.get('postgres:username') || '', + }, + { + name: 'postgres:password', + description: 'Password of your PostgreSQL database', + hidden: true, + default: nconf.get('postgres:password') || '', + before: function (value) { value = value || nconf.get('postgres:password') || ''; return value; }, + }, + { + name: 'postgres:database', + description: 'PostgreSQL database name', + default: nconf.get('postgres:database') || 'nodebb', + }, + { + name: 'postgres:ssl', + description: 'Enable SSL for PostgreSQL database access', + default: nconf.get('postgres:ssl') || false, + }, +]; + +postgresModule.init = async function () { + const { Pool } = require('pg'); + const connOptions = connection.getConnectionOptions(); + const pool = new Pool(connOptions); + postgresModule.pool = pool; + postgresModule.client = pool; + const client = await pool.connect(); + try { + await checkUpgrade(client); + } catch (err) { + winston.error(`NodeBB could not connect to your PostgreSQL database. PostgreSQL returned the following error: ${err.message}`); + throw err; + } finally { + client.release(); + } +}; + + +async function checkUpgrade(client) { + const res = await client.query(` +SELECT EXISTS(SELECT * + FROM "information_schema"."columns" + WHERE "table_schema" = 'public' + AND "table_name" = 'objects' + AND "column_name" = 'data') a, + EXISTS(SELECT * + FROM "information_schema"."columns" + WHERE "table_schema" = 'public' + AND "table_name" = 'legacy_hash' + AND "column_name" = '_key') b, + EXISTS(SELECT * + FROM "information_schema"."routines" + WHERE "routine_schema" = 'public' + AND "routine_name" = 'nodebb_get_sorted_set_members') c`); + + if (res.rows[0].a && res.rows[0].b && res.rows[0].c) { + return; + } + + await client.query(`BEGIN`); + try { + if (!res.rows[0].b) { + await client.query(` +CREATE TYPE LEGACY_OBJECT_TYPE AS ENUM ( + 'hash', 'zset', 'set', 'list', 'string' +)`); + await client.query(` +CREATE TABLE "legacy_object" ( + "_key" TEXT NOT NULL + PRIMARY KEY, + "type" LEGACY_OBJECT_TYPE NOT NULL, + "expireAt" TIMESTAMPTZ DEFAULT NULL, + UNIQUE ( "_key", "type" ) +)`); + await client.query(` +CREATE TABLE "legacy_hash" ( + "_key" TEXT NOT NULL + PRIMARY KEY, + "data" JSONB NOT NULL, + "type" LEGACY_OBJECT_TYPE NOT NULL + DEFAULT 'hash'::LEGACY_OBJECT_TYPE + CHECK ( "type" = 'hash' ), + CONSTRAINT "fk__legacy_hash__key" + FOREIGN KEY ("_key", "type") + REFERENCES "legacy_object"("_key", "type") + ON UPDATE CASCADE + ON DELETE CASCADE +)`); + await client.query(` +CREATE TABLE "legacy_zset" ( + "_key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "score" NUMERIC NOT NULL, + "type" LEGACY_OBJECT_TYPE NOT NULL + DEFAULT 'zset'::LEGACY_OBJECT_TYPE + CHECK ( "type" = 'zset' ), + PRIMARY KEY ("_key", "value"), + CONSTRAINT "fk__legacy_zset__key" + FOREIGN KEY ("_key", "type") + REFERENCES "legacy_object"("_key", "type") + ON UPDATE CASCADE + ON DELETE CASCADE +)`); + await client.query(` +CREATE TABLE "legacy_set" ( + "_key" TEXT NOT NULL, + "member" TEXT NOT NULL, + "type" LEGACY_OBJECT_TYPE NOT NULL + DEFAULT 'set'::LEGACY_OBJECT_TYPE + CHECK ( "type" = 'set' ), + PRIMARY KEY ("_key", "member"), + CONSTRAINT "fk__legacy_set__key" + FOREIGN KEY ("_key", "type") + REFERENCES "legacy_object"("_key", "type") + ON UPDATE CASCADE + ON DELETE CASCADE +)`); + await client.query(` +CREATE TABLE "legacy_list" ( + "_key" TEXT NOT NULL + PRIMARY KEY, + "array" TEXT[] NOT NULL, + "type" LEGACY_OBJECT_TYPE NOT NULL + DEFAULT 'list'::LEGACY_OBJECT_TYPE + CHECK ( "type" = 'list' ), + CONSTRAINT "fk__legacy_list__key" + FOREIGN KEY ("_key", "type") + REFERENCES "legacy_object"("_key", "type") + ON UPDATE CASCADE + ON DELETE CASCADE +)`); + await client.query(` +CREATE TABLE "legacy_string" ( + "_key" TEXT NOT NULL + PRIMARY KEY, + "data" TEXT NOT NULL, + "type" LEGACY_OBJECT_TYPE NOT NULL + DEFAULT 'string'::LEGACY_OBJECT_TYPE + CHECK ( "type" = 'string' ), + CONSTRAINT "fk__legacy_string__key" + FOREIGN KEY ("_key", "type") + REFERENCES "legacy_object"("_key", "type") + ON UPDATE CASCADE + ON DELETE CASCADE +)`); + + if (res.rows[0].a) { + await client.query(` +INSERT INTO "legacy_object" ("_key", "type", "expireAt") +SELECT DISTINCT "data"->>'_key', + CASE WHEN (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 2 + THEN CASE WHEN ("data" ? 'value') + OR ("data" ? 'data') + THEN 'string' + WHEN "data" ? 'array' + THEN 'list' + WHEN "data" ? 'members' + THEN 'set' + ELSE 'hash' + END + WHEN (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 3 + THEN CASE WHEN ("data" ? 'value') + AND ("data" ? 'score') + THEN 'zset' + ELSE 'hash' + END + ELSE 'hash' + END::LEGACY_OBJECT_TYPE, + CASE WHEN ("data" ? 'expireAt') + THEN to_timestamp(("data"->>'expireAt')::double precision / 1000) + ELSE NULL + END + FROM "objects"`); + await client.query(` +INSERT INTO "legacy_hash" ("_key", "data") +SELECT "data"->>'_key', + "data" - '_key' - 'expireAt' + FROM "objects" + WHERE CASE WHEN (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 2 + THEN NOT (("data" ? 'value') + OR ("data" ? 'data') + OR ("data" ? 'members') + OR ("data" ? 'array')) + WHEN (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 3 + THEN NOT (("data" ? 'value') + AND ("data" ? 'score')) + ELSE TRUE + END`); + await client.query(` +INSERT INTO "legacy_zset" ("_key", "value", "score") +SELECT "data"->>'_key', + "data"->>'value', + ("data"->>'score')::NUMERIC + FROM "objects" + WHERE (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 3 + AND ("data" ? 'value') + AND ("data" ? 'score')`); + await client.query(` +INSERT INTO "legacy_set" ("_key", "member") +SELECT "data"->>'_key', + jsonb_array_elements_text("data"->'members') + FROM "objects" + WHERE (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 2 + AND ("data" ? 'members')`); + await client.query(` +INSERT INTO "legacy_list" ("_key", "array") +SELECT "data"->>'_key', + ARRAY(SELECT t + FROM jsonb_array_elements_text("data"->'list') WITH ORDINALITY l(t, i) + ORDER BY i ASC) + FROM "objects" + WHERE (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 2 + AND ("data" ? 'array')`); + await client.query(` +INSERT INTO "legacy_string" ("_key", "data") +SELECT "data"->>'_key', + CASE WHEN "data" ? 'value' + THEN "data"->>'value' + ELSE "data"->>'data' + END + FROM "objects" + WHERE (SELECT COUNT(*) + FROM jsonb_object_keys("data" - 'expireAt')) = 2 + AND (("data" ? 'value') + OR ("data" ? 'data'))`); + await client.query(`DROP TABLE "objects" CASCADE`); + await client.query(`DROP FUNCTION "fun__objects__expireAt"() CASCADE`); + } + await client.query(` +CREATE VIEW "legacy_object_live" AS +SELECT "_key", "type" + FROM "legacy_object" + WHERE "expireAt" IS NULL + OR "expireAt" > CURRENT_TIMESTAMP`); + } + + if (!res.rows[0].c) { + await client.query(` +CREATE FUNCTION "nodebb_get_sorted_set_members"(TEXT) RETURNS TEXT[] AS $$ + SELECT array_agg(z."value" ORDER BY z."score" ASC) + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1 +$$ LANGUAGE sql +STABLE +STRICT +PARALLEL SAFE`); + } + } catch (ex) { + await client.query(`ROLLBACK`); + throw ex; + } + await client.query(`COMMIT`); +} + +postgresModule.createSessionStore = async function (options) { + const meta = require('../meta'); + + function done(db) { + const sessionStore = require('connect-pg-simple')(session); + return new sessionStore({ + pool: db, + ttl: meta.getSessionTTLSeconds(), + pruneSessionInterval: nconf.get('isPrimary') ? 60 : false, + }); + } + + const db = await connection.connect(options); + + if (!nconf.get('isPrimary')) { + return done(db); + } + + await db.query(` +CREATE TABLE IF NOT EXISTS "session" ( + "sid" CHAR(32) NOT NULL + COLLATE "C" + PRIMARY KEY, + "sess" JSONB NOT NULL, + "expire" TIMESTAMPTZ NOT NULL +) WITHOUT OIDS; + +CREATE INDEX IF NOT EXISTS "session_expire_idx" ON "session"("expire"); + +ALTER TABLE "session" + ALTER "sid" SET STORAGE MAIN, + CLUSTER ON "session_expire_idx";`); + + return done(db); +}; + +postgresModule.createIndices = function (callback) { + if (!postgresModule.pool) { + winston.warn('[database/createIndices] database not initialized'); + return callback(); + } + + const query = postgresModule.pool.query.bind(postgresModule.pool); + + winston.info('[database] Checking database indices.'); + async.series([ + async.apply(query, `CREATE INDEX IF NOT EXISTS "idx__legacy_zset__key__score" ON "legacy_zset"("_key" ASC, "score" DESC)`), + async.apply(query, `CREATE INDEX IF NOT EXISTS "idx__legacy_object__expireAt" ON "legacy_object"("expireAt" ASC)`), + ], (err) => { + if (err) { + winston.error(`Error creating index ${err.message}`); + return callback(err); + } + winston.info('[database] Checking database indices done!'); + callback(); + }); +}; + +postgresModule.checkCompatibility = function (callback) { + const postgresPkg = require('pg/package.json'); + postgresModule.checkCompatibilityVersion(postgresPkg.version, callback); +}; + +postgresModule.checkCompatibilityVersion = function (version, callback) { + if (semver.lt(version, '7.0.0')) { + return callback(new Error('The `pg` package is out-of-date, please run `./nodebb setup` again.')); + } + + callback(); +}; + +postgresModule.info = async function (db) { + if (!db) { + db = await connection.connect(nconf.get('postgres')); + } + postgresModule.pool = postgresModule.pool || db; + const res = await db.query(` + SELECT true "postgres", + current_setting('server_version') "version", + EXTRACT(EPOCH FROM NOW() - pg_postmaster_start_time()) * 1000 "uptime" + `); + return { + ...res.rows[0], + raw: JSON.stringify(res.rows[0], null, 4), + }; +}; + +postgresModule.close = async function () { + await postgresModule.pool.end(); +}; + +require('./postgres/main')(postgresModule); +require('./postgres/hash')(postgresModule); +require('./postgres/sets')(postgresModule); +require('./postgres/sorted')(postgresModule); +require('./postgres/list')(postgresModule); +require('./postgres/transaction')(postgresModule); + +require('../promisify')(postgresModule, ['client', 'sessionStore', 'pool', 'transaction']); diff --git a/src/database/postgres/connection.js b/src/database/postgres/connection.js new file mode 100644 index 0000000000..d81b294007 --- /dev/null +++ b/src/database/postgres/connection.js @@ -0,0 +1,44 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); +const _ = require('lodash'); + +const connection = module.exports; + +connection.getConnectionOptions = function (postgres) { + postgres = postgres || nconf.get('postgres'); + // Sensible defaults for PostgreSQL, if not set + if (!postgres.host) { + postgres.host = '127.0.0.1'; + } + if (!postgres.port) { + postgres.port = 5432; + } + const dbName = postgres.database; + if (dbName === undefined || dbName === '') { + winston.warn('You have no database name, using "nodebb"'); + postgres.database = 'nodebb'; + } + + const connOptions = { + host: postgres.host, + port: postgres.port, + user: postgres.username, + password: postgres.password, + database: postgres.database, + ssl: String(postgres.ssl) === 'true', + }; + + return _.merge(connOptions, postgres.options || {}); +}; + +connection.connect = async function (options) { + const { Pool } = require('pg'); + const connOptions = connection.getConnectionOptions(options); + const db = new Pool(connOptions); + await db.connect(); + return db; +}; + +require('../../promisify')(connection); diff --git a/src/database/postgres/hash.js b/src/database/postgres/hash.js new file mode 100644 index 0000000000..724fdcb97f --- /dev/null +++ b/src/database/postgres/hash.js @@ -0,0 +1,388 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.setObject = async function (key, data) { + if (!key || !data) { + return; + } + + if (data.hasOwnProperty('')) { + delete data['']; + } + if (!Object.keys(data).length) { + return; + } + await module.transaction(async (client) => { + const dataString = JSON.stringify(data); + + if (Array.isArray(key)) { + await helpers.ensureLegacyObjectsType(client, key, 'hash'); + await client.query({ + name: 'setObjectKeys', + text: ` + INSERT INTO "legacy_hash" ("_key", "data") + SELECT k, $2::TEXT::JSONB + FROM UNNEST($1::TEXT[]) vs(k) + ON CONFLICT ("_key") + DO UPDATE SET "data" = "legacy_hash"."data" || $2::TEXT::JSONB`, + values: [key, dataString], + }); + } else { + await helpers.ensureLegacyObjectType(client, key, 'hash'); + await client.query({ + name: 'setObject', + text: ` + INSERT INTO "legacy_hash" ("_key", "data") + VALUES ($1::TEXT, $2::TEXT::JSONB) + ON CONFLICT ("_key") + DO UPDATE SET "data" = "legacy_hash"."data" || $2::TEXT::JSONB`, + values: [key, dataString], + }); + } + }); + }; + + module.setObjectBulk = async function (...args) { + let data = args[0]; + if (!Array.isArray(data) || !data.length) { + return; + } + if (Array.isArray(args[1])) { + console.warn('[deprecated] db.setObjectBulk(keys, data) usage is deprecated, please use db.setObjectBulk(data)'); + // conver old format to new format for backwards compatibility + data = args[0].map((key, i) => [key, args[1][i]]); + } + await module.transaction(async (client) => { + data = data.filter((item) => { + if (item[1].hasOwnProperty('')) { + delete item[1]['']; + } + return !!Object.keys(item[1]).length; + }); + const keys = data.map(item => item[0]); + if (!keys.length) { + return; + } + + await helpers.ensureLegacyObjectsType(client, keys, 'hash'); + const dataStrings = data.map(item => JSON.stringify(item[1])); + await client.query({ + name: 'setObjectBulk', + text: ` + INSERT INTO "legacy_hash" ("_key", "data") + SELECT k, d + FROM UNNEST($1::TEXT[], $2::TEXT::JSONB[]) vs(k, d) + ON CONFLICT ("_key") + DO UPDATE SET "data" = "legacy_hash"."data" || EXCLUDED.data`, + values: [keys, dataStrings], + }); + }); + }; + + module.setObjectField = async function (key, field, value) { + if (!field) { + return; + } + + await module.transaction(async (client) => { + const valueString = JSON.stringify(value); + if (Array.isArray(key)) { + await module.setObject(key, { [field]: value }); + } else { + await helpers.ensureLegacyObjectType(client, key, 'hash'); + await client.query({ + name: 'setObjectField', + text: ` + INSERT INTO "legacy_hash" ("_key", "data") + VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::TEXT::JSONB)) + ON CONFLICT ("_key") + DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], $3::TEXT::JSONB)`, + values: [key, field, valueString], + }); + } + }); + }; + + module.getObject = async function (key, fields = []) { + if (!key) { + return null; + } + if (fields.length) { + return await module.getObjectFields(key, fields); + } + const res = await module.pool.query({ + name: 'getObject', + text: ` +SELECT h."data" + FROM "legacy_object_live" o + INNER JOIN "legacy_hash" h + ON o."_key" = h."_key" + AND o."type" = h."type" + WHERE o."_key" = $1::TEXT + LIMIT 1`, + values: [key], + }); + + return res.rows.length ? res.rows[0].data : null; + }; + + module.getObjects = async function (keys, fields = []) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + if (fields.length) { + return await module.getObjectsFields(keys, fields); + } + const res = await module.pool.query({ + name: 'getObjects', + text: ` +SELECT h."data" + FROM UNNEST($1::TEXT[]) WITH ORDINALITY k("_key", i) + LEFT OUTER JOIN "legacy_object_live" o + ON o."_key" = k."_key" + LEFT OUTER JOIN "legacy_hash" h + ON o."_key" = h."_key" + AND o."type" = h."type" + ORDER BY k.i ASC`, + values: [keys], + }); + + return res.rows.map(row => row.data); + }; + + module.getObjectField = async function (key, field) { + if (!key) { + return null; + } + + const res = await module.pool.query({ + name: 'getObjectField', + text: ` +SELECT h."data"->>$2::TEXT f + FROM "legacy_object_live" o + INNER JOIN "legacy_hash" h + ON o."_key" = h."_key" + AND o."type" = h."type" + WHERE o."_key" = $1::TEXT + LIMIT 1`, + values: [key, field], + }); + + return res.rows.length ? res.rows[0].f : null; + }; + + module.getObjectFields = async function (key, fields) { + if (!key) { + return null; + } + if (!Array.isArray(fields) || !fields.length) { + return await module.getObject(key); + } + const res = await module.pool.query({ + name: 'getObjectFields', + text: ` +SELECT (SELECT jsonb_object_agg(f, d."value") + FROM UNNEST($2::TEXT[]) f + LEFT OUTER JOIN jsonb_each(h."data") d + ON d."key" = f) d + FROM "legacy_object_live" o + INNER JOIN "legacy_hash" h + ON o."_key" = h."_key" + AND o."type" = h."type" + WHERE o."_key" = $1::TEXT`, + values: [key, fields], + }); + + if (res.rows.length) { + return res.rows[0].d; + } + + const obj = {}; + fields.forEach((f) => { + obj[f] = null; + }); + + return obj; + }; + + module.getObjectsFields = async function (keys, fields) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + if (!Array.isArray(fields) || !fields.length) { + return await module.getObjects(keys); + } + const res = await module.pool.query({ + name: 'getObjectsFields', + text: ` +SELECT (SELECT jsonb_object_agg(f, d."value") + FROM UNNEST($2::TEXT[]) f + LEFT OUTER JOIN jsonb_each(h."data") d + ON d."key" = f) d + FROM UNNEST($1::text[]) WITH ORDINALITY k("_key", i) + LEFT OUTER JOIN "legacy_object_live" o + ON o."_key" = k."_key" + LEFT OUTER JOIN "legacy_hash" h + ON o."_key" = h."_key" + AND o."type" = h."type" + ORDER BY k.i ASC`, + values: [keys, fields], + }); + + return res.rows.map(row => row.d); + }; + + module.getObjectKeys = async function (key) { + if (!key) { + return; + } + + const res = await module.pool.query({ + name: 'getObjectKeys', + text: ` +SELECT ARRAY(SELECT jsonb_object_keys(h."data")) k + FROM "legacy_object_live" o + INNER JOIN "legacy_hash" h + ON o."_key" = h."_key" + AND o."type" = h."type" + WHERE o."_key" = $1::TEXT + LIMIT 1`, + values: [key], + }); + + return res.rows.length ? res.rows[0].k : []; + }; + + module.getObjectValues = async function (key) { + const data = await module.getObject(key); + return data ? Object.values(data) : []; + }; + + module.isObjectField = async function (key, field) { + if (!key) { + return; + } + + const res = await module.pool.query({ + name: 'isObjectField', + text: ` +SELECT (h."data" ? $2::TEXT AND h."data"->>$2::TEXT IS NOT NULL) b + FROM "legacy_object_live" o + INNER JOIN "legacy_hash" h + ON o."_key" = h."_key" + AND o."type" = h."type" + WHERE o."_key" = $1::TEXT + LIMIT 1`, + values: [key, field], + }); + + return res.rows.length ? res.rows[0].b : false; + }; + + module.isObjectFields = async function (key, fields) { + if (!key) { + return; + } + + const data = await module.getObjectFields(key, fields); + if (!data) { + return fields.map(() => false); + } + return fields.map(field => data.hasOwnProperty(field) && data[field] !== null); + }; + + module.deleteObjectField = async function (key, field) { + await module.deleteObjectFields(key, [field]); + }; + + module.deleteObjectFields = async function (key, fields) { + if (!key || (Array.isArray(key) && !key.length) || !Array.isArray(fields) || !fields.length) { + return; + } + + if (Array.isArray(key)) { + await module.pool.query({ + name: 'deleteObjectFieldsKeys', + text: ` + UPDATE "legacy_hash" + SET "data" = COALESCE((SELECT jsonb_object_agg("key", "value") + FROM jsonb_each("data") + WHERE "key" <> ALL ($2::TEXT[])), '{}') + WHERE "_key" = ANY($1::TEXT[])`, + values: [key, fields], + }); + } else { + await module.pool.query({ + name: 'deleteObjectFields', + text: ` + UPDATE "legacy_hash" + SET "data" = COALESCE((SELECT jsonb_object_agg("key", "value") + FROM jsonb_each("data") + WHERE "key" <> ALL ($2::TEXT[])), '{}') + WHERE "_key" = $1::TEXT`, + values: [key, fields], + }); + } + }; + + module.incrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, 1); + }; + + module.decrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, -1); + }; + + module.incrObjectFieldBy = async function (key, field, value) { + value = parseInt(value, 10); + + if (!key || isNaN(value)) { + return null; + } + + return await module.transaction(async (client) => { + if (Array.isArray(key)) { + await helpers.ensureLegacyObjectsType(client, key, 'hash'); + } else { + await helpers.ensureLegacyObjectType(client, key, 'hash'); + } + + const res = await client.query(Array.isArray(key) ? { + name: 'incrObjectFieldByMulti', + text: ` +INSERT INTO "legacy_hash" ("_key", "data") +SELECT UNNEST($1::TEXT[]), jsonb_build_object($2::TEXT, $3::NUMERIC) +ON CONFLICT ("_key") +DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC)) +RETURNING ("data"->>$2::TEXT)::NUMERIC v`, + values: [key, field, value], + } : { + name: 'incrObjectFieldBy', + text: ` +INSERT INTO "legacy_hash" ("_key", "data") +VALUES ($1::TEXT, jsonb_build_object($2::TEXT, $3::NUMERIC)) +ON CONFLICT ("_key") +DO UPDATE SET "data" = jsonb_set("legacy_hash"."data", ARRAY[$2::TEXT], to_jsonb(COALESCE(("legacy_hash"."data"->>$2::TEXT)::NUMERIC, 0) + $3::NUMERIC)) +RETURNING ("data"->>$2::TEXT)::NUMERIC v`, + values: [key, field, value], + }); + return Array.isArray(key) ? res.rows.map(r => parseFloat(r.v)) : parseFloat(res.rows[0].v); + }); + }; + + module.incrObjectFieldByBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + // TODO: perf? + await Promise.all(data.map(async (item) => { + for (const [field, value] of Object.entries(item[1])) { + // eslint-disable-next-line no-await-in-loop + await module.incrObjectFieldBy(item[0], field, value); + } + })); + }; +}; diff --git a/src/database/postgres/helpers.js b/src/database/postgres/helpers.js new file mode 100644 index 0000000000..f2e9cc7162 --- /dev/null +++ b/src/database/postgres/helpers.js @@ -0,0 +1,97 @@ +'use strict'; + +const helpers = module.exports; + +helpers.valueToString = function (value) { + return String(value); +}; + +helpers.removeDuplicateValues = function (values, ...others) { + for (let i = 0; i < values.length; i++) { + if (values.lastIndexOf(values[i]) !== i) { + values.splice(i, 1); + for (let j = 0; j < others.length; j++) { + others[j].splice(i, 1); + } + i -= 1; + } + } +}; + +helpers.ensureLegacyObjectType = async function (db, key, type) { + await db.query({ + name: 'ensureLegacyObjectTypeBefore', + text: ` +DELETE FROM "legacy_object" + WHERE "expireAt" IS NOT NULL + AND "expireAt" <= CURRENT_TIMESTAMP`, + }); + + await db.query({ + name: 'ensureLegacyObjectType1', + text: ` +INSERT INTO "legacy_object" ("_key", "type") +VALUES ($1::TEXT, $2::TEXT::LEGACY_OBJECT_TYPE) + ON CONFLICT + DO NOTHING`, + values: [key, type], + }); + + const res = await db.query({ + name: 'ensureLegacyObjectType2', + text: ` +SELECT "type" + FROM "legacy_object_live" + WHERE "_key" = $1::TEXT`, + values: [key], + }); + + if (res.rows[0].type !== type) { + throw new Error(`database: cannot insert ${JSON.stringify(key)} as ${type} because it already exists as ${res.rows[0].type}`); + } +}; + +helpers.ensureLegacyObjectsType = async function (db, keys, type) { + await db.query({ + name: 'ensureLegacyObjectTypeBefore', + text: ` +DELETE FROM "legacy_object" + WHERE "expireAt" IS NOT NULL + AND "expireAt" <= CURRENT_TIMESTAMP`, + }); + + await db.query({ + name: 'ensureLegacyObjectsType1', + text: ` +INSERT INTO "legacy_object" ("_key", "type") +SELECT k, $2::TEXT::LEGACY_OBJECT_TYPE + FROM UNNEST($1::TEXT[]) k + ON CONFLICT + DO NOTHING`, + values: [keys, type], + }); + + const res = await db.query({ + name: 'ensureLegacyObjectsType2', + text: ` +SELECT "_key", "type" + FROM "legacy_object_live" + WHERE "_key" = ANY($1::TEXT[])`, + values: [keys], + }); + + const invalid = res.rows.filter(r => r.type !== type); + + if (invalid.length) { + const parts = invalid.map(r => `${JSON.stringify(r._key)} is ${r.type}`); + throw new Error(`database: cannot insert multiple objects as ${type} because they already exist: ${parts.join(', ')}`); + } + + const missing = keys.filter(k => !res.rows.some(r => r._key === k)); + + if (missing.length) { + throw new Error(`database: failed to insert keys for objects: ${JSON.stringify(missing)}`); + } +}; + +helpers.noop = function () {}; diff --git a/src/database/postgres/list.js b/src/database/postgres/list.js new file mode 100644 index 0000000000..db94f6e391 --- /dev/null +++ b/src/database/postgres/list.js @@ -0,0 +1,189 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.listPrepend = async function (key, value) { + if (!key) { + return; + } + + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectType(client, key, 'list'); + value = Array.isArray(value) ? value : [value]; + value.reverse(); + await client.query({ + name: 'listPrependValues', + text: ` +INSERT INTO "legacy_list" ("_key", "array") +VALUES ($1::TEXT, $2::TEXT[]) +ON CONFLICT ("_key") +DO UPDATE SET "array" = EXCLUDED.array || "legacy_list"."array"`, + values: [key, value], + }); + }); + }; + + module.listAppend = async function (key, value) { + if (!key) { + return; + } + await module.transaction(async (client) => { + value = Array.isArray(value) ? value : [value]; + + await helpers.ensureLegacyObjectType(client, key, 'list'); + await client.query({ + name: 'listAppend', + text: ` +INSERT INTO "legacy_list" ("_key", "array") +VALUES ($1::TEXT, $2::TEXT[]) +ON CONFLICT ("_key") +DO UPDATE SET "array" = "legacy_list"."array" || EXCLUDED.array`, + values: [key, value], + }); + }); + }; + + module.listRemoveLast = async function (key) { + if (!key) { + return; + } + + const res = await module.pool.query({ + name: 'listRemoveLast', + text: ` +WITH A AS ( + SELECT l.* + FROM "legacy_object_live" o + INNER JOIN "legacy_list" l + ON o."_key" = l."_key" + AND o."type" = l."type" + WHERE o."_key" = $1::TEXT + FOR UPDATE) +UPDATE "legacy_list" l + SET "array" = A."array"[1 : array_length(A."array", 1) - 1] + FROM A + WHERE A."_key" = l."_key" +RETURNING A."array"[array_length(A."array", 1)] v`, + values: [key], + }); + + return res.rows.length ? res.rows[0].v : null; + }; + + module.listRemoveAll = async function (key, value) { + if (!key) { + return; + } + // TODO: remove all values with one query + if (Array.isArray(value)) { + await Promise.all(value.map(v => module.listRemoveAll(key, v))); + return; + } + await module.pool.query({ + name: 'listRemoveAll', + text: ` +UPDATE "legacy_list" l + SET "array" = array_remove(l."array", $2::TEXT) + FROM "legacy_object_live" o + WHERE o."_key" = l."_key" + AND o."type" = l."type" + AND o."_key" = $1::TEXT`, + values: [key, value], + }); + }; + + module.listTrim = async function (key, start, stop) { + if (!key) { + return; + } + + stop += 1; + + await module.pool.query(stop > 0 ? { + name: 'listTrim', + text: ` +UPDATE "legacy_list" l + SET "array" = ARRAY(SELECT m.m + FROM UNNEST(l."array") WITH ORDINALITY m(m, i) + ORDER BY m.i ASC + LIMIT ($3::INTEGER - $2::INTEGER) + OFFSET $2::INTEGER) + FROM "legacy_object_live" o + WHERE o."_key" = l."_key" + AND o."type" = l."type" + AND o."_key" = $1::TEXT`, + values: [key, start, stop], + } : { + name: 'listTrimBack', + text: ` +UPDATE "legacy_list" l + SET "array" = ARRAY(SELECT m.m + FROM UNNEST(l."array") WITH ORDINALITY m(m, i) + ORDER BY m.i ASC + LIMIT ($3::INTEGER - $2::INTEGER + array_length(l."array", 1)) + OFFSET $2::INTEGER) + FROM "legacy_object_live" o + WHERE o."_key" = l."_key" + AND o."type" = l."type" + AND o."_key" = $1::TEXT`, + values: [key, start, stop], + }); + }; + + module.getListRange = async function (key, start, stop) { + if (!key) { + return; + } + + stop += 1; + + const res = await module.pool.query(stop > 0 ? { + name: 'getListRange', + text: ` +SELECT ARRAY(SELECT m.m + FROM UNNEST(l."array") WITH ORDINALITY m(m, i) + ORDER BY m.i ASC + LIMIT ($3::INTEGER - $2::INTEGER) + OFFSET $2::INTEGER) l + FROM "legacy_object_live" o + INNER JOIN "legacy_list" l + ON o."_key" = l."_key" + AND o."type" = l."type" + WHERE o."_key" = $1::TEXT`, + values: [key, start, stop], + } : { + name: 'getListRangeBack', + text: ` +SELECT ARRAY(SELECT m.m + FROM UNNEST(l."array") WITH ORDINALITY m(m, i) + ORDER BY m.i ASC + LIMIT ($3::INTEGER - $2::INTEGER + array_length(l."array", 1)) + OFFSET $2::INTEGER) l + FROM "legacy_object_live" o + INNER JOIN "legacy_list" l + ON o."_key" = l."_key" + AND o."type" = l."type" + WHERE o."_key" = $1::TEXT`, + values: [key, start, stop], + }); + + return res.rows.length ? res.rows[0].l : []; + }; + + module.listLength = async function (key) { + const res = await module.pool.query({ + name: 'listLength', + text: ` +SELECT array_length(l."array", 1) l + FROM "legacy_object_live" o + INNER JOIN "legacy_list" l + ON o."_key" = l."_key" + AND o."type" = l."type" + WHERE o."_key" = $1::TEXT`, + values: [key], + }); + + return res.rows.length ? res.rows[0].l : 0; + }; +}; diff --git a/src/database/postgres/main.js b/src/database/postgres/main.js new file mode 100644 index 0000000000..5a6957fc3e --- /dev/null +++ b/src/database/postgres/main.js @@ -0,0 +1,244 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.flushdb = async function () { + await module.pool.query(`DROP SCHEMA "public" CASCADE`); + await module.pool.query(`CREATE SCHEMA "public"`); + }; + + module.emptydb = async function () { + await module.pool.query(`DELETE FROM "legacy_object"`); + }; + + module.exists = async function (key) { + if (!key) { + return; + } + + // Redis/Mongo consider empty zsets as non-existent, match that behaviour + const type = await module.type(key); + if (type === 'zset') { + if (Array.isArray(key)) { + const members = await Promise.all(key.map(key => module.getSortedSetRange(key, 0, 0))); + return members.map(member => member.length > 0); + } + const members = await module.getSortedSetRange(key, 0, 0); + return members.length > 0; + } + + if (Array.isArray(key)) { + const res = await module.pool.query({ + name: 'existsArray', + text: ` + SELECT o."_key" k + FROM "legacy_object_live" o + WHERE o."_key" = ANY($1::TEXT[])`, + values: [key], + }); + return key.map(k => res.rows.some(r => r.k === k)); + } + const res = await module.pool.query({ + name: 'exists', + text: ` + SELECT EXISTS(SELECT * + FROM "legacy_object_live" + WHERE "_key" = $1::TEXT + LIMIT 1) e`, + values: [key], + }); + return res.rows[0].e; + }; + + module.scan = async function (params) { + let { match } = params; + if (match.startsWith('*')) { + match = `%${match.substring(1)}`; + } + if (match.endsWith('*')) { + match = `${match.substring(0, match.length - 1)}%`; + } + + const res = await module.pool.query({ + text: ` + SELECT o."_key" + FROM "legacy_object_live" o + WHERE o."_key" LIKE '${match}'`, + }); + + return res.rows.map(r => r._key); + }; + + module.delete = async function (key) { + if (!key) { + return; + } + + await module.pool.query({ + name: 'delete', + text: ` +DELETE FROM "legacy_object" + WHERE "_key" = $1::TEXT`, + values: [key], + }); + }; + + module.deleteAll = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + + await module.pool.query({ + name: 'deleteAll', + text: ` +DELETE FROM "legacy_object" + WHERE "_key" = ANY($1::TEXT[])`, + values: [keys], + }); + }; + + module.get = async function (key) { + if (!key) { + return; + } + + const res = await module.pool.query({ + name: 'get', + text: ` +SELECT s."data" t + FROM "legacy_object_live" o + INNER JOIN "legacy_string" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = $1::TEXT + LIMIT 1`, + values: [key], + }); + + return res.rows.length ? res.rows[0].t : null; + }; + + module.set = async function (key, value) { + if (!key) { + return; + } + + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectType(client, key, 'string'); + await client.query({ + name: 'set', + text: ` +INSERT INTO "legacy_string" ("_key", "data") +VALUES ($1::TEXT, $2::TEXT) +ON CONFLICT ("_key") +DO UPDATE SET "data" = $2::TEXT`, + values: [key, value], + }); + }); + }; + + module.increment = async function (key) { + if (!key) { + return; + } + + return await module.transaction(async (client) => { + await helpers.ensureLegacyObjectType(client, key, 'string'); + const res = await client.query({ + name: 'increment', + text: ` +INSERT INTO "legacy_string" ("_key", "data") +VALUES ($1::TEXT, '1') +ON CONFLICT ("_key") +DO UPDATE SET "data" = ("legacy_string"."data"::NUMERIC + 1)::TEXT +RETURNING "data" d`, + values: [key], + }); + return parseFloat(res.rows[0].d); + }); + }; + + module.rename = async function (oldKey, newKey) { + await module.transaction(async (client) => { + await client.query({ + name: 'deleteRename', + text: ` + DELETE FROM "legacy_object" + WHERE "_key" = $1::TEXT`, + values: [newKey], + }); + await client.query({ + name: 'rename', + text: ` +UPDATE "legacy_object" +SET "_key" = $2::TEXT +WHERE "_key" = $1::TEXT`, + values: [oldKey, newKey], + }); + }); + }; + + module.type = async function (key) { + const res = await module.pool.query({ + name: 'type', + text: ` +SELECT "type"::TEXT t + FROM "legacy_object_live" + WHERE "_key" = $1::TEXT + LIMIT 1`, + values: [key], + }); + + return res.rows.length ? res.rows[0].t : null; + }; + + async function doExpire(key, date) { + await module.pool.query({ + name: 'expire', + text: ` +UPDATE "legacy_object" + SET "expireAt" = $2::TIMESTAMPTZ + WHERE "_key" = $1::TEXT`, + values: [key, date], + }); + } + + module.expire = async function (key, seconds) { + await doExpire(key, new Date(((Date.now() / 1000) + seconds) * 1000)); + }; + + module.expireAt = async function (key, timestamp) { + await doExpire(key, new Date(timestamp * 1000)); + }; + + module.pexpire = async function (key, ms) { + await doExpire(key, new Date(Date.now() + parseInt(ms, 10))); + }; + + module.pexpireAt = async function (key, timestamp) { + await doExpire(key, new Date(timestamp)); + }; + + async function getExpire(key) { + const res = await module.pool.query({ + name: 'ttl', + text: ` +SELECT "expireAt"::TEXT + FROM "legacy_object" + WHERE "_key" = $1::TEXT + LIMIT 1`, + values: [key], + }); + + return res.rows.length ? new Date(res.rows[0].expireAt).getTime() : null; + } + + module.ttl = async function (key) { + return Math.round((await getExpire(key) - Date.now()) / 1000); + }; + + module.pttl = async function (key) { + return await getExpire(key) - Date.now(); + }; +}; diff --git a/src/database/postgres/sets.js b/src/database/postgres/sets.js new file mode 100644 index 0000000000..5ff9369291 --- /dev/null +++ b/src/database/postgres/sets.js @@ -0,0 +1,261 @@ +'use strict'; + +const _ = require('lodash'); + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.setAdd = async function (key, value) { + if (!Array.isArray(value)) { + value = [value]; + } + if (!value.length) { + return; + } + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectType(client, key, 'set'); + await client.query({ + name: 'setAdd', + text: ` +INSERT INTO "legacy_set" ("_key", "member") +SELECT $1::TEXT, m +FROM UNNEST($2::TEXT[]) m +ON CONFLICT ("_key", "member") +DO NOTHING`, + values: [key, value], + }); + }); + }; + + module.setsAdd = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + + if (!Array.isArray(value)) { + value = [value]; + } + + keys = _.uniq(keys); + + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectsType(client, keys, 'set'); + await client.query({ + name: 'setsAdd', + text: ` +INSERT INTO "legacy_set" ("_key", "member") +SELECT k, m +FROM UNNEST($1::TEXT[]) k +CROSS JOIN UNNEST($2::TEXT[]) m +ON CONFLICT ("_key", "member") +DO NOTHING`, + values: [keys, value], + }); + }); + }; + + module.setRemove = async function (key, value) { + if (!Array.isArray(key)) { + key = [key]; + } + + if (!Array.isArray(value)) { + value = [value]; + } + + await module.pool.query({ + name: 'setRemove', + text: ` +DELETE FROM "legacy_set" + WHERE "_key" = ANY($1::TEXT[]) + AND "member" = ANY($2::TEXT[])`, + values: [key, value], + }); + }; + + module.setsRemove = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + + await module.pool.query({ + name: 'setsRemove', + text: ` +DELETE FROM "legacy_set" + WHERE "_key" = ANY($1::TEXT[]) + AND "member" = $2::TEXT`, + values: [keys, value], + }); + }; + + module.isSetMember = async function (key, value) { + if (!key) { + return false; + } + + const res = await module.pool.query({ + name: 'isSetMember', + text: ` +SELECT 1 + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = $1::TEXT + AND s."member" = $2::TEXT`, + values: [key, value], + }); + + return !!res.rows.length; + }; + + module.isSetMembers = async function (key, values) { + if (!key || !Array.isArray(values) || !values.length) { + return []; + } + + values = values.map(helpers.valueToString); + + const res = await module.pool.query({ + name: 'isSetMembers', + text: ` +SELECT s."member" m + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = $1::TEXT + AND s."member" = ANY($2::TEXT[])`, + values: [key, values], + }); + + return values.map(v => res.rows.some(r => r.m === v)); + }; + + module.isMemberOfSets = async function (sets, value) { + if (!Array.isArray(sets) || !sets.length) { + return []; + } + + value = helpers.valueToString(value); + + const res = await module.pool.query({ + name: 'isMemberOfSets', + text: ` +SELECT o."_key" k + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = ANY($1::TEXT[]) + AND s."member" = $2::TEXT`, + values: [sets, value], + }); + + return sets.map(s => res.rows.some(r => r.k === s)); + }; + + module.getSetMembers = async function (key) { + if (!key) { + return []; + } + + const res = await module.pool.query({ + name: 'getSetMembers', + text: ` +SELECT s."member" m + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = $1::TEXT`, + values: [key], + }); + + return res.rows.map(r => r.m); + }; + + module.getSetsMembers = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + const res = await module.pool.query({ + name: 'getSetsMembers', + text: ` +SELECT o."_key" k, + array_agg(s."member") m + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = ANY($1::TEXT[]) + GROUP BY o."_key"`, + values: [keys], + }); + + return keys.map(k => (res.rows.find(r => r.k === k) || { m: [] }).m); + }; + + module.setCount = async function (key) { + if (!key) { + return 0; + } + + const res = await module.pool.query({ + name: 'setCount', + text: ` +SELECT COUNT(*) c + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = $1::TEXT`, + values: [key], + }); + + return parseInt(res.rows[0].c, 10); + }; + + module.setsCount = async function (keys) { + const res = await module.pool.query({ + name: 'setsCount', + text: ` +SELECT o."_key" k, + COUNT(*) c + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = ANY($1::TEXT[]) + GROUP BY o."_key"`, + values: [keys], + }); + + return keys.map(k => (res.rows.find(r => r.k === k) || { c: 0 }).c); + }; + + module.setRemoveRandom = async function (key) { + const res = await module.pool.query({ + name: 'setRemoveRandom', + text: ` +WITH A AS ( + SELECT s."member" + FROM "legacy_object_live" o + INNER JOIN "legacy_set" s + ON o."_key" = s."_key" + AND o."type" = s."type" + WHERE o."_key" = $1::TEXT + ORDER BY RANDOM() + LIMIT 1 + FOR UPDATE) +DELETE FROM "legacy_set" s + USING A + WHERE s."_key" = $1::TEXT + AND s."member" = A."member" +RETURNING A."member" m`, + values: [key], + }); + return res.rows.length ? res.rows[0].m : null; + }; +}; diff --git a/src/database/postgres/sorted.js b/src/database/postgres/sorted.js new file mode 100644 index 0000000000..eff5e80081 --- /dev/null +++ b/src/database/postgres/sorted.js @@ -0,0 +1,682 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + const util = require('util'); + const Cursor = require('pg-cursor'); + Cursor.prototype.readAsync = util.promisify(Cursor.prototype.read); + const sleep = util.promisify(setTimeout); + + require('./sorted/add')(module); + require('./sorted/remove')(module); + require('./sorted/union')(module); + require('./sorted/intersect')(module); + + module.getSortedSetRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, 1, false); + }; + + module.getSortedSetRevRange = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, -1, false); + }; + + module.getSortedSetRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, 1, true); + }; + + module.getSortedSetRevRangeWithScores = async function (key, start, stop) { + return await getSortedSetRange(key, start, stop, -1, true); + }; + + async function getSortedSetRange(key, start, stop, sort, withScores) { + if (!key) { + return; + } + + if (!Array.isArray(key)) { + key = [key]; + } + + if (start < 0 && start > stop) { + return []; + } + + let reverse = false; + if (start === 0 && stop < -1) { + reverse = true; + sort *= -1; + start = Math.abs(stop + 1); + stop = -1; + } else if (start < 0 && stop > start) { + const tmp1 = Math.abs(stop + 1); + stop = Math.abs(start + 1); + start = tmp1; + } + + let limit = stop - start + 1; + if (limit <= 0) { + limit = null; + } + + const res = await module.pool.query({ + name: `getSortedSetRangeWithScores${sort > 0 ? 'Asc' : 'Desc'}`, + text: ` +SELECT z."value", + z."score" + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = ANY($1::TEXT[]) + ORDER BY z."score" ${sort > 0 ? 'ASC' : 'DESC'} + LIMIT $3::INTEGER +OFFSET $2::INTEGER`, + values: [key, start, limit], + }); + + if (reverse) { + res.rows.reverse(); + } + + if (withScores) { + res.rows = res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) })); + } else { + res.rows = res.rows.map(r => r.value); + } + + return res.rows; + } + + module.getSortedSetRangeByScore = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, false); + }; + + module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, false); + }; + + module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { + return await getSortedSetRangeByScore(key, start, count, min, max, 1, true); + }; + + module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { + return await getSortedSetRangeByScore(key, start, count, min, max, -1, true); + }; + + async function getSortedSetRangeByScore(key, start, count, min, max, sort, withScores) { + if (!key) { + return; + } + + if (!Array.isArray(key)) { + key = [key]; + } + + if (parseInt(count, 10) === -1) { + count = null; + } + + if (min === '-inf') { + min = null; + } + if (max === '+inf') { + max = null; + } + + const res = await module.pool.query({ + name: `getSortedSetRangeByScoreWithScores${sort > 0 ? 'Asc' : 'Desc'}`, + text: ` +SELECT z."value", + z."score" + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = ANY($1::TEXT[]) + AND (z."score" >= $4::NUMERIC OR $4::NUMERIC IS NULL) + AND (z."score" <= $5::NUMERIC OR $5::NUMERIC IS NULL) + ORDER BY z."score" ${sort > 0 ? 'ASC' : 'DESC'} + LIMIT $3::INTEGER +OFFSET $2::INTEGER`, + values: [key, start, count, min, max], + }); + + if (withScores) { + res.rows = res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) })); + } else { + res.rows = res.rows.map(r => r.value); + } + + return res.rows; + } + + module.sortedSetCount = async function (key, min, max) { + if (!key) { + return; + } + + if (min === '-inf') { + min = null; + } + if (max === '+inf') { + max = null; + } + + const res = await module.pool.query({ + name: 'sortedSetCount', + text: ` +SELECT COUNT(*) c + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT + AND (z."score" >= $2::NUMERIC OR $2::NUMERIC IS NULL) + AND (z."score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)`, + values: [key, min, max], + }); + + return parseInt(res.rows[0].c, 10); + }; + + module.sortedSetCard = async function (key) { + if (!key) { + return 0; + } + + const res = await module.pool.query({ + name: 'sortedSetCard', + text: ` +SELECT COUNT(*) c + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT`, + values: [key], + }); + + return parseInt(res.rows[0].c, 10); + }; + + module.sortedSetsCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + const res = await module.pool.query({ + name: 'sortedSetsCard', + text: ` +SELECT o."_key" k, + COUNT(*) c + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = ANY($1::TEXT[]) + GROUP BY o."_key"`, + values: [keys], + }); + + return keys.map(k => parseInt((res.rows.find(r => r.k === k) || { c: 0 }).c, 10)); + }; + + module.sortedSetsCardSum = async function (keys) { + if (!keys || (Array.isArray(keys) && !keys.length)) { + return 0; + } + if (!Array.isArray(keys)) { + keys = [keys]; + } + const counts = await module.sortedSetsCard(keys); + const sum = counts.reduce((acc, val) => acc + val, 0); + return sum; + }; + + module.sortedSetRank = async function (key, value) { + const result = await getSortedSetRank('ASC', [key], [value]); + return result ? result[0] : null; + }; + + module.sortedSetRevRank = async function (key, value) { + const result = await getSortedSetRank('DESC', [key], [value]); + return result ? result[0] : null; + }; + + async function getSortedSetRank(sort, keys, values) { + values = values.map(helpers.valueToString); + const res = await module.pool.query({ + name: `getSortedSetRank${sort}`, + text: ` +SELECT (SELECT r + FROM (SELECT z."value" v, + RANK() OVER (PARTITION BY o."_key" + ORDER BY z."score" ${sort}, + z."value" ${sort}) - 1 r + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = kvi.k) r + WHERE v = kvi.v) r + FROM UNNEST($1::TEXT[], $2::TEXT[]) WITH ORDINALITY kvi(k, v, i) + ORDER BY kvi.i ASC`, + values: [keys, values], + }); + + return res.rows.map(r => (r.r === null ? null : parseFloat(r.r))); + } + + module.sortedSetsRanks = async function (keys, values) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + return await getSortedSetRank('ASC', keys, values); + }; + + module.sortedSetsRevRanks = async function (keys, values) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + return await getSortedSetRank('DESC', keys, values); + }; + + module.sortedSetRanks = async function (key, values) { + if (!Array.isArray(values) || !values.length) { + return []; + } + + return await getSortedSetRank('ASC', new Array(values.length).fill(key), values); + }; + + module.sortedSetRevRanks = async function (key, values) { + if (!Array.isArray(values) || !values.length) { + return []; + } + + return await getSortedSetRank('DESC', new Array(values.length).fill(key), values); + }; + + module.sortedSetScore = async function (key, value) { + if (!key) { + return null; + } + + value = helpers.valueToString(value); + + const res = await module.pool.query({ + name: 'sortedSetScore', + text: ` +SELECT z."score" s + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT + AND z."value" = $2::TEXT`, + values: [key, value], + }); + if (res.rows.length) { + return parseFloat(res.rows[0].s); + } + return null; + }; + + module.sortedSetsScore = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + value = helpers.valueToString(value); + + const res = await module.pool.query({ + name: 'sortedSetsScore', + text: ` +SELECT o."_key" k, + z."score" s + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = ANY($1::TEXT[]) + AND z."value" = $2::TEXT`, + values: [keys, value], + }); + + return keys.map((k) => { + const s = res.rows.find(r => r.k === k); + return s ? parseFloat(s.s) : null; + }); + }; + + module.sortedSetScores = async function (key, values) { + if (!key) { + return null; + } + if (!values.length) { + return []; + } + values = values.map(helpers.valueToString); + + const res = await module.pool.query({ + name: 'sortedSetScores', + text: ` +SELECT z."value" v, + z."score" s + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT + AND z."value" = ANY($2::TEXT[])`, + values: [key, values], + }); + + return values.map((v) => { + const s = res.rows.find(r => r.v === v); + return s ? parseFloat(s.s) : null; + }); + }; + + module.isSortedSetMember = async function (key, value) { + if (!key) { + return; + } + + value = helpers.valueToString(value); + + const res = await module.pool.query({ + name: 'isSortedSetMember', + text: ` +SELECT 1 + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT + AND z."value" = $2::TEXT`, + values: [key, value], + }); + + return !!res.rows.length; + }; + + module.isSortedSetMembers = async function (key, values) { + if (!key) { + return; + } + + if (!values.length) { + return []; + } + values = values.map(helpers.valueToString); + + const res = await module.pool.query({ + name: 'isSortedSetMembers', + text: ` +SELECT z."value" v + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT + AND z."value" = ANY($2::TEXT[])`, + values: [key, values], + }); + + return values.map(v => res.rows.some(r => r.v === v)); + }; + + module.isMemberOfSortedSets = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + value = helpers.valueToString(value); + + const res = await module.pool.query({ + name: 'isMemberOfSortedSets', + text: ` +SELECT o."_key" k + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = ANY($1::TEXT[]) + AND z."value" = $2::TEXT`, + values: [keys, value], + }); + + return keys.map(k => res.rows.some(r => r.k === k)); + }; + + module.getSortedSetMembers = async function (key) { + const data = await module.getSortedSetsMembers([key]); + return data && data[0]; + }; + + module.getSortedSetsMembers = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + const res = await module.pool.query({ + name: 'getSortedSetsMembers', + text: ` +SELECT "_key" k, + "nodebb_get_sorted_set_members"("_key") m + FROM UNNEST($1::TEXT[]) "_key";`, + values: [keys], + }); + + return keys.map(k => (res.rows.find(r => r.k === k) || {}).m || []); + }; + + module.sortedSetIncrBy = async function (key, increment, value) { + if (!key) { + return; + } + + value = helpers.valueToString(value); + increment = parseFloat(increment); + + return await module.transaction(async (client) => { + await helpers.ensureLegacyObjectType(client, key, 'zset'); + const res = await client.query({ + name: 'sortedSetIncrBy', + text: ` +INSERT INTO "legacy_zset" ("_key", "value", "score") +VALUES ($1::TEXT, $2::TEXT, $3::NUMERIC) +ON CONFLICT ("_key", "value") +DO UPDATE SET "score" = "legacy_zset"."score" + $3::NUMERIC +RETURNING "score" s`, + values: [key, value, increment], + }); + return parseFloat(res.rows[0].s); + }); + }; + + module.sortedSetIncrByBulk = async function (data) { + // TODO: perf single query? + return await Promise.all(data.map(item => module.sortedSetIncrBy(item[0], item[1], item[2]))); + }; + + module.getSortedSetRangeByLex = async function (key, min, max, start, count) { + return await sortedSetLex(key, min, max, 1, start, count); + }; + + module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) { + return await sortedSetLex(key, min, max, -1, start, count); + }; + + module.sortedSetLexCount = async function (key, min, max) { + const q = buildLexQuery(key, min, max); + + const res = await module.pool.query({ + name: `sortedSetLexCount${q.suffix}`, + text: ` +SELECT COUNT(*) c + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE ${q.where}`, + values: q.values, + }); + + return parseInt(res.rows[0].c, 10); + }; + + async function sortedSetLex(key, min, max, sort, start, count) { + start = start !== undefined ? start : 0; + count = count !== undefined ? count : 0; + + const q = buildLexQuery(key, min, max); + q.values.push(start); + q.values.push(count <= 0 ? null : count); + const res = await module.pool.query({ + name: `sortedSetLex${sort > 0 ? 'Asc' : 'Desc'}${q.suffix}`, + text: ` +SELECT z."value" v + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE ${q.where} + ORDER BY z."value" ${sort > 0 ? 'ASC' : 'DESC'} + LIMIT $${q.values.length}::INTEGER +OFFSET $${q.values.length - 1}::INTEGER`, + values: q.values, + }); + + return res.rows.map(r => r.v); + } + + module.sortedSetRemoveRangeByLex = async function (key, min, max) { + const q = buildLexQuery(key, min, max); + await module.pool.query({ + name: `sortedSetRemoveRangeByLex${q.suffix}`, + text: ` +DELETE FROM "legacy_zset" z + USING "legacy_object_live" o + WHERE o."_key" = z."_key" + AND o."type" = z."type" + AND ${q.where}`, + values: q.values, + }); + }; + + function buildLexQuery(key, min, max) { + const q = { + suffix: '', + where: `o."_key" = $1::TEXT`, + values: [key], + }; + + if (min !== '-') { + if (min.match(/^\(/)) { + q.values.push(min.slice(1)); + q.suffix += 'GT'; + q.where += ` AND z."value" > $${q.values.length}::TEXT COLLATE "C"`; + } else if (min.match(/^\[/)) { + q.values.push(min.slice(1)); + q.suffix += 'GE'; + q.where += ` AND z."value" >= $${q.values.length}::TEXT COLLATE "C"`; + } else { + q.values.push(min); + q.suffix += 'GE'; + q.where += ` AND z."value" >= $${q.values.length}::TEXT COLLATE "C"`; + } + } + + if (max !== '+') { + if (max.match(/^\(/)) { + q.values.push(max.slice(1)); + q.suffix += 'LT'; + q.where += ` AND z."value" < $${q.values.length}::TEXT COLLATE "C"`; + } else if (max.match(/^\[/)) { + q.values.push(max.slice(1)); + q.suffix += 'LE'; + q.where += ` AND z."value" <= $${q.values.length}::TEXT COLLATE "C"`; + } else { + q.values.push(max); + q.suffix += 'LE'; + q.where += ` AND z."value" <= $${q.values.length}::TEXT COLLATE "C"`; + } + } + + return q; + } + + module.getSortedSetScan = async function (params) { + let { match } = params; + if (match.startsWith('*')) { + match = `%${match.substring(1)}`; + } + + if (match.endsWith('*')) { + match = `${match.substring(0, match.length - 1)}%`; + } + + const res = await module.pool.query({ + text: ` +SELECT z."value", + z."score" + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT + AND z."value" LIKE '${match}' + LIMIT $2::INTEGER`, + values: [params.key, params.limit], + }); + if (!params.withScores) { + return res.rows.map(r => r.value); + } + return res.rows.map(r => ({ value: r.value, score: parseFloat(r.score) })); + }; + + module.processSortedSet = async function (setKey, process, options) { + const client = await module.pool.connect(); + const batchSize = (options || {}).batch || 100; + const cursor = client.query(new Cursor(` +SELECT z."value", z."score" + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = $1::TEXT + ORDER BY z."score" ASC, z."value" ASC`, [setKey])); + + if (process && process.constructor && process.constructor.name !== 'AsyncFunction') { + process = util.promisify(process); + } + + while (true) { + /* eslint-disable no-await-in-loop */ + let rows = await cursor.readAsync(batchSize); + if (!rows.length) { + client.release(); + return; + } + + if (options.withScores) { + rows = rows.map(r => ({ value: r.value, score: parseFloat(r.score) })); + } else { + rows = rows.map(r => r.value); + } + try { + await process(rows); + } catch (err) { + await client.release(); + throw err; + } + if (options.interval) { + await sleep(options.interval); + } + } + }; +}; diff --git a/src/database/postgres/sorted/add.js b/src/database/postgres/sorted/add.js new file mode 100644 index 0000000000..6271ff14e6 --- /dev/null +++ b/src/database/postgres/sorted/add.js @@ -0,0 +1,133 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + const utils = require('../../../utils'); + + module.sortedSetAdd = async function (key, score, value) { + if (!key) { + return; + } + + if (Array.isArray(score) && Array.isArray(value)) { + return await sortedSetAddBulk(key, score, value); + } + if (!utils.isNumber(score)) { + throw new Error(`[[error:invalid-score, ${score}]]`); + } + value = helpers.valueToString(value); + score = parseFloat(score); + + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectType(client, key, 'zset'); + await client.query({ + name: 'sortedSetAdd', + text: ` + INSERT INTO "legacy_zset" ("_key", "value", "score") + VALUES ($1::TEXT, $2::TEXT, $3::NUMERIC) + ON CONFLICT ("_key", "value") + DO UPDATE SET "score" = $3::NUMERIC`, + values: [key, value, score], + }); + }); + }; + + async function sortedSetAddBulk(key, scores, values) { + if (!scores.length || !values.length) { + return; + } + if (scores.length !== values.length) { + throw new Error('[[error:invalid-data]]'); + } + for (let i = 0; i < scores.length; i += 1) { + if (!utils.isNumber(scores[i])) { + throw new Error(`[[error:invalid-score, ${scores[i]}]]`); + } + } + values = values.map(helpers.valueToString); + scores = scores.map(score => parseFloat(score)); + + helpers.removeDuplicateValues(values, scores); + + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectType(client, key, 'zset'); + await client.query({ + name: 'sortedSetAddBulk', + text: ` +INSERT INTO "legacy_zset" ("_key", "value", "score") +SELECT $1::TEXT, v, s +FROM UNNEST($2::TEXT[], $3::NUMERIC[]) vs(v, s) +ON CONFLICT ("_key", "value") +DO UPDATE SET "score" = EXCLUDED."score"`, + values: [key, values, scores], + }); + }); + } + + module.sortedSetsAdd = async function (keys, scores, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + const isArrayOfScores = Array.isArray(scores); + if ((!isArrayOfScores && !utils.isNumber(scores)) || + (isArrayOfScores && scores.map(s => utils.isNumber(s)).includes(false))) { + throw new Error(`[[error:invalid-score, ${scores}]]`); + } + + if (isArrayOfScores && scores.length !== keys.length) { + throw new Error('[[error:invalid-data]]'); + } + + value = helpers.valueToString(value); + scores = isArrayOfScores ? scores.map(score => parseFloat(score)) : parseFloat(scores); + + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectsType(client, keys, 'zset'); + await client.query({ + name: isArrayOfScores ? 'sortedSetsAddScores' : 'sortedSetsAdd', + text: isArrayOfScores ? ` +INSERT INTO "legacy_zset" ("_key", "value", "score") +SELECT k, $2::TEXT, s +FROM UNNEST($1::TEXT[], $3::NUMERIC[]) vs(k, s) +ON CONFLICT ("_key", "value") + DO UPDATE SET "score" = EXCLUDED."score"` : ` +INSERT INTO "legacy_zset" ("_key", "value", "score") + SELECT k, $2::TEXT, $3::NUMERIC + FROM UNNEST($1::TEXT[]) k + ON CONFLICT ("_key", "value") + DO UPDATE SET "score" = $3::NUMERIC`, + values: [keys, value, scores], + }); + }); + }; + + module.sortedSetAddBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + const keys = []; + const values = []; + const scores = []; + data.forEach((item) => { + if (!utils.isNumber(item[1])) { + throw new Error(`[[error:invalid-score, ${item[1]}]]`); + } + keys.push(item[0]); + scores.push(item[1]); + values.push(item[2]); + }); + await module.transaction(async (client) => { + await helpers.ensureLegacyObjectsType(client, keys, 'zset'); + await client.query({ + name: 'sortedSetAddBulk2', + text: ` +INSERT INTO "legacy_zset" ("_key", "value", "score") +SELECT k, v, s +FROM UNNEST($1::TEXT[], $2::TEXT[], $3::NUMERIC[]) vs(k, v, s) +ON CONFLICT ("_key", "value") +DO UPDATE SET "score" = EXCLUDED."score"`, + values: [keys, values, scores], + }); + }); + }; +}; diff --git a/src/database/postgres/sorted/intersect.js b/src/database/postgres/sorted/intersect.js new file mode 100644 index 0000000000..934bcd159a --- /dev/null +++ b/src/database/postgres/sorted/intersect.js @@ -0,0 +1,92 @@ +'use strict'; + +module.exports = function (module) { + module.sortedSetIntersectCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return 0; + } + + const res = await module.pool.query({ + name: 'sortedSetIntersectCard', + text: ` +WITH A AS (SELECT z."value" v, + COUNT(*) c + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = ANY($1::TEXT[]) + GROUP BY z."value") +SELECT COUNT(*) c + FROM A + WHERE A.c = array_length($1::TEXT[], 1)`, + values: [keys], + }); + + return parseInt(res.rows[0].c, 10); + }; + + module.getSortedSetIntersect = async function (params) { + params.sort = 1; + return await getSortedSetIntersect(params); + }; + + module.getSortedSetRevIntersect = async function (params) { + params.sort = -1; + return await getSortedSetIntersect(params); + }; + + async function getSortedSetIntersect(params) { + const { sets } = params; + const start = params.hasOwnProperty('start') ? params.start : 0; + const stop = params.hasOwnProperty('stop') ? params.stop : -1; + let weights = params.weights || []; + const aggregate = params.aggregate || 'SUM'; + + if (sets.length < weights.length) { + weights = weights.slice(0, sets.length); + } + while (sets.length > weights.length) { + weights.push(1); + } + + let limit = stop - start + 1; + if (limit <= 0) { + limit = null; + } + + const res = await module.pool.query({ + name: `getSortedSetIntersect${aggregate}${params.sort > 0 ? 'Asc' : 'Desc'}WithScores`, + text: ` +WITH A AS (SELECT z."value", + ${aggregate}(z."score" * k."weight") "score", + COUNT(*) c + FROM UNNEST($1::TEXT[], $2::NUMERIC[]) k("_key", "weight") + INNER JOIN "legacy_object_live" o + ON o."_key" = k."_key" + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + GROUP BY z."value") +SELECT A."value", + A."score" + FROM A + WHERE c = array_length($1::TEXT[], 1) + ORDER BY A."score" ${params.sort > 0 ? 'ASC' : 'DESC'} + LIMIT $4::INTEGER +OFFSET $3::INTEGER`, + values: [sets, weights, start, limit], + }); + + if (params.withScores) { + res.rows = res.rows.map(r => ({ + value: r.value, + score: parseFloat(r.score), + })); + } else { + res.rows = res.rows.map(r => r.value); + } + + return res.rows; + } +}; diff --git a/src/database/postgres/sorted/remove.js b/src/database/postgres/sorted/remove.js new file mode 100644 index 0000000000..eb9baa9d08 --- /dev/null +++ b/src/database/postgres/sorted/remove.js @@ -0,0 +1,91 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + + module.sortedSetRemove = async function (key, value) { + if (!key) { + return; + } + const isValueArray = Array.isArray(value); + if (!value || (isValueArray && !value.length)) { + return; + } + + if (!Array.isArray(key)) { + key = [key]; + } + + if (!isValueArray) { + value = [value]; + } + value = value.map(helpers.valueToString); + await module.pool.query({ + name: 'sortedSetRemove', + text: ` +DELETE FROM "legacy_zset" + WHERE "_key" = ANY($1::TEXT[]) + AND "value" = ANY($2::TEXT[])`, + values: [key, value], + }); + }; + + module.sortedSetsRemove = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + + value = helpers.valueToString(value); + + await module.pool.query({ + name: 'sortedSetsRemove', + text: ` +DELETE FROM "legacy_zset" + WHERE "_key" = ANY($1::TEXT[]) + AND "value" = $2::TEXT`, + values: [keys, value], + }); + }; + + module.sortedSetsRemoveRangeByScore = async function (keys, min, max) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + + if (min === '-inf') { + min = null; + } + if (max === '+inf') { + max = null; + } + + await module.pool.query({ + name: 'sortedSetsRemoveRangeByScore', + text: ` +DELETE FROM "legacy_zset" + WHERE "_key" = ANY($1::TEXT[]) + AND ("score" >= $2::NUMERIC OR $2::NUMERIC IS NULL) + AND ("score" <= $3::NUMERIC OR $3::NUMERIC IS NULL)`, + values: [keys, min, max], + }); + }; + + module.sortedSetRemoveBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + const keys = data.map(d => d[0]); + const values = data.map(d => d[1]); + + await module.pool.query({ + name: 'sortedSetRemoveBulk', + text: ` + DELETE FROM "legacy_zset" + WHERE (_key, value) IN ( + SELECT k, v + FROM UNNEST($1::TEXT[], $2::TEXT[]) vs(k, v) + )`, + values: [keys, values], + }); + }; +}; diff --git a/src/database/postgres/sorted/union.js b/src/database/postgres/sorted/union.js new file mode 100644 index 0000000000..9277269748 --- /dev/null +++ b/src/database/postgres/sorted/union.js @@ -0,0 +1,83 @@ +'use strict'; + +module.exports = function (module) { + module.sortedSetUnionCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return 0; + } + + const res = await module.pool.query({ + name: 'sortedSetUnionCard', + text: ` +SELECT COUNT(DISTINCT z."value") c + FROM "legacy_object_live" o + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + WHERE o."_key" = ANY($1::TEXT[])`, + values: [keys], + }); + return res.rows[0].c; + }; + + module.getSortedSetUnion = async function (params) { + params.sort = 1; + return await getSortedSetUnion(params); + }; + + module.getSortedSetRevUnion = async function (params) { + params.sort = -1; + return await getSortedSetUnion(params); + }; + + async function getSortedSetUnion(params) { + const { sets } = params; + const start = params.hasOwnProperty('start') ? params.start : 0; + const stop = params.hasOwnProperty('stop') ? params.stop : -1; + let weights = params.weights || []; + const aggregate = params.aggregate || 'SUM'; + + if (sets.length < weights.length) { + weights = weights.slice(0, sets.length); + } + while (sets.length > weights.length) { + weights.push(1); + } + + let limit = stop - start + 1; + if (limit <= 0) { + limit = null; + } + + const res = await module.pool.query({ + name: `getSortedSetUnion${aggregate}${params.sort > 0 ? 'Asc' : 'Desc'}WithScores`, + text: ` +WITH A AS (SELECT z."value", + ${aggregate}(z."score" * k."weight") "score" + FROM UNNEST($1::TEXT[], $2::NUMERIC[]) k("_key", "weight") + INNER JOIN "legacy_object_live" o + ON o."_key" = k."_key" + INNER JOIN "legacy_zset" z + ON o."_key" = z."_key" + AND o."type" = z."type" + GROUP BY z."value") +SELECT A."value", + A."score" + FROM A + ORDER BY A."score" ${params.sort > 0 ? 'ASC' : 'DESC'} + LIMIT $4::INTEGER +OFFSET $3::INTEGER`, + values: [sets, weights, start, limit], + }); + + if (params.withScores) { + res.rows = res.rows.map(r => ({ + value: r.value, + score: parseFloat(r.score), + })); + } else { + res.rows = res.rows.map(r => r.value); + } + return res.rows; + } +}; diff --git a/src/database/postgres/transaction.js b/src/database/postgres/transaction.js new file mode 100644 index 0000000000..1dcb5210c1 --- /dev/null +++ b/src/database/postgres/transaction.js @@ -0,0 +1,32 @@ +'use strict'; + +module.exports = function (module) { + module.transaction = async function (perform, txClient) { + let res; + if (txClient) { + await txClient.query(`SAVEPOINT nodebb_subtx`); + try { + res = await perform(txClient); + } catch (err) { + await txClient.query(`ROLLBACK TO SAVEPOINT nodebb_subtx`); + throw err; + } + await txClient.query(`RELEASE SAVEPOINT nodebb_subtx`); + return res; + } + // see https://node-postgres.com/features/transactions#a-pooled-client-with-async-await + const client = await module.pool.connect(); + + try { + await client.query('BEGIN'); + res = await perform(client); + await client.query('COMMIT'); + } catch (err) { + await client.query('ROLLBACK'); + throw err; + } finally { + client.release(); + } + return res; + }; +}; diff --git a/src/database/redis.js b/src/database/redis.js new file mode 100644 index 0000000000..8f74c08a26 --- /dev/null +++ b/src/database/redis.js @@ -0,0 +1,119 @@ +'use strict'; + +const nconf = require('nconf'); +const semver = require('semver'); +const session = require('express-session'); + +const connection = require('./redis/connection'); + +const redisModule = module.exports; + +redisModule.questions = [ + { + name: 'redis:host', + description: 'Host IP or address of your Redis instance', + default: nconf.get('redis:host') || '127.0.0.1', + }, + { + name: 'redis:port', + description: 'Host port of your Redis instance', + default: nconf.get('redis:port') || 6379, + }, + { + name: 'redis:password', + description: 'Password of your Redis database', + hidden: true, + default: nconf.get('redis:password') || '', + before: function (value) { value = value || nconf.get('redis:password') || ''; return value; }, + }, + { + name: 'redis:database', + description: 'Which database to use (0..n)', + default: nconf.get('redis:database') || 0, + }, +]; + + +redisModule.init = async function () { + redisModule.client = await connection.connect(nconf.get('redis')); +}; + +redisModule.createSessionStore = async function (options) { + const meta = require('../meta'); + const sessionStore = require('connect-redis')(session); + const client = await connection.connect(options); + const store = new sessionStore({ + client: client, + ttl: meta.getSessionTTLSeconds(), + }); + return store; +}; + +redisModule.checkCompatibility = async function () { + const info = await redisModule.info(redisModule.client); + await redisModule.checkCompatibilityVersion(info.redis_version); +}; + +redisModule.checkCompatibilityVersion = function (version, callback) { + if (semver.lt(version, '2.8.9')) { + callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.')); + } + callback(); +}; + +redisModule.close = async function () { + await redisModule.client.quit(); +}; + +redisModule.info = async function (cxn) { + if (!cxn) { + cxn = await connection.connect(nconf.get('redis')); + } + redisModule.client = redisModule.client || cxn; + const data = await cxn.info(); + const lines = data.toString().split('\r\n').sort(); + const redisData = {}; + lines.forEach((line) => { + const parts = line.split(':'); + if (parts[1]) { + redisData[parts[0]] = parts[1]; + } + }); + + const keyInfo = redisData[`db${nconf.get('redis:database')}`]; + if (keyInfo) { + const split = keyInfo.split(','); + redisData.keys = (split[0] || '').replace('keys=', ''); + redisData.expires = (split[1] || '').replace('expires=', ''); + redisData.avg_ttl = (split[2] || '').replace('avg_ttl=', ''); + } + + redisData.instantaneous_input = (redisData.instantaneous_input_kbps / 1024).toFixed(3); + redisData.instantaneous_output = (redisData.instantaneous_output_kbps / 1024).toFixed(3); + + redisData.total_net_input = (redisData.total_net_input_bytes / (1024 * 1024 * 1024)).toFixed(3); + redisData.total_net_output = (redisData.total_net_output_bytes / (1024 * 1024 * 1024)).toFixed(3); + + redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(3); + redisData.raw = JSON.stringify(redisData, null, 4); + redisData.redis = true; + return redisData; +}; + +redisModule.socketAdapter = async function () { + const redisAdapter = require('@socket.io/redis-adapter'); + const pub = await connection.connect(nconf.get('redis')); + const sub = await connection.connect(nconf.get('redis')); + return redisAdapter(pub, sub, { + key: `db:${nconf.get('redis:database')}:adapter_key`, + }); +}; + +require('./redis/main')(redisModule); +require('./redis/hash')(redisModule); +require('./redis/sets')(redisModule); +require('./redis/sorted')(redisModule); +require('./redis/list')(redisModule); +require('./redis/transaction')(redisModule); + +require('../promisify')(redisModule, ['client', 'sessionStore']); diff --git a/src/database/redis/connection.js b/src/database/redis/connection.js new file mode 100644 index 0000000000..8876fa48d7 --- /dev/null +++ b/src/database/redis/connection.js @@ -0,0 +1,62 @@ +'use strict'; + +const nconf = require('nconf'); +const Redis = require('ioredis'); +const winston = require('winston'); + +const connection = module.exports; + +connection.connect = async function (options) { + return new Promise((resolve, reject) => { + options = options || nconf.get('redis'); + const redis_socket_or_host = options.host; + + let cxn; + if (options.cluster) { + cxn = new Redis.Cluster(options.cluster, options.options); + } else if (options.sentinels) { + cxn = new Redis({ + sentinels: options.sentinels, + ...options.options, + }); + } else if (redis_socket_or_host && String(redis_socket_or_host).indexOf('/') >= 0) { + // If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock + cxn = new Redis({ + ...options.options, + path: redis_socket_or_host, + password: options.password, + db: options.database, + }); + } else { + // Else, connect over tcp/ip + cxn = new Redis({ + ...options.options, + host: redis_socket_or_host, + port: options.port, + password: options.password, + db: options.database, + }); + } + + const dbIdx = parseInt(options.database, 10); + if (!(dbIdx >= 0)) { + throw new Error('[[error:no-database-selected]]'); + } + + cxn.on('error', (err) => { + winston.error(err.stack); + reject(err); + }); + cxn.on('ready', () => { + // back-compat with node_redis + cxn.batch = cxn.pipeline; + resolve(cxn); + }); + + if (options.password) { + cxn.auth(options.password); + } + }); +}; + +require('../../promisify')(connection); diff --git a/src/database/redis/hash.js b/src/database/redis/hash.js new file mode 100644 index 0000000000..e82a7bafdb --- /dev/null +++ b/src/database/redis/hash.js @@ -0,0 +1,237 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + const cache = require('../cache').create('redis'); + + module.objectCache = cache; + + module.setObject = async function (key, data) { + if (!key || !data) { + return; + } + + if (data.hasOwnProperty('')) { + delete data['']; + } + + Object.keys(data).forEach((key) => { + if (data[key] === undefined || data[key] === null) { + delete data[key]; + } + }); + + if (!Object.keys(data).length) { + return; + } + if (Array.isArray(key)) { + const batch = module.client.batch(); + key.forEach(k => batch.hmset(k, data)); + await helpers.execBatch(batch); + } else { + await module.client.hmset(key, data); + } + + cache.del(key); + }; + + module.setObjectBulk = async function (...args) { + let data = args[0]; + if (!Array.isArray(data) || !data.length) { + return; + } + if (Array.isArray(args[1])) { + console.warn('[deprecated] db.setObjectBulk(keys, data) usage is deprecated, please use db.setObjectBulk(data)'); + // conver old format to new format for backwards compatibility + data = args[0].map((key, i) => [key, args[1][i]]); + } + + const batch = module.client.batch(); + data.forEach((item) => { + if (Object.keys(item[1]).length) { + batch.hmset(item[0], item[1]); + } + }); + await helpers.execBatch(batch); + cache.del(data.map(item => item[0])); + }; + + module.setObjectField = async function (key, field, value) { + if (!field) { + return; + } + if (Array.isArray(key)) { + const batch = module.client.batch(); + key.forEach(k => batch.hset(k, field, value)); + await helpers.execBatch(batch); + } else { + await module.client.hset(key, field, value); + } + + cache.del(key); + }; + + module.getObject = async function (key, fields = []) { + if (!key) { + return null; + } + + const data = await module.getObjectsFields([key], fields); + return data && data.length ? data[0] : null; + }; + + module.getObjects = async function (keys, fields = []) { + return await module.getObjectsFields(keys, fields); + }; + + module.getObjectField = async function (key, field) { + if (!key) { + return null; + } + const cachedData = {}; + cache.getUnCachedKeys([key], cachedData); + if (cachedData[key]) { + return cachedData[key].hasOwnProperty(field) ? cachedData[key][field] : null; + } + return await module.client.hget(key, String(field)); + }; + + module.getObjectFields = async function (key, fields) { + if (!key) { + return null; + } + const results = await module.getObjectsFields([key], fields); + return results ? results[0] : null; + }; + + module.getObjectsFields = async function (keys, fields) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + + const cachedData = {}; + const unCachedKeys = cache.getUnCachedKeys(keys, cachedData); + + let data = []; + if (unCachedKeys.length > 1) { + const batch = module.client.batch(); + unCachedKeys.forEach(k => batch.hgetall(k)); + data = await helpers.execBatch(batch); + } else if (unCachedKeys.length === 1) { + data = [await module.client.hgetall(unCachedKeys[0])]; + } + + // convert empty objects into null for back-compat with node_redis + data = data.map((elem) => { + if (!Object.keys(elem).length) { + return null; + } + return elem; + }); + + unCachedKeys.forEach((key, i) => { + cachedData[key] = data[i] || null; + cache.set(key, cachedData[key]); + }); + + if (!Array.isArray(fields) || !fields.length) { + return keys.map(key => (cachedData[key] ? { ...cachedData[key] } : null)); + } + return keys.map((key) => { + const item = cachedData[key] || {}; + const result = {}; + fields.forEach((field) => { + result[field] = item[field] !== undefined ? item[field] : null; + }); + return result; + }); + }; + + module.getObjectKeys = async function (key) { + return await module.client.hkeys(key); + }; + + module.getObjectValues = async function (key) { + return await module.client.hvals(key); + }; + + module.isObjectField = async function (key, field) { + const exists = await module.client.hexists(key, field); + return exists === 1; + }; + + module.isObjectFields = async function (key, fields) { + const batch = module.client.batch(); + fields.forEach(f => batch.hexists(String(key), String(f))); + const results = await helpers.execBatch(batch); + return Array.isArray(results) ? helpers.resultsToBool(results) : null; + }; + + module.deleteObjectField = async function (key, field) { + if (key === undefined || key === null || field === undefined || field === null) { + return; + } + await module.client.hdel(key, field); + cache.del(key); + }; + + module.deleteObjectFields = async function (key, fields) { + if (!key || (Array.isArray(key) && !key.length) || !Array.isArray(fields) || !fields.length) { + return; + } + fields = fields.filter(Boolean); + if (!fields.length) { + return; + } + if (Array.isArray(key)) { + const batch = module.client.batch(); + key.forEach(k => batch.hdel(k, fields)); + await helpers.execBatch(batch); + } else { + await module.client.hdel(key, fields); + } + + cache.del(key); + }; + + module.incrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, 1); + }; + + module.decrObjectField = async function (key, field) { + return await module.incrObjectFieldBy(key, field, -1); + }; + + module.incrObjectFieldBy = async function (key, field, value) { + value = parseInt(value, 10); + if (!key || isNaN(value)) { + return null; + } + let result; + if (Array.isArray(key)) { + const batch = module.client.batch(); + key.forEach(k => batch.hincrby(k, field, value)); + result = await helpers.execBatch(batch); + } else { + result = await module.client.hincrby(key, field, value); + } + cache.del(key); + return Array.isArray(result) ? result.map(value => parseInt(value, 10)) : parseInt(result, 10); + }; + + module.incrObjectFieldByBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + + const batch = module.client.batch(); + data.forEach((item) => { + for (const [field, value] of Object.entries(item[1])) { + batch.hincrby(item[0], field, value); + } + }); + await helpers.execBatch(batch); + cache.del(data.map(item => item[0])); + }; +}; diff --git a/src/database/redis/helpers.js b/src/database/redis/helpers.js new file mode 100644 index 0000000000..d9b8a680b3 --- /dev/null +++ b/src/database/redis/helpers.js @@ -0,0 +1,30 @@ +'use strict'; + +const helpers = module.exports; + +helpers.noop = function () {}; + +helpers.execBatch = async function (batch) { + const results = await batch.exec(); + return results.map(([err, res]) => { + if (err) { + throw err; + } + return res; + }); +}; + +helpers.resultsToBool = function (results) { + for (let i = 0; i < results.length; i += 1) { + results[i] = results[i] === 1; + } + return results; +}; + +helpers.zsetToObjectArray = function (data) { + const objects = new Array(data.length / 2); + for (let i = 0, k = 0; i < objects.length; i += 1, k += 2) { + objects[i] = { value: data[k], score: parseFloat(data[k + 1]) }; + } + return objects; +}; diff --git a/src/database/redis/list.js b/src/database/redis/list.js new file mode 100644 index 0000000000..135be1c73f --- /dev/null +++ b/src/database/redis/list.js @@ -0,0 +1,57 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.listPrepend = async function (key, value) { + if (!key) { + return; + } + await module.client.lpush(key, value); + }; + + module.listAppend = async function (key, value) { + if (!key) { + return; + } + await module.client.rpush(key, value); + }; + + module.listRemoveLast = async function (key) { + if (!key) { + return; + } + return await module.client.rpop(key); + }; + + module.listRemoveAll = async function (key, value) { + if (!key) { + return; + } + if (Array.isArray(value)) { + const batch = module.client.batch(); + value.forEach(value => batch.lrem(key, 0, value)); + await helpers.execBatch(batch); + } else { + await module.client.lrem(key, 0, value); + } + }; + + module.listTrim = async function (key, start, stop) { + if (!key) { + return; + } + await module.client.ltrim(key, start, stop); + }; + + module.getListRange = async function (key, start, stop) { + if (!key) { + return; + } + return await module.client.lrange(key, start, stop); + }; + + module.listLength = async function (key) { + return await module.client.llen(key); + }; +}; diff --git a/src/database/redis/main.js b/src/database/redis/main.js new file mode 100644 index 0000000000..140bce59fa --- /dev/null +++ b/src/database/redis/main.js @@ -0,0 +1,111 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.flushdb = async function () { + await module.client.send_command('flushdb', []); + }; + + module.emptydb = async function () { + await module.flushdb(); + module.objectCache.reset(); + }; + + module.exists = async function (key) { + if (Array.isArray(key)) { + const batch = module.client.batch(); + key.forEach(key => batch.exists(key)); + const data = await helpers.execBatch(batch); + return data.map(exists => exists === 1); + } + const exists = await module.client.exists(key); + return exists === 1; + }; + + module.scan = async function (params) { + let cursor = '0'; + let returnData = []; + const seen = {}; + do { + /* eslint-disable no-await-in-loop */ + const res = await module.client.scan(cursor, 'MATCH', params.match, 'COUNT', 10000); + cursor = res[0]; + const values = res[1].filter((value) => { + const isSeen = !!seen[value]; + if (!isSeen) { + seen[value] = 1; + } + return !isSeen; + }); + returnData = returnData.concat(values); + } while (cursor !== '0'); + return returnData; + }; + + module.delete = async function (key) { + await module.client.del(key); + module.objectCache.del(key); + }; + + module.deleteAll = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + await module.client.del(keys); + module.objectCache.del(keys); + }; + + module.get = async function (key) { + return await module.client.get(key); + }; + + module.set = async function (key, value) { + await module.client.set(key, value); + }; + + module.increment = async function (key) { + return await module.client.incr(key); + }; + + module.rename = async function (oldKey, newKey) { + try { + await module.client.rename(oldKey, newKey); + } catch (err) { + if (err && err.message !== 'ERR no such key') { + throw err; + } + } + + module.objectCache.del([oldKey, newKey]); + }; + + module.type = async function (key) { + const type = await module.client.type(key); + return type !== 'none' ? type : null; + }; + + module.expire = async function (key, seconds) { + await module.client.expire(key, seconds); + }; + + module.expireAt = async function (key, timestamp) { + await module.client.expireat(key, timestamp); + }; + + module.pexpire = async function (key, ms) { + await module.client.pexpire(key, ms); + }; + + module.pexpireAt = async function (key, timestamp) { + await module.client.pexpireat(key, timestamp); + }; + + module.ttl = async function (key) { + return await module.client.ttl(key); + }; + + module.pttl = async function (key) { + return await module.client.pttl(key); + }; +}; diff --git a/src/database/redis/pubsub.js b/src/database/redis/pubsub.js new file mode 100644 index 0000000000..b39d24a29b --- /dev/null +++ b/src/database/redis/pubsub.js @@ -0,0 +1,49 @@ +'use strict'; + +const nconf = require('nconf'); +const util = require('util'); +const winston = require('winston'); +const { EventEmitter } = require('events'); +const connection = require('./connection'); + +let channelName; +const PubSub = function () { + const self = this; + channelName = `db:${nconf.get('redis:database')}:pubsub_channel`; + self.queue = []; + connection.connect().then((client) => { + self.subClient = client; + self.subClient.subscribe(channelName); + self.subClient.on('message', (channel, message) => { + if (channel !== channelName) { + return; + } + + try { + const msg = JSON.parse(message); + self.emit(msg.event, msg.data); + } catch (err) { + winston.error(err.stack); + } + }); + }); + + connection.connect().then((client) => { + self.pubClient = client; + self.queue.forEach(payload => client.publish(channelName, payload)); + self.queue.length = 0; + }); +}; + +util.inherits(PubSub, EventEmitter); + +PubSub.prototype.publish = function (event, data) { + const payload = JSON.stringify({ event: event, data: data }); + if (this.pubClient) { + this.pubClient.publish(channelName, payload); + } else { + this.queue.push(payload); + } +}; + +module.exports = new PubSub(); diff --git a/src/database/redis/sets.js b/src/database/redis/sets.js new file mode 100644 index 0000000000..d0ea63c59b --- /dev/null +++ b/src/database/redis/sets.js @@ -0,0 +1,91 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('./helpers'); + + module.setAdd = async function (key, value) { + if (!Array.isArray(value)) { + value = [value]; + } + if (!value.length) { + return; + } + await module.client.sadd(key, value); + }; + + module.setsAdd = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + const batch = module.client.batch(); + keys.forEach(k => batch.sadd(String(k), String(value))); + await helpers.execBatch(batch); + }; + + module.setRemove = async function (key, value) { + if (!Array.isArray(value)) { + value = [value]; + } + if (!Array.isArray(key)) { + key = [key]; + } + if (!value.length) { + return; + } + + const batch = module.client.batch(); + key.forEach(k => batch.srem(String(k), value)); + await helpers.execBatch(batch); + }; + + module.setsRemove = async function (keys, value) { + const batch = module.client.batch(); + keys.forEach(k => batch.srem(String(k), value)); + await helpers.execBatch(batch); + }; + + module.isSetMember = async function (key, value) { + const result = await module.client.sismember(key, value); + return result === 1; + }; + + module.isSetMembers = async function (key, values) { + const batch = module.client.batch(); + values.forEach(v => batch.sismember(String(key), String(v))); + const results = await helpers.execBatch(batch); + return results ? helpers.resultsToBool(results) : null; + }; + + module.isMemberOfSets = async function (sets, value) { + const batch = module.client.batch(); + sets.forEach(s => batch.sismember(String(s), String(value))); + const results = await helpers.execBatch(batch); + return results ? helpers.resultsToBool(results) : null; + }; + + module.getSetMembers = async function (key) { + return await module.client.smembers(key); + }; + + module.getSetsMembers = async function (keys) { + const batch = module.client.batch(); + keys.forEach(k => batch.smembers(String(k))); + return await helpers.execBatch(batch); + }; + + module.setCount = async function (key) { + return await module.client.scard(key); + }; + + module.setsCount = async function (keys) { + const batch = module.client.batch(); + keys.forEach(k => batch.scard(String(k))); + return await helpers.execBatch(batch); + }; + + module.setRemoveRandom = async function (key) { + return await module.client.spop(key); + }; + + return module; +}; diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js new file mode 100644 index 0000000000..a19fa9e8f3 --- /dev/null +++ b/src/database/redis/sorted.js @@ -0,0 +1,325 @@ +'use strict'; + +module.exports = function (module) { + const utils = require('../../utils'); + const helpers = require('./helpers'); + const dbHelpers = require('../helpers'); + + require('./sorted/add')(module); + require('./sorted/remove')(module); + require('./sorted/union')(module); + require('./sorted/intersect')(module); + + module.getSortedSetRange = async function (key, start, stop) { + return await sortedSetRange('zrange', key, start, stop, '-inf', '+inf', false); + }; + + module.getSortedSetRevRange = async function (key, start, stop) { + return await sortedSetRange('zrevrange', key, start, stop, '-inf', '+inf', false); + }; + + module.getSortedSetRangeWithScores = async function (key, start, stop) { + return await sortedSetRange('zrange', key, start, stop, '-inf', '+inf', true); + }; + + module.getSortedSetRevRangeWithScores = async function (key, start, stop) { + return await sortedSetRange('zrevrange', key, start, stop, '-inf', '+inf', true); + }; + + async function sortedSetRange(method, key, start, stop, min, max, withScores) { + if (Array.isArray(key)) { + if (!key.length) { + return []; + } + const batch = module.client.batch(); + key.forEach(key => batch[method](genParams(method, key, 0, stop, min, max, true))); + const data = await helpers.execBatch(batch); + + const batchData = data.map(setData => helpers.zsetToObjectArray(setData)); + + let objects = dbHelpers.mergeBatch(batchData, 0, stop, method === 'zrange' ? 1 : -1); + + if (start > 0) { + objects = objects.slice(start, stop !== -1 ? stop + 1 : undefined); + } + if (!withScores) { + objects = objects.map(item => item.value); + } + return objects; + } + + const params = genParams(method, key, start, stop, min, max, withScores); + const data = await module.client[method](params); + if (!withScores) { + return data; + } + const objects = helpers.zsetToObjectArray(data); + return objects; + } + + function genParams(method, key, start, stop, min, max, withScores) { + const params = { + zrevrange: [key, start, stop], + zrange: [key, start, stop], + zrangebyscore: [key, min, max], + zrevrangebyscore: [key, max, min], + }; + if (withScores) { + params[method].push('WITHSCORES'); + } + + if (method === 'zrangebyscore' || method === 'zrevrangebyscore') { + const count = stop !== -1 ? stop - start + 1 : stop; + params[method].push('LIMIT', start, count); + } + return params[method]; + } + + module.getSortedSetRangeByScore = async function (key, start, count, min, max) { + return await sortedSetRangeByScore('zrangebyscore', key, start, count, min, max, false); + }; + + module.getSortedSetRevRangeByScore = async function (key, start, count, max, min) { + return await sortedSetRangeByScore('zrevrangebyscore', key, start, count, min, max, false); + }; + + module.getSortedSetRangeByScoreWithScores = async function (key, start, count, min, max) { + return await sortedSetRangeByScore('zrangebyscore', key, start, count, min, max, true); + }; + + module.getSortedSetRevRangeByScoreWithScores = async function (key, start, count, max, min) { + return await sortedSetRangeByScore('zrevrangebyscore', key, start, count, min, max, true); + }; + + async function sortedSetRangeByScore(method, key, start, count, min, max, withScores) { + if (parseInt(count, 10) === 0) { + return []; + } + const stop = (parseInt(count, 10) === -1) ? -1 : (start + count - 1); + return await sortedSetRange(method, key, start, stop, min, max, withScores); + } + + module.sortedSetCount = async function (key, min, max) { + return await module.client.zcount(key, min, max); + }; + + module.sortedSetCard = async function (key) { + return await module.client.zcard(key); + }; + + module.sortedSetsCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const batch = module.client.batch(); + keys.forEach(k => batch.zcard(String(k))); + return await helpers.execBatch(batch); + }; + + module.sortedSetsCardSum = async function (keys) { + if (!keys || (Array.isArray(keys) && !keys.length)) { + return 0; + } + if (!Array.isArray(keys)) { + keys = [keys]; + } + const counts = await module.sortedSetsCard(keys); + const sum = counts.reduce((acc, val) => acc + val, 0); + return sum; + }; + + module.sortedSetRank = async function (key, value) { + return await module.client.zrank(key, value); + }; + + module.sortedSetRevRank = async function (key, value) { + return await module.client.zrevrank(key, value); + }; + + module.sortedSetsRanks = async function (keys, values) { + const batch = module.client.batch(); + for (let i = 0; i < values.length; i += 1) { + batch.zrank(keys[i], String(values[i])); + } + return await helpers.execBatch(batch); + }; + + module.sortedSetsRevRanks = async function (keys, values) { + const batch = module.client.batch(); + for (let i = 0; i < values.length; i += 1) { + batch.zrevrank(keys[i], String(values[i])); + } + return await helpers.execBatch(batch); + }; + + module.sortedSetRanks = async function (key, values) { + const batch = module.client.batch(); + for (let i = 0; i < values.length; i += 1) { + batch.zrank(key, String(values[i])); + } + return await helpers.execBatch(batch); + }; + + module.sortedSetRevRanks = async function (key, values) { + const batch = module.client.batch(); + for (let i = 0; i < values.length; i += 1) { + batch.zrevrank(key, String(values[i])); + } + return await helpers.execBatch(batch); + }; + + module.sortedSetScore = async function (key, value) { + if (!key || value === undefined) { + return null; + } + + const score = await module.client.zscore(key, value); + return score === null ? score : parseFloat(score); + }; + + module.sortedSetsScore = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const batch = module.client.batch(); + keys.forEach(key => batch.zscore(String(key), String(value))); + const scores = await helpers.execBatch(batch); + return scores.map(d => (d === null ? d : parseFloat(d))); + }; + + module.sortedSetScores = async function (key, values) { + if (!values.length) { + return []; + } + const batch = module.client.batch(); + values.forEach(value => batch.zscore(String(key), String(value))); + const scores = await helpers.execBatch(batch); + return scores.map(d => (d === null ? d : parseFloat(d))); + }; + + module.isSortedSetMember = async function (key, value) { + const score = await module.sortedSetScore(key, value); + return utils.isNumber(score); + }; + + module.isSortedSetMembers = async function (key, values) { + if (!values.length) { + return []; + } + const batch = module.client.batch(); + values.forEach(v => batch.zscore(key, String(v))); + const results = await helpers.execBatch(batch); + return results.map(utils.isNumber); + }; + + module.isMemberOfSortedSets = async function (keys, value) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const batch = module.client.batch(); + keys.forEach(k => batch.zscore(k, String(value))); + const results = await helpers.execBatch(batch); + return results.map(utils.isNumber); + }; + + module.getSortedSetMembers = async function (key) { + return await module.client.zrange(key, 0, -1); + }; + + module.getSortedSetsMembers = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return []; + } + const batch = module.client.batch(); + keys.forEach(k => batch.zrange(k, 0, -1)); + return await helpers.execBatch(batch); + }; + + module.sortedSetIncrBy = async function (key, increment, value) { + const newValue = await module.client.zincrby(key, increment, value); + return parseFloat(newValue); + }; + + module.sortedSetIncrByBulk = async function (data) { + const multi = module.client.multi(); + data.forEach((item) => { + multi.zincrby(item[0], item[1], item[2]); + }); + const result = await multi.exec(); + return result.map(item => item && parseFloat(item[1])); + }; + + module.getSortedSetRangeByLex = async function (key, min, max, start, count) { + return await sortedSetLex('zrangebylex', false, key, min, max, start, count); + }; + + module.getSortedSetRevRangeByLex = async function (key, max, min, start, count) { + return await sortedSetLex('zrevrangebylex', true, key, max, min, start, count); + }; + + module.sortedSetRemoveRangeByLex = async function (key, min, max) { + await sortedSetLex('zremrangebylex', false, key, min, max); + }; + + module.sortedSetLexCount = async function (key, min, max) { + return await sortedSetLex('zlexcount', false, key, min, max); + }; + + async function sortedSetLex(method, reverse, key, min, max, start, count) { + let minmin; + let maxmax; + if (reverse) { + minmin = '+'; + maxmax = '-'; + } else { + minmin = '-'; + maxmax = '+'; + } + + if (min !== minmin && !min.match(/^[[(]/)) { + min = `[${min}`; + } + if (max !== maxmax && !max.match(/^[[(]/)) { + max = `[${max}`; + } + const args = [key, min, max]; + if (count) { + args.push('LIMIT', start, count); + } + return await module.client[method](args); + } + + module.getSortedSetScan = async function (params) { + let cursor = '0'; + + const returnData = []; + let done = false; + const seen = {}; + do { + /* eslint-disable no-await-in-loop */ + const res = await module.client.zscan(params.key, cursor, 'MATCH', params.match, 'COUNT', 5000); + cursor = res[0]; + done = cursor === '0'; + const data = res[1]; + + for (let i = 0; i < data.length; i += 2) { + const value = data[i]; + if (!seen[value]) { + seen[value] = 1; + + if (params.withScores) { + returnData.push({ value: value, score: parseFloat(data[i + 1]) }); + } else { + returnData.push(value); + } + if (params.limit && returnData.length >= params.limit) { + done = true; + break; + } + } + } + } while (!done); + + return returnData; + }; +}; diff --git a/src/database/redis/sorted/add.js b/src/database/redis/sorted/add.js new file mode 100644 index 0000000000..e77f17e374 --- /dev/null +++ b/src/database/redis/sorted/add.js @@ -0,0 +1,76 @@ +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + const utils = require('../../../utils'); + + module.sortedSetAdd = async function (key, score, value) { + if (!key) { + return; + } + if (Array.isArray(score) && Array.isArray(value)) { + return await sortedSetAddMulti(key, score, value); + } + if (!utils.isNumber(score)) { + throw new Error(`[[error:invalid-score, ${score}]]`); + } + await module.client.zadd(key, score, String(value)); + }; + + async function sortedSetAddMulti(key, scores, values) { + if (!scores.length || !values.length) { + return; + } + + if (scores.length !== values.length) { + throw new Error('[[error:invalid-data]]'); + } + for (let i = 0; i < scores.length; i += 1) { + if (!utils.isNumber(scores[i])) { + throw new Error(`[[error:invalid-score, ${scores[i]}]]`); + } + } + const args = [key]; + for (let i = 0; i < scores.length; i += 1) { + args.push(scores[i], String(values[i])); + } + await module.client.zadd(args); + } + + module.sortedSetsAdd = async function (keys, scores, value) { + if (!Array.isArray(keys) || !keys.length) { + return; + } + const isArrayOfScores = Array.isArray(scores); + if ((!isArrayOfScores && !utils.isNumber(scores)) || + (isArrayOfScores && scores.map(s => utils.isNumber(s)).includes(false))) { + throw new Error(`[[error:invalid-score, ${scores}]]`); + } + + if (isArrayOfScores && scores.length !== keys.length) { + throw new Error('[[error:invalid-data]]'); + } + + const batch = module.client.batch(); + for (let i = 0; i < keys.length; i += 1) { + if (keys[i]) { + batch.zadd(keys[i], isArrayOfScores ? scores[i] : scores, String(value)); + } + } + await helpers.execBatch(batch); + }; + + module.sortedSetAddBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + const batch = module.client.batch(); + data.forEach((item) => { + if (!utils.isNumber(item[1])) { + throw new Error(`[[error:invalid-score, ${item[1]}]]`); + } + batch.zadd(item[0], item[1], item[2]); + }); + await helpers.execBatch(batch); + }; +}; diff --git a/src/database/redis/sorted/intersect.js b/src/database/redis/sorted/intersect.js new file mode 100644 index 0000000000..56757e6736 --- /dev/null +++ b/src/database/redis/sorted/intersect.js @@ -0,0 +1,66 @@ + +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + module.sortedSetIntersectCard = async function (keys) { + if (!Array.isArray(keys) || !keys.length) { + return 0; + } + const tempSetName = `temp_${Date.now()}`; + + const interParams = [tempSetName, keys.length].concat(keys); + + const multi = module.client.multi(); + multi.zinterstore(interParams); + multi.zcard(tempSetName); + multi.del(tempSetName); + const results = await helpers.execBatch(multi); + return results[1] || 0; + }; + + module.getSortedSetIntersect = async function (params) { + params.method = 'zrange'; + return await getSortedSetRevIntersect(params); + }; + + module.getSortedSetRevIntersect = async function (params) { + params.method = 'zrevrange'; + return await getSortedSetRevIntersect(params); + }; + + async function getSortedSetRevIntersect(params) { + const { sets } = params; + const start = params.hasOwnProperty('start') ? params.start : 0; + const stop = params.hasOwnProperty('stop') ? params.stop : -1; + const weights = params.weights || []; + + const tempSetName = `temp_${Date.now()}`; + + let interParams = [tempSetName, sets.length].concat(sets); + if (weights.length) { + interParams = interParams.concat(['WEIGHTS'].concat(weights)); + } + + if (params.aggregate) { + interParams = interParams.concat(['AGGREGATE', params.aggregate]); + } + + const rangeParams = [tempSetName, start, stop]; + if (params.withScores) { + rangeParams.push('WITHSCORES'); + } + + const multi = module.client.multi(); + multi.zinterstore(interParams); + multi[params.method](rangeParams); + multi.del(tempSetName); + let results = await helpers.execBatch(multi); + + if (!params.withScores) { + return results ? results[1] : null; + } + results = results[1] || []; + return helpers.zsetToObjectArray(results); + } +}; diff --git a/src/database/redis/sorted/remove.js b/src/database/redis/sorted/remove.js new file mode 100644 index 0000000000..232076ec7a --- /dev/null +++ b/src/database/redis/sorted/remove.js @@ -0,0 +1,46 @@ + +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + + module.sortedSetRemove = async function (key, value) { + if (!key) { + return; + } + const isValueArray = Array.isArray(value); + if (!value || (isValueArray && !value.length)) { + return; + } + if (!isValueArray) { + value = [value]; + } + + if (Array.isArray(key)) { + const batch = module.client.batch(); + key.forEach(k => batch.zrem(k, value)); + await helpers.execBatch(batch); + } else { + await module.client.zrem(key, value); + } + }; + + module.sortedSetsRemove = async function (keys, value) { + await module.sortedSetRemove(keys, value); + }; + + module.sortedSetsRemoveRangeByScore = async function (keys, min, max) { + const batch = module.client.batch(); + keys.forEach(k => batch.zremrangebyscore(k, min, max)); + await helpers.execBatch(batch); + }; + + module.sortedSetRemoveBulk = async function (data) { + if (!Array.isArray(data) || !data.length) { + return; + } + const batch = module.client.batch(); + data.forEach(item => batch.zrem(item[0], item[1])); + await helpers.execBatch(batch); + }; +}; diff --git a/src/database/redis/sorted/union.js b/src/database/redis/sorted/union.js new file mode 100644 index 0000000000..3db6425607 --- /dev/null +++ b/src/database/redis/sorted/union.js @@ -0,0 +1,52 @@ + +'use strict'; + +module.exports = function (module) { + const helpers = require('../helpers'); + module.sortedSetUnionCard = async function (keys) { + const tempSetName = `temp_${Date.now()}`; + if (!keys.length) { + return 0; + } + const multi = module.client.multi(); + multi.zunionstore([tempSetName, keys.length].concat(keys)); + multi.zcard(tempSetName); + multi.del(tempSetName); + const results = await helpers.execBatch(multi); + return Array.isArray(results) && results.length ? results[1] : 0; + }; + + module.getSortedSetUnion = async function (params) { + params.method = 'zrange'; + return await module.sortedSetUnion(params); + }; + + module.getSortedSetRevUnion = async function (params) { + params.method = 'zrevrange'; + return await module.sortedSetUnion(params); + }; + + module.sortedSetUnion = async function (params) { + if (!params.sets.length) { + return []; + } + + const tempSetName = `temp_${Date.now()}`; + + const rangeParams = [tempSetName, params.start, params.stop]; + if (params.withScores) { + rangeParams.push('WITHSCORES'); + } + + const multi = module.client.multi(); + multi.zunionstore([tempSetName, params.sets.length].concat(params.sets)); + multi[params.method](rangeParams); + multi.del(tempSetName); + let results = await helpers.execBatch(multi); + if (!params.withScores) { + return results ? results[1] : null; + } + results = results[1] || []; + return helpers.zsetToObjectArray(results); + }; +}; diff --git a/src/database/redis/transaction.js b/src/database/redis/transaction.js new file mode 100644 index 0000000000..1e98aac8c3 --- /dev/null +++ b/src/database/redis/transaction.js @@ -0,0 +1,8 @@ +'use strict'; + +module.exports = function (module) { + // TODO + module.transaction = function (perform, callback) { + perform(module.client, callback); + }; +}; diff --git a/src/emailer.js b/src/emailer.js new file mode 100644 index 0000000000..24f6deca21 --- /dev/null +++ b/src/emailer.js @@ -0,0 +1,368 @@ +'use strict'; + +const winston = require('winston'); +const nconf = require('nconf'); +const Benchpress = require('benchpressjs'); +const nodemailer = require('nodemailer'); +const wellKnownServices = require('nodemailer/lib/well-known/services'); +const { htmlToText } = require('html-to-text'); +const url = require('url'); +const path = require('path'); +const fs = require('fs'); +const _ = require('lodash'); +const jwt = require('jsonwebtoken'); + +const User = require('./user'); +const Plugins = require('./plugins'); +const meta = require('./meta'); +const translator = require('./translator'); +const pubsub = require('./pubsub'); +const file = require('./file'); + +const viewsDir = nconf.get('views_dir'); +const Emailer = module.exports; + +let prevConfig; +let app; + +Emailer.fallbackNotFound = false; + +Emailer.transports = { + sendmail: nodemailer.createTransport({ + sendmail: true, + newline: 'unix', + }), + smtp: undefined, +}; + +Emailer.listServices = () => Object.keys(wellKnownServices); +Emailer._defaultPayload = {}; + +const smtpSettingsChanged = (config) => { + const settings = [ + 'email:smtpTransport:enabled', + 'email:smtpTransport:pool', + 'email:smtpTransport:user', + 'email:smtpTransport:pass', + 'email:smtpTransport:service', + 'email:smtpTransport:port', + 'email:smtpTransport:host', + 'email:smtpTransport:security', + ]; + // config only has these properties if settings are saved on /admin/settings/email + return settings.some(key => config.hasOwnProperty(key) && config[key] !== prevConfig[key]); +}; + +const getHostname = () => { + const configUrl = nconf.get('url'); + const parsed = url.parse(configUrl); + return parsed.hostname; +}; + +const buildCustomTemplates = async (config) => { + try { + // If the new config contains any email override values, re-compile those templates + const toBuild = Object + .keys(config) + .filter(prop => prop.startsWith('email:custom:')) + .map(key => key.split(':')[2]); + + if (!toBuild.length) { + return; + } + + const [templates, allPaths] = await Promise.all([ + Emailer.getTemplates(config), + file.walk(viewsDir), + ]); + + const templatesToBuild = templates.filter(template => toBuild.includes(template.path)); + const paths = _.fromPairs(allPaths.map((p) => { + const relative = path.relative(viewsDir, p).replace(/\\/g, '/'); + return [relative, p]; + })); + + await Promise.all(templatesToBuild.map(async (template) => { + const source = await meta.templates.processImports(paths, template.path, template.text); + const compiled = await Benchpress.precompile(source, { filename: template.path }); + await fs.promises.writeFile(template.fullpath.replace(/\.tpl$/, '.js'), compiled); + })); + + Benchpress.flush(); + winston.verbose('[emailer] Built custom email templates'); + } catch (err) { + winston.error(`[emailer] Failed to build custom email templates\n${err.stack}`); + } +}; + +Emailer.getTemplates = async (config) => { + const emailsPath = path.join(viewsDir, 'emails'); + let emails = await file.walk(emailsPath); + emails = emails.filter(email => !email.endsWith('.js')); + + const templates = await Promise.all(emails.map(async (email) => { + const path = email.replace(emailsPath, '').slice(1).replace('.tpl', ''); + const original = await fs.promises.readFile(email, 'utf8'); + + return { + path: path, + fullpath: email, + text: config[`email:custom:${path}`] || original, + original: original, + isCustom: !!config[`email:custom:${path}`], + }; + })); + return templates; +}; + +Emailer.setupFallbackTransport = (config) => { + winston.verbose('[emailer] Setting up fallback transport'); + // Enable SMTP transport if enabled in ACP + if (parseInt(config['email:smtpTransport:enabled'], 10) === 1) { + const smtpOptions = { + name: getHostname(), + pool: config['email:smtpTransport:pool'], + }; + + if (config['email:smtpTransport:user'] || config['email:smtpTransport:pass']) { + smtpOptions.auth = { + user: config['email:smtpTransport:user'], + pass: config['email:smtpTransport:pass'], + }; + } + + if (config['email:smtpTransport:service'] === 'nodebb-custom-smtp') { + smtpOptions.port = config['email:smtpTransport:port']; + smtpOptions.host = config['email:smtpTransport:host']; + + if (config['email:smtpTransport:security'] === 'NONE') { + smtpOptions.secure = false; + smtpOptions.requireTLS = false; + smtpOptions.ignoreTLS = true; + } else if (config['email:smtpTransport:security'] === 'STARTTLS') { + smtpOptions.secure = false; + smtpOptions.requireTLS = true; + smtpOptions.ignoreTLS = false; + } else { + // meta.config['email:smtpTransport:security'] === 'ENCRYPTED' or undefined + smtpOptions.secure = true; + smtpOptions.requireTLS = true; + smtpOptions.ignoreTLS = false; + } + } else { + smtpOptions.service = String(config['email:smtpTransport:service']); + } + + Emailer.transports.smtp = nodemailer.createTransport(smtpOptions); + Emailer.fallbackTransport = Emailer.transports.smtp; + } else { + Emailer.fallbackTransport = Emailer.transports.sendmail; + } +}; + +Emailer.registerApp = (expressApp) => { + app = expressApp; + + let logo = null; + if (meta.config.hasOwnProperty('brand:emailLogo')) { + logo = (!meta.config['brand:emailLogo'].startsWith('http') ? nconf.get('url') : '') + meta.config['brand:emailLogo']; + } + + Emailer._defaultPayload = { + url: nconf.get('url'), + site_title: meta.config.title || 'NodeBB', + logo: { + src: logo, + height: meta.config['brand:emailLogo:height'], + width: meta.config['brand:emailLogo:width'], + }, + }; + + Emailer.setupFallbackTransport(meta.config); + buildCustomTemplates(meta.config); + + // need to shallow clone the config object + // otherwise prevConfig holds reference to meta.config object, + // which is updated before the pubsub handler is called + prevConfig = { ...meta.config }; + + pubsub.on('config:update', (config) => { + // config object only contains properties for the specific acp settings page + // not the entire meta.config object + if (config) { + // Update default payload if new logo is uploaded + if (config.hasOwnProperty('brand:emailLogo')) { + Emailer._defaultPayload.logo.src = config['brand:emailLogo']; + } + if (config.hasOwnProperty('brand:emailLogo:height')) { + Emailer._defaultPayload.logo.height = config['brand:emailLogo:height']; + } + if (config.hasOwnProperty('brand:emailLogo:width')) { + Emailer._defaultPayload.logo.width = config['brand:emailLogo:width']; + } + + if (smtpSettingsChanged(config)) { + Emailer.setupFallbackTransport(config); + } + buildCustomTemplates(config); + + prevConfig = { ...prevConfig, ...config }; + } + }); + + return Emailer; +}; + +Emailer.send = async (template, uid, params) => { + if (!app) { + throw Error('[emailer] App not ready!'); + } + + let userData = await User.getUserFields(uid, ['email', 'username', 'email:confirmed', 'banned']); + + // 'welcome' and 'verify-email' explicitly used passed-in email address + if (['welcome', 'verify-email'].includes(template)) { + userData.email = params.email; + } + + ({ template, userData, params } = await Plugins.hooks.fire('filter:email.prepare', { template, uid, userData, params })); + + if (!meta.config.sendEmailToBanned && template !== 'banned') { + if (userData.banned) { + winston.warn(`[emailer/send] User ${userData.username} (uid: ${uid}) is banned; not sending email due to system config.`); + return; + } + } + + if (!userData || !userData.email) { + if (process.env.NODE_ENV === 'development') { + winston.warn(`uid : ${uid} has no email, not sending "${template}" email.`); + } + return; + } + + const allowedTpls = ['verify-email', 'welcome', 'registration_accepted', 'reset', 'reset_notify']; + if (!meta.config.includeUnverifiedEmails && !userData['email:confirmed'] && !allowedTpls.includes(template)) { + if (process.env.NODE_ENV === 'development') { + winston.warn(`uid : ${uid} (${userData.email}) has not confirmed email, not sending "${template}" email.`); + } + return; + } + const userSettings = await User.getSettings(uid); + // Combined passed-in payload with default values + params = { ...Emailer._defaultPayload, ...params }; + params.uid = uid; + params.username = userData.username; + params.rtl = await translator.translate('[[language:dir]]', userSettings.userLang) === 'rtl'; + + const result = await Plugins.hooks.fire('filter:email.cancel', { + cancel: false, // set to true in plugin to cancel sending email + template: template, + params: params, + }); + + if (result.cancel) { + return; + } + await Emailer.sendToEmail(template, userData.email, userSettings.userLang, params); +}; + +Emailer.sendToEmail = async (template, email, language, params) => { + const lang = language || meta.config.defaultLang || 'en-GB'; + const unsubscribable = ['digest', 'notification']; + + // Digests and notifications can be one-click unsubbed + let payload = { + template: template, + uid: params.uid, + }; + + if (unsubscribable.includes(template)) { + if (template === 'notification') { + payload.type = params.notification.type; + } + payload = jwt.sign(payload, nconf.get('secret'), { + expiresIn: '30d', + }); + + const unsubUrl = [nconf.get('url'), 'email', 'unsubscribe', payload].join('/'); + params.headers = { + 'List-Id': `<${[template, params.uid, getHostname()].join('.')}>`, + 'List-Unsubscribe': `<${unsubUrl}>`, + 'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click', + ...params.headers, + }; + params.unsubUrl = unsubUrl; + } + + const result = await Plugins.hooks.fire('filter:email.params', { + template: template, + email: email, + language: lang, + params: params, + }); + + template = result.template; + email = result.email; + params = result.params; + + const [html, subject] = await Promise.all([ + Emailer.renderAndTranslate(template, params, result.language), + translator.translate(params.subject, result.language), + ]); + + const data = await Plugins.hooks.fire('filter:email.modify', { + _raw: params, + to: email, + from: meta.config['email:from'] || `no-reply@${getHostname()}`, + from_name: meta.config['email:from_name'] || 'NodeBB', + subject: `[${meta.config.title}] ${_.unescape(subject)}`, + html: html, + plaintext: htmlToText(html, { + tags: { img: { format: 'skip' } }, + }), + template: template, + uid: params.uid, + pid: params.pid, + fromUid: params.fromUid, + headers: params.headers, + rtl: params.rtl, + }); + const usingFallback = !Plugins.hooks.hasListeners('filter:email.send') && + !Plugins.hooks.hasListeners('static:email.send'); + try { + if (Plugins.hooks.hasListeners('filter:email.send')) { + // Deprecated, remove in v1.19.0 + await Plugins.hooks.fire('filter:email.send', data); + } else if (Plugins.hooks.hasListeners('static:email.send')) { + await Plugins.hooks.fire('static:email.send', data); + } else { + await Emailer.sendViaFallback(data); + } + } catch (err) { + if (err.code === 'ENOENT' && usingFallback) { + Emailer.fallbackNotFound = true; + throw new Error('[[error:sendmail-not-found]]'); + } else { + throw err; + } + } +}; + +Emailer.sendViaFallback = async (data) => { + // Some minor alterations to the data to conform to nodemailer standard + data.text = data.plaintext; + delete data.plaintext; + + // NodeMailer uses a combined "from" + data.from = `${data.from_name}<${data.from}>`; + delete data.from_name; + await Emailer.fallbackTransport.sendMail(data); +}; + +Emailer.renderAndTranslate = async (template, params, lang) => { + const html = await app.renderAsync(`emails/${template}`, params); + return await translator.translate(html, lang); +}; + +require('./promisify')(Emailer, ['transports']); diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000000..c53af4abfe --- /dev/null +++ b/src/events.js @@ -0,0 +1,174 @@ + +'use strict'; + +const validator = require('validator'); +const _ = require('lodash'); + +const db = require('./database'); +const batch = require('./batch'); +const user = require('./user'); +const utils = require('./utils'); +const plugins = require('./plugins'); + +const events = module.exports; + +events.types = [ + 'plugin-activate', + 'plugin-deactivate', + 'plugin-install', + 'plugin-uninstall', + 'restart', + 'build', + 'config-change', + 'settings-change', + 'category-purge', + 'privilege-change', + 'post-delete', + 'post-restore', + 'post-purge', + 'post-edit', + 'post-move', + 'post-change-owner', + 'post-queue-reply-accept', + 'post-queue-topic-accept', + 'post-queue-reply-reject', + 'post-queue-topic-reject', + 'topic-delete', + 'topic-restore', + 'topic-purge', + 'topic-rename', + 'topic-merge', + 'topic-fork', + 'topic-move', + 'topic-move-all', + 'password-reset', + 'user-makeAdmin', + 'user-removeAdmin', + 'user-ban', + 'user-unban', + 'user-mute', + 'user-unmute', + 'user-delete', + 'user-deleteAccount', + 'user-deleteContent', + 'password-change', + 'email-confirmation-sent', + 'email-change', + 'username-change', + 'ip-blacklist-save', + 'ip-blacklist-addRule', + 'registration-approved', + 'registration-rejected', + 'group-join', + 'group-request-membership', + 'group-add-member', + 'group-leave', + 'group-owner-grant', + 'group-owner-rescind', + 'group-accept-membership', + 'group-reject-membership', + 'group-invite', + 'group-invite-accept', + 'group-invite-reject', + 'group-kick', + 'theme-set', + 'export:uploads', + 'account-locked', + 'getUsersCSV', + // To add new types from plugins, just Array.push() to this array +]; + +/** + * Useful options in data: type, uid, ip, targetUid + * Everything else gets stringified and shown as pretty JSON string + */ +events.log = async function (data) { + const eid = await db.incrObjectField('global', 'nextEid'); + data.timestamp = Date.now(); + data.eid = eid; + + await Promise.all([ + db.sortedSetsAdd([ + 'events:time', + `events:time:${data.type}`, + ], data.timestamp, eid), + db.setObject(`event:${eid}`, data), + ]); + plugins.hooks.fire('action:events.log', { data: data }); +}; + +events.getEvents = async function (filter, start, stop, from, to) { + // from/to optional + if (from === undefined) { + from = 0; + } + if (to === undefined) { + to = Date.now(); + } + + const eids = await db.getSortedSetRevRangeByScore(`events:time${filter ? `:${filter}` : ''}`, start, stop - start + 1, to, from); + let eventsData = await db.getObjects(eids.map(eid => `event:${eid}`)); + eventsData = eventsData.filter(Boolean); + await addUserData(eventsData, 'uid', 'user'); + await addUserData(eventsData, 'targetUid', 'targetUser'); + eventsData.forEach((event) => { + Object.keys(event).forEach((key) => { + if (typeof event[key] === 'string') { + event[key] = validator.escape(String(event[key] || '')); + } + }); + const e = utils.merge(event); + e.eid = undefined; + e.uid = undefined; + e.type = undefined; + e.ip = undefined; + e.user = undefined; + event.jsonString = JSON.stringify(e, null, 4); + event.timestampISO = new Date(parseInt(event.timestamp, 10)).toUTCString(); + }); + return eventsData; +}; + +async function addUserData(eventsData, field, objectName) { + const uids = _.uniq(eventsData.map(event => event && event[field])); + + if (!uids.length) { + return eventsData; + } + + const [isAdmin, userData] = await Promise.all([ + user.isAdministrator(uids), + user.getUsersFields(uids, ['username', 'userslug', 'picture']), + ]); + + const map = {}; + userData.forEach((user, index) => { + user.isAdmin = isAdmin[index]; + map[user.uid] = user; + }); + + eventsData.forEach((event) => { + if (map[event[field]]) { + event[objectName] = map[event[field]]; + } + }); + return eventsData; +} + +events.deleteEvents = async function (eids) { + const keys = eids.map(eid => `event:${eid}`); + const eventData = await db.getObjectsFields(keys, ['type']); + const sets = _.uniq(['events:time'].concat(eventData.map(e => `events:time:${e.type}`))); + await Promise.all([ + db.deleteAll(keys), + db.sortedSetRemove(sets, eids), + ]); +}; + +events.deleteAll = async function () { + await batch.processSortedSet('events:time', async (eids) => { + await events.deleteEvents(eids); + }, { alwaysStartAt: 0, batch: 500 }); +}; + +require('./promisify')(events); diff --git a/src/file.js b/src/file.js new file mode 100644 index 0000000000..def9e8fb89 --- /dev/null +++ b/src/file.js @@ -0,0 +1,158 @@ +'use strict'; + +const fs = require('fs'); +const nconf = require('nconf'); +const path = require('path'); +const winston = require('winston'); +const mkdirp = require('mkdirp'); +const mime = require('mime'); +const graceful = require('graceful-fs'); + +const slugify = require('./slugify'); + +graceful.gracefulify(fs); + +const file = module.exports; + +file.saveFileToLocal = async function (filename, folder, tempPath) { + /* + * remarkable doesn't allow spaces in hyperlinks, once that's fixed, remove this. + */ + filename = filename.split('.').map(name => slugify(name)).join('.'); + + const uploadPath = path.join(nconf.get('upload_path'), folder, filename); + if (!uploadPath.startsWith(nconf.get('upload_path'))) { + throw new Error('[[error:invalid-path]]'); + } + + winston.verbose(`Saving file ${filename} to : ${uploadPath}`); + await mkdirp(path.dirname(uploadPath)); + await fs.promises.copyFile(tempPath, uploadPath); + return { + url: `/assets/uploads/${folder ? `${folder}/` : ''}${filename}`, + path: uploadPath, + }; +}; + +file.base64ToLocal = async function (imageData, uploadPath) { + const buffer = Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64'); + uploadPath = path.join(nconf.get('upload_path'), uploadPath); + + await fs.promises.writeFile(uploadPath, buffer, { + encoding: 'base64', + }); + return uploadPath; +}; + +// https://stackoverflow.com/a/31205878/583363 +file.appendToFileName = function (filename, string) { + const dotIndex = filename.lastIndexOf('.'); + if (dotIndex === -1) { + return filename + string; + } + return filename.substring(0, dotIndex) + string + filename.substring(dotIndex); +}; + +file.allowedExtensions = function () { + const meta = require('./meta'); + let allowedExtensions = (meta.config.allowedFileExtensions || '').trim(); + if (!allowedExtensions) { + return []; + } + allowedExtensions = allowedExtensions.split(','); + allowedExtensions = allowedExtensions.filter(Boolean).map((extension) => { + extension = extension.trim(); + if (!extension.startsWith('.')) { + extension = `.${extension}`; + } + return extension.toLowerCase(); + }); + + if (allowedExtensions.includes('.jpg') && !allowedExtensions.includes('.jpeg')) { + allowedExtensions.push('.jpeg'); + } + + return allowedExtensions; +}; + +file.exists = async function (path) { + try { + await fs.promises.stat(path); + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + return true; +}; + +file.existsSync = function (path) { + try { + fs.statSync(path); + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + throw err; + } + + return true; +}; + +file.delete = async function (path) { + if (!path) { + return; + } + try { + await fs.promises.unlink(path); + } catch (err) { + if (err.code === 'ENOENT') { + winston.verbose(`[file] Attempted to delete non-existent file: ${path}`); + return; + } + + winston.warn(err); + } +}; + +file.link = async function link(filePath, destPath, relative) { + if (relative && process.platform !== 'win32') { + filePath = path.relative(path.dirname(destPath), filePath); + } + + if (process.platform === 'win32') { + await fs.promises.link(filePath, destPath); + } else { + await fs.promises.symlink(filePath, destPath, 'file'); + } +}; + +file.linkDirs = async function linkDirs(sourceDir, destDir, relative) { + if (relative && process.platform !== 'win32') { + sourceDir = path.relative(path.dirname(destDir), sourceDir); + } + + const type = (process.platform === 'win32') ? 'junction' : 'dir'; + await fs.promises.symlink(sourceDir, destDir, type); +}; + +file.typeToExtension = function (type) { + let extension = ''; + if (type) { + extension = `.${mime.getExtension(type)}`; + } + return extension; +}; + +// Adapted from http://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search +file.walk = async function (dir) { + const subdirs = await fs.promises.readdir(dir); + const files = await Promise.all(subdirs.map(async (subdir) => { + const res = path.resolve(dir, subdir); + return (await fs.promises.stat(res)).isDirectory() ? file.walk(res) : res; + })); + return files.reduce((a, f) => a.concat(f), []); +}; + +require('./promisify')(file); diff --git a/src/flags.js b/src/flags.js new file mode 100644 index 0000000000..2930c5c150 --- /dev/null +++ b/src/flags.js @@ -0,0 +1,956 @@ +'use strict'; + +const _ = require('lodash'); +const winston = require('winston'); +const validator = require('validator'); + +const db = require('./database'); +const user = require('./user'); +const groups = require('./groups'); +const meta = require('./meta'); +const notifications = require('./notifications'); +const analytics = require('./analytics'); +const categories = require('./categories'); +const topics = require('./topics'); +const posts = require('./posts'); +const privileges = require('./privileges'); +const plugins = require('./plugins'); +const utils = require('./utils'); +const batch = require('./batch'); + +const Flags = module.exports; + +Flags._states = new Map([ + ['open', { + label: '[[flags:state-open]]', + class: 'danger', + }], + ['wip', { + label: '[[flags:state-wip]]', + class: 'warning', + }], + ['resolved', { + label: '[[flags:state-resolved]]', + class: 'success', + }], + ['rejected', { + label: '[[flags:state-rejected]]', + class: 'secondary', + }], +]); + +Flags.init = async function () { + // Query plugins for custom filter strategies and merge into core filter strategies + function prepareSets(sets, orSets, prefix, value) { + if (!Array.isArray(value)) { + sets.push(prefix + value); + } else if (value.length) { + if (value.length === 1) { + sets.push(prefix + value[0]); + } else { + orSets.push(value.map(x => prefix + x)); + } + } + } + + const hookData = { + filters: { + type: function (sets, orSets, key) { + prepareSets(sets, orSets, 'flags:byType:', key); + }, + state: function (sets, orSets, key) { + prepareSets(sets, orSets, 'flags:byState:', key); + }, + reporterId: function (sets, orSets, key) { + prepareSets(sets, orSets, 'flags:byReporter:', key); + }, + assignee: function (sets, orSets, key) { + prepareSets(sets, orSets, 'flags:byAssignee:', key); + }, + targetUid: function (sets, orSets, key) { + prepareSets(sets, orSets, 'flags:byTargetUid:', key); + }, + cid: function (sets, orSets, key) { + prepareSets(sets, orSets, 'flags:byCid:', key); + }, + page: function () { /* noop */ }, + perPage: function () { /* noop */ }, + quick: function (sets, orSets, key, uid) { + switch (key) { + case 'mine': + sets.push(`flags:byAssignee:${uid}`); + break; + + case 'unresolved': + prepareSets(sets, orSets, 'flags:byState:', ['open', 'wip']); + break; + } + }, + }, + states: Flags._states, + helpers: { + prepareSets: prepareSets, + }, + }; + + try { + ({ filters: Flags._filters } = await plugins.hooks.fire('filter:flags.getFilters', hookData)); + ({ filters: Flags._filters, states: Flags._states } = await plugins.hooks.fire('filter:flags.init', hookData)); + } catch (err) { + winston.error(`[flags/init] Could not retrieve filters\n${err.stack}`); + Flags._filters = {}; + } +}; + +Flags.get = async function (flagId) { + const [base, notes, reports] = await Promise.all([ + db.getObject(`flag:${flagId}`), + Flags.getNotes(flagId), + Flags.getReports(flagId), + ]); + if (!base) { + return; + } + const flagObj = { + state: 'open', + assignee: null, + ...base, + datetimeISO: utils.toISOString(base.datetime), + target_readable: `${base.type.charAt(0).toUpperCase() + base.type.slice(1)} ${base.targetId}`, + target: await Flags.getTarget(base.type, base.targetId, 0), + notes, + reports, + }; + + const data = await plugins.hooks.fire('filter:flags.get', { + flag: flagObj, + }); + return data.flag; +}; + +Flags.getCount = async function ({ uid, filters, query }) { + filters = filters || {}; + const flagIds = await Flags.getFlagIdsWithFilters({ filters, uid, query }); + return flagIds.length; +}; + +Flags.getFlagIdsWithFilters = async function ({ filters, uid, query }) { + let sets = []; + const orSets = []; + + // Default filter + filters.page = filters.hasOwnProperty('page') ? Math.abs(parseInt(filters.page, 10) || 1) : 1; + filters.perPage = filters.hasOwnProperty('perPage') ? Math.abs(parseInt(filters.perPage, 10) || 20) : 20; + + for (const type of Object.keys(filters)) { + if (Flags._filters.hasOwnProperty(type)) { + Flags._filters[type](sets, orSets, filters[type], uid); + } else { + winston.warn(`[flags/list] No flag filter type found: ${type}`); + } + } + sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default + + let flagIds = []; + if (sets.length === 1) { + flagIds = await db.getSortedSetRevRange(sets[0], 0, -1); + } else if (sets.length > 1) { + flagIds = await db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }); + } + + if (orSets.length) { + let _flagIds = await Promise.all(orSets.map(async orSet => await db.getSortedSetRevUnion({ sets: orSet, start: 0, stop: -1, aggregate: 'MAX' }))); + + // Each individual orSet is ANDed together to construct the final list of flagIds + _flagIds = _.intersection(..._flagIds); + + // Merge with flagIds returned by sets + if (sets.length) { + // If flag ids are already present, return a subset of flags that are in both sets + flagIds = _.intersection(flagIds, _flagIds); + } else { + // Otherwise, return all flags returned via orSets + flagIds = _.union(flagIds, _flagIds); + } + } + + const result = await plugins.hooks.fire('filter:flags.getFlagIdsWithFilters', { + filters, + uid, + query, + flagIds, + }); + return result.flagIds; +}; + +Flags.list = async function (data) { + const filters = data.filters || {}; + let flagIds = await Flags.getFlagIdsWithFilters({ + filters, + uid: data.uid, + query: data.query, + }); + flagIds = await Flags.sort(flagIds, data.sort); + + // Create subset for parsing based on page number (n=20) + const flagsPerPage = Math.abs(parseInt(filters.perPage, 10) || 1); + const pageCount = Math.ceil(flagIds.length / flagsPerPage); + flagIds = flagIds.slice((filters.page - 1) * flagsPerPage, filters.page * flagsPerPage); + + const reportCounts = await db.sortedSetsCard(flagIds.map(flagId => `flag:${flagId}:reports`)); + + const flags = await Promise.all(flagIds.map(async (flagId, idx) => { + let flagObj = await db.getObject(`flag:${flagId}`); + flagObj = { + state: 'open', + assignee: null, + heat: reportCounts[idx], + ...flagObj, + }; + flagObj.labelClass = Flags._states.get(flagObj.state).class; + + return Object.assign(flagObj, { + target_readable: `${flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1)} ${flagObj.targetId}`, + datetimeISO: utils.toISOString(flagObj.datetime), + }); + })); + + const payload = await plugins.hooks.fire('filter:flags.list', { + flags: flags, + page: filters.page, + uid: data.uid, + }); + + return { + flags: payload.flags, + page: payload.page, + pageCount: pageCount, + }; +}; + +Flags.sort = async function (flagIds, sort) { + const filterPosts = async (flagIds) => { + const keys = flagIds.map(id => `flag:${id}`); + const types = await db.getObjectsFields(keys, ['type']); + return flagIds.filter((id, idx) => types[idx].type === 'post'); + }; + + switch (sort) { + // 'newest' is not handled because that is default + case 'oldest': + flagIds = flagIds.reverse(); + break; + + case 'reports': { + const keys = flagIds.map(id => `flag:${id}:reports`); + const heat = await db.sortedSetsCard(keys); + const mapped = heat.map((el, i) => ({ + index: i, heat: el, + })); + mapped.sort((a, b) => b.heat - a.heat); + flagIds = mapped.map(obj => flagIds[obj.index]); + break; + } + + case 'upvotes': // fall-through + case 'downvotes': + case 'replies': { + flagIds = await filterPosts(flagIds); + const keys = flagIds.map(id => `flag:${id}`); + const pids = (await db.getObjectsFields(keys, ['targetId'])).map(obj => obj.targetId); + const votes = (await posts.getPostsFields(pids, [sort])).map(obj => parseInt(obj[sort], 10) || 0); + const sortRef = flagIds.reduce((memo, cur, idx) => { + memo[cur] = votes[idx]; + return memo; + }, {}); + + flagIds = flagIds.sort((a, b) => sortRef[b] - sortRef[a]); + } + } + + return flagIds; +}; + +Flags.validate = async function (payload) { + const [target, reporter] = await Promise.all([ + Flags.getTarget(payload.type, payload.id, payload.uid), + user.getUserData(payload.uid), + ]); + + if (!target) { + throw new Error('[[error:invalid-data]]'); + } else if (target.deleted) { + throw new Error('[[error:post-deleted]]'); + } else if (!reporter || !reporter.userslug) { + throw new Error('[[error:no-user]]'); + } else if (reporter.banned) { + throw new Error('[[error:user-banned]]'); + } + + // Disallow flagging of profiles/content of privileged users + const [targetPrivileged, reporterPrivileged] = await Promise.all([ + user.isPrivileged(target.uid), + user.isPrivileged(reporter.uid), + ]); + if (targetPrivileged && !reporterPrivileged) { + throw new Error('[[error:cant-flag-privileged]]'); + } + + if (payload.type === 'post') { + const editable = await privileges.posts.canEdit(payload.id, payload.uid); + if (!editable.flag && !meta.config['reputation:disabled'] && reporter.reputation < meta.config['min:rep:flag']) { + throw new Error(`[[error:not-enough-reputation-to-flag, ${meta.config['min:rep:flag']}]]`); + } + } else if (payload.type === 'user') { + if (parseInt(payload.id, 10) === parseInt(payload.uid, 10)) { + throw new Error('[[error:cant-flag-self]]'); + } + const editable = await privileges.users.canEdit(payload.uid, payload.id); + if (!editable && !meta.config['reputation:disabled'] && reporter.reputation < meta.config['min:rep:flag']) { + throw new Error(`[[error:not-enough-reputation-to-flag, ${meta.config['min:rep:flag']}]]`); + } + } else { + throw new Error('[[error:invalid-data]]'); + } +}; + +Flags.getNotes = async function (flagId) { + let notes = await db.getSortedSetRevRangeWithScores(`flag:${flagId}:notes`, 0, -1); + notes = await modifyNotes(notes); + return notes; +}; + +Flags.getNote = async function (flagId, datetime) { + datetime = parseInt(datetime, 10); + if (isNaN(datetime)) { + throw new Error('[[error:invalid-data]]'); + } + + let notes = await db.getSortedSetRangeByScoreWithScores(`flag:${flagId}:notes`, 0, 1, datetime, datetime); + if (!notes.length) { + throw new Error('[[error:invalid-data]]'); + } + + notes = await modifyNotes(notes); + return notes[0]; +}; + +Flags.getFlagIdByTarget = async function (type, id) { + let method; + switch (type) { + case 'post': + method = posts.getPostField; + break; + + case 'user': + method = user.getUserField; + break; + + default: + throw new Error('[[error:invalid-data]]'); + } + + return await method(id, 'flagId'); +}; + +async function modifyNotes(notes) { + const uids = []; + notes = notes.map((note) => { + const noteObj = JSON.parse(note.value); + uids.push(noteObj[0]); + return { + uid: noteObj[0], + content: noteObj[1], + datetime: note.score, + datetimeISO: utils.toISOString(note.score), + }; + }); + const userData = await user.getUsersFields(uids, ['username', 'userslug', 'picture']); + return notes.map((note, idx) => { + note.user = userData[idx]; + note.content = validator.escape(note.content); + return note; + }); +} + +Flags.deleteNote = async function (flagId, datetime) { + datetime = parseInt(datetime, 10); + if (isNaN(datetime)) { + throw new Error('[[error:invalid-data]]'); + } + + const note = await db.getSortedSetRangeByScore(`flag:${flagId}:notes`, 0, 1, datetime, datetime); + if (!note.length) { + throw new Error('[[error:invalid-data]]'); + } + + await db.sortedSetRemove(`flag:${flagId}:notes`, note[0]); +}; + +Flags.create = async function (type, id, uid, reason, timestamp, forceFlag = false) { + let doHistoryAppend = false; + if (!timestamp) { + timestamp = Date.now(); + doHistoryAppend = true; + } + const [flagExists, targetExists,, targetFlagged, targetUid, targetCid] = await Promise.all([ + // Sanity checks + Flags.exists(type, id, uid), + Flags.targetExists(type, id), + Flags.canFlag(type, id, uid, forceFlag), + Flags.targetFlagged(type, id), + + // Extra data for zset insertion + Flags.getTargetUid(type, id), + Flags.getTargetCid(type, id), + ]); + if (!forceFlag && flagExists) { + throw new Error(`[[error:${type}-already-flagged]]`); + } else if (!targetExists) { + throw new Error('[[error:invalid-data]]'); + } + + // If the flag already exists, just add the report + if (targetFlagged) { + const flagId = await Flags.getFlagIdByTarget(type, id); + await Promise.all([ + Flags.addReport(flagId, type, id, uid, reason, timestamp), + Flags.update(flagId, uid, { state: 'open' }), + ]); + + return await Flags.get(flagId); + } + + const flagId = await db.incrObjectField('global', 'nextFlagId'); + const batched = []; + + batched.push( + db.setObject(`flag:${flagId}`, { + flagId: flagId, + type: type, + targetId: id, + targetUid: targetUid, + datetime: timestamp, + }), + Flags.addReport(flagId, type, id, uid, reason, timestamp), + db.sortedSetAdd('flags:datetime', timestamp, flagId), // by time, the default + db.sortedSetAdd(`flags:byType:${type}`, timestamp, flagId), // by flag type + db.sortedSetIncrBy('flags:byTarget', 1, [type, id].join(':')), // by flag target (score is count) + analytics.increment('flags') // some fancy analytics + ); + + if (targetUid) { + batched.push(db.sortedSetAdd(`flags:byTargetUid:${targetUid}`, timestamp, flagId)); // by target uid + } + + if (targetCid) { + batched.push(db.sortedSetAdd(`flags:byCid:${targetCid}`, timestamp, flagId)); // by target cid + } + + if (type === 'post') { + batched.push( + db.sortedSetAdd(`flags:byPid:${id}`, timestamp, flagId), // by target pid + posts.setPostField(id, 'flagId', flagId) + ); + + if (targetUid && parseInt(targetUid, 10) !== parseInt(uid, 10)) { + batched.push(user.incrementUserFlagsBy(targetUid, 1)); + } + } else if (type === 'user') { + batched.push(user.setUserField(id, 'flagId', flagId)); + } + + // Run all the database calls in one single batched call... + await Promise.all(batched); + + if (doHistoryAppend) { + await Flags.update(flagId, uid, { state: 'open' }); + } + + const flagObj = await Flags.get(flagId); + + plugins.hooks.fire('action:flags.create', { flag: flagObj }); + return flagObj; +}; + +Flags.purge = async function (flagIds) { + const flagData = (await db.getObjects(flagIds.map(flagId => `flag:${flagId}`))).filter(Boolean); + const postFlags = flagData.filter(flagObj => flagObj.type === 'post'); + const userFlags = flagData.filter(flagObj => flagObj.type === 'user'); + const assignedFlags = flagData.filter(flagObj => !!flagObj.assignee); + + const [allReports, cids] = await Promise.all([ + db.getSortedSetsMembers(flagData.map(flagObj => `flag:${flagObj.flagId}:reports`)), + categories.getAllCidsFromSet('categories:cid'), + ]); + const allReporterUids = allReports.map(flagReports => flagReports.map(report => report && report.split(';')[0])); + const removeReporters = []; + flagData.forEach((flagObj, i) => { + if (Array.isArray(allReporterUids[i])) { + allReporterUids[i].forEach((uid) => { + removeReporters.push([`flags:hash`, [flagObj.type, flagObj.targetId, uid].join(':')]); + removeReporters.push([`flags:byReporter:${uid}`, flagObj.flagId]); + }); + } + }); + await Promise.all([ + db.sortedSetRemoveBulk([ + ...flagData.map(flagObj => ([`flags:byType:${flagObj.type}`, flagObj.flagId])), + ...flagData.map(flagObj => ([`flags:byState:${flagObj.state}`, flagObj.flagId])), + ...removeReporters, + ...postFlags.map(flagObj => ([`flags:byPid:${flagObj.targetId}`, flagObj.flagId])), + ...assignedFlags.map(flagObj => ([`flags:byAssignee:${flagObj.assignee}`, flagObj.flagId])), + ...userFlags.map(flagObj => ([`flags:byTargetUid:${flagObj.targetUid}`, flagObj.flagId])), + ]), + db.deleteObjectFields(postFlags.map(flagObj => `post:${flagObj.targetId}`, ['flagId'])), + db.deleteObjectFields(userFlags.map(flagObj => `user:${flagObj.targetId}`, ['flagId'])), + db.deleteAll([ + ...flagIds.map(flagId => `flag:${flagId}`), + ...flagIds.map(flagId => `flag:${flagId}:notes`), + ...flagIds.map(flagId => `flag:${flagId}:reports`), + ...flagIds.map(flagId => `flag:${flagId}:history`), + ]), + db.sortedSetRemove(cids.map(cid => `flags:byCid:${cid}`), flagIds), + db.sortedSetRemove('flags:datetime', flagIds), + db.sortedSetRemove( + 'flags:byTarget', + flagData.map(flagObj => [flagObj.type, flagObj.targetId].join(':')) + ), + ]); +}; + +Flags.getReports = async function (flagId) { + const payload = await db.getSortedSetRevRangeWithScores(`flag:${flagId}:reports`, 0, -1); + const [reports, uids] = payload.reduce((memo, cur) => { + const value = cur.value.split(';'); + memo[1].push(value.shift()); + cur.value = validator.escape(String(value.join(';'))); + memo[0].push(cur); + + return memo; + }, [[], []]); + + await Promise.all(reports.map(async (report, idx) => { + report.timestamp = report.score; + report.timestampISO = new Date(report.score).toISOString(); + delete report.score; + report.reporter = await user.getUserFields(uids[idx], ['username', 'userslug', 'picture', 'reputation']); + })); + + return reports; +}; + +Flags.addReport = async function (flagId, type, id, uid, reason, timestamp) { + await db.sortedSetAddBulk([ + [`flags:byReporter:${uid}`, timestamp, flagId], + [`flag:${flagId}:reports`, timestamp, [uid, reason].join(';')], + + ['flags:hash', flagId, [type, id, uid].join(':')], + ]); + + plugins.hooks.fire('action:flags.addReport', { flagId, type, id, uid, reason, timestamp }); +}; + +Flags.exists = async function (type, id, uid) { + return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':')); +}; + +Flags.canView = async (flagId, uid) => { + const exists = await db.isSortedSetMember('flags:datetime', flagId); + if (!exists) { + return false; + } + + const [{ type, targetId }, isAdminOrGlobalMod] = await Promise.all([ + db.getObject(`flag:${flagId}`), + user.isAdminOrGlobalMod(uid), + ]); + + if (type === 'post') { + const cid = await Flags.getTargetCid(type, targetId); + const isModerator = await user.isModerator(uid, cid); + + return isAdminOrGlobalMod || isModerator; + } + + return isAdminOrGlobalMod; +}; + +Flags.canFlag = async function (type, id, uid, skipLimitCheck = false) { + const limit = meta.config['flags:limitPerTarget']; + if (!skipLimitCheck && limit > 0) { + const score = await db.sortedSetScore('flags:byTarget', `${type}:${id}`); + if (score >= limit) { + throw new Error(`[[error:${type}-flagged-too-many-times]]`); + } + } + + const canRead = await privileges.posts.can('topics:read', id, uid); + switch (type) { + case 'user': + return true; + + case 'post': + if (!canRead) { + throw new Error('[[error:no-privileges]]'); + } + break; + + default: + throw new Error('[[error:invalid-data]]'); + } +}; + +Flags.getTarget = async function (type, id, uid) { + if (type === 'user') { + const userData = await user.getUserData(id); + return userData && userData.uid ? userData : {}; + } + if (type === 'post') { + let postData = await posts.getPostData(id); + if (!postData) { + return {}; + } + postData = await posts.parsePost(postData); + postData = await topics.addPostData([postData], uid); + return postData[0]; + } + throw new Error('[[error:invalid-data]]'); +}; + +Flags.targetExists = async function (type, id) { + if (type === 'post') { + return await posts.exists(id); + } else if (type === 'user') { + return await user.exists(id); + } + throw new Error('[[error:invalid-data]]'); +}; + +Flags.targetFlagged = async function (type, id) { + return await db.sortedSetScore('flags:byTarget', [type, id].join(':')) >= 1; +}; + +Flags.getTargetUid = async function (type, id) { + if (type === 'post') { + return await posts.getPostField(id, 'uid'); + } + return id; +}; + +Flags.getTargetCid = async function (type, id) { + if (type === 'post') { + return await posts.getCidByPid(id); + } + return null; +}; + +Flags.update = async function (flagId, uid, changeset) { + const current = await db.getObjectFields(`flag:${flagId}`, ['uid', 'state', 'assignee', 'type', 'targetId']); + if (!current.type) { + return; + } + const now = changeset.datetime || Date.now(); + const notifyAssignee = async function (assigneeId) { + if (assigneeId === '' || parseInt(uid, 10) === parseInt(assigneeId, 10)) { + return; + } + const notifObj = await notifications.create({ + type: 'my-flags', + bodyShort: `[[notifications:flag_assigned_to_you, ${flagId}]]`, + bodyLong: '', + path: `/flags/${flagId}`, + nid: `flags:assign:${flagId}:uid:${assigneeId}`, + from: uid, + }); + await notifications.push(notifObj, [assigneeId]); + }; + const isAssignable = async function (assigneeId) { + let allowed = false; + allowed = await user.isAdminOrGlobalMod(assigneeId); + + // Mods are also allowed to be assigned, if flag target is post in uid's moderated cid + 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 = []; + for (const prop of Object.keys(changeset)) { + if (current[prop] === changeset[prop]) { + delete changeset[prop]; + } else if (prop === 'state') { + if (!Flags._states.has(changeset[prop])) { + delete changeset[prop]; + } else { + tasks.push(db.sortedSetAdd(`flags:byState:${changeset[prop]}`, now, flagId)); + tasks.push(db.sortedSetRemove(`flags:byState:${current[prop]}`, flagId)); + if (changeset[prop] === 'resolved' && meta.config['flags:actionOnResolve'] === 'rescind') { + tasks.push(notifications.rescind(`flag:${current.type}:${current.targetId}`)); + } + if (changeset[prop] === 'rejected' && meta.config['flags:actionOnReject'] === 'rescind') { + tasks.push(notifications.rescind(`flag:${current.type}:${current.targetId}`)); + } + } + } else if (prop === 'assignee') { + if (changeset[prop] === '') { + tasks.push(db.sortedSetRemove(`flags:byAssignee:${changeset[prop]}`, flagId)); + /* eslint-disable-next-line */ + } else if (!await isAssignable(parseInt(changeset[prop], 10))) { + delete changeset[prop]; + } else { + tasks.push(db.sortedSetAdd(`flags:byAssignee:${changeset[prop]}`, now, flagId)); + tasks.push(notifyAssignee(changeset[prop])); + } + } + } + + if (!Object.keys(changeset).length) { + return; + } + + tasks.push(db.setObject(`flag:${flagId}`, changeset)); + tasks.push(Flags.appendHistory(flagId, uid, changeset)); + await Promise.all(tasks); + + plugins.hooks.fire('action:flags.update', { flagId: flagId, changeset: changeset, uid: uid }); +}; + +Flags.resolveFlag = async function (type, id, uid) { + const flagId = await Flags.getFlagIdByTarget(type, id); + if (parseInt(flagId, 10)) { + await Flags.update(flagId, uid, { state: 'resolved' }); + } +}; + +Flags.resolveUserPostFlags = async function (uid, callerUid) { + if (meta.config['flags:autoResolveOnBan']) { + await batch.processSortedSet(`uid:${uid}:posts`, async (pids) => { + let postData = await posts.getPostsFields(pids, ['pid', 'flagId']); + postData = postData.filter(p => p && p.flagId); + for (const postObj of postData) { + if (parseInt(postObj.flagId, 10)) { + // eslint-disable-next-line no-await-in-loop + await Flags.update(postObj.flagId, callerUid, { state: 'resolved' }); + } + } + }, { + batch: 500, + }); + } +}; + +Flags.getHistory = async function (flagId) { + const uids = []; + let history = await db.getSortedSetRevRangeWithScores(`flag:${flagId}:history`, 0, -1); + const targetUid = await db.getObjectField(`flag:${flagId}`, 'targetUid'); + + history = history.map((entry) => { + entry.value = JSON.parse(entry.value); + + uids.push(entry.value[0]); + + // Deserialise changeset + const changeset = entry.value[1]; + if (changeset.hasOwnProperty('state')) { + changeset.state = changeset.state === undefined ? '' : `[[flags:state-${changeset.state}]]`; + } + + return { + uid: entry.value[0], + fields: changeset, + datetime: entry.score, + datetimeISO: utils.toISOString(entry.score), + }; + }); + + // Append ban history and username change data + history = await mergeBanHistory(history, targetUid, uids); + history = await mergeMuteHistory(history, targetUid, uids); + history = await mergeUsernameEmailChanges(history, targetUid, uids); + + const userData = await user.getUsersFields(uids, ['username', 'userslug', 'picture']); + history.forEach((event, idx) => { event.user = userData[idx]; }); + + // Resort by date + history = history.sort((a, b) => b.datetime - a.datetime); + + return history; +}; + +Flags.appendHistory = async function (flagId, uid, changeset) { + const datetime = changeset.datetime || Date.now(); + delete changeset.datetime; + const payload = JSON.stringify([uid, changeset, datetime]); + await db.sortedSetAdd(`flag:${flagId}:history`, datetime, payload); +}; + +Flags.appendNote = async function (flagId, uid, note, datetime) { + if (datetime) { + try { + await Flags.deleteNote(flagId, datetime); + } catch (e) { + // Do not throw if note doesn't exist + if (!e.message === '[[error:invalid-data]]') { + throw e; + } + } + } + datetime = datetime || Date.now(); + + const payload = JSON.stringify([uid, note]); + await db.sortedSetAdd(`flag:${flagId}:notes`, datetime, payload); + await Flags.appendHistory(flagId, uid, { + notes: null, + datetime: datetime, + }); +}; + +Flags.notify = async function (flagObj, uid, notifySelf = false) { + const [admins, globalMods] = await Promise.all([ + groups.getMembers('administrators', 0, -1), + groups.getMembers('Global Moderators', 0, -1), + ]); + let uids = admins.concat(globalMods); + let notifObj = null; + + const { displayname } = flagObj.reports[flagObj.reports.length - 1].reporter; + + if (flagObj.type === 'post') { + const [title, cid] = await Promise.all([ + topics.getTitleByPid(flagObj.targetId), + posts.getCidByPid(flagObj.targetId), + ]); + + const modUids = await categories.getModeratorUids([cid]); + const titleEscaped = utils.decodeHTMLEntities(title).replace(/%/g, '%').replace(/,/g, ','); + + notifObj = await notifications.create({ + type: 'new-post-flag', + bodyShort: `[[notifications:user_flagged_post_in, ${displayname}, ${titleEscaped}]]`, + bodyLong: await plugins.hooks.fire('filter:parse.raw', String(flagObj.description || '')), + pid: flagObj.targetId, + path: `/flags/${flagObj.flagId}`, + nid: `flag:post:${flagObj.targetId}`, + from: uid, + mergeId: `notifications:user_flagged_post_in|${flagObj.targetId}`, + topicTitle: title, + }); + uids = uids.concat(modUids[0]); + } else if (flagObj.type === 'user') { + const targetDisplayname = flagObj.target && flagObj.target.user ? flagObj.target.user.displayname : '[[global:guest]]'; + notifObj = await notifications.create({ + type: 'new-user-flag', + bodyShort: `[[notifications:user_flagged_user, ${displayname}, ${targetDisplayname}]]`, + bodyLong: await plugins.hooks.fire('filter:parse.raw', String(flagObj.description || '')), + path: `/flags/${flagObj.flagId}`, + nid: `flag:user:${flagObj.targetId}`, + from: uid, + mergeId: `notifications:user_flagged_user|${flagObj.targetId}`, + }); + } else { + throw new Error('[[error:invalid-data]]'); + } + + plugins.hooks.fire('action:flags.notify', { + flag: flagObj, + notification: notifObj, + from: uid, + to: uids, + }); + if (!notifySelf) { + uids = uids.filter(_uid => parseInt(_uid, 10) !== parseInt(uid, 10)); + } + await notifications.push(notifObj, uids); +}; + +async function mergeBanHistory(history, targetUid, uids) { + return await mergeBanMuteHistory(history, uids, { + set: `uid:${targetUid}:bans:timestamp`, + label: '[[user:banned]]', + reasonDefault: '[[user:info.banned-no-reason]]', + expiryKey: '[[user:info.banned-expiry]]', + }); +} + +async function mergeMuteHistory(history, targetUid, uids) { + return await mergeBanMuteHistory(history, uids, { + set: `uid:${targetUid}:mutes:timestamp`, + label: '[[user:muted]]', + reasonDefault: '[[user:info.muted-no-reason]]', + expiryKey: '[[user:info.muted-expiry]]', + }); +} + +async function mergeBanMuteHistory(history, uids, params) { + let recentObjs = await db.getSortedSetRevRange(params.set, 0, 19); + recentObjs = await db.getObjects(recentObjs); + + return history.concat(recentObjs.reduce((memo, cur) => { + uids.push(cur.fromUid); + memo.push({ + uid: cur.fromUid, + meta: [ + { + key: params.label, + value: validator.escape(String(cur.reason || params.reasonDefault)), + labelClass: 'danger', + }, + { + key: params.expiryKey, + value: new Date(parseInt(cur.expire, 10)).toISOString(), + labelClass: 'default', + }, + ], + datetime: parseInt(cur.timestamp, 10), + datetimeISO: utils.toISOString(parseInt(cur.timestamp, 10)), + }); + + return memo; + }, [])); +} + +async function mergeUsernameEmailChanges(history, targetUid, uids) { + const usernameChanges = await user.getHistory(`user:${targetUid}:usernames`); + const emailChanges = await user.getHistory(`user:${targetUid}:emails`); + + return history.concat(usernameChanges.reduce((memo, changeObj) => { + uids.push(targetUid); + memo.push({ + uid: targetUid, + meta: [ + { + key: '[[user:change_username]]', + value: changeObj.value, + labelClass: 'primary', + }, + ], + datetime: changeObj.timestamp, + datetimeISO: changeObj.timestampISO, + }); + + return memo; + }, [])).concat(emailChanges.reduce((memo, changeObj) => { + uids.push(targetUid); + memo.push({ + uid: targetUid, + meta: [ + { + key: '[[user:change_email]]', + value: changeObj.value, + labelClass: 'primary', + }, + ], + datetime: changeObj.timestamp, + datetimeISO: changeObj.timestampISO, + }); + + return memo; + }, [])); +} + +require('./promisify')(Flags); diff --git a/src/groups/cache.js b/src/groups/cache.js new file mode 100644 index 0000000000..86262ed6ad --- /dev/null +++ b/src/groups/cache.js @@ -0,0 +1,19 @@ +'use strict'; + +const cacheCreate = require('../cache/lru'); + +module.exports = function (Groups) { + Groups.cache = cacheCreate({ + name: 'group', + max: 40000, + ttl: 0, + }); + + Groups.clearCache = function (uid, groupNames) { + if (!Array.isArray(groupNames)) { + groupNames = [groupNames]; + } + const keys = groupNames.map(name => `${uid}:${name}`); + Groups.cache.del(keys); + }; +}; diff --git a/src/groups/cover.js b/src/groups/cover.js new file mode 100644 index 0000000000..b8d16c8fad --- /dev/null +++ b/src/groups/cover.js @@ -0,0 +1,80 @@ +'use strict'; + +const path = require('path'); + +const nconf = require('nconf'); + +const db = require('../database'); +const image = require('../image'); +const file = require('../file'); + +module.exports = function (Groups) { + const allowedTypes = ['image/png', 'image/jpeg', 'image/bmp']; + Groups.updateCoverPosition = async function (groupName, position) { + if (!groupName) { + throw new Error('[[error:invalid-data]]'); + } + await Groups.setGroupField(groupName, 'cover:position', position); + }; + + Groups.updateCover = async function (uid, data) { + let tempPath = data.file ? data.file.path : ''; + try { + // Position only? That's fine + if (!data.imageData && !data.file && data.position) { + return await Groups.updateCoverPosition(data.groupName, data.position); + } + const type = data.file ? data.file.type : image.mimeFromBase64(data.imageData); + if (!type || !allowedTypes.includes(type)) { + throw new Error('[[error:invalid-image]]'); + } + + if (!tempPath) { + tempPath = await image.writeImageDataToTempFile(data.imageData); + } + + const filename = `groupCover-${data.groupName}${path.extname(tempPath)}`; + const uploadData = await image.uploadImage(filename, 'files', { + path: tempPath, + uid: uid, + name: 'groupCover', + }); + const { url } = uploadData; + await Groups.setGroupField(data.groupName, 'cover:url', url); + + await image.resizeImage({ + path: tempPath, + width: 358, + }); + const thumbUploadData = await image.uploadImage(`groupCoverThumb-${data.groupName}${path.extname(tempPath)}`, 'files', { + path: tempPath, + uid: uid, + name: 'groupCover', + }); + await Groups.setGroupField(data.groupName, 'cover:thumb:url', thumbUploadData.url); + + if (data.position) { + await Groups.updateCoverPosition(data.groupName, data.position); + } + + return { url: url }; + } finally { + file.delete(tempPath); + } + }; + + Groups.removeCover = async function (data) { + const fields = ['cover:url', 'cover:thumb:url']; + const values = await Groups.getGroupFields(data.groupName, fields); + await Promise.all(fields.map((field) => { + if (!values[field] || !values[field].startsWith(`${nconf.get('relative_path')}/assets/uploads/files/`)) { + return; + } + const filename = values[field].split('/').pop(); + const filePath = path.join(nconf.get('upload_path'), 'files', filename); + return file.delete(filePath); + })); + + await db.deleteObjectFields(`group:${data.groupName}`, ['cover:url', 'cover:thumb:url', 'cover:position']); + }; +}; diff --git a/src/groups/create.js b/src/groups/create.js new file mode 100644 index 0000000000..da538aa038 --- /dev/null +++ b/src/groups/create.js @@ -0,0 +1,95 @@ +'use strict'; + +const meta = require('../meta'); +const plugins = require('../plugins'); +const slugify = require('../slugify'); +const db = require('../database'); + +module.exports = function (Groups) { + Groups.create = async function (data) { + const isSystem = isSystemGroup(data); + const timestamp = data.timestamp || Date.now(); + let disableJoinRequests = parseInt(data.disableJoinRequests, 10) === 1 ? 1 : 0; + if (data.name === 'administrators') { + disableJoinRequests = 1; + } + const disableLeave = parseInt(data.disableLeave, 10) === 1 ? 1 : 0; + const isHidden = parseInt(data.hidden, 10) === 1; + + Groups.validateGroupName(data.name); + + const exists = await meta.userOrGroupExists(data.name); + if (exists) { + throw new Error('[[error:group-already-exists]]'); + } + + const memberCount = data.hasOwnProperty('ownerUid') ? 1 : 0; + const isPrivate = data.hasOwnProperty('private') && data.private !== undefined ? parseInt(data.private, 10) === 1 : true; + let groupData = { + name: data.name, + slug: slugify(data.name), + createtime: timestamp, + userTitle: data.userTitle || data.name, + userTitleEnabled: parseInt(data.userTitleEnabled, 10) === 1 ? 1 : 0, + description: data.description || '', + memberCount: memberCount, + hidden: isHidden ? 1 : 0, + system: isSystem ? 1 : 0, + private: isPrivate ? 1 : 0, + disableJoinRequests: disableJoinRequests, + disableLeave: disableLeave, + }; + + await plugins.hooks.fire('filter:group.create', { group: groupData, data: data }); + + await db.sortedSetAdd('groups:createtime', groupData.createtime, groupData.name); + await db.setObject(`group:${groupData.name}`, groupData); + + if (data.hasOwnProperty('ownerUid')) { + await db.setAdd(`group:${groupData.name}:owners`, data.ownerUid); + await db.sortedSetAdd(`group:${groupData.name}:members`, timestamp, data.ownerUid); + } + + if (!isHidden && !isSystem) { + await db.sortedSetAddBulk([ + ['groups:visible:createtime', timestamp, groupData.name], + ['groups:visible:memberCount', groupData.memberCount, groupData.name], + ['groups:visible:name', 0, `${groupData.name.toLowerCase()}:${groupData.name}`], + ]); + } + + await db.setObjectField('groupslug:groupname', groupData.slug, groupData.name); + + groupData = await Groups.getGroupData(groupData.name); + plugins.hooks.fire('action:group.create', { group: groupData }); + return groupData; + }; + + function isSystemGroup(data) { + return data.system === true || parseInt(data.system, 10) === 1 || + Groups.systemGroups.includes(data.name) || + Groups.isPrivilegeGroup(data.name); + } + + Groups.validateGroupName = function (name) { + if (!name) { + 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) { + throw new Error('[[error:group-name-too-long]]'); + } + + if (name === 'guests' || (!Groups.isPrivilegeGroup(name) && name.includes(':'))) { + throw new Error('[[error:invalid-group-name]]'); + } + + if (name.includes('/') || !slugify(name)) { + throw new Error('[[error:invalid-group-name]]'); + } + }; +}; diff --git a/src/groups/data.js b/src/groups/data.js new file mode 100644 index 0000000000..78f0c571bf --- /dev/null +++ b/src/groups/data.js @@ -0,0 +1,108 @@ +'use strict'; + +const validator = require('validator'); +const nconf = require('nconf'); + +const db = require('../database'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const translator = require('../translator'); + +const intFields = [ + 'createtime', 'memberCount', 'hidden', 'system', 'private', + 'userTitleEnabled', 'disableJoinRequests', 'disableLeave', +]; + +module.exports = function (Groups) { + Groups.getGroupsFields = async function (groupNames, fields) { + if (!Array.isArray(groupNames) || !groupNames.length) { + return []; + } + + const ephemeralIdx = groupNames.reduce((memo, cur, idx) => { + if (Groups.ephemeralGroups.includes(cur)) { + memo.push(idx); + } + return memo; + }, []); + + const keys = groupNames.map(groupName => `group:${groupName}`); + const groupData = await db.getObjects(keys, fields); + if (ephemeralIdx.length) { + ephemeralIdx.forEach((idx) => { + groupData[idx] = Groups.getEphemeralGroup(groupNames[idx]); + }); + } + + groupData.forEach(group => modifyGroup(group, fields)); + + const results = await plugins.hooks.fire('filter:groups.get', { groups: groupData }); + return results.groups; + }; + + Groups.getGroupsData = async function (groupNames) { + return await Groups.getGroupsFields(groupNames, []); + }; + + Groups.getGroupData = async function (groupName) { + const groupsData = await Groups.getGroupsData([groupName]); + 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) { + const groups = await Groups.getGroupsFields([groupName], fields); + return groups ? groups[0] : null; + }; + + Groups.setGroupField = async function (groupName, field, value) { + await db.setObjectField(`group:${groupName}`, field, value); + plugins.hooks.fire('action:group.set', { field: field, value: value, type: 'set' }); + }; +}; + +function modifyGroup(group, fields) { + if (group) { + db.parseIntFields(group, intFields, fields); + + escapeGroupData(group); + group.userTitleEnabled = ([null, undefined].includes(group.userTitleEnabled)) ? 1 : group.userTitleEnabled; + group.labelColor = validator.escape(String(group.labelColor || '#000000')); + group.textColor = validator.escape(String(group.textColor || '#ffffff')); + group.icon = validator.escape(String(group.icon || '')); + group.createtimeISO = utils.toISOString(group.createtime); + group.private = ([null, undefined].includes(group.private)) ? 1 : group.private; + group.memberPostCids = group.memberPostCids || ''; + group.memberPostCidsArray = group.memberPostCids.split(',').map(cid => parseInt(cid, 10)).filter(Boolean); + + group['cover:thumb:url'] = group['cover:thumb:url'] || group['cover:url']; + + if (group['cover:url']) { + group['cover:url'] = group['cover:url'].startsWith('http') ? group['cover:url'] : (nconf.get('relative_path') + group['cover:url']); + } else { + group['cover:url'] = require('../coverPhoto').getDefaultGroupCover(group.name); + } + + if (group['cover:thumb:url']) { + group['cover:thumb:url'] = group['cover:thumb:url'].startsWith('http') ? group['cover:thumb:url'] : (nconf.get('relative_path') + group['cover:thumb:url']); + } else { + group['cover:thumb:url'] = require('../coverPhoto').getDefaultGroupCover(group.name); + } + + group['cover:position'] = validator.escape(String(group['cover:position'] || '50% 50%')); + } +} + +function escapeGroupData(group) { + if (group) { + group.nameEncoded = encodeURIComponent(group.name); + group.displayName = validator.escape(String(group.name)); + group.description = validator.escape(String(group.description || '')); + group.userTitle = validator.escape(String(group.userTitle || '')); + group.userTitleEscaped = translator.escape(group.userTitle); + } +} diff --git a/src/groups/delete.js b/src/groups/delete.js new file mode 100644 index 0000000000..1b2eb36e1d --- /dev/null +++ b/src/groups/delete.js @@ -0,0 +1,57 @@ +'use strict'; + +const plugins = require('../plugins'); +const slugify = require('../slugify'); +const db = require('../database'); +const batch = require('../batch'); + +module.exports = function (Groups) { + Groups.destroy = async function (groupNames) { + if (!Array.isArray(groupNames)) { + groupNames = [groupNames]; + } + + let groupsData = await Groups.getGroupsData(groupNames); + groupsData = groupsData.filter(Boolean); + if (!groupsData.length) { + return; + } + const keys = []; + groupNames.forEach((groupName) => { + keys.push( + `group:${groupName}`, + `group:${groupName}:members`, + `group:${groupName}:pending`, + `group:${groupName}:invited`, + `group:${groupName}:owners`, + `group:${groupName}:member:pids` + ); + }); + const sets = groupNames.map(groupName => `${groupName.toLowerCase()}:${groupName}`); + const fields = groupNames.map(groupName => slugify(groupName)); + + await Promise.all([ + db.deleteAll(keys), + db.sortedSetRemove([ + 'groups:createtime', + 'groups:visible:createtime', + 'groups:visible:memberCount', + ], groupNames), + db.sortedSetRemove('groups:visible:name', sets), + db.deleteObjectFields('groupslug:groupname', fields), + removeGroupsFromPrivilegeGroups(groupNames), + ]); + Groups.cache.reset(); + plugins.hooks.fire('action:groups.destroy', { groups: groupsData }); + }; + + async function removeGroupsFromPrivilegeGroups(groupNames) { + await batch.processSortedSet('groups:createtime', async (otherGroups) => { + const privilegeGroups = otherGroups.filter(group => Groups.isPrivilegeGroup(group)); + const keys = privilegeGroups.map(group => `group:${group}:members`); + await db.sortedSetRemove(keys, groupNames); + }, { + batch: 500, + }); + } +}; diff --git a/src/groups/index.js b/src/groups/index.js new file mode 100644 index 0000000000..fa37b433aa --- /dev/null +++ b/src/groups/index.js @@ -0,0 +1,247 @@ +'use strict'; + +const user = require('../user'); +const db = require('../database'); +const plugins = require('../plugins'); +const slugify = require('../slugify'); + +const Groups = module.exports; + +require('./data')(Groups); +require('./create')(Groups); +require('./delete')(Groups); +require('./update')(Groups); +require('./invite')(Groups); +require('./membership')(Groups); +require('./ownership')(Groups); +require('./search')(Groups); +require('./cover')(Groups); +require('./posts')(Groups); +require('./user')(Groups); +require('./join')(Groups); +require('./leave')(Groups); +require('./cache')(Groups); + +Groups.BANNED_USERS = 'banned-users'; + +Groups.ephemeralGroups = ['guests', 'spiders']; + +Groups.systemGroups = [ + 'registered-users', + 'verified-users', + 'unverified-users', + Groups.BANNED_USERS, + 'administrators', + 'Global Moderators', +]; + +Groups.getEphemeralGroup = function (groupName) { + return { + name: groupName, + slug: slugify(groupName), + description: '', + hidden: 0, + system: 1, + }; +}; + +Groups.removeEphemeralGroups = function (groups) { + for (let x = groups.length; x >= 0; x -= 1) { + if (Groups.ephemeralGroups.includes(groups[x])) { + groups.splice(x, 1); + } + } + + return groups; +}; + +const isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w\-:]+$/; +Groups.isPrivilegeGroup = function (groupName) { + return isPrivilegeGroupRegex.test(groupName); +}; + +Groups.getGroupsFromSet = async function (set, start, stop) { + let groupNames; + if (set === 'groups:visible:name') { + groupNames = await db.getSortedSetRangeByLex(set, '-', '+', start, stop - start + 1); + } else { + groupNames = await db.getSortedSetRevRange(set, start, stop); + } + if (set === 'groups:visible:name') { + groupNames = groupNames.map(name => name.split(':')[1]); + } + + return await Groups.getGroupsAndMembers(groupNames); +}; + +Groups.getGroupsBySort = async function (sort, start, stop) { + let set = 'groups:visible:name'; + if (sort === 'count') { + set = 'groups:visible:memberCount'; + } else if (sort === 'date') { + set = 'groups:visible:createtime'; + } + return await Groups.getGroupsFromSet(set, start, stop); +}; + +Groups.getNonPrivilegeGroups = async function (set, start, stop) { + let groupNames = await db.getSortedSetRevRange(set, start, stop); + groupNames = groupNames.concat(Groups.ephemeralGroups).filter(groupName => !Groups.isPrivilegeGroup(groupName)); + const groupsData = await Groups.getGroupsData(groupNames); + return groupsData.filter(Boolean); +}; + +Groups.getGroups = async function (set, start, stop) { + return await db.getSortedSetRevRange(set, start, stop); +}; + +Groups.getGroupsAndMembers = async function (groupNames) { + const [groups, members] = await Promise.all([ + Groups.getGroupsData(groupNames), + Groups.getMemberUsers(groupNames, 0, 9), + ]); + groups.forEach((group, index) => { + if (group) { + group.members = members[index] || []; + group.truncated = group.memberCount > group.members.length; + } + }); + return groups; +}; + +Groups.get = async function (groupName, options) { + if (!groupName) { + throw new Error('[[error:invalid-group]]'); + } + + let stop = -1; + + if (options.truncateUserList) { + stop = (parseInt(options.userListCount, 10) || 4) - 1; + } + + const [groupData, members, pending, invited, isMember, isPending, isInvited, isOwner] = await Promise.all([ + Groups.getGroupData(groupName), + Groups.getOwnersAndMembers(groupName, options.uid, 0, stop), + Groups.getUsersFromSet(`group:${groupName}:pending`, ['username', 'userslug', 'picture']), + Groups.getUsersFromSet(`group:${groupName}:invited`, ['username', 'userslug', 'picture']), + Groups.isMember(options.uid, groupName), + Groups.isPending(options.uid, groupName), + Groups.isInvited(options.uid, groupName), + Groups.ownership.isOwner(options.uid, groupName), + ]); + + if (!groupData) { + return null; + } + const descriptionParsed = await plugins.hooks.fire('filter:parse.raw', String(groupData.description || '')); + groupData.descriptionParsed = descriptionParsed; + groupData.members = members; + groupData.membersNextStart = stop + 1; + groupData.pending = pending.filter(Boolean); + groupData.invited = invited.filter(Boolean); + groupData.isMember = isMember; + groupData.isPending = isPending; + groupData.isInvited = isInvited; + groupData.isOwner = isOwner; + const results = await plugins.hooks.fire('filter:group.get', { group: groupData }); + return results.group; +}; + +Groups.getOwners = async function (groupName) { + return await db.getSetMembers(`group:${groupName}:owners`); +}; + +Groups.getOwnersAndMembers = async function (groupName, uid, start, stop) { + const ownerUids = await db.getSetMembers(`group:${groupName}:owners`); + const countToReturn = stop - start + 1; + const ownerUidsOnPage = ownerUids.slice(start, stop !== -1 ? stop + 1 : undefined); + const owners = await user.getUsers(ownerUidsOnPage, uid); + owners.forEach((user) => { + if (user) { + user.isOwner = true; + } + }); + + let done = false; + let returnUsers = owners; + let memberStart = start - ownerUids.length; + let memberStop = memberStart + countToReturn - 1; + memberStart = Math.max(0, memberStart); + memberStop = Math.max(0, memberStop); + async function addMembers(start, stop) { + let batch = await user.getUsersFromSet(`group:${groupName}:members`, uid, start, stop); + if (!batch.length) { + done = true; + } + batch = batch.filter(user => user && user.uid && !ownerUids.includes(user.uid.toString())); + returnUsers = returnUsers.concat(batch); + } + + if (stop === -1) { + await addMembers(memberStart, -1); + } else { + while (returnUsers.length < countToReturn && !done) { + /* eslint-disable no-await-in-loop */ + await addMembers(memberStart, memberStop); + memberStart = memberStop + 1; + memberStop = memberStart + countToReturn - 1; + } + } + returnUsers = countToReturn > 0 ? returnUsers.slice(0, countToReturn) : returnUsers; + const result = await plugins.hooks.fire('filter:group.getOwnersAndMembers', { + users: returnUsers, + uid: uid, + start: start, + stop: stop, + }); + return result.users; +}; + +Groups.getByGroupslug = async function (slug, options) { + options = options || {}; + const groupName = await db.getObjectField('groupslug:groupname', slug); + if (!groupName) { + throw new Error('[[error:no-group]]'); + } + return await Groups.get(groupName, options); +}; + +Groups.getGroupNameByGroupSlug = async function (slug) { + return await db.getObjectField('groupslug:groupname', slug); +}; + +Groups.isPrivate = async function (groupName) { + return await isFieldOn(groupName, 'private'); +}; + +Groups.isHidden = async function (groupName) { + return await isFieldOn(groupName, 'hidden'); +}; + +async function isFieldOn(groupName, field) { + const value = await db.getObjectField(`group:${groupName}`, field); + return parseInt(value, 10) === 1; +} + +Groups.exists = async function (name) { + if (Array.isArray(name)) { + const slugs = name.map(groupName => slugify(groupName)); + const isMembersOfRealGroups = await db.isSortedSetMembers('groups:createtime', name); + const isMembersOfEphemeralGroups = slugs.map(slug => Groups.ephemeralGroups.includes(slug)); + return name.map((n, index) => isMembersOfRealGroups[index] || isMembersOfEphemeralGroups[index]); + } + const slug = slugify(name); + const isMemberOfRealGroups = await db.isSortedSetMember('groups:createtime', name); + const isMemberOfEphemeralGroups = Groups.ephemeralGroups.includes(slug); + return isMemberOfRealGroups || isMemberOfEphemeralGroups; +}; + +Groups.existsBySlug = async function (slug) { + if (Array.isArray(slug)) { + return await db.isObjectFields('groupslug:groupname', slug); + } + return await db.isObjectField('groupslug:groupname', slug); +}; + +require('../promisify')(Groups); diff --git a/src/groups/invite.js b/src/groups/invite.js new file mode 100644 index 0000000000..92f0983c68 --- /dev/null +++ b/src/groups/invite.js @@ -0,0 +1,117 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const user = require('../user'); +const slugify = require('../slugify'); +const plugins = require('../plugins'); +const notifications = require('../notifications'); + +module.exports = function (Groups) { + Groups.requestMembership = async function (groupName, uid) { + await inviteOrRequestMembership(groupName, uid, 'request'); + const { displayname } = await user.getUserFields(uid, ['username']); + + const [notification, owners] = await Promise.all([ + notifications.create({ + type: 'group-request-membership', + bodyShort: `[[groups:request.notification_title, ${displayname}]]`, + bodyLong: `[[groups:request.notification_text, ${displayname}, ${groupName}]]`, + nid: `group:${groupName}:uid:${uid}:request`, + path: `/groups/${slugify(groupName)}`, + from: uid, + }), + Groups.getOwners(groupName), + ]); + + await notifications.push(notification, owners); + }; + + Groups.acceptMembership = async function (groupName, uid) { + await db.setsRemove([`group:${groupName}:pending`, `group:${groupName}:invited`], uid); + await Groups.join(groupName, uid); + + const notification = await notifications.create({ + type: 'group-invite', + bodyShort: `[[groups:membership.accept.notification_title, ${groupName}]]`, + nid: `group:${groupName}:uid:${uid}:invite-accepted`, + path: `/groups/${slugify(groupName)}`, + }); + await notifications.push(notification, [uid]); + }; + + Groups.rejectMembership = async function (groupNames, uid) { + if (!Array.isArray(groupNames)) { + groupNames = [groupNames]; + } + const sets = []; + groupNames.forEach(groupName => sets.push(`group:${groupName}:pending`, `group:${groupName}:invited`)); + await db.setsRemove(sets, uid); + }; + + Groups.invite = async function (groupName, uids) { + uids = Array.isArray(uids) ? uids : [uids]; + uids = await inviteOrRequestMembership(groupName, uids, 'invite'); + + const notificationData = await Promise.all(uids.map(uid => notifications.create({ + type: 'group-invite', + bodyShort: `[[groups:invited.notification_title, ${groupName}]]`, + bodyLong: '', + nid: `group:${groupName}:uid:${uid}:invite`, + path: `/groups/${slugify(groupName)}`, + }))); + + await Promise.all(uids.map((uid, index) => notifications.push(notificationData[index], uid))); + }; + + async function inviteOrRequestMembership(groupName, uids, type) { + uids = Array.isArray(uids) ? uids : [uids]; + uids = uids.filter(uid => parseInt(uid, 10) > 0); + const [exists, isMember, isPending, isInvited] = await Promise.all([ + Groups.exists(groupName), + Groups.isMembers(uids, groupName), + Groups.isPending(uids, groupName), + Groups.isInvited(uids, groupName), + ]); + + if (!exists) { + throw new Error('[[error:no-group]]'); + } + + uids = uids.filter((uid, i) => !isMember[i] && ((type === 'invite' && !isInvited[i]) || (type === 'request' && !isPending[i]))); + + const set = type === 'invite' ? `group:${groupName}:invited` : `group:${groupName}:pending`; + await db.setAdd(set, uids); + const hookName = type === 'invite' ? 'inviteMember' : 'requestMembership'; + plugins.hooks.fire(`action:group.${hookName}`, { + groupName: groupName, + uids: uids, + }); + return uids; + } + + Groups.isInvited = async function (uids, groupName) { + return await checkInvitePending(uids, `group:${groupName}:invited`); + }; + + Groups.isPending = async function (uids, groupName) { + return await checkInvitePending(uids, `group:${groupName}:pending`); + }; + + async function checkInvitePending(uids, set) { + const isArray = Array.isArray(uids); + uids = isArray ? uids : [uids]; + const checkUids = uids.filter(uid => parseInt(uid, 10) > 0); + const isMembers = await db.isSetMembers(set, checkUids); + const map = _.zipObject(checkUids, isMembers); + return isArray ? uids.map(uid => !!map[uid]) : !!map[uids[0]]; + } + + Groups.getPending = async function (groupName) { + if (!groupName) { + return []; + } + return await db.getSetMembers(`group:${groupName}:pending`); + }; +}; diff --git a/src/groups/join.js b/src/groups/join.js new file mode 100644 index 0000000000..1c08d4d49d --- /dev/null +++ b/src/groups/join.js @@ -0,0 +1,109 @@ +'use strict'; + +const winston = require('winston'); + +const db = require('../database'); +const user = require('../user'); +const plugins = require('../plugins'); +const cache = require('../cache'); + +module.exports = function (Groups) { + Groups.join = async function (groupNames, uid) { + if (!groupNames) { + throw new Error('[[error:invalid-data]]'); + } + if (Array.isArray(groupNames) && !groupNames.length) { + return; + } + if (!Array.isArray(groupNames)) { + groupNames = [groupNames]; + } + + if (!uid) { + throw new Error('[[error:invalid-uid]]'); + } + + const [isMembers, exists, isAdmin] = await Promise.all([ + Groups.isMemberOfGroups(uid, groupNames), + Groups.exists(groupNames), + user.isAdministrator(uid), + ]); + + const groupsToCreate = groupNames.filter((groupName, index) => groupName && !exists[index]); + const groupsToJoin = groupNames.filter((groupName, index) => !isMembers[index]); + + if (!groupsToJoin.length) { + return; + } + await createNonExistingGroups(groupsToCreate); + + const promises = [ + db.sortedSetsAdd(groupsToJoin.map(groupName => `group:${groupName}:members`), Date.now(), uid), + db.incrObjectField(groupsToJoin.map(groupName => `group:${groupName}`), 'memberCount'), + ]; + if (isAdmin) { + promises.push(db.setsAdd(groupsToJoin.map(groupName => `group:${groupName}:owners`), uid)); + } + + await Promise.all(promises); + + Groups.clearCache(uid, groupsToJoin); + cache.del(groupsToJoin.map(name => `group:${name}:members`)); + + const groupData = await Groups.getGroupsFields(groupsToJoin, ['name', 'hidden', 'memberCount']); + const visibleGroups = groupData.filter(groupData => groupData && !groupData.hidden); + + if (visibleGroups.length) { + await db.sortedSetAdd( + 'groups:visible:memberCount', + visibleGroups.map(groupData => groupData.memberCount), + visibleGroups.map(groupData => groupData.name) + ); + } + + await setGroupTitleIfNotSet(groupsToJoin, uid); + + plugins.hooks.fire('action:group.join', { + groupNames: groupsToJoin, + uid: uid, + }); + }; + + async function createNonExistingGroups(groupsToCreate) { + if (!groupsToCreate.length) { + return; + } + + for (const groupName of groupsToCreate) { + try { + // eslint-disable-next-line no-await-in-loop + await Groups.create({ + name: groupName, + hidden: 1, + }); + } catch (err) { + if (err && err.message !== '[[error:group-already-exists]]') { + winston.error(`[groups.join] Could not create new hidden group (${groupName})\n${err.stack}`); + throw err; + } + } + } + } + + async function setGroupTitleIfNotSet(groupNames, uid) { + const ignore = ['registered-users', 'verified-users', 'unverified-users', Groups.BANNED_USERS]; + groupNames = groupNames.filter( + groupName => !ignore.includes(groupName) && !Groups.isPrivilegeGroup(groupName) + ); + if (!groupNames.length) { + return; + } + + const currentTitle = await db.getObjectField(`user:${uid}`, 'groupTitle'); + if (currentTitle || currentTitle === '') { + return; + } + + await user.setUserField(uid, 'groupTitle', JSON.stringify(groupNames)); + } +}; diff --git a/src/groups/leave.js b/src/groups/leave.js new file mode 100644 index 0000000000..4ad844181b --- /dev/null +++ b/src/groups/leave.js @@ -0,0 +1,100 @@ +'use strict'; + +const db = require('../database'); +const user = require('../user'); +const plugins = require('../plugins'); +const cache = require('../cache'); + +module.exports = function (Groups) { + Groups.leave = async function (groupNames, uid) { + if (Array.isArray(groupNames) && !groupNames.length) { + return; + } + if (!Array.isArray(groupNames)) { + groupNames = [groupNames]; + } + + const isMembers = await Groups.isMemberOfGroups(uid, groupNames); + + const groupsToLeave = groupNames.filter((groupName, index) => isMembers[index]); + if (!groupsToLeave.length) { + return; + } + + await Promise.all([ + db.sortedSetRemove(groupsToLeave.map(groupName => `group:${groupName}:members`), uid), + db.setRemove(groupsToLeave.map(groupName => `group:${groupName}:owners`), uid), + db.decrObjectField(groupsToLeave.map(groupName => `group:${groupName}`), 'memberCount'), + ]); + + Groups.clearCache(uid, groupsToLeave); + cache.del(groupsToLeave.map(name => `group:${name}:members`)); + + const groupData = await Groups.getGroupsFields(groupsToLeave, ['name', 'hidden', 'memberCount']); + if (!groupData) { + return; + } + + const emptyPrivilegeGroups = groupData.filter(g => g && Groups.isPrivilegeGroup(g.name) && g.memberCount === 0); + const visibleGroups = groupData.filter(g => g && !g.hidden); + + const promises = []; + if (emptyPrivilegeGroups.length) { + promises.push(Groups.destroy, emptyPrivilegeGroups); + } + if (visibleGroups.length) { + promises.push( + db.sortedSetAdd, + 'groups:visible:memberCount', + visibleGroups.map(groupData => groupData.memberCount), + visibleGroups.map(groupData => groupData.name) + ); + } + + await Promise.all(promises); + + await clearGroupTitleIfSet(groupsToLeave, uid); + + plugins.hooks.fire('action:group.leave', { + groupNames: groupsToLeave, + uid: uid, + }); + }; + + async function clearGroupTitleIfSet(groupNames, uid) { + groupNames = groupNames.filter(groupName => groupName !== 'registered-users' && !Groups.isPrivilegeGroup(groupName)); + if (!groupNames.length) { + return; + } + const userData = await user.getUserData(uid); + if (!userData) { + return; + } + + const newTitleArray = userData.groupTitleArray.filter(groupTitle => !groupNames.includes(groupTitle)); + if (newTitleArray.length) { + await db.setObjectField(`user:${uid}`, 'groupTitle', JSON.stringify(newTitleArray)); + } else { + await db.deleteObjectField(`user:${uid}`, 'groupTitle'); + } + } + + Groups.leaveAllGroups = async function (uid) { + const groups = await db.getSortedSetRange('groups:createtime', 0, -1); + await Promise.all([ + Groups.leave(groups, uid), + Groups.rejectMembership(groups, uid), + ]); + }; + + Groups.kick = async function (uid, groupName, isOwner) { + if (isOwner) { + // If the owners set only contains one member, error out! + const numOwners = await db.setCount(`group:${groupName}:owners`); + if (numOwners <= 1) { + throw new Error('[[error:group-needs-owner]]'); + } + } + await Groups.leave(groupName, uid); + }; +}; diff --git a/src/groups/membership.js b/src/groups/membership.js new file mode 100644 index 0000000000..472b2159ea --- /dev/null +++ b/src/groups/membership.js @@ -0,0 +1,175 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const user = require('../user'); +const cache = require('../cache'); + +module.exports = function (Groups) { + Groups.getMembers = async function (groupName, start, stop) { + return await db.getSortedSetRevRange(`group:${groupName}:members`, start, stop); + }; + + Groups.getMemberUsers = async function (groupNames, start, stop) { + async function get(groupName) { + const uids = await Groups.getMembers(groupName, start, stop); + return await user.getUsersFields(uids, ['uid', 'username', 'picture', 'userslug']); + } + return await Promise.all(groupNames.map(name => get(name))); + }; + + Groups.getMembersOfGroups = async function (groupNames) { + return await db.getSortedSetsMembers(groupNames.map(name => `group:${name}:members`)); + }; + + Groups.isMember = async function (uid, groupName) { + if (!uid || parseInt(uid, 10) <= 0 || !groupName) { + return false; + } + + const cacheKey = `${uid}:${groupName}`; + let isMember = Groups.cache.get(cacheKey); + if (isMember !== undefined) { + return isMember; + } + isMember = await db.isSortedSetMember(`group:${groupName}:members`, uid); + Groups.cache.set(cacheKey, isMember); + return isMember; + }; + + Groups.isMembers = async function (uids, groupName) { + if (!groupName || !uids.length) { + return uids.map(() => false); + } + + if (groupName === 'guests') { + return uids.map(uid => parseInt(uid, 10) === 0); + } + + const cachedData = {}; + const nonCachedUids = uids.filter(uid => filterNonCached(cachedData, uid, groupName)); + + if (!nonCachedUids.length) { + return uids.map(uid => cachedData[`${uid}:${groupName}`]); + } + + const isMembers = await db.isSortedSetMembers(`group:${groupName}:members`, nonCachedUids); + nonCachedUids.forEach((uid, index) => { + cachedData[`${uid}:${groupName}`] = isMembers[index]; + Groups.cache.set(`${uid}:${groupName}`, isMembers[index]); + }); + return uids.map(uid => cachedData[`${uid}:${groupName}`]); + }; + + Groups.isMemberOfGroups = async function (uid, groups) { + if (!uid || parseInt(uid, 10) <= 0 || !groups.length) { + return groups.map(groupName => groupName === 'guests'); + } + const cachedData = {}; + const nonCachedGroups = groups.filter(groupName => filterNonCached(cachedData, uid, groupName)); + + if (!nonCachedGroups.length) { + return groups.map(groupName => cachedData[`${uid}:${groupName}`]); + } + const nonCachedGroupsMemberSets = nonCachedGroups.map(groupName => `group:${groupName}:members`); + const isMembers = await db.isMemberOfSortedSets(nonCachedGroupsMemberSets, uid); + nonCachedGroups.forEach((groupName, index) => { + cachedData[`${uid}:${groupName}`] = isMembers[index]; + Groups.cache.set(`${uid}:${groupName}`, isMembers[index]); + }); + + return groups.map(groupName => cachedData[`${uid}:${groupName}`]); + }; + + function filterNonCached(cachedData, uid, groupName) { + const isMember = Groups.cache.get(`${uid}:${groupName}`); + const isInCache = isMember !== undefined; + if (isInCache) { + cachedData[`${uid}:${groupName}`] = isMember; + } + return !isInCache; + } + + Groups.isMemberOfAny = async function (uid, groups) { + if (!groups.length) { + return false; + } + const isMembers = await Groups.isMemberOfGroups(uid, groups); + return isMembers.includes(true); + }; + + Groups.getMemberCount = async function (groupName) { + const count = await db.getObjectField(`group:${groupName}`, 'memberCount'); + return parseInt(count, 10); + }; + + Groups.isMemberOfGroupList = async function (uid, groupListKey) { + let groupNames = await getGroupNames(groupListKey); + groupNames = Groups.removeEphemeralGroups(groupNames); + if (!groupNames.length) { + return false; + } + + const isMembers = await Groups.isMemberOfGroups(uid, groupNames); + return isMembers.includes(true); + }; + + Groups.isMemberOfGroupsList = async function (uid, groupListKeys) { + const members = await getGroupNames(groupListKeys); + + let uniqueGroups = _.uniq(_.flatten(members)); + uniqueGroups = Groups.removeEphemeralGroups(uniqueGroups); + + const isMembers = await Groups.isMemberOfGroups(uid, uniqueGroups); + const isGroupMember = _.zipObject(uniqueGroups, isMembers); + + return members.map(groupNames => !!groupNames.find(name => isGroupMember[name])); + }; + + Groups.isMembersOfGroupList = async function (uids, groupListKey) { + const results = uids.map(() => false); + + let groupNames = await getGroupNames(groupListKey); + groupNames = Groups.removeEphemeralGroups(groupNames); + if (!groupNames.length) { + return results; + } + const isGroupMembers = await Promise.all(groupNames.map(name => Groups.isMembers(uids, name))); + + isGroupMembers.forEach((isMembers) => { + results.forEach((isMember, index) => { + if (!isMember && isMembers[index]) { + results[index] = true; + } + }); + }); + return results; + }; + + async function getGroupNames(keys) { + const isArray = Array.isArray(keys); + keys = isArray ? keys : [keys]; + + const cachedData = {}; + const nonCachedKeys = keys.filter((groupName) => { + const groupMembers = cache.get(`group:${groupName}:members`); + const isInCache = groupMembers !== undefined; + if (isInCache) { + cachedData[groupName] = groupMembers; + } + return !isInCache; + }); + + if (!nonCachedKeys.length) { + return isArray ? keys.map(groupName => cachedData[groupName]) : cachedData[keys[0]]; + } + const groupMembers = await db.getSortedSetsMembers(nonCachedKeys.map(name => `group:${name}:members`)); + + nonCachedKeys.forEach((groupName, index) => { + cachedData[groupName] = groupMembers[index]; + cache.set(`group:${groupName}:members`, groupMembers[index]); + }); + return isArray ? keys.map(groupName => cachedData[groupName]) : cachedData[keys[0]]; + } +}; diff --git a/src/groups/ownership.js b/src/groups/ownership.js new file mode 100644 index 0000000000..02fc1d052b --- /dev/null +++ b/src/groups/ownership.js @@ -0,0 +1,39 @@ +'use strict'; + +const db = require('../database'); +const plugins = require('../plugins'); + +module.exports = function (Groups) { + Groups.ownership = {}; + + Groups.ownership.isOwner = async function (uid, groupName) { + if (!(parseInt(uid, 10) > 0)) { + return false; + } + return await db.isSetMember(`group:${groupName}:owners`, uid); + }; + + Groups.ownership.isOwners = async function (uids, groupName) { + if (!Array.isArray(uids)) { + return []; + } + + return await db.isSetMembers(`group:${groupName}:owners`, uids); + }; + + Groups.ownership.grant = async function (toUid, groupName) { + await db.setAdd(`group:${groupName}:owners`, toUid); + plugins.hooks.fire('action:group.grantOwnership', { uid: toUid, groupName: groupName }); + }; + + Groups.ownership.rescind = async function (toUid, groupName) { + // If the owners set only contains one member (and toUid is that member), error out! + const numOwners = await db.setCount(`group:${groupName}:owners`); + const isOwner = await db.isSortedSetMember(`group:${groupName}:owners`); + if (numOwners <= 1 && isOwner) { + throw new Error('[[error:group-needs-owner]]'); + } + await db.setRemove(`group:${groupName}:owners`, toUid); + plugins.hooks.fire('action:group.rescindOwnership', { uid: toUid, groupName: groupName }); + }; +}; diff --git a/src/groups/posts.js b/src/groups/posts.js new file mode 100644 index 0000000000..47ef6c96ab --- /dev/null +++ b/src/groups/posts.js @@ -0,0 +1,44 @@ +'use strict'; + +const db = require('../database'); +const groups = require('.'); +const privileges = require('../privileges'); +const posts = require('../posts'); + +module.exports = function (Groups) { + Groups.onNewPostMade = async function (postData) { + if (!parseInt(postData.uid, 10)) { + return; + } + + let groupNames = await Groups.getUserGroupMembership('groups:visible:createtime', [postData.uid]); + groupNames = groupNames[0]; + + // Only process those groups that have the cid in its memberPostCids setting (or no setting at all) + const groupData = await groups.getGroupsFields(groupNames, ['memberPostCids']); + groupNames = groupNames.filter((groupName, idx) => ( + !groupData[idx].memberPostCidsArray.length || + groupData[idx].memberPostCidsArray.includes(postData.cid) + )); + + const keys = groupNames.map(groupName => `group:${groupName}:member:pids`); + await db.sortedSetsAdd(keys, postData.timestamp, postData.pid); + await Promise.all(groupNames.map(name => truncateMemberPosts(name))); + }; + + async function truncateMemberPosts(groupName) { + let lastPid = await db.getSortedSetRevRange(`group:${groupName}:member:pids`, 10, 10); + lastPid = lastPid[0]; + if (!parseInt(lastPid, 10)) { + return; + } + const score = await db.sortedSetScore(`group:${groupName}:member:pids`, lastPid); + await db.sortedSetsRemoveRangeByScore([`group:${groupName}:member:pids`], '-inf', score); + } + + Groups.getLatestMemberPosts = async function (groupName, max, uid) { + let pids = await db.getSortedSetRevRange(`group:${groupName}:member:pids`, 0, max - 1); + pids = await privileges.posts.filter('topics:read', pids, uid); + return await posts.getPostSummaryByPids(pids, uid, { stripTags: false }); + }; +}; diff --git a/src/groups/search.js b/src/groups/search.js new file mode 100644 index 0000000000..b6751c52f2 --- /dev/null +++ b/src/groups/search.js @@ -0,0 +1,84 @@ +'use strict'; + +const user = require('../user'); +const db = require('../database'); + +module.exports = function (Groups) { + Groups.search = async function (query, options) { + if (!query) { + return []; + } + query = String(query).toLowerCase(); + let groupNames = await db.getSortedSetRange('groups:createtime', 0, -1); + if (!options.hideEphemeralGroups) { + groupNames = Groups.ephemeralGroups.concat(groupNames); + } + groupNames = groupNames.filter(name => name.toLowerCase().includes(query) && + name !== Groups.BANNED_USERS && // hide banned-users in searches + !Groups.isPrivilegeGroup(name)); + groupNames = groupNames.slice(0, 100); + + let groupsData; + if (options.showMembers) { + groupsData = await Groups.getGroupsAndMembers(groupNames); + } else { + groupsData = await Groups.getGroupsData(groupNames); + } + groupsData = groupsData.filter(Boolean); + if (options.filterHidden) { + groupsData = groupsData.filter(group => !group.hidden); + } + return Groups.sort(options.sort, groupsData); + }; + + Groups.sort = function (strategy, groups) { + switch (strategy) { + case 'count': + groups.sort((a, b) => a.slug > b.slug) + .sort((a, b) => b.memberCount - a.memberCount); + break; + + case 'date': + groups.sort((a, b) => b.createtime - a.createtime); + break; + + case 'alpha': // intentional fall-through + default: + groups.sort((a, b) => (a.slug > b.slug ? 1 : -1)); + } + + return groups; + }; + + Groups.searchMembers = async function (data) { + if (!data.query) { + const users = await Groups.getOwnersAndMembers(data.groupName, data.uid, 0, 19); + return { users: users }; + } + + const results = await user.search({ + ...data, + paginate: false, + hardCap: -1, + }); + + const uids = results.users.map(user => user && user.uid); + const isOwners = await Groups.ownership.isOwners(uids, data.groupName); + + results.users.forEach((user, index) => { + if (user) { + user.isOwner = isOwners[index]; + } + }); + + results.users.sort((a, b) => { + if (a.isOwner && !b.isOwner) { + return -1; + } else if (!a.isOwner && b.isOwner) { + return 1; + } + return 0; + }); + return results; + }; +}; diff --git a/src/groups/update.js b/src/groups/update.js new file mode 100644 index 0000000000..fb659111d7 --- /dev/null +++ b/src/groups/update.js @@ -0,0 +1,291 @@ +'use strict'; + +const winston = require('winston'); + +const categories = require('../categories'); +const plugins = require('../plugins'); +const slugify = require('../slugify'); +const db = require('../database'); +const user = require('../user'); +const batch = require('../batch'); +const meta = require('../meta'); +const cache = require('../cache'); + + +module.exports = function (Groups) { + Groups.update = async function (groupName, values) { + const exists = await db.exists(`group:${groupName}`); + if (!exists) { + throw new Error('[[error:no-group]]'); + } + + ({ values } = await plugins.hooks.fire('filter:group.update', { + groupName: groupName, + values: values, + })); + + // Cast some values as bool (if not boolean already) + // 'true' and '1' = true, everything else false + ['userTitleEnabled', 'private', 'hidden', 'disableJoinRequests', 'disableLeave'].forEach((prop) => { + if (values.hasOwnProperty(prop) && typeof values[prop] !== 'boolean') { + values[prop] = values[prop] === 'true' || parseInt(values[prop], 10) === 1; + } + }); + + const payload = { + description: values.description || '', + icon: values.icon || '', + labelColor: values.labelColor || '#000000', + textColor: values.textColor || '#ffffff', + }; + + if (values.hasOwnProperty('userTitle')) { + payload.userTitle = values.userTitle || ''; + } + + if (values.hasOwnProperty('userTitleEnabled')) { + payload.userTitleEnabled = values.userTitleEnabled ? '1' : '0'; + } + + if (values.hasOwnProperty('hidden')) { + payload.hidden = values.hidden ? '1' : '0'; + } + + if (values.hasOwnProperty('private')) { + payload.private = values.private ? '1' : '0'; + } + + if (values.hasOwnProperty('disableJoinRequests')) { + payload.disableJoinRequests = values.disableJoinRequests ? '1' : '0'; + } + + if (values.hasOwnProperty('disableLeave')) { + payload.disableLeave = values.disableLeave ? '1' : '0'; + } + + if (values.hasOwnProperty('name')) { + await checkNameChange(groupName, values.name); + } + + if (values.hasOwnProperty('private')) { + await updatePrivacy(groupName, values.private); + } + + if (values.hasOwnProperty('hidden')) { + await updateVisibility(groupName, values.hidden); + } + + if (values.hasOwnProperty('memberPostCids')) { + const validCids = await categories.getCidsByPrivilege('categories:cid', groupName, 'topics:read'); + const cidsArray = values.memberPostCids.split(',').map(cid => parseInt(cid.trim(), 10)).filter(Boolean); + payload.memberPostCids = cidsArray.filter(cid => validCids.includes(cid)).join(',') || ''; + } + + await db.setObject(`group:${groupName}`, payload); + await Groups.renameGroup(groupName, values.name); + + plugins.hooks.fire('action:group.update', { + name: groupName, + values: values, + }); + }; + + async function updateVisibility(groupName, hidden) { + if (hidden) { + await db.sortedSetRemoveBulk([ + ['groups:visible:createtime', groupName], + ['groups:visible:memberCount', groupName], + ['groups:visible:name', `${groupName.toLowerCase()}:${groupName}`], + ]); + return; + } + const groupData = await db.getObjectFields(`group:${groupName}`, ['createtime', 'memberCount']); + await db.sortedSetAddBulk([ + ['groups:visible:createtime', groupData.createtime, groupName], + ['groups:visible:memberCount', groupData.memberCount, groupName], + ['groups:visible:name', 0, `${groupName.toLowerCase()}:${groupName}`], + ]); + } + + Groups.hide = async function (groupName) { + await showHide(groupName, 'hidden'); + }; + + Groups.show = async function (groupName) { + await showHide(groupName, 'show'); + }; + + async function showHide(groupName, hidden) { + hidden = hidden === 'hidden'; + await Promise.all([ + db.setObjectField(`group:${groupName}`, 'hidden', hidden ? 1 : 0), + updateVisibility(groupName, hidden), + ]); + } + + async function updatePrivacy(groupName, isPrivate) { + const groupData = await Groups.getGroupFields(groupName, ['private']); + const currentlyPrivate = groupData.private === 1; + if (!currentlyPrivate || currentlyPrivate === isPrivate) { + return; + } + const pendingUids = await db.getSetMembers(`group:${groupName}:pending`); + if (!pendingUids.length) { + return; + } + + winston.verbose(`[groups.update] Group is now public, automatically adding ${pendingUids.length} new members, who were pending prior.`); + + for (const uid of pendingUids) { + /* eslint-disable no-await-in-loop */ + await Groups.join(groupName, uid); + } + await db.delete(`group:${groupName}:pending`); + } + + async function checkNameChange(currentName, newName) { + if (Groups.isPrivilegeGroup(newName)) { + throw new Error('[[error:invalid-group-name]]'); + } + const currentSlug = slugify(currentName); + const newSlug = slugify(newName); + if (currentName === newName || currentSlug === newSlug) { + return; + } + Groups.validateGroupName(newName); + const [group, exists] = await Promise.all([ + Groups.getGroupData(currentName), + Groups.existsBySlug(newSlug), + ]); + + if (exists) { + throw new Error('[[error:group-already-exists]]'); + } + + if (!group) { + throw new Error('[[error:no-group]]'); + } + + if (group.system) { + throw new Error('[[error:not-allowed-to-rename-system-group]]'); + } + } + + Groups.renameGroup = async function (oldName, newName) { + if (oldName === newName || !newName || String(newName).length === 0) { + return; + } + const group = await db.getObject(`group:${oldName}`); + if (!group) { + return; + } + + const exists = await Groups.exists(newName); + if (exists) { + throw new Error('[[error:group-already-exists]]'); + } + + await updateMemberGroupTitles(oldName, newName); + await updateNavigationItems(oldName, newName); + await updateWidgets(oldName, newName); + await updateConfig(oldName, newName); + await db.setObject(`group:${oldName}`, { name: newName, slug: slugify(newName) }); + await db.deleteObjectField('groupslug:groupname', group.slug); + await db.setObjectField('groupslug:groupname', slugify(newName), newName); + + const allGroups = await db.getSortedSetRange('groups:createtime', 0, -1); + const keys = allGroups.map(group => `group:${group}:members`); + await renameGroupsMember(keys, oldName, newName); + cache.del(keys); + + await db.rename(`group:${oldName}`, `group:${newName}`); + await db.rename(`group:${oldName}:members`, `group:${newName}:members`); + await db.rename(`group:${oldName}:owners`, `group:${newName}:owners`); + await db.rename(`group:${oldName}:pending`, `group:${newName}:pending`); + await db.rename(`group:${oldName}:invited`, `group:${newName}:invited`); + await db.rename(`group:${oldName}:member:pids`, `group:${newName}:member:pids`); + + await renameGroupsMember(['groups:createtime', 'groups:visible:createtime', 'groups:visible:memberCount'], oldName, newName); + await renameGroupsMember(['groups:visible:name'], `${oldName.toLowerCase()}:${oldName}`, `${newName.toLowerCase()}:${newName}`); + + plugins.hooks.fire('action:group.rename', { + old: oldName, + new: newName, + }); + Groups.cache.reset(); + }; + + async function updateMemberGroupTitles(oldName, newName) { + await batch.processSortedSet(`group:${oldName}:members`, async (uids) => { + let usersData = await user.getUsersData(uids); + usersData = usersData.filter(userData => userData && userData.groupTitleArray.includes(oldName)); + + usersData.forEach((userData) => { + userData.newTitleArray = userData.groupTitleArray + .map(oldTitle => (oldTitle === oldName ? newName : oldTitle)); + }); + + await Promise.all(usersData.map(u => user.setUserField(u.uid, 'groupTitle', JSON.stringify(u.newTitleArray)))); + }, {}); + } + + async function renameGroupsMember(keys, oldName, newName) { + const isMembers = await db.isMemberOfSortedSets(keys, oldName); + keys = keys.filter((key, index) => isMembers[index]); + if (!keys.length) { + return; + } + const scores = await db.sortedSetsScore(keys, oldName); + await db.sortedSetsRemove(keys, oldName); + await db.sortedSetsAdd(keys, scores, newName); + } + + async function updateNavigationItems(oldName, newName) { + const navigation = require('../navigation/admin'); + const navItems = await navigation.get(); + navItems.forEach((navItem) => { + if (navItem && Array.isArray(navItem.groups) && navItem.groups.includes(oldName)) { + navItem.groups.splice(navItem.groups.indexOf(oldName), 1, newName); + } + }); + navigation.unescapeFields(navItems); + await navigation.save(navItems); + } + + async function updateWidgets(oldName, newName) { + const admin = require('../widgets/admin'); + const widgets = require('../widgets'); + + const data = await admin.get(); + + data.areas.forEach((area) => { + area.widgets = area.data; + area.widgets.forEach((widget) => { + if (widget && widget.data && Array.isArray(widget.data.groups) && + widget.data.groups.includes(oldName)) { + widget.data.groups.splice(widget.data.groups.indexOf(oldName), 1, newName); + } + }); + }); + for (const area of data.areas) { + if (area.data.length) { + await widgets.setArea(area); + } + } + } + + async function updateConfig(oldName, newName) { + if (meta.config.groupsExemptFromPostQueue.includes(oldName)) { + meta.config.groupsExemptFromPostQueue.splice( + meta.config.groupsExemptFromPostQueue.indexOf(oldName), 1, newName + ); + await meta.configs.set('groupsExemptFromPostQueue', meta.config.groupsExemptFromPostQueue); + } + if (meta.config.groupsExemptFromMaintenanceMode.includes(oldName)) { + meta.config.groupsExemptFromMaintenanceMode.splice( + meta.config.groupsExemptFromMaintenanceMode.indexOf(oldName), 1, newName + ); + await meta.configs.set('groupsExemptFromMaintenanceMode', meta.config.groupsExemptFromMaintenanceMode); + } + } +}; diff --git a/src/groups/user.js b/src/groups/user.js new file mode 100644 index 0000000000..3561423b69 --- /dev/null +++ b/src/groups/user.js @@ -0,0 +1,67 @@ +'use strict'; + +const db = require('../database'); +const user = require('../user'); + +module.exports = function (Groups) { + Groups.getUsersFromSet = async function (set, fields) { + const uids = await db.getSetMembers(set); + + if (fields) { + return await user.getUsersFields(uids, fields); + } + return await user.getUsersData(uids); + }; + + Groups.getUserGroups = async function (uids) { + return await Groups.getUserGroupsFromSet('groups:visible:createtime', uids); + }; + + Groups.getUserGroupsFromSet = async function (set, uids) { + const memberOf = await Groups.getUserGroupMembership(set, uids); + return await Promise.all(memberOf.map(memberOf => Groups.getGroupsData(memberOf))); + }; + + Groups.getUserGroupMembership = async function (set, uids) { + const groupNames = await db.getSortedSetRevRange(set, 0, -1); + return await Promise.all(uids.map(uid => findUserGroups(uid, groupNames))); + }; + + async function findUserGroups(uid, groupNames) { + const isMembers = await Groups.isMemberOfGroups(uid, groupNames); + return groupNames.filter((name, i) => isMembers[i]); + } + + Groups.getUserInviteGroups = async function (uid) { + let allGroups = await Groups.getNonPrivilegeGroups('groups:createtime', 0, -1); + allGroups = allGroups.filter(group => !Groups.ephemeralGroups.includes(group.name)); + + const publicGroups = allGroups.filter(group => group.hidden === 0 && group.system === 0 && group.private === 0); + const adminModGroups = [ + { name: 'administrators', displayName: 'administrators' }, + { name: 'Global Moderators', displayName: 'Global Moderators' }, + ]; + // Private (but not hidden) + const privateGroups = allGroups.filter(group => group.hidden === 0 && + group.system === 0 && group.private === 1); + + const [ownership, isAdmin, isGlobalMod] = await Promise.all([ + Promise.all(privateGroups.map(group => Groups.ownership.isOwner(uid, group.name))), + user.isAdministrator(uid), + user.isGlobalModerator(uid), + ]); + const ownGroups = privateGroups.filter((group, index) => ownership[index]); + + let inviteGroups = []; + if (isAdmin) { + inviteGroups = inviteGroups.concat(adminModGroups).concat(privateGroups); + } else if (isGlobalMod) { + inviteGroups = inviteGroups.concat(privateGroups); + } else { + inviteGroups = inviteGroups.concat(ownGroups); + } + + return inviteGroups + .concat(publicGroups); + }; +}; diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000000..b072c4e648 --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = require('../public/src/modules/helpers.common')( + require('./utils'), + require('benchpressjs'), + require('nconf').get('relative_path'), +); diff --git a/src/image.js b/src/image.js new file mode 100644 index 0000000000..4a70190361 --- /dev/null +++ b/src/image.js @@ -0,0 +1,182 @@ +'use strict'; + +const os = require('os'); +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const winston = require('winston'); + +const file = require('./file'); +const plugins = require('./plugins'); +const meta = require('./meta'); + +const image = module.exports; + +function requireSharp() { + const sharp = require('sharp'); + if (os.platform() === 'win32') { + // https://github.com/lovell/sharp/issues/1259 + sharp.cache(false); + } + return sharp; +} + +image.isFileTypeAllowed = async function (path) { + const plugins = require('./plugins'); + if (plugins.hooks.hasListeners('filter:image.isFileTypeAllowed')) { + return await plugins.hooks.fire('filter:image.isFileTypeAllowed', path); + } + const sharp = require('sharp'); + await sharp(path, { + failOnError: true, + }).metadata(); +}; + +image.resizeImage = async function (data) { + if (plugins.hooks.hasListeners('filter:image.resize')) { + await plugins.hooks.fire('filter:image.resize', { + path: data.path, + target: data.target, + width: data.width, + height: data.height, + quality: data.quality, + }); + } else { + const sharp = requireSharp(); + const buffer = await fs.promises.readFile(data.path); + const sharpImage = sharp(buffer, { + failOnError: true, + animated: data.path.endsWith('gif'), + }); + const metadata = await sharpImage.metadata(); + + sharpImage.rotate(); // auto-orients based on exif data + sharpImage.resize(data.hasOwnProperty('width') ? data.width : null, data.hasOwnProperty('height') ? data.height : null); + + if (data.quality) { + switch (metadata.format) { + case 'jpeg': { + sharpImage.jpeg({ + quality: data.quality, + mozjpeg: true, + }); + break; + } + + case 'png': { + sharpImage.png({ + quality: data.quality, + compressionLevel: 9, + }); + break; + } + } + } + + await sharpImage.toFile(data.target || data.path); + } +}; + +image.normalise = async function (path) { + if (plugins.hooks.hasListeners('filter:image.normalise')) { + await plugins.hooks.fire('filter:image.normalise', { + path: path, + }); + } else { + const sharp = requireSharp(); + await sharp(path, { failOnError: true }).png().toFile(`${path}.png`); + } + return `${path}.png`; +}; + +image.size = async function (path) { + let imageData; + if (plugins.hooks.hasListeners('filter:image.size')) { + imageData = await plugins.hooks.fire('filter:image.size', { + path: path, + }); + } else { + const sharp = requireSharp(); + imageData = await sharp(path, { failOnError: true }).metadata(); + } + return imageData ? { width: imageData.width, height: imageData.height } : undefined; +}; + +image.stripEXIF = async function (path) { + if (!meta.config.stripEXIFData || path.endsWith('.gif') || path.endsWith('.svg')) { + return; + } + try { + if (plugins.hooks.hasListeners('filter:image.stripEXIF')) { + await plugins.hooks.fire('filter:image.stripEXIF', { + path: path, + }); + return; + } + const buffer = await fs.promises.readFile(path); + const sharp = requireSharp(); + await sharp(buffer, { failOnError: true }).rotate().toFile(path); + } catch (err) { + winston.error(err.stack); + } +}; + +image.checkDimensions = async function (path) { + const meta = require('./meta'); + const result = await image.size(path); + + if (result.width > meta.config.rejectImageWidth || result.height > meta.config.rejectImageHeight) { + throw new Error('[[error:invalid-image-dimensions]]'); + } + + return result; +}; + +image.convertImageToBase64 = async function (path) { + return await fs.promises.readFile(path, 'base64'); +}; + +image.mimeFromBase64 = function (imageData) { + return imageData.slice(5, imageData.indexOf('base64') - 1); +}; + +image.extensionFromBase64 = function (imageData) { + return file.typeToExtension(image.mimeFromBase64(imageData)); +}; + +image.writeImageDataToTempFile = async function (imageData) { + const filename = crypto.createHash('md5').update(imageData).digest('hex'); + + const type = image.mimeFromBase64(imageData); + const extension = file.typeToExtension(type); + + const filepath = path.join(os.tmpdir(), filename + extension); + + const buffer = Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64'); + + await fs.promises.writeFile(filepath, buffer, { encoding: 'base64' }); + return filepath; +}; + +image.sizeFromBase64 = function (imageData) { + return Buffer.from(imageData.slice(imageData.indexOf('base64') + 7), 'base64').length; +}; + +image.uploadImage = async function (filename, folder, imageData) { + if (plugins.hooks.hasListeners('filter:uploadImage')) { + return await plugins.hooks.fire('filter:uploadImage', { + image: imageData, + uid: imageData.uid, + folder: folder, + }); + } + await image.isFileTypeAllowed(imageData.path); + const upload = await file.saveFileToLocal(filename, folder, imageData.path); + return { + url: upload.url, + path: upload.path, + name: imageData.name, + }; +}; + +require('./promisify')(image); diff --git a/src/install.js b/src/install.js new file mode 100644 index 0000000000..5bf7863ff6 --- /dev/null +++ b/src/install.js @@ -0,0 +1,618 @@ +'use strict'; + +const fs = require('fs'); +const url = require('url'); +const path = require('path'); +const prompt = require('prompt'); +const winston = require('winston'); +const nconf = require('nconf'); +const _ = require('lodash'); + +const utils = require('./utils'); + +const install = module.exports; +const questions = {}; + +questions.main = [ + { + name: 'url', + description: 'URL used to access this NodeBB', + default: + nconf.get('url') || 'http://127.0.0.1:4567', + pattern: /^http(?:s)?:\/\//, + message: 'Base URL must begin with \'http://\' or \'https://\'', + }, + { + name: 'secret', + description: 'Please enter a NodeBB secret', + default: nconf.get('secret') || utils.generateUUID(), + }, + { + name: 'submitPluginUsage', + description: 'Would you like to submit anonymous plugin usage to nbbpm?', + default: 'yes', + }, + { + name: 'database', + description: 'Which database to use', + default: nconf.get('database') || 'mongo', + }, +]; + +questions.optional = [ + { + name: 'port', + default: nconf.get('port') || 4567, + }, +]; + +function checkSetupFlagEnv() { + let setupVal = install.values; + + const envConfMap = { + NODEBB_URL: 'url', + NODEBB_PORT: 'port', + NODEBB_ADMIN_USERNAME: 'admin:username', + NODEBB_ADMIN_PASSWORD: 'admin:password', + NODEBB_ADMIN_EMAIL: 'admin:email', + NODEBB_DB: 'database', + NODEBB_DB_HOST: 'host', + NODEBB_DB_PORT: 'port', + NODEBB_DB_USER: 'username', + NODEBB_DB_PASSWORD: 'password', + NODEBB_DB_NAME: 'database', + NODEBB_DB_SSL: 'ssl', + }; + + // Set setup values from env vars (if set) + const envKeys = Object.keys(process.env); + if (Object.keys(envConfMap).some(key => envKeys.includes(key))) { + winston.info('[install/checkSetupFlagEnv] checking env vars for setup info...'); + setupVal = setupVal || {}; + + Object.entries(process.env).forEach(([evName, evValue]) => { // get setup values from env + if (evName.startsWith('NODEBB_DB_')) { + setupVal[`${process.env.NODEBB_DB}:${envConfMap[evName]}`] = evValue; + } else if (evName.startsWith('NODEBB_')) { + setupVal[envConfMap[evName]] = evValue; + } + }); + + setupVal['admin:password:confirm'] = setupVal['admin:password']; + } + + // try to get setup values from json, if successful this overwrites all values set by env + // TODO: better behaviour would be to support overrides per value, i.e. in order of priority (generic pattern): + // flag, env, config file, default + try { + if (nconf.get('setup')) { + const setupJSON = JSON.parse(nconf.get('setup')); + setupVal = { ...setupVal, ...setupJSON }; + } + } catch (err) { + winston.error('[install/checkSetupFlagEnv] invalid json in nconf.get(\'setup\'), ignoring setup values from json'); + } + + if (setupVal && typeof setupVal === 'object') { + if (setupVal['admin:username'] && setupVal['admin:password'] && setupVal['admin:password:confirm'] && setupVal['admin:email']) { + install.values = setupVal; + } else { + winston.error('[install/checkSetupFlagEnv] required values are missing for automated setup:'); + if (!setupVal['admin:username']) { + winston.error(' admin:username'); + } + if (!setupVal['admin:password']) { + winston.error(' admin:password'); + } + if (!setupVal['admin:password:confirm']) { + winston.error(' admin:password:confirm'); + } + if (!setupVal['admin:email']) { + winston.error(' admin:email'); + } + + process.exit(); + } + } else if (nconf.get('database')) { + install.values = install.values || {}; + install.values.database = nconf.get('database'); + } +} + +function checkCIFlag() { + let ciVals; + try { + ciVals = JSON.parse(nconf.get('ci')); + } catch (e) { + ciVals = undefined; + } + + if (ciVals && ciVals instanceof Object) { + if (ciVals.hasOwnProperty('host') && ciVals.hasOwnProperty('port') && ciVals.hasOwnProperty('database')) { + install.ciVals = ciVals; + } else { + winston.error('[install/checkCIFlag] required values are missing for automated CI integration:'); + if (!ciVals.hasOwnProperty('host')) { + winston.error(' host'); + } + if (!ciVals.hasOwnProperty('port')) { + winston.error(' port'); + } + if (!ciVals.hasOwnProperty('database')) { + winston.error(' database'); + } + + process.exit(); + } + } +} + +async function setupConfig() { + const configureDatabases = require('../install/databases'); + + // prompt prepends "prompt: " to questions, let's clear that. + prompt.start(); + prompt.message = ''; + prompt.delimiter = ''; + prompt.colors = false; + let config = {}; + + if (install.values) { + // Use provided values, fall back to defaults + const redisQuestions = require('./database/redis').questions; + const mongoQuestions = require('./database/mongo').questions; + const postgresQuestions = require('./database/postgres').questions; + const allQuestions = [ + ...questions.main, + ...questions.optional, + ...redisQuestions, + ...mongoQuestions, + ...postgresQuestions, + ]; + + allQuestions.forEach((question) => { + if (install.values.hasOwnProperty(question.name)) { + config[question.name] = install.values[question.name]; + } else if (question.hasOwnProperty('default')) { + config[question.name] = question.default; + } else { + config[question.name] = undefined; + } + }); + } else { + config = await prompt.get(questions.main); + } + await configureDatabases(config); + await completeConfigSetup(config); +} + +async function completeConfigSetup(config) { + // Add CI object + if (install.ciVals) { + config.test_database = { ...install.ciVals }; + } + + // Add package_manager object if set + if (nconf.get('package_manager')) { + config.package_manager = nconf.get('package_manager'); + } + nconf.overrides(config); + const db = require('./database'); + await db.init(); + if (db.hasOwnProperty('createIndices')) { + await db.createIndices(); + } + + // Sanity-check/fix url/port + if (!/^http(?:s)?:\/\//.test(config.url)) { + config.url = `http://${config.url}`; + } + + // If port is explicitly passed via install vars, use it. Otherwise, glean from url if set. + const urlObj = url.parse(config.url); + if (urlObj.port && (!install.values || !install.values.hasOwnProperty('port'))) { + config.port = urlObj.port; + } + + // Remove trailing slash from non-subfolder installs + if (urlObj.path === '/') { + urlObj.path = ''; + urlObj.pathname = ''; + } + + config.url = url.format(urlObj); + + // ref: https://github.com/indexzero/nconf/issues/300 + delete config.type; + + const meta = require('./meta'); + await meta.configs.set('submitPluginUsage', config.submitPluginUsage === 'yes' ? 1 : 0); + delete config.submitPluginUsage; + + await install.save(config); +} + +async function setupDefaultConfigs() { + console.log('Populating database with default configs, if not already set...'); + const meta = require('./meta'); + const defaults = require(path.join(__dirname, '../', 'install/data/defaults.json')); + + await meta.configs.setOnEmpty(defaults); + await meta.configs.init(); +} + +async function enableDefaultTheme() { + const meta = require('./meta'); + + const id = await meta.configs.get('theme:id'); + if (id) { + console.log('Previous theme detected, skipping enabling default theme'); + return; + } + + const defaultTheme = nconf.get('defaultTheme') || 'nodebb-theme-persona'; + console.log(`Enabling default theme: ${defaultTheme}`); + await meta.themes.set({ + type: 'local', + id: defaultTheme, + }); +} + +async function createDefaultUserGroups() { + const groups = require('./groups'); + async function createGroup(name) { + await groups.create({ + name: name, + hidden: 1, + private: 1, + system: 1, + disableLeave: 1, + disableJoinRequests: 1, + }); + } + + const [verifiedExists, unverifiedExists, bannedExists] = await groups.exists([ + 'verified-users', 'unverified-users', 'banned-users', + ]); + if (!verifiedExists) { + await createGroup('verified-users'); + } + + if (!unverifiedExists) { + await createGroup('unverified-users'); + } + + if (!bannedExists) { + await createGroup('banned-users'); + } +} + +async function createAdministrator() { + const Groups = require('./groups'); + const memberCount = await Groups.getMemberCount('administrators'); + if (memberCount > 0) { + console.log('Administrator found, skipping Admin setup'); + return; + } + return await createAdmin(); +} + +async function createAdmin() { + const User = require('./user'); + const Groups = require('./groups'); + let password; + + winston.warn('No administrators have been detected, running initial user setup\n'); + + let questions = [{ + name: 'username', + description: 'Administrator username', + required: true, + type: 'string', + }, { + name: 'email', + description: 'Administrator email address', + pattern: /.+@.+/, + required: true, + }]; + const passwordQuestions = [{ + name: 'password', + description: 'Password', + required: true, + hidden: true, + type: 'string', + }, { + name: 'password:confirm', + description: 'Confirm Password', + required: true, + hidden: true, + type: 'string', + }]; + + async function success(results) { + if (!results) { + throw new Error('aborted'); + } + + if (results['password:confirm'] !== results.password) { + winston.warn('Passwords did not match, please try again'); + return await retryPassword(results); + } + + try { + User.isPasswordValid(results.password); + } catch (err) { + const [namespace, key] = err.message.slice(2, -2).split(':', 2); + if (namespace && key && err.message.startsWith('[[') && err.message.endsWith(']]')) { + const lang = require(path.join(__dirname, `../public/language/en-GB/${namespace}`)); + if (lang && lang[key]) { + err.message = lang[key]; + } + } + + winston.warn(`Password error, please try again. ${err.message}`); + return await retryPassword(results); + } + + const adminUid = await User.create({ + username: results.username, + password: results.password, + email: results.email, + }); + await Groups.join('administrators', adminUid); + await Groups.show('administrators'); + await Groups.ownership.grant(adminUid, 'administrators'); + + return password ? results : undefined; + } + + async function retryPassword(originalResults) { + // Ask only the password questions + const results = await prompt.get(passwordQuestions); + + // Update the original data with newly collected password + originalResults.password = results.password; + originalResults['password:confirm'] = results['password:confirm']; + + // Send back to success to handle + return await success(originalResults); + } + + // Add the password questions + questions = questions.concat(passwordQuestions); + + if (!install.values) { + const results = await prompt.get(questions); + return await success(results); + } + // If automated setup did not provide a user password, generate one, + // it will be shown to the user upon setup completion + if (!install.values.hasOwnProperty('admin:password') && !nconf.get('admin:password')) { + console.log('Password was not provided during automated setup, generating one...'); + password = utils.generateUUID().slice(0, 8); + } + + const results = { + username: install.values['admin:username'] || nconf.get('admin:username') || 'admin', + email: install.values['admin:email'] || nconf.get('admin:email') || '', + password: install.values['admin:password'] || nconf.get('admin:password') || password, + 'password:confirm': install.values['admin:password:confirm'] || nconf.get('admin:password') || password, + }; + + return await success(results); +} + +async function createGlobalModeratorsGroup() { + const groups = require('./groups'); + const exists = await groups.exists('Global Moderators'); + if (exists) { + winston.info('Global Moderators group found, skipping creation!'); + } else { + await groups.create({ + name: 'Global Moderators', + userTitle: 'Global Moderator', + description: 'Forum wide moderators', + hidden: 0, + private: 1, + disableJoinRequests: 1, + }); + } + await groups.show('Global Moderators'); +} + +async function giveGlobalPrivileges() { + const privileges = require('./privileges'); + const defaultPrivileges = [ + 'groups:chat', 'groups:upload:post:image', 'groups:signature', 'groups:search:content', + 'groups:search:users', 'groups:search:tags', 'groups:view:users', 'groups:view:tags', 'groups:view:groups', + 'groups:local:login', + ]; + await privileges.global.give(defaultPrivileges, 'registered-users'); + await privileges.global.give(defaultPrivileges.concat([ + 'groups:ban', 'groups:upload:post:file', 'groups:view:users:info', + ]), 'Global Moderators'); + await privileges.global.give(['groups:view:users', 'groups:view:tags', 'groups:view:groups'], 'guests'); + await privileges.global.give(['groups:view:users', 'groups:view:tags', 'groups:view:groups'], 'spiders'); +} + +async function createCategories() { + const Categories = require('./categories'); + const db = require('./database'); + const cids = await db.getSortedSetRange('categories:cid', 0, -1); + if (Array.isArray(cids) && cids.length) { + console.log(`Categories OK. Found ${cids.length} categories.`); + return; + } + + console.log('No categories found, populating instance with default categories'); + + const default_categories = JSON.parse( + await fs.promises.readFile(path.join(__dirname, '../', 'install/data/categories.json'), 'utf8') + ); + for (const categoryData of default_categories) { + // eslint-disable-next-line no-await-in-loop + await Categories.create(categoryData); + } +} + +async function createMenuItems() { + const db = require('./database'); + + const exists = await db.exists('navigation:enabled'); + if (exists) { + return; + } + const navigation = require('./navigation/admin'); + const data = require('../install/data/navigation.json'); + await navigation.save(data); +} + +async function createWelcomePost() { + const db = require('./database'); + const Topics = require('./topics'); + + const [content, numTopics] = await Promise.all([ + fs.promises.readFile(path.join(__dirname, '../', 'install/data/welcome.md'), 'utf8'), + db.getObjectField('global', 'topicCount'), + ]); + + if (!parseInt(numTopics, 10)) { + console.log('Creating welcome post!'); + await Topics.post({ + uid: 1, + cid: 2, + title: 'Welcome to your NodeBB!', + content: content, + }); + } +} + +async function enableDefaultPlugins() { + console.log('Enabling default plugins'); + + let defaultEnabled = [ + 'nodebb-plugin-composer-default', + 'nodebb-plugin-markdown', + 'nodebb-plugin-mentions', + 'nodebb-widget-essentials', + 'nodebb-rewards-essentials', + 'nodebb-plugin-emoji', + 'nodebb-plugin-emoji-android', + ]; + let customDefaults = nconf.get('defaultplugins') || nconf.get('defaultPlugins'); + + winston.info(`[install/defaultPlugins] customDefaults ${String(customDefaults)}`); + + if (customDefaults && customDefaults.length) { + try { + customDefaults = Array.isArray(customDefaults) ? customDefaults : JSON.parse(customDefaults); + defaultEnabled = defaultEnabled.concat(customDefaults); + } catch (e) { + // Invalid value received + winston.info('[install/enableDefaultPlugins] Invalid defaultPlugins value received. Ignoring.'); + } + } + + defaultEnabled = _.uniq(defaultEnabled); + + winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled); + + const db = require('./database'); + const order = defaultEnabled.map((plugin, index) => index); + await db.sortedSetAdd('plugins:active', order, defaultEnabled); +} + +async function setCopyrightWidget() { + const db = require('./database'); + const [footerJSON, footer] = await Promise.all([ + fs.promises.readFile(path.join(__dirname, '../', 'install/data/footer.json'), 'utf8'), + db.getObjectField('widgets:global', 'footer'), + ]); + + if (!footer && footerJSON) { + await db.setObjectField('widgets:global', 'footer', footerJSON); + } +} + +async function copyFavicon() { + const file = require('./file'); + const pathToIco = path.join(nconf.get('upload_path'), 'system', 'favicon.ico'); + const defaultIco = path.join(nconf.get('base_dir'), 'public', 'favicon.ico'); + const targetExists = await file.exists(pathToIco); + const defaultExists = await file.exists(defaultIco); + + if (defaultExists && !targetExists) { + try { + await fs.promises.copyFile(defaultIco, pathToIco); + } catch (err) { + winston.error(`Cannot copy favicon.ico\n${err.stack}`); + } + } +} + +async function checkUpgrade() { + const upgrade = require('./upgrade'); + try { + await upgrade.check(); + } catch (err) { + if (err.message === 'schema-out-of-date') { + await upgrade.run(); + return; + } + throw err; + } +} + +install.setup = async function () { + try { + checkSetupFlagEnv(); + checkCIFlag(); + await setupConfig(); + await setupDefaultConfigs(); + await enableDefaultTheme(); + await createCategories(); + await createDefaultUserGroups(); + const adminInfo = await createAdministrator(); + await createGlobalModeratorsGroup(); + await giveGlobalPrivileges(); + await createMenuItems(); + await createWelcomePost(); + await enableDefaultPlugins(); + await setCopyrightWidget(); + await copyFavicon(); + await checkUpgrade(); + + const data = { + ...adminInfo, + }; + return data; + } catch (err) { + if (err) { + winston.warn(`NodeBB Setup Aborted.\n ${err.stack}`); + process.exit(1); + } + } +}; + +install.save = async function (server_conf) { + let serverConfigPath = path.join(__dirname, '../config.json'); + + if (nconf.get('config')) { + serverConfigPath = path.resolve(__dirname, '../', nconf.get('config')); + } + + let currentConfig = {}; + try { + currentConfig = require(serverConfigPath); + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err; + } + } + + await fs.promises.writeFile(serverConfigPath, JSON.stringify({ ...currentConfig, ...server_conf }, null, 4)); + console.log('Configuration Saved OK'); + nconf.file({ + file: serverConfigPath, + }); +}; diff --git a/src/languages.js b/src/languages.js new file mode 100644 index 0000000000..eeb01d090f --- /dev/null +++ b/src/languages.js @@ -0,0 +1,87 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const utils = require('./utils'); +const { paths } = require('./constants'); +const plugins = require('./plugins'); + +const Languages = module.exports; +const languagesPath = path.join(__dirname, '../build/public/language'); + +const files = fs.readdirSync(path.join(paths.nodeModules, '/timeago/locales')); +Languages.timeagoCodes = files.filter(f => f.startsWith('jquery.timeago')).map(f => f.split('.')[2]); + +Languages.get = async function (language, namespace) { + const pathToLanguageFile = path.join(languagesPath, language, `${namespace}.json`); + if (!pathToLanguageFile.startsWith(languagesPath)) { + throw new Error('[[error:invalid-path]]'); + } + const data = await fs.promises.readFile(pathToLanguageFile, 'utf8'); + const parsed = JSON.parse(data) || {}; + const result = await plugins.hooks.fire('filter:languages.get', { + language, + namespace, + data: parsed, + }); + return result.data; +}; + +let codeCache = null; +Languages.listCodes = async function () { + if (codeCache && codeCache.length) { + return codeCache; + } + try { + const file = await fs.promises.readFile(path.join(languagesPath, 'metadata.json'), 'utf8'); + const parsed = JSON.parse(file); + + codeCache = parsed.languages; + return parsed.languages; + } catch (err) { + if (err.code === 'ENOENT') { + return []; + } + throw err; + } +}; + +let listCache = null; +Languages.list = async function () { + if (listCache && listCache.length) { + return listCache; + } + + const codes = await Languages.listCodes(); + + let languages = await Promise.all(codes.map(async (folder) => { + try { + const configPath = path.join(languagesPath, folder, 'language.json'); + const file = await fs.promises.readFile(configPath, 'utf8'); + const lang = JSON.parse(file); + return lang; + } catch (err) { + if (err.code === 'ENOENT') { + return; + } + throw err; + } + })); + + // filter out invalid ones + languages = languages.filter(lang => lang && lang.code && lang.name && lang.dir); + + listCache = languages; + return languages; +}; + +Languages.userTimeagoCode = async function (userLang) { + const languageCodes = await Languages.listCodes(); + const timeagoCode = utils.userLangToTimeagoCode(userLang); + if (languageCodes.includes(userLang) && Languages.timeagoCodes.includes(timeagoCode)) { + return timeagoCode; + } + return ''; +}; + +require('./promisify')(Languages); diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000000..6963b9accc --- /dev/null +++ b/src/logger.js @@ -0,0 +1,217 @@ +'use strict'; + +/* + * Logger module: ability to dynamically turn on/off logging for http requests & socket.io events + */ + +const fs = require('fs'); +const path = require('path'); +const winston = require('winston'); +const util = require('util'); +const morgan = require('morgan'); + +const file = require('./file'); +const meta = require('./meta'); + + +const opts = { + /* + * state used by Logger + */ + express: { + app: {}, + set: 0, + ofn: null, + }, + streams: { + log: { f: process.stdout }, + }, +}; + +/* -- Logger -- */ +const Logger = module.exports; + +Logger.init = function (app) { + opts.express.app = app; + /* Open log file stream & initialize express logging if meta.config.logger* variables are set */ + Logger.setup(); +}; + +Logger.setup = function () { + Logger.setup_one('loggerPath', meta.config.loggerPath); +}; + +Logger.setup_one = function (key, value) { + /* + * 1. Open the logger stream: stdout or file + * 2. Re-initialize the express logger hijack + */ + if (key === 'loggerPath') { + Logger.setup_one_log(value); + Logger.express_open(); + } +}; + +Logger.setup_one_log = function (value) { + /* + * If logging is currently enabled, create a stream. + * Otherwise, close the current stream + */ + if (meta.config.loggerStatus > 0 || meta.config.loggerIOStatus) { + const stream = Logger.open(value); + if (stream) { + opts.streams.log.f = stream; + } else { + opts.streams.log.f = process.stdout; + } + } else { + Logger.close(opts.streams.log); + } +}; + +Logger.open = function (value) { + /* Open the streams to log to: either a path or stdout */ + let stream; + if (value) { + if (file.existsSync(value)) { + const stats = fs.statSync(value); + if (stats) { + if (stats.isDirectory()) { + stream = fs.createWriteStream(path.join(value, 'nodebb.log'), { flags: 'a' }); + } else { + stream = fs.createWriteStream(value, { flags: 'a' }); + } + } + } else { + stream = fs.createWriteStream(value, { flags: 'a' }); + } + + if (stream) { + stream.on('error', (err) => { + winston.error(err.stack); + }); + } + } else { + stream = process.stdout; + } + return stream; +}; + +Logger.close = function (stream) { + if (stream.f !== process.stdout && stream.f) { + stream.end(); + } + stream.f = null; +}; + +Logger.monitorConfig = function (socket, data) { + /* + * This monitor's when a user clicks "save" in the Logger section of the admin panel + */ + Logger.setup_one(data.key, data.value); + Logger.io_close(socket); + Logger.io(socket); +}; + +Logger.express_open = function () { + if (opts.express.set !== 1) { + opts.express.set = 1; + opts.express.app.use(Logger.expressLogger); + } + /* + * Always initialize "ofn" (original function) with the original logger function + */ + opts.express.ofn = morgan('combined', { stream: opts.streams.log.f }); +}; + +Logger.expressLogger = function (req, res, next) { + /* + * The new express.logger + * + * This hijack allows us to turn logger on/off dynamically within express + */ + if (meta.config.loggerStatus > 0) { + return opts.express.ofn(req, res, next); + } + return next(); +}; + +Logger.prepare_io_string = function (_type, _uid, _args) { + /* + * This prepares the output string for intercepted socket.io events + * + * The format is: io: + */ + try { + return `io: ${_uid} ${_type} ${util.inspect(Array.prototype.slice.call(_args), { depth: 3 })}\n`; + } catch (err) { + winston.info('Logger.prepare_io_string: Failed', err); + return 'error'; + } +}; + +Logger.io_close = function (socket) { + /* + * Restore all hijacked sockets to their original emit/on functions + */ + if (!socket || !socket.io || !socket.io.sockets || !socket.io.sockets.sockets) { + return; + } + + const clientsMap = socket.io.sockets.sockets; + + for (const [, client] of clientsMap) { + if (client.oEmit && client.oEmit !== client.emit) { + client.emit = client.oEmit; + } + + if (client.$onevent && client.$onevent !== client.onevent) { + client.onevent = client.$onevent; + } + } +}; + +Logger.io = function (socket) { + /* + * Go through all of the currently established sockets & hook their .emit/.on + */ + + if (!socket || !socket.io || !socket.io.sockets || !socket.io.sockets.sockets) { + return; + } + + const clientsMap = socket.io.sockets.sockets; + for (const [, socketObj] of clientsMap) { + Logger.io_one(socketObj, socketObj.uid); + } +}; + +Logger.io_one = function (socket, uid) { + /* + * This function replaces a socket's .emit/.on functions in order to intercept events + */ + function override(method, name, errorMsg) { + return (...args) => { + if (opts.streams.log.f) { + opts.streams.log.f.write(Logger.prepare_io_string(name, uid, args)); + } + + try { + method.apply(socket, args); + } catch (err) { + winston.info(errorMsg, err); + } + }; + } + + if (socket && meta.config.loggerIOStatus > 0) { + // courtesy of: http://stackoverflow.com/a/9674248 + socket.oEmit = socket.emit; + const { emit } = socket; + socket.emit = override(emit, 'emit', 'Logger.io_one: emit.apply: Failed'); + + socket.$onvent = socket.onevent; + const $onevent = socket.onevent; + socket.onevent = override($onevent, 'on', 'Logger.io_one: $emit.apply: Failed'); + } +}; diff --git a/src/messaging/create.js b/src/messaging/create.js new file mode 100644 index 0000000000..2a655e6afe --- /dev/null +++ b/src/messaging/create.js @@ -0,0 +1,102 @@ +'use strict'; + +const meta = require('../meta'); +const plugins = require('../plugins'); +const db = require('../database'); +const user = require('../user'); + +module.exports = function (Messaging) { + Messaging.sendMessage = async (data) => { + await Messaging.checkContent(data.content); + const inRoom = await Messaging.isUserInRoom(data.uid, data.roomId); + if (!inRoom) { + throw new Error('[[error:not-allowed]]'); + } + + return await Messaging.addMessage(data); + }; + + Messaging.checkContent = async (content) => { + if (!content) { + throw new Error('[[error:invalid-chat-message]]'); + } + + const maximumChatMessageLength = meta.config.maximumChatMessageLength || 1000; + content = String(content).trim(); + let { length } = content; + ({ content, length } = await plugins.hooks.fire('filter:messaging.checkContent', { content, length })); + if (!content) { + throw new Error('[[error:invalid-chat-message]]'); + } + if (length > maximumChatMessageLength) { + throw new Error(`[[error:chat-message-too-long, ${maximumChatMessageLength}]]`); + } + }; + + Messaging.addMessage = async (data) => { + const mid = await db.incrObjectField('global', 'nextMid'); + const timestamp = data.timestamp || Date.now(); + let message = { + content: String(data.content), + timestamp: timestamp, + fromuid: data.uid, + roomId: data.roomId, + deleted: 0, + system: data.system || 0, + }; + + if (data.ip) { + message.ip = data.ip; + } + + message = await plugins.hooks.fire('filter:messaging.save', message); + await db.setObject(`message:${mid}`, message); + const isNewSet = await Messaging.isNewSet(data.uid, data.roomId, timestamp); + let uids = await db.getSortedSetRange(`chat:room:${data.roomId}:uids`, 0, -1); + uids = await user.blocks.filterUids(data.uid, uids); + + await Promise.all([ + Messaging.addRoomToUsers(data.roomId, uids, timestamp), + Messaging.addMessageToUsers(data.roomId, uids, mid, timestamp), + Messaging.markUnread(uids.filter(uid => uid !== String(data.uid)), data.roomId), + ]); + + const messages = await Messaging.getMessagesData([mid], data.uid, data.roomId, true); + if (!messages || !messages[0]) { + return null; + } + + messages[0].newSet = isNewSet; + messages[0].mid = mid; + messages[0].roomId = data.roomId; + plugins.hooks.fire('action:messaging.save', { message: messages[0], data: data }); + return messages[0]; + }; + + Messaging.addSystemMessage = async (content, uid, roomId) => { + const message = await Messaging.addMessage({ + content: content, + uid: uid, + roomId: roomId, + system: 1, + }); + Messaging.notifyUsersInRoom(uid, roomId, message); + }; + + Messaging.addRoomToUsers = async (roomId, uids, timestamp) => { + if (!uids.length) { + return; + } + + const keys = uids.map(uid => `uid:${uid}:chat:rooms`); + await db.sortedSetsAdd(keys, timestamp, roomId); + }; + + Messaging.addMessageToUsers = async (roomId, uids, mid, timestamp) => { + if (!uids.length) { + return; + } + const keys = uids.map(uid => `uid:${uid}:chat:room:${roomId}:mids`); + await db.sortedSetsAdd(keys, timestamp, mid); + }; +}; diff --git a/src/messaging/data.js b/src/messaging/data.js new file mode 100644 index 0000000000..7a7d80ed51 --- /dev/null +++ b/src/messaging/data.js @@ -0,0 +1,156 @@ +'use strict'; + +const validator = require('validator'); + +const db = require('../database'); +const user = require('../user'); +const utils = require('../utils'); +const plugins = require('../plugins'); + +const intFields = ['timestamp', 'edited', 'fromuid', 'roomId', 'deleted', 'system']; + +module.exports = function (Messaging) { + Messaging.newMessageCutoff = 1000 * 60 * 3; + + Messaging.getMessagesFields = async (mids, fields) => { + if (!Array.isArray(mids) || !mids.length) { + return []; + } + + const keys = mids.map(mid => `message:${mid}`); + const messages = await db.getObjects(keys, fields); + + return await Promise.all(messages.map( + async (message, idx) => modifyMessage(message, fields, parseInt(mids[idx], 10)) + )); + }; + + Messaging.getMessageField = async (mid, field) => { + const fields = await Messaging.getMessageFields(mid, [field]); + return fields ? fields[field] : null; + }; + + Messaging.getMessageFields = async (mid, fields) => { + const messages = await Messaging.getMessagesFields([mid], fields); + return messages ? messages[0] : null; + }; + + Messaging.setMessageField = async (mid, field, content) => { + await db.setObjectField(`message:${mid}`, field, content); + }; + + Messaging.setMessageFields = async (mid, data) => { + await db.setObject(`message:${mid}`, data); + }; + + Messaging.getMessagesData = async (mids, uid, roomId, isNew) => { + let messages = await Messaging.getMessagesFields(mids, []); + messages = await user.blocks.filter(uid, 'fromuid', messages); + messages = messages + .map((msg, idx) => { + if (msg) { + msg.messageId = parseInt(mids[idx], 10); + msg.ip = undefined; + } + return msg; + }) + .filter(Boolean); + + const users = await user.getUsersFields( + messages.map(msg => msg && msg.fromuid), + ['uid', 'username', 'userslug', 'picture', 'status', 'banned'] + ); + + messages.forEach((message, index) => { + message.fromUser = users[index]; + message.fromUser.banned = !!message.fromUser.banned; + message.fromUser.deleted = message.fromuid !== message.fromUser.uid && message.fromUser.uid === 0; + + const self = message.fromuid === parseInt(uid, 10); + message.self = self ? 1 : 0; + + message.newSet = false; + message.roomId = String(message.roomId || roomId); + message.deleted = !!message.deleted; + message.system = !!message.system; + }); + + messages = await Promise.all(messages.map(async (message) => { + if (message.system) { + message.content = validator.escape(String(message.content)); + message.cleanedContent = utils.stripHTMLTags(utils.decodeHTMLEntities(message.content)); + return message; + } + + const result = await Messaging.parse(message.content, message.fromuid, uid, roomId, isNew); + message.content = result; + message.cleanedContent = utils.stripHTMLTags(utils.decodeHTMLEntities(result)); + return message; + })); + + if (messages.length > 1) { + // Add a spacer in between messages with time gaps between them + messages = messages.map((message, index) => { + // Compare timestamps with the previous message, and check if a spacer needs to be added + if (index > 0 && message.timestamp > messages[index - 1].timestamp + Messaging.newMessageCutoff) { + // If it's been 5 minutes, this is a new set of messages + message.newSet = true; + } else if (index > 0 && message.fromuid !== messages[index - 1].fromuid) { + // If the previous message was from the other person, this is also a new set + message.newSet = true; + } else if (index === 0) { + message.newSet = true; + } + + return message; + }); + } else if (messages.length === 1) { + // For single messages, we don't know the context, so look up the previous message and compare + const key = `uid:${uid}:chat:room:${roomId}:mids`; + const index = await db.sortedSetRank(key, messages[0].messageId); + if (index > 0) { + const mid = await db.getSortedSetRange(key, index - 1, index - 1); + const fields = await Messaging.getMessageFields(mid, ['fromuid', 'timestamp']); + if ((messages[0].timestamp > fields.timestamp + Messaging.newMessageCutoff) || + (messages[0].fromuid !== fields.fromuid)) { + // If it's been 5 minutes, this is a new set of messages + messages[0].newSet = true; + } + } else { + messages[0].newSet = true; + } + } else { + messages = []; + } + + const data = await plugins.hooks.fire('filter:messaging.getMessages', { + messages: messages, + uid: uid, + roomId: roomId, + isNew: isNew, + mids: mids, + }); + + return data && data.messages; + }; +}; + +async function modifyMessage(message, fields, mid) { + if (message) { + db.parseIntFields(message, intFields, fields); + if (message.hasOwnProperty('timestamp')) { + message.timestampISO = utils.toISOString(message.timestamp); + } + if (message.hasOwnProperty('edited')) { + message.editedISO = utils.toISOString(message.edited); + } + } + + const payload = await plugins.hooks.fire('filter:messaging.getFields', { + mid: mid, + message: message, + fields: fields, + }); + + return payload.message; +} diff --git a/src/messaging/delete.js b/src/messaging/delete.js new file mode 100644 index 0000000000..09eb67ad1d --- /dev/null +++ b/src/messaging/delete.js @@ -0,0 +1,33 @@ +'use strict'; + +const sockets = require('../socket.io'); + +module.exports = function (Messaging) { + Messaging.deleteMessage = async (mid, uid) => await doDeleteRestore(mid, 1, uid); + Messaging.restoreMessage = async (mid, uid) => await doDeleteRestore(mid, 0, uid); + + async function doDeleteRestore(mid, state, uid) { + const field = state ? 'deleted' : 'restored'; + const { deleted, roomId } = await Messaging.getMessageFields(mid, ['deleted', 'roomId']); + if (deleted === state) { + throw new Error(`[[error:chat-${field}-already]]`); + } + + await Messaging.setMessageField(mid, 'deleted', state); + + const [uids, messages] = await Promise.all([ + Messaging.getUidsInRoom(roomId, 0, -1), + Messaging.getMessagesData([mid], uid, roomId, true), + ]); + + uids.forEach((_uid) => { + if (parseInt(_uid, 10) !== parseInt(uid, 10)) { + if (state === 1) { + sockets.in(`uid_${_uid}`).emit('event:chats.delete', mid); + } else if (state === 0) { + sockets.in(`uid_${_uid}`).emit('event:chats.restore', messages[0]); + } + } + }); + } +}; diff --git a/src/messaging/edit.js b/src/messaging/edit.js new file mode 100644 index 0000000000..95492a21ed --- /dev/null +++ b/src/messaging/edit.js @@ -0,0 +1,92 @@ +'use strict'; + +const meta = require('../meta'); +const user = require('../user'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); + +const sockets = require('../socket.io'); + + +module.exports = function (Messaging) { + Messaging.editMessage = async (uid, mid, roomId, content) => { + await Messaging.checkContent(content); + const raw = await Messaging.getMessageField(mid, 'content'); + if (raw === content) { + return; + } + + const payload = await plugins.hooks.fire('filter:messaging.edit', { + content: content, + edited: Date.now(), + }); + + if (!String(payload.content).trim()) { + throw new Error('[[error:invalid-chat-message]]'); + } + await Messaging.setMessageFields(mid, payload); + + // Propagate this change to users in the room + const [uids, messages] = await Promise.all([ + Messaging.getUidsInRoom(roomId, 0, -1), + Messaging.getMessagesData([mid], uid, roomId, true), + ]); + + uids.forEach((uid) => { + sockets.in(`uid_${uid}`).emit('event:chats.edit', { + messages: messages, + }); + }); + }; + + const canEditDelete = async (messageId, uid, type) => { + let durationConfig = ''; + if (type === 'edit') { + durationConfig = 'chatEditDuration'; + } else if (type === 'delete') { + durationConfig = 'chatDeleteDuration'; + } + + const exists = await Messaging.messageExists(messageId); + if (!exists) { + throw new Error('[[error:invalid-mid]]'); + } + + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(uid); + + if (meta.config.disableChat) { + throw new Error('[[error:chat-disabled]]'); + } else if (!isAdminOrGlobalMod && meta.config.disableChatMessageEditing) { + throw new Error('[[error:chat-message-editing-disabled]]'); + } + + const userData = await user.getUserFields(uid, ['banned']); + if (userData.banned) { + throw new Error('[[error:user-banned]]'); + } + + const canChat = await privileges.global.can('chat', uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + + const messageData = await Messaging.getMessageFields(messageId, ['fromuid', 'timestamp', 'system']); + if (isAdminOrGlobalMod && !messageData.system) { + return; + } + + const chatConfigDuration = meta.config[durationConfig]; + if (chatConfigDuration && Date.now() - messageData.timestamp > chatConfigDuration * 1000) { + throw new Error(`[[error:chat-${type}-duration-expired, ${meta.config[durationConfig]}]]`); + } + + if (messageData.fromuid === parseInt(uid, 10) && !messageData.system) { + return; + } + + throw new Error(`[[error:cant-${type}-chat-message]]`); + }; + + Messaging.canEdit = async (messageId, uid) => await canEditDelete(messageId, uid, 'edit'); + Messaging.canDelete = async (messageId, uid) => await canEditDelete(messageId, uid, 'delete'); +}; diff --git a/src/messaging/index.js b/src/messaging/index.js new file mode 100644 index 0000000000..88213eacb8 --- /dev/null +++ b/src/messaging/index.js @@ -0,0 +1,306 @@ +'use strict'; + + +const validator = require('validator'); + +const db = require('../database'); +const user = require('../user'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const utils = require('../utils'); + +const Messaging = module.exports; + +require('./data')(Messaging); +require('./create')(Messaging); +require('./delete')(Messaging); +require('./edit')(Messaging); +require('./rooms')(Messaging); +require('./unread')(Messaging); +require('./notifications')(Messaging); + +Messaging.messageExists = async mid => db.exists(`message:${mid}`); + +Messaging.getMessages = async (params) => { + const isNew = params.isNew || false; + const start = params.hasOwnProperty('start') ? params.start : 0; + const stop = parseInt(start, 10) + ((params.count || 50) - 1); + + const indices = {}; + const ok = await canGet('filter:messaging.canGetMessages', params.callerUid, params.uid); + if (!ok) { + return; + } + + const mids = await db.getSortedSetRevRange(`uid:${params.uid}:chat:room:${params.roomId}:mids`, start, stop); + if (!mids.length) { + return []; + } + mids.forEach((mid, index) => { + indices[mid] = start + index; + }); + mids.reverse(); + + const messageData = await Messaging.getMessagesData(mids, params.uid, params.roomId, isNew); + messageData.forEach((messageData) => { + messageData.index = indices[messageData.messageId.toString()]; + messageData.isOwner = messageData.fromuid === parseInt(params.uid, 10); + if (messageData.deleted && !messageData.isOwner) { + messageData.content = '[[modules:chat.message-deleted]]'; + messageData.cleanedContent = messageData.content; + } + }); + + return messageData; +}; + +async function canGet(hook, callerUid, uid) { + const data = await plugins.hooks.fire(hook, { + callerUid: callerUid, + uid: uid, + canGet: parseInt(callerUid, 10) === parseInt(uid, 10), + }); + + return data ? data.canGet : false; +} + +Messaging.parse = async (message, fromuid, uid, roomId, isNew) => { + const parsed = await plugins.hooks.fire('filter:parse.raw', String(message || '')); + let messageData = { + message: message, + parsed: parsed, + fromuid: fromuid, + uid: uid, + roomId: roomId, + isNew: isNew, + parsedMessage: parsed, + }; + + messageData = await plugins.hooks.fire('filter:messaging.parse', messageData); + return messageData ? messageData.parsedMessage : ''; +}; + +Messaging.isNewSet = async (uid, roomId, timestamp) => { + const setKey = `uid:${uid}:chat:room:${roomId}:mids`; + const messages = await db.getSortedSetRevRangeWithScores(setKey, 0, 0); + if (messages && messages.length) { + return parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + Messaging.newMessageCutoff; + } + return true; +}; + +Messaging.getRecentChats = async (callerUid, uid, start, stop) => { + const ok = await canGet('filter:messaging.canGetRecentChats', callerUid, uid); + if (!ok) { + return null; + } + + const roomIds = await db.getSortedSetRevRange(`uid:${uid}:chat:rooms`, start, stop); + const results = await utils.promiseParallel({ + roomData: Messaging.getRoomsData(roomIds), + unread: db.isSortedSetMembers(`uid:${uid}:chat:rooms:unread`, roomIds), + users: Promise.all(roomIds.map(async (roomId) => { + let uids = await db.getSortedSetRevRange(`chat:room:${roomId}:uids`, 0, 9); + uids = uids.filter(_uid => _uid && parseInt(_uid, 10) !== parseInt(uid, 10)); + return await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline']); + })), + teasers: Promise.all(roomIds.map(async roomId => Messaging.getTeaser(uid, roomId))), + }); + + results.roomData.forEach((room, index) => { + if (room) { + room.users = results.users[index]; + room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; + room.unread = results.unread[index]; + room.teaser = results.teasers[index]; + + room.users.forEach((userData) => { + if (userData && parseInt(userData.uid, 10)) { + userData.status = user.getStatus(userData); + } + }); + room.users = room.users.filter(user => user && parseInt(user.uid, 10)); + room.lastUser = room.users[0]; + + room.usernames = Messaging.generateUsernames(room.users, uid); + } + }); + + results.roomData = results.roomData.filter(Boolean); + const ref = { rooms: results.roomData, nextStart: stop + 1 }; + return await plugins.hooks.fire('filter:messaging.getRecentChats', { + rooms: ref.rooms, + nextStart: ref.nextStart, + uid: uid, + callerUid: callerUid, + }); +}; + +Messaging.generateUsernames = (users, excludeUid) => users.filter(user => user && parseInt(user.uid, 10) !== excludeUid) + .map(user => user.username).join(', '); + +Messaging.getTeaser = async (uid, roomId) => { + const mid = await Messaging.getLatestUndeletedMessage(uid, roomId); + if (!mid) { + return null; + } + const teaser = await Messaging.getMessageFields(mid, ['fromuid', 'content', 'timestamp']); + if (!teaser.fromuid) { + return null; + } + const blocked = await user.blocks.is(teaser.fromuid, uid); + if (blocked) { + return null; + } + + teaser.user = await user.getUserFields(teaser.fromuid, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline']); + if (teaser.content) { + teaser.content = utils.stripHTMLTags(utils.decodeHTMLEntities(teaser.content)); + teaser.content = validator.escape(String(teaser.content)); + } + + const payload = await plugins.hooks.fire('filter:messaging.getTeaser', { teaser: teaser }); + return payload.teaser; +}; + +Messaging.getLatestUndeletedMessage = async (uid, roomId) => { + let done = false; + let latestMid = null; + let index = 0; + let mids; + + while (!done) { + /* eslint-disable no-await-in-loop */ + mids = await db.getSortedSetRevRange(`uid:${uid}:chat:room:${roomId}:mids`, index, index); + if (mids.length) { + const states = await Messaging.getMessageFields(mids[0], ['deleted', 'system']); + done = !states.deleted && !states.system; + if (done) { + latestMid = mids[0]; + } + index += 1; + } else { + done = true; + } + } + + return latestMid; +}; + +Messaging.canMessageUser = async (uid, toUid) => { + if (meta.config.disableChat || uid <= 0) { + throw new Error('[[error:chat-disabled]]'); + } + + if (parseInt(uid, 10) === parseInt(toUid, 10)) { + throw new Error('[[error:cant-chat-with-yourself]]'); + } + const [exists, canChat] = await Promise.all([ + user.exists(toUid), + privileges.global.can('chat', uid), + checkReputation(uid), + ]); + + if (!exists) { + throw new Error('[[error:no-user]]'); + } + + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + + const [settings, isAdmin, isModerator, isFollowing, isBlocked] = await Promise.all([ + user.getSettings(toUid), + user.isAdministrator(uid), + user.isModeratorOfAnyCategory(uid), + user.isFollowing(toUid, uid), + user.blocks.is(uid, toUid), + ]); + + if (isBlocked || (settings.restrictChat && !isAdmin && !isModerator && !isFollowing)) { + throw new Error('[[error:chat-restricted]]'); + } + + await plugins.hooks.fire('static:messaging.canMessageUser', { + uid: uid, + toUid: toUid, + }); +}; + +Messaging.canMessageRoom = async (uid, roomId) => { + if (meta.config.disableChat || uid <= 0) { + throw new Error('[[error:chat-disabled]]'); + } + + const [inRoom, canChat] = await Promise.all([ + Messaging.isUserInRoom(uid, roomId), + privileges.global.can('chat', uid), + checkReputation(uid), + ]); + + if (!inRoom) { + throw new Error('[[error:not-in-room]]'); + } + + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + + await plugins.hooks.fire('static:messaging.canMessageRoom', { + uid: uid, + roomId: roomId, + }); +}; + +async function checkReputation(uid) { + if (meta.config['min:rep:chat'] > 0) { + const reputation = await user.getUserField(uid, 'reputation'); + if (meta.config['min:rep:chat'] > reputation) { + throw new Error(`[[error:not-enough-reputation-to-chat, ${meta.config['min:rep:chat']}]]`); + } + } +} + +Messaging.hasPrivateChat = async (uid, withUid) => { + if (parseInt(uid, 10) === parseInt(withUid, 10)) { + return 0; + } + + const results = await utils.promiseParallel({ + myRooms: db.getSortedSetRevRange(`uid:${uid}:chat:rooms`, 0, -1), + theirRooms: db.getSortedSetRevRange(`uid:${withUid}:chat:rooms`, 0, -1), + }); + const roomIds = results.myRooms.filter(roomId => roomId && results.theirRooms.includes(roomId)); + + if (!roomIds.length) { + return 0; + } + + let index = 0; + let roomId = 0; + while (index < roomIds.length && !roomId) { + /* eslint-disable no-await-in-loop */ + const count = await Messaging.getUserCountInRoom(roomIds[index]); + if (count === 2) { + roomId = roomIds[index]; + } else { + index += 1; + } + } + + return roomId; +}; + +Messaging.canViewMessage = async (mids, roomId, uid) => { + let single = false; + if (!Array.isArray(mids) && isFinite(mids)) { + mids = [mids]; + single = true; + } + + const canView = await db.isSortedSetMembers(`uid:${uid}:chat:room:${roomId}:mids`, mids); + return single ? canView.pop() : canView; +}; + +require('../promisify')(Messaging); diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js new file mode 100644 index 0000000000..915ccf59d1 --- /dev/null +++ b/src/messaging/notifications.js @@ -0,0 +1,82 @@ +'use strict'; + +const winston = require('winston'); + +const user = require('../user'); +const notifications = require('../notifications'); +const sockets = require('../socket.io'); +const plugins = require('../plugins'); +const meta = require('../meta'); + +module.exports = function (Messaging) { + Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser + + Messaging.notifyUsersInRoom = async (fromUid, roomId, messageObj) => { + let uids = await Messaging.getUidsInRoom(roomId, 0, -1); + uids = await user.blocks.filterUids(fromUid, uids); + + let data = { + roomId: roomId, + fromUid: fromUid, + message: messageObj, + uids: uids, + }; + data = await plugins.hooks.fire('filter:messaging.notify', data); + if (!data || !data.uids || !data.uids.length) { + return; + } + + uids = data.uids; + uids.forEach((uid) => { + data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0; + Messaging.pushUnreadCount(uid); + sockets.in(`uid_${uid}`).emit('event:chats.receive', data); + }); + if (messageObj.system) { + return; + } + // Delayed notifications + let queueObj = Messaging.notifyQueue[`${fromUid}:${roomId}`]; + if (queueObj) { + queueObj.message.content += `\n${messageObj.content}`; + clearTimeout(queueObj.timeout); + } else { + queueObj = { + message: messageObj, + }; + Messaging.notifyQueue[`${fromUid}:${roomId}`] = queueObj; + } + + queueObj.timeout = setTimeout(async () => { + try { + await sendNotifications(fromUid, uids, roomId, queueObj.message); + } catch (err) { + winston.error(`[messaging/notifications] Unabled to send notification\n${err.stack}`); + } + }, meta.config.notificationSendDelay * 1000); + }; + + async function sendNotifications(fromuid, uids, roomId, messageObj) { + const isOnline = await user.isOnline(uids); + uids = uids.filter((uid, index) => !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10)); + if (!uids.length) { + return; + } + + const { displayname } = messageObj.fromUser; + + const isGroupChat = await Messaging.isGroupChat(roomId); + const notification = await notifications.create({ + type: isGroupChat ? 'new-group-chat' : 'new-chat', + subject: `[[email:notif.chat.subject, ${displayname}]]`, + bodyShort: `[[notifications:new_message_from, ${displayname}]]`, + bodyLong: messageObj.content, + nid: `chat_${fromuid}_${roomId}`, + from: fromuid, + path: `/chats/${messageObj.roomId}`, + }); + + delete Messaging.notifyQueue[`${fromuid}:${roomId}`]; + notifications.push(notification, uids); + } +}; diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js new file mode 100644 index 0000000000..b0bf9b6de8 --- /dev/null +++ b/src/messaging/rooms.js @@ -0,0 +1,261 @@ +'use strict'; + +const validator = require('validator'); + +const db = require('../database'); +const user = require('../user'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); +const meta = require('../meta'); + +module.exports = function (Messaging) { + Messaging.getRoomData = async (roomId) => { + const data = await db.getObject(`chat:room:${roomId}`); + if (!data) { + throw new Error('[[error:no-chat-room]]'); + } + + modifyRoomData([data]); + return data; + }; + + Messaging.getRoomsData = async (roomIds) => { + const roomData = await db.getObjects(roomIds.map(roomId => `chat:room:${roomId}`)); + modifyRoomData(roomData); + return roomData; + }; + + function modifyRoomData(rooms) { + rooms.forEach((data) => { + if (data) { + data.roomName = data.roomName || ''; + data.roomName = validator.escape(String(data.roomName)); + if (data.hasOwnProperty('groupChat')) { + data.groupChat = parseInt(data.groupChat, 10) === 1; + } + } + }); + } + + Messaging.newRoom = async (uid, toUids) => { + const now = Date.now(); + const roomId = await db.incrObjectField('global', 'nextChatRoomId'); + const room = { + owner: uid, + roomId: roomId, + }; + + await Promise.all([ + db.setObject(`chat:room:${roomId}`, room), + db.sortedSetAdd(`chat:room:${roomId}:uids`, now, uid), + ]); + await Promise.all([ + Messaging.addUsersToRoom(uid, toUids, roomId), + Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now), + ]); + // chat owner should also get the user-join system message + await Messaging.addSystemMessage('user-join', uid, roomId); + + return roomId; + }; + + Messaging.isUserInRoom = async (uid, roomId) => { + const inRoom = await db.isSortedSetMember(`chat:room:${roomId}:uids`, uid); + const data = await plugins.hooks.fire('filter:messaging.isUserInRoom', { uid: uid, roomId: roomId, inRoom: inRoom }); + return data.inRoom; + }; + + Messaging.roomExists = async roomId => db.exists(`chat:room:${roomId}:uids`); + + Messaging.getUserCountInRoom = async roomId => db.sortedSetCard(`chat:room:${roomId}:uids`); + + Messaging.isRoomOwner = async (uids, roomId) => { + const isArray = Array.isArray(uids); + if (!isArray) { + uids = [uids]; + } + const owner = await db.getObjectField(`chat:room:${roomId}`, 'owner'); + const isOwners = uids.map(uid => parseInt(uid, 10) === parseInt(owner, 10)); + + const result = await Promise.all(isOwners.map(async (isOwner, index) => { + const payload = await plugins.hooks.fire('filter:messaging.isRoomOwner', { uid: uids[index], roomId, owner, isOwner }); + return payload.isOwner; + })); + return isArray ? result : result[0]; + }; + + Messaging.addUsersToRoom = async function (uid, uids, roomId) { + const inRoom = await Messaging.isUserInRoom(uid, roomId); + const payload = await plugins.hooks.fire('filter:messaging.addUsersToRoom', { uid, uids, roomId, inRoom }); + + if (!payload.inRoom) { + throw new Error('[[error:cant-add-users-to-chat-room]]'); + } + + const now = Date.now(); + const timestamps = payload.uids.map(() => now); + await db.sortedSetAdd(`chat:room:${payload.roomId}:uids`, timestamps, payload.uids); + await updateGroupChatField([payload.roomId]); + await Promise.all(payload.uids.map(uid => Messaging.addSystemMessage('user-join', uid, payload.roomId))); + }; + + Messaging.removeUsersFromRoom = async (uid, uids, roomId) => { + const [isOwner, userCount] = await Promise.all([ + Messaging.isRoomOwner(uid, roomId), + Messaging.getUserCountInRoom(roomId), + ]); + const payload = await plugins.hooks.fire('filter:messaging.removeUsersFromRoom', { uid, uids, roomId, isOwner, userCount }); + + if (!payload.isOwner) { + throw new Error('[[error:cant-remove-users-from-chat-room]]'); + } + + await Messaging.leaveRoom(payload.uids, payload.roomId); + }; + + Messaging.isGroupChat = async function (roomId) { + return (await Messaging.getRoomData(roomId)).groupChat; + }; + + async function updateGroupChatField(roomIds) { + const userCounts = await db.sortedSetsCard(roomIds.map(roomId => `chat:room:${roomId}:uids`)); + const groupChats = roomIds.filter((roomId, index) => userCounts[index] > 2); + const privateChats = roomIds.filter((roomId, index) => userCounts[index] <= 2); + await db.setObjectBulk([ + ...groupChats.map(id => [`chat:room:${id}`, { groupChat: 1 }]), + ...privateChats.map(id => [`chat:room:${id}`, { groupChat: 0 }]), + ]); + } + + 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 + .map(uid => `uid:${uid}:chat:rooms`) + .concat(uids.map(uid => `uid:${uid}:chat:rooms:unread`)); + + await Promise.all([ + db.sortedSetRemove(`chat:room:${roomId}:uids`, uids), + db.sortedSetsRemove(keys, roomId), + ]); + + await Promise.all(uids.map(uid => Messaging.addSystemMessage('user-leave', uid, roomId))); + await updateOwner(roomId); + await updateGroupChatField([roomId]); + }; + + 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`); + await Promise.all([ + db.sortedSetsRemove(roomKeys, uid), + db.sortedSetRemove([ + `uid:${uid}:chat:rooms`, + `uid:${uid}:chat:rooms:unread`, + ], roomIds), + ]); + + await Promise.all( + roomIds.map(roomId => updateOwner(roomId)) + .concat(roomIds.map(roomId => Messaging.addSystemMessage('user-leave', uid, roomId))) + ); + await updateGroupChatField(roomIds); + }; + + async function updateOwner(roomId) { + const uids = await db.getSortedSetRange(`chat:room:${roomId}:uids`, 0, 0); + const newOwner = uids[0] || 0; + await db.setObjectField(`chat:room:${roomId}`, 'owner', newOwner); + } + + Messaging.getUidsInRoom = async (roomId, start, stop) => db.getSortedSetRevRange(`chat:room:${roomId}:uids`, start, stop); + + Messaging.getUsersInRoom = async (roomId, start, stop) => { + const uids = await Messaging.getUidsInRoom(roomId, start, stop); + const [users, isOwners] = await Promise.all([ + user.getUsersFields(uids, ['uid', 'username', 'picture', 'status']), + Messaging.isRoomOwner(uids, roomId), + ]); + + return users.map((user, index) => { + user.isOwner = isOwners[index]; + return user; + }); + }; + + Messaging.renameRoom = async function (uid, roomId, newName) { + if (!newName) { + throw new Error('[[error:invalid-data]]'); + } + newName = newName.trim(); + if (newName.length > 75) { + throw new Error('[[error:chat-room-name-too-long]]'); + } + + const payload = await plugins.hooks.fire('filter:chat.renameRoom', { + uid: uid, + roomId: roomId, + newName: newName, + }); + const isOwner = await Messaging.isRoomOwner(payload.uid, payload.roomId); + if (!isOwner) { + throw new Error('[[error:no-privileges]]'); + } + + await db.setObjectField(`chat:room:${payload.roomId}`, 'roomName', payload.newName); + await Messaging.addSystemMessage(`room-rename, ${payload.newName.replace(',', ',')}`, payload.uid, payload.roomId); + + plugins.hooks.fire('action:chat.renameRoom', { + roomId: payload.roomId, + newName: payload.newName, + }); + }; + + Messaging.canReply = async (roomId, uid) => { + const inRoom = await db.isSortedSetMember(`chat:room:${roomId}:uids`, uid); + const data = await plugins.hooks.fire('filter:messaging.canReply', { uid: uid, roomId: roomId, inRoom: inRoom, canReply: inRoom }); + return data.canReply; + }; + + Messaging.loadRoom = async (uid, data) => { + const canChat = await privileges.global.can('chat', uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + const inRoom = await Messaging.isUserInRoom(uid, data.roomId); + if (!inRoom) { + return null; + } + + const [room, canReply, users, messages, isAdminOrGlobalMod] = await Promise.all([ + Messaging.getRoomData(data.roomId), + Messaging.canReply(data.roomId, uid), + Messaging.getUsersInRoom(data.roomId, 0, -1), + Messaging.getMessages({ + callerUid: uid, + uid: data.uid || uid, + roomId: data.roomId, + isNew: false, + }), + user.isAdminOrGlobalMod(uid), + ]); + + room.messages = messages; + room.isOwner = await Messaging.isRoomOwner(uid, room.roomId); + room.users = users.filter(user => user && parseInt(user.uid, 10) && + parseInt(user.uid, 10) !== parseInt(uid, 10)); + room.canReply = canReply; + room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : users.length > 2; + room.usernames = Messaging.generateUsernames(users, uid); + room.maximumUsersInChatRoom = meta.config.maximumUsersInChatRoom; + room.maximumChatMessageLength = meta.config.maximumChatMessageLength; + room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2; + room.isAdminOrGlobalMod = isAdminOrGlobalMod; + + const payload = await plugins.hooks.fire('filter:messaging.loadRoom', { uid, data, room }); + return payload.room; + }; +}; diff --git a/src/messaging/unread.js b/src/messaging/unread.js new file mode 100644 index 0000000000..ce5fb860d3 --- /dev/null +++ b/src/messaging/unread.js @@ -0,0 +1,39 @@ +'use strict'; + +const db = require('../database'); +const sockets = require('../socket.io'); + +module.exports = function (Messaging) { + Messaging.getUnreadCount = async (uid) => { + if (parseInt(uid, 10) <= 0) { + return 0; + } + + return await db.sortedSetCard(`uid:${uid}:chat:rooms:unread`); + }; + + Messaging.pushUnreadCount = async (uid) => { + if (parseInt(uid, 10) <= 0) { + return; + } + const unreadCount = await Messaging.getUnreadCount(uid); + sockets.in(`uid_${uid}`).emit('event:unread.updateChatCount', unreadCount); + }; + + Messaging.markRead = async (uid, roomId) => { + await db.sortedSetRemove(`uid:${uid}:chat:rooms:unread`, roomId); + }; + + Messaging.markAllRead = async (uid) => { + await db.delete(`uid:${uid}:chat:rooms:unread`); + }; + + Messaging.markUnread = async (uids, roomId) => { + const exists = await Messaging.roomExists(roomId); + if (!exists) { + return; + } + const keys = uids.map(uid => `uid:${uid}:chat:rooms:unread`); + return await db.sortedSetsAdd(keys, Date.now(), roomId); + }; +}; diff --git a/src/meta/aliases.js b/src/meta/aliases.js new file mode 100644 index 0000000000..509a4d743f --- /dev/null +++ b/src/meta/aliases.js @@ -0,0 +1,43 @@ +'use strict'; + +const _ = require('lodash'); +const chalk = require('chalk'); + +const aliases = { + 'plugin static dirs': ['staticdirs'], + 'requirejs modules': ['rjs', 'modules'], + 'client js bundle': ['clientjs', 'clientscript', 'clientscripts'], + 'admin js bundle': ['adminjs', 'adminscript', 'adminscripts'], + javascript: ['js'], + 'client side styles': [ + 'clientcss', 'clientless', 'clientstyles', 'clientstyle', + ], + 'admin control panel styles': [ + 'admincss', 'adminless', 'adminstyles', 'adminstyle', 'acpcss', 'acpless', 'acpstyles', 'acpstyle', + ], + styles: ['css', 'less', 'style'], + templates: ['tpl'], + languages: ['lang', 'i18n'], +}; + +exports.aliases = aliases; + +function buildTargets() { + let length = 0; + const output = Object.keys(aliases).map((name) => { + const arr = aliases[name]; + if (name.length > length) { + length = name.length; + } + + return [name, arr.join(', ')]; + }).map(tuple => ` ${chalk.magenta(_.padEnd(`"${tuple[0]}"`, length + 2))} | ${tuple[1]}`).join('\n'); + process.stdout.write( + '\n\n Build targets:\n' + + `${chalk.green(`\n ${_.padEnd('Target', length + 2)} | Aliases`)}` + + `${chalk.blue('\n ------------------------------------------------------\n')}` + + `${output}\n\n` + ); +} + +exports.buildTargets = buildTargets; diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js new file mode 100644 index 0000000000..b8925749ee --- /dev/null +++ b/src/meta/blacklist.js @@ -0,0 +1,171 @@ +'use strict'; + +const ipaddr = require('ipaddr.js'); +const winston = require('winston'); +const _ = require('lodash'); +const validator = require('validator'); + +const db = require('../database'); +const pubsub = require('../pubsub'); +const plugins = require('../plugins'); +const analytics = require('../analytics'); + +const Blacklist = module.exports; +Blacklist._rules = {}; + +Blacklist.load = async function () { + let rules = await Blacklist.get(); + rules = Blacklist.validate(rules); + + winston.verbose(`[meta/blacklist] Loading ${rules.valid.length} blacklist rule(s)${rules.duplicateCount > 0 ? `, ignored ${rules.duplicateCount} duplicate(s)` : ''}`); + if (rules.invalid.length) { + winston.warn(`[meta/blacklist] ${rules.invalid.length} invalid blacklist rule(s) were ignored.`); + } + + Blacklist._rules = { + ipv4: rules.ipv4, + ipv6: rules.ipv6, + cidr: rules.cidr, + cidr6: rules.cidr6, + }; +}; + +pubsub.on('blacklist:reload', Blacklist.load); + +Blacklist.save = async function (rules) { + await db.setObject('ip-blacklist-rules', { rules: rules }); + await Blacklist.load(); + pubsub.publish('blacklist:reload'); +}; + +Blacklist.get = async function () { + const data = await db.getObject('ip-blacklist-rules'); + return data && data.rules; +}; + +Blacklist.test = async function (clientIp) { + // Some handy test addresses + // clientIp = '2001:db8:85a3:0:0:8a2e:370:7334'; // IPv6 + // clientIp = '127.0.15.1'; // IPv4 + // clientIp = '127.0.15.1:3443'; // IPv4 with port strip port to not fail + if (!clientIp) { + return; + } + clientIp = clientIp.split(':').length === 2 ? clientIp.split(':')[0] : clientIp; + + let addr; + try { + addr = ipaddr.parse(clientIp); + } catch (err) { + winston.error(`[meta/blacklist] Error parsing client IP : ${clientIp}`); + throw err; + } + + if ( + !Blacklist._rules.ipv4.includes(clientIp) && // not explicitly specified in ipv4 list + !Blacklist._rules.ipv6.includes(clientIp) && // not explicitly specified in ipv6 list + !Blacklist._rules.cidr.some((subnet) => { + const cidr = ipaddr.parseCIDR(subnet); + if (addr.kind() !== cidr[0].kind()) { + return false; + } + return addr.match(cidr); + }) // not in a blacklisted IPv4 or IPv6 cidr range + ) { + try { + // To return test failure, pass back an error in callback + await plugins.hooks.fire('filter:blacklist.test', { ip: clientIp }); + } catch (err) { + analytics.increment('blacklist'); + throw err; + } + } else { + const err = new Error('[[error:blacklisted-ip]]'); + err.code = 'blacklisted-ip'; + + analytics.increment('blacklist'); + throw err; + } +}; + +Blacklist.validate = function (rules) { + rules = (rules || '').split('\n'); + const ipv4 = []; + const ipv6 = []; + const cidr = []; + const invalid = []; + let duplicateCount = 0; + + const inlineCommentMatch = /#.*$/; + const whitelist = ['127.0.0.1', '::1', '::ffff:0:127.0.0.1']; + + // Filter out blank lines and lines starting with the hash character (comments) + // Also trim inputs and remove inline comments + rules = rules.map((rule) => { + rule = rule.replace(inlineCommentMatch, '').trim(); + return rule.length && !rule.startsWith('#') ? rule : null; + }).filter(Boolean); + + // Filter out duplicates + const uniqRules = _.uniq(rules); + duplicateCount += rules.length - uniqRules.length; + rules = uniqRules; + + // Filter out invalid rules + rules = rules.filter((rule) => { + let addr; + let isRange = false; + try { + addr = ipaddr.parse(rule); + } catch (e) { + // Do nothing + } + + try { + addr = ipaddr.parseCIDR(rule); + isRange = true; + } catch (e) { + // Do nothing + } + + if (!addr || whitelist.includes(rule)) { + invalid.push(validator.escape(rule)); + return false; + } + + if (!isRange) { + if (addr.kind() === 'ipv4' && ipaddr.IPv4.isValid(rule)) { + ipv4.push(rule); + return true; + } + if (addr.kind() === 'ipv6' && ipaddr.IPv6.isValid(rule)) { + ipv6.push(rule); + return true; + } + } else { + cidr.push(rule); + return true; + } + return false; + }); + + return { + numRules: rules.length + invalid.length, + ipv4: ipv4, + ipv6: ipv6, + cidr: cidr, + valid: rules, + invalid: invalid, + duplicateCount: duplicateCount, + }; +}; + +Blacklist.addRule = async function (rule) { + const { valid } = Blacklist.validate(rule); + if (!valid.length) { + throw new Error('[[error:invalid-rule]]'); + } + let rules = await Blacklist.get(); + rules = `${rules}\n${valid[0]}`; + await Blacklist.save(rules); +}; diff --git a/src/meta/build.js b/src/meta/build.js new file mode 100644 index 0000000000..00b91fe2c3 --- /dev/null +++ b/src/meta/build.js @@ -0,0 +1,264 @@ +'use strict'; + +const os = require('os'); +const winston = require('winston'); +const nconf = require('nconf'); +const _ = require('lodash'); +const path = require('path'); +const mkdirp = require('mkdirp'); +const chalk = require('chalk'); +const { exec } = require('child_process'); +const util = require('util'); + +const cacheBuster = require('./cacheBuster'); +const { aliases } = require('./aliases'); + +let meta; + +const targetHandlers = { + 'plugin static dirs': async function () { + await meta.js.linkStatics(); + }, + 'requirejs modules': async function (parallel) { + await meta.js.buildModules(parallel); + }, + 'client js bundle': async function (parallel) { + await meta.js.buildBundle('client', parallel); + }, + 'admin js bundle': async function (parallel) { + await meta.js.buildBundle('admin', parallel); + }, + javascript: [ + 'plugin static dirs', + 'requirejs modules', + 'client js bundle', + 'admin js bundle', + ], + 'client side styles': async function (parallel) { + await meta.css.buildBundle('client', parallel); + }, + 'admin control panel styles': async function (parallel) { + await meta.css.buildBundle('admin', parallel); + }, + styles: [ + 'client side styles', + 'admin control panel styles', + ], + templates: async function () { + await meta.templates.compile(); + }, + languages: async function () { + await meta.languages.build(); + }, +}; + +const aliasMap = Object.keys(aliases).reduce((prev, key) => { + const arr = aliases[key]; + arr.forEach((alias) => { + prev[alias] = key; + }); + prev[key] = key; + return prev; +}, {}); + +async function beforeBuild(targets) { + const db = require('../database'); + process.stdout.write(`${chalk.green(' started')}\n`); + try { + await db.init(); + meta = require('./index'); + await meta.themes.setupPaths(); + const plugins = require('../plugins'); + await plugins.prepareForBuild(targets); + await mkdirp(path.join(__dirname, '../../build/public')); + } catch (err) { + winston.error(`[build] Encountered error preparing for build\n${err.stack}`); + throw err; + } +} + +const allTargets = Object.keys(targetHandlers).filter(name => typeof targetHandlers[name] === 'function'); + +async function buildTargets(targets, parallel, options) { + const length = Math.max(...targets.map(name => name.length)); + const jsTargets = targets.filter(target => targetHandlers.javascript.includes(target)); + const otherTargets = targets.filter(target => !targetHandlers.javascript.includes(target)); + + // Compile TypeScript into JavaScript + winston.info(`[build] Building TypeScript files`); + const execAsync = util.promisify(exec); + await execAsync('npx tsc'); + winston.info(`[build] TypeScript building complete`); + + async function buildJSTargets() { + await Promise.all( + jsTargets.map( + target => step(target, parallel, `${_.padStart(target, length)} `) + ) + ); + // run webpack after jstargets are done, no need to wait for css/templates etc. + if (options.webpack || options.watch) { + await exports.webpack(options); + } + } + if (parallel) { + await Promise.all([ + buildJSTargets(), + ...otherTargets.map( + target => step(target, parallel, `${_.padStart(target, length)} `) + ), + ]); + } else { + for (const target of targets) { + // eslint-disable-next-line no-await-in-loop + await step(target, parallel, `${_.padStart(target, length)} `); + } + if (options.webpack || options.watch) { + await exports.webpack(options); + } + } +} + +async function step(target, parallel, targetStr) { + const startTime = Date.now(); + winston.info(`[build] ${targetStr} build started`); + try { + await targetHandlers[target](parallel); + const time = (Date.now() - startTime) / 1000; + + winston.info(`[build] ${targetStr} build completed in ${time}sec`); + } catch (err) { + winston.error(`[build] ${targetStr} build failed`); + throw err; + } +} + +exports.build = async function (targets, options) { + if (!options) { + options = {}; + } + + if (targets === true) { + targets = allTargets; + } else if (!Array.isArray(targets)) { + targets = targets.split(','); + } + + 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 + // get full target name + .map((target) => { + target = target.toLowerCase().replace(/-/g, ''); + if (!aliasMap[target]) { + winston.warn(`[build] Unknown target: ${target}`); + if (target.includes(',')) { + winston.warn('[build] Are you specifying multiple targets? Separate them with spaces:'); + winston.warn('[build] e.g. `./nodebb build adminjs tpl`'); + } + + return false; + } + + return aliasMap[target]; + }) + // filter nonexistent targets + .filter(Boolean); + + // map multitargets to their sets + targets = _.uniq(_.flatMap(targets, target => ( + Array.isArray(targetHandlers[target]) ? + targetHandlers[target] : + target + ))); + + winston.verbose(`[build] building the following targets: ${targets.join(', ')}`); + + if (!targets) { + winston.info('[build] No valid targets supplied. Aborting.'); + return; + } + + try { + await beforeBuild(targets); + const threads = parseInt(nconf.get('threads'), 10); + if (threads) { + require('./minifier').maxThreads = threads - 1; + } + + if (!series) { + winston.info('[build] Building in parallel mode'); + } else { + winston.info('[build] Building in series mode'); + } + + const startTime = Date.now(); + await buildTargets(targets, !series, options); + + const totalTime = (Date.now() - startTime) / 1000; + await cacheBuster.write(); + winston.info(`[build] Asset compilation successful. Completed in ${totalTime}sec.`); + } catch (err) { + winston.error(`[build] Encountered error during build step\n${err.stack ? err.stack : err}`); + throw err; + } +}; + +function getWebpackConfig() { + return require(process.env.NODE_ENV !== 'development' ? '../../webpack.prod' : '../../webpack.dev'); +} + +exports.webpack = async function (options) { + winston.info(`[build] ${(options.watch ? 'Watching' : 'Bundling')} with Webpack.`); + const webpack = require('webpack'); + const fs = require('fs'); + const util = require('util'); + const plugins = require('../plugins/data'); + + const activePlugins = (await plugins.getActive()).map(p => p.id); + if (!activePlugins.includes('nodebb-plugin-composer-default')) { + activePlugins.push('nodebb-plugin-composer-default'); + } + await fs.promises.writeFile(path.resolve(__dirname, '../../build/active_plugins.json'), JSON.stringify(activePlugins)); + + const webpackCfg = getWebpackConfig(); + const compiler = webpack(webpackCfg); + const webpackRun = util.promisify(compiler.run).bind(compiler); + const webpackWatch = util.promisify(compiler.watch).bind(compiler); + try { + let stats; + if (options.watch) { + stats = await webpackWatch(webpackCfg.watchOptions); + compiler.hooks.assetEmitted.tap('nbbWatchPlugin', (file) => { + console.log(`webpack:assetEmitted > ${webpackCfg.output.publicPath}${file}`); + }); + } else { + stats = await webpackRun(); + } + + if (stats.hasErrors() || stats.hasWarnings()) { + console.log(stats.toString('minimal')); + } else { + const statsJson = stats.toJson(); + winston.info(`[build] ${(options.watch ? 'Watching' : 'Bundling')} took ${statsJson.time} ms`); + } + } catch (err) { + console.error(err.stack || err); + if (err.details) { + console.error(err.details); + } + } +}; + +exports.buildAll = async function () { + await exports.build(allTargets, { webpack: true }); +}; + +require('../promisify')(exports); diff --git a/src/meta/cacheBuster.js b/src/meta/cacheBuster.js new file mode 100644 index 0000000000..e24f8aa95b --- /dev/null +++ b/src/meta/cacheBuster.js @@ -0,0 +1,41 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const mkdirp = require('mkdirp'); +const winston = require('winston'); + +const filePath = path.join(__dirname, '../../build/cache-buster'); + +let cached; + +// cache buster is an 11-character, lowercase, alphanumeric string +function generate() { + return (Math.random() * 1e18).toString(32).slice(0, 11); +} + +exports.write = async function write() { + await mkdirp(path.dirname(filePath)); + await fs.promises.writeFile(filePath, generate()); +}; + +exports.read = async function read() { + if (cached) { + return cached; + } + try { + const buster = await fs.promises.readFile(filePath, 'utf8'); + if (!buster || buster.length !== 11) { + winston.warn(`[cache-buster] cache buster string invalid: expected /[a-z0-9]{11}/, got \`${buster}\``); + return generate(); + } + + cached = buster; + return cached; + } catch (err) { + winston.warn('[cache-buster] could not read cache buster', err); + return generate(); + } +}; + +require('../promisify')(exports); diff --git a/src/meta/configs.js b/src/meta/configs.js new file mode 100644 index 0000000000..f0c28d4944 --- /dev/null +++ b/src/meta/configs.js @@ -0,0 +1,289 @@ + +'use strict'; + +const nconf = require('nconf'); +const path = require('path'); +const winston = require('winston'); + +const db = require('../database'); +const pubsub = require('../pubsub'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const Meta = require('./index'); +const cacheBuster = require('./cacheBuster'); +const defaults = require('../../install/data/defaults.json'); + +const Configs = module.exports; + +Meta.config = {}; + +// called after data is loaded from db +function deserialize(config) { + const deserialized = {}; + Object.keys(config).forEach((key) => { + const defaultType = typeof defaults[key]; + const type = typeof config[key]; + const number = parseFloat(config[key]); + + if (defaultType === 'string' && type === 'number') { + deserialized[key] = String(config[key]); + } else if (defaultType === 'number' && type === 'string') { + if (!isNaN(number) && isFinite(config[key])) { + deserialized[key] = number; + } else { + deserialized[key] = defaults[key]; + } + } else if (config[key] === 'true') { + deserialized[key] = true; + } else if (config[key] === 'false') { + deserialized[key] = false; + } else if (config[key] === null) { + deserialized[key] = defaults[key]; + } else if (defaultType === 'undefined' && !isNaN(number) && isFinite(config[key])) { + deserialized[key] = number; + } else if (Array.isArray(defaults[key]) && !Array.isArray(config[key])) { + try { + deserialized[key] = JSON.parse(config[key] || '[]'); + } catch (err) { + winston.error(err.stack); + deserialized[key] = defaults[key]; + } + } else { + deserialized[key] = config[key]; + } + }); + return deserialized; +} + +// called before data is saved to db +function serialize(config) { + const serialized = {}; + Object.keys(config).forEach((key) => { + const defaultType = typeof defaults[key]; + const type = typeof config[key]; + const number = parseFloat(config[key]); + + if (defaultType === 'string' && type === 'number') { + serialized[key] = String(config[key]); + } else if (defaultType === 'number' && type === 'string') { + if (!isNaN(number) && isFinite(config[key])) { + serialized[key] = number; + } else { + serialized[key] = defaults[key]; + } + } else if (config[key] === null) { + serialized[key] = defaults[key]; + } else if (defaultType === 'undefined' && !isNaN(number) && isFinite(config[key])) { + serialized[key] = number; + } else if (Array.isArray(defaults[key]) && Array.isArray(config[key])) { + serialized[key] = JSON.stringify(config[key]); + } else { + serialized[key] = config[key]; + } + }); + return serialized; +} + +Configs.deserialize = deserialize; +Configs.serialize = serialize; + +Configs.init = async function () { + const config = await Configs.list(); + const buster = await cacheBuster.read(); + config['cache-buster'] = `v=${buster || Date.now()}`; + Meta.config = config; +}; + +Configs.list = async function () { + return await Configs.getFields([]); +}; + +Configs.get = async function (field) { + const values = await Configs.getFields([field]); + return (values.hasOwnProperty(field) && values[field] !== undefined) ? values[field] : null; +}; + +Configs.getFields = async function (fields) { + let values; + if (fields.length) { + values = await db.getObjectFields('config', fields); + } else { + values = await db.getObject('config'); + } + + values = { ...defaults, ...(values ? deserialize(values) : {}) }; + + if (!fields.length) { + values.version = nconf.get('version'); + values.registry = nconf.get('registry'); + } + return values; +}; + +Configs.set = async function (field, value) { + if (!field) { + throw new Error('[[error:invalid-data]]'); + } + + await Configs.setMultiple({ + [field]: value, + }); +}; + +Configs.setMultiple = async function (data) { + await processConfig(data); + data = serialize(data); + await db.setObject('config', data); + updateConfig(deserialize(data)); +}; + +Configs.setOnEmpty = async function (values) { + const data = await db.getObject('config'); + values = serialize(values); + const config = { ...values, ...(data ? serialize(data) : {}) }; + await db.setObject('config', config); +}; + +Configs.remove = async function (field) { + await db.deleteObjectField('config', field); +}; + +Configs.registerHooks = () => { + plugins.hooks.register('core', { + hook: 'filter:settings.set', + method: async ({ plugin, settings, quiet }) => { + if (plugin === 'core.api' && Array.isArray(settings.tokens)) { + // Generate tokens if not present already + settings.tokens.forEach((set) => { + if (set.token === '') { + set.token = utils.generateUUID(); + } + + if (isNaN(parseInt(set.uid, 10))) { + set.uid = 0; + } + }); + } + + return { plugin, settings, quiet }; + }, + }); + + plugins.hooks.register('core', { + hook: 'filter:settings.get', + method: async ({ plugin, values }) => { + if (plugin === 'core.api' && Array.isArray(values.tokens)) { + values.tokens = values.tokens.map((tokenObj) => { + tokenObj.uid = parseInt(tokenObj.uid, 10); + if (tokenObj.timestamp) { + tokenObj.timestampISO = new Date(parseInt(tokenObj.timestamp, 10)).toISOString(); + } + + return tokenObj; + }); + } + + return { plugin, values }; + }, + }); +}; + +Configs.cookie = { + get: () => { + const cookie = {}; + + if (nconf.get('cookieDomain') || Meta.config.cookieDomain) { + cookie.domain = nconf.get('cookieDomain') || Meta.config.cookieDomain; + } + + if (nconf.get('secure')) { + cookie.secure = true; + } + + const relativePath = nconf.get('relative_path'); + if (relativePath !== '') { + cookie.path = relativePath; + } + + // Ideally configurable from ACP, but cannot be "Strict" as then top-level access will treat it as guest. + cookie.sameSite = 'Lax'; + + return cookie; + }, +}; + +async function processConfig(data) { + ensureInteger(data, 'maximumUsernameLength', 1); + ensureInteger(data, 'minimumUsernameLength', 1); + ensureInteger(data, 'minimumPasswordLength', 1); + ensureInteger(data, 'maximumAboutMeLength', 0); + if (data.minimumUsernameLength > data.maximumUsernameLength) { + throw new Error('[[error:invalid-data]]'); + } + + await Promise.all([ + saveRenderedCss(data), + getLogoSize(data), + ]); +} + +function ensureInteger(data, field, min) { + if (data.hasOwnProperty(field)) { + data[field] = parseInt(data[field], 10); + if (!(data[field] >= min)) { + throw new Error('[[error:invalid-data]]'); + } + } +} + +async function saveRenderedCss(data) { + if (!data.customCSS) { + return; + } + const less = require('less'); + const lessObject = await less.render(data.customCSS, { + compress: true, + javascriptEnabled: false, + }); + data.renderedCustomCSS = lessObject.css; +} + +async function getLogoSize(data) { + const image = require('../image'); + if (!data['brand:logo']) { + return; + } + let size; + try { + size = await image.size(path.join(nconf.get('upload_path'), 'system', 'site-logo-x50.png')); + } catch (err) { + if (err.code === 'ENOENT') { + // For whatever reason the x50 logo wasn't generated, gracefully error out + winston.warn('[logo] The email-safe logo doesn\'t seem to have been created, please re-upload your site logo.'); + size = { + height: 0, + width: 0, + }; + } else { + throw err; + } + } + data['brand:emailLogo'] = nconf.get('url') + path.join(nconf.get('upload_url'), 'system', 'site-logo-x50.png'); + data['brand:emailLogo:height'] = size.height; + data['brand:emailLogo:width'] = size.width; +} + +function updateConfig(config) { + updateLocalConfig(config); + pubsub.publish('config:update', config); +} + +function updateLocalConfig(config) { + Object.assign(Meta.config, config); +} + +pubsub.on('config:update', (config) => { + if (typeof config === 'object' && Meta.config) { + updateLocalConfig(config); + } +}); diff --git a/src/meta/css.js b/src/meta/css.js new file mode 100644 index 0000000000..0516400776 --- /dev/null +++ b/src/meta/css.js @@ -0,0 +1,154 @@ +'use strict'; + +const winston = require('winston'); +const nconf = require('nconf'); +const fs = require('fs'); +const util = require('util'); +const path = require('path'); +const rimraf = require('rimraf'); + +const rimrafAsync = util.promisify(rimraf); + +const plugins = require('../plugins'); +const db = require('../database'); +const file = require('../file'); +const minifier = require('./minifier'); + +const CSS = module.exports; + +CSS.supportedSkins = [ + 'cerulean', 'cyborg', 'flatly', 'journal', 'lumen', 'paper', 'simplex', + 'spacelab', 'united', 'cosmo', 'darkly', 'readable', 'sandstone', + 'slate', 'superhero', 'yeti', +]; + +const buildImports = { + client: function (source) { + return `@import "./theme";\n${source}\n${[ + '@import "../public/vendor/fontawesome/less/regular.less";', + '@import "../public/vendor/fontawesome/less/solid.less";', + '@import "../public/vendor/fontawesome/less/brands.less";', + '@import "../public/vendor/fontawesome/less/fontawesome.less";', + '@import "../public/vendor/fontawesome/less/v4-shims.less";', + '@import "../public/vendor/fontawesome/less/nodebb-shims.less";', + '@import "../../public/less/jquery-ui.less";', + '@import (inline) "../node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.css";', + '@import (inline) "../node_modules/cropperjs/dist/cropper.css";', + '@import "../../public/less/flags.less";', + '@import "../../public/less/generics.less";', + '@import "../../public/less/mixins.less";', + '@import "../../public/less/global.less";', + '@import "../../public/less/modals.less";', + ].map(str => str.replace(/\//g, path.sep)).join('\n')}`; + }, + admin: function (source) { + return `${source}\n${[ + '@import "../public/vendor/fontawesome/less/regular.less";', + '@import "../public/vendor/fontawesome/less/solid.less";', + '@import "../public/vendor/fontawesome/less/brands.less";', + '@import "../public/vendor/fontawesome/less/fontawesome.less";', + '@import "../public/vendor/fontawesome/less/v4-shims.less";', + '@import "../public/vendor/fontawesome/less/nodebb-shims.less";', + '@import "../public/less/admin/admin";', + '@import "../public/less/generics.less";', + '@import "../../public/less/jquery-ui.less";', + '@import (inline) "../node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.css";', + '@import (inline) "../public/vendor/mdl/material.css";', + ].map(str => str.replace(/\//g, path.sep)).join('\n')}`; + }, +}; + +async function filterMissingFiles(filepaths) { + const exists = await Promise.all( + filepaths.map(async (filepath) => { + const exists = await file.exists(path.join(__dirname, '../../node_modules', filepath)); + if (!exists) { + winston.warn(`[meta/css] File not found! ${filepath}`); + } + return exists; + }) + ); + return filepaths.filter((filePath, i) => exists[i]); +} + +async function getImports(files, prefix, extension) { + const pluginDirectories = []; + let source = ''; + + files.forEach((styleFile) => { + if (styleFile.endsWith(extension)) { + source += `${prefix + path.sep + styleFile}";`; + } else { + pluginDirectories.push(styleFile); + } + }); + await Promise.all(pluginDirectories.map(async (directory) => { + const styleFiles = await file.walk(directory); + styleFiles.forEach((styleFile) => { + source += `${prefix + path.sep + styleFile}";`; + }); + })); + return source; +} + +async function getBundleMetadata(target) { + const paths = [ + path.join(__dirname, '../../node_modules'), + path.join(__dirname, '../../public/less'), + path.join(__dirname, '../../public/vendor/fontawesome/less'), + ]; + + // Skin support + let skin; + if (target.startsWith('client-')) { + skin = target.split('-')[1]; + + if (CSS.supportedSkins.includes(skin)) { + target = 'client'; + } + } + let skinImport = []; + if (target === 'client') { + const themeData = await db.getObjectFields('config', ['theme:type', 'theme:id', 'bootswatchSkin']); + const themeId = (themeData['theme:id'] || 'nodebb-theme-persona'); + const baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')); + paths.unshift(baseThemePath); + + themeData.bootswatchSkin = skin || themeData.bootswatchSkin; + if (themeData && themeData.bootswatchSkin) { + skinImport.push(`\n@import "./@nodebb/bootswatch/${themeData.bootswatchSkin}/variables.less";`); + skinImport.push(`\n@import "./@nodebb/bootswatch/${themeData.bootswatchSkin}/bootswatch.less";`); + } + skinImport = skinImport.join(''); + } + + const [lessImports, cssImports, acpLessImports] = await Promise.all([ + filterGetImports(plugins.lessFiles, '\n@import ".', '.less'), + filterGetImports(plugins.cssFiles, '\n@import (inline) ".', '.css'), + target === 'client' ? '' : filterGetImports(plugins.acpLessFiles, '\n@import ".', '.less'), + ]); + + async function filterGetImports(files, prefix, extension) { + const filteredFiles = await filterMissingFiles(files); + return await getImports(filteredFiles, prefix, extension); + } + + let imports = `${skinImport}\n${cssImports}\n${lessImports}\n${acpLessImports}`; + imports = buildImports[target](imports); + + return { paths: paths, imports: imports }; +} + +CSS.buildBundle = async function (target, fork) { + if (target === 'client') { + await rimrafAsync(path.join(__dirname, '../../build/public/client*')); + } + + const data = await getBundleMetadata(target); + const minify = process.env.NODE_ENV !== 'development'; + const bundle = await minifier.css.bundle(data.imports, data.paths, minify, fork); + + const filename = `${target}.css`; + await fs.promises.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code); + return bundle.code; +}; diff --git a/src/meta/debugFork.js b/src/meta/debugFork.js new file mode 100644 index 0000000000..27c6563cfa --- /dev/null +++ b/src/meta/debugFork.js @@ -0,0 +1,37 @@ +'use strict'; + +const { fork } = require('child_process'); + +let debugArg = process.execArgv.find(arg => /^--(debug|inspect)/.test(arg)); +const debugging = !!debugArg; + +debugArg = debugArg ? debugArg.replace('-brk', '').split('=') : ['--debug', 5859]; +let lastAddress = parseInt(debugArg[1], 10); + +/** + * child-process.fork, but safe for use in debuggers + * @param {string} modulePath + * @param {string[]} [args] + * @param {any} [options] + */ +function debugFork(modulePath, args, options) { + let execArgv = []; + if (global.v8debug || debugging) { + lastAddress += 1; + + execArgv = [`${debugArg[0]}=${lastAddress}`, '--nolazy']; + } + + if (!Array.isArray(args)) { + options = args; + args = []; + } + + options = options || {}; + options = { ...options, execArgv: execArgv }; + + return fork(modulePath, args, options); +} +debugFork.debugging = debugging; + +module.exports = debugFork; diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js new file mode 100644 index 0000000000..b51c1c1c5a --- /dev/null +++ b/src/meta/dependencies.js @@ -0,0 +1,72 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +const semver = require('semver'); +const winston = require('winston'); +const chalk = require('chalk'); + +const pkg = require('../../package.json'); +const { paths, pluginNamePattern } = require('../constants'); + +const Dependencies = module.exports; + +let depsMissing = false; +let depsOutdated = false; + +Dependencies.check = async function () { + const modules = Object.keys(pkg.dependencies); + + winston.verbose('Checking dependencies for outdated modules'); + + await Promise.all(modules.map(module => Dependencies.checkModule(module))); + + if (depsMissing) { + throw new Error('dependencies-missing'); + } else if (depsOutdated && global.env !== 'development') { + throw new Error('dependencies-out-of-date'); + } +}; + +Dependencies.checkModule = async function (moduleName) { + try { + let pkgData = await fs.promises.readFile(path.join(paths.nodeModules, moduleName, 'package.json'), 'utf8'); + pkgData = Dependencies.parseModuleData(moduleName, pkgData); + + const satisfies = Dependencies.doesSatisfy(pkgData, pkg.dependencies[moduleName]); + return satisfies; + } catch (err) { + if (err.code === 'ENOENT' && pluginNamePattern.test(moduleName)) { + winston.warn(`[meta/dependencies] Bundled plugin ${moduleName} not found, skipping dependency check.`); + return true; + } + throw err; + } +}; + +Dependencies.parseModuleData = function (moduleName, pkgData) { + try { + pkgData = JSON.parse(pkgData); + } catch (e) { + winston.warn(`[${chalk.red('missing')}] ${chalk.bold(moduleName)} is a required dependency but could not be found\n`); + depsMissing = true; + return null; + } + return pkgData; +}; + +Dependencies.doesSatisfy = function (moduleData, packageJSONVersion) { + if (!moduleData) { + return false; + } + const versionOk = !semver.validRange(packageJSONVersion) || + semver.satisfies(moduleData.version, packageJSONVersion); + const githubRepo = moduleData._resolved && moduleData._resolved.includes('//github.com'); + const satisfies = versionOk || githubRepo; + if (!satisfies) { + winston.warn(`[${chalk.yellow('outdated')}] ${chalk.bold(moduleData.name)} installed v${moduleData.version}, package.json requires ${packageJSONVersion}\n`); + depsOutdated = true; + } + return satisfies; +}; diff --git a/src/meta/errors.js b/src/meta/errors.js new file mode 100644 index 0000000000..34c19bed7f --- /dev/null +++ b/src/meta/errors.js @@ -0,0 +1,56 @@ +'use strict'; + +const winston = require('winston'); +const validator = require('validator'); +const cronJob = require('cron').CronJob; + +const db = require('../database'); +const analytics = require('../analytics'); + +const Errors = module.exports; + +let counters = {}; + +new cronJob('0 * * * * *', (() => { + Errors.writeData(); +}), null, true); + +Errors.writeData = async function () { + try { + const _counters = { ...counters }; + counters = {}; + const keys = Object.keys(_counters); + if (!keys.length) { + return; + } + + for (const key of keys) { + /* eslint-disable no-await-in-loop */ + await db.sortedSetIncrBy('errors:404', _counters[key], key); + } + } catch (err) { + winston.error(err.stack); + } +}; + +Errors.log404 = function (route) { + if (!route) { + return; + } + route = route.slice(0, 512).replace(/\/$/, ''); // remove trailing slashes + analytics.increment('errors:404'); + counters[route] = counters[route] || 0; + counters[route] += 1; +}; + +Errors.get = async function (escape) { + const data = await db.getSortedSetRevRangeWithScores('errors:404', 0, 199); + data.forEach((nfObject) => { + nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value; + }); + return data; +}; + +Errors.clear = async function () { + await db.delete('errors:404'); +}; diff --git a/src/meta/index.js b/src/meta/index.js new file mode 100644 index 0000000000..5f5ee85510 --- /dev/null +++ b/src/meta/index.js @@ -0,0 +1,73 @@ +'use strict'; + +const winston = require('winston'); +const os = require('os'); +const nconf = require('nconf'); + +const pubsub = require('../pubsub'); +const slugify = require('../slugify'); + +const Meta = module.exports; + +Meta.reloadRequired = false; + +Meta.configs = require('./configs'); +Meta.themes = require('./themes'); +Meta.js = require('./js'); +Meta.css = require('./css'); +Meta.settings = require('./settings'); +Meta.logs = require('./logs'); +Meta.errors = require('./errors'); +Meta.tags = require('./tags'); +Meta.dependencies = require('./dependencies'); +Meta.templates = require('./templates'); +Meta.blacklist = require('./blacklist'); +Meta.languages = require('./languages'); + + +/* Assorted */ +Meta.userOrGroupExists = async function (slug) { + if (!slug) { + throw new Error('[[error:invalid-data]]'); + } + const user = require('../user'); + const groups = require('../groups'); + slug = slugify(slug); + const [userExists, groupExists] = await Promise.all([ + user.existsBySlug(slug), + groups.existsBySlug(slug), + ]); + return userExists || groupExists; +}; + +if (nconf.get('isPrimary')) { + pubsub.on('meta:restart', (data) => { + if (data.hostname !== os.hostname()) { + restart(); + } + }); +} + +Meta.restart = function () { + pubsub.publish('meta:restart', { hostname: os.hostname() }); + restart(); +}; + +function restart() { + if (process.send) { + process.send({ + action: 'restart', + }); + } else { + winston.error('[meta.restart] Could not restart, are you sure NodeBB was started with `./nodebb start`?'); + } +} + +Meta.getSessionTTLSeconds = function () { + const ttlDays = 60 * 60 * 24 * Meta.config.loginDays; + const ttlSeconds = Meta.config.loginSeconds; + const ttl = ttlSeconds || ttlDays || 1209600; // Default to 14 days + return ttl; +}; + +require('../promisify')(Meta); diff --git a/src/meta/js.js b/src/meta/js.js new file mode 100644 index 0000000000..52b4b1e31d --- /dev/null +++ b/src/meta/js.js @@ -0,0 +1,140 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const util = require('util'); +const mkdirp = require('mkdirp'); +const rimraf = require('rimraf'); + +const rimrafAsync = util.promisify(rimraf); + +const file = require('../file'); +const plugins = require('../plugins'); +const minifier = require('./minifier'); + +const JS = module.exports; + +JS.scripts = { + base: [ + 'node_modules/@adactive/bootstrap-tagsinput/src/bootstrap-tagsinput.js', + 'node_modules/jquery-serializeobject/jquery.serializeObject.js', + 'node_modules/jquery-deserialize/src/jquery.deserialize.js', + 'public/vendor/bootbox/wrapper.js', + ], + + // plugins add entries into this object, + // they get linked into /build/public/src/modules + modules: { + '../admin/plugins/persona.js': 'themes/nodebb-theme-persona/public/admin.js', + 'persona/quickreply.js': 'themes/nodebb-theme-persona/public/modules/quickreply.js', + '../client/account/theme.js': 'themes/nodebb-theme-persona/public/settings.js', + }, +}; + +const basePath = path.resolve(__dirname, '../..'); + +async function linkModules() { + const { modules } = JS.scripts; + + await Promise.all([ + mkdirp(path.join(__dirname, '../../build/public/src/admin/plugins')), + mkdirp(path.join(__dirname, '../../build/public/src/client/plugins')), + ]); + + await Promise.all(Object.keys(modules).map(async (relPath) => { + const srcPath = path.join(__dirname, '../../', modules[relPath]); + const destPath = path.join(__dirname, '../../build/public/src/modules', relPath); + const [stats] = await Promise.all([ + fs.promises.stat(srcPath), + mkdirp(path.dirname(destPath)), + ]); + if (stats.isDirectory()) { + await file.linkDirs(srcPath, destPath, true); + } else { + await fs.promises.copyFile(srcPath, destPath); + } + })); +} + +const moduleDirs = ['modules', 'admin', 'client']; + +async function clearModules() { + const builtPaths = moduleDirs.map( + p => path.join(__dirname, '../../build/public/src', p) + ); + await Promise.all( + builtPaths.map(builtPath => rimrafAsync(builtPath)) + ); +} + +JS.buildModules = async function () { + await clearModules(); + + const fse = require('fs-extra'); + await fse.copy( + path.join(__dirname, `../../public/src`), + path.join(__dirname, `../../build/public/src`) + ); + + await linkModules(); +}; + +JS.linkStatics = async function () { + await rimrafAsync(path.join(__dirname, '../../build/public/plugins')); + + await Promise.all(Object.keys(plugins.staticDirs).map(async (mappedPath) => { + const sourceDir = plugins.staticDirs[mappedPath]; + const destDir = path.join(__dirname, '../../build/public/plugins', mappedPath); + + await mkdirp(path.dirname(destDir)); + await file.linkDirs(sourceDir, destDir, true); + })); +}; + +async function getBundleScriptList(target) { + const pluginDirectories = []; + + if (target === 'admin') { + target = 'acp'; + } + let pluginScripts = plugins[`${target}Scripts`].filter((path) => { + if (path.endsWith('.js')) { + return true; + } + + pluginDirectories.push(path); + return false; + }); + + await Promise.all(pluginDirectories.map(async (directory) => { + const scripts = await file.walk(directory); + pluginScripts = pluginScripts.concat(scripts); + })); + + pluginScripts = JS.scripts.base.concat(pluginScripts).map((script) => { + const srcPath = path.resolve(basePath, script).replace(/\\/g, '/'); + return { + srcPath: srcPath, + filename: path.relative(basePath, srcPath).replace(/\\/g, '/'), + }; + }); + + return pluginScripts; +} + +JS.buildBundle = async function (target, fork) { + const filename = `scripts-${target}.js`; + const files = await getBundleScriptList(target); + const minify = false; // webpack will minify in prod + const filePath = path.join(__dirname, '../../build/public', filename); + + await minifier.js.bundle({ + files: files, + filename: filename, + destPath: filePath, + }, minify, fork); +}; + +JS.killMinifier = function () { + minifier.killAll(); +}; diff --git a/src/meta/languages.js b/src/meta/languages.js new file mode 100644 index 0000000000..05406a1bf9 --- /dev/null +++ b/src/meta/languages.js @@ -0,0 +1,143 @@ +'use strict'; + +const _ = require('lodash'); +const nconf = require('nconf'); +const path = require('path'); +const fs = require('fs'); +const util = require('util'); +let mkdirp = require('mkdirp'); + +mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp); +const rimraf = require('rimraf'); + +const rimrafAsync = util.promisify(rimraf); + +const file = require('../file'); +const Plugins = require('../plugins'); +const { paths } = require('../constants'); + +const buildLanguagesPath = path.join(paths.baseDir, 'build/public/language'); +const coreLanguagesPath = path.join(paths.baseDir, 'public/language'); + +async function getTranslationMetadata() { + const paths = await file.walk(coreLanguagesPath); + let languages = []; + let namespaces = []; + + paths.forEach((p) => { + if (!p.endsWith('.json')) { + return; + } + + const rel = path.relative(coreLanguagesPath, p).split(/[/\\]/); + const language = rel.shift().replace('_', '-').replace('@', '-x-'); + const namespace = rel.join('/').replace(/\.json$/, ''); + + if (!language || !namespace) { + return; + } + + languages.push(language); + namespaces.push(namespace); + }); + + + languages = _.union(languages, Plugins.languageData.languages).sort().filter(Boolean); + namespaces = _.union(namespaces, Plugins.languageData.namespaces).sort().filter(Boolean); + const configLangs = nconf.get('languages'); + if (process.env.NODE_ENV === 'development' && Array.isArray(configLangs) && configLangs.length) { + languages = configLangs; + } + // save a list of languages to `${buildLanguagesPath}/metadata.json` + // avoids readdirs later on + await mkdirp(buildLanguagesPath); + const result = { + languages: languages, + namespaces: namespaces, + }; + await fs.promises.writeFile(path.join(buildLanguagesPath, 'metadata.json'), JSON.stringify(result)); + return result; +} + +async function writeLanguageFile(language, namespace, translations) { + const dev = process.env.NODE_ENV === 'development'; + const filePath = path.join(buildLanguagesPath, language, `${namespace}.json`); + + await mkdirp(path.dirname(filePath)); + await fs.promises.writeFile(filePath, JSON.stringify(translations, null, dev ? 2 : 0)); +} + +// for each language and namespace combination, +// run through core and all plugins to generate +// a full translation hash +async function buildTranslations(ref) { + const { namespaces } = ref; + const { languages } = ref; + const plugins = _.values(Plugins.pluginsData).filter(plugin => typeof plugin.languages === 'string'); + + const promises = []; + + namespaces.forEach((namespace) => { + languages.forEach((language) => { + promises.push(buildNamespaceLanguage(language, namespace, plugins)); + }); + }); + + await Promise.all(promises); +} + +async function buildNamespaceLanguage(lang, namespace, plugins) { + const translations = {}; + // core first + await assignFileToTranslations(translations, path.join(coreLanguagesPath, lang, `${namespace}.json`)); + + await Promise.all(plugins.map(pluginData => addPlugin(translations, pluginData, lang, namespace))); + + if (Object.keys(translations).length) { + await writeLanguageFile(lang, namespace, translations); + } +} + +async function addPlugin(translations, pluginData, lang, namespace) { + // if plugin doesn't have this namespace no need to continue + if (pluginData.languageData && !pluginData.languageData.namespaces.includes(namespace)) { + return; + } + + const pathToPluginLanguageFolder = path.join(paths.nodeModules, pluginData.id, pluginData.languages); + const defaultLang = pluginData.defaultLang || 'en-GB'; + + // for each plugin, fallback in this order: + // 1. correct language string (en-GB) + // 2. old language string (en_GB) + // 3. corrected plugin defaultLang (en-US) + // 4. old plugin defaultLang (en_US) + const langs = _.uniq([ + defaultLang.replace('-', '_').replace('-x-', '@'), + defaultLang.replace('_', '-').replace('@', '-x-'), + lang.replace('-', '_').replace('-x-', '@'), + lang, + ]); + + for (const language of langs) { + /* eslint-disable no-await-in-loop */ + await assignFileToTranslations(translations, path.join(pathToPluginLanguageFolder, language, `${namespace}.json`)); + } +} + +async function assignFileToTranslations(translations, path) { + try { + const fileData = await fs.promises.readFile(path, 'utf8'); + Object.assign(translations, JSON.parse(fileData)); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } +} + +exports.build = async function buildLanguages() { + await rimrafAsync(buildLanguagesPath); + const data = await getTranslationMetadata(); + await buildTranslations(data); +}; diff --git a/src/meta/logs.js b/src/meta/logs.js new file mode 100644 index 0000000000..4202e8de3d --- /dev/null +++ b/src/meta/logs.js @@ -0,0 +1,16 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +const Logs = module.exports; + +Logs.path = path.resolve(__dirname, '../../logs/output.log'); + +Logs.get = async function () { + return await fs.promises.readFile(Logs.path, 'utf-8'); +}; + +Logs.clear = async function () { + await fs.promises.truncate(Logs.path, 0); +}; diff --git a/src/meta/minifier.js b/src/meta/minifier.js new file mode 100644 index 0000000000..539ab2374b --- /dev/null +++ b/src/meta/minifier.js @@ -0,0 +1,256 @@ +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const uglify = require('uglify-es'); +const async = require('async'); +const winston = require('winston'); +const less = require('less'); +const postcss = require('postcss'); +const autoprefixer = require('autoprefixer'); +const clean = require('postcss-clean'); + +const fork = require('./debugFork'); +require('../file'); // for graceful-fs + +const Minifier = module.exports; + +const pool = []; +const free = []; + +let maxThreads = 0; + +Object.defineProperty(Minifier, 'maxThreads', { + get: function () { + return maxThreads; + }, + set: function (val) { + maxThreads = val; + if (!process.env.minifier_child) { + winston.verbose(`[minifier] utilizing a maximum of ${maxThreads} additional threads`); + } + }, + configurable: true, + enumerable: true, +}); + +Minifier.maxThreads = os.cpus().length - 1; + +Minifier.killAll = function () { + pool.forEach((child) => { + child.kill('SIGTERM'); + }); + + pool.length = 0; + free.length = 0; +}; + +function getChild() { + if (free.length) { + return free.shift(); + } + + const proc = fork(__filename, [], { + cwd: __dirname, + env: { + minifier_child: true, + }, + }); + pool.push(proc); + + return proc; +} + +function freeChild(proc) { + proc.removeAllListeners(); + free.push(proc); +} + +function removeChild(proc) { + const i = pool.indexOf(proc); + if (i !== -1) { + pool.splice(i, 1); + } +} + +function forkAction(action) { + return new Promise((resolve, reject) => { + const proc = getChild(); + proc.on('message', (message) => { + freeChild(proc); + + if (message.type === 'error') { + return reject(new Error(message.message)); + } + + if (message.type === 'end') { + resolve(message.result); + } + }); + proc.on('error', (err) => { + proc.kill(); + removeChild(proc); + reject(err); + }); + + proc.send({ + type: 'action', + action: action, + }); + }); +} + +const actions = {}; + +if (process.env.minifier_child) { + process.on('message', async (message) => { + if (message.type === 'action') { + const { action } = message; + if (typeof actions[action.act] !== 'function') { + process.send({ + type: 'error', + message: 'Unknown action', + }); + return; + } + try { + const result = await actions[action.act](action); + process.send({ + type: 'end', + result: result, + }); + } catch (err) { + process.send({ + type: 'error', + message: err.stack || err.message || 'unknown error', + }); + } + } + }); +} + +async function executeAction(action, fork) { + if (fork && (pool.length - free.length) < Minifier.maxThreads) { + return await forkAction(action); + } + if (typeof actions[action.act] !== 'function') { + throw new Error('Unknown action'); + } + return await actions[action.act](action); +} + +actions.concat = async function concat(data) { + if (data.files && data.files.length) { + const files = await async.mapLimit(data.files, 1000, async ref => await fs.promises.readFile(ref.srcPath, 'utf8')); + const output = files.join('\n;'); + await fs.promises.writeFile(data.destPath, output); + } +}; + +actions.minifyJS_batch = async function minifyJS_batch(data) { + await async.eachLimit(data.files, 100, async (fileObj) => { + const source = await fs.promises.readFile(fileObj.srcPath, 'utf8'); + const filesToMinify = [ + { + srcPath: fileObj.srcPath, + filename: fileObj.filename, + source: source, + }, + ]; + + await minifyAndSave({ + files: filesToMinify, + destPath: fileObj.destPath, + filename: fileObj.filename, + }); + }); +}; + +actions.minifyJS = async function minifyJS(data) { + const filesToMinify = await async.mapLimit(data.files, 1000, async (fileObj) => { + const source = await fs.promises.readFile(fileObj.srcPath, 'utf8'); + return { + srcPath: fileObj.srcPath, + filename: fileObj.filename, + source: source, + }; + }); + await minifyAndSave({ + files: filesToMinify, + destPath: data.destPath, + filename: data.filename, + }); +}; + +async function minifyAndSave(data) { + const scripts = {}; + data.files.forEach((ref) => { + if (ref && ref.filename && ref.source) { + scripts[ref.filename] = ref.source; + } + }); + + const minified = uglify.minify(scripts, { + sourceMap: { + filename: data.filename, + url: `${String(data.filename).split(/[/\\]/).pop()}.map`, + includeSources: true, + }, + compress: false, + }); + + if (minified.error) { + throw new Error(`Error minifying ${minified.error.filename}\n${minified.error.stack}`); + } + await Promise.all([ + fs.promises.writeFile(data.destPath, minified.code), + fs.promises.writeFile(`${data.destPath}.map`, minified.map), + ]); +} + +Minifier.js = {}; +Minifier.js.bundle = async function (data, minify, fork) { + return await executeAction({ + act: minify ? 'minifyJS' : 'concat', + files: data.files, + filename: data.filename, + destPath: data.destPath, + }, fork); +}; + +Minifier.js.minifyBatch = async function (scripts, fork) { + return await executeAction({ + act: 'minifyJS_batch', + files: scripts, + }, fork); +}; + +actions.buildCSS = async function buildCSS(data) { + const lessOutput = await less.render(data.source, { + paths: data.paths, + javascriptEnabled: false, + }); + + const postcssArgs = [autoprefixer]; + if (data.minify) { + postcssArgs.push(clean({ + processImportFrom: ['local'], + })); + } + const result = await postcss(postcssArgs).process(lessOutput.css, { + from: undefined, + }); + return { code: result.css }; +}; + +Minifier.css = {}; +Minifier.css.bundle = async function (source, paths, minify, fork) { + return await executeAction({ + act: 'buildCSS', + source: source, + paths: paths, + minify: minify, + }, fork); +}; + +require('../promisify')(exports); diff --git a/src/meta/settings.js b/src/meta/settings.js new file mode 100644 index 0000000000..e6567ecbe7 --- /dev/null +++ b/src/meta/settings.js @@ -0,0 +1,127 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const plugins = require('../plugins'); +const Meta = require('./index'); +const pubsub = require('../pubsub'); +const cache = require('../cache'); + +const Settings = module.exports; + +Settings.get = async function (hash) { + const cached = cache.get(`settings:${hash}`); + if (cached) { + return _.cloneDeep(cached); + } + const [data, sortedLists] = await Promise.all([ + db.getObject(`settings:${hash}`), + db.getSetMembers(`settings:${hash}:sorted-lists`), + ]); + const values = data || {}; + await Promise.all(sortedLists.map(async (list) => { + const members = await db.getSortedSetRange(`settings:${hash}:sorted-list:${list}`, 0, -1); + const keys = members.map(order => `settings:${hash}:sorted-list:${list}:${order}`); + + values[list] = []; + + const objects = await db.getObjects(keys); + objects.forEach((obj) => { + values[list].push(obj); + }); + })); + + const result = await plugins.hooks.fire('filter:settings.get', { plugin: hash, values: values }); + cache.set(`settings:${hash}`, result.values); + return _.cloneDeep(result.values); +}; + +Settings.getOne = async function (hash, field) { + const data = await Settings.get(hash); + return data[field] !== undefined ? data[field] : null; +}; + +Settings.set = async function (hash, values, quiet) { + quiet = quiet || false; + + ({ plugin: hash, settings: values, quiet } = await plugins.hooks.fire('filter:settings.set', { plugin: hash, settings: values, quiet })); + + const sortedListData = {}; + for (const [key, value] of Object.entries(values)) { + if (Array.isArray(value) && typeof value[0] !== 'string') { + sortedListData[key] = value; + delete values[key]; + } + } + const sortedLists = Object.keys(sortedListData); + + if (sortedLists.length) { + // Remove provided (but empty) sorted lists from the hash set + await db.setRemove(`settings:${hash}:sorted-lists`, sortedLists.filter(list => !sortedListData[list].length)); + await db.setAdd(`settings:${hash}:sorted-lists`, sortedLists); + + await Promise.all(sortedLists.map(async (list) => { + const numItems = await db.sortedSetCard(`settings:${hash}:sorted-list:${list}`); + const deleteKeys = [`settings:${hash}:sorted-list:${list}`]; + for (let x = 0; x < numItems; x++) { + deleteKeys.push(`settings:${hash}:sorted-list:${list}:${x}`); + } + await db.deleteAll(deleteKeys); + })); + + const sortedSetData = []; + const objectData = []; + sortedLists.forEach((list) => { + const arr = sortedListData[list]; + arr.forEach((data, order) => { + sortedSetData.push([`settings:${hash}:sorted-list:${list}`, order, order]); + objectData.push([`settings:${hash}:sorted-list:${list}:${order}`, data]); + }); + }); + + await Promise.all([ + db.sortedSetAddBulk(sortedSetData), + db.setObjectBulk(objectData), + ]); + } + + if (Object.keys(values).length) { + await db.setObject(`settings:${hash}`, values); + } + + cache.del(`settings:${hash}`); + + plugins.hooks.fire('action:settings.set', { + plugin: hash, + settings: { ...values, ...sortedListData }, // Add back sorted list data to values hash + quiet, + }); + + pubsub.publish(`action:settings.set.${hash}`, values); + if (!Meta.reloadRequired && !quiet) { + Meta.reloadRequired = true; + } +}; + +Settings.setOne = async function (hash, field, value) { + const data = {}; + data[field] = value; + await Settings.set(hash, data); +}; + +Settings.setOnEmpty = async function (hash, values) { + const settings = await Settings.get(hash) || {}; + const empty = {}; + + Object.keys(values).forEach((key) => { + if (!settings.hasOwnProperty(key)) { + empty[key] = values[key]; + } + }); + + + if (Object.keys(empty).length) { + await Settings.set(hash, empty); + } +}; diff --git a/src/meta/tags.js b/src/meta/tags.js new file mode 100644 index 0000000000..fc2bd1005e --- /dev/null +++ b/src/meta/tags.js @@ -0,0 +1,269 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); + +const plugins = require('../plugins'); +const Meta = require('./index'); +const utils = require('../utils'); + +const Tags = module.exports; + +const url = nconf.get('url'); +const relative_path = nconf.get('relative_path'); +const upload_url = nconf.get('upload_url'); + +Tags.parse = async (req, data, meta, link) => { + // Meta tags + const defaultTags = [{ + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, { + name: 'content-type', + content: 'text/html; charset=UTF-8', + noEscape: true, + }, { + name: 'apple-mobile-web-app-capable', + content: 'yes', + }, { + name: 'mobile-web-app-capable', + content: 'yes', + }, { + property: 'og:site_name', + content: Meta.config.title || 'NodeBB', + }, { + name: 'msapplication-badge', + content: `frequency=30; polling-uri=${url}/sitemap.xml`, + noEscape: true, + }, { + name: 'theme-color', + content: Meta.config.themeColor || '#ffffff', + }]; + + if (Meta.config.keywords) { + defaultTags.push({ + name: 'keywords', + content: Meta.config.keywords, + }); + } + + if (Meta.config['brand:logo']) { + defaultTags.push({ + name: 'msapplication-square150x150logo', + content: Meta.config['brand:logo'], + noEscape: true, + }); + } + + const faviconPath = `${relative_path}/assets/uploads/system/favicon.ico`; + const cacheBuster = `${Meta.config['cache-buster'] ? `?${Meta.config['cache-buster']}` : ''}`; + + // Link Tags + const defaultLinks = [{ + rel: 'icon', + type: 'image/x-icon', + href: `${faviconPath}${cacheBuster}`, + }, { + rel: 'manifest', + href: `${relative_path}/manifest.webmanifest`, + crossorigin: `use-credentials`, + }]; + + if (plugins.hooks.hasListeners('filter:search.query')) { + defaultLinks.push({ + rel: 'search', + type: 'application/opensearchdescription+xml', + title: utils.escapeHTML(String(Meta.config.title || Meta.config.browserTitle || 'NodeBB')), + href: `${relative_path}/osd.xml`, + }); + } + + // Touch icons for mobile-devices + if (Meta.config['brand:touchIcon']) { + defaultLinks.push({ + rel: 'apple-touch-icon', + href: `${relative_path + upload_url}/system/touchicon-orig.png`, + }, { + rel: 'icon', + sizes: '36x36', + href: `${relative_path + upload_url}/system/touchicon-36.png`, + }, { + rel: 'icon', + sizes: '48x48', + href: `${relative_path + upload_url}/system/touchicon-48.png`, + }, { + rel: 'icon', + sizes: '72x72', + href: `${relative_path + upload_url}/system/touchicon-72.png`, + }, { + rel: 'icon', + sizes: '96x96', + href: `${relative_path + upload_url}/system/touchicon-96.png`, + }, { + rel: 'icon', + sizes: '144x144', + href: `${relative_path + upload_url}/system/touchicon-144.png`, + }, { + rel: 'icon', + sizes: '192x192', + href: `${relative_path + upload_url}/system/touchicon-192.png`, + }); + } else { + defaultLinks.push({ + rel: 'apple-touch-icon', + href: `${relative_path}/assets/images/touch/512.png`, + }, { + rel: 'icon', + sizes: '36x36', + href: `${relative_path}/assets/images/touch/36.png`, + }, { + rel: 'icon', + sizes: '48x48', + href: `${relative_path}/assets/images/touch/48.png`, + }, { + rel: 'icon', + sizes: '72x72', + href: `${relative_path}/assets/images/touch/72.png`, + }, { + rel: 'icon', + sizes: '96x96', + href: `${relative_path}/assets/images/touch/96.png`, + }, { + rel: 'icon', + sizes: '144x144', + href: `${relative_path}/assets/images/touch/144.png`, + }, { + rel: 'icon', + sizes: '192x192', + href: `${relative_path}/assets/images/touch/192.png`, + }, { + rel: 'icon', + sizes: '512x512', + href: `${relative_path}/assets/images/touch/512.png`, + }); + } + + const results = await utils.promiseParallel({ + tags: plugins.hooks.fire('filter:meta.getMetaTags', { req: req, data: data, tags: defaultTags }), + links: plugins.hooks.fire('filter:meta.getLinkTags', { req: req, data: data, links: defaultLinks }), + }); + + meta = results.tags.tags.concat(meta || []).map((tag) => { + if (!tag || typeof tag.content !== 'string') { + winston.warn('Invalid meta tag. ', tag); + return tag; + } + + if (!tag.noEscape) { + const attributes = Object.keys(tag); + attributes.forEach((attr) => { + tag[attr] = utils.escapeHTML(String(tag[attr])); + }); + } + + return tag; + }); + + await addSiteOGImage(meta); + + addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB'); + const ogUrl = url + (req.originalUrl !== '/' ? stripRelativePath(req.originalUrl) : ''); + addIfNotExists(meta, 'property', 'og:url', ogUrl); + addIfNotExists(meta, 'name', 'description', Meta.config.description); + addIfNotExists(meta, 'property', 'og:description', Meta.config.description); + + link = results.links.links.concat(link || []).map((tag) => { + if (!tag.noEscape) { + const attributes = Object.keys(tag); + attributes.forEach((attr) => { + tag[attr] = utils.escapeHTML(String(tag[attr])); + }); + } + + return tag; + }); + + return { meta, link }; +}; + +function addIfNotExists(meta, keyName, tagName, value) { + let exists = false; + meta.forEach((tag) => { + if (tag[keyName] === tagName) { + exists = true; + } + }); + + if (!exists && value) { + const data = { + content: utils.escapeHTML(String(value)), + }; + data[keyName] = tagName; + meta.push(data); + } +} + +function stripRelativePath(url) { + if (url.startsWith(relative_path)) { + return url.slice(relative_path.length); + } + + return url; +} + +async function addSiteOGImage(meta) { + const key = Meta.config['og:image'] ? 'og:image' : 'brand:logo'; + let ogImage = stripRelativePath(Meta.config[key] || ''); + if (ogImage && !ogImage.startsWith('http')) { + ogImage = url + ogImage; + } + + const { images } = await plugins.hooks.fire('filter:meta.addSiteOGImage', { + images: [{ + url: ogImage || `${url}/assets/images/logo@3x.png`, + width: ogImage ? Meta.config[`${key}:width`] : 963, + height: ogImage ? Meta.config[`${key}:height`] : 225, + }], + }); + + const properties = ['url', 'secure_url', 'type', 'width', 'height', 'alt']; + images.forEach((image) => { + for (const property of properties) { + if (image.hasOwnProperty(property)) { + switch (property) { + case 'url': { + meta.push({ + property: 'og:image', + content: image.url, + noEscape: true, + }, { + property: 'og:image:url', + content: image.url, + noEscape: true, + }); + break; + } + + case 'secure_url': { + meta.push({ + property: `og:${property}`, + content: image[property], + noEscape: true, + }); + break; + } + + case 'type': + case 'alt': + case 'width': + case 'height': { + meta.push({ + property: `og:image:${property}`, + content: String(image[property]), + }); + } + } + } + } + }); +} diff --git a/src/meta/templates.js b/src/meta/templates.js new file mode 100644 index 0000000000..39e06b828c --- /dev/null +++ b/src/meta/templates.js @@ -0,0 +1,139 @@ +'use strict'; + +const util = require('util'); +let mkdirp = require('mkdirp'); + +mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp); +const rimraf = require('rimraf'); +const winston = require('winston'); +const path = require('path'); +const fs = require('fs'); + +const nconf = require('nconf'); +const _ = require('lodash'); +const Benchpress = require('benchpressjs'); + +const plugins = require('../plugins'); +const file = require('../file'); +const { themeNamePattern, paths } = require('../constants'); + +const viewsPath = nconf.get('views_dir'); + +const Templates = module.exports; + +async function processImports(paths, templatePath, source) { + const regex = //; + + const matches = source.match(regex); + + if (!matches) { + return source; + } + + const partial = matches[1]; + if (paths[partial] && templatePath !== partial) { + const partialSource = await fs.promises.readFile(paths[partial], 'utf8'); + source = source.replace(regex, partialSource); + return await processImports(paths, templatePath, source); + } + + winston.warn(`[meta/templates] Partial not loaded: ${matches[1]}`); + source = source.replace(regex, ''); + + return await processImports(paths, templatePath, source); +} +Templates.processImports = processImports; + +async function getTemplateDirs(activePlugins) { + const pluginTemplates = activePlugins.map((id) => { + if (themeNamePattern.test(id)) { + return nconf.get('theme_templates_path'); + } + if (!plugins.pluginsData[id]) { + return ''; + } + return path.join(paths.nodeModules, id, plugins.pluginsData[id].templates || 'templates'); + }).filter(Boolean); + + let themeConfig = require(nconf.get('theme_config')); + let theme = themeConfig.baseTheme; + + let themePath; + let themeTemplates = []; + while (theme) { + themePath = path.join(nconf.get('themes_path'), theme); + themeConfig = require(path.join(themePath, 'theme.json')); + + themeTemplates.push(path.join(themePath, themeConfig.templates || 'templates')); + theme = themeConfig.baseTheme; + } + + themeTemplates.push(nconf.get('base_templates_path')); + themeTemplates = _.uniq(themeTemplates.reverse()); + + const coreTemplatesPath = nconf.get('core_templates_path'); + + let templateDirs = _.uniq([coreTemplatesPath].concat(themeTemplates, pluginTemplates)); + + templateDirs = await Promise.all(templateDirs.map(async path => (await file.exists(path) ? path : false))); + return templateDirs.filter(Boolean); +} + +async function getTemplateFiles(dirs) { + const buckets = await Promise.all(dirs.map(async (dir) => { + let files = await file.walk(dir); + files = files.filter(path => path.endsWith('.tpl')).map(file => ({ + name: path.relative(dir, file).replace(/\\/g, '/'), + path: file, + })); + return files; + })); + + const dict = {}; + buckets.forEach((files) => { + files.forEach((file) => { + dict[file.name] = file.path; + }); + }); + + return dict; +} + +async function compileTemplate(filename, source) { + let paths = await file.walk(viewsPath); + paths = _.fromPairs(paths.map((p) => { + const relative = path.relative(viewsPath, p).replace(/\\/g, '/'); + return [relative, p]; + })); + + source = await processImports(paths, filename, source); + const compiled = await Benchpress.precompile(source, { filename }); + return await fs.promises.writeFile(path.join(viewsPath, filename.replace(/\.tpl$/, '.js')), compiled); +} +Templates.compileTemplate = compileTemplate; + +async function compile() { + const _rimraf = util.promisify(rimraf); + + await _rimraf(viewsPath); + await mkdirp(viewsPath); + + let files = await plugins.getActive(); + files = await getTemplateDirs(files); + files = await getTemplateFiles(files); + + await Promise.all(Object.keys(files).map(async (name) => { + const filePath = files[name]; + let imported = await fs.promises.readFile(filePath, 'utf8'); + imported = await processImports(files, name, imported); + + await mkdirp(path.join(viewsPath, path.dirname(name))); + + await fs.promises.writeFile(path.join(viewsPath, name), imported); + const compiled = await Benchpress.precompile(imported, { filename: name }); + await fs.promises.writeFile(path.join(viewsPath, name.replace(/\.tpl$/, '.js')), compiled); + })); + + winston.verbose('[meta/templates] Successfully compiled templates.'); +} +Templates.compile = compile; diff --git a/src/meta/themes.js b/src/meta/themes.js new file mode 100644 index 0000000000..d5e37ca3c2 --- /dev/null +++ b/src/meta/themes.js @@ -0,0 +1,167 @@ +'use strict'; + +const path = require('path'); +const nconf = require('nconf'); +const winston = require('winston'); +const _ = require('lodash'); +const fs = require('fs'); + +const file = require('../file'); +const Meta = require('./index'); +const events = require('../events'); +const utils = require('../utils'); +const { themeNamePattern } = require('../constants'); + +const Themes = module.exports; + +Themes.get = async () => { + const themePath = nconf.get('themes_path'); + if (typeof themePath !== 'string') { + return []; + } + + let themes = await getThemes(themePath); + themes = _.flatten(themes).filter(Boolean); + themes = await Promise.all(themes.map(async (theme) => { + const config = path.join(themePath, theme, 'theme.json'); + const pack = path.join(themePath, theme, 'package.json'); + try { + const [configFile, packageFile] = await Promise.all([ + fs.promises.readFile(config, 'utf8'), + fs.promises.readFile(pack, 'utf8'), + ]); + const configObj = JSON.parse(configFile); + const packageObj = JSON.parse(packageFile); + + configObj.id = packageObj.name; + + // Minor adjustments for API output + configObj.type = 'local'; + if (configObj.screenshot) { + configObj.screenshot_url = `${nconf.get('relative_path')}/css/previews/${encodeURIComponent(configObj.id)}`; + } else { + configObj.screenshot_url = `${nconf.get('relative_path')}/assets/images/themes/default.png`; + } + + return configObj; + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + + winston.error(`[themes] Unable to parse theme.json ${theme}`); + return false; + } + })); + + return themes.filter(Boolean); +}; + +async function getThemes(themePath) { + let dirs = await fs.promises.readdir(themePath); + dirs = dirs.filter(dir => themeNamePattern.test(dir) || dir.startsWith('@')); + return await Promise.all(dirs.map(async (dir) => { + try { + const dirpath = path.join(themePath, dir); + const stat = await fs.promises.stat(dirpath); + if (!stat.isDirectory()) { + return false; + } + + if (!dir.startsWith('@')) { + return dir; + } + + const themes = await getThemes(path.join(themePath, dir)); + return themes.map(theme => path.join(dir, theme)); + } catch (err) { + if (err.code === 'ENOENT') { + return false; + } + + throw err; + } + })); +} + +Themes.set = async (data) => { + switch (data.type) { + case 'local': { + const current = await Meta.configs.get('theme:id'); + + if (current !== data.id) { + const pathToThemeJson = path.join(nconf.get('themes_path'), data.id, 'theme.json'); + if (!pathToThemeJson.startsWith(nconf.get('themes_path'))) { + throw new Error('[[error:invalid-theme-id]]'); + } + + let config = await fs.promises.readFile(pathToThemeJson, 'utf8'); + config = JSON.parse(config); + + // Re-set the themes path (for when NodeBB is reloaded) + Themes.setPath(config); + + await Meta.configs.setMultiple({ + 'theme:type': data.type, + 'theme:id': data.id, + 'theme:staticDir': config.staticDir ? config.staticDir : '', + 'theme:templates': config.templates ? config.templates : '', + 'theme:src': '', + bootswatchSkin: '', + }); + + await events.log({ + type: 'theme-set', + uid: parseInt(data.uid, 10) || 0, + ip: data.ip || '127.0.0.1', + text: data.id, + }); + + Meta.reloadRequired = true; + } + break; + } + case 'bootswatch': + await Meta.configs.setMultiple({ + 'theme:src': data.src, + bootswatchSkin: data.id.toLowerCase(), + }); + break; + } +}; + +Themes.setupPaths = async () => { + const data = await utils.promiseParallel({ + themesData: Themes.get(), + currentThemeId: Meta.configs.get('theme:id'), + }); + + const themeId = data.currentThemeId || 'nodebb-theme-persona'; + + if (process.env.NODE_ENV === 'development') { + winston.info(`[themes] Using theme ${themeId}`); + } + + const themeObj = data.themesData.find(themeObj => themeObj.id === themeId); + + if (!themeObj) { + throw new Error('[[error:theme-not-found]]'); + } + + Themes.setPath(themeObj); +}; + +Themes.setPath = function (themeObj) { + // Theme's templates path + let themePath = nconf.get('base_templates_path'); + const fallback = path.join(nconf.get('themes_path'), themeObj.id, 'templates'); + + if (themeObj.templates) { + themePath = path.join(nconf.get('themes_path'), themeObj.id, themeObj.templates); + } else if (file.existsSync(fallback)) { + themePath = fallback; + } + + nconf.set('theme_templates_path', themePath); + nconf.set('theme_config', path.join(nconf.get('themes_path'), themeObj.id, 'theme.json')); +}; diff --git a/src/middleware/admin.js b/src/middleware/admin.js new file mode 100644 index 0000000000..e3f11f2f5a --- /dev/null +++ b/src/middleware/admin.js @@ -0,0 +1,176 @@ +'use strict'; + +const winston = require('winston'); +const jsesc = require('jsesc'); +const nconf = require('nconf'); +const semver = require('semver'); + +const user = require('../user'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); +const utils = require('../utils'); +const versions = require('../admin/versions'); +const helpers = require('./helpers'); + +const controllers = { + api: require('../controllers/api'), + helpers: require('../controllers/helpers'), +}; + +const middleware = module.exports; + +middleware.buildHeader = helpers.try(async (req, res, next) => { + res.locals.renderAdminHeader = true; + if (req.method === 'GET') { + await require('./index').applyCSRFasync(req, res); + } + + res.locals.config = await controllers.api.loadConfig(req); + next(); +}); + +middleware.renderHeader = async (req, res, data) => { + const custom_header = { + plugins: [], + authentication: [], + }; + res.locals.config = res.locals.config || {}; + + const results = await utils.promiseParallel({ + userData: user.getUserFields(req.uid, ['username', 'userslug', 'email', 'picture', 'email:confirmed']), + scripts: getAdminScripts(), + custom_header: plugins.hooks.fire('filter:admin.header.build', custom_header), + configs: meta.configs.list(), + latestVersion: getLatestVersion(), + privileges: privileges.admin.get(req.uid), + tags: meta.tags.parse(req, {}, [], []), + }); + + const { userData } = results; + userData.uid = req.uid; + userData['email:confirmed'] = userData['email:confirmed'] === 1; + userData.privileges = results.privileges; + + let acpPath = req.path.slice(1).split('/'); + acpPath.forEach((path, i) => { + acpPath[i] = path.charAt(0).toUpperCase() + path.slice(1); + }); + acpPath = acpPath.join(' > '); + + const version = nconf.get('version'); + + res.locals.config.userLang = res.locals.config.acpLang || res.locals.config.userLang; + let templateValues = { + config: res.locals.config, + configJSON: jsesc(JSON.stringify(res.locals.config), { isScriptContext: true }), + relative_path: res.locals.config.relative_path, + adminConfigJSON: encodeURIComponent(JSON.stringify(results.configs)), + metaTags: results.tags.meta, + linkTags: results.tags.link, + user: userData, + userJSON: jsesc(JSON.stringify(userData), { isScriptContext: true }), + plugins: results.custom_header.plugins, + authentication: results.custom_header.authentication, + scripts: results.scripts, + 'cache-buster': meta.config['cache-buster'] || '', + env: !!process.env.NODE_ENV, + title: `${acpPath || 'Dashboard'} | NodeBB Admin Control Panel`, + bodyClass: data.bodyClass, + version: version, + latestVersion: results.latestVersion, + upgradeAvailable: results.latestVersion && semver.gt(results.latestVersion, version), + showManageMenu: results.privileges.superadmin || ['categories', 'privileges', 'users', 'admins-mods', 'groups', 'tags', 'settings'].some(priv => results.privileges[`admin:${priv}`]), + }; + + templateValues.template = { name: res.locals.template }; + templateValues.template[res.locals.template] = true; + ({ templateData: templateValues } = await plugins.hooks.fire('filter:middleware.renderAdminHeader', { + req, + res, + templateData: templateValues, + data, + })); + + return await req.app.renderAsync('admin/header', templateValues); +}; + +async function getAdminScripts() { + const scripts = await plugins.hooks.fire('filter:admin.scripts.get', []); + return scripts.map(script => ({ src: script })); +} + +async function getLatestVersion() { + try { + const result = await versions.getLatestVersion(); + return result; + } catch (err) { + winston.error(`[acp] Failed to fetch latest version${err.stack}`); + } + return null; +} + +middleware.renderFooter = async function (req, res, data) { + return await req.app.renderAsync('admin/footer', data); +}; + +middleware.checkPrivileges = helpers.try(async (req, res, next) => { + // Kick out guests, obviously + if (req.uid <= 0) { + return controllers.helpers.notAllowed(req, res); + } + + // Otherwise, check for privilege based on page (if not in mapping, deny access) + const path = req.path.replace(/^(\/api)?(\/v3)?\/admin\/?/g, ''); + if (path) { + const privilege = privileges.admin.resolve(path); + if (!await privileges.admin.can(privilege, req.uid)) { + return controllers.helpers.notAllowed(req, res); + } + } else { + // If accessing /admin, check for any valid admin privs + const privilegeSet = await privileges.admin.get(req.uid); + if (!Object.values(privilegeSet).some(Boolean)) { + return controllers.helpers.notAllowed(req, res); + } + } + + // If user does not have password + const hasPassword = await user.hasPassword(req.uid); + if (!hasPassword) { + return next(); + } + + // Reject if they need to re-login (due to ACP timeout), otherwise extend logout timer + const loginTime = req.session.meta ? req.session.meta.datetime : 0; + const adminReloginDuration = meta.config.adminReloginDuration * 60000; + const disabled = meta.config.adminReloginDuration === 0; + if (disabled || (loginTime && parseInt(loginTime, 10) > Date.now() - adminReloginDuration)) { + const timeLeft = parseInt(loginTime, 10) - (Date.now() - adminReloginDuration); + if (req.session.meta && timeLeft < Math.min(60000, adminReloginDuration)) { + req.session.meta.datetime += Math.min(60000, adminReloginDuration); + } + + return next(); + } + + let returnTo = req.path; + if (nconf.get('relative_path')) { + returnTo = req.path.replace(new RegExp(`^${nconf.get('relative_path')}`), ''); + } + returnTo = returnTo.replace(/^\/api/, ''); + + req.session.returnTo = returnTo; + req.session.forceLogin = 1; + + await plugins.hooks.fire('response:auth.relogin', { req, res }); + if (res.headersSent) { + return; + } + + if (res.locals.isAPI) { + res.status(401).json({}); + } else { + res.redirect(`${nconf.get('relative_path')}/login?local=1`); + } +}); diff --git a/src/middleware/assert.js b/src/middleware/assert.js new file mode 100644 index 0000000000..a9130edcaf --- /dev/null +++ b/src/middleware/assert.js @@ -0,0 +1,141 @@ +'use strict'; + +/** + * The middlewares here strictly act to "assert" validity of the incoming + * payload and throw an error otherwise. + */ + +const path = require('path'); +const nconf = require('nconf'); + +const file = require('../file'); +const user = require('../user'); +const groups = require('../groups'); +const topics = require('../topics'); +const posts = require('../posts'); +const messaging = require('../messaging'); +const flags = require('../flags'); +const slugify = require('../slugify'); + +const helpers = require('./helpers'); +const controllerHelpers = require('../controllers/helpers'); + +const Assert = module.exports; + +Assert.user = helpers.try(async (req, res, next) => { + if (!await user.exists(req.params.uid)) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-user]]')); + } + + next(); +}); + +Assert.group = helpers.try(async (req, res, next) => { + const name = await groups.getGroupNameByGroupSlug(req.params.slug); + if (!name || !await groups.exists(name)) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-group]]')); + } + + next(); +}); + +Assert.topic = helpers.try(async (req, res, next) => { + if (!await topics.exists(req.params.tid)) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-topic]]')); + } + + next(); +}); + +Assert.post = helpers.try(async (req, res, next) => { + if (!await posts.exists(req.params.pid)) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-post]]')); + } + + next(); +}); + +Assert.flag = helpers.try(async (req, res, next) => { + const canView = await flags.canView(req.params.flagId, req.uid); + if (!canView) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-flag]]')); + } + + next(); +}); + +Assert.path = helpers.try(async (req, res, next) => { + // file: URL support + if (req.body.path.startsWith('file:///')) { + req.body.path = new URL(req.body.path).pathname; + } + + // Strip upload_url if found + if (req.body.path.startsWith(nconf.get('upload_url'))) { + req.body.path = req.body.path.slice(nconf.get('upload_url').length); + } + + const pathToFile = path.join(nconf.get('upload_path'), req.body.path); + res.locals.cleanedPath = pathToFile; + + // Guard against path traversal + if (!pathToFile.startsWith(nconf.get('upload_path'))) { + return controllerHelpers.formatApiResponse(403, res, new Error('[[error:invalid-path]]')); + } + + if (!await file.exists(pathToFile)) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:invalid-path]]')); + } + + next(); +}); + +Assert.folderName = helpers.try(async (req, res, next) => { + const folderName = slugify(path.basename(req.body.folderName.trim())); + const folderPath = path.join(res.locals.cleanedPath, folderName); + + // slugify removes invalid characters, folderName may become empty + if (!folderName) { + return controllerHelpers.formatApiResponse(403, res, new Error('[[error:invalid-path]]')); + } + if (await file.exists(folderPath)) { + return controllerHelpers.formatApiResponse(403, res, new Error('[[error:folder-exists]]')); + } + + res.locals.folderPath = folderPath; + + next(); +}); + +Assert.room = helpers.try(async (req, res, next) => { + if (!isFinite(req.params.roomId)) { + return controllerHelpers.formatApiResponse(400, res, new Error('[[error:invalid-data]]')); + } + + const [exists, inRoom] = await Promise.all([ + await messaging.roomExists(req.params.roomId), + await messaging.isUserInRoom(req.uid, req.params.roomId), + ]); + + if (!exists) { + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:chat-room-does-not-exist]]')); + } + + if (!inRoom) { + return controllerHelpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + next(); +}); + +Assert.message = helpers.try(async (req, res, next) => { + if ( + !isFinite(req.params.mid) || + !(await messaging.messageExists(req.params.mid)) || + !(await messaging.canViewMessage(req.params.mid, req.params.roomId, req.uid)) + ) { + return controllerHelpers.formatApiResponse(400, res, new Error('[[error:invalid-mid]]')); + } + + next(); +}); diff --git a/src/middleware/expose.js b/src/middleware/expose.js new file mode 100644 index 0000000000..f6251a146d --- /dev/null +++ b/src/middleware/expose.js @@ -0,0 +1,49 @@ +'use strict'; + +/** + * The middlewares here strictly act to "expose" certain values from the database, + * into `res.locals` for use in middlewares and/or controllers down the line + */ + +const user = require('../user'); +const privileges = require('../privileges'); +const utils = require('../utils'); + +module.exports = function (middleware) { + middleware.exposeAdmin = async (req, res, next) => { + // Unlike `requireAdmin`, this middleware just checks the uid, and sets `isAdmin` in `res.locals` + res.locals.isAdmin = false; + + if (!req.user) { + return next(); + } + + res.locals.isAdmin = await user.isAdministrator(req.user.uid); + next(); + }; + + middleware.exposePrivileges = async (req, res, next) => { + // Exposes a hash of user's ranks (admin, gmod, etc.) + const hash = await utils.promiseParallel({ + isAdmin: user.isAdministrator(req.user.uid), + isGmod: user.isGlobalModerator(req.user.uid), + isPrivileged: user.isPrivileged(req.user.uid), + }); + + if (req.params.uid) { + hash.isSelf = parseInt(req.params.uid, 10) === req.user.uid; + } + + res.locals.privileges = hash; + next(); + }; + + middleware.exposePrivilegeSet = async (req, res, next) => { + // Exposes a user's global/admin privilege set + res.locals.privileges = { + ...await privileges.global.get(req.user.uid), + ...await privileges.admin.get(req.user.uid), + }; + next(); + }; +}; diff --git a/src/middleware/header.js b/src/middleware/header.js new file mode 100644 index 0000000000..54513ca18a --- /dev/null +++ b/src/middleware/header.js @@ -0,0 +1,264 @@ +'use strict'; + +const nconf = require('nconf'); +const jsesc = require('jsesc'); +const _ = require('lodash'); +const validator = require('validator'); +const util = require('util'); + +const user = require('../user'); +const topics = require('../topics'); +const messaging = require('../messaging'); +const flags = require('../flags'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const navigation = require('../navigation'); +const translator = require('../translator'); +const privileges = require('../privileges'); +const languages = require('../languages'); +const utils = require('../utils'); +const helpers = require('./helpers'); + +const controllers = { + api: require('../controllers/api'), + helpers: require('../controllers/helpers'), +}; + +const middleware = module.exports; + +const relative_path = nconf.get('relative_path'); + +middleware.buildHeader = helpers.try(async (req, res, next) => { + res.locals.renderHeader = true; + res.locals.isAPI = false; + if (req.method === 'GET') { + await require('./index').applyCSRFasync(req, res); + } + const [config, canLoginIfBanned] = await Promise.all([ + controllers.api.loadConfig(req), + user.bans.canLoginIfBanned(req.uid), + plugins.hooks.fire('filter:middleware.buildHeader', { req: req, locals: res.locals }), + ]); + + if (!canLoginIfBanned && req.loggedIn) { + req.logout(() => { + res.redirect('/'); + }); + return; + } + + res.locals.config = config; + next(); +}); + +middleware.buildHeaderAsync = util.promisify(middleware.buildHeader); + +middleware.renderHeader = async function renderHeader(req, res, data) { + const registrationType = meta.config.registrationType || 'normal'; + res.locals.config = res.locals.config || {}; + const templateValues = { + title: meta.config.title || '', + 'title:url': meta.config['title:url'] || '', + description: meta.config.description || '', + 'cache-buster': meta.config['cache-buster'] || '', + 'brand:logo': meta.config['brand:logo'] || '', + 'brand:logo:url': meta.config['brand:logo:url'] || '', + 'brand:logo:alt': meta.config['brand:logo:alt'] || '', + 'brand:logo:display': meta.config['brand:logo'] ? '' : 'hide', + allowRegistration: registrationType === 'normal', + searchEnabled: plugins.hooks.hasListeners('filter:search.query'), + postQueueEnabled: !!meta.config.postQueue, + config: res.locals.config, + relative_path, + bodyClass: data.bodyClass, + }; + + templateValues.configJSON = jsesc(JSON.stringify(res.locals.config), { isScriptContext: true }); + + const results = await utils.promiseParallel({ + isAdmin: user.isAdministrator(req.uid), + isGlobalMod: user.isGlobalModerator(req.uid), + isModerator: user.isModeratorOfAnyCategory(req.uid), + privileges: privileges.global.get(req.uid), + user: user.getUserData(req.uid), + isEmailConfirmSent: req.uid <= 0 ? false : await user.email.isValidationPending(req.uid), + languageDirection: translator.translate('[[language:dir]]', res.locals.config.userLang), + timeagoCode: languages.userTimeagoCode(res.locals.config.userLang), + browserTitle: translator.translate(controllers.helpers.buildTitle(translator.unescape(data.title))), + navigation: navigation.get(req.uid), + }); + + const unreadData = { + '': {}, + new: {}, + watched: {}, + unreplied: {}, + }; + + results.user.unreadData = unreadData; + results.user.isAdmin = results.isAdmin; + results.user.isGlobalMod = results.isGlobalMod; + results.user.isMod = !!results.isModerator; + results.user.privileges = results.privileges; + results.user.timeagoCode = results.timeagoCode; + results.user[results.user.status] = true; + + results.user.email = String(results.user.email); + results.user['email:confirmed'] = results.user['email:confirmed'] === 1; + results.user.isEmailConfirmSent = !!results.isEmailConfirmSent; + + templateValues.bootswatchSkin = (parseInt(meta.config.disableCustomUserSkins, 10) !== 1 ? res.locals.config.bootswatchSkin : '') || meta.config.bootswatchSkin || ''; + templateValues.browserTitle = results.browserTitle; + ({ + navigation: templateValues.navigation, + unreadCount: templateValues.unreadCount, + } = await appendUnreadCounts({ + uid: req.uid, + query: req.query, + navigation: results.navigation, + unreadData, + })); + templateValues.isAdmin = results.user.isAdmin; + templateValues.isGlobalMod = results.user.isGlobalMod; + templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod; + templateValues.canChat = results.privileges.chat && meta.config.disableChat !== 1; + templateValues.user = results.user; + templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true }); + templateValues.useCustomCSS = meta.config.useCustomCSS && meta.config.customCSS; + templateValues.customCSS = templateValues.useCustomCSS ? (meta.config.renderedCustomCSS || '') : ''; + templateValues.useCustomHTML = meta.config.useCustomHTML; + templateValues.customHTML = templateValues.useCustomHTML ? meta.config.customHTML : ''; + templateValues.maintenanceHeader = meta.config.maintenanceMode && !results.isAdmin; + templateValues.defaultLang = meta.config.defaultLang || 'en-GB'; + templateValues.userLang = res.locals.config.userLang; + templateValues.languageDirection = results.languageDirection; + if (req.query.noScriptMessage) { + templateValues.noScriptMessage = validator.escape(String(req.query.noScriptMessage)); + } + + templateValues.template = { name: res.locals.template }; + templateValues.template[res.locals.template] = true; + + if (data.hasOwnProperty('_header')) { + templateValues.metaTags = data._header.tags.meta; + templateValues.linkTags = data._header.tags.link; + } + + if (req.route && req.route.path === '/') { + modifyTitle(templateValues); + } + + const hookReturn = await plugins.hooks.fire('filter:middleware.renderHeader', { + req: req, + res: res, + templateValues: templateValues, + data: data, + }); + + return await req.app.renderAsync('header', hookReturn.templateValues); +}; + +async function appendUnreadCounts({ uid, navigation, unreadData, query }) { + const originalRoutes = navigation.map(nav => nav.originalRoute); + const calls = { + unreadData: topics.getUnreadData({ uid: uid, query: query }), + unreadChatCount: messaging.getUnreadCount(uid), + unreadNotificationCount: user.notifications.getUnreadCount(uid), + unreadFlagCount: (async function () { + if (originalRoutes.includes('/flags') && await user.isPrivileged(uid)) { + return flags.getCount({ + uid, + query, + filters: { + quick: 'unresolved', + cid: (await user.isAdminOrGlobalMod(uid)) ? [] : (await user.getModeratedCids(uid)), + }, + }); + } + return 0; + }()), + }; + const results = await utils.promiseParallel(calls); + + const unreadCounts = results.unreadData.counts; + const unreadCount = { + topic: unreadCounts[''] || 0, + newTopic: unreadCounts.new || 0, + watchedTopic: unreadCounts.watched || 0, + unrepliedTopic: unreadCounts.unreplied || 0, + mobileUnread: 0, + unreadUrl: '/unread', + chat: results.unreadChatCount || 0, + notification: results.unreadNotificationCount || 0, + flags: results.unreadFlagCount || 0, + }; + + Object.keys(unreadCount).forEach((key) => { + if (unreadCount[key] > 99) { + unreadCount[key] = '99+'; + } + }); + + const { tidsByFilter } = results.unreadData; + navigation = navigation.map((item) => { + function modifyNavItem(item, route, filter, content) { + if (item && item.originalRoute === route) { + unreadData[filter] = _.zipObject(tidsByFilter[filter], tidsByFilter[filter].map(() => true)); + item.content = content; + unreadCount.mobileUnread = content; + unreadCount.unreadUrl = route; + if (unreadCounts[filter] > 0) { + item.iconClass += ' unread-count'; + } + } + } + modifyNavItem(item, '/unread', '', unreadCount.topic); + modifyNavItem(item, '/unread?filter=new', 'new', unreadCount.newTopic); + modifyNavItem(item, '/unread?filter=watched', 'watched', unreadCount.watchedTopic); + modifyNavItem(item, '/unread?filter=unreplied', 'unreplied', unreadCount.unrepliedTopic); + + ['flags'].forEach((prop) => { + if (item && item.originalRoute === `/${prop}` && unreadCount[prop] > 0) { + item.iconClass += ' unread-count'; + item.content = unreadCount.flags; + } + }); + + return item; + }); + + return { navigation, unreadCount }; +} + +middleware.renderFooter = async function renderFooter(req, res, templateValues) { + const data = await plugins.hooks.fire('filter:middleware.renderFooter', { + req: req, + res: res, + templateValues: templateValues, + }); + + const scripts = await plugins.hooks.fire('filter:scripts.get', []); + + data.templateValues.scripts = scripts.map(script => ({ src: script })); + + data.templateValues.useCustomJS = meta.config.useCustomJS; + data.templateValues.customJS = data.templateValues.useCustomJS ? meta.config.customJS : ''; + data.templateValues.isSpider = req.uid === -1; + + return await req.app.renderAsync('footer', data.templateValues); +}; + +function modifyTitle(obj) { + const title = controllers.helpers.buildTitle(meta.config.homePageTitle || '[[pages:home]]'); + obj.browserTitle = title; + + if (obj.metaTags) { + obj.metaTags.forEach((tag, i) => { + if (tag.property === 'og:title') { + obj.metaTags[i].content = title; + } + }); + } + + return title; +} diff --git a/src/middleware/headers.js b/src/middleware/headers.js new file mode 100644 index 0000000000..014e8992c4 --- /dev/null +++ b/src/middleware/headers.js @@ -0,0 +1,116 @@ +'use strict'; + +const os = require('os'); +const winston = require('winston'); +const _ = require('lodash'); + +const meta = require('../meta'); +const languages = require('../languages'); +const helpers = require('./helpers'); +const plugins = require('../plugins'); + +module.exports = function (middleware) { + middleware.addHeaders = helpers.try((req, res, next) => { + const headers = { + 'X-Powered-By': encodeURI(meta.config['powered-by'] || 'NodeBB'), + 'Access-Control-Allow-Methods': encodeURI(meta.config['access-control-allow-methods'] || ''), + 'Access-Control-Allow-Headers': encodeURI(meta.config['access-control-allow-headers'] || ''), + }; + + if (meta.config['csp-frame-ancestors']) { + headers['Content-Security-Policy'] = `frame-ancestors ${meta.config['csp-frame-ancestors']}`; + if (meta.config['csp-frame-ancestors'] === '\'none\'') { + headers['X-Frame-Options'] = 'DENY'; + } + } else { + headers['Content-Security-Policy'] = 'frame-ancestors \'self\''; + headers['X-Frame-Options'] = 'SAMEORIGIN'; + } + + if (meta.config['access-control-allow-origin']) { + let origins = meta.config['access-control-allow-origin'].split(','); + origins = origins.map(origin => origin && origin.trim()); + + if (origins.includes(req.get('origin'))) { + headers['Access-Control-Allow-Origin'] = encodeURI(req.get('origin')); + headers.Vary = headers.Vary ? `${headers.Vary}, Origin` : 'Origin'; + } + } + + if (meta.config['access-control-allow-origin-regex']) { + let originsRegex = meta.config['access-control-allow-origin-regex'].split(','); + originsRegex = originsRegex.map((origin) => { + try { + origin = new RegExp(origin.trim()); + } catch (err) { + winston.error(`[middleware.addHeaders] Invalid RegExp For access-control-allow-origin ${origin}`); + origin = null; + } + return origin; + }); + + originsRegex.forEach((regex) => { + if (regex && regex.test(req.get('origin'))) { + headers['Access-Control-Allow-Origin'] = encodeURI(req.get('origin')); + headers.Vary = headers.Vary ? `${headers.Vary}, Origin` : 'Origin'; + } + }); + } + + if (meta.config['permissions-policy']) { + headers['Permissions-Policy'] = meta.config['permissions-policy']; + } + + if (meta.config['access-control-allow-credentials']) { + headers['Access-Control-Allow-Credentials'] = meta.config['access-control-allow-credentials']; + } + + if (process.env.NODE_ENV === 'development') { + headers['X-Upstream-Hostname'] = os.hostname(); + } + + for (const [key, value] of Object.entries(headers)) { + if (value) { + res.setHeader(key, value); + } + } + + next(); + }); + + middleware.autoLocale = helpers.try(async (req, res, next) => { + await plugins.hooks.fire('filter:middleware.autoLocale', { + req: req, + res: res, + }); + if (req.query.lang) { + const langs = await listCodes(); + if (!langs.includes(req.query.lang)) { + req.query.lang = meta.config.defaultLang; + } + return next(); + } + + if (meta.config.autoDetectLang && req.uid === 0) { + const langs = await listCodes(); + const lang = req.acceptsLanguages(langs); + if (!lang) { + return next(); + } + req.query.lang = lang; + } + + next(); + }); + + async function listCodes() { + const defaultLang = meta.config.defaultLang || 'en-GB'; + try { + const codes = await languages.listCodes(); + return _.uniq([defaultLang, ...codes]); + } catch (err) { + winston.error(`[middleware/autoLocale] Could not retrieve languages codes list! ${err.stack}`); + return [defaultLang]; + } + } +}; diff --git a/src/middleware/helpers.js b/src/middleware/helpers.js new file mode 100644 index 0000000000..0a78e443b6 --- /dev/null +++ b/src/middleware/helpers.js @@ -0,0 +1,68 @@ +'use strict'; + +const winston = require('winston'); +const validator = require('validator'); +const slugify = require('../slugify'); + +const meta = require('../meta'); + +const helpers = module.exports; + +helpers.try = function (middleware) { + if (middleware && middleware.constructor && middleware.constructor.name === 'AsyncFunction') { + return async function (req, res, next) { + try { + await middleware(req, res, next); + } catch (err) { + next(err); + } + }; + } + return function (req, res, next) { + try { + middleware(req, res, next); + } catch (err) { + next(err); + } + }; +}; + +helpers.buildBodyClass = function (req, res, templateData = {}) { + const clean = req.path.replace(/^\/api/, '').replace(/^\/|\/$/g, ''); + const parts = clean.split('/').slice(0, 3); + parts.forEach((p, index) => { + try { + p = slugify(decodeURIComponent(p)); + } catch (err) { + winston.error(`Error decoding URI: ${p}`); + winston.error(err.stack); + p = ''; + } + p = validator.escape(String(p)); + parts[index] = index ? `${parts[0]}-${p}` : `page-${p || 'home'}`; + }); + + if (templateData.template && templateData.template.topic) { + parts.push(`page-topic-category-${templateData.category.cid}`); + parts.push(`page-topic-category-${slugify(templateData.category.name)}`); + } + + if (Array.isArray(templateData.breadcrumbs)) { + templateData.breadcrumbs.forEach((crumb) => { + if (crumb && crumb.hasOwnProperty('cid')) { + parts.push(`parent-category-${crumb.cid}`); + } + }); + } + + parts.push(`page-status-${res.statusCode}`); + + parts.push(`theme-${meta.config['theme:id'].split('-')[2]}`); + + if (req.loggedIn) { + parts.push('user-loggedin'); + } else { + parts.push('user-guest'); + } + return parts.join(' '); +}; diff --git a/src/middleware/index.js b/src/middleware/index.js new file mode 100644 index 0000000000..3a51c8e20c --- /dev/null +++ b/src/middleware/index.js @@ -0,0 +1,254 @@ +'use strict'; + +const async = require('async'); +const path = require('path'); +const csrf = require('csurf'); +const validator = require('validator'); +const nconf = require('nconf'); +const toobusy = require('toobusy-js'); +const util = require('util'); + +const plugins = require('../plugins'); +const meta = require('../meta'); +const user = require('../user'); +const groups = require('../groups'); +const analytics = require('../analytics'); +const privileges = require('../privileges'); +const cacheCreate = require('../cache/lru'); +const helpers = require('./helpers'); + +const controllers = { + api: require('../controllers/api'), + helpers: require('../controllers/helpers'), +}; + +const delayCache = cacheCreate({ + ttl: 1000 * 60, +}); + +const middleware = module.exports; + +const relative_path = nconf.get('relative_path'); + +middleware.regexes = { + timestampedUpload: /^\d+-.+$/, +}; + +const csrfMiddleware = csrf(); + +middleware.applyCSRF = function (req, res, next) { + if (req.uid >= 0) { + csrfMiddleware(req, res, next); + } else { + next(); + } +}; +middleware.applyCSRFasync = util.promisify(middleware.applyCSRF); + +middleware.ensureLoggedIn = (req, res, next) => { + if (!req.loggedIn) { + return controllers.helpers.notAllowed(req, res); + } + + setImmediate(next); +}; + +Object.assign(middleware, { + admin: require('./admin'), + ...require('./header'), +}); +require('./render')(middleware); +require('./maintenance')(middleware); +require('./user')(middleware); +middleware.uploads = require('./uploads'); +require('./headers')(middleware); +require('./expose')(middleware); +middleware.assert = require('./assert'); + +middleware.stripLeadingSlashes = function stripLeadingSlashes(req, res, next) { + const target = req.originalUrl.replace(relative_path, ''); + if (target.startsWith('//')) { + return res.redirect(relative_path + target.replace(/^\/+/, '/')); + } + next(); +}; + +middleware.pageView = helpers.try(async (req, res, next) => { + if (req.loggedIn) { + await Promise.all([ + user.updateOnlineUsers(req.uid), + user.updateLastOnlineTime(req.uid), + ]); + } + next(); + await analytics.pageView({ ip: req.ip, uid: req.uid }); + plugins.hooks.fire('action:middleware.pageView', { req: req }); +}); + +middleware.pluginHooks = helpers.try(async (req, res, next) => { + // TODO: Deprecate in v2.0 + await async.each(plugins.loadedHooks['filter:router.page'] || [], (hookObj, next) => { + hookObj.method(req, res, next); + }); + + await plugins.hooks.fire('response:router.page', { + req: req, + res: res, + }); + + if (!res.headersSent) { + next(); + } +}); + +middleware.validateFiles = function validateFiles(req, res, next) { + if (!Array.isArray(req.files.files) || !req.files.files.length) { + return next(new Error(['[[error:invalid-files]]'])); + } + + next(); +}; + +middleware.prepareAPI = function prepareAPI(req, res, next) { + res.locals.isAPI = true; + next(); +}; + +middleware.routeTouchIcon = function routeTouchIcon(req, res) { + if (meta.config['brand:touchIcon'] && validator.isURL(meta.config['brand:touchIcon'])) { + return res.redirect(meta.config['brand:touchIcon']); + } + let iconPath = ''; + if (meta.config['brand:touchIcon']) { + iconPath = path.join(nconf.get('upload_path'), meta.config['brand:touchIcon'].replace(/assets\/uploads/, '')); + } else { + iconPath = path.join(nconf.get('base_dir'), 'public/images/touch/512.png'); + } + + return res.sendFile(iconPath, { + maxAge: req.app.enabled('cache') ? 5184000000 : 0, + }); +}; + +middleware.privateTagListing = helpers.try(async (req, res, next) => { + const canView = await privileges.global.can('view:tags', req.uid); + if (!canView) { + return controllers.helpers.notAllowed(req, res); + } + next(); +}); + +middleware.exposeGroupName = helpers.try(async (req, res, next) => { + await expose('groupName', groups.getGroupNameByGroupSlug, 'slug', req, res, next); +}); + +middleware.exposeUid = helpers.try(async (req, res, next) => { + await expose('uid', user.getUidByUserslug, 'userslug', req, res, next); +}); + +async function expose(exposedField, method, field, req, res, next) { + if (!req.params.hasOwnProperty(field)) { + return next(); + } + res.locals[exposedField] = await method(req.params[field]); + next(); +} + +middleware.privateUploads = function privateUploads(req, res, next) { + if (req.loggedIn || !meta.config.privateUploads) { + return next(); + } + + if (req.path.startsWith(`${nconf.get('relative_path')}/assets/uploads/files`)) { + const extensions = (meta.config.privateUploadsExtensions || '').split(',').filter(Boolean); + let ext = path.extname(req.path); + ext = ext ? ext.replace(/^\./, '') : ext; + if (!extensions.length || extensions.includes(ext)) { + return res.status(403).json('not-allowed'); + } + } + next(); +}; + +middleware.busyCheck = function busyCheck(req, res, next) { + if (global.env === 'production' && meta.config.eventLoopCheckEnabled && toobusy()) { + analytics.increment('errors:503'); + res.status(503).type('text/html').sendFile(path.join(__dirname, '../../public/503.html')); + } else { + setImmediate(next); + } +}; + +middleware.applyBlacklist = async function applyBlacklist(req, res, next) { + try { + await meta.blacklist.test(req.ip); + next(); + } catch (err) { + next(err); + } +}; + +middleware.delayLoading = function delayLoading(req, res, next) { + // Introduces an artificial delay during load so that brute force attacks are effectively mitigated + + // Add IP to cache so if too many requests are made, subsequent requests are blocked for a minute + let timesSeen = delayCache.get(req.ip) || 0; + if (timesSeen > 10) { + return res.sendStatus(429); + } + delayCache.set(req.ip, timesSeen += 1); + + setTimeout(next, 1000); +}; + +middleware.buildSkinAsset = helpers.try(async (req, res, next) => { + // If this middleware is reached, a skin was requested, so it is built on-demand + const target = path.basename(req.originalUrl).match(/(client-[a-z]+)/); + if (!target) { + return next(); + } + + await plugins.prepareForBuild(['client side styles']); + const css = await meta.css.buildBundle(target[0], true); + require('../meta/minifier').killAll(); + res.status(200).type('text/css').send(css); +}); + +middleware.addUploadHeaders = function addUploadHeaders(req, res, next) { + // Trim uploaded files' timestamps when downloading + force download if html + let basename = path.basename(req.path); + const extname = path.extname(req.path); + if (req.path.startsWith('/uploads/files/') && middleware.regexes.timestampedUpload.test(basename)) { + basename = basename.slice(14); + res.header('Content-Disposition', `${extname.startsWith('.htm') ? 'attachment' : 'inline'}; filename="${basename}"`); + } + + next(); +}; + +middleware.validateAuth = helpers.try(async (req, res, next) => { + try { + await plugins.hooks.fire('static:auth.validate', { + user: res.locals.user, + strategy: res.locals.strategy, + }); + next(); + } catch (err) { + const regenerateSession = util.promisify(cb => req.session.regenerate(cb)); + await regenerateSession(); + req.uid = 0; + req.loggedIn = false; + next(err); + } +}); + +middleware.checkRequired = function (fields, req, res, next) { + // Used in API calls to ensure that necessary parameters/data values are present + const missing = fields.filter(field => !req.body.hasOwnProperty(field)); + + if (!missing.length) { + return next(); + } + + controllers.helpers.formatApiResponse(400, res, new Error(`[[error:required-parameters-missing, ${missing.join(' ')}]]`)); +}; diff --git a/src/middleware/maintenance.js b/src/middleware/maintenance.js new file mode 100644 index 0000000000..7e8b1c02d4 --- /dev/null +++ b/src/middleware/maintenance.js @@ -0,0 +1,46 @@ +'use strict'; + +const util = require('util'); +const nconf = require('nconf'); +const meta = require('../meta'); +const user = require('../user'); +const groups = require('../groups'); +const helpers = require('./helpers'); + +module.exports = function (middleware) { + middleware.maintenanceMode = helpers.try(async (req, res, next) => { + if (!meta.config.maintenanceMode) { + return next(); + } + + const hooksAsync = util.promisify(middleware.pluginHooks); + await hooksAsync(req, res); + + const url = req.url.replace(nconf.get('relative_path'), ''); + if (url.startsWith('/login') || url.startsWith('/api/login')) { + return next(); + } + + const [isAdmin, isMemberOfExempt] = await Promise.all([ + user.isAdministrator(req.uid), + groups.isMemberOfAny(req.uid, meta.config.groupsExemptFromMaintenanceMode), + ]); + + if (isAdmin || isMemberOfExempt) { + return next(); + } + + res.status(meta.config.maintenanceModeStatus); + + const data = { + site_title: meta.config.title || 'NodeBB', + message: meta.config.maintenanceModeMessage, + }; + + if (res.locals.isAPI) { + return res.json(data); + } + await middleware.buildHeaderAsync(req, res); + res.render('503', data); + }); +}; diff --git a/src/middleware/ratelimit.js b/src/middleware/ratelimit.js new file mode 100644 index 0000000000..6c5ad57681 --- /dev/null +++ b/src/middleware/ratelimit.js @@ -0,0 +1,32 @@ +'use strict'; + +const winston = require('winston'); + +const ratelimit = module.exports; + +const allowedCalls = 100; +const timeframe = 10000; + +ratelimit.isFlooding = function (socket) { + socket.callsPerSecond = socket.callsPerSecond || 0; + socket.elapsedTime = socket.elapsedTime || 0; + socket.lastCallTime = socket.lastCallTime || Date.now(); + + socket.callsPerSecond += 1; + + const now = Date.now(); + socket.elapsedTime += now - socket.lastCallTime; + + if (socket.callsPerSecond > allowedCalls && socket.elapsedTime < timeframe) { + winston.warn(`Flooding detected! Calls : ${socket.callsPerSecond}, Duration : ${socket.elapsedTime}`); + return true; + } + + if (socket.elapsedTime >= timeframe) { + socket.elapsedTime = 0; + socket.callsPerSecond = 0; + } + + socket.lastCallTime = now; + return false; +}; diff --git a/src/middleware/render.js b/src/middleware/render.js new file mode 100644 index 0000000000..b68eff2344 --- /dev/null +++ b/src/middleware/render.js @@ -0,0 +1,137 @@ +'use strict'; + +const nconf = require('nconf'); +const validator = require('validator'); + +const plugins = require('../plugins'); +const meta = require('../meta'); +const translator = require('../translator'); +const widgets = require('../widgets'); +const utils = require('../utils'); +const helpers = require('./helpers'); + +const relative_path = nconf.get('relative_path'); + +module.exports = function (middleware) { + middleware.processRender = function processRender(req, res, next) { + // res.render post-processing, modified from here: https://gist.github.com/mrlannigan/5051687 + const { render } = res; + + res.render = async function renderOverride(template, options, fn) { + const self = this; + const { req } = this; + async function renderMethod(template, options, fn) { + options = options || {}; + if (typeof options === 'function') { + fn = options; + options = {}; + } + + options.loggedIn = req.uid > 0; + options.relative_path = relative_path; + options.template = { name: template, [template]: true }; + options.url = (req.baseUrl + req.path.replace(/^\/api/, '')); + options.bodyClass = helpers.buildBodyClass(req, res, options); + + if (req.loggedIn) { + res.set('cache-control', 'private'); + } + + const buildResult = await plugins.hooks.fire(`filter:${template}.build`, { req: req, res: res, templateData: options }); + if (res.headersSent) { + return; + } + const templateToRender = buildResult.templateData.templateToRender || template; + + const renderResult = await plugins.hooks.fire('filter:middleware.render', { req: req, res: res, templateData: buildResult.templateData }); + if (res.headersSent) { + return; + } + options = renderResult.templateData; + options._header = { + tags: await meta.tags.parse(req, renderResult, res.locals.metaTags, res.locals.linkTags), + }; + options.widgets = await widgets.render(req.uid, { + template: `${template}.tpl`, + url: options.url, + templateData: options, + req: req, + res: res, + }); + res.locals.template = template; + options._locals = undefined; + + if (res.locals.isAPI) { + if (req.route && req.route.path === '/api/') { + options.title = '[[pages:home]]'; + } + req.app.set('json spaces', global.env === 'development' || req.query.pretty ? 4 : 0); + return res.json(options); + } + const optionsString = JSON.stringify(options).replace(/<\//g, '<\\/'); + const results = await utils.promiseParallel({ + header: renderHeaderFooter('renderHeader', req, res, options), + content: renderContent(render, templateToRender, req, res, options), + footer: renderHeaderFooter('renderFooter', req, res, options), + }); + + const str = `${results.header + + (res.locals.postHeader || '') + + results.content + }${ + res.locals.preFooter || '' + }${results.footer}`; + + if (typeof fn !== 'function') { + self.send(str); + } else { + fn(null, str); + } + } + + try { + await renderMethod(template, options, fn); + } catch (err) { + next(err); + } + }; + + next(); + }; + + async function renderContent(render, tpl, req, res, options) { + return new Promise((resolve, reject) => { + render.call(res, tpl, options, async (err, str) => { + if (err) reject(err); + else resolve(await translate(str, getLang(req, res))); + }); + }); + } + + async function renderHeaderFooter(method, req, res, options) { + let str = ''; + if (res.locals.renderHeader) { + str = await middleware[method](req, res, options); + } else if (res.locals.renderAdminHeader) { + str = await middleware.admin[method](req, res, options); + } else { + str = ''; + } + return await translate(str, getLang(req, res)); + } + + function getLang(req, res) { + let language = (res.locals.config && res.locals.config.userLang) || 'en-GB'; + if (res.locals.renderAdminHeader) { + language = (res.locals.config && res.locals.config.acpLang) || 'en-GB'; + } + return req.query.lang ? validator.escape(String(req.query.lang)) : language; + } + + async function translate(str, language) { + const translated = await translator.translate(str, language); + return translator.unescape(translated); + } +}; diff --git a/src/middleware/uploads.js b/src/middleware/uploads.js new file mode 100644 index 0000000000..8b2082acb6 --- /dev/null +++ b/src/middleware/uploads.js @@ -0,0 +1,29 @@ +'use strict'; + +const cacheCreate = require('../cache/ttl'); +const meta = require('../meta'); +const helpers = require('./helpers'); +const user = require('../user'); + +const cache = cacheCreate({ + ttl: meta.config.uploadRateLimitCooldown * 1000, +}); + +exports.clearCache = function () { + cache.clear(); +}; + +exports.ratelimit = helpers.try(async (req, res, next) => { + const { uid } = req; + if (!meta.config.uploadRateLimitThreshold || (uid && await user.isAdminOrGlobalMod(uid))) { + return next(); + } + + const count = (cache.get(`${req.ip}:uploaded_file_count`) || 0) + req.files.files.length; + if (count > meta.config.uploadRateLimitThreshold) { + return next(new Error(['[[error:upload-ratelimit-reached]]'])); + } + cache.set(`${req.ip}:uploaded_file_count`, count); + next(); +}); + diff --git a/src/middleware/user.js b/src/middleware/user.js new file mode 100644 index 0000000000..3c2733a318 --- /dev/null +++ b/src/middleware/user.js @@ -0,0 +1,245 @@ +'use strict'; + +const winston = require('winston'); +const passport = require('passport'); +const nconf = require('nconf'); +const path = require('path'); +const util = require('util'); + +const user = require('../user'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const helpers = require('./helpers'); +const auth = require('../routes/authentication'); +const writeRouter = require('../routes/write'); + +const controllers = { + helpers: require('../controllers/helpers'), + authentication: require('../controllers/authentication'), +}; + +const passportAuthenticateAsync = function (req, res) { + return new Promise((resolve, reject) => { + passport.authenticate('core.api', (err, user) => { + if (err) { + reject(err); + } else { + resolve(user); + res.on('finish', writeRouter.cleanup.bind(null, req)); + } + })(req, res); + }); +}; + +module.exports = function (middleware) { + async function authenticate(req, res) { + async function finishLogin(req, user) { + const loginAsync = util.promisify(req.login).bind(req); + await loginAsync(user, { keepSessionInfo: true }); + await controllers.authentication.onSuccessfulLogin(req, user.uid); + req.uid = user.uid; + req.loggedIn = req.uid > 0; + return true; + } + + if (res.locals.isAPI && (req.loggedIn || !req.headers.hasOwnProperty('authorization'))) { + // If authenticated via cookie (express-session), protect routes with CSRF checking + await middleware.applyCSRFasync(req, res); + } + + if (req.loggedIn) { + return true; + } else if (req.headers.hasOwnProperty('authorization')) { + const user = await passportAuthenticateAsync(req, res); + if (!user) { return true; } + + if (user.hasOwnProperty('uid')) { + return await finishLogin(req, user); + } else if (user.hasOwnProperty('master') && user.master === true) { + // If the token received was a master token, a _uid must also be present for all calls + if (req.body.hasOwnProperty('_uid') || req.query.hasOwnProperty('_uid')) { + user.uid = req.body._uid || req.query._uid; + delete user.master; + return await finishLogin(req, user); + } + + throw new Error('[[error:api.master-token-no-uid]]'); + } else { + winston.warn('[api/authenticate] Unable to find user after verifying token'); + return true; + } + } + + await plugins.hooks.fire('response:middleware.authenticate', { + req: req, + res: res, + next: function () {}, // no-op for backwards compatibility + }); + + if (!res.headersSent) { + auth.setAuthVars(req); + } + return !res.headersSent; + } + + middleware.authenticateRequest = helpers.try(async (req, res, next) => { + const { skip } = await plugins.hooks.fire('filter:middleware.authenticate', { + skip: { + // get: [], + post: ['/api/v3/utilities/login'], + // etc... + }, + }); + + const mountedPath = path.join(req.baseUrl, req.path).replace(nconf.get('relative_path'), ''); + const method = req.method.toLowerCase(); + if (skip[method] && skip[method].includes(mountedPath)) { + return next(); + } + + if (!await authenticate(req, res)) { + return; + } + next(); + }); + + middleware.ensureSelfOrGlobalPrivilege = helpers.try(async (req, res, next) => { + await ensureSelfOrMethod(user.isAdminOrGlobalMod, req, res, next); + }); + + middleware.ensureSelfOrPrivileged = helpers.try(async (req, res, next) => { + await ensureSelfOrMethod(user.isPrivileged, req, res, next); + }); + + async function ensureSelfOrMethod(method, req, res, next) { + /* + The "self" part of this middleware hinges on you having used + middleware.exposeUid prior to invoking this middleware. + */ + if (!req.loggedIn) { + return controllers.helpers.notAllowed(req, res); + } + if (req.uid === parseInt(res.locals.uid, 10)) { + return next(); + } + const allowed = await method(req.uid); + if (!allowed) { + return controllers.helpers.notAllowed(req, res); + } + + return next(); + } + + middleware.canViewUsers = helpers.try(async (req, res, next) => { + if (parseInt(res.locals.uid, 10) === req.uid) { + return next(); + } + const canView = await privileges.global.can('view:users', req.uid); + if (canView) { + return next(); + } + controllers.helpers.notAllowed(req, res); + }); + + middleware.canViewGroups = helpers.try(async (req, res, next) => { + const canView = await privileges.global.can('view:groups', req.uid); + if (canView) { + return next(); + } + controllers.helpers.notAllowed(req, res); + }); + + middleware.canChat = helpers.try(async (req, res, next) => { + const canChat = await privileges.global.can('chat', req.uid); + if (canChat) { + return next(); + } + controllers.helpers.notAllowed(req, res); + }); + + middleware.checkAccountPermissions = helpers.try(async (req, res, next) => { + // This middleware ensures that only the requested user and admins can pass + + // This check if left behind for legacy purposes. Older plugins may call this middleware without ensureLoggedIn + if (!req.loggedIn) { + return controllers.helpers.notAllowed(req, res); + } + + if (!['uid', 'userslug'].some(param => req.params.hasOwnProperty(param))) { + return controllers.helpers.notAllowed(req, res); + } + + const uid = req.params.uid || await user.getUidByUserslug(req.params.userslug); + let allowed = await privileges.users.canEdit(req.uid, uid); + if (allowed) { + return next(); + } + + if (/user\/.+\/info$/.test(req.path)) { + allowed = await privileges.global.can('view:users:info', req.uid); + } + if (allowed) { + return next(); + } + controllers.helpers.notAllowed(req, res); + }); + + middleware.redirectToAccountIfLoggedIn = helpers.try(async (req, res, next) => { + if (req.session.forceLogin || req.uid <= 0) { + return next(); + } + const userslug = await user.getUserField(req.uid, 'userslug'); + controllers.helpers.redirect(res, `/user/${userslug}`); + }); + + middleware.redirectUidToUserslug = helpers.try(async (req, res, next) => { + const uid = parseInt(req.params.uid, 10); + if (uid <= 0) { + return next(); + } + const userslug = await user.getUserField(uid, 'userslug'); + if (!userslug) { + return next(); + } + const path = req.url.replace(/^\/api/, '') + .replace(`/uid/${uid}`, () => `/user/${userslug}`); + controllers.helpers.redirect(res, path); + }); + + middleware.redirectMeToUserslug = helpers.try(async (req, res) => { + const userslug = await user.getUserField(req.uid, 'userslug'); + if (!userslug) { + return controllers.helpers.notAllowed(req, res); + } + const path = req.url.replace(/^(\/api)?\/me/, () => `/user/${userslug}`); + controllers.helpers.redirect(res, path); + }); + + middleware.requireUser = function (req, res, next) { + if (req.loggedIn) { + return next(); + } + + res.status(403).render('403', { title: '[[global:403.title]]' }); + }; + + middleware.registrationComplete = async function registrationComplete(req, res, next) { + // If the user's session contains registration data, redirect the user to complete registration + if (!req.session.hasOwnProperty('registration')) { + return setImmediate(next); + } + + const path = req.path.startsWith('/api/') ? req.path.replace('/api', '') : req.path; + const { allowed } = await plugins.hooks.fire('filter:middleware.registrationComplete', { + allowed: ['/register/complete'], + }); + if (!allowed.includes(path)) { + // Append user data if present + req.session.registration.uid = req.session.registration.uid || req.uid; + + controllers.helpers.redirect(res, '/register/complete'); + } else { + setImmediate(next); + } + }; +}; diff --git a/src/navigation/admin.js b/src/navigation/admin.js new file mode 100644 index 0000000000..f72d850b56 --- /dev/null +++ b/src/navigation/admin.js @@ -0,0 +1,104 @@ +'use strict'; + +const validator = require('validator'); +const winston = require('winston'); + +const plugins = require('../plugins'); +const db = require('../database'); +const pubsub = require('../pubsub'); + +const admin = module.exports; +let cache = null; + +pubsub.on('admin:navigation:save', () => { + cache = null; +}); + +admin.save = async function (data) { + const order = Object.keys(data); + const bulkSet = []; + data.forEach((item, index) => { + item.order = order[index]; + if (item.hasOwnProperty('groups')) { + item.groups = JSON.stringify(item.groups); + } + bulkSet.push([`navigation:enabled:${item.order}`, item]); + }); + + cache = null; + pubsub.publish('admin:navigation:save'); + const ids = await db.getSortedSetRange('navigation:enabled', 0, -1); + await db.deleteAll(ids.map(id => `navigation:enabled:${id}`)); + await db.setObjectBulk(bulkSet); + await db.delete('navigation:enabled'); + await db.sortedSetAdd('navigation:enabled', order, order); +}; + +admin.getAdmin = async function () { + const [enabled, available] = await Promise.all([ + admin.get(), + getAvailable(), + ]); + return { enabled: enabled, available: available }; +}; + +const fieldsToEscape = ['iconClass', 'class', 'route', 'id', 'text', 'textClass', 'title']; + +admin.escapeFields = navItems => toggleEscape(navItems, true); +admin.unescapeFields = navItems => toggleEscape(navItems, false); + +function toggleEscape(navItems, flag) { + navItems.forEach((item) => { + if (item) { + fieldsToEscape.forEach((field) => { + if (item.hasOwnProperty(field)) { + item[field] = validator[flag ? 'escape' : 'unescape'](String(item[field])); + } + }); + } + }); +} + +admin.get = async function () { + if (cache) { + return cache.map(item => ({ ...item })); + } + const ids = await db.getSortedSetRange('navigation:enabled', 0, -1); + const data = await db.getObjects(ids.map(id => `navigation:enabled:${id}`)); + cache = data.map((item) => { + if (item.hasOwnProperty('groups')) { + try { + item.groups = JSON.parse(item.groups); + } catch (err) { + winston.error(err.stack); + item.groups = []; + } + } + item.groups = item.groups || []; + if (item.groups && !Array.isArray(item.groups)) { + item.groups = [item.groups]; + } + return item; + }); + admin.escapeFields(cache); + + return cache.map(item => ({ ...item })); +}; + +async function getAvailable() { + const core = require('../../install/data/navigation.json').map((item) => { + item.core = true; + item.id = item.id || ''; + return item; + }); + + const navItems = await plugins.hooks.fire('filter:navigation.available', core); + navItems.forEach((item) => { + if (item && !item.hasOwnProperty('enabled')) { + item.enabled = true; + } + }); + return navItems; +} + +require('../promisify')(admin); diff --git a/src/navigation/index.js b/src/navigation/index.js new file mode 100644 index 0000000000..43fda13695 --- /dev/null +++ b/src/navigation/index.js @@ -0,0 +1,34 @@ +'use strict'; + +const nconf = require('nconf'); +const validator = require('validator'); +const admin = require('./admin'); +const groups = require('../groups'); + +const navigation = module.exports; + +const relative_path = nconf.get('relative_path'); + +navigation.get = async function (uid) { + let data = await admin.get(); + + data = data.filter(item => item && item.enabled).map((item) => { + item.originalRoute = validator.unescape(item.route); + + if (!item.route.startsWith('http')) { + item.route = relative_path + item.route; + } + + return item; + }); + + const pass = await Promise.all(data.map(async (navItem) => { + if (!navItem.groups.length) { + return true; + } + return await groups.isMemberOfAny(uid, navItem.groups); + })); + return data.filter((navItem, i) => pass[i]); +}; + +require('../promisify')(navigation); diff --git a/src/notifications.js b/src/notifications.js new file mode 100644 index 0000000000..2b2d4c9691 --- /dev/null +++ b/src/notifications.js @@ -0,0 +1,447 @@ +'use strict'; + +const async = require('async'); +const winston = require('winston'); +const cron = require('cron').CronJob; +const nconf = require('nconf'); +const _ = require('lodash'); + +const db = require('./database'); +const User = require('./user'); +const posts = require('./posts'); +const groups = require('./groups'); +const meta = require('./meta'); +const batch = require('./batch'); +const plugins = require('./plugins'); +const utils = require('./utils'); +const emailer = require('./emailer'); + +const Notifications = module.exports; + +Notifications.baseTypes = [ + 'notificationType_upvote', + 'notificationType_new-topic', + 'notificationType_new-reply', + 'notificationType_post-edit', + 'notificationType_follow', + 'notificationType_new-chat', + 'notificationType_new-group-chat', + 'notificationType_group-invite', + 'notificationType_group-leave', + 'notificationType_group-request-membership', +]; + +Notifications.privilegedTypes = [ + 'notificationType_new-register', + 'notificationType_post-queue', + 'notificationType_new-post-flag', + 'notificationType_new-user-flag', +]; + +const notificationPruneCutoff = 2592000000; // one month + +Notifications.getAllNotificationTypes = async function () { + const results = await plugins.hooks.fire('filter:user.notificationTypes', { + types: Notifications.baseTypes.slice(), + privilegedTypes: Notifications.privilegedTypes.slice(), + }); + return results.types.concat(results.privilegedTypes); +}; + +Notifications.startJobs = function () { + winston.verbose('[notifications.init] Registering jobs.'); + new cron('*/30 * * * *', Notifications.prune, null, true); +}; + +Notifications.get = async function (nid) { + const notifications = await Notifications.getMultiple([nid]); + return Array.isArray(notifications) && notifications.length ? notifications[0] : null; +}; + +Notifications.getMultiple = async function (nids) { + if (!Array.isArray(nids) || !nids.length) { + return []; + } + + const keys = nids.map(nid => `notifications:${nid}`); + const notifications = await db.getObjects(keys); + + const userKeys = notifications.map(n => n && n.from); + const usersData = await User.getUsersFields(userKeys, ['username', 'userslug', 'picture']); + + notifications.forEach((notification, index) => { + if (notification) { + if (notification.path && !notification.path.startsWith('http')) { + notification.path = nconf.get('relative_path') + notification.path; + } + notification.datetimeISO = utils.toISOString(notification.datetime); + + if (notification.bodyLong) { + notification.bodyLong = utils.stripHTMLTags(notification.bodyLong, ['img', 'p', 'a']); + } + + notification.user = usersData[index]; + if (notification.user) { + notification.image = notification.user.picture || null; + if (notification.user.username === '[[global:guest]]') { + notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2'); + } + } else if (notification.image === 'brand:logo' || !notification.image) { + notification.image = meta.config['brand:logo'] || `${nconf.get('relative_path')}/logo.png`; + } + } + }); + return notifications; +}; + +Notifications.filterExists = async function (nids) { + const exists = await db.isSortedSetMembers('notifications', nids); + return nids.filter((nid, idx) => exists[idx]); +}; + +Notifications.findRelated = async function (mergeIds, set) { + mergeIds = mergeIds.filter(Boolean); + if (!mergeIds.length) { + return []; + } + // A related notification is one in a zset that has the same mergeId + const nids = await db.getSortedSetRevRange(set, 0, -1); + + const keys = nids.map(nid => `notifications:${nid}`); + const notificationData = await db.getObjectsFields(keys, ['mergeId']); + const notificationMergeIds = notificationData.map(notifObj => String(notifObj.mergeId)); + const mergeSet = new Set(mergeIds.map(id => String(id))); + return nids.filter((nid, idx) => mergeSet.has(notificationMergeIds[idx])); +}; + +Notifications.create = async function (data) { + if (!data.nid) { + throw new Error('[[error:no-notification-id]]'); + } + data.importance = data.importance || 5; + const oldNotif = await db.getObject(`notifications:${data.nid}`); + if ( + oldNotif && + parseInt(oldNotif.pid, 10) === parseInt(data.pid, 10) && + parseInt(oldNotif.importance, 10) > parseInt(data.importance, 10) + ) { + return null; + } + const now = Date.now(); + data.datetime = now; + const result = await plugins.hooks.fire('filter:notifications.create', { + data: data, + }); + if (!result.data) { + return null; + } + await Promise.all([ + db.sortedSetAdd('notifications', now, data.nid), + db.setObject(`notifications:${data.nid}`, data), + ]); + return data; +}; + +Notifications.push = async function (notification, uids) { + if (!notification || !notification.nid) { + return; + } + uids = Array.isArray(uids) ? _.uniq(uids) : [uids]; + if (!uids.length) { + return; + } + + setTimeout(() => { + batch.processArray(uids, async (uids) => { + await pushToUids(uids, notification); + }, { interval: 1000, batch: 500 }, (err) => { + if (err) { + winston.error(err.stack); + } + }); + }, 1000); +}; + +async function pushToUids(uids, notification) { + async function sendNotification(uids) { + if (!uids.length) { + return; + } + const cutoff = Date.now() - notificationPruneCutoff; + const unreadKeys = uids.map(uid => `uid:${uid}:notifications:unread`); + const readKeys = uids.map(uid => `uid:${uid}:notifications:read`); + await Promise.all([ + db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid), + db.sortedSetsRemove(readKeys, notification.nid), + ]); + await db.sortedSetsRemoveRangeByScore(unreadKeys.concat(readKeys), '-inf', cutoff); + const websockets = require('./socket.io'); + if (websockets.server) { + uids.forEach((uid) => { + websockets.in(`uid_${uid}`).emit('event:new_notification', notification); + }); + } + } + + async function sendEmail(uids) { + // Update CTA messaging (as not all notification types need custom text) + if (['new-reply', 'new-chat'].includes(notification.type)) { + notification['cta-type'] = notification.type; + } + let body = notification.bodyLong || ''; + if (meta.config.removeEmailNotificationImages) { + body = body.replace(/]*>/, ''); + } + body = posts.relativeToAbsolute(body, posts.urlRegex); + body = posts.relativeToAbsolute(body, posts.imgRegex); + let errorLogged = false; + await async.eachLimit(uids, 3, async (uid) => { + await emailer.send('notification', uid, { + path: notification.path, + notification_url: notification.path.startsWith('http') ? notification.path : nconf.get('url') + notification.path, + subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'), + intro: utils.stripHTMLTags(notification.bodyShort), + body: body, + notification: notification, + showUnsubscribe: true, + }).catch((err) => { + if (!errorLogged) { + winston.error(`[emailer.send] ${err.stack}`); + errorLogged = true; + } + }); + }); + } + + async function getUidsBySettings(uids) { + const uidsToNotify = []; + const uidsToEmail = []; + const usersSettings = await User.getMultipleUserSettings(uids); + usersSettings.forEach((userSettings) => { + const setting = userSettings[`notificationType_${notification.type}`] || 'notification'; + + if (setting === 'notification' || setting === 'notificationemail') { + uidsToNotify.push(userSettings.uid); + } + + if (setting === 'email' || setting === 'notificationemail') { + uidsToEmail.push(userSettings.uid); + } + }); + return { uidsToNotify: uidsToNotify, uidsToEmail: uidsToEmail }; + } + + // Remove uid from recipients list if they have blocked the user triggering the notification + uids = await User.blocks.filterUids(notification.from, uids); + const data = await plugins.hooks.fire('filter:notification.push', { notification: notification, uids: uids }); + if (!data || !data.notification || !data.uids || !data.uids.length) { + return; + } + + notification = data.notification; + let results = { uidsToNotify: data.uids, uidsToEmail: [] }; + if (notification.type) { + results = await getUidsBySettings(data.uids); + } + await Promise.all([ + sendNotification(results.uidsToNotify), + sendEmail(results.uidsToEmail), + ]); + plugins.hooks.fire('action:notification.pushed', { + notification: notification, + uids: results.uidsToNotify, + uidsNotified: results.uidsToNotify, + uidsEmailed: results.uidsToEmail, + }); +} + +Notifications.pushGroup = async function (notification, groupName) { + if (!notification) { + return; + } + const members = await groups.getMembers(groupName, 0, -1); + await Notifications.push(notification, members); +}; + +Notifications.pushGroups = async function (notification, groupNames) { + if (!notification) { + return; + } + let groupMembers = await groups.getMembersOfGroups(groupNames); + groupMembers = _.uniq(_.flatten(groupMembers)); + await Notifications.push(notification, groupMembers); +}; + +Notifications.rescind = async function (nids) { + nids = Array.isArray(nids) ? nids : [nids]; + await Promise.all([ + db.sortedSetRemove('notifications', nids), + db.deleteAll(nids.map(nid => `notifications:${nid}`)), + ]); +}; + +Notifications.markRead = async function (nid, uid) { + if (parseInt(uid, 10) <= 0 || !nid) { + return; + } + await Notifications.markReadMultiple([nid], uid); +}; + +Notifications.markUnread = async function (nid, uid) { + if (!(parseInt(uid, 10) > 0) || !nid) { + return; + } + const notification = await db.getObject(`notifications:${nid}`); + if (!notification) { + throw new Error('[[error:no-notification]]'); + } + notification.datetime = notification.datetime || Date.now(); + + await Promise.all([ + db.sortedSetRemove(`uid:${uid}:notifications:read`, nid), + db.sortedSetAdd(`uid:${uid}:notifications:unread`, notification.datetime, nid), + ]); +}; + +Notifications.markReadMultiple = async function (nids, uid) { + nids = nids.filter(Boolean); + if (!Array.isArray(nids) || !nids.length || !(parseInt(uid, 10) > 0)) { + return; + } + + let notificationKeys = nids.map(nid => `notifications:${nid}`); + let mergeIds = await db.getObjectsFields(notificationKeys, ['mergeId']); + // Isolate mergeIds and find related notifications + mergeIds = _.uniq(mergeIds.map(set => set.mergeId)); + + const relatedNids = await Notifications.findRelated(mergeIds, `uid:${uid}:notifications:unread`); + notificationKeys = _.union(nids, relatedNids).map(nid => `notifications:${nid}`); + + let notificationData = await db.getObjectsFields(notificationKeys, ['nid', 'datetime']); + notificationData = notificationData.filter(n => n && n.nid); + + nids = notificationData.map(n => n.nid); + const datetimes = notificationData.map(n => (n && n.datetime) || Date.now()); + await Promise.all([ + db.sortedSetRemove(`uid:${uid}:notifications:unread`, nids), + db.sortedSetAdd(`uid:${uid}:notifications:read`, datetimes, nids), + ]); +}; + +Notifications.markAllRead = async function (uid) { + const nids = await db.getSortedSetRevRange(`uid:${uid}:notifications:unread`, 0, 99); + await Notifications.markReadMultiple(nids, uid); +}; + +Notifications.prune = async function () { + const cutoffTime = Date.now() - notificationPruneCutoff; + const nids = await db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime); + if (!nids.length) { + return; + } + try { + await Promise.all([ + db.sortedSetRemove('notifications', nids), + db.deleteAll(nids.map(nid => `notifications:${nid}`)), + ]); + + await batch.processSortedSet('users:joindate', async (uids) => { + const unread = uids.map(uid => `uid:${uid}:notifications:unread`); + const read = uids.map(uid => `uid:${uid}:notifications:read`); + await db.sortedSetsRemoveRangeByScore(unread.concat(read), '-inf', cutoffTime); + }, { batch: 500, interval: 100 }); + } catch (err) { + if (err) { + winston.error(`Encountered error pruning notifications\n${err.stack}`); + } + } +}; + +Notifications.merge = async function (notifications) { + // When passed a set of notification objects, merge any that can be merged + const mergeIds = [ + 'notifications:upvoted_your_post_in', + 'notifications:user_started_following_you', + 'notifications:user_posted_to', + 'notifications:user_flagged_post_in', + 'notifications:user_flagged_user', + 'new_register', + 'post-queue', + ]; + + notifications = mergeIds.reduce((notifications, mergeId) => { + const isolated = notifications.filter(n => n && n.hasOwnProperty('mergeId') && n.mergeId.split('|')[0] === mergeId); + if (isolated.length <= 1) { + return notifications; // Nothing to merge + } + + // Each isolated mergeId may have multiple differentiators, so process each separately + const differentiators = isolated.reduce((cur, next) => { + const differentiator = next.mergeId.split('|')[1] || 0; + if (!cur.includes(differentiator)) { + cur.push(differentiator); + } + + return cur; + }, []); + + differentiators.forEach((differentiator) => { + let set; + if (differentiator === 0 && differentiators.length === 1) { + set = isolated; + } else { + set = isolated.filter(n => n.mergeId === (`${mergeId}|${differentiator}`)); + } + + const modifyIndex = notifications.indexOf(set[0]); + if (modifyIndex === -1 || set.length === 1) { + return notifications; + } + + switch (mergeId) { + case 'notifications:upvoted_your_post_in': + case 'notifications:user_started_following_you': + case 'notifications:user_posted_to': + case 'notifications:user_flagged_post_in': + case 'notifications:user_flagged_user': { + const usernames = _.uniq(set.map(notifObj => notifObj && notifObj.user && notifObj.user.username)); + const numUsers = usernames.length; + + const title = utils.decodeHTMLEntities(notifications[modifyIndex].topicTitle || ''); + let titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + titleEscaped = titleEscaped ? (`, ${titleEscaped}`) : ''; + + if (numUsers === 2) { + notifications[modifyIndex].bodyShort = `[[${mergeId}_dual, ${usernames.join(', ')}${titleEscaped}]]`; + } else if (numUsers > 2) { + notifications[modifyIndex].bodyShort = `[[${mergeId}_multiple, ${usernames[0]}, ${numUsers - 1}${titleEscaped}]]`; + } + + notifications[modifyIndex].path = set[set.length - 1].path; + } break; + + case 'new_register': + notifications[modifyIndex].bodyShort = `[[notifications:${mergeId}_multiple, ${set.length}]]`; + break; + } + + // Filter out duplicates + notifications = notifications.filter((notifObj, idx) => { + if (!notifObj || !notifObj.mergeId) { + return true; + } + + return !(notifObj.mergeId === (mergeId + (differentiator ? `|${differentiator}` : '')) && idx !== modifyIndex); + }); + }); + + return notifications; + }, notifications); + + const data = await plugins.hooks.fire('filter:notifications.merge', { + notifications: notifications, + }); + return data && data.notifications; +}; + +require('./promisify')(Notifications); diff --git a/src/pagination.js b/src/pagination.js new file mode 100644 index 0000000000..037f922e9a --- /dev/null +++ b/src/pagination.js @@ -0,0 +1,81 @@ +'use strict'; + +const qs = require('querystring'); +const _ = require('lodash'); + +const pagination = module.exports; + +pagination.create = function (currentPage, pageCount, queryObj) { + if (pageCount <= 1) { + return { + prev: { page: 1, active: currentPage > 1 }, + next: { page: 1, active: currentPage < pageCount }, + first: { page: 1, active: currentPage === 1 }, + last: { page: 1, active: currentPage === pageCount }, + rel: [], + pages: [], + currentPage: 1, + pageCount: 1, + }; + } + pageCount = parseInt(pageCount, 10); + let pagesToShow = [1, 2, pageCount - 1, pageCount]; + + currentPage = parseInt(currentPage, 10) || 1; + const previous = Math.max(1, currentPage - 1); + const next = Math.min(pageCount, currentPage + 1); + + let startPage = Math.max(1, currentPage - 2); + if (startPage > pageCount - 5) { + startPage -= 2 - (pageCount - currentPage); + } + let i; + for (i = 0; i < 5; i += 1) { + pagesToShow.push(startPage + i); + } + + pagesToShow = _.uniq(pagesToShow).filter(page => page > 0 && page <= pageCount).sort((a, b) => a - b); + + queryObj = { ...(queryObj || {}) }; + + delete queryObj._; + + const pages = pagesToShow.map((page) => { + queryObj.page = page; + return { page: page, active: page === currentPage, qs: qs.stringify(queryObj) }; + }); + + for (i = pages.length - 1; i > 0; i -= 1) { + if (pages[i].page - 2 === pages[i - 1].page) { + pages.splice(i, 0, { page: pages[i].page - 1, active: false, qs: qs.stringify(queryObj) }); + } else if (pages[i].page - 1 !== pages[i - 1].page) { + pages.splice(i, 0, { separator: true }); + } + } + + const data = { rel: [], pages: pages, currentPage: currentPage, pageCount: pageCount }; + queryObj.page = previous; + data.prev = { page: previous, active: currentPage > 1, qs: qs.stringify(queryObj) }; + queryObj.page = next; + data.next = { page: next, active: currentPage < pageCount, qs: qs.stringify(queryObj) }; + + queryObj.page = 1; + data.first = { page: 1, active: currentPage === 1, qs: qs.stringify(queryObj) }; + queryObj.page = pageCount; + data.last = { page: pageCount, active: currentPage === pageCount, qs: qs.stringify(queryObj) }; + + if (currentPage < pageCount) { + data.rel.push({ + rel: 'next', + href: `?${qs.stringify({ ...queryObj, page: next })}`, + }); + } + + if (currentPage > 1) { + data.rel.push({ + rel: 'prev', + href: `?${qs.stringify({ ...queryObj, page: previous })}`, + }); + } + return data; +}; diff --git a/src/password.js b/src/password.js new file mode 100644 index 0000000000..9ad6924b07 --- /dev/null +++ b/src/password.js @@ -0,0 +1,81 @@ +'use strict'; + +const path = require('path'); +const crypto = require('crypto'); +const util = require('util'); + +const bcrypt = require('bcryptjs'); + +const fork = require('./meta/debugFork'); + +function forkChild(message, callback) { + const child = fork(path.join(__dirname, 'password')); + + child.on('message', (msg) => { + callback(msg.err ? new Error(msg.err) : null, msg.result); + }); + child.on('error', (err) => { + console.error(err.stack); + callback(err); + }); + + child.send(message); +} + +const forkChildAsync = util.promisify(forkChild); + +exports.hash = async function (rounds, password) { + password = crypto.createHash('sha512').update(password).digest('hex'); + return await forkChildAsync({ type: 'hash', rounds: rounds, password: password }); +}; + +exports.compare = async function (password, hash, shaWrapped) { + const fakeHash = await getFakeHash(); + + if (shaWrapped) { + password = crypto.createHash('sha512').update(password).digest('hex'); + } + + return await forkChildAsync({ type: 'compare', password: password, hash: hash || fakeHash }); +}; + +let fakeHashCache; +async function getFakeHash() { + if (fakeHashCache) { + return fakeHashCache; + } + fakeHashCache = await exports.hash(12, Math.random().toString()); + return fakeHashCache; +} + +// child process +process.on('message', (msg) => { + if (msg.type === 'hash') { + tryMethod(hashPassword, msg); + } else if (msg.type === 'compare') { + tryMethod(compare, msg); + } +}); + +async function tryMethod(method, msg) { + try { + const result = await method(msg); + process.send({ result: result }); + } catch (err) { + process.send({ err: err.message }); + } finally { + process.disconnect(); + } +} + +async function hashPassword(msg) { + const salt = await bcrypt.genSalt(parseInt(msg.rounds, 10)); + const hash = await bcrypt.hash(msg.password, salt); + return hash; +} + +async function compare(msg) { + return await bcrypt.compare(String(msg.password || ''), String(msg.hash || '')); +} + +require('./promisify')(exports); diff --git a/src/plugins/data.js b/src/plugins/data.js new file mode 100644 index 0000000000..8e1914d435 --- /dev/null +++ b/src/plugins/data.js @@ -0,0 +1,265 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const winston = require('winston'); +const _ = require('lodash'); +const nconf = require('nconf'); + +const db = require('../database'); +const file = require('../file'); +const { paths } = require('../constants'); + +const Data = module.exports; + +const basePath = path.join(__dirname, '../../'); + +// to get this functionality use `plugins.getActive()` from `src/plugins/install.js` instead +// this method duplicates that one, because requiring that file here would have side effects +async function getActiveIds() { + if (nconf.get('plugins:active')) { + return nconf.get('plugins:active'); + } + return await db.getSortedSetRange('plugins:active', 0, -1); +} + +Data.getPluginPaths = async function () { + const plugins = await getActiveIds(); + const pluginPaths = plugins.filter(plugin => plugin && typeof plugin === 'string') + .map(plugin => path.join(paths.nodeModules, plugin)); + const exists = await Promise.all(pluginPaths.map(file.exists)); + exists.forEach((exists, i) => { + if (!exists) { + winston.warn(`[plugins] "${plugins[i]}" is active but not installed.`); + } + }); + return pluginPaths.filter((p, i) => exists[i]); +}; + +Data.loadPluginInfo = async function (pluginPath) { + const [packageJson, pluginJson] = await Promise.all([ + fs.promises.readFile(path.join(pluginPath, 'package.json'), 'utf8'), + fs.promises.readFile(path.join(pluginPath, 'plugin.json'), 'utf8'), + ]); + + let pluginData; + let packageData; + try { + pluginData = JSON.parse(pluginJson); + packageData = JSON.parse(packageJson); + + pluginData.license = parseLicense(packageData); + + pluginData.id = packageData.name; + pluginData.name = packageData.name; + pluginData.description = packageData.description; + pluginData.version = packageData.version; + pluginData.repository = packageData.repository; + pluginData.nbbpm = packageData.nbbpm; + pluginData.path = pluginPath; + } catch (err) { + const pluginDir = path.basename(pluginPath); + + winston.error(`[plugins/${pluginDir}] Error in plugin.json or package.json!${err.stack}`); + throw new Error('[[error:parse-error]]'); + } + return pluginData; +}; + +function parseLicense(packageData) { + try { + const licenseData = require(`spdx-license-list/licenses/${packageData.license}`); + return { + name: licenseData.name, + text: licenseData.licenseText, + }; + } catch (e) { + // No license matched + return null; + } +} + +Data.getActive = async function () { + const pluginPaths = await Data.getPluginPaths(); + return await Promise.all(pluginPaths.map(p => Data.loadPluginInfo(p))); +}; + + +Data.getStaticDirectories = async function (pluginData) { + const validMappedPath = /^[\w\-_]+$/; + + if (!pluginData.staticDirs) { + return; + } + + const dirs = Object.keys(pluginData.staticDirs); + if (!dirs.length) { + return; + } + + const staticDirs = {}; + + async function processDir(route) { + if (!validMappedPath.test(route)) { + winston.warn(`[plugins/${pluginData.id}] Invalid mapped path specified: ${ + route}. Path must adhere to: ${validMappedPath.toString()}`); + return; + } + const dirPath = await resolveModulePath(pluginData.path, pluginData.staticDirs[route]); + if (!dirPath) { + winston.warn(`[plugins/${pluginData.id}] Invalid mapped path specified: ${ + route} => ${pluginData.staticDirs[route]}`); + return; + } + try { + const stats = await fs.promises.stat(dirPath); + if (!stats.isDirectory()) { + winston.warn(`[plugins/${pluginData.id}] Mapped path '${ + route} => ${dirPath}' is not a directory.`); + return; + } + + staticDirs[`${pluginData.id}/${route}`] = dirPath; + } catch (err) { + if (err.code === 'ENOENT') { + winston.warn(`[plugins/${pluginData.id}] Mapped path '${ + route} => ${dirPath}' not found.`); + return; + } + throw err; + } + } + + await Promise.all(dirs.map(route => processDir(route))); + winston.verbose(`[plugins] found ${Object.keys(staticDirs).length} static directories for ${pluginData.id}`); + return staticDirs; +}; + + +Data.getFiles = async function (pluginData, type) { + if (!Array.isArray(pluginData[type]) || !pluginData[type].length) { + return; + } + + winston.verbose(`[plugins] Found ${pluginData[type].length} ${type} file(s) for plugin ${pluginData.id}`); + + return pluginData[type].map(file => path.join(pluginData.id, file)); +}; + +/** + * With npm@3, dependencies can become flattened, and appear at the root level. + * This method resolves these differences if it can. + */ +async function resolveModulePath(basePath, modulePath) { + const isNodeModule = /node_modules/; + + const currentPath = path.join(basePath, modulePath); + const exists = await file.exists(currentPath); + if (exists) { + return currentPath; + } + if (!isNodeModule.test(modulePath)) { + winston.warn(`[plugins] File not found: ${currentPath} (Ignoring)`); + return; + } + + const dirPath = path.dirname(basePath); + if (dirPath === basePath) { + winston.warn(`[plugins] File not found: ${currentPath} (Ignoring)`); + return; + } + + return await resolveModulePath(dirPath, modulePath); +} + + +Data.getScripts = async function getScripts(pluginData, target) { + target = (target === 'client') ? 'scripts' : 'acpScripts'; + + const input = pluginData[target]; + if (!Array.isArray(input) || !input.length) { + return; + } + + const scripts = []; + + for (const filePath of input) { + /* eslint-disable no-await-in-loop */ + const modulePath = await resolveModulePath(pluginData.path, filePath); + if (modulePath) { + scripts.push(modulePath); + } + } + if (scripts.length) { + winston.verbose(`[plugins] Found ${scripts.length} js file(s) for plugin ${pluginData.id}`); + } + return scripts; +}; + + +Data.getModules = async function getModules(pluginData) { + if (!pluginData.modules || !pluginData.hasOwnProperty('modules')) { + return; + } + + let pluginModules = pluginData.modules; + + if (Array.isArray(pluginModules)) { + const strip = parseInt(pluginData.modulesStrip, 10) || 0; + + pluginModules = pluginModules.reduce((prev, modulePath) => { + let key; + if (strip) { + key = modulePath.replace(new RegExp(`.?(/[^/]+){${strip}}/`), ''); + } else { + key = path.basename(modulePath); + } + + prev[key] = modulePath; + return prev; + }, {}); + } + + const modules = {}; + async function processModule(key) { + const modulePath = await resolveModulePath(pluginData.path, pluginModules[key]); + if (modulePath) { + modules[key] = path.relative(basePath, modulePath); + } + } + + await Promise.all(Object.keys(pluginModules).map(key => processModule(key))); + + const len = Object.keys(modules).length; + winston.verbose(`[plugins] Found ${len} AMD-style module(s) for plugin ${pluginData.id}`); + return modules; +}; + +Data.getLanguageData = async function getLanguageData(pluginData) { + if (typeof pluginData.languages !== 'string') { + return; + } + + const pathToFolder = path.join(paths.nodeModules, pluginData.id, pluginData.languages); + const filepaths = await file.walk(pathToFolder); + + const namespaces = []; + const languages = []; + + filepaths.forEach((p) => { + const rel = path.relative(pathToFolder, p).split(/[/\\]/); + const language = rel.shift().replace('_', '-').replace('@', '-x-'); + const namespace = rel.join('/').replace(/\.json$/, ''); + + if (!language || !namespace) { + return; + } + + languages.push(language); + namespaces.push(namespace); + }); + return { + languages: _.uniq(languages), + namespaces: _.uniq(namespaces), + }; +}; diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js new file mode 100644 index 0000000000..23b3aa9a86 --- /dev/null +++ b/src/plugins/hooks.js @@ -0,0 +1,280 @@ +'use strict'; + +const util = require('util'); +const winston = require('winston'); +const plugins = require('.'); +const utils = require('../utils'); + +const Hooks = module.exports; + +Hooks._deprecated = new Map([ + ['filter:email.send', { + new: 'static:email.send', + since: 'v1.17.0', + until: 'v2.0.0', + }], + ['filter:router.page', { + new: 'response:router.page', + since: 'v1.15.3', + until: 'v2.1.0', + }], + ['filter:post.purge', { + new: 'filter:posts.purge', + since: 'v1.19.6', + until: 'v2.1.0', + }], + ['action:post.purge', { + new: 'action:posts.purge', + since: 'v1.19.6', + until: 'v2.1.0', + }], + ['filter:user.verify.code', { + new: 'filter:user.verify', + since: 'v2.2.0', + until: 'v3.0.0', + }], + ['filter:flags.getFilters', { + new: 'filter:flags.init', + since: 'v2.7.0', + until: 'v3.0.0', + }], +]); + +Hooks.internals = { + _register: function (data) { + plugins.loadedHooks[data.hook] = plugins.loadedHooks[data.hook] || []; + plugins.loadedHooks[data.hook].push(data); + }, +}; + +const hookTypeToMethod = { + filter: fireFilterHook, + action: fireActionHook, + static: fireStaticHook, + response: fireResponseHook, +}; + +/* + `data` is an object consisting of (* is required): + `data.hook`*, the name of the NodeBB hook + `data.method`*, the method called in that plugin (can be an array of functions) + `data.priority`, the relative priority of the method when it is eventually called (default: 10) +*/ +Hooks.register = function (id, data) { + if (!data.hook || !data.method) { + winston.warn(`[plugins/${id}] registerHook called with invalid data.hook/method`, data); + return; + } + + // `hasOwnProperty` needed for hooks with no alternative (set to null) + if (Hooks._deprecated.has(data.hook)) { + const deprecation = Hooks._deprecated.get(data.hook); + if (!deprecation.hasOwnProperty('affected')) { + deprecation.affected = new Set(); + } + deprecation.affected.add(id); + Hooks._deprecated.set(data.hook, deprecation); + } + + data.id = id; + if (!data.priority) { + data.priority = 10; + } + + if (Array.isArray(data.method) && data.method.every(method => typeof method === 'function' || typeof method === 'string')) { + // Go go gadget recursion! + data.method.forEach((method) => { + const singularData = { ...data, method: method }; + Hooks.register(id, singularData); + }); + } else if (typeof data.method === 'string' && data.method.length > 0) { + const method = data.method.split('.').reduce((memo, prop) => { + if (memo && memo[prop]) { + return memo[prop]; + } + // Couldn't find method by path, aborting + return null; + }, plugins.libraries[data.id]); + + // Write the actual method reference to the hookObj + data.method = method; + + Hooks.internals._register(data); + } else if (typeof data.method === 'function') { + Hooks.internals._register(data); + } else { + winston.warn(`[plugins/${id}] Hook method mismatch: ${data.hook} => ${data.method}`); + } +}; + +Hooks.unregister = function (id, hook, method) { + const hooks = plugins.loadedHooks[hook] || []; + plugins.loadedHooks[hook] = hooks.filter(hookData => hookData && hookData.id !== id && hookData.method !== method); +}; + +Hooks.fire = async function (hook, params) { + const hookList = plugins.loadedHooks[hook]; + const hookType = hook.split(':')[0]; + if (global.env === 'development' && hook !== 'action:plugins.firehook' && hook !== 'filter:plugins.firehook') { + winston.verbose(`[plugins/fireHook] ${hook}`); + } + + if (!hookTypeToMethod[hookType]) { + winston.warn(`[plugins] Unknown hookType: ${hookType}, hook : ${hook}`); + return; + } + let deleteCaller = false; + if (params && typeof params === 'object' && !Array.isArray(params) && !params.hasOwnProperty('caller')) { + const als = require('../als'); + params.caller = als.getStore(); + deleteCaller = true; + } + const result = await hookTypeToMethod[hookType](hook, hookList, params); + + if (hook !== 'action:plugins.firehook' && hook !== 'filter:plugins.firehook') { + const payload = await Hooks.fire('filter:plugins.firehook', { hook: hook, params: result || params }); + Hooks.fire('action:plugins.firehook', payload); + } + if (result !== undefined) { + if (deleteCaller && result && result.hasOwnProperty('caller')) { + delete result.caller; + } + return result; + } +}; + +Hooks.hasListeners = function (hook) { + return !!(plugins.loadedHooks[hook] && plugins.loadedHooks[hook].length > 0); +}; + +async function fireFilterHook(hook, hookList, params) { + if (!Array.isArray(hookList) || !hookList.length) { + return params; + } + + async function fireMethod(hookObj, params) { + if (typeof hookObj.method !== 'function') { + if (global.env === 'development') { + winston.warn(`[plugins] Expected method for hook '${hook}' in plugin '${hookObj.id}' not found, skipping.`); + } + return params; + } + + if (hookObj.method.constructor && hookObj.method.constructor.name === 'AsyncFunction') { + return await hookObj.method(params); + } + return new Promise((resolve, reject) => { + let resolved = false; + function _resolve(result) { + if (resolved) { + winston.warn(`[plugins] ${hook} already resolved in plugin ${hookObj.id}`); + return; + } + resolved = true; + resolve(result); + } + const returned = hookObj.method(params, (err, result) => { + if (err) reject(err); else _resolve(result); + }); + + if (utils.isPromise(returned)) { + returned.then( + payload => _resolve(payload), + err => reject(err) + ); + return; + } + if (returned) { + _resolve(returned); + } + }); + } + + for (const hookObj of hookList) { + // eslint-disable-next-line + params = await fireMethod(hookObj, params); + } + return params; +} + +async function fireActionHook(hook, hookList, params) { + if (!Array.isArray(hookList) || !hookList.length) { + return; + } + for (const hookObj of hookList) { + if (typeof hookObj.method !== 'function') { + if (global.env === 'development') { + winston.warn(`[plugins] Expected method for hook '${hook}' in plugin '${hookObj.id}' not found, skipping.`); + } + } else { + // eslint-disable-next-line + await hookObj.method(params); + } + } +} + +async function fireStaticHook(hook, hookList, params) { + if (!Array.isArray(hookList) || !hookList.length) { + 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']; + + for (const hookObj of hookList) { + if (typeof hookObj.method !== 'function') { + if (global.env === 'development') { + winston.warn(`[plugins] Expected method for hook '${hook}' in plugin '${hookObj.id}' not found, skipping.`); + } + } else { + let hookFn = hookObj.method; + if (hookFn.constructor && hookFn.constructor.name !== 'AsyncFunction') { + hookFn = util.promisify(hookFn); + } + + try { + // eslint-disable-next-line + await timeout(hookFn(params), 5000, 'timeout'); + } catch (err) { + if (err && err.message === 'timeout') { + winston.warn(`[plugins] Callback timed out, hook '${hook}' in plugin '${hookObj.id}'`); + } else { + winston.error(`[plugins] Error executing '${hook}' in plugin '${hookObj.id}'\n${err.stack}`); + if (!noErrorHooks.includes(hook)) { + throw err; + } + } + } + } + } +} + +// https://advancedweb.hu/how-to-add-timeout-to-a-promise-in-javascript/ +const timeout = (prom, time, error) => { + let timer; + return Promise.race([ + prom, + new Promise((resolve, reject) => { + timer = setTimeout(reject, time, new Error(error)); + }), + ]).finally(() => clearTimeout(timer)); +}; + +async function fireResponseHook(hook, hookList, params) { + if (!Array.isArray(hookList) || !hookList.length) { + return; + } + for (const hookObj of hookList) { + if (typeof hookObj.method !== 'function') { + if (global.env === 'development') { + winston.warn(`[plugins] Expected method for hook '${hook}' in plugin '${hookObj.id}' not found, skipping.`); + } + } else { + // Skip remaining hooks if headers have been sent + if (params.res.headersSent) { + return; + } + // eslint-disable-next-line + await hookObj.method(params); + } + } +} diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 0000000000..d16e048ac6 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,320 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const winston = require('winston'); +const semver = require('semver'); +const nconf = require('nconf'); +const chalk = require('chalk'); +const request = require('request-promise-native'); + +const user = require('../user'); +const posts = require('../posts'); +const meta = require('../meta'); + +const { pluginNamePattern, themeNamePattern, paths } = require('../constants'); + +let app; +let middleware; + +const Plugins = module.exports; + +require('./install')(Plugins); +require('./load')(Plugins); +require('./usage')(Plugins); +Plugins.data = require('./data'); +Plugins.hooks = require('./hooks'); + +Plugins.getPluginPaths = Plugins.data.getPluginPaths; +Plugins.loadPluginInfo = Plugins.data.loadPluginInfo; + +Plugins.pluginsData = {}; +Plugins.libraries = {}; +Plugins.loadedHooks = {}; +Plugins.staticDirs = {}; +Plugins.cssFiles = []; +Plugins.lessFiles = []; +Plugins.acpLessFiles = []; +Plugins.clientScripts = []; +Plugins.acpScripts = []; +Plugins.libraryPaths = []; +Plugins.versionWarning = []; +Plugins.languageData = {}; +Plugins.loadedPlugins = []; + +Plugins.initialized = false; + +Plugins.requireLibrary = function (pluginData) { + let libraryPath; + // attempt to load a plugin directly with `require("nodebb-plugin-*")` + // Plugins should define their entry point in the standard `main` property of `package.json` + try { + libraryPath = pluginData.path; + Plugins.libraries[pluginData.id] = require(libraryPath); + } catch (e) { + // DEPRECATED: @1.15.0, remove in version >=1.17 + // for backwards compatibility + // if that fails, fall back to `pluginData.library` + if (pluginData.library) { + winston.warn(` [plugins/${pluginData.id}] The plugin.json field "library" is deprecated. Please use the package.json field "main" instead.`); + winston.verbose(`[plugins/${pluginData.id}] See https://github.com/NodeBB/NodeBB/issues/8686`); + + libraryPath = path.join(pluginData.path, pluginData.library); + Plugins.libraries[pluginData.id] = require(libraryPath); + } else { + throw e; + } + } + + Plugins.libraryPaths.push(libraryPath); +}; + +Plugins.init = async function (nbbApp, nbbMiddleware) { + if (Plugins.initialized) { + return; + } + + if (nbbApp) { + app = nbbApp; + middleware = nbbMiddleware; + } + + if (global.env === 'development') { + winston.verbose('[plugins] Initializing plugins system'); + } + + await Plugins.reload(); + if (global.env === 'development') { + winston.info('[plugins] Plugins OK'); + } + + Plugins.initialized = true; +}; + +Plugins.reload = async function () { + // Resetting all local plugin data + Plugins.libraries = {}; + Plugins.loadedHooks = {}; + Plugins.staticDirs = {}; + Plugins.versionWarning = []; + Plugins.cssFiles.length = 0; + Plugins.lessFiles.length = 0; + Plugins.acpLessFiles.length = 0; + Plugins.clientScripts.length = 0; + Plugins.acpScripts.length = 0; + Plugins.libraryPaths.length = 0; + Plugins.loadedPlugins.length = 0; + + await user.addInterstitials(); + + const paths = await Plugins.getPluginPaths(); + for (const path of paths) { + /* eslint-disable no-await-in-loop */ + await Plugins.loadPlugin(path); + } + + // If some plugins are incompatible, throw the warning here + if (Plugins.versionWarning.length && nconf.get('isPrimary')) { + console.log(''); + winston.warn('[plugins/load] The following plugins may not be compatible with your version of NodeBB. This may cause unintended behaviour or crashing. In the event of an unresponsive NodeBB caused by this plugin, run `./nodebb reset -p PLUGINNAME` to disable it.'); + for (let x = 0, numPlugins = Plugins.versionWarning.length; x < numPlugins; x += 1) { + console.log(`${chalk.yellow(' * ') + Plugins.versionWarning[x]}`); + } + console.log(''); + } + + // Core hooks + posts.registerHooks(); + meta.configs.registerHooks(); + + // Deprecation notices + Plugins.hooks._deprecated.forEach((deprecation, hook) => { + if (!deprecation.affected || !deprecation.affected.size) { + return; + } + + const replacement = deprecation.hasOwnProperty('new') ? `Please use ${chalk.yellow(deprecation.new)} instead.` : 'There is no alternative.'; + winston.warn(`[plugins/load] ${chalk.white.bgRed.bold('DEPRECATION')} The hook ${chalk.yellow(hook)} has been deprecated as of ${deprecation.since}, and slated for removal in ${deprecation.until}. ${replacement} The following plugins are still listening for this hook:`); + deprecation.affected.forEach(id => console.log(` ${chalk.yellow('*')} ${id}`)); + }); + + // Lower priority runs earlier + Object.keys(Plugins.loadedHooks).forEach((hook) => { + Plugins.loadedHooks[hook].sort((a, b) => a.priority - b.priority); + }); + + // Post-reload actions + await posts.configureSanitize(); +}; + +Plugins.reloadRoutes = async function (params) { + const controllers = require('../controllers'); + await Plugins.hooks.fire('static:app.load', { app: app, router: params.router, middleware: middleware, controllers: controllers }); + winston.verbose('[plugins] All plugins reloaded and rerouted'); +}; + +Plugins.get = async function (id) { + const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins/${id}`; + const body = await request(url, { + json: true, + }); + + let normalised = await Plugins.normalise([body ? body.payload : {}]); + normalised = normalised.filter(plugin => plugin.id === id); + return normalised.length ? normalised[0] : undefined; +}; + +Plugins.list = async function (matching) { + if (matching === undefined) { + matching = true; + } + const { version } = require(paths.currentPackage); + const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins${matching !== false ? `?version=${version}` : ''}`; + try { + const body = await request(url, { + json: true, + }); + return await Plugins.normalise(body); + } catch (err) { + winston.error(`Error loading ${url}`, err); + return await Plugins.normalise([]); + } +}; + +Plugins.listTrending = async () => { + const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/analytics/top/week`; + return await request(url, { + json: true, + }); +}; + +Plugins.normalise = async function (apiReturn) { + const pluginMap = {}; + const { dependencies } = require(paths.currentPackage); + apiReturn = Array.isArray(apiReturn) ? apiReturn : []; + apiReturn.forEach((packageData) => { + packageData.id = packageData.name; + packageData.installed = false; + packageData.active = false; + packageData.url = packageData.url || (packageData.repository ? packageData.repository.url : ''); + pluginMap[packageData.name] = packageData; + }); + + let installedPlugins = await Plugins.showInstalled(); + installedPlugins = installedPlugins.filter(plugin => plugin && !plugin.system); + + installedPlugins.forEach((plugin) => { + // If it errored out because a package.json or plugin.json couldn't be read, no need to do this stuff + if (plugin.error) { + pluginMap[plugin.id] = pluginMap[plugin.id] || {}; + pluginMap[plugin.id].installed = true; + pluginMap[plugin.id].error = true; + return; + } + + pluginMap[plugin.id] = pluginMap[plugin.id] || {}; + pluginMap[plugin.id].id = pluginMap[plugin.id].id || plugin.id; + pluginMap[plugin.id].name = plugin.name || pluginMap[plugin.id].name; + pluginMap[plugin.id].description = plugin.description; + pluginMap[plugin.id].url = pluginMap[plugin.id].url || plugin.url; + pluginMap[plugin.id].installed = true; + pluginMap[plugin.id].isTheme = themeNamePattern.test(plugin.id); + pluginMap[plugin.id].error = plugin.error || false; + pluginMap[plugin.id].active = plugin.active; + pluginMap[plugin.id].version = plugin.version; + pluginMap[plugin.id].settingsRoute = plugin.settingsRoute; + pluginMap[plugin.id].license = plugin.license; + + // If package.json defines a version to use, stick to that + if (dependencies.hasOwnProperty(plugin.id) && semver.valid(dependencies[plugin.id])) { + pluginMap[plugin.id].latest = dependencies[plugin.id]; + } else { + pluginMap[plugin.id].latest = pluginMap[plugin.id].latest || plugin.version; + } + pluginMap[plugin.id].outdated = semver.gt(pluginMap[plugin.id].latest, pluginMap[plugin.id].version); + }); + + const pluginArray = Object.values(pluginMap); + + pluginArray.sort((a, b) => { + if (a.name > b.name) { + return 1; + } else if (a.name < b.name) { + return -1; + } + return 0; + }); + + return pluginArray; +}; + +Plugins.nodeModulesPath = paths.nodeModules; + +Plugins.showInstalled = async function () { + const dirs = await fs.promises.readdir(Plugins.nodeModulesPath); + + let pluginPaths = await findNodeBBModules(dirs); + pluginPaths = pluginPaths.map(dir => path.join(Plugins.nodeModulesPath, dir)); + + async function load(file) { + try { + const pluginData = await Plugins.loadPluginInfo(file); + const isActive = await Plugins.isActive(pluginData.name); + delete pluginData.hooks; + delete pluginData.library; + pluginData.active = isActive; + pluginData.installed = true; + pluginData.error = false; + return pluginData; + } catch (err) { + winston.error(err.stack); + } + } + const plugins = await Promise.all(pluginPaths.map(file => load(file))); + return plugins.filter(Boolean); +}; + +async function findNodeBBModules(dirs) { + const pluginPaths = []; + await Promise.all(dirs.map(async (dirname) => { + const dirPath = path.join(Plugins.nodeModulesPath, dirname); + const isDir = await isDirectory(dirPath); + if (!isDir) { + return; + } + if (pluginNamePattern.test(dirname)) { + pluginPaths.push(dirname); + return; + } + + if (dirname[0] === '@') { + const subdirs = await fs.promises.readdir(dirPath); + await Promise.all(subdirs.map(async (subdir) => { + if (!pluginNamePattern.test(subdir)) { + return; + } + + const subdirPath = path.join(dirPath, subdir); + const isDir = await isDirectory(subdirPath); + if (isDir) { + pluginPaths.push(`${dirname}/${subdir}`); + } + })); + } + })); + return pluginPaths; +} + +async function isDirectory(dirPath) { + try { + const stats = await fs.promises.stat(dirPath); + return stats.isDirectory(); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + return false; + } +} + +require('../promisify')(Plugins); diff --git a/src/plugins/install.js b/src/plugins/install.js new file mode 100644 index 0000000000..3bc83cf078 --- /dev/null +++ b/src/plugins/install.js @@ -0,0 +1,180 @@ +'use strict'; + +const winston = require('winston'); +const path = require('path'); +const fs = require('fs').promises; +const nconf = require('nconf'); +const os = require('os'); +const cproc = require('child_process'); +const util = require('util'); +const request = require('request-promise-native'); + +const db = require('../database'); +const meta = require('../meta'); +const pubsub = require('../pubsub'); +const { paths } = require('../constants'); +const pkgInstall = require('../cli/package-install'); + +const packageManager = pkgInstall.getPackageManager(); +let packageManagerExecutable = packageManager; +const packageManagerCommands = { + yarn: { + install: 'add', + uninstall: 'remove', + }, + npm: { + install: 'install', + uninstall: 'uninstall', + }, + cnpm: { + install: 'install', + uninstall: 'uninstall', + }, + pnpm: { + install: 'install', + uninstall: 'uninstall', + }, +}; + +if (process.platform === 'win32') { + packageManagerExecutable += '.cmd'; +} + +module.exports = function (Plugins) { + if (nconf.get('isPrimary')) { + pubsub.on('plugins:toggleInstall', (data) => { + if (data.hostname !== os.hostname()) { + toggleInstall(data.id, data.version); + } + }); + + pubsub.on('plugins:upgrade', (data) => { + if (data.hostname !== os.hostname()) { + upgrade(data.id, data.version); + } + }); + } + + Plugins.toggleActive = async function (id) { + if (nconf.get('plugins:active')) { + winston.error('Cannot activate plugins while plugin state is set in the configuration (config.json, environmental variables or terminal arguments), please modify the configuration instead'); + throw new Error('[[error:plugins-set-in-configuration]]'); + } + const isActive = await Plugins.isActive(id); + if (isActive) { + await db.sortedSetRemove('plugins:active', id); + } else { + const count = await db.sortedSetCard('plugins:active'); + await db.sortedSetAdd('plugins:active', count, id); + } + meta.reloadRequired = true; + const hook = isActive ? 'deactivate' : 'activate'; + Plugins.hooks.fire(`action:plugin.${hook}`, { id: id }); + return { id: id, active: !isActive }; + }; + + Plugins.checkWhitelist = async function (id, version) { + const body = await request({ + method: 'GET', + url: `https://packages.nodebb.org/api/v1/plugins/${encodeURIComponent(id)}`, + json: true, + }); + + if (body && body.code === 'ok' && (version === 'latest' || body.payload.valid.includes(version))) { + return; + } + + throw new Error('[[error:plugin-not-whitelisted]]'); + }; + + Plugins.suggest = async function (pluginId, nbbVersion) { + const body = await request({ + method: 'GET', + url: `https://packages.nodebb.org/api/v1/suggest?package=${encodeURIComponent(pluginId)}&version=${encodeURIComponent(nbbVersion)}`, + json: true, + }); + return body; + }; + + Plugins.toggleInstall = async function (id, version) { + pubsub.publish('plugins:toggleInstall', { hostname: os.hostname(), id: id, version: version }); + return await toggleInstall(id, version); + }; + + const runPackageManagerCommandAsync = util.promisify(runPackageManagerCommand); + + async function toggleInstall(id, version) { + const [installed, active] = await Promise.all([ + Plugins.isInstalled(id), + Plugins.isActive(id), + ]); + const type = installed ? 'uninstall' : 'install'; + if (active) { + await Plugins.toggleActive(id); + } + await runPackageManagerCommandAsync(type, id, version || 'latest'); + const pluginData = await Plugins.get(id); + Plugins.hooks.fire(`action:plugin.${type}`, { id: id, version: version }); + return pluginData; + } + + function runPackageManagerCommand(command, pkgName, version, callback) { + cproc.execFile(packageManagerExecutable, [ + packageManagerCommands[packageManager][command], + pkgName + (command === 'install' ? `@${version}` : ''), + '--save', + ], (err, stdout) => { + if (err) { + return callback(err); + } + + winston.verbose(`[plugins/${command}] ${stdout}`); + callback(); + }); + } + + + Plugins.upgrade = async function (id, version) { + pubsub.publish('plugins:upgrade', { hostname: os.hostname(), id: id, version: version }); + return await upgrade(id, version); + }; + + async function upgrade(id, version) { + await runPackageManagerCommandAsync('install', id, version || 'latest'); + const isActive = await Plugins.isActive(id); + meta.reloadRequired = isActive; + return isActive; + } + + Plugins.isInstalled = async function (id) { + const pluginDir = path.join(paths.nodeModules, id); + try { + const stats = await fs.stat(pluginDir); + return stats.isDirectory(); + } catch (err) { + return false; + } + }; + + Plugins.isActive = async function (id) { + if (nconf.get('plugins:active')) { + return nconf.get('plugins:active').includes(id); + } + return await db.isSortedSetMember('plugins:active', id); + }; + + Plugins.getActive = async function () { + if (nconf.get('plugins:active')) { + return nconf.get('plugins:active'); + } + return await db.getSortedSetRange('plugins:active', 0, -1); + }; + + Plugins.autocomplete = async (fragment) => { + const pluginDir = paths.nodeModules; + const plugins = (await fs.readdir(pluginDir)).filter(filename => filename.startsWith(fragment)); + + // Autocomplete only if single match + return plugins.length === 1 ? plugins.pop() : fragment; + }; +}; diff --git a/src/plugins/load.js b/src/plugins/load.js new file mode 100644 index 0000000000..afcff23205 --- /dev/null +++ b/src/plugins/load.js @@ -0,0 +1,171 @@ +'use strict'; + +const semver = require('semver'); +const async = require('async'); +const winston = require('winston'); +const nconf = require('nconf'); +const _ = require('lodash'); + +const meta = require('../meta'); +const { themeNamePattern } = require('../constants'); + +module.exports = function (Plugins) { + async function registerPluginAssets(pluginData, fields) { + function add(dest, arr) { + dest.push(...(arr || [])); + } + + const handlers = { + staticDirs: function (next) { + Plugins.data.getStaticDirectories(pluginData, next); + }, + cssFiles: function (next) { + Plugins.data.getFiles(pluginData, 'css', next); + }, + lessFiles: function (next) { + Plugins.data.getFiles(pluginData, 'less', next); + }, + acpLessFiles: function (next) { + Plugins.data.getFiles(pluginData, 'acpLess', next); + }, + clientScripts: function (next) { + Plugins.data.getScripts(pluginData, 'client', next); + }, + acpScripts: function (next) { + Plugins.data.getScripts(pluginData, 'acp', next); + }, + modules: function (next) { + Plugins.data.getModules(pluginData, next); + }, + languageData: function (next) { + Plugins.data.getLanguageData(pluginData, next); + }, + }; + + let methods = {}; + if (Array.isArray(fields)) { + fields.forEach((field) => { + methods[field] = handlers[field]; + }); + } else { + methods = handlers; + } + + const results = await async.parallel(methods); + + Object.assign(Plugins.staticDirs, results.staticDirs || {}); + add(Plugins.cssFiles, results.cssFiles); + add(Plugins.lessFiles, results.lessFiles); + add(Plugins.acpLessFiles, results.acpLessFiles); + add(Plugins.clientScripts, results.clientScripts); + add(Plugins.acpScripts, results.acpScripts); + Object.assign(meta.js.scripts.modules, results.modules || {}); + if (results.languageData) { + Plugins.languageData.languages = _.union(Plugins.languageData.languages, results.languageData.languages); + Plugins.languageData.namespaces = _.union(Plugins.languageData.namespaces, results.languageData.namespaces); + pluginData.languageData = results.languageData; + } + Plugins.pluginsData[pluginData.id] = pluginData; + } + + Plugins.prepareForBuild = async function (targets) { + const map = { + 'plugin static dirs': ['staticDirs'], + 'requirejs modules': ['modules'], + 'client js bundle': ['clientScripts'], + 'admin js bundle': ['acpScripts'], + 'client side styles': ['cssFiles', 'lessFiles'], + 'admin control panel styles': ['cssFiles', 'lessFiles', 'acpLessFiles'], + languages: ['languageData'], + }; + + const fields = _.uniq(_.flatMap(targets, target => map[target] || [])); + + // clear old data before build + fields.forEach((field) => { + switch (field) { + case 'clientScripts': + case 'acpScripts': + case 'cssFiles': + case 'lessFiles': + case 'acpLessFiles': + Plugins[field].length = 0; + break; + case 'languageData': + Plugins.languageData.languages = []; + Plugins.languageData.namespaces = []; + break; + // do nothing for modules and staticDirs + } + }); + + winston.verbose(`[plugins] loading the following fields from plugin data: ${fields.join(', ')}`); + const plugins = await Plugins.data.getActive(); + await Promise.all(plugins.map(p => registerPluginAssets(p, fields))); + }; + + Plugins.loadPlugin = async function (pluginPath) { + let pluginData; + try { + pluginData = await Plugins.data.loadPluginInfo(pluginPath); + } catch (err) { + if (err.message === '[[error:parse-error]]') { + return; + } + if (!themeNamePattern.test(pluginPath)) { + throw err; + } + return; + } + checkVersion(pluginData); + + try { + registerHooks(pluginData); + await registerPluginAssets(pluginData); + } catch (err) { + winston.error(err.stack); + winston.verbose(`[plugins] Could not load plugin : ${pluginData.id}`); + return; + } + + if (!pluginData.private) { + Plugins.loadedPlugins.push({ + id: pluginData.id, + version: pluginData.version, + }); + } + + winston.verbose(`[plugins] Loaded plugin: ${pluginData.id}`); + }; + + function checkVersion(pluginData) { + function add() { + if (!Plugins.versionWarning.includes(pluginData.id)) { + Plugins.versionWarning.push(pluginData.id); + } + } + + if (pluginData.nbbpm && pluginData.nbbpm.compatibility && semver.validRange(pluginData.nbbpm.compatibility)) { + if (!semver.satisfies(nconf.get('version'), pluginData.nbbpm.compatibility)) { + add(); + } + } else { + add(); + } + } + + function registerHooks(pluginData) { + try { + if (!Plugins.libraries[pluginData.id]) { + Plugins.requireLibrary(pluginData); + } + + if (Array.isArray(pluginData.hooks)) { + pluginData.hooks.forEach(hook => Plugins.hooks.register(pluginData.id, hook)); + } + } catch (err) { + winston.warn(`[plugins] Unable to load library for: ${pluginData.id}`); + throw err; + } + } +}; diff --git a/src/plugins/usage.js b/src/plugins/usage.js new file mode 100644 index 0000000000..561ae7a846 --- /dev/null +++ b/src/plugins/usage.js @@ -0,0 +1,48 @@ +'use strict'; + +const nconf = require('nconf'); +const request = require('request'); +const winston = require('winston'); +const crypto = require('crypto'); +const cronJob = require('cron').CronJob; + +const pkg = require('../../package.json'); + +const meta = require('../meta'); + +module.exports = function (Plugins) { + Plugins.startJobs = function () { + new cronJob('0 0 0 * * *', (() => { + Plugins.submitUsageData(); + }), null, true); + }; + + Plugins.submitUsageData = function (callback) { + callback = callback || function () {}; + if (!meta.config.submitPluginUsage || !Plugins.loadedPlugins.length || global.env !== 'production') { + return callback(); + } + + const hash = crypto.createHash('sha256'); + hash.update(nconf.get('url')); + request.post(`${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugin/usage`, { + form: { + id: hash.digest('hex'), + version: pkg.version, + plugins: Plugins.loadedPlugins, + }, + timeout: 5000, + }, (err, res, body) => { + if (err) { + winston.error(err.stack); + return callback(err); + } + if (res.statusCode !== 200) { + winston.error(`[plugins.submitUsageData] received ${res.statusCode} ${body}`); + callback(new Error(`[[error:nbbpm-${res.statusCode}]]`)); + } else { + callback(); + } + }); + }; +}; diff --git a/src/posts/bookmarks.js b/src/posts/bookmarks.js new file mode 100644 index 0000000000..9924664bce --- /dev/null +++ b/src/posts/bookmarks.js @@ -0,0 +1,68 @@ +'use strict'; + +const db = require('../database'); +const plugins = require('../plugins'); + +module.exports = function (Posts) { + Posts.bookmark = async function (pid, uid) { + return await toggleBookmark('bookmark', pid, uid); + }; + + Posts.unbookmark = async function (pid, uid) { + return await toggleBookmark('unbookmark', pid, uid); + }; + + async function toggleBookmark(type, pid, uid) { + if (parseInt(uid, 10) <= 0) { + throw new Error('[[error:not-logged-in]]'); + } + + const isBookmarking = type === 'bookmark'; + + const [postData, hasBookmarked] = await Promise.all([ + Posts.getPostFields(pid, ['pid', 'uid']), + Posts.hasBookmarked(pid, uid), + ]); + + if (isBookmarking && hasBookmarked) { + throw new Error('[[error:already-bookmarked]]'); + } + + if (!isBookmarking && !hasBookmarked) { + throw new Error('[[error:already-unbookmarked]]'); + } + + if (isBookmarking) { + await db.sortedSetAdd(`uid:${uid}:bookmarks`, Date.now(), pid); + } else { + await db.sortedSetRemove(`uid:${uid}:bookmarks`, pid); + } + await db[isBookmarking ? 'setAdd' : 'setRemove'](`pid:${pid}:users_bookmarked`, uid); + postData.bookmarks = await db.setCount(`pid:${pid}:users_bookmarked`); + await Posts.setPostField(pid, 'bookmarks', postData.bookmarks); + + plugins.hooks.fire(`action:post.${type}`, { + pid: pid, + uid: uid, + owner: postData.uid, + current: hasBookmarked ? 'bookmarked' : 'unbookmarked', + }); + + return { + post: postData, + isBookmarked: isBookmarking, + }; + } + + Posts.hasBookmarked = async function (pid, uid) { + if (parseInt(uid, 10) <= 0) { + return Array.isArray(pid) ? pid.map(() => false) : false; + } + + if (Array.isArray(pid)) { + const sets = pid.map(pid => `pid:${pid}:users_bookmarked`); + return await db.isMemberOfSets(sets, uid); + } + return await db.isSetMember(`pid:${pid}:users_bookmarked`, uid); + }; +}; diff --git a/src/posts/cache.js b/src/posts/cache.js new file mode 100644 index 0000000000..5daee085aa --- /dev/null +++ b/src/posts/cache.js @@ -0,0 +1,12 @@ +'use strict'; + +const cacheCreate = require('../cache/lru'); +const meta = require('../meta'); + +module.exports = cacheCreate({ + name: 'post', + maxSize: meta.config.postCacheSize, + sizeCalculation: function (n) { return n.length || 1; }, + ttl: 0, + enabled: global.env === 'production', +}); diff --git a/src/posts/category.js b/src/posts/category.js new file mode 100644 index 0000000000..43334c8a5b --- /dev/null +++ b/src/posts/category.js @@ -0,0 +1,41 @@ + +'use strict'; + + +const _ = require('lodash'); + +const db = require('../database'); +const topics = require('../topics'); + +module.exports = function (Posts) { + Posts.getCidByPid = async function (pid) { + const tid = await Posts.getPostField(pid, 'tid'); + return await topics.getTopicField(tid, 'cid'); + }; + + Posts.getCidsByPids = async function (pids) { + const postData = await Posts.getPostsFields(pids, ['tid']); + const tids = _.uniq(postData.map(post => post && post.tid).filter(Boolean)); + const topicData = await topics.getTopicsFields(tids, ['cid']); + const tidToTopic = _.zipObject(tids, topicData); + const cids = postData.map(post => tidToTopic[post.tid] && tidToTopic[post.tid].cid); + return cids; + }; + + Posts.filterPidsByCid = async function (pids, cid) { + if (!cid) { + return pids; + } + + if (!Array.isArray(cid) || cid.length === 1) { + return await filterPidsBySingleCid(pids, cid); + } + const pidsArr = await Promise.all(cid.map(c => Posts.filterPidsByCid(pids, c))); + return _.union(...pidsArr); + }; + + async function filterPidsBySingleCid(pids, cid) { + const isMembers = await db.isSortedSetMembers(`cid:${parseInt(cid, 10)}:pids`, pids); + return pids.filter((pid, index) => pid && isMembers[index]); + } +}; diff --git a/src/posts/create.js b/src/posts/create.js new file mode 100644 index 0000000000..094ae1c650 --- /dev/null +++ b/src/posts/create.js @@ -0,0 +1,83 @@ +'use strict'; + +const _ = require('lodash'); + +const meta = require('../meta'); +const db = require('../database'); +const plugins = require('../plugins'); +const user = require('../user'); +const topics = require('../topics'); +const categories = require('../categories'); +const groups = require('../groups'); +const utils = require('../utils'); + +module.exports = function (Posts) { + Posts.create = async function (data) { + // This is an internal method, consider using Topics.reply instead + const { uid } = data; + const { tid } = data; + const content = data.content.toString(); + const timestamp = data.timestamp || Date.now(); + const isMain = data.isMain || false; + + if (!uid && parseInt(uid, 10) !== 0) { + throw new Error('[[error:invalid-uid]]'); + } + + if (data.toPid && !utils.isNumber(data.toPid)) { + throw new Error('[[error:invalid-pid]]'); + } + + const pid = await db.incrObjectField('global', 'nextPid'); + let postData = { + pid: pid, + uid: uid, + tid: tid, + content: content, + timestamp: timestamp, + }; + + if (data.toPid) { + postData.toPid = data.toPid; + } + if (data.ip && meta.config.trackIpPerPost) { + postData.ip = data.ip; + } + if (data.handle && !parseInt(uid, 10)) { + postData.handle = data.handle; + } + + let result = await plugins.hooks.fire('filter:post.create', { post: postData, data: data }); + postData = result.post; + await db.setObject(`post:${postData.pid}`, postData); + + const topicData = await topics.getTopicFields(tid, ['cid', 'pinned']); + postData.cid = topicData.cid; + + await Promise.all([ + db.sortedSetAdd('posts:pid', timestamp, postData.pid), + db.incrObjectField('global', 'postCount'), + user.onNewPostMade(postData), + topics.onNewPostMade(postData), + categories.onNewPostMade(topicData.cid, topicData.pinned, postData), + groups.onNewPostMade(postData), + addReplyTo(postData, timestamp), + Posts.uploads.sync(postData.pid), + ]); + + result = await plugins.hooks.fire('filter:post.get', { post: postData, uid: data.uid }); + result.post.isMain = isMain; + plugins.hooks.fire('action:post.save', { post: _.clone(result.post) }); + return result.post; + }; + + async function addReplyTo(postData, timestamp) { + if (!postData.toPid) { + return; + } + await Promise.all([ + db.sortedSetAdd(`pid:${postData.toPid}:replies`, timestamp, postData.pid), + db.incrObjectField(`post:${postData.toPid}`, 'replies'), + ]); + } +}; diff --git a/src/posts/data.js b/src/posts/data.js new file mode 100644 index 0000000000..adbfb32d6d --- /dev/null +++ b/src/posts/data.js @@ -0,0 +1,71 @@ +'use strict'; + +const db = require('../database'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const intFields = [ + 'uid', 'pid', 'tid', 'deleted', 'timestamp', + 'upvotes', 'downvotes', 'deleterUid', 'edited', + 'replies', 'bookmarks', +]; + +module.exports = function (Posts) { + Posts.getPostsFields = async function (pids, fields) { + if (!Array.isArray(pids) || !pids.length) { + return []; + } + const keys = pids.map(pid => `post:${pid}`); + const postData = await db.getObjects(keys, fields); + const result = await plugins.hooks.fire('filter:post.getFields', { + pids: pids, + posts: postData, + fields: fields, + }); + result.posts.forEach(post => modifyPost(post, fields)); + return result.posts; + }; + + Posts.getPostData = async function (pid) { + const posts = await Posts.getPostsFields([pid], []); + return posts && posts.length ? posts[0] : null; + }; + + Posts.getPostsData = async function (pids) { + return await Posts.getPostsFields(pids, []); + }; + + Posts.getPostField = async function (pid, field) { + const post = await Posts.getPostFields(pid, [field]); + return post ? post[field] : null; + }; + + Posts.getPostFields = async function (pid, fields) { + const posts = await Posts.getPostsFields([pid], fields); + return posts ? posts[0] : null; + }; + + Posts.setPostField = async function (pid, field, value) { + await Posts.setPostFields(pid, { [field]: value }); + }; + + Posts.setPostFields = async function (pid, data) { + await db.setObject(`post:${pid}`, data); + plugins.hooks.fire('action:post.setFields', { data: { ...data, pid } }); + }; +}; + +function modifyPost(post, fields) { + if (post) { + db.parseIntFields(post, intFields, fields); + if (post.hasOwnProperty('upvotes') && post.hasOwnProperty('downvotes')) { + post.votes = post.upvotes - post.downvotes; + } + if (post.hasOwnProperty('timestamp')) { + post.timestampISO = utils.toISOString(post.timestamp); + } + if (post.hasOwnProperty('edited')) { + post.editedISO = post.edited !== 0 ? utils.toISOString(post.edited) : ''; + } + } +} diff --git a/src/posts/delete.js b/src/posts/delete.js new file mode 100644 index 0000000000..5def0897b6 --- /dev/null +++ b/src/posts/delete.js @@ -0,0 +1,232 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const topics = require('../topics'); +const categories = require('../categories'); +const user = require('../user'); +const notifications = require('../notifications'); +const plugins = require('../plugins'); +const flags = require('../flags'); + +module.exports = function (Posts) { + Posts.delete = async function (pid, uid) { + return await deleteOrRestore('delete', pid, uid); + }; + + Posts.restore = async function (pid, uid) { + return await deleteOrRestore('restore', pid, uid); + }; + + async function deleteOrRestore(type, pid, uid) { + const isDeleting = type === 'delete'; + await plugins.hooks.fire(`filter:post.${type}`, { pid: pid, uid: uid }); + await Posts.setPostFields(pid, { + deleted: isDeleting ? 1 : 0, + deleterUid: isDeleting ? uid : 0, + }); + const postData = await Posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'timestamp']); + const topicData = await topics.getTopicFields(postData.tid, ['tid', 'cid', 'pinned']); + postData.cid = topicData.cid; + await Promise.all([ + topics.updateLastPostTimeFromLastPid(postData.tid), + topics.updateTeaser(postData.tid), + isDeleting ? + db.sortedSetRemove(`cid:${topicData.cid}:pids`, pid) : + db.sortedSetAdd(`cid:${topicData.cid}:pids`, postData.timestamp, pid), + ]); + await categories.updateRecentTidForCid(postData.cid); + plugins.hooks.fire(`action:post.${type}`, { post: _.clone(postData), uid: uid }); + if (type === 'delete') { + await flags.resolveFlag('post', pid, uid); + } + return postData; + } + + Posts.purge = async function (pids, uid) { + pids = Array.isArray(pids) ? pids : [pids]; + let postData = await Posts.getPostsData(pids); + pids = pids.filter((pid, index) => !!postData[index]); + postData = postData.filter(Boolean); + if (!postData.length) { + return; + } + const uniqTids = _.uniq(postData.map(p => p.tid)); + const topicData = await topics.getTopicsFields(uniqTids, ['tid', 'cid', 'pinned', 'postcount']); + const tidToTopic = _.zipObject(uniqTids, topicData); + + postData.forEach((p) => { + p.topic = tidToTopic[p.tid]; + p.cid = tidToTopic[p.tid] && tidToTopic[p.tid].cid; + }); + + // deprecated hook + await Promise.all(postData.map(p => plugins.hooks.fire('filter:post.purge', { post: p, pid: p.pid, uid: uid }))); + + // new hook + await plugins.hooks.fire('filter:posts.purge', { + posts: postData, + pids: postData.map(p => p.pid), + uid: uid, + }); + + await Promise.all([ + deleteFromTopicUserNotification(postData), + deleteFromCategoryRecentPosts(postData), + deleteFromUsersBookmarks(pids), + deleteFromUsersVotes(pids), + deleteFromReplies(postData), + deleteFromGroups(pids), + deleteDiffs(pids), + deleteFromUploads(pids), + db.sortedSetsRemove(['posts:pid', 'posts:votes', 'posts:flagged'], pids), + ]); + + await resolveFlags(postData, uid); + + // deprecated hook + Promise.all(postData.map(p => plugins.hooks.fire('action:post.purge', { post: p, uid: uid }))); + + // new hook + plugins.hooks.fire('action:posts.purge', { posts: postData, uid: uid }); + + await db.deleteAll(postData.map(p => `post:${p.pid}`)); + }; + + async function deleteFromTopicUserNotification(postData) { + const bulkRemove = []; + postData.forEach((p) => { + bulkRemove.push([`tid:${p.tid}:posts`, p.pid]); + bulkRemove.push([`tid:${p.tid}:posts:votes`, p.pid]); + bulkRemove.push([`uid:${p.uid}:posts`, p.pid]); + bulkRemove.push([`cid:${p.cid}:uid:${p.uid}:pids`, p.pid]); + bulkRemove.push([`cid:${p.cid}:uid:${p.uid}:pids:votes`, p.pid]); + }); + await db.sortedSetRemoveBulk(bulkRemove); + + const incrObjectBulk = [['global', { postCount: -postData.length }]]; + + const postsByCategory = _.groupBy(postData, p => parseInt(p.cid, 10)); + for (const [cid, posts] of Object.entries(postsByCategory)) { + incrObjectBulk.push([`category:${cid}`, { post_count: -posts.length }]); + } + + const postsByTopic = _.groupBy(postData, p => parseInt(p.tid, 10)); + const topicPostCountTasks = []; + const topicTasks = []; + const zsetIncrBulk = []; + for (const [tid, posts] of Object.entries(postsByTopic)) { + incrObjectBulk.push([`topic:${tid}`, { postcount: -posts.length }]); + if (posts.length && posts[0]) { + const topicData = posts[0].topic; + const newPostCount = topicData.postcount - posts.length; + topicPostCountTasks.push(['topics:posts', newPostCount, tid]); + if (!topicData.pinned) { + zsetIncrBulk.push([`cid:${topicData.cid}:tids:posts`, -posts.length, tid]); + } + } + topicTasks.push(topics.updateTeaser(tid)); + topicTasks.push(topics.updateLastPostTimeFromLastPid(tid)); + const postsByUid = _.groupBy(posts, p => parseInt(p.uid, 10)); + for (const [uid, uidPosts] of Object.entries(postsByUid)) { + zsetIncrBulk.push([`tid:${tid}:posters`, -uidPosts.length, uid]); + } + topicTasks.push(db.sortedSetIncrByBulk(zsetIncrBulk)); + } + + await Promise.all([ + db.incrObjectFieldByBulk(incrObjectBulk), + db.sortedSetAddBulk(topicPostCountTasks), + ...topicTasks, + user.updatePostCount(_.uniq(postData.map(p => p.uid))), + notifications.rescind(...postData.map(p => `new_post:tid:${p.tid}:pid:${p.pid}:uid:${p.uid}`)), + ]); + } + + async function deleteFromCategoryRecentPosts(postData) { + const uniqCids = _.uniq(postData.map(p => p.cid)); + const sets = uniqCids.map(cid => `cid:${cid}:pids`); + await db.sortedSetRemove(sets, postData.map(p => p.pid)); + await Promise.all(uniqCids.map(categories.updateRecentTidForCid)); + } + + async function deleteFromUsersBookmarks(pids) { + const arrayOfUids = await db.getSetsMembers(pids.map(pid => `pid:${pid}:users_bookmarked`)); + const bulkRemove = []; + pids.forEach((pid, index) => { + arrayOfUids[index].forEach((uid) => { + bulkRemove.push([`uid:${uid}:bookmarks`, pid]); + }); + }); + await db.sortedSetRemoveBulk(bulkRemove); + await db.deleteAll(pids.map(pid => `pid:${pid}:users_bookmarked`)); + } + + async function deleteFromUsersVotes(pids) { + const [upvoters, downvoters] = await Promise.all([ + db.getSetsMembers(pids.map(pid => `pid:${pid}:upvote`)), + db.getSetsMembers(pids.map(pid => `pid:${pid}:downvote`)), + ]); + const bulkRemove = []; + pids.forEach((pid, index) => { + upvoters[index].forEach((upvoterUid) => { + bulkRemove.push([`uid:${upvoterUid}:upvote`, pid]); + }); + downvoters[index].forEach((downvoterUid) => { + bulkRemove.push([`uid:${downvoterUid}:downvote`, pid]); + }); + }); + + await Promise.all([ + db.sortedSetRemoveBulk(bulkRemove), + db.deleteAll([ + ...pids.map(pid => `pid:${pid}:upvote`), + ...pids.map(pid => `pid:${pid}:downvote`), + ]), + ]); + } + + async function deleteFromReplies(postData) { + const arrayOfReplyPids = await db.getSortedSetsMembers(postData.map(p => `pid:${p.pid}:replies`)); + const allReplyPids = _.flatten(arrayOfReplyPids); + const promises = [ + db.deleteObjectFields( + allReplyPids.map(pid => `post:${pid}`), ['toPid'] + ), + db.deleteAll(postData.map(p => `pid:${p.pid}:replies`)), + ]; + + const postsWithParents = postData.filter(p => parseInt(p.toPid, 10)); + const bulkRemove = postsWithParents.map(p => [`pid:${p.toPid}:replies`, p.pid]); + promises.push(db.sortedSetRemoveBulk(bulkRemove)); + await Promise.all(promises); + + const parentPids = _.uniq(postsWithParents.map(p => p.toPid)); + const counts = await db.sortedSetsCard(parentPids.map(pid => `pid:${pid}:replies`)); + await db.setObjectBulk(parentPids.map((pid, index) => [`post:${pid}`, { replies: counts[index] }])); + } + + async function deleteFromGroups(pids) { + const groupNames = await db.getSortedSetMembers('groups:visible:createtime'); + const keys = groupNames.map(groupName => `group:${groupName}:member:pids`); + await db.sortedSetRemove(keys, pids); + } + + async function deleteDiffs(pids) { + const timestamps = await Promise.all(pids.map(pid => Posts.diffs.list(pid))); + await db.deleteAll([ + ...pids.map(pid => `post:${pid}:diffs`), + ..._.flattenDeep(pids.map((pid, index) => timestamps[index].map(t => `diff:${pid}.${t}`))), + ]); + } + + async function deleteFromUploads(pids) { + await Promise.all(pids.map(Posts.uploads.dissociateAll)); + } + + async function resolveFlags(postData, uid) { + const flaggedPosts = postData.filter(p => parseInt(p.flagId, 10)); + await Promise.all(flaggedPosts.map(p => flags.update(p.flagId, uid, { state: 'resolved' }))); + } +}; diff --git a/src/posts/diffs.js b/src/posts/diffs.js new file mode 100644 index 0000000000..ee3bbeb155 --- /dev/null +++ b/src/posts/diffs.js @@ -0,0 +1,175 @@ +'use strict'; + +const validator = require('validator'); +const diff = require('diff'); + +const db = require('../database'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const translator = require('../translator'); +const topics = require('../topics'); + +module.exports = function (Posts) { + const Diffs = {}; + Posts.diffs = Diffs; + Diffs.exists = async function (pid) { + if (meta.config.enablePostHistory !== 1) { + return false; + } + + const numDiffs = await db.listLength(`post:${pid}:diffs`); + return !!numDiffs; + }; + + Diffs.get = async function (pid, since) { + const timestamps = await Diffs.list(pid); + if (!since) { + since = 0; + } + + // Pass those made after `since`, and create keys + const keys = timestamps.filter(t => (parseInt(t, 10) || 0) > since) + .map(t => `diff:${pid}.${t}`); + return await db.getObjects(keys); + }; + + Diffs.list = async function (pid) { + return await db.getListRange(`post:${pid}:diffs`, 0, -1); + }; + + Diffs.save = async function (data) { + const { pid, uid, oldContent, newContent, edited, topic } = data; + const editTimestamp = edited || Date.now(); + const diffData = { + uid: uid, + pid: pid, + }; + if (oldContent !== newContent) { + diffData.patch = diff.createPatch('', newContent, oldContent); + } + if (topic.renamed) { + diffData.title = topic.oldTitle; + } + if (topic.tagsupdated && Array.isArray(topic.oldTags)) { + diffData.tags = topic.oldTags.map(tag => tag && tag.value).filter(Boolean).join(','); + } + await Promise.all([ + db.listPrepend(`post:${pid}:diffs`, editTimestamp), + db.setObject(`diff:${pid}.${editTimestamp}`, diffData), + ]); + }; + + Diffs.load = async function (pid, since, uid) { + since = getValidatedTimestamp(since); + const post = await postDiffLoad(pid, since, uid); + post.content = String(post.content || ''); + + const result = await plugins.hooks.fire('filter:parse.post', { postData: post }); + result.postData.content = translator.escape(result.postData.content); + return result.postData; + }; + + Diffs.restore = async function (pid, since, uid, req) { + since = getValidatedTimestamp(since); + const post = await postDiffLoad(pid, since, uid); + + return await Posts.edit({ + uid: uid, + pid: pid, + content: post.content, + req: req, + timestamp: since, + title: post.topic.title, + tags: post.topic.tags.map(tag => tag.value), + }); + }; + + Diffs.delete = async function (pid, timestamp, uid) { + getValidatedTimestamp(timestamp); + + const [post, diffs, timestamps] = await Promise.all([ + Posts.getPostSummaryByPids([pid], uid, { parse: false }), + Diffs.get(pid), + Diffs.list(pid), + ]); + + const timestampIndex = timestamps.indexOf(timestamp); + const lastTimestampIndex = timestamps.length - 1; + + if (timestamp === String(post[0].timestamp)) { + // Deleting oldest diff, so history rewrite is not needed + return Promise.all([ + db.delete(`diff:${pid}.${timestamps[lastTimestampIndex]}`), + db.listRemoveAll(`post:${pid}:diffs`, timestamps[lastTimestampIndex]), + ]); + } + if (timestampIndex === 0 || timestampIndex === -1) { + throw new Error('[[error:invalid-data]]'); + } + + const postContent = validator.unescape(post[0].content); + const versionContents = {}; + for (let i = 0, content = postContent; i < timestamps.length; ++i) { + versionContents[timestamps[i]] = applyPatch(content, diffs[i]); + content = versionContents[timestamps[i]]; + } + + /* eslint-disable no-await-in-loop */ + for (let i = lastTimestampIndex; i >= timestampIndex; --i) { + // Recreate older diffs with skipping the deleted diff + const newContentIndex = i === timestampIndex ? i - 2 : i - 1; + const timestampToUpdate = newContentIndex + 1; + const newContent = newContentIndex < 0 ? postContent : versionContents[timestamps[newContentIndex]]; + const patch = diff.createPatch('', newContent, versionContents[timestamps[i]]); + await db.setObject(`diff:${pid}.${timestamps[timestampToUpdate]}`, { patch }); + } + + return Promise.all([ + db.delete(`diff:${pid}.${timestamp}`), + db.listRemoveAll(`post:${pid}:diffs`, timestamp), + ]); + }; + + async function postDiffLoad(pid, since, uid) { + // Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` + const [post, diffs] = await Promise.all([ + Posts.getPostSummaryByPids([pid], uid, { parse: false }), + Posts.diffs.get(pid, since), + ]); + + // Replace content with re-constructed content from that point in time + post[0].content = diffs.reduce(applyPatch, validator.unescape(post[0].content)); + + const titleDiffs = diffs.filter(d => d.hasOwnProperty('title') && d.title); + if (titleDiffs.length && post[0].topic) { + post[0].topic.title = validator.unescape(String(titleDiffs[titleDiffs.length - 1].title)); + } + const tagDiffs = diffs.filter(d => d.hasOwnProperty('tags') && d.tags); + if (tagDiffs.length && post[0].topic) { + const tags = tagDiffs[tagDiffs.length - 1].tags.split(',').map(tag => ({ value: tag })); + post[0].topic.tags = await topics.getTagData(tags); + } + + return post[0]; + } + + function getValidatedTimestamp(timestamp) { + timestamp = parseInt(timestamp, 10); + + if (isNaN(timestamp)) { + throw new Error('[[error:invalid-data]]'); + } + + return timestamp; + } + + function applyPatch(content, aDiff) { + if (aDiff && aDiff.patch) { + const result = diff.applyPatch(content, aDiff.patch, { + fuzzFactor: 1, + }); + return typeof result === 'string' ? result : content; + } + return content; + } +}; diff --git a/src/posts/edit.js b/src/posts/edit.js new file mode 100644 index 0000000000..6349148cc1 --- /dev/null +++ b/src/posts/edit.js @@ -0,0 +1,217 @@ +'use strict'; + +const validator = require('validator'); +const _ = require('lodash'); + +const db = require('../database'); +const meta = require('../meta'); +const topics = require('../topics'); +const user = require('../user'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const pubsub = require('../pubsub'); +const utils = require('../utils'); +const slugify = require('../slugify'); +const translator = require('../translator'); + +module.exports = function (Posts) { + pubsub.on('post:edit', (pid) => { + require('./cache').del(pid); + }); + + Posts.edit = async function (data) { + const canEdit = await privileges.posts.canEdit(data.pid, data.uid); + if (!canEdit.flag) { + throw new Error(canEdit.message); + } + const postData = await Posts.getPostData(data.pid); + if (!postData) { + throw new Error('[[error:no-post]]'); + } + + const topicData = await topics.getTopicFields(postData.tid, [ + 'cid', 'mainPid', 'title', 'timestamp', 'scheduled', 'slug', 'tags', + ]); + + await scheduledTopicCheck(data, topicData); + + const oldContent = postData.content; // for diffing purposes + const editPostData = getEditPostData(data, topicData, postData); + + if (data.handle) { + editPostData.handle = data.handle; + } + + const result = await plugins.hooks.fire('filter:post.edit', { + req: data.req, + post: editPostData, + data: data, + uid: data.uid, + }); + + const [editor, topic] = await Promise.all([ + user.getUserFields(data.uid, ['username', 'userslug']), + editMainPost(data, postData, topicData), + ]); + + await Posts.setPostFields(data.pid, result.post); + const contentChanged = data.content !== oldContent || + topic.renamed || + topic.tagsupdated; + + if (meta.config.enablePostHistory === 1 && contentChanged) { + await Posts.diffs.save({ + pid: data.pid, + uid: data.uid, + oldContent: oldContent, + newContent: data.content, + edited: editPostData.edited, + topic, + }); + } + await Posts.uploads.sync(data.pid); + + // Normalize data prior to constructing returnPostData (match types with getPostSummaryByPids) + postData.deleted = !!postData.deleted; + + const returnPostData = { ...postData, ...result.post }; + returnPostData.cid = topic.cid; + returnPostData.topic = topic; + returnPostData.editedISO = utils.toISOString(editPostData.edited); + returnPostData.changed = contentChanged; + returnPostData.oldContent = oldContent; + returnPostData.newContent = data.content; + + await topics.notifyFollowers(returnPostData, data.uid, { + type: 'post-edit', + bodyShort: translator.compile('notifications:user_edited_post', editor.username, topic.title), + nid: `edit_post:${data.pid}:uid:${data.uid}`, + }); + await topics.syncBacklinks(returnPostData); + + plugins.hooks.fire('action:post.edit', { post: _.clone(returnPostData), data: data, uid: data.uid }); + + require('./cache').del(String(postData.pid)); + pubsub.publish('post:edit', String(postData.pid)); + + await Posts.parsePost(returnPostData); + + return { + topic: topic, + editor: editor, + post: returnPostData, + }; + }; + + async function editMainPost(data, postData, topicData) { + const { tid } = postData; + const title = data.title ? data.title.trim() : ''; + + const isMain = parseInt(data.pid, 10) === parseInt(topicData.mainPid, 10); + if (!isMain) { + return { + tid: tid, + cid: topicData.cid, + title: validator.escape(String(topicData.title)), + isMainPost: false, + renamed: false, + tagsupdated: false, + }; + } + + const newTopicData = { + tid: tid, + cid: topicData.cid, + uid: postData.uid, + mainPid: data.pid, + timestamp: rescheduling(data, topicData) ? data.timestamp : topicData.timestamp, + }; + if (title) { + newTopicData.title = title; + newTopicData.slug = `${tid}/${slugify(title) || 'topic'}`; + } + + const tagsupdated = Array.isArray(data.tags) && + !_.isEqual(data.tags, topicData.tags.map(tag => tag.value)); + + if (tagsupdated) { + const canTag = await privileges.categories.can('topics:tag', topicData.cid, data.uid); + if (!canTag) { + throw new Error('[[error:no-privileges]]'); + } + await topics.validateTags(data.tags, topicData.cid, data.uid, tid); + } + + const results = await plugins.hooks.fire('filter:topic.edit', { + req: data.req, + topic: newTopicData, + data: data, + }); + await db.setObject(`topic:${tid}`, results.topic); + if (tagsupdated) { + await topics.updateTopicTags(tid, data.tags); + } + const tags = await topics.getTopicTagsObjects(tid); + + if (rescheduling(data, topicData)) { + await topics.scheduled.reschedule(newTopicData); + } + + newTopicData.tags = data.tags; + newTopicData.oldTitle = topicData.title; + const renamed = title && translator.escape(validator.escape(String(title))) !== topicData.title; + plugins.hooks.fire('action:topic.edit', { topic: newTopicData, uid: data.uid }); + return { + tid: tid, + cid: newTopicData.cid, + uid: postData.uid, + title: validator.escape(String(title)), + oldTitle: topicData.title, + slug: newTopicData.slug || topicData.slug, + isMainPost: true, + renamed: renamed, + tagsupdated: tagsupdated, + tags: tags, + oldTags: topicData.tags, + rescheduled: rescheduling(data, topicData), + }; + } + + async function scheduledTopicCheck(data, topicData) { + if (!topicData.scheduled) { + return; + } + const canSchedule = await privileges.categories.can('topics:schedule', topicData.cid, data.uid); + if (!canSchedule) { + throw new Error('[[error:no-privileges]]'); + } + const isMain = parseInt(data.pid, 10) === parseInt(topicData.mainPid, 10); + if (isMain && (isNaN(data.timestamp) || data.timestamp < Date.now())) { + throw new Error('[[error:invalid-data]]'); + } + } + + function getEditPostData(data, topicData, postData) { + const editPostData = { + content: data.content, + editor: data.uid, + }; + + // For posts in scheduled topics, if edited before, use edit timestamp + editPostData.edited = topicData.scheduled ? (postData.edited || postData.timestamp) + 1 : Date.now(); + + // if rescheduling the main post + if (rescheduling(data, topicData)) { + // For main posts, use timestamp coming from user (otherwise, it is ignored) + editPostData.edited = data.timestamp; + editPostData.timestamp = data.timestamp; + } + + return editPostData; + } + + function rescheduling(data, topicData) { + const isMain = parseInt(data.pid, 10) === parseInt(topicData.mainPid, 10); + return isMain && topicData.scheduled && topicData.timestamp !== data.timestamp; + } +}; diff --git a/src/posts/index.js b/src/posts/index.js new file mode 100644 index 0000000000..baf25d20a0 --- /dev/null +++ b/src/posts/index.js @@ -0,0 +1,104 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const utils = require('../utils'); +const user = require('../user'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); + +const Posts = module.exports; + +require('./data')(Posts); +require('./create')(Posts); +require('./delete')(Posts); +require('./edit')(Posts); +require('./parse')(Posts); +require('./user')(Posts); +require('./topics')(Posts); +require('./category')(Posts); +require('./summary')(Posts); +require('./recent')(Posts); +require('./tools')(Posts); +require('./votes')(Posts); +require('./bookmarks')(Posts); +require('./queue')(Posts); +require('./diffs')(Posts); +require('./uploads')(Posts); + +Posts.exists = async function (pids) { + return await db.exists( + Array.isArray(pids) ? pids.map(pid => `post:${pid}`) : `post:${pids}` + ); +}; + +Posts.getPidsFromSet = async function (set, start, stop, reverse) { + if (isNaN(start) || isNaN(stop)) { + return []; + } + return await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop); +}; + +Posts.getPostsByPids = async function (pids, uid) { + if (!Array.isArray(pids) || !pids.length) { + return []; + } + let posts = await Posts.getPostsData(pids); + posts = await Promise.all(posts.map(Posts.parsePost)); + const data = await plugins.hooks.fire('filter:post.getPosts', { posts: posts, uid: uid }); + if (!data || !Array.isArray(data.posts)) { + return []; + } + return data.posts.filter(Boolean); +}; + +Posts.getPostSummariesFromSet = async function (set, uid, start, stop) { + let pids = await db.getSortedSetRevRange(set, start, stop); + pids = await privileges.posts.filter('topics:read', pids, uid); + const posts = await Posts.getPostSummaryByPids(pids, uid, { stripTags: false }); + return { posts: posts, nextStart: stop + 1 }; +}; + +Posts.getPidIndex = async function (pid, tid, topicPostSort) { + const set = topicPostSort === 'most_votes' ? `tid:${tid}:posts:votes` : `tid:${tid}:posts`; + const reverse = topicPostSort === 'newest_to_oldest' || topicPostSort === 'most_votes'; + const index = await db[reverse ? 'sortedSetRevRank' : 'sortedSetRank'](set, pid); + if (!utils.isNumber(index)) { + return 0; + } + return utils.isNumber(index) ? parseInt(index, 10) + 1 : 0; +}; + +Posts.getPostIndices = async function (posts, uid) { + if (!Array.isArray(posts) || !posts.length) { + return []; + } + const settings = await user.getSettings(uid); + + const byVotes = settings.topicPostSort === 'most_votes'; + let sets = posts.map(p => (byVotes ? `tid:${p.tid}:posts:votes` : `tid:${p.tid}:posts`)); + const reverse = settings.topicPostSort === 'newest_to_oldest' || settings.topicPostSort === 'most_votes'; + + const uniqueSets = _.uniq(sets); + let method = reverse ? 'sortedSetsRevRanks' : 'sortedSetsRanks'; + if (uniqueSets.length === 1) { + method = reverse ? 'sortedSetRevRanks' : 'sortedSetRanks'; + sets = uniqueSets[0]; + } + + const pids = posts.map(post => post.pid); + const indices = await db[method](sets, pids); + return indices.map(index => (utils.isNumber(index) ? parseInt(index, 10) + 1 : 0)); +}; + +Posts.modifyPostByPrivilege = function (post, privileges) { + if (post && post.deleted && !(post.selfPost || privileges['posts:view_deleted'])) { + post.content = '[[topic:post_is_deleted]]'; + if (post.user) { + post.user.signature = ''; + } + } +}; + +require('../promisify')(Posts); diff --git a/src/posts/parse.js b/src/posts/parse.js new file mode 100644 index 0000000000..771fd0d054 --- /dev/null +++ b/src/posts/parse.js @@ -0,0 +1,174 @@ +'use strict'; + +const nconf = require('nconf'); +const url = require('url'); +const winston = require('winston'); +const sanitize = require('sanitize-html'); +const _ = require('lodash'); + +const meta = require('../meta'); +const plugins = require('../plugins'); +const translator = require('../translator'); +const utils = require('../utils'); + +let sanitizeConfig = { + allowedTags: sanitize.defaults.allowedTags.concat([ + // Some safe-to-use tags to add + 'sup', 'ins', 'del', 'img', 'button', + 'video', 'audio', 'iframe', 'embed', + // 'sup' still necessary until https://github.com/apostrophecms/sanitize-html/pull/422 merged + ]), + allowedAttributes: { + ...sanitize.defaults.allowedAttributes, + a: ['href', 'name', 'hreflang', 'media', 'rel', 'target', 'type'], + img: ['alt', 'height', 'ismap', 'src', 'usemap', 'width', 'srcset'], + iframe: ['height', 'name', 'src', 'width'], + video: ['autoplay', 'controls', 'height', 'loop', 'muted', 'poster', 'preload', 'src', 'width'], + audio: ['autoplay', 'controls', 'loop', 'muted', 'preload', 'src'], + embed: ['height', 'src', 'type', 'width'], + }, + globalAttributes: ['accesskey', 'class', 'contenteditable', 'dir', + 'draggable', 'dropzone', 'hidden', 'id', 'lang', 'spellcheck', 'style', + 'tabindex', 'title', 'translate', 'aria-expanded', 'data-*', + ], + allowedClasses: { + ...sanitize.defaults.allowedClasses, + }, +}; + +module.exports = function (Posts) { + Posts.urlRegex = { + regex: /href="([^"]+)"/g, + length: 6, + }; + + Posts.imgRegex = { + regex: /src="([^"]+)"/g, + length: 5, + }; + + Posts.parsePost = async function (postData) { + if (!postData) { + return postData; + } + postData.content = String(postData.content || ''); + const cache = require('./cache'); + const pid = String(postData.pid); + const cachedContent = cache.get(pid); + if (postData.pid && cachedContent !== undefined) { + postData.content = cachedContent; + return postData; + } + + const data = await plugins.hooks.fire('filter:parse.post', { postData: postData }); + data.postData.content = translator.escape(data.postData.content); + if (data.postData.pid) { + cache.set(pid, data.postData.content); + } + return data.postData; + }; + + Posts.parseSignature = async function (userData, uid) { + userData.signature = sanitizeSignature(userData.signature || ''); + return await plugins.hooks.fire('filter:parse.signature', { userData: userData, uid: uid }); + }; + + Posts.relativeToAbsolute = function (content, regex) { + // Turns relative links in content to absolute urls + if (!content) { + return content; + } + let parsed; + let current = regex.regex.exec(content); + let absolute; + while (current !== null) { + if (current[1]) { + try { + parsed = url.parse(current[1]); + if (!parsed.protocol) { + if (current[1].startsWith('/')) { + // Internal link + absolute = nconf.get('base_url') + current[1]; + } else { + // External link + absolute = `//${current[1]}`; + } + + content = content.slice(0, current.index + regex.length) + + absolute + + content.slice(current.index + regex.length + current[1].length); + } + } catch (err) { + winston.verbose(err.messsage); + } + } + current = regex.regex.exec(content); + } + + return content; + }; + + Posts.sanitize = function (content) { + return sanitize(content, { + allowedTags: sanitizeConfig.allowedTags, + allowedAttributes: sanitizeConfig.allowedAttributes, + allowedClasses: sanitizeConfig.allowedClasses, + }); + }; + + Posts.configureSanitize = async () => { + // Each allowed tags should have some common global attributes... + sanitizeConfig.allowedTags.forEach((tag) => { + sanitizeConfig.allowedAttributes[tag] = _.union( + sanitizeConfig.allowedAttributes[tag], + sanitizeConfig.globalAttributes + ); + }); + + // Some plugins might need to adjust or whitelist their own tags... + sanitizeConfig = await plugins.hooks.fire('filter:sanitize.config', sanitizeConfig); + }; + + Posts.registerHooks = () => { + plugins.hooks.register('core', { + hook: 'filter:parse.post', + method: async (data) => { + data.postData.content = Posts.sanitize(data.postData.content); + return data; + }, + }); + + plugins.hooks.register('core', { + hook: 'filter:parse.raw', + method: async content => Posts.sanitize(content), + }); + + plugins.hooks.register('core', { + hook: 'filter:parse.aboutme', + method: async content => Posts.sanitize(content), + }); + + plugins.hooks.register('core', { + hook: 'filter:parse.signature', + method: async (data) => { + data.userData.signature = Posts.sanitize(data.userData.signature); + return data; + }, + }); + }; + + function sanitizeSignature(signature) { + signature = translator.escape(signature); + const tagsToStrip = []; + + if (meta.config['signatures:disableLinks']) { + tagsToStrip.push('a'); + } + + if (meta.config['signatures:disableImages']) { + tagsToStrip.push('img'); + } + + return utils.stripHTMLTags(signature, tagsToStrip); + } +}; diff --git a/src/posts/queue.js b/src/posts/queue.js new file mode 100644 index 0000000000..2aa9576f56 --- /dev/null +++ b/src/posts/queue.js @@ -0,0 +1,367 @@ +'use strict'; + +const _ = require('lodash'); +const validator = require('validator'); +const nconf = require('nconf'); + +const db = require('../database'); +const user = require('../user'); +const meta = require('../meta'); +const groups = require('../groups'); +const topics = require('../topics'); +const categories = require('../categories'); +const notifications = require('../notifications'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const cache = require('../cache'); +const socketHelpers = require('../socket.io/helpers'); + +module.exports = function (Posts) { + Posts.getQueuedPosts = async (filter = {}, options = {}) => { + options = { metadata: true, ...options }; // defaults + let postData = _.cloneDeep(cache.get('post-queue')); + if (!postData) { + const ids = await db.getSortedSetRange('post:queue', 0, -1); + const keys = ids.map(id => `post:queue:${id}`); + postData = await db.getObjects(keys); + postData.forEach((data) => { + if (data) { + data.data = JSON.parse(data.data); + data.data.timestampISO = utils.toISOString(data.data.timestamp); + } + }); + const uids = postData.map(data => data && data.uid); + const userData = await user.getUsersFields(uids, ['username', 'userslug', 'picture']); + postData.forEach((postData, index) => { + if (postData) { + postData.user = userData[index]; + postData.data.rawContent = validator.escape(String(postData.data.content)); + postData.data.title = validator.escape(String(postData.data.title || '')); + } + }); + cache.set('post-queue', _.cloneDeep(postData)); + } + if (filter.id) { + postData = postData.filter(p => p.id === filter.id); + } + if (options.metadata) { + await Promise.all(postData.map(p => addMetaData(p))); + } + + // Filter by tid if present + if (utils.isNumber(filter.tid)) { + const tid = parseInt(filter.tid, 10); + postData = postData.filter(item => item.data.tid && parseInt(item.data.tid, 10) === tid); + } else if (Array.isArray(filter.tid)) { + const tids = filter.tid.map(tid => parseInt(tid, 10)); + postData = postData.filter( + item => item.data.tid && tids.includes(parseInt(item.data.tid, 10)) + ); + } + + return postData; + }; + + async function addMetaData(postData) { + if (!postData) { + return; + } + postData.topic = { cid: 0 }; + if (postData.data.cid) { + postData.topic = { cid: parseInt(postData.data.cid, 10) }; + } else if (postData.data.tid) { + postData.topic = await topics.getTopicFields(postData.data.tid, ['title', 'cid']); + } + postData.category = await categories.getCategoryData(postData.topic.cid); + const result = await plugins.hooks.fire('filter:parse.post', { postData: postData.data }); + postData.data.content = result.postData.content; + } + + Posts.shouldQueue = async function (uid, data) { + const [userData, isMemberOfExempt, categoryQueueEnabled] = await Promise.all([ + user.getUserFields(uid, ['uid', 'reputation', 'postcount']), + groups.isMemberOfAny(uid, meta.config.groupsExemptFromPostQueue), + isCategoryQueueEnabled(data), + ]); + + const shouldQueue = meta.config.postQueue && categoryQueueEnabled && + !isMemberOfExempt && + (!userData.uid || userData.reputation < meta.config.postQueueReputationThreshold || + userData.postcount <= 0); + const result = await plugins.hooks.fire('filter:post.shouldQueue', { + shouldQueue: !!shouldQueue, + uid: uid, + data: data, + }); + return result.shouldQueue; + }; + + async function isCategoryQueueEnabled(data) { + const type = getType(data); + const cid = await getCid(type, data); + if (!cid) { + throw new Error('[[error:invalid-cid]]'); + } + return await categories.getCategoryField(cid, 'postQueue'); + } + + function getType(data) { + if (data.hasOwnProperty('tid')) { + return 'reply'; + } else if (data.hasOwnProperty('cid')) { + return 'topic'; + } + throw new Error('[[error:invalid-type]]'); + } + + async function removeQueueNotification(id) { + await notifications.rescind(`post-queue-${id}`); + const data = await getParsedObject(id); + if (!data) { + return; + } + const cid = await getCid(data.type, data); + const uids = await getNotificationUids(cid); + uids.forEach(uid => user.notifications.pushCount(uid)); + } + + async function getNotificationUids(cid) { + const results = await Promise.all([ + groups.getMembersOfGroups(['administrators', 'Global Moderators']), + categories.getModeratorUids([cid]), + ]); + return _.uniq(_.flattenDeep(results)); + } + + Posts.addToQueue = async function (data) { + const type = getType(data); + const now = Date.now(); + const id = `${type}-${now}`; + await canPost(type, data); + + let payload = { + id: id, + uid: data.uid, + type: type, + data: data, + }; + payload = await plugins.hooks.fire('filter:post-queue.save', payload); + payload.data = JSON.stringify(data); + + await db.sortedSetAdd('post:queue', now, id); + await db.setObject(`post:queue:${id}`, payload); + await user.setUserField(data.uid, 'lastqueuetime', now); + cache.del('post-queue'); + + const cid = await getCid(type, data); + const uids = await getNotificationUids(cid); + const bodyLong = await parseBodyLong(cid, type, data); + + const notifObj = await notifications.create({ + type: 'post-queue', + nid: `post-queue-${id}`, + mergeId: 'post-queue', + bodyShort: '[[notifications:post_awaiting_review]]', + bodyLong: bodyLong, + path: `/post-queue/${id}`, + }); + await notifications.push(notifObj, uids); + return { + id: id, + type: type, + queued: true, + message: '[[success:post-queued]]', + }; + }; + + async function parseBodyLong(cid, type, data) { + const url = nconf.get('url'); + const [content, category, userData] = await Promise.all([ + plugins.hooks.fire('filter:parse.raw', data.content), + categories.getCategoryFields(cid, ['name', 'slug']), + user.getUserFields(data.uid, ['uid', 'username']), + ]); + + category.url = `${url}/category/${category.slug}`; + if (userData.uid > 0) { + userData.url = `${url}/uid/${userData.uid}`; + } + + const topic = { cid: cid, title: data.title, tid: data.tid }; + if (type === 'reply') { + topic.title = await topics.getTopicField(data.tid, 'title'); + topic.url = `${url}/topic/${data.tid}`; + } + const { app } = require('../webserver'); + return await app.renderAsync('emails/partials/post-queue-body', { + content: content, + category: category, + user: userData, + topic: topic, + }); + } + + async function getCid(type, data) { + if (type === 'topic') { + return data.cid; + } else if (type === 'reply') { + return await topics.getTopicField(data.tid, 'cid'); + } + return null; + } + + async function canPost(type, data) { + const cid = await getCid(type, data); + const typeToPrivilege = { + topic: 'topics:create', + reply: 'topics:reply', + }; + + topics.checkContent(data.content); + if (type === 'topic') { + topics.checkTitle(data.title); + if (data.tags) { + await topics.validateTags(data.tags, cid, data.uid); + } + } + + const [canPost] = await Promise.all([ + privileges.categories.can(typeToPrivilege[type], cid, data.uid), + user.isReadyToQueue(data.uid, cid), + ]); + if (!canPost) { + throw new Error('[[error:no-privileges]]'); + } + } + + Posts.removeFromQueue = async function (id) { + const data = await getParsedObject(id); + if (!data) { + return null; + } + const result = await plugins.hooks.fire('filter:post-queue:removeFromQueue', { data: data }); + await removeFromQueue(id); + plugins.hooks.fire('action:post-queue:removeFromQueue', { data: result.data }); + return result.data; + }; + + async function removeFromQueue(id) { + await removeQueueNotification(id); + await db.sortedSetRemove('post:queue', id); + await db.delete(`post:queue:${id}`); + cache.del('post-queue'); + } + + Posts.submitFromQueue = async function (id) { + let data = await getParsedObject(id); + if (!data) { + return null; + } + const result = await plugins.hooks.fire('filter:post-queue:submitFromQueue', { data: data }); + data = result.data; + if (data.type === 'topic') { + const result = await createTopic(data.data); + data.pid = result.postData.pid; + } else if (data.type === 'reply') { + const result = await createReply(data.data); + data.pid = result.pid; + } + await removeFromQueue(id); + plugins.hooks.fire('action:post-queue:submitFromQueue', { data: data }); + return data; + }; + + Posts.getFromQueue = async function (id) { + return await getParsedObject(id); + }; + + async function getParsedObject(id) { + const data = await db.getObject(`post:queue:${id}`); + if (!data) { + return null; + } + data.data = JSON.parse(data.data); + data.data.fromQueue = true; + return data; + } + + async function createTopic(data) { + const result = await topics.post(data); + socketHelpers.notifyNew(data.uid, 'newTopic', { posts: [result.postData], topic: result.topicData }); + return result; + } + + async function createReply(data) { + const postData = await topics.reply(data); + const result = { + posts: [postData], + 'reputation:disabled': !!meta.config['reputation:disabled'], + 'downvote:disabled': !!meta.config['downvote:disabled'], + }; + socketHelpers.notifyNew(data.uid, 'newPost', result); + return postData; + } + + Posts.editQueuedContent = async function (uid, editData) { + const canEditQueue = await Posts.canEditQueue(uid, editData, 'edit'); + if (!canEditQueue) { + throw new Error('[[error:no-privileges]]'); + } + const data = await getParsedObject(editData.id); + if (!data) { + return; + } + if (editData.content !== undefined) { + data.data.content = editData.content; + } + if (editData.title !== undefined) { + data.data.title = editData.title; + } + if (editData.cid !== undefined) { + data.data.cid = editData.cid; + } + await db.setObjectField(`post:queue:${editData.id}`, 'data', JSON.stringify(data.data)); + cache.del('post-queue'); + }; + + Posts.canEditQueue = async function (uid, editData, action) { + const [isAdminOrGlobalMod, data] = await Promise.all([ + user.isAdminOrGlobalMod(uid), + getParsedObject(editData.id), + ]); + if (!data) { + return false; + } + const selfPost = parseInt(uid, 10) === parseInt(data.uid, 10); + if (isAdminOrGlobalMod || ((action === 'reject' || action === 'edit') && selfPost)) { + return true; + } + + let cid; + if (data.type === 'topic') { + cid = data.data.cid; + } else if (data.type === 'reply') { + cid = await topics.getTopicField(data.data.tid, 'cid'); + } + const isModerator = await user.isModerator(uid, cid); + let isModeratorOfTargetCid = true; + if (editData.cid) { + isModeratorOfTargetCid = await user.isModerator(uid, editData.cid); + } + return isModerator && isModeratorOfTargetCid; + }; + + Posts.updateQueuedPostsTopic = async function (newTid, tids) { + const postData = await Posts.getQueuedPosts({ tid: tids }, { metadata: false }); + if (postData.length) { + postData.forEach((post) => { + post.data.tid = newTid; + }); + await db.setObjectBulk( + postData.map(p => [`post:queue:${p.id}`, { data: JSON.stringify(p.data) }]), + ); + cache.del('post-queue'); + } + }; +}; diff --git a/src/posts/recent.js b/src/posts/recent.js new file mode 100644 index 0000000000..d08102aa66 --- /dev/null +++ b/src/posts/recent.js @@ -0,0 +1,33 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const privileges = require('../privileges'); + + +module.exports = function (Posts) { + const terms = { + day: 86400000, + week: 604800000, + month: 2592000000, + }; + + Posts.getRecentPosts = async function (uid, start, stop, term) { + let min = 0; + if (terms[term]) { + min = Date.now() - terms[term]; + } + + const count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; + let pids = await db.getSortedSetRevRangeByScore('posts:pid', start, count, '+inf', min); + pids = await privileges.posts.filter('topics:read', pids, uid); + return await Posts.getPostSummaryByPids(pids, uid, { stripTags: true }); + }; + + Posts.getRecentPosterUids = async function (start, stop) { + const pids = await db.getSortedSetRevRange('posts:pid', start, stop); + const postData = await Posts.getPostsFields(pids, ['uid']); + return _.uniq(postData.map(p => p && p.uid).filter(uid => parseInt(uid, 10))); + }; +}; diff --git a/src/posts/summary.js b/src/posts/summary.js new file mode 100644 index 0000000000..65878653a8 --- /dev/null +++ b/src/posts/summary.js @@ -0,0 +1,105 @@ + +'use strict'; + +const validator = require('validator'); +const _ = require('lodash'); + +const topics = require('../topics'); +const user = require('../user'); +const plugins = require('../plugins'); +const categories = require('../categories'); +const utils = require('../utils'); + +module.exports = function (Posts) { + Posts.getPostSummaryByPids = async function (pids, uid, options) { + if (!Array.isArray(pids) || !pids.length) { + return []; + } + + options.stripTags = options.hasOwnProperty('stripTags') ? options.stripTags : false; + options.parse = options.hasOwnProperty('parse') ? options.parse : true; + options.extraFields = options.hasOwnProperty('extraFields') ? options.extraFields : []; + + const fields = ['pid', 'tid', 'content', 'uid', 'timestamp', 'deleted', 'upvotes', 'downvotes', 'replies', 'handle'].concat(options.extraFields); + + let posts = await Posts.getPostsFields(pids, fields); + posts = posts.filter(Boolean); + posts = await user.blocks.filter(uid, posts); + + const uids = _.uniq(posts.map(p => p && p.uid)); + const tids = _.uniq(posts.map(p => p && p.tid)); + + const [users, topicsAndCategories] = await Promise.all([ + user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status']), + getTopicAndCategories(tids), + ]); + + const uidToUser = toObject('uid', users); + const tidToTopic = toObject('tid', topicsAndCategories.topics); + const cidToCategory = toObject('cid', topicsAndCategories.categories); + + posts.forEach((post) => { + // If the post author isn't represented in the retrieved users' data, + // then it means they were deleted, assume guest. + if (!uidToUser.hasOwnProperty(post.uid)) { + post.uid = 0; + } + post.user = uidToUser[post.uid]; + Posts.overrideGuestHandle(post, post.handle); + post.handle = undefined; + post.topic = tidToTopic[post.tid]; + post.category = post.topic && cidToCategory[post.topic.cid]; + post.isMainPost = post.topic && post.pid === post.topic.mainPid; + post.deleted = post.deleted === 1; + post.timestampISO = utils.toISOString(post.timestamp); + }); + + posts = posts.filter(post => tidToTopic[post.tid]); + + posts = await parsePosts(posts, options); + const result = await plugins.hooks.fire('filter:post.getPostSummaryByPids', { posts: posts, uid: uid }); + return result.posts; + }; + + async function parsePosts(posts, options) { + return await Promise.all(posts.map(async (post) => { + if (!post.content || !options.parse) { + post.content = post.content ? validator.escape(String(post.content)) : post.content; + return post; + } + post = await Posts.parsePost(post); + if (options.stripTags) { + post.content = stripTags(post.content); + } + return post; + })); + } + + async function getTopicAndCategories(tids) { + const topicsData = await topics.getTopicsFields(tids, [ + 'uid', 'tid', 'title', 'cid', 'tags', 'slug', + 'deleted', 'scheduled', 'postcount', 'mainPid', 'teaserPid', + ]); + const cids = _.uniq(topicsData.map(topic => topic && topic.cid)); + const categoriesData = await categories.getCategoriesFields(cids, [ + 'cid', 'name', 'icon', 'slug', 'parentCid', + 'bgColor', 'color', 'backgroundImage', 'imageClass', + ]); + return { topics: topicsData, categories: categoriesData }; + } + + function toObject(key, data) { + const obj = {}; + for (let i = 0; i < data.length; i += 1) { + obj[data[i][key]] = data[i]; + } + return obj; + } + + function stripTags(content) { + if (content) { + return utils.stripHTMLTags(content, utils.stripTags); + } + return content; + } +}; diff --git a/src/posts/tools.js b/src/posts/tools.js new file mode 100644 index 0000000000..60367d4839 --- /dev/null +++ b/src/posts/tools.js @@ -0,0 +1,44 @@ +'use strict'; + +const privileges = require('../privileges'); + +module.exports = function (Posts) { + Posts.tools = {}; + + Posts.tools.delete = async function (uid, pid) { + return await togglePostDelete(uid, pid, true); + }; + + Posts.tools.restore = async function (uid, pid) { + return await togglePostDelete(uid, pid, false); + }; + + async function togglePostDelete(uid, pid, isDelete) { + const [postData, canDelete] = await Promise.all([ + Posts.getPostData(pid), + privileges.posts.canDelete(pid, uid), + ]); + if (!postData) { + throw new Error('[[error:no-post]]'); + } + + if (postData.deleted && isDelete) { + throw new Error('[[error:post-already-deleted]]'); + } else if (!postData.deleted && !isDelete) { + throw new Error('[[error:post-already-restored]]'); + } + + if (!canDelete.flag) { + throw new Error(canDelete.message); + } + let post; + if (isDelete) { + require('./cache').del(pid); + post = await Posts.delete(pid, uid); + } else { + post = await Posts.restore(pid, uid); + post = await Posts.parsePost(post); + } + return post; + } +}; diff --git a/src/posts/topics.js b/src/posts/topics.js new file mode 100644 index 0000000000..64f5667fce --- /dev/null +++ b/src/posts/topics.js @@ -0,0 +1,54 @@ + +'use strict'; + +const topics = require('../topics'); +const user = require('../user'); +const utils = require('../utils'); + +module.exports = function (Posts) { + Posts.getPostsFromSet = async function (set, start, stop, uid, reverse) { + const pids = await Posts.getPidsFromSet(set, start, stop, reverse); + const posts = await Posts.getPostsByPids(pids, uid); + return await user.blocks.filter(uid, posts); + }; + + Posts.isMain = async function (pids) { + const isArray = Array.isArray(pids); + pids = isArray ? pids : [pids]; + const postData = await Posts.getPostsFields(pids, ['tid']); + const topicData = await topics.getTopicsFields(postData.map(t => t.tid), ['mainPid']); + const result = pids.map((pid, i) => parseInt(pid, 10) === parseInt(topicData[i].mainPid, 10)); + return isArray ? result : result[0]; + }; + + Posts.getTopicFields = async function (pid, fields) { + const tid = await Posts.getPostField(pid, 'tid'); + return await topics.getTopicFields(tid, fields); + }; + + Posts.generatePostPath = async function (pid, uid) { + const paths = await Posts.generatePostPaths([pid], uid); + return Array.isArray(paths) && paths.length ? paths[0] : null; + }; + + Posts.generatePostPaths = async function (pids, uid) { + const postData = await Posts.getPostsFields(pids, ['pid', 'tid']); + const tids = postData.map(post => post && post.tid); + const [indices, topicData] = await Promise.all([ + Posts.getPostIndices(postData, uid), + topics.getTopicsFields(tids, ['slug']), + ]); + + const paths = pids.map((pid, index) => { + const slug = topicData[index] ? topicData[index].slug : null; + const postIndex = utils.isNumber(indices[index]) ? parseInt(indices[index], 10) + 1 : null; + + if (slug && postIndex) { + return `/topic/${slug}/${postIndex}`; + } + return null; + }); + + return paths; + }; +}; diff --git a/src/posts/uploads.js b/src/posts/uploads.js new file mode 100644 index 0000000000..c0287bb93f --- /dev/null +++ b/src/posts/uploads.js @@ -0,0 +1,231 @@ +'use strict'; + +const nconf = require('nconf'); +const fs = require('fs').promises; +const crypto = require('crypto'); +const path = require('path'); +const winston = require('winston'); +const mime = require('mime'); +const validator = require('validator'); +const cronJob = require('cron').CronJob; +const chalk = require('chalk'); + +const db = require('../database'); +const image = require('../image'); +const user = require('../user'); +const topics = require('../topics'); +const file = require('../file'); +const meta = require('../meta'); + +module.exports = function (Posts) { + Posts.uploads = {}; + + const md5 = filename => crypto.createHash('md5').update(filename).digest('hex'); + const pathPrefix = path.join(nconf.get('upload_path')); + const searchRegex = /\/assets\/uploads\/(files\/[^\s")]+\.?[\w]*)/g; + + const _getFullPath = relativePath => path.join(pathPrefix, relativePath); + const _filterValidPaths = async filePaths => (await Promise.all(filePaths.map(async (filePath) => { + const fullPath = _getFullPath(filePath); + return fullPath.startsWith(pathPrefix) && await file.exists(fullPath) ? filePath : false; + }))).filter(Boolean); + + const runJobs = nconf.get('runJobs'); + if (runJobs) { + new cronJob('0 2 * * 0', async () => { + const orphans = await Posts.uploads.cleanOrphans(); + if (orphans.length) { + winston.info(`[posts/uploads] Deleting ${orphans.length} orphaned uploads...`); + orphans.forEach((relPath) => { + process.stdout.write(`${chalk.red(' - ')} ${relPath}`); + }); + } + }, null, true); + } + + Posts.uploads.sync = async function (pid) { + // Scans a post's content and updates sorted set of uploads + + const [content, currentUploads, isMainPost] = await Promise.all([ + Posts.getPostField(pid, 'content'), + Posts.uploads.list(pid), + Posts.isMain(pid), + ]); + + // Extract upload file paths from post content + let match = searchRegex.exec(content); + const uploads = []; + while (match) { + uploads.push(match[1].replace('-resized', '')); + match = searchRegex.exec(content); + } + + // Main posts can contain topic thumbs, which are also tracked by pid + if (isMainPost) { + const tid = await Posts.getPostField(pid, 'tid'); + let thumbs = await topics.thumbs.get(tid); + const replacePath = path.posix.join(`${nconf.get('relative_path')}${nconf.get('upload_url')}/`); + thumbs = thumbs.map(thumb => thumb.url.replace(replacePath, '')).filter(path => !validator.isURL(path, { + require_protocol: true, + })); + uploads.push(...thumbs); + } + + // Create add/remove sets + const add = uploads.filter(path => !currentUploads.includes(path)); + const remove = currentUploads.filter(path => !uploads.includes(path)); + await Promise.all([ + Posts.uploads.associate(pid, add), + Posts.uploads.dissociate(pid, remove), + ]); + }; + + Posts.uploads.list = async function (pid) { + return await db.getSortedSetMembers(`post:${pid}:uploads`); + }; + + Posts.uploads.listWithSizes = async function (pid) { + const paths = await Posts.uploads.list(pid); + const sizes = await db.getObjects(paths.map(path => `upload:${md5(path)}`)) || []; + + return sizes.map((sizeObj, idx) => ({ + ...sizeObj, + name: paths[idx], + })); + }; + + Posts.uploads.getOrphans = async () => { + let files = await fs.readdir(_getFullPath('/files')); + files = files.filter(filename => filename !== '.gitignore'); + + // Exclude non-timestamped files (e.g. group covers; see gh#10783/gh#10705) + const tsPrefix = /^\d{13}-/; + files = files.filter(filename => tsPrefix.test(filename)); + + files = await Promise.all(files.map(async filename => (await Posts.uploads.isOrphan(`files/${filename}`) ? `files/${filename}` : null))); + files = files.filter(Boolean); + + return files; + }; + + Posts.uploads.cleanOrphans = async () => { + const now = Date.now(); + const expiration = now - (1000 * 60 * 60 * 24 * meta.config.orphanExpiryDays); + const days = meta.config.orphanExpiryDays; + if (!days) { + return []; + } + + let orphans = await Posts.uploads.getOrphans(); + + orphans = await Promise.all(orphans.map(async (relPath) => { + const { mtimeMs } = await fs.stat(_getFullPath(relPath)); + return mtimeMs < expiration ? relPath : null; + })); + orphans = orphans.filter(Boolean); + + // Note: no await. Deletion not guaranteed by method end. + orphans.forEach((relPath) => { + file.delete(_getFullPath(relPath)); + }); + + return orphans; + }; + + Posts.uploads.isOrphan = async function (filePath) { + const length = await db.sortedSetCard(`upload:${md5(filePath)}:pids`); + return length === 0; + }; + + Posts.uploads.getUsage = async function (filePaths) { + // Given an array of file names, determines which pids they are used in + if (!Array.isArray(filePaths)) { + filePaths = [filePaths]; + } + + const keys = filePaths.map(fileObj => `upload:${md5(fileObj.path.replace('-resized', ''))}:pids`); + return await Promise.all(keys.map(k => db.getSortedSetRange(k, 0, -1))); + }; + + Posts.uploads.associate = async function (pid, filePaths) { + // Adds an upload to a post's sorted set of uploads + filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; + if (!filePaths.length) { + return; + } + + // Only process files that exist and are within uploads directory + filePaths = await _filterValidPaths(filePaths); + + const now = Date.now(); + const scores = filePaths.map(() => now); + const bulkAdd = filePaths.map(path => [`upload:${md5(path)}:pids`, now, pid]); + await Promise.all([ + db.sortedSetAdd(`post:${pid}:uploads`, scores, filePaths), + db.sortedSetAddBulk(bulkAdd), + Posts.uploads.saveSize(filePaths), + ]); + }; + + Posts.uploads.dissociate = async function (pid, filePaths) { + // Removes an upload from a post's sorted set of uploads + filePaths = !Array.isArray(filePaths) ? [filePaths] : filePaths; + if (!filePaths.length) { + return; + } + + const bulkRemove = filePaths.map(path => [`upload:${md5(path)}:pids`, pid]); + const promises = [ + db.sortedSetRemove(`post:${pid}:uploads`, filePaths), + db.sortedSetRemoveBulk(bulkRemove), + ]; + + await Promise.all(promises); + + if (!meta.config.preserveOrphanedUploads) { + const deletePaths = (await Promise.all( + filePaths.map(async filePath => (await Posts.uploads.isOrphan(filePath) ? filePath : false)) + )).filter(Boolean); + + const uploaderUids = (await db.getObjectsFields(deletePaths.map(path => `upload:${md5(path)}`, ['uid']))).map(o => (o ? o.uid || null : null)); + await Promise.all(uploaderUids.map((uid, idx) => ( + uid && isFinite(uid) ? user.deleteUpload(uid, uid, deletePaths[idx]) : null + )).filter(Boolean)); + await Posts.uploads.deleteFromDisk(deletePaths); + } + }; + + Posts.uploads.dissociateAll = async (pid) => { + const current = await Posts.uploads.list(pid); + await Posts.uploads.dissociate(pid, current); + }; + + Posts.uploads.deleteFromDisk = async (filePaths) => { + if (typeof filePaths === 'string') { + filePaths = [filePaths]; + } else if (!Array.isArray(filePaths)) { + throw new Error(`[[error:wrong-parameter-type, filePaths, ${typeof filePaths}, array]]`); + } + + filePaths = (await _filterValidPaths(filePaths)).map(_getFullPath); + await Promise.all(filePaths.map(file.delete)); + }; + + Posts.uploads.saveSize = async (filePaths) => { + filePaths = filePaths.filter((fileName) => { + const type = mime.getType(fileName); + return type && type.match(/image./); + }); + await Promise.all(filePaths.map(async (fileName) => { + try { + const size = await image.size(_getFullPath(fileName)); + await db.setObject(`upload:${md5(fileName)}`, { + width: size.width, + height: size.height, + }); + } catch (err) { + winston.error(`[posts/uploads] Error while saving post upload sizes (${fileName}): ${err.message}`); + } + })); + }; +}; diff --git a/src/posts/user.js b/src/posts/user.js new file mode 100644 index 0000000000..0675524c5d --- /dev/null +++ b/src/posts/user.js @@ -0,0 +1,261 @@ +'use strict'; + +const async = require('async'); +const validator = require('validator'); +const _ = require('lodash'); + +const db = require('../database'); +const user = require('../user'); +const topics = require('../topics'); +const groups = require('../groups'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); + +module.exports = function (Posts) { + Posts.getUserInfoForPosts = async function (uids, uid) { + const [userData, userSettings, signatureUids] = await Promise.all([ + getUserData(uids, uid), + user.getMultipleUserSettings(uids), + privileges.global.filterUids('signature', uids), + ]); + const uidsSignatureSet = new Set(signatureUids.map(uid => parseInt(uid, 10))); + const groupsMap = await getGroupsMap(userData); + + userData.forEach((userData, index) => { + userData.signature = validator.escape(String(userData.signature || '')); + userData.fullname = userSettings[index].showfullname ? validator.escape(String(userData.fullname || '')) : undefined; + userData.selectedGroups = []; + + if (meta.config.hideFullname) { + userData.fullname = undefined; + } + }); + + const result = await Promise.all(userData.map(async (userData) => { + const [isMemberOfGroups, signature, customProfileInfo] = await Promise.all([ + checkGroupMembership(userData.uid, userData.groupTitleArray), + parseSignature(userData, uid, uidsSignatureSet), + plugins.hooks.fire('filter:posts.custom_profile_info', { profile: [], uid: userData.uid }), + ]); + + if (isMemberOfGroups && userData.groupTitleArray) { + userData.groupTitleArray.forEach((userGroup, index) => { + if (isMemberOfGroups[index] && groupsMap[userGroup]) { + userData.selectedGroups.push(groupsMap[userGroup]); + } + }); + } + userData.signature = signature; + userData.custom_profile_info = customProfileInfo.profile; + + return await plugins.hooks.fire('filter:posts.modifyUserInfo', userData); + })); + const hookResult = await plugins.hooks.fire('filter:posts.getUserInfoForPosts', { users: result }); + return hookResult.users; + }; + + Posts.overrideGuestHandle = function (postData, handle) { + if (meta.config.allowGuestHandles && postData && postData.user && parseInt(postData.uid, 10) === 0 && handle) { + postData.user.username = validator.escape(String(handle)); + if (postData.user.hasOwnProperty('fullname')) { + postData.user.fullname = postData.user.username; + } + postData.user.displayname = postData.user.username; + } + }; + + async function checkGroupMembership(uid, groupTitleArray) { + if (!Array.isArray(groupTitleArray) || !groupTitleArray.length) { + return null; + } + return await groups.isMemberOfGroups(uid, groupTitleArray); + } + + async function parseSignature(userData, uid, signatureUids) { + if (!userData.signature || !signatureUids.has(userData.uid) || meta.config.disableSignatures) { + return ''; + } + const result = await Posts.parseSignature(userData, uid); + return result.userData.signature; + } + + async function getGroupsMap(userData) { + const groupTitles = _.uniq(_.flatten(userData.map(u => u && u.groupTitleArray))); + const groupsMap = {}; + const groupsData = await groups.getGroupsData(groupTitles); + groupsData.forEach((group) => { + if (group && group.userTitleEnabled && !group.hidden) { + groupsMap[group.name] = { + name: group.name, + slug: group.slug, + labelColor: group.labelColor, + textColor: group.textColor, + icon: group.icon, + userTitle: group.userTitle, + }; + } + }); + return groupsMap; + } + + async function getUserData(uids, uid) { + const fields = [ + 'uid', 'username', 'fullname', 'userslug', + 'reputation', 'postcount', 'topiccount', 'picture', + 'signature', 'banned', 'banned:expire', 'status', + 'lastonline', 'groupTitle', 'mutedUntil', + ]; + const result = await plugins.hooks.fire('filter:posts.addUserFields', { + fields: fields, + uid: uid, + uids: uids, + }); + return await user.getUsersFields(result.uids, _.uniq(result.fields)); + } + + Posts.isOwner = async function (pids, uid) { + uid = parseInt(uid, 10); + const isArray = Array.isArray(pids); + pids = isArray ? pids : [pids]; + if (uid <= 0) { + return isArray ? pids.map(() => false) : false; + } + const postData = await Posts.getPostsFields(pids, ['uid']); + const result = postData.map(post => post && post.uid === uid); + return isArray ? result : result[0]; + }; + + Posts.isModerator = async function (pids, uid) { + if (parseInt(uid, 10) <= 0) { + return pids.map(() => false); + } + const cids = await Posts.getCidsByPids(pids); + return await user.isModerator(uid, cids); + }; + + Posts.changeOwner = async function (pids, toUid) { + const exists = await user.exists(toUid); + if (!exists) { + throw new Error('[[error:no-user]]'); + } + let postData = await Posts.getPostsFields(pids, [ + 'pid', 'tid', 'uid', 'content', 'deleted', 'timestamp', 'upvotes', 'downvotes', + ]); + postData = postData.filter(p => p.pid && p.uid !== parseInt(toUid, 10)); + pids = postData.map(p => p.pid); + + const cids = await Posts.getCidsByPids(pids); + + const bulkRemove = []; + const bulkAdd = []; + let repChange = 0; + const postsByUser = {}; + postData.forEach((post, i) => { + post.cid = cids[i]; + repChange += post.votes; + bulkRemove.push([`uid:${post.uid}:posts`, post.pid]); + bulkRemove.push([`cid:${post.cid}:uid:${post.uid}:pids`, post.pid]); + bulkRemove.push([`cid:${post.cid}:uid:${post.uid}:pids:votes`, post.pid]); + + bulkAdd.push([`uid:${toUid}:posts`, post.timestamp, post.pid]); + bulkAdd.push([`cid:${post.cid}:uid:${toUid}:pids`, post.timestamp, post.pid]); + if (post.votes > 0 || post.votes < 0) { + bulkAdd.push([`cid:${post.cid}:uid:${toUid}:pids:votes`, post.votes, post.pid]); + } + postsByUser[post.uid] = postsByUser[post.uid] || []; + postsByUser[post.uid].push(post); + }); + + await Promise.all([ + db.setObjectField(pids.map(pid => `post:${pid}`), 'uid', toUid), + db.sortedSetRemoveBulk(bulkRemove), + db.sortedSetAddBulk(bulkAdd), + user.incrementUserReputationBy(toUid, repChange), + handleMainPidOwnerChange(postData, toUid), + updateTopicPosters(postData, toUid), + ]); + + await Promise.all([ + user.updatePostCount(toUid), + reduceCounters(postsByUser), + ]); + + plugins.hooks.fire('action:post.changeOwner', { + posts: _.cloneDeep(postData), + toUid: toUid, + }); + return postData; + }; + + async function reduceCounters(postsByUser) { + await async.eachOfSeries(postsByUser, async (posts, uid) => { + const repChange = posts.reduce((acc, val) => acc + val.votes, 0); + await Promise.all([ + user.updatePostCount(uid), + user.incrementUserReputationBy(uid, -repChange), + ]); + }); + } + + async function updateTopicPosters(postData, toUid) { + const postsByTopic = _.groupBy(postData, p => parseInt(p.tid, 10)); + await async.eachOf(postsByTopic, async (posts, tid) => { + const postsByUser = _.groupBy(posts, p => parseInt(p.uid, 10)); + await db.sortedSetIncrBy(`tid:${tid}:posters`, posts.length, toUid); + await async.eachOf(postsByUser, async (posts, uid) => { + await db.sortedSetIncrBy(`tid:${tid}:posters`, -posts.length, uid); + }); + }); + } + + async function handleMainPidOwnerChange(postData, toUid) { + const tids = _.uniq(postData.map(p => p.tid)); + const topicData = await topics.getTopicsFields(tids, [ + 'tid', 'cid', 'deleted', 'title', 'uid', 'mainPid', 'timestamp', + ]); + const tidToTopic = _.zipObject(tids, topicData); + + const mainPosts = postData.filter(p => p.pid === tidToTopic[p.tid].mainPid); + if (!mainPosts.length) { + return; + } + + const bulkAdd = []; + const bulkRemove = []; + const postsByUser = {}; + mainPosts.forEach((post) => { + bulkRemove.push([`cid:${post.cid}:uid:${post.uid}:tids`, post.tid]); + bulkRemove.push([`uid:${post.uid}:topics`, post.tid]); + + bulkAdd.push([`cid:${post.cid}:uid:${toUid}:tids`, tidToTopic[post.tid].timestamp, post.tid]); + bulkAdd.push([`uid:${toUid}:topics`, tidToTopic[post.tid].timestamp, post.tid]); + postsByUser[post.uid] = postsByUser[post.uid] || []; + postsByUser[post.uid].push(post); + }); + + await Promise.all([ + db.setObjectField(mainPosts.map(p => `topic:${p.tid}`), 'uid', toUid), + db.sortedSetRemoveBulk(bulkRemove), + db.sortedSetAddBulk(bulkAdd), + user.incrementUserFieldBy(toUid, 'topiccount', mainPosts.length), + reduceTopicCounts(postsByUser), + ]); + + const changedTopics = mainPosts.map(p => tidToTopic[p.tid]); + plugins.hooks.fire('action:topic.changeOwner', { + topics: _.cloneDeep(changedTopics), + toUid: toUid, + }); + } + + async function reduceTopicCounts(postsByUser) { + await async.eachSeries(Object.keys(postsByUser), async (uid) => { + const posts = postsByUser[uid]; + const exists = await user.exists(uid); + if (exists) { + await user.incrementUserFieldBy(uid, 'topiccount', -posts.length); + } + }); + } +}; diff --git a/src/posts/votes.js b/src/posts/votes.js new file mode 100644 index 0000000000..2f812f3f4a --- /dev/null +++ b/src/posts/votes.js @@ -0,0 +1,293 @@ +'use strict'; + +const meta = require('../meta'); +const db = require('../database'); +const flags = require('../flags'); +const user = require('../user'); +const topics = require('../topics'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); +const translator = require('../translator'); + +module.exports = function (Posts) { + const votesInProgress = {}; + + Posts.upvote = async function (pid, uid) { + if (meta.config['reputation:disabled']) { + throw new Error('[[error:reputation-system-disabled]]'); + } + const canUpvote = await privileges.posts.can('posts:upvote', pid, uid); + if (!canUpvote) { + throw new Error('[[error:no-privileges]]'); + } + + if (voteInProgress(pid, uid)) { + throw new Error('[[error:already-voting-for-this-post]]'); + } + putVoteInProgress(pid, uid); + + try { + return await toggleVote('upvote', pid, uid); + } finally { + clearVoteProgress(pid, uid); + } + }; + + Posts.downvote = async function (pid, uid) { + if (meta.config['reputation:disabled']) { + throw new Error('[[error:reputation-system-disabled]]'); + } + + if (meta.config['downvote:disabled']) { + throw new Error('[[error:downvoting-disabled]]'); + } + const canDownvote = await privileges.posts.can('posts:downvote', pid, uid); + if (!canDownvote) { + throw new Error('[[error:no-privileges]]'); + } + + if (voteInProgress(pid, uid)) { + throw new Error('[[error:already-voting-for-this-post]]'); + } + + putVoteInProgress(pid, uid); + try { + return await toggleVote('downvote', pid, uid); + } finally { + clearVoteProgress(pid, uid); + } + }; + + Posts.unvote = async function (pid, uid) { + if (voteInProgress(pid, uid)) { + throw new Error('[[error:already-voting-for-this-post]]'); + } + + putVoteInProgress(pid, uid); + try { + const voteStatus = await Posts.hasVoted(pid, uid); + return await unvote(pid, uid, 'unvote', voteStatus); + } finally { + clearVoteProgress(pid, uid); + } + }; + + Posts.hasVoted = async function (pid, uid) { + if (parseInt(uid, 10) <= 0) { + return { upvoted: false, downvoted: false }; + } + const hasVoted = await db.isMemberOfSets([`pid:${pid}:upvote`, `pid:${pid}:downvote`], uid); + return { upvoted: hasVoted[0], downvoted: hasVoted[1] }; + }; + + Posts.getVoteStatusByPostIDs = async function (pids, uid) { + if (parseInt(uid, 10) <= 0) { + const data = pids.map(() => false); + return { upvotes: data, downvotes: data }; + } + const upvoteSets = pids.map(pid => `pid:${pid}:upvote`); + const downvoteSets = pids.map(pid => `pid:${pid}:downvote`); + const data = await db.isMemberOfSets(upvoteSets.concat(downvoteSets), uid); + return { + upvotes: data.slice(0, pids.length), + downvotes: data.slice(pids.length, pids.length * 2), + }; + }; + + Posts.getUpvotedUidsByPids = async function (pids) { + return await db.getSetsMembers(pids.map(pid => `pid:${pid}:upvote`)); + }; + + function voteInProgress(pid, uid) { + return Array.isArray(votesInProgress[uid]) && votesInProgress[uid].includes(parseInt(pid, 10)); + } + + function putVoteInProgress(pid, uid) { + votesInProgress[uid] = votesInProgress[uid] || []; + votesInProgress[uid].push(parseInt(pid, 10)); + } + + function clearVoteProgress(pid, uid) { + if (Array.isArray(votesInProgress[uid])) { + const index = votesInProgress[uid].indexOf(parseInt(pid, 10)); + if (index !== -1) { + votesInProgress[uid].splice(index, 1); + } + } + } + + async function toggleVote(type, pid, uid) { + const voteStatus = await Posts.hasVoted(pid, uid); + await unvote(pid, uid, type, voteStatus); + return await vote(type, false, pid, uid, voteStatus); + } + + async function unvote(pid, uid, type, voteStatus) { + const owner = await Posts.getPostField(pid, 'uid'); + if (parseInt(uid, 10) === parseInt(owner, 10)) { + throw new Error('[[error:self-vote]]'); + } + + if (type === 'downvote' || type === 'upvote') { + await checkVoteLimitation(pid, uid, type); + } + + if (!voteStatus || (!voteStatus.upvoted && !voteStatus.downvoted)) { + return; + } + + return await vote(voteStatus.upvoted ? 'downvote' : 'upvote', true, pid, uid, voteStatus); + } + + async function checkVoteLimitation(pid, uid, type) { + // type = 'upvote' or 'downvote' + const oneDay = 86400000; + const [reputation, targetUid, votedPidsToday] = await Promise.all([ + user.getUserField(uid, 'reputation'), + Posts.getPostField(pid, 'uid'), + db.getSortedSetRevRangeByScore( + `uid:${uid}:${type}`, 0, -1, '+inf', Date.now() - oneDay + ), + ]); + + if (reputation < meta.config[`min:rep:${type}`]) { + throw new Error(`[[error:not-enough-reputation-to-${type}, ${meta.config[`min:rep:${type}`]}]]`); + } + const votesToday = meta.config[`${type}sPerDay`]; + if (votesToday && votedPidsToday.length >= votesToday) { + throw new Error(`[[error:too-many-${type}s-today, ${votesToday}]]`); + } + const voterPerUserToday = meta.config[`${type}sPerUserPerDay`]; + if (voterPerUserToday) { + const postData = await Posts.getPostsFields(votedPidsToday, ['uid']); + const targetUpVotes = postData.filter(p => p.uid === targetUid).length; + if (targetUpVotes >= voterPerUserToday) { + throw new Error(`[[error:too-many-${type}s-today-user, ${voterPerUserToday}]]`); + } + } + } + + async function vote(type, unvote, pid, uid, voteStatus) { + uid = parseInt(uid, 10); + if (uid <= 0) { + throw new Error('[[error:not-logged-in]]'); + } + const now = Date.now(); + + if (type === 'upvote' && !unvote) { + await db.sortedSetAdd(`uid:${uid}:upvote`, now, pid); + } else { + await db.sortedSetRemove(`uid:${uid}:upvote`, pid); + } + + if (type === 'upvote' || unvote) { + await db.sortedSetRemove(`uid:${uid}:downvote`, pid); + } else { + await db.sortedSetAdd(`uid:${uid}:downvote`, now, pid); + } + + const postData = await Posts.getPostFields(pid, ['pid', 'uid', 'tid']); + const newReputation = await user.incrementUserReputationBy(postData.uid, type === 'upvote' ? 1 : -1); + + await adjustPostVotes(postData, uid, type, unvote); + + await fireVoteHook(postData, uid, type, unvote, voteStatus); + + return { + user: { + reputation: newReputation, + }, + fromuid: uid, + post: postData, + upvote: type === 'upvote' && !unvote, + downvote: type === 'downvote' && !unvote, + }; + } + + async function fireVoteHook(postData, uid, type, unvote, voteStatus) { + let hook = type; + let current = voteStatus.upvoted ? 'upvote' : 'downvote'; + if (unvote) { // e.g. unvoting, removing a upvote or downvote + hook = 'unvote'; + } else { // e.g. User *has not* voted, clicks upvote or downvote + current = 'unvote'; + } + // action:post.upvote + // action:post.downvote + // action:post.unvote + plugins.hooks.fire(`action:post.${hook}`, { + pid: postData.pid, + uid: uid, + owner: postData.uid, + current: current, + }); + } + + async function adjustPostVotes(postData, uid, type, unvote) { + const notType = (type === 'upvote' ? 'downvote' : 'upvote'); + if (unvote) { + await db.setRemove(`pid:${postData.pid}:${type}`, uid); + } else { + await db.setAdd(`pid:${postData.pid}:${type}`, uid); + } + await db.setRemove(`pid:${postData.pid}:${notType}`, uid); + + const [upvotes, downvotes] = await Promise.all([ + db.setCount(`pid:${postData.pid}:upvote`), + db.setCount(`pid:${postData.pid}:downvote`), + ]); + postData.upvotes = upvotes; + postData.downvotes = downvotes; + postData.votes = postData.upvotes - postData.downvotes; + await Posts.updatePostVoteCount(postData); + } + + Posts.updatePostVoteCount = async function (postData) { + if (!postData || !postData.pid || !postData.tid) { + return; + } + const threshold = meta.config['flags:autoFlagOnDownvoteThreshold']; + if (threshold && postData.votes <= (-threshold)) { + const adminUid = await user.getFirstAdminUid(); + const reportMsg = await translator.translate(`[[flags:auto-flagged, ${-postData.votes}]]`); + const flagObj = await flags.create('post', postData.pid, adminUid, reportMsg, null, true); + await flags.notify(flagObj, adminUid, true); + } + await Promise.all([ + updateTopicVoteCount(postData), + db.sortedSetAdd('posts:votes', postData.votes, postData.pid), + Posts.setPostFields(postData.pid, { + upvotes: postData.upvotes, + downvotes: postData.downvotes, + }), + ]); + plugins.hooks.fire('action:post.updatePostVoteCount', { post: postData }); + }; + + async function updateTopicVoteCount(postData) { + const topicData = await topics.getTopicFields(postData.tid, ['mainPid', 'cid', 'pinned']); + + if (postData.uid) { + if (postData.votes !== 0) { + await db.sortedSetAdd(`cid:${topicData.cid}:uid:${postData.uid}:pids:votes`, postData.votes, postData.pid); + } else { + await db.sortedSetRemove(`cid:${topicData.cid}:uid:${postData.uid}:pids:votes`, postData.pid); + } + } + + if (parseInt(topicData.mainPid, 10) !== parseInt(postData.pid, 10)) { + return await db.sortedSetAdd(`tid:${postData.tid}:posts:votes`, postData.votes, postData.pid); + } + const promises = [ + topics.setTopicFields(postData.tid, { + upvotes: postData.upvotes, + downvotes: postData.downvotes, + }), + db.sortedSetAdd('topics:votes', postData.votes, postData.tid), + ]; + if (!topicData.pinned) { + promises.push(db.sortedSetAdd(`cid:${topicData.cid}:tids:votes`, postData.votes, postData.tid)); + } + await Promise.all(promises); + } +}; diff --git a/src/prestart.js b/src/prestart.js new file mode 100644 index 0000000000..f39ec7653c --- /dev/null +++ b/src/prestart.js @@ -0,0 +1,125 @@ +'use strict'; + +const nconf = require('nconf'); +const url = require('url'); +const winston = require('winston'); +const path = require('path'); +const chalk = require('chalk'); + +const pkg = require('../package.json'); +const { paths } = require('./constants'); + +function setupWinston() { + if (!winston.format) { + return; + } + + const formats = []; + if (nconf.get('log-colorize') !== 'false') { + formats.push(winston.format.colorize()); + } + + if (nconf.get('json-logging')) { + formats.push(winston.format.timestamp()); + formats.push(winston.format.json()); + } else { + const timestampFormat = winston.format((info) => { + const dateString = `${new Date().toISOString()} [${nconf.get('port')}/${global.process.pid}]`; + info.level = `${dateString} - ${info.level}`; + return info; + }); + formats.push(timestampFormat()); + formats.push(winston.format.splat()); + formats.push(winston.format.simple()); + } + + winston.configure({ + level: nconf.get('log-level') || (process.env.NODE_ENV === 'production' ? 'info' : 'verbose'), + format: winston.format.combine.apply(null, formats), + transports: [ + new winston.transports.Console({ + handleExceptions: true, + }), + ], + }); +} + +function loadConfig(configFile) { + nconf.file({ + file: configFile, + }); + + nconf.defaults({ + base_dir: paths.baseDir, + themes_path: paths.themes, + upload_path: 'public/uploads', + views_dir: path.join(paths.baseDir, 'build/public/templates'), + version: pkg.version, + isCluster: false, + isPrimary: true, + jobsDisabled: false, + }); + + // Explicitly cast as Bool, loader.js passes in isCluster as string 'true'/'false' + const castAsBool = ['isCluster', 'isPrimary', 'jobsDisabled']; + nconf.stores.env.readOnly = false; + castAsBool.forEach((prop) => { + const value = nconf.get(prop); + if (value !== undefined) { + nconf.set(prop, ['1', 1, 'true', true].includes(value)); + } + }); + nconf.stores.env.readOnly = true; + nconf.set('runJobs', nconf.get('isPrimary') && !nconf.get('jobsDisabled')); + + // Ensure themes_path is a full filepath + nconf.set('themes_path', path.resolve(paths.baseDir, nconf.get('themes_path'))); + nconf.set('core_templates_path', path.join(paths.baseDir, 'src/views')); + nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); + + nconf.set('upload_path', path.resolve(nconf.get('base_dir'), nconf.get('upload_path'))); + nconf.set('upload_url', '/assets/uploads'); + + + // nconf defaults, if not set in config + if (!nconf.get('sessionKey')) { + nconf.set('sessionKey', 'express.sid'); + } + + if (nconf.get('url')) { + nconf.set('url', nconf.get('url').replace(/\/$/, '')); + nconf.set('url_parsed', url.parse(nconf.get('url'))); + // Parse out the relative_url and other goodies from the configured URL + const urlObject = url.parse(nconf.get('url')); + const relativePath = urlObject.pathname !== '/' ? urlObject.pathname.replace(/\/+$/, '') : ''; + nconf.set('base_url', `${urlObject.protocol}//${urlObject.host}`); + nconf.set('secure', urlObject.protocol === 'https:'); + nconf.set('use_port', !!urlObject.port); + nconf.set('relative_path', relativePath); + if (!nconf.get('asset_base_url')) { + nconf.set('asset_base_url', `${relativePath}/assets`); + } + nconf.set('port', nconf.get('PORT') || nconf.get('port') || urlObject.port || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); + + // cookies don't provide isolation by port: http://stackoverflow.com/a/16328399/122353 + const domain = nconf.get('cookieDomain') || urlObject.hostname; + const origins = nconf.get('socket.io:origins') || `${urlObject.protocol}//${domain}:*`; + nconf.set('socket.io:origins', origins); + } +} + +function versionCheck() { + const version = process.version.slice(1); + const range = pkg.engines.node; + const semver = require('semver'); + const compatible = semver.satisfies(version, range); + + if (!compatible) { + winston.warn('Your version of Node.js is too outdated for NodeBB. Please update your version of Node.js.'); + winston.warn(`Recommended ${chalk.green(range)}, ${chalk.yellow(version)} provided\n`); + } +} + +exports.setupWinston = setupWinston; +exports.loadConfig = loadConfig; +exports.versionCheck = versionCheck; diff --git a/src/privileges/admin.js b/src/privileges/admin.js new file mode 100644 index 0000000000..4b778da112 --- /dev/null +++ b/src/privileges/admin.js @@ -0,0 +1,212 @@ + +'use strict'; + +const _ = require('lodash'); + +const user = require('../user'); +const groups = require('../groups'); +const helpers = require('./helpers'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const privsAdmin = module.exports; + +/** + * Looking to add a new admin privilege via plugin/theme? Attach a hook to + * `static:privileges.admin.init` and call .set() on the privilege map passed + * in to your listener. + */ +const _privilegeMap = new Map([ + ['admin:dashboard', { label: '[[admin/manage/privileges:admin-dashboard]]' }], + ['admin:categories', { label: '[[admin/manage/privileges:admin-categories]]' }], + ['admin:privileges', { label: '[[admin/manage/privileges:admin-privileges]]' }], + ['admin:admins-mods', { label: '[[admin/manage/privileges:admin-admins-mods]]' }], + ['admin:users', { label: '[[admin/manage/privileges:admin-users]]' }], + ['admin:groups', { label: '[[admin/manage/privileges:admin-groups]]' }], + ['admin:tags', { label: '[[admin/manage/privileges:admin-tags]]' }], + ['admin:settings', { label: '[[admin/manage/privileges:admin-settings]]' }], +]); + +privsAdmin.getUserPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.admin.list', Array.from(_privilegeMap.keys())); +privsAdmin.getGroupPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.admin.groups.list', Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`)); +privsAdmin.getPrivilegeList = async () => { + const [user, group] = await Promise.all([ + privsAdmin.getUserPrivilegeList(), + privsAdmin.getGroupPrivilegeList(), + ]); + return user.concat(group); +}; + +privsAdmin.init = async () => { + await plugins.hooks.fire('static:privileges.admin.init', { + privileges: _privilegeMap, + }); +}; + +// Mapping for a page route (via direct match or regexp) to a privilege +privsAdmin.routeMap = { + dashboard: 'admin:dashboard', + 'manage/categories': 'admin:categories', + 'manage/privileges': 'admin:privileges', + 'manage/admins-mods': 'admin:admins-mods', + 'manage/users': 'admin:users', + 'manage/groups': 'admin:groups', + 'manage/tags': 'admin:tags', + 'settings/tags': 'admin:tags', + 'extend/plugins': 'admin:settings', + 'extend/widgets': 'admin:settings', + 'extend/rewards': 'admin:settings', + // uploads + 'category/uploadpicture': 'admin:categories', + uploadfavicon: 'admin:settings', + uploadTouchIcon: 'admin:settings', + uploadMaskableIcon: 'admin:settings', + uploadlogo: 'admin:settings', + uploadOgImage: 'admin:settings', + uploadDefaultAvatar: 'admin:settings', +}; +privsAdmin.routePrefixMap = { + 'manage/categories/': 'admin:categories', + 'manage/privileges/': 'admin:privileges', + 'manage/groups/': 'admin:groups', + 'settings/': 'admin:settings', + 'appearance/': 'admin:settings', + 'plugins/': 'admin:settings', +}; + +// Mapping for socket call methods to a privilege +// In NodeBB v2, these socket calls will be removed in favour of xhr calls +privsAdmin.socketMap = { + 'admin.rooms.getAll': 'admin:dashboard', + 'admin.analytics.get': 'admin:dashboard', + + 'admin.categories.copySettingsFrom': 'admin:categories', + 'admin.categories.copyPrivilegesToChildren': 'admin:privileges', + 'admin.categories.copyPrivilegesFrom': 'admin:privileges', + 'admin.categories.copyPrivilegesToAllCategories': 'admin:privileges', + + 'admin.user.makeAdmins': 'admin:admins-mods', + 'admin.user.removeAdmins': 'admin:admins-mods', + + 'admin.user.loadGroups': 'admin:users', + 'admin.groups.join': 'admin:users', + 'admin.groups.leave': 'admin:users', + 'admin.user.resetLockouts': 'admin:users', + 'admin.user.validateEmail': 'admin:users', + 'admin.user.sendValidationEmail': 'admin:users', + 'admin.user.sendPasswordResetEmail': 'admin:users', + 'admin.user.forcePasswordReset': 'admin:users', + 'admin.user.invite': 'admin:users', + + 'admin.tags.create': 'admin:tags', + 'admin.tags.rename': 'admin:tags', + 'admin.tags.deleteTags': 'admin:tags', + + 'admin.getSearchDict': 'admin:settings', + 'admin.config.setMultiple': 'admin:settings', + 'admin.config.remove': 'admin:settings', + 'admin.themes.getInstalled': 'admin:settings', + 'admin.themes.set': 'admin:settings', + 'admin.reloadAllSessions': 'admin:settings', + 'admin.settings.get': 'admin:settings', + 'admin.settings.set': 'admin:settings', +}; + +privsAdmin.resolve = (path) => { + if (privsAdmin.routeMap.hasOwnProperty(path)) { + return privsAdmin.routeMap[path]; + } + + const found = Object.entries(privsAdmin.routePrefixMap) + .filter(entry => path.startsWith(entry[0])) + .sort((entry1, entry2) => entry2[0].length - entry1[0].length); + if (!found.length) { + return undefined; + } + return found[0][1]; // [0] is path [1] is privilege +}; + +privsAdmin.list = async function (uid) { + const privilegeLabels = Array.from(_privilegeMap.values()).map(data => data.label); + const userPrivilegeList = await privsAdmin.getUserPrivilegeList(); + const groupPrivilegeList = await privsAdmin.getGroupPrivilegeList(); + + // Restrict privileges column to superadmins + if (!(await user.isAdministrator(uid))) { + const idx = Array.from(_privilegeMap.keys()).indexOf('admin:privileges'); + privilegeLabels.splice(idx, 1); + userPrivilegeList.splice(idx, 1); + groupPrivilegeList.splice(idx, 1); + } + + const labels = await utils.promiseParallel({ + users: plugins.hooks.fire('filter:privileges.admin.list_human', privilegeLabels.slice()), + groups: plugins.hooks.fire('filter:privileges.admin.groups.list_human', privilegeLabels.slice()), + }); + + const keys = { + users: userPrivilegeList, + groups: groupPrivilegeList, + }; + + const payload = await utils.promiseParallel({ + labels, + users: helpers.getUserPrivileges(0, keys.users), + groups: helpers.getGroupPrivileges(0, keys.groups), + }); + payload.keys = keys; + + return payload; +}; + +privsAdmin.get = async function (uid) { + const userPrivilegeList = await privsAdmin.getUserPrivilegeList(); + const [userPrivileges, isAdministrator] = await Promise.all([ + helpers.isAllowedTo(userPrivilegeList, uid, 0), + user.isAdministrator(uid), + ]); + + const combined = userPrivileges.map(allowed => allowed || isAdministrator); + const privData = _.zipObject(userPrivilegeList, combined); + + privData.superadmin = isAdministrator; + return await plugins.hooks.fire('filter:privileges.admin.get', privData); +}; + +privsAdmin.can = async function (privilege, uid) { + const [isUserAllowedTo, isAdministrator] = await Promise.all([ + helpers.isAllowedTo(privilege, uid, [0]), + user.isAdministrator(uid), + ]); + return isAdministrator || isUserAllowedTo[0]; +}; + +privsAdmin.canGroup = async function (privilege, groupName) { + return await groups.isMember(groupName, `cid:0:privileges:groups:${privilege}`); +}; + +privsAdmin.give = async function (privileges, groupName) { + await helpers.giveOrRescind(groups.join, privileges, 0, groupName); + plugins.hooks.fire('action:privileges.admin.give', { + privileges: privileges, + groupNames: Array.isArray(groupName) ? groupName : [groupName], + }); +}; + +privsAdmin.rescind = async function (privileges, groupName) { + await helpers.giveOrRescind(groups.leave, privileges, 0, groupName); + plugins.hooks.fire('action:privileges.admin.rescind', { + privileges: privileges, + groupNames: Array.isArray(groupName) ? groupName : [groupName], + }); +}; + +privsAdmin.userPrivileges = async function (uid) { + const userPrivilegeList = await privsAdmin.getUserPrivilegeList(); + return await helpers.userOrGroupPrivileges(0, uid, userPrivilegeList); +}; + +privsAdmin.groupPrivileges = async function (groupName) { + const groupPrivilegeList = await privsAdmin.getGroupPrivilegeList(); + return await helpers.userOrGroupPrivileges(0, groupName, groupPrivilegeList); +}; diff --git a/src/privileges/categories.js b/src/privileges/categories.js new file mode 100644 index 0000000000..4f8b0e6581 --- /dev/null +++ b/src/privileges/categories.js @@ -0,0 +1,220 @@ + +'use strict'; + +const _ = require('lodash'); + +const categories = require('../categories'); +const user = require('../user'); +const groups = require('../groups'); +const helpers = require('./helpers'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const privsCategories = module.exports; + +/** + * Looking to add a new category privilege via plugin/theme? Attach a hook to + * `static:privileges.category.init` and call .set() on the privilege map passed + * in to your listener. + */ +const _privilegeMap = new Map([ + ['find', { label: '[[admin/manage/privileges:find-category]]' }], + ['read', { label: '[[admin/manage/privileges:access-category]]' }], + ['topics:read', { label: '[[admin/manage/privileges:access-topics]]' }], + ['topics:create', { label: '[[admin/manage/privileges:create-topics]]' }], + ['topics:reply', { label: '[[admin/manage/privileges:reply-to-topics]]' }], + ['topics:schedule', { label: '[[admin/manage/privileges:schedule-topics]]' }], + ['topics:tag', { label: '[[admin/manage/privileges:tag-topics]]' }], + ['posts:edit', { label: '[[admin/manage/privileges:edit-posts]]' }], + ['posts:history', { label: '[[admin/manage/privileges:view-edit-history]]' }], + ['posts:delete', { label: '[[admin/manage/privileges:delete-posts]]' }], + ['posts:upvote', { label: '[[admin/manage/privileges:upvote-posts]]' }], + ['posts:downvote', { label: '[[admin/manage/privileges:downvote-posts]]' }], + ['topics:delete', { label: '[[admin/manage/privileges:delete-topics]]' }], + ['posts:view_deleted', { label: '[[admin/manage/privileges:view_deleted]]' }], + ['purge', { label: '[[admin/manage/privileges:purge]]' }], + ['moderate', { label: '[[admin/manage/privileges:moderate]]' }], +]); + +privsCategories.getUserPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.list', Array.from(_privilegeMap.keys())); +privsCategories.getGroupPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.groups.list', Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`)); +privsCategories.getPrivilegeList = async () => { + const [user, group] = await Promise.all([ + privsCategories.getUserPrivilegeList(), + privsCategories.getGroupPrivilegeList(), + ]); + return user.concat(group); +}; + +privsCategories.init = async () => { + privsCategories._coreSize = _privilegeMap.size; + await plugins.hooks.fire('static:privileges.categories.init', { + privileges: _privilegeMap, + }); +}; + +// Method used in admin/category controller to show all users/groups with privs in that given cid +privsCategories.list = async function (cid) { + let labels = Array.from(_privilegeMap.values()).map(data => data.label); + labels = await utils.promiseParallel({ + users: plugins.hooks.fire('filter:privileges.list_human', labels.slice()), + groups: plugins.hooks.fire('filter:privileges.groups.list_human', labels.slice()), + }); + + const keys = await utils.promiseParallel({ + users: privsCategories.getUserPrivilegeList(), + groups: privsCategories.getGroupPrivilegeList(), + }); + + const payload = await utils.promiseParallel({ + labels, + users: helpers.getUserPrivileges(cid, keys.users), + groups: helpers.getGroupPrivileges(cid, keys.groups), + }); + payload.keys = keys; + + payload.columnCountUserOther = payload.labels.users.length - privsCategories._coreSize; + payload.columnCountGroupOther = payload.labels.groups.length - privsCategories._coreSize; + + return payload; +}; + +privsCategories.get = async function (cid, uid) { + const privs = [ + 'topics:create', 'topics:read', 'topics:schedule', + 'topics:tag', 'read', 'posts:view_deleted', + ]; + + const [userPrivileges, isAdministrator, isModerator] = await Promise.all([ + helpers.isAllowedTo(privs, uid, cid), + user.isAdministrator(uid), + user.isModerator(uid, cid), + ]); + + const combined = userPrivileges.map(allowed => allowed || isAdministrator); + const privData = _.zipObject(privs, combined); + const isAdminOrMod = isAdministrator || isModerator; + + return await plugins.hooks.fire('filter:privileges.categories.get', { + ...privData, + cid: cid, + uid: uid, + editable: isAdminOrMod, + view_deleted: isAdminOrMod || privData['posts:view_deleted'], + isAdminOrMod: isAdminOrMod, + }); +}; + +privsCategories.isAdminOrMod = async function (cid, uid) { + if (parseInt(uid, 10) <= 0) { + return false; + } + const [isAdmin, isMod] = await Promise.all([ + user.isAdministrator(uid), + user.isModerator(uid, cid), + ]); + return isAdmin || isMod; +}; + +privsCategories.isUserAllowedTo = async function (privilege, cid, uid) { + if ((Array.isArray(privilege) && !privilege.length) || (Array.isArray(cid) && !cid.length)) { + return []; + } + if (!cid) { + return false; + } + const results = await helpers.isAllowedTo(privilege, uid, Array.isArray(cid) ? cid : [cid]); + + if (Array.isArray(results) && results.length) { + return Array.isArray(cid) ? results : results[0]; + } + return false; +}; + +privsCategories.can = async function (privilege, cid, uid) { + if (!cid) { + return false; + } + const [disabled, isAdmin, isAllowed] = await Promise.all([ + categories.getCategoryField(cid, 'disabled'), + user.isAdministrator(uid), + privsCategories.isUserAllowedTo(privilege, cid, uid), + ]); + return !disabled && (isAllowed || isAdmin); +}; + +privsCategories.filterCids = async function (privilege, cids, uid) { + if (!Array.isArray(cids) || !cids.length) { + return []; + } + + cids = _.uniq(cids); + const [categoryData, allowedTo, isAdmin] = await Promise.all([ + categories.getCategoriesFields(cids, ['disabled']), + helpers.isAllowedTo(privilege, uid, cids), + user.isAdministrator(uid), + ]); + return cids.filter( + (cid, index) => !!cid && !categoryData[index].disabled && (allowedTo[index] || isAdmin) + ); +}; + +privsCategories.getBase = async function (privilege, cids, uid) { + return await utils.promiseParallel({ + categories: categories.getCategoriesFields(cids, ['disabled']), + allowedTo: helpers.isAllowedTo(privilege, uid, cids), + view_deleted: helpers.isAllowedTo('posts:view_deleted', uid, cids), + view_scheduled: helpers.isAllowedTo('topics:schedule', uid, cids), + isAdmin: user.isAdministrator(uid), + }); +}; + +privsCategories.filterUids = async function (privilege, cid, uids) { + if (!uids.length) { + return []; + } + + uids = _.uniq(uids); + + const [allowedTo, isAdmins] = await Promise.all([ + helpers.isUsersAllowedTo(privilege, uids, cid), + user.isAdministrator(uids), + ]); + return uids.filter((uid, index) => allowedTo[index] || isAdmins[index]); +}; + +privsCategories.give = async function (privileges, cid, members) { + await helpers.giveOrRescind(groups.join, privileges, cid, members); + plugins.hooks.fire('action:privileges.categories.give', { + privileges: privileges, + cids: Array.isArray(cid) ? cid : [cid], + members: Array.isArray(members) ? members : [members], + }); +}; + +privsCategories.rescind = async function (privileges, cid, members) { + await helpers.giveOrRescind(groups.leave, privileges, cid, members); + plugins.hooks.fire('action:privileges.categories.rescind', { + privileges: privileges, + cids: Array.isArray(cid) ? cid : [cid], + members: Array.isArray(members) ? members : [members], + }); +}; + +privsCategories.canMoveAllTopics = async function (currentCid, targetCid, uid) { + const [isAdmin, isModerators] = await Promise.all([ + user.isAdministrator(uid), + user.isModerator(uid, [currentCid, targetCid]), + ]); + return isAdmin || !isModerators.includes(false); +}; + +privsCategories.userPrivileges = async function (cid, uid) { + const userPrivilegeList = await privsCategories.getUserPrivilegeList(); + return await helpers.userOrGroupPrivileges(cid, uid, userPrivilegeList); +}; + +privsCategories.groupPrivileges = async function (cid, groupName) { + const groupPrivilegeList = await privsCategories.getGroupPrivilegeList(); + return await helpers.userOrGroupPrivileges(cid, groupName, groupPrivilegeList); +}; diff --git a/src/privileges/global.js b/src/privileges/global.js new file mode 100644 index 0000000000..79567ea1da --- /dev/null +++ b/src/privileges/global.js @@ -0,0 +1,136 @@ + +'use strict'; + +const _ = require('lodash'); + +const user = require('../user'); +const groups = require('../groups'); +const helpers = require('./helpers'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const privsGlobal = module.exports; + +/** + * Looking to add a new global privilege via plugin/theme? Attach a hook to + * `static:privileges.global.init` and call .set() on the privilege map passed + * in to your listener. + */ +const _privilegeMap = new Map([ + ['chat', { label: '[[admin/manage/privileges:chat]]' }], + ['upload:post:image', { label: '[[admin/manage/privileges:upload-images]]' }], + ['upload:post:file', { label: '[[admin/manage/privileges:upload-files]]' }], + ['signature', { label: '[[admin/manage/privileges:signature]]' }], + ['invite', { label: '[[admin/manage/privileges:invite]]' }], + ['group:create', { label: '[[admin/manage/privileges:allow-group-creation]]' }], + ['search:content', { label: '[[admin/manage/privileges:search-content]]' }], + ['search:users', { label: '[[admin/manage/privileges:search-users]]' }], + ['search:tags', { label: '[[admin/manage/privileges:search-tags]]' }], + ['view:users', { label: '[[admin/manage/privileges:view-users]]' }], + ['view:tags', { label: '[[admin/manage/privileges:view-tags]]' }], + ['view:groups', { label: '[[admin/manage/privileges:view-groups]]' }], + ['local:login', { label: '[[admin/manage/privileges:allow-local-login]]' }], + ['ban', { label: '[[admin/manage/privileges:ban]]' }], + ['mute', { label: '[[admin/manage/privileges:mute]]' }], + ['view:users:info', { label: '[[admin/manage/privileges:view-users-info]]' }], +]); + +privsGlobal.getUserPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.global.list', Array.from(_privilegeMap.keys())); +privsGlobal.getGroupPrivilegeList = async () => await plugins.hooks.fire('filter:privileges.global.groups.list', Array.from(_privilegeMap.keys()).map(privilege => `groups:${privilege}`)); +privsGlobal.getPrivilegeList = async () => { + const [user, group] = await Promise.all([ + privsGlobal.getUserPrivilegeList(), + privsGlobal.getGroupPrivilegeList(), + ]); + return user.concat(group); +}; + +privsGlobal.init = async () => { + privsGlobal._coreSize = _privilegeMap.size; + await plugins.hooks.fire('static:privileges.global.init', { + privileges: _privilegeMap, + }); +}; + +privsGlobal.list = async function () { + async function getLabels() { + const labels = Array.from(_privilegeMap.values()).map(data => data.label); + return await utils.promiseParallel({ + users: plugins.hooks.fire('filter:privileges.global.list_human', labels.slice()), + groups: plugins.hooks.fire('filter:privileges.global.groups.list_human', labels.slice()), + }); + } + + const keys = await utils.promiseParallel({ + users: privsGlobal.getUserPrivilegeList(), + groups: privsGlobal.getGroupPrivilegeList(), + }); + + const payload = await utils.promiseParallel({ + labels: getLabels(), + users: helpers.getUserPrivileges(0, keys.users), + groups: helpers.getGroupPrivileges(0, keys.groups), + }); + payload.keys = keys; + + payload.columnCountUserOther = keys.users.length - privsGlobal._coreSize; + payload.columnCountGroupOther = keys.groups.length - privsGlobal._coreSize; + + return payload; +}; + +privsGlobal.get = async function (uid) { + const userPrivilegeList = await privsGlobal.getUserPrivilegeList(); + const [userPrivileges, isAdministrator] = await Promise.all([ + helpers.isAllowedTo(userPrivilegeList, uid, 0), + user.isAdministrator(uid), + ]); + + const combined = userPrivileges.map(allowed => allowed || isAdministrator); + const privData = _.zipObject(userPrivilegeList, combined); + + return await plugins.hooks.fire('filter:privileges.global.get', privData); +}; + +privsGlobal.can = async function (privilege, uid) { + const [isAdministrator, isUserAllowedTo] = await Promise.all([ + user.isAdministrator(uid), + helpers.isAllowedTo(privilege, uid, [0]), + ]); + return isAdministrator || isUserAllowedTo[0]; +}; + +privsGlobal.canGroup = async function (privilege, groupName) { + return await groups.isMember(groupName, `cid:0:privileges:groups:${privilege}`); +}; + +privsGlobal.filterUids = async function (privilege, uids) { + const privCategories = require('./categories'); + return await privCategories.filterUids(privilege, 0, uids); +}; + +privsGlobal.give = async function (privileges, groupName) { + await helpers.giveOrRescind(groups.join, privileges, 0, groupName); + plugins.hooks.fire('action:privileges.global.give', { + privileges: privileges, + groupNames: Array.isArray(groupName) ? groupName : [groupName], + }); +}; + +privsGlobal.rescind = async function (privileges, groupName) { + await helpers.giveOrRescind(groups.leave, privileges, 0, groupName); + plugins.hooks.fire('action:privileges.global.rescind', { + privileges: privileges, + groupNames: Array.isArray(groupName) ? groupName : [groupName], + }); +}; + +privsGlobal.userPrivileges = async function (uid) { + const userPrivilegeList = await privsGlobal.getUserPrivilegeList(); + return await helpers.userOrGroupPrivileges(0, uid, userPrivilegeList); +}; + +privsGlobal.groupPrivileges = async function (groupName) { + const groupPrivilegeList = await privsGlobal.getGroupPrivilegeList(); + return await helpers.userOrGroupPrivileges(0, groupName, groupPrivilegeList); +}; diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js new file mode 100644 index 0000000000..b32deb7301 --- /dev/null +++ b/src/privileges/helpers.js @@ -0,0 +1,192 @@ + +'use strict'; + +const _ = require('lodash'); +const validator = require('validator'); + +const groups = require('../groups'); +const user = require('../user'); +const plugins = require('../plugins'); +const translator = require('../translator'); + +const helpers = module.exports; + +const uidToSystemGroup = { + 0: 'guests', + '-1': 'spiders', +}; + +helpers.isUsersAllowedTo = async function (privilege, uids, cid) { + const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([ + groups.isMembers(uids, `cid:${cid}:privileges:${privilege}`), + groups.isMembersOfGroupList(uids, `cid:${cid}:privileges:groups:${privilege}`), + ]); + const allowed = uids.map((uid, index) => hasUserPrivilege[index] || hasGroupPrivilege[index]); + const result = await plugins.hooks.fire('filter:privileges:isUsersAllowedTo', { allowed: allowed, privilege: privilege, uids: uids, cid: cid }); + return result.allowed; +}; + +helpers.isAllowedTo = async function (privilege, uidOrGroupName, cid) { + let allowed; + if (Array.isArray(privilege) && !Array.isArray(cid)) { + allowed = await isAllowedToPrivileges(privilege, uidOrGroupName, cid); + } else if (Array.isArray(cid) && !Array.isArray(privilege)) { + allowed = await isAllowedToCids(privilege, uidOrGroupName, cid); + } + if (allowed) { + ({ allowed } = await plugins.hooks.fire('filter:privileges:isAllowedTo', { allowed: allowed, privilege: privilege, uid: uidOrGroupName, cid: cid })); + return allowed; + } + throw new Error('[[error:invalid-data]]'); +}; + +async function isAllowedToCids(privilege, uidOrGroupName, cids) { + if (!privilege) { + return cids.map(() => false); + } + + const groupKeys = cids.map(cid => `cid:${cid}:privileges:groups:${privilege}`); + + // Group handling + if (isNaN(parseInt(uidOrGroupName, 10)) && (uidOrGroupName || '').length) { + return await checkIfAllowedGroup(uidOrGroupName, groupKeys); + } + + // User handling + if (parseInt(uidOrGroupName, 10) <= 0) { + return await isSystemGroupAllowedToCids(privilege, uidOrGroupName, cids); + } + + const userKeys = cids.map(cid => `cid:${cid}:privileges:${privilege}`); + return await checkIfAllowedUser(uidOrGroupName, userKeys, groupKeys); +} + +async function isAllowedToPrivileges(privileges, uidOrGroupName, cid) { + const groupKeys = privileges.map(privilege => `cid:${cid}:privileges:groups:${privilege}`); + // Group handling + if (isNaN(parseInt(uidOrGroupName, 10)) && (uidOrGroupName || '').length) { + return await checkIfAllowedGroup(uidOrGroupName, groupKeys); + } + + // User handling + if (parseInt(uidOrGroupName, 10) <= 0) { + return await isSystemGroupAllowedToPrivileges(privileges, uidOrGroupName, cid); + } + + const userKeys = privileges.map(privilege => `cid:${cid}:privileges:${privilege}`); + return await checkIfAllowedUser(uidOrGroupName, userKeys, groupKeys); +} + +async function checkIfAllowedUser(uid, userKeys, groupKeys) { + const [hasUserPrivilege, hasGroupPrivilege] = await Promise.all([ + groups.isMemberOfGroups(uid, userKeys), + groups.isMemberOfGroupsList(uid, groupKeys), + ]); + return userKeys.map((key, index) => hasUserPrivilege[index] || hasGroupPrivilege[index]); +} + +async function checkIfAllowedGroup(groupName, groupKeys) { + const sets = await Promise.all([ + groups.isMemberOfGroups(groupName, groupKeys), + groups.isMemberOfGroups('registered-users', groupKeys), + ]); + return groupKeys.map((key, index) => sets[0][index] || sets[1][index]); +} + +async function isSystemGroupAllowedToCids(privilege, uid, cids) { + const groupKeys = cids.map(cid => `cid:${cid}:privileges:groups:${privilege}`); + return await groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys); +} + +async function isSystemGroupAllowedToPrivileges(privileges, uid, cid) { + const groupKeys = privileges.map(privilege => `cid:${cid}:privileges:groups:${privilege}`); + return await groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys); +} + +helpers.getUserPrivileges = async function (cid, userPrivileges) { + let memberSets = await groups.getMembersOfGroups(userPrivileges.map(privilege => `cid:${cid}:privileges:${privilege}`)); + memberSets = memberSets.map(set => set.map(uid => parseInt(uid, 10))); + + const members = _.uniq(_.flatten(memberSets)); + const memberData = await user.getUsersFields(members, ['picture', 'username', 'banned']); + + memberData.forEach((member) => { + member.privileges = {}; + for (let x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) { + member.privileges[userPrivileges[x]] = memberSets[x].includes(parseInt(member.uid, 10)); + } + }); + + return memberData; +}; + +helpers.getGroupPrivileges = async function (cid, groupPrivileges) { + const [memberSets, allGroupNames] = await Promise.all([ + groups.getMembersOfGroups(groupPrivileges.map(privilege => `cid:${cid}:privileges:${privilege}`)), + groups.getGroups('groups:createtime', 0, -1), + ]); + + const uniqueGroups = _.uniq(_.flatten(memberSets)); + + let groupNames = allGroupNames.filter(groupName => !groupName.includes(':privileges:') && uniqueGroups.includes(groupName)); + + groupNames = groups.ephemeralGroups.concat(groupNames); + moveToFront(groupNames, groups.BANNED_USERS); + moveToFront(groupNames, 'Global Moderators'); + moveToFront(groupNames, 'unverified-users'); + moveToFront(groupNames, 'verified-users'); + moveToFront(groupNames, 'registered-users'); + + const adminIndex = groupNames.indexOf('administrators'); + if (adminIndex !== -1) { + groupNames.splice(adminIndex, 1); + } + const groupData = await groups.getGroupsFields(groupNames, ['private', 'system']); + const memberData = groupNames.map((member, index) => { + const memberPrivs = {}; + + for (let x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) { + memberPrivs[groupPrivileges[x]] = memberSets[x].includes(member); + } + return { + name: validator.escape(member), + nameEscaped: translator.escape(validator.escape(member)), + privileges: memberPrivs, + isPrivate: groupData[index] && !!groupData[index].private, + isSystem: groupData[index] && !!groupData[index].system, + }; + }); + return memberData; +}; + +function moveToFront(groupNames, groupToMove) { + const index = groupNames.indexOf(groupToMove); + if (index !== -1) { + groupNames.splice(0, 0, groupNames.splice(index, 1)[0]); + } else { + groupNames.unshift(groupToMove); + } +} + +helpers.giveOrRescind = async function (method, privileges, cids, members) { + members = Array.isArray(members) ? members : [members]; + cids = Array.isArray(cids) ? cids : [cids]; + for (const member of members) { + const groupKeys = []; + cids.forEach((cid) => { + privileges.forEach((privilege) => { + groupKeys.push(`cid:${cid}:privileges:${privilege}`); + }); + }); + /* eslint-disable no-await-in-loop */ + await method(groupKeys, member); + } +}; + +helpers.userOrGroupPrivileges = async function (cid, uidOrGroup, privilegeList) { + const groupNames = privilegeList.map(privilege => `cid:${cid}:privileges:${privilege}`); + const isMembers = await groups.isMemberOfGroups(uidOrGroup, groupNames); + return _.zipObject(privilegeList, isMembers); +}; + +require('../promisify')(helpers); diff --git a/src/privileges/index.js b/src/privileges/index.js new file mode 100644 index 0000000000..0fddedc2d7 --- /dev/null +++ b/src/privileges/index.js @@ -0,0 +1,17 @@ +'use strict'; + +const privileges = module.exports; +privileges.global = require('./global'); +privileges.admin = require('./admin'); +privileges.categories = require('./categories'); +privileges.topics = require('./topics'); +privileges.posts = require('./posts'); +privileges.users = require('./users'); + +privileges.init = async () => { + await privileges.global.init(); + await privileges.admin.init(); + await privileges.categories.init(); +}; + +require('../promisify')(privileges); diff --git a/src/privileges/posts.js b/src/privileges/posts.js new file mode 100644 index 0000000000..e46b749c56 --- /dev/null +++ b/src/privileges/posts.js @@ -0,0 +1,234 @@ + +'use strict'; + +const _ = require('lodash'); + +const meta = require('../meta'); +const posts = require('../posts'); +const topics = require('../topics'); +const user = require('../user'); +const helpers = require('./helpers'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const privsCategories = require('./categories'); +const privsTopics = require('./topics'); + +const privsPosts = module.exports; + +privsPosts.get = async function (pids, uid) { + if (!Array.isArray(pids) || !pids.length) { + return []; + } + const cids = await posts.getCidsByPids(pids); + const uniqueCids = _.uniq(cids); + + const results = await utils.promiseParallel({ + isAdmin: user.isAdministrator(uid), + isModerator: user.isModerator(uid, uniqueCids), + isOwner: posts.isOwner(pids, uid), + 'topics:read': helpers.isAllowedTo('topics:read', uid, uniqueCids), + read: helpers.isAllowedTo('read', uid, uniqueCids), + 'posts:edit': helpers.isAllowedTo('posts:edit', uid, uniqueCids), + 'posts:history': helpers.isAllowedTo('posts:history', uid, uniqueCids), + 'posts:view_deleted': helpers.isAllowedTo('posts:view_deleted', uid, uniqueCids), + }); + + const isModerator = _.zipObject(uniqueCids, results.isModerator); + const privData = {}; + privData['topics:read'] = _.zipObject(uniqueCids, results['topics:read']); + privData.read = _.zipObject(uniqueCids, results.read); + privData['posts:edit'] = _.zipObject(uniqueCids, results['posts:edit']); + privData['posts:history'] = _.zipObject(uniqueCids, results['posts:history']); + privData['posts:view_deleted'] = _.zipObject(uniqueCids, results['posts:view_deleted']); + + const privileges = cids.map((cid, i) => { + const isAdminOrMod = results.isAdmin || isModerator[cid]; + const editable = (privData['posts:edit'][cid] && (results.isOwner[i] || results.isModerator)) || results.isAdmin; + const viewDeletedPosts = results.isOwner[i] || privData['posts:view_deleted'][cid] || results.isAdmin; + const viewHistory = results.isOwner[i] || privData['posts:history'][cid] || results.isAdmin; + + return { + editable: editable, + move: isAdminOrMod, + isAdminOrMod: isAdminOrMod, + 'topics:read': privData['topics:read'][cid] || results.isAdmin, + read: privData.read[cid] || results.isAdmin, + 'posts:history': viewHistory, + 'posts:view_deleted': viewDeletedPosts, + }; + }); + + return privileges; +}; + +privsPosts.can = async function (privilege, pid, uid) { + const cid = await posts.getCidByPid(pid); + return await privsCategories.can(privilege, cid, uid); +}; + +privsPosts.filter = async function (privilege, pids, uid) { + if (!Array.isArray(pids) || !pids.length) { + return []; + } + + pids = _.uniq(pids); + const postData = await posts.getPostsFields(pids, ['uid', 'tid', 'deleted']); + const tids = _.uniq(postData.map(post => post && post.tid).filter(Boolean)); + const topicData = await topics.getTopicsFields(tids, ['deleted', 'scheduled', 'cid']); + + const tidToTopic = _.zipObject(tids, topicData); + + let cids = postData.map((post, index) => { + if (post) { + post.pid = pids[index]; + post.topic = tidToTopic[post.tid]; + } + return tidToTopic[post.tid] && tidToTopic[post.tid].cid; + }).filter(cid => parseInt(cid, 10)); + + cids = _.uniq(cids); + + const results = await privsCategories.getBase(privilege, cids, uid); + const allowedCids = cids.filter((cid, index) => !results.categories[index].disabled && + (results.allowedTo[index] || results.isAdmin)); + + const cidsSet = new Set(allowedCids); + const canViewDeleted = _.zipObject(cids, results.view_deleted); + const canViewScheduled = _.zipObject(cids, results.view_scheduled); + + pids = postData.filter(post => ( + post.topic && + cidsSet.has(post.topic.cid) && + (privsTopics.canViewDeletedScheduled({ + deleted: post.topic.deleted || post.deleted, + scheduled: post.topic.scheduled, + }, {}, canViewDeleted[post.topic.cid], canViewScheduled[post.topic.cid]) || results.isAdmin) + )).map(post => post.pid); + + const data = await plugins.hooks.fire('filter:privileges.posts.filter', { + privilege: privilege, + uid: uid, + pids: pids, + }); + + return data ? data.pids : null; +}; + +privsPosts.canEdit = async function (pid, uid) { + const results = await utils.promiseParallel({ + isAdmin: user.isAdministrator(uid), + isMod: posts.isModerator([pid], uid), + owner: posts.isOwner(pid, uid), + edit: privsPosts.can('posts:edit', pid, uid), + postData: posts.getPostFields(pid, ['tid', 'timestamp', 'deleted', 'deleterUid']), + userData: user.getUserFields(uid, ['reputation']), + }); + + results.isMod = results.isMod[0]; + if (results.isAdmin) { + return { flag: true }; + } + + if ( + !results.isMod && + meta.config.postEditDuration && + (Date.now() - results.postData.timestamp > meta.config.postEditDuration * 1000) + ) { + return { flag: false, message: `[[error:post-edit-duration-expired, ${meta.config.postEditDuration}]]` }; + } + if ( + !results.isMod && + meta.config.newbiePostEditDuration > 0 && + meta.config.newbiePostDelayThreshold > results.userData.reputation && + Date.now() - results.postData.timestamp > meta.config.newbiePostEditDuration * 1000 + ) { + return { flag: false, message: `[[error:post-edit-duration-expired, ${meta.config.newbiePostEditDuration}]]` }; + } + + const isLocked = await topics.isLocked(results.postData.tid); + if (!results.isMod && isLocked) { + return { flag: false, message: '[[error:topic-locked]]' }; + } + + if (!results.isMod && results.postData.deleted && parseInt(uid, 10) !== parseInt(results.postData.deleterUid, 10)) { + return { flag: false, message: '[[error:post-deleted]]' }; + } + + results.pid = parseInt(pid, 10); + results.uid = uid; + + const result = await plugins.hooks.fire('filter:privileges.posts.edit', results); + return { flag: result.edit && (result.owner || result.isMod), message: '[[error:no-privileges]]' }; +}; + +privsPosts.canDelete = async function (pid, uid) { + const postData = await posts.getPostFields(pid, ['uid', 'tid', 'timestamp', 'deleterUid']); + const results = await utils.promiseParallel({ + isAdmin: user.isAdministrator(uid), + isMod: posts.isModerator([pid], uid), + isLocked: topics.isLocked(postData.tid), + isOwner: posts.isOwner(pid, uid), + 'posts:delete': privsPosts.can('posts:delete', pid, uid), + }); + results.isMod = results.isMod[0]; + if (results.isAdmin) { + return { flag: true }; + } + + if (!results.isMod && results.isLocked) { + return { flag: false, message: '[[error:topic-locked]]' }; + } + + const { postDeleteDuration } = meta.config; + if (!results.isMod && postDeleteDuration && (Date.now() - postData.timestamp > postDeleteDuration * 1000)) { + return { flag: false, message: `[[error:post-delete-duration-expired, ${meta.config.postDeleteDuration}]]` }; + } + const { deleterUid } = postData; + const flag = results['posts:delete'] && ((results.isOwner && (deleterUid === 0 || deleterUid === postData.uid)) || results.isMod); + return { flag: flag, message: '[[error:no-privileges]]' }; +}; + +privsPosts.canFlag = async function (pid, uid) { + const targetUid = await posts.getPostField(pid, 'uid'); + const [userReputation, isAdminOrModerator, targetPrivileged, reporterPrivileged] = await Promise.all([ + user.getUserField(uid, 'reputation'), + isAdminOrMod(pid, uid), + user.isPrivileged(targetUid), + user.isPrivileged(uid), + ]); + const minimumReputation = meta.config['min:rep:flag']; + let canFlag = isAdminOrModerator || (userReputation >= minimumReputation); + + if (targetPrivileged && !reporterPrivileged) { + canFlag = false; + } + + return { flag: canFlag }; +}; + +privsPosts.canMove = async function (pid, uid) { + const isMain = await posts.isMain(pid); + if (isMain) { + throw new Error('[[error:cant-move-mainpost]]'); + } + return await isAdminOrMod(pid, uid); +}; + +privsPosts.canPurge = async function (pid, uid) { + const cid = await posts.getCidByPid(pid); + const results = await utils.promiseParallel({ + purge: privsCategories.isUserAllowedTo('purge', cid, uid), + owner: posts.isOwner(pid, uid), + isAdmin: user.isAdministrator(uid), + isModerator: user.isModerator(uid, cid), + }); + return (results.purge && (results.owner || results.isModerator)) || results.isAdmin; +}; + +async function isAdminOrMod(pid, uid) { + if (parseInt(uid, 10) <= 0) { + return false; + } + const cid = await posts.getCidByPid(pid); + return await privsCategories.isAdminOrMod(cid, uid); +} diff --git a/src/privileges/topics.js b/src/privileges/topics.js new file mode 100644 index 0000000000..6523c81ad2 --- /dev/null +++ b/src/privileges/topics.js @@ -0,0 +1,192 @@ + +'use strict'; + +const _ = require('lodash'); + +const meta = require('../meta'); +const topics = require('../topics'); +const user = require('../user'); +const helpers = require('./helpers'); +const categories = require('../categories'); +const plugins = require('../plugins'); +const privsCategories = require('./categories'); + +const privsTopics = module.exports; + +privsTopics.get = async function (tid, uid) { + uid = parseInt(uid, 10); + + const privs = [ + 'topics:reply', 'topics:read', 'topics:schedule', 'topics:tag', + 'topics:delete', 'posts:edit', 'posts:history', + 'posts:delete', 'posts:view_deleted', 'read', 'purge', + ]; + const topicData = await topics.getTopicFields(tid, ['cid', 'uid', 'locked', 'deleted', 'scheduled']); + const [userPrivileges, isAdministrator, isModerator, disabled] = await Promise.all([ + helpers.isAllowedTo(privs, uid, topicData.cid), + user.isAdministrator(uid), + user.isModerator(uid, topicData.cid), + categories.getCategoryField(topicData.cid, 'disabled'), + ]); + const privData = _.zipObject(privs, userPrivileges); + const isOwner = uid > 0 && uid === topicData.uid; + const isAdminOrMod = isAdministrator || isModerator; + const editable = isAdminOrMod; + const deletable = (privData['topics:delete'] && (isOwner || isModerator)) || isAdministrator; + const mayReply = privsTopics.canViewDeletedScheduled(topicData, {}, false, privData['topics:schedule']); + + return await plugins.hooks.fire('filter:privileges.topics.get', { + 'topics:reply': (privData['topics:reply'] && ((!topicData.locked && mayReply) || isModerator)) || isAdministrator, + 'topics:read': privData['topics:read'] || isAdministrator, + 'topics:schedule': privData['topics:schedule'] || isAdministrator, + 'topics:tag': privData['topics:tag'] || isAdministrator, + 'topics:delete': (privData['topics:delete'] && (isOwner || isModerator)) || isAdministrator, + 'posts:edit': (privData['posts:edit'] && (!topicData.locked || isModerator)) || isAdministrator, + 'posts:history': privData['posts:history'] || isAdministrator, + 'posts:delete': (privData['posts:delete'] && (!topicData.locked || isModerator)) || isAdministrator, + 'posts:view_deleted': privData['posts:view_deleted'] || isAdministrator, + read: privData.read || isAdministrator, + purge: (privData.purge && (isOwner || isModerator)) || isAdministrator, + + view_thread_tools: editable || deletable, + editable: editable, + deletable: deletable, + view_deleted: isAdminOrMod || isOwner || privData['posts:view_deleted'], + view_scheduled: privData['topics:schedule'] || isAdministrator, + isAdminOrMod: isAdminOrMod, + disabled: disabled, + tid: tid, + uid: uid, + }); +}; + +privsTopics.can = async function (privilege, tid, uid) { + const cid = await topics.getTopicField(tid, 'cid'); + return await privsCategories.can(privilege, cid, uid); +}; + +privsTopics.filterTids = async function (privilege, tids, uid) { + if (!Array.isArray(tids) || !tids.length) { + return []; + } + + const topicsData = await topics.getTopicsFields(tids, ['tid', 'cid', 'deleted', 'scheduled']); + const cids = _.uniq(topicsData.map(topic => topic.cid)); + const results = await privsCategories.getBase(privilege, cids, uid); + + const allowedCids = cids.filter((cid, index) => ( + !results.categories[index].disabled && + (results.allowedTo[index] || results.isAdmin) + )); + + const cidsSet = new Set(allowedCids); + const canViewDeleted = _.zipObject(cids, results.view_deleted); + const canViewScheduled = _.zipObject(cids, results.view_scheduled); + + tids = topicsData.filter(t => ( + cidsSet.has(t.cid) && + (results.isAdmin || privsTopics.canViewDeletedScheduled(t, {}, canViewDeleted[t.cid], canViewScheduled[t.cid])) + )).map(t => t.tid); + + const data = await plugins.hooks.fire('filter:privileges.topics.filter', { + privilege: privilege, + uid: uid, + tids: tids, + }); + return data ? data.tids : []; +}; + +privsTopics.filterUids = async function (privilege, tid, uids) { + if (!Array.isArray(uids) || !uids.length) { + return []; + } + + uids = _.uniq(uids); + const topicData = await topics.getTopicFields(tid, ['tid', 'cid', 'deleted', 'scheduled']); + const [disabled, allowedTo, isAdmins] = await Promise.all([ + categories.getCategoryField(topicData.cid, 'disabled'), + helpers.isUsersAllowedTo(privilege, uids, topicData.cid), + user.isAdministrator(uids), + ]); + + if (topicData.scheduled) { + const canViewScheduled = await helpers.isUsersAllowedTo('topics:schedule', uids, topicData.cid); + uids = uids.filter((uid, index) => canViewScheduled[index]); + } + + return uids.filter((uid, index) => !disabled && + ((allowedTo[index] && (topicData.scheduled || !topicData.deleted)) || isAdmins[index])); +}; + +privsTopics.canPurge = async function (tid, uid) { + const cid = await topics.getTopicField(tid, 'cid'); + const [purge, owner, isAdmin, isModerator] = await Promise.all([ + privsCategories.isUserAllowedTo('purge', cid, uid), + topics.isOwner(tid, uid), + user.isAdministrator(uid), + user.isModerator(uid, cid), + ]); + return (purge && (owner || isModerator)) || isAdmin; +}; + +privsTopics.canDelete = async function (tid, uid) { + const topicData = await topics.getTopicFields(tid, ['uid', 'cid', 'postcount', 'deleterUid']); + const [isModerator, isAdministrator, isOwner, allowedTo] = await Promise.all([ + user.isModerator(uid, topicData.cid), + user.isAdministrator(uid), + topics.isOwner(tid, uid), + helpers.isAllowedTo('topics:delete', uid, [topicData.cid]), + ]); + + if (isAdministrator) { + return true; + } + + const { preventTopicDeleteAfterReplies } = meta.config; + if (!isModerator && preventTopicDeleteAfterReplies && (topicData.postcount - 1) >= preventTopicDeleteAfterReplies) { + const langKey = preventTopicDeleteAfterReplies > 1 ? + `[[error:cant-delete-topic-has-replies, ${meta.config.preventTopicDeleteAfterReplies}]]` : + '[[error:cant-delete-topic-has-reply]]'; + throw new Error(langKey); + } + + const { deleterUid } = topicData; + return allowedTo[0] && ((isOwner && (deleterUid === 0 || deleterUid === topicData.uid)) || isModerator); +}; + +privsTopics.canEdit = async function (tid, uid) { + return await privsTopics.isOwnerOrAdminOrMod(tid, uid); +}; + +privsTopics.isOwnerOrAdminOrMod = async function (tid, uid) { + const [isOwner, isAdminOrMod] = await Promise.all([ + topics.isOwner(tid, uid), + privsTopics.isAdminOrMod(tid, uid), + ]); + return isOwner || isAdminOrMod; +}; + +privsTopics.isAdminOrMod = async function (tid, uid) { + if (parseInt(uid, 10) <= 0) { + return false; + } + const cid = await topics.getTopicField(tid, 'cid'); + return await privsCategories.isAdminOrMod(cid, uid); +}; + +privsTopics.canViewDeletedScheduled = function (topic, privileges = {}, viewDeleted = false, viewScheduled = false) { + if (!topic) { + return false; + } + const { deleted = false, scheduled = false } = topic; + const { view_deleted = viewDeleted, view_scheduled = viewScheduled } = privileges; + + // conceptually exclusive, scheduled topics deemed to be not deleted (they can only be purged) + if (scheduled) { + return view_scheduled; + } else if (deleted) { + return view_deleted; + } + + return true; +}; diff --git a/src/privileges/users.js b/src/privileges/users.js new file mode 100644 index 0000000000..d986cc5a85 --- /dev/null +++ b/src/privileges/users.js @@ -0,0 +1,154 @@ + +'use strict'; + +const _ = require('lodash'); + +const user = require('../user'); +const meta = require('../meta'); +const groups = require('../groups'); +const plugins = require('../plugins'); +const helpers = require('./helpers'); + +const privsUsers = module.exports; + +privsUsers.isAdministrator = async function (uid) { + return await isGroupMember(uid, 'administrators'); +}; + +privsUsers.isGlobalModerator = async function (uid) { + return await isGroupMember(uid, 'Global Moderators'); +}; + +async function isGroupMember(uid, groupName) { + return await groups[Array.isArray(uid) ? 'isMembers' : 'isMember'](uid, groupName); +} + +privsUsers.isModerator = async function (uid, cid) { + if (Array.isArray(cid)) { + return await isModeratorOfCategories(cid, uid); + } else if (Array.isArray(uid)) { + return await isModeratorsOfCategory(cid, uid); + } + return await isModeratorOfCategory(cid, uid); +}; + +async function isModeratorOfCategories(cids, uid) { + if (parseInt(uid, 10) <= 0) { + return await filterIsModerator(cids, uid, cids.map(() => false)); + } + + const isGlobalModerator = await privsUsers.isGlobalModerator(uid); + if (isGlobalModerator) { + return await filterIsModerator(cids, uid, cids.map(() => true)); + } + const uniqueCids = _.uniq(cids); + const isAllowed = await helpers.isAllowedTo('moderate', uid, uniqueCids); + + const cidToIsAllowed = _.zipObject(uniqueCids, isAllowed); + const isModerator = cids.map(cid => cidToIsAllowed[cid]); + return await filterIsModerator(cids, uid, isModerator); +} + +async function isModeratorsOfCategory(cid, uids) { + const [check1, check2, check3] = await Promise.all([ + privsUsers.isGlobalModerator(uids), + groups.isMembers(uids, `cid:${cid}:privileges:moderate`), + groups.isMembersOfGroupList(uids, `cid:${cid}:privileges:groups:moderate`), + ]); + const isModerator = uids.map((uid, idx) => check1[idx] || check2[idx] || check3[idx]); + return await filterIsModerator(cid, uids, isModerator); +} + +async function isModeratorOfCategory(cid, uid) { + const result = await isModeratorOfCategories([cid], uid); + return result ? result[0] : false; +} + +async function filterIsModerator(cid, uid, isModerator) { + const data = await plugins.hooks.fire('filter:user.isModerator', { uid: uid, cid: cid, isModerator: isModerator }); + if ((Array.isArray(uid) || Array.isArray(cid)) && !Array.isArray(data.isModerator)) { + throw new Error('filter:user.isModerator - i/o mismatch'); + } + + return data.isModerator; +} + +privsUsers.canEdit = async function (callerUid, uid) { + if (parseInt(callerUid, 10) === parseInt(uid, 10)) { + return true; + } + const [isAdmin, isGlobalMod, isTargetAdmin] = await Promise.all([ + privsUsers.isAdministrator(callerUid), + privsUsers.isGlobalModerator(callerUid), + privsUsers.isAdministrator(uid), + ]); + + const data = await plugins.hooks.fire('filter:user.canEdit', { + isAdmin: isAdmin, + isGlobalMod: isGlobalMod, + isTargetAdmin: isTargetAdmin, + canEdit: isAdmin || (isGlobalMod && !isTargetAdmin), + callerUid: callerUid, + uid: uid, + }); + return data.canEdit; +}; + +privsUsers.canBanUser = async function (callerUid, uid) { + const privsGlobal = require('./global'); + const [canBan, isTargetAdmin] = await Promise.all([ + privsGlobal.can('ban', callerUid), + privsUsers.isAdministrator(uid), + ]); + + const data = await plugins.hooks.fire('filter:user.canBanUser', { + canBan: canBan && !isTargetAdmin, + callerUid: callerUid, + uid: uid, + }); + return data.canBan; +}; + +privsUsers.canMuteUser = async function (callerUid, uid) { + const privsGlobal = require('./global'); + const [canMute, isTargetAdmin] = await Promise.all([ + privsGlobal.can('mute', callerUid), + privsUsers.isAdministrator(uid), + ]); + + const data = await plugins.hooks.fire('filter:user.canMuteUser', { + canMute: canMute && !isTargetAdmin, + callerUid: callerUid, + uid: uid, + }); + return data.canMute; +}; + +privsUsers.canFlag = async function (callerUid, uid) { + const [userReputation, targetPrivileged, reporterPrivileged] = await Promise.all([ + user.getUserField(callerUid, 'reputation'), + user.isPrivileged(uid), + user.isPrivileged(callerUid), + ]); + const minimumReputation = meta.config['min:rep:flag']; + let canFlag = reporterPrivileged || (userReputation >= minimumReputation); + + if (targetPrivileged && !reporterPrivileged) { + canFlag = false; + } + + return { flag: canFlag }; +}; + +privsUsers.hasBanPrivilege = async uid => await hasGlobalPrivilege('ban', uid); +privsUsers.hasMutePrivilege = async uid => await hasGlobalPrivilege('mute', uid); +privsUsers.hasInvitePrivilege = async uid => await hasGlobalPrivilege('invite', uid); + +async function hasGlobalPrivilege(privilege, uid) { + const privsGlobal = require('./global'); + const privilegeName = privilege.split('-').map(word => word.slice(0, 1).toUpperCase() + word.slice(1)).join(''); + let payload = { uid }; + payload[`can${privilegeName}`] = await privsGlobal.can(privilege, uid); + payload = await plugins.hooks.fire(`filter:user.has${privilegeName}Privilege`, payload); + return payload[`can${privilegeName}`]; +} diff --git a/src/promisify.js b/src/promisify.js new file mode 100644 index 0000000000..03c3b20860 --- /dev/null +++ b/src/promisify.js @@ -0,0 +1,61 @@ +'use strict'; + +const util = require('util'); + +module.exports = function (theModule, ignoreKeys) { + ignoreKeys = ignoreKeys || []; + function isCallbackedFunction(func) { + if (typeof func !== 'function') { + return false; + } + const str = func.toString().split('\n')[0]; + return str.includes('callback)'); + } + + function isAsyncFunction(fn) { + return fn && fn.constructor && fn.constructor.name === 'AsyncFunction'; + } + + function promisifyRecursive(module) { + if (!module) { + return; + } + + const keys = Object.keys(module); + keys.forEach((key) => { + if (ignoreKeys.includes(key)) { + return; + } + if (isAsyncFunction(module[key])) { + module[key] = wrapCallback(module[key], util.callbackify(module[key])); + } else if (isCallbackedFunction(module[key])) { + module[key] = wrapPromise(module[key], util.promisify(module[key])); + } else if (typeof module[key] === 'object') { + promisifyRecursive(module[key]); + } + }); + } + + function wrapCallback(origFn, callbackFn) { + return async function wrapperCallback(...args) { + if (args.length && typeof args[args.length - 1] === 'function') { + const cb = args.pop(); + args.push((err, res) => (res !== undefined ? cb(err, res) : cb(err))); + return callbackFn(...args); + } + return origFn(...args); + }; + } + + function wrapPromise(origFn, promiseFn) { + return function wrapperPromise(...args) { + if (args.length && typeof args[args.length - 1] === 'function') { + return origFn(...args); + } + + return promiseFn(...args); + }; + } + + promisifyRecursive(theModule); +}; diff --git a/src/pubsub.js b/src/pubsub.js new file mode 100644 index 0000000000..1a141101cf --- /dev/null +++ b/src/pubsub.js @@ -0,0 +1,71 @@ +'use strict'; + +const EventEmitter = require('events'); +const nconf = require('nconf'); + +let real; +let noCluster; +let singleHost; + +function get() { + if (real) { + return real; + } + + let pubsub; + + if (!nconf.get('isCluster')) { + if (noCluster) { + real = noCluster; + return real; + } + noCluster = new EventEmitter(); + noCluster.publish = noCluster.emit.bind(noCluster); + pubsub = noCluster; + } else if (nconf.get('singleHostCluster')) { + if (singleHost) { + real = singleHost; + return real; + } + singleHost = new EventEmitter(); + if (!process.send) { + singleHost.publish = singleHost.emit.bind(singleHost); + } else { + singleHost.publish = function (event, data) { + process.send({ + action: 'pubsub', + event: event, + data: data, + }); + }; + process.on('message', (message) => { + if (message && typeof message === 'object' && message.action === 'pubsub') { + singleHost.emit(message.event, message.data); + } + }); + } + pubsub = singleHost; + } else if (nconf.get('redis')) { + pubsub = require('./database/redis/pubsub'); + } else { + throw new Error('[[error:redis-required-for-pubsub]]'); + } + + real = pubsub; + return pubsub; +} + +module.exports = { + publish: function (event, data) { + get().publish(event, data); + }, + on: function (event, callback) { + get().on(event, callback); + }, + removeAllListeners: function (event) { + get().removeAllListeners(event); + }, + reset: function () { + real = null; + }, +}; diff --git a/src/rewards/admin.js b/src/rewards/admin.js new file mode 100644 index 0000000000..f46ad78687 --- /dev/null +++ b/src/rewards/admin.js @@ -0,0 +1,81 @@ +'use strict'; + +const plugins = require('../plugins'); +const db = require('../database'); +const utils = require('../utils'); + +const rewards = module.exports; + +rewards.save = async function (data) { + async function save(data) { + if (!Object.keys(data.rewards).length) { + return; + } + const rewardsData = data.rewards; + delete data.rewards; + if (!parseInt(data.id, 10)) { + data.id = await db.incrObjectField('global', 'rewards:id'); + } + await rewards.delete(data); + await db.setAdd('rewards:list', data.id); + await db.setObject(`rewards:id:${data.id}`, data); + await db.setObject(`rewards:id:${data.id}:rewards`, rewardsData); + } + + await Promise.all(data.map(data => save(data))); + await saveConditions(data); + return data; +}; + +rewards.delete = async function (data) { + await Promise.all([ + db.setRemove('rewards:list', data.id), + db.delete(`rewards:id:${data.id}`), + db.delete(`rewards:id:${data.id}:rewards`), + ]); +}; + +rewards.get = async function () { + return await utils.promiseParallel({ + active: getActiveRewards(), + conditions: plugins.hooks.fire('filter:rewards.conditions', []), + conditionals: plugins.hooks.fire('filter:rewards.conditionals', []), + rewards: plugins.hooks.fire('filter:rewards.rewards', []), + }); +}; + +async function saveConditions(data) { + const rewardsPerCondition = {}; + await db.delete('conditions:active'); + const conditions = []; + + data.forEach((reward) => { + conditions.push(reward.condition); + rewardsPerCondition[reward.condition] = rewardsPerCondition[reward.condition] || []; + rewardsPerCondition[reward.condition].push(reward.id); + }); + + await db.setAdd('conditions:active', conditions); + + await Promise.all(Object.keys(rewardsPerCondition).map(c => db.setAdd(`condition:${c}:rewards`, rewardsPerCondition[c]))); +} + +async function getActiveRewards() { + async function load(id) { + const [main, rewards] = await Promise.all([ + db.getObject(`rewards:id:${id}`), + db.getObject(`rewards:id:${id}:rewards`), + ]); + if (main) { + main.disabled = main.disabled === 'true'; + main.rewards = rewards; + } + return main; + } + + const rewardsList = await db.getSetMembers('rewards:list'); + const rewardData = await Promise.all(rewardsList.map(id => load(id))); + return rewardData.filter(Boolean); +} + +require('../promisify')(rewards); diff --git a/src/rewards/index.js b/src/rewards/index.js new file mode 100644 index 0000000000..396c94c01c --- /dev/null +++ b/src/rewards/index.js @@ -0,0 +1,80 @@ +'use strict'; + +const util = require('util'); + +const db = require('../database'); +const plugins = require('../plugins'); + +const rewards = module.exports; + +rewards.checkConditionAndRewardUser = async function (params) { + const { uid, condition, method } = params; + const isActive = await isConditionActive(condition); + if (!isActive) { + return; + } + const ids = await getIDsByCondition(condition); + let rewardData = await getRewardDataByIDs(ids); + rewardData = await filterCompletedRewards(uid, rewardData); + rewardData = rewardData.filter(Boolean); + if (!rewardData || !rewardData.length) { + return; + } + const eligible = await Promise.all(rewardData.map(reward => checkCondition(reward, method))); + const eligibleRewards = rewardData.filter((reward, index) => eligible[index]); + await giveRewards(uid, eligibleRewards); +}; + +async function isConditionActive(condition) { + return await db.isSetMember('conditions:active', condition); +} + +async function getIDsByCondition(condition) { + return await db.getSetMembers(`condition:${condition}:rewards`); +} + +async function filterCompletedRewards(uid, rewards) { + const data = await db.getSortedSetRangeByScoreWithScores(`uid:${uid}:rewards`, 0, -1, 1, '+inf'); + const userRewards = {}; + + data.forEach((obj) => { + userRewards[obj.value] = parseInt(obj.score, 10); + }); + + return rewards.filter((reward) => { + if (!reward) { + return false; + } + + const claimable = parseInt(reward.claimable, 10); + return claimable === 0 || (!userRewards[reward.id] || userRewards[reward.id] < reward.claimable); + }); +} + +async function getRewardDataByIDs(ids) { + return await db.getObjects(ids.map(id => `rewards:id:${id}`)); +} + +async function getRewardsByRewardData(rewards) { + return await db.getObjects(rewards.map(reward => `rewards:id:${reward.id}:rewards`)); +} + +async function checkCondition(reward, method) { + if (method.constructor && method.constructor.name !== 'AsyncFunction') { + method = util.promisify(method); + } + const value = await method(); + const bool = await plugins.hooks.fire(`filter:rewards.checkConditional:${reward.conditional}`, { left: value, right: reward.value }); + return bool; +} + +async function giveRewards(uid, rewards) { + const rewardData = await getRewardsByRewardData(rewards); + for (let i = 0; i < rewards.length; i++) { + /* eslint-disable no-await-in-loop */ + await plugins.hooks.fire(`action:rewards.award:${rewards[i].rid}`, { uid: uid, reward: rewardData[i] }); + await db.sortedSetIncrBy(`uid:${uid}:rewards`, 1, rewards[i].id); + } +} + +require('../promisify')(rewards); diff --git a/src/routes/admin.js b/src/routes/admin.js new file mode 100644 index 0000000000..1bf3c119f1 --- /dev/null +++ b/src/routes/admin.js @@ -0,0 +1,85 @@ +'use strict'; + +const helpers = require('./helpers'); + +module.exports = function (app, name, middleware, controllers) { + const middlewares = [middleware.pluginHooks]; + + helpers.setupAdminPageRoute(app, `/${name}`, middlewares, controllers.admin.routeIndex); + + helpers.setupAdminPageRoute(app, `/${name}/dashboard`, middlewares, controllers.admin.dashboard.get); + helpers.setupAdminPageRoute(app, `/${name}/dashboard/logins`, middlewares, controllers.admin.dashboard.getLogins); + helpers.setupAdminPageRoute(app, `/${name}/dashboard/users`, middlewares, controllers.admin.dashboard.getUsers); + helpers.setupAdminPageRoute(app, `/${name}/dashboard/topics`, middlewares, controllers.admin.dashboard.getTopics); + helpers.setupAdminPageRoute(app, `/${name}/dashboard/searches`, middlewares, controllers.admin.dashboard.getSearches); + + helpers.setupAdminPageRoute(app, `/${name}/manage/categories`, middlewares, controllers.admin.categories.getAll); + helpers.setupAdminPageRoute(app, `/${name}/manage/categories/:category_id`, middlewares, controllers.admin.categories.get); + helpers.setupAdminPageRoute(app, `/${name}/manage/categories/:category_id/analytics`, middlewares, controllers.admin.categories.getAnalytics); + + helpers.setupAdminPageRoute(app, `/${name}/manage/privileges/:cid?`, middlewares, controllers.admin.privileges.get); + helpers.setupAdminPageRoute(app, `/${name}/manage/tags`, middlewares, controllers.admin.tags.get); + + helpers.setupAdminPageRoute(app, `/${name}/manage/users`, middlewares, controllers.admin.users.index); + helpers.setupAdminPageRoute(app, `/${name}/manage/registration`, middlewares, controllers.admin.users.registrationQueue); + + helpers.setupAdminPageRoute(app, `/${name}/manage/admins-mods`, middlewares, controllers.admin.adminsMods.get); + + helpers.setupAdminPageRoute(app, `/${name}/manage/groups`, middlewares, controllers.admin.groups.list); + helpers.setupAdminPageRoute(app, `/${name}/manage/groups/:name`, middlewares, controllers.admin.groups.get); + + helpers.setupAdminPageRoute(app, `/${name}/manage/uploads`, middlewares, controllers.admin.uploads.get); + helpers.setupAdminPageRoute(app, `/${name}/manage/digest`, middlewares, controllers.admin.digest.get); + + helpers.setupAdminPageRoute(app, `/${name}/settings/email`, middlewares, controllers.admin.settings.email); + helpers.setupAdminPageRoute(app, `/${name}/settings/user`, middlewares, controllers.admin.settings.user); + helpers.setupAdminPageRoute(app, `/${name}/settings/post`, middlewares, controllers.admin.settings.post); + helpers.setupAdminPageRoute(app, `/${name}/settings/advanced`, middlewares, controllers.admin.settings.advanced); + helpers.setupAdminPageRoute(app, `/${name}/settings/languages`, middlewares, controllers.admin.settings.languages); + helpers.setupAdminPageRoute(app, `/${name}/settings/navigation`, middlewares, controllers.admin.settings.navigation); + helpers.setupAdminPageRoute(app, `/${name}/settings/homepage`, middlewares, controllers.admin.settings.homepage); + helpers.setupAdminPageRoute(app, `/${name}/settings/social`, middlewares, controllers.admin.settings.social); + helpers.setupAdminPageRoute(app, `/${name}/settings/:term?`, middlewares, controllers.admin.settings.get); + + helpers.setupAdminPageRoute(app, `/${name}/appearance/:term?`, middlewares, controllers.admin.appearance.get); + + helpers.setupAdminPageRoute(app, `/${name}/extend/plugins`, middlewares, controllers.admin.plugins.get); + helpers.setupAdminPageRoute(app, `/${name}/extend/widgets`, middlewares, controllers.admin.extend.widgets.get); + helpers.setupAdminPageRoute(app, `/${name}/extend/rewards`, middlewares, controllers.admin.extend.rewards.get); + + helpers.setupAdminPageRoute(app, `/${name}/advanced/database`, middlewares, controllers.admin.database.get); + helpers.setupAdminPageRoute(app, `/${name}/advanced/events`, middlewares, controllers.admin.events.get); + helpers.setupAdminPageRoute(app, `/${name}/advanced/hooks`, middlewares, controllers.admin.hooks.get); + helpers.setupAdminPageRoute(app, `/${name}/advanced/logs`, middlewares, controllers.admin.logs.get); + helpers.setupAdminPageRoute(app, `/${name}/advanced/errors`, middlewares, controllers.admin.errors.get); + helpers.setupAdminPageRoute(app, `/${name}/advanced/errors/export`, middlewares, controllers.admin.errors.export); + helpers.setupAdminPageRoute(app, `/${name}/advanced/cache`, middlewares, controllers.admin.cache.get); + + helpers.setupAdminPageRoute(app, `/${name}/development/logger`, middlewares, controllers.admin.logger.get); + helpers.setupAdminPageRoute(app, `/${name}/development/info`, middlewares, controllers.admin.info.get); + + apiRoutes(app, name, middleware, controllers); +}; + + +function apiRoutes(router, name, middleware, controllers) { + router.get(`/api/${name}/users/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.users.getCSV)); + router.get(`/api/${name}/groups/:groupname/csv`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.groups.getCSV)); + router.get(`/api/${name}/analytics`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.dashboard.getAnalytics)); + router.get(`/api/${name}/advanced/cache/dump`, middleware.ensureLoggedIn, helpers.tryRoute(controllers.admin.cache.dump)); + + const multipart = require('connect-multiparty'); + const multipartMiddleware = multipart(); + + const middlewares = [multipartMiddleware, middleware.validateFiles, + middleware.applyCSRF, middleware.ensureLoggedIn]; + + router.post(`/api/${name}/category/uploadpicture`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadCategoryPicture)); + router.post(`/api/${name}/uploadfavicon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadFavicon)); + router.post(`/api/${name}/uploadTouchIcon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadTouchIcon)); + router.post(`/api/${name}/uploadMaskableIcon`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadMaskableIcon)); + router.post(`/api/${name}/uploadlogo`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadLogo)); + router.post(`/api/${name}/uploadOgImage`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadOgImage)); + router.post(`/api/${name}/upload/file`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadFile)); + router.post(`/api/${name}/uploadDefaultAvatar`, middlewares, helpers.tryRoute(controllers.admin.uploads.uploadDefaultAvatar)); +} diff --git a/src/routes/api.js b/src/routes/api.js new file mode 100644 index 0000000000..0119bfed80 --- /dev/null +++ b/src/routes/api.js @@ -0,0 +1,56 @@ +'use strict'; + +const express = require('express'); +const winston = require('winston'); + +const uploadsController = require('../controllers/uploads'); +const helpers = require('./helpers'); + +module.exports = function (app, middleware, controllers) { + const middlewares = [middleware.authenticateRequest]; + const router = express.Router(); + app.use('/api', router); + + router.get('/config', [...middlewares, middleware.applyCSRF], helpers.tryRoute(controllers.api.getConfig)); + + router.get('/self', [...middlewares], helpers.tryRoute(controllers.user.getCurrentUser)); + router.get('/user/uid/:uid', [...middlewares, middleware.canViewUsers], helpers.tryRoute(controllers.user.getUserByUID)); + router.get('/user/username/:username', [...middlewares, middleware.canViewUsers], helpers.tryRoute(controllers.user.getUserByUsername)); + router.get('/user/email/:email', [...middlewares, middleware.canViewUsers], helpers.tryRoute(controllers.user.getUserByEmail)); + + router.get('/user/:userslug/export/posts', [...middlewares, middleware.authenticateRequest, middleware.ensureLoggedIn, middleware.checkAccountPermissions, middleware.exposeUid], helpers.tryRoute(controllers.user.exportPosts)); + router.get('/user/:userslug/export/uploads', [...middlewares, middleware.authenticateRequest, middleware.ensureLoggedIn, middleware.checkAccountPermissions, middleware.exposeUid], helpers.tryRoute(controllers.user.exportUploads)); + router.get('/user/:userslug/export/profile', [...middlewares, middleware.authenticateRequest, middleware.ensureLoggedIn, middleware.checkAccountPermissions, middleware.exposeUid], helpers.tryRoute(controllers.user.exportProfile)); + + // Deprecated, remove in v1.20.0 + router.get('/user/uid/:userslug/export/:type', (req, res) => { + winston.warn(`[router] \`/api/user/uid/${req.params.userslug}/export/${req.params.type}\` is deprecated, call it \`/api/user/${req.params.userslug}/export/${req.params.type}\`instead.`); + res.redirect(`/api/user/${req.params.userslug}/export/${req.params.type}`); + }); + + router.get('/categories/:cid/moderators', [...middlewares], helpers.tryRoute(controllers.api.getModerators)); + router.get('/recent/posts/:term?', [...middlewares], helpers.tryRoute(controllers.posts.getRecentPosts)); + router.get('/unread/total', [...middlewares, middleware.ensureLoggedIn], helpers.tryRoute(controllers.unread.unreadTotal)); + router.get('/topic/teaser/:topic_id', [...middlewares], helpers.tryRoute(controllers.topics.teaser)); + router.get('/topic/pagination/:topic_id', [...middlewares], helpers.tryRoute(controllers.topics.pagination)); + + const multipart = require('connect-multiparty'); + const multipartMiddleware = multipart(); + const postMiddlewares = [ + middleware.maintenanceMode, + multipartMiddleware, + middleware.validateFiles, + middleware.uploads.ratelimit, + middleware.applyCSRF, + ]; + + router.post('/post/upload', postMiddlewares, helpers.tryRoute(uploadsController.uploadPost)); + router.post('/user/:userslug/uploadpicture', [ + ...middlewares, + ...postMiddlewares, + middleware.exposeUid, + middleware.ensureLoggedIn, + middleware.canViewUsers, + middleware.checkAccountPermissions, + ], helpers.tryRoute(controllers.accounts.edit.uploadPicture)); +}; diff --git a/src/routes/authentication.js b/src/routes/authentication.js new file mode 100644 index 0000000000..406d2e934e --- /dev/null +++ b/src/routes/authentication.js @@ -0,0 +1,187 @@ +'use strict'; + +const async = require('async'); +const passport = require('passport'); +const passportLocal = require('passport-local').Strategy; +const BearerStrategy = require('passport-http-bearer').Strategy; +const winston = require('winston'); + +const meta = require('../meta'); +const controllers = require('../controllers'); +const helpers = require('../controllers/helpers'); +const plugins = require('../plugins'); + +let loginStrategies = []; + +const Auth = module.exports; + +Auth.initialize = function (app, middleware) { + app.use(passport.initialize()); + app.use(passport.session()); + app.use((req, res, next) => { + Auth.setAuthVars(req, res); + next(); + }); + + Auth.app = app; + Auth.middleware = middleware; + + // Apply wrapper around passport.authenticate to pass in keepSessionInfo option + const _authenticate = passport.authenticate; + passport.authenticate = (strategy, options, callback) => { + if (!callback && typeof options === 'function') { + return _authenticate.call(passport, strategy, options); + } + + if (!options.hasOwnProperty('keepSessionInfo')) { + options.keepSessionInfo = true; + } + + return _authenticate.call(passport, strategy, options, callback); + }; +}; + +Auth.setAuthVars = function setAuthVars(req) { + const isSpider = req.isSpider(); + req.loggedIn = !isSpider && !!req.user; + if (req.user) { + req.uid = parseInt(req.user.uid, 10); + } else if (isSpider) { + req.uid = -1; + } else { + req.uid = 0; + } +}; + +Auth.getLoginStrategies = function () { + return loginStrategies; +}; + +Auth.verifyToken = async function (token, done) { + const { tokens = [] } = await meta.settings.get('core.api'); + const tokenObj = tokens.find(t => t.token === token); + const uid = tokenObj ? tokenObj.uid : undefined; + + if (uid !== undefined) { + if (parseInt(uid, 10) > 0) { + done(null, { + uid: uid, + }); + } else { + done(null, { + master: true, + }); + } + } else { + done(false); + } +}; + +Auth.reloadRoutes = async function (params) { + loginStrategies.length = 0; + const { router } = params; + + // Local Logins + if (plugins.hooks.hasListeners('action:auth.overrideLogin')) { + winston.warn('[authentication] Login override detected, skipping local login strategy.'); + plugins.hooks.fire('action:auth.overrideLogin'); + } else { + passport.use(new passportLocal({ passReqToCallback: true }, controllers.authentication.localLogin)); + } + + // HTTP bearer authentication + passport.use('core.api', new BearerStrategy({}, Auth.verifyToken)); + + // Additional logins via SSO plugins + try { + loginStrategies = await plugins.hooks.fire('filter:auth.init', loginStrategies); + } catch (err) { + winston.error(`[authentication] ${err.stack}`); + } + loginStrategies = loginStrategies || []; + loginStrategies.forEach((strategy) => { + if (strategy.url) { + router[strategy.urlMethod || 'get'](strategy.url, Auth.middleware.applyCSRF, async (req, res, next) => { + let opts = { + scope: strategy.scope, + prompt: strategy.prompt || undefined, + }; + + if (strategy.checkState !== false) { + req.session.ssoState = req.csrfToken && req.csrfToken(); + opts.state = req.session.ssoState; + } + + // Allow SSO plugins to override/append options (for use in passport prototype authorizationParams) + ({ opts } = await plugins.hooks.fire('filter:auth.options', { req, res, opts })); + passport.authenticate(strategy.name, opts)(req, res, next); + }); + } + + router[strategy.callbackMethod || 'get'](strategy.callbackURL, (req, res, next) => { + // Ensure the passed-back state value is identical to the saved ssoState (unless explicitly skipped) + if (strategy.checkState === false) { + return next(); + } + + next(req.query.state !== req.session.ssoState ? new Error('[[error:csrf-invalid]]') : null); + }, (req, res, next) => { + // Trigger registration interstitial checks + req.session.registration = req.session.registration || {}; + // save returnTo for later usage in /register/complete + // passport seems to remove `req.session.returnTo` after it redirects + req.session.registration.returnTo = req.session.returnTo; + + passport.authenticate(strategy.name, (err, user) => { + if (err) { + if (req.session && req.session.registration) { + delete req.session.registration; + } + return next(err); + } + + if (!user) { + if (req.session && req.session.registration) { + delete req.session.registration; + } + return helpers.redirect(res, strategy.failureUrl !== undefined ? strategy.failureUrl : '/login'); + } + + res.locals.user = user; + res.locals.strategy = strategy; + next(); + })(req, res, next); + }, Auth.middleware.validateAuth, (req, res, next) => { + async.waterfall([ + async.apply(req.login.bind(req), res.locals.user, { keepSessionInfo: true }), + async.apply(controllers.authentication.onSuccessfulLogin, req, req.uid), + ], (err) => { + if (err) { + return next(err); + } + + helpers.redirect(res, strategy.successUrl !== undefined ? strategy.successUrl : '/'); + }); + }); + }); + + const multipart = require('connect-multiparty'); + const multipartMiddleware = multipart(); + const middlewares = [multipartMiddleware, Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist]; + + router.post('/register', middlewares, controllers.authentication.register); + router.post('/register/complete', middlewares, controllers.authentication.registerComplete); + router.post('/register/abort', Auth.middleware.applyCSRF, controllers.authentication.registerAbort); + router.post('/login', Auth.middleware.applyCSRF, Auth.middleware.applyBlacklist, controllers.authentication.login); + router.post('/logout', Auth.middleware.applyCSRF, controllers.authentication.logout); +}; + +passport.serializeUser((user, done) => { + done(null, user.uid); +}); + +passport.deserializeUser((uid, done) => { + done(null, { + uid: uid, + }); +}); diff --git a/src/routes/debug.js b/src/routes/debug.js new file mode 100644 index 0000000000..110e8ed07d --- /dev/null +++ b/src/routes/debug.js @@ -0,0 +1,35 @@ +'use strict'; + +const express = require('express'); +const nconf = require('nconf'); + +const fs = require('fs').promises; +const path = require('path'); + +module.exports = function (app) { + const router = express.Router(); + + router.get('/test', async (req, res) => { + res.redirect(404); + }); + + // Redoc + router.get('/spec/:type', async (req, res, next) => { + const types = ['read', 'write']; + const { type } = req.params; + if (!types.includes(type)) { + return next(); + } + + const handle = await fs.open(path.resolve(__dirname, '../../public/vendor/redoc/index.html'), 'r'); + let html = await handle.readFile({ + encoding: 'utf-8', + }); + await handle.close(); + + html = html.replace('apiUrl', `${nconf.get('relative_path')}/assets/openapi/${type}.yaml`); + res.status(200).type('text/html').send(html); + }); + + app.use(`${nconf.get('relative_path')}/debug`, router); +}; diff --git a/src/routes/feeds.js b/src/routes/feeds.js new file mode 100644 index 0000000000..31ec431756 --- /dev/null +++ b/src/routes/feeds.js @@ -0,0 +1,423 @@ +'use strict'; + +const rss = require('rss'); +const nconf = require('nconf'); +const validator = require('validator'); + +const posts = require('../posts'); +const topics = require('../topics'); +const user = require('../user'); +const categories = require('../categories'); +const meta = require('../meta'); +const helpers = require('../controllers/helpers'); +const privileges = require('../privileges'); +const db = require('../database'); +const utils = require('../utils'); +const controllers404 = require('../controllers/404'); + +const terms = { + daily: 'day', + weekly: 'week', + monthly: 'month', + alltime: 'alltime', +}; + +module.exports = function (app, middleware) { + app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic); + app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory); + app.get('/topics.rss', middleware.maintenanceMode, generateForTopics); + app.get('/recent.rss', middleware.maintenanceMode, generateForRecent); + app.get('/top.rss', middleware.maintenanceMode, generateForTop); + app.get('/top/:term.rss', middleware.maintenanceMode, generateForTop); + app.get('/popular.rss', middleware.maintenanceMode, generateForPopular); + app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular); + app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts); + app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, generateForCategoryRecentPosts); + app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, generateForUserTopics); + app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag); +}; + +async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) { + const uid = parseInt(req.query.uid, 10) || 0; + const { token } = req.query; + + if (!requiresLogin) { + return true; + } + + if (uid <= 0 || !token) { + return helpers.notAllowed(req, res); + } + const userToken = await db.getObjectField(`user:${uid}`, 'rss_token'); + if (userToken !== token) { + await user.auth.logAttempt(uid, req.ip); + return helpers.notAllowed(req, res); + } + const userPrivileges = await privileges.categories.get(cid, uid); + if (!userPrivileges.read) { + return helpers.notAllowed(req, res); + } + return true; +} + +async function generateForTopic(req, res, next) { + if (meta.config['feeds:disableRSS']) { + return next(); + } + + const tid = req.params.topic_id; + + const [userPrivileges, topic] = await Promise.all([ + privileges.topics.get(tid, req.uid), + topics.getTopicData(tid), + ]); + + if (!privileges.topics.canViewDeletedScheduled(topic, userPrivileges)) { + return next(); + } + + if (await validateTokenIfRequiresLogin(!userPrivileges['topics:read'], topic.cid, req, res)) { + const topicData = await topics.getTopicWithPosts(topic, `tid:${tid}:posts`, req.uid || req.query.uid || 0, 0, 24, true); + + topics.modifyPostsByPrivilege(topicData, userPrivileges); + + const feed = new rss({ + title: utils.stripHTMLTags(topicData.title, utils.tags), + description: topicData.posts.length ? topicData.posts[0].content : '', + feed_url: `${nconf.get('url')}/topic/${tid}.rss`, + site_url: `${nconf.get('url')}/topic/${topicData.slug}`, + image_url: topicData.posts.length ? topicData.posts[0].picture : '', + author: topicData.posts.length ? topicData.posts[0].username : '', + ttl: 60, + }); + + if (topicData.posts.length > 0) { + feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString(); + } + const replies = topicData.posts.slice(1); + replies.forEach((postData) => { + if (!postData.deleted) { + const dateStamp = new Date( + parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10) + ).toUTCString(); + + feed.item({ + title: `Reply to ${utils.stripHTMLTags(topicData.title, utils.tags)} on ${dateStamp}`, + description: postData.content, + url: `${nconf.get('url')}/post/${postData.pid}`, + author: postData.user ? postData.user.username : '', + date: dateStamp, + }); + } + }); + + sendFeed(feed, res); + } +} + +async function generateForCategory(req, res, next) { + const cid = req.params.category_id; + if (meta.config['feeds:disableRSS'] || !parseInt(cid, 10)) { + return next(); + } + const uid = req.uid || req.query.uid || 0; + const [userPrivileges, category, tids] = await Promise.all([ + privileges.categories.get(cid, req.uid), + categories.getCategoryData(cid), + db.getSortedSetRevIntersect({ + sets: ['topics:tid', `cid:${cid}:tids:lastposttime`], + start: 0, + stop: 25, + weights: [1, 0], + }), + ]); + + if (!category || !category.name) { + return next(); + } + + if (await validateTokenIfRequiresLogin(!userPrivileges.read, cid, req, res)) { + let topicsData = await topics.getTopicsByTids(tids, uid); + topicsData = await user.blocks.filter(uid, topicsData); + const feed = await generateTopicsFeed({ + uid: uid, + title: category.name, + description: category.description, + feed_url: `/category/${cid}.rss`, + site_url: `/category/${category.cid}`, + }, topicsData, 'timestamp'); + + sendFeed(feed, res); + } +} + +async function generateForTopics(req, res, next) { + if (meta.config['feeds:disableRSS']) { + return next(); + } + let token = null; + if (req.query.token && req.query.uid) { + token = await db.getObjectField(`user:${req.query.uid}`, 'rss_token'); + } + + await sendTopicsFeed({ + uid: token && token === req.query.token ? req.query.uid : req.uid, + title: 'Most recently created topics', + description: 'A list of topics that have been created recently', + feed_url: '/topics.rss', + useMainPost: true, + }, 'topics:tid', res); +} + +async function generateForRecent(req, res, next) { + await generateSorted({ + title: 'Recently Active Topics', + description: 'A list of topics that have been active within the past 24 hours', + feed_url: '/recent.rss', + site_url: '/recent', + sort: 'recent', + timestampField: 'lastposttime', + term: 'alltime', + }, req, res, next); +} + +async function generateForTop(req, res, next) { + await generateSorted({ + title: 'Top Voted Topics', + description: 'A list of topics that have received the most votes', + feed_url: `/top/${req.params.term || 'daily'}.rss`, + site_url: `/top/${req.params.term || 'daily'}`, + sort: 'votes', + timestampField: 'timestamp', + term: 'day', + }, req, res, next); +} + +async function generateForPopular(req, res, next) { + await generateSorted({ + title: 'Popular Topics', + description: 'A list of topics that are sorted by post count', + feed_url: `/popular/${req.params.term || 'daily'}.rss`, + site_url: `/popular/${req.params.term || 'daily'}`, + sort: 'posts', + timestampField: 'timestamp', + term: 'day', + }, req, res, next); +} + +async function generateSorted(options, req, res, next) { + if (meta.config['feeds:disableRSS']) { + return next(); + } + + const term = terms[req.params.term] || options.term; + + let token = null; + if (req.query.token && req.query.uid) { + token = await db.getObjectField(`user:${req.query.uid}`, 'rss_token'); + } + + const uid = token && token === req.query.token ? req.query.uid : req.uid; + + const params = { + uid: uid, + start: 0, + stop: 19, + term: term, + sort: options.sort, + }; + + const { cid } = req.query; + if (cid) { + if (!await privileges.categories.can('topics:read', cid, uid)) { + return helpers.notAllowed(req, res); + } + params.cids = [cid]; + } + + const result = await topics.getSortedTopics(params); + const feed = await generateTopicsFeed({ + uid: uid, + title: options.title, + description: options.description, + feed_url: options.feed_url, + site_url: options.site_url, + }, result.topics, options.timestampField); + + sendFeed(feed, res); +} + +async function sendTopicsFeed(options, set, res, timestampField) { + const start = options.hasOwnProperty('start') ? options.start : 0; + const stop = options.hasOwnProperty('stop') ? options.stop : 19; + const topicData = await topics.getTopicsFromSet(set, options.uid, start, stop); + const feed = await generateTopicsFeed(options, topicData.topics, timestampField); + sendFeed(feed, res); +} + +async function generateTopicsFeed(feedOptions, feedTopics, timestampField) { + feedOptions.ttl = 60; + feedOptions.feed_url = nconf.get('url') + feedOptions.feed_url; + feedOptions.site_url = nconf.get('url') + feedOptions.site_url; + + feedTopics = feedTopics.filter(Boolean); + + const feed = new rss(feedOptions); + + if (feedTopics.length > 0) { + feed.pubDate = new Date(feedTopics[0][timestampField]).toUTCString(); + } + + async function addFeedItem(topicData) { + const feedItem = { + title: utils.stripHTMLTags(topicData.title, utils.tags), + url: `${nconf.get('url')}/topic/${topicData.slug}`, + date: new Date(topicData[timestampField]).toUTCString(), + }; + + if (topicData.deleted) { + return; + } + + if (topicData.teaser && topicData.teaser.user && !feedOptions.useMainPost) { + feedItem.description = topicData.teaser.content; + feedItem.author = topicData.teaser.user.username; + feed.item(feedItem); + return; + } + + const mainPost = await topics.getMainPost(topicData.tid, feedOptions.uid); + if (!mainPost) { + feed.item(feedItem); + return; + } + feedItem.description = mainPost.content; + feedItem.author = mainPost.user && mainPost.user.username; + feed.item(feedItem); + } + + for (const topicData of feedTopics) { + /* eslint-disable no-await-in-loop */ + await addFeedItem(topicData); + } + return feed; +} + +async function generateForRecentPosts(req, res, next) { + if (meta.config['feeds:disableRSS']) { + return next(); + } + const page = parseInt(req.query.page, 10) || 1; + const postsPerPage = 20; + const start = Math.max(0, (page - 1) * postsPerPage); + const stop = start + postsPerPage - 1; + const postData = await posts.getRecentPosts(req.uid, start, stop, 'month'); + const feed = generateForPostsFeed({ + title: 'Recent Posts', + description: 'A list of recent posts', + feed_url: '/recentposts.rss', + site_url: '/recentposts', + }, postData); + + sendFeed(feed, res); +} + +async function generateForCategoryRecentPosts(req, res) { + if (meta.config['feeds:disableRSS']) { + return controllers404.handle404(req, res); + } + const cid = req.params.category_id; + const page = parseInt(req.query.page, 10) || 1; + const topicsPerPage = 20; + const start = Math.max(0, (page - 1) * topicsPerPage); + const stop = start + topicsPerPage - 1; + const [userPrivileges, category, postData] = await Promise.all([ + privileges.categories.get(cid, req.uid), + categories.getCategoryData(cid), + categories.getRecentReplies(cid, req.uid || req.query.uid || 0, start, stop), + ]); + + if (!category) { + return controllers404.handle404(req, res); + } + + if (await validateTokenIfRequiresLogin(!userPrivileges.read, cid, req, res)) { + const feed = generateForPostsFeed({ + title: `${category.name} Recent Posts`, + description: `A list of recent posts from ${category.name}`, + feed_url: `/category/${cid}/recentposts.rss`, + site_url: `/category/${cid}/recentposts`, + }, postData); + + sendFeed(feed, res); + } +} + +function generateForPostsFeed(feedOptions, posts) { + feedOptions.ttl = 60; + feedOptions.feed_url = nconf.get('url') + feedOptions.feed_url; + feedOptions.site_url = nconf.get('url') + feedOptions.site_url; + + const feed = new rss(feedOptions); + + if (posts.length > 0) { + feed.pubDate = new Date(parseInt(posts[0].timestamp, 10)).toUTCString(); + } + + posts.forEach((postData) => { + feed.item({ + title: postData.topic ? postData.topic.title : '', + description: postData.content, + url: `${nconf.get('url')}/post/${postData.pid}`, + author: postData.user ? postData.user.username : '', + date: new Date(parseInt(postData.timestamp, 10)).toUTCString(), + }); + }); + + return feed; +} + +async function generateForUserTopics(req, res, next) { + if (meta.config['feeds:disableRSS']) { + return next(); + } + + const { userslug } = req.params; + const uid = await user.getUidByUserslug(userslug); + if (!uid) { + return next(); + } + const userData = await user.getUserFields(uid, ['uid', 'username']); + await sendTopicsFeed({ + uid: req.uid, + title: `Topics by ${userData.username}`, + description: `A list of topics that are posted by ${userData.username}`, + feed_url: `/user/${userslug}/topics.rss`, + site_url: `/user/${userslug}/topics`, + }, `uid:${userData.uid}:topics`, res); +} + +async function generateForTag(req, res) { + if (meta.config['feeds:disableRSS']) { + return controllers404.handle404(req, res); + } + const tag = validator.escape(String(req.params.tag)); + const page = parseInt(req.query.page, 10) || 1; + const topicsPerPage = meta.config.topicsPerPage || 20; + const start = Math.max(0, (page - 1) * topicsPerPage); + const stop = start + topicsPerPage - 1; + await sendTopicsFeed({ + uid: req.uid, + title: `Topics tagged with ${tag}`, + description: `A list of topics that have been tagged with ${tag}`, + feed_url: `/tags/${tag}.rss`, + site_url: `/tags/${tag}`, + start: start, + stop: stop, + }, `tag:${tag}:topics`, res); +} + +function sendFeed(feed, res) { + const xml = feed.xml(); + res.type('xml').set('Content-Length', Buffer.byteLength(xml)).send(xml); +} diff --git a/src/routes/helpers.js b/src/routes/helpers.js new file mode 100644 index 0000000000..cf1f295bac --- /dev/null +++ b/src/routes/helpers.js @@ -0,0 +1,84 @@ +'use strict'; + +const helpers = module.exports; +const winston = require('winston'); +const middleware = require('../middleware'); +const controllerHelpers = require('../controllers/helpers'); + +// router, name, middleware(deprecated), middlewares(optional), controller +helpers.setupPageRoute = function (...args) { + const [router, name] = args; + let middlewares = args.length > 3 ? args[args.length - 2] : []; + const controller = args[args.length - 1]; + + if (args.length === 5) { + winston.warn(`[helpers.setupPageRoute(${name})] passing \`middleware\` as the third param is deprecated, it can now be safely removed`); + } + + middlewares = [ + middleware.authenticateRequest, + middleware.maintenanceMode, + middleware.registrationComplete, + middleware.pluginHooks, + ...middlewares, + middleware.pageView, + ]; + + router.get( + name, + middleware.busyCheck, + middlewares, + middleware.buildHeader, + helpers.tryRoute(controller) + ); + router.get(`/api${name}`, middlewares, helpers.tryRoute(controller)); +}; + +// router, name, middleware(deprecated), middlewares(optional), controller +helpers.setupAdminPageRoute = function (...args) { + const [router, name] = args; + const middlewares = args.length > 3 ? args[args.length - 2] : []; + const controller = args[args.length - 1]; + if (args.length === 5) { + winston.warn(`[helpers.setupAdminPageRoute(${name})] passing \`middleware\` as the third param is deprecated, it can now be safely removed`); + } + router.get(name, middleware.admin.buildHeader, middlewares, helpers.tryRoute(controller)); + router.get(`/api${name}`, middlewares, helpers.tryRoute(controller)); +}; + +// router, verb, name, middlewares(optional), controller +helpers.setupApiRoute = function (...args) { + const [router, verb, name] = args; + let middlewares = args.length > 4 ? args[args.length - 2] : []; + const controller = args[args.length - 1]; + + middlewares = [ + middleware.authenticateRequest, + middleware.maintenanceMode, + middleware.registrationComplete, + middleware.pluginHooks, + ...middlewares, + ]; + + router[verb](name, middlewares, helpers.tryRoute(controller, (err, res) => { + controllerHelpers.formatApiResponse(400, res, err); + })); +}; + +helpers.tryRoute = function (controller, handler) { + // `handler` is optional + if (controller && controller.constructor && controller.constructor.name === 'AsyncFunction') { + return async function (req, res, next) { + try { + await controller(req, res, next); + } catch (err) { + if (handler) { + return handler(err, res); + } + + next(err); + } + }; + } + return controller; +}; diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000000..91e9f001f3 --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,231 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); +const path = require('path'); +const express = require('express'); +const chalk = require('chalk'); + +const meta = require('../meta'); +const controllers = require('../controllers'); +const controllerHelpers = require('../controllers/helpers'); +const plugins = require('../plugins'); + +const authRoutes = require('./authentication'); +const writeRoutes = require('./write'); +const helpers = require('./helpers'); + +const { setupPageRoute } = helpers; + +const _mounts = { + user: require('./user'), + meta: require('./meta'), + api: require('./api'), + admin: require('./admin'), + feed: require('./feeds'), +}; + +_mounts.main = (app, middleware, controllers) => { + const loginRegisterMiddleware = [middleware.redirectToAccountIfLoggedIn]; + + setupPageRoute(app, '/login', loginRegisterMiddleware, controllers.login); + setupPageRoute(app, '/register', loginRegisterMiddleware, controllers.register); + setupPageRoute(app, '/register/complete', [], controllers.registerInterstitial); + setupPageRoute(app, '/compose', [], controllers.composer.get); + setupPageRoute(app, '/confirm/:code', [], controllers.confirmEmail); + setupPageRoute(app, '/outgoing', [], controllers.outgoing); + setupPageRoute(app, '/search', [], controllers.search.search); + setupPageRoute(app, '/reset/:code?', [middleware.delayLoading], controllers.reset); + setupPageRoute(app, '/tos', [], controllers.termsOfUse); + + setupPageRoute(app, '/email/unsubscribe/:token', [], controllers.accounts.settings.unsubscribe); + app.post('/email/unsubscribe/:token', controllers.accounts.settings.unsubscribePost); + + app.post('/compose', middleware.applyCSRF, controllers.composer.post); +}; + +_mounts.mod = (app, middleware, controllers) => { + setupPageRoute(app, '/flags', [], controllers.mods.flags.list); + setupPageRoute(app, '/flags/:flagId', [], controllers.mods.flags.detail); + setupPageRoute(app, '/post-queue/:id?', [], controllers.mods.postQueue); +}; + +_mounts.globalMod = (app, middleware, controllers) => { + setupPageRoute(app, '/ip-blacklist', [], controllers.globalMods.ipBlacklist); + setupPageRoute(app, '/registration-queue', [], controllers.globalMods.registrationQueue); +}; + +_mounts.topic = (app, name, middleware, controllers) => { + setupPageRoute(app, `/${name}/:topic_id/:slug/:post_index?`, [], controllers.topics.get); + setupPageRoute(app, `/${name}/:topic_id/:slug?`, [], controllers.topics.get); +}; + +_mounts.post = (app, name, middleware, controllers) => { + const middlewares = [ + middleware.maintenanceMode, + middleware.authenticateRequest, + middleware.registrationComplete, + middleware.pluginHooks, + ]; + app.get(`/${name}/:pid`, middleware.busyCheck, middlewares, controllers.posts.redirectToPost); + app.get(`/api/${name}/:pid`, middlewares, controllers.posts.redirectToPost); +}; + +_mounts.tags = (app, name, middleware, controllers) => { + setupPageRoute(app, `/${name}/:tag`, [middleware.privateTagListing], controllers.tags.getTag); + setupPageRoute(app, `/${name}`, [middleware.privateTagListing], controllers.tags.getTags); +}; + +_mounts.category = (app, name, middleware, controllers) => { + setupPageRoute(app, '/categories', [], controllers.categories.list); + setupPageRoute(app, '/popular', [], controllers.popular.get); + setupPageRoute(app, '/recent', [], controllers.recent.get); + setupPageRoute(app, '/top', [], controllers.top.get); + setupPageRoute(app, '/unread', [middleware.ensureLoggedIn], controllers.unread.get); + + setupPageRoute(app, `/${name}/:category_id/:slug/:topic_index`, [], controllers.category.get); + setupPageRoute(app, `/${name}/:category_id/:slug?`, [], controllers.category.get); +}; + +_mounts.career = (app, name, middleware, controllers) => { + const middlewares = [middleware.ensureLoggedIn]; + + setupPageRoute(app, `/${name}`, middlewares, controllers.career.get); +}; + +_mounts.users = (app, name, middleware, controllers) => { + const middlewares = [middleware.canViewUsers]; + + setupPageRoute(app, `/${name}`, middlewares, controllers.users.index); +}; + +_mounts.groups = (app, name, middleware, controllers) => { + const middlewares = [middleware.canViewGroups]; + + setupPageRoute(app, `/${name}`, middlewares, controllers.groups.list); + setupPageRoute(app, `/${name}/:slug`, middlewares, controllers.groups.details); + setupPageRoute(app, `/${name}/:slug/members`, middlewares, controllers.groups.members); +}; + +module.exports = async function (app, middleware) { + const router = express.Router(); + router.render = function (...args) { + app.render(...args); + }; + + // Allow plugins/themes to mount some routes elsewhere + const remountable = ['admin', 'category', 'topic', 'post', 'users', 'user', 'groups', 'tags', 'career']; + const { mounts } = await plugins.hooks.fire('filter:router.add', { + mounts: remountable.reduce((memo, mount) => { + memo[mount] = mount; + return memo; + }, {}), + }); + // Guard against plugins sending back missing/extra mounts + Object.keys(mounts).forEach((mount) => { + if (!remountable.includes(mount)) { + delete mounts[mount]; + } else if (typeof mount !== 'string') { + mounts[mount] = mount; + } + }); + remountable.forEach((mount) => { + if (!mounts.hasOwnProperty(mount)) { + mounts[mount] = mount; + } + }); + + router.all('(/+api|/+api/*?)', middleware.prepareAPI); + router.all(`(/+api/admin|/+api/admin/*?${mounts.admin !== 'admin' ? `|/+api/${mounts.admin}|/+api/${mounts.admin}/*?` : ''})`, middleware.authenticateRequest, middleware.ensureLoggedIn, middleware.admin.checkPrivileges); + router.all(`(/+admin|/+admin/*?${mounts.admin !== 'admin' ? `|/+${mounts.admin}|/+${mounts.admin}/*?` : ''})`, middleware.ensureLoggedIn, middleware.applyCSRF, middleware.admin.checkPrivileges); + + app.use(middleware.stripLeadingSlashes); + + // handle custom homepage routes + router.use('/', controllers.home.rewrite); + + // homepage handled by `action:homepage.get:[route]` + setupPageRoute(router, '/', [], controllers.home.pluginHook); + + await plugins.reloadRoutes({ router: router }); + await authRoutes.reloadRoutes({ router: router }); + await writeRoutes.reload({ router: router }); + addCoreRoutes(app, router, middleware, mounts); + + winston.info('[router] Routes added'); +}; + +function addCoreRoutes(app, router, middleware, mounts) { + _mounts.meta(router, middleware, controllers); + _mounts.api(router, middleware, controllers); + _mounts.feed(router, middleware, controllers); + + _mounts.main(router, middleware, controllers); + _mounts.mod(router, middleware, controllers); + _mounts.globalMod(router, middleware, controllers); + + addRemountableRoutes(app, router, middleware, mounts); + + const relativePath = nconf.get('relative_path'); + app.use(relativePath || '/', router); + + if (process.env.NODE_ENV === 'development') { + require('./debug')(app, middleware, controllers); + } + + app.use(middleware.privateUploads); + + const statics = [ + { route: '/assets', path: path.join(__dirname, '../../build/public') }, + { route: '/assets', path: path.join(__dirname, '../../public') }, + ]; + const staticOptions = { + maxAge: app.enabled('cache') ? 5184000000 : 0, + }; + + if (path.resolve(__dirname, '../../public/uploads') !== nconf.get('upload_path')) { + statics.unshift({ route: '/assets/uploads', path: nconf.get('upload_path') }); + } + + statics.forEach((obj) => { + app.use(relativePath + obj.route, middleware.addUploadHeaders, express.static(obj.path, staticOptions)); + }); + app.use(`${relativePath}/uploads`, (req, res) => { + res.redirect(`${relativePath}/assets/uploads${req.path}?${meta.config['cache-buster']}`); + }); + app.use(`${relativePath}/plugins`, (req, res) => { + winston.warn(`${chalk.bold.red('[deprecation]')} The \`/plugins\` shorthand prefix is deprecated, prefix with \`/assets/plugins\` instead (path: ${req.path})`); + res.redirect(`${relativePath}/assets/plugins${req.path}${req._parsedUrl.search || ''}`); + }); + + // Skins + meta.css.supportedSkins.forEach((skin) => { + app.use(`${relativePath}/assets/client-${skin}.css`, middleware.buildSkinAsset); + }); + + app.use(controllers['404'].handle404); + app.use(controllers.errors.handleURIErrors); + app.use(controllers.errors.handleErrors); +} + +function addRemountableRoutes(app, router, middleware, mounts) { + Object.keys(mounts).map(async (mount) => { + const original = mount; + mount = mounts[original]; + + if (!mount) { // do not mount at all + winston.warn(`[router] Not mounting /${original}`); + return; + } + + if (mount !== original) { + // Set up redirect for fallback handling (some js/tpls may still refer to the traditional mount point) + winston.info(`[router] /${original} prefix re-mounted to /${mount}. Requests to /${original}/* will now redirect to /${mount}`); + router.use(new RegExp(`/(api/)?${original}`), (req, res) => { + controllerHelpers.redirect(res, `${nconf.get('relative_path')}/${mount}${req.path}`); + }); + } + + _mounts[original](router, mount, middleware, controllers); + }); +} diff --git a/src/routes/meta.js b/src/routes/meta.js new file mode 100644 index 0000000000..3eb28df480 --- /dev/null +++ b/src/routes/meta.js @@ -0,0 +1,18 @@ +'use strict'; + +const path = require('path'); +const nconf = require('nconf'); + +module.exports = function (app, middleware, controllers) { + app.get('/sitemap.xml', controllers.sitemap.render); + app.get('/sitemap/pages.xml', controllers.sitemap.getPages); + app.get('/sitemap/categories.xml', controllers.sitemap.getCategories); + app.get(/\/sitemap\/topics\.(\d+)\.xml/, controllers.sitemap.getTopicPage); + app.get('/robots.txt', controllers.robots); + app.get('/manifest.webmanifest', controllers.manifest); + app.get('/css/previews/:theme', controllers.admin.themes.get); + app.get('/osd.xml', controllers.osd.handle); + app.get('/service-worker.js', (req, res) => { + res.status(200).type('application/javascript').set('Service-Worker-Allowed', `${nconf.get('relative_path')}/`).sendFile(path.join(__dirname, '../../public/src/service-worker.js')); + }); +}; diff --git a/src/routes/user.js b/src/routes/user.js new file mode 100644 index 0000000000..286910c764 --- /dev/null +++ b/src/routes/user.js @@ -0,0 +1,53 @@ +'use strict'; + +const helpers = require('./helpers'); + +const { setupPageRoute } = helpers; + +module.exports = function (app, name, middleware, controllers) { + const middlewares = [middleware.exposeUid, middleware.canViewUsers]; + const accountMiddlewares = [ + middleware.exposeUid, + middleware.ensureLoggedIn, + middleware.canViewUsers, + middleware.checkAccountPermissions, + ]; + + setupPageRoute(app, '/me', [], middleware.redirectMeToUserslug); + setupPageRoute(app, '/me/*', [], middleware.redirectMeToUserslug); + setupPageRoute(app, '/uid/:uid*', [], middleware.redirectUidToUserslug); + + setupPageRoute(app, `/${name}/:userslug`, middlewares, controllers.accounts.profile.get); + setupPageRoute(app, `/${name}/:userslug/following`, middlewares, controllers.accounts.follow.getFollowing); + setupPageRoute(app, `/${name}/:userslug/followers`, middlewares, controllers.accounts.follow.getFollowers); + + setupPageRoute(app, `/${name}/:userslug/posts`, middlewares, controllers.accounts.posts.getPosts); + setupPageRoute(app, `/${name}/:userslug/topics`, middlewares, controllers.accounts.posts.getTopics); + setupPageRoute(app, `/${name}/:userslug/best`, middlewares, controllers.accounts.posts.getBestPosts); + setupPageRoute(app, `/${name}/:userslug/controversial`, middlewares, controllers.accounts.posts.getControversialPosts); + setupPageRoute(app, `/${name}/:userslug/groups`, middlewares, controllers.accounts.groups.get); + + setupPageRoute(app, `/${name}/:userslug/categories`, accountMiddlewares, controllers.accounts.categories.get); + setupPageRoute(app, `/${name}/:userslug/bookmarks`, accountMiddlewares, controllers.accounts.posts.getBookmarks); + setupPageRoute(app, `/${name}/:userslug/watched`, accountMiddlewares, controllers.accounts.posts.getWatchedTopics); + setupPageRoute(app, `/${name}/:userslug/ignored`, accountMiddlewares, controllers.accounts.posts.getIgnoredTopics); + setupPageRoute(app, `/${name}/:userslug/upvoted`, accountMiddlewares, controllers.accounts.posts.getUpVotedPosts); + setupPageRoute(app, `/${name}/:userslug/downvoted`, accountMiddlewares, controllers.accounts.posts.getDownVotedPosts); + setupPageRoute(app, `/${name}/:userslug/edit`, accountMiddlewares, controllers.accounts.edit.get); + setupPageRoute(app, `/${name}/:userslug/edit/username`, accountMiddlewares, controllers.accounts.edit.username); + setupPageRoute(app, `/${name}/:userslug/edit/email`, accountMiddlewares, controllers.accounts.edit.email); + setupPageRoute(app, `/${name}/:userslug/edit/password`, accountMiddlewares, controllers.accounts.edit.password); + app.use('/.well-known/change-password', (req, res) => { + res.redirect('/me/edit/password'); + }); + setupPageRoute(app, `/${name}/:userslug/info`, accountMiddlewares, controllers.accounts.info.get); + setupPageRoute(app, `/${name}/:userslug/settings`, accountMiddlewares, controllers.accounts.settings.get); + setupPageRoute(app, `/${name}/:userslug/uploads`, accountMiddlewares, controllers.accounts.uploads.get); + setupPageRoute(app, `/${name}/:userslug/consent`, accountMiddlewares, controllers.accounts.consent.get); + setupPageRoute(app, `/${name}/:userslug/blocks`, accountMiddlewares, controllers.accounts.blocks.getBlocks); + setupPageRoute(app, `/${name}/:userslug/sessions`, accountMiddlewares, controllers.accounts.sessions.get); + + setupPageRoute(app, '/notifications', [middleware.ensureLoggedIn], controllers.accounts.notifications.get); + setupPageRoute(app, `/${name}/:userslug/chats/:roomid?`, middlewares, controllers.accounts.chats.get); + setupPageRoute(app, '/chats/:roomid?', [middleware.ensureLoggedIn], controllers.accounts.chats.redirectToChat); +}; diff --git a/src/routes/write/admin.js b/src/routes/write/admin.js new file mode 100644 index 0000000000..873e814c47 --- /dev/null +++ b/src/routes/write/admin.js @@ -0,0 +1,19 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn, middleware.admin.checkPrivileges]; + + setupApiRoute(router, 'put', '/settings/:setting', [...middlewares, middleware.checkRequired.bind(null, ['value'])], controllers.write.admin.updateSetting); + + setupApiRoute(router, 'get', '/analytics', [...middlewares], controllers.write.admin.getAnalyticsKeys); + setupApiRoute(router, 'get', '/analytics/:set', [...middlewares], controllers.write.admin.getAnalyticsData); + + return router; +}; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js new file mode 100644 index 0000000000..ce0ece3fdd --- /dev/null +++ b/src/routes/write/categories.js @@ -0,0 +1,26 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn]; + + setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.categories.create); + setupApiRoute(router, 'get', '/:cid', [], controllers.write.categories.get); + setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); + setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete); + + setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); + setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); + setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); + + setupApiRoute(router, 'put', '/:cid/moderator/:uid', [...middlewares], controllers.write.categories.setModerator); + setupApiRoute(router, 'delete', '/:cid/moderator/:uid', [...middlewares], controllers.write.categories.setModerator); + + return router; +}; diff --git a/src/routes/write/chats.js b/src/routes/write/chats.js new file mode 100644 index 0000000000..89ba95f518 --- /dev/null +++ b/src/routes/write/chats.js @@ -0,0 +1,34 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn, middleware.canChat]; + + setupApiRoute(router, 'get', '/', [...middlewares], controllers.write.chats.list); + setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.create); + + setupApiRoute(router, 'head', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.exists); + setupApiRoute(router, 'get', '/:roomId', [...middlewares, middleware.assert.room], controllers.write.chats.get); + setupApiRoute(router, 'post', '/:roomId', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['message'])], controllers.write.chats.post); + setupApiRoute(router, 'put', '/:roomId', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['name'])], controllers.write.chats.rename); + // no route for room deletion, noted here just in case... + + setupApiRoute(router, 'get', '/:roomId/users', [...middlewares, middleware.assert.room], controllers.write.chats.users); + setupApiRoute(router, 'post', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.invite); + setupApiRoute(router, 'delete', '/:roomId/users', [...middlewares, middleware.assert.room, middleware.checkRequired.bind(null, ['uids'])], controllers.write.chats.kick); + setupApiRoute(router, 'delete', '/:roomId/users/:uid', [...middlewares, middleware.assert.room, middleware.assert.user], controllers.write.chats.kickUser); + + setupApiRoute(router, 'get', '/:roomId/messages', [...middlewares, middleware.assert.room], controllers.write.chats.messages.list); + setupApiRoute(router, 'get', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.get); + setupApiRoute(router, 'put', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.edit); + setupApiRoute(router, 'post', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.restore); + setupApiRoute(router, 'delete', '/:roomId/messages/:mid', [...middlewares, middleware.assert.room, middleware.assert.message], controllers.write.chats.messages.delete); + + return router; +}; diff --git a/src/routes/write/files.js b/src/routes/write/files.js new file mode 100644 index 0000000000..6f8d78763c --- /dev/null +++ b/src/routes/write/files.js @@ -0,0 +1,33 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn, middleware.admin.checkPrivileges]; + + // setupApiRoute(router, 'put', '/', [ + // ...middlewares, + // middleware.checkRequired.bind(null, ['path']), + // middleware.assert.folder + // ], controllers.write.files.upload); + setupApiRoute(router, 'delete', '/', [ + ...middlewares, + middleware.checkRequired.bind(null, ['path']), + middleware.assert.path, + ], controllers.write.files.delete); + + setupApiRoute(router, 'put', '/folder', [ + ...middlewares, + middleware.checkRequired.bind(null, ['path', 'folderName']), + middleware.assert.path, + // Should come after assert.path + middleware.assert.folderName, + ], controllers.write.files.createFolder); + + return router; +}; diff --git a/src/routes/write/flags.js b/src/routes/write/flags.js new file mode 100644 index 0000000000..76b8998a85 --- /dev/null +++ b/src/routes/write/flags.js @@ -0,0 +1,23 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn]; + + setupApiRoute(router, 'post', '/', [...middlewares], controllers.write.flags.create); + + setupApiRoute(router, 'get', '/:flagId', [...middlewares, middleware.assert.flag], controllers.write.flags.get); + setupApiRoute(router, 'put', '/:flagId', [...middlewares, middleware.assert.flag], controllers.write.flags.update); + setupApiRoute(router, 'delete', '/:flagId', [...middlewares, middleware.assert.flag], controllers.write.flags.delete); + + setupApiRoute(router, 'post', '/:flagId/notes', [...middlewares, middleware.assert.flag], controllers.write.flags.appendNote); + setupApiRoute(router, 'delete', '/:flagId/notes/:datetime', [...middlewares, middleware.assert.flag], controllers.write.flags.deleteNote); + + return router; +}; diff --git a/src/routes/write/groups.js b/src/routes/write/groups.js new file mode 100644 index 0000000000..050f3fc66a --- /dev/null +++ b/src/routes/write/groups.js @@ -0,0 +1,23 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn]; + + setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.groups.create); + setupApiRoute(router, 'head', '/:slug', [middleware.assert.group], controllers.write.groups.exists); + setupApiRoute(router, 'put', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.update); + setupApiRoute(router, 'delete', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.delete); + setupApiRoute(router, 'put', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.join); + setupApiRoute(router, 'delete', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.leave); + setupApiRoute(router, 'put', '/:slug/ownership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.grant); + setupApiRoute(router, 'delete', '/:slug/ownership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.rescind); + + return router; +}; diff --git a/src/routes/write/index.js b/src/routes/write/index.js new file mode 100644 index 0000000000..153d25e51d --- /dev/null +++ b/src/routes/write/index.js @@ -0,0 +1,73 @@ +'use strict'; + +const winston = require('winston'); +const meta = require('../../meta'); +const plugins = require('../../plugins'); +const middleware = require('../../middleware'); +const writeControllers = require('../../controllers/write'); +const helpers = require('../../controllers/helpers'); + +const Write = module.exports; + +Write.reload = async (params) => { + const { router } = params; + let apiSettings = await meta.settings.get('core.api'); + plugins.hooks.register('core', { + hook: 'action:settings.set', + method: async (data) => { + if (data.plugin === 'core.api') { + apiSettings = await meta.settings.get('core.api'); + } + }, + }); + + router.use('/api/v3', (req, res, next) => { + // Require https if configured so + if (apiSettings.requireHttps === 'on' && req.protocol !== 'https') { + res.set('Upgrade', 'TLS/1.0, HTTP/1.1'); + return helpers.formatApiResponse(426, res); + } + + res.locals.isAPI = true; + next(); + }); + + router.use('/api/v3/users', require('./users')()); + router.use('/api/v3/groups', require('./groups')()); + router.use('/api/v3/categories', require('./categories')()); + router.use('/api/v3/topics', require('./topics')()); + router.use('/api/v3/posts', require('./posts')()); + router.use('/api/v3/chats', require('./chats')()); + router.use('/api/v3/flags', require('./flags')()); + router.use('/api/v3/admin', require('./admin')()); + router.use('/api/v3/files', require('./files')()); + router.use('/api/v3/utilities', require('./utilities')()); + + router.get('/api/v3/ping', writeControllers.utilities.ping.get); + router.post('/api/v3/ping', middleware.authenticateRequest, middleware.ensureLoggedIn, writeControllers.utilities.ping.post); + + /** + * Plugins can add routes to the Write API by attaching a listener to the + * below hook. The hooks added to the passed-in router will be mounted to + * `/api/v3/plugins`. + */ + const pluginRouter = require('express').Router(); + await plugins.hooks.fire('static:api.routes', { + router: pluginRouter, + middleware, + helpers, + }); + winston.info(`[api] Adding ${pluginRouter.stack.length} route(s) to \`api/v3/plugins\``); + router.use('/api/v3/plugins', pluginRouter); + + // 404 handling + router.use('/api/v3', (req, res) => { + helpers.formatApiResponse(404, res); + }); +}; + +Write.cleanup = (req) => { + if (req && req.session) { + req.session.destroy(); + } +}; diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js new file mode 100644 index 0000000000..e39703ad14 --- /dev/null +++ b/src/routes/write/posts.js @@ -0,0 +1,35 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn]; + + setupApiRoute(router, 'get', '/:pid', [], controllers.write.posts.get); + // There is no POST route because you POST to a topic to create a new post. Intuitive, no? + setupApiRoute(router, 'put', '/:pid', [...middlewares, middleware.checkRequired.bind(null, ['content'])], controllers.write.posts.edit); + setupApiRoute(router, 'delete', '/:pid', [...middlewares, middleware.assert.post], controllers.write.posts.purge); + + setupApiRoute(router, 'put', '/:pid/state', [...middlewares, middleware.assert.post], controllers.write.posts.restore); + setupApiRoute(router, 'delete', '/:pid/state', [...middlewares, middleware.assert.post], controllers.write.posts.delete); + + setupApiRoute(router, 'put', '/:pid/move', [...middlewares, middleware.assert.post, middleware.checkRequired.bind(null, ['tid'])], controllers.write.posts.move); + + setupApiRoute(router, 'put', '/:pid/vote', [...middlewares, middleware.checkRequired.bind(null, ['delta']), middleware.assert.post], controllers.write.posts.vote); + setupApiRoute(router, 'delete', '/:pid/vote', [...middlewares, middleware.assert.post], controllers.write.posts.unvote); + + setupApiRoute(router, 'put', '/:pid/bookmark', [...middlewares, middleware.assert.post], controllers.write.posts.bookmark); + setupApiRoute(router, 'delete', '/:pid/bookmark', [...middlewares, middleware.assert.post], controllers.write.posts.unbookmark); + + setupApiRoute(router, 'get', '/:pid/diffs', [middleware.assert.post], controllers.write.posts.getDiffs); + setupApiRoute(router, 'get', '/:pid/diffs/:since', [middleware.assert.post], controllers.write.posts.loadDiff); + setupApiRoute(router, 'put', '/:pid/diffs/:since', [...middlewares, middleware.assert.post], controllers.write.posts.restoreDiff); + setupApiRoute(router, 'delete', '/:pid/diffs/:timestamp', [...middlewares, middleware.assert.post], controllers.write.posts.deleteDiff); + + return router; +}; diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js new file mode 100644 index 0000000000..55b9b5a58a --- /dev/null +++ b/src/routes/write/topics.js @@ -0,0 +1,48 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + const middlewares = [middleware.ensureLoggedIn]; + + const multipart = require('connect-multiparty'); + const multipartMiddleware = multipart(); + + setupApiRoute(router, 'post', '/', [middleware.checkRequired.bind(null, ['cid', 'title', 'content'])], controllers.write.topics.create); + setupApiRoute(router, 'get', '/:tid', [], controllers.write.topics.get); + setupApiRoute(router, 'post', '/:tid', [middleware.checkRequired.bind(null, ['content']), middleware.assert.topic], controllers.write.topics.reply); + setupApiRoute(router, 'delete', '/:tid', [...middlewares], controllers.write.topics.purge); + + setupApiRoute(router, 'put', '/:tid/state', [...middlewares], controllers.write.topics.restore); + setupApiRoute(router, 'delete', '/:tid/state', [...middlewares], controllers.write.topics.delete); + + setupApiRoute(router, 'put', '/:tid/pin', [...middlewares, middleware.assert.topic], controllers.write.topics.pin); + setupApiRoute(router, 'delete', '/:tid/pin', [...middlewares], controllers.write.topics.unpin); + + setupApiRoute(router, 'put', '/:tid/lock', [...middlewares], controllers.write.topics.lock); + setupApiRoute(router, 'delete', '/:tid/lock', [...middlewares], controllers.write.topics.unlock); + + setupApiRoute(router, 'put', '/:tid/follow', [...middlewares, middleware.assert.topic], controllers.write.topics.follow); + setupApiRoute(router, 'delete', '/:tid/follow', [...middlewares, middleware.assert.topic], controllers.write.topics.unfollow); + setupApiRoute(router, 'put', '/:tid/ignore', [...middlewares, middleware.assert.topic], controllers.write.topics.ignore); + setupApiRoute(router, 'delete', '/:tid/ignore', [...middlewares, middleware.assert.topic], controllers.write.topics.unfollow); // intentional, unignore == unfollow + + setupApiRoute(router, 'put', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.addTags); + setupApiRoute(router, 'delete', '/:tid/tags', [...middlewares, middleware.assert.topic], controllers.write.topics.deleteTags); + + setupApiRoute(router, 'get', '/:tid/thumbs', [], controllers.write.topics.getThumbs); + setupApiRoute(router, 'post', '/:tid/thumbs', [multipartMiddleware, middleware.validateFiles, middleware.uploads.ratelimit, ...middlewares], controllers.write.topics.addThumb); + setupApiRoute(router, 'put', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['tid'])], controllers.write.topics.migrateThumbs); + setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['path'])], controllers.write.topics.deleteThumb); + setupApiRoute(router, 'put', '/:tid/thumbs/order', [...middlewares, middleware.checkRequired.bind(null, ['path', 'order'])], controllers.write.topics.reorderThumbs); + + setupApiRoute(router, 'get', '/:tid/events', [middleware.assert.topic], controllers.write.topics.getEvents); + setupApiRoute(router, 'delete', '/:tid/events/:eventId', [middleware.assert.topic], controllers.write.topics.deleteEvent); + + return router; +}; diff --git a/src/routes/write/users.js b/src/routes/write/users.js new file mode 100644 index 0000000000..ad071df40c --- /dev/null +++ b/src/routes/write/users.js @@ -0,0 +1,66 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +// eslint-disable-next-line no-unused-vars +function guestRoutes() { + // like registration, login... +} + +function authenticatedRoutes() { + const middlewares = [middleware.ensureLoggedIn]; + + setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['username'])], controllers.write.users.create); + setupApiRoute(router, 'delete', '/', [...middlewares, middleware.checkRequired.bind(null, ['uids'])], controllers.write.users.deleteMany); + + setupApiRoute(router, 'head', '/:uid', [middleware.assert.user], controllers.write.users.exists); + setupApiRoute(router, 'get', '/:uid', [...middlewares, middleware.assert.user], controllers.write.users.get); + setupApiRoute(router, 'put', '/:uid', [...middlewares, middleware.assert.user], controllers.write.users.update); + setupApiRoute(router, 'delete', '/:uid', [...middlewares, middleware.assert.user], controllers.write.users.delete); + setupApiRoute(router, 'put', '/:uid/picture', [...middlewares, middleware.assert.user], controllers.write.users.changePicture); + setupApiRoute(router, 'delete', '/:uid/content', [...middlewares, middleware.assert.user], controllers.write.users.deleteContent); + setupApiRoute(router, 'delete', '/:uid/account', [...middlewares, middleware.assert.user], controllers.write.users.deleteAccount); + + setupApiRoute(router, 'put', '/:uid/settings', [...middlewares, middleware.checkRequired.bind(null, ['settings'])], controllers.write.users.updateSettings); + + setupApiRoute(router, 'put', '/:uid/password', [...middlewares, middleware.checkRequired.bind(null, ['newPassword']), middleware.assert.user], controllers.write.users.changePassword); + + setupApiRoute(router, 'put', '/:uid/follow', [...middlewares, middleware.assert.user], controllers.write.users.follow); + setupApiRoute(router, 'delete', '/:uid/follow', [...middlewares, middleware.assert.user], controllers.write.users.unfollow); + + setupApiRoute(router, 'put', '/:uid/ban', [...middlewares, middleware.assert.user], controllers.write.users.ban); + setupApiRoute(router, 'delete', '/:uid/ban', [...middlewares, middleware.assert.user], controllers.write.users.unban); + + setupApiRoute(router, 'put', '/:uid/mute', [...middlewares, middleware.assert.user], controllers.write.users.mute); + setupApiRoute(router, 'delete', '/:uid/mute', [...middlewares, middleware.assert.user], controllers.write.users.unmute); + + setupApiRoute(router, 'post', '/:uid/tokens', [...middlewares, middleware.assert.user], controllers.write.users.generateToken); + setupApiRoute(router, 'delete', '/:uid/tokens/:token', [...middlewares, middleware.assert.user], controllers.write.users.deleteToken); + + setupApiRoute(router, 'delete', '/:uid/sessions/:uuid', [...middlewares, middleware.assert.user], controllers.write.users.revokeSession); + + setupApiRoute(router, 'post', '/:uid/invites', middlewares, controllers.write.users.invite); + setupApiRoute(router, 'get', '/:uid/invites/groups', [...middlewares, middleware.assert.user], controllers.write.users.getInviteGroups); + + setupApiRoute(router, 'get', '/:uid/emails', [...middlewares, middleware.assert.user], controllers.write.users.listEmails); + setupApiRoute(router, 'get', '/:uid/emails/:email', [...middlewares, middleware.assert.user], controllers.write.users.getEmail); + setupApiRoute(router, 'post', '/:uid/emails/:email/confirm', [...middlewares, middleware.assert.user], controllers.write.users.confirmEmail); + + setupApiRoute(router, 'head', '/:uid/exports/:type', [...middlewares, middleware.assert.user, middleware.checkAccountPermissions], controllers.write.users.checkExportByType); + setupApiRoute(router, 'get', '/:uid/exports/:type', [...middlewares, middleware.assert.user, middleware.checkAccountPermissions], controllers.write.users.getExportByType); + setupApiRoute(router, 'post', '/:uid/exports/:type', [...middlewares, middleware.assert.user, middleware.checkAccountPermissions], controllers.write.users.generateExportsByType); + + // Shorthand route to access user routes by userslug + router.all('/+bySlug/:userslug*?', [], controllers.write.users.redirectBySlug); +} + +module.exports = function () { + authenticatedRoutes(); + + return router; +}; diff --git a/src/routes/write/utilities.js b/src/routes/write/utilities.js new file mode 100644 index 0000000000..6e29c0c079 --- /dev/null +++ b/src/routes/write/utilities.js @@ -0,0 +1,17 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const { setupApiRoute } = routeHelpers; + +module.exports = function () { + // The "ping" routes are mounted at root level, but for organizational purposes, + // the controllers are in `utilities.js` + const middlewares = middleware.checkRequired.bind(null, ['username', 'password']); + setupApiRoute(router, 'post', '/login', [middlewares], controllers.write.utilities.login); + + return router; +}; diff --git a/src/search.js b/src/search.js new file mode 100644 index 0000000000..1a86d863f1 --- /dev/null +++ b/src/search.js @@ -0,0 +1,316 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('./database'); +const posts = require('./posts'); +const topics = require('./topics'); +const categories = require('./categories'); +const user = require('./user'); +const plugins = require('./plugins'); +const privileges = require('./privileges'); +const utils = require('./utils'); + +const search = module.exports; + +search.search = async function (data) { + const start = process.hrtime(); + data.sortBy = data.sortBy || 'relevance'; + + let result; + if (data.searchIn === 'posts' || data.searchIn === 'titles' || data.searchIn === 'titlesposts') { + result = await searchInContent(data); + } else if (data.searchIn === 'users') { + result = await user.search(data); + } else if (data.searchIn === 'categories') { + result = await categories.search(data); + } else if (data.searchIn === 'tags') { + result = await topics.searchAndLoadTags(data); + } else if (data.searchIn) { + result = await plugins.hooks.fire('filter:search.searchIn', { + data, + }); + } else { + throw new Error('[[error:unknown-search-filter]]'); + } + + result.time = (process.elapsedTimeSince(start) / 1000).toFixed(2); + return result; +}; + +async function searchInContent(data) { + data.uid = data.uid || 0; + + const [searchCids, searchUids] = await Promise.all([ + getSearchCids(data), + getSearchUids(data), + ]); + + async function doSearch(type, searchIn) { + if (searchIn.includes(data.searchIn)) { + const result = await plugins.hooks.fire('filter:search.query', { + index: type, + content: data.query, + matchWords: data.matchWords || 'all', + cid: searchCids, + uid: searchUids, + searchData: data, + ids: [], + }); + return Array.isArray(result) ? result : result.ids; + } + return []; + } + let pids = []; + let tids = []; + const inTopic = String(data.query || '').match(/^in:topic-([\d]+) /); + if (inTopic) { + const tid = inTopic[1]; + const cleanedTerm = data.query.replace(inTopic[0], ''); + pids = await topics.search(tid, cleanedTerm); + } else { + [pids, tids] = await Promise.all([ + doSearch('post', ['posts', 'titlesposts']), + doSearch('topic', ['titles', 'titlesposts']), + ]); + } + + const mainPids = await topics.getMainPids(tids); + + let allPids = mainPids.concat(pids).filter(Boolean); + + allPids = await privileges.posts.filter('topics:read', allPids, data.uid); + allPids = await filterAndSort(allPids, data); + + const metadata = await plugins.hooks.fire('filter:search.inContent', { + pids: allPids, + data: data, + }); + + if (data.returnIds) { + const mainPidsSet = new Set(mainPids); + const mainPidToTid = _.zipObject(mainPids, tids); + const pidsSet = new Set(pids); + const returnPids = allPids.filter(pid => pidsSet.has(pid)); + const returnTids = allPids.filter(pid => mainPidsSet.has(pid)).map(pid => mainPidToTid[pid]); + return { pids: returnPids, tids: returnTids }; + } + + const itemsPerPage = Math.min(data.itemsPerPage || 10, 100); + const returnData = { + posts: [], + matchCount: metadata.pids.length, + pageCount: Math.max(1, Math.ceil(parseInt(metadata.pids.length, 10) / itemsPerPage)), + }; + + if (data.page) { + const start = Math.max(0, (data.page - 1)) * itemsPerPage; + metadata.pids = metadata.pids.slice(start, start + itemsPerPage); + } + + returnData.posts = await posts.getPostSummaryByPids(metadata.pids, data.uid, {}); + await plugins.hooks.fire('filter:search.contentGetResult', { result: returnData, data: data }); + delete metadata.pids; + delete metadata.data; + return Object.assign(returnData, metadata); +} + +async function filterAndSort(pids, data) { + if (data.sortBy === 'relevance' && !data.replies && !data.timeRange && !data.hasTags && !plugins.hooks.hasListeners('filter:search.filterAndSort')) { + return pids; + } + let postsData = await getMatchedPosts(pids, data); + if (!postsData.length) { + return pids; + } + postsData = postsData.filter(Boolean); + + postsData = filterByPostcount(postsData, data.replies, data.repliesFilter); + postsData = filterByTimerange(postsData, data.timeRange, data.timeFilter); + postsData = filterByTags(postsData, data.hasTags); + + sortPosts(postsData, data); + + const result = await plugins.hooks.fire('filter:search.filterAndSort', { pids: pids, posts: postsData, data: data }); + return result.posts.map(post => post && post.pid); +} + +async function getMatchedPosts(pids, data) { + const postFields = ['pid', 'uid', 'tid', 'timestamp', 'deleted', 'upvotes', 'downvotes']; + + let postsData = await posts.getPostsFields(pids, postFields); + postsData = postsData.filter(post => post && !post.deleted); + const uids = _.uniq(postsData.map(post => post.uid)); + const tids = _.uniq(postsData.map(post => post.tid)); + + const [users, topics] = await Promise.all([ + getUsers(uids, data), + getTopics(tids, data), + ]); + + const tidToTopic = _.zipObject(tids, topics); + const uidToUser = _.zipObject(uids, users); + postsData.forEach((post) => { + if (topics && tidToTopic[post.tid]) { + post.topic = tidToTopic[post.tid]; + if (post.topic && post.topic.category) { + post.category = post.topic.category; + } + } + + if (uidToUser[post.uid]) { + post.user = uidToUser[post.uid]; + } + }); + + return postsData.filter(post => post && post.topic && !post.topic.deleted); +} + +async function getUsers(uids, data) { + if (data.sortBy.startsWith('user')) { + return user.getUsersFields(uids, ['username']); + } + return []; +} + +async function getTopics(tids, data) { + const topicsData = await topics.getTopicsData(tids); + const cids = _.uniq(topicsData.map(topic => topic && topic.cid)); + const categories = await getCategories(cids, data); + + const cidToCategory = _.zipObject(cids, categories); + topicsData.forEach((topic) => { + if (topic && categories && cidToCategory[topic.cid]) { + topic.category = cidToCategory[topic.cid]; + } + if (topic && topic.tags) { + topic.tags = topic.tags.map(tag => tag.value); + } + }); + + return topicsData; +} + +async function getCategories(cids, data) { + const categoryFields = []; + + if (data.sortBy.startsWith('category.')) { + categoryFields.push(data.sortBy.split('.')[1]); + } + if (!categoryFields.length) { + return null; + } + + return await db.getObjectsFields(cids.map(cid => `category:${cid}`), categoryFields); +} + +function filterByPostcount(posts, postCount, repliesFilter) { + postCount = parseInt(postCount, 10); + if (postCount) { + if (repliesFilter === 'atleast') { + posts = posts.filter(post => post.topic && post.topic.postcount >= postCount); + } else { + posts = posts.filter(post => post.topic && post.topic.postcount <= postCount); + } + } + return posts; +} + +function filterByTimerange(posts, timeRange, timeFilter) { + timeRange = parseInt(timeRange, 10) * 1000; + if (timeRange) { + const time = Date.now() - timeRange; + if (timeFilter === 'newer') { + posts = posts.filter(post => post.timestamp >= time); + } else { + posts = posts.filter(post => post.timestamp <= time); + } + } + return posts; +} + +function filterByTags(posts, hasTags) { + if (Array.isArray(hasTags) && hasTags.length) { + posts = posts.filter((post) => { + let hasAllTags = false; + if (post && post.topic && Array.isArray(post.topic.tags) && post.topic.tags.length) { + hasAllTags = hasTags.every(tag => post.topic.tags.includes(tag)); + } + return hasAllTags; + }); + } + return posts; +} + +function sortPosts(posts, data) { + if (!posts.length || data.sortBy === 'relevance') { + return; + } + + data.sortDirection = data.sortDirection || 'desc'; + const direction = data.sortDirection === 'desc' ? 1 : -1; + const fields = data.sortBy.split('.'); + if (fields.length === 1) { + return posts.sort((p1, p2) => direction * (p2[fields[0]] - p1[fields[0]])); + } + + const firstPost = posts[0]; + if (!fields || fields.length !== 2 || !firstPost[fields[0]] || !firstPost[fields[0]][fields[1]]) { + return; + } + + const isNumeric = utils.isNumber(firstPost[fields[0]][fields[1]]); + + if (isNumeric) { + posts.sort((p1, p2) => direction * (p2[fields[0]][fields[1]] - p1[fields[0]][fields[1]])); + } else { + posts.sort((p1, p2) => { + if (p1[fields[0]][fields[1]] > p2[fields[0]][fields[1]]) { + return direction; + } else if (p1[fields[0]][fields[1]] < p2[fields[0]][fields[1]]) { + return -direction; + } + return 0; + }); + } +} + +async function getSearchCids(data) { + if (!Array.isArray(data.categories) || !data.categories.length) { + return []; + } + + if (data.categories.includes('all')) { + return await categories.getCidsByPrivilege('categories:cid', data.uid, 'read'); + } + + const [watchedCids, childrenCids] = await Promise.all([ + getWatchedCids(data), + getChildrenCids(data), + ]); + return _.uniq(watchedCids.concat(childrenCids).concat(data.categories).filter(Boolean)); +} + +async function getWatchedCids(data) { + if (!data.categories.includes('watched')) { + return []; + } + return await user.getWatchedCategories(data.uid); +} + +async function getChildrenCids(data) { + if (!data.searchChildren) { + return []; + } + const childrenCids = await Promise.all(data.categories.map(cid => categories.getChildrenCids(cid))); + return await privileges.categories.filterCids('find', _.uniq(_.flatten(childrenCids)), data.uid); +} + +async function getSearchUids(data) { + if (!data.postedBy) { + return []; + } + return await user.getUidsByUsernames(Array.isArray(data.postedBy) ? data.postedBy : [data.postedBy]); +} + +require('./promisify')(search); diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 0000000000..7ae73dca73 --- /dev/null +++ b/src/settings.js @@ -0,0 +1,240 @@ +'use strict'; + +const meta = require('./meta'); +const pubsub = require('./pubsub'); + +function expandObjBy(obj1, obj2) { + let changed = false; + if (!obj1 || !obj2) { + return changed; + } + for (const [key, val2] of Object.entries(obj2)) { + const val1 = obj1[key]; + const xorIsArray = Array.isArray(val1) !== Array.isArray(val2); + if (xorIsArray || !obj1.hasOwnProperty(key) || typeof val2 !== typeof val1) { + obj1[key] = val2; + changed = true; + } else if (typeof val2 === 'object' && !Array.isArray(val2)) { + if (expandObjBy(val1, val2)) { + changed = true; + } + } + } + return changed; +} + +function trim(obj1, obj2) { + for (const [key, val1] of Object.entries(obj1)) { + if (!obj2.hasOwnProperty(key)) { + delete obj1[key]; + } else if (typeof val1 === 'object' && !Array.isArray(val1)) { + trim(val1, obj2[key]); + } + } +} + +function mergeSettings(cfg, defCfg) { + if (typeof defCfg !== 'object') { + return; + } + if (typeof cfg._ !== 'object') { + cfg._ = defCfg; + } else { + expandObjBy(cfg._, defCfg); + trim(cfg._, defCfg); + } +} + +/** + A class to manage Objects saved in {@link meta.settings} within property "_". + Constructor, synchronizes the settings and repairs them if version differs. + @param hash The hash to use for {@link meta.settings}. + @param version The version of the settings, used to determine whether the saved settings may be corrupt. + @param defCfg The default settings. + @param callback Gets called once the Settings-object is ready. + @param forceUpdate Whether to trigger structure-update even if the version doesn't differ from saved one. + Should be true while plugin-development to ensure structure-changes within settings persist. + @param reset Whether to reset the settings. + */ +function Settings(hash, version, defCfg, callback, forceUpdate, reset) { + this.hash = hash; + this.version = version || this.version; + this.defCfg = defCfg; + const self = this; + + if (reset) { + this.reset(callback); + } else { + this.sync(function () { + this.checkStructure(callback, forceUpdate); + }); + } + pubsub.on(`action:settings.set.${hash}`, (data) => { + try { + self.cfg._ = JSON.parse(data._); + } catch (err) {} + }); +} + +Settings.prototype.hash = ''; +Settings.prototype.defCfg = {}; +Settings.prototype.cfg = {}; +Settings.prototype.version = '0.0.0'; + +/** + Synchronizes the local object with the saved object (reverts changes). + @param callback Gets called when done. + */ +Settings.prototype.sync = function (callback) { + const _this = this; + meta.settings.get(this.hash, (err, settings) => { + try { + if (settings._) { + settings._ = JSON.parse(settings._); + } + } catch (_error) {} + _this.cfg = settings; + if (typeof _this.cfg._ !== 'object') { + _this.cfg._ = _this.defCfg; + _this.persist(callback); + } else if (expandObjBy(_this.cfg._, _this.defCfg)) { + _this.persist(callback); + } else if (typeof callback === 'function') { + callback.apply(_this, err); + } + }); +}; + +/** + Persists the local object. + @param callback Gets called when done. + */ +Settings.prototype.persist = function (callback) { + let conf = this.cfg._; + const _this = this; + if (typeof conf === 'object') { + conf = JSON.stringify(conf); + } + meta.settings.set(this.hash, this.createWrapper(this.cfg.v, conf), (...args) => { + if (typeof callback === 'function') { + callback.apply(_this, args || []); + } + }); + return this; +}; + +/** + Returns the setting of given key or default value if not set. + @param key The key of the setting to return. + @param def The default value, if not set global default value gets used. + @returns Object The setting to be used. + */ +Settings.prototype.get = function (key, def) { + let obj = this.cfg._; + const parts = (key || '').split('.'); + let part; + for (let i = 0; i < parts.length; i += 1) { + part = parts[i]; + if (part && obj != null) { + obj = obj[part]; + } + } + if (obj === undefined) { + if (def === undefined) { + def = this.defCfg; + for (let j = 0; j < parts.length; j += 1) { + part = parts[j]; + if (part && def != null) { + def = def[part]; + } + } + } + return def; + } + return obj; +}; + +/** + Returns the settings-wrapper object. + @returns Object The settings-wrapper. + */ +Settings.prototype.getWrapper = function () { + return this.cfg; +}; + +/** + Creates a new wrapper for the given settings with the given version. + @returns Object The new settings-wrapper. + */ +Settings.prototype.createWrapper = function (version, settings) { + return { + v: version, + _: settings, + }; +}; + +/** + Creates a new wrapper for the default settings. + @returns Object The new settings-wrapper. + */ +Settings.prototype.createDefaultWrapper = function () { + return this.createWrapper(this.version, this.defCfg); +}; + +/** + Sets the setting of given key to given value. + @param key The key of the setting to set. + @param val The value to set. + */ +Settings.prototype.set = function (key, val) { + let part; + let obj; + let parts; + this.cfg.v = this.version; + if (val == null || !key) { + this.cfg._ = val || key; + } else { + obj = this.cfg._; + parts = key.split('.'); + for (let i = 0, _len = parts.length - 1; i < _len; i += 1) { + part = parts[i]; + if (part) { + if (!obj.hasOwnProperty(part)) { + obj[part] = {}; + } + obj = obj[part]; + } + } + obj[parts[parts.length - 1]] = val; + } + return this; +}; + +/** + Resets the saved settings to default settings. + @param callback Gets called when done. + */ +Settings.prototype.reset = function (callback) { + this.set(this.defCfg).persist(callback); + return this; +}; + +/** + If the version differs the settings get updated and persisted. + @param callback Gets called when done. + @param force Whether to update and persist the settings even if the versions ara equal. + */ +Settings.prototype.checkStructure = function (callback, force) { + if (!force && this.cfg.v === this.version) { + if (typeof callback === 'function') { + callback(); + } + } else { + mergeSettings(this.cfg, this.defCfg); + this.cfg.v = this.version; + this.persist(callback); + } + return this; +}; + +module.exports = Settings; diff --git a/src/sitemap.js b/src/sitemap.js new file mode 100644 index 0000000000..49c6e2a531 --- /dev/null +++ b/src/sitemap.js @@ -0,0 +1,180 @@ +'use strict'; + +const { SitemapStream, streamToPromise } = require('sitemap'); +const nconf = require('nconf'); + +const db = require('./database'); +const categories = require('./categories'); +const topics = require('./topics'); +const privileges = require('./privileges'); +const meta = require('./meta'); +const plugins = require('./plugins'); +const utils = require('./utils'); + +const sitemap = module.exports; +sitemap.maps = { + topics: [], +}; + +sitemap.render = async function () { + const topicsPerPage = meta.config.sitemapTopics; + const returnData = { + url: nconf.get('url'), + topics: [], + }; + const [topicCount, categories, pages] = await Promise.all([ + db.getObjectField('global', 'topicCount'), + getSitemapCategories(), + getSitemapPages(), + ]); + returnData.categories = categories.length > 0; + returnData.pages = pages.length > 0; + const numPages = Math.ceil(Math.max(0, topicCount / topicsPerPage)); + for (let x = 1; x <= numPages; x += 1) { + returnData.topics.push(x); + } + + return returnData; +}; + +async function getSitemapPages() { + const urls = [{ + url: '', + changefreq: 'weekly', + priority: 0.6, + }, { + url: `${nconf.get('relative_path')}/recent`, + changefreq: 'daily', + priority: 0.4, + }, { + url: `${nconf.get('relative_path')}/users`, + changefreq: 'daily', + priority: 0.4, + }, { + url: `${nconf.get('relative_path')}/groups`, + changefreq: 'daily', + priority: 0.4, + }]; + + const data = await plugins.hooks.fire('filter:sitemap.getPages', { urls: urls }); + return data.urls; +} + +sitemap.getPages = async function () { + if (sitemap.maps.pages && Date.now() < sitemap.maps.pagesCacheExpireTimestamp) { + return sitemap.maps.pages; + } + + const urls = await getSitemapPages(); + if (!urls.length) { + sitemap.maps.pages = ''; + sitemap.maps.pagesCacheExpireTimestamp = Date.now() + (1000 * 60 * 60 * 24); + return sitemap.maps.pages; + } + + sitemap.maps.pages = await urlsToSitemap(urls); + sitemap.maps.pagesCacheExpireTimestamp = Date.now() + (1000 * 60 * 60 * 24); + return sitemap.maps.pages; +}; + +async function getSitemapCategories() { + const cids = await categories.getCidsByPrivilege('categories:cid', 0, 'find'); + return await categories.getCategoriesFields(cids, ['slug']); +} + +sitemap.getCategories = async function () { + if (sitemap.maps.categories && Date.now() < sitemap.maps.categoriesCacheExpireTimestamp) { + return sitemap.maps.categories; + } + + const categoryUrls = []; + const categoriesData = await getSitemapCategories(); + categoriesData.forEach((category) => { + if (category) { + categoryUrls.push({ + url: `${nconf.get('relative_path')}/category/${category.slug}`, + changefreq: 'weekly', + priority: 0.4, + }); + } + }); + + if (!categoryUrls.length) { + sitemap.maps.categories = ''; + sitemap.maps.categoriesCacheExpireTimestamp = Date.now() + (1000 * 60 * 60 * 24); + return sitemap.maps.categories; + } + + sitemap.maps.categories = await urlsToSitemap(categoryUrls); + sitemap.maps.categoriesCacheExpireTimestamp = Date.now() + (1000 * 60 * 60 * 24); + return sitemap.maps.categories; +}; + +sitemap.getTopicPage = async function (page) { + if (parseInt(page, 10) <= 0) { + return; + } + + const numTopics = meta.config.sitemapTopics; + const start = (parseInt(page, 10) - 1) * numTopics; + const stop = start + numTopics - 1; + + if (sitemap.maps.topics[page - 1] && Date.now() < sitemap.maps.topics[page - 1].cacheExpireTimestamp) { + return sitemap.maps.topics[page - 1].sm; + } + + const topicUrls = []; + let tids = await db.getSortedSetRange('topics:tid', start, stop); + tids = await privileges.topics.filterTids('topics:read', tids, 0); + const topicData = await topics.getTopicsFields(tids, ['tid', 'title', 'slug', 'lastposttime']); + + if (!topicData.length) { + sitemap.maps.topics[page - 1] = { + sm: '', + cacheExpireTimestamp: Date.now() + (1000 * 60 * 60 * 24), + }; + return sitemap.maps.topics[page - 1].sm; + } + + topicData.forEach((topic) => { + if (topic) { + topicUrls.push({ + url: `${nconf.get('relative_path')}/topic/${topic.slug}`, + lastmodISO: utils.toISOString(topic.lastposttime), + changefreq: 'daily', + priority: 0.6, + }); + } + }); + + sitemap.maps.topics[page - 1] = { + sm: await urlsToSitemap(topicUrls), + cacheExpireTimestamp: Date.now() + (1000 * 60 * 60 * 24), + }; + + return sitemap.maps.topics[page - 1].sm; +}; + +async function urlsToSitemap(urls) { + if (!urls.length) { + return ''; + } + const smStream = new SitemapStream({ hostname: nconf.get('url') }); + urls.forEach(url => smStream.write(url)); + smStream.end(); + return (await streamToPromise(smStream)).toString(); +} + +sitemap.clearCache = function () { + if (sitemap.maps.pages) { + sitemap.maps.pagesCacheExpireTimestamp = 0; + } + if (sitemap.maps.categories) { + sitemap.maps.categoriesCacheExpireTimestamp = 0; + } + sitemap.maps.topics.forEach((topicMap) => { + topicMap.cacheExpireTimestamp = 0; + }); +}; + +require('./promisify')(sitemap); diff --git a/src/slugify.js b/src/slugify.js new file mode 100644 index 0000000000..6ef70c1b87 --- /dev/null +++ b/src/slugify.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('../public/src/modules/slugify'); diff --git a/src/social.js b/src/social.js new file mode 100644 index 0000000000..1346bdbbd7 --- /dev/null +++ b/src/social.js @@ -0,0 +1,72 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.setActivePostSharingNetworks = exports.getActivePostSharing = exports.getPostSharing = void 0; +const lodash_1 = __importDefault(require("lodash")); +const plugins_1 = __importDefault(require("./plugins")); +const database_1 = __importDefault(require("./database")); +let postSharing = null; +function getPostSharing() { + return __awaiter(this, void 0, void 0, function* () { + if (postSharing) { + return lodash_1.default.cloneDeep(postSharing); + } + let networks = [ + { + id: 'facebook', + name: 'Facebook', + class: 'fa-facebook', + activated: null, + }, + { + id: 'twitter', + name: 'Twitter', + class: 'fa-twitter', + activated: null, + }, + ]; + networks = (yield plugins_1.default.hooks.fire('filter:social.posts', networks)); + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const activated = yield database_1.default.getSetMembers('social:posts.activated'); + networks.forEach((network) => { + network.activated = activated.includes(network.id); + }); + postSharing = networks; + return lodash_1.default.cloneDeep(networks); + }); +} +exports.getPostSharing = getPostSharing; +function getActivePostSharing() { + return __awaiter(this, void 0, void 0, function* () { + const networks = yield getPostSharing(); + return networks.filter(network => network && network.activated); + }); +} +exports.getActivePostSharing = getActivePostSharing; +function setActivePostSharingNetworks(networkIDs) { + return __awaiter(this, void 0, void 0, function* () { + postSharing = null; + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + yield database_1.default.delete('social:posts.activated'); + if (!networkIDs.length) { + return; + } + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + yield database_1.default.setAdd('social:posts.activated', networkIDs); + }); +} +exports.setActivePostSharingNetworks = setActivePostSharingNetworks; diff --git a/src/social.ts b/src/social.ts new file mode 100644 index 0000000000..3430f72d0a --- /dev/null +++ b/src/social.ts @@ -0,0 +1,61 @@ +import _ from 'lodash'; +import plugins from './plugins'; +import db from './database'; + +import { Network } from './types'; + +let postSharing: Network[] | null = null; + +export async function getPostSharing(): Promise { + if (postSharing) { + return _.cloneDeep(postSharing); + } + + let networks: Network[] = [ + { + id: 'facebook', + name: 'Facebook', + class: 'fa-facebook', + activated: null, + }, + { + id: 'twitter', + name: 'Twitter', + class: 'fa-twitter', + activated: null, + }, + ]; + + networks = await plugins.hooks.fire('filter:social.posts', networks) as Network[]; + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + const activated: string[] = await db.getSetMembers('social:posts.activated') as string[]; + + networks.forEach((network) => { + network.activated = activated.includes(network.id); + }); + + postSharing = networks; + return _.cloneDeep(networks); +} + +export async function getActivePostSharing(): Promise { + const networks: Network[] = await getPostSharing(); + return networks.filter(network => network && network.activated); +} + +export async function setActivePostSharingNetworks(networkIDs: string[]): Promise { + postSharing = null; + + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + await db.delete('social:posts.activated'); + + if (!networkIDs.length) { + return; + } + + // The next line calls a function in a module that has not been updated to TS yet + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + await db.setAdd('social:posts.activated', networkIDs); +} diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js new file mode 100644 index 0000000000..38c2ff14b8 --- /dev/null +++ b/src/socket.io/admin.js @@ -0,0 +1,121 @@ +'use strict'; + +const winston = require('winston'); + +const meta = require('../meta'); +const user = require('../user'); +const events = require('../events'); +const db = require('../database'); +const privileges = require('../privileges'); +const websockets = require('./index'); +const index = require('./index'); +const getAdminSearchDict = require('../admin/search').getDictionary; + +const SocketAdmin = module.exports; +SocketAdmin.user = require('./admin/user'); +SocketAdmin.categories = require('./admin/categories'); +SocketAdmin.settings = require('./admin/settings'); +SocketAdmin.tags = require('./admin/tags'); +SocketAdmin.rewards = require('./admin/rewards'); +SocketAdmin.navigation = require('./admin/navigation'); +SocketAdmin.rooms = require('./admin/rooms'); +SocketAdmin.social = require('./admin/social'); +SocketAdmin.themes = require('./admin/themes'); +SocketAdmin.plugins = require('./admin/plugins'); +SocketAdmin.widgets = require('./admin/widgets'); +SocketAdmin.config = require('./admin/config'); +SocketAdmin.settings = require('./admin/settings'); +SocketAdmin.email = require('./admin/email'); +SocketAdmin.analytics = require('./admin/analytics'); +SocketAdmin.logs = require('./admin/logs'); +SocketAdmin.errors = require('./admin/errors'); +SocketAdmin.digest = require('./admin/digest'); +SocketAdmin.cache = require('./admin/cache'); + +SocketAdmin.before = async function (socket, method) { + const isAdmin = await user.isAdministrator(socket.uid); + if (isAdmin) { + return; + } + + // Check admin privileges mapping (if not in mapping, deny access) + const privilegeSet = privileges.admin.socketMap.hasOwnProperty(method) ? privileges.admin.socketMap[method].split(';') : []; + const hasPrivilege = (await Promise.all(privilegeSet.map( + async privilege => privileges.admin.can(privilege, socket.uid) + ))).some(Boolean); + if (privilegeSet.length && hasPrivilege) { + return; + } + + winston.warn(`[socket.io] Call to admin method ( ${method} ) blocked (accessed by uid ${socket.uid})`); + throw new Error('[[error:no-privileges]]'); +}; + +SocketAdmin.restart = async function (socket) { + await logRestart(socket); + meta.restart(); +}; + +async function logRestart(socket) { + await events.log({ + type: 'restart', + uid: socket.uid, + ip: socket.ip, + }); + await db.setObject('lastrestart', { + uid: socket.uid, + ip: socket.ip, + timestamp: Date.now(), + }); +} + +SocketAdmin.reload = async function (socket) { + await require('../meta/build').buildAll(); + await events.log({ + type: 'build', + uid: socket.uid, + ip: socket.ip, + }); + + await logRestart(socket); + meta.restart(); +}; + +SocketAdmin.fireEvent = function (socket, data, callback) { + index.server.emit(data.name, data.payload || {}); + callback(); +}; + +SocketAdmin.deleteEvents = function (socket, eids, callback) { + events.deleteEvents(eids, callback); +}; + +SocketAdmin.deleteAllEvents = function (socket, data, callback) { + events.deleteAll(callback); +}; + +SocketAdmin.getSearchDict = async function (socket) { + const settings = await user.getSettings(socket.uid); + const lang = settings.userLang || meta.config.defaultLang || 'en-GB'; + return await getAdminSearchDict(lang); +}; + +SocketAdmin.deleteAllSessions = function (socket, data, callback) { + user.auth.deleteAllSessions(callback); +}; + +SocketAdmin.reloadAllSessions = function (socket, data, callback) { + websockets.in(`uid_${socket.uid}`).emit('event:livereload'); + callback(); +}; + +SocketAdmin.getServerTime = function (socket, data, callback) { + const now = new Date(); + + callback(null, { + timestamp: now.getTime(), + offset: now.getTimezoneOffset(), + }); +}; + +require('../promisify')(SocketAdmin); diff --git a/src/socket.io/admin/analytics.js b/src/socket.io/admin/analytics.js new file mode 100644 index 0000000000..5dbeb2eb81 --- /dev/null +++ b/src/socket.io/admin/analytics.js @@ -0,0 +1,36 @@ +'use strict'; + +const analytics = require('../../analytics'); +const utils = require('../../utils'); + +const Analytics = module.exports; + +Analytics.get = async function (socket, data) { + if (!data || !data.graph || !data.units) { + throw new Error('[[error:invalid-data]]'); + } + + // Default returns views from past 24 hours, by hour + if (!data.amount) { + if (data.units === 'days') { + data.amount = 30; + } else { + data.amount = 24; + } + } + const getStats = data.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; + if (data.graph === 'traffic') { + const result = await utils.promiseParallel({ + uniqueVisitors: getStats('analytics:uniquevisitors', data.until || Date.now(), data.amount), + pageviews: getStats('analytics:pageviews', data.until || Date.now(), data.amount), + pageviewsRegistered: getStats('analytics:pageviews:registered', data.until || Date.now(), data.amount), + pageviewsGuest: getStats('analytics:pageviews:guest', data.until || Date.now(), data.amount), + pageviewsBot: getStats('analytics:pageviews:bot', data.until || Date.now(), data.amount), + summary: analytics.getSummary(), + }); + result.pastDay = result.pageviews.reduce((a, b) => parseInt(a, 10) + parseInt(b, 10)); + const last = result.pageviews.length - 1; + result.pageviews[last] = parseInt(result.pageviews[last], 10) + analytics.getUnwrittenPageviews(); + return result; + } +}; diff --git a/src/socket.io/admin/cache.js b/src/socket.io/admin/cache.js new file mode 100644 index 0000000000..3ebb39bf35 --- /dev/null +++ b/src/socket.io/admin/cache.js @@ -0,0 +1,34 @@ +'use strict'; + +const SocketCache = module.exports; + +const db = require('../../database'); +const plugins = require('../../plugins'); + +SocketCache.clear = async function (socket, data) { + let caches = { + post: require('../../posts/cache'), + object: db.objectCache, + group: require('../../groups').cache, + local: require('../../cache'), + }; + caches = await plugins.hooks.fire('filter:admin.cache.get', caches); + if (!caches[data.name]) { + return; + } + caches[data.name].reset(); +}; + +SocketCache.toggle = async function (socket, data) { + let caches = { + post: require('../../posts/cache'), + object: db.objectCache, + group: require('../../groups').cache, + local: require('../../cache'), + }; + caches = await plugins.hooks.fire('filter:admin.cache.get', caches); + if (!caches[data.name]) { + return; + } + caches[data.name].enabled = data.enabled; +}; diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js new file mode 100644 index 0000000000..7f3db89d17 --- /dev/null +++ b/src/socket.io/admin/categories.js @@ -0,0 +1,44 @@ +'use strict'; + + +const categories = require('../../categories'); + +const Categories = module.exports; + +Categories.getNames = async function () { + return await categories.getAllCategoryFields(['cid', 'name']); +}; + +Categories.copyPrivilegesToChildren = async function (socket, data) { + const result = await categories.getChildren([data.cid], socket.uid); + const children = result[0]; + for (const child of children) { + // eslint-disable-next-line no-await-in-loop + await copyPrivilegesToChildrenRecursive(data.cid, child, data.group, data.filter); + } +}; + +async function copyPrivilegesToChildrenRecursive(parentCid, category, group, filter) { + await categories.copyPrivilegesFrom(parentCid, category.cid, group, filter); + for (const child of category.children) { + // eslint-disable-next-line no-await-in-loop + await copyPrivilegesToChildrenRecursive(parentCid, child, group, filter); + } +} + +Categories.copySettingsFrom = async function (socket, data) { + return await categories.copySettingsFrom(data.fromCid, data.toCid, data.copyParent); +}; + +Categories.copyPrivilegesFrom = async function (socket, data) { + await categories.copyPrivilegesFrom(data.fromCid, data.toCid, data.group, data.filter); +}; + +Categories.copyPrivilegesToAllCategories = async function (socket, data) { + let cids = await categories.getAllCidsFromSet('categories:cid'); + cids = cids.filter(cid => parseInt(cid, 10) !== parseInt(data.cid, 10)); + for (const toCid of cids) { + // eslint-disable-next-line no-await-in-loop + await categories.copyPrivilegesFrom(data.cid, toCid, data.group, data.filter); + } +}; diff --git a/src/socket.io/admin/config.js b/src/socket.io/admin/config.js new file mode 100644 index 0000000000..b4c2429fa0 --- /dev/null +++ b/src/socket.io/admin/config.js @@ -0,0 +1,50 @@ +'use strict'; + +const meta = require('../../meta'); +const plugins = require('../../plugins'); +const logger = require('../../logger'); +const events = require('../../events'); +const index = require('../index'); + +const Config = module.exports; + +Config.set = async function (socket, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + const _data = {}; + _data[data.key] = data.value; + await Config.setMultiple(socket, _data); +}; + +Config.setMultiple = async function (socket, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + + const changes = {}; + const newData = meta.configs.serialize(data); + const oldData = meta.configs.serialize(meta.config); + Object.keys(newData).forEach((key) => { + if (newData[key] !== oldData[key]) { + changes[key] = newData[key]; + changes[`${key}_old`] = meta.config[key]; + } + }); + await meta.configs.setMultiple(data); + for (const [key, value] of Object.entries(data)) { + const setting = { key, value }; + plugins.hooks.fire('action:config.set', setting); + logger.monitorConfig({ io: index.server }, setting); + } + if (Object.keys(changes).length) { + changes.type = 'config-change'; + changes.uid = socket.uid; + changes.ip = socket.ip; + await events.log(changes); + } +}; + +Config.remove = async function (socket, key) { + await meta.configs.remove(key); +}; diff --git a/src/socket.io/admin/digest.js b/src/socket.io/admin/digest.js new file mode 100644 index 0000000000..cb664b71df --- /dev/null +++ b/src/socket.io/admin/digest.js @@ -0,0 +1,24 @@ +'use strict'; + +const meta = require('../../meta'); +const userDigest = require('../../user/digest'); + +const Digest = module.exports; + +Digest.resend = async (socket, data) => { + const { uid } = data; + const interval = data.action.startsWith('resend-') ? data.action.slice(7) : await userDigest.getUsersInterval(uid); + + if (!interval && meta.config.dailyDigestFreq === 'off') { + throw new Error('[[error:digest-not-enabled]]'); + } + + if (uid) { + await userDigest.execute({ + interval: interval || meta.config.dailyDigestFreq, + subscribers: [uid], + }); + } else { + await userDigest.execute({ interval: interval }); + } +}; diff --git a/src/socket.io/admin/email.js b/src/socket.io/admin/email.js new file mode 100644 index 0000000000..de22fedf98 --- /dev/null +++ b/src/socket.io/admin/email.js @@ -0,0 +1,68 @@ +'use strict'; + +const meta = require('../../meta'); +const userDigest = require('../../user/digest'); +const userEmail = require('../../user/email'); +const notifications = require('../../notifications'); +const emailer = require('../../emailer'); +const utils = require('../../utils'); + +const Email = module.exports; + +Email.test = async function (socket, data) { + const payload = { + ...(data.payload || {}), + subject: '[[email:test-email.subject]]', + }; + + switch (data.template) { + case 'digest': + await userDigest.execute({ + interval: 'month', + subscribers: [socket.uid], + }); + break; + + case 'banned': + Object.assign(payload, { + username: 'test-user', + until: utils.toISOString(Date.now()), + reason: 'Test Reason', + }); + await emailer.send(data.template, socket.uid, payload); + break; + + case 'verify-email': + case 'welcome': + await userEmail.sendValidationEmail(socket.uid, { + force: 1, + template: data.template, + subject: data.template === 'welcome' ? `[[email:welcome-to, ${meta.config.title || meta.config.browserTitle || 'NodeBB'}]]` : undefined, + }); + break; + + case 'notification': { + const notification = await notifications.create({ + type: 'test', + bodyShort: '[[email:notif.test.short]]', + bodyLong: '[[email:notif.test.long]]', + nid: `uid:${socket.uid}:test`, + path: '/', + from: socket.uid, + }); + await emailer.send('notification', socket.uid, { + path: notification.path, + subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'), + intro: utils.stripHTMLTags(notification.bodyShort), + body: notification.bodyLong || '', + notification, + showUnsubscribe: true, + }); + break; + } + + default: + await emailer.send(data.template, socket.uid, payload); + break; + } +}; diff --git a/src/socket.io/admin/errors.js b/src/socket.io/admin/errors.js new file mode 100644 index 0000000000..84f5867ff7 --- /dev/null +++ b/src/socket.io/admin/errors.js @@ -0,0 +1,9 @@ +'use strict'; + +const meta = require('../../meta'); + +const Errors = module.exports; + +Errors.clear = async function () { + await meta.errors.clear(); +}; diff --git a/src/socket.io/admin/logs.js b/src/socket.io/admin/logs.js new file mode 100644 index 0000000000..96f3a85454 --- /dev/null +++ b/src/socket.io/admin/logs.js @@ -0,0 +1,13 @@ +'use strict'; + +const meta = require('../../meta'); + +const Logs = module.exports; + +Logs.get = async function () { + return await meta.logs.get(); +}; + +Logs.clear = async function () { + await meta.logs.clear(); +}; diff --git a/src/socket.io/admin/navigation.js b/src/socket.io/admin/navigation.js new file mode 100644 index 0000000000..8bc840ba2a --- /dev/null +++ b/src/socket.io/admin/navigation.js @@ -0,0 +1,9 @@ +'use strict'; + +const navigationAdmin = require('../../navigation/admin'); + +const SocketNavigation = module.exports; + +SocketNavigation.save = async function (socket, data) { + await navigationAdmin.save(data); +}; diff --git a/src/socket.io/admin/plugins.js b/src/socket.io/admin/plugins.js new file mode 100644 index 0000000000..53aae6bbdb --- /dev/null +++ b/src/socket.io/admin/plugins.js @@ -0,0 +1,49 @@ +'use strict'; + +const nconf = require('nconf'); + +const plugins = require('../../plugins'); +const events = require('../../events'); +const db = require('../../database'); + +const Plugins = module.exports; + +Plugins.toggleActive = async function (socket, plugin_id) { + require('../../posts/cache').reset(); + const data = await plugins.toggleActive(plugin_id); + await events.log({ + type: `plugin-${data.active ? 'activate' : 'deactivate'}`, + text: plugin_id, + uid: socket.uid, + }); + return data; +}; + +Plugins.toggleInstall = async function (socket, data) { + require('../../posts/cache').reset(); + await plugins.checkWhitelist(data.id, data.version); + const pluginData = await plugins.toggleInstall(data.id, data.version); + await events.log({ + type: `plugin-${pluginData.installed ? 'install' : 'uninstall'}`, + text: data.id, + version: data.version, + uid: socket.uid, + }); + return pluginData; +}; + +Plugins.getActive = async function () { + return await plugins.getActive(); +}; + +Plugins.orderActivePlugins = async function (socket, data) { + if (nconf.get('plugins:active')) { + throw new Error('[[error:plugins-set-in-configuration]]'); + } + data = data.filter(plugin => plugin && plugin.name); + await Promise.all(data.map(plugin => db.sortedSetAdd('plugins:active', plugin.order || 0, plugin.name))); +}; + +Plugins.upgrade = async function (socket, data) { + return await plugins.upgrade(data.id, data.version); +}; diff --git a/src/socket.io/admin/rewards.js b/src/socket.io/admin/rewards.js new file mode 100644 index 0000000000..b66d4f757f --- /dev/null +++ b/src/socket.io/admin/rewards.js @@ -0,0 +1,13 @@ +'use strict'; + +const rewardsAdmin = require('../../rewards/admin'); + +const SocketRewards = module.exports; + +SocketRewards.save = async function (socket, data) { + return await rewardsAdmin.save(data); +}; + +SocketRewards.delete = async function (socket, data) { + await rewardsAdmin.delete(data); +}; diff --git a/src/socket.io/admin/rooms.js b/src/socket.io/admin/rooms.js new file mode 100644 index 0000000000..c4cb137ba3 --- /dev/null +++ b/src/socket.io/admin/rooms.js @@ -0,0 +1,160 @@ +'use strict'; + +const os = require('os'); +const nconf = require('nconf'); + +const topics = require('../../topics'); +const pubsub = require('../../pubsub'); +const utils = require('../../utils'); + +const stats = {}; +const totals = {}; + +const SocketRooms = module.exports; + +SocketRooms.stats = stats; +SocketRooms.totals = totals; + +pubsub.on('sync:stats:start', () => { + const stats = SocketRooms.getLocalStats(); + pubsub.publish('sync:stats:end', { + stats: stats, + id: `${os.hostname()}:${nconf.get('port')}`, + }); +}); + +pubsub.on('sync:stats:end', (data) => { + stats[data.id] = data.stats; +}); + +pubsub.on('sync:stats:guests', (eventId) => { + const Sockets = require('../index'); + const guestCount = Sockets.getCountInRoom('online_guests'); + pubsub.publish(eventId, guestCount); +}); + +SocketRooms.getTotalGuestCount = function (callback) { + let count = 0; + const eventId = `sync:stats:guests:end:${utils.generateUUID()}`; + pubsub.on(eventId, (guestCount) => { + count += guestCount; + }); + + pubsub.publish('sync:stats:guests', eventId); + + setTimeout(() => { + pubsub.removeAllListeners(eventId); + callback(null, count); + }, 100); +}; + + +SocketRooms.getAll = async function () { + pubsub.publish('sync:stats:start'); + + totals.onlineGuestCount = 0; + totals.onlineRegisteredCount = 0; + totals.socketCount = 0; + totals.topics = {}; + totals.users = { + categories: 0, + recent: 0, + unread: 0, + topics: 0, + category: 0, + }; + + for (const instance of Object.values(stats)) { + totals.onlineGuestCount += instance.onlineGuestCount; + totals.onlineRegisteredCount += instance.onlineRegisteredCount; + totals.socketCount += instance.socketCount; + totals.users.categories += instance.users.categories; + totals.users.recent += instance.users.recent; + totals.users.unread += instance.users.unread; + totals.users.topics += instance.users.topics; + totals.users.category += instance.users.category; + + instance.topics.forEach((topic) => { + totals.topics[topic.tid] = totals.topics[topic.tid] || { count: 0, tid: topic.tid }; + totals.topics[topic.tid].count += topic.count; + }); + } + + let topTenTopics = []; + Object.keys(totals.topics).forEach((tid) => { + topTenTopics.push({ tid: tid, count: totals.topics[tid].count || 0 }); + }); + + topTenTopics = topTenTopics.sort((a, b) => b.count - a.count).slice(0, 10); + + const topTenTids = topTenTopics.map(topic => topic.tid); + + const titles = await topics.getTopicsFields(topTenTids, ['title']); + totals.topTenTopics = topTenTopics.map((topic, index) => { + topic.title = titles[index].title; + return topic; + }); + return totals; +}; + +SocketRooms.getOnlineUserCount = function (io) { + let count = 0; + + if (io) { + for (const [key] of io.sockets.adapter.rooms) { + if (key.startsWith('uid_')) { + count += 1; + } + } + } + + return count; +}; + +SocketRooms.getLocalStats = function () { + const Sockets = require('../index'); + const io = Sockets.server; + + const socketData = { + onlineGuestCount: 0, + onlineRegisteredCount: 0, + socketCount: 0, + users: { + categories: 0, + recent: 0, + unread: 0, + topics: 0, + category: 0, + }, + topics: {}, + }; + + if (io && io.sockets) { + socketData.onlineGuestCount = Sockets.getCountInRoom('online_guests'); + socketData.onlineRegisteredCount = SocketRooms.getOnlineUserCount(io); + socketData.socketCount = io.sockets.sockets.size; + socketData.users.categories = Sockets.getCountInRoom('categories'); + socketData.users.recent = Sockets.getCountInRoom('recent_topics'); + socketData.users.unread = Sockets.getCountInRoom('unread_topics'); + + let topTenTopics = []; + let tid; + + for (const [room, clients] of io.sockets.adapter.rooms) { + tid = room.match(/^topic_(\d+)/); + if (tid) { + socketData.users.topics += clients.size; + topTenTopics.push({ tid: tid[1], count: clients.size }); + } else if (room.match(/^category/)) { + socketData.users.category += clients.size; + } + } + + topTenTopics = topTenTopics.sort((a, b) => b.count - a.count).slice(0, 10); + socketData.topics = topTenTopics; + } + + return socketData; +}; + +require('../../promisify')(SocketRooms); diff --git a/src/socket.io/admin/settings.js b/src/socket.io/admin/settings.js new file mode 100644 index 0000000000..031a08a8e3 --- /dev/null +++ b/src/socket.io/admin/settings.js @@ -0,0 +1,24 @@ +'use strict'; + +const meta = require('../../meta'); +const events = require('../../events'); + +const Settings = module.exports; + +Settings.get = async function (socket, data) { + return await meta.settings.get(data.hash); +}; + +Settings.set = async function (socket, data) { + await meta.settings.set(data.hash, data.values); + const eventData = data.values; + eventData.type = 'settings-change'; + eventData.uid = socket.uid; + eventData.ip = socket.ip; + eventData.hash = data.hash; + await events.log(eventData); +}; + +Settings.clearSitemapCache = async function () { + require('../../sitemap').clearCache(); +}; diff --git a/src/socket.io/admin/social.js b/src/socket.io/admin/social.js new file mode 100644 index 0000000000..2358ed3dc9 --- /dev/null +++ b/src/socket.io/admin/social.js @@ -0,0 +1,9 @@ +'use strict'; + +const social = require('../../social'); + +const SocketSocial = module.exports; + +SocketSocial.savePostSharingNetworks = async function (socket, data) { + await social.setActivePostSharingNetworks(data); +}; diff --git a/src/socket.io/admin/tags.js b/src/socket.io/admin/tags.js new file mode 100644 index 0000000000..64ba212551 --- /dev/null +++ b/src/socket.io/admin/tags.js @@ -0,0 +1,29 @@ +'use strict'; + +const topics = require('../../topics'); + +const Tags = module.exports; + +Tags.create = async function (socket, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + + await topics.createEmptyTag(data.tag); +}; + +Tags.rename = async function (socket, data) { + if (!Array.isArray(data)) { + throw new Error('[[error:invalid-data]]'); + } + + await topics.renameTags(data); +}; + +Tags.deleteTags = async function (socket, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + + await topics.deleteTags(data.tags); +}; diff --git a/src/socket.io/admin/themes.js b/src/socket.io/admin/themes.js new file mode 100644 index 0000000000..27260e0697 --- /dev/null +++ b/src/socket.io/admin/themes.js @@ -0,0 +1,24 @@ +'use strict'; + +const meta = require('../../meta'); +const widgets = require('../../widgets'); + +const Themes = module.exports; + +Themes.getInstalled = async function () { + return await meta.themes.get(); +}; + +Themes.set = async function (socket, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + if (data.type === 'local') { + await widgets.reset(); + } + + data.ip = socket.ip; + data.uid = socket.uid; + + await meta.themes.set(data); +}; diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js new file mode 100644 index 0000000000..e0e498372a --- /dev/null +++ b/src/socket.io/admin/user.js @@ -0,0 +1,165 @@ +'use strict'; + +const async = require('async'); +const winston = require('winston'); + +const db = require('../../database'); +const groups = require('../../groups'); +const user = require('../../user'); +const events = require('../../events'); +const translator = require('../../translator'); +const sockets = require('..'); + +const User = module.exports; + +User.makeAdmins = async function (socket, uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + const isMembersOfBanned = await groups.isMembers(uids, groups.BANNED_USERS); + if (isMembersOfBanned.includes(true)) { + throw new Error('[[error:cant-make-banned-users-admin]]'); + } + for (const uid of uids) { + /* eslint-disable no-await-in-loop */ + await groups.join('administrators', uid); + await events.log({ + type: 'user-makeAdmin', + uid: socket.uid, + targetUid: uid, + ip: socket.ip, + }); + } +}; + +User.removeAdmins = async function (socket, uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + for (const uid of uids) { + /* eslint-disable no-await-in-loop */ + const count = await groups.getMemberCount('administrators'); + if (count === 1) { + throw new Error('[[error:cant-remove-last-admin]]'); + } + await groups.leave('administrators', uid); + await events.log({ + type: 'user-removeAdmin', + uid: socket.uid, + targetUid: uid, + ip: socket.ip, + }); + } +}; + +User.resetLockouts = async function (socket, uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + await Promise.all(uids.map(uid => user.auth.resetLockout(uid))); +}; + +User.validateEmail = async function (socket, uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + + for (const uid of uids) { + await user.email.confirmByUid(uid); + } +}; + +User.sendValidationEmail = async function (socket, uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + + const failed = []; + let errorLogged = false; + await async.eachLimit(uids, 50, async (uid) => { + await user.email.sendValidationEmail(uid, { force: true }).catch((err) => { + if (!errorLogged) { + winston.error(`[user.create] Validation email failed to send\n[emailer.send] ${err.stack}`); + errorLogged = true; + } + + failed.push(uid); + }); + }); + + if (failed.length) { + throw Error(`Email sending failed for the following uids, check server logs for more info: ${failed.join(',')}`); + } +}; + +User.sendPasswordResetEmail = async function (socket, uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + + uids = uids.filter(uid => parseInt(uid, 10)); + + await Promise.all(uids.map(async (uid) => { + const userData = await user.getUserFields(uid, ['email', 'username']); + if (!userData.email) { + throw new Error(`[[error:user-doesnt-have-email, ${userData.username}]]`); + } + await user.reset.send(userData.email); + })); +}; + +User.forcePasswordReset = async function (socket, uids) { + if (!Array.isArray(uids)) { + throw new Error('[[error:invalid-data]]'); + } + + uids = uids.filter(uid => parseInt(uid, 10)); + + await db.setObjectField(uids.map(uid => `user:${uid}`), 'passwordExpiry', Date.now()); + await user.auth.revokeAllSessions(uids); + uids.forEach(uid => sockets.in(`uid_${uid}`).emit('event:logout')); +}; + +User.restartJobs = async function () { + user.startJobs(); +}; + +User.loadGroups = async function (socket, uids) { + const [userData, groupData] = await Promise.all([ + user.getUsersData(uids), + groups.getUserGroupsFromSet('groups:createtime', uids), + ]); + userData.forEach((data, index) => { + data.groups = groupData[index].filter(group => !groups.isPrivilegeGroup(group.name)); + data.groups.forEach((group) => { + group.nameEscaped = translator.escape(group.displayName); + }); + }); + return { users: userData }; +}; + +User.exportUsersCSV = async function (socket) { + await events.log({ + type: 'exportUsersCSV', + uid: socket.uid, + ip: socket.ip, + }); + setTimeout(async () => { + try { + await user.exportUsersCSV(); + if (socket.emit) { + socket.emit('event:export-users-csv'); + } + const notifications = require('../../notifications'); + const n = await notifications.create({ + bodyShort: '[[notifications:users-csv-exported]]', + path: '/api/admin/users/csv', + nid: 'users:csv:export', + from: socket.uid, + }); + await notifications.push(n, [socket.uid]); + } catch (err) { + winston.error(err.stack); + } + }, 0); +}; diff --git a/src/socket.io/admin/widgets.js b/src/socket.io/admin/widgets.js new file mode 100644 index 0000000000..9f67fbaaa1 --- /dev/null +++ b/src/socket.io/admin/widgets.js @@ -0,0 +1,12 @@ +'use strict'; + +const widgets = require('../../widgets'); + +const Widgets = module.exports; + +Widgets.set = async function (socket, data) { + if (!Array.isArray(data)) { + throw new Error('[[error:invalid-data]]'); + } + await widgets.setAreas(data); +}; diff --git a/src/socket.io/blacklist.js b/src/socket.io/blacklist.js new file mode 100644 index 0000000000..9b28e62a71 --- /dev/null +++ b/src/socket.io/blacklist.js @@ -0,0 +1,36 @@ + +'use strict'; + +const user = require('../user'); +const meta = require('../meta'); +const events = require('../events'); + +const SocketBlacklist = module.exports; + +SocketBlacklist.validate = async function (socket, data) { + return meta.blacklist.validate(data.rules); +}; + +SocketBlacklist.save = async function (socket, rules) { + await blacklist(socket, 'save', rules); +}; + +SocketBlacklist.addRule = async function (socket, rule) { + await blacklist(socket, 'addRule', rule); +}; + +async function blacklist(socket, method, rule) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + await meta.blacklist[method](rule); + await events.log({ + type: `ip-blacklist-${method}`, + uid: socket.uid, + ip: socket.ip, + rule: rule, + }); +} + +require('../promisify')(SocketBlacklist); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js new file mode 100644 index 0000000000..ce71fc7ff3 --- /dev/null +++ b/src/socket.io/categories.js @@ -0,0 +1,167 @@ +'use strict'; + +const categories = require('../categories'); +const privileges = require('../privileges'); +const user = require('../user'); +const topics = require('../topics'); + +const SocketCategories = module.exports; + +require('./categories/search')(SocketCategories); + +SocketCategories.getRecentReplies = async function (socket, cid) { + return await categories.getRecentReplies(cid, socket.uid, 0, 4); +}; + +SocketCategories.get = async function (socket) { + async function getCategories() { + const cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'find'); + return await categories.getCategoriesData(cids); + } + const [isAdmin, categoriesData] = await Promise.all([ + user.isAdministrator(socket.uid), + getCategories(), + ]); + return categoriesData.filter(category => category && (!category.disabled || isAdmin)); +}; + +SocketCategories.getWatchedCategories = async function (socket) { + const [categoriesData, ignoredCids] = await Promise.all([ + categories.getCategoriesByPrivilege('cid:0:children', socket.uid, 'find'), + user.getIgnoredCategories(socket.uid), + ]); + return categoriesData.filter(category => category && !ignoredCids.includes(String(category.cid))); +}; + +SocketCategories.loadMore = async function (socket, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + data.query = data.query || {}; + const [userPrivileges, settings, targetUid] = await Promise.all([ + privileges.categories.get(data.cid, socket.uid), + user.getSettings(socket.uid), + user.getUidByUserslug(data.query.author), + ]); + + if (!userPrivileges.read) { + throw new Error('[[error:no-privileges]]'); + } + + const infScrollTopicsPerPage = 20; + const sort = data.sort || data.categoryTopicSort; + + let start = Math.max(0, parseInt(data.after, 10)); + + if (data.direction === -1) { + start -= infScrollTopicsPerPage; + } + + let stop = start + infScrollTopicsPerPage - 1; + + start = Math.max(0, start); + stop = Math.max(0, stop); + const result = await categories.getCategoryTopics({ + uid: socket.uid, + cid: data.cid, + start: start, + stop: stop, + sort: sort, + settings: settings, + query: data.query, + tag: data.query.tag, + targetUid: targetUid, + }); + categories.modifyTopicsByPrivilege(result.topics, userPrivileges); + + result.privileges = userPrivileges; + result.template = { + category: true, + name: 'category', + }; + return result; +}; + +SocketCategories.getTopicCount = async function (socket, cid) { + return await categories.getCategoryField(cid, 'topic_count'); +}; + +SocketCategories.getCategoriesByPrivilege = async function (socket, privilege) { + return await categories.getCategoriesByPrivilege('categories:cid', socket.uid, privilege); +}; + +SocketCategories.getMoveCategories = async function (socket, data) { + return await SocketCategories.getSelectCategories(socket, data); +}; + +SocketCategories.getSelectCategories = async function (socket) { + const [isAdmin, categoriesData] = await Promise.all([ + user.isAdministrator(socket.uid), + categories.buildForSelect(socket.uid, 'find', ['disabled', 'link']), + ]); + return categoriesData.filter(category => category && (!category.disabled || isAdmin) && !category.link); +}; + +SocketCategories.setWatchState = async function (socket, data) { + if (!data || !data.cid || !data.state) { + throw new Error('[[error:invalid-data]]'); + } + return await ignoreOrWatch(async (uid, cids) => { + await user.setCategoryWatchState(uid, cids, categories.watchStates[data.state]); + }, socket, data); +}; + +SocketCategories.watch = async function (socket, data) { + return await ignoreOrWatch(user.watchCategory, socket, data); +}; + +SocketCategories.ignore = async function (socket, data) { + return await ignoreOrWatch(user.ignoreCategory, socket, data); +}; + +async function ignoreOrWatch(fn, socket, data) { + let targetUid = socket.uid; + const cids = Array.isArray(data.cid) ? data.cid.map(cid => parseInt(cid, 10)) : [parseInt(data.cid, 10)]; + if (data.hasOwnProperty('uid')) { + targetUid = data.uid; + } + await user.isAdminOrGlobalModOrSelf(socket.uid, targetUid); + const allCids = await categories.getAllCidsFromSet('categories:cid'); + const categoryData = await categories.getCategoriesFields(allCids, ['cid', 'parentCid']); + + // filter to subcategories of cid + let cat; + do { + cat = categoryData.find(c => !cids.includes(c.cid) && cids.includes(c.parentCid)); + if (cat) { + cids.push(cat.cid); + } + } while (cat); + + await fn(targetUid, cids); + await topics.pushUnreadCount(targetUid); + return cids; +} + +SocketCategories.isModerator = async function (socket, cid) { + return await user.isModerator(socket.uid, cid); +}; + +SocketCategories.loadMoreSubCategories = async function (socket, data) { + if (!data || !data.cid || !(parseInt(data.start, 10) > 0)) { + throw new Error('[[error:invalid-data]]'); + } + const allowed = await privileges.categories.can('read', data.cid, socket.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + const category = await categories.getCategoryData(data.cid); + await categories.getChildrenTree(category, socket.uid); + const allCategories = []; + categories.flattenCategories(allCategories, category.children); + await categories.getRecentTopicReplies(allCategories, socket.uid); + const start = parseInt(data.start, 10); + return category.children.slice(start, start + category.subCategoriesPerPage); +}; + +require('../promisify')(SocketCategories); diff --git a/src/socket.io/categories/search.js b/src/socket.io/categories/search.js new file mode 100644 index 0000000000..cf7a83762e --- /dev/null +++ b/src/socket.io/categories/search.js @@ -0,0 +1,101 @@ +'use strict'; + +const _ = require('lodash'); + +const meta = require('../../meta'); +const categories = require('../../categories'); +const privileges = require('../../privileges'); +const controllersHelpers = require('../../controllers/helpers'); +const plugins = require('../../plugins'); + +module.exports = function (SocketCategories) { + // used by categorySearch module + SocketCategories.categorySearch = async function (socket, data) { + let cids = []; + let matchedCids = []; + const privilege = data.privilege || 'topics:read'; + data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map( + state => categories.watchStates[state] + ); + + if (data.search) { + ({ cids, matchedCids } = await findMatchedCids(socket.uid, data)); + } else { + cids = await loadCids(socket.uid, data.parentCid); + } + + const visibleCategories = await controllersHelpers.getVisibleCategories({ + cids, uid: socket.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid, + }); + + if (Array.isArray(data.selectedCids)) { + data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10)); + } + + let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid); + categoriesData = categoriesData.slice(0, 200); + + categoriesData.forEach((category) => { + category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false; + if (matchedCids.includes(category.cid)) { + category.match = true; + } + }); + const result = await plugins.hooks.fire('filter:categories.categorySearch', { + categories: categoriesData, + ...data, + uid: socket.uid, + }); + return result.categories; + }; + + async function findMatchedCids(uid, data) { + const result = await categories.search({ + uid: uid, + query: data.search, + qs: data.query, + paginate: false, + }); + + let matchedCids = result.categories.map(c => c.cid); + // no need to filter if all 3 states are used + const filterByWatchState = !Object.values(categories.watchStates) + .every(state => data.states.includes(state)); + + if (filterByWatchState) { + const states = await categories.getWatchState(matchedCids, uid); + matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index])); + } + + const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids)))); + const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids)))); + + return { + cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)), + matchedCids: matchedCids, + }; + } + + async function loadCids(uid, parentCid) { + let resultCids = []; + async function getCidsRecursive(cids) { + const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']); + const cidToData = _.zipObject(cids, categoryData); + await Promise.all(cids.map(async (cid) => { + const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`); + if (allChildCids.length) { + const childCids = await privileges.categories.filterCids('find', allChildCids, uid); + resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage)); + await getCidsRecursive(childCids); + } + })); + } + + const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`); + const rootCids = await privileges.categories.filterCids('find', allRootCids, uid); + const pageCids = rootCids.slice(0, meta.config.categoriesPerPage); + resultCids = pageCids; + await getCidsRecursive(pageCids); + return resultCids; + } +}; diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js new file mode 100644 index 0000000000..760a2ecf8e --- /dev/null +++ b/src/socket.io/groups.js @@ -0,0 +1,291 @@ +'use strict'; + +const groups = require('../groups'); +const user = require('../user'); +const utils = require('../utils'); +const events = require('../events'); +const privileges = require('../privileges'); + +const SocketGroups = module.exports; + +SocketGroups.before = async (socket, method, data) => { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } +}; + +SocketGroups.addMember = async (socket, data) => { + await isOwner(socket, data); + if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) { + throw new Error('[[error:not-allowed]]'); + } + if (!data.uid) { + throw new Error('[[error:invalid-data]]'); + } + data.uid = !Array.isArray(data.uid) ? [data.uid] : data.uid; + if (data.uid.filter(uid => !(parseInt(uid, 10) > 0)).length) { + throw new Error('[[error:invalid-uid]]'); + } + for (const uid of data.uid) { + // eslint-disable-next-line no-await-in-loop + await groups.join(data.groupName, uid); + } + + logGroupEvent(socket, 'group-add-member', { + groupName: data.groupName, + targetUid: String(data.uid), + }); +}; + +async function isOwner(socket, data) { + if (typeof data.groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } + const results = await utils.promiseParallel({ + hasAdminPrivilege: privileges.admin.can('admin:groups', socket.uid), + isGlobalModerator: user.isGlobalModerator(socket.uid), + isOwner: groups.ownership.isOwner(socket.uid, data.groupName), + group: groups.getGroupData(data.groupName), + }); + + const isOwner = results.isOwner || + results.hasAdminPrivilege || + (results.isGlobalModerator && !results.group.system); + if (!isOwner) { + throw new Error('[[error:no-privileges]]'); + } +} + +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); + if (!invited) { + throw new Error('[[error:not-invited]]'); + } +} + +SocketGroups.accept = async (socket, data) => { + await isOwner(socket, data); + await groups.acceptMembership(data.groupName, data.toUid); + logGroupEvent(socket, 'group-accept-membership', { + groupName: data.groupName, + targetUid: data.toUid, + }); +}; + +SocketGroups.reject = async (socket, data) => { + await isOwner(socket, data); + await groups.rejectMembership(data.groupName, data.toUid); + logGroupEvent(socket, 'group-reject-membership', { + groupName: data.groupName, + targetUid: data.toUid, + }); +}; + +SocketGroups.acceptAll = async (socket, data) => { + await isOwner(socket, data); + await acceptRejectAll(SocketGroups.accept, socket, data); +}; + +SocketGroups.rejectAll = async (socket, data) => { + await isOwner(socket, data); + await acceptRejectAll(SocketGroups.reject, 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); + await Promise.all(uids.map(async (uid) => { + await method(socket, { groupName: data.groupName, toUid: uid }); + })); +} + +SocketGroups.issueInvite = async (socket, data) => { + await isOwner(socket, data); + await groups.invite(data.groupName, data.toUid); + logGroupEvent(socket, 'group-invite', { + groupName: data.groupName, + targetUid: data.toUid, + }); +}; + +SocketGroups.issueMassInvite = async (socket, data) => { + await isOwner(socket, data); + if (!data || !data.usernames || !data.groupName) { + throw new Error('[[error:invalid-data]]'); + } + let usernames = String(data.usernames).split(','); + usernames = usernames.map(username => username && username.trim()); + + let uids = await user.getUidsByUsernames(usernames); + uids = uids.filter(uid => !!uid && parseInt(uid, 10)); + + await groups.invite(data.groupName, uids); + + for (const uid of uids) { + logGroupEvent(socket, 'group-invite', { + groupName: data.groupName, + targetUid: uid, + }); + } +}; + +SocketGroups.rescindInvite = async (socket, data) => { + await isOwner(socket, data); + await groups.rejectMembership(data.groupName, data.toUid); +}; + +SocketGroups.acceptInvite = async (socket, data) => { + await isInvited(socket, data); + await groups.acceptMembership(data.groupName, socket.uid); + logGroupEvent(socket, 'group-invite-accept', { + groupName: data.groupName, + }); +}; + +SocketGroups.rejectInvite = async (socket, data) => { + await isInvited(socket, data); + await groups.rejectMembership(data.groupName, socket.uid); + logGroupEvent(socket, 'group-invite-reject', { + groupName: data.groupName, + }); +}; + +SocketGroups.kick = async (socket, data) => { + await isOwner(socket, data); + if (socket.uid === parseInt(data.uid, 10)) { + throw new Error('[[error:cant-kick-self]]'); + } + + const isOwnerBit = await groups.ownership.isOwner(data.uid, data.groupName); + await groups.kick(data.uid, data.groupName, isOwnerBit); + logGroupEvent(socket, 'group-kick', { + groupName: data.groupName, + targetUid: data.uid, + }); +}; + +SocketGroups.search = async (socket, data) => { + data.options = data.options || {}; + + if (!data.query) { + const groupsPerPage = 15; + const groupData = await groups.getGroupsBySort(data.options.sort, 0, groupsPerPage - 1); + return groupData; + } + data.options.filterHidden = data.options.filterHidden || !await user.isAdministrator(socket.uid); + return await groups.search(data.query, data.options); +}; + +SocketGroups.loadMore = async (socket, data) => { + if (!data.sort || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { + throw new Error('[[error:invalid-data]]'); + } + + const groupsPerPage = 10; + const start = parseInt(data.after, 10); + const stop = start + groupsPerPage - 1; + const groupData = await groups.getGroupsBySort(data.sort, start, stop); + return { groups: groupData, nextStart: stop + 1 }; +}; + +SocketGroups.searchMembers = async (socket, data) => { + if (!data.groupName) { + throw new Error('[[error:invalid-data]]'); + } + await canSearchMembers(socket.uid, data.groupName); + if (!await privileges.global.can('search:users', socket.uid)) { + throw new Error('[[error:no-privileges]]'); + } + return await groups.searchMembers({ + uid: socket.uid, + query: data.query, + groupName: data.groupName, + }); +}; + +SocketGroups.loadMoreMembers = async (socket, data) => { + if (!data.groupName || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { + throw new Error('[[error:invalid-data]]'); + } + await canSearchMembers(socket.uid, data.groupName); + data.after = parseInt(data.after, 10); + const users = await groups.getOwnersAndMembers(data.groupName, socket.uid, data.after, data.after + 9); + return { + users: users, + nextStart: data.after + 10, + }; +}; + +async function canSearchMembers(uid, groupName) { + const [isHidden, isMember, hasAdminPrivilege, isGlobalMod, viewGroups] = await Promise.all([ + groups.isHidden(groupName), + groups.isMember(uid, groupName), + privileges.admin.can('admin:groups', uid), + user.isGlobalModerator(uid), + privileges.global.can('view:groups', uid), + ]); + + if (!viewGroups || (isHidden && !isMember && !hasAdminPrivilege && !isGlobalMod)) { + throw new Error('[[error:no-privileges]]'); + } +} + +SocketGroups.cover = {}; + +SocketGroups.cover.update = async (socket, data) => { + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + if (data.file || (!data.imageData && !data.position)) { + throw new Error('[[error:invalid-data]]'); + } + await canModifyGroup(socket.uid, data.groupName); + return await groups.updateCover(socket.uid, { + groupName: data.groupName, + imageData: data.imageData, + position: data.position, + }); +}; + +SocketGroups.cover.remove = async (socket, data) => { + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + + await canModifyGroup(socket.uid, data.groupName); + await groups.removeCover({ + groupName: data.groupName, + }); +}; + +async function canModifyGroup(uid, groupName) { + if (typeof groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } + const results = await utils.promiseParallel({ + isOwner: groups.ownership.isOwner(uid, groupName), + system: groups.getGroupField(groupName, 'system'), + hasAdminPrivilege: privileges.admin.can('admin:groups', uid), + isGlobalMod: user.isGlobalModerator(uid), + }); + + if (!(results.isOwner || results.hasAdminPrivilege || (results.isGlobalMod && !results.system))) { + throw new Error('[[error:no-privileges]]'); + } +} + +function logGroupEvent(socket, event, additional) { + events.log({ + type: event, + uid: socket.uid, + ip: socket.ip, + ...additional, + }); +} + +require('../promisify')(SocketGroups); diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js new file mode 100644 index 0000000000..2be0f0c34b --- /dev/null +++ b/src/socket.io/helpers.js @@ -0,0 +1,200 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const websockets = require('./index'); +const user = require('../user'); +const posts = require('../posts'); +const topics = require('../topics'); +const categories = require('../categories'); +const privileges = require('../privileges'); +const notifications = require('../notifications'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const batch = require('../batch'); + +const SocketHelpers = module.exports; + +SocketHelpers.notifyNew = async function (uid, type, result) { + let uids = await user.getUidsFromSet('users:online', 0, -1); + uids = uids.filter(toUid => parseInt(toUid, 10) !== uid); + await batch.processArray(uids, async (uids) => { + await notifyUids(uid, uids, type, result); + }, { + interval: 1000, + }); +}; + +async function notifyUids(uid, uids, type, result) { + const post = result.posts[0]; + const { tid } = post.topic; + const { cid } = post.topic; + uids = await privileges.topics.filterUids('topics:read', tid, uids); + const watchStateUids = uids; + + const watchStates = await getWatchStates(watchStateUids, tid, cid); + + const categoryWatchStates = _.zipObject(watchStateUids, watchStates.categoryWatchStates); + const topicFollowState = _.zipObject(watchStateUids, watchStates.topicFollowed); + uids = filterTidCidIgnorers(watchStateUids, watchStates); + uids = await user.blocks.filterUids(uid, uids); + uids = await user.blocks.filterUids(post.topic.uid, uids); + const data = await plugins.hooks.fire('filter:sockets.sendNewPostToUids', { + uidsTo: uids, + uidFrom: uid, + type: type, + post: post, + }); + + post.ip = undefined; + + data.uidsTo.forEach((toUid) => { + post.categoryWatchState = categoryWatchStates[toUid]; + post.topic.isFollowing = topicFollowState[toUid]; + websockets.in(`uid_${toUid}`).emit('event:new_post', result); + if (result.topic && type === 'newTopic') { + websockets.in(`uid_${toUid}`).emit('event:new_topic', result.topic); + } + }); +} + +async function getWatchStates(uids, tid, cid) { + return await utils.promiseParallel({ + topicFollowed: db.isSetMembers(`tid:${tid}:followers`, uids), + topicIgnored: db.isSetMembers(`tid:${tid}:ignorers`, uids), + categoryWatchStates: categories.getUidsWatchStates(cid, uids), + }); +} + +function filterTidCidIgnorers(uids, watchStates) { + return uids.filter((uid, index) => watchStates.topicFollowed[index] || + (!watchStates.topicIgnored[index] && + watchStates.categoryWatchStates[index] !== categories.watchStates.ignoring)); +} + +SocketHelpers.sendNotificationToPostOwner = async function (pid, fromuid, command, notification) { + if (!pid || !fromuid || !notification) { + return; + } + fromuid = parseInt(fromuid, 10); + const postData = await posts.getPostFields(pid, ['tid', 'uid', 'content']); + const [canRead, isIgnoring] = await Promise.all([ + privileges.posts.can('topics:read', pid, postData.uid), + topics.isIgnoring([postData.tid], postData.uid), + ]); + if (!canRead || isIgnoring[0] || !postData.uid || fromuid === postData.uid) { + return; + } + const [userData, topicTitle, postObj] = await Promise.all([ + user.getUserFields(fromuid, ['username']), + topics.getTopicField(postData.tid, 'title'), + posts.parsePost(postData), + ]); + + const { displayname } = userData; + + const title = utils.decodeHTMLEntities(topicTitle); + const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + + const notifObj = await notifications.create({ + type: command, + bodyShort: `[[${notification}, ${displayname}, ${titleEscaped}]]`, + bodyLong: postObj.content, + pid: pid, + tid: postData.tid, + path: `/post/${pid}`, + nid: `${command}:post:${pid}:uid:${fromuid}`, + from: fromuid, + mergeId: `${notification}|${pid}`, + topicTitle: topicTitle, + }); + + notifications.push(notifObj, [postData.uid]); +}; + + +SocketHelpers.sendNotificationToTopicOwner = async function (tid, fromuid, command, notification) { + if (!tid || !fromuid || !notification) { + return; + } + + fromuid = parseInt(fromuid, 10); + + const [userData, topicData] = await Promise.all([ + user.getUserFields(fromuid, ['username']), + topics.getTopicFields(tid, ['uid', 'slug', 'title']), + ]); + + if (fromuid === topicData.uid) { + return; + } + + const { displayname } = userData; + + const ownerUid = topicData.uid; + const title = utils.decodeHTMLEntities(topicData.title); + const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + + const notifObj = await notifications.create({ + bodyShort: `[[${notification}, ${displayname}, ${titleEscaped}]]`, + path: `/topic/${topicData.slug}`, + nid: `${command}:tid:${tid}:uid:${fromuid}`, + from: fromuid, + }); + + if (ownerUid) { + notifications.push(notifObj, [ownerUid]); + } +}; + +SocketHelpers.upvote = async function (data, notification) { + if (!data || !data.post || !data.post.uid || !data.post.votes || !data.post.pid || !data.fromuid) { + return; + } + + const { votes } = data.post; + const touid = data.post.uid; + const { fromuid } = data; + const { pid } = data.post; + + const shouldNotify = { + all: function () { + return votes > 0; + }, + first: function () { + return votes === 1; + }, + everyTen: function () { + return votes > 0 && votes % 10 === 0; + }, + threshold: function () { + return [1, 5, 10, 25].includes(votes) || (votes >= 50 && votes % 50 === 0); + }, + logarithmic: function () { + return votes > 1 && Math.log10(votes) % 1 === 0; + }, + disabled: function () { + return false; + }, + }; + const settings = await user.getSettings(touid); + const should = shouldNotify[settings.upvoteNotifFreq] || shouldNotify.all; + + if (should()) { + SocketHelpers.sendNotificationToPostOwner(pid, fromuid, 'upvote', notification); + } +}; + +SocketHelpers.rescindUpvoteNotification = async function (pid, fromuid) { + await notifications.rescind(`upvote:post:${pid}:uid:${fromuid}`); + const uid = await posts.getPostField(pid, 'uid'); + const count = await user.notifications.getUnreadCount(uid); + websockets.in(`uid_${uid}`).emit('event:notifications.updateCount', count); +}; + +SocketHelpers.emitToUids = async function (event, data, uids) { + uids.forEach(toUid => websockets.in(`uid_${toUid}`).emit(event, data)); +}; + +require('../promisify')(SocketHelpers); diff --git a/src/socket.io/index.js b/src/socket.io/index.js new file mode 100644 index 0000000000..7e2262ff87 --- /dev/null +++ b/src/socket.io/index.js @@ -0,0 +1,278 @@ +'use strict'; + +const os = require('os'); +const nconf = require('nconf'); +const winston = require('winston'); +const util = require('util'); +const validator = require('validator'); +const cookieParser = require('cookie-parser')(nconf.get('secret')); + +const db = require('../database'); +const user = require('../user'); +const logger = require('../logger'); +const plugins = require('../plugins'); +const ratelimit = require('../middleware/ratelimit'); + +const Namespaces = Object.create(null); + +const Sockets = module.exports; + +Sockets.init = async function (server) { + requireModules(); + + const SocketIO = require('socket.io').Server; + const io = new SocketIO({ + path: `${nconf.get('relative_path')}/socket.io`, + }); + + if (nconf.get('isCluster')) { + if (nconf.get('redis')) { + const adapter = await require('../database/redis').socketAdapter(); + io.adapter(adapter); + } else { + winston.warn('clustering detected, you should setup redis!'); + } + } + + io.use(authorize); + + io.on('connection', onConnection); + + const opts = { + transports: nconf.get('socket.io:transports') || ['polling', 'websocket'], + cookie: false, + }; + /* + * Restrict socket.io listener to cookie domain. If none is set, infer based on url. + * Production only so you don't get accidentally locked out. + * Can be overridden via config (socket.io:origins) + */ + if (process.env.NODE_ENV !== 'development' || nconf.get('socket.io:cors')) { + const origins = nconf.get('socket.io:origins'); + opts.cors = nconf.get('socket.io:cors') || { + origin: origins, + methods: ['GET', 'POST'], + allowedHeaders: ['content-type'], + }; + winston.info(`[socket.io] Restricting access to origin: ${origins}`); + } + + io.listen(server, opts); + Sockets.server = io; +}; + +function onConnection(socket) { + socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0]; + socket.request.ip = socket.ip; + logger.io_one(socket, socket.uid); + + onConnect(socket); + socket.onAny((event, ...args) => { + const payload = { data: [event].concat(args) }; + const als = require('../als'); + als.run({ uid: socket.uid }, onMessage, socket, payload); + }); + + socket.on('disconnect', () => { + onDisconnect(socket); + }); +} + +function onDisconnect(socket) { + require('./uploads').clear(socket.id); + plugins.hooks.fire('action:sockets.disconnect', { socket: socket }); +} + +async function onConnect(socket) { + try { + await validateSession(socket, '[[error:invalid-session]]'); + } catch (e) { + if (e.message === '[[error:invalid-session]]') { + socket.emit('event:invalid_session'); + } + + return; + } + + if (socket.uid) { + socket.join(`uid_${socket.uid}`); + socket.join('online_users'); + } else { + socket.join('online_guests'); + } + + socket.join(`sess_${socket.request.signedCookies[nconf.get('sessionKey')]}`); + socket.emit('checkSession', socket.uid); + socket.emit('setHostname', os.hostname()); + plugins.hooks.fire('action:sockets.connect', { socket: socket }); +} + +async function onMessage(socket, payload) { + if (!payload.data.length) { + return winston.warn('[socket.io] Empty payload'); + } + + const eventName = payload.data[0]; + const params = typeof payload.data[1] === 'function' ? {} : payload.data[1]; + const callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {}; + + if (!eventName) { + return winston.warn('[socket.io] Empty method name'); + } + + const parts = eventName.toString().split('.'); + const namespace = parts[0]; + const methodToCall = parts.reduce((prev, cur) => { + if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) { + return prev[cur]; + } + return null; + }, Namespaces); + + if (!methodToCall || typeof methodToCall !== 'function') { + if (process.env.NODE_ENV === 'development') { + winston.warn(`[socket.io] Unrecognized message: ${eventName}`); + } + const escapedName = validator.escape(String(eventName)); + return callback({ message: `[[error:invalid-event, ${escapedName}]]` }); + } + + socket.previousEvents = socket.previousEvents || []; + socket.previousEvents.push(eventName); + if (socket.previousEvents.length > 20) { + socket.previousEvents.shift(); + } + + if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { + winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`); + return socket.disconnect(); + } + + try { + await checkMaintenance(socket); + await validateSession(socket, '[[error:revalidate-failure]]'); + + if (Namespaces[namespace].before) { + await Namespaces[namespace].before(socket, eventName, params); + } + + if (methodToCall.constructor && methodToCall.constructor.name === 'AsyncFunction') { + const result = await methodToCall(socket, params); + callback(null, result); + } else { + methodToCall(socket, params, (err, result) => { + callback(err ? { message: err.message } : null, result); + }); + } + } catch (err) { + winston.error(`${eventName}\n${err.stack ? err.stack : err.message}`); + callback({ message: err.message }); + } +} + +function requireModules() { + const modules = [ + 'admin', 'categories', 'groups', 'meta', 'modules', + 'notifications', 'plugins', 'posts', 'topics', 'user', + 'blacklist', 'uploads', + ]; + + modules.forEach((module) => { + Namespaces[module] = require(`./${module}`); + }); +} + +async function checkMaintenance(socket) { + const meta = require('../meta'); + if (!meta.config.maintenanceMode) { + return; + } + const isAdmin = await user.isAdministrator(socket.uid); + if (isAdmin) { + return; + } + const validator = require('validator'); + throw new Error(`[[pages:maintenance.text, ${validator.escape(String(meta.config.title || 'NodeBB'))}]]`); +} + +const getSessionAsync = util.promisify( + (sid, callback) => db.sessionStore.get(sid, (err, sessionObj) => callback(err, sessionObj || null)) +); + +async function validateSession(socket, errorMsg) { + const req = socket.request; + const { sessionId } = await plugins.hooks.fire('filter:sockets.sessionId', { + sessionId: req.signedCookies ? req.signedCookies[nconf.get('sessionKey')] : null, + request: req, + }); + + if (!sessionId) { + return; + } + + const sessionData = await getSessionAsync(sessionId); + + if (!sessionData) { + throw new Error(errorMsg); + } + + await plugins.hooks.fire('static:sockets.validateSession', { + req: req, + socket: socket, + session: sessionData, + }); +} + +const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err))); + +async function authorize(socket, callback) { + const { request } = socket; + + if (!request) { + return callback(new Error('[[error:not-authorized]]')); + } + + await cookieParserAsync(request); + + const { sessionId } = await plugins.hooks.fire('filter:sockets.sessionId', { + sessionId: request.signedCookies ? request.signedCookies[nconf.get('sessionKey')] : null, + request: request, + }); + + const sessionData = await getSessionAsync(sessionId); + + if (sessionData && sessionData.passport && sessionData.passport.user) { + request.session = sessionData; + socket.uid = parseInt(sessionData.passport.user, 10); + } else { + socket.uid = 0; + } + request.uid = socket.uid; + callback(); +} + +Sockets.in = function (room) { + return Sockets.server && Sockets.server.in(room); +}; + +Sockets.getUserSocketCount = function (uid) { + return Sockets.getCountInRoom(`uid_${uid}`); +}; + +Sockets.getCountInRoom = function (room) { + if (!Sockets.server) { + return 0; + } + const roomMap = Sockets.server.sockets.adapter.rooms.get(room); + return roomMap ? roomMap.size : 0; +}; + +Sockets.warnDeprecated = (socket, replacement) => { + if (socket.previousEvents && socket.emit) { + socket.emit('event:deprecated_call', { + eventName: socket.previousEvents[socket.previousEvents.length - 1], + replacement: replacement, + }); + } + winston.warn(`[deprecated]\n ${new Error('-').stack.split('\n').slice(2, 5).join('\n')}\n use ${replacement}`); +}; diff --git a/src/socket.io/meta.js b/src/socket.io/meta.js new file mode 100644 index 0000000000..3591394a93 --- /dev/null +++ b/src/socket.io/meta.js @@ -0,0 +1,63 @@ +'use strict'; + + +const user = require('../user'); +const topics = require('../topics'); + +const SocketMeta = { + rooms: {}, +}; + +SocketMeta.reconnected = function (socket, data, callback) { + callback = callback || function () {}; + if (socket.uid) { + topics.pushUnreadCount(socket.uid); + user.notifications.pushCount(socket.uid); + } + callback(); +}; + +/* Rooms */ + +SocketMeta.rooms.enter = function (socket, data, callback) { + if (!socket.uid) { + return callback(); + } + + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + if (data.enter) { + data.enter = data.enter.toString(); + } + + if (data.enter && data.enter.startsWith('uid_') && data.enter !== `uid_${socket.uid}`) { + return callback(new Error('[[error:not-allowed]]')); + } + + leaveCurrentRoom(socket); + + if (data.enter) { + socket.join(data.enter); + socket.currentRoom = data.enter; + } + callback(); +}; + +SocketMeta.rooms.leaveCurrent = function (socket, data, callback) { + if (!socket.uid || !socket.currentRoom) { + return callback(); + } + leaveCurrentRoom(socket); + callback(); +}; + +function leaveCurrentRoom(socket) { + if (socket.currentRoom) { + socket.leave(socket.currentRoom); + socket.currentRoom = ''; + } +} + +module.exports = SocketMeta; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js new file mode 100644 index 0000000000..26659756ab --- /dev/null +++ b/src/socket.io/modules.js @@ -0,0 +1,254 @@ +'use strict'; + +const db = require('../database'); +const notifications = require('../notifications'); +const Messaging = require('../messaging'); +const utils = require('../utils'); +const server = require('./index'); +const user = require('../user'); +const privileges = require('../privileges'); + +const sockets = require('.'); +const api = require('../api'); + +const SocketModules = module.exports; + +SocketModules.chats = {}; +SocketModules.settings = {}; + +/* Chat */ + +SocketModules.chats.getRaw = async function (socket, data) { + if (!data || !data.hasOwnProperty('mid')) { + throw new Error('[[error:invalid-data]]'); + } + const roomId = await Messaging.getMessageField(data.mid, 'roomId'); + const [isAdmin, hasMessage, inRoom] = await Promise.all([ + user.isAdministrator(socket.uid), + db.isSortedSetMember(`uid:${socket.uid}:chat:room:${roomId}:mids`, data.mid), + Messaging.isUserInRoom(socket.uid, roomId), + ]); + + if (!isAdmin && (!inRoom || !hasMessage)) { + throw new Error('[[error:not-allowed]]'); + } + + return await Messaging.getMessageField(data.mid, 'content'); +}; + +SocketModules.chats.isDnD = async function (socket, uid) { + const status = await db.getObjectField(`user:${uid}`, 'status'); + return status === 'dnd'; +}; + +SocketModules.chats.newRoom = async function (socket, data) { + sockets.warnDeprecated(socket, 'POST /api/v3/chats'); + + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + + const roomObj = await api.chats.create(socket, { + uids: [data.touid], + }); + return roomObj.roomId; +}; + +SocketModules.chats.send = async function (socket, data) { + sockets.warnDeprecated(socket, 'POST /api/v3/chats/:roomId'); + + if (!data || !data.roomId || !socket.uid) { + throw new Error('[[error:invalid-data]]'); + } + + const canChat = await privileges.global.can('chat', socket.uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + + return api.chats.post(socket, data); +}; + +SocketModules.chats.loadRoom = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId'); + + if (!data || !data.roomId) { + throw new Error('[[error:invalid-data]]'); + } + + return await Messaging.loadRoom(socket.uid, data); +}; + +SocketModules.chats.getUsersInRoom = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/users'); + + if (!data || !data.roomId) { + throw new Error('[[error:invalid-data]]'); + } + const isUserInRoom = await Messaging.isUserInRoom(socket.uid, data.roomId); + if (!isUserInRoom) { + throw new Error('[[error:no-privileges]]'); + } + + return api.chats.users(socket, data); +}; + +SocketModules.chats.addUserToRoom = async function (socket, data) { + sockets.warnDeprecated(socket, 'POST /api/v3/chats/:roomId/users'); + + if (!data || !data.roomId || !data.username) { + throw new Error('[[error:invalid-data]]'); + } + + const canChat = await privileges.global.can('chat', socket.uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + + // Revised API now takes uids, not usernames + data.uids = [await user.getUidByUsername(data.username)]; + delete data.username; + + await api.chats.invite(socket, data); +}; + +SocketModules.chats.removeUserFromRoom = async function (socket, data) { + sockets.warnDeprecated(socket, 'DELETE /api/v3/chats/:roomId/users OR DELETE /api/v3/chats/:roomId/users/:uid'); + + if (!data || !data.roomId) { + throw new Error('[[error:invalid-data]]'); + } + + // Revised API can accept multiple uids now + data.uids = [data.uid]; + delete data.uid; + + await api.chats.kick(socket, data); +}; + +SocketModules.chats.leave = async function (socket, roomid) { + sockets.warnDeprecated(socket, 'DELETE /api/v3/chats/:roomId/users OR DELETE /api/v3/chats/:roomId/users/:uid'); + + if (!socket.uid || !roomid) { + throw new Error('[[error:invalid-data]]'); + } + + await Messaging.leaveRoom([socket.uid], roomid); +}; + +SocketModules.chats.edit = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v3/chats/:roomId/:mid'); + + if (!data || !data.roomId || !data.message) { + throw new Error('[[error:invalid-data]]'); + } + await Messaging.canEdit(data.mid, socket.uid); + await Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message); +}; + +SocketModules.chats.delete = async function (socket, data) { + sockets.warnDeprecated(socket, 'DELETE /api/v3/chats/:roomId/:mid'); + + if (!data || !data.roomId || !data.messageId) { + throw new Error('[[error:invalid-data]]'); + } + await Messaging.canDelete(data.messageId, socket.uid); + await Messaging.deleteMessage(data.messageId, socket.uid); +}; + +SocketModules.chats.restore = async function (socket, data) { + sockets.warnDeprecated(socket, 'POST /api/v3/chats/:roomId/:mid'); + + if (!data || !data.roomId || !data.messageId) { + throw new Error('[[error:invalid-data]]'); + } + await Messaging.canDelete(data.messageId, socket.uid); + await Messaging.restoreMessage(data.messageId, socket.uid); +}; + +SocketModules.chats.canMessage = async function (socket, roomId) { + await Messaging.canMessageRoom(socket.uid, roomId); +}; + +SocketModules.chats.markRead = async function (socket, roomId) { + if (!socket.uid || !roomId) { + throw new Error('[[error:invalid-data]]'); + } + const [uidsInRoom] = await Promise.all([ + Messaging.getUidsInRoom(roomId, 0, -1), + Messaging.markRead(socket.uid, roomId), + ]); + + Messaging.pushUnreadCount(socket.uid); + server.in(`uid_${socket.uid}`).emit('event:chats.markedAsRead', { roomId: roomId }); + + if (!uidsInRoom.includes(String(socket.uid))) { + return; + } + + // Mark notification read + const nids = uidsInRoom.filter(uid => parseInt(uid, 10) !== socket.uid) + .map(uid => `chat_${uid}_${roomId}`); + + await notifications.markReadMultiple(nids, socket.uid); + await user.notifications.pushCount(socket.uid); +}; + +SocketModules.chats.markAllRead = async function (socket) { + await Messaging.markAllRead(socket.uid); + Messaging.pushUnreadCount(socket.uid); +}; + +SocketModules.chats.renameRoom = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v3/chats/:roomId'); + + if (!data || !data.roomId || !data.newName) { + throw new Error('[[error:invalid-data]]'); + } + + data.name = data.newName; + delete data.newName; + await api.chats.rename(socket, data); +}; + +SocketModules.chats.getRecentChats = async function (socket, data) { + if (!data || !utils.isNumber(data.after) || !utils.isNumber(data.uid)) { + throw new Error('[[error:invalid-data]]'); + } + const start = parseInt(data.after, 10); + const stop = start + 9; + return await Messaging.getRecentChats(socket.uid, data.uid, start, stop); +}; + +SocketModules.chats.hasPrivateChat = async function (socket, uid) { + if (socket.uid <= 0 || uid <= 0) { + throw new Error('[[error:invalid-data]]'); + } + return await Messaging.hasPrivateChat(socket.uid, uid); +}; + +SocketModules.chats.getMessages = async function (socket, data) { + sockets.warnDeprecated(socket, 'GET /api/v3/chats/:roomId/messages'); + + if (!socket.uid || !data || !data.uid || !data.roomId) { + throw new Error('[[error:invalid-data]]'); + } + + return await Messaging.getMessages({ + callerUid: socket.uid, + uid: data.uid, + roomId: data.roomId, + start: parseInt(data.start, 10) || 0, + count: 50, + }); +}; + +SocketModules.chats.getIP = async function (socket, mid) { + const allowed = await privileges.global.can('view:users:info', socket.uid); + if (!allowed) { + throw new Error('[[error:no-privilege]]'); + } + return await Messaging.getMessageField(mid, 'ip'); +}; + +require('../promisify')(SocketModules); diff --git a/src/socket.io/notifications.js b/src/socket.io/notifications.js new file mode 100644 index 0000000000..1f9236afb5 --- /dev/null +++ b/src/socket.io/notifications.js @@ -0,0 +1,42 @@ +'use strict'; + +const user = require('../user'); +const notifications = require('../notifications'); + +const SocketNotifs = module.exports; + +SocketNotifs.get = async function (socket, data) { + if (data && Array.isArray(data.nids) && socket.uid) { + return await user.notifications.getNotifications(data.nids, socket.uid); + } + return await user.notifications.get(socket.uid); +}; + +SocketNotifs.getCount = async function (socket) { + return await user.notifications.getUnreadCount(socket.uid); +}; + +SocketNotifs.deleteAll = async function (socket) { + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + + await user.notifications.deleteAll(socket.uid); +}; + +SocketNotifs.markRead = async function (socket, nid) { + await notifications.markRead(nid, socket.uid); + user.notifications.pushCount(socket.uid); +}; + +SocketNotifs.markUnread = async function (socket, nid) { + await notifications.markUnread(nid, socket.uid); + user.notifications.pushCount(socket.uid); +}; + +SocketNotifs.markAllRead = async function (socket) { + await notifications.markAllRead(socket.uid); + user.notifications.pushCount(socket.uid); +}; + +require('../promisify')(SocketNotifs); diff --git a/src/socket.io/plugins.js b/src/socket.io/plugins.js new file mode 100644 index 0000000000..637bb8c7ca --- /dev/null +++ b/src/socket.io/plugins.js @@ -0,0 +1,17 @@ +'use strict'; + +const SocketPlugins = {}; + +/* + This file is provided exclusively so that plugins can require it and add their own socket listeners. + + How? From your plugin: + + const SocketPlugins = require.main.require('./src/socket.io/plugins'); + SocketPlugins.myPlugin = {}; + SocketPlugins.myPlugin.myMethod = function(socket, data, callback) { ... }; + + Be a good lad and namespace your methods. +*/ + +module.exports = SocketPlugins; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js new file mode 100644 index 0000000000..0b7cc144fd --- /dev/null +++ b/src/socket.io/posts.js @@ -0,0 +1,184 @@ +'use strict'; + +const validator = require('validator'); + +const db = require('../database'); +const posts = require('../posts'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const topics = require('../topics'); +const user = require('../user'); +const notifications = require('../notifications'); +const utils = require('../utils'); +const events = require('../events'); + +const SocketPosts = module.exports; + +require('./posts/votes')(SocketPosts); +require('./posts/tools')(SocketPosts); + +SocketPosts.getRawPost = async function (socket, pid) { + const canRead = await privileges.posts.can('topics:read', pid, socket.uid); + if (!canRead) { + throw new Error('[[error:no-privileges]]'); + } + + const postData = await posts.getPostFields(pid, ['content', 'deleted']); + if (postData.deleted) { + throw new Error('[[error:no-post]]'); + } + postData.pid = pid; + const result = await plugins.hooks.fire('filter:post.getRawPost', { uid: socket.uid, postData: postData }); + return result.postData.content; +}; + +SocketPosts.getPostSummaryByIndex = async function (socket, data) { + if (data.index < 0) { + data.index = 0; + } + let pid; + if (data.index === 0) { + pid = await topics.getTopicField(data.tid, 'mainPid'); + } else { + pid = await db.getSortedSetRange(`tid:${data.tid}:posts`, data.index - 1, data.index - 1); + } + pid = Array.isArray(pid) ? pid[0] : pid; + if (!pid) { + return 0; + } + + const topicPrivileges = await privileges.topics.get(data.tid, socket.uid); + if (!topicPrivileges['topics:read']) { + throw new Error('[[error:no-privileges]]'); + } + + const postsData = await posts.getPostSummaryByPids([pid], socket.uid, { stripTags: false }); + posts.modifyPostByPrivilege(postsData[0], topicPrivileges); + return postsData[0]; +}; + +SocketPosts.getPostSummaryByPid = async function (socket, data) { + if (!data || !data.pid) { + throw new Error('[[error:invalid-data]]'); + } + const { pid } = data; + const tid = await posts.getPostField(pid, 'tid'); + const topicPrivileges = await privileges.topics.get(tid, socket.uid); + if (!topicPrivileges['topics:read']) { + throw new Error('[[error:no-privileges]]'); + } + + const postsData = await posts.getPostSummaryByPids([pid], socket.uid, { stripTags: false }); + posts.modifyPostByPrivilege(postsData[0], topicPrivileges); + return postsData[0]; +}; + +SocketPosts.getCategory = async function (socket, pid) { + return await posts.getCidByPid(pid); +}; + +SocketPosts.getPidIndex = async function (socket, data) { + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + return await posts.getPidIndex(data.pid, data.tid, data.topicPostSort); +}; + +SocketPosts.getReplies = async function (socket, pid) { + if (!utils.isNumber(pid)) { + throw new Error('[[error:invalid-data]]'); + } + const { topicPostSort } = await user.getSettings(socket.uid); + const pids = await posts.getPidsFromSet(`pid:${pid}:replies`, 0, -1, topicPostSort === 'newest_to_oldest'); + + let [postData, postPrivileges] = await Promise.all([ + posts.getPostsByPids(pids, socket.uid), + privileges.posts.get(pids, socket.uid), + ]); + postData = await topics.addPostData(postData, socket.uid); + postData.forEach((postData, index) => posts.modifyPostByPrivilege(postData, postPrivileges[index])); + postData = postData.filter((postData, index) => postData && postPrivileges[index].read); + postData = await user.blocks.filter(socket.uid, postData); + return postData; +}; + +SocketPosts.accept = async function (socket, data) { + await canEditQueue(socket, data, 'accept'); + const result = await posts.submitFromQueue(data.id); + if (result && socket.uid !== parseInt(result.uid, 10)) { + await sendQueueNotification('post-queue-accepted', result.uid, `/post/${result.pid}`); + } + await logQueueEvent(socket, result, 'accept'); +}; + +SocketPosts.reject = async function (socket, data) { + await canEditQueue(socket, data, 'reject'); + const result = await posts.removeFromQueue(data.id); + if (result && socket.uid !== parseInt(result.uid, 10)) { + await sendQueueNotification('post-queue-rejected', result.uid, '/'); + } + await logQueueEvent(socket, result, 'reject'); +}; + +async function logQueueEvent(socket, result, type) { + const eventData = { + type: `post-queue-${result.type}-${type}`, + uid: socket.uid, + ip: socket.ip, + content: result.data.content, + targetUid: result.uid, + }; + if (result.type === 'topic') { + eventData.cid = result.data.cid; + eventData.title = result.data.title; + } else { + eventData.tid = result.data.tid; + } + if (result.pid) { + eventData.pid = result.pid; + } + await events.log(eventData); +} + +SocketPosts.notify = async function (socket, data) { + await canEditQueue(socket, data, 'notify'); + const result = await posts.getFromQueue(data.id); + if (result) { + await sendQueueNotification('post-queue-notify', result.uid, `/post-queue/${data.id}`, validator.escape(String(data.message))); + } +}; + +async function canEditQueue(socket, data, action) { + const canEditQueue = await posts.canEditQueue(socket.uid, data, action); + if (!canEditQueue) { + throw new Error('[[error:no-privileges]]'); + } +} + +async function sendQueueNotification(type, targetUid, path, notificationText) { + const notifData = { + type: type, + nid: `${type}-${targetUid}-${path}`, + bodyShort: notificationText ? `[[notifications:${type}, ${notificationText}]]` : `[[notifications:${type}]]`, + path: path, + }; + if (parseInt(meta.config.postQueueNotificationUid, 10) > 0) { + notifData.from = meta.config.postQueueNotificationUid; + } + const notifObj = await notifications.create(notifData); + await notifications.push(notifObj, [targetUid]); +} + +SocketPosts.editQueuedContent = async function (socket, data) { + if (!data || !data.id || (!data.content && !data.title && !data.cid)) { + throw new Error('[[error:invalid-data]]'); + } + await posts.editQueuedContent(socket.uid, data); + if (data.content) { + return await plugins.hooks.fire('filter:parse.post', { postData: data }); + } + return { postData: data }; +}; + +require('../promisify')(SocketPosts); diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js new file mode 100644 index 0000000000..5f075b50e9 --- /dev/null +++ b/src/socket.io/posts/tools.js @@ -0,0 +1,94 @@ +'use strict'; + +const nconf = require('nconf'); + +const db = require('../../database'); +const posts = require('../../posts'); +const flags = require('../../flags'); +const events = require('../../events'); +const privileges = require('../../privileges'); +const plugins = require('../../plugins'); +const social = require('../../social'); +const user = require('../../user'); +const utils = require('../../utils'); + +module.exports = function (SocketPosts) { + SocketPosts.loadPostTools = async function (socket, data) { + if (!data || !data.pid || !data.cid) { + throw new Error('[[error:invalid-data]]'); + } + + const results = await utils.promiseParallel({ + posts: posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid', 'ip', 'flagId']), + isAdmin: user.isAdministrator(socket.uid), + isGlobalMod: user.isGlobalModerator(socket.uid), + isModerator: user.isModerator(socket.uid, data.cid), + canEdit: privileges.posts.canEdit(data.pid, socket.uid), + canDelete: privileges.posts.canDelete(data.pid, socket.uid), + canPurge: privileges.posts.canPurge(data.pid, socket.uid), + canFlag: privileges.posts.canFlag(data.pid, socket.uid), + flagged: flags.exists('post', data.pid, socket.uid), // specifically, whether THIS calling user flagged + bookmarked: posts.hasBookmarked(data.pid, socket.uid), + postSharing: social.getActivePostSharing(), + history: posts.diffs.exists(data.pid), + canViewInfo: privileges.global.can('view:users:info', socket.uid), + }); + + const postData = results.posts; + postData.absolute_url = `${nconf.get('url')}/post/${data.pid}`; + postData.bookmarked = results.bookmarked; + postData.selfPost = socket.uid && socket.uid === postData.uid; + postData.display_edit_tools = results.canEdit.flag; + postData.display_delete_tools = results.canDelete.flag; + postData.display_purge_tools = results.canPurge; + postData.display_flag_tools = socket.uid && results.canFlag.flag; + postData.display_moderator_tools = postData.display_edit_tools || postData.display_delete_tools; + postData.display_move_tools = results.isAdmin || results.isModerator; + postData.display_change_owner_tools = results.isAdmin || results.isModerator; + postData.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !postData.selfPost; + postData.display_history = results.history; + postData.flags = { + flagId: parseInt(results.posts.flagId, 10) || null, + can: results.canFlag.flag, + exists: !!results.posts.flagId, + flagged: results.flagged, + state: await db.getObjectField(`flag:${postData.flagId}`, 'state'), + }; + + if (!results.isAdmin && !results.canViewInfo) { + postData.ip = undefined; + } + const { tools } = await plugins.hooks.fire('filter:post.tools', { + pid: data.pid, + post: postData, + uid: socket.uid, + tools: [], + }); + postData.tools = tools; + + return results; + }; + + SocketPosts.changeOwner = async function (socket, data) { + if (!data || !Array.isArray(data.pids) || !data.toUid) { + throw new Error('[[error:invalid-data]]'); + } + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + + const postData = await posts.changeOwner(data.pids, data.toUid); + const 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); + }; +}; diff --git a/src/socket.io/posts/votes.js b/src/socket.io/posts/votes.js new file mode 100644 index 0000000000..83092691db --- /dev/null +++ b/src/socket.io/posts/votes.js @@ -0,0 +1,62 @@ +'use strict'; + +const db = require('../../database'); +const user = require('../../user'); +const posts = require('../../posts'); +const privileges = require('../../privileges'); +const meta = require('../../meta'); + +module.exports = function (SocketPosts) { + SocketPosts.getVoters = async function (socket, data) { + if (!data || !data.pid || !data.cid) { + throw new Error('[[error:invalid-data]]'); + } + const showDownvotes = !meta.config['downvote:disabled']; + const canSeeVotes = meta.config.votesArePublic || + await privileges.categories.isAdminOrMod(data.cid, socket.uid); + if (!canSeeVotes) { + throw new Error('[[error:no-privileges]]'); + } + const [upvoteUids, downvoteUids] = await Promise.all([ + db.getSetMembers(`pid:${data.pid}:upvote`), + showDownvotes ? db.getSetMembers(`pid:${data.pid}:downvote`) : [], + ]); + + const [upvoters, downvoters] = await Promise.all([ + user.getUsersFields(upvoteUids, ['username', 'userslug', 'picture']), + user.getUsersFields(downvoteUids, ['username', 'userslug', 'picture']), + ]); + + return { + upvoteCount: upvoters.length, + downvoteCount: downvoters.length, + showDownvotes: showDownvotes, + upvoters: upvoters, + downvoters: downvoters, + }; + }; + + SocketPosts.getUpvoters = async function (socket, pids) { + if (!Array.isArray(pids)) { + throw new Error('[[error:invalid-data]]'); + } + const data = await posts.getUpvotedUidsByPids(pids); + if (!data.length) { + return []; + } + + const result = await Promise.all(data.map(async (uids) => { + let otherCount = 0; + if (uids.length > 6) { + otherCount = uids.length - 5; + uids = uids.slice(0, 5); + } + const usernames = await user.getUsernamesByUids(uids); + return { + otherCount: otherCount, + usernames: usernames, + }; + })); + return result; + }; +}; diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js new file mode 100644 index 0000000000..cbbd93f525 --- /dev/null +++ b/src/socket.io/topics.js @@ -0,0 +1,128 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const posts = require('../posts'); +const topics = require('../topics'); +const user = require('../user'); +const meta = require('../meta'); +const privileges = require('../privileges'); +const cache = require('../cache'); +const events = require('../events'); + +const SocketTopics = module.exports; + +require('./topics/unread')(SocketTopics); +require('./topics/move')(SocketTopics); +require('./topics/tools')(SocketTopics); +require('./topics/infinitescroll')(SocketTopics); +require('./topics/tags')(SocketTopics); +require('./topics/merge')(SocketTopics); + +SocketTopics.postcount = async function (socket, tid) { + const canRead = await privileges.topics.can('topics:read', tid, socket.uid); + if (!canRead) { + throw new Error('[[no-privileges]]'); + } + return await topics.getTopicField(tid, 'postcount'); +}; + +SocketTopics.bookmark = async function (socket, data) { + if (!socket.uid || !data) { + throw new Error('[[error:invalid-data]]'); + } + const postcount = await topics.getTopicField(data.tid, 'postcount'); + if (data.index > meta.config.bookmarkThreshold && postcount > meta.config.bookmarkThreshold) { + await topics.setUserBookmark(data.tid, socket.uid, data.index); + } +}; + +SocketTopics.createTopicFromPosts = async function (socket, data) { + if (!socket.uid) { + throw new Error('[[error:not-logged-in]]'); + } + + if (!data || !data.title || !data.pids || !Array.isArray(data.pids)) { + throw new Error('[[error:invalid-data]]'); + } + + const result = await topics.createTopicFromPosts(socket.uid, data.title, data.pids, data.fromTid); + await events.log({ + type: `topic-fork`, + uid: socket.uid, + ip: socket.ip, + pids: String(data.pids), + fromTid: data.fromTid, + toTid: result.tid, + }); + return result; +}; + +SocketTopics.isFollowed = async function (socket, tid) { + const isFollowing = await topics.isFollowing([tid], socket.uid); + return isFollowing[0]; +}; + +SocketTopics.isModerator = async function (socket, tid) { + const cid = await topics.getTopicField(tid, 'cid'); + return await user.isModerator(socket.uid, cid); +}; + +SocketTopics.getMyNextPostIndex = async function (socket, data) { + if (!data || !data.tid || !data.index || !data.sort) { + throw new Error('[[error:invalid-data]]'); + } + + async function getTopicPids(index) { + const topicSet = data.sort === 'most_votes' ? `tid:${data.tid}:posts:votes` : `tid:${data.tid}:posts`; + const reverse = data.sort === 'newest_to_oldest' || data.sort === 'most_votes'; + const cacheKey = `np:s:${topicSet}:r:${String(reverse)}:tid:${data.tid}:pids`; + const topicPids = cache.get(cacheKey); + if (topicPids) { + return topicPids.slice(index - 1); + } + const pids = await db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](topicSet, 0, -1); + cache.set(cacheKey, pids, 30000); + return pids.slice(index - 1); + } + + async function getUserPids() { + const cid = await topics.getTopicField(data.tid, 'cid'); + const cacheKey = `np:cid:${cid}:uid:${socket.uid}:pids`; + const userPids = cache.get(cacheKey); + if (userPids) { + return userPids; + } + const pids = await db.getSortedSetRange(`cid:${cid}:uid:${socket.uid}:pids`, 0, -1); + cache.set(cacheKey, pids, 30000); + return pids; + } + const postCountInTopic = await db.sortedSetScore(`tid:${data.tid}:posters`, socket.uid); + if (postCountInTopic <= 0) { + return 0; + } + const [topicPids, userPidsInCategory] = await Promise.all([ + getTopicPids(data.index), + getUserPids(), + ]); + const userPidsInTopic = _.intersection(topicPids, userPidsInCategory); + if (!userPidsInTopic.length) { + if (postCountInTopic > 0) { + // wrap around to beginning + const wrapIndex = await SocketTopics.getMyNextPostIndex(socket, { ...data, index: 1 }); + return wrapIndex; + } + return 0; + } + return await posts.getPidIndex(userPidsInTopic[0], data.tid, data.sort); +}; + +SocketTopics.getPostCountInTopic = async function (socket, tid) { + if (!socket.uid || !tid) { + return 0; + } + return await db.sortedSetScore(`tid:${tid}:posters`, socket.uid); +}; + +require('../promisify')(SocketTopics); diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js new file mode 100644 index 0000000000..c825bbe86c --- /dev/null +++ b/src/socket.io/topics/infinitescroll.js @@ -0,0 +1,55 @@ +'use strict'; + +const topics = require('../../topics'); +const privileges = require('../../privileges'); +const meta = require('../../meta'); +const utils = require('../../utils'); +const social = require('../../social'); + +module.exports = function (SocketTopics) { + SocketTopics.loadMore = async function (socket, data) { + if (!data || !data.tid || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { + throw new Error('[[error:invalid-data]]'); + } + + const [userPrivileges, topicData] = await Promise.all([ + privileges.topics.get(data.tid, socket.uid), + topics.getTopicData(data.tid), + ]); + + if (!userPrivileges['topics:read'] || !privileges.topics.canViewDeletedScheduled(topicData, userPrivileges)) { + throw new Error('[[error:no-privileges]]'); + } + + const set = data.topicPostSort === 'most_votes' ? `tid:${data.tid}:posts:votes` : `tid:${data.tid}:posts`; + const reverse = data.topicPostSort === 'newest_to_oldest' || data.topicPostSort === 'most_votes'; + let start = Math.max(0, parseInt(data.after, 10)); + + const infScrollPostsPerPage = Math.max(0, Math.min( + meta.config.postsPerPage || 20, + parseInt(data.count, 10) || meta.config.postsPerPage || 20 + )); + + if (data.direction === -1) { + start -= infScrollPostsPerPage; + } + + let stop = start + infScrollPostsPerPage - 1; + + start = Math.max(0, start); + stop = Math.max(0, stop); + const [posts, postSharing] = await Promise.all([ + topics.getTopicPosts(topicData, set, start, stop, socket.uid, reverse), + social.getActivePostSharing(), + ]); + + topicData.posts = posts; + topicData.privileges = userPrivileges; + topicData.postSharing = postSharing; + topicData['reputation:disabled'] = meta.config['reputation:disabled'] === 1; + topicData['downvote:disabled'] = meta.config['downvote:disabled'] === 1; + + topics.modifyPostsByPrivilege(topicData, userPrivileges); + return topicData; + }; +}; diff --git a/src/socket.io/topics/merge.js b/src/socket.io/topics/merge.js new file mode 100644 index 0000000000..7a143ed526 --- /dev/null +++ b/src/socket.io/topics/merge.js @@ -0,0 +1,29 @@ +'use strict'; + +const topics = require('../../topics'); +const privileges = require('../../privileges'); +const events = require('../../events'); + +module.exports = function (SocketTopics) { + SocketTopics.merge = async function (socket, data) { + if (!data || !Array.isArray(data.tids)) { + throw new Error('[[error:invalid-data]]'); + } + const allowed = await Promise.all(data.tids.map(tid => privileges.topics.isAdminOrMod(tid, socket.uid))); + if (allowed.includes(false)) { + throw new Error('[[error:no-privileges]]'); + } + if (data.options && data.options.mainTid && !data.tids.includes(data.options.mainTid)) { + throw new Error('[[error:invalid-data]]'); + } + const mergeIntoTid = await topics.merge(data.tids, socket.uid, data.options); + await events.log({ + type: `topic-merge`, + uid: socket.uid, + ip: socket.ip, + mergeIntoTid: mergeIntoTid, + tids: String(data.tids), + }); + return mergeIntoTid; + }; +}; diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js new file mode 100644 index 0000000000..acdde55d24 --- /dev/null +++ b/src/socket.io/topics/move.js @@ -0,0 +1,73 @@ +'use strict'; + +const async = require('async'); +const user = require('../../user'); +const topics = require('../../topics'); +const categories = require('../../categories'); +const privileges = require('../../privileges'); +const socketHelpers = require('../helpers'); +const events = require('../../events'); + +module.exports = function (SocketTopics) { + SocketTopics.move = async function (socket, data) { + if (!data || !Array.isArray(data.tids) || !data.cid) { + throw new Error('[[error:invalid-data]]'); + } + + const canMove = await privileges.categories.isAdminOrMod(data.cid, socket.uid); + if (!canMove) { + throw new Error('[[error:no-privileges]]'); + } + + const uids = await user.getUidsFromSet('users:online', 0, -1); + + await async.eachLimit(data.tids, 10, async (tid) => { + const canMove = await privileges.topics.isAdminOrMod(tid, socket.uid); + if (!canMove) { + throw new Error('[[error:no-privileges]]'); + } + const topicData = await topics.getTopicFields(tid, ['tid', 'cid', 'slug', 'deleted']); + data.uid = socket.uid; + await topics.tools.move(tid, data); + + const notifyUids = await privileges.categories.filterUids('topics:read', topicData.cid, uids); + socketHelpers.emitToUids('event:topic_moved', topicData, notifyUids); + if (!topicData.deleted) { + socketHelpers.sendNotificationToTopicOwner(tid, socket.uid, 'move', 'notifications:moved_your_topic'); + } + + await events.log({ + type: `topic-move`, + uid: socket.uid, + ip: socket.ip, + tid: tid, + fromCid: topicData.cid, + toCid: data.cid, + }); + }); + }; + + + SocketTopics.moveAll = async function (socket, data) { + if (!data || !data.cid || !data.currentCid) { + throw new Error('[[error:invalid-data]]'); + } + const canMove = await privileges.categories.canMoveAllTopics(data.currentCid, data.cid, socket.uid); + if (!canMove) { + throw new Error('[[error:no-privileges]]'); + } + + const tids = await categories.getAllTopicIds(data.currentCid, 0, -1); + data.uid = socket.uid; + await async.eachLimit(tids, 50, async (tid) => { + await topics.tools.move(tid, data); + }); + await events.log({ + type: `topic-move-all`, + uid: socket.uid, + ip: socket.ip, + fromCid: data.currentCid, + toCid: data.cid, + }); + }; +}; diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js new file mode 100644 index 0000000000..daac701965 --- /dev/null +++ b/src/socket.io/topics/tags.js @@ -0,0 +1,85 @@ +'use strict'; + +const meta = require('../../meta'); +const user = require('../../user'); +const topics = require('../../topics'); +const categories = require('../../categories'); +const privileges = require('../../privileges'); +const utils = require('../../utils'); + +module.exports = function (SocketTopics) { + SocketTopics.isTagAllowed = async function (socket, data) { + if (!data || !utils.isNumber(data.cid) || !data.tag) { + throw new Error('[[error:invalid-data]]'); + } + + const systemTags = (meta.config.systemTags || '').split(','); + const [tagWhitelist, isPrivileged] = await Promise.all([ + categories.getTagWhitelist([data.cid]), + user.isPrivileged(socket.uid), + ]); + return isPrivileged || + ( + !systemTags.includes(data.tag) && + (!tagWhitelist[0].length || tagWhitelist[0].includes(data.tag)) + ); + }; + + SocketTopics.canRemoveTag = async function (socket, data) { + if (!data || !data.tag) { + throw new Error('[[error:invalid-data]]'); + } + + const systemTags = (meta.config.systemTags || '').split(','); + const isPrivileged = await user.isPrivileged(socket.uid); + return isPrivileged || !systemTags.includes(String(data.tag).trim()); + }; + + SocketTopics.autocompleteTags = async function (socket, data) { + if (data.cid) { + const canRead = await privileges.categories.can('topics:read', data.cid, socket.uid); + if (!canRead) { + throw new Error('[[error:no-privileges]]'); + } + } + data.cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'topics:read'); + const result = await topics.autocompleteTags(data); + return result.map(tag => tag.value); + }; + + SocketTopics.searchTags = async function (socket, data) { + const result = await searchTags(socket.uid, topics.searchTags, data); + return result.map(tag => tag.value); + }; + + SocketTopics.searchAndLoadTags = async function (socket, data) { + return await searchTags(socket.uid, topics.searchAndLoadTags, data); + }; + + async function searchTags(uid, method, data) { + const allowed = await privileges.global.can('search:tags', uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + if (data.cid) { + const canRead = await privileges.categories.can('topics:read', data.cid, uid); + if (!canRead) { + throw new Error('[[error:no-privileges]]'); + } + } + data.cids = await categories.getCidsByPrivilege('categories:cid', uid, 'topics:read'); + return await method(data); + } + + SocketTopics.loadMoreTags = async function (socket, data) { + if (!data || !utils.isNumber(data.after)) { + throw new Error('[[error:invalid-data]]'); + } + + const start = parseInt(data.after, 10); + const stop = start + 99; + const cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'topics:read'); + const tags = await topics.getCategoryTagsData(cids, start, stop); + return { tags: tags.filter(Boolean), nextStart: stop + 1 }; + }; +}; diff --git a/src/socket.io/topics/tools.js b/src/socket.io/topics/tools.js new file mode 100644 index 0000000000..2a4064d52d --- /dev/null +++ b/src/socket.io/topics/tools.js @@ -0,0 +1,40 @@ +'use strict'; + +const topics = require('../../topics'); +const privileges = require('../../privileges'); +const plugins = require('../../plugins'); + +module.exports = function (SocketTopics) { + SocketTopics.loadTopicTools = async function (socket, data) { + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + if (!data) { + throw new Error('[[error:invalid-data]]'); + } + + const [topicData, userPrivileges] = await Promise.all([ + topics.getTopicData(data.tid), + privileges.topics.get(data.tid, socket.uid), + ]); + + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + if (!userPrivileges['topics:read']) { + throw new Error('[[error:no-privileges]]'); + } + topicData.privileges = userPrivileges; + const result = await plugins.hooks.fire('filter:topic.thread_tools', { topic: topicData, uid: socket.uid, tools: [] }); + result.topic.thread_tools = result.tools; + return result.topic; + }; + + SocketTopics.orderPinnedTopics = async function (socket, data) { + if (!data || !data.tid) { + throw new Error('[[error:invalid-data]]'); + } + + await topics.tools.orderPinnedTopics(socket.uid, data); + }; +}; diff --git a/src/socket.io/topics/unread.js b/src/socket.io/topics/unread.js new file mode 100644 index 0000000000..123a356cbd --- /dev/null +++ b/src/socket.io/topics/unread.js @@ -0,0 +1,74 @@ +'use strict'; + +const db = require('../../database'); +const user = require('../../user'); +const topics = require('../../topics'); + +module.exports = function (SocketTopics) { + SocketTopics.markAsRead = async function (socket, tids) { + if (!Array.isArray(tids) || socket.uid <= 0) { + throw new Error('[[error:invalid-data]]'); + } + const hasMarked = await topics.markAsRead(tids, socket.uid); + const promises = [topics.markTopicNotificationsRead(tids, socket.uid)]; + if (hasMarked) { + promises.push(topics.pushUnreadCount(socket.uid)); + } + await Promise.all(promises); + }; + + SocketTopics.markTopicNotificationsRead = async function (socket, tids) { + if (!Array.isArray(tids) || !socket.uid) { + throw new Error('[[error:invalid-data]]'); + } + await topics.markTopicNotificationsRead(tids, socket.uid); + }; + + SocketTopics.markAllRead = async function (socket) { + if (socket.uid <= 0) { + throw new Error('[[error:invalid-uid]]'); + } + await topics.markAllRead(socket.uid); + topics.pushUnreadCount(socket.uid); + }; + + SocketTopics.markCategoryTopicsRead = async function (socket, cid) { + const tids = await topics.getUnreadTids({ cid: cid, uid: socket.uid, filter: '' }); + await SocketTopics.markAsRead(socket, tids); + }; + + SocketTopics.markUnread = async function (socket, tid) { + if (!tid || socket.uid <= 0) { + throw new Error('[[error:invalid-data]]'); + } + await topics.markUnread(tid, socket.uid); + topics.pushUnreadCount(socket.uid); + }; + + SocketTopics.markAsUnreadForAll = async function (socket, tids) { + if (!Array.isArray(tids)) { + throw new Error('[[error:invalid-tid]]'); + } + + if (socket.uid <= 0) { + throw new Error('[[error:no-privileges]]'); + } + const isAdmin = await user.isAdministrator(socket.uid); + const now = Date.now(); + await Promise.all(tids.map(async (tid) => { + const topicData = await topics.getTopicFields(tid, ['tid', 'cid']); + if (!topicData.tid) { + throw new Error('[[error:no-topic]]'); + } + const isMod = await user.isModerator(socket.uid, topicData.cid); + if (!isAdmin && !isMod) { + throw new Error('[[error:no-privileges]]'); + } + await topics.markAsUnreadForAll(tid); + await topics.updateRecent(tid, now); + await db.sortedSetAdd(`cid:${topicData.cid}:tids:lastposttime`, now, tid); + await topics.setTopicField(tid, 'lastposttime', now); + })); + topics.pushUnreadCount(socket.uid); + }; +}; diff --git a/src/socket.io/uploads.js b/src/socket.io/uploads.js new file mode 100644 index 0000000000..c7f05b61f9 --- /dev/null +++ b/src/socket.io/uploads.js @@ -0,0 +1,53 @@ +'use strict'; + +const socketUser = require('./user'); +const socketGroup = require('./groups'); +const image = require('../image'); +const meta = require('../meta'); + +const inProgress = {}; + +const uploads = module.exports; + +uploads.upload = async function (socket, data) { + const methodToFunc = { + 'user.uploadCroppedPicture': socketUser.uploadCroppedPicture, + 'user.updateCover': socketUser.updateCover, + 'groups.cover.update': socketGroup.cover.update, + }; + if (!socket.uid || !data || !data.chunk || + !data.params || !data.params.method || !methodToFunc.hasOwnProperty(data.params.method)) { + throw new Error('[[error:invalid-data]]'); + } + + inProgress[socket.id] = inProgress[socket.id] || Object.create(null); + const socketUploads = inProgress[socket.id]; + const { method } = data.params; + + socketUploads[method] = socketUploads[method] || { imageData: '' }; + socketUploads[method].imageData += data.chunk; + + try { + const maxSize = data.params.method === 'user.uploadCroppedPicture' ? + meta.config.maximumProfileImageSize : meta.config.maximumCoverImageSize; + const size = image.sizeFromBase64(socketUploads[method].imageData); + + if (size > maxSize * 1024) { + throw new Error(`[[error:file-too-big, ${maxSize}]]`); + } + if (socketUploads[method].imageData.length < data.params.size) { + return; + } + data.params.imageData = socketUploads[method].imageData; + const result = await methodToFunc[data.params.method](socket, data.params); + delete socketUploads[method]; + return result; + } catch (err) { + delete inProgress[socket.id]; + throw err; + } +}; + +uploads.clear = function (sid) { + delete inProgress[sid]; +}; diff --git a/src/socket.io/user.js b/src/socket.io/user.js new file mode 100644 index 0000000000..a70bc5a27f --- /dev/null +++ b/src/socket.io/user.js @@ -0,0 +1,189 @@ +'use strict'; + +const util = require('util'); +const winston = require('winston'); + +const sleep = util.promisify(setTimeout); + +const user = require('../user'); +const topics = require('../topics'); +const messaging = require('../messaging'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const events = require('../events'); +const emailer = require('../emailer'); +const db = require('../database'); +const userController = require('../controllers/user'); +const privileges = require('../privileges'); +const utils = require('../utils'); +const sockets = require('.'); + +const SocketUser = module.exports; + +require('./user/profile')(SocketUser); +require('./user/status')(SocketUser); +require('./user/picture')(SocketUser); +require('./user/registration')(SocketUser); + +SocketUser.emailConfirm = async function (socket) { + sockets.warnDeprecated(socket, 'HTTP 302 /me/edit/email'); + + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + + return await user.email.sendValidationEmail(socket.uid); +}; + +// Password Reset +SocketUser.reset = {}; + +SocketUser.reset.send = async function (socket, email) { + if (!email) { + throw new Error('[[error:invalid-data]]'); + } + + if (meta.config['password:disableEdit']) { + throw new Error('[[error:no-privileges]]'); + } + async function logEvent(text) { + await events.log({ + type: 'password-reset', + text: text, + ip: socket.ip, + uid: socket.uid, + email: email, + }); + } + try { + await user.reset.send(email); + await logEvent('[[success:success]]'); + await sleep(2500 + ((Math.random() * 500) - 250)); + } catch (err) { + await logEvent(err.message); + await sleep(2500 + ((Math.random() * 500) - 250)); + const internalErrors = ['[[error:invalid-email]]', '[[error:reset-rate-limited]]']; + if (!internalErrors.includes(err.message)) { + throw err; + } + } +}; + +SocketUser.reset.commit = async function (socket, data) { + if (!data || !data.code || !data.password) { + throw new Error('[[error:invalid-data]]'); + } + const [uid] = await Promise.all([ + db.getObjectField('reset:uid', data.code), + user.reset.commit(data.code, data.password), + plugins.hooks.fire('action:password.reset', { uid: socket.uid }), + ]); + + await events.log({ + type: 'password-reset', + uid: uid, + ip: socket.ip, + }); + + const username = await user.getUserField(uid, 'username'); + const now = new Date(); + const parsedDate = `${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}`; + emailer.send('reset_notify', uid, { + username: username, + date: parsedDate, + subject: '[[email:reset.notify.subject]]', + }).catch(err => winston.error(`[emailer.send] ${err.stack}`)); +}; + +SocketUser.isFollowing = async function (socket, data) { + if (!socket.uid || !data.uid) { + return false; + } + + return await user.isFollowing(socket.uid, data.uid); +}; + +SocketUser.getUnreadCount = async function (socket) { + if (!socket.uid) { + return 0; + } + return await topics.getTotalUnread(socket.uid, ''); +}; + +SocketUser.getUnreadChatCount = async function (socket) { + if (!socket.uid) { + return 0; + } + return await messaging.getUnreadCount(socket.uid); +}; + +SocketUser.getUnreadCounts = async function (socket) { + if (!socket.uid) { + return {}; + } + const results = await utils.promiseParallel({ + unreadCounts: topics.getUnreadTids({ uid: socket.uid, count: true }), + unreadChatCount: messaging.getUnreadCount(socket.uid), + unreadNotificationCount: user.notifications.getUnreadCount(socket.uid), + }); + results.unreadTopicCount = results.unreadCounts['']; + results.unreadNewTopicCount = results.unreadCounts.new; + results.unreadWatchedTopicCount = results.unreadCounts.watched; + results.unreadUnrepliedTopicCount = results.unreadCounts.unreplied; + return results; +}; + +SocketUser.getUserByUID = async function (socket, uid) { + return await userController.getUserDataByField(socket.uid, 'uid', uid); +}; + +SocketUser.getUserByUsername = async function (socket, username) { + return await userController.getUserDataByField(socket.uid, 'username', username); +}; + +SocketUser.getUserByEmail = async function (socket, email) { + return await userController.getUserDataByField(socket.uid, 'email', email); +}; + +SocketUser.setModerationNote = async function (socket, data) { + if (!socket.uid || !data || !data.uid || !data.note) { + throw new Error('[[error:invalid-data]]'); + } + const noteData = { + uid: socket.uid, + note: data.note, + timestamp: Date.now(), + }; + let canEdit = await privileges.users.canEdit(socket.uid, data.uid); + if (!canEdit) { + canEdit = await user.isModeratorOfAnyCategory(socket.uid); + } + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + await user.appendModerationNote({ uid: data.uid, noteData }); +}; + +SocketUser.deleteUpload = async function (socket, data) { + if (!data || !data.name || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + await user.deleteUpload(socket.uid, data.uid, data.name); +}; + +SocketUser.gdpr = {}; + +SocketUser.gdpr.consent = async function (socket) { + await user.setUserField(socket.uid, 'gdpr_consent', 1); +}; + +SocketUser.gdpr.check = async function (socket, data) { + const isAdmin = await user.isAdministrator(socket.uid); + if (!isAdmin) { + data.uid = socket.uid; + } + return await db.getObjectField(`user:${data.uid}`, 'gdpr_consent'); +}; + +require('../promisify')(SocketUser); diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js new file mode 100644 index 0000000000..5493772f2e --- /dev/null +++ b/src/socket.io/user/picture.js @@ -0,0 +1,44 @@ +'use strict'; + +const user = require('../../user'); +const plugins = require('../../plugins'); + +module.exports = function (SocketUser) { + SocketUser.removeUploadedPicture = async function (socket, data) { + if (!socket.uid || !data || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + await user.isAdminOrSelf(socket.uid, data.uid); + // 'keepAllUserImages' is ignored, since there is explicit user intent + const userData = await user.removeProfileImage(data.uid); + plugins.hooks.fire('action:user.removeUploadedPicture', { + callerUid: socket.uid, + uid: data.uid, + user: userData, + }); + }; + + SocketUser.getProfilePictures = async function (socket, data) { + if (!data || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + + const [list, uploaded] = await Promise.all([ + plugins.hooks.fire('filter:user.listPictures', { + uid: data.uid, + pictures: [], + }), + user.getUserField(data.uid, 'uploadedpicture'), + ]); + + if (uploaded) { + list.pictures.push({ + type: 'uploaded', + url: uploaded, + text: '[[user:uploaded_picture]]', + }); + } + + return list.pictures; + }; +}; diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js new file mode 100644 index 0000000000..6f680732a6 --- /dev/null +++ b/src/socket.io/user/profile.js @@ -0,0 +1,79 @@ +'use strict'; + +const user = require('../../user'); +const privileges = require('../../privileges'); +const plugins = require('../../plugins'); + +const sockets = require('..'); +const api = require('../../api'); + +module.exports = function (SocketUser) { + SocketUser.updateCover = async function (socket, data) { + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid); + await user.checkMinReputation(socket.uid, data.uid, 'min:rep:cover-picture'); + return await user.updateCoverPicture(data); + }; + + SocketUser.uploadCroppedPicture = async function (socket, data) { + if (!socket.uid || !(await privileges.users.canEdit(socket.uid, data.uid))) { + throw new Error('[[error:no-privileges]]'); + } + + await user.checkMinReputation(socket.uid, data.uid, 'min:rep:profile-picture'); + data.callerUid = socket.uid; + return await user.uploadCroppedPicture(data); + }; + + SocketUser.removeCover = async function (socket, data) { + if (!socket.uid) { + throw new Error('[[error:no-privileges]]'); + } + await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid); + const userData = await user.getUserFields(data.uid, ['cover:url']); + // 'keepAllUserImages' is ignored, since there is explicit user intent + await user.removeCoverPicture(data); + plugins.hooks.fire('action:user.removeCoverPicture', { + callerUid: socket.uid, + uid: data.uid, + user: userData, + }); + }; + + SocketUser.toggleBlock = async function (socket, data) { + const isBlocked = await user.blocks.is(data.blockeeUid, data.blockerUid); + await user.blocks.can(socket.uid, data.blockerUid, data.blockeeUid, isBlocked ? 'unblock' : 'block'); + await user.blocks[isBlocked ? 'remove' : 'add'](data.blockeeUid, data.blockerUid); + return !isBlocked; + }; + + SocketUser.exportProfile = async function (socket, data) { + await doExport(socket, data, 'profile'); + }; + + SocketUser.exportPosts = async function (socket, data) { + await doExport(socket, data, 'posts'); + }; + + SocketUser.exportUploads = async function (socket, data) { + await doExport(socket, data, 'uploads'); + }; + + async function doExport(socket, data, type) { + sockets.warnDeprecated(socket, 'POST /api/v3/users/:uid/exports/:type'); + + if (!socket.uid) { + throw new Error('[[error:invalid-uid]]'); + } + + if (!data || parseInt(data.uid, 10) <= 0) { + throw new Error('[[error:invalid-data]]'); + } + + await user.isAdminOrSelf(socket.uid, data.uid); + + api.users.generateExport(socket, { type, ...data }); + } +}; diff --git a/src/socket.io/user/registration.js b/src/socket.io/user/registration.js new file mode 100644 index 0000000000..0d173a34f5 --- /dev/null +++ b/src/socket.io/user/registration.js @@ -0,0 +1,43 @@ +'use strict'; + +const user = require('../../user'); +const events = require('../../events'); + +module.exports = function (SocketUser) { + SocketUser.acceptRegistration = async function (socket, data) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + const uid = await user.acceptRegistration(data.username); + await events.log({ + type: 'registration-approved', + uid: socket.uid, + ip: socket.ip, + targetUid: uid, + }); + return uid; + }; + + SocketUser.rejectRegistration = async function (socket, data) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + await user.rejectRegistration(data.username); + await events.log({ + type: 'registration-rejected', + uid: socket.uid, + ip: socket.ip, + username: data.username, + }); + }; + + SocketUser.deleteInvitation = async function (socket, data) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + await user.deleteInvitation(data.invitedBy, data.email); + }; +}; diff --git a/src/socket.io/user/status.js b/src/socket.io/user/status.js new file mode 100644 index 0000000000..b81f1de8ef --- /dev/null +++ b/src/socket.io/user/status.js @@ -0,0 +1,40 @@ +'use strict'; + +const user = require('../../user'); +const websockets = require('../index'); + +module.exports = function (SocketUser) { + SocketUser.checkStatus = async function (socket, uid) { + if (!socket.uid) { + throw new Error('[[error:invalid-uid]]'); + } + const userData = await user.getUserFields(uid, ['lastonline', 'status']); + return user.getStatus(userData); + }; + + SocketUser.setStatus = async function (socket, status) { + if (socket.uid <= 0) { + throw new Error('[[error:invalid-uid]]'); + } + + const allowedStatus = ['online', 'offline', 'dnd', 'away']; + if (!allowedStatus.includes(status)) { + throw new Error('[[error:invalid-user-status]]'); + } + + const userData = { status: status }; + if (status !== 'offline') { + userData.lastonline = Date.now(); + } + await user.setUserFields(socket.uid, userData); + if (status !== 'offline') { + await user.updateOnlineUsers(socket.uid); + } + const eventData = { + uid: socket.uid, + status: status, + }; + websockets.server.emit('event:user_status_change', eventData); + return eventData; + }; +}; diff --git a/src/start.js b/src/start.js new file mode 100644 index 0000000000..7dd6af99f2 --- /dev/null +++ b/src/start.js @@ -0,0 +1,145 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); + +const start = module.exports; + +start.start = async function () { + printStartupInfo(); + + addProcessHandlers(); + + try { + const db = require('./database'); + await db.init(); + await db.checkCompatibility(); + + const meta = require('./meta'); + await meta.configs.init(); + + if (nconf.get('runJobs')) { + await runUpgrades(); + } + + if (nconf.get('dep-check') === undefined || nconf.get('dep-check') !== false) { + await meta.dependencies.check(); + } else { + winston.warn('[init] Dependency checking skipped!'); + } + + await db.initSessionStore(); + + const webserver = require('./webserver'); + const sockets = require('./socket.io'); + await sockets.init(webserver.server); + + if (nconf.get('runJobs')) { + require('./notifications').startJobs(); + require('./user').startJobs(); + require('./plugins').startJobs(); + require('./topics').scheduled.startJobs(); + await db.delete('locks'); + } + + await webserver.listen(); + + if (process.send) { + process.send({ + action: 'listening', + }); + } + } catch (err) { + switch (err.message) { + case 'dependencies-out-of-date': + winston.error('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:'); + winston.error(' ./nodebb upgrade'); + break; + case 'dependencies-missing': + winston.error('One or more of NodeBB\'s dependent packages are missing. Please run the following command to update them:'); + winston.error(' ./nodebb upgrade'); + break; + default: + winston.error(err.stack); + break; + } + + // Either way, bad stuff happened. Abort start. + process.exit(); + } +}; + +async function runUpgrades() { + const upgrade = require('./upgrade'); + try { + await upgrade.check(); + } catch (err) { + if (err && err.message === 'schema-out-of-date') { + await upgrade.run(); + } else { + throw err; + } + } +} + +function printStartupInfo() { + if (nconf.get('isPrimary')) { + winston.info('Initializing NodeBB v%s %s', nconf.get('version'), nconf.get('url')); + + const host = nconf.get(`${nconf.get('database')}:host`); + const storeLocation = host ? `at ${host}${!host.includes('/') ? `:${nconf.get(`${nconf.get('database')}:port`)}` : ''}` : ''; + + winston.verbose('* using %s store %s', nconf.get('database'), storeLocation); + winston.verbose('* using themes stored in: %s', nconf.get('themes_path')); + } +} + +function addProcessHandlers() { + process.on('SIGTERM', shutdown); + process.on('SIGINT', shutdown); + process.on('SIGHUP', restart); + process.on('uncaughtException', (err) => { + winston.error(err.stack); + + require('./meta').js.killMinifier(); + shutdown(1); + }); + process.on('message', (msg) => { + if (msg && msg.compiling === 'tpl') { + const benchpressjs = require('benchpressjs'); + benchpressjs.flush(); + } else if (msg && msg.compiling === 'lang') { + const translator = require('./translator'); + translator.flush(); + } + }); +} + +function restart() { + if (process.send) { + winston.info('[app] Restarting...'); + process.send({ + action: 'restart', + }); + } else { + winston.error('[app] Could not restart server. Shutting down.'); + shutdown(1); + } +} + +async function shutdown(code) { + winston.info('[app] Shutdown (SIGTERM/SIGINT) Initialised.'); + try { + await require('./webserver').destroy(); + winston.info('[app] Web server closed to connections.'); + await require('./analytics').writeData(); + winston.info('[app] Live analytics saved.'); + await require('./database').close(); + winston.info('[app] Database connection closed.'); + winston.info('[app] Shutdown complete.'); + process.exit(code || 0); + } catch (err) { + winston.error(err.stack); + return process.exit(code || 0); + } +} diff --git a/src/topics/bookmarks.js b/src/topics/bookmarks.js new file mode 100644 index 0000000000..9c94f21692 --- /dev/null +++ b/src/topics/bookmarks.js @@ -0,0 +1,66 @@ + +'use strict'; + +const async = require('async'); + +const db = require('../database'); +const user = require('../user'); + +module.exports = function (Topics) { + Topics.getUserBookmark = async function (tid, uid) { + if (parseInt(uid, 10) <= 0) { + return null; + } + return await db.sortedSetScore(`tid:${tid}:bookmarks`, uid); + }; + + Topics.getUserBookmarks = async function (tids, uid) { + if (parseInt(uid, 10) <= 0) { + return tids.map(() => null); + } + return await db.sortedSetsScore(tids.map(tid => `tid:${tid}:bookmarks`), uid); + }; + + Topics.setUserBookmark = async function (tid, uid, index) { + await db.sortedSetAdd(`tid:${tid}:bookmarks`, index, uid); + }; + + Topics.getTopicBookmarks = async function (tid) { + return await db.getSortedSetRangeWithScores(`tid:${tid}:bookmarks`, 0, -1); + }; + + Topics.updateTopicBookmarks = async function (tid, pids) { + const maxIndex = await Topics.getPostCount(tid); + const indices = await db.sortedSetRanks(`tid:${tid}:posts`, pids); + const postIndices = indices.map(i => (i === null ? 0 : i + 1)); + const minIndex = Math.min(...postIndices); + + const bookmarks = await Topics.getTopicBookmarks(tid); + + const uidData = bookmarks.map(b => ({ uid: b.value, bookmark: parseInt(b.score, 10) })) + .filter(data => data.bookmark >= minIndex); + + await async.eachLimit(uidData, 50, async (data) => { + let bookmark = Math.min(data.bookmark, maxIndex); + + postIndices.forEach((i) => { + if (i < data.bookmark) { + bookmark -= 1; + } + }); + + // make sure the bookmark is valid if we removed the last post + bookmark = Math.min(bookmark, maxIndex - pids.length); + if (bookmark === data.bookmark) { + return; + } + + const settings = await user.getSettings(data.uid); + if (settings.topicPostSort === 'most_votes') { + return; + } + + await Topics.setUserBookmark(tid, data.uid, bookmark); + }); + }; +}; diff --git a/src/topics/create.js b/src/topics/create.js new file mode 100644 index 0000000000..c366c2197d --- /dev/null +++ b/src/topics/create.js @@ -0,0 +1,310 @@ + +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const utils = require('../utils'); +const slugify = require('../slugify'); +const plugins = require('../plugins'); +const analytics = require('../analytics'); +const user = require('../user'); +const meta = require('../meta'); +const posts = require('../posts'); +const privileges = require('../privileges'); +const categories = require('../categories'); +const translator = require('../translator'); + +module.exports = function (Topics) { + Topics.create = async function (data) { + // This is an internal method, consider using Topics.post instead + const timestamp = data.timestamp || Date.now(); + + const tid = await db.incrObjectField('global', 'nextTid'); + + let topicData = { + tid: tid, + uid: data.uid, + cid: data.cid, + mainPid: 0, + title: data.title, + slug: `${tid}/${slugify(data.title) || 'topic'}`, + timestamp: timestamp, + lastposttime: 0, + postcount: 0, + viewcount: 0, + }; + + if (Array.isArray(data.tags) && data.tags.length) { + topicData.tags = data.tags.join(','); + } + + const result = await plugins.hooks.fire('filter:topic.create', { topic: topicData, data: data }); + topicData = result.topic; + await db.setObject(`topic:${topicData.tid}`, topicData); + + const timestampedSortedSetKeys = [ + 'topics:tid', + `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:uid:${topicData.uid}:tids`, + ]; + + const scheduled = timestamp > Date.now(); + if (scheduled) { + timestampedSortedSetKeys.push('topics:scheduled'); + } + + await Promise.all([ + db.sortedSetsAdd(timestampedSortedSetKeys, timestamp, topicData.tid), + db.sortedSetsAdd([ + 'topics:views', 'topics:posts', 'topics:votes', + `cid:${topicData.cid}:tids:votes`, + `cid:${topicData.cid}:tids:posts`, + `cid:${topicData.cid}:tids:views`, + ], 0, topicData.tid), + user.addTopicIdToUser(topicData.uid, topicData.tid, timestamp), + db.incrObjectField(`category:${topicData.cid}`, 'topic_count'), + db.incrObjectField('global', 'topicCount'), + Topics.createTags(data.tags, topicData.tid, timestamp), + scheduled ? Promise.resolve() : categories.updateRecentTid(topicData.cid, topicData.tid), + ]); + if (scheduled) { + await Topics.scheduled.pin(tid, topicData); + } + + plugins.hooks.fire('action:topic.save', { topic: _.clone(topicData), data: data }); + return topicData.tid; + }; + + Topics.post = async function (data) { + data = await plugins.hooks.fire('filter:topic.post', data); + const { uid } = data; + + data.title = String(data.title).trim(); + data.tags = data.tags || []; + if (data.content) { + data.content = utils.rtrim(data.content); + } + Topics.checkTitle(data.title); + await Topics.validateTags(data.tags, data.cid, uid); + data.tags = await Topics.filterTags(data.tags, data.cid); + if (!data.fromQueue) { + Topics.checkContent(data.content); + } + + const [categoryExists, canCreate, canTag] = await Promise.all([ + categories.exists(data.cid), + privileges.categories.can('topics:create', data.cid, uid), + privileges.categories.can('topics:tag', data.cid, uid), + ]); + + if (!categoryExists) { + throw new Error('[[error:no-category]]'); + } + + if (!canCreate || (!canTag && data.tags.length)) { + throw new Error('[[error:no-privileges]]'); + } + + await guestHandleValid(data); + if (!data.fromQueue) { + await user.isReadyToPost(uid, data.cid); + } + + const tid = await Topics.create(data); + + let postData = data; + postData.tid = tid; + postData.ip = data.req ? data.req.ip : null; + postData.isMain = true; + postData = await posts.create(postData); + postData = await onNewPost(postData, data); + + const [settings, topics] = await Promise.all([ + user.getSettings(uid), + Topics.getTopicsByTids([postData.tid], uid), + ]); + + if (!Array.isArray(topics) || !topics.length) { + throw new Error('[[error:no-topic]]'); + } + + if (uid > 0 && settings.followTopicsOnCreate) { + await Topics.follow(postData.tid, uid); + } + const topicData = topics[0]; + topicData.unreplied = true; + topicData.mainPost = postData; + topicData.index = 0; + postData.index = 0; + + if (topicData.scheduled) { + await Topics.delete(tid); + } + + analytics.increment(['topics', `topics:byCid:${topicData.cid}`]); + plugins.hooks.fire('action:topic.post', { topic: topicData, post: postData, data: data }); + + if (parseInt(uid, 10) && !topicData.scheduled) { + user.notifications.sendTopicNotificationToFollowers(uid, topicData, postData); + } + + return { + topicData: topicData, + postData: postData, + }; + }; + + Topics.reply = async function (data) { + data = await plugins.hooks.fire('filter:topic.reply', data); + const { tid } = data; + const { uid } = data; + + const topicData = await Topics.getTopicData(tid); + + await canReply(data, topicData); + + data.cid = topicData.cid; + + await guestHandleValid(data); + if (data.content) { + data.content = utils.rtrim(data.content); + } + if (!data.fromQueue) { + await user.isReadyToPost(uid, data.cid); + Topics.checkContent(data.content); + } + + // For replies to scheduled topics, don't have a timestamp older than topic's itself + if (topicData.scheduled) { + data.timestamp = topicData.lastposttime + 1; + } + + data.ip = data.req ? data.req.ip : null; + let postData = await posts.create(data); + postData = await onNewPost(postData, data); + + const settings = await user.getSettings(uid); + if (uid > 0 && settings.followTopicsOnReply) { + await Topics.follow(postData.tid, uid); + } + + if (parseInt(uid, 10)) { + user.setUserField(uid, 'lastonline', Date.now()); + } + + if (parseInt(uid, 10) || meta.config.allowGuestReplyNotifications) { + const { displayname } = postData.user; + + Topics.notifyFollowers(postData, uid, { + type: 'new-reply', + bodyShort: translator.compile('notifications:user_posted_to', displayname, postData.topic.title), + nid: `new_post:tid:${postData.topic.tid}:pid:${postData.pid}:uid:${uid}`, + mergeId: `notifications:user_posted_to|${postData.topic.tid}`, + }); + } + + analytics.increment(['posts', `posts:byCid:${data.cid}`]); + plugins.hooks.fire('action:topic.reply', { post: _.clone(postData), data: data }); + + return postData; + }; + + async function onNewPost(postData, data) { + const { tid } = postData; + const { uid } = postData; + await Topics.markAsUnreadForAll(tid); + await Topics.markAsRead([tid], uid); + const [ + userInfo, + topicInfo, + ] = await Promise.all([ + posts.getUserInfoForPosts([postData.uid], uid), + Topics.getTopicFields(tid, ['tid', 'uid', 'title', 'slug', 'cid', 'postcount', 'mainPid', 'scheduled']), + Topics.addParentPosts([postData]), + Topics.syncBacklinks(postData), + posts.parsePost(postData), + ]); + + postData.user = userInfo[0]; + postData.topic = topicInfo; + postData.index = topicInfo.postcount - 1; + + posts.overrideGuestHandle(postData, data.handle); + + postData.votes = 0; + postData.bookmarked = false; + postData.display_edit_tools = true; + postData.display_delete_tools = true; + postData.display_moderator_tools = true; + postData.display_move_tools = true; + postData.selfPost = false; + postData.timestampISO = utils.toISOString(postData.timestamp); + postData.topic.title = String(postData.topic.title); + + return postData; + } + + Topics.checkTitle = function (title) { + check(title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long'); + }; + + Topics.checkContent = function (content) { + check(content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long'); + }; + + function check(item, min, max, minError, maxError) { + // Trim and remove HTML (latter for composers that send in HTML, like redactor) + if (typeof item === 'string') { + item = utils.stripHTMLTags(item).trim(); + } + + if (item === null || item === undefined || item.length < parseInt(min, 10)) { + throw new Error(`[[error:${minError}, ${min}]]`); + } else if (item.length > parseInt(max, 10)) { + throw new Error(`[[error:${maxError}, ${max}]]`); + } + } + + async function guestHandleValid(data) { + if (meta.config.allowGuestHandles && parseInt(data.uid, 10) === 0 && data.handle) { + if (data.handle.length > meta.config.maximumUsernameLength) { + throw new Error('[[error:guest-handle-invalid]]'); + } + const exists = await user.existsBySlug(slugify(data.handle)); + if (exists) { + throw new Error('[[error:username-taken]]'); + } + } + } + + async function canReply(data, topicData) { + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + const { tid, uid } = data; + const { cid, deleted, locked, scheduled } = topicData; + + const [canReply, canSchedule, isAdminOrMod] = await Promise.all([ + privileges.topics.can('topics:reply', tid, uid), + privileges.topics.can('topics:schedule', tid, uid), + privileges.categories.isAdminOrMod(cid, uid), + ]); + + if (locked && !isAdminOrMod) { + throw new Error('[[error:topic-locked]]'); + } + + if (!scheduled && deleted && !isAdminOrMod) { + throw new Error('[[error:topic-deleted]]'); + } + + if (scheduled && !canSchedule) { + throw new Error('[[error:no-privileges]]'); + } + + if (!canReply) { + throw new Error('[[error:no-privileges]]'); + } + } +}; diff --git a/src/topics/data.js b/src/topics/data.js new file mode 100644 index 0000000000..3d3051d1d7 --- /dev/null +++ b/src/topics/data.js @@ -0,0 +1,142 @@ +'use strict'; + +const validator = require('validator'); + +const db = require('../database'); +const categories = require('../categories'); +const utils = require('../utils'); +const translator = require('../translator'); +const plugins = require('../plugins'); + +const intFields = [ + 'tid', 'cid', 'uid', 'mainPid', 'postcount', + 'viewcount', 'postercount', 'deleted', 'locked', 'pinned', + 'pinExpiry', 'timestamp', 'upvotes', 'downvotes', 'lastposttime', + 'deleterUid', +]; + +module.exports = function (Topics) { + Topics.getTopicsFields = async function (tids, fields) { + if (!Array.isArray(tids) || !tids.length) { + return []; + } + + // "scheduled" is derived from "timestamp" + if (fields.includes('scheduled') && !fields.includes('timestamp')) { + fields.push('timestamp'); + } + + const keys = tids.map(tid => `topic:${tid}`); + const topics = await db.getObjects(keys, fields); + const result = await plugins.hooks.fire('filter:topic.getFields', { + tids: tids, + topics: topics, + fields: fields, + keys: keys, + }); + result.topics.forEach(topic => modifyTopic(topic, fields)); + return result.topics; + }; + + Topics.getTopicField = async function (tid, field) { + const topic = await Topics.getTopicFields(tid, [field]); + return topic ? topic[field] : null; + }; + + Topics.getTopicFields = async function (tid, fields) { + const topics = await Topics.getTopicsFields([tid], fields); + return topics ? topics[0] : null; + }; + + Topics.getTopicData = async function (tid) { + const topics = await Topics.getTopicsFields([tid], []); + return topics && topics.length ? topics[0] : null; + }; + + Topics.getTopicsData = async function (tids) { + return await Topics.getTopicsFields(tids, []); + }; + + Topics.getCategoryData = async function (tid) { + const cid = await Topics.getTopicField(tid, 'cid'); + return await categories.getCategoryData(cid); + }; + + Topics.setTopicField = async function (tid, field, value) { + await db.setObjectField(`topic:${tid}`, field, value); + }; + + Topics.setTopicFields = async function (tid, data) { + await db.setObject(`topic:${tid}`, data); + }; + + Topics.deleteTopicField = async function (tid, field) { + await db.deleteObjectField(`topic:${tid}`, field); + }; + + Topics.deleteTopicFields = async function (tid, fields) { + await db.deleteObjectFields(`topic:${tid}`, fields); + }; +}; + +function escapeTitle(topicData) { + if (topicData) { + if (topicData.title) { + topicData.title = translator.escape(validator.escape(topicData.title)); + } + if (topicData.titleRaw) { + topicData.titleRaw = translator.escape(topicData.titleRaw); + } + } +} + +function modifyTopic(topic, fields) { + if (!topic) { + return; + } + + db.parseIntFields(topic, intFields, fields); + + if (topic.hasOwnProperty('title')) { + topic.titleRaw = topic.title; + topic.title = String(topic.title); + } + + escapeTitle(topic); + + if (topic.hasOwnProperty('timestamp')) { + topic.timestampISO = utils.toISOString(topic.timestamp); + if (!fields.length || fields.includes('scheduled')) { + topic.scheduled = topic.timestamp > Date.now(); + } + } + + if (topic.hasOwnProperty('lastposttime')) { + topic.lastposttimeISO = utils.toISOString(topic.lastposttime); + } + + if (topic.hasOwnProperty('pinExpiry')) { + topic.pinExpiryISO = utils.toISOString(topic.pinExpiry); + } + + if (topic.hasOwnProperty('upvotes') && topic.hasOwnProperty('downvotes')) { + topic.votes = topic.upvotes - topic.downvotes; + } + + if (fields.includes('teaserPid') || !fields.length) { + topic.teaserPid = topic.teaserPid || null; + } + + if (fields.includes('tags') || !fields.length) { + const tags = String(topic.tags || ''); + topic.tags = tags.split(',').filter(Boolean).map((tag) => { + const escaped = validator.escape(String(tag)); + return { + value: tag, + valueEscaped: escaped, + valueEncoded: encodeURIComponent(escaped), + class: escaped.replace(/\s/g, '-'), + }; + }); + } +} diff --git a/src/topics/delete.js b/src/topics/delete.js new file mode 100644 index 0000000000..c8d776c848 --- /dev/null +++ b/src/topics/delete.js @@ -0,0 +1,141 @@ +'use strict'; + +const db = require('../database'); + +const user = require('../user'); +const posts = require('../posts'); +const categories = require('../categories'); +const plugins = require('../plugins'); +const batch = require('../batch'); + + +module.exports = function (Topics) { + Topics.delete = async function (tid, uid) { + await removeTopicPidsFromCid(tid); + await Topics.setTopicFields(tid, { + deleted: 1, + deleterUid: uid, + deletedTimestamp: Date.now(), + }); + }; + + async function removeTopicPidsFromCid(tid) { + const [cid, pids] = await Promise.all([ + Topics.getTopicField(tid, 'cid'), + Topics.getPids(tid), + ]); + await db.sortedSetRemove(`cid:${cid}:pids`, pids); + await categories.updateRecentTidForCid(cid); + } + + async function addTopicPidsToCid(tid) { + const [cid, pids] = await Promise.all([ + Topics.getTopicField(tid, 'cid'), + Topics.getPids(tid), + ]); + let postData = await posts.getPostsFields(pids, ['pid', 'timestamp', 'deleted']); + postData = postData.filter(post => post && !post.deleted); + const pidsToAdd = postData.map(post => post.pid); + const scores = postData.map(post => post.timestamp); + await db.sortedSetAdd(`cid:${cid}:pids`, scores, pidsToAdd); + await categories.updateRecentTidForCid(cid); + } + + Topics.restore = async function (tid) { + await Promise.all([ + Topics.deleteTopicFields(tid, [ + 'deleterUid', 'deletedTimestamp', + ]), + addTopicPidsToCid(tid), + ]); + await Topics.setTopicField(tid, 'deleted', 0); + }; + + Topics.purgePostsAndTopic = async function (tid, uid) { + const mainPid = await Topics.getTopicField(tid, 'mainPid'); + await batch.processSortedSet(`tid:${tid}:posts`, async (pids) => { + await posts.purge(pids, uid); + }, { alwaysStartAt: 0, batch: 500 }); + await posts.purge(mainPid, uid); + await Topics.purge(tid, uid); + }; + + Topics.purge = async function (tid, uid) { + const [deletedTopic, tags] = await Promise.all([ + Topics.getTopicData(tid), + Topics.getTopicTags(tid), + ]); + if (!deletedTopic) { + return; + } + deletedTopic.tags = tags; + await deleteFromFollowersIgnorers(tid); + + await Promise.all([ + db.deleteAll([ + `tid:${tid}:followers`, + `tid:${tid}:ignorers`, + `tid:${tid}:posts`, + `tid:${tid}:posts:votes`, + `tid:${tid}:bookmarks`, + `tid:${tid}:posters`, + ]), + db.sortedSetsRemove([ + 'topics:tid', + 'topics:recent', + 'topics:posts', + 'topics:views', + 'topics:votes', + 'topics:scheduled', + ], tid), + deleteTopicFromCategoryAndUser(tid), + Topics.deleteTopicTags(tid), + Topics.events.purge(tid), + Topics.thumbs.deleteAll(tid), + reduceCounters(tid), + ]); + plugins.hooks.fire('action:topic.purge', { topic: deletedTopic, uid: uid }); + await db.delete(`topic:${tid}`); + }; + + async function deleteFromFollowersIgnorers(tid) { + const [followers, ignorers] = await Promise.all([ + db.getSetMembers(`tid:${tid}:followers`), + db.getSetMembers(`tid:${tid}:ignorers`), + ]); + const followerKeys = followers.map(uid => `uid:${uid}:followed_tids`); + const ignorerKeys = ignorers.map(uid => `uid:${uid}ignored_tids`); + await db.sortedSetsRemove(followerKeys.concat(ignorerKeys), tid); + } + + async function deleteTopicFromCategoryAndUser(tid) { + const topicData = await Topics.getTopicFields(tid, ['cid', 'uid']); + await Promise.all([ + db.sortedSetsRemove([ + `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:pinned`, + `cid:${topicData.cid}:tids:posts`, + `cid:${topicData.cid}:tids:lastposttime`, + `cid:${topicData.cid}:tids:votes`, + `cid:${topicData.cid}:tids:views`, + `cid:${topicData.cid}:recent_tids`, + `cid:${topicData.cid}:uid:${topicData.uid}:tids`, + `uid:${topicData.uid}:topics`, + ], tid), + user.decrementUserFieldBy(topicData.uid, 'topiccount', 1), + ]); + await categories.updateRecentTidForCid(topicData.cid); + } + + async function reduceCounters(tid) { + const incr = -1; + await db.incrObjectFieldBy('global', 'topicCount', incr); + const topicData = await Topics.getTopicFields(tid, ['cid', 'postcount']); + const postCountChange = incr * topicData.postcount; + await Promise.all([ + db.incrObjectFieldBy('global', 'postCount', postCountChange), + db.incrObjectFieldBy(`category:${topicData.cid}`, 'post_count', postCountChange), + db.incrObjectFieldBy(`category:${topicData.cid}`, 'topic_count', incr), + ]); + } +}; diff --git a/src/topics/events.js b/src/topics/events.js new file mode 100644 index 0000000000..07eaa85971 --- /dev/null +++ b/src/topics/events.js @@ -0,0 +1,212 @@ +'use strict'; + +const _ = require('lodash'); +const db = require('../database'); +const meta = require('../meta'); +const user = require('../user'); +const posts = require('../posts'); +const categories = require('../categories'); +const plugins = require('../plugins'); +const translator = require('../translator'); +const privileges = require('../privileges'); + +const Events = module.exports; + +/** + * Note: Plugins! + * + * You are able to define additional topic event types here. + * Register to hook `filter:topicEvents.init` and append your custom type to the `types` object. + * You can then log a custom topic event by calling `topics.events.log(tid, { type, uid });` + * `uid` is optional; if you pass in a valid uid in the payload, + * the user avatar/username will be rendered as part of the event text + * + */ +Events._types = { + pin: { + icon: 'fa-thumb-tack', + text: '[[topic:pinned-by]]', + }, + unpin: { + icon: 'fa-thumb-tack', + text: '[[topic:unpinned-by]]', + }, + lock: { + icon: 'fa-lock', + text: '[[topic:locked-by]]', + }, + unlock: { + icon: 'fa-unlock', + text: '[[topic:unlocked-by]]', + }, + delete: { + icon: 'fa-trash', + text: '[[topic:deleted-by]]', + }, + restore: { + icon: 'fa-trash-o', + text: '[[topic:restored-by]]', + }, + move: { + icon: 'fa-arrow-circle-right', + // text: '[[topic:moved-from-by]]', + }, + 'post-queue': { + icon: 'fa-history', + text: '[[topic:queued-by]]', + href: '/post-queue', + }, + backlink: { + icon: 'fa-link', + text: '[[topic:backlink]]', + }, + fork: { + icon: 'fa-code-fork', + text: '[[topic:forked-by]]', + }, +}; + +Events.init = async () => { + // Allow plugins to define additional topic event types + const { types } = await plugins.hooks.fire('filter:topicEvents.init', { types: Events._types }); + Events._types = types; +}; + +Events.get = async (tid, uid, reverse = false) => { + const topics = require('.'); + + if (!await topics.exists(tid)) { + throw new Error('[[error:no-topic]]'); + } + + let eventIds = await db.getSortedSetRangeWithScores(`topic:${tid}:events`, 0, -1); + const keys = eventIds.map(obj => `topicEvent:${obj.value}`); + const timestamps = eventIds.map(obj => obj.score); + eventIds = eventIds.map(obj => obj.value); + let events = await db.getObjects(keys); + events = await modifyEvent({ tid, uid, eventIds, timestamps, events }); + if (reverse) { + events.reverse(); + } + return events; +}; + +async function getUserInfo(uids) { + uids = uids.filter((uid, idx) => !isNaN(parseInt(uid, 10)) && uids.indexOf(uid) === idx); + const userData = await user.getUsersFields(uids, ['picture', 'username', 'userslug']); + const userMap = userData.reduce((memo, cur) => memo.set(cur.uid, cur), new Map()); + userMap.set('system', { + system: true, + }); + + return userMap; +} + +async function getCategoryInfo(cids) { + const uniqCids = _.uniq(cids); + const catData = await categories.getCategoriesFields(uniqCids, ['name', 'slug', 'icon', 'color', 'bgColor']); + return _.zipObject(uniqCids, catData); +} + +async function modifyEvent({ tid, uid, eventIds, timestamps, events }) { + // Add posts from post queue + const isPrivileged = await user.isPrivileged(uid); + if (isPrivileged) { + const queuedPosts = await posts.getQueuedPosts({ tid }, { metadata: false }); + events.push(...queuedPosts.map(item => ({ + type: 'post-queue', + timestamp: item.data.timestamp || Date.now(), + uid: item.data.uid, + }))); + queuedPosts.forEach((item) => { + timestamps.push(item.data.timestamp || Date.now()); + }); + } + + const [users, fromCategories] = await Promise.all([ + getUserInfo(events.map(event => event.uid).filter(Boolean)), + getCategoryInfo(events.map(event => event.fromCid).filter(Boolean)), + ]); + + // Remove backlink events if backlinks are disabled + if (meta.config.topicBacklinks !== 1) { + events = events.filter(event => event.type !== 'backlink'); + } else { + // remove backlinks that we dont have read permission + const backlinkPids = events.filter(e => e.type === 'backlink') + .map(e => e.href.split('/').pop()); + const pids = await privileges.posts.filter('topics:read', backlinkPids, uid); + events = events.filter( + e => e.type !== 'backlink' || pids.includes(e.href.split('/').pop()) + ); + } + + // Remove events whose types no longer exist (e.g. plugin uninstalled) + events = events.filter(event => Events._types.hasOwnProperty(event.type)); + + // Add user & metadata + events.forEach((event, idx) => { + event.id = parseInt(eventIds[idx], 10); + event.timestamp = timestamps[idx]; + event.timestampISO = new Date(timestamps[idx]).toISOString(); + if (event.hasOwnProperty('uid')) { + event.user = users.get(event.uid === 'system' ? 'system' : parseInt(event.uid, 10)); + } + if (event.hasOwnProperty('fromCid')) { + event.fromCategory = fromCategories[event.fromCid]; + event.text = translator.compile('topic:moved-from-by', event.fromCategory.name); + } + + Object.assign(event, Events._types[event.type]); + }); + + // Sort events + events.sort((a, b) => a.timestamp - b.timestamp); + + return events; +} + +Events.log = async (tid, payload) => { + const topics = require('.'); + const { type } = payload; + const timestamp = payload.timestamp || Date.now(); + + if (!Events._types.hasOwnProperty(type)) { + throw new Error(`[[error:topic-event-unrecognized, ${type}]]`); + } else if (!await topics.exists(tid)) { + throw new Error('[[error:no-topic]]'); + } + + const eventId = await db.incrObjectField('global', 'nextTopicEventId'); + + await Promise.all([ + db.setObject(`topicEvent:${eventId}`, payload), + db.sortedSetAdd(`topic:${tid}:events`, timestamp, eventId), + ]); + + let events = await modifyEvent({ + eventIds: [eventId], + timestamps: [timestamp], + events: [payload], + }); + + ({ events } = await plugins.hooks.fire('filter:topic.events.log', { events })); + return events; +}; + +Events.purge = async (tid, eventIds = []) => { + if (eventIds.length) { + const isTopicEvent = await db.isSortedSetMembers(`topic:${tid}:events`, eventIds); + eventIds = eventIds.filter((id, index) => isTopicEvent[index]); + await Promise.all([ + db.sortedSetRemove(`topic:${tid}:events`, eventIds), + db.deleteAll(eventIds.map(id => `topicEvent:${id}`)), + ]); + } else { + const keys = [`topic:${tid}:events`]; + const eventIds = await db.getSortedSetRange(keys[0], 0, -1); + keys.push(...eventIds.map(id => `topicEvent:${id}`)); + + await db.deleteAll(keys); + } +}; diff --git a/src/topics/follow.js b/src/topics/follow.js new file mode 100644 index 0000000000..fd4f34e7f9 --- /dev/null +++ b/src/topics/follow.js @@ -0,0 +1,177 @@ + +'use strict'; + +const db = require('../database'); +const notifications = require('../notifications'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +module.exports = function (Topics) { + Topics.toggleFollow = async function (tid, uid) { + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); + } + const isFollowing = await Topics.isFollowing([tid], uid); + if (isFollowing[0]) { + await Topics.unfollow(tid, uid); + } else { + await Topics.follow(tid, uid); + } + return !isFollowing[0]; + }; + + Topics.follow = async function (tid, uid) { + await setWatching(follow, unignore, 'action:topic.follow', tid, uid); + }; + + Topics.unfollow = async function (tid, uid) { + await setWatching(unfollow, unignore, 'action:topic.unfollow', tid, uid); + }; + + Topics.ignore = async function (tid, uid) { + await setWatching(ignore, unfollow, 'action:topic.ignore', tid, uid); + }; + + async function setWatching(method1, method2, hook, tid, uid) { + if (!(parseInt(uid, 10) > 0)) { + throw new Error('[[error:not-logged-in]]'); + } + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); + } + await method1(tid, uid); + await method2(tid, uid); + plugins.hooks.fire(hook, { uid: uid, tid: tid }); + } + + async function follow(tid, uid) { + await addToSets(`tid:${tid}:followers`, `uid:${uid}:followed_tids`, tid, uid); + } + + async function unfollow(tid, uid) { + await removeFromSets(`tid:${tid}:followers`, `uid:${uid}:followed_tids`, tid, uid); + } + + async function ignore(tid, uid) { + await addToSets(`tid:${tid}:ignorers`, `uid:${uid}:ignored_tids`, tid, uid); + } + + async function unignore(tid, uid) { + await removeFromSets(`tid:${tid}:ignorers`, `uid:${uid}:ignored_tids`, tid, uid); + } + + async function addToSets(set1, set2, tid, uid) { + await db.setAdd(set1, uid); + await db.sortedSetAdd(set2, Date.now(), tid); + } + + async function removeFromSets(set1, set2, tid, uid) { + await db.setRemove(set1, uid); + await db.sortedSetRemove(set2, tid); + } + + Topics.isFollowing = async function (tids, uid) { + return await isIgnoringOrFollowing('followers', tids, uid); + }; + + Topics.isIgnoring = async function (tids, uid) { + return await isIgnoringOrFollowing('ignorers', tids, uid); + }; + + Topics.getFollowData = async function (tids, uid) { + if (!Array.isArray(tids)) { + return; + } + if (parseInt(uid, 10) <= 0) { + return tids.map(() => ({ following: false, ignoring: false })); + } + const keys = []; + tids.forEach(tid => keys.push(`tid:${tid}:followers`, `tid:${tid}:ignorers`)); + + const data = await db.isMemberOfSets(keys, uid); + + const followData = []; + for (let i = 0; i < data.length; i += 2) { + followData.push({ + following: data[i], + ignoring: data[i + 1], + }); + } + return followData; + }; + + async function isIgnoringOrFollowing(set, tids, uid) { + if (!Array.isArray(tids)) { + return; + } + if (parseInt(uid, 10) <= 0) { + return tids.map(() => false); + } + const keys = tids.map(tid => `tid:${tid}:${set}`); + return await db.isMemberOfSets(keys, uid); + } + + Topics.getFollowers = async function (tid) { + return await db.getSetMembers(`tid:${tid}:followers`); + }; + + Topics.getIgnorers = async function (tid) { + return await db.getSetMembers(`tid:${tid}:ignorers`); + }; + + Topics.filterIgnoringUids = async function (tid, uids) { + const isIgnoring = await db.isSetMembers(`tid:${tid}:ignorers`, uids); + const readingUids = uids.filter((uid, index) => uid && !isIgnoring[index]); + return readingUids; + }; + + Topics.filterWatchedTids = async function (tids, uid) { + if (parseInt(uid, 10) <= 0) { + return []; + } + const scores = await db.sortedSetScores(`uid:${uid}:followed_tids`, tids); + return tids.filter((tid, index) => tid && !!scores[index]); + }; + + Topics.filterNotIgnoredTids = async function (tids, uid) { + if (parseInt(uid, 10) <= 0) { + return tids; + } + const scores = await db.sortedSetScores(`uid:${uid}:ignored_tids`, tids); + return tids.filter((tid, index) => tid && !scores[index]); + }; + + Topics.notifyFollowers = async function (postData, exceptUid, notifData) { + notifData = notifData || {}; + let followers = await Topics.getFollowers(postData.topic.tid); + const index = followers.indexOf(String(exceptUid)); + if (index !== -1) { + followers.splice(index, 1); + } + + followers = await privileges.topics.filterUids('topics:read', postData.topic.tid, followers); + if (!followers.length) { + return; + } + + let { title } = postData.topic; + if (title) { + title = utils.decodeHTMLEntities(title); + } + + const notification = await notifications.create({ + subject: title, + bodyLong: postData.content, + pid: postData.pid, + path: `/post/${postData.pid}`, + tid: postData.topic.tid, + from: exceptUid, + topicTitle: title, + ...notifData, + }); + notifications.push(notification, followers); + }; +}; diff --git a/src/topics/fork.js b/src/topics/fork.js new file mode 100644 index 0000000000..49c5d9dcd4 --- /dev/null +++ b/src/topics/fork.js @@ -0,0 +1,159 @@ + +'use strict'; + +const db = require('../database'); +const posts = require('../posts'); +const categories = require('../categories'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const meta = require('../meta'); + +module.exports = function (Topics) { + Topics.createTopicFromPosts = async function (uid, title, pids, fromTid) { + if (title) { + title = title.trim(); + } + + if (title.length < meta.config.minimumTitleLength) { + throw new Error(`[[error:title-too-short, ${meta.config.minimumTitleLength}]]`); + } else if (title.length > meta.config.maximumTitleLength) { + throw new Error(`[[error:title-too-long, ${meta.config.maximumTitleLength}]]`); + } + + if (!pids || !pids.length) { + throw new Error('[[error:invalid-pid]]'); + } + + pids.sort((a, b) => a - b); + + const mainPid = pids[0]; + const cid = await posts.getCidByPid(mainPid); + + const [postData, isAdminOrMod] = await Promise.all([ + posts.getPostData(mainPid), + privileges.categories.isAdminOrMod(cid, uid), + ]); + + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + + const scheduled = postData.timestamp > Date.now(); + const params = { + uid: postData.uid, + title: title, + cid: cid, + timestamp: scheduled && postData.timestamp, + }; + const result = await plugins.hooks.fire('filter:topic.fork', { + params: params, + tid: postData.tid, + }); + + const tid = await Topics.create(result.params); + await Topics.updateTopicBookmarks(fromTid, pids); + + for (const pid of pids) { + /* eslint-disable no-await-in-loop */ + const canEdit = await privileges.posts.canEdit(pid, uid); + if (!canEdit.flag) { + throw new Error(canEdit.message); + } + await Topics.movePostToTopic(uid, pid, tid, scheduled); + } + + await Topics.updateLastPostTime(tid, scheduled ? (postData.timestamp + 1) : Date.now()); + + await Promise.all([ + Topics.setTopicFields(tid, { + upvotes: postData.upvotes, + downvotes: postData.downvotes, + }), + db.sortedSetsAdd(['topics:votes', `cid:${cid}:tids:votes`], postData.votes, tid), + Topics.events.log(fromTid, { type: 'fork', uid, href: `/topic/${tid}`, timestamp: postData.timestamp }), + ]); + + plugins.hooks.fire('action:topic.fork', { tid: tid, fromTid: fromTid, uid: uid }); + + return await Topics.getTopicData(tid); + }; + + Topics.movePostToTopic = async function (callerUid, pid, tid, forceScheduled = false) { + tid = parseInt(tid, 10); + const topicData = await Topics.getTopicFields(tid, ['tid', 'scheduled']); + if (!topicData.tid) { + throw new Error('[[error:no-topic]]'); + } + if (!forceScheduled && topicData.scheduled) { + throw new Error('[[error:cant-move-posts-to-scheduled]]'); + } + const postData = await posts.getPostFields(pid, ['tid', 'uid', 'timestamp', 'upvotes', 'downvotes']); + if (!postData || !postData.tid) { + throw new Error('[[error:no-post]]'); + } + + const isSourceTopicScheduled = await Topics.getTopicField(postData.tid, 'scheduled'); + if (!forceScheduled && isSourceTopicScheduled) { + throw new Error('[[error:cant-move-from-scheduled-to-existing]]'); + } + + if (postData.tid === tid) { + throw new Error('[[error:cant-move-to-same-topic]]'); + } + + postData.pid = pid; + + await Topics.removePostFromTopic(postData.tid, postData); + await Promise.all([ + updateCategory(postData, tid), + posts.setPostField(pid, 'tid', tid), + Topics.addPostToTopic(tid, postData), + ]); + + await Promise.all([ + Topics.updateLastPostTimeFromLastPid(tid), + Topics.updateLastPostTimeFromLastPid(postData.tid), + ]); + plugins.hooks.fire('action:post.move', { uid: callerUid, post: postData, tid: tid }); + }; + + async function updateCategory(postData, toTid) { + const topicData = await Topics.getTopicsFields([postData.tid, toTid], ['cid', 'pinned']); + + if (!topicData[0].cid || !topicData[1].cid) { + return; + } + + if (!topicData[0].pinned) { + await db.sortedSetIncrBy(`cid:${topicData[0].cid}:tids:posts`, -1, postData.tid); + } + if (!topicData[1].pinned) { + await db.sortedSetIncrBy(`cid:${topicData[1].cid}:tids:posts`, 1, toTid); + } + if (topicData[0].cid === topicData[1].cid) { + await categories.updateRecentTidForCid(topicData[0].cid); + return; + } + const removeFrom = [ + `cid:${topicData[0].cid}:pids`, + `cid:${topicData[0].cid}:uid:${postData.uid}:pids`, + `cid:${topicData[0].cid}:uid:${postData.uid}:pids:votes`, + ]; + const tasks = [ + db.incrObjectFieldBy(`category:${topicData[0].cid}`, 'post_count', -1), + db.incrObjectFieldBy(`category:${topicData[1].cid}`, 'post_count', 1), + db.sortedSetRemove(removeFrom, postData.pid), + db.sortedSetAdd(`cid:${topicData[1].cid}:pids`, postData.timestamp, postData.pid), + db.sortedSetAdd(`cid:${topicData[1].cid}:uid:${postData.uid}:pids`, postData.timestamp, postData.pid), + ]; + if (postData.votes > 0 || postData.votes < 0) { + tasks.push(db.sortedSetAdd(`cid:${topicData[1].cid}:uid:${postData.uid}:pids:votes`, postData.votes, postData.pid)); + } + + await Promise.all(tasks); + await Promise.all([ + categories.updateRecentTidForCid(topicData[0].cid), + categories.updateRecentTidForCid(topicData[1].cid), + ]); + } +}; diff --git a/src/topics/index.js b/src/topics/index.js new file mode 100644 index 0000000000..0287cb62fe --- /dev/null +++ b/src/topics/index.js @@ -0,0 +1,288 @@ +'use strict'; + +const _ = require('lodash'); +const validator = require('validator'); + +const db = require('../database'); +const posts = require('../posts'); +const utils = require('../utils'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const user = require('../user'); +const categories = require('../categories'); +const privileges = require('../privileges'); +const social = require('../social'); + +const Topics = module.exports; + +require('./data')(Topics); +require('./create')(Topics); +require('./delete')(Topics); +require('./sorted')(Topics); +require('./unread')(Topics); +require('./recent')(Topics); +require('./user')(Topics); +require('./fork')(Topics); +require('./posts')(Topics); +require('./follow')(Topics); +require('./tags')(Topics); +require('./teaser')(Topics); +Topics.scheduled = require('./scheduled'); +require('./suggested')(Topics); +require('./tools')(Topics); +Topics.thumbs = require('./thumbs'); +require('./bookmarks')(Topics); +require('./merge')(Topics); +Topics.events = require('./events'); + +Topics.exists = async function (tids) { + return await db.exists( + Array.isArray(tids) ? tids.map(tid => `topic:${tid}`) : `topic:${tids}` + ); +}; + +Topics.getTopicsFromSet = async function (set, uid, start, stop) { + const tids = await db.getSortedSetRevRange(set, start, stop); + const topics = await Topics.getTopics(tids, uid); + Topics.calculateTopicIndices(topics, start); + return { topics: topics, nextStart: stop + 1 }; +}; + +Topics.getTopics = async function (tids, options) { + let uid = options; + if (typeof options === 'object') { + uid = options.uid; + } + + tids = await privileges.topics.filterTids('topics:read', tids, uid); + return await Topics.getTopicsByTids(tids, options); +}; + +Topics.getTopicsByTids = async function (tids, options) { + if (!Array.isArray(tids) || !tids.length) { + return []; + } + let uid = options; + if (typeof options === 'object') { + uid = options.uid; + } + + async function loadTopics() { + const topics = await Topics.getTopicsData(tids); + const uids = _.uniq(topics.map(t => t && t.uid && t.uid.toString()).filter(v => utils.isNumber(v))); + const cids = _.uniq(topics.map(t => t && t.cid && t.cid.toString()).filter(v => utils.isNumber(v))); + const guestTopics = topics.filter(t => t && t.uid === 0); + + async function loadGuestHandles() { + const mainPids = guestTopics.map(t => t.mainPid); + const postData = await posts.getPostsFields(mainPids, ['handle']); + return postData.map(p => p.handle); + } + + async function loadShowfullnameSettings() { + if (meta.config.hideFullname) { + return uids.map(() => ({ showfullname: false })); + } + const data = await db.getObjectsFields(uids.map(uid => `user:${uid}:settings`), ['showfullname']); + data.forEach((settings) => { + settings.showfullname = parseInt(settings.showfullname, 10) === 1; + }); + return data; + } + + const [teasers, users, userSettings, categoriesData, guestHandles, thumbs] = await Promise.all([ + Topics.getTeasers(topics, options), + user.getUsersFields(uids, ['uid', 'username', 'fullname', 'userslug', 'reputation', 'postcount', 'picture', 'signature', 'banned', 'status']), + loadShowfullnameSettings(), + categories.getCategoriesFields(cids, ['cid', 'name', 'slug', 'icon', 'backgroundImage', 'imageClass', 'bgColor', 'color', 'disabled']), + loadGuestHandles(), + Topics.thumbs.load(topics), + ]); + + users.forEach((userObj, idx) => { + // Hide fullname if needed + if (!userSettings[idx].showfullname) { + userObj.fullname = undefined; + } + }); + + return { + topics, + teasers, + usersMap: _.zipObject(uids, users), + categoriesMap: _.zipObject(cids, categoriesData), + tidToGuestHandle: _.zipObject(guestTopics.map(t => t.tid), guestHandles), + thumbs, + }; + } + + const [result, hasRead, isIgnored, bookmarks, callerSettings] = await Promise.all([ + loadTopics(), + Topics.hasReadTopics(tids, uid), + Topics.isIgnoring(tids, uid), + Topics.getUserBookmarks(tids, uid), + user.getSettings(uid), + ]); + + const sortNewToOld = callerSettings.topicPostSort === 'newest_to_oldest'; + result.topics.forEach((topic, i) => { + if (topic) { + topic.thumbs = result.thumbs[i]; + topic.category = result.categoriesMap[topic.cid]; + topic.user = topic.uid ? result.usersMap[topic.uid] : { ...result.usersMap[topic.uid] }; + if (result.tidToGuestHandle[topic.tid]) { + topic.user.username = validator.escape(result.tidToGuestHandle[topic.tid]); + topic.user.displayname = topic.user.username; + } + topic.teaser = result.teasers[i] || null; + topic.isOwner = topic.uid === parseInt(uid, 10); + topic.ignored = isIgnored[i]; + topic.unread = parseInt(uid, 10) <= 0 || (!hasRead[i] && !isIgnored[i]); + topic.bookmark = sortNewToOld ? + Math.max(1, topic.postcount + 2 - bookmarks[i]) : + Math.min(topic.postcount, bookmarks[i] + 1); + topic.unreplied = !topic.teaser; + + topic.icons = []; + } + }); + + const filteredTopics = result.topics.filter(topic => topic && topic.category && !topic.category.disabled); + + const hookResult = await plugins.hooks.fire('filter:topics.get', { topics: filteredTopics, uid: uid }); + return hookResult.topics; +}; + +Topics.getTopicWithPosts = async function (topicData, set, uid, start, stop, reverse) { + const [ + posts, + category, + tagWhitelist, + threadTools, + followData, + bookmark, + postSharing, + deleter, + merger, + related, + thumbs, + events, + ] = await Promise.all([ + Topics.getTopicPosts(topicData, set, start, stop, uid, reverse), + categories.getCategoryData(topicData.cid), + categories.getTagWhitelist([topicData.cid]), + plugins.hooks.fire('filter:topic.thread_tools', { topic: topicData, uid: uid, tools: [] }), + Topics.getFollowData([topicData.tid], uid), + Topics.getUserBookmark(topicData.tid, uid), + social.getActivePostSharing(), + getDeleter(topicData), + getMerger(topicData), + Topics.getRelatedTopics(topicData, uid), + Topics.thumbs.load([topicData]), + Topics.events.get(topicData.tid, uid, reverse), + ]); + + topicData.thumbs = thumbs[0]; + topicData.posts = posts; + topicData.events = events; + topicData.posts.forEach((p) => { + p.events = events.filter( + event => event.timestamp >= p.eventStart && event.timestamp < p.eventEnd + ); + }); + + topicData.category = category; + topicData.tagWhitelist = tagWhitelist[0]; + topicData.minTags = category.minTags; + topicData.maxTags = category.maxTags; + topicData.thread_tools = threadTools.tools; + topicData.isFollowing = followData[0].following; + topicData.isNotFollowing = !followData[0].following && !followData[0].ignoring; + topicData.isIgnoring = followData[0].ignoring; + topicData.bookmark = bookmark; + topicData.postSharing = postSharing; + topicData.deleter = deleter; + if (deleter) { + topicData.deletedTimestampISO = utils.toISOString(topicData.deletedTimestamp); + } + topicData.merger = merger; + if (merger) { + topicData.mergedTimestampISO = utils.toISOString(topicData.mergedTimestamp); + } + topicData.related = related || []; + topicData.unreplied = topicData.postcount === 1; + topicData.icons = []; + + const result = await plugins.hooks.fire('filter:topic.get', { topic: topicData, uid: uid }); + return result.topic; +}; + +async function getDeleter(topicData) { + if (!parseInt(topicData.deleterUid, 10)) { + return null; + } + return await user.getUserFields(topicData.deleterUid, ['username', 'userslug', 'picture']); +} + +async function getMerger(topicData) { + if (!parseInt(topicData.mergerUid, 10)) { + return null; + } + const [ + merger, + mergedIntoTitle, + ] = await Promise.all([ + user.getUserFields(topicData.mergerUid, ['username', 'userslug', 'picture']), + Topics.getTopicField(topicData.mergeIntoTid, 'title'), + ]); + merger.mergedIntoTitle = mergedIntoTitle; + return merger; +} + +Topics.getMainPost = async function (tid, uid) { + const mainPosts = await Topics.getMainPosts([tid], uid); + return Array.isArray(mainPosts) && mainPosts.length ? mainPosts[0] : null; +}; + +Topics.getMainPids = async function (tids) { + if (!Array.isArray(tids) || !tids.length) { + return []; + } + const topicData = await Topics.getTopicsFields(tids, ['mainPid']); + return topicData.map(topic => topic && topic.mainPid); +}; + +Topics.getMainPosts = async function (tids, uid) { + const mainPids = await Topics.getMainPids(tids); + return await getMainPosts(mainPids, uid); +}; + +async function getMainPosts(mainPids, uid) { + let postData = await posts.getPostsByPids(mainPids, uid); + postData = await user.blocks.filter(uid, postData); + postData.forEach((post) => { + if (post) { + post.index = 0; + } + }); + return await Topics.addPostData(postData, uid); +} + +Topics.isLocked = async function (tid) { + const locked = await Topics.getTopicField(tid, 'locked'); + return locked === 1; +}; + +Topics.search = async function (tid, term) { + if (!tid || !term) { + throw new Error('[[error:invalid-data]]'); + } + const result = await plugins.hooks.fire('filter:topic.search', { + tid: tid, + term: term, + ids: [], + }); + return Array.isArray(result) ? result : result.ids; +}; + +require('../promisify')(Topics); diff --git a/src/topics/merge.js b/src/topics/merge.js new file mode 100644 index 0000000000..d6e238eef8 --- /dev/null +++ b/src/topics/merge.js @@ -0,0 +1,82 @@ +'use strict'; + +const plugins = require('../plugins'); +const posts = require('../posts'); + +module.exports = function (Topics) { + Topics.merge = async function (tids, uid, options) { + options = options || {}; + + const topicsData = await Topics.getTopicsFields(tids, ['scheduled']); + if (topicsData.some(t => t.scheduled)) { + throw new Error('[[error:cant-merge-scheduled]]'); + } + + const oldestTid = findOldestTopic(tids); + let mergeIntoTid = oldestTid; + if (options.mainTid) { + mergeIntoTid = options.mainTid; + } else if (options.newTopicTitle) { + mergeIntoTid = await createNewTopic(options.newTopicTitle, oldestTid); + } + + const otherTids = tids.sort((a, b) => a - b) + .filter(tid => tid && parseInt(tid, 10) !== parseInt(mergeIntoTid, 10)); + + for (const tid of otherTids) { + /* eslint-disable no-await-in-loop */ + const pids = await Topics.getPids(tid); + for (const pid of pids) { + await Topics.movePostToTopic(uid, pid, mergeIntoTid); + } + + await Topics.setTopicField(tid, 'mainPid', 0); + await Topics.delete(tid, uid); + await Topics.setTopicFields(tid, { + mergeIntoTid: mergeIntoTid, + mergerUid: uid, + mergedTimestamp: Date.now(), + }); + } + + await Promise.all([ + posts.updateQueuedPostsTopic(mergeIntoTid, otherTids), + updateViewCount(mergeIntoTid, tids), + ]); + + plugins.hooks.fire('action:topic.merge', { + uid: uid, + tids: tids, + mergeIntoTid: mergeIntoTid, + otherTids: otherTids, + }); + return mergeIntoTid; + }; + + async function createNewTopic(title, oldestTid) { + const topicData = await Topics.getTopicFields(oldestTid, ['uid', 'cid']); + const params = { + uid: topicData.uid, + cid: topicData.cid, + title: title, + }; + const result = await plugins.hooks.fire('filter:topic.mergeCreateNewTopic', { + oldestTid: oldestTid, + params: params, + }); + const tid = await Topics.create(result.params); + return tid; + } + + async function updateViewCount(mergeIntoTid, tids) { + const topicData = await Topics.getTopicsFields(tids, ['viewcount']); + const totalViewCount = topicData.reduce( + (count, topic) => count + parseInt(topic.viewcount, 10), 0 + ); + await Topics.setTopicField(mergeIntoTid, 'viewcount', totalViewCount); + } + + function findOldestTopic(tids) { + return Math.min.apply(null, tids); + } +}; diff --git a/src/topics/posts.js b/src/topics/posts.js new file mode 100644 index 0000000000..d045d56808 --- /dev/null +++ b/src/topics/posts.js @@ -0,0 +1,407 @@ + +'use strict'; + +const _ = require('lodash'); +const validator = require('validator'); +const nconf = require('nconf'); + +const db = require('../database'); +const user = require('../user'); +const posts = require('../posts'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const backlinkRegex = new RegExp(`(?:${nconf.get('url').replace('/', '\\/')}|\b|\\s)\\/topic\\/(\\d+)(?:\\/\\w+)?`, 'g'); + +module.exports = function (Topics) { + Topics.onNewPostMade = async function (postData) { + await Topics.updateLastPostTime(postData.tid, postData.timestamp); + await Topics.addPostToTopic(postData.tid, postData); + }; + + Topics.getTopicPosts = async function (topicData, set, start, stop, uid, reverse) { + if (!topicData) { + return []; + } + + let repliesStart = start; + let repliesStop = stop; + if (stop > 0) { + repliesStop -= 1; + if (start > 0) { + repliesStart -= 1; + } + } + let pids = []; + if (start !== 0 || stop !== 0) { + pids = await posts.getPidsFromSet(set, repliesStart, repliesStop, reverse); + } + if (!pids.length && !topicData.mainPid) { + return []; + } + + if (topicData.mainPid && start === 0) { + pids.unshift(topicData.mainPid); + } + let postData = await posts.getPostsByPids(pids, uid); + if (!postData.length) { + return []; + } + let replies = postData; + if (topicData.mainPid && start === 0) { + postData[0].index = 0; + replies = postData.slice(1); + } + + Topics.calculatePostIndices(replies, repliesStart); + await addEventStartEnd(postData, set, reverse, topicData); + const allPosts = postData.slice(); + postData = await user.blocks.filter(uid, postData); + if (allPosts.length !== postData.length) { + const includedPids = new Set(postData.map(p => p.pid)); + allPosts.reverse().forEach((p, index) => { + if (!includedPids.has(p.pid) && allPosts[index + 1] && !reverse) { + allPosts[index + 1].eventEnd = p.eventEnd; + } + }); + } + + const result = await plugins.hooks.fire('filter:topic.getPosts', { + topic: topicData, + uid: uid, + posts: await Topics.addPostData(postData, uid), + }); + return result.posts; + }; + + async function addEventStartEnd(postData, set, reverse, topicData) { + if (!postData.length) { + return; + } + postData.forEach((p, index) => { + if (p && p.index === 0 && reverse) { + p.eventStart = topicData.lastposttime; + p.eventEnd = Date.now(); + } else if (p && postData[index + 1]) { + p.eventStart = reverse ? postData[index + 1].timestamp : p.timestamp; + p.eventEnd = reverse ? p.timestamp : postData[index + 1].timestamp; + } + }); + const lastPost = postData[postData.length - 1]; + if (lastPost) { + lastPost.eventStart = reverse ? topicData.timestamp : lastPost.timestamp; + lastPost.eventEnd = reverse ? lastPost.timestamp : Date.now(); + if (lastPost.index) { + const nextPost = await db[reverse ? 'getSortedSetRevRangeWithScores' : 'getSortedSetRangeWithScores'](set, lastPost.index, lastPost.index); + if (reverse) { + lastPost.eventStart = nextPost.length ? nextPost[0].score : lastPost.eventStart; + } else { + lastPost.eventEnd = nextPost.length ? nextPost[0].score : lastPost.eventEnd; + } + } + } + } + + Topics.addPostData = async function (postData, uid) { + if (!Array.isArray(postData) || !postData.length) { + return []; + } + const pids = postData.map(post => post && post.pid); + + async function getPostUserData(field, method) { + const uids = _.uniq(postData.filter(p => p && parseInt(p[field], 10) >= 0).map(p => p[field])); + const userData = await method(uids); + return _.zipObject(uids, userData); + } + const [ + bookmarks, + voteData, + userData, + editors, + replies, + ] = await Promise.all([ + posts.hasBookmarked(pids, uid), + posts.getVoteStatusByPostIDs(pids, uid), + getPostUserData('uid', async uids => await posts.getUserInfoForPosts(uids, uid)), + getPostUserData('editor', async uids => await user.getUsersFields(uids, ['uid', 'username', 'userslug'])), + getPostReplies(pids, uid), + Topics.addParentPosts(postData), + ]); + + postData.forEach((postObj, i) => { + if (postObj) { + postObj.user = postObj.uid ? userData[postObj.uid] : { ...userData[postObj.uid] }; + postObj.editor = postObj.editor ? editors[postObj.editor] : null; + postObj.bookmarked = bookmarks[i]; + postObj.upvoted = voteData.upvotes[i]; + postObj.downvoted = voteData.downvotes[i]; + postObj.votes = postObj.votes || 0; + postObj.replies = replies[i]; + postObj.selfPost = parseInt(uid, 10) > 0 && parseInt(uid, 10) === postObj.uid; + + // Username override for guests, if enabled + if (meta.config.allowGuestHandles && postObj.uid === 0 && postObj.handle) { + postObj.user.username = validator.escape(String(postObj.handle)); + postObj.user.displayname = postObj.user.username; + } + } + }); + + const result = await plugins.hooks.fire('filter:topics.addPostData', { + posts: postData, + uid: uid, + }); + return result.posts; + }; + + Topics.modifyPostsByPrivilege = function (topicData, topicPrivileges) { + const loggedIn = parseInt(topicPrivileges.uid, 10) > 0; + topicData.posts.forEach((post) => { + if (post) { + post.topicOwnerPost = parseInt(topicData.uid, 10) === parseInt(post.uid, 10); + post.display_edit_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:edit']); + post.display_delete_tools = topicPrivileges.isAdminOrMod || (post.selfPost && topicPrivileges['posts:delete']); + post.display_moderator_tools = post.display_edit_tools || post.display_delete_tools; + post.display_move_tools = topicPrivileges.isAdminOrMod && post.index !== 0; + post.display_post_menu = topicPrivileges.isAdminOrMod || + (post.selfPost && + ((!topicData.locked && !post.deleted) || + (post.deleted && parseInt(post.deleterUid, 10) === parseInt(topicPrivileges.uid, 10)))) || + ((loggedIn || topicData.postSharing.length) && !post.deleted); + post.ip = topicPrivileges.isAdminOrMod ? post.ip : undefined; + + posts.modifyPostByPrivilege(post, topicPrivileges); + } + }); + }; + + Topics.addParentPosts = async function (postData) { + let parentPids = postData.map(postObj => (postObj && postObj.hasOwnProperty('toPid') ? parseInt(postObj.toPid, 10) : null)).filter(Boolean); + + if (!parentPids.length) { + return; + } + parentPids = _.uniq(parentPids); + const parentPosts = await posts.getPostsFields(parentPids, ['uid']); + const parentUids = _.uniq(parentPosts.map(postObj => postObj && postObj.uid)); + const userData = await user.getUsersFields(parentUids, ['username']); + + const usersMap = {}; + userData.forEach((user) => { + usersMap[user.uid] = user.username; + }); + const parents = {}; + parentPosts.forEach((post, i) => { + parents[parentPids[i]] = { username: usersMap[post.uid] }; + }); + + postData.forEach((post) => { + post.parent = parents[post.toPid]; + }); + }; + + Topics.calculatePostIndices = function (posts, start) { + posts.forEach((post, index) => { + if (post) { + post.index = start + index + 1; + } + }); + }; + + Topics.getLatestUndeletedPid = async function (tid) { + const pid = await Topics.getLatestUndeletedReply(tid); + if (pid) { + return pid; + } + const mainPid = await Topics.getTopicField(tid, 'mainPid'); + const mainPost = await posts.getPostFields(mainPid, ['pid', 'deleted']); + return mainPost.pid && !mainPost.deleted ? mainPost.pid : null; + }; + + Topics.getLatestUndeletedReply = async function (tid) { + let isDeleted = false; + let index = 0; + do { + /* eslint-disable no-await-in-loop */ + const pids = await db.getSortedSetRevRange(`tid:${tid}:posts`, index, index); + if (!pids.length) { + return null; + } + isDeleted = await posts.getPostField(pids[0], 'deleted'); + if (!isDeleted) { + return parseInt(pids[0], 10); + } + index += 1; + } while (isDeleted); + }; + + Topics.addPostToTopic = async function (tid, postData) { + const mainPid = await Topics.getTopicField(tid, 'mainPid'); + if (!parseInt(mainPid, 10)) { + await Topics.setTopicField(tid, 'mainPid', postData.pid); + } else { + const upvotes = parseInt(postData.upvotes, 10) || 0; + const downvotes = parseInt(postData.downvotes, 10) || 0; + const votes = upvotes - downvotes; + await db.sortedSetsAdd([ + `tid:${tid}:posts`, `tid:${tid}:posts:votes`, + ], [postData.timestamp, votes], postData.pid); + } + await Topics.increasePostCount(tid); + await db.sortedSetIncrBy(`tid:${tid}:posters`, 1, postData.uid); + const posterCount = await db.sortedSetCard(`tid:${tid}:posters`); + await Topics.setTopicField(tid, 'postercount', posterCount); + await Topics.updateTeaser(tid); + }; + + Topics.removePostFromTopic = async function (tid, postData) { + await db.sortedSetsRemove([ + `tid:${tid}:posts`, + `tid:${tid}:posts:votes`, + ], postData.pid); + await Topics.decreasePostCount(tid); + await db.sortedSetIncrBy(`tid:${tid}:posters`, -1, postData.uid); + await db.sortedSetsRemoveRangeByScore([`tid:${tid}:posters`], '-inf', 0); + const posterCount = await db.sortedSetCard(`tid:${tid}:posters`); + await Topics.setTopicField(tid, 'postercount', posterCount); + await Topics.updateTeaser(tid); + }; + + Topics.getPids = async function (tid) { + let [mainPid, pids] = await Promise.all([ + Topics.getTopicField(tid, 'mainPid'), + db.getSortedSetRange(`tid:${tid}:posts`, 0, -1), + ]); + if (parseInt(mainPid, 10)) { + pids = [mainPid].concat(pids); + } + return pids; + }; + + Topics.increasePostCount = async function (tid) { + incrementFieldAndUpdateSortedSet(tid, 'postcount', 1, 'topics:posts'); + }; + + Topics.decreasePostCount = async function (tid) { + incrementFieldAndUpdateSortedSet(tid, 'postcount', -1, 'topics:posts'); + }; + + Topics.increaseViewCount = async function (tid) { + const cid = await Topics.getTopicField(tid, 'cid'); + incrementFieldAndUpdateSortedSet(tid, 'viewcount', 1, ['topics:views', `cid:${cid}:tids:views`]); + }; + + async function incrementFieldAndUpdateSortedSet(tid, field, by, set) { + const value = await db.incrObjectFieldBy(`topic:${tid}`, field, by); + await db[Array.isArray(set) ? 'sortedSetsAdd' : 'sortedSetAdd'](set, value, tid); + } + + Topics.getTitleByPid = async function (pid) { + return await Topics.getTopicFieldByPid('title', pid); + }; + + Topics.getTopicFieldByPid = async function (field, pid) { + const tid = await posts.getPostField(pid, 'tid'); + return await Topics.getTopicField(tid, field); + }; + + Topics.getTopicDataByPid = async function (pid) { + const tid = await posts.getPostField(pid, 'tid'); + return await Topics.getTopicData(tid); + }; + + Topics.getPostCount = async function (tid) { + return await db.getObjectField(`topic:${tid}`, 'postcount'); + }; + + async function getPostReplies(pids, callerUid) { + const keys = pids.map(pid => `pid:${pid}:replies`); + const arrayOfReplyPids = await db.getSortedSetsMembers(keys); + + const uniquePids = _.uniq(_.flatten(arrayOfReplyPids)); + + let replyData = await posts.getPostsFields(uniquePids, ['pid', 'uid', 'timestamp']); + const result = await plugins.hooks.fire('filter:topics.getPostReplies', { + uid: callerUid, + replies: replyData, + }); + replyData = await user.blocks.filter(callerUid, result.replies); + + const uids = replyData.map(replyData => replyData && replyData.uid); + + const uniqueUids = _.uniq(uids); + + const userData = await user.getUsersWithFields(uniqueUids, ['uid', 'username', 'userslug', 'picture'], callerUid); + + const uidMap = _.zipObject(uniqueUids, userData); + const pidMap = _.zipObject(replyData.map(r => r.pid), replyData); + + const returnData = arrayOfReplyPids.map((replyPids) => { + replyPids = replyPids.filter(pid => pidMap[pid]); + const uidsUsed = {}; + const currentData = { + hasMore: false, + users: [], + text: replyPids.length > 1 ? `[[topic:replies_to_this_post, ${replyPids.length}]]` : '[[topic:one_reply_to_this_post]]', + count: replyPids.length, + timestampISO: replyPids.length ? utils.toISOString(pidMap[replyPids[0]].timestamp) : undefined, + }; + + replyPids.sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); + + replyPids.forEach((replyPid) => { + const replyData = pidMap[replyPid]; + if (!uidsUsed[replyData.uid] && currentData.users.length < 6) { + currentData.users.push(uidMap[replyData.uid]); + uidsUsed[replyData.uid] = true; + } + }); + + if (currentData.users.length > 5) { + currentData.users.pop(); + currentData.hasMore = true; + } + + return currentData; + }); + + return returnData; + } + + Topics.syncBacklinks = async (postData) => { + if (!postData) { + throw new Error('[[error:invalid-data]]'); + } + + // Scan post content for topic links + const matches = [...postData.content.matchAll(backlinkRegex)]; + if (!matches) { + return 0; + } + + const { pid, uid, tid } = postData; + let add = _.uniq(matches.map(match => match[1]).map(tid => parseInt(tid, 10))); + + const now = Date.now(); + const topicsExist = await Topics.exists(add); + const current = (await db.getSortedSetMembers(`pid:${pid}:backlinks`)).map(tid => parseInt(tid, 10)); + const remove = current.filter(tid => !add.includes(tid)); + add = add.filter((_tid, idx) => topicsExist[idx] && !current.includes(_tid) && tid !== _tid); + + // Remove old backlinks + await db.sortedSetRemove(`pid:${pid}:backlinks`, remove); + + // Add new backlinks + await db.sortedSetAdd(`pid:${pid}:backlinks`, add.map(() => now), add); + await Promise.all(add.map(async (tid) => { + await Topics.events.log(tid, { + uid, + type: 'backlink', + href: `/post/${pid}`, + }); + })); + + return add.length + (current - remove); + }; +}; diff --git a/src/topics/recent.js b/src/topics/recent.js new file mode 100644 index 0000000000..c972fa09fc --- /dev/null +++ b/src/topics/recent.js @@ -0,0 +1,79 @@ + +'use strict'; + +const db = require('../database'); +const plugins = require('../plugins'); +const posts = require('../posts'); + +module.exports = function (Topics) { + const terms = { + day: 86400000, + week: 604800000, + month: 2592000000, + year: 31104000000, + }; + + Topics.getRecentTopics = async function (cid, uid, start, stop, filter) { + return await Topics.getSortedTopics({ + cids: cid, + uid: uid, + start: start, + stop: stop, + filter: filter, + sort: 'recent', + }); + }; + + /* not an orphan method, used in widget-essentials */ + Topics.getLatestTopics = async function (options) { + // uid, start, stop, term + const tids = await Topics.getLatestTidsFromSet('topics:recent', options.start, options.stop, options.term); + const topics = await Topics.getTopics(tids, options); + return { topics: topics, nextStart: options.stop + 1 }; + }; + + Topics.getLatestTidsFromSet = async function (set, start, stop, term) { + let since = terms.day; + if (terms[term]) { + since = terms[term]; + } + + const count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; + return await db.getSortedSetRevRangeByScore(set, start, count, '+inf', Date.now() - since); + }; + + Topics.updateLastPostTimeFromLastPid = async function (tid) { + const pid = await Topics.getLatestUndeletedPid(tid); + if (!pid) { + return; + } + const timestamp = await posts.getPostField(pid, 'timestamp'); + if (!timestamp) { + return; + } + await Topics.updateLastPostTime(tid, timestamp); + }; + + Topics.updateLastPostTime = async function (tid, lastposttime) { + await Topics.setTopicField(tid, 'lastposttime', lastposttime); + const topicData = await Topics.getTopicFields(tid, ['cid', 'deleted', 'pinned']); + + await db.sortedSetAdd(`cid:${topicData.cid}:tids:lastposttime`, lastposttime, tid); + + await Topics.updateRecent(tid, lastposttime); + + if (!topicData.pinned) { + await db.sortedSetAdd(`cid:${topicData.cid}:tids`, lastposttime, tid); + } + }; + + Topics.updateRecent = async function (tid, timestamp) { + let data = { tid: tid, timestamp: timestamp }; + if (plugins.hooks.hasListeners('filter:topics.updateRecent')) { + data = await plugins.hooks.fire('filter:topics.updateRecent', { tid: tid, timestamp: timestamp }); + } + if (data && data.tid && data.timestamp) { + await db.sortedSetAdd('topics:recent', data.timestamp, data.tid); + } + }; +}; diff --git a/src/topics/scheduled.js b/src/topics/scheduled.js new file mode 100644 index 0000000000..f79bf32a5d --- /dev/null +++ b/src/topics/scheduled.js @@ -0,0 +1,129 @@ +'use strict'; + +const _ = require('lodash'); +const winston = require('winston'); +const { CronJob } = require('cron'); + +const db = require('../database'); +const posts = require('../posts'); +const socketHelpers = require('../socket.io/helpers'); +const topics = require('./index'); +const user = require('../user'); + +const Scheduled = module.exports; + +Scheduled.startJobs = function () { + winston.verbose('[scheduled topics] Starting jobs.'); + new CronJob('*/1 * * * *', Scheduled.handleExpired, null, true); +}; + +Scheduled.handleExpired = async function () { + const now = Date.now(); + const tids = await db.getSortedSetRangeByScore('topics:scheduled', 0, -1, '-inf', now); + + if (!tids.length) { + return; + } + + let topicsData = await topics.getTopicsData(tids); + // Filter deleted + topicsData = topicsData.filter(topicData => Boolean(topicData)); + const uids = _.uniq(topicsData.map(topicData => topicData.uid)).filter(uid => uid); // Filter guests topics + + // Restore first to be not filtered for being deleted + // Restoring handles "updateRecentTid" + await Promise.all([].concat( + topicsData.map(topicData => topics.restore(topicData.tid)), + topicsData.map(topicData => topics.updateLastPostTimeFromLastPid(topicData.tid)) + )); + + await Promise.all([].concat( + sendNotifications(uids, topicsData), + updateUserLastposttimes(uids, topicsData), + ...topicsData.map(topicData => unpin(topicData.tid, topicData)), + db.sortedSetsRemoveRangeByScore([`topics:scheduled`], '-inf', now) + )); +}; + +// topics/tools.js#pin/unpin would block non-admins/mods, thus the local versions +Scheduled.pin = async function (tid, topicData) { + return Promise.all([ + topics.setTopicField(tid, 'pinned', 1), + db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid), + db.sortedSetsRemove([ + `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:posts`, + `cid:${topicData.cid}:tids:votes`, + `cid:${topicData.cid}:tids:views`, + ], tid), + ]); +}; + +Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) { + await Promise.all([ + db.sortedSetsAdd([ + 'topics:scheduled', + `uid:${uid}:topics`, + 'topics:tid', + `cid:${cid}:uid:${uid}:tids`, + ], timestamp, tid), + shiftPostTimes(tid, timestamp), + ]); + return topics.updateLastPostTimeFromLastPid(tid); +}; + +function unpin(tid, topicData) { + return [ + topics.setTopicField(tid, 'pinned', 0), + topics.deleteTopicField(tid, 'pinExpiry'), + db.sortedSetRemove(`cid:${topicData.cid}:tids:pinned`, tid), + db.sortedSetAddBulk([ + [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], + [`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid], + [`cid:${topicData.cid}:tids:votes`, parseInt(topicData.votes, 10) || 0, tid], + [`cid:${topicData.cid}:tids:views`, topicData.viewcount, tid], + ]), + ]; +} + +async function sendNotifications(uids, topicsData) { + const usernames = await Promise.all(uids.map(uid => user.getUserField(uid, 'username'))); + const uidToUsername = Object.fromEntries(uids.map((uid, idx) => [uid, usernames[idx]])); + + const postsData = await posts.getPostsData(topicsData.map(({ mainPid }) => mainPid)); + postsData.forEach((postData, idx) => { + postData.user = {}; + postData.user.username = uidToUsername[postData.uid]; + postData.topic = topicsData[idx]; + }); + + return Promise.all(topicsData.map( + (t, idx) => user.notifications.sendTopicNotificationToFollowers(t.uid, t, postsData[idx]) + ).concat( + topicsData.map( + (t, idx) => socketHelpers.notifyNew(t.uid, 'newTopic', { posts: [postsData[idx]], topic: t }) + ) + )); +} + +async function updateUserLastposttimes(uids, topicsData) { + const lastposttimes = (await user.getUsersFields(uids, ['lastposttime'])).map(u => u.lastposttime); + + let tstampByUid = {}; + topicsData.forEach((tD) => { + tstampByUid[tD.uid] = tstampByUid[tD.uid] ? tstampByUid[tD.uid].concat(tD.lastposttime) : [tD.lastposttime]; + }); + tstampByUid = Object.fromEntries( + Object.entries(tstampByUid).map(uidTimestamp => [uidTimestamp[0], Math.max(...uidTimestamp[1])]) + ); + + const uidsToUpdate = uids.filter((uid, idx) => tstampByUid[uid] > lastposttimes[idx]); + return Promise.all(uidsToUpdate.map(uid => user.setUserField(uid, 'lastposttime', tstampByUid[uid]))); +} + +async function shiftPostTimes(tid, timestamp) { + const pids = (await posts.getPidsFromSet(`tid:${tid}:posts`, 0, -1, false)); + // Leaving other related score values intact, since they reflect post order correctly, + // and it seems that's good enough + return db.setObjectBulk(pids.map((pid, idx) => [`post:${pid}`, { timestamp: timestamp + idx + 1 }])); +} diff --git a/src/topics/sorted.js b/src/topics/sorted.js new file mode 100644 index 0000000000..ff94bcbedc --- /dev/null +++ b/src/topics/sorted.js @@ -0,0 +1,220 @@ + +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const privileges = require('../privileges'); +const user = require('../user'); +const categories = require('../categories'); +const meta = require('../meta'); +const plugins = require('../plugins'); + +module.exports = function (Topics) { + Topics.getSortedTopics = async function (params) { + const data = { + nextStart: 0, + topicCount: 0, + topics: [], + }; + + params.term = params.term || 'alltime'; + params.sort = params.sort || 'recent'; + params.query = params.query || {}; + if (params.hasOwnProperty('cids') && params.cids && !Array.isArray(params.cids)) { + params.cids = [params.cids]; + } + params.tags = params.tags || []; + if (params.tags && !Array.isArray(params.tags)) { + params.tags = [params.tags]; + } + data.tids = await getTids(params); + data.tids = await sortTids(data.tids, params); + data.tids = await filterTids(data.tids.slice(0, meta.config.recentMaxTopics), params); + data.topicCount = data.tids.length; + data.topics = await getTopics(data.tids, params); + data.nextStart = params.stop + 1; + return data; + }; + + async function getTids(params) { + if (plugins.hooks.hasListeners('filter:topics.getSortedTids')) { + const result = await plugins.hooks.fire('filter:topics.getSortedTids', { params: params, tids: [] }); + return result.tids; + } + let tids = []; + if (params.term !== 'alltime') { + tids = await Topics.getLatestTidsFromSet('topics:tid', 0, -1, params.term); + if (params.filter === 'watched') { + tids = await Topics.filterWatchedTids(tids, params.uid); + } + } else if (params.filter === 'watched') { + tids = await db.getSortedSetRevRange(`uid:${params.uid}:followed_tids`, 0, -1); + } else if (params.cids) { + tids = await getCidTids(params); + } else if (params.tags.length) { + tids = await getTagTids(params); + } else if (params.sort === 'old') { + tids = await db.getSortedSetRange(`topics:recent`, 0, meta.config.recentMaxTopics - 1); + } else { + tids = await db.getSortedSetRevRange(`topics:${params.sort}`, 0, meta.config.recentMaxTopics - 1); + } + + return tids; + } + + async function getTagTids(params) { + const sets = [ + params.sort === 'old' ? + 'topics:recent' : + `topics:${params.sort}`, + ...params.tags.map(tag => `tag:${tag}:topics`), + ]; + const method = params.sort === 'old' ? + 'getSortedSetIntersect' : + 'getSortedSetRevIntersect'; + return await db[method]({ + sets: sets, + start: 0, + stop: meta.config.recentMaxTopics - 1, + weights: sets.map((s, index) => (index ? 0 : 1)), + }); + } + + async function getCidTids(params) { + if (params.tags.length) { + return _.intersection(...await Promise.all(params.tags.map(async (tag) => { + const sets = params.cids.map(cid => `cid:${cid}:tag:${tag}:topics`); + return await db.getSortedSetRevRange(sets, 0, -1); + }))); + } + + const sets = []; + const pinnedSets = []; + params.cids.forEach((cid) => { + if (params.sort === 'recent' || params.sort === 'old') { + sets.push(`cid:${cid}:tids`); + } else { + sets.push(`cid:${cid}:tids${params.sort ? `:${params.sort}` : ''}`); + } + pinnedSets.push(`cid:${cid}:tids:pinned`); + }); + let pinnedTids = await db.getSortedSetRevRange(pinnedSets, 0, -1); + pinnedTids = await Topics.tools.checkPinExpiry(pinnedTids); + const method = params.sort === 'old' ? + 'getSortedSetRange' : + 'getSortedSetRevRange'; + const tids = await db[method](sets, 0, meta.config.recentMaxTopics - 1); + return pinnedTids.concat(tids); + } + + async function sortTids(tids, params) { + if (params.term === 'alltime' && !params.cids && !params.tags.length && params.filter !== 'watched' && !params.floatPinned) { + return tids; + } + const topicData = await Topics.getTopicsFields(tids, ['tid', 'lastposttime', 'upvotes', 'downvotes', 'postcount', 'pinned']); + const sortMap = { + recent: sortRecent, + old: sortOld, + posts: sortPopular, + votes: sortVotes, + views: sortViews, + }; + const sortFn = sortMap[params.sort] || sortRecent; + + if (params.floatPinned) { + floatPinned(topicData, sortFn); + } else { + topicData.sort(sortFn); + } + + return topicData.map(topic => topic && topic.tid); + } + + function floatPinned(topicData, sortFn) { + topicData.sort((a, b) => (a.pinned !== b.pinned ? b.pinned - a.pinned : sortFn(a, b))); + } + + function sortRecent(a, b) { + return b.lastposttime - a.lastposttime; + } + + function sortOld(a, b) { + return a.lastposttime - b.lastposttime; + } + + function sortVotes(a, b) { + if (a.votes !== b.votes) { + return b.votes - a.votes; + } + return b.postcount - a.postcount; + } + + function sortPopular(a, b) { + if (a.postcount !== b.postcount) { + return b.postcount - a.postcount; + } + return b.viewcount - a.viewcount; + } + + function sortViews(a, b) { + return b.viewcount - a.viewcount; + } + + async function filterTids(tids, params) { + const { filter } = params; + const { uid } = params; + + if (filter === 'new') { + tids = await Topics.filterNewTids(tids, uid); + } else if (filter === 'unreplied') { + tids = await Topics.filterUnrepliedTids(tids); + } else { + tids = await Topics.filterNotIgnoredTids(tids, uid); + } + + tids = await privileges.topics.filterTids('topics:read', tids, uid); + let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid']); + const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); + + async function getIgnoredCids() { + if (params.cids || filter === 'watched' || meta.config.disableRecentCategoryFilter) { + return []; + } + return await categories.isIgnored(topicCids, uid); + } + const [ignoredCids, filtered] = await Promise.all([ + getIgnoredCids(), + user.blocks.filter(uid, topicData), + ]); + + const isCidIgnored = _.zipObject(topicCids, ignoredCids); + topicData = filtered; + + const cids = params.cids && params.cids.map(String); + tids = topicData.filter(t => ( + t && + t.cid && + !isCidIgnored[t.cid] && + (!cids || cids.includes(String(t.cid))) + )).map(t => t.tid); + + const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { tids: tids, params: params }); + return result.tids; + } + + async function getTopics(tids, params) { + tids = tids.slice(params.start, params.stop !== -1 ? params.stop + 1 : undefined); + const topicData = await Topics.getTopicsByTids(tids, params); + Topics.calculateTopicIndices(topicData, params.start); + return topicData; + } + + Topics.calculateTopicIndices = function (topicData, start) { + topicData.forEach((topic, index) => { + if (topic) { + topic.index = start + index; + } + }); + }; +}; diff --git a/src/topics/suggested.js b/src/topics/suggested.js new file mode 100644 index 0000000000..de19931892 --- /dev/null +++ b/src/topics/suggested.js @@ -0,0 +1,70 @@ + +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const user = require('../user'); +const privileges = require('../privileges'); +const search = require('../search'); + +module.exports = function (Topics) { + Topics.getSuggestedTopics = async function (tid, uid, start, stop, cutoff = 0) { + let tids; + tid = parseInt(tid, 10); + cutoff = cutoff === 0 ? cutoff : (cutoff * 2592000000); + const [tagTids, searchTids] = await Promise.all([ + getTidsWithSameTags(tid, cutoff), + getSearchTids(tid, uid, cutoff), + ]); + + tids = _.uniq(tagTids.concat(searchTids)); + + let categoryTids = []; + if (stop !== -1 && tids.length < stop - start + 1) { + categoryTids = await getCategoryTids(tid, cutoff); + } + tids = _.shuffle(_.uniq(tids.concat(categoryTids))); + tids = await privileges.topics.filterTids('topics:read', tids, uid); + + let topicData = await Topics.getTopicsByTids(tids, uid); + topicData = topicData.filter(topic => topic && topic.tid !== tid); + topicData = await user.blocks.filter(uid, topicData); + topicData = topicData.slice(start, stop !== -1 ? stop + 1 : undefined) + .sort((t1, t2) => t2.timestamp - t1.timestamp); + return topicData; + }; + + async function getTidsWithSameTags(tid, cutoff) { + const tags = await Topics.getTopicTags(tid); + let tids = cutoff === 0 ? + await db.getSortedSetRevRange(tags.map(tag => `tag:${tag}:topics`), 0, -1) : + await db.getSortedSetRevRangeByScore(tags.map(tag => `tag:${tag}:topics`), 0, -1, '+inf', Date.now() - cutoff); + tids = tids.filter(_tid => _tid !== tid); // remove self + return _.shuffle(_.uniq(tids)).slice(0, 10).map(Number); + } + + async function getSearchTids(tid, uid, cutoff) { + const topicData = await Topics.getTopicFields(tid, ['title', 'cid']); + const data = await search.search({ + query: topicData.title, + searchIn: 'titles', + matchWords: 'any', + categories: [topicData.cid], + uid: uid, + returnIds: true, + timeRange: cutoff !== 0 ? cutoff / 1000 : 0, + timeFilter: 'newer', + }); + data.tids = data.tids.filter(_tid => _tid !== tid); // remove self + return _.shuffle(data.tids).slice(0, 10).map(Number); + } + + async function getCategoryTids(tid, cutoff) { + const cid = await Topics.getTopicField(tid, 'cid'); + const tids = cutoff === 0 ? + await db.getSortedSetRevRange(`cid:${cid}:tids:lastposttime`, 0, 9) : + await db.getSortedSetRevRangeByScore(`cid:${cid}:tids:lastposttime`, 0, 9, '+inf', Date.now() - cutoff); + return _.shuffle(tids.map(Number).filter(_tid => _tid !== tid)); + } +}; diff --git a/src/topics/tags.js b/src/topics/tags.js new file mode 100644 index 0000000000..d07808264c --- /dev/null +++ b/src/topics/tags.js @@ -0,0 +1,528 @@ + +'use strict'; + +const async = require('async'); +const validator = require('validator'); +const _ = require('lodash'); + +const db = require('../database'); +const meta = require('../meta'); +const user = require('../user'); +const categories = require('../categories'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const batch = require('../batch'); +const cache = require('../cache'); + +module.exports = function (Topics) { + Topics.createTags = async function (tags, tid, timestamp) { + if (!Array.isArray(tags) || !tags.length) { + return; + } + + const cid = await Topics.getTopicField(tid, 'cid'); + const topicSets = tags.map(tag => `tag:${tag}:topics`).concat( + tags.map(tag => `cid:${cid}:tag:${tag}:topics`) + ); + await db.sortedSetsAdd(topicSets, timestamp, tid); + await Topics.updateCategoryTagsCount([cid], tags); + await Promise.all(tags.map(updateTagCount)); + }; + + Topics.filterTags = async function (tags, cid) { + const result = await plugins.hooks.fire('filter:tags.filter', { tags: tags, cid: cid }); + tags = _.uniq(result.tags) + .map(tag => utils.cleanUpTag(tag, meta.config.maximumTagLength)) + .filter(tag => tag && tag.length >= (meta.config.minimumTagLength || 3)); + + return await filterCategoryTags(tags, cid); + }; + + Topics.updateCategoryTagsCount = async function (cids, tags) { + await Promise.all(cids.map(async (cid) => { + const counts = await db.sortedSetsCard( + tags.map(tag => `cid:${cid}:tag:${tag}:topics`) + ); + const tagToCount = _.zipObject(tags, counts); + const set = `cid:${cid}:tags`; + + const bulkAdd = tags.filter(tag => tagToCount[tag] > 0) + .map(tag => [set, tagToCount[tag], tag]); + + const bulkRemove = tags.filter(tag => tagToCount[tag] <= 0) + .map(tag => [set, tag]); + + await Promise.all([ + db.sortedSetAddBulk(bulkAdd), + db.sortedSetRemoveBulk(bulkRemove), + ]); + })); + + await db.sortedSetsRemoveRangeByScore( + cids.map(cid => `cid:${cid}:tags`), '-inf', 0 + ); + }; + + Topics.validateTags = async function (tags, cid, uid, tid = null) { + if (!Array.isArray(tags)) { + throw new Error('[[error:invalid-data]]'); + } + tags = _.uniq(tags); + const [categoryData, isPrivileged, currentTags] = await Promise.all([ + categories.getCategoryFields(cid, ['minTags', 'maxTags']), + user.isPrivileged(uid), + tid ? Topics.getTopicTags(tid) : [], + ]); + if (tags.length < parseInt(categoryData.minTags, 10)) { + throw new Error(`[[error:not-enough-tags, ${categoryData.minTags}]]`); + } else if (tags.length > parseInt(categoryData.maxTags, 10)) { + throw new Error(`[[error:too-many-tags, ${categoryData.maxTags}]]`); + } + + const addedTags = tags.filter(tag => !currentTags.includes(tag)); + const removedTags = currentTags.filter(tag => !tags.includes(tag)); + const systemTags = (meta.config.systemTags || '').split(','); + + if (!isPrivileged && systemTags.length && + addedTags.length && addedTags.some(tag => systemTags.includes(tag))) { + throw new Error('[[error:cant-use-system-tag]]'); + } + + if (!isPrivileged && systemTags.length && + removedTags.length && removedTags.some(tag => systemTags.includes(tag))) { + throw new Error('[[error:cant-remove-system-tag]]'); + } + }; + + async function filterCategoryTags(tags, cid) { + const tagWhitelist = await categories.getTagWhitelist([cid]); + if (!Array.isArray(tagWhitelist[0]) || !tagWhitelist[0].length) { + return tags; + } + const whitelistSet = new Set(tagWhitelist[0]); + return tags.filter(tag => whitelistSet.has(tag)); + } + + Topics.createEmptyTag = async function (tag) { + if (!tag) { + throw new Error('[[error:invalid-tag]]'); + } + if (tag.length < (meta.config.minimumTagLength || 3)) { + throw new Error('[[error:tag-too-short]]'); + } + const isMember = await db.isSortedSetMember('tags:topic:count', tag); + if (!isMember) { + await db.sortedSetAdd('tags:topic:count', 0, tag); + cache.del('tags:topic:count'); + } + const allCids = await categories.getAllCidsFromSet('categories:cid'); + const isMembers = await db.isMemberOfSortedSets( + allCids.map(cid => `cid:${cid}:tags`), tag + ); + const bulkAdd = allCids.filter((cid, index) => !isMembers[index]) + .map(cid => ([`cid:${cid}:tags`, 0, tag])); + await db.sortedSetAddBulk(bulkAdd); + }; + + Topics.renameTags = async function (data) { + for (const tagData of data) { + // eslint-disable-next-line no-await-in-loop + await renameTag(tagData.value, tagData.newName); + } + }; + + async function renameTag(tag, newTagName) { + if (!newTagName || tag === newTagName) { + return; + } + newTagName = utils.cleanUpTag(newTagName, meta.config.maximumTagLength); + + await Topics.createEmptyTag(newTagName); + const allCids = {}; + + await batch.processSortedSet(`tag:${tag}:topics`, async (tids) => { + const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'tags']); + const cids = topicData.map(t => t.cid); + topicData.forEach((t) => { allCids[t.cid] = true; }); + const scores = await db.sortedSetScores(`tag:${tag}:topics`, tids); + // update tag::topics + await db.sortedSetAdd(`tag:${newTagName}:topics`, scores, tids); + await db.sortedSetRemove(`tag:${tag}:topics`, tids); + + // update cid::tag::topics + await db.sortedSetAddBulk(topicData.map( + (t, index) => [`cid:${t.cid}:tag:${newTagName}:topics`, scores[index], t.tid] + )); + await db.sortedSetRemove(cids.map(cid => `cid:${cid}:tag:${tag}:topics`), tids); + + // update 'tags' field in topic hash + topicData.forEach((topic) => { + topic.tags = topic.tags.map(tagItem => tagItem.value); + const index = topic.tags.indexOf(tag); + if (index !== -1) { + topic.tags.splice(index, 1, newTagName); + } + }); + await db.setObjectBulk( + topicData.map(t => [`topic:${t.tid}`, { tags: t.tags.join(',') }]), + ); + }, {}); + await Topics.deleteTag(tag); + await updateTagCount(newTagName); + await Topics.updateCategoryTagsCount(Object.keys(allCids), [newTagName]); + } + + async function updateTagCount(tag) { + const count = await Topics.getTagTopicCount(tag); + await db.sortedSetAdd('tags:topic:count', count || 0, tag); + cache.del('tags:topic:count'); + } + + Topics.getTagTids = async function (tag, start, stop) { + const tids = await db.getSortedSetRevRange(`tag:${tag}:topics`, start, stop); + const payload = await plugins.hooks.fire('filter:topics.getTagTids', { tag, start, stop, tids }); + return payload.tids; + }; + + Topics.getTagTidsByCids = async function (tag, cids, start, stop) { + const keys = cids.map(cid => `cid:${cid}:tag:${tag}:topics`); + const tids = await db.getSortedSetRevRange(keys, start, stop); + const payload = await plugins.hooks.fire('filter:topics.getTagTidsByCids', { tag, cids, start, stop, tids }); + return payload.tids; + }; + + Topics.getTagTopicCount = async function (tag, cids = []) { + let count = 0; + if (cids.length) { + count = await db.sortedSetsCardSum( + cids.map(cid => `cid:${cid}:tag:${tag}:topics`) + ); + } else { + count = await db.sortedSetCard(`tag:${tag}:topics`); + } + + const payload = await plugins.hooks.fire('filter:topics.getTagTopicCount', { tag, count, cids }); + return payload.count; + }; + + Topics.deleteTags = async function (tags) { + if (!Array.isArray(tags) || !tags.length) { + return; + } + await removeTagsFromTopics(tags); + const keys = tags.map(tag => `tag:${tag}:topics`); + await db.deleteAll(keys); + await db.sortedSetRemove('tags:topic:count', tags); + cache.del('tags:topic:count'); + const cids = await categories.getAllCidsFromSet('categories:cid'); + + await db.sortedSetRemove(cids.map(cid => `cid:${cid}:tags`), tags); + + const deleteKeys = []; + tags.forEach((tag) => { + deleteKeys.push(`tag:${tag}`); + cids.forEach((cid) => { + deleteKeys.push(`cid:${cid}:tag:${tag}:topics`); + }); + }); + await db.deleteAll(deleteKeys); + }; + + async function removeTagsFromTopics(tags) { + await async.eachLimit(tags, 50, async (tag) => { + const tids = await db.getSortedSetRange(`tag:${tag}:topics`, 0, -1); + if (!tids.length) { + return; + } + + await db.deleteObjectFields( + tids.map(tid => `topic:${tid}`), + ['tags'], + ); + }); + } + + Topics.deleteTag = async function (tag) { + await Topics.deleteTags([tag]); + }; + + Topics.getTags = async function (start, stop) { + return await getFromSet('tags:topic:count', start, stop); + }; + + Topics.getCategoryTags = async function (cids, start, stop) { + if (Array.isArray(cids)) { + return await db.getSortedSetRevUnion({ + sets: cids.map(cid => `cid:${cid}:tags`), + start, + stop, + }); + } + return await db.getSortedSetRevRange(`cid:${cids}:tags`, start, stop); + }; + + Topics.getCategoryTagsData = async function (cids, start, stop) { + return await getFromSet( + Array.isArray(cids) ? cids.map(cid => `cid:${cid}:tags`) : `cid:${cids}:tags`, + start, + stop + ); + }; + + async function getFromSet(set, start, stop) { + let tags; + if (Array.isArray(set)) { + tags = await db.getSortedSetRevUnion({ + sets: set, + start, + stop, + withScores: true, + }); + } else { + tags = await db.getSortedSetRevRangeWithScores(set, start, stop); + } + + const payload = await plugins.hooks.fire('filter:tags.getAll', { + tags: tags, + }); + return await Topics.getTagData(payload.tags); + } + + Topics.getTagData = async function (tags) { + if (!tags.length) { + return []; + } + tags.forEach((tag) => { + tag.valueEscaped = validator.escape(String(tag.value)); + tag.valueEncoded = encodeURIComponent(tag.valueEscaped); + tag.class = tag.valueEscaped.replace(/\s/g, '-'); + }); + return tags; + }; + + Topics.getTopicTags = async function (tid) { + const data = await Topics.getTopicsTags([tid]); + return data && data[0]; + }; + + Topics.getTopicsTags = async function (tids) { + const topicTagData = await Topics.getTopicsFields(tids, ['tags']); + return tids.map((tid, i) => topicTagData[i].tags.map(tagData => tagData.value)); + }; + + Topics.getTopicTagsObjects = async function (tid) { + const data = await Topics.getTopicsTagsObjects([tid]); + return Array.isArray(data) && data.length ? data[0] : []; + }; + + Topics.getTopicsTagsObjects = async function (tids) { + const topicTags = await Topics.getTopicsTags(tids); + const uniqueTopicTags = _.uniq(_.flatten(topicTags)); + + const tags = uniqueTopicTags.map(tag => ({ value: tag })); + const tagData = await Topics.getTagData(tags); + const tagDataMap = _.zipObject(uniqueTopicTags, tagData); + + topicTags.forEach((tags, index) => { + if (Array.isArray(tags)) { + topicTags[index] = tags.map(tag => tagDataMap[tag]); + } + }); + + return topicTags; + }; + + Topics.addTags = async function (tags, tids) { + const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp', 'tags']); + const bulkAdd = []; + const bulkSet = []; + topicData.forEach((t) => { + const topicTags = t.tags.map(tagItem => tagItem.value); + tags.forEach((tag) => { + bulkAdd.push([`tag:${tag}:topics`, t.timestamp, t.tid]); + bulkAdd.push([`cid:${t.cid}:tag:${tag}:topics`, t.timestamp, t.tid]); + if (!topicTags.includes(tag)) { + topicTags.push(tag); + } + }); + bulkSet.push([`topic:${t.tid}`, { tags: topicTags.join(',') }]); + }); + await Promise.all([ + db.sortedSetAddBulk(bulkAdd), + db.setObjectBulk(bulkSet), + ]); + + await Promise.all(tags.map(updateTagCount)); + await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags); + }; + + Topics.removeTags = async function (tags, tids) { + const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid', 'tags']); + const bulkRemove = []; + const bulkSet = []; + + topicData.forEach((t) => { + const topicTags = t.tags.map(tagItem => tagItem.value); + tags.forEach((tag) => { + bulkRemove.push([`tag:${tag}:topics`, t.tid]); + bulkRemove.push([`cid:${t.cid}:tag:${tag}:topics`, t.tid]); + if (topicTags.includes(tag)) { + topicTags.splice(topicTags.indexOf(tag), 1); + } + }); + bulkSet.push([`topic:${t.tid}`, { tags: topicTags.join(',') }]); + }); + await Promise.all([ + db.sortedSetRemoveBulk(bulkRemove), + db.setObjectBulk(bulkSet), + ]); + + await Promise.all(tags.map(updateTagCount)); + await Topics.updateCategoryTagsCount(_.uniq(topicData.map(t => t.cid)), tags); + }; + + Topics.updateTopicTags = async function (tid, tags) { + await Topics.deleteTopicTags(tid); + const cid = await Topics.getTopicField(tid, 'cid'); + + tags = await Topics.filterTags(tags, cid); + await Topics.addTags(tags, [tid]); + }; + + Topics.deleteTopicTags = async function (tid) { + const topicData = await Topics.getTopicFields(tid, ['cid', 'tags']); + const { cid } = topicData; + const tags = topicData.tags.map(tagItem => tagItem.value); + await db.deleteObjectField(`topic:${tid}`, 'tags'); + + const sets = tags.map(tag => `tag:${tag}:topics`) + .concat(tags.map(tag => `cid:${cid}:tag:${tag}:topics`)); + await db.sortedSetsRemove(sets, tid); + + await Topics.updateCategoryTagsCount([cid], tags); + await Promise.all(tags.map(updateTagCount)); + }; + + Topics.searchTags = async function (data) { + if (!data || !data.query) { + return []; + } + let result; + if (plugins.hooks.hasListeners('filter:topics.searchTags')) { + result = await plugins.hooks.fire('filter:topics.searchTags', { data: data }); + } else { + result = await findMatches(data); + } + result = await plugins.hooks.fire('filter:tags.search', { data: data, matches: result.matches }); + return result.matches; + }; + + Topics.autocompleteTags = async function (data) { + if (!data || !data.query) { + return []; + } + let result; + if (plugins.hooks.hasListeners('filter:topics.autocompleteTags')) { + result = await plugins.hooks.fire('filter:topics.autocompleteTags', { data: data }); + } else { + result = await findMatches(data); + } + return result.matches; + }; + + async function getAllTags() { + const cached = cache.get('tags:topic:count'); + if (cached !== undefined) { + return cached; + } + const tags = await db.getSortedSetRevRangeWithScores('tags:topic:count', 0, -1); + cache.set('tags:topic:count', tags); + return tags; + } + + async function findMatches(data) { + let { query } = data; + let tagWhitelist = []; + if (parseInt(data.cid, 10)) { + tagWhitelist = await categories.getTagWhitelist([data.cid]); + } + let tags = []; + if (Array.isArray(tagWhitelist[0]) && tagWhitelist[0].length) { + const scores = await db.sortedSetScores(`cid:${data.cid}:tags`, tagWhitelist[0]); + tags = tagWhitelist[0].map((tag, index) => ({ value: tag, score: scores[index] })); + } else if (data.cids) { + tags = await db.getSortedSetRevUnion({ + sets: data.cids.map(cid => `cid:${cid}:tags`), + start: 0, + stop: -1, + withScores: true, + }); + } else { + tags = await getAllTags(); + } + + query = query.toLowerCase(); + + const matches = []; + for (let i = 0; i < tags.length; i += 1) { + if (tags[i].value && tags[i].value.toLowerCase().startsWith(query)) { + matches.push(tags[i]); + if (matches.length > 39) { + break; + } + } + } + + matches.sort((a, b) => { + if (a.value < b.value) { + return -1; + } else if (a.value > b.value) { + return 1; + } + return 0; + }); + return { matches: matches }; + } + + Topics.searchAndLoadTags = async function (data) { + const searchResult = { + tags: [], + matchCount: 0, + pageCount: 1, + }; + + if (!data || !data.query || !data.query.length) { + return searchResult; + } + const tags = await Topics.searchTags(data); + + const tagData = await Topics.getTagData(tags.map(tag => ({ value: tag.value }))); + + tagData.forEach((tag, index) => { + tag.score = tags[index].score; + }); + tagData.sort((a, b) => b.score - a.score); + searchResult.tags = tagData; + searchResult.matchCount = tagData.length; + searchResult.pageCount = 1; + return searchResult; + }; + + Topics.getRelatedTopics = async function (topicData, uid) { + if (plugins.hooks.hasListeners('filter:topic.getRelatedTopics')) { + const result = await plugins.hooks.fire('filter:topic.getRelatedTopics', { topic: topicData, uid: uid, topics: [] }); + return result.topics; + } + + let maximumTopics = meta.config.maximumRelatedTopics; + if (maximumTopics === 0 || !topicData.tags || !topicData.tags.length) { + return []; + } + + maximumTopics = maximumTopics || 5; + let tids = await Promise.all(topicData.tags.map(tag => Topics.getTagTids(tag.value, 0, 5))); + tids = _.shuffle(_.uniq(_.flatten(tids))).slice(0, maximumTopics); + const topics = await Topics.getTopics(tids, uid); + return topics.filter(t => t && !t.deleted && parseInt(t.uid, 10) !== parseInt(uid, 10)); + }; +}; diff --git a/src/topics/teaser.js b/src/topics/teaser.js new file mode 100644 index 0000000000..52edbb33fc --- /dev/null +++ b/src/topics/teaser.js @@ -0,0 +1,176 @@ + +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const meta = require('../meta'); +const user = require('../user'); +const posts = require('../posts'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +module.exports = function (Topics) { + Topics.getTeasers = async function (topics, options) { + if (!Array.isArray(topics) || !topics.length) { + return []; + } + let uid = options; + let { teaserPost } = meta.config; + if (typeof options === 'object') { + uid = options.uid; + teaserPost = options.teaserPost || meta.config.teaserPost; + } + + const counts = []; + const teaserPids = []; + const tidToPost = {}; + + topics.forEach((topic) => { + counts.push(topic && topic.postcount); + if (topic) { + if (topic.teaserPid === 'null') { + delete topic.teaserPid; + } + if (teaserPost === 'first') { + teaserPids.push(topic.mainPid); + } else if (teaserPost === 'last-post') { + teaserPids.push(topic.teaserPid || topic.mainPid); + } else { // last-reply and everything else uses teaserPid like `last` that was used before + teaserPids.push(topic.teaserPid); + } + } + }); + + const [allPostData, callerSettings] = await Promise.all([ + posts.getPostsFields(teaserPids, ['pid', 'uid', 'timestamp', 'tid', 'content']), + user.getSettings(uid), + ]); + let postData = allPostData.filter(post => post && post.pid); + postData = await handleBlocks(uid, postData); + postData = postData.filter(Boolean); + const uids = _.uniq(postData.map(post => post.uid)); + const sortNewToOld = callerSettings.topicPostSort === 'newest_to_oldest'; + const usersData = await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']); + + const users = {}; + usersData.forEach((user) => { + users[user.uid] = user; + }); + postData.forEach((post) => { + // If the post author isn't represented in the retrieved users' data, + // then it means they were deleted, assume guest. + if (!users.hasOwnProperty(post.uid)) { + post.uid = 0; + } + + post.user = users[post.uid]; + post.timestampISO = utils.toISOString(post.timestamp); + tidToPost[post.tid] = post; + }); + await Promise.all(postData.map(p => posts.parsePost(p))); + + const { tags } = await plugins.hooks.fire('filter:teasers.configureStripTags', { tags: utils.stripTags.slice(0) }); + + const teasers = topics.map((topic, index) => { + if (!topic) { + return null; + } + + const topicPost = tidToPost[topic.tid]; + if (topicPost) { + topicPost.index = calcTeaserIndex(teaserPost, counts[index], sortNewToOld); + if (topicPost.content) { + topicPost.content = utils.stripHTMLTags(replaceImgWithAltText(topicPost.content), tags); + } + } + return topicPost; + }); + + const result = await plugins.hooks.fire('filter:teasers.get', { teasers: teasers, uid: uid }); + return result.teasers; + }; + + function calcTeaserIndex(teaserPost, postCountInTopic, sortNewToOld) { + if (teaserPost === 'first') { + return 1; + } + + if (sortNewToOld) { + return Math.min(2, postCountInTopic); + } + return postCountInTopic; + } + + function replaceImgWithAltText(str) { + return String(str).replace(/]*>/gi, '$1'); + } + + async function handleBlocks(uid, teasers) { + const blockedUids = await user.blocks.list(uid); + if (!blockedUids.length) { + return teasers; + } + + return await Promise.all(teasers.map(async (postData) => { + if (blockedUids.includes(parseInt(postData.uid, 10))) { + return await getPreviousNonBlockedPost(postData, blockedUids); + } + return postData; + })); + } + + async function getPreviousNonBlockedPost(postData, blockedUids) { + let isBlocked = false; + let prevPost = postData; + const postsPerIteration = 5; + let start = 0; + let stop = start + postsPerIteration - 1; + let checkedAllReplies = false; + + function checkBlocked(post) { + const isPostBlocked = blockedUids.includes(parseInt(post.uid, 10)); + prevPost = !isPostBlocked ? post : prevPost; + return isPostBlocked; + } + + do { + /* eslint-disable no-await-in-loop */ + let pids = await db.getSortedSetRevRange(`tid:${postData.tid}:posts`, start, stop); + if (!pids.length) { + checkedAllReplies = true; + const mainPid = await Topics.getTopicField(postData.tid, 'mainPid'); + pids = [mainPid]; + } + const prevPosts = await posts.getPostsFields(pids, ['pid', 'uid', 'timestamp', 'tid', 'content']); + isBlocked = prevPosts.every(checkBlocked); + start += postsPerIteration; + stop = start + postsPerIteration - 1; + } while (isBlocked && prevPost && prevPost.pid && !checkedAllReplies); + + return prevPost; + } + + Topics.getTeasersByTids = async function (tids, uid) { + if (!Array.isArray(tids) || !tids.length) { + return []; + } + const topics = await Topics.getTopicsFields(tids, ['tid', 'postcount', 'teaserPid', 'mainPid']); + return await Topics.getTeasers(topics, uid); + }; + + Topics.getTeaser = async function (tid, uid) { + const teasers = await Topics.getTeasersByTids([tid], uid); + return Array.isArray(teasers) && teasers.length ? teasers[0] : null; + }; + + Topics.updateTeaser = async function (tid) { + let pid = await Topics.getLatestUndeletedReply(tid); + pid = pid || null; + if (pid) { + await Topics.setTopicField(tid, 'teaserPid', pid); + } else { + await Topics.deleteTopicField(tid, 'teaserPid'); + } + }; +}; diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js new file mode 100644 index 0000000000..6f40553cdc --- /dev/null +++ b/src/topics/thumbs.js @@ -0,0 +1,162 @@ + +'use strict'; + +const _ = require('lodash'); +const nconf = require('nconf'); +const path = require('path'); +const validator = require('validator'); + +const db = require('../database'); +const file = require('../file'); +const plugins = require('../plugins'); +const posts = require('../posts'); +const meta = require('../meta'); +const cache = require('../cache'); + +const Thumbs = module.exports; + +Thumbs.exists = async function (id, path) { + const isDraft = validator.isUUID(String(id)); + const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; + + return db.isSortedSetMember(set, path); +}; + +Thumbs.load = async function (topicData) { + const topicsWithThumbs = topicData.filter(t => t && parseInt(t.numThumbs, 10) > 0); + const tidsWithThumbs = topicsWithThumbs.map(t => t.tid); + const thumbs = await Thumbs.get(tidsWithThumbs); + const tidToThumbs = _.zipObject(tidsWithThumbs, thumbs); + return topicData.map(t => (t && t.tid ? (tidToThumbs[t.tid] || []) : [])); +}; + +Thumbs.get = async function (tids) { + // Allow singular or plural usage + let singular = false; + if (!Array.isArray(tids)) { + tids = [tids]; + singular = true; + } + + if (!meta.config.allowTopicsThumbnail || !tids.length) { + return singular ? [] : tids.map(() => []); + } + + const hasTimestampPrefix = /^\d+-/; + const upload_url = nconf.get('relative_path') + nconf.get('upload_url'); + const sets = tids.map(tid => `${validator.isUUID(String(tid)) ? 'draft' : 'topic'}:${tid}:thumbs`); + const thumbs = await Promise.all(sets.map(getThumbs)); + let response = thumbs.map((thumbSet, idx) => thumbSet.map(thumb => ({ + id: tids[idx], + name: (() => { + const name = path.basename(thumb); + return hasTimestampPrefix.test(name) ? name.slice(14) : name; + })(), + url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, thumb), + }))); + + ({ thumbs: response } = await plugins.hooks.fire('filter:topics.getThumbs', { tids, thumbs: response })); + return singular ? response.pop() : response; +}; + +async function getThumbs(set) { + const cached = cache.get(set); + if (cached !== undefined) { + return cached.slice(); + } + const thumbs = await db.getSortedSetRange(set, 0, -1); + cache.set(set, thumbs); + return thumbs.slice(); +} + +Thumbs.associate = async function ({ id, path, score }) { + // Associates a newly uploaded file as a thumb to the passed-in draft or topic + const isDraft = validator.isUUID(String(id)); + const isLocal = !path.startsWith('http'); + const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; + const numThumbs = await db.sortedSetCard(set); + + // Normalize the path to allow for changes in upload_path (and so upload_url can be appended if needed) + if (isLocal) { + path = path.replace(nconf.get('upload_path'), ''); + } + const topics = require('.'); + await db.sortedSetAdd(set, isFinite(score) ? score : numThumbs, path); + if (!isDraft) { + const numThumbs = await db.sortedSetCard(set); + await topics.setTopicField(id, 'numThumbs', numThumbs); + } + cache.del(set); + + // Associate thumbnails with the main pid (only on local upload) + if (!isDraft && isLocal) { + const mainPid = (await topics.getMainPids([id]))[0]; + await posts.uploads.associate(mainPid, path.slice(1)); + } +}; + +Thumbs.migrate = async function (uuid, id) { + // Converts the draft thumb zset to the topic zset (combines thumbs if applicable) + const set = `draft:${uuid}:thumbs`; + const thumbs = await db.getSortedSetRangeWithScores(set, 0, -1); + await Promise.all(thumbs.map(async thumb => await Thumbs.associate({ + id, + path: thumb.value, + score: thumb.score, + }))); + await db.delete(set); + cache.del(set); +}; + +Thumbs.delete = async function (id, relativePaths) { + const isDraft = validator.isUUID(String(id)); + const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; + + if (typeof relativePaths === 'string') { + relativePaths = [relativePaths]; + } else if (!Array.isArray(relativePaths)) { + throw new Error('[[error:invalid-data]]'); + } + + const absolutePaths = relativePaths.map(relativePath => path.join(nconf.get('upload_path'), relativePath)); + const [associated, existsOnDisk] = await Promise.all([ + db.isSortedSetMembers(set, relativePaths), + Promise.all(absolutePaths.map(async absolutePath => file.exists(absolutePath))), + ]); + + const toRemove = []; + const toDelete = []; + relativePaths.forEach((relativePath, idx) => { + if (associated[idx]) { + toRemove.push(relativePath); + } + + if (existsOnDisk[idx]) { + toDelete.push(absolutePaths[idx]); + } + }); + + await db.sortedSetRemove(set, toRemove); + + if (isDraft && toDelete.length) { // drafts only; post upload dissociation handles disk deletion for topics + await Promise.all(toDelete.map(async absolutePath => file.delete(absolutePath))); + } + + if (toRemove.length && !isDraft) { + const topics = require('.'); + const mainPid = (await topics.getMainPids([id]))[0]; + + await Promise.all([ + db.incrObjectFieldBy(`topic:${id}`, 'numThumbs', -toRemove.length), + Promise.all(toRemove.map(async relativePath => posts.uploads.dissociate(mainPid, relativePath.slice(1)))), + ]); + } +}; + +Thumbs.deleteAll = async (id) => { + const isDraft = validator.isUUID(String(id)); + const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; + + const thumbs = await db.getSortedSetRange(set, 0, -1); + await Thumbs.delete(id, thumbs); +}; diff --git a/src/topics/tools.js b/src/topics/tools.js new file mode 100644 index 0000000000..c2a254b2be --- /dev/null +++ b/src/topics/tools.js @@ -0,0 +1,295 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const topics = require('.'); +const categories = require('../categories'); +const user = require('../user'); +const plugins = require('../plugins'); +const privileges = require('../privileges'); +const utils = require('../utils'); + + +module.exports = function (Topics) { + const topicTools = {}; + Topics.tools = topicTools; + + topicTools.delete = async function (tid, uid) { + return await toggleDelete(tid, uid, true); + }; + + topicTools.restore = async function (tid, uid) { + return await toggleDelete(tid, uid, false); + }; + + async function toggleDelete(tid, uid, isDelete) { + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + // Scheduled topics can only be purged + if (topicData.scheduled) { + throw new Error('[[error:invalid-data]]'); + } + const canDelete = await privileges.topics.canDelete(tid, uid); + + const hook = isDelete ? 'delete' : 'restore'; + const data = await plugins.hooks.fire(`filter:topic.${hook}`, { topicData: topicData, uid: uid, isDelete: isDelete, canDelete: canDelete, canRestore: canDelete }); + + if ((!data.canDelete && data.isDelete) || (!data.canRestore && !data.isDelete)) { + throw new Error('[[error:no-privileges]]'); + } + if (data.topicData.deleted && data.isDelete) { + throw new Error('[[error:topic-already-deleted]]'); + } else if (!data.topicData.deleted && !data.isDelete) { + throw new Error('[[error:topic-already-restored]]'); + } + if (data.isDelete) { + await Topics.delete(data.topicData.tid, data.uid); + } else { + await Topics.restore(data.topicData.tid); + } + const events = await Topics.events.log(tid, { type: isDelete ? 'delete' : 'restore', uid }); + + data.topicData.deleted = data.isDelete ? 1 : 0; + + if (data.isDelete) { + plugins.hooks.fire('action:topic.delete', { topic: data.topicData, uid: data.uid }); + } else { + plugins.hooks.fire('action:topic.restore', { topic: data.topicData, uid: data.uid }); + } + const userData = await user.getUserFields(data.uid, ['username', 'userslug']); + return { + tid: data.topicData.tid, + cid: data.topicData.cid, + isDelete: data.isDelete, + uid: data.uid, + user: userData, + events, + }; + } + + topicTools.purge = async function (tid, uid) { + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + const canPurge = await privileges.topics.canPurge(tid, uid); + if (!canPurge) { + throw new Error('[[error:no-privileges]]'); + } + + await Topics.purgePostsAndTopic(tid, uid); + return { tid: tid, cid: topicData.cid, uid: uid }; + }; + + topicTools.lock = async function (tid, uid) { + return await toggleLock(tid, uid, true); + }; + + topicTools.unlock = async function (tid, uid) { + return await toggleLock(tid, uid, false); + }; + + async function toggleLock(tid, uid, lock) { + const topicData = await Topics.getTopicFields(tid, ['tid', 'uid', 'cid']); + if (!topicData || !topicData.cid) { + throw new Error('[[error:no-topic]]'); + } + const isAdminOrMod = await privileges.categories.isAdminOrMod(topicData.cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + await Topics.setTopicField(tid, 'locked', lock ? 1 : 0); + topicData.events = await Topics.events.log(tid, { type: lock ? 'lock' : 'unlock', uid }); + topicData.isLocked = lock; // deprecate in v2.0 + topicData.locked = lock; + + plugins.hooks.fire('action:topic.lock', { topic: _.clone(topicData), uid: uid }); + return topicData; + } + + topicTools.pin = async function (tid, uid) { + return await togglePin(tid, uid, true); + }; + + topicTools.unpin = async function (tid, uid) { + return await togglePin(tid, uid, false); + }; + + topicTools.setPinExpiry = async (tid, expiry, uid) => { + if (isNaN(parseInt(expiry, 10)) || expiry <= Date.now()) { + throw new Error('[[error:invalid-data]]'); + } + + const topicData = await Topics.getTopicFields(tid, ['tid', 'uid', 'cid']); + const isAdminOrMod = await privileges.categories.isAdminOrMod(topicData.cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + + await Topics.setTopicField(tid, 'pinExpiry', expiry); + plugins.hooks.fire('action:topic.setPinExpiry', { topic: _.clone(topicData), uid: uid }); + }; + + topicTools.checkPinExpiry = async (tids) => { + const expiry = (await topics.getTopicsFields(tids, ['pinExpiry'])).map(obj => obj.pinExpiry); + const now = Date.now(); + + tids = await Promise.all(tids.map(async (tid, idx) => { + if (expiry[idx] && parseInt(expiry[idx], 10) <= now) { + await togglePin(tid, 'system', false); + return null; + } + + return tid; + })); + + return tids.filter(Boolean); + }; + + async function togglePin(tid, uid, pin) { + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + + if (topicData.scheduled) { + throw new Error('[[error:cant-pin-scheduled]]'); + } + + if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) { + throw new Error('[[error:no-privileges]]'); + } + + const promises = [ + Topics.setTopicField(tid, 'pinned', pin ? 1 : 0), + Topics.events.log(tid, { type: pin ? 'pin' : 'unpin', uid }), + ]; + if (pin) { + promises.push(db.sortedSetAdd(`cid:${topicData.cid}:tids:pinned`, Date.now(), tid)); + promises.push(db.sortedSetsRemove([ + `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:posts`, + `cid:${topicData.cid}:tids:votes`, + `cid:${topicData.cid}:tids:views`, + ], tid)); + } else { + promises.push(db.sortedSetRemove(`cid:${topicData.cid}:tids:pinned`, tid)); + promises.push(Topics.deleteTopicField(tid, 'pinExpiry')); + promises.push(db.sortedSetAddBulk([ + [`cid:${topicData.cid}:tids`, topicData.lastposttime, tid], + [`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid], + [`cid:${topicData.cid}:tids:votes`, parseInt(topicData.votes, 10) || 0, tid], + [`cid:${topicData.cid}:tids:views`, topicData.viewcount, tid], + ])); + topicData.pinExpiry = undefined; + topicData.pinExpiryISO = undefined; + } + + const results = await Promise.all(promises); + + topicData.isPinned = pin; // deprecate in v2.0 + topicData.pinned = pin; + topicData.events = results[1]; + + plugins.hooks.fire('action:topic.pin', { topic: _.clone(topicData), uid }); + + return topicData; + } + + topicTools.orderPinnedTopics = async function (uid, data) { + const { tid, order } = data; + const cid = await Topics.getTopicField(tid, 'cid'); + + if (!cid || !tid || !utils.isNumber(order) || order < 0) { + throw new Error('[[error:invalid-data]]'); + } + + const isAdminOrMod = await privileges.categories.isAdminOrMod(cid, uid); + if (!isAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + + const pinnedTids = await db.getSortedSetRange(`cid:${cid}:tids:pinned`, 0, -1); + const currentIndex = pinnedTids.indexOf(String(tid)); + if (currentIndex === -1) { + return; + } + const newOrder = pinnedTids.length - order - 1; + // moves tid to index order in the array + if (pinnedTids.length > 1) { + pinnedTids.splice(Math.max(0, newOrder), 0, pinnedTids.splice(currentIndex, 1)[0]); + } + + await db.sortedSetAdd( + `cid:${cid}:tids:pinned`, + pinnedTids.map((tid, index) => index), + pinnedTids + ); + }; + + topicTools.move = async function (tid, data) { + const cid = parseInt(data.cid, 10); + const topicData = await Topics.getTopicData(tid); + if (!topicData) { + throw new Error('[[error:no-topic]]'); + } + if (cid === topicData.cid) { + throw new Error('[[error:cant-move-topic-to-same-category]]'); + } + const tags = await Topics.getTopicTags(tid); + await db.sortedSetsRemove([ + `cid:${topicData.cid}:tids`, + `cid:${topicData.cid}:tids:pinned`, + `cid:${topicData.cid}:tids:posts`, + `cid:${topicData.cid}:tids:votes`, + `cid:${topicData.cid}:tids:views`, + `cid:${topicData.cid}:tids:lastposttime`, + `cid:${topicData.cid}:recent_tids`, + `cid:${topicData.cid}:uid:${topicData.uid}:tids`, + ...tags.map(tag => `cid:${topicData.cid}:tag:${tag}:topics`), + ], tid); + + topicData.postcount = topicData.postcount || 0; + const votes = topicData.upvotes - topicData.downvotes; + + const bulk = [ + [`cid:${cid}:tids:lastposttime`, topicData.lastposttime, tid], + [`cid:${cid}:uid:${topicData.uid}:tids`, topicData.timestamp, tid], + ...tags.map(tag => [`cid:${cid}:tag:${tag}:topics`, topicData.timestamp, tid]), + ]; + if (topicData.pinned) { + bulk.push([`cid:${cid}:tids:pinned`, Date.now(), tid]); + } else { + bulk.push([`cid:${cid}:tids`, topicData.lastposttime, tid]); + bulk.push([`cid:${cid}:tids:posts`, topicData.postcount, tid]); + bulk.push([`cid:${cid}:tids:votes`, votes, tid]); + bulk.push([`cid:${cid}:tids:views`, topicData.viewcount, tid]); + } + await db.sortedSetAddBulk(bulk); + + const oldCid = topicData.cid; + await categories.moveRecentReplies(tid, oldCid, cid); + + await Promise.all([ + categories.incrementCategoryFieldBy(oldCid, 'topic_count', -1), + categories.incrementCategoryFieldBy(cid, 'topic_count', 1), + categories.updateRecentTidForCid(cid), + categories.updateRecentTidForCid(oldCid), + Topics.setTopicFields(tid, { + cid: cid, + oldCid: oldCid, + }), + Topics.updateCategoryTagsCount([oldCid, cid], tags), + Topics.events.log(tid, { type: 'move', uid: data.uid, fromCid: oldCid }), + ]); + const hookData = _.clone(data); + hookData.fromCid = oldCid; + hookData.toCid = cid; + hookData.tid = tid; + + plugins.hooks.fire('action:topic.move', hookData); + }; +}; diff --git a/src/topics/unread.js b/src/topics/unread.js new file mode 100644 index 0000000000..f66611e490 --- /dev/null +++ b/src/topics/unread.js @@ -0,0 +1,389 @@ + +'use strict'; + +const async = require('async'); +const _ = require('lodash'); + +const db = require('../database'); +const user = require('../user'); +const posts = require('../posts'); +const notifications = require('../notifications'); +const categories = require('../categories'); +const privileges = require('../privileges'); +const meta = require('../meta'); +const utils = require('../utils'); +const plugins = require('../plugins'); + +module.exports = function (Topics) { + Topics.getTotalUnread = async function (uid, filter) { + filter = filter || ''; + const counts = await Topics.getUnreadTids({ cid: 0, uid: uid, count: true }); + return counts && counts[filter]; + }; + + Topics.getUnreadTopics = async function (params) { + const unreadTopics = { + showSelect: true, + nextStart: 0, + topics: [], + }; + let tids = await Topics.getUnreadTids(params); + unreadTopics.topicCount = tids.length; + + if (!tids.length) { + return unreadTopics; + } + + tids = tids.slice(params.start, params.stop !== -1 ? params.stop + 1 : undefined); + + const topicData = await Topics.getTopicsByTids(tids, params.uid); + if (!topicData.length) { + return unreadTopics; + } + Topics.calculateTopicIndices(topicData, params.start); + unreadTopics.topics = topicData; + unreadTopics.nextStart = params.stop + 1; + return unreadTopics; + }; + + Topics.unreadCutoff = async function (uid) { + const cutoff = Date.now() - (meta.config.unreadCutoff * 86400000); + const data = await plugins.hooks.fire('filter:topics.unreadCutoff', { uid: uid, cutoff: cutoff }); + return parseInt(data.cutoff, 10); + }; + + Topics.getUnreadTids = async function (params) { + const results = await Topics.getUnreadData(params); + return params.count ? results.counts : results.tids; + }; + + Topics.getUnreadData = async function (params) { + const uid = parseInt(params.uid, 10); + + params.filter = params.filter || ''; + + if (params.cid && !Array.isArray(params.cid)) { + params.cid = [params.cid]; + } + + const data = await getTids(params); + if (uid <= 0 || !data.tids || !data.tids.length) { + return data; + } + + const result = await plugins.hooks.fire('filter:topics.getUnreadTids', { + uid: uid, + tids: data.tids, + counts: data.counts, + tidsByFilter: data.tidsByFilter, + cid: params.cid, + filter: params.filter, + query: params.query || {}, + }); + return result; + }; + + async function getTids(params) { + const counts = { '': 0, new: 0, watched: 0, unreplied: 0 }; + const tidsByFilter = { '': [], new: [], watched: [], unreplied: [] }; + + if (params.uid <= 0) { + return { counts: counts, tids: [], tidsByFilter: tidsByFilter }; + } + + params.cutoff = await Topics.unreadCutoff(params.uid); + + const [followedTids, ignoredTids, categoryTids, userScores, tids_unread] = await Promise.all([ + getFollowedTids(params), + user.getIgnoredTids(params.uid, 0, -1), + getCategoryTids(params), + db.getSortedSetRevRangeByScoreWithScores(`uid:${params.uid}:tids_read`, 0, -1, '+inf', params.cutoff), + db.getSortedSetRevRangeWithScores(`uid:${params.uid}:tids_unread`, 0, -1), + ]); + + const userReadTimes = _.mapValues(_.keyBy(userScores, 'value'), 'score'); + const isTopicsFollowed = {}; + followedTids.forEach((t) => { + isTopicsFollowed[t.value] = true; + }); + const unreadFollowed = await db.isSortedSetMembers( + `uid:${params.uid}:followed_tids`, tids_unread.map(t => t.value) + ); + + tids_unread.forEach((t, i) => { + isTopicsFollowed[t.value] = unreadFollowed[i]; + }); + + const unreadTopics = _.unionWith(categoryTids, followedTids, (a, b) => a.value === b.value) + .filter(t => !ignoredTids.includes(t.value) && + (!userReadTimes[t.value] || t.score > userReadTimes[t.value])) + .concat(tids_unread.filter(t => !ignoredTids.includes(t.value))) + .sort((a, b) => b.score - a.score); + + let tids = _.uniq(unreadTopics.map(topic => topic.value)).slice(0, 200); + + if (!tids.length) { + return { counts: counts, tids: tids, tidsByFilter: tidsByFilter }; + } + + const blockedUids = await user.blocks.list(params.uid); + + tids = await filterTidsThatHaveBlockedPosts({ + uid: params.uid, + tids: tids, + blockedUids: blockedUids, + recentTids: categoryTids, + }); + + tids = await privileges.topics.filterTids('topics:read', tids, params.uid); + const topicData = (await Topics.getTopicsFields(tids, ['tid', 'cid', 'uid', 'postcount', 'deleted', 'scheduled'])) + .filter(t => t.scheduled || !t.deleted); + const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); + + const categoryWatchState = await categories.getWatchState(topicCids, params.uid); + const userCidState = _.zipObject(topicCids, categoryWatchState); + + const filterCids = params.cid && params.cid.map(cid => parseInt(cid, 10)); + + topicData.forEach((topic) => { + if (topic && topic.cid && (!filterCids || filterCids.includes(topic.cid)) && + !blockedUids.includes(topic.uid)) { + if (isTopicsFollowed[topic.tid] || userCidState[topic.cid] === categories.watchStates.watching) { + tidsByFilter[''].push(topic.tid); + } + + if (isTopicsFollowed[topic.tid]) { + tidsByFilter.watched.push(topic.tid); + } + + if (topic.postcount <= 1) { + tidsByFilter.unreplied.push(topic.tid); + } + + if (!userReadTimes[topic.tid]) { + tidsByFilter.new.push(topic.tid); + } + } + }); + + counts[''] = tidsByFilter[''].length; + counts.watched = tidsByFilter.watched.length; + counts.unreplied = tidsByFilter.unreplied.length; + counts.new = tidsByFilter.new.length; + + return { + counts: counts, + tids: tidsByFilter[params.filter], + tidsByFilter: tidsByFilter, + }; + } + + async function getCategoryTids(params) { + if (plugins.hooks.hasListeners('filter:topics.unread.getCategoryTids')) { + const result = await plugins.hooks.fire('filter:topics.unread.getCategoryTids', { params: params, tids: [] }); + return result.tids; + } + if (params.filter === 'watched') { + return []; + } + const cids = params.cid || await user.getWatchedCategories(params.uid); + const keys = cids.map(cid => `cid:${cid}:tids:lastposttime`); + return await db.getSortedSetRevRangeByScoreWithScores(keys, 0, -1, '+inf', params.cutoff); + } + + async function getFollowedTids(params) { + let tids = await db.getSortedSetMembers(`uid:${params.uid}:followed_tids`); + const filterCids = params.cid && params.cid.map(cid => parseInt(cid, 10)); + if (filterCids) { + const topicData = await Topics.getTopicsFields(tids, ['tid', 'cid']); + tids = topicData.filter(t => filterCids.includes(t.cid)).map(t => t.tid); + } + const scores = await db.sortedSetScores('topics:recent', tids); + const data = tids.map((tid, index) => ({ value: String(tid), score: scores[index] })); + return data.filter(item => item.score > params.cutoff); + } + + async function filterTidsThatHaveBlockedPosts(params) { + if (!params.blockedUids.length) { + return params.tids; + } + const topicScores = _.mapValues(_.keyBy(params.recentTids, 'value'), 'score'); + + const results = await db.sortedSetScores(`uid:${params.uid}:tids_read`, params.tids); + + const userScores = _.zipObject(params.tids, results); + + return await async.filter(params.tids, async tid => await doesTidHaveUnblockedUnreadPosts(tid, { + blockedUids: params.blockedUids, + topicTimestamp: topicScores[tid], + userLastReadTimestamp: userScores[tid], + })); + } + + async function doesTidHaveUnblockedUnreadPosts(tid, params) { + const { userLastReadTimestamp } = params; + if (!userLastReadTimestamp) { + return true; + } + let start = 0; + const count = 3; + let done = false; + let hasUnblockedUnread = params.topicTimestamp > userLastReadTimestamp; + if (!params.blockedUids.length) { + return hasUnblockedUnread; + } + while (!done) { + /* eslint-disable no-await-in-loop */ + const pidsSinceLastVisit = await db.getSortedSetRangeByScore(`tid:${tid}:posts`, start, count, userLastReadTimestamp, '+inf'); + if (!pidsSinceLastVisit.length) { + return hasUnblockedUnread; + } + let postData = await posts.getPostsFields(pidsSinceLastVisit, ['pid', 'uid']); + postData = postData.filter(post => !params.blockedUids.includes(parseInt(post.uid, 10))); + + done = postData.length > 0; + hasUnblockedUnread = postData.length > 0; + start += count; + } + return hasUnblockedUnread; + } + + Topics.pushUnreadCount = async function (uid) { + if (!uid || parseInt(uid, 10) <= 0) { + return; + } + const results = await Topics.getUnreadTids({ uid: uid, count: true }); + require('../socket.io').in(`uid_${uid}`).emit('event:unread.updateCount', { + unreadTopicCount: results[''], + unreadNewTopicCount: results.new, + unreadWatchedTopicCount: results.watched, + unreadUnrepliedTopicCount: results.unreplied, + }); + }; + + Topics.markAsUnreadForAll = async function (tid) { + await Topics.markCategoryUnreadForAll(tid); + }; + + Topics.markAsRead = async function (tids, uid) { + if (!Array.isArray(tids) || !tids.length) { + return false; + } + + tids = _.uniq(tids).filter(tid => tid && utils.isNumber(tid)); + + if (!tids.length) { + return false; + } + const [topicScores, userScores] = await Promise.all([ + Topics.getTopicsFields(tids, ['tid', 'lastposttime', 'scheduled']), + db.sortedSetScores(`uid:${uid}:tids_read`, tids), + ]); + + const topics = topicScores.filter((t, i) => t.lastposttime && + (!userScores[i] || userScores[i] < t.lastposttime)); + tids = topics.map(t => t.tid); + + if (!tids.length) { + return false; + } + + const now = Date.now(); + const scores = topics.map(topic => (topic.scheduled ? topic.lastposttime : now)); + const [topicData] = await Promise.all([ + Topics.getTopicsFields(tids, ['cid']), + db.sortedSetAdd(`uid:${uid}:tids_read`, scores, tids), + db.sortedSetRemove(`uid:${uid}:tids_unread`, tids), + ]); + + const cids = _.uniq(topicData.map(t => t && t.cid).filter(Boolean)); + await categories.markAsRead(cids, uid); + + plugins.hooks.fire('action:topics.markAsRead', { uid: uid, tids: tids }); + return true; + }; + + Topics.markAllRead = async function (uid) { + const cutoff = await Topics.unreadCutoff(uid); + const tids = await db.getSortedSetRevRangeByScore('topics:recent', 0, -1, '+inf', cutoff); + Topics.markTopicNotificationsRead(tids, uid); + await Topics.markAsRead(tids, uid); + await db.delete(`uid:${uid}:tids_unread`); + }; + + Topics.markTopicNotificationsRead = async function (tids, uid) { + if (!Array.isArray(tids) || !tids.length) { + return; + } + const nids = await user.notifications.getUnreadByField(uid, 'tid', tids); + await notifications.markReadMultiple(nids, uid); + user.notifications.pushCount(uid); + }; + + Topics.markCategoryUnreadForAll = async function (tid) { + const cid = await Topics.getTopicField(tid, 'cid'); + await categories.markAsUnreadForAll(cid); + }; + + Topics.hasReadTopics = async function (tids, uid) { + if (!(parseInt(uid, 10) > 0)) { + return tids.map(() => false); + } + const [topicScores, userScores, tids_unread, blockedUids] = await Promise.all([ + db.sortedSetScores('topics:recent', tids), + db.sortedSetScores(`uid:${uid}:tids_read`, tids), + db.sortedSetScores(`uid:${uid}:tids_unread`, tids), + user.blocks.list(uid), + ]); + + const cutoff = await Topics.unreadCutoff(uid); + const result = tids.map((tid, index) => { + const read = !tids_unread[index] && + (topicScores[index] < cutoff || + !!(userScores[index] && userScores[index] >= topicScores[index])); + return { tid: tid, read: read, index: index }; + }); + + return await async.map(result, async (data) => { + if (data.read) { + return true; + } + const hasUnblockedUnread = await doesTidHaveUnblockedUnreadPosts(data.tid, { + topicTimestamp: topicScores[data.index], + userLastReadTimestamp: userScores[data.index], + blockedUids: blockedUids, + }); + if (!hasUnblockedUnread) { + data.read = true; + } + return data.read; + }); + }; + + Topics.hasReadTopic = async function (tid, uid) { + const hasRead = await Topics.hasReadTopics([tid], uid); + return Array.isArray(hasRead) && hasRead.length ? hasRead[0] : false; + }; + + Topics.markUnread = async function (tid, uid) { + const exists = await Topics.exists(tid); + if (!exists) { + throw new Error('[[error:no-topic]]'); + } + await db.sortedSetRemove(`uid:${uid}:tids_read`, tid); + await db.sortedSetAdd(`uid:${uid}:tids_unread`, Date.now(), tid); + }; + + Topics.filterNewTids = async function (tids, uid) { + if (parseInt(uid, 10) <= 0) { + return []; + } + const scores = await db.sortedSetScores(`uid:${uid}:tids_read`, tids); + return tids.filter((tid, index) => tid && !scores[index]); + }; + + Topics.filterUnrepliedTids = async function (tids) { + const scores = await db.sortedSetScores('topics:posts', tids); + return tids.filter((tid, index) => tid && scores[index] !== null && scores[index] <= 1); + }; +}; diff --git a/src/topics/user.js b/src/topics/user.js new file mode 100644 index 0000000000..3fec6efc85 --- /dev/null +++ b/src/topics/user.js @@ -0,0 +1,18 @@ +'use strict'; + +const db = require('../database'); + +module.exports = function (Topics) { + Topics.isOwner = async function (tid, uid) { + uid = parseInt(uid, 10); + if (uid <= 0) { + return false; + } + const author = await Topics.getTopicField(tid, 'uid'); + return author === uid; + }; + + Topics.getUids = async function (tid) { + return await db.getSortedSetRevRangeByScore(`tid:${tid}:posters`, 0, -1, '+inf', 1); + }; +}; diff --git a/src/translator.js b/src/translator.js new file mode 100644 index 0000000000..1efff6bc55 --- /dev/null +++ b/src/translator.js @@ -0,0 +1,12 @@ +'use strict'; + +const winston = require('winston'); + +function warn(msg) { + winston.warn(msg); +} + +module.exports = require('../public/src/modules/translator.common')(require('./utils'), (lang, namespace) => { + const languages = require('./languages'); + return languages.get(lang, namespace); +}, warn); diff --git a/src/types/admin.js b/src/types/admin.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/admin.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/admin.ts b/src/types/admin.ts new file mode 100644 index 0000000000..761c75ed0d --- /dev/null +++ b/src/types/admin.ts @@ -0,0 +1,25 @@ +export type Stats = { + stats: Stat[]; +}; + +export type Stat = { + yesterday: number; + today: number; + lastweek: number; + thisweek: number; + lastmonth: number; + thismonth: number; + alltime: number; + dayIncrease: string; + dayTextClass: string; + weekIncrease: string; + weekTextClass: string; + monthIncrease: string; + monthTextClass: string; + name: string; +} & StatOptionalProperties; + +export type StatOptionalProperties = { + name: string; + href: string; +}; diff --git a/src/types/breadcrumbs.js b/src/types/breadcrumbs.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/breadcrumbs.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/breadcrumbs.ts b/src/types/breadcrumbs.ts new file mode 100644 index 0000000000..336440ecfd --- /dev/null +++ b/src/types/breadcrumbs.ts @@ -0,0 +1,9 @@ +export type Breadcrumbs = { + breadcrumbs: Breadcrumb[]; +}; + +export type Breadcrumb = { + text: string; + url: string; + cid: number; +}; diff --git a/src/types/category.js b/src/types/category.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/category.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/category.ts b/src/types/category.ts new file mode 100644 index 0000000000..3683bf27dd --- /dev/null +++ b/src/types/category.ts @@ -0,0 +1,31 @@ +export type CategoryObject = { + cid: number; + name: string; + description: string; + descriptionParsed: string; + icon: string; + bgColor: string; + color: string; + slug: string; + parentCid: number; + topic_count: number; + post_count: number; + disabled: number; + order: number; + link: string; + numRecentReplies: number; + class: string; + imageClass: string; + isSection: number; + minTags: number; + maxTags: number; + postQueue: number; + totalPostCount: number; + totalTopicCount: number; + subCategoriesPerPage: number; +}; + +export type CategoryOptionalProperties = { + cid: number; + backgroundImage: string; +}; diff --git a/src/types/chat.js b/src/types/chat.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/chat.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/chat.ts b/src/types/chat.ts new file mode 100644 index 0000000000..319313f9a0 --- /dev/null +++ b/src/types/chat.ts @@ -0,0 +1,41 @@ +import { UserObjectSlim } from './user'; + +export type MessageObject = { + content: string; + timestamp: number; + fromuid: number; + roomId: number; + deleted: boolean; + system: boolean; + edited: number; + timestampISO: string; + editedISO: string; + messageId: number; + fromUser: UserObjectSlim; + self: number; + newSet: boolean; + cleanedContent: string; +}; + +export type RoomObject = { + owner: number; + roomId: number; + roomName: string; + groupChat: boolean; +}; + +export type RoomUserList = { + users: UserObjectSlim[]; +}; + +export type RoomObjectFull = { + isOwner: boolean; + users: UserObjectSlim[]; + canReply: boolean; + groupChat: boolean; + usernames: string; + maximumUsersInChatRoom: number; + maximumChatMessageLength: number; + showUserInput: boolean; + isAdminOrGlobalMod: boolean; +} & RoomObject & MessageObject; diff --git a/src/types/commonProps.js b/src/types/commonProps.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/commonProps.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/commonProps.ts b/src/types/commonProps.ts new file mode 100644 index 0000000000..99bb673707 --- /dev/null +++ b/src/types/commonProps.ts @@ -0,0 +1,33 @@ +import { TagObject } from './tag'; + +export type CommonProps = { + loggedIn: boolean; + relative_path: string; + template: Template; + url: string; + bodyClass: string; + _header: Header; + widgets: Widget[]; +}; + +export interface Template { + name: string; +} + +export interface Header { + tags: TagObject[]; + link: Link[]; +} + +export interface Link { + rel: string; + type: string; + href: string; + title: string; + sizes: string; + as: string; +} + +export interface Widget { + html: string; +} diff --git a/src/types/error.js b/src/types/error.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/error.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/error.ts b/src/types/error.ts new file mode 100644 index 0000000000..ba1e709593 --- /dev/null +++ b/src/types/error.ts @@ -0,0 +1,5 @@ +import { StatusObject } from './status'; + +export type ErrorObject = { + status: StatusObject; +}; diff --git a/src/types/flag.js b/src/types/flag.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/flag.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/flag.ts b/src/types/flag.ts new file mode 100644 index 0000000000..ef183300c9 --- /dev/null +++ b/src/types/flag.ts @@ -0,0 +1,55 @@ +import { UserObjectSlim } from './user'; + +export type FlagHistoryObject = { + history: History[]; +}; + +interface History { + uid: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fields: any; + meta: Meta[]; + datetime: number; + datetimeISO: string; + user: UserObjectSlim; +} + +interface Meta { + key: string; + value: string; + labelClass: string; +} + +export type FlagNotesObject = { + notes: Note[]; +}; + + +export interface Note { + uid: number; + content: string; + datetime: number; + datetimeISO: string; + user: UserObjectSlim; +} + +export type FlagObject = { + state: string; + flagId: number; + type: string; + targetId: number; + targetUid: number; + datetime: number; + datetimeISO: string; + target_readable: string; + target: object; + assignee: number; + reports: Reports; +} & FlagHistoryObject & FlagNotesObject; + +export interface Reports { + value: string; + timestamp: number; + timestampISO: string; + reporter: UserObjectSlim; +} diff --git a/src/types/group.js b/src/types/group.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/group.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/group.ts b/src/types/group.ts new file mode 100644 index 0000000000..c930d0f46a --- /dev/null +++ b/src/types/group.ts @@ -0,0 +1,44 @@ +import { UserObjectSlim } from './user'; + +export type GroupDataObject = { + name: string; + slug: string; + createtime: number; + userTitle: number; + userTitleEscaped: number; + userTitleEnabled: number; + description: string; + memberCount: number; + hidden: number; + system: number; + private: number; + disableJoinRequests: number; + disableLeave: number; + 'cover:url': string; + 'cover:thumb:url': string; + 'cover:position': string; + nameEncoded: string; + displayName: string; + labelColor: string; + textColor: string; + icon: string; + createtimeISO: string; + memberPostCids: string; + memberPostCidsArray: number[]; +}; + +export type GroupFullObject = GroupDataObject & GroupFullObjectProperties; + +export type GroupFullObjectProperties = { + descriptionParsed: string; + members: UserObjectSlim[]; + membersNextStart: number; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pending: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + invited: any[]; + isMember: boolean; + isPending: boolean; + isInvited: boolean; + isOwner: boolean; +}; diff --git a/src/types/index.js b/src/types/index.js new file mode 100644 index 0000000000..61b55517ae --- /dev/null +++ b/src/types/index.js @@ -0,0 +1,30 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./admin"), exports); +__exportStar(require("./breadcrumbs"), exports); +__exportStar(require("./category"), exports); +__exportStar(require("./commonProps"), exports); +__exportStar(require("./error"), exports); +__exportStar(require("./group"), exports); +__exportStar(require("./pagination"), exports); +__exportStar(require("./post"), exports); +__exportStar(require("./settings"), exports); +__exportStar(require("./social"), exports); +__exportStar(require("./status"), exports); +__exportStar(require("./tag"), exports); +__exportStar(require("./topic"), exports); +__exportStar(require("./user"), exports); diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000000..69444fc57d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,14 @@ +export * from './admin'; +export * from './breadcrumbs'; +export * from './category'; +export * from './commonProps'; +export * from './error'; +export * from './group'; +export * from './pagination'; +export * from './post'; +export * from './settings'; +export * from './social'; +export * from './status'; +export * from './tag'; +export * from './topic'; +export * from './user'; diff --git a/src/types/pagination.js b/src/types/pagination.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/pagination.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/pagination.ts b/src/types/pagination.ts new file mode 100644 index 0000000000..06141af59d --- /dev/null +++ b/src/types/pagination.ts @@ -0,0 +1,30 @@ +export type PaginationObject = { + pagination: Pagination; +}; + +export interface Pagination { + prev: ActivePage; + next: ActivePage; + first: ActivePage; + last: ActivePage; + rel: Relation[]; + pages: Page[]; + currentPage: number; + pageCount: number; +} + +interface ActivePage { + page: number; + active: boolean; +} + +interface Relation { + rel: string; + href: string; +} + +interface Page { + page: number; + active: boolean; + qs: string; +} diff --git a/src/types/post.js b/src/types/post.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/post.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/post.ts b/src/types/post.ts new file mode 100644 index 0000000000..982b7364ef --- /dev/null +++ b/src/types/post.ts @@ -0,0 +1,21 @@ +import { CategoryObject } from './category'; +import { TopicObject } from './topic'; +import { UserObjectSlim } from './user'; + +export type PostObject = { + pid: number; + tid: number; + content: string; + uid: number; + timestamp: number; + deleted: boolean; + upvotes: number; + downvotes: number; + votes: number; + timestampISO: string; + user: UserObjectSlim; + topic: TopicObject; + category: CategoryObject; + isMainPost: boolean; + replies: number; +}; diff --git a/src/types/settings.js b/src/types/settings.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/settings.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/settings.ts b/src/types/settings.ts new file mode 100644 index 0000000000..79b00187f1 --- /dev/null +++ b/src/types/settings.ts @@ -0,0 +1,41 @@ +export type SettingsObject = { + showemail: boolean; + usePagination: boolean; + topicsPerPage: number; + postsPerPage: number; + topicPostSort: string; + openOutgoingLinksInNewTab: boolean; + dailyDigestFreq: string; + showfullname: boolean; + followTopicsOnCreate: boolean; + followTopicsOnReply: boolean; + restrictChat: boolean; + topicSearchEnabled: boolean; + updateUrlWithPostIndex: boolean; + categoryTopicSort: string; + userLang: string; + bootswatchSkin: string; + homePageRoute: string; + scrollToMyPost: boolean; + 'notificationType_new-chat': string; + 'notificationType_new-group-chat': string; + 'notificationType_new-reply': string; + 'notificationType_post-edit': string; + sendChatNotifications: boolean; + sendPostNotifications: boolean; + notificationType_upvote: string; + 'notificationType_new-topic': string; + notificationType_follow: string; + 'notificationType_group-invite': string; + 'notificationType_group-leave': string; + upvoteNotifFreq: string; + notificationType_mention: string; + acpLang: string; + 'notificationType_new-register': string; + 'notificationType_post-queue': string; + 'notificationType_new-post-flag': string; + 'notificationType_new-user-flag': string; + categoryWatchState: string; + 'notificationType_group-request-membership': string; + uid: number; +}; diff --git a/src/types/social.js b/src/types/social.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/social.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/social.ts b/src/types/social.ts new file mode 100644 index 0000000000..9c9847f431 --- /dev/null +++ b/src/types/social.ts @@ -0,0 +1,6 @@ +export type Network = { + id: string; + name: string; + class: string; + activated: boolean | null; +}; diff --git a/src/types/status.js b/src/types/status.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/status.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/status.ts b/src/types/status.ts new file mode 100644 index 0000000000..9432fa29da --- /dev/null +++ b/src/types/status.ts @@ -0,0 +1,4 @@ +export type StatusObject = { + code: string; + message: string; +}; diff --git a/src/types/tag.js b/src/types/tag.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/tag.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/tag.ts b/src/types/tag.ts new file mode 100644 index 0000000000..46a8fd07c5 --- /dev/null +++ b/src/types/tag.ts @@ -0,0 +1,7 @@ +export type TagObject = { + value: string; + score: number; + valueEscaped: string; + color: string; + bgColor: string; +}; diff --git a/src/types/topic.js b/src/types/topic.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/topic.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/topic.ts b/src/types/topic.ts new file mode 100644 index 0000000000..0d2c08f1e6 --- /dev/null +++ b/src/types/topic.ts @@ -0,0 +1,81 @@ +import { CategoryObject } from './category'; +import { TagObject } from './tag'; +import { UserObjectSlim } from './user'; + +export type TopicObject = + TopicObjectSlim & TopicObjectCoreProperties & TopicObjectOptionalProperties; + +export type TopicObjectCoreProperties = { + lastposttime: number; + category: CategoryObject; + user: UserObjectSlim; + teaser: Teaser; + tags: TagObject[]; + isOwner: boolean; + ignored: boolean; + unread: boolean; + bookmark: number; + unreplied: boolean; + icons: string[]; +}; + +export type TopicObjectOptionalProperties = { + tid: number; + thumb: string; + pinExpiry: number; + pinExpiryISO: string; + index: number; +}; + +interface Teaser { + pid: number; + uid: number; + timestamp: number; + tid: number; + content: string; + timestampISO: string; + user: UserObjectSlim; + index: number; +} + +export type TopicObjectSlim = TopicSlimProperties & TopicSlimOptionalProperties; + +export type TopicSlimProperties = { + tid: number; + uid: number; + cid: number; + title: string; + slug: string; + mainPid: number; + postcount: string; + viewcount: string; + postercount: string; + scheduled: string; + deleted: string; + deleterUid: string; + titleRaw: string; + locked: string; + pinned: number; + timestamp: string; + timestampISO: number; + lastposttime: string; + lastposttimeISO: number; + pinExpiry: number; + pinExpiryISO: number; + upvotes: string; + downvotes: string; + votes: string; + teaserPid: number | string; + thumbs: Thumb[]; +}; + +export type Thumb = { + id: number; + name: string; + url: string; +}; + +export type TopicSlimOptionalProperties = { + tid: number; + numThumbs: number; +}; diff --git a/src/types/user.js b/src/types/user.js new file mode 100644 index 0000000000..c8ad2e549b --- /dev/null +++ b/src/types/user.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000000..a6572771d3 --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,133 @@ +import { GroupFullObject } from './group'; +import { StatusObject } from './status'; + +export type UserObjectSlim = { + uid: number; + username: string; + displayname: string; + userslug: string; + picture: string; + status: StatusObject; + postcount: number; + reputation: number; + 'email:confirmed': number; + lastonline: number; + flags: number; + banned: number; + 'banned:expire': number; + joindate: number; + accounttype: string; + 'icon:text': string; + 'icon:bgColor': string; + joindateISO: string; + lastonlineISO: string; + banned_until: number; + banned_until_readable: string; +}; + +export type UserObjectACP = UserObjectSlim & { + administrator: boolean; + ip: string; + ips: string[]; +}; + +export type UserObject = UserObjectSlim & { + email: string; + fullname: string; + location: string; + birthday: string; + website: string; + aboutme: string; + signature: string; + uploadedpicture: string; + profileviews: number; + topiccount: number; + lastposttime: number; + followerCount: number; + followingCount: number; + 'cover:url': string; + 'cover:position': string; + groupTitle: string; + groupTitleArray: string[]; +}; + +export type UserObjectFull = UserObject & { + aboutmeParsed: string; + age: number; + emailClass: string; + ips: string[]; + moderationNote: string; + counts: Counts; + isBlocked: boolean; + blocksCount: number; + yourid: number; + theirid: number; + isTargetAdmin: boolean; + isAdmin: boolean; + isGlobalModerator: boolean; + isModerator: boolean; + isAdminOrGlobalModerator: boolean; + isAdminOrGlobalModeratorOrModerator: boolean; + isSelfOrAdminOrGlobalModerator: boolean; + canEdit: boolean; + canBan: boolean; + canFlag: boolean; + canChangePassword: boolean; + isSelf: boolean; + isFollowing: boolean; + hasPrivateChat: number; + showHidden: boolean; + groups: GroupFullObject[]; + disableSignatures: boolean; + 'reputation:disabled': boolean; + 'downvote:disabled': boolean; + profile_links: ProfileLink[]; + sso: SSO[]; + websiteLink: string; + websiteName: string; + 'username:disableEdit': number; + 'email:disableEdit': number; +}; + +export type Counts = { + best: number; + blocks: number; + bookmarks: number; + categoriesWatched: number; + downvoted: number; + followers: number; + following: number; + groups: number; + ignored: number; + posts: number; + topics: number; + uploaded: number; + upvoted: number; + watched: number; +}; + +export type ProfileLink = { + id: string; + route: string; + name: string; + visibility: Visibility; + public: boolean; + icon: string; +}; + +export type Visibility = { + self: boolean; + other: boolean; + moderator: boolean; + globalMod: boolean; + admin: boolean; + canViewInfo: boolean; +}; + +export type SSO = { + associated: boolean; + url: string; + name: string; + icon: string; + deathUrl: string; +}; diff --git a/src/upgrade.js b/src/upgrade.js new file mode 100644 index 0000000000..d9e4e56f9c --- /dev/null +++ b/src/upgrade.js @@ -0,0 +1,204 @@ + +'use strict'; + +const path = require('path'); +const util = require('util'); +const semver = require('semver'); +const readline = require('readline'); +const winston = require('winston'); +const chalk = require('chalk'); + +const plugins = require('./plugins'); +const db = require('./database'); +const file = require('./file'); +const { paths } = require('./constants'); +/* + * Need to write an upgrade script for NodeBB? Cool. + * + * 1. Copy TEMPLATE to a unique file name of your choice. Try to be succinct. + * 2. Open up that file and change the user-friendly name (can be longer/more descriptive than the file name) + * and timestamp (don't forget the timestamp!) + * 3. Add your script under the "method" property + */ + +const Upgrade = module.exports; + +Upgrade.getAll = async function () { + let files = await file.walk(path.join(__dirname, './upgrades')); + + // Sort the upgrade scripts based on version + files = files.filter(file => path.basename(file) !== 'TEMPLATE').sort((a, b) => { + const versionA = path.dirname(a).split(path.sep).pop(); + const versionB = path.dirname(b).split(path.sep).pop(); + const semverCompare = semver.compare(versionA, versionB); + if (semverCompare) { + return semverCompare; + } + const timestampA = require(a).timestamp; + const timestampB = require(b).timestamp; + return timestampA - timestampB; + }); + + await Upgrade.appendPluginScripts(files); + + // check duplicates and error + const seen = {}; + const dupes = []; + files.forEach((file) => { + if (seen[file]) { + dupes.push(file); + } else { + seen[file] = true; + } + }); + if (dupes.length) { + winston.error(`Found duplicate upgrade scripts\n${dupes}`); + throw new Error('[[error:duplicate-upgrade-scripts]]'); + } + + return files; +}; + +Upgrade.appendPluginScripts = async function (files) { + // Find all active plugins + const activePlugins = await plugins.getActive(); + activePlugins.forEach((plugin) => { + const configPath = path.join(paths.nodeModules, plugin, 'plugin.json'); + try { + const pluginConfig = require(configPath); + if (pluginConfig.hasOwnProperty('upgrades') && Array.isArray(pluginConfig.upgrades)) { + pluginConfig.upgrades.forEach((script) => { + files.push(path.join(path.dirname(configPath), script)); + }); + } + } catch (e) { + if (e.code !== 'MODULE_NOT_FOUND') { + winston.error(e.stack); + } + } + }); + return files; +}; + +Upgrade.check = async function () { + // Throw 'schema-out-of-date' if not all upgrade scripts have run + const files = await Upgrade.getAll(); + const executed = await db.getSortedSetRange('schemaLog', 0, -1); + const remainder = files.filter(name => !executed.includes(path.basename(name, '.js'))); + if (remainder.length > 0) { + throw new Error('schema-out-of-date'); + } +}; + +Upgrade.run = async function () { + console.log('\nParsing upgrade scripts... '); + + const [completed, available] = await Promise.all([ + db.getSortedSetRange('schemaLog', 0, -1), + Upgrade.getAll(), + ]); + + let skipped = 0; + const queue = available.filter((cur) => { + const upgradeRan = completed.includes(path.basename(cur, '.js')); + if (upgradeRan) { + skipped += 1; + } + return !upgradeRan; + }); + + await Upgrade.process(queue, skipped); +}; + +Upgrade.runParticular = async function (names) { + console.log('\nParsing upgrade scripts... '); + const files = await file.walk(path.join(__dirname, './upgrades')); + await Upgrade.appendPluginScripts(files); + const upgrades = files.filter(file => names.includes(path.basename(file, '.js'))); + await Upgrade.process(upgrades, 0); +}; + +Upgrade.process = async function (files, skipCount) { + console.log(`${chalk.green('OK')} | ${chalk.cyan(`${files.length} script(s) found`)}${skipCount > 0 ? chalk.cyan(`, ${skipCount} skipped`) : ''}`); + const [schemaDate, schemaLogCount] = await Promise.all([ + db.get('schemaDate'), + db.sortedSetCard('schemaLog'), + ]); + + for (const file of files) { + /* eslint-disable no-await-in-loop */ + const scriptExport = require(file); + const date = new Date(scriptExport.timestamp); + const version = path.dirname(file).split('/').pop(); + const progress = { + current: 0, + counter: 0, + total: 0, + incr: Upgrade.incrementProgress, + script: scriptExport, + date: date, + }; + + process.stdout.write(`${chalk.white(' → ') + chalk.gray(`[${[date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()].join('/')}] `) + scriptExport.name}...`); + + // For backwards compatibility, cross-reference with schemaDate (if found). If a script's date is older, skip it + if ((!schemaDate && !schemaLogCount) || (scriptExport.timestamp <= schemaDate && semver.lt(version, '1.5.0'))) { + process.stdout.write(chalk.grey(' skipped\n')); + + await db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js')); + // eslint-disable-next-line no-continue + continue; + } + + // Promisify method if necessary + if (scriptExport.method.constructor && scriptExport.method.constructor.name !== 'AsyncFunction') { + scriptExport.method = util.promisify(scriptExport.method); + } + + // Do the upgrade... + const upgradeStart = Date.now(); + try { + await scriptExport.method.bind({ + progress: progress, + })(); + } catch (err) { + console.error('Error occurred'); + throw err; + } + const upgradeDuration = ((Date.now() - upgradeStart) / 1000).toFixed(2); + process.stdout.write(chalk.green(` OK (${upgradeDuration} seconds)\n`)); + + // Record success in schemaLog + await db.sortedSetAdd('schemaLog', Date.now(), path.basename(file, '.js')); + } + + console.log(chalk.green('Schema update complete!\n')); +}; + +Upgrade.incrementProgress = function (value) { + // Newline on first invocation + if (this.current === 0) { + process.stdout.write('\n'); + } + + this.current += value || 1; + this.counter += value || 1; + const step = (this.total ? Math.floor(this.total / 100) : 100); + + if (this.counter > step || this.current >= this.total) { + this.counter -= step; + let percentage = 0; + let filled = 0; + let unfilled = 15; + if (this.total) { + percentage = `${Math.floor((this.current / this.total) * 100)}%`; + filled = Math.floor((this.current / this.total) * 15); + unfilled = Math.max(0, 15 - filled); + } + + readline.cursorTo(process.stdout, 0); + process.stdout.write(` [${filled ? new Array(filled).join('#') : ''}${new Array(unfilled).join(' ')}] (${this.current}/${this.total || '??'}) ${percentage} `); + } +}; + +require('./promisify')(Upgrade); diff --git a/src/upgrades/1.0.0/chat_room_hashes.js b/src/upgrades/1.0.0/chat_room_hashes.js new file mode 100644 index 0000000000..37e035e1c7 --- /dev/null +++ b/src/upgrades/1.0.0/chat_room_hashes.js @@ -0,0 +1,39 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); + + +module.exports = { + name: 'Chat room hashes', + timestamp: Date.UTC(2015, 11, 23), + method: function (callback) { + db.getObjectField('global', 'nextChatRoomId', (err, nextChatRoomId) => { + if (err) { + return callback(err); + } + let currentChatRoomId = 1; + async.whilst((next) => { + next(null, currentChatRoomId <= nextChatRoomId); + }, (next) => { + db.getSortedSetRange(`chat:room:${currentChatRoomId}:uids`, 0, 0, (err, uids) => { + if (err) { + return next(err); + } + if (!Array.isArray(uids) || !uids.length || !uids[0]) { + currentChatRoomId += 1; + return next(); + } + + db.setObject(`chat:room:${currentChatRoomId}`, { owner: uids[0], roomId: currentChatRoomId }, (err) => { + if (err) { + return next(err); + } + currentChatRoomId += 1; + next(); + }); + }); + }, callback); + }); + }, +}; diff --git a/src/upgrades/1.0.0/chat_upgrade.js b/src/upgrades/1.0.0/chat_upgrade.js new file mode 100644 index 0000000000..73b82ae3ee --- /dev/null +++ b/src/upgrades/1.0.0/chat_upgrade.js @@ -0,0 +1,83 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Upgrading chats', + timestamp: Date.UTC(2015, 11, 15), + method: function (callback) { + db.getObjectFields('global', ['nextMid', 'nextChatRoomId'], (err, globalData) => { + if (err) { + return callback(err); + } + + const rooms = {}; + let roomId = globalData.nextChatRoomId || 1; + let currentMid = 1; + + async.whilst((next) => { + next(null, currentMid <= globalData.nextMid); + }, (next) => { + db.getObject(`message:${currentMid}`, (err, message) => { + if (err || !message) { + winston.verbose('skipping chat message ', currentMid); + currentMid += 1; + return next(err); + } + + const pairID = [parseInt(message.fromuid, 10), parseInt(message.touid, 10)].sort().join(':'); + const msgTime = parseInt(message.timestamp, 10); + + function addMessageToUids(roomId, callback) { + async.parallel([ + function (next) { + db.sortedSetAdd(`uid:${message.fromuid}:chat:room:${roomId}:mids`, msgTime, currentMid, next); + }, + function (next) { + db.sortedSetAdd(`uid:${message.touid}:chat:room:${roomId}:mids`, msgTime, currentMid, next); + }, + ], callback); + } + + if (rooms[pairID]) { + winston.verbose(`adding message ${currentMid} to existing roomID ${roomId}`); + addMessageToUids(rooms[pairID], (err) => { + if (err) { + return next(err); + } + currentMid += 1; + next(); + }); + } else { + winston.verbose(`adding message ${currentMid} to new roomID ${roomId}`); + async.parallel([ + function (next) { + db.sortedSetAdd(`uid:${message.fromuid}:chat:rooms`, msgTime, roomId, next); + }, + function (next) { + db.sortedSetAdd(`uid:${message.touid}:chat:rooms`, msgTime, roomId, next); + }, + function (next) { + db.sortedSetAdd(`chat:room:${roomId}:uids`, [msgTime, msgTime + 1], [message.fromuid, message.touid], next); + }, + function (next) { + addMessageToUids(roomId, next); + }, + ], (err) => { + if (err) { + return next(err); + } + rooms[pairID] = roomId; + roomId += 1; + currentMid += 1; + db.setObjectField('global', 'nextChatRoomId', roomId, next); + }); + } + }); + }, callback); + }); + }, +}; diff --git a/src/upgrades/1.0.0/global_moderators.js b/src/upgrades/1.0.0/global_moderators.js new file mode 100644 index 0000000000..46e6799ccc --- /dev/null +++ b/src/upgrades/1.0.0/global_moderators.js @@ -0,0 +1,22 @@ +'use strict'; + +module.exports = { + name: 'Creating Global moderators group', + timestamp: Date.UTC(2016, 0, 23), + method: async function () { + const groups = require('../../groups'); + const exists = await groups.exists('Global Moderators'); + if (exists) { + return; + } + await groups.create({ + name: 'Global Moderators', + userTitle: 'Global Moderator', + description: 'Forum wide moderators', + hidden: 0, + private: 1, + disableJoinRequests: 1, + }); + await groups.show('Global Moderators'); + }, +}; diff --git a/src/upgrades/1.0.0/social_post_sharing.js b/src/upgrades/1.0.0/social_post_sharing.js new file mode 100644 index 0000000000..240af1d419 --- /dev/null +++ b/src/upgrades/1.0.0/social_post_sharing.js @@ -0,0 +1,21 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); + + +module.exports = { + name: 'Social: Post Sharing', + timestamp: Date.UTC(2016, 1, 25), + method: function (callback) { + const social = require('../../social'); + async.parallel([ + async function () { + await social.setActivePostSharingNetworks(['facebook', 'google', 'twitter']); + }, + async function () { + await db.deleteObjectField('config', 'disableSocialButtons'); + }, + ], callback); + }, +}; diff --git a/src/upgrades/1.0.0/theme_to_active_plugins.js b/src/upgrades/1.0.0/theme_to_active_plugins.js new file mode 100644 index 0000000000..9af759b2ad --- /dev/null +++ b/src/upgrades/1.0.0/theme_to_active_plugins.js @@ -0,0 +1,13 @@ +'use strict'; + +const db = require('../../database'); + + +module.exports = { + name: 'Adding theme to active plugins sorted set', + timestamp: Date.UTC(2015, 11, 23), + method: async function () { + const themeId = await db.getObjectField('config', 'theme:id'); + await db.sortedSetAdd('plugins:active', 0, themeId); + }, +}; diff --git a/src/upgrades/1.0.0/user_best_posts.js b/src/upgrades/1.0.0/user_best_posts.js new file mode 100644 index 0000000000..abfd20dcb7 --- /dev/null +++ b/src/upgrades/1.0.0/user_best_posts.js @@ -0,0 +1,33 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Creating user best post sorted sets', + timestamp: Date.UTC(2016, 0, 14), + method: function (callback) { + const batch = require('../../batch'); + const { progress } = this; + + batch.processSortedSet('posts:pid', (ids, next) => { + async.eachSeries(ids, (id, next) => { + db.getObjectFields(`post:${id}`, ['pid', 'uid', 'votes'], (err, postData) => { + if (err) { + return next(err); + } + if (!postData || !parseInt(postData.votes, 10) || !parseInt(postData.uid, 10)) { + return next(); + } + winston.verbose(`processing pid: ${postData.pid} uid: ${postData.uid} votes: ${postData.votes}`); + db.sortedSetAdd(`uid:${postData.uid}:posts:votes`, postData.votes, postData.pid, next); + progress.incr(); + }); + }, next); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.0.0/users_notvalidated.js b/src/upgrades/1.0.0/users_notvalidated.js new file mode 100644 index 0000000000..22b05aa224 --- /dev/null +++ b/src/upgrades/1.0.0/users_notvalidated.js @@ -0,0 +1,29 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Creating users:notvalidated', + timestamp: Date.UTC(2016, 0, 20), + method: function (callback) { + const batch = require('../../batch'); + const now = Date.now(); + batch.processSortedSet('users:joindate', (ids, next) => { + async.eachSeries(ids, (id, next) => { + db.getObjectFields(`user:${id}`, ['uid', 'email:confirmed'], (err, userData) => { + if (err) { + return next(err); + } + if (!userData || !parseInt(userData.uid, 10) || parseInt(userData['email:confirmed'], 10) === 1) { + return next(); + } + winston.verbose(`processing uid: ${userData.uid} email:confirmed: ${userData['email:confirmed']}`); + db.sortedSetAdd('users:notvalidated', now, userData.uid, next); + }); + }, next); + }, callback); + }, +}; diff --git a/src/upgrades/1.1.0/assign_topic_read_privilege.js b/src/upgrades/1.1.0/assign_topic_read_privilege.js new file mode 100644 index 0000000000..a9dd452a71 --- /dev/null +++ b/src/upgrades/1.1.0/assign_topic_read_privilege.js @@ -0,0 +1,35 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Giving topics:read privs to any group/user that was previously allowed to Find & Access Category', + timestamp: Date.UTC(2016, 4, 28), + method: async function () { + const groupsAPI = require('../../groups'); + const privilegesAPI = require('../../privileges'); + + const cids = await db.getSortedSetRange('categories:cid', 0, -1); + for (const cid of cids) { + const { groups, users } = await privilegesAPI.categories.list(cid); + + for (const group of groups) { + if (group.privileges['groups:read']) { + await groupsAPI.join(`cid:${cid}:privileges:groups:topics:read`, group.name); + winston.verbose(`cid:${cid}:privileges:groups:topics:read granted to gid: ${group.name}`); + } + } + + for (const user of users) { + if (user.privileges.read) { + await groupsAPI.join(`cid:${cid}:privileges:topics:read`, user.uid); + winston.verbose(`cid:${cid}:privileges:topics:read granted to uid: ${user.uid}`); + } + } + winston.verbose(`-- cid ${cid} upgraded`); + } + }, +}; diff --git a/src/upgrades/1.1.0/dismiss_flags_from_deleted_topics.js b/src/upgrades/1.1.0/dismiss_flags_from_deleted_topics.js new file mode 100644 index 0000000000..d860959650 --- /dev/null +++ b/src/upgrades/1.1.0/dismiss_flags_from_deleted_topics.js @@ -0,0 +1,56 @@ +'use strict'; + + +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Dismiss flags from deleted topics', + timestamp: Date.UTC(2016, 3, 29), + method: async function () { + const posts = require('../../posts'); + const topics = require('../../topics'); + + const pids = await db.getSortedSetRange('posts:flagged', 0, -1); + const postData = await posts.getPostsFields(pids, ['tid']); + const tids = postData.map(t => t.tid); + const topicData = await topics.getTopicsFields(tids, ['deleted']); + const toDismiss = topicData.map((t, idx) => (parseInt(t.deleted, 10) === 1 ? pids[idx] : null)).filter(Boolean); + + winston.verbose(`[2016/04/29] ${toDismiss.length} dismissable flags found`); + await Promise.all(toDismiss.map(dismissFlag)); + }, +}; + +// copied from core since this function was removed +// https://github.com/NodeBB/NodeBB/blob/v1.x.x/src/posts/flags.js +async function dismissFlag(pid) { + const postData = await db.getObjectFields(`post:${pid}`, ['pid', 'uid', 'flags']); + if (!postData.pid) { + return; + } + if (parseInt(postData.uid, 10) && parseInt(postData.flags, 10) > 0) { + await Promise.all([ + db.sortedSetIncrBy('users:flags', -postData.flags, postData.uid), + db.incrObjectFieldBy(`user:${postData.uid}`, 'flags', -postData.flags), + ]); + } + const uids = await db.getSortedSetRange(`pid:${pid}:flag:uids`, 0, -1); + const nids = uids.map(uid => `post_flag:${pid}:uid:${uid}`); + + await Promise.all([ + db.deleteAll(nids.map(nid => `notifications:${nid}`)), + db.sortedSetRemove('notifications', nids), + db.delete(`pid:${pid}:flag:uids`), + db.sortedSetsRemove([ + 'posts:flagged', + 'posts:flags:count', + `uid:${postData.uid}:flag:pids`, + ], pid), + db.deleteObjectField(`post:${pid}`, 'flags'), + db.delete(`pid:${pid}:flag:uid:reason`), + db.deleteObjectFields(`post:${pid}`, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history']), + ]); + + await db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0); +} diff --git a/src/upgrades/1.1.0/group_title_update.js b/src/upgrades/1.1.0/group_title_update.js new file mode 100644 index 0000000000..fd308cea44 --- /dev/null +++ b/src/upgrades/1.1.0/group_title_update.js @@ -0,0 +1,30 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Group title from settings to user profile', + timestamp: Date.UTC(2016, 3, 14), + method: function (callback) { + const user = require('../../user'); + const batch = require('../../batch'); + let count = 0; + batch.processSortedSet('users:joindate', (uids, next) => { + winston.verbose(`upgraded ${count} users`); + user.getMultipleUserSettings(uids, (err, settings) => { + if (err) { + return next(err); + } + count += uids.length; + settings = settings.filter(setting => setting && setting.groupTitle); + + async.each(settings, (setting, next) => { + db.setObjectField(`user:${setting.uid}`, 'groupTitle', setting.groupTitle, next); + }, next); + }); + }, {}, callback); + }, +}; diff --git a/src/upgrades/1.1.0/separate_upvote_downvote.js b/src/upgrades/1.1.0/separate_upvote_downvote.js new file mode 100644 index 0000000000..ac44c32fd7 --- /dev/null +++ b/src/upgrades/1.1.0/separate_upvote_downvote.js @@ -0,0 +1,54 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Store upvotes/downvotes separately', + timestamp: Date.UTC(2016, 5, 13), + method: function (callback) { + const batch = require('../../batch'); + const posts = require('../../posts'); + let count = 0; + const { progress } = this; + + batch.processSortedSet('posts:pid', (pids, next) => { + winston.verbose(`upgraded ${count} posts`); + count += pids.length; + async.each(pids, (pid, next) => { + async.parallel({ + upvotes: function (next) { + db.setCount(`pid:${pid}:upvote`, next); + }, + downvotes: function (next) { + db.setCount(`pid:${pid}:downvote`, next); + }, + }, (err, results) => { + if (err) { + return next(err); + } + const data = {}; + + if (parseInt(results.upvotes, 10) > 0) { + data.upvotes = results.upvotes; + } + if (parseInt(results.downvotes, 10) > 0) { + data.downvotes = results.downvotes; + } + + if (Object.keys(data).length) { + posts.setPostFields(pid, data, next); + } else { + next(); + } + + progress.incr(); + }, next); + }, next); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.1.0/user_post_count_per_tid.js b/src/upgrades/1.1.0/user_post_count_per_tid.js new file mode 100644 index 0000000000..11957df32e --- /dev/null +++ b/src/upgrades/1.1.0/user_post_count_per_tid.js @@ -0,0 +1,48 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Users post count per tid', + timestamp: Date.UTC(2016, 3, 19), + method: function (callback) { + const batch = require('../../batch'); + const topics = require('../../topics'); + let count = 0; + batch.processSortedSet('topics:tid', (tids, next) => { + winston.verbose(`upgraded ${count} topics`); + count += tids.length; + async.each(tids, (tid, next) => { + db.delete(`tid:${tid}:posters`, (err) => { + if (err) { + return next(err); + } + topics.getPids(tid, (err, pids) => { + if (err) { + return next(err); + } + + if (!pids.length) { + return next(); + } + + async.eachSeries(pids, (pid, next) => { + db.getObjectField(`post:${pid}`, 'uid', (err, uid) => { + if (err) { + return next(err); + } + if (!parseInt(uid, 10)) { + return next(); + } + db.sortedSetIncrBy(`tid:${tid}:posters`, 1, uid, next); + }); + }, next); + }); + }); + }, next); + }, {}, callback); + }, +}; diff --git a/src/upgrades/1.1.1/remove_negative_best_posts.js b/src/upgrades/1.1.1/remove_negative_best_posts.js new file mode 100644 index 0000000000..4d848e2bb2 --- /dev/null +++ b/src/upgrades/1.1.1/remove_negative_best_posts.js @@ -0,0 +1,20 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Removing best posts with negative scores', + timestamp: Date.UTC(2016, 7, 5), + method: function (callback) { + const batch = require('../../batch'); + batch.processSortedSet('users:joindate', (ids, next) => { + async.each(ids, (id, next) => { + winston.verbose(`processing uid ${id}`); + db.sortedSetsRemoveRangeByScore([`uid:${id}:posts:votes`], '-inf', 0, next); + }, next); + }, {}, callback); + }, +}; diff --git a/src/upgrades/1.1.1/upload_privileges.js b/src/upgrades/1.1.1/upload_privileges.js new file mode 100644 index 0000000000..73d2aed5a4 --- /dev/null +++ b/src/upgrades/1.1.1/upload_privileges.js @@ -0,0 +1,38 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); + + +module.exports = { + name: 'Giving upload privileges', + timestamp: Date.UTC(2016, 6, 12), + method: function (callback) { + const privilegesAPI = require('../../privileges'); + const meta = require('../../meta'); + + db.getSortedSetRange('categories:cid', 0, -1, (err, cids) => { + if (err) { + return callback(err); + } + + async.eachSeries(cids, (cid, next) => { + privilegesAPI.categories.list(cid, (err, data) => { + if (err) { + return next(err); + } + async.eachSeries(data.groups, (group, next) => { + if (group.name === 'guests' && parseInt(meta.config.allowGuestUploads, 10) !== 1) { + return next(); + } + if (group.privileges['groups:read']) { + privilegesAPI.categories.give(['upload:post:image'], cid, group.name, next); + } else { + next(); + } + }, next); + }); + }, callback); + }); + }, +}; diff --git a/src/upgrades/1.10.0/hash_recent_ip_addresses.js b/src/upgrades/1.10.0/hash_recent_ip_addresses.js new file mode 100644 index 0000000000..a97c4f1ea4 --- /dev/null +++ b/src/upgrades/1.10.0/hash_recent_ip_addresses.js @@ -0,0 +1,41 @@ +'use strict'; + + +const async = require('async'); +const crypto = require('crypto'); +const nconf = require('nconf'); +const batch = require('../../batch'); +const db = require('../../database'); + +module.exports = { + name: 'Hash all IP addresses stored in Recent IPs zset', + timestamp: Date.UTC(2018, 5, 22), + method: function (callback) { + const { progress } = this; + const hashed = /[a-f0-9]{32}/; + let hash; + + batch.processSortedSet('ip:recent', (ips, next) => { + async.each(ips, (set, next) => { + // Short circuit if already processed + if (hashed.test(set.value)) { + progress.incr(); + return setImmediate(next); + } + + hash = crypto.createHash('sha1').update(set.value + nconf.get('secret')).digest('hex'); + + async.series([ + async.apply(db.sortedSetAdd, 'ip:recent', set.score, hash), + async.apply(db.sortedSetRemove, 'ip:recent', set.value), + ], (err) => { + progress.incr(); + next(err); + }); + }, next); + }, { + withScores: 1, + progress: this.progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.10.0/post_history_privilege.js b/src/upgrades/1.10.0/post_history_privilege.js new file mode 100644 index 0000000000..c556e65969 --- /dev/null +++ b/src/upgrades/1.10.0/post_history_privilege.js @@ -0,0 +1,22 @@ +'use strict'; + + +const async = require('async'); + +const privileges = require('../../privileges'); +const db = require('../../database'); + +module.exports = { + name: 'Give post history viewing privilege to registered-users on all categories', + timestamp: Date.UTC(2018, 5, 7), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, (err, cids) => { + if (err) { + return callback(err); + } + async.eachSeries(cids, (cid, next) => { + privileges.categories.give(['groups:posts:history'], cid, 'registered-users', next); + }, callback); + }); + }, +}; diff --git a/src/upgrades/1.10.0/search_privileges.js b/src/upgrades/1.10.0/search_privileges.js new file mode 100644 index 0000000000..ed9dbd2ef9 --- /dev/null +++ b/src/upgrades/1.10.0/search_privileges.js @@ -0,0 +1,23 @@ +'use strict'; + +module.exports = { + name: 'Give global search privileges', + timestamp: Date.UTC(2018, 4, 28), + method: async function () { + const meta = require('../../meta'); + const privileges = require('../../privileges'); + const allowGuestSearching = parseInt(meta.config.allowGuestSearching, 10) === 1; + const allowGuestUserSearching = parseInt(meta.config.allowGuestUserSearching, 10) === 1; + + await privileges.global.give(['groups:search:content', 'groups:search:users', 'groups:search:tags'], 'registered-users'); + const guestPrivs = []; + if (allowGuestSearching) { + guestPrivs.push('groups:search:content'); + } + if (allowGuestUserSearching) { + guestPrivs.push('groups:search:users'); + } + guestPrivs.push('groups:search:tags'); + await privileges.global.give(guestPrivs, 'guests'); + }, +}; diff --git a/src/upgrades/1.10.0/view_deleted_privilege.js b/src/upgrades/1.10.0/view_deleted_privilege.js new file mode 100644 index 0000000000..d099e0fb94 --- /dev/null +++ b/src/upgrades/1.10.0/view_deleted_privilege.js @@ -0,0 +1,22 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const groups = require('../../groups'); +const db = require('../../database'); + +module.exports = { + name: 'Give deleted post viewing privilege to moderators on all categories', + timestamp: Date.UTC(2018, 5, 8), + method: async function () { + const { progress } = this; + const cids = await db.getSortedSetRange('categories:cid', 0, -1); + for (const cid of cids) { + const uids = await db.getSortedSetRange(`group:cid:${cid}:privileges:moderate:members`, 0, -1); + for (const uid of uids) { + await groups.join(`cid:${cid}:privileges:posts:view_deleted`, uid); + } + progress.incr(); + } + }, +}; diff --git a/src/upgrades/1.10.2/event_filters.js b/src/upgrades/1.10.2/event_filters.js new file mode 100644 index 0000000000..6f8877fd58 --- /dev/null +++ b/src/upgrades/1.10.2/event_filters.js @@ -0,0 +1,37 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); + +module.exports = { + name: 'add filters to events', + timestamp: Date.UTC(2018, 9, 4), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('events:time', async (eids) => { + for (const eid of eids) { + progress.incr(); + + const eventData = await db.getObject(`event:${eid}`); + if (!eventData) { + await db.sortedSetRemove('events:time', eid); + return; + } + // privilege events we're missing type field + if (!eventData.type && eventData.privilege) { + eventData.type = 'privilege-change'; + await db.setObjectField(`event:${eid}`, 'type', 'privilege-change'); + await db.sortedSetAdd(`events:time:${eventData.type}`, eventData.timestamp, eid); + return; + } + await db.sortedSetAdd(`events:time:${eventData.type || ''}`, eventData.timestamp, eid); + } + }, { + progress: this.progress, + }); + }, +}; diff --git a/src/upgrades/1.10.2/fix_category_post_zsets.js b/src/upgrades/1.10.2/fix_category_post_zsets.js new file mode 100644 index 0000000000..82003c8b36 --- /dev/null +++ b/src/upgrades/1.10.2/fix_category_post_zsets.js @@ -0,0 +1,32 @@ +'use strict'; + +const db = require('../../database'); +const posts = require('../../posts'); +const topics = require('../../topics'); +const batch = require('../../batch'); + +module.exports = { + name: 'Fix category post zsets', + timestamp: Date.UTC(2018, 9, 10), + method: async function () { + const { progress } = this; + + const cids = await db.getSortedSetRange('categories:cid', 0, -1); + const keys = cids.map(cid => `cid:${cid}:pids`); + + await batch.processSortedSet('posts:pid', async (postData) => { + const pids = postData.map(p => p.value); + const topicData = await posts.getPostsFields(pids, ['tid']); + const categoryData = await topics.getTopicsFields(topicData.map(t => t.tid), ['cid']); + + await db.sortedSetRemove(keys, pids); + const bulkAdd = postData.map((p, i) => ([`cid:${categoryData[i].cid}:pids`, p.score, p.value])); + await db.sortedSetAddBulk(bulkAdd); + progress.incr(postData.length); + }, { + batch: 500, + progress: progress, + withScores: true, + }); + }, +}; diff --git a/src/upgrades/1.10.2/fix_category_topic_zsets.js b/src/upgrades/1.10.2/fix_category_topic_zsets.js new file mode 100644 index 0000000000..b4becca0d3 --- /dev/null +++ b/src/upgrades/1.10.2/fix_category_topic_zsets.js @@ -0,0 +1,30 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); + +module.exports = { + name: 'Fix category topic zsets', + timestamp: Date.UTC(2018, 9, 11), + method: async function () { + const { progress } = this; + + const topics = require('../../topics'); + await batch.processSortedSet('topics:tid', async (tids) => { + for (const tid of tids) { + progress.incr(); + const topicData = await db.getObjectFields(`topic:${tid}`, ['cid', 'pinned', 'postcount']); + if (parseInt(topicData.pinned, 10) !== 1) { + topicData.postcount = parseInt(topicData.postcount, 10) || 0; + await db.sortedSetAdd(`cid:${topicData.cid}:tids:posts`, topicData.postcount, tid); + } + await topics.updateLastPostTimeFromLastPid(tid); + } + }, { + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.10.2/local_login_privileges.js b/src/upgrades/1.10.2/local_login_privileges.js new file mode 100644 index 0000000000..daedd2db13 --- /dev/null +++ b/src/upgrades/1.10.2/local_login_privileges.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = { + name: 'Give global local login privileges', + timestamp: Date.UTC(2018, 8, 28), + method: function (callback) { + const meta = require('../../meta'); + const privileges = require('../../privileges'); + const allowLocalLogin = parseInt(meta.config.allowLocalLogin, 10) !== 0; + + if (allowLocalLogin) { + privileges.global.give(['groups:local:login'], 'registered-users', callback); + } else { + callback(); + } + }, +}; diff --git a/src/upgrades/1.10.2/postgres_sessions.js b/src/upgrades/1.10.2/postgres_sessions.js new file mode 100644 index 0000000000..cbf8a80d9a --- /dev/null +++ b/src/upgrades/1.10.2/postgres_sessions.js @@ -0,0 +1,41 @@ +'use strict'; + +const nconf = require('nconf'); +const db = require('../../database'); + +module.exports = { + name: 'Optimize PostgreSQL sessions', + timestamp: Date.UTC(2018, 9, 1), + method: function (callback) { + if (nconf.get('database') !== 'postgres' || nconf.get('redis')) { + return callback(); + } + + db.pool.query(` +BEGIN TRANSACTION; + +CREATE TABLE IF NOT EXISTS "session" ( + "sid" CHAR(32) NOT NULL + COLLATE "C" + PRIMARY KEY, + "sess" JSONB NOT NULL, + "expire" TIMESTAMPTZ NOT NULL +) WITHOUT OIDS; + +CREATE INDEX IF NOT EXISTS "session_expire_idx" ON "session"("expire"); + +ALTER TABLE "session" + ALTER "sid" TYPE CHAR(32) COLLATE "C", + ALTER "sid" SET STORAGE PLAIN, + ALTER "sess" TYPE JSONB, + ALTER "expire" TYPE TIMESTAMPTZ, + CLUSTER ON "session_expire_idx"; + +CLUSTER "session"; +ANALYZE "session"; + +COMMIT;`, (err) => { + callback(err); + }); + }, +}; diff --git a/src/upgrades/1.10.2/upgrade_bans_to_hashes.js b/src/upgrades/1.10.2/upgrade_bans_to_hashes.js new file mode 100644 index 0000000000..250b859db9 --- /dev/null +++ b/src/upgrades/1.10.2/upgrade_bans_to_hashes.js @@ -0,0 +1,59 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Upgrade bans to hashes', + timestamp: Date.UTC(2018, 8, 24), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (uids) => { + for (const uid of uids) { + progress.incr(); + const [bans, reasons, userData] = await Promise.all([ + db.getSortedSetRevRangeWithScores(`uid:${uid}:bans`, 0, -1), + db.getSortedSetRevRangeWithScores(`banned:${uid}:reasons`, 0, -1), + db.getObjectFields(`user:${uid}`, ['banned', 'banned:expire', 'joindate', 'lastposttime', 'lastonline']), + ]); + + // has no history, but is banned, create plain object with just uid and timestmap + if (!bans.length && parseInt(userData.banned, 10)) { + const banTimestamp = ( + userData.lastonline || + userData.lastposttime || + userData.joindate || + Date.now() + ); + const banKey = `uid:${uid}:ban:${banTimestamp}`; + await addBan(uid, banKey, { uid: uid, timestamp: banTimestamp }); + } else if (bans.length) { + // process ban history + for (const ban of bans) { + const reasonData = reasons.find(reasonData => reasonData.score === ban.score); + const banKey = `uid:${uid}:ban:${ban.score}`; + const data = { + uid: uid, + timestamp: ban.score, + expire: parseInt(ban.value, 10), + }; + if (reasonData) { + data.reason = reasonData.value; + } + await addBan(uid, banKey, data); + } + } + } + }, { + progress: this.progress, + }); + }, +}; + +async function addBan(uid, key, data) { + await db.setObject(key, data); + await db.sortedSetAdd(`uid:${uid}:bans:timestamp`, data.timestamp, key); +} diff --git a/src/upgrades/1.10.2/username_email_history.js b/src/upgrades/1.10.2/username_email_history.js new file mode 100644 index 0000000000..c8cc2f7c0f --- /dev/null +++ b/src/upgrades/1.10.2/username_email_history.js @@ -0,0 +1,37 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); +const user = require('../../user'); + +module.exports = { + name: 'Record first entry in username/email history', + timestamp: Date.UTC(2018, 7, 28), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (uids) => { + async function updateHistory(uid, set, fieldName) { + const count = await db.sortedSetCard(set); + if (count <= 0) { + // User has not changed their username/email before, record original username + const userData = await user.getUserFields(uid, [fieldName, 'joindate']); + if (userData && userData.joindate && userData[fieldName]) { + await db.sortedSetAdd(set, userData.joindate, [userData[fieldName], userData.joindate].join(':')); + } + } + } + + await Promise.all(uids.map(async (uid) => { + await Promise.all([ + updateHistory(uid, `user:${uid}:usernames`, 'username'), + updateHistory(uid, `user:${uid}:emails`, 'email'), + ]); + progress.incr(); + })); + }, { + progress: this.progress, + }); + }, +}; diff --git a/src/upgrades/1.11.0/navigation_visibility_groups.js b/src/upgrades/1.11.0/navigation_visibility_groups.js new file mode 100644 index 0000000000..556f601642 --- /dev/null +++ b/src/upgrades/1.11.0/navigation_visibility_groups.js @@ -0,0 +1,58 @@ +'use strict'; + +module.exports = { + name: 'Navigation item visibility groups', + timestamp: Date.UTC(2018, 10, 10), + method: async function () { + const data = await navigationAdminGet(); + data.forEach((navItem) => { + if (navItem && navItem.properties) { + navItem.groups = []; + if (navItem.properties.adminOnly) { + navItem.groups.push('administrators'); + } else if (navItem.properties.globalMod) { + navItem.groups.push('Global Moderators'); + } + + if (navItem.properties.loggedIn) { + navItem.groups.push('registered-users'); + } else if (navItem.properties.guestOnly) { + navItem.groups.push('guests'); + } + } + }); + await navigationAdminSave(data); + }, +}; +// use navigation.get/save as it was in 1.11.0 so upgrade script doesn't crash on latest nbb +// see https://github.com/NodeBB/NodeBB/pull/11013 +async function navigationAdminGet() { + const db = require('../../database'); + const data = await db.getSortedSetRange('navigation:enabled', 0, -1); + return data.filter(Boolean).map((item) => { + item = JSON.parse(item); + item.groups = item.groups || []; + if (item.groups && !Array.isArray(item.groups)) { + item.groups = [item.groups]; + } + return item; + }); +} + +async function navigationAdminSave(data) { + const db = require('../../database'); + const translator = require('../../translator'); + const order = Object.keys(data); + const items = data.map((item, index) => { + Object.keys(item).forEach((key) => { + if (item.hasOwnProperty(key) && typeof item[key] === 'string' && (key === 'title' || key === 'text')) { + item[key] = translator.escape(item[key]); + } + }); + item.order = order[index]; + return JSON.stringify(item); + }); + + await db.delete('navigation:enabled'); + await db.sortedSetAdd('navigation:enabled', order, items); +} diff --git a/src/upgrades/1.11.0/resize_image_width.js b/src/upgrades/1.11.0/resize_image_width.js new file mode 100644 index 0000000000..251d4374b4 --- /dev/null +++ b/src/upgrades/1.11.0/resize_image_width.js @@ -0,0 +1,14 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Rename maximumImageWidth to resizeImageWidth', + timestamp: Date.UTC(2018, 9, 24), + method: async function () { + const meta = require('../../meta'); + const value = await meta.configs.get('maximumImageWidth'); + await meta.configs.set('resizeImageWidth', value); + await db.deleteObjectField('config', 'maximumImageWidth'); + }, +}; diff --git a/src/upgrades/1.11.0/widget_visibility_groups.js b/src/upgrades/1.11.0/widget_visibility_groups.js new file mode 100644 index 0000000000..bbe4a6cc50 --- /dev/null +++ b/src/upgrades/1.11.0/widget_visibility_groups.js @@ -0,0 +1,38 @@ +'use strict'; + +module.exports = { + name: 'Widget visibility groups', + timestamp: Date.UTC(2018, 10, 10), + method: async function () { + const widgetAdmin = require('../../widgets/admin'); + const widgets = require('../../widgets'); + const areas = await widgetAdmin.getAreas(); + for (const area of areas) { + if (area.data.length) { + // area.data is actually an array of widgets + area.widgets = area.data; + area.widgets.forEach((widget) => { + if (widget && widget.data) { + const groupsToShow = ['administrators', 'Global Moderators']; + if (widget.data['hide-guests'] !== 'on') { + groupsToShow.push('guests'); + } + if (widget.data['hide-registered'] !== 'on') { + groupsToShow.push('registered-users'); + } + + widget.data.groups = groupsToShow; + + // if we are showing to all 4 groups, set to empty array + // empty groups is shown to everyone + if (groupsToShow.length === 4) { + widget.data.groups.length = 0; + } + } + }); + // eslint-disable-next-line no-await-in-loop + await widgets.setArea(area); + } + } + }, +}; diff --git a/src/upgrades/1.11.1/remove_ignored_cids_per_user.js b/src/upgrades/1.11.1/remove_ignored_cids_per_user.js new file mode 100644 index 0000000000..6c89975656 --- /dev/null +++ b/src/upgrades/1.11.1/remove_ignored_cids_per_user.js @@ -0,0 +1,22 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); + +module.exports = { + name: 'Remove uid::ignored:cids', + timestamp: Date.UTC(2018, 11, 11), + method: function (callback) { + const { progress } = this; + + batch.processSortedSet('users:joindate', (uids, next) => { + progress.incr(uids.length); + const keys = uids.map(uid => `uid:${uid}:ignored:cids`); + db.deleteAll(keys, next); + }, { + progress: this.progress, + batch: 500, + }, callback); + }, +}; diff --git a/src/upgrades/1.12.0/category_watch_state.js b/src/upgrades/1.12.0/category_watch_state.js new file mode 100644 index 0000000000..76d36b6008 --- /dev/null +++ b/src/upgrades/1.12.0/category_watch_state.js @@ -0,0 +1,35 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); +const categories = require('../../categories'); + +module.exports = { + name: 'Update category watch data', + timestamp: Date.UTC(2018, 11, 13), + method: async function () { + const { progress } = this; + + const cids = await db.getSortedSetRange('categories:cid', 0, -1); + const keys = cids.map(cid => `cid:${cid}:ignorers`); + + await batch.processSortedSet('users:joindate', async (uids) => { + progress.incr(uids.length); + for (const cid of cids) { + const isMembers = await db.isSortedSetMembers(`cid:${cid}:ignorers`, uids); + uids = uids.filter((uid, index) => isMembers[index]); + if (uids.length) { + const states = uids.map(() => categories.watchStates.ignoring); + await db.sortedSetAdd(`cid:${cid}:uid:watch:state`, states, uids); + } + } + }, { + progress: progress, + batch: 500, + }); + + await db.deleteAll(keys); + }, +}; diff --git a/src/upgrades/1.12.0/global_view_privileges.js b/src/upgrades/1.12.0/global_view_privileges.js new file mode 100644 index 0000000000..1dbdf4bbad --- /dev/null +++ b/src/upgrades/1.12.0/global_view_privileges.js @@ -0,0 +1,28 @@ +'use strict'; + +const async = require('async'); +const privileges = require('../../privileges'); + +module.exports = { + name: 'Global view privileges', + timestamp: Date.UTC(2019, 0, 5), + method: function (callback) { + const meta = require('../../meta'); + + const tasks = [ + async.apply(privileges.global.give, ['groups:view:users', 'groups:view:tags', 'groups:view:groups'], 'registered-users'), + ]; + + if (parseInt(meta.config.privateUserInfo, 10) !== 1) { + tasks.push(async.apply(privileges.global.give, ['groups:view:users', 'groups:view:groups'], 'guests')); + tasks.push(async.apply(privileges.global.give, ['groups:view:users', 'groups:view:groups'], 'spiders')); + } + + if (parseInt(meta.config.privateTagListing, 10) !== 1) { + tasks.push(async.apply(privileges.global.give, ['groups:view:tags'], 'guests')); + tasks.push(async.apply(privileges.global.give, ['groups:view:tags'], 'spiders')); + } + + async.series(tasks, callback); + }, +}; diff --git a/src/upgrades/1.12.0/group_create_privilege.js b/src/upgrades/1.12.0/group_create_privilege.js new file mode 100644 index 0000000000..5525772fc3 --- /dev/null +++ b/src/upgrades/1.12.0/group_create_privilege.js @@ -0,0 +1,16 @@ +'use strict'; + +const privileges = require('../../privileges'); + +module.exports = { + name: 'Group create global privilege', + timestamp: Date.UTC(2019, 0, 4), + method: function (callback) { + const meta = require('../../meta'); + if (parseInt(meta.config.allowGroupCreation, 10) === 1) { + privileges.global.give(['groups:group:create'], 'registered-users', callback); + } else { + setImmediate(callback); + } + }, +}; diff --git a/src/upgrades/1.12.1/clear_username_email_history.js b/src/upgrades/1.12.1/clear_username_email_history.js new file mode 100644 index 0000000000..08f989799d --- /dev/null +++ b/src/upgrades/1.12.1/clear_username_email_history.js @@ -0,0 +1,45 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); +const user = require('../../user'); + +module.exports = { + name: 'Delete username email history for deleted users', + timestamp: Date.UTC(2019, 2, 25), + method: function (callback) { + const { progress } = this; + let currentUid = 1; + db.getObjectField('global', 'nextUid', (err, nextUid) => { + if (err) { + return callback(err); + } + progress.total = nextUid; + async.whilst((next) => { + next(null, currentUid < nextUid); + }, + (next) => { + progress.incr(); + user.exists(currentUid, (err, exists) => { + if (err) { + return next(err); + } + if (exists) { + currentUid += 1; + return next(); + } + db.deleteAll([`user:${currentUid}:usernames`, `user:${currentUid}:emails`], (err) => { + if (err) { + return next(err); + } + currentUid += 1; + next(); + }); + }); + }, + (err) => { + callback(err); + }); + }); + }, +}; diff --git a/src/upgrades/1.12.1/moderation_notes_refactor.js b/src/upgrades/1.12.1/moderation_notes_refactor.js new file mode 100644 index 0000000000..9068b8fc07 --- /dev/null +++ b/src/upgrades/1.12.1/moderation_notes_refactor.js @@ -0,0 +1,35 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Update moderation notes to hashes', + timestamp: Date.UTC(2019, 3, 5), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (uids) => { + await Promise.all(uids.map(async (uid) => { + progress.incr(); + + const notes = await db.getSortedSetRevRange(`uid:${uid}:moderation:notes`, 0, -1); + for (const note of notes) { + const noteData = JSON.parse(note); + noteData.timestamp = noteData.timestamp || Date.now(); + await db.sortedSetRemove(`uid:${uid}:moderation:notes`, note); + await db.setObject(`uid:${uid}:moderation:note:${noteData.timestamp}`, { + uid: noteData.uid, + timestamp: noteData.timestamp, + note: noteData.note, + }); + await db.sortedSetAdd(`uid:${uid}:moderation:notes`, noteData.timestamp, noteData.timestamp); + } + })); + }, { + progress: this.progress, + }); + }, +}; diff --git a/src/upgrades/1.12.1/post_upload_sizes.js b/src/upgrades/1.12.1/post_upload_sizes.js new file mode 100644 index 0000000000..103e470b9b --- /dev/null +++ b/src/upgrades/1.12.1/post_upload_sizes.js @@ -0,0 +1,23 @@ +'use strict'; + +const batch = require('../../batch'); +const posts = require('../../posts'); +const db = require('../../database'); + +module.exports = { + name: 'Calculate image sizes of all uploaded images', + timestamp: Date.UTC(2019, 2, 16), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('posts:pid', async (pids) => { + const keys = pids.map(p => `post:${p}:uploads`); + const uploads = await db.getSortedSetRange(keys, 0, -1); + await posts.uploads.saveSize(uploads); + progress.incr(pids.length); + }, { + batch: 100, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.12.3/disable_plugin_metrics.js b/src/upgrades/1.12.3/disable_plugin_metrics.js new file mode 100644 index 0000000000..96df2bf5cd --- /dev/null +++ b/src/upgrades/1.12.3/disable_plugin_metrics.js @@ -0,0 +1,11 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Disable plugin metrics for existing installs', + timestamp: Date.UTC(2019, 4, 21), + method: async function (callback) { + db.setObjectField('config', 'submitPluginUsage', 0, callback); + }, +}; diff --git a/src/upgrades/1.12.3/give_mod_info_privilege.js b/src/upgrades/1.12.3/give_mod_info_privilege.js new file mode 100644 index 0000000000..37d7a89d3c --- /dev/null +++ b/src/upgrades/1.12.3/give_mod_info_privilege.js @@ -0,0 +1,27 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); +const privileges = require('../../privileges'); +const groups = require('../../groups'); + +module.exports = { + name: 'give mod info privilege', + timestamp: Date.UTC(2019, 9, 8), + method: async function () { + const cids = await db.getSortedSetRevRange('categories:cid', 0, -1); + for (const cid of cids) { + await givePrivsToModerators(cid, ''); + await givePrivsToModerators(cid, 'groups:'); + } + await privileges.global.give(['groups:view:users:info'], 'Global Moderators'); + + async function givePrivsToModerators(cid, groupPrefix) { + const members = await db.getSortedSetRevRange(`group:cid:${cid}:privileges:${groupPrefix}moderate:members`, 0, -1); + for (const member of members) { + await groups.join(['cid:0:privileges:view:users:info'], member); + } + } + }, +}; diff --git a/src/upgrades/1.12.3/give_mod_privileges.js b/src/upgrades/1.12.3/give_mod_privileges.js new file mode 100644 index 0000000000..0ef92700c3 --- /dev/null +++ b/src/upgrades/1.12.3/give_mod_privileges.js @@ -0,0 +1,63 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const privileges = require('../../privileges'); +const groups = require('../../groups'); +const db = require('../../database'); + +module.exports = { + name: 'Give mods explicit privileges', + timestamp: Date.UTC(2019, 4, 28), + method: async function () { + const defaultPrivileges = [ + 'find', + 'read', + 'topics:read', + 'topics:create', + 'topics:reply', + 'topics:tag', + 'posts:edit', + 'posts:history', + 'posts:delete', + 'posts:upvote', + 'posts:downvote', + 'topics:delete', + ]; + const modPrivileges = defaultPrivileges.concat([ + 'posts:view_deleted', + 'purge', + ]); + + const globalModPrivs = [ + 'groups:chat', + 'groups:upload:post:image', + 'groups:upload:post:file', + 'groups:signature', + 'groups:ban', + 'groups:search:content', + 'groups:search:users', + 'groups:search:tags', + 'groups:view:users', + 'groups:view:tags', + 'groups:view:groups', + 'groups:local:login', + ]; + + const cids = await db.getSortedSetRevRange('categories:cid', 0, -1); + for (const cid of cids) { + await givePrivsToModerators(cid, ''); + await givePrivsToModerators(cid, 'groups:'); + await privileges.categories.give(modPrivileges.map(p => `groups:${p}`), cid, ['Global Moderators']); + } + await privileges.global.give(globalModPrivs, 'Global Moderators'); + + async function givePrivsToModerators(cid, groupPrefix) { + const privGroups = modPrivileges.map(priv => `cid:${cid}:privileges:${groupPrefix}${priv}`); + const members = await db.getSortedSetRevRange(`group:cid:${cid}:privileges:${groupPrefix}moderate:members`, 0, -1); + for (const member of members) { + await groups.join(privGroups, member); + } + } + }, +}; diff --git a/src/upgrades/1.12.3/update_registration_type.js b/src/upgrades/1.12.3/update_registration_type.js new file mode 100644 index 0000000000..9b80546050 --- /dev/null +++ b/src/upgrades/1.12.3/update_registration_type.js @@ -0,0 +1,20 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Update registration type', + timestamp: Date.UTC(2019, 5, 4), + method: function (callback) { + const meta = require('../../meta'); + const registrationType = meta.config.registrationType || 'normal'; + if (registrationType === 'admin-approval' || registrationType === 'admin-approval-ip') { + db.setObject('config', { + registrationType: 'normal', + registrationApprovalType: registrationType, + }, callback); + } else { + setImmediate(callback); + } + }, +}; diff --git a/src/upgrades/1.12.3/user_pid_sets.js b/src/upgrades/1.12.3/user_pid_sets.js new file mode 100644 index 0000000000..6cab55a379 --- /dev/null +++ b/src/upgrades/1.12.3/user_pid_sets.js @@ -0,0 +1,35 @@ + +'use strict'; + + +const db = require('../../database'); +const batch = require('../../batch'); +const posts = require('../../posts'); +const topics = require('../../topics'); + +module.exports = { + name: 'Create zsets for user posts per category', + timestamp: Date.UTC(2019, 5, 23), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('posts:pid', async (pids) => { + progress.incr(pids.length); + const postData = await posts.getPostsFields(pids, ['pid', 'uid', 'tid', 'upvotes', 'downvotes', 'timestamp']); + const tids = postData.map(p => p.tid); + const topicData = await topics.getTopicsFields(tids, ['cid']); + const bulk = []; + postData.forEach((p, index) => { + if (p && p.uid && p.pid && p.tid && p.timestamp) { + bulk.push([`cid:${topicData[index].cid}:uid:${p.uid}:pids`, p.timestamp, p.pid]); + if (p.votes > 0) { + bulk.push([`cid:${topicData[index].cid}:uid:${p.uid}:pids:votes`, p.votes, p.pid]); + } + } + }); + await db.sortedSetAddBulk(bulk); + }, { + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.13.0/clean_flag_byCid.js b/src/upgrades/1.13.0/clean_flag_byCid.js new file mode 100644 index 0000000000..c4bb66d566 --- /dev/null +++ b/src/upgrades/1.13.0/clean_flag_byCid.js @@ -0,0 +1,27 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Clean flag byCid zsets', + timestamp: Date.UTC(2019, 8, 24), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('flags:datetime', async (flagIds) => { + progress.incr(flagIds.length); + const flagData = await db.getObjects(flagIds.map(id => `flag:${id}`)); + const bulkRemove = []; + for (const flagObj of flagData) { + if (flagObj && flagObj.type === 'user' && flagObj.targetId && flagObj.flagId) { + bulkRemove.push([`flags:byCid:${flagObj.targetId}`, flagObj.flagId]); + } + } + + await db.sortedSetRemoveBulk(bulkRemove); + }, { + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.13.0/clean_post_topic_hash.js b/src/upgrades/1.13.0/clean_post_topic_hash.js new file mode 100644 index 0000000000..61c67a3a48 --- /dev/null +++ b/src/upgrades/1.13.0/clean_post_topic_hash.js @@ -0,0 +1,95 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Clean up post hash data', + timestamp: Date.UTC(2019, 9, 7), + method: async function () { + const { progress } = this; + await cleanPost(progress); + await cleanTopic(progress); + }, +}; + +async function cleanPost(progress) { + await batch.processSortedSet('posts:pid', async (pids) => { + progress.incr(pids.length); + + const postData = await db.getObjects(pids.map(pid => `post:${pid}`)); + await Promise.all(postData.map(async (post) => { + if (!post) { + return; + } + const fieldsToDelete = []; + if (post.hasOwnProperty('editor') && post.editor === '') { + fieldsToDelete.push('editor'); + } + if (post.hasOwnProperty('deleted') && parseInt(post.deleted, 10) === 0) { + fieldsToDelete.push('deleted'); + } + if (post.hasOwnProperty('edited') && parseInt(post.edited, 10) === 0) { + fieldsToDelete.push('edited'); + } + + // cleanup legacy fields, these are not used anymore + const legacyFields = [ + 'show_banned', 'fav_star_class', 'relativeEditTime', + 'post_rep', 'relativeTime', 'fav_button_class', + 'edited-class', + ]; + legacyFields.forEach((field) => { + if (post.hasOwnProperty(field)) { + fieldsToDelete.push(field); + } + }); + + if (fieldsToDelete.length) { + await db.deleteObjectFields(`post:${post.pid}`, fieldsToDelete); + } + })); + }, { + batch: 500, + progress: progress, + }); +} + +async function cleanTopic(progress) { + await batch.processSortedSet('topics:tid', async (tids) => { + progress.incr(tids.length); + const topicData = await db.getObjects(tids.map(tid => `topic:${tid}`)); + await Promise.all(topicData.map(async (topic) => { + if (!topic) { + return; + } + const fieldsToDelete = []; + if (topic.hasOwnProperty('deleted') && parseInt(topic.deleted, 10) === 0) { + fieldsToDelete.push('deleted'); + } + if (topic.hasOwnProperty('pinned') && parseInt(topic.pinned, 10) === 0) { + fieldsToDelete.push('pinned'); + } + if (topic.hasOwnProperty('locked') && parseInt(topic.locked, 10) === 0) { + fieldsToDelete.push('locked'); + } + + // cleanup legacy fields, these are not used anymore + const legacyFields = [ + 'category_name', 'category_slug', + ]; + legacyFields.forEach((field) => { + if (topic.hasOwnProperty(field)) { + fieldsToDelete.push(field); + } + }); + + if (fieldsToDelete.length) { + await db.deleteObjectFields(`topic:${topic.tid}`, fieldsToDelete); + } + })); + }, { + batch: 500, + progress: progress, + }); +} diff --git a/src/upgrades/1.13.0/cleanup_old_notifications.js b/src/upgrades/1.13.0/cleanup_old_notifications.js new file mode 100644 index 0000000000..6521846be2 --- /dev/null +++ b/src/upgrades/1.13.0/cleanup_old_notifications.js @@ -0,0 +1,51 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); +const user = require('../../user'); + +module.exports = { + name: 'Clean up old notifications and hash data', + timestamp: Date.UTC(2019, 9, 7), + method: async function () { + const { progress } = this; + const week = 604800000; + const cutoffTime = Date.now() - week; + await batch.processSortedSet('users:joindate', async (uids) => { + progress.incr(uids.length); + await Promise.all([ + db.sortedSetsRemoveRangeByScore(uids.map(uid => `uid:${uid}:notifications:unread`), '-inf', cutoffTime), + db.sortedSetsRemoveRangeByScore(uids.map(uid => `uid:${uid}:notifications:read`), '-inf', cutoffTime), + ]); + const userData = await user.getUsersData(uids); + await Promise.all(userData.map(async (user) => { + if (!user) { + return; + } + const fields = []; + ['picture', 'fullname', 'location', 'birthday', 'website', 'signature', 'uploadedpicture'].forEach((field) => { + if (user[field] === '') { + fields.push(field); + } + }); + ['profileviews', 'reputation', 'postcount', 'topiccount', 'lastposttime', 'banned', 'followerCount', 'followingCount'].forEach((field) => { + if (user[field] === 0) { + fields.push(field); + } + }); + if (user['icon:text']) { + fields.push('icon:text'); + } + if (user['icon:bgColor']) { + fields.push('icon:bgColor'); + } + if (fields.length) { + await db.deleteObjectFields(`user:${user.uid}`, fields); + } + })); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.13.3/fix_users_sorted_sets.js b/src/upgrades/1.13.3/fix_users_sorted_sets.js new file mode 100644 index 0000000000..9d2cc22852 --- /dev/null +++ b/src/upgrades/1.13.3/fix_users_sorted_sets.js @@ -0,0 +1,62 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Fix user sorted sets', + timestamp: Date.UTC(2020, 4, 2), + method: async function () { + const { progress } = this; + const nextUid = await db.getObjectField('global', 'nextUid'); + const allUids = []; + for (let i = 1; i <= nextUid; i++) { + allUids.push(i); + } + + progress.total = nextUid; + let totalUserCount = 0; + + await db.delete('user:null'); + await db.sortedSetsRemove([ + 'users:joindate', + 'users:reputation', + 'users:postcount', + 'users:flags', + ], 'null'); + + await batch.processArray(allUids, async (uids) => { + progress.incr(uids.length); + const userData = await db.getObjects(uids.map(id => `user:${id}`)); + + await Promise.all(userData.map(async (userData, index) => { + if (!userData || !userData.uid) { + await db.sortedSetsRemove([ + 'users:joindate', + 'users:reputation', + 'users:postcount', + 'users:flags', + ], uids[index]); + if (userData && !userData.uid) { + await db.delete(`user:${uids[index]}`); + } + return; + } + totalUserCount += 1; + await db.sortedSetAddBulk([ + ['users:joindate', userData.joindate || Date.now(), uids[index]], + ['users:reputation', userData.reputation || 0, uids[index]], + ['users:postcount', userData.postcount || 0, uids[index]], + ]); + if (userData.hasOwnProperty('flags') && parseInt(userData.flags, 10) > 0) { + await db.sortedSetAdd('users:flags', userData.flags, uids[index]); + } + })); + }, { + progress: progress, + batch: 500, + }); + + await db.setObjectField('global', 'userCount', totalUserCount); + }, +}; diff --git a/src/upgrades/1.13.4/remove_allowFileUploads_priv.js b/src/upgrades/1.13.4/remove_allowFileUploads_priv.js new file mode 100644 index 0000000000..e4f1c16089 --- /dev/null +++ b/src/upgrades/1.13.4/remove_allowFileUploads_priv.js @@ -0,0 +1,22 @@ +'use strict'; + +const db = require('../../database'); +const privileges = require('../../privileges'); + +module.exports = { + name: 'Removing file upload privilege if file uploads were disabled (`allowFileUploads`)', + timestamp: Date.UTC(2020, 4, 21), + method: async () => { + const allowFileUploads = parseInt(await db.getObjectField('config', 'allowFileUploads'), 10); + if (allowFileUploads === 1) { + await db.deleteObjectField('config', 'allowFileUploads'); + return; + } + + // Remove `upload:post:file` privilege for all groups + await privileges.categories.rescind(['groups:upload:post:file'], 0, ['guests', 'registered-users', 'Global Moderators']); + + // Clean up the old option from the config hash + await db.deleteObjectField('config', 'allowFileUploads'); + }, +}; diff --git a/src/upgrades/1.14.0/fix_category_image_field.js b/src/upgrades/1.14.0/fix_category_image_field.js new file mode 100644 index 0000000000..879c123aa4 --- /dev/null +++ b/src/upgrades/1.14.0/fix_category_image_field.js @@ -0,0 +1,23 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Remove duplicate image field for categories', + timestamp: Date.UTC(2020, 5, 9), + method: async () => { + const batch = require('../../batch'); + await batch.processSortedSet('categories:cid', async (cids) => { + let categoryData = await db.getObjects(cids.map(c => `category:${c}`)); + categoryData = categoryData.filter(c => c && (c.image || c.backgroundImage)); + if (categoryData.length) { + await Promise.all(categoryData.map(async (data) => { + if (data.image && !data.backgroundImage) { + await db.setObjectField(`category:${data.cid}`, 'backgroundImage', data.image); + } + await db.deleteObjectField(`category:${data.cid}`, 'image', data.image); + })); + } + }, { batch: 500 }); + }, +}; diff --git a/src/upgrades/1.14.0/unescape_navigation_titles.js b/src/upgrades/1.14.0/unescape_navigation_titles.js new file mode 100644 index 0000000000..a26fc87541 --- /dev/null +++ b/src/upgrades/1.14.0/unescape_navigation_titles.js @@ -0,0 +1,32 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Unescape navigation titles', + timestamp: Date.UTC(2020, 5, 26), + method: async function () { + const data = await db.getSortedSetRangeWithScores('navigation:enabled', 0, -1); + const translator = require('../../translator'); + const order = []; + const items = []; + data.forEach((item) => { + const navItem = JSON.parse(item.value); + if (navItem.hasOwnProperty('title')) { + navItem.title = translator.unescape(navItem.title); + navItem.title = navItem.title.replace(/\/g, ''); + } + if (navItem.hasOwnProperty('text')) { + navItem.text = translator.unescape(navItem.text); + navItem.text = navItem.text.replace(/\/g, ''); + } + if (navItem.hasOwnProperty('route')) { + navItem.route = navItem.route.replace('/', '/'); + } + order.push(item.score); + items.push(JSON.stringify(navItem)); + }); + await db.delete('navigation:enabled'); + await db.sortedSetAdd('navigation:enabled', order, items); + }, +}; diff --git a/src/upgrades/1.14.1/readd_deleted_recent_topics.js b/src/upgrades/1.14.1/readd_deleted_recent_topics.js new file mode 100644 index 0000000000..91c2c3f329 --- /dev/null +++ b/src/upgrades/1.14.1/readd_deleted_recent_topics.js @@ -0,0 +1,56 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); + +module.exports = { + name: 'Re-add deleted topics to topics:recent', + timestamp: Date.UTC(2018, 9, 11), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('topics:tid', async (tids) => { + progress.incr(tids.length); + const topicData = await db.getObjectsFields( + tids.map(tid => `topic:${tid}`), + ['tid', 'lastposttime', 'viewcount', 'postcount', 'upvotes', 'downvotes'] + ); + if (!topicData.tid) { + return; + } + topicData.forEach((t) => { + if (t.hasOwnProperty('upvotes') && t.hasOwnProperty('downvotes')) { + t.votes = parseInt(t.upvotes, 10) - parseInt(t.downvotes, 10); + } + }); + + await db.sortedSetAdd( + 'topics:recent', + topicData.map(t => t.lastposttime || 0), + topicData.map(t => t.tid) + ); + + await db.sortedSetAdd( + 'topics:views', + topicData.map(t => t.viewcount || 0), + topicData.map(t => t.tid) + ); + + await db.sortedSetAdd( + 'topics:posts', + topicData.map(t => t.postcount || 0), + topicData.map(t => t.tid) + ); + + await db.sortedSetAdd( + 'topics:votes', + topicData.map(t => t.votes || 0), + topicData.map(t => t.tid) + ); + }, { + progress: progress, + batchSize: 500, + }); + }, +}; diff --git a/src/upgrades/1.15.0/add_target_uid_to_flags.js b/src/upgrades/1.15.0/add_target_uid_to_flags.js new file mode 100644 index 0000000000..a7789baf10 --- /dev/null +++ b/src/upgrades/1.15.0/add_target_uid_to_flags.js @@ -0,0 +1,37 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); +const posts = require('../../posts'); + +module.exports = { + name: 'Add target uid to flag objects', + timestamp: Date.UTC(2020, 7, 22), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('flags:datetime', async (flagIds) => { + progress.incr(flagIds.length); + const flagData = await db.getObjects(flagIds.map(id => `flag:${id}`)); + for (const flagObj of flagData) { + /* eslint-disable no-await-in-loop */ + if (flagObj) { + const { targetId } = flagObj; + if (targetId) { + if (flagObj.type === 'post') { + const targetUid = await posts.getPostField(targetId, 'uid'); + if (targetUid) { + await db.setObjectField(`flag:${flagObj.flagId}`, 'targetUid', targetUid); + } + } else if (flagObj.type === 'user') { + await db.setObjectField(`flag:${flagObj.flagId}`, 'targetUid', targetId); + } + } + } + } + }, { + progress: progress, + batch: 500, + }); + }, +}; diff --git a/src/upgrades/1.15.0/consolidate_flags.js b/src/upgrades/1.15.0/consolidate_flags.js new file mode 100644 index 0000000000..adf7cfa0ef --- /dev/null +++ b/src/upgrades/1.15.0/consolidate_flags.js @@ -0,0 +1,46 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); +const posts = require('../../posts'); +const user = require('../../user'); + +module.exports = { + name: 'Consolidate multiple flags reports, going forward', + timestamp: Date.UTC(2020, 6, 16), + method: async function () { + const { progress } = this; + + let flags = await db.getSortedSetRange('flags:datetime', 0, -1); + flags = flags.map(flagId => `flag:${flagId}`); + flags = await db.getObjectsFields(flags, ['flagId', 'type', 'targetId', 'uid', 'description', 'datetime']); + progress.total = flags.length; + + await batch.processArray(flags, async (subset) => { + progress.incr(subset.length); + + await Promise.all(subset.map(async (flagObj) => { + const methods = []; + switch (flagObj.type) { + case 'post': + methods.push(posts.setPostField.bind(posts, flagObj.targetId, 'flagId', flagObj.flagId)); + break; + + case 'user': + methods.push(user.setUserField.bind(user, flagObj.targetId, 'flagId', flagObj.flagId)); + break; + } + + methods.push( + db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reports`, flagObj.datetime, String(flagObj.description).slice(0, 250)), + db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reporters`, flagObj.datetime, flagObj.uid) + ); + + await Promise.all(methods.map(async method => method())); + })); + }, { + progress: progress, + batch: 500, + }); + }, +}; diff --git a/src/upgrades/1.15.0/disable_sounds_plugin.js b/src/upgrades/1.15.0/disable_sounds_plugin.js new file mode 100644 index 0000000000..fde7963301 --- /dev/null +++ b/src/upgrades/1.15.0/disable_sounds_plugin.js @@ -0,0 +1,11 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Disable nodebb-plugin-soundpack-default', + timestamp: Date.UTC(2020, 8, 6), + method: async function () { + await db.sortedSetRemove('plugins:active', 'nodebb-plugin-soundpack-default'); + }, +}; diff --git a/src/upgrades/1.15.0/fix_category_colors.js b/src/upgrades/1.15.0/fix_category_colors.js new file mode 100644 index 0000000000..f8cfef44f9 --- /dev/null +++ b/src/upgrades/1.15.0/fix_category_colors.js @@ -0,0 +1,21 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Fix category colors that are 3 digit hex colors', + timestamp: Date.UTC(2020, 9, 11), + method: async () => { + const batch = require('../../batch'); + await batch.processSortedSet('categories:cid', async (cids) => { + let categoryData = await db.getObjects(cids.map(c => `category:${c}`)); + categoryData = categoryData.filter(c => c && (c.color === '#fff' || c.color === '#333' || String(c.color).length !== 7)); + if (categoryData.length) { + await Promise.all(categoryData.map(async (data) => { + const color = `#${new Array(6).fill((data.color && data.color[1]) || 'f').join('')}`; + await db.setObjectField(`category:${data.cid}`, 'color', color); + })); + } + }, { batch: 500 }); + }, +}; diff --git a/src/upgrades/1.15.0/fullname_search_set.js b/src/upgrades/1.15.0/fullname_search_set.js new file mode 100644 index 0000000000..a398d46a52 --- /dev/null +++ b/src/upgrades/1.15.0/fullname_search_set.js @@ -0,0 +1,26 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); +const user = require('../../user'); + +module.exports = { + name: 'Create fullname search set', + timestamp: Date.UTC(2020, 8, 11), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (uids) => { + progress.incr(uids.length); + const userData = await user.getUsersFields(uids, ['uid', 'fullname']); + const bulkAdd = userData + .filter(u => u.uid && u.fullname) + .map(u => ['fullname:sorted', 0, `${String(u.fullname).slice(0, 255).toLowerCase()}:${u.uid}`]); + await db.sortedSetAddBulk(bulkAdd); + }, { + batch: 500, + progress: this.progress, + }); + }, +}; diff --git a/src/upgrades/1.15.0/remove_allow_from_uri.js b/src/upgrades/1.15.0/remove_allow_from_uri.js new file mode 100644 index 0000000000..24a9dd434b --- /dev/null +++ b/src/upgrades/1.15.0/remove_allow_from_uri.js @@ -0,0 +1,15 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Remove allow from uri setting', + timestamp: Date.UTC(2020, 8, 6), + method: async function () { + const meta = require('../../meta'); + if (meta.config['allow-from-uri']) { + await db.setObjectField('config', 'csp-frame-ancestors', meta.config['allow-from-uri']); + } + await db.deleteObjectField('config', 'allow-from-uri'); + }, +}; diff --git a/src/upgrades/1.15.0/remove_flag_reporters_zset.js b/src/upgrades/1.15.0/remove_flag_reporters_zset.js new file mode 100644 index 0000000000..b64a84bf4e --- /dev/null +++ b/src/upgrades/1.15.0/remove_flag_reporters_zset.js @@ -0,0 +1,33 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Remove flag reporters sorted set', + timestamp: Date.UTC(2020, 6, 31), + method: async function () { + const { progress } = this; + progress.total = await db.sortedSetCard('flags:datetime'); + + await batch.processSortedSet('flags:datetime', async (flagIds) => { + await Promise.all(flagIds.map(async (flagId) => { + const [reports, reporterUids] = await Promise.all([ + db.getSortedSetRevRangeWithScores(`flag:${flagId}:reports`, 0, -1), + db.getSortedSetRevRange(`flag:${flagId}:reporters`, 0, -1), + ]); + + const values = reports.reduce((memo, cur, idx) => { + memo.push([`flag:${flagId}:reports`, cur.score, [(reporterUids[idx] || 0), cur.value].join(';')]); + return memo; + }, []); + + await db.delete(`flag:${flagId}:reports`); + await db.sortedSetAddBulk(values); + })); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.15.0/topic_poster_count.js b/src/upgrades/1.15.0/topic_poster_count.js new file mode 100644 index 0000000000..55834cfb77 --- /dev/null +++ b/src/upgrades/1.15.0/topic_poster_count.js @@ -0,0 +1,30 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); + +module.exports = { + name: 'Store poster count in topic hash', + timestamp: Date.UTC(2020, 9, 24), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('topics:tid', async (tids) => { + progress.incr(tids.length); + const keys = tids.map(tid => `tid:${tid}:posters`); + await db.sortedSetsRemoveRangeByScore(keys, '-inf', 0); + const counts = await db.sortedSetsCard(keys); + const bulkSet = []; + for (let i = 0; i < tids.length; i++) { + if (counts[i] > 0) { + bulkSet.push([`topic:${tids[i]}`, { postercount: counts[i] }]); + } + } + await db.setObjectBulk(bulkSet); + }, { + progress: progress, + batchSize: 500, + }); + }, +}; diff --git a/src/upgrades/1.15.0/track_flags_by_target.js b/src/upgrades/1.15.0/track_flags_by_target.js new file mode 100644 index 0000000000..952dae5a4f --- /dev/null +++ b/src/upgrades/1.15.0/track_flags_by_target.js @@ -0,0 +1,15 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'New sorted set for tracking flags by target', + timestamp: Date.UTC(2020, 6, 15), + method: async () => { + const flags = await db.getSortedSetRange('flags:hash', 0, -1); + await Promise.all(flags.map(async (flag) => { + flag = flag.split(':').slice(0, 2); + await db.sortedSetIncrBy('flags:byTarget', 1, flag.join(':')); + })); + }, +}; diff --git a/src/upgrades/1.15.0/verified_users_group.js b/src/upgrades/1.15.0/verified_users_group.js new file mode 100644 index 0000000000..b5eb6e81f0 --- /dev/null +++ b/src/upgrades/1.15.0/verified_users_group.js @@ -0,0 +1,110 @@ +'use strict'; + +const db = require('../../database'); + +const batch = require('../../batch'); +const user = require('../../user'); +const groups = require('../../groups'); +const meta = require('../../meta'); +const privileges = require('../../privileges'); + +const now = Date.now(); +module.exports = { + name: 'Create verified/unverified user groups', + timestamp: Date.UTC(2020, 9, 13), + method: async function () { + const { progress } = this; + + const maxGroupLength = meta.config.maximumGroupNameLength; + meta.config.maximumGroupNameLength = 30; + const timestamp = await db.getObjectField('group:administrators', 'timestamp'); + const verifiedExists = await groups.exists('verified-users'); + if (!verifiedExists) { + await groups.create({ + name: 'verified-users', + hidden: 1, + private: 1, + system: 1, + disableLeave: 1, + disableJoinRequests: 1, + timestamp: timestamp + 1, + }); + } + const unverifiedExists = await groups.exists('unverified-users'); + if (!unverifiedExists) { + await groups.create({ + name: 'unverified-users', + hidden: 1, + private: 1, + system: 1, + disableLeave: 1, + disableJoinRequests: 1, + timestamp: timestamp + 1, + }); + } + // restore setting + meta.config.maximumGroupNameLength = maxGroupLength; + await batch.processSortedSet('users:joindate', async (uids) => { + progress.incr(uids.length); + const userData = await user.getUsersFields(uids, ['uid', 'email:confirmed']); + + const verified = userData.filter(u => parseInt(u['email:confirmed'], 10) === 1); + const unverified = userData.filter(u => parseInt(u['email:confirmed'], 10) !== 1); + + await db.sortedSetAdd( + 'group:verified-users:members', + verified.map(() => now), + verified.map(u => u.uid) + ); + + await db.sortedSetAdd( + 'group:unverified-users:members', + unverified.map(() => now), + unverified.map(u => u.uid) + ); + }, { + batch: 500, + progress: this.progress, + }); + + await db.delete('users:notvalidated'); + await updatePrivilges(); + + const verifiedCount = await db.sortedSetCard('group:verified-users:members'); + const unverifiedCount = await db.sortedSetCard('group:unverified-users:members'); + await db.setObjectField('group:verified-users', 'memberCount', verifiedCount); + await db.setObjectField('group:unverified-users', 'memberCount', unverifiedCount); + }, +}; + +async function updatePrivilges() { + // if email confirmation is required + // give chat, posting privs to "verified-users" group + // remove chat, posting privs from "registered-users" group + + // This config property has been removed from v1.18.0+, but is still present in old datasets + if (meta.config.requireEmailConfirmation) { + const cids = await db.getSortedSetRevRange('categories:cid', 0, -1); + const canChat = await privileges.global.canGroup('chat', 'registered-users'); + if (canChat) { + await privileges.global.give(['groups:chat'], 'verified-users'); + await privileges.global.rescind(['groups:chat'], 'registered-users'); + } + for (const cid of cids) { + /* eslint-disable no-await-in-loop */ + const data = await privileges.categories.list(cid); + + const registeredUsersPrivs = data.groups.find(d => d.name === 'registered-users').privileges; + + if (registeredUsersPrivs['groups:topics:create']) { + await privileges.categories.give(['groups:topics:create'], cid, 'verified-users'); + await privileges.categories.rescind(['groups:topics:create'], cid, 'registered-users'); + } + + if (registeredUsersPrivs['groups:topics:reply']) { + await privileges.categories.give(['groups:topics:reply'], cid, 'verified-users'); + await privileges.categories.rescind(['groups:topics:reply'], cid, 'registered-users'); + } + } + } +} diff --git a/src/upgrades/1.15.4/clear_purged_replies.js b/src/upgrades/1.15.4/clear_purged_replies.js new file mode 100644 index 0000000000..c039494af9 --- /dev/null +++ b/src/upgrades/1.15.4/clear_purged_replies.js @@ -0,0 +1,33 @@ +'use strict'; + +const _ = require('lodash'); +const db = require('../../database'); + +const batch = require('../../batch'); + +module.exports = { + name: 'Clear purged replies and toPid', + timestamp: Date.UTC(2020, 10, 26), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('posts:pid', async (pids) => { + progress.incr(pids.length); + let postData = await db.getObjects(pids.map(pid => `post:${pid}`)); + postData = postData.filter(p => p && parseInt(p.toPid, 10)); + if (!postData.length) { + return; + } + const toPids = postData.map(p => p.toPid); + const exists = await db.exists(toPids.map(pid => `post:${pid}`)); + const pidsToDelete = postData.filter((p, index) => !exists[index]).map(p => p.pid); + await db.deleteObjectFields(pidsToDelete.map(pid => `post:${pid}`), ['toPid']); + + const repliesToDelete = _.uniq(toPids.filter((pid, index) => !exists[index])); + await db.deleteAll(repliesToDelete.map(pid => `pid:${pid}:replies`)); + }, { + progress: progress, + batchSize: 500, + }); + }, +}; diff --git a/src/upgrades/1.16.0/category_tags.js b/src/upgrades/1.16.0/category_tags.js new file mode 100644 index 0000000000..52de5ffe23 --- /dev/null +++ b/src/upgrades/1.16.0/category_tags.js @@ -0,0 +1,46 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); +const batch = require('../../batch'); +const topics = require('../../topics'); + +module.exports = { + name: 'Create category tags sorted sets', + timestamp: Date.UTC(2020, 10, 23), + method: async function () { + const { progress } = this; + + async function getTopicsTags(tids) { + return await db.getSetsMembers( + tids.map(tid => `topic:${tid}:tags`), + ); + } + + await batch.processSortedSet('topics:tid', async (tids) => { + const [topicData, tags] = await Promise.all([ + topics.getTopicsFields(tids, ['tid', 'cid', 'timestamp']), + getTopicsTags(tids), + ]); + const topicsWithTags = topicData.map((t, i) => { + t.tags = tags[i]; + return t; + }).filter(t => t && t.tags.length); + + await async.eachSeries(topicsWithTags, async (topicObj) => { + const { cid, tags } = topicObj; + await db.sortedSetsAdd( + tags.map(tag => `cid:${cid}:tag:${tag}:topics`), + topicObj.timestamp, + topicObj.tid + ); + const counts = await db.sortedSetsCard(tags.map(tag => `cid:${cid}:tag:${tag}:topics`)); + await db.sortedSetAdd(`cid:${cid}:tags`, counts, tags); + }); + progress.incr(tids.length); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.16.0/migrate_thumbs.js b/src/upgrades/1.16.0/migrate_thumbs.js new file mode 100644 index 0000000000..5125f34906 --- /dev/null +++ b/src/upgrades/1.16.0/migrate_thumbs.js @@ -0,0 +1,42 @@ +'use strict'; + +const nconf = require('nconf'); + +const db = require('../../database'); +const meta = require('../../meta'); +const topics = require('../../topics'); +const batch = require('../../batch'); + +module.exports = { + name: 'Migrate existing topic thumbnails to new format', + timestamp: Date.UTC(2020, 11, 11), + method: async function () { + const { progress } = this; + const current = await meta.configs.get('topicThumbSize'); + + if (parseInt(current, 10) === 120) { + await meta.configs.set('topicThumbSize', 512); + } + + await batch.processSortedSet('topics:tid', async (tids) => { + const keys = tids.map(tid => `topic:${tid}`); + const topicThumbs = (await db.getObjectsFields(keys, ['thumb'])) + .map(obj => (obj.thumb ? obj.thumb.replace(nconf.get('upload_url'), '') : null)); + + await Promise.all(tids.map(async (tid, idx) => { + const path = topicThumbs[idx]; + if (path) { + if (path.length < 255 && !path.startsWith('data:')) { + await topics.thumbs.associate({ id: tid, path }); + } + await db.deleteObjectField(keys[idx], 'thumb'); + } + + progress.incr(); + })); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.17.0/banned_users_group.js b/src/upgrades/1.17.0/banned_users_group.js new file mode 100644 index 0000000000..a5b931c4bc --- /dev/null +++ b/src/upgrades/1.17.0/banned_users_group.js @@ -0,0 +1,63 @@ +'use strict'; + +const batch = require('../../batch'); +const db = require('../../database'); +const groups = require('../../groups'); + +const now = Date.now(); + +module.exports = { + name: 'Move banned users to banned-users group', + timestamp: Date.UTC(2020, 11, 13), + method: async function () { + const { progress } = this; + const timestamp = await db.getObjectField('group:administrators', 'timestamp'); + const bannedExists = await groups.exists('banned-users'); + if (!bannedExists) { + await groups.create({ + name: 'banned-users', + hidden: 1, + private: 1, + system: 1, + disableLeave: 1, + disableJoinRequests: 1, + timestamp: timestamp + 1, + }); + } + + await batch.processSortedSet('users:banned', async (uids) => { + progress.incr(uids.length); + + await db.sortedSetAdd( + 'group:banned-users:members', + uids.map(() => now), + uids + ); + + await db.sortedSetRemove( + [ + 'group:registered-users:members', + 'group:verified-users:members', + 'group:unverified-users:members', + 'group:Global Moderators:members', + ], + uids + ); + }, { + batch: 500, + progress: this.progress, + }); + + + const bannedCount = await db.sortedSetCard('group:banned-users:members'); + const registeredCount = await db.sortedSetCard('group:registered-users:members'); + const verifiedCount = await db.sortedSetCard('group:verified-users:members'); + const unverifiedCount = await db.sortedSetCard('group:unverified-users:members'); + const globalModCount = await db.sortedSetCard('group:Global Moderators:members'); + await db.setObjectField('group:banned-users', 'memberCount', bannedCount); + await db.setObjectField('group:registered-users', 'memberCount', registeredCount); + await db.setObjectField('group:verified-users', 'memberCount', verifiedCount); + await db.setObjectField('group:unverified-users', 'memberCount', unverifiedCount); + await db.setObjectField('group:Global Moderators', 'memberCount', globalModCount); + }, +}; diff --git a/src/upgrades/1.17.0/category_name_zset.js b/src/upgrades/1.17.0/category_name_zset.js new file mode 100644 index 0000000000..c5398dba3a --- /dev/null +++ b/src/upgrades/1.17.0/category_name_zset.js @@ -0,0 +1,28 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Create category name sorted set', + timestamp: Date.UTC(2021, 0, 27), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('categories:cid', async (cids) => { + const keys = cids.map(cid => `category:${cid}`); + let categoryData = await db.getObjectsFields(keys, ['cid', 'name']); + categoryData = categoryData.filter(c => c.cid && c.name); + const bulkAdd = categoryData.map(cat => [ + 'categories:name', + 0, + `${String(cat.name).slice(0, 200).toLowerCase()}:${cat.cid}`, + ]); + await db.sortedSetAddBulk(bulkAdd); + progress.incr(cids.length); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.17.0/default_favicon.js b/src/upgrades/1.17.0/default_favicon.js new file mode 100644 index 0000000000..057d99f741 --- /dev/null +++ b/src/upgrades/1.17.0/default_favicon.js @@ -0,0 +1,20 @@ +'use strict'; + +const nconf = require('nconf'); +const path = require('path'); +const fs = require('fs'); +const file = require('../../file'); + +module.exports = { + name: 'Store default favicon if it does not exist', + timestamp: Date.UTC(2021, 2, 9), + method: async function () { + const pathToIco = path.join(nconf.get('upload_path'), 'system', 'favicon.ico'); + const defaultIco = path.join(nconf.get('base_dir'), 'public', 'favicon.ico'); + const targetExists = await file.exists(pathToIco); + const defaultExists = await file.exists(defaultIco); + if (defaultExists && !targetExists) { + await fs.promises.copyFile(defaultIco, pathToIco); + } + }, +}; diff --git a/src/upgrades/1.17.0/schedule_privilege_for_existing_categories.js b/src/upgrades/1.17.0/schedule_privilege_for_existing_categories.js new file mode 100644 index 0000000000..0d1a3ef50e --- /dev/null +++ b/src/upgrades/1.17.0/schedule_privilege_for_existing_categories.js @@ -0,0 +1,18 @@ +'use strict'; + +const db = require('../../database'); +const privileges = require('../../privileges'); + +module.exports = { + name: 'Add "schedule" to default privileges of admins and gmods for existing categories', + timestamp: Date.UTC(2021, 2, 11), + method: async () => { + const privilegeToGive = ['groups:topics:schedule']; + + const cids = await db.getSortedSetRevRange('categories:cid', 0, -1); + for (const cid of cids) { + /* eslint-disable no-await-in-loop */ + await privileges.categories.give(privilegeToGive, cid, ['administrators', 'Global Moderators']); + } + }, +}; diff --git a/src/upgrades/1.17.0/subcategories_per_page.js b/src/upgrades/1.17.0/subcategories_per_page.js new file mode 100644 index 0000000000..5fb4acfd6d --- /dev/null +++ b/src/upgrades/1.17.0/subcategories_per_page.js @@ -0,0 +1,23 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Create subCategoriesPerPage property for categories', + timestamp: Date.UTC(2021, 0, 31), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('categories:cid', async (cids) => { + const keys = cids.map(cid => `category:${cid}`); + await db.setObject(keys, { + subCategoriesPerPage: 10, + }); + progress.incr(cids.length); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.17.0/topic_thumb_count.js b/src/upgrades/1.17.0/topic_thumb_count.js new file mode 100644 index 0000000000..b3366e65ab --- /dev/null +++ b/src/upgrades/1.17.0/topic_thumb_count.js @@ -0,0 +1,28 @@ +'use strict'; + +const _ = require('lodash'); +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Store number of thumbs a topic has in the topic object', + timestamp: Date.UTC(2021, 1, 7), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('topics:tid', async (tids) => { + const keys = tids.map(tid => `topic:${tid}:thumbs`); + const counts = await db.sortedSetsCard(keys); + const tidToCount = _.zipObject(tids, counts); + const tidsWithThumbs = tids.filter((t, i) => counts[i] > 0); + await db.setObjectBulk( + tidsWithThumbs.map(tid => [`topic:${tid}`, { numThumbs: tidToCount[tid] }]), + ); + + progress.incr(tids.length); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.18.0/enable_include_unverified_emails.js b/src/upgrades/1.18.0/enable_include_unverified_emails.js new file mode 100644 index 0000000000..060e8861cf --- /dev/null +++ b/src/upgrades/1.18.0/enable_include_unverified_emails.js @@ -0,0 +1,12 @@ +'use strict'; + +const meta = require('../../meta'); + +module.exports = { + name: 'Enable setting to include unverified emails for all mailings', + // remember, month is zero-indexed (so January is 0, December is 11) + timestamp: Date.UTC(2021, 5, 18), + method: async () => { + await meta.configs.set('includeUnverifiedEmails', 1); + }, +}; diff --git a/src/upgrades/1.18.0/topic_tags_refactor.js b/src/upgrades/1.18.0/topic_tags_refactor.js new file mode 100644 index 0000000000..b1425f5b06 --- /dev/null +++ b/src/upgrades/1.18.0/topic_tags_refactor.js @@ -0,0 +1,37 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Store tags in topic hash', + timestamp: Date.UTC(2021, 8, 9), + method: async function () { + const { progress } = this; + + async function getTopicsTags(tids) { + return await db.getSetsMembers( + tids.map(tid => `topic:${tid}:tags`), + ); + } + + await batch.processSortedSet('topics:tid', async (tids) => { + const tags = await getTopicsTags(tids); + + const topicsWithTags = tids.map((tid, i) => { + const topic = { tid: tid }; + topic.tags = tags[i]; + return topic; + }).filter(t => t && t.tags.length); + + await db.setObjectBulk( + topicsWithTags.map(t => [`topic:${t.tid}`, { tags: t.tags.join(',') }]), + ); + await db.deleteAll(tids.map(tid => `topic:${tid}:tags`)); + progress.incr(tids.length); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.18.4/category_topics_views.js b/src/upgrades/1.18.4/category_topics_views.js new file mode 100644 index 0000000000..f5601a77d6 --- /dev/null +++ b/src/upgrades/1.18.4/category_topics_views.js @@ -0,0 +1,23 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); +const topics = require('../../topics'); + +module.exports = { + name: 'Category topics sorted sets by views', + timestamp: Date.UTC(2021, 8, 28), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('topics:tid', async (tids) => { + let topicData = await topics.getTopicsData(tids); + topicData = topicData.filter(t => t && t.cid); + await db.sortedSetAddBulk(topicData.map(t => ([`cid:${t.cid}:tids:views`, t.viewcount || 0, t.tid]))); + progress.incr(tids.length); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.19.0/navigation-enabled-hashes.js b/src/upgrades/1.19.0/navigation-enabled-hashes.js new file mode 100644 index 0000000000..3700986a3d --- /dev/null +++ b/src/upgrades/1.19.0/navigation-enabled-hashes.js @@ -0,0 +1,31 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Upgrade navigation items to hashes', + timestamp: Date.UTC(2021, 11, 13), + method: async function () { + const data = await db.getSortedSetRangeWithScores('navigation:enabled', 0, -1); + const order = []; + const bulkSet = []; + + data.forEach((item) => { + const navItem = JSON.parse(item.value); + if (navItem.hasOwnProperty('properties') && navItem.properties) { + if (navItem.properties.hasOwnProperty('targetBlank')) { + navItem.targetBlank = navItem.properties.targetBlank; + } + delete navItem.properties; + } + if (navItem.hasOwnProperty('groups') && (Array.isArray(navItem.groups) || typeof navItem.groups === 'string')) { + navItem.groups = JSON.stringify(navItem.groups); + } + bulkSet.push([`navigation:enabled:${item.score}`, navItem]); + order.push(item.score); + }); + await db.setObjectBulk(bulkSet); + await db.delete('navigation:enabled'); + await db.sortedSetAdd('navigation:enabled', order, order); + }, +}; diff --git a/src/upgrades/1.19.0/reenable-username-login.js b/src/upgrades/1.19.0/reenable-username-login.js new file mode 100644 index 0000000000..a3bed38d07 --- /dev/null +++ b/src/upgrades/1.19.0/reenable-username-login.js @@ -0,0 +1,15 @@ +'use strict'; + +const meta = require('../../meta'); + +module.exports = { + name: 'Re-enable username login', + timestamp: Date.UTC(2021, 10, 23), + method: async () => { + const setting = await meta.config.allowLoginWith; + + if (setting === 'email') { + await meta.configs.set('allowLoginWith', 'username-email'); + } + }, +}; diff --git a/src/upgrades/1.19.2/remove_leftover_thumbs_after_topic_purge.js b/src/upgrades/1.19.2/remove_leftover_thumbs_after_topic_purge.js new file mode 100644 index 0000000000..da7de18c32 --- /dev/null +++ b/src/upgrades/1.19.2/remove_leftover_thumbs_after_topic_purge.js @@ -0,0 +1,51 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs').promises; +const nconf = require('nconf'); + +const db = require('../../database'); +const batch = require('../../batch'); +const file = require('../../file'); + +module.exports = { + name: 'Clean up leftover topic thumb sorted sets and files for since-purged topics', + timestamp: Date.UTC(2022, 1, 7), + method: async function () { + const { progress } = this; + const nextTid = await db.getObjectField('global', 'nextTid'); + const tids = []; + for (let x = 1; x < nextTid; x++) { + tids.push(x); + } + + const purgedTids = (await db.isSortedSetMembers('topics:tid', tids)) + .map((exists, idx) => (exists ? false : tids[idx])) + .filter(Boolean); + + const affectedTids = (await db.exists(purgedTids.map(tid => `topic:${tid}:thumbs`))) + .map((exists, idx) => (exists ? purgedTids[idx] : false)) + .filter(Boolean); + + progress.total = affectedTids.length; + + await batch.processArray(affectedTids, async (tids) => { + await Promise.all(tids.map(async (tid) => { + const relativePaths = await db.getSortedSetMembers(`topic:${tid}:thumbs`); + const absolutePaths = relativePaths.map(relativePath => path.join(nconf.get('upload_path'), relativePath)); + + await Promise.all(absolutePaths.map(async (absolutePath) => { + const exists = await file.exists(absolutePath); + if (exists) { + await fs.unlink(absolutePath); + } + })); + await db.delete(`topic:${tid}:thumbs`); + progress.incr(); + })); + }, { + progress, + batch: 100, + }); + }, +}; diff --git a/src/upgrades/1.19.2/store_downvoted_posts_in_zset.js b/src/upgrades/1.19.2/store_downvoted_posts_in_zset.js new file mode 100644 index 0000000000..31830dc301 --- /dev/null +++ b/src/upgrades/1.19.2/store_downvoted_posts_in_zset.js @@ -0,0 +1,31 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Store downvoted posts in user votes sorted set', + timestamp: Date.UTC(2022, 1, 4), + method: async function () { + const batch = require('../../batch'); + const posts = require('../../posts'); + const { progress } = this; + + await batch.processSortedSet('posts:pid', async (pids) => { + const postData = await posts.getPostsFields(pids, ['pid', 'uid', 'upvotes', 'downvotes']); + const cids = await posts.getCidsByPids(pids); + + const bulkAdd = []; + postData.forEach((post, index) => { + if (post.votes > 0 || post.votes < 0) { + const cid = cids[index]; + bulkAdd.push([`cid:${cid}:uid:${post.uid}:pids:votes`, post.votes, post.pid]); + } + }); + await db.sortedSetAddBulk(bulkAdd); + progress.incr(postData.length); + }, { + progress, + batch: 500, + }); + }, +}; diff --git a/src/upgrades/1.19.3/fix_user_uploads_zset.js b/src/upgrades/1.19.3/fix_user_uploads_zset.js new file mode 100644 index 0000000000..109df3a331 --- /dev/null +++ b/src/upgrades/1.19.3/fix_user_uploads_zset.js @@ -0,0 +1,43 @@ +'use strict'; + +const crypto = require('crypto'); + +const db = require('../../database'); +const batch = require('../../batch'); + +const md5 = filename => crypto.createHash('md5').update(filename).digest('hex'); + +module.exports = { + name: 'Fix paths in user uploads sorted sets', + timestamp: Date.UTC(2022, 1, 10), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (uids) => { + progress.incr(uids.length); + + await Promise.all(uids.map(async (uid) => { + const key = `uid:${uid}:uploads`; + // Rename the paths within + let uploads = await db.getSortedSetRangeWithScores(key, 0, -1); + if (uploads.length) { + // Don't process those that have already the right format + uploads = uploads.filter(upload => upload.value.startsWith('/files/')); + + await db.sortedSetRemove(key, uploads.map(upload => upload.value)); + await db.sortedSetAdd( + key, + uploads.map(upload => upload.score), + uploads.map(upload => upload.value.slice(1)) + ); + // Add uid to the upload's hash object + uploads = await db.getSortedSetMembers(key); + await db.setObjectBulk(uploads.map(relativePath => [`upload:${md5(relativePath)}`, { uid: uid }])); + } + })); + }, { + batch: 500, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.19.3/rename_post_upload_hashes.js b/src/upgrades/1.19.3/rename_post_upload_hashes.js new file mode 100644 index 0000000000..6287c436ca --- /dev/null +++ b/src/upgrades/1.19.3/rename_post_upload_hashes.js @@ -0,0 +1,63 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const crypto = require('crypto'); + +const db = require('../../database'); +const batch = require('../../batch'); + +const md5 = filename => crypto.createHash('md5').update(filename).digest('hex'); + +module.exports = { + name: 'Rename object and sorted sets used in post uploads', + timestamp: Date.UTC(2022, 1, 10), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('posts:pid', async (pids) => { + let keys = pids.map(pid => `post:${pid}:uploads`); + const exists = await db.exists(keys); + keys = keys.filter((key, idx) => exists[idx]); + + progress.incr(pids.length); + + for (const key of keys) { + // Rename the paths within + let uploads = await db.getSortedSetRangeWithScores(key, 0, -1); + + // Don't process those that have already the right format + uploads = uploads.filter(upload => upload && upload.value && !upload.value.startsWith('files/')); + + // Rename the zset members + await db.sortedSetRemove(key, uploads.map(upload => upload.value)); + await db.sortedSetAdd( + key, + uploads.map(upload => upload.score), + uploads.map(upload => `files/${upload.value}`) + ); + + // Rename the object and pids zsets + const hashes = uploads.map(upload => md5(upload.value)); + const newHashes = uploads.map(upload => md5(`files/${upload.value}`)); + + // cant use db.rename since `fix_user_uploads_zset.js` upgrade script already creates + // `upload:md5(upload.value) hash, trying to rename to existing key results in dupe error + const oldData = await db.getObjects(hashes.map(hash => `upload:${hash}`)); + const bulkSet = []; + oldData.forEach((data, idx) => { + if (data) { + bulkSet.push([`upload:${newHashes[idx]}`, data]); + } + }); + await db.setObjectBulk(bulkSet); + await db.deleteAll(hashes.map(hash => `upload:${hash}`)); + + await Promise.all(hashes.map((hash, idx) => db.rename(`upload:${hash}:pids`, `upload:${newHashes[idx]}:pids`))); + } + }, { + batch: 100, + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.2.0/category_recent_tids.js b/src/upgrades/1.2.0/category_recent_tids.js new file mode 100644 index 0000000000..4a75746fd5 --- /dev/null +++ b/src/upgrades/1.2.0/category_recent_tids.js @@ -0,0 +1,31 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); + + +module.exports = { + name: 'Category recent tids', + timestamp: Date.UTC(2016, 8, 22), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, (err, cids) => { + if (err) { + return callback(err); + } + + async.eachSeries(cids, (cid, next) => { + db.getSortedSetRevRange(`cid:${cid}:pids`, 0, 0, (err, pid) => { + if (err || !pid) { + return next(err); + } + db.getObjectFields(`post:${pid}`, ['tid', 'timestamp'], (err, postData) => { + if (err || !postData || !postData.tid) { + return next(err); + } + db.sortedSetAdd(`cid:${cid}:recent_tids`, postData.timestamp, postData.tid, next); + }); + }); + }, callback); + }); + }, +}; diff --git a/src/upgrades/1.2.0/edit_delete_deletetopic_privileges.js b/src/upgrades/1.2.0/edit_delete_deletetopic_privileges.js new file mode 100644 index 0000000000..11a070586f --- /dev/null +++ b/src/upgrades/1.2.0/edit_delete_deletetopic_privileges.js @@ -0,0 +1,52 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Granting edit/delete/delete topic on existing categories', + timestamp: Date.UTC(2016, 7, 7), + method: async function () { + const groupsAPI = require('../../groups'); + const privilegesAPI = require('../../privileges'); + + const cids = await db.getSortedSetRange('categories:cid', 0, -1); + + for (const cid of cids) { + const data = await privilegesAPI.categories.list(cid); + const { groups, users } = data; + + for (const group of groups) { + if (group.privileges['groups:topics:reply']) { + await Promise.all([ + groupsAPI.join(`cid:${cid}:privileges:groups:posts:edit`, group.name), + groupsAPI.join(`cid:${cid}:privileges:groups:posts:delete`, group.name), + ]); + winston.verbose(`cid:${cid}:privileges:groups:posts:edit, cid:${cid}:privileges:groups:posts:delete granted to gid: ${group.name}`); + } + + if (group.privileges['groups:topics:create']) { + await groupsAPI.join(`cid:${cid}:privileges:groups:topics:delete`, group.name); + winston.verbose(`cid:${cid}:privileges:groups:topics:delete granted to gid: ${group.name}`); + } + } + + for (const user of users) { + if (user.privileges['topics:reply']) { + await Promise.all([ + groupsAPI.join(`cid:${cid}:privileges:posts:edit`, user.uid), + groupsAPI.join(`cid:${cid}:privileges:posts:delete`, user.uid), + ]); + winston.verbose(`cid:${cid}:privileges:posts:edit, cid:${cid}:privileges:posts:delete granted to uid: ${user.uid}`); + } + if (user.privileges['topics:create']) { + await groupsAPI.join(`cid:${cid}:privileges:topics:delete`, user.uid); + winston.verbose(`cid:${cid}:privileges:topics:delete granted to uid: ${user.uid}`); + } + } + winston.verbose(`-- cid ${cid} upgraded`); + } + }, +}; diff --git a/src/upgrades/1.3.0/favourites_to_bookmarks.js b/src/upgrades/1.3.0/favourites_to_bookmarks.js new file mode 100644 index 0000000000..79adb59731 --- /dev/null +++ b/src/upgrades/1.3.0/favourites_to_bookmarks.js @@ -0,0 +1,39 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Favourites to Bookmarks', + timestamp: Date.UTC(2016, 9, 8), + method: async function () { + const { progress } = this; + const batch = require('../../batch'); + + async function upgradePosts() { + await batch.processSortedSet('posts:pid', async (ids) => { + await Promise.all(ids.map(async (id) => { + progress.incr(); + await db.rename(`pid:${id}:users_favourited`, `pid:${id}:users_bookmarked`); + const reputation = await db.getObjectField(`post:${id}`, 'reputation'); + if (parseInt(reputation, 10)) { + await db.setObjectField(`post:${id}`, 'bookmarks', reputation); + } + await db.deleteObjectField(`post:${id}`, 'reputation'); + })); + }, { + progress: progress, + }); + } + + async function upgradeUsers() { + await batch.processSortedSet('users:joindate', async (ids) => { + await Promise.all(ids.map(async (id) => { + await db.rename(`uid:${id}:favourites`, `uid:${id}:bookmarks`); + })); + }, {}); + } + + await upgradePosts(); + await upgradeUsers(); + }, +}; diff --git a/src/upgrades/1.3.0/sorted_sets_for_post_replies.js b/src/upgrades/1.3.0/sorted_sets_for_post_replies.js new file mode 100644 index 0000000000..5fa0e41d54 --- /dev/null +++ b/src/upgrades/1.3.0/sorted_sets_for_post_replies.js @@ -0,0 +1,39 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Sorted sets for post replies', + timestamp: Date.UTC(2016, 9, 14), + method: function (callback) { + const posts = require('../../posts'); + const batch = require('../../batch'); + const { progress } = this; + + batch.processSortedSet('posts:pid', (ids, next) => { + posts.getPostsFields(ids, ['pid', 'toPid', 'timestamp'], (err, data) => { + if (err) { + return next(err); + } + + progress.incr(); + + async.eachSeries(data, (postData, next) => { + if (!parseInt(postData.toPid, 10)) { + return next(null); + } + winston.verbose(`processing pid: ${postData.pid} toPid: ${postData.toPid}`); + async.parallel([ + async.apply(db.sortedSetAdd, `pid:${postData.toPid}:replies`, postData.timestamp, postData.pid), + async.apply(db.incrObjectField, `post:${postData.toPid}`, 'replies'), + ], next); + }, next); + }); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.4.0/global_and_user_language_keys.js b/src/upgrades/1.4.0/global_and_user_language_keys.js new file mode 100644 index 0000000000..10a8bc06be --- /dev/null +++ b/src/upgrades/1.4.0/global_and_user_language_keys.js @@ -0,0 +1,37 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Update global and user language keys', + timestamp: Date.UTC(2016, 10, 22), + method: async function () { + const { progress } = this; + const user = require('../../user'); + const meta = require('../../meta'); + const batch = require('../../batch'); + + const defaultLang = await meta.configs.get('defaultLang'); + if (defaultLang) { + const newLanguage = defaultLang.replace('_', '-').replace('@', '-x-'); + if (newLanguage !== defaultLang) { + await meta.configs.set('defaultLang', newLanguage); + } + } + + await batch.processSortedSet('users:joindate', async (ids) => { + await Promise.all(ids.map(async (uid) => { + progress.incr(); + const language = await db.getObjectField(`user:${uid}:settings`, 'userLang'); + if (language) { + const newLanguage = language.replace('_', '-').replace('@', '-x-'); + if (newLanguage !== language) { + await user.setSetting(uid, 'userLang', newLanguage); + } + } + })); + }, { + progress: progress, + }); + }, +}; diff --git a/src/upgrades/1.4.0/sorted_set_for_pinned_topics.js b/src/upgrades/1.4.0/sorted_set_for_pinned_topics.js new file mode 100644 index 0000000000..e8e96a734e --- /dev/null +++ b/src/upgrades/1.4.0/sorted_set_for_pinned_topics.js @@ -0,0 +1,34 @@ +'use strict'; + + +const async = require('async'); +const winston = require('winston'); +const db = require('../../database'); + +module.exports = { + name: 'Sorted set for pinned topics', + timestamp: Date.UTC(2016, 10, 25), + method: function (callback) { + const topics = require('../../topics'); + const batch = require('../../batch'); + batch.processSortedSet('topics:tid', (ids, next) => { + topics.getTopicsFields(ids, ['tid', 'cid', 'pinned', 'lastposttime'], (err, data) => { + if (err) { + return next(err); + } + + data = data.filter(topicData => parseInt(topicData.pinned, 10) === 1); + + async.eachSeries(data, (topicData, next) => { + winston.verbose(`processing tid: ${topicData.tid}`); + + async.parallel([ + async.apply(db.sortedSetAdd, `cid:${topicData.cid}:tids:pinned`, Date.now(), topicData.tid), + async.apply(db.sortedSetRemove, `cid:${topicData.cid}:tids`, topicData.tid), + async.apply(db.sortedSetRemove, `cid:${topicData.cid}:tids:posts`, topicData.tid), + ], next); + }, next); + }); + }, callback); + }, +}; diff --git a/src/upgrades/1.4.4/config_urls_update.js b/src/upgrades/1.4.4/config_urls_update.js new file mode 100644 index 0000000000..14f31ca2be --- /dev/null +++ b/src/upgrades/1.4.4/config_urls_update.js @@ -0,0 +1,34 @@ +'use strict'; + + +const db = require('../../database'); + +module.exports = { + name: 'Upgrading config urls to use assets route', + timestamp: Date.UTC(2017, 1, 28), + method: async function () { + const config = await db.getObject('config'); + if (config) { + const keys = [ + 'brand:favicon', + 'brand:touchicon', + 'og:image', + 'brand:logo:url', + 'defaultAvatar', + 'profile:defaultCovers', + ]; + + keys.forEach((key) => { + const oldValue = config[key]; + + if (!oldValue || typeof oldValue !== 'string') { + return; + } + + config[key] = oldValue.replace(/(?:\/assets)?\/(images|uploads)\//g, '/assets/$1/'); + }); + + await db.setObject('config', config); + } + }, +}; diff --git a/src/upgrades/1.4.4/sound_settings.js b/src/upgrades/1.4.4/sound_settings.js new file mode 100644 index 0000000000..d0a403490a --- /dev/null +++ b/src/upgrades/1.4.4/sound_settings.js @@ -0,0 +1,65 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); + + +module.exports = { + name: 'Update global and user sound settings', + timestamp: Date.UTC(2017, 1, 25), + method: function (callback) { + const meta = require('../../meta'); + const batch = require('../../batch'); + + const map = { + 'notification.mp3': 'Default | Deedle-dum', + 'waterdrop-high.mp3': 'Default | Water drop (high)', + 'waterdrop-low.mp3': 'Default | Water drop (low)', + }; + + async.parallel([ + function (cb) { + const keys = ['chat-incoming', 'chat-outgoing', 'notification']; + + db.getObject('settings:sounds', (err, settings) => { + if (err || !settings) { + return cb(err); + } + + keys.forEach((key) => { + if (settings[key] && !settings[key].includes(' | ')) { + settings[key] = map[settings[key]] || ''; + } + }); + + meta.configs.setMultiple(settings, cb); + }); + }, + function (cb) { + const keys = ['notificationSound', 'incomingChatSound', 'outgoingChatSound']; + + batch.processSortedSet('users:joindate', (ids, next) => { + async.each(ids, (uid, next) => { + db.getObject(`user:${uid}:settings`, (err, settings) => { + if (err || !settings) { + return next(err); + } + const newSettings = {}; + keys.forEach((key) => { + if (settings[key] && !settings[key].includes(' | ')) { + newSettings[key] = map[settings[key]] || ''; + } + }); + + if (Object.keys(newSettings).length) { + db.setObject(`user:${uid}:settings`, newSettings, next); + } else { + setImmediate(next); + } + }); + }, next); + }, cb); + }, + ], callback); + }, +}; diff --git a/src/upgrades/1.4.6/delete_sessions.js b/src/upgrades/1.4.6/delete_sessions.js new file mode 100644 index 0000000000..b5f6a6d8d3 --- /dev/null +++ b/src/upgrades/1.4.6/delete_sessions.js @@ -0,0 +1,41 @@ +'use strict'; + +const nconf = require('nconf'); +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Delete accidentally long-lived sessions', + timestamp: Date.UTC(2017, 3, 16), + method: async function () { + let configJSON; + try { + configJSON = require('../../../config.json') || { [process.env.database]: true }; + } catch (err) { + configJSON = { [process.env.database]: true }; + } + + const isRedisSessionStore = configJSON.hasOwnProperty('redis'); + const { progress } = this; + + if (isRedisSessionStore) { + const connection = require('../../database/redis/connection'); + const client = await connection.connect(nconf.get('redis')); + const sessionKeys = await client.keys('sess:*'); + progress.total = sessionKeys.length; + + await batch.processArray(sessionKeys, async (keys) => { + const multi = client.multi(); + keys.forEach((key) => { + progress.incr(); + multi.del(key); + }); + await multi.exec(); + }, { + batch: 1000, + }); + } else if (db.client && db.client.collection) { + await db.client.collection('sessions').deleteMany({}, {}); + } + }, +}; diff --git a/src/upgrades/1.5.0/allowed_file_extensions.js b/src/upgrades/1.5.0/allowed_file_extensions.js new file mode 100644 index 0000000000..74ff7502f5 --- /dev/null +++ b/src/upgrades/1.5.0/allowed_file_extensions.js @@ -0,0 +1,16 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Set default allowed file extensions', + timestamp: Date.UTC(2017, 3, 14), + method: function (callback) { + db.getObjectField('config', 'allowedFileExtensions', (err, value) => { + if (err || value) { + return callback(err); + } + db.setObjectField('config', 'allowedFileExtensions', 'png,jpg,bmp', callback); + }); + }, +}; diff --git a/src/upgrades/1.5.0/flags_refactor.js b/src/upgrades/1.5.0/flags_refactor.js new file mode 100644 index 0000000000..54a01ea401 --- /dev/null +++ b/src/upgrades/1.5.0/flags_refactor.js @@ -0,0 +1,57 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Migrating flags to new schema', + timestamp: Date.UTC(2016, 11, 7), + method: async function () { + const batch = require('../../batch'); + const posts = require('../../posts'); + const flags = require('../../flags'); + const { progress } = this; + + await batch.processSortedSet('posts:pid', async (ids) => { + let postData = await posts.getPostsByPids(ids, 1); + postData = postData.filter(post => post.hasOwnProperty('flags')); + await Promise.all(postData.map(async (post) => { + progress.incr(); + + const [uids, reasons] = await Promise.all([ + db.getSortedSetRangeWithScores(`pid:${post.pid}:flag:uids`, 0, -1), + db.getSortedSetRange(`pid:${post.pid}:flag:uid:reason`, 0, -1), + ]); + + // Adding in another check here in case a post was improperly dismissed + // (flag count > 1 but no flags in db) + if (uids.length && reasons.length) { + // Just take the first entry + const datetime = uids[0].score; + const reason = reasons[0].split(':')[1]; + + try { + const flagObj = await flags.create('post', post.pid, uids[0].value, reason, datetime); + if (post['flag:state'] || post['flag:assignee']) { + await flags.update(flagObj.flagId, 1, { + state: post['flag:state'], + assignee: post['flag:assignee'], + datetime: datetime, + }); + } + if (post.hasOwnProperty('flag:notes') && post['flag:notes'].length) { + let history = JSON.parse(post['flag:history']); + history = history.filter(event => event.type === 'notes')[0]; + await flags.appendNote(flagObj.flagId, history.uid, post['flag:notes'], history.timestamp); + } + } catch (err) { + if (err.message !== '[[error:post-already-flagged]]') { + throw err; + } + } + } + })); + }, { + progress: this.progress, + }); + }, +}; diff --git a/src/upgrades/1.5.0/moderation_history_refactor.js b/src/upgrades/1.5.0/moderation_history_refactor.js new file mode 100644 index 0000000000..7ae08537f0 --- /dev/null +++ b/src/upgrades/1.5.0/moderation_history_refactor.js @@ -0,0 +1,35 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); +const batch = require('../../batch'); + + +module.exports = { + name: 'Update moderation notes to zset', + timestamp: Date.UTC(2017, 2, 22), + method: function (callback) { + const { progress } = this; + + batch.processSortedSet('users:joindate', (ids, next) => { + async.each(ids, (uid, next) => { + db.getObjectField(`user:${uid}`, 'moderationNote', (err, moderationNote) => { + if (err || !moderationNote) { + progress.incr(); + return next(err); + } + const note = { + uid: 1, + note: moderationNote, + timestamp: Date.now(), + }; + + progress.incr(); + db.sortedSetAdd(`uid:${uid}:moderation:notes`, note.timestamp, JSON.stringify(note), next); + }); + }, next); + }, { + progress: this.progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.5.0/post_votes_zset.js b/src/upgrades/1.5.0/post_votes_zset.js new file mode 100644 index 0000000000..51a901f0c2 --- /dev/null +++ b/src/upgrades/1.5.0/post_votes_zset.js @@ -0,0 +1,29 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); + + +module.exports = { + name: 'New sorted set posts:votes', + timestamp: Date.UTC(2017, 1, 27), + method: function (callback) { + const { progress } = this; + + require('../../batch').processSortedSet('posts:pid', (pids, next) => { + async.each(pids, (pid, next) => { + db.getObjectFields(`post:${pid}`, ['upvotes', 'downvotes'], (err, postData) => { + if (err || !postData) { + return next(err); + } + + progress.incr(); + const votes = parseInt(postData.upvotes || 0, 10) - parseInt(postData.downvotes || 0, 10); + db.sortedSetAdd('posts:votes', votes, pid, next); + }); + }, next); + }, { + progress: this.progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.5.0/remove_relative_uploaded_profile_cover.js b/src/upgrades/1.5.0/remove_relative_uploaded_profile_cover.js new file mode 100644 index 0000000000..a457355338 --- /dev/null +++ b/src/upgrades/1.5.0/remove_relative_uploaded_profile_cover.js @@ -0,0 +1,26 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Remove relative_path from uploaded profile cover urls', + timestamp: Date.UTC(2017, 3, 26), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (ids) => { + await Promise.all(ids.map(async (uid) => { + const url = await db.getObjectField(`user:${uid}`, 'cover:url'); + progress.incr(); + + if (url) { + const newUrl = url.replace(/^.*?\/uploads\//, '/assets/uploads/'); + await db.setObjectField(`user:${uid}`, 'cover:url', newUrl); + } + })); + }, { + progress: this.progress, + }); + }, +}; diff --git a/src/upgrades/1.5.1/rename_mods_group.js b/src/upgrades/1.5.1/rename_mods_group.js new file mode 100644 index 0000000000..f694d91a94 --- /dev/null +++ b/src/upgrades/1.5.1/rename_mods_group.js @@ -0,0 +1,33 @@ +'use strict'; + +const async = require('async'); +const winston = require('winston'); + +const batch = require('../../batch'); +const groups = require('../../groups'); + + +module.exports = { + name: 'rename user mod privileges group', + timestamp: Date.UTC(2017, 4, 26), + method: function (callback) { + const { progress } = this; + batch.processSortedSet('categories:cid', (cids, next) => { + async.eachSeries(cids, (cid, next) => { + const groupName = `cid:${cid}:privileges:mods`; + const newName = `cid:${cid}:privileges:moderate`; + groups.exists(groupName, (err, exists) => { + if (err || !exists) { + progress.incr(); + return next(err); + } + winston.verbose(`renaming ${groupName} to ${newName}`); + progress.incr(); + groups.renameGroup(groupName, newName, next); + }); + }, next); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.5.2/rss_token_wipe.js b/src/upgrades/1.5.2/rss_token_wipe.js new file mode 100644 index 0000000000..bee35e0892 --- /dev/null +++ b/src/upgrades/1.5.2/rss_token_wipe.js @@ -0,0 +1,22 @@ +'use strict'; + +const async = require('async'); +const batch = require('../../batch'); +const db = require('../../database'); + +module.exports = { + name: 'Wipe all existing RSS tokens', + timestamp: Date.UTC(2017, 6, 5), + method: function (callback) { + const { progress } = this; + + batch.processSortedSet('users:joindate', (uids, next) => { + async.eachLimit(uids, 500, (uid, next) => { + progress.incr(); + db.deleteObjectField(`user:${uid}`, 'rss_token', next); + }, next); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.5.2/tags_privilege.js b/src/upgrades/1.5.2/tags_privilege.js new file mode 100644 index 0000000000..fd9f5bb02d --- /dev/null +++ b/src/upgrades/1.5.2/tags_privilege.js @@ -0,0 +1,22 @@ +'use strict'; + +const async = require('async'); + +const batch = require('../../batch'); + +module.exports = { + name: 'Give tag privilege to registered-users on all categories', + timestamp: Date.UTC(2017, 5, 16), + method: function (callback) { + const { progress } = this; + const privileges = require('../../privileges'); + batch.processSortedSet('categories:cid', (cids, next) => { + async.eachSeries(cids, (cid, next) => { + progress.incr(); + privileges.categories.give(['groups:topics:tag'], cid, 'registered-users', next); + }, next); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.6.0/clear-stale-digest-template.js b/src/upgrades/1.6.0/clear-stale-digest-template.js new file mode 100644 index 0000000000..8677f7f569 --- /dev/null +++ b/src/upgrades/1.6.0/clear-stale-digest-template.js @@ -0,0 +1,21 @@ +'use strict'; + +const crypto = require('crypto'); +const meta = require('../../meta'); + +module.exports = { + name: 'Clearing stale digest templates that were accidentally saved as custom', + timestamp: Date.UTC(2017, 8, 6), + method: async function () { + const matches = [ + '112e541b40023d6530dd44df4b0d9c5d', // digest @ 75917e25b3b5ad7bed8ed0c36433fb35c9ab33eb + '110b8805f70395b0282fd10555059e9f', // digest @ 9b02bb8f51f0e47c6e335578f776ffc17bc03537 + '9538e7249edb369b2a25b03f2bd3282b', // digest @ 3314ab4b83138c7ae579ac1f1f463098b8c2d414 + ]; + const fieldset = await meta.configs.getFields(['email:custom:digest']); + const hash = fieldset['email:custom:digest'] ? crypto.createHash('md5').update(fieldset['email:custom:digest']).digest('hex') : null; + if (matches.includes(hash)) { + await meta.configs.remove('email:custom:digest'); + } + }, +}; diff --git a/src/upgrades/1.6.0/generate-email-logo.js b/src/upgrades/1.6.0/generate-email-logo.js new file mode 100644 index 0000000000..61c2b02ca4 --- /dev/null +++ b/src/upgrades/1.6.0/generate-email-logo.js @@ -0,0 +1,53 @@ +'use strict'; + + +const async = require('async'); +const path = require('path'); +const nconf = require('nconf'); +const fs = require('fs'); +const meta = require('../../meta'); +const image = require('../../image'); + +module.exports = { + name: 'Generate email logo for use in email header', + timestamp: Date.UTC(2017, 6, 17), + method: function (callback) { + let skip = false; + + async.series([ + function (next) { + // Resize existing logo (if present) to email header size + const uploadPath = path.join(nconf.get('upload_path'), 'system', 'site-logo-x50.png'); + const sourcePath = meta.config['brand:logo'] ? path.join(nconf.get('upload_path'), 'system', path.basename(meta.config['brand:logo'])) : null; + + if (!sourcePath) { + skip = true; + return setImmediate(next); + } + + fs.access(sourcePath, (err) => { + if (err || path.extname(sourcePath) === '.svg') { + skip = true; + return setImmediate(next); + } + + image.resizeImage({ + path: sourcePath, + target: uploadPath, + height: 50, + }, next); + }); + }, + function (next) { + if (skip) { + return setImmediate(next); + } + + meta.configs.setMultiple({ + 'brand:logo': path.join('/assets/uploads/system', path.basename(meta.config['brand:logo'])), + 'brand:emailLogo': '/assets/uploads/system/site-logo-x50.png', + }, next); + }, + ], callback); + }, +}; diff --git a/src/upgrades/1.6.0/ipblacklist-fix.js b/src/upgrades/1.6.0/ipblacklist-fix.js new file mode 100644 index 0000000000..f6b75d47f8 --- /dev/null +++ b/src/upgrades/1.6.0/ipblacklist-fix.js @@ -0,0 +1,13 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Changing ip blacklist storage to object', + timestamp: Date.UTC(2017, 8, 7), + method: async function () { + const rules = await db.get('ip-blacklist-rules'); + await db.delete('ip-blacklist-rules'); + await db.setObject('ip-blacklist-rules', { rules: rules }); + }, +}; diff --git a/src/upgrades/1.6.0/robots-config-change.js b/src/upgrades/1.6.0/robots-config-change.js new file mode 100644 index 0000000000..b56e1808a9 --- /dev/null +++ b/src/upgrades/1.6.0/robots-config-change.js @@ -0,0 +1,21 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Fix incorrect robots.txt schema', + timestamp: Date.UTC(2017, 6, 10), + method: async function () { + const config = await db.getObject('config'); + if (config) { + // fix mongo nested data + if (config.robots && config.robots.txt) { + await db.setObjectField('config', 'robots:txt', config.robots.txt); + } else if (typeof config['robots.txt'] === 'string' && config['robots.txt']) { + await db.setObjectField('config', 'robots:txt', config['robots.txt']); + } + await db.deleteObjectField('config', 'robots'); + await db.deleteObjectField('config', 'robots.txt'); + } + }, +}; diff --git a/src/upgrades/1.6.2/topics_lastposttime_zset.js b/src/upgrades/1.6.2/topics_lastposttime_zset.js new file mode 100644 index 0000000000..459e97a8b3 --- /dev/null +++ b/src/upgrades/1.6.2/topics_lastposttime_zset.js @@ -0,0 +1,29 @@ +'use strict'; + +const async = require('async'); + +const db = require('../../database'); + +module.exports = { + name: 'New sorted set cid::tids:lastposttime', + timestamp: Date.UTC(2017, 9, 30), + method: function (callback) { + const { progress } = this; + + require('../../batch').processSortedSet('topics:tid', (tids, next) => { + async.eachSeries(tids, (tid, next) => { + db.getObjectFields(`topic:${tid}`, ['cid', 'timestamp', 'lastposttime'], (err, topicData) => { + if (err || !topicData) { + return next(err); + } + progress.incr(); + + const timestamp = topicData.lastposttime || topicData.timestamp || Date.now(); + db.sortedSetAdd(`cid:${topicData.cid}:tids:lastposttime`, timestamp, tid, next); + }, next); + }, next); + }, { + progress: this.progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.7.0/generate-custom-html.js b/src/upgrades/1.7.0/generate-custom-html.js new file mode 100644 index 0000000000..cb453a2ae8 --- /dev/null +++ b/src/upgrades/1.7.0/generate-custom-html.js @@ -0,0 +1,43 @@ +'use strict'; + +const db = require('../../database'); +const meta = require('../../meta'); + +module.exports = { + name: 'Generate customHTML block from old customJS setting', + timestamp: Date.UTC(2017, 9, 12), + method: function (callback) { + db.getObjectField('config', 'customJS', (err, newHTML) => { + if (err) { + return callback(err); + } + + let newJS = []; + + // Forgive me for parsing HTML with regex... + const scriptMatch = /^([\s\S]+?)<\/script>/m; + let match = scriptMatch.exec(newHTML); + + while (match) { + if (match[1]) { + // Append to newJS array + newJS.push(match[1].trim()); + + // Remove the match from the existing value + newHTML = ((match.index > 0 ? newHTML.slice(0, match.index) : '') + newHTML.slice(match.index + match[0].length)).trim(); + } + + match = scriptMatch.exec(newHTML); + } + + // Combine newJS array + newJS = newJS.join('\n\n'); + + // Write both values to config + meta.configs.setMultiple({ + customHTML: newHTML, + customJS: newJS, + }, callback); + }); + }, +}; diff --git a/src/upgrades/1.7.1/notification-settings.js b/src/upgrades/1.7.1/notification-settings.js new file mode 100644 index 0000000000..144945c1da --- /dev/null +++ b/src/upgrades/1.7.1/notification-settings.js @@ -0,0 +1,31 @@ +'use strict'; + +const batch = require('../../batch'); +const db = require('../../database'); + +module.exports = { + name: 'Convert old notification digest settings', + timestamp: Date.UTC(2017, 10, 15), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (uids) => { + await Promise.all(uids.map(async (uid) => { + progress.incr(); + const userSettings = await db.getObjectFields(`user:${uid}:settings`, ['sendChatNotifications', 'sendPostNotifications']); + if (userSettings) { + if (parseInt(userSettings.sendChatNotifications, 10) === 1) { + await db.setObjectField(`user:${uid}:settings`, 'notificationType_new-chat', 'notificationemail'); + } + if (parseInt(userSettings.sendPostNotifications, 10) === 1) { + await db.setObjectField(`user:${uid}:settings`, 'notificationType_new-reply', 'notificationemail'); + } + } + await db.deleteObjectFields(`user:${uid}:settings`, ['sendChatNotifications', 'sendPostNotifications']); + })); + }, { + progress: progress, + batch: 500, + }); + }, +}; diff --git a/src/upgrades/1.7.3/key_value_schema_change.js b/src/upgrades/1.7.3/key_value_schema_change.js new file mode 100644 index 0000000000..692b379361 --- /dev/null +++ b/src/upgrades/1.7.3/key_value_schema_change.js @@ -0,0 +1,45 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Change the schema of simple keys so they don\'t use value field (mongodb only)', + timestamp: Date.UTC(2017, 11, 18), + method: async function () { + let configJSON; + try { + configJSON = require('../../../config.json') || { [process.env.database]: true, database: process.env.database }; + } catch (err) { + configJSON = { [process.env.database]: true, database: process.env.database }; + } + const isMongo = configJSON.hasOwnProperty('mongo') && configJSON.database === 'mongo'; + const { progress } = this; + if (!isMongo) { + return; + } + const { client } = db; + const query = { + _key: { $exists: true }, + value: { $exists: true }, + score: { $exists: false }, + }; + progress.total = await client.collection('objects').countDocuments(query); + const cursor = await client.collection('objects').find(query).batchSize(1000); + + let done = false; + while (!done) { + const item = await cursor.next(); + progress.incr(); + if (item === null) { + done = true; + } else { + delete item.expireAt; + if (Object.keys(item).length === 3 && item.hasOwnProperty('_key') && item.hasOwnProperty('value')) { + await client.collection('objects').updateOne({ _key: item._key }, { $rename: { value: 'data' } }); + } + } + } + }, +}; diff --git a/src/upgrades/1.7.3/topic_votes.js b/src/upgrades/1.7.3/topic_votes.js new file mode 100644 index 0000000000..968a9b7b16 --- /dev/null +++ b/src/upgrades/1.7.3/topic_votes.js @@ -0,0 +1,42 @@ +'use strict'; + + +const batch = require('../../batch'); +const db = require('../../database'); + +module.exports = { + name: 'Add votes to topics', + timestamp: Date.UTC(2017, 11, 8), + method: async function () { + const { progress } = this; + + batch.processSortedSet('topics:tid', async (tids) => { + await Promise.all(tids.map(async (tid) => { + progress.incr(); + const topicData = await db.getObjectFields(`topic:${tid}`, ['mainPid', 'cid', 'pinned']); + if (topicData.mainPid && topicData.cid) { + const postData = await db.getObject(`post:${topicData.mainPid}`); + if (postData) { + const upvotes = parseInt(postData.upvotes, 10) || 0; + const downvotes = parseInt(postData.downvotes, 10) || 0; + const data = { + upvotes: upvotes, + downvotes: downvotes, + }; + const votes = upvotes - downvotes; + await Promise.all([ + db.setObject(`topic:${tid}`, data), + db.sortedSetAdd('topics:votes', votes, tid), + ]); + if (parseInt(topicData.pinned, 10) !== 1) { + await db.sortedSetAdd(`cid:${topicData.cid}:tids:votes`, votes, tid); + } + } + } + })); + }, { + progress: progress, + batch: 500, + }); + }, +}; diff --git a/src/upgrades/1.7.4/chat_privilege.js b/src/upgrades/1.7.4/chat_privilege.js new file mode 100644 index 0000000000..5ce78c17b1 --- /dev/null +++ b/src/upgrades/1.7.4/chat_privilege.js @@ -0,0 +1,12 @@ +'use strict'; + + +const groups = require('../../groups'); + +module.exports = { + name: 'Give chat privilege to registered-users', + timestamp: Date.UTC(2017, 11, 18), + method: function (callback) { + groups.join('cid:0:privileges:groups:chat', 'registered-users', callback); + }, +}; diff --git a/src/upgrades/1.7.4/fix_moved_topics_byvotes.js b/src/upgrades/1.7.4/fix_moved_topics_byvotes.js new file mode 100644 index 0000000000..cfc37badbc --- /dev/null +++ b/src/upgrades/1.7.4/fix_moved_topics_byvotes.js @@ -0,0 +1,31 @@ +'use strict'; + +const batch = require('../../batch'); +const db = require('../../database'); + +module.exports = { + name: 'Fix sort by votes for moved topics', + timestamp: Date.UTC(2018, 0, 8), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('topics:tid', async (tids) => { + await Promise.all(tids.map(async (tid) => { + progress.incr(); + const topicData = await db.getObjectFields(`topic:${tid}`, ['cid', 'oldCid', 'upvotes', 'downvotes', 'pinned']); + if (topicData.cid && topicData.oldCid) { + const upvotes = parseInt(topicData.upvotes, 10) || 0; + const downvotes = parseInt(topicData.downvotes, 10) || 0; + const votes = upvotes - downvotes; + await db.sortedSetRemove(`cid:${topicData.oldCid}:tids:votes`, tid); + if (parseInt(topicData.pinned, 10) !== 1) { + await db.sortedSetAdd(`cid:${topicData.cid}:tids:votes`, votes, tid); + } + } + })); + }, { + progress: progress, + batch: 500, + }); + }, +}; diff --git a/src/upgrades/1.7.4/fix_user_topics_per_category.js b/src/upgrades/1.7.4/fix_user_topics_per_category.js new file mode 100644 index 0000000000..a5bda080c7 --- /dev/null +++ b/src/upgrades/1.7.4/fix_user_topics_per_category.js @@ -0,0 +1,29 @@ +'use strict'; + +const batch = require('../../batch'); +const db = require('../../database'); + +module.exports = { + name: 'Fix topics in categories per user if they were moved', + timestamp: Date.UTC(2018, 0, 22), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('topics:tid', async (tids) => { + await Promise.all(tids.map(async (tid) => { + progress.incr(); + const topicData = await db.getObjectFields(`topic:${tid}`, ['cid', 'tid', 'uid', 'oldCid', 'timestamp']); + if (topicData.cid && topicData.oldCid) { + const isMember = await db.isSortedSetMember(`cid:${topicData.oldCid}:uid:${topicData.uid}`, topicData.tid); + if (isMember) { + await db.sortedSetRemove(`cid:${topicData.oldCid}:uid:${topicData.uid}:tids`, tid); + await db.sortedSetAdd(`cid:${topicData.cid}:uid:${topicData.uid}:tids`, topicData.timestamp, tid); + } + } + })); + }, { + progress: progress, + batch: 500, + }); + }, +}; diff --git a/src/upgrades/1.7.4/global_upload_privilege.js b/src/upgrades/1.7.4/global_upload_privilege.js new file mode 100644 index 0000000000..69495c2892 --- /dev/null +++ b/src/upgrades/1.7.4/global_upload_privilege.js @@ -0,0 +1,45 @@ +'use strict'; + + +const async = require('async'); +const groups = require('../../groups'); +const privileges = require('../../privileges'); +const db = require('../../database'); + +module.exports = { + name: 'Give upload privilege to registered-users globally if it is given on a category', + timestamp: Date.UTC(2018, 0, 3), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, (err, cids) => { + if (err) { + return callback(err); + } + async.eachSeries(cids, (cid, next) => { + getGroupPrivileges(cid, (err, groupPrivileges) => { + if (err) { + return next(err); + } + + const privs = []; + if (groupPrivileges['groups:upload:post:image']) { + privs.push('groups:upload:post:image'); + } + if (groupPrivileges['groups:upload:post:file']) { + privs.push('groups:upload:post:file'); + } + privileges.global.give(privs, 'registered-users', next); + }); + }, callback); + }); + }, +}; + +function getGroupPrivileges(cid, callback) { + const tasks = {}; + + ['groups:upload:post:image', 'groups:upload:post:file'].forEach((privilege) => { + tasks[privilege] = async.apply(groups.isMember, 'registered-users', `cid:${cid}:privileges:${privilege}`); + }); + + async.parallel(tasks, callback); +} diff --git a/src/upgrades/1.7.4/rename_min_reputation_settings.js b/src/upgrades/1.7.4/rename_min_reputation_settings.js new file mode 100644 index 0000000000..2caec134ce --- /dev/null +++ b/src/upgrades/1.7.4/rename_min_reputation_settings.js @@ -0,0 +1,25 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Rename privileges:downvote and privileges:flag to min:rep:downvote, min:rep:flag respectively', + timestamp: Date.UTC(2018, 0, 12), + method: function (callback) { + db.getObjectFields('config', ['privileges:downvote', 'privileges:flag'], (err, config) => { + if (err) { + return callback(err); + } + + db.setObject('config', { + 'min:rep:downvote': parseInt(config['privileges:downvote'], 10) || 0, + 'min:rep:flag': parseInt(config['privileges:downvote'], 10) || 0, + }, (err) => { + if (err) { + return callback(err); + } + db.deleteObjectFields('config', ['privileges:downvote', 'privileges:flag'], callback); + }); + }); + }, +}; diff --git a/src/upgrades/1.7.4/vote_privilege.js b/src/upgrades/1.7.4/vote_privilege.js new file mode 100644 index 0000000000..3eb4be46de --- /dev/null +++ b/src/upgrades/1.7.4/vote_privilege.js @@ -0,0 +1,22 @@ +'use strict'; + + +const async = require('async'); + +const privileges = require('../../privileges'); +const db = require('../../database'); + +module.exports = { + name: 'Give vote privilege to registered-users on all categories', + timestamp: Date.UTC(2018, 0, 9), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, (err, cids) => { + if (err) { + return callback(err); + } + async.eachSeries(cids, (cid, next) => { + privileges.categories.give(['groups:posts:upvote', 'groups:posts:downvote'], cid, 'registered-users', next); + }, callback); + }); + }, +}; diff --git a/src/upgrades/1.7.6/flatten_navigation_data.js b/src/upgrades/1.7.6/flatten_navigation_data.js new file mode 100644 index 0000000000..623933a764 --- /dev/null +++ b/src/upgrades/1.7.6/flatten_navigation_data.js @@ -0,0 +1,24 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Flatten navigation data', + timestamp: Date.UTC(2018, 1, 17), + method: async function () { + const data = await db.getSortedSetRangeWithScores('navigation:enabled', 0, -1); + const order = []; + const items = []; + data.forEach((item) => { + let navItem = JSON.parse(item.value); + const keys = Object.keys(navItem); + if (keys.length && parseInt(keys[0], 10) >= 0) { + navItem = navItem[keys[0]]; + } + order.push(item.score); + items.push(JSON.stringify(navItem)); + }); + await db.delete('navigation:enabled'); + await db.sortedSetAdd('navigation:enabled', order, items); + }, +}; diff --git a/src/upgrades/1.7.6/notification_types.js b/src/upgrades/1.7.6/notification_types.js new file mode 100644 index 0000000000..d82726d135 --- /dev/null +++ b/src/upgrades/1.7.6/notification_types.js @@ -0,0 +1,21 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Add default settings for notification delivery types', + timestamp: Date.UTC(2018, 1, 14), + method: async function () { + const config = await db.getObject('config'); + const postNotifications = parseInt(config.sendPostNotifications, 10) === 1 ? 'notification' : 'none'; + const chatNotifications = parseInt(config.sendChatNotifications, 10) === 1 ? 'notification' : 'none'; + await db.setObject('config', { + notificationType_upvote: config.notificationType_upvote || 'notification', + 'notificationType_new-topic': config['notificationType_new-topic'] || 'notification', + 'notificationType_new-reply': config['notificationType_new-reply'] || postNotifications, + notificationType_follow: config.notificationType_follow || 'notification', + 'notificationType_new-chat': config['notificationType_new-chat'] || chatNotifications, + 'notificationType_group-invite': config['notificationType_group-invite'] || 'notification', + }); + }, +}; diff --git a/src/upgrades/1.7.6/update_min_pass_strength.js b/src/upgrades/1.7.6/update_min_pass_strength.js new file mode 100644 index 0000000000..988854436b --- /dev/null +++ b/src/upgrades/1.7.6/update_min_pass_strength.js @@ -0,0 +1,14 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + name: 'Revising minimum password strength to 1 (from 0)', + timestamp: Date.UTC(2018, 1, 21), + method: async function () { + const strength = await db.getObjectField('config', 'minimumPasswordStrength'); + if (!strength) { + await db.setObjectField('config', 'minimumPasswordStrength', 1); + } + }, +}; diff --git a/src/upgrades/1.8.0/give_signature_privileges.js b/src/upgrades/1.8.0/give_signature_privileges.js new file mode 100644 index 0000000000..9302171e4c --- /dev/null +++ b/src/upgrades/1.8.0/give_signature_privileges.js @@ -0,0 +1,11 @@ +'use strict'; + +const privileges = require('../../privileges'); + +module.exports = { + name: 'Give registered users signature privilege', + timestamp: Date.UTC(2018, 1, 28), + method: function (callback) { + privileges.global.give(['groups:signature'], 'registered-users', callback); + }, +}; diff --git a/src/upgrades/1.8.0/give_spiders_privileges.js b/src/upgrades/1.8.0/give_spiders_privileges.js new file mode 100644 index 0000000000..4169469d22 --- /dev/null +++ b/src/upgrades/1.8.0/give_spiders_privileges.js @@ -0,0 +1,49 @@ +'use strict'; + + +const async = require('async'); +const groups = require('../../groups'); +const privileges = require('../../privileges'); +const db = require('../../database'); + +module.exports = { + name: 'Give category access privileges to spiders system group', + timestamp: Date.UTC(2018, 0, 31), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, (err, cids) => { + if (err) { + return callback(err); + } + async.eachSeries(cids, (cid, next) => { + getGroupPrivileges(cid, (err, groupPrivileges) => { + if (err) { + return next(err); + } + + const privs = []; + if (groupPrivileges['groups:find']) { + privs.push('groups:find'); + } + if (groupPrivileges['groups:read']) { + privs.push('groups:read'); + } + if (groupPrivileges['groups:topics:read']) { + privs.push('groups:topics:read'); + } + + privileges.categories.give(privs, cid, 'spiders', next); + }); + }, callback); + }); + }, +}; + +function getGroupPrivileges(cid, callback) { + const tasks = {}; + + ['groups:find', 'groups:read', 'groups:topics:read'].forEach((privilege) => { + tasks[privilege] = async.apply(groups.isMember, 'guests', `cid:${cid}:privileges:${privilege}`); + }); + + async.parallel(tasks, callback); +} diff --git a/src/upgrades/1.8.1/diffs_zset_to_listhash.js b/src/upgrades/1.8.1/diffs_zset_to_listhash.js new file mode 100644 index 0000000000..a432b7cb4c --- /dev/null +++ b/src/upgrades/1.8.1/diffs_zset_to_listhash.js @@ -0,0 +1,57 @@ +'use strict'; + +const async = require('async'); +const db = require('../../database'); +const batch = require('../../batch'); + + +module.exports = { + name: 'Reformatting post diffs to be stored in lists and hash instead of single zset', + timestamp: Date.UTC(2018, 2, 15), + method: function (callback) { + const { progress } = this; + + batch.processSortedSet('posts:pid', (pids, next) => { + async.each(pids, (pid, next) => { + db.getSortedSetRangeWithScores(`post:${pid}:diffs`, 0, -1, (err, diffs) => { + if (err) { + return next(err); + } + + if (!diffs || !diffs.length) { + progress.incr(); + return next(); + } + + // For each diff, push to list + async.each(diffs, (diff, next) => { + async.series([ + async.apply(db.delete.bind(db), `post:${pid}:diffs`), + async.apply(db.listPrepend.bind(db), `post:${pid}:diffs`, diff.score), + async.apply(db.setObject.bind(db), `diff:${pid}.${diff.score}`, { + pid: pid, + patch: diff.value, + }), + ], next); + }, (err) => { + if (err) { + return next(err); + } + + progress.incr(); + return next(); + }); + }); + }, (err) => { + if (err) { + // Probably type error, ok to incr and continue + progress.incr(); + } + + return next(); + }); + }, { + progress: progress, + }, callback); + }, +}; diff --git a/src/upgrades/1.9.0/refresh_post_upload_associations.js b/src/upgrades/1.9.0/refresh_post_upload_associations.js new file mode 100644 index 0000000000..0713fc8940 --- /dev/null +++ b/src/upgrades/1.9.0/refresh_post_upload_associations.js @@ -0,0 +1,21 @@ +'use strict'; + +const async = require('async'); +const posts = require('../../posts'); + +module.exports = { + name: 'Refresh post-upload associations', + timestamp: Date.UTC(2018, 3, 16), + method: function (callback) { + const { progress } = this; + + require('../../batch').processSortedSet('posts:pid', (pids, next) => { + async.each(pids, (pid, next) => { + posts.uploads.sync(pid, next); + progress.incr(); + }, next); + }, { + progress: this.progress, + }, callback); + }, +}; diff --git a/src/upgrades/TEMPLATE b/src/upgrades/TEMPLATE new file mode 100644 index 0000000000..23dd3d1e3b --- /dev/null +++ b/src/upgrades/TEMPLATE @@ -0,0 +1,14 @@ +'use strict'; + +const db = require('../../database'); + +module.exports = { + // you should use spaces + // the underscores are there so you can double click to select the whole thing + name: 'User_friendly_upgrade_script_name', + // remember, month is zero-indexed (so January is 0, December is 11) + timestamp: Date.UTC(2020, 0, 1), + method: async () => { + // Do stuff here... + }, +}; diff --git a/src/user/admin.js b/src/user/admin.js new file mode 100644 index 0000000000..21222c3181 --- /dev/null +++ b/src/user/admin.js @@ -0,0 +1,89 @@ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const winston = require('winston'); +const validator = require('validator'); + +const { baseDir } = require('../constants').paths; +const db = require('../database'); +const plugins = require('../plugins'); +const batch = require('../batch'); + +module.exports = function (User) { + User.logIP = async function (uid, ip) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const now = Date.now(); + const bulk = [ + [`uid:${uid}:ip`, now, ip || 'Unknown'], + ]; + if (ip) { + bulk.push([`ip:${ip}:uid`, now, uid]); + } + await db.sortedSetAddBulk(bulk); + }; + + User.getIPs = async function (uid, stop) { + const ips = await db.getSortedSetRevRange(`uid:${uid}:ip`, 0, stop); + return ips.map(ip => validator.escape(String(ip))); + }; + + User.getUsersCSV = async function () { + winston.verbose('[user/getUsersCSV] Compiling User CSV data'); + + const data = await plugins.hooks.fire('filter:user.csvFields', { fields: ['uid', 'email', 'username'] }); + let csvContent = `${data.fields.join(',')}\n`; + await batch.processSortedSet('users:joindate', async (uids) => { + const usersData = await User.getUsersFields(uids, data.fields); + csvContent += usersData.reduce((memo, user) => { + memo += `${data.fields.map(field => user[field]).join(',')}\n`; + return memo; + }, ''); + }, {}); + + return csvContent; + }; + + User.exportUsersCSV = async function () { + winston.verbose('[user/exportUsersCSV] Exporting User CSV data'); + + const { fields, showIps } = await plugins.hooks.fire('filter:user.csvFields', { + fields: ['email', 'username', 'uid'], + showIps: true, + }); + const fd = await fs.promises.open( + path.join(baseDir, 'build/export', 'users.csv'), + 'w' + ); + fs.promises.appendFile(fd, `${fields.join(',')}${showIps ? ',ip' : ''}\n`); + await batch.processSortedSet('users:joindate', async (uids) => { + const usersData = await User.getUsersFields(uids, fields.slice()); + let userIPs = ''; + let ips = []; + + if (showIps) { + ips = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:ip`)); + } + + let line = ''; + usersData.forEach((user, index) => { + line += `${fields.map(field => user[field]).join(',')}`; + if (showIps) { + userIPs = ips[index] ? ips[index].join(',') : ''; + line += `,"${userIPs}"\n`; + } else { + line += '\n'; + } + }); + + await fs.promises.appendFile(fd, line); + }, { + batch: 5000, + interval: 250, + }); + await fd.close(); + }; +}; diff --git a/src/user/approval.js b/src/user/approval.js new file mode 100644 index 0000000000..5a5d7d0537 --- /dev/null +++ b/src/user/approval.js @@ -0,0 +1,167 @@ +'use strict'; + +const validator = require('validator'); +const winston = require('winston'); +const cronJob = require('cron').CronJob; + +const db = require('../database'); +const meta = require('../meta'); +const emailer = require('../emailer'); +const notifications = require('../notifications'); +const groups = require('../groups'); +const utils = require('../utils'); +const slugify = require('../slugify'); +const plugins = require('../plugins'); + +module.exports = function (User) { + new cronJob('0 * * * *', (() => { + User.autoApprove(); + }), null, true); + + User.addToApprovalQueue = async function (userData) { + userData.username = userData.username.trim(); + userData.userslug = slugify(userData.username); + await canQueue(userData); + const hashedPassword = await User.hashPassword(userData.password); + const data = { + username: userData.username, + email: userData.email, + ip: userData.ip, + hashedPassword: hashedPassword, + }; + const results = await plugins.hooks.fire('filter:user.addToApprovalQueue', { data: data, userData: userData }); + await db.setObject(`registration:queue:name:${userData.username}`, results.data); + await db.sortedSetAdd('registration:queue', Date.now(), userData.username); + await sendNotificationToAdmins(userData.username); + }; + + async function canQueue(userData) { + await User.isDataValid(userData); + const usernames = await db.getSortedSetRange('registration:queue', 0, -1); + if (usernames.includes(userData.username)) { + throw new Error('[[error:username-taken]]'); + } + const keys = usernames.filter(Boolean).map(username => `registration:queue:name:${username}`); + const data = await db.getObjectsFields(keys, ['email']); + const emails = data.map(data => data && data.email).filter(Boolean); + if (userData.email && emails.includes(userData.email)) { + throw new Error('[[error:email-taken]]'); + } + } + + async function sendNotificationToAdmins(username) { + const notifObj = await notifications.create({ + type: 'new-register', + bodyShort: `[[notifications:new_register, ${username}]]`, + nid: `new_register:${username}`, + path: '/admin/manage/registration', + mergeId: 'new_register', + }); + await notifications.pushGroup(notifObj, 'administrators'); + } + + User.acceptRegistration = async function (username) { + const userData = await db.getObject(`registration:queue:name:${username}`); + if (!userData) { + throw new Error('[[error:invalid-data]]'); + } + const creation_time = await db.sortedSetScore('registration:queue', username); + const uid = await User.create(userData); + await User.setUserFields(uid, { + password: userData.hashedPassword, + 'password:shaWrapped': 1, + }); + await removeFromQueue(username); + await markNotificationRead(username); + await plugins.hooks.fire('filter:register.complete', { uid: uid }); + await emailer.send('registration_accepted', uid, { + username: username, + subject: `[[email:welcome-to, ${meta.config.title || meta.config.browserTitle || 'NodeBB'}]]`, + template: 'registration_accepted', + uid: uid, + }).catch(err => winston.error(`[emailer.send] ${err.stack}`)); + const total = await db.incrObjectFieldBy('registration:queue:approval:times', 'totalTime', Math.floor((Date.now() - creation_time) / 60000)); + const counter = await db.incrObjectField('registration:queue:approval:times', 'counter'); + await db.setObjectField('registration:queue:approval:times', 'average', total / counter); + return uid; + }; + + async function markNotificationRead(username) { + const nid = `new_register:${username}`; + const uids = await groups.getMembers('administrators', 0, -1); + const promises = uids.map(uid => notifications.markRead(nid, uid)); + await Promise.all(promises); + } + + User.rejectRegistration = async function (username) { + await removeFromQueue(username); + await markNotificationRead(username); + }; + + async function removeFromQueue(username) { + await Promise.all([ + db.sortedSetRemove('registration:queue', username), + db.delete(`registration:queue:name:${username}`), + ]); + } + + User.shouldQueueUser = async function (ip) { + const { registrationApprovalType } = meta.config; + if (registrationApprovalType === 'admin-approval') { + return true; + } else if (registrationApprovalType === 'admin-approval-ip') { + const count = await db.sortedSetCard(`ip:${ip}:uid`); + return !!count; + } + return false; + }; + + User.getRegistrationQueue = async function (start, stop) { + const data = await db.getSortedSetRevRangeWithScores('registration:queue', start, stop); + const keys = data.filter(Boolean).map(user => `registration:queue:name:${user.value}`); + let users = await db.getObjects(keys); + users = users.filter(Boolean).map((user, index) => { + user.timestampISO = utils.toISOString(data[index].score); + user.email = validator.escape(String(user.email)); + user.usernameEscaped = validator.escape(String(user.username)); + delete user.hashedPassword; + return user; + }); + await Promise.all(users.map(async (user) => { + // temporary: see http://www.stopforumspam.com/forum/viewtopic.php?id=6392 + // need to keep this for getIPMatchedUsers + user.ip = user.ip.replace('::ffff:', ''); + await getIPMatchedUsers(user); + user.customActions = [].concat(user.customActions); + /* + // then spam prevention plugins, using the "filter:user.getRegistrationQueue" hook can be like: + user.customActions.push({ + title: '[[spam-be-gone:report-user]]', + id: 'report-spam-user-' + user.username, + class: 'btn-warning report-spam-user', + icon: 'fa-flag' + }); + */ + })); + + const results = await plugins.hooks.fire('filter:user.getRegistrationQueue', { users: users }); + return results.users; + }; + + async function getIPMatchedUsers(user) { + const uids = await User.getUidsFromSet(`ip:${user.ip}:uid`, 0, -1); + user.ipMatch = await User.getUsersFields(uids, ['uid', 'username', 'picture']); + } + + User.autoApprove = async function () { + if (meta.config.autoApproveTime <= 0) { + return; + } + const users = await db.getSortedSetRevRangeWithScores('registration:queue', 0, -1); + const now = Date.now(); + for (const user of users.filter(user => now - user.score >= meta.config.autoApproveTime * 3600000)) { + // eslint-disable-next-line no-await-in-loop + await User.acceptRegistration(user.value); + } + }; +}; diff --git a/src/user/auth.js b/src/user/auth.js new file mode 100644 index 0000000000..cdd500d972 --- /dev/null +++ b/src/user/auth.js @@ -0,0 +1,163 @@ +'use strict'; + +const winston = require('winston'); +const validator = require('validator'); +const util = require('util'); +const _ = require('lodash'); +const db = require('../database'); +const meta = require('../meta'); +const events = require('../events'); +const batch = require('../batch'); +const utils = require('../utils'); + +module.exports = function (User) { + User.auth = {}; + + User.auth.logAttempt = async function (uid, ip) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const exists = await db.exists(`lockout:${uid}`); + if (exists) { + throw new Error('[[error:account-locked]]'); + } + const attempts = await db.increment(`loginAttempts:${uid}`); + if (attempts <= meta.config.loginAttempts) { + return await db.pexpire(`loginAttempts:${uid}`, 1000 * 60 * 60); + } + // Lock out the account + await db.set(`lockout:${uid}`, ''); + const duration = 1000 * 60 * meta.config.lockoutDuration; + + await db.delete(`loginAttempts:${uid}`); + await db.pexpire(`lockout:${uid}`, duration); + await events.log({ + type: 'account-locked', + uid: uid, + ip: ip, + }); + throw new Error('[[error:account-locked]]'); + }; + + User.auth.getFeedToken = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const _token = await db.getObjectField(`user:${uid}`, 'rss_token'); + const token = _token || utils.generateUUID(); + if (!_token) { + await User.setUserField(uid, 'rss_token', token); + } + return token; + }; + + User.auth.clearLoginAttempts = async function (uid) { + await db.delete(`loginAttempts:${uid}`); + }; + + User.auth.resetLockout = async function (uid) { + await db.deleteAll([ + `loginAttempts:${uid}`, + `lockout:${uid}`, + ]); + }; + + const getSessionFromStore = util.promisify( + (sid, callback) => db.sessionStore.get(sid, (err, sessObj) => callback(err, sessObj || null)) + ); + const sessionStoreDestroy = util.promisify( + (sid, callback) => db.sessionStore.destroy(sid, err => callback(err)) + ); + + User.auth.getSessions = async function (uid, curSessionId) { + await cleanExpiredSessions(uid); + const sids = await db.getSortedSetRevRange(`uid:${uid}:sessions`, 0, 19); + let sessions = await Promise.all(sids.map(sid => getSessionFromStore(sid))); + sessions = sessions.map((sessObj, idx) => { + if (sessObj && sessObj.meta) { + sessObj.meta.current = curSessionId === sids[idx]; + sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString(); + sessObj.meta.ip = validator.escape(String(sessObj.meta.ip)); + } + return sessObj && sessObj.meta; + }).filter(Boolean); + return sessions; + }; + + async function cleanExpiredSessions(uid) { + const uuidMapping = await db.getObject(`uid:${uid}:sessionUUID:sessionId`); + if (!uuidMapping) { + return; + } + const expiredUUIDs = []; + const expiredSids = []; + await Promise.all(Object.keys(uuidMapping).map(async (uuid) => { + const sid = uuidMapping[uuid]; + const sessionObj = await getSessionFromStore(sid); + const expired = !sessionObj || !sessionObj.hasOwnProperty('passport') || + !sessionObj.passport.hasOwnProperty('user') || + parseInt(sessionObj.passport.user, 10) !== parseInt(uid, 10); + if (expired) { + expiredUUIDs.push(uuid); + expiredSids.push(sid); + } + })); + await db.deleteObjectFields(`uid:${uid}:sessionUUID:sessionId`, expiredUUIDs); + await db.sortedSetRemove(`uid:${uid}:sessions`, expiredSids); + } + + User.auth.addSession = async function (uid, sessionId) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + await cleanExpiredSessions(uid); + await db.sortedSetAdd(`uid:${uid}:sessions`, Date.now(), sessionId); + await revokeSessionsAboveThreshold(uid, meta.config.maxUserSessions); + }; + + async function revokeSessionsAboveThreshold(uid, maxUserSessions) { + const activeSessions = await db.getSortedSetRange(`uid:${uid}:sessions`, 0, -1); + if (activeSessions.length > maxUserSessions) { + const sessionsToRevoke = activeSessions.slice(0, activeSessions.length - maxUserSessions); + await Promise.all(sessionsToRevoke.map(sessionId => User.auth.revokeSession(sessionId, uid))); + } + } + + User.auth.revokeSession = async function (sessionId, uid) { + winston.verbose(`[user.auth] Revoking session ${sessionId} for user ${uid}`); + const sessionObj = await getSessionFromStore(sessionId); + if (sessionObj && sessionObj.meta && sessionObj.meta.uuid) { + await db.deleteObjectField(`uid:${uid}:sessionUUID:sessionId`, sessionObj.meta.uuid); + } + await Promise.all([ + db.sortedSetRemove(`uid:${uid}:sessions`, sessionId), + sessionStoreDestroy(sessionId), + ]); + }; + + User.auth.revokeAllSessions = async function (uids, except) { + uids = Array.isArray(uids) ? uids : [uids]; + const sids = await db.getSortedSetsMembers(uids.map(uid => `uid:${uid}:sessions`)); + const promises = []; + uids.forEach((uid, index) => { + const ids = sids[index].filter(id => id !== except); + if (ids.length) { + promises.push(ids.map(s => User.auth.revokeSession(s, uid))); + } + }); + await Promise.all(promises); + }; + + User.auth.deleteAllSessions = async function () { + await batch.processSortedSet('users:joindate', async (uids) => { + const sessionKeys = uids.map(uid => `uid:${uid}:sessions`); + const sessionUUIDKeys = uids.map(uid => `uid:${uid}:sessionUUID:sessionId`); + const sids = _.flatten(await db.getSortedSetRange(sessionKeys, 0, -1)); + + await Promise.all([ + db.deleteAll(sessionKeys.concat(sessionUUIDKeys)), + ...sids.map(sid => sessionStoreDestroy(sid)), + ]); + }, { batch: 1000 }); + }; +}; diff --git a/src/user/bans.js b/src/user/bans.js new file mode 100644 index 0000000000..49fb61ab41 --- /dev/null +++ b/src/user/bans.js @@ -0,0 +1,143 @@ +'use strict'; + +const winston = require('winston'); + +const meta = require('../meta'); +const emailer = require('../emailer'); +const db = require('../database'); +const groups = require('../groups'); +const privileges = require('../privileges'); + +module.exports = function (User) { + User.bans = {}; + + User.bans.ban = async function (uid, until, reason) { + // "until" (optional) is unix timestamp in milliseconds + // "reason" (optional) is a string + until = until || 0; + reason = reason || ''; + + const now = Date.now(); + + until = parseInt(until, 10); + if (isNaN(until)) { + throw new Error('[[error:ban-expiry-missing]]'); + } + + const banKey = `uid:${uid}:ban:${now}`; + const banData = { + uid: uid, + timestamp: now, + expire: until > now ? until : 0, + }; + if (reason) { + banData.reason = reason; + } + + // Leaving all other system groups to have privileges constrained to the "banned-users" group + const systemGroups = groups.systemGroups.filter(group => group !== groups.BANNED_USERS); + await groups.leave(systemGroups, uid); + await groups.join(groups.BANNED_USERS, uid); + await db.sortedSetAdd('users:banned', now, uid); + await db.sortedSetAdd(`uid:${uid}:bans:timestamp`, now, banKey); + await db.setObject(banKey, banData); + await User.setUserField(uid, 'banned:expire', banData.expire); + if (until > now) { + await db.sortedSetAdd('users:banned:expire', until, uid); + } else { + await db.sortedSetRemove('users:banned:expire', uid); + } + + // Email notification of ban + const username = await User.getUserField(uid, 'username'); + const siteTitle = meta.config.title || 'NodeBB'; + + const data = { + subject: `[[email:banned.subject, ${siteTitle}]]`, + username: username, + until: until ? (new Date(until)).toUTCString().replace(/,/g, '\\,') : false, + reason: reason, + }; + await emailer.send('banned', uid, data).catch(err => winston.error(`[emailer.send] ${err.stack}`)); + + return banData; + }; + + User.bans.unban = async function (uids) { + uids = Array.isArray(uids) ? uids : [uids]; + const userData = await User.getUsersFields(uids, ['email:confirmed']); + + await db.setObject(uids.map(uid => `user:${uid}`), { 'banned:expire': 0 }); + + /* eslint-disable no-await-in-loop */ + for (const user of userData) { + const systemGroupsToJoin = [ + 'registered-users', + (parseInt(user['email:confirmed'], 10) === 1 ? 'verified-users' : 'unverified-users'), + ]; + await groups.leave(groups.BANNED_USERS, user.uid); + // An unbanned user would lost its previous "Global Moderator" status + await groups.join(systemGroupsToJoin, user.uid); + } + + await db.sortedSetRemove(['users:banned', 'users:banned:expire'], uids); + }; + + User.bans.isBanned = async function (uids) { + const isArray = Array.isArray(uids); + uids = isArray ? uids : [uids]; + const result = await User.bans.unbanIfExpired(uids); + return isArray ? result.map(r => r.banned) : result[0].banned; + }; + + User.bans.canLoginIfBanned = async function (uid) { + let canLogin = true; + + const { banned } = (await User.bans.unbanIfExpired([uid]))[0]; + // Group privilege overshadows individual one + if (banned) { + canLogin = await privileges.global.canGroup('local:login', groups.BANNED_USERS); + } + if (banned && !canLogin) { + // Checking a single privilege of user + canLogin = await groups.isMember(uid, 'cid:0:privileges:local:login'); + } + + return canLogin; + }; + + User.bans.unbanIfExpired = async function (uids) { + // loading user data will unban if it has expired -barisu + const userData = await User.getUsersFields(uids, ['banned:expire']); + return User.bans.calcExpiredFromUserData(userData); + }; + + User.bans.calcExpiredFromUserData = async function (userData) { + const isArray = Array.isArray(userData); + userData = isArray ? userData : [userData]; + const banned = await groups.isMembers(userData.map(u => u.uid), groups.BANNED_USERS); + userData = userData.map((userData, index) => ({ + banned: banned[index], + 'banned:expire': userData && userData['banned:expire'], + banExpired: userData && userData['banned:expire'] <= Date.now() && userData['banned:expire'] !== 0, + })); + return isArray ? userData : userData[0]; + }; + + User.bans.filterBanned = async function (uids) { + const isBanned = await User.bans.isBanned(uids); + return uids.filter((uid, index) => !isBanned[index]); + }; + + User.bans.getReason = async function (uid) { + if (parseInt(uid, 10) <= 0) { + return ''; + } + const keys = await db.getSortedSetRevRange(`uid:${uid}:bans:timestamp`, 0, 0); + if (!keys.length) { + return ''; + } + const banObj = await db.getObject(keys[0]); + return banObj && banObj.reason ? banObj.reason : ''; + }; +}; diff --git a/src/user/blocks.js b/src/user/blocks.js new file mode 100644 index 0000000000..3d36f6d660 --- /dev/null +++ b/src/user/blocks.js @@ -0,0 +1,113 @@ +'use strict'; + +const db = require('../database'); +const plugins = require('../plugins'); +const cacheCreate = require('../cache/lru'); + +module.exports = function (User) { + User.blocks = { + _cache: cacheCreate({ + name: 'user:blocks', + max: 100, + ttl: 0, + }), + }; + + User.blocks.is = async function (targetUid, uids) { + const isArray = Array.isArray(uids); + uids = isArray ? uids : [uids]; + const blocks = await User.blocks.list(uids); + const isBlocked = uids.map((uid, index) => blocks[index] && blocks[index].includes(parseInt(targetUid, 10))); + return isArray ? isBlocked : isBlocked[0]; + }; + + User.blocks.can = async function (callerUid, blockerUid, blockeeUid, type) { + // Guests can't block + if (blockerUid === 0 || blockeeUid === 0) { + throw new Error('[[error:cannot-block-guest]]'); + } else if (blockerUid === blockeeUid) { + throw new Error('[[error:cannot-block-self]]'); + } + + // Administrators and global moderators cannot be blocked + // Only admins/mods can block users as another user + const [isCallerAdminOrMod, isBlockeeAdminOrMod] = await Promise.all([ + User.isAdminOrGlobalMod(callerUid), + User.isAdminOrGlobalMod(blockeeUid), + ]); + if (isBlockeeAdminOrMod && type === 'block') { + throw new Error('[[error:cannot-block-privileged]]'); + } + if (parseInt(callerUid, 10) !== parseInt(blockerUid, 10) && !isCallerAdminOrMod) { + throw new Error('[[error:no-privileges]]'); + } + }; + + User.blocks.list = async function (uids) { + const isArray = Array.isArray(uids); + uids = (isArray ? uids : [uids]).map(uid => parseInt(uid, 10)); + const cachedData = {}; + const unCachedUids = User.blocks._cache.getUnCachedKeys(uids, cachedData); + if (unCachedUids.length) { + const unCachedData = await db.getSortedSetsMembers(unCachedUids.map(uid => `uid:${uid}:blocked_uids`)); + unCachedUids.forEach((uid, index) => { + cachedData[uid] = (unCachedData[index] || []).map(uid => parseInt(uid, 10)); + User.blocks._cache.set(uid, cachedData[uid]); + }); + } + const result = uids.map(uid => cachedData[uid] || []); + return isArray ? result.slice() : result[0]; + }; + + User.blocks.add = async function (targetUid, uid) { + await User.blocks.applyChecks('block', targetUid, uid); + await db.sortedSetAdd(`uid:${uid}:blocked_uids`, Date.now(), targetUid); + await User.incrementUserFieldBy(uid, 'blocksCount', 1); + User.blocks._cache.del(parseInt(uid, 10)); + plugins.hooks.fire('action:user.blocks.add', { uid: uid, targetUid: targetUid }); + }; + + User.blocks.remove = async function (targetUid, uid) { + await User.blocks.applyChecks('unblock', targetUid, uid); + await db.sortedSetRemove(`uid:${uid}:blocked_uids`, targetUid); + await User.decrementUserFieldBy(uid, 'blocksCount', 1); + User.blocks._cache.del(parseInt(uid, 10)); + plugins.hooks.fire('action:user.blocks.remove', { uid: uid, targetUid: targetUid }); + }; + + User.blocks.applyChecks = async function (type, targetUid, uid) { + await User.blocks.can(uid, uid, targetUid); + const isBlock = type === 'block'; + const is = await User.blocks.is(targetUid, uid); + if (is === isBlock) { + throw new Error(`[[error:already-${isBlock ? 'blocked' : 'unblocked'}]]`); + } + }; + + User.blocks.filterUids = async function (targetUid, uids) { + const isBlocked = await User.blocks.is(targetUid, uids); + return uids.filter((uid, index) => !isBlocked[index]); + }; + + User.blocks.filter = async function (uid, property, set) { + // Given whatever is passed in, iterates through it, and removes entries made by blocked uids + // property is optional + if (Array.isArray(property) && typeof set === 'undefined') { + set = property; + property = 'uid'; + } + + if (!Array.isArray(set) || !set.length) { + return set; + } + + const isPlain = typeof set[0] !== 'object'; + const blocked_uids = await User.blocks.list(uid); + const blockedSet = new Set(blocked_uids); + + set = set.filter(item => !blockedSet.has(parseInt(isPlain ? item : (item && item[property]), 10))); + const data = await plugins.hooks.fire('filter:user.blocks.filter', { set: set, property: property, uid: uid, blockedSet: blockedSet }); + + return data.set; + }; +}; diff --git a/src/user/categories.js b/src/user/categories.js new file mode 100644 index 0000000000..42087d5f17 --- /dev/null +++ b/src/user/categories.js @@ -0,0 +1,76 @@ +'use strict'; + +const _ = require('lodash'); + +const db = require('../database'); +const categories = require('../categories'); +const plugins = require('../plugins'); + +module.exports = function (User) { + User.setCategoryWatchState = async function (uid, cids, state) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const isStateValid = Object.values(categories.watchStates).includes(parseInt(state, 10)); + if (!isStateValid) { + throw new Error('[[error:invalid-watch-state]]'); + } + cids = Array.isArray(cids) ? cids : [cids]; + const exists = await categories.exists(cids); + if (exists.includes(false)) { + throw new Error('[[error:no-category]]'); + } + await db.sortedSetsAdd(cids.map(cid => `cid:${cid}:uid:watch:state`), state, uid); + }; + + User.getCategoryWatchState = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return {}; + } + + const cids = await categories.getAllCidsFromSet('categories:cid'); + const states = await categories.getWatchState(cids, uid); + return _.zipObject(cids, states); + }; + + User.getIgnoredCategories = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return []; + } + const cids = await User.getCategoriesByStates(uid, [categories.watchStates.ignoring]); + const result = await plugins.hooks.fire('filter:user.getIgnoredCategories', { + uid: uid, + cids: cids, + }); + return result.cids; + }; + + User.getWatchedCategories = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return []; + } + const cids = await User.getCategoriesByStates(uid, [categories.watchStates.watching]); + const result = await plugins.hooks.fire('filter:user.getWatchedCategories', { + uid: uid, + cids: cids, + }); + return result.cids; + }; + + User.getCategoriesByStates = async function (uid, states) { + if (!(parseInt(uid, 10) > 0)) { + return await categories.getAllCidsFromSet('categories:cid'); + } + const cids = await categories.getAllCidsFromSet('categories:cid'); + const userState = await categories.getWatchState(cids, uid); + return cids.filter((cid, index) => states.includes(userState[index])); + }; + + User.ignoreCategory = async function (uid, cid) { + await User.setCategoryWatchState(uid, cid, categories.watchStates.ignoring); + }; + + User.watchCategory = async function (uid, cid) { + await User.setCategoryWatchState(uid, cid, categories.watchStates.watching); + }; +}; diff --git a/src/user/create.js b/src/user/create.js new file mode 100644 index 0000000000..1e66017664 --- /dev/null +++ b/src/user/create.js @@ -0,0 +1,199 @@ +'use strict'; + +const zxcvbn = require('zxcvbn'); +const winston = require('winston'); + +const db = require('../database'); +const utils = require('../utils'); +const slugify = require('../slugify'); +const plugins = require('../plugins'); +const groups = require('../groups'); +const meta = require('../meta'); +const analytics = require('../analytics'); + +module.exports = function (User) { + User.create = async function (data) { + data.username = data.username.trim(); + data.userslug = slugify(data.username); + if (data.email !== undefined) { + data.email = String(data.email).trim(); + } + if (data.accounttype !== undefined) { + data.accounttype = data.accounttype.trim(); + } + + await User.isDataValid(data); + + await lock(data.username, '[[error:username-taken]]'); + if (data.email && data.email !== data.username) { + await lock(data.email, '[[error:email-taken]]'); + } + + try { + return await create(data); + } finally { + await db.deleteObjectFields('locks', [data.username, data.email]); + } + }; + + async function lock(value, error) { + const count = await db.incrObjectField('locks', value); + if (count > 1) { + throw new Error(error); + } + } + + async function create(data) { + const timestamp = data.timestamp || Date.now(); + + let userData = { + username: data.username, + userslug: data.userslug, + accounttype: data.accounttype || 'student', + email: data.email || '', + joindate: timestamp, + lastonline: timestamp, + status: 'online', + }; + ['picture', 'fullname', 'location', 'birthday'].forEach((field) => { + if (data[field]) { + userData[field] = data[field]; + } + }); + if (data.gdpr_consent === true) { + userData.gdpr_consent = 1; + } + if (data.acceptTos === true) { + userData.acceptTos = 1; + } + + const renamedUsername = await User.uniqueUsername(userData); + const userNameChanged = !!renamedUsername; + if (userNameChanged) { + userData.username = renamedUsername; + userData.userslug = slugify(renamedUsername); + } + + const results = await plugins.hooks.fire('filter:user.create', { user: userData, data: data }); + userData = results.user; + + const uid = await db.incrObjectField('global', 'nextUid'); + const isFirstUser = uid === 1; + userData.uid = uid; + + await db.setObject(`user:${uid}`, userData); + + const bulkAdd = [ + ['username:uid', userData.uid, userData.username], + [`user:${userData.uid}:usernames`, timestamp, `${userData.username}:${timestamp}`], + ['username:sorted', 0, `${userData.username.toLowerCase()}:${userData.uid}`], + ['userslug:uid', userData.uid, userData.userslug], + ['users:joindate', timestamp, userData.uid], + ['users:online', timestamp, userData.uid], + ['users:postcount', 0, userData.uid], + ['users:reputation', 0, userData.uid], + ]; + + if (userData.fullname) { + bulkAdd.push(['fullname:sorted', 0, `${userData.fullname.toLowerCase()}:${userData.uid}`]); + } + + await Promise.all([ + db.incrObjectField('global', 'userCount'), + analytics.increment('registrations'), + db.sortedSetAddBulk(bulkAdd), + groups.join(['registered-users', 'unverified-users'], userData.uid), + User.notifications.sendWelcomeNotification(userData.uid), + storePassword(userData.uid, data.password), + User.updateDigestSetting(userData.uid, meta.config.dailyDigestFreq), + ]); + + if (userData.email && isFirstUser) { + await User.email.confirmByUid(userData.uid); + } + + if (userData.email && userData.uid > 1) { + await User.email.sendValidationEmail(userData.uid, { + email: userData.email, + template: 'welcome', + subject: `[[email:welcome-to, ${meta.config.title || meta.config.browserTitle || 'NodeBB'}]]`, + }).catch(err => winston.error(`[user.create] Validation email failed to send\n[emailer.send] ${err.stack}`)); + } + if (userNameChanged) { + await User.notifications.sendNameChangeNotification(userData.uid, userData.username); + } + plugins.hooks.fire('action:user.create', { user: userData, data: data }); + return userData.uid; + } + + async function storePassword(uid, password) { + if (!password) { + return; + } + const hash = await User.hashPassword(password); + await Promise.all([ + User.setUserFields(uid, { + password: hash, + 'password:shaWrapped': 1, + }), + User.reset.updateExpiry(uid), + ]); + } + + User.isDataValid = async function (userData) { + if (userData.email && !utils.isEmailValid(userData.email)) { + throw new Error('[[error:invalid-email]]'); + } + + if (!utils.isUserNameValid(userData.username) || !userData.userslug) { + throw new Error(`[[error:invalid-username, ${userData.username}]]`); + } + + if (userData.password) { + User.isPasswordValid(userData.password); + } + + if (userData.email) { + const available = await User.email.available(userData.email); + if (!available) { + throw new Error('[[error:email-taken]]'); + } + } + }; + + User.isPasswordValid = function (password, minStrength) { + minStrength = (minStrength || minStrength === 0) ? minStrength : meta.config.minimumPasswordStrength; + + // Sanity checks: Checks if defined and is string + if (!password || !utils.isPasswordValid(password)) { + throw new Error('[[error:invalid-password]]'); + } + + if (password.length < meta.config.minimumPasswordLength) { + throw new Error('[[reset_password:password_too_short]]'); + } + + if (password.length > 512) { + throw new Error('[[error:password-too-long]]'); + } + + const strength = zxcvbn(password); + if (strength.score < minStrength) { + throw new Error('[[user:weak_password]]'); + } + }; + + User.uniqueUsername = async function (userData) { + let numTries = 0; + let { username } = userData; + while (true) { + /* eslint-disable no-await-in-loop */ + const exists = await meta.userOrGroupExists(username); + if (!exists) { + return numTries ? username : null; + } + username = `${userData.username} ${numTries.toString(32)}`; + numTries += 1; + } + }; +}; diff --git a/src/user/data.js b/src/user/data.js new file mode 100644 index 0000000000..77c255ac45 --- /dev/null +++ b/src/user/data.js @@ -0,0 +1,356 @@ +'use strict'; + +const validator = require('validator'); +const nconf = require('nconf'); +const _ = require('lodash'); + +const db = require('../database'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const relative_path = nconf.get('relative_path'); + +const intFields = [ + 'uid', 'postcount', 'topiccount', 'reputation', 'profileviews', + 'banned', 'banned:expire', 'email:confirmed', 'joindate', 'lastonline', + 'lastqueuetime', 'lastposttime', 'followingCount', 'followerCount', + 'blocksCount', 'passwordExpiry', 'mutedUntil', +]; + +module.exports = function (User) { + const fieldWhitelist = [ + 'uid', 'username', 'userslug', 'email', 'email:confirmed', 'joindate', 'accounttype', + 'lastonline', 'picture', 'icon:bgColor', 'fullname', 'location', 'birthday', 'website', + 'aboutme', 'signature', 'uploadedpicture', 'profileviews', 'reputation', + 'postcount', 'topiccount', 'lastposttime', 'banned', 'banned:expire', + 'status', 'flags', 'followerCount', 'followingCount', 'cover:url', + 'cover:position', 'groupTitle', 'mutedUntil', 'mutedReason', + ]; + + User.guestData = { + uid: 0, + username: '[[global:guest]]', + displayname: '[[global:guest]]', + userslug: '', + fullname: '[[global:guest]]', + email: '', + 'icon:text': '?', + 'icon:bgColor': '#aaa', + groupTitle: '', + groupTitleArray: [], + status: 'offline', + reputation: 0, + 'email:confirmed': 0, + }; + + User.getUsersFields = async function (uids, fields) { + if (!Array.isArray(uids) || !uids.length) { + return []; + } + + uids = uids.map(uid => (isNaN(uid) ? 0 : parseInt(uid, 10))); + + const fieldsToRemove = []; + fields = fields.slice(); + ensureRequiredFields(fields, fieldsToRemove); + + const uniqueUids = _.uniq(uids).filter(uid => uid > 0); + + const results = await plugins.hooks.fire('filter:user.whitelistFields', { + uids: uids, + whitelist: fieldWhitelist.slice(), + }); + if (!fields.length) { + fields = results.whitelist; + } else { + // Never allow password retrieval via this method + fields = fields.filter(value => value !== 'password'); + } + + const users = await db.getObjectsFields(uniqueUids.map(uid => `user:${uid}`), fields); + const result = await plugins.hooks.fire('filter:user.getFields', { + uids: uniqueUids, + users: users, + fields: fields, + }); + result.users.forEach((user, index) => { + if (uniqueUids[index] > 0 && !user.uid) { + user.oldUid = uniqueUids[index]; + } + }); + await modifyUserData(result.users, fields, fieldsToRemove); + return uidsToUsers(uids, uniqueUids, result.users); + }; + + function ensureRequiredFields(fields, fieldsToRemove) { + function addField(field) { + if (!fields.includes(field)) { + fields.push(field); + fieldsToRemove.push(field); + } + } + + if (fields.length && !fields.includes('uid')) { + fields.push('uid'); + } + + if (fields.includes('picture')) { + addField('uploadedpicture'); + } + + if (fields.includes('status')) { + addField('lastonline'); + } + + if (fields.includes('banned') && !fields.includes('banned:expire')) { + addField('banned:expire'); + } + + if (fields.includes('username') && !fields.includes('fullname')) { + addField('fullname'); + } + } + + function uidsToUsers(uids, uniqueUids, usersData) { + const uidToUser = _.zipObject(uniqueUids, usersData); + const users = uids.map((uid) => { + const user = uidToUser[uid] || { ...User.guestData }; + if (!parseInt(user.uid, 10)) { + user.username = (user.hasOwnProperty('oldUid') && parseInt(user.oldUid, 10)) ? '[[global:former_user]]' : '[[global:guest]]'; + user.displayname = user.username; + } + + return user; + }); + return users; + } + + User.getUserField = async function (uid, field) { + const user = await User.getUserFields(uid, [field]); + return user ? user[field] : null; + }; + + User.getUserFields = async function (uid, fields) { + const users = await User.getUsersFields([uid], fields); + return users ? users[0] : null; + }; + + User.getUserData = async function (uid) { + const users = await User.getUsersData([uid]); + return users ? users[0] : null; + }; + + User.getUsersData = async function (uids) { + return await User.getUsersFields(uids, []); + }; + + User.hidePrivateData = async function (users, callerUID) { + let single = false; + if (!Array.isArray(users)) { + users = [users]; + single = true; + } + + const [userSettings, isAdmin, isGlobalModerator] = await Promise.all([ + User.getMultipleUserSettings(users.map(user => user.uid)), + User.isAdministrator(callerUID), + User.isGlobalModerator(callerUID), + ]); + + users = await Promise.all(users.map(async (userData, idx) => { + const _userData = { ...userData }; + + const isSelf = parseInt(callerUID, 10) === parseInt(_userData.uid, 10); + const privilegedOrSelf = isAdmin || isGlobalModerator || isSelf; + + if (!privilegedOrSelf && (!userSettings[idx].showemail || meta.config.hideEmail)) { + _userData.email = ''; + } + if (!privilegedOrSelf && (!userSettings[idx].showfullname || meta.config.hideFullname)) { + _userData.fullname = ''; + } + return _userData; + })); + + return single ? users.pop() : users; + }; + + async function modifyUserData(users, requestedFields, fieldsToRemove) { + let uidToSettings = {}; + if (meta.config.showFullnameAsDisplayName) { + const uids = users.map(user => user.uid); + uidToSettings = _.zipObject(uids, await db.getObjectsFields( + uids.map(uid => `user:${uid}:settings`), + ['showfullname'] + )); + } + + await Promise.all(users.map(async (user) => { + if (!user) { + return; + } + + db.parseIntFields(user, intFields, requestedFields); + + if (user.hasOwnProperty('username')) { + parseDisplayName(user, uidToSettings); + user.username = validator.escape(user.username ? user.username.toString() : ''); + } + + if (user.hasOwnProperty('email')) { + user.email = validator.escape(user.email ? user.email.toString() : ''); + } + + if (!parseInt(user.uid, 10)) { + for (const [key, value] of Object.entries(User.guestData)) { + user[key] = value; + } + user.picture = User.getDefaultAvatar(); + } + + if (user.hasOwnProperty('groupTitle')) { + parseGroupTitle(user); + } + + if (user.picture && user.picture === user.uploadedpicture) { + user.uploadedpicture = user.picture.startsWith('http') ? user.picture : relative_path + user.picture; + user.picture = user.uploadedpicture; + } else if (user.uploadedpicture) { + user.uploadedpicture = user.uploadedpicture.startsWith('http') ? user.uploadedpicture : relative_path + user.uploadedpicture; + } + if (meta.config.defaultAvatar && !user.picture) { + user.picture = User.getDefaultAvatar(); + } + + if (user.hasOwnProperty('status') && user.hasOwnProperty('lastonline')) { + user.status = User.getStatus(user); + } + + for (let i = 0; i < fieldsToRemove.length; i += 1) { + user[fieldsToRemove[i]] = undefined; + } + + // User Icons + if (requestedFields.includes('picture') && user.username && parseInt(user.uid, 10) && !meta.config.defaultAvatar) { + const iconBackgrounds = await User.getIconBackgrounds(user.uid); + let bgColor = await User.getUserField(user.uid, 'icon:bgColor'); + if (!iconBackgrounds.includes(bgColor)) { + bgColor = Array.prototype.reduce.call(user.username, (cur, next) => cur + next.charCodeAt(), 0); + bgColor = iconBackgrounds[bgColor % iconBackgrounds.length]; + } + user['icon:text'] = (user.username[0] || '').toUpperCase(); + user['icon:bgColor'] = bgColor; + } + + if (user.hasOwnProperty('joindate')) { + user.joindateISO = utils.toISOString(user.joindate); + } + + if (user.hasOwnProperty('lastonline')) { + user.lastonlineISO = utils.toISOString(user.lastonline) || user.joindateISO; + } + + if (user.hasOwnProperty('banned') || user.hasOwnProperty('banned:expire')) { + const result = await User.bans.calcExpiredFromUserData(user); + user.banned = result.banned; + const unban = result.banned && result.banExpired; + user.banned_until = unban ? 0 : user['banned:expire']; + user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned'; + if (unban) { + await User.bans.unban(user.uid); + user.banned = false; + } + } + + if (user.hasOwnProperty('mutedUntil')) { + user.muted = user.mutedUntil > Date.now(); + } + })); + + return await plugins.hooks.fire('filter:users.get', users); + } + + function parseDisplayName(user, uidToSettings) { + let showfullname = parseInt(meta.config.showfullname, 10) === 1; + if (uidToSettings[user.uid]) { + if (parseInt(uidToSettings[user.uid].showfullname, 10) === 0) { + showfullname = false; + } else if (parseInt(uidToSettings[user.uid].showfullname, 10) === 1) { + showfullname = true; + } + } + + user.displayname = validator.escape(String( + meta.config.showFullnameAsDisplayName && showfullname && user.fullname ? + user.fullname : + user.username + )); + } + + function parseGroupTitle(user) { + try { + user.groupTitleArray = JSON.parse(user.groupTitle); + } catch (err) { + if (user.groupTitle) { + user.groupTitleArray = [user.groupTitle]; + } else { + user.groupTitle = ''; + user.groupTitleArray = []; + } + } + if (!Array.isArray(user.groupTitleArray)) { + if (user.groupTitleArray) { + user.groupTitleArray = [user.groupTitleArray]; + } else { + user.groupTitleArray = []; + } + } + if (!meta.config.allowMultipleBadges && user.groupTitleArray.length) { + user.groupTitleArray = [user.groupTitleArray[0]]; + } + } + + User.getIconBackgrounds = async (uid = 0) => { + let iconBackgrounds = [ + '#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', + '#009688', '#1b5e20', '#33691e', '#827717', '#e65100', '#ff5722', + '#795548', '#607d8b', + ]; + + ({ iconBackgrounds } = await plugins.hooks.fire('filter:user.iconBackgrounds', { uid, iconBackgrounds })); + return iconBackgrounds; + }; + + User.getDefaultAvatar = function () { + if (!meta.config.defaultAvatar) { + return ''; + } + return meta.config.defaultAvatar.startsWith('http') ? meta.config.defaultAvatar : relative_path + meta.config.defaultAvatar; + }; + + User.setUserField = async function (uid, field, value) { + await User.setUserFields(uid, { [field]: value }); + }; + + User.setUserFields = async function (uid, data) { + await db.setObject(`user:${uid}`, data); + for (const [field, value] of Object.entries(data)) { + plugins.hooks.fire('action:user.set', { uid, field, value, type: 'set' }); + } + }; + + User.incrementUserFieldBy = async function (uid, field, value) { + return await incrDecrUserFieldBy(uid, field, value, 'increment'); + }; + + User.decrementUserFieldBy = async function (uid, field, value) { + return await incrDecrUserFieldBy(uid, field, -value, 'decrement'); + }; + + async function incrDecrUserFieldBy(uid, field, value, type) { + const newValue = await db.incrObjectFieldBy(`user:${uid}`, field, value); + plugins.hooks.fire('action:user.set', { uid: uid, field: field, value: newValue, type: type }); + return newValue; + } +}; diff --git a/src/user/delete.js b/src/user/delete.js new file mode 100644 index 0000000000..12c2546c0b --- /dev/null +++ b/src/user/delete.js @@ -0,0 +1,217 @@ +'use strict'; + +const async = require('async'); +const _ = require('lodash'); +const path = require('path'); +const nconf = require('nconf'); +const util = require('util'); +const rimrafAsync = util.promisify(require('rimraf')); + +const db = require('../database'); +const posts = require('../posts'); +const flags = require('../flags'); +const topics = require('../topics'); +const groups = require('../groups'); +const messaging = require('../messaging'); +const plugins = require('../plugins'); +const batch = require('../batch'); + +module.exports = function (User) { + const deletesInProgress = {}; + + User.delete = async (callerUid, uid) => { + await User.deleteContent(callerUid, uid); + return await User.deleteAccount(uid); + }; + + User.deleteContent = async function (callerUid, uid) { + if (parseInt(uid, 10) <= 0) { + throw new Error('[[error:invalid-uid]]'); + } + if (deletesInProgress[uid]) { + throw new Error('[[error:already-deleting]]'); + } + deletesInProgress[uid] = 'user.delete'; + await deletePosts(callerUid, uid); + await deleteTopics(callerUid, uid); + await deleteUploads(callerUid, uid); + await deleteQueued(uid); + delete deletesInProgress[uid]; + }; + + async function deletePosts(callerUid, uid) { + await batch.processSortedSet(`uid:${uid}:posts`, async (pids) => { + await posts.purge(pids, callerUid); + }, { alwaysStartAt: 0, batch: 500 }); + } + + async function deleteTopics(callerUid, uid) { + await batch.processSortedSet(`uid:${uid}:topics`, async (ids) => { + await async.eachSeries(ids, async (tid) => { + await topics.purge(tid, callerUid); + }); + }, { alwaysStartAt: 0 }); + } + + async function deleteUploads(callerUid, uid) { + const uploads = await db.getSortedSetMembers(`uid:${uid}:uploads`); + await User.deleteUpload(callerUid, uid, uploads); + } + + async function deleteQueued(uid) { + let deleteIds = []; + await batch.processSortedSet('post:queue', async (ids) => { + const data = await db.getObjects(ids.map(id => `post:queue:${id}`)); + const userQueuedIds = data.filter(d => parseInt(d.uid, 10) === parseInt(uid, 10)).map(d => d.id); + deleteIds = deleteIds.concat(userQueuedIds); + }, { batch: 500 }); + await async.eachSeries(deleteIds, posts.removeFromQueue); + } + + async function removeFromSortedSets(uid) { + await db.sortedSetsRemove([ + 'users:joindate', + 'users:postcount', + 'users:reputation', + 'users:banned', + 'users:banned:expire', + 'users:flags', + 'users:online', + 'digest:day:uids', + 'digest:week:uids', + 'digest:biweek:uids', + 'digest:month:uids', + ], uid); + } + + User.deleteAccount = async function (uid) { + if (deletesInProgress[uid] === 'user.deleteAccount') { + throw new Error('[[error:already-deleting]]'); + } + deletesInProgress[uid] = 'user.deleteAccount'; + + await removeFromSortedSets(uid); + const userData = await db.getObject(`user:${uid}`); + + if (!userData || !userData.username) { + delete deletesInProgress[uid]; + throw new Error('[[error:no-user]]'); + } + + await plugins.hooks.fire('static:user.delete', { uid: uid, userData: userData }); + await deleteVotes(uid); + await deleteChats(uid); + await User.auth.revokeAllSessions(uid); + + const keys = [ + `uid:${uid}:notifications:read`, + `uid:${uid}:notifications:unread`, + `uid:${uid}:bookmarks`, + `uid:${uid}:tids_read`, + `uid:${uid}:tids_unread`, + `uid:${uid}:followed_tids`, + `uid:${uid}:ignored_tids`, + `uid:${uid}:blocked_uids`, + `user:${uid}:settings`, + `user:${uid}:usernames`, + `user:${uid}:emails`, + `uid:${uid}:topics`, `uid:${uid}:posts`, + `uid:${uid}:chats`, `uid:${uid}:chats:unread`, + `uid:${uid}:chat:rooms`, `uid:${uid}:chat:rooms:unread`, + `uid:${uid}:upvote`, `uid:${uid}:downvote`, + `uid:${uid}:flag:pids`, + `uid:${uid}:sessions`, `uid:${uid}:sessionUUID:sessionId`, + `invitation:uid:${uid}`, + ]; + + const bulkRemove = [ + ['username:uid', userData.username], + ['username:sorted', `${userData.username.toLowerCase()}:${uid}`], + ['userslug:uid', userData.userslug], + ['fullname:uid', userData.fullname], + ]; + if (userData.email) { + bulkRemove.push(['email:uid', userData.email.toLowerCase()]); + bulkRemove.push(['email:sorted', `${userData.email.toLowerCase()}:${uid}`]); + } + + if (userData.fullname) { + bulkRemove.push(['fullname:sorted', `${userData.fullname.toLowerCase()}:${uid}`]); + } + + await Promise.all([ + db.sortedSetRemoveBulk(bulkRemove), + db.decrObjectField('global', 'userCount'), + db.deleteAll(keys), + db.setRemove('invitation:uids', uid), + deleteUserIps(uid), + deleteUserFromFollowers(uid), + deleteImages(uid), + groups.leaveAllGroups(uid), + flags.resolveFlag('user', uid, uid), + User.reset.cleanByUid(uid), + ]); + await db.deleteAll([`followers:${uid}`, `following:${uid}`, `user:${uid}`]); + delete deletesInProgress[uid]; + return userData; + }; + + async function deleteVotes(uid) { + const [upvotedPids, downvotedPids] = await Promise.all([ + db.getSortedSetRange(`uid:${uid}:upvote`, 0, -1), + db.getSortedSetRange(`uid:${uid}:downvote`, 0, -1), + ]); + const pids = _.uniq(upvotedPids.concat(downvotedPids).filter(Boolean)); + await async.eachSeries(pids, async (pid) => { + await posts.unvote(pid, uid); + }); + } + + async function deleteChats(uid) { + const roomIds = await db.getSortedSetRange(`uid:${uid}:chat:rooms`, 0, -1); + const userKeys = roomIds.map(roomId => `uid:${uid}:chat:room:${roomId}:mids`); + + await Promise.all([ + messaging.leaveRooms(uid, roomIds), + db.deleteAll(userKeys), + ]); + } + + async function deleteUserIps(uid) { + const ips = await db.getSortedSetRange(`uid:${uid}:ip`, 0, -1); + await db.sortedSetsRemove(ips.map(ip => `ip:${ip}:uid`), uid); + await db.delete(`uid:${uid}:ip`); + } + + async function deleteUserFromFollowers(uid) { + const [followers, following] = await Promise.all([ + db.getSortedSetRange(`followers:${uid}`, 0, -1), + db.getSortedSetRange(`following:${uid}`, 0, -1), + ]); + + async function updateCount(uids, name, fieldName) { + await async.each(uids, async (uid) => { + let count = await db.sortedSetCard(name + uid); + count = parseInt(count, 10) || 0; + await db.setObjectField(`user:${uid}`, fieldName, count); + }); + } + + const followingSets = followers.map(uid => `following:${uid}`); + const followerSets = following.map(uid => `followers:${uid}`); + + await Promise.all([ + db.sortedSetsRemove(followerSets.concat(followingSets), uid), + updateCount(following, 'followers:', 'followerCount'), + updateCount(followers, 'following:', 'followingCount'), + ]); + } + + async function deleteImages(uid) { + const folder = path.join(nconf.get('upload_path'), 'profile'); + await Promise.all([ + rimrafAsync(path.join(folder, `${uid}-profilecover*`)), + rimrafAsync(path.join(folder, `${uid}-profileavatar*`)), + ]); + } +}; diff --git a/src/user/digest.js b/src/user/digest.js new file mode 100644 index 0000000000..f81237b70a --- /dev/null +++ b/src/user/digest.js @@ -0,0 +1,212 @@ +'use strict'; + +const winston = require('winston'); +const nconf = require('nconf'); + +const db = require('../database'); +const batch = require('../batch'); +const meta = require('../meta'); +const user = require('./index'); +const topics = require('../topics'); +const plugins = require('../plugins'); +const emailer = require('../emailer'); +const utils = require('../utils'); + +const Digest = module.exports; + +const baseUrl = nconf.get('base_url'); + +Digest.execute = async function (payload) { + const digestsDisabled = meta.config.disableEmailSubscriptions === 1; + if (digestsDisabled) { + winston.info(`[user/jobs] Did not send digests (${payload.interval}) because subscription system is disabled.`); + return; + } + let { subscribers } = payload; + if (!subscribers) { + subscribers = await Digest.getSubscribers(payload.interval); + } + if (!subscribers.length) { + return; + } + try { + winston.info(`[user/jobs] Digest (${payload.interval}) scheduling completed (${subscribers.length} subscribers). Sending emails; this may take some time...`); + await Digest.send({ + interval: payload.interval, + subscribers: subscribers, + }); + winston.info(`[user/jobs] Digest (${payload.interval}) complete.`); + } catch (err) { + winston.error(`[user/jobs] Could not send digests (${payload.interval})\n${err.stack}`); + throw err; + } +}; + +Digest.getUsersInterval = async (uids) => { + // Checks whether user specifies digest setting, or false for system default setting + let single = false; + if (!Array.isArray(uids) && !isNaN(parseInt(uids, 10))) { + uids = [uids]; + single = true; + } + + const settings = await db.getObjects(uids.map(uid => `user:${uid}:settings`)); + const interval = uids.map((uid, index) => (settings[index] && settings[index].dailyDigestFreq) || false); + return single ? interval[0] : interval; +}; + +Digest.getSubscribers = async function (interval) { + let subscribers = []; + + await batch.processSortedSet('users:joindate', async (uids) => { + const settings = await user.getMultipleUserSettings(uids); + let subUids = []; + settings.forEach((hash) => { + if (hash.dailyDigestFreq === interval) { + subUids.push(hash.uid); + } + }); + subUids = await user.bans.filterBanned(subUids); + subscribers = subscribers.concat(subUids); + }, { + interval: 1000, + batch: 500, + }); + + const results = await plugins.hooks.fire('filter:digest.subscribers', { + interval: interval, + subscribers: subscribers, + }); + return results.subscribers; +}; + +Digest.send = async function (data) { + let emailsSent = 0; + if (!data || !data.subscribers || !data.subscribers.length) { + return emailsSent; + } + let errorLogged = false; + await batch.processArray(data.subscribers, async (uids) => { + let userData = await user.getUsersFields(uids, ['uid', 'email', 'email:confirmed', 'username', 'userslug', 'lastonline']); + userData = userData.filter(u => u && u.email && (meta.config.includeUnverifiedEmails || u['email:confirmed'])); + if (!userData.length) { + return; + } + await Promise.all(userData.map(async (userObj) => { + const [notifications, topics] = await Promise.all([ + user.notifications.getUnreadInterval(userObj.uid, data.interval), + getTermTopics(data.interval, userObj.uid), + ]); + const unreadNotifs = notifications.filter(Boolean); + // If there are no notifications and no new topics, don't bother sending a digest + if (!unreadNotifs.length && !topics.top.length && !topics.popular.length && !topics.recent.length) { + return; + } + + unreadNotifs.forEach((n) => { + if (n.image && !n.image.startsWith('http')) { + n.image = baseUrl + n.image; + } + if (n.path) { + n.notification_url = n.path.startsWith('http') ? n.path : baseUrl + n.path; + } + }); + + emailsSent += 1; + const now = new Date(); + await emailer.send('digest', userObj.uid, { + subject: `[[email:digest.subject, ${now.getFullYear()}/${now.getMonth() + 1}/${now.getDate()}]]`, + username: userObj.username, + userslug: userObj.userslug, + notifications: unreadNotifs, + recent: topics.recent, + topTopics: topics.top, + popularTopics: topics.popular, + interval: data.interval, + showUnsubscribe: true, + }).catch((err) => { + if (!errorLogged) { + winston.error(`[user/jobs] Could not send digest email\n[emailer.send] ${err.stack}`); + errorLogged = true; + } + }); + })); + if (data.interval !== 'alltime') { + const now = Date.now(); + await db.sortedSetAdd('digest:delivery', userData.map(() => now), userData.map(u => u.uid)); + } + }, { + interval: 1000, + batch: 100, + }); + winston.info(`[user/jobs] Digest (${data.interval}) sending completed. ${emailsSent} emails sent.`); +}; + +Digest.getDeliveryTimes = async (start, stop) => { + const count = await db.sortedSetCard('users:joindate'); + const uids = await user.getUidsFromSet('users:joindate', start, stop); + if (!uids.length) { + return []; + } + + const [scores, settings] = await Promise.all([ + // Grab the last time a digest was successfully delivered to these uids + db.sortedSetScores('digest:delivery', uids), + // Get users' digest settings + Digest.getUsersInterval(uids), + ]); + + // Populate user data + let userData = await user.getUsersFields(uids, ['username', 'picture']); + userData = userData.map((user, idx) => { + user.lastDelivery = scores[idx] ? new Date(scores[idx]).toISOString() : '[[admin/manage/digest:null]]'; + user.setting = settings[idx]; + return user; + }); + + return { + users: userData, + count: count, + }; +}; + +async function getTermTopics(term, uid) { + const data = await topics.getSortedTopics({ + uid: uid, + start: 0, + stop: 199, + term: term, + sort: 'votes', + teaserPost: 'first', + }); + data.topics = data.topics.filter(topic => topic && !topic.deleted); + + const top = data.topics.filter(t => t.votes > 0).slice(0, 10); + const topTids = top.map(t => t.tid); + + const popular = data.topics + .filter(t => t.postcount > 1 && !topTids.includes(t.tid)) + .sort((a, b) => b.postcount - a.postcount) + .slice(0, 10); + const popularTids = popular.map(t => t.tid); + + const recent = data.topics + .filter(t => !topTids.includes(t.tid) && !popularTids.includes(t.tid)) + .sort((a, b) => b.lastposttime - a.lastposttime) + .slice(0, 10); + + [...top, ...popular, ...recent].forEach((topicObj) => { + if (topicObj) { + if (topicObj.teaser && topicObj.teaser.content && topicObj.teaser.content.length > 255) { + topicObj.teaser.content = `${topicObj.teaser.content.slice(0, 255)}...`; + } + // Fix relative paths in topic data + const user = topicObj.hasOwnProperty('teaser') && topicObj.teaser && topicObj.teaser.user ? + topicObj.teaser.user : topicObj.user; + if (user && user.picture && utils.isRelativeUrl(user.picture)) { + user.picture = baseUrl + user.picture; + } + } + }); + return { top, popular, recent }; +} diff --git a/src/user/email.js b/src/user/email.js new file mode 100644 index 0000000000..6cc363ad49 --- /dev/null +++ b/src/user/email.js @@ -0,0 +1,212 @@ + +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); + +const user = require('./index'); +const utils = require('../utils'); +const plugins = require('../plugins'); +const db = require('../database'); +const meta = require('../meta'); +const emailer = require('../emailer'); +const groups = require('../groups'); +const events = require('../events'); + +const UserEmail = module.exports; + +UserEmail.exists = async function (email) { + const uid = await user.getUidByEmail(email.toLowerCase()); + return !!uid; +}; + +UserEmail.available = async function (email) { + const exists = await db.isSortedSetMember('email:uid', email.toLowerCase()); + return !exists; +}; + +UserEmail.remove = async function (uid, sessionId) { + const email = await user.getUserField(uid, 'email'); + if (!email) { + return; + } + + await Promise.all([ + user.setUserFields(uid, { + email: '', + 'email:confirmed': 0, + }), + db.sortedSetRemove('email:uid', email.toLowerCase()), + db.sortedSetRemove('email:sorted', `${email.toLowerCase()}:${uid}`), + user.email.expireValidation(uid), + user.auth.revokeAllSessions(uid, sessionId), + events.log({ type: 'email-change', email, newEmail: '' }), + ]); +}; + +UserEmail.isValidationPending = async (uid, email) => { + const code = await db.get(`confirm:byUid:${uid}`); + + if (email) { + const confirmObj = await db.getObject(`confirm:${code}`); + return !!(confirmObj && email === confirmObj.email); + } + + return !!code; +}; + +UserEmail.getValidationExpiry = async (uid) => { + const pending = await UserEmail.isValidationPending(uid); + return pending ? db.pttl(`confirm:byUid:${uid}`) : null; +}; + +UserEmail.expireValidation = async (uid) => { + const code = await db.get(`confirm:byUid:${uid}`); + await db.deleteAll([ + `confirm:byUid:${uid}`, + `confirm:${code}`, + ]); +}; + +UserEmail.canSendValidation = async (uid, email) => { + const pending = UserEmail.isValidationPending(uid, email); + if (!pending) { + return true; + } + + const ttl = await UserEmail.getValidationExpiry(uid); + const max = meta.config.emailConfirmExpiry * 60 * 60 * 1000; + const interval = meta.config.emailConfirmInterval * 60 * 1000; + + return ttl + interval < max; +}; + +UserEmail.sendValidationEmail = async function (uid, options) { + /* + * Options: + * - email, overrides email retrieval + * - force, sends email even if it is too soon to send another + * - template, changes the template used for email sending + */ + + if (meta.config.sendValidationEmail !== 1) { + winston.verbose(`[user/email] Validation email for uid ${uid} not sent due to config settings`); + return; + } + + options = options || {}; + + // Fallback behaviour (email passed in as second argument) + if (typeof options === 'string') { + options = { + email: options, + }; + } + + const confirm_code = utils.generateUUID(); + const confirm_link = `${nconf.get('url')}/confirm/${confirm_code}`; + + const { emailConfirmInterval, emailConfirmExpiry } = meta.config; + + // If no email passed in (default), retrieve email from uid + if (!options.email || !options.email.length) { + options.email = await user.getUserField(uid, 'email'); + } + if (!options.email) { + return; + } + + if (!options.force && !await UserEmail.canSendValidation(uid, options.email)) { + throw new Error(`[[error:confirm-email-already-sent, ${emailConfirmInterval}]]`); + } + + const username = await user.getUserField(uid, 'username'); + const data = await plugins.hooks.fire('filter:user.verify', { + uid, + username, + confirm_link, + confirm_code: await plugins.hooks.fire('filter:user.verify.code', confirm_code), + email: options.email, + + subject: options.subject || '[[email:email.verify-your-email.subject]]', + template: options.template || 'verify-email', + }); + + await UserEmail.expireValidation(uid); + await db.set(`confirm:byUid:${uid}`, confirm_code); + await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 24 * 60 * 60 * 1000); + + await db.setObject(`confirm:${confirm_code}`, { + email: options.email.toLowerCase(), + uid: uid, + }); + await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 24 * 60 * 60 * 1000); + + winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`); + events.log({ + type: 'email-confirmation-sent', + uid, + confirm_code, + ...options, + }); + + if (plugins.hooks.hasListeners('action:user.verify')) { + plugins.hooks.fire('action:user.verify', { uid: uid, data: data }); + } else { + await emailer.send(data.template, uid, data); + } + return confirm_code; +}; + +// confirm email by code sent by confirmation email +UserEmail.confirmByCode = async function (code, sessionId) { + const confirmObj = await db.getObject(`confirm:${code}`); + if (!confirmObj || !confirmObj.uid || !confirmObj.email) { + throw new Error('[[error:invalid-data]]'); + } + + // If another uid has the same email, remove it + const oldUid = await db.sortedSetScore('email:uid', confirmObj.email.toLowerCase()); + if (oldUid) { + await UserEmail.remove(oldUid, sessionId); + } + + const oldEmail = await user.getUserField(confirmObj.uid, 'email'); + if (oldEmail && confirmObj.email !== oldEmail) { + await UserEmail.remove(confirmObj.uid, sessionId); + } else { + await user.auth.revokeAllSessions(confirmObj.uid, sessionId); + } + + await user.setUserField(confirmObj.uid, 'email', confirmObj.email); + await Promise.all([ + UserEmail.confirmByUid(confirmObj.uid), + db.delete(`confirm:${code}`), + events.log({ type: 'email-change', oldEmail, newEmail: confirmObj.email }), + ]); +}; + +// confirm uid's email via ACP +UserEmail.confirmByUid = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + throw new Error('[[error:invalid-uid]]'); + } + const currentEmail = await user.getUserField(uid, 'email'); + if (!currentEmail) { + throw new Error('[[error:invalid-email]]'); + } + + await Promise.all([ + db.sortedSetAddBulk([ + ['email:uid', uid, currentEmail.toLowerCase()], + ['email:sorted', 0, `${currentEmail.toLowerCase()}:${uid}`], + [`user:${uid}:emails`, Date.now(), `${currentEmail}:${Date.now()}`], + ]), + user.setUserField(uid, 'email:confirmed', 1), + groups.join('verified-users', uid), + groups.leave('unverified-users', uid), + user.email.expireValidation(uid), + user.reset.cleanByUid(uid), + ]); + await plugins.hooks.fire('action:user.email.confirmed', { uid: uid, email: currentEmail }); +}; diff --git a/src/user/follow.js b/src/user/follow.js new file mode 100644 index 0000000000..d9ea4705e1 --- /dev/null +++ b/src/user/follow.js @@ -0,0 +1,90 @@ + +'use strict'; + +const plugins = require('../plugins'); +const db = require('../database'); + +module.exports = function (User) { + User.follow = async function (uid, followuid) { + await toggleFollow('follow', uid, followuid); + }; + + User.unfollow = async function (uid, unfollowuid) { + await toggleFollow('unfollow', uid, unfollowuid); + }; + + async function toggleFollow(type, uid, theiruid) { + if (parseInt(uid, 10) <= 0 || parseInt(theiruid, 10) <= 0) { + throw new Error('[[error:invalid-uid]]'); + } + + if (parseInt(uid, 10) === parseInt(theiruid, 10)) { + throw new Error('[[error:you-cant-follow-yourself]]'); + } + const exists = await User.exists(theiruid); + if (!exists) { + throw new Error('[[error:no-user]]'); + } + const isFollowing = await User.isFollowing(uid, theiruid); + if (type === 'follow') { + if (isFollowing) { + throw new Error('[[error:already-following]]'); + } + const now = Date.now(); + await Promise.all([ + db.sortedSetAddBulk([ + [`following:${uid}`, now, theiruid], + [`followers:${theiruid}`, now, uid], + ]), + ]); + } else { + if (!isFollowing) { + throw new Error('[[error:not-following]]'); + } + await Promise.all([ + db.sortedSetRemoveBulk([ + [`following:${uid}`, theiruid], + [`followers:${theiruid}`, uid], + ]), + ]); + } + + const [followingCount, followerCount] = await Promise.all([ + db.sortedSetCard(`following:${uid}`), + db.sortedSetCard(`followers:${theiruid}`), + ]); + await Promise.all([ + User.setUserField(uid, 'followingCount', followingCount), + User.setUserField(theiruid, 'followerCount', followerCount), + ]); + } + + User.getFollowing = async function (uid, start, stop) { + return await getFollow(uid, 'following', start, stop); + }; + + User.getFollowers = async function (uid, start, stop) { + return await getFollow(uid, 'followers', start, stop); + }; + + async function getFollow(uid, type, start, stop) { + if (parseInt(uid, 10) <= 0) { + return []; + } + const uids = await db.getSortedSetRevRange(`${type}:${uid}`, start, stop); + const data = await plugins.hooks.fire(`filter:user.${type}`, { + uids: uids, + uid: uid, + start: start, + stop: stop, + }); + return await User.getUsers(data.uids, uid); + } + + User.isFollowing = async function (uid, theirid) { + if (parseInt(uid, 10) <= 0 || parseInt(theirid, 10) <= 0) { + return false; + } + return await db.isSortedSetMember(`following:${uid}`, theirid); + }; +}; diff --git a/src/user/index.js b/src/user/index.js new file mode 100644 index 0000000000..3d1594af26 --- /dev/null +++ b/src/user/index.js @@ -0,0 +1,248 @@ +'use strict'; + +const _ = require('lodash'); + +const groups = require('../groups'); +const plugins = require('../plugins'); +const db = require('../database'); +const privileges = require('../privileges'); +const categories = require('../categories'); +const meta = require('../meta'); +const utils = require('../utils'); + +const User = module.exports; + +User.email = require('./email'); +User.notifications = require('./notifications'); +User.reset = require('./reset'); +User.digest = require('./digest'); +User.interstitials = require('./interstitials'); + +require('./data')(User); +require('./auth')(User); +require('./bans')(User); +require('./create')(User); +require('./posts')(User); +require('./topics')(User); +require('./categories')(User); +require('./follow')(User); +require('./profile')(User); +require('./admin')(User); +require('./delete')(User); +require('./settings')(User); +require('./search')(User); +require('./jobs')(User); +require('./picture')(User); +require('./approval')(User); +require('./invite')(User); +require('./password')(User); +require('./info')(User); +require('./online')(User); +require('./blocks')(User); +require('./uploads')(User); + +User.exists = async function (uids) { + return await ( + Array.isArray(uids) ? + db.isSortedSetMembers('users:joindate', uids) : + db.isSortedSetMember('users:joindate', uids) + ); +}; + +User.existsBySlug = async function (userslug) { + const exists = await User.getUidByUserslug(userslug); + return !!exists; +}; + +User.getUidsFromSet = async function (set, start, stop) { + if (set === 'users:online') { + const count = parseInt(stop, 10) === -1 ? stop : stop - start + 1; + const now = Date.now(); + return await db.getSortedSetRevRangeByScore(set, start, count, '+inf', now - (meta.config.onlineCutoff * 60000)); + } + return await db.getSortedSetRevRange(set, start, stop); +}; + +User.getUsersFromSet = async function (set, uid, start, stop) { + const uids = await User.getUidsFromSet(set, start, stop); + return await User.getUsers(uids, uid); +}; + +User.getUsersWithFields = async function (uids, fields, uid) { + let results = await plugins.hooks.fire('filter:users.addFields', { fields: fields }); + results.fields = _.uniq(results.fields); + const userData = await User.getUsersFields(uids, results.fields); + results = await plugins.hooks.fire('filter:userlist.get', { users: userData, uid: uid }); + return results.users; +}; + +User.getUsers = async function (uids, uid) { + const userData = await User.getUsersWithFields(uids, [ + 'uid', 'username', 'userslug', 'accounttype', 'picture', 'status', + 'postcount', 'reputation', 'email:confirmed', 'lastonline', + 'flags', 'banned', 'banned:expire', 'joindate', + ], uid); + + return User.hidePrivateData(userData, uid); +}; + +User.getStatus = function (userData) { + if (userData.uid <= 0) { + return 'offline'; + } + const isOnline = (Date.now() - userData.lastonline) < (meta.config.onlineCutoff * 60000); + return isOnline ? (userData.status || 'online') : 'offline'; +}; + +User.getUidByUsername = async function (username) { + if (!username) { + return 0; + } + return await db.sortedSetScore('username:uid', username); +}; + +User.getUidsByUsernames = async function (usernames) { + return await db.sortedSetScores('username:uid', usernames); +}; + +User.getUidByUserslug = async function (userslug) { + if (!userslug) { + return 0; + } + return await db.sortedSetScore('userslug:uid', userslug); +}; + +User.getUsernamesByUids = async function (uids) { + const users = await User.getUsersFields(uids, ['username']); + return users.map(user => user.username); +}; + +User.getUsernameByUserslug = async function (slug) { + const uid = await User.getUidByUserslug(slug); + return await User.getUserField(uid, 'username'); +}; + +User.getUidByEmail = async function (email) { + return await db.sortedSetScore('email:uid', email.toLowerCase()); +}; + +User.getUidsByEmails = async function (emails) { + emails = emails.map(email => email && email.toLowerCase()); + return await db.sortedSetScores('email:uid', emails); +}; + +User.getUsernameByEmail = async function (email) { + const uid = await db.sortedSetScore('email:uid', String(email).toLowerCase()); + return await User.getUserField(uid, 'username'); +}; + +User.isModerator = async function (uid, cid) { + return await privileges.users.isModerator(uid, cid); +}; + +User.isModeratorOfAnyCategory = async function (uid) { + const cids = await User.getModeratedCids(uid); + return Array.isArray(cids) ? !!cids.length : false; +}; + +User.isAdministrator = async function (uid) { + return await privileges.users.isAdministrator(uid); +}; + +User.isGlobalModerator = async function (uid) { + return await privileges.users.isGlobalModerator(uid); +}; + +User.getPrivileges = async function (uid) { + return await utils.promiseParallel({ + isAdmin: User.isAdministrator(uid), + isGlobalModerator: User.isGlobalModerator(uid), + isModeratorOfAnyCategory: User.isModeratorOfAnyCategory(uid), + }); +}; + +User.isPrivileged = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return false; + } + const results = await User.getPrivileges(uid); + return results ? (results.isAdmin || results.isGlobalModerator || results.isModeratorOfAnyCategory) : false; +}; + +User.isAdminOrGlobalMod = async function (uid) { + const [isAdmin, isGlobalMod] = await Promise.all([ + User.isAdministrator(uid), + User.isGlobalModerator(uid), + ]); + return isAdmin || isGlobalMod; +}; + +User.isAdminOrSelf = async function (callerUid, uid) { + await isSelfOrMethod(callerUid, uid, User.isAdministrator); +}; + +User.isAdminOrGlobalModOrSelf = async function (callerUid, uid) { + await isSelfOrMethod(callerUid, uid, User.isAdminOrGlobalMod); +}; + +User.isPrivilegedOrSelf = async function (callerUid, uid) { + await isSelfOrMethod(callerUid, uid, User.isPrivileged); +}; + +async function isSelfOrMethod(callerUid, uid, method) { + if (parseInt(callerUid, 10) === parseInt(uid, 10)) { + return; + } + const isPass = await method(callerUid); + if (!isPass) { + throw new Error('[[error:no-privileges]]'); + } +} + +User.getAdminsandGlobalMods = async function () { + const results = await groups.getMembersOfGroups(['administrators', 'Global Moderators']); + return await User.getUsersData(_.union(...results)); +}; + +User.getAdminsandGlobalModsandModerators = async function () { + const results = await Promise.all([ + groups.getMembers('administrators', 0, -1), + groups.getMembers('Global Moderators', 0, -1), + User.getModeratorUids(), + ]); + return await User.getUsersData(_.union(...results)); +}; + +User.getFirstAdminUid = async function () { + return (await db.getSortedSetRange('group:administrators:members', 0, 0))[0]; +}; + +User.getModeratorUids = async function () { + const cids = await categories.getAllCidsFromSet('categories:cid'); + const uids = await categories.getModeratorUids(cids); + return _.union(...uids); +}; + +User.getModeratedCids = async function (uid) { + if (parseInt(uid, 10) <= 0) { + return []; + } + const cids = await categories.getAllCidsFromSet('categories:cid'); + const isMods = await User.isModerator(uid, cids); + return cids.filter((cid, index) => cid && isMods[index]); +}; + +User.addInterstitials = function (callback) { + plugins.hooks.register('core', { + hook: 'filter:register.interstitial', + method: [ + User.interstitials.email, // Email address (for password reset + digest) + User.interstitials.gdpr, // GDPR information collection/processing consent + email consent + User.interstitials.tou, // Forum Terms of Use + ], + }); + + callback(); +}; + +require('../promisify')(User); diff --git a/src/user/info.js b/src/user/info.js new file mode 100644 index 0000000000..45e28c4cbc --- /dev/null +++ b/src/user/info.js @@ -0,0 +1,144 @@ +'use strict'; + +const _ = require('lodash'); +const validator = require('validator'); + +const db = require('../database'); +const posts = require('../posts'); +const topics = require('../topics'); +const utils = require('../utils'); + +module.exports = function (User) { + User.getLatestBanInfo = async function (uid) { + // Simply retrieves the last record of the user's ban, even if they've been unbanned since then. + const record = await db.getSortedSetRevRange(`uid:${uid}:bans:timestamp`, 0, 0); + if (!record.length) { + throw new Error('no-ban-info'); + } + const banInfo = await db.getObject(record[0]); + const expire = parseInt(banInfo.expire, 10); + const expire_readable = utils.toISOString(expire); + return { + uid: uid, + timestamp: banInfo.timestamp, + banned_until: expire, + expiry: expire, /* backward compatible alias */ + banned_until_readable: expire_readable, + expiry_readable: expire_readable, /* backward compatible alias */ + reason: validator.escape(String(banInfo.reason || '')), + }; + }; + + User.getModerationHistory = async function (uid) { + let [flags, bans, mutes] = await Promise.all([ + db.getSortedSetRevRangeWithScores(`flags:byTargetUid:${uid}`, 0, 19), + db.getSortedSetRevRange(`uid:${uid}:bans:timestamp`, 0, 19), + db.getSortedSetRevRange(`uid:${uid}:mutes:timestamp`, 0, 19), + ]); + + // Get pids from flag objects + const keys = flags.map(flagObj => `flag:${flagObj.value}`); + const payload = await db.getObjectsFields(keys, ['type', 'targetId']); + + // Only pass on flag ids from posts + flags = payload.reduce((memo, cur, idx) => { + if (cur.type === 'post') { + memo.push({ + value: parseInt(cur.targetId, 10), + score: flags[idx].score, + }); + } + + return memo; + }, []); + + [flags, bans, mutes] = await Promise.all([ + getFlagMetadata(flags), + formatBanMuteData(bans, '[[user:info.banned-no-reason]]'), + formatBanMuteData(mutes, '[[user:info.muted-no-reason]]'), + ]); + + return { + flags: flags, + bans: bans, + mutes: mutes, + }; + }; + + User.getHistory = async function (set) { + const data = await db.getSortedSetRevRangeWithScores(set, 0, -1); + return data.map((set) => { + set.timestamp = set.score; + set.timestampISO = utils.toISOString(set.score); + set.value = validator.escape(String(set.value.split(':')[0])); + delete set.score; + return set; + }); + }; + + async function getFlagMetadata(flags) { + const pids = flags.map(flagObj => parseInt(flagObj.value, 10)); + const postData = await posts.getPostsFields(pids, ['tid']); + const tids = postData.map(post => post.tid); + + const topicData = await topics.getTopicsFields(tids, ['title']); + flags = flags.map((flagObj, idx) => { + flagObj.pid = flagObj.value; + flagObj.timestamp = flagObj.score; + flagObj.timestampISO = new Date(flagObj.score).toISOString(); + flagObj.timestampReadable = new Date(flagObj.score).toString(); + + delete flagObj.value; + delete flagObj.score; + if (!tids[idx]) { + flagObj.targetPurged = true; + } + return _.extend(flagObj, topicData[idx]); + }); + return flags; + } + + async function formatBanMuteData(keys, noReasonLangKey) { + const data = await db.getObjects(keys); + const uids = data.map(d => d.fromUid); + const usersData = await User.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']); + return data.map((banObj, index) => { + banObj.user = usersData[index]; + banObj.until = parseInt(banObj.expire, 10); + banObj.untilReadable = new Date(banObj.until).toString(); + banObj.timestampReadable = new Date(parseInt(banObj.timestamp, 10)).toString(); + banObj.timestampISO = utils.toISOString(banObj.timestamp); + banObj.reason = validator.escape(String(banObj.reason || '')) || noReasonLangKey; + return banObj; + }); + } + + User.getModerationNotes = async function (uid, start, stop) { + const noteIds = await db.getSortedSetRevRange(`uid:${uid}:moderation:notes`, start, stop); + const keys = noteIds.map(id => `uid:${uid}:moderation:note:${id}`); + const notes = await db.getObjects(keys); + const uids = []; + + const noteData = notes.map((note) => { + if (note) { + uids.push(note.uid); + note.timestampISO = utils.toISOString(note.timestamp); + note.note = validator.escape(String(note.note)); + } + return note; + }); + + const userData = await User.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']); + noteData.forEach((note, index) => { + if (note) { + note.user = userData[index]; + } + }); + return noteData; + }; + + User.appendModerationNote = async ({ uid, noteData }) => { + await db.sortedSetAdd(`uid:${uid}:moderation:notes`, noteData.timestamp, noteData.timestamp); + await db.setObject(`uid:${uid}:moderation:note:${noteData.timestamp}`, noteData); + }; +}; diff --git a/src/user/interstitials.js b/src/user/interstitials.js new file mode 100644 index 0000000000..b7903d0ffc --- /dev/null +++ b/src/user/interstitials.js @@ -0,0 +1,198 @@ +'use strict'; + +const winston = require('winston'); +const util = require('util'); + +const user = require('.'); +const db = require('../database'); +const meta = require('../meta'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const sleep = util.promisify(setTimeout); + +const Interstitials = module.exports; + +Interstitials.email = async (data) => { + if (!data.userData) { + throw new Error('[[error:invalid-data]]'); + } + if (!data.userData.updateEmail) { + return data; + } + + const [isAdminOrGlobalMod, hasPassword] = await Promise.all([ + user.isAdminOrGlobalMod(data.req.uid), + user.hasPassword(data.userData.uid), + ]); + + let email; + if (data.userData.uid) { + email = await user.getUserField(data.userData.uid, 'email'); + } + + data.interstitials.push({ + template: 'partials/email_update', + data: { + email, + requireEmailAddress: meta.config.requireEmailAddress, + issuePasswordChallenge: !!data.userData.uid && hasPassword, + }, + callback: async (userData, formData) => { + // Validate and send email confirmation + if (userData.uid) { + const [isPasswordCorrect, canEdit, { email: current, 'email:confirmed': confirmed }, { allowed, error }] = await Promise.all([ + user.isPasswordCorrect(userData.uid, formData.password, data.req.ip), + privileges.users.canEdit(data.req.uid, userData.uid), + user.getUserFields(userData.uid, ['email', 'email:confirmed']), + plugins.hooks.fire('filter:user.saveEmail', { + uid: userData.uid, + email: formData.email, + registration: false, + allowed: true, // change this value to disallow + error: '[[error:invalid-email]]', + }), + ]); + + if (!isAdminOrGlobalMod && !isPasswordCorrect) { + await sleep(2000); + } + + if (formData.email && formData.email.length) { + if (!allowed || !utils.isEmailValid(formData.email)) { + throw new Error(error); + } + + // Handle errors when setting to same email (unconfirmed accts only) + if (formData.email === current) { + if (confirmed) { + throw new Error('[[error:email-nochange]]'); + } else if (await user.email.canSendValidation(userData.uid, current)) { + throw new Error(`[[error:confirm-email-already-sent, ${meta.config.emailConfirmInterval}]]`); + } + } + + // Admins editing will auto-confirm, unless editing their own email + if (isAdminOrGlobalMod && userData.uid !== data.req.uid) { + await user.setUserField(userData.uid, 'email', formData.email); + await user.email.confirmByUid(userData.uid); + } else if (canEdit) { + if (hasPassword && !isPasswordCorrect) { + throw new Error('[[error:invalid-password]]'); + } + + await user.email.sendValidationEmail(userData.uid, { + email: formData.email, + force: true, + }).catch((err) => { + winston.error(`[user.interstitials.email] Validation email failed to send\n[emailer.send] ${err.stack}`); + }); + data.req.session.emailChanged = 1; + } else { + // User attempting to edit another user's email -- not allowed + throw new Error('[[error:no-privileges]]'); + } + } else { + if (meta.config.requireEmailAddress) { + throw new Error('[[error:invalid-email]]'); + } + + if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) { + // User explicitly clearing their email + await user.email.remove(userData.uid, data.req.session.id); + } + } + } else { + const { allowed, error } = await plugins.hooks.fire('filter:user.saveEmail', { + uid: null, + email: formData.email, + registration: true, + allowed: true, // change this value to disallow + error: '[[error:invalid-email]]', + }); + + if (!allowed || (meta.config.requireEmailAddress && !(formData.email && formData.email.length))) { + throw new Error(error); + } + + // New registrants have the confirm email sent from user.create() + userData.email = formData.email; + } + + delete userData.updateEmail; + }, + }); + + return data; +}; + +Interstitials.gdpr = async function (data) { + if (!meta.config.gdpr_enabled || (data.userData && data.userData.gdpr_consent)) { + return data; + } + if (!data.userData) { + throw new Error('[[error:invalid-data]]'); + } + + if (data.userData.uid) { + const consented = await db.getObjectField(`user:${data.userData.uid}`, 'gdpr_consent'); + if (parseInt(consented, 10)) { + return data; + } + } + + data.interstitials.push({ + template: 'partials/gdpr_consent', + data: { + digestFrequency: meta.config.dailyDigestFreq, + digestEnabled: meta.config.dailyDigestFreq !== 'off', + }, + callback: function (userData, formData, next) { + if (formData.gdpr_agree_data === 'on' && formData.gdpr_agree_email === 'on') { + userData.gdpr_consent = true; + } + + next(userData.gdpr_consent ? null : new Error('[[register:gdpr_consent_denied]]')); + }, + }); + return data; +}; + +Interstitials.tou = async function (data) { + if (!data.userData) { + throw new Error('[[error:invalid-data]]'); + } + if (!meta.config.termsOfUse || data.userData.acceptTos) { + // no ToS or ToS accepted, nothing to do + return data; + } + + if (data.userData.uid) { + const accepted = await db.getObjectField(`user:${data.userData.uid}`, 'acceptTos'); + if (parseInt(accepted, 10)) { + return data; + } + } + + const termsOfUse = await plugins.hooks.fire('filter:parse.post', { + postData: { + content: meta.config.termsOfUse || '', + }, + }); + + data.interstitials.push({ + template: 'partials/acceptTos', + data: { + termsOfUse: termsOfUse.postData.content, + }, + callback: function (userData, formData, next) { + if (formData['agree-terms'] === 'on') { + userData.acceptTos = true; + } + + next(userData.acceptTos ? null : new Error('[[register:terms_of_use_error]]')); + }, + }); + return data; +}; diff --git a/src/user/invite.js b/src/user/invite.js new file mode 100644 index 0000000000..c69daf9b79 --- /dev/null +++ b/src/user/invite.js @@ -0,0 +1,187 @@ + +'use strict'; + +const async = require('async'); +const nconf = require('nconf'); +const validator = require('validator'); + +const db = require('../database'); +const meta = require('../meta'); +const emailer = require('../emailer'); +const groups = require('../groups'); +const translator = require('../translator'); +const utils = require('../utils'); +const plugins = require('../plugins'); + +module.exports = function (User) { + User.getInvites = async function (uid) { + const emails = await db.getSetMembers(`invitation:uid:${uid}`); + return emails.map(email => validator.escape(String(email))); + }; + + User.getInvitesNumber = async function (uid) { + return await db.setCount(`invitation:uid:${uid}`); + }; + + User.getInvitingUsers = async function () { + return await db.getSetMembers('invitation:uids'); + }; + + User.getAllInvites = async function () { + const uids = await User.getInvitingUsers(); + const invitations = await async.map(uids, User.getInvites); + return invitations.map((invites, index) => ({ + uid: uids[index], + invitations: invites, + })); + }; + + User.sendInvitationEmail = async function (uid, email, groupsToJoin) { + if (!uid) { + throw new Error('[[error:invalid-uid]]'); + } + + const email_exists = await User.getUidByEmail(email); + if (email_exists) { + // Silently drop the invitation if the invited email already exists locally + return true; + } + + const invitation_exists = await db.exists(`invitation:uid:${uid}:invited:${email}`); + if (invitation_exists) { + throw new Error('[[error:email-invited]]'); + } + + const data = await prepareInvitation(uid, email, groupsToJoin); + await emailer.sendToEmail('invitation', email, meta.config.defaultLang, data); + plugins.hooks.fire('action:user.invite', { uid, email, groupsToJoin }); + }; + + User.verifyInvitation = async function (query) { + if (!query.token) { + if (meta.config.registrationType.startsWith('admin-')) { + throw new Error('[[register:invite.error-admin-only]]'); + } else { + throw new Error('[[register:invite.error-invite-only]]'); + } + } + const token = await db.getObjectField(`invitation:token:${query.token}`, 'token'); + if (!token || token !== query.token) { + throw new Error('[[register:invite.error-invalid-data]]'); + } + }; + + User.confirmIfInviteEmailIsUsed = async function (token, enteredEmail, uid) { + if (!enteredEmail) { + return; + } + const email = await db.getObjectField(`invitation:token:${token}`, 'email'); + // "Confirm" user's email if registration completed with invited address + if (email && email === enteredEmail) { + await User.email.confirmByUid(uid); + } + }; + + User.joinGroupsFromInvitation = async function (uid, token) { + let groupsToJoin = await db.getObjectField(`invitation:token:${token}`, 'groupsToJoin'); + + try { + groupsToJoin = JSON.parse(groupsToJoin); + } catch (e) { + return; + } + + if (!groupsToJoin || groupsToJoin.length < 1) { + return; + } + + await groups.join(groupsToJoin, uid); + }; + + User.deleteInvitation = async function (invitedBy, email) { + const invitedByUid = await User.getUidByUsername(invitedBy); + if (!invitedByUid) { + throw new Error('[[error:invalid-username]]'); + } + const token = await db.get(`invitation:uid:${invitedByUid}:invited:${email}`); + await Promise.all([ + deleteFromReferenceList(invitedByUid, email), + db.setRemove(`invitation:invited:${email}`, token), + db.delete(`invitation:token:${token}`), + ]); + }; + + User.deleteInvitationKey = async function (registrationEmail, token) { + if (registrationEmail) { + const uids = await User.getInvitingUsers(); + await Promise.all(uids.map(uid => deleteFromReferenceList(uid, registrationEmail))); + // Delete all invites to an email address if it has joined + const tokens = await db.getSetMembers(`invitation:invited:${registrationEmail}`); + const keysToDelete = [`invitation:invited:${registrationEmail}`].concat(tokens.map(token => `invitation:token:${token}`)); + await db.deleteAll(keysToDelete); + } + if (token) { + const invite = await db.getObject(`invitation:token:${token}`); + if (!invite) { + return; + } + await deleteFromReferenceList(invite.inviter, invite.email); + await db.deleteAll([ + `invitation:invited:${invite.email}`, + `invitation:token:${token}`, + ]); + } + }; + + async function deleteFromReferenceList(uid, email) { + await Promise.all([ + db.setRemove(`invitation:uid:${uid}`, email), + db.delete(`invitation:uid:${uid}:invited:${email}`), + ]); + const count = await db.setCount(`invitation:uid:${uid}`); + if (count === 0) { + await db.setRemove('invitation:uids', uid); + } + } + + async function prepareInvitation(uid, email, groupsToJoin) { + const inviterExists = await User.exists(uid); + if (!inviterExists) { + throw new Error('[[error:invalid-uid]]'); + } + + const token = utils.generateUUID(); + const registerLink = `${nconf.get('url')}/register?token=${token}`; + + const expireDays = meta.config.inviteExpiration; + const expireIn = expireDays * 86400000; + + await db.setAdd(`invitation:uid:${uid}`, email); + await db.setAdd('invitation:uids', uid); + // Referencing from uid and email to token + await db.set(`invitation:uid:${uid}:invited:${email}`, token); + // Keeping references for all invites to this email address + await db.setAdd(`invitation:invited:${email}`, token); + await db.setObject(`invitation:token:${token}`, { + email, + token, + groupsToJoin: JSON.stringify(groupsToJoin), + inviter: uid, + }); + await db.pexpireAt(`invitation:token:${token}`, Date.now() + expireIn); + + const username = await User.getUserField(uid, 'username'); + const title = meta.config.title || meta.config.browserTitle || 'NodeBB'; + const subject = await translator.translate(`[[email:invite, ${title}]]`, meta.config.defaultLang); + + return { + ...emailer._defaultPayload, // Append default data to this email payload + site_title: title, + registerLink: registerLink, + subject: subject, + username: username, + template: 'invitation', + expireDays: expireDays, + }; + } +}; diff --git a/src/user/jobs.js b/src/user/jobs.js new file mode 100644 index 0000000000..2b244e4c3e --- /dev/null +++ b/src/user/jobs.js @@ -0,0 +1,66 @@ +'use strict'; + +const winston = require('winston'); +const cronJob = require('cron').CronJob; +const db = require('../database'); +const meta = require('../meta'); + +const jobs = {}; + +module.exports = function (User) { + User.startJobs = function () { + winston.verbose('[user/jobs] (Re-)starting jobs...'); + + let { digestHour } = meta.config; + + // Fix digest hour if invalid + if (isNaN(digestHour)) { + digestHour = 17; + } else if (digestHour > 23 || digestHour < 0) { + digestHour = 0; + } + + User.stopJobs(); + + startDigestJob('digest.daily', `0 ${digestHour} * * *`, 'day'); + startDigestJob('digest.weekly', `0 ${digestHour} * * 0`, 'week'); + startDigestJob('digest.monthly', `0 ${digestHour} 1 * *`, 'month'); + + jobs['reset.clean'] = new cronJob('0 0 * * *', User.reset.clean, null, true); + winston.verbose('[user/jobs] Starting job (reset.clean)'); + + winston.verbose(`[user/jobs] jobs started`); + }; + + function startDigestJob(name, cronString, term) { + jobs[name] = new cronJob(cronString, (async () => { + winston.verbose(`[user/jobs] Digest job (${name}) started.`); + try { + if (name === 'digest.weekly') { + const counter = await db.increment('biweeklydigestcounter'); + if (counter % 2) { + await User.digest.execute({ interval: 'biweek' }); + } + } + await User.digest.execute({ interval: term }); + } catch (err) { + winston.error(err.stack); + } + }), null, true); + winston.verbose(`[user/jobs] Starting job (${name})`); + } + + User.stopJobs = function () { + let terminated = 0; + // Terminate any active cron jobs + for (const jobId of Object.keys(jobs)) { + winston.verbose(`[user/jobs] Terminating job (${jobId})`); + jobs[jobId].stop(); + delete jobs[jobId]; + terminated += 1; + } + if (terminated > 0) { + winston.verbose(`[user/jobs] ${terminated} jobs terminated`); + } + }; +}; diff --git a/src/user/jobs/export-posts.js b/src/user/jobs/export-posts.js new file mode 100644 index 0000000000..c07a089eb8 --- /dev/null +++ b/src/user/jobs/export-posts.js @@ -0,0 +1,56 @@ +'use strict'; + +const nconf = require('nconf'); + +nconf.argv().env({ + separator: '__', +}); + +const fs = require('fs'); +const path = require('path'); +const json2csvAsync = require('json2csv').parseAsync; + +process.env.NODE_ENV = process.env.NODE_ENV || 'production'; + +// Alternate configuration file support +const configFile = path.resolve(__dirname, '../../../', nconf.any(['config', 'CONFIG']) || 'config.json'); +const prestart = require('../../prestart'); + +prestart.loadConfig(configFile); +prestart.setupWinston(); + +const db = require('../../database'); +const batch = require('../../batch'); + +process.on('message', async (msg) => { + if (msg && msg.uid) { + await db.init(); + + const targetUid = msg.uid; + const filePath = path.join(__dirname, '../../../build/export', `${targetUid}_posts.csv`); + + const posts = require('../../posts'); + + let payload = []; + await batch.processSortedSet(`uid:${targetUid}:posts`, async (pids) => { + let postData = await posts.getPostsData(pids); + // Remove empty post references and convert newlines in content + postData = postData.filter(Boolean).map((post) => { + post.content = `"${String(post.content || '').replace(/\n/g, '\\n').replace(/"/g, '\\"')}"`; + return post; + }); + payload = payload.concat(postData); + }, { + batch: 500, + interval: 1000, + }); + + const fields = payload.length ? Object.keys(payload[0]) : []; + const opts = { fields }; + const csv = await json2csvAsync(payload, opts); + await fs.promises.writeFile(filePath, csv); + + await db.close(); + process.exit(0); + } +}); diff --git a/src/user/jobs/export-profile.js b/src/user/jobs/export-profile.js new file mode 100644 index 0000000000..eb36b0e7f1 --- /dev/null +++ b/src/user/jobs/export-profile.js @@ -0,0 +1,124 @@ +'use strict'; + +const nconf = require('nconf'); + +nconf.argv().env({ + separator: '__', +}); + +const fs = require('fs'); +const path = require('path'); +const _ = require('lodash'); + +process.env.NODE_ENV = process.env.NODE_ENV || 'production'; + +// Alternate configuration file support +const configFile = path.resolve(__dirname, '../../../', nconf.any(['config', 'CONFIG']) || 'config.json'); +const prestart = require('../../prestart'); + +prestart.loadConfig(configFile); +prestart.setupWinston(); + +const db = require('../../database'); +const batch = require('../../batch'); + +process.on('message', async (msg) => { + if (msg && msg.uid) { + await db.init(); + await db.initSessionStore(); + + const targetUid = msg.uid; + + const profileFile = `${targetUid}_profile.json`; + const profilePath = path.join(__dirname, '../../../build/export', profileFile); + + const user = require('../index'); + const [ + userData, + userSettings, + ips, + sessions, + usernames, + emails, + bookmarks, + watchedTopics, + upvoted, + downvoted, + following, + ] = await Promise.all([ + db.getObject(`user:${targetUid}`), + db.getObject(`user:${targetUid}:settings`), + user.getIPs(targetUid, 9), + user.auth.getSessions(targetUid), + user.getHistory(`user:${targetUid}:usernames`), + user.getHistory(`user:${targetUid}:emails`), + getSetData(`uid:${targetUid}:bookmarks`, 'post:', targetUid), + getSetData(`uid:${targetUid}:followed_tids`, 'topic:', targetUid), + getSetData(`uid:${targetUid}:upvote`, 'post:', targetUid), + getSetData(`uid:${targetUid}:downvote`, 'post:', targetUid), + getSetData(`following:${targetUid}`, 'user:', targetUid), + ]); + delete userData.password; + + let chatData = []; + await batch.processSortedSet(`uid:${targetUid}:chat:rooms`, async (roomIds) => { + const result = await Promise.all(roomIds.map(roomId => getRoomMessages(targetUid, roomId))); + chatData = chatData.concat(_.flatten(result)); + }, { batch: 100, interval: 1000 }); + + await fs.promises.writeFile(profilePath, JSON.stringify({ + user: userData, + settings: userSettings, + ips: ips, + sessions: sessions, + usernames: usernames, + emails: emails, + messages: chatData, + bookmarks: bookmarks, + watchedTopics: watchedTopics, + upvoted: upvoted, + downvoted: downvoted, + following: following, + }, null, 4)); + + await db.close(); + process.exit(0); + } +}); + +async function getRoomMessages(uid, roomId) { + const batch = require('../../batch'); + let data = []; + await batch.processSortedSet(`uid:${uid}:chat:room:${roomId}:mids`, async (mids) => { + const messageData = await db.getObjects(mids.map(mid => `message:${mid}`)); + data = data.concat( + messageData + .filter(m => m && m.fromuid === uid && !m.system) + .map(m => ({ content: m.content, timestamp: m.timestamp })) + ); + }, { batch: 500, interval: 1000 }); + return data; +} + +async function getSetData(set, keyPrefix, uid) { + const privileges = require('../../privileges'); + const batch = require('../../batch'); + let data = []; + await batch.processSortedSet(set, async (ids) => { + if (keyPrefix === 'post:') { + ids = await privileges.posts.filter('topics:read', ids, uid); + } else if (keyPrefix === 'topic:') { + ids = await privileges.topics.filterTids('topics:read', ids, uid); + } + let objData = await db.getObjects(ids.map(id => keyPrefix + id)); + if (keyPrefix === 'post:') { + objData = objData.map(o => _.pick(o, ['pid', 'content', 'timestamp'])); + } else if (keyPrefix === 'topic:') { + objData = objData.map(o => _.pick(o, ['tid', 'title', 'timestamp'])); + } else if (keyPrefix === 'user:') { + objData = objData.map(o => _.pick(o, ['uid', 'username'])); + } + data = data.concat(objData); + }, { batch: 500, interval: 1000 }); + return data; +} diff --git a/src/user/jobs/export-uploads.js b/src/user/jobs/export-uploads.js new file mode 100644 index 0000000000..11569e6aed --- /dev/null +++ b/src/user/jobs/export-uploads.js @@ -0,0 +1,87 @@ +'use strict'; + +const nconf = require('nconf'); + +nconf.argv().env({ + separator: '__', +}); + +const fs = require('fs'); +const path = require('path'); +const archiver = require('archiver'); +const winston = require('winston'); + +process.env.NODE_ENV = process.env.NODE_ENV || 'production'; + +// Alternate configuration file support +const configFile = path.resolve(__dirname, '../../../', nconf.any(['config', 'CONFIG']) || 'config.json'); +const prestart = require('../../prestart'); + +prestart.loadConfig(configFile); +prestart.setupWinston(); + +const db = require('../../database'); + +process.on('message', async (msg) => { + if (msg && msg.uid) { + await db.init(); + + const targetUid = msg.uid; + + const archivePath = path.join(__dirname, '../../../build/export', `${targetUid}_uploads.zip`); + const rootDirectory = path.join(__dirname, '../../../public/uploads/'); + + const user = require('../index'); + + const archive = archiver('zip', { + zlib: { level: 9 }, // Sets the compression level. + }); + + archive.on('warning', (err) => { + switch (err.code) { + case 'ENOENT': + winston.warn(`[user/export/uploads] File not found: ${err.path}`); + break; + + default: + winston.warn(`[user/export/uploads] Unexpected warning: ${err.message}`); + break; + } + }); + + archive.on('error', (err) => { + const trimPath = function (path) { + return path.replace(rootDirectory, ''); + }; + switch (err.code) { + case 'EACCES': + winston.error(`[user/export/uploads] File inaccessible: ${trimPath(err.path)}`); + break; + + default: + winston.error(`[user/export/uploads] Unable to construct archive: ${err.message}`); + break; + } + }); + + const output = fs.createWriteStream(archivePath); + output.on('close', async () => { + await db.close(); + process.exit(0); + }); + + archive.pipe(output); + winston.verbose(`[user/export/uploads] Collating uploads for uid ${targetUid}`); + await user.collateUploads(targetUid, archive); + + const uploadedPicture = await user.getUserField(targetUid, 'uploadedpicture'); + if (uploadedPicture) { + const filePath = uploadedPicture.replace(nconf.get('upload_url'), ''); + archive.file(path.join(nconf.get('upload_path'), filePath), { + name: path.basename(filePath), + }); + } + + archive.finalize(); + } +}); diff --git a/src/user/notifications.js b/src/user/notifications.js new file mode 100644 index 0000000000..de19e56f5d --- /dev/null +++ b/src/user/notifications.js @@ -0,0 +1,233 @@ + +'use strict'; + +const winston = require('winston'); +const _ = require('lodash'); + +const db = require('../database'); +const meta = require('../meta'); +const notifications = require('../notifications'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +const UserNotifications = module.exports; + +UserNotifications.get = async function (uid) { + if (parseInt(uid, 10) <= 0) { + return { read: [], unread: [] }; + } + + let unread = await getNotificationsFromSet(`uid:${uid}:notifications:unread`, uid, 0, 49); + unread = unread.filter(Boolean); + let read = []; + if (unread.length < 50) { + read = await getNotificationsFromSet(`uid:${uid}:notifications:read`, uid, 0, 49 - unread.length); + } + + return await plugins.hooks.fire('filter:user.notifications.get', { + uid, + read: read.filter(Boolean), + unread: unread, + }); +}; + +async function filterNotifications(nids, filter) { + if (!filter) { + return nids; + } + const keys = nids.map(nid => `notifications:${nid}`); + const notifications = await db.getObjectsFields(keys, ['nid', 'type']); + return notifications.filter(n => n && n.nid && n.type === filter).map(n => n.nid); +} + +UserNotifications.getAll = async function (uid, filter) { + let nids = await db.getSortedSetRevRange([ + `uid:${uid}:notifications:unread`, + `uid:${uid}:notifications:read`, + ], 0, -1); + nids = _.uniq(nids); + const exists = await db.isSortedSetMembers('notifications', nids); + const deleteNids = []; + + nids = nids.filter((nid, index) => { + if (!nid || !exists[index]) { + deleteNids.push(nid); + } + return nid && exists[index]; + }); + + await deleteUserNids(deleteNids, uid); + return await filterNotifications(nids, filter); +}; + +async function deleteUserNids(nids, uid) { + await db.sortedSetRemove([ + `uid:${uid}:notifications:read`, + `uid:${uid}:notifications:unread`, + ], nids); +} + +async function getNotificationsFromSet(set, uid, start, stop) { + const nids = await db.getSortedSetRevRange(set, start, stop); + return await UserNotifications.getNotifications(nids, uid); +} + +UserNotifications.getNotifications = async function (nids, uid) { + if (!Array.isArray(nids) || !nids.length) { + return []; + } + + const [notifObjs, hasRead] = await Promise.all([ + notifications.getMultiple(nids), + db.isSortedSetMembers(`uid:${uid}:notifications:read`, nids), + ]); + + const deletedNids = []; + let notificationData = notifObjs.filter((notification, index) => { + if (!notification || !notification.nid) { + deletedNids.push(nids[index]); + } + if (notification) { + notification.read = hasRead[index]; + notification.readClass = !notification.read ? 'unread' : ''; + } + + return notification; + }); + + await deleteUserNids(deletedNids, uid); + notificationData = await notifications.merge(notificationData); + const result = await plugins.hooks.fire('filter:user.notifications.getNotifications', { + uid: uid, + notifications: notificationData, + }); + return result && result.notifications; +}; + +UserNotifications.getUnreadInterval = async function (uid, interval) { + const dayInMs = 1000 * 60 * 60 * 24; + const times = { + day: dayInMs, + week: 7 * dayInMs, + month: 30 * dayInMs, + }; + if (!times[interval]) { + return []; + } + const min = Date.now() - times[interval]; + const nids = await db.getSortedSetRevRangeByScore(`uid:${uid}:notifications:unread`, 0, 20, '+inf', min); + return await UserNotifications.getNotifications(nids, uid); +}; + +UserNotifications.getDailyUnread = async function (uid) { + return await UserNotifications.getUnreadInterval(uid, 'day'); +}; + +UserNotifications.getUnreadCount = async function (uid) { + if (parseInt(uid, 10) <= 0) { + return 0; + } + let nids = await db.getSortedSetRevRange(`uid:${uid}:notifications:unread`, 0, 99); + nids = await notifications.filterExists(nids); + const keys = nids.map(nid => `notifications:${nid}`); + const notifData = await db.getObjectsFields(keys, ['mergeId']); + const mergeIds = notifData.map(n => n.mergeId); + + // Collapse any notifications with identical mergeIds + let count = mergeIds.reduce((count, mergeId, idx, arr) => { + // A missing (null) mergeId means that notification is counted separately. + if (mergeId === null || idx === arr.indexOf(mergeId)) { + count += 1; + } + + return count; + }, 0); + + ({ count } = await plugins.hooks.fire('filter:user.notifications.getCount', { uid, count })); + return count; +}; + +UserNotifications.getUnreadByField = async function (uid, field, values) { + const nids = await db.getSortedSetRevRange(`uid:${uid}:notifications:unread`, 0, 99); + if (!nids.length) { + return []; + } + const keys = nids.map(nid => `notifications:${nid}`); + const notifData = await db.getObjectsFields(keys, ['nid', field]); + const valuesSet = new Set(values.map(value => String(value))); + return notifData.filter(n => n && n[field] && valuesSet.has(String(n[field]))).map(n => n.nid); +}; + +UserNotifications.deleteAll = async function (uid) { + if (parseInt(uid, 10) <= 0) { + return; + } + await db.deleteAll([ + `uid:${uid}:notifications:unread`, + `uid:${uid}:notifications:read`, + ]); +}; + +UserNotifications.sendTopicNotificationToFollowers = async function (uid, topicData, postData) { + try { + let followers = await db.getSortedSetRange(`followers:${uid}`, 0, -1); + followers = await privileges.categories.filterUids('read', topicData.cid, followers); + if (!followers.length) { + return; + } + let { title } = topicData; + if (title) { + title = utils.decodeHTMLEntities(title); + title = title.replace(/,/g, '\\,'); + } + + const notifObj = await notifications.create({ + type: 'new-topic', + bodyShort: `[[notifications:user_posted_topic, ${postData.user.displayname}, ${title}]]`, + bodyLong: postData.content, + pid: postData.pid, + path: `/post/${postData.pid}`, + nid: `tid:${postData.tid}:uid:${uid}`, + tid: postData.tid, + from: uid, + }); + + await notifications.push(notifObj, followers); + } catch (err) { + winston.error(err.stack); + } +}; + +UserNotifications.sendWelcomeNotification = async function (uid) { + if (!meta.config.welcomeNotification) { + return; + } + + const path = meta.config.welcomeLink ? meta.config.welcomeLink : '#'; + const notifObj = await notifications.create({ + bodyShort: meta.config.welcomeNotification, + path: path, + nid: `welcome_${uid}`, + from: meta.config.welcomeUid ? meta.config.welcomeUid : null, + }); + + await notifications.push(notifObj, [uid]); +}; + +UserNotifications.sendNameChangeNotification = async function (uid, username) { + const notifObj = await notifications.create({ + bodyShort: `[[user:username_taken_workaround, ${username}]]`, + image: 'brand:logo', + nid: `username_taken:${uid}`, + datetime: Date.now(), + }); + + await notifications.push(notifObj, uid); +}; + +UserNotifications.pushCount = async function (uid) { + const websockets = require('../socket.io'); + const count = await UserNotifications.getUnreadCount(uid); + websockets.in(`uid_${uid}`).emit('event:notifications.updateCount', count); +}; diff --git a/src/user/online.js b/src/user/online.js new file mode 100644 index 0000000000..a57f25fdeb --- /dev/null +++ b/src/user/online.js @@ -0,0 +1,43 @@ +'use strict'; + +const db = require('../database'); +const topics = require('../topics'); +const plugins = require('../plugins'); +const meta = require('../meta'); + +module.exports = function (User) { + User.updateLastOnlineTime = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const userData = await db.getObjectFields(`user:${uid}`, ['status', 'lastonline']); + const now = Date.now(); + if (userData.status === 'offline' || now - parseInt(userData.lastonline, 10) < 300000) { + return; + } + await User.setUserField(uid, 'lastonline', now); + }; + + User.updateOnlineUsers = async function (uid) { + if (!(parseInt(uid, 10) > 0)) { + return; + } + const now = Date.now(); + const userOnlineTime = await db.sortedSetScore('users:online', uid); + if (now - parseInt(userOnlineTime, 10) < 300000) { + return; + } + await db.sortedSetAdd('users:online', now, uid); + topics.pushUnreadCount(uid); + plugins.hooks.fire('action:user.online', { uid: uid, timestamp: now }); + }; + + User.isOnline = async function (uid) { + const now = Date.now(); + const isArray = Array.isArray(uid); + uid = isArray ? uid : [uid]; + const lastonline = await db.sortedSetScores('users:online', uid); + const isOnline = uid.map((uid, index) => (now - lastonline[index]) < (meta.config.onlineCutoff * 60000)); + return isArray ? isOnline : isOnline[0]; + }; +}; diff --git a/src/user/password.js b/src/user/password.js new file mode 100644 index 0000000000..0285f4cfa3 --- /dev/null +++ b/src/user/password.js @@ -0,0 +1,47 @@ +'use strict'; + + +const nconf = require('nconf'); + +const db = require('../database'); +const Password = require('../password'); + +module.exports = function (User) { + User.hashPassword = async function (password) { + if (!password) { + return password; + } + + return await Password.hash(nconf.get('bcrypt_rounds') || 12, password); + }; + + User.isPasswordCorrect = async function (uid, password, ip) { + password = password || ''; + let { + password: hashedPassword, + 'password:shaWrapped': shaWrapped, + } = await db.getObjectFields(`user:${uid}`, ['password', 'password:shaWrapped']); + if (!hashedPassword) { + // Non-existant user, submit fake hash for comparison + hashedPassword = ''; + } + + try { + User.isPasswordValid(password, 0); + } catch (e) { + return false; + } + + await User.auth.logAttempt(uid, ip); + const ok = await Password.compare(password, hashedPassword, !!parseInt(shaWrapped, 10)); + if (ok) { + await User.auth.clearLoginAttempts(uid); + } + return ok; + }; + + User.hasPassword = async function (uid) { + const hashedPassword = await db.getObjectField(`user:${uid}`, 'password'); + return !!hashedPassword; + }; +}; diff --git a/src/user/picture.js b/src/user/picture.js new file mode 100644 index 0000000000..234f5c3607 --- /dev/null +++ b/src/user/picture.js @@ -0,0 +1,233 @@ +'use strict'; + +const winston = require('winston'); +const mime = require('mime'); +const path = require('path'); +const nconf = require('nconf'); + +const db = require('../database'); +const file = require('../file'); +const image = require('../image'); +const meta = require('../meta'); + +module.exports = function (User) { + User.getAllowedProfileImageExtensions = function () { + const exts = User.getAllowedImageTypes().map(type => mime.getExtension(type)); + if (exts.includes('jpeg')) { + exts.push('jpg'); + } + return exts; + }; + + User.getAllowedImageTypes = function () { + return ['image/png', 'image/jpeg', 'image/bmp', 'image/gif']; + }; + + User.updateCoverPosition = async function (uid, position) { + // Reject anything that isn't two percentages + if (!/^[\d.]+%\s[\d.]+%$/.test(position)) { + winston.warn(`[user/updateCoverPosition] Invalid position received: ${position}`); + throw new Error('[[error:invalid-data]]'); + } + + await User.setUserField(uid, 'cover:position', position); + }; + + User.updateCoverPicture = async function (data) { + const picture = { + name: 'profileCover', + uid: data.uid, + }; + + try { + if (!data.imageData && data.position) { + return await User.updateCoverPosition(data.uid, data.position); + } + + validateUpload(data, meta.config.maximumCoverImageSize, ['image/png', 'image/jpeg', 'image/bmp']); + + picture.path = await image.writeImageDataToTempFile(data.imageData); + + const extension = file.typeToExtension(image.mimeFromBase64(data.imageData)); + const filename = `${data.uid}-profilecover-${Date.now()}${extension}`; + const uploadData = await image.uploadImage(filename, 'profile', picture); + + await deleteCurrentPicture(data.uid, 'cover:url'); + await User.setUserField(data.uid, 'cover:url', uploadData.url); + + if (data.position) { + await User.updateCoverPosition(data.uid, data.position); + } + + return { + url: uploadData.url, + }; + } finally { + await file.delete(picture.path); + } + }; + + // uploads a image file as profile picture + User.uploadCroppedPictureFile = async function (data) { + const userPhoto = data.file; + if (!meta.config.allowProfileImageUploads) { + throw new Error('[[error:profile-image-uploads-disabled]]'); + } + + if (userPhoto.size > meta.config.maximumProfileImageSize * 1024) { + throw new Error(`[[error:file-too-big, ${meta.config.maximumProfileImageSize}]]`); + } + + if (!userPhoto.type || !User.getAllowedImageTypes().includes(userPhoto.type)) { + throw new Error('[[error:invalid-image]]'); + } + + const extension = file.typeToExtension(userPhoto.type); + if (!extension) { + throw new Error('[[error:invalid-image-extension]]'); + } + + const newPath = await convertToPNG(userPhoto.path); + + await image.resizeImage({ + path: newPath, + width: meta.config.profileImageDimension, + height: meta.config.profileImageDimension, + }); + + const filename = generateProfileImageFilename(data.uid, extension); + const uploadedImage = await image.uploadImage(filename, 'profile', { + uid: data.uid, + path: newPath, + name: 'profileAvatar', + }); + + await deleteCurrentPicture(data.uid, 'uploadedpicture'); + await User.updateProfile(data.callerUid, { + uid: data.uid, + uploadedpicture: uploadedImage.url, + picture: uploadedImage.url, + }, ['uploadedpicture', 'picture']); + return uploadedImage; + }; + + // uploads image data in base64 as profile picture + User.uploadCroppedPicture = async function (data) { + const picture = { + name: 'profileAvatar', + uid: data.uid, + }; + + try { + if (!meta.config.allowProfileImageUploads) { + throw new Error('[[error:profile-image-uploads-disabled]]'); + } + + validateUpload(data, meta.config.maximumProfileImageSize, User.getAllowedImageTypes()); + + const extension = file.typeToExtension(image.mimeFromBase64(data.imageData)); + if (!extension) { + throw new Error('[[error:invalid-image-extension]]'); + } + + picture.path = await image.writeImageDataToTempFile(data.imageData); + picture.path = await convertToPNG(picture.path); + + await image.resizeImage({ + path: picture.path, + width: meta.config.profileImageDimension, + height: meta.config.profileImageDimension, + }); + + const filename = generateProfileImageFilename(data.uid, extension); + const uploadedImage = await image.uploadImage(filename, 'profile', picture); + + await deleteCurrentPicture(data.uid, 'uploadedpicture'); + await User.updateProfile(data.callerUid, { + uid: data.uid, + uploadedpicture: uploadedImage.url, + picture: uploadedImage.url, + }, ['uploadedpicture', 'picture']); + return uploadedImage; + } finally { + await file.delete(picture.path); + } + }; + + async function deleteCurrentPicture(uid, field) { + if (meta.config['profile:keepAllUserImages']) { + return; + } + await deletePicture(uid, field); + } + + async function deletePicture(uid, field) { + const uploadPath = await getPicturePath(uid, field); + if (uploadPath) { + await file.delete(uploadPath); + } + } + + function validateUpload(data, maxSize, allowedTypes) { + if (!data.imageData) { + throw new Error('[[error:invalid-data]]'); + } + const size = image.sizeFromBase64(data.imageData); + if (size > maxSize * 1024) { + throw new Error(`[[error:file-too-big, ${maxSize}]]`); + } + + const type = image.mimeFromBase64(data.imageData); + if (!type || !allowedTypes.includes(type)) { + throw new Error('[[error:invalid-image]]'); + } + } + + async function convertToPNG(path) { + const convertToPNG = meta.config['profile:convertProfileImageToPNG'] === 1; + if (!convertToPNG) { + return path; + } + const newPath = await image.normalise(path); + await file.delete(path); + return newPath; + } + + function generateProfileImageFilename(uid, extension) { + const convertToPNG = meta.config['profile:convertProfileImageToPNG'] === 1; + return `${uid}-profileavatar-${Date.now()}${convertToPNG ? '.png' : extension}`; + } + + User.removeCoverPicture = async function (data) { + await deletePicture(data.uid, 'cover:url'); + await db.deleteObjectFields(`user:${data.uid}`, ['cover:url', 'cover:position']); + }; + + User.removeProfileImage = async function (uid) { + const userData = await User.getUserFields(uid, ['uploadedpicture', 'picture']); + await deletePicture(uid, 'uploadedpicture'); + await User.setUserFields(uid, { + uploadedpicture: '', + // if current picture is uploaded picture, reset to user icon + picture: userData.uploadedpicture === userData.picture ? '' : userData.picture, + }); + return userData; + }; + + User.getLocalCoverPath = async function (uid) { + return getPicturePath(uid, 'cover:url'); + }; + + User.getLocalAvatarPath = async function (uid) { + return getPicturePath(uid, 'uploadedpicture'); + }; + + async function getPicturePath(uid, field) { + const value = await User.getUserField(uid, field); + if (!value || !value.startsWith(`${nconf.get('relative_path')}/assets/uploads/profile/`)) { + return false; + } + const filename = value.split('/').pop(); + return path.join(nconf.get('upload_path'), 'profile', filename); + } +}; diff --git a/src/user/posts.js b/src/user/posts.js new file mode 100644 index 0000000000..47a6eb3179 --- /dev/null +++ b/src/user/posts.js @@ -0,0 +1,122 @@ +'use strict'; + +const db = require('../database'); +const meta = require('../meta'); +const privileges = require('../privileges'); + +module.exports = function (User) { + User.isReadyToPost = async function (uid, cid) { + await isReady(uid, cid, 'lastposttime'); + }; + + User.isReadyToQueue = async function (uid, cid) { + await isReady(uid, cid, 'lastqueuetime'); + }; + + async function isReady(uid, cid, field) { + if (parseInt(uid, 10) === 0) { + return; + } + const [userData, isAdminOrMod] = await Promise.all([ + User.getUserFields(uid, ['uid', 'mutedUntil', 'joindate', 'email', 'reputation'].concat([field])), + privileges.categories.isAdminOrMod(cid, uid), + ]); + + if (!userData.uid) { + throw new Error('[[error:no-user]]'); + } + + if (isAdminOrMod) { + return; + } + + const now = Date.now(); + if (userData.mutedUntil > now) { + let muteLeft = ((userData.mutedUntil - now) / (1000 * 60)); + if (muteLeft > 60) { + muteLeft = (muteLeft / 60).toFixed(0); + throw new Error(`[[error:user-muted-for-hours, ${muteLeft}]]`); + } else { + throw new Error(`[[error:user-muted-for-minutes, ${muteLeft.toFixed(0)}]]`); + } + } + + if (now - userData.joindate < meta.config.initialPostDelay * 1000) { + throw new Error(`[[error:user-too-new, ${meta.config.initialPostDelay}]]`); + } + + const lasttime = userData[field] || 0; + + if ( + meta.config.newbiePostDelay > 0 && + meta.config.newbiePostDelayThreshold > userData.reputation && + now - lasttime < meta.config.newbiePostDelay * 1000 + ) { + throw new Error(`[[error:too-many-posts-newbie, ${meta.config.newbiePostDelay}, ${meta.config.newbiePostDelayThreshold}]]`); + } else if (now - lasttime < meta.config.postDelay * 1000) { + throw new Error(`[[error:too-many-posts, ${meta.config.postDelay}]]`); + } + } + + User.onNewPostMade = async function (postData) { + // For scheduled posts, use "action" time. It'll be updated in related cron job when post is published + const lastposttime = postData.timestamp > Date.now() ? Date.now() : postData.timestamp; + + await Promise.all([ + User.addPostIdToUser(postData), + User.setUserField(postData.uid, 'lastposttime', lastposttime), + User.updateLastOnlineTime(postData.uid), + ]); + }; + + User.addPostIdToUser = async function (postData) { + await db.sortedSetsAdd([ + `uid:${postData.uid}:posts`, + `cid:${postData.cid}:uid:${postData.uid}:pids`, + ], postData.timestamp, postData.pid); + await User.updatePostCount(postData.uid); + }; + + User.updatePostCount = async (uids) => { + uids = Array.isArray(uids) ? uids : [uids]; + const exists = await User.exists(uids); + uids = uids.filter((uid, index) => exists[index]); + if (uids.length) { + const counts = await db.sortedSetsCard(uids.map(uid => `uid:${uid}:posts`)); + await Promise.all([ + db.setObjectBulk(uids.map((uid, index) => ([`user:${uid}`, { postcount: counts[index] }]))), + db.sortedSetAdd('users:postcount', counts, uids), + ]); + } + }; + + User.incrementUserPostCountBy = async function (uid, value) { + return await incrementUserFieldAndSetBy(uid, 'postcount', 'users:postcount', value); + }; + + User.incrementUserReputationBy = async function (uid, value) { + return await incrementUserFieldAndSetBy(uid, 'reputation', 'users:reputation', value); + }; + + User.incrementUserFlagsBy = async function (uid, value) { + return await incrementUserFieldAndSetBy(uid, 'flags', 'users:flags', value); + }; + + async function incrementUserFieldAndSetBy(uid, field, set, value) { + value = parseInt(value, 10); + if (!value || !field || !(parseInt(uid, 10) > 0)) { + return; + } + const exists = await User.exists(uid); + if (!exists) { + return; + } + const newValue = await User.incrementUserFieldBy(uid, field, value); + await db.sortedSetAdd(set, newValue, uid); + return newValue; + } + + User.getPostIds = async function (uid, start, stop) { + return await db.getSortedSetRevRange(`uid:${uid}:posts`, start, stop); + }; +}; diff --git a/src/user/profile.js b/src/user/profile.js new file mode 100644 index 0000000000..5274a6dbf0 --- /dev/null +++ b/src/user/profile.js @@ -0,0 +1,335 @@ + +'use strict'; + +const _ = require('lodash'); +const validator = require('validator'); +const winston = require('winston'); + +const utils = require('../utils'); +const slugify = require('../slugify'); +const meta = require('../meta'); +const db = require('../database'); +const groups = require('../groups'); +const plugins = require('../plugins'); + +module.exports = function (User) { + User.updateProfile = async function (uid, data, extraFields) { + let fields = [ + 'username', 'email', 'fullname', 'website', 'location', + 'groupTitle', 'birthday', 'signature', 'aboutme', + ]; + if (Array.isArray(extraFields)) { + fields = _.uniq(fields.concat(extraFields)); + } + if (!data.uid) { + throw new Error('[[error:invalid-update-uid]]'); + } + const updateUid = data.uid; + + const result = await plugins.hooks.fire('filter:user.updateProfile', { + uid: uid, + data: data, + fields: fields, + }); + fields = result.fields; + data = result.data; + + await validateData(uid, data); + + const oldData = await User.getUserFields(updateUid, fields); + const updateData = {}; + await Promise.all(fields.map(async (field) => { + if (!(data[field] !== undefined && typeof data[field] === 'string')) { + return; + } + + data[field] = data[field].trim(); + + if (field === 'email') { + return await updateEmail(updateUid, data.email); + } else if (field === 'username') { + return await updateUsername(updateUid, data.username); + } else if (field === 'fullname') { + return await updateFullname(updateUid, data.fullname); + } + updateData[field] = data[field]; + })); + + if (Object.keys(updateData).length) { + await User.setUserFields(updateUid, updateData); + } + + plugins.hooks.fire('action:user.updateProfile', { + uid: uid, + data: data, + fields: fields, + oldData: oldData, + }); + + return await User.getUserFields(updateUid, [ + 'email', 'username', 'userslug', + 'picture', 'icon:text', 'icon:bgColor', + ]); + }; + + async function validateData(callerUid, data) { + await isEmailValid(data); + await isUsernameAvailable(data, data.uid); + await isWebsiteValid(callerUid, data); + await isAboutMeValid(callerUid, data); + await isSignatureValid(callerUid, data); + isFullnameValid(data); + isLocationValid(data); + isBirthdayValid(data); + isGroupTitleValid(data); + } + + async function isEmailValid(data) { + if (!data.email) { + return; + } + + data.email = data.email.trim(); + if (!utils.isEmailValid(data.email)) { + throw new Error('[[error:invalid-email]]'); + } + } + + async function isUsernameAvailable(data, uid) { + if (!data.username) { + return; + } + data.username = data.username.trim(); + + let userData; + if (uid) { + userData = await User.getUserFields(uid, ['username', 'userslug']); + if (userData.username === data.username) { + return; + } + } + + if (data.username.length < meta.config.minimumUsernameLength) { + throw new Error('[[error:username-too-short]]'); + } + + if (data.username.length > meta.config.maximumUsernameLength) { + throw new Error('[[error:username-too-long]]'); + } + + const userslug = slugify(data.username); + if (!utils.isUserNameValid(data.username) || !userslug) { + throw new Error('[[error:invalid-username]]'); + } + + if (uid && userslug === userData.userslug) { + return; + } + const exists = await User.existsBySlug(userslug); + if (exists) { + throw new Error('[[error:username-taken]]'); + } + + const { error } = await plugins.hooks.fire('filter:username.check', { + username: data.username, + error: undefined, + }); + if (error) { + throw error; + } + } + User.checkUsername = async username => isUsernameAvailable({ username }); + + async function isWebsiteValid(callerUid, data) { + if (!data.website) { + return; + } + if (data.website.length > 255) { + throw new Error('[[error:invalid-website]]'); + } + await User.checkMinReputation(callerUid, data.uid, 'min:rep:website'); + } + + async function isAboutMeValid(callerUid, data) { + if (!data.aboutme) { + return; + } + if (data.aboutme !== undefined && data.aboutme.length > meta.config.maximumAboutMeLength) { + throw new Error(`[[error:about-me-too-long, ${meta.config.maximumAboutMeLength}]]`); + } + + await User.checkMinReputation(callerUid, data.uid, 'min:rep:aboutme'); + } + + async function isSignatureValid(callerUid, data) { + if (!data.signature) { + return; + } + const signature = data.signature.replace(/\r\n/g, '\n'); + if (signature.length > meta.config.maximumSignatureLength) { + throw new Error(`[[error:signature-too-long, ${meta.config.maximumSignatureLength}]]`); + } + await User.checkMinReputation(callerUid, data.uid, 'min:rep:signature'); + } + + function isFullnameValid(data) { + if (data.fullname && (validator.isURL(data.fullname) || data.fullname.length > 255)) { + throw new Error('[[error:invalid-fullname]]'); + } + } + + function isLocationValid(data) { + if (data.location && (validator.isURL(data.location) || data.location.length > 255)) { + throw new Error('[[error:invalid-location]]'); + } + } + + function isBirthdayValid(data) { + if (!data.birthday) { + return; + } + + const result = new Date(data.birthday); + if (result && result.toString() === 'Invalid Date') { + throw new Error('[[error:invalid-birthday]]'); + } + } + + function isGroupTitleValid(data) { + function checkTitle(title) { + if (title === 'registered-users' || groups.isPrivilegeGroup(title)) { + throw new Error('[[error:invalid-group-title]]'); + } + } + if (!data.groupTitle) { + return; + } + let groupTitles = []; + if (validator.isJSON(data.groupTitle)) { + groupTitles = JSON.parse(data.groupTitle); + if (!Array.isArray(groupTitles)) { + throw new Error('[[error:invalid-group-title]]'); + } + groupTitles.forEach(title => checkTitle(title)); + } else { + groupTitles = [data.groupTitle]; + checkTitle(data.groupTitle); + } + if (!meta.config.allowMultipleBadges && groupTitles.length > 1) { + data.groupTitle = JSON.stringify(groupTitles[0]); + } + } + + User.checkMinReputation = async function (callerUid, uid, setting) { + const isSelf = parseInt(callerUid, 10) === parseInt(uid, 10); + if (!isSelf || meta.config['reputation:disabled']) { + return; + } + const reputation = await User.getUserField(uid, 'reputation'); + if (reputation < meta.config[setting]) { + throw new Error(`[[error:not-enough-reputation-${setting.replace(/:/g, '-')}, ${meta.config[setting]}]]`); + } + }; + + async function updateEmail(uid, newEmail) { + let oldEmail = await User.getUserField(uid, 'email'); + oldEmail = oldEmail || ''; + if (oldEmail === newEmail) { + return; + } + + // 👉 Looking for email change logic? src/user/email.js (UserEmail.confirmByUid) + if (newEmail) { + await User.email.sendValidationEmail(uid, { + email: newEmail, + force: 1, + }).catch(err => winston.error(`[user.create] Validation email failed to send\n[emailer.send] ${err.stack}`)); + } + } + + async function updateUsername(uid, newUsername) { + if (!newUsername) { + return; + } + const userData = await User.getUserFields(uid, ['username', 'userslug']); + if (userData.username === newUsername) { + return; + } + const newUserslug = slugify(newUsername); + const now = Date.now(); + await Promise.all([ + updateUidMapping('username', uid, newUsername, userData.username), + updateUidMapping('userslug', uid, newUserslug, userData.userslug), + db.sortedSetAdd(`user:${uid}:usernames`, now, `${newUsername}:${now}`), + ]); + await db.sortedSetRemove('username:sorted', `${userData.username.toLowerCase()}:${uid}`); + await db.sortedSetAdd('username:sorted', 0, `${newUsername.toLowerCase()}:${uid}`); + } + + async function updateUidMapping(field, uid, value, oldValue) { + if (value === oldValue) { + return; + } + await db.sortedSetRemove(`${field}:uid`, oldValue); + await User.setUserField(uid, field, value); + if (value) { + await db.sortedSetAdd(`${field}:uid`, uid, value); + } + } + + async function updateFullname(uid, newFullname) { + const fullname = await User.getUserField(uid, 'fullname'); + await updateUidMapping('fullname', uid, newFullname, fullname); + if (newFullname !== fullname) { + if (fullname) { + await db.sortedSetRemove('fullname:sorted', `${fullname.toLowerCase()}:${uid}`); + } + if (newFullname) { + await db.sortedSetAdd('fullname:sorted', 0, `${newFullname.toLowerCase()}:${uid}`); + } + } + } + + User.changePassword = async function (uid, data) { + if (uid <= 0 || !data || !data.uid) { + throw new Error('[[error:invalid-uid]]'); + } + User.isPasswordValid(data.newPassword); + const [isAdmin, hasPassword] = await Promise.all([ + User.isAdministrator(uid), + User.hasPassword(uid), + ]); + + if (meta.config['password:disableEdit'] && !isAdmin) { + throw new Error('[[error:no-privileges]]'); + } + + const isSelf = parseInt(uid, 10) === parseInt(data.uid, 10); + + if (!isAdmin && !isSelf) { + throw new Error('[[user:change_password_error_privileges]]'); + } + + if (isSelf && hasPassword) { + const correct = await User.isPasswordCorrect(data.uid, data.currentPassword, data.ip); + if (!correct) { + throw new Error('[[user:change_password_error_wrong_current]]'); + } + } + + const hashedPassword = await User.hashPassword(data.newPassword); + await Promise.all([ + User.setUserFields(data.uid, { + password: hashedPassword, + 'password:shaWrapped': 1, + rss_token: utils.generateUUID(), + }), + User.reset.cleanByUid(data.uid), + User.reset.updateExpiry(data.uid), + User.auth.revokeAllSessions(data.uid), + User.email.expireValidation(data.uid), + ]); + + plugins.hooks.fire('action:password.change', { uid: uid, targetUid: data.uid }); + }; +}; diff --git a/src/user/reset.js b/src/user/reset.js new file mode 100644 index 0000000000..7940b4609c --- /dev/null +++ b/src/user/reset.js @@ -0,0 +1,165 @@ +'use strict'; + +const nconf = require('nconf'); +const winston = require('winston'); + +const user = require('./index'); +const groups = require('../groups'); +const utils = require('../utils'); +const batch = require('../batch'); + +const db = require('../database'); +const meta = require('../meta'); +const emailer = require('../emailer'); +const Password = require('../password'); + +const UserReset = module.exports; + +const twoHours = 7200000; + +UserReset.validate = async function (code) { + const uid = await db.getObjectField('reset:uid', code); + if (!uid) { + return false; + } + const issueDate = await db.sortedSetScore('reset:issueDate', code); + return parseInt(issueDate, 10) > Date.now() - twoHours; +}; + +UserReset.generate = async function (uid) { + const code = utils.generateUUID(); + + // Invalidate past tokens (must be done prior) + await UserReset.cleanByUid(uid); + + await Promise.all([ + db.setObjectField('reset:uid', code, uid), + db.sortedSetAdd('reset:issueDate', Date.now(), code), + ]); + return code; +}; + +async function canGenerate(uid) { + const score = await db.sortedSetScore('reset:issueDate:uid', uid); + if (score > Date.now() - (1000 * 60)) { + throw new Error('[[error:reset-rate-limited]]'); + } +} + +UserReset.send = async function (email) { + const uid = await user.getUidByEmail(email); + if (!uid) { + throw new Error('[[error:invalid-email]]'); + } + await canGenerate(uid); + await db.sortedSetAdd('reset:issueDate:uid', Date.now(), uid); + const code = await UserReset.generate(uid); + await emailer.send('reset', uid, { + reset_link: `${nconf.get('url')}/reset/${code}`, + subject: '[[email:password-reset-requested]]', + template: 'reset', + uid: uid, + }).catch(err => winston.error(`[emailer.send] ${err.stack}`)); + + return code; +}; + +UserReset.commit = async function (code, password) { + user.isPasswordValid(password); + const validated = await UserReset.validate(code); + if (!validated) { + throw new Error('[[error:reset-code-not-valid]]'); + } + const uid = await db.getObjectField('reset:uid', code); + if (!uid) { + throw new Error('[[error:reset-code-not-valid]]'); + } + const userData = await db.getObjectFields( + `user:${uid}`, + ['password', 'passwordExpiry', 'password:shaWrapped'] + ); + const ok = await Password.compare(password, userData.password, !!parseInt(userData['password:shaWrapped'], 10)); + if (ok) { + throw new Error('[[error:reset-same-password]]'); + } + const hash = await user.hashPassword(password); + const data = { + password: hash, + 'password:shaWrapped': 1, + }; + + // don't verify email if password reset is due to expiry + const isPasswordExpired = userData.passwordExpiry && userData.passwordExpiry < Date.now(); + if (!isPasswordExpired) { + data['email:confirmed'] = 1; + await groups.join('verified-users', uid); + await groups.leave('unverified-users', uid); + } + + await Promise.all([ + user.setUserFields(uid, data), + db.deleteObjectField('reset:uid', code), + db.sortedSetRemoveBulk([ + ['reset:issueDate', code], + ['reset:issueDate:uid', uid], + ]), + user.reset.updateExpiry(uid), + user.auth.resetLockout(uid), + user.auth.revokeAllSessions(uid), + user.email.expireValidation(uid), + ]); +}; + +UserReset.updateExpiry = async function (uid) { + const expireDays = meta.config.passwordExpiryDays; + if (expireDays > 0) { + const oneDay = 1000 * 60 * 60 * 24; + const expiry = Date.now() + (oneDay * expireDays); + await user.setUserField(uid, 'passwordExpiry', expiry); + } else { + await db.deleteObjectField(`user:${uid}`, 'passwordExpiry'); + } +}; + +UserReset.clean = async function () { + const [tokens, uids] = await Promise.all([ + db.getSortedSetRangeByScore('reset:issueDate', 0, -1, '-inf', Date.now() - twoHours), + db.getSortedSetRangeByScore('reset:issueDate:uid', 0, -1, '-inf', Date.now() - twoHours), + ]); + if (!tokens.length && !uids.length) { + return; + } + + winston.verbose(`[UserReset.clean] Removing ${tokens.length} reset tokens from database`); + await cleanTokensAndUids(tokens, uids); +}; + +UserReset.cleanByUid = async function (uid) { + const tokensToClean = []; + uid = parseInt(uid, 10); + + await batch.processSortedSet('reset:issueDate', async (tokens) => { + const results = await db.getObjectFields('reset:uid', tokens); + for (const [code, result] of Object.entries(results)) { + if (parseInt(result, 10) === uid) { + tokensToClean.push(code); + } + } + }, { batch: 500 }); + + if (!tokensToClean.length) { + winston.verbose(`[UserReset.cleanByUid] No tokens found for uid (${uid}).`); + return; + } + + winston.verbose(`[UserReset.cleanByUid] Found ${tokensToClean.length} token(s), removing...`); + await cleanTokensAndUids(tokensToClean, uid); +}; + +async function cleanTokensAndUids(tokens, uids) { + await Promise.all([ + db.deleteObjectFields('reset:uid', tokens), + db.sortedSetRemove('reset:issueDate', tokens), + db.sortedSetRemove('reset:issueDate:uid', uids), + ]); +} diff --git a/src/user/search.js b/src/user/search.js new file mode 100644 index 0000000000..cbacef3c13 --- /dev/null +++ b/src/user/search.js @@ -0,0 +1,159 @@ + +'use strict'; + +const _ = require('lodash'); + +const meta = require('../meta'); +const plugins = require('../plugins'); +const db = require('../database'); +const groups = require('../groups'); +const utils = require('../utils'); + +module.exports = function (User) { + const filterFnMap = { + online: user => user.status !== 'offline' && (Date.now() - user.lastonline < 300000), + flagged: user => parseInt(user.flags, 10) > 0, + verified: user => !!user['email:confirmed'], + unverified: user => !user['email:confirmed'], + }; + + const filterFieldMap = { + online: ['status', 'lastonline'], + flagged: ['flags'], + verified: ['email:confirmed'], + unverified: ['email:confirmed'], + }; + + + User.search = async function (data) { + const query = data.query || ''; + const searchBy = data.searchBy || 'username'; + const page = data.page || 1; + const uid = data.uid || 0; + const paginate = data.hasOwnProperty('paginate') ? data.paginate : true; + + const startTime = process.hrtime(); + + let uids = []; + if (searchBy === 'ip') { + uids = await searchByIP(query); + } else if (searchBy === 'uid') { + uids = [query]; + } else { + const searchMethod = data.findUids || findUids; + uids = await searchMethod(query, searchBy, data.hardCap); + } + + uids = await filterAndSortUids(uids, data); + const result = await plugins.hooks.fire('filter:users.search', { uids: uids, uid: uid }); + uids = result.uids; + + const searchResult = { + matchCount: uids.length, + }; + + if (paginate) { + const resultsPerPage = data.resultsPerPage || meta.config.userSearchResultsPerPage; + const start = Math.max(0, page - 1) * resultsPerPage; + const stop = start + resultsPerPage; + searchResult.pageCount = Math.ceil(uids.length / resultsPerPage); + uids = uids.slice(start, stop); + } + + const userData = await User.getUsers(uids, uid); + searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2); + searchResult.users = userData.filter(user => user && user.uid > 0); + return searchResult; + }; + + async function findUids(query, searchBy, hardCap) { + if (!query) { + return []; + } + query = String(query).toLowerCase(); + const min = query; + const max = query.substr(0, query.length - 1) + String.fromCharCode(query.charCodeAt(query.length - 1) + 1); + + const resultsPerPage = meta.config.userSearchResultsPerPage; + hardCap = hardCap || resultsPerPage * 10; + + const data = await db.getSortedSetRangeByLex(`${searchBy}:sorted`, min, max, 0, hardCap); + const uids = data.map(data => data.split(':').pop()); + return uids; + } + + async function filterAndSortUids(uids, data) { + uids = uids.filter(uid => parseInt(uid, 10)); + let filters = data.filters || []; + filters = Array.isArray(filters) ? filters : [data.filters]; + const fields = []; + + if (data.sortBy) { + fields.push(data.sortBy); + } + + filters.forEach((filter) => { + if (filterFieldMap[filter]) { + fields.push(...filterFieldMap[filter]); + } + }); + + if (data.groupName) { + const isMembers = await groups.isMembers(uids, data.groupName); + uids = uids.filter((uid, index) => isMembers[index]); + } + + if (!fields.length) { + return uids; + } + + if (filters.includes('banned') || filters.includes('notbanned')) { + const isMembersOfBanned = await groups.isMembers(uids, groups.BANNED_USERS); + const checkBanned = filters.includes('banned'); + uids = uids.filter((uid, index) => (checkBanned ? isMembersOfBanned[index] : !isMembersOfBanned[index])); + } + + fields.push('uid'); + let userData = await User.getUsersFields(uids, fields); + + filters.forEach((filter) => { + if (filterFnMap[filter]) { + userData = userData.filter(filterFnMap[filter]); + } + }); + + if (data.sortBy) { + sortUsers(userData, data.sortBy, data.sortDirection); + } + + return userData.map(user => user.uid); + } + + function sortUsers(userData, sortBy, sortDirection) { + if (!userData || !userData.length) { + return; + } + sortDirection = sortDirection || 'desc'; + const direction = sortDirection === 'desc' ? 1 : -1; + + const isNumeric = utils.isNumber(userData[0][sortBy]); + if (isNumeric) { + userData.sort((u1, u2) => direction * (u2[sortBy] - u1[sortBy])); + } else { + userData.sort((u1, u2) => { + if (u1[sortBy] < u2[sortBy]) { + return direction * -1; + } else if (u1[sortBy] > u2[sortBy]) { + return direction * 1; + } + return 0; + }); + } + } + + async function searchByIP(ip) { + const ipKeys = await db.scan({ match: `ip:${ip}*` }); + const uids = await db.getSortedSetRevRange(ipKeys, 0, -1); + return _.uniq(uids); + } +}; diff --git a/src/user/settings.js b/src/user/settings.js new file mode 100644 index 0000000000..a11892ceee --- /dev/null +++ b/src/user/settings.js @@ -0,0 +1,171 @@ + +'use strict'; + +const validator = require('validator'); + +const meta = require('../meta'); +const db = require('../database'); +const plugins = require('../plugins'); +const notifications = require('../notifications'); +const languages = require('../languages'); + +module.exports = function (User) { + User.getSettings = async function (uid) { + if (parseInt(uid, 10) <= 0) { + return await onSettingsLoaded(0, {}); + } + let settings = await db.getObject(`user:${uid}:settings`); + settings = settings || {}; + settings.uid = uid; + return await onSettingsLoaded(uid, settings); + }; + + User.getMultipleUserSettings = async function (uids) { + if (!Array.isArray(uids) || !uids.length) { + return []; + } + + const keys = uids.map(uid => `user:${uid}:settings`); + let settings = await db.getObjects(keys); + settings = settings.map((userSettings, index) => { + userSettings = userSettings || {}; + userSettings.uid = uids[index]; + return userSettings; + }); + return await Promise.all(settings.map(s => onSettingsLoaded(s.uid, s))); + }; + + async function onSettingsLoaded(uid, settings) { + const data = await plugins.hooks.fire('filter:user.getSettings', { uid: uid, settings: settings }); + settings = data.settings; + + const defaultTopicsPerPage = meta.config.topicsPerPage; + const defaultPostsPerPage = meta.config.postsPerPage; + + settings.showemail = parseInt(getSetting(settings, 'showemail', 0), 10) === 1; + settings.showfullname = parseInt(getSetting(settings, 'showfullname', 0), 10) === 1; + settings.openOutgoingLinksInNewTab = parseInt(getSetting(settings, 'openOutgoingLinksInNewTab', 0), 10) === 1; + settings.dailyDigestFreq = getSetting(settings, 'dailyDigestFreq', 'off'); + settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1; + settings.topicsPerPage = Math.min( + meta.config.maxTopicsPerPage, + settings.topicsPerPage ? parseInt(settings.topicsPerPage, 10) : defaultTopicsPerPage, + defaultTopicsPerPage + ); + settings.postsPerPage = Math.min( + meta.config.maxPostsPerPage, + settings.postsPerPage ? parseInt(settings.postsPerPage, 10) : defaultPostsPerPage, + defaultPostsPerPage + ); + settings.userLang = settings.userLang || meta.config.defaultLang || 'en-GB'; + settings.acpLang = settings.acpLang || settings.userLang; + settings.topicPostSort = getSetting(settings, 'topicPostSort', 'oldest_to_newest'); + settings.categoryTopicSort = getSetting(settings, 'categoryTopicSort', 'newest_to_oldest'); + settings.followTopicsOnCreate = parseInt(getSetting(settings, 'followTopicsOnCreate', 1), 10) === 1; + settings.followTopicsOnReply = parseInt(getSetting(settings, 'followTopicsOnReply', 0), 10) === 1; + settings.upvoteNotifFreq = getSetting(settings, 'upvoteNotifFreq', 'all'); + settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; + settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; + settings.updateUrlWithPostIndex = parseInt(getSetting(settings, 'updateUrlWithPostIndex', 1), 10) === 1; + settings.bootswatchSkin = validator.escape(String(settings.bootswatchSkin || '')); + settings.homePageRoute = validator.escape(String(settings.homePageRoute || '')).replace(///g, '/'); + settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; + settings.categoryWatchState = getSetting(settings, 'categoryWatchState', 'notwatching'); + + const notificationTypes = await notifications.getAllNotificationTypes(); + notificationTypes.forEach((notificationType) => { + settings[notificationType] = getSetting(settings, notificationType, 'notification'); + }); + + return settings; + } + + function getSetting(settings, key, defaultValue) { + if (settings[key] || settings[key] === 0) { + return settings[key]; + } else if (meta.config[key] || meta.config[key] === 0) { + return meta.config[key]; + } + return defaultValue; + } + + User.saveSettings = async function (uid, data) { + const maxPostsPerPage = meta.config.maxPostsPerPage || 20; + if ( + !data.postsPerPage || + parseInt(data.postsPerPage, 10) <= 1 || + parseInt(data.postsPerPage, 10) > maxPostsPerPage + ) { + throw new Error(`[[error:invalid-pagination-value, 2, ${maxPostsPerPage}]]`); + } + + const maxTopicsPerPage = meta.config.maxTopicsPerPage || 20; + if ( + !data.topicsPerPage || + parseInt(data.topicsPerPage, 10) <= 1 || + parseInt(data.topicsPerPage, 10) > 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; + + plugins.hooks.fire('action:user.saveSettings', { uid: uid, settings: data }); + + const settings = { + showemail: data.showemail, + showfullname: data.showfullname, + openOutgoingLinksInNewTab: data.openOutgoingLinksInNewTab, + dailyDigestFreq: data.dailyDigestFreq || 'off', + usePagination: data.usePagination, + topicsPerPage: Math.min(data.topicsPerPage, parseInt(maxTopicsPerPage, 10) || 20), + postsPerPage: Math.min(data.postsPerPage, parseInt(maxPostsPerPage, 10) || 20), + userLang: data.userLang || meta.config.defaultLang, + acpLang: data.acpLang || meta.config.defaultLang, + followTopicsOnCreate: data.followTopicsOnCreate, + followTopicsOnReply: data.followTopicsOnReply, + restrictChat: data.restrictChat, + topicSearchEnabled: data.topicSearchEnabled, + updateUrlWithPostIndex: data.updateUrlWithPostIndex, + homePageRoute: ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''), + scrollToMyPost: data.scrollToMyPost, + upvoteNotifFreq: data.upvoteNotifFreq, + bootswatchSkin: data.bootswatchSkin, + categoryWatchState: data.categoryWatchState, + categoryTopicSort: data.categoryTopicSort, + topicPostSort: data.topicPostSort, + }; + const notificationTypes = await notifications.getAllNotificationTypes(); + notificationTypes.forEach((notificationType) => { + if (data[notificationType]) { + settings[notificationType] = data[notificationType]; + } + }); + const result = await plugins.hooks.fire('filter:user.saveSettings', { uid: uid, settings: settings, data: data }); + await db.setObject(`user:${uid}:settings`, result.settings); + await User.updateDigestSetting(uid, data.dailyDigestFreq); + return await User.getSettings(uid); + }; + + User.updateDigestSetting = async function (uid, dailyDigestFreq) { + await db.sortedSetsRemove(['digest:day:uids', 'digest:week:uids', 'digest:month:uids'], uid); + if (['day', 'week', 'biweek', 'month'].includes(dailyDigestFreq)) { + await db.sortedSetAdd(`digest:${dailyDigestFreq}:uids`, Date.now(), uid); + } + }; + + User.setSetting = async function (uid, key, value) { + if (parseInt(uid, 10) <= 0) { + return; + } + + await db.setObjectField(`user:${uid}:settings`, key, value); + }; +}; diff --git a/src/user/topics.js b/src/user/topics.js new file mode 100644 index 0000000000..7080cac7d8 --- /dev/null +++ b/src/user/topics.js @@ -0,0 +1,16 @@ +'use strict'; + +const db = require('../database'); + +module.exports = function (User) { + User.getIgnoredTids = async function (uid, start, stop) { + return await db.getSortedSetRevRange(`uid:${uid}:ignored_tids`, start, stop); + }; + + User.addTopicIdToUser = async function (uid, tid, timestamp) { + await Promise.all([ + db.sortedSetAdd(`uid:${uid}:topics`, timestamp, tid), + User.incrementUserFieldBy(uid, 'topiccount', 1), + ]); + }; +}; diff --git a/src/user/uploads.js b/src/user/uploads.js new file mode 100644 index 0000000000..eb190c03fa --- /dev/null +++ b/src/user/uploads.js @@ -0,0 +1,90 @@ +'use strict'; + +const path = require('path'); +const nconf = require('nconf'); +const winston = require('winston'); +const crypto = require('crypto'); + +const db = require('../database'); +const posts = require('../posts'); +const file = require('../file'); +const batch = require('../batch'); + +const md5 = filename => crypto.createHash('md5').update(filename).digest('hex'); +const _getFullPath = relativePath => path.resolve(nconf.get('upload_path'), relativePath); +const _validatePath = async (relativePaths) => { + if (typeof relativePaths === 'string') { + relativePaths = [relativePaths]; + } else if (!Array.isArray(relativePaths)) { + throw new Error(`[[error:wrong-parameter-type, relativePaths, ${typeof relativePaths}, array]]`); + } + + const fullPaths = relativePaths.map(path => _getFullPath(path)); + const exists = await Promise.all(fullPaths.map(async fullPath => file.exists(fullPath))); + + if (!fullPaths.every(fullPath => fullPath.startsWith(nconf.get('upload_path'))) || !exists.every(Boolean)) { + throw new Error('[[error:invalid-path]]'); + } +}; + +module.exports = function (User) { + User.associateUpload = async (uid, relativePath) => { + await _validatePath(relativePath); + await Promise.all([ + db.sortedSetAdd(`uid:${uid}:uploads`, Date.now(), relativePath), + db.setObjectField(`upload:${md5(relativePath)}`, 'uid', uid), + ]); + }; + + User.deleteUpload = async function (callerUid, uid, uploadNames) { + if (typeof uploadNames === 'string') { + uploadNames = [uploadNames]; + } else if (!Array.isArray(uploadNames)) { + throw new Error(`[[error:wrong-parameter-type, uploadNames, ${typeof uploadNames}, array]]`); + } + + await _validatePath(uploadNames); + + const [isUsersUpload, isAdminOrGlobalMod] = await Promise.all([ + db.isSortedSetMembers(`uid:${callerUid}:uploads`, uploadNames), + User.isAdminOrGlobalMod(callerUid), + ]); + if (!isAdminOrGlobalMod && !isUsersUpload.every(Boolean)) { + throw new Error('[[error:no-privileges]]'); + } + + await batch.processArray(uploadNames, async (uploadNames) => { + const fullPaths = uploadNames.map(path => _getFullPath(path)); + + await Promise.all(fullPaths.map(async (fullPath, idx) => { + winston.verbose(`[user/deleteUpload] Deleting ${uploadNames[idx]}`); + await Promise.all([ + file.delete(fullPath), + file.delete(file.appendToFileName(fullPath, '-resized')), + ]); + await Promise.all([ + db.sortedSetRemove(`uid:${uid}:uploads`, uploadNames[idx]), + db.delete(`upload:${md5(uploadNames[idx])}`), + ]); + })); + + // Dissociate the upload from pids, if any + const pids = await db.getSortedSetsMembers(uploadNames.map(relativePath => `upload:${md5(relativePath)}:pids`)); + await Promise.all(pids.map(async (pids, idx) => Promise.all( + pids.map(async pid => posts.uploads.dissociate(pid, uploadNames[idx])) + ))); + }, { batch: 50 }); + }; + + User.collateUploads = async function (uid, archive) { + await batch.processSortedSet(`uid:${uid}:uploads`, (files, next) => { + files.forEach((file) => { + archive.file(_getFullPath(file), { + name: path.basename(file), + }); + }); + + setImmediate(next); + }, { batch: 100 }); + }; +}; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000000..0e87bb1241 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,32 @@ +'use strict'; + +const crypto = require('crypto'); + +process.profile = function (operation, start) { + console.log('%s took %d milliseconds', operation, process.elapsedTimeSince(start)); +}; + +process.elapsedTimeSince = function (start) { + const diff = process.hrtime(start); + return (diff[0] * 1e3) + (diff[1] / 1e6); +}; +const utils = { ...require('../public/src/utils.common') }; + +utils.getLanguage = function () { + const meta = require('./meta'); + return meta.config && meta.config.defaultLang ? meta.config.defaultLang : 'en-GB'; +}; + +utils.generateUUID = function () { + // from https://github.com/tracker1/node-uuid4/blob/master/index.js + let rnd = crypto.randomBytes(16); + /* eslint-disable no-bitwise */ + rnd[6] = (rnd[6] & 0x0f) | 0x40; + rnd[8] = (rnd[8] & 0x3f) | 0x80; + /* eslint-enable no-bitwise */ + rnd = rnd.toString('hex').match(/(.{8})(.{4})(.{4})(.{4})(.{12})/); + rnd.shift(); + return rnd.join('-'); +}; + +module.exports = utils; diff --git a/src/views/400.tpl b/src/views/400.tpl new file mode 100644 index 0000000000..7c4ce69820 --- /dev/null +++ b/src/views/400.tpl @@ -0,0 +1,12 @@ +

    diff --git a/src/views/403.tpl b/src/views/403.tpl new file mode 100644 index 0000000000..e334b49e0c --- /dev/null +++ b/src/views/403.tpl @@ -0,0 +1,16 @@ +
    + [[global:403.title]] + +

    {error}

    + +

    [[global:403.message]]

    + + + +

    [[error:goback]]

    + + + +

    [[global:403.login, {config.relative_path}]]

    + +
    \ No newline at end of file diff --git a/src/views/404.tpl b/src/views/404.tpl new file mode 100644 index 0000000000..65b9e297e3 --- /dev/null +++ b/src/views/404.tpl @@ -0,0 +1,8 @@ +
    + {path} [[global:404.title]] + +

    {error}

    + +

    [[global:404.message, {config.relative_path}]]

    + +
    \ No newline at end of file diff --git a/src/views/500.tpl b/src/views/500.tpl new file mode 100644 index 0000000000..a2fcd82fc1 --- /dev/null +++ b/src/views/500.tpl @@ -0,0 +1,10 @@ +
    + [[global:500.title]] +

    [[global:500.message]]

    +

    {path}

    +

    {error}

    + + +

    [[error:goback]]

    + +
    diff --git a/src/views/503.tpl b/src/views/503.tpl new file mode 100644 index 0000000000..b21faa4806 --- /dev/null +++ b/src/views/503.tpl @@ -0,0 +1,12 @@ +

    [[pages:maintenance.text, {site_title}]]

    +

    + +
    +
    +

    [[pages:maintenance.messageIntro]]

    +
    + {message} +
    +
    +
    + \ No newline at end of file diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl new file mode 100644 index 0000000000..b88d821bd9 --- /dev/null +++ b/src/views/admin/advanced/cache.tpl @@ -0,0 +1,46 @@ + +
    + +
    + + diff --git a/src/views/admin/advanced/database.tpl b/src/views/admin/advanced/database.tpl new file mode 100644 index 0000000000..66d3252b52 --- /dev/null +++ b/src/views/admin/advanced/database.tpl @@ -0,0 +1,146 @@ + +
    + {{{ if mongo }}} +
    + {{{ if mongo.serverStatusError }}} +
    + {mongo.serverStatusError} +
    + {{{ end }}} +
    +
    [[admin/advanced/database:mongo]]
    +
    +
    + [[admin/advanced/database:mongo.version]] {mongo.version}
    +
    + [[admin/advanced/database:uptime-seconds]] {mongo.uptime}
    + [[admin/advanced/database:mongo.storage-engine]] {mongo.storageEngine}
    + [[admin/advanced/database:mongo.collections]] {mongo.collections}
    + [[admin/advanced/database:mongo.objects]] {mongo.objects}
    + [[admin/advanced/database:mongo.avg-object-size]] [[admin/advanced/database:x-b, {mongo.avgObjSize}]]
    +
    + [[admin/advanced/database:mongo.data-size]] [[admin/advanced/database:x-gb, {mongo.dataSize}]]
    + [[admin/advanced/database:mongo.storage-size]] [[admin/advanced/database:x-gb, {mongo.storageSize}]]
    + [[admin/advanced/database:mongo.index-size]] [[admin/advanced/database:x-gb, {mongo.indexSize}]]
    + + [[admin/advanced/database:mongo.file-size]] [[admin/advanced/database:x-gb, {mongo.fileSize}]]
    + +
    + [[admin/advanced/database:mongo.resident-memory]] [[admin/advanced/database:x-gb, {mongo.mem.resident}]]
    + [[admin/advanced/database:mongo.virtual-memory]] [[admin/advanced/database:x-gb, {mongo.mem.virtual}]]
    + [[admin/advanced/database:mongo.mapped-memory]] [[admin/advanced/database:x-gb, {mongo.mem.mapped}]]
    +
    + [[admin/advanced/database:mongo.bytes-in]] [[admin/advanced/database:x-gb, {mongo.network.bytesIn}]]
    + [[admin/advanced/database:mongo.bytes-out]] [[admin/advanced/database:x-gb, {mongo.network.bytesOut}]]
    + [[admin/advanced/database:mongo.num-requests]] {mongo.network.numRequests}
    +
    +
    +
    +
    + {{{ end }}} + + {{{ if redis }}} +
    +
    +
    [[admin/advanced/database:redis]]
    +
    +
    + [[admin/advanced/database:redis.version]] {redis.redis_version}
    +
    + [[admin/advanced/database:uptime-seconds]] {redis.uptime_in_seconds}
    + [[admin/advanced/database:uptime-days]] {redis.uptime_in_days}
    +
    + [[admin/advanced/database:redis.keys]] {redis.keys}
    + [[admin/advanced/database:redis.expires]] {redis.expires}
    + [[admin/advanced/database:redis.avg-ttl]] {redis.avg_ttl}
    + [[admin/advanced/database:redis.connected-clients]] {redis.connected_clients}
    + [[admin/advanced/database:redis.connected-slaves]] {redis.connected_slaves}
    + [[admin/advanced/database:redis.blocked-clients]] {redis.blocked_clients}
    +
    + + [[admin/advanced/database:redis.used-memory]] [[admin/advanced/database:x-gb, {redis.used_memory_human}]]
    + [[admin/advanced/database:redis.memory-frag-ratio]] {redis.mem_fragmentation_ratio}
    +
    + [[admin/advanced/database:redis.total-connections-recieved]] {redis.total_connections_received}
    + [[admin/advanced/database:redis.total-commands-processed]] {redis.total_commands_processed}
    + [[admin/advanced/database:redis.iops]] {redis.instantaneous_ops_per_sec}
    + + [[admin/advanced/database:redis.iinput]] [[admin/advanced/database:x-mb, {redis.instantaneous_input}]]
    + [[admin/advanced/database:redis.ioutput]] [[admin/advanced/database:x-mb, {redis.instantaneous_output}]]
    + [[admin/advanced/database:redis.total-input]] [[admin/advanced/database:x-gb, {redis.total_net_input}]]
    + [[admin/advanced/database:redis.total-output]] [[admin/advanced/database:x-gb, {redis.total_net_output}]]
    + +
    + [[admin/advanced/database:redis.keyspace-hits]] {redis.keyspace_hits}
    + [[admin/advanced/database:redis.keyspace-misses]] {redis.keyspace_misses}
    +
    +
    +
    +
    + {{{ end }}} + + {{{ if postgres }}} +
    +
    +
    [[admin/advanced/database:postgres]]
    +
    +
    + [[admin/advanced/database:postgres.version]] {postgres.version}
    +
    + [[admin/advanced/database:uptime-seconds]] {postgres.uptime}
    +
    +
    +
    +
    + {{{ end }}} +
    + +
    + {{{ if mongo }}} +
    +
    +
    +

    [[admin/advanced/database:mongo.raw-info]]

    +
    + +
    +
    +
    {mongo.raw}
    +
    +
    +
    +
    + {{{ end }}} + + {{{ if redis }}} +
    +
    +
    +

    [[admin/advanced/database:redis.raw-info]]

    +
    + +
    +
    +
    {redis.raw}
    +
    +
    +
    +
    + {{{ end }}} + + {{{ if postgres }}} +
    +
    +
    +

    [[admin/advanced/database:postgres.raw-info]]

    +
    + +
    +
    +
    {postgres.raw}
    +
    +
    +
    +
    + {{{ end }}} +
    diff --git a/src/views/admin/advanced/errors.tpl b/src/views/admin/advanced/errors.tpl new file mode 100644 index 0000000000..980480571e --- /dev/null +++ b/src/views/admin/advanced/errors.tpl @@ -0,0 +1,78 @@ +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    [[admin/advanced/errors:manage-error-log]]
    +
    +
    + + [[admin/advanced/errors:export-error-log]] + + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + [[admin/advanced/errors:error.404]] +
    +
    + + + + + + + + + + + + + + + + + + +
    [[admin/advanced/errors:route]][[admin/advanced/errors:count]]
    {../value}{../score}
    +
    + [[admin/advanced/errors:no-routes-not-found]] +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/src/views/admin/advanced/events.tpl b/src/views/admin/advanced/events.tpl new file mode 100644 index 0000000000..2855c3e978 --- /dev/null +++ b/src/views/admin/advanced/events.tpl @@ -0,0 +1,71 @@ +
    +
    +
    +
    [[admin/advanced/events:events]]
    +
    + +
    [[admin/advanced/events:no-events]]
    + +
    + +
    + #{events.eid} + {events.type} + uid {events.uid} + {events.ip} + + + + +
    {events.user.icon:text}
    + +
    + {events.user.username} + + {events.timestampISO} +
    {events.jsonString}
    +
    + + +
    +
    +
    +
    +
    +
    +
    [[admin/advanced/events:filters]]
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    diff --git a/src/views/admin/advanced/hooks.tpl b/src/views/admin/advanced/hooks.tpl new file mode 100644 index 0000000000..d6d0b12422 --- /dev/null +++ b/src/views/admin/advanced/hooks.tpl @@ -0,0 +1,31 @@ +
    + {{{ each hooks }}} +
    + +
    +
    + {{{ each hooks.methods }}} +
    + {hooks.methods.id} + Priority: {hooks.methods.priority} + + +
    +
    +
    {hooks.methods.method}
    +
    + {{{ end }}} +
    +
    +
    + {{{ end }}} +
    \ No newline at end of file diff --git a/src/views/admin/advanced/logs.tpl b/src/views/admin/advanced/logs.tpl new file mode 100644 index 0000000000..f20aad40a4 --- /dev/null +++ b/src/views/admin/advanced/logs.tpl @@ -0,0 +1,23 @@ +
    +
    +
    +
    [[admin/advanced/logs:logs]]
    +
    +
    {data}
    +
    +
    +
    +
    +
    +
    [[admin/advanced/logs:control-panel]]
    +
    + + +
    +
    +
    +
    diff --git a/src/views/admin/appearance/customise.tpl b/src/views/admin/appearance/customise.tpl new file mode 100644 index 0000000000..d26e9d0002 --- /dev/null +++ b/src/views/admin/appearance/customise.tpl @@ -0,0 +1,80 @@ +
    + +
    +
    +
    +

    + [[admin/appearance/customise:custom-css.description]] +

    +
    + + +
    +
    +
    + +
    +
    +
    + +
    +

    + [[admin/appearance/customise:custom-js.description]] +

    +
    + + +
    +
    +
    + +
    +
    +
    + +
    +

    + [[admin/appearance/customise:custom-header.description]] +

    + +
    + + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/src/views/admin/appearance/skins.tpl b/src/views/admin/appearance/skins.tpl new file mode 100644 index 0000000000..db73b134f8 --- /dev/null +++ b/src/views/admin/appearance/skins.tpl @@ -0,0 +1,11 @@ +
    +
    + [[admin/appearance/skins:loading]] +
    + +
    + +
    +
    diff --git a/src/views/admin/appearance/themes.tpl b/src/views/admin/appearance/themes.tpl new file mode 100644 index 0000000000..7ee1492250 --- /dev/null +++ b/src/views/admin/appearance/themes.tpl @@ -0,0 +1,9 @@ +
    +
    + [[admin/appearance/themes:checking-for-installed]] +
    +
    + + \ No newline at end of file diff --git a/src/views/admin/dashboard.tpl b/src/views/admin/dashboard.tpl new file mode 100644 index 0000000000..791b4c80df --- /dev/null +++ b/src/views/admin/dashboard.tpl @@ -0,0 +1,154 @@ +
    +
    + + + +
    +
    +
    +
    [[admin/dashboard:guest-registered-users]]
    +
    +
    + +
      +
    • () [[admin/dashboard:registered]]
    • +
    • () [[admin/dashboard:guest]]
    • +
    +
    +
    +
    +
    + +
    +
    +
    [[admin/dashboard:user-presence]]
    +
    +
    + +
      +
    • () [[admin/dashboard:reading-posts]]
    • +
    • () [[admin/dashboard:on-categories]]
    • +
    • () [[admin/dashboard:browsing-topics]]
    • +
    • () [[admin/dashboard:recent]]
    • +
    • () [[admin/dashboard:unread]]
    • +
    +
    +
    +
    +
    +
    +
    +
    [[admin/dashboard:high-presence-topics]]
    +
    +
    + +
      +
      +
      +
      +
      +
      +
      +
      [[admin/dashboard:popular-searches]]
      +
      +
      + +
      +
      +
      +
      +
      +
      + +
      + {{{ if showSystemControls }}} +
      +
      [[admin/dashboard:control-panel]]
      +
      +

      + + +

      + +

      + [[admin/dashboard:last-restarted-by]]
      + {lastrestart.user.username} +

      + +

      + + [[admin/dashboard:restart-warning]] + + [[admin/dashboard:restart-disabled]] + +

      +

      + [[admin/dashboard:maintenance-mode]] +

      + +
      + [[admin/dashboard:realtime-chart-updates]] OFF +
      +
      + {{{ end }}} + +
      +
      [[admin/dashboard:active-users]]
      +
      +
      +
      +
      + +
      +
      [[admin/dashboard:updates]]
      +
      +
      +

      [[admin/dashboard:running-version, {version}]]

      +

      + + [[admin/dashboard:latest-lookup-failed]] + + + + [[admin/dashboard:prerelease-upgrade-available, {latestVersion}]] + + [[admin/dashboard:upgrade-available, {latestVersion}]] + + + + [[admin/dashboard:prerelease-warning]] + + [[admin/dashboard:up-to-date]] + + + +

      +
      +

      + [[admin/dashboard:keep-updated]] +

      +
      +
      + +
      +
      [[admin/dashboard:notices]]
      +
      + +
      + + {notices.doneText} + + + {notices.notDoneText} + + +
      + +
      +
      +
      +
      \ No newline at end of file diff --git a/src/views/admin/dashboard/logins.tpl b/src/views/admin/dashboard/logins.tpl new file mode 100644 index 0000000000..2574180a32 --- /dev/null +++ b/src/views/admin/dashboard/logins.tpl @@ -0,0 +1,35 @@ +
      +
      + + + [[admin/dashboard:back-to-dashboard]] + + + + + +
      [[admin/dashboard:details.logins-static, {loginDays}]]
      + + + + + + + {{{ if !sessions.length}}} + + + + {{{ end }}} + {{{ each sessions }}} + + + + + {{{ end }}} + +
      [[admin/manage/users:users.username]][[admin/dashboard:details.logins-login-time]]
      [[admin/dashboard:details.no-logins]]
      + {buildAvatar(./user, "sm", true)} {../username} + {function.userAgentIcons} {../browser} {../version} on {../platform} +
      +
      +
      \ No newline at end of file diff --git a/src/views/admin/dashboard/searches.tpl b/src/views/admin/dashboard/searches.tpl new file mode 100644 index 0000000000..f5ca42f167 --- /dev/null +++ b/src/views/admin/dashboard/searches.tpl @@ -0,0 +1,25 @@ +
      +
      + + + [[admin/dashboard:back-to-dashboard]] + + + + + + {{{ if !searches.length}}} + + + + {{{ end }}} + {{{ each searches }}} + + + + + {{{ end }}} + +
      [[admin/dashboard:details.no-searches]]
      {searches.value}{searches.score}
      +
      +
      \ No newline at end of file diff --git a/src/views/admin/dashboard/topics.tpl b/src/views/admin/dashboard/topics.tpl new file mode 100644 index 0000000000..76ca231b25 --- /dev/null +++ b/src/views/admin/dashboard/topics.tpl @@ -0,0 +1,28 @@ +
      +
      + + + [[admin/dashboard:back-to-dashboard]] + + + + + + + + {{{ if !topics.length}}} + + + + {{{ end }}} + {{{ each topics }}} + + + + + + {{{ end }}} + +
      [[admin/dashboard:details.no-topics]]
      {../title}[[topic:posted_by, {../user.username}]]
      +
      +
      \ No newline at end of file diff --git a/src/views/admin/dashboard/users.tpl b/src/views/admin/dashboard/users.tpl new file mode 100644 index 0000000000..e7b92c52dc --- /dev/null +++ b/src/views/admin/dashboard/users.tpl @@ -0,0 +1,35 @@ +
      +
      + + + [[admin/dashboard:back-to-dashboard]] + + + + + + + + + + + + + + {{{ if !users.length}}} + + + + {{{ end }}} + {{{ each users }}} + + + + + + + {{{ end }}} + +
      [[admin/manage/users:users.uid]][[admin/manage/users:users.username]][[admin/manage/users:users.email]][[admin/manage/users:users.joined]]
      [[admin/dashboard:details.no-logins]]
      {../uid}{../username}{../email}
      +
      +
      \ No newline at end of file diff --git a/src/views/admin/development/info.tpl b/src/views/admin/development/info.tpl new file mode 100644 index 0000000000..dd45dc3230 --- /dev/null +++ b/src/views/admin/development/info.tpl @@ -0,0 +1,71 @@ +
      +
      +
      +

      [[admin/development/info:you-are-on, {host}, {port}]] • [[admin/development/info:ip, {ip}]]

      +
      + +
      + [[admin/development/info:nodes-responded, {nodeCount}, {timeout}]] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      [[admin/development/info:host]][[admin/development/info:primary]][[admin/development/info:pid]][[admin/development/info:nodejs]][[admin/development/info:online]][[admin/development/info:git]][[admin/development/info:cpu-usage]][[admin/development/info:process-memory]][[admin/development/info:system-memory]][[admin/development/info:load]][[admin/development/info:uptime]]
      {info.os.hostname}:{info.process.port} + {{{if info.nodebb.isPrimary}}}{{{else}}}{{{end}}} / + {{{if info.nodebb.runJobs}}}{{{else}}}{{{end}}} + {info.process.pid}{info.process.version} + {info.stats.onlineRegisteredCount} / + {info.stats.onlineGuestCount} / + {info.stats.socketCount} + {info.git.branch}@{info.git.hashShort}{info.process.cpuUsage}% + {info.process.memoryUsage.humanReadable} gb + + {info.os.usedmem} gb / + {info.os.totalmem} gb + {info.os.load}{info.process.uptimeHumanReadable}
      +
      +
      +
      + +
      +
      +

      [[admin/development/info:info]]

      +
      + +
      +
      +
      {infoJSON}
      +
      +
      +
      +
      \ No newline at end of file diff --git a/src/views/admin/development/logger.tpl b/src/views/admin/development/logger.tpl new file mode 100644 index 0000000000..11f3c35f6e --- /dev/null +++ b/src/views/admin/development/logger.tpl @@ -0,0 +1,38 @@ + +
      +
      +
      +
      [[admin/development/logger:logger-settings]]
      +
      +

      + [[admin/development/logger:description]] +

      +
      +

      + [[admin/development/logger:explanation]] +

      +
      + +
      + + +
      +
      + + +
      +
      + + + +
      +
      +
      +
      +
      + + diff --git a/src/views/admin/extend/plugins.tpl b/src/views/admin/extend/plugins.tpl new file mode 100644 index 0000000000..89335d48f5 --- /dev/null +++ b/src/views/admin/extend/plugins.tpl @@ -0,0 +1,146 @@ +{{{ if !canChangeState }}} +
      [[error:plugins-set-in-configuration]]
      +{{{ end }}} + +
      + +
      +
      +
      +
      [[admin/extend/plugins:plugin-search]]
      +
      +
      +
      +
      + +
      +
      +
      + +
      +
      +
      + +
      +
      [[admin/extend/plugins:reorder-plugins]]
      +
      + +
      +
      + +
      +
      [[admin/extend/plugins:dev-interested]]
      +
      +

      + [[admin/extend/plugins:docs-info]] +

      +
      +
      +
      + +
      +
      + +
      + +
        + + + +
      +
      +
      + +
        +
        +
        + +
          +
          +
          + +
            +
            +
            + +
              + + + +
            +
            +
            +
            + + + + +
            + + diff --git a/src/views/admin/extend/rewards.tpl b/src/views/admin/extend/rewards.tpl new file mode 100644 index 0000000000..4f4bf69a0e --- /dev/null +++ b/src/views/admin/extend/rewards.tpl @@ -0,0 +1,82 @@ + +
            +
              + {{{ each active }}} +
            • +
              +
              +
              +
              +
              + +
              +
              +
              +
              +
              + +
              +
              + +
              +
              +
              +
              +
              + +
              +
              +
              +
              +
              +
              +
              +
              +
              + +
              +
              +
              +
              + +
              +
              +
              + +
              +
              + + + + + + +
              +
              +
              +
            • + {{{ end }}} +
            +
            + +
            + + + +
            \ No newline at end of file diff --git a/src/views/admin/extend/widgets.tpl b/src/views/admin/extend/widgets.tpl new file mode 100644 index 0000000000..0f02be6176 --- /dev/null +++ b/src/views/admin/extend/widgets.tpl @@ -0,0 +1,142 @@ +
            +
            + + +
            +
            +
            + {{{ each templates }}} +
            + {{{ each templates.areas }}} +
            +

            {../name} {templates.template} / {../location}

            +
            + +
            +
            + {{{ end }}} +
            + {{{ end }}} +
            +
            +
            +
            + +
            +
            +
            [[admin/extend/widgets:available]]
            +
            +
            +

            [[admin/extend/widgets:explanation]]

            + +
            [[admin/extend/widgets:none-installed, {config.relative_path}/admin/extend/plugins]]
            + +

            + +

            +
            + +
            +
            +
            + {availableWidgets.name} +
            {availableWidgets.description}
            +
            + +
            +
            + +
            + +
            + + + +
            +
            +
            +
            +
            +
            [[admin/extend/widgets:containers.available]]
            +
            +

            [[admin/extend/widgets:containers.explanation]]

            +
            +
            +
            + [[admin/extend/widgets:containers.none]] +
            +
            + [[admin/extend/widgets:container.well]] +
            +
            + [[admin/extend/widgets:container.jumbotron]] +
            +
            +
            + [[admin/extend/widgets:container.panel]] +
            +
            +
            +
            + [[admin/extend/widgets:container.panel-header]] +
            + + + + + + +
            +
            +
            + [[admin/extend/widgets:container.panel-body]] +
            +
            + +
            + [[admin/extend/widgets:container.alert]] +
            + + + + +
            +
            +
            +
            +
            +
            +
            +
            + + \ No newline at end of file diff --git a/src/views/admin/footer.tpl b/src/views/admin/footer.tpl new file mode 100644 index 0000000000..5556593c4c --- /dev/null +++ b/src/views/admin/footer.tpl @@ -0,0 +1,25 @@ +
            + + + + +
            + + + + + diff --git a/src/views/admin/header.tpl b/src/views/admin/header.tpl new file mode 100644 index 0000000000..6c09155b11 --- /dev/null +++ b/src/views/admin/header.tpl @@ -0,0 +1,30 @@ + + + + {title} + + {{{each metaTags}}}{function.buildMetaTag}{{{end}}} + {{{each linkTags}}}{function.buildLinkTag}{{{end}}} + + + + + + + + + + + + + + +
            \ No newline at end of file diff --git a/src/views/admin/manage/admins-mods.tpl b/src/views/admin/manage/admins-mods.tpl new file mode 100644 index 0000000000..424601504a --- /dev/null +++ b/src/views/admin/manage/admins-mods.tpl @@ -0,0 +1,77 @@ +
            +

            [[admin/manage/admins-mods:administrators]]

            +
            + +
            + + + +
            {admins.members.icon:text}
            + + {admins.members.username} + +
            + +
            + + +
            + +

            [[admin/manage/admins-mods:global-moderators]]

            +
            + +
            + + + +
            {globalMods.members.icon:text}
            + + {globalMods.members.username} + +
            + +
            + +
            [[admin/manage/admins-mods:no-global-moderators]]
            + + + +
            + +

            [[admin/manage/admins-mods:moderators]]

            + + + + + + {{{ if !categoryMods.length }}} +

            [[admin/manage/admins-mods:no-sub-categories]]

            + {{{ end }}} + + {{{ each categoryMods }}} +
            +

            {{{ if categoryMods.icon }}} {{{ end }}}{categoryMods.name} {{{ if categoryMods.subCategoryCount }}}[[admin/manage/admins-mods:subcategories, {categoryMods.subCategoryCount}]]{{{ else }}}{{{ end }}}{{{if categoryMods.disabled}}}[[admin/manage/admins-mods:disabled]]{{{end}}}

            +
            + {{{ each categoryMods.moderators }}} +
            + {{{ if categoryMods.moderators.picture }}} + + {{{ else }}} +
            {categoryMods.moderators.icon:text}
            + {{{ end }}} + {categoryMods.moderators.username} + +
            + {{{ end }}} +
            + +
            [[admin/manage/admins-mods:no-moderators]]
            + + +
            +
            + {{{ end }}} +
            + +
            +
            diff --git a/src/views/admin/manage/categories.tpl b/src/views/admin/manage/categories.tpl new file mode 100644 index 0000000000..608decb44e --- /dev/null +++ b/src/views/admin/manage/categories.tpl @@ -0,0 +1,25 @@ + +
            +
            +
            + +
            +
            + +
            +
            + +
            +
            +
            + + +
            + +
            +
            + +
            + \ No newline at end of file diff --git a/src/views/admin/manage/category-analytics.tpl b/src/views/admin/manage/category-analytics.tpl new file mode 100644 index 0000000000..36f62d9c8d --- /dev/null +++ b/src/views/admin/manage/category-analytics.tpl @@ -0,0 +1,55 @@ + + [[admin/manage/categories:analytics.back]] + + +

            [[admin/manage/categories:analytics.title, {name}]]

            +
            + +
            +
            +
            +
            +
            +

            + +

            +
            + +
            +
            +
            +
            +
            +
            +

            + +

            +
            + +
            +
            +
            +
            +
            +
            +
            +
            +

            + +

            +
            + +
            +
            +
            +
            +
            +
            +

            + +

            +
            + +
            +
            +
            \ No newline at end of file diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl new file mode 100644 index 0000000000..44de29b2d3 --- /dev/null +++ b/src/views/admin/manage/category.tpl @@ -0,0 +1,229 @@ +
            +
            +
            + +
            +
            + +
            + +
            +
            +
            +
            + +
            + + +
            +
            + +
            +
            +
            + + +
            +
            +
            +
            + + +
            +
            + +
            +
            + + +
            +

            +
            +
            + + + + + + + +
            +
            +
            +
            + + +
            +
            +
            +
            + + +
            +
            +
            +
            +
            +
            + + +
            +
            +
            +
            + + +
            +
            +
            +
            + + +
            +
            +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            +
            +
            + +
            +
            +
            + {{{ if postQueueEnabled }}} +
            +
            +
            + +
            +
            +
            + {{{ end }}} +
            +
            +
            + +
            +
            +
            +
            +
            + +
            +
            +
            +
            + +
            + +
            + +
            + +

            + +
            +
            + +
            + +
            +
            + +
            +
            + +
            +
            + + +
            + +
            +
            +
            + + [[admin/manage/privileges:edit-privileges]] + + + [[admin/manage/categories:view-category]] + + +
            + + +
            +
            +
            +
            +
            + + diff --git a/src/views/admin/manage/digest.tpl b/src/views/admin/manage/digest.tpl new file mode 100644 index 0000000000..ebf5b9838f --- /dev/null +++ b/src/views/admin/manage/digest.tpl @@ -0,0 +1,52 @@ +

            [[admin/manage/digest:lead]]

            +

            [[admin/manage/digest:disclaimer]]

            +

            [[admin/manage/digest:disclaimer-continued]]

            + +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            [[admin/manage/digest:user]][[admin/manage/digest:subscription]][[admin/manage/digest:last-delivery]]
            {buildAvatar(delivery, "sm", true)} {../username}{{{if ../setting}}}{../setting}{{{else}}}[[admin/manage/digest:default]]{{{end}}}{../lastDelivery}
            +
            + [[admin/manage/digest:no-delivery-data]] +
            +
            + [[admin/manage/digest:default-help, {default}]] +
            + [[admin/manage/digest:manual-run]] + + + + +
            diff --git a/src/views/admin/manage/group.tpl b/src/views/admin/manage/group.tpl new file mode 100644 index 0000000000..c96d6c0b0b --- /dev/null +++ b/src/views/admin/manage/group.tpl @@ -0,0 +1,165 @@ +
            +
            +
            +
            +
            + + readonly/>
            +
            + +
            + +
            +
            + +
            +
            +
            + + {group.userTitle} +
            +
            +
            +
            +
            +
            +
            +
            +
            +
            +
            + + +
            +
            +
            +
            +
            + +
            +
            +
            +
            +
            +
            + +
            +
            + +
            +
            + +
            +
            + +
            +
            + +
            +
            + +
            +
            + +
            +
            + +
            +
            + +
            +
            + +
            + +
            + +
            +
            + +
            +
            + +
            +
            +
            + +
            + +
            +
            +
            +

            [[admin/manage/groups:edit.members]]

            +
            +
            + +
            +
            +
            +
            +
            +
            +
            + + + +
            +
            +
            + [[admin/manage/privileges:edit-privileges]]
            + +
            +
            +
            +
            + + + + diff --git a/src/views/admin/manage/groups.tpl b/src/views/admin/manage/groups.tpl new file mode 100644 index 0000000000..e98ae0c1b8 --- /dev/null +++ b/src/views/admin/manage/groups.tpl @@ -0,0 +1,114 @@ +
            +
            +
            + + +
            +
            +
            +
            +
            + + + + + + + + + + + + + + + + + + + + + + + + + +
            [[admin/manage/groups:name]][[admin/manage/groups:badge]][[admin/manage/groups:properties]]
            + {groups.displayName} ({groups.memberCount}) +

            {groups.description}

            +
            + {groups.userTitle} + + + [[admin/manage/groups:system]] + + + [[admin/manage/groups:private]] + + + [[admin/manage/groups:hidden]] + + + +


            + + +
            + + +
            + + diff --git a/src/views/admin/manage/privileges.tpl b/src/views/admin/manage/privileges.tpl new file mode 100644 index 0000000000..a752a27d8c --- /dev/null +++ b/src/views/admin/manage/privileges.tpl @@ -0,0 +1,32 @@ +
            +
            +
            +

            + [[admin/manage/categories:privileges.description]] +

            + +
            + [[admin/manage/categories:privileges.category-selector]] + +
            + +
            + {{{ if cid }}} + + {{{ else }}} + + {{{ end }}} +
            +
            +
            +
            + +
            + + + +
            \ No newline at end of file diff --git a/src/views/admin/manage/registration.tpl b/src/views/admin/manage/registration.tpl new file mode 100644 index 0000000000..44112ff674 --- /dev/null +++ b/src/views/admin/manage/registration.tpl @@ -0,0 +1,132 @@ +
            +
            +
            +
            + [[admin/manage/registration:queue]] +
            + +

            + [[admin/manage/registration:description, {config.relative_path}/admin/settings/user#user-registration]] +

            + +
            + + + + + + + + + + + + + + + {{{ each users }}} + + + + + + + {{{ each users.customRows }}} + + {{{ end }}} + + + + {{{ end }}} + +
            [[admin/manage/registration:list.name]][[admin/manage/registration:list.email]]
            + + + + + + + + {users.username} + + + + + + + + + {users.email} + +
            + + + {{{ each users.customActions }}} + + {{{ end }}} +
            +
            +
            + + +
            + +
            +
            + [[admin/manage/registration:invitations]] +
            +

            + [[admin/manage/registration:invitations.description]] +

            +
            + + + + + + + + + + {{{ each invites }}} + {{{ each invites.invitations }}} + + + + + + {{{ end }}} + {{{ end }}} + +
            [[admin/manage/registration:invitations.inviter-username]][[admin/manage/registration:invitations.invitee-email]][[admin/manage/registration:invitations.invitee-username]]
            {invites.username}{invites.invitations.email}{invites.invitations.username} +
            + +
            +
            +
            +
            +
            +
            diff --git a/src/views/admin/manage/tags.tpl b/src/views/admin/manage/tags.tpl new file mode 100644 index 0000000000..8c24d8b4c4 --- /dev/null +++ b/src/views/admin/manage/tags.tpl @@ -0,0 +1,80 @@ +
            +
            +
            +
            +
            +

            [[admin/manage/tags:description]]

            +
            + + + [[admin/manage/tags:none]] + + +
            + +
            +
            + + {tags.score} + {tags.valueEscaped} + +
            +
            + +
            +
            +
            +
            + +
            +
            +
            + + + +
            + + + [[admin/manage/tags:settings]] + +
            +
            + +
            +
            +
            +
            +
            +
            + + + + +
            diff --git a/src/views/admin/manage/uploads.tpl b/src/views/admin/manage/uploads.tpl new file mode 100644 index 0000000000..d4e83407df --- /dev/null +++ b/src/views/admin/manage/uploads.tpl @@ -0,0 +1,58 @@ + +
            +
            +
            + +
            +
            + +
            +
            +
            + +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            [[admin/manage/uploads:filename]][[admin/manage/uploads:usage]][[admin/manage/uploads:size/filecount]]
            + {files.name} + + {files.name} + + {{{ each ../inPids }}} + {@value} + {{{ end }}} + + [[admin/manage/uploads:orphaned]] + + {files.sizeHumanReadable}[[admin/manage/uploads:filecount, {files.fileCount}]]
            +
            + + \ No newline at end of file diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl new file mode 100644 index 0000000000..3c71219dbd --- /dev/null +++ b/src/views/admin/manage/users.tpl @@ -0,0 +1,139 @@ +
            +
            +
            + + + +
            +
            + + +
            + + + + +
            +
            + +
            + + + + + [[admin/manage/users:inactive.3-months]] + [[admin/manage/users:inactive.6-months]] + [[admin/manage/users:inactive.12-months]] + + +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            [[admin/manage/users:users.uid]][[admin/manage/users:users.username]][[admin/manage/users:users.email]][[admin/manage/users:users.ip]][[admin/manage/users:users.postcount]] {{{if sort_postcount}}}{{{end}}}[[admin/manage/users:users.reputation]] {{{if sort_reputation}}}{{{end}}}[[admin/manage/users:users.flags]] {{{if sort_flags}}}{{{end}}}[[admin/manage/users:users.joined]] {{{if sort_joindate}}}{{{end}}}[[admin/manage/users:users.last-online]] {{{if sort_lastonline}}}{{{end}}}
            {users.uid} + + + {users.username} + + {{{ if ../email }}} + + + {../email} + {{{ else }}} + + [[admin/manage/users:users.no-email]] + {{{ end }}} + {users.ip}{users.postcount}{users.reputation}{users.flags}0
            +
            + + + +
            +
            diff --git a/src/views/admin/partials/api/sorted-list/form.tpl b/src/views/admin/partials/api/sorted-list/form.tpl new file mode 100644 index 0000000000..b69447aecb --- /dev/null +++ b/src/views/admin/partials/api/sorted-list/form.tpl @@ -0,0 +1,15 @@ +
            + + +
            + + +

            + [[admin/settings/api:uid-help-text]] +

            +
            +
            + + +
            +
            \ No newline at end of file diff --git a/src/views/admin/partials/api/sorted-list/item.tpl b/src/views/admin/partials/api/sorted-list/item.tpl new file mode 100644 index 0000000000..8c8daaa11f --- /dev/null +++ b/src/views/admin/partials/api/sorted-list/item.tpl @@ -0,0 +1,21 @@ +
          • +
            +
            + {{{ if uid }}}uid {uid}{{{ else }}}master{{{ end }}} + {{{ if token }}}{{{ else }}}[[admin/settings/api:token-on-save]]{{{ end }}}
            +

            + {{{ if description }}} + {description} + {{{ else }}} + [[admin/settings/api:no-description]] + {{{ end }}} +
            + {timestampISO} +

            +
            +
            + + +
            +
            +
          • \ No newline at end of file diff --git a/src/views/admin/partials/blacklist-validate.tpl b/src/views/admin/partials/blacklist-validate.tpl new file mode 100644 index 0000000000..92ea018c73 --- /dev/null +++ b/src/views/admin/partials/blacklist-validate.tpl @@ -0,0 +1,14 @@ +

            + [[ip-blacklist:validate.x-valid, {valid.length}, {numRules}]] +

            + + +

            + [[ip-blacklist:validate.x-invalid, {invalid.length}]] +

            +
              + +
            • {@value}
            • + +
            + \ No newline at end of file diff --git a/src/views/admin/partials/categories/category-rows.tpl b/src/views/admin/partials/categories/category-rows.tpl new file mode 100644 index 0000000000..7c4429766e --- /dev/null +++ b/src/views/admin/partials/categories/category-rows.tpl @@ -0,0 +1,60 @@ + diff --git a/src/views/admin/partials/categories/copy-settings.tpl b/src/views/admin/partials/categories/copy-settings.tpl new file mode 100644 index 0000000000..ab1f13c36a --- /dev/null +++ b/src/views/admin/partials/categories/copy-settings.tpl @@ -0,0 +1,7 @@ + +
            + +
            \ No newline at end of file diff --git a/src/views/admin/partials/categories/create.tpl b/src/views/admin/partials/categories/create.tpl new file mode 100644 index 0000000000..fe3d303298 --- /dev/null +++ b/src/views/admin/partials/categories/create.tpl @@ -0,0 +1,27 @@ +
            +
            + + +
            +
            + + +
            + +
            + + + +
            + +
            + +
            + +
            \ No newline at end of file diff --git a/src/views/admin/partials/categories/groups.tpl b/src/views/admin/partials/categories/groups.tpl new file mode 100644 index 0000000000..f88bc18c8c --- /dev/null +++ b/src/views/admin/partials/categories/groups.tpl @@ -0,0 +1,20 @@ + +
          • + + {groups.displayName} +
          • + diff --git a/src/views/admin/partials/categories/purge.tpl b/src/views/admin/partials/categories/purge.tpl new file mode 100644 index 0000000000..c20b70a0ab --- /dev/null +++ b/src/views/admin/partials/categories/purge.tpl @@ -0,0 +1,8 @@ +
            + [[admin/manage/categories:alert.confirm-purge, {name}]] +
            +
            +
            +
            +
            +
            \ No newline at end of file diff --git a/src/views/admin/partials/categories/select-category.tpl b/src/views/admin/partials/categories/select-category.tpl new file mode 100644 index 0000000000..ee4f8d1a96 --- /dev/null +++ b/src/views/admin/partials/categories/select-category.tpl @@ -0,0 +1,25 @@ +
            +
            +
            + + + +
            +
            +
            +{{{ if message }}} +
            {message}
            +{{{ end }}} \ No newline at end of file diff --git a/src/views/admin/partials/categories/users.tpl b/src/views/admin/partials/categories/users.tpl new file mode 100644 index 0000000000..aa31fdb39d --- /dev/null +++ b/src/views/admin/partials/categories/users.tpl @@ -0,0 +1,22 @@ + +
          • + + {users.username} +
          • + diff --git a/src/views/admin/partials/create_user_modal.tpl b/src/views/admin/partials/create_user_modal.tpl new file mode 100644 index 0000000000..863e8158a2 --- /dev/null +++ b/src/views/admin/partials/create_user_modal.tpl @@ -0,0 +1,21 @@ + +
            +
            + + +
            +
            + + +
            + +
            + + +
            + +
            + + +
            +
            diff --git a/src/views/admin/partials/dashboard/graph.tpl b/src/views/admin/partials/dashboard/graph.tpl new file mode 100644 index 0000000000..6c699a75aa --- /dev/null +++ b/src/views/admin/partials/dashboard/graph.tpl @@ -0,0 +1,35 @@ +
            +
            + [[admin/dashboard:forum-traffic]] +
            + +
            +
            + +
            +
            +
            +
            + +
            +
            +
            + +
            +
            {{{ if summary.week }}}{./summary.week}{{{ else }}}0{{{ end }}}
            + +
            + + +
            +
            +
            \ No newline at end of file diff --git a/src/views/admin/partials/dashboard/stats.tpl b/src/views/admin/partials/dashboard/stats.tpl new file mode 100644 index 0000000000..0804ab3b38 --- /dev/null +++ b/src/views/admin/partials/dashboard/stats.tpl @@ -0,0 +1,47 @@ +
            + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            [[admin/dashboard:stats.yesterday]][[admin/dashboard:stats.today]][[admin/dashboard:stats.last-week]][[admin/dashboard:stats.this-week]][[admin/dashboard:stats.last-month]][[admin/dashboard:stats.this-month]][[admin/dashboard:stats.all]]
            + + {{{ if ../href }}} + {../name} + {{{ else }}} + {../name} + {{{ end }}} + + {stats.yesterday}{stats.today}{stats.dayIncrease}%{stats.lastweek}{stats.thisweek}{stats.weekIncrease}%{stats.lastmonth}{stats.thismonth}{stats.monthIncrease}%{stats.alltime}
            +
            \ No newline at end of file diff --git a/src/views/admin/partials/download_plugin_item.tpl b/src/views/admin/partials/download_plugin_item.tpl new file mode 100644 index 0000000000..04ce28ed40 --- /dev/null +++ b/src/views/admin/partials/download_plugin_item.tpl @@ -0,0 +1,25 @@ +
          • +
            + + +
            + +

            {../name}

            + + +

            {../description}

            + + + [[admin/extend/plugins:plugin-item.latest]] {../latest} +

            + + [[admin/extend/plugins:plugin-item.compatible, {version}]] + + [[admin/extend/plugins:plugin-item.not-compatible]] + +

            + + +

            [[admin/extend/plugins:plugin-item.more-info]] {../url}

            + +
          • diff --git a/src/views/admin/partials/groups/add-members.tpl b/src/views/admin/partials/groups/add-members.tpl new file mode 100644 index 0000000000..7db00be2bb --- /dev/null +++ b/src/views/admin/partials/groups/add-members.tpl @@ -0,0 +1,8 @@ + + + +
            + {{{each users}}} + {users.username} + {{{end}}} +
            diff --git a/src/views/admin/partials/groups/memberlist.tpl b/src/views/admin/partials/groups/memberlist.tpl new file mode 100644 index 0000000000..ffbe0b9aaf --- /dev/null +++ b/src/views/admin/partials/groups/memberlist.tpl @@ -0,0 +1,46 @@ +
            + +
            + +
            + +
            +
            + + +
            +
            +
            + + + + + + + + + + +
            + + + + +
            {group.members.icon:text}
            + +
            +
            + {group.members.username} + + +
            + + + + + + + +
            + +
            \ No newline at end of file diff --git a/src/views/admin/partials/groups/privileges-select-category.tpl b/src/views/admin/partials/groups/privileges-select-category.tpl new file mode 100644 index 0000000000..9100e5ddf3 --- /dev/null +++ b/src/views/admin/partials/groups/privileges-select-category.tpl @@ -0,0 +1,18 @@ +
            + + + +
            \ No newline at end of file diff --git a/src/views/admin/partials/installed_plugin_item.tpl b/src/views/admin/partials/installed_plugin_item.tpl new file mode 100644 index 0000000000..2975b844fb --- /dev/null +++ b/src/views/admin/partials/installed_plugin_item.tpl @@ -0,0 +1,60 @@ + +
          • +
            + {{{ if ../installed }}} + + [[admin/extend/plugins:plugin-item.themes]] + + + + + + + + + [[admin/extend/plugins:plugin-item.settings]] + + + {{{ else }}} + + {{{ end }}} +
            + +

            {../name}

            + + +

            {../description}

            + + + [[admin/extend/plugins:plugin-item.installed]] {../version} | [[admin/extend/plugins:plugin-item.latest]] {../latest} + + + +

            + + [[admin/extend/plugins:plugin-item.compatible, {version}]] + + [[admin/extend/plugins:plugin-item.not-compatible]] + +

            + + + +

            [[admin/extend/plugins:plugin-item.more-info]] {../url}

            + +
          • + + +
          • +
            + + +
            + +

            {../id}

            +

            + [[admin/extend/plugins:plugin-item.unknown-explanation]] +

            +
          • + diff --git a/src/views/admin/partials/manage_user_groups.tpl b/src/views/admin/partials/manage_user_groups.tpl new file mode 100644 index 0000000000..a7599f9675 --- /dev/null +++ b/src/views/admin/partials/manage_user_groups.tpl @@ -0,0 +1,13 @@ +{{{ each users }}} +
            +
            {users.username}
            +
            + {{{ each users.groups }}} + + {{{ end }}} +
            + +
            +{{{ end }}} \ No newline at end of file diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl new file mode 100644 index 0000000000..8074b84d4a --- /dev/null +++ b/src/views/admin/partials/menu.tpl @@ -0,0 +1,303 @@ + + +
            + \ No newline at end of file diff --git a/src/views/admin/partials/pageviews-range-select.tpl b/src/views/admin/partials/pageviews-range-select.tpl new file mode 100644 index 0000000000..20e942470e --- /dev/null +++ b/src/views/admin/partials/pageviews-range-select.tpl @@ -0,0 +1,20 @@ +
            +
            + +
            +
            +
            + + +
            +
            +
            +
            + + +
            +
            +
            +

            [[admin/dashboard:page-views-custom-help]]

            +
            +
            \ No newline at end of file diff --git a/src/views/admin/partials/plugins/license.tpl b/src/views/admin/partials/plugins/license.tpl new file mode 100644 index 0000000000..6c1bef8f04 --- /dev/null +++ b/src/views/admin/partials/plugins/license.tpl @@ -0,0 +1,5 @@ +[[admin/extend/plugins:license.intro, {name}, {license.name}]] + +
            {license.text}
            + +[[admin/extend/plugins:license.cta]] \ No newline at end of file diff --git a/src/views/admin/partials/plugins/no-plugins.tpl b/src/views/admin/partials/plugins/no-plugins.tpl new file mode 100644 index 0000000000..a31721c962 --- /dev/null +++ b/src/views/admin/partials/plugins/no-plugins.tpl @@ -0,0 +1 @@ +
            [[admin/extend/plugins:none-found]]
            \ No newline at end of file diff --git a/src/views/admin/partials/privileges/category.tpl b/src/views/admin/partials/privileges/category.tpl new file mode 100644 index 0000000000..f80554dd16 --- /dev/null +++ b/src/views/admin/partials/privileges/category.tpl @@ -0,0 +1,137 @@ + + + + + + + + + + {{{ each privileges.labels.groups }}} + + {{{ end }}} + + + + + + + + + {function.spawnPrivilegeStates, privileges.groups.name, ../privileges} + + + + + + + + + +
            + + + + + + +
            [[admin/manage/categories:privileges.section-group]][[admin/manage/privileges:select-clear-all]]{@value}
            + {{{ if privileges.groups.isPrivate }}} + {{{ if (privileges.groups.name == "banned-users") }}} + + {{{ else }}} + + {{{ end }}} + {{{ else }}} + + {{{ end }}} + {privileges.groups.name} + + +
            +
            + + + + +
            +
            +
            + [[admin/manage/categories:privileges.inherit]] +
            +
            + + + + + + + + + + {{{ each privileges.labels.users }}} + + {{{ end }}} + + + + + + + + + {function.spawnPrivilegeStates, privileges.users.username, ../privileges} + + + + + + + + + +
            + + + + + + +
            [[admin/manage/categories:privileges.section-user]][[admin/manage/privileges:select-clear-all]]{@value}
            + + + +
            {../icon:text}
            + +
            + {{{ if privileges.users.banned }}} + + {{{ end }}} + {privileges.users.username} +
            + +
            diff --git a/src/views/admin/partials/privileges/global.tpl b/src/views/admin/partials/privileges/global.tpl new file mode 100644 index 0000000000..c180de1723 --- /dev/null +++ b/src/views/admin/partials/privileges/global.tpl @@ -0,0 +1,118 @@ + + + + {{{ if !isAdminPriv }}} + + + + {{{ end }}} + + + + {{{ each privileges.labels.groups }}} + + {{{ end }}} + + + + + + + + + {function.spawnPrivilegeStates, privileges.groups.name, ../privileges} + + + + + + + + + +
            + + + + + + +
            [[admin/manage/categories:privileges.section-group]][[admin/manage/privileges:select-clear-all]]{@value}
            + {{{ if privileges.groups.isPrivate }}} + {{{ if (privileges.groups.name == "banned-users") }}} + + {{{ else }}} + + {{{ end }}} + {{{ else }}} + + {{{ end }}} + {privileges.groups.name} +
            +
            + +
            +
            +
            + [[admin/manage/categories:privileges.inherit]] +
            +
            + + + + {{{ if !isAdminPriv }}} + + + + {{{ end }}} + + + + {{{ each privileges.labels.users }}} + + {{{ end }}} + + + + + + + + + {function.spawnPrivilegeStates, privileges.users.username, ../privileges} + + + + + + + + + +
            + + + + + + +
            [[admin/manage/categories:privileges.section-user]][[admin/manage/privileges:select-clear-all]]{@value}
            + + + +
            {../icon:text}
            + +
            + {{{ if privileges.users.banned }}} + + {{{ end }}} + {privileges.users.username} +
            + +
            diff --git a/src/views/admin/partials/quick_actions/alerts.tpl b/src/views/admin/partials/quick_actions/alerts.tpl new file mode 100644 index 0000000000..a081a18273 --- /dev/null +++ b/src/views/admin/partials/quick_actions/alerts.tpl @@ -0,0 +1,10 @@ +
            + [[admin/menu:alerts.version, {version}]] + + + + [[admin/menu:alerts.upgrade, {latestVersion}]] + + + +
            \ No newline at end of file diff --git a/src/views/admin/partials/quick_actions/buttons.tpl b/src/views/admin/partials/quick_actions/buttons.tpl new file mode 100644 index 0000000000..504a6a3312 --- /dev/null +++ b/src/views/admin/partials/quick_actions/buttons.tpl @@ -0,0 +1,24 @@ +
          • + + + +
          • + +{{{ if user.privileges.superadmin }}} +
          • + + + +
          • +
          • + + + +
          • +{{{ end }}} + +
          • + + + +
          • \ No newline at end of file diff --git a/src/views/admin/partials/settings/footer.tpl b/src/views/admin/partials/settings/footer.tpl new file mode 100644 index 0000000000..6c3f416795 --- /dev/null +++ b/src/views/admin/partials/settings/footer.tpl @@ -0,0 +1,5 @@ +
            + + diff --git a/src/views/admin/partials/settings/header.tpl b/src/views/admin/partials/settings/header.tpl new file mode 100644 index 0000000000..5d15cc5a68 --- /dev/null +++ b/src/views/admin/partials/settings/header.tpl @@ -0,0 +1,11 @@ +
            +
            +
            + [[admin/admin:settings-header-contents]] +
            +
            + +
            +
            \ No newline at end of file diff --git a/src/views/admin/partials/temporary-ban.tpl b/src/views/admin/partials/temporary-ban.tpl new file mode 100644 index 0000000000..d7f1a59a11 --- /dev/null +++ b/src/views/admin/partials/temporary-ban.tpl @@ -0,0 +1,32 @@ +
            +
            +
            +
            + + +
            +
            +
            +
            + + +
            +
            +
            +
            +
            +
            + + +    + + +
            +
            +
            +

            + [[admin/manage/users:temp-ban.explanation]] +

            +
            +
            +
            diff --git a/src/views/admin/partials/temporary-mute.tpl b/src/views/admin/partials/temporary-mute.tpl new file mode 100644 index 0000000000..d4977ce67f --- /dev/null +++ b/src/views/admin/partials/temporary-mute.tpl @@ -0,0 +1,27 @@ +
            +
            +
            +
            + + +
            +
            +
            +
            + + +
            +
            +
            +
            +
            +
            + + +    + + +
            +
            +
            +
            diff --git a/src/views/admin/partials/theme_list.tpl b/src/views/admin/partials/theme_list.tpl new file mode 100644 index 0000000000..0d809bc899 --- /dev/null +++ b/src/views/admin/partials/theme_list.tpl @@ -0,0 +1,24 @@ + +
            data-css="{themes.css}"> + +
            + diff --git a/src/views/admin/partials/widget-settings.tpl b/src/views/admin/partials/widget-settings.tpl new file mode 100644 index 0000000000..2549b27d1b --- /dev/null +++ b/src/views/admin/partials/widget-settings.tpl @@ -0,0 +1,13 @@ +
            + +
            + + + + +
            + + +
            + +
            diff --git a/src/views/admin/partials/widgets/show_hide_groups.tpl b/src/views/admin/partials/widgets/show_hide_groups.tpl new file mode 100644 index 0000000000..644ef90e76 --- /dev/null +++ b/src/views/admin/partials/widgets/show_hide_groups.tpl @@ -0,0 +1,18 @@ +
            +
            + + +
            +
            + + +
            +
            \ No newline at end of file diff --git a/src/views/admin/settings/advanced.tpl b/src/views/admin/settings/advanced.tpl new file mode 100644 index 0000000000..83a546677b --- /dev/null +++ b/src/views/admin/settings/advanced.tpl @@ -0,0 +1,226 @@ + + +
            +
            [[admin/settings/advanced:maintenance-mode]]
            +
            +
            +
            + +
            +

            + [[admin/settings/advanced:maintenance-mode.help]] +

            +
            + + +
            +
            + + +
            +
            + + +
            +
            +
            +
            + +
            +
            [[admin/settings/advanced:headers]]
            +
            +
            +
            + +
            +

            + [[admin/settings/advanced:headers.csp-frame-ancestors-help]] +

            +
            +
            + +
            +
            +
            + +
            +

            + [[admin/settings/advanced:headers.acao-help]] +

            +
            +
            + +
            +

            + [[admin/settings/advanced:headers.acao-regex-help]] +

            +
            +
            + +
            +
            +
            + +
            +
            +
            + +
            +
            +
            + +
            +

            [[admin/settings/advanced:headers.coep-help]]

            +
            + + +
            + +
            + + +
            +
            + +
            + + +

            [[admin/settings/advanced:headers.permissions-policy-help]]

            +
            +
            +
            +
            + +
            +
            [[admin/settings/advanced:hsts]]
            +
            +
            +
            + +
            +
            + +
            +
            +
            + +
            +
            + +
            +

            + [[admin/settings/advanced:hsts.help, https:\/\/hstspreload.org\/]] +

            +
            +
            +
            + +
            +
            [[admin/settings/advanced:traffic-management]]
            +
            +

            + [[admin/settings/advanced:traffic.help]] +

            +
            +
            + +
            +
            + + +

            + [[admin/settings/advanced:traffic.event-lag-help]] +

            +
            +
            + + +

            + [[admin/settings/advanced:traffic.lag-check-interval-help]] +

            +
            +
            +
            +
            + +
            +
            [[admin/settings/advanced:sockets.settings]]
            +
            +
            +
            + + +
            +
            + + +
            +
            +
            +
            + +
            +
            [[admin/settings/advanced:analytics.settings]]
            +
            +
            +
            + + +

            + [[admin/settings/advanced:analytics.max-cache-help]] +

            +
            +
            +
            +
            + +
            +
            [[admin/settings/advanced:compression.settings]]
            +
            +
            +
            +

            + [[admin/settings/advanced:compression.help]] +

            +
            + +
            +
            +
            +
            +
            + + diff --git a/src/views/admin/settings/api.tpl b/src/views/admin/settings/api.tpl new file mode 100644 index 0000000000..1c5292bfb2 --- /dev/null +++ b/src/views/admin/settings/api.tpl @@ -0,0 +1,40 @@ +
            +

            [[admin/settings/api:lead-text]]

            +

            [[admin/settings/api:intro]]

            +

            + + + [[admin/settings/api:docs]] + +

            + +
            + +
            +
            [[admin/settings/api:settings]]
            +
            +
            + +
            +

            [[admin/settings/api:require-https-caveat]]

            +
            +
            + +
            +
            [[admin/settings/api:tokens]]
            +
            +
            + +
              + +
              +
              +
              +
              + + diff --git a/src/views/admin/settings/chat.tpl b/src/views/admin/settings/chat.tpl new file mode 100644 index 0000000000..e7abb2093a --- /dev/null +++ b/src/views/admin/settings/chat.tpl @@ -0,0 +1,59 @@ + + + +
              +
              [[admin/settings/chat:chat-settings]]
              +
              +
              +
              + +
              +
              + +
              +
              + +
              +

              [[admin/settings/chat:disable-editing-help]]

              +
              + +
              + + +
              + +
              + + +
              + +
              + + +
              + +
              + + +
              + + +
              + + +
              + +
              + + +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/cookies.tpl b/src/views/admin/settings/cookies.tpl new file mode 100644 index 0000000000..2fb9df32a7 --- /dev/null +++ b/src/views/admin/settings/cookies.tpl @@ -0,0 +1,74 @@ + + +
              +
              [[admin/settings/cookies:eu-consent]]
              +
              +
              +
              +
              + +
              +
              +
              + + +

              + [[admin/settings/cookies:consent.blank-localised-default]] +

              +
              +
              + + +

              + [[admin/settings/cookies:consent.blank-localised-default]] +

              +
              +
              + + +

              + [[admin/settings/cookies:consent.blank-localised-default]] +

              +
              +
              + + +
              +
              +
              +
              + +
              +
              Settings
              +
              +
              +
              + +
              +

              + [[admin/settings/cookies:blank-default]] +

              +
              + +
              + +
              +

              + [[admin/settings/cookies:blank-default]] +

              +
              + +
              + +

              + This will delete all sessions, you will be logged out and will have to login again! +

              +
              +
              +
              +
              + + diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl new file mode 100644 index 0000000000..9b5425ddd2 --- /dev/null +++ b/src/views/admin/settings/email.tpl @@ -0,0 +1,219 @@ + + +
              +
              [[admin/settings/email:email-settings]]
              +
              +
              +
              + +

              + [[admin/settings/email:address-help]] +

              +
              +
              + +
              + +

              + [[admin/settings/email:from-help]] +

              +
              +
              + +
              + +
              +

              [[admin/settings/email:require-email-address-warning]]

              + +
              + +
              + +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/email:confirmation-settings]]
              +
              +
              + + + +
              + +
              + + +
              + +
              + +
              + +
              + +
              +

              [[admin/settings/email:include-unverified-warning]]

              + +
              + +
              +

              [[admin/settings/email:prompt-help]]

              +
              +
              + +
              +
              [[admin/settings/email:subscriptions]]
              +
              +
              +
              + +
              + +
              + + +

              + [[admin/settings/email:subscriptions.hour-help]] +

              +
              +
              +
              +
              + +
              +
              [[admin/settings/email:smtp-transport]]
              +
              +
              +

              + [[admin/settings/email:smtp-transport-help]] +

              +
              +
              +
              + +
              +
              +
              + +
              +

              + [[admin/settings/email:smtp-transport.pool-help]] +

              +
              +
              + + +

              + [[admin/settings/email:smtp-transport.service-help]] +
              + [[admin/settings/email:smtp-transport.gmail-warning1]] +
              + [[admin/settings/email:smtp-transport.gmail-warning2]] +

              +
              + +
              + + +

              + [[admin/settings/email:smtp-transport.username-help]] +

              +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/email:template]]
              +
              +
              + +
              +
              + +
              + +
              +
              + +
              +
              [[admin/settings/email:testing]]
              +
              +
              + + +
              + +

              + [[admin/settings/email:testing.send-help]] +

              +
              +
              + + diff --git a/src/views/admin/settings/general.tpl b/src/views/admin/settings/general.tpl new file mode 100644 index 0000000000..7112456041 --- /dev/null +++ b/src/views/admin/settings/general.tpl @@ -0,0 +1,226 @@ + + +
              +
              + [[admin/settings/general:site-settings]] +
              +
              +
              + + + + + + +

              + [[admin/settings/general:title.url-help]] +

              + +
              + +
              + + + +

              + [[admin/settings/general:browser-title-help]] +

              + + + +

              + [[admin/settings/general:title-layout-help]] +

              + + +
              + +
              +
              +
              +
              +
              + +
              +
              [[admin/settings/general:logo]]
              +
              +
              + +
              + + + + + +
              +
              + +
              + + +

              + [[admin/settings/general:logo.url-help]] +

              +
              +
              + + +
              + +
              + +
              + + + + + +
              +
              +
              +
              + +
              +
              + [[admin/settings/general:favicon]] +
              +
              +
              +
              + + + + + +
              +
              +
              +
              + +
              +
              + [[admin/settings/general:pwa]] +
              +
              +
              + +
              + + + + + +
              +

              + [[admin/settings/general:touch-icon.help]] +

              +
              + +
              + +
              + + + + + +
              +

              + [[admin/settings/general:maskable-icon.help]] +

              +
              +
              +
              + +
              +
              [[admin/settings/general:search]]
              +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              +
              + +
              +
              [[admin/settings/general:outgoing-links]]
              +
              +
              +
              + +
              + +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/general:site-colors]]
              +
              +
              + + + + + +

              + [[admin/settings/general:background-color-help]] +

              +
              +
              +
              + +
              +
              [[admin/settings/general:topic-tools]]
              +
              +
              + + +

              + [[admin/settings/general:undo-timeout-help]] +

              +
              +
              +
              + \ No newline at end of file diff --git a/src/views/admin/settings/group.tpl b/src/views/admin/settings/group.tpl new file mode 100644 index 0000000000..cd7678c666 --- /dev/null +++ b/src/views/admin/settings/group.tpl @@ -0,0 +1,54 @@ + + +
              +
              [[admin/settings/group:general]]
              +
              +
              +
              + +
              + +

              + [[admin/settings/group:private-groups.help]] +

              +

              + [[admin/settings/group:private-groups.warning]] +

              + +
              + +
              + +

              + [[admin/settings/group:allow-multiple-badges-help]] +

              + + + + + + +
              +
              +
              + +
              +
              [[admin/settings/group:cover-image]]
              +
              +
              + +

              + [[admin/settings/group:default-cover-help]] +

              +
              +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/guest.tpl b/src/views/admin/settings/guest.tpl new file mode 100644 index 0000000000..c25918e5ae --- /dev/null +++ b/src/views/admin/settings/guest.tpl @@ -0,0 +1,36 @@ + + +
              +
              [[admin/settings/guest:settings]]
              +
              +
              +
              + +
              +

              + [[admin/settings/guest:handles.enabled-help]] +

              +
              +
              +
              + +
              +
              +
              +
              + +
              +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/homepage.tpl b/src/views/admin/settings/homepage.tpl new file mode 100644 index 0000000000..bac0acb30d --- /dev/null +++ b/src/views/admin/settings/homepage.tpl @@ -0,0 +1,37 @@ + +
              +
              [[admin/settings/homepage:home-page]]
              +
              +

              + [[admin/settings/homepage:description]] +

              +
              +
              + + + +
              +
              + +
              +
              + + +
              +
              +
              +
              + + diff --git a/src/views/admin/settings/languages.tpl b/src/views/admin/settings/languages.tpl new file mode 100644 index 0000000000..c1622877c5 --- /dev/null +++ b/src/views/admin/settings/languages.tpl @@ -0,0 +1,34 @@ + + +
              +
              [[admin/settings/languages:language-settings]]
              +
              +

              + [[admin/settings/languages:description]] +

              + +
              +
              + + +
              +
              + +
              +
              +
              + +
              +
              +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/navigation.tpl b/src/views/admin/settings/navigation.tpl new file mode 100644 index 0000000000..edcd7b08a1 --- /dev/null +++ b/src/views/admin/settings/navigation.tpl @@ -0,0 +1,153 @@ + + + \ No newline at end of file diff --git a/src/views/admin/settings/notifications.tpl b/src/views/admin/settings/notifications.tpl new file mode 100644 index 0000000000..542c4adaab --- /dev/null +++ b/src/views/admin/settings/notifications.tpl @@ -0,0 +1,15 @@ + + +
              +
              [[admin/settings/notifications:notifications]]
              +
              +
              + [[admin/settings/notifications:welcome-notification]]

              + [[admin/settings/notifications:welcome-notification-link]]

              + [[admin/settings/notifications:welcome-notification-uid]]

              + [[admin/settings/notifications:post-queue-notification-uid]]

              +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/pagination.tpl b/src/views/admin/settings/pagination.tpl new file mode 100644 index 0000000000..92cc0d03e1 --- /dev/null +++ b/src/views/admin/settings/pagination.tpl @@ -0,0 +1,46 @@ + + +
              +
              [[admin/settings/pagination:pagination]]
              +
              +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/pagination:posts]]
              +
              +
              + [[admin/settings/pagination:posts-per-page]]

              + [[admin/settings/pagination:max-posts-per-page]]

              +
              +
              +
              + +
              +
              [[admin/settings/pagination:topics]]
              +
              +
              + [[admin/settings/pagination:topics-per-page]]

              + [[admin/settings/pagination:max-topics-per-page]]

              +
              +
              +
              + +
              +
              [[admin/settings/pagination:categories]]
              +
              +
              + [[admin/settings/pagination:categories-per-page]]

              +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl new file mode 100644 index 0000000000..f4b13c287a --- /dev/null +++ b/src/views/admin/settings/post.tpl @@ -0,0 +1,337 @@ + + +
              +
              [[admin/settings/post:sorting]]
              +
              +
              +
              + + +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:length]]
              +
              +
              +
              +
              +
              + + +
              +
              + + +
              +
              +
              +
              + + +
              +
              + + +
              +
              +
              +
              +
              +
              + +
              +
              [[admin/settings/post:restrictions]]
              +
              +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              + +
              + + +

              + [[admin/settings/post:restrictions.stale-help]] +

              +
              +
              +
              +
              + +
              +
              [[admin/settings/post:restrictions-new]]
              +
              +
              +
              + + +
              + +
              + + +
              + +
              + + +
              + +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:post-queue]]
              +
              +
              +
              +
              +
              +
              + +
              +

              + [[admin/settings/post:restrictions.post-queue-help]] +

              +
              +
              +
              +
              + + +
              +
              +
              +
              +
              + + +
              +
              +
              +
              +
              + +
              +
              [[admin/settings/post:timestamp]]
              +
              +
              +
              + + +

              + [[admin/settings/post:timestamp.cut-off-help]] +

              +
              +
              + + +

              + [[admin/settings/post:timestamp.necro-threshold-help]] +

              +
              +
              + + +

              + [[admin/settings/post:timestamp.topic-views-interval-help]] +

              +
              +
              +
              +
              + +
              +
              Teaser
              +
              +
              +
              + + +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:unread]]
              +
              +
              +
              + + +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:recent]]
              +
              +
              +
              + + +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:signature]]
              +
              +
              +
              + +
              +
              + +
              +
              + +
              +
              + +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:composer]]
              +
              +
              +

              + [[admin/settings/post:composer-help]] +

              +
              + +
              +
              + +
              +
              + + +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:backlinks]]
              +
              +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/post:ip-tracking]]
              +
              +
              +
              + +
              +
              +
              +
              + \ No newline at end of file diff --git a/src/views/admin/settings/reputation.tpl b/src/views/admin/settings/reputation.tpl new file mode 100644 index 0000000000..e8a973eae0 --- /dev/null +++ b/src/views/admin/settings/reputation.tpl @@ -0,0 +1,136 @@ + + + +
              +
              [[admin/settings/reputation:reputation]]
              +
              +
              +
              + +
              +
              + +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/reputation:thresholds]]
              +
              +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              + +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/reputation:flags]]
              +
              +
              +
              + + +

              + [[admin/settings/reputation:flags.limit-per-target-help]] +

              +
              +
              + + +
              +
              +
              +
              + + +
              +
              +
              +
              + + +
              +
              +
              +
              + +
              +
              +
              +
              + + diff --git a/src/views/admin/settings/social.tpl b/src/views/admin/settings/social.tpl new file mode 100644 index 0000000000..2bbe50a5de --- /dev/null +++ b/src/views/admin/settings/social.tpl @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/views/admin/settings/sockets.tpl b/src/views/admin/settings/sockets.tpl new file mode 100644 index 0000000000..468f41ab6f --- /dev/null +++ b/src/views/admin/settings/sockets.tpl @@ -0,0 +1,19 @@ + + +
              +
              [[admin/settings/sockets:reconnection]]
              +
              +
              +
              + + +
              +
              + + +
              +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/tags.tpl b/src/views/admin/settings/tags.tpl new file mode 100644 index 0000000000..471b7b3d54 --- /dev/null +++ b/src/views/admin/settings/tags.tpl @@ -0,0 +1,52 @@ + + +
              +
              [[admin/settings/tags:tag]]
              +
              +
              + +
              + + +

              + [[admin/settings/tags:system-tags-help]] +

              +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/tags:related-topics]]
              +
              +
              +
              + + +
              +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/uploads.tpl b/src/views/admin/settings/uploads.tpl new file mode 100644 index 0000000000..4e674b1b60 --- /dev/null +++ b/src/views/admin/settings/uploads.tpl @@ -0,0 +1,224 @@ + + +
              +
              + [[admin/settings/uploads:posts]] +
              +
              +
              +
              + +
              + +
              + +
              + +
              + + +

              + [[admin/settings/uploads:private-uploads-extensions-help]] +

              +
              + +
              +
              +
              + + +

              + [[admin/settings/uploads:resize-image-width-threshold-help]] +

              +
              +
              + +
              +
              + + +

              + [[admin/settings/uploads:resize-image-width-help]] +

              +
              +
              +
              + +
              + + +

              + [[admin/settings/uploads:resize-image-quality-help]] +

              +
              + +
              + + +

              + [[admin/settings/uploads:max-file-size-help]] +

              +
              + +
              + + +

              + [[admin/settings/uploads:reject-image-width-help]] +

              +
              + +
              + + +

              + [[admin/settings/uploads:reject-image-height-help]] +

              +
              + +
              + +
              + +
              + + +
              + +
              + + +

              + [[admin/settings/uploads:allowed-file-extensions-help]] +

              +
              + +
              + +
              +
              + +
              +
              + +
              +
              +
              +
              +
              +
              + +
              +
              + [[admin/settings/uploads:orphans]] +
              +
              +
              + +
              + +
              +
              + + +

              [[admin/settings/uploads:orphanExpiryDays-help]]

              +
              +
              +
              +
              + +
              +
              + [[admin/settings/uploads:profile-avatars]] +
              +
              +
              +
              + +
              + +
              + +
              + +
              + +
              + + + + +
              +
              + +
              + + +

              + [[admin/settings/uploads:profile-image-dimension-help]] +

              +
              + +
              + + +

              + [[admin/settings/uploads:max-profile-image-size-help]] +

              +
              + +
              + + +

              + [[admin/settings/uploads:max-cover-image-size-help]] +

              +
              + +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/uploads:profile-covers]]
              +
              +
              + +

              + [[admin/settings/uploads:default-covers-help]] +

              + +
              +
              +
              + + diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl new file mode 100644 index 0000000000..ead26403e7 --- /dev/null +++ b/src/views/admin/settings/user.tpl @@ -0,0 +1,365 @@ + + +
              +
              [[admin/settings/user:authentication]]
              +
              +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/user:account-settings]]
              +
              +
              +
              + +

              [[admin/settings/user:gdpr_enabled_help]]

              +
              +
              + +
              +
              + +
              +
              + +
              +
              + +
              +
              + +
              +
              + +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/user:themes]]
              +
              +
              +
              + +
              +
              +
              +
              + +
              +
              [[admin/settings/user:account-protection]]
              +
              +
              +
              + + +

              + [[admin/settings/user:admin-relogin-duration-help]] +

              +
              +
              + + +

              + [[admin/settings/user:login-attempts-help]] +

              +
              +
              + + +
              +
              + + +
              +
              +
              +
              + +
              +
              + [[admin/settings/user:session-time]] +
              +
              +
              +
              +
              +
              + + +
              +
              +
              +
              + + +
              +
              +
              +

              + [[admin/settings/user:session-time-help]] +

              +
              +
              +
              + + +

              [[admin/settings/user:online-cutoff-help]]

              +
              +
              +
              +
              + +
              +
              [[admin/settings/user:registration]]
              +
              +
              +
              + + +

              + [[admin/settings/user:registration-type.help, {config.relative_path}]] +

              +
              +
              + + +

              + [[admin/settings/user:registration-approval-type.help, {config.relative_path}]] +

              +
              +
              + + +

              + [[admin/settings/user:registration-queue-auto-approve-time-help]] +

              +
              +
              + +
              + +
              + +
              +

              [[admin/settings/email:require-email-address-warning]]

              + +
              + + +

              + [[admin/settings/user:max-invites-help]] +

              +
              +
              + + +

              + [[admin/settings/user:invite-expiration-help]] +

              +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              + + +
              +
              +
              +
              + +
              +
              [[admin/settings/user:user-search]]
              +
              +
              +
              + + +
              + +
              +
              +
              + +
              +
              [[admin/settings/user:default-user-settings]]
              +
              +
              +
              + +
              + +
              + +
              + +
              + +
              + +
              + +
              + +
              + +
              + +
              + +
              + +
              + + +
              + +
              + +
              + +
              + +
              + +
              + + +
              + + + + +
              +
              + +
              +
              + +
              +
              + + +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/admin/settings/web-crawler.tpl b/src/views/admin/settings/web-crawler.tpl new file mode 100644 index 0000000000..f3fee3a58f --- /dev/null +++ b/src/views/admin/settings/web-crawler.tpl @@ -0,0 +1,46 @@ + + +
              +
              [[admin/settings/web-crawler:crawlability-settings]]
              +
              +
              + [[admin/settings/web-crawler:robots-txt]]
              + +
              +
              +
              + +
              +
              [[admin/settings/web-crawler:sitemap-feed-settings]]
              +
              +
              +
              + +
              + +
              + +
              + +
              + + +
              + +
              +

              + + [[admin/settings/web-crawler:view-sitemap]] +

              + +
              +
              +
              + + \ No newline at end of file diff --git a/src/views/emails/banned.tpl b/src/views/emails/banned.tpl new file mode 100644 index 0000000000..55e43ddac5 --- /dev/null +++ b/src/views/emails/banned.tpl @@ -0,0 +1,48 @@ + + + + + + + + + + +
              + + + + + + + + + + + + + + + + + +
              +

              [[email:greeting_with_name, {username}]]

              +
              +

              [[email:banned.text1, {username}, {site_title}]]

              +
              +

              + [[email:banned.text3]] +

              +

              + {reason} +

              +
              +

              + [[email:banned.text2, {until}]] +

              +
              +
              + + + diff --git a/src/views/emails/digest.tpl b/src/views/emails/digest.tpl new file mode 100644 index 0000000000..4d5fd09871 --- /dev/null +++ b/src/views/emails/digest.tpl @@ -0,0 +1,174 @@ + + + + + + + + + + + +
              + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
              +

              [[email:greeting_with_name, {username}]]

              +
              +

              [[email:digest.title.{interval}]]

              +
              + +
              +

              [[email:digest.top-topics, {site_title}]]

              + +
              +

              [[email:digest.popular-topics, {site_title}]]

              + +
              +

              [[email:digest.latest_topics, {site_title}]]

              + +
              + + + + + +
              + + [[email:digest.cta, {site_title}]] → + +
              + +
              +
              + + + diff --git a/src/views/emails/invitation.tpl b/src/views/emails/invitation.tpl new file mode 100644 index 0000000000..c7c71bc440 --- /dev/null +++ b/src/views/emails/invitation.tpl @@ -0,0 +1,47 @@ + + + + + + + + + + +
              + + + + + + + + + + + + + +
              +

              [[email:greeting_no_name]]

              +
              +

              [[email:invitation.text1, {username}, {site_title}]]

              +
              +

              [[email:invitation.text2, {expireDays}]]

              +
              + + + + + +
              + + [[email:invitation.cta]] → + +
              + +
              +
              + + + diff --git a/src/views/emails/notification.tpl b/src/views/emails/notification.tpl new file mode 100644 index 0000000000..27c30a9c71 --- /dev/null +++ b/src/views/emails/notification.tpl @@ -0,0 +1,50 @@ + + + + + + + + + + + +
              + + + + + + + + + + + + + +
              +

              [[email:greeting_with_name, {username}]]

              +
              +

              {intro}

              +
              +

              + {body} +

              +
              + + + + + +
              + + [[email:notif.cta-{notification.cta-type}]] → + +
              + +
              +
              + + + diff --git a/src/views/emails/partials/footer.tpl b/src/views/emails/partials/footer.tpl new file mode 100644 index 0000000000..e56dc26e62 --- /dev/null +++ b/src/views/emails/partials/footer.tpl @@ -0,0 +1,26 @@ + + + + + +
              +

              + + [[email:notif.post.unsub.info]] [[email:unsub.cta]]. +
              [[email:notif.post.unsub.one-click]] [[email:unsubscribe]]. + +

              +
              + + + +
              + + + + + \ No newline at end of file diff --git a/src/views/emails/partials/header.tpl b/src/views/emails/partials/header.tpl new file mode 100644 index 0000000000..4efaf09487 --- /dev/null +++ b/src/views/emails/partials/header.tpl @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
              + + +

              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 Qf0t1!3;W|wj^R@N0}0i75C8xG literal 0 HcmV?d00001 diff --git a/public/language/README.md b/public/language/README.md new file mode 100644 index 0000000000..26c4db1304 --- /dev/null +++ b/public/language/README.md @@ -0,0 +1,14 @@ +# Important note about localising NodeBB + +The files here are read-only and overwritten daily (if there are changes) by the +helper bot [Misty](https://github.com/nodebb-misty). + +Our localisation efforts are handled via [our Transifex Project](https://explore.transifex.com/nodebb/nodebb/), +and any pull requests made to this directory will be automatically closed because +localisations can go out-of-sync when edited directly. + +If there are non-localised strings and you cannot find them in Transifex, please +[open a new issue on our bug tracker](https://github.com/NodeBB/NodeBB/issues/new) +so we can take a look. + +Thank you for helping localise NodeBB! \ No newline at end of file diff --git a/public/language/ar/_DO_NOT_EDIT_FILES_HERE.md b/public/language/ar/_DO_NOT_EDIT_FILES_HERE.md new file mode 100644 index 0000000000..1faf87ad65 --- /dev/null +++ b/public/language/ar/_DO_NOT_EDIT_FILES_HERE.md @@ -0,0 +1,3 @@ +# The files here are not meant to be edited directly + +Please see the → [Internalization README](../README.md). \ No newline at end of file diff --git a/public/language/ar/admin/admin.json b/public/language/ar/admin/admin.json new file mode 100644 index 0000000000..d4bac4e476 --- /dev/null +++ b/public/language/ar/admin/admin.json @@ -0,0 +1,11 @@ +{ + "alert.confirm-rebuild-and-restart": "هل أنت متأكد إنك تريد إعادة بناء وتشغيل الـ NodeBB؟", + "alert.confirm-restart": "هل تريد بالتأكيد إعادة تشغيل NodeBB؟", + + "acp-title": "لوحة تحكم إدارة NodeBB | %1", + "settings-header-contents": "محتويات", + "changes-saved": "Changes Saved", + "changes-saved-message": "Your changes to the NodeBB configuration have been saved.", + "changes-not-saved": "Changes Not Saved", + "changes-not-saved-message": "NodeBB encountered a problem saving your changes. (%1)" +} \ No newline at end of file diff --git a/public/language/ar/admin/advanced/cache.json b/public/language/ar/admin/advanced/cache.json new file mode 100644 index 0000000000..77ff9a4387 --- /dev/null +++ b/public/language/ar/admin/advanced/cache.json @@ -0,0 +1,9 @@ +{ + "post-cache": "التخزين المؤقت للمشاركات", + "group-cache": "Group Cache", + "local-cache": "Local Cache", + "object-cache": "Object Cache", + "percent-full": "1% كاملة", + "post-cache-size": "حجم التخزين المؤقت للمشاركات", + "items-in-cache": "العناصر في التخزين المؤقت" +} \ No newline at end of file diff --git a/public/language/ar/admin/advanced/database.json b/public/language/ar/admin/advanced/database.json new file mode 100644 index 0000000000..cb68e704a0 --- /dev/null +++ b/public/language/ar/admin/advanced/database.json @@ -0,0 +1,52 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Uptime in Seconds", + "uptime-days": "Uptime in Days", + + "mongo": "Mongo", + "mongo.version": "MongoDB Version", + "mongo.storage-engine": "Storage Engine", + "mongo.collections": "Collections", + "mongo.objects": "Objects", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Data Size", + "mongo.storage-size": "Storage Size", + "mongo.index-size": "Index Size", + "mongo.file-size": "File Size", + "mongo.resident-memory": "Resident Memory", + "mongo.virtual-memory": "الذاكرة الإفتراضية", + "mongo.mapped-memory": "Mapped Memory", + "mongo.bytes-in": "Bytes In", + "mongo.bytes-out": "Bytes Out", + "mongo.num-requests": "Number of Requests", + "mongo.raw-info": "MongoDB Raw Info", + "mongo.unauthorized": "NodeBB was unable to query the MongoDB database for relevant statistics. Please ensure that the user in use by NodeBB contains the "clusterMonitor" role for the "admin" database.", + + "redis": "Redis", + "redis.version": "Redis Version", + "redis.keys": "Keys", + "redis.expires": "Expires", + "redis.avg-ttl": "Average TTL", + "redis.connected-clients": "Connected Clients", + "redis.connected-slaves": "Connected Slaves", + "redis.blocked-clients": "Blocked Clients", + "redis.used-memory": "الذاكرة المستخدمة", + "redis.memory-frag-ratio": "Memory Fragmentation Ratio", + "redis.total-connections-recieved": "إجمالي الاتصالات المستلمة", + "redis.total-commands-processed": "إجمالي الأوامر التي تمت معالجتها", + "redis.iops": "Instantaneous Ops. Per Second", + "redis.iinput": "Instantaneous Input Per Second", + "redis.ioutput": "Instantaneous Output Per Second", + "redis.total-input": "Total Input", + "redis.total-output": "Total Ouput", + + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis Raw Info", + + "postgres": "Postgres", + "postgres.version": "PostgreSQL Version", + "postgres.raw-info": "Postgres Raw Info" +} diff --git a/public/language/ar/admin/advanced/errors.json b/public/language/ar/admin/advanced/errors.json new file mode 100644 index 0000000000..bf9bc97443 --- /dev/null +++ b/public/language/ar/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "شكل %1", + "error-events-per-day": "%1 حدث كل يوم ", + "error.404": "404 لم يتم العثور", + "error.503": "503 الخدمة غير متوفرة", + "manage-error-log": "إدارة سجل الأخطاء", + "export-error-log": "تصدير سجل الأخطاء (CSV)", + "clear-error-log": "محو سجل الأخطاء", + "route": "مسار", + "count": "عدد", + "no-routes-not-found": "لا توجد اخطاء 404!", + "clear404-confirm": "هل تريد بالتأكيد محو سجلات الخطأ 404؟", + "clear404-success": "أخطاء \"404 لم يتم العثور\" تم محوها بنجاح" +} \ No newline at end of file diff --git a/public/language/ar/admin/advanced/events.json b/public/language/ar/admin/advanced/events.json new file mode 100644 index 0000000000..218d088830 --- /dev/null +++ b/public/language/ar/admin/advanced/events.json @@ -0,0 +1,13 @@ +{ + "events": "أحداث", + "no-events": "لا توجد أحداث", + "control-panel": "لوحة تحكم الأحداث", + "delete-events": "حذف الاحداث", + "confirm-delete-all-events": "Are you sure you want to delete all logged events?", + "filters": "تصفية", + "filters-apply": "تطبيق التصفية", + "filter-type": "نوع الحدث", + "filter-start": "تاريخ البدء", + "filter-end": "تاريخ الانتهاء", + "filter-perPage": "لكل صفحة" +} \ No newline at end of file diff --git a/public/language/ar/admin/advanced/logs.json b/public/language/ar/admin/advanced/logs.json new file mode 100644 index 0000000000..cb6a87021b --- /dev/null +++ b/public/language/ar/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "السجلات", + "control-panel": "لوحة تحكم السجلات", + "reload": "إعادة تحميل السجلات", + "clear": "محو السجلات", + "clear-success": "تم محو السجلات!" +} \ No newline at end of file diff --git a/public/language/ar/admin/appearance/customise.json b/public/language/ar/admin/appearance/customise.json new file mode 100644 index 0000000000..036fa67df2 --- /dev/null +++ b/public/language/ar/admin/appearance/customise.json @@ -0,0 +1,16 @@ +{ + "custom-css": "Custom CSS/LESS", + "custom-css.description": "Enter your own CSS/LESS declarations here, which will be applied after all other styles.", + "custom-css.enable": "Enable Custom CSS/LESS", + + "custom-js": "Javascript مخصصة", + "custom-js.description": "أدخل Javascript الخاص بك هنا. سيتم تنفيذها بعد تحميل الصفحة بالكامل.", + "custom-js.enable": "تفعيل Javascript المخصصة", + + "custom-header": "ترويسة مخصصة", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.enable": "تفعيل الترويسة المخصصة", + + "custom-css.livereload": "تفعيل إعادة التحميل المباشرة", + "custom-css.livereload.description": "فعل هذا الخيار لإجبار جميع الجلسات في الأجهزة التي قمت بتسجيل الدخول فيها بحسابك على التحديث عند النقر على زر الحفظ" +} \ No newline at end of file diff --git a/public/language/ar/admin/appearance/skins.json b/public/language/ar/admin/appearance/skins.json new file mode 100644 index 0000000000..7c97d46cc6 --- /dev/null +++ b/public/language/ar/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "جاري تحميل السمات...", + "homepage": "الصفحة الرئيسية", + "select-skin": "إختيار السمة", + "current-skin": "السمة الحالية", + "skin-updated": "تم تحديث السمة", + "applied-success": "تم تطبيق السمة %1 بنجاح", + "revert-success": "تم إستعادة الألوان الاساسية للسمة" +} \ No newline at end of file diff --git a/public/language/ar/admin/appearance/themes.json b/public/language/ar/admin/appearance/themes.json new file mode 100644 index 0000000000..87dcc31000 --- /dev/null +++ b/public/language/ar/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "جاري التحقق من القوالب المثبتة...", + "homepage": "الصفحة الرئيسية", + "select-theme": "إختيار القالب", + "current-theme": "القالب المستخدم حالياً", + "no-themes": "لم يتم العثور على قوالب مثبتة", + "revert-confirm": "هل أنت متأكد من أنك ترغب في استعادة قااب NodeBB الافتراضي؟", + "theme-changed": "تم تغيير القالب", + "revert-success": "لقد قمت بنجاح بإستعادة القالب الأساسي لـNodeBB", + "restart-to-activate": "Please rebuild and restart your NodeBB to fully activate this theme." +} \ No newline at end of file diff --git a/public/language/ar/admin/dashboard.json b/public/language/ar/admin/dashboard.json new file mode 100644 index 0000000000..a54c39d931 --- /dev/null +++ b/public/language/ar/admin/dashboard.json @@ -0,0 +1,90 @@ +{ + "forum-traffic": "Forum Traffic", + "page-views": "مشاهدات الصفحات", + "unique-visitors": "زائرين فريدين", + "logins": "Logins", + "new-users": "New Users", + "posts": "مشاركات", + "topics": "مواضيع", + "page-views-seven": "آخر 7 ايام", + "page-views-thirty": "آخر 30 يوماً", + "page-views-last-day": "آخر 24 ساعة", + "page-views-custom": "مدة زمنية مخصصة", + "page-views-custom-start": "بداية المدة", + "page-views-custom-end": "نهاية المده", + "page-views-custom-help": "أدخل نطاقا زمنيا لمرات مشاهدة الصفحات التي ترغب في عرضها. إذا لم يظهر منتقي التاريخ، فإن التنسيق المقبول هو YYYY-MM-DD", + "page-views-custom-error": "الرجاء إدخال نطاق تاريخ صالح بالتنسيق YYYY-MM-DD", + + "stats.yesterday": "Yesterday", + "stats.today": "Today", + "stats.last-week": "Last Week", + "stats.this-week": "This Week", + "stats.last-month": "Last Month", + "stats.this-month": "This Month", + "stats.all": "كل الوقت", + + "updates": "تحديثات", + "running-version": "المنتدى يعمل حاليا على NodeBB الإصدار%1.", + "keep-updated": "تأكد دائما من أن NodeBB يعمل على احدث إصدار للحصول على أحدث التصحيحات الأمنية وإصلاحات الأخطاء.", + "up-to-date": "

              المنتدى يعمل على أحدث إصدار

              ", + "upgrade-available": "

              A new version (v%1) has been released. Consider upgrading your NodeBB.

              ", + "prerelease-upgrade-available": "

              This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

              ", + "prerelease-warning": "

              هذه نسخة ماقبل الإصدار من NodeBB. قد تحدث أخطاء غير مقصودة.

              ", + "fallback-emailer-not-found": "Fallback emailer not found!", + "running-in-development": "المنتدى قيد التشغيل في وضع \"المطورين\". وقد تكون هناك ثغرات أمنية مفتوحة؛ من فضلك تواصل مع مسؤول نظامك.", + "latest-lookup-failed": "

              Failed to look up latest available version of NodeBB

              ", + + "notices": "إشعارات", + "restart-not-required": "إعادة التشغيل غير مطلوب", + "restart-required": "إعادة التشغيل مطلوبة", + "search-plugin-installed": "إضافة البحث منصبة", + "search-plugin-not-installed": "إضافة البحث غير منصبة", + "search-plugin-tooltip": "نصب إضافة البحث من صفحة الإضافات البرمجية لتنشيط وظيفة البحث", + + "control-panel": "التحكم بالنظام", + "rebuild-and-restart": "Rebuild & Restart", + "restart": "Restart", + "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", + "restart-disabled": "Rebuilding and Restarting your NodeBB has been disabled as you do not seem to be running it via the appropriate daemon.", + "maintenance-mode": "وضع الصيانة", + "maintenance-mode-title": "انقر هنا لإعداد وضع الصيانة لـNodeBB", + "realtime-chart-updates": "التحديث الفوري للرسم البياني", + + "active-users": "المستخدمين النشطين", + "active-users.users": "الأعضاء", + "active-users.guests": "الزوار", + "active-users.total": "المجموع", + "active-users.connections": "Connections", + + "guest-registered-users": "Guest vs Registered Users", + "guest": "Guest", + "registered": "مسجل", + + "user-presence": "تواجد المستخدمين", + "on-categories": "في قائمة الأقسام", + "reading-posts": "قراءة المشاركات", + "browsing-topics": "تصفح المواضيع", + "recent": "الأخيرة", + "unread": "غير مقروء", + + "high-presence-topics": "مواضيع ذات حضور قوي", + "popular-searches": "Popular Searches", + + "graphs.page-views": "مشاهدات الصفحة", + "graphs.page-views-registered": "Page Views Registered", + "graphs.page-views-guest": "Page Views Guest", + "graphs.page-views-bot": "Page Views Bot", + "graphs.unique-visitors": "زوار فريدين", + "graphs.registered-users": "مستخدمين مسجلين", + "graphs.guest-users": "Guest Users", + "last-restarted-by": "Last restarted by", + "no-users-browsing": "No users browsing", + + "back-to-dashboard": "Back to Dashboard", + "details.no-users": "No users have joined within the selected timeframe", + "details.no-topics": "No topics have been posted within the selected timeframe", + "details.no-searches": "No searches have been made yet", + "details.no-logins": "No logins have been recorded within the selected timeframe", + "details.logins-static": "NodeBB only saves session data for %1 days, and so this table below will only show the most recently active sessions", + "details.logins-login-time": "Login Time" +} diff --git a/public/language/ar/admin/development/info.json b/public/language/ar/admin/development/info.json new file mode 100644 index 0000000000..11202d9c3a --- /dev/null +++ b/public/language/ar/admin/development/info.json @@ -0,0 +1,25 @@ +{ + "you-are-on": "You are on %1:%2", + "ip": "IP %1", + "nodes-responded": "%1 nodes responded within %2ms!", + "host": "host", + "primary": "primary / run jobs", + "pid": "pid", + "nodejs": "nodejs", + "online": "online", + "git": "git", + "process-memory": "process memory", + "system-memory": "system memory", + "used-memory-process": "Used memory by process", + "used-memory-os": "Used system memory", + "total-memory-os": "Total system memory", + "load": "system load", + "cpu-usage": "cpu usage", + "uptime": "uptime", + + "registered": "Registered", + "sockets": "Sockets", + "guests": "Guests", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/ar/admin/development/logger.json b/public/language/ar/admin/development/logger.json new file mode 100644 index 0000000000..6ab9558149 --- /dev/null +++ b/public/language/ar/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Logger Settings", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", + "enable-http": "Enable HTTP logging", + "enable-socket": "Enable socket.io event logging", + "file-path": "Path to log file", + "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", + + "control-panel": "Logger Control Panel", + "update-settings": "Update Logger Settings" +} \ No newline at end of file diff --git a/public/language/ar/admin/extend/plugins.json b/public/language/ar/admin/extend/plugins.json new file mode 100644 index 0000000000..f4aeaf4f3f --- /dev/null +++ b/public/language/ar/admin/extend/plugins.json @@ -0,0 +1,57 @@ +{ + "trending": "Trending", + "installed": "منصبة", + "active": "مفعلة", + "inactive": "معطلة", + "out-of-date": "غير محدثة", + "none-found": "لم يتم العثور على إضافات", + "none-active": "لا توجد إضافات مفعلة", + "find-plugins": "العثور على الإضافات", + + "plugin-search": "البحث عن الإضافات", + "plugin-search-placeholder": "جاري البحث عن الإضافات...", + "submit-anonymous-usage": "Submit anonymous plugin usage data.", + "reorder-plugins": "إعادة ترتيب الإضافات", + "order-active": "ترتيب الإضافات المفعلة", + "dev-interested": "هل انته مهتم ببرمجة إضافات لـNodeBB؟", + "docs-info": "دليل كامل حول برمجة الإضافات بالإمكان العثور عليه في NodeBB Docs Portal.", + + "order.description": "بعض الإضافات تعمل بشكل مثالي عندما يتم تفعيلها قبل أو بعد الإضافات الأخرى.", + "order.explanation": "يتم تحميل الإضافات حسب الترتيب المحدد هنا، من الأعلى إلى الأسفل", + + "plugin-item.themes": "القوالب", + "plugin-item.deactivate": "تعطيل", + "plugin-item.activate": "تفعيل", + "plugin-item.install": "تنصيب", + "plugin-item.uninstall": "إلغاء التنصيب", + "plugin-item.settings": "الإعدادات", + "plugin-item.installed": "المنصبة", + "plugin-item.latest": "الأحدث", + "plugin-item.upgrade": "ترقية", + "plugin-item.more-info": "لمزيد من المعلومات:", + "plugin-item.unknown": "غير معروف", + "plugin-item.unknown-explanation": "تعذر تحديد حالة هذه الإضافة، ربما بسبب خطأ في الإعدادات.", + "plugin-item.compatible": "This plugin works on NodeBB %1", + "plugin-item.not-compatible": "This plugin has no compatibility data, make sure it works before installing on your production environment.", + + "alert.enabled": "الإضافة مفعلة", + "alert.disabled": "الإضافة معطلة", + "alert.upgraded": "الإضافة مرقاة", + "alert.installed": "الإضافة منصبة", + "alert.uninstalled": "تم إلغاء تنصيب الإضافة", + "alert.activate-success": "Please rebuild and restart your NodeBB to fully activate this plugin", + "alert.deactivate-success": "تم تعطيل الإضافة بنجاح", + "alert.upgrade-success": "Please rebuild and restart your NodeBB to fully upgrade this plugin.", + "alert.install-success": "تم تثبيت الإضافة بنجاح، يرجى تفعيلها.", + "alert.uninstall-success": "تم تعطيل الإضافة وإلغاء تنصيبها بنجاح.", + "alert.suggest-error": "

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

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

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

              ", + "alert.incompatible": "

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

              ", + "alert.possibly-incompatible": "

              No Compatibility Information Found

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

              In the event that NodeBB cannot boot properly:

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

              Continue installation of latest version of this plugin?

              ", + "alert.reorder": "Plugins Re-ordered", + "alert.reorder-success": "Please rebuild and restart your NodeBB to fully complete the process.", + + "license.title": "معلومات ترخيص الإضافة", + "license.intro": "The plugin %1 is licensed under the %2. Please read and understand the license terms prior to activating this plugin.", + "license.cta": "هل ترغب بالاستمرار في تفعيل هذه الإضافة؟" +} diff --git a/public/language/ar/admin/extend/rewards.json b/public/language/ar/admin/extend/rewards.json new file mode 100644 index 0000000000..df89d441a7 --- /dev/null +++ b/public/language/ar/admin/extend/rewards.json @@ -0,0 +1,15 @@ +{ + "rewards": "Rewards", + "condition-if-users": "If User's", + "condition-is": "Is:", + "condition-then": "Then:", + "max-claims": "Amount of times reward is claimable", + "zero-infinite": "Enter 0 for infinite", + "delete": "Delete", + "enable": "Enable", + "disable": "Disable", + + "alert.delete-success": "Successfully deleted reward", + "alert.no-inputs-found": "Illegal reward - no inputs found!", + "alert.save-success": "Successfully saved rewards" +} \ No newline at end of file diff --git a/public/language/ar/admin/extend/widgets.json b/public/language/ar/admin/extend/widgets.json new file mode 100644 index 0000000000..ab9bfb4cdb --- /dev/null +++ b/public/language/ar/admin/extend/widgets.json @@ -0,0 +1,30 @@ +{ + "available": "Available Widgets", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the widget essentials plugin in the plugins control panel.", + "clone-from": "Clone widgets from", + "containers.available": "Available Containers", + "containers.explanation": "Drag and drop on top of any active widget", + "containers.none": "None", + "container.well": "Well", + "container.jumbotron": "Jumbotron", + "container.panel": "Panel", + "container.panel-header": "Panel Header", + "container.panel-body": "Panel Body", + "container.alert": "Alert", + + "alert.confirm-delete": "Are you sure you wish to delete this widget?", + "alert.updated": "Widgets Updated", + "alert.update-success": "Successfully updated widgets", + "alert.clone-success": "Successfully cloned widgets", + + "error.select-clone": "Please select a page to clone from", + + "title": "Title", + "title.placeholder": "Title (only shown on some containers)", + "container": "Container", + "container.placeholder": "Drag and drop a container or enter HTML here.", + "show-to-groups": "Show to groups", + "hide-from-groups": "Hide from groups", + "hide-on-mobile": "Hide on mobile" +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/admins-mods.json b/public/language/ar/admin/manage/admins-mods.json new file mode 100644 index 0000000000..f9bbc63632 --- /dev/null +++ b/public/language/ar/admin/manage/admins-mods.json @@ -0,0 +1,12 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "moderators": "Moderators", + "no-global-moderators": "No Global Moderators", + "no-sub-categories": "No subcategories", + "subcategories": "%1 subcategories", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/ar/admin/manage/categories.json b/public/language/ar/admin/manage/categories.json new file mode 100644 index 0000000000..9139872a6d --- /dev/null +++ b/public/language/ar/admin/manage/categories.json @@ -0,0 +1,92 @@ +{ + "settings": "اعدادات القسم", + "privileges": "الصلاحيات", + + "name": "Category Name", + "description": "Category Description", + "bg-color": "Background Colour", + "text-color": "Text Colour", + "bg-image-size": "Background Image Size", + "custom-class": "Custom Class", + "num-recent-replies": "# of Recent Replies", + "ext-link": "External Link", + "subcategories-per-page": "Subcategories per page", + "is-section": "Treat this category as a section", + "post-queue": "Post queue", + "tag-whitelist": "Tag Whitelist", + "upload-image": "Upload Image", + "delete-image": "Remove", + "category-image": "Category Image", + "parent-category": "Parent Category", + "optional-parent-category": "(Optional) Parent Category", + "top-level": "Top Level", + "parent-category-none": "(None)", + "copy-parent": "Copy Parent", + "copy-settings": "Copy Settings From", + "optional-clone-settings": "(Optional) Clone Settings From Category", + "clone-children": "Clone Children Categories And Settings", + "purge": "Purge Category", + + "enable": "Enable", + "disable": "Disable", + "edit": "Edit", + "analytics": "Analytics", + "view-category": "View category", + "set-order": "Set order", + "set-order-help": "Setting the order of the category will move this category to that order and update the order of other categories as necessary. Minimum order is 1 which puts the category at the top.", + + "select-category": "Select Category", + "set-parent-category": "Set Parent Category", + + "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.category-selector": "Configuring privileges for ", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Viewing Privileges", + "privileges.section-posting": "Posting Privileges", + "privileges.section-moderation": "Moderation Privileges", + "privileges.section-other": "Other", + "privileges.section-user": "User", + "privileges.search-user": "Add User", + "privileges.no-users": "No user-specific privileges in this category.", + "privileges.section-group": "Group", + "privileges.group-private": "This group is private", + "privileges.inheritance-exception": "This group does not inherit privileges from registered-users group", + "privileges.banned-user-inheritance": "Banned users inherit privileges from banned-users group", + "privileges.search-group": "Add Group", + "privileges.copy-to-children": "Copy to Children", + "privileges.copy-from-category": "Copy from Category", + "privileges.copy-privileges-to-all-categories": "Copy to All Categories", + "privileges.copy-group-privileges-to-children": "Copy this group's privileges to the children of this category.", + "privileges.copy-group-privileges-to-all-categories": "Copy this group's privileges to all categories.", + "privileges.copy-group-privileges-from": "Copy this group's privileges from another category.", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.copy-success": "Privileges copied!", + + "analytics.back": "Back to Categories List", + "analytics.title": "Analytics for \"%1\" category", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", + "analytics.topics-daily": "Figure 3 – Daily topics created in this category", + "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + + "alert.created": "Created", + "alert.create-success": "Category successfully created!", + "alert.none-active": "You have no active categories.", + "alert.create": "Create a Category", + "alert.confirm-purge": "

              Do you really want to purge this category \"%1\"?